vail

Internet morse code repeater
git clone https://git.woozle.org/neale/vail.git

vail / static / scripts
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}