Contributing
Stock server
wellfed-stock is the server a feeder running stock 1.1.15 installs the replacement from. This guide sets it up, first with synthetic inputs and then for a real device or an install. It is for developers at the bench; the owner's installer, wellfed install, runs it for you.
The server exists only for stock. It holds an embedded authenticated broker, the stock protocol as far as an install needs it, the image server stock downloads from, and a bearer-authenticated loopback API. The replacement firmware speaks no MQTT. Its updates and status go over its device API, which wellfed-bench and the owner's wellfed CLI use directly.
How the crate is laid out#
The wellfed-stock crate is both the library and the binary.
- The library holds everything stock speaks: its MQTT protocol, and the broker with stock's two identities (the stock device and the server) and the CONNECT normalization in front of it. It also holds the server's MQTT session with stock, the setup frame, the image server and the install-trial ledger.
- The binary adds the command line, the loopback API and the
networkredirect.
The integration suite runs the binary as a process. The suite and the bench page speak its API through wellfed-stock-api, which links none of the server. cargo xtask check-repository refuses any other crate that depends on wellfed-stock.
The server does not control feeding on stock. The product uses stock only to install from and roll back to.
Try it with synthetic inputs#
No feeder needed.
-
Build it:
cargo build --locked -p wellfed-stock -
Write a configuration from the example inputs:
./target/debug/wellfed-stock init \ --credentials src/tools/wellfed-stock/examples/mqtt-credentials.json \ --capture src/tools/wellfed-stock/examples/mqtt-capture.json \ --mqtt-listen 127.0.0.1:18883 \ --hotspot-interface test_wifi \ --output private/example-server.jsonYou should see
private/example-server.jsonwritten, mode 0600 on Unix. This sends no device commands.
The example inputs contain only synthetic identities. A second import to the same path is refused. --timezone takes an IANA time zone and defaults to UTC. The generated API token and the server password are independent.
Configure a real device#
Use your own isolated gateway or access point, and capture only the device you mean to control. A laptop that merely shares the Wi-Fi may not see the feeder's unicast traffic. Initial stock pairing normally uses the vendor app. Cloud-free provisioning through the setup frame and an installer's server is host-tested only.
-
Store the device's single MQTT CONNECT credential record as a JSON array, in the shape the credential example shows.
-
Export the decoded MQTT packets as Wireshark/TShark JSON:
tshark -r private/capture.pcap -Y mqtt -T json > private/mqtt.jsonYou should see
_source.layers.mqttand the Ethernet source fields, as in the capture example. A capture with different dissector nesting needs normalizing first. Keep credentials and raw exports private. -
Check the export holds what the importer needs: one device namespace, its source MAC, and an authoritative captured feeding-plan response (
GET_FEEDING_PLAN_EVENTorFEEDING_PLAN_SERVICEwithplans). The binding response is optional. Never invent an empty snapshot for a device that has schedules. -
Run
initwith your own credential and capture paths, a specific IPv4 broker address the feeder can reach, your gateway interface and your time zone.You should see
private/server.json, the default output. Wildcard and IPv6 broker binds are refused. The HTTP API defaults to loopback127.0.0.1:8765. -
Start the server and check it:
./target/debug/wellfed-stock serve --config private/server.json ./target/debug/wellfed-stock status --config private/server.json ./target/debug/wellfed-stock network preview --config private/server.jsonStarting the service does not redirect traffic or command feeding.
The configuration format is version 1. An older configuration may still carry max_portions, which is refused as an unknown field: delete that line. Edit the configuration while the service is stopped, and keep the ledger beside it.
Redirect the feeder's broker traffic#
network apply is a separate, privileged Linux gateway operation. It validates an nft transaction, redirects the configured feeder MAC's TCP/1883 traffic on the configured interface, and blocks that feeder's forwarded WAN traffic.
- Read the
network previewoutput, and confirm the device identity it names. - Run
network apply. - Check the counters and the actual traffic. Don't assume WAN isolation without checking.
network remove removes Wellfed's dedicated table and its tagged input rules. Their names are unchanged since the server was renamed, so rules applied before the rename are removed too.
Setting up the access point, host-wide firewall policy, persistent services and reboot persistence are outside this workflow. If the feeder depends on the redirected route, restore the prior routing before you shut the service down.
An installer's server: trust on first use#
An install needs no capture. init-installer writes a config with fresh secrets and the broker address the setup frame names. It needs no device at all.
./target/debug/wellfed-stock init-installer --mqtt-listen 192.168.1.20:1883 --output private/installer.json
./target/debug/wellfed-stock serve --config private/installer.json --enroll-minutes 15
- Enrollment. With no device known,
servestarts the broker first and waits. Within the window, it admits the first CONNECT that looks like stock's: a client ID stock's topics can carry (not the server's), with a username and password, from a known relayed address. It records that device beside the config, mode 0600, named for the config (installer.json's isinstaller.device.json). The record holds the client ID (which is the device ID), username, password and the address it connected from. Then the rest of the server starts with that device. - Pinned. The record is created whole or not at all: written aside, then hard-linked into place, which never replaces. So
servepolling for it never reads half a record, and a killed broker leaves none behind. A second feeder, a wrong password, or anyone after the window is refused, as with a captured config. A laterserveneeds no window and answers the enrolled feeder alone. To start over, stop the server and remove the record. - Contested. Inside the window, any second stock-shaped identity, or a wrong password for the enrolled one, is noted in
installer.contested.json, without its credentials. The status document'senrollmentshows the device, its address and the contest.POST /api/ota/trialrefuses (409) while the contest stands, because which feeder is the owner's can no longer be told. Only attempts inside the window count. After it closes, a second identity is refused but not flagged, so the installer's owner check of the device ID and address is then the only one. - One or the other. A config that names a captured device and has an enrollment beside it is refused.
- No redirect. An installer's config has no hotspot interface or feeder MAC, so
networkrefuses it.ota-imagesadmits the enrolled feeder's address unless--peeris given.
wellfed install (consumer installation) runs this server as its child.
The install ledger#
Install trials are recorded in stock-ledger.json beside the configuration, under an exclusive lock.
- Each trial is persisted before its one
OTA_UPGRADEgoes out, and is never resent. A repeated request ID returns the stored record. - A trial in flight when the server stops or loses the broker becomes uncertain.
- An uncertain or failed trial blocks the next until
ota-trial-resolvecloses it from fresh evidence. Resolving sends no command. - A replacement install closes instead on
ota-trial-answered, once the operator's client saw the served build answer its device API from bank 0 and stock has gone quiet (below).
serve --enable-ota-trial holds the bench lease beside its configuration for as long as it runs, since an install it dispatches runs for minutes. ota-trial only asks that server, so it takes no lease of its own. setup-send takes the lease for its one frame. A plain serve, broker and ota-images take none: they only answer, beside the command that drives the feeder.
An older ledger, feed-ledger.json, also held feeds and probe queries. serve refuses to start while it is present. Check that no trial in it is submitted, uncertain or failed, then rename it.
HTTP API#
Write bodies are limited to 4096 bytes. Authenticate with the bearer token from the private configuration. The CLI handles it without putting the token in shell arguments.
| Route | Does |
|---|---|
GET /health |
Public process and build identity ("service": "wellfed-stock") |
GET /api/status |
Authenticated stock telemetry, build identity, install trials and stock's OTA reports |
POST /api/ota/trial |
{ "request_id": "UUID", "server_token": "UUID" }: the stock-to-replacement install. Needs explicit trial mode and a live image lease |
POST /api/ota/trial/resolve |
Explicit uncertain-trial reconciliation. Sends no command |
POST /api/ota/trial/answered |
{ "request_id": "UUID", "build": "64 hex", "bank": 0, "note": "…" }: the latest submitted or uncertain trial's served build answered the device API from bank 0 after it. Closes it as replacement_healthy and sends no command. 409 while stock still reports, or for another build or bank |
Normal serve mode keeps trial dispatch disabled. Trial dispatch needs a clean, identifiable source commit. Test-only bypasses are not a deployment procedure; see fixtures and recovery limits.
Install the replacement#
wellfed-bench stock-ota drives this whole sequence (bench).
-
Serve the frozen review:
./target/debug/wellfed-stock ota-images --directory private/verification/<review>You should see the review's
image.binserved as/ota/<token>/replacement.bin.ota-imagesis only a transfer service for one reviewed build; it does not command an update.--peerdefaults to the feeder found by its MAC on the hotspot interface.It refuses an image that is not a build's, is larger than the stock footprint (1,376,288 bytes), lacks the loader marker, or does not match the review's
ota_sha256. The review's layout audit is what establishes the container header and byte-sum stock checks. There is nooriginal.binbeside it: the way back is the UART stock recovery.The lease records the served digest and the build's identity: the startup identity of the review's frozen
build-manifest.json, which must match the review'smanifest_sha256. -
Check the schedule snapshot is empty. The server answers stock's schedule queries from the imported snapshot, and a trial needs it empty.
-
Start a trial with
POST /api/ota/trialagainst aserve --enable-ota-trial. It needs the lease, announces target version2.0.0with the lease's digest, and records both in the ledger. -
Close it with
POST /api/ota/trial/answered. Stock never reports after installing the build, and the build speaks no MQTT, so the operator's client reports instead. The report gives the build identity and bank the build's device API answered with after the submission, and a note of what it saw.
The server sends nothing, and closes only the latest submitted or uncertain trial, and only when both hold:
- The answer names the served build on bank 0. Its
buildequals the identity recorded at submission, andbankis 0. A record from before the identity was kept never closes this way. - Stock is not demonstrably running. Its telemetry is not current (none for 90 s), and it has reported no version since the submission. A version report after the submission marks the trial for good (
stock_reportedin the ledger). Stock was still running then, so a dropped session that came back on 1.1.15 cannot be closed by an answer; onlyota-trial-resolve's version evidence resolves it. This fails safe: if stock also reports its version during a download, an install that did succeed waits for the resolution as before.
The answer is still the client's word. The server checks it against what it served, not against the device API itself. wellfed-itest's bank-0 step asks for 120 s, past stock's telemetry window.
Operation with a nonempty plan, and stock's time-recovery behavior, are unvalidated; see stock behavior.
The replacement firmware (deleted roles)#
The broker once served the replacement firmware too, as a role with its own credential and namespace (firmware-credential, health, requests and ota-offer). That role is deleted, with the firmware's MQTT transport. Builds take updates over the device API, which wellfed-bench ota pushes, and wellfed-build no longer reads private/fw-mqtt.json.
A configuration written while the role existed keeps its firmware section. wellfed-stock ignores it unread, so the file loads as it is.
Evidence#
- Observed: local dispensing on stock 1.1.15 was demonstrated on the research unit, before stock control was removed from this server. The code is in Git history.
- Modeled:
tests/enroll_stack.rsruns the realserve, which enrolls a simulated feeder with never-seen credentials, hears its version report, refuses a second identity and a guessed password, and after a restart without the window admits only the enrolled feeder. Not yet run against stock.