Files
apress/deploy/uno-q/skills/arduino-uno-q/references/04-bridge-rpc.md
T
Omar SobhandClaude Opus 4.8 d2135a1938 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]>
2026-07-16 09:14:00 -07:00

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.