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:
Microscope Camera
2026-10-31 00:44:46 -04:00
commit 34cd247dca
18 changed files with 3159 additions and 0 deletions

96
system/microscope-ssd Executable file
View 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