first commit

This commit is contained in:
2026-02-09 22:07:10 +01:00
commit 5117a9ef4d
5 changed files with 352 additions and 0 deletions

264
PiScope.py Executable file
View File

@@ -0,0 +1,264 @@
import time
import os
import psutil
import socket
import subprocess
from datetime import datetime
from collections import deque
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw, ImageFont
# ---------------- CONSTANTS ----------------
I2C_PORT = 3
I2C_ADDR = 0x3C
CPU_HISTORY_LEN = 100
UPDATE_INTERVAL = 0.5
GRAPH_HEIGHT = 30
GRAPH_FOOTER_HEIGHT = 10
RAM_BAR_WIDTH = 25
RAM_BAR_HEIGHT = 52
FS_BOX = (64, 42, 100, 63)
FS_PIE_CENTER = (74, 52)
FS_PIE_RADIUS = 9
scroll_offset = 0
SCROLL_SPEED = 2 # pixels per update
# ---------------- OLED SETUP ----------------
serial = i2c(port=I2C_PORT, address=I2C_ADDR)
device = ssd1306(serial)
WIDTH, HEIGHT = device.width, device.height
cpu_history = deque([0] * CPU_HISTORY_LEN, maxlen=CPU_HISTORY_LEN)
try:
small_font = ImageFont.truetype("DejaVuSans.ttf", 10)
except IOError:
small_font = ImageFont.load_default()
# ---------------- HELPERS ----------------
def get_cpu_temp():
temps = psutil.sensors_temperatures()
if temps:
return next(iter(temps.values()))[0].current
try:
with open("/sys/class/thermal/thermal_zone0/temp") as f:
return int(f.read()) / 1000
except OSError:
return 0.0
def get_root_fs_usage():
return psutil.disk_usage("/").percent
def format_uptime():
boot = datetime.fromtimestamp(psutil.boot_time())
seconds = int((datetime.now() - boot).total_seconds())
hours, rem = divmod(seconds, 3600)
minutes, _ = divmod(rem, 60)
return hours, minutes
def truncate_text(text, max_len=8):
if len(text) <= max_len:
return text
return text[:max_len - 1] + ""
def get_wifi_info(interface="wlan0"):
try:
result = subprocess.run(
["iw", "dev", interface, "link"],
capture_output=True,
text=True
)
if "Not connected" in result.stdout:
return "--", 0
ssid = "--"
signal_dbm = None
for line in result.stdout.splitlines():
line = line.strip()
if line.startswith("SSID:"):
ssid = line.split("SSID:")[1].strip()
if line.startswith("signal:"):
signal_dbm = int(line.split()[1])
# Convert dBm → percentage (rough but standard)
if signal_dbm is not None:
signal_pct = max(0, min(100, 2 * (signal_dbm + 100)))
else:
signal_pct = 0
return truncate_text(ssid), signal_pct
except Exception:
return "--", 0
def estimate_power_w(cpu_percent):
# Constants (from measurement)
BASE_POWER = 1.9 # W
IDLE_FREQ_POWER = 0.3 # W at max freq
MAX_CPU_POWER = 3.0 # W at full load + max freq
MIN_FREQ = 614.0 # MHz
MAX_FREQ = 1600.0 # MHz
# Read current frequency (assume all cores similar)
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") as f:
freq_mhz = int(f.read()) / 1000
freq_factor = max(
0.0,
min(1.0, (freq_mhz - MIN_FREQ) / (MAX_FREQ - MIN_FREQ))
)
load = cpu_percent / 100.0
power = (
BASE_POWER +
IDLE_FREQ_POWER * freq_factor +
MAX_CPU_POWER * freq_factor * load
)
return round(power, 2)
# ---------------- DRAWING ----------------
def draw_cpu_graph(draw, cpu, load, temp, watts):
draw.rectangle((0, 0, CPU_HISTORY_LEN, GRAPH_HEIGHT), outline=255)
if len(cpu_history) > 1:
points = [
(i, GRAPH_HEIGHT - (v / 100 * GRAPH_HEIGHT))
for i, v in enumerate(cpu_history)
]
points += [(len(cpu_history) - 1, GRAPH_HEIGHT), (0, GRAPH_HEIGHT)]
draw.polygon(points, fill=255)
footer_top = GRAPH_HEIGHT
footer_bottom = GRAPH_HEIGHT + GRAPH_FOOTER_HEIGHT
draw.rectangle((0, footer_top, CPU_HISTORY_LEN, footer_bottom), outline=255)
draw.rectangle((0, footer_top, 25, footer_bottom), fill=255)
draw.text((1, footer_top - 1), "CPU", font=small_font, fill=0)
draw.line((59, footer_top, 59, footer_bottom), fill=255, width=2)
load_text = f"{watts:.1f} W"
lw = draw.textbbox((0, 0), load_text, font=small_font)[2]
load_x = 25 + (59 - 25 - lw) // 2
draw.text((load_x, footer_top - 1), load_text, font=small_font, fill=255)
cpu_text = f"{cpu:.0f}%"
cw = draw.textbbox((0, 0), cpu_text, font=small_font)[2]
cpu_x = 59 + (CPU_HISTORY_LEN - 59 - cw) // 2
draw.text((cpu_x, footer_top - 1), cpu_text, font=small_font, fill=255)
draw.rectangle((0, 0, 26, 10), fill=255)
draw.text((1, -1), f"{temp:.0f}°C", font=small_font, fill=0)
def draw_ram_bar(draw, mem_percent):
x = WIDTH - 1 - RAM_BAR_WIDTH
y = 0
draw.rectangle((x, y, WIDTH - 1, RAM_BAR_HEIGHT), outline=255)
fill_h = int((mem_percent / 100) * RAM_BAR_HEIGHT)
draw.rectangle(
(x, RAM_BAR_HEIGHT - fill_h, WIDTH - 1, RAM_BAR_HEIGHT),
fill=255
)
label = f"{mem_percent:.0f}%"
tw = draw.textbbox((0, 0), label, font=small_font)[2]
draw.text((x + (RAM_BAR_WIDTH - tw) // 2 + 1,
RAM_BAR_HEIGHT - fill_h - 11),
label, font=small_font, fill=255)
draw.rectangle((x, RAM_BAR_HEIGHT, WIDTH - 1, HEIGHT - 1), outline=0, fill=255)
draw.text((103, 52), "RAM", font=small_font, fill=0)
def draw_fs_pie(draw, used_pct):
draw.rectangle(FS_BOX, outline=255)
cx, cy = FS_PIE_CENTER
r = FS_PIE_RADIUS
bbox = (cx - r, cy - r, cx + r, cy + r)
draw.ellipse(bbox, outline=255)
angle = int(360 * (used_pct / 100))
draw.pieslice(bbox, start=270, end=270 + angle, fill=255)
draw.text((86, 42), "FS", font=small_font, fill=255)
draw.text((90, 51), "/", font=small_font, fill=255)
def draw_uptime(draw, hours, minutes):
draw.rectangle((-1, 53, 62, HEIGHT), outline=255)
draw.text((0, 53), f"up: {hours}h {minutes}m",
font=small_font, fill=255)
def draw_wifi_icon(draw, ssid, signal_pct, x=0, y=0):
bars = 4
bar_width = 2
bar_spacing = 1
max_height = 8 # tallest bar
filled_bars = int(signal_pct / 100 * bars + 0.5)
for i in range(bars):
bar_height = int((i + 1) / bars * max_height)
y_top = y + (max_height - bar_height)
fill = 1 if i < filled_bars else 0
draw.rectangle(
(x + i * (bar_width + bar_spacing), y_top,
x + i * (bar_width + bar_spacing) + bar_width,
y + max_height),
fill=fill
)
# Draw SSID text right next to icon
text_x = x + bars * (bar_width + bar_spacing) + 1
draw.text((text_x, y-3), ssid, font=small_font, fill=255)
# ---------------- MAIN UPDATE ----------------
def update_display():
cpu = psutil.cpu_percent(interval=0.1)
watts = estimate_power_w(cpu)
mem = psutil.virtual_memory().percent
load1, _, _ = os.getloadavg()
cpu_history.append(cpu)
temp = get_cpu_temp()
fs_used = get_root_fs_usage()
hours, minutes = format_uptime()
image = Image.new("1", (WIDTH, HEIGHT))
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, WIDTH, HEIGHT), fill=0)
ssid, signal = get_wifi_info("wlan0")
draw_wifi_icon(draw, ssid, signal, x=0, y=43)
draw_cpu_graph(draw, cpu, load1, temp, watts)
draw_ram_bar(draw, mem)
draw_fs_pie(draw, fs_used)
draw_uptime(draw, hours, minutes)
device.display(image)
# ---------------- MAIN LOOP ----------------
if __name__ == "__main__":
try:
for _ in range(CPU_HISTORY_LEN):
cpu_history.append(psutil.cpu_percent(interval=0.01))
while True:
update_display()
time.sleep(UPDATE_INTERVAL)
except KeyboardInterrupt:
device.clear()