feat: #1 telemetry record/playback with auto-race detection

- TelemetryRecorder: manual + auto mode (IsRaceOn edge detection)
- TelemetryPlayer: timed packet replay into _handle_packet pipeline
- .tsr format: [uint32 elapsed_ms][uint32 len][bytes]
- Settings UI: start/stop, auto toggle, recording list, play button
- API: /record/*, /playback/*, /recordings
This commit is contained in:
2026-07-26 18:40:09 +08:00
parent 4bba4b850d
commit a9bd35baa8
4 changed files with 225 additions and 1 deletions
+47
View File
@@ -422,3 +422,50 @@ async def api_update_forward_targets(data: dict[str, Any]):
save_config(cfg)
telemetry_listener.forwarder.reload_targets()
return {"ok": True, "active": telemetry_listener.forwarder.active}
# ---- Record & Playback ----
from server.recorder import TelemetryRecorder, TelemetryPlayer
_recorder = None
_player = None
def _get_recorder():
global _recorder
if _recorder is None: _recorder = TelemetryRecorder(telemetry_listener)
return _recorder
def _get_player():
global _player
if _player is None: _player = TelemetryPlayer(telemetry_listener)
return _player
@router.post("/record/start")
async def api_start_record():
_get_recorder().start()
return {"ok": True, "recording": True}
@router.post("/record/stop")
async def api_stop_record():
_get_recorder().stop()
return {"ok": True, "recording": False}
@router.get("/record/status")
async def api_record_status():
return {"recording": _get_recorder().is_recording, "playing": _get_player().is_playing}
@router.get("/recordings")
async def api_list_recordings():
return TelemetryPlayer.list_recordings()
@router.post("/playback/start")
async def api_start_playback(data: dict):
filename = data.get("filename", "")
if not filename: raise HTTPException(400, "filename required")
_get_player().start(filename)
return {"ok": True, "playing": True}
@router.post("/playback/stop")
async def api_stop_playback():
_get_player().stop()
return {"ok": True, "playing": False}