From 78daa1befaba8d6ac9f39bd72801d0182b6dd9ff Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Wed, 1 Apr 2026 11:20:01 -0700 Subject: [PATCH 1/4] Fix for all the connection issues --- src/radios/dm32uv/connection.ts | 15 ++++++++------- src/radios/dm32uv/constants.ts | 4 ++-- src/radios/dm32uv/protocol.ts | 26 +++++++++++++++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/radios/dm32uv/connection.ts b/src/radios/dm32uv/connection.ts index f89383d..eddb98c 100644 --- a/src/radios/dm32uv/connection.ts +++ b/src/radios/dm32uv/connection.ts @@ -447,21 +447,20 @@ export class DM32Connection { this.readBuffer = new Uint8Array(0); this.isReading = false; - // Release reader lock (but keep the port open for reuse) + // Cancel reader (aborts any in-flight read) and close writer if (this.reader) { try { - this.reader.releaseLock(); + await this.reader.cancel(); } catch (e) { - // Reader might already be released + // Reader might already be cancelled/released } this.reader = null; } - // Release writer lock (but keep the port open for reuse) if (this.writer) { try { - this.writer.releaseLock(); + await this.writer.close(); } catch (e) { - // Writer might already be released + // Writer might already be closed/released } this.writer = null; } @@ -633,9 +632,11 @@ export class DM32Connection { await Promise.race([fillPromise, timeoutPromise]); // Read one more time in case there's a second packet + // Create a new timeout promise - the original is already resolved and would race immediately await this.delay(20); try { - await Promise.race([this.fillBuffer(), timeoutPromise]); + const timeoutPromise2 = new Promise((resolve) => { setTimeout(() => resolve(), 50); }); + await Promise.race([this.fillBuffer(), timeoutPromise2]); } catch (e) { // Ignore errors } diff --git a/src/radios/dm32uv/constants.ts b/src/radios/dm32uv/constants.ts index 8722032..17acac6 100644 --- a/src/radios/dm32uv/constants.ts +++ b/src/radios/dm32uv/constants.ts @@ -86,8 +86,8 @@ export const CONNECTION = { BAUD_RATE: 115200, INIT_DELAY: 400, // ms after port open (increased for DM32.01.01.049 and similar) CLEAR_BUFFER_DELAY: 200, // ms after clearing buffer - PSEARCH_READ_DELAY: 150, // ms after PSEARCH before reading response (radio needs time to reply) - REOPEN_DELAY: 400, // ms to wait after closing port before reopening (fresh handshake) + PSEARCH_READ_DELAY: 100, // ms after PSEARCH before reading response (radio needs time to reply) + REOPEN_DELAY: 2000, // ms to wait after closing port before reopening (radio needs time to reset) BLOCK_READ_DELAY: 150, // ms between block reads (radio needs time after sending 4KB before next request) // Timeout values (in milliseconds) // Per-request timeout: 5s per message/ack cycle (matches C code), resets with each response diff --git a/src/radios/dm32uv/protocol.ts b/src/radios/dm32uv/protocol.ts index 8433370..e4d458b 100644 --- a/src/radios/dm32uv/protocol.ts +++ b/src/radios/dm32uv/protocol.ts @@ -233,6 +233,8 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port open' ); + // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) + await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); log.debug('Successfully opened previously granted port', 'Protocol'); return port; } catch (e: unknown) { @@ -294,6 +296,8 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port reopen' ); + // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) + await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); log.debug('Successfully reopened stored port', 'Protocol'); return port; } catch (e: unknown) { @@ -335,13 +339,25 @@ export class DM32UVProtocol implements RadioProtocol { // Check if port is already open const isAlreadyOpen = port.readable !== null && port.writable !== null; - + if (isAlreadyOpen && port.readable && port.writable) { // Check if streams are locked (from a previous connection) if (port.readable.locked || port.writable.locked) { throw new Error('Port is in use by another connection. Please wait for the previous operation to complete.'); } - log.debug('Port is already open, will use existing connection', 'Protocol'); + // Port is open but unlocked (e.g. reselected after a failed attempt that didn't close it). + // Close and reopen so the radio gets a clean DTR reset and is ready for PSEARCH. + log.debug('Selected port was already open; closing and reopening for fresh handshake', 'Protocol'); + try { + await port.close(); + } catch { /* ignore - port might be in a weird state */ } + await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); + await withTimeout( + port.open({ baudRate: CONNECTION.BAUD_RATE }), + CONNECTION.TIMEOUT.PORT_OPEN, + 'Port reopen' + ); + await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); } else { // Port is not open, so open it - wrap in timeout try { @@ -350,6 +366,8 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port open' ); + // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) + await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); } catch (e: unknown) { const error = e as Error; // If it says already open (race condition), check for locked streams @@ -375,9 +393,7 @@ export class DM32UVProtocol implements RadioProtocol { } private async connectWithPort(port: WebSerialPort): Promise { - // Brief delay after opening port (as per spec) - await new Promise(resolve => setTimeout(resolve, CONNECTION.INIT_DELAY)); - + // Note: DM32Connection.connect() handles the post-open INIT_DELAY internally this.port = port; this.connection = new DM32Connection(); // Each request/response in connect() has its own 2s timeout (per-request basis) From 3542348d5b8d9c1cb0986fcc1699eed1c4e4ef81 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Wed, 1 Apr 2026 14:19:18 -0700 Subject: [PATCH 2/4] More fixing, better approach --- src/hooks/useRadioConnection.ts | 11 ++++------ src/radios/dm32uv/connection.ts | 4 ++-- src/radios/dm32uv/constants.ts | 2 +- src/radios/dm32uv/protocol.ts | 38 +++++++++++---------------------- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 0e3a466..39ba471 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -118,7 +118,7 @@ export function useRadioConnection() { try { // Create protocol for the radio selected in the pick-a-radio modal protocol = createProtocolForModel(selectedRadioModel ?? '') ?? createDefaultProtocol(); - + // Set up progress callback that forwards to our callback protocol.onProgress = (progress, message) => { onProgress?.(progress, message); @@ -145,7 +145,7 @@ export function useRadioConnection() { setRadioInfo(radioInfo); setConnected(true); - + // Step 4: Bulk read when capability says so (e.g. DM-32UV); otherwise protocol reads on demand if (caps?.supportsBulkRead && typeof (protocol as any).bulkReadRequiredBlocks === 'function') { onProgress?.(15, 'Reading all memory blocks...', steps[3]); @@ -338,17 +338,14 @@ export function useRadioConnection() { // If it's not a port selection cancellation, try retrying with forced port selection if (!isPortSelectionCancelled && protocol) { console.warn('Read failed, will retry with port selection:', errorMessage); - - // Clear the stored port so we force port selection on retry - (protocol as any).port = null; - + // Try to disconnect the failed connection try { await protocol.disconnect(); } catch (disconnectErr) { console.warn('Error during disconnect cleanup:', disconnectErr); } - + // Retry the entire read operation with forced port selection try { onProgress?.(5, 'Retrying with port selection...', steps[0]); diff --git a/src/radios/dm32uv/connection.ts b/src/radios/dm32uv/connection.ts index eddb98c..7103802 100644 --- a/src/radios/dm32uv/connection.ts +++ b/src/radios/dm32uv/connection.ts @@ -458,9 +458,9 @@ export class DM32Connection { } if (this.writer) { try { - await this.writer.close(); + this.writer.releaseLock(); } catch (e) { - // Writer might already be closed/released + // Writer might already be released } this.writer = null; } diff --git a/src/radios/dm32uv/constants.ts b/src/radios/dm32uv/constants.ts index 17acac6..ec188a6 100644 --- a/src/radios/dm32uv/constants.ts +++ b/src/radios/dm32uv/constants.ts @@ -87,7 +87,7 @@ export const CONNECTION = { INIT_DELAY: 400, // ms after port open (increased for DM32.01.01.049 and similar) CLEAR_BUFFER_DELAY: 200, // ms after clearing buffer PSEARCH_READ_DELAY: 100, // ms after PSEARCH before reading response (radio needs time to reply) - REOPEN_DELAY: 2000, // ms to wait after closing port before reopening (radio needs time to reset) + REOPEN_DELAY: 400, // ms to wait after closing port before reopening (fresh handshake) BLOCK_READ_DELAY: 150, // ms between block reads (radio needs time after sending 4KB before next request) // Timeout values (in milliseconds) // Per-request timeout: 5s per message/ack cycle (matches C code), resets with each response diff --git a/src/radios/dm32uv/protocol.ts b/src/radios/dm32uv/protocol.ts index e4d458b..a78c9dd 100644 --- a/src/radios/dm32uv/protocol.ts +++ b/src/radios/dm32uv/protocol.ts @@ -233,8 +233,6 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port open' ); - // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) - await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); log.debug('Successfully opened previously granted port', 'Protocol'); return port; } catch (e: unknown) { @@ -296,8 +294,6 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port reopen' ); - // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) - await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); log.debug('Successfully reopened stored port', 'Protocol'); return port; } catch (e: unknown) { @@ -345,19 +341,8 @@ export class DM32UVProtocol implements RadioProtocol { if (port.readable.locked || port.writable.locked) { throw new Error('Port is in use by another connection. Please wait for the previous operation to complete.'); } - // Port is open but unlocked (e.g. reselected after a failed attempt that didn't close it). - // Close and reopen so the radio gets a clean DTR reset and is ready for PSEARCH. - log.debug('Selected port was already open; closing and reopening for fresh handshake', 'Protocol'); - try { - await port.close(); - } catch { /* ignore - port might be in a weird state */ } - await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); - await withTimeout( - port.open({ baudRate: CONNECTION.BAUD_RATE }), - CONNECTION.TIMEOUT.PORT_OPEN, - 'Port reopen' - ); - await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); + // Port is already open and unlocked - use existing connection + log.debug('Port is already open, will use existing connection', 'Protocol'); } else { // Port is not open, so open it - wrap in timeout try { @@ -366,8 +351,6 @@ export class DM32UVProtocol implements RadioProtocol { CONNECTION.TIMEOUT.PORT_OPEN, 'Port open' ); - // Wait for radio to boot after port open (opening port toggles DTR, which resets some radios) - await new Promise((resolve) => setTimeout(resolve, CONNECTION.REOPEN_DELAY)); } catch (e: unknown) { const error = e as Error; // If it says already open (race condition), check for locked streams @@ -455,13 +438,18 @@ export class DM32UVProtocol implements RadioProtocol { await this.connection.disconnect(); this.connection = null; } - // Keep the port reference so we can reuse it for subsequent operations - // Don't close the port - just release the reader/writer locks - // The port will stay open and can be reused - // Only clear the port if it's explicitly closed or if we want to force a new selection - // this.port = null; // Commented out to allow port reuse + // Close the port so the radio gets a DTR reset and starts exiting programming mode + // immediately. This is important: if we leave the port open, the radio stays in + // programming mode and won't respond to PSEARCH on the next connect attempt. + // We keep this.port reference so navigator.serial.getPorts() can still find it. + if (this.port) { + try { + await this.port.close(); + } catch { + // Port might already be closed or in an error state + } + } // Keep radioInfo and cachedBlockData - they're needed for parsing - // Only clear connection-related state } /** From 3c4be9858262aa9bf78e4f86e5f7078c191a1104 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Wed, 1 Apr 2026 16:24:24 -0700 Subject: [PATCH 3/4] Last pass for the fixes --- src/hooks/useRadioConnection.ts | 25 +++++++++++++++++-------- src/radios/dm32uv/connection.ts | 4 ++-- src/radios/dm32uv/protocol.ts | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 39ba471..81398ab 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -116,8 +116,13 @@ export function useRadioConnection() { const steps = READ_STEPS; try { + // Resolve effective model — selectedRadioModel is explicitly chosen in the picker; + // fall back to radioInfo.model for users who re-read without re-selecting (model looks + // pre-selected in the UI via useEffectiveRadioModel but selectedRadioModel is still null). + const effectiveModel = selectedRadioModel ?? radioInfo?.model ?? null; + // Create protocol for the radio selected in the pick-a-radio modal - protocol = createProtocolForModel(selectedRadioModel ?? '') ?? createDefaultProtocol(); + protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol(); // Set up progress callback that forwards to our callback protocol.onProgress = (progress, message) => { @@ -125,7 +130,7 @@ export function useRadioConnection() { }; // Step 1: Request port (serial or BLE) in same user gesture - const caps = getCapabilitiesForModel(selectedRadioModel ?? null); + const caps = getCapabilitiesForModel(effectiveModel); const transport = caps?.supportsBle ? (preferredTransport ?? caps?.preferredTransport ?? 'serial') : undefined; @@ -147,11 +152,15 @@ export function useRadioConnection() { setConnected(true); // Step 4: Bulk read when capability says so (e.g. DM-32UV); otherwise protocol reads on demand + console.log('[read] caps.supportsBulkRead=', caps?.supportsBulkRead, 'hasBulkFn=', typeof (protocol as any).bulkReadRequiredBlocks === 'function', 'model=', effectiveModel); if (caps?.supportsBulkRead && typeof (protocol as any).bulkReadRequiredBlocks === 'function') { onProgress?.(15, 'Reading all memory blocks...', steps[3]); await (protocol as any).bulkReadRequiredBlocks(); + console.log('[read] bulkReadRequiredBlocks done — cachedBlockData.length=', (protocol as any).cachedBlockData?.length, 'discoveredBlocks.length=', (protocol as any).discoveredBlocks?.length); + } else { + console.log('[read] skipping bulkRead'); } - + // Step 5: Parse channels (from cache after bulk read, or over connection) onProgress?.(20, 'Parsing channels...', steps[4]); const channels = await protocol.readChannels(); @@ -350,22 +359,22 @@ export function useRadioConnection() { try { onProgress?.(5, 'Retrying with port selection...', steps[0]); // Create a new protocol instance to ensure clean state (same radio as initial read) - protocol = createProtocolForModel(selectedRadioModel ?? '') ?? createDefaultProtocol(); + protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol(); protocol.onProgress = (progress, message) => { onProgress?.(progress, message); }; - + // Force port selection for retry (protocol as any).port = null; await protocol.connect(); - + // Continue with the read operation from the beginning onProgress?.(10, 'Reading radio information...', steps[2]); const radioInfo = await protocol.getRadioInfo(); setRadioInfo(radioInfo); setConnected(true); - - const retryCaps = getCapabilitiesForModel(selectedRadioModel ?? null); + + const retryCaps = getCapabilitiesForModel(effectiveModel); if (retryCaps?.supportsBulkRead && typeof (protocol as any).bulkReadRequiredBlocks === 'function') { onProgress?.(15, 'Reading all memory blocks...', steps[3]); await (protocol as any).bulkReadRequiredBlocks(); diff --git a/src/radios/dm32uv/connection.ts b/src/radios/dm32uv/connection.ts index 7103802..bb91e6b 100644 --- a/src/radios/dm32uv/connection.ts +++ b/src/radios/dm32uv/connection.ts @@ -76,9 +76,9 @@ export class DM32Connection { log.info('Ready to communicate.', 'Connection'); // Step 1: PSEARCH - // According to serial capture: response is exactly 8 bytes: 06 44 50 35 37 30 55 56 + // Send once and wait — the radio has a timing window and may be slow to reply. + // Use a longer timeout than other commands to avoid cutting off a late response. await this.sendCommand('PSEARCH'); - // CRITICAL: Send→read delay. Radio needs this before we read; removing it can cause radio reboot / connection failure. await this.delay(CONNECTION.PSEARCH_READ_DELAY); let psearchResponse: Uint8Array; diff --git a/src/radios/dm32uv/protocol.ts b/src/radios/dm32uv/protocol.ts index a78c9dd..58988b8 100644 --- a/src/radios/dm32uv/protocol.ts +++ b/src/radios/dm32uv/protocol.ts @@ -1133,7 +1133,7 @@ export class DM32UVProtocol implements RadioProtocol { // Ensure blocks have been read if (this.cachedBlockData.length === 0 || this.discoveredBlocks.length === 0) { - throw new Error('Blocks must be read first. Call bulkReadRequiredBlocks() before processing.'); + throw new Error(`Blocks must be read first. Call bulkReadRequiredBlocks() before processing. (cachedBlockData=${this.cachedBlockData.length}, discoveredBlocks=${this.discoveredBlocks.length})`); } this.onProgress?.(0, 'Parsing channels from cached blocks...'); From 71bac66ba5f62f9ac6272c4974342bf731734fc6 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Wed, 1 Apr 2026 16:44:15 -0700 Subject: [PATCH 4/4] TS oops --- src/hooks/useRadioConnection.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 81398ab..4963e71 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -115,12 +115,14 @@ export function useRadioConnection() { // Use the exported READ_STEPS array (single source of truth) const steps = READ_STEPS; - try { - // Resolve effective model — selectedRadioModel is explicitly chosen in the picker; - // fall back to radioInfo.model for users who re-read without re-selecting (model looks - // pre-selected in the UI via useEffectiveRadioModel but selectedRadioModel is still null). - const effectiveModel = selectedRadioModel ?? radioInfo?.model ?? null; + // Read model directly from the live store to avoid stale closure issues. + // selectedRadioModel may still be null if the user never explicitly clicked the picker + // button (the UI shows it pre-selected via useEffectiveRadioModel but the store value is + // null). Fall back to radioInfo.model from the last successful read. + const { selectedRadioModel: liveModel, radioInfo: liveRadioInfo } = useRadioStore.getState(); + const effectiveModel: string | null = liveModel ?? liveRadioInfo?.model ?? null; + try { // Create protocol for the radio selected in the pick-a-radio modal protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol();