Close
0%
0%

Scottina

A pocket-sized front panel for the diagnostic tools you already use

Similar projects worth following
A pocket-sized touchscreen front panel for the bench diagnostic tools you already use — a Pi 5 + 3.5" screen that boots straight to a tap-driven tile grid, strips each tool down to the two controls and two numbers you actually watch, and skips the other 90%. Diagnostics only — not a Flipper, not a Marauder, not a wardriving toy.

The gap I'm closing

Bench diagnostics live at two extremes. Either you're squinting at a terminal over SSH on a headless box, or you're hauling a laptop to the bench to open a full-size GUI. Headless is too little. The full interface is far too much.

Here's what I kept noticing: for any single diagnostic question, I'm pressing about two buttons of that tool and watching about two numbers come back. The other 90% of the interface is just in the way. I don't need the ribbon menus, the config panels, the twelve tabs. I need to plug a thing in, glance at a 3.5" screen, and get my answer.

Scottina is that middle ground. It doesn't invent new tools — it gives the ones you already trust a front panel sized to the one question you're asking.

Read more »

  • 1 × Raspberry Pi 5 Runs Kali. The whole panel is a Python app rendering to the framebuffer — the Pi 5's headroom is what makes SDR decode and live CAN counters feel instant instead of laggy.
  • 1 × 3.5" ILI9486 SPI touchscreen, 480×320, ADS7846 resistive touch The panel Scottina is built around. Driven by the piscreen DRM overlay, rendered straight to /dev/fb0, touch read from evdev. No X server anywhere in the stack.
  • 1 × Cheapo Pi Case off Amazon This one comes with a screen already: https://amzn.to/3R4uJzr
  • 1 × microSD card, 32GB+ Kali install. A10/U3-class or better — the panel is I/O-sensitive at boot.
  • 1 × USB-C power supply, 27W PD Pi 5 wants the official 5V/5A brick. Underpowering a Pi 5 with dongles attached produces the worst class of bug: intermittent and blamed on your code.

View all 9 components

  • Nobody is coming back to press Stop

    Scottskyan hour ago 0 comments

    The converter is where vendor PDFs turn into NMEA2000 decode tables. That work happens on a laptop or a phone, on a real screen, because reviewing a bit-field offset on a 3.5" panel is not a thing anyone should do. So the usage pattern is: tap the Tables tile on the Pi, walk to the bench, do the review in a browser, close the laptop, forget about it.

    That last step is the design problem. A Flask app with an upload endpoint, bound to 0.0.0.0 so the laptop can reach it, is not something you want sitting there for the next six weeks because a person got distracted. And asking the person to come back and press Stop is not a design, it is a wish.

    So the shutdown does not live in kilodash at all. kilodash/tableconv.py runs a small watchdog thread that wakes every 15 s, asks one object how long the app has been idle, and calls os._exit(0) once that passes the --idle-min window — 15 minutes by default. The app hangs up on itself. The front panel is not involved and does not need to be running.

    Two things count as "busy", not one

    The obvious idle clock is "time since the last HTTP request", and it is wrong. A big vendor PDF can take three minutes to parse in the extraction subprocess, and during those three minutes there are no requests at all — just a browser waiting on one that hasn't returned. An HTTP-only clock kills the job it is waiting on.

    So the Activity object tracks two things behind a lock: the last touch time, and a count of in-flight jobs. idle_secs() returns a flat 0.0 while any job is running, whatever the clock says. An upload calls job_start() before extraction and job_end() in a finally, so a parse that crashes still releases the counter.

    There is a second, smaller trap in the same place. The Tables tile polls /status every 5 s to draw its countdown — and if that poll counted as activity, the app would never idle out as long as anyone was looking at the tile. So /status is excluded from the touch, by path, in the before_request hook. The observer does not get to change what it observes.

    The unit has to agree

    setup/kilodash-tables.service is Restart=no, and that line is load-bearing. A clean idle exit is exit code 0 — with a restart policy in place, systemd would dutifully bring the app back up, and the two halves of the design would spend all night fighting each other. On-demand start, self-managed stop, no supervisor second-guessing either.

    Being killable at any instant is what makes the whole arrangement safe. Every write into the table store is tmp-file plus atomic rename, so there is no moment where the timer firing leaves a half-written table behind. The timeout does not need to be polite because the storage layer does not need it to be.

    The tile mirrors, it never computes

    The Tables screen is deliberately thin. It starts the service, and it never stops it on leave — navigating away from the tile must not kill a review session in progress on the laptop. What it shows is read back from the service's own /status: table count, and the idle clock rendered as running · idle-3:42, so the countdown to shutdown is visible rather than a surprise. Below that, the URL for the advertised address, and a QR code of the same, because typing an IP into a phone is the small friction this whole project exists to delete.

    Next log: the step inside that web app we have not talked about — the side-by-side review page, and why extraction is assistive but approval is always human.

  • A launched process is never a serving one.

    Scottsky7 days ago 0 comments

    A launched process is not a serving one

    Starting Kismet or Node-RED is one line: Popen a command, or systemctl start a unit. Neither of those tells you the thing actually works. A process can spawn and then crash three seconds later reading a stale config; a systemd unit can report success while the app inside it is still binding its listener. None of that is "ready" in any sense a person standing at the bench cares about — ready means the browser UI will actually load if they type the address in.

    So kilodash/webapp.py doesn't trust the launch call. It trusts a TCP probe against the app's own port. probe() is nothing exotic — socket. create_connection with a 0.4-second timeout — but it's the one signal that can't be faked by a process existing. Only once that connection succeeds does a tile's status card turn green and say "Web UI confirmed."

    Read more »

  • The day Scottina Prime turned into three documents arguing

    Scottsky08/10/2026 at 02:31 2 comments

    Every earlier log in this series describes a piece of Scottina working. This one is about a piece of process that had to work first, before any of that code could get written without stepping on itself.

    Work stalled entirely, for a full day — not on a bug, but on not knowing which document to believe.

    The scene

    Early on, README.md and CLAUDE.md laid out a CAN addressing and message schema — one that had worked before, on an earlier project. As real edge cases turned up on the bench, that schema needed a small rework. Small on paper. In practice, code built against the old schema didn't fail loudly — it produced silently corrupted values instead of a visible fault. Anything built without the new schema in mind still ran, just wrong. That surfaced as a string of errors with no obvious common cause, and panic mode followed.

    Read more »

  • Now you see me, now you don't.

    Scottsky08/06/2026 at 17:19 0 comments

    The tile that shows up on its own — and disappears just as fast

    Log 1 closed with a promise: hotplug, and how a screen's tile appears the instant its dongle is plugged in and vanishes the instant it isn't. That behavior is what makes the home grid trustworthy — you never wonder if a tool is available, you just look. Here is how it works.

    Why not udev rules and hope

    The obvious tool for USB presence is udev: write a rule, get an event, react to it. Scottina does not use it for tile visibility. devices.py polls sysfs instead — cheap, no pyudev dependency, no rule files to keep in sync across a Pi image rebuild. The launcher calls refresh() a few times a second, and the tile grid shows a device's tile only while the check comes back positive. Simple beats clever here: one function, one data source, one place to look when a device does not show up.

    A catalog of USB ids, and their opinions

    Each device screen is gated on a set of (vendor, product) USB ids — SDR_IDS for the RTL2832U dongles, ALFA_IDS for the Wi-Fi sniff adapter, CANABLE_IDS for candleLight/gs_usb boards, FTDI_IDS for the serial adapters. Most of these are a clean lookup. A few are not:

    • The FX2LP logic analyzer changes identity mid-session. It enumerates as a bare Cypress bootloader until firmware loads, then re-enumerates under an fx2lafw id. FX2LA_IDS matches both, plus a couple of Saleae/ USBee clone EEPROM ids, because a board scanned once this session is sitting in its post-load state and the tile still has to recognize it.
    • CanTick shares its vendor id with every ESP32 board on the market. Espressif's VID is not enough to identify anything. Presence detection matches the USB product string instead — and on the ESP32-S3, that string is not even the firmware's own name; the built-in USB-serial-JTAG reports the fixed hardware string "USB JTAG/serial debug unit" regardless of what firmware is flashed. A string-less descriptor counts too, since that is what a fresh, unconfigured board reports.

    None of this is guesswork. Every id in the table is a fact recorded on the bench with the actual device in hand, not a datasheet guess — the comments in devices.py say so, including the date for the CanTick case.

    The tile follows the truth on the panel

    Fixed screens — LAN Scan, Wi-Fi, Pi Health — are always on the grid. Device tiles are not: they appear only while refresh() finds their id present, and they carry a small green "live" badge so it is obvious at a glance which tools are physically available right now versus which ones would need a dongle first. Unplug the RTL-SDR mid-session and its tile is gone on the next poll, no stale entry pointing at hardware that is not there anymore.

    Adding a device is a known recipe, not a one-off

    This pattern gets reused every time a new piece of hardware joins Scottina. The logic-analyzer integration is the clearest recent example: Phase 2 of that work is nothing but "add the FX2LP ids to devices.py using the same mechanism the other screens use, gate the new screen's tile on that presence, done." No new detection framework, no special case in the launcher — just another entry in the same table. That reuse is the actual payoff of building the mechanism once and keeping it boring.

    Next log: what happens after a tile is tapped and the tool is a web app — how Scottina waits for Node-RED or Kismet to actually answer on their port before it tells you where to point a browser, instead of guessing that a spawned process is a serving one.

  • No X server, no mouse — and the panel that made me earn it

    Scottsky07/30/2026 at 17:12 0 comments

    Log 1 mentioned "render directly to the framebuffer" as one of six ground rules and moved on. It deserves its own post, because this is the decision that almost every other design choice in Scottina traces back to.

    Why not just run Xorg like everybody else?

    The obvious path for a touchscreen kiosk is a desktop stack: Xorg or Wayland, SDL or Qt, a browser or a toolkit on top. I tried variations on that and kept running into the same wall — anything that can lose the DRM device eventually does, and on a 3.5" panel wired over SPI, "eventually" shows up as a frozen or blank screen at the worst possible bench moment. The only approach that's proven reliable on this hardware is the boring one: own /dev/fb0 directly.

    Read more »

  • Who said I need all these buttons in my face?

    Scottsky07/26/2026 at 23:50 0 comments

    Starting the logs where the project started: with the thing that annoyed me enough to build hardware.

    The problem, stated plainly

    Every diagnostic session I run lands in one of two bad places.

    Place one: SSH into a headless box and squint at a terminal. Fine if you already know the exact command and the exact flag. Miserable when you're standing at a bench with one hand on a probe.

    Place two: carry a laptop to the bench, open the full GUI, and navigate a tool built for someone doing everything, when you are doing one thing.

    The observation that turned into a project: for any single diagnostic question, I use roughly two controls and watch roughly two numbers. Consistently. Across CAN, I2C, serial, Wi-Fi, SDR. The tools aren't wrong — they're general-purpose, and general-purpose means the 90% you're not using is between you and the 10% you are.

    Read more »

View all 6 project logs

  • 1
    Step 1 — Gather the hardware

    Full list with notes is in the Components section. The minimum to get a working panel: Pi 5, the 3.5" ILI9486 screen, a 27W USB-C supply, an active cooler, and a decent microSD. The ALFA, CAN dongle, and SDR are all optional — their tiles simply don't appear until they're plugged in.

    Do not skimp on the power supply. An underpowered Pi 5 with dongles attached produces intermittent faults you will spend a day blaming on software.

  • 2
    Step 2 — Flash Kali and get in over SSH

    Write the Kali Raspberry Pi image to the microSD, enable SSH, and boot the Pi with a network cable and no screen attached. Everything from here is done over SSH — the panel isn't alive yet.

    Update before you touch anything else:

    sudo apt update && sudo apt full-upgrade -y
  • 3
    Step 3 — Bring up the panel

    This is the step that eats afternoons, so it's deliberately minimal. Two lines in /boot/firmware/config.txt:

    dtparam=spi=on
    dtoverlay=piscreen,drm,rotate=90

    Reboot. That's the entire display configuration.

    rotate is the only display setting that requires a reboot — pick 90 or 270 depending on which way up your case holds the panel. Touch orientation is handled in software, so you never come back to this file to fix taps landing in the wrong place.

    Verify the framebuffer exists:

    ls -l /dev/fb0
    cat /sys/class/graphics/fb0/name

    You should see the ILI9486 DRM framebuffer. If /dev/fb0 is missing, stop here — nothing downstream will work.

View all 10 instructions

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates