Neale Pickett
·
2024-09-18
keyers.mjs
1/**
2 * A number of keyers.
3 *
4 * The document "All About Squeeze-Keying" by Karl Fischer, DJ5IL, was
5 * absolutely instrumental in correctly (I hope) implementing everything more
6 * advanced than the bug keyer.
7 */
8
9import * as RoboKeyer from "./robokeyer.mjs"
10import * as time from "./time.mjs"
11
12/** Silent period between dits and dash */
13const PAUSE = -1
14/** Length of a dit */
15const DIT = 1
16/** Length of a dah */
17const DAH = 3
18
19/**
20 * Queue Set: A Set you can shift and pop.
21 *
22 * Performance of this implementation may be bad for large sets.
23 */
24class QSet extends Set {
25 shift() {
26 let r = [...this].shift()
27 this.delete(r)
28 return r
29 }
30
31 pop() {
32 let r = [...this].pop()
33 this.delete(r)
34 return r
35 }
36}
37
38/**
39 * Definition of a transmitter type.
40 *
41 * The VailClient class implements this.
42 */
43class Transmitter {
44 /** Begin transmitting */
45 BeginTx() {}
46
47 /** End transmitting */
48 EndTx() {}
49}
50
51/**
52 * A straight keyer.
53 *
54 * This is one or more relays wired in parallel. Each relay has an associated
55 * input key. You press any key, and it starts transmitting until all keys are
56 * released.
57*/
58class StraightKeyer {
59 /**
60 * @param {Transmitter} output Transmitter object
61 */
62 constructor(output) {
63 this.output = output
64 this.Reset()
65 }
66
67 /**
68 * Returns a list of names for keys supported by this keyer.
69 *
70 * @returns {Array.<string>} A list of key names
71 */
72 KeyNames() {
73 return ["Key"]
74 }
75
76 /**
77 * Reset state and stop all transmissions.
78 */
79 Reset() {
80 this.output.EndTx()
81 this.txRelays = []
82 }
83
84 /**
85 * Set the duration of dit.
86 *
87 * @param {Duration} d New dit duration
88 */
89 SetDitDuration(d) {
90 this.ditDuration = d
91 }
92
93 /**
94 * Clean up all timers, etc.
95 */
96 Release() {}
97
98 /**
99 * Returns the state of a single transmit relay.
100 *
101 * If n is not provided, return the state of all relays wired in parallel.
102 *
103 * @param {number} n Relay number
104 * @returns {bool} True if relay is closed
105 */
106 TxClosed(n=null) {
107 if (n == null) {
108 return this.txRelays.some(Boolean)
109 }
110 return this.txRelays[n]
111 }
112
113 /**
114 * Close a transmit relay.
115 *
116 * In most of these keyers, you have multiple things that can transmit. In
117 * the circuit, they'd all be wired together in parallel. We instead keep
118 * track of relay state here, and start or stop transmitting based on the
119 * logical of of all relays.
120 *
121 * @param {number} n Relay number
122 * @param {bool} closed True if relay should be closed
123 */
124 Tx(n, closed) {
125 let wasClosed = this.TxClosed()
126 this.txRelays[n] = closed
127 let nowClosed = this.TxClosed()
128
129 if (wasClosed != nowClosed) {
130 if (nowClosed) {
131 this.output.BeginTx()
132 } else {
133 this.output.EndTx()
134 }
135 }
136 }
137
138 /**
139 * React to a key being pressed.
140 *
141 * @param {number} key Which key was pressed
142 * @param {bool} pressed True if the key was pressed
143 */
144 Key(key, pressed) {
145 this.Tx(key, pressed)
146 }
147}
148
149/**
150 * A "Cootie" or "Double Speed Key" is just two straight keys in parallel.
151 */
152class CootieKeyer extends StraightKeyer {
153 KeyNames() {
154 return ["Key", "Key"]
155 }
156}
157
158/**
159 * A Vibroplex "Bug".
160 *
161 * Left key send dits over and over until you let go.
162 * Right key works just like a stright key.
163 */
164class BugKeyer extends StraightKeyer {
165 KeyNames() {
166 return ["Dit ", "Key"]
167 }
168
169 Reset() {
170 super.Reset()
171 this.SetDitDuration(100 * time.Millisecond)
172 if (this.pulseTimer) {
173 clearInterval(this.pulseTimer)
174 this.pulseTimer = null
175 }
176 this.keyPressed = [false, false]
177 }
178
179 Key(key, pressed) {
180 this.keyPressed[key] = pressed
181 if (key == 0) {
182 this.beginPulsing()
183 } else {
184 super.Key(key, pressed)
185 }
186 }
187
188 /**
189 * Begin a pulse if it hasn't already begun
190 */
191 beginPulsing() {
192 if (!this.pulseTimer) {
193 this.pulse()
194 }
195 }
196
197 pulse() {
198 if (this.TxClosed(0)) {
199 // If we were transmitting, pause
200 this.Tx(0, false)
201 } else if (this.keyPressed[0]) {
202 // If the key was pressed, transmit
203 this.Tx(0, true)
204 } else {
205 // If the key wasn't pressed, stop pulsing
206 this.pulseTimer = null
207 return
208 }
209 this.pulseTimer = setTimeout(() => this.pulse(), this.ditDuration)
210 }
211}
212
213/**
214 * Electronic Bug Keyer
215 *
216 * Repeats both dits and dahs, ensuring proper pauses.
217 *
218 * I think the original ElBug Keyers did not have two paddles, so I've taken the
219 * liberty of making it so that whatever you pressed last is what gets repeated,
220 * similar to a modern computer keyboard.
221 */
222class ElBugKeyer extends BugKeyer {
223 KeyNames() {
224 return ["Dit ", "Dah"]
225 }
226
227 Reset() {
228 super.Reset()
229 this.nextRepeat = -1 // What to send next, if we're repeating
230 }
231
232 Key(key, pressed) {
233 this.keyPressed[key] = pressed
234 if (pressed) {
235 this.nextRepeat = key
236 } else {
237 this.nextRepeat = this.keyPressed.findIndex(Boolean)
238 }
239 this.beginPulsing()
240 }
241
242 /**
243 * Computes transmission duration for a given key.
244 *
245 * @param {number} key Key to calculate
246 * @returns {Duration} Duration of transmission
247 */
248 keyDuration(key) {
249 switch (key) {
250 case 0:
251 return DIT * this.ditDuration
252 case 1:
253 return DAH * this.ditDuration
254 }
255 return 0
256 }
257
258 /**
259 * Calculates the key to auto-transmit next.
260 *
261 * If there is nothing to send, returns -1.
262 *
263 * @returns {number} Key to transmit
264 */
265 nextTx() {
266 if (!this.keyPressed.some(Boolean)) {
267 return -1
268 }
269 return this.nextRepeat
270 }
271
272 pulse() {
273 let nextPulse = 0
274
275 if (this.TxClosed(0)) {
276 // Pause if we're currently transmitting
277 nextPulse = this.ditDuration
278 this.Tx(0, false)
279 } else {
280 let next = this.nextTx()
281 if (next >= 0) {
282 nextPulse = this.keyDuration(next)
283 this.Tx(0, true)
284 }
285 }
286
287 if (nextPulse) {
288 this.pulseTimer = setTimeout(() => this.pulse(), nextPulse)
289 } else {
290 this.pulseTimer = null
291 }
292 }
293}
294
295/**
296 * Ultimatic Keyer.
297 *
298 * If you know what an Iambic keyer does, this works similarly, but doesn't go
299 * back and forth when both keys are held.
300 */
301class UltimaticKeyer extends ElBugKeyer {
302 Reset() {
303 super.Reset()
304 this.queue = new QSet()
305 }
306
307 Key(key, pressed) {
308 if (pressed) {
309 this.queue.add(key)
310 }
311 super.Key(key, pressed)
312 }
313
314 nextTx() {
315 let key = this.queue.shift()
316 if (key != null) {
317 return key
318 }
319 return super.nextTx()
320 }
321}
322
323/**
324 * Single dot memory keyer.
325 *
326 * If you tap dit while a dah is sending, it queues up a dit to send, but
327 * reverts back to dah until the dah key is released or the dit key is pressed
328 * again. In other words, if the dah is held, it only pay attention to the edge
329 * on dit.
330 */
331class SingleDotKeyer extends ElBugKeyer {
332 Reset() {
333 super.Reset()
334 this.queue = new QSet()
335 }
336
337 Key(key, pressed) {
338 if (pressed && (key == 0)) {
339 this.queue.add(key)
340 }
341 super.Key(key, pressed)
342 }
343
344 nextTx() {
345 let key = this.queue.shift()
346 if (key != null) {
347 return key
348 }
349 for (let key of [1, 0]) {
350 if (this.keyPressed[key]) {
351 return key
352 }
353 }
354 return -1
355 }
356}
357
358/**
359 * "Plain" Iambic keyer.
360 */
361class IambicKeyer extends ElBugKeyer {
362 nextTx() {
363 let next = super.nextTx()
364 if (this.keyPressed.every(Boolean)) {
365 this.nextRepeat = 1 - this.nextRepeat
366 }
367 return next
368 }
369}
370
371class IambicAKeyer extends IambicKeyer {
372 Reset() {
373 super.Reset()
374 this.queue = new QSet()
375 }
376
377 Key(key, pressed) {
378 if (pressed && (key == 0)) {
379 this.queue.add(key)
380 }
381 super.Key(key, pressed)
382 }
383
384 nextTx() {
385 let next = super.nextTx()
386 let key = this.queue.shift()
387 if (key != null) {
388 return key
389 }
390 return next
391 }
392}
393
394/**
395 * "Iambic B"
396 *
397 * I have gotten a lot of helpful feedback on this one!
398 *
399 * Quoting DJ5IL:
400 *
401 * > if anytime during generation of an element the
402 * > opposite lever was pressed, generate one extra
403 * > alternate element.
404 */
405class IambicBKeyer extends IambicKeyer {
406 Reset() {
407 super.Reset()
408 this.queue = new QSet()
409 }
410
411 Key(key, pressed) {
412 if (pressed && (this.sending != key)) {
413 this.queue.add(key)
414 }
415 super.Key(key, pressed)
416 }
417
418 nextTx() {
419 for (let key of [0,1]) {
420 if (this.keyPressed[key]) {
421 this.queue.add(key)
422 }
423 }
424 let next = this.queue.shift()
425 this.sending = next
426 if (next == null) {
427 return -1
428 }
429 return next
430 }
431}
432
433class KeyaheadKeyer extends ElBugKeyer {
434 Reset() {
435 super.Reset()
436 this.queue = []
437 }
438
439 Key(key, pressed) {
440 if (pressed) {
441 this.queue.push(key)
442 }
443 super.Key(key, pressed)
444 }
445
446 nextTx() {
447 let next = this.queue.shift()
448 if (next != null) {
449 return next
450 }
451 return super.nextTx()
452 }
453}
454
455
456/**
457 * A dictionary of all available keyers
458 */
459const Keyers = {
460 straight: StraightKeyer,
461 cootie: CootieKeyer,
462 bug: BugKeyer,
463 elbug: ElBugKeyer,
464 singledot: SingleDotKeyer,
465 ultimatic: UltimaticKeyer,
466 iambic: IambicKeyer,
467 iambica: IambicAKeyer,
468 iambicb: IambicBKeyer,
469 keyahead: KeyaheadKeyer,
470
471 robo: RoboKeyer.Keyer,
472}
473
474const Numbers = {
475 straight: 1,
476 cootie: 1,
477 bug: 2,
478 elbug: 3,
479 singledot: 4,
480 ultimatic: 5,
481 iambic: 6,
482 iambica: 7,
483 iambicb: 8,
484 keyahead: 9,
485}
486
487export {
488 Keyers, Numbers,
489}