diff --git a/gui_interface/fyne-ui/engine/mercury_bridge.c b/gui_interface/fyne-ui/engine/mercury_bridge.c index b915cc2b..251c5ab5 100644 --- a/gui_interface/fyne-ui/engine/mercury_bridge.c +++ b/gui_interface/fyne-ui/engine/mercury_bridge.c @@ -177,3 +177,8 @@ void mercury_ui_set_waterfall(bool enabled) { ui_comm_set_waterfall(enabled); } + +void mercury_ui_get_tcp_ports(int *arq_base_port, int *broadcast_port) +{ + ui_comm_get_tcp_ports(arq_base_port, broadcast_port); +} diff --git a/gui_interface/fyne-ui/engine/mercury_bridge.h b/gui_interface/fyne-ui/engine/mercury_bridge.h index 8ff17a75..02aed13a 100644 --- a/gui_interface/fyne-ui/engine/mercury_bridge.h +++ b/gui_interface/fyne-ui/engine/mercury_bridge.h @@ -79,6 +79,9 @@ int mercury_ui_get_input_channel(void); /* Enable / disable waterfall/spectrum at runtime. Saves to mercury.ini. */ void mercury_ui_set_waterfall(bool enabled); +/* Read the TNC TCP ports the engine listens on. */ +void mercury_ui_get_tcp_ports(int *arq_base_port, int *broadcast_port); + #ifdef __cplusplus } #endif diff --git a/gui_interface/fyne-ui/link_engine.go b/gui_interface/fyne-ui/link_engine.go index 8c577edc..e1fed9ae 100644 --- a/gui_interface/fyne-ui/link_engine.go +++ b/gui_interface/fyne-ui/link_engine.go @@ -43,6 +43,11 @@ const ( engineSpectrumInterval = 20 * time.Millisecond // Matches MODEM_STATS_NSPEC; the bridge clamps to its own size anyway. engineSpectrumBins = 512 + // After a config change that restarts a subsystem (audioio, hamlib), + // re-read the device lists several times so a slow restart is not + // reported as the stale pre-change selection. + refreshRetryInterval = 200 * time.Millisecond + refreshRetries = 5 ) func newEngineLink() *engineLink { @@ -119,9 +124,20 @@ func (l *engineLink) Start(ctx context.Context) (<-chan Event, error) { case <-l.refresh: // A device or radio change was just applied; re-read so the - // pickers show what the engine actually ended up using, which - // is not always what was asked for. - _ = l.emitDeviceLists(ctx, events) + // pickers show what the engine actually ended up using, which is + // not always what was asked for. Retry a few times because the + // restart it triggered may take longer than one read to settle. + for i := 0; i <= refreshRetries; i++ { + _ = l.emitDeviceLists(ctx, events) + if i == refreshRetries { + break + } + select { + case <-ctx.Done(): + return + case <-time.After(refreshRetryInterval): + } + } } } }() @@ -147,12 +163,12 @@ func (l *engineLink) Send(cmd Command) error { switch cmd.Name { case "set_audio_config", "set_radio_config": - time.AfterFunc(100*time.Millisecond, func() { - select { - case l.refresh <- struct{}{}: - default: - } - }) + // The Start goroutine owns the retry timing (cancellable via ctx), + // so no bare time.AfterFunc here that would outlive Close(). + select { + case l.refresh <- struct{}{}: + default: + } } return nil } @@ -260,6 +276,14 @@ func (l *engineLink) SetWaterfall(enabled bool) { C.mercury_ui_set_waterfall(cEn) } +// TCPPorts returns the ARQ base and broadcast TCP ports the engine is +// actually listening on (from its config). +func (l *engineLink) TCPPorts() (arqBase, broadcast int) { + var a, b C.int + C.mercury_ui_get_tcp_ports(&a, &b) + return int(a), int(b) +} + func (l *engineLink) Close() {} // statusFromC converts the engine's status struct into the UI's own type. This diff --git a/gui_interface/fyne-ui/link_engine_stub.go b/gui_interface/fyne-ui/link_engine_stub.go index 9a5a8bbf..8a2579ac 100644 --- a/gui_interface/fyne-ui/link_engine_stub.go +++ b/gui_interface/fyne-ui/link_engine_stub.go @@ -31,3 +31,7 @@ func (l *engineLink) Send(cmd Command) error { func (l *engineLink) Close() {} func (l *engineLink) SetWaterfall(enabled bool) {} + +func (l *engineLink) TCPPorts() (arqBase, broadcast int) { + return 8300, 8100 +} diff --git a/gui_interface/fyne-ui/main.go b/gui_interface/fyne-ui/main.go index 546ddb62..f3bb9452 100644 --- a/gui_interface/fyne-ui/main.go +++ b/gui_interface/fyne-ui/main.go @@ -827,10 +827,15 @@ func main() { } mercuryClientButton := widget.NewButton("Launch Mercury Client", func() { - state.mu.Lock() + state.mu.RLock() tel := state.telemetry - state.mu.Unlock() - openMercuryClientWindow(myApp, tel) + link := state.link + state.mu.RUnlock() + arqPort, broadcastPort := 8300, 8100 + if engLink, ok := link.(*engineLink); ok { + arqPort, broadcastPort = engLink.TCPPorts() + } + openMercuryClientWindow(myApp, tel, arqPort, broadcastPort) }) topBar := container.NewHBox( diff --git a/gui_interface/fyne-ui/mercury_chat_window.go b/gui_interface/fyne-ui/mercury_chat_window.go index e4b0b75d..0d34c656 100644 --- a/gui_interface/fyne-ui/mercury_chat_window.go +++ b/gui_interface/fyne-ui/mercury_chat_window.go @@ -19,7 +19,7 @@ import ( // transfers: each append would otherwise rebuild a growing string / prepend a // new widget forever, making the window unusable. const ( - maxLogLines = 1000 + maxLogLines = 200 maxChatMessages = 200 ) @@ -28,13 +28,13 @@ const ( // the first and tear down its ARQ session. Reuse the window instead. var mercuryClientSingleton *chatWindow -func openMercuryClientWindow(app fyne.App, telemetry telemetryState) { +func openMercuryClientWindow(app fyne.App, telemetry telemetryState, arqPort, broadcastPort int) { if mercuryClientSingleton != nil { mercuryClientSingleton.win.RequestFocus() return } cw := &chatWindow{} - cw.build(app, telemetry) + cw.build(app, telemetry, arqPort, broadcastPort) mercuryClientSingleton = cw } @@ -43,6 +43,9 @@ type chatWindow struct { mc *client.Client done chan struct{} log *widget.Entry + // logLines is the bounded ring (newest first) backing the log Entry, + // so appending never re-splits the widget's own text. + logLines []string arqBox *fyne.Container arqScroll *container.Scroll @@ -66,7 +69,7 @@ type chatWindow struct { bcastMsg *widget.Entry } -func (cw *chatWindow) build(app fyne.App, telemetry telemetryState) { +func (cw *chatWindow) build(app fyne.App, telemetry telemetryState, arqPort, broadcastPort int) { cw.win = app.NewWindow("Mercury Client") cw.myCall = widget.NewEntry() @@ -76,9 +79,9 @@ func (cw *chatWindow) build(app fyne.App, telemetry telemetryState) { cw.ip = widget.NewEntry() cw.ip.SetText("127.0.0.1") cw.arqPort = widget.NewEntry() - cw.arqPort.SetText("8300") + cw.arqPort.SetText(strconv.Itoa(arqPort)) cw.bcastPort = widget.NewEntry() - cw.bcastPort.SetText("8100") + cw.bcastPort.SetText(strconv.Itoa(broadcastPort)) cw.arqMsg = widget.NewEntry() cw.arqMsg.SetPlaceHolder("Type message to be sent...") @@ -176,17 +179,11 @@ func (cw *chatWindow) logMsg(format string, args ...any) { fyne.Do(func() { ts := time.Now().Format("15:04:05") line := fmt.Sprintf("[%s] "+format, append([]any{ts}, args...)...) - cur := cw.log.Text - if cur == "" { - cw.log.SetText(line) - } else { - newText := fmt.Sprintf("%s\n%s", line, cur) - lines := strings.Split(newText, "\n") - if len(lines) > maxLogLines { - lines = lines[:maxLogLines] - } - cw.log.SetText(strings.Join(lines, "\n")) + cw.logLines = append([]string{line}, cw.logLines...) + if len(cw.logLines) > maxLogLines { + cw.logLines = cw.logLines[:maxLogLines] } + cw.log.SetText(strings.Join(cw.logLines, "\n")) cw.log.Refresh() }) } @@ -263,8 +260,33 @@ func (cw *chatWindow) setARQ(on bool) { } func (cw *chatWindow) onConnect() { - arqPort, _ := strconv.Atoi(cw.arqPort.Text) - bcastPort, _ := strconv.Atoi(cw.bcastPort.Text) + // Synchronous guard: a double-tap (or key-repeat on a focused button) + // can fire this twice in one poll batch before setTCP's fyne.Do runs. + cw.connectBtn.Disable() + + // Disconnect any existing client before opening a new one, so a stale + // control client is not left open to be evicted by the new connection. + if cw.mc != nil { + cw.mc.Disconnect() + cw.mc = nil + } + if cw.done != nil { + close(cw.done) + cw.done = nil + } + + arqPort, err := strconv.Atoi(cw.arqPort.Text) + if err != nil { + dialog.ShowError(fmt.Errorf("invalid ARQ port: %v", err), cw.win) + cw.connectBtn.Enable() + return + } + bcastPort, err := strconv.Atoi(cw.bcastPort.Text) + if err != nil { + dialog.ShowError(fmt.Errorf("invalid broadcast port: %v", err), cw.win) + cw.connectBtn.Enable() + return + } cfg := client.Config{ MyCallsign: cw.myCall.Text, TargetCallsign: cw.target.Text, @@ -276,11 +298,9 @@ func (cw *chatWindow) onConnect() { if err := mc.Connect(); err != nil { dialog.ShowError(err, cw.win) cw.logMsg("connect: %v", err) + cw.connectBtn.Enable() return } - if cw.done != nil { - close(cw.done) - } cw.mc = mc cw.setTCP(true) cw.done = make(chan struct{}) @@ -316,17 +336,23 @@ func (cw *chatWindow) onARQConnect() { go func() { if err := mc.ConnectARQ(); err != nil { cw.logMsg("ARQ connect: %v", err) - cw.setARQ(false) + // Only re-enable the button if the modem is still up: a + // disconnect mid-handshake leaves cw.mc nil. + if cw.mc == mc { + cw.setARQ(false) + } return } cw.logMsg("ARQ connected.") - cw.setARQ(true) + if cw.mc == mc { + cw.setARQ(true) + } }() } func (cw *chatWindow) onARQDisconnect() { - if cw.mc != nil { - cw.mc.DisconnectARQ() + if mc := cw.mc; mc != nil { + mc.DisconnectARQ() } cw.setARQ(false) cw.logMsg("ARQ disconnected.") diff --git a/gui_interface/mercury-client/client/client.go b/gui_interface/mercury-client/client/client.go index 0c4644b0..28a05afb 100644 --- a/gui_interface/mercury-client/client/client.go +++ b/gui_interface/mercury-client/client/client.go @@ -237,6 +237,18 @@ func (c *Client) SendBroadcast(msg string) error { return nil } +// sendOrStop forwards v to ch unless done is closed first. It returns false +// when the goroutine should stop instead of blocking forever on a full channel +// whose only drainer has already exited. +func sendOrStop[T any](ch chan<- T, v T, done <-chan struct{}) bool { + select { + case ch <- v: + return true + case <-done: + return false + } +} + // updateLog forwards modem log lines to LogCh. func (c *Client) updateLog() { c.mu.Lock() @@ -252,7 +264,9 @@ func (c *Client) updateLog() { if !ok { return } - c.LogCh <- logMsg + if !sendOrStop(c.LogCh, logMsg, done) { + return + } case <-done: return } @@ -274,9 +288,13 @@ func (c *Client) handleIncomingARQ() { if !ok { return } - c.LogCh <- fmt.Sprintf("ARQ Control: %s", arqMsg) + if !sendOrStop(c.LogCh, fmt.Sprintf("ARQ Control: %s", arqMsg), done) { + return + } if strings.HasPrefix(arqMsg, "CONNECTED") { - c.updateRemoteCall(arqMsg) + if !c.updateRemoteCall(arqMsg, done) { + return + } } case <-done: return @@ -286,10 +304,10 @@ func (c *Client) handleIncomingARQ() { // updateRemoteCall derives the remote callsign from a "CONNECTED ..." // line by matching against the local callsign. -func (c *Client) updateRemoteCall(connectedLine string) { +func (c *Client) updateRemoteCall(connectedLine string, done <-chan struct{}) bool { fields := strings.Fields(connectedLine) if len(fields) < 3 { - return + return true } myCall := strings.ToUpper(strings.TrimSpace(c.cfg.MyCallsign)) callA := fields[1] @@ -306,7 +324,7 @@ func (c *Client) updateRemoteCall(connectedLine string) { c.mu.Lock() c.remoteCall = call c.mu.Unlock() - c.LogCh <- fmt.Sprintf("Remote ARQ callsign set to: %s", call) + return sendOrStop(c.LogCh, fmt.Sprintf("Remote ARQ callsign set to: %s", call), done) } // handleIncomingARQData buffers incoming ARQ data and emits complete @@ -325,7 +343,9 @@ func (c *Client) handleIncomingARQData() { if !ok { return } - c.LogCh <- fmt.Sprintf("ARQ Data RX: %d bytes: %q", len(data), string(data)) + if !sendOrStop(c.LogCh, fmt.Sprintf("ARQ Data RX: %d bytes: %q", len(data), string(data)), done) { + return + } c.mu.Lock() c.chatRxBuffer += string(data) var lines []ChatMessage @@ -349,7 +369,9 @@ func (c *Client) handleIncomingARQData() { } c.mu.Unlock() for _, msg := range lines { - c.ARQChatCh <- msg + if !sendOrStop(c.ARQChatCh, msg, done) { + return + } } case <-done: return @@ -374,7 +396,9 @@ func (c *Client) handleIncomingBroadcast() { if !ok { return } - c.LogCh <- fmt.Sprintf("Broadcast RX (Decoded): %s", string(data)) + if !sendOrStop(c.LogCh, fmt.Sprintf("Broadcast RX (Decoded): %s", string(data)), done) { + return + } c.mu.Lock() c.broadcastRxBuffer += string(data) var lines []ChatMessage @@ -394,7 +418,9 @@ func (c *Client) handleIncomingBroadcast() { } c.mu.Unlock() for _, msg := range lines { - c.BroadcastChatCh <- msg + if !sendOrStop(c.BroadcastChatCh, msg, done) { + return + } } case <-done: return @@ -418,8 +444,12 @@ func (c *Client) handleStatus() { if !ok { return } - c.LogCh <- fmt.Sprintf("TNC Status: %s", status) - c.StatusCh <- status + if !sendOrStop(c.LogCh, fmt.Sprintf("TNC Status: %s", status), done) { + return + } + if !sendOrStop(c.StatusCh, status, done) { + return + } if status == "DISCONNECTED" { c.mu.Lock() c.remoteCall = "" diff --git a/gui_interface/mercury-client/modem/modem.go b/gui_interface/mercury-client/modem/modem.go index 933b995e..d56b49b4 100644 --- a/gui_interface/mercury-client/modem/modem.go +++ b/gui_interface/mercury-client/modem/modem.go @@ -112,21 +112,32 @@ func NewModemClient(arqControlAddr, arqDataAddr, broadcastAddr string) *ModemCli func (mc *ModemClient) Connect() (err error) { mc.mu.Lock() - defer mc.mu.Unlock() + + var logLines []string + + // Runs last: release the mutex, then flush the collected log lines. + defer func() { + mc.mu.Unlock() + for _, line := range logLines { + mc.LogCh <- line + } + }() if mc.ARQControlConn != nil || mc.ARQDataConn != nil || mc.BroadcastConn != nil { return fmt.Errorf("already connected") } + // Runs first: on failure, close any conns opened up to this point. defer func() { if err == nil { return } - if mc.ARQControlConn != nil { - mc.ARQControlConn.Close() + control := mc.ARQControlConn + if control != nil { + control.Close() mc.ARQControlConn = nil } - if mc.ARQDataConn != nil && mc.ARQDataConn != mc.ARQControlConn { + if mc.ARQDataConn != nil && mc.ARQDataConn != control { mc.ARQDataConn.Close() mc.ARQDataConn = nil } @@ -144,7 +155,7 @@ func (mc *ModemClient) Connect() (err error) { if err != nil { return fmt.Errorf("connect to ARQ control port %s: %w", mc.ARQControlAddr, err) } - mc.LogCh <- fmt.Sprintf("Connected to ARQ Control: %s", mc.ARQControlAddr) + logLines = append(logLines, fmt.Sprintf("Connected to ARQ Control: %s", mc.ARQControlAddr)) if mc.ARQDataAddr != "" && mc.ARQDataAddr != mc.ARQControlAddr { arqDataTCPAddr, err := net.ResolveTCPAddr("tcp", mc.ARQDataAddr) @@ -155,9 +166,9 @@ func (mc *ModemClient) Connect() (err error) { if err != nil { return fmt.Errorf("connect to ARQ data port %s: %w", mc.ARQDataAddr, err) } - mc.LogCh <- fmt.Sprintf("Connected to ARQ Data: %s", mc.ARQDataAddr) + logLines = append(logLines, fmt.Sprintf("Connected to ARQ Data: %s", mc.ARQDataAddr)) } else { - mc.LogCh <- "ARQ Data port not specified or same as control, using control for data." + logLines = append(logLines, "ARQ Data port not specified or same as control, using control for data.") mc.ARQDataConn = mc.ARQControlConn } @@ -169,7 +180,7 @@ func (mc *ModemClient) Connect() (err error) { if err != nil { return fmt.Errorf("connect to Broadcast port %s: %w", mc.BroadcastAddr, err) } - mc.LogCh <- fmt.Sprintf("Connected to Broadcast: %s", mc.BroadcastAddr) + logLines = append(logLines, fmt.Sprintf("Connected to Broadcast: %s", mc.BroadcastAddr)) go mc.readARQControl() go mc.readARQData() @@ -180,12 +191,13 @@ func (mc *ModemClient) Connect() (err error) { func (mc *ModemClient) SendCommand(cmd string) error { mc.mu.Lock() - defer mc.mu.Unlock() - if mc.ARQControlConn == nil { + conn := mc.ARQControlConn + mc.mu.Unlock() + if conn == nil { return fmt.Errorf("not connected") } mc.LogCh <- fmt.Sprintf("TX Command: %s", cmd) - _, err := mc.ARQControlConn.Write([]byte(cmd + "\r")) + _, err := conn.Write([]byte(cmd + "\r")) return err } @@ -210,6 +222,7 @@ func (mc *ModemClient) ConnectARQ(src, dst string) error { mc.connectRespCh = make(chan string, 4) respCh := mc.connectRespCh + quit := mc.quit mc.mu.Unlock() defer func() { @@ -252,6 +265,8 @@ func (mc *ModemClient) ConnectARQ(src, dst string) error { mc.StatusCh <- "DISCONNECTED" return fmt.Errorf("disconnected before connection established") } + case <-quit: + return fmt.Errorf("disconnected") case <-timeout: return fmt.Errorf("connect timeout waiting for CONNECTED") } @@ -362,12 +377,13 @@ func (mc *ModemClient) Disconnect() { var disconnected []string mc.mu.Lock() - if mc.ARQControlConn != nil { - mc.ARQControlConn.Close() + control := mc.ARQControlConn + if control != nil { + control.Close() mc.ARQControlConn = nil disconnected = append(disconnected, "Disconnected from ARQ Control.") } - if mc.ARQDataConn != nil && mc.ARQDataConn != mc.ARQControlConn { + if mc.ARQDataConn != nil && mc.ARQDataConn != control { mc.ARQDataConn.Close() mc.ARQDataConn = nil disconnected = append(disconnected, "Disconnected from ARQ Data.") diff --git a/gui_interface/ui_communication.c b/gui_interface/ui_communication.c index 071c17c0..c8088dc8 100644 --- a/gui_interface/ui_communication.c +++ b/gui_interface/ui_communication.c @@ -201,9 +201,9 @@ int ui_comm_handle_command(ui_ctx_t *ctx, const ws_command_t *cmd) if (db > 20.0f) db = 20.0f; float linear = powf(10.0f, db / 20.0f); modem_set_tx_gain(linear); - ctx->cfg.tx_gain_db = db; HLOGI(UI_LOG_TAG, "TX gain set to %.2f dB (linear=%.4f)", db, linear); pthread_mutex_lock(&ctx->cfg_mutex); + ctx->cfg.tx_gain_db = db; if (ctx->cfg_path[0] && cfg_write(&ctx->cfg, ctx->cfg_path)) HLOGI(UI_LOG_TAG, "Config saved to %s", ctx->cfg_path); pthread_mutex_unlock(&ctx->cfg_mutex); @@ -222,17 +222,28 @@ void ui_comm_set_waterfall(bool enabled) if (!ctx) return; modem_set_spectrum_enabled(enabled); - ctx->waterfall_enabled = enabled; pthread_mutex_lock(&ctx->cfg_mutex); + ctx->waterfall_enabled = enabled; ctx->cfg.waterfall_enabled = enabled; - if (enabled && ctx->spec_tid == 0) { - if (pthread_create(&ctx->spec_tid, NULL, spectrum_publisher_thread, ctx) != 0) { - HLOGE(UI_LOG_TAG, "pthread_create(spec) failed: %s", strerror(errno)); - } else { - pthread_detach(ctx->spec_tid); - HLOGI(UI_LOG_TAG, "Spectrum publisher thread started (delayed start)"); + if (enabled) { + if (ctx->spec_tid == 0) { + atomic_store_explicit(&ctx->spec_run, true, memory_order_relaxed); + if (pthread_create(&ctx->spec_tid, NULL, spectrum_publisher_thread, ctx) != 0) { + atomic_store_explicit(&ctx->spec_run, false, memory_order_relaxed); + ctx->spec_tid = 0; + HLOGE(UI_LOG_TAG, "pthread_create(spec) failed: %s", strerror(errno)); + } else { + HLOGI(UI_LOG_TAG, "Spectrum publisher thread started (delayed start)"); + } + } + } else { + if (ctx->spec_tid != 0) { + atomic_store_explicit(&ctx->spec_run, false, memory_order_relaxed); + pthread_join(ctx->spec_tid, NULL); + ctx->spec_tid = 0; + HLOGI(UI_LOG_TAG, "Spectrum publisher thread stopped"); } } @@ -241,6 +252,20 @@ void ui_comm_set_waterfall(bool enabled) pthread_mutex_unlock(&ctx->cfg_mutex); } +void ui_comm_get_tcp_ports(int *arq_base_port, int *broadcast_port) +{ + ui_ctx_t *ctx = g_ui_ctx; + if (!ctx) { + if (arq_base_port) *arq_base_port = 8300; + if (broadcast_port) *broadcast_port = 8100; + return; + } + pthread_mutex_lock(&ctx->cfg_mutex); + if (arq_base_port) *arq_base_port = ctx->cfg.arq_tcp_base_port; + if (broadcast_port) *broadcast_port = ctx->cfg.broadcast_tcp_port; + pthread_mutex_unlock(&ctx->cfg_mutex); +} + // ---------------- UI PUBLISHER THREAD ---------------- // Periodically gathers modem/ARQ/network status and sends it to the UI. @@ -563,7 +588,7 @@ void *spectrum_publisher_thread(void *arg) uint64_t last_seq = 0; - while (!shutdown_) + while (!shutdown_ && atomic_load_explicit(&ctx->spec_run, memory_order_relaxed)) { float spec_dB[MODEM_STATS_NSPEC]; uint64_t seq = 0; @@ -697,11 +722,13 @@ int ui_comm_init(ui_ctx_t *ctx, uint16_t ws_port, bool tls_enabled, if (waterfall_enabled) { // Start spectrum publisher thread - high-rate FFT/waterfall broadcaster (via WebSocket) + atomic_store_explicit(&ctx->spec_run, true, memory_order_relaxed); if (pthread_create(&ctx->spec_tid, NULL, spectrum_publisher_thread, ctx) != 0) { + atomic_store_explicit(&ctx->spec_run, false, memory_order_relaxed); + ctx->spec_tid = 0; HLOGE(UI_LOG_TAG, "pthread_create(spec) failed: %s", strerror(errno)); HLOGW(UI_LOG_TAG, "Spectrum publisher thread not started (waterfall may not update)"); } else { - pthread_detach(ctx->spec_tid); HLOGI(UI_LOG_TAG, "Spectrum publisher thread started"); } } @@ -714,6 +741,9 @@ void ui_comm_shutdown(ui_ctx_t *ctx) g_ui_ctx = NULL; ws_shutdown(&ctx->ws); - pthread_mutex_destroy(&ctx->cfg_mutex); + // NOTE: cfg_mutex is deliberately NOT destroyed. ui_comm_set_waterfall + // may have already read g_ui_ctx before the NULL above and could still + // lock it, so destroying it here would race a live user. The process is + // exiting anyway. HLOGI(UI_LOG_TAG, "Shut down"); } diff --git a/gui_interface/ui_communication.h b/gui_interface/ui_communication.h index d08f7a96..72828f1a 100644 --- a/gui_interface/ui_communication.h +++ b/gui_interface/ui_communication.h @@ -25,6 +25,7 @@ #define UI_COMMUNICATION_H #include +#include #include #include "websocket/mercury_websocket.h" @@ -69,6 +70,7 @@ struct ui_ctx { pthread_t pub_tid; pthread_t spec_tid; // dedicated spectrum publisher thread (20 fps) + _Atomic bool spec_run; // drives the spectrum publisher loop (0 = stop) int waterfall_enabled; // 1 = send spectrum data to UI, 0 = disabled // Audio subsystem info for soundcard enumeration @@ -132,6 +134,10 @@ int ui_comm_command(const char *command, const char *value, * choice to mercury.ini so it survives restarts. */ void ui_comm_set_waterfall(bool enabled); +/* Read the TNC TCP ports the engine actually listens on (arq_tcp_base_port + * and broadcast_tcp_port from the config). */ +void ui_comm_get_tcp_ports(int *arq_base_port, int *broadcast_port); + /* Copy the most recent status snapshot. False until the first publish. */ bool ui_comm_get_status(ui_status_t *out);