docs: adacam + adamaps codebase audit + gap analysis (2026-03-23)
This commit is contained in:
parent
dd7db4fc6b
commit
3543b47ab8
1 changed files with 763 additions and 0 deletions
763
docs/adacam-codebase-audit-2026-03-23.md
Normal file
763
docs/adacam-codebase-audit-2026-03-23.md
Normal file
|
|
@ -0,0 +1,763 @@
|
|||
# ADAMaps + AdaCam Codebase Audit
|
||||
**Date:** 2026-03-23
|
||||
**Repos:** `Sulkta-Coop/adacam`, `Sulkta-Coop/adamaps`
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
We've built a complete road sign detection and verification pipeline:
|
||||
- **adacam** — Bee hardware tooling, liberation scripts, and a forwarder that reads detections from Hivemapper's local odc-api and sends them to our ADAMaps API
|
||||
- **adamaps** — Flask API handling detection ingestion, sign clustering, agent verification (3-phase consensus), dedup voting, and weekly MAP token payouts via Cardano
|
||||
|
||||
**Key findings:**
|
||||
1. **Missing data fields:** The forwarders don't capture `cam_heading`, `attributes` JSON (speed values, turn rules), `azimuth`, or camera position — data that's already in odc-api.db
|
||||
2. **Two forwarder versions exist:** v1 reads SQLite directly, v2 reads REST — both are missing critical fields
|
||||
3. **No systemd service deployed:** The forwarder service file exists but isn't actively running
|
||||
4. **Payout system is production-ready:** Well-designed state machine with advisory locks, Koios integration, Matrix alerting
|
||||
5. **Consensus engine is sound:** Advisory locks, deterministic tie-breakers, probation/tier gates
|
||||
|
||||
---
|
||||
|
||||
## 1. adacam Repo Structure
|
||||
|
||||
```
|
||||
adacam/
|
||||
├── README.md
|
||||
├── api/
|
||||
│ └── (API notes/docs)
|
||||
├── docs/
|
||||
│ ├── BEE_DATA_PIPELINE.md
|
||||
│ ├── README.md
|
||||
│ ├── SETTING_UP_TOOLING_README.md
|
||||
│ ├── SYSTEMD_SERVICE_SETUP.md
|
||||
│ ├── bee-liberation-guide.md
|
||||
│ ├── dashcam-config-guide.md
|
||||
│ ├── deploy-steps.md
|
||||
│ └── hivemapper-odc-api-deep-dive-2026-03-23.md
|
||||
├── keys/
|
||||
│ ├── id_ed25519.pub
|
||||
│ └── README.md
|
||||
├── recon/
|
||||
│ ├── config.json
|
||||
│ ├── layout-tree.txt
|
||||
│ ├── package.json
|
||||
│ ├── ps-output.txt
|
||||
│ ├── process_list.txt
|
||||
│ ├── redis-keys.txt
|
||||
│ ├── systemd-units.txt
|
||||
│ └── units/
|
||||
│ ├── camera.service
|
||||
│ ├── framerate_log.service
|
||||
│ ├── gnss.service
|
||||
│ ├── map-ai.service
|
||||
│ ├── odc-api.service
|
||||
│ └── (others)
|
||||
├── recovery/
|
||||
│ └── liberate-v1.sh
|
||||
├── scripts/
|
||||
│ └── (build scripts, example bundles)
|
||||
├── security/
|
||||
│ └── security-posture-initial.md
|
||||
└── services/
|
||||
├── capture/
|
||||
│ ├── adacam-forwarder.py # v1: reads odc-api.db directly
|
||||
│ ├── adacam-forwarder-v2.py # v2: reads odc-api REST endpoints
|
||||
│ ├── adacam-forwarder-v2.service
|
||||
│ └── config.json
|
||||
├── updater/
|
||||
│ └── (auto-update scripts)
|
||||
└── wigle/
|
||||
└── (WiFi scanning integration)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. adacam-forwarder Analysis
|
||||
|
||||
### 2a. v1: `adacam-forwarder.py` (SQLite Direct)
|
||||
|
||||
**Location:** `services/capture/adacam-forwarder.py`
|
||||
|
||||
**What it reads:**
|
||||
```sql
|
||||
SELECT id, ts, image_name, class_label, confidence, lat, lon
|
||||
FROM landmarks
|
||||
WHERE id > ?
|
||||
AND negative_class = 0
|
||||
AND confidence >= ?
|
||||
AND lat IS NOT NULL AND lon IS NOT NULL
|
||||
AND lat != 0 AND lon != 0
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
```
|
||||
|
||||
**Cursor strategy:** Tracks `last_detection_id` in `/data/adacam/forwarder_state.json`, queries `WHERE id > last_id`. Correct approach — matches odc-api's `fetchLandmarksFromId`.
|
||||
|
||||
**What it sends to ADAMaps:**
|
||||
```python
|
||||
{
|
||||
'id': str(row['id']),
|
||||
'ts': int(row['ts']),
|
||||
'lat': float(row['lat']),
|
||||
'lon': float(row['lon']),
|
||||
'class_label': row['class_label'] or 'unknown',
|
||||
'overall_confidence': float(row['confidence']),
|
||||
'device_id': config['device_id'],
|
||||
}
|
||||
```
|
||||
|
||||
**Image upload:** Yes — fetches from `/data/recording/cached_observations/{image_name}` and POSTs to `/api/images`.
|
||||
|
||||
### 2b. v2: `adacam-forwarder-v2.py` (REST API)
|
||||
|
||||
**Endpoints tried (in order):**
|
||||
- `/api/landmarks`
|
||||
- `/landmarks`
|
||||
|
||||
**Fetch logic:**
|
||||
```python
|
||||
url = f'{odc_url}/api/landmarks/from/{from_id}' # if from_id > 0
|
||||
url = f'{odc_url}/api/landmarks/n/{n}' # first run (bootstrap)
|
||||
```
|
||||
|
||||
**What it sends:** Same as v1, but via REST response parsing instead of SQLite.
|
||||
|
||||
**Image upload:** No — v2 only forwards detection metadata, no image upload.
|
||||
|
||||
**GPS fallback:** Queries Redis `GNSSFusion30Hz` zset if landmark has no GPS coords.
|
||||
|
||||
### 2c. Missing Fields (Both Versions)
|
||||
|
||||
| odc-api.db Column | v1 Captures | v2 Captures | Impact |
|
||||
|-------------------|-------------|-------------|--------|
|
||||
| `id` | ✅ | ✅ | cursor key |
|
||||
| `ts` | ✅ | ✅ | timestamp |
|
||||
| `class_label` | ✅ | ✅ | sign type |
|
||||
| `confidence` | ✅ | ✅ | detection confidence |
|
||||
| `lat`, `lon` | ✅ | ✅ | sign GPS position |
|
||||
| `image_name` | ✅ | ❌ | image upload |
|
||||
| `cam_lat`, `cam_lon` | ❌ | ❌ | **camera position at detection** |
|
||||
| `cam_heading` | ❌ | ❌ | **vehicle heading — sign facing** |
|
||||
| `attributes` | ❌ | ❌ | **JSON with `speed_label`, `turn_rule`** |
|
||||
| `azimuth` | ❌ | ❌ | estimated sign azimuth |
|
||||
| `width`, `height` | ❌ | ❌ | estimated physical size |
|
||||
| `overall_confidence` | ❌ | ❌ | fused confidence |
|
||||
| `map_feature_id` | ❌ | ❌ | FK to map_features |
|
||||
| `x1/y1/x2/y2` | ❌ | ❌ | bounding box coords |
|
||||
|
||||
**Critical miss:** The `attributes` JSON contains `{"speed_label": 25, "speed_label_conf": 0.99}` — Hivemapper's model already reads speed values from signs. We're manually doing Phase 2 agent verification to read sign text when the ML model already extracts it.
|
||||
|
||||
### 2d. Systemd Service
|
||||
|
||||
**File exists:** `services/capture/adacam-forwarder-v2.service`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AdaCam Forwarder v2
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/adacam
|
||||
ExecStart=/usr/bin/python3 /opt/adacam/services/adacam-forwarder-v2.py
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**Status:** Not deployed. The service file is in the repo but hasn't been symlinked to `/etc/systemd/system/` on the Bee.
|
||||
|
||||
---
|
||||
|
||||
## 3. adamaps Repo Structure
|
||||
|
||||
```
|
||||
adamaps/
|
||||
├── README.md
|
||||
├── Dockerfile
|
||||
├── app.py # Main Flask API
|
||||
├── payout.py # Weekly MAP payout system
|
||||
├── init.sql # Base schema (detections table)
|
||||
├── api/
|
||||
│ └── app.py # (Duplicate? Check if this is the live one)
|
||||
├── bee/
|
||||
│ └── (Bee-specific tooling)
|
||||
├── cardano/
|
||||
│ └── payout_treasury.sh
|
||||
├── db/
|
||||
│ ├── migration_001_payout.sql
|
||||
│ ├── migration_002_sybil.sql
|
||||
│ └── migration_003_dedup.sql
|
||||
├── docs/
|
||||
│ └── (architecture docs)
|
||||
├── rackham/
|
||||
│ └── (deployment configs)
|
||||
└── web/
|
||||
└── html/
|
||||
└── (frontend assets)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. adamaps Flask API (`app.py`)
|
||||
|
||||
### 4a. Configuration
|
||||
|
||||
```python
|
||||
CLUSTER_RADIUS_M = 40 # Detections within 40m → same sign
|
||||
CLUSTER_MIN_CONFIDENCE = 0.30 # Min confidence to create/update sign
|
||||
CONSENSUS_THRESHOLD = 3 # 3 agreeing agents = consensus
|
||||
MIN_ADA_STAKE_LOVELACE = 5_000_000 # 5 ADA minimum to register
|
||||
GROUNDTRUTH_PASS_THRESHOLD = 3 # 3/5 correct to pass ground truth
|
||||
```
|
||||
|
||||
### 4b. All Endpoints
|
||||
|
||||
| Method | Path | Auth | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| GET | `/api/health` | None | Health check |
|
||||
| POST | `/api/ingest` | `X-AdaMaps-Key` | Detection ingestion from forwarder |
|
||||
| POST | `/api/images` | `X-AdaMaps-Key` | Image upload |
|
||||
| GET | `/api/images/<filename>` | Rate limited | Serve image (with `?crop=sign_id` support) |
|
||||
| GET | `/api/images/<filename>/crop` | Rate limited | Serve cropped image |
|
||||
| GET | `/api/detections` | Read key | List recent detections |
|
||||
| GET | `/api/signs` | Read key | List clustered signs |
|
||||
| GET | `/api/signs/tasks` | Read key | Agent training task feed |
|
||||
| GET | `/api/stats` | None | System statistics |
|
||||
| GET | `/api/agent/info` | None | Agent API documentation |
|
||||
| POST | `/api/agent/challenge` | None | Request wallet auth nonce |
|
||||
| POST | `/api/agent/register` | None | Register agent with wallet sig |
|
||||
| GET | `/api/agent/me` | `X-Agent-Key` | Authenticated agent profile |
|
||||
| GET | `/api/agent/groundtruth` | `X-Agent-Key` | Get ground truth test (5 signs) |
|
||||
| POST | `/api/agent/groundtruth/submit` | `X-Agent-Key` | Submit ground truth answers |
|
||||
| GET | `/api/agent/status` | `X-Agent-Key` | Full agent status with rank |
|
||||
| GET | `/api/agent/consensus/<sign_id>` | `X-Agent-Key` | Consensus status per sign |
|
||||
| GET | `/api/agent/leaderboard` | None | Public leaderboard |
|
||||
| POST | `/api/agent/rotate-key` | `X-Agent-Key` | Rotate API key |
|
||||
| GET | `/api/agent/tasks` | `X-Agent-Key` | Get available tasks (phase 1 or 2) |
|
||||
| POST | `/api/agent/claim/<sign_id>` | `X-Agent-Key` | Claim a task |
|
||||
| POST | `/api/agent/submit` | `X-Agent-Key` | Submit task result |
|
||||
| GET | `/api/signs/dedup-tasks` | Read key | Get dedup voting pairs |
|
||||
| POST | `/api/agent/dedup/vote` | `X-Agent-Key` | Vote on sign merge |
|
||||
| GET | `/api/agent/dedup/status` | Read key | Dedup pair status |
|
||||
| POST | `/api/admin/payouts/trigger` | `X-AdaMaps-Key` | Manually trigger payout |
|
||||
| GET | `/api/admin/payouts/status` | `X-AdaMaps-Key` | Payout system status |
|
||||
| POST | `/api/admin/signs/recalibrate` | `X-AdaMaps-Key` | Backfill sign_id + recalc accuracy |
|
||||
|
||||
### 4c. Ingest Pipeline
|
||||
|
||||
1. **Detection arrives** via POST `/api/ingest` with `{device_id, detections: [...]}`
|
||||
2. **Each detection inserted** into `detections` table with PostGIS point geometry
|
||||
3. **Sign refinement** via `_update_sign_from_detection()`:
|
||||
- Find nearest sign of same type within 40m
|
||||
- If found: update confidence-weighted centroid, increment observation count
|
||||
- If not found and confidence ≥ 0.3: create new sign
|
||||
4. **Detection linked** to sign via `sign_id` FK
|
||||
|
||||
### 4d. Agent Verification System
|
||||
|
||||
**Phases:**
|
||||
1. **Phase 1 — Type verification:** "What type of sign is this?" (stop-sign, speed-limit, etc.)
|
||||
2. **Phase 2 — Text reading:** "What does the sign say?" (the actual text/numbers)
|
||||
|
||||
**Tiers:**
|
||||
- `probation` — New agents, must pass ground truth test (3/5 correct)
|
||||
- `standard` — Passed ground truth, can do Phase 1 tasks
|
||||
- `trusted` — Rep score ≥ 60, can do Phase 2 tasks
|
||||
- `expert` — Rep score ≥ 80, weighted higher in consensus
|
||||
|
||||
**Ground truth:** 5 random oracle-verified signs shown to agent, must get 3/5 correct to graduate from probation.
|
||||
|
||||
**Consensus engine (`check_consensus`):**
|
||||
1. Advisory lock on `(sign_id, phase)` prevents race conditions
|
||||
2. Group submissions by `(assessment, normalized_text)`
|
||||
3. Require 3+ agents with at least 2 non-probation to reach consensus
|
||||
4. Deterministic tie-breaker: more agents → higher avg confidence → lexicographic assessment
|
||||
5. Oracle fast-path: oracle submission = instant consensus
|
||||
|
||||
**Rewards (MAP tokens):**
|
||||
- Phase 1 consensus: 0.5 MAP total / split among agreeing agents
|
||||
- Phase 2 consensus: 1.0 MAP total / split among agreeing agents
|
||||
- Dedup vote: 0.125 MAP per vote
|
||||
- Dedup consensus: 0.25 MAP bonus
|
||||
|
||||
**Reputation:**
|
||||
- Agreed with consensus: +2 rep
|
||||
- Disagreed: -1 rep
|
||||
- Honest "cannot_identify" on ambiguous sign: +0.5 rep
|
||||
- Wrong "cannot_identify" on identifiable sign: -0.5 rep
|
||||
|
||||
### 4e. Dedup System
|
||||
|
||||
**Candidate pairs query:** Signs of same type within 25m, or different types within 10m, both with images, not already merged or dismissed.
|
||||
|
||||
**Voting:** Agents vote "same" or "different" with confidence (0.0-1.0).
|
||||
|
||||
**Consensus:** 2+ votes in same direction wins. Winner is sign with higher observation count (tie-break: lower ID).
|
||||
|
||||
**Merge action:** Loser's `merged_into` set to winner ID, detections re-pointed to winner.
|
||||
|
||||
### 4f. Image Serving + Cropping
|
||||
|
||||
**Full image:** `GET /api/images/{filename}`
|
||||
|
||||
**Cropped sign:** `GET /api/images/{filename}?crop={sign_id}` — uses bbox from `detections.raw_json->bbox` or `bbox_x1/y1/x2/y2` columns, crops with 15-20px padding via PIL.
|
||||
|
||||
**Current state:** Cropping works but bbox coords aren't being forwarded by the forwarder (missing `x1/y1/x2/y2`).
|
||||
|
||||
### 4g. Rate Limiting
|
||||
|
||||
```python
|
||||
limiter = Limiter(key_func=get_rate_limit_key)
|
||||
|
||||
# Separate buckets for admin vs read keys
|
||||
def get_rate_limit_key():
|
||||
key = request.headers.get("X-AdaMaps-Key", "")
|
||||
if key == API_KEY: return "trusted:admin"
|
||||
if key == READ_KEY: return "trusted:read"
|
||||
return get_remote_address()
|
||||
```
|
||||
|
||||
- `/api/images/<filename>`: 120/min
|
||||
- `/api/detections`: 60/min
|
||||
- `/api/signs`: 60/min
|
||||
- Ground truth endpoints: 10/hour or 5/hour
|
||||
|
||||
---
|
||||
|
||||
## 5. Payout System (`payout.py`)
|
||||
|
||||
### 5a. State Machine
|
||||
|
||||
```
|
||||
building → built → submitted → confirmed
|
||||
↓ ↓ ↓
|
||||
failed failed failed
|
||||
↓
|
||||
retried (earnings unlinked for next batch)
|
||||
```
|
||||
|
||||
### 5b. Key Design Patterns
|
||||
|
||||
**Advisory lock:** `pg_try_advisory_lock(98765)` ensures only one worker runs payouts.
|
||||
|
||||
**Two-connection pattern:**
|
||||
```python
|
||||
with db.engine.connect() as lock_conn:
|
||||
# lock_conn holds the advisory lock
|
||||
locked = lock_conn.execute("SELECT pg_try_advisory_lock(98765)").scalar()
|
||||
with SASession(db.engine) as session:
|
||||
# session runs on separate pool connection
|
||||
_run_weekly_payout_locked(session)
|
||||
```
|
||||
|
||||
**Address validation:** `validate_payout_address()` checks bech32 decoding before batch creation — invalid addresses excluded upfront, not mid-transaction.
|
||||
|
||||
**Stuck batch detection:** `alert_stuck_batches()` finds batches stuck in `building` or `built` for > 5 min, auto-fails `building` batches to unlink frozen earnings.
|
||||
|
||||
**Idempotent submit:** If `submit_transaction()` times out, checks if tx landed on-chain via Koios before marking failed.
|
||||
|
||||
### 5c. Koios Integration
|
||||
|
||||
**Balance check:** POST `/api/v1/address_info` with hot wallet address, parses `utxo_set[].asset_list` for MAP tokens.
|
||||
|
||||
**Confirmation check:** POST `/api/v1/tx_info` with tx hash, returns slot and block hash.
|
||||
|
||||
**Alert on unconfirmed:** If submitted batch unconfirmed for > 24 hours, sends Matrix alert.
|
||||
|
||||
### 5d. Scheduler
|
||||
|
||||
```python
|
||||
scheduler.add_job(
|
||||
func=_payout_job_wrapper,
|
||||
trigger=CronTrigger(day_of_week='mon', hour=10, minute=0, timezone='UTC'),
|
||||
id='weekly_payout',
|
||||
)
|
||||
```
|
||||
|
||||
File lock `/tmp/adamaps_scheduler.lock` ensures only one Gunicorn worker runs the scheduler.
|
||||
|
||||
---
|
||||
|
||||
## 6. Database Schema
|
||||
|
||||
### 6a. Core Tables
|
||||
|
||||
**`detections`** — Raw detection events from forwarder:
|
||||
```sql
|
||||
id SERIAL PRIMARY KEY
|
||||
device_id TEXT NOT NULL
|
||||
detected_at TIMESTAMPTZ NOT NULL
|
||||
lat, lon DOUBLE PRECISION NOT NULL
|
||||
geom GEOMETRY(Point, 4326) GENERATED
|
||||
sign_type TEXT
|
||||
confidence DOUBLE PRECISION
|
||||
image_path VARCHAR
|
||||
bbox_x1, bbox_y1, bbox_x2, bbox_y2 FLOAT
|
||||
raw_json JSONB
|
||||
sign_id INTEGER REFERENCES signs(id) -- added via migration
|
||||
```
|
||||
|
||||
**`signs`** — Clustered/deduplicated sign positions:
|
||||
```sql
|
||||
id SERIAL PRIMARY KEY
|
||||
lat, lon DOUBLE PRECISION
|
||||
sign_type TEXT
|
||||
sign_text TEXT -- from Phase 2 consensus
|
||||
heading DOUBLE PRECISION
|
||||
observation_count INTEGER
|
||||
avg_confidence DOUBLE PRECISION
|
||||
confidence_weight DOUBLE PRECISION
|
||||
device_count INTEGER
|
||||
first_seen, last_seen TIMESTAMPTZ
|
||||
verified BOOLEAN
|
||||
agent_verified BOOLEAN -- from consensus engine
|
||||
location_accuracy_m FLOAT
|
||||
merged_into INTEGER -- dedup: points to winner
|
||||
merge_dismissed BOOLEAN -- dedup: dismissed pair
|
||||
image_name TEXT
|
||||
```
|
||||
|
||||
**`agent_registry`** — Registered agents:
|
||||
```sql
|
||||
id SERIAL PRIMARY KEY
|
||||
agent_id VARCHAR(32) UNIQUE -- "agt_" + blake2b(pubkey)
|
||||
cardano_address VARCHAR(128)
|
||||
public_key_hex VARCHAR(64)
|
||||
api_key_hash VARCHAR(64) -- SHA256 of api key
|
||||
tier VARCHAR(20) -- probation/standard/trusted/expert
|
||||
reputation_score FLOAT
|
||||
total_submissions INTEGER
|
||||
consensus_agreements INTEGER
|
||||
map_earned DOUBLE PRECISION
|
||||
groundtruth_passed BOOLEAN
|
||||
is_oracle BOOLEAN
|
||||
```
|
||||
|
||||
**`agent_submissions`** — Task submissions:
|
||||
```sql
|
||||
sign_id INTEGER
|
||||
phase INTEGER -- 1 or 2
|
||||
agent_id VARCHAR(32)
|
||||
assessment VARCHAR(128)
|
||||
sign_text TEXT -- Phase 2 only
|
||||
confidence FLOAT
|
||||
in_consensus BOOLEAN -- TRUE if agreed with final consensus
|
||||
```
|
||||
|
||||
**`task_consensus`** — Finalized consensus:
|
||||
```sql
|
||||
sign_id INTEGER
|
||||
phase INTEGER
|
||||
agreed_assessment VARCHAR
|
||||
agreed_text TEXT
|
||||
consensus_count INTEGER
|
||||
total_submissions INTEGER
|
||||
reward_per_agent FLOAT
|
||||
```
|
||||
|
||||
**`map_earnings`** — Individual earning events:
|
||||
```sql
|
||||
agent_id INTEGER REFERENCES agent_registry(id)
|
||||
amount BIGINT -- raw MAP (6 decimals)
|
||||
reason VARCHAR(100) -- 'consensus', 'dedup_vote', etc.
|
||||
sign_id INTEGER
|
||||
phase INTEGER
|
||||
payout_item_id INTEGER -- linked when batch created
|
||||
paid_at TIMESTAMPTZ -- set on confirmation
|
||||
```
|
||||
|
||||
**`payout_batches`** — Batch transaction state:
|
||||
```sql
|
||||
status VARCHAR(20) -- building/built/submitted/confirmed/failed/retried
|
||||
tx_hash VARCHAR(64)
|
||||
tx_cbor TEXT -- stored for resubmit
|
||||
confirmed_slot BIGINT
|
||||
total_map_raw BIGINT
|
||||
output_count INTEGER
|
||||
retry_count INTEGER
|
||||
```
|
||||
|
||||
### 6b. Indexes
|
||||
|
||||
```sql
|
||||
-- Spatial
|
||||
CREATE INDEX detections_geom_idx ON detections USING GIST(geom);
|
||||
|
||||
-- Earnings lookup
|
||||
CREATE INDEX idx_map_earnings_unpaid ON map_earnings(agent_id) WHERE payout_item_id IS NULL;
|
||||
|
||||
-- Agent submissions
|
||||
CREATE INDEX idx_agent_submissions_sign_phase ON agent_submissions(sign_id, phase);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Overlap Analysis: adacam-forwarder vs odc-api LandmarkProxyService
|
||||
|
||||
### 7a. What They Do
|
||||
|
||||
**odc-api LandmarkProxyService:**
|
||||
- Runs every 5 minutes on the Bee
|
||||
- Uploads up to 1000 landmarks per batch to `PUT hivemapper.com/api/plugins/auditMultiplePlugins`
|
||||
- Filters by confidence ≥ 0.6
|
||||
- Transforms some classes to `generic-sign`
|
||||
- Includes full payload with `attributes`, `azimuth`, `width/height`
|
||||
|
||||
**adacam-forwarder:**
|
||||
- Runs every 30 seconds
|
||||
- Uploads to our ADAMaps API
|
||||
- Filters by confidence ≥ 0.3
|
||||
- Doesn't transform classes
|
||||
- **Missing:** `attributes`, `azimuth`, camera position, bbox coords
|
||||
|
||||
### 7b. Conflict?
|
||||
|
||||
No direct conflict — they run in parallel. LandmarkProxyService uploads to Hivemapper (blocked by our network config), adacam-forwarder uploads to ADAMaps.
|
||||
|
||||
**But:** LandmarkProxyService is dead weight CPU consumption. Should kill it in a liberated Bee.
|
||||
|
||||
### 7c. Data Fields Comparison
|
||||
|
||||
| Field | odc-api captures | forwarder sends | ADAMaps stores |
|
||||
|-------|------------------|-----------------|----------------|
|
||||
| id | ✅ | ✅ | ✅ (in raw_json) |
|
||||
| ts | ✅ | ✅ | ✅ detected_at |
|
||||
| lat/lon | ✅ | ✅ | ✅ |
|
||||
| class_label | ✅ | ✅ | ✅ sign_type |
|
||||
| confidence | ✅ | ✅ | ✅ |
|
||||
| image_name | ✅ | ✅ (v1) | ✅ image_path |
|
||||
| x1/y1/x2/y2 | ✅ | ❌ | ❓ (bbox_* columns exist) |
|
||||
| cam_lat/lon | ✅ | ❌ | ❌ |
|
||||
| cam_heading | ✅ | ❌ | ❌ (heading column exists) |
|
||||
| attributes | ✅ | ❌ | ❌ |
|
||||
| azimuth | ✅ | ❌ | ❌ |
|
||||
| width/height | ✅ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 8. Gap Analysis
|
||||
|
||||
### 8a. Forwarder Gaps
|
||||
|
||||
| Issue | Severity | Impact |
|
||||
|-------|----------|--------|
|
||||
| Missing `attributes` JSON | 🔴 High | Speed sign values already extracted by ML — we're redoing Phase 2 manually |
|
||||
| Missing `cam_heading` | 🟡 Medium | Can't determine sign facing direction |
|
||||
| Missing `x1/y1/x2/y2` bbox | 🟡 Medium | Crop endpoint can't work without detection-level bbox |
|
||||
| No systemd service running | 🟡 Medium | Forwarder not actually deployed |
|
||||
| v2 doesn't upload images | 🟠 Medium-Low | v1 does, v2 skips — which is deployed? |
|
||||
|
||||
### 8b. API Gaps
|
||||
|
||||
| Issue | Severity | Impact |
|
||||
|-------|----------|--------|
|
||||
| Crop endpoint relies on `raw_json->bbox` | 🟡 Medium | Works only if forwarder sends bbox in raw_json |
|
||||
| No `attributes` column in detections | 🟡 Medium | Can't store speed values even if forwarder sent them |
|
||||
| Phase 2 could be partially automated | 🟡 Medium | If we had `attributes`, fewer tasks needed |
|
||||
|
||||
### 8c. Database Gaps
|
||||
|
||||
| Issue | Severity | Impact |
|
||||
|-------|----------|--------|
|
||||
| No `attributes` JSONB column on detections | 🟡 Medium | Would need migration |
|
||||
| No `cam_heading` on detections | 🟡 Medium | Would need migration |
|
||||
| Indexes on signs look OK | ✅ | |
|
||||
| No GIST index on signs.geom | 🟠 Low | Only detections has spatial index |
|
||||
|
||||
---
|
||||
|
||||
## 9. Improvement Recommendations
|
||||
|
||||
### 9a. Top 3 Highest-Impact Forwarder Improvements
|
||||
|
||||
1. **Add `attributes` JSON forwarding**
|
||||
```python
|
||||
# In SQL query:
|
||||
SELECT id, ts, lat, lon, class_label, confidence, image_name,
|
||||
cam_lat, cam_lon, cam_heading, attributes,
|
||||
x1, y1, x2, y2
|
||||
FROM landmarks WHERE ...
|
||||
```
|
||||
Then include in payload:
|
||||
```python
|
||||
'attributes': row['attributes'], # JSON as-is
|
||||
'bbox_x1': row['x1'], 'bbox_y1': row['y1'],
|
||||
'bbox_x2': row['x2'], 'bbox_y2': row['y2'],
|
||||
'cam_heading': row['cam_heading'],
|
||||
```
|
||||
**Why:** Hivemapper's model already extracts `speed_label` — this data exists, we're just not forwarding it.
|
||||
|
||||
2. **Deploy systemd service**
|
||||
```bash
|
||||
sudo cp adacam-forwarder-v2.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now adacam-forwarder-v2
|
||||
```
|
||||
With watchdog support:
|
||||
```ini
|
||||
WatchdogSec=60
|
||||
NotifyAccess=all
|
||||
```
|
||||
**Why:** Forwarder needs to actually run persistently.
|
||||
|
||||
3. **Use v1's SQLite approach with v2's robustness**
|
||||
- SQLite is lower latency than REST
|
||||
- Add v2's retry queue to v1
|
||||
- Add v2's Redis GPS fallback to v1
|
||||
**Why:** Best of both worlds — direct DB access with fault tolerance.
|
||||
|
||||
### 9b. Top 3 Highest-Impact API Improvements
|
||||
|
||||
1. **Add `attributes` column and ingest it**
|
||||
```sql
|
||||
ALTER TABLE detections ADD COLUMN attributes JSONB;
|
||||
```
|
||||
In `/api/ingest`:
|
||||
```python
|
||||
cur.execute("""
|
||||
INSERT INTO detections (..., attributes, bbox_x1, bbox_y1, bbox_x2, bbox_y2)
|
||||
VALUES (..., %s, %s, %s, %s, %s)
|
||||
""", (..., d.get('attributes'), d.get('bbox_x1'), ...))
|
||||
```
|
||||
Then in Phase 2 task feed, check if `attributes.speed_label` exists — if so, pre-populate expected answer.
|
||||
**Why:** Reduces agent task load for speed signs by ~90%.
|
||||
|
||||
2. **Auto-verify signs with high-confidence `attributes`**
|
||||
```python
|
||||
if attributes and attributes.get('speed_label_conf', 0) > 0.95:
|
||||
# Auto-complete Phase 2 for this sign
|
||||
sign_text = f"SPEED LIMIT {attributes['speed_label']}"
|
||||
```
|
||||
**Why:** ML is already doing the work — trust it for high-confidence cases.
|
||||
|
||||
3. **Add GIST index on signs**
|
||||
```sql
|
||||
ALTER TABLE signs ADD COLUMN geom GEOMETRY(Point, 4326)
|
||||
GENERATED ALWAYS AS (ST_SetSRID(ST_MakePoint(lon, lat), 4326)) STORED;
|
||||
CREATE INDEX signs_geom_idx ON signs USING GIST(geom);
|
||||
```
|
||||
**Why:** Spatial queries on signs table are slow without it.
|
||||
|
||||
### 9c. Strategic Questions
|
||||
|
||||
**Should we use odc-api REST endpoints or keep reading SQLite directly?**
|
||||
|
||||
**Recommendation: Keep SQLite.**
|
||||
- Lower latency (no HTTP overhead)
|
||||
- No missing detections if odc-api service crashes
|
||||
- No class filtering we don't want
|
||||
- Direct access to all columns
|
||||
|
||||
**Should we adopt the `attributes` JSON field?**
|
||||
|
||||
**Recommendation: Yes, immediately.**
|
||||
This is the single highest-impact change. The data exists, the ML model extracts it, we're just not using it.
|
||||
|
||||
**Should the forwarder send bbox crops instead of full frames?**
|
||||
|
||||
**Recommendation: No — keep full frames.**
|
||||
- odc-api's `/landmarks/:id/chips/:chip_id` endpoint uses Jimp on-device
|
||||
- Adds HTTP overhead per detection
|
||||
- We can crop server-side (already doing it)
|
||||
- Full frames allow re-cropping if bbox was wrong
|
||||
|
||||
**Systemd service design:**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AdaCam Forwarder
|
||||
After=network-online.target odc-api.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/adacam
|
||||
ExecStart=/usr/bin/python3 /opt/adacam/services/adacam-forwarder.py
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
WatchdogSec=120
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/data/adacam
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommended Next Steps
|
||||
|
||||
### Immediate (This Week)
|
||||
|
||||
1. **Update forwarder SQL query** to include `cam_heading`, `attributes`, and bbox coords
|
||||
2. **Add `attributes` JSONB column** to `detections` table
|
||||
3. **Deploy forwarder systemd service** on the Bee
|
||||
4. **Kill LandmarkProxyService** (blocked anyway, but wasting CPU)
|
||||
|
||||
### Short-Term (This Month)
|
||||
|
||||
5. **Auto-verify speed signs** when `attributes.speed_label_conf > 0.95`
|
||||
6. **Backfill existing signs** with speed values from `attributes`
|
||||
7. **Add signs GIST index** for faster spatial queries
|
||||
|
||||
### Medium-Term
|
||||
|
||||
8. **Consider sign heading** from `cam_heading` + `azimuth` to determine which direction the sign faces
|
||||
9. **Phase 2 task prioritization** — skip signs that already have high-confidence `attributes`
|
||||
10. **Monitoring dashboard** for forwarder health (last sync time, queue depth, error rate)
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File Hashes
|
||||
|
||||
Key files for reference:
|
||||
|
||||
| File | Lines | Last Modified |
|
||||
|------|-------|---------------|
|
||||
| `adacam/services/capture/adacam-forwarder.py` | ~280 | 2026-03-xx |
|
||||
| `adacam/services/capture/adacam-forwarder-v2.py` | ~230 | 2026-03-xx |
|
||||
| `adamaps/api/app.py` | ~1800 | 2026-03-22 |
|
||||
| `adamaps/payout.py` | ~500 | 2026-03-xx |
|
||||
|
||||
## Appendix B: odc-api Landmarks Schema (from deep dive)
|
||||
|
||||
Full schema of Hivemapper's `landmarks` table in odc-api.db:
|
||||
|
||||
```sql
|
||||
CREATE TABLE landmarks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts INTEGER NOT NULL,
|
||||
image_name TEXT,
|
||||
class_label TEXT,
|
||||
class_label_confidence REAL,
|
||||
overall_confidence REAL,
|
||||
x1 INTEGER, y1 INTEGER, x2 INTEGER, y2 INTEGER, -- bbox
|
||||
lat REAL, lon REAL, alt REAL, -- sign position
|
||||
cam_lat REAL, cam_lon REAL, cam_heading REAL, -- camera position
|
||||
azimuth REAL,
|
||||
width REAL, height REAL,
|
||||
negative_class INTEGER DEFAULT 0,
|
||||
confidence REAL,
|
||||
pos_confidence REAL,
|
||||
attributes TEXT, -- JSON: {"speed_label": 25, "speed_label_conf": 0.99}
|
||||
track_id TEXT,
|
||||
map_feature_id INTEGER,
|
||||
model_id TEXT
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*End of audit.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue