import network import time import urequests import ujson import framebuf from machine import RTC, SPI, Pin from st7789nf import ( ST7789, COLOR_BLACK, COLOR_WHITE, COLOR_CYAN, COLOR_AMBER, COLOR_YELLOW, COLOR_NAVY, COLOR_GRAY, COLOR_DARKGRAY ) # ========================================== # CONFIGURATION # ========================================== WIFI_SSID = "YOUR_WIFI_SSID" WIFI_PASS = "YOUR_WIFI_PASSWORD" # London Coordinates LATITUDE = "51.5074" LONGITUDE = "-0.1278" # BST / UK Daylight Saving Time offset (1 for BST, 0 for GMT) BST_OFFSET = 1 # Open-Meteo API Endpoint (configured for mph) WEATHER_URL = ( "https://api.open-meteo.com/v1/forecast?" f"latitude={LATITUDE}&longitude={LONGITUDE}" "¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m" "&hourly=temperature_2m,weather_code" "&wind_speed_unit=mph" "&timezone=auto" ) # ========================================== # HARDWARE PINS (SPI1) # ========================================== LCD_WIDTH = 320 LCD_HEIGHT = 240 PIN_SCK = 10 # GP10 PIN_MOSI = 11 # GP11 PIN_RST = 12 # GP12 PIN_DC = 13 # GP13 PIN_CS = 14 # GP14 # ========================================== # BITMAP ICONS (16x16) # ========================================== ICON_SUN_16 = bytes([ 0x01, 0x80, 0x41, 0x82, 0x20, 0x04, 0x10, 0x08, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x07, 0xE0, 0x03, 0xC0, 0x10, 0x08, 0x20, 0x04, 0x41, 0x82, 0x01, 0x80 ]) ICON_CLOUD_16 = bytes([ 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00 ]) ICON_RAIN_16 = bytes([ 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFE, 0x00, 0x00, 0x24, 0x48, 0x48, 0x90, 0x12, 0x24, 0x24, 0x48, 0x48, 0x90, 0x00, 0x00 ]) ICON_THUNDER_16 = bytes([ 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1F, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x06, 0x00, 0x04, 0x00 ]) # ========================================== # NETWORK & API FUNCTIONS # ========================================== def connect_wifi(ssid, password, timeout=15): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(f"Connecting to Wi-Fi: {ssid}...") wlan.connect(ssid, password) start_time = time.time() while not wlan.isconnected(): if time.time() - start_time > timeout: print("Wi-Fi connection failed.") return False time.sleep(0.5) print(f"Wi-Fi Connected! IP: {wlan.ifconfig()[0]}") return True def sync_ntp(): import ntptime try: ntptime.settime() print("NTP Sync Success!") return True except Exception as e: print(f"NTP Sync Failed: {e}") return False def fetch_weather(): print("Fetching updated weather data...") try: response = urequests.get(WEATHER_URL) if response.status_code == 200: data = ujson.loads(response.text) response.close() current = data.get("current", {}) current_weather = { "temp": current.get("temperature_2m"), "feels_like": current.get("apparent_temperature"), "humidity": current.get("relative_humidity_2m"), "wind_speed": current.get("wind_speed_10m"), "code": current.get("weather_code") } hourly = data.get("hourly", {}) hourly_times = hourly.get("time", []) hourly_temps = hourly.get("temperature_2m", []) hourly_codes = hourly.get("weather_code", []) rtc = RTC() now_hour = rtc.datetime()[4] forecast = [] for i in range(len(hourly_times)): slot_hour = int(hourly_times[i].split("T")[1].split(":")[0]) if slot_hour >= now_hour and len(forecast) < 5: forecast.append({ "hour": f"{slot_hour:02d}:00", "temp": hourly_temps[i], "code": hourly_codes[i] }) return {"current": current_weather, "forecast": forecast} else: response.close() return None except Exception as e: print(f"Weather fetch failed: {e}") return None # ========================================== # GRAPHICS HELPER FUNCTIONS # ========================================== def draw_text_scaled(display, text, x, y, color, scale=2): """Renders scaled text using character framebuffers.""" char_buf = bytearray(8 * 8 * 2) char_fb = framebuf.FrameBuffer(char_buf, 8, 8, framebuf.RGB565) for idx, char in enumerate(text): char_fb.fill(COLOR_BLACK) char_fb.text(char, 0, 0, color) char_x = x + int(idx * 8 * scale) for row in range(8): for col in range(8): pixel = char_fb.pixel(col, row) if pixel != COLOR_BLACK: display.fill_rect(char_x + int(col * scale), y + int(row * scale), int(scale), int(scale), color) def draw_bitmap_scaled(display, bitmap_bytes, x, y, width, height, fg_color, scale=1): """Draws 1-bit monochrome bitmaps to screen.""" for row in range(height): for col in range(width): byte_idx = (row * width + col) // 8 bit_idx = 7 - (col % 8) pixel = (bitmap_bytes[byte_idx] >> bit_idx) & 1 if pixel: if scale == 1: display.pixel(x + col, y + row, fg_color) else: display.fill_rect(x + col * scale, y + row * scale, scale, scale, fg_color) def get_wmo_icon_and_color(wmo_code): """Maps Open-Meteo WMO weather codes to icons and colors.""" if wmo_code in (0, 1): return ICON_SUN_16, COLOR_YELLOW elif wmo_code in (2, 3, 45, 48): return ICON_CLOUD_16, COLOR_WHITE elif 51 <= wmo_code <= 67 or 80 <= wmo_code <= 82: return ICON_RAIN_16, COLOR_CYAN elif wmo_code >= 95: return ICON_THUNDER_16, COLOR_AMBER else: return ICON_CLOUD_16, COLOR_GRAY def render_ui(display, weather_data): """Draws dark theme UI layout.""" display.fill(COLOR_BLACK) # --- TOP BAR --- display.fill_rect(0, 0, 320, 32, COLOR_NAVY) display.hline(0, 32, 320, COLOR_CYAN) rtc = RTC() dt = rtc.datetime() hour = (dt[4] + BST_OFFSET) % 24 time_str = f"{hour:02d}:{dt[5]:02d}:{dt[6]:02d}" # Header Title (Larger Purbrook text) & Live Clock draw_text_scaled(display, "Purbrook", 10, 8, COLOR_WHITE, scale=1.9) draw_text_scaled(display, time_str, 185, 8, COLOR_AMBER, scale=2) # --- CENTER PANEL --- if weather_data and "current" in weather_data: curr = weather_data["current"] # Main Weather Icon (Scale=3 -> 48x48) icon_data, icon_color = get_wmo_icon_and_color(curr["code"]) draw_bitmap_scaled(display, icon_data, x=15, y=42, width=16, height=16, fg_color=icon_color, scale=3) # Temperature & Feels Like draw_text_scaled(display, f"{curr['temp']:.1f}C", 75, 40, COLOR_WHITE, scale=3) draw_text_scaled(display, f"Feels: {curr['feels_like']:.1f}C", 75, 70, COLOR_GRAY, scale=2) # Environmental Details (In mph) display.hline(15, 98, 290, COLOR_DARKGRAY) draw_text_scaled(display, f"Humidity: {curr['humidity']}%", 15, 106, COLOR_CYAN, scale=2) draw_text_scaled(display, f"Wind: {curr['wind_speed']} mph", 15, 128, COLOR_AMBER, scale=2) # --- BOTTOM STRIP (5-Hour Forecast) --- display.hline(0, 152, 320, COLOR_CYAN) if weather_data and "forecast" in weather_data: slot_width = 320 // 5 for idx, slot in enumerate(weather_data["forecast"][:5]): x = idx * slot_width if idx > 0: display.vline(x, 152, 88, COLOR_DARKGRAY) # Forecast Hour Header draw_text_scaled(display, slot["hour"][:2], x + 18, 158, COLOR_AMBER, scale=1.5) # Forecast Icon f_icon, f_color = get_wmo_icon_and_color(slot["code"]) draw_bitmap_scaled(display, f_icon, x=x + 16, y=175, width=16, height=16, fg_color=f_color, scale=2) # Forecast Temperature draw_text_scaled(display, f"{int(slot['temp'])}C", x + 12, 212, COLOR_WHITE, scale=2) # Push to display display.show() # ========================================== # MAIN ROUTINE # ========================================== spi1 = SPI(1, baudrate=20_000_000, sck=Pin(PIN_SCK), mosi=Pin(PIN_MOSI)) tft = ST7789( spi=spi1, width=LCD_WIDTH, height=LCD_HEIGHT, dc=Pin(PIN_DC), reset=Pin(PIN_RST), cs=Pin(PIN_CS) ) if connect_wifi(WIFI_SSID, WIFI_PASS): sync_ntp() current_weather_data = fetch_weather() last_weather_fetch = time.time() WEATHER_REFRESH_INTERVAL = 900 # 15 Minutes print("Starting main dashboard execution loop...") while True: now = time.time() if now - last_weather_fetch >= WEATHER_REFRESH_INTERVAL: updated_data = fetch_weather() if updated_data: current_weather_data = updated_data last_weather_fetch = now render_ui(tft, current_weather_data) time.sleep(1)