import { z } from 'zod'; /** * 中继差频方向 */ export const RepeaterShiftSchema = z.enum(['none', 'minus', 'plus']); /** * 哑音类型 */ export const ToneSquelchModeSchema = z.enum(['none', 'ctcss', 'dcs']); /** * 预设频率Schema */ export const PresetFrequencySchema = z.object({ band: z.string(), mode: z.string(), // 协议模式,如 FT8, FT4 radioMode: z.string().optional(), // 电台调制模式,如 USB, LSB, AM, FM frequency: z.number(), description: z.string().optional(), repeaterShift: RepeaterShiftSchema.optional(), // 中继差频方向,默认 none repeaterOffsetHz: z.number().int().positive().optional(), // 中继偏移,单位 Hz toneMode: ToneSquelchModeSchema.optional(), // 哑音类型,默认 none ctcssToneTenthsHz: z.number().int().positive().optional(), // CTCSS,单位 0.1Hz dcsCode: z.number().int().positive().optional(), // DCS 码 }).superRefine((preset, ctx) => { const isVoiceFmPreset = preset.mode === 'VOICE' && preset.radioMode?.toUpperCase() === 'FM'; const hasRepeaterDuplex = preset.repeaterShift === 'minus' || preset.repeaterShift === 'plus'; if (hasRepeaterDuplex && !isVoiceFmPreset) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['repeaterShift'], message: 'repeater duplex is only supported for VOICE FM presets', }); } if ( hasRepeaterDuplex && preset.repeaterOffsetHz === undefined ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['repeaterOffsetHz'], message: 'repeaterOffsetHz is required when repeaterShift is plus or minus', }); } if (!isVoiceFmPreset && preset.toneMode && preset.toneMode !== 'none') { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['toneMode'], message: 'tone squelch is only supported for VOICE FM presets', }); } if (preset.toneMode === 'ctcss') { if (preset.ctcssToneTenthsHz === undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['ctcssToneTenthsHz'], message: 'ctcssToneTenthsHz is required when toneMode is ctcss', }); } if (preset.dcsCode !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['dcsCode'], message: 'dcsCode cannot be set when toneMode is ctcss', }); } } if (preset.toneMode === 'dcs') { if (preset.dcsCode === undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['dcsCode'], message: 'dcsCode is required when toneMode is dcs', }); } if (preset.ctcssToneTenthsHz !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['ctcssToneTenthsHz'], message: 'ctcssToneTenthsHz cannot be set when toneMode is dcs', }); } } if ((preset.toneMode === undefined || preset.toneMode === 'none') && (preset.ctcssToneTenthsHz !== undefined || preset.dcsCode !== undefined) ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['toneMode'], message: 'toneMode must be ctcss or dcs when tone values are set', }); } }); /** * 频率列表响应Schema */ export const FrequencyListResponseSchema = z.object({ success: z.boolean(), presets: z.array(PresetFrequencySchema), }); /** * 串口配置参数Schema */ export const SerialConfigSchema = z.object({ // 基础串口设置 data_bits: z.enum(['5', '6', '7', '8']).optional(), stop_bits: z.enum(['1', '2']).optional(), serial_parity: z.enum(['None', 'Even', 'Odd', 'Mark', 'Space']).optional(), serial_handshake: z.enum(['None', 'Hardware', 'Software']).optional(), // 控制信号 rts_state: z.enum(['ON', 'OFF', 'UNSET']).optional(), dtr_state: z.enum(['ON', 'OFF', 'UNSET']).optional(), // 通信设置 rate: z.number().int().min(150).max(4000000).optional(), // 波特率 timeout: z.number().int().min(0).optional(), // 超时时间(ms) retry: z.number().int().min(0).optional(), // 重试次数 // 时序控制 write_delay: z.number().int().min(0).optional(), // 字节间延迟(ms) post_write_delay: z.number().int().min(0).optional(), // 命令间延迟(ms) }); export const HamlibBackendConfigSchema = z.record(z.string()); export const HamlibConfigFieldTypeSchema = z.enum([ 'string', 'combo', 'numeric', 'checkbutton', 'button', 'binary', 'int', 'unknown', ]); export const HamlibConfigFieldSchema = z.object({ token: z.number().int(), name: z.string(), label: z.string(), tooltip: z.string(), defaultValue: z.string(), effectiveDefaultValue: z.string().optional(), effectiveDefaultSource: z.enum(['hamlib-schema', 'rig-caps']).optional(), type: HamlibConfigFieldTypeSchema, numeric: z.object({ min: z.number(), max: z.number(), step: z.number(), }).optional(), options: z.array(z.string()).optional(), }); export const RigEndpointKindSchema = z.enum([ 'serial-port', 'network-address', 'device-path', ]); /** * 网络 RigCtld 连接配置Schema */ export const NetworkConfigSchema = z.object({ host: z.string(), port: z.number().int().min(1).max(65535), }); /** * ICOM WLAN 连接配置Schema */ export const IcomWlanConfigSchema = z.object({ ip: z.string(), port: z.number().int().min(1).max(65535), userName: z.string().optional(), password: z.string().optional(), /** * 数据模式(Data Mode) * 启用时使用 USB-D 等数字模式,适用于 FT8/FT4 等数字通信 * 默认值: true */ dataMode: z.boolean().optional().default(true), }); /** * 串口连接配置Schema */ export const SerialConnectionConfigSchema = z.object({ path: z.string(), rigModel: z.number().int(), serialConfig: SerialConfigSchema.optional(), backendConfig: HamlibBackendConfigSchema.optional(), }); export const HamlibSpectrumConfigSchema = z.object({ speed: z.number().int().min(0).max(255).optional(), }); /** * PTT (Push-to-Talk) 方法Schema * * - cat: 通过 CAT 命令控制 PTT(Hamlib RIG 类型,推荐) * - vox: 不主动控制 PTT,电台通过检测音频信号自动发射(适用于 SignaLink USB 等) * - dtr: 通过 RS-232 DTR 引脚控制 PTT(适用于古老电台或外部功放) * - rts: 通过 RS-232 RTS 引脚控制 PTT(适用于古老电台或外部功放) */ export const PttMethodSchema = z.enum(['cat', 'vox', 'dtr', 'rts']); /** * Hamlib配置Schema - 嵌套对象结构 * * 设计理念: * - type: 当前使用的连接类型 * - network/icomWlan/serial: 各连接类型的独立配置对象 * - 所有配置对象共存,切换连接类型时保留历史配置 * - 根据 type 字段读取对应的配置对象 */ export const HamlibConfigSchema = z.object({ type: z.enum(['none', 'network', 'serial', 'icom-wlan']), // 网络模式配置 network: NetworkConfigSchema.optional(), // ICOM WLAN 模式配置 icomWlan: IcomWlanConfigSchema.optional(), // 串口模式配置 serial: SerialConnectionConfigSchema.optional(), // Hamlib 频谱配置(当前仅用于官方频谱流 speed) spectrum: HamlibSpectrumConfigSchema.optional(), // 发射时序补偿(毫秒)- 用于补偿电台和网络的处理延迟 // 正值表示提前发射,负值表示延后发射 // 范围限制:-1000~1000ms,适用于各种网络和设备延迟场景 transmitCompensationMs: z.number().int().min(-1000).max(1000).optional(), // PTT 方法(默认 'cat',即 Hamlib RIG 类型) // 仅对 network 和 serial 连接类型有效,icom-wlan 固定使用 CI-V PTT pttMethod: PttMethodSchema.optional(), // PTT 独立串口路径(仅当 pttMethod 为 dtr/rts 时有效) // 留空则复用 CAT 同一串口 pttPort: z.string().optional(), // CW 键控串口路径(用于 DTR/RTS 引脚驱动电台 CW KEY 输入) cwKeyPort: z.string().optional(), // CW 键控引脚类型(dtr 或 rts) cwKeyMethod: z.enum(['dtr', 'rts']).optional(), }); /** * 电台配置响应Schema */ export const RadioConfigResponseSchema = z.object({ success: z.boolean(), config: HamlibConfigSchema, }); /** * 支持的电台型号Schema */ export const SupportedRigSchema = z.object({ rigModel: z.number(), mfgName: z.string(), modelName: z.string(), }); /** * 支持的电台列表响应Schema */ export const SupportedRigsResponseSchema = z.object({ rigs: z.array(SupportedRigSchema), }); export const RigConfigSchemaResponseSchema = z.object({ rigModel: z.number().int(), portType: z.string(), endpointKind: RigEndpointKindSchema, fields: z.array(HamlibConfigFieldSchema), }); /** * 电台信息Schema * 用于描述当前连接的电台的详细信息 */ export const RadioInfoSchema = z.object({ /** 制造商名称,如 "Yaesu", "ICOM", "Network" */ manufacturer: z.string(), /** 型号名称,如 "FT-991A", "IC-705", "RigCtrl" */ model: z.string(), /** Hamlib 电台型号 ID (serial/network 模式使用,icom-wlan 模式可选) */ rigModel: z.number().optional(), /** 连接类型 */ connectionType: z.enum(['serial', 'network', 'icom-wlan']), /** 固件版本 (如果可获取) */ firmwareVersion: z.string().optional(), /** 序列号 (如果可获取) */ serialNumber: z.string().optional(), }); /** * 串口信息Schema */ export const SerialPortSchema = z.object({ path: z.string(), manufacturer: z.string().optional(), serialNumber: z.string().optional(), pnpId: z.string().optional(), locationId: z.string().optional(), productId: z.string().optional(), vendorId: z.string().optional(), }); /** * 串口列表响应Schema */ export const SerialPortsResponseSchema = z.object({ ports: z.array(SerialPortSchema), }); /** * 测试响应Schema */ export const TestResponseSchema = z.object({ success: z.boolean(), message: z.string().optional(), }); /** * 天调能力Schema * 描述电台的天线调谐器功能支持情况 */ export const TunerCapabilitiesSchema = z.object({ /** 是否支持天调功能 */ supported: z.boolean(), /** 是否支持开关控制 */ hasSwitch: z.boolean(), /** 是否支持手动调谐 */ hasManualTune: z.boolean(), }); /** * 天调状态Schema * 描述天线调谐器的当前状态 */ export const TunerStatusSchema = z.object({ /** 天调是否已启用 */ enabled: z.boolean(), /** 是否正在调谐中 */ active: z.boolean(), /** 驻波比 (SWR) 值 */ swr: z.number().optional(), /** 调谐状态 */ status: z.enum(['idle', 'tuning', 'success', 'failed']).optional(), }); /** * 天调能力响应Schema */ export const TunerCapabilitiesResponseSchema = z.object({ success: z.boolean(), capabilities: TunerCapabilitiesSchema, }); /** * 天调状态响应Schema */ export const TunerStatusResponseSchema = z.object({ success: z.boolean(), status: TunerStatusSchema, }); /** * 电台连接状态枚举 * 精细化的连接状态,贯穿 server → contracts → web */ export enum RadioConnectionStatus { /** 未配置电台 (type=none) */ NOT_CONFIGURED = 'not_configured', /** 已断开 (type!=none, 但未连接/初始状态) */ DISCONNECTED = 'disconnected', /** 连接中 */ CONNECTING = 'connecting', /** 已连接 */ CONNECTED = 'connected', /** 自动重连中(运行中断线后自动重连) */ RECONNECTING = 'reconnecting', /** 连接丢失(重连耗尽或运行中断连后停止重连) */ CONNECTION_LOST = 'connection_lost', } /** * 重连进度信息 */ export const ReconnectProgressSchema = z.object({ attempt: z.number(), maxAttempts: z.number(), nextRetryMs: z.number().optional(), }); export type ReconnectProgress = z.infer; export const RadioConnectionStatusSchema = z.nativeEnum(RadioConnectionStatus); /** * 核心电台能力 * 用于描述主流程是否仍可继续使用某项核心读写能力。 * * 语义: * - true: 当前连接仍可继续尝试该能力(已确认支持,或尚未确认不支持) * - false: 已明确确认当前电台/连接不支持该能力,后续不再重复访问底层 */ export const CoreRadioCapabilitiesSchema = z.object({ readFrequency: z.boolean(), writeFrequency: z.boolean(), readRadioMode: z.boolean(), writeRadioMode: z.boolean(), }); export const CoreCapabilityDiagnosticSchema = z.object({ capability: z.enum(['readFrequency', 'writeFrequency', 'readRadioMode', 'writeRadioMode']), message: z.string(), stack: z.string(), recordedAt: z.number(), }); export const CoreCapabilityDiagnosticsSchema = z.object({ readFrequency: CoreCapabilityDiagnosticSchema.optional(), writeFrequency: CoreCapabilityDiagnosticSchema.optional(), readRadioMode: CoreCapabilityDiagnosticSchema.optional(), writeRadioMode: CoreCapabilityDiagnosticSchema.optional(), }); /** * 自定义频率预设设置Schema */ export const CustomFrequencyPresetsSchema = z.object({ presets: z.array(PresetFrequencySchema).min(1), }); // 导出类型 export type CustomFrequencyPresets = z.infer; export type PresetFrequency = z.infer; export type RepeaterShift = z.infer; export type ToneSquelchMode = z.infer; export type FrequencyListResponse = z.infer; export type SerialConfig = z.infer; export type HamlibBackendConfig = z.infer; export type HamlibConfigFieldType = z.infer; export type HamlibConfigField = z.infer; export type NetworkConfig = z.infer; export type IcomWlanConfig = z.infer; export type SerialConnectionConfig = z.infer; export type HamlibSpectrumConfig = z.infer; export type PttMethod = z.infer; export type HamlibConfig = z.infer; export type RadioConfigResponse = z.infer; export type SupportedRig = z.infer; export type SupportedRigsResponse = z.infer; export type RigConfigSchemaResponse = z.infer; export type RadioInfo = z.infer; export type SerialPort = z.infer; export type SerialPortsResponse = z.infer; export type TestResponse = z.infer; export type TunerCapabilities = z.infer; export type TunerStatus = z.infer; export type TunerCapabilitiesResponse = z.infer; export type TunerStatusResponse = z.infer; export type CoreRadioCapabilities = z.infer; export type CoreCapabilityDiagnostic = z.infer; export type CoreCapabilityDiagnostics = z.infer;