diff --git a/examples/spi/README.md b/examples/spi/README.md index ec34f36..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 @@ -73,10 +76,12 @@ m.changes() # [Change('temp', 27.61, m.status('air') # 'alive', 'stale' or 'gone': the part's row; m.temp raises Stale when it is not alive ``` -Ops: `d` the description (each code with the status of its row), `v` get by code (the status first, then the value; a part that is not alive answers its last value), `w` set by code (through the node: its limits, its capture, its register), `n` the changes since the last `n` (a value, or a status change of a row), `f` one -fault key (`x` resets the air sensor behind the host's back, `v` and `p` reset the RFID reader). The changes wait in a `fail::Buffer` of 8: when a consumer -does not read for a while the newest are refused and counted, and the reply of `n` says how many it missed (`m.missed`); read the values again with -`m.temp`. `examples/spi/rig_session.py` is a session on the real board; `test/link/build.sh` runs the same consumer against a simulated one. +Ops: `d` the description by its hash and each code's status (the text is in the build output: `.pio/build//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 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 @@ -113,6 +118,9 @@ first three bytes (cascade level 1 only). answering goes Stale, is probed, and is initialised again when it answers; one that was reset without the sketch knowing (its configuration read back is gone) is initialised again at once. `test/discover/spi_fail.cpp` runs both against the register model with RST driven. `HealthT` is not composed here. +- A part that goes Gone (retries and probes spent) is looked for again every 5 s at its declared address, by the same entries + (`World::reviveGone()`, enabled by `revive = true` in the app): when it answers it takes its row again, its init runs, and the + value it was wanted to hold is written back. `test/discover/revive.cpp` runs it on the mock bus. - The BMP280 on I2C has no failure edge: a supply disturbance that resets it leaves it in sleep mode, silent. ## Faults diff --git a/examples/spi/describe.cpp b/examples/spi/describe.cpp new file mode 100644 index 0000000..0d6e290 --- /dev/null +++ b/examples/spi/describe.cpp @@ -0,0 +1,85 @@ +// The description of the spi example's link, written at build time from the same types the firmware is built from (src/air_tree.h): the device +// answers 'd' with the hash only, and the consumer reads the text from here (python: Tree(link, descriptions=)). +// 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 +#include +#include "src/air_tree.h" + +struct NoApp {}; // the description needs the machine's types only: no bus, no registry +using M = airTree::Machine; +static void noNote(int32_t) {} +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::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); + uint32_t h = 2166136261u; for (char c : t.s) h = (h ^ uint8_t(c)) * 16777619u; + constexpr uint32_t want = folded(); + if (h != want) { fprintf(stderr, "describe: the text hashes to %08x, the compile-time fold to %08x\n", unsigned(h), unsigned(want)); return 1; } + char path[4096]; snprintf(path, sizeof path, "%s/%08x.txt", argv[1], unsigned(h)); + FILE* f = fopen(path, "w"); if (!f) { perror(path); return 1; } + fwrite(t.s.data(), 1, t.s.size(), f); fclose(f); + char fpath[4096]; snprintf(fpath, sizeof fpath, "%s/facts.txt", argv[1]); + const std::string fx = facts(); + FILE* g = fopen(fpath, "w"); if (!g) { perror(fpath); return 1; } + fwrite(fx.data(), 1, fx.size(), g); fclose(g); + printf("%s\n", path); + return 0; +} 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..402a14a --- /dev/null +++ b/examples/spi/src/air_tree.h @@ -0,0 +1,103 @@ +// What the spi example publishes of its air sensor: the codes, the published nodes and the card's line, in one place. The sketch (main.cpp), the +// simulated device (test/link/tree_device.cpp), the AVR image (test/link/avr_tree.cpp) and the description generator (examples/spi/describe.cpp) +// all build them from here, so the description the generator writes is the one the firmware's hash is computed from. +// M the machine (bmpm::Machine) +// 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 +#include +#include +#include "bmp280_machine.h" +#include "rc522.h" + +namespace airTree { + + // the codes, numbered by their position in the description (the link's change records use the numbers) + struct CodeTemp { static constexpr uint8_t num = 0; ONEMACHINE_STATE_NAME(name, "temp"); }; + struct CodePress { static constexpr uint8_t num = 1; ONEMACHINE_STATE_NAME(name, "press"); }; + struct CodeAir { static constexpr uint8_t num = 2; ONEMACHINE_STATE_NAME(name, "air"); }; + struct CodeConfig { static constexpr uint8_t num = 3; ONEMACHINE_STATE_NAME(name, "air/config"); }; + struct CodeCtrlMeas { static constexpr uint8_t num = 4; ONEMACHINE_STATE_NAME(name, "air/ctrl_meas"); }; + struct CodeCard { static constexpr uint8_t num = 5; ONEMACHINE_STATE_NAME(name, "card"); }; + + // ---- the wiring: the rig's pins, the one place the sketch takes them from (experiment 6) ------------------------------------------------ + // The board's pins and their facts are OneChip's (Esp8266Pins); who drives each line is the drivers' manifests' (rc522::Manifest, bmpm::Manifest) + // and the buses' (discover::I2cLines, SpiLines). The rules below are the wiring check at compile time; python/onemachine/wiring.py checks a + // wiring spec by the same facts, which describe.cpp prints from these types. +#ifdef AIRTREE_WIRING + #include AIRTREE_WIRING // a Wiring emitted from a wiring spec (python3 -m onemachine.wiring emit): the rules below judge it as they judge this one +#else + struct Wiring { + using Board = hw::esp8266::Esp8266Pins; + static constexpr const char* board = "wemos-d1-mini"; + static constexpr uint8_t sda = Board::D2, scl = Board::D1; // I2C + static constexpr uint8_t sck = Board::D5, miso = Board::D6, mosi = Board::D7; // SPI (the ESP8266's HSPI pins) + static constexpr uint8_t rfidCs = Board::D8, emptyCs = Board::D3; // the SPI slots: the reader, and one declared with nothing on it + static constexpr uint8_t rfidRst = Board::D4, rfidIrq = Board::D0; + static constexpr uint8_t airAddr = 0x76; + }; +#endif + // a line that the part drives (or shares) must not be on a pin the chip samples at reset; the MCU's own lines may be (D3 and D8 as chip selects) + template 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 + template class Note> using Pubs = hapi::Chain< + 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'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'); } + + // ---- 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 68c951b..05cbe5e 100644 --- a/examples/spi/src/bmp280_machine.h +++ b/examples/spi/src/bmp280_machine.h @@ -16,12 +16,12 @@ // // 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. // // What is specific to this device and what is not: Machine (the nodes, the registers' defaults, the identity hash, the driver) is the -// BMP280's. PathRef, Published/PublishedAt, the register mimic (Reg), Capture/RestoreEnd, Limits, the description walk and the status of a machine's row +// BMP280's. PathRef, Published/PublishedAt, the register mimic (Reg), Reconcile, Limits, the description walk and the status of a machine's row // are written against a machine and not this one; they stay here until a second device is described the same way (the RC522, as a machine), which will // show which of them are the framework's and which are this machine's. tree_ops.h, the link's ops over the published nodes, is in the same position. #pragma once @@ -33,6 +33,7 @@ #include #include #include +#include namespace bmpm { @@ -41,6 +42,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; }; @@ -56,22 +69,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; } }; }; @@ -94,6 +106,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; @@ -101,7 +114,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; @@ -124,7 +137,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>>>; @@ -170,12 +183,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) { @@ -192,7 +204,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() { @@ -200,14 +212,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; } @@ -262,14 +272,14 @@ 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 { 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(); } @@ -285,7 +295,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); @@ -300,7 +310,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 ------------------------------------------------------------------------------- @@ -317,8 +327,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. @@ -334,7 +344,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 @@ -414,6 +424,47 @@ namespace bmpm { 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"); + 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); } } + } + 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(); put('\n'); + } + 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 void describe(P& put, uint8_t bus) { Walk

w{put}; 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 f54d08e..3554df0 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 @@ -54,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; @@ -66,17 +74,11 @@ 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>; -// 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 @@ -138,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>; @@ -150,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; @@ -172,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; @@ -182,10 +184,10 @@ 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; + static constexpr bool lifecycle = true, revive = true; static void release(RowId) {} static void unbindAll() {} static void busReset() { Twi::begin(); } @@ -196,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('-'); @@ -204,17 +207,14 @@ 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 + 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>>; -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 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; struct SerialPut { void operator()(char c) { Log.write(c); } }; @@ -227,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) @@ -346,11 +348,12 @@ struct Extra { // a code that only notifies: the card (an event with a value, 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 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::describeStatic(put); } }; using Ops = bmpm::TreeOps; struct SerialOut { static void put(uint8_t b) { Serial.write(b); } }; @@ -413,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/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/src/tree_ops.h b/examples/spi/src/tree_ops.h index 5f07ab5..63898a2 100644 --- a/examples/spi/src/tree_ops.h +++ b/examples/spi/src/tree_ops.h @@ -1,19 +1,30 @@ // 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) +// '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 +// (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: 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 -// 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 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) -// 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 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): every bit is set, and the reply is made as any other. #pragma once #include #include @@ -23,7 +34,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 +49,20 @@ 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 (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 bits[bytes] = {}; + inline static uint16_t seq = 0, served = 0; + 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 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 @@ -66,8 +90,23 @@ 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 (a record has room for 64)"); - template static void describe(P& put) { bmpm::describe(put, Bus); Extra::describe(put); } + // 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); +#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) { @@ -90,6 +129,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; @@ -115,7 +167,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::mark(c); last[c] = s; } seeded = true; @@ -158,8 +210,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; @@ -169,16 +227,29 @@ 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; } case 'n': { + if (n != 2) { r.status(role::LinkBadLength); return; } + 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(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; + 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] = uint8_t(resync | ((State::any() || !q.empty()) << 1)); return; } default: r.status(role::LinkUnknown); return; diff --git a/examples/spi/wiring/rig.toml b/examples/spi/wiring/rig.toml new file mode 100644 index 0000000..d821832 --- /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 and their facts: OneChip's Esp8266Pins, read through facts.txt (examples/spi/describe.cpp) + +[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" # 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" } + +[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/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/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/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 c7a8d5f..cfbaf87 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; }; @@ -89,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; @@ -106,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/python/onemachine/tree.py b/python/onemachine/tree.py index ab84c24..0eb08f9 100644 --- a/python/onemachine/tree.py +++ b/python/onemachine/tree.py @@ -8,19 +8,26 @@ 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 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 +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 os, 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 +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 @@ -45,11 +52,33 @@ 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 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(' ') @@ -80,9 +109,14 @@ 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) + 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''): @@ -93,16 +127,42 @@ def refresh(self): st, data = self._call('d') if st != OK: raise LinkError('description: status %d' % st) text = data.decode() + 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) + 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, '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}) 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 _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('> 6, data[i] & 0x3F, struct.unpack_from('] 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 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 and with ids its driver knows + - every published code names a node (or the event) its part has +""" +import os, re, subprocess, sys, tempfile, tomllib +from .tree import _parse, wiring as _wiring + +HERE = os.path.dirname(os.path.abspath(__file__)) +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 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']] + +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_of(v): return v['pin'] if isinstance(v, dict) else v + +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, {}): 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 = 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['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['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 = 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, 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) + 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(): + 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 = 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 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, 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] + path, kind, dec = facts.drivers[p['driver']]['nodes'][node] + machine = names.index(part) + 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): + 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 args[0] == 'check': + errors = check(spec, facts) + for e in errors: print('error: ' + e) + if not errors: print('ok: %s' % args[1]) + return 1 if errors else 0 + 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' % args[1]) + 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/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/discover/build_spi1.sh b/test/discover/build_spi1.sh index 37124a5..7eea3d0 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. @@ -78,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 @@ -99,7 +100,10 @@ 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 if command -v avr-g++ >/dev/null; then echo; echo "=== avr-g++ $(avr-g++ -dumpversion) -Os atmega328p (linked, real SPI core) ===" 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/link/avr_tree.cpp b/test/link/avr_tree.cpp new file mode 100644 index 0000000..f6e5951 --- /dev/null +++ b/test/link/avr_tree.cpp @@ -0,0 +1,105 @@ +// 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" +#endif +#include "../../examples/spi/src/air_tree.h" + +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 = airTree::Machine; +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; + +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) +#ifdef LINK +#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 + +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 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::describeStatic(put); } +}; +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/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..2c46458 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,23 @@ def op(c, payload=b''): def advance(ms): check(op('t', struct.pack('.txt (facts.txt is beside it) + static = '\n'.join(re.sub(r' status (alive|stale|gone)$', '', l) for l in m.description.split('\n')) + 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') @@ -55,15 +72,20 @@ def reg(a): return op('R', bytes([a]))[1][0] 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(' 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, description %s' % ('ctypes' if CTYPES else 'a pipe', 'by hash' if HASHED else 'as text')) sys.exit(1 if failures else 0) 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" diff --git a/test/link/tree_device.cpp b/test/link/tree_device.cpp index 3430249..330e6b5 100644 --- a/test/link/tree_device.cpp +++ b/test/link/tree_device.cpp @@ -20,6 +20,7 @@ #include #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,32 +44,24 @@ 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); } +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>>; -using PubAir = bmpm::PublishedAt>; -using PubConfig = bmpm::PublishedAt>; -using PubCtrlMeas = bmpm::PublishedAt>; -using Pubs = Chain; +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 void describe(P& put) { - const char* s = " card -> 0/1 notify event ro value u32 status "; while (*s) put(*s++); + 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::describeStatic(put); } }; using Ops = bmpm::TreeOps; diff --git a/test/native.sh b/test/native.sh index e2a08e1..a8affbb 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 diff --git a/test/wiring/build.sh b/test/wiring/build.sh new file mode 100755 index 0000000..0fd204a --- /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) 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" diff --git a/test/wiring/check_wiring.py b/test/wiring/check_wiring.py new file mode 100644 index 0000000..0ea37f9 --- /dev/null +++ b/test/wiring/check_wiring.py @@ -0,0 +1,103 @@ +"""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): + global failures + if not cond: failures += 1; print('FAIL:', what) + +RIG = os.path.join(ROOT, 'examples', 'spi', 'wiring', 'rig.toml') +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 + +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 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) + + # ---- 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) + + # ---- 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) + + 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)