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]>
146 lines
6.7 KiB
Markdown
146 lines
6.7 KiB
Markdown
# 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.
|