discstation 0.1.23 → 0.1.27

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,18 +1,29 @@
1
1
  #include <Wire.h>
2
2
  #include <Adafruit_GFX.h>
3
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>
4
10
  #include <QRCode.h>
5
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
+
6
18
  #define SCREEN_WIDTH 128
7
19
  #define SCREEN_HEIGHT 64
8
20
  #define OLED_RESET -1
9
21
  #define I2C_ADDRESS 0x3C
22
+ #define TCP_PORT 2323
10
23
 
11
24
  #define BTN_SELECT_PIN 21 // D3
12
25
  #define BTN_UP_PIN 20 // D9/MISO
13
26
  #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
27
  #define POT_PIN 0 // D0/A0 (ADC)
17
28
 
18
29
  #define DEBOUNCE_MS 50
@@ -20,8 +31,12 @@
20
31
  #define POT_READ_MS 200
21
32
  #define DONE_RESET_MS 30000
22
33
  #define STANDBY_BLANK_MS 60000
34
+ #define IDLE_BLANK_MS 45000 // blank the OLED after this long with no input on HOME/STANDBY
23
35
  #define PING_TIMEOUT_MS 30000
24
- #define LED_BLINK_MS 250
36
+
37
+ #define WIFI_RESET_HOLD_MS 10000
38
+ #define WIFI_CONNECT_TIMEOUT_MS 18000
39
+ #define WIFI_RETRY_MS 15000
25
40
 
26
41
  #define MAX_HOME_MODES 5
27
42
  #define BURN_COUNT 4
@@ -31,6 +46,137 @@ int homeCount = 3;
31
46
  String homeModes[MAX_HOME_MODES] = {"BURN", "PLAY", "RIP"};
32
47
  String discName = "";
33
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
+
34
180
  Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
35
181
 
36
182
  String ipUrl = "";
@@ -44,7 +190,8 @@ enum UiState {
44
190
  UI_WAITING,
45
191
  UI_IP,
46
192
  UI_DISCONNECTED,
47
- UI_LOADING
193
+ UI_LOADING,
194
+ UI_SETUP
48
195
  };
49
196
 
50
197
  const char* BURN_MODES[BURN_COUNT] = {"AUTO", "BEST", "LONG", "TEST"};
@@ -87,8 +234,6 @@ unsigned long downLastDebounce = 0;
87
234
  int ffSpeedIndex = 0;
88
235
  int rewSpeedIndex = 0;
89
236
 
90
- bool actLedState = false;
91
- unsigned long lastLedBlink = 0;
92
237
  int homeIndex = 0;
93
238
  int burnModeIndex = 0;
94
239
  int burnSpeedIndex = 0;
@@ -96,6 +241,7 @@ bool editingSpeed = false;
96
241
  int playVolume = 50;
97
242
  unsigned long standbyStartTime = 0;
98
243
  unsigned long lastMsgTime = 0;
244
+ unsigned long lastInputTime = 0; // last button press / screen change; drives OLED idle-blank
99
245
  bool displayBlank = false;
100
246
  bool audioPlayMode = false;
101
247
  int displayRotation = 0;
@@ -121,14 +267,14 @@ int readPercent() {
121
267
 
122
268
  void sendHomeMode() {
123
269
  if (homeIndex >= 0 && homeIndex < homeCount) {
124
- Serial.print("MENU:");
125
- Serial.println(homeModes[homeIndex]);
270
+ Out.print("MENU:");
271
+ Out.println(homeModes[homeIndex]);
126
272
  }
127
273
  }
128
274
 
129
275
  void sendBurnMode() {
130
- Serial.print("MODE:");
131
- Serial.println(BURN_MODES[burnModeIndex]);
276
+ Out.print("MODE:");
277
+ Out.println(BURN_MODES[burnModeIndex]);
132
278
  }
133
279
 
134
280
  void printUpper(String value) {
@@ -162,6 +308,7 @@ void drawHeader() {
162
308
  void drawHome() {
163
309
  uiState = UI_HOME;
164
310
  returnToHomeAt = 0;
311
+ lastInputTime = millis();
165
312
  if (!displayOk) return;
166
313
 
167
314
  display.clearDisplay();
@@ -311,9 +458,11 @@ void drawIP() {
311
458
  }
312
459
 
313
460
  void drawStandby() {
461
+ if (portalActive) { drawSetup(); return; }
314
462
  uiState = UI_STANDBY;
315
463
  returnToHomeAt = 0;
316
464
  standbyStartTime = millis();
465
+ lastInputTime = millis();
317
466
  displayBlank = false;
318
467
  if (!displayOk) return;
319
468
 
@@ -326,6 +475,38 @@ void drawStandby() {
326
475
  display.display();
327
476
  }
328
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
+
329
510
  void drawDisconnected() {
330
511
  uiState = UI_DISCONNECTED;
331
512
  returnToHomeAt = 0;
@@ -378,6 +559,7 @@ void wakeDisplay() {
378
559
  case UI_STANDBY: drawStandby(); break;
379
560
  case UI_DISCONNECTED: drawDisconnected(); break;
380
561
  case UI_LOADING: drawLoading(); break;
562
+ case UI_SETUP: drawSetup(); break;
381
563
  }
382
564
  }
383
565
 
@@ -410,7 +592,7 @@ void parseMessage(String msg) {
410
592
 
411
593
  if (msg == "PING") {
412
594
  lastMsgTime = millis();
413
- Serial.println("PONG");
595
+ Out.println("PONG");
414
596
  if (uiState == UI_DISCONNECTED) drawStandby();
415
597
  return;
416
598
  }
@@ -418,8 +600,8 @@ void parseMessage(String msg) {
418
600
  wakeDisplay();
419
601
  lastMsgTime = millis();
420
602
 
421
- Serial.print("RCV:");
422
- Serial.println(msg);
603
+ Out.print("RCV:");
604
+ Out.println(msg);
423
605
 
424
606
  if (msg.startsWith("HOME:")) {
425
607
  homeIndex = readBucket(homeCount);
@@ -593,6 +775,7 @@ void parseMessage(String msg) {
593
775
  }
594
776
 
595
777
  void setup() {
778
+ setCpuFrequencyMhz(160); // 240 -> 160: ~halves CPU power; I2C/GPIO all fine at 160
596
779
  Serial.begin(115200);
597
780
  Wire.begin(22, 23);
598
781
 
@@ -604,10 +787,6 @@ void setup() {
604
787
  pinMode(BTN_SELECT_PIN, INPUT_PULLUP);
605
788
  pinMode(BTN_UP_PIN, INPUT_PULLUP);
606
789
  pinMode(BTN_DOWN_PIN, INPUT_PULLUP);
607
- pinMode(LED_POWER_PIN, OUTPUT);
608
- pinMode(LED_ACT_PIN, OUTPUT);
609
- digitalWrite(LED_POWER_PIN, HIGH);
610
- digitalWrite(LED_ACT_PIN, LOW);
611
790
  pinMode(POT_PIN, INPUT);
612
791
  analogReadResolution(12);
613
792
 
@@ -618,37 +797,32 @@ void setup() {
618
797
  displayOk = display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS);
619
798
 
620
799
  if (displayOk) {
621
- Serial.println("Display OK");
800
+ Out.println("Display OK");
622
801
  } else {
623
- Serial.println("Display FAILED");
802
+ Out.println("Display FAILED");
624
803
  delay(3000);
625
804
  }
626
805
 
627
- for (int i = 0; i < 3; i++) {
628
- digitalWrite(LED_ACT_PIN, HIGH);
629
- delay(150);
630
- digitalWrite(LED_ACT_PIN, LOW);
631
- delay(150);
632
- }
633
-
634
806
  drawStandby();
635
807
  lastMsgTime = millis();
636
- Serial.println("DISCSTATION_READY");
808
+ lastInputTime = millis();
809
+ Out.println("DISCSTATION_READY");
637
810
  }
638
811
 
639
812
  void handleSelectPress(bool longPress) {
640
813
  wakeDisplay();
814
+ lastInputTime = millis();
641
815
  if (uiState == UI_HOME) {
642
816
  if (longPress) {
643
- Serial.println("EJECT");
817
+ Out.println("EJECT");
644
818
  line1 = "Ejecting...";
645
819
  line2 = "";
646
820
  line3 = "";
647
821
  returnToHomeAt = millis() + 5000;
648
822
  drawStatus();
649
823
  } else if (homeIndex >= 0 && homeIndex < homeCount) {
650
- Serial.print("SELECT:");
651
- Serial.println(homeModes[homeIndex]);
824
+ Out.print("SELECT:");
825
+ Out.println(homeModes[homeIndex]);
652
826
  line1 = "Selected";
653
827
  line2 = homeModes[homeIndex];
654
828
  line3 = "";
@@ -656,7 +830,7 @@ void handleSelectPress(bool longPress) {
656
830
  }
657
831
 
658
832
  } else if (uiState == UI_STANDBY) {
659
- Serial.println("EJECT");
833
+ Out.println("EJECT");
660
834
  line1 = "Ejecting...";
661
835
  line2 = "";
662
836
  line3 = "";
@@ -668,16 +842,16 @@ void handleSelectPress(bool longPress) {
668
842
 
669
843
  } else if (uiState == UI_IP) {
670
844
  if (longPress) {
671
- Serial.println("CANCEL");
845
+ Out.println("CANCEL");
672
846
  drawHome();
673
847
  }
674
848
 
675
849
  } else if (uiState == UI_WAITING) {
676
850
  if (longPress) {
677
- Serial.println("CANCEL");
851
+ Out.println("CANCEL");
678
852
  drawHome();
679
853
  } else {
680
- Serial.println("CONFIRM");
854
+ Out.println("CONFIRM");
681
855
  line1 = "Confirmed!";
682
856
  line2 = "";
683
857
  line3 = "";
@@ -686,8 +860,8 @@ void handleSelectPress(bool longPress) {
686
860
 
687
861
  } else if (uiState == UI_BURN_READY) {
688
862
  if (longPress) {
689
- Serial.print("START:");
690
- Serial.println(BURN_MODES[burnModeIndex]);
863
+ Out.print("START:");
864
+ Out.println(BURN_MODES[burnModeIndex]);
691
865
  line1 = "Starting...";
692
866
  line2 = BURN_MODES[burnModeIndex];
693
867
  line3 = "";
@@ -699,7 +873,7 @@ void handleSelectPress(bool longPress) {
699
873
 
700
874
  } else if (uiState == UI_STATUS) {
701
875
  if (longPress) {
702
- Serial.println("CANCEL");
876
+ Out.println("CANCEL");
703
877
  line1 = "Cancelling...";
704
878
  line2 = "";
705
879
  line3 = "";
@@ -707,22 +881,23 @@ void handleSelectPress(bool longPress) {
707
881
  }
708
882
 
709
883
  } else if (uiState == UI_DISCONNECTED) {
710
- Serial.println("CANCEL");
884
+ Out.println("CANCEL");
711
885
  drawStandby();
712
886
 
713
887
  } else if (uiState == UI_PLAY) {
714
888
  ffSpeedIndex = 0;
715
889
  rewSpeedIndex = 0;
716
890
  if (longPress) {
717
- Serial.println("PLAY_STOP");
891
+ Out.println("PLAY_STOP");
718
892
  } else {
719
- Serial.println("PLAY_BUTTON");
893
+ Out.println("PLAY_BUTTON");
720
894
  }
721
895
  }
722
896
  }
723
897
 
724
898
  void handleUp(bool longPress) {
725
899
  wakeDisplay();
900
+ lastInputTime = millis();
726
901
  if (uiState == UI_HOME) {
727
902
  if (homeCount > 0) {
728
903
  homeIndex = (homeIndex - 1 + homeCount) % homeCount;
@@ -732,8 +907,8 @@ void handleUp(bool longPress) {
732
907
  } else if (uiState == UI_BURN_READY) {
733
908
  if (editingSpeed) {
734
909
  burnSpeedIndex = (burnSpeedIndex - 1 + SPEED_COUNT) % SPEED_COUNT;
735
- Serial.print("SPEED:");
736
- Serial.println(SPEED_MODES[burnSpeedIndex]);
910
+ Out.print("SPEED:");
911
+ Out.println(SPEED_MODES[burnSpeedIndex]);
737
912
  drawBurnReady();
738
913
  } else {
739
914
  burnModeIndex = (burnModeIndex - 1 + BURN_COUNT) % BURN_COUNT;
@@ -742,23 +917,24 @@ void handleUp(bool longPress) {
742
917
  }
743
918
  } else if (uiState == UI_PLAY) {
744
919
  if (audioPlayMode) {
745
- Serial.println(displayRotation == 0 ? (longPress ? "REW:BIG" : "REW:10") : (longPress ? "FF:BIG" : "FF:10"));
920
+ Out.println(displayRotation == 0 ? (longPress ? "REW:BIG" : "REW:10") : (longPress ? "FF:BIG" : "FF:10"));
746
921
  return;
747
922
  }
748
923
  rewSpeedIndex = 0;
749
924
  if (longPress) {
750
- Serial.println("FF:BIG");
925
+ Out.println("FF:BIG");
751
926
  } else {
752
927
  ffSpeedIndex = (ffSpeedIndex + 1) % 4;
753
928
  int seek_sec[] = {10, 20, 30, 60};
754
- Serial.print("FF:");
755
- Serial.println(seek_sec[ffSpeedIndex]);
929
+ Out.print("FF:");
930
+ Out.println(seek_sec[ffSpeedIndex]);
756
931
  }
757
932
  }
758
933
  }
759
934
 
760
935
  void handleDown(bool longPress) {
761
936
  wakeDisplay();
937
+ lastInputTime = millis();
762
938
  if (uiState == UI_HOME) {
763
939
  if (homeCount > 0) {
764
940
  homeIndex = (homeIndex + 1) % homeCount;
@@ -768,8 +944,8 @@ void handleDown(bool longPress) {
768
944
  } else if (uiState == UI_BURN_READY) {
769
945
  if (editingSpeed) {
770
946
  burnSpeedIndex = (burnSpeedIndex + 1) % SPEED_COUNT;
771
- Serial.print("SPEED:");
772
- Serial.println(SPEED_MODES[burnSpeedIndex]);
947
+ Out.print("SPEED:");
948
+ Out.println(SPEED_MODES[burnSpeedIndex]);
773
949
  drawBurnReady();
774
950
  } else {
775
951
  burnModeIndex = (burnModeIndex + 1) % BURN_COUNT;
@@ -778,22 +954,66 @@ void handleDown(bool longPress) {
778
954
  }
779
955
  } else if (uiState == UI_PLAY) {
780
956
  if (audioPlayMode) {
781
- Serial.println(displayRotation == 0 ? (longPress ? "FF:BIG" : "FF:10") : (longPress ? "REW:BIG" : "REW:10"));
957
+ Out.println(displayRotation == 0 ? (longPress ? "FF:BIG" : "FF:10") : (longPress ? "REW:BIG" : "REW:10"));
782
958
  return;
783
959
  }
784
960
  ffSpeedIndex = 0;
785
961
  if (longPress) {
786
- Serial.println("REW:BIG");
962
+ Out.println("REW:BIG");
787
963
  } else {
788
964
  rewSpeedIndex = (rewSpeedIndex + 1) % 4;
789
965
  int seek_sec[] = {10, 20, 30, 60};
790
- Serial.print("REW:");
791
- Serial.println(seek_sec[rewSpeedIndex]);
966
+ Out.print("REW:");
967
+ Out.println(seek_sec[rewSpeedIndex]);
792
968
  }
793
969
  }
794
970
  }
795
971
 
796
972
  void loop() {
973
+ if (!wifiInitDone && millis() > 3000) {
974
+ wifiInitDone = true;
975
+ initWiFi();
976
+ }
977
+
978
+ if (credsJustSaved) {
979
+ drawWifiReset();
980
+ delay(600);
981
+ ESP.restart();
982
+ }
983
+
984
+ if (portalActive) {
985
+ wm.process();
986
+ if (WiFi.status() == WL_CONNECTED) onWifiUp();
987
+ }
988
+
989
+ if (wifiConnected && WiFi.status() != WL_CONNECTED) {
990
+ if (wifiDropAt == 0) wifiDropAt = millis();
991
+ else if (millis() - wifiDropAt > WIFI_RETRY_MS) {
992
+ wifiDropAt = millis();
993
+ WiFi.disconnect();
994
+ WiFi.begin();
995
+ }
996
+ } else if (wifiConnected) {
997
+ wifiDropAt = 0;
998
+ }
999
+
1000
+ if (wifiConnected && WiFi.status() == WL_CONNECTED) {
1001
+ if (otaEnabled) ArduinoOTA.handle();
1002
+ if (tcpServer.hasClient()) {
1003
+ if (tcpClient) tcpClient.stop();
1004
+ tcpClient = tcpServer.available();
1005
+ Out.setClient(&tcpClient);
1006
+ }
1007
+ if (tcpClient && !tcpClient.connected()) {
1008
+ tcpClient.stop();
1009
+ Out.setClient(nullptr);
1010
+ }
1011
+ if (tcpClient && tcpClient.available()) {
1012
+ String tmsg = tcpClient.readStringUntil('\n');
1013
+ if (tmsg.length() > 0) parseMessage(tmsg);
1014
+ }
1015
+ }
1016
+
797
1017
  if (Serial.available()) {
798
1018
  String msg = Serial.readStringUntil('\n');
799
1019
  parseMessage(msg);
@@ -814,8 +1034,8 @@ void loop() {
814
1034
  int nextVolume = readPercent();
815
1035
  if (abs(nextVolume - playVolume) >= 3) {
816
1036
  playVolume = nextVolume;
817
- Serial.print("POT:");
818
- Serial.println(playVolume);
1037
+ Out.print("POT:");
1038
+ Out.println(playVolume);
819
1039
  drawPlay();
820
1040
  }
821
1041
  }
@@ -825,11 +1045,26 @@ void loop() {
825
1045
  if (sel && !selectDown && millis() - selectLastDebounce > DEBOUNCE_MS) {
826
1046
  selectDown = true;
827
1047
  selectDownAt = millis();
1048
+ wifiResetArmed = false;
1049
+ }
1050
+ // 10s hold on HOME/SETUP/STANDBY = wipe Wi-Fi creds + reboot to portal.
1051
+ if (sel && selectDown && !wifiResetArmed &&
1052
+ (uiState == UI_HOME || uiState == UI_SETUP || uiState == UI_STANDBY) &&
1053
+ millis() - selectDownAt >= WIFI_RESET_HOLD_MS) {
1054
+ wifiResetArmed = true;
1055
+ Out.println("WiFi: creds wiped, rebooting");
1056
+ clearCreds();
1057
+ drawWifiReset();
1058
+ delay(800);
1059
+ ESP.restart();
828
1060
  }
829
1061
  if (!sel && selectDown) {
830
1062
  selectDown = false;
831
1063
  selectLastDebounce = millis();
832
- handleSelectPress(millis() - selectDownAt >= LONG_PRESS_MS);
1064
+ if (!wifiResetArmed) {
1065
+ handleSelectPress(millis() - selectDownAt >= LONG_PRESS_MS);
1066
+ }
1067
+ wifiResetArmed = false;
833
1068
  }
834
1069
  }
835
1070
 
@@ -878,14 +1113,11 @@ void loop() {
878
1113
  if (!displayBlank) drawLoading();
879
1114
  }
880
1115
 
881
- if (displayOk && uiState == UI_STANDBY && !displayBlank &&
882
- (long)(millis() - standbyStartTime) >= STANDBY_BLANK_MS) {
1116
+ if (displayOk && !displayBlank &&
1117
+ (uiState == UI_HOME || uiState == UI_STANDBY) &&
1118
+ (long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
883
1119
  display.ssd1306_command(0xAE);
884
1120
  displayBlank = true;
885
- digitalWrite(LED_POWER_PIN, LOW);
886
- }
887
- if (!displayBlank) {
888
- digitalWrite(LED_POWER_PIN, HIGH);
889
1121
  }
890
1122
 
891
1123
  if (uiState != UI_DISCONNECTED &&
@@ -893,28 +1125,5 @@ void loop() {
893
1125
  drawDisconnected();
894
1126
  }
895
1127
 
896
- {
897
- bool ledOn = false;
898
- if (uiState == UI_BURN_READY || uiState == UI_PLAY || uiState == UI_WAITING || uiState == UI_IP) {
899
- ledOn = true;
900
- } else if (uiState == UI_STATUS) {
901
- if (progressPercent >= 0) {
902
- ledOn = (millis() - lastLedBlink > LED_BLINK_MS);
903
- if (ledOn) {
904
- lastLedBlink = millis();
905
- actLedState = !actLedState;
906
- }
907
- ledOn = actLedState;
908
- } else {
909
- ledOn = true;
910
- }
911
- } else if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_LOADING) {
912
- ledOn = false;
913
- } else if (uiState == UI_DISCONNECTED) {
914
- ledOn = true;
915
- }
916
- digitalWrite(LED_ACT_PIN, ledOn ? HIGH : LOW);
917
- }
918
-
919
1128
  delay(20);
920
1129
  }
@@ -0,0 +1,14 @@
1
+ // Optional build-time Wi-Fi override for the ESP32-C6 remote.
2
+ //
3
+ // cp secrets.h.example secrets.h (secrets.h is git-ignored)
4
+ //
5
+ // If WIFI_SSID is defined, the firmware connects straight to this network and
6
+ // the runtime setup portal is skipped entirely. Leave secrets.h absent to use
7
+ // the normal captive-portal provisioning ("DiscStation-XXXX" AP).
8
+
9
+ #define WIFI_SSID "YourNetwork"
10
+ #define WIFI_PASS "YourPassword"
11
+
12
+ // Optional: enable Arduino OTA updates with this password. Omit to keep OTA off
13
+ // (recommended unless you actually use it).
14
+ // #define OTA_PASSWORD "change-me"