Microscope camera kiosk
Touchscreen kiosk for the Raspberry Pi HQ camera (IMX477) used as a microscope: live preview from boot, raw frame recording, full-resolution stills, and automatic capture-SSD mounting. - app.py: GTK3 fullscreen UI over cage — preview, REC/STOP, PHOTO, 90-degree rotate button, tap-to-open exposure/gain/settings sliders, and an "SSD NOT MOUNTED" badge - camera.py: picamera2 wrapper with separate video (ISP preview + packed 12-bit raw) and still (4056x3040) configurations - recorder.py: writer thread turning the raw stream into one Bayer frame per file plus index.jsonl/session.json, with dropped/missed accounting - photos.py: still capture to DNG + JPEG, and a background worker that renders a viewable 16-bit sRGB TIFF from the 12-bit Bayer frame - storage.py: SSD mount state and the date/time/format session layout - tools/unpack_raw.py: desktop-side unpacker/demosaicer - system/ + install.sh: systemd units, udev rule, mount helper, sudoers Note: raw frames are written byte-for-byte as the sensor produced them, so they stay decodeable regardless of how the stills pipeline evolves.
This commit is contained in:
7
system/99-microscope-ssd.rules
Normal file
7
system/99-microscope-ssd.rules
Normal file
@@ -0,0 +1,7 @@
|
||||
# Auto-mount the microscope capture SSD as soon as it is plugged in.
|
||||
#
|
||||
# The mount itself must happen in a systemd service, not in RUN: udev workers
|
||||
# run without CAP_SYS_ADMIN, so mount(2) there fails with EPERM. The service is
|
||||
# oneshot *without* RemainAfterExit so it goes inactive again after each run and
|
||||
# a later replug can start it afresh.
|
||||
ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}=="caf4320b-0fa3-4674-bfa9-f17f6165f681", ENV{SYSTEMD_WANTS}+="microscope-ssd-mount.service", TAG+="systemd"
|
||||
17
system/microscope-boot-check
Executable file
17
system/microscope-boot-check
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# One-shot boot verification: proves the kiosk came up and the SSD mounted.
|
||||
LOG=/home/tyler/boot-check.log
|
||||
export XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0
|
||||
sleep 20
|
||||
{
|
||||
echo "=== boot check $(date -Is) ==="
|
||||
echo "camera service : $(systemctl is-active microscope-camera.service) (enabled: $(systemctl is-enabled microscope-camera.service))"
|
||||
echo "ssd mount : $(/usr/local/sbin/microscope-ssd status)"
|
||||
echo "findmnt : $(findmnt -no SOURCE,TARGET,FSTYPE /mnt/microscope || echo 'not mounted')"
|
||||
echo "cage : $(pgrep -a cage | head -1)"
|
||||
echo "app : $(pgrep -af 'python3 app.py' | head -1)"
|
||||
} >> "$LOG"
|
||||
for d in HDMI-A-1 HDMI-A-2 DSI-1 DPI-1 HDMI-1; do
|
||||
if grim -o "$d" /home/tyler/boot-check.png 2>>"$LOG"; then echo "screenshot : $d" >> "$LOG"; break; fi
|
||||
done
|
||||
chown tyler:tyler "$LOG" /home/tyler/boot-check.png 2>/dev/null
|
||||
10
system/microscope-boot-check.service
Normal file
10
system/microscope-boot-check.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Microscope camera boot verification
|
||||
After=microscope-camera.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/microscope-boot-check
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
35
system/microscope-camera.service
Normal file
35
system/microscope-camera.service
Normal file
@@ -0,0 +1,35 @@
|
||||
[Unit]
|
||||
Description=Microscope camera kiosk (cage + capture UI)
|
||||
Documentation=file:/opt/microscope-camera/README.md
|
||||
After=systemd-user-sessions.service systemd-logind.service
|
||||
Wants=systemd-logind.service
|
||||
Conflicts=getty@tty1.service
|
||||
After=getty@tty1.service
|
||||
ConditionPathExists=/opt/microscope-camera/app.py
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=tyler
|
||||
Group=tyler
|
||||
# A logind session on tty1 gives cage DRM master plus input device access.
|
||||
PAMName=login
|
||||
TTYPath=/dev/tty1
|
||||
TTYReset=yes
|
||||
TTYVHangup=yes
|
||||
TTYVTDisallocate=yes
|
||||
StandardInput=tty
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
Environment=HOME=/home/tyler
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStartPre=/bin/sh -c 'install -d -m 700 -o tyler -g tyler /run/user/1000'
|
||||
ExecStart=/usr/bin/cage -s -- /usr/bin/python3 /opt/microscope-camera/app.py
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
# Keep the recorder alive long enough to flush its last frames on restart.
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
96
system/microscope-ssd
Executable file
96
system/microscope-ssd
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# Manage the microscope capture SSD mount.
|
||||
#
|
||||
# microscope-ssd mount mount the SSD (waits briefly for it to appear)
|
||||
# microscope-ssd umount flush and unmount safely
|
||||
# microscope-ssd status print: mounted | stale | unmounted | absent
|
||||
#
|
||||
# Called by the udev-triggered microscope-ssd-mount.service when the drive is
|
||||
# plugged in, by fstab at boot, and by the kiosk UI (through a NOPASSWD sudoers
|
||||
# entry) when the operator taps MOUNT SSD.
|
||||
#
|
||||
# stdout stays machine-readable (status) while everything also lands in a log
|
||||
# file, because udev and systemd capture stdout in ways that are awkward to read.
|
||||
set -u
|
||||
|
||||
MOUNT_POINT="${MICROSCOPE_MOUNT:-/mnt/microscope}"
|
||||
UUID="${MICROSCOPE_UUID:-caf4320b-0fa3-4674-bfa9-f17f6165f681}"
|
||||
OWNER="${MICROSCOPE_OWNER:-tyler:tyler}"
|
||||
DEV="/dev/disk/by-uuid/${UUID}"
|
||||
LOG_FILE="${MICROSCOPE_LOG:-/run/microscope-ssd.log}"
|
||||
|
||||
log() {
|
||||
local message="microscope-ssd: $*"
|
||||
echo "$message" >>"$LOG_FILE" 2>/dev/null || true
|
||||
echo "$message"
|
||||
}
|
||||
|
||||
in_mounts() { grep -qs " ${MOUNT_POINT} " /proc/self/mounts; }
|
||||
|
||||
do_status() {
|
||||
# 'stale' means a mount entry survived a yank: the device node is gone.
|
||||
if in_mounts; then
|
||||
if [ -e "$DEV" ]; then echo mounted; else echo stale; fi
|
||||
elif [ -e "$DEV" ]; then
|
||||
echo unmounted
|
||||
else
|
||||
echo absent
|
||||
fi
|
||||
}
|
||||
|
||||
do_mount() {
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
if in_mounts; then
|
||||
if [ -e "$DEV" ]; then
|
||||
log "already mounted"
|
||||
return 0
|
||||
fi
|
||||
log "clearing stale mount entry"
|
||||
umount -l "$MOUNT_POINT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
[ -e "$DEV" ] && break
|
||||
sleep 0.25
|
||||
done
|
||||
if [ ! -e "$DEV" ]; then
|
||||
log "SSD not present ($DEV)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if mount -t ext4 -o noatime,errors=remount-ro "$DEV" "$MOUNT_POINT"; then
|
||||
chown "$OWNER" "$MOUNT_POINT" 2>/dev/null || true
|
||||
chmod 755 "$MOUNT_POINT" 2>/dev/null || true
|
||||
log "mounted $DEV at $MOUNT_POINT"
|
||||
return 0
|
||||
fi
|
||||
log "mount failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
do_umount() {
|
||||
if ! in_mounts; then
|
||||
log "not mounted"
|
||||
return 0
|
||||
fi
|
||||
sync
|
||||
if umount "$MOUNT_POINT"; then
|
||||
log "unmounted (safe to unplug)"
|
||||
return 0
|
||||
fi
|
||||
log "unmount failed - something is still using $MOUNT_POINT"
|
||||
return 1
|
||||
}
|
||||
|
||||
ACTION="${1:-status}"
|
||||
case "$ACTION" in
|
||||
mount|umount) log "--- $(date -Is) action=$ACTION euid=$(id -u) ---" ;;
|
||||
esac
|
||||
|
||||
case "$ACTION" in
|
||||
mount) do_mount ;;
|
||||
umount) do_umount ;;
|
||||
status) do_status ;;
|
||||
*) echo "usage: $0 {mount|umount|status}" >&2; exit 2 ;;
|
||||
esac
|
||||
13
system/microscope-ssd-mount.service
Normal file
13
system/microscope-ssd-mount.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Mount the microscope capture SSD
|
||||
Documentation=man:mount(8)
|
||||
After=systemd-udevd.service
|
||||
Before=local-fs.target
|
||||
|
||||
[Service]
|
||||
# No RemainAfterExit: the unit returns to inactive after each run so that every
|
||||
# subsequent plug-in event can start it again. The helper is idempotent, so an
|
||||
# already-mounted drive is a no-op.
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/microscope-ssd mount
|
||||
TimeoutStartSec=30
|
||||
1
system/sudoers-microscope-ssd
Normal file
1
system/sudoers-microscope-ssd
Normal file
@@ -0,0 +1 @@
|
||||
tyler ALL=(root) NOPASSWD: /usr/local/sbin/microscope-ssd
|
||||
Reference in New Issue
Block a user