use crate::cm::{DeviceModel, SlotModel, SubmoduleModel};
use crate::data::FieldType;
use crate::dcp::{DcpConfig, DeviceProperties};
use crate::device::{DeviceSetup, RtOptions};
use crate::eth::MacAddr;
use crate::im::{Im0, ImError};
use crate::rpc::Uuid;
use thiserror::Error;
pub const MAX_SUBMODULE_BYTES: u16 = 1440;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Slot(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Input,
Output,
InputOutput,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubmoduleSpec {
pub slot: Slot,
pub name: String,
pub inputs: Vec<FieldType>,
pub outputs: Vec<FieldType>,
}
impl SubmoduleSpec {
pub fn direction(&self) -> Direction {
match (self.inputs.is_empty(), self.outputs.is_empty()) {
(false, true) => Direction::Input,
(true, false) => Direction::Output,
_ => Direction::InputOutput,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FieldRef {
pub byte: u16,
pub bit: u8,
pub ty: FieldType,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ConfigError {
#[error("slot 0 is the DAP and cannot carry process data")]
SlotZeroReserved,
#[error("slot {0} declared twice")]
DuplicateSlot(u16),
#[error("slot {0} has neither inputs nor outputs")]
EmptySubmodule(u16),
#[error("no submodule declared")]
NoSubmodule,
#[error("slot {slot}: {bytes} bytes exceed the {max}-byte submodule limit")]
TooLong {
slot: u16,
bytes: u32,
max: u16,
},
#[error("station name {0:?} is not a valid PROFINET name of station")]
BadStationName(String),
#[error("min device interval {0} is not one of 16, 32")]
BadInterval(u16),
#[error("vendor id must be non-zero")]
BadIdentity,
#[error(
"total {direction:?} C-SDU is {bytes} bytes, exceeding the {max}-byte RT frame budget"
)]
TooLongTotal {
direction: Direction,
bytes: u32,
max: u16,
},
#[error("invalid I&M0 identity: {0}")]
Im(#[from] ImError),
}
pub fn layout(fields: &[FieldType]) -> (Vec<FieldRef>, u16) {
let mut refs = Vec::with_capacity(fields.len());
let mut next_byte: u32 = 0; let mut bit_byte: Option<(u32, u8)> = None; for &ty in fields {
match ty.byte_len() {
None => {
let (byte, bit) = match bit_byte {
Some((b, bit)) if bit < 8 => (b, bit),
_ => {
let b = next_byte;
next_byte += 1;
(b, 0)
}
};
bit_byte = Some((byte, bit + 1));
refs.push(FieldRef {
byte: byte as u16,
bit,
ty,
});
}
Some(n) => {
bit_byte = None;
refs.push(FieldRef {
byte: next_byte as u16,
bit: 0,
ty,
});
next_byte += n as u32;
}
}
}
(refs, next_byte as u16)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Derived {
inputs: Vec<FieldRef>,
input_len: u16,
outputs: Vec<FieldRef>,
output_len: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceConfig {
station_name: String,
station_type: String,
vendor_id: u16,
device_id: u16,
min_device_interval: u16,
submodules: Vec<SubmoduleSpec>,
derived: Vec<Derived>, im0: Im0,
}
pub struct DeviceConfigBuilder {
station_name: String,
station_type: String,
vendor_id: u16,
device_id: u16,
min_device_interval: u16,
submodules: Vec<SubmoduleSpec>,
im0: Option<Im0>,
}
impl DeviceConfig {
pub fn builder(station_name: &str) -> DeviceConfigBuilder {
DeviceConfigBuilder {
station_name: station_name.to_string(),
station_type: "pnio device".to_string(),
vendor_id: 0xFFFF,
device_id: 0x0001,
min_device_interval: 32,
submodules: Vec::new(),
im0: None,
}
}
pub fn station_name(&self) -> &str {
&self.station_name
}
pub fn station_type(&self) -> &str {
&self.station_type
}
pub fn vendor_id(&self) -> u16 {
self.vendor_id
}
pub fn device_id(&self) -> u16 {
self.device_id
}
pub fn min_device_interval(&self) -> u16 {
self.min_device_interval
}
pub fn submodules(&self) -> &[SubmoduleSpec] {
&self.submodules
}
pub fn im0(&self) -> &Im0 {
&self.im0
}
fn index_of(&self, slot: Slot) -> Option<usize> {
self.submodules.iter().position(|s| s.slot == slot)
}
pub fn submodule(&self, slot: Slot) -> Option<&SubmoduleSpec> {
self.index_of(slot).map(|i| &self.submodules[i])
}
pub fn fields(&self, slot: Slot, dir: Direction) -> Option<&[FieldRef]> {
let d = &self.derived[self.index_of(slot)?];
match dir {
Direction::Input if !d.inputs.is_empty() => Some(&d.inputs),
Direction::Output if !d.outputs.is_empty() => Some(&d.outputs),
_ => None,
}
}
pub fn field(&self, slot: Slot, dir: Direction, index: usize) -> Option<FieldRef> {
self.fields(slot, dir)?.get(index).copied()
}
pub fn input_len(&self, slot: Slot) -> Option<u16> {
self.index_of(slot).map(|i| self.derived[i].input_len)
}
pub fn output_len(&self, slot: Slot) -> Option<u16> {
self.index_of(slot).map(|i| self.derived[i].output_len)
}
pub fn input_cr_len(&self) -> u16 {
cr_lengths(&self.submodules, &self.derived).0 as u16
}
pub fn output_cr_len(&self) -> u16 {
cr_lengths(&self.submodules, &self.derived).1 as u16
}
pub fn model(&self, mac: MacAddr) -> DeviceModel {
let sm = |subslot: u16, ident: u32, i: u16, o: u16| SubmoduleModel {
subslot,
submodule_ident: ident,
input_len: i,
output_len: o,
};
let mut slots = vec![SlotModel {
slot: 0,
module_ident: 0x1,
submodules: vec![
sm(1, 0x1, 0, 0),
sm(0x8000, 0x8000, 0, 0),
sm(0x8001, 0x8001, 0, 0),
],
}];
for (spec, d) in self.submodules.iter().zip(&self.derived) {
slots.push(SlotModel {
slot: spec.slot.0,
module_ident: 0x100 + spec.slot.0 as u32,
submodules: vec![sm(1, 0x1, d.input_len, d.output_len)],
});
}
DeviceModel {
vendor_id: self.vendor_id,
device_id: self.device_id,
instance: 1,
station_name: self.station_name.clone(),
mac,
max_alarm_data_length: 200,
slots,
}
}
pub fn dcp_properties(&self, ip: [u8; 4]) -> DeviceProperties {
DeviceProperties {
name_of_station: self.station_name.clone(),
type_of_station: self.station_type.clone(),
vendor_id: self.vendor_id,
device_id: self.device_id,
device_role: 0x0100,
device_instance: 1,
device_options: vec![1, 2, 2, 2, 2, 3],
ip,
subnet: [255, 255, 255, 0],
gateway: ip,
ip_block_info: 1,
}
}
pub fn activity_seed(mac: MacAddr) -> Uuid {
let mut b = [
0x14, 0xaf, 0x19, 0x8a, 0x12, 0x34, 0x10, 0x56, 0x80, 0x79, 0, 0, 0, 0, 0, 0,
];
b[10..].copy_from_slice(&mac.0);
Uuid(b)
}
pub fn setup(&self, mac: MacAddr, ip: [u8; 4], rt: Option<RtOptions>) -> DeviceSetup {
let mut im0 = self.im0.clone();
if im0.serial_number.is_empty() {
im0.serial_number = format!("PNIO-{:02X}{:02X}{:02X}", mac.0[3], mac.0[4], mac.0[5]);
}
DeviceSetup {
dcp: DcpConfig {
mac,
properties: self.dcp_properties(ip),
},
model: self.model(mac),
activity_seed: Self::activity_seed(mac),
rt,
im0,
im_store: None,
}
}
}
impl DeviceConfigBuilder {
pub fn station_type(mut self, s: &str) -> Self {
self.station_type = s.to_string();
self
}
pub fn identity(mut self, vendor_id: u16, device_id: u16) -> Self {
self.vendor_id = vendor_id;
self.device_id = device_id;
self
}
pub fn min_device_interval(mut self, v: u16) -> Self {
self.min_device_interval = v;
self
}
pub fn im0(mut self, im0: Im0) -> Self {
self.im0 = Some(im0);
self
}
pub fn input(self, slot: Slot, fields: &[FieldType]) -> Self {
self.submodule(slot, &format!("in{}", slot.0), fields, &[])
}
pub fn output(self, slot: Slot, fields: &[FieldType]) -> Self {
self.submodule(slot, &format!("out{}", slot.0), &[], fields)
}
pub fn submodule(
mut self,
slot: Slot,
name: &str,
inputs: &[FieldType],
outputs: &[FieldType],
) -> Self {
self.submodules.push(SubmoduleSpec {
slot,
name: name.to_string(),
inputs: inputs.to_vec(),
outputs: outputs.to_vec(),
});
self
}
pub fn build(self) -> Result<DeviceConfig, ConfigError> {
let station_name = normalize_station_name(&self.station_name)
.ok_or_else(|| ConfigError::BadStationName(self.station_name.clone()))?;
if !matches!(self.min_device_interval, 16 | 32) {
return Err(ConfigError::BadInterval(self.min_device_interval));
}
if self.vendor_id == 0 {
return Err(ConfigError::BadIdentity);
}
let im0 = self.im0.unwrap_or_else(|| Im0 {
order_id: order_id_from_station_type(&self.station_type),
..Im0::default()
});
im0.validate()?;
if self.submodules.is_empty() {
return Err(ConfigError::NoSubmodule);
}
let mut submodules = self.submodules;
submodules.sort_by_key(|s| s.slot);
let mut derived = Vec::with_capacity(submodules.len());
for (i, sm) in submodules.iter().enumerate() {
if sm.slot.0 == 0 {
return Err(ConfigError::SlotZeroReserved);
}
if i > 0 && submodules[i - 1].slot == sm.slot {
return Err(ConfigError::DuplicateSlot(sm.slot.0));
}
if sm.inputs.is_empty() && sm.outputs.is_empty() {
return Err(ConfigError::EmptySubmodule(sm.slot.0));
}
let (inputs, input_len) = checked_layout(sm.slot, &sm.inputs)?;
let (outputs, output_len) = checked_layout(sm.slot, &sm.outputs)?;
derived.push(Derived {
inputs,
input_len,
outputs,
output_len,
});
}
check_total_csdu(&submodules, &derived)?;
Ok(DeviceConfig {
station_name,
station_type: self.station_type,
vendor_id: self.vendor_id,
device_id: self.device_id,
min_device_interval: self.min_device_interval,
submodules,
derived,
im0,
})
}
}
fn order_id_from_station_type(station_type: &str) -> String {
let id: String = station_type
.chars()
.filter(char::is_ascii)
.take(20)
.collect();
if id.trim().is_empty() {
Im0::default().order_id
} else {
id
}
}
fn checked_layout(slot: Slot, fields: &[FieldType]) -> Result<(Vec<FieldRef>, u16), ConfigError> {
let bytes: u32 = {
let mut n = 0u32;
let mut open_bits = 0u32;
for f in fields {
match f.byte_len() {
None => {
if open_bits == 0 {
n += 1;
}
open_bits = (open_bits + 1) % 8;
}
Some(k) => {
open_bits = 0;
n += k as u32;
}
}
}
n
};
if bytes > MAX_SUBMODULE_BYTES as u32 {
return Err(ConfigError::TooLong {
slot: slot.0,
bytes,
max: MAX_SUBMODULE_BYTES,
});
}
Ok(layout(fields))
}
fn is_reserved_port_name(s: &str) -> bool {
fn all_digits(s: &str, len: usize) -> bool {
s.len() == len && s.bytes().all(|b| b.is_ascii_digit())
}
let Some(rest) = s.strip_prefix("port-") else {
return false;
};
match rest.split_once('-') {
None => all_digits(rest, 3),
Some((xyz, abcde)) => all_digits(xyz, 3) && all_digits(abcde, 5),
}
}
fn cr_lengths(submodules: &[SubmoduleSpec], derived: &[Derived]) -> (u32, u32) {
let (mut input_bytes, mut output_bytes): (u32, u32) = (3, 3);
for (sm, d) in submodules.iter().zip(derived) {
let has_in = !sm.inputs.is_empty();
let has_out = !sm.outputs.is_empty();
if has_in {
input_bytes += d.input_len as u32 + 1;
}
if has_out {
output_bytes += d.output_len as u32 + 1;
}
if has_out {
input_bytes += 1; }
if has_in {
output_bytes += 1;
}
}
(input_bytes, output_bytes)
}
fn check_total_csdu(submodules: &[SubmoduleSpec], derived: &[Derived]) -> Result<(), ConfigError> {
let (input_bytes, output_bytes) = cr_lengths(submodules, derived);
if input_bytes > MAX_SUBMODULE_BYTES as u32 {
return Err(ConfigError::TooLongTotal {
direction: Direction::Input,
bytes: input_bytes,
max: MAX_SUBMODULE_BYTES,
});
}
if output_bytes > MAX_SUBMODULE_BYTES as u32 {
return Err(ConfigError::TooLongTotal {
direction: Direction::Output,
bytes: output_bytes,
max: MAX_SUBMODULE_BYTES,
});
}
Ok(())
}
fn normalize_station_name(s: &str) -> Option<String> {
let s = s.to_ascii_lowercase();
if s.is_empty() || s.len() > 240 || !s.is_ascii() || is_reserved_port_name(&s) {
return None;
}
let mut any_non_numeric = false;
for label in s.split('.') {
if label.is_empty() || label.starts_with('-') || label.ends_with('-') {
return None;
}
if !label
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
{
return None;
}
if !label.bytes().all(|b| b.is_ascii_digit()) {
any_non_numeric = true;
}
}
any_non_numeric.then_some(s)
}
#[cfg(test)]
mod tests {
use super::*;
use FieldType::*;
fn refs(v: &[(u16, u8, FieldType)]) -> Vec<FieldRef> {
v.iter()
.map(|&(byte, bit, ty)| FieldRef { byte, bit, ty })
.collect()
}
#[test]
fn layout_mixes_bools_and_byte_types_in_declaration_order() {
let (f, len) = layout(&[Real, Bool, Bool, Int, Bool]);
assert_eq!(
f,
refs(&[
(0, 0, Real),
(4, 0, Bool),
(4, 1, Bool),
(5, 0, Int),
(7, 0, Bool)
])
);
assert_eq!(len, 8);
}
#[test]
fn layout_packs_bools_eight_per_byte() {
assert_eq!(layout(&[Bool; 32]).1, 4);
let (f, len) = layout(&[Bool; 9]);
assert_eq!(len, 2);
assert_eq!(
f[8],
FieldRef {
byte: 1,
bit: 0,
ty: Bool
}
);
assert_eq!(layout(&[Bool, Int, Bool]).1, 4);
assert_eq!(layout(&[]).1, 0);
}
fn sample() -> DeviceConfig {
DeviceConfig::builder("pnio-dev")
.input(Slot(1), &[Real; 16])
.input(Slot(2), &[Bool; 32])
.output(Slot(3), &[Real; 16])
.output(Slot(4), &[Bool; 32])
.build()
.unwrap()
}
#[test]
fn builder_defaults_and_getters() {
let cfg = sample();
assert_eq!(cfg.station_name(), "pnio-dev");
assert_eq!(cfg.station_type(), "pnio device");
assert_eq!((cfg.vendor_id(), cfg.device_id()), (0xFFFF, 0x0001));
assert_eq!(cfg.min_device_interval(), 32);
assert_eq!(cfg.submodules().len(), 4);
assert_eq!(cfg.input_len(Slot(1)), Some(64));
assert_eq!(cfg.output_len(Slot(1)), Some(0));
assert_eq!(cfg.output_len(Slot(4)), Some(4));
assert_eq!(
cfg.submodule(Slot(2)).unwrap().direction(),
Direction::Input
);
assert_eq!(
cfg.field(Slot(2), Direction::Input, 31),
Some(FieldRef {
byte: 3,
bit: 7,
ty: Bool
})
);
assert_eq!(cfg.field(Slot(2), Direction::Input, 32), None);
assert_eq!(cfg.field(Slot(2), Direction::Output, 0), None);
assert_eq!(cfg.fields(Slot(3), Direction::Output).unwrap().len(), 16);
assert_eq!(cfg.fields(Slot(9), Direction::Output), None);
}
#[test]
fn cr_lengths_include_the_iops_iocs_bytes() {
let cfg = sample();
assert_eq!((cfg.input_cr_len(), cfg.output_cr_len()), (75, 75));
let one_bool = DeviceConfig::builder("a")
.input(Slot(1), &[Bool])
.build()
.unwrap();
assert_eq!((one_bool.input_cr_len(), one_bool.output_cr_len()), (5, 4));
}
#[test]
fn slots_are_sorted_regardless_of_insertion_order() {
let cfg = DeviceConfig::builder("a")
.output(Slot(4), &[Bool])
.input(Slot(1), &[Int])
.build()
.unwrap();
let slots: Vec<u16> = cfg.submodules().iter().map(|s| s.slot.0).collect();
assert_eq!(slots, vec![1, 4]);
}
#[test]
fn mixed_submodule_has_both_directions() {
let cfg = DeviceConfig::builder("a")
.submodule(Slot(5), "mixed", &[Int, Bool], &[Dint])
.build()
.unwrap();
let sm = cfg.submodule(Slot(5)).unwrap();
assert_eq!(sm.direction(), Direction::InputOutput);
assert_eq!(
(cfg.input_len(Slot(5)), cfg.output_len(Slot(5))),
(Some(3), Some(4))
);
assert_eq!(sm.name, "mixed");
}
#[test]
fn every_config_error_is_reported() {
let e = |b: DeviceConfigBuilder| b.build().unwrap_err();
assert_eq!(
e(DeviceConfig::builder("a").input(Slot(0), &[Bool])),
ConfigError::SlotZeroReserved
);
assert_eq!(
e(DeviceConfig::builder("a")
.input(Slot(1), &[Bool])
.output(Slot(1), &[Bool])),
ConfigError::DuplicateSlot(1)
);
assert_eq!(
e(DeviceConfig::builder("a").input(Slot(1), &[])),
ConfigError::EmptySubmodule(1)
);
assert_eq!(e(DeviceConfig::builder("a")), ConfigError::NoSubmodule);
assert_eq!(
e(DeviceConfig::builder("a").input(Slot(1), &[Real; 361])),
ConfigError::TooLong {
slot: 1,
bytes: 1444,
max: MAX_SUBMODULE_BYTES
}
);
for bad in [
"Edge_01",
"-edge",
"edge-",
"123",
"",
"a..b",
"édge",
"123.456",
"port-001",
"port-001-12345",
] {
assert_eq!(
e(DeviceConfig::builder(bad).input(Slot(1), &[Bool])),
ConfigError::BadStationName(bad.to_string()),
"{bad}"
);
}
assert!(DeviceConfig::builder("edge-reg-01.plant2")
.input(Slot(1), &[Bool])
.build()
.is_ok());
assert!(DeviceConfig::builder("port-a")
.input(Slot(1), &[Bool])
.build()
.is_ok());
assert_eq!(
e(DeviceConfig::builder("a")
.min_device_interval(24)
.input(Slot(1), &[Bool])),
ConfigError::BadInterval(24)
);
assert_eq!(
e(DeviceConfig::builder("a")
.min_device_interval(64)
.input(Slot(1), &[Bool])),
ConfigError::BadInterval(64)
);
assert_eq!(
e(DeviceConfig::builder("a")
.identity(0, 1)
.input(Slot(1), &[Bool])),
ConfigError::BadIdentity
);
}
#[test]
fn total_csdu_over_1440_bytes_is_rejected() {
let e = DeviceConfig::builder("a")
.input(Slot(1), &[Real; 180])
.input(Slot(2), &[Real; 180])
.build()
.unwrap_err();
assert_eq!(
e,
ConfigError::TooLongTotal {
direction: Direction::Input,
bytes: 1445,
max: MAX_SUBMODULE_BYTES,
}
);
assert!(sample().input_len(Slot(1)).is_some());
}
#[test]
fn station_name_is_lowercased_on_input() {
let cfg = DeviceConfig::builder("Pnio-Dev")
.input(Slot(1), &[Bool])
.build()
.unwrap();
assert_eq!(cfg.station_name(), "pnio-dev");
}
#[test]
fn model_is_derived_deterministically() {
use crate::cm::{DeviceModel, SubmoduleModel};
let mac = crate::eth::MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
let m = sample().model(mac);
let dap = DeviceModel::pnet_sample(mac);
assert_eq!(m.slots[0], dap.slots[0]); assert_eq!((m.vendor_id, m.device_id, m.instance), (0xFFFF, 0x0001, 1));
assert_eq!(m.station_name, "pnio-dev");
assert_eq!(m.max_alarm_data_length, 200);
let idents: Vec<(u16, u32)> = m.slots[1..]
.iter()
.map(|s| (s.slot, s.module_ident))
.collect();
assert_eq!(idents, vec![(1, 0x101), (2, 0x102), (3, 0x103), (4, 0x104)]);
assert_eq!(
m.slots[1].submodules,
vec![SubmoduleModel {
subslot: 1,
submodule_ident: 0x1,
input_len: 64,
output_len: 0
}]
);
assert_eq!(m.find(4, 1).unwrap().output_len, 4);
}
#[test]
fn dcp_properties_and_setup_carry_the_identity() {
let mac = crate::eth::MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
let cfg = sample();
let p = cfg.dcp_properties([172, 16, 2, 10]);
assert_eq!(
(p.vendor_id, p.device_id, p.device_role, p.device_instance),
(0xFFFF, 1, 0x0100, 1)
);
assert_eq!(p.name_of_station, "pnio-dev");
assert_eq!(p.type_of_station, "pnio device");
assert_eq!(
(p.ip, p.subnet, p.gateway, p.ip_block_info),
([172, 16, 2, 10], [255, 255, 255, 0], [172, 16, 2, 10], 1)
);
assert_eq!(p.device_options, vec![1, 2, 2, 2, 2, 3]);
let s = cfg.setup(mac, [172, 16, 2, 10], None);
assert_eq!(s.dcp.mac, mac);
assert_eq!(s.model, cfg.model(mac));
assert_eq!(s.activity_seed.0[10..], mac.0);
assert!(s.rt.is_none());
}
#[test]
fn builder_accepts_im0_and_rejects_bad_ones() {
use crate::im::ImError;
let e = DeviceConfig::builder("a")
.input(Slot(1), &[Bool])
.im0(Im0 {
order_id: "x".repeat(21),
..Im0::default()
})
.build()
.unwrap_err();
assert_eq!(
e,
ConfigError::Im(ImError::TooLong {
field: "order_id",
max: 20
})
);
let custom = Im0 {
order_id: "test order".into(),
..Im0::default()
};
let cfg = DeviceConfig::builder("a")
.input(Slot(1), &[Bool])
.im0(custom.clone())
.build()
.unwrap();
assert_eq!(cfg.im0(), &custom);
let mac = crate::eth::MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
let s = sample().setup(mac, [172, 16, 2, 10], None);
assert_eq!(s.im0.serial_number, "PNIO-CD19F8");
}
#[test]
fn default_im0_takes_its_order_id_from_the_station_type() {
let cfg = DeviceConfig::builder("a")
.station_type("Foo")
.input(Slot(1), &[Bool])
.build()
.unwrap();
assert_eq!(cfg.im0().order_id, "Foo");
assert_eq!(
cfg.im0(),
&Im0 {
order_id: "Foo".into(),
..Im0::default()
}
);
let cfg = DeviceConfig::builder("a")
.station_type("a station type far longer than twenty bytes")
.input(Slot(1), &[Bool])
.build()
.unwrap();
assert_eq!(cfg.im0().order_id, "a station type far l");
let cfg = DeviceConfig::builder("a")
.station_type("Foo")
.input(Slot(1), &[Bool])
.im0(Im0 {
order_id: "Bar".into(),
..Im0::default()
})
.build()
.unwrap();
assert_eq!(cfg.im0().order_id, "Bar");
assert_eq!(Im0::default().order_id, "pnio device");
}
#[test]
fn layout_from_ar_accepts_the_derived_model() {
use crate::cm::{validate, ConnectReq};
use crate::rt::Layout;
use crate::testutil::{synthetic_connect_req, SYNTH_BLOCKS_OFF};
let mac = crate::eth::MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
let model = sample().model(mac);
let pdu = synthetic_connect_req(&model);
let req = ConnectReq::parse(&pdu[SYNTH_BLOCKS_OFF..]).unwrap();
let params = validate(&req, &model).unwrap();
let layout = Layout::from_ar(¶ms, &model).unwrap();
assert_eq!(layout.input_cr.data_length, 75);
assert_eq!(layout.output_cr.data_length, 75);
let s1 = layout
.input_cr
.objects
.iter()
.find(|o| o.slot == 1)
.unwrap();
assert_eq!((s1.data_off, s1.data_len, s1.iops_off), (3, 64, 67));
}
}
#[cfg(test)]
mod order_id_fallback_tests {
use super::*;
#[test]
fn empty_or_non_ascii_station_type_falls_back_to_the_default_order_id() {
assert_eq!(order_id_from_station_type(""), Im0::default().order_id);
assert_eq!(order_id_from_station_type(" "), Im0::default().order_id);
assert_eq!(order_id_from_station_type("éèà"), Im0::default().order_id);
assert_eq!(order_id_from_station_type("Foo"), "Foo");
assert_eq!(order_id_from_station_type(&"x".repeat(30)), "x".repeat(20));
}
}