offline track

This commit is contained in:
2026-06-19 11:09:20 +03:00
parent 4891933879
commit 8812cf9b40
23 changed files with 924 additions and 57 deletions
+2 -1
View File
@@ -22,7 +22,7 @@ python flask_app.py
| `LORATESTER_PORT` | `7634` |
| `LORATESTER_DB` | `./loratester.db` |
| `LORATESTER_TELEMETRY_LIMIT` | `5000` (записей истории на устройство) |
| `LORATESTER_TRACK_POINTS_LIMIT` | `10000` (точек на один трек) |
| `LORATESTER_TRACK_POINTS_LIMIT` | `500000` (точек на один трек) |
| `LORATESTER_ELEVATION_URL` | `http://192.168.1.109:8085/v1/elevation` |
| `LORATESTER_ELEVATION_PROBE_TTL` | `60` (сек, кэш проверки доступности) |
| `LORATESTER_ELEVATION_TIMEOUT` | `8` (сек, таймаут HTTP к сервису высот) |
@@ -99,6 +99,7 @@ curl http://127.0.0.1:7634/api/health
### Треки (запись с Android)
- `POST /api/tracks/sync``{device_id, track_id?, started_at?, points[], finish?}` — офлайн-догрузка точек и завершение трека
- `POST /api/tracks/start``{device_id}``{track_id}`
- `POST /api/tracks/{id}/points``{points: [{ts, lat, lon, altitude_gps?, rssi?, role?, meta?}]}`
- `POST /api/tracks/{id}/finish`
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -8,7 +8,7 @@ DATABASE_PATH = os.environ.get(
HOST = os.environ.get("LORATESTER_HOST", "0.0.0.0")
PORT = int(os.environ.get("LORATESTER_PORT", "7634"))
TELEMETRY_LIMIT = int(os.environ.get("LORATESTER_TELEMETRY_LIMIT", "5000"))
TRACK_POINTS_LIMIT = int(os.environ.get("LORATESTER_TRACK_POINTS_LIMIT", "10000"))
TRACK_POINTS_LIMIT = int(os.environ.get("LORATESTER_TRACK_POINTS_LIMIT", "500000"))
ELEVATION_OPENTOPO_URL = os.environ.get(
"LORATESTER_ELEVATION_OPENTOPO_URL",
"http://grigowashere.ru:5300/v1/srtm30",
+73
View File
@@ -353,6 +353,79 @@ def finish_track(track_id: int) -> dict[str, Any]:
return {"ok": True, "track_id": track_id, "ended_at": ts, "point_count": count}
def sync_track(
device_id: str,
points: list[dict[str, Any]],
track_id: Optional[int] = None,
started_at: Optional[float] = None,
finish: bool = False,
label: Optional[str] = None,
) -> dict[str, Any]:
"""Upload buffered points after offline recording; optionally create track and finish."""
if not is_valid_device_id(device_id):
raise ValueError(f"invalid device_id '{device_id}'")
points = points or []
if track_id is not None:
with _db() as conn:
track = conn.execute(
"SELECT id, device_id, ended_at FROM tracks WHERE id = ?",
(track_id,),
).fetchone()
if not track:
raise ValueError(f"track {track_id} not found")
if track["device_id"] != device_id:
raise ValueError("device_id does not match track owner")
if track["ended_at"] is not None:
raise ValueError(f"track {track_id} already finished")
else:
if not points and not finish:
raise ValueError("points required when creating a new track")
ts = float(started_at) if started_at is not None else time.time()
with _db() as conn:
cur = conn.execute(
"""
INSERT INTO tracks (device_id, started_at, label)
VALUES (?, ?, ?)
""",
(device_id, ts, label),
)
track_id = int(cur.lastrowid)
added = 0
batch_size = 100
for i in range(0, len(points), batch_size):
chunk = points[i : i + batch_size]
if not chunk:
continue
result = add_track_points(track_id, chunk)
added += int(result.get("added") or 0)
finished = False
ended_at = None
point_count = added
if finish:
fin = finish_track(track_id)
finished = True
ended_at = fin.get("ended_at")
point_count = int(fin.get("point_count") or 0)
else:
with _db() as conn:
point_count = conn.execute(
"SELECT COUNT(*) FROM track_points WHERE track_id = ?",
(track_id,),
).fetchone()[0]
return {
"ok": True,
"track_id": track_id,
"added": added,
"point_count": point_count,
"finished": finished,
"ended_at": ended_at,
}
def list_tracks(device_id: Optional[str] = None, limit: int = 50) -> list[dict[str, Any]]:
limit = min(max(1, limit), 200)
with _db() as conn:
+1 -1
View File
@@ -16,7 +16,7 @@ services:
LORATESTER_ELEVATION_PROBE_TTL: ${LORATESTER_ELEVATION_PROBE_TTL:-60}
LORATESTER_ELEVATION_TIMEOUT: ${LORATESTER_ELEVATION_TIMEOUT:-8}
LORATESTER_TELEMETRY_LIMIT: ${LORATESTER_TELEMETRY_LIMIT:-5000}
LORATESTER_TRACK_POINTS_LIMIT: ${LORATESTER_TRACK_POINTS_LIMIT:-10000}
LORATESTER_TRACK_POINTS_LIMIT: ${LORATESTER_TRACK_POINTS_LIMIT:-500000}
volumes:
loratester-data:
+30 -1
View File
@@ -72,6 +72,15 @@ class TrackPointsBody(BaseModel):
points: list[TrackPoint] = Field(default_factory=list)
class TrackSyncBody(BaseModel):
device_id: str
track_id: Optional[int] = None
started_at: Optional[float] = None
points: list[TrackPoint] = Field(default_factory=list)
finish: bool = False
label: Optional[str] = None
class CommandBody(BaseModel):
from_device_id: str
to_device_id: str
@@ -193,6 +202,26 @@ def tracks_finish(
raise HTTPException(400, detail=str(e)) from e
@app.post("/api/tracks/sync")
def tracks_sync(
body: TrackSyncBody,
x_lora_client: Optional[str] = Header(None, alias=ANDROID_CLIENT_HEADER),
):
_require_android(x_lora_client)
try:
points = [p.model_dump(exclude_none=True) for p in body.points]
return storage.sync_track(
body.device_id,
points,
track_id=body.track_id,
started_at=body.started_at,
finish=body.finish,
label=body.label,
)
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.get("/api/tracks")
def tracks_list(
device_id: Optional[str] = None,
@@ -379,7 +408,7 @@ def health():
return {
"ok": status["db_ok"],
"ts": time.time(),
"api_build": "2026-06-16i",
"api_build": "2026-06-19a",
**status,
**elevation_status(),
}
+44
View File
@@ -45,6 +45,50 @@ def test_old_telemetry_without_meta_gets_migrated(temp_db):
conn.close()
def test_sync_track_offline_upload(temp_db, monkeypatch):
storage.init_db()
monkeypatch.setattr(storage, "fetch_elevation_m", lambda lat, lon: 100.0)
start = storage.start_track("android-12345678")
tid = start["track_id"]
result = storage.sync_track(
"android-12345678",
[
{"ts": 1.0, "lat": 55.75, "lon": 37.62, "role": "TX"},
{"ts": 2.0, "lat": 55.751, "lon": 37.621, "role": "TX"},
],
track_id=tid,
finish=True,
)
assert result["added"] == 2
assert result["finished"] is True
assert result["point_count"] == 2
track = storage.get_track(tid)
assert len(track["points"]) == 2
assert track["ended_at"] is not None
def test_sync_track_create_offline(temp_db, monkeypatch):
storage.init_db()
monkeypatch.setattr(storage, "fetch_elevation_m", lambda lat, lon: 50.0)
result = storage.sync_track(
"android-abcdef01",
[
{"ts": 10.0, "lat": 59.93, "lon": 30.33, "role": "RX"},
],
track_id=None,
started_at=10.0,
finish=True,
)
assert result["track_id"] > 0
assert result["point_count"] == 1
track = storage.get_track(result["track_id"])
assert track["started_at"] == 10.0
def test_tracks_crud(temp_db):
storage.init_db()
start = storage.start_track("android-12345678")