Skip to content

fix(kiss): the TNC built a SharedPtt and never started its watchdog (#1299) - #1306

Merged
dc0sk merged 1 commit into
mainfrom
fix/1299-kiss-ptt-watchdog
Sep 8, 2026
Merged

fix(kiss): the TNC built a SharedPtt and never started its watchdog (#1299)#1306
dc0sk merged 1 commit into
mainfrom
fix/1299-kiss-ptt-watchdog

Conversation

@dc0sk

@dc0sk dc0sk commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes the KISS half of #1299. The CLI half stays open.

What was wrong

openpulse-kiss constructs SharedPtt::new(ptt, DEFAULT_PTT_MAX) (bridge.rs:105) and takes guards from it (bridge.rs:131) — but called spawn_watchdog nowhere in the crate, so force_release_if_expired had no caller and the 180 s deadline was never checked. A SharedPtt with no watchdog thread is the bare Box with extra steps, which is this PR's own phrasing from #1260.

What that did and did not leave exposed

The RAII guard already covers an early return and an unwind, so this is narrower than #1260's leak. What the guard cannot reach is a transmit that blocks rather than returns — which is precisely the case a watchdog exists for, and on a real rig it is a stuck carrier with nothing to take it back.

Where it goes

The constructor (KissServer::with_ptt), matching ARDOP (ardop/src/lib.rs:112) — not run_with_listener, because a caller holding bridge() can transmit without ever running the listener. The JoinHandle is dropped deliberately: the thread exits when the last SharedPtt clone drops.

No observer, unlike ARDOP's: KISS takes its guards with keyed(None) and has no event surface to notify.

How it was found

Writing #1260's Twins section, whose first draft claimed the repeater was "the last hand-rolled keying path". It was not, and the census that corrected it put KISS in a third state nobody had named — guarded but unwatched:

crate SharedPtt guard watchdog
daemon yes yes yes
ardop yes yes yes
repeater yes (#1260) yes yes
kiss yes yes this PR
cli no no no

Tests

the_watchdog_force_releases_a_key_that_outlives_its_deadline drives the real constructor — a test that built its own SharedPtt would pass against the unfixed crate, since the defect was the wiring. It shortens the deadline and keys without holding a guard, standing in for a blocking transmit; a dropped guard would release on its own and prove nothing about the watchdog.

Sabotage-verified: removing the spawn_watchdog line fails it with "the transmitter is STILL KEYED past its deadline".

GATE: PASS d2f751a7ba0602814a51107103451fa838233561 clean — 325 suites, 2497 passed, 0 failed.

Still open

openpulse-cli keys a bare PttController at transmit.rs:18, calibrate.rs:201 and :328. Lower risk — foreground, attended, and transmit.rs releases before its ? — but outside the discipline every other path is now under. #1299 stays open for it.

Consumer

KissServer::with_pttKissBridge::keyed_transmit (bridge.rs:124), the only way this TNC transmits (#1259).

Prior art

ardop/src/lib.rs:112 is the same line for the same reason, added by the #972 follow-up that moved the watchdog core into openpulse-radio "for these front-ends and never wired it" — its own comment. KISS was the front-end that stayed unwired.

Twins

The full census is the table above; it was the output of #1260's Twins check, so this PR is that follow-up. openpulse-mesh had its keying capability removed rather than guarded.

Implements: REQ-PTT-01

🤖 Generated with Claude Code

https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6

…1299)

`openpulse-kiss` constructs `SharedPtt::new(ptt, DEFAULT_PTT_MAX)` and takes
guards from it, but called `spawn_watchdog` nowhere — so
`force_release_if_expired` had no caller in the crate and the deadline was never
checked. A `SharedPtt` with no watchdog thread is the bare `Box` with extra steps.

The RAII guard already covers an early return and an unwind. What it cannot
reach is a transmit that BLOCKS rather than returns, which is the case the
watchdog exists for — on a real rig, a stuck carrier with nothing to take it back.

Spawned in the constructor as ARDOP does, not in `run_with_listener`: a caller
holding `bridge()` can transmit without running the listener.

Found while writing #1260's Twins section, whose first draft claimed the repeater
was the last hand-rolled keying path. The census that corrected it put KISS in a
third state — guarded but unwatched.

#1299's other half stays open: `openpulse-cli` still keys a bare `PttController`
at three sites.

Implements: REQ-PTT-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
@dc0sk
dc0sk merged commit d8dfedc into main Sep 8, 2026
5 of 6 checks passed
@dc0sk
dc0sk deleted the fix/1299-kiss-ptt-watchdog branch September 8, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant