# Deployment (Jetson) Repo: `https://git.proit.id/andrew/karung-counting-feedmill-semarang` (`git remote set-url origin ` after the ervan → andrew transfer). ## Systemd services | Unit | Runs | After | |---|---|---| | `karung-counter.service` | `/usr/bin/python3 predict.py` (cwd `/home/jetson/karung`, `QT_QPA_PLATFORM=offscreen`) | `network.target` | | `karung-counter-dashboard.service` | `/usr/bin/python3 counter_dashboard.py` | `network.target` + counter | | `mediamtx.service` | MediaMTX restream (see `zones.json:external_stream_url`) | — | Both app units: `Restart=always`, `RestartSec=5`, load `EnvironmentFile=/home/jetson/karung/.env`. ```bash sudo systemctl enable --now karung-counter karung-counter-dashboard sudo systemctl restart karung-counter karung-counter-dashboard systemctl status karung-counter karung-counter-dashboard --no-pager ``` ## Deploy flow (`deploy_to_jetson.py`) Paramiko sync of `templates/{operator,monitoring,base}.html`, `counter_dashboard.py`, `predict.py`, `config.yaml`, `.env` **plus `models/*.engine`** (v4-best, yolo11n-sack+box, best, truck-detector, model_karung_truk) → `192.168.192.96:/home/jetson/karung/` (creates remote `models/` if missing, skips missing local files), then restarts both services and checks status + ports (5000/5721). Run from the dev machine. `.pt`/`.onnx` stay local-only (dev/export). ## TensorRT export On the Jetson (needs CUDA): `python3 export_model.py models/.pt` exports to FP16 `.engine` next to the `.pt` (default: karung-dimuat seg model). Production loads `.engine` only — see `models/modelREADME.md` for which weights each mode uses. ## Runtime data files - SQLite `jetson_counter.db`: `batches(counting_date, batch_number, camera_name, object_label, count, start/end_time, box_*, plate, do_numbers, expected_*, net_sack, net_box)`, `daily_summaries(...)`, `delivery_orders(...)` (DO photos). - `current_batch.json` (crash recovery), `batch_mode.json` (batch flow mode only: `auto`|`do_manual`|`manual` — model mode lives in `config.yaml`), `do_settings.json` (require_plate/do + ocr_engine), `$OUTPUT_DIR/do_photos/YYYY-MM-DD/` (7-day retention), `batch_history_folder/batch_.json` + `hasil_perhitungan.json` (per-batch reports). - Live frame: `/dev/shm/jetson-counter/live_frame.jpg` (written every 2nd frame, consumed by `/api/live-video` MJPEG). - Helpers: `check_jetson_db.py` (root); retired ops scripts in `archive/` (`backup.py`, `dump_db.py`, `migrate_jetson_db.py`, `merge_batches_*.py`, `update_batches.py`, `diagnose_truck_jetson.py`). ## DO OCR packages Default engine is **RapidOCR** (PP-OCR via onnxruntime, bundled models — offline-friendly; installed with `requirements.txt`): ```bash pip install rapidocr_onnxruntime ``` Backups (`ocr_engine` — office monitoring UI toggle, no restart): `Tesseract (Backup, Light)`: ```bash # lighter; system package + Indonesian traineddata sudo apt install tesseract-ocr tesseract-ocr-ind pip install pytesseract ``` `PaddleOCR (Accuracy, Heavy to run)`: ```bash # optional; heavier — see paddleocr docs for Jetson wheels pip install paddleocr ``` Missing deps → upload returns explicit error (never silent fallback); flip engine back to `RapidOCR`/`Tesseract` from the office dashboard (`:5721`). Operator page: `http://:5000/operator` (smartphone camera capture for DO photos). Photo dir under `output.dir` with 7-day retention (hourly purge in dashboard process). ## Dashboard (`counter_dashboard.py`) Pages: `/` + `/monitoring`, `/operator` (manual start/stop; DO panel in `do_manual`), `/history`, `/analytics`. Key APIs: `/api/live-video`, `/api/current-batch`, `/api/previous-batch`, `/api/batch/{start,stop,stop-preview,mode}`, `/api/model-modes` (mode list is derived from `config.yaml`, so future modes appear automatically), `/api/do/{upload,photo/,staged,settings,retention}` + `PUT/DELETE /api/do/`, `/api/summary`, `/api/daily-data`, `/api/day-detail/`, `/api/recent-batches`, `/api/available-dates`, `/api/export-daily-csv`, `/api/export-day-csv/` (Excel via openpyxl), `/api/batch-clip//` + `/status`, `/file`, `/api/batch-clip/pending` (office-only, see below). Port split: mode / model_mode / require_plate / require_do / ocr_engine POSTs → **office 5721 only** (403 on 5000). Smartphones open `http://:5000/operator` for camera capture. ## Batch clip download (motionEye) History page `Klip` button runs an **async job** (all endpoints **office port 5721 only**, 403 on 5000): - `POST /api/batch-clip//` — idempotent: starts the job or returns the existing one → `200 {"success": true, "status": "running"|"ready", "job": "/"}`. - `GET …///status` → `{"success": true, "status": "running"|"ready"|"error", "error": "", "elapsed": }` (404 if no job). - `GET …///file` → mp4 attachment (409 while running, 404 if error/none). - `GET /api/batch-clip/pending` → all jobs, newest first — restores button state after reload/navigation and feeds the cross-page toast. Button states: `Klip` → `Memproses… dtk` (disabled; status polled every 1.5 s, `pending` refreshed every 3 s while a job on the page runs) → `Siap — unduh` → `Unduh lagi` after the first download (instant, file already exists); failure → `Gagal` (click shows the error). Jobs for other dates are left to the toast. Toast: every office page (`templates/base.html`) polls `pending` every 3 s — bootstrap once, a 403 on the operator port disables polling for that tab — and shows a non-blocking bottom-right **`Klip siap`** / **`Klip gagal`** card (auto-dismiss 8 s, click → `/history`). Shown once per job per tab (`sessionStorage clipNotified`, capped at 50 keys). Rendering itself is unchanged (`clip_batch` in `src/clip.py`, ffmpeg under the hood). The finished temp file is kept **≤1 h** so `Unduh lagi` re-downloads without re-rendering; older `clip_*.mp4` files are swept then. Jobs live in dashboard **memory** — a `karung-counter-dashboard` restart drops them (status 404 → button falls back to `Klip`, press again). - URL keys (`.env`): `MOTIONEYE_URL` (base URL; empty → 404 `motionEye belum dikonfigurasi (MOTIONEYE_URL)`), `MOTIONEYE_CAMERA_ID` (default `2`). - `MOTIONEYE_CLIP_PAD` — float seconds padded before/after the batch window at both cut points (default `3` → ±3 s; empty/invalid falls back to `3.0`). - `MOTIONEYE_OSD_ALIGN` — OSD-clock alignment, default **on** (unset = on; `0` / `false` / `no` / empty = off). Why: the camera's burned-in OSD clock and the recording clock drift — up to ~+22 s inside a long clip — because of dropped frames, so filename-based offsets cut at the wrong media time. When on, `clip_batch` OCRs **1 frame** at each cut point (~2–6 s extra per clip, result cached per clip+second) and corrects the cut times. `0` → filename-based offsets only. If OCR fails, it silently falls back to filename-based offsets; both cases log one `[CLIP] batch=... pad=... align=...` line at request start (plus any `[CLIP]` alignment detail lines) — check with `journalctl -u karung-counter-dashboard`. - motionEye HTTP API used: `GET /movie//list/` (recording segments in the window) and `GET /movie//download` (segment bytes). - Requires `ffmpeg` / `ffprobe` on PATH — both already installed on the Jetson. - **Recording retention: motionEye `preserve_movies: 3` → only the last 3 days of recordings exist.** Older batches return 404 (`ClipError`, no recording in window). - Errors are surfaced on the job: `…/status` reports `status: "error"` with the `error` message (`…/file` → 404), and a failing `POST` answers non-2xx `{"success": false, "error": ""}`. Finished file lives at `$(dirname DB)/tmp/clip_.mp4` (kept ≤1 h, then swept). - Output filename built from the batch **start_time**: `b{batch}-{YYYY-MM-DD}-{HH-MM-SS}.mp4` (e.g. batch 21 starting `2026-09-26T14:25:09.123456` → `b21-2026-09-26-14-25-09.mp4`).