Skip to content

Instantly share code, notes, and snippets.

@Tom32i
Last active July 17, 2026 08:04
Show Gist options
  • Select an option

  • Save Tom32i/ad7dd2f30fbff5747522 to your computer and use it in GitHub Desktop.

Select an option

Save Tom32i/ad7dd2f30fbff5747522 to your computer and use it in GitHub Desktop.
A very simple script to display the IP of my RaspberyPi

Tom32i's Pi boot:

Boot scripts for a Raspberry Pi fitted with a Display-o-tron 3000 (ST7036 LCD, SN3218 RGB backlight and a joystick).

Features:

  • Welcome screen — shows a boot logo on the LCD for a couple of seconds at startup.
  • IP display — press the joystick right to show the Pi's current IP address. It checks wired ethernet (eth0) first, then WiFi (wlan0), and labels which one is in use (My ip (eth): / My ip (wifi):). If neither is connected it shows No network :(.
  • Clear screen — press the joystick left to blank the display and turn off the backlight.
  • Shutdown — press the joystick down to show a confirmation, then press the centre button to cleanly power off the Pi. The prompt auto-cancels after a few seconds, and left cancels it too.

Installation:

Run these steps on the Raspberry Pi (over SSH or locally). On a fresh headless install you can reach the Pi at raspberrypi.local, or find its IP in your router — you don't need the display to get the first SSH in.

Connect with SSH agent forwarding:

Since the gist is private, connect with ssh -A so the Pi can use your local SSH key to clone it (your private key is never copied onto the Pi):

ssh -A tom32i@raspberrypi.local        # or ssh -A tom32i@<pi-ip>

Make sure your key is loaded in the agent first (ssh-add -l; add it with ssh-add --apple-use-keychain ~/.ssh/id_rsa if missing).

Clone the project:

cd ~
git clone git@gist.github.com:/ad7dd2f30fbff5747522.git boot

Clone as your normal user — don't sudo it, or the forwarded agent (and your key) becomes invisible.

Run the installer:

cd ~/boot
./install.sh

The installer takes care of everything:

  • copies the scripts to the target directory (default: the repo location),
  • installs the dependencies (st7036 for the LCD, smbus2 for the RGB backlight, gpiozero/lgpio for the joystick) and enables SPI (the LCD) and I2C (the backlight),
  • installs a dot3k-boot systemd service that runs the display on every boot (welcome logo, then the IP viewer) and restarts it if it crashes.

Reboot required on first install. Enabling SPI/I2C only takes effect after a reboot, so if the installer tells you to sudo reboot, do it — the service is already enabled and will start on its own once the Pi is back up.

To install somewhere other than the repo folder, pass a target directory:

./install.sh /home/tom32i/boot

Useful commands afterwards:

sudo systemctl status dot3k-boot   # check it's running
journalctl -u dot3k-boot -f        # follow the logs

Manual alternative (rc.local):

If you'd rather not use systemd, make the launcher executable and add it to /etc/rc.local before the exit 0 line:

sudo chmod 755 /home/tom32i/boot/launcher
sudo vim /etc/rc.local        # add: /home/tom32i/boot/launcher &

Note: rc.local is deprecated on recent Raspberry Pi OS releases — the systemd service above is preferred.

#!/usr/bin/env python
"""Self-contained driver for the Display-o-Tron 3000 RGB backlight.
The backlight is an SN3218 18-channel LED driver on the I2C bus. This talks to
it directly via smbus2, replacing both the old `dot3k.backlight` wrapper and the
`sn3218` package (whose 2.x rewrite dropped the API dot3k relied on).
The channel layout and gamma correction are reproduced from dot3k so the colours
look identical to before.
"""
import smbus2
I2C_ADDRESS = 0x54
# SN3218 command registers
CMD_ENABLE_OUTPUT = 0x00
CMD_SET_PWM = 0x01 # channels 1..18 -> registers 0x01..0x12
CMD_ENABLE_LEDS = 0x13 # 0x13..0x15, 6 channels each
CMD_UPDATE = 0x16
CMD_RESET = 0x17
# Three RGB backlight zones wired to SN3218 channels (right / middle / left),
# each as (red, green, blue) channel indices. Channels 9..17 are the bargraph.
ZONES = ((0, 1, 2), (3, 4, 5), (6, 7, 8))
# Per-channel gamma, reproduced from dot3k so brightness stays balanced.
_gamma = [int(pow(255, (i - 1) / 255.0)) for i in range(256)]
_channel_gamma = [_gamma] * 18
for _c in (0, 3, 6): # red channels
_channel_gamma[_c] = [int(v / 1.4) for v in _gamma]
for _c in (1, 4, 7): # green channels
_channel_gamma[_c] = [int(v / 1.6) for v in _gamma]
for _c in range(9, 18): # bargraph (white)
_channel_gamma[_c] = [int(v / 24) for v in _gamma]
_bus = smbus2.SMBus(1)
_values = [0] * 18
def _update():
corrected = [_channel_gamma[i][_values[i]] for i in range(18)]
_bus.write_i2c_block_data(I2C_ADDRESS, CMD_SET_PWM, corrected)
_bus.write_byte_data(I2C_ADDRESS, CMD_UPDATE, 0xFF)
def enable():
"""Reset the chip and turn all channels on."""
_bus.write_byte_data(I2C_ADDRESS, CMD_RESET, 0xFF)
_bus.write_byte_data(I2C_ADDRESS, CMD_ENABLE_OUTPUT, 0x01)
_bus.write_i2c_block_data(I2C_ADDRESS, CMD_ENABLE_LEDS, [0x3F, 0x3F, 0x3F])
_update()
def rgb(r, g, b):
"""Set all three backlight zones to the same r, g, b colour (0-255)."""
r, g, b = (max(0, min(255, int(v))) for v in (r, g, b))
for (rc, gc, bc) in ZONES:
_values[rc], _values[gc], _values[bc] = r, g, b
_update()
def off():
"""Turn the backlight off."""
rgb(0, 0, 0)
enable()

Raspberry Pi boot launcher

Boot scripts for a Raspberry Pi with a Pimoroni Display-o-Tron 3000 (ST7036 character LCD over SPI, SN3218 RGB backlight over I2C, and a 5-way joystick). The gist is private: git@gist.github.com:/ad7dd2f30fbff5747522.git.

What it does

  • welcome.py — boot logo for ~3s, then clears.
  • ip.py — long-running joystick listener (the systemd main process):
    • RIGHT → show IP (tries eth0 then wlan0, labelled eth/wifi)
    • LEFT → clear screen
    • DOWN → arm a shutdown prompt; center BUTTON confirms → systemctl poweroff (auto-cancels after 5s; LEFT/RIGHT also cancel). Clears the LCD on SIGTERM.
  • screen.py — LCD text via st7036 + colour via our backlight.py.
  • backlight.py — self-contained SN3218 driver over smbus2 (channel map + gamma reproduced from dot3k).
  • scanner.py — IP lookup via SIOCGIFADDR ioctl.

Hardware facts (non-obvious)

  • Joystick GPIO pins are BCM: RIGHT=22, LEFT=17, DOWN=9, UP=27, BUTTON=4.
  • The LCD register-select pin is BCM 25 (used by st7036 via RPi.GPIO output).
  • The middle-zone RED LED is dead (burnt out on this ~2014 unit). The code is correct — red-heavy colours just look off in the centre. Not a bug.
  • SN3218 is at I2C address 0x54 on bus 1; three RGB zones on channels 0-8 (right=0-2, middle=3-5, left=6-8), bargraph on 9-17.
  • The LCD has a hardware Green/Blue swap; screen.set_color compensates by passing rgb(r, b, g).

Dependency history — why the stack looks like this

Original code used the unmaintained dot3k library. On current Raspberry Pi OS (trixie, Python 3.13, Pi with new GPIO chip) it broke three ways, all now fixed:

  • RPi.GPIO edge detection raises "Failed to add edge detection" → joystick driven via gpiozero (lgpio) instead.
  • sn3218 2.x dropped the module API dot3k needed → replaced with our own smbus2 SN3218 driver (no sn3218 dependency, no version pin).
  • LCD text uses st7036 directly instead of dot3k.lcd.

Current deps (all maintained): st7036, smbus2, gpiozero, lgpio. Do NOT reintroduce dot3k or sn3218.

Install / deploy (runs ON the Pi, not the Mac)

  • Reach a fresh Pi via raspberrypi.local or the router; the display is NOT a first-boot bootstrap. sshd starts late in boot — early "Connection refused" just means it's still booting.
  • Clone the private gist with SSH agent forwarding: ssh -A user@pi, then clone as the normal user (never sudo the clone — it drops the forwarded agent).
  • ./install.sh [target_dir] copies files, installs deps, enables SPI+I2C, and installs/enables the dot3k-boot systemd service. Enabling SPI/I2C needs a reboot to create /dev/spidev* and /dev/i2c-*; the installer detects this and tells you to reboot instead of failing.

Service

  • dot3k-boot.service runs as root: ExecStartPre = welcome.py, ExecStart = ip.py, Restart=on-failure. Logs: journalctl -u dot3k-boot -f.
  • A restart loop (welcome logo repeating every few seconds) means ip.py is crashing on startup — check the traceback in journald.

Testing gotcha

Only one process can own the joystick GPIOs. To run ip.py by hand, sudo systemctl stop dot3k-boot first, or you'll get lgpio.error: GPIO busy.

#!/usr/bin/env bash
#
# Installer for Tom32i's Display-o-tron 3000 boot display.
# Run this ON the Raspberry Pi (over SSH or locally).
#
# Usage:
# ./install.sh [target_dir]
#
# target_dir Where to install the scripts (default: this repo's location).
#
set -euo pipefail
SERVICE_NAME="dot3k-boot"
# --- pretty output --------------------------------------------------------
log() { printf '\033[0;36m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[0;32m ✓\033[0m %s\n' "$*"; }
warn() { printf '\033[0;33m !\033[0m %s\n' "$*"; }
die() { printf '\033[0;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
# --- sanity checks --------------------------------------------------------
command -v systemctl >/dev/null 2>&1 || \
die "systemctl not found. Run this on the Raspberry Pi, not on your Mac."
# Re-run as root so we can pip-install and write the systemd unit.
if [ "$(id -u)" -ne 0 ]; then
log "Re-running with sudo..."
exec sudo -E bash "$0" "$@"
fi
# Directory this script lives in (the cloned repo), resolved before any cd.
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Install target: first argument, or the repo location itself (install in place).
TARGET="${1:-$SRC}"
PYTHON="$(command -v python3 || command -v python || true)"
[ -n "$PYTHON" ] || die "No python interpreter found."
log "Source: $SRC"
log "Target: $TARGET"
log "Python: $PYTHON"
# --- copy files -----------------------------------------------------------
FILES=(welcome.py ip.py screen.py scanner.py backlight.py launcher)
if [ "$TARGET" != "$SRC" ]; then
log "Copying scripts to $TARGET"
mkdir -p "$TARGET"
for f in "${FILES[@]}"; do
[ -f "$SRC/$f" ] || die "Missing source file: $SRC/$f"
cp "$SRC/$f" "$TARGET/$f"
done
ok "Copied ${#FILES[@]} files"
else
ok "Installing in place (no copy needed)"
fi
chmod 755 "$TARGET/launcher"
ok "launcher is executable"
# --- dependencies ---------------------------------------------------------
"$PYTHON" -m pip --version >/dev/null 2>&1 || apt-get install -y python3-pip
# Install a pip package, retrying with --break-system-packages for PEP 668
# (Bookworm+ marks the system Python env as externally managed).
pip_install() {
"$PYTHON" -m pip install "$@" >/dev/null 2>&1 \
|| "$PYTHON" -m pip install --break-system-packages "$@" >/dev/null 2>&1
}
# LCD text driver: st7036 (SPI). This is the actual chip driver; screen.py
# uses it directly rather than through the unmaintained dot3k wrapper.
if "$PYTHON" -c 'import st7036' >/dev/null 2>&1; then
ok "st7036 already installed"
elif pip_install st7036; then
ok "st7036 installed"
else
warn "Could not install st7036 (LCD driver) automatically."
warn "Install it manually, e.g.: sudo $PYTHON -m pip install --break-system-packages st7036"
fi
# RGB backlight: our backlight.py talks to the SN3218 directly over I2C via
# smbus2, so neither dot3k nor the sn3218 package is needed.
if "$PYTHON" -c 'import smbus2' >/dev/null 2>&1; then
ok "smbus2 already installed"
elif apt-get install -y python3-smbus2 >/dev/null 2>&1 || pip_install smbus2; then
ok "smbus2 installed"
else
warn "Could not install smbus2 (I2C backlight access)."
fi
# The joystick is driven via gpiozero (lgpio backend) instead of the RPi.GPIO
# edge detection in dot3k.joystick, which is broken on recent Pi OS kernels.
if "$PYTHON" -c 'import gpiozero, lgpio' >/dev/null 2>&1; then
ok "gpiozero + lgpio already installed"
elif apt-get install -y python3-gpiozero python3-lgpio >/dev/null 2>&1; then
ok "gpiozero + lgpio installed"
elif pip_install gpiozero lgpio; then
ok "gpiozero + lgpio installed (pip)"
else
warn "Could not install gpiozero/lgpio — the joystick will not work."
fi
# The display needs SPI (LCD / st7036) and I2C (RGB backlight / SN3218).
# Best effort, non-fatal. Enabling these requires a reboot to take effect.
NEED_REBOOT=0
if command -v raspi-config >/dev/null 2>&1; then
if [ ! -e /dev/spidev0.0 ]; then NEED_REBOOT=1; fi
if [ ! -e /dev/i2c-1 ]; then NEED_REBOOT=1; fi
raspi-config nonint do_spi 0 >/dev/null 2>&1 && ok "SPI enabled" || warn "Could not enable SPI automatically"
raspi-config nonint do_i2c 0 >/dev/null 2>&1 && ok "I2C enabled" || warn "Could not enable I2C automatically"
fi
# --- systemd service ------------------------------------------------------
UNIT="/etc/systemd/system/${SERVICE_NAME}.service"
log "Writing systemd unit: $UNIT"
cat > "$UNIT" <<EOF
[Unit]
Description=Display-o-tron 3000 boot display (welcome + IP viewer)
After=multi-user.target
[Service]
Type=simple
# Show the welcome logo (self-terminates after ~2s), then run the IP viewer.
ExecStartPre=$PYTHON $TARGET/welcome.py
ExecStart=$PYTHON $TARGET/ip.py
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE_NAME" >/dev/null 2>&1
ok "Service '$SERVICE_NAME' enabled (starts on boot)"
# --- summary --------------------------------------------------------------
echo
if [ "$NEED_REBOOT" -eq 1 ]; then
warn "SPI/I2C were just enabled and only activate after a reboot."
warn "The display won't work until you reboot, so do that now:"
echo " sudo reboot"
echo " The service is enabled and will come up automatically on boot."
elif systemctl restart "$SERVICE_NAME" 2>/dev/null; then
ok "Service started"
log "Done! The display is running and starts on every boot."
else
warn "Service did not start — check: journalctl -xeu $SERVICE_NAME"
fi
echo
echo " Status: sudo systemctl status $SERVICE_NAME"
echo " Logs: journalctl -u $SERVICE_NAME -f"
echo " Stop: sudo systemctl stop $SERVICE_NAME"
echo " Usage: joystick RIGHT = show IP (eth/wifi), LEFT = clear screen"
#!/usr/bin/env python
import signal
import sys
from subprocess import call
from threading import Timer
from gpiozero import Button
import screen
import scanner
color_ok = [0, 200, 180]
color_ko = [180, 0, 50]
color_warn = [255, 120, 0]
# Wired first, then WiFi
interfaces = [('eth0', 'eth'), ('wlan0', 'wifi')]
# Display-o-tron 3000 joystick pins (BCM), active-low.
# Driven via gpiozero (lgpio) because RPi.GPIO edge detection is broken on
# recent Raspberry Pi OS kernels (dot3k.joystick relies on it and crashes).
PIN_RIGHT = 22
PIN_LEFT = 17
PIN_DOWN = 9
PIN_BUTTON = 4
# Seconds a shutdown prompt stays armed (awaiting the button) before cancelling.
CONFIRM_TIMEOUT = 5
_confirm_timer = None
def _cancel_confirm():
global _confirm_timer
if _confirm_timer is not None:
_confirm_timer.cancel()
_confirm_timer = None
def show_ip():
_cancel_confirm()
for (ifname, label) in interfaces:
ip = scanner.get_ip_address(ifname)
if ip:
screen.display(["My ip (" + label + "):", " " + ip], color_ok)
return
screen.display(["", "No network :("], color_ko)
def hide_ip():
_cancel_confirm()
screen.clear()
def request_shutdown():
# DOWN: arm the shutdown prompt and wait for the button to confirm.
global _confirm_timer
_cancel_confirm()
screen.display(["Shutdown?", "Button = confirm", "Left = cancel"], color_warn)
_confirm_timer = Timer(CONFIRM_TIMEOUT, _confirm_expired)
_confirm_timer.start()
def confirm_shutdown():
# BUTTON: only acts while a shutdown prompt is armed.
if _confirm_timer is None:
return
_cancel_confirm()
screen.display(["", "Shutting down..."], color_ko)
call(["systemctl", "poweroff"])
def _confirm_expired():
global _confirm_timer
_confirm_timer = None
screen.clear()
right = Button(PIN_RIGHT, pull_up=True, bounce_time=0.1)
left = Button(PIN_LEFT, pull_up=True, bounce_time=0.1)
down = Button(PIN_DOWN, pull_up=True, bounce_time=0.1)
button = Button(PIN_BUTTON, pull_up=True, bounce_time=0.1)
right.when_pressed = show_ip
left.when_pressed = hide_ip
down.when_pressed = request_shutdown
button.when_pressed = confirm_shutdown
# Blank the display on exit (service stop / shutdown) so it doesn't freeze on
# the last frame.
def _cleanup(*_):
screen.clear()
sys.exit(0)
signal.signal(signal.SIGTERM, _cleanup)
signal.pause()
#!/bin/bash
sudo python /home/tom32i/boot/welcome.py
sudo python /home/tom32i/boot/ip.py &
#!/usr/bin/env python
import socket
import fcntl
import struct
def get_ip_address(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack('256s', ifname[:15].encode('utf-8'))
)[20:24])
except OSError:
return None
finally:
s.close()
#!/usr/bin/env python
import st7036
import backlight
# Display-o-Tron 3000: ST7036 character LCD (3 rows x 16 cols), register-select
# on GPIO25. Driven straight through st7036 + our own backlight driver, rather
# than the unmaintained dot3k wrapper.
lcd = st7036.st7036(register_select_pin=25)
lcd.clear()
def display(messages, color = False):
if not isinstance(messages, list):
messages = [messages]
if len(messages) > 3:
raise Exception("Only 3 rows on the Display-o-tron 3000!")
lcd.clear()
if color:
set_color(color)
for (row, message) in enumerate(messages):
lcd.set_cursor_position(0, row)
lcd.write(message)
def set_color(color):
# Green & Blue seem to be mixed up :/
backlight.rgb(color[0], color[2], color[1])
def clear():
lcd.clear()
backlight.off()
#!/usr/bin/env bash
#
# sdbench.sh — compare SD cards on a Raspberry Pi.
#
# Runs each card one at a time (mount it, e.g. via a USB reader), then prints
# a side-by-side table. Uses fio with --direct=1 so the page cache is bypassed
# and the numbers reflect the card, not the Pi's RAM.
#
# Usage:
# ./sdbench.sh run <mountpoint> <label> # benchmark a mounted card
# ./sdbench.sh compare # print table of all saved runs
# ./sdbench.sh caps <mountpoint> <label> # (optional) fake-capacity check via f3
#
# Example:
# sudo mount /dev/sda1 /mnt/card # card A in a USB reader
# ./sdbench.sh run /mnt/card sandisk-A
# # swap cards, remount...
# ./sdbench.sh run /mnt/card kingston-B
# ./sdbench.sh compare
#
set -euo pipefail
RESULTS_DIR="${SDBENCH_RESULTS:-./sdbench-results}"
SIZE="${SIZE:-1G}" # test-file size; keep > Pi RAM-ish and < free space
RAMP="${RAMP:-2}" # seconds to ramp before measuring (settle the card)
RUNTIME="${RUNTIME:-30}" # seconds per random test (time-capped so slow cards
# don't take 20+ min on the 4K random-write pass)
die() { echo "error: $*" >&2; exit 1; }
need_fio() {
command -v fio >/dev/null 2>&1 || die "fio not installed. Run: sudo apt install -y fio"
}
run_bench() {
local target="$1" label="$2"
[ -n "$target" ] && [ -n "$label" ] || die "usage: $0 run <mountpoint> <label>"
[ -d "$target" ] || die "$target is not a directory / not mounted"
[ -w "$target" ] || die "$target is not writable (mount it or use sudo)"
need_fio
mkdir -p "$RESULTS_DIR"
local testfile="$target/.sdbench.tmp"
local out="$RESULTS_DIR/$label"
# shellcheck disable=SC2064
trap "rm -f '$testfile'" EXIT
echo "==> Benchmarking '$label' at $target (test file: $SIZE)"
echo " This writes/reads a $SIZE file. Give it a few minutes."
# 1) sequential write — creates the file
echo " [1/5] sequential write (1M blocks)..."
fio --name=seqwrite --filename="$testfile" --size="$SIZE" --bs=1M \
--rw=write --direct=1 --ioengine=libaio --iodepth=8 \
--ramp_time="$RAMP" --end_fsync=1 --output-format=json > "$out.seqwrite.json"
# 2) sequential read
echo " [2/5] sequential read (1M blocks)..."
fio --name=seqread --filename="$testfile" --size="$SIZE" --bs=1M \
--rw=read --direct=1 --ioengine=libaio --iodepth=8 \
--ramp_time="$RAMP" --output-format=json > "$out.seqread.json"
# 3) random 4K read — the number that decides boot/app responsiveness
echo " [3/5] random 4K read (IOPS, ${RUNTIME}s)..."
fio --name=randread --filename="$testfile" --size="$SIZE" --bs=4k \
--rw=randread --direct=1 --ioengine=libaio --iodepth=32 \
--ramp_time="$RAMP" --runtime="$RUNTIME" --time_based \
--output-format=json > "$out.randread.json"
# 4) random 4K write — the A1/A2 application-class number (slowest test)
echo " [4/5] random 4K write (IOPS, ${RUNTIME}s)..."
fio --name=randwrite --filename="$testfile" --size="$SIZE" --bs=4k \
--rw=randwrite --direct=1 --ioengine=libaio --iodepth=32 \
--ramp_time="$RAMP" --runtime="$RUNTIME" --time_based \
--output-format=json > "$out.randwrite.json"
# 5) mixed 4K random 70/30 read/write — closest to a real running OS
echo " [5/5] mixed 4K random 70/30 read/write (IOPS, ${RUNTIME}s)..."
fio --name=randrw --filename="$testfile" --size="$SIZE" --bs=4k \
--rw=randrw --rwmixread=70 --direct=1 --ioengine=libaio --iodepth=32 \
--ramp_time="$RAMP" --runtime="$RUNTIME" --time_based \
--output-format=json > "$out.randrw.json"
rm -f "$testfile"; trap - EXIT
echo "==> Done. Results saved to $RESULTS_DIR/$label.*.json"
echo
compare
}
compare() {
[ -d "$RESULTS_DIR" ] || die "no results in $RESULTS_DIR yet — run a benchmark first"
python3 - "$RESULTS_DIR" <<'PY'
import glob, json, os, sys
d = sys.argv[1]
labels = sorted({os.path.basename(p).split('.')[0] for p in glob.glob(os.path.join(d,'*.json'))})
if not labels:
sys.exit("no results found")
def load(label, job, section):
p = os.path.join(d, f"{label}.{job}.json")
if not os.path.exists(p): return None
j = json.load(open(p))["jobs"][0][section]
return j["bw_bytes"]/1e6, j["iops"] # MB/s, IOPS
rows = [
("Seq read (MB/s)", "seqread", "read", "bw"),
("Seq write (MB/s)", "seqwrite", "write", "bw"),
("Rand 4K read (IOPS)", "randread", "read", "iops"),
("Rand 4K write (IOPS)", "randwrite", "write", "iops"),
("Mixed 70/30 rd (IOPS)", "randrw", "read", "iops"),
("Mixed 70/30 wr (IOPS)", "randrw", "write", "iops"),
]
w = max(22, *(len(l) for l in labels)) + 2
print("SD card comparison\n" + "="*60)
hdr = "Metric".ljust(24) + "".join(l.ljust(w) for l in labels)
print(hdr); print("-"*len(hdr))
for name, job, sec, kind in rows:
line = name.ljust(24)
vals = []
for l in labels:
r = load(l, job, sec)
vals.append(None if r is None else (r[0] if kind=="bw" else r[1]))
for v in vals:
line += ("n/a" if v is None else f"{v:,.1f}" if kind=="bw" else f"{v:,.0f}").ljust(w)
# mark the winner
good = [v for v in vals if v is not None]
if len(good) > 1 and len(set(good)) > 1:
best = max(vals, key=lambda x: -1 if x is None else x)
line += " ← " + labels[vals.index(best)]
print(line)
print("="*60)
print("Higher is better on every row. Random 4K IOPS matter most for OS")
print("responsiveness/boot time; sequential MB/s for big file copies.")
PY
}
caps() {
local target="$1" label="${2:-card}"
[ -d "$target" ] || die "usage: $0 caps <mountpoint> [label]"
command -v f3write >/dev/null 2>&1 || die "f3 not installed. Run: sudo apt install -y f3"
echo "==> Fake-capacity / integrity check on '$label' ($target)"
echo " Fills the card, then verifies. Slow but catches counterfeit cards."
f3write "$target" && f3read "$target"
}
cmd="${1:-}"; shift || true
case "$cmd" in
run) run_bench "${1:-}" "${2:-}" ;;
compare) compare ;;
caps) caps "${1:-}" "${2:-}" ;;
*) cat >&2 <<EOF
sdbench.sh — compare SD cards on a Raspberry Pi
$0 run <mountpoint> <label> benchmark a mounted card, save results
$0 compare print side-by-side table of saved runs
$0 caps <mountpoint> [label] fake-capacity / integrity check (needs f3)
Env: SIZE=$SIZE (test-file size) RESULTS in $RESULTS_DIR
EOF
exit 1 ;;
esac
#!/usr/bin/env python
import time
import screen
screen.display([" ___ _ _", " | _ _ _) ) o", " |(_)|||_)/_ |"], [0, 255, 156])
time.sleep(3)
screen.clear()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment