From 3c3cc9a1dfacf53771175e3e1cec7d62f7997054 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Fri, 22 May 2026 10:09:18 -0700 Subject: [PATCH 1/2] Clean up of some deadcode + some dedupping that needed to get done --- src/App.tsx | 63 ++- src/hooks/useRadioConnection.ts | 463 +++++------------------ src/services/channelMerger.ts | 35 +- src/services/csv/csvImporter.ts | 46 +-- src/services/jsonLoader.ts | 1 - src/services/locationChannelGenerator.ts | 253 ------------- 6 files changed, 126 insertions(+), 735 deletions(-) delete mode 100644 src/services/locationChannelGenerator.ts diff --git a/src/App.tsx b/src/App.tsx index 374ca78..98b87db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import { useEncryptionKeysStore } from './store/encryptionKeysStore'; import { useRadioStore } from './store/radioStore'; import { useRadioConnection } from './hooks/useRadioConnection'; import { importChannelsFromCSV, importContactsFromCSV } from './services/csv'; +import type { CodeplugData } from './services/codeplugExport'; import { sampleChannels, sampleContacts, sampleZones } from './utils/sampleData'; import { setLogStore, logger, LogLevel } from './utils/protocolLogger'; import { useLogStore } from './store/logStore'; @@ -138,6 +139,27 @@ function App() { }, 100); }; + const applyCodeplugToStores = (codeplugData: CodeplugData) => { + setChannels(codeplugData.channels); + setZones(codeplugData.zones); + setScanLists(codeplugData.scanLists); + setContacts(codeplugData.contacts); + setDigitalEmergencies(codeplugData.digitalEmergencies); + if (codeplugData.digitalEmergencyConfig) { + setDigitalEmergencyConfig(codeplugData.digitalEmergencyConfig); + } + setAnalogEmergencies(codeplugData.analogEmergencies); + if (codeplugData.radioSettings) { + setRadioSettings(codeplugData.radioSettings); + } + setRadioInfo(codeplugData.radioInfo ?? null); + setMessages(codeplugData.messages ?? []); + setRadioIds(codeplugData.radioIds ?? []); + setQuickContacts(codeplugData.quickContacts ?? []); + setRXGroups(codeplugData.rxGroups ?? []); + setEncryptionKeys(codeplugData.encryptionKeys ?? []); + }; + const handleFileSelect = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; @@ -151,25 +173,7 @@ function App() { const { importCodeplug } = await import('./services/codeplugExport'); const codeplugData = await importCodeplug(file); - // Populate all stores with imported data - setChannels(codeplugData.channels); - setZones(codeplugData.zones); - setScanLists(codeplugData.scanLists); - setContacts(codeplugData.contacts); - setDigitalEmergencies(codeplugData.digitalEmergencies); - if (codeplugData.digitalEmergencyConfig) { - setDigitalEmergencyConfig(codeplugData.digitalEmergencyConfig); - } - setAnalogEmergencies(codeplugData.analogEmergencies); - if (codeplugData.radioSettings) { - setRadioSettings(codeplugData.radioSettings); - } - setRadioInfo(codeplugData.radioInfo ?? null); - setMessages(codeplugData.messages ?? []); - setRadioIds(codeplugData.radioIds ?? []); - setQuickContacts(codeplugData.quickContacts ?? []); - setRXGroups(codeplugData.rxGroups ?? []); - setEncryptionKeys(codeplugData.encryptionKeys ?? []); + applyCodeplugToStores(codeplugData); setShowStartupModal(false); const { saveSnapshot } = await import('./services/codeplugSnapshots'); @@ -241,25 +245,8 @@ function App() { setZones(sampleZones); }; - const handleRestoreSnapshot = (codeplugData: import('./services/codeplugExport').CodeplugData) => { - setChannels(codeplugData.channels); - setZones(codeplugData.zones); - setScanLists(codeplugData.scanLists); - setContacts(codeplugData.contacts); - setDigitalEmergencies(codeplugData.digitalEmergencies); - if (codeplugData.digitalEmergencyConfig) { - setDigitalEmergencyConfig(codeplugData.digitalEmergencyConfig); - } - setAnalogEmergencies(codeplugData.analogEmergencies); - if (codeplugData.radioSettings) { - setRadioSettings(codeplugData.radioSettings); - } - setRadioInfo(codeplugData.radioInfo ?? null); - setMessages(codeplugData.messages ?? []); - setRadioIds(codeplugData.radioIds ?? []); - setQuickContacts(codeplugData.quickContacts ?? []); - setRXGroups(codeplugData.rxGroups ?? []); - setEncryptionKeys(codeplugData.encryptionKeys ?? []); + const handleRestoreSnapshot = (codeplugData: CodeplugData) => { + applyCodeplugToStores(codeplugData); setShowStartupModal(false); setShowPickRadioModal(false); }; diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 9928f31..c8f7c2f 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -51,7 +51,7 @@ export function useRadioConnection() { const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); - const { selectedRadioModel, preferredTransport, radioInfo, setConnected, setRadioInfo, setSettings, setRawRadioSettingsData, setRawContactBlockData, setRawContactBlocks, setBlockMetadata, setBlockData, setWriteBlockData, setZoneComparisonData, setBootImageRaw, setBootImageDescription, setConnectionError } = useRadioStore(); + const { selectedRadioModel, preferredTransport, radioInfo, setConnected, setRadioInfo, setRawRadioSettingsData, setRawContactBlockData, setRawContactBlocks, setBlockMetadata, setBlockData, setWriteBlockData, setZoneComparisonData, setBootImageRaw, setBootImageDescription, setConnectionError } = useRadioStore(); const { setChannels, setRawChannelData } = useChannelsStore(); const { setZones, setRawZoneData } = useZonesStore(); const { setScanLists, setRawScanListData } = useScanListsStore(); @@ -111,457 +111,178 @@ export function useRadioConnection() { }; document.addEventListener('visibilitychange', onVisibilityChange); - // Define steps once - this is the single source of truth - // 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. + // Read model from live store — selectedRadioModel may be null if the user never explicitly + // used the picker (UI pre-selects it via useEffectiveRadioModel but doesn't write the store). + // Fall back to the 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(); - - // 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 - // Use effectiveModel for transport selection (may be null on first connect; that's OK — we - // re-resolve caps below after getRadioInfo() returns the actual model from the radio). - let caps = getCapabilitiesForModel(effectiveModel); - const transport = caps?.supportsBle - ? (preferredTransport ?? caps?.preferredTransport ?? 'serial') - : undefined; - onProgress?.( - 5, - transport === 'ble' ? 'Select BLE device...' : 'Select serial port...', - steps[0] - ); - await protocol.connect({ - forcePortSelection: true, - ...(transport != null && { transport }), - }); - - // Step 2: Get radio info + // All data-reading steps after connect() are extracted here so both the first attempt + // and the retry go through exactly the same code path. + const performRead = async (proto: RadioProtocol) => { onProgress?.(10, 'Reading radio information...', steps[2]); - const radioInfo = await protocol.getRadioInfo(); - - setRadioInfo(radioInfo); + const info = await proto.getRadioInfo(); + setRadioInfo(info); setConnected(true); - // Re-resolve caps using the actual model string returned by the radio. - // effectiveModel is null when the user hasn't previously connected (no stored radioInfo), - // so caps would be null and bulk read would be skipped. Use radioInfo.model if available. - if (radioInfo.model) { - caps = getCapabilitiesForModel(radioInfo.model) ?? caps; - } + // Resolve caps from the actual model the radio reported — effectiveModel may be null + // on first connect, which would cause bulk read to be skipped if we used it here. + const caps = getCapabilitiesForModel(info.model ?? effectiveModel); - // 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') { + if (caps?.supportsBulkRead && typeof (proto as any).bulkReadRequiredBlocks === 'function') { onProgress?.(15, 'Reading all memory blocks...', steps[3]); - await (protocol as any).bulkReadRequiredBlocks(); + await (proto as any).bulkReadRequiredBlocks(); } - // Step 5: Parse channels (from cache after bulk read, or over connection) onProgress?.(20, 'Parsing channels...', steps[4]); - const channels = await protocol.readChannels(); + const channels = await proto.readChannels(); setChannels(channels); - // Enrich radioInfo with firmware from cached image (UV5R-Mini; getRadioInfo may have missed it) - if (typeof (protocol as any).getFirmwareFromCache === 'function') { - const fw = (protocol as any).getFirmwareFromCache(); + // Enrich radioInfo with firmware from cached image (UV5R-Mini path) + if (typeof (proto as any).getFirmwareFromCache === 'function') { + const fw = (proto as any).getFirmwareFromCache(); if (fw) { - const store = useRadioStore.getState(); - const current = store.radioInfo; + const current = useRadioStore.getState().radioInfo; if (current) setRadioInfo({ ...current, firmware: fw }); } } - // Store raw channel data for debug export - if ((protocol as any).rawChannelData) { - setRawChannelData((protocol as any).rawChannelData); - } - // Store all block metadata and data for debug export - if ((protocol as any).allBlockMetadata) { - const metadata = (protocol as any).allBlockMetadata; - // Create a new Map to ensure Zustand stores it properly - const metadataCopy = new Map(metadata); - setBlockMetadata(metadataCopy); - } - if ((protocol as any).allBlockData) { - const data = (protocol as any).allBlockData; - // Create a new Map to ensure Zustand stores it properly - const dataCopy = new Map(data); - setBlockData(dataCopy); - } + if ((proto as any).rawChannelData) setRawChannelData((proto as any).rawChannelData); + if ((proto as any).allBlockMetadata) setBlockMetadata(new Map((proto as any).allBlockMetadata)); + if ((proto as any).allBlockData) setBlockData(new Map((proto as any).allBlockData)); - // Step 6: Parse configuration (zones, scan lists, quick messages, etc.) - // Suppress detailed messages and only show high-level progress - const originalConfigProgress = protocol.onProgress; - protocol.onProgress = (progress, _message) => { - // Only update progress percentage, don't forward detailed messages - const overallProgress = 70 + (progress * 0.25); // 70% to 95% - // Only forward progress percentage, keep the high-level message - onProgress?.(overallProgress, 'Parsing configuration...', steps[5]); + // Suppress per-item progress messages during config parsing; only surface the percentage. + const savedProgress = proto.onProgress; + proto.onProgress = (progress, _msg) => { + onProgress?.(70 + (progress * 0.25), 'Parsing configuration...', steps[5]); }; onProgress?.(70, 'Parsing configuration from cache...', steps[5]); - - // Read zones - const zones = await protocol.readZones(); + + const zones = await proto.readZones(); setZones(zones); - // Store raw zone data for debug export - if ((protocol as any).rawZoneData) { - setRawZoneData((protocol as any).rawZoneData); - } + if ((proto as any).rawZoneData) setRawZoneData((proto as any).rawZoneData); - // Read scan lists - const scanLists = await protocol.readScanLists(); + const scanLists = await proto.readScanLists(); setScanLists(scanLists); - // Store raw scan list data for debug export - if ((protocol as any).rawScanListData) { - setRawScanListData((protocol as any).rawScanListData); - } - // Update blockData with scan list blocks - if ((protocol as any).blockData) { - setBlockData((protocol as any).blockData); - } + if ((proto as any).rawScanListData) setRawScanListData((proto as any).rawScanListData); + if ((proto as any).blockData) setBlockData((proto as any).blockData); - // Read quick messages (optional - don't fail if missing) try { - const messages = await (protocol as any).readQuickMessages(); + const messages = await (proto as any).readQuickMessages(); setMessages(messages); - // Store raw message data for debug export - const rawDataMap = new Map(); - for (const [index, rawData] of (protocol as any).rawMessageData.entries()) { - rawDataMap.set(index, rawData); - } - setRawMessageData(rawDataMap); - } catch (err) { - // Quick messages are optional - log error but don't fail the entire read - console.warn('Failed to read quick messages:', err); - } + const rawMsgMap = new Map(); + for (const [i, raw] of (proto as any).rawMessageData.entries()) rawMsgMap.set(i, raw); + setRawMessageData(rawMsgMap); + } catch { console.warn('Could not read Quick Messages'); } - // Read DMR Radio IDs (optional - don't fail if missing) try { - const radioIds = await protocol.readDMRRadioIDs(); + const radioIds = await proto.readDMRRadioIDs(); setRadioIds(radioIds); - // Store raw radio ID data for debug export - const rawIdDataMap = new Map(); - for (const [index, rawData] of (protocol as any).rawDMRRadioIDData.entries()) { - rawIdDataMap.set(index, rawData); - } - setRawRadioIdData(rawIdDataMap); - } catch (err) { - // DMR Radio IDs are optional - log error but don't fail the entire read - console.warn('Failed to read DMR Radio IDs:', err); - } + const rawIdMap = new Map(); + for (const [i, raw] of (proto as any).rawDMRRadioIDData.entries()) rawIdMap.set(i, raw); + setRawRadioIdData(rawIdMap); + } catch { console.warn('Could not read DMR Radio IDs'); } - // Read calibration data (optional - don't fail if missing) try { - const calibration = await (protocol as any).readCalibration(); - setCalibration(calibration); - } catch (err) { - // Calibration is optional - log error but don't fail the entire read - console.warn('Failed to read calibration data:', err); - } + setCalibration(await (proto as any).readCalibration()); + } catch { console.warn('Could not read calibration data'); } - // Read DMR RX Groups (optional - don't fail if missing) try { - const rxGroups = await (protocol as any).readRXGroups(); + const rxGroups = await (proto as any).readRXGroups(); setRXGroups(rxGroups); - // Store raw DMR RX group data for debug export - const rawGroupDataMap = new Map(); - for (const [index, rawData] of (protocol as any).rawRXGroupData.entries()) { - rawGroupDataMap.set(index, rawData); - } - setRawGroupData(rawGroupDataMap); - } catch (err) { - console.warn('Could not read RX Groups:', err); - } + const rawGroupMap = new Map(); + for (const [i, raw] of (proto as any).rawRXGroupData.entries()) rawGroupMap.set(i, raw); + setRawGroupData(rawGroupMap); + } catch { console.warn('Could not read RX Groups'); } - // Read Talk Groups (metadata 0x44) try { - const quickContacts = await (protocol as any).readQuickContacts(); - setQuickContacts(quickContacts); - } catch (err) { - console.warn('Could not read Talk Groups:', err); - } + setQuickContacts(await (proto as any).readQuickContacts()); + } catch { console.warn('Could not read Talk Groups'); } - // Step 7: Read configuration blocks (Radio Settings, Emergency Systems, etc.) try { onProgress?.(90, 'Reading configuration...', 'Reading configuration'); - - // Read Radio Settings (for Radio Boot Text) + try { - const radioSettings = await protocol.readRadioSettings(); - if (radioSettings) { - setRadioSettings(radioSettings); - } - // Store raw radio settings data for diagnostics - if ((protocol as any).rawRadioSettingsData) { - setRawRadioSettingsData((protocol as any).rawRadioSettingsData); - } - } catch (err) { - // Radio settings are optional - don't fail the entire read if they're missing or cause errors - console.warn('Could not read Radio Settings:', err); - } + const radioSettings = await proto.readRadioSettings(); + if (radioSettings) setRadioSettings(radioSettings); + if ((proto as any).rawRadioSettingsData) setRawRadioSettingsData((proto as any).rawRadioSettingsData); + } catch { console.warn('Could not read Radio Settings'); } - // Read Digital Emergency Systems try { - const digitalEmergency = await (protocol as any).readDigitalEmergencies(); + const digitalEmergency = await (proto as any).readDigitalEmergencies(); if (digitalEmergency) { setDigitalEmergencies(digitalEmergency.systems); setDigitalEmergencyConfig(digitalEmergency.config); } - } catch (err) { - console.warn('Could not read Digital Emergency Systems:', err); - } + } catch { console.warn('Could not read Digital Emergency Systems'); } - // Read Analog Emergency Systems try { - const analogEmergencies = await (protocol as any).readAnalogEmergencies(); - if (analogEmergencies) { - setAnalogEmergencies(analogEmergencies); - } - } catch (err) { - console.warn('Could not read Analog Emergency Systems:', err); - } - - // Update blockData with all configuration blocks for debug export - if ((protocol as any).blockData) { - setBlockData((protocol as any).blockData); - } - } catch (err) { - // Configuration blocks are optional - don't fail the entire read if they're missing or cause errors - console.warn('Error reading configuration blocks:', err); - } + const analogEmergencies = await (proto as any).readAnalogEmergencies(); + if (analogEmergencies) setAnalogEmergencies(analogEmergencies); + } catch { console.warn('Could not read Analog Emergency Systems'); } - // Restore original progress handler - protocol.onProgress = originalConfigProgress; + if ((proto as any).blockData) setBlockData((proto as any).blockData); + } catch { console.warn('Error reading configuration blocks'); } - // Step 6: Complete (contacts are read separately on demand) + proto.onProgress = savedProgress; onProgress?.(100, 'Read complete!', steps[5]); + }; + + try { + protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol(); + protocol.onProgress = (progress, message) => onProgress?.(progress, message); + + // caps here is only used for transport selection — re-resolved inside performRead + // from the actual model string the radio returns. + const caps = getCapabilitiesForModel(effectiveModel); + const transport = caps?.supportsBle + ? (preferredTransport ?? caps?.preferredTransport ?? 'serial') + : undefined; + onProgress?.(5, transport === 'ble' ? 'Select BLE device...' : 'Select serial port...', steps[0]); + await protocol.connect({ forcePortSelection: true, ...(transport != null && { transport }) }); + + await performRead(protocol); } catch (err) { const rawMessage = err instanceof Error ? err.message : 'Read failed'; const errorMessage = withVisibilityContext(rawMessage, tabWentHiddenDuringOperation); const isPortSelectionCancelled = rawMessage.includes('cancelled') || rawMessage.includes('Port selection cancelled'); - - // 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); - // Try to disconnect the failed connection - try { - await protocol.disconnect(); - } catch (disconnectErr) { - console.warn('Error during disconnect cleanup:', disconnectErr); - } + if (!isPortSelectionCancelled && protocol) { + console.warn('Read failed, will retry:', errorMessage); + try { await protocol.disconnect(); } catch { /* ignore */ } - // 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) + onProgress?.(5, 'Retrying...', steps[0]); protocol = createProtocolForModel(effectiveModel ?? '') ?? createDefaultProtocol(); - protocol.onProgress = (progress, message) => { - onProgress?.(progress, message); - }; - - // Force port selection for retry - (protocol as any).port = null; + protocol.onProgress = (progress, message) => onProgress?.(progress, message); 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); - - // Re-resolve caps from actual radio model (same fix as first attempt above). - const retryCaps = getCapabilitiesForModel(radioInfo.model ?? effectiveModel); - if (retryCaps?.supportsBulkRead && typeof (protocol as any).bulkReadRequiredBlocks === 'function') { - onProgress?.(15, 'Reading all memory blocks...', steps[3]); - await (protocol as any).bulkReadRequiredBlocks(); - } - - onProgress?.(20, 'Parsing channels from cache...', steps[4]); - const channels = await protocol.readChannels(); - setChannels(channels); - if ((protocol as any).rawChannelData) { - setRawChannelData((protocol as any).rawChannelData); - } - if ((protocol as any).allBlockMetadata) { - setBlockMetadata((protocol as any).allBlockMetadata); - } - if ((protocol as any).allBlockData) { - setBlockData((protocol as any).allBlockData); - } - - const originalConfigProgress = protocol.onProgress; - protocol.onProgress = (progress, _message) => { - const overallProgress = 70 + (progress * 0.25); - onProgress?.(overallProgress, 'Parsing configuration...', steps[5]); - }; - - onProgress?.(70, 'Parsing configuration from cache...', steps[5]); - - const zones = await protocol.readZones(); - setZones(zones); - if ((protocol as any).rawZoneData) { - setRawZoneData((protocol as any).rawZoneData); - } - - const scanLists = await protocol.readScanLists(); - setScanLists(scanLists); - if ((protocol as any).rawScanListData) { - setRawScanListData((protocol as any).rawScanListData); - } - if ((protocol as any).blockData) { - setBlockData((protocol as any).blockData); - } - - try { - const messages = await (protocol as any).readQuickMessages(); - setMessages(messages); - const rawDataMap = new Map(); - for (const [index, rawData] of (protocol as any).rawMessageData.entries()) { - rawDataMap.set(index, rawData); - } - setRawMessageData(rawDataMap); - } catch (msgErr) { - console.warn('Could not read Quick Messages:', msgErr); - } - - try { - const radioSettings = await protocol.readRadioSettings(); - setRadioSettings(radioSettings); - if ((protocol as any).rawRadioSettingsData) { - setRawRadioSettingsData((protocol as any).rawRadioSettingsData); - } - } catch (settingsErr) { - console.warn('Could not read Radio Settings:', settingsErr); - } - - try { - const digitalEmergencies = await (protocol as any).readDigitalEmergencies(); - if (digitalEmergencies) { - setDigitalEmergencies(digitalEmergencies.systems); - setDigitalEmergencyConfig(digitalEmergencies.config); - } - } catch (err) { - console.warn('Could not read Digital Emergency Systems:', err); - } - - try { - const radioIds = await protocol.readDMRRadioIDs(); - if (radioIds) { - setRadioIds(radioIds); - } - } catch (err) { - console.warn('Could not read DMR Radio IDs:', err); - } - - try { - const calibration = await (protocol as any).readCalibration(); - if (calibration) { - setCalibration(calibration); - } - } catch (err) { - console.warn('Could not read Calibration:', err); - } - - try { - const rxGroups = await (protocol as any).readRXGroups(); - if (rxGroups) { - setRXGroups(rxGroups); - } - } catch (err) { - console.warn('Could not read RX Groups:', err); - } - - // Read Talk Groups - try { - const quickContacts = await (protocol as any).readQuickContacts(); - if (quickContacts) { - setQuickContacts(quickContacts); - } - } catch (err) { - console.warn('Could not read Talk Groups:', err); - } - - try { - const analogEmergencies = await (protocol as any).readAnalogEmergencies(); - if (analogEmergencies) { - setAnalogEmergencies(analogEmergencies); - } - } catch (err) { - console.warn('Could not read Analog Emergency Systems:', err); - } - - if ((protocol as any).blockData) { - setBlockData((protocol as any).blockData); - } - - protocol.onProgress = originalConfigProgress; - - onProgress?.(100, 'Read complete!', steps[5]); - return; // Success - exit without throwing + await performRead(protocol); + return; } catch (retryErr) { - // Retry also failed, fall through to show error - console.error('Retry with port selection also failed:', retryErr); const retryRawMessage = retryErr instanceof Error ? retryErr.message : 'Read failed'; const retryErrorMessage = withVisibilityContext(retryRawMessage, tabWentHiddenDuringOperation); setError(retryErrorMessage); setConnectionError(retryErrorMessage); onProgress?.(0, `Error: ${retryErrorMessage}`, 'Error'); setIsConnecting(false); - - if (protocol) { - try { - await protocol.disconnect(); - } catch (disconnectErr) { - console.warn('Error during disconnect cleanup:', disconnectErr); - } - } + try { await protocol?.disconnect(); } catch { /* ignore */ } throw retryErr; } } - - // If port selection was cancelled or retry didn't happen, show error + setError(errorMessage); setConnectionError(errorMessage); onProgress?.(0, `Error: ${errorMessage}`, 'Error'); - console.error('Radio read error:', err); - - // Set connecting to false so modal can show error state setIsConnecting(false); - - // Try to disconnect on error (if connection exists) - if (protocol) { - try { - await protocol.disconnect(); - } catch (disconnectErr) { - // Ignore disconnect errors - connection might already be closed - console.warn('Error during disconnect cleanup:', disconnectErr); - } - } - - // Re-throw the error so the caller (Toolbar) can handle it and show error in modal + try { await protocol?.disconnect(); } catch { /* ignore */ } throw err; } finally { document.removeEventListener('visibilitychange', onVisibilityChange); - // Only set connecting to false if we didn't already (success case) - // On error, we set it in the catch block so modal stays open to show error - if (!error) { - setIsConnecting(false); - } + if (!error) setIsConnecting(false); } - }, [selectedRadioModel, preferredTransport, setConnected, setRadioInfo, setSettings, setRawRadioSettingsData, setChannels, setZones, setScanLists, setContacts, setContactsLoaded, setRawChannelData, setRawZoneData, setRawScanListData, setBlockMetadata, setBlockData, setRadioSettings, setDigitalEmergencies, setDigitalEmergencyConfig, setAnalogEmergencies, setMessages, setRawMessageData, setMessagesLoaded, setQuickContacts, setQuickContactsLoaded, setRadioIds, setRawRadioIdData, setRadioIdsLoaded, setCalibration, setCalibrationLoaded, setRXGroups, setRawGroupData, setGroupsLoaded, setConnectionError]); + }, [selectedRadioModel, preferredTransport, setConnected, setRadioInfo, setRawRadioSettingsData, setChannels, setZones, setScanLists, setContacts, setContactsLoaded, setRawChannelData, setRawZoneData, setRawScanListData, setBlockMetadata, setBlockData, setRadioSettings, setDigitalEmergencies, setDigitalEmergencyConfig, setAnalogEmergencies, setMessages, setRawMessageData, setMessagesLoaded, setQuickContacts, setQuickContactsLoaded, setRadioIds, setRawRadioIdData, setRadioIdsLoaded, setCalibration, setCalibrationLoaded, setRXGroups, setRawGroupData, setGroupsLoaded, setConnectionError]); const readContacts = useCallback(async ( onProgress?: (progress: number, message: string) => void diff --git a/src/services/channelMerger.ts b/src/services/channelMerger.ts index 8c792be..f3883fe 100644 --- a/src/services/channelMerger.ts +++ b/src/services/channelMerger.ts @@ -58,65 +58,46 @@ export function mergeOverlappingChannels( const frequencyMap = new Map(); // "rx-tx" -> channel let nextChannelNumber = startChannelNumber; - let debugChannel722 = false; - + // Process all channels from all sets for (const channelSet of channelSets) { for (const channel of channelSet) { const freqKey = `${channel.rxFrequency.toFixed(4)}-${channel.txFrequency.toFixed(4)}`; - - // Debug logging for channel 722 - if (channel.number === 722 || channel.name.includes('151.625')) { - console.log(`[ChannelMerger] Processing channel ${channel.number} "${channel.name}" (${channel.rxFrequency} MHz) - freqKey: ${freqKey}`); - debugChannel722 = true; - } - + if (frequencyMap.has(freqKey)) { // Channel with same frequencies exists - merge them const existingChannel = frequencyMap.get(freqKey)!; const mergedChannel = mergeChannels(existingChannel, channel); - + // Update the merged channel in the map frequencyMap.set(freqKey, mergedChannel); - + // Update the merged channel in the array - const existingIndex = mergedChannels.findIndex(ch => + const existingIndex = mergedChannels.findIndex(ch => ch.number === existingChannel.number ); if (existingIndex >= 0) { mergedChannels[existingIndex] = { ...mergedChannel, number: existingChannel.number }; } - + // Map original channel number to merged channel number channelMapping.set(channel.number, existingChannel.number); - - if (debugChannel722 && channel.number === 722) { - console.log(`[ChannelMerger] Channel 722 DUPLICATE: mapped to existing channel ${existingChannel.number} "${existingChannel.name}"`); - } } else { // New unique frequency - add as new channel const newChannel = { ...channel, number: nextChannelNumber++, }; - + frequencyMap.set(freqKey, newChannel); mergedChannels.push(newChannel); - + // Map original channel number to new channel number channelMapping.set(channel.number, newChannel.number); - - if (debugChannel722 && channel.number === 722) { - console.log(`[ChannelMerger] Channel 722 NEW: assigned new number ${newChannel.number}`); - } } } } - if (debugChannel722) { - console.log(`[ChannelMerger] Final mapping for 722:`, channelMapping.get(722)); - } - return { mergedChannels, channelMapping }; } diff --git a/src/services/csv/csvImporter.ts b/src/services/csv/csvImporter.ts index 168b928..d9004e8 100644 --- a/src/services/csv/csvImporter.ts +++ b/src/services/csv/csvImporter.ts @@ -8,57 +8,13 @@ export interface ImportResult { } export function parseCSV(content: string): string[][] { - const lines: string[] = []; - let currentLine = ''; - let inQuotes = false; - - for (let i = 0; i < content.length; i++) { - const char = content[i]; - const nextChar = content[i + 1]; - - if (char === '"') { - if (inQuotes && nextChar === '"') { - currentLine += '"'; - i++; // Skip next quote - } else { - inQuotes = !inQuotes; - } - } else if (char === ',' && !inQuotes) { - lines.push(currentLine); - currentLine = ''; - } else if (char === '\n' && !inQuotes) { - lines.push(currentLine); - currentLine = ''; - } else { - currentLine += char; - } - } - if (currentLine) { - lines.push(currentLine); - } - - // Split into rows - const rows: string[][] = []; - let row: string[] = []; - for (const line of lines) { - row.push(line.trim()); - if (line.includes('\n') || row.length > 50) { // Assume max 50 columns - rows.push(row); - row = []; - } - } - if (row.length > 0) { - rows.push(row); - } - - // Simple approach: split by newlines first, then by commas return content.split('\n') .filter(line => line.trim()) .map(line => { const result: string[] = []; let current = ''; let inQuotes = false; - + for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { diff --git a/src/services/jsonLoader.ts b/src/services/jsonLoader.ts index d373005..f2b51e8 100644 --- a/src/services/jsonLoader.ts +++ b/src/services/jsonLoader.ts @@ -142,7 +142,6 @@ export async function loadJsonFile( for (const path of pathsToTry) { try { const data = await tryLoadFromUrl(path, onProgress); - console.log(`Successfully loaded ${filename} from ${path}`); return data as T; } catch (error) { // Store the error but continue trying other paths diff --git a/src/services/locationChannelGenerator.ts b/src/services/locationChannelGenerator.ts deleted file mode 100644 index 42c7b88..0000000 --- a/src/services/locationChannelGenerator.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Location-based Channel and Zone Generator - * Converts repeater data into channels and zones - */ - -import type { Channel, Zone } from '../models'; -import type { Repeater } from './repeaterFinder'; -import { createDefaultChannel } from '../utils/channelHelpers'; -import { generateZoneId } from '../utils/zoneHelpers'; -import { getStandardOffset } from './repeaterFinder'; -import { isValidFrequencyRange } from './validation/frequencyValidator'; - -export interface GenerationOptions { - startChannelNumber?: number; // Starting channel number (default: find next available) - groupByDistance?: boolean; // Group repeaters into zones by distance - groupByBand?: boolean; // Group repeaters into zones by band - maxDistancePerZone?: number; // Max distance in miles for zone grouping (default: 25) - zoneNameTemplate?: string; // Template for zone names (default: "{band} - {region}") -} - -export interface GenerationResult { - channels: Channel[]; - zones: Zone[]; - summary: { - channelsCreated: number; - zonesCreated: number; - repeatersProcessed: number; - }; -} - -/** - * Generate channels from repeaters - */ -export function generateChannelsFromRepeaters( - repeaters: Repeater[], - existingChannels: Channel[], - options: GenerationOptions = {} -): Channel[] { - const { - startChannelNumber, - } = options; - - // Find starting channel number - const existingNumbers = new Set(existingChannels.map(ch => ch.number)); - let nextChannelNumber = startChannelNumber || 1; - while (existingNumbers.has(nextChannelNumber)) { - nextChannelNumber++; - } - - const channels: Channel[] = []; - - for (const repeater of repeaters) { - // Calculate input frequency (user TX, repeater RX) - const inputOffset = repeater.inputOffset || getStandardOffset(repeater.band); - const txFrequency = repeater.frequency + inputOffset; - const rxFrequency = repeater.frequency; - - // Filter out repeaters with frequencies outside supported ranges (87-174 MHz or 400-470 MHz) - if (!isValidFrequencyRange(rxFrequency) || !isValidFrequencyRange(txFrequency)) { - console.warn(`Skipping repeater ${repeater.callsign} - frequency ${rxFrequency.toFixed(4)} MHz outside supported range (87-174 MHz or 400-470 MHz)`); - continue; - } - - // Determine mode - let mode: Channel['mode'] = 'Analog'; - if (repeater.mode === 'DMR') mode = 'Digital'; - else if (repeater.mode === 'D-STAR' || repeater.mode === 'C4FM' || repeater.mode === 'P25' || repeater.mode === 'NXDN') { - mode = 'Fixed Digital'; - } - - // Parse CTCSS/DCS - let rxCtcssDcs: Channel['rxCtcssDcs'] = { type: 'None' }; - let txCtcssDcs: Channel['txCtcssDcs'] = { type: 'None' }; - - if (repeater.ctcss) { - rxCtcssDcs = { type: 'CTCSS', value: repeater.ctcss }; - txCtcssDcs = { type: 'CTCSS', value: repeater.ctcss }; - } else if (repeater.dcs) { - rxCtcssDcs = { type: 'DCS', value: repeater.dcs, polarity: 'N' }; - txCtcssDcs = { type: 'DCS', value: repeater.dcs, polarity: 'N' }; - } - - // Create channel name - let channelName = repeater.callsign; - if (repeater.location) { - channelName += ` ${repeater.location}`; - } - if (channelName.length > 16) { - channelName = channelName.substring(0, 16); - } - - // Determine bandwidth based on band - let bandwidth: Channel['bandwidth'] = '25kHz'; - if (repeater.band === '2m' || repeater.band === '70cm') { - bandwidth = '25kHz'; // Most modern repeaters use 25kHz - } else if (repeater.band === '6m' || repeater.band === '10m') { - bandwidth = '25kHz'; - } - - // Create channel - const channel = createDefaultChannel({ - number: nextChannelNumber++, - name: channelName, - rxFrequency, - txFrequency, - mode, - bandwidth, - rxCtcssDcs, - txCtcssDcs, - power: 'High', // Repeaters typically need high power - scanAdd: true, // Add to scan by default - }); - - channels.push(channel); - } - - return channels; -} - -/** - * Generate zones from repeaters and channels - */ -export function generateZonesFromRepeaters( - repeaters: Repeater[], - channels: Channel[], - options: GenerationOptions = {} -): Zone[] { - const { - groupByDistance = false, - groupByBand = true, - maxDistancePerZone = 25, - } = options; - - const zones: Zone[] = []; - - if (groupByBand) { - // Group by band (2m, 70cm, etc.) - const bands = new Set(repeaters.map(r => r.band)); - - for (const band of bands) { - const bandRepeaters = repeaters.filter(r => r.band === band); - const bandChannels = channels.filter(ch => { - const repeater = bandRepeaters.find(r => { - const offset = r.inputOffset || getStandardOffset(r.band); - return Math.abs(ch.rxFrequency - r.frequency) < 0.001 && - Math.abs(ch.txFrequency - (r.frequency + offset)) < 0.001; - }); - return !!repeater; - }); - - if (bandChannels.length > 0) { - zones.push({ - id: generateZoneId(), - name: `${band.toUpperCase()} Repeaters`, - channels: bandChannels.map(ch => ch.number), - }); - } - } - } else if (groupByDistance) { - // Group by distance regions - const sortedRepeaters = [...repeaters].sort((a, b) => - (a.distance || 0) - (b.distance || 0) - ); - - let currentZoneRepeaters: Repeater[] = []; - let currentZoneStartDistance = 0; - - for (const repeater of sortedRepeaters) { - const distance = repeater.distance || 0; - - if (currentZoneRepeaters.length === 0) { - currentZoneRepeaters = [repeater]; - currentZoneStartDistance = distance; - } else if (distance - currentZoneStartDistance <= maxDistancePerZone) { - currentZoneRepeaters.push(repeater); - } else { - // Create zone from current group - const zoneChannels = channels.filter(ch => { - return currentZoneRepeaters.some(r => { - const offset = r.inputOffset || getStandardOffset(r.band); - return Math.abs(ch.rxFrequency - r.frequency) < 0.001 && - Math.abs(ch.txFrequency - (r.frequency + offset)) < 0.001; - }); - }); - - if (zoneChannels.length > 0) { - const zoneName = `${Math.round(currentZoneStartDistance)}-${Math.round(currentZoneStartDistance + maxDistancePerZone)}mi`; - zones.push({ - id: generateZoneId(), - name: zoneName.substring(0, 10), // Limit to 10 chars - channels: zoneChannels.map(ch => ch.number), - }); - } - - // Start new zone - currentZoneRepeaters = [repeater]; - currentZoneStartDistance = distance; - } - } - - // Create final zone - if (currentZoneRepeaters.length > 0) { - const zoneChannels = channels.filter(ch => { - return currentZoneRepeaters.some(r => { - const offset = r.inputOffset || getStandardOffset(r.band); - return Math.abs(ch.rxFrequency - r.frequency) < 0.001 && - Math.abs(ch.txFrequency - (r.frequency + offset)) < 0.001; - }); - }); - - if (zoneChannels.length > 0) { - const zoneName = `${Math.round(currentZoneStartDistance)}+mi`; - zones.push({ - id: generateZoneId(), - name: zoneName.substring(0, 10), // Limit to 10 chars - channels: zoneChannels.map(ch => ch.number), - }); - } - } - } else { - // Single zone with all repeaters - zones.push({ - id: generateZoneId(), - name: 'Loc Rptrs', - channels: channels.map(ch => ch.number), - }); - } - - return zones; -} - -/** - * Generate channels and zones from repeaters - */ -export function generateChannelsAndZones( - repeaters: Repeater[], - existingChannels: Channel[], - options: GenerationOptions = {} -): GenerationResult { - const channels = generateChannelsFromRepeaters(repeaters, existingChannels, options); - const zones = generateZonesFromRepeaters(repeaters, channels, options); - - return { - channels, - zones, - summary: { - channelsCreated: channels.length, - zonesCreated: zones.length, - repeatersProcessed: repeaters.length, - }, - }; -} - From 1bd4a78338cb3031205163ac3df586c5f9dfdc6a Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Fri, 22 May 2026 12:04:05 -0700 Subject: [PATCH 2/2] Fixes a write edit write bug for settings --- src/store/radioSettingsStore.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/radioSettingsStore.ts b/src/store/radioSettingsStore.ts index 76f8a7a..a896ba1 100644 --- a/src/store/radioSettingsStore.ts +++ b/src/store/radioSettingsStore.ts @@ -90,6 +90,9 @@ export const useRadioSettingsStore = create((set, get) => ({ const { changedFields } = get(); return Array.from(changedFields); }, - clearChanges: () => set({ changedFields: new Set() }), + clearChanges: () => set((state) => ({ + changedFields: new Set(), + originalSettings: state.settings ? JSON.parse(JSON.stringify(state.settings)) : null, + })), }));