use approx::{assert_relative_eq, relative_eq};
use haply::http::InverseHttpClient;
use haply::{device_model::*, HaplyDevice};
use std::collections::HashMap;
use std::time::Instant;
use std::net::TcpListener;
use tokio;
use haply_mock::models::Device as MockDevice;
use haply_mock::models::DeviceInfo as MockDeviceInfo;
use haply_mock::models::Inverse3Info as MockInverse3Info;
use haply_mock::models::VerseGripWirelessInfo as MockVerseGripWirelessInfo;
use haply_mock::models::Inverse3RTState as MockInverse3RTState;
use haply_mock::models::WirelessVerseGripRTState as MockWirelessVerseGripRTState;
use haply_mock::DeviceRTState as MockDeviceRTState;
use haply_mock::Service as MockService;
use haply_mock::ServiceConfig as MockServiceConfig;
fn inverse_device(id: &str) -> MockDevice {
MockDevice {
id: id.to_string(),
specific_info: MockDeviceInfo::Inverse3(MockInverse3Info::default()),
..MockDevice::default()
}
}
fn versegrip_device(id: &str) -> MockDevice {
MockDevice {
id: id.to_string(),
specific_info: MockDeviceInfo::VerseGripWireless(MockVerseGripWirelessInfo::default()),
..MockDevice::default()
}
}
async fn setup_mock_service(inverse_id: &str, versegrip_id: &str) -> (MockService, u16, u16, u16) {
fn pick_free_port() -> std::io::Result<u16> {
let listener = TcpListener::bind(("127.0.0.1", 0))?;
let port = listener.local_addr()?.port();
drop(listener);
Ok(port)
}
let http_port = pick_free_port().expect("failed to pick free http port");
let simulation_ws_port = pick_free_port().expect("failed to pick free simulation ws port");
let events_ws_port = pick_free_port().expect("failed to pick free events ws port");
let config = MockServiceConfig {
http_port,
simulation_ws_port,
events_ws_port,
};
let service_mock = MockService::new(config);
let inverse = inverse_device(inverse_id);
let versegrip = versegrip_device(versegrip_id);
let inverse_state = MockInverse3RTState::default();
let versegrip_state = MockWirelessVerseGripRTState::default();
service_mock
.set_connected_devices(vec![inverse.clone(), versegrip.clone()])
.await;
let mut devices_state = HashMap::new();
devices_state.insert(inverse_id.to_string(), inverse_state.into());
devices_state.insert(versegrip_id.to_string(), versegrip_state.into());
service_mock.set_devices_data(devices_state).await;
(service_mock, http_port, simulation_ws_port, events_ws_port)
}
fn compare_inverse_leader_with_state(
leader_state: &Inverse3Device,
state: &MockInverse3RTState,
) -> bool {
for i in 0..3 {
if !relative_eq!(
leader_state.state.angular_position.unwrap().0[i],
state.angular_position[i] as f32,
epsilon = 1e-6
) {
return false;
}
if !relative_eq!(
leader_state.state.angular_velocity.unwrap().0[i],
state.angular_velocity[i] as f32,
epsilon = 1e-6
) {
return false;
}
if !relative_eq!(
leader_state.state.cursor_position.unwrap()[i],
state.cursor_position[i] as f32,
epsilon = 1e-6
) {
return false;
}
if !relative_eq!(
leader_state.state.cursor_velocity.unwrap()[i],
state.cursor_velocity[i] as f32,
epsilon = 1e-6
) {
return false;
}
}
for i in 0..4 {
if !relative_eq!(
leader_state.state.body_orientation.unwrap()[i],
state.body_orientation[i] as f32,
epsilon = 1e-6
) {
return false;
}
}
true
}
fn compare_versegrip_leader_with_state(
leader_state: &WirelessVerseGripDevice,
state: &MockWirelessVerseGripRTState,
) -> bool {
for i in 0..4 {
if !relative_eq!(
leader_state.state.orientation.unwrap()[i],
state.ori[i] as f32,
epsilon = 1e-6
) {
return false;
}
}
leader_state.state.buttons.unwrap().a == state.buttons[0]
&& leader_state.state.buttons.unwrap().b == state.buttons[1]
&& leader_state.state.buttons.unwrap().c == state.buttons[2]
}
#[tokio::test]
async fn test_get_version() {
let (mut service, http_port, _sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
let src_version = haply_mock::models::Version {
build_time: "2024-01-01T00:00:00Z".to_string(),
git_branch: "main".to_string(),
git_describe: "v1.0.0".to_string(),
git_hash: "abcdef1234567890".to_string(),
git_tag: "v1.0.0".to_string(),
project_name: "haply-service-mock".to_string(),
project_version: "1.0.0".to_string(),
};
service.set_service_version(src_version.clone()).await;
service.start();
let client = InverseHttpClient::new(&format!("http://localhost:{}", http_port));
let version = client.get_version().await.expect("Failed to get version");
assert_eq!(src_version.build_time, version.build_time);
assert_eq!(src_version.git_branch, version.git_branch);
assert_eq!(src_version.git_describe, version.git_describe);
assert_eq!(src_version.git_hash, version.git_hash);
assert_eq!(src_version.git_tag, version.git_tag);
assert_eq!(src_version.project_name, version.project_name);
assert_eq!(src_version.project_version, version.project_version);
}
#[tokio::test]
async fn test_http_get_devices() {
let (mut service, http_port, _sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
service.start();
let client = InverseHttpClient::new(&format!("http://localhost:{}", http_port));
let devices = client.get_devices().await.expect("Failed to get devices");
assert_eq!(devices.len(), 2);
assert!(devices
.iter()
.any(|d| matches!(d, Config::DeviceConfig(_)) && d.id() == "inv1"));
assert!(devices
.iter()
.any(|d| matches!(d, Config::WVGConfig(_)) && d.id() == "vg1"));
}
#[tokio::test]
async fn test_device_list_devices() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
service.start();
let device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let devices = device.list_devices().await.expect("Failed to list devices");
assert_eq!(devices.len(), 2);
assert!(devices
.iter()
.any(|d| matches!(d, Config::DeviceConfig(_)) && d.id() == "inv1"));
assert!(devices
.iter()
.any(|d| matches!(d, Config::WVGConfig(_)) && d.id() == "vg1"));
}
#[tokio::test]
async fn test_update_force() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let forces = vec![ForceInput {
device_id: "inv1".to_string(),
forces: Force {
x: 1.0,
y: 2.0,
z: 2.5,
},
}];
device
.update_force(forces.clone(), Some(true), Some(true))
.await
.expect("Failed to update force");
let mut got_target = false;
let start = std::time::Instant::now();
while start.elapsed().as_millis() < 1000 {
let targets = service.get_inverse_targets().await;
if let Some(target) = targets.get("inv1") {
if target.is_empty() {
continue;
}
assert_eq!(target.len(), 1);
assert_relative_eq!(target[0].0[0], 1.0, epsilon = 1e-6);
assert_relative_eq!(target[0].0[1], 2.0, epsilon = 1e-6);
assert_relative_eq!(target[0].0[2], 2.5, epsilon = 1e-6);
got_target = true;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(got_target, "Did not receive inverse targets for inv1");
}
#[tokio::test]
async fn test_update_force_multiple_devices() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inverse_id", "versegrip_id").await;
let inverse = inverse_device("inv1");
let versegrip = versegrip_device("vg1");
let inverse2 = inverse_device("inv2");
let versegrip2 = versegrip_device("vg2");
let inverse3 = inverse_device("inv3");
let inverse_state = MockInverse3RTState::default();
let versegrip_state = MockWirelessVerseGripRTState::default();
service
.set_connected_devices(vec![inverse, versegrip, inverse2, versegrip2, inverse3])
.await;
let mut devices_state = HashMap::new();
devices_state.insert("inv1".to_string(), inverse_state.clone().into());
devices_state.insert("vg1".to_string(), versegrip_state.clone().into());
devices_state.insert("inv2".to_string(), inverse_state.clone().into());
devices_state.insert("vg2".to_string(), versegrip_state.clone().into());
devices_state.insert("inv3".to_string(), inverse_state.clone().into());
service.set_devices_data(devices_state).await;
service.start();
let mut device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let forces = vec![
ForceInput {
device_id: "inv1".to_string(),
forces: Force {
x: 1.0,
y: 2.0,
z: 2.5,
},
},
ForceInput {
device_id: "inv2".to_string(),
forces: Force {
x: 0.5,
y: 1.5,
z: 2.0,
},
},
ForceInput {
device_id: "inv3".to_string(),
forces: Force {
x: 0.0,
y: 1.0,
z: 1.5,
},
},
];
device
.update_force(forces.clone(), Some(true), None)
.await
.expect("Failed to update force");
let mut got_target = false;
let start = std::time::Instant::now();
while start.elapsed().as_millis() < 200 {
let targets = service.get_inverse_targets().await;
if targets.iter().any(|el| !el.1.is_empty()) {
got_target = true;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(!got_target, "Expected no inverse targets to be received");
device.send_command().await.expect("Failed to send command");
let mut got_targets = HashMap::new();
let start = std::time::Instant::now();
while start.elapsed().as_millis() < 200 {
let targets = service.get_inverse_targets().await;
for (device_id, target) in targets {
if !target.is_empty() {
got_targets.insert(device_id, target);
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert_eq!(got_targets.len(), 3);
let target1 = got_targets
.get("inv1")
.expect("Did not receive target for inv1");
assert_eq!(target1.len(), 1);
assert_relative_eq!(target1[0].0[0], 1.0, epsilon = 1e-6);
assert_relative_eq!(target1[0].0[1], 2.0, epsilon = 1e-6);
assert_relative_eq!(target1[0].0[2], 2.5, epsilon = 1e-6);
let target2 = got_targets
.get("inv2")
.expect("Did not receive target for inv2");
assert_eq!(target2.len(), 1);
assert_relative_eq!(target2[0].0[0], 0.5, epsilon = 1e-6);
assert_relative_eq!(target2[0].0[1], 1.5, epsilon = 1e-6);
assert_relative_eq!(target2[0].0[2], 2.0, epsilon = 1e-6);
let target3 = got_targets
.get("inv3")
.expect("Did not receive target for inv3");
assert_eq!(target3.len(), 1);
assert_relative_eq!(target3[0].0[0], 0.0, epsilon = 1e-6);
assert_relative_eq!(target3[0].0[1], 1.0, epsilon = 1e-6);
assert_relative_eq!(target3[0].0[2], 1.5, epsilon = 1e-6);
}
#[tokio::test]
async fn test_read_state() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
let inverse_state = MockDeviceRTState::Inverse3(MockInverse3RTState::new_random());
let versegrip_state =
MockDeviceRTState::WirelessVerseGrip(MockWirelessVerseGripRTState::new_random());
service
.update_device_states("inv1", inverse_state.clone())
.await;
service
.update_device_states("vg1", versegrip_state.clone())
.await;
service.start();
let device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let mut state = None;
let start = Instant::now();
while start.elapsed().as_millis() < 200 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() && !st.wireless_verse_grip.is_empty() {
state = Some(st);
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let state = state.expect("Did not receive full device state");
assert!(state.verse_grip.is_empty());
assert!(state.custom_verse_grip.is_empty());
assert_eq!(state.inverse3.len(), 1);
assert_eq!(state.inverse3[0].device_id, "inv1");
let inv_state = match inverse_state {
MockDeviceRTState::Inverse3(s) => s,
_ => panic!("Expected Inverse3 state"),
};
assert!(compare_inverse_leader_with_state(
&state.inverse3[0],
&inv_state
));
assert_eq!(state.wireless_verse_grip.len(), 1);
assert_eq!(state.wireless_verse_grip[0].device_id, "vg1");
let vg_state = match versegrip_state {
MockDeviceRTState::WirelessVerseGrip(s) => s,
_ => panic!("Expected WirelessVerseGrip state"),
};
assert!(compare_versegrip_leader_with_state(
&state.wireless_verse_grip[0],
&vg_state
));
}
#[tokio::test]
async fn test_read_state_updates() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let mut state = None;
let start = Instant::now();
while start.elapsed().as_millis() < 200 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() && !st.wireless_verse_grip.is_empty() {
state = Some(st);
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let _ = state.expect("Did not receive full device state");
let inverse_state = MockDeviceRTState::Inverse3(MockInverse3RTState::new_random());
let versegrip_state =
MockDeviceRTState::WirelessVerseGrip(MockWirelessVerseGripRTState::new_random());
service
.update_device_states("inv1", inverse_state.clone())
.await;
service
.update_device_states("vg1", versegrip_state.clone())
.await;
let mut state = None;
let start = Instant::now();
while start.elapsed().as_millis() < 200 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() && !st.wireless_verse_grip.is_empty() {
state = Some(st);
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let state = state.expect("Did not receive full device state");
assert!(compare_inverse_leader_with_state(&state.inverse3[0], &MockInverse3RTState::default()));
assert!(compare_versegrip_leader_with_state(&state.wireless_verse_grip[0], &MockWirelessVerseGripRTState::default()));
device.send_command().await.expect("Failed to send command");
let mut state = None;
let start = Instant::now();
while start.elapsed().as_millis() < 200 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() && !st.wireless_verse_grip.is_empty() {
state = Some(st);
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let state = state.expect("Did not receive full device state");
let inv_state = match inverse_state {
MockDeviceRTState::Inverse3(s) => s,
_ => panic!("Expected Inverse3 state"),
};
assert!(compare_inverse_leader_with_state(&state.inverse3[0], &inv_state));
let wg_state = match versegrip_state {
MockDeviceRTState::WirelessVerseGrip(s) => s,
_ => panic!("Expected WirelessVerseGrip state"),
};
assert!(compare_versegrip_leader_with_state(&state.wireless_verse_grip[0], &wg_state));
}
#[tokio::test]
async fn test_send_ping_keeps_connection() {
let (mut service, http_port, sim_ws_port, _events_ws_port) = setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(&format!("http://localhost:{}", http_port), &format!("ws://localhost:{}", sim_ws_port))
.await
.expect("Failed to create device");
let start = Instant::now();
let mut got_initial = false;
while start.elapsed().as_millis() < 500 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() && !st.wireless_verse_grip.is_empty() {
got_initial = true;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(got_initial, "Did not receive initial state");
for _ in 0..5 {
device.send_ping().await.expect("send_ping should not fail");
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
let new_inverse_state = MockDeviceRTState::Inverse3(MockInverse3RTState::new_random());
let new_versegrip_state = MockDeviceRTState::WirelessVerseGrip(MockWirelessVerseGripRTState::new_random());
service.update_device_states("inv1", new_inverse_state.clone()).await;
service.update_device_states("vg1", new_versegrip_state.clone()).await;
device.send_force_full_render().await.expect("send_force_full_render should succeed after pings");
let mut got_updated = false;
let start = Instant::now();
let inv_expected = match &new_inverse_state {
MockDeviceRTState::Inverse3(s) => s.clone(),
_ => unreachable!(),
};
let vg_expected = match &new_versegrip_state {
MockDeviceRTState::WirelessVerseGrip(s) => s.clone(),
_ => unreachable!(),
};
while start.elapsed().as_millis() < 500 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty()
&& !st.wireless_verse_grip.is_empty()
&& compare_inverse_leader_with_state(&st.inverse3[0], &inv_expected)
&& compare_versegrip_leader_with_state(&st.wireless_verse_grip[0], &vg_expected)
{
got_updated = true;
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(got_updated, "Connection was not alive after send_ping — did not receive updated state");
}
#[tokio::test]
async fn test_read_state_3_5_fields() {
let (mut service, http_port, sim_ws_port, _events_ws_port) =
setup_mock_service("inv1", "vg1").await;
service.start();
let device = HaplyDevice::new(
&format!("http://localhost:{}", http_port),
&format!("ws://localhost:{}", sim_ws_port),
)
.await
.expect("Failed to create device");
let mut state = None;
let start = Instant::now();
while start.elapsed().as_millis() < 500 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty()
&& st.inverse3[0].config.is_some()
&& !st.wireless_verse_grip.is_empty()
{
state = Some(st);
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let state = state.expect("Did not receive full state with config");
let session = state.session.as_ref().expect("session info should be present");
assert_eq!(session.session_id, 0);
let session_config = session.config.as_ref().expect("session config should be present");
assert_eq!(
session_config.profile.as_ref().expect("profile should be present").name,
"default"
);
assert_eq!(
session_config.profile_name.as_deref(),
Some("default")
);
assert_eq!(
session_config.required_version.as_deref(),
Some(">=3.5,<4.0")
);
let inv_config = state.inverse3[0].config.as_ref().unwrap();
assert_eq!(inv_config.preset.as_ref().unwrap(), "defaults");
assert_eq!(
inv_config.coordinate_system.as_ref().unwrap().permutation,
"XYZ"
);
assert!(inv_config.mount.is_none()); let filters = inv_config.filters.as_ref().expect("filters should be present");
assert_eq!(filters.force_gate.as_ref().unwrap().gain, 0.0);
assert_eq!(filters.damping.as_ref().unwrap().scalar, 0.0);
let inv_state = &state.inverse3[0].state;
assert!(inv_state.current_cursor_force.is_some());
assert_eq!(inv_state.current_cursor_force.as_ref().unwrap().x, 0.0);
assert!(inv_state.current_cursor_position.is_some());
assert!(inv_state.current_angular_torques.is_some());
assert!(inv_state.current_angular_position.is_some());
assert_eq!(
*inv_state.control_domain.as_ref().unwrap(),
ControlDomain::Undefined
);
assert_eq!(*inv_state.control_mode.as_ref().unwrap(), ControlMode::Idle);
assert!(inv_state.transform.is_none());
assert!(inv_state.transform_velocity.is_none());
let wvg_config = state.wireless_verse_grip[0]
.config
.as_ref()
.unwrap();
assert_eq!(wvg_config.preset.as_ref().unwrap(), "defaults");
assert_eq!(
wvg_config.coordinate_system.as_ref().unwrap().permutation,
"XYZ"
);
assert!(wvg_config.mount.is_none()); }
#[tokio::test]
async fn test_multiple_commands_one_frame() {
let (mut service, http_port, sim_ws_port, _events_ws_port) =
setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(
&format!("http://localhost:{}", http_port),
&format!("ws://localhost:{}", sim_ws_port),
)
.await
.expect("Failed to create device");
device
.update_force(
vec![ForceInput {
device_id: "inv1".to_string(),
forces: Force { x: 0.5, y: 1.0, z: 1.5 },
}],
Some(true),
None,
)
.await
.unwrap();
device
.probe_cursor_position(vec!["inv1".to_string()], None)
.await
.unwrap();
device.send_command().await.unwrap();
let mut got_target = false;
let start = Instant::now();
while start.elapsed().as_millis() < 1000 {
let targets = service.get_inverse_targets().await;
if let Some(target) = targets.get("inv1") {
if !target.is_empty() {
assert_relative_eq!(target[0].0[0], 0.5, epsilon = 1e-6);
assert_relative_eq!(target[0].0[1], 1.0, epsilon = 1e-6);
assert_relative_eq!(target[0].0[2], 1.5, epsilon = 1e-6);
got_target = true;
break;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(got_target, "Force command was not received when sent alongside probe");
}
#[tokio::test]
async fn test_configure_session_profile() {
let (mut service, http_port, sim_ws_port, _events_ws_port) =
setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(
&format!("http://localhost:{}", http_port),
&format!("ws://localhost:{}", sim_ws_port),
)
.await
.expect("Failed to create device");
let start = Instant::now();
while start.elapsed().as_millis() < 500 {
let st = device.read_state().await.unwrap();
if !st.inverse3.is_empty() {
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
device
.configure_session(
SessionConfigure {
profile: Some(ProfileConfig {
name: "my_app".to_string(),
required_version: Some(">=3.5".to_string()),
}),
basis: None,
serialization: Some(SessionSerializationConfigure {
wireless_verse_grip: Some(SessionWvgSerializationConfigure {
legacy_mode: Some(true),
explicit_custom: Some(false),
extended_data: Some(SessionWvgExtendedDataConfigure {
raw_data: Some(true),
custom_fields: Some(false),
}),
}),
}),
sdf: None,
},
Some(true),
)
.await
.expect("configure_session should not fail");
device
.configure_inverse3(
"inv1",
Inverse3Configure {
damping: Some(DampingConfig {
scalar: Some(0.5),
vector: None,
}),
..Default::default()
},
Some(true),
)
.await
.expect("configure_inverse3 should not fail");
device
.update_force(
vec![ForceInput {
device_id: "inv1".to_string(),
forces: Force { x: 0.1, y: 0.2, z: 0.3 },
}],
Some(true),
Some(true),
)
.await
.expect("update_force after configure should succeed");
let mut got_target = false;
let start = Instant::now();
while start.elapsed().as_millis() < 1000 {
let targets = service.get_inverse_targets().await;
if let Some(target) = targets.get("inv1") {
if !target.is_empty() {
assert_relative_eq!(target[0].0[0], 0.1, epsilon = 1e-6);
got_target = true;
break;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
assert!(
got_target,
"Connection was not alive after configure commands"
);
}
#[tokio::test]
async fn test_configure_cleared_after_send() {
let (mut service, http_port, sim_ws_port, _events_ws_port) =
setup_mock_service("inv1", "vg1").await;
service.start();
let mut device = HaplyDevice::new(
&format!("http://localhost:{}", http_port),
&format!("ws://localhost:{}", sim_ws_port),
)
.await
.expect("Failed to create device");
device
.update_force(
vec![ForceInput {
device_id: "inv1".to_string(),
forces: Force { x: 1.0, y: 0.0, z: 0.0 },
}],
Some(true),
None,
)
.await
.unwrap();
device
.configure_inverse3(
"inv1",
Inverse3Configure {
damping: Some(DampingConfig { scalar: Some(0.3), vector: None }),
..Default::default()
},
None,
)
.await
.unwrap();
{
let msg = device.device_cmd_msg.lock().await;
assert!(msg.inverse3[0].commands.is_some());
assert!(msg.inverse3[0].configure.is_some());
}
device.send_command().await.unwrap();
{
let msg = device.device_cmd_msg.lock().await;
assert!(
msg.inverse3[0].commands.is_some(),
"Per-tick commands should persist after send"
);
let cfg = msg.inverse3[0].configure.as_ref()
.expect("Configure should persist because it holds the sticky damping field");
assert_eq!(
cfg.damping.as_ref().and_then(|d| d.scalar),
Some(0.3),
"Damping should persist after send (sticky configure field)"
);
assert!(cfg.preset.is_none(), "One-shot configure fields should be cleared after send");
assert!(cfg.navigation.is_none(), "One-shot configure fields should be cleared after send");
assert!(
msg.session.force_render_full_state.is_none(),
"force_render_full_state should be cleared after send"
);
}
}