Cancel homing and shut down cleanly when the program is stopped during homing

This commit is contained in:
pgdalmeida 2026-10-06 19:41:19 +02:00
parent 9f02b70dae
commit eb91f470dd
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
3 changed files with 50 additions and 10 deletions

View File

@ -722,7 +722,8 @@ It is modular (`test gimbal`, `test imu drift`, `test host`, …) and driven by
**profiles** configured under `[Test]` / `[TestProfile.<name>]` in `config.ini` **profiles** configured under `[Test]` / `[TestProfile.<name>]` in `config.ini`
(repetition counts, sampling windows, pass thresholds; default `standard`). While a (repetition counts, sampling windows, pass thresholds; default `standard`). While a
test (or calibration, or homing) runs, interfering commands are ignored and **Esc** test (or calibration, or homing) runs, interfering commands are ignored and **Esc**
cancels. Full reference: [test-command.md](test-command.md). Replaces the former cancels. A stop is not ignored: `exit`, Ctrl-C or SIGTERM (`scripts/fgc stop`) during homing
cancels the homing, as Esc does, and shuts the program down. Full reference: [test-command.md](test-command.md). Replaces the former
`gimbal diag` (now folded into `test gimbal encoder`). `gimbal diag` (now folded into `test gimbal encoder`).
## Terminal dashboard (TUI) ## Terminal dashboard (TUI)

View File

@ -146,8 +146,11 @@ it is shown dimmed.
`scripts/fgc stop` sends SIGTERM and waits up to 120 s; the program handles it `scripts/fgc stop` sends SIGTERM and waits up to 120 s; the program handles it
with the same shutdown path as the `exit` command (stop capture, close the with the same shutdown path as the `exit` command (stop capture, close the
camera, flush the image queue). Do **not** `kill -9` - an unclean exit leaves the camera, flush the image queue). A stop while the gimbal is homing cancels the
camera wedged for the next run. If it will not stop, attach and look at why. homing rather than waiting for it - a full home takes minutes on some towers,
longer than this wait - so the gimbal is left un-homed, and the next start homes
it again. Do **not** `kill -9` - an unclean exit leaves the camera wedged for the
next run. If it will not stop, attach and look at why.
`scripts/fgc start` refuses if a run is already up, and prints the previous `scripts/fgc start` refuses if a run is already up, and prints the previous
run's last output if it died rather than silently discarding it. run's last output if it died rather than silently discarding it.

View File

@ -84,8 +84,18 @@ constexpr long kEncoderQuantumCounts = 13;
// (StopContinuousImageAcquisition + Close). Without this, a Ctrl-C / kill / systemd // (StopContinuousImageAcquisition + Close). Without this, a Ctrl-C / kill / systemd
// stop skips teardown and leaves the camera acquiring - which wedges it so the NEXT // stop skips teardown and leaves the camera acquiring - which wedges it so the NEXT
// run gets no frames. Only an atomic flag is set here (async-signal-safe). // run gets no frames. Only an atomic flag is set here (async-signal-safe).
//
// Installed as the very first thing run() does, so a stop that arrives while the
// camera opens or the gimbal homes is not the default action - which kills the
// process on the spot and wedges the camera. A second signal of the same kind
// goes back to that default: Ctrl-C twice still gets out of a start that hangs.
std::atomic<bool> g_shutdown_requested{false}; std::atomic<bool> g_shutdown_requested{false};
void handleShutdownSignal(int) { g_shutdown_requested.store(true); } void handleShutdownSignal(int sig) {
if (g_shutdown_requested.exchange(true)) {
std::signal(sig, SIG_DFL);
std::raise(sig);
}
}
long long nowEpochMs() { long long nowEpochMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>( return std::chrono::duration_cast<std::chrono::milliseconds>(
@ -1195,6 +1205,12 @@ struct Application::Impl {
} }
} }
// A stop that came in while the program was starting up: do not begin a
// home that would only be cancelled.
if (shutdownRequested()) {
LOG_INFO << "stop requested: not homing";
return;
}
motor->sendCommand("HOME"); // homes all axes; firmware runs it non-blocking motor->sendCommand("HOME"); // homes all axes; firmware runs it non-blocking
auto allReady = [](const MotorTelemetry& t) { auto allReady = [](const MotorTelemetry& t) {
@ -2494,11 +2510,27 @@ struct Application::Impl {
Command c = parseCommand(line); Command c = parseCommand(line);
if (c.verb == "cancel" || (c.verb == "mode" && c.device == "manual")) if (c.verb == "cancel" || (c.verb == "mode" && c.device == "manual"))
cancel_procedure_ = true; cancel_procedure_ = true;
else if (c.verb == "exit")
running = false; // not an interfering command: a stop
else if (!c.empty()) else if (!c.empty())
LOG_WARN << "ignored '" << c.verb << "': homing in progress (Esc to cancel)"; LOG_WARN << "ignored '" << c.verb << "': homing in progress (Esc to cancel)";
local.pop(); local.pop();
} }
// A stop cancels homing, as Esc does, rather than waiting for it to finish:
// a full home takes minutes on some towers - longer than `fgc stop` (120 s)
// and systemd (150 s) wait before giving up or killing the program outright,
// which wedges the camera. Dropping the stop instead, as this once did, left
// the program running with its dashboard already closed.
if (shutdownRequested() && !cancel_procedure_) {
LOG_INFO << "stop requested: cancelling homing to shut down";
cancel_procedure_ = true;
} }
}
// A stop asked for in any of the ways there are: the `exit` command - which is
// also what the dashboard sends when it closes, on Ctrl-C and SIGTERM too - or
// SIGINT/SIGTERM caught by handleShutdownSignal() when there is no dashboard.
bool shutdownRequested() const { return !running || g_shutdown_requested.load(); }
// ---- `test` command ---- // ---- `test` command ----
static std::string lowerStr(std::string s) { static std::string lowerStr(std::string s) {
@ -2977,6 +3009,14 @@ struct Application::Impl {
int run() { int run() {
start_time = std::chrono::steady_clock::now(); start_time = std::chrono::steady_clock::now();
// Clean up on Ctrl-C / kill / systemd stop, not just on "exit"/Ctrl-D - so the
// camera is always stopped and closed (an unclean exit wedges it for next time).
// First, before anything slow: homing alone takes minutes on some towers.
// The TUI's own handler takes over while the dashboard runs (it turns the
// signal into "exit") and puts this one back when the dashboard closes.
std::signal(SIGINT, handleShutdownSignal);
std::signal(SIGTERM, handleShutdownSignal);
// Log level: config first, CLI overrides. // Log level: config first, CLI overrides.
if (!cfg.logging.level.empty() && !Logger::setLevelFromString(cfg.logging.level)) if (!cfg.logging.level.empty() && !Logger::setLevelFromString(cfg.logging.level))
LOG_WARN << "unknown Logging.level '" << cfg.logging.level << "', keeping default"; LOG_WARN << "unknown Logging.level '" << cfg.logging.level << "', keeping default";
@ -3162,12 +3202,8 @@ struct Application::Impl {
sendAnchorConfig(); sendAnchorConfig();
applyHomedTravel(); applyHomedTravel();
} }
if (start_in != Mode::Manual) setMode(start_in); // A stop that came in during homing: do not start sweeping on the way out.
if (start_in != Mode::Manual && !shutdownRequested()) setMode(start_in);
// Clean up on Ctrl-C / kill / systemd stop, not just on "exit"/Ctrl-D - so the
// camera is always stopped and closed (an unclean exit wedges it for next time).
std::signal(SIGINT, handleShutdownSignal);
std::signal(SIGTERM, handleShutdownSignal);
LOG_INFO << "Entering control loop (type 'exit' to quit, or Ctrl-C)"; LOG_INFO << "Entering control loop (type 'exit' to quit, or Ctrl-C)";
oneshot_since_ = std::chrono::steady_clock::now(); oneshot_since_ = std::chrono::steady_clock::now();