@tx5dr/contracts 1.7.3 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/__tests__/ability.test.d.ts +2 -0
- package/dist/schema/__tests__/ability.test.d.ts.map +1 -0
- package/dist/schema/__tests__/ability.test.js +32 -0
- package/dist/schema/__tests__/ability.test.js.map +1 -0
- package/dist/schema/__tests__/cw-decoder.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/cw-decoder.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/cw-decoder.schema.test.js +114 -0
- package/dist/schema/__tests__/cw-decoder.schema.test.js.map +1 -0
- package/dist/schema/__tests__/device-ui.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/device-ui.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/device-ui.schema.test.js +111 -0
- package/dist/schema/__tests__/device-ui.schema.test.js.map +1 -0
- package/dist/schema/__tests__/process-monitor.schema.test.js +96 -0
- package/dist/schema/__tests__/process-monitor.schema.test.js.map +1 -1
- package/dist/schema/__tests__/radio.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/radio.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/radio.schema.test.js +143 -0
- package/dist/schema/__tests__/radio.schema.test.js.map +1 -0
- package/dist/schema/ability.d.ts +3 -1
- package/dist/schema/ability.d.ts.map +1 -1
- package/dist/schema/ability.js +11 -0
- package/dist/schema/ability.js.map +1 -1
- package/dist/schema/api-response.schema.d.ts +168 -1
- package/dist/schema/api-response.schema.d.ts.map +1 -1
- package/dist/schema/api-response.schema.js +22 -0
- package/dist/schema/api-response.schema.js.map +1 -1
- package/dist/schema/cpu-profile.schema.d.ts +6 -6
- package/dist/schema/cw-decoder.schema.d.ts +2501 -0
- package/dist/schema/cw-decoder.schema.d.ts.map +1 -0
- package/dist/schema/cw-decoder.schema.js +196 -0
- package/dist/schema/cw-decoder.schema.js.map +1 -0
- package/dist/schema/cw-keyer.schema.d.ts +368 -0
- package/dist/schema/cw-keyer.schema.d.ts.map +1 -0
- package/dist/schema/cw-keyer.schema.js +143 -0
- package/dist/schema/cw-keyer.schema.js.map +1 -0
- package/dist/schema/desktop-https.schema.js +1 -1
- package/dist/schema/desktop-https.schema.js.map +1 -1
- package/dist/schema/device-ui.schema.d.ts +1782 -0
- package/dist/schema/device-ui.schema.d.ts.map +1 -0
- package/dist/schema/device-ui.schema.js +156 -0
- package/dist/schema/device-ui.schema.js.map +1 -0
- package/dist/schema/mode.schema.d.ts +1 -0
- package/dist/schema/mode.schema.d.ts.map +1 -1
- package/dist/schema/mode.schema.js +8 -0
- package/dist/schema/mode.schema.js.map +1 -1
- package/dist/schema/plugin.schema.d.ts +637 -54
- package/dist/schema/plugin.schema.d.ts.map +1 -1
- package/dist/schema/plugin.schema.js +22 -2
- package/dist/schema/plugin.schema.js.map +1 -1
- package/dist/schema/process-monitor.schema.d.ts +1948 -261
- package/dist/schema/process-monitor.schema.d.ts.map +1 -1
- package/dist/schema/process-monitor.schema.js +11 -0
- package/dist/schema/process-monitor.schema.js.map +1 -1
- package/dist/schema/radio-capability.schema.d.ts +1 -1
- package/dist/schema/radio-capability.schema.d.ts.map +1 -1
- package/dist/schema/radio-capability.schema.js +41 -0
- package/dist/schema/radio-capability.schema.js.map +1 -1
- package/dist/schema/radio-power.schema.d.ts +6 -0
- package/dist/schema/radio-power.schema.d.ts.map +1 -1
- package/dist/schema/radio-profile.schema.d.ts +86 -0
- package/dist/schema/radio-profile.schema.d.ts.map +1 -1
- package/dist/schema/radio.schema.d.ts +160 -3
- package/dist/schema/radio.schema.d.ts.map +1 -1
- package/dist/schema/radio.schema.js +82 -0
- package/dist/schema/radio.schema.js.map +1 -1
- package/dist/schema/realtime.schema.d.ts +6 -6
- package/dist/schema/voice.schema.d.ts +1 -1
- package/dist/schema/voice.schema.d.ts.map +1 -1
- package/dist/schema/voice.schema.js +1 -1
- package/dist/schema/voice.schema.js.map +1 -1
- package/dist/schema/websocket.schema.d.ts +5615 -412
- package/dist/schema/websocket.schema.d.ts.map +1 -1
- package/dist/schema/websocket.schema.js +102 -2
- package/dist/schema/websocket.schema.js.map +1 -1
- package/dist/utils/__tests__/cwTiming.test.d.ts +2 -0
- package/dist/utils/__tests__/cwTiming.test.d.ts.map +1 -0
- package/dist/utils/__tests__/cwTiming.test.js +74 -0
- package/dist/utils/__tests__/cwTiming.test.js.map +1 -0
- package/dist/utils/cwTiming.d.ts +36 -0
- package/dist/utils/cwTiming.d.ts.map +1 -0
- package/dist/utils/cwTiming.js +155 -0
- package/dist/utils/cwTiming.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/schema/__tests__/ability.test.ts +40 -0
- package/src/schema/__tests__/cw-decoder.schema.test.ts +127 -0
- package/src/schema/__tests__/device-ui.schema.test.ts +122 -0
- package/src/schema/__tests__/process-monitor.schema.test.ts +99 -0
- package/src/schema/__tests__/radio.schema.test.ts +160 -0
- package/src/schema/ability.ts +13 -0
- package/src/schema/api-response.schema.ts +22 -0
- package/src/schema/cw-decoder.schema.ts +234 -0
- package/src/schema/cw-keyer.schema.ts +177 -0
- package/src/schema/desktop-https.schema.ts +1 -1
- package/src/schema/device-ui.schema.ts +182 -0
- package/src/schema/mode.schema.ts +8 -0
- package/src/schema/plugin.schema.ts +28 -2
- package/src/schema/process-monitor.schema.ts +18 -0
- package/src/schema/radio-capability.schema.ts +41 -0
- package/src/schema/radio.schema.ts +97 -0
- package/src/schema/voice.schema.ts +1 -1
- package/src/schema/websocket.schema.ts +131 -2
- package/src/utils/__tests__/cwTiming.test.ts +91 -0
- package/src/utils/cwTiming.ts +203 -0
|
@@ -27,6 +27,8 @@ import { RadioProfileSchema, ProfileChangedEventSchema } from './radio-profile.s
|
|
|
27
27
|
import { UserRole } from './auth.schema.js';
|
|
28
28
|
import type { VoicePTTLock } from './voice.schema.js';
|
|
29
29
|
import type { VoiceKeyerStatus } from './voice-keyer.schema.js';
|
|
30
|
+
import type { CWKeyerStatus, CWKeyerConfig } from './cw-keyer.schema.js';
|
|
31
|
+
import { CWDecoderEventSchema, CWDecoderStatusSchema, type CWDecoderEvent, type CWDecoderStatus } from './cw-decoder.schema.js';
|
|
30
32
|
import { CapabilityListSchema, CapabilityStateSchema, WriteCapabilityPayloadSchema } from './radio-capability.schema.js';
|
|
31
33
|
import { RadioPowerStateEventSchema } from './radio-power.schema.js';
|
|
32
34
|
import { AudioSidecarStatusPayloadSchema } from './audio-sidecar.schema.js';
|
|
@@ -56,11 +58,13 @@ export enum WSMessageType {
|
|
|
56
58
|
SLOT_PACK_UPDATED = 'slotPackUpdated',
|
|
57
59
|
SPECTRUM_CAPABILITIES = 'spectrumCapabilities',
|
|
58
60
|
SUBSCRIBE_SPECTRUM = 'subscribeSpectrum',
|
|
61
|
+
SPECTRUM_SUBSCRIPTION_CHANGED = 'spectrumSubscriptionChanged',
|
|
59
62
|
SPECTRUM_FRAME = 'spectrumFrame',
|
|
60
63
|
SPECTRUM_SESSION_STATE_CHANGED = 'spectrumSessionStateChanged',
|
|
61
64
|
INVOKE_SPECTRUM_CONTROL = 'invokeSpectrumControl',
|
|
62
65
|
DECODE_ERROR = 'decodeError',
|
|
63
66
|
SYSTEM_STATUS = 'systemStatus',
|
|
67
|
+
BOOTSTRAP_STATUS_CHANGED = 'bootstrapStatusChanged',
|
|
64
68
|
CLIENT_COUNT_CHANGED = 'clientCountChanged',
|
|
65
69
|
CLOCK_STATUS_CHANGED = 'clockStatusChanged',
|
|
66
70
|
RIGCTLD_STATUS = 'rigctldStatus',
|
|
@@ -175,6 +179,24 @@ export enum WSMessageType {
|
|
|
175
179
|
VOICE_KEYER_STOP = 'voiceKeyerStop',
|
|
176
180
|
VOICE_KEYER_STATUS_CHANGED = 'voiceKeyerStatusChanged',
|
|
177
181
|
|
|
182
|
+
// ===== CW 模式 =====
|
|
183
|
+
/** CW 键控器状态变化(server → client) */
|
|
184
|
+
CW_KEYER_STATUS = 'cwKeyerStatus',
|
|
185
|
+
/** CW 手键动作(client → server) */
|
|
186
|
+
CW_KEY_ACTION = 'cwKeyAction',
|
|
187
|
+
/** CW 文字输入(client → server) */
|
|
188
|
+
CW_TEXT_INPUT = 'cwTextInput',
|
|
189
|
+
/** CW 播放预设报文(client → server) */
|
|
190
|
+
CW_PLAY_MESSAGE = 'cwPlayMessage',
|
|
191
|
+
/** CW 停止播放(client → server) */
|
|
192
|
+
CW_STOP_MESSAGE = 'cwStopMessage',
|
|
193
|
+
/** CW 配置变更通知(server → client) */
|
|
194
|
+
CW_CONFIG_CHANGED = 'cwConfigChanged',
|
|
195
|
+
/** CW 解码器状态快照(server → client) */
|
|
196
|
+
CW_DECODER_STATUS = 'cwDecoderStatus',
|
|
197
|
+
/** CW 解码器增量事件(server → client) */
|
|
198
|
+
CW_DECODER_EVENT = 'cwDecoderEvent',
|
|
199
|
+
|
|
178
200
|
// ===== 进程监控 =====
|
|
179
201
|
PROCESS_SNAPSHOT = 'processSnapshot',
|
|
180
202
|
PROCESS_SNAPSHOT_HISTORY = 'processSnapshotHistory',
|
|
@@ -215,8 +237,8 @@ export const SystemStatusSchema = z.object({
|
|
|
215
237
|
radioConnectionHealth: z.object({
|
|
216
238
|
connectionHealthy: z.boolean(),
|
|
217
239
|
}).optional(),
|
|
218
|
-
/** 引擎模式:digital(FT8/FT4
|
|
219
|
-
engineMode: z.enum(['digital', 'voice']).default('digital'),
|
|
240
|
+
/** 引擎模式:digital(FT8/FT4)、voice(语音通联)或 cw(莫尔斯码) */
|
|
241
|
+
engineMode: z.enum(['digital', 'voice', 'cw']).default('digital'),
|
|
220
242
|
/** 当前电台调制模式(语音模式下使用,如 USB/LSB/FM/AM) */
|
|
221
243
|
currentRadioMode: z.string().optional(),
|
|
222
244
|
/** 引擎状态机当前状态 */
|
|
@@ -229,6 +251,62 @@ export const SystemStatusSchema = z.object({
|
|
|
229
251
|
}).nullable().optional(),
|
|
230
252
|
});
|
|
231
253
|
|
|
254
|
+
export const BootstrapLifecycleSchema = z.enum(['booting', 'completed', 'degraded', 'failed', 'dismissed']);
|
|
255
|
+
export const BootstrapPhaseStateSchema = z.enum([
|
|
256
|
+
'pending',
|
|
257
|
+
'running',
|
|
258
|
+
'ready',
|
|
259
|
+
'skipped',
|
|
260
|
+
'warning',
|
|
261
|
+
'failed',
|
|
262
|
+
'timed_out',
|
|
263
|
+
]);
|
|
264
|
+
export const BootstrapPhaseIdSchema = z.enum([
|
|
265
|
+
'config-auth',
|
|
266
|
+
'core-http',
|
|
267
|
+
'engine-bootstrap',
|
|
268
|
+
'audio-device-discovery',
|
|
269
|
+
'logbook-prewarm',
|
|
270
|
+
'plugin-bootstrap',
|
|
271
|
+
'ntp-initial-check',
|
|
272
|
+
'active-profile-autostart',
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
export const BootstrapPhaseStatusSchema = z.object({
|
|
276
|
+
id: BootstrapPhaseIdSchema,
|
|
277
|
+
label: z.string(),
|
|
278
|
+
description: z.string().optional(),
|
|
279
|
+
state: BootstrapPhaseStateSchema,
|
|
280
|
+
startedAt: z.number().optional(),
|
|
281
|
+
completedAt: z.number().optional(),
|
|
282
|
+
updatedAt: z.number(),
|
|
283
|
+
durationMs: z.number().optional(),
|
|
284
|
+
message: z.string().optional(),
|
|
285
|
+
retryable: z.boolean().optional(),
|
|
286
|
+
userVisible: z.boolean().optional(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export const BootstrapStatusSchema = z.object({
|
|
290
|
+
bootSessionId: z.string(),
|
|
291
|
+
lifecycle: BootstrapLifecycleSchema,
|
|
292
|
+
startedAt: z.number(),
|
|
293
|
+
updatedAt: z.number(),
|
|
294
|
+
completedAt: z.number().optional(),
|
|
295
|
+
durationMs: z.number().optional(),
|
|
296
|
+
blockingReady: z.boolean().default(false),
|
|
297
|
+
phases: z.array(BootstrapPhaseStatusSchema),
|
|
298
|
+
summary: z.object({
|
|
299
|
+
total: z.number(),
|
|
300
|
+
pending: z.number(),
|
|
301
|
+
running: z.number(),
|
|
302
|
+
ready: z.number(),
|
|
303
|
+
skipped: z.number(),
|
|
304
|
+
warning: z.number(),
|
|
305
|
+
failed: z.number(),
|
|
306
|
+
timedOut: z.number(),
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
232
310
|
// 子窗口信息数据结构
|
|
233
311
|
export const SubWindowInfoSchema = z.object({
|
|
234
312
|
slotInfo: SlotInfoSchema,
|
|
@@ -387,6 +465,11 @@ export const MeterDataSchema = z.object({
|
|
|
387
465
|
|
|
388
466
|
// ===== 导出共享类型 =====
|
|
389
467
|
export type SystemStatus = z.infer<typeof SystemStatusSchema>;
|
|
468
|
+
export type BootstrapLifecycle = z.infer<typeof BootstrapLifecycleSchema>;
|
|
469
|
+
export type BootstrapPhaseState = z.infer<typeof BootstrapPhaseStateSchema>;
|
|
470
|
+
export type BootstrapPhaseId = z.infer<typeof BootstrapPhaseIdSchema>;
|
|
471
|
+
export type BootstrapPhaseStatus = z.infer<typeof BootstrapPhaseStatusSchema>;
|
|
472
|
+
export type BootstrapStatus = z.infer<typeof BootstrapStatusSchema>;
|
|
390
473
|
export type SubWindowInfo = z.infer<typeof SubWindowInfoSchema>;
|
|
391
474
|
export type DecodeErrorInfo = z.infer<typeof DecodeErrorInfoSchema>;
|
|
392
475
|
export type FrequencyState = z.infer<typeof FrequencyStateSchema>;
|
|
@@ -448,6 +531,17 @@ export const WSSubscribeSpectrumMessageSchema = WSBaseMessageSchema.extend({
|
|
|
448
531
|
}),
|
|
449
532
|
});
|
|
450
533
|
|
|
534
|
+
export const WSSpectrumSubscriptionChangedMessageSchema = WSBaseMessageSchema.extend({
|
|
535
|
+
type: z.literal(WSMessageType.SPECTRUM_SUBSCRIPTION_CHANGED),
|
|
536
|
+
data: z.object({
|
|
537
|
+
requestedKind: SpectrumKindSchema.nullable(),
|
|
538
|
+
effectiveKind: SpectrumKindSchema.nullable(),
|
|
539
|
+
ok: z.boolean(),
|
|
540
|
+
reason: z.string().optional(),
|
|
541
|
+
capabilities: SpectrumCapabilitiesSchema.optional(),
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
|
|
451
545
|
export const WSSpectrumFrameMessageSchema = WSBaseMessageSchema.extend({
|
|
452
546
|
type: z.literal(WSMessageType.SPECTRUM_FRAME),
|
|
453
547
|
data: SpectrumFrameSchema,
|
|
@@ -473,6 +567,11 @@ export const WSSystemStatusMessageSchema = WSBaseMessageSchema.extend({
|
|
|
473
567
|
data: SystemStatusSchema,
|
|
474
568
|
});
|
|
475
569
|
|
|
570
|
+
export const WSBootstrapStatusChangedMessageSchema = WSBaseMessageSchema.extend({
|
|
571
|
+
type: z.literal(WSMessageType.BOOTSTRAP_STATUS_CHANGED),
|
|
572
|
+
data: BootstrapStatusSchema,
|
|
573
|
+
});
|
|
574
|
+
|
|
476
575
|
export const WSClientCountChangedMessageSchema = WSBaseMessageSchema.extend({
|
|
477
576
|
type: z.literal(WSMessageType.CLIENT_COUNT_CHANGED),
|
|
478
577
|
data: z.object({
|
|
@@ -1109,6 +1208,20 @@ export const WSTextMessageSchema = WSBaseMessageSchema.extend({
|
|
|
1109
1208
|
|
|
1110
1209
|
export type WSTextMessage = z.infer<typeof WSTextMessageSchema>;
|
|
1111
1210
|
|
|
1211
|
+
export const WSCWDecoderStatusMessageSchema = WSBaseMessageSchema.extend({
|
|
1212
|
+
type: z.literal(WSMessageType.CW_DECODER_STATUS),
|
|
1213
|
+
data: CWDecoderStatusSchema,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
export type WSCWDecoderStatusMessage = z.infer<typeof WSCWDecoderStatusMessageSchema>;
|
|
1217
|
+
|
|
1218
|
+
export const WSCWDecoderEventMessageSchema = WSBaseMessageSchema.extend({
|
|
1219
|
+
type: z.literal(WSMessageType.CW_DECODER_EVENT),
|
|
1220
|
+
data: CWDecoderEventSchema,
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
export type WSCWDecoderEventMessage = z.infer<typeof WSCWDecoderEventMessageSchema>;
|
|
1224
|
+
|
|
1112
1225
|
// ===== 统一电台控制能力消息 =====
|
|
1113
1226
|
|
|
1114
1227
|
/**
|
|
@@ -1270,11 +1383,13 @@ export const WSMessageSchema = z.discriminatedUnion('type', [
|
|
|
1270
1383
|
WSSlotPackUpdatedMessageSchema,
|
|
1271
1384
|
WSSpectrumCapabilitiesMessageSchema,
|
|
1272
1385
|
WSSubscribeSpectrumMessageSchema,
|
|
1386
|
+
WSSpectrumSubscriptionChangedMessageSchema,
|
|
1273
1387
|
WSSpectrumFrameMessageSchema,
|
|
1274
1388
|
WSSpectrumSessionStateChangedMessageSchema,
|
|
1275
1389
|
WSInvokeSpectrumControlMessageSchema,
|
|
1276
1390
|
WSDecodeErrorMessageSchema,
|
|
1277
1391
|
WSSystemStatusMessageSchema,
|
|
1392
|
+
WSBootstrapStatusChangedMessageSchema,
|
|
1278
1393
|
WSClientCountChangedMessageSchema,
|
|
1279
1394
|
WSClockStatusChangedMessageSchema,
|
|
1280
1395
|
WSErrorMessageSchema,
|
|
@@ -1347,6 +1462,10 @@ export const WSMessageSchema = z.discriminatedUnion('type', [
|
|
|
1347
1462
|
// 文本消息(Toast通知)
|
|
1348
1463
|
WSTextMessageSchema,
|
|
1349
1464
|
|
|
1465
|
+
// CW 解码器消息
|
|
1466
|
+
WSCWDecoderStatusMessageSchema,
|
|
1467
|
+
WSCWDecoderEventMessageSchema,
|
|
1468
|
+
|
|
1350
1469
|
// 认证消息
|
|
1351
1470
|
WSAuthRequiredMessageSchema,
|
|
1352
1471
|
WSAuthTokenMessageSchema,
|
|
@@ -1378,11 +1497,13 @@ export type WSSubWindowMessage = z.infer<typeof WSSubWindowMessageSchema>;
|
|
|
1378
1497
|
export type WSSlotPackUpdatedMessage = z.infer<typeof WSSlotPackUpdatedMessageSchema>;
|
|
1379
1498
|
export type WSSpectrumCapabilitiesMessage = z.infer<typeof WSSpectrumCapabilitiesMessageSchema>;
|
|
1380
1499
|
export type WSSubscribeSpectrumMessage = z.infer<typeof WSSubscribeSpectrumMessageSchema>;
|
|
1500
|
+
export type WSSpectrumSubscriptionChangedMessage = z.infer<typeof WSSpectrumSubscriptionChangedMessageSchema>;
|
|
1381
1501
|
export type WSSpectrumFrameMessage = z.infer<typeof WSSpectrumFrameMessageSchema>;
|
|
1382
1502
|
export type WSSpectrumSessionStateChangedMessage = z.infer<typeof WSSpectrumSessionStateChangedMessageSchema>;
|
|
1383
1503
|
export type WSInvokeSpectrumControlMessage = z.infer<typeof WSInvokeSpectrumControlMessageSchema>;
|
|
1384
1504
|
export type WSDecodeErrorMessage = z.infer<typeof WSDecodeErrorMessageSchema>;
|
|
1385
1505
|
export type WSSystemStatusMessage = z.infer<typeof WSSystemStatusMessageSchema>;
|
|
1506
|
+
export type WSBootstrapStatusChangedMessage = z.infer<typeof WSBootstrapStatusChangedMessageSchema>;
|
|
1386
1507
|
export type WSClientCountChangedMessage = z.infer<typeof WSClientCountChangedMessageSchema>;
|
|
1387
1508
|
export type WSClockStatusChangedMessage = z.infer<typeof WSClockStatusChangedMessageSchema>;
|
|
1388
1509
|
export type WSErrorMessage = z.infer<typeof WSErrorMessageSchema>;
|
|
@@ -1432,6 +1553,7 @@ export interface DigitalRadioEngineEvents {
|
|
|
1432
1553
|
slotPackUpdated: (slotPack: z.infer<typeof SlotPackSchema>) => void;
|
|
1433
1554
|
slotPacksReset: (data: z.infer<typeof WSSlotPacksResetMessageSchema>['data']) => void;
|
|
1434
1555
|
spectrumCapabilities: (data: z.infer<typeof SpectrumCapabilitiesSchema>) => void;
|
|
1556
|
+
spectrumSubscriptionChanged: (data: z.infer<typeof WSSpectrumSubscriptionChangedMessageSchema>['data']) => void;
|
|
1435
1557
|
spectrumFrame: (data: z.infer<typeof SpectrumFrameSchema>) => void;
|
|
1436
1558
|
spectrumSessionStateChanged: (data: z.infer<typeof SpectrumSessionStateSchema>) => void;
|
|
1437
1559
|
|
|
@@ -1455,6 +1577,7 @@ export interface DigitalRadioEngineEvents {
|
|
|
1455
1577
|
// 错误和状态事件
|
|
1456
1578
|
decodeError: (errorInfo: DecodeErrorInfo) => void;
|
|
1457
1579
|
systemStatus: (status: SystemStatus) => void;
|
|
1580
|
+
bootstrapStatusChanged: (status: BootstrapStatus) => void;
|
|
1458
1581
|
clientCountChanged: (data: { count: number; timestamp: number }) => void;
|
|
1459
1582
|
clockStatusChanged: (data: import('./system.schema.js').ClockStatusSummary) => void;
|
|
1460
1583
|
|
|
@@ -1519,6 +1642,12 @@ export interface DigitalRadioEngineEvents {
|
|
|
1519
1642
|
voiceRadioModeChanged: (data: { radioMode: string }) => void;
|
|
1520
1643
|
voiceKeyerStatusChanged: (data: VoiceKeyerStatus) => void;
|
|
1521
1644
|
|
|
1645
|
+
// CW 模式事件
|
|
1646
|
+
cwKeyerStatusChanged: (data: CWKeyerStatus) => void;
|
|
1647
|
+
cwConfigChanged: (data: CWKeyerConfig) => void;
|
|
1648
|
+
cwDecoderStatusChanged: (data: CWDecoderStatus) => void;
|
|
1649
|
+
cwDecoderEvent: (data: CWDecoderEvent) => void;
|
|
1650
|
+
|
|
1522
1651
|
// 进程监控事件
|
|
1523
1652
|
processSnapshot: (data: import('./process-monitor.schema.js').ProcessSnapshot) => void;
|
|
1524
1653
|
processSnapshotHistory: (data: import('./process-monitor.schema.js').ProcessSnapshotHistory) => void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
encodeTextToCWEvents,
|
|
4
|
+
encodeTextToCWKeyStateSegments,
|
|
5
|
+
estimateCWMessageDurationMs,
|
|
6
|
+
} from '../cwTiming.js';
|
|
7
|
+
|
|
8
|
+
function totalSegmentDuration(text: string, wpm: number): number {
|
|
9
|
+
return encodeTextToCWKeyStateSegments(text, wpm).reduce((sum, segment) => sum + segment.durationMs, 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('CW timing utilities', () => {
|
|
13
|
+
it('returns no events and zero duration for blank text', () => {
|
|
14
|
+
expect(encodeTextToCWEvents(' ', 20)).toEqual([]);
|
|
15
|
+
expect(encodeTextToCWKeyStateSegments(' ', 20)).toEqual([]);
|
|
16
|
+
expect(estimateCWMessageDurationMs('', 20)).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('encodes exact key events for simple characters and spacing', () => {
|
|
20
|
+
expect(encodeTextToCWEvents('E', 20)).toEqual([
|
|
21
|
+
{ type: 'key-down', afterMs: 0 },
|
|
22
|
+
{ type: 'key-up', afterMs: 60 },
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
expect(encodeTextToCWEvents('EE', 20)).toEqual([
|
|
26
|
+
{ type: 'key-down', afterMs: 0 },
|
|
27
|
+
{ type: 'key-up', afterMs: 60 },
|
|
28
|
+
{ type: 'key-down', afterMs: 180 },
|
|
29
|
+
{ type: 'key-up', afterMs: 60 },
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(encodeTextToCWEvents('E E', 20)).toEqual([
|
|
33
|
+
{ type: 'key-down', afterMs: 0 },
|
|
34
|
+
{ type: 'key-up', afterMs: 60 },
|
|
35
|
+
{ type: 'key-down', afterMs: 420 },
|
|
36
|
+
{ type: 'key-up', afterMs: 60 },
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
expect(encodeTextToCWEvents('A', 20)).toEqual([
|
|
40
|
+
{ type: 'key-down', afterMs: 0 },
|
|
41
|
+
{ type: 'key-up', afterMs: 60 },
|
|
42
|
+
{ type: 'key-down', afterMs: 60 },
|
|
43
|
+
{ type: 'key-up', afterMs: 180 },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('encodes exact key state segments for browser sidetone adapters', () => {
|
|
48
|
+
expect(encodeTextToCWKeyStateSegments('E', 20)).toEqual([
|
|
49
|
+
{ keyDown: true, durationMs: 60 },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
expect(encodeTextToCWKeyStateSegments('EE', 20)).toEqual([
|
|
53
|
+
{ keyDown: true, durationMs: 60 },
|
|
54
|
+
{ keyDown: false, durationMs: 180 },
|
|
55
|
+
{ keyDown: true, durationMs: 60 },
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
expect(encodeTextToCWKeyStateSegments('E E', 20)).toEqual([
|
|
59
|
+
{ keyDown: true, durationMs: 60 },
|
|
60
|
+
{ keyDown: false, durationMs: 420 },
|
|
61
|
+
{ keyDown: true, durationMs: 60 },
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(encodeTextToCWKeyStateSegments('A', 20)).toEqual([
|
|
65
|
+
{ keyDown: true, durationMs: 60 },
|
|
66
|
+
{ keyDown: false, durationMs: 60 },
|
|
67
|
+
{ keyDown: true, durationMs: 180 },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('scales duration by WPM', () => {
|
|
72
|
+
expect(estimateCWMessageDurationMs('EE', 20)).toBe(300);
|
|
73
|
+
expect(estimateCWMessageDurationMs('EE', 40)).toBe(150);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('uses the Hamlib/PARIS exact dot duration for non-divisible WPM values', () => {
|
|
77
|
+
expect(estimateCWMessageDurationMs('EE', 17)).toBe(353);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('treats prosigns as single Morse tokens rather than re-encoding dots and dashes', () => {
|
|
81
|
+
expect(estimateCWMessageDurationMs('<SK>', 20)).toBe(900);
|
|
82
|
+
expect(totalSegmentDuration('<SK>', 20)).toBe(900);
|
|
83
|
+
expect(totalSegmentDuration('<CL>', 20)).toBeGreaterThan(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps unsupported-character and WPM clamp behavior', () => {
|
|
87
|
+
expect(estimateCWMessageDurationMs('E☃E', 20)).toBe(300);
|
|
88
|
+
expect(estimateCWMessageDurationMs('E', 1)).toBe(240);
|
|
89
|
+
expect(estimateCWMessageDurationMs('E', 1000)).toBe(20);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CW text to Morse timing encoder.
|
|
3
|
+
*
|
|
4
|
+
* Timing follows the ITU/PARIS convention:
|
|
5
|
+
* - dit = 1200 / WPM ms
|
|
6
|
+
* - dah = 3 dits
|
|
7
|
+
* - intra-character gap = 1 dit
|
|
8
|
+
* - character gap = 3 dits
|
|
9
|
+
* - word gap = 7 dits
|
|
10
|
+
*/
|
|
11
|
+
export interface CWTimingEvent {
|
|
12
|
+
type: 'key-down' | 'key-up';
|
|
13
|
+
/** Delay from the previous event, in milliseconds. */
|
|
14
|
+
afterMs: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CWKeyStateSegment {
|
|
18
|
+
/** true when the key/tone is closed, false for silence/open key. */
|
|
19
|
+
keyDown: boolean;
|
|
20
|
+
/** Segment duration in milliseconds. */
|
|
21
|
+
durationMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DOT_UNITS = {
|
|
25
|
+
dit: 1,
|
|
26
|
+
dah: 3,
|
|
27
|
+
intraChar: 1,
|
|
28
|
+
interChar: 3,
|
|
29
|
+
interWord: 7,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const MORSE_TABLE: Record<string, string> = {
|
|
33
|
+
A: '.-', B: '-...', C: '-.-.', D: '-..',
|
|
34
|
+
E: '.', F: '..-.', G: '--.', H: '....',
|
|
35
|
+
I: '..', J: '.---', K: '-.-', L: '.-..',
|
|
36
|
+
M: '--', N: '-.', O: '---', P: '.--.',
|
|
37
|
+
Q: '--.-', R: '.-.', S: '...', T: '-',
|
|
38
|
+
U: '..-', V: '...-', W: '.--', X: '-..-',
|
|
39
|
+
Y: '-.--', Z: '--..',
|
|
40
|
+
'0': '-----', '1': '.----', '2': '..---', '3': '...--',
|
|
41
|
+
'4': '....-', '5': '.....', '6': '-....', '7': '--...',
|
|
42
|
+
'8': '---..', '9': '----.',
|
|
43
|
+
'.': '.-.-.-', ',': '--..--', '?': '..--..', '/': '-..-.',
|
|
44
|
+
'@': '.--.-.', '=': '-...-', '-': '-....-', '+': '.-.-.',
|
|
45
|
+
':': '---...', ';': '-.-.-.', '\'': '.----.', '"': '.-..-.',
|
|
46
|
+
'!': '-.-.--', '&': '.-...', '(': '-.--.', ')': '-.--.-',
|
|
47
|
+
'_': '..--.-', '$': '...-..-',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const PROSIGN_MAP: Record<string, string> = {
|
|
51
|
+
'<AR>': '.-.-.',
|
|
52
|
+
'<SK>': '...-.-',
|
|
53
|
+
'<BT>': '-...-',
|
|
54
|
+
'<KN>': '-.--.',
|
|
55
|
+
'<BK>': '-...-.-',
|
|
56
|
+
'<CL>': '-.-..-..',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const PROSIGN_TOKENS = Object.keys(PROSIGN_MAP).sort((a, b) => b.length - a.length);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Encodes supported CW text into a sequence of timing events.
|
|
63
|
+
*/
|
|
64
|
+
export function encodeTextToCWEvents(text: string, wpm: number): CWTimingEvent[] {
|
|
65
|
+
const events: CWTimingEvent[] = [];
|
|
66
|
+
let nextEventDelayMs = 0;
|
|
67
|
+
|
|
68
|
+
for (const segment of encodeTextToCWKeyStateSegments(text, wpm)) {
|
|
69
|
+
if (!segment.keyDown) {
|
|
70
|
+
nextEventDelayMs += segment.durationMs;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
events.push({ type: 'key-down', afterMs: nextEventDelayMs });
|
|
75
|
+
events.push({ type: 'key-up', afterMs: segment.durationMs });
|
|
76
|
+
nextEventDelayMs = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return events;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Encodes supported CW text into key-down/silence duration segments.
|
|
84
|
+
* This is the shared adapter input for browser sidetone and other local keying outputs.
|
|
85
|
+
*/
|
|
86
|
+
export function encodeTextToCWKeyStateSegments(text: string, wpm: number): CWKeyStateSegment[] {
|
|
87
|
+
const dotMs = getRoundedDotMs(wpm);
|
|
88
|
+
const tokenizedWords = tokenizeText(text);
|
|
89
|
+
const segments: CWKeyStateSegment[] = [];
|
|
90
|
+
let firstToken = true;
|
|
91
|
+
|
|
92
|
+
for (const word of tokenizedWords) {
|
|
93
|
+
for (let ti = 0; ti < word.length; ti += 1) {
|
|
94
|
+
if (!firstToken) {
|
|
95
|
+
appendSegment(segments, false, (ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar) * dotMs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
appendCodeSegments(segments, word[ti], dotMs);
|
|
99
|
+
firstToken = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return segments;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Estimates total CW message duration from the first key-down to the final key-up.
|
|
108
|
+
* Uses the PARIS timing base from Hamlib: dot milliseconds = 1200 / WPM.
|
|
109
|
+
*/
|
|
110
|
+
export function estimateCWMessageDurationMs(text: string, wpm: number): number {
|
|
111
|
+
const safeWpm = getSafeWpm(wpm);
|
|
112
|
+
const dotUnits = countCWMessageDotUnits(text);
|
|
113
|
+
if (dotUnits <= 0) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
return Math.ceil(dotUnits * (1200 / safeWpm));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function appendCodeSegments(segments: CWKeyStateSegment[], code: string, dotMs: number): void {
|
|
120
|
+
for (let si = 0; si < code.length; si += 1) {
|
|
121
|
+
if (si > 0) {
|
|
122
|
+
appendSegment(segments, false, DOT_UNITS.intraChar * dotMs);
|
|
123
|
+
}
|
|
124
|
+
appendSegment(segments, true, (code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit) * dotMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function appendSegment(segments: CWKeyStateSegment[], keyDown: boolean, durationMs: number): void {
|
|
129
|
+
if (durationMs <= 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const previous = segments[segments.length - 1];
|
|
133
|
+
if (previous?.keyDown === keyDown) {
|
|
134
|
+
previous.durationMs += durationMs;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
segments.push({ keyDown, durationMs });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function countCWMessageDotUnits(text: string): number {
|
|
141
|
+
const tokenizedWords = tokenizeText(text);
|
|
142
|
+
let firstToken = true;
|
|
143
|
+
let totalUnits = 0;
|
|
144
|
+
|
|
145
|
+
for (const word of tokenizedWords) {
|
|
146
|
+
for (let ti = 0; ti < word.length; ti += 1) {
|
|
147
|
+
if (!firstToken) {
|
|
148
|
+
totalUnits += ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const code = word[ti];
|
|
152
|
+
for (let si = 0; si < code.length; si += 1) {
|
|
153
|
+
totalUnits += si === 0 ? 0 : DOT_UNITS.intraChar;
|
|
154
|
+
totalUnits += code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit;
|
|
155
|
+
}
|
|
156
|
+
firstToken = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return totalUnits;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function tokenizeText(text: string): string[][] {
|
|
164
|
+
const normalized = text.toUpperCase().trim();
|
|
165
|
+
if (!normalized) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return normalized
|
|
170
|
+
.split(/\s+/)
|
|
171
|
+
.map(tokenizeWord)
|
|
172
|
+
.filter((word) => word.length > 0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function tokenizeWord(word: string): string[] {
|
|
176
|
+
const codes: string[] = [];
|
|
177
|
+
let index = 0;
|
|
178
|
+
|
|
179
|
+
while (index < word.length) {
|
|
180
|
+
const prosign = PROSIGN_TOKENS.find((token) => word.startsWith(token, index));
|
|
181
|
+
if (prosign) {
|
|
182
|
+
codes.push(PROSIGN_MAP[prosign]);
|
|
183
|
+
index += prosign.length;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const code = MORSE_TABLE[word[index]];
|
|
188
|
+
if (code) {
|
|
189
|
+
codes.push(code);
|
|
190
|
+
}
|
|
191
|
+
index += 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return codes;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getRoundedDotMs(wpm: number): number {
|
|
198
|
+
return Math.round(1200 / getSafeWpm(wpm));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getSafeWpm(wpm: number): number {
|
|
202
|
+
return Math.max(5, Math.min(60, Math.round(Number(wpm) || 20)));
|
|
203
|
+
}
|