#include "fgc/HelpText.h" #include #include namespace fgc { namespace { std::string lower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(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& helpCatalog() { // clang-format off static const std::vector catalog = { {"Positioning", "Aim the gimbal. Yaw is a TRUE BEARING (0 = north); pitch is elevation (0 = level).", { {"gimbal move ,", "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 ,", "Move both axes to absolute encoder COUNTS (no degree conversion).", { "Example: gimbal steps 100000,250000"}}, {"gimbal nudge <+/-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 <+/-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 : : [...]", "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 ", "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 ", "Set the max slew speed (counts/s) for an axis.", {}}, {"gimbal reset [y|p] / gimbal enable|disable / gimbal setpos ", "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 []", "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 []", "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 [ []] []", "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 \"\"", "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 ", "Set capture rate in images/second.", {}}, {"set camera profile [] ", "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.] 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 ", "Set JPEG-XL distance (lower = higher quality).", {}}, {"set camera jxle ", "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 [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 ' expands one section.", { "Example: help positioning / help imu"}}, {"exit", "Shut down and quit.", {}}, }}, }; // clang-format on return catalog; } std::vector renderHelp(const std::string& topic) { std::vector out; const std::string q = lower(topic); if (q.empty()) { out.emplace_back("Available commands (type 'help ' 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