278 lines
16 KiB
C++
278 lines
16 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 = {
|
|
{"Modes",
|
|
"What the tower is doing: auto, pan or manual. StatusCode on MQTT says which (0/1/2).", {
|
|
{"mode",
|
|
"Say which mode the tower is in (and, in pan, where it is looking).", {}},
|
|
{"mode auto",
|
|
"Sweep the scan grid, a picture at every waypoint. Key: a.", {
|
|
"The normal, unattended state; the boot service starts here (--start).",
|
|
"In auto and pan only reading commands, logging, 'mode', 'cancel' and",
|
|
"'exit' are accepted: nothing that moves the gimbal or changes a setting."}},
|
|
{"mode pan <heading> <span> [<pitch>]",
|
|
"Watch one direction: sweep a span centred on a bearing, repeatedly.", {
|
|
"Left to right across heading +/- span/2, one picture every pan_step_deg",
|
|
"(hardware.ini) or less, both edges included; then back to the left edge.",
|
|
"A span under one picture is the one heading. Pitch defaults to 0 (level).",
|
|
"Example: mode pan 90 30 (75, 81, ..., 105: six pictures). Key p pans",
|
|
"30 deg around where the gimbal points now."}},
|
|
{"mode manual",
|
|
"Stop capturing and take the gimbal by hand. Key: m.", {
|
|
"Every command is accepted. With a test, homing or calibration running,",
|
|
"cancels it instead (as Esc and 'cancel' do)."}},
|
|
}},
|
|
{"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.", {
|
|
"Aim at a landmark, then 'gimbal heading mark <its true bearing>'; repeat,",
|
|
"then 'gimbal heading fit'. Two landmarks minimum, the further apart the",
|
|
"better. On the dashboard it runs in the activity strip: arrows aim, Tab",
|
|
"step size, Enter mark, Space image, f fit, Esc cancel. Needs no IMU."}},
|
|
{"gimbal heading mark <deg>",
|
|
"Record where the gimbal points now as the landmark's true bearing.", {
|
|
"Only during 'gimbal heading setup'. Example: gimbal heading mark 137.4"}},
|
|
{"gimbal heading fit [<counts>:<deg> ...]",
|
|
"Fit the yaw map from the marks, or from (encoder position, bearing) pairs.", {
|
|
"Bare, it fits this heading setup's marks. With pairs, it needs no setup -",
|
|
"for a headless tower whose pairs are known. Two 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 cancel",
|
|
"Stop heading setup and drop its marks (so do 'cancel' and Esc).", {}},
|
|
{"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; bare, every axis. The axis holds where it stops.", {
|
|
"To cancel a running procedure use 'cancel' (or Esc, or m):",
|
|
"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 's' 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."}},
|
|
{"test camera [frames|dark|bright]",
|
|
"Check the camera works, whatever it is pointed at.", {
|
|
"frames: whole frames arrive in time. dark: minimum exposure and gain take",
|
|
"every pixel to black. bright: maximum gain and a long exposure make the",
|
|
"frame measurably different - the sensor is alive and took the settings.",
|
|
"The picture itself is never judged, so fog, night or a lens cap cannot",
|
|
"decide it. The profile is put back after; capture pauses meanwhile."}},
|
|
{"camera reset",
|
|
"Reboot the camera and put it back on its profile.", {
|
|
"Sends the camera its own DeviceReset, waits for it to come back on USB, and",
|
|
"re-applies the profile (manual mode). For a stream that has",
|
|
"stopped delivering frames and will not restart. The program also does this",
|
|
"by itself when restarting the stream stops helping."}},
|
|
{"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; 'cancel' 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.", {
|
|
{"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).", {
|
|
{"a / p / m",
|
|
"Auto mode / pan around where the gimbal points now / manual mode.", {
|
|
"p is 'mode pan <current heading> 30 <current pitch>'. m also cancels a",
|
|
"running test, homing or calibration, as Esc does."}},
|
|
{"? / g / s / c / n",
|
|
"Toggle the Help, Gimbal, Sensors, Cameras and Connectivity views.", {
|
|
"A view takes the full terminal height; press its key again, or Esc, to",
|
|
"close it. With none open, Esc cancels a running procedure. 'n' shows",
|
|
"broker state, per-topic traffic and the recent MQTT messages."}},
|
|
{"Arrows",
|
|
"Nudge the gimbal: left/right yaw 5%, up/down pitch 10% (manual mode).", {
|
|
"In every view. During heading setup they aim in the step Tab selects."}},
|
|
{"PageUp/PageDown",
|
|
"Scroll the log, or the open view. The only scroll keys.", {
|
|
"The log and the message history jump back to the newest line when one",
|
|
"arrives; pause (f) first to read further back. In Help, j/k change",
|
|
"section."}},
|
|
{"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."}},
|
|
{"f",
|
|
"Freeze the screen: to select and copy text, or to scroll back.", {
|
|
"While frozen nothing new is drawn, so the log stays where you page it.",
|
|
"The header says when it froze. f again resumes."}},
|
|
{"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."}},
|
|
}},
|
|
{"Session", "Help and exit.", {
|
|
{"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
|