From 1fe5b756c5fb440ed7754d43c1c38a090b3651a5 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 02:23:58 +0000 Subject: [PATCH 01/19] examples/spi: RC522 IRQ line on D0, sampled; supply switch removed 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 --- examples/spi/README.md | 13 ++++---- examples/spi/src/main.cpp | 70 +++++++++++++++++++++++++++++---------- examples/spi/src/rc522.h | 31 +++++++++++++++-- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/examples/spi/README.md b/examples/spi/README.md index 79ef831..bd2bb6f 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -20,15 +20,17 @@ Wemos D1 mini (ESP8266), everything at 3.3V. | D6 (GPIO12) | MISO | | | D7 (GPIO13) | MOSI | | | D8 (GPIO15) | SDA (its chip select) | | -| D0 or 3V3 | RST | D0 is held high by the sketch; it must not be a chip select | +| D4 (GPIO2) | RST | held high by the sketch (the fault keys pull it low); or 3V3 | +| D0 (GPIO16) | IRQ | | | D2 (GPIO4) | | SDA | | D1 (GPIO5) | | SCL | Slot 1 is D3 (GPIO0) with nothing on it: the scan reports it empty. -Optional: D4 (GPIO2) is held low as a supply switch for the RC522's VCC (a PNP transistor, for example a 2N2907: emitter -to 3V3, collector to VCC, base through 1 k to D4; D4 is high at reset, so the RC522 is off until the sketch starts). -Keys `x` and `l` below cut it for 3 s or 30 s. Without a switch there, they change nothing. +D0 (GPIO16) is the RC522's IRQ input (push-pull, active low, set by the driver). The receive wait of each poll is this line instead +of reads of ComIrqReg; the ESP8266 has no interrupt on GPIO16, so it is sampled, and ComIrqReg then tells RxIRq from TimerIRq. Requests +are enabled only while a command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8): the RC522 keeps its state across a reset +of the board, and a pending request would hold such a pin low at the next boot. ## Build and flash @@ -69,13 +71,12 @@ first three bytes (cascade level 1 only). ## Faults -Type a key in the serial monitor to drive RC522 RST (D0) from the sketch: +Type a key in the serial monitor to drive RC522 RST (D4) from the sketch: | Key | Fault | Log | | --- | --- | --- | | `v` | RST low for 3 s: the reader vanishes | `rfid[1] stale`, `card[1]=0` if a card was held, then `rfid[1] alive, init #n` after release | | `p` | RST low for 1 ms: a silent reset, the ID still answers | `rfid[1] reinit, init #n`, the row stays Alive | -| `x`, `l` | supply off for 3 s, or 30 s (with the switch on D4) | as `v`; the chip may still answer through its signal pins, so it can flap before it goes quiet | A card that picks a new UID each time its field restarts (random-UID tags, phones; UIDs starting `08`) shows a new UID after every fault. diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 4847600..99cf93d 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -1,11 +1,13 @@ // spi -- discovery on two buses of one board: an RC522 RFID reader found on an SPI bus whose chip selects are declared // statically (one slot each), and a BMP280/BME280 found on I2C. Each bus is its own World: discover once, pump on its // own period. Wemos D1 mini (ESP8266), Serial 115200. Wiring: -// RC522 SCK D5, MISO D6, MOSI D7, SDA (its CS) D8, RST D0 (held high here) or 3V3, 3V3, GND +// RC522 SCK D5, MISO D6, MOSI D7, SDA (its CS) D8, RST D4 (held high here) or 3V3, IRQ D0, 3V3, GND // BMP280 SDA D2, SCL D1, 3V3, GND (CSB high or open: I2C mode) // Slot 1 (D3) is declared with nothing on it: the scan reports it empty. -// Optional supply switch (a PNP transistor, base through 1k): D4 drives it, low = RC522 VCC on (D4 is -// high at reset, as boot and flashing need). Without one, key x does nothing visible. +// D0 (GPIO16) is the RC522's IRQ input: the receive wait of each poll is the IRQ line instead of reads of ComIrqReg. The +// ESP8266 has no interrupt on GPIO16, so the line is sampled in transceive()'s wait; the RC522 drives it (push-pull, active +// low), then ComIrqReg tells RxIRq from TimerIRq. Not on a boot strapping pin (D3, D4, D8) because the RC522 keeps its state +// across a reset of the board and a pending request would hold the line low at the next boot. // // Line format: []= a card's UID in hex when one arrives, 0 when it leaves (after 3 polls // without it, or when the reader stops answering). miss[1]=: polls in a row that found no card while one is held. @@ -18,8 +20,7 @@ // row: one that keeps flapping is quarantined (not polled) for a growing time. Faults, from the serial monitor: // v RST low for 3 s: the reader vanishes // p RST low for 1 ms: a silent reset, the reader still answers and has lost its configuration -// x supply off for 3 s (needs the switch on D4) -// l supply off for 30 s +// IRQ rx=R tmo=T spurious=S missed=M none=N low@arm=L 0x: ... every 5 s, the IRQ line against ComIrqReg #include #undef bit // Arduino's bit(b) macro; fail:: has its own bit(Kind) #include @@ -67,6 +68,43 @@ struct Printer { }; }; +// The RC522 IRQ line, sampled. Active low (IRqInv=1), RxIRq and TimerIRq enabled while a command runs. The counters tell the +// line from ComIrqReg: rx/tmo: the line fell and the register agrees (a card answered / the timer ran out); spurious: it fell +// but the register shows neither; missed: the register shows one but the line never fell; none: neither. +struct IrqFlag { + static constexpr bool on = true; + static constexpr uint8_t enable = 0x80 | 0x20 | 0x01; // IRqInv, RxIEn, TimerIEn: while a command runs + static constexpr uint8_t idle = 0x80; // IRqInv only: the line stays high between polls + static constexpr uint8_t pushPull = 0x80; // DivIEnReg IRQPushPull: D0 has no pull-up + static constexpr uint8_t irqPin = 16; // D0 + static inline uint32_t rx = 0, tmo = 0, spurious = 0, missed = 0, none = 0, low0 = 0; + static inline uint8_t vals[8] = {}; static inline uint32_t cnt[8] = {}; + static void arm() { if (!digitalRead(irqPin)) ++low0; } // low here: a request left over, the line was not released + static bool wait() { + const uint32_t t = millis(); + while (digitalRead(irqPin) && millis() - t < 40) yield(); + return !digitalRead(irqPin); + } + static void seen(uint8_t irq, bool fired) { + const uint8_t hit = irq & 0x21; + if (fired) { if (!hit) ++spurious; else if (irq & 0x20) ++rx; else ++tmo; } + else if (hit) ++missed; else ++none; + for (uint8_t i = 0; i < 8; ++i) { + if (cnt[i] && vals[i] == irq) { ++cnt[i]; return; } + if (!cnt[i]) { vals[i] = irq; cnt[i] = 1; return; } + } + } + static void report(uint32_t now) { + Serial.print(F("IRQ ")); Serial.print(now); + Serial.print(F(" rx=")); Serial.print(rx); Serial.print(F(" tmo=")); Serial.print(tmo); + Serial.print(F(" spurious=")); Serial.print(spurious); Serial.print(F(" missed=")); Serial.print(missed); + Serial.print(F(" none=")); Serial.print(none); + Serial.print(F(" low@arm=")); Serial.print(low0); + for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Serial.print(F(" 0x")); Serial.print(vals[i], HEX); Serial.print(':'); Serial.print(cnt[i]); } + Serial.println(); + } +}; + struct RfidApp; struct AirApp; @@ -75,6 +113,7 @@ struct RfidMode { template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; template using Access = fail::SpiAccess; + using Irq = IrqFlag; }; using Rfid = rc522::Rc522; using RfidDrivers = discover::DriversIn>; @@ -87,8 +126,7 @@ struct RfidApp : discover::World, Chain, 3, d using Health = fail::HealthT; }; using RfidTicker = fail::Ticks; -constexpr uint8_t rstPin = 16; // D0: the RC522's RST -constexpr uint8_t pwrPin = 2; // D4: the RC522's supply switch, active low +constexpr uint8_t rstPin = 2; // D4: the RC522's RST struct AirApp : discover::World, bmp::Entries, 3, discover::I2cScan> {}; template static void table(const __FlashStringHelper* bus) { @@ -103,8 +141,8 @@ void setup() { delay(200); Serial.println(F("\nOneMachine SPI + I2C discovery")); Serial.println(F("build " BUILD_REV " " __DATE__ " " __TIME__)); - esp::OutPin::begin(); esp::OutPin::off(); // RC522 supply on (low), when switched - delay(50); // let the module come up before discovery + pinMode(IrqFlag::irqPin, INPUT); + Serial.print(F("reset: ")); Serial.print(ESP.getResetReason()); Serial.print(F(", IRQ pin reads ")); Serial.println(digitalRead(IrqFlag::irqPin)); esp::OutPin::begin(); esp::OutPin::on(); // RC522 RST high; a chip select on this pin would reset it Twi::begin(); Spi::begin(); @@ -128,6 +166,10 @@ void setup() { Rfid::wr(1, rc522::TReloadRegL, 0x5A); Serial.print(F("), write test: TReloadRegL 0x5A reads 0x")); Serial.println(Rfid::rd(1, rc522::TReloadRegL), HEX); Rfid::wr(1, rc522::TReloadRegL, 0xE8); + Serial.print(F("RC522 IRQ: ComIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIEnReg), HEX); + Serial.print(F(" DivIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::DivIEnReg), HEX); + Serial.print(F(" ComIrqReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIrqReg), HEX); + Serial.print(F(", IRQ pin reads ")); Serial.println(digitalRead(IrqFlag::irqPin)); } } @@ -167,22 +209,14 @@ static void logRfid() { static void faults(uint32_t now) { static uint32_t vanishEnd = 0; static bool vanished = false; - static uint32_t unpoweredEnd = 0; - static bool unpowered = false; if (Serial.available()) { const int c = Serial.read(); if (c == 'v' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST low 3 s")); esp::OutPin::off(); vanished = true; vanishEnd = now + 3000; } - else if ((c == 'x' || c == 'l') && !unpowered) { - const uint32_t ms = c == 'x' ? 3000 : 30000; - Serial.print(now); Serial.print(F(" fault: supply off ")); Serial.print(ms / 1000); Serial.println(F(" s")); - esp::OutPin::on(); unpowered = true; unpoweredEnd = now + ms; - } else if (c == 'p' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST pulse")); esp::OutPin::off(); delayMicroseconds(1000); esp::OutPin::on(); } } - if (unpowered && int32_t(now - unpoweredEnd) >= 0) { esp::OutPin::off(); unpowered = false; Serial.print(now); Serial.println(F(" fault: supply on")); } if (vanished && int32_t(now - vanishEnd) >= 0) { esp::OutPin::on(); vanished = false; Serial.print(now); Serial.println(F(" fault: RST high")); } } @@ -202,4 +236,6 @@ void loop() { logRfid(); logHealth(); if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); } + static uint32_t nextIrq = 5000; + if (int32_t(now - nextIrq) >= 0) { nextIrq = now + 5000; IrqFlag::report(now); } } diff --git a/examples/spi/src/rc522.h b/examples/spi/src/rc522.h index e015187..cd97648 100644 --- a/examples/spi/src/rc522.h +++ b/examples/spi/src/rc522.h @@ -22,7 +22,7 @@ namespace rc522 { struct Card { using Value = uint32_t; static constexpr uint8_t id = 20, decimals = 0; static constexpr const char* name = "card"; }; enum Reg : uint8_t { - CommandReg = 0x01, ComIrqReg = 0x04, ErrorReg = 0x06, FIFODataReg = 0x09, FIFOLevelReg = 0x0A, + CommandReg = 0x01, ComIEnReg = 0x02, DivIEnReg = 0x03, ComIrqReg = 0x04, ErrorReg = 0x06, FIFODataReg = 0x09, FIFOLevelReg = 0x0A, ControlReg = 0x0C, BitFramingReg = 0x0D, CollReg = 0x0E, ModeReg = 0x11, TxControlReg = 0x14, TxASKReg = 0x15, TModeReg = 0x2A, TPrescalerReg = 0x2B, TReloadRegH = 0x2C, TReloadRegL = 0x2D, VersionReg = 0x37, }; @@ -36,6 +36,22 @@ namespace rc522 { static_assert(enterQ > exitQ, "Health: enter threshold must exceed exit threshold (hysteresis)"); }; + // The IRQ pin, optional: a mode that has `using Irq = X` makes transceive() wait on X::wait() instead of polling ComIrqReg. + // X::pushPull is written to DivIEnReg at init. X::enable is written to ComIEnReg once the command's requests are cleared and X::idle (IRqInv only) when it is done, with the + // requests cleared again: the line is low only while a command is in flight, never while the chip waits for the next poll + // (a pin shared with a boot strapping input must be high across a host reset). X::arm() runs before each command is started, + // X::wait() returns true when the line fired (an interrupt flag, or a sampled level), X::seen(irq, fired) is told the ComIrqReg value read afterwards. + struct NoIrq { + static constexpr bool on = false; + static constexpr uint8_t enable = 0, idle = 0, pushPull = 0; + static void arm() {} + static bool wait() { return false; } + static void seen(uint8_t, bool) {} + }; + template using Void = void; + template struct IrqOf { using type = NoIrq; }; + template struct IrqOf> { using type = typename M::Irq; }; + // no failure handling: the driver polls and nothing is retried, probed or reported struct NoFail { static constexpr bool lifecycle = false, returnPath = false, idempotent = true; @@ -47,6 +63,7 @@ namespace rc522 { struct Rc522 : discover::SpiDriverBase, W>, fail::DevEdge, W, M, K> { using B = discover::SpiDriverBase; using Edge = fail::DevEdge; + using Irq = typename IrqOf::type; using Produces = hapi::Chain; static constexpr bool mayIsolate = true; // a health monitor may quarantine the row (it has no isolate(): nothing cuts its supply) static constexpr uint8_t recoverMask = fail::bit(fail::Kind::Corrupt); // a reset the host did not see: init again @@ -99,6 +116,7 @@ namespace rc522 { wr(row, TxASKReg, 0x40); // 100% ASK wr(row, ModeReg, 0x3D); // CRC preset 0x6363 wr(row, TxControlReg, uint8_t(rd(row, TxControlReg) | 0x03)); // antenna on + if constexpr (Irq::on) wr(row, DivIEnReg, Irq::pushPull); // IRQ pin driven, not open-drain if (configured(row)) { B::dev(row).initTries = uint16_t(i + 1); return; } } } @@ -108,13 +126,22 @@ namespace rc522 { static int8_t transceive(RowId row, const uint8_t* tx, uint8_t n, uint8_t* rx, uint8_t max, uint8_t lastBits) { wr(row, CommandReg, Idle); wr(row, ComIrqReg, 0x7F); // clear every interrupt request bit + if constexpr (Irq::on) { Irq::arm(); wr(row, ComIEnReg, Irq::enable); } wr(row, FIFOLevelReg, 0x80); // flush the FIFO for (uint8_t i = 0; i < n; ++i) wr(row, FIFODataReg, tx[i]); wr(row, BitFramingReg, lastBits); wr(row, CommandReg, Transceive); wr(row, BitFramingReg, uint8_t(lastBits | 0x80)); // StartSend uint8_t irq = 0; - for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (irq & 0x31) break; } // RxIRq, IdleIRq, TimerIRq + if constexpr (Irq::on) { + const bool fired = Irq::wait(); + irq = rd(row, ComIrqReg); + wr(row, ComIEnReg, Irq::idle); // line released before the requests are cleared + wr(row, ComIrqReg, 0x7F); + Irq::seen(irq, fired); + } else { + for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (irq & 0x31) break; } // RxIRq, IdleIRq, TimerIRq + } wr(row, BitFramingReg, 0x00); if (!(irq & 0x30)) return 0; // the timer ran out (or the loop did): no card answered if (rd(row, ErrorReg) & 0x1B) return -1; // BufferOvfl, CollErr, ParityErr, ProtocolErr From cef137b85abae465f432dfa9782d489cbebde5c2 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 03:29:07 +0000 Subject: [PATCH 02/19] rc522: a command in two halves, with the IRQ line as an interrupt part 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 (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 --- examples/spi/README.md | 10 +- examples/spi/src/irq_esp8266.h | 33 +++++ examples/spi/src/main.cpp | 69 +++++----- examples/spi/src/rc522.h | 204 +++++++++++++++++++++------- test/discover/build_spi1.sh | 21 +++ test/discover/irq_delivery.cpp | 29 ++++ test/discover/spi_irq.cpp | 178 ++++++++++++++++++++++++ test/support/arduino_stub/Arduino.h | 11 ++ test/support/mockSpi.h | 11 +- 9 files changed, 484 insertions(+), 82 deletions(-) create mode 100644 examples/spi/src/irq_esp8266.h create mode 100644 test/discover/irq_delivery.cpp create mode 100644 test/discover/spi_irq.cpp create mode 100644 test/support/arduino_stub/Arduino.h diff --git a/examples/spi/README.md b/examples/spi/README.md index bd2bb6f..b910763 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -27,10 +27,12 @@ Wemos D1 mini (ESP8266), everything at 3.3V. Slot 1 is D3 (GPIO0) with nothing on it: the scan reports it empty. -D0 (GPIO16) is the RC522's IRQ input (push-pull, active low, set by the driver). The receive wait of each poll is this line instead -of reads of ComIrqReg; the ESP8266 has no interrupt on GPIO16, so it is sampled, and ComIrqReg then tells RxIRq from TimerIRq. Requests -are enabled only while a command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8): the RC522 keeps its state across a reset -of the board, and a pending request would hold such a pin low at the next boot. +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 `Rfid::service()` 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` sets a +flag in the ISR instead. The RxIRq/TimerIRq bits tell a card from none, and a line that disagrees with the register is reported as a +device failure. Requests are enabled only while a command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8, rejected at +compile time): the RC522 keeps its state across a reset of the board, and a pending request would hold such a pin low at the next boot. ## Build and flash diff --git a/examples/spi/src/irq_esp8266.h b/examples/spi/src/irq_esp8266.h new file mode 100644 index 0000000..400e0c7 --- /dev/null +++ b/examples/spi/src/irq_esp8266.h @@ -0,0 +1,33 @@ +// How an interrupt line reaches the loop on an ESP8266 (the Delivery of rc522::Interrupt, or any driver's interrupt part): +// Sampled the line is read where the loop asks; for a pin without an interrupt (GPIO16) +// IsrFlag a falling edge sets a flag in the ISR, which does nothing else; the loop reads the flag +// ready() is true while the line is asserted (low); arm() runs before the source is enabled. +// GPIO0, GPIO2 and GPIO15 (D3, D4, D8) are sampled by the chip at reset to choose the boot mode: a source that keeps its request +// across a reset of the board and drives one of them low stops the next boot. +#pragma once +#include + +namespace irq { + + template struct Esp8266Line { + static_assert(Pin <= 16, "an ESP8266 has GPIO0 to GPIO16"); + static_assert(Pin != 0 && Pin != 2 && Pin != 15, + "an interrupt line on GPIO0, GPIO2 or GPIO15 (D3, D4, D8): a request pending at reset holds the pin low and the board does not boot"); + }; + + template struct Sampled : Esp8266Line { + static void begin() { pinMode(Pin, INPUT); } + static void arm() {} + static bool ready() { return !digitalRead(Pin); } + }; + + template struct IsrFlag : Esp8266Line { + static_assert(Pin != 16, "GPIO16 has no interrupt: use Sampled"); + static inline volatile bool flag = false; + static void IRAM_ATTR isr() { flag = true; } + static void begin() { pinMode(Pin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(Pin), isr, FALLING); } + static void arm() { flag = false; } + static bool ready() { return flag; } + }; + +} diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 99cf93d..268cee7 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -4,10 +4,12 @@ // RC522 SCK D5, MISO D6, MOSI D7, SDA (its CS) D8, RST D4 (held high here) or 3V3, IRQ D0, 3V3, GND // BMP280 SDA D2, SCL D1, 3V3, GND (CSB high or open: I2C mode) // Slot 1 (D3) is declared with nothing on it: the scan reports it empty. -// D0 (GPIO16) is the RC522's IRQ input: the receive wait of each poll is the IRQ line instead of reads of ComIrqReg. The -// ESP8266 has no interrupt on GPIO16, so the line is sampled in transceive()'s wait; the RC522 drives it (push-pull, active -// low), then ComIrqReg tells RxIRq from TimerIRq. Not on a boot strapping pin (D3, D4, D8) because the RC522 keeps its state -// across a reset of the board and a pending request would hold the line low at the next boot. +// D0 (GPIO16) is the RC522's IRQ input. A poll starts its command and returns; the loop finishes it (Rfid::service) 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); where the pin has one, irq::IsrFlag sets a flag in the ISR instead. The chip drives the line +// (push-pull, active low); ComIrqReg then tells RxIRq (a card answered) from TimerIRq (none did), and a line that disagrees with +// the register is a device failure. Not on a boot strapping pin (D3, D4, D8): the RC522 keeps its state across a reset of the +// board and a pending request would hold the line low at the next boot. // // Line format: []= a card's UID in hex when one arrives, 0 when it leaves (after 3 polls // without it, or when the reader stops answering). miss[1]=: polls in a row that found no card while one is held. @@ -20,7 +22,8 @@ // row: one that keeps flapping is quarantined (not polled) for a growing time. Faults, from the serial monitor: // v RST low for 3 s: the reader vanishes // p RST low for 1 ms: a silent reset, the reader still answers and has lost its configuration -// IRQ rx=R tmo=T spurious=S missed=M none=N low@arm=L 0x: ... every 5 s, the IRQ line against ComIrqReg +// IRQ rx=R tmo=T spurious=S missed=M none=N 0x: ... every 5 s, the IRQ line against ComIrqReg +// LOOP n= max= pump= svc= every 5 s, the loop's iterations and the longest iteration, pump() and service() #include #undef bit // Arduino's bit(b) macro; fail:: has its own bit(Kind) #include @@ -33,6 +36,7 @@ #include #include #include "rc522.h" +#include "irq_esp8266.h" #include "bmp280.h" #ifndef BUILD_REV @@ -68,26 +72,15 @@ struct Printer { }; }; -// The RC522 IRQ line, sampled. Active low (IRqInv=1), RxIRq and TimerIRq enabled while a command runs. The counters tell the -// line from ComIrqReg: rx/tmo: the line fell and the register agrees (a card answered / the timer ran out); spurious: it fell -// but the register shows neither; missed: the register shows one but the line never fell; none: neither. -struct IrqFlag { - static constexpr bool on = true; - static constexpr uint8_t enable = 0x80 | 0x20 | 0x01; // IRqInv, RxIEn, TimerIEn: while a command runs - static constexpr uint8_t idle = 0x80; // IRqInv only: the line stays high between polls - static constexpr uint8_t pushPull = 0x80; // DivIEnReg IRQPushPull: D0 has no pull-up - static constexpr uint8_t irqPin = 16; // D0 - static inline uint32_t rx = 0, tmo = 0, spurious = 0, missed = 0, none = 0, low0 = 0; - static inline uint8_t vals[8] = {}; static inline uint32_t cnt[8] = {}; - static void arm() { if (!digitalRead(irqPin)) ++low0; } // low here: a request left over, the line was not released - static bool wait() { - const uint32_t t = millis(); - while (digitalRead(irqPin) && millis() - t < 40) yield(); - return !digitalRead(irqPin); - } - static void seen(uint8_t irq, bool fired) { +// Rig diagnostics: the IRQ line against ComIrqReg after every command. rx/tmo: the line fell and the register agrees (a card +// answered / the timer ran out); spurious: it fell but the register shows nothing; missed: the register shows a request and the +// line never fell; none: neither. +struct IrqCounters { + inline static uint32_t rx = 0, tmo = 0, spurious = 0, missed = 0, none = 0; + inline static uint8_t vals[8] = {}; inline static uint32_t cnt[8] = {}; + static void seen(uint8_t irq, bool line, const fail::Outcome&) { const uint8_t hit = irq & 0x21; - if (fired) { if (!hit) ++spurious; else if (irq & 0x20) ++rx; else ++tmo; } + if (line) { if (!hit) ++spurious; else if (irq & 0x20) ++rx; else ++tmo; } else if (hit) ++missed; else ++none; for (uint8_t i = 0; i < 8; ++i) { if (cnt[i] && vals[i] == irq) { ++cnt[i]; return; } @@ -99,7 +92,6 @@ struct IrqFlag { Serial.print(F(" rx=")); Serial.print(rx); Serial.print(F(" tmo=")); Serial.print(tmo); Serial.print(F(" spurious=")); Serial.print(spurious); Serial.print(F(" missed=")); Serial.print(missed); Serial.print(F(" none=")); Serial.print(none); - Serial.print(F(" low@arm=")); Serial.print(low0); for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Serial.print(F(" 0x")); Serial.print(vals[i], HEX); Serial.print(':'); Serial.print(cnt[i]); } Serial.println(); } @@ -113,7 +105,7 @@ struct RfidMode { template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; template using Access = fail::SpiAccess; - using Irq = IrqFlag; + using Irq = rc522::Interrupt, IrqCounters>; // D0 }; using Rfid = rc522::Rc522; using RfidDrivers = discover::DriversIn>; @@ -141,8 +133,8 @@ void setup() { delay(200); Serial.println(F("\nOneMachine SPI + I2C discovery")); Serial.println(F("build " BUILD_REV " " __DATE__ " " __TIME__)); - pinMode(IrqFlag::irqPin, INPUT); - Serial.print(F("reset: ")); Serial.print(ESP.getResetReason()); Serial.print(F(", IRQ pin reads ")); Serial.println(digitalRead(IrqFlag::irqPin)); + Rfid::Irq::begin(); + Serial.print(F("reset: ")); Serial.print(ESP.getResetReason()); Serial.print(F(", IRQ line ")); Serial.println(Rfid::Irq::line() ? F("low") : F("high")); esp::OutPin::begin(); esp::OutPin::on(); // RC522 RST high; a chip select on this pin would reset it Twi::begin(); Spi::begin(); @@ -169,7 +161,7 @@ void setup() { Serial.print(F("RC522 IRQ: ComIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIEnReg), HEX); Serial.print(F(" DivIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::DivIEnReg), HEX); Serial.print(F(" ComIrqReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIrqReg), HEX); - Serial.print(F(", IRQ pin reads ")); Serial.println(digitalRead(IrqFlag::irqPin)); + Serial.print(F(", IRQ line ")); Serial.println(Rfid::Irq::line() ? F("low") : F("high")); } } @@ -220,12 +212,27 @@ static void faults(uint32_t now) { if (vanished && int32_t(now - vanishEnd) >= 0) { esp::OutPin::on(); vanished = false; Serial.print(now); Serial.println(F(" fault: RST high")); } } +// loop timing, rig diagnostics: iterations since the last report and the longest iteration, pump() and service() (microseconds) +struct LoopStats { + inline static uint32_t n = 0, last = 0, maxLoop = 0, maxPump = 0, maxSvc = 0; + static void lap(uint32_t us) { if (last && us - last > maxLoop) maxLoop = us - last; last = us; ++n; } + static void note(uint32_t& m, uint32_t from) { const uint32_t d = micros() - from; if (d > m) m = d; } + static void report(uint32_t now) { + Serial.print(F("LOOP ")); Serial.print(now); Serial.print(F(" n=")); Serial.print(n); Serial.print(F(" max=")); Serial.print(maxLoop); + Serial.print(F(" pump=")); Serial.print(maxPump); Serial.print(F(" svc=")); Serial.println(maxSvc); + n = 0; maxLoop = maxPump = maxSvc = 0; + } +}; + void loop() { static uint32_t nextCard = 0, nextAir = 0; + LoopStats::lap(micros()); const uint32_t now = millis(); faults(now); + if (RfidApp::reg.count > 1) { const uint32_t t = micros(); Rfid::service(1, now); LoopStats::note(LoopStats::maxSvc, t); } if (int32_t(now - nextCard) >= 0) { - nextCard = now + 100; RfidApp::pump(); + nextCard = now + 100; + { const uint32_t t = micros(); RfidApp::pump(); LoopStats::note(LoopStats::maxPump, t); } static uint8_t lastMiss = 0; // a card held still that misses polls: the streak, printed when it grows const uint8_t miss = RfidApp::reg.count > 1 ? RfidApp::devState(1).missStreak : 0; if (miss != lastMiss) { lastMiss = miss; if (miss) { Serial.print(millis()); Serial.print(F(" miss[1]=")); Serial.println(miss); } } @@ -237,5 +244,5 @@ void loop() { logHealth(); if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); } static uint32_t nextIrq = 5000; - if (int32_t(now - nextIrq) >= 0) { nextIrq = now + 5000; IrqFlag::report(now); } + if (int32_t(now - nextIrq) >= 0) { nextIrq = now + 5000; IrqCounters::report(now); LoopStats::report(now); } } diff --git a/examples/spi/src/rc522.h b/examples/spi/src/rc522.h index cd97648..4913569 100644 --- a/examples/spi/src/rc522.h +++ b/examples/spi/src/rc522.h @@ -36,17 +36,63 @@ namespace rc522 { static_assert(enterQ > exitQ, "Health: enter threshold must exceed exit threshold (hysteresis)"); }; - // The IRQ pin, optional: a mode that has `using Irq = X` makes transceive() wait on X::wait() instead of polling ComIrqReg. - // X::pushPull is written to DivIEnReg at init. X::enable is written to ComIEnReg once the command's requests are cleared and X::idle (IRqInv only) when it is done, with the - // requests cleared again: the line is low only while a command is in flight, never while the chip waits for the next poll - // (a pin shared with a boot strapping input must be high across a host reset). X::arm() runs before each command is started, - // X::wait() returns true when the line fired (an interrupt flag, or a sampled level), X::seen(irq, fired) is told the ComIrqReg value read afterwards. + // ---- the RC522's interrupt part ------------------------------------------------------------------------------ + // A mode that has `using Irq = rc522::Interrupt` makes a command wait on the IRQ line instead of polling ComIrqReg: the + // poll starts the command and returns, and service() finishes it when the line is asserted, or after Interrupt::timeoutMs. The register values + // (what is enabled while a command runs, the pin driven push-pull) belong to the chip and are here; the App chooses the pin and how the line + // reaches the loop: + // Delivery begin(), arm() (before a command starts), ready() (the line is asserted) + // Observer seen(irq, line, outcome): rig diagnostics, after every command + // The requests are enabled only while a command runs and cleared after it: the chip keeps its state across a reset of the host, and an + // enabled pending request would hold the line low at the next boot (a boot strapping pin must be high then). + struct NoObserver { static void seen(uint8_t, bool, const fail::Outcome&) {} }; + + // what the ComIrqReg bits mean for a Transceive: RxIRq or IdleIRq, a card answered; TimerIRq alone, nobody did + struct IrqMeaning { + static constexpr uint8_t rxIrq = 0x20, idleIrq = 0x10, timerIrq = 0x01; + static constexpr bool answered(uint8_t irq) { return (irq & (rxIrq | idleIrq)) != 0; } + static constexpr bool finished(uint8_t irq) { return (irq & (rxIrq | idleIrq | timerIrq)) != 0; } + }; + + // The line against the register after a command, as a device failure: Fault with detail 1 when the line is asserted and the register shows no + // request that was enabled (spurious), 2 when it shows one and the line never was (the line is not connected, or not driven); Timeout when + // neither (the chip's own timer did not end the command). + struct LineCheck { + static constexpr uint8_t spurious = 1, missed = 2; + static fail::Outcome of(uint8_t irq, bool line) { + const bool hit = IrqMeaning::finished(irq); + if (line && !hit) return fail::Outcome::Fail(fail::Kind::Fault, spurious); + if (!line && hit) return fail::Outcome::Fail(fail::Kind::Fault, missed); + if (!line && !hit) return fail::Outcome::Fail(fail::Kind::Timeout, irq); + return fail::Outcome::Ok(); + } + }; + + template + struct Interrupt { + static constexpr bool on = true; + static constexpr uint8_t enable = 0x80 | IrqMeaning::rxIrq | IrqMeaning::timerIrq; // ComIEnReg while a command runs: IRqInv, RxIEn, TimerIEn + static constexpr uint8_t idle = 0x80; // ComIEnReg between commands: IRqInv only + static constexpr uint8_t pushPull = 0x80; // DivIEnReg IRQPushPull: the chip drives the line + static constexpr uint16_t timeoutMs = 40; // the chip's own timer ends a command in 25 ms + static void begin() { Delivery::begin(); } + static void arm() { Delivery::arm(); } + static bool line() { return Delivery::ready(); } + static fail::Outcome check(uint8_t irq, bool line) { + const fail::Outcome o = LineCheck::of(irq, line); + Observer::seen(irq, line, o); + return o; + } + }; + struct NoIrq { static constexpr bool on = false; static constexpr uint8_t enable = 0, idle = 0, pushPull = 0; + static constexpr uint16_t timeoutMs = 0; + static void begin() {} static void arm() {} - static bool wait() { return false; } - static void seen(uint8_t, bool) {} + static bool line() { return false; } + static fail::Outcome check(uint8_t, bool) { return fail::Outcome::Ok(); } }; template using Void = void; template struct IrqOf { using type = NoIrq; }; @@ -78,7 +124,17 @@ namespace rc522 { // missStreak: consecutive polls that found no card while a UID is held; a departure is emitted at missPolls // inits: how many times the chip was initialised (the first is at discovery) // corrupt: canary failures in a row while the ID still answers - struct DeviceState { uint32_t uid; uint16_t bccErrors, collisions, initTries, inits; uint8_t missStreak, corrupt; }; + // The poll in flight: phase (Rest: none), rx (the UID answer), limit (the deadline of the command in flight). With an interrupt part it outlives + // the call that started it, so each row holds one; without, the poll runs inside one call and the driver shares one. + enum Phase : uint8_t { Rest, Wake0, Wake1, Anti }; + struct PollState { fail::Deadline limit; uint8_t phase, rx[5]; }; + struct NoPoll {}; + struct DeviceState : std::conditional_t { uint32_t uid; uint16_t bccErrors, collisions, initTries, inits; uint8_t missStreak, corrupt; }; + inline static PollState shared{}; // used only without an interrupt part + static PollState& pollOf([[maybe_unused]] RowId row) { + if constexpr (Irq::on) return B::dev(row); + else return shared; + } static constexpr uint8_t corruptMax = 3; // Corrupts in a row (initialised again each time) before the chip counts as not there static constexpr uint8_t missPolls = 3; @@ -103,7 +159,9 @@ namespace rc522 { // allows 150). Writes made before that are lost and the antenna stays off, and PowerDown may not show the whole // window: so the configuration is then written until it reads back. static void init(RowId row) { - ++B::dev(row).inits; + auto& st = B::dev(row); + ++st.inits; + if constexpr (Irq::on) { pollOf(row).phase = Rest; pollOf(row).limit.disarm(); } // the reset ends any command in flight wr(row, TReloadRegL, 0x5A); wr(row, CommandReg, SoftReset); for (uint16_t i = 0; i < 60000 && rd(row, TReloadRegL) != 0x00; ++i) {} @@ -116,14 +174,13 @@ namespace rc522 { wr(row, TxASKReg, 0x40); // 100% ASK wr(row, ModeReg, 0x3D); // CRC preset 0x6363 wr(row, TxControlReg, uint8_t(rd(row, TxControlReg) | 0x03)); // antenna on - if constexpr (Irq::on) wr(row, DivIEnReg, Irq::pushPull); // IRQ pin driven, not open-drain + if constexpr (Irq::on) { wr(row, DivIEnReg, Irq::pushPull); wr(row, ComIEnReg, Irq::idle); } // the IRQ pin driven, nothing enabled if (configured(row)) { B::dev(row).initTries = uint16_t(i + 1); return; } } } - // one exchange with a card: FIFO in, Transceive, wait for the receive (or the timer), FIFO out. - // Returns the bytes received, 0 when nothing answered, -1 on an error (collision, parity, protocol, overflow). - static int8_t transceive(RowId row, const uint8_t* tx, uint8_t n, uint8_t* rx, uint8_t max, uint8_t lastBits) { + // One exchange with a card, in two halves. start(): FIFO in, Transceive, StartSend; it returns at once. + static void start(RowId row, const uint8_t* tx, uint8_t n, uint8_t lastBits) { wr(row, CommandReg, Idle); wr(row, ComIrqReg, 0x7F); // clear every interrupt request bit if constexpr (Irq::on) { Irq::arm(); wr(row, ComIEnReg, Irq::enable); } @@ -132,32 +189,96 @@ namespace rc522 { wr(row, BitFramingReg, lastBits); wr(row, CommandReg, Transceive); wr(row, BitFramingReg, uint8_t(lastBits | 0x80)); // StartSend - uint8_t irq = 0; - if constexpr (Irq::on) { - const bool fired = Irq::wait(); - irq = rd(row, ComIrqReg); - wr(row, ComIEnReg, Irq::idle); // line released before the requests are cleared - wr(row, ComIrqReg, 0x7F); - Irq::seen(irq, fired); - } else { - for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (irq & 0x31) break; } // RxIRq, IdleIRq, TimerIRq - } + } + + // finish(): what the command came to, given the ComIrqReg value read at its end. Returns the bytes received, 0 when nothing + // answered, -1 on an error (collision, parity, protocol, overflow). + static int8_t finish(RowId row, uint8_t irq, uint8_t* rx, uint8_t max) { wr(row, BitFramingReg, 0x00); - if (!(irq & 0x30)) return 0; // the timer ran out (or the loop did): no card answered - if (rd(row, ErrorReg) & 0x1B) return -1; // BufferOvfl, CollErr, ParityErr, ProtocolErr + if (!IrqMeaning::answered(irq)) return 0; // the timer ran out (or the wait did): no card answered + if (rd(row, ErrorReg) & 0x1B) return -1; // BufferOvfl, CollErr, ParityErr, ProtocolErr uint8_t got = rd(row, FIFOLevelReg); if (got > max) got = max; for (uint8_t i = 0; i < got; ++i) rx[i] = rd(row, FIFODataReg); return int8_t(got); } - // WUPA (7 bits): a card in the field answers ATQA. A card left READY by an anticollision without a SELECT ignores - // the first wake-up and falls back to IDLE, so a silent first try gets one more. - static bool present(RowId row) { + // The poll is a sequence of those commands: a WUPA (7 bits; a card in the field answers ATQA; a card left READY by an anticollision without a + // SELECT ignores the first wake-up and falls back to IDLE, so a silent first try gets one more), then the anticollision (cascade level 1). + static void startPoll(RowId row) { const uint8_t wupa = 0x52; - uint8_t atqa[2]; - for (uint8_t t = 0; t < 2; ++t) if (transceive(row, &wupa, 1, atqa, 2, 0x07) == 2) return true; - return false; + pollOf(row).phase = Wake0; + start(row, &wupa, 1, 0x07); + } + + // a command finished: the next step of the poll + static void advance(RowId row, int8_t got) { + auto& st = B::dev(row); + auto& p = pollOf(row); + if (p.phase == Wake0 || p.phase == Wake1) { + if (got == 2) { // ATQA: a card is there + st.missStreak = 0; p.phase = Anti; + const uint8_t anticoll[2] = {0x93, 0x20}; // cascade level 1, no UID bits known + start(row, anticoll, 2, 0x00); + return; + } + if (p.phase == Wake0) { startPoll(row); p.phase = Wake1; return; } + p.phase = Rest; // nobody answered twice + if (!st.uid) return; + if (++st.missStreak < missPolls) return; // a card held still can miss a poll: it left only after missPolls + commit(row, 0); + return; + } + p.phase = Rest; + if (got < 0) { ++st.collisions; return; } + if (got != 5) return; + const uint8_t* r = p.rx; + if (uint8_t(r[0] ^ r[1] ^ r[2] ^ r[3]) != r[4]) { ++st.bccErrors; return; } + commit(row, (uint32_t(r[0]) << 24) | (uint32_t(r[1]) << 16) | (uint32_t(r[2]) << 8) | r[3]); + } + + static void commit(RowId row, uint32_t uid) { + auto& st = B::dev(row); + if (uid != st.uid) { st.uid = uid; st.missStreak = 0; B::template emit(row, uid); } + } + + // The end of the command in flight. Without an interrupt part the register is polled until the chip says it is done. + // With one, service() calls this when the line is asserted or the wait ran out; the line is read before the register. + static int8_t complete(RowId row, fail::Outcome& check) { + uint8_t irq = 0; + if constexpr (Irq::on) { + const bool line = Irq::line(); + irq = rd(row, ComIrqReg); + wr(row, ComIEnReg, Irq::idle); // the line is released before the requests are cleared + wr(row, ComIrqReg, 0x7F); + check = Irq::check(irq, line); + } else { + for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (IrqMeaning::finished(irq)) break; } + check = fail::Outcome::Ok(); + } + return finish(row, irq, pollOf(row).rx, 5); + } + + // The loop's step, every iteration: the command in flight is finished when the line is asserted or after the timeout. Nothing waits here. + // A check that fails (the line against the register) ends the poll and is reported to the failure edge as the device's failure. + static void service(RowId row, [[maybe_unused]] uint32_t now) { + if constexpr (Irq::on) { + auto& p = pollOf(row); + if (p.phase == Rest) return; + if (!p.limit.armed) p.limit.arm(now, Irq::timeoutMs); + if (!Irq::line() && !p.limit.due(now)) return; + p.limit.disarm(); + W::route(W::reg.rows[row].parent); + fail::Outcome check; + const int8_t got = complete(row, check); + if (!check.isOk()) { p.phase = Rest; report(row, check); return; } + advance(row, got); + } + } + + // a failure found after the poll was accepted: the stack sees it as the poll's own + static void report(RowId row, fail::Outcome o) { + Edge::Tab::serve(row, fail::Cause::Fresh, [o]() -> fail::Outcome { return o; }); } static void reinit(RowId row) { init(row); } @@ -166,6 +287,7 @@ namespace rc522 { static void onStale(RowId row) { auto& st = B::dev(row); st.missStreak = 0; st.corrupt = 0; + if constexpr (Irq::on) { pollOf(row).phase = Rest; pollOf(row).limit.disarm(); } if (st.uid) { st.uid = 0; B::template emit(row, 0); } } @@ -187,23 +309,13 @@ namespace rc522 { return fail::Outcome::Ok(); } + // Without an interrupt part the whole poll runs here; with one it is started and service() carries it on. A poll still in flight is left to finish. static void pollCard(RowId row) { - auto& st = B::dev(row); - uint32_t uid = 0; - if (!present(row)) { - if (!st.uid) return; - if (++st.missStreak < missPolls) return; // a card held still can miss a poll: it left only after missPolls - } else { - st.missStreak = 0; - const uint8_t anticoll[2] = {0x93, 0x20}; // cascade level 1, no UID bits known - uint8_t r[5]; - const int8_t got = transceive(row, anticoll, 2, r, 5, 0x00); - if (got < 0) { ++st.collisions; return; } - if (got != 5) return; - if (uint8_t(r[0] ^ r[1] ^ r[2] ^ r[3]) != r[4]) { ++st.bccErrors; return; } - uid = (uint32_t(r[0]) << 24) | (uint32_t(r[1]) << 16) | (uint32_t(r[2]) << 8) | r[3]; + if (pollOf(row).phase != Rest) return; + startPoll(row); + if constexpr (!Irq::on) { + while (pollOf(row).phase != Rest) { fail::Outcome ok; advance(row, complete(row, ok)); } } - if (uid != st.uid) { st.uid = uid; st.missStreak = 0; B::template emit(row, uid); } } }; diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index d50f91c..36a9a82 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -2,6 +2,7 @@ # SPI discovery round 1 verification. # native : g++ -O2 (+ASan/UBSan, + clang if present) -- the identification, empty-slot and RC522 card assertions. # rules : each compile-time rule rejects its case with its own message. +# irq : the RC522's interrupt part (spi_irq.cpp) and the ESP8266 delivery components' pin rules (irq_delivery.cpp). # AVR : avr-g++ -Os atmega328p, linked over the real AVR SPI core -- it builds, and its size. # Exits non-zero if an assertion fails, a rule does not fire, or the AVR image does not build. set -e @@ -44,6 +45,26 @@ if command -v clang++ >/dev/null; then "$OUT/shclang" | tail -1 || rc=1 fi +echo; echo "=== RC522 interrupt part (spi_irq.cpp): g++ -O2, then ASan/UBSan ===" +g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi_irq.cpp -o "$OUT/si" +"$OUT/si" || rc=1 +g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $INC spi_irq.cpp -o "$OUT/sisan" +"$OUT/sisan" | tail -1 || rc=1 +if command -v clang++ >/dev/null; then + clang++ -std=c++17 -O2 -Wall -Wextra $INC spi_irq.cpp -o "$OUT/siclang" + "$OUT/siclang" | tail -1 || rc=1 +fi + +echo; echo "=== ESP8266 interrupt delivery (irq_delivery.cpp, host stub): the pins that compile, the ones rejected ===" +DINC="$INC -I ../support/arduino_stub" +g++ -std=c++17 -Wall -Wextra -Werror $DINC irq_delivery.cpp -o "$OUT/idl" && "$OUT/idl" && echo "OK: Sampled<16> and IsrFlag<5> build" || { echo "FAIL: the delivery components do not build"; rc=1; } +for pair in "NEG_D3:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" "NEG_D4:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" "NEG_D8:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" \ + "NEG_ISR16:GPIO16 has no interrupt: use Sampled" "NEG_PAST:an ESP8266 has GPIO0 to GPIO16"; do + def=${pair%%:*}; msg=${pair#*:} + if g++ -std=c++17 -D$def $DINC -fsyntax-only irq_delivery.cpp 2>&1 | grep -qF "$msg"; then echo "OK: -D$def rejected: $msg" + else echo "FAIL: -D$def was not rejected with '$msg'"; rc=1; fi +done + echo; echo "=== compile-fail guards (each must be rejected with its own message) ===" for pair in "NEG_ID_IDLE:an SPI id of 0x00 or 0xFF is what an empty slot reads" \ "NEG_FIXED_TWICE:two Fixed entries name the same slot" \ diff --git a/test/discover/irq_delivery.cpp b/test/discover/irq_delivery.cpp new file mode 100644 index 0000000..31d1caf --- /dev/null +++ b/test/discover/irq_delivery.cpp @@ -0,0 +1,29 @@ +// The ESP8266 delivery components (examples/spi/src/irq_esp8266.h) against the host stub: the pins they accept compile, the strapping pins +// and GPIO16 for an ISR are rejected with their own message (-DNEG_*). +#include +#include "../../examples/spi/src/irq_esp8266.h" + +#if defined(NEG_D3) + using L = irq::Sampled<0>; +#elif defined(NEG_D4) + using L = irq::IsrFlag<2>; +#elif defined(NEG_D8) + using L = irq::Sampled<15>; +#elif defined(NEG_ISR16) + using L = irq::IsrFlag<16>; +#elif defined(NEG_PAST) + using L = irq::Sampled<17>; +#else + using L = irq::Sampled<16>; // D0: no interrupt, sampled + using F = irq::IsrFlag<5>; // D1: an ISR flag +#endif + +int main() { + L::begin(); L::arm(); +#if !defined(NEG_D3) && !defined(NEG_D4) && !defined(NEG_D8) && !defined(NEG_ISR16) && !defined(NEG_PAST) + F::begin(); F::arm(); F::isr(); + return F::ready() && !L::ready() ? 0 : 1; // the stub reads the line high +#else + return L::ready(); +#endif +} diff --git a/test/discover/spi_irq.cpp b/test/discover/spi_irq.cpp new file mode 100644 index 0000000..b161ebc --- /dev/null +++ b/test/discover/spi_irq.cpp @@ -0,0 +1,178 @@ +// The RC522 with its interrupt part (rc522::Interrupt) against the register model with a modelled IRQ line (test/support/mockSpi.h): +// - a poll starts its command and returns: nothing waits for the chip, and nothing is delivered until service() finishes it +// - service() finishes a command when the line is asserted, or after the timeout; one step per command (WUPA, a second WUPA, anticollision) +// - what the bits mean: RxIRq a card answered, TimerIRq alone nobody did +// - the line is released after every command (ComIEnReg back to IRqInv only, the request cleared), and push-pull is set at init +// - the line against the register is a device failure through the failure edge: a line that never fell (Fault, detail 2), one that fell with no +// request (Fault, detail 1), a command that never ended (Timeout) +// Native only. Time is simulated. +#include +#include +#include +#include +#include +#include "../support/mockSpi.h" +#include "../../examples/spi/src/rc522.h" + +using discover::RowId; +using discover::Sample; +using discover::Status; +using hapi::Chain; + +struct CardLog { + using Accepts = Chain; + inline static uint32_t log[16]; + inline static uint8_t n = 0; + template struct Body { + template struct Part : T { + using T::T; + void on(const Sample& s) { if (n < 16) log[n++] = s.value; } + }; + }; +}; + +struct MockLine { + static void begin() {} + static void arm() {} + static bool ready() { return mspi::State::rc.line(); } +}; +struct Seen { + inline static uint32_t n = 0; inline static uint8_t lastIrq = 0; inline static bool lastLine = false; inline static fail::Outcome last; + static void seen(uint8_t irq, bool line, const fail::Outcome& o) { ++n; lastIrq = irq; lastLine = line; last = o; } +}; + +struct Mode { + static constexpr bool lifecycle = true, returnPath = false, idempotent = true; + template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; + template using Access = fail::SpiAccess; + using Irq = rc522::Interrupt; +}; + +struct App; +using Rfid = rc522::Rc522; +struct App : discover::World, Chain, 3, discover::SpiScan, Chain>> { + static constexpr bool lifecycle = true; + static void release(RowId) {} + static void unbindAll() {} +}; +using Ticker = fail::Ticks; + +static int failures = 0; +#define CHECK(c) do { if (!(c)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #c); } } while (0) + +static uint32_t now = 0; +static auto& st() { return App::devState(1); } +static bool inFlight() { return st().phase != Rfid::Rest; } + +static void start(bool card) { + mspi::State::reset(); + mspi::State::kind[0] = mspi::Kind::Rc522; + mspi::State::card = mspi::Card{card, {0xDE, 0xAD, 0xBE, 0xEF}, false, false, false}; + CardLog::n = 0; Seen::n = 0; now = 0; + Rfid::resetFail(); + App::discover(); +} +// service the poll until it is at rest; the number of steps it took (a step finishes one command) +static int run(uint32_t limitMs = 200) { + int steps = 0; + for (uint32_t t = 0; inFlight() && t < limitMs; ++t, ++now) { + const uint8_t before = st().phase; + const uint32_t seen = Seen::n; + Rfid::service(1, now); + if (Seen::n != seen || st().phase != before) ++steps; + } + return steps; +} +static uint32_t bytes() { return mspi::State::bytes[0]; } + +int main() { + using mspi::State; + auto& rc = State::rc; + + // ---- init: push-pull set, nothing enabled between commands --------------------------------------------- + start(true); + CHECK(App::reg.count == 2); + CHECK(rc.regs[0x03] == rc522::Interrupt::pushPull); + CHECK(rc.regs[0x02] == rc522::Interrupt::idle && !rc.line()); + + // ---- a card: the poll returns at once, service() finishes it in two steps (WUPA, anticollision) ------------------ + const uint32_t b0 = bytes(); + App::pump(); + CHECK(inFlight()); // started, not waited for + CHECK(CardLog::n == 0); + CHECK(bytes() - b0 < 80); // a canary and a command: no polling loop + CHECK(rc.regs[0x02] == rc522::Interrupt::enable); // enabled while the command runs + CHECK(rc.line()); // the mock ends it at StartSend + CHECK(run() == 2); + CHECK(!inFlight()); + CHECK(CardLog::n == 1 && CardLog::log[0] == 0xDEADBEEFu); + CHECK(Seen::n == 2 && Seen::lastLine && (Seen::lastIrq & 0x20) && Seen::last.isOk()); + CHECK(rc.regs[0x02] == rc522::Interrupt::idle && rc.regs[0x04] == 0 && !rc.line()); // released and cleared + CHECK(Rfid::failStatus(1).fails == 0); + + // ---- no card: two silent wake-ups (the second is the retry), the timer ended both --------------------------------------- + start(false); + App::pump(); + CHECK(inFlight()); + CHECK(run() == 2); + CHECK(CardLog::n == 0 && !inFlight()); + CHECK(Seen::n == 2 && Seen::lastLine && !(Seen::lastIrq & 0x20) && (Seen::lastIrq & 0x01) && Seen::last.isOk()); + CHECK(Rfid::failStatus(1).fails == 0 && App::reg.status(1) == Status::Alive); + CHECK(rc.regs[0x02] == rc522::Interrupt::idle && !rc.line()); + + // ---- the card leaves: reported gone after 3 polls, the same as without the interrupt part --------------------- + start(true); + App::pump(); run(); + CHECK(CardLog::n == 1); + State::card.present = false; + for (int i = 0; i < 3; ++i) { App::pump(); run(); } + CHECK(CardLog::n == 2 && CardLog::log[1] == 0 && st().uid == 0); + + // ---- a poll still in flight is not started again ------------------------------------------------------------- + start(true); + App::pump(); + const uint32_t b1 = bytes(); + App::pump(); // the canary only + CHECK(bytes() - b1 < 20 && inFlight()); + run(); + CHECK(CardLog::n == 1); + + // ---- a command that never ends: nothing before the timeout, then a Timeout through the edge ------------------ + start(true); + rc.stall = true; + App::pump(); + const uint32_t t0 = now; + Rfid::service(1, t0); // arms the deadline + Rfid::service(1, t0 + rc522::Interrupt::timeoutMs - 1); + CHECK(inFlight() && Seen::n == 0); + Rfid::service(1, t0 + rc522::Interrupt::timeoutMs); + CHECK(!inFlight() && Seen::n == 1 && Seen::last.failed() && Seen::last.kind() == fail::Kind::Timeout); + CHECK(Rfid::failStatus(1).fails >= 1 && uint8_t(Rfid::failStatus(1).lastKind) == uint8_t(fail::Kind::Timeout)); + CHECK(st().inits == 2); // Recover initialised the chip again + CHECK(CardLog::n == 0); + + // ---- the line never fell though the register shows the answer (not connected): Fault, detail 2 ---------------- + start(true); + rc.lineFault = 1; + App::pump(); + CHECK(inFlight() && !rc.line()); + Rfid::service(1, now); // the line is not asserted: waits for the timeout + CHECK(inFlight()); + Rfid::service(1, now + rc522::Interrupt::timeoutMs); + CHECK(!inFlight() && Seen::last.failed() && Seen::last.kind() == fail::Kind::Fault && Seen::last.detail == rc522::LineCheck::missed); + CHECK(Rfid::failStatus(1).fails >= 1 && uint8_t(Rfid::failStatus(1).lastKind) == uint8_t(fail::Kind::Fault)); + CHECK(CardLog::n == 0); // the poll ended: no card is claimed on a line that disagrees + + // ---- the line fell and the register shows no request (stuck low): Fault, detail 1 ------------------------ + start(true); + rc.stall = true; rc.lineFault = 2; + App::pump(); + CHECK(inFlight()); + Rfid::service(1, now); // asserted: finished at once, no timeout wait + CHECK(!inFlight() && Seen::last.failed() && Seen::last.kind() == fail::Kind::Fault && Seen::last.detail == rc522::LineCheck::spurious); + + if (failures) { std::printf("FAILED: %d\n", failures); return 1; } + std::printf("OK: RC522 interrupt part native\n"); + return 0; +} diff --git a/test/support/arduino_stub/Arduino.h b/test/support/arduino_stub/Arduino.h new file mode 100644 index 0000000..1416d03 --- /dev/null +++ b/test/support/arduino_stub/Arduino.h @@ -0,0 +1,11 @@ +// Just enough of Arduino.h for the delivery components' compile checks on the host (never run). +#pragma once +#include +#define IRAM_ATTR +#define INPUT 0 +#define INPUT_PULLUP 2 +#define FALLING 2 +inline void pinMode(uint8_t, uint8_t) {} +inline int digitalRead(uint8_t) { return 1; } +inline uint8_t digitalPinToInterrupt(uint8_t p) { return p; } +inline void attachInterrupt(uint8_t, void (*)(), int) {} diff --git a/test/support/mockSpi.h b/test/support/mockSpi.h index 3dbbac6..e07e3a9 100644 --- a/test/support/mockSpi.h +++ b/test/support/mockSpi.h @@ -35,6 +35,14 @@ namespace mspi { for (auto& r : regs) r = 0; regs[0x37] = version; regs[0x14] = 0x80; n = 0; resetReads = 0; deaf = 0; pending = 0; held = false; } + bool stall = false; // a command never ends: no request is raised + uint8_t lineFault = 0; // the IRQ line: 0 as the chip drives it, 1 stuck high (not connected), 2 stuck low + // the IRQ line, active low (ComIEnReg IRqInv set): asserted while a request that is enabled is pending + bool line() const { + if (lineFault == 1) return false; + if (lineFault == 2) return true; + return !held && (regs[0x04] & regs[0x02] & 0x7F) != 0; + } bool noStore = false; // the ID answers and nothing written is kept (a part living off its signal pins) bool held = false; // RST low: the part reads 0x00 and ignores writes; released, it comes up at its register defaults (a hard reset, not a SoftReset) void hold(bool on) { @@ -77,6 +85,7 @@ namespace mspi { void transceive(Card& card) { const uint8_t len = n; uint8_t in[16]; for (uint8_t i = 0; i < len; ++i) in[i] = fifo[i]; n = 0; regs[0x06] = 0; + if (stall) return; const bool antenna = (regs[0x14] & 0x03) == 0x03; if (antenna && card.present && len == 1 && (in[0] == 0x52 || in[0] == 0x26)) { if (card.mute) { --card.mute; regs[0x04] |= 0x01; return; } // a held card that does not answer this wake-up @@ -117,7 +126,7 @@ namespace mspi { static void reset() { for (uint8_t k = 0; k < slots; ++k) { kind[k] = Kind::Empty; bytes[k] = 0; modeAt[k] = 0xFF; } bmxId = 0x58; idle = 0xFF; floating = stuckLow = false; noise = 0; - rc.reset(0x92); card = Card{}; + rc.reset(0x92); rc.stall = false; rc.lineFault = 0; card = Card{}; sel = -1; phase = 0; setups = 0; hz = 0; mode = 0; } static void clearCounts() { for (auto& b : bytes) b = 0; setups = 0; } From ce501a555404465d45917369fd518ab491529652 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 03:44:45 +0000 Subject: [PATCH 03/19] rc522: no interrupt part costs nothing; service over all rows; optional 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: the loop's service(row, now) over every row of the drivers that serve. - rc522::Interrupt: 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 --- examples/spi/README.md | 11 ++- examples/spi/src/main.cpp | 33 ++++--- examples/spi/src/rc522.h | 130 +++++++++++++++++++++------- include/oneMachine/fail/world.h | 19 ++++ test/baselines/rc522_noirq_avr.size | 1 + test/discover/avr_spi.cpp | 33 ++++++- test/discover/build_spi1.sh | 34 +++++--- test/discover/spi_irq.cpp | 74 +++++++++++++++- 8 files changed, 275 insertions(+), 60 deletions(-) create mode 100644 test/baselines/rc522_noirq_avr.size diff --git a/examples/spi/README.md b/examples/spi/README.md index b910763..6c7f258 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -28,11 +28,14 @@ Wemos D1 mini (ESP8266), everything at 3.3V. Slot 1 is D3 (GPIO0) with nothing on it: the scan reports it empty. 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 `Rfid::service()` finishes it when the line is asserted, or after 40 ms, so nothing in the loop waits for the reader. 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` sets a -flag in the ISR instead. The RxIRq/TimerIRq bits tell a card from none, and a line that disagrees with the register is reported as a -device failure. Requests are enabled only while a command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8, rejected at -compile time): the RC522 keeps its state across a reset of the board, and a pending request would hold such a pin low at the next boot. +flag in the ISR instead. The RxIRq/TimerIRq bits tell a card from none. The sketch also uses the optional line check: a line that +disagrees with the register (not connected, stuck) is reported to the failure edge as the delivery's fault, and that row polls the +register from then on (`rc522::PollOnLineFault`); without the fallback the same fault is the chip's and Recover initialises it. The +interrupt part is optional as a whole: a mode without `using Irq` is the polling driver, unchanged. Requests are enabled only while a +command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8, rejected at compile time): the RC522 keeps its state across a +reset of the board, and a pending request would hold such a pin low at the next boot. ## Build and flash diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 268cee7..fcc678a 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -4,11 +4,11 @@ // RC522 SCK D5, MISO D6, MOSI D7, SDA (its CS) D8, RST D4 (held high here) or 3V3, IRQ D0, 3V3, GND // BMP280 SDA D2, SCL D1, 3V3, GND (CSB high or open: I2C mode) // Slot 1 (D3) is declared with nothing on it: the scan reports it empty. -// D0 (GPIO16) is the RC522's IRQ input. A poll starts its command and returns; the loop finishes it (Rfid::service) when the +// D0 (GPIO16) is the RC522's IRQ input. A poll starts its command and returns; the loop finishes it (fail::Services) 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); where the pin has one, irq::IsrFlag sets a flag in the ISR instead. The chip drives the line // (push-pull, active low); ComIrqReg then tells RxIRq (a card answered) from TimerIRq (none did), and a line that disagrees with -// the register is a device failure. Not on a boot strapping pin (D3, D4, D8): the RC522 keeps its state across a reset of the +// the register is reported to the failure edge, and the row then polls the register instead (rc522::PollOnLineFault). Not on a boot strapping pin (D3, D4, D8): the RC522 keeps its state across a reset of the // board and a pending request would hold the line low at the next boot. // // Line format: []= a card's UID in hex when one arrives, 0 when it leaves (after 3 polls @@ -87,14 +87,7 @@ struct IrqCounters { if (!cnt[i]) { vals[i] = irq; cnt[i] = 1; return; } } } - static void report(uint32_t now) { - Serial.print(F("IRQ ")); Serial.print(now); - Serial.print(F(" rx=")); Serial.print(rx); Serial.print(F(" tmo=")); Serial.print(tmo); - Serial.print(F(" spurious=")); Serial.print(spurious); Serial.print(F(" missed=")); Serial.print(missed); - Serial.print(F(" none=")); Serial.print(none); - for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Serial.print(F(" 0x")); Serial.print(vals[i], HEX); Serial.print(':'); Serial.print(cnt[i]); } - Serial.println(); - } + static void report(uint32_t now); // below: it reads the reader's row }; struct RfidApp; @@ -105,7 +98,7 @@ struct RfidMode { template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; template using Access = fail::SpiAccess; - using Irq = rc522::Interrupt, IrqCounters>; // D0 + using Irq = rc522::Interrupt, IrqCounters, rc522::LineCheck, rc522::PollOnLineFault>; // D0 }; using Rfid = rc522::Rc522; using RfidDrivers = discover::DriversIn>; @@ -118,6 +111,22 @@ struct RfidApp : discover::World, Chain, 3, d using Health = fail::HealthT; }; using RfidTicker = fail::Ticks; +using RfidServices = fail::Services; + +inline void IrqCounters::report(uint32_t now) { + Serial.print(F("IRQ ")); Serial.print(now); + Serial.print(F(" rx=")); Serial.print(rx); Serial.print(F(" tmo=")); Serial.print(tmo); + Serial.print(F(" spurious=")); Serial.print(spurious); Serial.print(F(" missed=")); Serial.print(missed); + Serial.print(F(" none=")); Serial.print(none); + for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Serial.print(F(" 0x")); Serial.print(vals[i], HEX); Serial.print(':'); Serial.print(cnt[i]); } + if (RfidApp::reg.count > 1) { // what the failure edge heard of the reader's row + const fail::FailStatus f = Rfid::failStatus(1); + Serial.print(F(" | edge fails=")); Serial.print(f.fails); Serial.print(F(" recovers=")); Serial.print(f.recovers); + Serial.print(F(" last=")); Serial.print(f.lastKind); Serial.print('/'); Serial.print(f.lastDetail); + } + Serial.println(); +} + constexpr uint8_t rstPin = 2; // D4: the RC522's RST struct AirApp : discover::World, bmp::Entries, 3, discover::I2cScan> {}; @@ -229,7 +238,7 @@ void loop() { LoopStats::lap(micros()); const uint32_t now = millis(); faults(now); - if (RfidApp::reg.count > 1) { const uint32_t t = micros(); Rfid::service(1, now); LoopStats::note(LoopStats::maxSvc, t); } + { const uint32_t t = micros(); RfidServices::run(now); LoopStats::note(LoopStats::maxSvc, t); } if (int32_t(now - nextCard) >= 0) { nextCard = now + 100; { const uint32_t t = micros(); RfidApp::pump(); LoopStats::note(LoopStats::maxPump, t); } diff --git a/examples/spi/src/rc522.h b/examples/spi/src/rc522.h index 4913569..dbfce3d 100644 --- a/examples/spi/src/rc522.h +++ b/examples/spi/src/rc522.h @@ -37,12 +37,15 @@ namespace rc522 { }; // ---- the RC522's interrupt part ------------------------------------------------------------------------------ - // A mode that has `using Irq = rc522::Interrupt` makes a command wait on the IRQ line instead of polling ComIrqReg: the - // poll starts the command and returns, and service() finishes it when the line is asserted, or after Interrupt::timeoutMs. The register values - // (what is enabled while a command runs, the pin driven push-pull) belong to the chip and are here; the App chooses the pin and how the line - // reaches the loop: + // A mode that has `using Irq = rc522::Interrupt` makes a command wait on the IRQ line instead of + // polling ComIrqReg: the poll starts the command and returns, and service() finishes it when the line is asserted, or after Interrupt::timeoutMs. + // The register values (what is enabled while a command runs, the pin driven push-pull) belong to the chip and are here; the App chooses the pin + // and how the line reaches the loop, and which of the optional parts it wants: // Delivery begin(), arm() (before a command starts), ready() (the line is asserted) - // Observer seen(irq, line, outcome): rig diagnostics, after every command + // Observer seen(irq, line, outcome): rig diagnostics, after every command (default NoObserver) + // Check the line against the register after a command, as a device failure (LineCheck; default NoCheck: none) + // Fallback what a line fault does to the row (PollOnLineFault: it polls the register from then on; default NoFallback: the fault is the + // chip's as far as the failure edge is concerned, and Recover initialises it again) // The requests are enabled only while a command runs and cleared after it: the chip keeps its state across a reset of the host, and an // enabled pending request would hold the line low at the next boot (a boot strapping pin must be high then). struct NoObserver { static void seen(uint8_t, bool, const fail::Outcome&) {} }; @@ -54,11 +57,18 @@ namespace rc522 { static constexpr bool finished(uint8_t irq) { return (irq & (rxIrq | idleIrq | timerIrq)) != 0; } }; + struct NoCheck { + static constexpr bool on = false; + static fail::Outcome of(uint8_t, bool) { return fail::Outcome::Ok(); } + }; + // The line against the register after a command, as a device failure: Fault with detail 1 when the line is asserted and the register shows no // request that was enabled (spurious), 2 when it shows one and the line never was (the line is not connected, or not driven); Timeout when // neither (the chip's own timer did not end the command). struct LineCheck { + static constexpr bool on = true; static constexpr uint8_t spurious = 1, missed = 2; + static constexpr bool isLineFault(const fail::Outcome& o) { return o.failed() && o.kind() == fail::Kind::Fault && (o.detail == spurious || o.detail == missed); } static fail::Outcome of(uint8_t irq, bool line) { const bool hit = IrqMeaning::finished(irq); if (line && !hit) return fail::Outcome::Fail(fail::Kind::Fault, spurious); @@ -68,9 +78,25 @@ namespace rc522 { } }; - template + struct NoFallback { + static constexpr bool on = false; + static constexpr bool handles(const fail::Outcome&) { return false; } + static fail::Outcome report(const fail::Outcome& o) { return o; } + }; + + // A line fault is the delivery's, not the chip's: the row polls ComIrqReg from then on (until the chip is initialised again, which tests the line + // again). The failure edge still hears of it, as a kind Recover does not act on (Refused, the line's detail kept): nothing initialises the chip. + struct PollOnLineFault { + static constexpr bool on = true; + static constexpr bool handles(const fail::Outcome& o) { return LineCheck::isLineFault(o); } + static fail::Outcome report(const fail::Outcome& o) { return fail::Outcome::Fail(fail::Kind::Refused, o.detail); } + }; + + template struct Interrupt { + static_assert(!Fallback::on || Check::on, "a fallback acts on what the line check finds: give the interrupt part a check"); static constexpr bool on = true; + using FallbackPart = Fallback; static constexpr uint8_t enable = 0x80 | IrqMeaning::rxIrq | IrqMeaning::timerIrq; // ComIEnReg while a command runs: IRqInv, RxIEn, TimerIEn static constexpr uint8_t idle = 0x80; // ComIEnReg between commands: IRqInv only static constexpr uint8_t pushPull = 0x80; // DivIEnReg IRQPushPull: the chip drives the line @@ -79,7 +105,7 @@ namespace rc522 { static void arm() { Delivery::arm(); } static bool line() { return Delivery::ready(); } static fail::Outcome check(uint8_t irq, bool line) { - const fail::Outcome o = LineCheck::of(irq, line); + const fail::Outcome o = Check::of(irq, line); Observer::seen(irq, line, o); return o; } @@ -89,6 +115,7 @@ namespace rc522 { static constexpr bool on = false; static constexpr uint8_t enable = 0, idle = 0, pushPull = 0; static constexpr uint16_t timeoutMs = 0; + using FallbackPart = NoFallback; static void begin() {} static void arm() {} static bool line() { return false; } @@ -124,17 +151,15 @@ namespace rc522 { // missStreak: consecutive polls that found no card while a UID is held; a departure is emitted at missPolls // inits: how many times the chip was initialised (the first is at discovery) // corrupt: canary failures in a row while the ID still answers - // The poll in flight: phase (Rest: none), rx (the UID answer), limit (the deadline of the command in flight). With an interrupt part it outlives - // the call that started it, so each row holds one; without, the poll runs inside one call and the driver shares one. + // The poll in flight, with an interrupt part only: phase (Rest: none), rx (the UID answer), limit (the deadline of the command in flight), fallen + // (the row polls the register: a line fault with a fallback). It outlives the call that started it, so each row holds one. Without an interrupt + // part the poll runs inside one call and none of this exists. enum Phase : uint8_t { Rest, Wake0, Wake1, Anti }; - struct PollState { fail::Deadline limit; uint8_t phase, rx[5]; }; + struct PollState { fail::Deadline limit; uint8_t phase, rx[5]; bool fallen; }; struct NoPoll {}; struct DeviceState : std::conditional_t { uint32_t uid; uint16_t bccErrors, collisions, initTries, inits; uint8_t missStreak, corrupt; }; - inline static PollState shared{}; // used only without an interrupt part - static PollState& pollOf([[maybe_unused]] RowId row) { - if constexpr (Irq::on) return B::dev(row); - else return shared; - } + static PollState& pollOf(RowId row) { return B::dev(row); } // with an interrupt part + static constexpr bool serves = Irq::on; // has a service(row, now) for the loop (fail::Services) static constexpr uint8_t corruptMax = 3; // Corrupts in a row (initialised again each time) before the chip counts as not there static constexpr uint8_t missPolls = 3; @@ -161,7 +186,7 @@ namespace rc522 { static void init(RowId row) { auto& st = B::dev(row); ++st.inits; - if constexpr (Irq::on) { pollOf(row).phase = Rest; pollOf(row).limit.disarm(); } // the reset ends any command in flight + if constexpr (Irq::on) { auto& p = pollOf(row); p.phase = Rest; p.limit.disarm(); p.fallen = false; } // the reset ends any command in flight; the line is tried again wr(row, TReloadRegL, 0x5A); wr(row, CommandReg, SoftReset); for (uint16_t i = 0; i < 60000 && rd(row, TReloadRegL) != 0x00; ++i) {} @@ -183,7 +208,7 @@ namespace rc522 { static void start(RowId row, const uint8_t* tx, uint8_t n, uint8_t lastBits) { wr(row, CommandReg, Idle); wr(row, ComIrqReg, 0x7F); // clear every interrupt request bit - if constexpr (Irq::on) { Irq::arm(); wr(row, ComIEnReg, Irq::enable); } + if constexpr (Irq::on) { if (!pollOf(row).fallen) { Irq::arm(); wr(row, ComIEnReg, Irq::enable); } } wr(row, FIFOLevelReg, 0x80); // flush the FIFO for (uint8_t i = 0; i < n; ++i) wr(row, FIFODataReg, tx[i]); wr(row, BitFramingReg, lastBits); @@ -203,8 +228,25 @@ namespace rc522 { return int8_t(got); } - // The poll is a sequence of those commands: a WUPA (7 bits; a card in the field answers ATQA; a card left READY by an anticollision without a - // SELECT ignores the first wake-up and falls back to IDLE, so a silent first try gets one more), then the anticollision (cascade level 1). + // The whole exchange, waiting for the chip: the register is polled until it says the command is done (no interrupt part). + static int8_t transceive(RowId row, const uint8_t* tx, uint8_t n, uint8_t* rx, uint8_t max, uint8_t lastBits) { + start(row, tx, n, lastBits); + uint8_t irq = 0; + for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (irq & 0x31) break; } // RxIRq, IdleIRq, TimerIRq + return finish(row, irq, rx, max); + } + + // WUPA (7 bits): a card in the field answers ATQA. A card left READY by an anticollision without a SELECT ignores + // the first wake-up and falls back to IDLE, so a silent first try gets one more. + static bool present(RowId row) { + const uint8_t wupa = 0x52; + uint8_t atqa[2]; + for (uint8_t t = 0; t < 2; ++t) if (transceive(row, &wupa, 1, atqa, 2, 0x07) == 2) return true; + return false; + } + + // ---- with an interrupt part: the poll as steps, one command each -------------------------------------------------------- + // a WUPA, a second WUPA when it is silent, then the anticollision (cascade level 1) static void startPoll(RowId row) { const uint8_t wupa = 0x52; pollOf(row).phase = Wake0; @@ -242,11 +284,12 @@ namespace rc522 { if (uid != st.uid) { st.uid = uid; st.missStreak = 0; B::template emit(row, uid); } } - // The end of the command in flight. Without an interrupt part the register is polled until the chip says it is done. - // With one, service() calls this when the line is asserted or the wait ran out; the line is read before the register. + // The end of the command in flight: by the line (read before the register, which is then released and cleared), or, for a row that fell + // back, by polling the register until the chip says it is done. static int8_t complete(RowId row, fail::Outcome& check) { uint8_t irq = 0; - if constexpr (Irq::on) { + check = fail::Outcome::Ok(); + if (!pollOf(row).fallen) { const bool line = Irq::line(); irq = rd(row, ComIrqReg); wr(row, ComIEnReg, Irq::idle); // the line is released before the requests are cleared @@ -254,13 +297,13 @@ namespace rc522 { check = Irq::check(irq, line); } else { for (uint16_t i = 0; i < 2000; ++i) { irq = rd(row, ComIrqReg); if (IrqMeaning::finished(irq)) break; } - check = fail::Outcome::Ok(); } return finish(row, irq, pollOf(row).rx, 5); } - // The loop's step, every iteration: the command in flight is finished when the line is asserted or after the timeout. Nothing waits here. - // A check that fails (the line against the register) ends the poll and is reported to the failure edge as the device's failure. + // The loop's step, every iteration (fail::Services): the command in flight is finished when the line is asserted or after the timeout. Nothing + // waits here. A check that fails ends the poll and is reported to the failure edge as the device's failure; a line fault with a fallback + // is reported as the delivery's, the row polls the register from then on, and the answer the register gave is used. static void service(RowId row, [[maybe_unused]] uint32_t now) { if constexpr (Irq::on) { auto& p = pollOf(row); @@ -271,7 +314,15 @@ namespace rc522 { W::route(W::reg.rows[row].parent); fail::Outcome check; const int8_t got = complete(row, check); - if (!check.isOk()) { p.phase = Rest; report(row, check); return; } + if (!check.isOk()) { + if (Irq::FallbackPart::handles(check)) { + p.fallen = true; report(row, Irq::FallbackPart::report(check)); + advance(row, got); + while (p.phase != Rest) { fail::Outcome ok; advance(row, complete(row, ok)); } // the rest of this poll, by the register + return; + } + p.phase = Rest; report(row, check); return; + } advance(row, got); } } @@ -309,12 +360,29 @@ namespace rc522 { return fail::Outcome::Ok(); } - // Without an interrupt part the whole poll runs here; with one it is started and service() carries it on. A poll still in flight is left to finish. + // Without an interrupt part the whole poll runs here. With one it is started and service() carries it on; a row that fell back runs it here too. static void pollCard(RowId row) { - if (pollOf(row).phase != Rest) return; - startPoll(row); - if constexpr (!Irq::on) { - while (pollOf(row).phase != Rest) { fail::Outcome ok; advance(row, complete(row, ok)); } + if constexpr (Irq::on) { + if (pollOf(row).phase != Rest) return; + startPoll(row); + if (pollOf(row).fallen) while (pollOf(row).phase != Rest) { fail::Outcome ok; advance(row, complete(row, ok)); } + } else { + auto& st = B::dev(row); + uint32_t uid = 0; + if (!present(row)) { + if (!st.uid) return; + if (++st.missStreak < missPolls) return; // a card held still can miss a poll: it left only after missPolls + } else { + st.missStreak = 0; + const uint8_t anticoll[2] = {0x93, 0x20}; // cascade level 1, no UID bits known + uint8_t r[5]; + const int8_t got = transceive(row, anticoll, 2, r, 5, 0x00); + if (got < 0) { ++st.collisions; return; } + if (got != 5) return; + if (uint8_t(r[0] ^ r[1] ^ r[2] ^ r[3]) != r[4]) { ++st.bccErrors; return; } + uid = (uint32_t(r[0]) << 24) | (uint32_t(r[1]) << 16) | (uint32_t(r[2]) << 8) | r[3]; + } + if (uid != st.uid) { st.uid = uid; st.missStreak = 0; B::template emit(row, uid); } } } }; diff --git a/include/oneMachine/fail/world.h b/include/oneMachine/fail/world.h index b45165d..37de74d 100644 --- a/include/oneMachine/fail/world.h +++ b/include/oneMachine/fail/world.h @@ -17,6 +17,25 @@ namespace fail { } }; + // A driver with `static constexpr bool serves` and service(row, now): the loop's step that finishes what a poll started (an interrupt-driven + // command). A driver that does not serve costs nothing: the fold does not look at its rows. + template struct Serves : std::false_type {}; + template struct Serves> : std::bool_constant {}; + + // W: the world (reg); Ds: Chain. Direct calls only, like TickFold. + template struct ServiceFold; + template struct ServiceFold> { + static void run([[maybe_unused]] uint32_t now) { (one(now), ...); } + private: + template static void one([[maybe_unused]] uint32_t now) { + if constexpr (Serves::value) { + for (RowId r = 0; r < W::reg.count; ++r) + if (W::reg.rows[r].drv == discover::instOf()) Dr::service(r, now); + } + } + }; + template using Services = ServiceFold; + // the drivers whose stack has tick(now); empty (and free) when no driver chose a ticking component template using Ticks = TickFold, Ds>>; diff --git a/test/baselines/rc522_noirq_avr.size b/test/baselines/rc522_noirq_avr.size new file mode 100644 index 0000000..e3a0078 --- /dev/null +++ b/test/baselines/rc522_noirq_avr.size @@ -0,0 +1 @@ +2146 10 81 diff --git a/test/discover/avr_spi.cpp b/test/discover/avr_spi.cpp index 3727590..250f59d 100644 --- a/test/discover/avr_spi.cpp +++ b/test/discover/avr_spi.cpp @@ -1,11 +1,17 @@ // SPI discovery on the real AVR SPI core: the RC522 driver and SpiScan built with avr-g++ 7.3 -Os (the oldest // compiler the library supports), so the size of an SPI world is on record and gcc 7 keeps compiling it. +// (default) the driver with no interrupt part: its image is the driver as it was before the interrupt part existed (build_spi1.sh checks the size) +// -DWITH_IRQ=1 with rc522::Interrupt (the IRQ on PD2, sampled), service() folded over the rows +// -DWITH_IRQ=2 and the LineCheck and PollOnLineFault parts #include #include #include #include #include #include "../../examples/spi/src/rc522.h" +#ifdef WITH_IRQ +#include +#endif template struct PinB { // CS pins on PORTB static void begin() { DDRB |= uint8_t(1u << Bit); } @@ -23,12 +29,37 @@ struct Sink { }; }; +#ifdef WITH_IRQ +struct IrqLine { // PD2, pulled up, active low + static void begin() { DDRD &= uint8_t(~(1u << 2)); PORTD |= uint8_t(1u << 2); } + static void arm() {} + static bool ready() { return !(PIND & (1u << 2)); } +}; +struct IrqMode : rc522::NoFail { + #if WITH_IRQ == 2 + using Irq = rc522::Interrupt; + #else + using Irq = rc522::Interrupt; + #endif +}; +struct App; +using Rfid = rc522::Rc522; +#else struct App; -struct App : discover::World, hapi::Chain>, 3, discover::SpiScan, +using Rfid = rc522::Rc522; +#endif +struct App : discover::World, hapi::Chain, 3, discover::SpiScan, hapi::Chain>> {}; int main() { Bus::begin(); +#ifdef WITH_IRQ + Rfid::Irq::begin(); +#endif App::discover(); +#ifdef WITH_IRQ + for (uint32_t now = 0;; ++now) { App::pump(); fail::Services::run(now); } +#else for (;;) App::pump(); +#endif } diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index 36a9a82..189dfaf 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -45,15 +45,17 @@ if command -v clang++ >/dev/null; then "$OUT/shclang" | tail -1 || rc=1 fi -echo; echo "=== RC522 interrupt part (spi_irq.cpp): g++ -O2, then ASan/UBSan ===" -g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi_irq.cpp -o "$OUT/si" -"$OUT/si" || rc=1 -g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $INC spi_irq.cpp -o "$OUT/sisan" -"$OUT/sisan" | tail -1 || rc=1 -if command -v clang++ >/dev/null; then - clang++ -std=c++17 -O2 -Wall -Wextra $INC spi_irq.cpp -o "$OUT/siclang" - "$OUT/siclang" | tail -1 || rc=1 -fi +echo; echo "=== RC522 interrupt part (spi_irq.cpp): fail-fast, -DFALLBACK, -DNOCHECK; g++ -O2, then ASan/UBSan ===" +for cfg in "" "-DFALLBACK" "-DNOCHECK"; do + g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $cfg $INC spi_irq.cpp -o "$OUT/si" + echo "[${cfg:-default}] $("$OUT/si")" || rc=1 + g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $cfg $INC spi_irq.cpp -o "$OUT/sisan" + "$OUT/sisan" | tail -1 || rc=1 + if command -v clang++ >/dev/null; then + clang++ -std=c++17 -O2 -Wall -Wextra $cfg $INC spi_irq.cpp -o "$OUT/siclang" + "$OUT/siclang" | tail -1 || rc=1 + fi +done echo; echo "=== ESP8266 interrupt delivery (irq_delivery.cpp, host stub): the pins that compile, the ones rejected ===" DINC="$INC -I ../support/arduino_stub" @@ -78,9 +80,19 @@ done if command -v avr-g++ >/dev/null; then echo; echo "=== avr-g++ $(avr-g++ -dumpversion) -Os atmega328p (linked, real SPI core) ===" - avr-g++ -std=gnu++17 -Os -mmcu=atmega328p -DF_CPU=16000000UL -ffunction-sections -fdata-sections \ - -fno-exceptions -fno-rtti -Wall -Wextra $INC -I ../../../OneChip/include avr_spi.cpp -Wl,--gc-sections -o "$OUT/spi.elf" \ + AVRFL="-std=gnu++17 -Os -mmcu=atmega328p -DF_CPU=16000000UL -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -Wall -Wextra" + avr-g++ $AVRFL $INC -I ../../../OneChip/include avr_spi.cpp -Wl,--gc-sections -o "$OUT/spi.elf" \ && avr-size "$OUT/spi.elf" || { echo "FAIL: the AVR SPI image does not build"; rc=1; } + # the driver with no interrupt part is what it was before the interrupt part existed: the same size, to the byte + got=$(avr-size "$OUT/spi.elf" | awk 'NR==2{print $1" "$2" "$3}') + want=$(cat ../baselines/rc522_noirq_avr.size) + if [ "$got" = "$want" ]; then echo "OK: no interrupt part: text/data/bss $got, as before the interrupt part (../baselines/rc522_noirq_avr.size)" + else echo "FAIL: no interrupt part: text/data/bss $got, the baseline says $want"; rc=1; fi + for v in 1 2; do + avr-g++ $AVRFL -DWITH_IRQ=$v $INC -I ../../../OneChip/include avr_spi.cpp -Wl,--gc-sections -o "$OUT/spi_irq$v.elf" \ + && echo "with the interrupt part ($( [ $v = 1 ] && echo 'sampled line' || echo '+ LineCheck + PollOnLineFault' )): $(avr-size "$OUT/spi_irq$v.elf" | awk 'NR==2{print "text "$1", data "$2", bss "$3}')" \ + || { echo "FAIL: the AVR image with the interrupt part ($v) does not build"; rc=1; } + done else echo "(avr-g++ not found: AVR build skipped)" fi diff --git a/test/discover/spi_irq.cpp b/test/discover/spi_irq.cpp index b161ebc..fd6e9de 100644 --- a/test/discover/spi_irq.cpp +++ b/test/discover/spi_irq.cpp @@ -5,6 +5,10 @@ // - the line is released after every command (ComIEnReg back to IRqInv only, the request cleared), and push-pull is set at init // - the line against the register is a device failure through the failure edge: a line that never fell (Fault, detail 2), one that fell with no // request (Fault, detail 1), a command that never ended (Timeout) +// Three configurations of the interrupt part, one build each: +// (default) LineCheck, no fallback: the line faults are the chip's as far as the edge is concerned (Recover initialises it) +// -DFALLBACK LineCheck + PollOnLineFault: a line fault is reported as the delivery's and the row polls the register, still reading cards +// -DNOCHECK no check: an unplugged line is not noticed, the commands end by the timeout and the register // Native only. Time is simulated. #include #include @@ -46,7 +50,13 @@ struct Mode { template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; template using Access = fail::SpiAccess; +#if defined(FALLBACK) + using Irq = rc522::Interrupt; +#elif defined(NOCHECK) using Irq = rc522::Interrupt; +#else + using Irq = rc522::Interrupt; +#endif }; struct App; @@ -57,6 +67,7 @@ struct App : discover::World, Chain, 3, dis static void unbindAll() {} }; using Ticker = fail::Ticks; +using Services = fail::Services; static int failures = 0; #define CHECK(c) do { if (!(c)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #c); } } while (0) @@ -139,6 +150,7 @@ int main() { CHECK(CardLog::n == 1); // ---- a command that never ends: nothing before the timeout, then a Timeout through the edge ------------------ +#ifndef NOCHECK start(true); rc.stall = true; App::pump(); @@ -149,9 +161,67 @@ int main() { Rfid::service(1, t0 + rc522::Interrupt::timeoutMs); CHECK(!inFlight() && Seen::n == 1 && Seen::last.failed() && Seen::last.kind() == fail::Kind::Timeout); CHECK(Rfid::failStatus(1).fails >= 1 && uint8_t(Rfid::failStatus(1).lastKind) == uint8_t(fail::Kind::Timeout)); - CHECK(st().inits == 2); // Recover initialised the chip again + CHECK(st().inits == 2); // Recover initialised the chip again: the chip's fault, with or without a fallback CHECK(CardLog::n == 0); +#endif + // ---- the services fold reaches every row of the driver --------------------------------------------------------------- + start(true); + App::pump(); + CHECK(inFlight()); + for (uint32_t t = 0; inFlight() && t < 200; ++t, ++now) Services::run(now); + CHECK(!inFlight() && CardLog::n == 1); + +#if defined(FALLBACK) + // ---- the line never fell (not connected): the row falls back to polling the register and keeps reading ------------ + start(true); + rc.lineFault = 1; + App::pump(); + CHECK(inFlight() && !rc.line()); + Rfid::service(1, now); // not asserted: waits for the timeout + CHECK(inFlight()); + Rfid::service(1, now + rc522::Interrupt::timeoutMs); + CHECK(!inFlight()); // the rest of that poll ran by the register + CHECK(CardLog::n == 1 && CardLog::log[0] == 0xDEADBEEFu); // and the card was read + CHECK(Seen::last.failed() && Seen::last.kind() == fail::Kind::Fault && Seen::last.detail == rc522::LineCheck::missed); // what the check found + const fail::FailStatus fb = Rfid::failStatus(1); + CHECK(fb.fails == 1 && uint8_t(fb.lastKind) == uint8_t(fail::Kind::Refused) && fb.lastDetail == rc522::LineCheck::missed); // what the edge heard + CHECK(fb.recovers == 0 && st().inits == 1 && App::reg.status(1) == Status::Alive); // the chip was left alone + const uint32_t seenBefore = Seen::n; + for (int i = 0; i < 40; ++i) { App::pump(); Rfid::service(1, now); now += 100; } // from now on the poll is inline: no flapping, no further reports + CHECK(!inFlight() && Seen::n == seenBefore && Rfid::failStatus(1).fails == 1 && Rfid::failStatus(1).recovers == 0); + CHECK(App::reg.status(1) == Status::Alive && st().inits == 1 && CardLog::n == 1); + State::card.present = false; + for (int i = 0; i < 3; ++i) { App::pump(); } + CHECK(CardLog::n == 2 && CardLog::log[1] == 0); // the departure is seen by the register polling too + State::card.present = true; State::card.ready = false; + App::pump(); + CHECK(CardLog::n == 3 && CardLog::log[2] == 0xDEADBEEFu); // and the arrival + // a chip initialised again tests the line again: still unplugged, it falls back again, reported once more + Rfid::reinit(1); + App::pump(); + CHECK(inFlight()); + Rfid::service(1, now); Rfid::service(1, now + rc522::Interrupt::timeoutMs); + CHECK(!inFlight() && Rfid::failStatus(1).fails == 2 && Rfid::failStatus(1).recovers == 0); + + // ---- the line fell with no request (stuck low): the same fallback, detail 1 ------------------------------ + start(true); + rc.stall = true; rc.lineFault = 2; + App::pump(); + Rfid::service(1, now); // asserted: finished at once + CHECK(!inFlight()); + CHECK(uint8_t(Rfid::failStatus(1).lastKind) == uint8_t(fail::Kind::Refused) && Rfid::failStatus(1).lastDetail == rc522::LineCheck::spurious); + CHECK(Rfid::failStatus(1).recovers == 0 && st().inits == 1); +#elif defined(NOCHECK) + // ---- no check: an unplugged line is not noticed; the command ends by the timeout and the register ----------------- + start(true); + rc.lineFault = 1; + App::pump(); + CHECK(inFlight()); + for (int i = 0; i < 4 && inFlight(); ++i) { Rfid::service(1, now); now += rc522::Interrupt::timeoutMs; Rfid::service(1, now); } + CHECK(!inFlight() && CardLog::n == 1 && CardLog::log[0] == 0xDEADBEEFu); + CHECK(Rfid::failStatus(1).fails == 0 && Rfid::failStatus(1).recovers == 0 && st().inits == 1); +#else // ---- the line never fell though the register shows the answer (not connected): Fault, detail 2 ---------------- start(true); rc.lineFault = 1; @@ -162,6 +232,7 @@ int main() { Rfid::service(1, now + rc522::Interrupt::timeoutMs); CHECK(!inFlight() && Seen::last.failed() && Seen::last.kind() == fail::Kind::Fault && Seen::last.detail == rc522::LineCheck::missed); CHECK(Rfid::failStatus(1).fails >= 1 && uint8_t(Rfid::failStatus(1).lastKind) == uint8_t(fail::Kind::Fault)); + CHECK(Rfid::failStatus(1).recovers >= 1 && st().inits == 2); // fail-fast: Recover initialised the chip CHECK(CardLog::n == 0); // the poll ended: no card is claimed on a line that disagrees // ---- the line fell and the register shows no request (stuck low): Fault, detail 1 ------------------------ @@ -171,6 +242,7 @@ int main() { CHECK(inFlight()); Rfid::service(1, now); // asserted: finished at once, no timeout wait CHECK(!inFlight() && Seen::last.failed() && Seen::last.kind() == fail::Kind::Fault && Seen::last.detail == rc522::LineCheck::spurious); +#endif if (failures) { std::printf("FAILED: %d\n", failures); return 1; } std::printf("OK: RC522 interrupt part native\n"); From fd5afb557e47ab9671a89080983ea90648b8940c Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 04:04:33 +0000 Subject: [PATCH 04/19] examples/spi: the BMP280 as a machine of OneMenu ItemDef nodes bmp280_machine.h: Machine 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>: 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 --- examples/spi/README.md | 10 ++ examples/spi/src/bmp280_machine.h | 257 ++++++++++++++++++++++++++++++ examples/spi/src/main.cpp | 56 ++++++- test/discover/bmp_machine.cpp | 112 +++++++++++++ test/discover/build_spi1.sh | 9 ++ test/support/mockBmpTwi.h | 82 ++++++++++ 6 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 examples/spi/src/bmp280_machine.h create mode 100644 test/discover/bmp_machine.cpp create mode 100644 test/support/mockBmpTwi.h diff --git a/examples/spi/README.md b/examples/spi/README.md index 6c7f258..f296135 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -37,6 +37,16 @@ interrupt part is optional as a whole: a mode without `using Irq` is the polling command runs. Keep the IRQ off the boot strapping pins (D3, D4, D8, rejected at compile time): the RC522 keeps its state across a reset of the board, and a pending request would hold such a pin low at the next boot. +## The air sensor as a machine + +The BMP280/BME280 is a static machine of OneMenu `ItemDef` nodes (`src/bmp280_machine.h`): `#0 temp` and `#1 press` (read-only values that +move when the sensor is read), `#2 cal` (the device's calibration constants: read when it is found, never state) and `#3 ctrl`, a group of +register mimics whose `get()` reads the chip and `set()` writes it; their defaults are the init. The App publishes two values under its own codes +with `Published>`: an outer node that refers to the inner one without copying it. A sync pass calls `fn(value)` for each +published value that changed, so `temp=` and `press=` appear only when they move. Key `d` prints the description: the machine's nodes, then the +published codes with their path (`/
/`), fields and whether they notify. Key `a` reads the control group by path, `o` sets +`ctrl_meas` to oversampling x1, `r` writes the registers' defaults again. + ## Build and flash ``` diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h new file mode 100644 index 0000000..dca30d0 --- /dev/null +++ b/examples/spi/src/bmp280_machine.h @@ -0,0 +1,257 @@ +// A BMP280/BME280 as a static machine of OneMenu ItemDef nodes, found by its chip id at 0x76/0x77 (0x58 BMP280, 0x60 BME280). +// +// Machine::Nodes #0 temp read-only, 0.01 C a value that moves outside set(): a sensor read +// #1 press read-only, 0.01 hPa +// #2 cal device constants: read at found(), never state, never published +// #3 ctrl a group of register mimics (get() reads the chip, set() writes it); the defaults are the init +// #0 config 0xF5 = 0x90 (standby 500 ms, filter x4) #1 ctrl_meas 0xF4 = 0x57 (T x2, P x16, normal mode) +// +// The device's data are static members of Machine::Dev (a static machine is its own type), so a node refers to them by address. +// A node is a singleton: an ItemRef names an object, and a template argument cannot name a member of another object. +// +// The App names what it publishes from outside: a Published> is an outer node with the code and a publish tag that +// refers to the inner node (ItemRef) without copying it. A sync pass over the published nodes calls fn(value) for each one that changed. +// describe() walks the machine and the published nodes and writes what a consumer needs: the codes, their path, their fields, which notify. +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace bmpm { + + using discover::RowId; + using oneMenu::ItemDef; + using oneMenu::MenuDef; + using oneMenu::StaticBody; + + // ---- what a node declares about itself, for the walk (components with no behaviour) ----------------------------------------- + // T is a tag with ONEMACHINE_STATE_NAME(name, "text"): the text is a flash array + template struct Label { template struct Part : O { using O::O; static constexpr state::Name label() { return T::name(); } }; }; + template struct Scaled { template struct Part : O { using O::O; static constexpr uint8_t decimals = N; }; }; + template struct RegAt { template struct Part : O { using O::O; static constexpr uint8_t regAddr = A, regDef = Def; }; }; + template struct ConstAt { template struct Part : O { using O::O; static constexpr uint8_t constAddr = A, constLen = N; }; }; + // the publish tag of an outer node: the code it is published under and the machine node it refers to + template struct PubTag { template struct Part : O { using O::O; using PubCode = Code; using Inner = Node; }; }; + + struct TagTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; + struct TagPress { ONEMACHINE_STATE_NAME(name, "press"); }; + struct TagCal { ONEMACHINE_STATE_NAME(name, "cal"); }; + struct TagCtrl { ONEMACHINE_STATE_NAME(name, "ctrl"); }; + struct TagConfig { ONEMACHINE_STATE_NAME(name, "config"); }; + struct TagCtrlMeas { ONEMACHINE_STATE_NAME(name, "ctrl_meas"); }; + struct TagBmp { ONEMACHINE_STATE_NAME(name, "bmp280"); }; + + // read-only: set() is deleted, everything else of W stays (oneData::ReadOnly inherits privately, which an ItemDef cannot compose) + template struct ReadOnly { + template struct Part : W::template Part { + using Base = typename W::template Part; + using Base::Base; + template void set(V&&) = delete; + }; + }; + + struct Cal { uint16_t T1, P1; int16_t T2, T3, P2, P3, P4, P5, P6, P7, P8, P9; }; + + template + struct Machine { + // the device: its data and its register access. Static, as the machine is. + struct Dev { + inline static uint8_t addr = 0; + inline static int32_t temp = 0, press = 0; // the last sample: 0.01 C, Pa + inline static Cal cal{}; // device constants + + static void rdN(uint8_t reg, uint8_t* out, uint8_t n) { + using Twi = typename W::Twi; + Twi::begin_write(addr); Twi::write_byte(reg); Twi::end_write(); + (void)Twi::request_from(addr, n); + for (uint8_t i = 0; i < n; ++i) out[i] = Twi::read_byte(); + } + static uint8_t rd(uint8_t reg) { uint8_t v = 0; rdN(reg, &v, 1); return v; } + static void wr(uint8_t reg, uint8_t v) { using Twi = typename W::Twi; Twi::begin_write(addr); Twi::write_byte(reg); Twi::write_byte(v); Twi::end_write(); } + + // the calibration is read until two reads agree: a corrupted read here would skew every later sample + static bool readCal(uint8_t* c) { + uint8_t c2[24] = {}; + rdN(0x88, c, 24); rdN(0x88, c2, 24); + for (uint8_t i = 0; i < 24; ++i) if (c[i] != c2[i]) return false; + const uint16_t t1 = uint16_t(c[0] | (c[1] << 8)), p1 = uint16_t(c[6] | (c[7] << 8)); + return t1 != 0 && t1 != 0xFFFF && p1 != 0 && p1 != 0xFFFF; + } + }; + + // a register mimic: get() reads the chip, set() writes it + template struct RegSrc { static uint8_t get() { return Dev::rd(A); } static void set(uint8_t v) { Dev::wr(A, v); } }; + template using Reg = ItemDef>, RegAt, Label>; + + using Temp = ItemDef, Label, ReadOnly>>>; + using Press = ItemDef, Label, ReadOnly>>>; + using Cal_ = ItemDef, Label>; + using Config = Reg; + using CtrlMeas = Reg; // written after config: config is written in sleep mode + using Ctrl = MenuDef>, StaticBody, Label>; + using Nodes = hapi::Chain; + + inline static Temp temp; + inline static Press press; + inline static Cal_ cal; + inline static Ctrl ctrl{ItemDef>{}, StaticBody{}}; + + // ---- path access: a node of the machine by position, then a register of the group --------------------------------------- + template static void visit(uint8_t i, F&& fn) { + switch (i) { case 0: fn(temp); break; case 1: fn(press); break; case 2: fn(cal); break; case 3: fn(ctrl); break; default: break; } + } + template static void visitReg(uint8_t i, F&& fn) { ctrl.body.visit(i, fn); } + + // ---- the registers' defaults are the init: restore from defaults writes them, in order ------------------------------------ + template static void writeDefaults(hapi::Chain*) { (Dev::wr(R::regAddr, R::regDef), ...); } + static void restoreDefaults() { writeDefaults(static_cast(nullptr)); } + + static void init(uint8_t addr) { + Dev::addr = addr; + Dev::wr(0xE0, 0xB6); // soft reset: a warm restart finds the part as the last firmware left it + for (uint8_t i = 0; i < 200; ++i) { // status bit 0 (im_update) is set while the NVM is copied in + if (!(Dev::rd(0xF3) & 1)) break; + } + uint8_t c[24] = {}; + bool ok = false; + for (uint8_t i = 0; i < 5 && !(ok = Dev::readCal(c)); ++i) {} + if (!ok) return; // calibration stays zero: poll() produces nothing + auto u = [&](uint8_t i) { return uint16_t(c[i] | (uint16_t(c[i + 1]) << 8)); }; + auto s = [&](uint8_t i) { return int16_t(u(i)); }; + auto& d = Dev::cal; + d.T1 = u(0); d.T2 = s(2); d.T3 = s(4); + d.P1 = u(6); d.P2 = s(8); d.P3 = s(10); d.P4 = s(12); d.P5 = s(14); d.P6 = s(16); d.P7 = s(18); d.P8 = s(20); d.P9 = s(22); + restoreDefaults(); + } + + // one burst read of the measurement registers, compensated (datasheet 8.2, integer form) into the two values + static void poll() { + uint8_t b[6] = {}; + Dev::rdN(0xF7, b, 6); + const int32_t adcP = int32_t((uint32_t(b[0]) << 12) | (uint32_t(b[1]) << 4) | (b[2] >> 4)); + const int32_t adcT = int32_t((uint32_t(b[3]) << 12) | (uint32_t(b[4]) << 4) | (b[5] >> 4)); + const auto& d = Dev::cal; + if (d.T1 == 0) return; // no calibration + if (adcT == 0x80000) return; // 0x80000: no conversion yet (reset value) + + const int32_t v1 = ((((adcT >> 3) - (int32_t(d.T1) << 1))) * int32_t(d.T2)) >> 11; + const int32_t v2 = (((((adcT >> 4) - int32_t(d.T1)) * ((adcT >> 4) - int32_t(d.T1))) >> 12) * int32_t(d.T3)) >> 14; + const int32_t tFine = v1 + v2; + Dev::temp = (tFine * 5 + 128) >> 8; // 0.01 C + + int64_t p1 = int64_t(tFine) - 128000; + int64_t p2 = p1 * p1 * int64_t(d.P6); + p2 = p2 + ((p1 * int64_t(d.P5)) << 17); + p2 = p2 + (int64_t(d.P4) << 35); + p1 = ((p1 * p1 * int64_t(d.P3)) >> 8) + ((p1 * int64_t(d.P2)) << 12); + p1 = (((int64_t(1) << 47) + p1) * int64_t(d.P1)) >> 33; + if (p1 == 0 || adcP == 0x80000) return; // no pressure conversion yet + int64_t p = 1048576 - adcP; + p = (((p << 31) - p2) * 3125) / p1; + p1 = (int64_t(d.P9) * (p >> 13) * (p >> 13)) >> 25; + p2 = (int64_t(d.P8) * p) >> 19; + p = ((p + p1 + p2) >> 8) + (int64_t(d.P7) << 4); + Dev::press = int32_t(p >> 8); // Pa (Q24.8 >> 8) + } + + // ---- discovery: the machine is the driver's one device ---------------------------------------------------------------- + struct Driver : discover::DriverBase { + using B = discover::DriverBase; + static constexpr bool polled = true; + static constexpr uint8_t addrLo = 0x76, addrHi = 0x77, idReg = 0xD0, id = 0x58; + static void init(RowId row) { Machine::init(B::addrOf(row)); } + static void read(RowId) { Machine::poll(); } + }; + // the BME280 answers 0x60 at the same register: a second entry, the same driver + using Entries = hapi::Chain, discover::Use, Driver>>; + + // ---- the index of a node in the machine ------------------------------------------------------------------------------- + template struct IndexOf; + template struct IndexOf> { + template struct Find { static constexpr int value = -1; }; + template struct Find { static constexpr int value = std::is_same::value ? int(I) : Find::value; }; + static constexpr int value = Find<0, T...>::value; + }; + }; + + // ---- publishing: an outer node with a code, referring to an inner node of a machine --------------------------------------------- + // Notify: OnSync (the value moves outside set) or none. Outer node = Notify..., the reference, the code. + template + using Published = ItemDef, Label, PubTag>; + + // which notify components an outer node carries + template struct OnSyncOf : std::false_type {}; + template struct OnSyncOf> : std::true_type {}; + template struct NotifiesSync : std::false_type {}; + template struct NotifiesSync> : std::bool_constant<(OnSyncOf::value || ...)> {}; + + // one sync pass over the published nodes: each outer node that changed calls its fn and the inner node takes its copy + template struct PublishAll; + template struct PublishAll> { static void sync() { (P{}.sync(), ...); } }; + + // ---- the description walk --------------------------------------------------------------------------------------------- + // machine bmp280 at 0x76 + // #0 temp ro value scaled 2 + // #2 cal const 0x88 [24] + // #3 ctrl group 2 + // #0 config reg 0xF5 default 0x90 rw + // published + // temp -> 1/76/0 notify sync ro scaled 2 + // air -> 1/76/3 group 2 + // A path is //, then a register inside a group; the identity is the device's address. + template struct HasDecimals : std::false_type {}; + template struct HasDecimals> : std::true_type {}; + template struct HasReg : std::false_type {}; + template struct HasReg> : std::true_type {}; + template struct HasConst : std::false_type {}; + template struct HasConst> : std::true_type {}; + template struct HasSet : std::false_type {}; + template struct HasSet().set(0))>> : std::true_type {}; + template struct IsGroup : std::false_type {}; + template struct IsGroup> : std::true_type {}; + + template struct Walk { + P& put; + void str(const char* s) { while (*s) put(*s++); } + void name(state::Name n) { state::put_name(put, n); } + void dec(uint32_t v) { state::put_dec(put, v); } + void hex(uint8_t v) { static const char d[] = "0123456789ABCDEF"; put('0'); put('x'); put(d[v >> 4]); put(d[v & 15]); } + + // what one node is: its fields, on the rest of the line + template void fields() { + if constexpr (IsGroup::value) { str(" group "); dec(N::Body::size()); } + else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); } + else if constexpr (HasConst::value) { str(" const "); hex(N::constAddr); str(" ["); dec(N::constLen); put(']'); } + else { str(HasSet::value ? " rw" : " ro"); str(" value"); if constexpr (HasDecimals::value) { str(" scaled "); dec(N::decimals); } } + } + + template void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } + template void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } + + template void machine(uint8_t addr) { + str("machine "); name(TagBmp::name()); str(" at "); hex(addr); put('\n'); + nodes(static_cast(nullptr)); + regs(static_cast(nullptr)); + } + + template void published(uint8_t bus, uint8_t addr) { + using Inner = typename Pub::Inner; + str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(addr); put('/'); dec(unsigned(M::template IndexOf::value)); + str(NotifiesSync::value ? " notify sync" : " silent"); fields(); put('\n'); + } + template void publishedAll(hapi::Chain*, uint8_t bus, uint8_t addr) { (published(bus, addr), ...); } + }; + + // M: the machine; Pubs: Chain; bus: the machine's position in the App; addr: the device's identity on its bus + template void describe(P& put, uint8_t bus, uint8_t addr) { + Walk

w{put}; + w.template machine(addr); + w.str("published\n"); + w.template publishedAll(static_cast(nullptr), bus, addr); + } + +} diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index fcc678a..e9c532e 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -11,6 +11,8 @@ // the register is reported to the failure edge, and the row then polls the register instead (rc522::PollOnLineFault). Not on a boot strapping pin (D3, D4, D8): the RC522 keeps its state across a reset of the // board and a pending request would hold the line low at the next boot. // +// The air sensor is a static machine of OneMenu ItemDef nodes (bmp280_machine.h); the App publishes two of its values under its own codes +// ( temp=26.99, press=1017.42) when they change, and the control group (air) by path. // Line format: []= a card's UID in hex when one arrives, 0 when it leaves (after 3 polls // without it, or when the reader stops answering). miss[1]=: polls in a row that found no card while one is held. // rfid[1] stale | gone | alive, init # | reinit, init # the reader's row, and how often it was initialised @@ -20,6 +22,8 @@ // The reader is under failure handling (fail::DevEdge): a reader that stops answering goes Stale, is probed, and is initialised again when // it answers; one that was reset without the sketch knowing (its configuration gone) is initialised again at once. A health monitor watches the // row: one that keeps flapping is quarantined (not polled) for a growing time. Faults, from the serial monitor: +// a the air control group, read by path (config, ctrl_meas) o ctrl_meas = 0x27 (oversampling x1) r the registers' defaults again +// d the air sensor's description: its machine, and what is published of it (codes, paths, fields, which ones notify) // v RST low for 3 s: the reader vanishes // p RST low for 1 ms: a silent reset, the reader still answers and has lost its configuration // IRQ rx=R tmo=T spurious=S missed=M none=N 0x: ... every 5 s, the IRQ line against ComIrqReg @@ -37,7 +41,7 @@ #include #include "rc522.h" #include "irq_esp8266.h" -#include "bmp280.h" +#include "bmp280_machine.h" #ifndef BUILD_REV #define BUILD_REV "unknown" // set by ../version.py: each repo's git commit @@ -52,7 +56,7 @@ using Spi = hapi::APIOf, esp::O oneBus::SpiMaster<4000000>, esp::Esp8266SpiCore>; struct Printer { - using Accepts = Chain; + using Accepts = Chain; template struct Body { template struct Part : T { @@ -128,7 +132,32 @@ inline void IrqCounters::report(uint32_t now) { } constexpr uint8_t rstPin = 2; // D4: the RC522's RST -struct AirApp : discover::World, bmp::Entries, 3, discover::I2cScan> {}; +struct AirApp; +using Bmp = bmpm::Machine; +struct AirApp : discover::World, Bmp::Entries, 3, discover::I2cScan> {}; + +// What the App publishes of the air sensor, under its own codes: an outer node per code that refers to the machine's node. temp and press +// call say(value) when they change (the sync pass in loop()); air is the control group, read and written by path, silent. +struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; +struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; +struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; +template static void say(int32_t v) { + Serial.print(millis()); Serial.print(' '); for (unsigned i = 0, c; (c = Code::name().rom(i)); ++i) Serial.print(char(c)); Serial.print('='); + int32_t p = 1; for (uint8_t i = 0; i < Decimals; ++i) p *= 10; + if (v < 0) Serial.print('-'); + const int32_t a = v < 0 ? -v : v; + Serial.print(a / p); Serial.print('.'); + const int32_t frac = a % p; + for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Serial.print('0'); + Serial.println(frac); +} +using PubTemp = bmpm::Published>>; +using PubPress = bmpm::Published>>; +using PubAir = bmpm::Published; +using Published = Chain; +constexpr uint8_t airBus = 1; // the air sensor's bus is the App's second machine: its path codes start with 1 + +struct SerialPut { void operator()(char c) { Serial.write(c); } }; template static void table(const __FlashStringHelper* bus) { Serial.print(bus); Serial.print(F(": ")); Serial.print(A::reg.count - 1); Serial.println(F(" device(s)")); @@ -137,6 +166,20 @@ template static void table(const __FlashStringHelper* bus) { } } +// the machine and what is published of it, for a consumer: a row's address is its identity in the path +static void describe() { + if (AirApp::reg.count < 2) return; + SerialPut put; + bmpm::describe(put, airBus, Bmp::Dev::addr); +} + +// the control group by path: node #3, then its registers #0 and #1; get() reads the chip +static void air() { + if (AirApp::reg.count < 2) return; + Serial.print(millis()); Serial.print(F(" air config=0x")); Bmp::visitReg(0, [](auto& r) { Serial.print(r.get(), HEX); }); + Serial.print(F(" ctrl_meas=0x")); Bmp::visitReg(1, [](auto& r) { Serial.print(r.get(), HEX); }); Serial.println(); +} + void setup() { Serial.begin(115200); delay(200); @@ -152,6 +195,7 @@ void setup() { AirApp::discover(); table(F("SPI slots")); table(F("I2C")); + describe(); if (RfidApp::reg.count > 1) { Serial.print(F("RC522 version 0x")); Serial.print(Rfid::rd(1, rc522::VersionReg), HEX); Serial.println(); @@ -213,6 +257,10 @@ static void faults(uint32_t now) { if (Serial.available()) { const int c = Serial.read(); if (c == 'v' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST low 3 s")); esp::OutPin::off(); vanished = true; vanishEnd = now + 3000; } + else if (c == 'd') describe(); + else if (c == 'a') air(); + else if (c == 'o') { Bmp::visitReg(1, [](auto& r) { r.set(0x27); }); air(); } // ctrl_meas: temperature x1, pressure x1, normal mode + else if (c == 'r') { Bmp::restoreDefaults(); air(); } else if (c == 'p' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST pulse")); esp::OutPin::off(); delayMicroseconds(1000); esp::OutPin::on(); @@ -251,7 +299,7 @@ void loop() { RfidApp::Health::onTick(now); logRfid(); logHealth(); - if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); } + if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); bmpm::PublishAll::sync(); } static uint32_t nextIrq = 5000; if (int32_t(now - nextIrq) >= 0) { nextIrq = now + 5000; IrqCounters::report(now); LoopStats::report(now); } } diff --git a/test/discover/bmp_machine.cpp b/test/discover/bmp_machine.cpp new file mode 100644 index 0000000..a2c8364 --- /dev/null +++ b/test/discover/bmp_machine.cpp @@ -0,0 +1,112 @@ +// The BMP280 as a static machine of OneMenu ItemDef nodes (examples/spi/src/bmp280_machine.h) against a simulated chip (test/support/mockBmpTwi.h) +// that holds the datasheet's worked example: 25.08 C and 100653.27 Pa. +// - discovery finds it, init reads the calibration and writes the registers' defaults, config first +// - one poll gives the datasheet's values; a register mimic reads and writes the real register +// - a published node calls its function once per change, not on a repeat, and the inner node takes its copy (it is referred to, not copied) +// - the description walk writes the codes, paths, fields and which ones notify +// Native only. +#include +#include +#include +#include +#include +#include +#include "../support/mockBmpTwi.h" +#include "../../examples/spi/src/bmp280_machine.h" + +using discover::RowId; +using hapi::Chain; + +struct App; +using M = bmpm::Machine; +struct App : discover::World, M::Entries, 3, discover::I2cScan> {}; + +struct TagCode { }; +struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; +struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; +struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; + +static int32_t gotT[8], gotP[8]; static int nT = 0, nP = 0; +static void onTemp(int32_t v) { if (nT < 8) gotT[nT] = v; ++nT; } +static void onPress(int32_t v) { if (nP < 8) gotP[nP] = v; ++nP; } + +using PubTemp = bmpm::Published>; +using PubPress = bmpm::Published>; +using PubAir = bmpm::Published; +using Pubs = Chain; + +static int failures = 0; +#define CHECK(...) do { if (!(__VA_ARGS__)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #__VA_ARGS__); } } while (0) + +struct Str { char b[1024]; unsigned n = 0; void operator()(char c) { if (n < sizeof(b) - 1) b[n++] = c; b[n] = 0; } }; + +int main() { + using mockbmp::State; + State::reset(); + App::discover(); + CHECK(App::reg.count == 2); + CHECK(M::Dev::addr == 0x76); + CHECK(M::Dev::cal.T1 == 27504 && M::Dev::cal.P9 == 6000 && M::Dev::cal.T3 == -1000); // the calibration read from the chip + CHECK(State::regs[0xF5] == 0x90 && State::regs[0xF4] == 0x57); // the defaults are the init + CHECK(State::nlog == 2 && State::log[0] == 0xF5 && State::log[1] == 0xF4); // config first, then ctrl_meas + CHECK(State::resets == 1); + + // ---- one poll: the datasheet's values -------------------------------------------------------------------- + App::pump(); + CHECK(M::Dev::temp == 2508); + CHECK(M::Dev::press == 100653); + + // ---- publishing: once per change ------------------------------------------------------------------------------ + CHECK(M::temp.changed() && M::press.changed()); // Watch starts from 0 + bmpm::PublishAll::sync(); + CHECK(nT == 1 && gotT[0] == 2508 && nP == 1 && gotP[0] == 100653); + CHECK(!M::temp.changed() && !M::press.changed()); // the inner node took its copy + bmpm::PublishAll::sync(); + CHECK(nT == 1 && nP == 1); // nothing moved: nothing said + App::pump(); // the same sample again + bmpm::PublishAll::sync(); + CHECK(nT == 1 && nP == 1); + State::setSample(415148, 519888 + 1600); // warmer + App::pump(); + bmpm::PublishAll::sync(); + CHECK(nT == 2 && gotT[1] > gotT[0] && nP == 2); // temperature moved; pressure moves with it (compensated against it) + + // ---- path access: a node of the machine, then a register of the group; get() and set() hit the real register ---- + uint8_t seen = 0; + M::visit(3, [&](auto& n) { (void)n; seen = 3; }); + CHECK(seen == 3); + uint8_t v = 0; + M::visitReg(1, [&](auto& r) { v = r.get(); }); + CHECK(v == 0x57); + M::visitReg(1, [&](auto& r) { r.set(0x23); }); + CHECK(State::regs[0xF4] == 0x23); + M::visitReg(0, [&](auto& r) { v = r.get(); }); + CHECK(v == 0x90); + + // ---- what the nodes are ----------------------------------------------------------------------------------------------- + CHECK(bmpm::NotifiesSync::value && bmpm::NotifiesSync::value && !bmpm::NotifiesSync::value); + CHECK(!bmpm::HasSet::value); // read-only + CHECK(bmpm::HasSet::value); + CHECK(M::IndexOf::value == 0 && M::IndexOf::value == 3); + + // ---- the description ------------------------------------------------------------------------------------------------- + Str s; + bmpm::describe(s, 1, 0x76); + static const char want[] = + "machine bmp280 at 0x76\n" + " #0 temp ro value scaled 2\n" + " #1 press ro value scaled 2\n" + " #2 cal const 0x88 [24]\n" + " #3 ctrl group 2\n" + " #0 config reg 0xF5 default 0x90 rw\n" + " #1 ctrl_meas reg 0xF4 default 0x57 rw\n" + "published\n" + " temp -> 1/118/0 notify sync ro value scaled 2\n" + " press -> 1/118/1 notify sync ro value scaled 2\n" + " air -> 1/118/3 silent group 2\n"; + if (std::strcmp(s.b, want) != 0) { ++failures; std::printf("FAIL description:\n%s--- wanted:\n%s", s.b, want); } + + if (failures) { std::printf("FAILED: %d\n", failures); return 1; } + std::printf("OK: BMP280 machine native\n"); + return 0; +} diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index 189dfaf..b3dbc3a 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -2,6 +2,7 @@ # SPI discovery round 1 verification. # native : g++ -O2 (+ASan/UBSan, + clang if present) -- the identification, empty-slot and RC522 card assertions. # rules : each compile-time rule rejects its case with its own message. +# bmp : the BMP280 as a machine of ItemDef nodes (bmp_machine.cpp, against a simulated chip with the datasheet's worked example). # irq : the RC522's interrupt part (spi_irq.cpp) and the ESP8266 delivery components' pin rules (irq_delivery.cpp). # AVR : avr-g++ -Os atmega328p, linked over the real AVR SPI core -- it builds, and its size. # Exits non-zero if an assertion fails, a rule does not fire, or the AVR image does not build. @@ -57,6 +58,14 @@ for cfg in "" "-DFALLBACK" "-DNOCHECK"; do fi done +echo; echo "=== BMP280 as a machine of ItemDef nodes (bmp_machine.cpp): g++ -O1, then ASan/UBSan, then clang++ ===" +MINC="$INC -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" +g++ -std=c++17 -O1 -Wall $MINC bmp_machine.cpp -o "$OUT/bm" && { "$OUT/bm" || rc=1; } +g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $MINC bmp_machine.cpp -o "$OUT/bmsan" && { "$OUT/bmsan" | tail -1 || rc=1; } +if command -v clang++ >/dev/null; then + clang++ -std=c++17 -O1 $MINC bmp_machine.cpp -o "$OUT/bmclang" && { "$OUT/bmclang" | tail -1 || rc=1; } +fi + echo; echo "=== ESP8266 interrupt delivery (irq_delivery.cpp, host stub): the pins that compile, the ones rejected ===" DINC="$INC -I ../support/arduino_stub" g++ -std=c++17 -Wall -Wextra -Werror $DINC irq_delivery.cpp -o "$OUT/idl" && "$OUT/idl" && echo "OK: Sampled<16> and IsrFlag<5> build" || { echo "FAIL: the delivery components do not build"; rc=1; } diff --git a/test/support/mockBmpTwi.h b/test/support/mockBmpTwi.h new file mode 100644 index 0000000..42db2a0 --- /dev/null +++ b/test/support/mockBmpTwi.h @@ -0,0 +1,82 @@ +// A simulated BMP280 on an I2C bus (0x76), behind a TwiAPI core like test/support/mockTwi.h: a 256-byte register file with the register pointer +// kept across STOP/START, the chip id at 0xD0, a soft reset at 0xE0 (0xB6) that puts ctrl_meas and config back to their reset values and keeps +// the calibration, and the calibration and raw samples of the datasheet's worked example (section 8.2): adc_T 519888, adc_P 415148 compensate +// to 25.08 C and 100653.27 Pa. +#pragma once +#include +#include +#include +#include + +namespace mockbmp { + + struct State { + inline static uint8_t regs[256]; + inline static uint8_t ptr = 0; + inline static bool present = true; + inline static uint8_t addr = 0x76; + inline static uint8_t idByte = 0x58; + inline static uint8_t log[16]; inline static uint8_t nlog = 0; // the registers written, in order (not the pointer) + inline static uint16_t resets = 0; + + enum Bus : uint8_t { Idle, Addressed, Data }; + inline static Bus bus = Idle; + inline static bool sel = false, rd = false, gotPtr = false; + + inline static uint32_t sampleP = 0, sampleT = 0; // what a conversion gives: loaded into the data registers while the mode is not sleep + static bool running() { return (regs[0xF4] & 3) != 0; } + static void load() { + const uint32_t adcP = sampleP, adcT = sampleT; + regs[0xF7] = uint8_t(adcP >> 12); regs[0xF8] = uint8_t(adcP >> 4); regs[0xF9] = uint8_t((adcP & 15) << 4); + regs[0xFA] = uint8_t(adcT >> 12); regs[0xFB] = uint8_t(adcT >> 4); regs[0xFC] = uint8_t((adcT & 15) << 4); + } + static void setSample(uint32_t adcP, uint32_t adcT) { sampleP = adcP; sampleT = adcT; if (running()) load(); } + static void softReset() { + ++resets; + regs[0xF4] = 0x00; regs[0xF5] = 0x00; regs[0xF3] = 0x00; + for (uint8_t r = 0xF7; r <= 0xFC; ++r) regs[r] = 0; + regs[0xF7] = 0x80; regs[0xFA] = 0x80; // no conversion yet: 0x80000 + } + static void reset() { + for (auto& r : regs) r = 0; + regs[0xD0] = idByte; + // 0x88..0x9F: T1 T2 T3 P1..P9, little endian + static constexpr uint8_t cal[24] = {0x70, 0x6B, 0x43, 0x67, 0x18, 0xFC, 0x7D, 0x8E, 0xBB, 0xD6, 0xD0, 0x0B, 0x27, 0x0B, 0x8C, 0x00, 0xF9, 0xFF, 0x8C, 0x3C, 0xF8, 0xC6, 0x70, 0x17}; + for (uint8_t i = 0; i < 24; ++i) regs[0x88 + i] = cal[i]; + softReset(); resets = 0; + ptr = 0; present = true; nlog = 0; bus = Idle; sel = rd = gotPtr = false; + setSample(415148, 519888); + } + }; + + struct TwiCore { + template + struct Part : O { + using Base = O; + static void twi_init(uint32_t) {} + static void twi_start() { State::bus = State::Addressed; } + static void twi_stop() { State::bus = State::Idle; } + static void twi_write(uint8_t b) { + if (State::bus == State::Addressed) { + State::sel = State::present && (b >> 1) == State::addr; State::rd = b & 1; State::gotPtr = false; State::bus = State::Data; + } else if (State::bus == State::Data && !State::rd && State::sel) { + if (!State::gotPtr) { State::ptr = b; State::gotPtr = true; return; } + const uint8_t reg = State::ptr++; + if (reg == 0xE0) { if (b == 0xB6) State::softReset(); return; } + State::regs[reg] = b; + if (reg == 0xF4 && State::running()) State::load(); // a conversion starts: its result is in the data registers + if (State::nlog < sizeof(State::log)) State::log[State::nlog++] = reg; + } + } + [[nodiscard]] static uint8_t twi_read(bool) { + if (State::bus != State::Data || !State::sel) return 0xFF; + return State::regs[State::ptr++]; + } + static void begin() { Base::begin(); } + }; + }; + + using Twi = hapi::APIOf, TwiCore>; + static_assert(oneBus::is_twi_master::value, "mockbmp::Twi must satisfy TwiMaster"); + +} From 425860f562a1111983c413202205f29857b25aea Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 04:30:11 +0000 Subject: [PATCH 05/19] BMP280 machine: capture and restore, the Criteria, nodes by path; the old driver goes - Machine: it takes its Criteria (Addr<0x76>), so two sensors are two types with their own statics. - PathRef / 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 --- examples/spi/README.md | 25 ++- examples/spi/src/bmp280.h | 94 ----------- examples/spi/src/bmp280_machine.h | 255 +++++++++++++++++++++++------- examples/spi/src/main.cpp | 52 +++++- test/discover/bmp_capture.cpp | 142 +++++++++++++++++ test/discover/bmp_machine.cpp | 106 ++++++++----- test/discover/build_spi1.sh | 32 ++-- test/support/mockBmpTwi.h | 103 +++++++----- 8 files changed, 555 insertions(+), 254 deletions(-) delete mode 100644 examples/spi/src/bmp280.h create mode 100644 test/discover/bmp_capture.cpp diff --git a/examples/spi/README.md b/examples/spi/README.md index f296135..620a751 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -39,13 +39,24 @@ reset of the board, and a pending request would hold such a pin low at the next ## The air sensor as a machine -The BMP280/BME280 is a static machine of OneMenu `ItemDef` nodes (`src/bmp280_machine.h`): `#0 temp` and `#1 press` (read-only values that -move when the sensor is read), `#2 cal` (the device's calibration constants: read when it is found, never state) and `#3 ctrl`, a group of -register mimics whose `get()` reads the chip and `set()` writes it; their defaults are the init. The App publishes two values under its own codes -with `Published>`: an outer node that refers to the inner one without copying it. A sync pass calls `fn(value)` for each -published value that changed, so `temp=` and `press=` appear only when they move. Key `d` prints the description: the machine's nodes, then the -published codes with their path (`/

/`), fields and whether they notify. Key `a` reads the control group by path, `o` sets -`ctrl_meas` to oversampling x1, `r` writes the registers' defaults again. +The BMP280/BME280 is a static machine of OneMenu `ItemDef` nodes (`src/bmp280_machine.h`), and takes its Criteria: `Machine>` +(a second sensor at 0x77 is a second type with its own data). Nodes: `#0 temp` and `#1 press` (read-only values that move when the sensor is +read), `#2 cal` (the device's calibration constants: read when it is found, never state) and `#3 ctrl`, a group of register mimics whose `get()` +reads the chip and `set()` writes it; their defaults are the init. + +The App publishes nodes under its own codes with `PublishedAt, OnSync>`: an outer node that reaches the inner one +by a compile-time path (node #3, child #1; any node, a leaf of a group too) without copying it. A sync pass calls `fn(value)` for each published +value that changed, so `temp=` and `press=` appear only when they move. Key `d` prints the description: the machine's nodes, then the published +codes with their path (`/
/[/]`), fields and whether they notify. + +Each register keeps the last value set (`Capture`), also while the sensor is gone. The sensor is under failure handling: each poll reads the control +registers back, and a register that no longer holds what was set means the part was reset behind the host's back. When the sensor is back, after +that or after it was unplugged, it is validated (the same chip id and calibration as the part that was here): validated, the last settings are +written again; another part, the settings are dropped and the defaults are the init. The log shows `STATUS air ->`, then +`air restored #n` or `air defaults #n`. + +Keys: `a` reads the control group by path, `o` sets `ctrl_meas` to oversampling x1, `q` to 0x2B (also while the sensor is unplugged), `r` writes the +registers' defaults, `x` resets the sensor behind the host's back. ## Build and flash diff --git a/examples/spi/src/bmp280.h b/examples/spi/src/bmp280.h deleted file mode 100644 index bfcd379..0000000 --- a/examples/spi/src/bmp280.h +++ /dev/null @@ -1,94 +0,0 @@ -// A BMP280/BME280 driver over I2C (temperature and pressure; a BME280's humidity is left out): found by its chip id at -// 0x76/0x77 (0x58 BMP280, 0x60 BME280: one entry each, the same driver), calibration read at found(), then normal mode -// (temperature x2, pressure x16, filter x4, 500 ms standby). Integer compensation from the datasheet (section 8.2). -#pragma once -#include -#include -#include -#include - -namespace bmp { - - using discover::RowId; - - struct Temp { using Value = int32_t; static constexpr uint8_t id = 31, decimals = 2; static constexpr const char* name = "temp"; }; - struct Press { using Value = int32_t; static constexpr uint8_t id = 32, decimals = 2; static constexpr const char* name = "hPa"; }; // Pa, read as hPa - - template - struct Bmp280 : discover::DriverBase, W> { - using B = discover::DriverBase, W>; - using Produces = hapi::Chain; - static constexpr uint8_t addrLo = 0x76, addrHi = 0x77, idReg = 0xD0, id = 0x58; - - struct DeviceState { uint16_t T1, P1; int16_t T2, T3, P2, P3, P4, P5, P6, P7, P8, P9; }; - - static void writeReg(RowId row, uint8_t reg, uint8_t v) { - using Twi = typename W::Twi; - Twi::begin_write(B::addrOf(row)); Twi::write_byte(reg); Twi::write_byte(v); Twi::end_write(); - } - - // the calibration is read until two reads agree: a corrupted read here would skew every later sample - static bool readCal(RowId row, uint8_t* c) { - uint8_t c2[24] = {}; - B::readRegs(B::addrOf(row), 0x88, c, 24); - B::readRegs(B::addrOf(row), 0x88, c2, 24); - for (uint8_t i = 0; i < 24; ++i) if (c[i] != c2[i]) return false; - const uint16_t t1 = uint16_t(c[0] | (c[1] << 8)), p1 = uint16_t(c[6] | (c[7] << 8)); - return t1 != 0 && t1 != 0xFFFF && p1 != 0 && p1 != 0xFFFF; - } - - static void init(RowId row) { - writeReg(row, 0xE0, 0xB6); // soft reset: a warm restart finds the part as the last firmware left it - for (uint8_t i = 0; i < 200; ++i) { // status bit 0 (im_update) is set while the NVM is copied in - uint8_t st = 1; - B::readRegs(B::addrOf(row), 0xF3, &st, 1); - if (!(st & 1)) break; - } - uint8_t c[24] = {}; - bool ok = false; - for (uint8_t i = 0; i < 5 && !(ok = readCal(row, c)); ++i) {} - if (!ok) return; // calibration stays zero: read() emits nothing - auto u = [&](uint8_t i) { return uint16_t(c[i] | (uint16_t(c[i + 1]) << 8)); }; - auto s = [&](uint8_t i) { return int16_t(u(i)); }; - auto& d = B::dev(row); - d.T1 = u(0); d.T2 = s(2); d.T3 = s(4); - d.P1 = u(6); d.P2 = s(8); d.P3 = s(10); d.P4 = s(12); d.P5 = s(14); d.P6 = s(16); d.P7 = s(18); d.P8 = s(20); d.P9 = s(22); - writeReg(row, 0xF5, 0x90); // standby 500 ms, filter x4 (config before ctrl_meas: written in sleep mode) - writeReg(row, 0xF4, 0x57); // temperature x2, pressure x16, normal mode - } - - static void read(RowId row) { - uint8_t b[6] = {}; - B::readRegs(B::addrOf(row), 0xF7, b, 6); - const int32_t adcP = int32_t((uint32_t(b[0]) << 12) | (uint32_t(b[1]) << 4) | (b[2] >> 4)); - const int32_t adcT = int32_t((uint32_t(b[3]) << 12) | (uint32_t(b[4]) << 4) | (b[5] >> 4)); - const auto& d = B::dev(row); - if (d.T1 == 0) return; // no calibration - if (adcT == 0x80000) return; // 0x80000: no conversion yet (reset value) - - const int32_t v1 = ((((adcT >> 3) - (int32_t(d.T1) << 1))) * int32_t(d.T2)) >> 11; - const int32_t v2 = (((((adcT >> 4) - int32_t(d.T1)) * ((adcT >> 4) - int32_t(d.T1))) >> 12) * int32_t(d.T3)) >> 14; - const int32_t tFine = v1 + v2; - B::template emit(row, (tFine * 5 + 128) >> 8); // 0.01 C - - int64_t p1 = int64_t(tFine) - 128000; - int64_t p2 = p1 * p1 * int64_t(d.P6); - p2 = p2 + ((p1 * int64_t(d.P5)) << 17); - p2 = p2 + (int64_t(d.P4) << 35); - p1 = ((p1 * p1 * int64_t(d.P3)) >> 8) + ((p1 * int64_t(d.P2)) << 12); - p1 = (((int64_t(1) << 47) + p1) * int64_t(d.P1)) >> 33; - if (p1 == 0 || adcP == 0x80000) return; // no calibration, or no pressure conversion yet - int64_t p = 1048576 - adcP; - p = (((p << 31) - p2) * 3125) / p1; - p1 = (int64_t(d.P9) * (p >> 13) * (p >> 13)) >> 25; - p2 = (int64_t(d.P8) * p) >> 19; - p = ((p + p1 + p2) >> 8) + (int64_t(d.P7) << 4); - B::template emit(row, int32_t(p >> 8)); // Pa (Q24.8 >> 8) - } - }; - - // the BME280 answers 0x60 at the same register: a second entry, the same driver - template using Entries = hapi::Chain>, - discover::Use, Bmp280>>; - -} diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index dca30d0..5bc1674 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -6,11 +6,13 @@ // #3 ctrl a group of register mimics (get() reads the chip, set() writes it); the defaults are the init // #0 config 0xF5 = 0x90 (standby 500 ms, filter x4) #1 ctrl_meas 0xF4 = 0x57 (T x2, P x16, normal mode) // -// The device's data are static members of Machine::Dev (a static machine is its own type), so a node refers to them by address. -// A node is a singleton: an ItemRef names an object, and a template argument cannot name a member of another object. +// A machine takes its Criteria, which says which device it is: Machine>. Two sensors (0x76 and 0x77) are two types, each with its +// own statics. The device's data are static members of Machine::Dev, so a node refers to them by address. A node is a singleton. // -// The App names what it publishes from outside: a Published> is an outer node with the code and a publish tag that -// refers to the inner node (ItemRef) without copying it. A sync pass over the published nodes calls fn(value) for each one that changed. +// The App names what it publishes from outside. An outer node carries the code and a publish tag that refers to the inner node without copying it: +// PublishedAt, OnSync> by a compile-time path into the machine (node #3, child #1): any node, a leaf of a group too +// Published> by an ItemRef, for a standalone object +// A sync pass over the published nodes calls fn(value) for each one that changed. // describe() walks the machine and the published nodes and writes what a consumer needs: the codes, their path, their fields, which notify. #pragma once #include @@ -20,6 +22,7 @@ #include #include #include +#include namespace bmpm { @@ -28,6 +31,9 @@ namespace bmpm { using oneMenu::MenuDef; using oneMenu::StaticBody; + // ---- the Criteria: which device a machine is ----------------------------------------------------------------------------------- + template struct Addr { static constexpr uint8_t addr = A; }; + // ---- what a node declares about itself, for the walk (components with no behaviour) ----------------------------------------- // T is a tag with ONEMACHINE_STATE_NAME(name, "text"): the text is a flash array template struct Label { template struct Part : O { using O::O; static constexpr state::Name label() { return T::name(); } }; }; @@ -35,7 +41,33 @@ namespace bmpm { template struct RegAt { template struct Part : O { using O::O; static constexpr uint8_t regAddr = A, regDef = Def; }; }; template struct ConstAt { template struct Part : O { using O::O; static constexpr uint8_t constAddr = A, constLen = N; }; }; // the publish tag of an outer node: the code it is published under and the machine node it refers to - template struct PubTag { template struct Part : O { using O::O; using PubCode = Code; using Inner = Node; }; }; + // Via: the PathRef it reaches the node by (void: an ItemRef) + template struct PubTag { template struct Part : O { using O::O; using PubCode = Code; using Inner = Node; using Path = Via; }; }; + + // ---- capture and restore (D20, D22, D13) ------------------------------------------------------------------------------------- + // The tail of the restore chain: every component that restores calls Base::restore() after its own, and this ends it. + struct RestoreEnd { template struct Part : O { using O::O; void restore() {} void forget() {} }; }; + // Keeps the last set() and replays it on restore(), then calls the tail. A set while the device is gone is captured too: the last intent is what + // comes back. forget() drops it (a part that is not the one that was here). + struct Capture { + template struct Part : O { + using Base = O; + using O::O; + using Type = typename Base::Type; + Type last{}; bool have = false; + void set(Type v) { last = v; have = true; Base::set(v); } + void restore() { if (have) Base::set(last); Base::restore(); } + void forget() { have = false; Base::forget(); } + [[nodiscard]] bool known() const { return have; } + [[nodiscard]] Type captured() const { return last; } + }; + }; + + // no failure handling: the driver polls and nothing is retried, probed or reported + struct Plain { + static constexpr bool checked = false, lifecycle = false, returnPath = false, idempotent = true; + template using DevStack = fail::Bare; + }; struct TagTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; struct TagPress { ONEMACHINE_STATE_NAME(name, "press"); }; @@ -45,24 +77,19 @@ namespace bmpm { struct TagCtrlMeas { ONEMACHINE_STATE_NAME(name, "ctrl_meas"); }; struct TagBmp { ONEMACHINE_STATE_NAME(name, "bmp280"); }; - // read-only: set() is deleted, everything else of W stays (oneData::ReadOnly inherits privately, which an ItemDef cannot compose) - template struct ReadOnly { - template struct Part : W::template Part { - using Base = typename W::template Part; - using Base::Base; - template void set(V&&) = delete; - }; - }; - struct Cal { uint16_t T1, P1; int16_t T2, T3, P2, P3, P4, P5, P6, P7, P8, P9; }; - template + template, typename Mode = Plain> struct Machine { + static constexpr uint8_t addr = Criteria::addr; // the device: its data and its register access. Static, as the machine is. struct Dev { - inline static uint8_t addr = 0; + static constexpr uint8_t addr = Criteria::addr; inline static int32_t temp = 0, press = 0; // the last sample: 0.01 C, Pa inline static Cal cal{}; // device constants + inline static uint32_t ident = 0; // the part that was here: a hash of its chip id and calibration + inline static bool known = false; // (ident is set) + inline static uint16_t restored = 0, defaulted = 0; // how often it came back and its captured state was replayed / the defaults were used static void rdN(uint8_t reg, uint8_t* out, uint8_t n) { using Twi = typename W::Twi; @@ -85,10 +112,10 @@ namespace bmpm { // a register mimic: get() reads the chip, set() writes it template struct RegSrc { static uint8_t get() { return Dev::rd(A); } static void set(uint8_t v) { Dev::wr(A, v); } }; - template using Reg = ItemDef>, RegAt, Label>; + template using Reg = ItemDef>, RegAt, Label, RestoreEnd>; - using Temp = ItemDef, Label, ReadOnly>>>; - using Press = ItemDef, Label, ReadOnly>>>; + using Temp = ItemDef, Label, oneData::ReadOnly>>>; + using Press = ItemDef, Label, oneData::ReadOnly>>>; using Cal_ = ItemDef, Label>; using Config = Reg; using CtrlMeas = Reg; // written after config: config is written in sleep mode @@ -100,38 +127,90 @@ namespace bmpm { inline static Cal_ cal; inline static Ctrl ctrl{ItemDef>{}, StaticBody{}}; - // ---- path access: a node of the machine by position, then a register of the group --------------------------------------- + // ---- path access: a node of the machine by position, then a child of a group, ... ----------------------------------------------- + // by compile-time path (D25: take the first #, call the child with the rest): the node's type, and the node itself + template struct TypeAt; + template struct TypeAt, I> { using type = typename TypeAt, I - 1>::type; }; + template struct TypeAt, 0> { using type = H; }; + template struct At { using type = N; }; + template struct At { using type = typename At::type, R...>::type; }; + template struct NodeAt { using type = typename At::type, R...>::type; }; + + template static auto& node() { + if constexpr (I == 0) return temp; else if constexpr (I == 1) return press; else if constexpr (I == 2) return cal; else return ctrl; + } + template static auto& child(B& b) { if constexpr (I == 0) return b.head; else return child(b.tail); } + template static auto& below(N& n) { + auto& c = child(n.body); + if constexpr (sizeof...(R) == 0) return c; else return below, R...>(c); + } + template static auto& resolve() { + auto& n = node(); + if constexpr (sizeof...(R) == 0) return n; else return below, R...>(n); + } + + // by run-time index: a node of the machine, then a register of the group template static void visit(uint8_t i, F&& fn) { switch (i) { case 0: fn(temp); break; case 1: fn(press); break; case 2: fn(cal); break; case 3: fn(ctrl); break; default: break; } } template static void visitReg(uint8_t i, F&& fn) { ctrl.body.visit(i, fn); } - // ---- the registers' defaults are the init: restore from defaults writes them, in order ------------------------------------ - template static void writeDefaults(hapi::Chain*) { (Dev::wr(R::regAddr, R::regDef), ...); } - static void restoreDefaults() { writeDefaults(static_cast(nullptr)); } + // ---- the registers of the control group, in order ------------------------------------------------------------------------- + template static void eachReg(F&& fn) { for (uint8_t i = 0; i < Ctrl::Body::size(); ++i) ctrl.body.visit(i, fn); } - static void init(uint8_t addr) { - Dev::addr = addr; - Dev::wr(0xE0, 0xB6); // soft reset: a warm restart finds the part as the last firmware left it - for (uint8_t i = 0; i < 200; ++i) { // status bit 0 (im_update) is set while the NVM is copied in - if (!(Dev::rd(0xF3) & 1)) break; - } - uint8_t c[24] = {}; - bool ok = false; - for (uint8_t i = 0; i < 5 && !(ok = Dev::readCal(c)); ++i) {} - if (!ok) return; // calibration stays zero: poll() produces nothing + // ---- restore: capture replayed in composition order, and the defaults as the init (D13, D20, D22, D33) ----------------------- + // restore(): each register writes its captured value, then calls the tail. forget(): the captured values are dropped (not the part that was here). + // restoreDefaults(): each register is set to its default, which captures it: restore from defaults is the device init. + static void restore() { eachReg([](auto& r) { r.restore(); }); } + static void forget() { eachReg([](auto& r) { r.forget(); }); } + static void restoreDefaults() { eachReg([](auto& r) { using R = std::remove_reference_t; r.set(R::regDef); }); } + + // ---- which part is it: a hash of its chip id and its calibration (a replaced part has its own) ------------------------------- + static uint32_t identOf(uint8_t id, const uint8_t* c) { + uint32_t h = 2166136261u; h = (h ^ id) * 16777619u; + for (uint8_t i = 0; i < 24; ++i) h = (h ^ c[i]) * 16777619u; + return h; + } + static void setCal(const uint8_t* c) { auto u = [&](uint8_t i) { return uint16_t(c[i] | (uint16_t(c[i + 1]) << 8)); }; auto s = [&](uint8_t i) { return int16_t(u(i)); }; auto& d = Dev::cal; d.T1 = u(0); d.T2 = s(2); d.T3 = s(4); d.P1 = u(6); d.P2 = s(8); d.P3 = s(10); d.P4 = s(12); d.P5 = s(14); d.P6 = s(16); d.P7 = s(18); d.P8 = s(20); d.P9 = s(22); - restoreDefaults(); } - // one burst read of the measurement registers, compensated (datasheet 8.2, integer form) into the two values - static void poll() { - uint8_t b[6] = {}; - Dev::rdN(0xF7, b, 6); + // The device is found, or is back (after it was gone, or lost its configuration). Validate: the same chip id and calibration as the part that was + // here. Validated, the captured state is replayed; not validated (a different part, or the first time), the slot is dropped and the defaults are + // the init. The calibration is device data, read again every time. + enum class How : uint8_t { Defaults, Restored }; + static How bring() { + uint8_t c[24] = {}; + bool ok = false; + for (uint8_t i = 0; i < 5 && !(ok = Dev::readCal(c)); ++i) {} + const uint32_t h = ok ? identOf(Dev::rd(0xD0), c) : 0; + if (ok && Dev::known && h == Dev::ident) { + setCal(c); restore(); ++Dev::restored; + return How::Restored; + } + forget(); + if (ok) setCal(c); else Dev::cal = Cal{}; // no calibration: poll() produces nothing + Dev::ident = h; Dev::known = ok; + restoreDefaults(); ++Dev::defaulted; + return How::Defaults; + } + + static void init() { + Dev::wr(0xE0, 0xB6); // soft reset: a warm restart finds the part as the last firmware left it + for (uint8_t i = 0; i < 200; ++i) { // status bit 0 (im_update) is set while the NVM is copied in + if (!(Dev::rd(0xF3) & 1)) break; + } + (void)bring(); + } + + static constexpr int64_t mul(int64_t v, unsigned n) { return v * (int64_t(1) << n); } + + // compensate the measurement registers (datasheet 8.2, integer form) into the two values + static void compute(const uint8_t* b) { const int32_t adcP = int32_t((uint32_t(b[0]) << 12) | (uint32_t(b[1]) << 4) | (b[2] >> 4)); const int32_t adcT = int32_t((uint32_t(b[3]) << 12) | (uint32_t(b[4]) << 4) | (b[5] >> 4)); const auto& d = Dev::cal; @@ -143,31 +222,65 @@ namespace bmpm { const int32_t tFine = v1 + v2; Dev::temp = (tFine * 5 + 128) >> 8; // 0.01 C + // the datasheet's shifts are multiplications here: left-shifting a negative value is undefined before C++20 (the same code) int64_t p1 = int64_t(tFine) - 128000; int64_t p2 = p1 * p1 * int64_t(d.P6); - p2 = p2 + ((p1 * int64_t(d.P5)) << 17); - p2 = p2 + (int64_t(d.P4) << 35); - p1 = ((p1 * p1 * int64_t(d.P3)) >> 8) + ((p1 * int64_t(d.P2)) << 12); - p1 = (((int64_t(1) << 47) + p1) * int64_t(d.P1)) >> 33; + p2 = p2 + mul(p1 * int64_t(d.P5), 17); + p2 = p2 + mul(int64_t(d.P4), 35); + p1 = ((p1 * p1 * int64_t(d.P3)) >> 8) + mul(p1 * int64_t(d.P2), 12); + p1 = (mul(1, 47) + p1) * int64_t(d.P1) >> 33; if (p1 == 0 || adcP == 0x80000) return; // no pressure conversion yet int64_t p = 1048576 - adcP; - p = (((p << 31) - p2) * 3125) / p1; + p = ((mul(p, 31) - p2) * 3125) / p1; p1 = (int64_t(d.P9) * (p >> 13) * (p >> 13)) >> 25; p2 = (int64_t(d.P8) * p) >> 19; - p = ((p + p1 + p2) >> 8) + (int64_t(d.P7) << 4); + p = ((p + p1 + p2) >> 8) + mul(int64_t(d.P7), 4); Dev::press = int32_t(p >> 8); // Pa (Q24.8 >> 8) } + // one burst read of the measurement registers + static void poll() { uint8_t b[6] = {}; Dev::rdN(0xF7, b, 6); compute(b); } // ---- discovery: the machine is the driver's one device ---------------------------------------------------------------- - struct Driver : discover::DriverBase { + // Under a failure edge (Mode::checked) each poll first reads the control registers back: a register that no longer holds what was captured + // means the part was reset without the host knowing (Corrupt: Recover calls reinit(), which validates it and replays the capture). A part that + // stops answering is Absent, retried and probed; when it answers again the edge calls reinit() the same way. + struct Driver : discover::DriverBase, fail::DevEdge { using B = discover::DriverBase; + using Edge = fail::DevEdge; static constexpr bool polled = true; - static constexpr uint8_t addrLo = 0x76, addrHi = 0x77, idReg = 0xD0, id = 0x58; - static void init(RowId row) { Machine::init(B::addrOf(row)); } - static void read(RowId) { Machine::poll(); } + static constexpr uint8_t addrLo = Criteria::addr, addrHi = Criteria::addr, idReg = 0xD0, id = 0x58; + static constexpr uint8_t recoverMask = fail::bit(fail::Kind::Corrupt); + static constexpr bool reinitOnBusReturn = true; // a bus that comes back may have taken the part's supply: validate it again + static void init(RowId) { Machine::init(); } + static void reinit(RowId) { (void)Machine::bring(); } + static void read(RowId row) { Edge::serve(row, fail::Cause::Fresh); } + static fail::Outcome attempt(RowId row) { + uint8_t b[6] = {}; + if constexpr (Mode::checked) { + fail::Outcome o = fail::Outcome::Ok(); + bool lost = false; uint8_t which = 0; + Machine::eachReg([&](auto& r) { + using R = std::remove_reference_t; + if (!o.isOk() || lost) return; + uint8_t v = 0; + o = Edge::checkedRead(row, R::regAddr, &v, 1); + if (o.isOk() && r.known() && v != r.captured()) { lost = true; which = R::regAddr; } + }); + if (!o.isOk()) return o; + if (lost) return fail::Outcome::Fail(fail::Kind::Corrupt, which); + o = Edge::checkedRead(row, 0xF7, b, 6); + if (!o.isOk()) return o; + } else { + Dev::rdN(0xF7, b, 6); + } + compute(b); + return fail::Outcome::Ok(); + } }; // the BME280 answers 0x60 at the same register: a second entry, the same driver - using Entries = hapi::Chain, discover::Use, Driver>>; + using UseBmp = discover::Use; + using UseBme = discover::Use, Driver>; + using Entries = hapi::Chain; // ---- the index of a node in the machine ------------------------------------------------------------------------------- template struct IndexOf; @@ -183,6 +296,26 @@ namespace bmpm { template using Published = ItemDef, Label, PubTag>; + // a node of a machine by compile-time path: P is the position in the machine, then in each group below it. It reaches any node, a leaf of + // a group too, by resolving the path to the node's singleton (or to the child inside a group) on use. get(), changed() and sync() are the node's. + template + struct PathRef { + static_assert(sizeof...(P) > 0, "PathRef: a path names a node of the machine"); + using Node = typename M::template NodeAt::type; + static constexpr unsigned depth = sizeof...(P); + static constexpr unsigned path[sizeof...(P)] = {P...}; + template struct Part : O { + using O::O; + static Node& ref() { return M::template resolve(); } + static decltype(auto) get() { return ref().get(); } + [[nodiscard]] static bool changed() { return ref().changed(); } + static void sync() { ref().sync(); } + }; + }; + // an outer node reaching its inner node by path + template + using PublishedAt = ItemDef, PubTag>; + // which notify components an outer node carries template struct OnSyncOf : std::false_type {}; template struct OnSyncOf> : std::true_type {}; @@ -200,8 +333,8 @@ namespace bmpm { // #3 ctrl group 2 // #0 config reg 0xF5 default 0x90 rw // published - // temp -> 1/76/0 notify sync ro scaled 2 - // air -> 1/76/3 group 2 + // temp -> 1/118/0 notify sync ro scaled 2 + // air -> 1/118/3 group 2 // A path is //, then a register inside a group; the identity is the device's address. template struct HasDecimals : std::false_type {}; template struct HasDecimals> : std::true_type {}; @@ -232,26 +365,28 @@ namespace bmpm { template void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } template void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } - template void machine(uint8_t addr) { - str("machine "); name(TagBmp::name()); str(" at "); hex(addr); put('\n'); + template void machine() { + str("machine "); name(TagBmp::name()); str(" at "); hex(M::addr); put('\n'); nodes(static_cast(nullptr)); regs(static_cast(nullptr)); } - template void published(uint8_t bus, uint8_t addr) { + template void published(uint8_t bus) { using Inner = typename Pub::Inner; - str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(addr); put('/'); dec(unsigned(M::template IndexOf::value)); + str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(M::addr); + if constexpr (std::is_void::value) { put('/'); dec(unsigned(M::template IndexOf::value)); } + else for (unsigned i = 0; i < Pub::Path::depth; ++i) { put('/'); dec(Pub::Path::path[i]); } str(NotifiesSync::value ? " notify sync" : " silent"); fields(); put('\n'); } - template void publishedAll(hapi::Chain*, uint8_t bus, uint8_t addr) { (published(bus, addr), ...); } + template void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } }; - // M: the machine; Pubs: Chain; bus: the machine's position in the App; addr: the device's identity on its bus - template void describe(P& put, uint8_t bus, uint8_t addr) { + // M: the machine (its Criteria is the device's identity in the path); Pubs: Chain; bus: the machine's position in the App + template void describe(P& put, uint8_t bus) { Walk

w{put}; - w.template machine(addr); + w.template machine(); w.str("published\n"); - w.template publishedAll(static_cast(nullptr), bus, addr); + w.template publishedAll(static_cast(nullptr), bus); } } diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index e9c532e..1b049f2 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -22,6 +22,8 @@ // The reader is under failure handling (fail::DevEdge): a reader that stops answering goes Stale, is probed, and is initialised again when // it answers; one that was reset without the sketch knowing (its configuration gone) is initialised again at once. A health monitor watches the // row: one that keeps flapping is quarantined (not polled) for a growing time. Faults, from the serial monitor: +// q ctrl_meas = 0x2B (also while the sensor is unplugged: the last intent is what comes back) +// x the air sensor is reset behind the host's back: the canary sees the registers lost, the part is validated and its last settings come back // a the air control group, read by path (config, ctrl_meas) o ctrl_meas = 0x27 (oversampling x1) r the registers' defaults again // d the air sensor's description: its machine, and what is published of it (codes, paths, fields, which ones notify) // v RST low for 3 s: the reader vanishes @@ -132,15 +134,32 @@ inline void IrqCounters::report(uint32_t now) { } constexpr uint8_t rstPin = 2; // D4: the RC522's RST +// The air sensor under failure handling: a bus edge and, per device, retry, recover (the part was reset without the host knowing), probe. +struct AirMode { + static constexpr bool checked = true, returnPath = false, idempotent = true, lifecycle = true; + template using BusStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Backoff<100, 400>, fail::Status>; + template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; +}; struct AirApp; -using Bmp = bmpm::Machine; -struct AirApp : discover::World, Bmp::Entries, 3, discover::I2cScan> {}; +using Bmp = bmpm::Machine, AirMode>; // the sensor at 0x76: its Criteria +using AirDrivers = Chain; +struct AirApp : discover::World, Bmp::Entries, 3, discover::I2cScan>, fail::BusEdge { + static constexpr bool lifecycle = true; + static void release(RowId) {} + static void unbindAll() {} + static void busReset() { Twi::begin(); } +}; +using AirTicker = fail::Ticks; // What the App publishes of the air sensor, under its own codes: an outer node per code that refers to the machine's node. temp and press -// call say(value) when they change (the sync pass in loop()); air is the control group, read and written by path, silent. +// call say(value) when they change (the sync pass in loop()); air is the control group and ctrl_meas a register of it, silent. +// Each reaches its node by a compile-time path into the machine (PathRef is node #3, child #1). struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; +struct CodeCtrlMeas { ONEMACHINE_STATE_NAME(name, "ctrl_meas"); }; template static void say(int32_t v) { Serial.print(millis()); Serial.print(' '); for (unsigned i = 0, c; (c = Code::name().rom(i)); ++i) Serial.print(char(c)); Serial.print('='); int32_t p = 1; for (uint8_t i = 0; i < Decimals; ++i) p *= 10; @@ -151,10 +170,11 @@ template static void say(int32_t v) { for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Serial.print('0'); Serial.println(frac); } -using PubTemp = bmpm::Published>>; -using PubPress = bmpm::Published>>; -using PubAir = bmpm::Published; -using Published = Chain; +using PubTemp = bmpm::PublishedAt, oneData::OnSync<&say>>; +using PubPress = bmpm::PublishedAt, oneData::OnSync<&say>>; +using PubAir = bmpm::PublishedAt>; +using PubCtrlMeas = bmpm::PublishedAt>; // a leaf of the group, by its path +using Published = Chain; constexpr uint8_t airBus = 1; // the air sensor's bus is the App's second machine: its path codes start with 1 struct SerialPut { void operator()(char c) { Serial.write(c); } }; @@ -170,7 +190,18 @@ template static void table(const __FlashStringHelper* bus) { static void describe() { if (AirApp::reg.count < 2) return; SerialPut put; - bmpm::describe(put, airBus, Bmp::Dev::addr); + bmpm::describe(put, airBus); +} + +// the air sensor's row: a status change, and each time it came back (its captured state replayed, or the defaults because it was another part) +static void logAir() { + static discover::Status last = discover::Status::Alive; + static uint16_t lastRestored = 0, lastDefaulted = 0; + if (AirApp::reg.count < 2) return; + const discover::Status st = AirApp::reg.status(1); + if (st != last) { Serial.print(F("STATUS ")); Serial.print(millis()); Serial.print(F(" air ")); Serial.print(uint8_t(last)); Serial.print(F("->")); Serial.println(uint8_t(st)); last = st; } + if (Bmp::Dev::restored != lastRestored) { lastRestored = Bmp::Dev::restored; Serial.print(millis()); Serial.print(F(" air restored #")); Serial.println(lastRestored); } + if (Bmp::Dev::defaulted != lastDefaulted) { lastDefaulted = Bmp::Dev::defaulted; Serial.print(millis()); Serial.print(F(" air defaults #")); Serial.println(lastDefaulted); } } // the control group by path: node #3, then its registers #0 and #1; get() reads the chip @@ -259,7 +290,9 @@ static void faults(uint32_t now) { if (c == 'v' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST low 3 s")); esp::OutPin::off(); vanished = true; vanishEnd = now + 3000; } else if (c == 'd') describe(); else if (c == 'a') air(); + else if (c == 'x') { Serial.print(now); Serial.println(F(" fault: air sensor soft reset")); Bmp::Dev::wr(0xE0, 0xB6); } else if (c == 'o') { Bmp::visitReg(1, [](auto& r) { r.set(0x27); }); air(); } // ctrl_meas: temperature x1, pressure x1, normal mode + else if (c == 'q') { Bmp::visitReg(1, [](auto& r) { r.set(0x2B); }); air(); } // ctrl_meas: temperature x1, pressure x2, normal mode else if (c == 'r') { Bmp::restoreDefaults(); air(); } else if (c == 'p' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST pulse")); @@ -294,10 +327,13 @@ void loop() { const uint8_t miss = RfidApp::reg.count > 1 ? RfidApp::devState(1).missStreak : 0; if (miss != lastMiss) { lastMiss = miss; if (miss) { Serial.print(millis()); Serial.print(F(" miss[1]=")); Serial.println(miss); } } } + AirApp::tickBuses(now); + AirTicker::run(now); RfidTicker::run(now); RfidApp::Health::onEdge(); RfidApp::Health::onTick(now); logRfid(); + logAir(); logHealth(); if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); bmpm::PublishAll::sync(); } static uint32_t nextIrq = 5000; diff --git a/test/discover/bmp_capture.cpp b/test/discover/bmp_capture.cpp new file mode 100644 index 0000000..94d8e55 --- /dev/null +++ b/test/discover/bmp_capture.cpp @@ -0,0 +1,142 @@ +// Capture and restore on the BMP280 machine, under a failure edge (fail::DevEdge over the I2C bus edge), against a simulated chip (mockBmpTwi.h). +// - found: the defaults are the init and are captured; the slot (the row, the machine's statics) is kept while the part is gone +// - a set is captured; the part is reset without the host knowing (soft reset): the canary sees a register that no longer holds the capture, +// Recover calls reinit(), the part is validated (same chip id and calibration) and the capture is replayed +// - the part is unplugged: Stale, probed; a set while it is gone is captured too; plugged in again (reset values), the last intent comes back +// - another part (its own calibration) or another chip id: not validated, the slot is dropped and the defaults are the init +// Native only. Time is simulated: the poll every 100 ms, the edges ticked every 10 ms. +#include +#include +#include +#include +#include +#include +#include +#include "../support/mockBmpTwi.h" +#include "../../examples/spi/src/bmp280_machine.h" + +using discover::RowId; +using discover::Status; +using hapi::Chain; + +struct Mode { + static constexpr bool checked = true, returnPath = false, idempotent = true, lifecycle = true; + template using BusStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Backoff<100, 400>, fail::Status>; + template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; +}; + +struct App; +using M = bmpm::Machine, Mode>; +using Drivers = Chain; +struct App : discover::World, M::Entries, 3, discover::I2cScan>, fail::BusEdge { + static constexpr bool lifecycle = true; + static void release(RowId) {} + static void unbindAll() {} + static void busReset() { mockbmp::Twi::begin(); } +}; +using Ticker = fail::Ticks; + +static int failures = 0; +#define CHECK(...) do { if (!(__VA_ARGS__)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #__VA_ARGS__); } } while (0) + +static uint32_t now = 0; +static bool leftAlive = false; +static void advance(uint32_t ms) { + for (const uint32_t end = now + ms; now < end; now += 10) { + if (now % 100 == 0) App::pump(); + App::tickBuses(now); + Ticker::run(now); + if (App::reg.status(1) != Status::Alive) leftAlive = true; + } +} +static bool runUntil(Status s, uint32_t limitMs) { + for (uint32_t t = 0; t < limitMs; t += 10) { if (App::reg.status(1) == s) return true; advance(10); } + return App::reg.status(1) == s; +} +static void start() { + mockbmp::State::reset(); + M::Dev::known = false; M::Dev::restored = M::Dev::defaulted = 0; M::Dev::temp = M::Dev::press = 0; + M::Driver::resetFail(); + now = 0; leftAlive = false; + App::discover(); +} +static uint8_t cap(unsigned i) { uint8_t v = 0; M::visitReg(uint8_t(i), [&](auto& r) { v = r.captured(); }); return v; } +static bool known(unsigned i) { bool k = false; M::visitReg(uint8_t(i), [&](auto& r) { k = r.known(); }); return k; } + +int main() { + using mockbmp::State; + auto& c = State::c76; + + // ---- found: the defaults are the init, and are captured ---------------------------------------------------------------------- + start(); + CHECK(App::reg.count == 2 && App::reg.status(1) == Status::Alive); + CHECK(M::Dev::defaulted == 1 && M::Dev::restored == 0); + CHECK(c.regs[0xF5] == 0x90 && c.regs[0xF4] == 0x57); + CHECK(known(0) && known(1) && cap(0) == 0x90 && cap(1) == 0x57); + CHECK(M::Dev::known); + advance(500); + CHECK(M::Dev::temp == 2508 && M::Dev::press == 100653); + CHECK(M::Driver::failStatus(1).fails == 0 && M::Driver::failStatus(1).recovers == 0 && !leftAlive); + + // ---- a set is captured, and written to the real register --------------------------------------------------------------------- + M::resolve<3, 1>().set(0x27); + CHECK(c.regs[0xF4] == 0x27 && cap(1) == 0x27); + advance(500); + CHECK(M::Driver::failStatus(1).fails == 0 && c.regs[0xF4] == 0x27); // the canary reads back what was set: nothing to report + + // ---- the part is reset without the host knowing: the capture comes back ------------------------------------------------------ + c.softReset(); // ctrl_meas and config at their reset values, calibration kept + CHECK(c.regs[0xF4] == 0x00 && c.regs[0xF5] == 0x00); + advance(300); + CHECK(c.regs[0xF4] == 0x27 && c.regs[0xF5] == 0x90); // replayed, not the defaults + CHECK(M::Dev::restored == 1 && M::Dev::defaulted == 1); + CHECK(M::Driver::failStatus(1).recovers == 1 && uint8_t(M::Driver::failStatus(1).lastKind) == uint8_t(fail::Kind::Corrupt)); + CHECK(App::reg.status(1) == Status::Alive && !leftAlive && App::reg.count == 2); // the row never left Alive + advance(500); + CHECK(M::Dev::restored == 1); // and it does not repeat: the canary is satisfied + + // ---- unplugged: Stale and probed; a set while it is gone is captured; plugged in again, the last intent comes back ---------- + c.unplug(); + CHECK(runUntil(Status::Stale, 3000)); + CHECK(App::reg.count == 2); // the slot is kept on loss + M::resolve<3, 1>().set(0x2B); // the write is not acknowledged; the intent is captured + CHECK(cap(1) == 0x2B && c.regs[0xF4] == 0x27); + advance(1000); + CHECK(App::reg.status(1) == Status::Stale); // still gone: probed, not given up + c.replug(); // powered up again: reset values + CHECK(c.regs[0xF4] == 0x00); + CHECK(runUntil(Status::Alive, 3000)); + advance(200); + CHECK(c.regs[0xF4] == 0x2B && c.regs[0xF5] == 0x90); // the last intent, not the value before the loss + CHECK(M::Dev::restored == 2 && M::Dev::defaulted == 1); + advance(500); + CHECK(M::Dev::temp == 2508); // and it measures again + + // ---- another part in its place (its own calibration): not validated, the slot is dropped, the defaults are the init ------------- + const uint32_t before = M::Dev::ident; + c.unplug(); + CHECK(runUntil(Status::Stale, 3000)); + c.regs[0x8B] = 0x70; // a different part: T2 differs + c.replug(); + CHECK(runUntil(Status::Alive, 3000)); + advance(200); + CHECK(c.regs[0xF4] == 0x57 && c.regs[0xF5] == 0x90); // the defaults, not 0x2B + CHECK(cap(1) == 0x57 && known(1)); // the old capture is gone; the defaults are captured + CHECK(M::Dev::defaulted == 2 && M::Dev::restored == 2 && M::Dev::ident != before); + CHECK(M::Dev::cal.T2 == 0x7043); // its calibration, read again + + // ---- another chip id (a BME280 in place of the BMP280): validation fails, the defaults are the init ----------------------- + M::resolve<3, 1>().set(0x27); + c.setId(0x60); + CHECK(M::bring() == M::How::Defaults); + CHECK(c.regs[0xF4] == 0x57 && M::Dev::defaulted == 3); + c.setId(0x58); + CHECK(M::bring() == M::How::Defaults); // the hash is of the part now known (the BME280's): a third change + CHECK(M::bring() == M::How::Restored); // the same part: validated, replayed + + if (failures) { std::printf("FAILED: %d\n", failures); return 1; } + std::printf("OK: BMP280 capture and restore native\n"); + return 0; +} diff --git a/test/discover/bmp_machine.cpp b/test/discover/bmp_machine.cpp index a2c8364..6dc4e75 100644 --- a/test/discover/bmp_machine.cpp +++ b/test/discover/bmp_machine.cpp @@ -1,8 +1,10 @@ -// The BMP280 as a static machine of OneMenu ItemDef nodes (examples/spi/src/bmp280_machine.h) against a simulated chip (test/support/mockBmpTwi.h) -// that holds the datasheet's worked example: 25.08 C and 100653.27 Pa. -// - discovery finds it, init reads the calibration and writes the registers' defaults, config first +// The BMP280 as a static machine of OneMenu ItemDef nodes (examples/spi/src/bmp280_machine.h) against simulated chips (test/support/mockBmpTwi.h): +// 0x76 holds the datasheet's worked example (25.08 C, 100653.27 Pa), 0x77 is another part with a calibration of its own. +// - two machines, Machine> and Machine>: two types, each with its own statics, both found +// - discovery finds a chip, init reads the calibration and writes the registers' defaults, config first // - one poll gives the datasheet's values; a register mimic reads and writes the real register -// - a published node calls its function once per change, not on a repeat, and the inner node takes its copy (it is referred to, not copied) +// - a published node calls its function once per change, not on a repeat, and the inner node takes its copy: reached by a compile-time path +// (PathRef, any node, a leaf of a group too) or by an ItemRef (a standalone object) // - the description walk writes the codes, paths, fields and which ones notify // Native only. #include @@ -18,22 +20,28 @@ using discover::RowId; using hapi::Chain; struct App; -using M = bmpm::Machine; -struct App : discover::World, M::Entries, 3, discover::I2cScan> {}; +using M = bmpm::Machine>; +using M77 = bmpm::Machine>; +struct App : discover::World, Chain, 4, discover::I2cScan> {}; -struct TagCode { }; -struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; -struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; -struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; +struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; +struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; +struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; +struct CodeCtrlMeas { ONEMACHINE_STATE_NAME(name, "ctrl_meas"); }; -static int32_t gotT[8], gotP[8]; static int nT = 0, nP = 0; +static int32_t gotT[8], gotP[8], gotI[8]; static int nT = 0, nP = 0, nI = 0; static void onTemp(int32_t v) { if (nT < 8) gotT[nT] = v; ++nT; } static void onPress(int32_t v) { if (nP < 8) gotP[nP] = v; ++nP; } +static void onTempRef(int32_t v) { if (nI < 8) gotI[nI] = v; ++nI; } -using PubTemp = bmpm::Published>; -using PubPress = bmpm::Published>; -using PubAir = bmpm::Published; -using Pubs = Chain; +// by path +using PubTemp = bmpm::PublishedAt, oneData::OnSync<&onTemp>>; +using PubPress = bmpm::PublishedAt, oneData::OnSync<&onPress>>; +using PubAir = bmpm::PublishedAt>; +using PubCtrlMeas = bmpm::PublishedAt>; // a leaf inside the group +using Pubs = Chain; +// by an ItemRef to the standalone object +using PubTempRef = bmpm::Published>; static int failures = 0; #define CHECK(...) do { if (!(__VA_ARGS__)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #__VA_ARGS__); } } while (0) @@ -44,54 +52,76 @@ int main() { using mockbmp::State; State::reset(); App::discover(); - CHECK(App::reg.count == 2); - CHECK(M::Dev::addr == 0x76); + CHECK(App::reg.count == 3); // the root, and one row per machine + CHECK(M::Dev::addr == 0x76 && M77::Dev::addr == 0x77); // the Criteria + CHECK(!std::is_same::value); // two types, two sets of statics CHECK(M::Dev::cal.T1 == 27504 && M::Dev::cal.P9 == 6000 && M::Dev::cal.T3 == -1000); // the calibration read from the chip - CHECK(State::regs[0xF5] == 0x90 && State::regs[0xF4] == 0x57); // the defaults are the init - CHECK(State::nlog == 2 && State::log[0] == 0xF5 && State::log[1] == 0xF4); // config first, then ctrl_meas - CHECK(State::resets == 1); + CHECK(M77::Dev::cal.T2 != M::Dev::cal.T2); // 0x77 is another part + CHECK(State::c76.regs[0xF5] == 0x90 && State::c76.regs[0xF4] == 0x57); // the defaults are the init + CHECK(State::c76.nlog == 2 && State::c76.log[0] == 0xF5 && State::c76.log[1] == 0xF4); // config first, then ctrl_meas + CHECK(State::c76.resets == 1 && State::c77.resets == 1); + CHECK(State::c77.regs[0xF5] == 0x90 && State::c77.regs[0xF4] == 0x57); - // ---- one poll: the datasheet's values -------------------------------------------------------------------- + // ---- one poll: the datasheet's values ------------------------------------------------------------------------- App::pump(); CHECK(M::Dev::temp == 2508); CHECK(M::Dev::press == 100653); + CHECK(M77::Dev::temp != M::Dev::temp); // its own calibration, its own value + const int32_t t77 = M77::Dev::temp; + State::c77.setSample(415148, 519888 + 3200); // only the other chip moves + App::pump(); + CHECK(M::Dev::temp == 2508 && M77::Dev::temp > t77); // the statics are separate - // ---- publishing: once per change ------------------------------------------------------------------------------ - CHECK(M::temp.changed() && M::press.changed()); // Watch starts from 0 + // ---- publishing by path: once per change ----------------------------------------------------------------------------- + CHECK(M::temp.changed() && M::press.changed()); // Watch starts from 0 bmpm::PublishAll::sync(); CHECK(nT == 1 && gotT[0] == 2508 && nP == 1 && gotP[0] == 100653); - CHECK(!M::temp.changed() && !M::press.changed()); // the inner node took its copy + CHECK(!M::temp.changed() && !M::press.changed()); // the inner node took its copy bmpm::PublishAll::sync(); - CHECK(nT == 1 && nP == 1); // nothing moved: nothing said - App::pump(); // the same sample again + CHECK(nT == 1 && nP == 1); // nothing moved: nothing said + App::pump(); bmpm::PublishAll::sync(); CHECK(nT == 1 && nP == 1); - State::setSample(415148, 519888 + 1600); // warmer + State::c76.setSample(415148, 519888 + 1600); // warmer App::pump(); bmpm::PublishAll::sync(); - CHECK(nT == 2 && gotT[1] > gotT[0] && nP == 2); // temperature moved; pressure moves with it (compensated against it) + CHECK(nT == 2 && gotT[1] > gotT[0] && nP == 2); + + // ---- publishing by an ItemRef to the same node: the same behaviour ----------------------------------------------------- + State::c76.setSample(415148, 519888 + 3200); + App::pump(); + bmpm::PublishAll>::sync(); + CHECK(nI == 1 && gotI[0] > gotT[1]); + bmpm::PublishAll>::sync(); + CHECK(nI == 1); + + // ---- a leaf of a group published by path: its value is the real register --------------------------------------------- + CHECK(PubCtrlMeas{}.get() == 0x57); + CHECK(PubAir::Inner::Body::size() == 2); + M::resolve<3, 1>().set(0x23); // by path, to the leaf + CHECK(State::c76.regs[0xF4] == 0x23 && PubCtrlMeas{}.get() == 0x23); + M::resolve<3, 1>().set(0x57); - // ---- path access: a node of the machine, then a register of the group; get() and set() hit the real register ---- - uint8_t seen = 0; + // ---- run-time path: a node of the machine, then a register of the group ------------------------------------------------- + uint8_t seen = 0, v = 0; M::visit(3, [&](auto& n) { (void)n; seen = 3; }); CHECK(seen == 3); - uint8_t v = 0; M::visitReg(1, [&](auto& r) { v = r.get(); }); CHECK(v == 0x57); - M::visitReg(1, [&](auto& r) { r.set(0x23); }); - CHECK(State::regs[0xF4] == 0x23); M::visitReg(0, [&](auto& r) { v = r.get(); }); CHECK(v == 0x90); + CHECK(&M::resolve<0>() == &M::temp && &M::resolve<3>() == &M::ctrl); // ---- what the nodes are ----------------------------------------------------------------------------------------------- CHECK(bmpm::NotifiesSync::value && bmpm::NotifiesSync::value && !bmpm::NotifiesSync::value); - CHECK(!bmpm::HasSet::value); // read-only + CHECK(!bmpm::HasSet::value); // read-only CHECK(bmpm::HasSet::value); CHECK(M::IndexOf::value == 0 && M::IndexOf::value == 3); + CHECK(std::is_same::type, M::CtrlMeas>::value && std::is_same::type, M::Temp>::value); // ---- the description ------------------------------------------------------------------------------------------------- Str s; - bmpm::describe(s, 1, 0x76); + bmpm::describe(s, 1); static const char want[] = "machine bmp280 at 0x76\n" " #0 temp ro value scaled 2\n" @@ -103,8 +133,12 @@ int main() { "published\n" " temp -> 1/118/0 notify sync ro value scaled 2\n" " press -> 1/118/1 notify sync ro value scaled 2\n" - " air -> 1/118/3 silent group 2\n"; + " air -> 1/118/3 silent group 2\n" + " ctrl_meas -> 1/118/3/1 silent reg 0xF4 default 0x57 rw\n"; if (std::strcmp(s.b, want) != 0) { ++failures; std::printf("FAIL description:\n%s--- wanted:\n%s", s.b, want); } + Str s2; + bmpm::describe>(s2, 1); + CHECK(std::strstr(s2.b, "machine bmp280 at 0x77\n") != nullptr); // the other machine names its own device if (failures) { std::printf("FAILED: %d\n", failures); return 1; } std::printf("OK: BMP280 machine native\n"); diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index b3dbc3a..6ae3436 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -2,7 +2,8 @@ # SPI discovery round 1 verification. # native : g++ -O2 (+ASan/UBSan, + clang if present) -- the identification, empty-slot and RC522 card assertions. # rules : each compile-time rule rejects its case with its own message. -# bmp : the BMP280 as a machine of ItemDef nodes (bmp_machine.cpp, against a simulated chip with the datasheet's worked example). +# bmp : the BMP280 as a machine of ItemDef nodes (bmp_machine.cpp, against a simulated chip with the datasheet's worked example), and its +# capture and restore under a failure edge: reset behind the host's back, unplugged, replaced (bmp_capture.cpp). # irq : the RC522's interrupt part (spi_irq.cpp) and the ESP8266 delivery components' pin rules (irq_delivery.cpp). # AVR : avr-g++ -Os atmega328p, linked over the real AVR SPI core -- it builds, and its size. # Exits non-zero if an assertion fails, a rule does not fire, or the AVR image does not build. @@ -11,6 +12,8 @@ cd "$(dirname "$0")" INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include" OUT=$(mktemp -d); trap 'rm -rf "$OUT"' EXIT rc=0 +# run a test binary: its last line, and a failure (its exit status, not tail's) when it does not exit 0 +run() { local o s; o=$("$@" 2>&1); s=$?; printf '%s\n' "$o" | tail -1; if [ $s -ne 0 ]; then echo "FAIL: $* exited $s"; printf '%s\n' "$o" | head -4; rc=1; fi; } echo "=== native g++ $(g++ -dumpversion) -O2 ===" g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi1.cpp -o "$OUT/s1" @@ -18,32 +21,32 @@ g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi1.cpp -o "$OUT/s1" echo; echo "=== native -O1 -fsanitize=address,undefined ===" g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $INC spi1.cpp -o "$OUT/s1san" -"$OUT/s1san" | tail -1 || rc=1 +run "$OUT/s1san" if command -v clang++ >/dev/null; then echo; echo "=== native clang++ $(clang++ -dumpversion) -O2 ===" clang++ -std=c++17 -O2 -Wall -Wextra $INC spi1.cpp -o "$OUT/s1clang" - "$OUT/s1clang" | tail -1 || rc=1 + run "$OUT/s1clang" fi echo; echo "=== RC522 under the failure edge (spi_fail.cpp): g++ -O2, then ASan/UBSan ===" g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi_fail.cpp -o "$OUT/sf" "$OUT/sf" || rc=1 g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $INC spi_fail.cpp -o "$OUT/sfsan" -"$OUT/sfsan" | tail -1 || rc=1 +run "$OUT/sfsan" if command -v clang++ >/dev/null; then clang++ -std=c++17 -O2 -Wall -Wextra $INC spi_fail.cpp -o "$OUT/sfclang" - "$OUT/sfclang" | tail -1 || rc=1 + run "$OUT/sfclang" fi echo; echo "=== RC522 under the health monitor (spi_health.cpp): g++ -O2, then ASan/UBSan ===" g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $INC spi_health.cpp -o "$OUT/sh" "$OUT/sh" || rc=1 g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $INC spi_health.cpp -o "$OUT/shsan" -"$OUT/shsan" | tail -1 || rc=1 +run "$OUT/shsan" if command -v clang++ >/dev/null; then clang++ -std=c++17 -O2 -Wall -Wextra $INC spi_health.cpp -o "$OUT/shclang" - "$OUT/shclang" | tail -1 || rc=1 + run "$OUT/shclang" fi echo; echo "=== RC522 interrupt part (spi_irq.cpp): fail-fast, -DFALLBACK, -DNOCHECK; g++ -O2, then ASan/UBSan ===" @@ -51,19 +54,26 @@ for cfg in "" "-DFALLBACK" "-DNOCHECK"; do g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror $cfg $INC spi_irq.cpp -o "$OUT/si" echo "[${cfg:-default}] $("$OUT/si")" || rc=1 g++ -std=c++17 -O1 -g -Wall -Wextra -fsanitize=address,undefined -fno-sanitize-recover=all $cfg $INC spi_irq.cpp -o "$OUT/sisan" - "$OUT/sisan" | tail -1 || rc=1 + run "$OUT/sisan" if command -v clang++ >/dev/null; then clang++ -std=c++17 -O2 -Wall -Wextra $cfg $INC spi_irq.cpp -o "$OUT/siclang" - "$OUT/siclang" | tail -1 || rc=1 + run "$OUT/siclang" fi done echo; echo "=== BMP280 as a machine of ItemDef nodes (bmp_machine.cpp): g++ -O1, then ASan/UBSan, then clang++ ===" MINC="$INC -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" g++ -std=c++17 -O1 -Wall $MINC bmp_machine.cpp -o "$OUT/bm" && { "$OUT/bm" || rc=1; } -g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $MINC bmp_machine.cpp -o "$OUT/bmsan" && { "$OUT/bmsan" | tail -1 || rc=1; } +g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $MINC bmp_machine.cpp -o "$OUT/bmsan" && { run "$OUT/bmsan"; } if command -v clang++ >/dev/null; then - clang++ -std=c++17 -O1 $MINC bmp_machine.cpp -o "$OUT/bmclang" && { "$OUT/bmclang" | tail -1 || rc=1; } + clang++ -std=c++17 -O1 $MINC bmp_machine.cpp -o "$OUT/bmclang" && { run "$OUT/bmclang"; } +fi + +echo; echo "=== BMP280 machine, capture and restore under a failure edge (bmp_capture.cpp): g++ -O1, ASan/UBSan, clang++ ===" +g++ -std=c++17 -O1 -Wall $MINC bmp_capture.cpp -o "$OUT/bc" && { "$OUT/bc" || rc=1; } +g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $MINC bmp_capture.cpp -o "$OUT/bcsan" && { run "$OUT/bcsan"; } +if command -v clang++ >/dev/null; then + clang++ -std=c++17 -O1 $MINC bmp_capture.cpp -o "$OUT/bcclang" && { run "$OUT/bcclang"; } fi echo; echo "=== ESP8266 interrupt delivery (irq_delivery.cpp, host stub): the pins that compile, the ones rejected ===" diff --git a/test/support/mockBmpTwi.h b/test/support/mockBmpTwi.h index 42db2a0..b1a41fc 100644 --- a/test/support/mockBmpTwi.h +++ b/test/support/mockBmpTwi.h @@ -1,7 +1,11 @@ -// A simulated BMP280 on an I2C bus (0x76), behind a TwiAPI core like test/support/mockTwi.h: a 256-byte register file with the register pointer -// kept across STOP/START, the chip id at 0xD0, a soft reset at 0xE0 (0xB6) that puts ctrl_meas and config back to their reset values and keeps -// the calibration, and the calibration and raw samples of the datasheet's worked example (section 8.2): adc_T 519888, adc_P 415148 compensate -// to 25.08 C and 100653.27 Pa. +// Simulated BMP280s on an I2C bus (0x76 and 0x77), behind a TwiAPI core that reports acknowledgement and the cause of a failure, as a real +// bus does (test/support/mockAck.h): an address nobody answers is not acknowledged (cause Nack). +// Each chip is a 256-byte register file with the register pointer kept across STOP/START, the chip id at 0xD0, a soft reset at 0xE0 (0xB6) +// that puts ctrl_meas and config back to their reset values and keeps the calibration, and a conversion that starts when ctrl_meas leaves +// sleep. Chip 0x76 holds the datasheet's worked example (section 8.2): calibration, adc_T 519888 and adc_P 415148 compensate to 25.08 C and +// 100653.27 Pa. Chip 0x77 has a calibration of its own, so a part that replaces another is told by it. +// present false: the chip is unplugged (its address is not acknowledged); true again: it is back, as after power-up (reset values) +// idByte 0x58 BMP280, 0x60 BME280 #pragma once #include #include @@ -10,67 +14,90 @@ namespace mockbmp { - struct State { - inline static uint8_t regs[256]; - inline static uint8_t ptr = 0; - inline static bool present = true; - inline static uint8_t addr = 0x76; - inline static uint8_t idByte = 0x58; - inline static uint8_t log[16]; inline static uint8_t nlog = 0; // the registers written, in order (not the pointer) - inline static uint16_t resets = 0; - - enum Bus : uint8_t { Idle, Addressed, Data }; - inline static Bus bus = Idle; - inline static bool sel = false, rd = false, gotPtr = false; + struct Chip { + uint8_t addr = 0; + uint8_t regs[256] = {}; + uint8_t ptr = 0; + bool present = true; + uint8_t idByte = 0x58; + uint8_t log[32] = {}; uint8_t nlog = 0; // the registers written, in order (not the pointer) + uint16_t resets = 0; + uint32_t sampleP = 0, sampleT = 0; // what a conversion gives: loaded into the data registers while the mode is not sleep - inline static uint32_t sampleP = 0, sampleT = 0; // what a conversion gives: loaded into the data registers while the mode is not sleep - static bool running() { return (regs[0xF4] & 3) != 0; } - static void load() { - const uint32_t adcP = sampleP, adcT = sampleT; - regs[0xF7] = uint8_t(adcP >> 12); regs[0xF8] = uint8_t(adcP >> 4); regs[0xF9] = uint8_t((adcP & 15) << 4); - regs[0xFA] = uint8_t(adcT >> 12); regs[0xFB] = uint8_t(adcT >> 4); regs[0xFC] = uint8_t((adcT & 15) << 4); + bool running() const { return (regs[0xF4] & 3) != 0; } + void load() { + regs[0xF7] = uint8_t(sampleP >> 12); regs[0xF8] = uint8_t(sampleP >> 4); regs[0xF9] = uint8_t((sampleP & 15) << 4); + regs[0xFA] = uint8_t(sampleT >> 12); regs[0xFB] = uint8_t(sampleT >> 4); regs[0xFC] = uint8_t((sampleT & 15) << 4); } - static void setSample(uint32_t adcP, uint32_t adcT) { sampleP = adcP; sampleT = adcT; if (running()) load(); } - static void softReset() { + void setSample(uint32_t adcP, uint32_t adcT) { sampleP = adcP; sampleT = adcT; if (running()) load(); } + void softReset() { ++resets; regs[0xF4] = 0x00; regs[0xF5] = 0x00; regs[0xF3] = 0x00; for (uint8_t r = 0xF7; r <= 0xFC; ++r) regs[r] = 0; - regs[0xF7] = 0x80; regs[0xFA] = 0x80; // no conversion yet: 0x80000 + regs[0xF7] = 0x80; regs[0xFA] = 0x80; // no conversion yet: 0x80000 } - static void reset() { + void unplug() { present = false; } + void replug() { present = true; softReset(); } // powered up again: reset values, calibration kept + void reset(uint8_t a, bool second) { for (auto& r : regs) r = 0; + addr = a; idByte = 0x58; present = true; nlog = 0; resets = 0; ptr = 0; regs[0xD0] = idByte; // 0x88..0x9F: T1 T2 T3 P1..P9, little endian static constexpr uint8_t cal[24] = {0x70, 0x6B, 0x43, 0x67, 0x18, 0xFC, 0x7D, 0x8E, 0xBB, 0xD6, 0xD0, 0x0B, 0x27, 0x0B, 0x8C, 0x00, 0xF9, 0xFF, 0x8C, 0x3C, 0xF8, 0xC6, 0x70, 0x17}; for (uint8_t i = 0; i < 24; ++i) regs[0x88 + i] = cal[i]; + if (second) regs[0x8B] = 0x70; // T2 differs (0x7043, not 0x6743): another part softReset(); resets = 0; - ptr = 0; present = true; nlog = 0; bus = Idle; sel = rd = gotPtr = false; setSample(415148, 519888); } + void setId(uint8_t id) { idByte = id; regs[0xD0] = id; } + }; + + struct State { + inline static Chip c76, c77; + enum Bus : uint8_t { Idle, Addressed, Data }; + inline static Bus bus = Idle; + inline static Chip* sel = nullptr; + inline static bool rd = false, gotPtr = false; + inline static oneBus::TwiCause cause = oneBus::TwiCause::None; + inline static uint32_t writes = 0; // data bytes written to any chip + + static Chip* find(uint8_t a) { Chip* c = a == 0x76 ? &c76 : a == 0x77 ? &c77 : nullptr; return c && c->present ? c : nullptr; } + static void reset() { + c76.reset(0x76, false); c77.reset(0x77, true); + bus = Idle; sel = nullptr; rd = gotPtr = false; cause = oneBus::TwiCause::None; writes = 0; + } }; struct TwiCore { template struct Part : O { using Base = O; + static oneBus::TwiCause twi_cause() { return State::cause; } static void twi_init(uint32_t) {} - static void twi_start() { State::bus = State::Addressed; } + static bool twi_start() { State::cause = oneBus::TwiCause::None; State::bus = State::Addressed; return true; } static void twi_stop() { State::bus = State::Idle; } - static void twi_write(uint8_t b) { + static bool twi_write(uint8_t b) { if (State::bus == State::Addressed) { - State::sel = State::present && (b >> 1) == State::addr; State::rd = b & 1; State::gotPtr = false; State::bus = State::Data; - } else if (State::bus == State::Data && !State::rd && State::sel) { - if (!State::gotPtr) { State::ptr = b; State::gotPtr = true; return; } - const uint8_t reg = State::ptr++; - if (reg == 0xE0) { if (b == 0xB6) State::softReset(); return; } - State::regs[reg] = b; - if (reg == 0xF4 && State::running()) State::load(); // a conversion starts: its result is in the data registers - if (State::nlog < sizeof(State::log)) State::log[State::nlog++] = reg; + State::sel = State::find(b >> 1); State::rd = b & 1; State::gotPtr = false; State::bus = State::Data; + if (!State::sel) { State::cause = oneBus::TwiCause::Nack; return false; } + return true; + } + if (State::bus == State::Data && !State::rd && State::sel) { + Chip& c = *State::sel; + if (!State::gotPtr) { c.ptr = b; State::gotPtr = true; return true; } + ++State::writes; + const uint8_t reg = c.ptr++; + if (reg == 0xE0) { if (b == 0xB6) c.softReset(); return true; } + c.regs[reg] = b; + if (reg == 0xF4 && c.running()) c.load(); // a conversion starts: its result is in the data registers + if (c.nlog < sizeof(c.log)) c.log[c.nlog++] = reg; + return true; } + return State::sel != nullptr; } [[nodiscard]] static uint8_t twi_read(bool) { if (State::bus != State::Data || !State::sel) return 0xFF; - return State::regs[State::ptr++]; + return State::sel->regs[State::sel->ptr++]; } static void begin() { Base::begin(); } }; From 655c70f226760e85c9b4d74f212f4c0bf0653338 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 05:43:08 +0000 Subject: [PATCH 06/19] A Python consumer of the machine tree over the existing link - 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 --- examples/spi/README.md | 20 +++ examples/spi/platformio.ini | 5 + examples/spi/rig_session.py | 73 ++++++++++ examples/spi/src/bmp280_machine.h | 10 +- examples/spi/src/main.cpp | 225 +++++++++++++++++++----------- examples/spi/src/tree_ops.h | 122 ++++++++++++++++ include/oneMachine/link/frame.h | 54 +++++++ python/onemachine/__init__.py | 1 + python/onemachine/tree.py | 147 +++++++++++++++++++ test/discover/bmp_machine.cpp | 6 +- test/discover/build_spi1.sh | 4 + test/link/build.sh | 13 ++ test/link/check_tree.py | 94 +++++++++++++ test/link/tree_device.cpp | 128 +++++++++++++++++ 14 files changed, 817 insertions(+), 85 deletions(-) create mode 100644 examples/spi/rig_session.py create mode 100644 examples/spi/src/tree_ops.h create mode 100644 include/oneMachine/link/frame.h create mode 100644 python/onemachine/tree.py create mode 100755 test/link/build.sh create mode 100644 test/link/check_tree.py create mode 100644 test/link/tree_device.cpp diff --git a/examples/spi/README.md b/examples/spi/README.md index 620a751..b7cab77 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -58,6 +58,26 @@ written again; another part, the settings are dropped and the defaults are the i Keys: `a` reads the control group by path, `o` sets `ctrl_meas` to oversampling x1, `q` to 0x2B (also while the sensor is unplugged), `r` writes the registers' defaults, `x` resets the sensor behind the host's back. +## A Python consumer over the serial port + +`pio run -e d1_mini_link -t upload` builds the same sketch with the serial port carrying the link (`link/frame.h`: the request and response framing of +`role/link.h`) instead of the log. `python/onemachine` reads it, with the package's own `StreamLink`: + +```python +import serial +from onemachine import Tree, StreamLink +ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=2) +m = Tree(StreamLink(ser.read, ser.write, ser.flush)) # reads the description +m.temp, m.press # 27.62, 1026.68: scaled as the description says (0.01 C, 0.01 hPa) +m.air.ctrl_meas = 0x27 # set by code; read-only and out-of-range are refused here, before anything is sent +m.changes() # [Change('temp', 27.61, 2761), Change('card', 4062320374, ...)]: what changed since the last call +``` + +Ops: `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`, `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. + ## Build and flash ``` diff --git a/examples/spi/platformio.ini b/examples/spi/platformio.ini index 2a880c3..c8b45e5 100644 --- a/examples/spi/platformio.ini +++ b/examples/spi/platformio.ini @@ -8,3 +8,8 @@ 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 + +; 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 diff --git a/examples/spi/rig_session.py b/examples/spi/rig_session.py new file mode 100644 index 0000000..625a5fb --- /dev/null +++ b/examples/spi/rig_session.py @@ -0,0 +1,73 @@ +"""A Python consumer of the spi example's link build (env d1_mini_link), over the serial port: the description, the values, a set by code and its +read-back, a reset behind the host's back, the notifications; with --unplug it also waits for the air sensor to be unplugged and plugged in again, +and with --watch N it prints the notifications (the RFID card too) for N seconds. + + ~/.platformio/penv/bin/python examples/spi/rig_session.py /dev/ttyUSB0 [--unplug] [--watch 30]""" +import os, sys, time +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'python')) +import serial +from onemachine import Tree, StreamLink, OutOfRange, ReadOnly, UnknownCode + +port = sys.argv[1] if len(sys.argv) > 1 else '/dev/ttyUSB0' +args = sys.argv[2:] +ser = serial.Serial(port, 115200, timeout=2) +ser.dtr = False; ser.rts = True; time.sleep(0.1); ser.rts = False # reset the board; the link build says nothing at boot +time.sleep(2.0); ser.reset_input_buffer() +m = Tree(StreamLink(ser.read, ser.write, ser.flush)) +t0 = time.time() +def say(s): print('%6.1f %s' % (time.time() - t0, s), flush=True) + +say('>>> m = Tree(StreamLink(ser.read, ser.write, ser.flush))') +for c in m.codes.values(): + say(' %-14s #%d path %-9s %-6s %s%s%s%s' % (c.name, c.num, '/'.join(map(str, c.path)), c.kind, 'ro' if c.ro else 'rw', + ' scaled %d' % c.scaled if c.scaled else '', ' range %d..%d default 0x%02X' % (c.lo, c.hi, c.default) if c.lo is not None else '', + ' notifies (%s)' % c.notify if c.notify else '')) +say('>>> m.temp, m.press -> %r, %r' % (m.temp, m.press)) +say('>>> m.air.config, m.air.ctrl_meas -> 0x%02X, 0x%02X' % (m.air.config, m.air.ctrl_meas)) +say('>>> m.changes() -> %r' % m.changes()) +time.sleep(3) +say('>>> m.changes() (3 s later) -> %r' % m.changes()) + +say('>>> m.air.ctrl_meas = 0x27') +m.air.ctrl_meas = 0x27 +say('>>> m.air.ctrl_meas -> 0x%02X (read back from the chip)' % m.air.ctrl_meas) +for bad in (300, -1): + try: m.air.ctrl_meas = bad + except OutOfRange as e: say('>>> m.air.ctrl_meas = %d -> OutOfRange: %s' % (bad, e)) +try: m.temp = 1 +except ReadOnly as e: say('>>> m.temp = 1 -> ReadOnly: %s' % e) +try: m.nothing +except UnknownCode as e: say('>>> m.nothing -> UnknownCode: %s' % str(e)[:60] + '...') +say('>>> m.air.ctrl_meas -> 0x%02X (nothing was sent for those)' % m.air.ctrl_meas) + +say('>>> m.fault("x") # the air sensor is reset behind the host\'s back') +m.fault('x') +time.sleep(2.5) +say('>>> m.air.config, m.air.ctrl_meas -> 0x%02X, 0x%02X (the setting came back: 0x27, not the default 0x57)' % (m.air.config, m.air.ctrl_meas)) + +if '--unplug' in args: + say('--- unplug the air sensor (all four wires), wait, plug it in again (any time in the next 60 s)') + gone = back = False + seen = [] + t = time.time() + while time.time() - t < 60 and not back: + seen += m.changes() # drain while waiting: the device's queue holds 8 + v = m.raw('air/ctrl_meas') + if not gone and v != 0x27: + gone = True; say(' the sensor is gone (ctrl_meas reads 0x%02X, not 0x27)' % v); time.sleep(2.0) + m.air.ctrl_meas = 0x2B; say('>>> m.air.ctrl_meas = 0x2B (while it is gone: captured as the last intent)') + elif gone and v == 0x2B: + back = True; time.sleep(0.5) + say(' the sensor is back: ctrl_meas = 0x%02X (0x2B: the last intent), config = 0x%02X' % (m.air.ctrl_meas, m.air.config)) + time.sleep(0.4) + if not back: say(' (the sensor was not seen going away and coming back)') + say('>>> m.changes() while waiting -> %d notifications (temp/press only on change); missed so far: %d' % (len(seen), m.missed)) + +if '--watch' in args: + secs = int(args[args.index('--watch') + 1]) + say('--- watching the notifications for %d s (move the RFID tag on and off the reader)' % secs) + t = time.time() + while time.time() - t < secs: + for c in m.changes(): say(' %r' % (c,)) + time.sleep(0.5) + say(' missed (refused by the device): %d' % m.missed) diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index 5bc1674..e70750f 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -39,6 +39,8 @@ namespace bmpm { template struct Label { template struct Part : O { using O::O; static constexpr state::Name label() { return T::name(); } }; }; template struct Scaled { template struct Part : O { using O::O; static constexpr uint8_t decimals = N; }; }; template struct RegAt { template struct Part : O { using O::O; static constexpr uint8_t regAddr = A, regDef = Def; }; }; + // the values a node accepts, inclusive: a set outside them is refused (the description carries them, so a consumer checks before it sends) + template struct Limits { template struct Part : O { using O::O; static constexpr long limLo = Lo, limHi = Hi; }; }; template struct ConstAt { template struct Part : O { using O::O; static constexpr uint8_t constAddr = A, constLen = N; }; }; // the publish tag of an outer node: the code it is published under and the machine node it refers to // Via: the PathRef it reaches the node by (void: an ItemRef) @@ -112,7 +114,7 @@ namespace bmpm { // a register mimic: get() reads the chip, set() writes it template struct RegSrc { static uint8_t get() { return Dev::rd(A); } static void set(uint8_t v) { Dev::wr(A, v); } }; - template using Reg = ItemDef>, RegAt, Label, RestoreEnd>; + template using Reg = ItemDef>, RegAt, Limits<0, 255>, Label, RestoreEnd>; using Temp = ItemDef, Label, oneData::ReadOnly>>>; using Press = ItemDef, Label, oneData::ReadOnly>>>; @@ -310,6 +312,7 @@ namespace bmpm { static decltype(auto) get() { return ref().get(); } [[nodiscard]] static bool changed() { return ref().changed(); } static void sync() { ref().sync(); } + template static void set(V&& v) { ref().set(std::forward(v)); } // through the node: its limits, its capture, its register }; }; // an outer node reaching its inner node by path @@ -340,6 +343,8 @@ namespace bmpm { template struct HasDecimals> : std::true_type {}; template struct HasReg : std::false_type {}; template struct HasReg> : std::true_type {}; + template struct HasLimits : std::false_type {}; + template struct HasLimits> : std::true_type {}; template struct HasConst : std::false_type {}; template struct HasConst> : std::true_type {}; template struct HasSet : std::false_type {}; @@ -357,7 +362,8 @@ namespace bmpm { // what one node is: its fields, on the rest of the line template void fields() { if constexpr (IsGroup::value) { str(" group "); dec(N::Body::size()); } - else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); } + else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); + if constexpr (HasLimits::value) { str(" range "); dec(uint32_t(N::limLo)); str(".."); dec(uint32_t(N::limHi)); } } else if constexpr (HasConst::value) { str(" const "); hex(N::constAddr); str(" ["); dec(N::constLen); put(']'); } else { str(HasSet::value ? " rw" : " ro"); str(" value"); if constexpr (HasDecimals::value) { str(" scaled "); dec(N::decimals); } } } diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 1b049f2..545b4ab 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -43,12 +43,25 @@ #include #include "rc522.h" #include "irq_esp8266.h" +#ifdef ONEMACHINE_LINK + #include "tree_ops.h" // the link's ops over the published nodes (link/frame.h) +#endif #include "bmp280_machine.h" #ifndef BUILD_REV #define BUILD_REV "unknown" // set by ../version.py: each repo's git commit #endif +// What the sketch says about itself goes to Log: the serial monitor. In the link build (env d1_mini_link, -DONEMACHINE_LINK) the serial port carries +// the link's frames instead (link/frame.h), and the log says nothing. +#ifdef ONEMACHINE_LINK +struct NullPrint : Print { size_t write(uint8_t) override { return 1; } size_t write(const uint8_t*, size_t n) override { return n; } }; +static NullPrint nullPrint; +static Print& Log = nullPrint; +#else +static Print& Log = Serial; +#endif + using discover::RowId; using hapi::Chain; namespace esp = hw::esp8266; @@ -57,22 +70,43 @@ using Twi = esp::Esp8266TwiMaster<4, 5, 100000>; using Spi = hapi::APIOf, esp::OutPin<0>>, // slot 0 D8, slot 1 D3 oneBus::SpiMaster<4000000>, esp::Esp8266SpiCore>; +// The codes the App publishes (the numbers are the codes' positions in the description: the link's change records use them) +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"); }; + +#ifdef ONEMACHINE_LINK +// the card, as a change the link can read: its UID when it arrives, 0 when it leaves +struct CardNotifier { + using Accepts = Chain; + template struct Body { + template struct Part : T { + using T::T; + void on(const discover::Sample& s) { bmpm::ChangeQueue<8>::note(CodeCard::num, int32_t(s.value)); } + }; + }; +}; +#endif + struct Printer { using Accepts = Chain; template struct Body { template struct Part : T { void on(const discover::Sample& s) { - Serial.print(millis()); Serial.print(' '); Serial.print(Cap::name); Serial.print('['); - Serial.print(s.row); Serial.print(F("]=")); - if constexpr (Cap::decimals == 0) { Serial.println(uint32_t(s.value), HEX); return; } + Log.print(millis()); Log.print(' '); Log.print(Cap::name); Log.print('['); + Log.print(s.row); Log.print(F("]=")); + if constexpr (Cap::decimals == 0) { Log.println(uint32_t(s.value), HEX); return; } const int32_t v = int32_t(s.value); int32_t p = 1; for (uint8_t i = 0; i < Cap::decimals; ++i) p *= 10; - if (v < 0) Serial.print('-'); + if (v < 0) Log.print('-'); const int32_t a = v < 0 ? -v : v; - Serial.print(a / p); Serial.print('.'); + Log.print(a / p); Log.print('.'); const int32_t frac = a % p; - for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Serial.print('0'); - Serial.println(frac); + for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Log.print('0'); + Log.println(frac); } }; }; @@ -108,7 +142,12 @@ struct RfidMode { }; using Rfid = rc522::Rc522; using RfidDrivers = discover::DriversIn>; -struct RfidApp : discover::World, Chain, 3, discover::SpiScan, +#ifdef ONEMACHINE_LINK +using RfidConsumers = Chain; +#else +using RfidConsumers = Chain; +#endif +struct RfidApp : discover::World, 3, discover::SpiScan, Chain>>, fail::DeviceOwnStale { static constexpr bool lifecycle = true; @@ -120,17 +159,17 @@ using RfidTicker = fail::Ticks; using RfidServices = fail::Services; inline void IrqCounters::report(uint32_t now) { - Serial.print(F("IRQ ")); Serial.print(now); - Serial.print(F(" rx=")); Serial.print(rx); Serial.print(F(" tmo=")); Serial.print(tmo); - Serial.print(F(" spurious=")); Serial.print(spurious); Serial.print(F(" missed=")); Serial.print(missed); - Serial.print(F(" none=")); Serial.print(none); - for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Serial.print(F(" 0x")); Serial.print(vals[i], HEX); Serial.print(':'); Serial.print(cnt[i]); } + Log.print(F("IRQ ")); Log.print(now); + Log.print(F(" rx=")); Log.print(rx); Log.print(F(" tmo=")); Log.print(tmo); + Log.print(F(" spurious=")); Log.print(spurious); Log.print(F(" missed=")); Log.print(missed); + Log.print(F(" none=")); Log.print(none); + for (uint8_t i = 0; i < 8 && cnt[i]; ++i) { Log.print(F(" 0x")); Log.print(vals[i], HEX); Log.print(':'); Log.print(cnt[i]); } if (RfidApp::reg.count > 1) { // what the failure edge heard of the reader's row const fail::FailStatus f = Rfid::failStatus(1); - Serial.print(F(" | edge fails=")); Serial.print(f.fails); Serial.print(F(" recovers=")); Serial.print(f.recovers); - Serial.print(F(" last=")); Serial.print(f.lastKind); Serial.print('/'); Serial.print(f.lastDetail); + Log.print(F(" | edge fails=")); Log.print(f.fails); Log.print(F(" recovers=")); Log.print(f.recovers); + Log.print(F(" last=")); Log.print(f.lastKind); Log.print('/'); Log.print(f.lastDetail); } - Serial.println(); + Log.println(); } constexpr uint8_t rstPin = 2; // D4: the RC522's RST @@ -156,33 +195,33 @@ using AirTicker = fail::Ticks; // What the App publishes of the air sensor, under its own codes: an outer node per code that refers to the machine's node. temp and press // call say(value) when they change (the sync pass in loop()); air is the control group and ctrl_meas a register of it, silent. // Each reaches its node by a compile-time path into the machine (PathRef is node #3, child #1). -struct CodeTemp { ONEMACHINE_STATE_NAME(name, "temp"); }; -struct CodePress { ONEMACHINE_STATE_NAME(name, "press"); }; -struct CodeAir { ONEMACHINE_STATE_NAME(name, "air"); }; -struct CodeCtrlMeas { ONEMACHINE_STATE_NAME(name, "ctrl_meas"); }; template static void say(int32_t v) { - Serial.print(millis()); Serial.print(' '); for (unsigned i = 0, c; (c = Code::name().rom(i)); ++i) Serial.print(char(c)); Serial.print('='); + Log.print(millis()); Log.print(' '); for (unsigned i = 0, c; (c = Code::name().rom(i)); ++i) Log.print(char(c)); Log.print('='); int32_t p = 1; for (uint8_t i = 0; i < Decimals; ++i) p *= 10; - if (v < 0) Serial.print('-'); + if (v < 0) Log.print('-'); const int32_t a = v < 0 ? -v : v; - Serial.print(a / p); Serial.print('.'); + Log.print(a / p); Log.print('.'); const int32_t frac = a % p; - for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Serial.print('0'); - Serial.println(frac); + for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Log.print('0'); + Log.println(frac); +#ifdef ONEMACHINE_LINK + bmpm::ChangeQueue<8>::note(Code::num, v); // and for the consumer that reads changes over the link +#endif } using PubTemp = bmpm::PublishedAt, oneData::OnSync<&say>>; using PubPress = bmpm::PublishedAt, oneData::OnSync<&say>>; using PubAir = bmpm::PublishedAt>; -using PubCtrlMeas = bmpm::PublishedAt>; // a leaf of the group, by its path -using Published = Chain; +using PubConfig = bmpm::PublishedAt>; // the leaves of the group, by their paths +using PubCtrlMeas = bmpm::PublishedAt>; +using Published = Chain; constexpr uint8_t airBus = 1; // the air sensor's bus is the App's second machine: its path codes start with 1 -struct SerialPut { void operator()(char c) { Serial.write(c); } }; +struct SerialPut { void operator()(char c) { Log.write(c); } }; template static void table(const __FlashStringHelper* bus) { - Serial.print(bus); Serial.print(F(": ")); Serial.print(A::reg.count - 1); Serial.println(F(" device(s)")); + Log.print(bus); Log.print(F(": ")); Log.print(A::reg.count - 1); Log.println(F(" device(s)")); for (RowId r = 1; r < A::reg.count; ++r) { - Serial.print(F(" row ")); Serial.print(r); Serial.print(F(" at 0x")); Serial.println(A::reg.rows[r].busId, HEX); + Log.print(F(" row ")); Log.print(r); Log.print(F(" at 0x")); Log.println(A::reg.rows[r].busId, HEX); } } @@ -199,25 +238,25 @@ static void logAir() { static uint16_t lastRestored = 0, lastDefaulted = 0; if (AirApp::reg.count < 2) return; const discover::Status st = AirApp::reg.status(1); - if (st != last) { Serial.print(F("STATUS ")); Serial.print(millis()); Serial.print(F(" air ")); Serial.print(uint8_t(last)); Serial.print(F("->")); Serial.println(uint8_t(st)); last = st; } - if (Bmp::Dev::restored != lastRestored) { lastRestored = Bmp::Dev::restored; Serial.print(millis()); Serial.print(F(" air restored #")); Serial.println(lastRestored); } - if (Bmp::Dev::defaulted != lastDefaulted) { lastDefaulted = Bmp::Dev::defaulted; Serial.print(millis()); Serial.print(F(" air defaults #")); Serial.println(lastDefaulted); } + if (st != last) { Log.print(F("STATUS ")); Log.print(millis()); Log.print(F(" air ")); Log.print(uint8_t(last)); Log.print(F("->")); Log.println(uint8_t(st)); last = st; } + if (Bmp::Dev::restored != lastRestored) { lastRestored = Bmp::Dev::restored; Log.print(millis()); Log.print(F(" air restored #")); Log.println(lastRestored); } + if (Bmp::Dev::defaulted != lastDefaulted) { lastDefaulted = Bmp::Dev::defaulted; Log.print(millis()); Log.print(F(" air defaults #")); Log.println(lastDefaulted); } } // the control group by path: node #3, then its registers #0 and #1; get() reads the chip static void air() { if (AirApp::reg.count < 2) return; - Serial.print(millis()); Serial.print(F(" air config=0x")); Bmp::visitReg(0, [](auto& r) { Serial.print(r.get(), HEX); }); - Serial.print(F(" ctrl_meas=0x")); Bmp::visitReg(1, [](auto& r) { Serial.print(r.get(), HEX); }); Serial.println(); + Log.print(millis()); Log.print(F(" air config=0x")); Bmp::visitReg(0, [](auto& r) { Log.print(r.get(), HEX); }); + Log.print(F(" ctrl_meas=0x")); Bmp::visitReg(1, [](auto& r) { Log.print(r.get(), HEX); }); Log.println(); } void setup() { Serial.begin(115200); delay(200); - Serial.println(F("\nOneMachine SPI + I2C discovery")); - Serial.println(F("build " BUILD_REV " " __DATE__ " " __TIME__)); + Log.println(F("\nOneMachine SPI + I2C discovery")); + Log.println(F("build " BUILD_REV " " __DATE__ " " __TIME__)); Rfid::Irq::begin(); - Serial.print(F("reset: ")); Serial.print(ESP.getResetReason()); Serial.print(F(", IRQ line ")); Serial.println(Rfid::Irq::line() ? F("low") : F("high")); + Log.print(F("reset: ")); Log.print(ESP.getResetReason()); Log.print(F(", IRQ line ")); Log.println(Rfid::Irq::line() ? F("low") : F("high")); esp::OutPin::begin(); esp::OutPin::on(); // RC522 RST high; a chip select on this pin would reset it Twi::begin(); Spi::begin(); @@ -228,24 +267,24 @@ void setup() { table(F("I2C")); describe(); if (RfidApp::reg.count > 1) { - Serial.print(F("RC522 version 0x")); Serial.print(Rfid::rd(1, rc522::VersionReg), HEX); - Serial.println(); + Log.print(F("RC522 version 0x")); Log.print(Rfid::rd(1, rc522::VersionReg), HEX); + Log.println(); // where the RC522's configuration stands: written at init (on which try), still there after the SPI scan, still // there now (after the I2C scan); then one register written and read back, to tell lost writes from a reset since const uint16_t tries = RfidApp::devState(1).initTries; - Serial.print(F("RC522 init: ")); - if (tries) { Serial.print(F("configured on write ")); Serial.print(tries); } else Serial.print(F("never configured")); - Serial.print(F(", after SPI scan ")); Serial.print(rcAfterSpi ? F("on") : F("off")); - Serial.print(F(", now ")); Serial.print(Rfid::configured(1) ? F("on") : F("off")); - Serial.print(F(" (CommandReg 0x")); Serial.print(Rfid::rd(1, rc522::CommandReg), HEX); - Serial.print(F(" TPrescalerReg 0x")); Serial.print(Rfid::rd(1, rc522::TPrescalerReg), HEX); + Log.print(F("RC522 init: ")); + if (tries) { Log.print(F("configured on write ")); Log.print(tries); } else Log.print(F("never configured")); + Log.print(F(", after SPI scan ")); Log.print(rcAfterSpi ? F("on") : F("off")); + Log.print(F(", now ")); Log.print(Rfid::configured(1) ? F("on") : F("off")); + Log.print(F(" (CommandReg 0x")); Log.print(Rfid::rd(1, rc522::CommandReg), HEX); + Log.print(F(" TPrescalerReg 0x")); Log.print(Rfid::rd(1, rc522::TPrescalerReg), HEX); Rfid::wr(1, rc522::TReloadRegL, 0x5A); - Serial.print(F("), write test: TReloadRegL 0x5A reads 0x")); Serial.println(Rfid::rd(1, rc522::TReloadRegL), HEX); + Log.print(F("), write test: TReloadRegL 0x5A reads 0x")); Log.println(Rfid::rd(1, rc522::TReloadRegL), HEX); Rfid::wr(1, rc522::TReloadRegL, 0xE8); - Serial.print(F("RC522 IRQ: ComIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIEnReg), HEX); - Serial.print(F(" DivIEnReg 0x")); Serial.print(Rfid::rd(1, rc522::DivIEnReg), HEX); - Serial.print(F(" ComIrqReg 0x")); Serial.print(Rfid::rd(1, rc522::ComIrqReg), HEX); - Serial.print(F(", IRQ line ")); Serial.println(Rfid::Irq::line() ? F("low") : F("high")); + Log.print(F("RC522 IRQ: ComIEnReg 0x")); Log.print(Rfid::rd(1, rc522::ComIEnReg), HEX); + Log.print(F(" DivIEnReg 0x")); Log.print(Rfid::rd(1, rc522::DivIEnReg), HEX); + Log.print(F(" ComIrqReg 0x")); Log.print(Rfid::rd(1, rc522::ComIrqReg), HEX); + Log.print(F(", IRQ line ")); Log.println(Rfid::Irq::line() ? F("low") : F("high")); } } @@ -257,9 +296,9 @@ static void logHealth() { const uint8_t state = uint8_t((h.quarantined << 2) | (h.probation << 1) | h.disconnected); if (state == last) return; last = state; - Serial.print(F("HLTH ")); Serial.print(millis()); Serial.print(F(" row 1 flap=")); Serial.print(h.flapEwma); - Serial.print(F(" cost=")); Serial.print(h.costEwma); Serial.print(F(" quarantined=")); Serial.print(h.quarantined); - Serial.print(F(" probation=")); Serial.print(h.probation); Serial.print(F(" disconnected=")); Serial.println(h.disconnected); + Log.print(F("HLTH ")); Log.print(millis()); Log.print(F(" row 1 flap=")); Log.print(h.flapEwma); + Log.print(F(" cost=")); Log.print(h.costEwma); Log.print(F(" quarantined=")); Log.print(h.quarantined); + Log.print(F(" probation=")); Log.print(h.probation); Log.print(F(" disconnected=")); Log.println(h.disconnected); } // the reader's row: a status change, and each initialisation @@ -270,37 +309,63 @@ static void logRfid() { const discover::Status st = RfidApp::reg.status(1); const uint16_t inits = RfidApp::devState(1).inits; if (st != last) { - if (last != discover::Status::Gone) { Serial.print(F("STATUS ")); Serial.print(millis()); Serial.print(F(" row 1 ")); Serial.print(uint8_t(last)); Serial.print(F("->")); Serial.println(uint8_t(st)); } - Serial.print(millis()); Serial.print(F(" rfid[1] ")); - if (st == discover::Status::Alive) { Serial.print(F("alive, init #")); Serial.println(inits); } - else Serial.println(st == discover::Status::Stale ? F("stale") : F("gone")); + if (last != discover::Status::Gone) { Log.print(F("STATUS ")); Log.print(millis()); Log.print(F(" row 1 ")); Log.print(uint8_t(last)); Log.print(F("->")); Log.println(uint8_t(st)); } + Log.print(millis()); Log.print(F(" rfid[1] ")); + if (st == discover::Status::Alive) { Log.print(F("alive, init #")); Log.println(inits); } + else Log.println(st == discover::Status::Stale ? F("stale") : F("gone")); last = st; lastInits = inits; } else if (inits != lastInits) { lastInits = inits; - Serial.print(millis()); Serial.print(F(" rfid[1] reinit, init #")); Serial.println(inits); + Log.print(millis()); Log.print(F(" rfid[1] reinit, init #")); Log.println(inits); } } -// the fault script: a serial key, no schedule -static void faults(uint32_t now) { - static uint32_t vanishEnd = 0; - static bool vanished = false; - if (Serial.available()) { - const int c = Serial.read(); - if (c == 'v' && !vanished) { Serial.print(now); Serial.println(F(" fault: RST low 3 s")); esp::OutPin::off(); vanished = true; vanishEnd = now + 3000; } - else if (c == 'd') describe(); - else if (c == 'a') air(); - else if (c == 'x') { Serial.print(now); Serial.println(F(" fault: air sensor soft reset")); Bmp::Dev::wr(0xE0, 0xB6); } - else if (c == 'o') { Bmp::visitReg(1, [](auto& r) { r.set(0x27); }); air(); } // ctrl_meas: temperature x1, pressure x1, normal mode - else if (c == 'q') { Bmp::visitReg(1, [](auto& r) { r.set(0x2B); }); air(); } // ctrl_meas: temperature x1, pressure x2, normal mode - else if (c == 'r') { Bmp::restoreDefaults(); air(); } - else if (c == 'p' && !vanished) { - Serial.print(now); Serial.println(F(" fault: RST pulse")); - esp::OutPin::off(); delayMicroseconds(1000); esp::OutPin::on(); - } +// the fault script: a key, from the serial monitor or, in the link build, the link's op 'f'; no schedule +static uint32_t vanishEnd = 0; +static bool vanished = false; +static void faultKey(int c, uint32_t now) { + if (c == 'v' && !vanished) { Log.print(now); Log.println(F(" fault: RST low 3 s")); esp::OutPin::off(); vanished = true; vanishEnd = now + 3000; } + else if (c == 'd') describe(); + else if (c == 'a') air(); + else if (c == 'x') { Log.print(now); Log.println(F(" fault: air sensor soft reset")); Bmp::Dev::wr(0xE0, 0xB6); } + else if (c == 'o') { Bmp::visitReg(1, [](auto& r) { r.set(0x27); }); air(); } // ctrl_meas: temperature x1, pressure x1, normal mode + else if (c == 'q') { Bmp::visitReg(1, [](auto& r) { r.set(0x2B); }); air(); } // ctrl_meas: temperature x1, pressure x2, normal mode + else if (c == 'r') { Bmp::restoreDefaults(); air(); } + else if (c == 'p' && !vanished) { + Log.print(now); Log.println(F(" fault: RST pulse")); + esp::OutPin::off(); delayMicroseconds(1000); esp::OutPin::on(); + } +} +static void faultTick(uint32_t now) { + if (vanished && int32_t(now - vanishEnd) >= 0) { esp::OutPin::on(); vanished = false; Log.print(now); Log.println(F(" fault: RST high")); } +} +#ifdef ONEMACHINE_LINK +// the link: the description, get and set by code, the changes since the last read (tree_ops.h), and the faults above (op 'f', one key) +struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves) + template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32\n"; while (*s) put(*s++); } +}; +using Ops = bmpm::TreeOps; +struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; +static uint32_t linkNow = 0; +struct LinkApp { + template static void describe(P& put) { Ops::describe(put); } + template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { + if (op == 'f') { if (n != 1) { r.status(link::BadLength); return; } faultKey(in[0], linkNow); r.status(link::Ok); return; } + Ops::request(op, in, n, r); } - if (vanished && int32_t(now - vanishEnd) >= 0) { esp::OutPin::on(); vanished = false; Serial.print(now); Serial.println(F(" fault: RST high")); } +}; +static link::Frame frame; +static void faults(uint32_t now) { + linkNow = now; + while (Serial.available()) frame.feed(uint8_t(Serial.read()), now); + faultTick(now); } +#else +static void faults(uint32_t now) { + if (Serial.available()) faultKey(Serial.read(), now); + faultTick(now); +} +#endif // loop timing, rig diagnostics: iterations since the last report and the longest iteration, pump() and service() (microseconds) struct LoopStats { @@ -308,8 +373,8 @@ struct LoopStats { static void lap(uint32_t us) { if (last && us - last > maxLoop) maxLoop = us - last; last = us; ++n; } static void note(uint32_t& m, uint32_t from) { const uint32_t d = micros() - from; if (d > m) m = d; } static void report(uint32_t now) { - Serial.print(F("LOOP ")); Serial.print(now); Serial.print(F(" n=")); Serial.print(n); Serial.print(F(" max=")); Serial.print(maxLoop); - Serial.print(F(" pump=")); Serial.print(maxPump); Serial.print(F(" svc=")); Serial.println(maxSvc); + Log.print(F("LOOP ")); Log.print(now); Log.print(F(" n=")); Log.print(n); Log.print(F(" max=")); Log.print(maxLoop); + Log.print(F(" pump=")); Log.print(maxPump); Log.print(F(" svc=")); Log.println(maxSvc); n = 0; maxLoop = maxPump = maxSvc = 0; } }; @@ -325,7 +390,7 @@ void loop() { { const uint32_t t = micros(); RfidApp::pump(); LoopStats::note(LoopStats::maxPump, t); } static uint8_t lastMiss = 0; // a card held still that misses polls: the streak, printed when it grows const uint8_t miss = RfidApp::reg.count > 1 ? RfidApp::devState(1).missStreak : 0; - if (miss != lastMiss) { lastMiss = miss; if (miss) { Serial.print(millis()); Serial.print(F(" miss[1]=")); Serial.println(miss); } } + if (miss != lastMiss) { lastMiss = miss; if (miss) { Log.print(millis()); Log.print(F(" miss[1]=")); Log.println(miss); } } } AirApp::tickBuses(now); AirTicker::run(now); diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h new file mode 100644 index 0000000..46ef1c4 --- /dev/null +++ b/examples/spi/src/tree_ops.h @@ -0,0 +1,122 @@ +// The link's ops over the published nodes of a machine tree (the framing is link/frame.h; the bytes are role/link.h's): +// 'd' the description: bmpm::describe's text, then the codes that only notify (App's Extra). Codes are numbered in the order they are listed. +// 'v' get by code payload: the code (text) reply: the value, i32 little-endian +// 'w' set by code payload: the value (i32), then the code reply: nothing; the set goes through the node: its limits, its capture, +// its register +// 'n' changes since payload: none reply: u8 how many were refused since the last 'n', then per change +// the code's number (u8) and its value (i32) +// status Ok, BadLength, BadValue (outside the node's limits), NoValue (a group has no value), ReadOnly, Unknown (no such code or op) +// What changed is told by the published nodes' OnSync/OnChange functions (note(code number, value)) into a fail::Buffer: a store that refuses the +// newest when it is full and counts the refusal. The consumer is told how many it missed (the u8 in the reply) and reads the values it follows again. +#pragma once +#include +#include +#include +#include +#include "bmp280_machine.h" + +namespace bmpm { + + // the changes waiting to be read: a Buffer of N records under a status that counts what it refused + template + struct ChangeQueue { + struct Env { + static constexpr uint8_t retryMask = 0; + static constexpr bool rawStatus = true, returnPath = false, idempotent = false, async = false; + static void busReset() {} + static void reissue(fail::RowId) {} + static fail::Outcome reprobe(fail::RowId) { return fail::Outcome::Ok(); } + static void setRowState(fail::RowId, uint8_t) {} + }; + using Q = fail::Controller, fail::LazyStatus>; + inline static Q q; + inline static uint8_t seenDrops = 0; + static void note(uint8_t code, int32_t v) { (void)q.offer(fail::Rec{code, 0, v}); } + }; + + // a code carries its number (`static constexpr uint8_t num`), the one the change records use; the numbers are the codes' positions in the list + template struct NumberedFrom : std::true_type {}; + template struct NumberedFrom : std::bool_constant::value> {}; + template struct Numbered; + template struct Numbered> : NumberedFrom<0, P...> {}; + + // M: the machine; Pubs: Chain; Extra: codes that only notify (describe(put) lists them; they take the numbers after Pubs'); + // bus: the machine's position in the App + template + struct TreeOps { + static_assert(Numbered::value, "the published codes' numbers (Code::num) must be their positions in the list"); + using Queue = ChangeQueue; + static constexpr uint8_t numPubs = uint8_t(Pubs::size); + + template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } + + // the code in the payload, against each published node's: its number, or -1 + template static bool is(const uint8_t* s, uint16_t n) { + const state::Name nm = Pub::PubCode::name(); + for (uint16_t i = 0; i < n; ++i) if (char(s[i]) != nm.rom(i)) return false; + return nm.rom(n) == 0; + } + template static int find(hapi::Chain*, const uint8_t* s, uint16_t n) { + int i = 0, hit = -1; + ((is(s, n) ? (hit = i, ++i) : ++i), ...); + return hit; + } + + template static uint8_t getOne(int32_t& v) { + using Node = typename Pub::Inner; + if constexpr (IsGroup::value) return link::NoValue; + else { v = int32_t(Pub{}.get()); return link::Ok; } + } + template static uint8_t setOne(int32_t v) { + using Node = typename Pub::Inner; + if constexpr (IsGroup::value) return link::NoValue; + else if constexpr (!HasSet::value) return link::ReadOnly; + else { + if constexpr (HasLimits::value) { if (v < Node::limLo || v > Node::limHi) return link::BadValue; } + Pub::set(v); + return link::Ok; + } + } + // the node at position `at` of the list + template static uint8_t get(hapi::Chain*, int at, int32_t& v) { + int i = 0; uint8_t st = link::Unknown; + ((i++ == at ? (st = getOne(v), 0) : 0), ...); + return st; + } + template static uint8_t set(hapi::Chain*, int at, int32_t v) { + int i = 0; uint8_t st = link::Unknown; + ((i++ == at ? (st = setOne(v), 0) : 0), ...); + return st; + } + + template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { + static Pubs* const list = nullptr; + switch (op) { + case 'v': { + const int at = find(list, in, n); + if (at < 0) { r.status(link::Unknown); return; } + int32_t v = 0; const uint8_t st = get(list, at, v); + r.status(st); if (st == link::Ok) r.put32(v); + return; + } + case 'w': { + if (n < 5) { r.status(link::BadLength); return; } + const int32_t v = int32_t(uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24); + const int at = find(list, in + 4, uint16_t(n - 4)); + if (at < 0) { r.status(link::Unknown); return; } + r.status(set(list, at, v)); + return; + } + case 'n': { + auto& q = Queue::q; + const uint8_t drops = q.status().drops; + r.status(link::Ok); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; + while (!q.empty() && unsigned(r.n) + 5 <= sizeof r.data) { const fail::Rec& c = q.front(); r.put(c.cap); r.put32(c.v); q.pop(); } + return; + } + default: r.status(link::Unknown); return; + } + } + }; + +} diff --git a/include/oneMachine/link/frame.h b/include/oneMachine/link/frame.h new file mode 100644 index 0000000..0154653 --- /dev/null +++ b/include/oneMachine/link/frame.h @@ -0,0 +1,54 @@ +#pragma once +// oneMachine/link/frame.h -- the request/response framing of role/link.h, for a device whose ops answer with a payload. +// +// request op (1 byte), length (u16 little-endian), payload +// response status (1 byte), length (u16 little-endian), payload +// +// The same bytes as role::Link, so a Python StreamLink or CtypesLink talks to it as to any machine (python/onemachine). role::Link carries a +// role machine's command and report frames and its op hook answers a status only; this one hands the request to the app and sends what the +// app answers. Status numbers keep role::Link's where they overlap. +// +// link::Frame: Out `static void put(uint8_t)`; feed(byte, now) as bytes arrive. App: +// static void describe(P& put) the text reply of op 'd' (a char sink; called twice: counted, then sent, so it changes nothing) +// static void request(uint8_t op, const uint8_t* in, uint16_t n, Reply& r) every other op: r.status(st), r.put(byte), at most ReplyCap bytes +// A request longer than Cap is read to its end and refused (TooLong); an op the app does not know answers Unknown. +#include + +namespace link { + constexpr uint8_t Ok = 0, BadLength = 2, BadValue = 3, NoValue = 4, Unknown = 0x80, TooLong = 0x81, ReadOnly = 0x82; + + template struct ReplyBuf { + uint8_t st = Unknown; uint8_t n = 0; uint8_t data[N]{}; bool over = false; + void status(uint8_t s) { st = s; } + void put(uint8_t b) { if (n < N) data[n++] = b; else over = true; } + void put32(int32_t v) { for (uint8_t i = 0; i < 4; ++i) put(uint8_t(uint32_t(v) >> (8 * i))); } + }; + + template + struct Frame { + uint8_t phase = 0, op = 0; uint16_t len = 0, got = 0; uint8_t buf[Cap]{}; + + void feed(uint8_t b, uint32_t) { + switch (phase) { + case 0: op = b; phase = 1; return; + case 1: len = b; phase = 2; return; + case 2: len = uint16_t(len | (uint16_t(b) << 8)); got = 0; phase = 3; if (len) return; break; + default: if (got < Cap) buf[got] = b; ++got; if (got < len) return; + } + phase = 0; request(); + } + + private: + struct Put { uint16_t n = 0; bool send = false; void operator()(char c) { if (send) Out::put(uint8_t(c)); else ++n; } }; + static void head(uint8_t st, uint16_t n) { Out::put(st); Out::put(uint8_t(n)); Out::put(uint8_t(n >> 8)); } + void request() { + if (len > Cap) { head(TooLong, 0); return; } + if (op == 'd') { Put p; App::describe(p); head(Ok, p.n); p.send = true; App::describe(p); return; } + ReplyBuf r; + App::request(op, buf, len, r); + if (r.over) { head(TooLong, 0); return; } + head(r.st, r.n); + for (uint8_t i = 0; i < r.n; ++i) Out::put(r.data[i]); + } + }; +} diff --git a/python/onemachine/__init__.py b/python/onemachine/__init__.py index 540d93a..21ba744 100644 --- a/python/onemachine/__init__.py +++ b/python/onemachine/__init__.py @@ -1,3 +1,4 @@ """OneMachine from Python: a machine's roles, command and report, read from the device's own description (docs/role.md).""" from .schema import Schema, SchemaError, BadHash, BadLength from .machine import Machine, Description, StreamLink, CtypesLink, LinkError, RoleChanged, OutOfLimits +from .tree import Tree, Code, Change, OutOfRange, ReadOnly, UnknownCode diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py new file mode 100644 index 0000000..a8197af --- /dev/null +++ b/python/onemachine/tree.py @@ -0,0 +1,147 @@ +"""A machine tree seen by its codes: the description the device publishes, and nothing else a consumer needs (examples/spi, link/frame.h). + + m = Tree(StreamLink(ser.read, ser.write, ser.flush)) # a serial port; CtypesLink('./tree.so') in-process; or any object with call(op, payload) + m.codes['temp'].scaled, m.codes['air/ctrl_meas'].lo # what the description says: 2, 0 + m.temp # 27.62: the value, scaled by the description (temp is in 0.01 C) + m.air.ctrl_meas = 0x27 # set by code: read-only and out-of-range are refused here, before anything is sent + m.air.ctrl_meas # 39: read back from the device + m.changes() # [Change('temp', 27.61, 2761), ...]: what changed since the last call + m.missed # how many notifications the device had to refuse (its queue was full) since the start + +A code is a name, with `/` for the part of a group (`air/ctrl_meas` is `m.air.ctrl_meas`). The description lists, per code: its path in the +machine (`/

/[/]`, for people), whether it notifies (`sync`: a value that moves; `event`), read-only or read-write, its +kind (a value, a register with its default, a group) and, for a value, how many decimals it is scaled by and the range a set accepts. +A device that refused notifications (`missed`) says how many; the values a consumer follows are read again with get().""" +import struct +from collections import namedtuple +from .machine import LinkError + +OK, BAD_LENGTH, BAD_VALUE, NO_VALUE, UNKNOWN, TOO_LONG, READ_ONLY = 0, 2, 3, 4, 0x80, 0x81, 0x82 + +class OutOfRange(ValueError): pass +class ReadOnly(AttributeError): pass +class UnknownCode(AttributeError): pass + +Change = namedtuple('Change', 'code value raw') + +class Code: + def __init__(self, num, name, path, notify): + self.num, self.name, self.path, self.notify = num, name, path, notify + self.kind, self.ro, self.scaled, self.lo, self.hi, self.default, self.group_size, self.unit = 'value', True, 0, None, None, None, 0, None + def __repr__(self): return 'Code(%d %s %s%s)' % (self.num, self.name, self.kind, ' ro' if self.ro else ' rw') + def to_value(self, raw): + if self.unit == 'u32': return raw & 0xFFFFFFFF + return raw / 10 ** self.scaled if self.scaled else raw + def to_raw(self, value): return int(round(value * 10 ** self.scaled)) if self.scaled else int(value) + +def _parse(text): + """The `published` section: one line per code, in the order the device numbers them.""" + codes, seen = [], False + for line in text.split('\n'): + if line == 'published': seen = True; continue + if not seen or not line.startswith(' '): continue + head, _, rest = line.strip().partition(' -> ') + words = rest.split(' ') + c = Code(len(codes), head, [int(p) for p in words[0].split('/')], words[1] if words[1] != 'silent' else None) + if c.notify == 'notify': c.notify = words[2]; words = words[1:] # `notify sync|event` is two words, `silent` one + w, i = words[1:] if c.notify is None else words[2:], 0 + while i < len(w): + t = w[i] + if t == 'group': c.kind, c.group_size, i = 'group', int(w[i + 1]), i + 2 + elif t == 'reg': c.kind, c.default, i = 'reg', int(w[i + 3], 16), i + 4 # reg 0xF4 default 0x57 + elif t in ('ro', 'rw'): c.ro, i = t == 'ro', i + 1 + elif t == 'value': i += 1 + elif t == 'scaled': c.scaled, i = int(w[i + 1]), i + 2 + elif t == 'range': c.lo, c.hi = (int(x) for x in w[i + 1].split('..')); i += 2 + elif t == 'u32': c.unit, i = 'u32', i + 1 + else: i += 1 + codes.append(c) + return codes + +class Group: + """m.air: the codes below `air/`.""" + def __init__(self, tree, prefix): object.__setattr__(self, '_t', tree); object.__setattr__(self, '_p', prefix) + def __getattr__(self, name): + if name.startswith('_'): raise AttributeError(name) + return self._t._access(self._p + '/' + name) + def __setattr__(self, name, value): self._t.set(self._p + '/' + name, value) + def __dir__(self): return [c.name[len(self._p) + 1:] for c in self._t._codes if c.name.startswith(self._p + '/')] + +class Tree: + def __init__(self, link): + object.__setattr__(self, 'link', link) + object.__setattr__(self, 'missed', 0) + self.refresh() + + def _call(self, op, payload=b''): + return self.link.call(op, payload) + + def refresh(self): + """Read the description again.""" + st, data = self._call('d') + if st != OK: raise LinkError('description: status %d' % st) + text = data.decode() + codes = _parse(text) + object.__setattr__(self, 'description', text) + object.__setattr__(self, '_codes', codes) + object.__setattr__(self, 'codes', {c.name: c for c in codes}) + + def raw(self, code): + """The device's integer for a code (temp: 2762).""" + c = self._known(code) + st, data = self._call('v', c.name.encode()) + if st == NO_VALUE: raise AttributeError('%s is a group: it has no value' % code) + if st != OK: raise LinkError('get %s: status %d' % (code, st)) + return struct.unpack(' c.hi): + raise OutOfRange('%s: %r is outside %d..%d' % (code, value, c.lo, c.hi)) + st, _ = self._call('w', struct.pack(' 1/118/0 notify sync ro value scaled 2\n" " press -> 1/118/1 notify sync ro value scaled 2\n" " air -> 1/118/3 silent group 2\n" - " ctrl_meas -> 1/118/3/1 silent reg 0xF4 default 0x57 rw\n"; + " ctrl_meas -> 1/118/3/1 silent reg 0xF4 default 0x57 rw range 0..255\n"; if (std::strcmp(s.b, want) != 0) { ++failures; std::printf("FAIL description:\n%s--- wanted:\n%s", s.b, want); } Str s2; bmpm::describe>(s2, 1); diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index 6ae3436..37124a5 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -4,6 +4,7 @@ # rules : each compile-time rule rejects its case with its own message. # bmp : the BMP280 as a machine of ItemDef nodes (bmp_machine.cpp, against a simulated chip with the datasheet's worked example), and its # capture and restore under a failure edge: reset behind the host's back, unplugged, replaced (bmp_capture.cpp). +# link : the Python consumer of the machine tree (python/onemachine/tree.py) against the air sensor on a simulated chip (test/link/build.sh). # irq : the RC522's interrupt part (spi_irq.cpp) and the ESP8266 delivery components' pin rules (irq_delivery.cpp). # AVR : avr-g++ -Os atmega328p, linked over the real AVR SPI core -- it builds, and its size. # Exits non-zero if an assertion fails, a rule does not fire, or the AVR image does not build. @@ -97,6 +98,9 @@ for pair in "NEG_ID_IDLE:an SPI id of 0x00 or 0xFF is what an empty slot reads" else echo "FAIL: -D$def was not rejected with '$msg'"; rc=1; fi done +echo; echo "=== the Python consumer of the machine tree (test/link): over a pipe and in-process (ctypes) ===" +if command -v python3 >/dev/null; then bash ../link/build.sh 2>&1 | grep -E "^OK|^FAIL|Error|Traceback" || rc=1; else echo "(python3 not found: skipped)"; fi + if command -v avr-g++ >/dev/null; then echo; echo "=== avr-g++ $(avr-g++ -dumpversion) -Os atmega328p (linked, real SPI core) ===" AVRFL="-std=gnu++17 -Os -mmcu=atmega328p -DF_CPU=16000000UL -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -Wall -Wextra" diff --git a/test/link/build.sh b/test/link/build.sh new file mode 100755 index 0000000..3acbe6d --- /dev/null +++ b/test/link/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# The Python consumer of a machine tree against the spi example's air sensor on a simulated chip: the device built as a process (a pipe, as over a +# serial port) and as a shared library (in-process through ctypes), and test/link/check_tree.py over each. Needs python3. +set -e +cd "$(dirname "$0")" +INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" +rc=0 +g++ -std=c++17 -O1 -Wall $INC tree_device.cpp -o tree_device +g++ -std=c++17 -O1 -Wall -shared -fPIC -DSIM_LIB $INC tree_device.cpp -o tree_device.so +python3 check_tree.py pipe || rc=1 +python3 check_tree.py ctypes || rc=1 +rm -f tree_device tree_device.so +exit $rc diff --git a/test/link/check_tree.py b/test/link/check_tree.py new file mode 100644 index 0000000..a74b945 --- /dev/null +++ b/test/link/check_tree.py @@ -0,0 +1,94 @@ +"""The Python consumer of a machine tree (python/onemachine/tree.py) against the spi example's air sensor on a simulated chip (tree_device.cpp), +over a pipe (StreamLink) and in-process (CtypesLink): the description, values, a set by code and its read-back from the chip, the refusals, +a reset behind the host's back, an unplug with a set while it is gone, the notifications, and a full queue.""" +import os, struct, sys +D = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(D, '..', '..', 'python')) +from onemachine import Tree, StreamLink, CtypesLink, OutOfRange, ReadOnly, UnknownCode + +CTYPES = len(sys.argv) > 1 and sys.argv[1] == 'ctypes' +failures = 0 +def check(cond, what): + global failures + if not cond: failures += 1; print('FAIL:', what) + +link = CtypesLink(os.path.join(D, 'tree_device.so'), autocycle=False) if CTYPES else StreamLink.popen([os.path.join(D, 'tree_device')]) +def op(c, payload=b''): + st, data = link.call(c, payload); return st, data +def advance(ms): check(op('t', struct.pack(' 25.08, 'a warmer sample: temp and press, once: %r' % ch) +check(m.changes() == [], 'and no more') + +# ---- set by code: through the node, to the register --------------------------------------------------------------------------- +m.air.ctrl_meas = 0x27 +check(reg(0xF4) == 0x27 and m.air.ctrl_meas == 0x27, 'set air/ctrl_meas: the chip has it, and it reads back') +check(m.changes() == [], 'a silent code does not notify') +for bad, exc in ((300, OutOfRange), (-1, OutOfRange)): + try: m.air.ctrl_meas = bad; check(False, 'range %r' % bad) + except exc: pass +check(reg(0xF4) == 0x27, 'refused here: nothing was sent') +st, _ = op('w', struct.pack(' +#include +#include +#include +#include +#include +#include +#include +#include "../support/mockBmpTwi.h" +#include "../../examples/spi/src/tree_ops.h" + +using discover::RowId; +using hapi::Chain; + +struct Mode { + static constexpr bool checked = true, returnPath = false, idempotent = true, lifecycle = true; + template using BusStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Backoff<100, 400>, fail::Status>; + template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; +}; +struct App; +using M = bmpm::Machine, Mode>; +using Drivers = Chain; +struct App : discover::World, M::Entries, 3, discover::I2cScan>, fail::BusEdge { + static constexpr bool lifecycle = true; + static void release(RowId) {} + static void unbindAll() {} + static void busReset() { mockbmp::Twi::begin(); } +}; +using Ticker = fail::Ticks; + +// the codes, with their numbers; `card` only notifies +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"); }; + +using Queue = bmpm::ChangeQueue<8>; +template static void note(int32_t v) { Queue::note(Code::num, v); } + +using PubTemp = bmpm::PublishedAt, oneData::OnSync<¬e>>; +using PubPress = bmpm::PublishedAt, oneData::OnSync<¬e>>; +using PubAir = bmpm::PublishedAt>; +using PubConfig = bmpm::PublishedAt>; +using PubCtrlMeas = bmpm::PublishedAt>; +using Pubs = Chain; + +struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves) + template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32\n"; while (*s) put(*s++); } +}; +using Ops = bmpm::TreeOps; + +static uint32_t now = 0; +// The machine's nodes are objects with constructors of their own (the machine's statics are initialised in no defined order), so discovery, which +// writes into them, runs from main or the first call, never from a static initialiser. +static void start() { static bool started = false; if (started) return; started = true; mockbmp::State::reset(); mockbmp::State::c77.unplug(); App::discover(); } + +struct LinkApp { + template static void describe(P& put) { Ops::describe(put); } + template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { + using mockbmp::State; + switch (op) { + case 't': { + if (n != 4) { r.status(link::BadLength); return; } + const uint32_t ms = uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24; + for (uint32_t t = 0; t < ms; t += 10, now += 10) { + if (now % 100 == 0) { App::pump(); bmpm::PublishAll::sync(); } + App::tickBuses(now); Ticker::run(now); + } + r.status(link::Ok); return; + } + case 'x': State::c76.softReset(); r.status(link::Ok); return; + case 'u': State::c76.unplug(); r.status(link::Ok); return; + case 'p': State::c76.replug(); r.status(link::Ok); return; + case 'k': { + if (n != 4) { r.status(link::BadLength); return; } + Queue::note(CodeCard::num, int32_t(uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24)); + r.status(link::Ok); return; + } + case 'R': if (n != 1) { r.status(link::BadLength); return; } r.status(link::Ok); r.put(State::c76.regs[in[0]]); return; + case 'S': { + if (n != 8) { r.status(link::BadLength); return; } + auto u = [&](int i) { return uint32_t(in[i]) | uint32_t(in[i + 1]) << 8 | uint32_t(in[i + 2]) << 16 | uint32_t(in[i + 3]) << 24; }; + State::c76.setSample(u(0), u(4)); r.status(link::Ok); return; + } + case 'W': if (n != 2) { r.status(link::BadLength); return; } State::c76.regs[in[0]] = in[1]; r.status(link::Ok); return; + default: Ops::request(op, in, n, r); return; + } + } +}; + +#ifdef SIM_LIB +struct BufOut { + static inline uint8_t* p = nullptr; static inline uint16_t cap = 0, n = 0; + static void put(uint8_t b) { if (n < cap) p[n] = b; ++n; } +}; +static link::Frame frame; +extern "C" __attribute__((visibility("default"))) int32_t onemachine_call(uint8_t op, const uint8_t* in, uint16_t n, uint8_t* out, uint16_t cap, uint32_t) { + start(); BufOut::p = out; BufOut::cap = cap; BufOut::n = 0; + frame.feed(op, now); frame.feed(uint8_t(n), now); frame.feed(uint8_t(n >> 8), now); + for (uint16_t i = 0; i < n; ++i) frame.feed(in[i], now); + return BufOut::n <= cap ? int32_t(BufOut::n) : -int32_t(BufOut::n); +} +extern "C" __attribute__((visibility("default"))) void onemachine_cycle(uint32_t) {} +#else +struct StdOut { static void put(uint8_t b) { fputc(b, stdout); } }; +int main() { + start(); + static link::Frame frame; + int c; + while ((c = fgetc(stdin)) != EOF) { frame.feed(uint8_t(c), now); if (frame.phase == 0) fflush(stdout); } + return 0; +} +#endif From df1cb5c5666534c406a937507dbc5c95dd610815 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 06:37:54 +0000 Subject: [PATCH 07/19] Round 6: a status per published code, one link, CI runs the machine and 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, ...>. 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 --- .github/workflows/ci.yml | 10 ++ examples/spi/README.md | 8 +- examples/spi/rig_session.py | 45 +++++---- examples/spi/src/bmp280_machine.h | 24 ++++- examples/spi/src/main.cpp | 27 ++++-- examples/spi/src/tree_ops.h | 151 +++++++++++++++++++++--------- include/oneMachine/link/frame.h | 54 ----------- include/oneMachine/role/link.h | 25 ++++- python/onemachine/__init__.py | 2 +- python/onemachine/tree.py | 62 ++++++++++-- test/discover/bmp_machine.cpp | 8 +- test/link/check_tree.py | 34 ++++++- test/link/tree_device.cpp | 51 ++++++---- 13 files changed, 335 insertions(+), 166 deletions(-) delete mode 100644 include/oneMachine/link/frame.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5bdfcc..5f716ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,16 @@ jobs: git clone --depth 1 $ORG/HAPI.git HAPI git clone --depth 1 --branch "$BRANCH" $ORG/OneBus.git OneBus || git clone --depth 1 $ORG/OneBus.git OneBus git clone --depth 1 --branch "$BRANCH" $ORG/OneChip.git OneChip || git clone --depth 1 $ORG/OneChip.git OneChip + # The air sensor as a machine of OneMenu nodes and its Python consumer (test/discover/build_spi1.sh, test/link): they build against OneData, OneMenu and the + # libraries OneMenu includes, at the same branch name when it exists, else main. + - name: Clone OneData, OneMenu and what OneMenu includes + run: | + for r in OneData OneMenu OneItem OneOutput OneBit OnePin OneParse OneInput OneIO; do + git clone --depth 1 --branch "$BRANCH" $ORG/$r.git $r || git clone --depth 1 $ORG/$r.git $r + done + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install the AVR toolchain and simulator run: | sudo apt-get update diff --git a/examples/spi/README.md b/examples/spi/README.md index b7cab77..ec34f36 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -60,8 +60,7 @@ registers' defaults, `x` resets the sensor behind the host's back. ## A Python consumer over the serial port -`pio run -e d1_mini_link -t upload` builds the same sketch with the serial port carrying the link (`link/frame.h`: the request and response framing of -`role/link.h`) instead of the log. `python/onemachine` reads it, with the package's own `StreamLink`: +`pio run -e d1_mini_link -t upload` builds the same sketch with the serial port carrying the link (`role/link.h`, with payload ops) instead of the log. `python/onemachine` reads it, with the package's own `StreamLink`: ```python import serial @@ -70,10 +69,11 @@ ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=2) m = Tree(StreamLink(ser.read, ser.write, ser.flush)) # reads the description m.temp, m.press # 27.62, 1026.68: scaled as the description says (0.01 C, 0.01 hPa) m.air.ctrl_meas = 0x27 # set by code; read-only and out-of-range are refused here, before anything is sent -m.changes() # [Change('temp', 27.61, 2761), Change('card', 4062320374, ...)]: what changed since the last call +m.changes() # [Change('temp', 27.61, 2761), Change('card', 4062320374, ...), Change('air', status='stale')] +m.status('air') # 'alive', 'stale' or 'gone': the part's row; m.temp raises Stale when it is not alive ``` -Ops: `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`, `f` one +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. diff --git a/examples/spi/rig_session.py b/examples/spi/rig_session.py index 625a5fb..3fcd8a6 100644 --- a/examples/spi/rig_session.py +++ b/examples/spi/rig_session.py @@ -1,12 +1,12 @@ """A Python consumer of the spi example's link build (env d1_mini_link), over the serial port: the description, the values, a set by code and its -read-back, a reset behind the host's back, the notifications; with --unplug it also waits for the air sensor to be unplugged and plugged in again, -and with --watch N it prints the notifications (the RFID card too) for N seconds. +read-back, a reset behind the host's back, the notifications; with --unplug it waits for the air sensor to be unplugged and plugged in again and follows +its status (stale, then alive) and its last setting, and with --watch N it prints the notifications (the RFID card too) for N seconds. ~/.platformio/penv/bin/python examples/spi/rig_session.py /dev/ttyUSB0 [--unplug] [--watch 30]""" import os, sys, time sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'python')) import serial -from onemachine import Tree, StreamLink, OutOfRange, ReadOnly, UnknownCode +from onemachine import Tree, StreamLink, OutOfRange, ReadOnly, UnknownCode, Stale port = sys.argv[1] if len(sys.argv) > 1 else '/dev/ttyUSB0' args = sys.argv[2:] @@ -19,11 +19,12 @@ def say(s): print('%6.1f %s' % (time.time() - t0, s), flush=True) say('>>> m = Tree(StreamLink(ser.read, ser.write, ser.flush))') for c in m.codes.values(): - say(' %-14s #%d path %-9s %-6s %s%s%s%s' % (c.name, c.num, '/'.join(map(str, c.path)), c.kind, 'ro' if c.ro else 'rw', + say(' %-14s #%d path %-9s %-6s %s%s%s%s status %s' % (c.name, c.num, '/'.join(map(str, c.path)), c.kind, 'ro' if c.ro else 'rw', ' scaled %d' % c.scaled if c.scaled else '', ' range %d..%d default 0x%02X' % (c.lo, c.hi, c.default) if c.lo is not None else '', - ' notifies (%s)' % c.notify if c.notify else '')) + ' notifies (%s)' % c.notify if c.notify else '', c.status)) say('>>> m.temp, m.press -> %r, %r' % (m.temp, m.press)) say('>>> m.air.config, m.air.ctrl_meas -> 0x%02X, 0x%02X' % (m.air.config, m.air.ctrl_meas)) +say('>>> m.status("air") -> %r' % m.status('air')) say('>>> m.changes() -> %r' % m.changes()) time.sleep(3) say('>>> m.changes() (3 s later) -> %r' % m.changes()) @@ -38,30 +39,38 @@ def say(s): print('%6.1f %s' % (time.time() - t0, s), flush=True) except ReadOnly as e: say('>>> m.temp = 1 -> ReadOnly: %s' % e) try: m.nothing except UnknownCode as e: say('>>> m.nothing -> UnknownCode: %s' % str(e)[:60] + '...') -say('>>> m.air.ctrl_meas -> 0x%02X (nothing was sent for those)' % m.air.ctrl_meas) say('>>> m.fault("x") # the air sensor is reset behind the host\'s back') m.fault('x') time.sleep(2.5) say('>>> m.air.config, m.air.ctrl_meas -> 0x%02X, 0x%02X (the setting came back: 0x27, not the default 0x57)' % (m.air.config, m.air.ctrl_meas)) +m.changes() if '--unplug' in args: say('--- unplug the air sensor (all four wires), wait, plug it in again (any time in the next 60 s)') gone = back = False - seen = [] t = time.time() while time.time() - t < 60 and not back: - seen += m.changes() # drain while waiting: the device's queue holds 8 - v = m.raw('air/ctrl_meas') - if not gone and v != 0x27: - gone = True; say(' the sensor is gone (ctrl_meas reads 0x%02X, not 0x27)' % v); time.sleep(2.0) - m.air.ctrl_meas = 0x2B; say('>>> m.air.ctrl_meas = 0x2B (while it is gone: captured as the last intent)') - elif gone and v == 0x2B: - back = True; time.sleep(0.5) - say(' the sensor is back: ctrl_meas = 0x%02X (0x2B: the last intent), config = 0x%02X' % (m.air.ctrl_meas, m.air.config)) - time.sleep(0.4) - if not back: say(' (the sensor was not seen going away and coming back)') - say('>>> m.changes() while waiting -> %d notifications (temp/press only on change); missed so far: %d' % (len(seen), m.missed)) + for ch in m.changes(): + if ch.status: say(' change: %r' % (ch,)) + if ch.code == 'air' and ch.status == 'stale' and not gone: + gone = True + say('>>> m.status("air") -> %r' % m.status('air')) + try: m.temp + except Stale as e: say('>>> m.temp -> Stale: %s' % e) + say('>>> m.reading("air/ctrl_meas") -> %r (the last setting, not 0xFF)' % (m.reading('air/ctrl_meas'),)) + m.air.ctrl_meas = 0x2B + say('>>> m.air.ctrl_meas = 0x2B (while it is gone: captured as the last intent)') + say('>>> m.reading("air/ctrl_meas") -> %r' % (m.reading('air/ctrl_meas'),)) + if ch.code == 'air' and ch.status == 'alive' and gone: + back = True + time.sleep(0.3) + if back: + time.sleep(0.5) + say('>>> m.status("air") -> %r' % m.status('air')) + say('>>> m.air.config, m.air.ctrl_meas -> 0x%02X, 0x%02X (0x2B: the last intent came back)' % (m.air.config, m.air.ctrl_meas)) + say('>>> m.temp -> %r' % m.temp) + else: say(' (the sensor was not seen going away and coming back)') if '--watch' in args: secs = int(args[args.index('--watch') + 1]) diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index e70750f..24d7b77 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -13,6 +13,11 @@ // PublishedAt, OnSync> by a compile-time path into the machine (node #3, child #1): any node, a leaf of a group too // Published> by an ItemRef, for a standalone object // A sync pass over the published nodes calls fn(value) for each one that changed. +// +// Never touch a node from a static initialiser: discover, set, publish, sync. A machine's nodes are objects with constructors of their own, held in +// inline static members of a class template, and those are initialised in no defined order: a static initialiser that runs discovery writes into a +// node before its constructor has run, and the constructor then wipes what was written (the captured registers, the Watch copies). Discover from +// setup() or main(). // describe() walks the machine and the published nodes and writes what a consumer needs: the codes, their path, their fields, which notify. #pragma once #include @@ -242,6 +247,15 @@ namespace bmpm { // one burst read of the measurement registers static void poll() { uint8_t b[6] = {}; Dev::rdN(0xF7, b, 6); compute(b); } + // The status of the row this machine's device is bound to in W's registry (discover::Status: Alive 0, Stale 1, Gone 2); Gone when it was never found. + // Every node of the machine has it: a part that is gone is Stale or Gone, not a register that reads 0xFF. + static uint8_t status() { + const discover::IDriver* d = discover::instOf(); + for (RowId r = 0; r < W::reg.count; ++r) + if (!W::reg.rows[r].isBus && W::reg.rows[r].drv == d) return uint8_t(W::reg.status(r)); + return 2; + } + // ---- discovery: the machine is the driver's one device ---------------------------------------------------------------- // Under a failure edge (Mode::checked) each poll first reads the control registers back: a register that no longer holds what was captured // means the part was reset without the host knowing (Corrupt: Recover calls reinit(), which validates it and replays the capture). A part that @@ -298,18 +312,24 @@ namespace bmpm { template using Published = ItemDef, Label, PubTag>; + template struct HasCaptured : std::false_type {}; + template struct HasCaptured().captured())>> : std::true_type {}; + // a node of a machine by compile-time path: P is the position in the machine, then in each group below it. It reaches any node, a leaf of // a group too, by resolving the path to the node's singleton (or to the child inside a group) on use. get(), changed() and sync() are the node's. template struct PathRef { static_assert(sizeof...(P) > 0, "PathRef: a path names a node of the machine"); using Node = typename M::template NodeAt::type; + using Src = M; // where its status comes from: the machine's device static constexpr unsigned depth = sizeof...(P); static constexpr unsigned path[sizeof...(P)] = {P...}; template struct Part : O { using O::O; static Node& ref() { return M::template resolve(); } static decltype(auto) get() { return ref().get(); } + // the value of a part that is not Alive: a register's last set (the intent that comes back), otherwise the last value read + static decltype(auto) last() { if constexpr (HasCaptured::value) return ref().captured(); else return ref().get(); } [[nodiscard]] static bool changed() { return ref().changed(); } static void sync() { ref().sync(); } template static void set(V&& v) { ref().set(std::forward(v)); } // through the node: its limits, its capture, its register @@ -382,8 +402,10 @@ namespace bmpm { str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(M::addr); if constexpr (std::is_void::value) { put('/'); dec(unsigned(M::template IndexOf::value)); } else for (unsigned i = 0; i < Pub::Path::depth; ++i) { put('/'); dec(Pub::Path::path[i]); } - str(NotifiesSync::value ? " notify sync" : " silent"); fields(); put('\n'); + str(NotifiesSync::value ? " notify sync" : " silent"); fields(); + str(" status "); str(statusName(M::status())); put('\n'); } + static const char* statusName(uint8_t st) { return st == 0 ? "alive" : st == 1 ? "stale" : "gone"; } template void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } }; diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 545b4ab..b097df3 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -44,7 +44,7 @@ #include "rc522.h" #include "irq_esp8266.h" #ifdef ONEMACHINE_LINK - #include "tree_ops.h" // the link's ops over the published nodes (link/frame.h) + #include "tree_ops.h" // the link's ops over the published nodes (role/link.h with payload ops) #endif #include "bmp280_machine.h" @@ -53,7 +53,7 @@ #endif // What the sketch says about itself goes to Log: the serial monitor. In the link build (env d1_mini_link, -DONEMACHINE_LINK) the serial port carries -// the link's frames instead (link/frame.h), and the log says nothing. +// the link's frames instead (role/link.h), and the log says nothing. #ifdef ONEMACHINE_LINK struct NullPrint : Print { size_t write(uint8_t) override { return 1; } size_t write(const uint8_t*, size_t n) override { return n; } }; static NullPrint nullPrint; @@ -340,24 +340,35 @@ static void faultTick(uint32_t now) { if (vanished && int32_t(now - vanishEnd) >= 0) { esp::OutPin::on(); vanished = false; Log.print(now); Log.println(F(" fault: RST high")); } } #ifdef ONEMACHINE_LINK -// the link: the description, get and set by code, the changes since the last read (tree_ops.h), and the faults above (op 'f', one key) -struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves) - template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32\n"; while (*s) put(*s++); } +// the link (role/link.h with payload ops): the description, get and set by code, the changes since the last read, a row's status (tree_ops.h), and the +// faults above (op 'f', one key) +struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of the reader's row + using Codes = Chain; + static uint8_t status(uint8_t) { return RfidApp::reg.count > 1 ? uint8_t(RfidApp::reg.status(1)) : 2; } + template static void describe(P& put) { + const char* s = " card -> 0/1 notify event ro value u32 status "; while (*s) put(*s++); + const uint8_t st = status(0); + s = st == 0 ? "alive" : st == 1 ? "stale" : "gone"; while (*s) put(*s++); + put('\n'); + } }; using Ops = bmpm::TreeOps; struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; static uint32_t linkNow = 0; struct LinkApp { + static constexpr bool payload = true; // role::Link: describe() for op 'd', request() for the others template static void describe(P& put) { Ops::describe(put); } template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { - if (op == 'f') { if (n != 1) { r.status(link::BadLength); return; } faultKey(in[0], linkNow); r.status(link::Ok); return; } + if (op == 'f') { if (n != 1) { r.status(role::LinkBadLength); return; } faultKey(in[0], linkNow); r.status(role::LinkOk); return; } Ops::request(op, in, n, r); } }; -static link::Frame frame; +static role::Machine<>::Report linkReport; // role::Link carries a role machine's frames; this device has no roles, its ops are the tree's +static role::Link, SerialOut, LinkApp, 48, 96> link(linkReport, 0); static void faults(uint32_t now) { linkNow = now; - while (Serial.available()) frame.feed(uint8_t(Serial.read()), now); + while (Serial.available()) link.feed(uint8_t(Serial.read()), now); + Ops::watch(); // a row whose status moved is a change faultTick(now); } #else diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index 46ef1c4..5f07ab5 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -1,18 +1,24 @@ -// The link's ops over the published nodes of a machine tree (the framing is link/frame.h; the bytes are role/link.h's): -// 'd' the description: bmpm::describe's text, then the codes that only notify (App's Extra). Codes are numbered in the order they are listed. -// 'v' get by code payload: the code (text) reply: the value, i32 little-endian +// The link's ops over the published nodes of a machine tree. The framing and the bytes are role/link.h's: this is an App with payload ops +// (`payload = true`: role::Link calls describe() for op 'd' and request() for the others). +// 'd' the description: bmpm::describe's text, then the codes that only notify (Extra). Codes are numbered in the order they are listed; each line +// ends with the status of the row the code is bound to (alive, stale or gone) as it is now. +// 'v' get by code payload: the code (text) reply: the status (u8), then the value (i32 little-endian) +// The status is the device's: Alive 0, Stale 1, Gone 2. A part that is not Alive has no live value: a register answers the last value set (its +// capture, the intent that comes back), a sensor value the last one measured. A group or an event has no value: the status alone, with NoValue. // 'w' set by code payload: the value (i32), then the code reply: nothing; the set goes through the node: its limits, its capture, -// its register +// its register (a part that is gone keeps it as the last intent) // 'n' changes since payload: none reply: u8 how many were refused since the last 'n', then per change -// the code's number (u8) and its value (i32) -// status Ok, BadLength, BadValue (outside the node's limits), NoValue (a group has no value), ReadOnly, Unknown (no such code or op) -// What changed is told by the published nodes' OnSync/OnChange functions (note(code number, value)) into a fail::Buffer: a store that refuses the -// newest when it is full and counts the refusal. The consumer is told how many it missed (the u8 in the reply) and reads the values it follows again. +// the code's number (u8) and its value (i32); a number with bit 7 set +// is a status change: the code that stands for its row, and the new status +// status Ok, BadLength, BadValue (outside the node's limits), NoValue, ReadOnly, Unknown (no such code or op) +// What changed is told by the published nodes' OnSync/OnChange functions (note(code number, value)) and by watch() (a row's status): into a +// fail::Buffer, a store that refuses the newest when it is full and counts the refusal. The consumer is told how many it missed (the u8 in the +// reply) and reads the values it follows again. #pragma once #include #include #include -#include +#include #include "bmp280_machine.h" namespace bmpm { @@ -32,6 +38,7 @@ namespace bmpm { inline static Q q; inline static uint8_t seenDrops = 0; static void note(uint8_t code, int32_t v) { (void)q.offer(fail::Rec{code, 0, v}); } + static void noteStatus(uint8_t code, uint8_t st) { note(uint8_t(0x80 | code), st); } }; // a code carries its number (`static constexpr uint8_t num`), the one the change records use; the numbers are the codes' positions in the list @@ -39,82 +46,142 @@ namespace bmpm { template struct NumberedFrom : std::bool_constant::value> {}; template struct Numbered; template struct Numbered> : NumberedFrom<0, P...> {}; + template struct CodesFrom : std::true_type {}; + template struct CodesFrom : std::bool_constant::value> {}; - // M: the machine; Pubs: Chain; Extra: codes that only notify (describe(put) lists them; they take the numbers after Pubs'); - // bus: the machine's position in the App + // where a published node's status comes from: the machine it is reached through (its device's row); void: it has none and is always Alive + template struct SrcOf { using type = void; }; + template struct SrcOf> { using type = typename Pub::Path::Src; }; + // the first code of the list that has source S: the code that stands for the row when its status changes + template struct FirstSrc { static constexpr int value = -1; }; + template struct FirstSrc { + static constexpr int value = std::is_same::type, S>::value ? int(I) : FirstSrc::value; + }; + // M: the machine; Pubs: Chain; Extra: the codes that only notify (Extra::Codes: Chain of code tags with `num` after Pubs', + // describe(put) lists them, status(i) is the status of the i-th); bus: the machine's position in the App template struct TreeOps { static_assert(Numbered::value, "the published codes' numbers (Code::num) must be their positions in the list"); - using Queue = ChangeQueue; static constexpr uint8_t numPubs = uint8_t(Pubs::size); + static constexpr uint8_t numCodes = uint8_t(numPubs + Extra::Codes::size); + static constexpr bool payload = true; + using Queue = ChangeQueue; template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } - // the code in the payload, against each published node's: its number, or -1 - template static bool is(const uint8_t* s, uint16_t n) { - const state::Name nm = Pub::PubCode::name(); + // the code in the payload, against each code tag's name: true when it is that code + template static bool named(const uint8_t* s, uint16_t n) { + const state::Name nm = Code::name(); for (uint16_t i = 0; i < n; ++i) if (char(s[i]) != nm.rom(i)) return false; return nm.rom(n) == 0; } - template static int find(hapi::Chain*, const uint8_t* s, uint16_t n) { + template static int findPub(hapi::Chain*, const uint8_t* s, uint16_t n) { int i = 0, hit = -1; - ((is(s, n) ? (hit = i, ++i) : ++i), ...); + ((named(s, n) ? (hit = i, ++i) : ++i), ...); + return hit; + } + template static int findExtra(hapi::Chain*, const uint8_t* s, uint16_t n) { + int i = numPubs, hit = -1; + ((named(s, n) ? (hit = i, ++i) : ++i), ...); return hit; } + static int find(const uint8_t* s, uint16_t n) { + const int p = findPub(static_cast(nullptr), s, n); + return p >= 0 ? p : findExtra(static_cast(nullptr), s, n); + } + + // ---- status: the row a code is bound to --------------------------------------------------------------------------- + template static uint8_t statusOne() { + using S = typename SrcOf::type; + if constexpr (std::is_void::value) return 0; else return S::status(); + } + template static uint8_t statusPubs(hapi::Chain*, int at) { + int i = 0; uint8_t st = 0; + ((i++ == at ? (st = statusOne(), 0) : 0), ...); + return st; + } + static uint8_t status(int code) { return code < numPubs ? statusPubs(static_cast(nullptr), code) : Extra::status(uint8_t(code - numPubs)); } + template static int repPubs(hapi::Chain*, int at) { + int i = 0, r = at; + ((i++ == at ? (r = FirstSrc<0, typename SrcOf::type, Pub...>::value, 0) : 0), ...); + return r; + } + static int rep(int code) { return code < numPubs ? repPubs(static_cast(nullptr), code) : code; } + + // The loop's step: a row whose status moved is a change, told once through the code that stands for it. The first call only learns the statuses. + static void watch() { + static uint8_t last[numCodes ? numCodes : 1]; + static bool seeded = false; + for (uint8_t c = 0; c < numCodes; ++c) { + if (rep(c) != c) continue; + const uint8_t s = status(c); + if (seeded && s != last[c]) Queue::noteStatus(c, s); + last[c] = s; + } + seeded = true; + } - template static uint8_t getOne(int32_t& v) { + // ---- get and set ------------------------------------------------------------------------------------------------------ + template static uint8_t getOne(int32_t& v, uint8_t st) { using Node = typename Pub::Inner; - if constexpr (IsGroup::value) return link::NoValue; - else { v = int32_t(Pub{}.get()); return link::Ok; } + if constexpr (IsGroup::value) return role::LinkNoValue; + else { + if constexpr (HasLast::value) v = st == 0 ? int32_t(Pub{}.get()) : int32_t(Pub::last()); + else v = int32_t(Pub{}.get()); + return role::LinkOk; + } } template static uint8_t setOne(int32_t v) { using Node = typename Pub::Inner; - if constexpr (IsGroup::value) return link::NoValue; - else if constexpr (!HasSet::value) return link::ReadOnly; + if constexpr (IsGroup::value) return role::LinkNoValue; + else if constexpr (!HasSet::value) return role::LinkReadOnly; else { - if constexpr (HasLimits::value) { if (v < Node::limLo || v > Node::limHi) return link::BadValue; } + if constexpr (HasLimits::value) { if (v < Node::limLo || v > Node::limHi) return role::LinkBadValue; } Pub::set(v); - return link::Ok; + return role::LinkOk; } } - // the node at position `at` of the list - template static uint8_t get(hapi::Chain*, int at, int32_t& v) { - int i = 0; uint8_t st = link::Unknown; - ((i++ == at ? (st = getOne(v), 0) : 0), ...); - return st; + template struct HasLast : std::false_type {}; + template struct HasLast> : std::true_type {}; + template static uint8_t get(hapi::Chain*, int at, int32_t& v, uint8_t st) { + int i = 0; uint8_t r = role::LinkUnknown; + ((i++ == at ? (r = getOne(v, st), 0) : 0), ...); + return r; } template static uint8_t set(hapi::Chain*, int at, int32_t v) { - int i = 0; uint8_t st = link::Unknown; - ((i++ == at ? (st = setOne(v), 0) : 0), ...); - return st; + int i = 0; uint8_t r = role::LinkUnknown; + ((i++ == at ? (r = setOne(v), 0) : 0), ...); + return r; } template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { static Pubs* const list = nullptr; switch (op) { case 'v': { - const int at = find(list, in, n); - if (at < 0) { r.status(link::Unknown); return; } - int32_t v = 0; const uint8_t st = get(list, at, v); - r.status(st); if (st == link::Ok) r.put32(v); + const int at = find(in, n); + if (at < 0) { r.status(role::LinkUnknown); return; } + const uint8_t st = status(at); + int32_t v = 0; + const uint8_t ls = at < numPubs ? get(list, at, v, st) : role::LinkNoValue; + r.status(ls); r.put(st); if (ls == role::LinkOk) r.put32(v); return; } case 'w': { - if (n < 5) { r.status(link::BadLength); return; } + if (n < 5) { r.status(role::LinkBadLength); return; } const int32_t v = int32_t(uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24); - const int at = find(list, in + 4, uint16_t(n - 4)); - if (at < 0) { r.status(link::Unknown); return; } - r.status(set(list, at, v)); + const int at = find(in + 4, uint16_t(n - 4)); + if (at < 0) { r.status(role::LinkUnknown); return; } + r.status(at < numPubs ? set(list, at, v) : role::LinkReadOnly); return; } case 'n': { auto& q = Queue::q; const uint8_t drops = q.status().drops; - r.status(link::Ok); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; + r.status(role::LinkOk); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; while (!q.empty() && unsigned(r.n) + 5 <= sizeof r.data) { const fail::Rec& c = q.front(); r.put(c.cap); r.put32(c.v); q.pop(); } return; } - default: r.status(link::Unknown); return; + default: r.status(role::LinkUnknown); return; } } }; diff --git a/include/oneMachine/link/frame.h b/include/oneMachine/link/frame.h deleted file mode 100644 index 0154653..0000000 --- a/include/oneMachine/link/frame.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once -// oneMachine/link/frame.h -- the request/response framing of role/link.h, for a device whose ops answer with a payload. -// -// request op (1 byte), length (u16 little-endian), payload -// response status (1 byte), length (u16 little-endian), payload -// -// The same bytes as role::Link, so a Python StreamLink or CtypesLink talks to it as to any machine (python/onemachine). role::Link carries a -// role machine's command and report frames and its op hook answers a status only; this one hands the request to the app and sends what the -// app answers. Status numbers keep role::Link's where they overlap. -// -// link::Frame: Out `static void put(uint8_t)`; feed(byte, now) as bytes arrive. App: -// static void describe(P& put) the text reply of op 'd' (a char sink; called twice: counted, then sent, so it changes nothing) -// static void request(uint8_t op, const uint8_t* in, uint16_t n, Reply& r) every other op: r.status(st), r.put(byte), at most ReplyCap bytes -// A request longer than Cap is read to its end and refused (TooLong); an op the app does not know answers Unknown. -#include - -namespace link { - constexpr uint8_t Ok = 0, BadLength = 2, BadValue = 3, NoValue = 4, Unknown = 0x80, TooLong = 0x81, ReadOnly = 0x82; - - template struct ReplyBuf { - uint8_t st = Unknown; uint8_t n = 0; uint8_t data[N]{}; bool over = false; - void status(uint8_t s) { st = s; } - void put(uint8_t b) { if (n < N) data[n++] = b; else over = true; } - void put32(int32_t v) { for (uint8_t i = 0; i < 4; ++i) put(uint8_t(uint32_t(v) >> (8 * i))); } - }; - - template - struct Frame { - uint8_t phase = 0, op = 0; uint16_t len = 0, got = 0; uint8_t buf[Cap]{}; - - void feed(uint8_t b, uint32_t) { - switch (phase) { - case 0: op = b; phase = 1; return; - case 1: len = b; phase = 2; return; - case 2: len = uint16_t(len | (uint16_t(b) << 8)); got = 0; phase = 3; if (len) return; break; - default: if (got < Cap) buf[got] = b; ++got; if (got < len) return; - } - phase = 0; request(); - } - - private: - struct Put { uint16_t n = 0; bool send = false; void operator()(char c) { if (send) Out::put(uint8_t(c)); else ++n; } }; - static void head(uint8_t st, uint16_t n) { Out::put(st); Out::put(uint8_t(n)); Out::put(uint8_t(n >> 8)); } - void request() { - if (len > Cap) { head(TooLong, 0); return; } - if (op == 'd') { Put p; App::describe(p); head(Ok, p.n); p.send = true; App::describe(p); return; } - ReplyBuf r; - App::request(op, buf, len, r); - if (r.over) { head(TooLong, 0); return; } - head(r.st, r.n); - for (uint8_t i = 0; i < r.n; ++i) Out::put(r.data[i]); - } - }; -} diff --git a/include/oneMachine/role/link.h b/include/oneMachine/role/link.h index 76dc9e2..15bb6b0 100644 --- a/include/oneMachine/role/link.h +++ b/include/oneMachine/role/link.h @@ -11,6 +11,10 @@ // when a value is outside the firmware's limits (nothing changes); Ok // takes effect at once, and the next take() re-applies the command // anything else goes to App::op(op, payload, n) -> a status (no payload), or Unknown when the app has no such op +// An app whose ops answer with a payload declares `static constexpr bool payload = true`, and then (instead of op()): +// static void describe(P& put) the text reply of op 'd' (a char sink, called twice: counted, then sent) +// static void request(uint8_t op, const uint8_t* in, uint16_t n, role::LinkReply& r) every other op: r.status(st), r.put(byte), r.put32(v) +// (examples/spi: the machine tree's ops d, v, w, n, f.) An app without it is served exactly as before. // // role::Link: M a role::Machine, Out `static void put(uint8_t)`, App optional. feed(byte, now) as bytes arrive, then at the // cycle boundary take(cmd) (a new command, if one was accepted) and quiet(now) (true once, when no command was accepted for QuietMs: @@ -21,6 +25,16 @@ namespace role { constexpr uint8_t LinkOk = 0, LinkBadHash = 1, LinkBadLength = 2, LinkBadValue = 3, LinkUnknown = 0x80, LinkTooLong = 0x81; + constexpr uint8_t LinkNoValue = 4, LinkReadOnly = 0x82; // for apps with payload ops: a group has no value; a write to a read-only code + // what an app's request() answers: a status and up to N payload bytes (more is TooLong) + template struct LinkReply { + uint8_t st = LinkUnknown; uint8_t n = 0; uint8_t data[N]{}; bool over = false; + void status(uint8_t s) { st = s; } + void put(uint8_t b) { if (n < N) data[n++] = b; else over = true; } + void put32(int32_t v) { for (uint8_t i = 0; i < 4; ++i) put(uint8_t(uint32_t(v) >> (8 * i))); } + }; + template struct PayloadOps : std::false_type {}; + template struct PayloadOps> : std::bool_constant {}; struct NoApp { static int op(uint8_t, const uint8_t*, uint16_t) { return -1; } }; // the longest request a link reads: a command frame, or a tuning frame when the machine has tuned roles @@ -29,7 +43,7 @@ namespace role { if constexpr (M::tunable) { unsigned t = state::wire_size(); return t > c ? t : c; } else return c; } - template()> + template(), unsigned ReplyCap = 48> struct Link { using Command = typename M::Command; using Report = typename M::Report; const Report& report; @@ -82,9 +96,16 @@ namespace role { if (st == state::Status::Ok) { M::tuning = t; retuned = true; } head(uint8_t(st), 0); return; } else break; + case 'd': if constexpr (PayloadOps::value) { text([](Put& p) { App::describe(p); }); return; } else break; default: break; } - { int st = App::op(op, buf, len); head(st < 0 ? LinkUnknown : uint8_t(st), 0); } + if constexpr (PayloadOps::value) { + LinkReply r; + App::request(op, buf, len, r); + if (r.over) { head(LinkTooLong, 0); return; } + head(r.st, r.n); + for (uint8_t i = 0; i < r.n; ++i) Out::put(r.data[i]); + } else { int st = App::op(op, buf, len); head(st < 0 ? LinkUnknown : uint8_t(st), 0); } } }; } diff --git a/python/onemachine/__init__.py b/python/onemachine/__init__.py index 21ba744..de41cb0 100644 --- a/python/onemachine/__init__.py +++ b/python/onemachine/__init__.py @@ -1,4 +1,4 @@ """OneMachine from Python: a machine's roles, command and report, read from the device's own description (docs/role.md).""" from .schema import Schema, SchemaError, BadHash, BadLength from .machine import Machine, Description, StreamLink, CtypesLink, LinkError, RoleChanged, OutOfLimits -from .tree import Tree, Code, Change, OutOfRange, ReadOnly, UnknownCode +from .tree import Tree, Code, Change, Reading, Stale, OutOfRange, ReadOnly, UnknownCode diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index a8197af..ab84c24 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -5,12 +5,16 @@ m.temp # 27.62: the value, scaled by the description (temp is in 0.01 C) m.air.ctrl_meas = 0x27 # set by code: read-only and out-of-range are refused here, before anything is sent m.air.ctrl_meas # 39: read back from the device - m.changes() # [Change('temp', 27.61, 2761), ...]: what changed since the last call + m.changes() # [Change('temp', 27.61, 2761), Change('air', status='stale'), ...]: what changed since the last call + m.status('air') # 'alive', 'stale' or 'gone': the row the code is bound to (the part's, not a register that reads 0xFF) + m.reading('temp') # Reading(value=27.61, status='stale'): never raises; m.temp raises Stale when the part is not alive m.missed # how many notifications the device had to refuse (its queue was full) since the start A code is a name, with `/` for the part of a group (`air/ctrl_meas` is `m.air.ctrl_meas`). The description lists, per code: its path in the machine (`/
/[/]`, for people), whether it notifies (`sync`: a value that moves; `event`), read-only or read-write, its kind (a value, a register with its default, a group) and, for a value, how many decimals it is scaled by and the range a set accepts. +A part that is not alive is Stale or Gone: reading one of its codes raises Stale (it carries the last value), and the change is announced by changes() +as one status change per code of that part (the device sends it once, through the first of them). A device that refused notifications (`missed`) says how many; the values a consumer follows are read again with get().""" import struct from collections import namedtuple @@ -22,12 +26,19 @@ class OutOfRange(ValueError): pass class ReadOnly(AttributeError): pass class UnknownCode(AttributeError): pass -Change = namedtuple('Change', 'code value raw') +Change = namedtuple('Change', 'code value raw status', defaults=(None, None, None)) # a value change has value and raw; a status change only status +Reading = namedtuple('Reading', 'value status') +STATUS = ('alive', 'stale', 'gone') + +class Stale(RuntimeError): + """A code of a part that is not alive was read: status says which, last is the last value (a register's last set, a sensor's last reading).""" + def __init__(self, code, status, last): super().__init__('%s: the part is %s (last value %r)' % (code, status, last)); self.code, self.status, self.last = code, status, last class Code: def __init__(self, num, name, path, notify): self.num, self.name, self.path, self.notify = num, name, path, notify - self.kind, self.ro, self.scaled, self.lo, self.hi, self.default, self.group_size, self.unit = 'value', True, 0, None, None, None, 0, None + self.kind, self.ro, self.scaled, self.lo, self.hi, self.default, self.group_size, self.unit, self.status = 'value', True, 0, None, None, None, 0, None, 'alive' + self.src = tuple(path[:2]) # the row it is bound to: /
def __repr__(self): return 'Code(%d %s %s%s)' % (self.num, self.name, self.kind, ' ro' if self.ro else ' rw') def to_value(self, raw): if self.unit == 'u32': return raw & 0xFFFFFFFF @@ -54,6 +65,7 @@ def _parse(text): elif t == 'scaled': c.scaled, i = int(w[i + 1]), i + 2 elif t == 'range': c.lo, c.hi = (int(x) for x in w[i + 1].split('..')); i += 2 elif t == 'u32': c.unit, i = 'u32', i + 1 + elif t == 'status': c.status, i = w[i + 1], i + 2 else: i += 1 codes.append(c) return codes @@ -85,18 +97,41 @@ def refresh(self): object.__setattr__(self, 'description', text) object.__setattr__(self, '_codes', codes) object.__setattr__(self, 'codes', {c.name: c for c in codes}) + object.__setattr__(self, '_status', {c.src: c.status for c in codes}) # by row: what the description said, then what the device tells - def raw(self, code): - """The device's integer for a code (temp: 2762).""" + def _read(self, code): + """(status, raw) from the device: raw is None for a group or an event. The status is the part's; a part that is not alive answers its last value.""" c = self._known(code) st, data = self._call('v', c.name.encode()) - if st == NO_VALUE: raise AttributeError('%s is a group: it has no value' % code) - if st != OK: raise LinkError('get %s: status %d' % (code, st)) - return struct.unpack(' 1/118/0 notify sync ro value scaled 2\n" - " press -> 1/118/1 notify sync ro value scaled 2\n" - " air -> 1/118/3 silent group 2\n" - " ctrl_meas -> 1/118/3/1 silent reg 0xF4 default 0x57 rw range 0..255\n"; + " temp -> 1/118/0 notify sync ro value scaled 2 status alive\n" + " press -> 1/118/1 notify sync ro value scaled 2 status alive\n" + " air -> 1/118/3 silent group 2 status alive\n" + " ctrl_meas -> 1/118/3/1 silent reg 0xF4 default 0x57 rw range 0..255 status alive\n"; if (std::strcmp(s.b, want) != 0) { ++failures; std::printf("FAIL description:\n%s--- wanted:\n%s", s.b, want); } Str s2; bmpm::describe>(s2, 1); diff --git a/test/link/check_tree.py b/test/link/check_tree.py index a74b945..72b707c 100644 --- a/test/link/check_tree.py +++ b/test/link/check_tree.py @@ -4,7 +4,7 @@ import os, struct, sys D = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(D, '..', '..', 'python')) -from onemachine import Tree, StreamLink, CtypesLink, OutOfRange, ReadOnly, UnknownCode +from onemachine import Tree, StreamLink, CtypesLink, OutOfRange, ReadOnly, UnknownCode, Stale, Reading, Change CTYPES = len(sys.argv) > 1 and sys.argv[1] == 'ctypes' failures = 0 @@ -26,6 +26,7 @@ def reg(a): return op('R', bytes([a]))[1][0] check(c.kind == 'reg' and not c.ro and (c.lo, c.hi, c.default) == (0, 255, 0x57) and c.notify is None, 'air/ctrl_meas: a register, rw, 0..255, default 0x57, silent') check(m.codes['card'].notify == 'event' and m.codes['card'].unit == 'u32', 'card: an event, u32') check([c.num for c in m._codes] == list(range(6)), 'numbered in order') +check(all(c.status == 'alive' for c in m._codes) and m.status('temp') == 'alive' and m.status('card') == 'alive', 'every code is alive in the description') # ---- values, scaled by the description ----------------------------------------------------------------------------------------- advance(500) @@ -69,12 +70,39 @@ def reg(a): return op('R', bytes([a]))[1][0] advance(300) check(reg(0xF4) == 0x27 and reg(0xF5) == 0x90 and m.air.ctrl_meas == 0x27, 'restored, not the defaults') -# ---- unplugged: a set while it is gone is the last intent -------------------------------------------------------------------- +# ---- unplugged: Stale, not a register that reads 0xFF; a set while it is gone is the last intent ---------------------------------- +m.changes() +check(m.status('air') == 'alive', 'alive before') +t_before = m.temp op('u'); advance(1500) -m.air.ctrl_meas = 0x2B +ch = m.changes() +check(sorted(x.code for x in ch if x.status == 'stale') == ['air', 'air/config', 'air/ctrl_meas', 'press', 'temp'] and all(x.value is None for x in ch if x.status), + 'the unplug is announced once, for every code of the part: %r' % ch) +check(len([x for x in ch if x.status]) == 5, 'and only once') +check(m.status('air') == 'stale' and m.status('temp') == 'stale' and m.status('card') == 'alive', 'stale for the part, alive for the card') +check(m.status('air', refresh=True) == 'stale', 'and the device says so') +try: m.temp; check(False, 'temp of a stale part raises') +except Stale as e: check(e.status == 'stale' and e.last == t_before, 'Stale carries the status and the last value: %r' % e.last) +try: m.air.ctrl_meas; check(False, 'a register of a stale part raises') +except Stale as e: check(e.last == 0x27, 'a register answers its last set, not 0xFF: %r' % e.last) +r = m.reading('air/ctrl_meas'); check(isinstance(r, Reading) and r.status == 'stale' and r.value == 0x27, 'reading() marks it instead: %r' % (r,)) +m.air.ctrl_meas = 0x2B # the write cannot reach it: it is the last intent +check(m.reading('air/ctrl_meas') == (0x2B, 'stale'), 'the intent is what it answers: %r' % (m.reading('air/ctrl_meas'),)) +check(m.changes() == [], 'a stale part says nothing more') op('p'); check(reg(0xF4) == 0x00, 'plugged in again: reset values') advance(3000) check(reg(0xF4) == 0x2B and m.air.ctrl_meas == 0x2B, 'the last intent came back') +ch = m.changes() +check(sorted(x.code for x in ch if x.status == 'alive') == ['air', 'air/config', 'air/ctrl_meas', 'press', 'temp'], 'and the return is announced: %r' % [x for x in ch if x.status]) +check(m.status('air') == 'alive' and m.status('temp') == 'alive', 'alive again') +check(m.temp == t_before, 'and reads again') + +# ---- the card's row has a status of its own ------------------------------------------------------------------------------------- +op('z', bytes([2])); advance(20) +ch = m.changes() +check([x for x in ch if x.status] == [Change('card', status='gone')] and m.status('card') == 'gone' and m.status('air') == 'alive', 'the card\'s row gone: %r' % ch) +op('z', bytes([0])); advance(20); m.changes() +st, d = op('v', b'card'); check(st == 4 and d == bytes([0]), 'an event has no value: NoValue and the status alone') # ---- the card: an event with a value ------------------------------------------------------------------------------------------ m.changes() diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index e32047b..8d6f6dc 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -1,4 +1,4 @@ -// The spi example's air sensor as a device a consumer reaches over the link (link/frame.h, examples/spi/src/tree_ops.h), on simulated time and a +// The spi example's air sensor as a device a consumer reaches over the link (role/link.h with payload ops, examples/spi/src/tree_ops.h), on simulated time and a // simulated BMP280 (test/support/mockBmpTwi.h: the datasheet's worked example at 0x76). Built two ways, as test/role does: // a process the link's bytes on stdin/stdout (check_tree.py over a pipe, as over a serial port) // -DSIM_LIB a shared library exporting onemachine_call / onemachine_cycle (check_tree.py in-process through ctypes) @@ -6,6 +6,7 @@ // 't' u32 ms advance simulated time in 10 ms steps: the poll every 100 ms, the failure edges, the sync pass of the published nodes // 'x' soft-reset the sensor behind the host's back 'u' unplug it 'p' plug it in again (reset values) // 'k' u32 a card arrives (the number is its UID; 0: it leaves): a code that only notifies +// 'z' status the status the card's row has from now on (0 alive, 1 stale, 2 gone) // 'R' reg reply: the register's byte, read from the simulated chip (not through the machine) // 'W' reg v write a register of the simulated chip (not through the machine) // 'S' u32 u32 the raw pressure and temperature the chip converts from now on @@ -59,8 +60,15 @@ using PubConfig = bmpm::PublishedAt>; using PubCtrlMeas = bmpm::PublishedAt>; using Pubs = Chain; -struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves) - template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32\n"; while (*s) put(*s++); } +struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of its row + using Codes = Chain; + static inline uint8_t cardStatus = 0; + static uint8_t status(uint8_t) { return cardStatus; } + template static void describe(P& put) { + const char* s = " card -> 0/1 notify event ro value u32 status "; while (*s) put(*s++); + s = cardStatus == 0 ? "alive" : cardStatus == 1 ? "stale" : "gone"; while (*s) put(*s++); + put('\n'); + } }; using Ops = bmpm::TreeOps; @@ -70,34 +78,37 @@ static uint32_t now = 0; static void start() { static bool started = false; if (started) return; started = true; mockbmp::State::reset(); mockbmp::State::c77.unplug(); App::discover(); } struct LinkApp { + static constexpr bool payload = true; template static void describe(P& put) { Ops::describe(put); } template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { using mockbmp::State; switch (op) { case 't': { - if (n != 4) { r.status(link::BadLength); return; } + if (n != 4) { r.status(role::LinkBadLength); return; } const uint32_t ms = uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24; for (uint32_t t = 0; t < ms; t += 10, now += 10) { if (now % 100 == 0) { App::pump(); bmpm::PublishAll::sync(); } App::tickBuses(now); Ticker::run(now); + Ops::watch(); } - r.status(link::Ok); return; + r.status(role::LinkOk); return; } - case 'x': State::c76.softReset(); r.status(link::Ok); return; - case 'u': State::c76.unplug(); r.status(link::Ok); return; - case 'p': State::c76.replug(); r.status(link::Ok); return; + case 'x': State::c76.softReset(); r.status(role::LinkOk); return; + case 'u': State::c76.unplug(); r.status(role::LinkOk); return; + case 'p': State::c76.replug(); r.status(role::LinkOk); return; case 'k': { - if (n != 4) { r.status(link::BadLength); return; } + if (n != 4) { r.status(role::LinkBadLength); return; } Queue::note(CodeCard::num, int32_t(uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24)); - r.status(link::Ok); return; + r.status(role::LinkOk); return; } - case 'R': if (n != 1) { r.status(link::BadLength); return; } r.status(link::Ok); r.put(State::c76.regs[in[0]]); return; + case 'z': if (n != 1) { r.status(role::LinkBadLength); return; } Extra::cardStatus = in[0]; r.status(role::LinkOk); return; + case 'R': if (n != 1) { r.status(role::LinkBadLength); return; } r.status(role::LinkOk); r.put(State::c76.regs[in[0]]); return; case 'S': { - if (n != 8) { r.status(link::BadLength); return; } + if (n != 8) { r.status(role::LinkBadLength); return; } auto u = [&](int i) { return uint32_t(in[i]) | uint32_t(in[i + 1]) << 8 | uint32_t(in[i + 2]) << 16 | uint32_t(in[i + 3]) << 24; }; - State::c76.setSample(u(0), u(4)); r.status(link::Ok); return; + State::c76.setSample(u(0), u(4)); r.status(role::LinkOk); return; } - case 'W': if (n != 2) { r.status(link::BadLength); return; } State::c76.regs[in[0]] = in[1]; r.status(link::Ok); return; + case 'W': if (n != 2) { r.status(role::LinkBadLength); return; } State::c76.regs[in[0]] = in[1]; r.status(role::LinkOk); return; default: Ops::request(op, in, n, r); return; } } @@ -108,11 +119,12 @@ struct BufOut { static inline uint8_t* p = nullptr; static inline uint16_t cap = 0, n = 0; static void put(uint8_t b) { if (n < cap) p[n] = b; ++n; } }; -static link::Frame frame; +static role::Machine<>::Report rep0; +static role::Link, BufOut, LinkApp, 48, 96> link(rep0, 0); extern "C" __attribute__((visibility("default"))) int32_t onemachine_call(uint8_t op, const uint8_t* in, uint16_t n, uint8_t* out, uint16_t cap, uint32_t) { start(); BufOut::p = out; BufOut::cap = cap; BufOut::n = 0; - frame.feed(op, now); frame.feed(uint8_t(n), now); frame.feed(uint8_t(n >> 8), now); - for (uint16_t i = 0; i < n; ++i) frame.feed(in[i], now); + link.feed(op, now); link.feed(uint8_t(n), now); link.feed(uint8_t(n >> 8), now); + for (uint16_t i = 0; i < n; ++i) link.feed(in[i], now); return BufOut::n <= cap ? int32_t(BufOut::n) : -int32_t(BufOut::n); } extern "C" __attribute__((visibility("default"))) void onemachine_cycle(uint32_t) {} @@ -120,9 +132,10 @@ extern "C" __attribute__((visibility("default"))) void onemachine_cycle(uint32_t struct StdOut { static void put(uint8_t b) { fputc(b, stdout); } }; int main() { start(); - static link::Frame frame; + static role::Machine<>::Report rep0; + static role::Link, StdOut, LinkApp, 48, 96> link(rep0, 0); int c; - while ((c = fgetc(stdin)) != EOF) { frame.feed(uint8_t(c), now); if (frame.phase == 0) fflush(stdout); } + while ((c = fgetc(stdin)) != EOF) { link.feed(uint8_t(c), now); if (link.phase == 0) fflush(stdout); } return 0; } #endif From 5d7e1066ca60c75628fc2e55e631dcc4de4d3815 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 06:45:51 +0000 Subject: [PATCH 08/19] CI: the examples job clones OneData and OneMenu (the spi example's machine builds against them) Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f716ea..7cf0e52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,10 +152,13 @@ jobs: git clone --depth 1 $ORG/HAPI.git HAPI git clone --depth 1 --branch "$BRANCH" $ORG/OneBus.git OneBus || git clone --depth 1 $ORG/OneBus.git OneBus git clone --depth 1 --branch "$BRANCH" $ORG/OneChip.git OneChip || git clone --depth 1 $ORG/OneChip.git OneChip + # the spi example's air sensor is a machine of OneMenu nodes: OneData and OneMenu, and what they depend on + git clone --depth 1 --branch "$BRANCH" $ORG/OneData.git OneData || git clone --depth 1 $ORG/OneData.git OneData + git clone --depth 1 --branch "$BRANCH" $ORG/OneMenu.git OneMenu || git clone --depth 1 $ORG/OneMenu.git OneMenu python3 - <<'PY' import json, os, subprocess - seen = {"OneMachine", "HAPI", "OneBus", "OneChip"} - queue = ["OneBus", "OneChip"] + seen = {"OneMachine", "HAPI", "OneBus", "OneChip", "OneData", "OneMenu"} + queue = ["OneBus", "OneChip", "OneData", "OneMenu"] while queue: name = queue.pop(0) path = os.path.join(name, "library.json") From c49ba6f5d21330c2261241efea970ae272b1e7ed Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 07:03:14 +0000 Subject: [PATCH 09/19] role::Link: the app declares its reply capacity (replyCap); Link's template 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 --- examples/spi/src/main.cpp | 3 ++- include/oneMachine/role/link.h | 9 ++++++--- test/link/tree_device.cpp | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index b097df3..f54d08e 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -357,6 +357,7 @@ struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; static uint32_t linkNow = 0; struct LinkApp { static constexpr bool payload = true; // role::Link: describe() for op 'd', request() for the others + static constexpr unsigned replyCap = 96; template static void describe(P& put) { Ops::describe(put); } template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { if (op == 'f') { if (n != 1) { r.status(role::LinkBadLength); return; } faultKey(in[0], linkNow); r.status(role::LinkOk); return; } @@ -364,7 +365,7 @@ struct LinkApp { } }; static role::Machine<>::Report linkReport; // role::Link carries a role machine's frames; this device has no roles, its ops are the tree's -static role::Link, SerialOut, LinkApp, 48, 96> link(linkReport, 0); +static role::Link, SerialOut, LinkApp, 48> link(linkReport, 0); static void faults(uint32_t now) { linkNow = now; while (Serial.available()) link.feed(uint8_t(Serial.read()), now); diff --git a/include/oneMachine/role/link.h b/include/oneMachine/role/link.h index 15bb6b0..2b1e7ac 100644 --- a/include/oneMachine/role/link.h +++ b/include/oneMachine/role/link.h @@ -11,7 +11,7 @@ // when a value is outside the firmware's limits (nothing changes); Ok // takes effect at once, and the next take() re-applies the command // anything else goes to App::op(op, payload, n) -> a status (no payload), or Unknown when the app has no such op -// An app whose ops answer with a payload declares `static constexpr bool payload = true`, and then (instead of op()): +// An app whose ops answer with a payload declares `static constexpr bool payload = true` (and, for replies over 48 bytes, `replyCap`), and then (instead of op()): // static void describe(P& put) the text reply of op 'd' (a char sink, called twice: counted, then sent) // static void request(uint8_t op, const uint8_t* in, uint16_t n, role::LinkReply& r) every other op: r.status(st), r.put(byte), r.put32(v) // (examples/spi: the machine tree's ops d, v, w, n, f.) An app without it is served exactly as before. @@ -33,6 +33,9 @@ namespace role { void put(uint8_t b) { if (n < N) data[n++] = b; else over = true; } void put32(int32_t v) { for (uint8_t i = 0; i < 4; ++i) put(uint8_t(uint32_t(v) >> (8 * i))); } }; + // how many payload bytes a reply may hold: the app's `static constexpr unsigned replyCap`, else 48 (a longer reply is TooLong) + template struct ReplyCapOf { static constexpr unsigned value = 48; }; + template struct ReplyCapOf> { static constexpr unsigned value = A::replyCap; }; template struct PayloadOps : std::false_type {}; template struct PayloadOps> : std::bool_constant {}; struct NoApp { static int op(uint8_t, const uint8_t*, uint16_t) { return -1; } }; @@ -43,7 +46,7 @@ namespace role { if constexpr (M::tunable) { unsigned t = state::wire_size(); return t > c ? t : c; } else return c; } - template(), unsigned ReplyCap = 48> + template()> struct Link { using Command = typename M::Command; using Report = typename M::Report; const Report& report; @@ -100,7 +103,7 @@ namespace role { default: break; } if constexpr (PayloadOps::value) { - LinkReply r; + LinkReply::value> r; App::request(op, buf, len, r); if (r.over) { head(LinkTooLong, 0); return; } head(r.st, r.n); diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index 8d6f6dc..3430249 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -79,6 +79,7 @@ static void start() { static bool started = false; if (started) return; started struct LinkApp { static constexpr bool payload = true; + static constexpr unsigned replyCap = 96; template static void describe(P& put) { Ops::describe(put); } template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { using mockbmp::State; @@ -120,7 +121,7 @@ struct BufOut { static void put(uint8_t b) { if (n < cap) p[n] = b; ++n; } }; static role::Machine<>::Report rep0; -static role::Link, BufOut, LinkApp, 48, 96> link(rep0, 0); +static role::Link, BufOut, LinkApp, 48> link(rep0, 0); extern "C" __attribute__((visibility("default"))) int32_t onemachine_call(uint8_t op, const uint8_t* in, uint16_t n, uint8_t* out, uint16_t cap, uint32_t) { start(); BufOut::p = out; BufOut::cap = cap; BufOut::n = 0; link.feed(op, now); link.feed(uint8_t(n), now); link.feed(uint8_t(n >> 8), now); @@ -133,7 +134,7 @@ struct StdOut { static void put(uint8_t b) { fputc(b, stdout); } }; int main() { start(); static role::Machine<>::Report rep0; - static role::Link, StdOut, LinkApp, 48, 96> link(rep0, 0); + static role::Link, StdOut, LinkApp, 48> link(rep0, 0); int c; while ((c = fgetc(stdin)) != EOF) { link.feed(uint8_t(c), now); if (link.phase == 0) fflush(stdout); } return 0; From 3b3ca08cd71405929557f483dab238c956d1cfc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 07:15:04 +0000 Subject: [PATCH 10/19] test/link: an ATmega328P image of the spi air sensor composition, and 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- test/link/avr_tree.cpp | 111 +++++++++++++++++++++++++++++++++++++++++ test/link/sizes.sh | 27 ++++++++++ 2 files changed, 138 insertions(+) create mode 100644 test/link/avr_tree.cpp create mode 100755 test/link/sizes.sh diff --git a/test/link/avr_tree.cpp b/test/link/avr_tree.cpp new file mode 100644 index 0000000..6537c9c --- /dev/null +++ b/test/link/avr_tree.cpp @@ -0,0 +1,111 @@ +// The spi example's air sensor composition on an ATmega328P, for its size: the BMP280 machine of OneMenu ItemDef nodes under the same failure edges +// as examples/spi, on the real AVR TWI core (OneChip), with or without the link. examples/spi is ESP8266-only, so this is the AVR balance of the +// same parts (test/link/sizes.sh builds every variant and prints the table). +// (default) bare: the machine, its failure edges, the sync pass of the published nodes into a sink; no link, no tree ops, no change store +// -DLINK Round 6: role::Link over USART0 with the tree's payload ops (examples/spi/src/tree_ops.h: d v w n), the card as an event +// millis() is Timer0 overflow, as the Arduino core does it; nothing here is the Arduino core. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef LINK + #include "../../examples/spi/src/tree_ops.h" +#else + #include "../../examples/spi/src/bmp280_machine.h" +#endif + +using discover::RowId; +using hapi::Chain; + +using Twi = hw::avr::mega::Twi<100000>; +using Uart = hw::avr::mega::Serial0<115200>; + +static volatile uint32_t ms = 0; +ISR(TIMER0_OVF_vect) { ms = ms + 1; } // 16 MHz / 64 / 256: 1.024 ms, close enough for a size image +uint32_t millis() { uint8_t s = SREG; cli(); const uint32_t m = ms; SREG = s; return m; } + +struct Mode { + static constexpr bool checked = true, returnPath = false, idempotent = true, lifecycle = true; + template using BusStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Backoff<100, 400>, fail::Status>; + template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, + fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; +}; +struct App; +using M = bmpm::Machine, Mode>; +using Drivers = Chain; +struct App : discover::World, M::Entries, 3, discover::I2cScan>, fail::BusEdge { + static constexpr bool lifecycle = true; + static void release(RowId) {} + static void unbindAll() {} + static void busReset() { Twi::begin(); } +}; +using Ticker = fail::Ticks; + +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"); }; + +volatile int32_t g_sink; +volatile uint32_t g_card; // a card's UID, written by whatever reads the reader (an RC522 is not part of this image) +#ifdef LINK +using Queue = bmpm::ChangeQueue<8>; +template static void note(int32_t v) { Queue::note(Code::num, v); } +#else +template static void note(int32_t v) { g_sink = v; } +#endif + +using PubTemp = bmpm::PublishedAt, oneData::OnSync<¬e>>; +using PubPress = bmpm::PublishedAt, oneData::OnSync<¬e>>; +using PubAir = bmpm::PublishedAt>; +using PubConfig = bmpm::PublishedAt>; +using PubCtrlMeas = bmpm::PublishedAt>; +using Pubs = Chain; + +#ifdef LINK +struct Extra { + using Codes = Chain; + static uint8_t status(uint8_t) { return 0; } + template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32 status alive\n"; while (*s) put(*s++); } +}; +using Ops = bmpm::TreeOps; +struct UartOut { static void put(uint8_t b) { Uart::putch(b); } }; +struct LinkApp { + static constexpr bool payload = true; + static constexpr unsigned replyCap = 96; + template static void describe(P& put) { Ops::describe(put); } + template static void request(uint8_t op, const uint8_t* in, uint16_t n, R& r) { Ops::request(op, in, n, r); } +}; +static role::Machine<>::Report linkReport; +static role::Link, UartOut, LinkApp, 48> link(linkReport, 0); +#endif + +int main() { + TCCR0B = (1 << CS01) | (1 << CS00); TIMSK0 = (1 << TOIE0); sei(); + Twi::begin(); + Uart::begin(); + App::discover(); + uint32_t nextAir = 0, lastCard = 0; + for (;;) { + const uint32_t now = millis(); +#ifdef LINK + while (Uart::available()) link.feed(Uart::getch(), now); + Ops::watch(); + if (g_card != lastCard) { lastCard = g_card; Queue::note(CodeCard::num, int32_t(lastCard)); } +#else + if (g_card != lastCard) { lastCard = g_card; g_sink = int32_t(lastCard); } +#endif + App::tickBuses(now); + Ticker::run(now); + if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; App::pump(); bmpm::PublishAll::sync(); } + } +} diff --git a/test/link/sizes.sh b/test/link/sizes.sh new file mode 100755 index 0000000..95dd907 --- /dev/null +++ b/test/link/sizes.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# The size of the machine tree and its link, as a full balance: every variant against the bare image (no link) on the same target. +# AVR test/link/avr_tree.cpp, avr-g++ -Os atmega328p: flash = text + data, RAM = data + bss +# ESP8266 examples/spi with PlatformIO (env d1_mini is bare, d1_mini_link is the link): flash and RAM as PlatformIO reports them +# Variants: "name:avr flags:pio env" per line in VARIANTS (an empty env: no ESP8266 build for that variant). ESP=0 skips the ESP8266 builds. +set -e +cd "$(dirname "$0")" +INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include -I ../../../OneChip/include -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" +AVRFL="-std=gnu++17 -Os -mmcu=atmega328p -DF_CPU=16000000UL -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics" +VARIANTS=${VARIANTS:-"bare::d1_mini +round6:-DLINK:d1_mini_link"} +OUT=$(mktemp -d); trap 'rm -rf "$OUT"' EXIT +printf '%-14s %10s %8s %12s %10s\n' variant avr_flash avr_ram esp_flash esp_ram +while IFS=: read -r name flags env; do + [ -z "$name" ] && continue + af=- ar=- ef=- er=- + if command -v avr-g++ >/dev/null; then + avr-g++ $AVRFL $flags $INC avr_tree.cpp -Wl,--gc-sections -o "$OUT/$name.elf" 2>"$OUT/$name.err" || { echo "FAIL: avr $name"; cat "$OUT/$name.err" | grep error | head; exit 1; } + read -r t d b _ < <(avr-size "$OUT/$name.elf" | awk 'NR==2') + af=$((t + d)); ar=$((d + b)) + fi + if [ -n "$env" ] && [ "${ESP:-1}" = 1 ] && command -v pio >/dev/null; then + log=$(cd ../../examples/spi && pio run -e "$env" 2>&1) || { echo "FAIL: pio $env"; echo "$log" | grep -E "error" | head; exit 1; } + er=$(echo "$log" | sed -n 's/^RAM:.*used \([0-9]*\) bytes.*/\1/p'); ef=$(echo "$log" | sed -n 's/^Flash:.*used \([0-9]*\) bytes.*/\1/p') + fi + printf '%-14s %10s %8s %12s %10s\n' "$name" "$af" "$ar" "$ef" "$er" +done <<< "$VARIANTS" From ba9487bc9624f9adb04cad65d36a6d6844121c53 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 07:19:21 +0000 Subject: [PATCH 11/19] Redrawn experiment 1: state changes as pending bits and a sequence number; 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/README.md | 10 +++--- examples/spi/src/main.cpp | 2 +- examples/spi/src/tree_ops.h | 67 +++++++++++++++++++++++++++++++------ python/onemachine/tree.py | 51 +++++++++++++++++----------- test/link/avr_tree.cpp | 9 +++-- test/link/build.sh | 3 ++ test/link/check_tree.py | 27 +++++++++++++++ test/link/tree_device.cpp | 2 +- 8 files changed, 131 insertions(+), 40 deletions(-) diff --git a/examples/spi/README.md b/examples/spi/README.md index ec34f36..86b767b 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -73,10 +73,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 (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 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 diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index f54d08e..ef46434 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -205,7 +205,7 @@ template static void say(int32_t v) { for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Log.print('0'); Log.println(frac); #ifdef ONEMACHINE_LINK - bmpm::ChangeQueue<8>::note(Code::num, v); // and for the consumer that reads changes over the link + bmpm::StateChanges<>::mark(Code::num); // and for the consumer that reads changes over the link: a pending bit, the value is read when it asks #endif } using PubTemp = bmpm::PublishedAt, oneData::OnSync<&say>>; diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index 5f07ab5..c342616 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -7,13 +7,20 @@ // capture, the intent that comes back), a sensor value the last one measured. A group or an event has no value: the status alone, with NoValue. // 'w' set by code payload: the value (i32), then the code reply: nothing; the set goes through the node: its limits, its capture, // its register (a part that is gone keeps it as the last intent) -// 'n' changes since payload: none reply: u8 how many were refused since the last 'n', then per change -// the code's number (u8) and its value (i32); a number with bit 7 set -// is a status change: the code that stands for its row, and the new status +// 'n' changes since payload: u16 the sequence number the consumer reply: u16 the sequence number now, u8 flags (bit 0 resync: the +// got from its last 'n' (0 at the start) consumer's number is not the one last answered, so every code is +// sent; bit 1 more: call again), u8 how many events were refused +// since the last 'n', then records: the code's number (u8) and an +// i32. A number with bit 7 set is a row's status (the code that stands +// for its row, and the status now); otherwise the code's value now +// (a state code) or one occurrence (an event code), oldest first. // status Ok, BadLength, BadValue (outside the node's limits), NoValue, ReadOnly, Unknown (no such code or op) -// What changed is told by the published nodes' OnSync/OnChange functions (note(code number, value)) and by watch() (a row's status): into a -// fail::Buffer, a store that refuses the newest when it is full and counts the refusal. The consumer is told how many it missed (the u8 in the -// reply) and reads the values it follows again. +// State and events are kept apart (OneMachine Redrawn, experiment 1). A state code (a value, a register, a row's status) has one pending bit per +// code (StateChanges): a change sets it and counts the machine's sequence number; 'n' sends the codes whose bit is set with their value as it is +// when the reply is made, and clears the bits. A burst of changes is one record with the latest value: state cannot overflow. An event code (the +// card) has a queue, fail::Buffer, that refuses the newest when full and counts the refusal (the u8 in the reply). +// The sequence number is what makes a lost reply safe: a consumer that did not get the last reply asks with an older number, and the device then +// sends every code (resync) instead of the bits it had already cleared. #pragma once #include #include @@ -23,7 +30,7 @@ namespace bmpm { - // the changes waiting to be read: a Buffer of N records under a status that counts what it refused + // the events waiting to be read: a Buffer of N records under a status that counts what it refused template struct ChangeQueue { struct Env { @@ -38,7 +45,19 @@ namespace bmpm { inline static Q q; inline static uint8_t seenDrops = 0; static void note(uint8_t code, int32_t v) { (void)q.offer(fail::Rec{code, 0, v}); } - static void noteStatus(uint8_t code, uint8_t st) { note(uint8_t(0x80 | code), st); } + }; + + // the state codes that changed since the last 'n': one bit per code for its value and one for its row's status, and the machine's sequence + // number (every change counts it; `served` is the number the last reply carried). Up to N codes. + template + struct StateChanges { + static constexpr uint8_t bytes = uint8_t((N + 7) / 8); + inline static uint8_t val[bytes] = {}, st[bytes] = {}; + inline static uint16_t seq = 0, served = 0; + static void mark(uint8_t code) { val[code >> 3] |= uint8_t(1u << (code & 7)); ++seq; } + static void markStatus(uint8_t code) { st[code >> 3] |= uint8_t(1u << (code & 7)); ++seq; } + static void note(uint8_t code, int32_t) { mark(code); } // the shape of an OnSync function's call: the value is read when the reply is made + static bool take(uint8_t* bits, uint8_t code) { const uint8_t m = uint8_t(1u << (code & 7)); if (!(bits[code >> 3] & m)) return false; bits[code >> 3] &= uint8_t(~m); return true; } }; // a code carries its number (`static constexpr uint8_t num`), the one the change records use; the numbers are the codes' positions in the list @@ -66,6 +85,8 @@ namespace bmpm { static constexpr uint8_t numCodes = uint8_t(numPubs + Extra::Codes::size); static constexpr bool payload = true; using Queue = ChangeQueue; + using State = StateChanges<>; + static_assert(numCodes <= 8, "StateChanges<> holds 8 codes: give it more"); template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } @@ -115,7 +136,7 @@ namespace bmpm { for (uint8_t c = 0; c < numCodes; ++c) { if (rep(c) != c) continue; const uint8_t s = status(c); - if (seeded && s != last[c]) Queue::noteStatus(c, s); + if (seeded && s != last[c]) State::markStatus(c); last[c] = s; } seeded = true; @@ -148,6 +169,11 @@ namespace bmpm { ((i++ == at ? (r = getOne(v, st), 0) : 0), ...); return r; } + template static bool hasValue(hapi::Chain*, int at) { + int i = 0; bool r = false; + ((i++ == at ? (r = !IsGroup::value, 0) : 0), ...); + return r; + } template static uint8_t set(hapi::Chain*, int at, int32_t v) { int i = 0; uint8_t r = role::LinkUnknown; ((i++ == at ? (r = setOne(v), 0) : 0), ...); @@ -175,10 +201,29 @@ namespace bmpm { return; } case 'n': { + if (n != 2) { r.status(role::LinkBadLength); return; } + const uint16_t since = uint16_t(in[0] | (in[1] << 8)); + uint8_t flags = 0; + if (since != State::served) { // the consumer missed the last reply (or is new to a device that answered before): every code + flags = 1; + for (uint8_t c = 0; c < numCodes; ++c) { + if (rep(c) == c) State::markStatus(c); + if (c < numPubs && hasValue(list, c)) State::mark(c); + } + } auto& q = Queue::q; const uint8_t drops = q.status().drops; - r.status(role::LinkOk); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; - while (!q.empty() && unsigned(r.n) + 5 <= sizeof r.data) { const fail::Rec& c = q.front(); r.put(c.cap); r.put32(c.v); q.pop(); } + r.status(role::LinkOk); r.put(0); r.put(0); r.put(0); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; + auto room = [&] { return unsigned(r.n) + 5 <= sizeof r.data; }; + for (uint8_t c = 0; c < numCodes; ++c) + if (room()) { if (State::take(State::st, c)) { r.put(uint8_t(0x80 | c)); r.put32(status(c)); } } + for (uint8_t c = 0; c < numPubs; ++c) + if (room()) { int32_t v = 0; if (State::take(State::val, c) && get(list, c, v, status(c)) == role::LinkOk) { r.put(c); r.put32(v); } } + while (!q.empty() && room()) { const fail::Rec& e = q.front(); r.put(e.cap); r.put32(e.v); q.pop(); } + for (uint8_t i = 0; i < State::bytes; ++i) if (State::st[i] | State::val[i]) flags |= 2; + if (!q.empty()) flags |= 2; + State::served = State::seq; + r.data[0] = uint8_t(State::seq); r.data[1] = uint8_t(State::seq >> 8); r.data[2] = flags; return; } default: r.status(role::LinkUnknown); return; diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index ab84c24..9d0d8dc 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -8,14 +8,18 @@ m.changes() # [Change('temp', 27.61, 2761), Change('air', status='stale'), ...]: what changed since the last call m.status('air') # 'alive', 'stale' or 'gone': the row the code is bound to (the part's, not a register that reads 0xFF) m.reading('temp') # Reading(value=27.61, status='stale'): never raises; m.temp raises Stale when the part is not alive - m.missed # how many notifications the device had to refuse (its queue was full) since the start + m.missed # how many events the device had to refuse (its event queue was full) since the start + m.resyncs # how often the device sent every code because a reply was lost A code is a name, with `/` for the part of a group (`air/ctrl_meas` is `m.air.ctrl_meas`). The description lists, per code: its path in the machine (`/
/[/]`, for people), whether it notifies (`sync`: a value that moves; `event`), read-only or read-write, its kind (a value, a register with its default, a group) and, for a value, how many decimals it is scaled by and the range a set accepts. A part that is not alive is Stale or Gone: reading one of its codes raises Stale (it carries the last value), and the change is announced by changes() as one status change per code of that part (the device sends it once, through the first of them). -A device that refused notifications (`missed`) says how many; the values a consumer follows are read again with get().""" +State and events are told apart. A state code (a value, a register, a row's status) is one pending bit on the device: changes() gets each code +that changed once, with its value as it is now, however often it changed in between; it cannot overflow. An event code (the card) is queued, and a +device that refused events (`missed`) says how many. changes() sends the sequence number of the last reply it got; a device that answered since +(a reply that was lost) sends every code again (`resyncs` counts it).""" import struct from collections import namedtuple from .machine import LinkError @@ -83,6 +87,8 @@ class Tree: def __init__(self, link): object.__setattr__(self, 'link', link) object.__setattr__(self, 'missed', 0) + object.__setattr__(self, 'resyncs', 0) + object.__setattr__(self, '_seq', 0) self.refresh() def _call(self, op, payload=b''): @@ -147,23 +153,28 @@ def set(self, code, value): if st != OK: raise LinkError('set %s: status %d' % (code, st)) def changes(self): - """What changed since the last call, oldest first. `missed` counts what the device could not queue (its store was full).""" - st, data = self._call('n') - if st != OK or len(data) < 1 or (len(data) - 1) % 5: raise LinkError('changes: status %d, %d bytes' % (st, len(data))) - object.__setattr__(self, 'missed', self.missed + data[0]) + """What changed since the last call: each state code that changed, once, with its value now; the events, oldest first. `missed` counts the + events the device could not queue; `resyncs` counts the replies that carried every code (the previous reply did not arrive).""" out = [] - for i in range(1, len(data), 5): - num, raw = data[i], struct.unpack_from('; -template static void note(int32_t v) { Queue::note(Code::num, v); } +#ifndef EVENTS + #define EVENTS 8 // the event queue's depth (the card) +#endif +using Queue = bmpm::ChangeQueue; +template static void note(int32_t) { bmpm::StateChanges<>::mark(Code::num); } // a state code: a pending bit #else template static void note(int32_t v) { g_sink = v; } #endif @@ -77,7 +80,7 @@ struct Extra { static uint8_t status(uint8_t) { return 0; } template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32 status alive\n"; while (*s) put(*s++); } }; -using Ops = bmpm::TreeOps; +using Ops = bmpm::TreeOps; struct UartOut { static void put(uint8_t b) { Uart::putch(b); } }; struct LinkApp { static constexpr bool payload = true; diff --git a/test/link/build.sh b/test/link/build.sh index 3acbe6d..94c5bff 100755 --- a/test/link/build.sh +++ b/test/link/build.sh @@ -9,5 +9,8 @@ g++ -std=c++17 -O1 -Wall $INC tree_device.cpp -o tree_device g++ -std=c++17 -O1 -Wall -shared -fPIC -DSIM_LIB $INC tree_device.cpp -o tree_device.so python3 check_tree.py pipe || rc=1 python3 check_tree.py ctypes || rc=1 +# and the device under ASan/UBSan, over the pipe +g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $INC tree_device.cpp -o tree_device +python3 check_tree.py pipe | sed 's/^OK: python Tree over a pipe$/OK: python Tree over a pipe (ASan+UBSan)/' ; [ "${PIPESTATUS[0]}" = 0 ] || rc=1 rm -f tree_device tree_device.so exit $rc diff --git a/test/link/check_tree.py b/test/link/check_tree.py index 72b707c..c4e0507 100644 --- a/test/link/check_tree.py +++ b/test/link/check_tree.py @@ -118,5 +118,32 @@ def reg(a): return op('R', bytes([a]))[1][0] check(m.missed - before == 4, 'and the 4 refused are counted: %d' % (m.missed - before)) check(m.changes() == [] and m.missed - before == 4, 'the count is told once') +# ---- a burst of state changes: one record per code, with the latest value; events in the same burst are still queued and counted ------- +m.changes(); before = m.missed +for i in range(100): + op('S', struct.pack(' 4, 'an answer that is lost') +r0 = m.resyncs +ch = m.changes() # still asks since seq0: the device answered since +check(m.resyncs == r0 + 1, 'the device saw the lost reply: resync') +check(sorted({x.code for x in ch if x.status is None}) == ['air/config', 'air/ctrl_meas', 'press', 'temp'] + and {x.code: x.raw for x in ch if x.status is None}['temp'] == m.raw('temp'), 'every code with a value, as it is now: %r' % ch) +check(sorted(x.code for x in ch if x.status == 'alive') == ['air', 'air/config', 'air/ctrl_meas', 'card', 'press', 'temp'], 'and every row\'s status') +check(m.changes() == [] and m.resyncs == r0 + 1, 'then in step again') + print('FAILED: %d' % failures if failures else 'OK: python Tree over %s' % ('ctypes' if CTYPES else 'a pipe')) sys.exit(1 if failures else 0) diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index 3430249..a6cb0de 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -51,7 +51,7 @@ struct CodeCtrlMeas { static constexpr uint8_t num = 4; ONEMACHINE_STATE_NAME(n struct CodeCard { static constexpr uint8_t num = 5; ONEMACHINE_STATE_NAME(name, "card"); }; using Queue = bmpm::ChangeQueue<8>; -template static void note(int32_t v) { Queue::note(Code::num, v); } +template static void note(int32_t) { bmpm::StateChanges<>::mark(Code::num); } // a state code: a pending bit using PubTemp = bmpm::PublishedAt, oneData::OnSync<¬e>>; using PubPress = bmpm::PublishedAt, oneData::OnSync<¬e>>; From 2ddfe51a40a17181b183d07f43779e1c63ea5733 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 07:48:30 +0000 Subject: [PATCH 12/19] Redrawn experiment 1, trimmed: one pending bit per code, and resync as 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/src/tree_ops.h | 62 ++++++++++++++----------------------- python/onemachine/tree.py | 24 ++++++++------ 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index c342616..e5ae64b 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -10,17 +10,17 @@ // 'n' changes since payload: u16 the sequence number the consumer reply: u16 the sequence number now, u8 flags (bit 0 resync: the // got from its last 'n' (0 at the start) consumer's number is not the one last answered, so every code is // sent; bit 1 more: call again), u8 how many events were refused -// since the last 'n', then records: the code's number (u8) and an -// i32. A number with bit 7 set is a row's status (the code that stands -// for its row, and the status now); otherwise the code's value now -// (a state code) or one occurrence (an event code), oldest first. +// since the last 'n', then records of 5 bytes: u8 (kind << 6 | code +// number), i32. Kind 0..2: a state code as it is now, the status of +// its row (Alive 0, Stale 1, Gone 2) and its value (0 for a group); +// kind 3: one occurrence of an event code, oldest first. // status Ok, BadLength, BadValue (outside the node's limits), NoValue, ReadOnly, Unknown (no such code or op) -// State and events are kept apart (OneMachine Redrawn, experiment 1). A state code (a value, a register, a row's status) has one pending bit per -// code (StateChanges): a change sets it and counts the machine's sequence number; 'n' sends the codes whose bit is set with their value as it is -// when the reply is made, and clears the bits. A burst of changes is one record with the latest value: state cannot overflow. An event code (the +// State and events are kept apart (OneMachine Redrawn, experiment 1). A state code has one pending bit (StateChanges): its value moved, or the status +// of its row did (the code that stands for the row); either sets the bit and counts the machine's sequence number. 'n' sends each code whose bit is +// set with its status and value as they are when the reply is made, and clears the bit. A burst of changes is one record with the latest value: state cannot overflow. An event code (the // card) has a queue, fail::Buffer, that refuses the newest when full and counts the refusal (the u8 in the reply). // The sequence number is what makes a lost reply safe: a consumer that did not get the last reply asks with an older number, and the device then -// sends every code (resync) instead of the bits it had already cleared. +// sends every code (resync): every bit is set, and the reply is made as any other. #pragma once #include #include @@ -47,17 +47,18 @@ namespace bmpm { static void note(uint8_t code, int32_t v) { (void)q.offer(fail::Rec{code, 0, v}); } }; - // the state codes that changed since the last 'n': one bit per code for its value and one for its row's status, and the machine's sequence - // number (every change counts it; `served` is the number the last reply carried). Up to N codes. + // the state codes that changed since the last 'n': one bit per code (its value or its row's status moved), and the machine's sequence number + // (every change counts it; `served` is the number the last reply carried). Up to N codes. template struct StateChanges { static constexpr uint8_t bytes = uint8_t((N + 7) / 8); - inline static uint8_t val[bytes] = {}, st[bytes] = {}; + inline static uint8_t bits[bytes] = {}; inline static uint16_t seq = 0, served = 0; - static void mark(uint8_t code) { val[code >> 3] |= uint8_t(1u << (code & 7)); ++seq; } - static void markStatus(uint8_t code) { st[code >> 3] |= uint8_t(1u << (code & 7)); ++seq; } + static void mark(uint8_t code) { bits[code >> 3] |= uint8_t(1u << (code & 7)); ++seq; } static void note(uint8_t code, int32_t) { mark(code); } // the shape of an OnSync function's call: the value is read when the reply is made - static bool take(uint8_t* bits, uint8_t code) { const uint8_t m = uint8_t(1u << (code & 7)); if (!(bits[code >> 3] & m)) return false; bits[code >> 3] &= uint8_t(~m); return true; } + static void fill(uint8_t n) { for (uint8_t i = 0; i < bytes; ++i) bits[i] = n >= 8 * (i + 1) ? 0xFF : n > 8 * i ? uint8_t((1u << (n - 8 * i)) - 1) : 0; } + static bool take(uint8_t code) { const uint8_t m = uint8_t(1u << (code & 7)); if (!(bits[code >> 3] & m)) return false; bits[code >> 3] &= uint8_t(~m); return true; } + static bool any() { for (uint8_t b : bits) if (b) return true; return false; } }; // a code carries its number (`static constexpr uint8_t num`), the one the change records use; the numbers are the codes' positions in the list @@ -86,7 +87,7 @@ namespace bmpm { static constexpr bool payload = true; using Queue = ChangeQueue; using State = StateChanges<>; - static_assert(numCodes <= 8, "StateChanges<> holds 8 codes: give it more"); + static_assert(numCodes <= 8, "StateChanges<> holds 8 codes: give it more (a record has room for 64)"); template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } @@ -136,7 +137,7 @@ namespace bmpm { for (uint8_t c = 0; c < numCodes; ++c) { if (rep(c) != c) continue; const uint8_t s = status(c); - if (seeded && s != last[c]) State::markStatus(c); + if (seeded && s != last[c]) State::mark(c); last[c] = s; } seeded = true; @@ -169,11 +170,6 @@ namespace bmpm { ((i++ == at ? (r = getOne(v, st), 0) : 0), ...); return r; } - template static bool hasValue(hapi::Chain*, int at) { - int i = 0; bool r = false; - ((i++ == at ? (r = !IsGroup::value, 0) : 0), ...); - return r; - } template static uint8_t set(hapi::Chain*, int at, int32_t v) { int i = 0; uint8_t r = role::LinkUnknown; ((i++ == at ? (r = setOne(v), 0) : 0), ...); @@ -202,28 +198,16 @@ namespace bmpm { } case 'n': { if (n != 2) { r.status(role::LinkBadLength); return; } - const uint16_t since = uint16_t(in[0] | (in[1] << 8)); - uint8_t flags = 0; - if (since != State::served) { // the consumer missed the last reply (or is new to a device that answered before): every code - flags = 1; - for (uint8_t c = 0; c < numCodes; ++c) { - if (rep(c) == c) State::markStatus(c); - if (c < numPubs && hasValue(list, c)) State::mark(c); - } - } + const uint8_t resync = uint16_t(in[0] | (in[1] << 8)) != State::served; // the consumer missed the last reply: every code + if (resync) State::fill(numCodes); auto& q = Queue::q; const uint8_t drops = q.status().drops; r.status(role::LinkOk); r.put(0); r.put(0); r.put(0); r.put(uint8_t(drops - Queue::seenDrops)); Queue::seenDrops = drops; - auto room = [&] { return unsigned(r.n) + 5 <= sizeof r.data; }; - for (uint8_t c = 0; c < numCodes; ++c) - if (room()) { if (State::take(State::st, c)) { r.put(uint8_t(0x80 | c)); r.put32(status(c)); } } - for (uint8_t c = 0; c < numPubs; ++c) - if (room()) { int32_t v = 0; if (State::take(State::val, c) && get(list, c, v, status(c)) == role::LinkOk) { r.put(c); r.put32(v); } } - while (!q.empty() && room()) { const fail::Rec& e = q.front(); r.put(e.cap); r.put32(e.v); q.pop(); } - for (uint8_t i = 0; i < State::bytes; ++i) if (State::st[i] | State::val[i]) flags |= 2; - if (!q.empty()) flags |= 2; + for (uint8_t c = 0; c < numCodes && unsigned(r.n) + 5 <= sizeof r.data; ++c) + if (State::take(c)) { const uint8_t st = status(c); int32_t v = 0; if (c < numPubs) (void)get(list, c, v, st); r.put(uint8_t(st << 6 | c)); r.put32(v); } + while (!q.empty() && unsigned(r.n) + 5 <= sizeof r.data) { const fail::Rec& e = q.front(); r.put(uint8_t(0xC0 | e.cap)); r.put32(e.v); q.pop(); } State::served = State::seq; - r.data[0] = uint8_t(State::seq); r.data[1] = uint8_t(State::seq >> 8); r.data[2] = flags; + r.data[0] = uint8_t(State::seq); r.data[1] = uint8_t(State::seq >> 8); r.data[2] = uint8_t(resync | ((State::any() || !q.empty()) << 1)); return; } default: r.status(role::LinkUnknown); return; diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index 9d0d8dc..6e3d6a0 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -89,6 +89,8 @@ def __init__(self, link): object.__setattr__(self, 'missed', 0) object.__setattr__(self, 'resyncs', 0) object.__setattr__(self, '_seq', 0) + object.__setattr__(self, '_last', {}) # the value changes() last gave per code + object.__setattr__(self, '_told', {}) # the status changes() last gave per row self.refresh() def _call(self, op, payload=b''): @@ -163,17 +165,21 @@ def changes(self): object.__setattr__(self, '_seq', seq) object.__setattr__(self, 'missed', self.missed + drops) if flags & 1: object.__setattr__(self, 'resyncs', self.resyncs + 1) + told = set() # the rows whose status this reply announced for i in range(4, len(data), 5): - num, raw = data[i], struct.unpack_from('> 6, data[i] & 0x3F, struct.unpack_from(' Date: Mon, 5 Oct 2026 07:54:59 +0000 Subject: [PATCH 13/19] Redrawn experiment 2: the description by hash, its text from the build output 'd' answers `hash ` 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 .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//description. -DONEMACHINE_DESC_TEXT (env d1_mini_link_text) keeps the text. The Python Tree takes descriptions=, loads .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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/README.md | 2 +- examples/spi/describe.cpp | 30 +++++++++++++++++++++ examples/spi/describe.py | 15 +++++++++++ examples/spi/platformio.ini | 7 ++++- examples/spi/src/air_tree.h | 36 +++++++++++++++++++++++++ examples/spi/src/bmp280_machine.h | 44 ++++++++++++++++++++++--------- examples/spi/src/main.cpp | 31 +++++++++------------- examples/spi/src/tree_ops.h | 23 +++++++++++++--- python/onemachine/tree.py | 36 ++++++++++++++++++++++--- test/link/avr_tree.cpp | 26 +++++++----------- test/link/build.sh | 18 +++++++++---- test/link/check_tree.py | 15 ++++++++--- test/link/tree_device.cpp | 24 ++++++----------- 13 files changed, 226 insertions(+), 81 deletions(-) create mode 100644 examples/spi/describe.cpp create mode 100644 examples/spi/describe.py create mode 100644 examples/spi/src/air_tree.h diff --git a/examples/spi/README.md b/examples/spi/README.md index ec34f36..e0a9de9 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -73,7 +73,7 @@ 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 +Ops: `d` the description by its hash and each code's status (the text is in the build output: `.pio/build//description/.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 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. diff --git a/examples/spi/describe.cpp b/examples/spi/describe.cpp new file mode 100644 index 0000000..54d7b57 --- /dev/null +++ b/examples/spi/describe.cpp @@ -0,0 +1,30 @@ +// 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=)). +// describe writes /.txt (the text without the status) and prints its path +// The hash is FNV-1a over the text, folded at compile time by the same walk the firmware folds (bmpm::Fnv); this program checks the two agree. +// Built on the host: g++ -std=c++17 -I describe.cpp (examples/spi/describe.py does it for PlatformIO). +#include +#include +#include "src/air_tree.h" + +struct NoApp {}; // the description needs the machine's types only: no bus, no registry +using M = airTree::Machine; +template struct NoNote { static void fn(int32_t) {} }; +using Pubs = airTree::Pubs; +struct Text { static constexpr bool noStatus = true; std::string s; void operator()(char c) { s += c; } }; + +template constexpr void describeAll(P& put) { bmpm::describe(put, airTree::bus); airTree::cardLine(put); put('\n'); } +constexpr uint32_t folded() { bmpm::Fnv f; describeAll(f); return f.h; } + +int main(int argc, char** argv) { + if (argc != 2) { fprintf(stderr, "usage: describe \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); + printf("%s\n", path); + return 0; +} diff --git a/examples/spi/describe.py b/examples/spi/describe.py new file mode 100644 index 0000000..a4851ed --- /dev/null +++ b/examples/spi/describe.py @@ -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 /description/.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()) diff --git a/examples/spi/platformio.ini b/examples/spi/platformio.ini index c8b45e5..3e157be 100644 --- a/examples/spi/platformio.ini +++ b/examples/spi/platformio.ini @@ -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 diff --git a/examples/spi/src/air_tree.h b/examples/spi/src/air_tree.h new file mode 100644 index 0000000..50a5782 --- /dev/null +++ b/examples/spi/src/air_tree.h @@ -0,0 +1,36 @@ +// 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) +// Note Note::fn(int32_t): what a published value calls when it changes (the App's: print it, mark it for the link, ...) +#pragma once +#include +#include +#include "bmp280_machine.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 sensor: the machine at 0x76 (its Criteria), under the App W's failure handling Mode + template using Machine = bmpm::Machine, 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 class Note> using Pubs = hapi::Chain< + bmpm::PublishedAt, oneData::OnSync<&Note::fn>>, + bmpm::PublishedAt, oneData::OnSync<&Note::fn>>, + bmpm::PublishedAt>, + bmpm::PublishedAt>, + bmpm::PublishedAt>>; + + // the card: a code that only notifies (an event with a value, the UID, 0 when it leaves); the App adds the status of the reader's row + template constexpr void cardLine(P& put) { const char* s = " card -> 0/1 notify event ro value u32"; while (*s) put(*s++); } + +} diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index 24d7b77..8e8d8cb 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -372,15 +372,32 @@ namespace bmpm { template struct IsGroup : std::false_type {}; template struct IsGroup> : std::true_type {}; + // A put may declare `static constexpr bool folds = true` (it is evaluated at compile time: names are read with at(), not from flash) and + // `static constexpr bool noStatus = true` (the lines leave out the status, which is the device's now, not part of what it is). + template struct Folds : std::false_type {}; + template struct Folds> : std::bool_constant {}; + template struct NoStatus : std::false_type {}; + template struct NoStatus> : std::bool_constant {}; + // FNV-1a over the description's text without the status: the description's hash, at compile time (state::fnv, as the schema hash) + struct Fnv { static constexpr bool folds = true, noStatus = true; uint32_t h = 2166136261u; constexpr void operator()(char c) { h = state::fnv(h, uint8_t(c)); } }; + + inline constexpr char hexDigits[] = "0123456789ABCDEF"; template struct Walk { P& put; - void str(const char* s) { while (*s) put(*s++); } - void name(state::Name n) { state::put_name(put, n); } - void dec(uint32_t v) { state::put_dec(put, v); } - void hex(uint8_t v) { static const char d[] = "0123456789ABCDEF"; put('0'); put('x'); put(d[v >> 4]); put(d[v & 15]); } + constexpr void str(const char* s) { while (*s) put(*s++); } + constexpr void name(state::Name n) { if constexpr (Folds

::value) { for (unsigned i = 0; n.at(i); ++i) put(n.at(i)); } else state::put_name(put, n); } + constexpr void dec(uint32_t v) { + if constexpr (Folds

::value) { char b[10] = {}; unsigned k = 0; do { b[k++] = char('0' + v % 10); v /= 10; } while (v); while (k) put(b[--k]); } + else state::put_dec(put, v); + } + constexpr void hex(uint8_t v) { + put('0'); put('x'); + if constexpr (Folds

::value) { const uint8_t hi = uint8_t(v >> 4), lo = uint8_t(v & 15); put(char(hi < 10 ? '0' + hi : 'A' + hi - 10)); put(char(lo < 10 ? '0' + lo : 'A' + lo - 10)); } + else { put(hexDigits[v >> 4]); put(hexDigits[v & 15]); } + } // what one node is: its fields, on the rest of the line - template void fields() { + template constexpr void fields() { if constexpr (IsGroup::value) { str(" group "); dec(N::Body::size()); } else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); if constexpr (HasLimits::value) { str(" range "); dec(uint32_t(N::limLo)); str(".."); dec(uint32_t(N::limHi)); } } @@ -388,29 +405,30 @@ namespace bmpm { else { str(HasSet::value ? " rw" : " ro"); str(" value"); if constexpr (HasDecimals::value) { str(" scaled "); dec(N::decimals); } } } - template void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } - template void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } + template constexpr void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } + template constexpr void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } - template void machine() { + template constexpr void machine() { str("machine "); name(TagBmp::name()); str(" at "); hex(M::addr); put('\n'); nodes(static_cast(nullptr)); regs(static_cast(nullptr)); } - template void published(uint8_t bus) { + template constexpr void published(uint8_t bus) { using Inner = typename Pub::Inner; str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(M::addr); if constexpr (std::is_void::value) { put('/'); dec(unsigned(M::template IndexOf::value)); } else for (unsigned i = 0; i < Pub::Path::depth; ++i) { put('/'); dec(Pub::Path::path[i]); } str(NotifiesSync::value ? " notify sync" : " silent"); fields(); - str(" status "); str(statusName(M::status())); put('\n'); + if constexpr (!NoStatus

::value) { str(" status "); str(statusName(M::status())); } + put('\n'); } - static const char* statusName(uint8_t st) { return st == 0 ? "alive" : st == 1 ? "stale" : "gone"; } - template void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } + static constexpr const char* statusName(uint8_t st) { return st == 0 ? "alive" : st == 1 ? "stale" : "gone"; } + template constexpr void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } }; // M: the machine (its Criteria is the device's identity in the path); Pubs: Chain; bus: the machine's position in the App - template void describe(P& put, uint8_t bus) { + template constexpr void describe(P& put, uint8_t bus) { Walk

w{put}; w.template machine(); w.str("published\n"); diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index f54d08e..43c5cf1 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -47,6 +47,7 @@ #include "tree_ops.h" // the link's ops over the published nodes (role/link.h with payload ops) #endif #include "bmp280_machine.h" +#include "air_tree.h" // the codes and the published nodes (shared with the description generator, ../describe.cpp) #ifndef BUILD_REV #define BUILD_REV "unknown" // set by ../version.py: each repo's git commit @@ -70,13 +71,7 @@ using Twi = esp::Esp8266TwiMaster<4, 5, 100000>; using Spi = hapi::APIOf, esp::OutPin<0>>, // slot 0 D8, slot 1 D3 oneBus::SpiMaster<4000000>, esp::Esp8266SpiCore>; -// The codes the App publishes (the numbers are the codes' positions in the description: the link's change records use them) -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"); }; +using airTree::CodeTemp; using airTree::CodePress; using airTree::CodeCard; // the codes the App publishes: air_tree.h #ifdef ONEMACHINE_LINK // the card, as a change the link can read: its UID when it arrives, 0 when it leaves @@ -182,7 +177,7 @@ struct AirMode { fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; }; struct AirApp; -using Bmp = bmpm::Machine, AirMode>; // the sensor at 0x76: its Criteria +using Bmp = airTree::Machine; // the sensor at 0x76: its Criteria using AirDrivers = Chain; struct AirApp : discover::World, Bmp::Entries, 3, discover::I2cScan>, fail::BusEdge { static constexpr bool lifecycle = true; @@ -208,13 +203,9 @@ template static void say(int32_t v) { bmpm::ChangeQueue<8>::note(Code::num, v); // and for the consumer that reads changes over the link #endif } -using PubTemp = bmpm::PublishedAt, oneData::OnSync<&say>>; -using PubPress = bmpm::PublishedAt, oneData::OnSync<&say>>; -using PubAir = bmpm::PublishedAt>; -using PubConfig = bmpm::PublishedAt>; // the leaves of the group, by their paths -using PubCtrlMeas = bmpm::PublishedAt>; -using Published = Chain; -constexpr uint8_t airBus = 1; // the air sensor's bus is the App's second machine: its path codes start with 1 +template struct Say { static void fn(int32_t v) { say(v); } }; +using Published = airTree::Pubs; // temp, press, air, air/config, air/ctrl_meas (air_tree.h) +constexpr uint8_t airBus = airTree::bus; struct SerialPut { void operator()(char c) { Log.write(c); } }; @@ -345,10 +336,12 @@ static void faultTick(uint32_t now) { struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of the reader's row using Codes = Chain; static uint8_t status(uint8_t) { return RfidApp::reg.count > 1 ? uint8_t(RfidApp::reg.status(1)) : 2; } - template static void describe(P& put) { - const char* s = " card -> 0/1 notify event ro value u32 status "; while (*s) put(*s++); - const uint8_t st = status(0); - s = st == 0 ? "alive" : st == 1 ? "stale" : "gone"; while (*s) put(*s++); + template static constexpr void describe(P& put) { + airTree::cardLine(put); + if constexpr (!bmpm::NoStatus

::value) { + put(' '); const uint8_t st = status(0); + const char* s = st == 0 ? "status alive" : st == 1 ? "status stale" : "status gone"; while (*s) put(*s++); + } put('\n'); } }; diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index 5f07ab5..8f54261 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -1,7 +1,10 @@ // The link's ops over the published nodes of a machine tree. The framing and the bytes are role/link.h's: this is an App with payload ops // (`payload = true`: role::Link calls describe() for op 'd' and request() for the others). -// 'd' the description: bmpm::describe's text, then the codes that only notify (Extra). Codes are numbered in the order they are listed; each line -// ends with the status of the row the code is bound to (alive, stale or gone) as it is now. +// 'd' the description, by its hash (the default): `hash <8 hex digits>\n` then one digit per code, the status of the row it is bound to now +// (0 alive, 1 stale, 2 gone), then `\n`. The hash is FNV-1a over the description's text without the status, computed at compile time from the +// same walk; the consumer has the text from the build (examples/spi/describe.cpp writes .txt; python: Tree(link, descriptions=dir)). +// With -DONEMACHINE_DESC_TEXT: the text itself, bmpm::describe's walk then the codes that only notify (Extra), each line ending with the +// status of its row. Codes are numbered in the order they are listed. // 'v' get by code payload: the code (text) reply: the status (u8), then the value (i32 little-endian) // The status is the device's: Alive 0, Stale 1, Gone 2. A part that is not Alive has no live value: a register answers the last value set (its // capture, the intent that comes back), a sensor value the last one measured. A group or an event has no value: the status alone, with NoValue. @@ -67,7 +70,21 @@ namespace bmpm { static constexpr bool payload = true; using Queue = ChangeQueue; - template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } + // the description's hash: the text without the status, folded at compile time (Extra::describe must be constexpr and leave out its status for a + // put with noStatus) + static constexpr uint32_t hash() { bmpm::Fnv f; bmpm::describe(f, Bus); Extra::describe(f); return f.h; } + template static void describe(P& put) { +#ifdef ONEMACHINE_DESC_TEXT + bmpm::describe(put, Bus); Extra::describe(put); +#else + constexpr uint32_t descHash = hash(); // a constant: the walk is not in the image + put('h'); put('a'); put('s'); put('h'); put(' '); + for (int i = 28; i >= 0; i -= 4) { const uint8_t d = uint8_t((descHash >> i) & 15); put(char(d < 10 ? '0' + d : 'a' + d - 10)); } + put('\n'); + for (uint8_t c = 0; c < numCodes; ++c) put(char('0' + status(c))); + put('\n'); +#endif + } // the code in the payload, against each code tag's name: true when it is that code template static bool named(const uint8_t* s, uint16_t n) { diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index ab84c24..a1cf3ee 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -10,13 +10,16 @@ m.reading('temp') # Reading(value=27.61, status='stale'): never raises; m.temp raises Stale when the part is not alive m.missed # how many notifications the device had to refuse (its queue was full) since the start +A device may describe itself by hash only (the default of examples/spi): `d` answers the hash and each code's status, and the text is read from the +build output, `Tree(link, descriptions=dir)`, where examples/spi/describe.cpp wrote .txt from the same types (a device built with +-DONEMACHINE_DESC_TEXT sends the text itself). A code is a name, with `/` for the part of a group (`air/ctrl_meas` is `m.air.ctrl_meas`). The description lists, per code: its path in the machine (`/

/[/]`, for people), whether it notifies (`sync`: a value that moves; `event`), read-only or read-write, its kind (a value, a register with its default, a group) and, for a value, how many decimals it is scaled by and the range a set accepts. A part that is not alive is Stale or Gone: reading one of its codes raises Stale (it carries the last value), and the change is announced by changes() as one status change per code of that part (the device sends it once, through the first of them). A device that refused notifications (`missed`) says how many; the values a consumer follows are read again with get().""" -import struct +import os, struct from collections import namedtuple from .machine import LinkError @@ -45,6 +48,10 @@ def to_value(self, raw): return raw / 10 ** self.scaled if self.scaled else raw def to_raw(self, value): return int(round(value * 10 ** self.scaled)) if self.scaled else int(value) +def _fnv(data, h=2166136261): + for b in data: h = ((h ^ b) * 16777619) & 0xFFFFFFFF + return h + def _parse(text): """The `published` section: one line per code, in the order the device numbers them.""" codes, seen = [], False @@ -80,8 +87,9 @@ def __setattr__(self, name, value): self._t.set(self._p + '/' + name, value) def __dir__(self): return [c.name[len(self._p) + 1:] for c in self._t._codes if c.name.startswith(self._p + '/')] class Tree: - def __init__(self, link): + def __init__(self, link, descriptions=None): object.__setattr__(self, 'link', link) + object.__setattr__(self, 'descriptions', [descriptions] if isinstance(descriptions, str) else list(descriptions or [])) object.__setattr__(self, 'missed', 0) self.refresh() @@ -93,12 +101,32 @@ def refresh(self): st, data = self._call('d') if st != OK: raise LinkError('description: status %d' % st) text = data.decode() + statuses = None + if text.startswith('hash '): # the description by its hash: the text is in the build output + head, digits = text.split('\n')[:2] + h = int(head[5:], 16) + text = self._load(h) + statuses = [STATUS[int(d)] if int(d) < len(STATUS) else 'gone' for d in digits] codes = _parse(text) + if statuses is not None: + if len(statuses) != len(codes): raise LinkError('description %08x: %d codes, the device has %d' % (h, len(codes), len(statuses))) + for c, st in zip(codes, statuses): c.status = st object.__setattr__(self, 'description', text) object.__setattr__(self, '_codes', codes) object.__setattr__(self, 'codes', {c.name: c for c in codes}) object.__setattr__(self, '_status', {c.src: c.status for c in codes}) # by row: what the description said, then what the device tells + def _load(self, h): + """The text of the description whose hash is h, from the build output (examples/spi/describe.cpp writes .txt).""" + name = '%08x.txt' % h + for d in self.descriptions: + p = os.path.join(d, name) + if os.path.isfile(p): + text = open(p).read() + if _fnv(text.encode()) != h: raise LinkError('%s: its text does not hash to %08x' % (p, h)) + return text + raise LinkError('the device describes itself by hash %08x: no %s in %s (give Tree(link, descriptions=), or build the device with -DONEMACHINE_DESC_TEXT)' % (h, name, self.descriptions or 'no directory')) + def _read(self, code): """(status, raw) from the device: raw is None for a group or an event. The status is the part's; a part that is not alive answers its last value.""" c = self._known(code) @@ -181,9 +209,9 @@ def _access(self, name): raise UnknownCode('no code %r (the device has %s)' % (name, ', '.join(self.codes))) def __getattr__(self, name): # m.temp, m.air - if name.startswith('_') or name in ('codes', 'description', 'link', 'missed'): raise AttributeError(name) + if name.startswith('_') or name in ('codes', 'description', 'descriptions', 'link', 'missed'): raise AttributeError(name) return self._access(name) def __setattr__(self, name, value): - if name.startswith('_') or name in ('link', 'missed'): object.__setattr__(self, name, value) + if name.startswith('_') or name in ('link', 'missed', 'descriptions'): object.__setattr__(self, name, value) else: self.set(name, value) def __dir__(self): return sorted({c.name.split('/')[0] for c in self._codes}) diff --git a/test/link/avr_tree.cpp b/test/link/avr_tree.cpp index 6537c9c..c1a42b9 100644 --- a/test/link/avr_tree.cpp +++ b/test/link/avr_tree.cpp @@ -16,9 +16,8 @@ #include #ifdef LINK #include "../../examples/spi/src/tree_ops.h" -#else - #include "../../examples/spi/src/bmp280_machine.h" #endif +#include "../../examples/spi/src/air_tree.h" using discover::RowId; using hapi::Chain; @@ -38,7 +37,7 @@ struct Mode { fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; }; struct App; -using M = bmpm::Machine, Mode>; +using M = airTree::Machine; using Drivers = Chain; struct App : discover::World, M::Entries, 3, discover::I2cScan>, fail::BusEdge { static constexpr bool lifecycle = true; @@ -48,12 +47,7 @@ struct App : discover::World, M::Entries, 3, discover::I2cScan }; using Ticker = fail::Ticks; -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"); }; +using airTree::CodeCard; // the codes and the published nodes: examples/spi/src/air_tree.h volatile int32_t g_sink; volatile uint32_t g_card; // a card's UID, written by whatever reads the reader (an RC522 is not part of this image) @@ -64,18 +58,18 @@ template static void note(int32_t v) { Queue::note(Code::num, v); template static void note(int32_t v) { g_sink = v; } #endif -using PubTemp = bmpm::PublishedAt, oneData::OnSync<¬e>>; -using PubPress = bmpm::PublishedAt, oneData::OnSync<¬e>>; -using PubAir = bmpm::PublishedAt>; -using PubConfig = bmpm::PublishedAt>; -using PubCtrlMeas = bmpm::PublishedAt>; -using Pubs = Chain; +template struct Note { static void fn(int32_t v) { note(v); } }; +using Pubs = airTree::Pubs; #ifdef LINK struct Extra { using Codes = Chain; static uint8_t status(uint8_t) { return 0; } - template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32 status alive\n"; while (*s) put(*s++); } + template static constexpr void describe(P& put) { + airTree::cardLine(put); + if constexpr (!bmpm::NoStatus

::value) { const char* s = " status alive"; while (*s) put(*s++); } + put('\n'); + } }; using Ops = bmpm::TreeOps; struct UartOut { static void put(uint8_t b) { Uart::putch(b); } }; diff --git a/test/link/build.sh b/test/link/build.sh index 3acbe6d..b474da1 100755 --- a/test/link/build.sh +++ b/test/link/build.sh @@ -5,9 +5,17 @@ set -e cd "$(dirname "$0")" INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" rc=0 -g++ -std=c++17 -O1 -Wall $INC tree_device.cpp -o tree_device -g++ -std=c++17 -O1 -Wall -shared -fPIC -DSIM_LIB $INC tree_device.cpp -o tree_device.so -python3 check_tree.py pipe || rc=1 -python3 check_tree.py ctypes || rc=1 -rm -f tree_device tree_device.so +# the description by hash (the default): the build output holds the text, written by the generator from the same types +DESC=$(mktemp -d); trap 'rm -rf "$DESC"' EXIT +g++ -std=c++17 -O1 -Wall $INC ../../examples/spi/describe.cpp -o describe && ./describe "$DESC" >/dev/null || rc=1 +for mode in "" "-DONEMACHINE_DESC_TEXT"; do + g++ -std=c++17 -O1 -Wall $mode $INC tree_device.cpp -o tree_device + g++ -std=c++17 -O1 -Wall -shared -fPIC -DSIM_LIB $mode $INC tree_device.cpp -o tree_device.so + python3 check_tree.py pipe "$DESC" || rc=1 + python3 check_tree.py ctypes "$DESC" || rc=1 + # and the device under ASan/UBSan, over the pipe + g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $mode $INC tree_device.cpp -o tree_device + python3 check_tree.py pipe "$DESC" | sed 's/^OK: python Tree over a pipe/OK: python Tree over a pipe (ASan+UBSan)/'; [ "${PIPESTATUS[0]}" = 0 ] || rc=1 +done +rm -f tree_device tree_device.so describe exit $rc diff --git a/test/link/check_tree.py b/test/link/check_tree.py index 72b707c..3fa319b 100644 --- a/test/link/check_tree.py +++ b/test/link/check_tree.py @@ -4,9 +4,10 @@ import os, struct, sys D = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(D, '..', '..', 'python')) -from onemachine import Tree, StreamLink, CtypesLink, OutOfRange, ReadOnly, UnknownCode, Stale, Reading, Change +from onemachine import Tree, StreamLink, CtypesLink, OutOfRange, ReadOnly, UnknownCode, Stale, Reading, Change, LinkError CTYPES = len(sys.argv) > 1 and sys.argv[1] == 'ctypes' +DESC = sys.argv[2] if len(sys.argv) > 2 else None # where the build wrote the description by hash (examples/spi/describe.cpp) failures = 0 def check(cond, what): global failures @@ -18,7 +19,14 @@ def op(c, payload=b''): def advance(ms): check(op('t', struct.pack(' #include "../support/mockBmpTwi.h" #include "../../examples/spi/src/tree_ops.h" +#include "../../examples/spi/src/air_tree.h" using discover::RowId; using hapi::Chain; @@ -32,7 +33,7 @@ struct Mode { fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; }; struct App; -using M = bmpm::Machine, Mode>; +using M = airTree::Machine; using Drivers = Chain; struct App : discover::World, M::Entries, 3, discover::I2cScan>, fail::BusEdge { static constexpr bool lifecycle = true; @@ -43,30 +44,21 @@ struct App : discover::World, M::Entries, 3, discover using Ticker = fail::Ticks; // the codes, with their numbers; `card` only notifies -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"); }; +using airTree::CodeCard; // the codes and the published nodes: examples/spi/src/air_tree.h using Queue = bmpm::ChangeQueue<8>; template static void note(int32_t v) { Queue::note(Code::num, v); } -using PubTemp = bmpm::PublishedAt, oneData::OnSync<¬e>>; -using PubPress = bmpm::PublishedAt, oneData::OnSync<¬e>>; -using PubAir = bmpm::PublishedAt>; -using PubConfig = bmpm::PublishedAt>; -using PubCtrlMeas = bmpm::PublishedAt>; -using Pubs = Chain; +template struct Note { static void fn(int32_t v) { note(v); } }; +using Pubs = airTree::Pubs; struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of its row using Codes = Chain; static inline uint8_t cardStatus = 0; static uint8_t status(uint8_t) { return cardStatus; } - template static void describe(P& put) { - const char* s = " card -> 0/1 notify event ro value u32 status "; while (*s) put(*s++); - s = cardStatus == 0 ? "alive" : cardStatus == 1 ? "stale" : "gone"; while (*s) put(*s++); + template static constexpr void describe(P& put) { + airTree::cardLine(put); + if constexpr (!bmpm::NoStatus

::value) { const char* s = cardStatus == 0 ? " status alive" : cardStatus == 1 ? " status stale" : " status gone"; while (*s) put(*s++); } put('\n'); } }; From bd863c89b4edb6dfba25a2f1ed85417f018473f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:14:38 +0000 Subject: [PATCH 14/19] Redrawn experiment 2: the hash by a walk of its own; the bare and text 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> 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/describe.cpp | 9 ++-- examples/spi/src/air_tree.h | 15 +++--- examples/spi/src/bmp280_machine.h | 79 ++++++++++++++++++++----------- examples/spi/src/main.cpp | 13 +++-- examples/spi/src/tree_ops.h | 9 ++-- test/link/avr_tree.cpp | 9 ++-- test/link/check_tree.py | 5 ++ test/link/tree_device.cpp | 9 ++-- 8 files changed, 88 insertions(+), 60 deletions(-) diff --git a/examples/spi/describe.cpp b/examples/spi/describe.cpp index 54d7b57..b6b5621 100644 --- a/examples/spi/describe.cpp +++ b/examples/spi/describe.cpp @@ -1,7 +1,7 @@ // 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=

)). // describe writes /.txt (the text without the status) and prints its path -// The hash is FNV-1a over the text, folded at compile time by the same walk the firmware folds (bmpm::Fnv); this program checks the two agree. +// 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 describe.cpp (examples/spi/describe.py does it for PlatformIO). #include #include @@ -9,11 +9,12 @@ struct NoApp {}; // the description needs the machine's types only: no bus, no registry using M = airTree::Machine; -template struct NoNote { static void fn(int32_t) {} }; +static void noNote(int32_t) {} +template struct NoNote { static constexpr auto fn = &noNote; }; using Pubs = airTree::Pubs; -struct Text { static constexpr bool noStatus = true; std::string s; void operator()(char c) { s += c; } }; +struct Text { std::string s; void operator()(char c) { s += c; } }; -template constexpr void describeAll(P& put) { bmpm::describe(put, airTree::bus); airTree::cardLine(put); put('\n'); } +template constexpr void describeAll(P& put) { bmpm::describeStatic(put, airTree::bus); airTree::cardLine(put); } constexpr uint32_t folded() { bmpm::Fnv f; describeAll(f); return f.h; } int main(int argc, char** argv) { diff --git a/examples/spi/src/air_tree.h b/examples/spi/src/air_tree.h index 50a5782..3b86a83 100644 --- a/examples/spi/src/air_tree.h +++ b/examples/spi/src/air_tree.h @@ -2,7 +2,7 @@ // 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) -// Note Note::fn(int32_t): what a published value calls when it changes (the App's: print it, mark it for the link, ...) +// Note Note::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 #include @@ -24,13 +24,16 @@ namespace airTree { // 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 class Note> using Pubs = hapi::Chain< - bmpm::PublishedAt, oneData::OnSync<&Note::fn>>, - bmpm::PublishedAt, oneData::OnSync<&Note::fn>>, + bmpm::PublishedAt, oneData::OnSync::fn>>, + bmpm::PublishedAt, oneData::OnSync::fn>>, bmpm::PublishedAt>, bmpm::PublishedAt>, bmpm::PublishedAt>>; - // the card: a code that only notifies (an event with a value, the UID, 0 when it leaves); the App adds the status of the reader's row - template constexpr void cardLine(P& put) { const char* s = " card -> 0/1 notify event ro value u32"; while (*s) put(*s++); } - + // 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 constexpr void cardLine(P& put) { for (unsigned i = 0; i + 8 < sizeof cardText - 1; ++i) put(cardText[i]); put('\n'); } } diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index 8e8d8cb..a5bad52 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -372,31 +372,54 @@ namespace bmpm { template struct IsGroup : std::false_type {}; template struct IsGroup> : std::true_type {}; - // A put may declare `static constexpr bool folds = true` (it is evaluated at compile time: names are read with at(), not from flash) and - // `static constexpr bool noStatus = true` (the lines leave out the status, which is the device's now, not part of what it is). - template struct Folds : std::false_type {}; - template struct Folds> : std::bool_constant {}; - template struct NoStatus : std::false_type {}; - template struct NoStatus> : std::bool_constant {}; - // FNV-1a over the description's text without the status: the description's hash, at compile time (state::fnv, as the schema hash) - struct Fnv { static constexpr bool folds = true, noStatus = true; uint32_t h = 2166136261u; constexpr void operator()(char c) { h = state::fnv(h, uint8_t(c)); } }; - - inline constexpr char hexDigits[] = "0123456789ABCDEF"; template struct Walk { P& put; - constexpr void str(const char* s) { while (*s) put(*s++); } - constexpr void name(state::Name n) { if constexpr (Folds

::value) { for (unsigned i = 0; n.at(i); ++i) put(n.at(i)); } else state::put_name(put, n); } - constexpr void dec(uint32_t v) { - if constexpr (Folds

::value) { char b[10] = {}; unsigned k = 0; do { b[k++] = char('0' + v % 10); v /= 10; } while (v); while (k) put(b[--k]); } - else state::put_dec(put, v); + void str(const char* s) { while (*s) put(*s++); } + void name(state::Name n) { state::put_name(put, n); } + void dec(uint32_t v) { state::put_dec(put, v); } + void hex(uint8_t v) { static const char d[] = "0123456789ABCDEF"; put('0'); put('x'); put(d[v >> 4]); put(d[v & 15]); } + + // what one node is: its fields, on the rest of the line + template void fields() { + if constexpr (IsGroup::value) { str(" group "); dec(N::Body::size()); } + else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); + if constexpr (HasLimits::value) { str(" range "); dec(uint32_t(N::limLo)); str(".."); dec(uint32_t(N::limHi)); } } + else if constexpr (HasConst::value) { str(" const "); hex(N::constAddr); str(" ["); dec(N::constLen); put(']'); } + else { str(HasSet::value ? " rw" : " ro"); str(" value"); if constexpr (HasDecimals::value) { str(" scaled "); dec(N::decimals); } } } - constexpr void hex(uint8_t v) { - put('0'); put('x'); - if constexpr (Folds

::value) { const uint8_t hi = uint8_t(v >> 4), lo = uint8_t(v & 15); put(char(hi < 10 ? '0' + hi : 'A' + hi - 10)); put(char(lo < 10 ? '0' + lo : 'A' + lo - 10)); } - else { put(hexDigits[v >> 4]); put(hexDigits[v & 15]); } + + template void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } + template void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } + + template void machine() { + str("machine "); name(TagBmp::name()); str(" at "); hex(M::addr); put('\n'); + nodes(static_cast(nullptr)); + regs(static_cast(nullptr)); } - // what one node is: its fields, on the rest of the line + template void published(uint8_t bus) { + using Inner = typename Pub::Inner; + str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(M::addr); + if constexpr (std::is_void::value) { put('/'); dec(unsigned(M::template IndexOf::value)); } + else for (unsigned i = 0; i < Pub::Path::depth; ++i) { put('/'); dec(Pub::Path::path[i]); } + str(NotifiesSync::value ? " notify sync" : " silent"); fields(); + str(" status "); str(statusName(M::status())); put('\n'); + } + static const char* statusName(uint8_t st) { return st == 0 ? "alive" : st == 1 ? "stale" : "gone"; } + template void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } + }; + + // ---- the description's hash: the same lines as Walk, without the status, by a walk of its own that folds at compile time ------------------- + // Walk is the text the device sends (-DONEMACHINE_DESC_TEXT, and the log); HashWalk is what the hash is folded from and what the build output + // holds (examples/spi/describe.cpp prints it). They are two walks so the text build stays what it was; describe.cpp refuses to write a file whose + // text does not hash to the fold, and test/link/check_tree.py checks the text build's description against that file, so they cannot drift apart. + struct Fnv { uint32_t h = 2166136261u; constexpr void operator()(char c) { h = state::fnv(h, uint8_t(c)); } }; + template struct HashWalk { + P& put; + constexpr void str(const char* s) { while (*s) put(*s++); } + constexpr void name(state::Name n) { for (unsigned i = 0; n.at(i); ++i) put(n.at(i)); } + constexpr void dec(uint32_t v) { char b[10] = {}; 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 hi = uint8_t(v >> 4), lo = uint8_t(v & 15); put('0'); put('x'); put(char(hi < 10 ? '0' + hi : 'A' + hi - 10)); put(char(lo < 10 ? '0' + lo : 'A' + lo - 10)); } template constexpr void fields() { if constexpr (IsGroup::value) { str(" group "); dec(N::Body::size()); } else if constexpr (HasReg::value) { str(" reg "); hex(N::regAddr); str(" default "); hex(N::regDef); str(HasSet::value ? " rw" : " ro"); @@ -404,31 +427,31 @@ namespace bmpm { else if constexpr (HasConst::value) { str(" const "); hex(N::constAddr); str(" ["); dec(N::constLen); put(']'); } else { str(HasSet::value ? " rw" : " ro"); str(" value"); if constexpr (HasDecimals::value) { str(" scaled "); dec(N::decimals); } } } - template constexpr void nodes(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(N::label()), fields(), put('\n')), ...); } template constexpr void regs(hapi::Chain*) { unsigned i = 0; ((str(" #"), dec(i++), put(' '), name(R::label()), fields(), put('\n')), ...); } - template constexpr void machine() { str("machine "); name(TagBmp::name()); str(" at "); hex(M::addr); put('\n'); nodes(static_cast(nullptr)); regs(static_cast(nullptr)); } - template constexpr void published(uint8_t bus) { using Inner = typename Pub::Inner; str(" "); name(Pub::PubCode::name()); str(" -> "); dec(bus); put('/'); dec(M::addr); if constexpr (std::is_void::value) { put('/'); dec(unsigned(M::template IndexOf::value)); } else for (unsigned i = 0; i < Pub::Path::depth; ++i) { put('/'); dec(Pub::Path::path[i]); } - str(NotifiesSync::value ? " notify sync" : " silent"); fields(); - if constexpr (!NoStatus

::value) { str(" status "); str(statusName(M::status())); } - put('\n'); + str(NotifiesSync::value ? " notify sync" : " silent"); fields(); put('\n'); } - static constexpr const char* statusName(uint8_t st) { return st == 0 ? "alive" : st == 1 ? "stale" : "gone"; } template constexpr void publishedAll(hapi::Chain*, uint8_t bus) { (published(bus), ...); } }; + template constexpr void describeStatic(P& put, uint8_t bus) { + HashWalk

w{put}; + w.template machine(); + w.str("published\n"); + w.template publishedAll(static_cast(nullptr), bus); + } // M: the machine (its Criteria is the device's identity in the path); Pubs: Chain; bus: the machine's position in the App - template constexpr void describe(P& put, uint8_t bus) { + template void describe(P& put, uint8_t bus) { Walk

w{put}; w.template machine(); w.str("published\n"); diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 43c5cf1..4ff089f 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -203,7 +203,7 @@ template static void say(int32_t v) { bmpm::ChangeQueue<8>::note(Code::num, v); // and for the consumer that reads changes over the link #endif } -template struct Say { static void fn(int32_t v) { say(v); } }; +template struct Say { static constexpr auto fn = &say; }; using Published = airTree::Pubs; // temp, press, air, air/config, air/ctrl_meas (air_tree.h) constexpr uint8_t airBus = airTree::bus; @@ -336,14 +336,13 @@ static void faultTick(uint32_t now) { struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of the reader's row using Codes = Chain; static uint8_t status(uint8_t) { return RfidApp::reg.count > 1 ? uint8_t(RfidApp::reg.status(1)) : 2; } - template static constexpr void describe(P& put) { - airTree::cardLine(put); - if constexpr (!bmpm::NoStatus

::value) { - put(' '); const uint8_t st = status(0); - const char* s = st == 0 ? "status alive" : st == 1 ? "status stale" : "status gone"; while (*s) put(*s++); - } + template static void describe(P& put) { + const char* s = AIRTREE_CARD_TEXT; while (*s) put(*s++); + const uint8_t st = status(0); + s = st == 0 ? "alive" : st == 1 ? "stale" : "gone"; while (*s) put(*s++); put('\n'); } + template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } }; using Ops = bmpm::TreeOps; struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index 8f54261..c32b23e 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -1,8 +1,8 @@ // The link's ops over the published nodes of a machine tree. The framing and the bytes are role/link.h's: this is an App with payload ops // (`payload = true`: role::Link calls describe() for op 'd' and request() for the others). // 'd' the description, by its hash (the default): `hash <8 hex digits>\n` then one digit per code, the status of the row it is bound to now -// (0 alive, 1 stale, 2 gone), then `\n`. The hash is FNV-1a over the description's text without the status, computed at compile time from the -// same walk; the consumer has the text from the build (examples/spi/describe.cpp writes .txt; python: Tree(link, descriptions=dir)). +// (0 alive, 1 stale, 2 gone), then `\n`. The hash is FNV-1a over the description's text without the status, computed at compile time +// (bmpm::HashWalk, Extra::describeStatic); the consumer has the text from the build (examples/spi/describe.cpp writes .txt; python: Tree(link, descriptions=dir)). // With -DONEMACHINE_DESC_TEXT: the text itself, bmpm::describe's walk then the codes that only notify (Extra), each line ending with the // status of its row. Codes are numbered in the order they are listed. // 'v' get by code payload: the code (text) reply: the status (u8), then the value (i32 little-endian) @@ -70,9 +70,8 @@ namespace bmpm { static constexpr bool payload = true; using Queue = ChangeQueue; - // the description's hash: the text without the status, folded at compile time (Extra::describe must be constexpr and leave out its status for a - // put with noStatus) - static constexpr uint32_t hash() { bmpm::Fnv f; bmpm::describe(f, Bus); Extra::describe(f); return f.h; } + // the description's hash: its static text (bmpm::describeStatic, then Extra::describeStatic), folded at compile time + static constexpr uint32_t hash() { bmpm::Fnv f; bmpm::describeStatic(f, Bus); Extra::describeStatic(f); return f.h; } template static void describe(P& put) { #ifdef ONEMACHINE_DESC_TEXT bmpm::describe(put, Bus); Extra::describe(put); diff --git a/test/link/avr_tree.cpp b/test/link/avr_tree.cpp index c1a42b9..cc7346c 100644 --- a/test/link/avr_tree.cpp +++ b/test/link/avr_tree.cpp @@ -58,18 +58,15 @@ template static void note(int32_t v) { Queue::note(Code::num, v); template static void note(int32_t v) { g_sink = v; } #endif -template struct Note { static void fn(int32_t v) { note(v); } }; +template struct Note { static constexpr auto fn = ¬e; }; using Pubs = airTree::Pubs; #ifdef LINK struct Extra { using Codes = Chain; static uint8_t status(uint8_t) { return 0; } - template static constexpr void describe(P& put) { - airTree::cardLine(put); - if constexpr (!bmpm::NoStatus

::value) { const char* s = " status alive"; while (*s) put(*s++); } - put('\n'); - } + template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32 status alive\n"; while (*s) put(*s++); } + template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } }; using Ops = bmpm::TreeOps; struct UartOut { static void put(uint8_t b) { Uart::putch(b); } }; diff --git a/test/link/check_tree.py b/test/link/check_tree.py index 3fa319b..353ef31 100644 --- a/test/link/check_tree.py +++ b/test/link/check_tree.py @@ -27,6 +27,11 @@ def reg(a): return op('R', bytes([a]))[1][0] except LinkError as e: check('-DONEMACHINE_DESC_TEXT' in str(e) and d[5:13].decode() in str(e), 'a readable error: %s' % e) m = Tree(link, descriptions=DESC) if HASHED: check(m.description == open(os.path.join(DESC, d[5:13].decode() + '.txt')).read(), 'the text is the build output\'s') +if not HASHED and DESC: # the two walks: the text the device sends, without its statuses, is the build output's + import re + files = [f for f in os.listdir(DESC) if f.endswith('.txt')] + static = '\n'.join(re.sub(r' status (alive|stale|gone)$', '', l) for l in m.description.split('\n')) + check(len(files) == 1 and static == open(os.path.join(DESC, files[0])).read(), 'the text walk and the hash walk write the same description') check(list(m.codes) == ['temp', 'press', 'air', 'air/config', 'air/ctrl_meas', 'card'], 'the codes, in the device\'s order: %s' % list(m.codes)) check(m.codes['temp'].scaled == 2 and m.codes['temp'].notify == 'sync' and m.codes['temp'].ro, 'temp: scaled 2, notifies, read-only') check(m.codes['air'].kind == 'group' and m.codes['air'].group_size == 2, 'air is a group of 2') diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index 86eb70a..4697ba3 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -49,18 +49,19 @@ using airTree::CodeCard; // the codes and the published nodes: examples/spi/sr using Queue = bmpm::ChangeQueue<8>; template static void note(int32_t v) { Queue::note(Code::num, v); } -template struct Note { static void fn(int32_t v) { note(v); } }; +template struct Note { static constexpr auto fn = ¬e; }; using Pubs = airTree::Pubs; struct Extra { // a code that only notifies: the card (an event with a value, the UID, 0 when it leaves), with the status of its row using Codes = Chain; static inline uint8_t cardStatus = 0; static uint8_t status(uint8_t) { return cardStatus; } - template static constexpr void describe(P& put) { - airTree::cardLine(put); - if constexpr (!bmpm::NoStatus

::value) { const char* s = cardStatus == 0 ? " status alive" : cardStatus == 1 ? " status stale" : " status gone"; while (*s) put(*s++); } + template static void describe(P& put) { + const char* s = AIRTREE_CARD_TEXT; while (*s) put(*s++); + s = cardStatus == 0 ? "alive" : cardStatus == 1 ? "stale" : "gone"; while (*s) put(*s++); put('\n'); } + template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } }; using Ops = bmpm::TreeOps; From 932143b0efabb9620e67566337c0cfebdedf878e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:17:40 +0000 Subject: [PATCH 15/19] Redrawn experiment 3: one Reconcile part on the BMP280's control registers 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/src/bmp280_machine.h | 64 +++++++++++++++---------------- test/discover/bmp_capture.cpp | 9 ++--- test/link/build.sh | 3 ++ 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index 24d7b77..df535e6 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -16,7 +16,7 @@ // // Never touch a node from a static initialiser: discover, set, publish, sync. A machine's nodes are objects with constructors of their own, held in // inline static members of a class template, and those are initialised in no defined order: a static initialiser that runs discovery writes into a -// node before its constructor has run, and the constructor then wipes what was written (the captured registers, the Watch copies). Discover from +// node before its constructor has run, and the constructor then wipes what was written (what the registers want, the Watch copies). Discover from // setup() or main(). // describe() walks the machine and the published nodes and writes what a consumer needs: the codes, their path, their fields, which notify. #pragma once @@ -51,22 +51,21 @@ namespace bmpm { // Via: the PathRef it reaches the node by (void: an ItemRef) template struct PubTag { template struct Part : O { using O::O; using PubCode = Code; using Inner = Node; using Path = Via; }; }; - // ---- capture and restore (D20, D22, D13) ------------------------------------------------------------------------------------- - // The tail of the restore chain: every component that restores calls Base::restore() after its own, and this ends it. - struct RestoreEnd { template struct Part : O { using O::O; void restore() {} void forget() {} }; }; - // Keeps the last set() and replays it on restore(), then calls the tail. A set while the device is gone is captured too: the last intent is what - // comes back. forget() drops it (a part that is not the one that was here). - struct Capture { + // ---- desired and observed: one Reconcile part (OneMachine Redrawn, experiment 3; replaces Capture, its restore chain and the defaults as init) --- + // The register holds what is wanted of it (`want`), from the start: its default (RegAt's regDef, below it in the composition). set() changes what is + // wanted and writes it; a set while the device is gone changes it too: the last intent is what comes back. apply() writes what is wanted (a part + // that came back, validated; the first init); reset() makes the default what is wanted again (another part, D13/D22). The driver's canary compares + // the register it reads back with desired(): a part reset behind the host's back no longer holds it. + struct Reconcile { template struct Part : O { using Base = O; using O::O; using Type = typename Base::Type; - Type last{}; bool have = false; - void set(Type v) { last = v; have = true; Base::set(v); } - void restore() { if (have) Base::set(last); Base::restore(); } - void forget() { have = false; Base::forget(); } - [[nodiscard]] bool known() const { return have; } - [[nodiscard]] Type captured() const { return last; } + Type want = Type(Base::regDef); + void set(Type v) { want = v; Base::set(v); } + void apply() { Base::set(want); } + void reset() { want = Type(Base::regDef); } + [[nodiscard]] Type desired() const { return want; } }; }; @@ -96,7 +95,7 @@ namespace bmpm { inline static Cal cal{}; // device constants inline static uint32_t ident = 0; // the part that was here: a hash of its chip id and calibration inline static bool known = false; // (ident is set) - inline static uint16_t restored = 0, defaulted = 0; // how often it came back and its captured state was replayed / the defaults were used + inline static uint16_t restored = 0, defaulted = 0; // how often it came back and what was wanted was applied again / the defaults were used static void rdN(uint8_t reg, uint8_t* out, uint8_t n) { using Twi = typename W::Twi; @@ -119,7 +118,7 @@ namespace bmpm { // a register mimic: get() reads the chip, set() writes it template struct RegSrc { static uint8_t get() { return Dev::rd(A); } static void set(uint8_t v) { Dev::wr(A, v); } }; - template using Reg = ItemDef>, RegAt, Limits<0, 255>, Label, RestoreEnd>; + template using Reg = ItemDef>, RegAt, Limits<0, 255>, Label>; using Temp = ItemDef, Label, oneData::ReadOnly>>>; using Press = ItemDef, Label, oneData::ReadOnly>>>; @@ -165,12 +164,11 @@ namespace bmpm { // ---- the registers of the control group, in order ------------------------------------------------------------------------- template static void eachReg(F&& fn) { for (uint8_t i = 0; i < Ctrl::Body::size(); ++i) ctrl.body.visit(i, fn); } - // ---- restore: capture replayed in composition order, and the defaults as the init (D13, D20, D22, D33) ----------------------- - // restore(): each register writes its captured value, then calls the tail. forget(): the captured values are dropped (not the part that was here). - // restoreDefaults(): each register is set to its default, which captures it: restore from defaults is the device init. - static void restore() { eachReg([](auto& r) { r.restore(); }); } - static void forget() { eachReg([](auto& r) { r.forget(); }); } - static void restoreDefaults() { eachReg([](auto& r) { using R = std::remove_reference_t; r.set(R::regDef); }); } + // ---- what is wanted of the registers, in order (D13, D22: the defaults are the init) --------------------------------------------- + // apply(): each register gets what is wanted of it. reset(): the defaults are what is wanted (another part). restoreDefaults(): both. + static void apply() { eachReg([](auto& r) { r.apply(); }); } + static void reset() { eachReg([](auto& r) { r.reset(); }); } + static void restoreDefaults() { reset(); apply(); } // ---- which part is it: a hash of its chip id and its calibration (a replaced part has its own) ------------------------------- static uint32_t identOf(uint8_t id, const uint8_t* c) { @@ -187,7 +185,7 @@ namespace bmpm { } // The device is found, or is back (after it was gone, or lost its configuration). Validate: the same chip id and calibration as the part that was - // here. Validated, the captured state is replayed; not validated (a different part, or the first time), the slot is dropped and the defaults are + // here. Validated, what was wanted is applied again; not validated (a different part, or the first time), the slot is dropped and the defaults are // the init. The calibration is device data, read again every time. enum class How : uint8_t { Defaults, Restored }; static How bring() { @@ -195,14 +193,12 @@ namespace bmpm { bool ok = false; for (uint8_t i = 0; i < 5 && !(ok = Dev::readCal(c)); ++i) {} const uint32_t h = ok ? identOf(Dev::rd(0xD0), c) : 0; - if (ok && Dev::known && h == Dev::ident) { - setCal(c); restore(); ++Dev::restored; - return How::Restored; - } - forget(); + const bool same = ok && Dev::known && h == Dev::ident; if (ok) setCal(c); else Dev::cal = Cal{}; // no calibration: poll() produces nothing - Dev::ident = h; Dev::known = ok; - restoreDefaults(); ++Dev::defaulted; + if (!same) { reset(); Dev::ident = h; Dev::known = ok; } + apply(); + if (same) { ++Dev::restored; return How::Restored; } + ++Dev::defaulted; return How::Defaults; } @@ -257,7 +253,7 @@ namespace bmpm { } // ---- discovery: the machine is the driver's one device ---------------------------------------------------------------- - // Under a failure edge (Mode::checked) each poll first reads the control registers back: a register that no longer holds what was captured + // Under a failure edge (Mode::checked) each poll first reads the control registers back: a register that no longer holds what is wanted of it (desired()) // means the part was reset without the host knowing (Corrupt: Recover calls reinit(), which validates it and replays the capture). A part that // stops answering is Absent, retried and probed; when it answers again the edge calls reinit() the same way. struct Driver : discover::DriverBase, fail::DevEdge { @@ -280,7 +276,7 @@ namespace bmpm { if (!o.isOk() || lost) return; uint8_t v = 0; o = Edge::checkedRead(row, R::regAddr, &v, 1); - if (o.isOk() && r.known() && v != r.captured()) { lost = true; which = R::regAddr; } + if (o.isOk() && v != r.desired()) { lost = true; which = R::regAddr; } }); if (!o.isOk()) return o; if (lost) return fail::Outcome::Fail(fail::Kind::Corrupt, which); @@ -312,8 +308,8 @@ namespace bmpm { template using Published = ItemDef, Label, PubTag>; - template struct HasCaptured : std::false_type {}; - template struct HasCaptured().captured())>> : std::true_type {}; + template struct HasDesired : std::false_type {}; + template struct HasDesired().desired())>> : std::true_type {}; // a node of a machine by compile-time path: P is the position in the machine, then in each group below it. It reaches any node, a leaf of // a group too, by resolving the path to the node's singleton (or to the child inside a group) on use. get(), changed() and sync() are the node's. @@ -329,7 +325,7 @@ namespace bmpm { static Node& ref() { return M::template resolve(); } static decltype(auto) get() { return ref().get(); } // the value of a part that is not Alive: a register's last set (the intent that comes back), otherwise the last value read - static decltype(auto) last() { if constexpr (HasCaptured::value) return ref().captured(); else return ref().get(); } + static decltype(auto) last() { if constexpr (HasDesired::value) return ref().desired(); else return ref().get(); } [[nodiscard]] static bool changed() { return ref().changed(); } static void sync() { ref().sync(); } template static void set(V&& v) { ref().set(std::forward(v)); } // through the node: its limits, its capture, its register diff --git a/test/discover/bmp_capture.cpp b/test/discover/bmp_capture.cpp index 94d8e55..96023ec 100644 --- a/test/discover/bmp_capture.cpp +++ b/test/discover/bmp_capture.cpp @@ -1,4 +1,4 @@ -// Capture and restore on the BMP280 machine, under a failure edge (fail::DevEdge over the I2C bus edge), against a simulated chip (mockBmpTwi.h). +// Capture and restore on the BMP280 machine (one Reconcile part per register: what is wanted of it, compared with what it reads back), under a failure edge (fail::DevEdge over the I2C bus edge), against a simulated chip (mockBmpTwi.h). // - found: the defaults are the init and are captured; the slot (the row, the machine's statics) is kept while the part is gone // - a set is captured; the part is reset without the host knowing (soft reset): the canary sees a register that no longer holds the capture, // Recover calls reinit(), the part is validated (same chip id and calibration) and the capture is replayed @@ -62,8 +62,7 @@ static void start() { now = 0; leftAlive = false; App::discover(); } -static uint8_t cap(unsigned i) { uint8_t v = 0; M::visitReg(uint8_t(i), [&](auto& r) { v = r.captured(); }); return v; } -static bool known(unsigned i) { bool k = false; M::visitReg(uint8_t(i), [&](auto& r) { k = r.known(); }); return k; } +static uint8_t cap(unsigned i) { uint8_t v = 0; M::visitReg(uint8_t(i), [&](auto& r) { v = r.desired(); }); return v; } // what the register wants (Reconcile) int main() { using mockbmp::State; @@ -74,7 +73,7 @@ int main() { CHECK(App::reg.count == 2 && App::reg.status(1) == Status::Alive); CHECK(M::Dev::defaulted == 1 && M::Dev::restored == 0); CHECK(c.regs[0xF5] == 0x90 && c.regs[0xF4] == 0x57); - CHECK(known(0) && known(1) && cap(0) == 0x90 && cap(1) == 0x57); + CHECK(cap(0) == 0x90 && cap(1) == 0x57); CHECK(M::Dev::known); advance(500); CHECK(M::Dev::temp == 2508 && M::Dev::press == 100653); @@ -123,7 +122,7 @@ int main() { CHECK(runUntil(Status::Alive, 3000)); advance(200); CHECK(c.regs[0xF4] == 0x57 && c.regs[0xF5] == 0x90); // the defaults, not 0x2B - CHECK(cap(1) == 0x57 && known(1)); // the old capture is gone; the defaults are captured + CHECK(cap(1) == 0x57); // the old intent is gone; the defaults are wanted CHECK(M::Dev::defaulted == 2 && M::Dev::restored == 2 && M::Dev::ident != before); CHECK(M::Dev::cal.T2 == 0x7043); // its calibration, read again diff --git a/test/link/build.sh b/test/link/build.sh index 3acbe6d..94c5bff 100755 --- a/test/link/build.sh +++ b/test/link/build.sh @@ -9,5 +9,8 @@ g++ -std=c++17 -O1 -Wall $INC tree_device.cpp -o tree_device g++ -std=c++17 -O1 -Wall -shared -fPIC -DSIM_LIB $INC tree_device.cpp -o tree_device.so python3 check_tree.py pipe || rc=1 python3 check_tree.py ctypes || rc=1 +# and the device under ASan/UBSan, over the pipe +g++ -std=c++17 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all $INC tree_device.cpp -o tree_device +python3 check_tree.py pipe | sed 's/^OK: python Tree over a pipe$/OK: python Tree over a pipe (ASan+UBSan)/' ; [ "${PIPESTATUS[0]}" = 0 ] || rc=1 rm -f tree_device tree_device.so exit $rc From 06ba14df35c2aa3e6042fc63bbf611bc681eb153 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:30:10 +0000 Subject: [PATCH 16/19] Redrawn experiment 4: the rig's wiring spec, a dry-run composition, and 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/wiring/rig.toml | 39 ++++++ examples/spi/wiring/rig_irq_d4.toml | 39 ++++++ python/onemachine/wiring.py | 184 ++++++++++++++++++++++++++++ test/discover/build_spi1.sh | 4 + test/wiring/build.sh | 9 ++ test/wiring/check_wiring.py | 90 ++++++++++++++ 6 files changed, 365 insertions(+) create mode 100644 examples/spi/wiring/rig.toml create mode 100644 examples/spi/wiring/rig_irq_d4.toml create mode 100644 python/onemachine/wiring.py create mode 100755 test/wiring/build.sh create mode 100644 test/wiring/check_wiring.py diff --git a/examples/spi/wiring/rig.toml b/examples/spi/wiring/rig.toml new file mode 100644 index 0000000..877a797 --- /dev/null +++ b/examples/spi/wiring/rig.toml @@ -0,0 +1,39 @@ +# The rig's wiring (examples/spi), written by hand: the parts, their pins, and the codes the device publishes, by the same names. +# python3 -m onemachine.wiring check|emit|diff examples/spi/wiring/rig.toml (python/onemachine/wiring.py) +# TOML, not the essay's YAML: Python reads it with the standard library (tomllib). +board = "wemos-d1-mini" # its pins: OneChip's chips/esp8266/esp8266Device.h (D0..D8 and what each one is) + +[i2c] +sda = "D2" +scl = "D1" +hz = 100000 + +[spi] +sck = "D5" +miso = "D6" +mosi = "D7" +hz = 4000000 +empty_cs = ["D3"] # a chip select declared with nothing on it: the scan reports the slot empty + +# The App's machines, in order: a code's path starts with its machine's position (rfid 0, air 1). +[parts.rfid] +driver = "rc522" +bus = "spi" +cs = "D8" +rst = "D4" # held high by the sketch; low resets the reader +irq = { pin = "D0", mode = "push-pull", delivery = "sampled" } + +[parts.air] +driver = "bmp280" +bus = "i2c" +addr = 0x76 +accept = [0x58, 0x60] # BMP280, BME280 + +# What the App publishes: code = "part/node", in the order the device numbers them +[publish] +temp = "air/temp" +press = "air/press" +air = "air/ctrl" +"air/config" = "air/ctrl/config" +"air/ctrl_meas" = "air/ctrl/ctrl_meas" +card = "rfid/card" diff --git a/examples/spi/wiring/rig_irq_d4.toml b/examples/spi/wiring/rig_irq_d4.toml new file mode 100644 index 0000000..b326829 --- /dev/null +++ b/examples/spi/wiring/rig_irq_d4.toml @@ -0,0 +1,39 @@ +# The rig with the RC522's IRQ on D4 (GPIO2, a boot strap pin): it must not build. RST moves to D3, the empty slot goes. +# python3 -m onemachine.wiring check examples/spi/wiring/rig_irq_d4.toml +# TOML, not the essay's YAML: Python reads it with the standard library (tomllib). +board = "wemos-d1-mini" # its pins: OneChip's chips/esp8266/esp8266Device.h (D0..D8 and what each one is) + +[i2c] +sda = "D2" +scl = "D1" +hz = 100000 + +[spi] +sck = "D5" +miso = "D6" +mosi = "D7" +hz = 4000000 +empty_cs = [] + +# The App's machines, in order: a code's path starts with its machine's position (rfid 0, air 1). +[parts.rfid] +driver = "rc522" +bus = "spi" +cs = "D8" +rst = "D3" # moved off D4 for the IRQ +irq = { pin = "D4", mode = "push-pull", delivery = "sampled" } + +[parts.air] +driver = "bmp280" +bus = "i2c" +addr = 0x76 +accept = [0x58, 0x60] # BMP280, BME280 + +# What the App publishes: code = "part/node", in the order the device numbers them +[publish] +temp = "air/temp" +press = "air/press" +air = "air/ctrl" +"air/config" = "air/ctrl/config" +"air/ctrl_meas" = "air/ctrl/ctrl_meas" +card = "rfid/card" diff --git a/python/onemachine/wiring.py b/python/onemachine/wiring.py new file mode 100644 index 0000000..92f2f05 --- /dev/null +++ b/python/onemachine/wiring.py @@ -0,0 +1,184 @@ +"""A wiring spec in, the App's composition out, and the device's own description as the test that the build matched the wiring +(OneMachine Redrawn, experiment 4: a dry run). + + python3 -m onemachine.wiring check examples/spi/wiring/rig.toml the wiring's rules; exit 1 with the reasons + python3 -m onemachine.wiring emit examples/spi/wiring/rig.toml the App composition (C++), after check + python3 -m onemachine.wiring diff examples/spi/wiring/rig.toml desc.txt the spec against a device's description ('d' as text) + +The spec is TOML (tomllib, no dependency): board, [i2c], [spi], [parts.] in the App's machine order, [publish] code = "part/node". +The board's pins come from OneChip (chips/esp8266/esp8266Device.h): each D pin's GPIO, and what its comment says of it ("boot strapping", +"no interrupt"). What a driver needs and publishes (its manifest) is DRIVERS below: a stand-in for a declaration that belongs beside each driver. + +The rules, each with the sentence a person needs: + - every pin is on the board and used once + - a line the device drives at any time (an IRQ) is not on a boot strap pin: the RC522 keeps a pending IRQ across a reset of the board, holds the + pin low, and the ESP8266 boots into ROM (download) mode. The C++ rejects it too (irq::Esp8266Line's static_assert), at the next build. + - an ISR delivery is not on a pin without an interrupt (GPIO16): it is sampled there + - a part is on the bus its driver speaks; at an address it can have; the ids it accepts are ids its driver knows + - every published code names a node its part has +""" +import os, re, sys, tomllib +from .tree import _parse + +HERE = os.path.dirname(os.path.abspath(__file__)) +ONECHIP = os.environ.get('ONECHIP', os.path.normpath(os.path.join(HERE, '..', '..', '..', 'OneChip'))) + +BOARDS = {'wemos-d1-mini': ('chips/esp8266/esp8266Device.h', 'Esp8266Dev')} + +class WiringError(Exception): + def __init__(self, errors): super().__init__('\n'.join(errors)); self.errors = errors + +class Board: + """D pins of a board, from OneChip: gpio, and the notes its comment gives (strap: sampled at reset; noirq: no interrupt).""" + def __init__(self, name): + if name not in BOARDS: raise WiringError(['board %r: not known (known: %s)' % (name, ', '.join(BOARDS))]) + path = os.path.join(ONECHIP, 'include', BOARDS[name][0]) + self.pins = {} + for m in re.finditer(r'static constexpr uint8_t (D\d+)\s*=\s*(\d+);\s*//(.*)', open(path).read()): + note = m.group(3).lower() + self.pins[m.group(1)] = dict(gpio=int(m.group(2)), strap='boot strapping' in note, noirq='no interrupt' in note) + self.source = path + def gpio(self, pin): return self.pins[pin]['gpio'] + def straps(self): return [p for p, v in self.pins.items() if v['strap']] + +# What each driver needs and publishes. node: its path in the machine (positions, as PathRef) and its kind, and for a value its decimals and +# whether it notifies. A stand-in: the declaration belongs beside the driver (the identify entries, OneChip's pin roles, the machine's Nodes). +DRIVERS = { + 'bmp280': dict(bus='i2c', addrs=(0x76, 0x77), ids=(0x58, 0x60), + nodes={'temp': ((0,), 'value', 2), 'press': ((1,), 'value', 2), 'ctrl': ((3,), 'group', None), + 'ctrl/config': ((3, 0), 'reg', None), 'ctrl/ctrl_meas': ((3, 1), 'reg', None)}), + 'rc522': dict(bus='spi', pins={'cs': 'out', 'rst': 'out', 'irq': 'driven'}, + nodes={'card': (None, 'event', None)}), +} + +def load(path): + with open(path, 'rb') as f: spec = tomllib.load(f) + spec['_path'] = path + return spec + +def _pin(errors, board, owner, pin): + if pin not in board.pins: errors.append('%s on %s: the %s has no such pin (it has %s)' % (owner, pin, 'board', ', '.join(board.pins))); return None + return board.pins[pin] + +def check(spec): + """The wiring's rules: a list of sentences, empty when it is sound.""" + errors, used = [], {} + board = Board(spec.get('board', '?')) + def use(owner, pin): + if _pin(errors, board, owner, pin) is None: return + if pin in used: errors.append('%s on %s: %s is on it already' % (owner, pin, used[pin])) + else: used[pin] = owner + for bus, roles in (('i2c', ('sda', 'scl')), ('spi', ('sck', 'miso', 'mosi'))): + for r in roles: + if r in spec.get(bus, {}): use('%s.%s' % (bus, r), spec[bus][r]) + for p in spec.get('spi', {}).get('empty_cs', []): use('spi.empty_cs', p) + for name, part in spec.get('parts', {}).items(): + drv = DRIVERS.get(part.get('driver')) + if drv is None: errors.append('%s: no driver %r (known: %s)' % (name, part.get('driver'), ', '.join(DRIVERS))); continue + if part.get('bus') != drv['bus']: errors.append('%s: a %s is on %s, not %s' % (name, part['driver'], drv['bus'], part.get('bus'))) + if 'addr' in part and part['addr'] not in drv.get('addrs', ()): + errors.append('%s: a %s answers at %s, not 0x%02X' % (name, part['driver'], ' or '.join('0x%02X' % a for a in drv['addrs']), part['addr'])) + for i in part.get('accept', []): + if i not in drv.get('ids', ()): errors.append('%s: accepts id 0x%02X, which its driver does not know (it knows %s)' % (name, i, ', '.join('0x%02X' % x for x in drv['ids']))) + for role, kind in drv.get('pins', {}).items(): + v = part.get(role) + if v is None: errors.append('%s: its %s pin is not given' % (name, role)); continue + pin = v['pin'] if isinstance(v, dict) else v + use('%s.%s' % (name, role), pin) + info = board.pins.get(pin) + if info is None: continue + if kind == 'driven' and info['strap']: + st = ['%s (GPIO%d)' % (p, board.gpio(p)) for p in board.straps()] + straps = ', '.join(st[:-1]) + ' or ' + st[-1] if len(st) > 1 else ''.join(st) + errors.append('%s.%s on %s (GPIO%d): a boot strap pin. The %s keeps a pending IRQ across a reset of the board and holds the pin low, ' + 'and the ESP8266 then boots into ROM (download) mode instead of the sketch. Put the IRQ on a pin that is not %s; ' + 'the rig uses D0 with delivery = "sampled".' % (name, role, pin, info['gpio'], part['driver'].upper(), straps)) + if kind == 'driven' and isinstance(v, dict) and v.get('delivery') == 'isr' and info['noirq']: + errors.append('%s.%s on %s (GPIO%d): this pin has no interrupt; use delivery = "sampled"' % (name, role, pin, info['gpio'])) + for code, where in spec.get('publish', {}).items(): + part, _, node = where.partition('/') + p = spec.get('parts', {}).get(part) + if p is None: errors.append('publish %s = %r: no part %r' % (code, where, part)); continue + drv = DRIVERS.get(p.get('driver'), {}) + if node not in drv.get('nodes', {}): errors.append('publish %s = %r: a %s has no node %r (it has %s)' % (code, where, p.get('driver'), node, ', '.join(drv.get('nodes', {})))) + return errors + +def _code_tag(code): return 'Code' + ''.join(w[:1].upper() + w[1:] for w in re.split(r'_', code.split('/')[-1])) # air/ctrl_meas: CodeCtrlMeas + +def emit(spec, checked=True): + """The App's composition for examples/spi, as C++ lines (a dry run: the sketch is not built from it). Raises WiringError when check() fails; + checked=False emits anyway (to show that the C++ rules reject the same wiring).""" + errors = check(spec) if checked else [] + if errors: raise WiringError(errors) + board, parts = Board(spec['board']), spec['parts'] + g = board.gpio + out = ['// generated from %s by python3 -m onemachine.wiring emit: the App composition of examples/spi (dry run)' % os.path.basename(spec['_path'])] + i2c, spi = spec['i2c'], spec['spi'] + out.append('using Twi = esp::Esp8266TwiMaster<%d, %d, %d>;' % (g(i2c['sda']), g(i2c['scl']), i2c.get('hz', 100000))) + cs = [p['cs'] for p in parts.values() if p['bus'] == 'spi'] + spi.get('empty_cs', []) + out.append('using Spi = hapi::APIOf, oneBus::SpiMaster<%d>, esp::Esp8266SpiCore>;' % + (', '.join('esp::OutPin<%d>' % g(p) for p in cs), spi.get('hz', 4000000))) + for name, p in parts.items(): + if p['driver'] == 'rc522': + out.append('constexpr uint8_t rstPin = %d;' % g(p['rst'])) + irq = p['irq'] + out.append('using RfidLine = irq::%s<%d>;' % ('Sampled' if irq.get('delivery', 'sampled') == 'sampled' else 'IsrFlag', g(irq['pin']))) + if p['driver'] == 'bmp280': + out.append('using Bmp = bmpm::Machine, AirMode>;' % p['addr']) + for num, (code, where) in enumerate(spec['publish'].items()): + out.append('struct %s { static constexpr uint8_t num = %d; ONEMACHINE_STATE_NAME(name, "%s"); };' % (_code_tag(code), num, code)) + for code, where in spec['publish'].items(): + part, _, node = where.partition('/') + path, kind, dec = DRIVERS[parts[part]['driver']]['nodes'][node] + if path is None: continue # an event: the App's Extra, not a published node + notify = ', oneData::OnSync<&say<%s, %d>>' % (_code_tag(code), dec) if kind == 'value' else '' + out.append('bmpm::PublishedAt<%s, bmpm::PathRef%s>' % (_code_tag(code), ', '.join(map(str, path)), notify)) + return out + +def diff(spec, description): + """The spec against the device's description ('d' as text): a list of mismatches, empty when the device is what was wired.""" + errors = check(spec) + if errors: raise WiringError(errors) + parts, names = spec['parts'], list(spec['parts']) + got = _parse(description) + want = list(spec['publish'].items()) + out = [] + if [c.name for c in got] != [c for c, _ in want]: + out.append('codes: the spec publishes %s, the device %s (a code\'s number is its position)' % ([c for c, _ in want], [c.name for c in got])) + dev = {c.name: c for c in got} + for code, where in want: + c = dev.get(code) + if c is None: continue + part, _, node = where.partition('/') + p = parts[part]; drv = DRIVERS[p['driver']] + path, kind, dec = drv['nodes'][node] + machine = names.index(part) + if path is None: expect = [machine, 1 + [q['cs'] for q in parts.values() if q['bus'] == 'spi'].index(p['cs'])] # an event: / + else: expect = [machine, p['addr']] + list(path) + if c.path != expect: + out.append('%s: the device has it at %s, the spec at %s (%s)' % (code, '/'.join(map(str, c.path)), '/'.join(map(str, expect)), + 'machine/address/node' if path is not None else 'machine/row')) + dkind = 'event' if c.notify == 'event' else c.kind + if dkind != kind: out.append('%s: the device has a %s, the spec\'s %s/%s is a %s' % (code, dkind, p['driver'], node, kind)) + if kind == 'value' and dec is not None and c.scaled != dec: out.append('%s: scaled %d on the device, %d in the spec' % (code, c.scaled, dec)) + return out + +def main(argv): + if len(argv) < 3 or argv[1] not in ('check', 'emit', 'diff'): print(__doc__); return 2 + spec = load(argv[2]) + try: + if argv[1] == 'check': + errors = check(spec) + for e in errors: print('error: ' + e) + if not errors: print('ok: %s' % argv[2]) + return 1 if errors else 0 + if argv[1] == 'emit': print('\n'.join(emit(spec))); return 0 + mism = diff(spec, open(argv[3]).read()) + for m in mism: print('mismatch: ' + m) + if not mism: print('ok: the device is what %s wires' % argv[2]) + return 1 if mism else 0 + except WiringError as e: + for x in e.errors: print('error: ' + x) + return 1 + +if __name__ == '__main__': sys.exit(main(sys.argv)) diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index 37124a5..14fe6d1 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -5,6 +5,7 @@ # bmp : the BMP280 as a machine of ItemDef nodes (bmp_machine.cpp, against a simulated chip with the datasheet's worked example), and its # capture and restore under a failure edge: reset behind the host's back, unplugged, replaced (bmp_capture.cpp). # link : the Python consumer of the machine tree (python/onemachine/tree.py) against the air sensor on a simulated chip (test/link/build.sh). +# wiring : the rig's wiring spec checked, emitted as the App composition and diffed against the device's description (test/wiring/build.sh). # irq : the RC522's interrupt part (spi_irq.cpp) and the ESP8266 delivery components' pin rules (irq_delivery.cpp). # AVR : avr-g++ -Os atmega328p, linked over the real AVR SPI core -- it builds, and its size. # Exits non-zero if an assertion fails, a rule does not fire, or the AVR image does not build. @@ -101,6 +102,9 @@ done echo; echo "=== the Python consumer of the machine tree (test/link): over a pipe and in-process (ctypes) ===" if command -v python3 >/dev/null; then bash ../link/build.sh 2>&1 | grep -E "^OK|^FAIL|Error|Traceback" || rc=1; else echo "(python3 not found: skipped)"; fi +echo; echo "=== the wiring spec dry run (test/wiring): the rig's spec, its composition, the device's description; the IRQ on D4 refused ===" +if python3 -c "import tomllib" 2>/dev/null; then bash ../wiring/build.sh > "$OUT/wiring.txt" 2>&1 || rc=1; grep -E "^OK|^FAIL|^emitted|error:|C\+\+ build|Traceback" "$OUT/wiring.txt"; else echo "(python3 3.11+ not found: skipped)"; fi + if command -v avr-g++ >/dev/null; then echo; echo "=== avr-g++ $(avr-g++ -dumpversion) -Os atmega328p (linked, real SPI core) ===" AVRFL="-std=gnu++17 -Os -mmcu=atmega328p -DF_CPU=16000000UL -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -Wall -Wextra" diff --git a/test/wiring/build.sh b/test/wiring/build.sh new file mode 100755 index 0000000..f4772d3 --- /dev/null +++ b/test/wiring/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# The wiring spec dry run (OneMachine Redrawn, experiment 4): test/wiring/check_wiring.py against the simulated device (test/link/tree_device.cpp). +# Needs python3 (3.11+: tomllib). +set -e +cd "$(dirname "$0")" +INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" +OUT=$(mktemp -d); trap 'rm -rf "$OUT"' EXIT +g++ -std=c++17 -O1 $INC ../link/tree_device.cpp -o "$OUT/tree_device" +python3 check_wiring.py "$OUT/tree_device" ../support/arduino_stub diff --git a/test/wiring/check_wiring.py b/test/wiring/check_wiring.py new file mode 100644 index 0000000..40d522b --- /dev/null +++ b/test/wiring/check_wiring.py @@ -0,0 +1,90 @@ +"""The wiring spec dry run (python/onemachine/wiring.py): the rig's spec (examples/spi/wiring/rig.toml) is sound, its emitted composition is the +sketch's (examples/spi/src/main.cpp), and the device's own description ('d' from the simulated device, test/link/tree_device.cpp) is what it wires. +A spec with the RC522's IRQ on D4 fails with a sentence, and the C++ rejects the same wiring (irq::Esp8266Line). Specs that differ from the +device by an address, the machines' order or the codes' order are told by the diff. + python3 check_wiring.py """ +import os, re, struct, subprocess, sys, tempfile +D = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.normpath(os.path.join(D, '..', '..')) +sys.path.insert(0, os.path.join(ROOT, 'python')) +from onemachine import StreamLink +from onemachine import wiring + +failures = 0 +def check(cond, what): + global failures + if not cond: failures += 1; print('FAIL:', what) + +RIG = os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig.toml') +spec = wiring.load(RIG) + +# ---- the rig's spec is sound, and its composition is the one written by hand in the sketch --------------------------------------------------- +check(wiring.check(spec) == [], 'rig.toml: %r' % wiring.check(spec)) +board = wiring.Board('wemos-d1-mini') +check(board.gpio('D4') == 2 and board.pins['D4']['strap'] and board.pins['D0']['noirq'] and board.straps() == ['D3', 'D4', 'D8'], + 'the board from OneChip: D4 is GPIO2, a strap; D0 has no interrupt; the straps are D3 D4 D8: %r' % board.straps()) +main = re.sub(r'//[^\n]*', '', open(os.path.join(ROOT, 'examples', 'spi', 'src', 'main.cpp')).read()) +flat = re.sub(r'\s+', '', main) +lines = wiring.emit(spec)[1:] +missing = [] +for l in lines: + frag = l.split('=', 1)[1].rstrip(';') if l.startswith('using ') else l # a `using X = ...`: what it is, whatever the sketch calls it + if re.sub(r'\s+', '', frag) not in flat: missing.append(l) +check(missing == [], 'every emitted line is in the sketch: missing %r' % missing) +print('emitted %d lines, all in examples/spi/src/main.cpp' % len(lines)) + +# ---- the device describes what the spec wires -------------------------------------------------------------------------------------------- +link = StreamLink.popen([sys.argv[1]]) +st, data = link.call('d', b'') +desc = data.decode() +check(st == 0 and wiring.diff(spec, desc) == [], 'diff against the device: %r' % wiring.diff(spec, desc)) + +def variant(text): + f = tempfile.NamedTemporaryFile('w', suffix='.toml', delete=False); f.write(text); f.close() + s = wiring.load(f.name); os.unlink(f.name); return s +raw = open(RIG).read() + +# another address: a sound wiring, but not this device's +s77 = variant(raw.replace('addr = 0x76', 'addr = 0x77')) +check(wiring.check(s77) == [], '0x77 is an address a BMP280 can have') +d = wiring.diff(s77, desc) +check(len(d) == 5 and all('1/118/' in m and '1/119/' in m for m in d), 'the device is at 0x76: every air code differs, by its path: %r' % d) + +# the machines in another order (air first): the path's first number is the machine's position in the App +a, b = raw.index('[parts.rfid]'), raw.index('[parts.air]'); c = raw.index('# What the App publishes') +order = variant(raw[:a] + raw[b:c] + raw[a:b] + raw[c:]) +d = wiring.diff(order, desc) +check(any(m.startswith('temp: the device has it at 1/118/0, the spec at 0/118/0') for m in d) and any(m.startswith('card: the device has it at 0/1, the spec at 1/1') for m in d), + 'machines in another order: the paths differ: %r' % d) + +# the codes in another order: a code's number is its position +swapped = variant(raw.replace('temp = "air/temp"\npress = "air/press"', 'press = "air/press"\ntemp = "air/temp"')) +d = wiring.diff(swapped, desc) +check(len(d) == 1 and d[0].startswith("codes:"), 'codes in another order: %r' % d) + +# a pin used twice, a driver that cannot be there, a node that does not exist +bad = variant(raw.replace('scl = "D1"', 'scl = "D2"').replace('addr = 0x76', 'addr = 0x40').replace('"air/ctrl/config"', '"air/ctrl/cfg"')) +e = wiring.check(bad) +check(any('i2c.scl on D2: i2c.sda is on it already' in x for x in e) and any('answers at 0x76 or 0x77, not 0x40' in x for x in e) + and any("has no node 'ctrl/cfg'" in x for x in e), 'three faults, three sentences: %r' % e) + +# ---- the RC522's IRQ on D4: the spec is refused with the reason, and the C++ refuses the same wiring ------------------------------------------ +d4 = wiring.load(os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig_irq_d4.toml')) +e = wiring.check(d4) +check(len(e) == 1 and e[0].startswith('rfid.irq on D4 (GPIO2): a boot strap pin') and 'ROM (download) mode' in e[0], 'D4: %r' % e) +print('rig_irq_d4.toml: error: ' + (e[0] if e else '(none)')) +try: wiring.emit(d4); check(False, 'emit refuses what check refuses') +except wiring.WiringError as x: check(x.errors == e, 'emit says the same') +irqline = [l for l in wiring.emit(d4, checked=False) if l.startswith('using RfidLine')] +check(irqline == ['using RfidLine = irq::Sampled<2>;'], 'emitted anyway: %r' % irqline) +with tempfile.TemporaryDirectory() as t: + src = os.path.join(t, 'irq_d4.cpp') + open(src, 'w').write('#include \n#include "irq_esp8266.h"\n%s\nint main() { RfidLine::begin(); return RfidLine::ready(); }\n' % irqline[0]) + r = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17', '-fsyntax-only', '-I', sys.argv[2], '-I', os.path.join(ROOT, 'examples', 'spi', 'src'), src], + capture_output=True, text=True) + msg = 'an interrupt line on GPIO0, GPIO2 or GPIO15 (D3, D4, D8)' + check(r.returncode != 0 and msg in r.stderr, 'the C++ rejects it with irq::Esp8266Line\'s message: %s' % r.stderr[-300:]) + print('and the C++ build: %s' % (msg if msg in r.stderr else '(not rejected)')) + +print('FAILED: %d' % failures if failures else 'OK: wiring spec dry run') +sys.exit(1 if failures else 0) From fcfaa7107c7904030bb9ea238e5298d0b0eb1c5d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:38:12 +0000 Subject: [PATCH 17/19] Redrawn experiment 5: code numbers in v and w, valid only under the description'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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/README.md | 2 +- examples/spi/src/main.cpp | 11 +++++++++++ examples/spi/src/tree_ops.h | 32 +++++++++++++++++++++++++++++--- python/onemachine/tree.py | 17 ++++++++++++----- test/link/check_tree.py | 15 +++++++++++---- 5 files changed, 64 insertions(+), 13 deletions(-) diff --git a/examples/spi/README.md b/examples/spi/README.md index e0a9de9..dde1c56 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -73,7 +73,7 @@ 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 by its hash and each code's status (the text is in the build output: `.pio/build//description/.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 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 +Ops: `d` the description by its hash and each code's status (the text is in the build output: `.pio/build//description/.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 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. diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 4ff089f..c065cd3 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -55,6 +55,13 @@ // What the sketch says about itself goes to Log: the serial monitor. In the link build (env d1_mini_link, -DONEMACHINE_LINK) the serial port carries // the link's frames instead (role/link.h), and the log says nothing. +// The link build by hash (d1_mini_link): its log goes nowhere, and the codes' names are in the build output only (describe.py), so the log lines that +// carry names are left out of it, and the names with them. The text build (d1_mini_link_text) and the build without the link keep them. +#if defined(ONEMACHINE_LINK) && !defined(ONEMACHINE_DESC_TEXT) + #define SPI_LOG_NAMES 0 +#else + #define SPI_LOG_NAMES 1 +#endif #ifdef ONEMACHINE_LINK struct NullPrint : Print { size_t write(uint8_t) override { return 1; } size_t write(const uint8_t*, size_t n) override { return n; } }; static NullPrint nullPrint; @@ -191,6 +198,7 @@ using AirTicker = fail::Ticks; // call say(value) when they change (the sync pass in loop()); air is the control group and ctrl_meas a register of it, silent. // Each reaches its node by a compile-time path into the machine (PathRef is node #3, child #1). template static void say(int32_t v) { +#if SPI_LOG_NAMES Log.print(millis()); Log.print(' '); for (unsigned i = 0, c; (c = Code::name().rom(i)); ++i) Log.print(char(c)); Log.print('='); int32_t p = 1; for (uint8_t i = 0; i < Decimals; ++i) p *= 10; if (v < 0) Log.print('-'); @@ -199,6 +207,7 @@ template static void say(int32_t v) { const int32_t frac = a % p; for (int32_t q = p / 10; q > frac && q > 1; q /= 10) Log.print('0'); Log.println(frac); +#endif #ifdef ONEMACHINE_LINK bmpm::ChangeQueue<8>::note(Code::num, v); // and for the consumer that reads changes over the link #endif @@ -218,9 +227,11 @@ template static void table(const __FlashStringHelper* bus) { // the machine and what is published of it, for a consumer: a row's address is its identity in the path static void describe() { +#if SPI_LOG_NAMES if (AirApp::reg.count < 2) return; SerialPut put; bmpm::describe(put, airBus); +#endif } // the air sensor's row: a status change, and each time it came back (its captured state replayed, or the defaults because it was another part) diff --git a/examples/spi/src/tree_ops.h b/examples/spi/src/tree_ops.h index c32b23e..7aeff92 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -5,10 +5,11 @@ // (bmpm::HashWalk, Extra::describeStatic); the consumer has the text from the build (examples/spi/describe.cpp writes .txt; python: Tree(link, descriptions=dir)). // With -DONEMACHINE_DESC_TEXT: the text itself, bmpm::describe's walk then the codes that only notify (Extra), each line ending with the // status of its row. Codes are numbered in the order they are listed. -// 'v' get by code payload: the code (text) reply: the status (u8), then the value (i32 little-endian) +// 'v' get by code payload: the code: u32 the description's hash and u8 the code's number (by hash, the default; BadHash when the hash +// is not this build's), or its name (text, -DONEMACHINE_DESC_TEXT) reply: the status (u8), then the value (i32 little-endian) // The status is the device's: Alive 0, Stale 1, Gone 2. A part that is not Alive has no live value: a register answers the last value set (its // capture, the intent that comes back), a sensor value the last one measured. A group or an event has no value: the status alone, with NoValue. -// 'w' set by code payload: the value (i32), then the code reply: nothing; the set goes through the node: its limits, its capture, +// 'w' set by code payload: the value (i32), then the code (as for 'v') reply: nothing; the set goes through the node: its limits, its capture, // its register (a part that is gone keeps it as the last intent) // 'n' changes since payload: none reply: u8 how many were refused since the last 'n', then per change // the code's number (u8) and its value (i32); a number with bit 7 set @@ -106,6 +107,19 @@ namespace bmpm { return p >= 0 ? p : findExtra(static_cast(nullptr), s, n); } + // ---- a code in a request ------------------------------------------------------------------------------------------------ +#ifndef ONEMACHINE_DESC_TEXT + // by its number, under the description's hash: u32 the hash (little-endian), u8 the number. A number is valid only under the hash it was read + // with: a consumer holding another build's description is refused (BadHash). The names are in the build output only, not in the image. + static int code(const uint8_t* s, uint16_t n, uint8_t& err) { + constexpr uint32_t mine = hash(); + if (n != 5) { err = role::LinkBadLength; return -1; } + if ((uint32_t(s[0]) | uint32_t(s[1]) << 8 | uint32_t(s[2]) << 16 | uint32_t(s[3]) << 24) != mine) { err = role::LinkBadHash; return -1; } + if (s[4] >= numCodes) { err = role::LinkUnknown; return -1; } + return s[4]; + } +#endif + // ---- status: the row a code is bound to --------------------------------------------------------------------------- template static uint8_t statusOne() { using S = typename SrcOf::type; @@ -174,8 +188,14 @@ namespace bmpm { static Pubs* const list = nullptr; switch (op) { case 'v': { - const int at = find(in, n); +#ifdef ONEMACHINE_DESC_TEXT + const int at = find(in, n); // by its name: the text build, whose consumer has the names from 'd' if (at < 0) { r.status(role::LinkUnknown); return; } +#else + uint8_t err = role::LinkUnknown; + const int at = code(in, n, err); + if (at < 0) { r.status(err); return; } +#endif const uint8_t st = status(at); int32_t v = 0; const uint8_t ls = at < numPubs ? get(list, at, v, st) : role::LinkNoValue; @@ -185,8 +205,14 @@ namespace bmpm { case 'w': { if (n < 5) { r.status(role::LinkBadLength); return; } const int32_t v = int32_t(uint32_t(in[0]) | uint32_t(in[1]) << 8 | uint32_t(in[2]) << 16 | uint32_t(in[3]) << 24); +#ifdef ONEMACHINE_DESC_TEXT const int at = find(in + 4, uint16_t(n - 4)); if (at < 0) { r.status(role::LinkUnknown); return; } +#else + uint8_t err = role::LinkUnknown; + const int at = code(in + 4, uint16_t(n - 4), err); + if (at < 0) { r.status(err); return; } +#endif r.status(at < numPubs ? set(list, at, v) : role::LinkReadOnly); return; } diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index a1cf3ee..36628e1 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -23,7 +23,7 @@ from collections import namedtuple from .machine import LinkError -OK, BAD_LENGTH, BAD_VALUE, NO_VALUE, UNKNOWN, TOO_LONG, READ_ONLY = 0, 2, 3, 4, 0x80, 0x81, 0x82 +OK, BAD_HASH, BAD_LENGTH, BAD_VALUE, NO_VALUE, UNKNOWN, TOO_LONG, READ_ONLY = 0, 1, 2, 3, 4, 0x80, 0x81, 0x82 class OutOfRange(ValueError): pass class ReadOnly(AttributeError): pass @@ -101,7 +101,7 @@ def refresh(self): st, data = self._call('d') if st != OK: raise LinkError('description: status %d' % st) text = data.decode() - statuses = None + statuses, h = None, None if text.startswith('hash '): # the description by its hash: the text is in the build output head, digits = text.split('\n')[:2] h = int(head[5:], 16) @@ -111,6 +111,7 @@ def refresh(self): if statuses is not None: if len(statuses) != len(codes): raise LinkError('description %08x: %d codes, the device has %d' % (h, len(codes), len(statuses))) for c, st in zip(codes, statuses): c.status = st + object.__setattr__(self, 'hash', h) # the description's hash: codes go by number under it (None: by name) object.__setattr__(self, 'description', text) object.__setattr__(self, '_codes', codes) object.__setattr__(self, 'codes', {c.name: c for c in codes}) @@ -127,10 +128,15 @@ def _load(self, h): return text raise LinkError('the device describes itself by hash %08x: no %s in %s (give Tree(link, descriptions=), or build the device with -DONEMACHINE_DESC_TEXT)' % (h, name, self.descriptions or 'no directory')) + def _key(self, c): + """A code in a request: u32 the description's hash and u8 its number (a device described by hash), or its name.""" + return struct.pack(' c.hi): raise OutOfRange('%s: %r is outside %d..%d' % (code, value, c.lo, c.hi)) - st, _ = self._call('w', struct.pack(' Date: Mon, 5 Oct 2026 08:51:25 +0000 Subject: [PATCH 18/19] Redrawn experiment 6: pin lines in the description, from driver manifests; 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 Claude-Session: https://claude.ai/code/session_01GDeNB62nMDNgj6xceZUWwx --- examples/spi/describe.cpp | 58 ++++- examples/spi/src/air_tree.h | 68 +++++- examples/spi/src/bmp280_machine.h | 18 +- examples/spi/src/irq_esp8266.h | 8 +- examples/spi/src/main.cpp | 10 +- examples/spi/src/rc522.h | 17 +- examples/spi/wiring/rig.toml | 2 +- include/oneMachine/discover/manifest.h | 23 ++ include/oneMachine/discover/spi.h | 2 + python/onemachine/tree.py | 18 ++ python/onemachine/wiring.py | 283 +++++++++++++++---------- test/discover/build_spi1.sh | 4 +- test/link/avr_tree.cpp | 2 +- test/link/check_tree.py | 6 +- test/link/tree_device.cpp | 2 +- test/wiring/build.sh | 4 +- test/wiring/check_wiring.py | 147 +++++++------ 17 files changed, 470 insertions(+), 202 deletions(-) create mode 100644 include/oneMachine/discover/manifest.h diff --git a/examples/spi/describe.cpp b/examples/spi/describe.cpp index b6b5621..0d6e290 100644 --- a/examples/spi/describe.cpp +++ b/examples/spi/describe.cpp @@ -1,6 +1,8 @@ // 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=

)). -// describe writes /.txt (the text without the status) and prints its path +// describe writes /.txt (the text without the status) and /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 describe.cpp (examples/spi/describe.py does it for PlatformIO). #include @@ -14,9 +16,57 @@ template struct NoNote { static constexpr auto fn = &noNote; }; using Pubs = airTree::Pubs; struct Text { std::string s; void operator()(char c) { s += c; } }; -template constexpr void describeAll(P& put) { bmpm::describeStatic(put, airTree::bus); airTree::cardLine(put); } +template constexpr void describeAll(P& put) { bmpm::describeStatic(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 static std::string kind() { + if constexpr (bmpm::IsGroup::value) return "group"; + else if constexpr (bmpm::HasReg::value) return "reg"; + else if constexpr (bmpm::HasConst::value) return "const"; + else if constexpr (bmpm::HasDecimals::value) return "value " + std::to_string(N::decimals); + else return "value 0"; +} +template static void children(std::string& o, const std::string& parent, unsigned at, hapi::Chain*) { + unsigned i = 0; ((o += "node bmp280 " + parent + "/" + nm(C::label()) + " " + std::to_string(at) + "/" + std::to_string(i++) + " " + kind() + "\n"), ...); +} +template static void nodes(std::string& o, hapi::Chain*) { + unsigned i = 0; + ((o += "node bmp280 " + nm(N::label()) + " " + std::to_string(i) + " " + kind() + "\n", + [&] { if constexpr (bmpm::IsGroup::value) children(o, nm(N::label()), i, static_cast(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(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 \n"); return 2; } Text t; describeAll(t); @@ -26,6 +76,10 @@ int main(int argc, char** argv) { 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; } diff --git a/examples/spi/src/air_tree.h b/examples/spi/src/air_tree.h index 3b86a83..402a14a 100644 --- a/examples/spi/src/air_tree.h +++ b/examples/spi/src/air_tree.h @@ -6,7 +6,10 @@ #pragma once #include #include +#include +#include #include "bmp280_machine.h" +#include "rc522.h" namespace airTree { @@ -18,8 +21,38 @@ namespace airTree { 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 sensor: the machine at 0x76 (its Criteria), under the App W's failure handling Mode - template using Machine = bmpm::Machine, Mode>; + // ---- 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 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(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() && line()); + static_assert(line() && line() && line()); + static_assert(line() && line()); + static_assert(line() && line()); + + // the sensor: the machine at the wiring's address (its Criteria), under the App W's failure handling Mode + template using Machine = bmpm::Machine, 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 @@ -36,4 +69,35 @@ namespace airTree { #define AIRTREE_CARD_TEXT " card -> 0/1 notify event ro value u32 status " inline constexpr char cardText[] = AIRTREE_CARD_TEXT; template 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 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 constexpr void wiringLines(P& put) { + using W = Wiring; + WiringPut

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 constexpr void describeStatic(P& put) { cardLine(put); wiringLines(put); } } diff --git a/examples/spi/src/bmp280_machine.h b/examples/spi/src/bmp280_machine.h index a5bad52..86cc057 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -28,6 +28,7 @@ #include #include #include +#include namespace bmpm { @@ -36,6 +37,18 @@ namespace bmpm { using oneMenu::MenuDef; using oneMenu::StaticBody; + // ---- the manifest: what the part needs and publishes (discover/manifest.h) ------------------------------------------------------- + // On I2C, at 0x76 or 0x77 (its SDO pin), with chip id 0x58 (BMP280) or 0x60 (BME280, the same registers here). No lines of its own: only the bus's. + // What it publishes is its machine's Nodes (Machine<...>::Nodes): a description walk lists them. + struct Manifest { + static constexpr const char* name = "bmp280"; + static constexpr const char* bus = "i2c"; + static constexpr uint8_t addrs[] = {0x76, 0x77}; + static constexpr uint8_t ids[] = {0x58, 0x60}; + template static constexpr void pins(F&&) {} + static constexpr bool at(uint8_t a) { return a == addrs[0] || a == addrs[1]; } + }; + // ---- the Criteria: which device a machine is ----------------------------------------------------------------------------------- template struct Addr { static constexpr uint8_t addr = A; }; @@ -89,6 +102,7 @@ namespace bmpm { template, typename Mode = Plain> struct Machine { static constexpr uint8_t addr = Criteria::addr; + static_assert(Manifest::at(Criteria::addr), "a BMP280/BME280 answers at 0x76 or 0x77 (its SDO pin)"); // the device: its data and its register access. Static, as the machine is. struct Dev { static constexpr uint8_t addr = Criteria::addr; @@ -264,7 +278,7 @@ namespace bmpm { using B = discover::DriverBase; using Edge = fail::DevEdge; static constexpr bool polled = true; - static constexpr uint8_t addrLo = Criteria::addr, addrHi = Criteria::addr, idReg = 0xD0, id = 0x58; + static constexpr uint8_t addrLo = Criteria::addr, addrHi = Criteria::addr, idReg = 0xD0, id = Manifest::ids[0]; static constexpr uint8_t recoverMask = fail::bit(fail::Kind::Corrupt); static constexpr bool reinitOnBusReturn = true; // a bus that comes back may have taken the part's supply: validate it again static void init(RowId) { Machine::init(); } @@ -295,7 +309,7 @@ namespace bmpm { }; // the BME280 answers 0x60 at the same register: a second entry, the same driver using UseBmp = discover::Use; - using UseBme = discover::Use, Driver>; + using UseBme = discover::Use, Driver>; using Entries = hapi::Chain; // ---- the index of a node in the machine ------------------------------------------------------------------------------- diff --git a/examples/spi/src/irq_esp8266.h b/examples/spi/src/irq_esp8266.h index 400e0c7..83b58ed 100644 --- a/examples/spi/src/irq_esp8266.h +++ b/examples/spi/src/irq_esp8266.h @@ -6,12 +6,14 @@ // across a reset of the board and drives one of them low stops the next boot. #pragma once #include +#include // the pins' facts: strap(), hasIrq(), gpio() namespace irq { template struct Esp8266Line { - static_assert(Pin <= 16, "an ESP8266 has GPIO0 to GPIO16"); - static_assert(Pin != 0 && Pin != 2 && Pin != 15, + using Pins = hw::esp8266::Esp8266Pins; + static_assert(Pins::gpio(Pin), "an ESP8266 has GPIO0 to GPIO16"); + static_assert(!Pins::strap(Pin), "an interrupt line on GPIO0, GPIO2 or GPIO15 (D3, D4, D8): a request pending at reset holds the pin low and the board does not boot"); }; @@ -22,7 +24,7 @@ namespace irq { }; template struct IsrFlag : Esp8266Line { - static_assert(Pin != 16, "GPIO16 has no interrupt: use Sampled"); + static_assert(hw::esp8266::Esp8266Pins::hasIrq(Pin), "GPIO16 has no interrupt: use Sampled"); static inline volatile bool flag = false; static void IRAM_ATTR isr() { flag = true; } static void begin() { pinMode(Pin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(Pin), isr, FALLING); } diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index c065cd3..bf9f17f 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -74,8 +74,8 @@ using discover::RowId; using hapi::Chain; namespace esp = hw::esp8266; -using Twi = esp::Esp8266TwiMaster<4, 5, 100000>; // SDA D2, SCL D1 -using Spi = hapi::APIOf, esp::OutPin<0>>, // slot 0 D8, slot 1 D3 +using Twi = esp::Esp8266TwiMaster; // SDA D2, SCL D1 +using Spi = hapi::APIOf, esp::OutPin>, // slot 0 D8, slot 1 D3 oneBus::SpiMaster<4000000>, esp::Esp8266SpiCore>; using airTree::CodeTemp; using airTree::CodePress; using airTree::CodeCard; // the codes the App publishes: air_tree.h @@ -140,7 +140,7 @@ struct RfidMode { template using DevStack = fail::Controller>, fail::Recover, fail::DetectError, fail::HoldOp, fail::Gate<50>, fail::TickPart>, fail::LazyStatus>; template using Access = fail::SpiAccess; - using Irq = rc522::Interrupt, IrqCounters, rc522::LineCheck, rc522::PollOnLineFault>; // D0 + using Irq = rc522::Interrupt, IrqCounters, rc522::LineCheck, rc522::PollOnLineFault>; // D0 }; using Rfid = rc522::Rc522; using RfidDrivers = discover::DriversIn>; @@ -174,7 +174,7 @@ inline void IrqCounters::report(uint32_t now) { Log.println(); } -constexpr uint8_t rstPin = 2; // D4: the RC522's RST +constexpr uint8_t rstPin = airTree::Wiring::rfidRst; // D4: the RC522's RST // The air sensor under failure handling: a bus edge and, per device, retry, recover (the part was reset without the host knowing), probe. struct AirMode { static constexpr bool checked = true, returnPath = false, idempotent = true, lifecycle = true; @@ -353,7 +353,7 @@ struct Extra { // a code that only notifies: the card (an event with a value, s = st == 0 ? "alive" : st == 1 ? "stale" : "gone"; while (*s) put(*s++); put('\n'); } - template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } + template static constexpr void describeStatic(P& put) { airTree::describeStatic(put); } }; using Ops = bmpm::TreeOps; struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; diff --git a/examples/spi/src/rc522.h b/examples/spi/src/rc522.h index dbfce3d..ef7a0ca 100644 --- a/examples/spi/src/rc522.h +++ b/examples/spi/src/rc522.h @@ -13,11 +13,26 @@ #include #include #include +#include namespace rc522 { using discover::RowId; + // ---- the manifest: what the part needs and publishes (discover/manifest.h) ------------------------------------------------------- + // On SPI; found by VersionReg among these ids (v1.0, v2.0, then the common clones: FM17522 and others). Its own lines: CS and RST, which the MCU + // drives, and IRQ, which the chip drives (push-pull, active low) and keeps asserted across a reset of the board when a request is pending. + // It publishes one event, the card. + using Ids = discover::IdSet<0x91, 0x92, 0x88, 0x12, 0xB2>; + struct Manifest { + static constexpr const char* name = "rc522"; + static constexpr const char* bus = "spi"; + using Id = Ids; + struct Pins { static constexpr discover::Drive cs = discover::Drive::Mcu, rst = discover::Drive::Mcu, irq = discover::Drive::Device; }; + template static constexpr void pins(F&& f) { f("cs", Pins::cs); f("rst", Pins::rst); f("irq", Pins::irq); } + static constexpr const char* event = "card"; + }; + // a card's UID, cascade level 1, first byte in the high byte; 0 when the card leaves struct Card { using Value = uint32_t; static constexpr uint8_t id = 20, decimals = 0; static constexpr const char* name = "card"; }; @@ -144,7 +159,7 @@ namespace rc522 { static constexpr uint32_t spiHz = 4000000; // the chip takes 10 MHz; 4 is kind to jumper wires static constexpr uint8_t spiMode = 0; static constexpr uint8_t idCmd = uint8_t((VersionReg << 1) | 0x80); - using Ids = discover::IdSet<0x91, 0x92, 0x88, 0x12, 0xB2>; // v1.0, v2.0, then the common clones (FM17522 and others) + using Ids = rc522::Ids; // the manifest's // what the poll found: the card in the field and the errors seen on the way (a BCC mismatch is a corrupt UID) // initTries: on which write of the configuration it read back (0: it never did) diff --git a/examples/spi/wiring/rig.toml b/examples/spi/wiring/rig.toml index 877a797..e43f72b 100644 --- a/examples/spi/wiring/rig.toml +++ b/examples/spi/wiring/rig.toml @@ -1,7 +1,7 @@ # The rig's wiring (examples/spi), written by hand: the parts, their pins, and the codes the device publishes, by the same names. # python3 -m onemachine.wiring check|emit|diff examples/spi/wiring/rig.toml (python/onemachine/wiring.py) # TOML, not the essay's YAML: Python reads it with the standard library (tomllib). -board = "wemos-d1-mini" # its pins: OneChip's chips/esp8266/esp8266Device.h (D0..D8 and what each one is) +board = "wemos-d1-mini" # its pins and their facts: OneChip's Esp8266Pins, read through facts.txt (examples/spi/describe.cpp) [i2c] sda = "D2" diff --git a/include/oneMachine/discover/manifest.h b/include/oneMachine/discover/manifest.h new file mode 100644 index 0000000..0c4470e --- /dev/null +++ b/include/oneMachine/discover/manifest.h @@ -0,0 +1,23 @@ +#pragma once +// oneMachine/discover/manifest.h -- what a driver declares about its part, beside the driver: the bus it speaks, its addresses and ids, its pins and +// who drives each one. A wiring is checked against it (the strap rule below; python/onemachine/wiring.py reads it through a host program), and the +// description's wiring lines carry it (OneMachine Redrawn, experiment 6). +// Drive::Mcu the MCU drives the line (a chip select, a reset, SCL with one master): safe on a boot strap pin while the MCU holds it at the +// level the strap needs, as a chip select idles high +// Drive::Device the part drives it (an IRQ, MISO): it can hold the line at reset, when the MCU is not running yet +// Drive::Shared both (open-drain SDA): a part can hold it low across a reset of the MCU +// The bus lines' roles (I2C, SPI) are here too for now; they belong with the buses (OneBus). +#include + +namespace discover { + + enum class Drive : uint8_t { Mcu, Device, Shared }; + constexpr const char* driveName(Drive d) { return d == Drive::Mcu ? "mcu" : d == Drive::Device ? "device" : "shared"; } + + // a line on a pin is safe at reset when the MCU drives it, or when the pin is not one the chip samples at reset + template constexpr bool safeAtReset(uint8_t gpio, Drive d) { return d == Drive::Mcu || !Board::strap(gpio); } + + struct I2cLines { static constexpr Drive sda = Drive::Shared, scl = Drive::Mcu; }; + struct SpiLines { static constexpr Drive sck = Drive::Mcu, miso = Drive::Device, mosi = Drive::Mcu, cs = Drive::Mcu; }; + +} diff --git a/include/oneMachine/discover/spi.h b/include/oneMachine/discover/spi.h index c7a8d5f..0392d02 100644 --- a/include/oneMachine/discover/spi.h +++ b/include/oneMachine/discover/spi.h @@ -32,6 +32,8 @@ namespace discover { static_assert(((V != 0x00 && V != 0xFF) && ...), "an SPI id of 0x00 or 0xFF is what an empty slot reads: it cannot identify a device"); static constexpr bool has(uint8_t v) { return ((v == V) || ...); } + static constexpr uint8_t list[] = {V...}; // in order, for a manifest + static constexpr unsigned count = sizeof...(V); }; template struct AtSlot { static constexpr uint8_t slot = S; }; diff --git a/python/onemachine/tree.py b/python/onemachine/tree.py index 36628e1..eb65a2f 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -52,11 +52,29 @@ def _fnv(data, h=2166136261): for b in data: h = ((h ^ b) * 16777619) & 0xFFFFFFFF return h +def wiring(text): + """The `wiring` section of a description: {'board': name, : {role: (gpio, drive), ...}, ...} (bus lines `i2c`, `spi`; parts by name, with + `driver`; `at` for an address). Empty when the description has none (the text a device sends; the build output by hash has it).""" + out, seen = {}, False + for line in text.split('\n'): + if line.startswith('wiring '): seen = True; out['board'] = line[7:]; continue + if not seen or not line.startswith(' '): continue + w = line.split() + name, rest, entry = w[0], w[1:], {} + if rest and rest[0] not in ('sda', 'scl', 'sck', 'miso', 'mosi', 'cs', 'rst', 'irq', 'at'): entry['driver'], rest = rest[0], rest[1:] + i = 0 + while i < len(rest): + if rest[i] == 'at': entry['at'] = int(rest[i + 1], 16); i += 2 + else: entry[rest[i]] = (int(rest[i + 1]), rest[i + 2]); i += 3 + out[name] = entry + return out + def _parse(text): """The `published` section: one line per code, in the order the device numbers them.""" codes, seen = [], False for line in text.split('\n'): if line == 'published': seen = True; continue + if seen and line and not line.startswith(' '): break # the next section (the wiring): the codes are done if not seen or not line.startswith(' '): continue head, _, rest = line.strip().partition(' -> ') words = rest.split(' ') diff --git a/python/onemachine/wiring.py b/python/onemachine/wiring.py index 92f2f05..8fc8ad1 100644 --- a/python/onemachine/wiring.py +++ b/python/onemachine/wiring.py @@ -1,181 +1,242 @@ -"""A wiring spec in, the App's composition out, and the device's own description as the test that the build matched the wiring -(OneMachine Redrawn, experiment 4: a dry run). - - python3 -m onemachine.wiring check examples/spi/wiring/rig.toml the wiring's rules; exit 1 with the reasons - python3 -m onemachine.wiring emit examples/spi/wiring/rig.toml the App composition (C++), after check - python3 -m onemachine.wiring diff examples/spi/wiring/rig.toml desc.txt the spec against a device's description ('d' as text) - -The spec is TOML (tomllib, no dependency): board, [i2c], [spi], [parts.] in the App's machine order, [publish] code = "part/node". -The board's pins come from OneChip (chips/esp8266/esp8266Device.h): each D pin's GPIO, and what its comment says of it ("boot strapping", -"no interrupt"). What a driver needs and publishes (its manifest) is DRIVERS below: a stand-in for a declaration that belongs beside each driver. - +"""A wiring spec in, the App's wiring out, and the device's own description as the test that the build matched the wiring +(OneMachine Redrawn, experiments 4 and 6: a dry run). + + python3 -m onemachine.wiring check examples/spi/wiring/rig.toml [--facts

] the wiring's rules; exit 1 with the reasons + python3 -m onemachine.wiring emit examples/spi/wiring/rig.toml [--facts ] the App's Wiring (C++), after check + python3 -m onemachine.wiring diff examples/spi/wiring/rig.toml [--facts ] the spec against a description (the build + output's .txt: its codes, paths and wiring lines) + +The spec is TOML (tomllib, no dependency): board, [i2c], [spi], [parts.] in the App's machine order (the canonical order, D45), and [publish] +code = "part/node". + +What the rules know comes from the C++, through facts.txt, which examples/spi/describe.cpp writes beside the description (describe.py does it on +every PlatformIO build; without --facts this module builds it with the host's g++): + board each D pin's GPIO, whether the chip samples it at reset (strap, and the level it needs) and whether it has an interrupt: OneChip's Esp8266Pins + bus who drives each bus line (I2C SDA shared, SCL the MCU; SPI MISO the part, the rest the MCU): discover::I2cLines, SpiLines + driver each driver's manifest beside the driver (bmpm::Manifest, rc522::Manifest): its bus, addresses, ids, its own lines and who drives each one, + its nodes (walked from its machine) or its event The rules, each with the sentence a person needs: - every pin is on the board and used once - - a line the device drives at any time (an IRQ) is not on a boot strap pin: the RC522 keeps a pending IRQ across a reset of the board, holds the - pin low, and the ESP8266 boots into ROM (download) mode. The C++ rejects it too (irq::Esp8266Line's static_assert), at the next build. + - a line the part drives or shares is not on a boot strap pin; the MCU's own lines may be (a chip select on D3 or D8 idles high and is fine) - an ISR delivery is not on a pin without an interrupt (GPIO16): it is sampled there - - a part is on the bus its driver speaks; at an address it can have; the ids it accepts are ids its driver knows - - every published code names a node its part has + - a part is on the bus its driver speaks, at an address and with ids its driver knows + - every published code names a node (or the event) its part has """ -import os, re, sys, tomllib -from .tree import _parse +import os, re, subprocess, sys, tempfile, tomllib +from .tree import _parse, wiring as _wiring HERE = os.path.dirname(os.path.abspath(__file__)) -ONECHIP = os.environ.get('ONECHIP', os.path.normpath(os.path.join(HERE, '..', '..', '..', 'OneChip'))) - -BOARDS = {'wemos-d1-mini': ('chips/esp8266/esp8266Device.h', 'Esp8266Dev')} +ROOT = os.path.normpath(os.path.join(HERE, '..', '..')) +LIBS = ['OneMachine', 'HAPI', 'OneBus', 'OneData', 'OneMenu', 'OneItem', 'OneOutput', 'OneBit', 'OnePin', 'OneChip', 'OneParse', 'OneInput', 'OneIO'] class WiringError(Exception): def __init__(self, errors): super().__init__('\n'.join(errors)); self.errors = errors -class Board: - """D pins of a board, from OneChip: gpio, and the notes its comment gives (strap: sampled at reset; noirq: no interrupt).""" - def __init__(self, name): - if name not in BOARDS: raise WiringError(['board %r: not known (known: %s)' % (name, ', '.join(BOARDS))]) - path = os.path.join(ONECHIP, 'include', BOARDS[name][0]) - self.pins = {} - for m in re.finditer(r'static constexpr uint8_t (D\d+)\s*=\s*(\d+);\s*//(.*)', open(path).read()): - note = m.group(3).lower() - self.pins[m.group(1)] = dict(gpio=int(m.group(2)), strap='boot strapping' in note, noirq='no interrupt' in note) - self.source = path +class Facts: + """facts.txt: the board's pins, the buses' lines and the drivers' manifests, as the C++ declares them.""" + def __init__(self, text): + self.board, self.pins, self.buses, self.drivers = None, {}, {}, {} + for line in text.split('\n'): + w = line.split() + if not w: continue + if w[0] == 'board': self.board = w[1] + elif w[0] == 'pin': + self.pins[w[1]] = dict(gpio=int(w[2]), strap='strap' in w, level=('high' if 'high' in w else 'low') if 'strap' in w else None, noirq='noirq' in w) + elif w[0] == 'bus': self.buses[w[1]] = dict(zip(w[2::2], w[3::2])) + elif w[0] == 'driver': + d = self.drivers.setdefault(w[1], dict(bus=w[2], addrs=(), ids=(), lines={}, nodes={}, event=None)) + key = None + for t in w[3:]: + if t in ('addrs', 'ids'): key = t; d[key] = () + else: d[key] = d[key] + (int(t, 16),) + elif w[0] == 'line': self.drivers[w[1]]['lines'][w[2]] = w[3] + elif w[0] == 'node': + kind = w[4]; dec = int(w[5]) if kind == 'value' else None + self.drivers[w[1]]['nodes'][w[2]] = (tuple(int(x) for x in w[3].split('/')), kind, dec) + elif w[0] == 'event': self.drivers[w[1]]['event'] = w[2]; self.drivers[w[1]]['nodes'][w[2]] = (None, 'event', None) + + @classmethod + def load(cls, where=None): + """From a facts.txt, or a directory holding one; None: build describe.cpp with the host's g++ and run it.""" + if where: + return cls(open(os.path.join(where, 'facts.txt') if os.path.isdir(where) else where).read()) + with tempfile.TemporaryDirectory() as t: + build(t) + return cls(open(os.path.join(t, 'facts.txt')).read()) def gpio(self, pin): return self.pins[pin]['gpio'] def straps(self): return [p for p, v in self.pins.items() if v['strap']] -# What each driver needs and publishes. node: its path in the machine (positions, as PathRef) and its kind, and for a value its decimals and -# whether it notifies. A stand-in: the declaration belongs beside the driver (the identify entries, OneChip's pin roles, the machine's Nodes). -DRIVERS = { - 'bmp280': dict(bus='i2c', addrs=(0x76, 0x77), ids=(0x58, 0x60), - nodes={'temp': ((0,), 'value', 2), 'press': ((1,), 'value', 2), 'ctrl': ((3,), 'group', None), - 'ctrl/config': ((3, 0), 'reg', None), 'ctrl/ctrl_meas': ((3, 1), 'reg', None)}), - 'rc522': dict(bus='spi', pins={'cs': 'out', 'rst': 'out', 'irq': 'driven'}, - nodes={'card': (None, 'event', None)}), -} +def build(out, wiring_header=None, cxx=None): + """Build examples/spi/describe.cpp on the host (with an emitted Wiring when given) and run it into out: .txt and facts.txt. + Returns (returncode, the compiler's or the program's output); raises when the build fails and wiring_header is None.""" + parent = os.path.dirname(ROOT) + inc = [f for n in LIBS for f in ('-I', os.path.join(parent, n, 'include'))] + exe = os.path.join(out, 'describe') + cmd = [cxx or os.environ.get('CXX', 'g++'), '-std=c++17', '-O1'] + inc + if wiring_header: cmd.append('-DAIRTREE_WIRING="%s"' % os.path.abspath(wiring_header)) + r = subprocess.run(cmd + [os.path.join(ROOT, 'examples', 'spi', 'describe.cpp'), '-o', exe], capture_output=True, text=True) + if r.returncode: + if wiring_header is None: raise RuntimeError('describe.cpp does not build:\n' + r.stderr[-2000:]) + return r.returncode, r.stderr + r = subprocess.run([exe, out], capture_output=True, text=True) + return r.returncode, r.stdout.strip() def load(path): with open(path, 'rb') as f: spec = tomllib.load(f) spec['_path'] = path return spec -def _pin(errors, board, owner, pin): - if pin not in board.pins: errors.append('%s on %s: the %s has no such pin (it has %s)' % (owner, pin, 'board', ', '.join(board.pins))); return None - return board.pins[pin] +def _pin_of(v): return v['pin'] if isinstance(v, dict) else v -def check(spec): - """The wiring's rules: a list of sentences, empty when it is sound.""" - errors, used = [], {} - board = Board(spec.get('board', '?')) - def use(owner, pin): - if _pin(errors, board, owner, pin) is None: return - if pin in used: errors.append('%s on %s: %s is on it already' % (owner, pin, used[pin])) - else: used[pin] = owner +def _lines(spec, facts): + """Every line the spec wires: (owner, pin, who drives it, the part's driver or None, its role).""" + out = [] for bus, roles in (('i2c', ('sda', 'scl')), ('spi', ('sck', 'miso', 'mosi'))): for r in roles: - if r in spec.get(bus, {}): use('%s.%s' % (bus, r), spec[bus][r]) - for p in spec.get('spi', {}).get('empty_cs', []): use('spi.empty_cs', p) + if r in spec.get(bus, {}): out.append(('%s.%s' % (bus, r), spec[bus][r], facts.buses.get(bus, {}).get(r, 'mcu'), None, r)) + for p in spec.get('spi', {}).get('empty_cs', []): out.append(('spi.empty_cs', p, facts.buses.get('spi', {}).get('cs', 'mcu'), None, 'cs')) for name, part in spec.get('parts', {}).items(): - drv = DRIVERS.get(part.get('driver')) - if drv is None: errors.append('%s: no driver %r (known: %s)' % (name, part.get('driver'), ', '.join(DRIVERS))); continue + drv = facts.drivers.get(part.get('driver')) + if drv is None: continue + for role, drive in drv['lines'].items(): + if part.get(role) is not None: out.append(('%s.%s' % (name, role), _pin_of(part[role]), drive, part['driver'], role)) + return out + +def check(spec, facts): + """The wiring's rules: a list of sentences, empty when it is sound.""" + errors, used = [], {} + if spec.get('board') != facts.board: errors.append('board %r: the facts are for %r' % (spec.get('board'), facts.board)) + for name, part in spec.get('parts', {}).items(): + drv = facts.drivers.get(part.get('driver')) + if drv is None: errors.append('%s: no driver %r (known: %s)' % (name, part.get('driver'), ', '.join(facts.drivers))); continue if part.get('bus') != drv['bus']: errors.append('%s: a %s is on %s, not %s' % (name, part['driver'], drv['bus'], part.get('bus'))) - if 'addr' in part and part['addr'] not in drv.get('addrs', ()): + if 'addr' in part and part['addr'] not in drv['addrs']: errors.append('%s: a %s answers at %s, not 0x%02X' % (name, part['driver'], ' or '.join('0x%02X' % a for a in drv['addrs']), part['addr'])) for i in part.get('accept', []): - if i not in drv.get('ids', ()): errors.append('%s: accepts id 0x%02X, which its driver does not know (it knows %s)' % (name, i, ', '.join('0x%02X' % x for x in drv['ids']))) - for role, kind in drv.get('pins', {}).items(): - v = part.get(role) - if v is None: errors.append('%s: its %s pin is not given' % (name, role)); continue - pin = v['pin'] if isinstance(v, dict) else v - use('%s.%s' % (name, role), pin) - info = board.pins.get(pin) - if info is None: continue - if kind == 'driven' and info['strap']: - st = ['%s (GPIO%d)' % (p, board.gpio(p)) for p in board.straps()] - straps = ', '.join(st[:-1]) + ' or ' + st[-1] if len(st) > 1 else ''.join(st) - errors.append('%s.%s on %s (GPIO%d): a boot strap pin. The %s keeps a pending IRQ across a reset of the board and holds the pin low, ' - 'and the ESP8266 then boots into ROM (download) mode instead of the sketch. Put the IRQ on a pin that is not %s; ' - 'the rig uses D0 with delivery = "sampled".' % (name, role, pin, info['gpio'], part['driver'].upper(), straps)) - if kind == 'driven' and isinstance(v, dict) and v.get('delivery') == 'isr' and info['noirq']: - errors.append('%s.%s on %s (GPIO%d): this pin has no interrupt; use delivery = "sampled"' % (name, role, pin, info['gpio'])) + if i not in drv['ids']: errors.append('%s: accepts id 0x%02X, which its driver does not know (it knows %s)' % (name, i, ', '.join('0x%02X' % x for x in drv['ids']))) + for role in drv['lines']: + if role not in part: errors.append('%s: its %s pin is not given' % (name, role)) + for owner, pin, drive, driver, role in _lines(spec, facts): + info = facts.pins.get(pin) + if info is None: errors.append('%s on %s: the board has no such pin (it has %s)' % (owner, pin, ', '.join(facts.pins))); continue + if pin in used: errors.append('%s on %s: %s is on it already' % (owner, pin, used[pin])); continue + used[pin] = owner + if drive != 'mcu' and info['strap']: + st = ['%s (GPIO%d)' % (p, facts.gpio(p)) for p in facts.straps()] + why = ('The %s keeps a pending IRQ across a reset of the board and holds the pin low' % driver.upper() if role == 'irq' and driver + else 'A part can hold an open-drain line low across a reset of the MCU' if drive == 'shared' + else 'The part can drive it while the MCU is held in reset') + errors.append('%s on %s (GPIO%d): a boot strap pin, and this line is driven by the %s, not the MCU alone. %s, and the ESP8266 then boots into ' + 'ROM (download) mode instead of the sketch (it needs GPIO%d %s at reset). Put it on a pin that is not %s; a line the MCU drives ' + '(a chip select, a reset) may stay there.' % (owner, pin, info['gpio'], 'part' if drive == 'device' else 'part and the MCU (shared)', + why, info['gpio'], info['level'], ', '.join(st[:-1]) + ' or ' + st[-1])) + v = spec.get('parts', {}).get(owner.split('.')[0], {}).get(role) if driver else None + if isinstance(v, dict) and v.get('delivery') == 'isr' and info['noirq']: + errors.append('%s on %s (GPIO%d): this pin has no interrupt; use delivery = "sampled"' % (owner, pin, info['gpio'])) for code, where in spec.get('publish', {}).items(): part, _, node = where.partition('/') p = spec.get('parts', {}).get(part) if p is None: errors.append('publish %s = %r: no part %r' % (code, where, part)); continue - drv = DRIVERS.get(p.get('driver'), {}) + drv = facts.drivers.get(p.get('driver'), {}) if node not in drv.get('nodes', {}): errors.append('publish %s = %r: a %s has no node %r (it has %s)' % (code, where, p.get('driver'), node, ', '.join(drv.get('nodes', {})))) return errors def _code_tag(code): return 'Code' + ''.join(w[:1].upper() + w[1:] for w in re.split(r'_', code.split('/')[-1])) # air/ctrl_meas: CodeCtrlMeas -def emit(spec, checked=True): - """The App's composition for examples/spi, as C++ lines (a dry run: the sketch is not built from it). Raises WiringError when check() fails; - checked=False emits anyway (to show that the C++ rules reject the same wiring).""" - errors = check(spec) if checked else [] +def emit(spec, facts, checked=True): + """The App's Wiring for examples/spi (a header air_tree.h includes with -DAIRTREE_WIRING), then its codes and published nodes, as C++ lines. + Raises WiringError when check() fails; checked=False emits anyway (to show that the C++ rules reject the same wiring). The Wiring ends at the + line `};`: wiring_header() is that part alone.""" + errors = check(spec, facts) if checked else [] if errors: raise WiringError(errors) - board, parts = Board(spec['board']), spec['parts'] - g = board.gpio - out = ['// generated from %s by python3 -m onemachine.wiring emit: the App composition of examples/spi (dry run)' % os.path.basename(spec['_path'])] - i2c, spi = spec['i2c'], spec['spi'] - out.append('using Twi = esp::Esp8266TwiMaster<%d, %d, %d>;' % (g(i2c['sda']), g(i2c['scl']), i2c.get('hz', 100000))) - cs = [p['cs'] for p in parts.values() if p['bus'] == 'spi'] + spi.get('empty_cs', []) - out.append('using Spi = hapi::APIOf, oneBus::SpiMaster<%d>, esp::Esp8266SpiCore>;' % - (', '.join('esp::OutPin<%d>' % g(p) for p in cs), spi.get('hz', 4000000))) + parts = spec['parts'] + def cam(part, role): return part + role[:1].upper() + role[1:] + out = ['// generated from %s by python3 -m onemachine.wiring emit: the App\'s Wiring (air_tree.h includes it with -DAIRTREE_WIRING)' % os.path.basename(spec['_path']), + 'struct Wiring {', + ' using Board = hw::esp8266::Esp8266Pins;', + ' static constexpr const char* board = "%s";' % spec['board'], + ' static constexpr uint8_t sda = Board::%s, scl = Board::%s;' % (spec['i2c']['sda'], spec['i2c']['scl']), + ' static constexpr uint8_t sck = Board::%s, miso = Board::%s, mosi = Board::%s;' % (spec['spi']['sck'], spec['spi']['miso'], spec['spi']['mosi'])] + cs = ['%s = Board::%s' % (cam(n, 'cs'), p['cs']) for n, p in parts.items() if p['bus'] == 'spi'] + ['emptyCs = Board::%s' % c for c in spec['spi'].get('empty_cs', [])] + out.append(' static constexpr uint8_t %s;' % ', '.join(cs)) for name, p in parts.items(): - if p['driver'] == 'rc522': - out.append('constexpr uint8_t rstPin = %d;' % g(p['rst'])) - irq = p['irq'] - out.append('using RfidLine = irq::%s<%d>;' % ('Sampled' if irq.get('delivery', 'sampled') == 'sampled' else 'IsrFlag', g(irq['pin']))) - if p['driver'] == 'bmp280': - out.append('using Bmp = bmpm::Machine, AirMode>;' % p['addr']) - for num, (code, where) in enumerate(spec['publish'].items()): + lines = [r for r in facts.drivers[p['driver']]['lines'] if r != 'cs'] + if lines: out.append(' static constexpr uint8_t %s;' % ', '.join('%s = Board::%s' % (cam(name, r), _pin_of(p[r])) for r in lines)) + if 'addr' in p: out.append(' static constexpr uint8_t %s = 0x%02X;' % (cam(name, 'addr'), p['addr'])) + out.append('};') + for num, code in enumerate(spec['publish']): out.append('struct %s { static constexpr uint8_t num = %d; ONEMACHINE_STATE_NAME(name, "%s"); };' % (_code_tag(code), num, code)) for code, where in spec['publish'].items(): part, _, node = where.partition('/') - path, kind, dec = DRIVERS[parts[part]['driver']]['nodes'][node] - if path is None: continue # an event: the App's Extra, not a published node - notify = ', oneData::OnSync<&say<%s, %d>>' % (_code_tag(code), dec) if kind == 'value' else '' - out.append('bmpm::PublishedAt<%s, bmpm::PathRef%s>' % (_code_tag(code), ', '.join(map(str, path)), notify)) + path, kind, dec = facts.drivers[parts[part]['driver']]['nodes'][node] + if path is None: continue # an event: the App's Extra, not a published node + notify = ', oneData::OnSync::fn>' % _code_tag(code) if kind == 'value' else '' + out.append('bmpm::PublishedAt<%s, bmpm::PathRef%s>' % (_code_tag(code), ', '.join(map(str, path)), notify)) return out -def diff(spec, description): - """The spec against the device's description ('d' as text): a list of mismatches, empty when the device is what was wired.""" - errors = check(spec) +def wiring_header(lines): return '\n'.join(lines[:lines.index('};') + 1]) + '\n' + +def diff(spec, facts, description): + """The spec against a description (the build output's text): its codes, paths, kinds and scales, and its wiring lines. A list of mismatches.""" + errors = check(spec, facts) if errors: raise WiringError(errors) parts, names = spec['parts'], list(spec['parts']) - got = _parse(description) + got, wired = _parse(description), _wiring(description) want = list(spec['publish'].items()) out = [] if [c.name for c in got] != [c for c, _ in want]: out.append('codes: the spec publishes %s, the device %s (a code\'s number is its position)' % ([c for c, _ in want], [c.name for c in got])) dev = {c.name: c for c in got} + spi_parts = [q['cs'] for q in parts.values() if q['bus'] == 'spi'] for code, where in want: c = dev.get(code) if c is None: continue part, _, node = where.partition('/') - p = parts[part]; drv = DRIVERS[p['driver']] - path, kind, dec = drv['nodes'][node] + p = parts[part] + path, kind, dec = facts.drivers[p['driver']]['nodes'][node] machine = names.index(part) - if path is None: expect = [machine, 1 + [q['cs'] for q in parts.values() if q['bus'] == 'spi'].index(p['cs'])] # an event: / - else: expect = [machine, p['addr']] + list(path) + expect = [machine, 1 + spi_parts.index(p['cs'])] if path is None else [machine, p['addr']] + list(path) if c.path != expect: out.append('%s: the device has it at %s, the spec at %s (%s)' % (code, '/'.join(map(str, c.path)), '/'.join(map(str, expect)), 'machine/address/node' if path is not None else 'machine/row')) dkind = 'event' if c.notify == 'event' else c.kind if dkind != kind: out.append('%s: the device has a %s, the spec\'s %s/%s is a %s' % (code, dkind, p['driver'], node, kind)) if kind == 'value' and dec is not None and c.scaled != dec: out.append('%s: scaled %d on the device, %d in the spec' % (code, c.scaled, dec)) + if not wired: + out.append('wiring: the description has no wiring lines (the build output\'s .txt has them; the text a device sends does not)') + return out + def same(where, role, pin, drive): + got_, want_ = wired.get(where, {}).get(role), (facts.gpio(pin), drive) + if got_ != want_: + out.append('wiring %s %s: the device has %s, the spec %s (GPIO%d, %s)' % (where, role, 'GPIO%d %s' % got_ if got_ else 'nothing', pin, want_[0], want_[1])) + for bus, roles in (('i2c', ('sda', 'scl')), ('spi', ('sck', 'miso', 'mosi'))): + for r in roles: same(bus, r, spec[bus][r], facts.buses[bus][r]) + for c in spec['spi'].get('empty_cs', []): same('empty', 'cs', c, facts.buses['spi']['cs']) + for name, p in parts.items(): + drv = facts.drivers[p['driver']] + if wired.get(name, {}).get('driver') != p['driver']: out.append('wiring %s: the device has a %s, the spec a %s' % (name, wired.get(name, {}).get('driver'), p['driver'])) + for role, drive in drv['lines'].items(): same(name, role, _pin_of(p[role]), drive) + if 'addr' in p and wired.get(name, {}).get('at') != p['addr']: + at = wired.get(name, {}).get('at') + out.append('wiring %s: the device is at %s, the spec at 0x%02X' % (name, '0x%02X' % at if at is not None else 'no address', p['addr'])) return out def main(argv): - if len(argv) < 3 or argv[1] not in ('check', 'emit', 'diff'): print(__doc__); return 2 - spec = load(argv[2]) + args = argv[1:] + facts_dir = None + if '--facts' in args: i = args.index('--facts'); facts_dir = args[i + 1]; del args[i:i + 2] + if len(args) < 2 or args[0] not in ('check', 'emit', 'diff'): print(__doc__); return 2 + spec, facts = load(args[1]), Facts.load(facts_dir) try: - if argv[1] == 'check': - errors = check(spec) + if args[0] == 'check': + errors = check(spec, facts) for e in errors: print('error: ' + e) - if not errors: print('ok: %s' % argv[2]) + if not errors: print('ok: %s' % args[1]) return 1 if errors else 0 - if argv[1] == 'emit': print('\n'.join(emit(spec))); return 0 - mism = diff(spec, open(argv[3]).read()) + if args[0] == 'emit': print('\n'.join(emit(spec, facts))); return 0 + mism = diff(spec, facts, open(args[2]).read()) for m in mism: print('mismatch: ' + m) - if not mism: print('ok: the device is what %s wires' % argv[2]) + if not mism: print('ok: the device is what %s wires' % args[1]) return 1 if mism else 0 except WiringError as e: for x in e.errors: print('error: ' + x) diff --git a/test/discover/build_spi1.sh b/test/discover/build_spi1.sh index 14fe6d1..7eea3d0 100755 --- a/test/discover/build_spi1.sh +++ b/test/discover/build_spi1.sh @@ -79,7 +79,7 @@ if command -v clang++ >/dev/null; then fi echo; echo "=== ESP8266 interrupt delivery (irq_delivery.cpp, host stub): the pins that compile, the ones rejected ===" -DINC="$INC -I ../support/arduino_stub" +DINC="$INC -I ../../../OneChip/include -I ../support/arduino_stub" # the pins' facts: OneChip's esp8266Pins.h g++ -std=c++17 -Wall -Wextra -Werror $DINC irq_delivery.cpp -o "$OUT/idl" && "$OUT/idl" && echo "OK: Sampled<16> and IsrFlag<5> build" || { echo "FAIL: the delivery components do not build"; rc=1; } for pair in "NEG_D3:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" "NEG_D4:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" "NEG_D8:GPIO0, GPIO2 or GPIO15 (D3, D4, D8)" \ "NEG_ISR16:GPIO16 has no interrupt: use Sampled" "NEG_PAST:an ESP8266 has GPIO0 to GPIO16"; do @@ -100,7 +100,7 @@ for pair in "NEG_ID_IDLE:an SPI id of 0x00 or 0xFF is what an empty slot reads" done echo; echo "=== the Python consumer of the machine tree (test/link): over a pipe and in-process (ctypes) ===" -if command -v python3 >/dev/null; then bash ../link/build.sh 2>&1 | grep -E "^OK|^FAIL|Error|Traceback" || rc=1; else echo "(python3 not found: skipped)"; fi +if command -v python3 >/dev/null; then bash ../link/build.sh > "$OUT/link.txt" 2>&1 || rc=1; grep -E "^OK|^FAIL|Error|Traceback" "$OUT/link.txt"; else echo "(python3 not found: skipped)"; fi echo; echo "=== the wiring spec dry run (test/wiring): the rig's spec, its composition, the device's description; the IRQ on D4 refused ===" if python3 -c "import tomllib" 2>/dev/null; then bash ../wiring/build.sh > "$OUT/wiring.txt" 2>&1 || rc=1; grep -E "^OK|^FAIL|^emitted|error:|C\+\+ build|Traceback" "$OUT/wiring.txt"; else echo "(python3 3.11+ not found: skipped)"; fi diff --git a/test/link/avr_tree.cpp b/test/link/avr_tree.cpp index cc7346c..c199a54 100644 --- a/test/link/avr_tree.cpp +++ b/test/link/avr_tree.cpp @@ -66,7 +66,7 @@ struct Extra { using Codes = Chain; static uint8_t status(uint8_t) { return 0; } template static void describe(P& put) { const char* s = " card -> 0/1 notify event ro value u32 status alive\n"; while (*s) put(*s++); } - template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } + template static constexpr void describeStatic(P& put) { airTree::describeStatic(put); } }; using Ops = bmpm::TreeOps; struct UartOut { static void put(uint8_t b) { Uart::putch(b); } }; diff --git a/test/link/check_tree.py b/test/link/check_tree.py index 324ca90..fc84108 100644 --- a/test/link/check_tree.py +++ b/test/link/check_tree.py @@ -31,9 +31,11 @@ def key(name): # a code in a raw reque if HASHED: check(m.description == open(os.path.join(DESC, d[5:13].decode() + '.txt')).read(), 'the text is the build output\'s') if not HASHED and DESC: # the two walks: the text the device sends, without its statuses, is the build output's import re - files = [f for f in os.listdir(DESC) if f.endswith('.txt')] + files = [f for f in os.listdir(DESC) if re.fullmatch(r'[0-9a-f]{8}\.txt', f)] # .txt (facts.txt is beside it) static = '\n'.join(re.sub(r' status (alive|stale|gone)$', '', l) for l in m.description.split('\n')) - check(len(files) == 1 and static == open(os.path.join(DESC, files[0])).read(), 'the text walk and the hash walk write the same description') + built = open(os.path.join(DESC, files[0])).read() + built = built[:built.index('wiring ')] if 'wiring ' in built else built # the wiring lines are the build output's only (the text build is Round 6's) + check(len(files) == 1 and static == built, 'the text walk and the hash walk write the same description') check(list(m.codes) == ['temp', 'press', 'air', 'air/config', 'air/ctrl_meas', 'card'], 'the codes, in the device\'s order: %s' % list(m.codes)) check(m.codes['temp'].scaled == 2 and m.codes['temp'].notify == 'sync' and m.codes['temp'].ro, 'temp: scaled 2, notifies, read-only') check(m.codes['air'].kind == 'group' and m.codes['air'].group_size == 2, 'air is a group of 2') diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index 4697ba3..4345bf4 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -61,7 +61,7 @@ struct Extra { // a code that only notifies: the card (an event with a value, s = cardStatus == 0 ? "alive" : cardStatus == 1 ? "stale" : "gone"; while (*s) put(*s++); put('\n'); } - template static constexpr void describeStatic(P& put) { airTree::cardLine(put); } + template static constexpr void describeStatic(P& put) { airTree::describeStatic(put); } }; using Ops = bmpm::TreeOps; diff --git a/test/wiring/build.sh b/test/wiring/build.sh index f4772d3..0fd204a 100755 --- a/test/wiring/build.sh +++ b/test/wiring/build.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # The wiring spec dry run (OneMachine Redrawn, experiment 4): test/wiring/check_wiring.py against the simulated device (test/link/tree_device.cpp). -# Needs python3 (3.11+: tomllib). +# Needs python3 (3.11+: tomllib) and the host's g++ (check_wiring.py builds examples/spi/describe.cpp for the facts and the description). set -e cd "$(dirname "$0")" INC="-I ../../include -I ../../../HAPI/include -I ../../../OneBus/include -I ../../../OneData/include -I ../../../OneMenu/include -I ../../../OneItem/include -I ../../../OneOutput/include -I ../../../OneBit/include -I ../../../OnePin/include -I ../../../OneChip/include -I ../../../OneParse/include -I ../../../OneInput/include -I ../../../OneIO/include" OUT=$(mktemp -d); trap 'rm -rf "$OUT"' EXIT g++ -std=c++17 -O1 $INC ../link/tree_device.cpp -o "$OUT/tree_device" -python3 check_wiring.py "$OUT/tree_device" ../support/arduino_stub +python3 check_wiring.py "$OUT/tree_device" diff --git a/test/wiring/check_wiring.py b/test/wiring/check_wiring.py index 40d522b..0ea37f9 100644 --- a/test/wiring/check_wiring.py +++ b/test/wiring/check_wiring.py @@ -1,14 +1,19 @@ -"""The wiring spec dry run (python/onemachine/wiring.py): the rig's spec (examples/spi/wiring/rig.toml) is sound, its emitted composition is the -sketch's (examples/spi/src/main.cpp), and the device's own description ('d' from the simulated device, test/link/tree_device.cpp) is what it wires. -A spec with the RC522's IRQ on D4 fails with a sentence, and the C++ rejects the same wiring (irq::Esp8266Line). Specs that differ from the -device by an address, the machines' order or the codes' order are told by the diff. - python3 check_wiring.py """ -import os, re, struct, subprocess, sys, tempfile +"""The wiring spec dry run (python/onemachine/wiring.py, experiments 4 and 6), on facts the C++ declares: OneChip's Esp8266Pins, the buses' lines, +the BMP280's and RC522's manifests (examples/spi/describe.cpp writes them to facts.txt beside the description). + - the rig's spec (examples/spi/wiring/rig.toml) is sound; the Wiring it emits, built in place of the hand-written one (air_tree.h), gives the same + description, to its hash; its codes and published nodes are air_tree.h's; the device built from these types describes itself by that hash + - the description's wiring lines are what the spec wires (diff); another address, another order of machines or codes, two MCU lines swapped + between strap pins are each told + - the strap rule goes by who drives the line: the MCU's lines may sit on D3, D4, D8; SDA (shared) may not; the RC522's IRQ on D4 is refused with + the reason, and the same wiring emitted anyway does not build (air_tree.h's rule, from the manifests' roles and OneChip's facts) + python3 check_wiring.py """ +import os, re, struct, sys, tempfile D = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.normpath(os.path.join(D, '..', '..')) sys.path.insert(0, os.path.join(ROOT, 'python')) from onemachine import StreamLink from onemachine import wiring +from onemachine.tree import wiring as wiring_lines failures = 0 def check(cond, what): @@ -16,75 +21,83 @@ def check(cond, what): if not cond: failures += 1; print('FAIL:', what) RIG = os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig.toml') -spec = wiring.load(RIG) - -# ---- the rig's spec is sound, and its composition is the one written by hand in the sketch --------------------------------------------------- -check(wiring.check(spec) == [], 'rig.toml: %r' % wiring.check(spec)) -board = wiring.Board('wemos-d1-mini') -check(board.gpio('D4') == 2 and board.pins['D4']['strap'] and board.pins['D0']['noirq'] and board.straps() == ['D3', 'D4', 'D8'], - 'the board from OneChip: D4 is GPIO2, a strap; D0 has no interrupt; the straps are D3 D4 D8: %r' % board.straps()) -main = re.sub(r'//[^\n]*', '', open(os.path.join(ROOT, 'examples', 'spi', 'src', 'main.cpp')).read()) -flat = re.sub(r'\s+', '', main) -lines = wiring.emit(spec)[1:] -missing = [] -for l in lines: - frag = l.split('=', 1)[1].rstrip(';') if l.startswith('using ') else l # a `using X = ...`: what it is, whatever the sketch calls it - if re.sub(r'\s+', '', frag) not in flat: missing.append(l) -check(missing == [], 'every emitted line is in the sketch: missing %r' % missing) -print('emitted %d lines, all in examples/spi/src/main.cpp' % len(lines)) - -# ---- the device describes what the spec wires -------------------------------------------------------------------------------------------- -link = StreamLink.popen([sys.argv[1]]) -st, data = link.call('d', b'') -desc = data.decode() -check(st == 0 and wiring.diff(spec, desc) == [], 'diff against the device: %r' % wiring.diff(spec, desc)) - +spec, raw = wiring.load(RIG), open(os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig.toml')).read() def variant(text): f = tempfile.NamedTemporaryFile('w', suffix='.toml', delete=False); f.write(text); f.close() s = wiring.load(f.name); os.unlink(f.name); return s -raw = open(RIG).read() -# another address: a sound wiring, but not this device's -s77 = variant(raw.replace('addr = 0x76', 'addr = 0x77')) -check(wiring.check(s77) == [], '0x77 is an address a BMP280 can have') -d = wiring.diff(s77, desc) -check(len(d) == 5 and all('1/118/' in m and '1/119/' in m for m in d), 'the device is at 0x76: every air code differs, by its path: %r' % d) +with tempfile.TemporaryDirectory() as hand, tempfile.TemporaryDirectory() as gen: + # ---- the facts and the description, from the hand-written Wiring ---------------------------------------------------------------------- + rc, path = wiring.build(hand) + check(rc == 0 and path.endswith('.txt'), 'describe.cpp: %r' % path) + facts = wiring.Facts.load(hand) + desc = open(path).read() + h = os.path.basename(path)[:8] + check(facts.pins['D4'] == dict(gpio=2, strap=True, level='high', noirq=False) and facts.pins['D8']['level'] == 'low' and facts.pins['D0']['noirq'] + and facts.straps() == ['D3', 'D4', 'D8'], 'the board, from OneChip\'s Esp8266Pins: %r' % facts.pins) + check(facts.drivers['rc522']['lines'] == {'cs': 'mcu', 'rst': 'mcu', 'irq': 'device'} and facts.drivers['rc522']['ids'][:2] == (0x91, 0x92) + and facts.drivers['bmp280']['addrs'] == (0x76, 0x77) and facts.drivers['bmp280']['ids'] == (0x58, 0x60) + and facts.drivers['bmp280']['nodes']['ctrl/ctrl_meas'] == ((3, 1), 'reg', None) and facts.buses['i2c'] == {'sda': 'shared', 'scl': 'mcu'}, + 'the manifests, from the drivers: %r' % facts.drivers) + + # ---- the rig's spec: sound; its Wiring is the hand-written one, to the description's hash ------------------------------------------------ + check(wiring.check(spec, facts) == [], 'rig.toml: %r' % wiring.check(spec, facts)) + lines = wiring.emit(spec, facts) + hdr = os.path.join(gen, 'wiring.h'); open(hdr, 'w').write(wiring.wiring_header(lines)) + rc, path2 = wiring.build(gen, hdr) + check(rc == 0 and os.path.basename(path2) == os.path.basename(path) and open(path2).read() == desc, + 'the emitted Wiring builds the same description: %s against %s' % (os.path.basename(path2), os.path.basename(path))) + tree = re.sub(r'\s+', '', re.sub(r'//[^\n]*', '', open(os.path.join(ROOT, 'examples', 'spi', 'src', 'air_tree.h')).read())) + rest = lines[lines.index('};') + 1:] + missing = [l for l in rest if re.sub(r'\s+', '', l) not in tree] + check(missing == [], 'the emitted codes and published nodes are air_tree.h\'s: missing %r' % missing) + print('emitted Wiring builds description %s, the hand-written one\'s; %d code and node lines, all in air_tree.h' % (os.path.basename(path2), len(rest))) -# the machines in another order (air first): the path's first number is the machine's position in the App -a, b = raw.index('[parts.rfid]'), raw.index('[parts.air]'); c = raw.index('# What the App publishes') -order = variant(raw[:a] + raw[b:c] + raw[a:b] + raw[c:]) -d = wiring.diff(order, desc) -check(any(m.startswith('temp: the device has it at 1/118/0, the spec at 0/118/0') for m in d) and any(m.startswith('card: the device has it at 0/1, the spec at 1/1') for m in d), - 'machines in another order: the paths differ: %r' % d) + # ---- the device built from the same types describes itself by that hash; the build output's description is what the spec wires ---------- + link = StreamLink.popen([sys.argv[1]]) + st, data = link.call('d', b'') + check(st == 0 and data.startswith(('hash ' + h).encode()), 'the device\'s hash is the build output\'s: %r, %s' % (data[:13], h)) + check(wiring.diff(spec, facts, desc) == [], 'diff: %r' % wiring.diff(spec, facts, desc)) + w = wiring_lines(desc) + check(w['rfid'] == {'driver': 'rc522', 'cs': (15, 'mcu'), 'rst': (2, 'mcu'), 'irq': (16, 'device')} and w['air'] == {'driver': 'bmp280', 'at': 0x76} + and w['empty'] == {'cs': (0, 'mcu')}, 'the wiring lines: %r' % w) -# the codes in another order: a code's number is its position -swapped = variant(raw.replace('temp = "air/temp"\npress = "air/press"', 'press = "air/press"\ntemp = "air/temp"')) -d = wiring.diff(swapped, desc) -check(len(d) == 1 and d[0].startswith("codes:"), 'codes in another order: %r' % d) + # ---- differences the diff tells ----------------------------------------------------------------------------------------------------- + d = wiring.diff(variant(raw.replace('addr = 0x76', 'addr = 0x77')), facts, desc) + check(len(d) == 6 and sum('1/118/' in m for m in d) == 5 and any(m.startswith('wiring air: the device is at 0x76, the spec at 0x77') for m in d), + 'another address: 5 paths and the wiring line: %r' % d) + a, b, c = raw.index('[parts.rfid]'), raw.index('[parts.air]'), raw.index('# What the App publishes') + d = wiring.diff(variant(raw[:a] + raw[b:c] + raw[a:b] + raw[c:]), facts, desc) + check(any(m.startswith('temp: the device has it at 1/118/0, the spec at 0/118/0') for m in d), 'machines in another order: %r' % d) + d = wiring.diff(variant(raw.replace('temp = "air/temp"\npress = "air/press"', 'press = "air/press"\ntemp = "air/temp"')), facts, desc) + check(len(d) == 1 and d[0].startswith('codes:'), 'codes in another order: %r' % d) + swapped = variant(raw.replace('rst = "D4"', 'rst = "D3"').replace('empty_cs = ["D3"]', 'empty_cs = ["D4"]')) + check(wiring.check(swapped, facts) == [], 'RST on D3 and the empty slot on D4: both the MCU\'s lines, on strap pins: allowed') + d = wiring.diff(swapped, facts, desc) + check(sorted(m.split(':')[0] for m in d) == ['wiring empty cs', 'wiring rfid rst'], 'and the diff tells the two pins: %r' % d) -# a pin used twice, a driver that cannot be there, a node that does not exist -bad = variant(raw.replace('scl = "D1"', 'scl = "D2"').replace('addr = 0x76', 'addr = 0x40').replace('"air/ctrl/config"', '"air/ctrl/cfg"')) -e = wiring.check(bad) -check(any('i2c.scl on D2: i2c.sda is on it already' in x for x in e) and any('answers at 0x76 or 0x77, not 0x40' in x for x in e) - and any("has no node 'ctrl/cfg'" in x for x in e), 'three faults, three sentences: %r' % e) + # ---- the strap rule by role --------------------------------------------------------------------------------------------------------- + e = wiring.check(variant(raw.replace('sda = "D2"', 'sda = "D3"').replace('empty_cs = ["D3"]', 'empty_cs = []')), facts) + check(len(e) == 1 and e[0].startswith('i2c.sda on D3 (GPIO0): a boot strap pin, and this line is driven by the part and the MCU (shared)'), + 'SDA (shared) on D3: refused: %r' % e) + bad = variant(raw.replace('scl = "D1"', 'scl = "D2"').replace('addr = 0x76', 'addr = 0x40').replace('"air/ctrl/config"', '"air/ctrl/cfg"')) + e = wiring.check(bad, facts) + check(any('i2c.scl on D2: i2c.sda is on it already' in x for x in e) and any('answers at 0x76 or 0x77, not 0x40' in x for x in e) + and any("has no node 'ctrl/cfg'" in x for x in e), 'three faults, three sentences: %r' % e) -# ---- the RC522's IRQ on D4: the spec is refused with the reason, and the C++ refuses the same wiring ------------------------------------------ -d4 = wiring.load(os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig_irq_d4.toml')) -e = wiring.check(d4) -check(len(e) == 1 and e[0].startswith('rfid.irq on D4 (GPIO2): a boot strap pin') and 'ROM (download) mode' in e[0], 'D4: %r' % e) -print('rig_irq_d4.toml: error: ' + (e[0] if e else '(none)')) -try: wiring.emit(d4); check(False, 'emit refuses what check refuses') -except wiring.WiringError as x: check(x.errors == e, 'emit says the same') -irqline = [l for l in wiring.emit(d4, checked=False) if l.startswith('using RfidLine')] -check(irqline == ['using RfidLine = irq::Sampled<2>;'], 'emitted anyway: %r' % irqline) -with tempfile.TemporaryDirectory() as t: - src = os.path.join(t, 'irq_d4.cpp') - open(src, 'w').write('#include \n#include "irq_esp8266.h"\n%s\nint main() { RfidLine::begin(); return RfidLine::ready(); }\n' % irqline[0]) - r = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17', '-fsyntax-only', '-I', sys.argv[2], '-I', os.path.join(ROOT, 'examples', 'spi', 'src'), src], - capture_output=True, text=True) - msg = 'an interrupt line on GPIO0, GPIO2 or GPIO15 (D3, D4, D8)' - check(r.returncode != 0 and msg in r.stderr, 'the C++ rejects it with irq::Esp8266Line\'s message: %s' % r.stderr[-300:]) - print('and the C++ build: %s' % (msg if msg in r.stderr else '(not rejected)')) + d4 = wiring.load(os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig_irq_d4.toml')) + e = wiring.check(d4, facts) + check(len(e) == 1 and e[0].startswith('rfid.irq on D4 (GPIO2): a boot strap pin, and this line is driven by the part') and 'ROM (download) mode' in e[0] + and 'needs GPIO2 high at reset' in e[0], 'D4: %r' % e) + print('rig_irq_d4.toml: error: ' + (e[0] if e else '(none)')) + try: wiring.emit(d4, facts); check(False, 'emit refuses what check refuses') + except wiring.WiringError as x: check(x.errors == e, 'emit says the same') + with tempfile.TemporaryDirectory() as bad: + hdr = os.path.join(bad, 'wiring.h'); open(hdr, 'w').write(wiring.wiring_header(wiring.emit(d4, facts, checked=False))) + rc, err = wiring.build(bad, hdr) + msg = 'wiring: a line the part drives is on a boot strap pin' + check(rc != 0 and msg in err, 'the emitted Wiring does not build: %s' % err[-400:]) + print('and the C++ build: %s' % (msg if msg in err else '(not rejected)')) print('FAILED: %d' % failures if failures else 'OK: wiring spec dry run') sys.exit(1 if failures else 0) From 661d9fb33e1622e2e1190543c01155e94d825024 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Mon, 5 Oct 2026 15:06:40 +0000 Subject: [PATCH 19/19] discover: reviveGone(), a Gone row is looked at again at its declared 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 --- examples/spi/README.md | 6 ++ examples/spi/src/main.cpp | 6 +- examples/spi/wiring/rig.toml | 2 +- include/oneMachine/discover/driver.h | 4 +- include/oneMachine/discover/identify.h | 2 + include/oneMachine/discover/registry.h | 29 +++++++++ include/oneMachine/discover/spi.h | 7 +++ test/discover/revive.cpp | 82 ++++++++++++++++++++++++++ test/native.sh | 1 + 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 test/discover/revive.cpp diff --git a/examples/spi/README.md b/examples/spi/README.md index b7a1ce8..11d7ffb 100644 --- a/examples/spi/README.md +++ b/examples/spi/README.md @@ -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` sets a @@ -115,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 diff --git a/examples/spi/src/main.cpp b/examples/spi/src/main.cpp index 918072d..3554df0 100644 --- a/examples/spi/src/main.cpp +++ b/examples/spi/src/main.cpp @@ -152,7 +152,7 @@ using RfidConsumers = Chain; struct RfidApp : discover::World, 3, discover::SpiScan, Chain>>, fail::DeviceOwnStale { - static constexpr bool lifecycle = true; + static constexpr bool lifecycle = true, revive = true; static void release(RowId) {} static void unbindAll() {} using Health = fail::HealthT; @@ -187,7 +187,7 @@ struct AirApp; using Bmp = airTree::Machine; // the sensor at 0x76: its Criteria using AirDrivers = Chain; struct AirApp : discover::World, Bmp::Entries, 3, discover::I2cScan>, fail::BusEdge { - static constexpr bool lifecycle = true; + static constexpr bool lifecycle = true, revive = true; static void release(RowId) {} static void unbindAll() {} static void busReset() { Twi::begin(); } @@ -416,6 +416,8 @@ void loop() { logAir(); logHealth(); if (int32_t(now - nextAir) >= 0) { nextAir = now + 1000; AirApp::pump(); bmpm::PublishAll::sync(); } + static uint32_t nextRevive = 5000; // a part that went Gone is looked for again, at its declared address, every 5 s + if (int32_t(now - nextRevive) >= 0) { nextRevive = now + 5000; RfidApp::reviveGone(); AirApp::reviveGone(); } static uint32_t nextIrq = 5000; if (int32_t(now - nextIrq) >= 0) { nextIrq = now + 5000; IrqCounters::report(now); LoopStats::report(now); } } diff --git a/examples/spi/wiring/rig.toml b/examples/spi/wiring/rig.toml index e43f72b..d821832 100644 --- a/examples/spi/wiring/rig.toml +++ b/examples/spi/wiring/rig.toml @@ -19,7 +19,7 @@ empty_cs = ["D3"] # a chip select declared with nothing on it: th [parts.rfid] driver = "rc522" bus = "spi" -cs = "D8" +cs = "D8" # GPIO15, low at reset: the RC522 can hold SDA high and the board boots in SDIO mode, so a 10k in series in this wire rst = "D4" # held high by the sketch; low resets the reader irq = { pin = "D0", mode = "push-pull", delivery = "sampled" } diff --git a/include/oneMachine/discover/driver.h b/include/oneMachine/discover/driver.h index 8557649..a825df4 100644 --- a/include/oneMachine/discover/driver.h +++ b/include/oneMachine/discover/driver.h @@ -79,7 +79,9 @@ namespace discover { } static void found(uint8_t addr, RowId bus) { - RowId self = W::reg.add(addr, instOf(), bus, false); + RowId self; + if constexpr (W::reviveRows) self = W::reg.addReusing(addr, instOf(), bus); // a part that came back takes its Gone row again + else self = W::reg.add(addr, instOf(), bus, false); if constexpr (Impl::isBridge) { Impl::clear(addr); // a bridge may keep a stale selection across a warm reset if (self != noRow) diff --git a/include/oneMachine/discover/identify.h b/include/oneMachine/discover/identify.h index 7543a51..4196aa5 100644 --- a/include/oneMachine/discover/identify.h +++ b/include/oneMachine/discover/identify.h @@ -239,6 +239,8 @@ namespace discover { for (uint8_t a = 0x08; a <= 0x77; ++a) if (!Self::claimedUpstream(bus, a)) Ident::template Pass::at(a, bus); } + // the declared address of a row that went Gone, looked at again by the same entries + template static void revive(RowId bus, uint8_t addr) { Identify::template Pass::at(addr, bus); } }; } diff --git a/include/oneMachine/discover/registry.h b/include/oneMachine/discover/registry.h index 7a3a407..52ec693 100644 --- a/include/oneMachine/discover/registry.h +++ b/include/oneMachine/discover/registry.h @@ -64,6 +64,15 @@ namespace discover { void reset() { count = 0; overflow = 0; add(0, nullptr, noRow, true); } + // a device row: the Gone row of the same address on the same bus is taken again (a part that came back), else a new one + RowId addReusing(BusIdT busId, IDriver* drv, RowId parent) { + for (RowId r = 0; r < count; ++r) { + Row& g = rows[r]; + if (!g.isBus && g.parent == parent && g.busId == busId && g.st_ == uint8_t(Status::Gone)) { g.drv = drv; g.st_ = uint8_t(Status::Alive); return r; } + } + return add(busId, drv, parent, false); + } + RowId add(BusIdT busId, IDriver* drv, RowId parent, bool isBus) { if (count >= N) { ++overflow; return noRow; } Row& r = rows[count]; @@ -99,6 +108,10 @@ namespace discover { template struct LifecycleOf : std::false_type {}; template struct LifecycleOf> : std::bool_constant {}; + // an app opts in to reviving Gone rows with `static constexpr bool revive = true;` (and a Scan that has revive(bus, busId)): World::reviveGone() + template struct ReviveOf : std::false_type {}; + template struct ReviveOf> : std::bool_constant {}; + // Self is the concrete application type (CRTP): drivers name it before it is complete. template struct World { @@ -168,6 +181,22 @@ namespace discover { // as if they sat on this bus; then everything else -- Scan's own concern, the registry only routes first static void scan(RowId bus) { route(bus); Scan::template run(bus); } + // an app that revives rows takes the Gone row of a part that came back again (found() reads this) + static constexpr bool reviveRows = ReviveOf::value; + + // Gone is final until the next discovery; this is a small one: each Gone device row's declared address is checked again, by the same entries + // (presence and identity), and a part that answers takes its row again (driver init, consumers bound). Call it slowly. + static void reviveGone() { + static_assert(ReviveOf::value, "reviveGone needs `static constexpr bool revive = true;` in the app"); + for (RowId r = 0; r < reg.count; ++r) { + const auto& row = reg.rows[r]; + if (row.isBus || row.status() != Status::Gone) continue; + const RowId bus = row.parent; const BusIdT id = row.busId; + route(bus); + Scan::template revive(bus, id); + } + } + // breadth-first: rows appended while scanning are visited by the same loop static void discover() { reg.reset(); diff --git a/include/oneMachine/discover/spi.h b/include/oneMachine/discover/spi.h index 0392d02..cfbaf87 100644 --- a/include/oneMachine/discover/spi.h +++ b/include/oneMachine/discover/spi.h @@ -91,6 +91,8 @@ namespace discover { if (bus != rootRow) return; // an SPI bus has no bridges: one root, its slots Run::all(bus); } + // the slot of a row that went Gone, looked at again by the same entries + template static void revive(RowId bus, uint8_t slot) { Run::one(slot, bus); } private: template struct Run; @@ -108,6 +110,11 @@ namespace discover { } } + static void one(uint8_t slot, RowId bus) { + const bool fixed = ((fixedSlotOf() == slot) || ...); + (void)(tryEntry(slot, bus, fixed) || ...); + } + template static bool tryEntry(uint8_t slot, RowId bus, bool fixed) { if constexpr (IsFixed::value) { if (slot != X::Probe::slot) return false; diff --git a/test/discover/revive.cpp b/test/discover/revive.cpp new file mode 100644 index 0000000..73e9eaa --- /dev/null +++ b/test/discover/revive.cpp @@ -0,0 +1,82 @@ +// A row that went Gone is looked at again, at its declared address, by the same entries: a part that answers takes its row again (the slot is +// reused, the driver's init runs), a part that does not stays Gone. An app without `revive` is as before: Gone is final until discover(). +#include +#include +#include +#include +#include +#include "../support/mockTwi.h" +#include "../support/sensors.h" + +using discover::RowId; +using discover::Status; +using hapi::Chain; + +struct Seen { + using Accepts = Chain; + inline static uint8_t n = 0; + template struct Body { template struct Part : T { using T::T; void on(const discover::Sample&) { ++n; } }; }; +}; + +template struct App; +template using Drivers = Chain>>; +template struct App : discover::World, mock::Twi, Chain, Drivers, 4, discover::I2cScan> { + static constexpr bool lifecycle = true, revive = Revive; + static void release(RowId) {} + static void unbindAll() {} +}; + +static int failures = 0; +#define CHECK(c) do { if (!(c)) { ++failures; std::printf("FAIL line %d: %s\n", __LINE__, #c); } } while (0) + + + +int main() { + using A = App; + auto& dev = mock::Bus::devs[1]; // the SensorB at 0x40 on the root bus + A::discover(); + CHECK(A::reg.count == 2 && A::reg.status(1) == Status::Alive && A::reg.rows[1].busId == 0x40); + const uint8_t row0 = A::reg.count; + + dev.addr = 0x41; // unplugged: nothing answers at 0x40 + A::setStatus(1, Status::Gone); + CHECK(A::reg.status(1) == Status::Gone); + + A::reviveGone(); // still unplugged: it stays Gone, no row is added + CHECK(A::reg.status(1) == Status::Gone && A::reg.count == row0); + A::reviveGone(); A::reviveGone(); + CHECK(A::reg.status(1) == Status::Gone && A::reg.count == row0); + + dev.addr = 0x40; // replugged + A::reviveGone(); + CHECK(A::reg.status(1) == Status::Alive && A::reg.count == row0 && A::reg.rows[1].busId == 0x40 && A::reg.overflow == 0); + Seen::n = 0; A::pump(); + CHECK(Seen::n == 1); // polled again: its Temperature reaches the consumer + + // and again: gone, back, gone, back, always the same row + for (int i = 0; i < 3; ++i) { + dev.addr = 0x41; A::setStatus(1, Status::Gone); A::reviveGone(); + CHECK(A::reg.status(1) == Status::Gone && A::reg.count == row0); + dev.addr = 0x40; A::reviveGone(); + CHECK(A::reg.status(1) == Status::Alive && A::reg.count == row0); + } + + // another part at the address: the entries decide, as at discovery (SensorB's id is 0xB2) + dev.addr = 0x41; A::setStatus(1, Status::Gone); + const uint8_t was = dev.regs[0]; dev.regs[0] = 0x55; dev.addr = 0x40; + A::reviveGone(); + CHECK(A::reg.status(1) == Status::Gone); + dev.regs[0] = was; + + // without `revive` the status stays final: the registry is as it was + using B = App; + B::discover(); + dev.addr = 0x41; B::setStatus(1, Status::Gone); dev.addr = 0x40; + CHECK(B::reg.status(1) == Status::Gone); + B::discover(); + CHECK(B::reg.status(1) == Status::Alive); + + if (failures) { std::printf("revive: %d failed\n", failures); return 1; } + std::printf("OK: revive, a Gone row comes back at its declared address\n"); + return 0; +} diff --git a/test/native.sh b/test/native.sh index 0de1b13..ab31492 100755 --- a/test/native.sh +++ b/test/native.sh @@ -18,6 +18,7 @@ PROGRAMS=( discover/round2:-DR2_NO_MQTT discover/round3 discover/round3b + discover/revive discover/spi1 discover/spi_fail discover/spi_health