diff --git a/docs/deployment.md b/docs/deployment.md index 5c10ab7..e5e473d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -221,37 +221,53 @@ point at a different lockfile. ## 7. Install the systemd service -An **alternative** to section 6b, not a companion to it: systemd gives you start-at-boot and -restart-on-crash, but no attachable dashboard — you get `journalctl` instead. Pick one. If you run both by -accident the second one to start fails on the single-instance lock (section 6c), so the failure is loud -rather than a pair of processes corrupting each other's state. +`scripts/install-tower` does this (it asks first, and shows the unit it will +write). The unit is the template `scripts/fire-gimbal-control.service`, and it +runs the program **through `scripts/fgc`, inside tmux** - so a tower started at +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 -sudo cp scripts/fire-gimbal-control.service /etc/systemd/system/ -sudoedit /etc/systemd/system/fire-gimbal-control.service +sudo cp fire-gimbal-control.service /etc/systemd/system/ +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/` 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 -User=ggs -WorkingDirectory=/opt/fire_gimbal_control -ExecStart=/opt/fire_gimbal_control/fire_gimbal_control --start -Environment=FGC_CONFIG=/opt/fire_gimbal_control/config.ini -EnvironmentFile=/etc/fire_gimbal_control/credentials.env -``` +**`--init` homes the gimbal unattended at every boot.** That is what a tower in +service needs - the firmware keeps no position across a power cycle, so without +it the gimbal sits un-homed and captures nothing - but it is a deliberate choice, +and the installer asks for the arguments. -(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 ```bash -sudo systemctl daemon-reload -sudo systemctl enable --now fire-gimbal-control -systemctl status fire-gimbal-control -journalctl -u fire-gimbal-control -f # live logs +sudo systemctl start fire-gimbal-control +scripts/fgc status # running, lockfile under /run/user/ +scripts/install-tower --verify # every check, including test hardware ``` +`[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: - Images accumulating under the configured `[Paths] output_dir` (default `~ggs/.local/share/fire_gimbal_control/images/RGB/`). diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 7c3f30b..61534eb 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -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 | | 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 | -| 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 | diff --git a/scripts/fire-gimbal-control.service b/scripts/fire-gimbal-control.service index 65d0fb1..09721b5 100644 --- a/scripts/fire-gimbal-control.service +++ b/scripts/fire-gimbal-control.service @@ -1,10 +1,27 @@ -# systemd unit template for Fire Gimbal Control. -# Copy to /etc/systemd/system/, adjust the paths/user, then: -# sudo systemctl daemon-reload -# sudo systemctl enable --now fire-gimbal-control +# systemd unit for Fire Gimbal Control - a TEMPLATE. scripts/install-tower fills +# in the @...@ fields and installs it; to do it by hand, replace them and copy it +# to /etc/systemd/system/, then: +# sudo loginctl enable-linger # 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 -# EnvironmentFile with 0600 perms over inline Environment= lines. +# The unit runs the program through scripts/fgc, inside tmux, so a tower started +# 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/ 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] Description=Fire Gimbal Control @@ -12,17 +29,14 @@ After=network-online.target Wants=network-online.target [Service] -Type=simple -User=ggs -WorkingDirectory=/opt/fire_gimbal_control -# --init is REQUIRED, not a convenience: the firmware keeps no state across a -# power cycle, so both axes boot into RESET and refuse to move until homed. Without -# it the service comes up and sits there, capturing nothing, with no obvious cause. -ExecStart=/opt/fire_gimbal_control/fire_gimbal_control --init --start -Environment=FGC_CONFIG=/opt/fire_gimbal_control/config.ini -# EnvironmentFile=/etc/fire_gimbal_control/credentials.env # FGC_MQTT_USER / FGC_MQTT_PW -Restart=on-failure -RestartSec=5 +Type=oneshot +RemainAfterExit=yes +User=@USER@ +WorkingDirectory=@REPO@ +ExecStart=@REPO@/scripts/fgc start @ARGS@ +ExecStop=@REPO@/scripts/fgc stop +TimeoutStartSec=120 +TimeoutStopSec=60 [Install] WantedBy=multi-user.target diff --git a/scripts/install-tower b/scripts/install-tower index 211d75c..f0ee551 100755 --- a/scripts/install-tower +++ b/scripts/install-tower @@ -621,34 +621,32 @@ fi plan test run "test hardware (moves nothing)" plan homing ask "asked at the end: home the gimbal and take a short capture" -UNIT_TEXT="[Unit] -Description=Fire Gimbal Control -After=network-online.target -Wants=network-online.target - -[Service] -# fgc runs the program in a tmux session, so 'scripts/fgc attach' still opens -# the dashboard. oneshot + RemainAfterExit: fgc start returns once it is up. -Type=oneshot -RemainAfterExit=yes -User=$USER -WorkingDirectory=$REPO -# --init homes at every start: the firmware keeps no position across a power cycle. -ExecStart=$REPO/scripts/fgc start --init --start -ExecStop=$REPO/scripts/fgc stop -TimeoutStartSec=120 -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" +# The unit is scripts/fire-gimbal-control.service with its @...@ fields filled. +render_unit() { + sed -e "s|@USER@|$USER|g" -e "s|@REPO@|$REPO|g" -e "s|@ARGS@|$1|g" \ + "$REPO/scripts/fire-gimbal-control.service" +} +# What an installed unit starts the program with, so a re-run keeps it. +installed_args() { + [[ -r $UNIT ]] && sed -n 's|^ExecStart=.*/scripts/fgc start *||p' "$UNIT" | head -1 +} +UNIT_TEXT="" +SVC_ARGS_DEFAULT="--init --start" +[[ -r $UNIT ]] && SVC_ARGS_DEFAULT="$(installed_args)" +[[ -z $SVC_ARGS_DEFAULT ]] && SVC_ARGS_DEFAULT=none +if [[ -r $UNIT && $S_SVC_ENABLED == enabled && $S_LINGER == yes && -z ${ANS[service_args]+x} ]] && + [[ "$(cat "$UNIT")" == "$(render_unit "$(installed_args)")" ]]; then + plan service skip "installed and enabled (fgc start ${SVC_ARGS_DEFAULT/#none/})" else echo - 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 "The boot service starts the program at every boot. With --init it HOMES THE" + 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 - 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 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 stop_app 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 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