#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 = { {"Modes", "What the tower is doing. MQTT StatusCode: 0 auto, 1 pan, 2 manual.", { {"mode", "Report the current mode.", {}}, {"mode auto", "Sweep the scan grid, one image per waypoint (key a).", { "The unattended state; config [Startup] mode = auto boots into it.", "In auto and pan only status, logging, 'mode', 'cancel' and 'exit'", "are accepted."}}, {"mode pan []", "Sweep a span centred on a bearing, repeatedly (key p).", { "One image every pan_step_deg (hardware.ini), both edges included.", "Pitch defaults to 0 (level); a yaw-only tower ignores it. Key p pans", "30 deg around the current aim.", "Example: mode pan 90 30"}}, {"mode manual", "Stop scanning; aim by hand (key m). Cancels a running procedure.", {}}, }}, {"Capture", "Images and cameras. Manual mode, homed gimbal.", { {"snapshot", "Take one image now (key Space).", { "Waits for standstill and the exposure to settle, as a scan waypoint", "does, so the file and CamEvent are the same as a scan frame's."}}, {"capture [on|off]", "Continuous capture where the gimbal is aimed (key r).", { "One image per [General] image_interval since the last, each once the", "gimbal stands still and the exposure has settled. The gimbal can still", "be nudged; images are kept only while it stands still. Bare 'capture'", "reports on/off. Ends on any mode change, homing, heading setup or test."}}, {"set camera profile [] ", "Switch a camera's VimbaX settings profile.", { " is an XML stem in that camera's profile_dir. The camera may be", "omitted when only one is configured.", "Example: set camera profile night"}}, {"camera reset", "Reboot the cameras and restore their profiles.", { "For a stream that has stopped and will not restart. Takes seconds."}}, }}, {"Positioning", "Yaw is a true bearing (0 = north); pitch is elevation (0 = level).", { {"gimbal move [,]", "Point at a bearing/elevation in degrees.", { "Bearings in the unreachable yaw arc are refused, not clamped.", "Without a pitch, only yaw moves. A yaw-only tower ignores the pitch", "and refuses the pitch-only commands below.", "Example: gimbal move 30,-10 / gimbal move 30"}}, {"gimbal steps [,]", "Move to absolute encoder counts.", { "Example: gimbal steps 100000,250000"}}, {"gimbal nudge <+/-pct>", "Relative move, percent of travel (arrow keys).", { "Example: gimbal nudge yaw -5"}}, {"gimbal jog <+/-counts>", "Relative move in encoder counts.", { "Steps below ~13 counts are finer than the encoder resolves.", "Example: gimbal jog yaw -25"}}, {"gimbal home [y|p]", "Home both axes, or one.", {}}, {"gimbal stop [y|p]", "Stop motion now (bare: every axis).", {}}, {"gimbal heading setup|mark |fit|cancel", "Tie yaw to true north by sighting landmarks.", { "setup, then aim at a landmark and 'mark '; repeat for", "at least two, far apart; then 'fit'. 'fit : ...' fits", "known pairs without a setup.", "Example: gimbal heading mark 137.4"}}, {"gimbal heading offset ", "Shift the whole yaw map by a fixed amount.", { "Example: gimbal heading offset -2.5"}}, {"gimbal pitch setup", "Calibrate pitch against gravity (needs the IMU).", { "Homes if needed, sweeps pitch, fits, parks level and offers to save.", "Takes minutes; Esc or 'cancel' stops it."}}, }}, {"Motor", "Firmware-level motor controls.", { {"gimbal speed ", "Max slew speed, counts/s.", {}}, {"gimbal current ", "Motor current as raw TMC fields.", { "Normally set from hardware.ini [Motor.] in mA."}}, {"gimbal reset|enable|disable ", "Reset, enable or disable an axis driver.", {}}, {"gimbal setpos ", "Redefine an axis's current position.", {}}, {"gimbal status", "Emit one telemetry line now.", {}}, {"gimbal dump", "Fetch the firmware state; shown in the gimbal view (g).", {}}, {"gimbal raw \"\"", "Send text to the firmware verbatim.", { "Example: gimbal raw \"MOVE 100000,250000\""}}, }}, {"IMU", "Xsens MTi orientation sensor.", { {"imu status", "Latest orientation reading (also in the sensors view, s).", {}}, {"imu config", "Re-read and print the device configuration.", {}}, {"imu profile []", "Select the XKF filter profile (bare: list them).", { "Persists on the device. Example: imu profile General"}}, {"imu zero", "Make the current pose yaw 0. Run 'imu norotation' first.", {}}, {"imu norotation []", "Gyro-bias update; hold still (default 3 s).", {}}, }}, {"Tests", "Self-tests. Reports land in logs/test_*.log.", { {"test [ []] []", "Run the self-test suite, or part of it.", { "Compared with the 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 matches hardware.ini.", {}}, {"test camera [frames|dark|bright]", "Check the camera delivers and responds.", { "Never judges the picture itself. Capture pauses meanwhile."}}, }}, {"Keys", "Dashboard shortcuts. Each sends the command shown.", { {"a / p / m", "mode auto / mode pan (30 deg around the aim) / mode manual.", {}}, {"h", "gimbal home.", {}}, {"Space / r", "snapshot / capture on|off.", {}}, {"Arrows", "gimbal nudge: yaw 5%, pitch 10% if the tower has it (manual).", {}}, {"Esc", "Close the view, or cancel a running procedure.", {}}, {"? / g / s / c / n", "Views: help, gimbal, sensors, cameras, connectivity.", {}}, {"PgUp / PgDn", "Scroll the log or the open view.", {}}, {"f", "Freeze the screen, to select text or read back.", {}}, {"q", "Quit, or leave the dashboard running in the background.", {}}, }}, {"Session", "Logging and exit.", { {"help [topic]", "This list; 'help ' adds detail.", { "Example: help capture / help positioning"}}, {"cancel", "Cancel the running test, homing or calibration.", {}}, {"debug", "Toggle debug-level logging.", {}}, {"trace [on|off]", "Toggle wire-trace categories.", { "Example: trace serial on"}}, {"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()) { // Summaries line up in one column; a syntax longer than the cap keeps its // own width rather than pushing every other line right. size_t width = 0; for (const auto& sec : helpCatalog()) for (const auto& e : sec.entries) width = std::max(width, e.syntax.size()); width = std::min(width, 40); out.emplace_back("Commands ('help ' for detail, e.g. 'help capture'):"); for (const auto& sec : helpCatalog()) { out.emplace_back(""); out.emplace_back(sec.title + " - " + sec.blurb); for (const auto& e : sec.entries) { std::string l = " " + e.syntax; l.append(e.syntax.size() < width ? width - e.syntax.size() : 0, ' '); out.emplace_back(l + " " + 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