fwt_software/src/core/HelpText.cpp

177 lines
9.7 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. 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 <heading> <span> [<pitch>]", "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 profile's settle time, 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 scan cycle: [General] image_interval, then standstill and",
"the profile's settle time. 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 [<camera>] <name>", "Switch a camera's VimbaX settings profile.", {
"<name> 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 <yaw>[,<pitch>]", "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 <yaw>[,<pitch>]", "Move to absolute encoder counts.", {
"Example: gimbal steps 100000,250000"}},
{"gimbal nudge <yaw|pitch> <+/-pct>", "Relative move, percent of travel (arrow keys).", {
"Example: gimbal nudge yaw -5"}},
{"gimbal jog <yaw|pitch> <+/-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 <deg>|fit|cancel", "Tie yaw to true north by sighting landmarks.", {
"setup, then aim at a landmark and 'mark <its true bearing>'; repeat for",
"at least two, far apart; then 'fit'. 'fit <counts>:<deg> ...' fits",
"known pairs without a setup.",
"Example: gimbal heading mark 137.4"}},
{"gimbal heading offset <deg>", "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 <y|p> <vel>", "Max slew speed, counts/s.", {}},
{"gimbal current <y|p> <scaler> <irun> <irun_home>", "Motor current as raw TMC fields.", {
"Normally set from hardware.ini [Motor.<axis>] in mA."}},
{"gimbal reset|enable|disable <y|p>", "Reset, enable or disable an axis driver.", {}},
{"gimbal setpos <y|p> <counts>", "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 \"<command>\"", "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 [<type|label>]", "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 [<seconds>]", "Gyro-bias update; hold still (default 3 s).", {}},
}},
{"Tests", "Self-tests. Reports land in logs/test_*.log.", {
{"test [<subsystem> [<leaf>]] [<profile>]", "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 <topic>' adds detail.", {
"Example: help capture / help positioning"}},
{"cancel", "Cancel the running test, homing or calibration.", {}},
{"debug", "Toggle debug-level logging.", {}},
{"trace <serial|mqtt|camera|control|all|off> [on|off]", "Toggle wire-trace categories.", {
"Example: trace serial on"}},
{"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()) {
// 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<size_t>(width, 40);
out.emplace_back("Commands ('help <topic>' 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