diff --git a/src/components/about/AboutTab.tsx b/src/components/about/AboutTab.tsx index 6377ea8..08e734b 100644 --- a/src/components/about/AboutTab.tsx +++ b/src/components/about/AboutTab.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useDebugStore } from '../../store/debugStore'; +import { useOutOfBandStore } from '../../store/outOfBandStore'; import { Card } from '../ui/Card'; import { SectionTitle } from '../ui/SectionTitle'; import { Button } from '../ui/Button'; @@ -15,6 +16,7 @@ const OFFLINE_VERSION_URL = 'https://infamy.github.io/NeonPlug/'; export const AboutTab: React.FC = () => { const { debugMode, setDebugMode } = useDebugStore(); + const { allowOutOfBandFrequencies, setAllowOutOfBandFrequencies } = useOutOfBandStore(); const [offlineFallbackOpen, setOfflineFallbackOpen] = useState(false); return ( @@ -299,6 +301,35 @@ npm run build:single )} + + {/* Out of Band Frequencies */} +
+

Out of Band Frequencies

+
+

+ Enable this if your radio has modified firmware that supports frequencies outside the + standard VHF (87–174 MHz) and UHF (400–470 MHz) ranges. When enabled, out-of-band + channels will be read, displayed, and written without filtering. This setting is saved + with your codeplug. +

+ + {allowOutOfBandFrequencies && ( +

+ Out of band frequencies are enabled. No frequency filtering will be applied on read or write. +

+ )} +
+
= ({ const effectiveModel = useEffectiveRadioModel(); const { settings: radioSettings, updateSettings } = useRadioSettingsStore(); const caps = getCapabilitiesForModel(effectiveModel); - const bandLimits = caps?.bandLimits ?? null; + const { allowOutOfBandFrequencies } = useOutOfBandStore(); + const bandLimits = allowOutOfBandFrequencies ? null : (caps?.bandLimits ?? null); const maxChannels = caps?.maxChannels ?? 4000; const analogOnly = caps?.analogOnly === true; const { scanLists } = useScanListsStore(); diff --git a/src/components/layout/Toolbar.tsx b/src/components/layout/Toolbar.tsx index bb1396b..ee2f576 100644 --- a/src/components/layout/Toolbar.tsx +++ b/src/components/layout/Toolbar.tsx @@ -21,6 +21,7 @@ import { migrateCodeplug, type MigrationLoss } from '../../services/codeplugMigr import { saveSnapshot, getSnapshots, getSnapshotData, clearSnapshots, type SnapshotEventType } from '../../services/codeplugSnapshots'; // Codeplug export/import are lazy loaded when needed import { useRadioConnection } from '../../hooks/useRadioConnection'; +import { useOutOfBandStore } from '../../store/outOfBandStore'; import { ReadProgressModal } from '../ui/ReadProgressModal'; import { ConfirmModal } from '../ui/ConfirmModal'; import { isWebSerialSupported } from '../../utils/browserSupport'; @@ -108,6 +109,7 @@ export const Toolbar: React.FC = () => { encryptionKeys, exportDate: new Date().toISOString(), version: '1.0.0', + allowOutOfBandFrequencies: useOutOfBandStore.getState().allowOutOfBandFrequencies || undefined, }); const buildCodeplugDataFromStores = () => { @@ -141,6 +143,7 @@ export const Toolbar: React.FC = () => { encryptionKeys: eks.keys, exportDate: new Date().toISOString(), version: '1.0.0', + allowOutOfBandFrequencies: useOutOfBandStore.getState().allowOutOfBandFrequencies || undefined, }; }; @@ -226,6 +229,9 @@ export const Toolbar: React.FC = () => { setQuickContacts(codeplugData.quickContacts ?? []); setRXGroups(codeplugData.rxGroups ?? []); setEncryptionKeys(codeplugData.encryptionKeys ?? []); + if (codeplugData.allowOutOfBandFrequencies) { + useOutOfBandStore.getState().setAllowOutOfBandFrequencies(true); + } const digCount = codeplugData.digitalEmergencies?.length ?? 0; const analogCount = codeplugData.analogEmergencies?.length ?? 0; diff --git a/src/hooks/useRadioConnection.ts b/src/hooks/useRadioConnection.ts index 4963e71..8d916a5 100644 --- a/src/hooks/useRadioConnection.ts +++ b/src/hooks/useRadioConnection.ts @@ -21,6 +21,7 @@ import type { Channel } from '../models/Channel'; import type { Zone } from '../models/Zone'; import type { ScanList } from '../models/ScanList'; import { isValidChannelFrequency } from '../services/validation/frequencyValidator'; +import { useOutOfBandStore } from '../store/outOfBandStore'; import { parseBootImageHeader } from '../utils/bootImage'; /** Augment error message when tab was hidden during a serial operation (better reporting). */ @@ -167,6 +168,18 @@ export function useRadioConnection() { onProgress?.(20, 'Parsing channels...', steps[4]); const channels = await protocol.readChannels(); setChannels(channels); + + // Auto-enable OOB flag if the radio has channels outside standard band limits + { + const { allowOutOfBandFrequencies, setAllowOutOfBandFrequencies } = useOutOfBandStore.getState(); + if (!allowOutOfBandFrequencies) { + const readBandLimits = getCapabilitiesForModel(effectiveModel)?.bandLimits; + if (readBandLimits && channels.some(ch => !isValidChannelFrequency(ch, readBandLimits))) { + setAllowOutOfBandFrequencies(true); + } + } + } + // 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(); @@ -779,10 +792,13 @@ export function useRadioConnection() { try { // Filter channels to only include those with valid frequencies (use effective model for capabilities) const effectiveModel = radioInfo?.model ?? selectedRadioModel ?? null; - const bandLimits = getCapabilitiesForModel(effectiveModel)?.bandLimits; - const validChannels = channels.filter(ch => isValidChannelFrequency(ch, bandLimits)); + const { allowOutOfBandFrequencies: writeAllowOob } = useOutOfBandStore.getState(); + const bandLimits = writeAllowOob ? null : getCapabilitiesForModel(effectiveModel)?.bandLimits; + const validChannels = bandLimits + ? channels.filter(ch => isValidChannelFrequency(ch, bandLimits)) + : channels; const filteredCount = channels.length - validChannels.length; - + if (filteredCount > 0) { console.warn(`Filtered out ${filteredCount} channel(s) with frequencies outside supported ranges`); } diff --git a/src/services/codeplugExport.ts b/src/services/codeplugExport.ts index c98378e..c5acb23 100644 --- a/src/services/codeplugExport.ts +++ b/src/services/codeplugExport.ts @@ -36,6 +36,7 @@ export interface CodeplugData { encryptionKeys: EncryptionKey[]; exportDate: string; version: string; + allowOutOfBandFrequencies?: boolean; } const CODEPLUG_VERSION = '1.0.0'; diff --git a/src/store/outOfBandStore.ts b/src/store/outOfBandStore.ts new file mode 100644 index 0000000..7111c71 --- /dev/null +++ b/src/store/outOfBandStore.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand'; + +interface OutOfBandState { + allowOutOfBandFrequencies: boolean; + setAllowOutOfBandFrequencies: (v: boolean) => void; +} + +const loadAllowOob = (): boolean => { + try { + return localStorage.getItem('neonplug-allow-oob') === 'true'; + } catch { + return false; + } +}; + +const saveAllowOob = (v: boolean): void => { + try { + localStorage.setItem('neonplug-allow-oob', v ? 'true' : 'false'); + } catch {} +}; + +export const useOutOfBandStore = create((set) => ({ + allowOutOfBandFrequencies: loadAllowOob(), + setAllowOutOfBandFrequencies: (v) => { + saveAllowOob(v); + set({ allowOutOfBandFrequencies: v }); + }, +}));