discstation 0.1.28 → 0.1.30

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.
@@ -1,1134 +0,0 @@
1
- #include <Wire.h>
2
- #include <Adafruit_GFX.h>
3
- #include <Adafruit_SSD1306.h>
4
- #include <WiFi.h>
5
- #include "esp_mac.h"
6
- #include <WiFiManager.h>
7
- #include <ESPmDNS.h>
8
- #include <Preferences.h>
9
- #include <ArduinoOTA.h>
10
- #include <QRCode.h>
11
-
12
- // Optional build-time Wi-Fi override for power users: copy secrets.h.example
13
- // to secrets.h (git-ignored) and set WIFI_SSID / WIFI_PASS / OTA_PASSWORD.
14
- #if __has_include("secrets.h")
15
- #include "secrets.h"
16
- #endif
17
-
18
- #define SCREEN_WIDTH 128
19
- #define SCREEN_HEIGHT 64
20
- #define OLED_RESET -1
21
- #define I2C_ADDRESS 0x3C
22
- #define TCP_PORT 2323
23
-
24
- #define BTN_SELECT_PIN 21 // D3
25
- #define BTN_UP_PIN 20 // D9/MISO
26
- #define BTN_DOWN_PIN 18 // D10/MOSI
27
- #define POT_PIN 0 // D0/A0 (ADC)
28
-
29
- #define DEBOUNCE_MS 50
30
- #define LONG_PRESS_MS 1000
31
- #define POT_READ_MS 200
32
- #define DONE_RESET_MS 30000
33
- #define STANDBY_BLANK_MS 60000
34
- #define IDLE_BLANK_MS 45000 // blank the OLED after this long with no input on HOME/STANDBY
35
- #define PING_TIMEOUT_MS 30000
36
-
37
- #define WIFI_RESET_HOLD_MS 10000
38
- #define WIFI_CONNECT_TIMEOUT_MS 18000
39
- #define WIFI_RETRY_MS 15000
40
-
41
- #define MAX_HOME_MODES 5
42
- #define BURN_COUNT 4
43
- #define SPEED_COUNT 4
44
-
45
- int homeCount = 3;
46
- String homeModes[MAX_HOME_MODES] = {"BURN", "PLAY", "RIP"};
47
- String discName = "";
48
-
49
- // --- Wi-Fi link: mirror every outbound line to Serial AND a TCP client ---
50
- WiFiServer tcpServer(TCP_PORT);
51
- WiFiClient tcpClient;
52
- WiFiManager wm;
53
- Preferences prefs;
54
- bool wifiConnected = false;
55
- bool wifiInitDone = false;
56
- bool portalActive = false;
57
- bool otaEnabled = false;
58
- bool credsJustSaved = false;
59
- bool wifiResetArmed = false;
60
- unsigned long wifiDropAt = 0;
61
- String apName = "DiscStation";
62
- String mdnsHost = "discstation";
63
-
64
- class DualPrint : public Print {
65
- public:
66
- void setClient(WiFiClient* c) { _client = c; }
67
- size_t write(uint8_t c) override {
68
- Serial.write(c);
69
- if (_client && _client->connected()) _client->write(c);
70
- return 1;
71
- }
72
- size_t write(const uint8_t* buf, size_t len) override {
73
- Serial.write(buf, len);
74
- if (_client && _client->connected()) _client->write(buf, len);
75
- return len;
76
- }
77
- private:
78
- WiFiClient* _client = nullptr;
79
- } Out;
80
-
81
- void drawSetup();
82
- void drawWifiReset();
83
-
84
- String deviceSuffix() {
85
- uint8_t mac[6];
86
- esp_read_mac(mac, ESP_MAC_WIFI_STA); // factory MAC from eFuse - valid before the Wi-Fi driver starts
87
- char buf[5];
88
- snprintf(buf, sizeof(buf), "%02X%02X", mac[4], mac[5]);
89
- return String(buf);
90
- }
91
-
92
- void loadCreds(String& ssid, String& pass) {
93
- prefs.begin("discstation", true);
94
- ssid = prefs.getString("ssid", "");
95
- pass = prefs.getString("pass", "");
96
- prefs.end();
97
- }
98
-
99
- void saveCreds(const String& ssid, const String& pass) {
100
- prefs.begin("discstation", false);
101
- prefs.putString("ssid", ssid);
102
- prefs.putString("pass", pass);
103
- prefs.end();
104
- }
105
-
106
- void clearCreds() {
107
- prefs.begin("discstation", false);
108
- prefs.clear();
109
- prefs.end();
110
- WiFi.disconnect(true, true);
111
- }
112
-
113
- void wmSaveCallback() {
114
- saveCreds(wm.getWiFiSSID(), wm.getWiFiPass());
115
- credsJustSaved = true;
116
- }
117
-
118
- void onWifiUp() {
119
- portalActive = false;
120
- wifiConnected = true;
121
- wifiDropAt = 0;
122
- WiFi.setSleep(WIFI_PS_MIN_MODEM);
123
- tcpServer.begin();
124
- if (MDNS.begin(mdnsHost.c_str())) {
125
- MDNS.addService("discstation", "tcp", TCP_PORT);
126
- }
127
- #ifdef OTA_PASSWORD
128
- ArduinoOTA.setHostname(mdnsHost.c_str());
129
- ArduinoOTA.setPassword(OTA_PASSWORD);
130
- ArduinoOTA.begin();
131
- otaEnabled = true;
132
- #endif
133
- Out.print("WiFi OK "); Out.println(WiFi.localIP());
134
- }
135
-
136
- void startPortal() {
137
- Out.print("WiFi: setup portal '"); Out.print(apName); Out.println("'");
138
- portalActive = true;
139
- WiFi.mode(WIFI_AP_STA);
140
- wm.setConfigPortalBlocking(false);
141
- wm.setConfigPortalTimeout(0);
142
- wm.setSaveConfigCallback(wmSaveCallback);
143
- wm.startConfigPortal(apName.c_str());
144
- drawSetup();
145
- }
146
-
147
- void initWiFi() {
148
- apName = "DiscStation-" + deviceSuffix();
149
- mdnsHost = "discstation-" + deviceSuffix();
150
- mdnsHost.toLowerCase();
151
-
152
- WiFi.mode(WIFI_STA);
153
- WiFi.setAutoReconnect(true);
154
- WiFi.setSleep(WIFI_PS_MIN_MODEM);
155
-
156
- String ssid, pass;
157
- #ifdef WIFI_SSID
158
- ssid = WIFI_SSID;
159
- pass = WIFI_PASS;
160
- #else
161
- loadCreds(ssid, pass);
162
- #endif
163
-
164
- if (ssid.length() > 0) {
165
- Out.print("WiFi "); Out.print(ssid); Out.print("...");
166
- WiFi.begin(ssid.c_str(), pass.c_str());
167
- unsigned long t0 = millis();
168
- while (WiFi.status() != WL_CONNECTED && millis() - t0 < WIFI_CONNECT_TIMEOUT_MS) delay(200);
169
- }
170
-
171
- if (WiFi.status() == WL_CONNECTED) {
172
- Out.println(" OK");
173
- onWifiUp();
174
- } else {
175
- if (ssid.length() > 0) Out.println(" fail (2.4GHz band / wrong password?)");
176
- startPortal();
177
- }
178
- }
179
-
180
- Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
181
-
182
- String ipUrl = "";
183
-
184
- enum UiState {
185
- UI_STATUS,
186
- UI_HOME,
187
- UI_BURN_READY,
188
- UI_PLAY,
189
- UI_STANDBY,
190
- UI_WAITING,
191
- UI_IP,
192
- UI_DISCONNECTED,
193
- UI_LOADING,
194
- UI_SETUP
195
- };
196
-
197
- const char* BURN_MODES[BURN_COUNT] = {"AUTO", "BEST", "LONG", "TEST"};
198
- const char* SPEED_MODES[SPEED_COUNT] = {"Auto", "6x", "4x", "3x"};
199
-
200
- UiState uiState = UI_STATUS;
201
-
202
- String titleLine = "";
203
- String metaLine = "";
204
- String fitLine = "";
205
- String discLine = "Disc: none";
206
- String line1 = "Status: READY";
207
- String line2 = "";
208
- String line3 = "";
209
- String waitingLine1 = "";
210
- String waitingLine2 = "";
211
- String loadingLine = "";
212
- int loadingDots = 0;
213
- unsigned long lastLoadingAnim = 0;
214
- int progressPercent = -1;
215
-
216
- bool displayOk = false;
217
- unsigned long returnToHomeAt = 0;
218
- unsigned long playStatusAt = 0;
219
- bool playStatusTemp = false;
220
- unsigned long lastPotRead = 0;
221
-
222
- unsigned long selectDownAt = 0;
223
- bool selectDown = false;
224
- unsigned long selectLastDebounce = 0;
225
-
226
- unsigned long upDownAt = 0;
227
- bool upDown = false;
228
- unsigned long upLastDebounce = 0;
229
-
230
- unsigned long downDownAt = 0;
231
- bool downDown = false;
232
- unsigned long downLastDebounce = 0;
233
-
234
- int ffSpeedIndex = 0;
235
- int rewSpeedIndex = 0;
236
-
237
- int homeIndex = 0;
238
- int burnModeIndex = 0;
239
- int burnSpeedIndex = 0;
240
- bool editingSpeed = false;
241
- int playVolume = 50;
242
- unsigned long standbyStartTime = 0;
243
- unsigned long lastMsgTime = 0;
244
- unsigned long lastInputTime = 0; // last button press / screen change; drives OLED idle-blank
245
- bool displayBlank = false;
246
- bool audioPlayMode = false;
247
- int displayRotation = 0;
248
-
249
- int indeterminateCount = 0;
250
- unsigned long lastIndeterminateAnim = 0;
251
-
252
- int readBucket(int count) {
253
- int raw = analogRead(POT_PIN);
254
- int idx = raw * count / 4096;
255
- if (idx < 0) idx = 0;
256
- if (idx >= count) idx = count - 1;
257
- return idx;
258
- }
259
-
260
- int readPercent() {
261
- int raw = analogRead(POT_PIN);
262
- int pct = raw * 100 / 4095;
263
- if (pct < 0) pct = 0;
264
- if (pct > 100) pct = 100;
265
- return ((pct + 2) / 5) * 5;
266
- }
267
-
268
- void sendHomeMode() {
269
- if (homeIndex >= 0 && homeIndex < homeCount) {
270
- Out.print("MENU:");
271
- Out.println(homeModes[homeIndex]);
272
- }
273
- }
274
-
275
- void sendBurnMode() {
276
- Out.print("MODE:");
277
- Out.println(BURN_MODES[burnModeIndex]);
278
- }
279
-
280
- void printUpper(String value) {
281
- value.toUpperCase();
282
- display.print(value);
283
- }
284
-
285
- void drawChrome() {
286
- display.setRotation(displayRotation);
287
- display.setTextSize(1);
288
- display.setTextColor(SSD1306_WHITE);
289
- display.drawLine(0, 0, 4, 0, SSD1306_WHITE);
290
- display.drawLine(0, 0, 0, 4, SSD1306_WHITE);
291
- display.drawLine(123, 0, 127, 0, SSD1306_WHITE);
292
- display.drawLine(127, 0, 127, 4, SSD1306_WHITE);
293
- display.drawLine(0, 63, 4, 63, SSD1306_WHITE);
294
- display.drawLine(0, 59, 0, 63, SSD1306_WHITE);
295
- display.drawLine(123, 63, 127, 63, SSD1306_WHITE);
296
- display.drawLine(127, 59, 127, 63, SSD1306_WHITE);
297
- }
298
-
299
- void drawHeader() {
300
- display.setTextSize(1);
301
- display.setTextColor(SSD1306_WHITE);
302
- drawChrome();
303
- display.setCursor(4, 0);
304
- display.print("DISCSTATION // PANEL");
305
- display.drawLine(6, 10, 121, 10, SSD1306_WHITE);
306
- }
307
-
308
- void drawHome() {
309
- uiState = UI_HOME;
310
- returnToHomeAt = 0;
311
- lastInputTime = millis();
312
- if (!displayOk) return;
313
-
314
- display.clearDisplay();
315
- drawHeader();
316
-
317
- int y = 14;
318
- if (discName.length() > 0) {
319
- display.setTextSize(1);
320
- display.setCursor(2, y);
321
- printUpper(discName);
322
- y += 10;
323
- } else {
324
- display.setCursor(2, y);
325
- printUpper(discLine);
326
- y += 10;
327
- }
328
-
329
- for (int i = 0; i < homeCount; i++) {
330
- display.setCursor(2, y);
331
- display.print(i == homeIndex ? ">" : " ");
332
- display.setCursor(14, y);
333
- printUpper(homeModes[i]);
334
- y += 10;
335
- }
336
-
337
- display.display();
338
- }
339
-
340
- void drawBurnReady() {
341
- uiState = UI_BURN_READY;
342
- returnToHomeAt = 0;
343
- if (!displayOk) return;
344
-
345
- display.clearDisplay();
346
- drawHeader();
347
-
348
- display.setCursor(2, 14);
349
- printUpper(titleLine);
350
-
351
- display.setCursor(2, 25);
352
- printUpper(metaLine);
353
-
354
- display.setCursor(2, 37);
355
- printUpper(fitLine);
356
-
357
- display.setCursor(2, 49);
358
- display.print(editingSpeed ? ' ' : '>');
359
- display.setCursor(14, 49);
360
- display.print(BURN_MODES[burnModeIndex]);
361
-
362
- display.setCursor(62, 49);
363
- display.print(editingSpeed ? '>' : ' ');
364
- display.setCursor(74, 49);
365
- display.print(SPEED_MODES[burnSpeedIndex]);
366
-
367
- display.setCursor(2, 57);
368
- display.print("HOLD SELECT // GO");
369
-
370
- display.display();
371
- }
372
-
373
- void drawWaiting() {
374
- uiState = UI_WAITING;
375
- returnToHomeAt = 0;
376
- if (!displayOk) return;
377
-
378
- display.clearDisplay();
379
- drawHeader();
380
-
381
- display.setCursor(2, 16);
382
- printUpper(waitingLine1);
383
-
384
- display.setCursor(2, 30);
385
- printUpper(waitingLine2);
386
-
387
- display.setCursor(2, 49);
388
- display.print("SHORT // GO");
389
-
390
- display.display();
391
- }
392
-
393
- void drawStatus() {
394
- uiState = UI_STATUS;
395
- if (!displayOk) return;
396
-
397
- display.clearDisplay();
398
- drawHeader();
399
-
400
- display.setCursor(2, 16);
401
- printUpper(line1);
402
- display.setCursor(2, 30);
403
- printUpper(line2);
404
- display.setCursor(2, 44);
405
- printUpper(line3);
406
-
407
- if (progressPercent >= 0) {
408
- int barW = map(progressPercent, 0, 100, 0, 124);
409
- display.drawRect(2, 54, 124, 8, SSD1306_WHITE);
410
- if (barW > 0) {
411
- display.fillRect(2, 54, barW, 8, SSD1306_WHITE);
412
- }
413
- } else {
414
- String dots = "";
415
- for (int i = 0; i < indeterminateCount; i++) dots += ".";
416
- display.setCursor(2, 56);
417
- display.print(dots);
418
- }
419
-
420
- display.display();
421
- }
422
-
423
- void drawQrCode(String value) {
424
- drawChrome();
425
- QRCode qr;
426
- uint8_t qrData[qrcode_getBufferSize(2)];
427
- if (qrcode_initText(&qr, qrData, 2, ECC_LOW, value.c_str()) < 0) {
428
- display.setCursor(2, 25);
429
- display.print("QR ERROR // USE URL");
430
- return;
431
- }
432
- const int scale = 2;
433
- const int quiet = 2;
434
- const int pixels = qr.size * scale;
435
- const int left = (SCREEN_WIDTH - pixels) / 2;
436
- const int top = 2;
437
- display.fillRect(left - quiet, top - quiet, pixels + quiet * 2, pixels + quiet * 2, SSD1306_WHITE);
438
- for (uint8_t y = 0; y < qr.size; y++) {
439
- for (uint8_t x = 0; x < qr.size; x++) {
440
- if (qrcode_getModule(&qr, x, y)) {
441
- display.fillRect(left + x * scale, top + y * scale, scale, scale, SSD1306_BLACK);
442
- }
443
- }
444
- }
445
- display.setCursor(2, 57);
446
- display.print("SCAN // DISCSTATION");
447
- }
448
-
449
- void drawIP() {
450
- uiState = UI_IP;
451
- returnToHomeAt = 0;
452
- if (!displayOk) return;
453
-
454
- display.clearDisplay();
455
- drawQrCode(ipUrl);
456
-
457
- display.display();
458
- }
459
-
460
- void drawStandby() {
461
- if (portalActive) { drawSetup(); return; }
462
- uiState = UI_STANDBY;
463
- returnToHomeAt = 0;
464
- standbyStartTime = millis();
465
- lastInputTime = millis();
466
- displayBlank = false;
467
- if (!displayOk) return;
468
-
469
- display.clearDisplay();
470
- drawHeader();
471
- display.setCursor(2, 25);
472
- display.print("STANDBY // READY");
473
- display.setCursor(2, 40);
474
- display.print("INSERT MEDIA");
475
- display.display();
476
- }
477
-
478
- void drawSetup() {
479
- uiState = UI_SETUP;
480
- returnToHomeAt = 0;
481
- displayBlank = false;
482
- lastInputTime = millis();
483
- if (!displayOk) return;
484
- display.clearDisplay();
485
- drawHeader();
486
- display.setCursor(2, 16);
487
- display.print("WIFI SETUP");
488
- display.setCursor(2, 28);
489
- display.print("JOIN:");
490
- display.setCursor(2, 38);
491
- display.print(apName);
492
- display.setCursor(2, 50);
493
- display.print("THEN 192.168.4.1");
494
- display.display();
495
- }
496
-
497
- void drawWifiReset() {
498
- uiState = UI_SETUP;
499
- displayBlank = false;
500
- if (!displayOk) return;
501
- display.clearDisplay();
502
- drawHeader();
503
- display.setCursor(2, 25);
504
- display.print("WIFI RESET");
505
- display.setCursor(2, 40);
506
- display.print("REBOOTING...");
507
- display.display();
508
- }
509
-
510
- void drawDisconnected() {
511
- uiState = UI_DISCONNECTED;
512
- returnToHomeAt = 0;
513
- displayBlank = false;
514
- if (!displayOk) return;
515
-
516
- display.clearDisplay();
517
- drawHeader();
518
- display.setCursor(2, 25);
519
- display.print("DISCONNECTED // LINK");
520
- display.setCursor(2, 40);
521
- display.print("CHECK USB");
522
- display.display();
523
- }
524
-
525
- void drawLoading() {
526
- uiState = UI_LOADING;
527
- returnToHomeAt = 0;
528
- displayBlank = false;
529
- if (!displayOk) return;
530
-
531
- display.clearDisplay();
532
- drawHeader();
533
-
534
- display.setTextSize(1);
535
- display.setCursor(2, 18);
536
- printUpper(loadingLine);
537
-
538
- String dots = "";
539
- for (int i = 0; i < loadingDots; i++) dots += ".";
540
- display.setCursor(2, 32);
541
- display.print(dots);
542
- display.setCursor(70, 32);
543
- display.print("LOADING //");
544
-
545
- display.display();
546
- }
547
-
548
- // Returns true if this call actually woke a blanked screen - callers use that
549
- // to swallow the wake press so it doesn't also trigger a menu action.
550
- bool wakeDisplay() {
551
- if (!displayBlank || !displayOk) return false;
552
- display.ssd1306_command(0xAF);
553
- displayBlank = false;
554
- switch (uiState) {
555
- case UI_HOME: drawHome(); break;
556
- case UI_STATUS: drawStatus(); break;
557
- case UI_IP: drawIP(); break;
558
- case UI_BURN_READY: drawBurnReady(); break;
559
- case UI_PLAY: drawPlay(); break;
560
- case UI_WAITING: drawWaiting(); break;
561
- case UI_STANDBY: drawStandby(); break;
562
- case UI_DISCONNECTED: drawDisconnected(); break;
563
- case UI_LOADING: drawLoading(); break;
564
- case UI_SETUP: drawSetup(); break;
565
- }
566
- return true;
567
- }
568
-
569
- void drawPlay() {
570
- uiState = UI_PLAY;
571
- returnToHomeAt = 0;
572
- if (!displayOk) return;
573
-
574
- display.clearDisplay();
575
- drawHeader();
576
-
577
- display.setCursor(2, 16);
578
- printUpper(line1);
579
- display.setCursor(2, 30);
580
- printUpper(line2);
581
- display.setCursor(2, 44);
582
- display.print("VOL // ");
583
- display.print(playVolume);
584
- display.print("%");
585
- if (!audioPlayMode) {
586
- display.setCursor(2, 56);
587
- display.print("SELECT // PLAY");
588
- }
589
-
590
- display.display();
591
- }
592
-
593
- void parseMessage(String msg) {
594
- msg.trim();
595
-
596
- if (msg == "PING") {
597
- lastMsgTime = millis();
598
- Out.println("PONG");
599
- if (uiState == UI_DISCONNECTED) drawStandby();
600
- return;
601
- }
602
-
603
- wakeDisplay();
604
- lastMsgTime = millis();
605
-
606
- Out.print("RCV:");
607
- Out.println(msg);
608
-
609
- if (msg.startsWith("HOME:")) {
610
- homeIndex = readBucket(homeCount);
611
- drawHome();
612
-
613
- } else if (msg.startsWith("DISC:")) {
614
- discLine = msg.substring(5);
615
- if (discLine.length() > 20) discLine = discLine.substring(0, 20);
616
- if (uiState == UI_HOME) drawHome();
617
-
618
- } else if (msg.startsWith("DISC_NAME:")) {
619
- discName = msg.substring(10);
620
- if (discName.length() > 18) discName = discName.substring(0, 18);
621
- if (uiState == UI_HOME) drawHome();
622
-
623
- } else if (msg.startsWith("MENU_ITEMS:")) {
624
- String items = msg.substring(11);
625
- homeCount = 0;
626
- while (items.length() > 0 && homeCount < MAX_HOME_MODES) {
627
- int comma = items.indexOf(',');
628
- if (comma < 0) {
629
- homeModes[homeCount++] = items;
630
- break;
631
- }
632
- homeModes[homeCount++] = items.substring(0, comma);
633
- items = items.substring(comma + 1);
634
- }
635
- if (homeIndex >= homeCount) homeIndex = 0;
636
- if (uiState == UI_HOME) drawHome();
637
-
638
- } else if (msg.startsWith("STANDBY:")) {
639
- drawStandby();
640
-
641
- } else if (msg.startsWith("TITLE:")) {
642
- titleLine = msg.substring(6);
643
- if (titleLine.length() > 20) titleLine = titleLine.substring(0, 20);
644
- burnModeIndex = readBucket(BURN_COUNT);
645
- drawBurnReady();
646
-
647
- } else if (msg.startsWith("META:")) {
648
- metaLine = msg.substring(5);
649
- if (metaLine.length() > 20) metaLine = metaLine.substring(0, 20);
650
- drawBurnReady();
651
-
652
- } else if (msg.startsWith("FIT:")) {
653
- fitLine = msg.substring(4);
654
- if (fitLine.length() > 20) fitLine = fitLine.substring(0, 20);
655
- drawBurnReady();
656
-
657
- } else if (msg.startsWith("PLAY:")) {
658
- line1 = msg.substring(5);
659
- line2 = "Playing disc";
660
- playVolume = readPercent();
661
- Out.print("POT:"); // push the knob's current level so playback starts at it
662
- Out.println(playVolume);
663
- drawPlay();
664
-
665
- } else if (msg.startsWith("PLAY_MODE:")) {
666
- audioPlayMode = msg.substring(10) == "AUDIO_CD";
667
- drawPlay();
668
-
669
- } else if (msg.startsWith("PLAY_STATUS:")) {
670
- line1 = msg.substring(12);
671
- if (line1.length() > 20) line1 = line1.substring(0, 20);
672
- if (line1 == "PLAYING" || line1 == "PAUSED" || (audioPlayMode && line1.startsWith("TRACK "))) {
673
- playStatusTemp = false;
674
- } else {
675
- playStatusAt = millis();
676
- playStatusTemp = true;
677
- }
678
- drawPlay();
679
-
680
- } else if (msg.startsWith("WAITING:")) {
681
- returnToHomeAt = 0;
682
- String rest = msg.substring(8);
683
- int slash = rest.indexOf('/');
684
- if (slash >= 0) {
685
- waitingLine1 = "Data: " + rest.substring(0, slash);
686
- waitingLine1.trim();
687
- waitingLine2 = "Disc: " + rest.substring(slash + 1);
688
- waitingLine2.trim();
689
- } else {
690
- waitingLine1 = rest;
691
- waitingLine2 = "";
692
- }
693
- drawWaiting();
694
-
695
- } else if (msg.startsWith("STATUS:")) {
696
- returnToHomeAt = 0;
697
- line1 = msg.substring(7);
698
- line2 = "";
699
- line3 = "";
700
- // Don't reset progressPercent here - a STATUS: label change (e.g.
701
- // "Copying files..." -> "Converting...") is often immediately
702
- // followed by a fresh PROGRESS: for the same ongoing operation.
703
- // Wiping it every time flipped the bar to the indeterminate dots
704
- // and back on every single label update, flickering between the
705
- // two. DONE:/CANCELLED:/ERROR:/NO_DISC: still reset it below - those
706
- // really are the end of an operation.
707
- drawStatus();
708
-
709
- } else if (msg.startsWith("PROGRESS:")) {
710
- returnToHomeAt = 0;
711
- line2 = msg.substring(9);
712
- {
713
- String rest = msg.substring(9);
714
- int pct = rest.indexOf('%');
715
- if (pct > 0) {
716
- String num = rest.substring(0, pct);
717
- num.trim();
718
- int val = num.toInt();
719
- if (val >= 0 && val <= 100) {
720
- progressPercent = val;
721
- }
722
- }
723
- }
724
- drawStatus();
725
-
726
- } else if (msg.startsWith("INFO:")) {
727
- returnToHomeAt = 0;
728
- line3 = msg.substring(5);
729
- drawStatus();
730
-
731
- } else if (msg.startsWith("DONE:")) {
732
- line1 = "DONE!";
733
- line2 = msg.substring(5);
734
- line3 = "Returning home";
735
- returnToHomeAt = millis() + DONE_RESET_MS;
736
- progressPercent = -1;
737
- drawStatus();
738
-
739
- } else if (msg.startsWith("CANCELLED:")) {
740
- returnToHomeAt = 0;
741
- line1 = "CANCELLED";
742
- line2 = msg.substring(10);
743
- line3 = "Use menu";
744
- progressPercent = -1;
745
- drawStatus();
746
-
747
- } else if (msg.startsWith("WARNING:")) {
748
- returnToHomeAt = 0;
749
- line1 = "!! WARNING !!";
750
- line2 = msg.substring(8);
751
- if (line2.length() > 20) line2 = line2.substring(0, 20);
752
- line3 = "";
753
- drawStatus();
754
-
755
- } else if (msg.startsWith("ERROR:")) {
756
- returnToHomeAt = 0;
757
- line1 = "!! ERROR";
758
- line2 = msg.substring(6);
759
- line3 = "";
760
- progressPercent = -1;
761
- drawStatus();
762
-
763
- } else if (msg.startsWith("IP:")) {
764
- ipUrl = msg.substring(3);
765
- drawIP();
766
-
767
- } else if (msg.startsWith("LOADING:")) {
768
- loadingLine = msg.substring(8);
769
- loadingDots = 0;
770
- lastLoadingAnim = millis();
771
- drawLoading();
772
-
773
- } else if (msg.startsWith("NO_DISC:")) {
774
- line1 = msg.substring(8);
775
- line2 = "";
776
- line3 = "";
777
- progressPercent = -1;
778
- drawStatus();
779
- }
780
- }
781
-
782
- void setup() {
783
- setCpuFrequencyMhz(160); // 240 -> 160: ~halves CPU power; I2C/GPIO all fine at 160
784
- Serial.begin(115200);
785
- Wire.begin(22, 23);
786
-
787
- pinMode(3, OUTPUT);
788
- digitalWrite(3, LOW);
789
- pinMode(14, OUTPUT);
790
- digitalWrite(14, LOW);
791
-
792
- pinMode(BTN_SELECT_PIN, INPUT_PULLUP);
793
- pinMode(BTN_UP_PIN, INPUT_PULLUP);
794
- pinMode(BTN_DOWN_PIN, INPUT_PULLUP);
795
- pinMode(POT_PIN, INPUT);
796
- analogReadResolution(12);
797
-
798
- homeIndex = readBucket(homeCount);
799
- burnModeIndex = readBucket(BURN_COUNT);
800
- playVolume = readPercent();
801
-
802
- displayOk = display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS);
803
-
804
- if (displayOk) {
805
- Out.println("Display OK");
806
- } else {
807
- Out.println("Display FAILED");
808
- delay(3000);
809
- }
810
-
811
- drawStandby();
812
- lastMsgTime = millis();
813
- lastInputTime = millis();
814
- Out.println("DISCSTATION_READY");
815
- }
816
-
817
- void handleSelectPress(bool longPress) {
818
- if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
819
- lastInputTime = millis();
820
- if (uiState == UI_HOME) {
821
- if (longPress) {
822
- Out.println("EJECT");
823
- line1 = "Ejecting...";
824
- line2 = "";
825
- line3 = "";
826
- returnToHomeAt = millis() + 5000;
827
- drawStatus();
828
- } else if (homeIndex >= 0 && homeIndex < homeCount) {
829
- Out.print("SELECT:");
830
- Out.println(homeModes[homeIndex]);
831
- line1 = "Selected";
832
- line2 = homeModes[homeIndex];
833
- line3 = "";
834
- drawStatus();
835
- }
836
-
837
- } else if (uiState == UI_STANDBY) {
838
- Out.println("EJECT");
839
- line1 = "Ejecting...";
840
- line2 = "";
841
- line3 = "";
842
- returnToHomeAt = millis() + 5000;
843
- drawStatus();
844
-
845
- } else if (uiState == UI_STATUS && line1 == "Ejecting...") {
846
- drawStandby();
847
-
848
- } else if (uiState == UI_IP) {
849
- if (longPress) {
850
- Out.println("CANCEL");
851
- drawHome();
852
- }
853
-
854
- } else if (uiState == UI_WAITING) {
855
- if (longPress) {
856
- Out.println("CANCEL");
857
- drawHome();
858
- } else {
859
- Out.println("CONFIRM");
860
- line1 = "Confirmed!";
861
- line2 = "";
862
- line3 = "";
863
- drawStatus();
864
- }
865
-
866
- } else if (uiState == UI_BURN_READY) {
867
- if (longPress) {
868
- Out.print("START:");
869
- Out.println(BURN_MODES[burnModeIndex]);
870
- line1 = "Starting...";
871
- line2 = BURN_MODES[burnModeIndex];
872
- line3 = "";
873
- drawStatus();
874
- } else {
875
- editingSpeed = !editingSpeed;
876
- drawBurnReady();
877
- }
878
-
879
- } else if (uiState == UI_STATUS) {
880
- if (longPress) {
881
- Out.println("CANCEL");
882
- line1 = "Cancelling...";
883
- line2 = "";
884
- line3 = "";
885
- drawStatus();
886
- }
887
-
888
- } else if (uiState == UI_DISCONNECTED) {
889
- Out.println("CANCEL");
890
- drawStandby();
891
-
892
- } else if (uiState == UI_PLAY) {
893
- ffSpeedIndex = 0;
894
- rewSpeedIndex = 0;
895
- if (longPress) {
896
- Out.println("PLAY_STOP");
897
- } else {
898
- Out.println("PLAY_BUTTON");
899
- }
900
- }
901
- }
902
-
903
- void handleUp(bool longPress) {
904
- if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
905
- lastInputTime = millis();
906
- if (uiState == UI_HOME) {
907
- if (homeCount > 0) {
908
- homeIndex = (homeIndex - 1 + homeCount) % homeCount;
909
- sendHomeMode();
910
- drawHome();
911
- }
912
- } else if (uiState == UI_BURN_READY) {
913
- if (editingSpeed) {
914
- burnSpeedIndex = (burnSpeedIndex - 1 + SPEED_COUNT) % SPEED_COUNT;
915
- Out.print("SPEED:");
916
- Out.println(SPEED_MODES[burnSpeedIndex]);
917
- drawBurnReady();
918
- } else {
919
- burnModeIndex = (burnModeIndex - 1 + BURN_COUNT) % BURN_COUNT;
920
- sendBurnMode();
921
- drawBurnReady();
922
- }
923
- } else if (uiState == UI_PLAY) {
924
- if (audioPlayMode) {
925
- Out.println(displayRotation == 0 ? (longPress ? "REW:BIG" : "REW:10") : (longPress ? "FF:BIG" : "FF:10"));
926
- return;
927
- }
928
- rewSpeedIndex = 0;
929
- if (longPress) {
930
- Out.println("FF:BIG");
931
- } else {
932
- ffSpeedIndex = (ffSpeedIndex + 1) % 4;
933
- int seek_sec[] = {10, 20, 30, 60};
934
- Out.print("FF:");
935
- Out.println(seek_sec[ffSpeedIndex]);
936
- }
937
- }
938
- }
939
-
940
- void handleDown(bool longPress) {
941
- if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
942
- lastInputTime = millis();
943
- if (uiState == UI_HOME) {
944
- if (homeCount > 0) {
945
- homeIndex = (homeIndex + 1) % homeCount;
946
- sendHomeMode();
947
- drawHome();
948
- }
949
- } else if (uiState == UI_BURN_READY) {
950
- if (editingSpeed) {
951
- burnSpeedIndex = (burnSpeedIndex + 1) % SPEED_COUNT;
952
- Out.print("SPEED:");
953
- Out.println(SPEED_MODES[burnSpeedIndex]);
954
- drawBurnReady();
955
- } else {
956
- burnModeIndex = (burnModeIndex + 1) % BURN_COUNT;
957
- sendBurnMode();
958
- drawBurnReady();
959
- }
960
- } else if (uiState == UI_PLAY) {
961
- if (audioPlayMode) {
962
- Out.println(displayRotation == 0 ? (longPress ? "FF:BIG" : "FF:10") : (longPress ? "REW:BIG" : "REW:10"));
963
- return;
964
- }
965
- ffSpeedIndex = 0;
966
- if (longPress) {
967
- Out.println("REW:BIG");
968
- } else {
969
- rewSpeedIndex = (rewSpeedIndex + 1) % 4;
970
- int seek_sec[] = {10, 20, 30, 60};
971
- Out.print("REW:");
972
- Out.println(seek_sec[rewSpeedIndex]);
973
- }
974
- }
975
- }
976
-
977
- void loop() {
978
- if (!wifiInitDone && millis() > 3000) {
979
- wifiInitDone = true;
980
- initWiFi();
981
- }
982
-
983
- if (credsJustSaved) {
984
- drawWifiReset();
985
- delay(600);
986
- ESP.restart();
987
- }
988
-
989
- if (portalActive) {
990
- wm.process();
991
- if (WiFi.status() == WL_CONNECTED) onWifiUp();
992
- }
993
-
994
- if (wifiConnected && WiFi.status() != WL_CONNECTED) {
995
- if (wifiDropAt == 0) wifiDropAt = millis();
996
- else if (millis() - wifiDropAt > WIFI_RETRY_MS) {
997
- wifiDropAt = millis();
998
- WiFi.disconnect();
999
- WiFi.begin();
1000
- }
1001
- } else if (wifiConnected) {
1002
- wifiDropAt = 0;
1003
- }
1004
-
1005
- if (wifiConnected && WiFi.status() == WL_CONNECTED) {
1006
- if (otaEnabled) ArduinoOTA.handle();
1007
- if (tcpServer.hasClient()) {
1008
- if (tcpClient) tcpClient.stop();
1009
- tcpClient = tcpServer.available();
1010
- Out.setClient(&tcpClient);
1011
- }
1012
- if (tcpClient && !tcpClient.connected()) {
1013
- tcpClient.stop();
1014
- Out.setClient(nullptr);
1015
- }
1016
- if (tcpClient && tcpClient.available()) {
1017
- String tmsg = tcpClient.readStringUntil('\n');
1018
- if (tmsg.length() > 0) parseMessage(tmsg);
1019
- }
1020
- }
1021
-
1022
- if (Serial.available()) {
1023
- String msg = Serial.readStringUntil('\n');
1024
- parseMessage(msg);
1025
- }
1026
-
1027
- if (returnToHomeAt != 0 && (long)(millis() - returnToHomeAt) >= 0) {
1028
- drawHome();
1029
- }
1030
-
1031
- if (playStatusTemp && (long)(millis() - playStatusAt) >= 2500) {
1032
- playStatusTemp = false;
1033
- line1 = "PLAYING";
1034
- drawPlay();
1035
- }
1036
-
1037
- if (uiState == UI_PLAY && millis() - lastPotRead > POT_READ_MS) {
1038
- lastPotRead = millis();
1039
- int nextVolume = readPercent();
1040
- if (abs(nextVolume - playVolume) >= 3) {
1041
- playVolume = nextVolume;
1042
- Out.print("POT:");
1043
- Out.println(playVolume);
1044
- drawPlay();
1045
- }
1046
- }
1047
-
1048
- {
1049
- bool sel = digitalRead(BTN_SELECT_PIN) == LOW;
1050
- if (sel && !selectDown && millis() - selectLastDebounce > DEBOUNCE_MS) {
1051
- selectDown = true;
1052
- selectDownAt = millis();
1053
- wifiResetArmed = false;
1054
- }
1055
- // 10s hold on HOME/SETUP/STANDBY = wipe Wi-Fi creds + reboot to portal.
1056
- if (sel && selectDown && !wifiResetArmed &&
1057
- (uiState == UI_HOME || uiState == UI_SETUP || uiState == UI_STANDBY) &&
1058
- millis() - selectDownAt >= WIFI_RESET_HOLD_MS) {
1059
- wifiResetArmed = true;
1060
- Out.println("WiFi: creds wiped, rebooting");
1061
- clearCreds();
1062
- drawWifiReset();
1063
- delay(800);
1064
- ESP.restart();
1065
- }
1066
- if (!sel && selectDown) {
1067
- selectDown = false;
1068
- selectLastDebounce = millis();
1069
- if (!wifiResetArmed) {
1070
- handleSelectPress(millis() - selectDownAt >= LONG_PRESS_MS);
1071
- }
1072
- wifiResetArmed = false;
1073
- }
1074
- }
1075
-
1076
- {
1077
- bool up = digitalRead(BTN_UP_PIN) == LOW;
1078
- if (up && !upDown && millis() - upLastDebounce > DEBOUNCE_MS) {
1079
- upDown = true;
1080
- upDownAt = millis();
1081
- }
1082
- if (!up && upDown) {
1083
- upDown = false;
1084
- upLastDebounce = millis();
1085
- bool lp = millis() - upDownAt >= LONG_PRESS_MS;
1086
- if (uiState == UI_PLAY || !lp) {
1087
- handleUp(lp);
1088
- }
1089
- }
1090
- }
1091
-
1092
- {
1093
- bool dn = digitalRead(BTN_DOWN_PIN) == LOW;
1094
- if (dn && !downDown && millis() - downLastDebounce > DEBOUNCE_MS) {
1095
- downDown = true;
1096
- downDownAt = millis();
1097
- }
1098
- if (!dn && downDown) {
1099
- downDown = false;
1100
- downLastDebounce = millis();
1101
- bool lp = millis() - downDownAt >= LONG_PRESS_MS;
1102
- if (uiState == UI_PLAY || !lp) {
1103
- handleDown(lp);
1104
- }
1105
- }
1106
- }
1107
-
1108
- if (uiState == UI_STATUS && progressPercent < 0 &&
1109
- millis() - lastIndeterminateAnim > 500) {
1110
- lastIndeterminateAnim = millis();
1111
- indeterminateCount = (indeterminateCount % 4) + 1;
1112
- if (!displayBlank) drawStatus();
1113
- }
1114
-
1115
- if (uiState == UI_LOADING && millis() - lastLoadingAnim > 400) {
1116
- lastLoadingAnim = millis();
1117
- loadingDots = (loadingDots % 4) + 1;
1118
- if (!displayBlank) drawLoading();
1119
- }
1120
-
1121
- if (displayOk && !displayBlank &&
1122
- (uiState == UI_HOME || uiState == UI_STANDBY) &&
1123
- (long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
1124
- display.ssd1306_command(0xAE);
1125
- displayBlank = true;
1126
- }
1127
-
1128
- if (uiState != UI_DISCONNECTED &&
1129
- (long)(millis() - lastMsgTime) >= PING_TIMEOUT_MS) {
1130
- drawDisconnected();
1131
- }
1132
-
1133
- delay(20);
1134
- }