#include #include "fgc/DumpParser.h" #include #include using namespace fgc; namespace { // A realistic two-axis firmware DUMP block (the on-wire format from // firmware/src/motor.cpp printDump): header, then per axis a state line and a // TMC register line, bracketed by DUMP BEGIN / DUMP END. const char* kDump = "DUMP BEGIN build=11dd3ce-dirty uptime=11740067 mcusr=0x01 free_ram=1852\n" "DUMP Y state=3 hsub=10 enabled=1 lim_neg=-82919 lim_pos=98687 hold_target=-90 " "speed=50000 eeprom_restored=0 soft_margin=5000 hold_corr=7 hold_peak=430 hold_cumul=2810 " "has_encoder=1\n" "DUMP Y TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x30000008 TSTEP=0x000FFFFF " "RAMPMODE=0x00000000 XACTUAL=0xFFFFFFA6 VACTUAL=0x00000000 XTARGET=0xFFFFFFA6 " "SW_MODE=0x000008A0 RAMP_STAT=0x00001680 X_ENC=0xFFFFFF7F ENC_STATUS=0x00000002 " "CHOPCONF=0x00410043 DRV_STATUS=0x80084000 PWM_SCALE=0x00000011 PWM_AUTO=0x003F0039\n" "DUMP P state=3 hsub=10 enabled=1 lim_neg=495183 lim_pos=1069042 hold_target=500183 " "speed=150000 eeprom_restored=0 has_encoder=1\n" "DUMP P TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x30000008 TSTEP=0x000FFFFF " "RAMPMODE=0x00000000 XACTUAL=0x0007A1C8 VACTUAL=0x00000000 XTARGET=0x0007A1C8 " "SW_MODE=0x000008A3 RAMP_STAT=0x00001688 X_ENC=0x0007A231 ENC_STATUS=0x00000002 " "CHOPCONF=0x00410043 DRV_STATUS=0x80084000 PWM_SCALE=0x00010011 PWM_AUTO=0x0021003C\n" "DUMP END\n"; bool has(const std::vector& v, const std::string& s) { return std::find(v.begin(), v.end(), s) != v.end(); } const DumpAxis* axis(const DumpData& d, char a) { for (const auto& ax : d.axes) if (ax.axis == a) return &ax; return nullptr; } } // namespace TEST_CASE("parseDump decodes the header") { DumpData d = parseDump(kDump); REQUIRE(d.valid); CHECK(d.build == "11dd3ce-dirty"); CHECK(d.uptime_ms == 11740067); CHECK(d.mcusr == 0x01u); CHECK(d.free_ram == 1852); CHECK(has(d.reset_flags, "PORF (power-on)")); REQUIRE(d.axes.size() == 2); } TEST_CASE("parseDump decodes the per-axis state line") { DumpData d = parseDump(kDump); const DumpAxis* y = axis(d, 'Y'); REQUIRE(y != nullptr); CHECK(y->state_name == "READY"); // state=3 CHECK(y->enabled); CHECK(y->has_encoder); CHECK_FALSE(y->eeprom_restored); CHECK(y->lim_neg == -82919); CHECK(y->lim_pos == 98687); CHECK(y->hold_target == -90); CHECK(y->speed == 50000); CHECK(y->hsub == 10); CHECK(y->soft_margin == 5000); CHECK(y->hold_corrections == 7); CHECK(y->hold_peak_dev == 430); CHECK(y->hold_cumulative_dev == 2810); const DumpAxis* p = axis(d, 'P'); REQUIRE(p != nullptr); CHECK(p->lim_neg == 495183); CHECK(p->lim_pos == 1069042); } TEST_CASE("parseDump keeps all 16 TMC registers (regression: TMC-line whitespace)") { // The "DUMP Y TMC ..." line was once misclassified due to a leading-space // slice, dropping every register. Guard that all of them survive. DumpData d = parseDump(kDump); const DumpAxis* y = axis(d, 'Y'); REQUIRE(y != nullptr); for (const char* r : {"GCONF", "GSTAT", "IOIN", "TSTEP", "RAMPMODE", "XACTUAL", "VACTUAL", "XTARGET", "SW_MODE", "RAMP_STAT", "X_ENC", "ENC_STATUS", "CHOPCONF", "DRV_STATUS", "PWM_SCALE", "PWM_AUTO"}) { CHECK_MESSAGE(y->regs.count(r) == 1, "missing register ", r); } CHECK(y->regs.at("RAMP_STAT") == "0x00001680"); CHECK(y->regs.at("DRV_STATUS") == "0x80084000"); } TEST_CASE("parseDump decodes status-register bit flags") { DumpData d = parseDump(kDump); const DumpAxis* y = axis(d, 'Y'); REQUIRE(y != nullptr); // DRV_STATUS = 0x80084000 -> bit31 stst, bit14 stealth; CS_ACTUAL=(>>16)&0x1F=8. CHECK(y->drv_status == 0x80084000u); CHECK(has(y->drv_flags, "stst")); CHECK(has(y->drv_flags, "stealth")); CHECK(y->cs_actual == 8); CHECK(y->sg_result == 0); // GSTAT = 0 -> no flags (placeholder "-"). CHECK(has(y->gstat_flags, "-")); // RAMP_STAT = 0x1680 -> bit7 event_pos_reached, bit9 position_reached, bit10 vzero. CHECK(y->ramp_stat == 0x00001680u); CHECK(has(y->ramp_flags, "event_pos_reached")); CHECK(has(y->ramp_flags, "position_reached")); CHECK(has(y->ramp_flags, "vzero")); } TEST_CASE("parseDump tolerates a corrupted/merged DUMP END (best-effort)") { // The host detects DUMP END anywhere in a line; a clean header still parses // even when later content is mangled — but a block with no BEGIN/END is invalid. CHECK_FALSE(parseDump("").valid); CHECK_FALSE(parseDump("garbage with no markers\n").valid); CHECK_FALSE(parseDump("DUMP Y state=3\nno end marker\n").valid); } TEST_CASE("formatDump renders valid and invalid dumps") { auto bad = formatDump(DumpData{}); REQUIRE(bad.size() == 1); CHECK(bad[0].find("no firmware dump") != std::string::npos); auto good = formatDump(parseDump(kDump)); std::string joined; for (const auto& l : good) joined += l + "\n"; CHECK(joined.find("build=11dd3ce-dirty") != std::string::npos); CHECK(joined.find("[Y]") != std::string::npos); CHECK(joined.find("[P]") != std::string::npos); CHECK(joined.find("RAMP_STAT") != std::string::npos); }