added help info
This commit is contained in:
parent
09b032b998
commit
0b62fef9c7
|
|
@ -50,6 +50,7 @@ add_library(fgc_core STATIC
|
||||||
src/core/TelemetryParser.cpp
|
src/core/TelemetryParser.cpp
|
||||||
src/core/CaptureScheduler.cpp
|
src/core/CaptureScheduler.cpp
|
||||||
src/core/CommandParser.cpp
|
src/core/CommandParser.cpp
|
||||||
|
src/core/HelpText.cpp
|
||||||
src/ui/UiSnapshot.cpp
|
src/ui/UiSnapshot.cpp
|
||||||
src/ui/HeadlessUi.cpp
|
src/ui/HeadlessUi.cpp
|
||||||
ini.c
|
ini.c
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
// Operator command reference, modelled as data so the headless console and the
|
||||||
|
// TUI render from one source of truth (avoids the two drifting apart). The
|
||||||
|
// console prints renderHelp(); the TUI walks helpCatalog() to draw its inline
|
||||||
|
// help pane.
|
||||||
|
|
||||||
|
struct HelpEntry {
|
||||||
|
std::string syntax; // e.g. "goto <yaw_deg> <pitch_deg>"
|
||||||
|
std::string summary; // one-line description
|
||||||
|
std::vector<std::string> detail; // expanded lines: examples, units, notes
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HelpSection {
|
||||||
|
std::string title; // e.g. "Positioning"
|
||||||
|
std::string blurb; // one line under the title
|
||||||
|
std::vector<HelpEntry> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full catalog (static, built once).
|
||||||
|
const std::vector<HelpSection>& helpCatalog();
|
||||||
|
|
||||||
|
// Flatten the catalog to printable lines for the console.
|
||||||
|
// topic == "" -> every section's title/blurb + one summary line per entry.
|
||||||
|
// topic == "<x>" -> the matching section(s)/entry(ies) with their detail lines.
|
||||||
|
// Matching is case-insensitive against section titles and the first token of
|
||||||
|
// each entry's syntax (the command verb).
|
||||||
|
std::vector<std::string> renderHelp(const std::string& topic = "");
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
|
|
@ -53,6 +53,11 @@ public:
|
||||||
// Latest telemetry snapshot (thread-safe in implementations).
|
// Latest telemetry snapshot (thread-safe in implementations).
|
||||||
virtual MotorTelemetry telemetry() = 0;
|
virtual MotorTelemetry telemetry() = 0;
|
||||||
|
|
||||||
|
// The most recently completed firmware DUMP block (the text between
|
||||||
|
// "DUMP BEGIN" and "DUMP END", inclusive), or "" if none has been captured.
|
||||||
|
// Thread-safe in implementations.
|
||||||
|
virtual std::string lastDump() = 0;
|
||||||
|
|
||||||
// Whether the underlying link is usable.
|
// Whether the underlying link is usable.
|
||||||
virtual bool connected() const = 0;
|
virtual bool connected() const = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public:
|
||||||
void stop() override;
|
void stop() override;
|
||||||
void sendCommand(const std::string& cmd) override;
|
void sendCommand(const std::string& cmd) override;
|
||||||
MotorTelemetry telemetry() override;
|
MotorTelemetry telemetry() override;
|
||||||
|
std::string lastDump() override;
|
||||||
bool connected() const override;
|
bool connected() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,20 @@ public:
|
||||||
} else if (verb == "STOP") {
|
} else if (verb == "STOP") {
|
||||||
yaw_target_ = yaw_.xenc;
|
yaw_target_ = yaw_.xenc;
|
||||||
pitch_target_ = pitch_.xenc;
|
pitch_target_ = pitch_.xenc;
|
||||||
|
} else if (verb == "DUMP") {
|
||||||
|
// Mirror the firmware DUMP block so the `dump` path is demoable
|
||||||
|
// without hardware. Values are illustrative, not simulated.
|
||||||
|
std::ostringstream d;
|
||||||
|
d << "DUMP BEGIN build=mock uptime_ms=0 reset=POR\n"
|
||||||
|
<< "Y state=" << (homed_ ? "READY" : "RESET")
|
||||||
|
<< " xactual=" << yaw_.xenc << " xenc=" << yaw_.xenc
|
||||||
|
<< " DRV_STATUS=80084000 GSTAT=00\n"
|
||||||
|
<< "P state=" << (homed_ ? "READY" : "RESET")
|
||||||
|
<< " xactual=" << pitch_.xenc << " xenc=" << pitch_.xenc
|
||||||
|
<< " DRV_STATUS=80084000 GSTAT=00\n"
|
||||||
|
<< "DUMP END\n";
|
||||||
|
dump_ = d.str();
|
||||||
|
LOG_INFO << "firmware dump:\n" << dump_;
|
||||||
}
|
}
|
||||||
// ENABLE/DISABLE/SPEED/SETPOS/RESET: accepted, no simulation effect.
|
// ENABLE/DISABLE/SPEED/SETPOS/RESET: accepted, no simulation effect.
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +75,11 @@ public:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string lastDump() override {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return dump_;
|
||||||
|
}
|
||||||
|
|
||||||
bool connected() const override { return true; }
|
bool connected() const override { return true; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -91,6 +110,7 @@ private:
|
||||||
long yaw_target_ = 0;
|
long yaw_target_ = 0;
|
||||||
long pitch_target_ = 0;
|
long pitch_target_ = 0;
|
||||||
bool homed_ = false;
|
bool homed_ = false;
|
||||||
|
std::string dump_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,12 @@ struct LogLine {
|
||||||
std::string text; // formatted line (no trailing newline)
|
std::string text; // formatted line (no trailing newline)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Most recent firmware DUMP block (raw text), surfaced in the TUI help pane.
|
||||||
|
struct DumpView {
|
||||||
|
bool has = false;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
struct UiSnapshot {
|
struct UiSnapshot {
|
||||||
HeaderView header;
|
HeaderView header;
|
||||||
GimbalView gimbal;
|
GimbalView gimbal;
|
||||||
|
|
@ -99,6 +105,7 @@ struct UiSnapshot {
|
||||||
CaptureView capture;
|
CaptureView capture;
|
||||||
ConnView conn;
|
ConnView conn;
|
||||||
std::vector<LogLine> log;
|
std::vector<LogLine> log;
|
||||||
|
DumpView dump;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Pure formatting helpers (unit-tested in tests/test_uisnapshot.cpp) ----
|
// ---- Pure formatting helpers (unit-tested in tests/test_uisnapshot.cpp) ----
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "fgc/CaptureScheduler.h"
|
#include "fgc/CaptureScheduler.h"
|
||||||
#include "fgc/CommandParser.h"
|
#include "fgc/CommandParser.h"
|
||||||
|
#include "fgc/HelpText.h"
|
||||||
#include "fgc/ICameraSource.h"
|
#include "fgc/ICameraSource.h"
|
||||||
#include "fgc/IControlChannel.h"
|
#include "fgc/IControlChannel.h"
|
||||||
#include "fgc/IMotorController.h"
|
#include "fgc/IMotorController.h"
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
|
@ -195,6 +197,10 @@ struct Application::Impl {
|
||||||
s.conn.control_code = scheduler ? scheduler->controlCode() : 0;
|
s.conn.control_code = scheduler ? scheduler->controlCode() : 0;
|
||||||
s.conn.target_heading = scheduler ? scheduler->targetHeading() : "0";
|
s.conn.target_heading = scheduler ? scheduler->targetHeading() : "0";
|
||||||
s.conn.last_status_code = s.conn.control_code; // echoed back as status
|
s.conn.last_status_code = s.conn.control_code; // echoed back as status
|
||||||
|
|
||||||
|
// --- Diagnostics (last firmware DUMP) ---
|
||||||
|
s.dump.text = motor->lastDump();
|
||||||
|
s.dump.has = !s.dump.text.empty();
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,12 +283,39 @@ struct Application::Impl {
|
||||||
LOG_INFO << "trace categories: " << traceNames(Logger::categories());
|
LOG_INFO << "trace categories: " << traceNames(Logger::categories());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `goto <yaw_deg> <pitch_deg>` — aim the gimbal at an absolute heading and
|
||||||
|
// elevation in degrees. Converts to encoder counts via the operator-calibrated
|
||||||
|
// Geometry maps (which soft-clamp to the travel limits) and issues a two-axis
|
||||||
|
// MOVE so both axes start together.
|
||||||
|
void handleGoto(const std::string& line) {
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::string verb;
|
||||||
|
double yaw_deg = 0.0, pitch_deg = 0.0;
|
||||||
|
if (!(iss >> verb >> yaw_deg >> pitch_deg)) {
|
||||||
|
LOG_WARN << "usage: goto <yaw_deg> <pitch_deg> (e.g. goto 30 -10)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long yc = cfg.geometry.yaw.toCounts(yaw_deg);
|
||||||
|
long pc = cfg.geometry.pitch.toCounts(pitch_deg);
|
||||||
|
LOG_INFO << "goto yaw=" << yaw_deg << "deg pitch=" << pitch_deg
|
||||||
|
<< "deg -> MOVE " << yc << "," << pc;
|
||||||
|
motor->sendCommand("MOVE " + std::to_string(yc) + "," + std::to_string(pc));
|
||||||
|
}
|
||||||
|
|
||||||
void handleCommand(const std::string& line) {
|
void handleCommand(const std::string& line) {
|
||||||
Command c = parseCommand(line);
|
Command c = parseCommand(line);
|
||||||
if (c.empty()) return;
|
if (c.empty()) return;
|
||||||
LOG_TRACE_CAT(LogCat::Control) << "cmd " << line;
|
LOG_TRACE_CAT(LogCat::Control) << "cmd " << line;
|
||||||
|
|
||||||
if (c.verb == "exit") {
|
if (c.verb == "help") {
|
||||||
|
// c.device holds the optional topic (first token after "help").
|
||||||
|
for (const auto& l : renderHelp(c.device)) LOG_INFO << l;
|
||||||
|
} else if (c.verb == "goto") {
|
||||||
|
handleGoto(line);
|
||||||
|
} else if (c.verb == "dump") {
|
||||||
|
LOG_INFO << "requesting firmware dump...";
|
||||||
|
motor->sendCommand("DUMP");
|
||||||
|
} else if (c.verb == "exit") {
|
||||||
running = false;
|
running = false;
|
||||||
} else if (c.verb == "start") {
|
} else if (c.verb == "start") {
|
||||||
startCapture();
|
startCapture();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include "fgc/HelpText.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string lower(std::string s) {
|
||||||
|
std::transform(s.begin(), s.end(), s.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First whitespace-delimited token of a syntax string (the command verb), lowercased.
|
||||||
|
std::string verbOf(const std::string& syntax) {
|
||||||
|
auto end = syntax.find_first_of(" \t");
|
||||||
|
return lower(syntax.substr(0, end == std::string::npos ? syntax.size() : end));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const std::vector<HelpSection>& helpCatalog() {
|
||||||
|
// clang-format off
|
||||||
|
static const std::vector<HelpSection> catalog = {
|
||||||
|
{"Positioning", "Aim the gimbal. 'goto' is degrees; raw MOVE is encoder counts.", {
|
||||||
|
{"goto <yaw_deg> <pitch_deg>",
|
||||||
|
"Point the gimbal at an absolute heading/elevation in degrees.", {
|
||||||
|
"Converts degrees to encoder counts (operator-calibrated) and sends a",
|
||||||
|
"two-axis MOVE so both axes start together. Degrees are soft-clamped to",
|
||||||
|
"the configured travel limits.",
|
||||||
|
"Example: goto 30 -10 (yaw 30 deg, pitch -10 deg)"}},
|
||||||
|
{"set motorctl MOVE <yaw>,<pitch>",
|
||||||
|
"Move both axes to absolute encoder counts (no degree conversion).", {
|
||||||
|
"Example: set motorctl MOVE 100000,250000",
|
||||||
|
"Single axis: set motorctl MOVE Y 100000 / set motorctl MOVE P 250000"}},
|
||||||
|
{"set motorctl HOME [Y|P]",
|
||||||
|
"Run the endstop-finding home sequence (both axes, or one).", {
|
||||||
|
"Example: set motorctl HOME / set motorctl HOME Y"}},
|
||||||
|
{"set motorctl STOP <Y|P|ALL>",
|
||||||
|
"Stop motion immediately on an axis or both.", {}},
|
||||||
|
{"set motorctl SPEED <Y|P> <vel>",
|
||||||
|
"Set the max slew speed (counts/s) for an axis.", {}},
|
||||||
|
}},
|
||||||
|
{"Diagnostics", "Inspect the firmware/driver state for debugging.", {
|
||||||
|
{"dump",
|
||||||
|
"Request a full firmware state dump and show it here.", {
|
||||||
|
"Sends DUMP to the firmware; the captured DUMP BEGIN..END block (build,",
|
||||||
|
"uptime, reset cause, and per-axis TMC5160 registers) is logged and, in",
|
||||||
|
"the TUI, shown in the Diagnostics help section.",
|
||||||
|
"Equivalent offline tool: ./firmware/dump.sh"}},
|
||||||
|
{"set motorctl DIAG [Y|P|ALL]",
|
||||||
|
"Run the motor self-test (emits DG lines, ends with DG DONE).", {
|
||||||
|
"Each axis is swept at several speeds/directions; results stream as DG",
|
||||||
|
"lines in the log. Axis must be homed first."}},
|
||||||
|
{"set motorctl STATUS",
|
||||||
|
"Ask the firmware to emit one telemetry (ST) line now.", {}},
|
||||||
|
}},
|
||||||
|
{"Capture", "Control image capture and encoding.", {
|
||||||
|
{"start", "Begin the capture scan.", {}},
|
||||||
|
{"stop", "Halt the capture scan.", {}},
|
||||||
|
{"set fps <rate>", "Set capture rate in images/second.", {}},
|
||||||
|
{"set camera fps <rate>", "Set the camera sensor frame rate.", {}},
|
||||||
|
{"set camera jxlq <dist>", "Set JPEG-XL distance (lower = higher quality).", {}},
|
||||||
|
{"set camera jxle <effort>", "Set JPEG-XL encode effort.", {}},
|
||||||
|
{"set camera display <0|1>", "Toggle the local display window.", {}},
|
||||||
|
}},
|
||||||
|
{"Logging", "Control console/log verbosity.", {
|
||||||
|
{"debug", "Toggle debug-level logging on/off.", {}},
|
||||||
|
{"trace <serial|mqtt|camera|control|all|off> [on|off]",
|
||||||
|
"Toggle verbatim wire-trace categories.", {
|
||||||
|
"Example: trace serial on / trace off"}},
|
||||||
|
}},
|
||||||
|
{"Session", "Help and exit.", {
|
||||||
|
{"help [topic]",
|
||||||
|
"Show this reference; 'help <topic>' expands one section.", {
|
||||||
|
"Example: help positioning / help dump"}},
|
||||||
|
{"exit", "Shut down and quit.", {}},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> renderHelp(const std::string& topic) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
const std::string q = lower(topic);
|
||||||
|
|
||||||
|
if (q.empty()) {
|
||||||
|
out.emplace_back("Available commands (type 'help <topic>' for detail, e.g. 'help goto'):");
|
||||||
|
for (const auto& sec : helpCatalog()) {
|
||||||
|
out.emplace_back("");
|
||||||
|
out.emplace_back("== " + sec.title + " == " + sec.blurb);
|
||||||
|
for (const auto& e : sec.entries)
|
||||||
|
out.emplace_back(" " + e.syntax + " - " + e.summary);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topic mode: match a section by title, or an entry by command verb.
|
||||||
|
bool matched = false;
|
||||||
|
for (const auto& sec : helpCatalog()) {
|
||||||
|
const bool sec_match = lower(sec.title).find(q) != std::string::npos;
|
||||||
|
for (const auto& e : sec.entries) {
|
||||||
|
if (!sec_match && verbOf(e.syntax) != q) continue;
|
||||||
|
matched = true;
|
||||||
|
out.emplace_back(e.syntax);
|
||||||
|
out.emplace_back(" " + e.summary);
|
||||||
|
for (const auto& d : e.detail) out.emplace_back(" " + d);
|
||||||
|
out.emplace_back("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) out.emplace_back("No help topic matching '" + topic + "'. Try 'help'.");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
|
|
@ -25,6 +25,13 @@ struct SerialMotorController::Impl {
|
||||||
MotorTelemetry latest;
|
MotorTelemetry latest;
|
||||||
std::atomic<bool> connected{false};
|
std::atomic<bool> connected{false};
|
||||||
|
|
||||||
|
// DUMP capture: the firmware emits a "DUMP BEGIN" ... "DUMP END" block in
|
||||||
|
// response to a DUMP command. We accumulate it line-by-line and publish the
|
||||||
|
// completed block (latest_dump) for the UI / `dump` command.
|
||||||
|
bool dumping = false;
|
||||||
|
std::string dump_buf;
|
||||||
|
std::string latest_dump;
|
||||||
|
|
||||||
void doRead() {
|
void doRead() {
|
||||||
boost::asio::async_read_until(
|
boost::asio::async_read_until(
|
||||||
serial, buffer, '\n',
|
serial, buffer, '\n',
|
||||||
|
|
@ -60,6 +67,28 @@ struct SerialMotorController::Impl {
|
||||||
} else if (!line.empty()) {
|
} else if (!line.empty()) {
|
||||||
// OK acks and other async output (DG/DUMP/BOOT/...).
|
// OK acks and other async output (DG/DUMP/BOOT/...).
|
||||||
LOG_TRACE_CAT(LogCat::Serial) << "RX " << line;
|
LOG_TRACE_CAT(LogCat::Serial) << "RX " << line;
|
||||||
|
captureDump(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the multi-line DUMP block. On completion, store it and emit it
|
||||||
|
// once at INFO so it's visible regardless of the SERIAL trace toggle.
|
||||||
|
void captureDump(const std::string& line) {
|
||||||
|
if (line.rfind("DUMP BEGIN", 0) == 0) {
|
||||||
|
dumping = true;
|
||||||
|
dump_buf = line + "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dumping) return;
|
||||||
|
dump_buf += line + "\n";
|
||||||
|
if (line.rfind("DUMP END", 0) == 0) {
|
||||||
|
dumping = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
latest_dump = dump_buf;
|
||||||
|
}
|
||||||
|
LOG_INFO << "firmware dump:\n" << dump_buf;
|
||||||
|
dump_buf.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -118,6 +147,11 @@ MotorTelemetry SerialMotorController::telemetry() {
|
||||||
return impl_->latest;
|
return impl_->latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string SerialMotorController::lastDump() {
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
||||||
|
return impl_->latest_dump;
|
||||||
|
}
|
||||||
|
|
||||||
bool SerialMotorController::connected() const { return impl_->connected; }
|
bool SerialMotorController::connected() const { return impl_->connected; }
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#include "fgc/ui/TuiUi.h"
|
#include "fgc/ui/TuiUi.h"
|
||||||
|
|
||||||
|
#include "fgc/HelpText.h"
|
||||||
#include "fgc/Logger.h"
|
#include "fgc/Logger.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
@ -146,6 +148,48 @@ Element logPanel(const std::vector<LogLine>& lines) {
|
||||||
vbox(std::move(rows)) | focusPositionRelative(0, 1) | yframe);
|
vbox(std::move(rows)) | focusPositionRelative(0, 1) | yframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline help pane (toggled with '?'). Lists every command section; the
|
||||||
|
// `sel`-th section is expanded to show each entry's detail. The Diagnostics
|
||||||
|
// section additionally renders the last captured firmware DUMP block.
|
||||||
|
Element helpPanel(int sel, const DumpView& dump) {
|
||||||
|
const auto& cat = helpCatalog();
|
||||||
|
std::vector<Element> rows;
|
||||||
|
for (int i = 0; i < static_cast<int>(cat.size()); ++i) {
|
||||||
|
const HelpSection& sec = cat[i];
|
||||||
|
const bool open = (i == sel);
|
||||||
|
Element title = hbox({
|
||||||
|
text(open ? " v " : " > "),
|
||||||
|
text(sec.title) | bold,
|
||||||
|
text(" " + sec.blurb) | dim,
|
||||||
|
});
|
||||||
|
rows.push_back(open ? (title | inverted) : title);
|
||||||
|
if (!open) continue;
|
||||||
|
|
||||||
|
for (const auto& e : sec.entries) {
|
||||||
|
rows.push_back(hbox({text(" "), text(e.syntax) | color(Color::Cyan) | bold}));
|
||||||
|
rows.push_back(hbox({text(" "), text(e.summary) | dim}));
|
||||||
|
for (const auto& d : e.detail)
|
||||||
|
rows.push_back(hbox({text(" "), text(d) | dim}));
|
||||||
|
}
|
||||||
|
// Diagnostics: show the most recent firmware dump inline.
|
||||||
|
if (sec.title == "Diagnostics") {
|
||||||
|
rows.push_back(text(" --- last firmware dump ---") | bold);
|
||||||
|
if (dump.has) {
|
||||||
|
std::istringstream iss(dump.text);
|
||||||
|
std::string l;
|
||||||
|
while (std::getline(iss, l))
|
||||||
|
rows.push_back(hbox({text(" "), text(l) | color(Color::Green)}));
|
||||||
|
} else {
|
||||||
|
rows.push_back(hbox({text(" "),
|
||||||
|
text("(none captured yet - run 'dump')") | dim}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push_back(text(""));
|
||||||
|
}
|
||||||
|
return window(text(" HELP (?:close Up/Down:section) ") | bold | color(Color::Cyan),
|
||||||
|
vbox(std::move(rows)) | yframe);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TuiUi::TuiUi() = default;
|
TuiUi::TuiUi() = default;
|
||||||
|
|
@ -190,6 +234,8 @@ 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;
|
||||||
|
int help_sel = 0;
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
|
|
@ -219,11 +265,13 @@ 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"), filler(), keyHint("q", "Quit"),
|
keyHint("r", "Reset"), keyHint(":", "Cmd"), keyHint("?", "Help"),
|
||||||
|
filler(), keyHint("q", "Quit"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return vbox({header, separator(), top, middle, logPanel(s.log) | flex, bottom});
|
Element main = help_open ? helpPanel(help_sel, s.dump) : logPanel(s.log);
|
||||||
|
return vbox({header, separator(), top, middle, main | flex, bottom});
|
||||||
});
|
});
|
||||||
|
|
||||||
auto root = CatchEvent(renderer, [&](Event e) {
|
auto root = CatchEvent(renderer, [&](Event e) {
|
||||||
|
|
@ -241,8 +289,19 @@ 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());
|
||||||
|
if (help_open) { // help pane navigation
|
||||||
|
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::Escape) { help_open = false; 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 (help_open) { // vim-style section nav while help is open
|
||||||
|
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 == "q") { if (sink_) sink_("exit"); return true; }
|
if (c == "q") { if (sink_) sink_("exit"); return true; }
|
||||||
if (c == "s") { if (sink_) sink_("start"); return true; }
|
if (c == "s") { if (sink_) sink_("start"); return true; }
|
||||||
if (c == "x") { if (sink_) sink_("stop"); return true; }
|
if (c == "x") { if (sink_) sink_("stop"); return true; }
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ struct FakeMotor : IMotorController {
|
||||||
void stop() override {}
|
void stop() override {}
|
||||||
void sendCommand(const std::string& c) override { cmds.push_back(c); }
|
void sendCommand(const std::string& c) override { cmds.push_back(c); }
|
||||||
MotorTelemetry telemetry() override { return tel; }
|
MotorTelemetry telemetry() override { return tel; }
|
||||||
|
std::string lastDump() override { return ""; }
|
||||||
bool connected() const override { return true; }
|
bool connected() const override { return true; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue