Skip to content

feat: APRS local RF integration — display TNC data alongside APRS-IS#845

Merged
accius merged 5 commits intoaccius:Stagingfrom
ceotjoe:feature/aprs-local-rf-integration
Mar 27, 2026
Merged

feat: APRS local RF integration — display TNC data alongside APRS-IS#845
accius merged 5 commits intoaccius:Stagingfrom
ceotjoe:feature/aprs-local-rf-integration

Conversation

@ceotjoe
Copy link
Copy Markdown
Collaborator

@ceotjoe ceotjoe commented Mar 26, 2026

Summary

This PR bridges the gap between local RF APRS data (packets decoded by a TNC connected to rig-bridge) and the existing APRS-IS internet feed, letting operators display both sources side-by-side — or choose one exclusively — without any duplicate handling or source confusion.


What changed

rig-bridge — aprs-tnc plugin

  • Each decoded RF packet is now forwarded directly to the local OHC server's POST /api/aprs/local (fire-and-forget, no retry).
  • Two new config keys in rig-bridge/core/config.js:
    • aprs.localForward (default true) — set to false when using the cloud-relay plugin to avoid injecting the same packet twice on the cloud instance.
    • aprs.ohcUrl (default http://localhost:8080) — the base URL of the OHC server to forward to.
  • CONFIG_VERSION bumped to 7; existing configs without these keys are patched in automatically.

Server — server/routes/aprs.js

  • GET /api/aprs/stations: every station object now carries source: 'local-tnc' | null so the frontend can tell RF-heard stations from internet ones. Added tncActive: true when at least one local-tnc station is cached — the panel activates from this flag even when APRS_ENABLED=false.
  • RF-wins dedup: if a station already has source: 'local-tnc', a later APRS-IS update for the same SSID will not overwrite that tag. The RF origin is preserved for the lifetime of the station.
  • POST /api/aprs/local: fixed station keying to use ssid (consistent with the APRS-IS ingestion path); added the same RF-wins dedup logic here.
  • GET /api/aprs/tnc-status: new endpoint that proxies the rig-bridge TNC status (host/port read from CONFIG.rigControl). The browser never needs to know the rig-bridge port directly.
  • Station cleanup: removed the if (!APRS_ENABLED) return guard from the 60-second expiry interval. RF-injected stations are now correctly aged out even when APRS_ENABLED is not set.

Frontend — src/hooks/useAPRS.js

  • New state: sourceFilter ('all' | 'internet' | 'rf'), tncConnected, hasRFStations.
  • TNC status polled every 10 s via /api/aprs/tnc-status.
  • filteredStations now applies the source filter first, then the group/watchlist filter.
  • aprsEnabled is now true when either data.enabled (APRS-IS) or data.tncActive (local TNC) is set, so the panel opens in RF-only setups without APRS_ENABLED=true in .env.

Frontend — src/components/APRSPanel.jsx

  • Source selector row: three toggle buttons — All, 🌐 Internet, 📡 Local RF. The RF button is greyed out when no RF stations are in cache; shows a pulsing green dot when the TNC is actively connected.
  • RF badge: every station row with source: 'local-tnc' shows a small green RF chip next to the callsign.
  • Disabled-state message updated to explain both paths: adding APRS_ENABLED=true for internet spots, and enabling the aprs-tnc plugin in rig-bridge for local RF — with no .env change required.
  • Full i18n: APRSPanel was previously entirely un-localised. All 33 user-visible strings are now wired through react-i18next (t()), with translations provided for all 15 supported locales (see below).

Frontend — src/components/WorldMap.jsx

  • RF stations render as green triangles (var(--color-aprs-rf) / #4ade80) on the map, distinct from internet stations (cyan) and watched stations (amber).
  • Station popup shows an RF label for locally-heard stations.

Styles — src/styles/themes.css

  • New CSS variable --color-aprs-rf added to all four themes (dark, light, retro, legacy).

i18n — src/lang/

  • en.json: 33 new aprsPanel.* keys added (alphabetically, consistent with existing flat-key convention).
  • All 15 other locales (ca, de, es, fr, it, ja, ka, ko, ms, nl, pt, ru, sl, th, zh): full translations provided for all 33 keys.
  • Technical proper nouns (APRS, APRS-IS, TNC, RF, EmComm, rig-bridge, .env) kept in English across all locales, which is standard in the ham radio community. Locale-appropriate terms used for UI chrome (EIN/AUS in de, ВКЛ/ВЫКЛ in ru, 开/关 in zh, etc.).

Files changed

File What changed
rig-bridge/core/config.js aprs.localForward, aprs.ohcUrl defaults; CONFIG_VERSION → 7
rig-bridge/plugins/aprs-tnc.js Fire-and-forget POST to OHC on each decoded packet
server/routes/aprs.js source + tncActive in stations response; RF-wins dedup (both paths); cleanup guard removed; ssid keying fix; /api/aprs/tnc-status proxy
src/hooks/useAPRS.js sourceFilter, tncConnected, hasRFStations; TNC status poll; aprsEnabled from tncActive
src/components/APRSPanel.jsx Source selector; RF badge; updated disabled message; full react-i18next wiring
src/components/WorldMap.jsx Green triangle markers + RF popup label for local-TNC stations
src/styles/themes.css --color-aprs-rf variable across all four themes
src/lang/en.json 33 new aprsPanel.* keys
src/lang/{ca,de,es,fr,it,ja,ka,ko,ms,nl,pt,ru,sl,th,zh}.json All 33 keys translated

Behaviour matrix

Setup APRS-IS tab Internet source Local RF source
APRS_ENABLED=true, no rig-bridge ✅ Panel opens ✅ Stations shown ⬜ Button greyed out
APRS_ENABLED=false, rig-bridge TNC active ✅ Panel opens ⬜ No internet data ✅ RF stations shown with badge
Both active ✅ Panel opens ✅ All sources in "All" view ✅ RF badge + green map markers
Station heard on both RF and internet RF origin preserved (local-tnc tag wins)

Test plan

  • APRS_ENABLED=true, rig-bridge not running: panel opens, shows internet stations, "Local RF" button is greyed out, no errors in console
  • APRS_ENABLED=false, rig-bridge TNC active with Direwolf: panel opens (no "APRS Not Enabled" screen), RF packets appear with green RF badge, map shows green triangles
  • Source filter "Local RF": only local-tnc stations shown; "Internet": only non-RF stations; "All": both combined
  • Station heard on both RF and internet retains source: 'local-tnc' (RF-wins dedup) — verify via GET /api/aprs/stations
  • TNC live green dot on RF button appears when rig-bridge TNC is connected; disappears when disconnected
  • aprs.localForward: false in rig-bridge config: TNC plugin no longer POSTs to OHC; cloud-relay path unaffected
  • Existing APRS-IS users with APRS_ENABLED=true and no rig-bridge: no regression in panel behaviour, watchlist groups still work
  • Language switcher: change to DE, FR, JA — all APRSPanel strings render in the selected language with no aprsPanel.* key showing raw
  • npm run build passes without new errors or warnings

🤖 Generated with Claude Code

- rig-bridge/aprs-tnc: forward received packets directly to the local
  OHC server's /api/aprs/local (fire-and-forget, configurable via
  aprs.localForward and aprs.ohcUrl). Disable when using cloud-relay
  to avoid duplicate injection on the cloud server.
- rig-bridge/config: add localForward and ohcUrl defaults to aprs
  section; bump CONFIG_VERSION to 7.
- server/aprs: expose `source` field on every station in the
  GET /api/aprs/stations response; apply RF-wins dedup so internet
  updates don't strip the `local-tnc` tag from stations heard over RF;
  fix /api/aprs/local to key by ssid (consistent with APRS-IS path);
  add GET /api/aprs/tnc-status proxy endpoint.
- useAPRS: add sourceFilter ('all'|'internet'|'rf') state that feeds
  into filteredStations; poll /api/aprs/tnc-status every 10s; expose
  tncConnected and hasRFStations.
- APRSPanel: add Source selector row (All / Internet / Local RF) with
  TNC live indicator dot; show green "RF" badge on locally-heard stations.
- WorldMap: render local-tnc stations in green (#4ade80) to distinguish
  them from internet stations (cyan) and watched stations (amber); add
  RF label in popup.
- themes.css: add --color-aprs-rf variable across all four themes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe marked this pull request as draft March 26, 2026 17:18
@ceotjoe
Copy link
Copy Markdown
Collaborator Author

ceotjoe commented Mar 26, 2026

@accius please leave that for a while I'm testing this still. It is just for reference to let you know I'm working on it. But you can already let me know if the implementation would suit you also.

ceotjoe and others added 3 commits March 26, 2026 23:42
Three bugs when APRS_ENABLED is not set in .env:

1. Station cleanup interval skipped entirely, so RF packets injected via
   /api/aprs/local would pile up in memory and never be evicted.
   Fix: remove the APRS_ENABLED guard — cleanup always runs.

2. GET /api/aprs/stations returned enabled:false even when local TNC
   stations were present. The panel then showed "APRS Not Enabled" and
   blocked all data.
   Fix: add tncActive flag (true when any local-tnc station is cached);
   useAPRS sets aprsEnabled=true when either enabled or tncActive is true.

3. APRSPanel "disabled" message only mentioned APRS_ENABLED=true, making
   the rig-bridge TNC path invisible to users.
   Fix: updated message explains both paths (APRS-IS and local RF).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
APRSPanel was entirely missing react-i18next wiring. This commit adds
useTranslation and replaces every hardcoded English string with a t()
call, covering both the strings that pre-existed and the new ones added
in the local RF integration:

New keys (aprsPanel.*) added to en.json:
- disabled.{title,internetBefore,internetAfter,rfBefore,rfAfter,
  filterBefore,filterAfter}
- source.{label,all,internet,rf,tncConnected,noRfData}
- groups.{title,description,newGroupPlaceholder,createButton,
  callsignPlaceholder,selectGroup,addButton,deleteButton,noCallsigns}
- groupTab.{all,watchlist}
- groupsButton, groupsButtonTitle, mapOn, mapOff
- loading, noStations, noStationsFiltered, quickSearch, rfBadgeTitle

Other locale files will fall back to en.json until translators fill them in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the 33 aprsPanel.* keys introduced in the previous commit to every
non-English locale file: ca, de, es, fr, it, ja, ka, ko, ms, nl, pt, ru,
sl, th, zh.

Translated strings cover the full panel: disabled-state instructions
(both APRS-IS and local RF paths), source selector (All / Internet /
Local RF), watchlist group manager, map toggle, station list states,
and the RF badge tooltip.

Technical terms kept in English across all locales: APRS, APRS-IS, TNC,
RF, EmComm, rig-bridge, .env. Locale-appropriate terms used for UI
chrome (e.g. EIN/AUS in de, ВКЛ/ВЫКЛ in ru, 開/關 in zh).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe marked this pull request as ready for review March 26, 2026 23:12
@ceotjoe
Copy link
Copy Markdown
Collaborator Author

ceotjoe commented Mar 26, 2026

@accius from my perspective ready to review now.

@accius
Copy link
Copy Markdown
Owner

accius commented Mar 27, 2026

Thanks for the solid work on this Jörg — merging as-is since the core functionality is sound, but here are improvements to address in follow-ups:

Should fix soon

  1. --color-aprs-rf CSS variable is defined but never used — The PR adds the variable to all four themes (with appropriate per-theme values like #16a34a for light, #00ff00 for retro) but the actual code hardcodes #4ade80 everywhere (WorldMap.jsx, APRSPanel.jsx badge). The theme-specific colors will never apply as written. Swap the hardcoded values to var(--color-aprs-rf).

  2. TNC status polling scalability — Each browser tab polls /api/aprs/tnc-status every 10s, which proxies to rig-bridge. At scale this could hammer rig-bridge. Two options: cache the rig-bridge response server-side for ~15 seconds, or piggyback the TNC status onto the existing /api/aprs/stations response that's already being polled.

  3. Verify ctx.fetch and rigHost URL construction — In the /api/aprs/tnc-status proxy, ctx.fetch needs to be wired up (won't crash if missing since the catch swallows it, but TNC status will always show disconnected). Also, if CONFIG.rigControl.host doesn't include a protocol prefix, the URL template `${rigHost}:${rigPort}/...` will be malformed.

Nice to have

  1. Dead code in POST /api/aprs/local — The RF-wins dedup check is a no-op since station.source is already set to 'local-tnc' two lines above. Only the APRS-IS path needs that guard.

  2. forwardToLocal batching — Currently sends one HTTP request per decoded packet. On a busy frequency, consider collecting packets for 1-2 seconds and flushing as a batch.

  3. POST /api/aprs/local has no authentication — Pre-existing issue, but now actively used. Worth noting for internet-facing deployments.

@accius accius merged commit 30440fe into accius:Staging Mar 27, 2026
1 check passed
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.

2 participants