# Serial protocol v1

Shared by the Android app (`android/app/src/main/java/com/hmlweb/robotctl/proto/Protocol.java`)
and the firmware (`firmware/uno_robot_controller/uno_robot_controller.ino`, block
`PROTOCOL BEGIN … PROTOCOL END`). The same bytes work over the HC-05 (SoftwareSerial) and over the
Uno's USB port, both at the configured baud (9600 by default).

## Conventions

- Newline-terminated ASCII lines, `\r` ignored, at most 95 characters per line.
- `<i>` is the **logical index** `board*16 + channel` (0..15 on the first PCA9685, 16..31 on the
  second at 0x41).
- Degrees are **absolute servo degrees** 0..180. Inversion, centre offsets and pose-space
  conversion happen in the app.
- The firmware only ever replies to `?` (handshake) and to malformed input (`ERR`). Everything else
  is silent so the 9600 baud link stays free for commands.

## Commands (phone → Uno)

| Command | Effect |
|---|---|
| `M<i>:<deg>[,<i>:<deg>]*` | set target(s). A channel becomes *active* and *driven* on its first `M`; the first move is a jump (real position unknown), later moves slew. |
| `H` | home: every active channel → its centre |
| `R` | relax: PWM off on every driven channel; the next `M` re-drives that channel (jump) |
| `S` | stop / hold: target = current position, freezing a slew |
| `V<deg/s>` | slew speed limit, `V0` = instant |
| `L<i>:<min>:<max>[,…]` | runtime limits (RAM, default 0..180) — targets are clamped to them |
| `C<i>:<deg>[,…]` | runtime centre used by `H` (default 90) |
| `U<i>:<minUs>:<maxUs>[,…]` | pulse range of the servo (default 500..2500 µs, clamped 400..2600) |
| `K<baud>:<rx>:<tx>:<boards>` | store the link settings in EEPROM and apply them at once (HC-05 baud 1200..115200, pins 2..13 and different, boards 1..2) → `OK K`; invalid values → `ERR` |
| `K` | → `OK K <baud> <rx> <tx> <boards>` (current link settings) |
| `?` | → `OK v1 <channels> <boards>` |

Malformed or over-long input → `ERR` (partial tuples before the error are still applied).

## Binary pose frame

For clip streaming the phone sends `FF FE n d0 … d(n-1) ck`:

- `n` = 1..32 channels, `d` = 0..180 or **255 = leave unchanged**, `ck = (Σ d) & 0xFF`.
- 0xFF / 0xFE never occur in ASCII, so frames and lines can be mixed freely.
- Bad length → ignored; bad checksum → frame dropped; a frame stalled > 100 ms → parser resets.
- 16 channels = 20 bytes; at 20 Hz that is 400 B/s of the ~960 B/s a 9600-baud link offers.

## Flashing over USB-OTG (what the app does)

The app carries the compiled reference sketch (`firmware/hex/uno_robot_controller.hex`) and talks
STK500v1 to the Uno's Optiboot bootloader through a USB-OTG serial adapter, exactly like
`avrdude -c arduino`: DTR/RTS low → high (auto-reset), `GET_SYNC` until `INSYNC OK`, `READ_SIGN`
(must be `1E 95 0F` / `1E 95 14` = ATmega328P/328), `ENTER_PROGMODE`, then per 128-byte page
`LOAD_ADDRESS` (word address) + `PROG_PAGE`, a `READ_PAGE` verify pass, `LEAVE_PROGMODE`. Afterwards
it reopens the port at the sketch baud, waits for the bootloader timeout and sends `K…`, `?`, `K` to
store and confirm the link settings. Same thing by hand:

```
avrdude -c arduino -p m328p -P /dev/ttyUSB0 -b 115200 -D -U flash:w:uno_robot_controller.hex:i
```

## Connect sequence used by the app

1. `?` (retried twice, 2 s apart) → `OK v1 16 1`, then `K` → `OK K 9600 10 11 1`
2. `U…`, `L…`, `C…` for every joint of the profile (chunked lines), then `V<profile speed>`
3. Sliders → coalesced `M` lines (≤ 4 channels) or one binary frame (more) every 50 ms, never more
   than one frame in flight.
4. Clip playback → `V0`, binary frames at 20 Hz, then `V<profile speed>` again.
5. Calibration dialog → `L<i>:0:180` + `V0` for that channel, restored when the dialog closes.
