Skip to content
Open
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
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ jobs:
with:
node-version: 22
cache: npm
cache-dependency-path: ui/package-lock.json
cache-dependency-path: |
ui/package-lock.json
packages/tiny-engineer-cli/package-lock.json
- name: Check OLED expression assets
run: |
node scripts/expressions/generate.js --check
Expand All @@ -79,7 +81,9 @@ jobs:
npm run lint --prefix ui
npm test --prefix ui
- name: Unified CLI package tests
run: npm test --prefix packages/tiny-engineer-cli
run: |
npm ci --prefix packages/tiny-engineer-cli
npm test --prefix packages/tiny-engineer-cli
- name: Cursor package tests
run: npm test --prefix packages/tiny-engineer-cursor
- name: Antigravity package tests
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ data/*.*
data/*/
artifacts/

node_modules/

# Cursor hook event log
cursor_events.log

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Format: `type(scope): summary`
| `feat!:` / `fix!:` or footer `BREAKING CHANGE:` | MAJOR |
| `docs`, `style`, `test`, `chore`, `ci`, `refactor` (unless breaking) | no bump |

Scopes: `firmware`, `http`, `settings`, `anim`, `servos`, `wifi`, `integrations`, `cad`, `mods`, `pcb`, `docs`, `scripts`, `ci`, `web`.
Scopes: `firmware`, `http`, `serial`, `settings`, `anim`, `servos`, `wifi`, `integrations`, `cad`, `mods`, `pcb`, `docs`, `scripts`, `ci`, `web`.

`integrations` is anything under `packages/` (Cursor, Antigravity, Claude Code, later agent CLIs). Do not add a new scope per package.

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Pick a path. Depth lives in the linked pages.
| You want to… | Start here |
| --- | --- |
| **Contribute a PCB** (KiCad) | [pcb.md](pcb.md) → [`hardware/`](../hardware/README.md) |
| **HTTP API / settings** | [api.md](api.md); add a setting: [settings.md](settings.md) |
| **HTTP API / settings** | [api.md](api.md); serial CDC: [serial-api.md](serial-api.md); add a setting: [settings.md](settings.md) |
| **CLI setup / REST for any IDE** | [integration.md](integration.md) |
| **Publish npm CLI** (`tiny-engineer`) | [cli-release.md](cli-release.md) |
| **Firmware / package tests** | [testing.md](testing.md) |
Expand Down
69 changes: 51 additions & 18 deletions docs/api.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/flash.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pio run -t upload # flash firmware + LittleFS
pio device monitor # serial (115200)
```

Before upload, a pre-script tries **`POST /servos/park`** over serial ([`scripts/park_servos_before_upload.py`](../scripts/park_servos_before_upload.py)) so joints sit at mid with PWM held — fewer snaps into the printed parts. If the board is cold or CDC is busy, the script warns and upload continues. For absolute safety, unplug servo **5 V / V+** first. Details: [hardware/servos.md](hardware/servos.md#flash-safe-park).

After firmware upload, a post-script also uploads **LittleFS** ([`scripts/upload_fs_after_upload.py`](../scripts/upload_fs_after_upload.py)) so WAV assets (`welcome`, `bell`, and friends) land on the board. If animations move but stay silent, run `pio run -t uploadfs` once.

Several serial ports:
Expand Down
20 changes: 17 additions & 3 deletions docs/hardware/servos.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,29 @@ Verified in [`include/pins.h`](../../include/pins.h) and [`include/servos.h`](..

On every boot (and after each flash reset):

1. **OE** — GP5 drives PCA9685 **OE** (active LOW). Firmware holds OE **HIGH** (outputs off) from early `setup`, then **LOW** after neutral PWM is written (`PCA9685_OE_WIRED = true` in [`include/pins.h`](../../include/pins.h)).
2. **Early init** — PCA9685 is probed and configured immediately after `Wire.begin`, before OLED, settings, or Wi-Fi.
3. **Neutral park** — all channels receive mid-pulse PWM while OE is still HIGH; then OE goes LOW.
1. **OE** — GP5 drives PCA9685 **OE** (active LOW). Firmware holds OE **HIGH** (outputs off) from early `setup`, then **LOW** after PWM is written (`PCA9685_OE_WIRED = true` in [`include/pins.h`](../../include/pins.h)).
2. **Early init** — PCA9685 is probed and configured immediately after `Wire.begin`, before OLED / Wi-Fi (settings already loaded).
3. **Last-pose restore** — NVS key `spose` (namespace `te`) holds the last commanded angles (5× degrees). While OE is still HIGH, firmware writes those pulses (mid fallback if missing), then OE goes LOW. That cuts the common flash snap when the robot was already near the saved pose.
4. **Smooth boot moves** — `centerAllServos()` and sleep-inertia pose use `servoMoveAllSmoothTo()` at `SERVO_BOOT_SPEED_DEG_S` (35°/s), not instant snaps.

### Flash-safe park

Rapid flash/upload can still yank joints if the body drooped after PWM limp (software pose ≠ physical). Mitigations:

| Tool | Role |
| --- | --- |
| `POST /servos/park` | Smooth mid, flush `spose`, keep PWM until reboot |
| PlatformIO `pre:upload` | [`scripts/park_servos_before_upload.py`](../../scripts/park_servos_before_upload.py) calls park over serial; warns and continues if the robot is offline |
| Setting `servo_hold` | Skip idle/sleep PWM full-off so the pose does not limp between flashes |
| `welcome=0` | Quieter post-boot motion while iterating (optional) |
| Unplug servo **5 V / V+** | Absolute safety when pose is unknown |

## Idle and sleep PWM release

Analog servos hunt while a 50 Hz pulse is present. Firmware does **not** use OE for this: after animation `none` parks (awake rest or device-sleep chin-down) and every joint is still for `SERVO_PWM_RELEASE_DELAY_MS` (**2000** ms), it writes PCA9685 full-off (`setPWM(channel, 0, 4096)`) on channels 0–4. SIG stays low; the servo goes **limp** (no holding torque). Head pitch can droop; hands-down usually rests on the desk.

Skipped when `servo_hold` is on, when `POST /servos/park` armed a until-reboot hold, or in setup AP.

The next animation, `/test/servo`, or `/setup/servo` rewrite the last commanded pulse before moving. Leaving `none` restores all five channels at their last angles first.

**Setup AP** (`wifiProvisioningMode`) never releases PWM, so “Move all to 90°” / horn install still holds.
Expand Down
17 changes: 9 additions & 8 deletions docs/hardware/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Stock flash: **[Web flash](https://jamro.github.io/tiny-engineer/flash/)**. Buil
| --- | --- |
| Built-in WS2812 | Green ready (GPIO10) |
| I2C init | `Wire.begin` on GPIO0/GPIO1 |
| PCA9685 | Probe `0x40` early; OE HIGH during init, park neutral, then OE LOW (`PCA9685_OE_WIRED`) |
| PCA9685 | Probe `0x40` early; OE HIGH during init, restore last pose (`spose`) or mid, then OE LOW (`PCA9685_OE_WIRED`) |
| OLED | Probe `0x3C`, init (optional) |
| Wi-Fi | STA connect from saved NVS credentials, or setup AP `TinyEngineer-XXXX` when unset/failed; mDNS `{hostname}.local` after STA connect |
| MAX98357A / I2S | `I2S.begin` 22.05 kHz 16-bit stereo |
Expand Down Expand Up @@ -106,15 +106,15 @@ curl -X POST "http://tiny-engineer.local/test/servo?index=0&angle=90"
| Method | Path | Body |
| --- | --- | --- |
| `GET` | `/` | HTML endpoint index |
| `GET` | `/auth` | Auth status (`ok`, `required`, `wifi_configured`, `provisioning`) — always public |
| `GET` | `/auth` | Auth status (`ok`, `required`, `wifi_configured`, `wifi_enabled`, `provisioning`) — always public |
| `GET` | `/health` | Health JSON — full field list in [api.md](../api.md#get-health) |
| `GET` / `POST` | `/anim` | Current animation / start one (`name`, optional `interrupt`) — full params in [api.md](../api.md#post-anim) |
| `POST` | `/play` | Stream a WAV while `talking` (or `/play/{name}`) runs; answers when the clip ends — full params in [api.md](../api.md#post-play) |
| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `continuous_timeout`, `loading`, `eyes_style`, `access_token_set`, `wifi_configured`, `servo_mins`, `servo_maxs`, `rgb_order`, `oled_rotate_180`, `wifi_networks`) |
| `POST` | `/settings?...` | Update NVS settings; `reboot_required` if hostname changed |
| `POST` | `/settings?...&servo_mins=&servo_maxs=` | Servo min/max comma lists; setup-AP-only |
| `POST` | `/settings?...&rgb_order=` | WS2812 byte order (`RGB`/`RBG`/`GRB`/`GBR`/`BRG`/`BGR`); setup-AP-only; default `GRB` |
| `POST` | `/settings?...&oled_rotate_180=` | OLED 180° rotation (`0`/`1`); setup-AP-only; default `0` |
| `GET` | `/settings` | Persistent settings (`sleep_timeout`, `hostname`, `volume`, `welcome`, `serial_log`, `servo_hold`, `wifi_enabled`, `continuous_timeout`, `loading`, `eyes_style`, `access_token_set`, `wifi_configured`, `servo_mins`, `servo_maxs`, `rgb_order`, `oled_rotate_180`, `wifi_networks`) |
| `POST` | `/settings?...` | Update NVS settings; `reboot_required` if hostname or `wifi_enabled` changed vs boot |
| `POST` | `/settings?...&servo_mins=&servo_maxs=` | Servo min/max comma lists; setup-AP or serial-only boot |
| `POST` | `/settings?...&rgb_order=` | WS2812 byte order (`RGB`/`RBG`/`GRB`/`GBR`/`BRG`/`BGR`); setup-AP or serial-only; default `GRB` |
| `POST` | `/settings?...&oled_rotate_180=` | OLED 180° rotation (`0`/`1`); setup-AP or serial-only; default `0` |
| `PUT` | `/settings/wifi` | Replace the saved WiFi networks (JSON array of 1 to 5); first one tested in setup AP mode |
| `POST` | `/settings/reset` | Factory reset settings to defaults (clears WiFi, keeps servo ranges, RGB mapping, and screen rotation); power-cycle to reopen setup AP |
| `POST` | `/test/audio` | `{"ok":true,"test":"audio"}` after `runSoundTest()` |
Expand All @@ -127,8 +127,9 @@ curl -X POST "http://tiny-engineer.local/test/servo?index=0&angle=90"
| `POST` | `/setup/led?color=R` / `G` / `B` or `?byte=0` / `1` / `2` / `off` | Setup AP only; light logical RGB or one WS2812 wire byte, or release hold |
| `POST` | `/setup/audio` | Setup AP only; play `welcome.wav` (`playWelcome()`) |
| `POST` | `/setup/oled?rotate_180=0` / `1` | Setup AP only; preview OLED 180° rotation (`THIS WAY UP`); omit param to restore provisioning text |
| `POST` | `/servos/park` | Smooth mid park, persist `spose`, hold PWM until reboot (no Wi-Fi gate) |

GET on a test path returns `405`. Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, or `/settings` params return `400`. Missing/wrong Bearer when auth enabled returns `401`. Control APIs return `503` when WiFi credentials are not saved (`/setup/servo`, `/setup/led`, `/setup/audio`, and `/setup/oled` stay available). Unknown path returns `404`. JSON `Content-Type`. Handlers block until the test finishes; the OLED returns to `ROBOT READY` after.
GET on a test path returns `405`. Bad `/test/servo`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, or `/settings` params return `400`. Missing/wrong Bearer when auth enabled returns `401`. Control APIs return `503` when WiFi credentials are not saved (`/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`, and `/servos/park` stay available). Unknown path returns `404`. JSON `Content-Type`. Handlers block until the test finishes; the OLED returns to `ROBOT READY` after.

HTTP runs on STA when connected, or on setup AP at `192.168.4.1` during provisioning.

Expand Down
27 changes: 17 additions & 10 deletions docs/integration.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Integrating Tiny Engineer

Tiny Engineer is a Wi-Fi desk robot. Prefer the unified **`tiny-engineer` CLI** for Cursor, Claude Code, and Antigravity hooks. For custom agents, other IDEs, or scripts, call the **REST API** directly.
Tiny Engineer is a Wi-Fi desk robot. Prefer the unified **`tiny-engineer` CLI** for Cursor, Claude Code, and Antigravity hooks. For custom agents, other IDEs, or scripts, call the **REST API** directly (or the [serial CDC API](serial-api.md)).

Robot must be on the same network. Base URL: `http://tiny-engineer.local` (or the IP shown on the OLED). Full HTTP reference: [`api.md`](api.md).
Robot must be on the same network for HTTP. Base URL: `http://tiny-engineer.local` (or the IP shown on the OLED). Full HTTP reference: [`api.md`](api.md). USB serial is an always-on extra transport for the same JSON control ops.

| Path | Best for | How |
|---|---|---|
| **`tiny-engineer` CLI** (default) | Cursor, Claude Code, Antigravity hooks | `tiny-engineer setup` → `hook <ide>` → `POST /anim` |
| **`tiny-engineer` CLI** (default) | Cursor, Claude Code, Antigravity hooks | `tiny-engineer setup` → `hook <ide>` → `/anim` (HTTP or serial) |
| **REST API** (advanced) | Custom agents, other IDEs, CI, one-off scripts | `POST /anim?name=…` ([`api.md`](api.md)) |
| **Serial API** | USB cable, flaky mDNS, bench | NDJSON over CDC ([`serial-api.md`](serial-api.md)); CLI `--serial` / `TINY_ENGINEER_SERIAL` |

```mermaid
flowchart TB
Expand All @@ -19,12 +20,14 @@ flowchart TB
end
subgraph advanced [Advanced / custom]
Script[Any script or IDE] --> Rest["POST /anim REST"]
Script --> Serial["serial NDJSON"]
end
Anim --> Robot[Tiny Engineer on Wi-Fi]
Anim --> Robot[Tiny Engineer]
Rest --> Robot
Serial --> Robot
```

Prerequisites: flash firmware, join 2.4 GHz Wi-Fi, confirm `http://tiny-engineer.local/health` (or the OLED IP) responds.
Prerequisites: flash firmware, join 2.4 GHz Wi-Fi, confirm `http://tiny-engineer.local/health` (or the OLED IP) responds. Serial works whenever CDC is up (including setup AP), but Wi-Fi credentials are still required for `/anim` and `/test/*`.

---

Expand All @@ -48,8 +51,10 @@ npx -y tiny-engineer doctor
```

- Cold `npx` can be slow: `setup` writes hook **`timeout` ≥ 30**.
- Optional: `--url http://192.168.x.x` (default `TINY_ENGINEER_URL`, else `http://tiny-engineer.local`).
- Auth: if the device has an `access_token`, set `TINY_ENGINEER_TOKEN` in the process env or a project-root `.env`. The CLI sends `Authorization: Bearer …`.
- Transport: interactive `setup` asks **HTTP (Wi-Fi)** vs **Serial (USB)** and writes one of `TINY_ENGINEER_URL` or `TINY_ENGINEER_SERIAL` into `.env` (clears the other). Non-interactive: `--url http://192.168.x.x` or `--serial` / `--serial=auto` / `--serial <path>` (not both). Runtime flags override the same way. List paths with `tiny-engineer ports`. Protocol: [`serial-api.md`](serial-api.md).
- Auto-detect caches the last good port under `~/.cache/tiny-engineer/`; cache clears when the port stops responding.
- `play` uses the same transport as `anim` (HTTP or serial). Serial `/play` sends a JSON begin line with `len` then raw WAV bytes — see [`serial-api.md`](serial-api.md).
- Auth (HTTP): if the device has an `access_token`, set `TINY_ENGINEER_TOKEN` in the process env or a project-root `.env`. The CLI sends `Authorization: Bearer …`. Serial skips Bearer (trusted USB).
- Prefer registry `npx` over `npx github:…` (SSH often fails in IDE hooks).

**This firmware repo (local CLI development):**
Expand All @@ -62,14 +67,15 @@ node packages/tiny-engineer-cli/bin/tiny-engineer.js doctor
### Setup wizard

```bash
npx -y tiny-engineer setup # interactive: pick IDEs
npx -y tiny-engineer setup # interactive: pick IDEs + HTTP vs serial
npx -y tiny-engineer setup cursor --yes
npx -y tiny-engineer setup --all --yes --url http://192.168.1.10
npx -y tiny-engineer setup cursor --yes --serial=auto
npx -y tiny-engineer doctor
npx -y tiny-engineer uninstall cursor --yes # remove only Tiny Engineer hook entries
```

`setup` merges hook commands into the project (Cursor `.cursor/hooks.json`, Claude Code `.claude/settings.json`, Antigravity `.agents/hooks.json`) using `npx -y tiny-engineer hook <ide>`, and can write `TINY_ENGINEER_URL` into `.env`. It does not delete foreign hooks.
`setup` merges hook commands into the project (Cursor `.cursor/hooks.json`, Claude Code `.claude/settings.json`, Antigravity `.agents/hooks.json`) using `npx -y tiny-engineer hook <ide>`, and writes **either** `TINY_ENGINEER_URL` or `TINY_ENGINEER_SERIAL` into `.env` (exclusive). It does not delete foreign hooks.

This firmware repo already ships configs pointing at the **local** CLI bin for developing the package.

Expand All @@ -87,9 +93,10 @@ Cursor deep dive (event table, sample `hooks.json`): [`hooks.md`](hooks.md).
tiny-engineer anim ring
tiny-engineer play /tmp/clip.wav
tiny-engineer play /tmp/clip.wav --name thinking
tiny-engineer play /tmp/clip.wav --serial=auto
```

WAV for `/play`: 16-bit mono PCM at 22050 Hz. Details: [`api.md#post-play`](api.md#post-play).
WAV for `/play`: 16-bit mono PCM at 22050 Hz. Details: [`api.md#post-play`](api.md#post-play). Serial framing: [`serial-api.md`](serial-api.md).

### Speaking through the robot (skill)

Expand Down
Loading
Loading