Conversation
iOS: Add full app shell (Phase 1) — 6-tab UI with mock data
…rfall Wire FT8AFKit engine into the Phase 1 UI shell: - AudioCaptureService: AVAudioEngine mic capture with 12 kHz downsampling - FFTProcessor: Welch-averaged vDSP FFT for waterfall power spectrum - LiveEngine: coordinator running decode + waterfall pipelines concurrently - WaterfallCanvas/SpectrumStrip: replace mock data with live engine output - AppState: add spectrum field, start decode list empty - project.yml: add NSMicrophoneUsageDescription, Accelerate framework Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FFTProcessor: window each segment in place via the buffer base pointer, dropping the redundant whole-buffer Hann pass and the per-segment Array allocation (output is numerically identical). - AudioCaptureService: guard `converter` reads/writes with the existing lock (it was written on the main thread in stop() and read on the RT audio thread); reuse a preallocated output buffer and push via UnsafeBufferPointer to avoid per-callback heap allocations on the RT thread. - SlotAccumulator: add UnsafeBufferPointer<Float> push overload (array push now delegates to it) and mark @unchecked Sendable (fully lock-protected). - LiveEngine: reset waterfall.isLive on stop() via a weak AppState reference; pass @mainactor @sendable update closures into the background loops so they no longer capture/reference AppState directly. - Tests: cover the new pointer-push overload (parity with array push + empty). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
iOS Phase 2: live audio + real-time decode + scrolling waterfall
…istence Wire the remaining engine capabilities into the iOS UI: - Add TxPlayerService: plays FT8 TX waveforms through speaker via AVAudioPlayerNode attached to the shared AVAudioEngine, with 12 kHz → hardware sample rate conversion - Add QsoLogStore: JSON file persistence for QSO records in the app documents directory, loaded on startup and saved after each QSO - Expand LiveEngine: owns QsoEngine + TxPlayerService, feeds decoded messages to the auto-sequencer, schedules TX at slot boundaries, exposes callCQ/stopTx/answerStation/toggleHunt/toggleSlotParity for UI control - Expose AudioCaptureService.audioEngine so TxPlayerService can attach its player node to the same engine instance - Wire TxStrip buttons: HUNT toggles hunt mode, CQ/STOP starts or stops CQ calling, TX1/TX2 toggles slot parity - Wire QsoSheet "Call" button: starts answering the tapped station - Wire ADIF export: generates ADIF via Adif.export() and presents a share sheet - Update logbook to start empty (loaded from disk) instead of mock data - Remove phase footer notes from RadioAudioSettings - Update version string from "1.0.0 (Phase 1)" to "1.0.0" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TxStrip and QsoSheet previously pulled LiveEngine from the SwiftUI environment directly, which caused the button row to not render if the environment value wasn't propagated (e.g. into presented sheets). Switch to a closure-based approach: DecodeScreen and WaterfallScreen (which have LiveEngine in their environment) pass action closures down to TxStrip and QsoSheet. This removes the fragile environment dependency from leaf views and ensures the buttons always render. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…self) @Environment(Type.self) with @observable fatally crashes if the type isn't found in the environment, silently killing the entire screen view (including the TxStrip button row). Fix: store the engine reference on AppState as an optional property (appState.engine) instead of injecting it as a separate environment value. Every view already has AppState in the environment, so appState.engine?.callCQ() etc. is always safe — nil before the engine starts, non-nil once .task wires it. - Remove .environment(engine) from FT8AFApp - Add engine: LiveEngine? to AppState, set in .task at startup - DecodeScreen / WaterfallScreen: use appState.engine? instead of @Environment(LiveEngine.self) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs fixed: 1. TxPlayerService crashed on launch by calling engine.connect() with the outputNode's format which reports 0 Hz on Simulator before hardware is ready. Fix: defer node attachment to first play() call via ensureAttached(), and use mainMixerNode.outputFormat instead of outputNode.outputFormat. 2. TxStrip (HUNT / CALL CQ / TX1 buttons) was rendered inside the screen content VStack but hidden underneath the overlaid custom tab bar ZStack. Fix: move TxStrip out of individual screens and into AppTabView's bottom chrome VStack, above SlotTimerBar and TabBarView. Only shown on Decode and Waterfall tabs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eases The repo holds three FT8 clients sharing one C DSP core, but all CI was Android-only and fired on every push/PR with no path filtering — an iOS-only change ran the full Android NDK build, and iOS/desktop had no CI at all. Split into per-project pipelines that only run when their files (or the shared core at ft8af/app/src/main/cpp/**) change: - android.yml: renamed from build-release.yml, folds in static-analysis.yml as a job, adds a `detect` (dorny/paths-filter) gate and an always-run `android-gate` aggregator. All existing behavior (NDK retry, signing, tag derivation, GitHub Release, Play internal publish, play-publish concurrency) is preserved; job names test/instrumented/build are unchanged. - ios.yml (new): swift test on FT8AFKit + unsigned iOS-Simulator build (xcodegen generate -> xcodebuild) on macos-14; `ios-gate` aggregator. - desktop.yml (new): Tauri compile-check on Windows/macOS/Linux for PRs and non-release pushes; on main / desktop-v* tags builds native bundles on all three OSes and publishes a namespaced desktop-v* / desktop-dev-N release via tauri-action; `desktop-gate` aggregator. Behavior model: PRs run only the affected project's checks; dev pushes build only affected platform(s) as prereleases; main pushes / tags build everything (one release per platform). native-tests.yml stays always-on as the shared-core gate; main-gate.yml and the discord workflows are untouched. Because path-filtered jobs can be skipped, the *-gate jobs always run and are the intended required status checks (a skipped required check otherwise leaves branch protection stuck "pending"). Branch protection must be updated to require android-gate / ios-gate / desktop-gate instead of the inner jobs — documented in the PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, TxStrip expansion, active QSO panel, map/logbook/QsoSheet enrichment, TX glow + celebration Close the UI/UX gap between iOS and Android: - Settings persistence via UserDefaults (survives app relaunch) - Decode screen: row color-coding (CQ=orange, for-me=pink, worked=cyan), time dividers between slots, auto-scroll, compact mode toggle - TxStrip: expand/collapse with volume slider + step buttons, tappable frequency label → FrequencyPickerSheet, CAT status chip - Active QSO panel: floating progress panel with 5-step stage dots, expand/collapse, TX message display - QSO sheet: distance/azimuth via Haversine, QSO sequence visualizer - Logbook: search bar, swipe-to-delete, tap-to-edit sheet, file-based ADIF export (.adi attachment) - Map: status-based marker colors, great circle lines to selected station, operator diamond marker, enhanced popup with SNR + distance - TransmitGlow: animated red/orange border during TX - QsoCelebration: confetti particle burst on QSO completion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…uild Address Copilot review on PR #296: - android-gate / ios-gate / desktop-gate now depend on (and check) their detect/version prerequisites, not just the build/verify/test legs. A failed `detect` (or desktop `version`) skips the downstream jobs, and a skip alone was being read as "path-filtered, nothing to do" — letting a broken workflow report success on the required check. Each gate now fails if any prerequisite result is failure/cancelled; a genuine path-skip (detect succeeded, run=false) still passes. Also fix the iOS verify job, which failed to compile: ft8_lib's `fmtmsg` collided with the POSIX `fmtmsg(long, ...)` from <fmtmsg.h>, visible when the shared C core is built under the macOS/Apple SDK (FT8AFKit swift test). Rename the unused helper to ft8_fmtmsg (no call sites anywhere in the repo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
…roject The ios.yml simulator-build leg failed with "future Xcode project file format (77)": XcodeGen 2.45.4 defaults the generated pbxproj to objectVersion 77 (Xcode 16-only), but the macos-14 runner's Xcode 15.4 — matching the project's pinned xcodeVersion 15.0 — can't read it. Pin objectVersion 56, which Xcode 15 opens and Xcode 16 still accepts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
…en the project" This reverts commit 98aad14.
XcodeGen 2.45.x always emits the pbxproj in Xcode 16's object format
(objectVersion 77); Xcode 15.4 on the macos-14 runner can't open it
("future Xcode project file format (77)"). Pinning objectVersion via
project.yml had no effect (not an honored XcodeGen option), so build the
simulator leg on macos-15 / Xcode 16 instead. The iOS 17 deployment target
still builds under the newer toolchain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
…ion log, POTA enrichment - Logbook stats tab with band donut chart, SNR sparkline, grid heatmap, award progress - Azimuthal equidistant projection with range rings, compass bearings, projected continents - Map dual projection toggle (equirectangular/azimuthal) with zoom controls - Waterfall functional NR/MSG toggles and update counter - Active QSO panel with TX/RX conversation log and auto-scroll - QSO sheet auto-open when CQ answered, auto-close after completion - POTA activate tab with park input, session card, progress bar, elapsed timer - POTA history tab showing past park references - Engine logs TX/RX messages to conversation panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ode filters, waterfall labels - Logbook Awards tab with DXCC Mixed, WAS, WAZ, VUCC, DXCC Challenge progress cards - Multi-format export sheet (ADIF, CSV, Cabrillo, JSON) with date range filtering - Logbook row enrichment: freq display under band pill, SNR color-coding - Toast notification overlay system with auto-dismiss for user feedback - Operator identity card in Settings with edit sheet - About screen with version info, debug log viewer, credits - Blocked callsigns settings with add/remove and decode filter integration - Decode filters: New DXCC shows unworked prefixes, Needed shows unworked CQ stations - CQ POTA filter expanded for VE-/DL- park references - Waterfall message frequency labels drawn on canvas when MSG toggle is on - TX frequency marker line on waterfall Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ubuntu desktop build failed in libudev-sys's build script: "Package libudev was not found in the pkg-config search path." The serialport crate (CAT rig control) needs libudev on Linux, which the apt step wasn't installing. Add libudev-dev alongside the existing Tauri/GTK system deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
The Settings operator card displayed the rig as a single letter (e.g. c) in release builds. It was reading baseRig.javaClass.simpleName, but R8 obfuscates the com.k1af.ft8af.rigs package (no keep rule), so the class name collapses to a meaningless short token. Selecting a different radio or rebooting did not help because every rig class is obfuscated the same way. Switch the card to GeneralVariables.myRigName, the user-selected model name from RigNameList that MainViewModel.connectRig already computes for exactly this reason (PSKReporter + display). Extract the resolution into a testable internal resolveRigDisplayName() helper and cover it with unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ettings, UI polish - Spectrum strip tap/drag to set TX frequency with live Hz readout - TX frequency display chip in waterfall info bar - ADIF import with duplicate detection and toast feedback - Full FT8 message text line in decode rows (non-compact mode) - Advanced settings screen (PTT delay, TX delay, late-start tolerance) - QsoSheet TX message banner (TX NOW/TX NEXT) - Slot timer bar with parity-based colors (even=orange, odd=blue) - FilterChip amber border on selection - Settings persistence for timing fields and advanced link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ectrum width - Auto-sequence toggles: Hunt CQ, Auto-call follow, Early decode, Auto-CQ after QSO - TX safety pickers: Watchdog timeout (off/5-95min), Stop-after limit (off/1-30) - QRZ lookup button in QsoSheet opens station profile in browser - Clear-all button in decode screen top bar - Spectrum width picker (2500-5000 Hz) in Radio & Audio settings - Enabled bands manager with per-band toggles and FT8 frequencies - All new settings persisted via UserDefaults Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Haptic feedback on TxStrip action buttons (medium/light impact) - Success haptic on QSO celebration confetti - Selection haptic on frequency picker band selection - Animated splash screen with compass rose, progress bar, version display - QSO path equirectangular mini-map in QsoSheet showing great circle path between operator and remote station with continent outlines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix rig name showing obfuscated single letter on operator card
With libudev-dev in place the Linux build now gets past serialport and fails next in alsa-sys: "Package alsa was not found in the pkg-config search path." The audio crate (cpal/rodio) needs ALSA on Linux. Add libasound2-dev to the apt step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9
The Linux desktop build hung indefinitely on "Install Linux dependencies" while the macOS/Windows builds finished in minutes. Two fixes: - ubuntu-latest is now Ubuntu 24.04, which dropped libappindicator3-dev. Switch to libayatana-appindicator3-dev (Tauri's current Linux prereq). - Harden apt against the classic CI hangs: DEBIAN_FRONTEND=noninteractive (suppress debconf prompts -y misses) and -o DPkg::Lock::Timeout=600 (wait up to 10 min for the dpkg lock instead of forever, then fail loudly). Also --no-install-recommends to keep the install lean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI: per-project path-filtered workflows + per-platform releases
The map canvas was locked to a 2:1 (standard) / 1:1 (azimuthal) box centered in a stacked Column, leaving dead-space bands above and below it on a tall phone. Make the map fill the whole tab edge-to-edge and move the chrome (grid/count chip, PSK + STD/AZ toggles, zoom controls, selected-station card) into floating overlays on top of it. New pure EquirectViewport owns the canvas-fit + pan math, keeping equirectProject untouched (still normalized [-1,1]). COVER scales the 2:1 world to fill the canvas (overflowing on the long axis, so a portrait phone shows the full latitude range and pans east-west). clampPan floors the allowed pan at 0 on an axis where the map is smaller than the canvas — the load-bearing fix vs. the old size*(scale-1)/2 formula, which assumed the unzoomed map exactly filled the canvas and would otherwise let it be dragged into empty space. drawWorldLand / drawEquirectGrid take the viewport and use inline float math (no per-vertex Offset allocation, since they run every animation frame). Tests: MapViewportTest covers COVER/FIT scaling, projection centering + pan, and clampPan on the overflow and underflow axes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every decoded station was drawn as the same small circle with an always-on text label, which overlapped into an unreadable smear in a busy opening, and the only state cue was colour. Give each station type a distinct glyph, tint it by SNR, and only draw the callsign label when zoomed in or selected. New pure markerStyleFor() decides (shape, fill, radius, showLabel) from a station's state — triangle = CQ, diamond = calling-me, donut = worked, dot = other; PSK receivers stay squares. snrTint() fades weak signals back and leaves strong ones near-opaque (selected markers ignore the fade). Labels gate on isSelected || zoom >= LABEL_ZOOM_THRESHOLD. The canvas is now a thin renderer: drawMarkerShape() switches on the shape; both the standard and azimuthal canvases and the PSK-spot loops go through it. Shape matters because Signal (calling-me) and StatusWorked share a colour — the diamond vs donut is what tells them apart. Tests: MarkerStyleTest (13 cases) covers shape priority (to-me > worked > CQ > dot), the SNR alpha ramp + clamping, and the label declutter rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lines used to appear only for the selected marker. GridTracker-style, draw a persistent line from the operator to the station we're actively calling and from any station whose decode is directed at us, so the active exchange is visible at a glance. New pure buildConnectionLines() takes the decoded stations (via a small StationLine interface that StationMarker implements) and the TX target callsign, and returns the lines to draw: a SOLID line to the TX target when it's been decoded (we can only place a callsign we have a grid for — an unmatched target draws nothing), and a DASHED line from every isToMe station, de-duplicated against the target. MapScreen resolves the target from ft8TransmitSignal.mutableToCallsign (null for CQ/idle). Both canvases draw each line through the same projection as the markers, so lines pan/zoom with the map; the azimuthal canvas skips lines whose endpoint falls off the disc. Tests: ConnectionLinesTest (8 cases) covers matched/unmatched target, dashed calling-me lines, target-also-calling-me de-dup, CQ/null/blank target, and the empty case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The PSK overlay was hardwired to a fixed 1-hour window, the rig's current mode, all bands, and "who heard me". Add a filter sheet so the operator can pick the time window, band, mode, and direction. - New pure PskFilter model: Band enum + freqHzToBand (client-side band cull, applyPskBandFilter), PskMode (incl. CURRENT = follow the rig), PskDirection, and the time-window presets (15m/1h/3h/6h/24h). - PskReporterClient.fetchSpotsForMe gains mode + byReceiver params. mode overrides the queried mode (null = current). byReceiver flips the query from senderCallsign=me (plot the receiver that heard us) to receiverCallsign=me (plot the sender we heard, using senderLocator). The cooldown / 429-503 back-off is unchanged. PskReporterSpot is now neutral (callsign/grid/lat/lon) since the plotted station depends on direction. - MapScreen holds the filter as saveable state, shows a floating filter sheet (chips for each axis), and re-runs the fetch when it changes — force-once so the change isn't swallowed by the 5-min cooldown, with band applied client-side. Direction is scoped to two single-callsign queries (heard-me / I-heard); the unbounded "all stations in area" query is deferred (it trips PSK Reporter's rate-limit on a 5-min poll). "I heard" depends on senderLocator being present in the report. Tests: PskFilterTest (band classification, boundaries, client-side cull, time labels); PskReporterClientTest extended for the mode param in the query and the byReceiver query/plot-sender path; existing cooldown/back-off tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The class doc said the world was 4*baseUniform wide by 2*baseUniform tall and described baseUniform as "px per normalized y half-unit", but the code computes worldPxW = 2*baseUniform and worldPxH = 1*baseUniform. Correct the doc to the actual model (baseUniform = full world height in px; world is 2*baseUniform by baseUniform), addressing the review comment on #299. No behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix CAT chip falsely turning red after FT8 transmit The liveness watchdog suppresses probing during TX, so lastRigResponseMs freezes for the whole over. An FT8 transmit is 12.64s — longer than the 8s stale timeout — so the first tick after TX ends judged the pre-TX timestamp as stale and flipped the chip to ERROR (sticky red) even though the rig was fully responsive. FT4 (~4.48s) stayed under the timeout, so this only manifested on FT8. Re-arm lastRigResponseMs on the TX->RX edge so the quiet window restarts from the end of transmit, giving the probe sent that same tick time to reply before any staleness judgement. Edge detection lives in a pure CatLiveness.shouldRearmAfterTx predicate with unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA * Sample wall clock once per liveness tick Address review feedback: catLivenessTick() read System.currentTimeMillis() twice (re-arm and staleness check). Capture a single nowMs at the top of the tick and reuse it for both, so a clock change mid-tick can't skew the comparison and the tick is easier to reason about. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Release: promote dev → staging (Android Auto status → Play internal)
Switch the car screen from PaneTemplate-only to direct Surface rendering via MapWithContentTemplate (car-app API level 5+). The 1 Hz tick now redraws the Canvas directly instead of calling invalidate(), which eliminates the host dimming/flashing on every countdown update. The surface draws a world map (land outlines from WorldOutlines) with decoded-station markers and an operator dot, covered by a 75% black overlay, then status text (headline, TX message, sequence step, slot countdown, band/mode, POTA activation line) and a TX indicator dot. Hosts below API level 5 fall back to the original PaneTemplate path (minCarApiLevel stays at 1 for backward compat). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The POI category added in the previous commit triggers Android Auto's driving restrictions, blocking the entire screen while in motion. The IOT category has no such restriction. MapWithContentTemplate (surface-based rendering) requires POI or Navigation category, so it can't be used with IOT while driving. Remove the surface rendering code and fix dimming differently: the tick now invalidates only at slot boundaries (every 15s for FT8) instead of every second. LiveData observers still trigger immediate refreshes for real state changes. POTA activation line is kept — added as a 4th PaneTemplate row. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Render a 480x240 equirectangular world map as the PaneTemplate hero image. Shows the operator's position (orange dot) and, during an active QSO, a line to the partner station (cyan dot). The bitmap is built with Canvas from WorldOutlines land data — no map templates, no POI category, works while driving. The image is cached by (opGrid, partnerCall, partnerGrid) so onGetTemplate() skips re-rendering when nothing moved. The partner's grid is looked up from the current decoded message list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ners Replaces the static QSO map image with a Surface-rendered, full-bleed map on the Android Auto NavigationTemplate (CarMapSurfaceRenderer), drawn directly each 1 Hz tick so countdown updates never trigger a template refresh (no dimming). Map: - Zoom-to-fit framing of the operator and QSO partner (CarMapProjection): world view when idle, zoomed in around the two stations when in a QSO, so the connection is actually legible. Clamped [1, 12x] with padding. - World land + national borders (new res/raw/world_countries.json) + US state borders, layered under markers; US state abbreviations labelled once zoomed in. Status: - Two full-width status banners (CarPanelLayout) — live QSO state (headline + slot) on top, band + POTA on the bottom — flush against the car's system bars (heights read from framework dimens; safe-area fallback), leaving the map centre clear. - The QSO partner's location rides next to the slot timer: US state (via point-in-polygon over us_states.json, UsStateLocator) or DXCC country (from the callsign database), e.g. "RX slot 7 s - Massachusetts" / "- Reunion Island". Testing: - DEBUG-ONLY DebugInject broadcast receiver (src/debug) + app-automotive host so the car map can be exercised on an Automotive OS emulator without a phone/DHU. - Unit tests for the projection, panel/banner geometry, GeoJSON label + region parsing, and point-in-polygon lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes eight review findings on the Android Auto surface map path: - Guard checkIsCQ() against a null callsignTo: it dereferences callsignTo unconditionally, so a decode with a null To field would crash the 1 Hz surface tick. buildStationMarkers now gates the call. - Dedupe station markers by the normalized getCallsignFrom() (strips the < > a hashed decode carries) instead of the raw callsignFrom field, so a hashed decode no longer produces a duplicate marker. - Match the current QSO partner on getCallsignFrom() too, so a hashed partner decode still renders its line/location. - Cache the empty fallback when a geo asset fails to load, so a missing/corrupt asset does not re-throw the parse on every frame. - Localize the POTA line: buildCarPotaLine now returns a CarStringSpec resolved against the (previously unused) @string/car_pota_line. - Correct stale KDoc/comments that still referenced MapWithContentTemplate and host-rendered status cards; the screen uses NavigationTemplate and draws the status banners on the surface itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Android Auto: live surface QSO map (zoom, borders, status banners, partner location)
Compose UI 1.8 (BOM 2025.04.01) deprecates the imperative AutofillNode / AutofillType / LocalAutofill(Tree) API in favor of the semantics-based ContentType APIs. Rewrite Modifier.autofill to tag the field's semantics with its ContentType and receive filled values via onAutofillText, dropping the hand-rolled node-registration / focus / bounds bookkeeping. The role -> ContentType mapping is extracted to credentialContentType (was credentialAutofillTypes) and its unit test is updated accordingly. Fixes #453. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
getDefaultAdapter() is deprecated on API 18+; the adapter should be obtained from the BluetoothManager system service instead. Add a small testable helper bluetoothAdapter(context) that does getSystemService(BLUETOOTH_SERVICE).adapter and preserves the old null contract, and route both call sites (autoConnect in ComposeMainActivity, the Bluetooth picker in ConnectionDialogs) through it. Fixes #454. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
android.os.AsyncTask is deprecated. Convert the GetCallsignMapGrid AsyncTask into a plain, directly-testable loadCallsignMapGrid(db) method plus an Executor-backed loadCallsignMapGridAsync(db), and route the Kotlin (ComposeMainActivity) and legacy Java (MainActivity) call sites through the async variant so no caller references the deprecated .execute() path. The remaining AsyncTask subclasses in DatabaseOpr are a separate, larger sweep and are intentionally left untouched here. Fixes #455. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Copilot review: the helper's KDoc implied getDefaultAdapter() was deprecated on API 18+. API 18 is when BluetoothManager.getAdapter() became available; the getDefaultAdapter() deprecation landed in Android 12 (API 31). Correct the doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
- DatabaseOpr.loadCallsignMapGrid: cache the callsign/grid column indices once before the cursor loop instead of resolving them on every row. - LoadCallsignMapGridTest: reset the process-global GeneralVariables map in @Before/@after for isolation, and move DB creation/close into setUp/tearDown so the handle is always closed even when an assertion fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Migrate off deprecated Compose Autofill API in AutofillModifier.kt
Replace deprecated BluetoothAdapter.getDefaultAdapter()
Replace deprecated AsyncTask in ComposeMainActivity.kt
FT8 is a standalone ADIF mode, but FT4 and FT2 are submodes of MFSK. Both ADIF writers copied the stored mode name straight into the MODE field, so an FT2 QSO exported as a bare <MODE:3>FT2 — which pota.app rejects as an invalid mode (its guidance is MODE=MFSK, SUBMODE=FT2). Add AdifFormat.mfskSubmode() as the single source of truth for which modes ADIF treats as MFSK submodes, and route both export paths through it: - PotaAdifExporter (POTA upload + share-sheet): new adifMode() emits MODE=MFSK + SUBMODE=FT2/FT4 for those modes, otherwise MODE verbatim. - ShareLogs (general logbook ADIF export): same mapping, fixing the identical latent bug for QRZ/LoTW/etc. exports. FT8 output is unchanged. Covers the new paths with unit tests for the classifier, adifMode, and an end-to-end FT2 QSO through buildActivationAdif. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
The MFSK-submode fix only covered PotaAdifExporter and ShareLogs, but two other live ADIF writers still emitted a bare <mode>FT2/FT4: - ThirdPartyService.QSLRecordToADIF — the payload for QRZ and Cloudlog uploads, so FT2/FT4 QSOs uploaded there hit the same invalid-mode rejection the PR set out to fix. - DatabaseOpr.downQSLTable — the logbook ADIF text export. Route both through AdifFormat.mfskSubmode() so FT2/FT4 export as MODE=MFSK + SUBMODE, matching the other export paths; FT8 stays a bare standalone MODE. Make QSLRecordToADIF package-private so its payload can be unit-tested directly, and add Robolectric tests pinning the ADIF output of both paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export FT4/FT2 QSOs as MFSK submodes in ADIF
- QsoStatusScreen.onStop now clears the AppManager SurfaceCallback it registered in onStart, so the host stops delivering surface events to a stopped screen and doesn't keep the callback/renderer referenced while another screen is on top. onStart re-registers on return. - PSK "who heard me" fetch loop forces the first fetch after (re)start so the client-wide cooldown (from a recent fetch on another screen) can't skip the initial car fetch and leave the overlay's PSK rings empty; and clears WhoHeardMeCache when the operator callsign is blank so stale rings don't linger. - Fix AndroidManifest comment: the surface map uses NavigationTemplate, not MapWithContentTemplate (matches QsoStatusScreen's KDoc and buildMapTemplate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Release: promote dev → staging
main and staging each performed the ft8cn->ft8af package/module rename independently, producing add/add conflicts across ~50 files, plus a few content conflicts where staging carries newer code (e.g. Kotlin 2.0.21 vs main's 1.9.22). staging is the forward-most branch and a content superset of main (no file outside the old ft8cn/ path exists in main but not in staging), so every conflict is resolved in favor of staging. The resulting tree is byte-identical to origin/staging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
|
Resolved the merge conflicts in 711a56a by merging `origin/main` into `staging`. Root cause: `main` and `staging` each performed the `ft8cn`→`ft8af` package/module rename independently, producing add/add conflicts across ~50 files, plus a handful of content conflicts where `staging` carries the newer code (e.g. Kotlin 2.0.21 vs main's 1.9.22). Verified that no file outside the old `ft8cn/` path exists in `main` but not in `staging` — i.e. `staging` is a content superset of `main` — so every conflict was resolved in favor of `staging`. The resulting merge tree is byte-identical to `origin/staging` ( No review threads to address (Copilot skipped inline review as the PR exceeds its 300-file limit; the Codecov comment is informational). |
Promote
staging→main.This is a large release — 296 commits across ~70 feature/fix PRs (#275–#464) since the last
maincut (#304). The headline is a fully open-source FT8/FT4 DSP core, a new native iOS app, an Android Auto experience, a redesigned map, and the Kotlin 2 / 16 KB page-size platform bump — plus a deep stack of decode, USB/audio, and rig-control fixes.🔊 Decode & DSP core
libft8cn.sowith a from-sourcelibft8af.sobuilt from the vendoredft8_lib— the entire FT8/FT4 signal path is now open and buildable from a fresh checkout (Replace prebuilt libft8cn.so with from-source libft8af.so #275, Replace prebuilt libft8cn.so and rename packages to ft8af #277).DT > +0.86 ssignals that the early-decode pass misses (Late full-slot decode: recover DT > +0.86s signals when earlyDecode is on #388), and feed those deep/late-pass decodes into the QSO auto-sequencer (Feed deep/late-pass decodes to the QSO auto-sequencer #406).<CALL>hashed decodes back to full calls (Resolve hashed compound calls in RX (combines #395 + #396, fix #392) #396), generalized across the EU-VHF and contest decoders (Generalize compound-call hash recovery to all contest decoders #415), with host tests for those decoders (Add host tests for the EU_VHF and CONTESTING native decoders #414).📻 Rig control, audio & USB
UNPROCESSED/VOICE_RECOGNITIONaudio source for FT8 RX (Fix #298: use UNPROCESSED/VOICE_RECOGNITION audio source for FT8 RX #303, Suggestion for FT8 audio routing: Disable internal mic when external transceiver is connected #298).MicRecorderreinitialize races — startup NPE + nativereleaseBufferabort (Fix MicRecorder reinitialize races (startup NPE + native releaseBuffer abort) #423) — and orderHamRecorderwatchdog reads against capture writes with a buffer lock (Order HamRecorder watchdog reads against capture writes with a buffer lock #413).🗺️ Map redesign
🚗 Android Auto
🍎 iOS app (new native port)
A ground-up Swift/SwiftUI app reusing only the C
ft8_libcore:📝 QSO workflow & UI
QsoSyncGatecheck-and-mark (Make QsoSyncGate check-and-mark atomic to close the TOCTOU race #409), serializedafterDecodestate (Serialize afterDecode shared state behind a synchronized DecodeCycleState #410), fixConcurrentModificationExceptionfrom live lists published to Compose (Fix ConcurrentModificationException from live lists published to Compose #424), and fix aRejectedExecutionExceptioncrash on ViewModel teardown (Fix RejectedExecutionException crash on ViewModel teardown #432).📇 Logging & integrations
🏗️ Platform, build & deprecations
mapping.txtuploaded to Play (Enable R8 shrink+obfuscate on release; upload mapping.txt to Play #292).ft8afidentity (Rename packages and module directory to new identity #276).BluetoothAdapter.getDefaultAdapter()(Replace deprecated BluetoothAdapter.getDefaultAdapter() #459, Replace deprecated BluetoothAdapter.getDefaultAdapter() #454), andAsyncTask(Replace deprecated AsyncTask in ComposeMainActivity.kt #460, Replace deprecated AsyncTask in ComposeMainActivity.kt #455).⚙️ CI, coverage & docs
LocationSubscriberbase for grid + GPS-clock updaters (Refactor: extract shared LocationSubscriber base for GridLocationUpdater and GpsClockUpdater #385); build-file hygiene pass (Build hygiene: truthful native-lib comments, single dataBinding flag, layout.buildDirectory (#371) #384).CLAUDE.md(strip usernames/paths/serials) (Genericize CLAUDE.md: strip usernames, personal paths, and device serials #422).Rolls up the intermediate
dev → stagingpromotions (#374, #391, #397, #452, #464).🤖 Generated with Claude Code