fyne-ui-v2: Mercury Client chat window, waterfall palette & toggle, UI polish - #174
Conversation
|
Review of the full diff (14 files, +1505/−87) against 11 findings, ordered by severity. Numbers 1, 3 and 5 are the ones I would fix 1. Re-enabling the waterfall never restarts the publisher thread —
|
- Add dialog import - Remove audioCard (capture/playback/channel selectors + Apply) from main layout - Add Configuration > Soundcards menu item that opens modal dialog with capture device, playback device, and capture input channel selectors - Fix channel name case: use lowercase names to match C engine strcmp - Add 100ms delay before device list refresh after set_audio_config to prevent race condition with engine command processing
…ion to Settings - Remove radioCard (model/path/baud selectors + Apply) from main layout - Add Settings > Radio Config menu item opening modal dialog with radio model, device path, and baud rate widgets - Rename Configuration menu to Settings
- Move Engine selector, Host, Port, Scheme from main window connection card into a new Remote Control dialog - Keep Connect button on main window - Connection card now shows only the Connect button
- Remove connectionCard from main window entirely - Move Connect button into Remote Control dialog - Rename Host to IP/Host, Port to UI Port - Remove Engine label (dropdown only)
- New button in the top bar launching the standalone mercury-client chat app in its own window via the engine's ARQ/broadcast TCP ports. - Binary discovery: remembered path (preferences) -> MERCURY_CLIENT env -> next to the UI executable -> PATH; fallback locate dialog with persistence. Validates candidate is a native executable for the running OS/arch to guard against exec-format errors. - Makefile: fyne-ui-mercury-client target builds mercury-client next to mercury-ui; fyne-ui and fyne-ui-windows depend on it. Windows zip, signed-zip, and installer-stage all copy/sign mercury-client.exe. Installer .iss ships it. 'make clean' removes both artifacts. - Tests for binary discovery, native-executable validation, and the end-to-end launch path with stdout forwarding.
- New gui_interface/mercury-client/ Go module (client + modem packages) vendored locally; fyne-ui imports it via a replace directive. - Replace the exec-based Launch Mercury Client button with an in-process chat window (mercury_chat_window.go) that opens in the same Fyne app. No external binary, path lookup, or locate dialog needed. - Remove all exec-launcher files (mercury_client*.go) and associated tests. - Revert Makefile changes (no separate mercury-client build; everything is compiled into mercury-ui via the vendored module). - Clean .gitignore, installer.iss, README.
Chat window: - Remove TCP label, rename Connect/Disconnect TCP to Connect/Disconnect modem - Rename ARQ label to Session, Send ARQ to Send message, Send Broadcast to Broadcast message - Update placeholders: Type message to be sent... / Type broadcast message... - Rename ARQ Chat to Chat messages, Broadcast Chat to Broadcast Messages - Switch chat display to RichText with bold callsigns - Fix broadcast chat to properly split callsign from payload text Main telemetry: - Rename User Callsign to My callsign, Dest Callsign to Target callsign
- New Settings > Waterfall dialog with palette select widget: turbo, hot, grayscale, and blackblue (the current default). - Palette choice persists via Fyne preferences and updates the waterfall raster in real time. - Ported palette functions from the web UI: turbo (polynomial), hot (R/G/B ramps), grayscale (linear), blackblue (existing).
…istence
- C side:
- New ui_comm_set_waterfall() in ui_communication.c; set_waterfall
command handler delegates to it. Persists enabled state to
mercury.ini so the choice survives restarts.
- New mercury_ui_set_waterfall() bridge function, exposed to Go via
CGo for direct access without the generic command dispatch.
- Fixed the set_waterfall handler nesting (was inside the radio
config block).
- Go side:
- engineLink.SetWaterfall() calls the bridge directly (no websocket,
no strcmp dispatch). Stub (non-embedded) has a no-op fallback.
- Waterfall toggle checkbox in Settings > Waterfall uses the direct
CGo path when the embedded engine is active; remote links still
fall back to sendWSCommand.
- waterfallCard visibility bound to telemetry.Waterfall: when the
engine reports waterfall disabled, the spectrum/waterfall card
vanishes from the layout; toggling on brings it back.
When waterfall was disabled at boot (waterfall_enabled=false in .ini), the spectrum publisher thread was never created. Re-enabling through Settings > Waterfall only flipped the FFT gate, leaving remote UIs (web, mercury-qt) with a permanently blank waterfall. Fix: check spec_tid == 0 when enabling; if the thread was never started, launch it now so websocket clients receive spectrum frames.
SetChecked() fires OnChanged, so every opening of Settings > Waterfall while the waterfall was on re-toggled it: a redundant CGo call that rewrote mercury.ini on an embedded engine, or a set_waterfall push to a remote station, or a bogus 'not connected' error when there is no link. Create the check with a nil callback, set Checked directly (no OnChanged fire), then assign OnChanged.
cw.mc is read inside goroutines (onARQConnect) and button closures (arqAbort, onSendARQ, onSendBroadcast) but written to nil by onDisconnect on the UI goroutine. If onDisconnect runs before the goroutine/closures are scheduled, the nil read dereferences and panics, taking down the entire mercury-ui process. Capture cw.mc into a local variable at the start of each of these functions so the snapshot remains valid regardless of when the goroutine or callback executes.
Each connect/disconnect cycle previously stranded four goroutines
(forwardLog/forwardARQChat/forwardBroadcastChat/forwardStatus) because
Client.Disconnect never closes the event channels. The goroutines
blocked forever on range, held the old Client alive, and called
fyne.Do against widgets of a possibly-closed window.
Add a done chan struct{} to chatWindow: created in onConnect, closed
in onDisconnect and the window-close handler. The forwarders now
select between the channel and done, and capture mc/done into locals
so they are safe against nil races. A guard in onConnect closes any
stale done channel before creating a new one.
cfg_write does fopen(path, 'w') with no locking. The Fyne UI goroutine reaches it via mercury_ui_set_waterfall, while the websocket thread can be inside ui_comm_handle_command writing set_audio_config / set_radio_config / set_tx_gain. Concurrent writes truncate or interleave mercury.ini. Add a pthread_mutex_t cfg_mutex to ui_ctx_t, init it in ui_comm_init, destroy it in ui_comm_shutdown, and lock around every cfg_write + cfg field-write block: set_audio_config, set_radio_config, set_tx_gain, and ui_comm_set_waterfall.
state.link is written under state.mu from the connect goroutine and disconnectLink, but was read without the lock in the waterfall checkbox's OnChanged callback. A torn read of the two-word interface value during a connect or disconnect would mis-identify the link or crash the type assertion under the race detector. Wrap the type assertion in state.mu.RLock/RUnlock.
Disconnect() closes and nils mc.ARQDataConn/mc.BroadcastConn under mc.mu while readARQData and readBroadcast dereference those fields in their read loops with no lock. A nil *net.TCPConn receiver happens to survive (returns EINVAL), but the data race is real. Capture the connection into a local once at goroutine start, the same way readARQControl already does it.
Disconnect() did close(mc.quit); mc.quit = make(chan struct{}) while
readers selected on mc.quit unsynchronized. A reader that had not yet
observed the close now selects on the fresh channel and hangs until
socket error.
Capture quit := mc.quit into a local at goroutine start in all three
readers (readARQControl, readARQData, readBroadcast) so the snapshot
is stable regardless of when Disconnect swaps the channel.
Also: Disconnect() held mc.mu while sending to mc.LogCh. A stalled
consumer would block the send, and with the mutex held the entire
modem subsystem stalls. Gather the log messages into a local slice
and send them after mc.mu.Unlock().
ConnectARQ created a shared connectRespCh that was overwritten on a second call. The first waiter's deferred mc.connectRespCh = nil then cleared the channel the second waiter was relying on, stranding both until their 120 s timeouts. Two fixes: - Guard ConnectARQ: if connectRespCh is already non-nil, return 'ARQ connection already in progress' immediately. - Disable the Connect ARQ button synchronously in onARQConnect before spawning the goroutine, so a double-click never reaches the modem.
If ARQ control dials OK and then the data or broadcast dial fails, the already-open connections were neither closed nor reachable — the caller drops the ModemClient and the socket lingers until the GC finalizer. Add a deferred cleanup that runs only when Connect() returns an error: close every conn that was opened up to the failure point, taking care not to double-close when ARQDataConn is aliased to ARQControlConn.
8cf1020 to
7731178
Compare
|
Re-reviewed after your fixes. All ten are real — I checked each diff rather Verified green: I also went looking for problems the fixes could have introduced, and found Verdict: approve with changes. Nothing below is a structural problem with Fix before mergeA.
|
The reader goroutines read mc.ARQControlConn, mc.ARQDataConn, mc.BroadcastConn and mc.quit unsynchronized at goroutine start, while Disconnect() nils them under mc.mu. This is a data race under -race, and readARQControl/readBroadcast had no nil guard: if Disconnect() ran before the goroutine was first scheduled, bufio.NewReader(nil) would dereference a nil *net.TCPConn and panic the process. Capture conn and quit under mc.mu in all three readers and return early if conn is nil. readARQData also snapshots ARQControlConn under the lock so the aliasing check is race-free.
The five client.Client goroutines (updateLog, handleIncomingARQ,
handleIncomingARQData, handleIncomingBroadcast, handleStatus) ranged
over modem channels that are never closed. Disconnect() only closes
sockets and swaps the modem quit channel, so these five blocked
forever, holding both Client and ModemClient alive across
connect/disconnect cycles.
Add a done chan struct{} to Client: created in Connect (closing any
stale one), closed in Disconnect. Each goroutine now selects between
its modem channel and done, capturing mc and done under c.mu.
logMsg rebuilt the full log text on every message (O(n) each), and appendRichChat prepended a new RichText widget forever. During a long ARQ transfer, handleIncomingARQData emits a line per RX chunk, so the window degrades to O(n^2) and becomes unusable. Cap both to a ring buffer: log keeps the last 1000 lines, each chat pane keeps the last 200 messages.
openMercuryClientWindow always built a new window. The engine only accepts one control client at a time, so a second window evicts the first and tears down its ARQ session, while the evicted window keeps showing connected. Track the open window in a package-level singleton: if one exists, bring it to focus instead of creating another. Clear the singleton in SetOnClosed so a fresh window can be opened after the old one closes.
Verdict: safe to merge — go ahead and merge it, @pedromessettiThird pass, verified from the diffs rather than the commit titles. All four Green on both build tags:
No panic, crash or wire/protocol regression remains. Merge it — the rest is Worth a follow-up PRThe first two are refinements of what you just fixed, not new problems. 1.
2.
3. 4. 5. 6. 7. Plus the earlier follow-ups still open, all low: Once this lands I will rebase #175 (TX waterfall) on top and drop my Also: Good, fast work on both rounds. |
Follow-up fixes for fyne-ui-v2 (PR #174 review comments)
Summary
This branch bundles the vendored Mercury Client into the Fyne UI and adds waterfall enhancements.
Mercury Client (vendored)
Waterfall
Telemetry labels