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