14 Commits

2 changed files with 147 additions and 42 deletions

64
boot.py Normal file
View File

@@ -0,0 +1,64 @@
"""
boot.py — runs before main.py on every ESP32 boot
Connects WiFi, runs OTA update, then hands off to main.py.
Keep this file as simple as possible — it is never OTA-updated itself
(it lives outside the repo folder) so bugs here require USB to fix.
"""
import gauge
import network
import gc
import utime
import sys
import ota
ota.load_config()
WIFI_SSID, WIFI_PASSWORD = ota.WIFI_SSID, ota.WIFI_PASSWORD
def _connect_wifi(timeout_s=20):
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(txpower=15)
if sta.isconnected():
return True
sta.connect(WIFI_SSID, WIFI_PASSWORD)
deadline = utime.time() + timeout_s
while not sta.isconnected():
if utime.time() > deadline:
return False
utime.sleep_ms(300)
return True
if WIFI_SSID is None:
print("[boot] No WiFi credentials — cannot connect, skipping OTA")
elif _connect_wifi():
ip = network.WLAN(network.STA_IF).ifconfig()[0]
print(f"[boot] WiFi connected — {ip}")
try:
ota.update()
except Exception as e:
print(f"[boot] OTA error: {e} — continuing with existing files")
sys.print_exception(e)
utime.sleep_ms(5000)
ota._fetch_commit_sha = None
ota._fetch_manifest = None
ota._fetch_dir = None
ota._api_get = None
ota._download = None
ota.urequests = None
del ota.urequests
del ota
gc.collect()
del sys.modules["ota"]
gc.collect()
else:
print("[boot] WiFi failed — skipping OTA, booting with existing files")
# main.py runs automatically after boot.py

125
gauge.py
View File

@@ -28,6 +28,16 @@ import gc
from umqtt.robust import MQTTClient
from machine import UART
# Activate WiFi driver before any heavy heap allocation so it can claim its
# contiguous DRAM block before the Python heap fragments the address space.
# Only activate if not already running (e.g. boot.py may have started it).
gc.collect()
_early_wlan = network.WLAN(network.STA_IF)
if not _early_wlan.active():
_early_wlan.active(True)
del _early_wlan
gc.collect()
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
@@ -524,46 +534,56 @@ _DEVICE = {
_wifi_check_interval_ms = 30000
_last_wifi_check = 0
_wifi_sta = None
_WIFI_CONNECT_ATTEMPTS = 3
def _reset_wifi_interface():
global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF)
if _wifi_sta.active():
_wifi_sta.active(False)
utime.sleep_ms(200)
_wifi_sta.active(True)
utime.sleep_ms(500)
def connect_wifi(ssid, password, timeout_s=15, force_reconnect=False):
global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF)
if force_reconnect:
try:
_wifi_sta.disconnect()
except Exception:
pass
try:
_wifi_sta.active(False)
utime.sleep_ms(250)
except Exception:
pass
_wifi_sta.active(True)
if _wifi_sta.isconnected() and not force_reconnect:
ip, mask, gw, dns = _wifi_sta.ifconfig()
info("WiFi already connected")
info(f" IP:{ip} mask:{mask} gw:{gw} dns:{dns}")
utime.sleep_ms(250)
return ip
info(f"WiFi connecting to '{ssid}' ...")
_wifi_sta.connect(ssid, password)
deadline = utime.time() + timeout_s
while not _wifi_sta.isconnected():
if utime.time() > deadline:
log_err(f"WiFi connect timeout after {timeout_s}s")
raise OSError("WiFi connect timeout")
utime.sleep_ms(200)
ip, mask, gw, dns = _wifi_sta.ifconfig()
mac = ":".join(f"{b:02x}" for b in _wifi_sta.config("mac"))
info("WiFi connected!")
info(f" SSID : {ssid}")
info(f" MAC : {mac}")
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
utime.sleep_ms(250)
return ip
last_error = None
for attempt in range(_WIFI_CONNECT_ATTEMPTS):
info(f"WiFi connecting to '{ssid}' (attempt {attempt + 1}/{_WIFI_CONNECT_ATTEMPTS}) ...")
_reset_wifi_interface()
try:
_wifi_sta.connect(ssid, password)
deadline = utime.time() + timeout_s
while not _wifi_sta.isconnected():
if utime.time() > deadline:
raise OSError("WiFi connect timeout")
utime.sleep_ms(250)
ip, mask, gw, dns = _wifi_sta.ifconfig()
mac = ":".join(f"{b:02x}" for b in _wifi_sta.config("mac"))
info("WiFi connected!")
info(f" SSID : {ssid}")
info(f" MAC : {mac}")
info(f" IP : {ip} mask:{mask} gw:{gw} dns:{dns}")
utime.sleep_ms(500)
return ip
except Exception as e:
last_error = e
log_err(f"WiFi connect attempt {attempt + 1} failed: {e}")
utime.sleep_ms(1000)
raise last_error
def check_wifi():
@@ -581,15 +601,7 @@ def check_wifi():
log_err("WiFi lost connection — attempting reconnect...")
try:
_wifi_sta.active(True)
_wifi_sta.connect(WIFI_SSID, WIFI_PASSWORD)
deadline = utime.time() + 15
while not _wifi_sta.isconnected():
if utime.time() > deadline:
log_err("WiFi reconnect timeout")
return
utime.sleep_ms(200)
ip, mask, gw, dns = _wifi_sta.ifconfig()
ip = connect_wifi(WIFI_SSID, WIFI_PASSWORD, timeout_s=15, force_reconnect=True)
info(f"WiFi reconnected! IP:{ip}")
except Exception as e:
log_err(f"WiFi reconnect failed: {e}")
@@ -874,6 +886,7 @@ def connect_mqtt():
info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT} ...")
last_error = None
for attempt in range(3):
gc.collect()
try:
if client_ref is not None:
try:
@@ -897,7 +910,12 @@ def connect_mqtt():
return
except Exception as e:
last_error = e
log_err(f"MQTT connect attempt {attempt + 1} failed: {e}")
log_err(f"MQTT connect attempt {attempt + 1} failed: {type(e).__name__}: {e}")
try:
client.sock.close()
except Exception:
pass
gc.collect()
utime.sleep_ms(1000)
_mqtt_connected = False
@@ -909,7 +927,7 @@ _last_mqtt_check = 0
_discovery_queue = []
_discovery_idx = 0
_last_discovery_ms = 0
_DISCOVERY_INTERVAL_MS = 200
_DISCOVERY_INTERVAL_MS = 350
def check_mqtt():
@@ -953,6 +971,11 @@ def check_mqtt():
return True
except Exception as e2:
log_err(f"MQTT reconnect attempt {attempt + 1} failed: {e2}")
try:
client_ref.sock.close()
except Exception:
pass
gc.collect()
utime.sleep_ms(2000)
log_err("MQTT reconnection failed after 3 attempts")
@@ -960,6 +983,7 @@ def check_mqtt():
def _publish_discovery_entity(client, topic, payload, log_msg):
gc.collect()
client.publish(topic, ujson.dumps(payload), retain=True)
info(log_msg)
@@ -1233,6 +1257,7 @@ def service_discovery():
if _last_discovery_ms and utime.ticks_diff(now, _last_discovery_ms) < _DISCOVERY_INTERVAL_MS:
return
gc.collect()
topic, payload, log_msg = _discovery_queue[_discovery_idx]
try:
if isinstance(payload, bytes):
@@ -1243,8 +1268,7 @@ def service_discovery():
log_err(f"Discovery publish failed for {topic}: {e}")
_discovery_idx += 1
_last_discovery_ms = utime.ticks_ms()
if (_discovery_idx & 3) == 0:
gc.collect()
gc.collect()
def publish_online(client):
@@ -1274,13 +1298,30 @@ def apply_motion_defaults():
def main():
gc.collect()
info("=" * 48)
info("Gauge MQTT controller starting")
info(f"Heap free: {gc.mem_free()} bytes")
info("=" * 48)
gc.collect()
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
connect_mqtt()
mqtt_attempts = 0
while True:
try:
connect_mqtt()
break
except Exception as e:
mqtt_attempts += 1
log_err(f"MQTT connect failed: {e} (attempt {mqtt_attempts})")
if mqtt_attempts % 3 == 0:
log_err("WiFi may be stale — forcing reconnect...")
try:
connect_wifi(WIFI_SSID, WIFI_PASSWORD, force_reconnect=True)
except Exception as we:
log_err(f"WiFi reconnect failed: {we}")
utime.sleep_ms(5000)
_subscribe_all(client_ref)
schedule_discovery()