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/CaptureScheduler.cpp
|
||||
src/core/CommandParser.cpp
|
||||
src/core/HelpText.cpp
|
||||
src/ui/UiSnapshot.cpp
|
||||
src/ui/HeadlessUi.cpp
|
||||
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).
|
||||
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.
|
||||
virtual bool connected() const = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public:
|
|||
void stop() override;
|
||||
void sendCommand(const std::string& cmd) override;
|
||||
MotorTelemetry telemetry() override;
|
||||
std::string lastDump() override;
|
||||
bool connected() const override;
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ public:
|
|||
} else if (verb == "STOP") {
|
||||
yaw_target_ = yaw_.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.
|
||||
}
|
||||
|
|
@ -61,6 +75,11 @@ public:
|
|||
return t;
|
||||
}
|
||||
|
||||
std::string lastDump() override {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return dump_;
|
||||
}
|
||||
|
||||
bool connected() const override { return true; }
|
||||
|
||||
private:
|
||||
|
|
@ -91,6 +110,7 @@ private:
|
|||
long yaw_target_ = 0;
|
||||
long pitch_target_ = 0;
|
||||
bool homed_ = false;
|
||||
std::string dump_;
|
||||
};
|
||||
|
||||
} // namespace fgc
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ struct LogLine {
|
|||
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 {
|
||||
HeaderView header;
|
||||
GimbalView gimbal;
|
||||
|
|
@ -99,6 +105,7 @@ struct UiSnapshot {
|
|||
CaptureView capture;
|
||||
ConnView conn;
|
||||
std::vector<LogLine> log;
|
||||
DumpView dump;
|
||||
};
|
||||
|
||||
// ---- Pure formatting helpers (unit-tested in tests/test_uisnapshot.cpp) ----
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "fgc/CaptureScheduler.h"
|
||||
#include "fgc/CommandParser.h"
|
||||
#include "fgc/HelpText.h"
|
||||
#include "fgc/ICameraSource.h"
|
||||
#include "fgc/IControlChannel.h"
|
||||
#include "fgc/IMotorController.h"
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
|
|
@ -195,6 +197,10 @@ struct Application::Impl {
|
|||
s.conn.control_code = scheduler ? scheduler->controlCode() : 0;
|
||||
s.conn.target_heading = scheduler ? scheduler->targetHeading() : "0";
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -277,12 +283,39 @@ struct Application::Impl {
|
|||
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) {
|
||||
Command c = parseCommand(line);
|
||||
if (c.empty()) return;
|
||||
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;
|
||||
} else if (c.verb == "start") {
|
||||
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;
|
||||
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() {
|
||||
boost::asio::async_read_until(
|
||||
serial, buffer, '\n',
|
||||
|
|
@ -60,6 +67,28 @@ struct SerialMotorController::Impl {
|
|||
} else if (!line.empty()) {
|
||||
// OK acks and other async output (DG/DUMP/BOOT/...).
|
||||
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;
|
||||
}
|
||||
|
||||
std::string SerialMotorController::lastDump() {
|
||||
std::lock_guard<std::mutex> lock(impl_->mutex);
|
||||
return impl_->latest_dump;
|
||||
}
|
||||
|
||||
bool SerialMotorController::connected() const { return impl_->connected; }
|
||||
|
||||
} // namespace fgc
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#include "fgc/ui/TuiUi.h"
|
||||
|
||||
#include "fgc/HelpText.h"
|
||||
#include "fgc/Logger.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
|
|
@ -146,6 +148,48 @@ Element logPanel(const std::vector<LogLine>& lines) {
|
|||
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
|
||||
|
||||
TuiUi::TuiUi() = default;
|
||||
|
|
@ -190,6 +234,8 @@ void TuiUi::refreshLoop() {
|
|||
void TuiUi::uiLoop() {
|
||||
std::string cmd_buffer;
|
||||
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");
|
||||
|
||||
|
|
@ -219,11 +265,13 @@ void TuiUi::uiLoop() {
|
|||
} else {
|
||||
bottom = hbox({
|
||||
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) {
|
||||
|
|
@ -241,8 +289,19 @@ void TuiUi::uiLoop() {
|
|||
}
|
||||
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;
|
||||
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 == "s") { if (sink_) sink_("start"); return true; }
|
||||
if (c == "x") { if (sink_) sink_("stop"); return true; }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ struct FakeMotor : IMotorController {
|
|||
void stop() override {}
|
||||
void sendCommand(const std::string& c) override { cmds.push_back(c); }
|
||||
MotorTelemetry telemetry() override { return tel; }
|
||||
std::string lastDump() override { return ""; }
|
||||
bool connected() const override { return true; }
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue