@zpm1066 Thanks to @Arty for this script. Let us know if you run into any issues.
# Source code for 0.96-inch SSD1306 (源码0.96寸SSD1306)
import time, socket, subprocess, os
from datetime import datetime
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import Image, ImageFont
import psutil
# OLED Initialization (OLED 初始化)
serial = i2c(port=2, address=0x3D)
# serial = i2c(port=2, address=0x3c)
# Screen displays correctly (positive orientation) (屏幕显示正向)
device = ssd1306(serial, width=128, height=64, rotate=0) # 0度
# If the screen is upside down, use rotate=2. (如果屏幕是反过来的,使用 rotate=2)
# device = ssd1306(serial, width=128, height=64, rotate=2)
# Function to load icons (with error handling)
# 加载图标函数(加上异常处理)
def load_icon(path):
if os.path.exists(path):
return Image.open(path).convert("1")
else:
print(f"[警告] 图标未找到: {path}")
return Image.new("1", (16, 16))
# wifi_icon = load_icon("/home/particle/Documents/app/wifi_16_16.png")
# temp_icon = load_icon("/home/particle/Documents/app/tem_16_16.png")
# disk_icon = load_icon("/home/particle/Documents/app/disk_16_16.png")
font_emoji = ImageFont.truetype("/home/particle/Documents/app/Symbola.ttf", 13)
wifi_icon = "📶"
temp_icon = "🌡️"
disk_icon = "💾"
battery_icon = "🔋"
# You can continue using PixelOperator, but we recommend trying the fonts mentioned above and choosing the one that is clearest.
# (继续使用 PixelOperator 也可以,建议尝试上述字体,选择最清晰的)
font = ImageFont.truetype("/home/particle/Documents/app/pixel_operator/PixelOperator.ttf", 16)
# Example: Using DejaVuSansMono (示例:使用 DejaVuSansMono)
# font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 16)
# font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf", 14)
# Example: Using Roboto-Regular (示例:使用 Roboto-Regular)
# font = ImageFont.truetype("/home/particle/Documents/app/Roboto-Regular.ttf", 14)
# Example: Using Arial Unicode MS (示例:使用 Arial Unicode MS)
# font = ImageFont.truetype("/usr/share/fonts/truetype/msttcorefonts/Arial.ttf", 14)
# Get IP address (获取 IP)
def get_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
except:
ip = "0.0.0.0"
return ip
# CPU Temperature (CPU 温度)
def get_cpu_temp():
try:
with open("/sys/class/thermal/thermal_zone0/temp") as f:
return f"{int(f.read()) / 1000:.1f}C"
except:
return "N/A"
# Battery percentage (电池电量百分比)
def get_battery_percent():
try:
# 常见路径:/sys/class/power_supply/BAT0/capacity
with open("/sys/class/power_supply/battery/capacity") as f:
# return f"{battery_icon} {int(f.read().strip())}%"
return f"Bat {int(f.read().strip())}%"
except:
return "N/A"
# Disk usage (磁盘使用率)
def get_disk_usage():
try:
out = subprocess.check_output(["df", "-h", "/"]).decode().split("\n")[1]
usage_percent = out.split()[4]
avail_gb = out.split()[3]
return f"{usage_percent} {avail_gb}B"
except:
return "N/A"
def get_mem_usage():
try:
mem = psutil.virtual_memory()
return f"Mem {mem.percent}%"
except:
return "Mem N/A"
def get_cpu_usage():
try:
return f"CPU {psutil.cpu_percent()}%"
except:
return "CPU N/A"
def get_battery_status():
try:
battery = psutil.sensors_battery()
if battery is None:
return "Bat N/A"
status = "Charging" if battery.power_plugged else "Discharging"
return f"{status}"
except:
return "Bat N/A"
def get_battery_ntc_temp():
try:
# Assuming the NTC temperature path is /sys/class/power_supply/battery/temp,units in 0.1°C
# (假设NTC温度路径为 /sys/class/power_supply/battery/temp,单位0.1°C)
with open("/sys/class/power_supply/battery/temp") as f:
temp = int(f.read().strip()) / 10
return f"NTC {temp:.1f}C"
except:
return "NTC N/A"
def get_disk_avail():
try:
disk = psutil.disk_usage("/")
avail_gb = disk.free / (1024 ** 3)
return f"Disk {avail_gb:.1f}GB"
except:
return "Disk N/A"
def get_date():
return datetime.now().strftime("%Y-%m-%d")
def build_scroll_text():
mem = get_mem_usage()
cpu = get_cpu_usage()
bat_status = get_battery_status()
ntc = get_battery_ntc_temp()
disk = get_disk_avail()
date = get_date()
return f" {date} | {mem} | {cpu} | {bat_status} | {ntc} | {disk}"
scroll_text = build_scroll_text()
scroll_pos = 0
text_width = len(scroll_text) * 6 # Each character is approximately 6 pixels wide. (每个字符约6像素)
# Main loop (主循环)
while True:
now = datetime.now().strftime("%H:%M:%S")
ip = get_ip()
cpu = get_cpu_temp()
battery = get_battery_percent()
disk = get_disk_usage()
with canvas(device) as draw:
# Top yellow area (first 16 pixels) (顶部黄色区域(前16像素))
draw.rectangle((0, 0, 128, 16), outline=0, fill=0) # 255
draw.text((0, 0), "Particle", font=font, fill=255)
# draw.text((0, 0), "Tachyon", font=font, fill=255)
draw.text((76, 0), now, font=font, fill=255)
# Icon + Information Area (图标+信息区域)
# draw.bitmap((0, 16), wifi_icon, fill=255)
draw.text((5, 14), wifi_icon, font=font_emoji, fill=255)
draw.text((20, 14), ip, font=font, fill=255)
# draw.bitmap((0, 32), temp_icon, fill=255)
draw.text((5, 28), temp_icon, font=font_emoji, fill=255)
draw.text((20, 28), cpu, font=font, fill=255)
draw.text((60, 28), battery, font=font, fill=255)
# draw.bitmap((0, 48), disk_icon, fill=255)
draw.text((5, 40), disk_icon, font=font_emoji, fill=255)
draw.text((20, 40), disk, font=font, fill=255)
# Scrolling text (display limited to the bottom line) (滚动文字(限制显示在底部行))
x = 128 - (scroll_pos % (text_width + 128))
draw.text((x, 50), scroll_text, font=font, fill=255)
scroll_pos += 4 # Scrolling speed (滚动速度)
time.sleep(0.5)