Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1fe5b75
examples/spi: RC522 IRQ line on D0, sampled; supply switch removed
neu-rah Oct 5, 2026
cef137b
rc522: a command in two halves, with the IRQ line as an interrupt part
neu-rah Oct 5, 2026
ce501a5
rc522: no interrupt part costs nothing; service over all rows; option…
neu-rah Oct 5, 2026
fd5afb5
examples/spi: the BMP280 as a machine of OneMenu ItemDef nodes
neu-rah Oct 5, 2026
425860f
BMP280 machine: capture and restore, the Criteria, nodes by path; the…
neu-rah Oct 5, 2026
655c70f
A Python consumer of the machine tree over the existing link
neu-rah Oct 5, 2026
2e15f28
Merge remote-tracking branch 'origin/main' into tree-status
neu-rah Oct 5, 2026
df1cb5c
Round 6: a status per published code, one link, CI runs the machine a…
neu-rah Oct 5, 2026
5d7e106
CI: the examples job clones OneData and OneMenu (the spi example's ma…
neu-rah Oct 5, 2026
c49ba6f
role::Link: the app declares its reply capacity (replyCap); Link's te…
neu-rah Oct 5, 2026
3b3ca08
test/link: an ATmega328P image of the spi air sensor composition, and…
claude Oct 5, 2026
ba9487b
Redrawn experiment 1: state changes as pending bits and a sequence nu…
claude Oct 5, 2026
2ddfe51
Redrawn experiment 1, trimmed: one pending bit per code, and resync a…
claude Oct 5, 2026
d8f58db
Redrawn experiment 2: the description by hash, its text from the buil…
claude Oct 5, 2026
bd863c8
Redrawn experiment 2: the hash by a walk of its own; the bare and tex…
claude Oct 5, 2026
932143b
Redrawn experiment 3: one Reconcile part on the BMP280's control regi…
claude Oct 5, 2026
06ba14d
Redrawn experiment 4: the rig's wiring spec, a dry-run composition, a…
claude Oct 5, 2026
fcfaa71
Redrawn experiment 5: code numbers in v and w, valid only under the d…
claude Oct 5, 2026
4e0f29f
Merge rnd/redrawn-4 (the wiring spec) into experiment 6
claude Oct 5, 2026
7bf9886
Redrawn experiment 6: pin lines in the description, from driver manif…
claude Oct 5, 2026
bba5886
Merge rnd/redrawn-3 (Reconcile) into rnd/redrawn-all
claude Oct 5, 2026
8138a72
Merge rnd/redrawn-1 (pending bits and a sequence number) into rnd/red…
claude Oct 5, 2026
661d9fb
discover: reviveGone(), a Gone row is looked at again at its declared…
neu-rah Oct 5, 2026
e627b0c
Merge main (#19-#22) into the redrawn work
claude Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions examples/spi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Wemos D1 mini (ESP8266), everything at 3.3V.

Slot 1 is D3 (GPIO0) with nothing on it: the scan reports it empty.

D8 (GPIO15) must be low when the board resets. An RC522 module can hold its SDA line high at that moment, and the ESP8266 then
boots from SDIO ("boot mode:(7,x)", "waiting for host") and runs nothing. Put a 10k resistor in series in the D8 to SDA wire.

D0 (GPIO16) is the RC522's IRQ input (push-pull, active low, set by the driver). A poll starts its command and returns; the
loop's `fail::Services` step finishes it when the line is asserted, or after 40 ms, so nothing in the loop waits for the reader. The
ESP8266 has no interrupt on GPIO16, so the line is sampled (`irq::Sampled<16>`); on a pin that has one, `irq::IsrFlag<Pin>` sets a
Expand Down Expand Up @@ -73,10 +76,12 @@ m.changes() # [Change('temp', 27.61,
m.status('air') # 'alive', 'stale' or 'gone': the part's row; m.temp raises Stale when it is not alive
```

Ops: `d` the description (each code with the status of its row), `v` get by code (the status first, then the value; a part that is not alive answers its last value), `w` set by code (through the node: its limits, its capture, its register), `n` the changes since the last `n` (a value, or a status change of a row), `f` one
fault key (`x` resets the air sensor behind the host's back, `v` and `p` reset the RFID reader). The changes wait in a `fail::Buffer` of 8: when a consumer
does not read for a while the newest are refused and counted, and the reply of `n` says how many it missed (`m.missed`); read the values again with
`m.temp`. `examples/spi/rig_session.py` is a session on the real board; `test/link/build.sh` runs the same consumer against a simulated one.
Ops: `d` the description by its hash and each code's status (the text is in the build output: `.pio/build/<env>/description/<hash>.txt`, written by `describe.py` from the same types, `src/air_tree.h`; give it as `Tree(link, descriptions=...)`; env `d1_mini_link_text` sends the text itself), `v` get by code (the code is its number under the description's hash, u32 hash then u8 number, or its name in the text build; the status first, then the value; a part that is not alive answers its last value), `w` set by code (through the node: its limits, its capture, its register), `n` the changes since the sequence number of the last reply (state codes once each with their value now, a row's status, and the events), `f` one
fault key (`x` resets the air sensor behind the host's back, `v` and `p` reset the RFID reader). State and events are kept apart: a state code
(temp, press, a register, a row's status) is one pending bit, so a consumer that does not read for a while gets each code once with its latest value
and nothing is lost; the card is an event and waits in a `fail::Buffer` of 8: when it is full the newest are refused and counted (`m.missed`). A reply
that is lost is seen by its sequence number, and the next one carries every code (`m.resyncs`).
`examples/spi/rig_session.py` is a session on the real board; `test/link/build.sh` runs the same consumer against a simulated one.

## Build and flash

Expand Down Expand Up @@ -113,6 +118,9 @@ first three bytes (cascade level 1 only).
answering goes Stale, is probed, and is initialised again when it answers; one that was reset without the sketch
knowing (its configuration read back is gone) is initialised again at once. `test/discover/spi_fail.cpp` runs both
against the register model with RST driven. `HealthT` is not composed here.
- A part that goes Gone (retries and probes spent) is looked for again every 5 s at its declared address, by the same entries
(`World::reviveGone()`, enabled by `revive = true` in the app): when it answers it takes its row again, its init runs, and the
value it was wanted to hold is written back. `test/discover/revive.cpp` runs it on the mock bus.
- The BMP280 on I2C has no failure edge: a supply disturbance that resets it leaves it in sleep mode, silent.

## Faults
Expand Down
85 changes: 85 additions & 0 deletions examples/spi/describe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// The description of the spi example's link, written at build time from the same types the firmware is built from (src/air_tree.h): the device
// answers 'd' with the hash only, and the consumer reads the text from here (python: Tree(link, descriptions=<dir>)).
// describe <dir> writes <dir>/<hash>.txt (the text without the status) and <dir>/facts.txt, and prints the first path
// facts.txt is what a wiring tool needs, from the same C++ (python/onemachine/wiring.py reads it): the board's pins and their facts (OneChip's
// Esp8266Pins), who drives the buses' lines, and each driver's manifest (bus, addresses, ids, its lines and who drives them, its nodes or event).
// The hash is FNV-1a over the text, folded at compile time by the walk the firmware folds (bmpm::HashWalk); this program checks the two agree.
// Built on the host: g++ -std=c++17 -I <the libraries' include dirs> describe.cpp (examples/spi/describe.py does it for PlatformIO).
#include <stdio.h>
#include <string>
#include "src/air_tree.h"

struct NoApp {}; // the description needs the machine's types only: no bus, no registry
using M = airTree::Machine<NoApp>;
static void noNote(int32_t) {}
template<typename Code> struct NoNote { static constexpr auto fn = &noNote; };
using Pubs = airTree::Pubs<M, NoNote>;
struct Text { std::string s; void operator()(char c) { s += c; } };

template<typename P> constexpr void describeAll(P& put) { bmpm::describeStatic<M, Pubs>(put, airTree::bus); airTree::describeStatic(put); }
constexpr uint32_t folded() { bmpm::Fnv f; describeAll(f); return f.h; }

// ---- facts.txt ------------------------------------------------------------------------------------------------------------------
using Board = airTree::Wiring::Board;
static void pinFact(std::string& o, const char* name, uint8_t g) {
o += std::string("pin ") + name + " " + std::to_string(g);
if (Board::strap(g)) o += Board::bootLevel(g) ? " strap high" : " strap low";
if (!Board::hasIrq(g)) o += " noirq";
o += "\n";
}
static std::string hex2(unsigned v) { char b[8]; snprintf(b, sizeof b, "0x%02X", v); return b; }
static std::string nm(state::Name n) { std::string r; for (unsigned i = 0; n.at(i); ++i) r += n.at(i); return r; }
template<typename N> static std::string kind() {
if constexpr (bmpm::IsGroup<N>::value) return "group";
else if constexpr (bmpm::HasReg<N>::value) return "reg";
else if constexpr (bmpm::HasConst<N>::value) return "const";
else if constexpr (bmpm::HasDecimals<N>::value) return "value " + std::to_string(N::decimals);
else return "value 0";
}
template<typename... C> static void children(std::string& o, const std::string& parent, unsigned at, hapi::Chain<C...>*) {
unsigned i = 0; ((o += "node bmp280 " + parent + "/" + nm(C::label()) + " " + std::to_string(at) + "/" + std::to_string(i++) + " " + kind<C>() + "\n"), ...);
}
template<typename... N> static void nodes(std::string& o, hapi::Chain<N...>*) {
unsigned i = 0;
((o += "node bmp280 " + nm(N::label()) + " " + std::to_string(i) + " " + kind<N>() + "\n",
[&] { if constexpr (bmpm::IsGroup<N>::value) children(o, nm(N::label()), i, static_cast<typename N::Body::Types*>(nullptr)); }(), ++i), ...);
}
static std::string facts() {
std::string o = std::string("board ") + airTree::Wiring::board + "\n";
const char* names[] = {"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8"};
const uint8_t gpios[] = {Board::D0, Board::D1, Board::D2, Board::D3, Board::D4, Board::D5, Board::D6, Board::D7, Board::D8};
for (unsigned i = 0; i < 9; ++i) pinFact(o, names[i], gpios[i]);
using discover::driveName; using I = discover::I2cLines; using S = discover::SpiLines;
o += std::string("bus i2c sda ") + driveName(I::sda) + " scl " + driveName(I::scl) + "\n";
o += std::string("bus spi sck ") + driveName(S::sck) + " miso " + driveName(S::miso) + " mosi " + driveName(S::mosi) + " cs " + driveName(S::cs) + "\n";
{ using F = bmpm::Manifest;
o += std::string("driver ") + F::name + " " + F::bus + " addrs";
for (uint8_t a : F::addrs) o += " " + hex2(a);
o += " ids"; for (uint8_t i : F::ids) o += " " + hex2(i);
o += "\n";
nodes(o, static_cast<M::Nodes*>(nullptr)); }
{ using F = rc522::Manifest;
o += std::string("driver ") + F::name + " " + F::bus + " ids";
for (unsigned i = 0; i < F::Id::count; ++i) o += " " + hex2(F::Id::list[i]);
o += "\n";
F::pins([&](const char* role, discover::Drive d) { o += std::string("line ") + F::name + " " + role + " " + driveName(d) + "\n"; });
o += std::string("event ") + F::name + " " + F::event + "\n"; }
return o;
}

int main(int argc, char** argv) {
if (argc != 2) { fprintf(stderr, "usage: describe <dir>\n"); return 2; }
Text t; describeAll(t);
uint32_t h = 2166136261u; for (char c : t.s) h = (h ^ uint8_t(c)) * 16777619u;
constexpr uint32_t want = folded();
if (h != want) { fprintf(stderr, "describe: the text hashes to %08x, the compile-time fold to %08x\n", unsigned(h), unsigned(want)); return 1; }
char path[4096]; snprintf(path, sizeof path, "%s/%08x.txt", argv[1], unsigned(h));
FILE* f = fopen(path, "w"); if (!f) { perror(path); return 1; }
fwrite(t.s.data(), 1, t.s.size(), f); fclose(f);
char fpath[4096]; snprintf(fpath, sizeof fpath, "%s/facts.txt", argv[1]);
const std::string fx = facts();
FILE* g = fopen(fpath, "w"); if (!g) { perror(fpath); return 1; }
fwrite(fx.data(), 1, fx.size(), g); fclose(g);
printf("%s\n", path);
return 0;
}
15 changes: 15 additions & 0 deletions examples/spi/describe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# PlatformIO post script: the link's description, written at build time from the same types the firmware is built from (describe.cpp, src/air_tree.h).
# The firmware answers 'd' with the hash; the text is <build dir>/description/<hash>.txt (python: Tree(link, descriptions=that dir)).
# Built with the host's g++ (CXX to choose another); the libraries are the firmware's, next to this repository (lib_extra_dirs = ../../..).
import os, subprocess
Import("env")
here = env.subst("$PROJECT_DIR")
root = os.path.normpath(os.path.join(here, "..", "..", ".."))
libs = ["OneMachine", "HAPI", "OneBus", "OneData", "OneMenu", "OneItem", "OneOutput", "OneBit", "OnePin", "OneChip", "OneParse", "OneInput", "OneIO"]
inc = [f for n in libs for f in ("-I", os.path.join(root, n, "include"))]
out = os.path.join(env.subst("$BUILD_DIR"), "description")
os.makedirs(out, exist_ok=True)
exe = os.path.join(out, "describe")
cxx = os.environ.get("CXX", "g++")
subprocess.run([cxx, "-std=c++17", "-O1"] + inc + [os.path.join(here, "describe.cpp"), "-o", exe], check=True)
print("description: " + subprocess.run([exe, out], check=True, capture_output=True, text=True).stdout.strip())
7 changes: 6 additions & 1 deletion examples/spi/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ monitor_speed = 115200
build_unflags = -std=c++11 -std=c++14 -std=gnu++11 -std=gnu++14
build_flags = -std=gnu++17 -Wall -Wextra
lib_extra_dirs = ../../..
extra_scripts = pre:../version.py
extra_scripts = pre:../version.py, post:describe.py

; the same sketch with the serial port carrying the link (link/frame.h) instead of the log: a consumer reads the description, gets and sets by code, reads the changes
[env:d1_mini_link]
extends = env:d1_mini
build_flags = ${env:d1_mini.build_flags} -DONEMACHINE_LINK

; the link build that sends its description as text ('d'), as before the description by hash: a consumer without the build output can read it
[env:d1_mini_link_text]
extends = env:d1_mini_link
build_flags = ${env:d1_mini_link.build_flags} -DONEMACHINE_DESC_TEXT
103 changes: 103 additions & 0 deletions examples/spi/src/air_tree.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// What the spi example publishes of its air sensor: the codes, the published nodes and the card's line, in one place. The sketch (main.cpp), the
// simulated device (test/link/tree_device.cpp), the AVR image (test/link/avr_tree.cpp) and the description generator (examples/spi/describe.cpp)
// all build them from here, so the description the generator writes is the one the firmware's hash is computed from.
// M the machine (bmpm::Machine<W, Criteria, Mode>)
// Note Note<Code>::fn: a constexpr pointer to what a published value calls when it changes (the App's: print it, mark it for the link, ...)
#pragma once
#include <stdint.h>
#include <hapi/hapi.h>
#include <chips/esp8266/esp8266Pins.h>
#include <oneMachine/discover/manifest.h>
#include "bmp280_machine.h"
#include "rc522.h"

namespace airTree {

// the codes, numbered by their position in the description (the link's change records use the numbers)
struct CodeTemp { static constexpr uint8_t num = 0; ONEMACHINE_STATE_NAME(name, "temp"); };
struct CodePress { static constexpr uint8_t num = 1; ONEMACHINE_STATE_NAME(name, "press"); };
struct CodeAir { static constexpr uint8_t num = 2; ONEMACHINE_STATE_NAME(name, "air"); };
struct CodeConfig { static constexpr uint8_t num = 3; ONEMACHINE_STATE_NAME(name, "air/config"); };
struct CodeCtrlMeas { static constexpr uint8_t num = 4; ONEMACHINE_STATE_NAME(name, "air/ctrl_meas"); };
struct CodeCard { static constexpr uint8_t num = 5; ONEMACHINE_STATE_NAME(name, "card"); };

// ---- the wiring: the rig's pins, the one place the sketch takes them from (experiment 6) ------------------------------------------------
// The board's pins and their facts are OneChip's (Esp8266Pins); who drives each line is the drivers' manifests' (rc522::Manifest, bmpm::Manifest)
// and the buses' (discover::I2cLines, SpiLines). The rules below are the wiring check at compile time; python/onemachine/wiring.py checks a
// wiring spec by the same facts, which describe.cpp prints from these types.
#ifdef AIRTREE_WIRING
#include AIRTREE_WIRING // a Wiring emitted from a wiring spec (python3 -m onemachine.wiring emit): the rules below judge it as they judge this one
#else
struct Wiring {
using Board = hw::esp8266::Esp8266Pins;
static constexpr const char* board = "wemos-d1-mini";
static constexpr uint8_t sda = Board::D2, scl = Board::D1; // I2C
static constexpr uint8_t sck = Board::D5, miso = Board::D6, mosi = Board::D7; // SPI (the ESP8266's HSPI pins)
static constexpr uint8_t rfidCs = Board::D8, emptyCs = Board::D3; // the SPI slots: the reader, and one declared with nothing on it
static constexpr uint8_t rfidRst = Board::D4, rfidIrq = Board::D0;
static constexpr uint8_t airAddr = 0x76;
};
#endif
// a line that the part drives (or shares) must not be on a pin the chip samples at reset; the MCU's own lines may be (D3 and D8 as chip selects)
template<uint8_t G, discover::Drive D> constexpr bool line() {
using B = Wiring::Board;
static_assert(B::gpio(G) && !B::flash(G), "wiring: not a GPIO of this board (GPIO6..GPIO11 are the flash)");
static_assert(discover::safeAtReset<B>(G, D), "wiring: a line the part drives is on a boot strap pin (GPIO0, GPIO2 or GPIO15: D3, D4, D8): "
"the part can hold it at reset (the RC522 keeps a pending IRQ across a reset of the board) and the ESP8266 boots into ROM mode");
return true;
}
static_assert(line<Wiring::sda, discover::I2cLines::sda>() && line<Wiring::scl, discover::I2cLines::scl>());
static_assert(line<Wiring::sck, discover::SpiLines::sck>() && line<Wiring::miso, discover::SpiLines::miso>() && line<Wiring::mosi, discover::SpiLines::mosi>());
static_assert(line<Wiring::rfidCs, rc522::Manifest::Pins::cs>() && line<Wiring::emptyCs, discover::SpiLines::cs>());
static_assert(line<Wiring::rfidRst, rc522::Manifest::Pins::rst>() && line<Wiring::rfidIrq, rc522::Manifest::Pins::irq>());

// the sensor: the machine at the wiring's address (its Criteria), under the App W's failure handling Mode
template<typename W, typename Mode = bmpm::Plain> using Machine = bmpm::Machine<W, bmpm::Addr<Wiring::airAddr>, Mode>;
constexpr uint8_t bus = 1; // the air sensor's bus is the App's second machine: its path codes start with 1

// temp and press notify on sync; air is the control group, config and ctrl_meas its registers (silent), each by its path in the machine
template<typename M, template<typename> class Note> using Pubs = hapi::Chain<
bmpm::PublishedAt<CodeTemp, bmpm::PathRef<M, 0>, oneData::OnSync<Note<CodeTemp>::fn>>,
bmpm::PublishedAt<CodePress, bmpm::PathRef<M, 1>, oneData::OnSync<Note<CodePress>::fn>>,
bmpm::PublishedAt<CodeAir, bmpm::PathRef<M, 3>>,
bmpm::PublishedAt<CodeConfig, bmpm::PathRef<M, 3, 0>>,
bmpm::PublishedAt<CodeCtrlMeas, bmpm::PathRef<M, 3, 1>>>;

// the card: a code that only notifies (an event with a value, the UID, 0 when it leaves). The App's text line is cardText then the status of the
// reader's row; the description's static line (its hash, the build output) is cardText without the trailing " status ".
// (a macro, so an App's text path uses it as a literal of its own, as before: the text build is the same image to the byte)
#define AIRTREE_CARD_TEXT " card -> 0/1 notify event ro value u32 status "
inline constexpr char cardText[] = AIRTREE_CARD_TEXT;
template<typename P> constexpr void cardLine(P& put) { for (unsigned i = 0; i + 8 < sizeof cardText - 1; ++i) put(cardText[i]); put('\n'); }

// ---- the wiring lines of the description (the build output's, by hash: they cost the device nothing) -----------------------------------
// wiring wemos-d1-mini
// i2c sda 4 shared scl 5 mcu
// spi sck 14 mcu miso 12 device mosi 13 mcu
// rfid rc522 cs 15 mcu rst 2 mcu irq 16 device
// empty cs 0 mcu
// air bmp280 at 0x76
// a part's lines: the GPIO the wiring gives each one, and who drives it (its manifest)
template<typename P> struct WiringPut {
P& put;
constexpr void str(const char* s) { while (*s) put(*s++); }
constexpr void dec(unsigned v) { char b[4] = {}; unsigned k = 0; do { b[k++] = char('0' + v % 10); v /= 10; } while (v); while (k) put(b[--k]); }
constexpr void hex(uint8_t v) { const uint8_t h = uint8_t(v >> 4), l = uint8_t(v & 15); str("0x"); put(char(h < 10 ? '0' + h : 'A' + h - 10)); put(char(l < 10 ? '0' + l : 'A' + l - 10)); }
constexpr void pin(const char* role, uint8_t g, discover::Drive d) { put(' '); str(role); put(' '); dec(g); put(' '); str(discover::driveName(d)); }
};
template<typename P> constexpr void wiringLines(P& put) {
using W = Wiring;
WiringPut<P> w{put};
w.str("wiring "); w.str(W::board); put('\n');
w.str(" i2c"); w.pin("sda", W::sda, discover::I2cLines::sda); w.pin("scl", W::scl, discover::I2cLines::scl); put('\n');
w.str(" spi"); w.pin("sck", W::sck, discover::SpiLines::sck); w.pin("miso", W::miso, discover::SpiLines::miso); w.pin("mosi", W::mosi, discover::SpiLines::mosi); put('\n');
w.str(" rfid "); w.str(rc522::Manifest::name);
using RP = rc522::Manifest::Pins;
w.pin("cs", W::rfidCs, RP::cs); w.pin("rst", W::rfidRst, RP::rst); w.pin("irq", W::rfidIrq, RP::irq);
put('\n');
w.str(" empty"); w.pin("cs", W::emptyCs, discover::SpiLines::cs); put('\n');
w.str(" air "); w.str(bmpm::Manifest::name); w.str(" at "); w.hex(W::airAddr); put('\n');
}
// the App's static tail of the description: the card's line, then the wiring
template<typename P> constexpr void describeStatic(P& put) { cardLine(put); wiringLines(put); }
}
Loading
Loading