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