diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 0e3a466..4963e71 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -115,17 +115,24 @@ export function useRadioConnection() { // Use the exported READ_STEPS array (single source of truth) const steps = READ_STEPS; + // 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(selectedRadioModel ?? '') ?? createDefaultProtocol(); - + protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol(); + // Set up progress callback that forwards to our callback protocol.onProgress = (progress, message) => { onProgress?.(progress, message); }; // 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; @@ -145,13 +152,17 @@ export function useRadioConnection() { setRadioInfo(radioInfo); 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(); @@ -338,37 +349,34 @@ 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]); // 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 f89383d..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; @@ -447,16 +447,15 @@ 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(); @@ -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..ec188a6 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: 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 8433370..58988b8 100644 --- a/src/radios/dm32uv/protocol.ts +++ b/src/radios/dm32uv/protocol.ts @@ -335,12 +335,13 @@ 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.'); } + // 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 @@ -375,9 +376,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) @@ -439,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 } /** @@ -1129,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...');