Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "display_control.h"
#include "led_control.h"
#include "protocol.h"
#include "robot_bridge.h"
#include "servo_control.h"
#include "vad.h"

Expand Down Expand Up @@ -124,6 +125,7 @@ void setup() {
Serial.begin(SERIAL_BAUD_RATE);
Serial.setTimeout(0);
delay(500); // Wait for serial stabilization
robot_bridge_init();

// Initialize LED -> indicate connecting (red)
led_init();
Expand Down Expand Up @@ -272,5 +274,6 @@ void loop() {
led_update_pattern(); // LED animation pattern
display_update(); // OLED face animation + blink (currently placeholder)
servo_update(); // Servo async action (rotate/wiggle) step processing
robot_bridge_update();
delay(1); // Feed watchdog + yield CPU
}
19 changes: 17 additions & 2 deletions arduino/atom_echo_m5stack_esp32_ino/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,24 @@ extern const uint16_t SERVER_PORT;
#define DISPLAY_HEIGHT 64
#define DISPLAY_I2C_ADDR 0x3C

// Companion robot bridge (Phase 1 scaffold)
// Disabled by default so the legacy direct-servo path keeps working.
// When enabled, the Grove pins are reassigned as UART TX/RX for a companion
// controller and local servo outputs are disabled.
#define ROBOT_BRIDGE_ENABLED 0
#define ROBOT_BRIDGE_UART_PORT 1
#define ROBOT_BRIDGE_BAUD 115200
#define ROBOT_BRIDGE_TX_PIN 26
#define ROBOT_BRIDGE_RX_PIN 32

// Servo motors — Grove HY2.0-4P port
#define SERVO_PIN_PITCH 26 // Servo #1: nod (up/down)
#define SERVO_PIN_TILT 32 // Servo #2: tilt (left/right)
#if ROBOT_BRIDGE_ENABLED
#define SERVO_PIN_PITCH -1
#define SERVO_PIN_TILT -1
#else
#define SERVO_PIN_PITCH 26 // Servo #1: nod (up/down)
#define SERVO_PIN_TILT 32 // Servo #2: tilt (left/right)
#endif
#define SERVO_MIN_ANGLE 0
#define SERVO_MAX_ANGLE 180
#define SERVO_CENTER_ANGLE 90
Expand Down
22 changes: 22 additions & 0 deletions arduino/atom_echo_m5stack_esp32_ino/protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "connection.h"
#include "config.h"
#include "led_control.h"
#include "robot_bridge.h"
#include "servo_control.h"
#include "display_control.h"
#include <M5Unified.h>
Expand Down Expand Up @@ -365,6 +366,27 @@ static void handleCmdJson(const uint8_t* payload, uint16_t len) {
return;
}

// ── ROBOT_STATE: companion-forward first, local fallback second ──
if (has_action && strcmp(action, "ROBOT_STATE") == 0) {
if (robot_bridge_ready()) {
robot_bridge_forward_json(json);
}

char face[32] = {0};
char display_text[32] = {0};
json_get_string(json, "face", face, sizeof(face));
json_get_string(json, "display_text", display_text, sizeof(display_text));

display_show_face(face_from_string(face));
if (display_text[0]) display_set_status_text(display_text);

ServoAction sa = action_from_string(servo_action);
if (sa != ACTION_NONE) servo_play_action(sa);

led_show_emotion(emotion);
return;
}

// ── ROBOT_EMOTION: display face + servo action + LED ──
if (has_action && strcmp(action, "ROBOT_EMOTION") == 0) {
char face[32] = {0};
Expand Down
55 changes: 55 additions & 0 deletions arduino/atom_echo_m5stack_esp32_ino/robot_bridge.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "robot_bridge.h"
#include "config.h"

#if ROBOT_BRIDGE_ENABLED

#include <HardwareSerial.h>
#include <string.h>

static HardwareSerial robot_bridge_serial(ROBOT_BRIDGE_UART_PORT);
static bool robot_bridge_started = false;

void robot_bridge_init() {
robot_bridge_serial.begin(
ROBOT_BRIDGE_BAUD,
SERIAL_8N1,
ROBOT_BRIDGE_RX_PIN,
ROBOT_BRIDGE_TX_PIN);
robot_bridge_started = true;
}

void robot_bridge_update() {
while (robot_bridge_serial.available() > 0) {
(void)robot_bridge_serial.read();
}
}

bool robot_bridge_ready() {
return robot_bridge_started;
}

bool robot_bridge_forward_json(const char* json) {
if (!robot_bridge_started || !json || !*json) return false;

size_t wanted = strlen(json);
size_t written = robot_bridge_serial.write((const uint8_t*)json, wanted);
if (written != wanted) return false;
return robot_bridge_serial.write('\n') == 1;
}

#else

void robot_bridge_init() {}

void robot_bridge_update() {}

bool robot_bridge_ready() {
return false;
}

bool robot_bridge_forward_json(const char* json) {
(void)json;
return false;
}

#endif
11 changes: 11 additions & 0 deletions arduino/atom_echo_m5stack_esp32_ino/robot_bridge.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef ROBOT_BRIDGE_H
#define ROBOT_BRIDGE_H

#include <Arduino.h>

void robot_bridge_init();
void robot_bridge_update();
bool robot_bridge_ready();
bool robot_bridge_forward_json(const char* json);

#endif
19 changes: 15 additions & 4 deletions arduino/atom_echo_m5stack_esp32_ino/servo_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ struct ServoState {
static Servo servos[2];
static ServoState servo_states[2];

static bool servo_hw_enabled() {
return SERVO_PIN_PITCH >= 0 && SERVO_PIN_TILT >= 0;
}

static int clamp_angle(int angle) {
if (angle < SERVO_MIN_ANGLE) return SERVO_MIN_ANGLE;
if (angle > SERVO_MAX_ANGLE) return SERVO_MAX_ANGLE;
Expand Down Expand Up @@ -72,19 +76,22 @@ static const ActionStep action_wiggle[] = {
};

void servo_init() {
servo_states[0] = {SERVO_CENTER_ANGLE, 0, 0, nullptr, false};
servo_states[1] = {SERVO_CENTER_ANGLE, 0, 0, nullptr, false};

if (!servo_hw_enabled()) return;

servos[0].setPeriodHertz(50);
servos[0].attach(SERVO_PIN_PITCH, 500, 2400);
servos[1].setPeriodHertz(50);
servos[1].attach(SERVO_PIN_TILT, 500, 2400);

servo_states[0] = {SERVO_CENTER_ANGLE, 0, 0, nullptr, false};
servo_states[1] = {SERVO_CENTER_ANGLE, 0, 0, nullptr, false};


servos[0].write(SERVO_CENTER_ANGLE);
servos[1].write(SERVO_CENTER_ANGLE);
}

void servo_set_angle(int servo_idx, int angle) {
if (!servo_hw_enabled()) return;
if (servo_idx < 0 || servo_idx > 1) return;
angle = clamp_angle(angle);
servos[servo_idx].write(angle);
Expand All @@ -96,6 +103,7 @@ void servo_set_angle(int angle) {
}

void servo_play_action(ServoAction action) {
if (!servo_hw_enabled()) return;
const ActionStep* steps = nullptr;

switch (action) {
Expand All @@ -120,6 +128,7 @@ void servo_play_action(ServoAction action) {
}

void servo_stop() {
if (!servo_hw_enabled()) return;
servo_states[0].busy = false;
servo_states[1].busy = false;
servo_states[0].current_action = nullptr;
Expand All @@ -129,6 +138,7 @@ void servo_stop() {
}

void servo_update() {
if (!servo_hw_enabled()) return;
if (!servo_states[0].busy || !servo_states[0].current_action) return;

unsigned long now = millis();
Expand All @@ -149,6 +159,7 @@ void servo_update() {
}

bool servo_is_busy() {
if (!servo_hw_enabled()) return false;
return servo_states[0].busy || servo_states[1].busy;
}

Expand Down
40 changes: 40 additions & 0 deletions arduino/robot_companion_controller/robot_companion_controller.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include <Arduino.h>

#if defined(ESP32)
HardwareSerial BridgeSerial(1);
static constexpr int BRIDGE_RX_PIN = 16;
static constexpr int BRIDGE_TX_PIN = 17;
static constexpr uint32_t BRIDGE_BAUD = 115200;
#else
#error "robot_companion_controller currently targets ESP32-class companion boards."
#endif

static String incoming_line;
static String last_robot_state;

void setup() {
Serial.begin(115200);
BridgeSerial.begin(BRIDGE_BAUD, SERIAL_8N1, BRIDGE_RX_PIN, BRIDGE_TX_PIN);
Serial.println("[robot-companion] bridge ready");
}

static void handle_robot_line(const String& line) {
last_robot_state = line;
Serial.print("[robot-companion] state: ");
Serial.println(last_robot_state);
}

void loop() {
while (BridgeSerial.available() > 0) {
char c = static_cast<char>(BridgeSerial.read());
if (c == '\r') continue;
if (c == '\n') {
if (incoming_line.length() > 0) {
handle_robot_line(incoming_line);
incoming_line = "";
}
continue;
}
incoming_line += c;
}
}
21 changes: 21 additions & 0 deletions docs/AGENT_FEATURE_PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
| SA-5 Voice ID | 화자 등록/식별/게이트 | Voice ID 기능 안정화 + 테스트 |
| SA-6 Channel Expansion | Telegram 기반 iOS 채널 | Bot 연동 MVP + 운영 가이드 |
| SA-7 Dev Productivity | 도구 PoC/자동화 | Ralph PoC 리포트 |
| SA-8 Robot Mode | Companion 기반 로봇 모드 | 로봇 PRD, bridge protocol, companion firmware, 하드웨어 가이드 |

---

Expand Down Expand Up @@ -123,6 +124,18 @@
- `docker compose -f docker/docker-compose.test.yml run --rm server-test pytest server/tests/test_runtime_controller.py server/tests/test_runtime_preferences.py`
- `docker compose -f docker/docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from server-test`

### EPIC-J: Robot Mode Re-Architecture (PRD 5.6)
- [ ] (SA-0, TODO) Atom Echo/SG90/Waveshare 공식 문서 기반 제약과 권장 아키텍처를 `docs/PRD.md` 및 `docs/ROBOT_MODE_WAVESHARE_PRD.md`에 고정
- [ ] (SA-2/SA-8, TODO) `server -> Atom Echo -> Companion` 로봇 상태 페이로드와 회귀 시나리오를 정의하고 protocol test에 편입
- [ ] (SA-8, TODO) `arduino/robot_companion_controller/` 신규 펌웨어 스켈레톤과 ST7789V2 + SG90(1~4채널) 드라이버 구조를 확정
- [ ] (SA-5/SA-8, TODO) `SOUL + RELATION + MOOD + AFFECT + BODY` 기반 감정 지속 모델과 idle action policy를 설계/테스트
- [ ] (SA-8, TODO) farewell/sleep/idle gesture preset과 display state machine(blink/gaze/yawn/talk overlay)을 구현
- [ ] (SA-1/SA-2/SA-8, TODO) Docker 검증 명령과 실기 하드웨어 smoke checklist를 단일 실행 계획으로 문서화
- 검증:
- `docker compose -f docker/docker-compose.test.yml run --rm server-test pytest server/tests/test_emotion_system.py server/tests/test_robot_mode_extended.py`
- `docker compose -f docker/docker-compose.test.yml run --rm server-test pytest server/tests/test_protocol.py`
- `docker compose -f docker/docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from server-test`

---

## 4) 실행 순서 (권장 스프린트)
Expand Down Expand Up @@ -151,6 +164,13 @@
- Telegram 채널 E2E smoke 통과
- 도구 도입 의사결정 리포트 완료

### Sprint 5 (Robot Mode)
- SA-0, SA-2, SA-5, SA-8 수행: Companion 구조 확정 + 감정 지속 엔진 + 디스플레이/서보 제어
- Exit Criteria:
- Atom Echo 단독 직결 대신 Companion 기반 배선/프로토콜이 문서와 설정에 반영됨
- insult -> apology lingering mood 시나리오가 단위 테스트로 재현됨
- ST7789V2 display state machine과 1~4채널 servo profile이 하드웨어 smoke checklist를 통과함

---

## 5) 공통 Definition of Ready / Done
Expand All @@ -172,3 +192,4 @@
- 외부 API 변동: mock-services와 표준 오류코드로 회귀 안정성 확보
- 채널 확장 복잡도: Telegram MVP로 범위 제한 후 iOS 앱은 인터페이스 추상화 우선
- 도구 도입 리스크: PoC 결과 기반 점진 적용, 런타임 경로 직접 치환 금지
- 로봇 하드웨어 제약: Atom Echo 단독 핀/전원 한계를 전제로 Companion 구조를 조기에 확정하고, direct path는 레거시 fallback으로만 유지
Loading
Loading