161 lines
8.0 KiB
Markdown
161 lines
8.0 KiB
Markdown
# Deployment (Jetson)
|
||
|
||
Repo: `https://git.proit.id/andrew/karung-counting-feedmill-semarang`
|
||
(`git remote set-url origin <url>` 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/<name>.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_<ts>.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://<jetson>: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/<id>,staged,settings,retention}` +
|
||
`PUT/DELETE /api/do/<id>`, `/api/summary`, `/api/daily-data`,
|
||
`/api/day-detail/<date>`, `/api/recent-batches`, `/api/available-dates`,
|
||
`/api/export-daily-csv`, `/api/export-day-csv/<date>` (Excel via openpyxl),
|
||
`/api/batch-clip/<date>/<batch_number>` + `/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://<host>: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/<date>/<batch_number>` — idempotent: starts the job
|
||
or returns the existing one → `200 {"success": true, "status":
|
||
"running"|"ready", "job": "<date>/<n>"}`.
|
||
- `GET …/<date>/<batch_number>/status` → `{"success": true, "status":
|
||
"running"|"ready"|"error", "error": "<msg|empty>", "elapsed": <s>}`
|
||
(404 if no job).
|
||
- `GET …/<date>/<batch_number>/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… <s> 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/<id>/list/` (recording segments in the
|
||
window) and `GET /movie/<id>/download<path>` (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": "<msg>"}`. Finished file lives at
|
||
`$(dirname DB)/tmp/clip_<uuid>.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`).
|