vail-adapter

Firmware for USB morse code key adapter
git clone https://git.woozle.org/neale/vail-adapter.git

WrathPak  ·  2025-08-18

adapter.cpp

  1#include <Arduino.h>
  2#include <Keyboard.h>
  3#include <MIDIUSB.h>
  4#include <cstddef>
  5#include "keyers.h"
  6#include "adapter.h"
  7#include "polybuzzer.h"
  8
  9// For SAMD21 software reset if needed by other parts of code
 10#if defined(ARDUINO_ARCH_SAMD)
 11// NVIC_SystemReset() is typically available through Arduino.h / CMSIS includes
 12#endif
 13
 14extern void saveSettingsToEEPROM(uint8_t keyerType, uint16_t ditDuration, uint8_t txNote);
 15
 16VailAdapter::VailAdapter(unsigned int PiezoPin) {
 17this->buzzer = new PolyBuzzer(PiezoPin);
 18this->buzzerEnabled = true;
 19this->radioModeActive = false;
 20this->keyIsPressed = false;
 21this->keyPressStartTime = 0;
 22this->lastDitTime = 0;
 23this->ditPressCount = 0;
 24this->lastCapDahTime = 0;
 25this->capDahPressCount = 0;
 26this->radioDitState = false;
 27this->radioDahState = false;
 28this->keyboardMode = true;
 29this->keyer = NULL;
 30this->txNote = DEFAULT_TONE_NOTE;
 31this->ditDuration = DEFAULT_ADAPTER_DIT_DURATION_MS;
 32}
 33
 34bool VailAdapter::KeyboardMode() {
 35return this->keyboardMode;
 36}
 37
 38uint8_t VailAdapter::getCurrentKeyerType() const {
 39return getKeyerNumber(this->keyer);
 40}
 41
 42uint16_t VailAdapter::getDitDuration() const {
 43return this->ditDuration;
 44}
 45
 46uint8_t VailAdapter::getTxNote() const {
 47return this->txNote;
 48}
 49
 50bool VailAdapter::isBuzzerEnabled() const {
 51return this->buzzerEnabled;
 52}
 53
 54bool VailAdapter::isRadioModeActive() const {
 55return this->radioModeActive;
 56}
 57
 58void VailAdapter::ResetDitCounter() {
 59this->ditPressCount = 0;
 60}
 61
 62void VailAdapter::ResetDahCounter() {
 63this->capDahPressCount = 0;
 64}
 65
 66// Corrected MIDI key event function
 67void VailAdapter::midiKey(uint8_t key, bool down) {
 68uint8_t header;
 69uint8_t status_byte;
 70uint8_t velocity;
 71
 72if (down) { // Note On
 73    header = 0x09;      // CIN = 9 (Note On) for USB MIDI event packet
 74    status_byte = 0x90; // MIDI Status = 0x90 (Note On, Channel 1)
 75    velocity = 0x7F;    // Standard velocity for Note On
 76} else { // Note Off
 77    header = 0x08;      // CIN = 8 (Note Off) for USB MIDI event packet
 78    status_byte = 0x80; // MIDI Status = 0x80 (Note Off, Channel 1)
 79    velocity = 0x00;    // Velocity for Note Off (0x00 is common)
 80}
 81// Construct the MIDI event packet for MIDIUSB library
 82midiEventPacket_t event = {header, status_byte, key, velocity};
 83MidiUSB.sendMIDI(event);
 84MidiUSB.flush();
 85}
 86
 87void VailAdapter::keyboardKey(uint8_t key, bool down) {
 88if (down) {
 89Keyboard.press(key);
 90} else {
 91Keyboard.release(key);
 92}
 93}
 94
 95#ifdef HAS_RADIO_OUTPUT
 96void VailAdapter::setRadioDit(bool active) {
 97digitalWrite(RADIO_DIT_PIN, active ? RADIO_ACTIVE_LEVEL : RADIO_INACTIVE_LEVEL);
 98}
 99
100void VailAdapter::setRadioDah(bool active) {
101digitalWrite(RADIO_DAH_PIN, active ? RADIO_ACTIVE_LEVEL : RADIO_INACTIVE_LEVEL);
102}
103#else
104void VailAdapter::setRadioDit(bool active) {(void)active;}
105void VailAdapter::setRadioDah(bool active) {(void)active;}
106#endif
107
108void VailAdapter::BeginTx() {
109if (!keyIsPressed) {
110keyIsPressed = true;
111if (!this->radioModeActive) {
112if (this->keyPressStartTime == 0) {
113this->keyPressStartTime = millis();
114}
115}
116}
117
118if (this->buzzerEnabled && !this->radioModeActive) { 
119    this->buzzer->Note(0, this->txNote);
120}
121
122if (!this->radioModeActive) { 
123    if (this->keyboardMode) {
124        this->keyboardKey(KEY_LEFT_CTRL, true);
125    } else {
126        this->midiKey(0, true); 
127    }
128}
129}
130
131void VailAdapter::EndTx() {
132if (keyIsPressed) {
133keyIsPressed = false;
134if (!this->radioModeActive) {
135this->keyPressStartTime = 0;
136}
137}
138
139this->buzzer->NoTone(0); 
140
141if (!this->radioModeActive) { 
142    if (this->keyboardMode) {
143        this->keyboardKey(KEY_LEFT_CTRL, false);
144    } else {
145        this->midiKey(0, false);
146    }
147}
148}
149
150void VailAdapter::DisableBuzzer() {
151this->buzzer->NoTone(0);
152this->buzzer->Note(1, 70); delay(100);
153this->buzzer->Note(1, 65); delay(100);
154this->buzzer->Note(1, 60); delay(100);
155this->buzzer->NoTone(1);
156this->buzzerEnabled = false;
157Serial.println("Buzzer Disabled");
158}
159
160void VailAdapter::ToggleRadioMode() {
161#ifdef HAS_RADIO_OUTPUT
162this->radioModeActive = !this->radioModeActive;
163
164if (keyer) keyer->Release(); 
165if (keyIsPressed) EndTx(); 
166
167setRadioDit(false); 
168setRadioDah(false);
169radioDitState = false;
170radioDahState = false;
171keyIsPressed = false; 
172
173if (this->radioModeActive) {
174    Serial.println("Radio Mode Activated (Sidetone Disabled)");
175    this->buzzer->NoTone(0); 
176    this->buzzer->Note(1, 60); delay(100);
177    this->buzzer->Note(1, 65); delay(100);
178    this->buzzer->Note(1, 70); delay(100);
179    this->buzzer->NoTone(1);
180} else {
181    Serial.println("Radio Mode Deactivated. Resetting controller...");
182    this->buzzer->Note(1, 70); delay(100);
183    this->buzzer->Note(1, 65); delay(100);
184    this->buzzer->Note(1, 60); delay(100);
185    this->buzzer->NoTone(1);
186    delay(100); 
187    
188    NVIC_SystemReset(); 
189}
190#else
191Serial.println("Radio output not configured. Radio mode unavailable.");
192this->buzzer->Tone(1, 100); delay(200); this->buzzer->NoTone(1);
193#endif
194}
195
196void VailAdapter::ProcessPaddleInput(Paddle paddle, bool pressed, bool isCapacitive) {
197unsigned long currentTime = millis();
198
199if (paddle == PADDLE_DIT && pressed && this->buzzerEnabled) {
200    if (currentTime - this->lastDitTime < SPAM_DISABLE_WINDOW) {
201        this->ditPressCount++;
202        if (this->ditPressCount >= DIT_SPAM_COUNT_BUZZER_DISABLE) {
203            this->DisableBuzzer();
204            this->ditPressCount = 0; 
205        }
206    } else {
207        this->ditPressCount = 1;
208    }
209    this->lastDitTime = currentTime;
210}
211#ifdef HAS_RADIO_OUTPUT
212if (paddle == PADDLE_DAH && isCapacitive && pressed) {
213if (currentTime - this->lastCapDahTime < SPAM_DISABLE_WINDOW) {
214this->capDahPressCount++;
215if (this->capDahPressCount >= DAH_SPAM_COUNT_RADIO_MODE) {
216this->ToggleRadioMode();
217this->capDahPressCount = 0;
218}
219} else {
220this->capDahPressCount = 1;
221}
222this->lastCapDahTime = currentTime;
223} else if (paddle == PADDLE_DAH && !isCapacitive && pressed) {
224this->capDahPressCount = 0;
225} else if (paddle == PADDLE_DIT && pressed) {
226this->capDahPressCount = 0;
227}
228#endif
229
230if (this->radioModeActive) {
231#ifdef HAS_RADIO_OUTPUT
232bool radioKeyIsActiveBefore = radioDitState || radioDahState;
233
234    if (paddle == PADDLE_DIT) {
235        radioDitState = pressed;
236        setRadioDit(radioDitState);
237    } else if (paddle == PADDLE_DAH) {
238        radioDahState = pressed;
239        setRadioDah(radioDahState);
240    } else if (paddle == PADDLE_STRAIGHT) { 
241        radioDitState = pressed; 
242        setRadioDit(pressed);
243    }
244
245    keyIsPressed = radioDitState || radioDahState; 
246
247    if (!keyIsPressed && radioKeyIsActiveBefore) { 
248         this->buzzer->NoTone(0);
249    }
250#endif
251} else {
252if (paddle == PADDLE_STRAIGHT) {
253if (pressed) BeginTx(); else EndTx();
254} else {
255if (this->keyer) {
256this->keyer->Key(paddle, pressed);
257} else {
258bool currentPaddleActivity = false;
259if (paddle == PADDLE_DIT) {
260if (this->keyboardMode) this->keyboardKey(DIT_KEYBOARD_KEY, pressed);
261else this->midiKey(1, pressed);
262if (pressed) currentPaddleActivity = true;
263} else if (paddle == PADDLE_DAH) {
264if (this->keyboardMode) this->keyboardKey(DAH_KEYBOARD_KEY, pressed);
265else this->midiKey(2, pressed);
266if (pressed) currentPaddleActivity = true;
267}
268
269            if (currentPaddleActivity) { 
270                if(!keyIsPressed) BeginTx(); 
271            } else { 
272                if(keyIsPressed) EndTx();
273            }
274        }
275    }
276  }
277}
278
279void VailAdapter::HandleMIDI(midiEventPacket_t event) {
280uint16_t msg = (event.byte1 << 8) | (event.byte2 << 0);
281switch (event.byte1) {
282case 0xB0:
283switch (event.byte2) {
284case 0:
285this->keyboardMode = (event.byte3 > 0x3f);
286Serial.print("Keyboard mode: "); Serial.println(this->keyboardMode ? "ON" : "OFF");
287MidiUSB.sendMIDI(event);
288break;
289case 1:
290this->ditDuration = event.byte3 * 2 * MILLISECOND;
291if (this->keyer) {
292this->keyer->SetDitDuration(this->ditDuration);
293}
294Serial.print("Dit duration set to: "); Serial.println(this->ditDuration);
295saveSettingsToEEPROM(getCurrentKeyerType(), this->ditDuration, this->txNote);
296break;
297case 2:
298this->txNote = event.byte3;
299Serial.print("TX Note set to: "); Serial.println(this->txNote);
300
301saveSettingsToEEPROM(getCurrentKeyerType(), this->ditDuration, this->txNote);
302break;
303}
304break;
305case 0xC0:
306if (this->keyer) {
307this->keyer->Release();
308this->keyer = NULL;
309}
310this->keyer = GetKeyerByNumber(event.byte2, this);
311if (this->keyer) {
312this->keyer->SetDitDuration(this->ditDuration);
313Serial.print("Keyer mode set to: "); Serial.println(event.byte2);
314} else {
315Serial.print("Keyer mode set to passthrough (or invalid): "); Serial.println(event.byte2);
316}
317saveSettingsToEEPROM(event.byte2, this->ditDuration, this->txNote);
318break;
319case 0x80:
320if (this->buzzerEnabled && !this->radioModeActive) this->buzzer->NoTone(1);
321break;
322case 0x90:
323if (this->buzzerEnabled && !this->radioModeActive) this->buzzer->Note(1, event.byte2);
324break;
325}
326}
327
328void VailAdapter::Tick(unsigned int currentMillis) {
329if (!radioModeActive && keyIsPressed && this->buzzerEnabled && this->keyPressStartTime > 0) {
330if (currentMillis - this->keyPressStartTime >= KEY_HOLD_DISABLE_THRESHOLD) {
331this->DisableBuzzer();
332}
333}
334
335if (this->keyer && !this->radioModeActive) { 
336    this->keyer->Tick(currentMillis);
337}
338}
339