Compare commits

...

3 commits

Author SHA1 Message Date
9ba41b1465 audit follow-ups: deps floor, LICENSE, gate /debug/redis-keys
- requirements.txt: bump floors past known CVEs (flask>=2.3.2 fixes
  CVE-2023-30861, requests>=2.32.0 fixes CVE-2023-32681 + CVE-2024-35195,
  redis>=5.0 fixes CVE-2023-28858/9).
- LICENSE: add MIT text (README claimed MIT but the file was missing).
- /api/1/debug/redis-keys: require auth. Was unauthenticated info-disclosure
  on the LAN/AP side.
2026-05-27 09:22:12 -07:00
67da0ad8e8 Rotate AdaMaps ingest+read keys (env-required, no inline default)
Previous values (adamaps-ingest-2026, adamaps-read-2026, mapnet-ingest-2026)
were inline defaults across adamaps + adacam-api + varroa. The ingest key
was briefly anon-visible during the 2026-05-27 Forgejo public-flip when
adacam-api + varroa were public for a short window before the leak was
spotted.

New values live in Vaultwarden:
  - AdaMaps — API_KEY (ingest)
  - AdaMaps — READ_KEY

Validators now hard-fail at boot if the env var is missing. Service is
on hold today; when it resumes, both env vars must be set.
2026-05-27 09:17:22 -07:00
9e639ead17 fix: GPS from SQLite framekms (confirmed live device schema)
odc-api.db confirmed present on device. framekms table has:
  latitude, longitude, altitude, hdop, satellites_used, time
NOT lat_deg/lon_deg/alt_m/num_satellites as previously assumed.
Redis fallback retained, supports both field naming conventions.
API response format unchanged (still returns lat_deg/lon_deg for Varroa compat).
2026-03-14 20:51:26 -07:00
6 changed files with 101 additions and 32 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sulkta Coop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -41,7 +41,7 @@ Config file: `/data/adacam/config.json`
```json
{
"device_id": "auto-generated UUID",
"adamaps_key": "***REMOVED***",
"adamaps_key": "<your-adamaps-ingest-key>",
"adamaps_api": "https://api.adamaps.org",
"ap_interface": "wlp1s0f0",
"tunnel_host": "",

View file

@ -112,6 +112,7 @@ def create_app():
# ── DEBUG ENDPOINTS ────────────────────────────────────────────────────
@app.route('/api/1/debug/redis-keys')
@require_auth
def redis_keys():
"""Debug endpoint — list Redis keys for GPS/IMU troubleshooting."""
try:

View file

@ -8,7 +8,7 @@ FIRMWARE_VERSION = "adacam-1.0.0"
_defaults = {
"device_id": None,
"adamaps_key": "***REMOVED***",
"adamaps_key": "",
"adamaps_api": "https://api.adamaps.org",
"ap_interface": "wlp1s0f0",
"tunnel_host": "",

View file

@ -1,40 +1,87 @@
"""Redis client for GPS and IMU data."""
"""GPS data reader for adacam-api.
Primary source: /data/recording/odc-api.db framekms table (confirmed schema from live device).
Fallback: Redis (keys may appear when device has outdoor GPS fix post-liberation).
Confirmed field names from live device:
latitude, longitude, altitude, hdop, satellites_used, speed, heading, time
"""
import json
import os
import sqlite3
import time
ODC_DB = '/data/recording/odc-api.db'
GNSS_REDIS_KEYS = ['GNSSFusion30Hz', 'GnssData', 'GnssPvt', 'GnssPosition']
_redis_client = None
def _get_redis():
global _redis_client
try:
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
if _redis_client is None:
_redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
_redis_client.ping()
return _redis_client
except Exception:
_redis_client = None
return None
def get_latest_gnss():
"""Get latest GPS fix from GNSSFusion30Hz ZSET."""
client = get_client()
"""Get latest GPS fix. Returns dict with keys: lat_deg, lon_deg, alt_m, hdop, num_satellites.
Field names kept compatible with original API response format."""
# Primary: SQLite framekms (confirmed live device schema)
if os.path.exists(ODC_DB):
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])
conn = sqlite3.connect(ODC_DB)
row = conn.execute(
'SELECT latitude, longitude, altitude, hdop, satellites_used, time '
'FROM framekms WHERE latitude IS NOT NULL AND latitude != 0 '
'ORDER BY time DESC LIMIT 1'
).fetchone()
conn.close()
if row:
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),
'lat_deg': row[0],
'lon_deg': row[1],
'alt_m': row[2],
'hdop': row[3],
'num_satellites': row[4],
'unix_milliseconds': int(row[5]) if row[5] else int(time.time() * 1000),
}
except (redis.RedisError, json.JSONDecodeError):
except Exception:
pass
# Fallback: Redis (may appear post-liberation with outdoor GPS fix)
r = _get_redis()
if r:
for key in GNSS_REDIS_KEYS:
try:
for getter in [lambda k: r.zrevrange(k, 0, 0), lambda k: [r.get(k)]]:
items = getter(key)
item = items[0] if items else None
if item:
data = json.loads(item)
lat = data.get('latitude') or data.get('lat_deg')
lon = data.get('longitude') or data.get('lon_deg')
if lat and lon:
return {
'lat_deg': lat,
'lon_deg': lon,
'alt_m': data.get('altitude') or data.get('alt_m', 0),
'hdop': data.get('hdop', 99),
'num_satellites': data.get('satellites_used') or data.get('num_satellites', 0),
'unix_milliseconds': int(data.get('unix_milliseconds', time.time() * 1000)),
}
except Exception:
continue
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
return gnss is not None and gnss.get('num_satellites', 0) >= 4

View file

@ -1,3 +1,3 @@
flask>=2.0
redis>=4.0
requests>=2.25
flask>=2.3.2
redis>=5.0
requests>=2.32.0