import framebuf import network import ntptime import time from machine import Pin, I2C import ssd1306 # --- Configuration --- try: import config as Config except ImportError: print("ERROR: config.py not found.") print("Create config.py on the device with your Wi-Fi and NTP settings.") while True: time.sleep(2) # --- Hardware Setup --- i2c = I2C(0, sda=Pin(16), scl=Pin(17), freq=400000) oled = ssd1306.SSD1306_I2C(128, 64, i2c) def display_message(line1, line2=""): oled.fill(0) oled.text(line1, 0, 16) oled.text(line2, 0, 32) oled.show() # --- Wi-Fi Connection --- display_message("Connecting to", "Wi-Fi...") wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(Config.WIFI_SSID, Config.WIFI_PASSWORD) timeout = 20 while not wlan.isconnected() and timeout > 0: time.sleep(1) timeout -= 1 if not wlan.isconnected(): display_message("Wi-Fi Error", "Check settings") raise RuntimeError("Failed to connect to Wi-Fi") # --- NTP Synchronization --- display_message("Syncing time...", "NTP server") try: ntptime.settime() # Sets the Pico internal RTC to UTC except Exception as e: display_message("NTP Failed", str(e)) time.sleep(3) # Disconnect Wi-Fi to conserve power once time is synchronized wlan.active(False) def draw_scaled_text(display, text, x, y, scale=2): """ Renders text scaled up by an integer factor (scale=2 -> 16x16, scale=3 -> 24x24). Uses a temporary 1-bit FrameBuffer to capture the default 8x8 font. """ width = len(text) * 8 height = 8 # 1-bit MONO_HLSB buffer: (width * height) / 8 bytes buf = bytearray((width * height) // 8) fb = framebuf.FrameBuffer(buf, width, height, framebuf.MONO_HLSB) # Render string with built-in font into the scratch buffer fb.fill(0) fb.text(text, 0, 0, 1) # Read each pixel and draw a (scale x scale) block on the OLED for r in range(height): for c in range(width): if fb.pixel(c, r): display.fill_rect(x + (c * scale), y + (r * scale), scale, scale, 1) def calculate_x_spacing(text, scale=1): """ Calculates the amount of horizontal (x) spacing is needed to center a string. """ scaled_text = len(text) * scale return (128 - (scaled_text * 8)) // 2 def is_dst(year, month, day, hour_utc, weekday): """ Returns True if UTC time falls within US/Canada Daylight Saving Time: - Starts: 2nd Sunday in March at 02:00 local (07:00 UTC for UTC-5) - Ends: 1st Sunday in November at 02:00 local (06:00 UTC for UTC-4) Note: weekday is 0=Monday ... 6=Sunday. """ if not Config.DST_OBSERVED: return False # 1. Months completely inside or outside DST if month < 3 or month > 11: return False if 3 < month < 11: return True # 2. March (Starts 2nd Sunday) # The 2nd Sunday always falls between March 8 and March 14 if month == 3: # Find the day of the 2nd Sunday second_sunday = 14 - ((weekday - day + 14) % 7) if day < second_sunday: return False elif day > second_sunday: return True else: # On the transition day itself, 02:00 EST is 07:00 UTC return hour_utc >= 7 # 3. November (Ends 1st Sunday) # The 1st Sunday always falls between Nov 1 and Nov 7 if month == 11: first_sunday = 7 - ((weekday - day + 7) % 7) if day < first_sunday: return True elif day > first_sunday: return False else: # On the transition day itself, 02:00 EDT (01:00 EST) is 06:00 UTC return hour_utc < 6 return False # --- Main Loop --- DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] TIME_Y = 12 # Keep vertical placement consistent last_second = -1 while True: # 1. Fast, single-integer read from RTC current_sec = time.time() # 2. Gate everything behind the 1-second boundary if current_sec != last_second: last_second = current_sec # Unpack UTC once per second utc_year, utc_month, utc_day, utc_hour, _, _, utc_weekday, _ = time.gmtime(current_sec) # Run DST check dst_offset = 1 if is_dst(utc_year, utc_month, utc_day, utc_hour, utc_weekday) else 0 total_offset = Config.UTC_OFFSET + dst_offset # Compute local wall-clock components local_epoch = current_sec + (total_offset * 3600) year, month, day, hour_24, minute, second, weekday, _ = time.localtime(local_epoch) # --- String formatting and OLED rendering --- sep = ":" if second % 2 == 0 else " " if Config.USE_24_HOUR: time_str = f"{hour_24:02d}{sep}{minute:02d}" period = None else: hour_12 = hour_24 % 12 or 12 time_str = f"{hour_12}{sep}{minute:02d}" period = "AM" if hour_24 < 12 else "PM" oled.fill(0) time_x = calculate_x_spacing(time_str, scale=2) draw_scaled_text(oled, time_str, x=time_x, y=TIME_Y, scale=2) if period: oled.text(period, time_x + (len(time_str) * 16) + 4, TIME_Y + 8) date_str = f"{DAYS[weekday]} {year}-{month:02d}-{day:02d}" oled.text(date_str, calculate_x_spacing(date_str), 42) oled.show() # Fast poll to catch the next second boundary immediately time.sleep_ms(25)