feat(uno-q): ship the arduino-uno-q expert skill on every node by default
Vendors the comprehensive UNO Q skill (SKILL.md + 7 references/*.md) and installs it into EVERY agent's workspace on each board, so agents know this board's specifics (dual-brain arch, Bridge/RPC, pin tables, LED matrix + the ArduinoGraphics-not-installed gotcha) instead of guessing generic Arduino. Why per-agent workspace: ZeroClaw's read_skill returns only SKILL.md; the agent reads references/*.md via the workspace-sandboxed file_read tool, so references are only reachable under ~/.zeroclaw/agents/<alias>/workspace/skills/. A shared/skills bundle surfaces the skill but its references get sandbox-blocked. - push-skill.sh installs a SKILL.md+references skill into every agent workspace (discovers aliases from the board); provision-fleet runs it per board. - config.template risk profile now allows + auto-approves read_skill + file_read so agents load skills without a human approver (webhook path is non-interactive). - Flattened the folded 'description: >-' to single-line (ZeroClaw's frontmatter parser is a flat scanner, not full YAML). Verified on board 65301572 with cloud/Sonnet-5: discovered arduino-uno-q → read_skill(SKILL.md) → file_read references/04-bridge-rpc.md → correct board-specific answer citing the file. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7233a4b1c0
commit
d2135a1938
@@ -0,0 +1,145 @@
|
||||
# UNO Q — The Bridge (Linux ↔ MCU communication)
|
||||
|
||||
This is the single most important capability of the board and the thing people get wrong most often. **"Connecting the Linux side to the Arduino side" means using the Bridge/RPC over the arduino-router. It never means wiring pins together.**
|
||||
|
||||
## The model: transparent remote procedure calls (RPC)
|
||||
|
||||
The two processors exchange data over a dedicated internal serial link. On top of it, a **Remote Procedure Call** system lets a function on one processor be invoked from the other *as if it were a local call*. So Python on Linux can call a function that actually runs on the STM32 (and vice-versa), passing arguments and getting results back.
|
||||
|
||||
Building blocks:
|
||||
- **`arduino-router`** — a background **Linux systemd service** implementing a **star-topology** network using **MessagePack-RPC**. Multiple Linux processes can talk to the MCU *and to each other* through it simultaneously (multipoint). Clients register the functions they offer; the router keeps a directory and routes calls (service discovery).
|
||||
- **The Bridge library** — on the MCU side, `Arduino_RouterBridge.h` (built on `Arduino_RPClite`) gives you the `Bridge` object. On the Linux side, App Lab's `arduino.app_utils` exposes the matching `Bridge` in Python.
|
||||
|
||||
Source repos (for reference): `github.com/arduino/arduino-router` and `github.com/arduino-libraries/Arduino_RouterBridge`.
|
||||
|
||||
## Reserved resources — DO NOT TOUCH
|
||||
The router owns the physical link. **Never open these in your own code:**
|
||||
- **Linux:** `/dev/ttyHS1`
|
||||
- **MCU:** the `Serial1` hardware port
|
||||
|
||||
(Opening them yourself breaks the bridge. Note this is distinct from ordinary `Serial` debug output, which is fine — see the I/O reference.)
|
||||
|
||||
## The MCU-side API (`Bridge`)
|
||||
- `Bridge.begin()` — initialize the bridge + serial transport (call in `setup()`; it can return false if it fails).
|
||||
- `Bridge.provide(name, fn)` — expose an MCU function so Linux can call it. **Runs in a high-priority RPC thread.**
|
||||
- `Bridge.provide_safe(name, fn)` — same, but the function runs inside the main `loop()` context, which is safe for using the normal Arduino APIs. **Prefer `provide_safe` when your handler calls Arduino functions** (`digitalWrite`, `analogRead`, most libraries).
|
||||
- `Bridge.call(method, args...)` — call a Linux-side function and wait for the result.
|
||||
- `Bridge.notify(method, args...)` — fire-and-forget call (no result awaited).
|
||||
- **`Monitor`** — a predefined object to stream text from Linux to the MCU (via the `mon/write` RPC method); legacy, `Serial` is now preferred for console output.
|
||||
- Concurrency is handled with Zephyr mutexes (`k_mutex`); a background thread services incoming updates.
|
||||
|
||||
⚠️ **Do not call `Bridge.call()`, `Monitor.print()`, or `Serial.print()` inside a `provide()` handler** — the handler runs in the RPC thread and these will misbehave/deadlock. Do the work in the handler, and do any printing/bridge-calling from `loop()` instead. (This is exactly why `provide_safe()` exists.)
|
||||
|
||||
## The Python-side API (App Lab)
|
||||
`from arduino.app_utils import *` gives you `App` and `Bridge`.
|
||||
- `Bridge.call("method_name", args...)` — invoke a function the MCU registered with `provide()`.
|
||||
- `App.run(user_loop=loop)` — App Lab's main loop runner for the Python program.
|
||||
|
||||
## Canonical example — Linux drives an MCU-owned pin
|
||||
|
||||
The MCU owns `LED_BUILTIN`; Python decides when to toggle it and calls across the bridge.
|
||||
|
||||
**Linux side — `main.py` (runs on the MPU):**
|
||||
```python
|
||||
from arduino.app_utils import *
|
||||
import time
|
||||
|
||||
led_state = False
|
||||
|
||||
def loop():
|
||||
global led_state
|
||||
time.sleep(1)
|
||||
led_state = not led_state
|
||||
Bridge.call("set_led_state", led_state) # invoke the MCU function
|
||||
|
||||
App.run(user_loop=loop)
|
||||
```
|
||||
|
||||
**MCU side — `sketch.ino` (runs on the STM32):**
|
||||
```cpp
|
||||
#include "Arduino_RouterBridge.h"
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
Bridge.begin();
|
||||
Bridge.provide("set_led_state", set_led_state); // expose to Linux
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
|
||||
void set_led_state(bool state) {
|
||||
digitalWrite(LED_BUILTIN, state ? LOW : HIGH); // built-in LED is active-low
|
||||
}
|
||||
```
|
||||
|
||||
That is the whole pattern for **controlling anything attached to the board from the Linux/Python side**: the sketch owns the pin and `provide()`s a function; Python `call()`s it. To go the other direction (MCU reads a sensor and pushes to Linux), have Python `provide` a function and the MCU `Bridge.call()` it — or have the MCU `notify()` events.
|
||||
|
||||
## Managing the router service (troubleshooting the bridge)
|
||||
```bash
|
||||
systemctl status arduino-router # is it running?
|
||||
sudo systemctl restart arduino-router # restart it
|
||||
journalctl -u arduino-router -f # live logs
|
||||
```
|
||||
Enable verbose logging by appending `--verbose` to the `ExecStart=` line in
|
||||
`/etc/systemd/system/arduino-router.service`:
|
||||
```
|
||||
ExecStart=/usr/bin/arduino-router --unix-port /var/run/arduino-router.sock --serial-port /dev/ttyHS1 --serial-baudrate 115200 --verbose
|
||||
```
|
||||
then:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart arduino-router
|
||||
journalctl -u arduino-router -f
|
||||
```
|
||||
|
||||
## Advanced: talk to the router from any language via the Unix socket
|
||||
The router listens on a **Unix domain socket** and speaks **MessagePack-RPC**, so any language (Python, C++, Rust, Go…) can drive the MCU without App Lab:
|
||||
```
|
||||
/var/run/arduino-router.sock
|
||||
```
|
||||
|
||||
MCU sketch (note `provide_safe`, since the handler uses `digitalWrite`):
|
||||
```cpp
|
||||
#include "Arduino_RouterBridge.h"
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
Bridge.begin();
|
||||
Bridge.provide_safe("set_led_state", set_led_state);
|
||||
}
|
||||
void loop() {}
|
||||
void set_led_state(bool state) { digitalWrite(LED_BUILTIN, state ? LOW : HIGH); }
|
||||
```
|
||||
|
||||
Raw Python client (no App Lab) — install `sudo apt install python3-msgpack`:
|
||||
```python
|
||||
import socket, msgpack, sys
|
||||
|
||||
SOCKET_PATH = "/var/run/arduino-router.sock"
|
||||
led_state = True
|
||||
if len(sys.argv) > 1:
|
||||
led_state = (sys.argv[1] == "1")
|
||||
|
||||
# MessagePack-RPC request: [type=0 (request), msgid, method, params]
|
||||
request = [0, 1, "set_led_state", [led_state]]
|
||||
packed_req = msgpack.packb(request)
|
||||
|
||||
try:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.connect(SOCKET_PATH)
|
||||
client.sendall(packed_req)
|
||||
response = msgpack.unpackb(client.recv(1024))
|
||||
print(f"Router Response: {response}")
|
||||
except Exception as e:
|
||||
print(f"Connection failed: {e}")
|
||||
```
|
||||
```bash
|
||||
python3 msgpack_test.py 1 # LED ON
|
||||
python3 msgpack_test.py 0 # LED OFF
|
||||
```
|
||||
|
||||
## Mental checklist when wiring the two sides together
|
||||
1. Which side owns the physical resource? → that side `provide()`s the function.
|
||||
2. Does the handler touch Arduino APIs? → use `provide_safe()`.
|
||||
3. Never print or `call()` from inside a `provide()` handler.
|
||||
4. Leave `/dev/ttyHS1` and `Serial1` alone.
|
||||
5. If nothing gets through, check `systemctl status arduino-router` and the verbose logs first.
|
||||
Reference in New Issue
Block a user