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
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/<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
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/<uid>
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/`).

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 |
| 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 |

View File

@ -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 <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
# 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/<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]
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

View File

@ -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