import os
import sys
import time
import psutil
import subprocess
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
# 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 = “put your api 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 radar image URL (replace if needed)
DOPPLER_URL = "https://radar.weather.gov/ridge/standard/KMUX_loop.gif"
# 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:
img = Image.open(BytesIO(response.content)).convert("RGBA")
doppler_cache["image"] = img
doppler_cache["last_updated"] = now
return img
except Exception:
pass
return None
# --- Moon phase logic ---
def get_moon_phase():
import math
days_since_new = (time.time() / 86400.0) % 29.53 # lunar cycle ~29.53 days
illumination = 0
phase_name = "New Moon"
if days_since_new < 1.84566:
phase_name = "New Moon"
illumination = 0
elif days_since_new < 5.53699:
phase_name = "Waxing Crescent"
illumination = int((days_since_new - 1.84566) / (5.53699 - 1.84566) * 50)
elif days_since_new < 9.22831:
phase_name = "First Quarter"
illumination = 50
elif days_since_new < 12.91963:
phase_name = "Waxing Gibbous"
illumination = int(50 + (days_since_new - 9.22831) / (12.91963 - 9.22831) * 50)
elif days_since_new < 16.61096:
phase_name = "Full Moon"
illumination = 100
elif days_since_new < 20.30228:
phase_name = "Waning Gibbous"
illumination = int(100 - (days_since_new - 16.61096) / (20.30228 - 16.61096) * 50)
elif days_since_new < 23.99361:
phase_name = "Last Quarter"
illumination = 50
elif days_since_new < 27.68493:
phase_name = "Waning Crescent"
illumination = int(50 - (days_since_new - 23.99361) / (27.68493 - 23.99361) * 50)
else:
phase_name = "New Moon"
illumination = 0
return phase_name, illumination
def draw_moon_icon(draw, center_x, center_y, radius, illumination):
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:
draw.ellipse((center_x - radius, center_y - radius, center_x + radius, center_y + radius), fill=shadow_color)
elif illumination == 100:
pass
else:
dark_frac = 1 - (illumination / 100.0)
offset = int(radius * 2 * dark_frac)
if illumination < 50:
draw.ellipse((center_x + radius - offset, center_y - radius, center_x + radius * 2, center_y + radius), fill=shadow_color)
else:
draw.ellipse((center_x - radius * 2, center_y - radius, center_x - radius + offset, center_y + radius), fill=shadow_color)
# Setup displays
gain = Gain_Param.Gain_Param()
spi0 = SPI.SpiDev(BUS_0, DEVICE_0)
spi1 = SPI.SpiDev(BUS_1, DEVICE_1)
spi_main = SPI.SpiDev(BUS, DEVICE)
spi0.open(BUS_0, DEVICE_0)
spi1.open(BUS_1, DEVICE_1)
spi_main.open(BUS, DEVICE)
spi0.max_speed_hz = 10000000
spi1.max_speed_hz = 10000000
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 = disp.gpio_mode(KEY1_PIN, disp.INPUT, None)
# 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}%")
import gpiozero
key2 = gpiozero.Button(KEY2_PIN)
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
def key1_callback():
global current_page
current_page = (current_page + 1) % 4 # 4 pages now
print(f"Button pressed: current_page = {current_page}")
key1.when_activated = key1_callback
# --- 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)
def draw_doppler_middle(image):
doppler_img = get_doppler_image()
if doppler_img:
doppler_resized = doppler_img.resize((image.width, image.height), Image.ANTIALIAS)
image.paste(doppler_resized, (0, 0), doppler_resized)
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)
# --- Main loop ---
last_bytes_sent = 0
last_bytes_recv = 0
try:
while True:
if current_page == 0:
# Original multi-display page
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 current_page == 1:
# Weather page
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 current_page == 2:
# Moon phase page
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)
else:
# Doppler radar page (page 3)
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("RGB", (disp.width, disp.height), (0, 0, 0))
draw_doppler_middle(image)
disp.ShowImage(image)
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)
time.sleep(1)
except KeyboardInterrupt:
print("Exiting...")
disp_0.clear()
disp_1.clear()
disp.clear()
sys.exit()
Here is a better version of the code.