Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions src/hooks/useRadioConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 8 additions & 7 deletions src/radios/dm32uv/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<void>((resolve) => { setTimeout(() => resolve(), 50); });
await Promise.race([this.fillBuffer(), timeoutPromise2]);
} catch (e) {
// Ignore errors
}
Expand Down
4 changes: 2 additions & 2 deletions src/radios/dm32uv/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 15 additions & 11 deletions src/radios/dm32uv/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -375,9 +376,7 @@ export class DM32UVProtocol implements RadioProtocol {
}

private async connectWithPort(port: WebSerialPort): Promise<void> {
// 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)
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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...');
Expand Down
Loading