Netgate Discussion Forum
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Search
    • Register
    • Login

    UNOFFICIAL GUIDE: Squid external Raspberry PI WPAD lighttpd server Guide with Raspberry Zero LCD HAT code.

    Scheduled Pinned Locked Moved Cache/Proxy
    wpadproxysquidsquid-proxy
    18 Posts 1 Posters 1.7k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • JonathanLeeJ
      JonathanLee
      last edited by

      Keep in mind that you also might want to disable bluetooth services as they are not needed, and or disable the bluetooth stack so that if a bluetooth usb is plugged into the port it won't work also.

      1 Reply Last reply Reply Quote 0
      • JonathanLeeJ
        JonathanLee
        last edited by

        Update: Set your SSH on the wpad to only allow access during business hours. This can be done with the PAM

        edit the following file

        /etc/security/time.conf
        

        add:

        sshd;*;*;AL0500-2300
        

        Meaning I can only access ssh into my wpad durring 5-2300

        After adapt /etc/ssh/sshd_config

        make sure your listenaddress is the ip of the wpad set your AllowUsers to your login

        Example

        Port 8085 #change port if needed
        AddressFamily inet #ipv4 only
        ListenAddress 192.168.1.6 #address of wpad
        AllowUsers Jonathan@192.168.1.* # any device that is 192.168.1.X
        

        Change

        PermitRootLogin no #no ssh login for root
        UsePam yes # turn on pam for use with time restrictions
        

        after adapt
        /etc/passwd

        for added security also change your login to use the shell rbash and lock down the wad.

        Also if you use ipv6 and ipv4 you will have a race condition and sshd will not start on reboots you must also adapt

        sudo -i
        systemctl edit --full sshd.service
        

        under [unit] add

        Requires=network-only.target
        After=network-only.taget
        

        This will only start sshd once the network target is running in my example 192.168.1.6 I also have ipv6 running so it would cause issues unless I changed this. If you do not use ipv4 forget about this.

        1 Reply Last reply Reply Quote 0
        • JonathanLeeJ JonathanLee referenced this topic on
        • JonathanLeeJ
          JonathanLee
          last edited by

          This post is deleted!
          1 Reply Last reply Reply Quote 0
          • JonathanLeeJ
            JonathanLee
            last edited by

            This post is deleted!
            1 Reply Last reply Reply Quote 0
            • JonathanLeeJ
              JonathanLee
              last edited by

              This post is deleted!
              1 Reply Last reply Reply Quote 0
              • JonathanLeeJ
                JonathanLee
                last edited by

                ok here is the updated code man I got to tell you ChatGPT helped me with 90% of this. I am amazed. Of course it took a lot of trial and errors with testing and implementation of the code along side the level of modularization involved. This version has the accurate moon too. Keep in mind this still runs the wpad it is just less boring

                import os
                import sys
                import time
                import psutil
                import subprocess
                import threading
                import gpiozero  # Import once at the top
                from PIL import Image, ImageDraw, ImageFont
                import spidev as SPI
                import requests
                from io import BytesIO  # <-- Needed for icon image loading
                
                # Add parent directory to path to locate 'lib'
                sys.path.append("..")
                from lib import LCD_0inch96, LCD_1inch3, Gain_Param
                
                # Raspberry Pi pin config
                RST_0 = 24
                DC_0 = 4
                BL_0 = 13
                BUS_0 = 0
                DEVICE_0 = 0
                
                RST_1 = 23
                DC_1 = 5
                BL_1 = 12
                BUS_1 = 0
                DEVICE_1 = 1
                
                RST = 27
                DC = 22
                BL = 19
                BUS = 1
                DEVICE = 0
                state_lock = threading.Lock()
                debounce_timer = None
                DEBOUNCE_DELAY = 0.3  # seconds debounce delay to batch rapid presses
                
                # Proxy setup (if needed, else remove or set to None)
                proxies = {
                    "http": "http://192.168.1.1:3128",
                    "https": "http://192.168.1.1:3128",
                }
                
                # OpenWeatherMap API details
                API_KEY = "get your own api key"
                ZIP_CODE = "zip code needed"
                UNITS = "imperial"
                
                # Weather cache to reduce API calls
                weather_cache = {
                    "data": None,
                    "last_updated": 0,  # Unix timestamp
                    "icon_code": None,
                    "icon_image": None
                }
                
                # Doppler radar cache
                doppler_cache = {
                    "image": None,
                    "last_updated": 0
                }
                doppler_frame_index = 0  # Track which frame to show
                
                # Frame caches and timers
                doppler_frames = []
                last_doppler_fetch = 0
                DOPPLER_URL = "https://radar.weather.gov/ridge/standard/KMUX_loop.gif"
                # GOES-18 product definitions
                GOES_PRODUCTS = [
                    {
                        "name": "GeoColor",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/GEOCOLOR/GOES18-PSW-GEOCOLOR-600x600.gif"
                    },
                    {
                        "name": "Air Mass RGB",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/AirMass/GOES18-PSW-AirMass-600x600.gif"
                    },
                    {
                        "name": "Sandwich RGB",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/Sandwich/GOES18-PSW-Sandwich-600x600.gif"
                    },
                    {
                        "name": "Day/Night Cloud",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/DayNightCloudMicroCombo/GOES18-PSW-DayNightCloudMicroCombo-600x600.gif"
                    },
                    {
                        "name": "Fire Temperature",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/FireTemperature/GOES18-PSW-FireTemperature-600x600.gif"
                    },
                    {
                        "name": "Band 02 - Red Visible",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/02/GOES18-PSW-02-600x600.gif"
                    },
                    {
                        "name": "Band 07 - Shortwave IR",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/07/GOES18-PSW-07-600x600.gif"
                    },
                    {
                        "name": "Band 08 - Water Vapor",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/08/GOES18-PSW-08-600x600.gif"
                    },
                    {
                        "name": "Band 13 - Clean IR",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/13/GOES18-PSW-13-600x600.gif"
                    },
                    {
                        "name": "Band 14 - IR Window",
                        "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/14/GOES18-PSW-14-600x600.gif"
                    },
                    {
                        "name": "SOHO EIT 171",
                        "url": "https://soho.nascom.nasa.gov/data/LATEST/current_eit_171.gif"
                    },
                    {
                        "name": "SOHO EIT 304",
                        "url": "https://soho.nascom.nasa.gov/data/LATEST/current_eit_304.gif"
                    }
                ]
                # GOES-18 GIF cache containers (one per product)
                goes_dynamic_cache = [{} for _ in GOES_PRODUCTS]
                goes_dynamic_frame_index = [0 for _ in GOES_PRODUCTS]
                
                FRAME_DELAYS = {
                    "default": 1,
                    "animation": 0.033
                }
                
                
                # Get IPv6 address
                def get_ipv6_address():
                    try:
                        output = subprocess.check_output("ip -6 addr", shell=True).decode()
                        for line in output.splitlines():
                            line = line.strip()
                            if line.startswith("inet6") and "scope global" in line:
                                return line.split()[1].split("/")[0]
                    except Exception:
                        pass
                    return "No IPv6"
                
                # Fetch detailed weather info (dict with multiple fields)
                def get_detailed_weather():
                    now = time.time()
                    if weather_cache["data"] and (now - weather_cache["last_updated"] < 300):
                        return weather_cache["data"]
                    try:
                        response = requests.get(
                            f"http://api.openweathermap.org/data/2.5/weather?zip={ZIP_CODE},us&appid={API_KEY}&units={UNITS}",
                            timeout=5,
                            proxies=proxies
                        )
                        if response.status_code == 200:
                            data = response.json()
                            weather_cache["data"] = data
                            weather_cache["last_updated"] = now
                
                            # Download and cache icon if changed
                            icon_code = data["weather"][0]["icon"]
                            if icon_code != weather_cache["icon_code"]:
                                icon_url = f"https://openweathermap.org/img/wn/{icon_code}@2x.png"
                                try:
                                    icon_response = requests.get(icon_url, timeout=5, proxies=proxies)
                                    if icon_response.status_code == 200:
                                        icon_image = Image.open(BytesIO(icon_response.content)).convert("RGBA")
                                        weather_cache["icon_image"] = icon_image
                                        weather_cache["icon_code"] = icon_code
                                    else:
                                        weather_cache["icon_image"] = None
                                        weather_cache["icon_code"] = None
                                except Exception:
                                    weather_cache["icon_image"] = None
                                    weather_cache["icon_code"] = None
                
                            return data
                        else:
                            return None
                    except Exception:
                        return None
                
                # Fetch Doppler radar image, cached for 5 minutes
                def get_doppler_image():
                    now = time.time()
                    if doppler_cache["image"] and (now - doppler_cache["last_updated"] < 300):
                        return doppler_cache["image"]
                
                    try:
                        response = requests.get(DOPPLER_URL, timeout=5, proxies=proxies)
                        if response.status_code == 200:
                            gif = Image.open(BytesIO(response.content))
                            frames = []
                            try:
                                while True:
                                    # Resize here once on load
                                    frame = gif.copy().convert("RGBA").resize((disp.width, disp.height), Image.ANTIALIAS)
                                    frames.append(frame)
                                    gif.seek(gif.tell() + 1)
                            except EOFError:
                                pass  # End of frames
                
                            doppler_cache["image"] = frames
                            doppler_cache["last_updated"] = now
                            return frames
                    except Exception:
                        pass
                    return None
                
                # --- Moon phase logic ---
                
                from datetime import datetime
                import math
                
                def get_moon_phase():
                    # Known new moon reference date (UTC)
                    # Jan 6, 2000 at 18:14 UTC is a commonly used epoch for moon phase calc
                    known_new_moon = datetime(2000, 1, 6, 18, 14)
                
                    now = datetime.utcnow()
                    diff = now - known_new_moon
                    days_since_new = diff.total_seconds() / 86400.0  # days elapsed
                
                    lunar_cycle = 29.53058867  # length of lunar cycle in days
                    age = days_since_new % lunar_cycle
                
                    # Approximate illumination calculation
                    # phase as fraction of cycle (0 to 1)
                    phase = age / lunar_cycle
                    illumination = int((1 - abs(phase * 2 - 1)) * 100)
                
                    # Determine phase name based on age in days
                    if age < 1.84566:
                        phase_name = "New Moon"
                    elif age < 5.53699:
                        phase_name = "Waxing Crescent"
                    elif age < 9.22831:
                        phase_name = "First Quarter"
                    elif age < 12.91963:
                        phase_name = "Waxing Gibbous"
                    elif age < 16.61096:
                        phase_name = "Full Moon"
                    elif age < 20.30228:
                        phase_name = "Waning Gibbous"
                    elif age < 23.99361:
                        phase_name = "Last Quarter"
                    elif age < 27.68493:
                        phase_name = "Waning Crescent"
                    else:
                        phase_name = "New Moon"
                
                    return phase_name, illumination
                
                def draw_moon_icon(draw, center_x, center_y, radius, illumination):
                    # Draw full moon base
                    draw.ellipse(
                        (center_x - radius, center_y - radius, center_x + radius, center_y + radius),
                        fill=(220, 220, 220),
                    )
                
                    shadow_color = (0, 0, 0)
                
                    if illumination == 0:
                        # New moon — fully shadowed
                        draw.ellipse(
                            (center_x - radius, center_y - radius, center_x + radius, center_y + radius),
                            fill=shadow_color,
                        )
                    elif illumination == 100:
                        # Full moon — no shadow needed
                        pass
                    else:
                        dark_frac = 1 - (illumination / 100.0)
                        offset = radius * 2 * dark_frac
                
                        if illumination < 50:
                            # Crescent — shadow on right side
                            draw.ellipse(
                                (
                                    center_x + radius - offset,
                                    center_y - radius,
                                    center_x + radius + offset,
                                    center_y + radius,
                                ),
                                fill=shadow_color,
                            )
                        else:
                            # Gibbous — shadow on left side
                            draw.ellipse(
                                (
                                    center_x - radius - offset,
                                    center_y - radius,
                                    center_x - radius + offset,
                                    center_y + radius,
                                ),
                                fill=shadow_color,
                            )
                
                def get_goes_gif(index):
                    now = time.time()
                    product = GOES_PRODUCTS[index]
                    cache = goes_dynamic_cache[index]
                
                    if "image" in cache and (now - cache.get("last_updated", 0) < 300):
                        return cache["image"]
                
                    try:
                        response = requests.get(product["url"], timeout=10, proxies=proxies)
                        if response.status_code == 200:
                            gif = Image.open(BytesIO(response.content))
                            frames = []
                            try:
                                while True:
                                    # Resize frames once on load here
                                    frame = gif.copy().convert("RGBA").resize((disp.width, disp.height), Image.ANTIALIAS)
                                    frames.append(frame)
                                    gif.seek(gif.tell() + 1)
                            except EOFError:
                                pass
                
                            cache["image"] = frames
                            cache["last_updated"] = now
                            return frames
                    except Exception as e:
                        print(f"Error fetching GOES [{product['name']}] GIF: {e}")
                    return None
                
                # Setup displays
                gain = Gain_Param.Gain_Param()
                spi0 = SPI.SpiDev()
                spi0.open(BUS_0, DEVICE_0)
                spi0.max_speed_hz = 10000000
                spi1 = SPI.SpiDev()
                spi1.open(BUS_1, DEVICE_1)
                spi1.max_speed_hz = 10000000
                spi_main = SPI.SpiDev()
                spi_main.open(BUS, DEVICE)
                spi_main.max_speed_hz = 10000000
                disp_0 = LCD_0inch96.LCD_0inch96(spi=spi0, spi_freq=10000000, rst=RST_0, dc=DC_0, bl=BL_0, bl_freq=1000)
                disp_1 = LCD_0inch96.LCD_0inch96(spi=spi1, spi_freq=10000000, rst=RST_1, dc=DC_1, bl=BL_1, bl_freq=1000)
                disp = LCD_1inch3.LCD_1inch3(spi=spi_main, spi_freq=10000000, rst=RST, dc=DC, bl=BL)
                disp.Init()
                disp_0.Init()
                disp_1.Init()
                disp.clear()
                disp_0.clear()
                disp_1.clear()
                disp.bl_DutyCycle(100)
                disp_0.bl_DutyCycle(100)
                disp_1.bl_DutyCycle(100)
                
                # Fonts
                FontXL = ImageFont.truetype("../Font/Font00.ttf", 28)
                FontLarge = ImageFont.truetype("../Font/Font00.ttf", 24)
                FontMid = ImageFont.truetype("../Font/Font00.ttf", 22)
                FontSmall = ImageFont.truetype("../Font/Font00.ttf", 16)
                FontTiny = ImageFont.truetype("../Font/Font00.ttf", 12)
                
                # Colors (RGB tuples)
                COLOR_IP = (0, 204, 204)           
                COLOR_IPV6 = (102, 153, 204)       
                COLOR_DATE = (76, 175, 80)         
                COLOR_TIME = (255, 193, 7)         
                
                COLOR_HEADER = (0, 153, 153)       
                COLOR_WEATHER = (153, 204, 255)    
                
                COLOR_CPU = (33, 150, 243)         
                COLOR_TEMP = (100, 181, 246)       
                COLOR_RAM = (255, 193, 7)          
                COLOR_DISK = (156, 39, 176)        
                COLOR_UP = (0, 188, 212)           
                COLOR_DN = (77, 182, 172)          
                
                COLOR_WPAD_HEADER = (0, 153, 153)  
                COLOR_WPAD_USERS = (244, 67, 54)   
                COLOR_WPAD_NONE = (117, 117, 117)  
                
                # Button GPIO pins (adjust if needed)
                KEY1_PIN = 25
                KEY2_PIN = 26
                
                key1 = gpiozero.Button(KEY1_PIN)
                key2 = gpiozero.Button(KEY2_PIN)
                
                # Initial brightness percentage (10 to 100)
                brightness = 100
                
                def update_brightness(new_brightness):
                    global brightness
                    brightness = max(10, min(100, new_brightness))  # clamp between 10 and 100
                    disp.bl_DutyCycle(brightness)
                    disp_0.bl_DutyCycle(brightness)
                    disp_1.bl_DutyCycle(brightness)
                    print(f"Brightness set to {brightness}%")
                
                def key2_callback():
                    new_bright = brightness + 10
                    if new_bright > 100:
                        new_bright = 10
                    update_brightness(new_bright)
                
                key2.when_pressed = key2_callback
                
                # State tracking for pages: 0=original,1=weather,2=moon,3=doppler
                current_page = 0
                NUM_PAGES = 4 + len(GOES_PRODUCTS)  # 4 original + new GOES pages
                
                key1_hold_timer = None
                key1_held = False
                HOLD_TIME_SECONDS = 3
                
                def key1_hold_action():
                    global current_page, key1_held
                    with state_lock:
                        key1_held = True
                        current_page = 0
                    print("Button held for 3 seconds: returning to page 0")
                
                
                def key1_pressed():
                    global key1_hold_timer, key1_held
                    with state_lock:
                        key1_held = False
                        key1_hold_timer = threading.Timer(HOLD_TIME_SECONDS, key1_hold_action)
                        key1_hold_timer.start()
                
                
                def update_display_after_debounce():
                    global debounce_timer, current_page
                    with state_lock:
                        debounce_timer = None
                        # Usually here you might want to trigger a redraw or notify main loop
                        # You print info for debugging only
                        print(f"Debounced update: Showing page {current_page}")
                
                
                def key1_released():
                    global key1_hold_timer, key1_held, current_page, debounce_timer
                    with state_lock:
                        if key1_hold_timer:
                            key1_hold_timer.cancel()
                            key1_hold_timer = None
                
                        if not key1_held:
                            current_page = (current_page + 1) % NUM_PAGES
                            print(f"Button released: page now {current_page}")
                
                            if debounce_timer:
                                debounce_timer.cancel()
                
                            debounce_timer = threading.Timer(DEBOUNCE_DELAY, update_display_after_debounce)
                            debounce_timer.start()
                
                key1.when_pressed = key1_pressed
                key1.when_released = key1_released
                
                # --- Original display functions ---
                
                def draw_top_screen(draw):
                    ip = gain.GET_IP()
                    ipv6 = get_ipv6_address()
                    now = time.localtime()
                    time_t = time.strftime("%H:%M:%S", now)
                    time_D = time.strftime("%Y-%m-%d", now)
                
                    draw.text((2, 0), 'IP: ' + ip, fill=COLOR_IP, font=FontSmall)
                    draw.text((2, 18), ipv6, fill=COLOR_IPV6, font=FontTiny)
                    draw.text((2, 36), 'DATE: ' + time_D, fill=COLOR_DATE, font=FontSmall)
                    draw.text((2, 54), 'TIME: ' + time_t, fill=COLOR_TIME, font=FontSmall)
                
                def draw_middle_screen(draw, last_bytes):
                    y = 0
                    spacing = 26
                
                    header_text = "ROSEVILLE   WEATHER"
                    w, _ = draw.textsize(header_text, font=FontSmall)
                    draw.text(((disp.width - w) // 2, y), header_text, fill=COLOR_HEADER, font=FontSmall)
                    y += spacing
                
                    weather = get_detailed_weather()
                    if weather:
                        temp_f = int(round(weather["main"]["temp"]))
                        humidity = int(weather["main"]["humidity"])
                        pressure_pa = weather["main"]["pressure"] * 100
                        pressure_inhg = pressure_pa / 3386.39
                        summary = f"{temp_f}°{humidity}%  {pressure_inhg:.2f} inHg"
                    else:
                        summary = "Weather: Unavailable"
                    w, _ = draw.textsize(summary, font=FontMid)
                    draw.text(((disp.width - w) // 2, y), summary, fill=COLOR_WEATHER, font=FontMid)
                    y += spacing
                
                    wpad_header = "WPAD   HEALTH"
                    w, _ = draw.textsize(wpad_header, font=FontSmall)
                    draw.text(((disp.width - w) // 2, y), wpad_header, fill=COLOR_HEADER, font=FontSmall)
                    y += spacing
                
                    cpu = int(psutil.cpu_percent(interval=None))
                    draw.text((2, y), f"CPU: {cpu}%", fill=COLOR_CPU, font=FontLarge)
                    y += spacing
                
                    temp = int(gain.GET_Temp())
                    draw.text((2, y), f"Temp: {temp}℃", fill=COLOR_TEMP, font=FontLarge)
                    y += spacing
                
                    ram = int(psutil.virtual_memory().percent)
                    draw.text((2, y), f"RAM: {ram}%", fill=COLOR_RAM, font=FontLarge)
                    y += spacing
                
                    disk = int(psutil.disk_usage('/').percent)
                    draw.text((2, y), f"Disk: {disk}%", fill=COLOR_DISK, font=FontLarge)
                    y += spacing
                
                    counters = psutil.net_io_counters()
                    new_sent = counters.bytes_sent
                    new_recv = counters.bytes_recv
                    up = (new_sent - last_bytes[0]) * 8 / 1_000_000
                    dn = (new_recv - last_bytes[1]) * 8 / 1_000_000
                    draw.text((2, y), f"UP: {up:.2f}Mbps", fill=COLOR_UP, font=FontLarge)
                    y += spacing
                    draw.text((2, y), f"DN: {dn:.2f}Mbps", fill=COLOR_DN, font=FontLarge)
                    return new_sent, new_recv
                
                def draw_wpad_status(draw):
                    draw.text((2, 0), "WPAD Users:", fill=COLOR_WPAD_HEADER, font=FontMid)
                    try:
                        users = subprocess.check_output(['who']).decode().splitlines()
                        usernames = sorted(set(line.split()[0] for line in users if line.strip()))
                        display = ', '.join(usernames) if usernames else "None"
                    except Exception as e:
                        display = f"Error: {e}"
                
                    color = COLOR_WPAD_USERS if display != "None" else COLOR_WPAD_NONE
                    draw.text((2, 20), display, fill=color, font=FontMid)
                
                # --- Weather page drawing functions ---
                
                def draw_weather_top(draw):
                    now = time.localtime()
                    time_t = time.strftime("%H:%M:%S", now)
                    date_t = time.strftime("%A, %b %d %Y", now)
                    ip = gain.GET_IP()
                    ipv6 = get_ipv6_address()
                
                    draw.text((2, 0), "Detailed Weather", fill=COLOR_HEADER, font=FontSmall)
                    draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                    draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                    draw.text((2, 44), date_t, fill=COLOR_DATE, font=FontTiny)
                    draw.text((2, 58), time_t, fill=COLOR_TIME, font=FontTiny)
                
                def draw_weather_middle(draw):
                    weather = get_detailed_weather()
                    y = 0
                    spacing = 20
                
                    if weather:
                        main = weather["main"]
                        wind = weather.get("wind", {})
                        weather_desc = weather["weather"][0]["description"].capitalize()
                
                        temp = main["temp"]
                        feels_like = main.get("feels_like", temp)
                        humidity = main["humidity"]
                        pressure_pa = main["pressure"] * 100
                        pressure_inhg = pressure_pa / 3386.39
                        wind_speed = wind.get("speed", 0)
                        wind_deg = wind.get("deg", 0)
                
                        draw.text((2, y), f"Temp: {temp:.1f}°F", fill=COLOR_WEATHER, font=FontMid)
                        y += spacing
                        draw.text((2, y), f"Feels Like: {feels_like:.1f}°F", fill=COLOR_WEATHER, font=FontMid)
                        y += spacing
                        draw.text((2, y), f"Humidity: {humidity}%", fill=COLOR_WEATHER, font=FontMid)
                        y += spacing
                        draw.text((2, y), f"Pressure: {pressure_inhg:.2f} inHg", fill=COLOR_WEATHER, font=FontMid)
                        y += spacing
                        draw.text((2, y), f"Wind: {wind_speed} mph @ {wind_deg}°", fill=COLOR_WEATHER, font=FontMid)
                        y += spacing
                        draw.text((2, y), f"Condition: {weather_desc}", fill=COLOR_WEATHER, font=FontMid)
                    else:
                        draw.text((2, y), "Weather data unavailable", fill=COLOR_WPAD_NONE, font=FontMid)
                
                def draw_weather_bottom(image):
                    draw = ImageDraw.Draw(image)
                    weather = get_detailed_weather()
                    if weather and "weather" in weather and len(weather["weather"]) > 0:
                        condition = weather["weather"][0]["description"].capitalize()
                        if weather_cache["icon_image"]:
                            try:
                                icon_img = weather_cache["icon_image"].resize((64, 64), Image.ANTIALIAS)
                                x = (image.width - icon_img.width) // 2
                                y = (image.height - icon_img.height) // 2 - 10
                                image.paste(icon_img, (x, y), icon_img)
                
                                font = FontTiny
                                w, h = draw.textsize(condition, font=font)
                                text_x = (image.width - w) // 2
                                text_y = y + icon_img.height + 2
                                draw.text((text_x, text_y), condition, fill=COLOR_WEATHER, font=font)
                            except Exception:
                                draw.text((10, 10), condition, fill=COLOR_WPAD_NONE, font=FontMid)
                        else:
                            draw.text((10, 10), condition, fill=COLOR_WPAD_NONE, font=FontMid)
                    else:
                        draw.text((10, 10), "No icon data", fill=COLOR_WPAD_NONE, font=FontMid)
                
                # --- Moon phase page ---
                
                def draw_moon_top(draw):
                    now = time.localtime()
                    time_t = time.strftime("%H:%M:%S", now)
                    date_t = time.strftime("%A, %b %d %Y", now)
                    ip = gain.GET_IP()
                    ipv6 = get_ipv6_address()
                
                    draw.text((2, 0), "Moon Phase", fill=COLOR_HEADER, font=FontSmall)
                    draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                    draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                    draw.text((2, 44), date_t, fill=COLOR_DATE, font=FontTiny)
                    draw.text((2, 58), time_t, fill=COLOR_TIME, font=FontTiny)
                
                def draw_moon_middle(draw):
                    phase_name, illumination = get_moon_phase()
                    w, h = draw.textsize(phase_name, font=FontLarge)
                    draw.text(((disp.width - w) // 2, 10), phase_name, fill=COLOR_WEATHER, font=FontLarge)
                    draw_moon_icon(draw, disp.width // 2, 80, 40, illumination)
                
                def draw_moon_bottom(draw):
                    draw.text((2, 0), "Lunar illumination %", fill=COLOR_WPAD_HEADER, font=FontSmall)
                    phase_name, illumination = get_moon_phase()
                    draw.text((2, 18), f"{illumination}%", fill=COLOR_WEATHER, font=FontLarge)
                
                # --- Doppler radar page ---
                
                def draw_doppler_top(draw):
                    now = time.localtime()
                    date_str = time.strftime("%A, %b %d %Y", now)
                    time_str = time.strftime("%H:%M:%S", now)
                    ip = gain.GET_IP()
                    ipv6 = get_ipv6_address()
                
                    draw.text((2, 0), "Doppler Radar", fill=COLOR_HEADER, font=FontSmall)
                    draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                    draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                    draw.text((2, 44), date_str, fill=COLOR_DATE, font=FontTiny)
                    draw.text((2, 58), time_str, fill=COLOR_TIME, font=FontTiny)
                
                doppler_frame_index = 0  # Add this at the top level outside the loop
                
                #---GOES page-------
                
                def draw_doppler_middle(image):
                    global doppler_frame_index
                    frames = get_doppler_image()
                    if frames:
                        frame = frames[doppler_frame_index % len(frames)]
                        resized = frame.resize((image.width, image.height), Image.ANTIALIAS)
                        image.paste(resized, (0, 0), resized)
                        doppler_frame_index += 1
                    else:
                        draw = ImageDraw.Draw(image)
                        msg = "Radar Image\nUnavailable"
                        w, h = draw.textsize(msg, font=FontLarge)
                        draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                
                def draw_doppler_bottom(draw):
                    draw.text((2, 2), "Radar updated every 5 min", fill=COLOR_WPAD_NONE, font=FontTiny)
                
                def draw_goes_top(draw, label="GOES-18 Product"):
                    now = time.localtime()
                    date_str = time.strftime("%A, %b %d %Y", now)
                    time_str = time.strftime("%H:%M:%S", now)
                    ip = gain.GET_IP()
                    ipv6 = get_ipv6_address()
                
                    draw.text((2, 0), label, fill=COLOR_HEADER, font=FontSmall)
                    draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                    draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                    draw.text((2, 44), date_str, fill=COLOR_DATE, font=FontTiny)
                    draw.text((2, 58), time_str, fill=COLOR_TIME, font=FontTiny)
                
                def draw_goes_middle(image, index):
                    global goes_dynamic_frame_index
                    frames = get_goes_gif(index)
                    if frames:
                        frame = frames[goes_dynamic_frame_index[index] % len(frames)]
                        resized = frame.resize((image.width, image.height), Image.ANTIALIAS)
                        image.paste(resized, (0, 0), resized)
                        goes_dynamic_frame_index[index] += 1
                    else:
                        draw = ImageDraw.Draw(image)
                        msg = "GOES GIF\nUnavailable"
                        w, h = draw.textsize(msg, font=FontLarge)
                        draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                
                def draw_goes_bottom(draw, product_name=None):
                    text = "Updated every 5 min"
                    if product_name:
                        text = f"{product_name} - {text}"
                    draw.text((2, 2), text, fill=COLOR_WPAD_NONE, font=FontTiny)
                
                
                # --- Main loop ---
                
                last_bytes_sent = 0
                last_bytes_recv = 0
                
                try:
                    last_page = None  # Track the previous page to detect transitions
                
                    while True:
                        with state_lock:
                            page = current_page  # safely snapshot current page under lock
                
                        # Reset GOES animation frame index if just switched from Doppler (page 3)
                        if (4 <= page < 4 + len(GOES_PRODUCTS)) and (last_page == 3):
                            goes_index = page - 4
                            goes_dynamic_frame_index[goes_index] = 0
                
                        if page == 0:
                            image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                            draw0 = ImageDraw.Draw(image0)
                            draw_top_screen(draw0)
                            disp_0.ShowImage(image0)
                
                            image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                            draw = ImageDraw.Draw(image)
                            last_bytes_sent, last_bytes_recv = draw_middle_screen(draw, (last_bytes_sent, last_bytes_recv))
                            disp.ShowImage(image)
                
                            image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                            draw1 = ImageDraw.Draw(image1)
                            draw_wpad_status(draw1)
                            disp_1.ShowImage(image1)
                
                        elif page == 1:
                            image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                            draw0 = ImageDraw.Draw(image0)
                            draw_weather_top(draw0)
                            disp_0.ShowImage(image0)
                
                            image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                            draw = ImageDraw.Draw(image)
                            draw_weather_middle(draw)
                            disp.ShowImage(image)
                
                            image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                            draw_weather_bottom(image1)
                            disp_1.ShowImage(image1)
                
                        elif page == 2:
                            image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                            draw0 = ImageDraw.Draw(image0)
                            draw_moon_top(draw0)
                            disp_0.ShowImage(image0)
                
                            image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                            draw = ImageDraw.Draw(image)
                            draw_moon_middle(draw)
                            disp.ShowImage(image)
                
                            image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                            draw1 = ImageDraw.Draw(image1)
                            draw_moon_bottom(draw1)
                            disp_1.ShowImage(image1)
                
                        elif page == 3:
                            if time.time() - last_doppler_fetch > 300 or not doppler_frames:
                                doppler_frames = get_doppler_image()
                                last_doppler_fetch = time.time()
                                doppler_frame_index = 0
                
                            image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                            draw0 = ImageDraw.Draw(image0)
                            draw_doppler_top(draw0)
                            disp_0.ShowImage(image0)
                
                            image = Image.new("RGBA", (disp.width, disp.height), (0, 0, 0, 0))
                            if doppler_frames:
                                frame = doppler_frames[doppler_frame_index % len(doppler_frames)]
                                image.paste(frame, (0, 0), frame)
                                doppler_frame_index += 1
                            disp.ShowImage(image.convert("RGB"))
                
                            image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                            draw1 = ImageDraw.Draw(image1)
                            draw_doppler_bottom(draw1)
                            disp_1.ShowImage(image1)
                
                        elif 4 <= page < 4 + len(GOES_PRODUCTS):
                            goes_index = page - 4
                            now = time.time()
                            cache = goes_dynamic_cache[goes_index]
                
                            if "last_updated" not in cache or (now - cache["last_updated"]) > 300:
                                frames = get_goes_gif(goes_index)
                                if frames:
                                    cache["image"] = frames
                                    cache["last_updated"] = now
                                    # We do NOT reset goes_dynamic_frame_index here; it's reset on page transition
                
                            image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                            draw0 = ImageDraw.Draw(image0)
                            draw_goes_top(draw0, GOES_PRODUCTS[goes_index]["name"])
                            disp_0.ShowImage(image0)
                
                            image = Image.new("RGBA", (disp.width, disp.height), (0, 0, 0, 0))
                            frames = cache.get("image")
                            if frames:
                                frame = frames[goes_dynamic_frame_index[goes_index] % len(frames)]
                                image.paste(frame, (0, 0), frame)
                                goes_dynamic_frame_index[goes_index] += 1
                            else:
                                draw = ImageDraw.Draw(image)
                                msg = "GOES GIF\nUnavailable"
                                w, h = draw.textsize(msg, font=FontLarge)
                                draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                
                            disp.ShowImage(image.convert("RGB"))
                
                            image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                            draw1 = ImageDraw.Draw(image1)
                            draw_goes_bottom(draw1, GOES_PRODUCTS[goes_index]["name"])
                            disp_1.ShowImage(image1)
                
                        # Sleep delays based on page type
                        if page in [3] + list(range(4, 4 + len(GOES_PRODUCTS))):
                            time.sleep(FRAME_DELAYS["animation"])
                        else:
                            time.sleep(FRAME_DELAYS["default"])
                
                        last_page = page  # Update last_page after processing
                
                except KeyboardInterrupt:
                    print("Exiting...")
                    disp_0.clear()
                    disp_1.clear()
                    disp.clear()
                    sys.exit()
                
                
                1 Reply Last reply Reply Quote 0
                • JonathanLeeJ
                  JonathanLee
                  last edited by

                  This post is deleted!
                  1 Reply Last reply Reply Quote 0
                  • JonathanLeeJ
                    JonathanLee
                    last edited by

                    This post is deleted!
                    1 Reply Last reply Reply Quote 0
                    • JonathanLeeJ
                      JonathanLee
                      last edited by JonathanLee

                      This is a better code for the screen if you have one

                      This code is even better it uses moon images and has corrections to the moon functionality

                      import os
                      import sys
                      import time
                      import math
                      from datetime import datetime
                      import psutil
                      import subprocess
                      import threading
                      import gpiozero  # Import once at the top
                      from PIL import Image, ImageDraw, ImageFont
                      import spidev as SPI
                      import requests
                      from io import BytesIO  # <-- Needed for icon image loading
                      
                      # Add parent directory to path to locate 'lib'
                      sys.path.append("..")
                      from lib import LCD_0inch96, LCD_1inch3, Gain_Param
                      
                      # Raspberry Pi pin config
                      RST_0 = 24
                      DC_0 = 4
                      BL_0 = 13
                      BUS_0 = 0
                      DEVICE_0 = 0
                      
                      RST_1 = 23
                      DC_1 = 5
                      BL_1 = 12
                      BUS_1 = 0
                      DEVICE_1 = 1
                      
                      RST = 27
                      DC = 22
                      BL = 19
                      BUS = 1
                      DEVICE = 0
                      state_lock = threading.Lock()
                      debounce_timer = None
                      DEBOUNCE_DELAY = 0.3  # seconds debounce delay to batch rapid presses
                      
                      # Proxy setup (if needed, else remove or set to None)
                      proxies = {
                          "http": "http://192.168.1.1:3128",
                          "https": "http://192.168.1.1:3128",
                      }
                      
                      # OpenWeatherMap API details
                      API_KEY = "API KEY HERE"
                      ZIP_CODE = "ZIP CODE HERE"
                      UNITS = "imperial"
                      
                      # Weather cache to reduce API calls
                      weather_cache = {
                          "data": None,
                          "last_updated": 0,  # Unix timestamp
                          "icon_code": None,
                          "icon_image": None
                      }
                      
                      # Doppler radar cache
                      doppler_cache = {
                          "image": None,
                          "last_updated": 0
                      }
                      doppler_frame_index = 0  # Track which frame to show
                      
                      # Frame caches and timers
                      doppler_frames = []
                      last_doppler_fetch = 0
                      DOPPLER_URL = "https://radar.weather.gov/ridge/standard/KMUX_loop.gif"
                      # GOES-18 product definitions
                      GOES_PRODUCTS = [
                          {
                              "name": "GeoColor",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/GEOCOLOR/GOES18-PSW-GEOCOLOR-600x600.gif"
                          },
                          {
                              "name": "Air Mass RGB",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/AirMass/GOES18-PSW-AirMass-600x600.gif"
                          },
                          {
                              "name": "Sandwich RGB",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/Sandwich/GOES18-PSW-Sandwich-600x600.gif"
                          },
                          {
                              "name": "Day/Night Cloud",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/DayNightCloudMicroCombo/GOES18-PSW-DayNightCloudMicroCombo-600x600.gif"
                          },
                          {
                              "name": "Fire Temperature",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/FireTemperature/GOES18-PSW-FireTemperature-600x600.gif"
                          },
                          {
                              "name": "Band 02 - Red Visible",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/02/GOES18-PSW-02-600x600.gif"
                          },
                          {
                              "name": "Band 07 - Shortwave IR",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/07/GOES18-PSW-07-600x600.gif"
                          },
                          {
                              "name": "Band 08 - Water Vapor",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/08/GOES18-PSW-08-600x600.gif"
                          },
                          {
                              "name": "Band 13 - Clean IR",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/13/GOES18-PSW-13-600x600.gif"
                          },
                          {
                              "name": "Band 14 - IR Window",
                              "url": "https://cdn.star.nesdis.noaa.gov/GOES18/ABI/SECTOR/psw/14/GOES18-PSW-14-600x600.gif"
                          },
                          {
                              "name": "SOHO EIT 171",
                              "url": "https://soho.nascom.nasa.gov/data/LATEST/current_eit_171.gif"
                          },
                          {
                              "name": "SOHO EIT 304",
                              "url": "https://soho.nascom.nasa.gov/data/LATEST/current_eit_304.gif"
                          }
                      ]
                      # GOES-18 GIF cache containers (one per product)
                      goes_dynamic_cache = [{} for _ in GOES_PRODUCTS]
                      goes_dynamic_frame_index = [0 for _ in GOES_PRODUCTS]
                      
                      FRAME_DELAYS = {
                          "default": 1,
                          "animation": 0.033
                      }
                      
                      
                      # Get IPv6 address
                      def get_ipv6_address():
                          try:
                              output = subprocess.check_output("ip -6 addr", shell=True).decode()
                              for line in output.splitlines():
                                  line = line.strip()
                                  if line.startswith("inet6") and "scope global" in line:
                                      return line.split()[1].split("/")[0]
                          except Exception:
                              pass
                          return "No IPv6"
                      
                      # Fetch detailed weather info (dict with multiple fields)
                      def get_detailed_weather():
                          now = time.time()
                          if weather_cache["data"] and (now - weather_cache["last_updated"] < 300):
                              return weather_cache["data"]
                          try:
                              response = requests.get(
                                  f"http://api.openweathermap.org/data/2.5/weather?zip={ZIP_CODE},us&appid={API_KEY}&units={UNITS}",
                                  timeout=5,
                                  proxies=proxies
                              )
                              if response.status_code == 200:
                                  data = response.json()
                                  weather_cache["data"] = data
                                  weather_cache["last_updated"] = now
                      
                                  # Download and cache icon if changed
                                  icon_code = data["weather"][0]["icon"]
                                  if icon_code != weather_cache["icon_code"]:
                                      icon_url = f"https://openweathermap.org/img/wn/{icon_code}@2x.png"
                                      try:
                                          icon_response = requests.get(icon_url, timeout=5, proxies=proxies)
                                          if icon_response.status_code == 200:
                                              icon_image = Image.open(BytesIO(icon_response.content)).convert("RGBA")
                                              weather_cache["icon_image"] = icon_image
                                              weather_cache["icon_code"] = icon_code
                                          else:
                                              weather_cache["icon_image"] = None
                                              weather_cache["icon_code"] = None
                                      except Exception:
                                          weather_cache["icon_image"] = None
                                          weather_cache["icon_code"] = None
                      
                                  return data
                              else:
                                  return None
                          except Exception:
                              return None
                      
                      # Fetch Doppler radar image, cached for 5 minutes
                      def get_doppler_image():
                          now = time.time()
                          if doppler_cache["image"] and (now - doppler_cache["last_updated"] < 300):
                              return doppler_cache["image"]
                      
                          try:
                              response = requests.get(DOPPLER_URL, timeout=5, proxies=proxies)
                              if response.status_code == 200:
                                  gif = Image.open(BytesIO(response.content))
                                  frames = []
                                  try:
                                      while True:
                                          # Resize here once on load
                                          frame = gif.copy().convert("RGBA").resize((disp.width, disp.height), Image.ANTIALIAS)
                                          frames.append(frame)
                                          gif.seek(gif.tell() + 1)
                                  except EOFError:
                                      pass  # End of frames
                      
                                  doppler_cache["image"] = frames
                                  doppler_cache["last_updated"] = now
                                  return frames
                          except Exception:
                              pass
                          return None
                      
                      # --- Moon phase logic ---
                      moon_image_folder = "/usr/Zero_LCD_HAT_A_Demo/python/example/moon/moon_phases"
                      moon_images_cache = {}
                      
                      def get_moon_image(illumination):
                          # Clamp illumination
                          illumination = max(0, min(illumination, 100))
                          phase_index = int(illumination / 6.25)
                          if phase_index > 15:
                              phase_index = 15
                          if phase_index in moon_images_cache:
                              return moon_images_cache[phase_index]
                          filename = f"Fase_{phase_index+1}.jpg"
                          path = os.path.join(moon_image_folder, filename)
                          try:
                              img = Image.open(path).convert("RGBA")
                          except FileNotFoundError:
                              print(f"Warning: {filename} not found.")
                              img = Image.new("RGBA", (100, 100), (0,0,0,0))  # transparent fallback
                          # Cache it
                          moon_images_cache[phase_index] = img
                          # Limit cache size (optional): keep only last 4 images
                          if len(moon_images_cache) > 4:
                              # remove oldest cache item
                              oldest_key = next(iter(moon_images_cache))
                              del moon_images_cache[oldest_key]
                      
                          return img
                      
                      def get_moon_phase():
                          # Calculate moon age and illumination as before
                          known_new_moon = datetime(2000, 1, 6, 18, 14)
                          now = datetime.utcnow()
                          diff = now - known_new_moon
                          days_since_new = diff.total_seconds() / 86400.0
                          lunar_cycle = 29.53058867
                          age = days_since_new % lunar_cycle
                      
                          phase = age / lunar_cycle
                          illumination = int((1 - abs(phase * 2 - 1)) * 100)
                      
                          # Use illumination % to assign phase name to match images better
                          if illumination <= 1:
                              phase_name = "New Moon"
                          elif illumination < 49:
                              phase_name = "Waxing Crescent"
                          elif 49 <= illumination <= 51:
                              phase_name = "First Quarter"
                          elif illumination < 99:
                              phase_name = "Waxing Gibbous"
                          elif illumination == 100:
                              phase_name = "Full Moon"
                          elif illumination > 51:
                              # Waning phases: just invert ranges or you can use age for waning/waxing detection
                              # For better accuracy, check if moon is waxing or waning by age
                              if age > lunar_cycle / 2:
                                  # Waning phases
                                  if illumination > 51 and illumination < 99:
                                      phase_name = "Waning Gibbous"
                                  elif 49 <= illumination <= 51:
                                      phase_name = "Last Quarter"
                                  elif illumination < 49:
                                      phase_name = "Waning Crescent"
                              else:
                                  phase_name = "Waxing Gibbous"  # fallback
                      
                          else:
                              phase_name = "New Moon"  # fallback
                      
                          return phase_name, illumination
                      
                      def draw_moon_icon(base_image, center_x, center_y, radius, illumination):
                          # Get the moon image based on illumination
                          moon_image = get_moon_image(illumination)
                          # Resize the moon image to match the desired diameter
                          moon_image_resized = moon_image.resize((radius * 2, radius * 2), Image.ANTIALIAS)
                          # Calculate position to paste
                          x_offset = center_x - radius
                          y_offset = center_y - radius
                          # Paste moon image onto base image (with transparency if any)
                          base_image.paste(moon_image_resized, (x_offset, y_offset), moon_image_resized.convert("RGBA"))
                      
                      def get_goes_gif(index):
                          now = time.time()
                          product = GOES_PRODUCTS[index]
                          cache = goes_dynamic_cache[index]
                      
                          if "image" in cache and (now - cache.get("last_updated", 0) < 300):
                              return cache["image"]
                      
                          try:
                              response = requests.get(product["url"], timeout=10, proxies=proxies)
                              if response.status_code == 200:
                                  gif = Image.open(BytesIO(response.content))
                                  frames = []
                                  try:
                                      while True:
                                          # Resize frames once on load here
                                          frame = gif.copy().convert("RGBA").resize((disp.width, disp.height), Image.ANTIALIAS)
                                          frames.append(frame)
                                          gif.seek(gif.tell() + 1)
                                  except EOFError:
                                      pass
                      
                                  cache["image"] = frames
                                  cache["last_updated"] = now
                                  return frames
                          except Exception as e:
                              print(f"Error fetching GOES [{product['name']}] GIF: {e}")
                          return None
                      
                      # Setup displays
                      gain = Gain_Param.Gain_Param()
                      spi0 = SPI.SpiDev()
                      spi0.open(BUS_0, DEVICE_0)
                      spi0.max_speed_hz = 10000000
                      spi1 = SPI.SpiDev()
                      spi1.open(BUS_1, DEVICE_1)
                      spi1.max_speed_hz = 10000000
                      spi_main = SPI.SpiDev()
                      spi_main.open(BUS, DEVICE)
                      spi_main.max_speed_hz = 10000000
                      disp_0 = LCD_0inch96.LCD_0inch96(spi=spi0, spi_freq=10000000, rst=RST_0, dc=DC_0, bl=BL_0, bl_freq=1000)
                      disp_1 = LCD_0inch96.LCD_0inch96(spi=spi1, spi_freq=10000000, rst=RST_1, dc=DC_1, bl=BL_1, bl_freq=1000)
                      disp = LCD_1inch3.LCD_1inch3(spi=spi_main, spi_freq=10000000, rst=RST, dc=DC, bl=BL)
                      disp.Init()
                      disp_0.Init()
                      disp_1.Init()
                      disp.clear()
                      disp_0.clear()
                      disp_1.clear()
                      disp.bl_DutyCycle(100)
                      disp_0.bl_DutyCycle(100)
                      disp_1.bl_DutyCycle(100)
                      
                      # Fonts
                      FontXL = ImageFont.truetype("../Font/Font00.ttf", 28)
                      FontLarge = ImageFont.truetype("../Font/Font00.ttf", 24)
                      FontMid = ImageFont.truetype("../Font/Font00.ttf", 22)
                      FontSmall = ImageFont.truetype("../Font/Font00.ttf", 16)
                      FontTiny = ImageFont.truetype("../Font/Font00.ttf", 12)
                      
                      # Colors (RGB tuples)
                      COLOR_IP = (0, 204, 204)           
                      COLOR_IPV6 = (102, 153, 204)       
                      COLOR_DATE = (76, 175, 80)         
                      COLOR_TIME = (255, 193, 7)         
                      
                      COLOR_HEADER = (0, 153, 153)       
                      COLOR_WEATHER = (153, 204, 255)    
                      
                      COLOR_CPU = (33, 150, 243)         
                      COLOR_TEMP = (100, 181, 246)       
                      COLOR_RAM = (255, 193, 7)          
                      COLOR_DISK = (156, 39, 176)        
                      COLOR_UP = (0, 188, 212)           
                      COLOR_DN = (77, 182, 172)          
                      
                      COLOR_WPAD_HEADER = (0, 153, 153)  
                      COLOR_WPAD_USERS = (244, 67, 54)   
                      COLOR_WPAD_NONE = (117, 117, 117)  
                      
                      # Button GPIO pins (adjust if needed)
                      KEY1_PIN = 25
                      KEY2_PIN = 26
                      
                      key1 = gpiozero.Button(KEY1_PIN)
                      key2 = gpiozero.Button(KEY2_PIN)
                      
                      # Initial brightness percentage (10 to 100)
                      brightness = 100
                      
                      def update_brightness(new_brightness):
                          global brightness
                          brightness = max(10, min(100, new_brightness))  # clamp between 10 and 100
                          disp.bl_DutyCycle(brightness)
                          disp_0.bl_DutyCycle(brightness)
                          disp_1.bl_DutyCycle(brightness)
                          print(f"Brightness set to {brightness}%")
                      
                      def key2_callback():
                          new_bright = brightness + 10
                          if new_bright > 100:
                              new_bright = 10
                          update_brightness(new_bright)
                      
                      key2.when_pressed = key2_callback
                      
                      # State tracking for pages: 0=original,1=weather,2=moon,3=doppler
                      current_page = 0
                      NUM_PAGES = 4 + len(GOES_PRODUCTS)  # 4 original + new GOES pages
                      
                      key1_hold_timer = None
                      key1_held = False
                      HOLD_TIME_SECONDS = 3
                      
                      def key1_hold_action():
                          global current_page, key1_held
                          with state_lock:
                              key1_held = True
                              current_page = 0
                          print("Button held for 3 seconds: returning to page 0")
                      
                      
                      def key1_pressed():
                          global key1_hold_timer, key1_held
                          with state_lock:
                              key1_held = False
                              key1_hold_timer = threading.Timer(HOLD_TIME_SECONDS, key1_hold_action)
                              key1_hold_timer.start()
                      
                      
                      def update_display_after_debounce():
                          global debounce_timer, current_page
                          with state_lock:
                              debounce_timer = None
                              # Usually here you might want to trigger a redraw or notify main loop
                              # You print info for debugging only
                              print(f"Debounced update: Showing page {current_page}")
                      
                      
                      def key1_released():
                          global key1_hold_timer, key1_held, current_page, debounce_timer
                          with state_lock:
                              if key1_hold_timer:
                                  key1_hold_timer.cancel()
                                  key1_hold_timer = None
                      
                              if not key1_held:
                                  current_page = (current_page + 1) % NUM_PAGES
                                  print(f"Button released: page now {current_page}")
                      
                                  if debounce_timer:
                                      debounce_timer.cancel()
                      
                                  debounce_timer = threading.Timer(DEBOUNCE_DELAY, update_display_after_debounce)
                                  debounce_timer.start()
                      
                      key1.when_pressed = key1_pressed
                      key1.when_released = key1_released
                      
                      # --- Original display functions ---
                      def draw_top_screen(draw):
                          ip = gain.GET_IP()
                          ipv6 = get_ipv6_address()
                          now = time.localtime()
                          time_t = time.strftime("%H:%M:%S", now)
                          time_D = time.strftime("%Y-%m-%d", now)
                      
                          draw.text((2, 0), 'IP: ' + ip, fill=COLOR_IP, font=FontSmall)
                          draw.text((2, 18), ipv6, fill=COLOR_IPV6, font=FontTiny)
                          draw.text((2, 36), 'DATE: ' + time_D, fill=COLOR_DATE, font=FontSmall)
                          draw.text((2, 54), 'TIME: ' + time_t, fill=COLOR_TIME, font=FontSmall)
                      
                      def draw_middle_screen(draw, last_bytes):
                          y = 0
                          spacing = 26
                      
                          header_text = "ROSEVILLE   WEATHER"
                          w, _ = draw.textsize(header_text, font=FontSmall)
                          draw.text(((disp.width - w) // 2, y), header_text, fill=COLOR_HEADER, font=FontSmall)
                          y += spacing
                      
                          weather = get_detailed_weather()
                          if weather:
                              temp_f = int(round(weather["main"]["temp"]))
                              humidity = int(weather["main"]["humidity"])
                              pressure_pa = weather["main"]["pressure"] * 100
                              pressure_inhg = pressure_pa / 3386.39
                              summary = f"{temp_f}°{humidity}%  {pressure_inhg:.2f} inHg"
                          else:
                              summary = "Weather: Unavailable"
                          w, _ = draw.textsize(summary, font=FontMid)
                          draw.text(((disp.width - w) // 2, y), summary, fill=COLOR_WEATHER, font=FontMid)
                          y += spacing
                      
                          wpad_header = "WPAD   HEALTH"
                          w, _ = draw.textsize(wpad_header, font=FontSmall)
                          draw.text(((disp.width - w) // 2, y), wpad_header, fill=COLOR_HEADER, font=FontSmall)
                          y += spacing
                      
                          cpu = int(psutil.cpu_percent(interval=None))
                          draw.text((2, y), f"CPU: {cpu}%", fill=COLOR_CPU, font=FontLarge)
                          y += spacing
                      
                          temp = int(gain.GET_Temp())
                          draw.text((2, y), f"Temp: {temp}℃", fill=COLOR_TEMP, font=FontLarge)
                          y += spacing
                      
                          ram = int(psutil.virtual_memory().percent)
                          draw.text((2, y), f"RAM: {ram}%", fill=COLOR_RAM, font=FontLarge)
                          y += spacing
                      
                          disk = int(psutil.disk_usage('/').percent)
                          draw.text((2, y), f"Disk: {disk}%", fill=COLOR_DISK, font=FontLarge)
                          y += spacing
                      
                          # Network speed calculations
                          counters = psutil.net_io_counters()
                          new_sent = counters.bytes_sent
                          new_recv = counters.bytes_recv
                      
                          sent_diff = new_sent - last_bytes[0]
                          recv_diff = new_recv - last_bytes[1]
                      
                          # Time passed between measurements (assumed 1 sec loop; adjust if different)
                          interval = 1.0  # seconds
                      
                          # Avoid negative values and division by zero
                          if sent_diff < 0:
                              sent_diff = 0
                          if recv_diff < 0:
                              recv_diff = 0
                      
                          up = (sent_diff * 8) / (1_000_000 * interval)  # Mbps
                          dn = (recv_diff * 8) / (1_000_000 * interval)  # Mbps
                      
                          draw.text((2, y), f"UP: {up:.2f} Mbps", fill=COLOR_UP, font=FontLarge)
                          y += spacing
                          draw.text((2, y), f"DN: {dn:.2f} Mbps", fill=COLOR_DN, font=FontLarge)
                      
                          return new_sent, new_recv
                      
                      def draw_wpad_status(draw):
                          draw.text((2, 0), "WPAD Users:", fill=COLOR_WPAD_HEADER, font=FontMid)
                          try:
                              users = subprocess.check_output(['who']).decode().splitlines()
                              usernames = sorted(set(line.split()[0] for line in users if line.strip()))
                              display = ', '.join(usernames) if usernames else "None"
                          except Exception as e:
                              display = f"Error: {e}"
                      
                          color = COLOR_WPAD_USERS if display != "None" else COLOR_WPAD_NONE
                          draw.text((2, 20), display, fill=color, font=FontMid)
                      
                      # --- Weather page drawing functions ---
                      
                      def draw_weather_top(draw):
                          now = time.localtime()
                          time_t = time.strftime("%H:%M:%S", now)
                          date_t = time.strftime("%A, %b %d %Y", now)
                          ip = gain.GET_IP()
                          ipv6 = get_ipv6_address()
                      
                          draw.text((2, 0), "Detailed Weather", fill=COLOR_HEADER, font=FontSmall)
                          draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                          draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                          draw.text((2, 44), date_t, fill=COLOR_DATE, font=FontTiny)
                          draw.text((2, 58), time_t, fill=COLOR_TIME, font=FontTiny)
                      
                      def draw_weather_middle(draw):
                          weather = get_detailed_weather()
                          y = 0
                          spacing = 20
                      
                          if weather:
                              main = weather["main"]
                              wind = weather.get("wind", {})
                              weather_desc = weather["weather"][0]["description"].capitalize()
                      
                              temp = main["temp"]
                              feels_like = main.get("feels_like", temp)
                              humidity = main["humidity"]
                              pressure_pa = main["pressure"] * 100
                              pressure_inhg = pressure_pa / 3386.39
                              wind_speed = wind.get("speed", 0)
                              wind_deg = wind.get("deg", 0)
                      
                              draw.text((2, y), f"Temp: {temp:.1f}°F", fill=COLOR_WEATHER, font=FontMid)
                              y += spacing
                              draw.text((2, y), f"Feels Like: {feels_like:.1f}°F", fill=COLOR_WEATHER, font=FontMid)
                              y += spacing
                              draw.text((2, y), f"Humidity: {humidity}%", fill=COLOR_WEATHER, font=FontMid)
                              y += spacing
                              draw.text((2, y), f"Pressure: {pressure_inhg:.2f} inHg", fill=COLOR_WEATHER, font=FontMid)
                              y += spacing
                              draw.text((2, y), f"Wind: {wind_speed} mph @ {wind_deg}°", fill=COLOR_WEATHER, font=FontMid)
                              y += spacing
                              draw.text((2, y), f"Condition: {weather_desc}", fill=COLOR_WEATHER, font=FontMid)
                          else:
                              draw.text((2, y), "Weather data unavailable", fill=COLOR_WPAD_NONE, font=FontMid)
                      
                      def draw_weather_bottom(image):
                          draw = ImageDraw.Draw(image)
                          weather = get_detailed_weather()
                          if weather and "weather" in weather and len(weather["weather"]) > 0:
                              condition = weather["weather"][0]["description"].capitalize()
                              if weather_cache["icon_image"]:
                                  try:
                                      icon_img = weather_cache["icon_image"].resize((64, 64), Image.ANTIALIAS)
                                      x = (image.width - icon_img.width) // 2
                                      y = (image.height - icon_img.height) // 2 - 10
                                      image.paste(icon_img, (x, y), icon_img)
                      
                                      font = FontTiny
                                      w, h = draw.textsize(condition, font=font)
                                      text_x = (image.width - w) // 2
                                      text_y = y + icon_img.height + 2
                                      draw.text((text_x, text_y), condition, fill=COLOR_WEATHER, font=font)
                                  except Exception:
                                      draw.text((10, 10), condition, fill=COLOR_WPAD_NONE, font=FontMid)
                              else:
                                  draw.text((10, 10), condition, fill=COLOR_WPAD_NONE, font=FontMid)
                          else:
                              draw.text((10, 10), "No icon data", fill=COLOR_WPAD_NONE, font=FontMid)
                      
                      # --- Moon phase page ---
                      
                      def draw_moon_top(draw):
                          now = time.localtime()
                          time_t = time.strftime("%H:%M:%S", now)
                          date_t = time.strftime("%A, %b %d %Y", now)
                          ip = gain.GET_IP()
                          ipv6 = get_ipv6_address()
                      
                          draw.text((2, 0), "Moon Phase", fill=COLOR_HEADER, font=FontSmall)
                          draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                          draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                          draw.text((2, 44), date_t, fill=COLOR_DATE, font=FontTiny)
                          draw.text((2, 58), time_t, fill=COLOR_TIME, font=FontTiny)
                      
                      def draw_moon_middle(image):
                          draw = ImageDraw.Draw(image)
                          phase_name, illumination = get_moon_phase()
                          # Draw phase name at the top with spacing
                          text_y = 10
                          w, h = draw.textsize(phase_name, font=FontLarge)
                          draw.text(((disp.width - w) // 2, text_y), phase_name, fill=COLOR_WEATHER, font=FontLarge)
                          # Calculate available space for moon below the text
                          top_of_moon_area = text_y + h + 10  # 10px padding below text
                          bottom_of_moon_area = disp.height - 10  # 10px padding from bottom
                          available_height = bottom_of_moon_area - top_of_moon_area
                          radius = min(available_height, disp.width) // 2  # keep it a circle
                          # Calculate center Y within the moon area
                          center_y = top_of_moon_area + available_height // 2
                          # Center X remains same
                          center_x = disp.width // 2
                          # Draw the moon
                          draw_moon_icon(image, center_x, center_y, radius, illumination)
                      
                      def draw_moon_bottom(draw):
                          draw.text((2, 0), "Lunar illumination %", fill=COLOR_WPAD_HEADER, font=FontSmall)
                          phase_name, illumination = get_moon_phase()
                          draw.text((2, 18), f"{illumination}%", fill=COLOR_WEATHER, font=FontLarge)
                      
                      # --- Doppler radar page ---
                      def draw_doppler_top(draw):
                          now = time.localtime()
                          date_str = time.strftime("%A, %b %d %Y", now)
                          time_str = time.strftime("%H:%M:%S", now)
                          ip = gain.GET_IP()
                          ipv6 = get_ipv6_address()
                      
                          draw.text((2, 0), "Doppler Radar", fill=COLOR_HEADER, font=FontSmall)
                          draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                          draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                          draw.text((2, 44), date_str, fill=COLOR_DATE, font=FontTiny)
                          draw.text((2, 58), time_str, fill=COLOR_TIME, font=FontTiny)
                      
                      #---GOES page-------
                      def draw_doppler_middle(image):
                          global doppler_frame_index
                          frames = get_doppler_image()
                          if frames:
                              frame = frames[doppler_frame_index % len(frames)]
                              resized = frame.resize((image.width, image.height), Image.ANTIALIAS)
                              image.paste(resized, (0, 0), resized)
                              doppler_frame_index += 1
                          else:
                              draw = ImageDraw.Draw(image)
                              msg = "Radar Image\nUnavailable"
                              w, h = draw.textsize(msg, font=FontLarge)
                              draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                      
                      def draw_doppler_bottom(draw):
                          draw.text((2, 2), "Radar updated every 5 min", fill=COLOR_WPAD_NONE, font=FontTiny)
                      
                      def draw_goes_top(draw, label="GOES-18 Product"):
                          now = time.localtime()
                          date_str = time.strftime("%A, %b %d %Y", now)
                          time_str = time.strftime("%H:%M:%S", now)
                          ip = gain.GET_IP()
                          ipv6 = get_ipv6_address()
                      
                          draw.text((2, 0), label, fill=COLOR_HEADER, font=FontSmall)
                          draw.text((2, 18), f"IP: {ip}", fill=COLOR_IP, font=FontTiny)
                          draw.text((2, 30), f"IPv6: {ipv6}", fill=COLOR_IPV6, font=FontTiny)
                          draw.text((2, 44), date_str, fill=COLOR_DATE, font=FontTiny)
                          draw.text((2, 58), time_str, fill=COLOR_TIME, font=FontTiny)
                      
                      def draw_goes_middle(image, index):
                          global goes_dynamic_frame_index
                          frames = get_goes_gif(index)
                          if frames:
                              frame = frames[goes_dynamic_frame_index[index] % len(frames)]
                              resized = frame.resize((image.width, image.height), Image.ANTIALIAS)
                              image.paste(resized, (0, 0), resized)
                              goes_dynamic_frame_index[index] += 1
                          else:
                              draw = ImageDraw.Draw(image)
                              msg = "GOES GIF\nUnavailable"
                              w, h = draw.textsize(msg, font=FontLarge)
                              draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                      
                      def draw_goes_bottom(draw, product_name=None):
                          # Compose the full text
                          base_text = "Updated every 5 min"
                          if product_name:
                              base_text = f"{product_name} - {base_text}"
                      
                          # Max width available for text on screen
                          max_width = disp_1.width - 4  # some margin (2px left and right)
                      
                          # Font to use
                          font = FontTiny
                      
                          # Simple word wrap helper
                          def wrap_text(text, font, max_width):
                              words = text.split()
                              lines = []
                              current_line = ""
                              for word in words:
                                  test_line = f"{current_line} {word}".strip()
                                  w, h = draw.textsize(test_line, font=font)
                                  if w <= max_width:
                                      current_line = test_line
                                  else:
                                      if current_line:
                                          lines.append(current_line)
                                      current_line = word
                              if current_line:
                                  lines.append(current_line)
                              return lines
                      
                          lines = wrap_text(base_text, font, max_width)
                          # Draw each line with vertical spacing
                          y = 2  # Start a bit down from top
                          line_spacing = 14  # pixels between lines
                      
                          for line in lines:
                              draw.text((2, y), line, fill=COLOR_WPAD_NONE, font=font)
                              y += line_spacing
                      
                      # --- Main loop ---
                      last_bytes_sent = 0
                      last_bytes_recv = 0
                      last_page = None  # Track the previous page to detect transitions
                      
                      try:
                          while True:
                              with state_lock:
                                  page = current_page  # safely snapshot current page under lock
                      
                              # Reset GOES animation frame index if just switched from Doppler (page 3)
                              if (4 <= page < 4 + len(GOES_PRODUCTS)) and (last_page == 3):
                                  goes_index = page - 4
                                  goes_dynamic_frame_index[goes_index] = 0
                      
                              if page == 0:
                                  image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                                  draw0 = ImageDraw.Draw(image0)
                                  draw_top_screen(draw0)
                                  disp_0.ShowImage(image0)
                      
                                  image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                                  draw = ImageDraw.Draw(image)
                                  last_bytes_sent, last_bytes_recv = draw_middle_screen(draw, (last_bytes_sent, last_bytes_recv))
                                  disp.ShowImage(image)
                      
                                  image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                                  draw1 = ImageDraw.Draw(image1)
                                  draw_wpad_status(draw1)
                                  disp_1.ShowImage(image1)
                      
                              elif page == 1:
                                  image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                                  draw0 = ImageDraw.Draw(image0)
                                  draw_weather_top(draw0)
                                  disp_0.ShowImage(image0)
                      
                                  image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                                  draw = ImageDraw.Draw(image)
                                  draw_weather_middle(draw)
                                  disp.ShowImage(image)
                      
                                  image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                                  # Pass image1 (not draw) to draw_weather_bottom if it requires the image object
                                  draw_weather_bottom(image1)
                                  disp_1.ShowImage(image1)
                      
                              elif page == 2:
                                  phase_name, illumination = get_moon_phase()
                                  # Create the display image for page 2
                                  image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                                  draw0 = ImageDraw.Draw(image0)
                                  draw_moon_top(draw0)  # top part uses draw
                                  disp_0.ShowImage(image0)
                      
                                  # Main display
                                  image = Image.new("RGB", (disp.width, disp.height), (0, 0, 0))
                                  # Pass the image (not draw) because your fixed function expects the image
                                  draw_moon_middle(image)
                                  disp.ShowImage(image)
                      
                                  image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                                  draw1 = ImageDraw.Draw(image1)
                                  draw_moon_bottom(draw1)  # bottom part uses draw
                                  disp_1.ShowImage(image1)
                      
                              elif page == 3:
                                  if time.time() - last_doppler_fetch > 300 or not doppler_frames:
                                      doppler_frames = get_doppler_image()
                                      last_doppler_fetch = time.time()
                                      doppler_frame_index = 0
                      
                                  image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                                  draw0 = ImageDraw.Draw(image0)
                                  draw_doppler_top(draw0)
                                  disp_0.ShowImage(image0)
                      
                                  image = Image.new("RGBA", (disp.width, disp.height), (0, 0, 0, 0))
                                  if doppler_frames:
                                      frame = doppler_frames[doppler_frame_index % len(doppler_frames)]
                                      image.paste(frame, (0, 0), frame)
                                      doppler_frame_index += 1
                                  disp.ShowImage(image.convert("RGB"))
                      
                                  image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                                  draw1 = ImageDraw.Draw(image1)
                                  draw_doppler_bottom(draw1)
                                  disp_1.ShowImage(image1)
                      
                              elif 4 <= page < 4 + len(GOES_PRODUCTS):
                                  goes_index = page - 4
                                  now = time.time()
                                  cache = goes_dynamic_cache[goes_index]
                      
                                  if "last_updated" not in cache or (now - cache["last_updated"]) > 300:
                                      frames = get_goes_gif(goes_index)
                                      if frames:
                                          cache["image"] = frames
                                          cache["last_updated"] = now
                                          # We do NOT reset goes_dynamic_frame_index here; it's reset on page transition
                      
                                  image0 = Image.new("RGB", (disp_0.width, disp_0.height), (0, 0, 0))
                                  draw0 = ImageDraw.Draw(image0)
                                  draw_goes_top(draw0, GOES_PRODUCTS[goes_index]["name"])
                                  disp_0.ShowImage(image0)
                      
                                  image = Image.new("RGBA", (disp.width, disp.height), (0, 0, 0, 0))
                                  frames = cache.get("image")
                                  if frames:
                                      frame = frames[goes_dynamic_frame_index[goes_index] % len(frames)]
                                      image.paste(frame, (0, 0), frame)
                                      goes_dynamic_frame_index[goes_index] += 1
                                  else:
                                      draw = ImageDraw.Draw(image)
                                      msg = "GOES GIF\nUnavailable"
                                      w, h = draw.textsize(msg, font=FontLarge)
                                      draw.text(((image.width - w) // 2, (image.height - h) // 2), msg, fill=COLOR_WPAD_NONE, font=FontLarge)
                      
                                  disp.ShowImage(image.convert("RGB"))
                      
                                  image1 = Image.new("RGB", (disp_1.width, disp_1.height), (0, 0, 0))
                                  draw1 = ImageDraw.Draw(image1)
                                  draw_goes_bottom(draw1, GOES_PRODUCTS[goes_index]["name"])
                                  disp_1.ShowImage(image1)
                      
                              # Sleep delays based on page type
                              if page in [3] + list(range(4, 4 + len(GOES_PRODUCTS))):
                                  time.sleep(FRAME_DELAYS["animation"])
                              else:
                                  time.sleep(FRAME_DELAYS["default"])
                      
                              last_page = page  # Update last_page after processing
                      
                      except KeyboardInterrupt:
                          print("Exiting...")
                          disp_0.clear()
                          disp_1.clear()
                          disp.clear()
                          sys.exit()
                      

                      use these images
                      https://commons.wikimedia.org/wiki/Category:Lunar_phases

                      Fase 1-16 need to be used

                      1 Reply Last reply Reply Quote 0
                      • JonathanLeeJ
                        JonathanLee
                        last edited by JonathanLee

                        This is a better WPAD file

                        server.modules = (
                            "mod_access",
                            "mod_staticfile",
                            "mod_expire",
                            "mod_setenv"
                        )
                        
                        server.document-root        = "/var/www/html"
                        server.errorlog             = "/var/log/lighttpd/error.log"
                        server.pid-file             = "/run/lighttpd.pid"
                        server.username             = "www-data"
                        server.groupname            = "www-data"
                        server.port                 = 80
                        server.bind                 = "192.168.1.6"
                        server.tag                  = ""
                        server.range-requests       = "disable"
                        server.max-connections      = 10
                        connect-timeout             = 2
                        server.max-keep-alive-idle  = 2
                        server.max-keep-alive-requests = 1
                        server.max-read-idle        = 2
                        server.max-write-idle       = 2
                        dir-listing = "disable"
                        
                        $HTTP["request-method"] =~ "^(TRACE|TRACK)$" {
                          url.access-deny = ( "" )
                        }
                        
                        # Cache WPAD and proxy PAC files for 1 day (good practice)
                        expire.url = (
                          "/wpad.dat" => "access plus 1 day",
                          "/proxy.pac" => "access plus 1 day"
                        )
                        
                        # Disable access logs to reduce SD card wear (optional)
                        accesslog = ""
                        
                        $HTTP["url"] =~ "^/(wpad\.dat|proxy\.pac)$" {
                          setenv.add-response-header = (
                            "X-Content-Type-Options"        => "nosniff",
                            "X-Frame-Options"               => "DENY",
                            "Content-Security-Policy"       => "default-src 'none';",
                            "Cache-Control"                 => "public, max-age=86400",
                            "Referrer-Policy"               => "no-referrer",
                            "X-Download-Options"            => "noopen",
                            "X-Permitted-Cross-Domain-Policies" => "none"
                          )
                        
                          # Allow only GET and HEAD methods
                          $HTTP["request-method"] !~ "^(GET|HEAD)$" {
                            url.access-deny = ( "" )
                          }
                        
                          # Restrict access by IP subnets
                          $HTTP["remoteip"] == "192.168.1.0/27" { }
                          else $HTTP["remoteip"] == "2001:470:8052:a::/64" { }
                          else {
                            url.access-deny = ( "" )
                          }
                        }
                        
                        # Deny all other URL requests
                        $HTTP["url"] !~ "^/(wpad\.dat|proxy\.pac)$" {
                          url.access-deny = ( "" )
                        }
                        
                        # Strict URL parsing for security and consistency
                        server.http-parseopts = (
                          "header-strict"           => "enable",
                          "host-strict"             => "enable",
                          "host-normalize"          => "enable",
                          "url-normalize-unreserved"=> "enable",
                          "url-normalize-required"  => "enable",
                          "url-ctrls-reject"        => "enable",
                          "url-path-2f-decode"      => "disable",
                          "url-path-2f-reject"      => "enable",
                          "url-path-dotseg-remove"  => "disable",
                          "url-path-dotseg-reject"  => "enable",
                        )
                        
                        url.access-deny             = ( "~", ".inc" )
                        static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
                        
                        # Add WPAD MIME type for correct browser handling
                        mimetype.assign = (
                          ".dat" => "application/x-ns-proxy-autoconfig",
                          ".pac" => "application/x-ns-proxy-autoconfig"
                        )
                        
                        1 Reply Last reply Reply Quote 0
                        • First post
                          Last post
                        Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.