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
|
||||
ARDUINO_DIR = /app/Arduino
|
||||
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.sh $< $(UF2_MOUNT)
|
||||
|
||||
clean:
|
||||
rm -rf build/*
|
||||
|
||||
# uf2conv.py is covered by an MIT license.
|
||||
build/uf2conv.py: build/uf2families.json
|
||||
mkdir -p build
|
||||
|
@ -16,13 +20,19 @@ build/uf2families.json:
|
|||
mkdir -p build
|
||||
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/%.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 \
|
||||
-build-cache ~/.cache/arduino \
|
||||
-build-path build \
|
||||
-build-path build/$* \
|
||||
-core-api-version 10813 \
|
||||
-fqbn $(FQBN) \
|
||||
-hardware ~/.arduino15/packages \
|
||||
|
@ -34,6 +44,7 @@ build/%.bin: % *.cpp *.h
|
|||
-libraries ~/Arduino/libraries \
|
||||
-compile \
|
||||
$<
|
||||
mv build/$*/vail-adapter.ino.bin $@
|
||||
|
||||
upload: vail-adapter.ino
|
||||
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 "bounce2.h"
|
||||
#include "touchbounce.h"
|
||||
#include "adapter.h"
|
||||
|
||||
#define DIT_PIN 2
|
||||
#define DAH_PIN 1
|
||||
|
@ -19,8 +20,8 @@
|
|||
#define LED_ON false // Xiao inverts this logic for some reason
|
||||
#define LED_OFF (!LED_ON)
|
||||
|
||||
#define DIT_KEY KEY_LEFT_CTRL
|
||||
#define DAH_KEY KEY_RIGHT_CTRL
|
||||
#define DIT_KEYBOARD_KEY KEY_LEFT_CTRL
|
||||
#define DAH_KEYBOARD_KEY KEY_RIGHT_CTRL
|
||||
#define TONE 550
|
||||
|
||||
#define MILLISECOND 1
|
||||
|
@ -35,10 +36,10 @@ Bounce key = Bounce();
|
|||
TouchBounce qt_dit = TouchBounce();
|
||||
TouchBounce qt_dah = TouchBounce();
|
||||
TouchBounce qt_key = TouchBounce();
|
||||
VailAdapter adapter = VailAdapter(PIEZO);
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
pinMode(PIEZO, OUTPUT);
|
||||
dit.attach(DIT_PIN, INPUT_PULLUP);
|
||||
dah.attach(DAH_PIN, INPUT_PULLUP);
|
||||
key.attach(KEY_PIN, INPUT_PULLUP);
|
||||
|
@ -85,76 +86,33 @@ void setLED() {
|
|||
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() {
|
||||
midiProbe();
|
||||
midiEventPacket_t event = MidiUSB.read();
|
||||
setLED();
|
||||
|
||||
if (event.header) {
|
||||
adapter.HandleMIDI(event);
|
||||
}
|
||||
|
||||
// Monitor straight key pin
|
||||
if (key.update() || qt_key.update()) {
|
||||
bool fell = key.fell() || qt_key.fell();
|
||||
midiKey(fell, 0);
|
||||
if (fell) {
|
||||
tone(PIEZO, TONE);
|
||||
} else {
|
||||
noTone(PIEZO);
|
||||
}
|
||||
bool pressed = key.read() || qt_key.read();
|
||||
adapter.HandlePaddle(PADDLE_STRAIGHT, pressed);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dit.update() || qt_dit.update()) {
|
||||
bool fell = dit.fell() || qt_dit.fell();
|
||||
midiKey(fell, 1);
|
||||
if (keyboard) {
|
||||
if (fell) {
|
||||
Keyboard.press(DIT_KEY);
|
||||
} else {
|
||||
Keyboard.release(DIT_KEY);
|
||||
}
|
||||
}
|
||||
bool pressed = dit.read() || qt_dit.read();
|
||||
adapter.HandlePaddle(PADDLE_DIT, pressed);
|
||||
}
|
||||
|
||||
// Monitor dah pin
|
||||
if (dah.update() || qt_dah.update()) {
|
||||
bool fell = dah.fell() || qt_dah.fell();
|
||||
midiKey(fell, 2);
|
||||
|
||||
if (keyboard) {
|
||||
if (fell) {
|
||||
Keyboard.press(DAH_KEY);
|
||||
} else {
|
||||
Keyboard.release(DAH_KEY);
|
||||
}
|
||||
}
|
||||
bool pressed = dah.read() | qt_dah.read();
|
||||
adapter.HandlePaddle(PADDLE_DAH, pressed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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