From 18df3e42ede5f3a16b6b8dd240a3dcf3c5353fac Mon Sep 17 00:00:00 2001 From: aSbiEL0 <76663314+aSbiEL0@users.noreply.github.com> Date: Sat, 28 Feb 2026 07:57:16 -0800 Subject: [PATCH 1/3] Reorganize project structure and update runtime paths --- README.md | 54 +++++++++- .../pihole-display-pre.sh | 0 .../piholestats_v1.0.py | 0 .../piholestats_v1.1.py | 0 .../piholestats_v1.2.py | 0 scripts/test_placeholder.py | 101 ++++++++++++++++++ systemd/pihole-display-dark.service | 42 ++++---- systemd/pihole-display.service | 42 ++++---- 8 files changed, 193 insertions(+), 46 deletions(-) rename pihole-display-pre.sh => scripts/pihole-display-pre.sh (100%) rename piholestats_v1.0.py => scripts/piholestats_v1.0.py (100%) rename piholestats_v1.1.py => scripts/piholestats_v1.1.py (100%) rename piholestats_v1.2.py => scripts/piholestats_v1.2.py (100%) create mode 100755 scripts/test_placeholder.py diff --git a/README.md b/README.md index 13b4f70..4308dfb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,26 @@ Minimal framebuffer-based Pi-hole dashboard for Raspberry Pi (320×240 SPI TFT). --- +## Suggested project structure + +```text +zero2dash/ +├── scripts/ +│ ├── pihole-display-pre.sh +│ ├── piholestats_v1.0.py +│ ├── piholestats_v1.1.py +│ ├── piholestats_v1.2.py +│ └── test_placeholder.py +├── systemd/ +│ ├── pihole-display.service +│ ├── pihole-display-dark.service +│ ├── day.timer +│ └── night.timer +└── README.md +``` + +--- + ## Requirements * Raspberry Pi OS (SPI enabled) @@ -38,15 +58,25 @@ Reboot → display active on `/dev/fb1`. ### 2. Install Python dependency ```bash -sudo apt install -y python3-pip -pip3 install pillow +sudo apt install -y python3-pip python3-pil +``` + +--- + +### 3. Deploy project files + +```bash +sudo mkdir -p /opt/zero2dash +sudo cp -r . /opt/zero2dash/ +sudo chmod +x /opt/zero2dash/scripts/pihole-display-pre.sh +sudo chmod +x /opt/zero2dash/scripts/test_placeholder.py ``` --- ## Configure -Edit in `piholestats_v1.2.py`: +Edit in `/opt/zero2dash/scripts/piholestats_v1.2.py`: * `PIHOLE_HOST` * `PIHOLE_PASSWORD` @@ -57,7 +87,7 @@ Edit in `piholestats_v1.2.py`: ## Run via systemd ```bash -sudo cp pihole-display*.service /etc/systemd/system/ +sudo cp /opt/zero2dash/systemd/pihole-display*.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now pihole-display.service ``` @@ -70,6 +100,22 @@ journalctl -u pihole-display.service -n 50 --no-pager --- +## Placeholder test script + +To verify basic rendering logic: + +```bash +python3 /opt/zero2dash/scripts/test_placeholder.py --fbdev /dev/fb1 +``` + +Or generate a local preview without touching framebuffer: + +```bash +python3 /opt/zero2dash/scripts/test_placeholder.py --output /tmp/test.png --no-framebuffer +``` + +--- + ## Architecture ```text diff --git a/pihole-display-pre.sh b/scripts/pihole-display-pre.sh similarity index 100% rename from pihole-display-pre.sh rename to scripts/pihole-display-pre.sh diff --git a/piholestats_v1.0.py b/scripts/piholestats_v1.0.py similarity index 100% rename from piholestats_v1.0.py rename to scripts/piholestats_v1.0.py diff --git a/piholestats_v1.1.py b/scripts/piholestats_v1.1.py similarity index 100% rename from piholestats_v1.1.py rename to scripts/piholestats_v1.1.py diff --git a/piholestats_v1.2.py b/scripts/piholestats_v1.2.py similarity index 100% rename from piholestats_v1.2.py rename to scripts/piholestats_v1.2.py diff --git a/scripts/test_placeholder.py b/scripts/test_placeholder.py new file mode 100755 index 0000000..22e2b74 --- /dev/null +++ b/scripts/test_placeholder.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Placeholder display test: draw black screen with bold white TEST text.""" + +import argparse +import mmap +import struct +import sys +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +FBDEV_DEFAULT = "/dev/fb1" +W, H = 320, 240 + + +def load_font(size: int, bold: bool = True) -> ImageFont.ImageFont: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf" if bold else "/usr/share/fonts/truetype/freefont/FreeSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + ] + for font_path in candidates: + try: + return ImageFont.truetype(font_path, size=size) + except Exception: + continue + return ImageFont.load_default() + + +def make_test_frame() -> Image.Image: + image = Image.new("RGB", (W, H), (0, 0, 0)) + draw = ImageDraw.Draw(image) + font = load_font(96, bold=True) + + text = "TEST" + x0, y0, x1, y1 = draw.textbbox((0, 0), text, font=font) + text_w = x1 - x0 + text_h = y1 - y0 + + x = (W - text_w) // 2 + y = (H - text_h) // 2 + draw.text((x, y), text, font=font, fill=(255, 255, 255)) + + return image + + +def rgb888_to_rgb565(image: Image.Image) -> bytes: + r, g, b = image.split() + r = r.point(lambda value: value >> 3) + g = g.point(lambda value: value >> 2) + b = b.point(lambda value: value >> 3) + + rgb565 = bytearray() + rp, gp, bp = r.tobytes(), g.tobytes(), b.tobytes() + for i in range(len(rp)): + value = ((rp[i] & 0x1F) << 11) | ((gp[i] & 0x3F) << 5) | (bp[i] & 0x1F) + rgb565 += struct.pack(" None: + payload = rgb888_to_rgb565(image) + with open(fbdev, "r+b", buffering=0) as framebuffer: + mm = mmap.mmap(framebuffer.fileno(), W * H * 2, mmap.MAP_SHARED, mmap.PROT_WRITE) + mm.seek(0) + mm.write(payload) + mm.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Display a TEST placeholder on the TFT framebuffer.") + parser.add_argument("--fbdev", default=FBDEV_DEFAULT, help=f"Framebuffer device path (default: {FBDEV_DEFAULT})") + parser.add_argument("--output", help="Optional output image path for local verification (PNG/JPG)") + parser.add_argument("--no-framebuffer", action="store_true", help="Skip framebuffer write (useful for local testing).") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + frame = make_test_frame() + + if args.output: + frame.save(args.output) + print(f"Saved preview image to {args.output}") + + if args.no_framebuffer: + print("Skipping framebuffer write (--no-framebuffer set)") + return 0 + + fb_path = Path(args.fbdev) + if not fb_path.exists(): + print(f"Framebuffer {args.fbdev} not found.", file=sys.stderr) + return 1 + + write_to_framebuffer(frame, args.fbdev) + print(f"Displayed TEST placeholder on {args.fbdev}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/systemd/pihole-display-dark.service b/systemd/pihole-display-dark.service index 42bfff6..2c90f54 100644 --- a/systemd/pihole-display-dark.service +++ b/systemd/pihole-display-dark.service @@ -1,21 +1,21 @@ -[Unit] -Description=Pi-hole TFT Stats Display DARK -After=network-online.target dev-fb1.device -Wants=network-online.target dev-fb1.device -ConditionPathExists=/dev/fb1 - -[Service] -Type=simple -WorkingDirectory=/home/pihole -Environment=PYTHONUNBUFFERED=1 - -ExecStartPre=/usr/local/bin/pihole-display-pre.sh -ExecStart=/usr/bin/python3 -u /home/pihole/piholestats_v1.2.py - -Restart=always -RestartSec=2 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target +[Unit] +Description=Pi-hole TFT Stats Display DARK +After=network-online.target dev-fb1.device +Wants=network-online.target dev-fb1.device +ConditionPathExists=/dev/fb1 + +[Service] +Type=simple +WorkingDirectory=/opt/zero2dash +Environment=PYTHONUNBUFFERED=1 + +ExecStartPre=/opt/zero2dash/scripts/pihole-display-pre.sh +ExecStart=/usr/bin/python3 -u /opt/zero2dash/scripts/piholestats_v1.2.py + +Restart=always +RestartSec=2 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/systemd/pihole-display.service b/systemd/pihole-display.service index 268baf8..aafb690 100644 --- a/systemd/pihole-display.service +++ b/systemd/pihole-display.service @@ -1,21 +1,21 @@ -[Unit] -Description=Pi-hole TFT Stats Display -After=network-online.target dev-fb1.device -Wants=network-online.target dev-fb1.device -ConditionPathExists=/dev/fb1 - -[Service] -Type=simple -WorkingDirectory=/home/pihole -Environment=PYTHONUNBUFFERED=1 - -ExecStartPre=/usr/local/bin/pihole-display-pre.sh -ExecStart=/usr/bin/python3 -u /home/pihole/piholestats_v1.1.py - -Restart=always -RestartSec=2 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target +[Unit] +Description=Pi-hole TFT Stats Display +After=network-online.target dev-fb1.device +Wants=network-online.target dev-fb1.device +ConditionPathExists=/dev/fb1 + +[Service] +Type=simple +WorkingDirectory=/opt/zero2dash +Environment=PYTHONUNBUFFERED=1 + +ExecStartPre=/opt/zero2dash/scripts/pihole-display-pre.sh +ExecStart=/usr/bin/python3 -u /opt/zero2dash/scripts/piholestats_v1.1.py + +Restart=always +RestartSec=2 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target From 87b18b3c95c2e69e9f28a74d15a1cd511f9b12df Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:37:23 +0000 Subject: [PATCH 2/3] Initial plan From 24378efe6b5d033b2dcd68ca271458da93010790 Mon Sep 17 00:00:00 2001 From: Codex <242516109+Codex@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:20:16 +0000 Subject: [PATCH 3/3] Initial plan (#7) Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com>