Files
timi/AGENTS.md

15 KiB

AGENTS.md — SciCam Logger

What this is

Android camera app (scicam/) for scientific archiving. Captures images + JSON sidecars. Exposes a NanoHTTPD REST API so a host computer can trigger captures remotely.

Build & Deploy

Host build (requires ANDROID_HOME, JDK 17)

cd scicam
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

Docker build (requires Docker; adb stays on host)

cd scicam
./docker-build.sh
adb install app/build/outputs/apk/debug/app-debug.apk
  • Min SDK 24, target SDK 34, JVM 17.
  • Build files are Groovy DSL (.gradle), not Kotlin DSL.

API

Server runs on device port 8080. Device IP is shown in the app UI.

api_spec.yaml at repo root is the OpenAPI 3.0 source of truth for endpoints.

Endpoint Method Purpose
/api GET API metadata
/status GET Current qr_item_id, tags, locked, last_qr, last_capture, auto_capture, debug, device_name, device_id
/captures GET List all images and videos on device with sidecar status.
/photo/last GET Download the most recent capture as JPEG. Returns 404 if none.
/photo/<filename> GET Download a specific photo by filename (e.g. Pixel_7_20260101_120000.jpg). Returns 404 if not found.
/video/last GET Download the most recent video as MP4. Returns 404 if none.
/video/<filename> GET Download a specific video by filename (e.g. Pixel_7_20260101_120000.mp4). Returns 404 if not found.
/sidecar/<filename> GET Download JSON sidecar for a given base filename. Returns 404 if not found.
/settings POST Set qr_item_id, tags, lock (boolean), debug (boolean). Returns updated state.
/capture POST Trigger photo. Use --max-time 15. Returns {success, filename, uri}.
/record POST Toggle video recording. Returns {recording, filename, duration_ms}.
/auto-capture POST Set enabled (boolean). Enables/disables QR auto-capture. Returns updated state.
/events GET SSE stream. Broadcasts capture results as data: <json>\n\n.

Critical: POST only for /capture, /settings, and /auto-capture. GET returns 404.

Testing

Run the API client test suite against a device on the same network:

./scicam_api_test.sh <device-ip>
  • Requires jq (used to parse capture-filename responses).
  • Add new API endpoints to this script as they are added to SciCamApiServer.kt.

Architecture Notes

  • MainActivity.kt implements SciCamApiServer.ApiListener. The server posts camera control to the UI thread but reads state fields directly.
  • Images → Pictures/SciCam/ (MediaStore, visible over MTP). Sidecars → Documents/SciCam/.
  • MetadataLogger.kt builds sidecar JSON with EXIF + camera_settings.
  • BoofCV is wired in for in-preview QR detection (replaced the ZXing Activity hop). CameraX + MediaStore storage unchanged.
  • capture() blocks up to 5 s waiting for the CameraX callback; curl callers should still use --max-time 15.

Clients

  • GoPro USB controllerclients/gopro-scicam/
  • GPCam (Generalplus / Beaver Point M2C)clients/gpcam/

The GoPro client history was merged from the upstream Gitea repo tami/gopro-scicam.

Gitea Workflow

  • Remotes point to https://git.telavivmakers.space. The tami org owns all repos (tami/timi, tami/gopro-scicam).
  • Token lives at ~/.config/timi/gitea-token (mode 0600). Never exported to shell env or embedded in URLs.

Agent rule: When the user asks to push, always run bin/gitea-push origin <branch> (branch is usually master). Do not run raw git push — it will hang waiting for a password because the remote is HTTPS and no credentials are in .git/config.

bin/gitea-push — authenticated push without leaking credentials

Uses git -c http.extraHeader so the token stays in process memory only — never written to .git/config or shell history.

bin/gitea-push origin master

bin/gitea-api — authenticated Gitea REST API calls

Uses Authorization: token header instead of ?token= query param so the token does not appear in server access logs.

bin/gitea-api POST /org/tami/repos -d '{"name":"repo-name","private":false}'
bin/gitea-api GET  /repos/tami/timi

Legacy (deprecated)

Do not use these patterns — they leak the token:

  • export GITEA_SERVER_TOKEN=... in .bashrc (removed; token in every process env)
  • ?token=$(cat ...) in curl URLs (token in server logs)
  • git remote set-url ... TOKEN@... dance (token in .git/config on failure)

Dashboard (dashboard/)

Terminal UI for monitoring and auto-syncing multiple SciCam devices to ~/timi/capture. Built in Rust with ratatui + tokio.

cd dashboard
cargo build --release
./target/release/scicam-dashboard

Agent instruction: starting the dashboard If the user says "start dashboard", check whether the current shell is inside a tmux/screen/byobu session ($TMUX or $STY or ps -p $$ -o ppid= chain includes tmux/screen). If so, open a new window titled tdash in the current session instead of spawning a detached session. If not in a multiplexer, create a new tmux session named scicam-dashboard as a fallback.

Key controls:

  • ↑/↓ j/k — Navigate devices
  • a — Add device (enter Name + IP)
  • d — Delete selected device
  • r / R — Refresh one / all devices
  • s / S — Sync one / all devices now
  • w — Toggle SSE watch (live capture events)
  • q — Quit

The dashboard polls registered devices every 10s. Devices marked auto_sync download missing .jpg, .mp4, and .json sidecars automatically. Manual syncs via s/S are always available.

Device registry and config live at timi.conf in the repo root (/home/user/timi/timi.conf). capture_dir defaults to ~/timi/capture.

Discovery Protocol

SciCam devices announce their presence via UDP multicast so hubs and clients can auto-discover them without hard-coding IP addresses.

See references/discovery_protocol.md for the full specification, verified implementation snippets, and hub behaviour.

GoPro Client (clients/gopro-scicam/)

Flask-based USB controller for GoPro HERO12. Serves web UI on port 5000, captures download to local captures/ with JSON sidecars.

Xbox Controller Mapping (Browser Gamepad API)

Tested with "Microsoft X-Box 360 pad" on Linux (/dev/input/js0). Note: Standard mapping (gp.mapping === 'standard') button indices differ from X-input labels:

Button Index Physical Label Action
0 A Capture photo
1 B Toggle QR display overlay
2 X Capture photo
3 Y Next Box (regenerate item_id)

QR View (--qrview)

  • Spawns a borderless Tkinter window on DP-1 (1920x1080+0+0)
  • Displays the current item_id as a maximized QR code
  • Polls for UUID changes every 500ms and redraws with green flash
  • Left-click or N key = regenerate box ID
  • Right-click or Q/Escape = close window

Sidecar ID Model

  • qr_item_id: single point of truth — the QR visible in the photo (either detected from image or current box ID fallback). May be empty/null if no QR is available.
  • capture_id: unique UUID generated per individual photo/video

Unified Sidecar Schema (version 1.5)

All SciCam devices (Android and GoPro) now emit the same sidecar JSON structure. Old sidecars remain at version 1 and are left as-is.

{
  "version": 1.5,
  "capture_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "qr_item_id": "ITEM-001",
  "timestamp": "2026-04-26T12:00:00+00:00",
  "filename": "Pixel_7_20260426_120000.jpg",
  "location": null,
  "tags": ["archive", "lab"],
  "camera_settings": {
    "iso": "100",
    "exposure_time": "1/125",
    "aperture": "f/1.8",
    "focal_length": "24mm",
    "lens_mode": null,
    "cx": null,
    "fx": null,
    "ae_locked": false,
    "awb_locked": false
  },
  "origin": {
    "ip_address": "192.168.1.101"
  },
  "source": "scicam-android",
  "device_name": "Pixel 7",
  "device_id": "a1b2c3d4e5f67890",
  "camera_file": "Pixel_7_20260426_120000.jpg"
}
Field Type Description
version number Sidecar schema version. 1 = legacy, 1.5 = unified.
capture_id string Unique UUID for this specific capture.
qr_item_id string | null QR text from the image, or fallback box ID. Null/empty if no QR.
timestamp string ISO 8601 UTC timestamp (YYYY-MM-DDTHH:mm:ssZ).
filename string Local filename on the device.
location null Reserved for future GPS data.
tags array of strings Tags applied at capture time.
camera_settings object Common baseline keys always present (iso, exposure_time, aperture, focal_length, lens_mode, cx, fx, ae_locked, awb_locked). Unsupported keys are null. Device-specific extras may be added.
origin.ip_address string | null Device IP at time of capture.
source string "scicam-android" or "gopro".
device_name string Human-readable device name (e.g. Build.MODEL).
device_id string Unique persistent device identifier.
camera_file string Original camera filename (same as filename for local captures).

Dataset Viewer (tools/dataset_viewer.py)

Rerun-based visualization tool for browsing SciCam capture datasets. Supports both local and remote SSH-mounted directories simultaneously.

Setup

cd tools
uv venv .venv --python python3.13
uv pip install rerun-sdk numpy pillow

Usage

# Single local mount
./tools/dataset_viewer.py ~/timi/capture

# Multiple mounts (local + remote)
./tools/dataset_viewer.py ~/timi/capture user@fedora.lan:~/timi/capture

# With filters
./tools/dataset_viewer.py user@fedora.lan:~/timi/capture --qr ITEM-001

# Save to .rrd for later viewing
./tools/dataset_viewer.py ~/timi/capture --save output.rrd
rerun output.rrd

Features

  • Multi-mount: Load any number of local or remote (user@host:path) capture directories
  • Sidecar normalization: Handles legacy v1 and unified v1.5 sidecars seamlessly
  • Remote streaming: Images stream over SSH (cat) — no local copy required
  • Filtering: --qr, --source, --device substring filters
  • Timeline plots: ISO, exposure, aperture, focal length, AE/AWB lock over time
  • Blueprint layout: Image gallery, metadata panel, and time-series plots

Agent instruction: running the viewer If the user asks to run the dataset viewer (or anything related to it), assume they want it in a tmux window named runio inside the current tmux session. Create the window if it does not exist, kill any previous viewer process in that window, and start the viewer with --serve there. Do not spawn detached background processes.

When using --serve, the Rerun web viewer is on port 9090 (not 9876). Port 9876 is the internal gRPC proxy and will return 400 to browsers. Use fedora.lan (not raw IP) when telling the user the URL.

Network Hostnames

When the user mentions machines by short name (devdesk, fedora, eight, pop-os), resolve them by appending .lan — e.g. fedora.lan, eight.lan. These are the LAN hostnames known to all machines on the network. Do not use raw IPs unless explicitly told to.

Tmux Workflow

Long-running services run in named windows inside the user's existing tmux session (never as detached background processes). Check $TMUX / $STY to detect if already inside a multiplexer.

Window Purpose Command / Notes
tdash SciCam dashboard cd dashboard && ./target/release/scicam-dashboard
runio Rerun dataset viewer ./tools/dataset_viewer.py ... --serve (port 9090)
gopro GoPro USB client cd clients/gopro-scicam && python app.py (port 5000)

Agent rule: If asked to start a service and you are inside tmux, open/reuse the dedicated window above (create it if missing, kill any previous process in that window first). If not inside tmux, create a new session named after the service as a fallback.

Plan Roadmap

This is an archive and dataset for audio-visual inventory. Treat existing files as immutable. The API-first UI (api-first branch heritage) is the primary interface for automated capture.

Milestones already shipped

  • BoofCV in-preview QR detection (replaced ZXing Activity hop) — BoofCvQrAnalyzer.kt
  • NanoHTTPD REST API (/status, /settings, /capture, /record, /auto-capture, /events)
  • Unified sidecar schema v1.5 across Android and GoPro
  • UDP multicast auto-discovery (239.255.0.1:9876) — dashboard + GoPro client
  • Dashboard: ratatui TUI with gallery/sidecar preview, SSE watch, auto-sync
  • GPCam Rust client for Beaver Point M2C / Generalplus (gpcam.rs)

Verified extension points (do not invent APIs — extend existing schema)

Extension Where Status
GPS location MetadataLogger.kt line 35 hard-codes JSONObject.NULL; populate once archive API schema is ready Blocked on external schema
Camera calibration (cx, fx) Sidecar keys reserved; BoofCV calibration exists in dependency tree (boofcv-core:1.3.0) Not wired yet
Focus distance slider Camera2 interop + LENS_FOCUS_DISTANCE SeekBar; scaffolded in README Not started
RAW capture ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY + OUTPUT_FORMAT_RAW on supported hardware Not started
Preview overlay for barcode region PreviewView overlay drawable + BoofCV detection bounding boxes Partial: detection works, no visual feedback
Motion auto-capture GoPro client has masked motion detection + ROI editor; Android has QR auto-capture only (/auto-capture) Platform gap

Operational rules

  • Do not regenerate or rename existing capture files or sidecars. The dataset is append-only.
  • Multi-device: Android SciCam uses port 8080. GoPro controller uses port 5000. GPCam control TCP is 8081; its RTSP stream is 192.168.10.1:8080.
  • Schema compatibility: If you add a sidecar field, it must be optional/nullable and documented in api_spec.yaml and MetadataLogger.kt.

Local Agent Skills

This repo bundles its own DokuWiki skill under dokuwiki/skills/dokuwiki/. The installed copy also mirrors to .agents/skills/dokuwiki/. When calling scripts directly, prefer the repo path:

./dokuwiki/skills/dokuwiki/scripts/doku-read.sh  "timi"
./dokuwiki/skills/dokuwiki/scripts/doku-edit.sh  "namespace:page"
./dokuwiki/skills/dokuwiki/scripts/doku-media-upload.sh <file> <media-id> [--overwrite]

The skill tool loads from .agents/skills/dokuwiki/ automatically, but shell-level operations should use the repo path above.