@tx5dr/contracts 1.7.2 → 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.
Files changed (142) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/ability.test.d.ts +2 -0
  6. package/dist/schema/__tests__/ability.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/ability.test.js +32 -0
  8. package/dist/schema/__tests__/ability.test.js.map +1 -0
  9. package/dist/schema/__tests__/audio.schema.test.js +4 -0
  10. package/dist/schema/__tests__/audio.schema.test.js.map +1 -1
  11. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/cw-decoder.schema.test.js +114 -0
  14. package/dist/schema/__tests__/cw-decoder.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/device-ui.schema.test.d.ts +2 -0
  16. package/dist/schema/__tests__/device-ui.schema.test.d.ts.map +1 -0
  17. package/dist/schema/__tests__/device-ui.schema.test.js +111 -0
  18. package/dist/schema/__tests__/device-ui.schema.test.js.map +1 -0
  19. package/dist/schema/__tests__/operator.schema.test.d.ts +2 -0
  20. package/dist/schema/__tests__/operator.schema.test.d.ts.map +1 -0
  21. package/dist/schema/__tests__/operator.schema.test.js +23 -0
  22. package/dist/schema/__tests__/operator.schema.test.js.map +1 -0
  23. package/dist/schema/__tests__/process-monitor.schema.test.d.ts +2 -0
  24. package/dist/schema/__tests__/process-monitor.schema.test.d.ts.map +1 -0
  25. package/dist/schema/__tests__/process-monitor.schema.test.js +235 -0
  26. package/dist/schema/__tests__/process-monitor.schema.test.js.map +1 -0
  27. package/dist/schema/__tests__/radio.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/radio.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/radio.schema.test.js +143 -0
  30. package/dist/schema/__tests__/radio.schema.test.js.map +1 -0
  31. package/dist/schema/ability.d.ts +3 -1
  32. package/dist/schema/ability.d.ts.map +1 -1
  33. package/dist/schema/ability.js +11 -0
  34. package/dist/schema/ability.js.map +1 -1
  35. package/dist/schema/api-response.schema.d.ts +170 -3
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +22 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/audio.schema.d.ts +369 -0
  40. package/dist/schema/audio.schema.d.ts.map +1 -1
  41. package/dist/schema/audio.schema.js +3 -0
  42. package/dist/schema/audio.schema.js.map +1 -1
  43. package/dist/schema/cpu-profile.schema.d.ts +6 -6
  44. package/dist/schema/cw-decoder.schema.d.ts +2501 -0
  45. package/dist/schema/cw-decoder.schema.d.ts.map +1 -0
  46. package/dist/schema/cw-decoder.schema.js +196 -0
  47. package/dist/schema/cw-decoder.schema.js.map +1 -0
  48. package/dist/schema/cw-keyer.schema.d.ts +368 -0
  49. package/dist/schema/cw-keyer.schema.d.ts.map +1 -0
  50. package/dist/schema/cw-keyer.schema.js +143 -0
  51. package/dist/schema/cw-keyer.schema.js.map +1 -0
  52. package/dist/schema/desktop-https.schema.js +1 -1
  53. package/dist/schema/desktop-https.schema.js.map +1 -1
  54. package/dist/schema/device-ui.schema.d.ts +1782 -0
  55. package/dist/schema/device-ui.schema.d.ts.map +1 -0
  56. package/dist/schema/device-ui.schema.js +156 -0
  57. package/dist/schema/device-ui.schema.js.map +1 -0
  58. package/dist/schema/logbook.schema.d.ts +2 -2
  59. package/dist/schema/mode.schema.d.ts +1 -0
  60. package/dist/schema/mode.schema.d.ts.map +1 -1
  61. package/dist/schema/mode.schema.js +8 -0
  62. package/dist/schema/mode.schema.js.map +1 -1
  63. package/dist/schema/operator.schema.d.ts +10 -10
  64. package/dist/schema/operator.schema.d.ts.map +1 -1
  65. package/dist/schema/operator.schema.js +6 -1
  66. package/dist/schema/operator.schema.js.map +1 -1
  67. package/dist/schema/plugin.schema.d.ts +653 -70
  68. package/dist/schema/plugin.schema.d.ts.map +1 -1
  69. package/dist/schema/plugin.schema.js +22 -2
  70. package/dist/schema/plugin.schema.js.map +1 -1
  71. package/dist/schema/process-monitor.schema.d.ts +3102 -42
  72. package/dist/schema/process-monitor.schema.d.ts.map +1 -1
  73. package/dist/schema/process-monitor.schema.js +54 -0
  74. package/dist/schema/process-monitor.schema.js.map +1 -1
  75. package/dist/schema/radio-capability.schema.d.ts +7 -7
  76. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  77. package/dist/schema/radio-capability.schema.js +41 -0
  78. package/dist/schema/radio-capability.schema.js.map +1 -1
  79. package/dist/schema/radio-power.schema.d.ts +6 -0
  80. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  81. package/dist/schema/radio-profile.schema.d.ts +86 -0
  82. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  83. package/dist/schema/radio.schema.d.ts +166 -9
  84. package/dist/schema/radio.schema.d.ts.map +1 -1
  85. package/dist/schema/radio.schema.js +82 -0
  86. package/dist/schema/radio.schema.js.map +1 -1
  87. package/dist/schema/realtime.schema.d.ts +6 -6
  88. package/dist/schema/slot-info.schema.d.ts +136 -0
  89. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  90. package/dist/schema/slot-info.schema.js +27 -2
  91. package/dist/schema/slot-info.schema.js.map +1 -1
  92. package/dist/schema/spectrum.schema.d.ts +24 -24
  93. package/dist/schema/voice-keyer.schema.d.ts +2 -2
  94. package/dist/schema/voice.schema.d.ts +1 -1
  95. package/dist/schema/voice.schema.d.ts.map +1 -1
  96. package/dist/schema/voice.schema.js +1 -1
  97. package/dist/schema/voice.schema.js.map +1 -1
  98. package/dist/schema/websocket.schema.d.ts +6066 -639
  99. package/dist/schema/websocket.schema.d.ts.map +1 -1
  100. package/dist/schema/websocket.schema.js +106 -4
  101. package/dist/schema/websocket.schema.js.map +1 -1
  102. package/dist/utils/__tests__/cwTiming.test.d.ts +2 -0
  103. package/dist/utils/__tests__/cwTiming.test.d.ts.map +1 -0
  104. package/dist/utils/__tests__/cwTiming.test.js +74 -0
  105. package/dist/utils/__tests__/cwTiming.test.js.map +1 -0
  106. package/dist/utils/callsign.d.ts +2 -0
  107. package/dist/utils/callsign.d.ts.map +1 -0
  108. package/dist/utils/callsign.js +8 -0
  109. package/dist/utils/callsign.js.map +1 -0
  110. package/dist/utils/cwTiming.d.ts +36 -0
  111. package/dist/utils/cwTiming.d.ts.map +1 -0
  112. package/dist/utils/cwTiming.js +155 -0
  113. package/dist/utils/cwTiming.js.map +1 -0
  114. package/package.json +1 -1
  115. package/src/index.ts +9 -0
  116. package/src/schema/__tests__/ability.test.ts +40 -0
  117. package/src/schema/__tests__/audio.schema.test.ts +4 -0
  118. package/src/schema/__tests__/cw-decoder.schema.test.ts +127 -0
  119. package/src/schema/__tests__/device-ui.schema.test.ts +122 -0
  120. package/src/schema/__tests__/operator.schema.test.ts +29 -0
  121. package/src/schema/__tests__/process-monitor.schema.test.ts +245 -0
  122. package/src/schema/__tests__/radio.schema.test.ts +160 -0
  123. package/src/schema/ability.ts +13 -0
  124. package/src/schema/api-response.schema.ts +22 -0
  125. package/src/schema/audio.schema.ts +3 -0
  126. package/src/schema/cw-decoder.schema.ts +234 -0
  127. package/src/schema/cw-keyer.schema.ts +177 -0
  128. package/src/schema/desktop-https.schema.ts +1 -1
  129. package/src/schema/device-ui.schema.ts +182 -0
  130. package/src/schema/mode.schema.ts +8 -0
  131. package/src/schema/operator.schema.ts +7 -1
  132. package/src/schema/plugin.schema.ts +28 -2
  133. package/src/schema/process-monitor.schema.ts +69 -0
  134. package/src/schema/radio-capability.schema.ts +41 -0
  135. package/src/schema/radio.schema.ts +97 -0
  136. package/src/schema/slot-info.schema.ts +33 -2
  137. package/src/schema/voice.schema.ts +1 -1
  138. package/src/schema/websocket.schema.ts +147 -4
  139. package/src/utils/__tests__/cwTiming.test.ts +91 -0
  140. package/src/utils/callsign.ts +9 -0
  141. package/src/utils/cwTiming.ts +203 -0
  142. package/test/decode-request-schema.test.ts +48 -0
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { encodeTextToCWEvents, encodeTextToCWKeyStateSegments, estimateCWMessageDurationMs, } from '../cwTiming.js';
3
+ function totalSegmentDuration(text, wpm) {
4
+ return encodeTextToCWKeyStateSegments(text, wpm).reduce((sum, segment) => sum + segment.durationMs, 0);
5
+ }
6
+ describe('CW timing utilities', () => {
7
+ it('returns no events and zero duration for blank text', () => {
8
+ expect(encodeTextToCWEvents(' ', 20)).toEqual([]);
9
+ expect(encodeTextToCWKeyStateSegments(' ', 20)).toEqual([]);
10
+ expect(estimateCWMessageDurationMs('', 20)).toBe(0);
11
+ });
12
+ it('encodes exact key events for simple characters and spacing', () => {
13
+ expect(encodeTextToCWEvents('E', 20)).toEqual([
14
+ { type: 'key-down', afterMs: 0 },
15
+ { type: 'key-up', afterMs: 60 },
16
+ ]);
17
+ expect(encodeTextToCWEvents('EE', 20)).toEqual([
18
+ { type: 'key-down', afterMs: 0 },
19
+ { type: 'key-up', afterMs: 60 },
20
+ { type: 'key-down', afterMs: 180 },
21
+ { type: 'key-up', afterMs: 60 },
22
+ ]);
23
+ expect(encodeTextToCWEvents('E E', 20)).toEqual([
24
+ { type: 'key-down', afterMs: 0 },
25
+ { type: 'key-up', afterMs: 60 },
26
+ { type: 'key-down', afterMs: 420 },
27
+ { type: 'key-up', afterMs: 60 },
28
+ ]);
29
+ expect(encodeTextToCWEvents('A', 20)).toEqual([
30
+ { type: 'key-down', afterMs: 0 },
31
+ { type: 'key-up', afterMs: 60 },
32
+ { type: 'key-down', afterMs: 60 },
33
+ { type: 'key-up', afterMs: 180 },
34
+ ]);
35
+ });
36
+ it('encodes exact key state segments for browser sidetone adapters', () => {
37
+ expect(encodeTextToCWKeyStateSegments('E', 20)).toEqual([
38
+ { keyDown: true, durationMs: 60 },
39
+ ]);
40
+ expect(encodeTextToCWKeyStateSegments('EE', 20)).toEqual([
41
+ { keyDown: true, durationMs: 60 },
42
+ { keyDown: false, durationMs: 180 },
43
+ { keyDown: true, durationMs: 60 },
44
+ ]);
45
+ expect(encodeTextToCWKeyStateSegments('E E', 20)).toEqual([
46
+ { keyDown: true, durationMs: 60 },
47
+ { keyDown: false, durationMs: 420 },
48
+ { keyDown: true, durationMs: 60 },
49
+ ]);
50
+ expect(encodeTextToCWKeyStateSegments('A', 20)).toEqual([
51
+ { keyDown: true, durationMs: 60 },
52
+ { keyDown: false, durationMs: 60 },
53
+ { keyDown: true, durationMs: 180 },
54
+ ]);
55
+ });
56
+ it('scales duration by WPM', () => {
57
+ expect(estimateCWMessageDurationMs('EE', 20)).toBe(300);
58
+ expect(estimateCWMessageDurationMs('EE', 40)).toBe(150);
59
+ });
60
+ it('uses the Hamlib/PARIS exact dot duration for non-divisible WPM values', () => {
61
+ expect(estimateCWMessageDurationMs('EE', 17)).toBe(353);
62
+ });
63
+ it('treats prosigns as single Morse tokens rather than re-encoding dots and dashes', () => {
64
+ expect(estimateCWMessageDurationMs('<SK>', 20)).toBe(900);
65
+ expect(totalSegmentDuration('<SK>', 20)).toBe(900);
66
+ expect(totalSegmentDuration('<CL>', 20)).toBeGreaterThan(0);
67
+ });
68
+ it('keeps unsupported-character and WPM clamp behavior', () => {
69
+ expect(estimateCWMessageDurationMs('E☃E', 20)).toBe(300);
70
+ expect(estimateCWMessageDurationMs('E', 1)).toBe(240);
71
+ expect(estimateCWMessageDurationMs('E', 1000)).toBe(20);
72
+ });
73
+ });
74
+ //# sourceMappingURL=cwTiming.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/cwTiming.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeCallsignInput(callsign?: string | null): string;
2
+ //# sourceMappingURL=callsign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callsign.d.ts","sourceRoot":"","sources":["../../src/utils/callsign.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMtE"}
@@ -0,0 +1,8 @@
1
+ const CALLSIGN_WHITESPACE_REGEX = /\s+/g;
2
+ export function sanitizeCallsignInput(callsign) {
3
+ if (!callsign) {
4
+ return '';
5
+ }
6
+ return callsign.toUpperCase().replace(CALLSIGN_WHITESPACE_REGEX, '');
7
+ }
8
+ //# sourceMappingURL=callsign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callsign.js","sourceRoot":"","sources":["../../src/utils/callsign.ts"],"names":[],"mappings":"AAAA,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEzC,MAAM,UAAU,qBAAqB,CAAC,QAAwB;IAC5D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC"}
@@ -0,0 +1,36 @@
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
+ export interface CWKeyStateSegment {
17
+ /** true when the key/tone is closed, false for silence/open key. */
18
+ keyDown: boolean;
19
+ /** Segment duration in milliseconds. */
20
+ durationMs: number;
21
+ }
22
+ /**
23
+ * Encodes supported CW text into a sequence of timing events.
24
+ */
25
+ export declare function encodeTextToCWEvents(text: string, wpm: number): CWTimingEvent[];
26
+ /**
27
+ * Encodes supported CW text into key-down/silence duration segments.
28
+ * This is the shared adapter input for browser sidetone and other local keying outputs.
29
+ */
30
+ export declare function encodeTextToCWKeyStateSegments(text: string, wpm: number): CWKeyStateSegment[];
31
+ /**
32
+ * Estimates total CW message duration from the first key-down to the final key-up.
33
+ * Uses the PARIS timing base from Hamlib: dot milliseconds = 1200 / WPM.
34
+ */
35
+ export declare function estimateCWMessageDurationMs(text: string, wpm: number): number;
36
+ //# sourceMappingURL=cwTiming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.d.ts","sourceRoot":"","sources":["../../src/utils/cwTiming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;CACpB;AAuCD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAgB/E;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkB7F;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7E"}
@@ -0,0 +1,155 @@
1
+ const DOT_UNITS = {
2
+ dit: 1,
3
+ dah: 3,
4
+ intraChar: 1,
5
+ interChar: 3,
6
+ interWord: 7,
7
+ };
8
+ const MORSE_TABLE = {
9
+ A: '.-', B: '-...', C: '-.-.', D: '-..',
10
+ E: '.', F: '..-.', G: '--.', H: '....',
11
+ I: '..', J: '.---', K: '-.-', L: '.-..',
12
+ M: '--', N: '-.', O: '---', P: '.--.',
13
+ Q: '--.-', R: '.-.', S: '...', T: '-',
14
+ U: '..-', V: '...-', W: '.--', X: '-..-',
15
+ Y: '-.--', Z: '--..',
16
+ '0': '-----', '1': '.----', '2': '..---', '3': '...--',
17
+ '4': '....-', '5': '.....', '6': '-....', '7': '--...',
18
+ '8': '---..', '9': '----.',
19
+ '.': '.-.-.-', ',': '--..--', '?': '..--..', '/': '-..-.',
20
+ '@': '.--.-.', '=': '-...-', '-': '-....-', '+': '.-.-.',
21
+ ':': '---...', ';': '-.-.-.', '\'': '.----.', '"': '.-..-.',
22
+ '!': '-.-.--', '&': '.-...', '(': '-.--.', ')': '-.--.-',
23
+ '_': '..--.-', '$': '...-..-',
24
+ };
25
+ const PROSIGN_MAP = {
26
+ '<AR>': '.-.-.',
27
+ '<SK>': '...-.-',
28
+ '<BT>': '-...-',
29
+ '<KN>': '-.--.',
30
+ '<BK>': '-...-.-',
31
+ '<CL>': '-.-..-..',
32
+ };
33
+ const PROSIGN_TOKENS = Object.keys(PROSIGN_MAP).sort((a, b) => b.length - a.length);
34
+ /**
35
+ * Encodes supported CW text into a sequence of timing events.
36
+ */
37
+ export function encodeTextToCWEvents(text, wpm) {
38
+ const events = [];
39
+ let nextEventDelayMs = 0;
40
+ for (const segment of encodeTextToCWKeyStateSegments(text, wpm)) {
41
+ if (!segment.keyDown) {
42
+ nextEventDelayMs += segment.durationMs;
43
+ continue;
44
+ }
45
+ events.push({ type: 'key-down', afterMs: nextEventDelayMs });
46
+ events.push({ type: 'key-up', afterMs: segment.durationMs });
47
+ nextEventDelayMs = 0;
48
+ }
49
+ return events;
50
+ }
51
+ /**
52
+ * Encodes supported CW text into key-down/silence duration segments.
53
+ * This is the shared adapter input for browser sidetone and other local keying outputs.
54
+ */
55
+ export function encodeTextToCWKeyStateSegments(text, wpm) {
56
+ const dotMs = getRoundedDotMs(wpm);
57
+ const tokenizedWords = tokenizeText(text);
58
+ const segments = [];
59
+ let firstToken = true;
60
+ for (const word of tokenizedWords) {
61
+ for (let ti = 0; ti < word.length; ti += 1) {
62
+ if (!firstToken) {
63
+ appendSegment(segments, false, (ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar) * dotMs);
64
+ }
65
+ appendCodeSegments(segments, word[ti], dotMs);
66
+ firstToken = false;
67
+ }
68
+ }
69
+ return segments;
70
+ }
71
+ /**
72
+ * Estimates total CW message duration from the first key-down to the final key-up.
73
+ * Uses the PARIS timing base from Hamlib: dot milliseconds = 1200 / WPM.
74
+ */
75
+ export function estimateCWMessageDurationMs(text, wpm) {
76
+ const safeWpm = getSafeWpm(wpm);
77
+ const dotUnits = countCWMessageDotUnits(text);
78
+ if (dotUnits <= 0) {
79
+ return 0;
80
+ }
81
+ return Math.ceil(dotUnits * (1200 / safeWpm));
82
+ }
83
+ function appendCodeSegments(segments, code, dotMs) {
84
+ for (let si = 0; si < code.length; si += 1) {
85
+ if (si > 0) {
86
+ appendSegment(segments, false, DOT_UNITS.intraChar * dotMs);
87
+ }
88
+ appendSegment(segments, true, (code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit) * dotMs);
89
+ }
90
+ }
91
+ function appendSegment(segments, keyDown, durationMs) {
92
+ if (durationMs <= 0) {
93
+ return;
94
+ }
95
+ const previous = segments[segments.length - 1];
96
+ if (previous?.keyDown === keyDown) {
97
+ previous.durationMs += durationMs;
98
+ return;
99
+ }
100
+ segments.push({ keyDown, durationMs });
101
+ }
102
+ function countCWMessageDotUnits(text) {
103
+ const tokenizedWords = tokenizeText(text);
104
+ let firstToken = true;
105
+ let totalUnits = 0;
106
+ for (const word of tokenizedWords) {
107
+ for (let ti = 0; ti < word.length; ti += 1) {
108
+ if (!firstToken) {
109
+ totalUnits += ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar;
110
+ }
111
+ const code = word[ti];
112
+ for (let si = 0; si < code.length; si += 1) {
113
+ totalUnits += si === 0 ? 0 : DOT_UNITS.intraChar;
114
+ totalUnits += code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit;
115
+ }
116
+ firstToken = false;
117
+ }
118
+ }
119
+ return totalUnits;
120
+ }
121
+ function tokenizeText(text) {
122
+ const normalized = text.toUpperCase().trim();
123
+ if (!normalized) {
124
+ return [];
125
+ }
126
+ return normalized
127
+ .split(/\s+/)
128
+ .map(tokenizeWord)
129
+ .filter((word) => word.length > 0);
130
+ }
131
+ function tokenizeWord(word) {
132
+ const codes = [];
133
+ let index = 0;
134
+ while (index < word.length) {
135
+ const prosign = PROSIGN_TOKENS.find((token) => word.startsWith(token, index));
136
+ if (prosign) {
137
+ codes.push(PROSIGN_MAP[prosign]);
138
+ index += prosign.length;
139
+ continue;
140
+ }
141
+ const code = MORSE_TABLE[word[index]];
142
+ if (code) {
143
+ codes.push(code);
144
+ }
145
+ index += 1;
146
+ }
147
+ return codes;
148
+ }
149
+ function getRoundedDotMs(wpm) {
150
+ return Math.round(1200 / getSafeWpm(wpm));
151
+ }
152
+ function getSafeWpm(wpm) {
153
+ return Math.max(5, Math.min(60, Math.round(Number(wpm) || 20)));
154
+ }
155
+ //# sourceMappingURL=cwTiming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.js","sourceRoot":"","sources":["../../src/utils/cwTiming.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tx5dr/contracts",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -118,11 +118,17 @@ export * from './schema/system.schema.js';
118
118
  export * from './schema/voice.schema.js';
119
119
  export * from './schema/voice-keyer.schema.js';
120
120
 
121
+ // CW Keyer Schema
122
+ export * from './schema/cw-keyer.schema.js';
123
+ export * from './schema/cw-decoder.schema.js';
124
+
121
125
  // Station Info Schema
122
126
  export * from './schema/station-info.schema.js';
123
127
 
124
128
  // Grid Utils
125
129
  export * from './utils/grid.js';
130
+ export * from './utils/callsign.js';
131
+ export * from './utils/cwTiming.js';
126
132
 
127
133
  // Process Monitor Schema
128
134
  export * from './schema/process-monitor.schema.js';
@@ -152,6 +158,9 @@ export * from './schema/realtime.schema.js';
152
158
  // Desktop HTTPS Schema
153
159
  export * from './schema/desktop-https.schema.js';
154
160
 
161
+ // Device UI Schema
162
+ export * from './schema/device-ui.schema.js';
163
+
155
164
  // Plugin Schema
156
165
  export * from './schema/plugin.schema.js';
157
166
  export * from './schema/update.schema.js';
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildAbilityRules,
4
+ Permission,
5
+ PERMISSION_GROUPS,
6
+ PERMISSION_RULE_MAP,
7
+ } from '../ability.js';
8
+ import { UserRole } from '../auth.schema.js';
9
+
10
+ describe('ability contracts', () => {
11
+ it('maps CW decoder permissions to separate control and config capabilities', () => {
12
+ expect(PERMISSION_RULE_MAP[Permission.CW_DECODER_CONTROL]).toEqual({
13
+ action: 'execute',
14
+ subject: 'CWDecoder',
15
+ });
16
+ expect(PERMISSION_RULE_MAP[Permission.CW_DECODER_CONFIG]).toEqual({
17
+ action: 'update',
18
+ subject: 'CWDecoderConfig',
19
+ });
20
+ });
21
+
22
+ it('keeps CW decoder permissions delegatable instead of granting them to every operator', () => {
23
+ expect(buildAbilityRules({ role: UserRole.OPERATOR, operatorIds: ['op1'] }))
24
+ .not.toContainEqual({ action: 'execute', subject: 'CWDecoder' });
25
+
26
+ expect(buildAbilityRules({
27
+ role: UserRole.OPERATOR,
28
+ operatorIds: ['op1'],
29
+ permissionGrants: [{ permission: Permission.CW_DECODER_CONTROL }],
30
+ })).toContainEqual({ action: 'execute', subject: 'CWDecoder' });
31
+ });
32
+
33
+ it('exposes CW permissions in the token permission groups', () => {
34
+ const cwGroup = PERMISSION_GROUPS.find(group => group.key === 'cw');
35
+ expect(cwGroup?.permissions).toEqual([
36
+ Permission.CW_DECODER_CONTROL,
37
+ Permission.CW_DECODER_CONFIG,
38
+ ]);
39
+ });
40
+ });
@@ -15,6 +15,9 @@ const device = {
15
15
  sampleRate: 48000,
16
16
  sampleRates: [44100, 48000],
17
17
  type: 'input' as const,
18
+ availability: 'available' as const,
19
+ isActiveByTx5dr: false,
20
+ lastSeenAt: 1_700_000_000_000,
18
21
  };
19
22
 
20
23
  describe('audio device resolution schemas', () => {
@@ -70,6 +73,7 @@ describe('audio device resolution schemas', () => {
70
73
  });
71
74
 
72
75
  expect(parsed.inputDevices[0]?.sampleRates).toEqual([44100, 48000]);
76
+ expect(parsed.inputDevices[0]?.availability).toBe('available');
73
77
  expect(parsed.inputBufferSizes).toContain(768);
74
78
  });
75
79
 
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CWDecoderConfigSchema,
4
+ CWDecoderEventSchema,
5
+ CWDecoderStatusSchema,
6
+ CWDecoderTuningUpdateSchema,
7
+ } from '../cw-decoder.schema.js';
8
+
9
+ describe('CW decoder contracts', () => {
10
+ it('parses v1 defaults', () => {
11
+ expect(CWDecoderConfigSchema.parse({})).toMatchObject({
12
+ enabled: false,
13
+ backend: 'deepcw-onnx',
14
+ runtimeBackend: 'cpu',
15
+ modelSize: 'tiny',
16
+ language: 'en',
17
+ mode: 'streaming',
18
+ targetFreqHz: 800,
19
+ filterWidthHz: 800,
20
+ windowSeconds: 12,
21
+ decodeIntervalMs: 1000,
22
+ muteWhileTransmitting: true,
23
+ workerCount: 1,
24
+ minCommitChars: 1,
25
+ commitStability: 2,
26
+ maxPendingAgeMs: 4000,
27
+ });
28
+ });
29
+
30
+ it('accepts pending and commit transcript events', () => {
31
+ expect(CWDecoderEventSchema.parse({
32
+ kind: 'pending',
33
+ text: 'CQ TEST',
34
+ confidence: 0.8,
35
+ timestamp: 1,
36
+ })).toMatchObject({ kind: 'pending', text: 'CQ TEST' });
37
+
38
+ expect(CWDecoderEventSchema.parse({
39
+ kind: 'commit',
40
+ text: 'CQ TEST',
41
+ confidence: 0.9,
42
+ timestamp: 2,
43
+ segment: {
44
+ id: 'seg-1',
45
+ sessionId: 'session-1',
46
+ sequence: 1,
47
+ text: 'CQ TEST',
48
+ startedAt: 1,
49
+ updatedAt: 2,
50
+ endedAt: 2,
51
+ confidence: 0.9,
52
+ finalized: true,
53
+ prependSpace: true,
54
+ },
55
+ })).toMatchObject({ kind: 'commit', segment: { finalized: true } });
56
+ });
57
+
58
+ it('accepts structured transcript reset, pending, and commit events', () => {
59
+ expect(CWDecoderEventSchema.parse({
60
+ kind: 'transcript_reset',
61
+ sessionId: 'session-1',
62
+ timestamp: 1,
63
+ })).toMatchObject({ kind: 'transcript_reset', sessionId: 'session-1' });
64
+
65
+ expect(CWDecoderEventSchema.parse({
66
+ kind: 'transcript_pending',
67
+ pending: {
68
+ sessionId: 'session-1',
69
+ version: 1,
70
+ text: 'CQ TE',
71
+ plainText: 'CQ TE',
72
+ finalized: false,
73
+ confidence: 0.7,
74
+ targetFreqHz: 800,
75
+ filterWidthHz: 800,
76
+ updatedAt: 2,
77
+ },
78
+ timestamp: 2,
79
+ })).toMatchObject({ kind: 'transcript_pending', pending: { finalized: false, version: 1 } });
80
+
81
+ expect(CWDecoderEventSchema.parse({
82
+ kind: 'transcript_commit',
83
+ segment: {
84
+ id: 'seg-1',
85
+ sessionId: 'session-1',
86
+ sequence: 1,
87
+ text: 'CQ TEST',
88
+ plainText: 'CQ TEST',
89
+ finalized: true,
90
+ prependSpace: false,
91
+ confidence: 0.9,
92
+ targetFreqHz: 800,
93
+ filterWidthHz: 800,
94
+ characterSpans: [{ char: 'C', startFrame: 1, endFrame: 2 }],
95
+ wordSpaceSpans: [{ startFrame: 10, endFrame: 12 }],
96
+ startedAt: 1,
97
+ endedAt: 3,
98
+ updatedAt: 3,
99
+ },
100
+ timestamp: 3,
101
+ })).toMatchObject({ kind: 'transcript_commit', segment: { sequence: 1, prependSpace: false } });
102
+ });
103
+
104
+ it('rejects model sizes that are not packaged in v1', () => {
105
+ expect(() => CWDecoderConfigSchema.parse({ modelSize: 'base' })).toThrow();
106
+ });
107
+
108
+ it('validates runtime tuning updates', () => {
109
+ expect(CWDecoderTuningUpdateSchema.parse({ targetFreqHz: 650 })).toEqual({ targetFreqHz: 650 });
110
+ expect(CWDecoderTuningUpdateSchema.parse({ filterWidthHz: 250 })).toEqual({ filterWidthHz: 250 });
111
+ expect(() => CWDecoderTuningUpdateSchema.parse({})).toThrow();
112
+ expect(() => CWDecoderTuningUpdateSchema.parse({ targetFreqHz: 50 })).toThrow();
113
+ expect(() => CWDecoderTuningUpdateSchema.parse({ filterWidthHz: 900 })).toThrow();
114
+ });
115
+
116
+ it('represents muted listening state', () => {
117
+ const config = CWDecoderConfigSchema.parse({ enabled: true });
118
+ expect(CWDecoderStatusSchema.parse({
119
+ enabled: true,
120
+ state: 'muted',
121
+ config,
122
+ muted: true,
123
+ active: false,
124
+ updatedAt: 3,
125
+ })).toMatchObject({ state: 'muted', muted: true });
126
+ });
127
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ DeviceUiBootstrapSnapshotSchema,
5
+ DeviceUiWsEventSchema,
6
+ DeviceUiJwtPayloadSchema,
7
+ DeviceUiSessionRequestSchema,
8
+ DeviceUiSessionResponseSchema,
9
+ } from '../device-ui.schema.js';
10
+
11
+ describe('device UI schemas', () => {
12
+ it('accepts the dedicated device UI JWT payload only', () => {
13
+ expect(DeviceUiJwtPayloadSchema.parse({
14
+ typ: 'device-ui',
15
+ aud: 'tx5dr-device-ui',
16
+ deviceId: 'panel-1',
17
+ sessionId: 'session-1',
18
+ iat: 1,
19
+ exp: 2,
20
+ }).aud).toBe('tx5dr-device-ui');
21
+
22
+ expect(() => DeviceUiJwtPayloadSchema.parse({
23
+ typ: 'access',
24
+ aud: 'tx5dr-device-ui',
25
+ deviceId: 'panel-1',
26
+ sessionId: 'session-1',
27
+ iat: 1,
28
+ exp: 2,
29
+ })).toThrow();
30
+ });
31
+
32
+ it('describes session request and response contracts', () => {
33
+ expect(DeviceUiSessionRequestSchema.parse({ deviceId: 'panel-1', sessionToken: 'secret' }).deviceId).toBe('panel-1');
34
+ expect(DeviceUiSessionResponseSchema.parse({
35
+ jwt: 'jwt',
36
+ deviceId: 'panel-1',
37
+ sessionId: 'session-1',
38
+ expiresAt: 1_700_000_001_000,
39
+ }).sessionId).toBe('session-1');
40
+ });
41
+
42
+ it('describes the mode-aware bootstrap snapshot and WS event', () => {
43
+ const snapshot = DeviceUiBootstrapSnapshotSchema.parse({
44
+ server: { status: 'ok', version: 'test', webPort: 8076 },
45
+ station: { callsign: 'BG5DRB', callsigns: ['BG5DRB', 'BG5AAA'] },
46
+ operators: [{ id: 'op1', callsign: 'BG5AAA', active: true, transmitting: false, ptt: false }],
47
+ engine: { running: true, mode: 'digital', currentMode: { name: 'FT8', slotMs: 15000 }, state: 'running' },
48
+ radio: { connected: true, frequency: 7074000, radioMode: 'USB-D', ptt: false, tx: false },
49
+ ft8: {
50
+ slot: null,
51
+ utc: null,
52
+ cycle: null,
53
+ periodMs: 15000,
54
+ recentDecodeRawMessages: ['CQ TEST AA00'],
55
+ lastDecodeRawMessage: 'CQ TEST AA00',
56
+ recentFramesSlotId: 'FT8-1',
57
+ recentFramesSlotStartMs: 1_700_000_000_000,
58
+ recentFrames: [{
59
+ slotId: 'FT8-1',
60
+ slotStartMs: 1_700_000_000_000,
61
+ snr: -10,
62
+ freq: 1200,
63
+ dt: 0.1,
64
+ message: 'CQ TEST AA00',
65
+ operatorId: null,
66
+ countryZh: '测试地区',
67
+ countryEn: 'Test Region',
68
+ }],
69
+ currentTx: { active: false, operatorIds: [], messages: [], lastMessage: null, slotStartMs: null },
70
+ },
71
+ voice: {
72
+ active: false,
73
+ radioMode: null,
74
+ pttLocked: false,
75
+ pttLockedByLabel: null,
76
+ keyerActive: false,
77
+ keyerMode: null,
78
+ keyerSlotId: null,
79
+ },
80
+ cw: {
81
+ decoder: {
82
+ enabled: false,
83
+ active: false,
84
+ state: 'disabled',
85
+ muted: false,
86
+ pendingText: '',
87
+ committedText: '',
88
+ lastDecodeAt: null,
89
+ updatedAt: 1,
90
+ },
91
+ keyer: {
92
+ active: false,
93
+ mode: null,
94
+ messageId: null,
95
+ currentText: null,
96
+ lastText: null,
97
+ },
98
+ currentTx: {
99
+ active: false,
100
+ messages: [],
101
+ lastMessage: null,
102
+ },
103
+ },
104
+ access: { localUrl: 'http://192.168.1.10:8076', localUrls: ['http://192.168.1.10:8076'] },
105
+ updatedAt: 1,
106
+ });
107
+
108
+ expect(snapshot.station.callsign).toBe('BG5DRB');
109
+ expect(snapshot.station.callsigns).toEqual(['BG5DRB', 'BG5AAA']);
110
+ expect(snapshot.operators[0]?.callsign).toBe('BG5AAA');
111
+ expect(snapshot.ft8.recentFramesSlotId).toBe('FT8-1');
112
+ expect(snapshot.ft8.recentFrames[0]?.slotId).toBe('FT8-1');
113
+ expect(snapshot.ft8.recentFrames[0]?.countryZh).toBe('测试地区');
114
+ expect(snapshot.ft8.lastDecodeRawMessage).toBe('CQ TEST AA00');
115
+ expect(snapshot.cw.decoder.state).toBe('disabled');
116
+ expect(DeviceUiWsEventSchema.parse({
117
+ type: 'snapshot',
118
+ payload: snapshot,
119
+ timestamp: '2026-05-14T00:00:00.000Z',
120
+ }).payload.server.status).toBe('ok');
121
+ });
122
+ });