Imported from reprahkcin/scant-pi (
AGENTS.md). Install upstream withnpx skills add reprahkcin/scant-pi. Copyright stays with the author.
Agent Onboarding — scant-pi
This document provides the context an AI coding agent needs to continue development on scant-pi. Read this first, then the README.md for user-facing docs.
What This Project Is
scant-pi is a Raspberry Pi–based 3D specimen scanner control system. It is a clean-room rewrite inspired by scAnt, keeping only the scan logic and Pololu Tic stepper protocol. The PyQt5 GUI, FLIR camera code, and Windows-specific code from scAnt were discarded.
The system is split into two halves:
- Pi (capture): FastAPI web server controlling a Pi HQ Camera and 3 stepper motors. Saves RAW TIFF images to a USB drive.
- PC (processing): Offline CLI script that focus-stacks and masks the captured images.
Repository Layout & Module Roles
scant_core/ — Hardware & logic layer (no web dependencies)
| File | Purpose |
|---|---|
config.py |
YAML config management. DEFAULT_CONFIG dict defines every valid key. load_config() deep-merges user YAML over defaults. create_project() builds the output directory and saves config. |
scanner_controller.py |
StepperAxis wraps ticcmd CLI via subprocess.run(). ScannerController manages 3 named axes (x, y, z). Homing order is Z-first (safety). |
scanner.py |
Scanner runs the nested X→Y→Z capture loop in a background thread. Supports pause/resume/abort via threading primitives. Writes scan_manifest.json on completion. Progress tracked via callback. |
storage.py |
Scans /media and /mnt for removable drives. Estimates storage (~35 MB per TIFF at 4056×3040). |
camera/base.py |
BaseCamera ABC. Any new camera backend must implement: connect, disconnect, capture_image, get_frame, get_settings, set_settings, start_stream, stop_stream. |
camera/picamera_hq.py |
PiCameraHQ uses the picamera2 library. Dual configuration: preview (640×480 RGB888) and still (full-res RGB888). capture_image() switches config, captures, saves TIFF via OpenCV, switches back. All operations are thread-locked. |
scant_web/ — FastAPI web application
| File | Purpose |
|---|---|
app.py |
Creates the FastAPI app. Holds global shared state: config, controller (ScannerController), camera (PiCameraHQ), scanner (Scanner). reload_config() rebuilds all objects from a new config dict. Registers 5 route modules. Shutdown event disconnects camera and de-energizes motors. |
routes/camera.py |
MJPEG stream endpoint (GET /api/camera/stream), test capture, get/set settings, connect/disconnect. |
routes/steppers.py |
Move, home, configure individual axes or all at once. Lists connected Tic serial numbers. |
routes/scanner.py |
Start/pause/resume/abort scan. SSE stream (GET /api/scanner/stream) pushes status JSON events. |
routes/config.py |
CRUD for the in-memory config dict. Save/load YAML files. Create project directory. |
routes/storage.py |
List removable drives, check free space, estimate required space. |
templates/index.html |
Single-page 4-panel responsive UI (Camera, Steppers, Project Setup, Scan Control). |
static/js/app.js |
Vanilla JS frontend. Makes fetch() API calls. MJPEG preview via <img src=>. SSE via EventSource. 3-second polling for stepper status. |
static/css/style.css |
Dark theme. |
tools/
| File | Purpose |
|---|---|
process_scan.py |
PC-side CLI. Groups images by X/Y position → focus-stacks each group → creates masks → generates cutouts. Uses focus-stack or enfuse for stacking, OpenCV for masking. |
Root files
| File | Purpose |
|---|---|
run.py |
Entry point. argparse for --config, --host, --port, --log-level. Loads config, calls reload_config(), starts uvicorn. |
install.sh |
Deployment script (run with sudo). Installs to /opt/scant-pi, creates systemd service and udev rules. |
requirements.txt |
pip dependencies. picamera2 and opencv come from apt (system-site-packages). |
config/example_config.yaml |
Fully annotated example config with all keys. |
Key Design Decisions
-
subprocess.run()for ticcmd — Pololu's Python bindings don't support ARM64 well. The CLI works reliably. Each command is a separate process invocation with a 10-second timeout. -
Thread-based scan loop — The scan runs in a
threading.Thread(daemon=True). Pause usesthreading.Event.wait(). Abort sets a flag checked between captures. This avoids asyncio complexity in the hardware layer. -
Dual picamera2 configurations — Preview runs at 640×480 for low-bandwidth MJPEG streaming.
capture_image()switches to the full-res still config, captures, then switches back. This is a picamera2 pattern (not a hack). -
--system-site-packagesvenv — Required becausepicamera2is installed via apt and is not on PyPI. The venv inherits system packages. -
Config deep-merge —
load_config()merges user YAML overDEFAULT_CONFIG. This means new keys added to defaults are automatically available without breaking existing configs. -
No database — All state is in-memory (config dict, scanner state). The only persistence is the YAML config file and the scan manifest JSON.
-
Images saved as TIFF — Lossless, large (~35 MB each), suitable for scientific/archival use. OpenCV's
imwritewith.tifextension.
Hardware Interface Details
Pololu Tic Steppers
- Each Tic controller has a unique serial number visible via
ticcmd --list - Commands:
--energize,--exit-safe-start,--position <n>,--halt,--deenergize,--position(read),--status - The
-d <serial>flag addresses a specific controller - Homing uses
--home fwdor--home revdepending on limit switch wiring - Position units are micro-steps (step_mode × full steps)
- Z axis homes first (safety — avoid crashing into specimen)
Pi HQ Camera (IMX477)
- Max resolution: 4056×3040 (12.3 MP)
- Interface: CSI ribbon cable (not USB)
- API:
picamera2(Python wrapper around libcamera) - Key controls:
ExposureTime(µs),AnalogueGain,ColourGains(tuple),AwbEnable,AeEnable - Max exposure: ~670 seconds (useful for very dark macro setups)
- Lens: C/CS-mount (user-supplied)
State Management Pattern
The FastAPI app in scant_web/app.py uses module-level globals for shared state:
config: dict = {}
controller: ScannerController | None = None
camera: PiCameraHQ | None = None
scanner: Scanner | None = None
Route modules import these via a helper function (e.g., from scant_web.app import camera, config). The reload_config(new_config) function rebuilds controller, camera, and scanner from the new config.
This is intentionally simple. If the app grows to need multiple concurrent scans or users, this pattern should be replaced with a proper dependency injection or application context.
Known Limitations & TODO
These are areas that are known to be incomplete or need work:
- No authentication — The web UI has no login. Anyone on the network can control the scanner. Add auth if the Pi is on a shared network.
- No HTTPS — Runs HTTP only. Use a reverse proxy (nginx/caddy) if TLS is needed.
- No EXIF writing — The config has EXIF fields but
capture_image()does not embed them in the TIFF. Needspiexifor similar. - Single camera backend — Only
PiCameraHQis implemented. TheBaseCameraABC exists for adding USB webcam or other backends. - No image thumbnails — The storage route lists images but doesn't serve thumbnails. Would improve the web UI.
- No config validation —
load_config()merges but doesn't validate ranges/types. Bad values cause runtime errors. - Scan resume — If the Pi loses power mid-scan, there's no way to resume. The manifest records what was captured, so a resume feature could skip already-captured positions.
- Processing script is basic — The masking assumes a light grey background. The focus threshold is a single global value. Both need tuning per specimen.
- Motor position calibration — Positions are in raw microsteps. There's no calibration to real-world units (degrees, mm).
Development Environment
On the Pi
cd /path/to/scant-pi
source venv/bin/activate # or use system-site-packages venv
python run.py --log-level debug
On a Mac/PC (without hardware)
The camera and stepper modules will fail without real hardware. For UI and API development:
- Create mock implementations of
BaseCameraandScannerController - Or run with
--log-level debugand handle the connection errors in the UI
Testing
There are no automated tests yet. This is a good area for contribution. Key testable units:
config.py— deep merge, load/save round-trip, default completenessscanner.py— scan loop with mock controller and camerastorage.py— drive detection (can mock/mediacontents)- API routes — FastAPI's
TestClient
Coding Conventions
- Python 3.11+ (Pi OS Bookworm ships 3.11)
- No type annotations on existing code (don't add them retroactively)
- Logging via
logging.getLogger(__name__)in every module - Config keys use
snake_case - API paths use kebab-case for multi-word segments
- Hardware access is always in
scant_core/, never inscant_web/ - Imports from
scant_web.appinside functions (avoid circular imports)
Useful Commands
# Check camera
libcamera-hello --list-cameras
# List stepper controllers
ticcmd --list
# Query a specific stepper
ticcmd -d <serial> --status --full
# Watch systemd logs
journalctl -u scant-pi -f
# Restart service after code changes
sudo systemctl restart scant-pi
# Check USB drives
lsblk
udisksctl status
Origin & License
- MIT license with attribution to scAnt
- The scan loop logic, stepper protocol, and config structure are adapted from scAnt
- All GUI code, FLIR camera code, and Windows-specific code was discarded
- GitHub: https://github.com/reprahkcin/scant-pi