Compare commits
3 commits
pre-libera
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| beead1d6b0 | |||
| 81a6d18c1d | |||
| 595c1cadf5 |
6 changed files with 101 additions and 32 deletions
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||
|
|
@ -41,7 +41,7 @@ Config file: `/data/adacam/config.json`
|
|||
```json
|
||||
{
|
||||
"device_id": "auto-generated UUID",
|
||||
"adamaps_key": "adamaps-ingest-2026",
|
||||
"adamaps_key": "<your-adamaps-ingest-key>",
|
||||
"adamaps_api": "https://api.adamaps.org",
|
||||
"ap_interface": "wlp1s0f0",
|
||||
"tunnel_host": "",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ FIRMWARE_VERSION = "adacam-1.0.0"
|
|||
|
||||
_defaults = {
|
||||
"device_id": None,
|
||||
"adamaps_key": "adamaps-ingest-2026",
|
||||
"adamaps_key": "",
|
||||
"adamaps_api": "https://api.adamaps.org",
|
||||
"ap_interface": "wlp1s0f0",
|
||||
"tunnel_host": "",
|
||||
|
|
|
|||
|
|
@ -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 redis
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
_client = None
|
||||
ODC_DB = '/data/recording/odc-api.db'
|
||||
GNSS_REDIS_KEYS = ['GNSSFusion30Hz', 'GnssData', 'GnssPvt', 'GnssPosition']
|
||||
|
||||
_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_redis():
|
||||
global _redis_client
|
||||
try:
|
||||
import redis
|
||||
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()
|
||||
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
|
||||
"""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:
|
||||
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': 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 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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
flask>=2.0
|
||||
redis>=4.0
|
||||
requests>=2.25
|
||||
flask>=2.3.2
|
||||
redis>=5.0
|
||||
requests>=2.32.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue