vail-adapter

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

WrathPak  ·  2025-05-28

vail-adapter.ino

  1#include "config.h" 
  2
  3#include <MIDIUSB.h>
  4#include <Keyboard.h>
  5#include <Adafruit_FreeTouch.h>
  6#include <FlashStorage_SAMD.h>
  7#include "bounce2.h"
  8#include "touchbounce.h"
  9#include "adapter.h"
 10#include "equal_temperament.h"
 11
 12bool trs = false;
 13
 14Bounce dit = Bounce();
 15Bounce dah = Bounce();
 16Bounce key = Bounce(); 
 17TouchBounce qt_dit = TouchBounce();
 18TouchBounce qt_dah = TouchBounce();
 19TouchBounce qt_key = TouchBounce(); 
 20
 21VailAdapter adapter = VailAdapter(PIEZO_PIN);
 22
 23uint8_t loadToneFromEEPROM(); 
 24
 25void playDot(uint8_t noteNumber) {
 26  digitalWrite(LED_BUILTIN, LED_ON);
 27  tone(PIEZO_PIN, equalTemperamentNote[noteNumber]);
 28  delay(DOT_DURATION);
 29  digitalWrite(LED_BUILTIN, LED_OFF);
 30  noTone(PIEZO_PIN);
 31  delay(ELEMENT_SPACE);
 32}
 33
 34void playDash(uint8_t noteNumber) {
 35  digitalWrite(LED_BUILTIN, LED_ON);
 36  tone(PIEZO_PIN, equalTemperamentNote[noteNumber]);
 37  delay(DASH_DURATION);
 38  digitalWrite(LED_BUILTIN, LED_OFF);
 39  noTone(PIEZO_PIN);
 40  delay(ELEMENT_SPACE);
 41}
 42
 43void playVAIL(uint8_t noteNumber) {
 44  playDot(noteNumber); playDot(noteNumber); playDot(noteNumber); playDash(noteNumber);
 45  delay(CHAR_SPACE - ELEMENT_SPACE);
 46  playDot(noteNumber); playDash(noteNumber);
 47  delay(CHAR_SPACE - ELEMENT_SPACE);
 48  playDot(noteNumber); playDot(noteNumber);
 49  delay(CHAR_SPACE - ELEMENT_SPACE);
 50  playDot(noteNumber); playDash(noteNumber); playDot(noteNumber); playDot(noteNumber);
 51  noTone(PIEZO_PIN);
 52}
 53
 54uint8_t loadToneFromEEPROM() {
 55  if (EEPROM.read(EEPROM_VALID_FLAG_ADDR) == EEPROM_VALID_VALUE) {
 56    uint8_t txNote = EEPROM.read(EEPROM_TX_NOTE_ADDR);
 57    return txNote;
 58  } else {
 59    Serial.println("EEPROM not initialized, using default tone");
 60    return DEFAULT_TONE_NOTE;
 61  }
 62}
 63
 64void saveSettingsToEEPROM(uint8_t keyerType, uint16_t ditDuration, uint8_t txNote) {
 65  EEPROM.write(EEPROM_KEYER_TYPE_ADDR, keyerType);
 66  EEPROM.put(EEPROM_DIT_DURATION_ADDR, ditDuration);
 67  EEPROM.write(EEPROM_TX_NOTE_ADDR, txNote);
 68  EEPROM.write(EEPROM_VALID_FLAG_ADDR, EEPROM_VALID_VALUE);
 69  EEPROM.commit();
 70  Serial.print("Saved to EEPROM - Keyer: "); Serial.print(keyerType);
 71  Serial.print(", Dit Duration: "); Serial.print(ditDuration);
 72  Serial.print(", TX Note: "); Serial.println(txNote);
 73}
 74
 75void loadSettingsFromEEPROM() {
 76  if (EEPROM.read(EEPROM_VALID_FLAG_ADDR) == EEPROM_VALID_VALUE) {
 77    uint8_t keyerType = EEPROM.read(EEPROM_KEYER_TYPE_ADDR);
 78    uint16_t ditDurationVal; 
 79    EEPROM.get(EEPROM_DIT_DURATION_ADDR, ditDurationVal);
 80    uint8_t txNoteVal = EEPROM.read(EEPROM_TX_NOTE_ADDR); 
 81
 82    Serial.print("EEPROM values - Keyer: "); Serial.print(keyerType);
 83    Serial.print(", Dit Duration: "); Serial.print(ditDurationVal);
 84    Serial.print(", TX Note: "); Serial.println(txNoteVal);
 85
 86    midiEventPacket_t event;
 87    event.header = 0x0B; event.byte1 = 0xB0; 
 88    event.byte2 = 1; 
 89    event.byte3 = ditDurationVal / (2 * MILLISECOND); 
 90    adapter.HandleMIDI(event);
 91
 92    event.byte2 = 2; 
 93    event.byte3 = txNoteVal; 
 94    adapter.HandleMIDI(event);
 95
 96    if (keyerType >= 0 && keyerType <= 9) { 
 97      event.header = 0x0C; event.byte1 = 0xC0; 
 98      event.byte2 = keyerType; event.byte3 = 0;
 99      adapter.HandleMIDI(event);
100    }
101  } else {
102    Serial.println("EEPROM initializing with default values...");
103    EEPROM.write(EEPROM_KEYER_TYPE_ADDR, 1); 
104    EEPROM.put(EEPROM_DIT_DURATION_ADDR, (uint16_t)DEFAULT_ADAPTER_DIT_DURATION_MS);
105    EEPROM.write(EEPROM_TX_NOTE_ADDR, DEFAULT_TONE_NOTE);
106    EEPROM.write(EEPROM_VALID_FLAG_ADDR, EEPROM_VALID_VALUE);
107    EEPROM.commit();
108    Serial.println("EEPROM initialized. Loading these defaults now.");
109    loadSettingsFromEEPROM(); 
110  }
111}
112
113void setup() {
114  Serial.begin(9600);
115  delay(500); 
116  Serial.print("\n\nVail Adapter starting on: ");
117  Serial.println(BOARD_NAME);
118
119  pinMode(LED_BUILTIN, OUTPUT);
120  digitalWrite(LED_BUILTIN, LED_OFF); 
121
122  dit.attach(DIT_PIN, INPUT_PULLUP);
123  dah.attach(DAH_PIN, INPUT_PULLUP);
124  key.attach(KEY_PIN, INPUT_PULLUP);
125  
126  qt_dit.attach(QT_DIT_PIN);
127  qt_dah.attach(QT_DAH_PIN);
128  qt_key.attach(QT_KEY_PIN);
129
130#ifdef HAS_RADIO_OUTPUT
131  pinMode(RADIO_DIT_PIN, OUTPUT);
132  pinMode(RADIO_DAH_PIN, OUTPUT);
133  digitalWrite(RADIO_DIT_PIN, RADIO_INACTIVE_LEVEL); // Use configured inactive level
134  digitalWrite(RADIO_DAH_PIN, RADIO_INACTIVE_LEVEL); // Use configured inactive level
135  Serial.print("Radio Output Pins Initialized. Inactive Level: ");
136  Serial.println(RADIO_INACTIVE_LEVEL == LOW ? "LOW" : "HIGH");
137#endif
138
139  uint8_t startupTone = loadToneFromEEPROM(); 
140  Serial.println("Playing VAIL in Morse code at 20 WPM");
141  playVAIL(startupTone);
142  
143  loadSettingsFromEEPROM(); 
144
145  Serial.print("Adapter settings loaded - Keyer: "); Serial.print(adapter.getCurrentKeyerType());
146  Serial.print(", Dit Duration (ms): "); Serial.print(adapter.getDitDuration());
147  Serial.print(", TX Note: "); Serial.println(adapter.getTxNote());
148  Serial.print("Buzzer initially: "); Serial.println(adapter.isBuzzerEnabled() ? "ON" : "OFF");
149  Serial.print("Radio Mode initially: "); Serial.println(adapter.isRadioModeActive() ? "ON" : "OFF");
150
151  Keyboard.begin();
152  MidiUSB.flush(); 
153
154  for (int i = 0; i < 16; i++) { 
155    delay(20); 
156    dah.update(); 
157  }
158  if (dah.read() == LOW) { 
159    trs = true;
160    Serial.println("TRS plug potentially detected (DAH pin grounded).");
161  }
162}
163
164void setLED() {
165  bool finalLedState = false; 
166
167  if (adapter.isRadioModeActive()) {
168    finalLedState = (millis() % 400 < 200); 
169  } else if (!adapter.isBuzzerEnabled()) {
170    finalLedState = (millis() % 2000 < 1000); 
171  } else {
172    finalLedState = adapter.KeyboardMode();
173  }
174  digitalWrite(LED_BUILTIN, finalLedState ? LED_ON : LED_OFF);
175}
176
177void loop() {
178  unsigned int currentTime = millis(); 
179  midiEventPacket_t event = MidiUSB.read();
180
181  setLED();
182  adapter.Tick(currentTime);
183
184  if (event.header) {
185    adapter.HandleMIDI(event);
186  }
187
188  if (key.update()) { 
189    adapter.ProcessPaddleInput(PADDLE_STRAIGHT, !key.read(), false); 
190  }
191
192  if (trs) {
193      // If DAH pin is grounded (TRS), this suggests a straight key might be plugged in
194      // where DIT line (tip) is the key and DAH line (ring) is shorted to GND (sleeve).
195      // The current Bounce objects 'dit' and 'dah' are still attached to their original pins.
196      // If your TRS straight key uses the DIT_PIN for keying and grounds DAH_PIN:
197      // You might want to only read the 'dit' object as a straight key when trs is true.
198      // if (dit.update()) {
199      //    adapter.ProcessPaddleInput(PADDLE_STRAIGHT, !dit.read(), false);
200      // }
201      // And then skip the separate DIT/DAH paddle processing below if trs == true.
202      // This part depends on your exact TRS wiring and desired behavior.
203      // For now, assuming all inputs are polled and 'trs' is just an indicator.
204  }
205
206
207  if (dit.update()) {
208    adapter.ProcessPaddleInput(PADDLE_DIT, !dit.read(), false);
209  }
210  if (dah.update()) {
211    adapter.ProcessPaddleInput(PADDLE_DAH, !dah.read(), false);
212  }
213
214  if (qt_key.update()) {
215    adapter.ProcessPaddleInput(PADDLE_STRAIGHT, qt_key.read(), true); 
216  }
217  if (qt_dit.update()) {
218    adapter.ProcessPaddleInput(PADDLE_DIT, qt_dit.read(), true);
219  }
220  if (qt_dah.update()) {
221    adapter.ProcessPaddleInput(PADDLE_DAH, qt_dah.read(), true);
222  }
223}