A Pico 2 W with a BME280 on four wires, told its broker once, joins its WiFi and publishes
temperature, pressure and humidity from a phone charger — with nothing typed. It announces itself
on arrival, and its will announces it gone when it dies, which is what lets a subscriber tell
"quiet because nothing changed" from "off".
The one piece of new plumbing was neither MQTT nor I2C
net/tcp.c handed out p9_link_t and nothing else, because when it was written the only thing
that spoke over a connection was 9P and its length prefix was the framing. A protocol with
framing of its own needed a second view of a connection rather than a second transport:
tcp_stream_*, on the same slot table, the same buffers and the same pump, costing no new memory.
Writing it taught the rest. The first version transmitted from the writer's task, which broke the
"everything on one call stack" premise net_task_body() had documented all along — a 64 KB echo
stalled about one run in four, and the capture showed 3840-byte segments on a 1500-byte MTU:
two frame builds interleaved in the one shared transmit buffer. The receive side had the same
shape, which a single hart merely hides, so a stream's receive buffer is now a lock-free SPSC ring.
Each measurement decides for itself when to speak
An exponential moving average kills the jitter; a rule per measurement decides the rest — publish
on a move of delta, never faster than a minimum, always within a maximum. The heartbeat is not
optional: a value that has not changed all afternoon must still arrive, or it cannot be told from a
dead node.
One board, one three-minute window, the same 5 s sampling, with a finger held on the sensor partway
through:
| topic | publishes | why |
|---|---|---|
temperature |
25 | touched: 26.3 → 30.7 °C and back |
humidity |
16 | followed the touch, but a 0.5 %RH delta |
pressure |
2 | unaffected — heartbeats only |
What silicon showed, again
The radio wedged seconds after every association and stayed wedged — and the cause was not the PIO
program. The CYW43 supervisor's RSSI poll reached the bus without the bus lock, and
pio_gspi_transfer() begins by disabling the state machine, so a preempted transfer had its SM
stopped underneath it. The capture said so literally: ctrl=0x00000000.
That bug was found twice independently, from opposite ends — once from the board's own boot log the
day it appeared, once from a packet capture five days later — and this release keeps the earlier,
better fix, which also drops the poll from every two seconds to every thirty. On top of it: a
warn-once guard so the next caller that forgets the lock is told rather than silently corrupting
the stream, and a recovery level that resets the chip and reloads its firmware, because levels 1
and 2 send ioctls and on a desynchronised bus an ioctl is what is broken. Fifteen minutes of ping
afterwards: 1797/1800, no stall.
It also produced the release's most misleading symptom. While the radio was wedged, I2C reads
started failing too — three sensor reads in eight — which is exactly the radio/I2C timing
contention the plan had listed in advance as a risk, and was nothing of the kind. The measurement
that separates them is a ping flood against a healthy radio: 10/10 reads, 0.8% loss. A
predicted-and-plausible wrong answer is the one that sticks, so it is written down next to the risk
it impersonated.
Also here
IDSTORE_FIELD_MQTT— the broker lives beside the address and WLAN credentials, so a stored
broker is the intent to publish, with no separate enable flag.I2C_OP_XFER— one generic transfer, so the next I2C part costs no kernel-surface change.tests/mqttbroker.py— a broker that can misbehave on purpose: refuse a CONNECT, split a
header across segments, or die and be replaced on the same port. A peer that can only behave
correctly cannot test a client.- 359 QEMU tests, on RV32 NOMMU, RV64 MMU and a two-hart RV64 SMP target.
Known limitations, stated plainly
- The password crosses the LAN in the clear. No TLS. This is phase 18's threat model unchanged:
auth proves who is talking, it does not hide what they say. pressureis station pressure, not reduced to sea level. At 520 m that is ~60 hPa from what
a weather service shows. Reducing it needs the sensor's installation altitude, which only the
installer knows — inventing one would be worse than publishing the honest raw quantity.- The 24-hour soak has not been run. The longest continuous evidence is fifteen minutes of ping
plus several minutes of publishing. - QoS 1 is unbuilt, on purpose: what it protects is the reconnect, and a superseded measurement
is not worth re-delivering. - ~1.4 KB of static RAM is paid by every persona whether or not it publishes; the build flags
that would have avoided that were judged disproportionate at 420 bytes and never added.