178 lines
9.7 KiB
C++
178 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 [General] image_interval since the last, each once the",
|
|
"gimbal has stood still for 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
|