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
    17 Posts 1 Posters 1.6k 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 JonathanLee

      Keep in mind you will have to install an SSL certificate on the Raspberry PI to download the GIT hub file and program. You have to copy the SSL file over to the pi by way of a USB drive and reconfigure your ca certificates for that, or just connect the pi to a non-proxied system to get your files after set it back. If you are like me you just add the certificate so you can access the proxy.

      To add the firewalls certificate used with the proxy first download it directly from the firewall so you know it is yours.
      after

      sudo mkdir /media/usb
      sudo mount /dev/sda2 /media/usb -o umask=000
      cd /media/usb 
      #find your file from the flash drive lets call it CA-Cert.crt
      #copy it to the locations is it needed at
      sudo cp CA-Cert.crt /etc/ssl/certs
      sudo cp CA-Cert.crt /usr/share/ca-certificates/
      sudo cp CA-Cert.crt /usr/local/share/ca-certificates/
      sudo update-ca-certificates
      

      This should fix your certificate issue to use wget while in the proxy.

      Final step we need to make sure that once inside the proxy that you can't call the wpad, meaning that the WPAD is a one-way ticket that the proxy or it's cache cannot access it for added security.

      Add this to custom options on the Squid configuration, it is not really needed I add this for fear of container issues,

      acl wpad urlpath_regex ^/wpad.dat$
      acl wpad urlpath_regex ^/proxy.pac$
      acl wpad urlpath_regex ^/wpad.da$
      #deny_info 200:/etc/squid/wpad.dat wpad # this is if you manage wpad from squid not needed here
      reply_header_access Content-Type deny wpad
      
      1 Reply Last reply Reply Quote 0
      • 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
                        • First post
                          Last post
                        Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.