Compare commits

...
4 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 4.8 2e2f6c846c docs(uno-q): shaker-campaign findings — position-target DVF, safe settings, sensor placement
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-27 19:53:49 +02:00
Omar SobhandClaude Opus 4.8 0a50476bb5 docs(uno-q): oscillator + DVF commands, gain findings, demo script
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-27 18:28:06 +02:00
Omar SobhandClaude Opus 4.8 139f2b25e5 docs(uno-q): actuator run-book (bring-up, calibration, restart rule)
ACTUATOR.md: hardware (TB6600 PUL=D4/DIR=D5), the stepper/stepper_status agent
tools + relay commands, the calibrated 0..9635 envelope, the safe daily bring-up,
the operating rules (park+zero before restart — a restart resets the MCU zero
while the envelope survives), mismatch recovery, recalibration, and the zigbuild
rebuild recipe. Linked from ONBOARDING.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-27 17:07:47 +02:00
Omar SobhandClaude Opus 4.8 b0f47ec271 feat(uno-q): allow the stepper + stepper_status agent tools in the onboard config
Adds 'stepper' and 'stepper_status' to the risk profile allowed_tools +
auto_approve so the cloud agent can drive the linear actuator within its
calibrated soft limits.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-27 17:04:43 +02:00
3 changed files with 154 additions and 3 deletions
+149
View File
@@ -0,0 +1,149 @@
# Linear actuator — bring-up & run-book
The workshop board can drive a **linear actuator** (stepper on a lead screw/belt) as the
**"Adapt"** half of the Sense→Forecast→Adapt loop: the ADXL355 senses, the agent decides, the
actuator moves. This is open-loop motion with a **calibrated software safety envelope** so it can
never overrun its ends.
> **Read this before powering an actuator-equipped board.** The one rule that bites: a restart
> resets the position zero (see [Operating rules](#operating-rules)).
---
## Hardware
| Part | Detail |
|---|---|
| Driver | **TB6600 / PB6600** (PUL / DIR / ENA, optically isolated). No feedback, no stall detection. |
| Wiring | **PUL → digital pin 4**, **DIR → digital pin 5** (common-cathode: signal `-` pins to GND). ENA left free (driver enabled). |
| Motion | `dir=1` = **into the rail** (away from the zero end) · `dir=0` = **back toward zero**. |
| Speed | ~830 steps/s (600 µs half-period), moderate — safe for most drivers without missed steps. |
| Sensor (same board) | ADXL355 @ `0x1d` behind a **PCA9548A mux** (`0x70`) on channel 0. Unrelated bus (I²C on SDA/SCL); doesn't compete with D4/D5. |
The **TB6600 has no way to sense position or the ends** — that's why travel is bounded in
firmware, not hardware. If you ever add physical limit switches, that becomes the robust upgrade;
until then the envelope below is the guard.
---
## How it's controlled
The resident MCU sketch (`firmware/zeroclaw-node/sketch/sketch.ino`) runs the pulse train and
enforces the limits. Two ways to reach it:
**Agent tools** (cloud brain, within limits):
- `stepper(steps, dir)` — move `steps` (1–4000) in direction `dir` (1 into rail / 0 toward zero).
- `stepper_status()` — read position + limit without moving.
**Relay commands** (`:9999`, for setup/calibration from the host — `nc`/socket):
- `step <count> <dir>` · `zero` · `pos` · `setmax <n>`
- `osc <amp_steps> <freq_cHz> <cycles>` — open-loop sinusoidal excitation (shaker mode)
- `oscm <amp> <freq_cHz> <cycles>` — oscillate while measuring the ADXL355 (per-axis p-p mg)
- `accel` — one ADXL355 sample (mg); `dvf <gain> <secs>` — closed-loop damping (below)
### Direct Velocity Feedback (`dvf`) — active damping
Closes the loop: ADXL355 X-accel @ ~250 Hz → leaky-integrated velocity → stepper
commands `-g·v` (velocity feedback adds damping, c → c+g). Envelope-clamped and
slew-limited. **Runs must be ≤ 8 s** — the RouterBridge RPC times out at 10 s.
Bench-measured gain range:
| gain | behaviour |
|---|---|
| 500 | gentle |
| **1000–2000** | **authoritative and stable — recommended** |
| 4000 | **UNSTABLE** — self-excites off its own step-vibration (velPk 30→1442, 26k steps); the soft-limit envelope catches the runaway |
Actuator FRF note: open-loop amplitude rolls off with frequency — full ~10 mm
holds to ~2–3 Hz, only a few mm by 10 Hz.
**Shaker-table campaign findings (2 Hz base excitation):**
- The control law is **position-target DVF**: carriage position target =
`center − K·v` (reaction force ∝ −v = true damping). A velocity-command law
(carriage velocity ∝ v) is force ∝ −a = *added mass* — no dissipation; it
amplified the response at every gain/sign. Don't regress to it.
- **Safe reference setting: `dvf 30 8`** with the slew limit at 8 steps/tick
(~2000 steps/s). Ran 2 min continuous, silent, self-centering, no runaway.
Slew 25 grinds the motor (lost steps → position corrupted).
- **Sensor placement is the binding constraint:** a deck-mounted sensor near
the rail reads the carriage's own motion (~856 mg open-loop) louder than the
structure sway (~435 mg), so closed-loop damping can't be scored (or cleanly
fed back). Mount the feedback sensor at the structure's max-sway point (tower
top), ideally a second ADXL355 on mux channel 1.
- `listen <secs>` (≤8 s) = passive baseline/ring-down instrument. Old numbers:
tap tests gave stable-looking g up to ~2000, but that predates the shaker
campaign — trust the shaker findings.
Every move is **clamped to `[0, stepMax]`** and the reply reports position, e.g. `pos=1234
max=9635`, ending in `LIMIT` if it hit the soft limit.
---
## Calibrated envelope (this actuator)
```
0 ─────────────────────────────── 9635 ····· 9685
zero (right end) armed safe max hard end
↑ 50-step margin ↑
```
`stepMax = 9635` is **baked into the sketch** (armed on every boot). Hard end measured at ~9685
steps; armed 50 short so a move never reaches the physical stop.
---
## Daily bring-up (safe sequence)
1. **Wire the actuator first, then start the app.** (Touching the bus on a running board resets the
MCU and crashes the app container — wire cold.)
2. Bring the app up (`arduino-app-cli app start …`); confirm `pos` reports `max=9635`.
3. **Home it:** manually park the carriage at the **right end**, then send **`zero`**.
- Now `pos=0` matches reality; the envelope is already armed. Ready.
That's it — the agent can now drive it safely.
---
## Operating rules
- **Park at the right end and `zero` BEFORE any restart.** A container `app restart` **resets the
MCU position to 0** while the carriage stays where it is. If it was parked anywhere but the right
end, firmware `pos` and reality now disagree — and a `dir=1` move would drive into the far stop.
The **envelope (max) survives** a restart; the **zero does not**.
- **Don't stall it.** Open-loop means a stall against a stop **loses steps**, so the zero drifts.
The soft limit exists precisely to avoid this — keep it armed.
- **One session = one home.** Re-`zero` at the start of each session (there's no home switch).
### Recovery — firmware/reality mismatch
If a restart left `pos=0` but the carriage isn't at the right end:
1. `setmax -1` — disarm the clamp temporarily.
2. Jog **`dir=0`** in bursts back to the **right end** (watch it; stop at the end).
3. `zero`, then `setmax 9635` to re-arm.
---
## Calibrating a *different* actuator
If the rail, motor, or TB6600 microstep DIP changes, re-measure:
1. Park at the right end → `zero`.
2. Jog `dir=1` toward the far end — coarse (`step 200 1`) then fine (`step 10 1`) as it nears —
watching. The firmware sums position for you; read it with `pos`.
3. Stop a hair short of the hard stop. Take that `pos`, subtract a ~50-step margin → that's the max.
4. Bake it: set `long stepMax = <value>;` in the sketch and reflash (push sketch → `app restart`,
~50s = a real recompile+flash).
---
## Rebuilding the binary (when the agent tools change)
The `stepper` / `stepper_status` tools live in the ZeroClaw binary
(`crates/zeroclaw-hardware/src/peripherals/uno_q_bridge.rs`). Cross-build for the board:
```sh
cd <zeroclaw>
cargo zigbuild --target aarch64-unknown-linux-gnu --profile release-fast --features hardware --bin zeroclaw
```
`cargo-zigbuild` + zig is the working cross path on macOS (the `aarch64-linux-gnu-gcc` linker isn't
installed). `--features hardware` is **required** or the peripheral tools are stripped. Then push
the binary to `…/apess-onboard/bin/zeroclaw` and `app restart`, or repackage the distributable app
with `package-onboard-app.sh`.
+3 -1
View File
@@ -2,7 +2,9 @@
> **The setup runbook.** For the participant *journey* (each screen's job, the
> module→ADD-layer map, open design questions), see
> [`WORKSHOP-FLOW.md`](./WORKSHOP-FLOW.md).
> [`WORKSHOP-FLOW.md`](./WORKSHOP-FLOW.md). For a board with the **linear
> actuator** (stepper bring-up, calibration, and the park-and-`zero`-before-restart
> rule), see [`ACTUATOR.md`](./ACTUATOR.md).
A team needs two things running: the **APESS stack on their laptop** and the
**ZeroClaw node on their Uno Q**. Both are containers. Nothing installs to a host.
+2 -2
View File
@@ -203,8 +203,8 @@ prompt_injection_mode = "compact"
[risk_profiles.default]
level = "supervised"
allowed_tools = ["matrix_pattern", "matrix_text", "matrix_count", "i2c_scan", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
auto_approve = ["matrix_pattern", "matrix_text", "matrix_count", "i2c_scan", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search", "memory_recall", "web_search_tool", "web_fetch", "calculator", "glob_search", "image_info", "weather", "tool_search", "browser", "browser_open"]
allowed_tools = ["matrix_pattern", "matrix_text", "matrix_count", "i2c_scan", "stepper", "stepper_status", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search"]
auto_approve = ["matrix_pattern", "matrix_text", "matrix_count", "i2c_scan", "stepper", "stepper_status", "uno_q_flash", "sysfs_led", "camera", "network", "i2cdetect", "read_skill", "file_read", "content_search", "memory_recall", "web_search_tool", "web_fetch", "calculator", "glob_search", "image_info", "weather", "tool_search", "browser", "browser_open"]
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date", "df", "du", "uname", "uptime", "hostname", "python", "python3", "pip", "node", "free"]
allowed_roots = []
always_ask = []