From dae69711127210800fb34578719efdcebe33940f Mon Sep 17 00:00:00 2001 From: sickprodigy Date: Sun, 9 Nov 2025 11:29:18 -0500 Subject: [PATCH] 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) --- Scripts/web_server.py | 55 ++++++++++++--- main.py | 156 ++++++++++++++++++++---------------------- 2 files changed, 120 insertions(+), 91 deletions(-) diff --git a/Scripts/web_server.py b/Scripts/web_server.py index 59f23c5..52b10a0 100644 --- a/Scripts/web_server.py +++ b/Scripts/web_server.py @@ -636,6 +636,13 @@ class TempWebServer: def _get_status_page(self, sensors, ac_monitor, heater_monitor, schedule_monitor=None, show_success=False): """Generate HTML 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: # Get current temperatures (use cached values to avoid blocking) inside_temp = getattr(sensors.get('inside'), 'last_temp', None) @@ -1453,18 +1460,45 @@ class TempWebServer: # Build mode buttons based on current state 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 """
-
✅ Automatic Mode
-
Temperatures adjust based on schedule
-
- - -
+
✅ Automatic Mode
+
Currently running: {active_schedule}
+
Temperatures adjust based on schedule
- """ + """.format(active_schedule=active_schedule_name) elif config.get('permanent_hold', False): return """
@@ -1472,7 +1506,7 @@ class TempWebServer:
🛑 Permanent Hold
Manual control only - Schedules disabled
- +
@@ -1483,9 +1517,8 @@ class TempWebServer:
⏸️ Temporary Hold
Manual override active
-
- - +
+
diff --git a/main.py b/main.py index 33e1294..e87723c 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,6 @@ import network # type: ignore import json import gc # type: ignore # ADD THIS - for garbage collection import sys -import socket # type: ignore -import struct # type: ignore -import ntptime # type: ignore # Initialize pins (LED light onboard) 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.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 ===== # Load saved settings from config.json file on Pico def load_config(): @@ -161,46 +205,17 @@ if wifi and wifi.isconnected(): web_server = TempWebServer(port=80) web_server.start() - # Attempt time sync with timeout (MicroPython compatible) + # ===== INITIAL NTP SYNC (using function) ===== ntp_synced = False - try: - def time_with_timeout(): - """NTP time fetch with 3-second timeout.""" - 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) - 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)) - + try: + ntp_synced = sync_ntp_time(TIMEZONE_OFFSET) + if ntp_synced: + print("Time synced with NTP server (UTC{:+d})".format(TIMEZONE_OFFSET)) + else: + print("Initial NTP sync failed, will retry in background...") except Exception as e: - print("Initial NTP sync failed: {}".format(e)) - print("Will retry in background...") + print("Initial NTP sync error: {}".format(e)) + # ===== END: INITIAL NTP SYNC ===== else: # WiFi connection failed @@ -349,6 +364,7 @@ print("Press Ctrl+C to stop\n") # Add NTP retry flags (before main loop) retry_ntp_attempts = 0 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 ===== # Main monitoring loop (runs forever until Ctrl+C) @@ -360,46 +376,27 @@ while True: # Web requests 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 retry_ntp_attempts == 0 or (time.time() % 10) < 1: - try: - import ntptime # type: ignore - import socket - import struct - - # 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: + if time.time() % 10 < 1: # Every 10 seconds + if sync_ntp_time(TIMEZONE_OFFSET): + ntp_synced = True + last_ntp_sync = time.time() + print("NTP sync succeeded on retry #{} (UTC{:+d})".format(retry_ntp_attempts + 1, TIMEZONE_OFFSET)) + else: 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 gc.collect() time.sleep(0.1) @@ -424,6 +421,5 @@ while True: print("❌ Main loop error: {}".format(e)) import sys sys.print_exception(e) - print("⚠️ Pausing 5 seconds before retrying...") time.sleep(5) # Brief pause before retrying # ===== END: Main Loop ===== \ No newline at end of file