discstation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,914 @@
1
+ #include <Wire.h>
2
+ #include <Adafruit_GFX.h>
3
+ #include <Adafruit_SSD1306.h>
4
+ #include <QRCode.h>
5
+
6
+ #define SCREEN_WIDTH 128
7
+ #define SCREEN_HEIGHT 64
8
+ #define OLED_RESET -1
9
+ #define I2C_ADDRESS 0x3C
10
+
11
+ #define BTN_SELECT_PIN 21 // D3
12
+ #define BTN_UP_PIN 20 // D9/MISO
13
+ #define BTN_DOWN_PIN 18 // D10/MOSI
14
+ #define LED_POWER_PIN 15 // Built-in user LED
15
+ #define LED_ACT_PIN 16 // D6/TX
16
+ #define POT_PIN 0 // D0/A0 (ADC)
17
+
18
+ #define DEBOUNCE_MS 50
19
+ #define LONG_PRESS_MS 1000
20
+ #define POT_READ_MS 200
21
+ #define DONE_RESET_MS 30000
22
+ #define STANDBY_BLANK_MS 60000
23
+ #define PING_TIMEOUT_MS 30000
24
+ #define LED_BLINK_MS 250
25
+
26
+ #define MAX_HOME_MODES 5
27
+ #define BURN_COUNT 4
28
+ #define SPEED_COUNT 4
29
+
30
+ int homeCount = 3;
31
+ String homeModes[MAX_HOME_MODES] = {"BURN", "PLAY", "RIP"};
32
+ String discName = "";
33
+
34
+ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
35
+
36
+ String ipUrl = "";
37
+
38
+ enum UiState {
39
+ UI_STATUS,
40
+ UI_HOME,
41
+ UI_BURN_READY,
42
+ UI_PLAY,
43
+ UI_STANDBY,
44
+ UI_WAITING,
45
+ UI_IP,
46
+ UI_DISCONNECTED,
47
+ UI_LOADING
48
+ };
49
+
50
+ const char* BURN_MODES[BURN_COUNT] = {"AUTO", "BEST", "LONG", "TEST"};
51
+ const char* SPEED_MODES[SPEED_COUNT] = {"Auto", "6x", "4x", "3x"};
52
+
53
+ UiState uiState = UI_STATUS;
54
+
55
+ String titleLine = "";
56
+ String metaLine = "";
57
+ String fitLine = "";
58
+ String discLine = "Disc: none";
59
+ String line1 = "Status: READY";
60
+ String line2 = "";
61
+ String line3 = "";
62
+ String waitingLine1 = "";
63
+ String waitingLine2 = "";
64
+ String loadingLine = "";
65
+ int loadingDots = 0;
66
+ unsigned long lastLoadingAnim = 0;
67
+ int progressPercent = -1;
68
+
69
+ bool displayOk = false;
70
+ unsigned long returnToHomeAt = 0;
71
+ unsigned long playStatusAt = 0;
72
+ bool playStatusTemp = false;
73
+ unsigned long lastPotRead = 0;
74
+
75
+ unsigned long selectDownAt = 0;
76
+ bool selectDown = false;
77
+ unsigned long selectLastDebounce = 0;
78
+
79
+ unsigned long upDownAt = 0;
80
+ bool upDown = false;
81
+ unsigned long upLastDebounce = 0;
82
+
83
+ unsigned long downDownAt = 0;
84
+ bool downDown = false;
85
+ unsigned long downLastDebounce = 0;
86
+
87
+ int ffSpeedIndex = 0;
88
+ int rewSpeedIndex = 0;
89
+
90
+ bool actLedState = false;
91
+ unsigned long lastLedBlink = 0;
92
+ int homeIndex = 0;
93
+ int burnModeIndex = 0;
94
+ int burnSpeedIndex = 0;
95
+ bool editingSpeed = false;
96
+ int playVolume = 50;
97
+ unsigned long standbyStartTime = 0;
98
+ unsigned long lastMsgTime = 0;
99
+ bool displayBlank = false;
100
+ bool audioPlayMode = false;
101
+ int displayRotation = 0;
102
+
103
+ int indeterminateCount = 0;
104
+ unsigned long lastIndeterminateAnim = 0;
105
+
106
+ int readBucket(int count) {
107
+ int raw = analogRead(POT_PIN);
108
+ int idx = raw * count / 4096;
109
+ if (idx < 0) idx = 0;
110
+ if (idx >= count) idx = count - 1;
111
+ return idx;
112
+ }
113
+
114
+ int readPercent() {
115
+ int raw = analogRead(POT_PIN);
116
+ int pct = raw * 100 / 4095;
117
+ if (pct < 0) pct = 0;
118
+ if (pct > 100) pct = 100;
119
+ return ((pct + 2) / 5) * 5;
120
+ }
121
+
122
+ void sendHomeMode() {
123
+ if (homeIndex >= 0 && homeIndex < homeCount) {
124
+ Serial.print("MENU:");
125
+ Serial.println(homeModes[homeIndex]);
126
+ }
127
+ }
128
+
129
+ void sendBurnMode() {
130
+ Serial.print("MODE:");
131
+ Serial.println(BURN_MODES[burnModeIndex]);
132
+ }
133
+
134
+ void printUpper(String value) {
135
+ value.toUpperCase();
136
+ display.print(value);
137
+ }
138
+
139
+ void drawChrome() {
140
+ display.setRotation(displayRotation);
141
+ display.setTextSize(1);
142
+ display.setTextColor(SSD1306_WHITE);
143
+ display.drawLine(0, 0, 4, 0, SSD1306_WHITE);
144
+ display.drawLine(0, 0, 0, 4, SSD1306_WHITE);
145
+ display.drawLine(123, 0, 127, 0, SSD1306_WHITE);
146
+ display.drawLine(127, 0, 127, 4, SSD1306_WHITE);
147
+ display.drawLine(0, 63, 4, 63, SSD1306_WHITE);
148
+ display.drawLine(0, 59, 0, 63, SSD1306_WHITE);
149
+ display.drawLine(123, 63, 127, 63, SSD1306_WHITE);
150
+ display.drawLine(127, 59, 127, 63, SSD1306_WHITE);
151
+ }
152
+
153
+ void drawHeader() {
154
+ display.setTextSize(1);
155
+ display.setTextColor(SSD1306_WHITE);
156
+ drawChrome();
157
+ display.setCursor(4, 0);
158
+ display.print("DISCSTATION // PANEL");
159
+ display.drawLine(6, 10, 121, 10, SSD1306_WHITE);
160
+ }
161
+
162
+ void drawHome() {
163
+ uiState = UI_HOME;
164
+ returnToHomeAt = 0;
165
+ if (!displayOk) return;
166
+
167
+ display.clearDisplay();
168
+ drawHeader();
169
+
170
+ int y = 14;
171
+ if (discName.length() > 0) {
172
+ display.setTextSize(1);
173
+ display.setCursor(2, y);
174
+ printUpper(discName);
175
+ y += 10;
176
+ } else {
177
+ display.setCursor(2, y);
178
+ printUpper(discLine);
179
+ y += 10;
180
+ }
181
+
182
+ for (int i = 0; i < homeCount; i++) {
183
+ display.setCursor(2, y);
184
+ display.print(i == homeIndex ? ">" : " ");
185
+ display.setCursor(14, y);
186
+ printUpper(homeModes[i]);
187
+ y += 10;
188
+ }
189
+
190
+ display.display();
191
+ }
192
+
193
+ void drawBurnReady() {
194
+ uiState = UI_BURN_READY;
195
+ returnToHomeAt = 0;
196
+ if (!displayOk) return;
197
+
198
+ display.clearDisplay();
199
+ drawHeader();
200
+
201
+ display.setCursor(2, 14);
202
+ printUpper(titleLine);
203
+
204
+ display.setCursor(2, 25);
205
+ printUpper(metaLine);
206
+
207
+ display.setCursor(2, 37);
208
+ printUpper(fitLine);
209
+
210
+ display.setCursor(2, 49);
211
+ display.print(editingSpeed ? ' ' : '>');
212
+ display.setCursor(14, 49);
213
+ display.print(BURN_MODES[burnModeIndex]);
214
+
215
+ display.setCursor(62, 49);
216
+ display.print(editingSpeed ? '>' : ' ');
217
+ display.setCursor(74, 49);
218
+ display.print(SPEED_MODES[burnSpeedIndex]);
219
+
220
+ display.setCursor(2, 57);
221
+ display.print("HOLD SELECT // GO");
222
+
223
+ display.display();
224
+ }
225
+
226
+ void drawWaiting() {
227
+ uiState = UI_WAITING;
228
+ returnToHomeAt = 0;
229
+ if (!displayOk) return;
230
+
231
+ display.clearDisplay();
232
+ drawHeader();
233
+
234
+ display.setCursor(2, 16);
235
+ printUpper(waitingLine1);
236
+
237
+ display.setCursor(2, 30);
238
+ printUpper(waitingLine2);
239
+
240
+ display.setCursor(2, 49);
241
+ display.print("SHORT // GO");
242
+
243
+ display.display();
244
+ }
245
+
246
+ void drawStatus() {
247
+ uiState = UI_STATUS;
248
+ if (!displayOk) return;
249
+
250
+ display.clearDisplay();
251
+ drawHeader();
252
+
253
+ display.setCursor(2, 16);
254
+ printUpper(line1);
255
+ display.setCursor(2, 30);
256
+ printUpper(line2);
257
+ display.setCursor(2, 44);
258
+ printUpper(line3);
259
+
260
+ if (progressPercent >= 0) {
261
+ int barW = map(progressPercent, 0, 100, 0, 124);
262
+ display.drawRect(2, 54, 124, 8, SSD1306_WHITE);
263
+ if (barW > 0) {
264
+ display.fillRect(2, 54, barW, 8, SSD1306_WHITE);
265
+ }
266
+ } else {
267
+ String dots = "";
268
+ for (int i = 0; i < indeterminateCount; i++) dots += ".";
269
+ display.setCursor(2, 56);
270
+ display.print(dots);
271
+ }
272
+
273
+ display.display();
274
+ }
275
+
276
+ void drawQrCode(String value) {
277
+ drawChrome();
278
+ QRCode qr;
279
+ uint8_t qrData[qrcode_getBufferSize(2)];
280
+ if (qrcode_initText(&qr, qrData, 2, ECC_LOW, value.c_str()) < 0) {
281
+ display.setCursor(2, 25);
282
+ display.print("QR ERROR // USE URL");
283
+ return;
284
+ }
285
+ const int scale = 2;
286
+ const int quiet = 2;
287
+ const int pixels = qr.size * scale;
288
+ const int left = (SCREEN_WIDTH - pixels) / 2;
289
+ const int top = 2;
290
+ display.fillRect(left - quiet, top - quiet, pixels + quiet * 2, pixels + quiet * 2, SSD1306_WHITE);
291
+ for (uint8_t y = 0; y < qr.size; y++) {
292
+ for (uint8_t x = 0; x < qr.size; x++) {
293
+ if (qrcode_getModule(&qr, x, y)) {
294
+ display.fillRect(left + x * scale, top + y * scale, scale, scale, SSD1306_BLACK);
295
+ }
296
+ }
297
+ }
298
+ display.setCursor(2, 57);
299
+ display.print("SCAN // DISCSTATION");
300
+ }
301
+
302
+ void drawIP() {
303
+ uiState = UI_IP;
304
+ returnToHomeAt = 0;
305
+ if (!displayOk) return;
306
+
307
+ display.clearDisplay();
308
+ drawQrCode(ipUrl);
309
+
310
+ display.display();
311
+ }
312
+
313
+ void drawStandby() {
314
+ uiState = UI_STANDBY;
315
+ returnToHomeAt = 0;
316
+ standbyStartTime = millis();
317
+ displayBlank = false;
318
+ if (!displayOk) return;
319
+
320
+ display.clearDisplay();
321
+ drawHeader();
322
+ display.setCursor(2, 25);
323
+ display.print("STANDBY // READY");
324
+ display.setCursor(2, 40);
325
+ display.print("INSERT MEDIA");
326
+ display.display();
327
+ }
328
+
329
+ void drawDisconnected() {
330
+ uiState = UI_DISCONNECTED;
331
+ returnToHomeAt = 0;
332
+ displayBlank = false;
333
+ if (!displayOk) return;
334
+
335
+ display.clearDisplay();
336
+ drawHeader();
337
+ display.setCursor(2, 25);
338
+ display.print("DISCONNECTED // LINK");
339
+ display.setCursor(2, 40);
340
+ display.print("CHECK USB");
341
+ display.display();
342
+ }
343
+
344
+ void drawLoading() {
345
+ uiState = UI_LOADING;
346
+ returnToHomeAt = 0;
347
+ displayBlank = false;
348
+ if (!displayOk) return;
349
+
350
+ display.clearDisplay();
351
+ drawHeader();
352
+
353
+ display.setTextSize(1);
354
+ display.setCursor(2, 18);
355
+ printUpper(loadingLine);
356
+
357
+ String dots = "";
358
+ for (int i = 0; i < loadingDots; i++) dots += ".";
359
+ display.setCursor(2, 32);
360
+ display.print(dots);
361
+ display.setCursor(70, 32);
362
+ display.print("LOADING //");
363
+
364
+ display.display();
365
+ }
366
+
367
+ void wakeDisplay() {
368
+ if (!displayBlank || !displayOk) return;
369
+ display.ssd1306_command(0xAF);
370
+ displayBlank = false;
371
+ switch (uiState) {
372
+ case UI_HOME: drawHome(); break;
373
+ case UI_STATUS: drawStatus(); break;
374
+ case UI_IP: drawIP(); break;
375
+ case UI_BURN_READY: drawBurnReady(); break;
376
+ case UI_PLAY: drawPlay(); break;
377
+ case UI_WAITING: drawWaiting(); break;
378
+ case UI_STANDBY: drawStandby(); break;
379
+ case UI_DISCONNECTED: drawDisconnected(); break;
380
+ case UI_LOADING: drawLoading(); break;
381
+ }
382
+ }
383
+
384
+ void drawPlay() {
385
+ uiState = UI_PLAY;
386
+ returnToHomeAt = 0;
387
+ if (!displayOk) return;
388
+
389
+ display.clearDisplay();
390
+ drawHeader();
391
+
392
+ display.setCursor(2, 16);
393
+ printUpper(line1);
394
+ display.setCursor(2, 30);
395
+ printUpper(line2);
396
+ display.setCursor(2, 44);
397
+ display.print("VOL // ");
398
+ display.print(playVolume);
399
+ display.print("%");
400
+ if (!audioPlayMode) {
401
+ display.setCursor(2, 56);
402
+ display.print("SELECT // PLAY");
403
+ }
404
+
405
+ display.display();
406
+ }
407
+
408
+ void parseMessage(String msg) {
409
+ msg.trim();
410
+
411
+ if (msg == "PING") {
412
+ lastMsgTime = millis();
413
+ Serial.println("PONG");
414
+ if (uiState == UI_DISCONNECTED) drawStandby();
415
+ return;
416
+ }
417
+
418
+ wakeDisplay();
419
+ lastMsgTime = millis();
420
+
421
+ Serial.print("RCV:");
422
+ Serial.println(msg);
423
+
424
+ if (msg.startsWith("HOME:")) {
425
+ homeIndex = readBucket(homeCount);
426
+ drawHome();
427
+
428
+ } else if (msg.startsWith("DISC:")) {
429
+ discLine = msg.substring(5);
430
+ if (discLine.length() > 20) discLine = discLine.substring(0, 20);
431
+ if (uiState == UI_HOME) drawHome();
432
+
433
+ } else if (msg.startsWith("DISC_NAME:")) {
434
+ discName = msg.substring(10);
435
+ if (discName.length() > 18) discName = discName.substring(0, 18);
436
+ if (uiState == UI_HOME) drawHome();
437
+
438
+ } else if (msg.startsWith("MENU_ITEMS:")) {
439
+ String items = msg.substring(11);
440
+ homeCount = 0;
441
+ while (items.length() > 0 && homeCount < MAX_HOME_MODES) {
442
+ int comma = items.indexOf(',');
443
+ if (comma < 0) {
444
+ homeModes[homeCount++] = items;
445
+ break;
446
+ }
447
+ homeModes[homeCount++] = items.substring(0, comma);
448
+ items = items.substring(comma + 1);
449
+ }
450
+ if (homeIndex >= homeCount) homeIndex = 0;
451
+ if (uiState == UI_HOME) drawHome();
452
+
453
+ } else if (msg.startsWith("STANDBY:")) {
454
+ drawStandby();
455
+
456
+ } else if (msg.startsWith("TITLE:")) {
457
+ titleLine = msg.substring(6);
458
+ if (titleLine.length() > 20) titleLine = titleLine.substring(0, 20);
459
+ burnModeIndex = readBucket(BURN_COUNT);
460
+ drawBurnReady();
461
+
462
+ } else if (msg.startsWith("META:")) {
463
+ metaLine = msg.substring(5);
464
+ if (metaLine.length() > 20) metaLine = metaLine.substring(0, 20);
465
+ drawBurnReady();
466
+
467
+ } else if (msg.startsWith("FIT:")) {
468
+ fitLine = msg.substring(4);
469
+ if (fitLine.length() > 20) fitLine = fitLine.substring(0, 20);
470
+ drawBurnReady();
471
+
472
+ } else if (msg.startsWith("PLAY:")) {
473
+ line1 = msg.substring(5);
474
+ line2 = "Playing disc";
475
+ playVolume = readPercent();
476
+ drawPlay();
477
+
478
+ } else if (msg.startsWith("PLAY_MODE:")) {
479
+ audioPlayMode = msg.substring(10) == "AUDIO_CD";
480
+ drawPlay();
481
+
482
+ } else if (msg.startsWith("PLAY_STATUS:")) {
483
+ line1 = msg.substring(12);
484
+ if (line1.length() > 20) line1 = line1.substring(0, 20);
485
+ if (line1 == "PLAYING" || line1 == "PAUSED" || (audioPlayMode && line1.startsWith("TRACK "))) {
486
+ playStatusTemp = false;
487
+ } else {
488
+ playStatusAt = millis();
489
+ playStatusTemp = true;
490
+ }
491
+ drawPlay();
492
+
493
+ } else if (msg.startsWith("WAITING:")) {
494
+ returnToHomeAt = 0;
495
+ String rest = msg.substring(8);
496
+ int slash = rest.indexOf('/');
497
+ if (slash >= 0) {
498
+ waitingLine1 = "Data: " + rest.substring(0, slash);
499
+ waitingLine1.trim();
500
+ waitingLine2 = "Disc: " + rest.substring(slash + 1);
501
+ waitingLine2.trim();
502
+ } else {
503
+ waitingLine1 = rest;
504
+ waitingLine2 = "";
505
+ }
506
+ drawWaiting();
507
+
508
+ } else if (msg.startsWith("STATUS:")) {
509
+ returnToHomeAt = 0;
510
+ line1 = msg.substring(7);
511
+ line2 = "";
512
+ line3 = "";
513
+ progressPercent = -1;
514
+ drawStatus();
515
+
516
+ } else if (msg.startsWith("PROGRESS:")) {
517
+ returnToHomeAt = 0;
518
+ line2 = msg.substring(9);
519
+ {
520
+ String rest = msg.substring(9);
521
+ int pct = rest.indexOf('%');
522
+ if (pct > 0) {
523
+ String num = rest.substring(0, pct);
524
+ num.trim();
525
+ int val = num.toInt();
526
+ if (val >= 0 && val <= 100) {
527
+ progressPercent = val;
528
+ }
529
+ }
530
+ }
531
+ drawStatus();
532
+
533
+ } else if (msg.startsWith("INFO:")) {
534
+ returnToHomeAt = 0;
535
+ line3 = msg.substring(5);
536
+ drawStatus();
537
+
538
+ } else if (msg.startsWith("DONE:")) {
539
+ line1 = "DONE!";
540
+ line2 = msg.substring(5);
541
+ line3 = "Returning home";
542
+ returnToHomeAt = millis() + DONE_RESET_MS;
543
+ progressPercent = -1;
544
+ drawStatus();
545
+
546
+ } else if (msg.startsWith("CANCELLED:")) {
547
+ returnToHomeAt = 0;
548
+ line1 = "CANCELLED";
549
+ line2 = msg.substring(10);
550
+ line3 = "Use menu";
551
+ progressPercent = -1;
552
+ drawStatus();
553
+
554
+ } else if (msg.startsWith("WARNING:")) {
555
+ returnToHomeAt = 0;
556
+ line1 = "!! WARNING !!";
557
+ line2 = msg.substring(8);
558
+ if (line2.length() > 20) line2 = line2.substring(0, 20);
559
+ line3 = "";
560
+ drawStatus();
561
+
562
+ } else if (msg.startsWith("ERROR:")) {
563
+ returnToHomeAt = 0;
564
+ line1 = "!! ERROR";
565
+ line2 = msg.substring(6);
566
+ line3 = "";
567
+ progressPercent = -1;
568
+ drawStatus();
569
+
570
+ } else if (msg.startsWith("IP:")) {
571
+ ipUrl = msg.substring(3);
572
+ drawIP();
573
+
574
+ } else if (msg.startsWith("LOADING:")) {
575
+ loadingLine = msg.substring(8);
576
+ loadingDots = 0;
577
+ lastLoadingAnim = millis();
578
+ drawLoading();
579
+
580
+ } else if (msg.startsWith("NO_DISC:")) {
581
+ line1 = msg.substring(8);
582
+ line2 = "";
583
+ line3 = "";
584
+ progressPercent = -1;
585
+ drawStatus();
586
+ }
587
+ }
588
+
589
+ void setup() {
590
+ Serial.begin(115200);
591
+ Wire.begin(22, 23);
592
+
593
+ pinMode(3, OUTPUT);
594
+ digitalWrite(3, LOW);
595
+ pinMode(14, OUTPUT);
596
+ digitalWrite(14, LOW);
597
+
598
+ pinMode(BTN_SELECT_PIN, INPUT_PULLUP);
599
+ pinMode(BTN_UP_PIN, INPUT_PULLUP);
600
+ pinMode(BTN_DOWN_PIN, INPUT_PULLUP);
601
+ pinMode(LED_POWER_PIN, OUTPUT);
602
+ pinMode(LED_ACT_PIN, OUTPUT);
603
+ digitalWrite(LED_POWER_PIN, HIGH);
604
+ digitalWrite(LED_ACT_PIN, LOW);
605
+ pinMode(POT_PIN, INPUT);
606
+ analogReadResolution(12);
607
+
608
+ homeIndex = readBucket(homeCount);
609
+ burnModeIndex = readBucket(BURN_COUNT);
610
+ playVolume = readPercent();
611
+
612
+ displayOk = display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS);
613
+
614
+ if (displayOk) {
615
+ Serial.println("Display OK");
616
+ } else {
617
+ Serial.println("Display FAILED");
618
+ delay(3000);
619
+ }
620
+
621
+ for (int i = 0; i < 3; i++) {
622
+ digitalWrite(LED_ACT_PIN, HIGH);
623
+ delay(150);
624
+ digitalWrite(LED_ACT_PIN, LOW);
625
+ delay(150);
626
+ }
627
+
628
+ drawStandby();
629
+ lastMsgTime = millis();
630
+ Serial.println("DISCSTATION_READY");
631
+ }
632
+
633
+ void handleSelectPress(bool longPress) {
634
+ wakeDisplay();
635
+ if (uiState == UI_HOME) {
636
+ if (longPress) {
637
+ Serial.println("EJECT");
638
+ line1 = "Ejecting...";
639
+ line2 = "";
640
+ line3 = "";
641
+ returnToHomeAt = millis() + 5000;
642
+ drawStatus();
643
+ } else if (homeIndex >= 0 && homeIndex < homeCount) {
644
+ Serial.print("SELECT:");
645
+ Serial.println(homeModes[homeIndex]);
646
+ line1 = "Selected";
647
+ line2 = homeModes[homeIndex];
648
+ line3 = "";
649
+ drawStatus();
650
+ }
651
+
652
+ } else if (uiState == UI_STANDBY) {
653
+ Serial.println("EJECT");
654
+ line1 = "Ejecting...";
655
+ line2 = "";
656
+ line3 = "";
657
+ returnToHomeAt = millis() + 5000;
658
+ drawStatus();
659
+
660
+ } else if (uiState == UI_STATUS && line1 == "Ejecting...") {
661
+ drawStandby();
662
+
663
+ } else if (uiState == UI_IP) {
664
+ if (longPress) {
665
+ Serial.println("CANCEL");
666
+ drawHome();
667
+ }
668
+
669
+ } else if (uiState == UI_WAITING) {
670
+ if (longPress) {
671
+ Serial.println("CANCEL");
672
+ drawHome();
673
+ } else {
674
+ Serial.println("CONFIRM");
675
+ line1 = "Confirmed!";
676
+ line2 = "";
677
+ line3 = "";
678
+ drawStatus();
679
+ }
680
+
681
+ } else if (uiState == UI_BURN_READY) {
682
+ if (longPress) {
683
+ Serial.print("START:");
684
+ Serial.println(BURN_MODES[burnModeIndex]);
685
+ line1 = "Starting...";
686
+ line2 = BURN_MODES[burnModeIndex];
687
+ line3 = "";
688
+ drawStatus();
689
+ } else {
690
+ editingSpeed = !editingSpeed;
691
+ drawBurnReady();
692
+ }
693
+
694
+ } else if (uiState == UI_STATUS) {
695
+ if (longPress) {
696
+ Serial.println("CANCEL");
697
+ line1 = "Cancelling...";
698
+ line2 = "";
699
+ line3 = "";
700
+ drawStatus();
701
+ }
702
+
703
+ } else if (uiState == UI_DISCONNECTED) {
704
+ Serial.println("CANCEL");
705
+ drawStandby();
706
+
707
+ } else if (uiState == UI_PLAY) {
708
+ ffSpeedIndex = 0;
709
+ rewSpeedIndex = 0;
710
+ if (longPress) {
711
+ Serial.println("PLAY_STOP");
712
+ } else {
713
+ Serial.println("PLAY_BUTTON");
714
+ }
715
+ }
716
+ }
717
+
718
+ void handleUp(bool longPress) {
719
+ wakeDisplay();
720
+ if (uiState == UI_HOME) {
721
+ if (homeCount > 0) {
722
+ homeIndex = (homeIndex - 1 + homeCount) % homeCount;
723
+ sendHomeMode();
724
+ drawHome();
725
+ }
726
+ } else if (uiState == UI_BURN_READY) {
727
+ if (editingSpeed) {
728
+ burnSpeedIndex = (burnSpeedIndex - 1 + SPEED_COUNT) % SPEED_COUNT;
729
+ Serial.print("SPEED:");
730
+ Serial.println(SPEED_MODES[burnSpeedIndex]);
731
+ drawBurnReady();
732
+ } else {
733
+ burnModeIndex = (burnModeIndex - 1 + BURN_COUNT) % BURN_COUNT;
734
+ sendBurnMode();
735
+ drawBurnReady();
736
+ }
737
+ } else if (uiState == UI_PLAY) {
738
+ if (audioPlayMode) {
739
+ Serial.println(displayRotation == 0 ? (longPress ? "REW:BIG" : "REW:10") : (longPress ? "FF:BIG" : "FF:10"));
740
+ return;
741
+ }
742
+ rewSpeedIndex = 0;
743
+ if (longPress) {
744
+ Serial.println("FF:BIG");
745
+ } else {
746
+ ffSpeedIndex = (ffSpeedIndex + 1) % 4;
747
+ int seek_sec[] = {10, 20, 30, 60};
748
+ Serial.print("FF:");
749
+ Serial.println(seek_sec[ffSpeedIndex]);
750
+ }
751
+ }
752
+ }
753
+
754
+ void handleDown(bool longPress) {
755
+ wakeDisplay();
756
+ if (uiState == UI_HOME) {
757
+ if (homeCount > 0) {
758
+ homeIndex = (homeIndex + 1) % homeCount;
759
+ sendHomeMode();
760
+ drawHome();
761
+ }
762
+ } else if (uiState == UI_BURN_READY) {
763
+ if (editingSpeed) {
764
+ burnSpeedIndex = (burnSpeedIndex + 1) % SPEED_COUNT;
765
+ Serial.print("SPEED:");
766
+ Serial.println(SPEED_MODES[burnSpeedIndex]);
767
+ drawBurnReady();
768
+ } else {
769
+ burnModeIndex = (burnModeIndex + 1) % BURN_COUNT;
770
+ sendBurnMode();
771
+ drawBurnReady();
772
+ }
773
+ } else if (uiState == UI_PLAY) {
774
+ if (audioPlayMode) {
775
+ Serial.println(displayRotation == 0 ? (longPress ? "FF:BIG" : "FF:10") : (longPress ? "REW:BIG" : "REW:10"));
776
+ return;
777
+ }
778
+ ffSpeedIndex = 0;
779
+ if (longPress) {
780
+ Serial.println("REW:BIG");
781
+ } else {
782
+ rewSpeedIndex = (rewSpeedIndex + 1) % 4;
783
+ int seek_sec[] = {10, 20, 30, 60};
784
+ Serial.print("REW:");
785
+ Serial.println(seek_sec[rewSpeedIndex]);
786
+ }
787
+ }
788
+ }
789
+
790
+ void loop() {
791
+ if (Serial.available()) {
792
+ String msg = Serial.readStringUntil('\n');
793
+ parseMessage(msg);
794
+ }
795
+
796
+ if (returnToHomeAt != 0 && (long)(millis() - returnToHomeAt) >= 0) {
797
+ drawHome();
798
+ }
799
+
800
+ if (playStatusTemp && (long)(millis() - playStatusAt) >= 2500) {
801
+ playStatusTemp = false;
802
+ line1 = "PLAYING";
803
+ drawPlay();
804
+ }
805
+
806
+ if (uiState == UI_PLAY && millis() - lastPotRead > POT_READ_MS) {
807
+ lastPotRead = millis();
808
+ int nextVolume = readPercent();
809
+ if (abs(nextVolume - playVolume) >= 3) {
810
+ playVolume = nextVolume;
811
+ Serial.print("POT:");
812
+ Serial.println(playVolume);
813
+ drawPlay();
814
+ }
815
+ }
816
+
817
+ {
818
+ bool sel = digitalRead(BTN_SELECT_PIN) == LOW;
819
+ if (sel && !selectDown && millis() - selectLastDebounce > DEBOUNCE_MS) {
820
+ selectDown = true;
821
+ selectDownAt = millis();
822
+ }
823
+ if (!sel && selectDown) {
824
+ selectDown = false;
825
+ selectLastDebounce = millis();
826
+ handleSelectPress(millis() - selectDownAt >= LONG_PRESS_MS);
827
+ }
828
+ }
829
+
830
+ {
831
+ bool up = digitalRead(BTN_UP_PIN) == LOW;
832
+ if (up && !upDown && millis() - upLastDebounce > DEBOUNCE_MS) {
833
+ upDown = true;
834
+ upDownAt = millis();
835
+ }
836
+ if (!up && upDown) {
837
+ upDown = false;
838
+ upLastDebounce = millis();
839
+ bool lp = millis() - upDownAt >= LONG_PRESS_MS;
840
+ if (uiState == UI_PLAY || !lp) {
841
+ handleUp(lp);
842
+ }
843
+ }
844
+ }
845
+
846
+ {
847
+ bool dn = digitalRead(BTN_DOWN_PIN) == LOW;
848
+ if (dn && !downDown && millis() - downLastDebounce > DEBOUNCE_MS) {
849
+ downDown = true;
850
+ downDownAt = millis();
851
+ }
852
+ if (!dn && downDown) {
853
+ downDown = false;
854
+ downLastDebounce = millis();
855
+ bool lp = millis() - downDownAt >= LONG_PRESS_MS;
856
+ if (uiState == UI_PLAY || !lp) {
857
+ handleDown(lp);
858
+ }
859
+ }
860
+ }
861
+
862
+ if (uiState == UI_STATUS && progressPercent < 0 &&
863
+ millis() - lastIndeterminateAnim > 500) {
864
+ lastIndeterminateAnim = millis();
865
+ indeterminateCount = (indeterminateCount % 4) + 1;
866
+ if (!displayBlank) drawStatus();
867
+ }
868
+
869
+ if (uiState == UI_LOADING && millis() - lastLoadingAnim > 400) {
870
+ lastLoadingAnim = millis();
871
+ loadingDots = (loadingDots % 4) + 1;
872
+ if (!displayBlank) drawLoading();
873
+ }
874
+
875
+ if (displayOk && uiState == UI_STANDBY && !displayBlank &&
876
+ (long)(millis() - standbyStartTime) >= STANDBY_BLANK_MS) {
877
+ display.ssd1306_command(0xAE);
878
+ displayBlank = true;
879
+ digitalWrite(LED_POWER_PIN, LOW);
880
+ }
881
+ if (!displayBlank) {
882
+ digitalWrite(LED_POWER_PIN, HIGH);
883
+ }
884
+
885
+ if (uiState != UI_DISCONNECTED &&
886
+ (long)(millis() - lastMsgTime) >= PING_TIMEOUT_MS) {
887
+ drawDisconnected();
888
+ }
889
+
890
+ {
891
+ bool ledOn = false;
892
+ if (uiState == UI_BURN_READY || uiState == UI_PLAY || uiState == UI_WAITING || uiState == UI_IP) {
893
+ ledOn = true;
894
+ } else if (uiState == UI_STATUS) {
895
+ if (progressPercent >= 0) {
896
+ ledOn = (millis() - lastLedBlink > LED_BLINK_MS);
897
+ if (ledOn) {
898
+ lastLedBlink = millis();
899
+ actLedState = !actLedState;
900
+ }
901
+ ledOn = actLedState;
902
+ } else {
903
+ ledOn = true;
904
+ }
905
+ } else if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_LOADING) {
906
+ ledOn = false;
907
+ } else if (uiState == UI_DISCONNECTED) {
908
+ ledOn = true;
909
+ }
910
+ digitalWrite(LED_ACT_PIN, ledOn ? HIGH : LOW);
911
+ }
912
+
913
+ delay(20);
914
+ }