Compare commits
No commits in common. "575e3c590bd7e40b1487af598c3ce12aa39aba9f" and "816242725a5f53b730e150ad7a4e8a17c5e3688a" have entirely different histories.
575e3c590b
...
816242725a
|
|
@ -19,7 +19,7 @@ Per-file reference for the refactored tree, plus the shared data structures.
|
||||||
| [include/fgc/HardwareChecks.h](../include/fgc/HardwareChecks.h), [src/core/HardwareChecks.cpp](../src/core/HardwareChecks.cpp) | The verdicts behind `test hardware`: `checkUsbfsMemory`, `checkUsb3Lpm`, `checkI2cFunctionality`, `checkDrivetrainAdopted`. Pure, so the thresholds that encode known-issues #2 and #19 cannot drift unnoticed |
|
| [include/fgc/HardwareChecks.h](../include/fgc/HardwareChecks.h), [src/core/HardwareChecks.cpp](../src/core/HardwareChecks.cpp) | The verdicts behind `test hardware`: `checkUsbfsMemory`, `checkUsb3Lpm`, `checkI2cFunctionality`, `checkDrivetrainAdopted`. Pure, so the thresholds that encode known-issues #2 and #19 cannot drift unnoticed |
|
||||||
| [include/fgc/AxisSpeed.h](../include/fgc/AxisSpeed.h), [src/core/AxisSpeed.cpp](../src/core/AxisSpeed.cpp) | `planAxisSpeed` (deg/s -> TMC `VMAX` through `counts_per_deg`) + `vmaxToDegPerSec`. Pure arithmetic; refuses an uncalibrated scale rather than commanding a near-zero speed |
|
| [include/fgc/AxisSpeed.h](../include/fgc/AxisSpeed.h), [src/core/AxisSpeed.cpp](../src/core/AxisSpeed.cpp) | `planAxisSpeed` (deg/s -> TMC `VMAX` through `counts_per_deg`) + `vmaxToDegPerSec`. Pure arithmetic; refuses an uncalibrated scale rather than commanding a near-zero speed |
|
||||||
| [include/fgc/FirmwareConfig.h](../include/fgc/FirmwareConfig.h), [src/core/FirmwareConfig.cpp](../src/core/FirmwareConfig.cpp) | `parseNeedLine` (firmware `NEED` line -> axis + keys) + `planFirmwareConfig` (-> the `SET` lines that answer it). Reports a key it cannot answer separately from one it does not recognise: incomplete `hardware.ini` vs. a firmware newer than the host |
|
| [include/fgc/FirmwareConfig.h](../include/fgc/FirmwareConfig.h), [src/core/FirmwareConfig.cpp](../src/core/FirmwareConfig.cpp) | `parseNeedLine` (firmware `NEED` line -> axis + keys) + `planFirmwareConfig` (-> the `SET` lines that answer it). Reports a key it cannot answer separately from one it does not recognise: incomplete `hardware.ini` vs. a firmware newer than the host |
|
||||||
| [include/fgc/DumpParser.h](../include/fgc/DumpParser.h), [src/core/DumpParser.cpp](../src/core/DumpParser.cpp) | `parseDump` firmware `DUMP` block → `DumpData` (per-axis state + decoded TMC registers/flags); `formatDump`. Prefers the firmware's own `reset=` name over any host-side decode - `mcusr` is an AVR bitmask on one board and a plain enum on the other, so a single table cannot be right for both |
|
| [include/fgc/DumpParser.h](../include/fgc/DumpParser.h), [src/core/DumpParser.cpp](../src/core/DumpParser.cpp) | `parseDump` firmware `DUMP` block → `DumpData` (per-axis state + decoded TMC registers/flags); `formatDump` |
|
||||||
| [include/fgc/DiagParser.h](../include/fgc/DiagParser.h), [src/core/DiagParser.cpp](../src/core/DiagParser.cpp) | `parseDiag` firmware `DG` self-test stream → `DiagResult` (per-axis tests, PASS/FAIL); `formatDiag` |
|
| [include/fgc/DiagParser.h](../include/fgc/DiagParser.h), [src/core/DiagParser.cpp](../src/core/DiagParser.cpp) | `parseDiag` firmware `DG` self-test stream → `DiagResult` (per-axis tests, PASS/FAIL); `formatDiag` |
|
||||||
| [include/fgc/Calibration.h](../include/fgc/Calibration.h), [src/core/Calibration.cpp](../src/core/Calibration.cpp) | `linearFit` (least-squares, R²) + `circularMeanDeg` for the IMU-referenced calibration |
|
| [include/fgc/Calibration.h](../include/fgc/Calibration.h), [src/core/Calibration.cpp](../src/core/Calibration.cpp) | `linearFit` (least-squares, R²) + `circularMeanDeg` for the IMU-referenced calibration |
|
||||||
| [include/fgc/PitchSetup.h](../include/fgc/PitchSetup.h), [src/core/PitchSetup.cpp](../src/core/PitchSetup.cpp) | `gimbal pitch setup` worker thread: home-if-needed → pitch sweep dwelling on the gravity-referenced IMU pitch → least-squares fit → park level; tunable via `CalibParams`; exposes `progress()`/`report()`/`takeResult()`. Yaw is not touched |
|
| [include/fgc/PitchSetup.h](../include/fgc/PitchSetup.h), [src/core/PitchSetup.cpp](../src/core/PitchSetup.cpp) | `gimbal pitch setup` worker thread: home-if-needed → pitch sweep dwelling on the gravity-referenced IMU pitch → least-squares fit → park level; tunable via `CalibParams`; exposes `progress()`/`report()`/`takeResult()`. Yaw is not touched |
|
||||||
|
|
|
||||||
|
|
@ -66,19 +66,7 @@ struct DumpData {
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
std::string build;
|
std::string build;
|
||||||
long uptime_ms = 0;
|
long uptime_ms = 0;
|
||||||
// Why the MCU last restarted, as the firmware reported it.
|
|
||||||
//
|
|
||||||
// `mcusr` is the RAW value and means DIFFERENT THINGS per board: on AVR it is
|
|
||||||
// the MCUSR bitmask (bit 0 power-on, bit 1 external, ...); on RP2040 it is
|
|
||||||
// rp2040.getResetReason(), a plain enum where 3 is one value, not two bits.
|
|
||||||
// Decoding one as the other is silently wrong - it reported an RP2040 soft
|
|
||||||
// reset as "PORF (power-on), EXTRF (external)" on tower 121.
|
|
||||||
//
|
|
||||||
// So the firmware now NAMES it in the dump (`reset=<text>`), and that text is
|
|
||||||
// authoritative because only the board knows its own encoding. reset_flags is
|
|
||||||
// the legacy AVR bit decode, kept for firmware that predates the field.
|
|
||||||
unsigned mcusr = 0;
|
unsigned mcusr = 0;
|
||||||
std::string reset_text; // empty => firmware did not name it
|
|
||||||
long free_ram = 0;
|
long free_ram = 0;
|
||||||
std::vector<std::string> reset_flags;
|
std::vector<std::string> reset_flags;
|
||||||
std::vector<DumpAxis> axes;
|
std::vector<DumpAxis> axes;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
#include "fgc/ui/IUserInterface.h"
|
#include "fgc/ui/IUserInterface.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -47,15 +46,7 @@ private:
|
||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
// When set, the 100ms auto-redraw is suspended so the screen holds still and
|
// When set, the 100ms auto-redraw is suspended so the screen holds still and
|
||||||
// the operator can select/copy text (terminals drop a selection on repaint).
|
// the operator can select/copy text (terminals drop a selection on repaint).
|
||||||
// Display freeze (the `p` key), for selecting text out of the pane. While
|
|
||||||
// set, refreshLoop() stops posting redraws, so the frame on screen ages.
|
|
||||||
// paused_at_ is when it was frozen, so the badge can say how stale it is.
|
|
||||||
std::atomic<bool> paused_{false};
|
std::atomic<bool> paused_{false};
|
||||||
// Wall-clock time of the freeze, "HH:MM:SS", shown in the badge. Captured
|
|
||||||
// once: the badge must stay truthful WITHOUT a repaint, because repainting
|
|
||||||
// would pull the live snapshot and unfreeze what the pause is holding still.
|
|
||||||
std::string paused_at_hms_;
|
|
||||||
static std::string nowHms();
|
|
||||||
// True while the gimbal overlay is open, so the refresh loop can periodically
|
// True while the gimbal overlay is open, so the refresh loop can periodically
|
||||||
// re-pull a firmware dump (keeps the tracking counters ticking without 'd').
|
// re-pull a firmware dump (keeps the tracking counters ticking without 'd').
|
||||||
std::atomic<bool> gimbal_overlay_open_{false};
|
std::atomic<bool> gimbal_overlay_open_{false};
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,6 @@ DumpData parseDump(const std::string& block) {
|
||||||
d.uptime_ms = asInt(get(hdr, "uptime", "0"));
|
d.uptime_ms = asInt(get(hdr, "uptime", "0"));
|
||||||
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
||||||
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
||||||
d.reset_text = get(hdr, "reset");
|
|
||||||
d.reset_flags = bitsSet(d.mcusr, kMcusr);
|
d.reset_flags = bitsSet(d.mcusr, kMcusr);
|
||||||
|
|
||||||
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
||||||
|
|
@ -200,17 +199,8 @@ std::vector<std::string> formatDump(const DumpData& d) {
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::ostringstream os;
|
std::ostringstream os;
|
||||||
os << "reset cause (0x" << std::hex << d.mcusr << std::dec << "): ";
|
os << "reset cause (mcusr=0x" << std::hex << d.mcusr << std::dec << "): "
|
||||||
if (!d.reset_text.empty()) {
|
<< join(d.reset_flags);
|
||||||
// The firmware named it, so no guessing is involved.
|
|
||||||
os << d.reset_text;
|
|
||||||
} else {
|
|
||||||
// Firmware predating the `reset=` field. The only decode available is
|
|
||||||
// the AVR one, and on a non-AVR board it is wrong - so say that it is
|
|
||||||
// an assumption rather than stating it as fact.
|
|
||||||
os << join(d.reset_flags) << " [assumed AVR MCUSR bits; firmware does "
|
|
||||||
"not name it]";
|
|
||||||
}
|
|
||||||
out.push_back(os.str());
|
out.push_back(os.str());
|
||||||
}
|
}
|
||||||
for (const auto& ax : d.axes) {
|
for (const auto& ax : d.axes) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#include "fgc/ui/TuiUi.h"
|
#include "fgc/ui/TuiUi.h"
|
||||||
|
|
||||||
#include <ctime>
|
|
||||||
|
|
||||||
#include "fgc/ChannelTopics.h"
|
#include "fgc/ChannelTopics.h"
|
||||||
|
|
||||||
#include "fgc/DumpParser.h"
|
#include "fgc/DumpParser.h"
|
||||||
|
|
@ -1249,30 +1247,13 @@ void TuiUi::pushLog(LogLevel level, const std::string& line) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wall-clock time the frame was frozen at, "HH:MM:SS". Captured once, because
|
|
||||||
// the badge must not need a repaint to stay truthful - repainting would unfreeze
|
|
||||||
// the very data the pause is holding still.
|
|
||||||
std::string TuiUi::nowHms() {
|
|
||||||
const auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
|
||||||
std::tm tm{};
|
|
||||||
localtime_r(&t, &tm);
|
|
||||||
char buf[16];
|
|
||||||
std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TuiUi::refreshLoop() {
|
void TuiUi::refreshLoop() {
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
auto last_dump = std::chrono::steady_clock::now();
|
auto last_dump = std::chrono::steady_clock::now();
|
||||||
while (running_) {
|
while (running_) {
|
||||||
std::this_thread::sleep_for(100ms);
|
std::this_thread::sleep_for(100ms);
|
||||||
if (!running_) break;
|
if (!running_) break;
|
||||||
// Frozen for text selection: do NOT repaint. Re-rendering would pull the
|
if (paused_) continue; // frozen for text selection; don't repaint
|
||||||
// live snapshot and the numbers would keep moving under the cursor,
|
|
||||||
// which is precisely what the freeze exists to prevent. The badge
|
|
||||||
// carries the wall-clock time of the freeze instead, so the frame says
|
|
||||||
// how old it is without having to be redrawn to say it.
|
|
||||||
if (paused_) continue;
|
|
||||||
// While the gimbal overlay is open, re-pull a firmware dump every ~3s so
|
// While the gimbal overlay is open, re-pull a firmware dump every ~3s so
|
||||||
// the encoder-tracking counters stay current without pressing 'd'.
|
// the encoder-tracking counters stay current without pressing 'd'.
|
||||||
if (gimbal_overlay_open_.load() && sink_ &&
|
if (gimbal_overlay_open_.load() && sink_ &&
|
||||||
|
|
@ -1351,15 +1332,7 @@ void TuiUi::uiLoop() {
|
||||||
text(" " + s.header.tower + " ") | bold,
|
text(" " + s.header.tower + " ") | bold,
|
||||||
text("build " + s.header.build) | dim,
|
text("build " + s.header.build) | dim,
|
||||||
filler(),
|
filler(),
|
||||||
// A frozen frame has to say that it is frozen, and for how long.
|
(paused_ ? text(" PAUSED ") | color(Color::Black) | bgcolor(Color::Yellow) | bold
|
||||||
// Without the age it is indistinguishable from a hung program: the
|
|
||||||
// numbers simply stop, which during a multi-minute home reads as a
|
|
||||||
// crash. Costing 15 minutes of "is it stuck?" is how this got added.
|
|
||||||
// Says WHEN it froze, not just that it is frozen. Without the time a
|
|
||||||
// held frame is indistinguishable from a hung program - the numbers
|
|
||||||
// simply stop - which during a multi-minute home reads as a crash.
|
|
||||||
(paused_ ? text(" PAUSED @" + paused_at_hms_ + " ")
|
|
||||||
| color(Color::Black) | bgcolor(Color::Yellow) | bold
|
|
||||||
: text("")),
|
: text("")),
|
||||||
text(" " + mode + " ") | (s.header.live ? color(Color::Green) : color(Color::Yellow)) | bold,
|
text(" " + mode + " ") | (s.header.live ? color(Color::Green) : color(Color::Yellow)) | bold,
|
||||||
text(" up " + std::to_string(s.header.uptime_ms / 1000) + "s ") | dim,
|
text(" up " + std::to_string(s.header.uptime_ms / 1000) + "s ") | dim,
|
||||||
|
|
@ -1676,7 +1649,6 @@ void TuiUi::uiLoop() {
|
||||||
}
|
}
|
||||||
if (c == "p") { // freeze/unfreeze the live refresh so text can be selected
|
if (c == "p") { // freeze/unfreeze the live refresh so text can be selected
|
||||||
paused_ = !paused_;
|
paused_ = !paused_;
|
||||||
if (paused_) paused_at_hms_ = nowHms();
|
|
||||||
return true; // this keypress redraws once so the indicator updates
|
return true; // this keypress redraws once so the indicator updates
|
||||||
}
|
}
|
||||||
if (overlay == Overlay::Help) { // vim-style section nav while help is open
|
if (overlay == Overlay::Help) { // vim-style section nav while help is open
|
||||||
|
|
|
||||||
|
|
@ -223,40 +223,3 @@ TEST_CASE("the numeric state codes did not shift when NEEDCFG was added") {
|
||||||
CHECK(one(4) == "ERROR");
|
CHECK(one(4) == "ERROR");
|
||||||
CHECK(one(5) == "NEEDCFG");
|
CHECK(one(5) == "NEEDCFG");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("the firmware's own name for the reset cause wins over any decode") {
|
|
||||||
// mcusr means different things per board: an AVR MCUSR bitmask, or the
|
|
||||||
// RP2040's getResetReason() enum. 0x3 is "two bits set" on one and a single
|
|
||||||
// enum value on the other, so a host-side table cannot be right for both.
|
|
||||||
const std::string text =
|
|
||||||
"DUMP BEGIN build=x uptime=1 mcusr=0x3 reset=SOFT_RESET free_ram=4096\n"
|
|
||||||
"DUMP Y state=1 hsub=0 enabled=0 lim_neg=0 lim_pos=0 hold_target=0 speed=1 center=0 "
|
|
||||||
"soft_margin=0 hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\n"
|
|
||||||
"DUMP END\n";
|
|
||||||
auto d = fgc::parseDump(text);
|
|
||||||
REQUIRE(d.valid);
|
|
||||||
CHECK(d.reset_text == "SOFT_RESET");
|
|
||||||
|
|
||||||
std::string out;
|
|
||||||
for (const auto& l : fgc::formatDump(d)) out += l + "\n";
|
|
||||||
CHECK(out.find("SOFT_RESET") != std::string::npos);
|
|
||||||
// The AVR names must not appear when the firmware said what it was.
|
|
||||||
CHECK(out.find("PORF") == std::string::npos);
|
|
||||||
CHECK(out.find("EXTRF") == std::string::npos);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("without that field the AVR decode is labelled as an assumption") {
|
|
||||||
const std::string text =
|
|
||||||
"DUMP BEGIN build=old uptime=1 mcusr=0x3 free_ram=4096\n"
|
|
||||||
"DUMP Y state=1 hsub=0 enabled=0 lim_neg=0 lim_pos=0 hold_target=0 speed=1 center=0 "
|
|
||||||
"soft_margin=0 hold_corr=0 hold_peak=0 hold_cumul=0 has_encoder=1\n"
|
|
||||||
"DUMP END\n";
|
|
||||||
auto d = fgc::parseDump(text);
|
|
||||||
REQUIRE(d.valid);
|
|
||||||
CHECK(d.reset_text.empty());
|
|
||||||
|
|
||||||
std::string out;
|
|
||||||
for (const auto& l : fgc::formatDump(d)) out += l + "\n";
|
|
||||||
CHECK(out.find("PORF") != std::string::npos); // still decoded
|
|
||||||
CHECK(out.find("assumed AVR") != std::string::npos); // but not stated as fact
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue