Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crates/obsbot-device/src/family/exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,38 @@ mod tests {
assert_eq!(result.unwrap(), vec![4, 5, 6]);
assert_eq!(reads, 3);
}

use crate::scripted::{self, Scripted};
use std::time::Instant;

#[test]
fn all_cameras_of_a_process_share_one_sequence() {
// separate handles must never repeat a number, so there is one counter
assert!(std::ptr::eq(sequence(), sequence()));
}

fn frame_of(command: u16) -> [u8; 60] {
let mut f = [0u8; 60];
f[10..12].copy_from_slice(&command.to_le_bytes());
f
}

#[test]
fn a_pair_is_sent_with_a_gap_between_its_frames() {
// back to back the camera applies neither frame
let camera = scripted::camera(Scripted::new([0; XU_SIZE]));
let start = Instant::now();
camera.pair([frame_of(1), frame_of(2)]).unwrap();
assert!(start.elapsed() >= PAIR_GAP);
}

#[test]
fn a_pair_fails_when_either_frame_cannot_be_sent() {
for failing in [1, 2] {
let mut script = Scripted::new([0; XU_SIZE]);
script.answers.insert(failing, scripted::Answer::SendFails);
let result = scripted::camera(script).pair([frame_of(1), frame_of(2)]);
assert!(matches!(result, Err(Error::Io(_))), "{failing}: {result:?}");
}
}
}
44 changes: 44 additions & 0 deletions crates/obsbot-device/src/family/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,48 @@ mod tests {
prev = cur;
}
}

#[test]
fn a_frame_holds_exactly_44_bytes_of_data() {
// 60 bytes in all, 16 of them headers: 44 bytes of data fill the frame to its end
assert_eq!(MAX_DATA, 44);
let f = frame(0x04, 1, &[0xab; 44], 0);
assert!(f[16..].iter().all(|b| *b == 0xab));
}

#[test]
#[should_panic(expected = "must not exceed 44 bytes")]
fn one_byte_past_the_data_section_is_refused() {
frame(0x04, 1, &[0u8; 45], 0);
}

#[test]
fn a_rejection_needs_a_16_byte_header_and_the_extension_tag() {
// layout: header length at 4..6, the extension `00 02` at 12..14, the i16 code at 14..16
let rejected = |header: u16, extension: [u8; 2]| {
let mut r = [0u8; 60];
r[0] = 0xaa;
r[1] = 0x89;
r[2..4].copy_from_slice(&7u16.to_le_bytes());
r[4..6].copy_from_slice(&header.to_le_bytes());
r[12..14].copy_from_slice(&extension);
r[14..16].copy_from_slice(&(-3i16).to_le_bytes());
parse_answer(&r, 7, 0).map(|a| matches!(a, Answer::Rejected(-3)))
};
assert_eq!(rejected(16, [0x00, 0x02]), Some(true));
assert_eq!(rejected(12, [0x00, 0x02]), Some(false), "12-byte header");
assert_eq!(rejected(16, [0x01, 0x02]), Some(false), "another extension");
}

#[test]
fn a_reply_must_hold_both_headers_to_be_an_answer() {
let full = answer(0x29, 7, &[]);
assert_eq!(parse_answer(&full[..12], 7, 0), None, "12 bytes");
assert_eq!(parse_answer(&full[..15], 7, 0), None, "one byte short");
assert_eq!(
parse_answer(&full[..16], 7, 0),
Some(Answer::Value(&[])),
"both headers and no data"
);
}
}
32 changes: 32 additions & 0 deletions crates/obsbot-device/src/legacy/full_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,22 @@ mod scripted_camera_tests {
/// (2.0), the live pose (pan 25, tilt 10), the microphone volume (55) and the absolute zoom
/// control (37). The tracking query gets `tracking`; every other query is rejected.
fn camera(tracking: Answer) -> Camera {
camera_in_mode(tracking, (0, 0), Vec::new())
}

/// The same camera in the AI mode of the status bytes 0x18 and 0x1c (0, 0 is Static), with
/// `target` as the answer to the target query when it is not empty.
fn camera_in_mode(tracking: Answer, ai_mode: (u8, u8), target: Vec<u8>) -> Camera {
let mut status = [0u8; 60];
status[0x06] = 1;
status[0x18] = ai_mode.0;
status[0x1c] = ai_mode.1;
status[0x21] = 2;
let mut script = Scripted::new(status);
script.answers.insert(0x0104, tracking);
if !target.is_empty() {
script.answers.insert(0x01c4, Answer::Value(target));
}
script
.answers
.insert(0x6804, Answer::Value(2.0f32.to_le_bytes().to_vec()));
Expand Down Expand Up @@ -514,4 +525,25 @@ mod scripted_camera_tests {
let result = camera(Answer::SendFails).full_status_with(zones, identity);
assert!(matches!(result, Err(Error::Io(_))), "{result:?}");
}

#[test]
fn the_target_is_asked_only_while_ai_tracking_is_on() {
// `01 01 <detected> 01 00 00` and four f32; a camera in Static would answer a single 00,
// so it is not asked at all
let mut answer = vec![1, 1, 1, 1, 0, 0];
for v in [0.25f32, 0.5, 0.75, 1.0] {
answer.extend(v.to_le_bytes());
}
let target = |ai_mode| {
let (zones, identity) = known();
camera_in_mode(Answer::Value(vec![0; 12]), ai_mode, answer.clone())
.full_status_with(zones, identity)
.unwrap()
.target
};
assert_eq!(target((0, 0)), None, "Static");
let followed = target((2, 0)).expect("Normal tracking asks for the target");
assert!(followed.detected);
assert_eq!(followed.coords, [0.25, 0.5, 0.75, 1.0]);
}
}
Loading