Initial commit: LoraTester Android + server

This commit is contained in:
2026-06-04 14:39:14 +03:00
parent 253a7d74ca
commit 81eaa95df3
26 changed files with 1898 additions and 106 deletions
+104
View File
@@ -66,6 +66,29 @@ class TrackPointsBody(BaseModel):
points: list[TrackPoint] = Field(default_factory=list)
class CommandBody(BaseModel):
from_device_id: str
to_device_id: str
kind: str
payload: Optional[dict[str, Any]] = None
class PairedTrackStartBody(BaseModel):
device_ids: Optional[list[str]] = None
initiator: Optional[str] = None
device_id: Optional[str] = None
class PairedTrackAckBody(BaseModel):
session_id: int
device_id: str
track_id: int
class PairedTrackCancelBody(BaseModel):
session_id: Optional[int] = None
@app.get("/")
def index():
return FileResponse(
@@ -194,6 +217,87 @@ def get_chat(since: float = 0, limit: int = Query(200, ge=1, le=500)):
return storage.get_chat(since, limit)
@app.post("/api/commands")
def post_command(body: CommandBody):
try:
return storage.enqueue_command(
body.from_device_id,
body.to_device_id,
body.kind,
body.payload,
)
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.get("/api/commands/pending")
def commands_pending(
device_id: str = Query(...),
limit: int = Query(20, ge=1, le=50),
x_lora_client: Optional[str] = Header(None, alias=ANDROID_CLIENT_HEADER),
):
_require_android(x_lora_client)
try:
return storage.poll_pending_commands(device_id, limit)
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.get("/api/commands")
def commands_list(
to_device_id: Optional[str] = None,
limit: int = Query(50, ge=1, le=200),
):
return storage.list_commands(to_device_id, limit)
@app.post("/api/paired-tracks/start")
def paired_tracks_start(
body: PairedTrackStartBody,
x_lora_client: Optional[str] = Header(None, alias=ANDROID_CLIENT_HEADER),
):
if body.initiator:
initiator = body.initiator
elif body.device_id:
initiator = body.device_id
elif (x_lora_client or "").strip().lower() == ANDROID_CLIENT_VALUE:
raise HTTPException(400, detail="initiator or device_id required")
else:
initiator = storage.WEB_SENDER_ID
try:
return storage.start_paired_track(body.device_ids, str(initiator))
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.get("/api/paired-tracks/active")
def paired_tracks_active():
session = storage.get_active_paired_track()
return {"active": session is not None, "session": session}
@app.post("/api/paired-tracks/ack")
def paired_tracks_ack(
body: PairedTrackAckBody,
x_lora_client: Optional[str] = Header(None, alias=ANDROID_CLIENT_HEADER),
):
_require_android(x_lora_client)
try:
return storage.ack_paired_track(
body.session_id, body.device_id, body.track_id
)
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.post("/api/paired-tracks/cancel")
def paired_tracks_cancel(body: PairedTrackCancelBody):
try:
return storage.cancel_paired_track(body.session_id)
except ValueError as e:
raise HTTPException(400, detail=str(e)) from e
@app.get("/api/health")
def health():
status = storage.db_status()