feat: KSUDP parser fix, warnings system, shift point rewrite, code quality (v0.5.1)
Bug Fixes:
- Fixed KSUDP binary parser offsets from packed (323B) to natural C
alignment (328B) — 3 bytes padding after char identifier, 2 bytes
after 6 bools
- Fixed setup_terminal() leaving terminal in raw mode on early exit
New Features:
- Thread-safe warning buffer (warnings.rs) with TUI bar + stderr fallback
- Rewrote shift point analysis per AC forum methodology: per-gear
acceleration curves, crossover detection, same-gear merging
- UseCase::guidance() — step-by-step recording instructions
- Recording metadata enrichment in analysis responses
- recording_id field on sessions
Code Quality:
- Extracted finalize_run() helper (session.rs)
- Extracted execute_and_store_analysis() helper (api.rs)
- Extracted lerp_search<T>() generic helper (analysis.rs)
Breaking API Changes:
- GET /api/use-cases: string[] -> [{id, name, guidance}[]]
- Shift point analysis: gear_ranges/shift_events -> gear_curves/crossover_points
Updated README.md with new API response examples and TUI warning bar.
This commit is contained in:
parent
090ab7e559
commit
e3aa511ac7
9 changed files with 696 additions and 312 deletions
87
src/api.rs
87
src/api.rs
|
|
@ -17,6 +17,7 @@ use crate::analysis;
|
|||
use crate::feed_state::FeedState;
|
||||
use crate::recording::{Game, RecordingManager, RecordingStatus, StartRecordingRequest, UseCase};
|
||||
use crate::session::SessionManager;
|
||||
use crate::telemetry_frame::TelemetryFrame;
|
||||
use crate::ui::FeedDisplayInfo;
|
||||
|
||||
pub struct AppState {
|
||||
|
|
@ -90,8 +91,9 @@ async fn create_session(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateSessionRequest>,
|
||||
) -> Response {
|
||||
let recording_id = state.recording_manager.active_recording_id().await;
|
||||
let mut mgr = lock_sessions(&state);
|
||||
match mgr.create_session(req.use_case, req.label) {
|
||||
match mgr.create_session(req.use_case, req.label, recording_id) {
|
||||
Ok(session) => (StatusCode::CREATED, Json(session)).into_response(),
|
||||
Err(msg) => err(StatusCode::BAD_REQUEST, &msg),
|
||||
}
|
||||
|
|
@ -147,28 +149,62 @@ async fn stop_run(
|
|||
}
|
||||
}
|
||||
|
||||
async fn enrich_with_recording_meta(
|
||||
state: &Arc<AppState>,
|
||||
result: &mut serde_json::Value,
|
||||
recording_id: Option<&str>,
|
||||
) {
|
||||
let rec_id = match recording_id {
|
||||
Some(id) => id,
|
||||
None => return,
|
||||
};
|
||||
if let Ok(manifest) = state.recording_manager.get_recording(rec_id).await {
|
||||
if let Some(obj) = result.as_object_mut() {
|
||||
obj.insert("car".to_string(), serde_json::json!(manifest.car));
|
||||
obj.insert("game".to_string(), serde_json::json!(manifest.game));
|
||||
obj.insert("track".to_string(), serde_json::json!(manifest.track));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn execute_and_store_analysis(
|
||||
state: &Arc<AppState>,
|
||||
session_id: &str,
|
||||
use_case: UseCase,
|
||||
frames_by_run: Vec<Vec<TelemetryFrame>>,
|
||||
run_labels: Vec<String>,
|
||||
recording_id: Option<String>,
|
||||
) -> serde_json::Value {
|
||||
let mut result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
|
||||
enrich_with_recording_meta(state, &mut result, recording_id.as_deref()).await;
|
||||
|
||||
let mut mgr = lock_sessions(state);
|
||||
let _ = mgr.set_analysis(session_id, result.clone());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn complete_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let (use_case, frames_by_run, run_labels) = {
|
||||
let (use_case, frames_by_run, run_labels, recording_id) = {
|
||||
let mut mgr = lock_sessions(&state);
|
||||
match mgr.complete_session(&id) {
|
||||
Ok(_session) => {}
|
||||
Err(msg) => return err(StatusCode::BAD_REQUEST, &msg),
|
||||
}
|
||||
match mgr.collect_run_frames(&id) {
|
||||
Some((uc, frames, labels)) => (uc, frames, labels),
|
||||
Some((uc, frames, labels)) => {
|
||||
let rec_id = mgr.get_session(&id).and_then(|s| s.recording_id.clone());
|
||||
(uc, frames, labels, rec_id)
|
||||
}
|
||||
None => return err(StatusCode::NOT_FOUND, "Session not found after completion"),
|
||||
}
|
||||
};
|
||||
|
||||
let result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
|
||||
|
||||
{
|
||||
let mut mgr = lock_sessions(&state);
|
||||
let _ = mgr.set_analysis(&id, result);
|
||||
}
|
||||
execute_and_store_analysis(&state, &id, use_case, frames_by_run, run_labels, recording_id).await;
|
||||
|
||||
let mgr = lock_sessions(&state);
|
||||
match mgr.get_session(&id) {
|
||||
|
|
@ -197,7 +233,7 @@ async fn run_analysis(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let (use_case, frames_by_run, run_labels) = {
|
||||
let (use_case, frames_by_run, run_labels, recording_id) = {
|
||||
let mgr = lock_sessions(&state);
|
||||
match mgr.get_session(&id) {
|
||||
Some(session) => {
|
||||
|
|
@ -207,19 +243,14 @@ async fn run_analysis(
|
|||
}
|
||||
None => return err(StatusCode::NOT_FOUND, "Session not found"),
|
||||
}
|
||||
let rec_id = mgr.get_session(&id).unwrap().recording_id.clone();
|
||||
match mgr.collect_run_frames(&id) {
|
||||
Some(data) => data,
|
||||
Some((uc, frames, labels)) => (uc, frames, labels, rec_id),
|
||||
None => return err(StatusCode::NOT_FOUND, "Session not found"),
|
||||
}
|
||||
};
|
||||
|
||||
let result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
|
||||
|
||||
{
|
||||
let mut mgr = lock_sessions(&state);
|
||||
let _ = mgr.set_analysis(&id, result.clone());
|
||||
}
|
||||
|
||||
let result = execute_and_store_analysis(&state, &id, use_case, frames_by_run, run_labels, recording_id).await;
|
||||
ok_json(result)
|
||||
}
|
||||
|
||||
|
|
@ -458,8 +489,26 @@ async fn get_live(State(state): State<Arc<AppState>>) -> Response {
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UseCaseInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
guidance: String,
|
||||
}
|
||||
|
||||
async fn list_use_cases() -> Response {
|
||||
ok_json(UseCase::all())
|
||||
let use_cases: Vec<UseCaseInfo> = UseCase::all()
|
||||
.into_iter()
|
||||
.map(|uc| UseCaseInfo {
|
||||
id: serde_json::to_value(&uc)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default(),
|
||||
name: format!("{:?}", uc),
|
||||
guidance: uc.guidance().to_string(),
|
||||
})
|
||||
.collect();
|
||||
ok_json(use_cases)
|
||||
}
|
||||
|
||||
async fn list_games() -> Response {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue