discstation 0.1.26 → 0.1.28

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.
@@ -24,8 +24,6 @@
24
24
  #define BTN_SELECT_PIN 21 // D3
25
25
  #define BTN_UP_PIN 20 // D9/MISO
26
26
  #define BTN_DOWN_PIN 18 // D10/MOSI
27
- #define LED_POWER_PIN 15 // Built-in user LED
28
- #define LED_ACT_PIN 16 // D6/TX
29
27
  #define POT_PIN 0 // D0/A0 (ADC)
30
28
 
31
29
  #define DEBOUNCE_MS 50
@@ -35,11 +33,10 @@
35
33
  #define STANDBY_BLANK_MS 60000
36
34
  #define IDLE_BLANK_MS 45000 // blank the OLED after this long with no input on HOME/STANDBY
37
35
  #define PING_TIMEOUT_MS 30000
38
- #define LED_BLINK_MS 250
39
36
 
40
37
  #define WIFI_RESET_HOLD_MS 10000
41
38
  #define WIFI_CONNECT_TIMEOUT_MS 18000
42
- #define WIFI_RETRY_MS 30000
39
+ #define WIFI_RETRY_MS 15000
43
40
 
44
41
  #define MAX_HOME_MODES 5
45
42
  #define BURN_COUNT 4
@@ -237,8 +234,6 @@ unsigned long downLastDebounce = 0;
237
234
  int ffSpeedIndex = 0;
238
235
  int rewSpeedIndex = 0;
239
236
 
240
- bool actLedState = false;
241
- unsigned long lastLedBlink = 0;
242
237
  int homeIndex = 0;
243
238
  int burnModeIndex = 0;
244
239
  int burnSpeedIndex = 0;
@@ -550,8 +545,10 @@ void drawLoading() {
550
545
  display.display();
551
546
  }
552
547
 
553
- void wakeDisplay() {
554
- if (!displayBlank || !displayOk) return;
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;
555
552
  display.ssd1306_command(0xAF);
556
553
  displayBlank = false;
557
554
  switch (uiState) {
@@ -566,6 +563,7 @@ void wakeDisplay() {
566
563
  case UI_LOADING: drawLoading(); break;
567
564
  case UI_SETUP: drawSetup(); break;
568
565
  }
566
+ return true;
569
567
  }
570
568
 
571
569
  void drawPlay() {
@@ -660,6 +658,8 @@ void parseMessage(String msg) {
660
658
  line1 = msg.substring(5);
661
659
  line2 = "Playing disc";
662
660
  playVolume = readPercent();
661
+ Out.print("POT:"); // push the knob's current level so playback starts at it
662
+ Out.println(playVolume);
663
663
  drawPlay();
664
664
 
665
665
  } else if (msg.startsWith("PLAY_MODE:")) {
@@ -792,10 +792,6 @@ void setup() {
792
792
  pinMode(BTN_SELECT_PIN, INPUT_PULLUP);
793
793
  pinMode(BTN_UP_PIN, INPUT_PULLUP);
794
794
  pinMode(BTN_DOWN_PIN, INPUT_PULLUP);
795
- pinMode(LED_POWER_PIN, OUTPUT);
796
- pinMode(LED_ACT_PIN, OUTPUT);
797
- digitalWrite(LED_POWER_PIN, HIGH);
798
- digitalWrite(LED_ACT_PIN, LOW);
799
795
  pinMode(POT_PIN, INPUT);
800
796
  analogReadResolution(12);
801
797
 
@@ -812,13 +808,6 @@ void setup() {
812
808
  delay(3000);
813
809
  }
814
810
 
815
- for (int i = 0; i < 3; i++) {
816
- digitalWrite(LED_ACT_PIN, HIGH);
817
- delay(150);
818
- digitalWrite(LED_ACT_PIN, LOW);
819
- delay(150);
820
- }
821
-
822
811
  drawStandby();
823
812
  lastMsgTime = millis();
824
813
  lastInputTime = millis();
@@ -826,7 +815,7 @@ void setup() {
826
815
  }
827
816
 
828
817
  void handleSelectPress(bool longPress) {
829
- wakeDisplay();
818
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
830
819
  lastInputTime = millis();
831
820
  if (uiState == UI_HOME) {
832
821
  if (longPress) {
@@ -912,7 +901,7 @@ void handleSelectPress(bool longPress) {
912
901
  }
913
902
 
914
903
  void handleUp(bool longPress) {
915
- wakeDisplay();
904
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
916
905
  lastInputTime = millis();
917
906
  if (uiState == UI_HOME) {
918
907
  if (homeCount > 0) {
@@ -949,7 +938,7 @@ void handleUp(bool longPress) {
949
938
  }
950
939
 
951
940
  void handleDown(bool longPress) {
952
- wakeDisplay();
941
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
953
942
  lastInputTime = millis();
954
943
  if (uiState == UI_HOME) {
955
944
  if (homeCount > 0) {
@@ -1134,10 +1123,6 @@ void loop() {
1134
1123
  (long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
1135
1124
  display.ssd1306_command(0xAE);
1136
1125
  displayBlank = true;
1137
- digitalWrite(LED_POWER_PIN, LOW);
1138
- }
1139
- if (!displayBlank) {
1140
- digitalWrite(LED_POWER_PIN, HIGH);
1141
1126
  }
1142
1127
 
1143
1128
  if (uiState != UI_DISCONNECTED &&
@@ -1145,28 +1130,5 @@ void loop() {
1145
1130
  drawDisconnected();
1146
1131
  }
1147
1132
 
1148
- {
1149
- bool ledOn = false;
1150
- if (uiState == UI_BURN_READY || uiState == UI_PLAY || uiState == UI_WAITING || uiState == UI_IP) {
1151
- ledOn = true;
1152
- } else if (uiState == UI_STATUS) {
1153
- if (progressPercent >= 0) {
1154
- ledOn = (millis() - lastLedBlink > LED_BLINK_MS);
1155
- if (ledOn) {
1156
- lastLedBlink = millis();
1157
- actLedState = !actLedState;
1158
- }
1159
- ledOn = actLedState;
1160
- } else {
1161
- ledOn = true;
1162
- }
1163
- } else if (uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_LOADING) {
1164
- ledOn = false;
1165
- } else if (uiState == UI_DISCONNECTED) {
1166
- ledOn = true;
1167
- }
1168
- digitalWrite(LED_ACT_PIN, ledOn ? HIGH : LOW);
1169
- }
1170
-
1171
1133
  delay(20);
1172
1134
  }
@@ -25,8 +25,6 @@
25
25
  #define BTN_SELECT_PIN 14
26
26
  #define BTN_UP_PIN 13
27
27
  #define BTN_DOWN_PIN 15
28
- #define LED_POWER_PIN 32
29
- #define LED_ACT_PIN 33
30
28
  #define POT_PIN 34
31
29
 
32
30
  #define DEBOUNCE_MS 50
@@ -36,11 +34,10 @@
36
34
  #define STANDBY_BLANK_MS 60000
37
35
  #define IDLE_BLANK_MS 45000 // blank the OLED after this long with no input on HOME/STANDBY
38
36
  #define PING_TIMEOUT_MS 30000
39
- #define LED_BLINK_MS 250
40
37
 
41
38
  #define WIFI_RESET_HOLD_MS 10000 // hold SELECT this long on HOME to wipe Wi-Fi creds
42
39
  #define WIFI_CONNECT_TIMEOUT_MS 18000 // give a stored-creds join this long before falling to the portal
43
- #define WIFI_RETRY_MS 30000 // if a live link drops, force a re-join after this
40
+ #define WIFI_RETRY_MS 15000 // if a live link drops, force a re-join after this
44
41
 
45
42
  #define MAX_HOME_MODES 5
46
43
  #define BURN_COUNT 4
@@ -241,9 +238,6 @@ unsigned long downLastDebounce = 0;
241
238
  int ffSpeedIndex = 0;
242
239
  int rewSpeedIndex = 0;
243
240
 
244
- // LED state
245
- bool actLedState = false;
246
- unsigned long lastLedBlink = 0;
247
241
  int homeIndex = 0;
248
242
  int burnModeIndex = 0;
249
243
  int burnSpeedIndex = 0;
@@ -537,8 +531,10 @@ void drawDisconnected() {
537
531
  display.display();
538
532
  }
539
533
 
540
- void wakeDisplay() {
541
- if (!displayBlank || !displayOk) return;
534
+ // Returns true if this call actually woke a blanked screen - callers use that
535
+ // to swallow the wake press so it doesn't also trigger a menu action.
536
+ bool wakeDisplay() {
537
+ if (!displayBlank || !displayOk) return false;
542
538
  display.ssd1306_command(0xAF);
543
539
  displayBlank = false;
544
540
  switch (uiState) {
@@ -552,6 +548,7 @@ void wakeDisplay() {
552
548
  case UI_DISCONNECTED: drawDisconnected(); break;
553
549
  case UI_SETUP: drawSetup(); break;
554
550
  }
551
+ return true;
555
552
  }
556
553
 
557
554
  void drawPlay() {
@@ -646,6 +643,8 @@ void parseMessage(String msg) {
646
643
  line1 = msg.substring(5);
647
644
  line2 = "Playing disc";
648
645
  playVolume = readPercent();
646
+ Out.print("POT:"); // push the knob's current level so playback starts at it
647
+ Out.println(playVolume);
649
648
  drawPlay();
650
649
 
651
650
  } else if (msg.startsWith("PLAY_MODE:")) {
@@ -757,10 +756,6 @@ void setup() {
757
756
  pinMode(BTN_SELECT_PIN, INPUT_PULLUP);
758
757
  pinMode(BTN_UP_PIN, INPUT_PULLUP);
759
758
  pinMode(BTN_DOWN_PIN, INPUT_PULLUP);
760
- pinMode(LED_POWER_PIN, OUTPUT);
761
- pinMode(LED_ACT_PIN, OUTPUT);
762
- digitalWrite(LED_POWER_PIN, HIGH);
763
- digitalWrite(LED_ACT_PIN, LOW);
764
759
  pinMode(POT_PIN, INPUT);
765
760
  analogReadResolution(12);
766
761
  analogSetPinAttenuation(POT_PIN, ADC_11db);
@@ -778,14 +773,6 @@ void setup() {
778
773
  delay(3000);
779
774
  }
780
775
 
781
- // Blink activity LED 3x to confirm it works
782
- for (int i = 0; i < 3; i++) {
783
- digitalWrite(LED_ACT_PIN, HIGH);
784
- delay(150);
785
- digitalWrite(LED_ACT_PIN, LOW);
786
- delay(150);
787
- }
788
-
789
776
  drawStandby();
790
777
  lastMsgTime = millis();
791
778
  lastInputTime = millis();
@@ -793,7 +780,7 @@ void setup() {
793
780
  }
794
781
 
795
782
  void handleSelectPress(bool longPress) {
796
- wakeDisplay();
783
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
797
784
  lastInputTime = millis();
798
785
  if (uiState == UI_HOME) {
799
786
  if (longPress) {
@@ -883,7 +870,7 @@ void handleSelectPress(bool longPress) {
883
870
  }
884
871
 
885
872
  void handleUp(bool longPress) {
886
- wakeDisplay();
873
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
887
874
  lastInputTime = millis();
888
875
  if (uiState == UI_HOME) {
889
876
  if (homeCount > 0) {
@@ -923,7 +910,7 @@ void handleUp(bool longPress) {
923
910
  }
924
911
 
925
912
  void handleDown(bool longPress) {
926
- wakeDisplay();
913
+ if (wakeDisplay()) { lastInputTime = millis(); return; } // wake-only press, swallow the action
927
914
  lastInputTime = millis();
928
915
  if (uiState == UI_HOME) {
929
916
  if (homeCount > 0) {
@@ -1115,10 +1102,6 @@ void loop() {
1115
1102
  (long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
1116
1103
  display.ssd1306_command(0xAE);
1117
1104
  displayBlank = true;
1118
- digitalWrite(LED_POWER_PIN, LOW);
1119
- }
1120
- if (!displayBlank) {
1121
- digitalWrite(LED_POWER_PIN, HIGH);
1122
1105
  }
1123
1106
 
1124
1107
  // --- PING timeout (disconnected) ---
@@ -1127,29 +1110,5 @@ void loop() {
1127
1110
  drawDisconnected();
1128
1111
  }
1129
1112
 
1130
- // --- Activity LED ---
1131
- {
1132
- bool ledOn = false;
1133
- if (uiState == UI_BURN_READY || uiState == UI_PLAY || uiState == UI_WAITING || uiState == UI_IP) {
1134
- ledOn = true;
1135
- } else if (uiState == UI_STATUS) {
1136
- if (progressPercent >= 0) {
1137
- ledOn = (millis() - lastLedBlink > LED_BLINK_MS);
1138
- if (ledOn) {
1139
- lastLedBlink = millis();
1140
- actLedState = !actLedState;
1141
- }
1142
- ledOn = actLedState;
1143
- } else {
1144
- ledOn = true;
1145
- }
1146
- } else if (uiState == UI_HOME || uiState == UI_STANDBY) {
1147
- ledOn = false;
1148
- } else if (uiState == UI_DISCONNECTED) {
1149
- ledOn = true;
1150
- }
1151
- digitalWrite(LED_ACT_PIN, ledOn ? HIGH : LOW);
1152
- }
1153
-
1154
1113
  delay(20);
1155
1114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discstation",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "DiscStation optical-media appliance host — one cross-OS installer",
5
5
  "bin": {
6
6
  "discstation-setup": "scripts/setup.mjs",
@@ -122,7 +122,7 @@ class TcpSerial:
122
122
  A dead link raises serial.SerialException from in_waiting/write, which is
123
123
  what check_serial_alive() / main()'s reconnect loop already expect."""
124
124
 
125
- def __init__(self, host, port=2323, connect_timeout=5):
125
+ def __init__(self, host, port=2323, connect_timeout=3):
126
126
  if host.count(":") == 1 and not host.startswith("["): # "ip:port"
127
127
  host, _, p = host.rpartition(":")
128
128
  if p.isdigit():
@@ -72,6 +72,7 @@ def serial_port():
72
72
  _REMOTE_MDNS_TYPE = "_discstation._tcp.local."
73
73
  _REMOTE_DEFAULT_HOST = "discstation.local"
74
74
  _remote_cache = {"at": 0.0, "value": None}
75
+ _remote_browser = {} # {"zc": Zeroconf|None, "found": {"addr": ...}} - built once, lives for the process
75
76
 
76
77
 
77
78
  def remote_host():
@@ -79,8 +80,10 @@ def remote_host():
79
80
 
80
81
  `DISC_REMOTE_HOST` unset / `auto` (default) -> mDNS browse for the
81
82
  firmware's `_discstation._tcp` advert, falling back to resolving
82
- `discstation.local` via the OS resolver. Result cached ~60s so the
83
- reconnect loop / hot-swap poll don't hammer mDNS.
83
+ `discstation.local` via the OS resolver. A hit is cached 60s (stable
84
+ address, don't hammer mDNS); a miss only ~8s, so a host that just
85
+ rejoined the appliance's network reconnects promptly instead of
86
+ waiting out a full minute.
84
87
  `DISC_REMOTE_HOST=<ip|host>` -> that address, verbatim, instantly.
85
88
  `DISC_REMOTE_HOST=off` / `none` / `0` -> None (never look; USB / web only).
86
89
 
@@ -92,52 +95,60 @@ def remote_host():
92
95
  if setting and setting != "auto":
93
96
  return (os.environ.get("DISC_REMOTE_HOST") or "").strip()
94
97
 
95
- if time.time() - _remote_cache["at"] < 60:
96
- return _remote_cache["value"]
98
+ addr = _mdns_addr()
99
+ if addr:
100
+ return addr
97
101
 
98
- value = _discover_remote()
102
+ # zeroconf absent / nothing advertised yet -> OS resolver for discstation.local.
103
+ # Cache it: a hit 60s, a miss ~8s (so a host that just rejoined reconnects fast).
104
+ ttl = 60 if _remote_cache["value"] else 8
105
+ if time.time() - _remote_cache["at"] < ttl:
106
+ return _remote_cache["value"]
107
+ try:
108
+ value = socket.gethostbyname(_REMOTE_DEFAULT_HOST)
109
+ except OSError:
110
+ value = None
99
111
  _remote_cache["at"] = time.time()
100
112
  _remote_cache["value"] = value
101
113
  return value
102
114
 
103
115
 
104
- def _discover_remote():
105
- try:
106
- from zeroconf import Zeroconf, ServiceBrowser
107
- except ImportError:
108
- Zeroconf = None
109
- if Zeroconf is not None:
116
+ def _mdns_addr():
117
+ """Latest IPv4 advertised for the firmware's _discstation._tcp service, or None.
118
+
119
+ A single background ServiceBrowser is started on the first call and reused for
120
+ the whole process. (Creating a fresh Zeroconf per lookup - this runs every few
121
+ seconds while the link is down - leaked sockets and engine threads until the
122
+ process hit its fd limit and every transport died.)"""
123
+ if not _remote_browser:
124
+ try:
125
+ from zeroconf import Zeroconf, ServiceBrowser
126
+ except ImportError:
127
+ _remote_browser["zc"] = None
128
+ return None
110
129
  found = {}
111
130
 
112
131
  class _Listener:
113
- def add_service(self, zc, type_, name):
114
- info = zc.get_service_info(type_, name, timeout=1500)
132
+ def _seen(self, zc, type_, name):
133
+ info = zc.get_service_info(type_, name, timeout=1000)
115
134
  if info:
116
135
  for addr in info.parsed_addresses():
117
136
  if "." in addr: # IPv4
118
137
  found["addr"] = addr
119
138
  return
120
139
 
121
- update_service = add_service
140
+ add_service = _seen
141
+ update_service = _seen
122
142
 
123
- def remove_service(self, *a):
124
- pass
143
+ def remove_service(self, zc, type_, name):
144
+ found.pop("addr", None)
125
145
 
126
- zc = Zeroconf()
127
- try:
128
- ServiceBrowser(zc, _REMOTE_MDNS_TYPE, _Listener())
129
- deadline = time.time() + 2.5
130
- while time.time() < deadline and "addr" not in found:
131
- time.sleep(0.1)
132
- finally:
133
- zc.close()
134
- if found.get("addr"):
135
- return found["addr"]
146
+ _remote_browser["found"] = found
147
+ _remote_browser["zc"] = Zeroconf()
148
+ ServiceBrowser(_remote_browser["zc"], _REMOTE_MDNS_TYPE, _Listener())
136
149
 
137
- try:
138
- return socket.gethostbyname(_REMOTE_DEFAULT_HOST)
139
- except OSError:
140
- return None
150
+ found = _remote_browser.get("found")
151
+ return found.get("addr") if found else None
141
152
 
142
153
 
143
154
  def _stable_serial_path(device):