feat: Implement NTP sync function with garbage collection and improve schedule handling in web server
reduce ram usage bascically. Fixes #22 (more garbage collection all it needed, but went further to try and cut more memory usage) Fixes #21 (Just forgot to already marke this one as completed. Possibly in this commit too)
This commit is contained in:
parent
3c2e936d56
commit
dae6971112
@ -636,6 +636,13 @@ class TempWebServer:
|
|||||||
def _get_status_page(self, sensors, ac_monitor, heater_monitor, schedule_monitor=None, show_success=False):
|
def _get_status_page(self, sensors, ac_monitor, heater_monitor, schedule_monitor=None, show_success=False):
|
||||||
"""Generate HTML status page."""
|
"""Generate HTML status page."""
|
||||||
print("DEBUG: Generating status page...")
|
print("DEBUG: Generating status page...")
|
||||||
|
|
||||||
|
# ===== FORCE GARBAGE COLLECTION BEFORE BIG ALLOCATION =====
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
print("DEBUG: Memory freed, {} bytes available".format(gc.mem_free()))
|
||||||
|
# ===== END GARBAGE COLLECTION =====
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get current temperatures (use cached values to avoid blocking)
|
# Get current temperatures (use cached values to avoid blocking)
|
||||||
inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
|
inside_temp = getattr(sensors.get('inside'), 'last_temp', None)
|
||||||
@ -1453,18 +1460,45 @@ class TempWebServer:
|
|||||||
|
|
||||||
# Build mode buttons based on current state
|
# Build mode buttons based on current state
|
||||||
if config.get('schedule_enabled'):
|
if config.get('schedule_enabled'):
|
||||||
|
# ===== NEW: Find active schedule =====
|
||||||
|
active_schedule_name = "None"
|
||||||
|
current_time = time.localtime()
|
||||||
|
current_minutes = current_time[3] * 60 + current_time[4]
|
||||||
|
|
||||||
|
# Sort schedules by time and find the active one
|
||||||
|
sorted_schedules = []
|
||||||
|
for schedule in schedules:
|
||||||
|
if schedule.get('time'):
|
||||||
|
try:
|
||||||
|
time_parts = schedule['time'].split(':')
|
||||||
|
schedule_minutes = int(time_parts[0]) * 60 + int(time_parts[1])
|
||||||
|
sorted_schedules.append((schedule_minutes, schedule))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sorted_schedules.sort()
|
||||||
|
|
||||||
|
# Find most recent schedule that has passed
|
||||||
|
for schedule_minutes, schedule in sorted_schedules:
|
||||||
|
if current_minutes >= schedule_minutes:
|
||||||
|
active_schedule_name = schedule.get('name', 'Unnamed')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no schedule found (before first one), use last from yesterday
|
||||||
|
if active_schedule_name == "None" and sorted_schedules:
|
||||||
|
active_schedule_name = sorted_schedules[-1][1].get('name', 'Unnamed')
|
||||||
|
# ===== END: Find active schedule =====
|
||||||
|
|
||||||
return """
|
return """
|
||||||
<form method="POST" action="/schedule" style="margin: 20px 0;">
|
<form method="POST" action="/schedule" style="margin: 20px 0;">
|
||||||
<div style="background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
|
<div style="background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
|
||||||
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">✅ Automatic Mode</div>
|
<div style="font-weight: bold; font-size: 18px; margin-bottom: 5px;">✅ Automatic Mode</div>
|
||||||
<div style="font-size: 14px; margin-bottom: 15px;">Temperatures adjust based on schedule</div>
|
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 10px;">Currently running: <strong>{active_schedule}</strong></div>
|
||||||
<div style="display: flex; gap: 10px; justify-content: center;">
|
<div style="font-size: 13px; opacity: 0.8;">Temperatures adjust based on schedule</div>
|
||||||
<button type="submit" name="mode_action" value="temporary_hold" style="padding: 10px 20px; background: #f39c12; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">⏸️ Temp Hold</button>
|
|
||||||
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">🛑 Perm Hold</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
"""
|
""".format(active_schedule=active_schedule_name)
|
||||||
elif config.get('permanent_hold', False):
|
elif config.get('permanent_hold', False):
|
||||||
return """
|
return """
|
||||||
<form method="POST" action="/schedule" style="margin: 20px 0;">
|
<form method="POST" action="/schedule" style="margin: 20px 0;">
|
||||||
@ -1472,7 +1506,7 @@ class TempWebServer:
|
|||||||
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">🛑 Permanent Hold</div>
|
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">🛑 Permanent Hold</div>
|
||||||
<div style="font-size: 14px; margin-bottom: 15px;">Manual control only - Schedules disabled</div>
|
<div style="font-size: 14px; margin-bottom: 15px;">Manual control only - Schedules disabled</div>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Enable Schedules</button>
|
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Resume Scheduling</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -1483,9 +1517,8 @@ class TempWebServer:
|
|||||||
<div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
|
<div style="background: linear-gradient(135deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
|
||||||
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">⏸️ Temporary Hold</div>
|
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">⏸️ Temporary Hold</div>
|
||||||
<div style="font-size: 14px; margin-bottom: 15px;">Manual override active</div>
|
<div style="font-size: 14px; margin-bottom: 15px;">Manual override active</div>
|
||||||
<div style="display: flex; gap: 10px; justify-content: center;">
|
<div style="text-align: center;">
|
||||||
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Resume</button>
|
<button type="submit" name="mode_action" value="resume" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">▶️ Resume Scheduling</button>
|
||||||
<button type="submit" name="mode_action" value="permanent_hold" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">🛑 Perm Hold</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
156
main.py
156
main.py
@ -4,9 +4,6 @@ import network # type: ignore
|
|||||||
import json
|
import json
|
||||||
import gc # type: ignore # ADD THIS - for garbage collection
|
import gc # type: ignore # ADD THIS - for garbage collection
|
||||||
import sys
|
import sys
|
||||||
import socket # type: ignore
|
|
||||||
import struct # type: ignore
|
|
||||||
import ntptime # type: ignore
|
|
||||||
|
|
||||||
# Initialize pins (LED light onboard)
|
# Initialize pins (LED light onboard)
|
||||||
led = Pin("LED", Pin.OUT)
|
led = Pin("LED", Pin.OUT)
|
||||||
@ -33,6 +30,53 @@ from scripts.web_server import TempWebServer
|
|||||||
from scripts.scheduler import ScheduleMonitor # NEW: Import scheduler for time-based temp changes
|
from scripts.scheduler import ScheduleMonitor # NEW: Import scheduler for time-based temp changes
|
||||||
from scripts.memory_check import check_memory_once # Just the function
|
from scripts.memory_check import check_memory_once # Just the function
|
||||||
|
|
||||||
|
# ===== NEW: NTP Sync Function (imports locally) =====
|
||||||
|
def sync_ntp_time(timezone_offset):
|
||||||
|
"""
|
||||||
|
Sync time with NTP server (imports modules locally to save RAM).
|
||||||
|
Returns True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Import ONLY when needed (freed by GC after function ends)
|
||||||
|
import socket # type: ignore
|
||||||
|
import struct # type: ignore
|
||||||
|
|
||||||
|
NTP_DELTA = 2208988800
|
||||||
|
host = "pool.ntp.org"
|
||||||
|
NTP_QUERY = bytearray(48)
|
||||||
|
NTP_QUERY[0] = 0x1B
|
||||||
|
|
||||||
|
# Create socket with timeout
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.settimeout(3.0) # 3-second timeout
|
||||||
|
|
||||||
|
try:
|
||||||
|
addr = socket.getaddrinfo(host, 123)[0][-1]
|
||||||
|
s.sendto(NTP_QUERY, addr)
|
||||||
|
msg = s.recv(48)
|
||||||
|
val = struct.unpack("!I", msg[40:44])[0]
|
||||||
|
utc_timestamp = val - NTP_DELTA
|
||||||
|
|
||||||
|
# Apply timezone offset
|
||||||
|
local_timestamp = utc_timestamp + (timezone_offset * 3600)
|
||||||
|
|
||||||
|
# Set RTC with local time
|
||||||
|
tm = time.gmtime(local_timestamp)
|
||||||
|
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("NTP sync failed: {}".format(e))
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
# Force garbage collection to free socket/struct modules
|
||||||
|
gc.collect()
|
||||||
|
# ===== END: NTP Sync Function =====
|
||||||
|
|
||||||
# ===== START: Configuration Loading =====
|
# ===== START: Configuration Loading =====
|
||||||
# Load saved settings from config.json file on Pico
|
# Load saved settings from config.json file on Pico
|
||||||
def load_config():
|
def load_config():
|
||||||
@ -161,46 +205,17 @@ if wifi and wifi.isconnected():
|
|||||||
web_server = TempWebServer(port=80)
|
web_server = TempWebServer(port=80)
|
||||||
web_server.start()
|
web_server.start()
|
||||||
|
|
||||||
# Attempt time sync with timeout (MicroPython compatible)
|
# ===== INITIAL NTP SYNC (using function) =====
|
||||||
ntp_synced = False
|
ntp_synced = False
|
||||||
try:
|
try:
|
||||||
def time_with_timeout():
|
ntp_synced = sync_ntp_time(TIMEZONE_OFFSET)
|
||||||
"""NTP time fetch with 3-second timeout."""
|
if ntp_synced:
|
||||||
NTP_DELTA = 2208988800
|
print("Time synced with NTP server (UTC{:+d})".format(TIMEZONE_OFFSET))
|
||||||
host = "pool.ntp.org"
|
else:
|
||||||
NTP_QUERY = bytearray(48)
|
print("Initial NTP sync failed, will retry in background...")
|
||||||
NTP_QUERY[0] = 0x1B
|
|
||||||
|
|
||||||
# Create socket with timeout
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.settimeout(3.0) # 3-second timeout
|
|
||||||
|
|
||||||
try:
|
|
||||||
addr = socket.getaddrinfo(host, 123)[0][-1]
|
|
||||||
s.sendto(NTP_QUERY, addr)
|
|
||||||
msg = s.recv(48)
|
|
||||||
s.close()
|
|
||||||
val = struct.unpack("!I", msg[40:44])[0]
|
|
||||||
return val - NTP_DELTA
|
|
||||||
finally:
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
# Get UTC time from NTP
|
|
||||||
utc_timestamp = time_with_timeout()
|
|
||||||
|
|
||||||
# Apply timezone offset
|
|
||||||
local_timestamp = utc_timestamp + (TIMEZONE_OFFSET * 3600)
|
|
||||||
|
|
||||||
# Set RTC with local time
|
|
||||||
tm = time.gmtime(local_timestamp)
|
|
||||||
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
|
|
||||||
|
|
||||||
ntp_synced = True
|
|
||||||
print("Time synced with NTP server (UTC{:+d})".format(TIMEZONE_OFFSET))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Initial NTP sync failed: {}".format(e))
|
print("Initial NTP sync error: {}".format(e))
|
||||||
print("Will retry in background...")
|
# ===== END: INITIAL NTP SYNC =====
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# WiFi connection failed
|
# WiFi connection failed
|
||||||
@ -349,6 +364,7 @@ print("Press Ctrl+C to stop\n")
|
|||||||
# Add NTP retry flags (before main loop)
|
# Add NTP retry flags (before main loop)
|
||||||
retry_ntp_attempts = 0
|
retry_ntp_attempts = 0
|
||||||
max_ntp_attempts = 5 # Try up to 5 times after initial failure
|
max_ntp_attempts = 5 # Try up to 5 times after initial failure
|
||||||
|
last_ntp_sync = time.time() # Track when we last synced
|
||||||
|
|
||||||
# ===== START: Main Loop =====
|
# ===== START: Main Loop =====
|
||||||
# Main monitoring loop (runs forever until Ctrl+C)
|
# Main monitoring loop (runs forever until Ctrl+C)
|
||||||
@ -360,46 +376,27 @@ while True:
|
|||||||
# Web requests
|
# Web requests
|
||||||
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
web_server.check_requests(sensors, ac_monitor, heater_monitor, schedule_monitor, config)
|
||||||
|
|
||||||
# Retry NTP sync every ~10s if not yet synced
|
# ===== RETRY NTP SYNC (if initial failed) =====
|
||||||
if not ntp_synced and retry_ntp_attempts < max_ntp_attempts:
|
if not ntp_synced and retry_ntp_attempts < max_ntp_attempts:
|
||||||
if retry_ntp_attempts == 0 or (time.time() % 10) < 1:
|
if time.time() % 10 < 1: # Every 10 seconds
|
||||||
try:
|
if sync_ntp_time(TIMEZONE_OFFSET):
|
||||||
import ntptime # type: ignore
|
ntp_synced = True
|
||||||
import socket
|
last_ntp_sync = time.time()
|
||||||
import struct
|
print("NTP sync succeeded on retry #{} (UTC{:+d})".format(retry_ntp_attempts + 1, TIMEZONE_OFFSET))
|
||||||
|
else:
|
||||||
# Quick NTP sync with timeout
|
|
||||||
NTP_DELTA = 2208988800
|
|
||||||
host = "pool.ntp.org"
|
|
||||||
NTP_QUERY = bytearray(48)
|
|
||||||
NTP_QUERY[0] = 0x1B
|
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.settimeout(3.0) # 3-second timeout
|
|
||||||
|
|
||||||
try:
|
|
||||||
addr = socket.getaddrinfo(host, 123)[0][-1]
|
|
||||||
s.sendto(NTP_QUERY, addr)
|
|
||||||
msg = s.recv(48)
|
|
||||||
val = struct.unpack("!I", msg[40:44])[0]
|
|
||||||
utc_timestamp = val - NTP_DELTA
|
|
||||||
|
|
||||||
# Apply timezone offset
|
|
||||||
local_timestamp = utc_timestamp + (TIMEZONE_OFFSET * 3600)
|
|
||||||
|
|
||||||
# Set RTC with local time
|
|
||||||
tm = time.gmtime(local_timestamp)
|
|
||||||
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
|
|
||||||
|
|
||||||
ntp_synced = True
|
|
||||||
print("NTP sync succeeded on retry #{} (UTC{:+d})".format(retry_ntp_attempts + 1, TIMEZONE_OFFSET))
|
|
||||||
finally:
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
retry_ntp_attempts += 1
|
retry_ntp_attempts += 1
|
||||||
print("NTP retry {} failed: {}".format(retry_ntp_attempts, e))
|
print("NTP retry {} failed".format(retry_ntp_attempts))
|
||||||
|
|
||||||
|
# ===== PERIODIC RE-SYNC (every 24 hours) =====
|
||||||
|
# Re-sync once a day to correct clock drift
|
||||||
|
if ntp_synced and (time.time() - last_ntp_sync) > 86400: # 24 hours in seconds
|
||||||
|
print("24-hour re-sync due...")
|
||||||
|
if sync_ntp_time(TIMEZONE_OFFSET):
|
||||||
|
last_ntp_sync = time.time()
|
||||||
|
print("Daily NTP re-sync successful")
|
||||||
|
else:
|
||||||
|
print("Daily NTP re-sync failed (will retry tomorrow)")
|
||||||
|
# ===== END: PERIODIC RE-SYNC =====
|
||||||
# Enable garbage collection to free memory
|
# Enable garbage collection to free memory
|
||||||
gc.collect()
|
gc.collect()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@ -424,6 +421,5 @@ while True:
|
|||||||
print("❌ Main loop error: {}".format(e))
|
print("❌ Main loop error: {}".format(e))
|
||||||
import sys
|
import sys
|
||||||
sys.print_exception(e)
|
sys.print_exception(e)
|
||||||
print("⚠️ Pausing 5 seconds before retrying...")
|
|
||||||
time.sleep(5) # Brief pause before retrying
|
time.sleep(5) # Brief pause before retrying
|
||||||
# ===== END: Main Loop =====
|
# ===== END: Main Loop =====
|
||||||
Loading…
x
Reference in New Issue
Block a user