Compare commits
3 Commits
dc19877132
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cef9e00819 | ||
|
|
7e3aca491c | ||
|
|
7bfdcd9bee |
203
src/main.py
203
src/main.py
@@ -6,6 +6,13 @@ import network
|
||||
from presets import Presets
|
||||
from utils import convert_and_reorder_colors
|
||||
import json
|
||||
import time
|
||||
import select
|
||||
import socket
|
||||
import ubinascii
|
||||
|
||||
BROADCAST_MAC = b"\xff\xff\xff\xff\xff\xff"
|
||||
CONTROLLER_TCP_PORT = 8765
|
||||
|
||||
settings = Settings()
|
||||
print(settings)
|
||||
@@ -13,7 +20,6 @@ print(settings)
|
||||
presets = Presets(settings["led_pin"], settings["num_leds"])
|
||||
presets.load(settings)
|
||||
presets.b = settings.get("brightness", 255)
|
||||
# Use the default preset name from settings (set via controller or defaults)
|
||||
default_preset = settings.get("default", "")
|
||||
if default_preset and default_preset in presets.presets:
|
||||
presets.select(default_preset)
|
||||
@@ -22,38 +28,32 @@ if default_preset and default_preset in presets.presets:
|
||||
wdt = WDT(timeout=10000)
|
||||
wdt.feed()
|
||||
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
sta_if.active(True)
|
||||
sta_if.disconnect()
|
||||
sta_if.config(channel=1)
|
||||
e = ESPNow()
|
||||
e.active(True)
|
||||
|
||||
# --- Controller JSON (bytes or str): parse v1, then apply -------------------------
|
||||
|
||||
|
||||
def as_dict(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def as_list(value):
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
|
||||
def receive_data(receiver):
|
||||
"""Read one ESPNow message and decode JSON dict payload."""
|
||||
def process_data(payload):
|
||||
"""Read one controller message; json.loads (bytes or str), then apply fields."""
|
||||
try:
|
||||
host, msg = receiver.recv()
|
||||
data = json.loads(msg)
|
||||
print(msg)
|
||||
data = as_dict(data)
|
||||
data = json.loads(payload)
|
||||
print(payload)
|
||||
if data.get("v", "") != "1":
|
||||
return None
|
||||
return data
|
||||
return
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return
|
||||
if "b" in data:
|
||||
apply_brightness(data)
|
||||
if "presets" in data:
|
||||
apply_presets(data)
|
||||
if "select" in data:
|
||||
apply_select(data)
|
||||
if "default" in data:
|
||||
apply_default(data)
|
||||
if "save" in data and ("presets" in data or "default" in data):
|
||||
presets.save()
|
||||
|
||||
|
||||
def apply_brightness(data):
|
||||
"""Apply and persist global brightness from payload."""
|
||||
try:
|
||||
presets.b = max(0, min(255, int(data["b"])))
|
||||
settings["brightness"] = presets.b
|
||||
@@ -62,10 +62,8 @@ def apply_brightness(data):
|
||||
|
||||
|
||||
def apply_presets(data):
|
||||
"""Create/update preset definitions from payload."""
|
||||
presets_map = as_dict(data.get("presets"))
|
||||
presets_map = data["presets"]
|
||||
for id, preset_data in presets_map.items():
|
||||
preset_data = as_dict(preset_data)
|
||||
if not preset_data:
|
||||
continue
|
||||
color_key = "c" if "c" in preset_data else ("colors" if "colors" in preset_data else None)
|
||||
@@ -81,23 +79,19 @@ def apply_presets(data):
|
||||
|
||||
|
||||
def apply_select(data):
|
||||
"""Select preset for this device when addressed."""
|
||||
select_map = as_dict(data.get("select"))
|
||||
select_map = data["select"]
|
||||
device_name = settings["name"]
|
||||
|
||||
# Case-sensitive: select key must match device name exactly.
|
||||
select_list = as_list(select_map.get(device_name))
|
||||
select_list = select_map.get(device_name, [])
|
||||
if not select_list:
|
||||
return
|
||||
preset_name = select_list[0]
|
||||
step = select_list[1] if len(select_list) > 1 else None
|
||||
if isinstance(preset_name, str):
|
||||
presets.select(preset_name, step=step)
|
||||
|
||||
|
||||
def apply_default(data):
|
||||
targets = as_list(data.get("targets"))
|
||||
default_name = data.get("default", "")
|
||||
targets = data.get("targets") or []
|
||||
default_name = data["default"]
|
||||
if (
|
||||
settings["name"] in targets
|
||||
and isinstance(default_name, str)
|
||||
@@ -106,19 +100,128 @@ def apply_default(data):
|
||||
settings["default"] = default_name
|
||||
|
||||
|
||||
# --- TCP framing (bytes) → process_data -------------------------------------------
|
||||
|
||||
|
||||
def tcp_append_and_drain_lines(buf, chunk):
|
||||
"""Return (new_buf, list of non-empty stripped line byte strings)."""
|
||||
buf += chunk
|
||||
lines = []
|
||||
while b"\n" in buf:
|
||||
line, buf = buf.split(b"\n", 1)
|
||||
line = line.strip()
|
||||
if line:
|
||||
lines.append(line)
|
||||
return buf, lines
|
||||
|
||||
|
||||
# --- Network + hello --------------------------------------------------------------
|
||||
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
sta_if.active(True)
|
||||
sta_if.config(pm=network.WLAN.PM_NONE)
|
||||
|
||||
mac = sta_if.config("mac")
|
||||
hello = {
|
||||
"v": "1",
|
||||
"device_name": settings.get("name", ""),
|
||||
"mac": ubinascii.hexlify(mac).decode().lower(),
|
||||
"type": "led",
|
||||
}
|
||||
hello_bytes = json.dumps(hello).encode("utf-8")
|
||||
hello_line = hello_bytes + b"\n"
|
||||
|
||||
if settings["transport_type"] == "espnow":
|
||||
sta_if.disconnect()
|
||||
sta_if.config(channel=settings.get("wifi_channel", 1))
|
||||
e = ESPNow()
|
||||
e.active(True)
|
||||
e.add_peer(BROADCAST_MAC)
|
||||
e.add_peer(mac)
|
||||
e.send(BROADCAST_MAC, hello_bytes)
|
||||
while True:
|
||||
wdt.feed()
|
||||
presets.tick()
|
||||
if e.any():
|
||||
if (data := receive_data(e)) is None:
|
||||
continue
|
||||
if "b" in data:
|
||||
apply_brightness(data)
|
||||
if "presets" in data:
|
||||
apply_presets(data)
|
||||
if "select" in data:
|
||||
apply_select(data)
|
||||
if "default" in data:
|
||||
apply_default(data)
|
||||
if "save" in data and ("presets" in data or "default" in data):
|
||||
presets.save()
|
||||
_peer, msg = e.recv()
|
||||
if msg:
|
||||
process_data(msg)
|
||||
presets.tick()
|
||||
wdt.feed()
|
||||
|
||||
elif settings["transport_type"] == "wifi":
|
||||
sta_if.connect(settings["ssid"], settings["password"])
|
||||
while not sta_if.isconnected():
|
||||
time.sleep(1)
|
||||
print(f"WiFi connected {sta_if.ifconfig()[0]}")
|
||||
reconnect_ms = 1000
|
||||
next_connect_at = 0
|
||||
client = None
|
||||
poller = None
|
||||
buf = b""
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
|
||||
if client is None and utime.ticks_diff(now, next_connect_at) >= 0:
|
||||
c = None
|
||||
try:
|
||||
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
c.connect((settings["server_ip"], CONTROLLER_TCP_PORT))
|
||||
c.send(hello_line)
|
||||
c.setblocking(False)
|
||||
p = select.poll()
|
||||
p.register(c, select.POLLIN)
|
||||
client = c
|
||||
poller = p
|
||||
buf = b""
|
||||
print("TCP connected")
|
||||
except Exception:
|
||||
if c is not None:
|
||||
try:
|
||||
c.close()
|
||||
except Exception:
|
||||
pass
|
||||
next_connect_at = utime.ticks_add(now, reconnect_ms)
|
||||
|
||||
if client is not None and poller is not None:
|
||||
try:
|
||||
events = poller.poll(0)
|
||||
except Exception:
|
||||
events = []
|
||||
|
||||
reconnect_needed = False
|
||||
for fd, event in events:
|
||||
if (event & select.POLLHUP) or (event & select.POLLERR):
|
||||
reconnect_needed = True
|
||||
break
|
||||
if event & select.POLLIN:
|
||||
try:
|
||||
chunk = client.recv(512)
|
||||
except OSError:
|
||||
reconnect_needed = True
|
||||
break
|
||||
|
||||
if not chunk:
|
||||
reconnect_needed = True
|
||||
break
|
||||
|
||||
buf, lines = tcp_append_and_drain_lines(buf, chunk)
|
||||
for raw_line in lines:
|
||||
process_data(raw_line)
|
||||
|
||||
if reconnect_needed:
|
||||
print("TCP disconnected, reconnecting...")
|
||||
try:
|
||||
poller.unregister(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
client = None
|
||||
poller = None
|
||||
buf = b""
|
||||
next_connect_at = utime.ticks_add(now, reconnect_ms)
|
||||
|
||||
presets.tick()
|
||||
wdt.feed()
|
||||
|
||||
@@ -21,6 +21,12 @@ class Settings(dict):
|
||||
self["debug"] = False
|
||||
self["default"] = "on"
|
||||
self["brightness"] = 32
|
||||
self["transport_type"] = "espnow"
|
||||
self["wifi_channel"] = 1
|
||||
# Wi-Fi + TCP to Pi: leave ssid empty for ESP-NOW-only.
|
||||
self["ssid"] = ""
|
||||
self["password"] = ""
|
||||
self["server_ip"] = ""
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
@@ -42,7 +48,6 @@ class Settings(dict):
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
|
||||
|
||||
def get_color_order(self, color_order):
|
||||
"""Convert color order string to tuple of hex string indices."""
|
||||
color_orders = {
|
||||
|
||||
Reference in New Issue
Block a user