vail

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

vail / static / scripts
Neale Pickett  ·  2024-10-21

inputs.mjs

  1class Input {
  2	constructor(keyer) {
  3		this.keyer = keyer
  4	}
  5
  6	SetDitDuration(delay) {
  7		// Nothing
  8	}
  9
 10	SetKeyerMode(mode) {
 11		// Nothing
 12	}
 13}
 14
 15export class HTML extends Input{
 16	constructor(keyer) {
 17		super(keyer)
 18
 19		// Listen to HTML buttons
 20		for (let e of document.querySelectorAll("button.key")) {
 21			// Chrome is going to suggest you use passive events here.
 22			// I tried that and it screws up Safari mobile,
 23			// making it so that hitting the button selects text on the page.
 24			e.addEventListener("contextmenu", e => { e.preventDefault(); return false }, {passive: false})
 25			e.addEventListener("touchstart", e => this.keyButton(e), {passive: false})
 26			e.addEventListener("touchend", e => this.keyButton(e), {passive: false})
 27			e.addEventListener("mousedown", e => this.keyButton(e), {passive: false})
 28			e.addEventListener("mouseup", e => this.keyButton(e), {passive: false})
 29			e.contentEditable = false
 30		}
 31	}
 32
 33	keyButton(event) {
 34		let down = event.type.endsWith("down") || event.type.endsWith("start")
 35		let key = Number(event.target.dataset.key)
 36		
 37		// Button 2 does the other key (assuming 2 keys)
 38		if (event.button == 2) {
 39			key = 1 - key
 40		}
 41		this.keyer.Key(key, down)
 42
 43		if (event.cancelable) {
 44			event.preventDefault()
 45		}
 46	}
 47}
 48
 49export class Keyboard extends Input{
 50	constructor(keyer) {
 51		super(keyer)
 52
 53		// Listen for keystrokes
 54		document.addEventListener("keydown", e => this.keyboard(e))
 55		document.addEventListener("keyup", e => this.keyboard(e))
 56		window.addEventListener("blur", e => this.loseFocus(e))
 57	}
 58
 59	keyboard(event) {
 60		if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) {
 61			// Ignore everything if the user is entering text somewhere
 62			return
 63		}
 64
 65		let down = event.type.endsWith("down")
 66
 67		if (
 68			(event.code == "KeyX")
 69			|| (event.code == "Period")
 70			|| (event.code == "BracketLeft")
 71			|| (event.code == "ControlLeft")
 72			|| (event.key == "[")
 73		) {
 74			// Dit
 75			if (this.ditDown != down) {
 76				this.keyer.Key(0, down)
 77				this.ditDown = down
 78			}
 79		}
 80		if (
 81			(event.code == "KeyZ")
 82			|| (event.code == "Slash")
 83			|| (event.code == "BracketRight")
 84			|| (event.code == "ControlRight")
 85			|| (event.key == "]")
 86		) {
 87			if (this.dahDown != down) {
 88				this.keyer.Key(1, down)
 89				this.dahDown = down
 90			}
 91		}
 92		if (
 93			(event.code == "KeyC")
 94			|| (event.code == "Comma")
 95			|| (event.key == "Enter")
 96			|| (event.key == "NumpadEnter")
 97		) {
 98			if (this.straightDown != down) {
 99				this.keyer.Straight(down)
100				this.straightDown = down
101			}
102		}
103	}
104
105	loseFocus(event) {
106		if (this.ditDown) {
107			this.keyer.Key(0, false)
108			this.ditDown = false
109		}
110		if (this.dahDown) {
111			this.keyer.Key(1, false)
112			this.dahDown = false
113		}
114		if (this.straightDown) {
115			this.keyer.key(2, false)
116			this.straightDown = false
117		}
118	}
119}
120
121export class MIDI extends Input{
122	constructor(keyer) {
123		super(keyer)
124		this.ditDuration = 100
125		this.keyerMode = 0
126		
127		this.midiAccess = {outputs: []} // stub while we wait for async stuff
128		if (navigator.requestMIDIAccess) {
129			this.midiInit()
130		}
131	}
132	
133	async midiInit(access) {
134		this.inputs = []
135		this.midiAccess = await navigator.requestMIDIAccess()
136		this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e))
137		this.midiStateChange()
138	}
139	
140	// If you're looking for the thing that sets the tx tone,
141	// that's in outputs.mjs:SetMIDINote
142
143	sendState() {
144		for (let output of this.midiAccess.outputs.values()) {
145			// Turn off keyboard mode
146			output.send([0xB0, 0x00, 0x00])
147
148			// MIDI only supports 7-bit values, so we have to divide dit duration by two
149			output.send([0xB0, 0x01, this.ditDuration/2])
150
151			// Send keyer mode
152			output.send([0xC0, this.keyerMode])
153		}
154
155	}
156
157	SetDitDuration(duration) {
158		this.ditDuration = duration
159		this.sendState()
160	}
161
162	SetKeyerMode(mode) {
163		this.keyerMode = mode
164		this.sendState()
165	}
166
167	midiStateChange(event) {
168		// Go through this.midiAccess.inputs and only listen on new things
169		for (let input of this.midiAccess.inputs.values()) {
170			if (!this.inputs.includes(input)) {
171				input.addEventListener("midimessage", e => this.midiMessage(e))
172				this.inputs.push(input)
173			}
174		}
175
176		// Tell the Vail adapter to disable keyboard events: we can do MIDI!
177		this.sendState()
178	}
179
180	midiMessage(event) {
181		let data = Array.from(event.data)
182
183		let begin
184		let cmd = data[0] >> 4
185		let chan = data[0] & 0xf
186		switch (cmd) {
187			case 9:
188				begin = true
189				break
190			case 8:
191				begin = false
192				break
193			default:
194				return
195		}
196
197		switch (data[1]) {
198			case 0: // Vail Adapter
199				this.keyer.Straight(begin)
200				break
201			case 1: // Vail Adapter
202			case 20: // N6ARA TinyMIDI
203				this.keyer.Key(0, begin)
204				break
205			case 2: // Vail Adapter
206			case 21: // N6ARA TinyMIDI
207				this.keyer.Key(1, begin)
208				break
209			default:
210				return
211		}
212
213
214	}	
215}
216
217export class Gamepad extends Input{
218	constructor(keyer) {
219		super(keyer)
220
221		// Set up for gamepad input
222		window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
223	}
224
225	/**
226	 * Gamepads must be polled, usually at 60fps.
227	 * This could be really expensive,
228	 * especially on devices with a power budget, like phones.
229	 * To be considerate, we only start polling if a gamepad appears.
230	 * 
231	 * @param event Gamepad Connected event
232	 */
233		gamepadConnected(event) {
234		if (!this.gamepadButtons) {
235			this.gamepadButtons = {}
236			this.gamepadPoll(event.timeStamp)
237		}
238	}
239
240	gamepadPoll(timestamp) {
241		let currentButtons = {}
242		for (let gp of navigator.getGamepads()) {
243			if (gp == null) {
244				continue
245			}
246			for (let i in gp.buttons) {
247				let pressed = gp.buttons[i].pressed
248				if (i < 2) {
249					currentButtons.key |= pressed
250				} else if (i % 2 == 0) {
251					currentButtons.dit |= pressed
252				} else {
253					currentButtons.dah |= pressed
254				}
255			}
256		}
257
258		if (currentButtons.key != this.gamepadButtons.key) {
259			this.keyer.Straight(currentButtons.key)
260		}
261		if (currentButtons.dit != this.gamepadButtons.dit) {
262			this.keyer.Key(0, currentButtons.dit)
263		}
264		if (currentButtons.dah != this.gamepadButtons.dah) {
265			this.keyer.Key(1, currentButtons.dah)
266		}
267		this.gamepadButtons = currentButtons
268
269		requestAnimationFrame(e => this.gamepadPoll(e))
270	}
271}
272
273class Collection {
274	constructor(keyer) {
275		this.html =new HTML(keyer)
276		this.keyboard =new Keyboard(keyer)
277		this.midi =new MIDI(keyer)
278		this.gamepad =new Gamepad(keyer)
279		this.collection = [this.html, this.keyboard, this.midi, this.gamepad]
280	}
281
282	/**
283	 * Set duration of all inputs
284	 * 
285	 * @param duration Duration to set
286	 */
287	SetDitDuration(duration) {
288		for (let e of this.collection) {
289			e.SetDitDuration(duration)
290		}
291	}
292
293	/**
294	 * Set keyer mode of all inputs
295	 * 
296	 * @param mode Keyer mode to set
297	 */
298	SetKeyerMode(mode) {
299		for (let e of this.collection) {
300			e.SetKeyerMode(mode)
301		}
302	}
303}
304
305export {Collection}