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

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

Scheduled Pinned Locked Moved Cache/Proxy
wpadproxysquidsquid-proxy
18 Posts 1 Posters 1.8k 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.
  • J
    JonathanLee
    last edited by JonathanLee Jan 6, 2025, 11:02 PM Dec 11, 2024, 10:41 PM

    Hello fellow Netgate community members, I wanted to share a simple how to guide for creating a WPAD server on a Raspberry Pi Zero 2 W.

    First install a headless Raspberrian OS on a fresh card.

    Second ssh into the new WPAD server.

    ssh username@192.168.1.6 or whatever you set your wpad IP address to.

    Second as you will have this behind the proxy set your environments to utilize the proxy system.

    First navigate to the following path

    /etc/apt/apt.conf.d
    
    run sudo nano 10proxy
    

    inside this new file add the following

    Acquire::http::Proxy "http://192.168.1.1:3128/";
    Acquire::https::Proxy "http://192.168.1.1:3128/";
    

    d6a333c2-80af-4140-8f80-25b79755a4b5-image.png

    ctl x save the file

    next run the following command

    sudo nano /etc/environment
    

    add the following config lines

    http_proxy="http://192.168.1.1:3128/"
    https_proxy="http://192.168.1.1:3128/"
    no_proxy="localhost,127.0.0.1"
    

    9249ee4e-884c-49fa-bdee-f589ea92aed8-image.png

    save the file also

    reboot the system

    now after reboot run

    apt update
    apt upgrade
    

    once this is completed reboot again run the following.

    sudo apt install lighttpd -y
    

    after this is installed take a system on the same lan and navigate to the address of the raspberry pi box http://192.168.1.6 as an example you should see a splash screen if not you have to make sure it is accessible.

    After it is accessible navigate to the following path

    /var/www/html
    

    sudo rm the file to delete the file that is inside this folder this is the default webserver file.

    Now let's make your wpad files.

    sudo nano proxy.pac
    

    inside this add the following this is my wpad or add your own version

    function FindProxyForURL(url, host) 
    {
    url = url.toLowerCase();
    host = host.toLowerCase();
    
    if (isPlainHostName(host)) 
    {
      return 'DIRECT';
    }
    
    if (isResolvable(host)) 
    {
    var hostIP = dnsResolve(host);
    
    if (isInNet(hostIP, '0.0.0.0', '255.0.0.0') || isInNet(hostIP, '10.0.0.0', '255.0.0.0') ||
    isInNet(hostIP, '127.0.0.0', '255.0.0.0') || isInNet(hostIP, '169.254.0.0', '255.255.0.0') ||
    isInNet(hostIP, '172.16.0.0', '255.240.0.0') || isInNet(hostIP, '192.168.0.0', '255.255.0.0') ||
    isInNet(hostIP, '198.18.0.0', '255.254.0.0') || isInNet(hostIP, '224.0.0.0', '240.0.0.0') ||
    isInNet(hostIP, '240.0.0.0', '240.0.0.0')) 
    {
      return 'DIRECT';
    }
    
    if (false) 
    {
      return 'DIRECT';
    }
    }
    
    if (url.substring(0, 5) == 'http:' || url.substring(0, 6) == 'https:' ||
    url.substring(0, 4) == 'ftp:' ||  url.substring(0, 7) == "gopher:") 
    {
      return 'PROXY 192.168.1.1:3128';
    }
    
    return 'DIRECT';
    }
    
    

    after
    lets create linker files

    sudo ln -s /var/www/html/proxy.pac /var/www/html/wpad.dat
    sudo ln -s /var/www/html/proxy.pac /var/www/html/wpad.da

    00a26901-c396-492a-bbdb-d5ba57f102ec-image.png
    you should have three files. Now if you navigate to http://192.168.1.10/proxy.pac it should download the file.

    Next add the following DHCP options in your pfsense
    a39b8511-7bcc-4256-b486-f7841e248950-image.png
    3 option 252 with string and the path to your wpad I am using 192.168.1.6 as an example.

    "http://192.168.1.6/proxy.pac"
    "http://192.168.1.6/wpad.dat"
    "http://192.168.1.6/wpad.da"

    After this also add a DNS entry to help resolve the wpad

    057c4d49-5bc2-4113-b030-2ffc581be691-image.png

    to test this you would do

    nslookup wpad
    

    84c1b852-30b2-4595-b06b-2481580f48f9-image.png
    and it should always respond with a wpad address.

    Now change your system that is the road warrior to auto proxy to test if it works. Once it does lock the WPAD out of using the internet as it only serves one purpose handing out your wpad files It doesn't really need internet.

    e3a8190f-5619-413d-b4f8-3eed1e2aedbb-image.png

    Please let me know if I missed something. This way you can have https only for the firewall and still use the older wpad protocol for auto configurations. I wish they would update the WPAD protocol to make a better version again, that has nothing to do with this.

    J 1 Reply Last reply Dec 11, 2024, 10:52 PM Reply Quote 0
    • J
      JonathanLee @JonathanLee
      last edited by Dec 11, 2024, 10:52 PM

      @JonathanLee I was so tried of manually setting this every day, this fixed all my issues for traveling laptops. Please if you have any security recommendations for lighttpd let me know. Right now I have disabled all WAN access and it also can not access the proxy or cache.

      1 Reply Last reply Reply Quote 0
      • J
        JonathanLee
        last edited by Dec 15, 2024, 8:29 PM

        I wanted to share some more info on security hardening your wpad after

        Added by gstrauss 4 days ago

        Security hardening of any webserver starts with restricting permissions to only what is needed.

        If you do not need the webserver to run as root, then you should run the webserver as a less-privileged user, e.g. I believe Raspberry Pi Zero runs lighttpd as user www-data by default, but you should check. The Debian-based Raspberry Pi sets up various permissions and locations for lighttpd to write access and error logs, jobs to clean up temp files, etc, so changing user under which the webserver runs requires additional steps to get back the functionality. Prefer to use www-data.

        If you do not need the webserver to be public-facing, make sure to configure the webserver and/or firewall so that the webserver can only serve requests from the local networks.

        If you do not need the webserver to do anything besides access files read-only, then you should might consider making the document root owned by a different user and read-only to webserver user.

        For lighttpd, if you are only serving read-only files, then you should restrict the size of HTTP requests to a low (non-zero) number. (0 disables the limit) See server.max-request-size
        For resource-constrained servers like the Raspberry Pi Zero, you might tune the server to reduce the chance that malicious clients can deny service to others. However, if you're serving wpad, that should be on an internal network, not internet-facing, so you should configure lighttpd to listen only on your internal network IP, and not on a public-facing IP. You could also configure lighttpd and/or your firewall to only allow access to port 80 via the local network.

        If you do not need access logs, then you might disable access logging in lighttpd to reduce resource usage.
        If you are only serving static files, you might reduce connection timeouts since you expect lighttpd to serve files very quickly.
        If you are only serving wpad, then you might reduce the number of keep-alive requests allowed per client before lighttpd closes the connection.

        Besides running as non-root, and listening and serving clients only from local network (not internet), which are strongly recommended for security hardening, the rest is resource tuning for availability and performance. Still, even without extra tuning on a Raspberry Pi Zero, you should find that lighttpd can serve thousands of requests per second for a small, static wpad file (proxy.pac or wpad.dat)

        See WikiStart and links to
        Docs_ConfigurationOptions
        Docs_ResourceTuning
        Docs_Performance

        Side note I really do not understand why WPAD has never been updated to something like WPAD2.0 protocol because of the associated risks with it. It seems from a security perspective that big tech should update this older protocol.

        Look up "zero trust architecture" in a search engine.
        If you the clients already have pre-installed an SSL certificate for the the proxy you assign, and only uses https, then a malicious wpad won't be able to direct the client to send http requests through a rogue server without certificate failures.

        Still, on new networks, if you have not already pre-configured (more secure), then many architectures follow TOFU (trust on first use) principles.

        1 Reply Last reply Reply Quote 0
        • J
          JonathanLee
          last edited by JonathanLee 10 days ago Dec 16, 2024, 8:31 AM

          security hardening continued for wpad server

          sudo nano /etc/lighttpd/lighttpd.conf

          add the following

          first line blocks all access but local network with a negated != rule. Get it Netgated rule :) funny funny.

          Ok ok and after get more granular and add rules so local network can only access the wpad files and nothing else on the external web server done.

          also add a shorter connection timeout

          $HTTP["remoteip"] != "192.168.1.0/27" {
          url.access-deny = ( "" )
          }
          }
          
          $HTTP["url"] =~ "^/wpad.dat" {
          $HTTP["remoteip"] == "192.168.1.0/27" {
          }
          else {
          url.access-deny = ( "" )
          }
          }
          
          $HTTP["url"] =~ "^/proxy.pac" {
          $HTTP["remoteip"] == "192.168.1.0/27" {
          }
          else {
          url.access-deny = ( "" )
          }
          }
          
          $HTTP["url"] =~ "^/wpad.da" {
          $HTTP["remoteip"] == "192.168.1.0/27" {
          }
          else {
          url.access-deny = ( "" )
          }
          }
          

          This helped me alot first block all but local network accessing it after make it only allow local to wpad files tested ok with many tests

          or a better /etc/lighttpd/lighttpd.conf

          server.modules = (
              "mod_access",
              "mod_staticfile",
              "mod_expire"
          )
          
          server.document-root        = "/var/www/html"
          server.errorlog             = "/var/log/lighttpd/error.log"
          server.pid-file             = "/run/lighttpd.pid"
          server.username             = "www-data"
          server.groupname            = "www-data"
          server.port                 = 80
          server.bind                 = "192.168.1.6"
          server.tag                  = ""
          server.range-requests       = "disable"
          server.max-connections      = 10
          connect-timeout             = 2
          server.max-keep-alive-idle = 2
          server.max-keep-alive-requests = 1
          server.max-read-idle = 2
          server.max-write-idle = 2
          
          dir-listing = "disable"
          
          # Cache WPAD and proxy PAC files for 1 day (good practice)
          expire.url = (
            "/wpad.dat" => "access plus 1 day",
            "/proxy.pac" => "access plus 1 day"
          )
          
          # Disable access logs to reduce SD card wear (optional)
          accesslog = ""
          
          # Restrict WPAD/PAC access to local network only
          $HTTP["remoteip"] !~ "^(192\.168\.1\.|2001:470:8052:a::)" {
            $HTTP["url"] =~ "^/(wpad\.dat|proxy\.pac)$" {
              url.access-deny = ( "" )
            }
          }
          
          # Block any URL except wpad.dat and proxy.pac for all clients
          $HTTP["url"] !~ "^/(wpad\.dat|proxy\.pac)$" {
            url.access-deny = ( "" )
          }
          
          # Strict URL parsing for security and consistency
          server.http-parseopts = (
            "header-strict"           => "enable",
            "host-strict"             => "enable",
            "host-normalize"          => "enable",
            "url-normalize-unreserved"=> "enable",
            "url-normalize-required"  => "enable",
            "url-ctrls-reject"        => "enable",
            "url-path-2f-decode"      => "enable",
            #"url-path-2f-reject"      => "enable",
            "url-path-dotseg-remove"  => "enable",
            #"url-path-dotseg-reject"  => "enable",
            #"url-query-20-plus"       => "enable",
          )
          
          url.access-deny             = ( "~", ".inc" )
          static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
          
          # Default IPv6 fallback
          include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
          
          # Add WPAD MIME type for correct browser handling
          mimetype.assign = (
            ".dat" => "application/x-ns-proxy-autoconfig",
            ".pac" => "application/x-ns-proxy-autoconfig"
          )
          
          1 Reply Last reply Reply Quote 0
          • J
            JonathanLee
            last edited by Dec 16, 2024, 4:47 PM

            Also some changes per lighted recommendations

            server.max-request-size     = 8
            server.tag                  = ""
            server.range-requests       = "disable"
            server.max-connections      = 12
            connect-timeout             = 2
            
            1 Reply Last reply Reply Quote 0
            • J
              JonathanLee
              last edited by Dec 18, 2024, 7:27 AM

              Also you can connect a LCD HAT to this from wave share and program it to show who is logged in.

              IMG_1498.jpg

              I have the code adapted to output w in a loop so I can see users logged in to the wpad pretty cool

              1 Reply Last reply Reply Quote 0
              • J
                JonathanLee
                last edited by JonathanLee 20 days ago Dec 18, 2024, 9:38 PM

                Continued additional security for your WPAD:

                I have added an LCD hat to the Zero Pi that uses an adapted vendor provided example python code file. This is so that the screen will also show if any users are logged into the WPAD server. Again, this is very simple to get you into programing a display, it is simple we are adding a string for users logged in to display and adding a service.

                Here is a how to guide for this also now that your WPAD is configured and running let us add the LCD hat.

                Supplies:
                Zero LCD HAT (A)

                https://www.amazon.com/LCD-HAT-Secondary-Compatible-Zero/dp/B0DKF8L7VF

                You will have to add the GPIO standoff as it is not included in the Zero 2 W you have to manually add the GPIO header so the LCD hat can be plugged in first.

                Ok so you got it connected follow the guide for configuration of this.
                https://www.waveshare.com/wiki/Zero_LCD_HAT_(A)
                I have included the code here also with my adapted steps.

                #Enable SPI
                sudo raspi-config nonint do_spi 0
                #Install Python3 
                sudo apt-get update
                sudo apt-get install python3-pip
                sudo apt-get install python3-pil
                sudo apt-get install python3-numpy
                sudo apt install python3-luma.oled
                sudo apt install python3-luma.lcd
                sudo apt install python3-RPi.GPIO
                sudo apt install python3-spidev
                #Get the Demo Files
                sudo apt-get install unzip -y
                sudo wget https://files.waveshare.com/wiki/Zero-LCD-HAT-A/Zero_LCD_HAT_A_Demo.zip
                sudo unzip ./Zero_LCD_HAT_A_Demo.zip
                cd Zero_LCD_HAT_A_Demo
                
                you need to add "dtoverlay=spi1-1cs" to the config.txt file for opening the SPI device.
                
                sudo nano /boot/firmware/config.txt
                
                
                #adapt the demo file 
                sudo cp CPU.py CPU2.py
                sudo nano CPU2.py
                

                Adapt the CPU2.py file to activate the middle screen and add the users section.

                This is a simple addition do not reinvent the program just activate the new screen with the code given add the string for the output of "w" so that you can always see the current users logged in.

                #!/usr/bin/python
                # -*- coding: UTF-8 -*-
                #import chardet
                import os
                import sys
                import time
                import logging
                import spidev as SPI
                import subprocess
                sys.path.append("..")
                from lib import LCD_0inch96
                from lib import LCD_1inch3
                from lib import Gain_Param
                from PIL import Image,ImageDraw,ImageFont
                import RPi.GPIO as GPIO
                import re
                import math
                
                # Raspberry Pi pin configuration:
                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
                
                logging.basicConfig(level=logging.DEBUG)
                
                try:
                    disp_0 = LCD_0inch96.LCD_0inch96(spi=SPI.SpiDev(bus_0, device_0),spi_freq=10000000,rst=RST_0,dc=DC_0,bl=BL_0,bl_freq=1000)
                    disp_1 = LCD_0inch96.LCD_0inch96(spi=SPI.SpiDev(bus_1, device_1),spi_freq=10000000,rst=RST_1,dc=DC_1,bl=BL_1,bl_freq=1000)
                    disp = LCD_1inch3.LCD_1inch3(spi=SPI.SpiDev(bus, device),spi_freq=10000000,rst=RST,dc=DC,bl=BL)
                
                    gain = Gain_Param.Gain_Param()
                
                    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)
                
                    image1 = Image.new("RGB", (disp_0.width, disp_0.height), "WHITE")
                    draw = ImageDraw.Draw(image1)
                    Font2 = ImageFont.truetype("../Font/Font00.ttf",7)
                    while True:
                        #IP
                        ip = gain.GET_IP()
                        Font1 = ImageFont.truetype("../Font/Font00.ttf",15)
                        draw.text((5, 0), 'IP : '+ip, fill = 0x3cbdc4,font=Font1)
                
                        #time
                        time_t = time.strftime("%H:%M:%S", time.localtime())
                        time_D = time.strftime("%Y-%m-%d ", time.localtime())
                        draw.text((5, 25), "Date: "+time_D, fill = 0x46D017,font=Font1)
                        draw.text((5, 50), "Time: "+time_t, fill = 0xf7ba47,font=Font1)
                        disp_0.ShowImage(image1)
                        draw.rectangle((0,0,disp_0.width,disp_0.height),fill = "WHITE") #Cache area covered with white
                
                        #CPU usage
                        CPU_usage= os.popen('top -bi -n 2 -d 0.02').read().split('\n\n\n')[0].split('\n')[2]
                        CPU_usage= re.sub('[a-zA-z%(): ]','',CPU_usage)
                        CPU_usage= CPU_usage.split(',')
                        CPU_usagex =100 - eval(CPU_usage[3])
                        draw.text((5, 0), "CPU Usage: " + str(math.floor(CPU_usagex))+'%', fill = 0x0b46e3,font=Font1,)
                
                        #Message
                        user = subprocess.run(['w'], stdout=subprocess.PIPE).stdout.decode('utf-8')
                        image3 = Image.new("RGB", (disp.width, disp.height), "WHITE")
                        draw1 = ImageDraw.Draw(image3)
                        draw1.text((5, 0), "WPAD SERVER: \n\n"+user, fill = 0x3cbdc4, font=Font2)
                        disp.ShowImage(image3)
                        draw1.rectangle((0,0,disp.width, disp.height), "WHITE")
                
                        #TEMP
                        temp_t = gain.GET_Temp()
                        draw.text((5, 25), "Temp: "+str(math.floor(temp_t))+'℃', fill = 0x0088ff,font=Font1)
                
                        #System disk usage
                        x = os.popen('df -h /')
                        i2 = 0
                        while 1:
                            i2 = i2 + 1
                            line = x.readline()
                            if i2==2:
                                Capacity_usage = line.split()[4] # Memory usage (%)
                                Hard_capacity = int(re.sub('[%]','',Capacity_usage))
                                break
                
                        draw.text((5, 50), "Disk Usage: "+str(math.floor(Hard_capacity))+'%', fill = 0x986DFC,font=Font1) # BGR
                
                        disp_1.ShowImage(image1)
                        draw.rectangle((0,0,disp_0.width,disp_0.height),fill = "WHITE")
                        time.sleep(0.01)
                
                
                    disp_0.module_exit()
                    disp_1.module_exit()
                    logging.info("quit:")
                except IOError as e:
                    logging.info(e)
                except KeyboardInterrupt:
                    disp_0.module_exit()
                    disp_1.module_exit()
                    logging.info("quit:")
                    exit()
                
                

                save your adapted CPU2.py example file.

                Test with

                sudo python CPU2.py
                

                It should run on and run, if not check connections.

                Now that it is working, we need to create a service so that it will run every time you reboot or turn on your Raspberry pi.
                You might have to add sudo chmod to the .py file to add +x to it to make it executable.

                #Navigate to:
                cd /lib/systemd/system/
                #create a new service
                sudo nano display.service
                #add the following into the service file. Make sure your working directory is where the python code is.
                
                [Unit]
                Description=Display
                After=network.target
                
                [Service]
                User=root
                WorkingDirectory=/usr/Zero_LCD_HAT_A_Demo/python/example/
                ExecStart=/usr/bin/python3 /usr/Zero_LCD_HAT_A_Demo/python/example/CPU2.py
                Restart=always
                RestartSec=3
                StandardOutput=file:/var/log/example_service_output.log
                StandardError=file:/var/log/example_service_error.log
                
                [Install]
                WantedBy=multi-user.target
                
                #save this ctl x
                
                #Run the following 
                sudo systemctl daemon-reload
                sudo systemctl enable display.service
                sudo systemctl start display.service
                #check for errors
                sudo systemctl status display.service
                #It should display this if it works:
                ● display.service - Display
                     Loaded: loaded (/lib/systemd/system/display.service; enabled; preset: enabled)
                     Active: active (running) since Wed 2024-12-18 06:45:23 PST; 6h ago
                   Main PID: 507 (python3)
                      Tasks: 6 (limit: 178)
                        CPU: 2h 18min 29.878s
                     CGroup: /system.slice/display.service
                             └─507 /usr/bin/python3 /usr/Zero_LCD_HAT_A_Demo/python/example/CPU2.py
                
                Dec 18 06:45:23 Zero systemd[1]: Started display.service - Display.
                
                #Once it says enabled run
                sudo reboot
                #the dispaly should always run now.
                
                

                That is it you should now know who is logged into the WPAD at all times, if your not in a SSH session into it the screen show should no users. Meaning that it will only change when someone is on a terminal session of your pi. The screen file is very basic but it can help push you to customize it yourself.

                1 Reply Last reply Reply Quote 0
                • J
                  JonathanLee
                  last edited by JonathanLee Dec 18, 2024, 10:00 PM Dec 18, 2024, 9:44 PM

                  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
                  • J
                    JonathanLee
                    last edited by Dec 22, 2024, 6:07 PM

                    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
                    • J
                      JonathanLee
                      last edited by Feb 1, 2025, 4:59 AM

                      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
                      • J JonathanLee referenced this topic on Mar 31, 2025, 11:47 PM
                      • J
                        JonathanLee
                        last edited by 18 days ago

                        This post is deleted!
                        1 Reply Last reply Reply Quote 0
                        • J
                          JonathanLee
                          last edited by 17 days ago

                          This post is deleted!
                          1 Reply Last reply Reply Quote 0
                          • J
                            JonathanLee
                            last edited by 16 days ago

                            This post is deleted!
                            1 Reply Last reply Reply Quote 0
                            • J
                              JonathanLee
                              last edited by 15 days ago

                              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
                              • J
                                JonathanLee
                                last edited by 14 days ago

                                This post is deleted!
                                1 Reply Last reply Reply Quote 0
                                • J
                                  JonathanLee
                                  last edited by 13 days ago

                                  This post is deleted!
                                  1 Reply Last reply Reply Quote 0
                                  • J
                                    JonathanLee
                                    last edited by JonathanLee 13 days ago 13 days ago

                                    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
                                    • J
                                      JonathanLee
                                      last edited by JonathanLee 9 days ago 9 days ago

                                      This is a better WPAD file

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