Set in config.ini whether the tower homes and which mode it starts in, instead of the --init and --start flags
This commit is contained in:
parent
f2c61e030e
commit
4488a2181d
|
|
@ -7,8 +7,10 @@ REMOTE_DIR=/home/ggs/projects/fwt_2a/software # where the project is rsync'
|
|||
# backends are built in. Drop -DWITH_VIMBA=ON to build mock-camera only.
|
||||
CMAKE_ARGS="-DWITH_MQTT=ON -DWITH_VIMBA=ON"
|
||||
|
||||
# Args passed to the binary by `./deploy.sh --run`.
|
||||
RUN_ARGS="--start"
|
||||
# Extra args passed to the binary by `./deploy.sh --run` (e.g. --no-mqtt).
|
||||
# Whether it homes and which mode it starts in are NOT args: they are [Startup]
|
||||
# home / mode in the device's config.ini.
|
||||
RUN_ARGS=""
|
||||
|
||||
# Optional overrides:
|
||||
# BUILD_JOBS=4 # parallel build jobs (default: remote nproc)
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -154,7 +154,7 @@ On a tower the program runs **detached, inside `tmux`**, so it keeps running
|
|||
when you close your ssh session. `scripts/fgc` does that for you:
|
||||
|
||||
```bash
|
||||
scripts/fgc start --init --start # start it: home the gimbal, then sweep
|
||||
scripts/fgc start # start it; config.ini [Startup] says whether it homes and sweeps
|
||||
scripts/fgc attach # open the dashboard (Ctrl-b d to leave it running)
|
||||
scripts/fgc status # is it running? since when? which PID?
|
||||
scripts/fgc peek # a text snapshot of the dashboard, without attaching
|
||||
|
|
@ -174,10 +174,19 @@ sudo systemctl status fire-gimbal-control # is the service running?
|
|||
sudo systemctl restart fire-gimbal-control # restart it (it homes the gimbal again)
|
||||
```
|
||||
|
||||
`--init` homes the gimbal at every start, which a tower in service needs: the
|
||||
motor board forgets its position whenever it loses power. On tower 111 the
|
||||
service starts it with `--init --start`, so after a power cut it is back to
|
||||
capturing within about a minute, with nobody touching it.
|
||||
What the program does as it starts is set in `config.ini`, not by flags:
|
||||
|
||||
```ini
|
||||
[Startup]
|
||||
home = true ; home the gimbal at every start
|
||||
mode = auto ; manual (wait for someone) or auto (sweep at once)
|
||||
```
|
||||
|
||||
`home = true` is what a tower in service needs: the motor board forgets its
|
||||
position whenever it loses power. Tower 111 homes and starts in auto, so after
|
||||
a power cut it is back to capturing within about a minute, with nobody touching
|
||||
it. A new tower starts with `mode = manual` until it is proven. See
|
||||
[configuration.md](docs/configuration.md#startup--homing-and-the-start-mode).
|
||||
|
||||
**Only one copy can run at a time.** Two would fight over the motor board and
|
||||
the camera, so a second one refuses to start and says which process holds the
|
||||
|
|
@ -473,8 +482,6 @@ settings the way the real firmware does.
|
|||
|---|---|
|
||||
| `-c, --config <path>` | use this `config.ini` (otherwise `$FGC_CONFIG`, then `./config.ini`, ...) |
|
||||
| `--hardware <path>` | use this `hardware.ini` (otherwise `$FGC_HARDWARE`, then `/etc/fire_gimbal_control/hardware.ini`, then beside `config.ini`) |
|
||||
| `-i, --init` | home the gimbal at start |
|
||||
| `-s, --start` | start sweeping at once |
|
||||
| `--test <selection>` | run one self-test and exit: 0 PASS, 4 FAIL, 5 could not start |
|
||||
| `--tui` / `--no-tui` | the dashboard / the plain console (overrides `[UI] enable_tui`) |
|
||||
| `--mock-serial`, `--mock-camera`, `--mock-env` | stand-ins instead of the motor board, camera, ambient sensor |
|
||||
|
|
|
|||
|
|
@ -19,6 +19,21 @@ image_interval = 5
|
|||
; 1 = print motor telemetry each loop tick, 0 = quiet.
|
||||
debug = 0
|
||||
|
||||
[Startup]
|
||||
; What the tower does as it comes up - the boot service passes no flags, so this
|
||||
; is the one place it is decided.
|
||||
; home: run the endstop-finding home sequence at every start. The firmware keeps
|
||||
; no position across a power cycle, so without it the axes stay un-homed and
|
||||
; refuse every move. A tower in service needs true - and then it HOMES
|
||||
; UNATTENDED at every boot. Absent => false.
|
||||
home = true
|
||||
; mode: manual or auto. manual waits and captures nothing until someone chooses
|
||||
; a mode (key a, or `mode auto` over MQTT); auto starts sweeping the scan grid
|
||||
; at once. auto is refused, and the tower stays in manual, if the gimbal is not
|
||||
; homed by then. pan needs a heading and a span, so it is by command only.
|
||||
; Absent => manual. Choose auto once the tower is proven.
|
||||
mode = manual
|
||||
|
||||
[Network]
|
||||
; MQTT broker address (the ground-station / ZKMS server).
|
||||
zkms_server_ip = 127.0.0.1
|
||||
|
|
|
|||
43
deploy.sh
43
deploy.sh
|
|
@ -8,7 +8,6 @@
|
|||
# Usage:
|
||||
# ./deploy.sh rsync + remote configure + build
|
||||
# ./deploy.sh --run ... then start the app on the device, detached, and return
|
||||
# ./deploy.sh --run --home ... and run the endstop-finding home sequence at startup
|
||||
# ./deploy.sh --run --attach ... start it, then attach straight away (the usual one)
|
||||
# ./deploy.sh --attach attach to the app already running (no sync, no build)
|
||||
# ./deploy.sh --status is it running on the device?
|
||||
|
|
@ -32,6 +31,11 @@
|
|||
# survives this ssh connection closing — reattach later with --attach. Only one
|
||||
# instance can run at a time; the binary itself enforces that with a lockfile.
|
||||
#
|
||||
# Whether the app homes the gimbal and which mode it starts in are NOT flags:
|
||||
# they are [Startup] home / mode in the device's config.ini, read at every start
|
||||
# (--run and the boot service alike). To home once without changing that, attach
|
||||
# and type `gimbal home`.
|
||||
#
|
||||
# Flags combine, e.g. ./deploy.sh --clean --run
|
||||
#
|
||||
# Config comes from .deploy.env (see .deploy.env.example).
|
||||
|
|
@ -52,13 +56,12 @@ source "$ENV_FILE"
|
|||
: "${REMOTE_DIR:?set REMOTE_DIR in .deploy.env}"
|
||||
CMAKE_ARGS="${CMAKE_ARGS:--DWITH_MQTT=ON -DWITH_VIMBA=ON}"
|
||||
BUILD_JOBS="${BUILD_JOBS:-}"
|
||||
RUN_ARGS="${RUN_ARGS:---start}"
|
||||
RUN_ARGS="${RUN_ARGS:-}"
|
||||
|
||||
CLEAN=0
|
||||
RUN=0
|
||||
CHECK_DEPS=0
|
||||
FORCE=0
|
||||
HOME_INIT=0
|
||||
FOREGROUND=0
|
||||
PUSH_CONFIG=0
|
||||
SESSION_CMD=""
|
||||
|
|
@ -67,7 +70,10 @@ for arg in "$@"; do
|
|||
--clean) CLEAN=1 ;;
|
||||
--push-config|--with-config) PUSH_CONFIG=1 ;;
|
||||
--run|--start) RUN=1 ;;
|
||||
--home|--init) HOME_INIT=1 ;;
|
||||
--home|--init)
|
||||
echo "error: $arg was removed: set [Startup] home = true in the device's config.ini," >&2
|
||||
echo " or attach and type 'gimbal home' to home once" >&2
|
||||
exit 1 ;;
|
||||
--check-deps) CHECK_DEPS=1 ;;
|
||||
--force) FORCE=1 ;;
|
||||
--foreground) FOREGROUND=1 ;;
|
||||
|
|
@ -108,11 +114,17 @@ if [[ -n "$SESSION_CMD" ]] && [[ "$SESSION_CMD" != "attach" || "$RUN" != "1" ]];
|
|||
"cd '$REMOTE_DIR' && exec scripts/fgc $SESSION_CMD"
|
||||
fi
|
||||
|
||||
# Homing is opt-in: append --init only when --home is passed, so a plain run
|
||||
# brings up the TUI without driving the endstop-finding sequence.
|
||||
if [[ $HOME_INIT -eq 1 && "$RUN_ARGS" != *--init* ]]; then
|
||||
RUN_ARGS="--init $RUN_ARGS"
|
||||
fi
|
||||
# The program refuses --init/--start now; catch an old .deploy.env here, before
|
||||
# a sync and a build, rather than at the very end.
|
||||
for a in $RUN_ARGS; do
|
||||
case "$a" in
|
||||
--init|-i|--start|-s)
|
||||
echo "error: RUN_ARGS in $ENV_FILE has '$a', which the program no longer takes." >&2
|
||||
echo " Homing and the start mode are [Startup] home / mode in the device's" >&2
|
||||
echo " config.ini: drop it from RUN_ARGS." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Locate cmake on the remote. Non-interactive ssh shells don't source .bashrc,
|
||||
# so allow an explicit REMOTE_CMAKE override in .deploy.env; otherwise search
|
||||
|
|
@ -265,6 +277,19 @@ ssh "$REMOTE_HOST" "$RESOLVE_CMAKE $CHECK_DEPS_CMD cd '$REMOTE_DIR' && \
|
|||
\"\$CMAKE\" --build build $JOBS_FLAG"
|
||||
echo ">> build complete: $REMOTE_DIR/build/fire_gimbal_control"
|
||||
|
||||
# A boot service installed before [Startup] existed still starts the program
|
||||
# with --init/--start, which this build refuses: the tower would not come up at
|
||||
# its next boot. Only the installer can fix that (it moves them into config.ini
|
||||
# and rewrites the unit), so say so loudly rather than touch the unit from here.
|
||||
# shellcheck disable=SC2029
|
||||
if ssh "$REMOTE_HOST" "grep -qE '^ExecStart=.*fgc start.*[[:space:]](--init|--start|-i|-s)([[:space:]]|$)' \
|
||||
/etc/systemd/system/fire-gimbal-control.service 2>/dev/null"; then
|
||||
echo "!! WARNING: the device's boot service still passes --init/--start, which this build" >&2
|
||||
echo "!! refuses - the program will NOT start at the next boot. On the device run" >&2
|
||||
echo "!! scripts/install-tower" >&2
|
||||
echo "!! which moves them into config.ini [Startup] and rewrites the service." >&2
|
||||
fi
|
||||
|
||||
if [[ "$RUN" == "1" ]]; then
|
||||
# Guard: a freshly seeded config.ini is the example template, with placeholders
|
||||
# that connect nowhere and silently drop to degraded mode. Refuse to launch on
|
||||
|
|
|
|||
|
|
@ -92,11 +92,16 @@ only shows up on the rig.
|
|||
## Run
|
||||
|
||||
```bash
|
||||
scripts/run.sh --init --start # real deployment: home, then start
|
||||
scripts/run.sh --start # only if already homed this power cycle
|
||||
scripts/run.sh --mock-serial --mock-camera --no-mqtt --start # dev, no hardware
|
||||
scripts/run.sh # real deployment
|
||||
scripts/run.sh --mock-serial --mock-camera --no-mqtt # dev, no hardware
|
||||
```
|
||||
|
||||
Whether it homes the gimbal and which mode it starts in come from `config.ini`
|
||||
`[Startup] home` / `mode` ([configuration.md](configuration.md#startup--homing-and-the-start-mode)),
|
||||
not from flags. A real deployment has `home = true`: the firmware keeps no position
|
||||
across a power cycle. Skip homing (`home = false`) only if the gimbal is already
|
||||
homed this power cycle.
|
||||
|
||||
[scripts/run.sh](../scripts/run.sh) locates the binary relative to itself, and
|
||||
[scripts/fire-gimbal-control.service](../scripts/fire-gimbal-control.service) is a systemd unit template
|
||||
(uses `WorkingDirectory` + `FGC_CONFIG`, so no paths are hardcoded in the binary).
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
|
|||
| `General` | `tower_name` | string | `Unnamed` | Tower identity; used in all MQTT topics/payloads |
|
||||
| `General` | `image_interval` | int > 0 | `5` | Seconds between captures (→ `image_rate = 1/interval`) |
|
||||
| `General` | `debug` | bool | `false` | Start with debug-level logging |
|
||||
| `Startup` | `home` | bool | `false` | Home the gimbal at every start (see [Startup](#startup--homing-and-the-start-mode)). A tower in service needs `true`, and then homes **unattended** at every boot |
|
||||
| `Startup` | `mode` | `manual`/`auto` | `manual` | Mode the tower starts in. `auto` is refused, leaving it in manual, if the gimbal is not homed by then. `pan` is by command only |
|
||||
| `Network` | `zkms_server_ip` | string | `127.0.0.1` | MQTT broker host/IP, or a full `tcp://…` URI |
|
||||
| `Network` | `mqtt_port` | int 1..65535 | `1883` | Broker port (`8883` for TLS). Ignored when `zkms_server_ip` carries its own port or a scheme |
|
||||
| `Network` | `mqtt_user` / `mqtt_pw` | string | — | MQTT credentials (see secrets below) |
|
||||
|
|
@ -88,6 +90,38 @@ Parsed and validated by `ConfigLoader` ([src/core/Config.cpp](../src/core/Config
|
|||
| `Motor.<axis>` | `sw_mode` / `has_encoder` | int / bool | — | TMC `SW_MODE` (physical endstop switches on REFL/REFR) and whether this axis's encoder is aligned and usable. **hardware.ini** |
|
||||
| `Scan` | `grid_file` | string | — | CSV of `yaw_deg,pitch_deg` waypoints (`yaw_deg` alone means pitch 0). The only source of a scan grid; empty → auto-sweep disabled |
|
||||
|
||||
### `[Startup]` — homing and the start mode
|
||||
|
||||
What the tower does as it comes up. It is in `config.ini`, not in flags, so the
|
||||
boot service, `deploy.sh --run` and a run by hand all do the same thing, and the
|
||||
boot service starts the program with no arguments.
|
||||
|
||||
```ini
|
||||
[Startup]
|
||||
home = true ; home the gimbal at every start
|
||||
mode = manual ; manual or auto
|
||||
```
|
||||
|
||||
- **`home`** runs the endstop-finding home sequence (see [Init sequence](#init-sequence-startup-home--true))
|
||||
before anything else. The firmware keeps no position across a power cycle, so
|
||||
without it the axes stay in `RESET` and refuse every move; the one case for
|
||||
`false` is a development tower, where nothing should move by itself. To home
|
||||
once without changing the file, type `gimbal home` (key `h`).
|
||||
- **`mode`** is the mode entered once startup (and homing) is done. `auto` needs
|
||||
the gimbal homed; if it is not - homing off, or homing failed - the program
|
||||
logs `mode auto refused: gimbal not homed` and stays in `manual`. `pan` needs a
|
||||
heading and a span, so it is chosen by command only (`mode pan …`).
|
||||
- Both absent is the idle start: no homing, manual. The example config ships
|
||||
`home = true`, `mode = manual` - what a new tower gets from `install-tower`:
|
||||
it homes at every boot, then waits for someone to choose auto.
|
||||
- A mode changed at runtime (`mode auto` over MQTT, key `a`) is **not** written
|
||||
back: after a reboot the tower is in its `[Startup]` mode again.
|
||||
- A `--test` run ignores `[Startup]` entirely: it neither homes nor leaves manual.
|
||||
|
||||
These replaced the `--init` / `--start` flags, which the program now refuses with
|
||||
a message pointing here. `scripts/install-tower` moves a boot service's old
|
||||
flags into this section (`--init` → `home = true`, `--start` → `mode = auto`).
|
||||
|
||||
### `[Motor]` calibration & `[Scan]` grid
|
||||
|
||||
The firmware reports only **encoder counts**; `[Motor]` maps them to the bearing/elevation degrees
|
||||
|
|
@ -578,8 +612,6 @@ Parsed by Boost.Program_options ([main.cpp](../main.cpp)). Flags override `[Feat
|
|||
|------|--------|
|
||||
| `-h, --help` | Show help and exit |
|
||||
| `-c, --config <path>` | Explicit config file path |
|
||||
| `-i, --init` | Run the endstop-finding init sequence before the loop |
|
||||
| `-s, --start` | Start capture automatically |
|
||||
| `--no-mqtt` | Disable MQTT (use the null channel) |
|
||||
| `--mock-camera` | Use the simulated camera |
|
||||
| `--mock-serial` | Use the simulated motor controller |
|
||||
|
|
@ -592,9 +624,14 @@ Parsed by Boost.Program_options ([main.cpp](../main.cpp)). Flags override `[Feat
|
|||
| `--log-file-level <lvl>` | File level, independent of `--log-level` (default `debug`) |
|
||||
| `--trace <cats>` | Verbatim wire trace; comma list `serial,mqtt,camera,control,all,none` |
|
||||
|
||||
Typical headless dev run: `scripts/run.sh --mock-serial --mock-camera --no-mqtt --start`.
|
||||
Homing and the start mode are not flags: they are [`[Startup]`](#startup--homing-and-the-start-mode)
|
||||
`home` / `mode`. The old `-i, --init` and `-s, --start` are refused with a message saying so.
|
||||
`--test` ignores `[Startup]`.
|
||||
|
||||
### Init sequence (`--init`)
|
||||
Typical headless dev run: `scripts/run.sh --mock-serial --mock-camera --no-mqtt`, with
|
||||
`[Startup] mode = auto` in the dev `config.ini` to start sweeping at once.
|
||||
|
||||
### Init sequence (`[Startup] home = true`)
|
||||
|
||||
Sends `ENABLE Y`, `ENABLE P`, `HOME`, then polls telemetry until both axes report `READY`
|
||||
(firmware `ST` state `A`), bounded by the firmware's 60 s homing timeout, then sets `SPEED Y/P`
|
||||
|
|
|
|||
|
|
@ -35,9 +35,14 @@ On the dev box, in `software/`, with a `.deploy.env` naming the tower:
|
|||
REMOTE_HOST=ggs@10.11.12.121
|
||||
REMOTE_DIR=/home/ggs/projects/fwt_2a/software
|
||||
CMAKE_ARGS="-DWITH_MQTT=ON -DWITH_VIMBA=ON"
|
||||
RUN_ARGS="--start"
|
||||
RUN_ARGS=""
|
||||
```
|
||||
|
||||
`RUN_ARGS` holds only extra flags (`--no-mqtt`, say). Whether the program homes
|
||||
and which mode it starts in are the tower's own `config.ini` `[Startup] home` /
|
||||
`mode`, the same for `--run` as for the boot service; `deploy.sh` refuses a
|
||||
`RUN_ARGS` that still has `--init` or `--start`.
|
||||
|
||||
```bash
|
||||
./deploy.sh # sync the tree, build on the tower
|
||||
./deploy.sh --check-deps # only check the tower's build dependencies
|
||||
|
|
@ -52,9 +57,17 @@ program already running keeps running the old binary until it is restarted:
|
|||
# on a tower started by its boot service
|
||||
ssh ggs@<tower> sudo systemctl restart fire-gimbal-control
|
||||
# on a tower started by hand
|
||||
./deploy.sh --stop && ./deploy.sh --run # --run --home adds --init
|
||||
./deploy.sh --stop && ./deploy.sh --run # homes / sweeps as its [Startup] says
|
||||
```
|
||||
|
||||
To home once on a tower whose `[Startup] home` is off, attach and type
|
||||
`gimbal home` (key `h`); `deploy.sh --home` is gone.
|
||||
|
||||
After building, `deploy.sh` also reads the tower's boot service: one that still
|
||||
passes `--init`/`--start` gets a loud warning, because the new program refuses
|
||||
them and **the tower would not start at its next boot**. Run
|
||||
`scripts/install-tower` on the tower to fix it (section 4).
|
||||
|
||||
`--run` starts the program detached through `scripts/fgc` (section 2) and first
|
||||
refuses a `config.ini` that still holds the example's placeholders
|
||||
(`ExampleTower`, `CHANGE_ME`, `DEV_0000000000`); `--force` overrides that. On a
|
||||
|
|
@ -115,7 +128,7 @@ closing. `scripts/fgc` runs it inside a tmux session, which owns the pty:
|
|||
|
||||
```bash
|
||||
cd ~/projects/fwt_2a/software
|
||||
scripts/fgc start --start # starts detached, returns as soon as it is up
|
||||
scripts/fgc start # starts detached, returns as soon as it is up
|
||||
scripts/fgc status # running? since when? which PID?
|
||||
scripts/fgc attach # the dashboard - Ctrl-b d detaches, leaves it running
|
||||
scripts/fgc stop # SIGTERM -> the same clean teardown as `exit`
|
||||
|
|
@ -198,7 +211,7 @@ systemctl is-enabled fire-gimbal-control # starts at boot?
|
|||
sudo systemctl disable fire-gimbal-control # not any more
|
||||
```
|
||||
|
||||
By hand, fill in `@USER@`, `@REPO@` and `@ARGS@` (normally `--init --start`) and:
|
||||
By hand, fill in `@USER@`, `@REPO@` and `@ARGS@` (normally empty) and:
|
||||
|
||||
```bash
|
||||
sudo cp fire-gimbal-control.service /etc/systemd/system/
|
||||
|
|
@ -212,15 +225,25 @@ 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.
|
||||
|
||||
**`--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.
|
||||
The service passes no startup flags. What the program does at boot is
|
||||
`config.ini` `[Startup]` ([configuration.md](configuration.md#startup--homing-and-the-start-mode)):
|
||||
|
||||
**`home = true` 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 it (`startup_home`), as it does
|
||||
for the start mode (`startup_mode`, `manual` or `auto`).
|
||||
|
||||
**Upgrading a tower from before `[Startup]`.** Its unit still passes `--init` /
|
||||
`--start`, which the program now refuses. Run `scripts/install-tower` on the
|
||||
tower **before its next reboot**: it moves them into `config.ini` (`--init` →
|
||||
`home = true`, `--start` → `mode = auto`) and rewrites the unit without them.
|
||||
`install-tower --verify` fails its `boot service args` check until that is done.
|
||||
|
||||
| Tower | Service | Starts with |
|
||||
|---|---|---|
|
||||
| 111 | enabled 2026-09-24 | `--init --start`: homes and captures at boot - `[verified]` by a reboot 2026-09-24 |
|
||||
| 121 | enabled 2026-09-23 | no arguments: starts idle, neither homes nor captures (the development tower) |
|
||||
| 111 | enabled 2026-09-24 | `--init --start`: homes and captures at boot - `[verified]` by a reboot 2026-09-24. **To migrate**: becomes `[Startup] home = true`, `mode = auto` |
|
||||
| 121 | enabled 2026-09-23 | no arguments and no `[Startup]`: starts idle, neither homes nor captures (the development tower) |
|
||||
|
||||
`[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`
|
||||
|
|
@ -291,7 +314,7 @@ list below and says which.
|
|||
| Camera opened, but no images and `no frame yet (stream warming up)` for minutes | the stream is stuck; the program restarts it and then resets the camera by itself (known-issues #15), or type `camera reset` |
|
||||
| `No camera found` / Vimba startup error | SDK installed, GenTL path set (`/etc/profile.d/*Vimba*GenTL*.sh` or `VIMBA_CTI_PATH`), `[Camera.rgb] id` in `hardware.ini` |
|
||||
| `MQTT connect failed` (degraded mode) | broker reachable, `credentials.env` readable by `ggs`, firewall |
|
||||
| Runs but no images | in auto mode (`--start`, or `mode auto`) and the gimbal homed; `output_dir` writable |
|
||||
| Runs but no images | in auto mode (`[Startup] mode = auto`, or `mode auto`) and the gimbal homed; `output_dir` writable |
|
||||
| `another instance is already running` (exit 2) | `scripts/fgc status`; the service and a hand-started run both up (section 3) |
|
||||
| `fgc status` says not running, but the program is | the service started it before anyone logged in, without lingering (section 4) |
|
||||
| Program dies when ssh disconnects | started with `run.sh` / `deploy.sh --foreground` instead of `scripts/fgc start` |
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ dashboard's `a` / `p` / `m` keys - and reports it on `StatusCode`.
|
|||
|
||||
| Mode | StatusCode | What the tower does | Command |
|
||||
|---|---|---|---|
|
||||
| auto | `0` | Sweeps the scan grid front to back, a picture at every waypoint, then starts over. The normal state: the boot service starts here (`--start`). | `mode auto` |
|
||||
| auto | `0` | Sweeps the scan grid front to back, a picture at every waypoint, then starts over. The normal state: a tower in service starts here (`config.ini` `[Startup] mode = auto`). | `mode auto` |
|
||||
| pan | `1` | Watches one direction: walks left to right across a span centred on a bearing, a picture at each position, then back to the left edge. | `mode pan <heading> <span> [<pitch>]` |
|
||||
| manual | `2` | Nothing by itself; a person drives it. The state without `--start`. | `mode manual` |
|
||||
| manual | `2` | Nothing by itself; a person drives it. The start mode unless `[Startup] mode = auto`; also where `auto` falls back if the gimbal is not homed. | `mode manual` |
|
||||
|
||||
**Pan positions.** `mode pan 90 30` covers 75°..105°. One picture covers
|
||||
`pan_step_deg` of bearing (hardware.ini `[Hardware]`, 6 until the lens is
|
||||
|
|
|
|||
|
|
@ -102,26 +102,36 @@ one yes. In order:
|
|||
1. packages, then the USB preconditions (section 3), then the Vimba X SDK (4)
|
||||
2. builds the host
|
||||
3. writes `hardware.ini` (5) - or, when one exists, only fills its gaps
|
||||
4. seeds `config.ini` (9) and `credentials.env`, or edits only what is out of date
|
||||
4. seeds `config.ini` (9) and `credentials.env`, or edits only what is out of date.
|
||||
This is where it asks what the tower does as it starts - `[Startup] home` and
|
||||
`mode` - and where a tower whose boot service still passes the old `--init` /
|
||||
`--start` has them moved into `config.ini`
|
||||
5. PlatformIO, then the camera profiles that are missing (8)
|
||||
6. builds and flashes the firmware (6) - **blocked** while any drivetrain value
|
||||
is still a `; TODO`, because that axis would refuse to move
|
||||
7. `test hardware`
|
||||
8. the boot service ([deployment.md](deployment.md) section 4), if wanted. A new
|
||||
tower gets `--init`: it homes at every boot, then **waits in manual and
|
||||
captures nothing** until someone chooses auto
|
||||
8. the boot service ([deployment.md](deployment.md) section 4), if wanted. It
|
||||
passes no startup flags; a new tower gets `[Startup] home = true`,
|
||||
`mode = manual`: it homes at every boot, then **waits in manual and captures
|
||||
nothing** until someone chooses auto
|
||||
9. asks whether someone is at the rig; only on a yes does it home the gimbal and
|
||||
take **one** picture (a snapshot), then leave it in manual - through the
|
||||
service, when the service is what homes it
|
||||
service, when `[Startup] home` has the service home it, and otherwise with
|
||||
`gimbal home`, leaving `config.ini` as it is
|
||||
|
||||
**Nothing starts capturing by itself.** Choose auto yourself: key `a` on the
|
||||
dashboard, or `mode auto` over MQTT. Once the tower is proven, make it boot into
|
||||
auto, as 111 does:
|
||||
|
||||
```bash
|
||||
scripts/install-tower --answer service_args="--init --start"
|
||||
scripts/install-tower --answer startup_mode=auto
|
||||
```
|
||||
|
||||
That writes `[Startup] mode = auto` into `config.ini`; editing the file by hand
|
||||
does the same (with the program stopped - see the warning at the top of the
|
||||
file). `--answer startup_home=y|n` sets homing the same way. `service_args` now
|
||||
holds only other flags, and refuses `--init` / `--start`.
|
||||
|
||||
Every question has a key, so any can be answered up front - `--answer
|
||||
tower_name=north`, `--answer yaw.r_sense_ohm=0.075` - or from a file of
|
||||
`key=value` lines with `--answers FILE`. `--non-interactive` stops instead of
|
||||
|
|
@ -637,7 +647,7 @@ On the dev box, in `software/`:
|
|||
REMOTE_HOST=ggs@10.11.12.121
|
||||
REMOTE_DIR=/home/ggs/projects/fwt_2a/software
|
||||
CMAKE_ARGS="-DWITH_MQTT=ON -DWITH_VIMBA=ON"
|
||||
RUN_ARGS="--start"
|
||||
RUN_ARGS="" # extra flags only; homing and mode are config.ini [Startup]
|
||||
```
|
||||
|
||||
```bash
|
||||
|
|
@ -922,7 +932,8 @@ reading. `[verified]` on tower 111, 2026-09-23: both.
|
|||
Only once every phase above is green:
|
||||
|
||||
```bash
|
||||
scripts/fgc start --start # detached, inside tmux
|
||||
# config.ini: [Startup] home = true, mode = auto
|
||||
scripts/fgc start # detached, inside tmux
|
||||
scripts/fgc status # running? since when? which PID?
|
||||
```
|
||||
|
||||
|
|
@ -1010,7 +1021,7 @@ host must behave exactly as it did before — which is also the guarantee the wh
|
|||
Delta fleet depends on.
|
||||
|
||||
```bash
|
||||
scripts/fgc start --start
|
||||
scripts/fgc start
|
||||
scripts/fgc peek
|
||||
test hardware motor
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ namespace fgc {
|
|||
|
||||
// Command-line / runtime options that override config at launch.
|
||||
struct RuntimeOptions {
|
||||
bool init = false; // run the endstop-finding init sequence
|
||||
bool start = false; // begin capture automatically
|
||||
// Whether to home and which mode to start in are NOT here: they are config.ini
|
||||
// [Startup] home / mode (AppConfig::startup), so the boot service needs no flags.
|
||||
// Start anyway when hardware.ini declares something this rig does not have.
|
||||
// Field triage only: it overrides [Features] strict_presence, and the log
|
||||
// says so in those words so it cannot become the normal way to run a tower.
|
||||
|
|
@ -35,7 +35,8 @@ struct RuntimeOptions {
|
|||
// --test: run this one self-test selection (the words after `test` at the
|
||||
// console, e.g. "hardware" or "hardware motor") once the motor link is up,
|
||||
// then exit with its verdict. Empty => normal operation. For scripts - the
|
||||
// installer above all - which need an answer, not a dashboard.
|
||||
// installer above all - which need an answer, not a dashboard. A --test run
|
||||
// ignores [Startup]: it neither homes nor leaves manual.
|
||||
std::string run_test;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "fgc/ImuMount.h"
|
||||
#include "fgc/Geometry.h"
|
||||
#include "fgc/OperatingMode.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
|
@ -312,6 +313,16 @@ struct LoggingConfig {
|
|||
std::string file_dir; // empty => paths::defaultLogDir()
|
||||
};
|
||||
|
||||
// [Startup]: what the tower does as it comes up. Absent keys are the idle
|
||||
// start: no homing, manual. The firmware keeps no position across a power
|
||||
// cycle, so a tower in service needs home = true - and then it homes UNATTENDED
|
||||
// at every start. mode = auto is refused at startup (and the tower stays in
|
||||
// manual) if the gimbal is not homed by then.
|
||||
struct StartupConfig {
|
||||
bool home = false; // run the endstop-finding home sequence at startup
|
||||
Mode mode = Mode::Manual; // manual or auto; pan needs a spec, so it is by command only
|
||||
};
|
||||
|
||||
struct UiConfig {
|
||||
bool enable_tui = false; // false => headless line console (default); CLI --tui/--no-tui override
|
||||
};
|
||||
|
|
@ -366,6 +377,7 @@ struct AppConfig {
|
|||
FeaturesConfig features;
|
||||
LoggingConfig logging;
|
||||
UiConfig ui; // [UI] terminal dashboard toggle
|
||||
StartupConfig startup; // [Startup] home + mode at startup
|
||||
Geometry geometry; // [Motor] degrees<->counts maps (yaw + pitch)
|
||||
DrivetrainConfig drivetrain; // [Motor.<axis>] from hardware.ini
|
||||
// [Motor] how long a full two-end homing may take before the host calls it
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@
|
|||
// The tower's three modes of operation, and what each one accepts.
|
||||
//
|
||||
// auto - sweep the scan grid, a picture at every waypoint (the normal,
|
||||
// unattended state; the boot service starts here with --start);
|
||||
// unattended state; config.ini [Startup] mode = auto starts here);
|
||||
// pan - watch one direction: centred on a heading, walk left to right
|
||||
// across a span, a picture at each position, then back to the left
|
||||
// edge and again;
|
||||
// manual - the gimbal does nothing by itself; a person drives it, over the
|
||||
// MQTT Command topic or at the console.
|
||||
//
|
||||
// Modes change only by command (`mode auto|pan|manual`), and the tower reports
|
||||
// its mode on StatusCode, as the numeric value of Mode.
|
||||
// Modes change only by command (`mode auto|pan|manual`), apart from the one
|
||||
// config.ini [Startup] mode sets as the tower comes up. The tower reports its
|
||||
// mode on StatusCode, as the numeric value of Mode.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
|
|||
34
main.cpp
34
main.cpp
|
|
@ -48,8 +48,6 @@ int main(int argc, char* argv[]) {
|
|||
("hardware", po::value<std::string>(),
|
||||
"path to hardware.ini (default: $FGC_HARDWARE, /etc/fire_gimbal_control/hardware.ini, "
|
||||
"or one beside config.ini)")
|
||||
("init,i", po::bool_switch(), "home the gimbal on startup (required: nothing is homed across a power cycle)")
|
||||
("start,s", po::bool_switch(), "start in auto mode (sweep and capture); without it, manual")
|
||||
("no-mqtt", po::bool_switch(), "disable MQTT (use a null control channel)")
|
||||
("mock-camera", po::bool_switch(), "use a simulated camera (no hardware)")
|
||||
("mock-serial", po::bool_switch(), "use a simulated motor controller (no hardware)")
|
||||
|
|
@ -72,13 +70,24 @@ int main(int argc, char* argv[]) {
|
|||
("test", po::value<std::string>(),
|
||||
"run one self-test once the motor link is up, then exit: 0 PASS, 4 FAIL, 5 could "
|
||||
"not start. Takes what follows `test` at the console, e.g. --test hardware or "
|
||||
"--test \"hardware motor\". Headless; cannot be combined with --start")
|
||||
"--test \"hardware motor\". Headless; ignores [Startup], so it neither homes nor "
|
||||
"leaves manual")
|
||||
("trace", po::value<std::string>(),
|
||||
"verbatim wire trace, comma list: serial,mqtt,camera,control,all,none");
|
||||
|
||||
// The removed startup flags, accepted only so that using one says where the
|
||||
// setting went instead of boost's bare "unrecognised option". A boot service
|
||||
// still carrying them must not look like a typo.
|
||||
po::options_description removed;
|
||||
removed.add_options()
|
||||
("init,i", po::bool_switch(), "")
|
||||
("start,s", po::bool_switch(), "");
|
||||
po::options_description all;
|
||||
all.add(desc).add(removed);
|
||||
|
||||
po::variables_map vm;
|
||||
try {
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::store(po::parse_command_line(argc, argv, all), vm);
|
||||
po::notify(vm);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Argument error: " << e.what() << "\n";
|
||||
|
|
@ -88,17 +97,22 @@ int main(int argc, char* argv[]) {
|
|||
std::cout << desc << "\n";
|
||||
return 0;
|
||||
}
|
||||
if (vm["init"].as<bool>() || vm["start"].as<bool>()) {
|
||||
std::cerr << "Argument error: --init and --start were removed. Whether the tower homes "
|
||||
"and which mode it starts in are set in config.ini:\n"
|
||||
" [Startup]\n"
|
||||
" home = true ; was --init\n"
|
||||
" mode = auto ; was --start (manual otherwise)\n"
|
||||
"A boot service still passing them: re-run scripts/install-tower, which "
|
||||
"moves them into config.ini.\n";
|
||||
return 1;
|
||||
}
|
||||
if (vm.count("test")) {
|
||||
const std::string t = vm["test"].as<std::string>();
|
||||
if (t.find_first_not_of(" \t") == std::string::npos) {
|
||||
std::cerr << "Argument error: --test needs a selection, e.g. --test hardware\n";
|
||||
return 1;
|
||||
}
|
||||
if (vm["start"].as<bool>()) {
|
||||
// Capture would move the gimbal and hold the serial link the test needs.
|
||||
std::cerr << "Argument error: --test cannot be combined with --start\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string cli_cfg = vm.count("config") ? vm["config"].as<std::string>() : "";
|
||||
|
|
@ -202,8 +216,6 @@ int main(int argc, char* argv[]) {
|
|||
}
|
||||
|
||||
RuntimeOptions opts;
|
||||
opts.init = vm["init"].as<bool>();
|
||||
opts.start = vm["start"].as<bool>();
|
||||
if (vm["no-mqtt"].as<bool>()) opts.use_mqtt = false;
|
||||
if (vm["mock-camera"].as<bool>()) opts.mock_camera = true;
|
||||
if (vm["mock-serial"].as<bool>()) opts.mock_serial = true;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@
|
|||
# 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.
|
||||
# @ARGS@ is normally empty. Whether the program homes and the mode it starts in
|
||||
# are config.ini [Startup] home / mode, not arguments (the program refuses the
|
||||
# old --init / --start). The firmware keeps no position across a power cycle, so
|
||||
# a tower in service has home = true - and 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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
#
|
||||
# Every question has a key, so any of them can be answered up front with
|
||||
# --answer KEY=VALUE (repeatable) - e.g. --answer tower_name=north
|
||||
# --answer yaw.r_sense_ohm=0.075. A drivetrain value may be answered `unknown`,
|
||||
# --answer yaw.r_sense_ohm=0.075. Whether the tower homes at every start and the
|
||||
# mode it starts in are config.ini [Startup] home / mode: --answer startup_home=y
|
||||
# --answer startup_mode=auto. A drivetrain value may be answered `unknown`,
|
||||
# which leaves a commented ; TODO in hardware.ini and that axis unable to move.
|
||||
# Nothing here ever makes one up: a wrong sense resistor cooks a driver, and a
|
||||
# wrong homing current shears an endstop - both have happened on this project.
|
||||
|
|
@ -194,6 +196,17 @@ py_serial() {
|
|||
|
||||
app_running() { "$FGC" status >/dev/null 2>&1; }
|
||||
|
||||
# The flags [Startup] replaced, and an argument list without them.
|
||||
is_startup_flag() { case "$1" in --init|-i|--start|-s) return 0 ;; *) return 1 ;; esac; }
|
||||
has_flag() { # "args" flag... -> 0 if any of the flags is in args
|
||||
local a f; for a in $1; do for f in "${@:2}"; do [[ $a == "$f" ]] && return 0; done; done; return 1
|
||||
}
|
||||
strip_startup() {
|
||||
local a out=(); for a in $1; do is_startup_flag "$a" || out+=("$a"); done; echo "${out[*]:-}"
|
||||
}
|
||||
# What the installed boot service passes to `fgc start`, as written in the unit.
|
||||
unit_args() { [[ -r $UNIT ]] && sed -n 's|^ExecStart=.*/scripts/fgc start *||p' "$UNIT" | head -1; }
|
||||
|
||||
# One command line into the running program, whether it shows the dashboard or
|
||||
# the plain console: the dashboard needs ':' to open its command line first.
|
||||
app_cmd() {
|
||||
|
|
@ -207,8 +220,9 @@ app_cmd() {
|
|||
|
||||
# The arguments the running app was started with, so a restart puts it back the
|
||||
# way it was. From the process itself, not its log: an older build writes no log
|
||||
# file, and restarting tower 111 without its --init --start would have left it
|
||||
# running but capturing nothing.
|
||||
# file. An older build may also carry --init/--start, which the program now
|
||||
# refuses - they are config.ini [Startup] - so a restart drops them (strip_startup)
|
||||
# and the survey moves them into config.ini instead (see [Startup] below).
|
||||
app_args() {
|
||||
ps -u "$USER" -o args= 2>/dev/null | grep -m1 -E '^[^ ]*/fire_gimbal_control( |$)' |
|
||||
awk '{ for (i = 2; i <= NF; i++) printf "%s%s", $i, (i < NF ? " " : "") }' || true
|
||||
|
|
@ -361,7 +375,7 @@ running firmware : ${S_FW_NOW:-unknown}
|
|||
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
|
||||
usbfs_memory_mb : $S_USBFS
|
||||
hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent)
|
||||
config.ini : $([[ -r $CFG ]] && echo "present, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?')" || echo absent)
|
||||
config.ini : $([[ -r $CFG ]] && echo "present, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent)
|
||||
app : $([[ $S_RUNNING -eq 1 ]] && echo "running (${S_ARGS:-no arguments})" || echo "not running")
|
||||
service : ${S_SVC_ENABLED:-absent} / ${S_SVC_ACTIVE:-inactive}, linger ${S_LINGER}
|
||||
PlatformIO : ${S_PIO:-not installed}
|
||||
|
|
@ -452,6 +466,12 @@ verify() {
|
|||
# Only once there IS a service: a tower run by hand is a choice, not a fault.
|
||||
if [[ -r $UNIT ]]; then
|
||||
check "boot service" "$([[ $S_SVC_ENABLED == enabled ]] && echo 0 || echo 1)" "$S_SVC_ENABLED"
|
||||
# A unit from before [Startup] passes flags the program now refuses: the
|
||||
# tower would not come up at its next boot.
|
||||
check "boot service args" "$(has_flag "$(unit_args || true)" --init -i --start -s && echo 1 || echo 0)" \
|
||||
"$(has_flag "$(unit_args || true)" --init -i --start -s &&
|
||||
echo "passes '$(unit_args)', which the program refuses - re-run install-tower to move it into config.ini [Startup]" ||
|
||||
echo "fgc start $(unit_args || true)")"
|
||||
check "user lingering" "$([[ $S_LINGER == yes ]] && echo 0 || echo 1)" \
|
||||
"$S_LINGER (without it a service-started app and an ssh session disagree on the lock file)"
|
||||
else
|
||||
|
|
@ -723,6 +743,45 @@ done
|
|||
if [[ -n $S_LPM ]] && [[ -z $(ini_get "$CFG" Test usb3_lpm_path 2>/dev/null || true) ]]; then
|
||||
CFG_SETS+=("Test.usb3_lpm_path=$S_LPM")
|
||||
fi
|
||||
# [Startup]: whether the tower homes at every start, and the mode it starts in.
|
||||
# These used to be the boot service's --init / --start; a tower still set up that
|
||||
# way has them moved here, as they were, and the service rewritten without them
|
||||
# (below) - the program refuses those flags now. Asked only when nothing says
|
||||
# yet: a config.ini that has them keeps them unless an --answer changes them.
|
||||
LEGACY_ARGS="$(unit_args || true)"; [[ -r $UNIT ]] || LEGACY_ARGS="${S_ARGS:-}"
|
||||
ST_HOME="$(ini_get "$CFG" Startup home 2>/dev/null || true)"
|
||||
ST_MODE="$(ini_get "$CFG" Startup mode 2>/dev/null || true)"
|
||||
WANT_HOME="" WANT_MODE=""
|
||||
if [[ -z $ST_HOME && -z $ST_MODE ]] && has_flag "$LEGACY_ARGS" --init -i --start -s; then
|
||||
has_flag "$LEGACY_ARGS" --init -i && WANT_HOME=true || WANT_HOME=false
|
||||
has_flag "$LEGACY_ARGS" --start -s && WANT_MODE=auto || WANT_MODE=manual
|
||||
say "the program was started with '$LEGACY_ARGS': moving that into config.ini [Startup] (home = $WANT_HOME, mode = $WANT_MODE)"
|
||||
fi
|
||||
if [[ -n ${ANS[startup_home]+x} || ( -z $ST_HOME && -z $WANT_HOME ) ]]; then
|
||||
echo
|
||||
echo "With home = y the program HOMES THE GIMBAL UNATTENDED at every start - which a"
|
||||
echo "tower in service needs, since the firmware keeps no position across a power cycle."
|
||||
ask_yn startup_home "Home the gimbal at every start?" y && WANT_HOME=true || WANT_HOME=false
|
||||
fi
|
||||
if [[ -n ${ANS[startup_mode]+x} || ( -z $ST_MODE && -z $WANT_MODE ) ]]; then
|
||||
echo
|
||||
echo "manual waits and captures nothing until someone chooses a mode; auto starts"
|
||||
echo "sweeping at once. Start a new tower in manual, and choose auto once it is proven."
|
||||
while :; do
|
||||
ask startup_mode "Mode it starts in (manual or auto)" manual
|
||||
case "${REPLY_VAL,,}" in manual|auto) WANT_MODE="${REPLY_VAL,,}"; break ;; esac
|
||||
warn "'$REPLY_VAL' is not manual or auto (pan is chosen by command only)"; unset "ANS[startup_mode]"
|
||||
[[ $NONINTERACTIVE -eq 1 ]] && die "startup_mode must be manual or auto"
|
||||
done
|
||||
fi
|
||||
[[ -n $WANT_HOME && $WANT_HOME != "$ST_HOME" ]] && CFG_SETS+=("Startup.home=$WANT_HOME")
|
||||
[[ -n $WANT_MODE && $WANT_MODE != "$ST_MODE" ]] && CFG_SETS+=("Startup.mode=$WANT_MODE")
|
||||
# What the program will do from here on, for the homing gate and the report.
|
||||
START_HOME="${WANT_HOME:-${ST_HOME:-false}}"; START_MODE="${WANT_MODE:-${ST_MODE:-manual}}"
|
||||
case "${START_HOME,,}" in 1|true|yes|on) START_HOME=true ;; *) START_HOME=false ;; esac
|
||||
# A restart puts the program back with its arguments, minus the flags it refuses.
|
||||
RESTART_ARGS="$(strip_startup "${S_ARGS:-}")"
|
||||
|
||||
# In an existing config.ini, the keys hardware.ini now owns are dead copies the
|
||||
# program warns about at every start. They are commented out, value and all.
|
||||
declare -a CFG_DROPS=()
|
||||
|
|
@ -789,33 +848,38 @@ 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
|
||||
}
|
||||
# What an installed unit starts the program with, so a re-run keeps it - minus
|
||||
# --init/--start, which the program refuses: they are config.ini [Startup] now.
|
||||
installed_args() { strip_startup "$(unit_args || true)"; }
|
||||
UNIT_TEXT=""
|
||||
# A new tower homes at every boot but captures nothing until a person has chosen
|
||||
# auto: --init, not --init --start. Once it is proven, the operator makes it boot
|
||||
# into auto by re-running with --answer service_args="--init --start". A service
|
||||
# of ours keeps the arguments it has; one that is not ours (a legacy tower's old
|
||||
# program) gets the same safe default.
|
||||
SVC_ARGS_DEFAULT="--init"
|
||||
[[ -r $UNIT && -n $(installed_args) ]] && SVC_ARGS_DEFAULT="$(installed_args)"
|
||||
# The service passes no startup flags: whether it homes and the mode it starts in
|
||||
# come from config.ini [Startup], asked above. Only other arguments (--no-mqtt,
|
||||
# say) belong here. A service of ours keeps the ones it has.
|
||||
SVC_ARGS_DEFAULT="$(installed_args)"
|
||||
if [[ ${ANS[service_args]+x} ]] && has_flag "${ANS[service_args]}" --init -i --start -s; then
|
||||
die "service_args may not hold --init/--start any more: pass --answer startup_home=y / startup_mode=auto"
|
||||
fi
|
||||
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/})"
|
||||
plan service skip "installed and enabled (fgc start${SVC_ARGS_DEFAULT:+ $SVC_ARGS_DEFAULT})"
|
||||
elif [[ -r $UNIT && $S_SVC_ENABLED == enabled && -z ${ANS[service_args]+x} ]] &&
|
||||
has_flag "$(unit_args || true)" --init -i --start -s; then
|
||||
# Ours, and set up before [Startup]: the program would refuse to start at the
|
||||
# next boot. No question - the flags have been moved into config.ini above.
|
||||
UNIT_TEXT="$(render_unit "$(installed_args)")"
|
||||
plan service change "drop '$(unit_args)' from $UNIT (now config.ini [Startup]); lingering for $USER"
|
||||
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 - which is what a tower in"
|
||||
echo "service needs, since the firmware keeps no position across a power cycle."
|
||||
echo "With --init alone it then WAITS IN MANUAL and captures nothing; add --start"
|
||||
echo "(--init --start) to boot straight into auto once the tower is proven."
|
||||
echo "The boot service starts the program at every boot. What it does then is"
|
||||
echo "config.ini [Startup]: this tower will $([[ $START_HOME == true ]] && echo "HOME THE GIMBAL UNATTENDED at every boot" || echo "NOT home at boot (it cannot move until someone homes it)"),"
|
||||
echo "then $([[ $START_MODE == auto ]] && echo "sweep in auto" || echo "wait in manual, capturing nothing until someone chooses auto")."
|
||||
if ask_yn service "Install and enable it?" y; then
|
||||
ask service_args "Arguments it starts the program with ('none' for none)" "$SVC_ARGS_DEFAULT"
|
||||
ask service_args "Other arguments it starts the program with ('none' for none)" "${SVC_ARGS_DEFAULT:-none}"
|
||||
svc_args="$REPLY_VAL"; [[ $svc_args == none ]] && svc_args=""
|
||||
has_flag "$svc_args" --init -i --start -s &&
|
||||
die "--init/--start are config.ini [Startup] now: answer startup_home / startup_mode instead"
|
||||
UNIT_TEXT="$(render_unit "$svc_args")"
|
||||
plan service change "write $UNIT (fgc start $svc_args), enable it, enable lingering for $USER"
|
||||
plan service change "write $UNIT (fgc start${svc_args:+ $svc_args}), enable it, enable lingering for $USER"
|
||||
else plan service skip "declined"; fi
|
||||
fi
|
||||
|
||||
|
|
@ -861,7 +925,7 @@ put_back() {
|
|||
warn "stopped before finishing - restarting the program as it was"
|
||||
if [[ $STOPPED_SVC -eq 1 ]]; then sudo systemctl start fire-gimbal-control
|
||||
else
|
||||
local a; read -r -a a <<< "${S_ARGS:-}"
|
||||
local a; read -r -a a <<< "${RESTART_ARGS:-}"
|
||||
( cd "$REPO" && "$FGC" start "${a[@]}" ) > /dev/null
|
||||
fi
|
||||
return $rc
|
||||
|
|
@ -1046,14 +1110,19 @@ if [[ $TEST_RC != 0 ]]; then
|
|||
elif ask_yn home "Someone is at the rig with a hand on the power - home now and take one picture?" n; then
|
||||
stop_app
|
||||
before="$(latest_log)"
|
||||
# Through the service when the service is what homes it, so the tower ends
|
||||
# up running under it; otherwise the same run by hand. Never --start here:
|
||||
# the tower is left in manual, and auto is a person's choice (a service
|
||||
# already set to --start is the exception - that choice was made).
|
||||
if [[ $SVC_ON -eq 1 && " $(installed_args) " == *" --init "* ]]; then sudo systemctl start fire-gimbal-control
|
||||
else ( cd "$REPO" && "$FGC" start --init ) > /dev/null; fi
|
||||
say "homing (this can take a few minutes)"
|
||||
# When config.ini [Startup] homes, the program homes as it starts - through
|
||||
# the service if there is one, so the tower ends up running under it. When it
|
||||
# does not, start it and home by command: config.ini is not changed for this.
|
||||
# It comes up in its [Startup] mode; auto stays a person's choice unless that
|
||||
# choice has already been made there.
|
||||
if [[ $START_HOME == true && $SVC_ON -eq 1 ]]; then sudo systemctl start fire-gimbal-control
|
||||
else ( cd "$REPO" && "$FGC" start ) > /dev/null; fi
|
||||
log=""; for _ in $(seq 1 30); do log="$(latest_log)"; [[ -n $log && $log != "$before" ]] && break; sleep 1; done
|
||||
if [[ $START_HOME != true ]]; then
|
||||
for _ in $(seq 1 60); do grep -q 'Entering control loop' "$log" 2>/dev/null && break; sleep 1; done
|
||||
app_cmd "gimbal home"
|
||||
fi
|
||||
say "homing (this can take a few minutes)"
|
||||
outdir=""; ok=0; snapped=0
|
||||
for _ in $(seq 1 420); do
|
||||
grep -q 'Gimbal homed' "$log" 2>/dev/null && ok=1
|
||||
|
|
@ -1084,12 +1153,12 @@ hr "restart"
|
|||
if app_running; then
|
||||
say "running"
|
||||
elif [[ $WAS_RUNNING -eq 1 ]]; then
|
||||
read -r -a restart_args <<< "${S_ARGS:-}"
|
||||
read -r -a restart_args <<< "${RESTART_ARGS:-}"
|
||||
before="$(latest_log)"
|
||||
( cd "$REPO" && "$FGC" start "${restart_args[@]}" ) > /dev/null && say "restarted as it was: fgc start${S_ARGS:+ $S_ARGS}"
|
||||
# With --init it is homing now, and refuses a test until it is done: wait,
|
||||
# or the checks below report a busy program as a failed one.
|
||||
if [[ " ${S_ARGS:-} " == *" --init "* ]]; then
|
||||
( cd "$REPO" && "$FGC" start "${restart_args[@]}" ) > /dev/null && say "restarted as it was: fgc start${RESTART_ARGS:+ $RESTART_ARGS}"
|
||||
# With [Startup] home it is homing now, and refuses a test until it is done:
|
||||
# wait, or the checks below report a busy program as a failed one.
|
||||
if [[ $START_HOME == true ]]; then
|
||||
say "waiting for it to home before checking"
|
||||
for _ in $(seq 1 420); do
|
||||
log="$(latest_log)"
|
||||
|
|
@ -1116,10 +1185,16 @@ open=0
|
|||
while read -r t; do [[ -n $t ]] && { echo " - hardware.ini: $t"; open=1; }; done < <(hw_todos)
|
||||
[[ $HOMED == homed* ]] || { echo " - homing: $HOMED"; open=1; }
|
||||
[[ $HOMED == homed* ]] && { echo " - calibration: run 'gimbal heading setup' to tie yaw to true north"; open=1; }
|
||||
if [[ -r $UNIT && " $(installed_args) " != *" --start "* ]]; then
|
||||
echo " - capture: the tower waits in MANUAL at every boot and captures nothing. Choose auto"
|
||||
echo " yourself (key a, or 'mode auto'); once it is proven, make it boot into auto:"
|
||||
echo " scripts/install-tower --answer service_args=\"--init --start\""
|
||||
if [[ $START_HOME != true ]]; then
|
||||
echo " - homing: config.ini [Startup] home is off, so after a power cut the gimbal cannot"
|
||||
echo " move until someone homes it ('gimbal home'). A tower in service homes at every start:"
|
||||
echo " scripts/install-tower --answer startup_home=y"
|
||||
open=1
|
||||
fi
|
||||
if [[ $START_MODE != auto ]]; then
|
||||
echo " - capture: the tower waits in MANUAL at every start and captures nothing. Choose auto"
|
||||
echo " yourself (key a, or 'mode auto'); once it is proven, make it start in auto:"
|
||||
echo " scripts/install-tower --answer startup_mode=auto"
|
||||
open=1
|
||||
fi
|
||||
[[ $(ini_get "$CFG" Features enable_mqtt 2>/dev/null) == true ]] &&
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
# Launch fire_gimbal_control, resolving the binary relative to this script so it
|
||||
# works regardless of where the repo is checked out. Extra args are forwarded.
|
||||
#
|
||||
# scripts/run.sh --init --start # homes first
|
||||
# scripts/run.sh --mock-serial --mock-camera --no-mqtt --start # dev, no hardware
|
||||
# scripts/run.sh # real deployment
|
||||
# scripts/run.sh --mock-serial --mock-camera --no-mqtt # dev, no hardware
|
||||
#
|
||||
# --init homes the gimbal. The firmware keeps nothing across a power cycle, so
|
||||
# without it the axes stay in RESET and refuse every move.
|
||||
# Homing and the start mode come from config.ini [Startup] home / mode. The
|
||||
# firmware keeps nothing across a power cycle, so without home = true the axes
|
||||
# stay in RESET and refuse every move.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ void VimbaCameraSource::close() {
|
|||
// happened at open(), and changes only through setProfile().
|
||||
void VimbaCameraSource::start() {
|
||||
// Idempotent. Several callers reach here without knowing whether another
|
||||
// already has - `start` (scan), `snapshot`, and --start on the command line -
|
||||
// already has - `start` (scan), `snapshot`, and a [Startup] mode = auto -
|
||||
// and re-entering would restart acquisition under the SDK's feet.
|
||||
if (impl_->started) return;
|
||||
impl_->startAcquisition();
|
||||
|
|
|
|||
|
|
@ -1774,7 +1774,7 @@ struct Application::Impl {
|
|||
: ": reset FAILED (see above)");
|
||||
}
|
||||
|
||||
// The one way the mode changes - `mode ...`, the a / p / m keys, --start, and a
|
||||
// The one way the mode changes - `mode ...`, the a / p / m keys, [Startup] mode, and a
|
||||
// test pausing the scan all come here. Returns false, having said why and
|
||||
// leaving the mode as it was, when the gimbal cannot do what was asked.
|
||||
// StatusCode is published on every change, so it always says what the tower
|
||||
|
|
@ -2338,7 +2338,7 @@ struct Application::Impl {
|
|||
motor->sendCommand(raw);
|
||||
} else if (sub == "home" && tok.size() <= 2) {
|
||||
// Full re-home: enable + HOME + wait for READY + re-anchor the yaw zero
|
||||
// (same path as startup --init), so the homing key behaves like a boot
|
||||
// (same path as the startup home, [Startup] home = true), so the homing key behaves like a boot
|
||||
// home rather than a fire-and-forget HOME that skips the re-anchor.
|
||||
runInitSequence();
|
||||
} else if (sub == "home" || sub == "stop" || sub == "reset" || sub == "enable" ||
|
||||
|
|
@ -3071,7 +3071,7 @@ struct Application::Impl {
|
|||
scheduler->setSettleWait(cfg.camera.maxSettleWaitMs(), cfg.camera.maxFrameTimeoutMs());
|
||||
|
||||
// Before the firmware is talked to: the registers are wanted by the very
|
||||
// first sendAnchorConfig(), on both the --init and the no---init path.
|
||||
// first sendAnchorConfig(), on both the homing and the no-homing startup path.
|
||||
planMotorCurrents();
|
||||
motor->start();
|
||||
if (imu) imu->start();
|
||||
|
|
@ -3091,7 +3091,7 @@ struct Application::Impl {
|
|||
}
|
||||
|
||||
pipeline->start();
|
||||
channel->publishStatus(static_cast<int>(Mode::Manual)); // until --start says otherwise
|
||||
channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise
|
||||
|
||||
// Seed an initial snapshot so the UI's first frame is populated, then
|
||||
// start the interface (headless console or TUI) on its own thread. The
|
||||
|
|
@ -3111,7 +3111,14 @@ struct Application::Impl {
|
|||
cmd_queue.push(line);
|
||||
});
|
||||
|
||||
if (opts.init) {
|
||||
// [Startup] decides; a --test run ignores it, so a test never homes the
|
||||
// gimbal or starts a sweep that nobody asked the test for.
|
||||
const bool test_run = !opts.run_test.empty();
|
||||
const bool home = cfg.startup.home && !test_run;
|
||||
const Mode start_in = test_run ? Mode::Manual : cfg.startup.mode;
|
||||
if (test_run && (cfg.startup.home || cfg.startup.mode != Mode::Manual))
|
||||
LOG_INFO << "--test: ignoring [Startup] (no homing, staying in manual)";
|
||||
if (home) {
|
||||
runInitSequence(); // homes, then derives the travel window
|
||||
} else {
|
||||
// No homing this launch. The firmware keeps nothing across a power
|
||||
|
|
@ -3121,7 +3128,7 @@ struct Application::Impl {
|
|||
sendAnchorConfig();
|
||||
applyHomedTravel();
|
||||
}
|
||||
if (opts.start) setMode(Mode::Auto);
|
||||
if (start_in != Mode::Manual) 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).
|
||||
|
|
|
|||
|
|
@ -477,6 +477,18 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
|||
|
||||
cfg.ui.enable_tui = getBool(kv, "UI.enable_tui", cfg.ui.enable_tui);
|
||||
|
||||
cfg.startup.home = getBool(kv, "Startup.home", cfg.startup.home);
|
||||
if (auto it = kv.find("Startup.mode"); it != kv.end() && !it->second.empty()) {
|
||||
if (it->second == "manual") cfg.startup.mode = Mode::Manual;
|
||||
else if (it->second == "auto") cfg.startup.mode = Mode::Auto;
|
||||
else
|
||||
throw std::runtime_error("Config key 'Startup.mode' must be manual or auto, got '" +
|
||||
it->second + "'" +
|
||||
(it->second == "pan" ? " (pan is chosen by command: "
|
||||
"`mode pan <heading> <span>`)"
|
||||
: ""));
|
||||
}
|
||||
|
||||
// [Motor]: the degrees<->counts maps, anchored on the homed mechanical centre
|
||||
// (see Geometry). min_deg/max_deg are NOT configured - they are derived from
|
||||
// the firmware's homed soft limits, the only place they can be correct.
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ const std::vector<HelpSection>& helpCatalog() {
|
|||
{"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; --start boots into it. In auto and pan only",
|
||||
"status, logging, 'mode', 'cancel' and 'exit' are accepted."}},
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ add_executable(fgc_tests
|
|||
test_testrunner.cpp
|
||||
)
|
||||
target_link_libraries(fgc_tests PRIVATE fgc_core doctest::doctest)
|
||||
# So a test can read the shipped config/config.example.ini and hold it to what
|
||||
# the docs promise a new tower gets.
|
||||
target_compile_definitions(fgc_tests PRIVATE FGC_SOURCE_DIR="${PROJECT_SOURCE_DIR}")
|
||||
|
||||
include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake)
|
||||
doctest_discover_tests(fgc_tests)
|
||||
|
|
|
|||
|
|
@ -135,6 +135,42 @@ TEST_CASE("brokerUri builds a Paho serverURI from host + port") {
|
|||
.network.brokerUri() == "tcp://10.0.0.5:8883");
|
||||
}
|
||||
|
||||
TEST_CASE("[Startup] defaults to the idle start: no homing, manual") {
|
||||
AppConfig d = ConfigLoader::fromMap({});
|
||||
CHECK(d.startup.home == false);
|
||||
CHECK(d.startup.mode == Mode::Manual);
|
||||
|
||||
AppConfig e = ConfigLoader::fromMap({{"Startup.home", ""}, {"Startup.mode", ""}});
|
||||
CHECK(e.startup.home == false);
|
||||
CHECK(e.startup.mode == Mode::Manual);
|
||||
}
|
||||
|
||||
TEST_CASE("[Startup] home and mode are read") {
|
||||
AppConfig c = ConfigLoader::fromMap({{"Startup.home", "true"}, {"Startup.mode", "auto"}});
|
||||
CHECK(c.startup.home == true);
|
||||
CHECK(c.startup.mode == Mode::Auto);
|
||||
|
||||
AppConfig m = ConfigLoader::fromMap({{"Startup.home", "false"}, {"Startup.mode", "manual"}});
|
||||
CHECK(m.startup.home == false);
|
||||
CHECK(m.startup.mode == Mode::Manual);
|
||||
}
|
||||
|
||||
TEST_CASE("[Startup] refuses a mode it cannot start in") {
|
||||
CHECK_THROWS(ConfigLoader::fromMap({{"Startup.mode", "atuo"}}));
|
||||
CHECK_THROWS(ConfigLoader::fromMap({{"Startup.mode", "pan"}})); // needs a spec
|
||||
CHECK_THROWS(ConfigLoader::fromMap({{"Startup.home", "maybe"}}));
|
||||
}
|
||||
|
||||
TEST_CASE("the example config starts idle-but-homed, like a new tower") {
|
||||
const auto kv = parseIniString([] {
|
||||
std::ifstream f(std::string(FGC_SOURCE_DIR) + "/config/config.example.ini");
|
||||
return std::string(std::istreambuf_iterator<char>(f), {});
|
||||
}());
|
||||
AppConfig c = ConfigLoader::fromMap(kv);
|
||||
CHECK(c.startup.home == true);
|
||||
CHECK(c.startup.mode == Mode::Manual);
|
||||
}
|
||||
|
||||
TEST_CASE("Network.mqtt_port is range-checked") {
|
||||
CHECK(ConfigLoader::fromMap({}).network.mqtt_port == 1883);
|
||||
CHECK(ConfigLoader::fromMap({{"Network.mqtt_port", "8883"}}).network.mqtt_port == 8883);
|
||||
|
|
|
|||
Loading…
Reference in New Issue