@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,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,9 @@
1
+ const CALLSIGN_WHITESPACE_REGEX = /\s+/g;
2
+
3
+ export function sanitizeCallsignInput(callsign?: string | null): string {
4
+ if (!callsign) {
5
+ return '';
6
+ }
7
+
8
+ return callsign.toUpperCase().replace(CALLSIGN_WHITESPACE_REGEX, '');
9
+ }
@@ -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
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DecodeRequestSchema } from '../src/schema/slot-info.schema.js';
3
+
4
+ function makePcm(): ArrayBuffer {
5
+ return new Float32Array(16).buffer;
6
+ }
7
+
8
+ describe('DecodeRequestSchema', () => {
9
+ it('accepts legacy decode requests without AP context', () => {
10
+ const parsed = DecodeRequestSchema.parse({
11
+ slotId: 'FT8-1-15000',
12
+ mode: 'FT8',
13
+ windowIdx: 0,
14
+ pcm: makePcm(),
15
+ sampleRate: 12000,
16
+ windowOffsetMs: 0,
17
+ });
18
+
19
+ expect(parsed.apContext).toBeUndefined();
20
+ });
21
+
22
+ it('accepts a single AP decode context', () => {
23
+ const parsed = DecodeRequestSchema.parse({
24
+ slotId: 'FT8-1-15000',
25
+ mode: 'FT8',
26
+ windowIdx: 1,
27
+ pcm: makePcm(),
28
+ sampleRate: 12000,
29
+ windowOffsetMs: 0,
30
+ apContext: {
31
+ operatorId: 'op1',
32
+ myCall: 'BG4IAJ',
33
+ myGrid: 'OM96',
34
+ dxCall: 'JA1AAA',
35
+ dxGrid: 'PM95',
36
+ frequencyHz: 1500,
37
+ qsoProgress: 4,
38
+ currentSlot: 'TX4',
39
+ },
40
+ });
41
+
42
+ expect(parsed.apContext).toEqual(expect.objectContaining({
43
+ operatorId: 'op1',
44
+ qsoProgress: 4,
45
+ currentSlot: 'TX4',
46
+ }));
47
+ });
48
+ });