116 lines
4.7 KiB
Markdown
116 lines
4.7 KiB
Markdown
# ERD — karung counter (SQLite)
|
||
|
||
Data model for the karung feedmill counter. SQLite DB at
|
||
`$OUTPUT_DIR/jetson_counter.db` (`config.yaml` `output.dir`,
|
||
default `/opt/jetson-counter/`; env override `DB_PATH`).
|
||
|
||
**No DB foreign keys enforced** (zero `FOREIGN KEY` in code) — all relationships
|
||
are soft, by naming/best-effort writes.
|
||
|
||
```mermaid
|
||
erDiagram
|
||
batches ||--o{ delivery_orders : "soft via batch_id / do_numbers"
|
||
batches ||--o{ daily_summaries : "rollup by counting_date"
|
||
|
||
batches {
|
||
INTEGER id PK
|
||
TEXT counting_date "NOT NULL, YYYY-MM-DD, cutoff-shifted"
|
||
INTEGER batch_number "NOT NULL, per date+camera+label"
|
||
TEXT camera_name "NOT NULL, default CC1"
|
||
TEXT object_label "NOT NULL, default karung-pakan"
|
||
INTEGER count "NOT NULL, gross sack loading"
|
||
TEXT start_time "NOT NULL, ISO datetime"
|
||
TEXT end_time "NOT NULL, ISO datetime"
|
||
TIMESTAMP created_at "default CURRENT_TIMESTAMP"
|
||
INTEGER box_loading "NOT NULL, default 0"
|
||
INTEGER box_unloading "NOT NULL, default 0"
|
||
TEXT model_mode "NOT NULL, default A"
|
||
TEXT plate "NOT NULL, default ''"
|
||
TEXT do_numbers "NOT NULL, JSON array of No. DO"
|
||
INTEGER expected_sack "NOT NULL, default 0"
|
||
INTEGER expected_box "NOT NULL, default 0"
|
||
INTEGER net_sack "NOT NULL, default 0"
|
||
INTEGER net_box "NOT NULL, default 0"
|
||
}
|
||
|
||
delivery_orders {
|
||
INTEGER id PK
|
||
TEXT counting_date "NOT NULL"
|
||
TEXT photo_path "NOT NULL, relative under do_photos/"
|
||
TEXT no_do "default ''"
|
||
TEXT plate "default ''"
|
||
INTEGER expected_sack "default 0"
|
||
INTEGER expected_box "default 0"
|
||
TEXT ocr_raw "nullable JSON: no_do, plate, ocr_text, engine, ocr_ok, ocr_error"
|
||
TEXT status "default 'draft'"
|
||
INTEGER batch_id "soft ref to batches.id, nullable"
|
||
TIMESTAMP created_at "default CURRENT_TIMESTAMP"
|
||
TIMESTAMP updated_at "default CURRENT_TIMESTAMP"
|
||
}
|
||
|
||
daily_summaries {
|
||
INTEGER id PK
|
||
TEXT counting_date "NOT NULL"
|
||
TEXT camera_name "NOT NULL"
|
||
TEXT object_label "NOT NULL"
|
||
INTEGER total_count "NOT NULL, default 0, SUM(batches.count)"
|
||
INTEGER total_batches "NOT NULL, default 0, COUNT(batches.id)"
|
||
TIMESTAMP updated_at "default CURRENT_TIMESTAMP"
|
||
}
|
||
```
|
||
|
||
## Keys
|
||
|
||
- `batches`: `UNIQUE(counting_date, batch_number, camera_name, object_label)`
|
||
- `daily_summaries`: `UNIQUE(counting_date, camera_name, object_label)`
|
||
- `delivery_orders`: `idx_do_date_status` on `(counting_date, status)`
|
||
|
||
## Relationships (logical)
|
||
|
||
| From | To | How |
|
||
|---|---|---|
|
||
| `delivery_orders` → `batches` | N—1 | On stop: `batches.do_numbers` JSON array of `No. DO`; `delivery_orders.batch_id` set best-effort. Active batch: `current_batch.json.do_ids`. |
|
||
| `batches` → `daily_summaries` | N—1 | Same `(counting_date, camera_name, object_label)`; stop recompute `SUM(count)`, `COUNT(id)`. |
|
||
|
||
## Derived columns
|
||
|
||
- `net_sack = count − sack unloading`, `net_box = box_loading − box_unloading`
|
||
(`src/do_batch.py:37`).
|
||
- **Sack unloading has no column** — it only survives inside `net_sack`
|
||
(legacy rows with `net_sack=0` from the `ALTER TABLE ... DEFAULT 0`
|
||
migration are not backfillable).
|
||
- History UI shows gross `count` / `box_loading` only (no Net).
|
||
|
||
## Status (`delivery_orders.status`)
|
||
|
||
`draft` (photo upload) → `staged` (saved/edited) → `attached` (in
|
||
active/stopped batch). Discard stop with both nets 0 returns DOs to
|
||
`staged`. `discarded` appears in older notes but **no code writes it** —
|
||
discard is a `DELETE`.
|
||
|
||
## Hazards
|
||
|
||
- `INSERT OR REPLACE` on `batches` (UNIQUE match = delete+insert) changes
|
||
`id` on re-stop → can orphan `delivery_orders.batch_id`.
|
||
- Schema divergence: `predict.py init_db()` creates only `batches` +
|
||
`daily_summaries` (3 extra cols, no `delivery_orders`); full 3-table
|
||
schema (9 extra `batches` cols) comes from `counter_dashboard.py`
|
||
`_ensure_db()` at import. Fresh DB touched only by `predict.py` is
|
||
incomplete; its DO updates fail silently (`try/except: pass`).
|
||
|
||
## JSON sidecars + photo tree (not in SQLite)
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `$OUTPUT_DIR/batch_mode.json` | `{mode: auto\|do_manual\|manual}` |
|
||
| `$OUTPUT_DIR/current_batch.json` | active batch + `do_ids`, plate, expected_*, nets inputs |
|
||
| `$OUTPUT_DIR/do_settings.json` | `require_plate`, `require_do`, `ocr_engine` |
|
||
| `$OUTPUT_DIR/do_photos/YYYY-MM-DD/do_<uuid>.jpg` | source photos (7-day retention) |
|
||
|
||
## Sources
|
||
|
||
- DDL: `counter_dashboard.py:123-198` (`_ensure_db`, superset),
|
||
`predict.py:117-166` (`init_db`, partial).
|
||
- Writes: `predict.py:249-306`, `counter_dashboard.py:610-668`.
|
||
- Doc flow: `docs/do-erd.md` (merged into this file).
|