fwt_software/src/core/HelpText.cpp

234 lines
13 KiB
C++

#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. Yaw is a TRUE BEARING (0 = north); pitch is elevation (0 = level).", {
{"gimbal move <yaw>,<pitch>",
"Point at an absolute bearing/elevation in DEGREES.", {
"Converts to encoder counts with the [Motor] calibration and moves both",
"axes together. Yaw travel is under a full turn, so bearings in the",
"unreachable arc are refused rather than clamped to the nearest limit.",
"Manual mode only - refused while a scan is running.",
"Example: gimbal move 30,-10 (bearing 30 deg, 10 deg below level)"}},
{"gimbal steps <yaw>,<pitch>",
"Move both axes to absolute encoder COUNTS (no degree conversion).", {
"Example: gimbal steps 100000,250000"}},
{"gimbal nudge <yaw|pitch> <+/-pct>",
"Relative move by a percent of the homed axis travel.", {
"The arrow keys do this when no overlay is open; inside an overlay they",
"scroll instead. Manual mode only.",
"Example: gimbal nudge yaw -5"}},
{"gimbal jog <yaw|pitch> <+/-counts>",
"Relative move in raw encoder counts.", {
"Finer than 'nudge' (a percent of travel) and relative unlike 'steps'.",
"Below ~13 counts the encoder cannot resolve the difference, so a smaller",
"step moves the motor to a position it cannot reproduce.",
"Example: gimbal jog yaw -25"}},
{"gimbal heading setup",
"Tie encoder counts to true north by sighting known landmarks (TUI).", {
"Opens a guided pane (dashboard key 'H'): aim with the arrow keys, Tab",
"cycles coarse/fine/very-fine steps, Space records an image, Enter enters",
"that landmark's true bearing. Two landmarks minimum - the further apart",
"the better. Needs no IMU, which is the point: IMU yaw drifts and not",
"every tower has one."}},
{"gimbal heading fit <counts>:<deg> <counts>:<deg> [...]",
"Fit the yaw map directly from known (encoder position, bearing) pairs.", {
"What the 'H' wizard emits, usable by hand on a headless tower. Two pairs",
"fit exactly; more are least-squares fitted and report R^2. Pairs closer",
"than 10 deg apart are refused - the fit would look perfect and be wrong.",
"Example: gimbal heading fit 480000:45.0 520000:115.0"}},
{"gimbal heading offset <deg>",
"Shift the whole yaw map by a known amount and save it.", {
"For when the tower is found to be off by a fixed bearing and re-sighting",
"landmarks is not worth it.",
"Example: gimbal heading offset -2.5"}},
{"gimbal home [y|p]",
"Run the endstop-finding home sequence (both axes, or one).", {
"Example: gimbal home / gimbal home y"}},
{"gimbal stop [y|p|all]",
"Stop motion immediately.", {
"To cancel a running procedure use the bare 'stop' (or 'cancel', or Esc):",
"the whole 'gimbal' family is ignored while one is in progress."}},
{"gimbal speed <y|p> <vel>",
"Set the max slew speed (counts/s) for an axis.", {}},
{"gimbal reset [y|p] / gimbal enable|disable <y|p> / gimbal setpos <y|p> <v>",
"Other firmware motor controls.", {}},
}},
{"IMU", "Read and configure the Xsens MTi orientation sensor.", {
{"imu status",
"Print the latest orientation reading.", {
"Yaw/pitch/roll, device temperature and sample counter. The Sensors panel",
"and the 'i' view show the same data continuously in the TUI."}},
{"imu config",
"Re-read the device configuration and print it.", {
"Identity, firmware, sample rate, active XKF profile and the available",
"list. Re-reads first, so it never reports a stale profile."}},
{"imu profile [<type|label>]",
"Select the XKF filter profile (persists on the device).", {
"The profile decides whether heading uses the magnetometer. Nothing here",
"affects the gimbal's own heading: that comes from the encoder, tied to",
"north by 'gimbal heading setup'. Typical MTi profiles are General, High_mag_dep,",
"Dynamic and VRU_general; run with no argument for this device's real list.",
"Example: imu profile General / imu profile 39"}},
{"imu zero",
"Redefine the current pose as yaw 0 (heading reset).", {
"Run 'imu norotation' first so the gyro bias is corrected. Mid-scan this",
"re-references every later CamEvent heading."}},
{"imu norotation [<seconds>]",
"Run the gyro-bias update; hold the unit still (default 3 s).", {
"Cuts heading drift, which matters most on a no-mag profile. The command",
"returns immediately - keeping the unit still is up to you.",
"Example: imu norotation 5"}},
}},
{"Diagnostics", "Inspect, self-test and calibrate the gimbal.", {
{"gimbal dump",
"Request a full firmware state dump and show it here.", {
"Build, uptime, reset cause and the per-axis TMC5160 registers; also shown",
"in the gimbal 'g' view. Offline equivalent: ./firmware/dump.sh"}},
{"test [<subsystem> [<leaf>]] [<profile>]",
"Run the hardware self-test suite; a report lands in logs/test_*.log.", {
"'test' runs everything; a subsystem or leaf narrows it, and a trailing",
"token names a config profile. Each run is compared to a pinned baseline",
"and the previous run. Esc cancels.",
"Example: test gimbal homing / test list / test baseline"}},
{"gimbal pitch setup",
"Calibrate PITCH steps<->degrees against gravity, using the IMU.", {
"Auto-homes if needed, sweeps pitch dwelling at each step, fits, then",
"parks level. Writes logs/pitch_setup_*.log and offers to save. Takes",
"minutes; 'stop' or Esc cancels. Requires the IMU - level is defined by",
"gravity and nothing else on the tower can measure it. Yaw is NOT touched:",
"use 'gimbal heading setup'."}},
{"gimbal status",
"Ask the firmware to emit one telemetry (ST) line now.", {}},
{"gimbal raw \"<command>\"",
"Send the quoted text to the firmware verbatim.", {
"Bypasses all unit conversion and wrappers - use with care; sends exactly",
"what you type, plus a newline.",
"Example: gimbal raw \"MOVE 100000,250000\" / gimbal raw \"DUMP\""}},
}},
{"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 profile [<camera>] <name>",
"Switch a camera's profile (rate, exposure, all imaging settings).", {
"A profile is a settings XML exported from the VimbaX Viewer, in that",
"camera's [Camera.<name>] profile_dir; the name is the filename stem. The",
"camera may be omitted when only one is configured.",
"Example: set camera profile night / set camera profile rgb night"}},
{"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"}},
}},
{"Display", "Terminal dashboard keys (TUI only).", {
{"? / g / i / c / m",
"Toggle the Help, Gimbal, Sensors, Camera and Connectivity overlays.", {
"An overlay takes the full terminal height. Esc closes it; with none open,",
"Esc cancels a running procedure. 'm' shows broker state, per-topic traffic",
"and the recent MQTT messages, including any dropped while offline."}},
{"Space",
"Take one image now and publish its CamEvent (manual mode only).", {
"Goes through the same settle-and-trigger path a scan waypoint uses, so",
"the file and the CamEvent are indistinguishable from a scan frame.",
"Refused while a scan is running."}},
{"H",
"Open the heading-setup wizard (see 'gimbal heading setup').", {
"Manual mode only. Blocks a scan from starting until it is finished or",
"cancelled with Esc."}},
{"Up/Down, j/k",
"Scroll the open overlay one line (Help: change section).", {
"The scrollbar on the right shows how much is off-screen."}},
{"PageUp/PageDown",
"Scroll the open overlay a screenful; on the dashboard, the log.", {
"Paging the log freezes it: new lines keep arriving but the view holds",
"still, and the LOG title shows 'scrollback'."}},
{"Home/End",
"Jump to the top/bottom; on the log, End resumes following the tail.", {}},
{"q",
"Ask whether to quit or leave the dashboard running in the background.", {
"Quitting stops capture and closes the camera; backgrounding detaches the",
"tmux client (same as Ctrl-b d) and leaves everything running. Ctrl-C still",
"shuts down immediately."}},
{"p",
"Freeze the 10 Hz repaint so terminal text selection survives.", {
"Any keypress repaints, so scrolling while paused drops the selection -",
"pause is for copying, not for reading scrollback."}},
}},
{"Session", "Help and exit.", {
{"refresh",
"Re-read live device state (TUI: press 'r').", {
"Re-queries the IMU configuration and requests a firmware dump, updating",
"the 'i' and 'g' views. Briefly pauses the IMU stream."}},
{"help [topic]",
"Show this reference; 'help <topic>' expands one section.", {
"Example: help positioning / help imu"}},
{"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 imu'):");
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