#pragma once #include namespace fgc { // The decisions behind `test hardware`, separated from the I/O that feeds them. // // Each function takes what was read - a sysfs string, an ioctl bitmask, a DUMP // echo - and returns a verdict with the sentence an operator needs. Keeping them // pure is what lets the thresholds be tested without a rig, which matters because // several of them encode traps that cost real debugging time (known-issues #2 // and #19) and must not drift silently. struct CheckResult { bool ran = true; // false => nothing was declared, so nothing was checked bool pass = true; std::string detail; // one line; names the fix when it failed }; // /sys/module/usbcore/parameters/usbfs_memory_mb. // // Reverts to 16 on every reboot unless persisted on the kernel cmdline, and // frame delivery stalls below ~1000 regardless of application code // (known-issues #19). A value that will not parse is a failure, not a pass: // an unreadable node means the check did not happen. CheckResult checkUsbfsMemory(const std::string& sysfs_value, int min_mb); // A usb3_lpm_permit node. U1/U2 link power management must be OFF on the // camera's port or the camera logs frame drops under load; the per-device // usb3_hardware_lpm_u* files are read-only, so this port-level node is the one // that can actually be set (known-issues #19). CheckResult checkUsb3Lpm(const std::string& permit_value); // The I2C_FUNCS ioctl bitmask for the bus the SHT41 is on. // // An SMBus-only adapter opens, binds its slave address, and then fails every // transaction - which reads as a dead sensor rather than as the wrong bus // (known-issues #2). `i2cdetect -F ` is the manual form of this check. // // The bit is spelled out rather than pulled from so this stays a // pure module: I2C_FUNC_I2C is 0x00000001 and is ABI-stable. constexpr unsigned long kI2cFuncI2c = 0x00000001UL; CheckResult checkI2cFunctionality(unsigned long funcs); // What the host computed from hardware.ini for one axis. struct DrivetrainExpectation { bool declared = false; // hardware.ini carries a current for this axis int global_scaler = 0; int run_irun = 0; int homing_irun = 0; }; // One value the host declared and the firmware should be holding. Used for the // parameters the config handshake added, where "not declared" and "not echoed" // both mean the check does not apply rather than that it failed. struct DeclaredValue { const char* name = ""; bool want = false; // hardware.ini declares it long value = 0; long got = -1; // as DUMP echoed it; -1 = absent }; // Does the firmware hold what hardware.ini says? This is the generalisation of // the old "did CENTER land?" check: with the drivetrain owned by the host, every // declared value is one the firmware can be silently wrong about, and a tower // running on stale or defaulted values looks identical to a correct one from the // outside. // // `values` may be empty, which is how an axis with nothing declared reports // "not checked" rather than passing. CheckResult checkDeclaredValues(const std::string& axis, const DeclaredValue* values, int count); // What DUMP echoed back. -1 means the field was absent, which is a firmware // predating the CURRENT command - a version skew, not a current of zero, and // the two must stay distinguishable. struct DrivetrainEcho { long global_scaler = -1; long run_irun = -1; long homing_irun = -1; bool absent() const { return global_scaler < 0 && run_irun < 0 && homing_irun < 0; } }; // Did the firmware adopt what the host sent? Both registers are write-only on // the TMC5160, so this echo is the only confirmation available - and without it // a tower can run its whole life on limp-home defaults while reporting healthy. CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want, const DrivetrainEcho& got); // Is the axis running the host's configuration, or the firmware's limp-home // fallback? // // `USE DEFAULTS` is the deliberate bench escape from NEEDCFG, and it fills the // firmware's cfg_rx mask completely - so an axis on limp-home values reports as // fully configured and passes every state check. What gives it away is the DUMP // `limp` flag, which says where the values came from rather than whether they // arrived. // // checkDeclaredValues() would also catch this, but only as a list of mismatched // numbers, which reads as "the host and firmware disagree" and sends someone to // re-send them. The truthful diagnosis is that nobody ever sent any. // // `limp` < 0 means the firmware predates the flag: reported as not-run, because // an old firmware is a version skew rather than a misconfigured tower. CheckResult checkNotLimpHome(const std::string& axis, long limp); // Is the running firmware built from the wiring hardware.ini declares? // // Pins are not sent over the wire: firmware/tools/gen_wiring.py compiles them in // from the tower's hardware.ini [Pins]. So a mismatch never means a lost SET - // it means the image was built from a different copy of that file, and the fix // is to rebuild and re-flash. The firmware drives whatever pins it was built // with, which on a tower wired differently means a chip-select into a driver // that was never meant to hear it. // // Same shape as checkDeclaredValues: `want` false = not declared, `got` -1 = // not echoed, i.e. a firmware predating the pin echo. CheckResult checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count); // Does the homed travel fit entirely in positive counts? // // `center_count` defines where the middle of the found travel sits, so the range // is center +/- half the travel. A center smaller than half the travel puts the // negative end below zero. The 500000 default was sized for the Delta, whose // half-travel is ~89k; a drivetrain geared up by 7x has ~605k and crosses zero. // // Whether the TMC5160 actually misbehaves there is NOT established - XACTUAL is // a signed 32-bit register and the firmware round-trips the sign correctly - but // homing is the only thing that has ever driven that far, so the region is // untested in normal operation. The cheap fix is to not be there. // // `center` and `half_travel` are in counts. A non-positive half_travel means the // axis has not homed, and nothing is checked. CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel); } // namespace fgc