Skip to content

Commit b743bef

Browse files
authored
Merge pull request #18 from OEDO-Dynamics/docs/can-tools-d-operation
docs: Add Operation section and update CI / config examples (#9 PR-D)
2 parents bbf5bff + 4537660 commit b743bef

4 files changed

Lines changed: 787 additions & 38 deletions

File tree

‎.github/workflows/shellcheck.yml‎

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
name: shellcheck
22

3-
on: [push, pull_request]
3+
on:
4+
push:
5+
branches: [develop, main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
410

511
jobs:
612
shellcheck:
@@ -18,48 +24,39 @@ jobs:
1824
1925
# Run shellcheck on repo-root *.sh and *.bash files, tests/
2026
# Exclude can-utils/, openimu/
21-
# Exclude files that will be modified in later PRs (PR-A2, PR-B):
22-
# - canhat_fd_setup.bash
23-
# - canhat_fd_rename.bash
24-
# - enable_canhat_fd.sh
25-
# - can_monitor.sh
26-
# (These exclusions will be removed in PR-D / #9)
2727
2828
echo "=== Running shellcheck -S warning ==="
2929
3030
# Build file list
3131
files=()
3232
33-
# Add repo-root *.sh and *.bash (excluding excluded patterns)
33+
# Add repo-root *.sh and *.bash (can-utils/ and openimu/ are not
34+
# matched by -maxdepth 1 in the first place, so no further
35+
# exclusion is needed for them).
3436
while IFS= read -r f; do
35-
basename="$(basename "$f")"
36-
# Skip files to be modified in later PRs (PR-A2, PR-B)
37-
if [[ "$basename" == "canhat_fd_setup.bash" ]] || \
38-
[[ "$basename" == "canhat_fd_rename.bash" ]] || \
39-
[[ "$basename" == "enable_canhat_fd.sh" ]] || \
40-
[[ "$basename" == "can_monitor.sh" ]]; then
41-
continue
42-
fi
4337
files+=("$f")
44-
done < <(find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.bash" \) ! -path "./can-utils/*" ! -path "./openimu/*" | sort)
38+
done < <(find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.bash" \) | sort)
4539
46-
# Add tests/ files: test_*.sh/bash and tests/fakes/* (bash scripts without extension)
40+
# Add tests/ files: tests/run.sh, test_*.sh/bash, fakes/*, diag_fakes/* (all shell scripts)
4741
while IFS= read -r f; do
48-
if [[ "$f" == "tests/test_"* ]] || [[ "$f" == "tests/fakes/"* ]]; then
49-
files+=("$f")
42+
if [[ -f "$f" ]] && {
43+
[[ "$f" == "tests/run.sh" ]] || \
44+
[[ "$f" == "tests/test_"* ]] || \
45+
[[ "$f" == "tests/fakes/"* ]] || \
46+
[[ "$f" == "tests/diag_fakes/"* ]]; }; then
47+
# Only add if it's a shell script (bash/sh shebang, or *.sh/*.bash)
48+
if head -1 "$f" 2>/dev/null | grep -qE '^#!.*\b(ba)?sh\b' || [[ "$f" =~ \.(sh|bash)$ ]]; then
49+
files+=("$f")
50+
fi
5051
fi
51-
done < <(find tests/ -type f \( -name "*.sh" -o -name "*.bash" -o -path "tests/fakes/*" \) | sort)
52+
done < <(find tests/ -type f 2>/dev/null | sort)
5253
5354
if (( ${#files[@]} > 0 )); then
5455
shellcheck -S warning "${files[@]}"
55-
rc=$?
5656
else
5757
echo "No shell files found to check"
58-
rc=0
5958
fi
6059
61-
exit $rc
62-
6360
- name: Run tests
6461
run: |
6562
cd tests

‎README.md‎

Lines changed: 202 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,20 @@ For full option details, see `/opt/can_tools/can_tools.conf.example`.
7373

7474
#### Applying configuration changes
7575

76-
A conf edit (or a reinstall that updates a script) only takes effect:
77-
- at the **next reboot**, or
78-
- by running `sudo systemctl restart canhat_fd_setup.service` while the
79-
**machine is stopped** (hydraulics locked, implement down, key state
80-
decided).
81-
82-
Never restart it, or run `ip link set ... down/up`, while the machine is
83-
live - bringing CAN down/up while the controller is sending/receiving is
84-
what caused the 2026-10-14 incident. Until the next reboot (or that manual,
85-
machine-stopped restart), the running `canhat_fd_setup.service` keeps using
86-
the previously-applied settings.
76+
> [!WARNING]
77+
> **Never edit `/opt/can_tools/can_tools.conf` while the machine is live, and
78+
> never run `systemctl restart canhat_fd_setup.service` or `ip link set ...
79+
> down/up` by hand while it is live.** `can_watchdog.service` re-reads the
80+
> conf file on **every cycle** (every `WD_LOOP_S`, 1 s by default) - it does
81+
> **not** wait for a reboot the way this used to be documented. See
82+
> [`docs/can_watchdog.md`](docs/can_watchdog.md#configuration-is-re-read-live-the-dangerous-part)
83+
> for exactly which keys take effect immediately, which ones only pause the
84+
> watchdog (safe), and which ones get applied live to a running interface the
85+
> next time the watchdog happens to flush/recover it (dangerous - this is how
86+
> the 2026-10-14 incident happened). Only edit the conf with the machine
87+
> mechanically stopped (hydraulics locked, implement lowered, key state
88+
> decided), and reboot (or restart `canhat_fd_setup.service`, still with the
89+
> machine stopped) right after.
8790
8891
### Systemd Services
8992

@@ -126,6 +129,194 @@ disable` above and put the machine back into the FD/Plus race this section
126129
exists to avoid. Update a CAN HAT Plus machine by hand too - copy over the
127130
changed files/units yourself instead of running `install.sh`.
128131

132+
## Operation
133+
134+
This section is an overview; the full operator runbook (complete reason-code
135+
table, CRIT triage, `--clear-degraded` caveats, the exact `can_pause`/
136+
`can_diag.sh` invocations, and the manual unbind/bind procedure) is in
137+
[`docs/can_watchdog.md`](docs/can_watchdog.md). Read that before touching a
138+
machine that is in operation.
139+
140+
### System Architecture
141+
142+
The CAN subsystem starts in this boot sequence order:
143+
144+
1. `canhat_fd_rename.service` (`Type=oneshot`,
145+
`After=sys-subsystem-net-devices-can0.device sys-subsystem-net-devices-can1.device`,
146+
`Before=canhat_fd_setup.service docker.service`) - pins `can0`/`can1` to
147+
their physical SPI devices per `CAN_SPI_MAP` (default
148+
`spi0.0:can0 spi1.0:can1`) using the lib's `can_pin_name` (`ip link set
149+
<iface> down` then `... name <want>`, with a probe-order swap if the two
150+
SPI devices enumerate in the opposite order).
151+
2. `canhat_fd_setup.service` (`Type=oneshot`, `After=canhat_fd_rename.service`)
152+
- brings each interface up via the lib's `can_setup_iface`: `ip link set
153+
<if> down` then `up type can bitrate ... restart-ms ... berr-reporting ...
154+
[dbitrate ... fd on | fd off]`, then `ip link set <if> txqueuelen ...`.
155+
**No `ethtool` is used or installed** on the machine - everything goes
156+
through `ip link set` / sysfs.
157+
3. `can_watchdog.service` (`Type=simple`, `Restart=always`,
158+
`Wants=canhat_fd_setup.service`, `After=canhat_fd_setup.service`) - the
159+
resident watchdog described below.
160+
161+
`canhat_fd_rename.service` does declare `Before=canhat_fd_setup.service
162+
docker.service` (so renaming happens before both `canhat_fd_setup.service`
163+
and `docker.service` start), but there is **no further `Before=` ordering**
164+
from `canhat_fd_setup.service` or `can_watchdog.service` onto whatever
165+
application or container speaks CAN - beyond that one `Before=`, only the
166+
`After=` chain above between the three can_tools units themselves.
167+
If an app or container needs to start strictly after the interfaces are up
168+
and the watchdog is running, that has to be arranged on the consumer's own
169+
unit (or handled by the app's own reconnect-on-ENETDOWN logic; see
170+
`CAN_DEPENDENT_UNITS` in `docs/can_watchdog.md`).
171+
172+
`can_watchdog` is **FD-only**: `install.sh` only ever installs/enables it
173+
alongside the FD units, and it is never enabled on a CAN HAT Plus machine
174+
(see "CAN HAT Plus" below).
175+
176+
### Deployment Workflow
177+
178+
**Step 1: Verify what would change (optional but recommended)**
179+
180+
```bash
181+
sudo ./install.sh --check
182+
```
183+
184+
Prints a unified diff for every file that would change and exits non-zero if
185+
there is anything to do. `install.sh --check` run again right after a plain
186+
`install.sh` should show no differences and exit 0 (running `--check` twice
187+
in a row with **no** `install.sh` in between shows the same diff both times -
188+
`--check` itself never changes anything).
189+
190+
**Step 2: Install or upgrade**
191+
192+
```bash
193+
sudo ./install.sh
194+
```
195+
196+
Idempotent and non-destructive: files are only written if their contents
197+
differ, running services are never stopped/restarted/bounced, and an
198+
existing `/opt/can_tools/can_tools.conf` is never overwritten. The legacy
199+
`can_monitor.timer`/`can_monitor.service` units **are** stopped, disabled and
200+
removed by this step if still present - that is the one "stops a service"
201+
exception, and it is intentional (`can_monitor` is retired).
202+
203+
**Step 3: Apply new settings - machine stopped, not live**
204+
205+
See the warning in "Applying configuration changes" above and
206+
[`docs/can_watchdog.md`](docs/can_watchdog.md#configuration-is-re-read-live-the-dangerous-part):
207+
`can_watchdog` re-reads the conf **every cycle**, not at reboot, so **never
208+
edit the conf while the machine is running** - no key is safe to change live
209+
(e.g. adding `can1` to `CAN_FLUSH_IFACES` can down/up a live `can1`). Changing `CAN_RESTART_MS` / `CAN_BERR_REPORTING` / `CAN_FD` makes
210+
the watchdog detect a mismatch against the still-running kernel settings and
211+
switch that interface to **observe-only** (`reason=config_mismatch_reboot_required`)
212+
until a reboot (or a machine-stopped `canhat_fd_setup.service` restart)
213+
actually applies them. Changing `CAN_BITRATE` / `CAN_DBITRATE` /
214+
`CAN_TXQUEUELEN` is **not** checked for a mismatch at all, so it is applied
215+
live, to a running interface, the next time the watchdog calls
216+
`can_setup_iface` for any reason (a TX flush, a bus-off recovery, a stall
217+
rebind) - with the machine still operating. Only ever edit the conf with the
218+
machine mechanically stopped, then reboot (or restart
219+
`canhat_fd_setup.service`, still stopped) right away.
220+
221+
### Watchdog Behavior and Logging
222+
223+
Once running, `can_watchdog.service` monitors each CAN interface every
224+
`WD_LOOP_S` (1 s default) and recovers autonomously from most error
225+
conditions. **It does not automatically reboot the machine** (see Issue #10).
226+
A one-paragraph summary of each mechanism follows; see
227+
[`docs/can_watchdog.md`](docs/can_watchdog.md) for the exact conditions,
228+
backoff schedules and the complete reason-code table.
229+
230+
- **TX flush** (ERROR-WARNING/ERROR-PASSIVE, `CAN_FLUSH_IFACES`, default
231+
`can0` only): if all of rx/tx/rx_fifo/tx_fifo stay unchanged for 2
232+
consecutive cycles, the watchdog re-runs `can_setup_iface` (a down/up) to
233+
discard stale queued TX frames so they are not resent once the ECU comes
234+
back. Any other time the interface is in ERROR-WARNING/ERROR-PASSIVE, the
235+
watchdog does not touch it at all.
236+
- **Bus-off recovery**: on `BUS-OFF`, the watchdog waits for the rx-offload
237+
queue to settle, then re-runs `can_setup_iface` (down/up; the ifindex does
238+
not change, so application sockets stay valid). Repeated bus-off within a
239+
short window backs off exponentially (1 s up to 30 s); a storm (5+ within
240+
60 s) logs one CRIT but recovery keeps running.
241+
- **rx-offload stall detection**: if `rx_packets` is flat while
242+
`rx_fifo_errors`/`tx_fifo_errors` keep growing for 3 consecutive cycles
243+
(or 2 cycles right after a bus-off recovery, or 3 bus-off recoveries in a
244+
row with no rx progress), the watchdog performs an **unbind/bind** cycle
245+
(not a down/up - the rx-offload lock this works around survives a plain
246+
down/up). This changes the ifindex, so any application holding an open CAN
247+
socket must reopen it; see `CAN_DEPENDENT_UNITS` in
248+
`docs/can_watchdog.md` to restart specific units/containers afterward.
249+
- **Degraded mode**: if a step above cannot complete (Oops/BUG detected in
250+
the kernel log, an unbind/bind timeout, a rebind-quota overrun, 3
251+
consecutive `ip` timeouts system-wide, ...), the affected interface(s) are
252+
marked degraded (`/run/can_tools/degraded_<iface>`) and the watchdog stops
253+
touching them - it only re-logs a CRIT every 10 minutes
254+
(`reason=manual_reboot_required`). See `docs/can_watchdog.md` for which
255+
reasons are safe to clear with `can_watchdog.sh --clear-degraded <if>` and
256+
which genuinely require a reboot.
257+
258+
#### Reading Watchdog Logs
259+
260+
```bash
261+
journalctl -u can_watchdog -n 50 -f # last 50 lines, follow
262+
journalctl -u can_watchdog -p crit # CRIT lines only (degraded/storm/Oops/etc.)
263+
```
264+
265+
Common log line shape: `iface=<if> reason=<reason> <details...>` (a repeated
266+
`iface+reason` pair is rate-limited to one line per `WD_LOG_RATE_S`, with
267+
`suppressed=N` on the next line that does get through). The 60 s periodic
268+
INFO summary line (`iface=.. rx=.. tx=.. rxfifo=.. txfifo=.. ifindex=..
269+
degraded=0|1`) has no `reason=` field - that is the only line with
270+
`degraded=`. See `docs/can_watchdog.md` for the full reason-code table and
271+
what each one means operationally.
272+
273+
#### Diagnosing a Stopped Interface
274+
275+
```bash
276+
cat /sys/class/net/can0/statistics/rx_packets # should be increasing
277+
cat /sys/class/net/can0/statistics/rx_fifo_errors # usually 0 / not growing
278+
cat /sys/class/net/can0/statistics/tx_fifo_errors # usually 0 / not growing
279+
```
280+
281+
`rx_packets` flat while `rx_fifo_errors`/`tx_fifo_errors` grow, for 3
282+
consecutive watchdog cycles, is exactly the rx-offload stall signal above -
283+
the watchdog is already acting on it; you do not need to intervene unless it
284+
shows up as degraded. `ip -s link show can0` does not expose the fifo
285+
counters - use sysfs as above.
286+
287+
### Pause and Diagnostics
288+
289+
See [`docs/can_watchdog.md`](docs/can_watchdog.md#pause) for the working
290+
`can_pause`/`can_unpause`/`can_diag.sh` invocations and for `can_diag.sh`'s
291+
actual argument (a duration in seconds, not an interface name), output path,
292+
and what to do if it was killed with SIGKILL mid-capture. In short:
293+
- `can_pause`/`can_unpause` are **shell functions in `can_tools_lib.bash`**,
294+
not standalone commands - there is nothing on `$PATH` called `can_pause`.
295+
- `can_diag.sh` is **bench / machine-stopped use only**: it pauses the
296+
watchdog and turns on bus-error reporting on every interface while it
297+
captures.
298+
299+
### Netlink Hangs (rtnl_hung)
300+
301+
`rtnl_hung` is a **global, all-interface** condition, not a per-interface
302+
one: every watchdog cycle calls one bounded `ip -d link show type can` to
303+
read interface state; if *that call itself* (not an unbind/bind) times out
304+
for `WD_RTNL_HUNG_CONSEC` (3) consecutive cycles, **every** configured
305+
interface is marked degraded with `reason=rtnl_hung`, and the watchdog stops
306+
calling `ip` at all - it falls back to sysfs-only observation for everything
307+
until a reboot. A single interface's unbind hanging instead produces
308+
`reason=unbind_timeout` on just that interface (see
309+
`docs/can_watchdog.md`). Either way, recovery is **manual reboot only** -
310+
there is no live recovery from a genuinely hung netlink socket.
311+
312+
### CAN HAT Plus (Standard CAN)
313+
314+
The notes in the "Systemd Services" section about CAN HAT Plus still apply:
315+
316+
- `install.sh` does not install or enable `canhat_plus_*` units.
317+
- `can_watchdog` is FD-only and will not run on a CAN HAT Plus machine.
318+
- Set up CAN HAT Plus by hand (see the "Systemd Services" section).
319+
129320
## CAN Interface Setup
130321

131322
### PCAN setup

‎can_tools.conf.example‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
# CAN_IFACES=can0 can1
2222
# CAN_SPI_MAP=spi0.0:can0 spi1.0:can1
2323
# CAN_BITRATE=250000
24-
# CAN_RESTART_MS=100
24+
# CAN_RESTART_MS=0
25+
# WARNING: Do NOT set CAN_RESTART_MS to a value other than 0. The kernel's
26+
# automatic bus-off restart (via echo_skb, see raspberrypi/linux#6916) will
27+
# race with can_watchdog's recovery logic, leading to unpredictable behavior.
28+
# The watchdog owns bus-off recovery; leave this at 0.
2529
# CAN_BERR_REPORTING=off
26-
# CAN_FD=on
30+
# CAN_FD=off
2731
# CAN_DBITRATE=2000000
2832
# CAN_TXQUEUELEN=10
2933

0 commit comments

Comments
 (0)