From 744056d5d74f5c4b76e8d5b003ed77db0e0cbd35 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Sun, 8 Feb 2026 14:45:12 -0800 Subject: [PATCH] Clean up central --- src/components/channels/ChannelEditModal.tsx | 24 ++++++++ src/components/channels/ChannelsTable.tsx | 5 ++ src/components/digital/DigitalTab.tsx | 27 +++++---- src/radios/dm32uv/memory.ts | 2 +- src/services/validation/channelValidator.ts | 57 +++++++++---------- src/services/validation/codeplugValidator.ts | 2 +- src/services/validation/frequencyValidator.ts | 51 +++-------------- 7 files changed, 82 insertions(+), 86 deletions(-) diff --git a/src/components/channels/ChannelEditModal.tsx b/src/components/channels/ChannelEditModal.tsx index ff1dbf5..b3a7c67 100644 --- a/src/components/channels/ChannelEditModal.tsx +++ b/src/components/channels/ChannelEditModal.tsx @@ -6,6 +6,8 @@ import type { EncryptionKey } from '../../models/EncryptionKey'; import type { QuickContact } from '../../models/QuickContact'; import { CTCSS_FREQUENCIES, DCS_CODES, formatCTCSSFrequency, formatDCSCode } from '../../utils/ctcssConstants'; import { isNoTxFrequency, isRxInNoTxBand } from '../../services/validation/frequencyValidator'; +import { validateChannel, type ValidationError } from '../../services/validation/channelValidator'; +import type { RadioBandLimits } from '../../types/radioCapabilities'; // Frequency input component that only updates parent on blur interface FrequencyInputProps { @@ -50,6 +52,8 @@ interface ChannelEditModalProps { onClose: () => void; channel: Channel; onSave: (channel: Channel) => void; + /** Band limits from radio capabilities (getCapabilitiesForModel(radioInfo?.model)?.bandLimits). */ + bandLimits?: RadioBandLimits | null; rxGroups?: RXGroup[]; encryptionKeys?: EncryptionKey[]; talkGroups?: QuickContact[]; @@ -60,11 +64,13 @@ export const ChannelEditModal: React.FC = ({ onClose, channel, onSave, + bandLimits = null, rxGroups = [], encryptionKeys = [], talkGroups = [], }) => { const [editedChannel, setEditedChannel] = React.useState(channel); + const [validationErrors, setValidationErrors] = React.useState([]); React.useEffect(() => { const updatedChannel = { ...channel }; @@ -75,13 +81,21 @@ export const ChannelEditModal: React.FC = ({ updatedChannel.name = 'VFO B'; } setEditedChannel(updatedChannel); + setValidationErrors([]); }, [channel]); const handleChange = (field: keyof Channel, value: any) => { setEditedChannel(prev => ({ ...prev, [field]: value })); + if (validationErrors.length > 0) setValidationErrors([]); }; const handleSave = () => { + const errors = validateChannel(editedChannel, bandLimits); + if (errors.length > 0) { + setValidationErrors(errors); + return; + } + setValidationErrors([]); onSave(editedChannel); onClose(); }; @@ -110,6 +124,16 @@ export const ChannelEditModal: React.FC = ({ >
+ {validationErrors.length > 0 && ( +
+

Please fix the following:

+
    + {validationErrors.map((e, i) => ( +
  • {e.field}: {e.message}
  • + ))} +
+
+ )}
{/* Basic Information */}
diff --git a/src/components/channels/ChannelsTable.tsx b/src/components/channels/ChannelsTable.tsx index 86d5ea1..af851f9 100644 --- a/src/components/channels/ChannelsTable.tsx +++ b/src/components/channels/ChannelsTable.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useRef } from 'react'; import { useChannelsStore } from '../../store/channelsStore'; +import { useRadioStore } from '../../store/radioStore'; import { useRadioSettingsStore } from '../../store/radioSettingsStore'; import { useScanListsStore } from '../../store/scanListsStore'; +import { getCapabilitiesForModel } from '../../radios/capabilities'; import { useRXGroupsStore } from '../../store/rxGroupsStore'; import { useEncryptionKeysStore } from '../../store/encryptionKeysStore'; import { useQuickContactsStore } from '../../store/quickContactsStore'; @@ -68,7 +70,9 @@ export const ChannelsTable: React.FC = ({ onSelectionChange, }) => { const { channels: channelsFromStore, updateChannel, deleteChannel, addChannel } = useChannelsStore(); + const { radioInfo } = useRadioStore(); const { settings: radioSettings, updateSettings } = useRadioSettingsStore(); + const bandLimits = getCapabilitiesForModel(radioInfo?.model)?.bandLimits ?? null; const { scanLists } = useScanListsStore(); const { groups: rxGroups } = useRXGroupsStore(); const { keys: encryptionKeys } = useEncryptionKeysStore(); @@ -1080,6 +1084,7 @@ export const ChannelsTable: React.FC = ({ updateChannel(updatedChannel.number, updatedChannel); setEditingChannel(null); }} + bandLimits={bandLimits} rxGroups={rxGroups} encryptionKeys={encryptionKeys} talkGroups={talkGroups} diff --git a/src/components/digital/DigitalTab.tsx b/src/components/digital/DigitalTab.tsx index a29b998..95dde6c 100644 --- a/src/components/digital/DigitalTab.tsx +++ b/src/components/digital/DigitalTab.tsx @@ -7,6 +7,7 @@ import { useQuickContactsStore } from '../../store/quickContactsStore'; import { useRXGroupsStore } from '../../store/rxGroupsStore'; import { useQuickMessagesStore } from '../../store/quickMessagesStore'; import { getCapabilitiesForModel } from '../../radios/capabilities'; +import { isValidDMRId } from '../../services/validation/dmrValidator'; import { RXGroupsList } from '../rxgroups/RXGroupsList'; import { Card } from '../ui/Card'; import { SectionTitle } from '../ui/SectionTitle'; @@ -266,22 +267,26 @@ export const DigitalTab: React.FC = () => { value={radioId.dmrId} onChange={(e) => { const dmrIdValue = parseInt(e.target.value, 10); - if (!isNaN(dmrIdValue) && dmrIdValue >= 0 && dmrIdValue <= 0xFFFFFF) { - updateRadioId(radioId.index, { - dmrId: e.target.value, - dmrIdValue: dmrIdValue, - dmrIdBytes: new Uint8Array([ - dmrIdValue & 0xFF, - (dmrIdValue >> 8) & 0xFF, - (dmrIdValue >> 16) & 0xFF, - ]), - }); + if (isNaN(dmrIdValue) || dmrIdValue < 0 || dmrIdValue > 0xFFFFFF) return; + if (dmrIdValue > 0 && !isValidDMRId(dmrIdValue)) { + setAlertMessage('DMR ID must be between 1 and 9,999,999 (0 = none).'); + setAlertOpen(true); + return; } + updateRadioId(radioId.index, { + dmrId: e.target.value, + dmrIdValue: dmrIdValue, + dmrIdBytes: new Uint8Array([ + dmrIdValue & 0xFF, + (dmrIdValue >> 8) & 0xFF, + (dmrIdValue >> 16) & 0xFF, + ]), + }); }} min="0" max="16777215" className="bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan w-full text-xs text-white font-mono" - placeholder="DMR ID (0-16777215)" + placeholder="DMR ID (1-9999999, 0=none)" /> diff --git a/src/radios/dm32uv/memory.ts b/src/radios/dm32uv/memory.ts index 0fc486c..cc57497 100644 --- a/src/radios/dm32uv/memory.ts +++ b/src/radios/dm32uv/memory.ts @@ -242,4 +242,4 @@ export function storeRawData= settings.bandLimits.vhfMin && - channel.rxFrequency <= settings.bandLimits.vhfMax; - const isUHF = channel.rxFrequency >= settings.bandLimits.uhfMin && - channel.rxFrequency <= settings.bandLimits.uhfMax; - + // Band limits validation (from radio capabilities) + if (bandLimits) { + const isVHF = channel.rxFrequency >= bandLimits.vhfMin && channel.rxFrequency <= bandLimits.vhfMax; + const isUHF = channel.rxFrequency >= bandLimits.uhfMin && channel.rxFrequency <= bandLimits.uhfMax; if (!isVHF && !isUHF) { - errors.push({ - field: 'rxFrequency', - message: `RX frequency must be within radio band limits (VHF: ${settings.bandLimits.vhfMin}-${settings.bandLimits.vhfMax} MHz, UHF: ${settings.bandLimits.uhfMin}-${settings.bandLimits.uhfMax} MHz)` + errors.push({ + field: 'rxFrequency', + message: `RX frequency must be within radio band limits (VHF: ${bandLimits.vhfMin}-${bandLimits.vhfMax} MHz, UHF: ${bandLimits.uhfMin}-${bandLimits.uhfMax} MHz)`, }); } } @@ -59,15 +51,20 @@ export function validateChannel( errors.push({ field: 'number', message: 'Channel number must be between 1 and 4000' }); } - // DMR-specific validation - if (channel.mode === 'Digital' || channel.mode === 'Fixed Digital') { - if (channel.colorCode < 0 || channel.colorCode > 15) { + // DMR-specific validation (digital only) + const isDigital = channel.mode === 'Digital' || channel.mode === 'Fixed Digital'; + if (isDigital) { + if (!isValidColorCode(channel.colorCode)) { errors.push({ field: 'colorCode', message: 'Color code must be between 0 and 15' }); } + const slotForValidation = (channel.slotOperation ?? 0) === 0 ? 1 : 2; + if (!isValidTimeSlot(slotForValidation)) { + errors.push({ field: 'slotOperation', message: 'Slot must be 1 (TS1) or 2 (TS2)' }); + } } - // Contact ID validation - if (channel.contactId < 0 || channel.contactId > 250) { + // Contact ID validation (digital only; analog does not use talk group) + if (isDigital && (channel.contactId < 0 || channel.contactId > 250)) { errors.push({ field: 'contactId', message: 'Contact ID must be between 0 and 250' }); } @@ -76,17 +73,15 @@ export function validateChannel( export function validateChannels( channels: Channel[], - settings?: RadioSettings | SettingsWithBandLimits + bandLimits?: RadioBandLimits | null ): Map { const errors = new Map(); - - channels.forEach(channel => { - const channelErrors = validateChannel(channel, settings); + channels.forEach((channel) => { + const channelErrors = validateChannel(channel, bandLimits); if (channelErrors.length > 0) { errors.set(channel.number, channelErrors); } }); - return errors; } diff --git a/src/services/validation/codeplugValidator.ts b/src/services/validation/codeplugValidator.ts index c9323ee..4bdd655 100644 --- a/src/services/validation/codeplugValidator.ts +++ b/src/services/validation/codeplugValidator.ts @@ -90,7 +90,7 @@ export function validateCodeplugForWrite( const warnings: CodeplugWriteWarning[] = []; // Always check: zones must not reference non-existent channels (prevents radio issues) - if (zones.length > 0 && channels.length >= 0) { + if (zones.length > 0) { const zoneRefs = getZonesWithInvalidChannelRefs(zones, channels); if (zoneRefs.length > 0) { const totalInvalid = zoneRefs.reduce((sum, z) => sum + z.invalidChannelNumbers.length, 0); diff --git a/src/services/validation/frequencyValidator.ts b/src/services/validation/frequencyValidator.ts index f55534f..bff49c3 100644 --- a/src/services/validation/frequencyValidator.ts +++ b/src/services/validation/frequencyValidator.ts @@ -1,17 +1,7 @@ -import type { RadioSettings } from '../../models/RadioSettings'; import type { Channel } from '../../models/Channel'; import type { RadioBandLimits } from '../../types/radioCapabilities'; import { DEFAULT_BAND_LIMITS } from '../../types/radioCapabilities'; -interface SettingsWithBandLimits { - bandLimits: { - vhfMin: number; - vhfMax: number; - uhfMin: number; - uhfMax: number; - }; -} - /** RX range where TX is not used (aviation/FM receive-only). TX bytes stored as 0xFF on radio. */ export const NO_TX_BAND_RX_MIN_MHZ = 87; export const NO_TX_BAND_RX_MAX_MHZ = 136; @@ -59,41 +49,18 @@ export function isValidChannelFrequency(channel: Channel, limits?: RadioBandLimi isValidFrequencyRange(channel.txFrequency, limits); } -export function isValidFrequency( - frequency: number, - settings?: RadioSettings | SettingsWithBandLimits -): boolean { +/** Band limits from radio capabilities (e.g. getCapabilitiesForModel(radioInfo?.model)?.bandLimits). */ +export function isValidFrequency(frequency: number, bandLimits?: RadioBandLimits | null): boolean { if (frequency <= 0) return false; - - if (!settings || !('bandLimits' in settings) || !settings.bandLimits) { - return true; // Skip validation if bandLimits not available - } - const isVHF = frequency >= settings.bandLimits.vhfMin && - frequency <= settings.bandLimits.vhfMax; - const isUHF = frequency >= settings.bandLimits.uhfMin && - frequency <= settings.bandLimits.uhfMax; - - return isVHF || isUHF; + if (!bandLimits) return true; + return isValidFrequencyRange(frequency, bandLimits); } -export function getFrequencyBand( - frequency: number, - settings?: RadioSettings | SettingsWithBandLimits -): 'VHF' | 'UHF' | 'Unknown' { - if (!settings || !('bandLimits' in settings) || !settings.bandLimits) { - return 'Unknown'; - } - - if (frequency >= settings.bandLimits.vhfMin && - frequency <= settings.bandLimits.vhfMax) { - return 'VHF'; - } - - if (frequency >= settings.bandLimits.uhfMin && - frequency <= settings.bandLimits.uhfMax) { - return 'UHF'; - } - +/** Band limits from radio capabilities. */ +export function getFrequencyBand(frequency: number, bandLimits?: RadioBandLimits | null): 'VHF' | 'UHF' | 'Unknown' { + if (!bandLimits) return 'Unknown'; + if (frequency >= bandLimits.vhfMin && frequency <= bandLimits.vhfMax) return 'VHF'; + if (frequency >= bandLimits.uhfMin && frequency <= bandLimits.uhfMax) return 'UHF'; return 'Unknown'; }