use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::model::CapabilityRole;
use crate::model::component::capability::CapabilityKind;
use crate::model::robot::KinematicConfig;
use crate::authoring::normalized;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct TestAltRobotDto {
identity: String,
frame: PathBuf,
drive: KinematicConfig,
limits: AltLimits,
#[serde(default)]
mounts: Vec<AltMount>,
#[serde(default)]
programs: Vec<AltProgram>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltLimits {
linear_mps: f64,
angular_radps: f64,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltMount {
name: String,
kind: String,
link: String,
#[serde(default)]
hardware: Option<AltHardware>,
#[serde(default)]
purpose: Vec<AltPurpose>,
#[serde(default)]
tuning: Vec<AltTuning>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltHardware {
can_bus: u8,
can_node: u8,
#[serde(default)]
step_ms: Option<u64>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltPurpose {
capability: String,
#[serde(rename = "for")]
roles: Vec<CapabilityRole>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltTuning {
capability: String,
kind: AltTuningKind,
#[serde(default)]
reversed: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum AltTuningKind {
Motor,
Encoder,
}
impl AltTuningKind {
const fn canonical(&self) -> CapabilityKind {
match self {
Self::Motor => CapabilityKind::Motor,
Self::Encoder => CapabilityKind::Encoder,
}
}
}
impl TestAltRobotDto {
fn normalize(self) -> normalized::Robot {
normalized::Robot {
id: self.identity,
structure: self.frame,
kinematic: self.drive,
motion_limits: crate::model::robot::MotionLimits {
max_linear_speed_mps: self.limits.linear_mps,
max_angular_speed_radps: self.limits.angular_radps,
},
instances: self
.mounts
.into_iter()
.map(|mount| {
(
mount.name,
normalized::ComponentInstance {
component_type: mount.kind,
mount_link: mount.link,
driver: mount.hardware.map(|hardware| {
crate::authoring::source::robot::driver::DriverConfig {
connection:
crate::authoring::source::robot::driver::ConnectionConfig::Can {
bus: hardware.can_bus,
node_id: hardware.can_node,
},
runtime_clock_ms: hardware.step_ms.unwrap_or(100),
}
}),
roles: mount
.purpose
.into_iter()
.map(|purpose| {
(purpose.capability, purpose.roles.into_iter().collect())
})
.collect(),
parameters: mount
.tuning
.into_iter()
.map(|tuning| {
(
tuning.capability,
normalized::CapabilityParameters {
kind: tuning.kind.canonical(),
direction_sign: if tuning.reversed { -1 } else { 1 },
},
)
})
.collect(),
},
)
})
.collect(),
services: self
.programs
.into_iter()
.map(|program| (program.name, program.settings))
.collect(),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AltProgram {
name: String,
#[serde(default)]
settings: Option<serde_json::Value>,
}
const ALT_DOCUMENT: &str = r#"
identity: rgbd-imu-diff-drive
frame: structure.urdf
limits:
linear_mps: 0.6
angular_radps: 2.0
drive:
kind: differential
left_actuators: [front_left_drive.motor, rear_left_drive.motor]
right_actuators: [front_right_drive.motor, rear_right_drive.motor]
left_encoders: [front_left_drive.encoder, rear_left_drive.encoder]
right_encoders: [front_right_drive.encoder, rear_right_drive.encoder]
wheel_radius_m: 0.1
wheel_base_m: 0.4
programs:
- name: localize
settings:
rate_hz: 10
mounts:
- name: front_left_drive
kind: drive_motor
link: front_left_wheel_mount
hardware: { can_bus: 0, can_node: 1 }
tuning:
- { capability: motor, kind: motor }
- { capability: encoder, kind: encoder }
- name: front_right_drive
kind: drive_motor
link: front_right_wheel_mount
hardware: { can_bus: 0, can_node: 2 }
tuning:
- { capability: motor, kind: motor, reversed: true }
- { capability: encoder, kind: encoder, reversed: true }
- name: rear_left_drive
kind: drive_motor
link: rear_left_wheel_mount
hardware: { can_bus: 0, can_node: 3 }
tuning:
- { capability: motor, kind: motor }
- { capability: encoder, kind: encoder }
- name: rear_right_drive
kind: drive_motor
link: rear_right_wheel_mount
hardware: { can_bus: 0, can_node: 4 }
tuning:
- { capability: motor, kind: motor, reversed: true }
- { capability: encoder, kind: encoder, reversed: true }
- name: imu
kind: imu
link: imu_mount
purpose:
- { capability: imu, for: [odometry, localization] }
- name: front_camera
kind: camera_rgbd_640x480
link: front_camera_mount
purpose:
- { capability: rgb, for: [localization] }
- { capability: depth, for: [localization, mapping, traversability] }
- name: front_center_tof
kind: range_tof
link: front_center_tof_mount
purpose:
- { capability: range, for: [mapping, traversability, safety] }
- name: gnss
kind: gnss
link: gnss_mount
purpose:
- { capability: gnss, for: [localization] }
"#;
const CURRENT_SERVICES: &str = "\nservices:\n localize:\n config:\n rate_hz: 10\n";
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(1)
.expect("this crate sits at phoxal/ under the workspace root")
.to_path_buf()
}
fn staged_project(temp: &Path) {
let fixture = workspace_root().join("fixture/robot/rgbd-imu-diff-drive");
let mut document =
std::fs::read_to_string(fixture.join("robot.yaml")).expect("the fixture robot document");
document.push_str(CURRENT_SERVICES);
std::fs::write(temp.join("robot.yaml"), document).expect("a writable staging directory");
std::fs::copy(fixture.join("structure.urdf"), temp.join("structure.urdf"))
.expect("a readable fixture file");
}
fn component_roots(robot: &normalized::Robot) -> BTreeMap<String, PathBuf> {
let components = workspace_root().join("fixture/components");
robot
.used_component_types()
.into_iter()
.map(|component_type| (component_type.to_string(), components.join(component_type)))
.collect()
}
fn compile(project_root: &Path, robot: &normalized::Robot) -> crate::authoring::CompiledProject {
let project_root = project_root
.canonicalize()
.expect("the staged project resolves");
let resolved = crate::authoring::ResolvedSources {
robot_manifest: project_root.join("robot.yaml"),
robot_root: project_root.clone(),
component_roots: component_roots(robot),
};
let model = resolved
.compile_model(robot, OFFICIAL_SERVICES.map(official))
.expect("the model compiles");
let assets = resolved
.compile_assets(&project_root, robot, &model)
.expect("the assets compile");
crate::authoring::CompiledProject {
robot: model,
assets,
}
}
const OFFICIAL_SERVICES: [&str; 2] = ["drive", "localize"];
fn official(id: &str) -> crate::model::identity::ServiceId {
crate::model::identity::ServiceId::new(id).expect("an official service id is a token")
}
fn rendered(project: &crate::authoring::CompiledProject) -> String {
let mut rendered = serde_json::to_string_pretty(project.robot()).expect("the model serializes");
rendered.push('\n');
for (id, bytes) in project.assets().iter() {
rendered.push_str(&format!("{} {}\n", id.as_str(), bytes.len()));
}
rendered
}
#[test]
fn a_second_source_generation_normalizes_into_the_same_robot() {
let temp = tempfile::tempdir().expect("a staging directory");
staged_project(temp.path());
let current = crate::authoring::source::robot::Manifest::load(temp.path().join("robot.yaml"))
.expect("the current-generation document parses")
.normalize()
.expect("the current-generation document normalizes");
let alternative = serde_yaml::from_str::<TestAltRobotDto>(ALT_DOCUMENT)
.expect("the alternative document parses")
.normalize();
assert_eq!(
alternative, current,
"a second source language must resolve into the same normalized robot"
);
}
#[test]
fn a_second_source_generation_compiles_to_the_same_canonical_output() {
let temp = tempfile::tempdir().expect("a staging directory");
staged_project(temp.path());
let current = crate::authoring::source::robot::Manifest::load(temp.path().join("robot.yaml"))
.expect("the current-generation document parses")
.normalize()
.expect("the current-generation document normalizes");
let alternative = serde_yaml::from_str::<TestAltRobotDto>(ALT_DOCUMENT)
.expect("the alternative document parses")
.normalize();
assert_eq!(
rendered(&compile(temp.path(), &alternative)),
rendered(&compile(temp.path(), ¤t)),
"both source languages must compile to identical canonical output"
);
}