Fix DUMP serial corruption by pacing USB-CDC output with Serial.flush()
This commit is contained in:
parent
0b62fef9c7
commit
df42a4fc69
|
|
@ -51,6 +51,7 @@ add_library(fgc_core STATIC
|
||||||
src/core/CaptureScheduler.cpp
|
src/core/CaptureScheduler.cpp
|
||||||
src/core/CommandParser.cpp
|
src/core/CommandParser.cpp
|
||||||
src/core/HelpText.cpp
|
src/core/HelpText.cpp
|
||||||
|
src/core/DumpParser.cpp
|
||||||
src/ui/UiSnapshot.cpp
|
src/ui/UiSnapshot.cpp
|
||||||
src/ui/HeadlessUi.cpp
|
src/ui/HeadlessUi.cpp
|
||||||
ini.c
|
ini.c
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
// Structured decode of a firmware DUMP block (the text between "DUMP BEGIN" and
|
||||||
|
// "DUMP END"). Mirrors firmware/tools/decode_dump.py, which is authoritative for
|
||||||
|
// the on-wire field names and the TMC status-register bit tables.
|
||||||
|
|
||||||
|
struct DumpAxis {
|
||||||
|
char axis = '?'; // 'Y' (yaw) / 'P' (pitch)
|
||||||
|
std::string state_name = "?"; // BOOT/RESET/HOMING/READY/ERROR
|
||||||
|
bool enabled = false;
|
||||||
|
bool eeprom_restored = false;
|
||||||
|
bool has_encoder = false;
|
||||||
|
long lim_neg = 0;
|
||||||
|
long lim_pos = 0;
|
||||||
|
long hold_target = 0;
|
||||||
|
long speed = 0;
|
||||||
|
int hsub = 0;
|
||||||
|
|
||||||
|
// Raw register hex strings as received, keyed by name (GCONF, DRV_STATUS...).
|
||||||
|
std::map<std::string, std::string> regs;
|
||||||
|
|
||||||
|
// Decoded status registers + their set-bit names.
|
||||||
|
unsigned drv_status = 0;
|
||||||
|
unsigned gstat = 0;
|
||||||
|
unsigned ramp_stat = 0;
|
||||||
|
int cs_actual = 0; // (DRV_STATUS >> 16) & 0x1F
|
||||||
|
int sg_result = 0; // DRV_STATUS & 0x3FF
|
||||||
|
std::vector<std::string> drv_flags;
|
||||||
|
std::vector<std::string> gstat_flags;
|
||||||
|
std::vector<std::string> ramp_flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DumpData {
|
||||||
|
bool valid = false;
|
||||||
|
std::string build;
|
||||||
|
long uptime_ms = 0;
|
||||||
|
unsigned mcusr = 0;
|
||||||
|
long free_ram = 0;
|
||||||
|
std::vector<std::string> reset_flags;
|
||||||
|
std::vector<DumpAxis> axes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse a raw DUMP block. Returns {valid=false} for empty/malformed input (no
|
||||||
|
// "DUMP BEGIN .. DUMP END" pair).
|
||||||
|
DumpData parseDump(const std::string& block);
|
||||||
|
|
||||||
|
// Human-readable lines for a decoded dump (console-reusable pretty print).
|
||||||
|
std::vector<std::string> formatDump(const DumpData& d);
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
|
|
@ -47,17 +47,30 @@ public:
|
||||||
yaw_target_ = yaw_.xenc;
|
yaw_target_ = yaw_.xenc;
|
||||||
pitch_target_ = pitch_.xenc;
|
pitch_target_ = pitch_.xenc;
|
||||||
} else if (verb == "DUMP") {
|
} else if (verb == "DUMP") {
|
||||||
// Mirror the firmware DUMP block so the `dump` path is demoable
|
// Mirror the real firmware DUMP wire format (see firmware/src/motor.cpp
|
||||||
// without hardware. Values are illustrative, not simulated.
|
// and main.cpp) so the host-side DumpParser is exercised without
|
||||||
|
// hardware. Register values are illustrative, not simulated; the
|
||||||
|
// encoder positions track the mock's live state.
|
||||||
|
const int st = homed_ ? 3 : 1; // READY : RESET
|
||||||
std::ostringstream d;
|
std::ostringstream d;
|
||||||
d << "DUMP BEGIN build=mock uptime_ms=0 reset=POR\n"
|
d << "DUMP BEGIN build=mock uptime=0 mcusr=0x01 free_ram=4096\n";
|
||||||
<< "Y state=" << (homed_ ? "READY" : "RESET")
|
auto axis = [&](char L, const AxisTelemetry& a) {
|
||||||
<< " xactual=" << yaw_.xenc << " xenc=" << yaw_.xenc
|
d << "DUMP " << L << " state=" << st
|
||||||
<< " DRV_STATUS=80084000 GSTAT=00\n"
|
<< " hsub=0 enabled=" << (homed_ ? 1 : 0)
|
||||||
<< "P state=" << (homed_ ? "READY" : "RESET")
|
<< " lim_neg=0 lim_pos=1000000 hold_target=" << a.xenc
|
||||||
<< " xactual=" << pitch_.xenc << " xenc=" << pitch_.xenc
|
<< " speed=200000 eeprom_restored=1 has_encoder=1\n";
|
||||||
<< " DRV_STATUS=80084000 GSTAT=00\n"
|
d << "DUMP " << L << " TMC GCONF=0x00000004 GSTAT=0x00000000"
|
||||||
<< "DUMP END\n";
|
<< " IOIN=0x30000008 TSTEP=0x000fffff RAMPMODE=0x00000000"
|
||||||
|
<< " XACTUAL=0x" << std::hex << (a.xactual & 0xffffffff)
|
||||||
|
<< " VACTUAL=0x00000000 XTARGET=0x" << (a.xenc & 0xffffffff) << std::dec
|
||||||
|
<< " SW_MODE=0x00000000 RAMP_STAT=0x00000300 X_ENC=0x" << std::hex
|
||||||
|
<< (a.xenc & 0xffffffff) << std::dec
|
||||||
|
<< " ENC_STATUS=0x00000000 CHOPCONF=0x10410150"
|
||||||
|
<< " DRV_STATUS=0x80084000 PWM_SCALE=0x00000008 PWM_AUTO=0x00000000\n";
|
||||||
|
};
|
||||||
|
axis('Y', yaw_);
|
||||||
|
axis('P', pitch_);
|
||||||
|
d << "DUMP END\n";
|
||||||
dump_ = d.str();
|
dump_ = d.str();
|
||||||
LOG_INFO << "firmware dump:\n" << dump_;
|
LOG_INFO << "firmware dump:\n" << dump_;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ struct AxisView {
|
||||||
long xactual = 0;
|
long xactual = 0;
|
||||||
long xenc = 0;
|
long xenc = 0;
|
||||||
double target_deg = 0.0; // current scheduler target
|
double target_deg = 0.0; // current scheduler target
|
||||||
|
int sg = 0; // SG_RESULT (live, from ST line)
|
||||||
|
int cs = 0; // CS_ACTUAL
|
||||||
|
int pwm = 0; // PWM_SCALE_SUM
|
||||||
|
unsigned drv_status = 0; // DRV_STATUS register
|
||||||
bool moving = false;
|
bool moving = false;
|
||||||
bool standstill = false;
|
bool standstill = false;
|
||||||
bool stall = false;
|
bool stall = false;
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ struct Application::Impl {
|
||||||
v.xactual = a.xactual;
|
v.xactual = a.xactual;
|
||||||
v.xenc = a.xenc;
|
v.xenc = a.xenc;
|
||||||
v.target_deg = map.toDeg(target_counts);
|
v.target_deg = map.toDeg(target_counts);
|
||||||
|
v.sg = a.sg;
|
||||||
|
v.cs = a.cs;
|
||||||
|
v.pwm = a.pwm;
|
||||||
|
v.drv_status = a.drv_status;
|
||||||
v.moving = a.moving();
|
v.moving = a.moving();
|
||||||
v.standstill = a.standstill;
|
v.standstill = a.standstill;
|
||||||
v.stall = a.stall;
|
v.stall = a.stall;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
#include "fgc/DumpParser.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <sstream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Bit tables, copied verbatim from firmware/tools/decode_dump.py (keep in sync).
|
||||||
|
const char* stateName(int s) {
|
||||||
|
switch (s) {
|
||||||
|
case 0: return "BOOT";
|
||||||
|
case 1: return "RESET";
|
||||||
|
case 2: return "HOMING";
|
||||||
|
case 3: return "READY";
|
||||||
|
case 4: return "ERROR";
|
||||||
|
default: return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bit { int shift; const char* name; };
|
||||||
|
|
||||||
|
const std::array<Bit, 5> kMcusr = {{
|
||||||
|
{0, "PORF (power-on)"}, {1, "EXTRF (external)"}, {2, "BORF (brown-out)"},
|
||||||
|
{3, "WDRF (watchdog)"}, {4, "JTRF (jtag)"},
|
||||||
|
}};
|
||||||
|
const std::array<Bit, 12> kDrv = {{
|
||||||
|
{31, "stst"}, {30, "olb"}, {29, "ola"}, {28, "s2gb"}, {27, "s2ga"},
|
||||||
|
{26, "otpw"}, {25, "ot"}, {24, "stallguard"}, {15, "fsactive"},
|
||||||
|
{14, "stealth"}, {12, "s2vsb"}, {11, "s2vsa"},
|
||||||
|
}};
|
||||||
|
const std::array<Bit, 3> kGstat = {{
|
||||||
|
{0, "reset"}, {1, "drv_err"}, {2, "uv_cp"},
|
||||||
|
}};
|
||||||
|
const std::array<Bit, 10> kRamp = {{
|
||||||
|
{0, "stop_l"}, {1, "stop_r"}, {4, "event_stop_l"}, {5, "event_stop_r"},
|
||||||
|
{6, "event_stop_sg"}, {7, "event_pos_reached"}, {8, "velocity_reached"},
|
||||||
|
{9, "position_reached"}, {10, "vzero"}, {13, "status_sg"},
|
||||||
|
}};
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
std::vector<std::string> bitsSet(unsigned val, const std::array<Bit, N>& table) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
for (const auto& b : table)
|
||||||
|
if (val & (1u << b.shift)) out.emplace_back(b.name);
|
||||||
|
if (out.empty()) out.emplace_back("-");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse "key=value key=value ..." into a map.
|
||||||
|
std::map<std::string, std::string> kvPairs(const std::string& s) {
|
||||||
|
std::map<std::string, std::string> m;
|
||||||
|
std::istringstream iss(s);
|
||||||
|
std::string tok;
|
||||||
|
while (iss >> tok) {
|
||||||
|
auto eq = tok.find('=');
|
||||||
|
if (eq != std::string::npos) m[tok.substr(0, eq)] = tok.substr(eq + 1);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integer from a decimal or "0x"-prefixed hex string; `fallback` on failure.
|
||||||
|
long asInt(const std::string& s, long fallback = 0) {
|
||||||
|
if (s.empty()) return fallback;
|
||||||
|
try {
|
||||||
|
size_t pos = 0;
|
||||||
|
bool hex = s.size() > 1 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X');
|
||||||
|
long v = std::stol(s, &pos, hex ? 16 : 10);
|
||||||
|
return v;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get(const std::map<std::string, std::string>& m, const std::string& k,
|
||||||
|
const std::string& fallback = "") {
|
||||||
|
auto it = m.find(k);
|
||||||
|
return it == m.end() ? fallback : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string join(const std::vector<std::string>& v) {
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) out += (i ? " " : "") + v[i];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DumpData parseDump(const std::string& block) {
|
||||||
|
DumpData d;
|
||||||
|
if (block.empty()) return d;
|
||||||
|
|
||||||
|
// Split into lines, locate the BEGIN..END envelope.
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
{
|
||||||
|
std::istringstream iss(block);
|
||||||
|
std::string l;
|
||||||
|
while (std::getline(iss, l)) {
|
||||||
|
if (!l.empty() && l.back() == '\r') l.pop_back();
|
||||||
|
lines.push_back(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t begin = lines.size(), end = lines.size();
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (begin == lines.size() && lines[i].rfind("DUMP BEGIN", 0) == 0) begin = i;
|
||||||
|
if (lines[i].rfind("DUMP END", 0) == 0) { end = i; break; }
|
||||||
|
}
|
||||||
|
if (begin == lines.size() || end <= begin) return d;
|
||||||
|
|
||||||
|
// Header.
|
||||||
|
auto hdr = kvPairs(lines[begin].substr(std::string("DUMP BEGIN").size()));
|
||||||
|
d.build = get(hdr, "build");
|
||||||
|
d.uptime_ms = asInt(get(hdr, "uptime", "0"));
|
||||||
|
d.mcusr = static_cast<unsigned>(asInt(get(hdr, "mcusr", "0")));
|
||||||
|
d.free_ram = asInt(get(hdr, "free_ram", "0"));
|
||||||
|
d.reset_flags = bitsSet(d.mcusr, kMcusr);
|
||||||
|
|
||||||
|
// Per-axis lines: "DUMP <Y|P> state=.." and "DUMP <Y|P> TMC ..".
|
||||||
|
auto axisFor = [&d](char a) -> DumpAxis& {
|
||||||
|
for (auto& ax : d.axes)
|
||||||
|
if (ax.axis == a) return ax;
|
||||||
|
d.axes.push_back(DumpAxis{});
|
||||||
|
d.axes.back().axis = a;
|
||||||
|
return d.axes.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = begin + 1; i < end; ++i) {
|
||||||
|
const std::string& l = lines[i];
|
||||||
|
if (l.rfind("DUMP ", 0) != 0 || l.size() < 7) continue;
|
||||||
|
char a = l[5];
|
||||||
|
if (a != 'Y' && a != 'P') continue;
|
||||||
|
// Skip past "DUMP X" and any whitespace; the payload is either
|
||||||
|
// "state=.." or "TMC GCONF=..". (substr(6) alone leaves a leading space,
|
||||||
|
// which would make the "TMC" prefix check below miss.)
|
||||||
|
std::string rest = l.substr(6);
|
||||||
|
size_t nb = rest.find_first_not_of(" \t");
|
||||||
|
rest = (nb == std::string::npos) ? "" : rest.substr(nb);
|
||||||
|
DumpAxis& ax = axisFor(a);
|
||||||
|
|
||||||
|
if (rest.rfind("TMC", 0) == 0) {
|
||||||
|
auto m = kvPairs(rest.substr(3));
|
||||||
|
ax.regs = m;
|
||||||
|
ax.drv_status = static_cast<unsigned>(asInt(get(m, "DRV_STATUS", "0")));
|
||||||
|
ax.gstat = static_cast<unsigned>(asInt(get(m, "GSTAT", "0")));
|
||||||
|
ax.ramp_stat = static_cast<unsigned>(asInt(get(m, "RAMP_STAT", "0")));
|
||||||
|
ax.cs_actual = static_cast<int>((ax.drv_status >> 16) & 0x1F);
|
||||||
|
ax.sg_result = static_cast<int>(ax.drv_status & 0x3FF);
|
||||||
|
ax.drv_flags = bitsSet(ax.drv_status, kDrv);
|
||||||
|
ax.gstat_flags = bitsSet(ax.gstat, kGstat);
|
||||||
|
ax.ramp_flags = bitsSet(ax.ramp_stat, kRamp);
|
||||||
|
} else {
|
||||||
|
auto m = kvPairs(rest);
|
||||||
|
ax.state_name = stateName(static_cast<int>(asInt(get(m, "state", "-1"))));
|
||||||
|
ax.hsub = static_cast<int>(asInt(get(m, "hsub", "0")));
|
||||||
|
ax.enabled = asInt(get(m, "enabled", "0")) != 0;
|
||||||
|
ax.eeprom_restored = asInt(get(m, "eeprom_restored", "0")) != 0;
|
||||||
|
ax.has_encoder = asInt(get(m, "has_encoder", "0")) != 0;
|
||||||
|
ax.lim_neg = asInt(get(m, "lim_neg", "0"));
|
||||||
|
ax.lim_pos = asInt(get(m, "lim_pos", "0"));
|
||||||
|
ax.hold_target = asInt(get(m, "hold_target", "0"));
|
||||||
|
ax.speed = asInt(get(m, "speed", "0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.valid = true;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> formatDump(const DumpData& d) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
if (!d.valid) {
|
||||||
|
out.emplace_back("(no firmware dump captured)");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::ostringstream os;
|
||||||
|
os << "build=" << d.build << " uptime=" << d.uptime_ms << " ms free_ram="
|
||||||
|
<< d.free_ram << " bytes";
|
||||||
|
out.push_back(os.str());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::ostringstream os;
|
||||||
|
os << "reset cause (mcusr=0x" << std::hex << d.mcusr << std::dec << "): "
|
||||||
|
<< join(d.reset_flags);
|
||||||
|
out.push_back(os.str());
|
||||||
|
}
|
||||||
|
for (const auto& ax : d.axes) {
|
||||||
|
out.emplace_back("");
|
||||||
|
{
|
||||||
|
std::ostringstream os;
|
||||||
|
os << "[" << ax.axis << "] state=" << ax.state_name
|
||||||
|
<< " enabled=" << (ax.enabled ? 1 : 0)
|
||||||
|
<< " limits=[" << ax.lim_neg << ".." << ax.lim_pos << "]"
|
||||||
|
<< " hold_target=" << ax.hold_target
|
||||||
|
<< " eeprom_restored=" << (ax.eeprom_restored ? 1 : 0)
|
||||||
|
<< " has_encoder=" << (ax.has_encoder ? 1 : 0);
|
||||||
|
out.push_back(os.str());
|
||||||
|
}
|
||||||
|
for (const char* k : {"GCONF", "CHOPCONF", "XACTUAL", "X_ENC", "XTARGET", "VACTUAL"}) {
|
||||||
|
auto it = ax.regs.find(k);
|
||||||
|
if (it != ax.regs.end()) out.push_back(std::string(" ") + k + " = " + it->second);
|
||||||
|
}
|
||||||
|
out.push_back(" DRV_STATUS = " + get(ax.regs, "DRV_STATUS", "?") + " [" +
|
||||||
|
join(ax.drv_flags) + "] CS_ACTUAL=" + std::to_string(ax.cs_actual) +
|
||||||
|
" SG_RESULT=" + std::to_string(ax.sg_result));
|
||||||
|
out.push_back(" GSTAT = " + get(ax.regs, "GSTAT", "?") + " [" +
|
||||||
|
join(ax.gstat_flags) + "]");
|
||||||
|
out.push_back(" RAMP_STAT = " + get(ax.regs, "RAMP_STAT", "?") + " [" +
|
||||||
|
join(ax.ramp_flags) + "]");
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
161
src/ui/TuiUi.cpp
161
src/ui/TuiUi.cpp
|
|
@ -1,9 +1,12 @@
|
||||||
#include "fgc/ui/TuiUi.h"
|
#include "fgc/ui/TuiUi.h"
|
||||||
|
|
||||||
|
#include "fgc/DumpParser.h"
|
||||||
#include "fgc/HelpText.h"
|
#include "fgc/HelpText.h"
|
||||||
#include "fgc/Logger.h"
|
#include "fgc/Logger.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -190,6 +193,122 @@ Element helpPanel(int sel, const DumpView& dump) {
|
||||||
vbox(std::move(rows)) | yframe);
|
vbox(std::move(rows)) | yframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aligned "label: value" row for the detail view.
|
||||||
|
Element kvRow(const std::string& k, const std::string& v, Color vc = Color::Default) {
|
||||||
|
return hbox({text(k) | dim | size(WIDTH, EQUAL, 12), text(v) | color(vc)});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hex8(unsigned v) {
|
||||||
|
char buf[11];
|
||||||
|
std::snprintf(buf, sizeof(buf), "0x%08X", v);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_or(const std::map<std::string, std::string>& m, const std::string& k) {
|
||||||
|
auto it = m.find(k);
|
||||||
|
return it == m.end() ? "?" : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live per-axis block: everything we get from the ST telemetry line.
|
||||||
|
Element axisLiveDetail(const AxisView& a) {
|
||||||
|
Element state = text(std::string(" ") + axisStateLabel(a.state) + " ")
|
||||||
|
| color(toColor(axisStateColor(a.state))) | bold;
|
||||||
|
std::vector<Element> rows = {
|
||||||
|
hbox({text(a.label + " ") | bold, state, filler(),
|
||||||
|
text(formatDegrees(a.deg) + " -> " + formatDegrees(a.target_deg)) | bold}),
|
||||||
|
kvRow("xactual", std::to_string(a.xactual)),
|
||||||
|
kvRow("xenc", std::to_string(a.xenc)),
|
||||||
|
kvRow("SG_RESULT", std::to_string(a.sg)),
|
||||||
|
kvRow("CS_ACTUAL", std::to_string(a.cs)),
|
||||||
|
kvRow("PWM", std::to_string(a.pwm)),
|
||||||
|
kvRow("DRV_STATUS", hex8(a.drv_status), Color::Cyan),
|
||||||
|
hbox({text("flags") | dim | size(WIDTH, EQUAL, 12),
|
||||||
|
badge("STILL", a.standstill, Color::GrayLight),
|
||||||
|
badge("MOVE", a.moving, Color::Cyan),
|
||||||
|
badge("STALL", a.stall, Color::Red),
|
||||||
|
badge("OT", a.overtemp, Color::Red),
|
||||||
|
badge("L", a.endstop_l, Color::Yellow),
|
||||||
|
badge("R", a.endstop_r, Color::Yellow)}),
|
||||||
|
};
|
||||||
|
return vbox(std::move(rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoded register block for one axis from a parsed firmware dump.
|
||||||
|
Element axisDumpDetail(const DumpAxis& ax) {
|
||||||
|
auto reg = [&](const char* k) -> Element {
|
||||||
|
auto it = ax.regs.find(k);
|
||||||
|
return kvRow(k, it == ax.regs.end() ? "-" : it->second);
|
||||||
|
};
|
||||||
|
auto flags = [](const std::vector<std::string>& f) {
|
||||||
|
std::string s;
|
||||||
|
for (size_t i = 0; i < f.size(); ++i) s += (i ? " " : "") + f[i];
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
auto statusRow = [&](const char* name, const std::string& raw,
|
||||||
|
const std::vector<std::string>& f) {
|
||||||
|
return hbox({text(name) | dim | size(WIDTH, EQUAL, 12),
|
||||||
|
text(raw + " ") | color(Color::Cyan),
|
||||||
|
text("[" + flags(f) + "]") | dim});
|
||||||
|
};
|
||||||
|
return vbox({
|
||||||
|
hbox({text(std::string(1, ax.axis) + " ") | bold,
|
||||||
|
text(ax.state_name) | color(Color::Green) | bold,
|
||||||
|
text(" enabled=" + std::to_string(ax.enabled ? 1 : 0)) | dim,
|
||||||
|
text(" enc=" + std::to_string(ax.has_encoder ? 1 : 0)) | dim,
|
||||||
|
text(" eeprom=" + std::to_string(ax.eeprom_restored ? 1 : 0)) | dim}),
|
||||||
|
kvRow("limits", "[" + std::to_string(ax.lim_neg) + ".." + std::to_string(ax.lim_pos) + "]"),
|
||||||
|
kvRow("hold_tgt", std::to_string(ax.hold_target)),
|
||||||
|
reg("GCONF"), reg("CHOPCONF"), reg("XACTUAL"), reg("X_ENC"),
|
||||||
|
reg("XTARGET"), reg("VACTUAL"),
|
||||||
|
statusRow("DRV_STATUS", get_or(ax.regs, "DRV_STATUS"), ax.drv_flags),
|
||||||
|
hbox({text("") | size(WIDTH, EQUAL, 12),
|
||||||
|
text("CS_ACTUAL=" + std::to_string(ax.cs_actual) +
|
||||||
|
" SG_RESULT=" + std::to_string(ax.sg_result)) | dim}),
|
||||||
|
statusRow("GSTAT", get_or(ax.regs, "GSTAT"), ax.gstat_flags),
|
||||||
|
statusRow("RAMP_STAT", get_or(ax.regs, "RAMP_STAT"), ax.ramp_flags),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-screen gimbal view (the takeover panel toggled with 'g'): live telemetry
|
||||||
|
// for both axes on the left, the decoded firmware register dump on the right.
|
||||||
|
Element gimbalDetailPanel(const GimbalView& g, const DumpView& dump) {
|
||||||
|
// Live column.
|
||||||
|
std::vector<Element> live;
|
||||||
|
if (!g.present) live.push_back(text("link down") | color(Color::Red) | bold);
|
||||||
|
live.push_back(axisLiveDetail(g.yaw));
|
||||||
|
if (g.pitch_present) {
|
||||||
|
live.push_back(separator());
|
||||||
|
live.push_back(axisLiveDetail(g.pitch));
|
||||||
|
}
|
||||||
|
Element live_col = panel("GIMBAL · live", Color::Cyan, vbox(std::move(live)));
|
||||||
|
|
||||||
|
// Register column (decoded firmware dump).
|
||||||
|
std::vector<Element> regs;
|
||||||
|
DumpData d = parseDump(dump.text);
|
||||||
|
if (!d.valid) {
|
||||||
|
regs.push_back(text("(requesting firmware dump...)") | dim);
|
||||||
|
regs.push_back(text("press 'd' to refresh") | dim);
|
||||||
|
} else {
|
||||||
|
regs.push_back(hbox({text("build " + d.build) | dim, filler(),
|
||||||
|
text("up " + std::to_string(d.uptime_ms) + "ms") | dim}));
|
||||||
|
{
|
||||||
|
std::string rc;
|
||||||
|
for (size_t i = 0; i < d.reset_flags.size(); ++i)
|
||||||
|
rc += (i ? " " : "") + d.reset_flags[i];
|
||||||
|
regs.push_back(text("reset: " + rc) | dim);
|
||||||
|
}
|
||||||
|
for (const auto& ax : d.axes) {
|
||||||
|
regs.push_back(separator());
|
||||||
|
regs.push_back(axisDumpDetail(ax));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Element reg_col = panel("GIMBAL · firmware registers", Color::Cyan,
|
||||||
|
vbox(std::move(regs)) | yframe);
|
||||||
|
|
||||||
|
return window(text(" GIMBAL (g/Esc:close d:refresh dump) ") | bold | color(Color::Cyan),
|
||||||
|
hbox({live_col, reg_col}));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TuiUi::TuiUi() = default;
|
TuiUi::TuiUi() = default;
|
||||||
|
|
@ -234,8 +353,10 @@ void TuiUi::refreshLoop() {
|
||||||
void TuiUi::uiLoop() {
|
void TuiUi::uiLoop() {
|
||||||
std::string cmd_buffer;
|
std::string cmd_buffer;
|
||||||
bool command_mode = false;
|
bool command_mode = false;
|
||||||
bool help_open = false;
|
enum class Overlay { None, Help, Gimbal };
|
||||||
|
Overlay overlay = Overlay::None; // which takeover panel owns the main area
|
||||||
int help_sel = 0;
|
int help_sel = 0;
|
||||||
|
bool gimbal_dump_requested = false; // auto-pull a dump the first time
|
||||||
|
|
||||||
auto input = Input(&cmd_buffer, "type a command, Enter to run, Esc to cancel");
|
auto input = Input(&cmd_buffer, "type a command, Enter to run, Esc to cancel");
|
||||||
|
|
||||||
|
|
@ -265,12 +386,17 @@ void TuiUi::uiLoop() {
|
||||||
} else {
|
} else {
|
||||||
bottom = hbox({
|
bottom = hbox({
|
||||||
keyHint("s", "Start"), keyHint("x", "Stop"), keyHint("h", "Home"),
|
keyHint("s", "Start"), keyHint("x", "Stop"), keyHint("h", "Home"),
|
||||||
keyHint("r", "Reset"), keyHint(":", "Cmd"), keyHint("?", "Help"),
|
keyHint("r", "Reset"), keyHint("g", "Gimbal"), keyHint(":", "Cmd"),
|
||||||
filler(), keyHint("q", "Quit"),
|
keyHint("?", "Help"), filler(), keyHint("q", "Quit"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Element main = help_open ? helpPanel(help_sel, s.dump) : logPanel(s.log);
|
Element main;
|
||||||
|
switch (overlay) {
|
||||||
|
case Overlay::Help: main = helpPanel(help_sel, s.dump); break;
|
||||||
|
case Overlay::Gimbal: main = gimbalDetailPanel(s.gimbal, s.dump); break;
|
||||||
|
default: main = logPanel(s.log); break;
|
||||||
|
}
|
||||||
return vbox({header, separator(), top, middle, main | flex, bottom});
|
return vbox({header, separator(), top, middle, main | flex, bottom});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -290,15 +416,34 @@ void TuiUi::uiLoop() {
|
||||||
return false; // let the Input edit the buffer
|
return false; // let the Input edit the buffer
|
||||||
}
|
}
|
||||||
const int n = static_cast<int>(helpCatalog().size());
|
const int n = static_cast<int>(helpCatalog().size());
|
||||||
if (help_open) { // help pane navigation
|
if (overlay == Overlay::Help) { // help pane navigation
|
||||||
if (e == Event::ArrowDown) { help_sel = (help_sel + 1) % n; return true; }
|
if (e == Event::ArrowDown) { help_sel = (help_sel + 1) % n; return true; }
|
||||||
if (e == Event::ArrowUp) { help_sel = (help_sel - 1 + n) % n; return true; }
|
if (e == Event::ArrowUp) { help_sel = (help_sel - 1 + n) % n; return true; }
|
||||||
if (e == Event::Escape) { help_open = false; return true; }
|
|
||||||
}
|
}
|
||||||
|
if (overlay != Overlay::None && e == Event::Escape) { overlay = Overlay::None; return true; }
|
||||||
if (!e.is_character()) return false;
|
if (!e.is_character()) return false;
|
||||||
const std::string& c = e.character();
|
const std::string& c = e.character();
|
||||||
if (c == "?") { help_open = !help_open; return true; }
|
if (c == "?") {
|
||||||
if (help_open) { // vim-style section nav while help is open
|
overlay = (overlay == Overlay::Help) ? Overlay::None : Overlay::Help;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c == "g") {
|
||||||
|
if (overlay == Overlay::Gimbal) {
|
||||||
|
overlay = Overlay::None;
|
||||||
|
} else {
|
||||||
|
overlay = Overlay::Gimbal;
|
||||||
|
if (!gimbal_dump_requested) { // auto-pull a dump the first time
|
||||||
|
if (sink_) sink_("dump");
|
||||||
|
gimbal_dump_requested = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (overlay == Overlay::Gimbal && c == "d") { // manual dump refresh
|
||||||
|
if (sink_) sink_("dump");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (overlay == Overlay::Help) { // vim-style section nav while help is open
|
||||||
if (c == "j") { help_sel = (help_sel + 1) % n; return true; }
|
if (c == "j") { help_sel = (help_sel + 1) % n; return true; }
|
||||||
if (c == "k") { help_sel = (help_sel - 1 + n) % n; return true; }
|
if (c == "k") { help_sel = (help_sel - 1 + n) % n; return true; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue