376 lines
14 KiB
C++
376 lines
14 KiB
C++
#include "fgc/HardwareChecks.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
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 <n>' 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 checkPinsBuilt(const std::string& label, const DeclaredValue* values, int count) {
|
|
CheckResult r;
|
|
int checked = 0, absent = 0;
|
|
std::string diff;
|
|
for (int i = 0; i < count; ++i) {
|
|
const DeclaredValue& v = values[i];
|
|
if (!v.want) continue;
|
|
if (v.got < 0) { ++absent; continue; }
|
|
++checked;
|
|
if (v.got != v.value) {
|
|
if (!diff.empty()) diff += ", ";
|
|
diff += std::string(v.name) + " declared " + std::to_string(v.value) +
|
|
", built with " + std::to_string(v.got);
|
|
}
|
|
}
|
|
if (checked == 0 && absent == 0) {
|
|
r.ran = false;
|
|
r.detail = label + ": no [Pins] declared";
|
|
return r;
|
|
}
|
|
if (!diff.empty()) {
|
|
r.pass = false;
|
|
r.detail = label + ": " + diff +
|
|
" -- the firmware was built from different wiring than hardware.ini "
|
|
"declares. Rebuild and re-flash it";
|
|
return r;
|
|
}
|
|
if (absent > 0) {
|
|
r.pass = false;
|
|
r.detail = label + ": the firmware does not report the pins it was built with, so its "
|
|
"wiring cannot be confirmed -- it predates the pin echo. Re-flash it";
|
|
return r;
|
|
}
|
|
r.detail = label + ": built with the declared pins";
|
|
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;
|
|
}
|
|
|
|
PixelStats pixelStats(const std::vector<uint8_t>& data, uint8_t low, uint8_t high) {
|
|
PixelStats st;
|
|
st.low = low;
|
|
st.high = high;
|
|
st.samples = data.size();
|
|
if (data.empty()) return st;
|
|
// A histogram, not per-byte arithmetic: a frame is 12 M bytes, and this runs
|
|
// on the tower in the middle of a test.
|
|
size_t hist[256] = {};
|
|
for (uint8_t b : data) ++hist[b];
|
|
double sum = 0.0;
|
|
size_t lo = 0, hi = 0;
|
|
for (int v = 0; v < 256; ++v) {
|
|
sum += static_cast<double>(v) * hist[v];
|
|
if (v <= low) lo += hist[v];
|
|
if (v >= high) hi += hist[v];
|
|
}
|
|
const double n = static_cast<double>(data.size());
|
|
st.mean = sum / n;
|
|
st.pct_low = 100.0 * lo / n;
|
|
st.pct_high = 100.0 * hi / n;
|
|
return st;
|
|
}
|
|
|
|
CheckResult checkFramesDelivered(const std::string& camera, const FrameRun& run) {
|
|
CheckResult r;
|
|
if (run.wanted <= 0) {
|
|
r.ran = false;
|
|
r.detail = camera + ": no frames asked for";
|
|
return r;
|
|
}
|
|
char geom[64];
|
|
std::snprintf(geom, sizeof(geom), "%ux%u x%d", run.width, run.height, run.channels);
|
|
if (run.got < run.wanted) {
|
|
r.pass = false;
|
|
r.detail = camera + ": " + std::to_string(run.got) + " of " + std::to_string(run.wanted) +
|
|
" frames arrived within " + std::to_string(run.timeout_ms) +
|
|
" ms each -- the stream is stalling or not running";
|
|
return r;
|
|
}
|
|
if (!run.consistent) {
|
|
r.pass = false;
|
|
r.detail = camera + ": frames changed size or depth mid-run (first " + geom + ")";
|
|
return r;
|
|
}
|
|
if (run.width == 0 || run.height == 0 || (run.channels != 1 && run.channels != 3)) {
|
|
r.pass = false;
|
|
r.detail = camera + ": frames arrived malformed (" + geom +
|
|
"); the program handles 1 or 3 channels";
|
|
return r;
|
|
}
|
|
if (run.dropped > 0) {
|
|
r.pass = false;
|
|
r.detail = camera + ": " + std::to_string(run.dropped) +
|
|
" incomplete frame(s) during the run -- usually USB bandwidth (known-issues #19)";
|
|
return r;
|
|
}
|
|
r.detail = camera + ": " + std::to_string(run.got) + " complete frames, " + geom;
|
|
return r;
|
|
}
|
|
|
|
CheckResult checkClipped(const std::string& camera, bool white, const PixelStats& st,
|
|
double min_pct) {
|
|
CheckResult r;
|
|
if (st.samples == 0) {
|
|
r.pass = false;
|
|
r.detail = camera + ": no frame to judge";
|
|
return r;
|
|
}
|
|
const double got = white ? st.pct_high : st.pct_low;
|
|
char buf[160];
|
|
std::snprintf(buf, sizeof(buf), "%.2f%% of pixels %s %d (want >= %.1f%%), mean %.1f", got,
|
|
white ? ">=" : "<=", white ? st.high : st.low, min_pct, st.mean);
|
|
r.pass = got >= min_pct;
|
|
r.detail = camera + ": " + buf;
|
|
if (!r.pass)
|
|
r.detail += white ? " -- the forced settings did not reach white: settings not applied, "
|
|
"or the sensor output is not reaching the host intact"
|
|
: " -- the forced settings did not reach black: settings not applied, "
|
|
"a stuck or noisy sensor, or light direct enough to beat the "
|
|
"minimum exposure";
|
|
return r;
|
|
}
|
|
|
|
CheckResult checkSensorResponds(const std::string& camera, const PixelStats& dark,
|
|
const PixelStats& bright, double min_delta) {
|
|
CheckResult r;
|
|
if (dark.samples == 0 || bright.samples == 0) {
|
|
r.pass = false;
|
|
r.detail = camera + ": no frame to compare";
|
|
return r;
|
|
}
|
|
const double delta = bright.mean - dark.mean;
|
|
char buf[160];
|
|
std::snprintf(buf, sizeof(buf), "mean %.1f at maximum gain and long exposure against %.1f "
|
|
"forced dark (+%.1f, want >= +%.1f)", bright.mean, dark.mean, delta, min_delta);
|
|
r.pass = delta >= min_delta;
|
|
r.detail = camera + ": " + buf;
|
|
if (!r.pass)
|
|
r.detail += " -- the sensor did not respond: the settings were not applied, or the "
|
|
"frames are not coming from a live sensor";
|
|
return r;
|
|
}
|
|
|
|
} // namespace fgc
|