Skip to content

rig-bridge: Direct SSE mode, plugin data pipeline & APRS improvements#852

Merged
accius merged 10 commits intoaccius:Stagingfrom
ceotjoe:rig-bridge-fixes
Mar 30, 2026
Merged

rig-bridge: Direct SSE mode, plugin data pipeline & APRS improvements#852
accius merged 10 commits intoaccius:Stagingfrom
ceotjoe:rig-bridge-fixes

Conversation

@ceotjoe
Copy link
Copy Markdown
Contributor

@ceotjoe ceotjoe commented Mar 28, 2026

What does this PR do?

Summary

This PR has two themes: eliminating unnecessary server traffic when rig-bridge is connected directly (local/LAN mode), and a set of APRS panel and map UX improvements.


1 — Disconnect Cloud Relay button

The Settings panel previously had no way to disconnect an active cloud relay session — the only option was to manually clear the session ID and save. A Disconnect Cloud Relay button is now shown when a session is active. It clears the session immediately without requiring a separate Save click.


2 — Route all rig-bridge plugin data over SSE (local mode)

Previously, when cloud relay was off, plugin data (WSJT-X decodes, digital mode status, QSOs, APRS RF packets) still made round-trips through the OHC server — either via HTTP polling or HTTP POST. The rig-bridge connection was only used for rig frequency/PTT.

What changed:

  • rig-bridge.js — central pluginBus listener fans out all plugin events (decode, status, qso, aprs) to SSE clients via broadcast() as type: 'plugin' messages
  • server.js — on SSE connect, sends a type: 'plugin-init' message with the running plugin list and a 100-entry decode ring-buffer (so the browser gets recent history immediately)
  • RigContext.jsx — dispatches a window CustomEvent rig-plugin-data for every plugin SSE message; hooks subscribe independently
  • useWSJTX.js — adds an isLocalMode ref that permanently stops the adaptive HTTP polling loop the moment the first SSE decode arrives
  • useDigitalModes.js — populates MSHV/JTDX/JS8Call status directly from SSE status events (the OHC server has no status routes for these in local mode)
  • rig-bridge/lib/aprs-parser.js — new standalone APRS position parser so rig-bridge can produce fully-parsed station objects (lat/lon/symbol/speed/course/altitude) before broadcasting, without a server round-trip
  • aprs-tnc.js — calls the parser before emitting on the bus; tags packets with stationSource: 'local-tnc'
  • useAPRS.js — maintains a separate rfStations Map fed purely by SSE; merges RF + internet stations (RF wins on duplicate callsign); 60-minute aging matches server APRS_MAX_AGE_MINUTES; server POST to /api/aprs/local removed entirely

Bandwidth comparison — cloud relay ON vs OFF

Traffic type Cloud relay ON Cloud relay OFF (this PR)
Rig freq/PTT SSE push from rig-bridge via relay server SSE push from rig-bridge direct
WSJT-X decodes Relay → OHC server → browser HTTP poll (2 s) SSE push from rig-bridge direct, polling eliminated
MSHV/JTDX/JS8Call status Relay → OHC server → browser HTTP poll SSE push from rig-bridge direct, polling eliminated
APRS RF packets Relay → OHC server → browser HTTP poll (15 s) SSE push from rig-bridge direct, no server POST, no poll
APRS-IS internet stations OHC server poll (15 s) OHC server poll (15 s) — unchanged
QSOs Relay → OHC server SSE push from rig-bridge direct

In local mode the OHC server receives zero real-time rig-bridge traffic. The only remaining server contact is the 15 s APRS-IS internet station poll — which is independent of rig-bridge. For a self-hosted install running entirely on a home network this cuts ongoing rig-bridge-related HTTP requests from ~3 req/s to 0 req/s.


3 — APRS panel improvements

  • Distance column — each station row shows distance from DE location, respecting the user's metric/imperial unit preference
  • Hover tooltip — hovering a station row shows a popup with the full untruncated comment, coordinates, distance, age, speed/course, altitude and APRS symbol code
  • Age fix (NaNh) — RF stations from SSE carry timestamp (epoch ms) but no pre-computed age (minutes). Age is now derived from timestamp when age is absent
  • APRS-not-activated flicker fix — the 15 s server poll was resetting aprsEnabled to false (server has APRS_ENABLED=false) even after SSE confirmed the TNC was running. A tncDetectedViaSse ref now prevents the server poll from overriding that state
  • QRZ SSID strip — clicking a callsign like W1ABC-9 now opens QRZ for W1ABC; the -N SSID suffix is stripped in extractBaseCall() before the URL is constructed

4 — APRS symbol sprites on map

All APRS map markers previously rendered as a generic coloured triangle regardless of the symbol field in the packet.

  • Added standard APRS symbol sprite sheets (hessu/aprs-symbols, 24 px) to public/ — primary table, alternate table, and overlay table
  • New src/utils/aprs-symbols.jsgetAprsSymbolIcon() maps the two-char APRS symbol string to the correct sprite cell using CSS background-position; supports primary /, alternate \, and alphanumeric overlay table chars; returns null for unknown symbols so the triangle fallback is used
  • WorldMap.jsx — uses the symbol sprite icon when available; colour ring (amber = watched, green = RF, cyan = internet) preserved via box-shadow on the icon wrapper; watched stations 20 px, others 16 px

5 — APRS clicks no longer move DX location

Clicking an APRS station on the map or in the panel was calling handleSpotClick, which sets the DX target location. APRS is a monitoring tool — it should not affect the DX target. Map clicks now only open the Leaflet popup; panel row clicks no longer call onSpotClick at all.


Files changed

Area Files
rig-bridge core rig-bridge.js, core/server.js, core/state.js, core/config.js
rig-bridge plugins plugins/aprs-tnc.js
rig-bridge lib lib/aprs-parser.js (new)
Frontend hooks useWSJTX.js, useAPRS.js, useDigitalModes.js
Frontend context RigContext.jsx
Components SettingsPanel.jsx, APRSPanel.jsx, WorldMap.jsx, CallsignLink.jsx
App wiring DockableApp.jsx
Assets public/aprs-symbols-24-{0,1,2}.png (new)
Utilities src/utils/aprs-symbols.js (new)

Test plan

  • Connect rig-bridge locally (no cloud relay) — verify WSJT-X decodes appear without any /api/wsjt polling in browser devtools Network tab
  • Confirm APRS RF packets appear in panel and on map with correct symbol icons
  • Confirm age display shows minutes/hours (not NaNh) for RF stations
  • Verify APRS panel does not flicker to "not activated" between server poll intervals
  • Click an APRS station on map — popup opens, DX location unchanged
  • Click a station with SSID (e.g. OE5XYZ-9) — QRZ opens for base callsign
  • Enable cloud relay — verify all data still flows normally through relay path
  • Disconnect cloud relay via new Settings button — verify session clears immediately

ceotjoe and others added 6 commits March 28, 2026 16:45
When a cloudRelaySession is active the Cloud Relay card now shows the
session status and a Disconnect button. Clicking it clears the session
and immediately saves config (no manual Save required), switching
RigContext back to the direct SSE connection path.

Previously the only way to leave cloud-relay mode was to manually clear
the hidden session field; without that the direct connection path in
RigContext was never reached even after disabling the relay in rig-bridge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of rig-bridge firing fire-and-forget HTTP POSTs to a hardcoded
localhost:8080 (wrong port, always failed silently), all plugin data now
flows over the same SSE /stream connection already used for freq/mode/ptt.

rig-bridge changes:
- state.js: add 100-entry decode ring-buffer (addToDecodeRingBuffer /
  getDecodeRingBuffer) so connecting browsers see recent decodes immediately
- rig-bridge.js: subscribe to pluginBus after connectIntegrations() and
  broadcast typed plugin messages for decode/status/qso/aprs events
- server.js: send plugin-init after the rig init message on /stream connect,
  carrying the ring-buffer replay and list of running integration plugins
- aprs-tnc.js: remove forwardToLocal() call from handleKissData — SSE
  broadcast replaces it; fix default ohcUrl port from 8080 → 3000
- config.js: fix default aprs.ohcUrl port 8080 → 3000

Frontend changes:
- RigContext.jsx: dispatch window CustomEvent 'rig-plugin-data' for
  type:'plugin' and type:'plugin-init' messages from local SSE
- useAPRS.js: listen for rig-plugin-data aprs events, POST raw packet to
  /api/aprs/local (same-origin, always reachable), then refresh stations;
  seed tncConnected from plugin-init
- useWSJTX.js: listen for decode/status events; seed decode list from
  plugin-init ring-buffer replay; populate clients map from status events
- useDigitalModes.js: listen for status events to update plugin statuses
  (OHC server has no /api/mshv|jtdx|js8call/status routes, so HTTP
  polling was always failing silently in local mode)

Cloud relay path is entirely unchanged — window events only fire when
the browser is connected directly to rig-bridge's /stream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useWSJTX: add isLocalMode ref that stops the adaptive polling loop the
  moment the first rig-bridge SSE event arrives; add qso event handling;
  avoid setting hasDataFlowing from SSE path (prevents spurious 2 s burst)
- rig-bridge/lib/aprs-parser.js: new standalone APRS position parser
  (!, =, /, @, ; data types) extracted so rig-bridge can produce fully-
  parsed station objects without a server round-trip
- aprs-tnc: parse packet before bus emit; spread lat/lon/symbol/comment
  into the SSE payload alongside raw AX.25 fields; tag with stationSource
- useAPRS: maintain separate rfStations Map fed purely by SSE events;
  merge RF + internet stations (RF wins on duplicate callsign); add
  60-minute aging cleanup matching server APRS_MAX_AGE_MINUTES; remove
  server POST call entirely for local-TNC path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- APRSPanel: add distance-to-DE column using calculateDistance/formatDistance,
  respecting user metric/imperial setting; pass deLocation + units from
  DockableApp
- APRSPanel: hover tooltip showing full comment, coordinates, distance, age,
  speed/course, altitude and symbol — fixed-position, pointer-events:none
- APRSPanel: fix age display for RF stations (NaNh) by computing age from
  timestamp when server-provided age field is absent
- APRSPanel: prevent server poll from resetting aprsEnabled to false when
  aprs-tnc was detected via SSE (tncDetectedViaSse ref)
- CallsignLink: strip APRS SSID suffix (-0…-15) before QRZ lookup so
  W1ABC-9 opens QRZ as W1ABC

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add standard APRS symbol sprite sheets (hessu/aprs-symbols, 24 px) to
  public/ — primary table (0), alternate table (1), overlay table (2)
- New src/utils/aprs-symbols.js: getAprsSymbolIcon() maps the two-char
  APRS symbol field to a Leaflet divIcon using CSS background-position
  into the sprite sheet; supports primary (/), alternate (\), and
  alphanumeric overlay table chars; falls back to null for unknown symbols
- WorldMap.jsx: use symbol sprite icon when available, keeping the CSS
  triangle as fallback; colour ring (amber/green/cyan) preserved via
  box-shadow on the icon wrapper; watched stations rendered at 20 px,
  others at 16 px
- WorldMap.jsx: fix age display for RF stations in popup (NaN) by falling
  back to timestamp when station.age is absent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking an APRS station on the map or in the panel should not set
the DX target — APRS is a monitoring tool, not a contact/spotting source.

- WorldMap: remove onSpotClick call from APRS marker click; popup still
  opens via Leaflet bindPopup as before
- DockableApp: stop passing onSpotClick to APRSPanel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe changed the title Rig bridge fixes rig-bridge: Direct SSE mode, plugin data pipeline & APRS improvements Mar 28, 2026
ceotjoe and others added 3 commits March 28, 2026 19:36
In local/LAN mode the browser already receives all decodes via the SSE
/stream — the HTTP batch POST to /api/wsjtx/relay on the OHC server is
redundant and generates unnecessary traffic.

- config.js: add wsjtxRelay.relayToServer (default false) — false means
  SSE-only, true means also POST batches to OHC server
- wsjtx-relay.js: compute willRelay flag at connect() time; gate message
  queue push, scheduleBatch, heartbeat and health-check intervals behind
  willRelay; SSE bus.emit paths are always active regardless of mode;
  getStatus() now surfaces relayToServer and serverUrl only when active
- state.js: export getSseClientCount() so other modules can read the
  number of live SSE connections
- server.js: import getSseClientCount; add GET /api/status returning
  { sseClients, uptime } — lightweight endpoint for UI health display
- SettingsPanel: add wsjtxRelayToServer toggle that immediately PATCHes
  rig-bridge config; handleConfigureWsjtxRelay now also sets
  relayToServer:true when pushing cloud-relay credentials; add SSE
  client count badge with Refresh button so users can verify local
  connections before disabling server relay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The delivery mode toggle belongs on the source side (rig-bridge) not in
the OHC settings panel.

rig-bridge UI (server.js):
- Replace the flat wsjtx opts section with a two-option delivery mode
  selector: "SSE only" (local/LAN) and "Relay to OHC server" (cloud)
- SSE mode shows only UDP port and multicast options — server fields
  (URL, relay key, session ID, batch interval) are hidden in a separate
  wsjtxServerOpts div that only appears in relay mode
- populateIntegrations() reads relayToServer flag to set the radio
- toggleWsjtxMode() shows/hides the server-specific block
- saveIntegrations() writes relayToServer from the selected radio

OHC SettingsPanel:
- Remove wsjtxRelayToServer state, handleToggleRelayToServer handler,
  fetchSseClientCount handler, toggle UI block and SSE count badge —
  all moved to rig-bridge UI
- relayToServer:true is still sent when the user clicks Configure
  Cloud Relay, since that action explicitly enables server delivery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nt and lib files

- Rewrite WSJT-X Relay section: document SSE-only (default) vs relay-to-server
  delivery modes, three setup options, updated config reference with relayToServer
- Add GET /api/status endpoint to API reference table ({sseClients, uptime})
- Add lib/aprs-parser.js and lib/wsjtx-protocol.js to project structure tree
- Update pluginBus event docs to include status, qso and aprs events
- Add SSE ring-buffer replay note to Digital Mode Plugins section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@accius accius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work Jörg — the SSE plugin pipeline is well-architected and the bandwidth reduction for local/LAN users is significant. A few things to address:

Needs fix

1. forwardToLocal removed from aprs-tnc.js — verify cloud relay APRS still works

The forwardToLocal([aprsPacket]) call was removed. In SSE-only mode this is fine (browser gets APRS via SSE directly). But for cloud relay users, that POST to /api/aprs/local was how RF packets got into the remote server's station cache. Now only bus.emit('aprs', ...) remains. Does the cloud-relay plugin pick up aprs bus events and forward them to the remote server? If not, cloud relay users will lose RF APRS data on the map.

2. isLocalMode is permanently latched — no recovery on SSE disconnect

In useWSJTX.js, isLocalMode.current = true is set on the first SSE plugin message and never reset. If rig-bridge drops mid-session, HTTP polling never resumes — the user sees stale data until they refresh. Consider resetting on SSE disconnect or adding a staleness timeout that falls back to polling.

3. ohcUrl default changed from 8080 to 3000

-    ohcUrl: 'http://localhost:8080',
+    ohcUrl: 'http://localhost:3000',

This changes the default for all new installs and will be patched into existing configs via the CONFIG_VERSION migration. Is 3000 the intended production default? If the dev server runs on 3000 but deployed/Docker installs use 8080, this could break self-hosted users on upgrade.

Worth addressing

4. Tooltip can overflow viewport

The hover tooltip positions at clientX + 14, clientY + 14 but doesn't clamp to viewport bounds. Stations near the right or bottom edge will have clipped tooltips. The comment in the code says "Keep tooltip within viewport" but the clamping isn't implemented.

5. Dead onSpotClick prop on APRSPanel

APRSPanel still accepts onSpotClick and the row onClick still calls onSpotClick?.(...), but DockableApp.jsx no longer passes it. Harmless (optional chaining), but dead code — clean it up for clarity.

6. CONFIG_VERSION not bumped for relayToServer

The new relayToServer: false key is added to the default config but CONFIG_VERSION stays at 7. Existing configs won't get it auto-patched. Works in practice since !!undefined is false, but breaks the convention of bumping for new keys.

Looks good

  • pluginBus → SSE broadcast()CustomEvent → per-hook subscription: clean decoupling
  • plugin-init with 100-entry decode ring buffer on SSE connect: great UX, no waiting for next FT8 cycle
  • APRS RF station store with 60-min aging, RF-wins merge, tncDetectedViaSse flicker fix — all solid
  • APRS symbol sprites with fallback triangle, overlay char support, colour ring via box-shadow
  • SSID strip in extractBaseCall() for QRZ links
  • Cloud Relay disconnect button with immediate save
  • APRS clicks no longer moving DX target

@accius
Copy link
Copy Markdown
Owner

accius commented Mar 30, 2026

Please note, this is a AI Review posted above, there was too much in the diff for me to parse through this morning

…ip clamp, config defaults

- useWSJTX: reset isLocalMode after 30 s SSE silence so HTTP polling resumes
  if rig-bridge disconnects mid-session (was permanently latched)
- APRSPanel: clamp hover tooltip to viewport bounds; remove dead onSpotClick prop
- rig-bridge config: revert ohcUrl default to localhost:8080 (was 3000, dev-only port)
- rig-bridge config: bump CONFIG_VERSION 7 → 8 for relayToServer key addition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe
Copy link
Copy Markdown
Contributor Author

ceotjoe commented Mar 30, 2026

I just committed a fix. Claude was complaining about the first finding:

Finding 1 — forwardToLocal / cloud relay APRS: No change needed
The reviewer's concern is unfounded. aprs-tnc.js:149-174 still contains forwardToLocal() intact. cloud-relay.js:329-332 subscribes to bus.emit('aprs', ...) and batches packets to the server. Both paths work correctly. :-)

If this will be committed I have another PR based on this in the pipeline to add HTTPS (optional) to the bridge. Target is to integrate better with browsers not allowing http combined with https. Like Safari.

When both are in I can work again on the MeshCom plugin.

@accius accius merged commit 4231443 into accius:Staging Mar 30, 2026
4 checks passed
@accius
Copy link
Copy Markdown
Owner

accius commented Mar 30, 2026

merged thank you

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