Initial commit: adacam-api v1.0.0

Clean Python Flask replacement for odc-api (434k lines Node.js → ~350 lines Python)
- GET /api/1/landmarks/last/{N} - last N detections from SQLite
- POST /api/1/landmarks - ingest detections + forward to AdaMaps
- GET /api/1/gnssConcise/latestValid - GPS fix from Redis
- GET /api/1/status - device status
- GET /api/1/deviceinfo - device identity
- GET /api/1/recording/frames/latest - latest frame path

No /api/1/cmd - that's the CVE, it's gone.

Includes:
- SQLite for local storage + offline queue
- Background thread for AdaMaps retry
- systemd service unit
- install.sh for device deployment
This commit is contained in:
Sulkta 2026-03-14 08:13:04 -07:00
parent 5dd91b0b15
commit 7acc5ab088
17 changed files with 507 additions and 1 deletions

View file

@ -0,0 +1 @@
"""API route blueprints."""

View file

@ -0,0 +1,22 @@
"""Recording frame endpoints."""
import os
import glob
from flask import Blueprint, jsonify
bp = Blueprint("frames", __name__, url_prefix="/api/1/recording")
FRAMES_DIR = "/tmp/recording/pics"
@bp.route("/frames/latest", methods=["GET"])
def latest_frame():
"""Get path to most recent frame file."""
if not os.path.isdir(FRAMES_DIR):
return jsonify({"error": "No frames directory"}), 404
files = glob.glob(os.path.join(FRAMES_DIR, "*"))
if not files:
return jsonify({"error": "No frames available"}), 404
latest = max(files, key=os.path.getmtime)
return jsonify({"path": latest})

14
adacam_api/routes/gnss.py Normal file
View file

@ -0,0 +1,14 @@
"""GNSS/GPS endpoints."""
from flask import Blueprint, jsonify
from .. import redis_client
bp = Blueprint("gnss", __name__, url_prefix="/api/1/gnssConcise")
@bp.route("/latestValid", methods=["GET"])
def latest_valid():
"""Get current GPS fix from Redis."""
gnss = redis_client.get_latest_gnss()
if gnss:
return jsonify(gnss)
return jsonify({"error": "No valid GPS fix"}), 503

View file

@ -0,0 +1,26 @@
"""Landmark detection endpoints."""
from flask import Blueprint, jsonify, request
from .. import db, forwarder
bp = Blueprint("landmarks", __name__, url_prefix="/api/1/landmarks")
@bp.route("/last/<int:n>", methods=["GET"])
def get_last(n):
"""Get last N detections."""
n = min(n, 1000) # Cap at 1000
landmarks = db.get_last_landmarks(n)
return jsonify(landmarks)
@bp.route("", methods=["POST"])
def ingest():
"""Ingest a new detection from camera pipeline."""
data = request.get_json()
if not data or "class_label" not in data:
return jsonify({"error": "Missing class_label"}), 400
db.insert_landmark(data)
forwarder.forward_detection(data)
return jsonify({"status": "ok"})

View file

@ -0,0 +1,28 @@
"""Device status and info endpoints."""
import time
from flask import Blueprint, jsonify
from .. import config, redis_client
bp = Blueprint("status", __name__, url_prefix="/api/1")
_start_time = time.time()
@bp.route("/status", methods=["GET"])
def status():
"""Device status: firmware, uptime, GPS lock, camera status."""
return jsonify({
"firmware_version": config.FIRMWARE_VERSION,
"uptime_seconds": int(time.time() - _start_time),
"gps_lock": redis_client.has_gps_lock(),
"camera_status": "active", # TODO: check camera pipeline
})
@bp.route("/deviceinfo", methods=["GET"])
def deviceinfo():
"""Device identity."""
return jsonify({
"device_id": config.get("device_id"),
"firmware_version": config.FIRMWARE_VERSION,
})