247 lines
14 KiB
C++
247 lines
14 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 current <y|p> <scaler> <irun> <irun_home>",
|
|
"Set motor current, as raw TMC register fields.",
|
|
{"Normally sent automatically from hardware.ini [Motor.<axis>], which states",
|
|
"the current in milliamps and lets the host convert it using this board's",
|
|
"sense resistor. Setting it by hand does not stay secret: the next home",
|
|
"checks the firmware's reported current against the configured one."}},
|
|
{"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"}},
|
|
{"test hardware [axes|motor|imu|env|camera]",
|
|
"Check the rig against what hardware.ini declares.", {
|
|
"Preconditions, not measurements: the declared axes appear in telemetry and",
|
|
"no others; the firmware echoes the drivetrain current hardware.ini asked for;",
|
|
"a declared IMU streams; a declared ambient sensor sits on a REAL I2C bus (not",
|
|
"SMBus-only) and its CRC checks out; the cameras opened and the USB",
|
|
"preconditions hold. Nothing declared reports 'not checked', never PASS."}},
|
|
{"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
|