use crate::ethercat::{SdoClient, SdoResult};
use crate::CommandClient;
use serde_json::json;
use strum_macros::FromRepr;
use std::time::{Duration, Instant};
use super::El3356View;
const TARE_PULSE: Duration = Duration::from_millis(100);
const SDO_TIMEOUT: Duration = Duration::from_secs(3);
const SDO_IDX: u16 = 0x8000;
const SUB_MV_V: u8 = 0x23;
const SUB_FULL_SCALE: u8 = 0x24;
const SUB_SCALE_FACTOR: u8 = 0x27;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Idle,
WritingMvV,
WritingFullScale,
WritingScaleFactor,
WaitWriteGeneralSdo,
ReadingMvV,
ReadingFullScale,
ReadingScaleFactor,
WaitReadGeneralSdo
}
#[repr(u16)]
#[derive(Copy, Clone, Debug, FromRepr)]
pub enum El3356Filters {
FIR50Hz = 0,
FIR60Hz = 1,
IIR1 = 2,
IIR2 = 3,
IIR3 = 4,
IIR4 = 5,
IIR5 = 6,
IIR6 = 7,
IIR7 = 8,
IIR8 = 9,
DynamicIIR = 10,
PDOFilterFrequency = 11
}
pub struct El3356 {
sdo: SdoClient,
pub peak_load: f32,
pub busy: bool,
pub error: bool,
pub error_message: String,
pub configured_mv_v: Option<f32>,
pub configured_full_scale_load: Option<f32>,
pub configured_scale_factor: Option<f32>,
state: State,
pending_tid: Option<u32>,
pending_full_scale: f32,
pending_mv_v: f32,
pending_scale_factor: f32,
tare_release_at: Option<Instant>,
sdo_res_value : serde_json::Value
}
fn parse_sdo_real32(data: &serde_json::Value) -> Option<f32> {
let v = data.get("value")?;
if let Some(bits) = v.as_u64() {
return Some(f32::from_bits(bits as u32));
}
if let Some(f) = v.as_f64() {
return Some(f as f32);
}
None
}
impl El3356 {
pub fn new(device: &str) -> Self {
Self {
sdo: SdoClient::new(device),
peak_load: 0.0,
busy: false,
error: false,
error_message: String::new(),
configured_mv_v: None,
configured_full_scale_load: None,
configured_scale_factor: None,
state: State::Idle,
pending_tid: None,
pending_full_scale: 0.0,
pending_mv_v: 0.0,
pending_scale_factor: 0.0,
tare_release_at: None,
sdo_res_value : serde_json::Value::Null
}
}
pub fn tick(&mut self, view: &mut El3356View, client: &mut CommandClient) {
let abs_load = view.load.abs();
if abs_load > self.peak_load.abs() {
self.peak_load = *view.load;
}
if let Some(release_at) = self.tare_release_at {
if Instant::now() >= release_at {
*view.tare = false;
self.tare_release_at = None;
} else {
*view.tare = true;
}
}
self.progress_sdo(client);
}
pub fn configure(
&mut self,
client: &mut CommandClient,
full_scale_load: f32,
sensitivity_mv_v: f32,
scale_factor: f32,
) {
if self.busy {
log::warn!("El3356::configure called while busy; request ignored");
return;
}
self.error = false;
self.error_message.clear();
self.pending_full_scale = full_scale_load;
self.pending_mv_v = sensitivity_mv_v;
self.pending_scale_factor = scale_factor;
let tid = self.sdo.write(client, SDO_IDX, SUB_MV_V, json!(sensitivity_mv_v));
self.pending_tid = Some(tid);
self.state = State::WritingMvV;
self.busy = true;
}
pub fn read_configuration(&mut self, client: &mut CommandClient) {
if self.busy {
log::warn!("El3356::read_configuration called while busy; request ignored");
return;
}
self.error = false;
self.error_message.clear();
self.configured_mv_v = None;
self.configured_full_scale_load = None;
self.configured_scale_factor = None;
let tid = self.sdo.read(client, SDO_IDX, SUB_MV_V);
self.pending_tid = Some(tid);
self.state = State::ReadingMvV;
self.busy = true;
}
pub fn reset_peak(&mut self) {
self.peak_load = 0.0;
}
pub fn tare(&mut self) {
self.peak_load = 0.0;
self.tare_release_at = Some(Instant::now() + TARE_PULSE);
}
pub fn clear_error(&mut self) {
self.error = false;
self.error_message.clear();
}
pub fn sdo_write(
&mut self,
client: &mut CommandClient,
index: u16,
sub_index: u8,
value: serde_json::Value,
) {
if self.busy {
log::warn!("El3356::sdo_write called while busy; request ignored");
return;
}
self.error = false;
self.error_message.clear();
let tid = self.sdo.write(client, index, sub_index, value);
self.pending_tid = Some(tid);
self.state = State::WaitWriteGeneralSdo;
self.busy = true;
}
pub fn sdo_read(
&mut self,
client: &mut CommandClient,
index: u16,
sub_index: u8,
) {
if self.busy {
log::warn!("El3356::sdo_read called while busy; request ignored");
return;
}
self.error = false;
self.error_message.clear();
let tid = self.sdo.read(client, index, sub_index);
self.pending_tid = Some(tid);
self.state = State::WaitReadGeneralSdo;
self.busy = true;
}
pub fn set_mode0_filter_enabled(&mut self, client: &mut CommandClient, enable : bool) {
if self.busy {
log::warn!("El3356::set_mode0_filter_enabled called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x01, json!(enable));
}
pub fn set_mode0_averager_enabled(&mut self, client: &mut CommandClient, enable : bool) {
if self.busy {
log::warn!("El3356::set_mode0_averager_enabled called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x03, json!(enable));
}
pub fn set_mode0_filter(&mut self, client: &mut CommandClient, filter : El3356Filters) {
if self.busy {
log::warn!("El3356::set_mode0_filter called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x11, json!(filter as u16));
}
pub fn set_mode1_filter_enabled(&mut self, client: &mut CommandClient, enable : bool) {
if self.busy {
log::warn!("El3356::set_mode1_filter_enabled called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x02, json!(enable));
}
pub fn set_mode1_averager_enabled(&mut self, client: &mut CommandClient, enable : bool) {
if self.busy {
log::warn!("El3356::set_mode1_averager_enabled called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x05, json!(enable));
}
pub fn set_mode1_filter(&mut self, client: &mut CommandClient, filter : El3356Filters) {
if self.busy {
log::warn!("El3356::set_mode1_filter called while busy; request ignored");
return;
}
self.sdo_write(client, 0x8000, 0x12, json!(filter as u16));
}
pub fn is_error(&self) -> bool {
return self.error;
}
pub fn is_busy(&self) -> bool {
return self.busy;
}
pub fn reset(&mut self) {
self.error = false;
self.error_message.clear();
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
self.tare_release_at = None;
self.sdo_res_value = serde_json::Value::Null;
}
pub fn result(&self) -> serde_json::Value {
self.sdo_res_value.clone()
}
pub fn result_as_f64(&self) -> Option<f64> {
self.sdo_res_value.get("value").and_then(|v| v.as_f64())
}
pub fn result_as_i64(&self) -> Option<i64> {
self.sdo_res_value.get("value").and_then(|v| v.as_i64())
}
pub fn result_as_f32(&self) -> Option<f32> {
parse_sdo_real32(&self.sdo_res_value)
}
fn progress_sdo(&mut self, client: &mut CommandClient) {
let tid = match self.pending_tid {
Some(t) => t,
None => return,
};
let result = self.sdo.result(client, tid, SDO_TIMEOUT);
match result {
SdoResult::Pending => {} SdoResult::Ok(data) => match self.state {
State::WritingMvV => {
self.configured_mv_v = Some(self.pending_mv_v);
let next_tid = self.sdo.write(
client, SDO_IDX, SUB_FULL_SCALE, json!(self.pending_full_scale),
);
self.pending_tid = Some(next_tid);
self.state = State::WritingFullScale;
}
State::WritingFullScale => {
self.configured_full_scale_load = Some(self.pending_full_scale);
let next_tid = self.sdo.write(
client, SDO_IDX, SUB_SCALE_FACTOR, json!(self.pending_scale_factor),
);
self.pending_tid = Some(next_tid);
self.state = State::WritingScaleFactor;
}
State::WritingScaleFactor => {
self.configured_scale_factor = Some(self.pending_scale_factor);
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
},
State::WaitWriteGeneralSdo => {
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
},
State::ReadingMvV => match parse_sdo_real32(&data) {
Some(v) => {
self.configured_mv_v = Some(v);
let next_tid = self.sdo.read(client, SDO_IDX, SUB_FULL_SCALE);
self.pending_tid = Some(next_tid);
self.state = State::ReadingFullScale;
}
None => self.set_error(&format!(
"SDO read 0x8000:0x{:02X} (mV/V) returned unparseable value: {}",
SUB_MV_V, data,
)),
},
State::ReadingFullScale => match parse_sdo_real32(&data) {
Some(v) => {
self.configured_full_scale_load = Some(v);
let next_tid = self.sdo.read(client, SDO_IDX, SUB_SCALE_FACTOR);
self.pending_tid = Some(next_tid);
self.state = State::ReadingScaleFactor;
}
None => self.set_error(&format!(
"SDO read 0x8000:0x{:02X} (full-scale) returned unparseable value: {}",
SUB_FULL_SCALE, data,
)),
},
State::ReadingScaleFactor => match parse_sdo_real32(&data) {
Some(v) => {
self.configured_scale_factor = Some(v);
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
}
None => self.set_error(&format!(
"SDO read 0x8000:0x{:02X} (scale factor) returned unparseable value: {}",
SUB_SCALE_FACTOR, data,
)),
},
State::WaitReadGeneralSdo => {
self.sdo_res_value = data;
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
},
State::Idle => {
self.pending_tid = None;
}
},
SdoResult::Err(e) => {
self.set_error(&format!("SDO {:?} failed: {}", self.state, e));
}
SdoResult::Timeout => {
self.set_error(&format!("SDO {:?} timed out after {:?}", self.state, SDO_TIMEOUT));
}
}
}
fn set_error(&mut self, message: &str) {
log::error!("El3356: {}", message);
self.error = true;
self.error_message = message.to_string();
self.pending_tid = None;
self.state = State::Idle;
self.busy = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use mechutil::ipc::CommandMessage;
use tokio::sync::mpsc;
#[derive(Default)]
struct TestPdo {
tare: bool,
load: f32,
load_steady: bool,
load_error: bool,
load_overrange: bool,
}
impl TestPdo {
fn view(&mut self) -> El3356View<'_> {
El3356View {
tare: &mut self.tare,
load: &self.load,
load_steady: &self.load_steady,
load_error: &self.load_error,
load_overrange: &self.load_overrange,
}
}
}
fn test_client() -> (
CommandClient,
mpsc::UnboundedSender<CommandMessage>,
mpsc::UnboundedReceiver<String>,
) {
let (write_tx, write_rx) = mpsc::unbounded_channel();
let (response_tx, response_rx) = mpsc::unbounded_channel();
let client = CommandClient::new(write_tx, response_rx);
(client, response_tx, write_rx)
}
fn last_sent_tid(rx: &mut mpsc::UnboundedReceiver<String>) -> u32 {
let msg_json = rx.try_recv().expect("expected a message on the wire");
let msg: CommandMessage = serde_json::from_str(&msg_json).unwrap();
msg.transaction_id
}
fn assert_last_sent(
rx: &mut mpsc::UnboundedReceiver<String>,
expected_topic: &str,
expected_sub: u8,
) -> u32 {
let msg_json = rx.try_recv().expect("expected a message on the wire");
let msg: CommandMessage = serde_json::from_str(&msg_json).unwrap();
assert_eq!(msg.topic, expected_topic);
assert_eq!(msg.data["index"], format!("0x{:04X}", SDO_IDX));
assert_eq!(msg.data["sub"], expected_sub);
msg.transaction_id
}
#[test]
fn peak_follows_largest_magnitude() {
let (mut client, _resp_tx, _write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
pdo.load = 10.0;
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, 10.0);
pdo.load = -25.0;
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, -25.0);
pdo.load = 20.0; fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, -25.0);
}
#[test]
fn reset_peak_zeroes_it() {
let (mut client, _resp_tx, _write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo { load: 42.0, ..Default::default() };
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, 42.0);
fb.reset_peak();
assert_eq!(fb.peak_load, 0.0);
}
#[test]
fn tare_resets_peak_and_pulses_bit() {
let (mut client, _resp_tx, _write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo { load: 50.0, ..Default::default() };
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, 50.0);
fb.tare();
assert_eq!(fb.peak_load, 0.0);
fb.tick(&mut pdo.view(), &mut client);
assert!(pdo.tare, "tare bit should be high within pulse window");
std::thread::sleep(TARE_PULSE + Duration::from_millis(20));
fb.tick(&mut pdo.view(), &mut client);
assert!(!pdo.tare, "tare bit should be cleared after pulse window");
}
#[test]
fn configure_sequences_three_sdo_writes() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.configure(&mut client, 1000.0, 2.0, 100000.0);
assert!(fb.busy);
assert_eq!(fb.state, State::WritingMvV);
let tid1 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_MV_V);
resp_tx.send(CommandMessage::response(tid1, json!(null))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_mv_v, Some(2.0));
assert_eq!(fb.state, State::WritingFullScale);
assert!(fb.busy);
let tid2 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_FULL_SCALE);
resp_tx.send(CommandMessage::response(tid2, json!(null))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_full_scale_load, Some(1000.0));
assert_eq!(fb.state, State::WritingScaleFactor);
assert!(fb.busy);
let tid3 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_SCALE_FACTOR);
resp_tx.send(CommandMessage::response(tid3, json!(null))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_scale_factor, Some(100000.0));
assert_eq!(fb.state, State::Idle);
assert!(!fb.busy);
assert!(!fb.error);
}
#[test]
fn configure_while_busy_is_noop() {
let (mut client, _resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
fb.configure(&mut client, 1000.0, 2.0, 100000.0);
let _tid1 = last_sent_tid(&mut write_rx);
assert!(fb.busy);
fb.configure(&mut client, 9999.0, 9.0, 99.0);
assert!(write_rx.try_recv().is_err(), "no new message should have been sent");
assert_eq!(fb.pending_mv_v, 2.0);
}
#[test]
fn sdo_error_sets_error_and_clears_busy() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.configure(&mut client, 1000.0, 2.0, 100000.0);
let tid1 = last_sent_tid(&mut write_rx);
let mut err_msg = CommandMessage::response(tid1, json!(null));
err_msg.success = false;
err_msg.error_message = "device offline".to_string();
resp_tx.send(err_msg).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert!(fb.error);
assert!(fb.error_message.contains("device offline"));
assert!(!fb.busy);
assert_eq!(fb.state, State::Idle);
}
#[test]
fn clear_error_resets_flag() {
let mut fb = El3356::new("EL3356_0");
fb.error = true;
fb.error_message = "boom".to_string();
fb.clear_error();
assert!(!fb.error);
assert!(fb.error_message.is_empty());
}
fn sdo_read_response_f32(v: f32) -> serde_json::Value {
json!({
"device": "EL3356_0",
"index": "0x8000",
"sub": 0,
"size": 4,
"value_hex": format!("0x{:08X}", v.to_bits()),
"value": v.to_bits() as u64,
})
}
fn assert_last_sent_read(
rx: &mut mpsc::UnboundedReceiver<String>,
expected_sub: u8,
) -> u32 {
let msg_json = rx.try_recv().expect("expected a message on the wire");
let msg: CommandMessage = serde_json::from_str(&msg_json).unwrap();
assert_eq!(msg.topic, "ethercat.read_sdo");
assert_eq!(msg.data["index"], format!("0x{:04X}", SDO_IDX));
assert_eq!(msg.data["sub"], expected_sub);
assert!(msg.data.get("value").is_none(), "reads must not include a value field");
msg.transaction_id
}
#[test]
fn read_configuration_fetches_three_sdos() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.read_configuration(&mut client);
assert!(fb.busy);
assert_eq!(fb.state, State::ReadingMvV);
assert_eq!(fb.configured_mv_v, None);
assert_eq!(fb.configured_full_scale_load, None);
assert_eq!(fb.configured_scale_factor, None);
let tid1 = assert_last_sent_read(&mut write_rx, SUB_MV_V);
resp_tx.send(CommandMessage::response(tid1, sdo_read_response_f32(2.5))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_mv_v, Some(2.5));
assert_eq!(fb.state, State::ReadingFullScale);
assert!(fb.busy);
let tid2 = assert_last_sent_read(&mut write_rx, SUB_FULL_SCALE);
resp_tx.send(CommandMessage::response(tid2, sdo_read_response_f32(500.0))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_full_scale_load, Some(500.0));
assert_eq!(fb.state, State::ReadingScaleFactor);
let tid3 = assert_last_sent_read(&mut write_rx, SUB_SCALE_FACTOR);
resp_tx.send(CommandMessage::response(tid3, sdo_read_response_f32(100_000.0))).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.configured_scale_factor, Some(100_000.0));
assert_eq!(fb.state, State::Idle);
assert!(!fb.busy);
assert!(!fb.error);
}
#[test]
fn read_configuration_while_busy_is_noop() {
let (mut client, _resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
fb.configure(&mut client, 1000.0, 2.0, 100_000.0);
let _tid = last_sent_tid(&mut write_rx);
assert!(fb.busy);
fb.read_configuration(&mut client);
assert!(write_rx.try_recv().is_err(), "no new message should have been sent");
assert_eq!(fb.state, State::WritingMvV);
}
#[test]
fn read_configuration_error_clears_busy_and_leaves_partial_none() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.configured_mv_v = Some(9.9);
fb.configured_full_scale_load = Some(9999.0);
fb.configured_scale_factor = Some(99.0);
fb.read_configuration(&mut client);
assert_eq!(fb.configured_mv_v, None);
assert_eq!(fb.configured_full_scale_load, None);
assert_eq!(fb.configured_scale_factor, None);
let tid1 = last_sent_tid(&mut write_rx);
let mut err_msg = CommandMessage::response(tid1, json!(null));
err_msg.success = false;
err_msg.error_message = "SDO abort: 0x06020000".to_string();
resp_tx.send(err_msg).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert!(fb.error);
assert!(fb.error_message.contains("SDO abort"));
assert!(!fb.busy);
assert_eq!(fb.state, State::Idle);
assert_eq!(fb.configured_mv_v, None);
assert_eq!(fb.configured_full_scale_load, None);
assert_eq!(fb.configured_scale_factor, None);
}
#[test]
fn parse_sdo_real32_from_u32_bits() {
let v = json!({"value": 0x40200000u64});
assert_eq!(parse_sdo_real32(&v), Some(2.5));
}
#[test]
fn parse_sdo_real32_from_f64_fallback() {
let v = json!({"value": 3.25});
assert_eq!(parse_sdo_real32(&v), Some(3.25));
}
#[test]
fn parse_sdo_real32_missing_field() {
let v = json!({"size": 4});
assert_eq!(parse_sdo_real32(&v), None);
}
#[test]
fn sdo_write_does_not_wipe_calibration_fields() {
let (mut client, _resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
fb.configured_mv_v = Some(2.0);
fb.configured_full_scale_load = Some(1000.0);
fb.configured_scale_factor = Some(100_000.0);
fb.sdo_write(&mut client, 0x8000, 0x11, json!(0u16));
let _tid = last_sent_tid(&mut write_rx);
assert_eq!(fb.configured_mv_v, Some(2.0));
assert_eq!(fb.configured_full_scale_load, Some(1000.0));
assert_eq!(fb.configured_scale_factor, Some(100_000.0));
assert!(fb.busy);
assert_eq!(fb.state, State::WaitWriteGeneralSdo);
}
#[test]
fn sdo_read_does_not_wipe_calibration_fields() {
let (mut client, _resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
fb.configured_mv_v = Some(2.0);
fb.configured_full_scale_load = Some(1000.0);
fb.configured_scale_factor = Some(100_000.0);
fb.sdo_read(&mut client, 0x8000, 0x11);
let _tid = last_sent_tid(&mut write_rx);
assert_eq!(fb.configured_mv_v, Some(2.0));
assert_eq!(fb.configured_full_scale_load, Some(1000.0));
assert_eq!(fb.configured_scale_factor, Some(100_000.0));
}
#[test]
fn sdo_read_populates_result_accessors() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.sdo_read(&mut client, 0x8000, 0x11);
let tid = last_sent_tid(&mut write_rx);
let payload = json!({
"device": "EL3356_0", "index": "0x8000", "sub": 0x11,
"size": 2, "value_hex": "0x0001", "value": 1u64,
});
resp_tx.send(CommandMessage::response(tid, payload)).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert!(!fb.busy);
assert_eq!(fb.result_as_i64(), Some(1));
assert_eq!(fb.result_as_f64(), Some(1.0));
assert_eq!(fb.result()["sub"], 0x11);
}
#[test]
fn result_as_f32_reinterprets_real32_bits() {
let (mut client, resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.sdo_read(&mut client, 0x8000, 0x23);
let tid = last_sent_tid(&mut write_rx);
let payload = json!({
"device": "EL3356_0", "index": "0x8000", "sub": 0x23,
"size": 4, "value_hex": format!("0x{:08X}", 2.5f32.to_bits()),
"value": 2.5f32.to_bits() as u64,
});
resp_tx.send(CommandMessage::response(tid, payload)).unwrap();
client.poll();
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.result_as_f32(), Some(2.5));
assert_eq!(fb.result_as_i64(), Some(2.5f32.to_bits() as i64));
}
#[test]
fn result_accessors_none_before_any_read() {
let fb = El3356::new("EL3356_0");
assert_eq!(fb.result_as_f64(), None);
assert_eq!(fb.result_as_i64(), None);
assert_eq!(fb.result_as_f32(), None);
assert_eq!(fb.result(), serde_json::Value::Null);
}
#[test]
fn reset_clears_latent_state() {
let (mut client, _resp_tx, mut write_rx) = test_client();
let mut fb = El3356::new("EL3356_0");
let mut pdo = TestPdo::default();
fb.tare();
assert!(fb.tare_release_at.is_some());
fb.sdo_read(&mut client, 0x8000, 0x11);
let _ = last_sent_tid(&mut write_rx);
fb.sdo_res_value = json!({"value": 99});
fb.reset();
assert!(!fb.busy);
assert!(!fb.error);
assert_eq!(fb.state, State::Idle);
assert!(fb.tare_release_at.is_none());
assert_eq!(fb.sdo_res_value, serde_json::Value::Null);
pdo.load = 42.0;
fb.tick(&mut pdo.view(), &mut client);
assert_eq!(fb.peak_load, 42.0);
}
#[test]
fn is_error_and_is_busy_accessors() {
let mut fb = El3356::new("EL3356_0");
assert!(!fb.is_error());
assert!(!fb.is_busy());
fb.error = true;
fb.busy = true;
assert!(fb.is_error());
assert!(fb.is_busy());
}
}