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:
parent
b05c0e3d03
commit
37aefb84c8
17 changed files with 507 additions and 1 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
60
README.md
60
README.md
|
|
@ -1,3 +1,61 @@
|
||||||
# adacam-api
|
# adacam-api
|
||||||
|
|
||||||
Clean Python Flask replacement for odc-api (Hivemapper Bee liberation)
|
Clean Python Flask replacement for Hivemapper's `odc-api` — a 434k-line Node.js monolith with a filed CVE. This service runs on the **Hivemapper Bee (HDC-S)** dashcam as part of the **adacam** liberation stack.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Serves API endpoints for the Varroa Android app and adamaps-forwarder
|
||||||
|
- Reads GPS data from Redis (`GNSSFusion30Hz`)
|
||||||
|
- Stores landmark detections in SQLite
|
||||||
|
- Forwards detections to AdaMaps API (with offline queuing)
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/api/1/landmarks/last/{N}` | Last N detections |
|
||||||
|
| POST | `/api/1/landmarks` | Ingest new detection |
|
||||||
|
| GET | `/api/1/gnssConcise/latestValid` | Current GPS fix |
|
||||||
|
| GET | `/api/1/status` | Device status |
|
||||||
|
| GET | `/api/1/deviceinfo` | Device identity |
|
||||||
|
| GET | `/api/1/recording/frames/latest` | Latest frame path |
|
||||||
|
|
||||||
|
**Note:** `/api/1/cmd` is intentionally NOT implemented — that was the CVE.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Copy files to `/opt/adacam/`
|
||||||
|
2. Install Python dependencies
|
||||||
|
3. Enable and start the systemd service
|
||||||
|
4. Generate a device ID on first run
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Config file: `/data/adacam/config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "auto-generated UUID",
|
||||||
|
"adamaps_key": "adamaps-ingest-2026",
|
||||||
|
"adamaps_api": "https://api.adamaps.org",
|
||||||
|
"ap_interface": "wlp1s0f0",
|
||||||
|
"tunnel_host": "",
|
||||||
|
"tunnel_user": "",
|
||||||
|
"tunnel_port": 2222
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Redis (for GPS/IMU data)
|
||||||
|
- Flask, redis-py, requests
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
|
||||||
2
adacam_api/__init__.py
Normal file
2
adacam_api/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
"""adacam-api: Clean replacement for odc-api."""
|
||||||
|
__version__ = "1.0.0"
|
||||||
24
adacam_api/app.py
Normal file
24
adacam_api/app.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Flask app factory."""
|
||||||
|
from flask import Flask
|
||||||
|
from . import config, db, forwarder
|
||||||
|
from .routes import landmarks, gnss, status, frames
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
"""Create and configure the Flask app."""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Load config and init DB
|
||||||
|
config.load()
|
||||||
|
db.init()
|
||||||
|
|
||||||
|
# Register blueprints
|
||||||
|
app.register_blueprint(landmarks.bp)
|
||||||
|
app.register_blueprint(gnss.bp)
|
||||||
|
app.register_blueprint(status.bp)
|
||||||
|
app.register_blueprint(frames.bp)
|
||||||
|
|
||||||
|
# Start background forwarder thread
|
||||||
|
forwarder.start_retry_thread()
|
||||||
|
|
||||||
|
return app
|
||||||
52
adacam_api/config.py
Normal file
52
adacam_api/config.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Config management for adacam-api."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
CONFIG_PATH = "/data/adacam/config.json"
|
||||||
|
FIRMWARE_VERSION = "adacam-1.0.0"
|
||||||
|
|
||||||
|
_defaults = {
|
||||||
|
"device_id": None,
|
||||||
|
"adamaps_key": "adamaps-ingest-2026",
|
||||||
|
"adamaps_api": "https://api.adamaps.org",
|
||||||
|
"ap_interface": "wlp1s0f0",
|
||||||
|
"tunnel_host": "",
|
||||||
|
"tunnel_user": "",
|
||||||
|
"tunnel_port": 2222,
|
||||||
|
}
|
||||||
|
|
||||||
|
_config = None
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
"""Load config from disk, creating defaults if needed."""
|
||||||
|
global _config
|
||||||
|
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
if os.path.exists(CONFIG_PATH):
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
_config = {**_defaults, **json.load(f)}
|
||||||
|
else:
|
||||||
|
_config = _defaults.copy()
|
||||||
|
|
||||||
|
# Generate device_id on first run
|
||||||
|
if not _config.get("device_id"):
|
||||||
|
_config["device_id"] = str(uuid.uuid4())
|
||||||
|
save()
|
||||||
|
|
||||||
|
return _config
|
||||||
|
|
||||||
|
|
||||||
|
def save():
|
||||||
|
"""Persist config to disk."""
|
||||||
|
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||||
|
with open(CONFIG_PATH, "w") as f:
|
||||||
|
json.dump(_config, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def get(key, default=None):
|
||||||
|
"""Get a config value."""
|
||||||
|
if _config is None:
|
||||||
|
load()
|
||||||
|
return _config.get(key, default)
|
||||||
88
adacam_api/db.py
Normal file
88
adacam_api/db.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""SQLite database for landmarks and offline queue."""
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
|
||||||
|
DB_PATH = "/data/adacam/adacam.db"
|
||||||
|
_local = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
"""Get thread-local database connection."""
|
||||||
|
if not hasattr(_local, "conn"):
|
||||||
|
_local.conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
_local.conn.row_factory = sqlite3.Row
|
||||||
|
return _local.conn
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
"""Initialize database schema."""
|
||||||
|
conn = get_conn()
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS landmarks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
class_label TEXT,
|
||||||
|
class_label_confidence REAL,
|
||||||
|
overall_confidence REAL,
|
||||||
|
lat REAL, lon REAL, alt REAL,
|
||||||
|
azimuth REAL,
|
||||||
|
width INTEGER, height INTEGER,
|
||||||
|
ts INTEGER,
|
||||||
|
forwarded INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS forward_queue (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
payload TEXT,
|
||||||
|
created_at INTEGER DEFAULT (strftime('%s','now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_landmarks_ts ON landmarks(ts DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_landmarks_forwarded ON landmarks(forwarded);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def insert_landmark(data):
|
||||||
|
"""Insert a landmark detection."""
|
||||||
|
conn = get_conn()
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO landmarks (id, class_label, class_label_confidence, overall_confidence,
|
||||||
|
lat, lon, alt, azimuth, width, height, ts)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (data.get("id"), data["class_label"], data.get("class_label_confidence"),
|
||||||
|
data.get("overall_confidence"), data.get("lat"), data.get("lon"),
|
||||||
|
data.get("alt"), data.get("azimuth"), data.get("width"),
|
||||||
|
data.get("height"), data.get("ts")))
|
||||||
|
conn.commit()
|
||||||
|
return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_landmarks(n):
|
||||||
|
"""Get last N landmarks."""
|
||||||
|
conn = get_conn()
|
||||||
|
rows = conn.execute("SELECT * FROM landmarks ORDER BY ts DESC LIMIT ?", (n,)).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def queue_forward(payload):
|
||||||
|
"""Queue a payload for later forwarding."""
|
||||||
|
conn = get_conn()
|
||||||
|
conn.execute("INSERT INTO forward_queue (payload) VALUES (?)", (json.dumps(payload),))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def pop_queue(limit=50):
|
||||||
|
"""Pop items from forward queue."""
|
||||||
|
conn = get_conn()
|
||||||
|
rows = conn.execute("SELECT id, payload FROM forward_queue ORDER BY id LIMIT ?", (limit,)).fetchall()
|
||||||
|
if rows:
|
||||||
|
ids = [r["id"] for r in rows]
|
||||||
|
conn.execute(f"DELETE FROM forward_queue WHERE id IN ({','.join('?'*len(ids))})", ids)
|
||||||
|
conn.commit()
|
||||||
|
return [(r["id"], json.loads(r["payload"])) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def mark_forwarded(landmark_id):
|
||||||
|
"""Mark a landmark as forwarded."""
|
||||||
|
conn = get_conn()
|
||||||
|
conn.execute("UPDATE landmarks SET forwarded = 1 WHERE id = ?", (landmark_id,))
|
||||||
|
conn.commit()
|
||||||
83
adacam_api/forwarder.py
Normal file
83
adacam_api/forwarder.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""AdaMaps forwarding with offline queue."""
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from . import config, db
|
||||||
|
|
||||||
|
_thread = None
|
||||||
|
_running = False
|
||||||
|
|
||||||
|
|
||||||
|
def forward_detection(data):
|
||||||
|
"""Forward a detection to AdaMaps, queue if offline."""
|
||||||
|
api_url = config.get("adamaps_api", "https://api.adamaps.org")
|
||||||
|
api_key = config.get("adamaps_key", "")
|
||||||
|
device_id = config.get("device_id", "unknown")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"device_id": device_id,
|
||||||
|
"detection": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{api_url}/api/ingest",
|
||||||
|
json=payload,
|
||||||
|
headers={"X-AdaMaps-Key": api_key},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return True
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Queue for retry
|
||||||
|
db.queue_forward(payload)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_loop():
|
||||||
|
"""Background thread to retry queued forwards."""
|
||||||
|
global _running
|
||||||
|
while _running:
|
||||||
|
try:
|
||||||
|
items = db.pop_queue(limit=20)
|
||||||
|
if not items:
|
||||||
|
time.sleep(30)
|
||||||
|
continue
|
||||||
|
|
||||||
|
api_url = config.get("adamaps_api", "https://api.adamaps.org")
|
||||||
|
api_key = config.get("adamaps_key", "")
|
||||||
|
|
||||||
|
for _, payload in items:
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{api_url}/api/ingest",
|
||||||
|
json=payload,
|
||||||
|
headers={"X-AdaMaps-Key": api_key},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
db.queue_forward(payload)
|
||||||
|
except requests.RequestException:
|
||||||
|
db.queue_forward(payload)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception:
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
|
def start_retry_thread():
|
||||||
|
"""Start the background retry thread."""
|
||||||
|
global _thread, _running
|
||||||
|
if _thread is None or not _thread.is_alive():
|
||||||
|
_running = True
|
||||||
|
_thread = threading.Thread(target=_retry_loop, daemon=True)
|
||||||
|
_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_retry_thread():
|
||||||
|
"""Stop the background retry thread."""
|
||||||
|
global _running
|
||||||
|
_running = False
|
||||||
40
adacam_api/redis_client.py
Normal file
40
adacam_api/redis_client.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""Redis client for GPS and IMU data."""
|
||||||
|
import json
|
||||||
|
import redis
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
"""Get Redis connection."""
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_gnss():
|
||||||
|
"""Get latest GPS fix from GNSSFusion30Hz ZSET."""
|
||||||
|
client = get_client()
|
||||||
|
try:
|
||||||
|
# Get the most recent entry (highest score = most recent timestamp)
|
||||||
|
results = client.zrevrange("GNSSFusion30Hz", 0, 0, withscores=True)
|
||||||
|
if results:
|
||||||
|
data = json.loads(results[0][0])
|
||||||
|
return {
|
||||||
|
"lat_deg": data.get("lat_deg"),
|
||||||
|
"lon_deg": data.get("lon_deg"),
|
||||||
|
"alt_m": data.get("alt_m"),
|
||||||
|
"unix_milliseconds": int(data.get("unix_milliseconds", 0)),
|
||||||
|
"hdop": data.get("hdop"),
|
||||||
|
"num_satellites": data.get("num_satellites", 0),
|
||||||
|
}
|
||||||
|
except (redis.RedisError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_gps_lock():
|
||||||
|
"""Check if we have a valid GPS lock."""
|
||||||
|
gnss = get_latest_gnss()
|
||||||
|
return gnss is not None and gnss.get("num_satellites", 0) >= 4
|
||||||
1
adacam_api/routes/__init__.py
Normal file
1
adacam_api/routes/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""API route blueprints."""
|
||||||
22
adacam_api/routes/frames.py
Normal file
22
adacam_api/routes/frames.py
Normal 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
14
adacam_api/routes/gnss.py
Normal 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
|
||||||
26
adacam_api/routes/landmarks.py
Normal file
26
adacam_api/routes/landmarks.py
Normal 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"})
|
||||||
28
adacam_api/routes/status.py
Normal file
28
adacam_api/routes/status.py
Normal 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,
|
||||||
|
})
|
||||||
34
install.sh
Executable file
34
install.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# adacam-api installer for Hivemapper Bee device
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/adacam"
|
||||||
|
DATA_DIR="/data/adacam"
|
||||||
|
|
||||||
|
echo "[*] Installing adacam-api..."
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
mkdir -p "$INSTALL_DIR" "$DATA_DIR"
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
cp -r adacam_api main.py requirements.txt "$INSTALL_DIR/"
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
pip3 install --no-cache-dir -r "$INSTALL_DIR/requirements.txt"
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
cp systemd/adacam-api.service /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable adacam-api
|
||||||
|
|
||||||
|
# Generate device ID if not present
|
||||||
|
if [ ! -f "$DATA_DIR/config.json" ]; then
|
||||||
|
python3 -c "from adacam_api import config; config.load()"
|
||||||
|
echo "[*] Generated new device ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Starting adacam-api..."
|
||||||
|
systemctl restart adacam-api
|
||||||
|
systemctl status adacam-api --no-pager
|
||||||
|
|
||||||
|
echo "[+] Installation complete. API running on port 5000"
|
||||||
8
main.py
Executable file
8
main.py
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""adacam-api entry point."""
|
||||||
|
from adacam_api.app import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
flask>=2.0
|
||||||
|
redis>=4.0
|
||||||
|
requests>=2.25
|
||||||
15
systemd/adacam-api.service
Normal file
15
systemd/adacam-api.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=AdaCam API Service
|
||||||
|
After=network.target redis.service
|
||||||
|
Wants=redis.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /opt/adacam/main.py
|
||||||
|
WorkingDirectory=/opt/adacam
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Add table
Add a link
Reference in a new issue