use crate::frame::{
Asset, Device, ForcePlate, FrameOfData, FrameTiming, RawSection, RigidBody, Skeleton,
};
use crate::modeldef::ModelDef;
use crate::version::Version;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PlannedSections {
pub skeletons: Vec<Skeleton>,
pub assets: Vec<Asset>,
pub force_plates: Vec<ForcePlate>,
pub devices: Vec<Device>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PlannedBody {
pub id: i32,
pub position: [f32; 3],
pub orientation: [f32; 4],
pub mean_marker_error: f32,
pub tracking_valid: bool,
}
impl PlannedBody {
#[must_use]
pub const fn tracked(id: i32, position: [f32; 3], orientation: [f32; 4]) -> Self {
Self {
id,
position,
orientation,
mean_marker_error: 0.0,
tracking_valid: true,
}
}
#[must_use]
pub const fn dropout(id: i32) -> Self {
Self {
id,
position: [0.0; 3],
orientation: [0.0, 0.0, 0.0, 1.0],
mean_marker_error: 0.0,
tracking_valid: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SyntheticServer {
pub version: Version,
pub high_res_clock_frequency: u64,
pub clock_offset_secs: f64,
}
impl SyntheticServer {
#[must_use]
pub const fn new(version: Version, high_res_clock_frequency: u64) -> Self {
Self {
version,
high_res_clock_frequency,
clock_offset_secs: 0.0,
}
}
#[must_use]
pub const fn with_clock_offset_secs(mut self, offset: f64) -> Self {
self.clock_offset_secs = offset;
self
}
#[must_use]
pub fn frame(
&self,
frame_number: i32,
true_time_secs: f64,
bodies: &[PlannedBody],
) -> FrameOfData {
let rigid_bodies = bodies
.iter()
.map(|b| RigidBody {
id: b.id,
position: b.position,
orientation: b.orientation,
mean_marker_error: b.mean_marker_error,
params: u16::from(b.tracking_valid),
})
.collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let camera_mid_exposure = (true_time_secs * self.high_res_clock_frequency as f64)
.round()
.max(0.0) as u64;
FrameOfData {
frame_number,
marker_sets: Vec::new(),
other_markers: Vec::new(),
rigid_bodies,
skeletons: empty_section(),
assets: empty_section(),
labeled_markers: Vec::new(),
force_plates: empty_section(),
devices: empty_section(),
timing: FrameTiming {
timecode: 0,
timecode_subframe: 0,
timestamp: true_time_secs + self.clock_offset_secs,
camera_mid_exposure,
camera_data_received: camera_mid_exposure,
transmit: camera_mid_exposure,
precision_timestamp_secs: 0,
precision_timestamp_fractional_secs: 0,
},
params: 0,
}
}
pub fn frame_with_sections(
&self,
frame_number: i32,
true_time_secs: f64,
bodies: &[PlannedBody],
sections: &PlannedSections,
) -> crate::Result<FrameOfData> {
let mut frame = self.frame(frame_number, true_time_secs, bodies);
frame.skeletons = RawSection::from_skeletons(§ions.skeletons)?;
frame.assets = RawSection::from_assets(§ions.assets)?;
frame.force_plates = RawSection::from_force_plates(§ions.force_plates)?;
frame.devices = RawSection::from_devices(§ions.devices)?;
Ok(frame)
}
pub fn datagram_with_sections(
&self,
frame_number: i32,
true_time_secs: f64,
bodies: &[PlannedBody],
sections: &PlannedSections,
) -> crate::Result<Vec<u8>> {
self.frame_with_sections(frame_number, true_time_secs, bodies, sections)?
.to_datagram(self.version)
}
pub fn model_def_datagram(&self, model: &ModelDef) -> crate::Result<Vec<u8>> {
model.to_datagram(self.version)
}
pub fn datagram(
&self,
frame_number: i32,
true_time_secs: f64,
bodies: &[PlannedBody],
) -> crate::Result<Vec<u8>> {
self.frame(frame_number, true_time_secs, bodies)
.to_datagram(self.version)
}
}
fn empty_section() -> RawSection {
RawSection {
count: 0,
payload: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn server() -> SyntheticServer {
SyntheticServer::new(Version::V4_1, 1_000_000)
}
#[test]
fn generated_datagram_round_trips_through_parse() {
let s = server();
let bodies = [
PlannedBody::tracked(3, [1.0, 2.0, 3.0], [0.0, 0.0, 0.0, 1.0]),
PlannedBody::dropout(4),
];
let dg = s.datagram(42, 0.35, &bodies).expect("valid frame encodes");
let parsed = FrameOfData::parse(&dg, Version::V4_1).expect("real wire bytes parse");
assert_eq!(parsed, s.frame(42, 0.35, &bodies));
assert_eq!(parsed.frame_number, 42);
assert_eq!(parsed.rigid_bodies.len(), 2);
}
#[test]
fn dropout_is_present_but_untracked() {
let s = server();
let frame = s.frame(1, 0.0, &[PlannedBody::dropout(3)]);
let rb = frame.rigid_bodies[0];
assert_eq!(rb.id, 3, "the body is present in the frame");
assert!(!rb.tracking_valid(), "but reports tracking-invalid (a gap)");
assert_eq!(rb.params & 0x01, 0);
}
#[test]
fn tracked_body_sets_tracking_valid_bit() {
let s = server();
let frame = s.frame(
1,
0.0,
&[PlannedBody::tracked(3, [0.0; 3], [0.0, 0.0, 0.0, 1.0])],
);
assert!(frame.rigid_bodies[0].tracking_valid());
}
#[test]
fn injected_offset_shifts_software_timestamp_but_not_camera_clock() {
let s = server().with_clock_offset_secs(0.030); let frame = s.frame(
10,
1.0,
&[PlannedBody::tracked(3, [0.0; 3], [0.0, 0.0, 0.0, 1.0])],
);
assert!((frame.timing.timestamp - 1.030).abs() < 1e-9);
assert_eq!(frame.timing.camera_mid_exposure, 1_000_000);
}
#[test]
fn generation_is_deterministic() {
let s = server().with_clock_offset_secs(0.005);
let bodies = [PlannedBody::tracked(
7,
[0.1, -0.2, 0.3],
[0.1, 0.2, 0.3, 0.9],
)];
assert_eq!(
s.datagram(5, 0.5, &bodies).expect("valid frame encodes"),
s.datagram(5, 0.5, &bodies).expect("valid frame encodes")
);
}
}