UNOFFICIAL GUIDE: Squid external Raspberry PI WPAD lighttpd server Guide with Raspberry Zero LCD HAT code.
-
Also some changes per lighted recommendations
server.max-request-size = 8 server.tag = "" server.range-requests = "disable" server.max-connections = 12 connect-timeout = 2
-
Also you can connect a LCD HAT to this from wave share and program it to show who is logged in.
I have the code adapted to output w in a loop so I can see users logged in to the wpad pretty cool
-
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.
-
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.
aftersudo 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
-
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.
-
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/passwdfor 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.
-
J JonathanLee referenced this topic on
-
This post is deleted! -
This post is deleted! -
This post is deleted! -
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()
-
This post is deleted! -
This post is deleted! -
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_phasesFase 1-16 need to be used