Skip to content

Redrawn machine description: change bits, description by hash, Reconcile, wiring spec, revive of gone rows - #23

Merged
neu-rah merged 24 commits into
mainfrom
claude/project-thread-xfo7jb
Oct 5, 2026
Merged

neu-rah merged 24 commits into
mainfrom
claude/project-thread-xfo7jb

Conversation

@neu-rah

@neu-rah neu-rah commented Oct 5, 2026

Copy link
Copy Markdown
Member

Requested by Roy · project thread

Before: a consumer of the spi example could lose the latest value under a burst of changes (the change queue refused the newest), the device sent its whole text description and looked codes up by name, restore was three parts (capture, canary, restore chain), wiring mistakes were found on the board, and a part that went gone needed a reset to come back.

After: state changes are one pending bit per code plus a sequence number (the latest value always arrives; only the card's events queue). d answers with a description hash, v/w use the hash and code numbers, and the text description is generated at build time. One Reconcile part holds each register's desired value and re-checks identity before writing it back. A wiring spec (examples/spi/wiring/rig.toml) is checked, turned into the App's wiring and diffed against the device's description; a line the part drives on a boot strap pin is refused before the build and at compile time. An app that opts in with revive = true can call World::reviveGone(), which re-checks a gone row's declared address only.

These are experiments 1 to 6 of the redrawn design (decisions D40-D48) plus the revive fix from the Round 7 rig check, as tested on the rig on rnd/redrawn-all (661d9fb), with main (#19-#22) merged in. Merge OneChip #3 first: this PR reads its esp8266Pins.h.

Cost (web agent's measurements on rnd/redrawn-all, before revive): link build by hash is 10,840 B flash / 286 B RAM on ATmega328P, −2,438 B / −225 B against the Round 6 link; ESP8266 289,355 B / 29,636 B, −3,736 B / −296 B. Revive adds +340 B / +4 B on the ESP8266 image when used; without revive the AVR baselines are byte-identical.

How

  • examples/spi/src/tree_ops.h, python/onemachine/tree.py: pending bits (status in the top two bits of the code byte, 5-byte records), resync on a sequence mismatch, description by hash, code numbers.
  • examples/spi/src/air_tree.h, describe.cpp, describe.py: one shared composition; a host generator writes <hash>.txt and facts.txt and refuses on a hash mismatch.
  • examples/spi/src/bmp280_machine.h: Reconcile replaces Capture; a read-back mismatch goes through the identity check first.
  • include/oneMachine/discover/manifest.h: who drives each line (Drive::Mcu, Device, Shared); driver manifests for the BMP280 and RC522; python/onemachine/wiring.py (check, emit, diff).
  • include/oneMachine/discover/registry.h, driver.h: reviveGone() reuses the row's slot.
  • Tests: test/link (burst, resync, both description modes, ASan), test/wiring, test/discover/revive.cpp.

Verified: host suites on the merged tree here (native.sh, build_spi1.sh, test/wiring; AVR builds not available in this environment, CI runs them). On the D1 mini rig (local agent, 661d9fb): description by hash and wiring diff ok, change bits under real changes, unplug/replug status, Reconcile after a soft reset and a set while unplugged, normal boots with a card on the reader, and a BMP280 back alive 31.6 s after gone with no reset and its ctrl_meas kept. Not verified on hardware: the RC522 revive path, and a BME280 swap (none on hand; host test covers id 0x60).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WEBoG3kUwkuTW9Yc2HtQhK


Generated by Claude Code

neu-rah and others added 24 commits October 5, 2026 02:23
rc522.h: an optional Irq policy on the mode (using Irq = X) makes transceive() wait on X::wait()
instead of polling ComIrqReg; ComIEnReg is enabled only while a command runs and cleared after it,
DivIEnReg selects push-pull at init. Modes without it are unchanged.

main.cpp: IRQ on D0 (GPIO16, sampled: no interrupt on the ESP8266), RST on D4, counters of the
line against ComIrqReg every 5 s. The D4 supply switch and keys x/l are gone. An IRQ on a boot
strapping pin (D4) holds it low across a reset of the board and the ROM loops in boot mode (2,x).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
start() returns at once; finish() reads what the command came to. Without an interrupt part the poll
runs both in one call, as before. With rc522::Interrupt<Delivery, Observer> (a mode's `using Irq`) the
poll starts and returns, and service(row, now) finishes the command when the line is asserted or after
40 ms. The register values (what is enabled while a command runs, push-pull) moved from the sketch to
the driver; the App chooses the pin and the delivery (irq::Sampled, irq::IsrFlag, which reject the
boot strapping pins). The line against ComIrqReg is checked and a mismatch is reported through the
failure edge (Fault: spurious / missed, Timeout); the counters are an Observer in the sketch.

Host tests with a modelled IRQ line (spi_irq.cpp) and the delivery pin rules (irq_delivery.cpp).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…al line check and fallback

- Without an interrupt part the driver is the polling driver as it was (same AVR image to the byte:
  test/baselines/rc522_noirq_avr.size); the poll state machine and its per-row state exist only with one.
- fail::Services<W, Drivers>: the loop's service(row, now) over every row of the drivers that serve.
- rc522::Interrupt<Delivery, Observer, Check, Fallback>: the line check is a part (NoCheck by default).
- rc522::PollOnLineFault: a line fault is reported on the failure edge as a kind Recover does not act on
  (Refused) and that row polls the register from then on, still reading cards; the chip is not initialised.
  Without it a line fault stays the chip's (Fault, Recover). A chip initialised again tests the line again.

Host test in three configurations (fail-fast, fallback, no check); AVR builds with the interrupt part.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
bmp280_machine.h: Machine<W> with nodes temp, press (read-only), cal (device constants) and ctrl, a group of
register mimics (get() reads the chip, set() writes it, the defaults are the init); the device's data are
static members. Published<Code, Node, node, OnSync<fn>>: an outer node that refers to the inner one (ItemRef)
and calls fn(value) from a sync pass when it changed. describe() writes the machine, the published codes,
their paths, fields and which ones notify. The sketch uses it for the air sensor: temp= and press= when they
move, keys d (description), a (the control group by path), o (set ctrl_meas), r (the defaults).

Host test against a simulated chip with the datasheet's worked example (bmp_machine.cpp, mockBmpTwi.h).
Needs OneData onsync-r3 (0.1.21) and OneMenu itemref-get.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… old driver goes

- Machine<W, Criteria, Mode>: it takes its Criteria (Addr<0x76>), so two sensors are two types with their own statics.
- PathRef<Machine, 3, 1> / PublishedAt: an outer node reaches its inner node by a compile-time path (any node, a leaf of a
  group too); ItemRef stays for standalone objects. The image is the same as with ItemRef.
- Capture on the register nodes keeps the last set (also while the part is gone); restore() replays it in order, then calls the
  tail; forget() drops it; the defaults are the init. On discovery and on return the part is validated (chip id and calibration
  hash): validated, the capture is replayed; otherwise the slot is dropped and the defaults are the init.
- The driver is under fail::DevEdge over the bus edge (Mode::checked): each poll reads the control registers back, a register that
  lost its capture is Corrupt (Recover -> reinit), a part that stops answering is retried and probed, and reinit validates it.
- Uses oneData::ReadOnly (OneData readonly-public, 0.1.22). The compensation multiplies where the datasheet shifts a possibly
  negative value (undefined before C++20); bmp280.h, the polling driver it replaced, is removed.
- Host tests: capture, reset behind the host's back, unplug with a set while gone, another part, another chip id
  (bmp_capture.cpp); two machines, paths and a published leaf (bmp_machine.cpp). build_spi1.sh now fails on a test's exit status.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- link/frame.h: the request/response framing of role/link.h (op, u16 length, payload; status, u16 length, payload) for a device whose ops answer with a
  payload; python/onemachine's StreamLink and CtypesLink talk to it unchanged.
- examples/spi/src/tree_ops.h: ops over the published nodes: d (the description), v (get by code), w (set by code, through the node: its limits,
  its capture, its register), n (the changes since the last n, from a fail::Buffer<8>: the newest are refused and counted when it is full, and the
  reply says how many were missed). Nodes carry Limits; the description lists ranges; PathRef forwards set().
- examples/spi: env d1_mini_link (-DONEMACHINE_LINK) puts the link on the serial port instead of the log; op f is one fault key; the RFID card is a
  code that notifies; air/config and air/ctrl_meas are published.
- python/onemachine/tree.py: Tree(link): the codes from the description, m.temp, m.air.ctrl_meas = 0x27 (refused here when read-only or out of range),
  m.changes(), m.missed.
- test/link: the consumer against the air sensor on a simulated chip, over a pipe and through ctypes (wired into build_spi1.sh); examples/spi/rig_session.py
  is the same on the real board.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	examples/spi/src/bmp280.h
…nd Python tests

- Status: every published code is bound to a row (Machine::status() reads it from the registry: Alive, Stale or Gone). It is listed in 'd' (a status
  column), carried in every 'v' reply (the status first, then the value; a part that is not alive answers its last value: a register its capture,
  a sensor its last reading), and a change of it is queued like a value change (bit 7 of the code number, the code that stands for the row).
  Python: m.status(code), m.reading(code) -> Reading(value, status), m.temp raises Stale (with the last value), changes() has a Change(code, status=...)
  for every code of the part.
- One link: role::Link's app hook can answer with a payload (`payload = true`: describe() for op 'd', request() for the others, role::LinkReply).
  link/frame.h is gone; the spi example's tree ops (d v w n f) are served by role::Link<role::Machine<>, ...>. The role layer is unchanged:
  the AVR role builds (no link, link, tuned + link) are byte-identical to before, and the role suite passes.
- CI: the suites job clones OneData, OneMenu and what OneMenu includes (the branch name if it exists, else main) and sets up Python, so
  build_spi1.sh runs the machine tests and test/link/check_tree.py.
- Machine header: nodes must not be touched from static initialisers (template inline statics initialise in no defined order).
- The old polling bmp280.h and its compensation test are gone (merge of main; the machine's own compensation is covered by the machine tests).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…chine builds against them)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mplate parameters are as they were

A defaulted extra template parameter changed the instantiation names in the image (not the code), which a symbol baseline shows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… sizes.sh for the full balance

avr_tree.cpp builds the BMP280 machine under the spi example's failure edges on the real AVR TWI core, bare or with the link and tree ops (-DLINK).
sizes.sh prints flash and RAM for each variant on AVR and on the ESP8266 (examples/spi via PlatformIO).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…mber; only the card queues

A state code (temp, press, a register, a row's status) is one pending bit (bmpm::StateChanges): a change sets it and counts the machine's sequence
number. 'n' takes the sequence number of the last reply the consumer got, and answers each pending code once with its value as it is now, then the
events (the card, still a fail::Buffer<8> that refuses the newest and counts it). A consumer whose number is not the last one answered missed a
reply: every code is sent (resync). The Python Tree sends its number, follows 'more', and counts resyncs.

check_tree.py: a burst of 100 temperature changes is one record with the latest value, while 13 cards in the same burst keep the oldest 8 and count
5; a lost reply gives a resync with every code. test/link/build.sh also runs the device under ASan+UBSan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…s every bit set

A record of 'n' carries the code's row status with its value (u8 status << 6 | code, i32), so a status change sets the same bit as a value change;
kind 3 marks an event. On a sequence mismatch the device sets every bit and answers as any other 'n'. The Python Tree announces a row's status
when it differs from the last one it told, and a value whenever its bit was set by the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…d output

'd' answers `hash <fnv>` and each code's status; the hash is FNV-1a over the description's text without the status, folded at compile time by
bmpm::Walk (now constexpr-capable: a put may fold, and may leave out the status). examples/spi/describe.cpp writes <hash>.txt from the same types the
firmware is built from (src/air_tree.h: the codes, the published nodes, the card's line, the sensor's Criteria), checking its text against the fold;
describe.py runs it as a PlatformIO post script into .pio/build/<env>/description. -DONEMACHINE_DESC_TEXT (env d1_mini_link_text) keeps the text.

The Python Tree takes descriptions=<dir>, loads <hash>.txt and checks its hash; without it, a readable error names the hash and the fallback.
test/link/build.sh runs check_tree.py on both builds (pipe, ctypes, ASan+UBSan).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…t builds are Round 6 to the byte

noinline on the text walk made the text build larger (AVR 13,440 B, Round 6 13,278 B), so bmpm::Walk is back to Round 6's code and bmpm::HashWalk
(constexpr, without the status) folds the hash and prints the build output. The App's card text is one macro (AIRTREE_CARD_TEXT), a literal at the
App's text path as before; a published value's notify function is passed as a constexpr pointer, so OnSync<&say<Code, 2>> is Round 6's type.
With the build stamps pinned, the AVR bare and text images and the ESP8266 d1_mini and d1_mini_link_text images are byte-identical to Round 6.

check_tree.py, on the text build: the description the device sends, without its statuses, is the generator's file (the two walks cannot drift
apart unnoticed; a one-word change to HashWalk fails it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…sters

Each register holds what is wanted of it (desired(), its default from the start: RegAt's regDef below it); set() changes it and writes the chip, also
while the part is gone (the last intent). The machine applies what is wanted when the part is found or comes back validated, and resets it to the
defaults for another part (D13/D22); the driver's canary compares the read-back with desired(). This replaces Capture (its value and `have` flag),
the restore/forget chain and RestoreEnd, and the separate defaults-as-init step.

The Round 4/6 scenarios pass unchanged (bmp_capture.cpp: found, set, soft reset behind the host's back, unplug with a set while gone, replug, another
part, another chip id; check_tree.py over pipe, ctypes and ASan+UBSan); bmp_capture.cpp reads desired() where it read captured() and known().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…nd a diff against the device's description

examples/spi/wiring/rig.toml is the rig's wiring by hand (TOML: tomllib, no dependency): the buses' pins, the parts in the App's machine order, and
the codes published as code = "part/node". python/onemachine/wiring.py reads the board's pins from OneChip (esp8266Device.h: each D pin's GPIO and
what its comment says, boot strapping or no interrupt), checks the wiring (each pin once; no IRQ on a strap pin; no ISR on GPIO16; a part on its
driver's bus, at an address and with ids its driver knows; published nodes that exist), emits the App's composition, and diffs the spec against the
device's 'd' (tree._parse). The drivers' manifests are a table in wiring.py, a stand-in for a declaration beside each driver.

test/wiring/check_wiring.py: rig.toml is sound; the 16 lines it emits are all in examples/spi/src/main.cpp; the simulated device's description is
what it wires; another address, another order of machines or of codes are each told; three faults give three sentences. rig_irq_d4.toml (the
RC522's IRQ on D4) is refused with the reason (a strap pin: a pending IRQ holds GPIO2 low and the ESP8266 boots into ROM mode), and the same wiring
emitted anyway is rejected by the C++ (irq::Esp8266Line). Run from test/discover/build_spi1.sh. No firmware changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…escription's hash

By hash (the default), a code in 'v' and 'w' is u32 the description's hash and u8 the code's number: a request under another hash is refused with
role::LinkBadHash and nothing is written; a name is BadLength. The names are only in the build output: on the AVR image none of the codes' names is
left, and the ESP8266 link build by hash leaves out the log lines that carry them (its log goes nowhere; SPI_LOG_NAMES), so they leave its image too.
The text build (-DONEMACHINE_DESC_TEXT) keeps names in v and w with Round 6's code: with the stamps pinned, the bare and text images on AVR and
ESP8266 are still byte-identical to Round 6.

The Python Tree sends numbers under the hash it read (Tree.hash) and names otherwise; a BadHash says the device is another build (refresh()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
…ests; the strap rule by who drives the line

Each driver declares what it needs beside the driver (discover/manifest.h): bmpm::Manifest (i2c, 0x76/0x77, ids 0x58/0x60, which the machine's
Driver and BME entry now read) and rc522::Manifest (spi, its IdSet, its lines: CS and RST driven by the MCU, IRQ by the chip). The buses' lines
have roles too (I2C SDA shared; SPI MISO the part's). The rig's pins are one Wiring in air_tree.h over OneChip's Esp8266Pins (OneChip
rnd/redrawn-6), the sketch takes Twi, the SPI slots, RST and the IRQ from it, and air_tree.h checks it at compile time: a line the part drives or
shares is not on a strap pin; the MCU's may be (D3 and D8 as chip selects). irq_esp8266.h reads strap() and hasIrq() from OneChip.

The description by hash gains a wiring section (the build output's only: the device pays nothing, the hash covers it):
  wiring wemos-d1-mini / i2c sda 4 shared scl 5 mcu / spi ... / rfid rc522 cs 15 mcu rst 2 mcu irq 16 device / empty cs 0 mcu / air bmp280 at 0x76
describe.cpp also writes facts.txt (the board's pins and facts, the buses' lines, each manifest with the BMP280's nodes walked from its machine),
and python/onemachine/wiring.py reads it instead of OneChip's comments and its own table; its strap rule goes by role, and diff compares the pins.
emit writes a Wiring that air_tree.h includes in place of its own (-DAIRTREE_WIRING): built, the rig's gives the same description to the hash.

test/wiring/check_wiring.py: the facts; the emitted Wiring's description is the hand-written one's; the device's hash is the build output's; the
wiring lines; another address, order of machines or codes, and two MCU lines swapped between strap pins are each told; SDA on D3 is refused,
RST on D3 is not; the IRQ on D4 is refused with the reason and its emitted Wiring does not build. test/discover/build_spi1.sh now fails on the
link step's and the wiring step's own exit status. With the stamps pinned the bare and text images (AVR, ESP8266) are Round 6's to the byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx
# Conflicts:
#	test/link/build.sh
…rawn-all

# Conflicts:
#	examples/spi/README.md
#	python/onemachine/tree.py
#	test/link/build.sh
#	test/link/check_tree.py
… address

An app that declares `revive = true` can call World::reviveGone(): each Gone device row's
declared address (I2C address, SPI slot) is checked again by the same entries, and a part
that answers takes its row again (its init runs, consumers are bound). Without `revive`
nothing changes: found() calls reg.add as before and the AVR baselines are byte-identical.

The spi example calls it every 5 s. README and rig.toml note the 10k series resistor on the
RC522's SS line (D8 must be low at reset).

Verified: test/discover/revive.cpp (native, ASan+UBSan), all suites, and on the D1 mini rig
(unplug, wait past Gone, replug: Alive again without a reset, wanted ctrl_meas written back).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
main carries Round 6 re-cut as #19-#22 plus the scripts follow-up; their changes since the
Round 6 tip (c49ba6f) are applied on top of rnd/redrawn-all: the spi_irq, services and
payload_check tests, the role_link AVR baseline, the test tools (baseline, lastok, selftest)
and the build scripts failing on their own status. The machine header note names Reconcile
where main's names Capture/RestoreEnd.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEBoG3kUwkuTW9Yc2HtQhK
@neu-rah neu-rah self-assigned this Oct 5, 2026
@neu-rah
neu-rah merged commit 035ca79 into main Oct 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants