Attempt to get local keyer modes going
This commit is contained in:
parent
3521861f18
commit
012ee5ae31
|
@ -0,0 +1,10 @@
|
||||||
|
CompileFlags:
|
||||||
|
Add:
|
||||||
|
- "--include-directory=/opt/arduino/hardware/arduino/avr/cores/arduino"
|
||||||
|
- "--include-directory=/opt/arduino/hardware/arduino/avr/variants/standard"
|
||||||
|
- "--include-directory=/opt/arduino/hardware/arduino/avr/libraries/HID/src"
|
||||||
|
- "--include-directory=/opt/arduino/hardware/tools/avr/avr/include"
|
||||||
|
- "--include-directory=/opt/arduino/libraries/Keyboard/src"
|
||||||
|
- "--include-directory=/opt/arduino/libraries/HID/src"
|
||||||
|
- "--include-directory=/home/dartcatcher/Arduino/libraries/MIDIUSB/src"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"clangd.arguments": [
|
||||||
|
"--enable-config"
|
||||||
|
]
|
||||||
|
}
|
21
Makefile
21
Makefile
|
@ -1,12 +1,16 @@
|
||||||
FQBN = Seeeduino:samd:seeed_XIAO_m0
|
FQBN_qtpy = adafruit:samd:adafruit_qtpy_m0
|
||||||
|
FQBN_xiao = Seeeduino:samd:seeed_XIAO_m0
|
||||||
UF2_MOUNT = /mnt/chromeos/removable/Arduino
|
UF2_MOUNT = /mnt/chromeos/removable/Arduino
|
||||||
ARDUINO_DIR = /app/Arduino
|
ARDUINO_DIR = /app/Arduino
|
||||||
BUILDER = flatpak run --command ${ARDUINO_DIR}/arduino-builder cc.arduino.arduinoide
|
BUILDER = flatpak run --command ${ARDUINO_DIR}/arduino-builder cc.arduino.arduinoide
|
||||||
|
|
||||||
default: build/vail-adapter.xiao.uf2
|
default: build/vail-adapter.qtpy.uf2 build/vail-adapter.xiao.uf2
|
||||||
install: build/vail-adapter.xiao.uf2
|
install: build/vail-adapter.xiao.uf2
|
||||||
./install.sh $< $(UF2_MOUNT)
|
./install.sh $< $(UF2_MOUNT)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/*
|
||||||
|
|
||||||
# uf2conv.py is covered by an MIT license.
|
# uf2conv.py is covered by an MIT license.
|
||||||
build/uf2conv.py: build/uf2families.json
|
build/uf2conv.py: build/uf2families.json
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
|
@ -16,13 +20,19 @@ build/uf2families.json:
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
curl -L https://raw.githubusercontent.com/microsoft/uf2/master/utils/$(@F) > $@
|
curl -L https://raw.githubusercontent.com/microsoft/uf2/master/utils/$(@F) > $@
|
||||||
|
|
||||||
%.xiao.uf2: %.ino.bin build/uf2conv.py
|
%.xiao.uf2: %.xiao.bin build/uf2conv.py
|
||||||
build/uf2conv.py -b 0x2000 -c -o $@ $<
|
build/uf2conv.py -b 0x2000 -c -o $@ $<
|
||||||
|
|
||||||
build/%.bin: % *.cpp *.h
|
%.qtpy.uf2: %.qtpy.bin build/uf2conv.py
|
||||||
|
build/uf2conv.py -b 0x2000 -c -o $@ $<
|
||||||
|
|
||||||
|
build/%.qtpy.bin: FQBN = adafruit:samd:adafruit_qtpy_m0
|
||||||
|
build/%.xiao.bin: FQBN = Seeeduino:samd:seeed_XIAO_m0
|
||||||
|
build/vail-adapter.%.bin: vail-adapter.ino *.cpp *.h
|
||||||
|
mkdir -p build/$*
|
||||||
arduino-builder \
|
arduino-builder \
|
||||||
-build-cache ~/.cache/arduino \
|
-build-cache ~/.cache/arduino \
|
||||||
-build-path build \
|
-build-path build/$* \
|
||||||
-core-api-version 10813 \
|
-core-api-version 10813 \
|
||||||
-fqbn $(FQBN) \
|
-fqbn $(FQBN) \
|
||||||
-hardware ~/.arduino15/packages \
|
-hardware ~/.arduino15/packages \
|
||||||
|
@ -34,6 +44,7 @@ build/%.bin: % *.cpp *.h
|
||||||
-libraries ~/Arduino/libraries \
|
-libraries ~/Arduino/libraries \
|
||||||
-compile \
|
-compile \
|
||||||
$<
|
$<
|
||||||
|
mv build/$*/vail-adapter.ino.bin $@
|
||||||
|
|
||||||
upload: vail-adapter.ino
|
upload: vail-adapter.ino
|
||||||
arduino --upload --board $(FQBN) $<
|
arduino --upload --board $(FQBN) $<
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Keyboard.h>
|
||||||
|
#include <MIDIUSB.h>
|
||||||
|
#include <cstddef>
|
||||||
|
#include "keyers.h"
|
||||||
|
#include "adapter.h"
|
||||||
|
#include "polybuzzer.h"
|
||||||
|
|
||||||
|
#define MILLISECOND 1
|
||||||
|
#define SECOND (1000 * MILLISECOND)
|
||||||
|
|
||||||
|
VailAdapter::VailAdapter(unsigned int PiezoPin) {
|
||||||
|
this->buzzer = new PolyBuzzer(PiezoPin);
|
||||||
|
this->txToneFrequency = 440;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a MIDI Key Event
|
||||||
|
void VailAdapter::midiKey(uint8_t key, bool down) {
|
||||||
|
midiEventPacket_t event = {down?9:8, down?0x90:0x80, key, 0x7f};
|
||||||
|
MidiUSB.sendMIDI(event);
|
||||||
|
MidiUSB.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a keyboard key event
|
||||||
|
void VailAdapter::keyboardKey(uint8_t key, bool down) {
|
||||||
|
if (down) {
|
||||||
|
Keyboard.press(key);
|
||||||
|
} else {
|
||||||
|
Keyboard.release(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transmitting
|
||||||
|
void VailAdapter::BeginTx() {
|
||||||
|
this->buzzer->Tone(0, this->txToneFrequency);
|
||||||
|
if (this->keyboardMode) {
|
||||||
|
this->keyboardKey(KEY_LEFT_CTRL, true);
|
||||||
|
} else {
|
||||||
|
this->midiKey(0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop transmitting
|
||||||
|
void VailAdapter::EndTx() {
|
||||||
|
this->buzzer->NoTone(0);
|
||||||
|
if (this->keyboardMode) {
|
||||||
|
this->keyboardKey(KEY_LEFT_CTRL, false);
|
||||||
|
} else {
|
||||||
|
this->midiKey(0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a paddle being pressed.
|
||||||
|
//
|
||||||
|
// The caller needs to debounce keys and deal with keys wired in parallel.
|
||||||
|
void VailAdapter::HandlePaddle(Paddle paddle, bool pressed) {
|
||||||
|
switch (paddle) {
|
||||||
|
case PADDLE_STRAIGHT:
|
||||||
|
if (pressed) {
|
||||||
|
this->BeginTx();
|
||||||
|
} else {
|
||||||
|
this->EndTx();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case PADDLE_DIT:
|
||||||
|
if (this->keyer) {
|
||||||
|
this->keyer->Key(paddle, pressed);
|
||||||
|
} else if (this->keyboardMode) {
|
||||||
|
this->keyboardKey(KEY_LEFT_CTRL, pressed);
|
||||||
|
} else {
|
||||||
|
this->midiKey(1, pressed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PADDLE_DAH:
|
||||||
|
if (this->keyer) {
|
||||||
|
this->keyer->Key(paddle, pressed);
|
||||||
|
} else if (this->keyboardMode) {
|
||||||
|
this->keyboardKey(KEY_RIGHT_CTRL, pressed);
|
||||||
|
} else {
|
||||||
|
this->midiKey(2, pressed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a MIDI event.
|
||||||
|
//
|
||||||
|
// We act as a MIDI
|
||||||
|
void VailAdapter::HandleMIDI(midiEventPacket_t event) {
|
||||||
|
uint16_t msg = (event.byte1 << 8) | (event.byte2 << 0);
|
||||||
|
switch (event.byte1) {
|
||||||
|
case 0xB0: // Controller Change
|
||||||
|
switch (event.byte2) {
|
||||||
|
case 0: // turn keyboard mode on/off
|
||||||
|
this->keyboardMode = (event.byte3 > 0x3f);
|
||||||
|
MidiUSB.sendMIDI(event); // Send it back to acknowledge
|
||||||
|
break;
|
||||||
|
case 1: // set dit duration (0-254) *2ms
|
||||||
|
this->ditDuration = event.byte3 * 2 * MILLISECOND;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0xC0: // Program Change
|
||||||
|
this->keyer = GetKeyerByNumber(event.byte2, this);
|
||||||
|
break;
|
||||||
|
case 0x80: // Note off
|
||||||
|
this->buzzer->NoTone(1);
|
||||||
|
break;
|
||||||
|
case 0x90: // Note on
|
||||||
|
this->buzzer->Note(1, event.byte2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <MIDIUSB.h>
|
||||||
|
#include "keyers.h"
|
||||||
|
#include "polybuzzer.h"
|
||||||
|
|
||||||
|
class VailAdapter: public Transmitter {
|
||||||
|
private:
|
||||||
|
unsigned int txToneFrequency;
|
||||||
|
unsigned int ditDuration = 100;
|
||||||
|
bool keyboardMode = false;
|
||||||
|
Keyer *keyer = NULL;
|
||||||
|
PolyBuzzer *buzzer = NULL;
|
||||||
|
|
||||||
|
void midiKey(uint8_t key, bool down);
|
||||||
|
void keyboardKey(uint8_t key, bool down);
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
VailAdapter(unsigned int PiezoPin);
|
||||||
|
void HandlePaddle(Paddle key, bool pressed);
|
||||||
|
void HandleMIDI(midiEventPacket_t event);
|
||||||
|
void BeginTx();
|
||||||
|
void EndTx();
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Vail MIDI Protocol
|
||||||
|
|
||||||
|
When it boots,
|
||||||
|
the Vail adapter sends left and right Control keyboard key up and down events.
|
||||||
|
It also shows up as a MIDI device.
|
||||||
|
|
||||||
|
The Vail web site sends MIDI control commands to enable MIDI keyer mode,
|
||||||
|
tells the keyer what sideband pitch to generate,
|
||||||
|
and can set the keyer mode.
|
||||||
|
|
||||||
|
|
||||||
|
## Controller 0 - MIDI Mode
|
||||||
|
|
||||||
|
`b0 00 ff` will enable MIDI mode and disable Keyboard mode
|
||||||
|
|
||||||
|
`b0 00 00` will enable Keyboard mode and disable MIDI mode
|
||||||
|
|
||||||
|
|
||||||
|
## Controller 1 - dit length
|
||||||
|
|
||||||
|
`b0 00 xx` will set the dit duration to `xx` times 2 milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
## Controller 2 - sidetone note
|
||||||
|
|
||||||
|
`b0 00 xx` will play note `xx` as the sidetone note
|
||||||
|
|
||||||
|
|
||||||
|
## Program Change
|
||||||
|
|
||||||
|
`c0 xx` will change the keyer mode to `xx`.
|
||||||
|
|
||||||
|
|
||||||
|
### Keyer Modes
|
||||||
|
|
||||||
|
* 0: passthrough (sends C# and D for dit and dah)
|
||||||
|
* 1: cootie / straight key
|
||||||
|
* 2: bug
|
||||||
|
* 3: electric bug
|
||||||
|
* 4: single dot
|
||||||
|
* 5: ultimatic
|
||||||
|
* 6: plain iambic
|
||||||
|
* 7: iambic a
|
||||||
|
* 8: iambic b
|
||||||
|
* 9: keyahead
|
||||||
|
|
||||||
|
Any other mode will set to passthrough.
|
||||||
|
|
||||||
|
|
||||||
|
## Notes (key down / key up)
|
||||||
|
|
||||||
|
`90 00 xx` will begin playing note `xx`
|
||||||
|
`80 00 xx` will end playing note `xx`
|
||||||
|
|
||||||
|
These work just like a regular MIDI synthesizer.
|
|
@ -1,24 +0,0 @@
|
||||||
# MIDI Negotiation
|
|
||||||
|
|
||||||
Morse code keyers are very simple devices,
|
|
||||||
they just connect two wires together.
|
|
||||||
You could use a button if you wanted to,
|
|
||||||
or even touch wires together.
|
|
||||||
|
|
||||||
The only real complication here is that some browsers
|
|
||||||
need to get keyboard events instead of musical instrument events.
|
|
||||||
|
|
||||||
The Vail adapter boots into a mode that sends both keyboard events
|
|
||||||
and MIDI messages.
|
|
||||||
If it receives a MIDI key release event
|
|
||||||
on channel 0
|
|
||||||
for note C0,
|
|
||||||
it will disable keyboard events.
|
|
||||||
|
|
||||||
Vail sends this "disable keyboard" MIDI event, so as soon as you
|
|
||||||
load up Vail, the keyboard events are disabled, and your adapter
|
|
||||||
will no longer interfere with your typing.
|
|
||||||
If your browser doesn't support MIDI,
|
|
||||||
the disable command can't be sent,
|
|
||||||
and it keeps on sending keystrokes.
|
|
||||||
|
|
|
@ -0,0 +1,404 @@
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "keyers.h"
|
||||||
|
|
||||||
|
#define len(t) (sizeof(t)/sizeof(*t))
|
||||||
|
|
||||||
|
// Queue Set: A Set you can shift and pop.
|
||||||
|
class QSet {
|
||||||
|
int arr[MAX_KEYER_QUEUE];
|
||||||
|
unsigned int arrlen = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
int shift() {
|
||||||
|
if (arrlen == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int ret = arr[0];
|
||||||
|
arrlen--;
|
||||||
|
for (int i = 0; i < arrlen; i++) {
|
||||||
|
arr[i] = arr[i+1];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pop() {
|
||||||
|
if (arrlen == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int ret = arr[arrlen];
|
||||||
|
arrlen--;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(int val) {
|
||||||
|
if (arrlen == MAX_KEYER_QUEUE-1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < arrlen; i++) {
|
||||||
|
if (arr[arrlen] == i) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr[arrlen] = val;
|
||||||
|
arrlen++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class StraightKeyer: public Keyer {
|
||||||
|
public:
|
||||||
|
Transmitter *output;
|
||||||
|
unsigned int ditDuration;
|
||||||
|
bool txRelays[2];
|
||||||
|
|
||||||
|
StraightKeyer(Transmitter *output) {
|
||||||
|
this->output = output;
|
||||||
|
this->ditDuration = 100;
|
||||||
|
this->Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
this->output->EndTx();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDitDuration(int duration) {
|
||||||
|
this->ditDuration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release() {}
|
||||||
|
|
||||||
|
bool TxClosed() {
|
||||||
|
for (int i = 0; i < len(this->txRelays); i++) {
|
||||||
|
if (this->TxClosed(i)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TxClosed(int relay) {
|
||||||
|
return this->txRelays[relay];
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tx(int relay, bool closed) {
|
||||||
|
bool wasClosed = this->TxClosed();
|
||||||
|
this->txRelays[relay] = closed;
|
||||||
|
bool nowClosed = this->TxClosed();
|
||||||
|
|
||||||
|
if (wasClosed != nowClosed) {
|
||||||
|
if (nowClosed) {
|
||||||
|
this->output->BeginTx();
|
||||||
|
} else {
|
||||||
|
this->output->EndTx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
this->Tx(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tick(unsigned int millis) {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class BugKeyer: public StraightKeyer {
|
||||||
|
public:
|
||||||
|
unsigned int pulseTime = 0;
|
||||||
|
bool keyPressed[2];
|
||||||
|
|
||||||
|
using StraightKeyer::StraightKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
StraightKeyer::Reset();
|
||||||
|
this->pulseTime = 0;
|
||||||
|
this->keyPressed[0] = false;
|
||||||
|
this->keyPressed[1] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
this->keyPressed[key] = pressed;
|
||||||
|
if (key == 0) {
|
||||||
|
this->beginPulsing();
|
||||||
|
} else {
|
||||||
|
StraightKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void beginPulsing() {
|
||||||
|
this->pulseTime = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pulse(unsigned int millis) {
|
||||||
|
if (this->TxClosed(0)) {
|
||||||
|
this->Tx(0, false);
|
||||||
|
} else if (this->keyPressed[0]) {
|
||||||
|
this->Tx(0, true);
|
||||||
|
} else {
|
||||||
|
this->pulseTime = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->pulseTime = millis + this->ditDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tick(unsigned int millis) {
|
||||||
|
if (this->pulseTime && (millis >= this->pulseTime)) {
|
||||||
|
this->pulse(millis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ElBugKeyer: public BugKeyer {
|
||||||
|
public:
|
||||||
|
unsigned int nextRepeat;
|
||||||
|
|
||||||
|
using BugKeyer::BugKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
BugKeyer::Reset();
|
||||||
|
this->nextRepeat = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return which key is pressed. If none, return -1.
|
||||||
|
int whichKeyPressed() {
|
||||||
|
for (int i = 0; i < len(this->keyPressed); i++) {
|
||||||
|
if (this->keyPressed[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
this->keyPressed[key] = pressed;
|
||||||
|
if (pressed) {
|
||||||
|
this->nextRepeat = key;
|
||||||
|
} else {
|
||||||
|
this->nextRepeat = this->whichKeyPressed();
|
||||||
|
}
|
||||||
|
this->beginPulsing();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int keyDuration(int key) {
|
||||||
|
switch (key) {
|
||||||
|
case 0:
|
||||||
|
return this->ditDuration;
|
||||||
|
case 1:
|
||||||
|
return 3 * this->ditDuration;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
if (this->whichKeyPressed() == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this->nextRepeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pulse(unsigned int millis) {
|
||||||
|
int nextPulse = 0;
|
||||||
|
if (this->TxClosed(0)) {
|
||||||
|
// Pause if we're currently transmitting
|
||||||
|
nextPulse = this->ditDuration;
|
||||||
|
this->Tx(0, false);
|
||||||
|
} else {
|
||||||
|
int next = this->nextTx();
|
||||||
|
if (next >= 0) {
|
||||||
|
nextPulse = this->keyDuration(next);
|
||||||
|
this->Tx(0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPulse) {
|
||||||
|
this->pulseTime = millis + nextPulse;
|
||||||
|
} else {
|
||||||
|
this->pulseTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class UltimaticKeyer: public ElBugKeyer {
|
||||||
|
public:
|
||||||
|
QSet *queue;
|
||||||
|
|
||||||
|
using ElBugKeyer::ElBugKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
ElBugKeyer::Reset();
|
||||||
|
this->queue = new QSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
if (pressed) {
|
||||||
|
this->queue->add(key);
|
||||||
|
}
|
||||||
|
ElBugKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
int key = this->queue->shift();
|
||||||
|
if (key != -1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return ElBugKeyer::nextTx();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class SingleDotKeyer: public ElBugKeyer {
|
||||||
|
public:
|
||||||
|
QSet *queue;
|
||||||
|
|
||||||
|
using ElBugKeyer::ElBugKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
ElBugKeyer::Reset();
|
||||||
|
this->queue = new QSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
if (pressed && (key == 0)) {
|
||||||
|
this->queue->add(key);
|
||||||
|
}
|
||||||
|
ElBugKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
int key = this->queue->shift();
|
||||||
|
if (key != -1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
if (this->keyPressed[1]) return 1;
|
||||||
|
if (this->keyPressed[0]) return 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IambicKeyer: public ElBugKeyer {
|
||||||
|
public:
|
||||||
|
|
||||||
|
using ElBugKeyer::ElBugKeyer;
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
int next = ElBugKeyer::nextTx();
|
||||||
|
if (this->whichKeyPressed() != -1) {
|
||||||
|
this->nextRepeat = 1 - this->nextRepeat;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IambicAKeyer: public IambicKeyer {
|
||||||
|
public:
|
||||||
|
QSet *queue;
|
||||||
|
|
||||||
|
using IambicKeyer::IambicKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
IambicKeyer::Reset();
|
||||||
|
this->queue = new QSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
if (pressed && (key == 0)) {
|
||||||
|
this->queue->add(key);
|
||||||
|
}
|
||||||
|
IambicKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
int next = IambicKeyer::nextTx();
|
||||||
|
int key = this->queue->shift();
|
||||||
|
if (key != -1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IambicBKeyer: public IambicKeyer {
|
||||||
|
public:
|
||||||
|
QSet *queue;
|
||||||
|
|
||||||
|
using IambicKeyer::IambicKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
IambicKeyer::Reset();
|
||||||
|
this->queue = new QSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
if (pressed) {
|
||||||
|
this->queue->add(key);
|
||||||
|
}
|
||||||
|
IambicKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
for (int key = 0; key < 2; key++) {
|
||||||
|
if (this->keyPressed[key]) {
|
||||||
|
this->queue->add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->queue->shift();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class KeyaheadKeyer: public ElBugKeyer {
|
||||||
|
public:
|
||||||
|
int queue[MAX_KEYER_QUEUE];
|
||||||
|
unsigned int qlen;
|
||||||
|
|
||||||
|
using ElBugKeyer::ElBugKeyer;
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
ElBugKeyer::Reset();
|
||||||
|
this->qlen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Key(Paddle key, bool pressed) {
|
||||||
|
if (pressed) {
|
||||||
|
if (this->qlen < MAX_KEYER_QUEUE) {
|
||||||
|
this->queue[this->qlen++] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElBugKeyer::Key(key, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextTx() {
|
||||||
|
if (this->qlen > 0) {
|
||||||
|
int next = this->queue[0];
|
||||||
|
this->qlen--;
|
||||||
|
for (int i = 0; i < this->qlen; i++) {
|
||||||
|
this->queue[i] = this->queue[i+1];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return ElBugKeyer::nextTx();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Keyer *GetKeyerByNumber(int n, Transmitter *output) {
|
||||||
|
switch (n) {
|
||||||
|
case 1:
|
||||||
|
return new StraightKeyer(output);
|
||||||
|
case 2:
|
||||||
|
return new BugKeyer(output);
|
||||||
|
case 3:
|
||||||
|
return new ElBugKeyer(output);
|
||||||
|
case 4:
|
||||||
|
return new SingleDotKeyer(output);
|
||||||
|
case 5:
|
||||||
|
return new UltimaticKeyer(output);
|
||||||
|
case 6:
|
||||||
|
return new IambicKeyer(output);
|
||||||
|
case 7:
|
||||||
|
return new IambicAKeyer(output);
|
||||||
|
case 8:
|
||||||
|
return new IambicBKeyer(output);
|
||||||
|
case 9:
|
||||||
|
return new KeyaheadKeyer(output);
|
||||||
|
default:
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define MAX_KEYER_QUEUE 5
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PADDLE_DIT = 0,
|
||||||
|
PADDLE_DAH,
|
||||||
|
PADDLE_STRAIGHT,
|
||||||
|
} Paddle;
|
||||||
|
|
||||||
|
class Transmitter {
|
||||||
|
public:
|
||||||
|
virtual void BeginTx();
|
||||||
|
virtual void EndTx();
|
||||||
|
};
|
||||||
|
|
||||||
|
class Keyer {
|
||||||
|
public:
|
||||||
|
virtual void Reset();
|
||||||
|
virtual void SetDitDuration(int d);
|
||||||
|
virtual void Release();
|
||||||
|
virtual bool TxClosed();
|
||||||
|
virtual bool TxClosed(int relay);
|
||||||
|
virtual void Tx(int relay, bool closed);
|
||||||
|
virtual void Key(Paddle key, bool pressed);
|
||||||
|
virtual void Tick(unsigned int millis);
|
||||||
|
};
|
||||||
|
|
||||||
|
Keyer *GetKeyerByNumber(int n, Transmitter *output);
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "polybuzzer.h"
|
||||||
|
|
||||||
|
PolyBuzzer::PolyBuzzer(uint8_t pin) {
|
||||||
|
for (int i = 0; i < POLYBUZZER_MAX_TONES; i++) {
|
||||||
|
this->tones[i] = 0;
|
||||||
|
}
|
||||||
|
this->playing = 0;
|
||||||
|
this->pin = pin;
|
||||||
|
pinMode(pin, OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PolyBuzzer::update() {
|
||||||
|
for (int i = 0; i < POLYBUZZER_MAX_TONES; i++) {
|
||||||
|
if (tones[i]) {
|
||||||
|
if (playing != tones[i]) {
|
||||||
|
playing = tones[i];
|
||||||
|
tone(this->pin, tones[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->playing = 0;
|
||||||
|
noTone(this->pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PolyBuzzer::Tone(int slot, unsigned int frequency) {
|
||||||
|
tones[slot] = frequency;
|
||||||
|
this->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PolyBuzzer::Note(int slot, int note) {
|
||||||
|
unsigned int frequency = 8.18; // MIDI note 0
|
||||||
|
for (int i = 0; i < note; i++) {
|
||||||
|
frequency *= 1.0594630943592953; // equal temperament half step
|
||||||
|
}
|
||||||
|
this->Tone(slot, frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PolyBuzzer::NoTone(int slot) {
|
||||||
|
tones[slot] = 0;
|
||||||
|
this->update();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#define POLYBUZZER_MAX_TONES 2
|
||||||
|
|
||||||
|
// PolyBuzzer provides a proritized monophonic buzzer.
|
||||||
|
//
|
||||||
|
// A given tone will only be played when all higher priority tones have stopped.
|
||||||
|
class PolyBuzzer {
|
||||||
|
public:
|
||||||
|
unsigned int tones[POLYBUZZER_MAX_TONES];
|
||||||
|
unsigned int playing;
|
||||||
|
uint8_t pin;
|
||||||
|
|
||||||
|
PolyBuzzer(uint8_t pin);
|
||||||
|
void update();
|
||||||
|
void Tone(int slot, unsigned int frequency);
|
||||||
|
void Note(int slot, int note);
|
||||||
|
void NoTone(int slot);
|
||||||
|
};
|
|
@ -8,6 +8,7 @@
|
||||||
#include <Adafruit_FreeTouch.h>
|
#include <Adafruit_FreeTouch.h>
|
||||||
#include "bounce2.h"
|
#include "bounce2.h"
|
||||||
#include "touchbounce.h"
|
#include "touchbounce.h"
|
||||||
|
#include "adapter.h"
|
||||||
|
|
||||||
#define DIT_PIN 2
|
#define DIT_PIN 2
|
||||||
#define DAH_PIN 1
|
#define DAH_PIN 1
|
||||||
|
@ -19,8 +20,8 @@
|
||||||
#define LED_ON false // Xiao inverts this logic for some reason
|
#define LED_ON false // Xiao inverts this logic for some reason
|
||||||
#define LED_OFF (!LED_ON)
|
#define LED_OFF (!LED_ON)
|
||||||
|
|
||||||
#define DIT_KEY KEY_LEFT_CTRL
|
#define DIT_KEYBOARD_KEY KEY_LEFT_CTRL
|
||||||
#define DAH_KEY KEY_RIGHT_CTRL
|
#define DAH_KEYBOARD_KEY KEY_RIGHT_CTRL
|
||||||
#define TONE 550
|
#define TONE 550
|
||||||
|
|
||||||
#define MILLISECOND 1
|
#define MILLISECOND 1
|
||||||
|
@ -35,10 +36,10 @@ Bounce key = Bounce();
|
||||||
TouchBounce qt_dit = TouchBounce();
|
TouchBounce qt_dit = TouchBounce();
|
||||||
TouchBounce qt_dah = TouchBounce();
|
TouchBounce qt_dah = TouchBounce();
|
||||||
TouchBounce qt_key = TouchBounce();
|
TouchBounce qt_key = TouchBounce();
|
||||||
|
VailAdapter adapter = VailAdapter(PIEZO);
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
pinMode(LED_BUILTIN, OUTPUT);
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
pinMode(PIEZO, OUTPUT);
|
|
||||||
dit.attach(DIT_PIN, INPUT_PULLUP);
|
dit.attach(DIT_PIN, INPUT_PULLUP);
|
||||||
dah.attach(DAH_PIN, INPUT_PULLUP);
|
dah.attach(DAH_PIN, INPUT_PULLUP);
|
||||||
key.attach(KEY_PIN, INPUT_PULLUP);
|
key.attach(KEY_PIN, INPUT_PULLUP);
|
||||||
|
@ -85,76 +86,33 @@ void setLED() {
|
||||||
digitalWrite(LED_BUILTIN, on?LED_ON:LED_OFF);
|
digitalWrite(LED_BUILTIN, on?LED_ON:LED_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
void midiKey(bool down, uint8_t key) {
|
|
||||||
midiEventPacket_t event = {down?9:8, down?0x90:0x80, key, 0x7f};
|
|
||||||
MidiUSB.sendMIDI(event);
|
|
||||||
MidiUSB.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
void midiProbe() {
|
|
||||||
midiEventPacket_t event = MidiUSB.read();
|
|
||||||
|
|
||||||
uint16_t msg = (event.byte1 << 8) | (event.byte2 << 0);
|
|
||||||
switch (msg) {
|
|
||||||
case 0x8B00: // Controller 0: turn keyboard mode on/off
|
|
||||||
keyboard = (event.byte3 > 0x3f);
|
|
||||||
break;
|
|
||||||
case 0x8B01: // Controller 1: set iambic speed (0-254)
|
|
||||||
// I am probably never going to use this,
|
|
||||||
// because as soon as I implement it,
|
|
||||||
// people are going to want a way to select mode A or B,
|
|
||||||
// or typeahead,
|
|
||||||
// or some other thing that I don't want to maintain
|
|
||||||
// simultaneously in both C and JavaScript
|
|
||||||
iambicDelay = event.byte3 << 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
midiProbe();
|
midiEventPacket_t event = MidiUSB.read();
|
||||||
setLED();
|
setLED();
|
||||||
|
|
||||||
|
if (event.header) {
|
||||||
|
adapter.HandleMIDI(event);
|
||||||
|
}
|
||||||
|
|
||||||
// Monitor straight key pin
|
// Monitor straight key pin
|
||||||
if (key.update() || qt_key.update()) {
|
if (key.update() || qt_key.update()) {
|
||||||
bool fell = key.fell() || qt_key.fell();
|
bool pressed = key.read() || qt_key.read();
|
||||||
midiKey(fell, 0);
|
adapter.HandlePaddle(PADDLE_STRAIGHT, pressed);
|
||||||
if (fell) {
|
|
||||||
tone(PIEZO, TONE);
|
|
||||||
} else {
|
|
||||||
noTone(PIEZO);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we made dit = dah, we have a straight key on the dit pin,
|
// If we made dit = dah, we have a straight key on the dit pin,
|
||||||
// so we skip iambic polling.
|
// so we skip other keys polling.
|
||||||
if (trs) {
|
if (trs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dit.update() || qt_dit.update()) {
|
if (dit.update() || qt_dit.update()) {
|
||||||
bool fell = dit.fell() || qt_dit.fell();
|
bool pressed = dit.read() || qt_dit.read();
|
||||||
midiKey(fell, 1);
|
adapter.HandlePaddle(PADDLE_DIT, pressed);
|
||||||
if (keyboard) {
|
|
||||||
if (fell) {
|
|
||||||
Keyboard.press(DIT_KEY);
|
|
||||||
} else {
|
|
||||||
Keyboard.release(DIT_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor dah pin
|
|
||||||
if (dah.update() || qt_dah.update()) {
|
if (dah.update() || qt_dah.update()) {
|
||||||
bool fell = dah.fell() || qt_dah.fell();
|
bool pressed = dah.read() | qt_dah.read();
|
||||||
midiKey(fell, 2);
|
adapter.HandlePaddle(PADDLE_DAH, pressed);
|
||||||
|
|
||||||
if (keyboard) {
|
|
||||||
if (fell) {
|
|
||||||
Keyboard.press(DAH_KEY);
|
|
||||||
} else {
|
|
||||||
Keyboard.release(DAH_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
let deviceFilter = { vendorId: 0x1234, productId: 0xabcd };
|
||||||
|
let requestParams = { filters: [deviceFilter] };
|
||||||
|
let outputReportId = 0x01;
|
||||||
|
let outputReport = new Uint8Array([42]);
|
||||||
|
|
||||||
|
function handleConnectedDevice(e) {
|
||||||
|
console.log("Device connected: " + e.device.productName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnectedDevice(e) {
|
||||||
|
console.log("Device disconnected: " + e.device.productName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputReport(e) {
|
||||||
|
console.log(e.device.productName + ": got input report " + e.reportId);
|
||||||
|
console.log(new Uint8Array(e.data.buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.hid.addEventListener("connect", handleConnectedDevice);
|
||||||
|
navigator.hid.addEventListener("disconnect", handleDisconnectedDevice);
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
navigator.hid.requestDevice(requestParams).then((devices) => {
|
||||||
|
if (devices.length == 0) return;
|
||||||
|
devices[0].open().then(() => {
|
||||||
|
console.log("Opened device: " + device.productName);
|
||||||
|
device.addEventListener("inputreport", handleInputReport);
|
||||||
|
device.sendReport(outputReportId, outputReport).then(() => {
|
||||||
|
console.log("Sent output report " + outputReportId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
document.querySelector("#moo").addEventListener("click", listen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init)
|
||||||
|
} else {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button id="moo">start</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue