fwt_software/include/fgc/HardwareChecks.h

188 lines
9.0 KiB
C++

#pragma once
#include <cstdint>
#include <string>
#include <vector>
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 <n>` is the manual form of this check.
//
// The bit is spelled out rather than pulled from <linux/i2c.h> 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);
// ---- `test camera` ------------------------------------------------------------
// What fraction of a frame's bytes sit at each end of the range. Every channel
// byte counts, so an RGB8 frame's 12 M bytes are 12 M samples.
struct PixelStats {
size_t samples = 0;
double mean = 0.0;
double pct_low = 0.0; // bytes <= low
double pct_high = 0.0; // bytes >= high
uint8_t low = 5; // the bounds they were counted against
uint8_t high = 250;
};
PixelStats pixelStats(const std::vector<uint8_t>& data, uint8_t low = 5, uint8_t high = 250);
// A run of frames asked for through the program's own camera path.
struct FrameRun {
int wanted = 0;
int got = 0; // arrived complete within the timeout
int timeout_ms = 0; // per frame
bool consistent = true; // every frame the size and depth of the first
uint32_t width = 0, height = 0;
int channels = 0;
long long dropped = 0; // incomplete deliveries during the run
};
// Did the camera deliver whole, well-formed frames? Says nothing about what is
// in them - that is checkClipped()'s job, done at forced settings so the scene
// cannot decide the verdict.
CheckResult checkFramesDelivered(const std::string& camera, const FrameRun& run);
// Did a frame taken at forced settings clip to the end they force? `white`
// false = black. For black the camera is driven there by its own settings -
// minimum exposure and gain - so it holds in daylight, at night and under a lens
// cap alike. "Black" is `stats.low`, not 0: sensors differ, and an Alvium 1800
// U-1240c's floor sits at 2..17 of 255 where a U-1620c's is 0.
CheckResult checkClipped(const std::string& camera, bool white, const PixelStats& stats,
double min_pct);
// Is the sensor alive? A frame at maximum gain and a long exposure must differ
// from the forced-dark one by at least `min_delta` in mean level. Nothing about
// the picture is interpreted, and nothing needs to clip: in daylight the bright
// frame saturates, at night or under a cap amplified dark current and noise still
// lift it - either way it is different, which only a sensor that took the new
// settings can produce. A frame that does not move is a camera that ignored
// them, or a sensor that is not in the loop.
CheckResult checkSensorResponds(const std::string& camera, const PixelStats& dark,
const PixelStats& bright, double min_delta);
} // namespace fgc