# 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) ```bash cd scicam ./gradlew assembleDebug adb install app/build/outputs/apk/debug/app-debug.apk ``` ### Docker build (requires Docker; adb stays on host) ```bash 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/` | 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/` | GET | Download a specific video by filename (e.g. `Pixel_7_20260101_120000.mp4`). Returns 404 if not found. | | `/sidecar/` | 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: \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: ```bash ./scicam_api_test.sh ``` - 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 controller** — `clients/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 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. ```bash 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. ```bash 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. ```bash 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`](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. ```json { "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 ```bash cd tools uv venv .venv --python python3.13 uv pip install rerun-sdk numpy pillow ``` ### Usage ```bash # 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: ```bash ./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 [--overwrite] ``` The `skill` tool loads from `.agents/skills/dokuwiki/` automatically, but shell-level operations should use the repo path above.