#include "fgc/HardwareChecks.h" #include #include #include namespace fgc { namespace { std::string trimmed(const std::string& s) { const auto b = s.find_first_not_of(" \t\r\n"); if (b == std::string::npos) return ""; const auto e = s.find_last_not_of(" \t\r\n"); return s.substr(b, e - b + 1); } // A sysfs integer, or nullopt when the node was empty/unreadable/not a number. bool asInt(const std::string& s, long& out) { const std::string t = trimmed(s); if (t.empty()) return false; if (!std::all_of(t.begin(), t.end(), [](unsigned char c) { return std::isdigit(c); })) return false; try { out = std::stol(t); } catch (...) { return false; } return true; } } // namespace CheckResult checkUsbfsMemory(const std::string& sysfs_value, int min_mb) { CheckResult r; long mb = 0; if (!asInt(sysfs_value, mb)) { r.pass = false; r.detail = "could not read usbfs_memory_mb -- expected a number at " "/sys/module/usbcore/parameters/usbfs_memory_mb"; return r; } r.pass = mb >= min_mb; if (r.pass) { r.detail = "usbfs_memory_mb " + std::to_string(mb); } else { r.detail = "usbfs_memory_mb is " + std::to_string(mb) + ", needs >= " + std::to_string(min_mb) + " -- frames will stall under load. It resets on every reboot unless " "usbcore.usbfs_memory_mb=" + std::to_string(min_mb) + " is on the kernel cmdline (known-issues #19)"; } return r; } CheckResult checkUsb3Lpm(const std::string& permit_value) { CheckResult r; const std::string v = trimmed(permit_value); if (v.empty()) { r.pass = false; r.detail = "could not read usb3_lpm_permit for the camera's port"; return r; } r.pass = (v == "0"); r.detail = r.pass ? "USB3 LPM off on the camera port" : "USB3 LPM is permitted (usb3_lpm_permit=" + v + ") -- write 0 and re-enumerate, and persist it with a udev rule; " "the per-device usb3_hardware_lpm_u* nodes are read-only " "(known-issues #19)"; return r; } CheckResult checkI2cFunctionality(unsigned long funcs) { CheckResult r; r.pass = (funcs & kI2cFuncI2c) != 0; r.detail = r.pass ? "adapter supports real I2C transactions" : "adapter is SMBus-only (I2C_FUNCS lacks I2C_FUNC_I2C) -- the SHT41 will " "bind and then fail every read. Point [Env] i2c_device at a real I2C " "adapter; 'i2cdetect -F ' is the manual check (known-issues #2)"; return r; } CheckResult checkNotLimpHome(const std::string& axis, long limp) { CheckResult r; if (limp < 0) { r.ran = false; r.detail = axis + ": firmware does not report the limp flag, so whether it is running " "hardware.ini's values or its own fallback cannot be told apart here"; return r; } r.pass = (limp == 0); r.detail = r.pass ? axis + ": running the host's declared values" : axis + ": running LIMP-HOME values, which describe no real drivetrain -- slow " "speeds, a reduced search current and a placeholder travel. Something " "sent USE DEFAULTS, so hardware.ini is NOT in force however healthy the " "axis looks. Restart the host to re-send it"; return r; } CheckResult checkDrivetrainAdopted(const std::string& axis, const DrivetrainExpectation& want, const DrivetrainEcho& got) { CheckResult r; // Nothing declared for this axis means nothing was sent, and the firmware's // compile-time defaults are the intended configuration. There is no skew to // find. if (!want.declared) { r.ran = false; r.detail = "no [Motor." + axis + "] current declared; firmware defaults are in force"; return r; } if (got.absent()) { r.pass = false; r.detail = axis + ": firmware did not echo gs/irun/irun_home -- it predates the CURRENT command, " "so whatever hardware.ini asks for is NOT being applied. Re-flash it"; return r; } // A field is compared only when it was actually echoed. Comparing against -1 // would report "firmware has -1", which reads as a wrong value rather than // as a field this firmware does not report - and sends someone to re-send a // CURRENT that was never the problem. int missing = 0; auto compare = [&](const char* field, int wanted, long saw) { if (saw < 0) { ++missing; return; } if (saw == wanted) return; r.pass = false; if (!r.detail.empty()) r.detail += "; "; r.detail += std::string(field) + " wanted " + std::to_string(wanted) + ", firmware has " + std::to_string(saw); }; compare("GLOBALSCALER", want.global_scaler, got.global_scaler); compare("IRUN", want.run_irun, got.run_irun); compare("homing IRUN", want.homing_irun, got.homing_irun); if (missing > 0 && r.pass) { r.pass = false; r.detail = axis + ": firmware echoes only part of the current (" + std::to_string(missing) + " of 3 fields absent), so it cannot be confirmed -- it predates the full " "CURRENT echo. Re-flash it"; return r; } if (r.pass) { r.detail = axis + ": firmware has the declared current (GLOBALSCALER " + std::to_string(want.global_scaler) + ", IRUN " + std::to_string(want.run_irun) + ", homing " + std::to_string(want.homing_irun) + ")"; } else { r.detail = axis + ": " + r.detail + " -- the CURRENT command did not land. A homing current that is not in force is " "the case that damages the endstop"; } return r; } CheckResult checkDeclaredValues(const std::string& axis, const DeclaredValue* values, int count) { CheckResult r; int checked = 0, absent = 0; for (int i = 0; i < count; ++i) { const DeclaredValue& v = values[i]; if (!v.want) continue; // nothing declared: nothing to be wrong about if (v.got < 0) { // The firmware does not echo this field at all, which is a version // skew rather than a mismatch - and it means the value cannot be // confirmed either way, so it must not read as a pass. ++absent; continue; } ++checked; if (v.got != v.value) { r.pass = false; if (!r.detail.empty()) r.detail += "; "; r.detail += std::string(v.name) + " wanted " + std::to_string(v.value) + ", firmware has " + std::to_string(v.got); } } if (checked == 0 && absent == 0) { r.ran = false; r.detail = axis + ": nothing declared for this axis beyond the current"; return r; } if (absent > 0 && r.pass) { r.pass = false; r.detail = axis + ": the firmware does not echo " + std::to_string(absent) + " declared value(s), so what it is running on cannot be confirmed -- it " "predates the config handshake. Re-flash it"; return r; } if (!r.pass) { r.detail = axis + ": " + r.detail + " -- the firmware is not running what hardware.ini declares. A SET was lost, " "or the axis never left NEEDCFG"; return r; } r.detail = axis + ": firmware holds all " + std::to_string(checked) + " declared values"; return r; } CheckResult checkTravelFitsPositive(const std::string& axis, long center, long half_travel) { CheckResult r; if (half_travel <= 0) { r.ran = false; r.detail = axis + ": not homed, so the travel range is not known yet"; return r; } const long lo = center - half_travel; if (lo >= 0) { r.detail = axis + ": travel is entirely positive (" + std::to_string(lo) + " .. " + std::to_string(center + half_travel) + ")"; return r; } r.pass = false; r.detail = axis + ": homed travel reaches " + std::to_string(lo) + " -- below zero. [Motor] " + axis + "_center_count is " + std::to_string(center) + " but half the travel is " + std::to_string(half_travel) + ", so raise the centre above that (a round " "number with headroom, e.g. " + std::to_string(((half_travel / 1000000) + 1) * 1000000) + "). The centre is arbitrary; only the span is physical"; return r; } } // namespace fgc