Start the program at boot through fgc, so the dashboard still works

This commit is contained in:
pgdalmeida 2026-09-23 22:39:18 +02:00
parent 12b071c668
commit 53fa96fb54
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
4 changed files with 94 additions and 63 deletions

View File

@ -221,37 +221,53 @@ point at a different lockfile.
## 7. Install the systemd service ## 7. Install the systemd service
An **alternative** to section 6b, not a companion to it: systemd gives you start-at-boot and `scripts/install-tower` does this (it asks first, and shows the unit it will
restart-on-crash, but no attachable dashboard — you get `journalctl` instead. Pick one. If you run both by write). The unit is the template `scripts/fire-gimbal-control.service`, and it
accident the second one to start fails on the single-instance lock (section 6c), so the failure is loud runs the program **through `scripts/fgc`, inside tmux** - so a tower started at
rather than a pair of processes corrupting each other's state. boot still has its dashboard (`scripts/fgc attach`), and `fgc status` / `peek` /
`stop` work from any ssh session. It is not an alternative to the tmux workflow
any more; it is how that workflow starts at boot.
By hand, fill in `@USER@`, `@REPO@` and `@ARGS@` (normally `--init --start`) and:
```bash ```bash
sudo cp scripts/fire-gimbal-control.service /etc/systemd/system/ sudo cp fire-gimbal-control.service /etc/systemd/system/
sudoedit /etc/systemd/system/fire-gimbal-control.service sudo loginctl enable-linger ggs # NOT optional - see below
sudo systemctl daemon-reload
sudo systemctl enable fire-gimbal-control
``` ```
Confirm/adjust these lines: **Lingering is required.** Without it `/run/user/<uid>` does not exist until
someone logs in, so a program started at boot takes its single-instance lock in
`/tmp`; a later ssh session looks in `/run/user`, finds nothing, reports "not
running", and a second copy could start beside the first.
```ini **`--init` homes the gimbal unattended at every boot.** That is what a tower in
User=ggs service needs - the firmware keeps no position across a power cycle, so without
WorkingDirectory=/opt/fire_gimbal_control it the gimbal sits un-homed and captures nothing - but it is a deliberate choice,
ExecStart=/opt/fire_gimbal_control/fire_gimbal_control --start and the installer asks for the arguments.
Environment=FGC_CONFIG=/opt/fire_gimbal_control/config.ini
EnvironmentFile=/etc/fire_gimbal_control/credentials.env
```
(Use `ExecStart=... --init --start` for the first power-on if endstops must be found at boot.) MQTT credentials go in `/etc/fire_gimbal_control/credentials.env`
(`FGC_MQTT_USER=` / `FGC_MQTT_PW=`, mode 0640 root:ggs). `fgc` reads it inside
the tmux pane rather than taking it from the unit's environment: a tmux server
that already exists hands a new session its own environment, not the caller's.
## 8. Enable and verify ## 8. Enable and verify
```bash ```bash
sudo systemctl daemon-reload sudo systemctl start fire-gimbal-control
sudo systemctl enable --now fire-gimbal-control scripts/fgc status # running, lockfile under /run/user/<uid>
systemctl status fire-gimbal-control scripts/install-tower --verify # every check, including test hardware
journalctl -u fire-gimbal-control -f # live logs
``` ```
`[verified]` on tower 121, 2026-09-23: the service came up by itself after a
reboot with nobody logged in, with its lock under `/run/user/1000`; `fgc status`
and the dashboard worked from a later ssh session; credentials reached the
program intact (a password with `$`, a quote and a space); `systemctl stop` shut
it down cleanly; and `install-tower --verify` passed after the reboot, USB
preconditions included. 121 runs it with no arguments, so it does not home at
boot - see the note on `--init` above.
Confirm end to end: Confirm end to end:
- Images accumulating under the configured `[Paths] output_dir` (default - Images accumulating under the configured `[Paths] output_dir` (default
`~ggs/.local/share/fire_gimbal_control/images/RGB/`). `~ggs/.local/share/fire_gimbal_control/images/RGB/`).

View File

@ -954,5 +954,6 @@ checks that the two files load together, and changes nothing.
| Endstop survives homing | inspect the stop after a home; the axis must stall against it, not push through | ✅ at 189 mA, after failing at full torque | | Endstop survives homing | inspect the stop after a home; the axis must stall against it, not push through | ✅ at 189 mA, after failing at full torque |
| Degree scale derived | startup logs `counts_per_deg` agreeing with the homed travel | ✅ 3552.87, saved with `;prev` history | | Degree scale derived | startup logs `counts_per_deg` agreeing with the homed travel | ✅ 3552.87, saved with `;prev` history |
| Images land | `.jxl` files appear under the configured output dir | ❌ needs a capture run | | Images land | `.jxl` files appear under the configured output dir | ❌ needs a capture run |
| Survives a disconnect | `scripts/fgc start`, close ssh, reconnect, `scripts/fgc status` | ❌ not attempted | | Survives a disconnect | `scripts/fgc start`, close ssh, reconnect, `scripts/fgc status` | ✅ runs under the boot service, independent of any ssh session |
| Starts at boot | reboot with nobody logged in; `scripts/fgc status` from a later ssh | ✅ 2026-09-23, lock under `/run/user/1000` (lingering on) |
| Reports to a broker | topics arrive at the configured broker | ❌ no broker on this tower yet | | Reports to a broker | topics arrive at the configured broker | ❌ no broker on this tower yet |

View File

@ -1,10 +1,27 @@
# systemd unit template for Fire Gimbal Control. # systemd unit for Fire Gimbal Control - a TEMPLATE. scripts/install-tower fills
# Copy to /etc/systemd/system/, adjust the paths/user, then: # in the @...@ fields and installs it; to do it by hand, replace them and copy it
# sudo systemctl daemon-reload # to /etc/systemd/system/, then:
# sudo systemctl enable --now fire-gimbal-control # sudo loginctl enable-linger <user> # see below - not optional
# sudo systemctl daemon-reload && sudo systemctl enable fire-gimbal-control
# #
# Credentials are passed via the environment (not the config file). Prefer an # The unit runs the program through scripts/fgc, inside tmux, so a tower started
# EnvironmentFile with 0600 perms over inline Environment= lines. # at boot still has the dashboard: `scripts/fgc attach` from any ssh session.
# fgc returns once the program is up, hence oneshot + RemainAfterExit; stopping
# the unit runs `fgc stop`, which shuts the camera and motors down cleanly.
#
# Lingering: without it /run/user/<uid> does not exist until someone logs in, so
# a program started at boot takes its single-instance lock in /tmp, and a later
# ssh session looks in /run/user, finds nothing, and `fgc status` says it is not
# running - while a second copy could start beside it.
#
# @ARGS@ is normally `--init --start`: the firmware keeps no position across a
# power cycle, so without --init the gimbal sits un-homed and captures nothing.
# --init also means the gimbal HOMES UNATTENDED at every boot.
#
# MQTT credentials: /etc/fire_gimbal_control/credentials.env (FGC_MQTT_USER,
# FGC_MQTT_PW), read by fgc inside the tmux pane rather than passed through this
# unit - a tmux server that already exists would not hand a new session the
# unit's environment.
[Unit] [Unit]
Description=Fire Gimbal Control Description=Fire Gimbal Control
@ -12,17 +29,14 @@ After=network-online.target
Wants=network-online.target Wants=network-online.target
[Service] [Service]
Type=simple Type=oneshot
User=ggs RemainAfterExit=yes
WorkingDirectory=/opt/fire_gimbal_control User=@USER@
# --init is REQUIRED, not a convenience: the firmware keeps no state across a WorkingDirectory=@REPO@
# power cycle, so both axes boot into RESET and refuse to move until homed. Without ExecStart=@REPO@/scripts/fgc start @ARGS@
# it the service comes up and sits there, capturing nothing, with no obvious cause. ExecStop=@REPO@/scripts/fgc stop
ExecStart=/opt/fire_gimbal_control/fire_gimbal_control --init --start TimeoutStartSec=120
Environment=FGC_CONFIG=/opt/fire_gimbal_control/config.ini TimeoutStopSec=60
# EnvironmentFile=/etc/fire_gimbal_control/credentials.env # FGC_MQTT_USER / FGC_MQTT_PW
Restart=on-failure
RestartSec=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -621,34 +621,32 @@ fi
plan test run "test hardware (moves nothing)" plan test run "test hardware (moves nothing)"
plan homing ask "asked at the end: home the gimbal and take a short capture" plan homing ask "asked at the end: home the gimbal and take a short capture"
UNIT_TEXT="[Unit] # The unit is scripts/fire-gimbal-control.service with its @...@ fields filled.
Description=Fire Gimbal Control render_unit() {
After=network-online.target sed -e "s|@USER@|$USER|g" -e "s|@REPO@|$REPO|g" -e "s|@ARGS@|$1|g" \
Wants=network-online.target "$REPO/scripts/fire-gimbal-control.service"
}
[Service] # What an installed unit starts the program with, so a re-run keeps it.
# fgc runs the program in a tmux session, so 'scripts/fgc attach' still opens installed_args() {
# the dashboard. oneshot + RemainAfterExit: fgc start returns once it is up. [[ -r $UNIT ]] && sed -n 's|^ExecStart=.*/scripts/fgc start *||p' "$UNIT" | head -1
Type=oneshot }
RemainAfterExit=yes UNIT_TEXT=""
User=$USER SVC_ARGS_DEFAULT="--init --start"
WorkingDirectory=$REPO [[ -r $UNIT ]] && SVC_ARGS_DEFAULT="$(installed_args)"
# --init homes at every start: the firmware keeps no position across a power cycle. [[ -z $SVC_ARGS_DEFAULT ]] && SVC_ARGS_DEFAULT=none
ExecStart=$REPO/scripts/fgc start --init --start if [[ -r $UNIT && $S_SVC_ENABLED == enabled && $S_LINGER == yes && -z ${ANS[service_args]+x} ]] &&
ExecStop=$REPO/scripts/fgc stop [[ "$(cat "$UNIT")" == "$(render_unit "$(installed_args)")" ]]; then
TimeoutStartSec=120 plan service skip "installed and enabled (fgc start ${SVC_ARGS_DEFAULT/#none/})"
TimeoutStopSec=60
[Install]
WantedBy=multi-user.target"
if [[ -r $UNIT ]] && [[ "$(cat "$UNIT")" == "$UNIT_TEXT" ]] && [[ $S_SVC_ENABLED == enabled && $S_LINGER == yes ]]; then
plan service skip "installed and enabled"
else else
echo echo
echo "The boot service starts the program at every boot WITH --init: it homes the" echo "The boot service starts the program at every boot. With --init it HOMES THE"
echo "gimbal unattended each time the tower powers up." echo "GIMBAL UNATTENDED each time the tower powers up - which is what a tower in"
echo "service needs, since the firmware keeps no position across a power cycle."
if ask_yn service "Install and enable it?" y; then if ask_yn service "Install and enable it?" y; then
plan service change "write $UNIT, enable it, enable lingering for $USER" ask service_args "Arguments it starts the program with ('none' for none)" "$SVC_ARGS_DEFAULT"
svc_args="$REPLY_VAL"; [[ $svc_args == none ]] && svc_args=""
UNIT_TEXT="$(render_unit "$svc_args")"
plan service change "write $UNIT (fgc start $svc_args), enable it, enable lingering for $USER"
else plan service skip "declined"; fi else plan service skip "declined"; fi
fi fi
@ -820,7 +818,9 @@ if [[ $TEST_RC != 0 ]]; then
elif ask_yn home "Someone is at the rig with a hand on the power - home now and take a short capture?" n; then elif ask_yn home "Someone is at the rig with a hand on the power - home now and take a short capture?" n; then
stop_app stop_app
before="$(latest_log)" before="$(latest_log)"
if [[ $SVC_ON -eq 1 ]]; then sudo systemctl start fire-gimbal-control # Through the service when the service is what homes it, so the tower ends
# up running under it; otherwise the same run by hand.
if [[ $SVC_ON -eq 1 && " $(installed_args) " == *" --init "* ]]; then sudo systemctl start fire-gimbal-control
else ( cd "$REPO" && "$FGC" start --init --start ) > /dev/null; fi else ( cd "$REPO" && "$FGC" start --init --start ) > /dev/null; fi
say "homing (this can take a few minutes)" say "homing (this can take a few minutes)"
log=""; for _ in $(seq 1 30); do log="$(latest_log)"; [[ -n $log && $log != "$before" ]] && break; sleep 1; done log=""; for _ in $(seq 1 30); do log="$(latest_log)"; [[ -n $log && $log != "$before" ]] && break; sleep 1; done