Skip to content

Animate RX packet path on live map#191

Open
fromalexx wants to merge 1133 commits into
chrissnell:mainfrom
fromalexx:beacon-map-path-animation
Open

Animate RX packet path on live map#191
fromalexx wants to merge 1133 commits into
chrissnell:mainfrom
fromalexx:beacon-map-path-animation

Conversation

@fromalexx

Copy link
Copy Markdown
Contributor

Summary

  • Adds an "Animate path" toggle to the live map's layer card. When on, every newly received RX packet draws a green dotted line that sweeps from the source station, through any H-bit digipeaters whose positions are known, to "My Position", then holds and fades. Geometry mirrors the existing hover-path overlay; the dotted dasharray is what distinguishes the live animation from the static hover.
  • Sweep duration scales with on-screen pixel length (~700 px/sec, clamped 350-2500 ms), so close packets snap quickly and distant ones still get a full sweep instead of a fixed wall-clock crawl.
  • No backend changes -- pkg/webapi/stations.go already exposes path, path_positions, and via per fix. The polling data store gains a subscribeNewFix(cb) hook that fires once per genuinely new RX fix during delta polling (initial snapshot is skipped). TX (own beacons) and IS (iGated) packets are intentionally not animated.

Disclosure

This was written by AI. Feel free to adjust or reject!

chrissnell added 30 commits May 17, 2026 11:56
…-tooltip

fix: channel-picker tooltip no longer shows phantom 'no audio input device' on KISS-only channels
Imports the design spec from docs/android-phase-4b-spec branch into
this feature branch and adds a thin task-decomposition plan that maps
each spec section to a discrete subagent task.
Add ptt_android_consts.rs (Rust) and PttMethodConsts.kt (Kotlin) as the
single canonical PTT method integer mapping for the Android actuator path.
Both declare the five PTT_METHOD_* constants (0–4) derived directly from
Appendix B of the phase-4b design spec.

The proto/platform.proto PttMethod enum already carries the correct values
and is unchanged. T13 will add the cross-language sync integration test that
reads all three sources and asserts they agree.
Move Logs out of the Settings group and into the main nav, placed
directly below Actions, with a file-text icon matching the inline-SVG
pattern used by the other main entries.
feat(web): promote Logs to a top-level nav tab
Adds graywolf-modem/src/android/upcall.rs with two pub(crate) helpers:
  jni_ptt_set(method, keyed)   — invoke UsbPttCallback.pttSet(IZ)Z
  jni_tx_push_samples(buf)     — invoke AudioTxCallback.pushSamples([SI)I

Android path: attaches the calling thread to the JVM (re-entrant attach
is cheap once the thread is attached), uses cached GlobalRef + JMethodID
stored at install time, and handles missing-callback / JNI-failure without
panicking — returns Err with a descriptive message.

Host stub path (android-test-stub feature): replaces JNI with
Mutex<Option<Box<dyn Fn>>> closures. Tests install a mock, call the helper,
and assert on observed args + return value. 7 unit tests all pass.

Two JNI export functions land in android/mod.rs:
  Java_com_nw5w_graywolf_jni_ModemBridge_installPttCallback
  Java_com_nw5w_graywolf_jni_ModemBridge_installAudioTxCallback
These are the contract T5 (ModemBridge.kt) must match.

The android-test-stub feature is declared in Cargo.toml [features] and
pulled into lib.rs via #[path] so the stub unit tests compile on the host
without an Android target or live JVM.

PTT method constants from T1 (ptt_android_consts.rs, commit c858504) are
available for T3 to import; this task does not re-import them as the JNI
bridge only passes an opaque i32 through to Kotlin.
Code review of b923b37 surfaced four issues; this commit fixes all of them.

1. Serial test isolation: add serial_test = "3" dev-dependency and annotate
   every stub-mode test with #[serial] to eliminate intermittent failures
   from Cargo's parallel runner stomping on shared PTT_MOCK/AUDIO_TX_MOCK
   global state.

2. Unified re-export path: add pub(crate) use aliases in lib.rs so T3/T4
   callers can write `use crate::{jni_ptt_set, jni_tx_push_samples}` once
   inside their own cfg block, regardless of whether the android target or
   android-test-stub host path is active. Also re-export the test hooks
   (install_ptt_mock, install_audio_tx_mock, clear_mocks) from crate:: so
   T3/T4 unit tests have a single import path.

3. Redundant inner cfg in android/mod.rs: the file is already gated
   #![cfg(target_os = "android")] at the top; the inner
   #[cfg(any(target_os = "android", feature = "android-test-stub"))]
   on pub mod upcall was unreachable for the feature branch. Simplified
   to a bare pub mod upcall;

4. Unused JValue import: removed JValue from the jni::objects import in
   android_impl; it was never used (call_method_unchecked takes raw
   jni::sys::jvalue unions, not JValue). Would have warned on Android builds.

Also resolved Minor (5): keyed_jni now spelled as
`let keyed_jni: jni::sys::jboolean = keyed as u8;`
making the type explicit and dropping the opaque JNI_TRUE.min(1) expression.
…pcall

Builds on T1 (ptt_android_consts) and T2 (JNI upcall infra, crate::jni_ptt_set).

New `graywolf-modem/src/tx/ptt_android.rs` (cfg-gated to android or
android-test-stub) implements the PttDriver trait by delegating to
crate::jni_ptt_set(method, keyed). The `method` i32 flows through the
new PttMethod::Android variant in build_driver; it is carried in
ConfigurePtt.gpio_pin (field reuse, no proto change needed for T3 --
gpio_pin is unused in the Android code path for its original CM108-pin
purpose, and adding a proto field would require coordinated changes
across the Go and Kotlin stacks).

build_driver validates the int against the four Appendix-B constants
(CP2102N_RTS=1, CM108_HID=2, AIOC_CDC_DTR=3, VOX=4) and returns Err
for PTT_METHOD_UNKNOWN (0) or any other value.

14 new tests cover: key/unkey callback dispatch, error propagation with
context (method int in message), all four valid method ints, all four
build_driver success paths, unknown-int rejection, and UNKNOWN(0)
rejection. Tests are serialized via serial_test to share global mock
state safely with T2's tests.
…se arm

- Update stale T3/T4 pending comments in upcall.rs: T3 now calls jni_ptt_set,
  only T4 (jni_tx_push_samples) remains pending
- Narrow #[allow(dead_code)] and fix comment on pub(crate) re-export to reflect
  actual call status
- Add assertion in parse_recognizes_known_method_strings_and_returns_none_for_unknown
  test to guarantee desktop build coverage of PttMethod::Android parse arm

Test results:
- cargo test --lib (no features): 157 passed + 1 pre-existing failure ✓
- cargo test --features android-test-stub --lib: 173 passed + 1 pre-existing failure ✓
…call

Adds `tx_emit_samples(buf: &[i16]) -> Result<usize, String>` in a new
`android/audio_tx.rs` module. The function drives the Rust modem TX
governor → Kotlin AudioTxPump path introduced in T2's upcall infra
(jni_tx_push_samples).

Return convention mirrors AudioTrack.write: a non-negative int is the
bytes-or-samples written (short writes returned as Ok(n) for the caller
to detect); a negative int — ERROR=-1, ERROR_BAD_VALUE=-2,
ERROR_INVALID_OPERATION=-3, ERROR_DEAD_OBJECT=-6 — becomes Err with the
code and input length included for log attribution. Empty buffers
short-circuit before the JNI call.

audio_tx.rs is gated any(target_os="android", feature="android-test-stub")
so the 5 stub-mode unit tests run on the host, satisfying the same
pattern T3 used for ptt_android.rs. audio.rs (RX path) retains its
android-only gate since it depends on config_state and other JNI
infrastructure. lib.rs wires audio_tx.rs in directly for the host stub
path (same technique as upcall.rs), which eliminates the dead-code
warning on jni_tx_push_samples that T2 left behind pending T4.

The Kotlin side (AudioTxPump, pushSamples signature) is T6; the JNI
exports that install the callback are T5.
…installers

Add UsbPttCallback and AudioTxCallback interfaces to ModemBridge.kt, along with
installPttCallback and installAudioTxCallback external functions. These match
the JNI exports already shipped in T2 commit 173a37b (graywolf-modem/src/android/mod.rs).

Interface signatures:
- UsbPttCallback.pttSet(method: Int, keyed: Boolean): Boolean (JNI: IZ)Z)
- AudioTxCallback.pushSamples(samples: ShortArray, count: Int): Int (JNI: ([SI)I)

Both interfaces are top-level types in the same file so they can be implemented
by UsbPttAdapter (T7) and AudioTxPump (T6) respectively.

AudioTxCallback.pushSamples returns bytes written per AudioTrack.write convention.
Refs spec §3.2 (AudioTxPump.pushSamples) and §3.4 (installPttCallback snippet).
Implements AudioTxCallback (T5) so the Rust modem TX governor can push PCM
samples through an AudioTrack(MODE_STREAM) to the first available USB audio
output. Spec §3.2 + §8 Q2.

Key points:
- Mirrors AudioPump (RX) shape: 22050 Hz, PCM16, CHANNEL_OUT_MONO, 4× min buffer
- Auto-routes to TYPE_USB_DEVICE output in start(); falls back to system default
  with a WARN log if no USB audio dongle is present
- AudioManager.AudioDeviceCallback registered in start() / unregistered in stop()
  handles hot-swap: rebinds setPreferredDevice on USB add; falls back to null
  (system default) on removal of the currently routed device
- start() and stop() are idempotent; pushSamples() returns -1 when stopped
- trackFactory seam (nullable, default null) keeps production code clean while
  allowing hermetic JVM unit tests via Mockito without Robolectric

Tests (AudioTxPumpTest, src/test/):
- No USB output → system default path, setPreferredDevice never called
- USB output present → setPreferredDevice called with TYPE_USB_DEVICE info
- pushSamples after stop() returns -1 (no crash)
- start() idempotent → single track created on double-call
…GS type

Manifest changes per spec §3.6 + Appendix A:

- Uncomment FOREGROUND_SERVICE_CONNECTED_DEVICE permission (phase 4b, not 5)
- Add USB_DEVICE_ATTACHED intent-filter to MainActivity
- Point intent-filter to curated device_filter.xml (15-entry VID/PID + CDC-ACM catch-all)
- Expand Service foregroundServiceType bitmap to include mediaPlayback + connectedDevice
  (gating at runtime per device permission is T10's job, not T8)

All XML validates with xmllint. Ready for T9 WebAppInterface + T10 lifecycle.
…tCallback

Extend object UsbPttAdapter to implement UsbPttCallback (T5/ModemBridge.kt)
and add the pttSet(method, keyed) dispatcher.

Dispatch table:
  PTT_METHOD_CP2102N_RTS (1) → setRts(keyed)
  PTT_METHOD_AIOC_CDC_DTR (3) → setAiocRts(keyed)
  PTT_METHOD_CM108_HID (2)   → setHidGpio(keyed)
  PTT_METHOD_VOX (4)         → true (audio path drives PTT, no wire)
  unknown                    → Log.w + false

AIOC path delegates to setAiocRts, NOT setDtr directly. setAiocRts
holds RTS=0 in both key/unkey states and drives DTR=keyed, matching
AIOC firmware >=1.2.0 (feedback_aioc_ptt_cdc_acm_dtr).

Method int constants come from PttMethodConsts (T1). JNI installation
of UsbPttAdapter as the callback is T10's responsibility.

Adds UsbPttAdapterTest covering all five dispatch cases: CP2102N,
AIOC, CM108 (with HID report byte verification), VOX, and unknown.
Implements spec §3.6. WebAppInterface gains two new @JavascriptInterface
methods:

  listUsbDevices()               — delegates to UsbPttAdapter.enumerateForJs();
                                   returns a JSON array of all attached USB
                                   devices with vid, pid, name, role, and
                                   permission_granted fields.

  requestUsbPermission(vid, pid, callbackId)
                                 — delegates to UsbPttAdapter.requestPermissionFor();
                                   fires window.__usbResult(callbackId, granted)
                                   via WebView.post / evaluateJavascript once
                                   the system dialog resolves (or immediately if
                                   no matching device is attached).

UsbPttAdapter gains two new public methods (enumerateForJs, requestPermissionFor)
plus a ConcurrentHashMap (pendingPermissionCallbacks) that the existing
permissionReceiver now consumes after tryOpen so each one-shot callback fires
exactly once on grant or deny.

MainActivity wired to pass the WebView instance to the updated
WebAppInterface constructor.

Tests: WebAppInterfaceTest updated + UsbPttAdapterEnumerateForJsTest added
(Mockito 5 inline; reflection seam for singleton usbManager injection).

T10 will own GraywolfService lifecycle wiring.
…Service lifecycle

Connects the pieces built in T5 (ModemBridge JNI), T6 (AudioTxPump),
T7 (UsbPttAdapter.pttSet), and T9 (WebAppInterface USB bridge) into
GraywolfService's onCreate/onDestroy so they actually run on Service boot.

Per spec §3.1:
- Install JNI callbacks (installPttCallback, installAudioTxCallback) after
  modemVersion() triggers loadLibrary, but before bootModem(), so any TX/PTT
  signal the modem fires on boot finds a registered callback.
- Start AudioTxPump, call UsbPttAdapter.init + enumerate after startForeground
  returns, before bootModem().
- Reverse on onDestroy: audioTxPump.stop + UsbPttAdapter.closeAll between
  audioPump.stop and platformServer.stop.

Per spec §3.6 (CONNECTED_DEVICE FGS runtime gate):
- MEDIA_PLAYBACK added unconditionally (no runtime perm required).
- CONNECTED_DEVICE added only when at least one USB device already has OS
  permission at Service start time; probed via UsbManager directly since
  UsbPttAdapter is not yet init'd at that point. Absence is logged; the bit
  can only be picked up on the next Service start (Android prohibits
  expanding FGS type while running).

MainActivity.kt and GraywolfApp.kt required no edits: no prior UsbPttAdapter
calls existed in those files to remove or adjust.
Implements T11 of the Android Phase 4b plan.

## What was built

- proto/graywolf.proto: add ManualPtt message (field tag 21) to the
  IpcMessage oneof. Regenerated pkg/ipcproto/graywolf.pb.go via
  `make proto` (protoc-gen-go v1.36.11 / protoc v7.34.1).

- graywolf-modem/src/modem/tx_worker.rs: add TxMessage::ManualKey{channel,
  keyed} variant; worker loop arm looks up the driver and calls key/unkey,
  logging every attempt. Add TxWorker::manual_key() public method.

- graywolf-modem/src/modem/mod.rs: dispatch Payload::ManualPtt(mp) in
  handle_ipc() by calling tx_worker.manual_key(mp.channel, mp.keyed).

- pkg/modembridge/ptt_watchdog.go: per-channel timer that auto-unkeys
  after the configured timeout (10s in production). onKey resets the
  timer on each heartbeat; onUnkey cancels it; timerFire logs and calls
  the unkey closure.

- pkg/modembridge/bridge.go: wire pttWatchdog into Bridge.New; add
  ManualPtt (raw IPC, watchdog-free, for tests) and ManualPttWithWatchdog
  (REST-facing); add InjectSendFnForTest helper.

- pkg/webapi/channels.go: register POST /api/channels/{id}/ptt; handler
  calls bridge.ManualPttWithWatchdog, returns 204 on success.

## Tests

- pkg/modembridge/ptt_watchdog_test.go: four table tests covering timeout
  fire, heartbeat reset, explicit unkey cancel, and per-channel isolation.
- pkg/webapi/channels_ptt_test.go: REST tests for happy path (IPC message
  captured via InjectSendFnForTest), nil-bridge 503, bad-id 400.
- graywolf-modem/src/modem/tx_worker.rs: manual_key_routes_to_registered_driver
  registers a MockPtt, sends ManualKey(true) then ManualKey(false), asserts
  Key+Unkey in the mock log.

## Spec deviation (documented)

Spec §3.5 says "Rust modem has a watchdog timeout (~10s)." The watchdog
is placed in Go instead:
- Single-language ownership: one process owns the timer; no cross-language
  timer coordination, no prost clock API needed in the modem crate.
- Simpler restart story: the Go watchdog survives a modem child crash+restart
  without any state handshake.
- Functionally equivalent: a stuck PTT is always resolved within 10s
  regardless of where the timer lives.

Spec refs: §3.5 (request flow), §3.7 (heartbeat semantics), §6.3 (Go tests).
…atus

Implements T12 per spec §3.7, wired to the T11 REST endpoint and T9 USB
JS bridge.

Changes:
- web/src/lib/api.js: add postChannelPtt(channelId, keyed) helper that
  POSTs to /api/channels/{id}/ptt; add matching mock branch.
- web/src/routes/Channels.svelte: Android-gated section (Platform.kind
  === 'android') inside the channel edit/create modal:
    * PTT method dropdown: CP2102N RTS / CDC-ACM DTR / CM108 HID / VOX,
      bound to androidPttMethod (int 1-4 per Appendix B constants).
    * Test PTT press-and-hold button: pointerdown keys, pointerup/cancel/
      leave unkeys; 2-second heartbeat interval keeps Go watchdog alive.
      Interval is cleared on unmount via onDestroy.
    * USB hardware status row: polls listUsbDevices() every 2 s, matches
      device by role string, shows "Grant access" button when permission
      not yet granted; requestUsbPermission dispatches through
      window.__usbResult/window.__usbCallbacks bridge.
    * Audio routing row: hard-coded "USB audio (auto)" pending a future
      status-endpoint ticket.
  TX timing fields (TX Delay, Tail, Slot, Persist, Full Duplex) were
  already unconditionally visible; no gate was added or removed.
- web/src/routes/Channels.android.ptt.test.js: 9 node:test unit tests
  covering postChannelPtt wire shape, press-and-hold heartbeat dispatch
  (mock timers), and USB device role matching.

Data-shape decision: androidPttMethod is stored via POST /api/ptt with
method='android' and gpio_pin=<method_int 1-4>. The PttConfig.gpio_pin
column carries the method int as a carrier field (different semantics
from the desktop CM108 use, but zero new schema columns required). The
Go modembridge reads this to configure UsbPttAdapter dispatch.
…ount

Math.random() can return 0 (slice(2) of '0' is ''), which the T9
Kotlin validator rejects — prefix with 'cb' to guarantee a non-empty
alphanumeric id.

Also remove globalThis.__usbResult / __usbCallbacks on component
unmount so a late grant callback can't fire into a dead component.
Spec Appendix B calls out the drift hazard: PTT method enum must remain in sync
across three sources (proto definition, Rust constants, Kotlin constants). This
integration test parses each source file and asserts that all three produce the
same int→name mapping at runtime.

The Go test runner (via proto reflection) is the canonical baseline; Rust and
Kotlin sources are parsed with regexes. Test failure lists exactly which language
disagrees on which value, making drift diagnosis trivial.
…roidTxSink

Final code review (C2 finding) caught that process_job always routed TX
through soundcard::spawn_output → AudioSink::submit. On Android, cpal
cannot reach the AudioTrack instance that Kotlin's AudioTxPump holds
open and setPreferredDevice-routes to the USB OTG dongle. Result:
PTT keys correctly, radio transmits silence, beacon never decodes.

This commit is the connector the rest of phase 4b assumed existed but
never grew:

- Add AndroidTxSink in android/audio_tx.rs implementing crate::modem::TxSink.
  submit() calls tx_emit_samples (blocking JNI upcall into AudioTrack.write
  WRITE_BLOCKING), then accumulates drained_samples so drive_tx_cycle's
  drain loop exits immediately on first check.
- Promote TxSink from pub(super) to pub(crate); re-export from crate::modem
  under the android cfg gate so the android module can implement the trait
  without exposing the private tx_worker module path.
- cfg-split process_job: Android arm constructs AndroidTxSink on the stack
  and drives drive_tx_cycle directly; non-Android arm is the existing cpal
  lazy-sink path, unchanged. sinks/pending_devices maps are untouched on
  Android; a submit error logs but does not attempt sink rebuild (Kotlin
  manages AudioTrack lifetime independently).
- Two new serial tests under android-test-stub: submit forwards buffer +
  accumulates drained_samples; submit error leaves drained_samples at zero.
… validation, lock scope, watchdog reset

C1: enumerateForJs now emits vid/pid as decimal Ints (for the JS bridge call) plus
vid_hex/pid_hex hex strings (for display). SPA requestGrant updated to read
usbDevice.vid / usbDevice.pid. JS test fixtures migrated from vendor_id/product_id
to the new schema. Kotlin test updated to assert decimal getInt("vid") and hex
getString("vid_hex"). Fixes silent undefined args passed to requestUsbPermission().

I1: PttRequest.Validate now rejects method=android unless gpio_pin is in 1..4
(spec Appendix B: 1=CP2102N_RTS, 2=CM108_HID, 3=AIOC_CDC_DTR, 4=VOX). New
pkg/webapi/dto/ptt_test.go covers all four valid pins plus the 0 and 99 rejection
cases. Deferred: non-Android client setting method=android requires platform-state
plumbing out of scope for this sweep.

I2: jni_ptt_set and jni_tx_push_samples in upcall.rs now clone the GlobalRef and
copy the JMethodID (both are inexpensive — GlobalRef::clone, JMethodID is Copy)
under the Mutex lock and drop the lock guard before attaching the JVM thread and
issuing the call. Prevents a potential deadlock if a future re-entrant upcall path
is added through the Kotlin Log/install-replacement channel.

I3: pttWatchdog gains cancelAll() which stops and removes all active timers.
Bridge.supervise calls cancelAll() at the top of each supervision cycle alongside
the existing dispatcher/status resets. Prevents a stale timer from a mid-press
modem crash firing an auto-unkey into a freshly-spawned child with no keyed state.
New TestPttWatchdog_CancelAll verifies no unkey fires after cancelAll.
…arget

Cross-compile via cargo ndk surfaced two issues that only manifest on
target_os=android (not on the host stub):

- pub(super) install_ptt / install_audio_tx can't be re-exported as
  pub(crate) at the crate root; widen the originals to pub(crate).
- jni 0.21 get_method_id takes a JClass, not a JObject; resolve the
  class via get_object_class first.

Also cfg-gate unused imports (Entry, soundcard::self) and the unused
sinks/pending_devices process_job parameters on android, since the
android TxSink arm doesn't touch them.
Android 14+ requires the matching FOREGROUND_SERVICE_<TYPE> permission
to be declared for every type used in startForeground. T10 added the
MEDIA_PLAYBACK bit to the FGS bitmap but the perm itself wasn't on
the manifest; live-launch surfaced SecurityException.
chrissnell and others added 26 commits May 22, 2026 16:34
The promote-to-closed job hardcoded track: graywolf-beta, which is not
a real Play track -- Play track IDs are fixed (internal, alpha, beta,
production) and the promote failed with 'Track graywolf-beta could not
be found'. A private 15-person beta is closed testing = the alpha track
(beta is open testing).

Replace the hardcode with a workflow_dispatch 'track' choice input
(alpha|beta|internal, default alpha) so the operator picks the target
at run time:
  gh workflow run android.yml --field version=0.13.8 --field track=alpha
…ck-input

ci(android): make promote track a dispatch input (fix bad track id)
The r0adkll promote re-uploaded the AAB, which Play rejects once the
versionCode exists ('version code 130800 has already been used'). To
move an existing build between tracks you PROMOTE it, not re-upload.

fastlane integration:
- Gemfile + fastlane/{Appfile,Fastfile}. Two lanes:
  - promote: supply with track_promote_to (no re-upload), params
    version_code/from/to. Auth via SUPPLY_JSON_KEY (file, local) or
    SUPPLY_JSON_KEY_DATA (raw JSON, CI from the GH secret).
  - upload_listing: supply images-only (icon, feature graphic, phone +
    tablet screenshots) from fastlane/metadata.
- Workflow promote-to-closed: drop the AAB download + r0adkll; set up
  Ruby, derive versionCode from X.Y.Z, run fastlane promote to the
  chosen track.
- run.sh stages generated assets into fastlane/metadata/.../images/
  (icon, featureGraphic, phoneScreenshots, sevenInchScreenshots);
  PNGs gitignored, dir structure kept via .gitkeep.
- Makefile: android-promote (VC/TO/JSON) + android-upload-listing (JSON).

Closes the 'screenshots should upload too' ask: make android-screenshots
generates+stages, make android-upload-listing pushes them to Play.
TestConcurrentChainsShareThrottleAndStayBounded asserted both ptt+kiss
components survive in the eviction ring after a 2000-write concurrent
burst -- but evict() keeps the globally-newest RingSize rows by id, so
whichever chain's goroutines committed last can own the entire tail. A
legal scheduling outcome, not starvation; the assertion flaked on slow
CI runners (saw 'distinct components = 1, want 2').

Fix: after the burst's bound check (which is the real shared-throttle
proof), write a short deterministic interleaved tail (20x chainA+chainB,
< RingSize) so both components are guaranteed in the survivor window,
then assert routing. Passes 6/6 locally.
ci(android): fastlane for track promotion + listing upload
…-test

test(logbuffer): de-flake concurrent-chains component assertion
The fastlane-based promote failed with 'Could not locate Gemfile' --
the promote-to-closed job had no Checkout step (the old r0adkll promote
didn't need the repo; the fastlane version needs the Gemfile +
fastlane/Fastfile). Add actions/checkout before the Ruby setup so
bundler-cache and 'bundle exec fastlane' can find them.
ci(android): checkout repo in promote job (fastlane needs Gemfile)
Swiping the app from recents tears down the backend, which releases the
radio's USB interfaces; they re-enumerate ~2s later and -- because
MainActivity declares a USB_DEVICE_ATTACHED intent-filter -- Android
auto-relaunches the app, reviving the station the operator just dismissed.

onTaskRemoved now records a deliberate-stop timestamp in graywolf-prefs;
MainActivity.onCreate finish()es immediately when launched by
USB_DEVICE_ATTACHED within a 15s window of that stop. Launcher taps and
genuine re-plugs after the window are unaffected. Guard onDestroy's
webView teardown for the early-finish path (lateinit not yet set).
…shutdown

Android: guaranteed clean shutdown on swipe and hard-kill
* fix(android): retry platformsvc bind instead of crashing on contention

* fix(android): stop duplicate service cleanly on platformsvc contention; expose socket name

* feat(android): gate launch on previous instance exiting, with a waiting screen

* docs(wiki): record single-instance serialized-startup invariant

* fix(android): run blocking audio/USB init off the main thread in onCreate

AudioTrack construction + setPreferredDevice and USB device opens are
synchronous HAL/binder calls that block for seconds when a USB audio
dongle is wedged, ANR'ing GraywolfService.onCreate within 5s. Move
txPump.start() and UsbPttAdapter.enumerate() to a worker thread; keep the
cheap UsbPttAdapter.init() on the main thread. onDestroy joins the worker
(bounded) before tearing the same resources down.

* docs(wiki): record off-main-thread audio/USB startup invariant
… is eligible (chrissnell#183)

The PTT page rendered detected hardware as clickable cards even when no
channel could accept a PttConfig — zero channels, only KISS-TNC channels
(input_device_id == null), or every modem-backed channel already
configured. Clicking only fired a transient error toast, so the card
appeared dead.

Add pttDetectionBlockedReason() to classify why configuration is blocked
(no-modem-channel / all-configured) and use it to disable the device
cards and show an inline notice pointing the operator to the Channels
page or to edit an existing config.
)

The Enable toggle was a bound form field that only persisted on Save,
so flipping it and switching tabs discarded the change (onMount reloads
enabled=false). Wire the toggle's onCheckedChange to persist
immediately, saving only the enabled bit merged onto the last-saved
snapshot so unsaved edits to sibling fields are not committed.

Preserve existing guards: the station-callsign-missing handler still
blocks the flip before onCheckedChange fires, and iGate refuses an
auto-enable onto a non-TX-capable channel (mirroring the disabled-Save
behavior). Revert the toggle and toast on API error.
…s permanently deaf) (chrissnell#185)

* feat(android): add RestartPolicy with capped-backoff degraded mode

* feat(android): supervisor retries in degraded mode instead of halting

* feat(android): notify operator when modem enters degraded restart mode

* feat(modem): add stop-aware IpcServer::accept_interruptible

* fix(modem): re-accept Go client on IPC send error instead of going deaf

* feat(modem): log when decoded frames are dropped with no IPC client

* docs(wiki): record Android RX IPC resilience + no-halt supervisor invariant

* fix(modem): resolve clippy type_complexity in ipc server

Factor the repeated (IpcHandle, Receiver<IpcInbound>, JoinHandle) tuple
into an AcceptedClient type alias used by accept, finish_accept, and
accept_interruptible. Fixes the -D warnings clippy failure.
* proto: add USB serial kind, baud, and available-USB-serial-devices messages

* platformsvc: bump wire schema to 3 for USB serial

* platformsvc: rename bt-handle multiplex machinery to transport-neutral serial*

* platformsvc: extract shared serial stream; slim btserial.go to BT surface

* platformsvc: add UsbSerialOpen and AvailableUsbSerialDevices

* platformsvc: USB serial round-trip, EOF, typed-error, denied-ack tests

* platformsvc: finish bt->serial comment rename in btserial_test.go

* configstore: add usbserial KISS interface type

* dto: accept usbserial KISS type (device + baud required)

* app: route vid:pid KISS device through UsbSerialOpen on Android

* app: start usbserial KISS interfaces via the serial supervisor

* webapi: GET /api/kiss/available-usb-serial-devices (501 off-Android)

* app: wire USB serial device source into webapi (Android)

* webapi+web: regenerate swagger and add kissUsb.availableDevices client

* android: add UsbDeviceArbiter for USB device ownership claims

* android: gate UsbPttAdapter.tryOpen on UsbDeviceArbiter; expose evictDevice

* android: revert local-only test workaround in UsbPttAdapterEnumerateForJsTest

This file does not compile on the local Mac Kotlin/AGP toolchain (HashMap
type inference) but CI's toolchain tolerates the original; keep the PR
scoped to the USB-serial feature and out of pre-existing test debt.

* android: add UsbSerialFacade (mik3y prober) with arbiter claim + PTT evict

* android: add UsbSerialAdapter (USB serial relay over platform UDS)

* android: UsbSerialAdapter tests (enumerate, open errors, detach)

* android: PlatformServer routes serial frames by kind (BT/USB)

* android: wire UsbSerialAdapter + USB detach receiver into GraywolfService

* web: add USB Serial KISS interface type, picker, baud, and grant CTA

* docs(handbook): USB serial KISS TNC operator page

* wiki: document USB serial KISS path, files, and PTT/KISS ownership invariant

* docs: link kiss-usb-serial from sibling handbook pages; fix UsbDeviceArbiter code-map note

* web: regenerate OpenAPI TypeScript client for USB serial endpoint
…acon) (chrissnell#184)

* commit

* commit

* Redesign map context menu: coord header + compact actions

Show the clicked coordinate once in a muted header instead of repeating
it inside every label, then group the actions: primary 'Add fixed beacon
here' (amber pin icon) above a divider, then a copy group with short
labels and a copy icon. Copy grid shows the locator as a right-aligned
hint. Narrows the menu and gives it clear visual hierarchy.

Also fix the hover background, which used a white-alpha fallback that was
nearly invisible on the light theme surface; tint toward --color-text via
color-mix so hover feedback shows in both themes.

The component gains header, per-item icon/hint/primary, and {divider}
support while staying presentational; the parent still owns the model.

* Seed context-menu clamp state with 0, not the prop

Initializing the adjusted x/y $state from the x/y props was a
non-reactive capture (svelte-check state_referenced_locally). The
positioning $effect already overwrites both before paint, so seed
with 0 to silence the warning; behavior is unchanged.
The delete confirm input lacked autocapitalize/spellcheck overrides, so
mobile keyboards capitalized the typed name and the exact-match gate
never passed -- the red Delete button stayed disabled and deletion did
nothing on Android. Replace the typed-name gate with a single confirm
button on both the unreferenced and referenced (cascade) paths; the
referrer impact list remains the confirmation surface for cascades.
@chrissnell

Copy link
Copy Markdown
Owner

I need to give this a shot to see if it works well. Thanks for the contribution!

@chrissnell chrissnell force-pushed the main branch 3 times, most recently from bdc4d52 to c6b4bbb Compare June 13, 2026 23:27
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.

3 participants