A C framework for commanding a Pico WH robot over USB, WiFi or BLE with a
small text DSL. No hardware attached yet, so the whole stack runs on the
host against a simulated HAL and is covered by tests.
Two specifications drive the code:
docs/grammar.md the language — one namespace of callables (native,
firmware-resident, and defined over the wire), procedures
with named arguments and defaults, REPEAT, and the rules
that keep it safe on a microcontroller.
docs/flow.md transport, bracket-balance framing, the envelope,
admission, execution, abort and failsafe.
Three properties the design leans on:
- Bodies may only reference callables that already exist, so the call
graph is acyclic by construction, recursion is unrepresentable, and
interpreter depth is checked at definition time rather than discovered
at runtime.
- Nothing blocks. The interpreter is a resumable state machine over an
explicit frame stack, so ABORT is honoured within one tick even in the
middle of a long move, and no program can overflow the MCU stack.
- No queue. One outstanding statement at a time, which keeps abort to a
single unambiguous victim and avoids inventing an answer to "the
running program failed, does the queued one still go?".
Named arguments are resolved to the callee's parameter order at definition
time, so they cost nothing at execution.
rubo_core is target-independent. Porting to the Pico means providing a
non-blocking rubo_transport_t and a rubo_hal_t, and nothing else.
Not built yet: the Pico transport and HAL, the camera DAT/DEND bulk path,
and IF/ELIF, which is reserved in the grammar and lands with the sensors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hfu1EFVbpee92zvzQunsKb
103 lines
3.1 KiB
Markdown
103 lines
3.1 KiB
Markdown
# rubo
|
|
|
|
A C framework for commanding a small robot — Raspberry Pi Pico WH, two
|
|
motors driving four wheels, a black-and-white camera — over USB, WiFi or
|
|
BLE, using a small command language.
|
|
|
|
The hardware isn't attached yet, so the whole stack runs on the host against
|
|
a simulated HAL. The language, the framer and the interpreter are exercised
|
|
by tests with no Pico in the loop.
|
|
|
|
## Specifications
|
|
|
|
| Document | Covers |
|
|
|----------|--------|
|
|
| [`docs/grammar.md`](docs/grammar.md) | the language: callables, procedures, named arguments, `REPEAT`, the rules that keep it safe |
|
|
| [`docs/flow.md`](docs/flow.md) | transport, framing, the envelope, admission, execution, abort and failsafe |
|
|
|
|
Read those before changing anything here. Most of what looks like an odd
|
|
decision in the code is a decision recorded in one of them.
|
|
|
|
## Build and test
|
|
|
|
```sh
|
|
cmake -S . -B build
|
|
cmake --build build
|
|
./build/test_rubo # or: ctest --test-dir build
|
|
```
|
|
|
|
## Drive it
|
|
|
|
```sh
|
|
./build/rubo_host --trace
|
|
```
|
|
|
|
`--trace` prints motor state changes. Try:
|
|
|
|
```
|
|
PING
|
|
STAT
|
|
SQUARE side=400
|
|
DEF ZIGZAG $deg=20 $reps=3 [
|
|
REPEAT $reps [
|
|
TURN dir=LEFT deg=$deg;
|
|
TURN dir=RIGHT deg=$deg
|
|
]
|
|
]
|
|
ZIGZAG deg=15
|
|
ABORT
|
|
```
|
|
|
|
`--script` makes it read piped input as a well-behaved client would: one
|
|
outstanding statement at a time, waiting for `DONE` or `ERR` before sending
|
|
the next (docs/flow.md §9).
|
|
|
|
```sh
|
|
./build/rubo_host --script < commands.txt
|
|
```
|
|
|
|
## Layout
|
|
|
|
```
|
|
include/rubo/rubo.h public interface: limits, error codes, framer, engine
|
|
include/rubo/hal_sim.h simulated hardware, host only
|
|
|
|
src/lexer.c tokeniser (grammar.md §3)
|
|
src/parser.c recursive descent, validation (grammar.md §4, §5)
|
|
src/registry.c callable table and arena (grammar.md §2)
|
|
src/sym.c identifier interning
|
|
src/exec.c resumable interpreter (flow.md §8)
|
|
src/framer.c bracket-balance framing (flow.md §4)
|
|
src/session.c envelope and admission (flow.md §5, §7, §9)
|
|
src/natives.c built-in callables (grammar.md §8)
|
|
src/stdlib.c firmware procedures, in the language itself
|
|
src/hal_sim.c simulated motors and camera
|
|
|
|
host/main.c the main loop from flow.md §8.3
|
|
host/transport_stdio.c non-blocking stdio transport
|
|
|
|
tests/test_rubo.c host tests
|
|
```
|
|
|
|
## Porting to the Pico
|
|
|
|
Two things, and nothing else:
|
|
|
|
1. a `rubo_transport_t` over USB CDC, lwIP or BLE NUS — `read()` must never
|
|
block;
|
|
2. a `rubo_hal_t` driving the real motors.
|
|
|
|
`rubo_core` is target-independent and does not change. That is the point of
|
|
the layering in docs/flow.md §2, and it is why the language is testable
|
|
today.
|
|
|
|
## Status
|
|
|
|
Working: the language end to end (definition, named arguments, defaults,
|
|
`REPEAT`, procedures calling procedures), framing, the envelope, admission,
|
|
abort, the deadman, and a simulated HAL.
|
|
|
|
Not built yet: the Pico transport and HAL, the camera `DAT`/`DEND` bulk path
|
|
(docs/flow.md §11), and `IF`/`ELIF`, which is reserved in the grammar and
|
|
lands with the sensor layer (docs/grammar.md §7).
|