# Quacky Field Protocol v1 (QFP/1)

꽥꽥이 로봇(A·B·C·D)과 PC·브라우저가 주고받는 **표준 통신 규격**입니다.
같은 JSON 메시지를 HTTP, WebSocket, UDP 세 가지 방법으로 보낼 수 있습니다.

- PC 구현: `fieldlab/protocol.py` (클라이언트 3종), `fieldlab/mockrobot.py` (가짜 로봇)
- 펌웨어 참조 구현: `firmware/quacky_bridge/quacky_bridge.ino` (ESP32-C3, Arduino)

## 1. 로봇 찾기

| 상황 | 주소 |
|---|---|
| 교실 공유기에 접속(STA) | mDNS `quacky-a.local` ~ `quacky-d.local` (또는 공유기가 준 IP) |
| 공유기가 없을 때(AP 모드) | 로봇이 Wi-Fi `QUACKY-A` ~ `QUACKY-D` 를 엽니다. PC 를 거기에 연결하면 로봇 주소 `192.168.4.1` |

로봇은 켜질 때 공유기 접속을 10초 동안 시도하고, 실패하면 AP 모드로 바뀝니다.
Windows 에서 `.local` 이름이 안 되면 IP 주소를 직접 쓰세요(로봇 시리얼 출력 또는 공유기 관리 화면).

## 2. 전송 방법(내용은 셋 다 같음)

| 전송 | 주소 | 설명 |
|---|---|---|
| HTTP | `POST http://<ip>/api/cmd` | 본문 = 명령 JSON, 응답 = 상태 JSON (오류면 HTTP 400) |
| | `GET http://<ip>/api/status` | 지금 상태 |
| | `GET http://<ip>/api/ping` | 살아 있는지 확인 `{"proto":1,"robot":"A","ok":true,"fw":"..."}` |
| WebSocket | `ws://<ip>:81/ws` | 명령을 보내면 응답이 오고, 그 밖에 **0.5초마다 상태가 push** 됨 |
| UDP | `<ip>:4210` | 데이터그램 1개 = 명령 1개, 응답은 **보낸 포트로** 돌아옴 |

## 3. 명령(PC → 로봇)

```json
{"proto":1, "robot":"A", "seq":12, "cmd":"forward", "steps":1}
{"proto":1, "robot":"A", "seq":13, "cmd":"drive", "lin":0.08, "ang":0.3, "ttl_ms":300}
{"proto":1, "robot":"A", "seq":14, "cmd":"stop"}
```

| 필드 | 형식 | 뜻 |
|---|---|---|
| `proto` | 정수 | 항상 `1` |
| `robot` | 문자 | 받을 로봇 이름 `A`~`D` (`"*"` 는 아무 로봇) — 다르면 오류 `wrong robot` |
| `seq` | 0 이상 정수 | 보낼 때마다 1씩 늘리는 번호. 응답의 `ack` 로 돌아옴 |
| `cmd` | 문자 | `forward` `back` `left` `right` `wait` `stop` `drive` |
| `steps` | 1~4 | 걸음 명령의 걸음 수 (기본 1) |
| `lin` | m/s | drive 전진 속도, ±0.12 로 잘림 |
| `ang` | rad/s | drive 회전 속도(왼쪽 +), ±0.8 로 잘림 |
| `ttl_ms` | 50~2000 | drive 유효 시간(기본 300). 이 안에 새 명령이 없으면 멈춤 |

걸음 1번: 전진 0.06 m / 후진 0.04 m / 회전 0.35 rad / 대기, 각각 약 0.5초(실측 전 가정값).

## 4. 응답·상태(로봇 → PC)

```json
{"proto":1, "robot":"A", "ack":12, "ok":true, "busy":"forward", "queue":0,
 "battery_v":7.4, "rssi":-55, "fw":"quacky-bridge 1.0"}
{"proto":1, "robot":"A", "ack":15, "ok":false, "error":"queue full"}
```

| 필드 | 뜻 |
|---|---|
| `ack` | 이 응답이 대답하는 명령의 `seq` (상태 push·`/api/status` 는 마지막으로 실행한 seq) |
| `ok` | 성공 여부. 실패면 `error` 만 있음 |
| `busy` | 지금 하는 일 `forward`…`drive`, 쉬면 `null` |
| `queue` | 기다리는 걸음 수 |
| `battery_v`, `rssi`, `fw` | 배터리 전압(V), Wi-Fi 세기(dBm), 펌웨어 판 |
| `ignored` | (UDP) 실행하지 않은 명령: `duplicate`(같은 seq) / `stale`(더 작은 seq) |

오류 문자열: `bad json`, `bad proto`, `wrong robot`, `bad seq`, `bad cmd`, `bad steps`, `bad ttl_ms`, `bad drive args`, `queue full`, `not found`.

## 5. 안전 규칙

1. **워치독**: drive 는 `ttl_ms` 안에 새 명령이 없으면 로봇이 스스로 멈춥니다. 계속 움직이려면 **0.1초마다 다시 보내세요**
   (`protocol.py` 의 `drive_for()` 가 해 줍니다). PC 프로그램이 멈추거나 Wi-Fi 가 끊겨도 로봇은 0.3초 안에 섭니다.
2. **stop** 은 큐를 비우고 즉시 멈춥니다. 위험하면 언제든 stop.
3. **걸음 큐**: 걸음 명령은 최대 4개까지 쌓여 차례로 실행됩니다. 넘치면 `queue full` 오류이고 그 명령은 버려집니다.
   drive 를 보내면 큐가 비워지고, drive 중에 걸음 명령을 보내면 drive 를 멈추고 걸음을 시작합니다.
4. **UDP 순서**: UDP 는 순서가 바뀌거나 두 번 도착할 수 있습니다. 로봇은 보낸 곳(ip:port)마다 마지막 seq 를 기억하고,
   같거나 더 작은 seq 는 실행하지 않습니다. 응답을 못 받으면 **같은 seq 로** 다시 보내면 안전합니다.
   프로그램을 다시 켜면 포트가 바뀌므로 seq 를 1부터 다시 시작해도 됩니다.

## 6. 예시

가짜 로봇으로 먼저 시험하세요(실물 없이 똑같이 동작):
```
run.bat mock-robot --name A --http 8080 --ws 8081 --udp 4210
```

### curl (명령 프롬프트)
```
curl http://127.0.0.1:8080/api/ping
curl -X POST http://127.0.0.1:8080/api/cmd -H "Content-Type: application/json" -d "{\"proto\":1,\"robot\":\"A\",\"seq\":1,\"cmd\":\"forward\",\"steps\":2}"
curl http://127.0.0.1:8080/api/status
```
실물이면 `127.0.0.1:8080` 대신 `quacky-a.local` (포트 80).

### Python (fieldlab)
```python
from fieldlab import protocol as P
r = P.connect("http://quacky-a.local", "A")      # 또는 "ws://quacky-a.local:81/ws", "udp://quacky-a.local:4210"
print(r.ping())
print(r.forward(steps=2))                          # {'ok': True, 'busy': 'forward', 'queue': 1, ...}
r.drive_for(0.08, 0.0, seconds=2.0)                # 2초 동안 0.1초마다 재전송, 끝나면 stop
r.stop()

from fieldlab import robot
bot = robot("udp://quacky-a.local:4210", name="A") # 시뮬레이터와 같은 명령: bot.forward(), bot.act(decide(obs))
```
`run.bat run my_policy.py --group 2 --robot A --real http://quacky-a.local --cam 0` 은 이 규격으로 로봇을 움직입니다.

### 브라우저 JavaScript (WebSocket)
```js
const ws = new WebSocket('ws://quacky-a.local:81/ws');
let seq = 1, timer = null;
ws.onmessage = e => { const m = JSON.parse(e.data); console.log(m.ack, m.busy, m.battery_v); };
ws.onopen = () => {
  ws.send(JSON.stringify({proto: 1, robot: 'A', seq: seq++, cmd: 'forward', steps: 1}));
  // 2초 동안 drive: 0.1초마다 다시 보내 워치독 유지
  timer = setInterval(() => ws.send(JSON.stringify({proto: 1, robot: 'A', seq: seq++, cmd: 'drive', lin: 0.08, ang: 0, ttl_ms: 300})), 100);
  setTimeout(() => { clearInterval(timer); ws.send(JSON.stringify({proto: 1, robot: 'A', seq: seq++, cmd: 'stop'})); }, 2000);
};
```
주의: https 로 열린 페이지에서는 브라우저가 `ws://` 연결을 막습니다(혼합 콘텐츠). 로컬 파일이나 http 페이지에서 시험하세요.

## 7. 펌웨어 연결 방법

`firmware/quacky_bridge/quacky_bridge.ino` (ESP32-C3, Arduino):

1. 라이브러리 설치: **WebSockets**(Markus Sattler, links2004), **ArduinoJson 7.x**. 보드 `ESP32C3 Dev Module`.
2. 맨 위 설정을 로봇마다 바꿉니다: `ROBOT_NAME`("A"~"D"), `WIFI_SSID`, `WIFI_PASS`, 배터리 핀 `BAT_PIN`.
3. 걸음 훅 4개를 기존 꽥꽥이 걸음 코드에 연결합니다.

| 훅 | 언제 | 할 일 |
|---|---|---|
| `doStep(cmd)` | 걸음 하나 시작 | `forward/back/left/right/wait` 동작을 **시작만** (막지 말 것) |
| `stepUpdate(ms)` | loop 마다 | 걸음을 한 칸 진행, 끝났으면 `true` (기본: 500 ms 지나면 끝) |
| `doDrive(lin, ang)` | drive 받을 때마다 | 연속 걸음 속도 설정 (0.1초마다 같은 값으로 다시 불릴 수 있음) |
| `doStop()` | stop·워치독·drive→걸음 전환 | 즉시 멈추고 선 자세 |

`delay()` 로 막는 걸음 코드를 그대로 넣으면 그동안 통신이 멈추고 워치독이 늦게 동작합니다.
서보 보간을 `stepUpdate()` 에서 조금씩 진행하는 상태 기계로 바꾸는 것을 권장합니다.
4. 업로드 후 시리얼(115200)에 `STA classroom IP …` 또는 `AP QUACKY-A IP 192.168.4.1` 이 나오면 성공입니다.
   `curl http://quacky-a.local/api/ping` 으로 확인하세요.
