-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathvail-adapter.ino
More file actions
395 lines (349 loc) · 13 KB
/
Copy pathvail-adapter.ino
File metadata and controls
395 lines (349 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
#include "config.h"
#include <MIDIUSB.h>
#include <Keyboard.h>
#ifndef NO_CAPACITIVE_TOUCH
#include <Adafruit_FreeTouch.h>
#endif
#include "bounce2.h"
#include "touchbounce.h"
#include "adapter.h"
#include "buttons.h"
#include "memory.h"
#include "morse_audio.h"
#include "settings_eeprom.h"
#include "menu_handler.h"
#include "equal_temperament.h"
bool trs = false;
unsigned long dahGroundedStartTime = 0; // Track how long DAH has been grounded
bool dahWasGroundedLastCheck = false; // Previous state for edge detection
unsigned long lastTrsCheckTime = 0; // Last time we checked for TRS
const unsigned long TRS_DETECTION_THRESHOLD = 1000; // 1 second of continuous grounding = TRS cable
const unsigned long TRS_CHECK_INTERVAL = 500; // Check every 500ms
Bounce dit = Bounce();
Bounce dah = Bounce();
Bounce key = Bounce();
#ifndef NO_CAPACITIVE_TOUCH
TouchBounce qt_dit = TouchBounce();
TouchBounce qt_dah = TouchBounce();
TouchBounce qt_key = TouchBounce();
#endif
VailAdapter adapter = VailAdapter(PIEZO_PIN);
#ifdef BUTTON_PIN
ButtonDebouncer buttonDebouncer;
// CW Memory system
CWMemory memorySlots[MAX_MEMORY_SLOTS]; // 3 memory slots
RecordingState recordingState; // Current recording state
PlaybackState playbackState; // Current playback state
#endif
void setup() {
Serial.begin(9600);
delay(500);
Serial.print("\n\nVail Adapter starting on: ");
Serial.println(BOARD_NAME);
#ifndef NO_LED
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_OFF);
#endif
#ifdef BUTTON_PIN
pinMode(BUTTON_PIN, INPUT);
Serial.println("Button input initialized on pin 3");
#endif
dit.attach(DIT_PIN, INPUT_PULLUP);
dah.attach(DAH_PIN, INPUT_PULLUP);
#ifndef TRRS_TRINKEY
// Trinkey doesn't need separate straight key pin - KEY_PIN same as DIT_PIN
key.attach(KEY_PIN, INPUT_PULLUP);
#endif
#ifdef TRRS_TRINKEY
// Configure TRRS jack ground pins - required for switched TRRS jack
pinMode(SLEEVE_PIN, OUTPUT);
digitalWrite(SLEEVE_PIN, LOW);
pinMode(RING2_PIN, OUTPUT);
digitalWrite(RING2_PIN, LOW);
Serial.println("TRRS ground pins (SLEEVE, RING2) configured as OUTPUT LOW");
// Increase debounce interval for TRRS jack pins - they have different electrical characteristics
dit.interval(25); // Increased from default 10ms to 25ms
dah.interval(25);
key.interval(25);
Serial.println("Debounce intervals increased to 25ms for TRRS jack stability");
#endif
#ifndef NO_CAPACITIVE_TOUCH
// Attach capacitive touch with calibrated per-pad thresholds
qt_dit.attach(QT_DIT_PIN, QT_DIT_THRESHOLD_PRESS, QT_DIT_THRESHOLD_RELEASE);
qt_dah.attach(QT_DAH_PIN, QT_DAH_THRESHOLD_PRESS, QT_DAH_THRESHOLD_RELEASE);
#ifdef QT_KEY_PIN
qt_key.attach(QT_KEY_PIN, QT_DIT_THRESHOLD_PRESS, QT_DIT_THRESHOLD_RELEASE); // Use DIT thresholds for KEY
#endif
#endif
#ifdef HAS_RADIO_OUTPUT
pinMode(RADIO_DIT_PIN, OUTPUT);
pinMode(RADIO_DAH_PIN, OUTPUT);
digitalWrite(RADIO_DIT_PIN, RADIO_INACTIVE_LEVEL); // Use configured inactive level
digitalWrite(RADIO_DAH_PIN, RADIO_INACTIVE_LEVEL); // Use configured inactive level
Serial.print("Radio Output Pins Initialized. Inactive Level: ");
Serial.println(RADIO_INACTIVE_LEVEL == LOW ? "LOW" : "HIGH");
#endif
// Initialize audio module
initMorseAudio(&adapter, PIEZO_PIN);
uint8_t startupTone = loadToneFromEEPROM();
Serial.println("Playing VAIL in Morse code at 20 WPM");
playVAIL(startupTone);
loadSettingsFromEEPROM(adapter);
loadRadioKeyerModeFromEEPROM(adapter);
#ifdef BUTTON_PIN
loadMemoriesFromEEPROM(memorySlots);
// Connect recording state to adapter for key capture
adapter.setRecordingState(&recordingState);
// Initialize menu handler with flush callback
initMenuHandler(&adapter, memorySlots, &recordingState, &playbackState, flushBounceState);
#endif
Serial.print("Adapter settings loaded - Keyer: "); Serial.print(adapter.getCurrentKeyerType());
Serial.print(", Dit Duration (ms): "); Serial.print(adapter.getDitDuration());
Serial.print(", TX Note: "); Serial.println(adapter.getTxNote());
Serial.print("Buzzer initially: "); Serial.println(adapter.isBuzzerEnabled() ? "ON" : "OFF");
Serial.print("Radio Mode initially: "); Serial.println(adapter.isRadioModeActive() ? "ON" : "OFF");
Serial.print("Radio Keyer Mode initially: "); Serial.println(adapter.isRadioKeyerMode() ? "ON" : "OFF");
Keyboard.begin();
MidiUSB.flush();
// Ensure clean keyboard state on startup
adapter.ReleaseAllKeys();
for (int i = 0; i < 16; i++) {
delay(20);
dah.update();
}
if (dah.read() == LOW) {
trs = true;
Serial.println("TRS plug potentially detected (DAH pin grounded).");
}
}
void flushBounceState() {
// Flush the Bounce state by updating multiple times without processing
// This clears any stale "pressed" states after mode changes
for (int i = 0; i < 3; i++) {
dit.update();
dah.update();
key.update();
delay(5);
}
Serial.println("Flushed Bounce state for dit/dah/key inputs");
}
void setLED() {
#ifndef NO_LED
bool finalLedState = false;
if (adapter.isRadioModeActive()) {
finalLedState = (millis() % 400 < 200);
} else if (!adapter.isBuzzerEnabled()) {
finalLedState = (millis() % 2000 < 1000);
} else {
finalLedState = adapter.KeyboardMode();
}
digitalWrite(LED_BUILTIN, finalLedState ? LED_ON : LED_OFF);
#endif
}
void loop() {
unsigned long currentTime = millis();
midiEventPacket_t event = MidiUSB.read();
setLED();
adapter.Tick(currentTime);
// Check for TRS cable hot-plug detection (every 500ms)
// ONLY active when already in Straight Key mode (keyer type 1)
// This requires user to manually switch to Straight Key mode before hot-plugging
if (currentTime - lastTrsCheckTime >= TRS_CHECK_INTERVAL) {
lastTrsCheckTime = currentTime;
// Only check for TRS if we're in Straight Key mode (keyer type 1)
bool inStraightKeyMode = (adapter.getCurrentKeyerType() == 1);
if (inStraightKeyMode && !trs) {
// Check if DAH pin is currently grounded (physical pin only, not capacitive)
bool dahIsGrounded = (digitalRead(DAH_PIN) == LOW);
if (dahIsGrounded) {
if (!dahWasGroundedLastCheck) {
// DAH just became grounded - start timer
dahGroundedStartTime = currentTime;
dahWasGroundedLastCheck = true;
} else {
// DAH has been continuously grounded - check duration
unsigned long groundedDuration = currentTime - dahGroundedStartTime;
if (groundedDuration >= TRS_DETECTION_THRESHOLD) {
// TRS cable detected! DAH has been continuously grounded for 1+ second
trs = true;
Serial.println("TRS CABLE DETECTED (hot-plug): DAH pin grounded while in Straight Key mode");
Serial.println("Enabling TRS mode: DIT pin will be used for straight key input, DAH pin ignored");
// Flush bounce state to clear any pending transitions
flushBounceState();
Serial.println("TRS mode active. Straight key input via DIT pin.");
}
}
} else {
dahWasGroundedLastCheck = false;
dahGroundedStartTime = 0;
}
} else if (trs) {
// If we're in TRS mode, check if cable was unplugged
bool dahIsGrounded = (digitalRead(DAH_PIN) == LOW);
if (!dahIsGrounded) {
// DAH is no longer grounded - cable unplugged
Serial.println("TRS cable unplugged (DAH no longer grounded)");
trs = false;
dahWasGroundedLastCheck = false;
dahGroundedStartTime = 0;
}
} else {
// Not in straight key mode and not in TRS mode - reset detection state
dahWasGroundedLastCheck = false;
dahGroundedStartTime = 0;
}
}
#ifdef BUTTON_PIN
MenuHandlerState& menuState = getMenuState();
// Update memory playback state machine
updatePlayback(playbackState);
// Control output during playback
static bool lastPlaybackKeyState = false;
static bool wasPlaying = false;
if (playbackState.isPlaying) {
wasPlaying = true;
if (playbackState.keyCurrentlyDown != lastPlaybackKeyState) {
if (playbackState.keyCurrentlyDown) {
// Key down
if (menuState.currentMode == MODE_PLAYING_MEMORY) {
// Normal mode playback: use BeginTx(relay) to pass paddle info for radio mode
// Convert paddle flag (0=DIT, 1=DAH) to PADDLE enum (PADDLE_DIT=0, PADDLE_DAH=1)
int relay = (playbackState.currentPaddle == 0) ? PADDLE_DIT : PADDLE_DAH;
adapter.BeginTx(relay);
} else {
// Memory management mode: piezo only (bypass buzzer enable check)
// Convert MIDI note to frequency
uint8_t midiNote = adapter.getTxNote();
int frequency = GET_EQUAL_TEMPERAMENT_NOTE(midiNote);
tone(PIEZO_PIN, frequency);
}
} else {
// Key up
if (menuState.currentMode == MODE_PLAYING_MEMORY) {
// Normal mode playback: use EndTx(relay) to pass paddle info for radio mode
// Convert paddle flag (0=DIT, 1=DAH) to PADDLE enum (PADDLE_DIT=0, PADDLE_DAH=1)
int relay = (playbackState.currentPaddle == 0) ? PADDLE_DIT : PADDLE_DAH;
adapter.EndTx(relay);
} else {
// Memory management mode: piezo only
noTone(PIEZO_PIN);
}
}
lastPlaybackKeyState = playbackState.keyCurrentlyDown;
}
} else if (wasPlaying) {
// Playback just finished
// The playback state machine ensures key is already released before stopping,
// so we don't need to call EndTx here (it would be a duplicate)
if (menuState.currentMode != MODE_PLAYING_MEMORY) {
// Memory management mode: ensure piezo is off
noTone(PIEZO_PIN);
}
lastPlaybackKeyState = false;
wasPlaying = false;
// Return to appropriate mode
if (menuState.currentMode == MODE_PLAYING_MEMORY) {
menuState.currentMode = MODE_NORMAL;
Serial.println("Playback finished - returned to normal mode");
}
}
// Check for recording timeout (25 seconds) or max transitions
if (recordingState.isRecording) {
if (recordingState.hasReachedMaxDuration() || recordingState.hasReachedMaxTransitions()) {
uint8_t activeSlot = recordingState.slotNumber;
Serial.println("Recording auto-stopped (timeout or max transitions reached)");
stopRecording(recordingState, memorySlots[activeSlot]);
saveMemoryToEEPROM(activeSlot, memorySlots[activeSlot]);
// Play completion tone
playAdjustmentBeep(false);
delay(100);
playAdjustmentBeep(true);
delay(100);
playAdjustmentBeep(true);
menuState.currentMode = MODE_MEMORY_MANAGEMENT;
Serial.println("Returned to memory management mode");
}
}
// Update menu handler (handles all button logic)
updateMenuHandler(currentTime, buttonDebouncer);
#endif
if (event.header) {
adapter.HandleMIDI(event);
}
#ifndef TRRS_TRINKEY
// Trinkey doesn't process separate straight key input
if (key.update()) {
adapter.ProcessPaddleInput(PADDLE_STRAIGHT, !key.read(), false);
#ifdef BUTTON_PIN
// Reset activity timer on CW key activity in setting modes
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
#endif
if (trs) {
// TRS mode: DAH pin is grounded (ring shorted to sleeve)
// DIT pin (tip) is the actual straight key input
// Only process DIT as straight key, ignore DAH completely
if (dit.update()) {
adapter.ProcessPaddleInput(PADDLE_STRAIGHT, !dit.read(), false);
#ifdef BUTTON_PIN
// Reset activity timer on CW key activity in setting modes
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
// Update DAH to keep Bounce state current, but don't process it
dah.update();
} else {
// Normal paddle mode: process both DIT and DAH separately
if (dit.update()) {
adapter.ProcessPaddleInput(PADDLE_DIT, !dit.read(), false);
#ifdef BUTTON_PIN
// Reset activity timer on CW key activity in setting modes
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
if (dah.update()) {
adapter.ProcessPaddleInput(PADDLE_DAH, !dah.read(), false);
#ifdef BUTTON_PIN
// Reset activity timer on CW key activity in setting modes
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
}
#ifndef NO_CAPACITIVE_TOUCH
#ifdef QT_KEY_PIN
if (qt_key.update()) {
adapter.ProcessPaddleInput(PADDLE_STRAIGHT, qt_key.read(), true);
#ifdef BUTTON_PIN
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
#endif
if (qt_dit.update()) {
adapter.ProcessPaddleInput(PADDLE_DIT, qt_dit.read(), true);
#ifdef BUTTON_PIN
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
if (qt_dah.update()) {
adapter.ProcessPaddleInput(PADDLE_DAH, qt_dah.read(), true);
#ifdef BUTTON_PIN
if (menuState.currentMode != MODE_NORMAL) {
menuState.lastActivityTime = currentTime;
}
#endif
}
#endif
}