Skip to content

About

Local control plane for the Microduck's daemons (quack-nav, quacksat, …)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

quack-control

A local control plane for the Pollen Robotics Microduck's daemons: one page in a browser on the home network — a phone, a laptop — that shows the map the duck built, live, and drives it. It runs on the duck beside the daemons it manages and talks to each one over its own unix sockets, through an adapter per kind: quack-nav now, quacksat and others later. Italian: README.it.md.

The page on the MuJoCo twin

The page on the MuJoCo twin (casa_grande, 2026-10-01): the duck on its way to a tapped point — the route ahead in green, the path walked in pink, the drops it booked in red, two named places — and its camera in the corner.

Independent project, not affiliated with Pollen Robotics or Hugging Face. Status: 0.1.0-rc2, the second release candidate (release notes, earlier: 0.1.0-rc1, releases); tested against quack-nav's MuJoCo twin (map, go_to, places, stop, knobs and restart, camera), not yet on a duck.

Compatibility

quack-control quack-nav quacksat
0.1.0-rc2 ≥ v0.2.0-rc2 (the 3D house: quack-nav ≥ v0.3.0-rc2's scripts/map3d) no control socket yet
0.1.0-rc1 ≥ v0.2.0-rc2 no control socket yet

With an older quack-navd the page degrades: what it lacks (nav.knobs, nav.restart, a place named at a tapped point, the rest / untrusted / self-started badges) answers "update quack-nav" or stays off. The camera needs Pollen's mediad, daemon-v0.14.4 or later (media.frame), or the twin's viewer (The camera).

What the page does

Map (the default view):

  • the live map (walls, floor, unknown) and, from quack-nav v0.3.0-rc2, its band: what the depth sensor found in the way at the duck's height (brown) and table tops or bed edges with floor under them (tan), which the navigation goes round as walls; the duck's pose and heading — red when it is lost or seated — and its uncertainty (one sigma), the path it walked on its last two runs (a run ends where it stood still 20 s), the route of the job it runs, the goal, the drops and obstacles on its books, the named places;
  • pan and zoom (drag, pinch, wheel), fit, follow the duck; turn the map (⟲ ⟳ by 90°, two fingers freely, ⊞ squares the walls to the screen, N back to the map as drawn) — kept per map in the browser, since a map starts wherever the duck stood, turned as it was facing;
  • tap the map → Go here (asks first) or Name the point; a big STOP that stops whatever the duck is doing;
  • places: the list with their distance, Go, Forget, and Teach the spot the duck stands on;
  • exploring: Start, Stop, Exploration complete (asks first);
  • the status line: mapping mode and map, localized or searching, the job (exploring, going somewhere, its outcome), how much of the house is mapped and in how many sessions, the daemon's own hint, and badges for what the duck does of its own accord — moving on its own (and why), may have been moved (it finds itself before the next job), resting, stopped by you; places say whether they are usable, waiting for the duck to find itself, on another map, or stale; a quack-navd that refuses says why, one that is unreachable says so, and the page reconnects by itself;
  • the camera: a small window over the map (the camera button, top right), with what the duck's head camera sees — drag it by its title to any corner, close it, full screen (the browser's own where it has one; on the iPhone, whose Safari has it for videos only, the window covers the page). It shows the frame rate and how old the frame is, or "camera unavailable" and why; open/closed and the corner are remembered in the browser. Frames are asked for only while the window is open, the map view shown and the page visible — none at all otherwise.

3D (experimental; shown when a house3d service is configured, as by default): the house in 3D with the camera's own colours, built off board by quack-nav's scripts/map3d from what the duck saw while exploring (The house in 3D). Drag to turn it, two fingers (or the right button, or shift) to move it, pinch or the wheel to zoom; isometric, from above (squared like the map tab), a height to cut it at so the rooms show, the duck's way through the house, and the duck where it is now when the house was built on the map it is on.

Services: each managed daemon and whether it answers. quacksat shows as not available: it has no control socket yet (docs/todo.md).

Advanced (collapsed, behind a warning):

  • All tools: every tool quack-navd lists in nav.catalog, with a form built from its JSON Schema (or raw JSON), the raw reply shown; a tool that acts asks for confirmation, and the server refuses it without;
  • Knobs: quack-navd's environment knobs (QK_*, MAPLOC_*) grouped, with their default, what the code says, the value saved and the value running; edit, reset one, reset all, and Apply, which restarts quack-navd so it reads them (it stops what the duck is doing, saves the map, and the homecoming runs as at boot).

Running it

Rust 1.89 or newer.

cargo build --release
target/release/quack-control /etc/robot/quack-control.toml

quack-control.example.toml:

bind = "0.0.0.0:8090"
# token = "a-long-random-string"     # better: QC_TOKEN in the environment

[[service]]
name = "quack-nav"
kind = "quack-nav"
nav_socket = "/run/quack-nav/nav.sock"
map_socket = "/run/quack-nav/map.sock"

[[service]]
name = "camera"
kind = "mediad"
media_socket = "/run/mediad/media.sock"

[[service]]
name = "quacksat"
kind = "quacksat"

No file: these defaults. Then open http://<the duck's address>:8090/.

Against the twin

quack-nav's MuJoCo twin serves quack-navd's sockets under its STATE directory (/tmp/quack-twin by default). A config that points there, on this machine only:

bind = "127.0.0.1:8090"

[[service]]
name = "quack-nav"
kind = "quack-nav"
nav_socket = "/tmp/quack-twin/nav.sock"
map_socket = "/tmp/quack-twin/map.sock"

[[service]]
name = "camera"
kind = "mediad"
media_socket = "/tmp/quack-twin/media.sock"
target/release/quack-control twin.toml
open http://127.0.0.1:8090/
# without a browser: the live stream, and the services
curl -N http://127.0.0.1:8090/api/quack-nav/events
curl http://127.0.0.1:8090/api/services

The camera on the twin: there is no mediad, so quack-nav's twin viewer (scripts/twin/viewer/eye.py, VIEWER=on) renders the duck's head camera in MuJoCo and answers mediad's call on $STATE/media.sock — the same adapter for the twin and the duck. With VIEWER=off, no camera.

The knobs on the twin: quack-navd writes $STATE/knobs.env, and Apply answers that it is not under systemd — scripts/twin/twin.sh restart-navd in quack-nav restarts it with them.

Security

Plain HTTP, for the home network only. Whoever reaches the port can open the page, and the page drives the duck. Never forward it to the internet; remote access is planned over Pollen's channel instead (docs/todo.md).

  • The token (token, or QC_TOKEN — the unit reads /etc/robot/quack-control.env, root-only, mode 600): when set, every call and the live stream need it, as the X-Token header or ?token=; the page asks for it once and keeps it in the browser. It travels in clear on the network, like everything else here: it keeps out a guest's phone, not someone sniffing the Wi-Fi.
  • Another site cannot drive the duck through the browser of someone at home: every call is a POST with a JSON body, which a page from another origin cannot send without a preflight that quack-control never answers.
  • What reaches the duck: the map view reaches seven tools in the shapes it uses (robot.go_to a point, a place or stop; explore start, stop, complete — never fresh; teach and forget a place; three reads). The advanced view reaches every catalog tool, and one that acts only with "confirmed": true; the knobs only through quack-navd's nav.knobs, which checks every value against its type.
  • On the duck quack-control runs as quackctl, in the robot group (that is what quack-navd's sockets ask), unprivileged and fenced (systemd/quack-control.service).

The camera

The mediad adapter (src/adapters/mediad.rs) reads Pollen's mediad (microduck, daemon-v0.14.4 and daemon-v0.15.0): media.frame on its local socket /run/mediad/media.sock (0660, group robot, which quackctl is in). One JSON-RPC line asks; one line answers with a header (width, height, UYVY, bytes, capture time, rotate) and the raw frame follows — 1280x720, 1.84 MB at the default rung. mediad copies a frame only when asked and waits for the next capture to answer (at most 500 ms).

GET /api/<service>/snapshot answers image/jpeg (token-protected like every call): the frame turned upright (rotate, 90 on every duck and on the twin), shrunk to at most 640 px on its long edge, JPEG quality 70 — 360x640, 5–40 kB — with X-Frame-Age-Ms, X-Frame-Captured-Us and X-Frame-Size. One fetch at a time, at most five a second for all the pages together: a frame younger than 200 ms is served from memory, and a failure answers for itself (503) for a second before the camera is asked again. Video (WebRTC, mediad's own stream) is for later (docs/todo.md).

Port 8090: mediad's own console listens on 0.0.0.0:8080 (--web-port, daemon-v0.15.0), so quack-control's default is 8090 (since 2026-10-01; it was 8080, and the second to start could not listen).

The house in 3D

The house3d adapter (src/adapters/house3d.rs) serves one file, /var/lib/quack-control/house3d.bin (house_file): GET info its header, GET model the file, for the page to draw with WebGL (no library, nothing from the network). Nothing is built on the duck — its board is too small for the depth model and keeps its cores for walking: quack-nav's scripts/map3d/record.py records beside an exploring duck, build.py builds the house on a Mac or a PC and writes house3d.bin (a few hundred kB at 2 cm), and scripts/map3d/send.sh (or build.py --send) copies it to the duck over ssh:

scripts/map3d/send.sh microduck@duck out/house3d.bin

By hand, the same:

scp house3d.bin microduck@duck:/tmp/ && ssh microduck@duck \
  sudo install -D -m 644 /tmp/house3d.bin /var/lib/quack-control/house3d.bin

The file is read again when it changes; the page's ↻ loads it again. The format: QH3D, a u16 version (1), a u16 header length, the header (JSON: points, voxel_m, map, frames, built, path) padded to a multiple of 4, then 12 bytes a point — i16 x, y, z in mm on the map's frame, u8 r, g, b, i8 normal x, y, z. On another map than the one it was built on, the house still shows but the duck is not drawn on it.

Installing on the duck

After quack-nav (quack-navd and the robot group).

Installing from a release

No checkout and no build: every release carries an install package, quack-control-<version>-aarch64-linux.tar.gz, with its .sha256 — the board's binary, the unit, the service account, the example config, install-on-duck.sh and a step-by-step README-install.md (Italian: README-install.it.md). The bare binary, quack-control-aarch64-linux, is attached too, with its .sha256. Your computer needs ssh, scp, tar and shasum.

V=0.1.0-rc1     # the release's tag without its v
gh release download "v$V" --repo andreagenovese/quack-control \
    --pattern "quack-control-$V-aarch64-linux.tar.gz*"
# or: curl -LO https://github.com/andreagenovese/quack-control/releases/download/v$V/quack-control-$V-aarch64-linux.tar.gz
#     (and the same URL with .sha256)
shasum -a 256 -c "quack-control-$V-aarch64-linux.tar.gz.sha256"   # prints OK
tar xzf "quack-control-$V-aarch64-linux.tar.gz" && cd "quack-control-$V"
./install-on-duck.sh --dry-run microduck@192.168.1.42   # optional: prints every command, connects to nothing
./install-on-duck.sh microduck@192.168.1.42

The script finds its files next to itself (bin/quack-control, systemd/, quack-control.example.toml) and installs them as in the table below. Then, on the duck: the services' sockets in /etc/robot/quack-control.toml if they differ from quack-navd's and mediad's defaults (Running it), and a token in /etc/robot/quack-control.env (Security):

sudo sh -c 'echo "QC_TOKEN=$(tr -dc a-z0-9 </dev/urandom | head -c 32)" > /etc/robot/quack-control.env'
sudo systemctl restart quack-control

Upgrading: the newer release's package, its ./install-on-duck.sh the same way; the config and the token file stay. scripts/package.sh <version> <binary> <outdir> packs the same package from a checkout, as CI does.

From a checkout

scripts/cross-build.sh                              # aarch64, glibc >= 2.31
scripts/install-on-duck.sh microduck@192.168.1.42   # binary, unit, account, config

Either way, --dry-run prints every command, the script it would run on the duck included, and connects to nothing; a second argument installs another binary.

on the duck from this repo
/usr/local/bin/quack-control the cross-built binary (1.7 MB)
/etc/systemd/system/quack-control.service systemd/quack-control.service
/etc/sysusers.d/quack-control.conf (user quackctl) systemd/sysusers.d/quack-control.conf
/etc/robot/quack-control.toml quack-control.example.toml, only when absent
/etc/robot/quack-control.env created empty (QC_TOKEN commented), root, 600, only when absent

systemctl status quack-control, journalctl -u quack-control -f. Uninstalling: sudo systemctl disable --now quack-control, then remove the binary, the unit and the sysusers file.

How it is built

One binary, std threads, no async runtime and no HTTP crate: a few hundred lines of HTTP/1.1 (src/http.rs), one thread per connection, server-sent events for the live data and POST JSON for commands. Dependencies: serde, serde_json, toml, tracing, anyhow, jpeg-encoder (pure Rust, for the camera's frames). The page is one HTML file with vanilla JavaScript and a canvas, embedded in the binary — no build step, nothing loaded from the network, it works offline.

file what it does
src/server.rs the routes, the token, the JSON-only rule, the event streams
src/adapters/mod.rs the Adapter trait and the kinds
src/adapters/quack_nav/ quack-navd: the map stream and the status poll (hub.rs), the checks on every call (bridge.rs)
src/adapters/mediad.rs the camera: media.frame, the shared cache, UYVY → upright JPEG
src/adapters/quacksat.rs a placeholder, and what quacksat would need to expose
src/page.html the page

The quack-nav adapter uses quack-nav's control contract (quack-nav's docs/control-contract.md): nav.catalog/nav.call on the nav socket, robot.map on the map socket, nav.knobs and nav.restart. Why a repository of its own, the adapter model and the road to Pollen's channel: docs/adr/0001-a-control-plane-of-its-own.md.

cargo test --release      # 40 tests: HTTP, token, routes, every check on the calls, the camera

License

Apache-2.0 (LICENSE, NOTICE).

About

Local control plane for the Microduck's daemons (quack-nav, quacksat, …)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages