feat: add track map implementation — persistent TrackModel, dual position mode, boundary recording, track API endpoints
Backend tasks A→B→C→F→E→D from the Track Map Implementation plan:
A. PCARS V1 position data: read world_position (3×i16) and current_lap_distance
from viewed participant entry, compute derived spline_position, add unit tests.
B. Dual position mode: auto-detect spline vs coordinate mode in track map analysis,
compute cumulative 2D distance as pseudo-spline for PCARS/future games.
C. Persistent TrackModel: Corner, Sector, TrackModel data structures with serde,
TrackModelStore with async CRUD at data/tracks/<game>/<track_name>.json.
F. Track model building algorithm: resample boundaries, compute center line,
track width, heading, curvature, auto-detect corners (κ > 0.005), 3 sectors,
coordinate normalization to [0,1]².
E. TrackMapping use case: new boundary recording workflow with step-by-step
guidance, session integration.
D. Track API endpoints: GET/DELETE /api/tracks/{game}/{track_name},
GET /api/tracks, GET .../corners, POST /api/tracks/build.
This commit is contained in:
parent
9fae8ba9d8
commit
937090681f
8 changed files with 786 additions and 22 deletions
|
|
@ -166,6 +166,21 @@ fn parse_v1_telemetry(data: &[u8]) -> Result<TelemetryFrame, PcarsParseError> {
|
|||
// Track length
|
||||
let track_length = read_f32(data, 1360);
|
||||
|
||||
// V1 position data from viewed participant
|
||||
let viewed_idx = read_i8_at(data, 4) as usize;
|
||||
let participant_offset = 464 + viewed_idx * 16;
|
||||
let v1_world_x = read_i16(data, participant_offset) as f32;
|
||||
let v1_world_y = read_i16(data, participant_offset + 2) as f32;
|
||||
let v1_world_z = read_i16(data, participant_offset + 4) as f32;
|
||||
let current_lap_distance = read_u16(data, participant_offset + 6);
|
||||
let spline_pos = if track_length > 0.0 {
|
||||
(current_lap_distance as f32 / track_length).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let laps_completed = (read_u8_at(data, participant_offset + 9) & 0x7F) as i32;
|
||||
let _current_lap = read_u8_at(data, participant_offset + 10) as i32;
|
||||
|
||||
let frame = TelemetryFrame {
|
||||
source: FeedSource::Pcars,
|
||||
raw_size: data.len(),
|
||||
|
|
@ -185,12 +200,12 @@ fn parse_v1_telemetry(data: &[u8]) -> Result<TelemetryFrame, PcarsParseError> {
|
|||
lap_time_ms: (current_lap_s * 1000.0) as i32,
|
||||
last_lap_ms: (last_lap_s * 1000.0) as i32,
|
||||
best_lap_ms: (best_lap_s * 1000.0) as i32,
|
||||
lap_count: 0, // Not directly in V1 telemetry packet
|
||||
lap_count: laps_completed,
|
||||
|
||||
world_x: 0.0,
|
||||
world_y: 0.0,
|
||||
world_z: 0.0,
|
||||
spline_position: 0.0,
|
||||
world_x: v1_world_x,
|
||||
world_y: v1_world_y,
|
||||
world_z: v1_world_z,
|
||||
spline_position: spline_pos,
|
||||
|
||||
acc_g_vertical: acc_vert,
|
||||
acc_g_horizontal: acc_horiz,
|
||||
|
|
@ -237,7 +252,8 @@ fn parse_v1_telemetry(data: &[u8]) -> Result<TelemetryFrame, PcarsParseError> {
|
|||
track_temperature: Some(track_temp),
|
||||
rain_density: Some(rain_density),
|
||||
track_length: Some(track_length),
|
||||
full_position: None, // V1 does not have full 3D position
|
||||
full_position: Some([v1_world_x, v1_world_y, v1_world_z]),
|
||||
current_lap_distance: Some(current_lap_distance),
|
||||
tyre_wear: Some(read_u8_4(data, 316)),
|
||||
brake_temp_celsius: Some(read_i16_4(data, 328)),
|
||||
suspension_damage: Some(read_u8_4(data, 324)),
|
||||
|
|
@ -359,6 +375,7 @@ fn parse_v2_car_physics(data: &[u8]) -> Result<TelemetryFrame, PcarsParseError>
|
|||
tyre_wear: Some(read_u8_4(data, 196)),
|
||||
brake_temp_celsius: Some(read_i16_4(data, 208)),
|
||||
suspension_damage: Some(read_u8_4(data, 204)),
|
||||
current_lap_distance: None,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -394,6 +411,15 @@ mod tests {
|
|||
// Current time at 20 = 42.5 seconds
|
||||
packet[20..24].copy_from_slice(&42.5f32.to_le_bytes());
|
||||
|
||||
// World position for participant 0 at offset 464: x=100, y=5, z=-200
|
||||
packet[464..466].copy_from_slice(&100i16.to_le_bytes());
|
||||
packet[466..468].copy_from_slice(&5i16.to_le_bytes());
|
||||
packet[468..470].copy_from_slice(&(-200i16).to_le_bytes());
|
||||
// Current lap distance at offset 464+6 = 470: 2500 meters
|
||||
packet[470..472].copy_from_slice(&2500u16.to_le_bytes());
|
||||
// Track length at offset 1360: 5000.0 meters
|
||||
packet[1360..1364].copy_from_slice(&5000.0f32.to_le_bytes());
|
||||
|
||||
let result = parse_pcars_packet(&packet).unwrap();
|
||||
match result {
|
||||
PcarsPacket::V1Telemetry(frame) => {
|
||||
|
|
@ -407,7 +433,13 @@ mod tests {
|
|||
assert_eq!(frame.best_lap_ms, 78123);
|
||||
assert_eq!(frame.last_lap_ms, 80456);
|
||||
assert_eq!(frame.lap_time_ms, 42500);
|
||||
assert_eq!(frame.oil_temp_celsius, Some(0)); // default
|
||||
assert!((frame.world_x - 100.0).abs() < 0.1);
|
||||
assert!((frame.world_y - 5.0).abs() < 0.1);
|
||||
assert!((frame.world_z - (-200.0)).abs() < 0.1);
|
||||
assert!((frame.spline_position - 0.5).abs() < 0.01);
|
||||
assert!(frame.full_position.is_some());
|
||||
assert_eq!(frame.current_lap_distance, Some(2500));
|
||||
assert_eq!(frame.oil_temp_celsius, Some(0));
|
||||
assert!(frame.aero_damage.is_some());
|
||||
}
|
||||
_ => panic!("Expected V1Telemetry"),
|
||||
|
|
@ -492,4 +524,35 @@ mod tests {
|
|||
let result = parse_pcars_packet(&packet);
|
||||
assert!(matches!(result, Ok(PcarsPacket::V2Other)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v1_position_nonzero_viewed_participant() {
|
||||
let mut packet = vec![0u8; V1_TELEMETRY_SIZE];
|
||||
packet[0..2].copy_from_slice(&1104u16.to_le_bytes());
|
||||
packet[2] = 0x00;
|
||||
|
||||
packet[4] = 3i8.to_le_bytes()[0];
|
||||
|
||||
packet[120..124].copy_from_slice(&30.0f32.to_le_bytes());
|
||||
|
||||
packet[1360..1364].copy_from_slice(&6000.0f32.to_le_bytes());
|
||||
|
||||
let p_offset = 464 + 3 * 16;
|
||||
packet[p_offset..p_offset+2].copy_from_slice(&300i16.to_le_bytes());
|
||||
packet[p_offset+2..p_offset+4].copy_from_slice(&10i16.to_le_bytes());
|
||||
packet[p_offset+4..p_offset+6].copy_from_slice(&(-400i16).to_le_bytes());
|
||||
packet[p_offset+6..p_offset+8].copy_from_slice(&3000u16.to_le_bytes());
|
||||
|
||||
let result = parse_pcars_packet(&packet).unwrap();
|
||||
match result {
|
||||
PcarsPacket::V1Telemetry(frame) => {
|
||||
assert!((frame.world_x - 300.0).abs() < 0.1);
|
||||
assert!((frame.world_y - 10.0).abs() < 0.1);
|
||||
assert!((frame.world_z - (-400.0)).abs() < 0.1);
|
||||
assert!((frame.spline_position - 0.5).abs() < 0.01);
|
||||
assert_eq!(frame.current_lap_distance, Some(3000));
|
||||
}
|
||||
_ => panic!("Expected V1Telemetry"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue