discstation 0.1.22 → 0.1.26
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.
- package/README.md +43 -8
- package/arduino/c6/DiscStation_C6.ino +293 -41
- package/arduino/c6/secrets.h.example +14 -0
- package/arduino/v1/DiscStation.ino +214 -19
- package/arduino/v1/secrets.h.example +14 -0
- package/discstation.env.example +7 -0
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/scripts/lib/net.mjs +14 -0
- package/scripts/open.mjs +2 -0
- package/scripts/setup.mjs +4 -1
- package/src/discstation.py +150 -15
- package/src/discstation_burn.py +9 -0
- package/src/discstation_host.py +73 -0
package/README.md
CHANGED
|
@@ -9,12 +9,37 @@ use every mode.
|
|
|
9
9
|
## Hardware
|
|
10
10
|
|
|
11
11
|
- **Brain:** Raspberry Pi 4/5 (or any Linux box)
|
|
12
|
-
- **Remote (optional):** ESP32-C6 with SSD1306 OLED,
|
|
13
|
-
buttons
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
- **Remote (optional):** ESP32 DevKit or ESP32-C6 with an SSD1306 OLED, 3
|
|
13
|
+
buttons, and a volume pot — connected by **USB serial** or over **Wi-Fi**
|
|
14
|
+
(see below) — or none at all; the web UI / mobile app's on-screen remote
|
|
15
|
+
covers the same controls when no ESP32 is detected, and steps aside
|
|
16
|
+
(greyed out, auto-collapsing) the moment one appears.
|
|
16
17
|
- **Drive:** ATAPI DVD writer (e.g. iHAS124) over USB
|
|
17
18
|
|
|
19
|
+
### Wi-Fi remote (optional)
|
|
20
|
+
|
|
21
|
+
The firmware also serves the same line protocol over TCP 2323, so the
|
|
22
|
+
remote can talk to the host over Wi-Fi instead of a USB cable.
|
|
23
|
+
|
|
24
|
+
- **Provisioning (no code edit):** on first boot with no stored credentials
|
|
25
|
+
the remote starts an open `DiscStation-XXXX` access point; join it from a
|
|
26
|
+
phone, the captive portal (or `http://192.168.4.1`) lists nearby networks
|
|
27
|
+
— pick yours, enter the password, done. It reconnects on every boot after
|
|
28
|
+
that. Change networks later: **hold SELECT for 10 s** on the home screen
|
|
29
|
+
to wipe the credentials and reopen the portal.
|
|
30
|
+
Power users can instead `cp arduino/<board>/secrets.h.example secrets.h`
|
|
31
|
+
and set `WIFI_SSID` / `WIFI_PASS` at build time (git-ignored).
|
|
32
|
+
- **Host side:** nothing to configure. With no USB cable present the host
|
|
33
|
+
finds the remote by mDNS automatically (`discstation-xxxx.local`). Set
|
|
34
|
+
`DISC_REMOTE_HOST` in `discstation.env` to an explicit IP only if mDNS is
|
|
35
|
+
blocked on your network, or `off` to never look. A USB cable always wins
|
|
36
|
+
when both are present.
|
|
37
|
+
- **Trust model:** TCP 2323 is unauthenticated on the LAN, same as the USB
|
|
38
|
+
link and the LAN web UI — fine for a home appliance. During the ~30 s of
|
|
39
|
+
portal setup the password crosses an open AP; the ESP32 is 2.4 GHz only
|
|
40
|
+
(a "connected to the router but no data" symptom usually means a
|
|
41
|
+
5 GHz-only / band-steering network or a wrong password).
|
|
42
|
+
|
|
18
43
|
## Features
|
|
19
44
|
|
|
20
45
|
| Mode | Description |
|
|
@@ -58,10 +83,12 @@ discstation-setup # dispatches to the installer for your OS, then opens
|
|
|
58
83
|
|
|
59
84
|
The host runs as a background service; the UI is the built-in web app at
|
|
60
85
|
`http://localhost:8081`. `discstation-setup` opens it when it finishes, and the
|
|
61
|
-
`discstation` command re-opens it any time
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
`discstation` command re-opens it any time — both also print the LAN address
|
|
87
|
+
(e.g. `Also reachable at http://192.168.1.50:8081/ ...`) so you know what to
|
|
88
|
+
open from a phone or another computer, or enter into the mobile app. It's a
|
|
89
|
+
PWA — use your browser's **Install DiscStation** (or the in-page
|
|
90
|
+
**INSTALL APP** button on Chromium) to get a standalone app window with a
|
|
91
|
+
dock/taskbar icon on macOS, Linux, and Windows.
|
|
65
92
|
|
|
66
93
|
`discstation-setup --help` lists the forwarded env vars. From a git clone,
|
|
67
94
|
`npm run setup` does the same thing.
|
|
@@ -123,6 +150,14 @@ self-signed cert. On startup the host prints the LAN URL to open
|
|
|
123
150
|
with no ESP32 attached; greys out and auto-collapses the instant a
|
|
124
151
|
physical remote is detected, so the two never fight for control.
|
|
125
152
|
|
|
153
|
+
## Mobile App
|
|
154
|
+
|
|
155
|
+
The `mobile/` Expo app (enter the host's LAN address in its settings screen)
|
|
156
|
+
has the same on-screen remote as the web UI — mode buttons, EJECT/CLOSE,
|
|
157
|
+
disc status, and playback transport, tapped from the "REMOTE" tag next to
|
|
158
|
+
the wordmark. Same auto-collapse behavior when a physical ESP32 shows up,
|
|
159
|
+
same underlying `/remote/button` protocol.
|
|
160
|
+
|
|
126
161
|
## Disc Support
|
|
127
162
|
|
|
128
163
|
- DVD-R, DVD+R (single and dual-layer)
|
|
@@ -1,12 +1,25 @@
|
|
|
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
|
|
@@ -20,9 +33,14 @@
|
|
|
20
33
|
#define POT_READ_MS 200
|
|
21
34
|
#define DONE_RESET_MS 30000
|
|
22
35
|
#define STANDBY_BLANK_MS 60000
|
|
36
|
+
#define IDLE_BLANK_MS 45000 // blank the OLED after this long with no input on HOME/STANDBY
|
|
23
37
|
#define PING_TIMEOUT_MS 30000
|
|
24
38
|
#define LED_BLINK_MS 250
|
|
25
39
|
|
|
40
|
+
#define WIFI_RESET_HOLD_MS 10000
|
|
41
|
+
#define WIFI_CONNECT_TIMEOUT_MS 18000
|
|
42
|
+
#define WIFI_RETRY_MS 30000
|
|
43
|
+
|
|
26
44
|
#define MAX_HOME_MODES 5
|
|
27
45
|
#define BURN_COUNT 4
|
|
28
46
|
#define SPEED_COUNT 4
|
|
@@ -31,6 +49,137 @@ int homeCount = 3;
|
|
|
31
49
|
String homeModes[MAX_HOME_MODES] = {"BURN", "PLAY", "RIP"};
|
|
32
50
|
String discName = "";
|
|
33
51
|
|
|
52
|
+
// --- Wi-Fi link: mirror every outbound line to Serial AND a TCP client ---
|
|
53
|
+
WiFiServer tcpServer(TCP_PORT);
|
|
54
|
+
WiFiClient tcpClient;
|
|
55
|
+
WiFiManager wm;
|
|
56
|
+
Preferences prefs;
|
|
57
|
+
bool wifiConnected = false;
|
|
58
|
+
bool wifiInitDone = false;
|
|
59
|
+
bool portalActive = false;
|
|
60
|
+
bool otaEnabled = false;
|
|
61
|
+
bool credsJustSaved = false;
|
|
62
|
+
bool wifiResetArmed = false;
|
|
63
|
+
unsigned long wifiDropAt = 0;
|
|
64
|
+
String apName = "DiscStation";
|
|
65
|
+
String mdnsHost = "discstation";
|
|
66
|
+
|
|
67
|
+
class DualPrint : public Print {
|
|
68
|
+
public:
|
|
69
|
+
void setClient(WiFiClient* c) { _client = c; }
|
|
70
|
+
size_t write(uint8_t c) override {
|
|
71
|
+
Serial.write(c);
|
|
72
|
+
if (_client && _client->connected()) _client->write(c);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
size_t write(const uint8_t* buf, size_t len) override {
|
|
76
|
+
Serial.write(buf, len);
|
|
77
|
+
if (_client && _client->connected()) _client->write(buf, len);
|
|
78
|
+
return len;
|
|
79
|
+
}
|
|
80
|
+
private:
|
|
81
|
+
WiFiClient* _client = nullptr;
|
|
82
|
+
} Out;
|
|
83
|
+
|
|
84
|
+
void drawSetup();
|
|
85
|
+
void drawWifiReset();
|
|
86
|
+
|
|
87
|
+
String deviceSuffix() {
|
|
88
|
+
uint8_t mac[6];
|
|
89
|
+
esp_read_mac(mac, ESP_MAC_WIFI_STA); // factory MAC from eFuse - valid before the Wi-Fi driver starts
|
|
90
|
+
char buf[5];
|
|
91
|
+
snprintf(buf, sizeof(buf), "%02X%02X", mac[4], mac[5]);
|
|
92
|
+
return String(buf);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void loadCreds(String& ssid, String& pass) {
|
|
96
|
+
prefs.begin("discstation", true);
|
|
97
|
+
ssid = prefs.getString("ssid", "");
|
|
98
|
+
pass = prefs.getString("pass", "");
|
|
99
|
+
prefs.end();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void saveCreds(const String& ssid, const String& pass) {
|
|
103
|
+
prefs.begin("discstation", false);
|
|
104
|
+
prefs.putString("ssid", ssid);
|
|
105
|
+
prefs.putString("pass", pass);
|
|
106
|
+
prefs.end();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void clearCreds() {
|
|
110
|
+
prefs.begin("discstation", false);
|
|
111
|
+
prefs.clear();
|
|
112
|
+
prefs.end();
|
|
113
|
+
WiFi.disconnect(true, true);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void wmSaveCallback() {
|
|
117
|
+
saveCreds(wm.getWiFiSSID(), wm.getWiFiPass());
|
|
118
|
+
credsJustSaved = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
void onWifiUp() {
|
|
122
|
+
portalActive = false;
|
|
123
|
+
wifiConnected = true;
|
|
124
|
+
wifiDropAt = 0;
|
|
125
|
+
WiFi.setSleep(WIFI_PS_MIN_MODEM);
|
|
126
|
+
tcpServer.begin();
|
|
127
|
+
if (MDNS.begin(mdnsHost.c_str())) {
|
|
128
|
+
MDNS.addService("discstation", "tcp", TCP_PORT);
|
|
129
|
+
}
|
|
130
|
+
#ifdef OTA_PASSWORD
|
|
131
|
+
ArduinoOTA.setHostname(mdnsHost.c_str());
|
|
132
|
+
ArduinoOTA.setPassword(OTA_PASSWORD);
|
|
133
|
+
ArduinoOTA.begin();
|
|
134
|
+
otaEnabled = true;
|
|
135
|
+
#endif
|
|
136
|
+
Out.print("WiFi OK "); Out.println(WiFi.localIP());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
void startPortal() {
|
|
140
|
+
Out.print("WiFi: setup portal '"); Out.print(apName); Out.println("'");
|
|
141
|
+
portalActive = true;
|
|
142
|
+
WiFi.mode(WIFI_AP_STA);
|
|
143
|
+
wm.setConfigPortalBlocking(false);
|
|
144
|
+
wm.setConfigPortalTimeout(0);
|
|
145
|
+
wm.setSaveConfigCallback(wmSaveCallback);
|
|
146
|
+
wm.startConfigPortal(apName.c_str());
|
|
147
|
+
drawSetup();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void initWiFi() {
|
|
151
|
+
apName = "DiscStation-" + deviceSuffix();
|
|
152
|
+
mdnsHost = "discstation-" + deviceSuffix();
|
|
153
|
+
mdnsHost.toLowerCase();
|
|
154
|
+
|
|
155
|
+
WiFi.mode(WIFI_STA);
|
|
156
|
+
WiFi.setAutoReconnect(true);
|
|
157
|
+
WiFi.setSleep(WIFI_PS_MIN_MODEM);
|
|
158
|
+
|
|
159
|
+
String ssid, pass;
|
|
160
|
+
#ifdef WIFI_SSID
|
|
161
|
+
ssid = WIFI_SSID;
|
|
162
|
+
pass = WIFI_PASS;
|
|
163
|
+
#else
|
|
164
|
+
loadCreds(ssid, pass);
|
|
165
|
+
#endif
|
|
166
|
+
|
|
167
|
+
if (ssid.length() > 0) {
|
|
168
|
+
Out.print("WiFi "); Out.print(ssid); Out.print("...");
|
|
169
|
+
WiFi.begin(ssid.c_str(), pass.c_str());
|
|
170
|
+
unsigned long t0 = millis();
|
|
171
|
+
while (WiFi.status() != WL_CONNECTED && millis() - t0 < WIFI_CONNECT_TIMEOUT_MS) delay(200);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (WiFi.status() == WL_CONNECTED) {
|
|
175
|
+
Out.println(" OK");
|
|
176
|
+
onWifiUp();
|
|
177
|
+
} else {
|
|
178
|
+
if (ssid.length() > 0) Out.println(" fail (2.4GHz band / wrong password?)");
|
|
179
|
+
startPortal();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
34
183
|
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
|
35
184
|
|
|
36
185
|
String ipUrl = "";
|
|
@@ -44,7 +193,8 @@ enum UiState {
|
|
|
44
193
|
UI_WAITING,
|
|
45
194
|
UI_IP,
|
|
46
195
|
UI_DISCONNECTED,
|
|
47
|
-
UI_LOADING
|
|
196
|
+
UI_LOADING,
|
|
197
|
+
UI_SETUP
|
|
48
198
|
};
|
|
49
199
|
|
|
50
200
|
const char* BURN_MODES[BURN_COUNT] = {"AUTO", "BEST", "LONG", "TEST"};
|
|
@@ -96,6 +246,7 @@ bool editingSpeed = false;
|
|
|
96
246
|
int playVolume = 50;
|
|
97
247
|
unsigned long standbyStartTime = 0;
|
|
98
248
|
unsigned long lastMsgTime = 0;
|
|
249
|
+
unsigned long lastInputTime = 0; // last button press / screen change; drives OLED idle-blank
|
|
99
250
|
bool displayBlank = false;
|
|
100
251
|
bool audioPlayMode = false;
|
|
101
252
|
int displayRotation = 0;
|
|
@@ -121,14 +272,14 @@ int readPercent() {
|
|
|
121
272
|
|
|
122
273
|
void sendHomeMode() {
|
|
123
274
|
if (homeIndex >= 0 && homeIndex < homeCount) {
|
|
124
|
-
|
|
125
|
-
|
|
275
|
+
Out.print("MENU:");
|
|
276
|
+
Out.println(homeModes[homeIndex]);
|
|
126
277
|
}
|
|
127
278
|
}
|
|
128
279
|
|
|
129
280
|
void sendBurnMode() {
|
|
130
|
-
|
|
131
|
-
|
|
281
|
+
Out.print("MODE:");
|
|
282
|
+
Out.println(BURN_MODES[burnModeIndex]);
|
|
132
283
|
}
|
|
133
284
|
|
|
134
285
|
void printUpper(String value) {
|
|
@@ -162,6 +313,7 @@ void drawHeader() {
|
|
|
162
313
|
void drawHome() {
|
|
163
314
|
uiState = UI_HOME;
|
|
164
315
|
returnToHomeAt = 0;
|
|
316
|
+
lastInputTime = millis();
|
|
165
317
|
if (!displayOk) return;
|
|
166
318
|
|
|
167
319
|
display.clearDisplay();
|
|
@@ -311,9 +463,11 @@ void drawIP() {
|
|
|
311
463
|
}
|
|
312
464
|
|
|
313
465
|
void drawStandby() {
|
|
466
|
+
if (portalActive) { drawSetup(); return; }
|
|
314
467
|
uiState = UI_STANDBY;
|
|
315
468
|
returnToHomeAt = 0;
|
|
316
469
|
standbyStartTime = millis();
|
|
470
|
+
lastInputTime = millis();
|
|
317
471
|
displayBlank = false;
|
|
318
472
|
if (!displayOk) return;
|
|
319
473
|
|
|
@@ -326,6 +480,38 @@ void drawStandby() {
|
|
|
326
480
|
display.display();
|
|
327
481
|
}
|
|
328
482
|
|
|
483
|
+
void drawSetup() {
|
|
484
|
+
uiState = UI_SETUP;
|
|
485
|
+
returnToHomeAt = 0;
|
|
486
|
+
displayBlank = false;
|
|
487
|
+
lastInputTime = millis();
|
|
488
|
+
if (!displayOk) return;
|
|
489
|
+
display.clearDisplay();
|
|
490
|
+
drawHeader();
|
|
491
|
+
display.setCursor(2, 16);
|
|
492
|
+
display.print("WIFI SETUP");
|
|
493
|
+
display.setCursor(2, 28);
|
|
494
|
+
display.print("JOIN:");
|
|
495
|
+
display.setCursor(2, 38);
|
|
496
|
+
display.print(apName);
|
|
497
|
+
display.setCursor(2, 50);
|
|
498
|
+
display.print("THEN 192.168.4.1");
|
|
499
|
+
display.display();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
void drawWifiReset() {
|
|
503
|
+
uiState = UI_SETUP;
|
|
504
|
+
displayBlank = false;
|
|
505
|
+
if (!displayOk) return;
|
|
506
|
+
display.clearDisplay();
|
|
507
|
+
drawHeader();
|
|
508
|
+
display.setCursor(2, 25);
|
|
509
|
+
display.print("WIFI RESET");
|
|
510
|
+
display.setCursor(2, 40);
|
|
511
|
+
display.print("REBOOTING...");
|
|
512
|
+
display.display();
|
|
513
|
+
}
|
|
514
|
+
|
|
329
515
|
void drawDisconnected() {
|
|
330
516
|
uiState = UI_DISCONNECTED;
|
|
331
517
|
returnToHomeAt = 0;
|
|
@@ -378,6 +564,7 @@ void wakeDisplay() {
|
|
|
378
564
|
case UI_STANDBY: drawStandby(); break;
|
|
379
565
|
case UI_DISCONNECTED: drawDisconnected(); break;
|
|
380
566
|
case UI_LOADING: drawLoading(); break;
|
|
567
|
+
case UI_SETUP: drawSetup(); break;
|
|
381
568
|
}
|
|
382
569
|
}
|
|
383
570
|
|
|
@@ -410,7 +597,7 @@ void parseMessage(String msg) {
|
|
|
410
597
|
|
|
411
598
|
if (msg == "PING") {
|
|
412
599
|
lastMsgTime = millis();
|
|
413
|
-
|
|
600
|
+
Out.println("PONG");
|
|
414
601
|
if (uiState == UI_DISCONNECTED) drawStandby();
|
|
415
602
|
return;
|
|
416
603
|
}
|
|
@@ -418,8 +605,8 @@ void parseMessage(String msg) {
|
|
|
418
605
|
wakeDisplay();
|
|
419
606
|
lastMsgTime = millis();
|
|
420
607
|
|
|
421
|
-
|
|
422
|
-
|
|
608
|
+
Out.print("RCV:");
|
|
609
|
+
Out.println(msg);
|
|
423
610
|
|
|
424
611
|
if (msg.startsWith("HOME:")) {
|
|
425
612
|
homeIndex = readBucket(homeCount);
|
|
@@ -593,6 +780,7 @@ void parseMessage(String msg) {
|
|
|
593
780
|
}
|
|
594
781
|
|
|
595
782
|
void setup() {
|
|
783
|
+
setCpuFrequencyMhz(160); // 240 -> 160: ~halves CPU power; I2C/GPIO all fine at 160
|
|
596
784
|
Serial.begin(115200);
|
|
597
785
|
Wire.begin(22, 23);
|
|
598
786
|
|
|
@@ -618,9 +806,9 @@ void setup() {
|
|
|
618
806
|
displayOk = display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS);
|
|
619
807
|
|
|
620
808
|
if (displayOk) {
|
|
621
|
-
|
|
809
|
+
Out.println("Display OK");
|
|
622
810
|
} else {
|
|
623
|
-
|
|
811
|
+
Out.println("Display FAILED");
|
|
624
812
|
delay(3000);
|
|
625
813
|
}
|
|
626
814
|
|
|
@@ -633,22 +821,24 @@ void setup() {
|
|
|
633
821
|
|
|
634
822
|
drawStandby();
|
|
635
823
|
lastMsgTime = millis();
|
|
636
|
-
|
|
824
|
+
lastInputTime = millis();
|
|
825
|
+
Out.println("DISCSTATION_READY");
|
|
637
826
|
}
|
|
638
827
|
|
|
639
828
|
void handleSelectPress(bool longPress) {
|
|
640
829
|
wakeDisplay();
|
|
830
|
+
lastInputTime = millis();
|
|
641
831
|
if (uiState == UI_HOME) {
|
|
642
832
|
if (longPress) {
|
|
643
|
-
|
|
833
|
+
Out.println("EJECT");
|
|
644
834
|
line1 = "Ejecting...";
|
|
645
835
|
line2 = "";
|
|
646
836
|
line3 = "";
|
|
647
837
|
returnToHomeAt = millis() + 5000;
|
|
648
838
|
drawStatus();
|
|
649
839
|
} else if (homeIndex >= 0 && homeIndex < homeCount) {
|
|
650
|
-
|
|
651
|
-
|
|
840
|
+
Out.print("SELECT:");
|
|
841
|
+
Out.println(homeModes[homeIndex]);
|
|
652
842
|
line1 = "Selected";
|
|
653
843
|
line2 = homeModes[homeIndex];
|
|
654
844
|
line3 = "";
|
|
@@ -656,7 +846,7 @@ void handleSelectPress(bool longPress) {
|
|
|
656
846
|
}
|
|
657
847
|
|
|
658
848
|
} else if (uiState == UI_STANDBY) {
|
|
659
|
-
|
|
849
|
+
Out.println("EJECT");
|
|
660
850
|
line1 = "Ejecting...";
|
|
661
851
|
line2 = "";
|
|
662
852
|
line3 = "";
|
|
@@ -668,16 +858,16 @@ void handleSelectPress(bool longPress) {
|
|
|
668
858
|
|
|
669
859
|
} else if (uiState == UI_IP) {
|
|
670
860
|
if (longPress) {
|
|
671
|
-
|
|
861
|
+
Out.println("CANCEL");
|
|
672
862
|
drawHome();
|
|
673
863
|
}
|
|
674
864
|
|
|
675
865
|
} else if (uiState == UI_WAITING) {
|
|
676
866
|
if (longPress) {
|
|
677
|
-
|
|
867
|
+
Out.println("CANCEL");
|
|
678
868
|
drawHome();
|
|
679
869
|
} else {
|
|
680
|
-
|
|
870
|
+
Out.println("CONFIRM");
|
|
681
871
|
line1 = "Confirmed!";
|
|
682
872
|
line2 = "";
|
|
683
873
|
line3 = "";
|
|
@@ -686,8 +876,8 @@ void handleSelectPress(bool longPress) {
|
|
|
686
876
|
|
|
687
877
|
} else if (uiState == UI_BURN_READY) {
|
|
688
878
|
if (longPress) {
|
|
689
|
-
|
|
690
|
-
|
|
879
|
+
Out.print("START:");
|
|
880
|
+
Out.println(BURN_MODES[burnModeIndex]);
|
|
691
881
|
line1 = "Starting...";
|
|
692
882
|
line2 = BURN_MODES[burnModeIndex];
|
|
693
883
|
line3 = "";
|
|
@@ -699,7 +889,7 @@ void handleSelectPress(bool longPress) {
|
|
|
699
889
|
|
|
700
890
|
} else if (uiState == UI_STATUS) {
|
|
701
891
|
if (longPress) {
|
|
702
|
-
|
|
892
|
+
Out.println("CANCEL");
|
|
703
893
|
line1 = "Cancelling...";
|
|
704
894
|
line2 = "";
|
|
705
895
|
line3 = "";
|
|
@@ -707,22 +897,23 @@ void handleSelectPress(bool longPress) {
|
|
|
707
897
|
}
|
|
708
898
|
|
|
709
899
|
} else if (uiState == UI_DISCONNECTED) {
|
|
710
|
-
|
|
900
|
+
Out.println("CANCEL");
|
|
711
901
|
drawStandby();
|
|
712
902
|
|
|
713
903
|
} else if (uiState == UI_PLAY) {
|
|
714
904
|
ffSpeedIndex = 0;
|
|
715
905
|
rewSpeedIndex = 0;
|
|
716
906
|
if (longPress) {
|
|
717
|
-
|
|
907
|
+
Out.println("PLAY_STOP");
|
|
718
908
|
} else {
|
|
719
|
-
|
|
909
|
+
Out.println("PLAY_BUTTON");
|
|
720
910
|
}
|
|
721
911
|
}
|
|
722
912
|
}
|
|
723
913
|
|
|
724
914
|
void handleUp(bool longPress) {
|
|
725
915
|
wakeDisplay();
|
|
916
|
+
lastInputTime = millis();
|
|
726
917
|
if (uiState == UI_HOME) {
|
|
727
918
|
if (homeCount > 0) {
|
|
728
919
|
homeIndex = (homeIndex - 1 + homeCount) % homeCount;
|
|
@@ -732,8 +923,8 @@ void handleUp(bool longPress) {
|
|
|
732
923
|
} else if (uiState == UI_BURN_READY) {
|
|
733
924
|
if (editingSpeed) {
|
|
734
925
|
burnSpeedIndex = (burnSpeedIndex - 1 + SPEED_COUNT) % SPEED_COUNT;
|
|
735
|
-
|
|
736
|
-
|
|
926
|
+
Out.print("SPEED:");
|
|
927
|
+
Out.println(SPEED_MODES[burnSpeedIndex]);
|
|
737
928
|
drawBurnReady();
|
|
738
929
|
} else {
|
|
739
930
|
burnModeIndex = (burnModeIndex - 1 + BURN_COUNT) % BURN_COUNT;
|
|
@@ -742,23 +933,24 @@ void handleUp(bool longPress) {
|
|
|
742
933
|
}
|
|
743
934
|
} else if (uiState == UI_PLAY) {
|
|
744
935
|
if (audioPlayMode) {
|
|
745
|
-
|
|
936
|
+
Out.println(displayRotation == 0 ? (longPress ? "REW:BIG" : "REW:10") : (longPress ? "FF:BIG" : "FF:10"));
|
|
746
937
|
return;
|
|
747
938
|
}
|
|
748
939
|
rewSpeedIndex = 0;
|
|
749
940
|
if (longPress) {
|
|
750
|
-
|
|
941
|
+
Out.println("FF:BIG");
|
|
751
942
|
} else {
|
|
752
943
|
ffSpeedIndex = (ffSpeedIndex + 1) % 4;
|
|
753
944
|
int seek_sec[] = {10, 20, 30, 60};
|
|
754
|
-
|
|
755
|
-
|
|
945
|
+
Out.print("FF:");
|
|
946
|
+
Out.println(seek_sec[ffSpeedIndex]);
|
|
756
947
|
}
|
|
757
948
|
}
|
|
758
949
|
}
|
|
759
950
|
|
|
760
951
|
void handleDown(bool longPress) {
|
|
761
952
|
wakeDisplay();
|
|
953
|
+
lastInputTime = millis();
|
|
762
954
|
if (uiState == UI_HOME) {
|
|
763
955
|
if (homeCount > 0) {
|
|
764
956
|
homeIndex = (homeIndex + 1) % homeCount;
|
|
@@ -768,8 +960,8 @@ void handleDown(bool longPress) {
|
|
|
768
960
|
} else if (uiState == UI_BURN_READY) {
|
|
769
961
|
if (editingSpeed) {
|
|
770
962
|
burnSpeedIndex = (burnSpeedIndex + 1) % SPEED_COUNT;
|
|
771
|
-
|
|
772
|
-
|
|
963
|
+
Out.print("SPEED:");
|
|
964
|
+
Out.println(SPEED_MODES[burnSpeedIndex]);
|
|
773
965
|
drawBurnReady();
|
|
774
966
|
} else {
|
|
775
967
|
burnModeIndex = (burnModeIndex + 1) % BURN_COUNT;
|
|
@@ -778,22 +970,66 @@ void handleDown(bool longPress) {
|
|
|
778
970
|
}
|
|
779
971
|
} else if (uiState == UI_PLAY) {
|
|
780
972
|
if (audioPlayMode) {
|
|
781
|
-
|
|
973
|
+
Out.println(displayRotation == 0 ? (longPress ? "FF:BIG" : "FF:10") : (longPress ? "REW:BIG" : "REW:10"));
|
|
782
974
|
return;
|
|
783
975
|
}
|
|
784
976
|
ffSpeedIndex = 0;
|
|
785
977
|
if (longPress) {
|
|
786
|
-
|
|
978
|
+
Out.println("REW:BIG");
|
|
787
979
|
} else {
|
|
788
980
|
rewSpeedIndex = (rewSpeedIndex + 1) % 4;
|
|
789
981
|
int seek_sec[] = {10, 20, 30, 60};
|
|
790
|
-
|
|
791
|
-
|
|
982
|
+
Out.print("REW:");
|
|
983
|
+
Out.println(seek_sec[rewSpeedIndex]);
|
|
792
984
|
}
|
|
793
985
|
}
|
|
794
986
|
}
|
|
795
987
|
|
|
796
988
|
void loop() {
|
|
989
|
+
if (!wifiInitDone && millis() > 3000) {
|
|
990
|
+
wifiInitDone = true;
|
|
991
|
+
initWiFi();
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (credsJustSaved) {
|
|
995
|
+
drawWifiReset();
|
|
996
|
+
delay(600);
|
|
997
|
+
ESP.restart();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (portalActive) {
|
|
1001
|
+
wm.process();
|
|
1002
|
+
if (WiFi.status() == WL_CONNECTED) onWifiUp();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (wifiConnected && WiFi.status() != WL_CONNECTED) {
|
|
1006
|
+
if (wifiDropAt == 0) wifiDropAt = millis();
|
|
1007
|
+
else if (millis() - wifiDropAt > WIFI_RETRY_MS) {
|
|
1008
|
+
wifiDropAt = millis();
|
|
1009
|
+
WiFi.disconnect();
|
|
1010
|
+
WiFi.begin();
|
|
1011
|
+
}
|
|
1012
|
+
} else if (wifiConnected) {
|
|
1013
|
+
wifiDropAt = 0;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (wifiConnected && WiFi.status() == WL_CONNECTED) {
|
|
1017
|
+
if (otaEnabled) ArduinoOTA.handle();
|
|
1018
|
+
if (tcpServer.hasClient()) {
|
|
1019
|
+
if (tcpClient) tcpClient.stop();
|
|
1020
|
+
tcpClient = tcpServer.available();
|
|
1021
|
+
Out.setClient(&tcpClient);
|
|
1022
|
+
}
|
|
1023
|
+
if (tcpClient && !tcpClient.connected()) {
|
|
1024
|
+
tcpClient.stop();
|
|
1025
|
+
Out.setClient(nullptr);
|
|
1026
|
+
}
|
|
1027
|
+
if (tcpClient && tcpClient.available()) {
|
|
1028
|
+
String tmsg = tcpClient.readStringUntil('\n');
|
|
1029
|
+
if (tmsg.length() > 0) parseMessage(tmsg);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
797
1033
|
if (Serial.available()) {
|
|
798
1034
|
String msg = Serial.readStringUntil('\n');
|
|
799
1035
|
parseMessage(msg);
|
|
@@ -814,8 +1050,8 @@ void loop() {
|
|
|
814
1050
|
int nextVolume = readPercent();
|
|
815
1051
|
if (abs(nextVolume - playVolume) >= 3) {
|
|
816
1052
|
playVolume = nextVolume;
|
|
817
|
-
|
|
818
|
-
|
|
1053
|
+
Out.print("POT:");
|
|
1054
|
+
Out.println(playVolume);
|
|
819
1055
|
drawPlay();
|
|
820
1056
|
}
|
|
821
1057
|
}
|
|
@@ -825,11 +1061,26 @@ void loop() {
|
|
|
825
1061
|
if (sel && !selectDown && millis() - selectLastDebounce > DEBOUNCE_MS) {
|
|
826
1062
|
selectDown = true;
|
|
827
1063
|
selectDownAt = millis();
|
|
1064
|
+
wifiResetArmed = false;
|
|
1065
|
+
}
|
|
1066
|
+
// 10s hold on HOME/SETUP/STANDBY = wipe Wi-Fi creds + reboot to portal.
|
|
1067
|
+
if (sel && selectDown && !wifiResetArmed &&
|
|
1068
|
+
(uiState == UI_HOME || uiState == UI_SETUP || uiState == UI_STANDBY) &&
|
|
1069
|
+
millis() - selectDownAt >= WIFI_RESET_HOLD_MS) {
|
|
1070
|
+
wifiResetArmed = true;
|
|
1071
|
+
Out.println("WiFi: creds wiped, rebooting");
|
|
1072
|
+
clearCreds();
|
|
1073
|
+
drawWifiReset();
|
|
1074
|
+
delay(800);
|
|
1075
|
+
ESP.restart();
|
|
828
1076
|
}
|
|
829
1077
|
if (!sel && selectDown) {
|
|
830
1078
|
selectDown = false;
|
|
831
1079
|
selectLastDebounce = millis();
|
|
832
|
-
|
|
1080
|
+
if (!wifiResetArmed) {
|
|
1081
|
+
handleSelectPress(millis() - selectDownAt >= LONG_PRESS_MS);
|
|
1082
|
+
}
|
|
1083
|
+
wifiResetArmed = false;
|
|
833
1084
|
}
|
|
834
1085
|
}
|
|
835
1086
|
|
|
@@ -878,8 +1129,9 @@ void loop() {
|
|
|
878
1129
|
if (!displayBlank) drawLoading();
|
|
879
1130
|
}
|
|
880
1131
|
|
|
881
|
-
if (displayOk &&
|
|
882
|
-
(
|
|
1132
|
+
if (displayOk && !displayBlank &&
|
|
1133
|
+
(uiState == UI_HOME || uiState == UI_STANDBY) &&
|
|
1134
|
+
(long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
|
|
883
1135
|
display.ssd1306_command(0xAE);
|
|
884
1136
|
displayBlank = true;
|
|
885
1137
|
digitalWrite(LED_POWER_PIN, LOW);
|
|
@@ -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"
|