1use crate::ethercat::{SdoClient, SdoResult};
63use crate::CommandClient;
64use serde_json::json;
65use std::time::{Duration, Instant};
66
67use super::El3356View;
68
69const TARE_PULSE: Duration = Duration::from_millis(100);
72
73const SDO_TIMEOUT: Duration = Duration::from_secs(3);
76
77const SDO_IDX: u16 = 0x8000;
79const SUB_MV_V: u8 = 0x23;
80const SUB_FULL_SCALE: u8 = 0x24;
81const SUB_SCALE_FACTOR: u8 = 0x27;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum State {
85 Idle,
86 WritingMvV,
87 WritingFullScale,
88 WritingScaleFactor,
89}
90
91pub struct El3356 {
93 sdo: SdoClient,
95
96 pub peak_load: f32,
99 pub busy: bool,
101 pub error: bool,
103 pub error_message: String,
105 pub configured_mv_v: Option<f32>,
108 pub configured_full_scale_load: Option<f32>,
111 pub configured_scale_factor: Option<f32>,
114
115 state: State,
117 pending_tid: Option<u32>,
118 pending_full_scale: f32,
120 pending_mv_v: f32,
121 pending_scale_factor: f32,
122 tare_release_at: Option<Instant>,
125}
126
127impl El3356 {
128 pub fn new(device: &str) -> Self {
133 Self {
134 sdo: SdoClient::new(device),
135 peak_load: 0.0,
136 busy: false,
137 error: false,
138 error_message: String::new(),
139 configured_mv_v: None,
140 configured_full_scale_load: None,
141 configured_scale_factor: None,
142 state: State::Idle,
143 pending_tid: None,
144 pending_full_scale: 0.0,
145 pending_mv_v: 0.0,
146 pending_scale_factor: 0.0,
147 tare_release_at: None,
148 }
149 }
150
151 pub fn tick(&mut self, view: &mut El3356View, client: &mut CommandClient) {
158 let abs_load = view.load.abs();
160 if abs_load > self.peak_load.abs() {
161 self.peak_load = *view.load;
162 }
163
164 if let Some(release_at) = self.tare_release_at {
166 if Instant::now() >= release_at {
167 *view.tare = false;
168 self.tare_release_at = None;
169 } else {
170 *view.tare = true;
171 }
172 }
173
174 self.progress_sdo(client);
176 }
177
178 pub fn configure(
186 &mut self,
187 client: &mut CommandClient,
188 full_scale_load: f32,
189 sensitivity_mv_v: f32,
190 scale_factor: f32,
191 ) {
192 if self.busy {
193 log::warn!("El3356::configure called while busy; request ignored");
194 return;
195 }
196 self.error = false;
197 self.error_message.clear();
198 self.pending_full_scale = full_scale_load;
199 self.pending_mv_v = sensitivity_mv_v;
200 self.pending_scale_factor = scale_factor;
201
202 let tid = self.sdo.write(client, SDO_IDX, SUB_MV_V, json!(sensitivity_mv_v));
204 self.pending_tid = Some(tid);
205 self.state = State::WritingMvV;
206 self.busy = true;
207 }
208
209 pub fn reset_peak(&mut self) {
211 self.peak_load = 0.0;
212 }
213
214 pub fn tare(&mut self) {
220 self.peak_load = 0.0;
221 self.tare_release_at = Some(Instant::now() + TARE_PULSE);
222 }
223
224 pub fn clear_error(&mut self) {
226 self.error = false;
227 self.error_message.clear();
228 }
229
230 pub fn sdo_write(
235 &mut self,
236 client: &mut CommandClient,
237 index: u16,
238 sub_index: u8,
239 value: serde_json::Value,
240 ) -> u32 {
241 self.sdo.write(client, index, sub_index, value)
242 }
243
244 fn progress_sdo(&mut self, client: &mut CommandClient) {
249 let tid = match self.pending_tid {
250 Some(t) => t,
251 None => return,
252 };
253
254 let result = self.sdo.result(client, tid, SDO_TIMEOUT);
255 match result {
256 SdoResult::Pending => {} SdoResult::Ok(_) => match self.state {
258 State::WritingMvV => {
259 self.configured_mv_v = Some(self.pending_mv_v);
260 let next_tid = self.sdo.write(
261 client, SDO_IDX, SUB_FULL_SCALE, json!(self.pending_full_scale),
262 );
263 self.pending_tid = Some(next_tid);
264 self.state = State::WritingFullScale;
265 }
266 State::WritingFullScale => {
267 self.configured_full_scale_load = Some(self.pending_full_scale);
268 let next_tid = self.sdo.write(
269 client, SDO_IDX, SUB_SCALE_FACTOR, json!(self.pending_scale_factor),
270 );
271 self.pending_tid = Some(next_tid);
272 self.state = State::WritingScaleFactor;
273 }
274 State::WritingScaleFactor => {
275 self.configured_scale_factor = Some(self.pending_scale_factor);
276 self.pending_tid = None;
277 self.state = State::Idle;
278 self.busy = false;
279 }
280 State::Idle => {
281 self.pending_tid = None;
283 }
284 },
285 SdoResult::Err(e) => {
286 self.set_error(&format!("SDO {:?} failed: {}", self.state, e));
287 }
288 SdoResult::Timeout => {
289 self.set_error(&format!("SDO {:?} timed out after {:?}", self.state, SDO_TIMEOUT));
290 }
291 }
292 }
293
294 fn set_error(&mut self, message: &str) {
295 log::error!("El3356: {}", message);
296 self.error = true;
297 self.error_message = message.to_string();
298 self.pending_tid = None;
299 self.state = State::Idle;
300 self.busy = false;
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use mechutil::ipc::CommandMessage;
308 use tokio::sync::mpsc;
309
310 #[derive(Default)]
313 struct TestPdo {
314 tare: bool,
315 load: f32,
316 load_steady: bool,
317 load_error: bool,
318 load_overrange: bool,
319 }
320
321 impl TestPdo {
322 fn view(&mut self) -> El3356View<'_> {
323 El3356View {
324 tare: &mut self.tare,
325 load: &self.load,
326 load_steady: &self.load_steady,
327 load_error: &self.load_error,
328 load_overrange: &self.load_overrange,
329 }
330 }
331 }
332
333 fn test_client() -> (
334 CommandClient,
335 mpsc::UnboundedSender<CommandMessage>,
336 mpsc::UnboundedReceiver<String>,
337 ) {
338 let (write_tx, write_rx) = mpsc::unbounded_channel();
339 let (response_tx, response_rx) = mpsc::unbounded_channel();
340 let client = CommandClient::new(write_tx, response_rx);
341 (client, response_tx, write_rx)
342 }
343
344 fn last_sent_tid(rx: &mut mpsc::UnboundedReceiver<String>) -> u32 {
346 let msg_json = rx.try_recv().expect("expected a message on the wire");
347 let msg: CommandMessage = serde_json::from_str(&msg_json).unwrap();
348 msg.transaction_id
349 }
350
351 fn assert_last_sent(
352 rx: &mut mpsc::UnboundedReceiver<String>,
353 expected_topic: &str,
354 expected_sub: u8,
355 ) -> u32 {
356 let msg_json = rx.try_recv().expect("expected a message on the wire");
357 let msg: CommandMessage = serde_json::from_str(&msg_json).unwrap();
358 assert_eq!(msg.topic, expected_topic);
359 assert_eq!(msg.data["index"], format!("0x{:04X}", SDO_IDX));
360 assert_eq!(msg.data["sub"], expected_sub);
361 msg.transaction_id
362 }
363
364 #[test]
367 fn peak_follows_largest_magnitude() {
368 let (mut client, _resp_tx, _write_rx) = test_client();
369 let mut fb = El3356::new("EL3356_0");
370 let mut pdo = TestPdo::default();
371
372 pdo.load = 10.0;
375 fb.tick(&mut pdo.view(), &mut client);
376 assert_eq!(fb.peak_load, 10.0);
377
378 pdo.load = -25.0;
379 fb.tick(&mut pdo.view(), &mut client);
380 assert_eq!(fb.peak_load, -25.0);
381
382 pdo.load = 20.0; fb.tick(&mut pdo.view(), &mut client);
384 assert_eq!(fb.peak_load, -25.0);
385 }
386
387 #[test]
388 fn reset_peak_zeroes_it() {
389 let (mut client, _resp_tx, _write_rx) = test_client();
390 let mut fb = El3356::new("EL3356_0");
391 let mut pdo = TestPdo { load: 42.0, ..Default::default() };
392 fb.tick(&mut pdo.view(), &mut client);
393 assert_eq!(fb.peak_load, 42.0);
394 fb.reset_peak();
395 assert_eq!(fb.peak_load, 0.0);
396 }
397
398 #[test]
401 fn tare_resets_peak_and_pulses_bit() {
402 let (mut client, _resp_tx, _write_rx) = test_client();
403 let mut fb = El3356::new("EL3356_0");
404 let mut pdo = TestPdo { load: 50.0, ..Default::default() };
405
406 fb.tick(&mut pdo.view(), &mut client);
407 assert_eq!(fb.peak_load, 50.0);
408
409 fb.tare();
410 assert_eq!(fb.peak_load, 0.0);
412
413 fb.tick(&mut pdo.view(), &mut client);
415 assert!(pdo.tare, "tare bit should be high within pulse window");
416
417 std::thread::sleep(TARE_PULSE + Duration::from_millis(20));
419 fb.tick(&mut pdo.view(), &mut client);
420 assert!(!pdo.tare, "tare bit should be cleared after pulse window");
421 }
422
423 #[test]
426 fn configure_sequences_three_sdo_writes() {
427 let (mut client, resp_tx, mut write_rx) = test_client();
428 let mut fb = El3356::new("EL3356_0");
429 let mut pdo = TestPdo::default();
430
431 fb.configure(&mut client, 1000.0, 2.0, 100000.0);
432 assert!(fb.busy);
433 assert_eq!(fb.state, State::WritingMvV);
434
435 let tid1 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_MV_V);
437 resp_tx.send(CommandMessage::response(tid1, json!(null))).unwrap();
438 client.poll();
439 fb.tick(&mut pdo.view(), &mut client);
440 assert_eq!(fb.configured_mv_v, Some(2.0));
441 assert_eq!(fb.state, State::WritingFullScale);
442 assert!(fb.busy);
443
444 let tid2 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_FULL_SCALE);
446 resp_tx.send(CommandMessage::response(tid2, json!(null))).unwrap();
447 client.poll();
448 fb.tick(&mut pdo.view(), &mut client);
449 assert_eq!(fb.configured_full_scale_load, Some(1000.0));
450 assert_eq!(fb.state, State::WritingScaleFactor);
451 assert!(fb.busy);
452
453 let tid3 = assert_last_sent(&mut write_rx, "ethercat.write_sdo", SUB_SCALE_FACTOR);
455 resp_tx.send(CommandMessage::response(tid3, json!(null))).unwrap();
456 client.poll();
457 fb.tick(&mut pdo.view(), &mut client);
458 assert_eq!(fb.configured_scale_factor, Some(100000.0));
459 assert_eq!(fb.state, State::Idle);
460 assert!(!fb.busy);
461 assert!(!fb.error);
462 }
463
464 #[test]
465 fn configure_while_busy_is_noop() {
466 let (mut client, _resp_tx, mut write_rx) = test_client();
467 let mut fb = El3356::new("EL3356_0");
468
469 fb.configure(&mut client, 1000.0, 2.0, 100000.0);
470 let _tid1 = last_sent_tid(&mut write_rx);
471 assert!(fb.busy);
472
473 fb.configure(&mut client, 9999.0, 9.0, 99.0);
475 assert!(write_rx.try_recv().is_err(), "no new message should have been sent");
476 assert_eq!(fb.pending_mv_v, 2.0);
477 }
478
479 #[test]
480 fn sdo_error_sets_error_and_clears_busy() {
481 let (mut client, resp_tx, mut write_rx) = test_client();
482 let mut fb = El3356::new("EL3356_0");
483 let mut pdo = TestPdo::default();
484
485 fb.configure(&mut client, 1000.0, 2.0, 100000.0);
486 let tid1 = last_sent_tid(&mut write_rx);
487
488 let mut err_msg = CommandMessage::response(tid1, json!(null));
490 err_msg.success = false;
491 err_msg.error_message = "device offline".to_string();
492 resp_tx.send(err_msg).unwrap();
493 client.poll();
494
495 fb.tick(&mut pdo.view(), &mut client);
496 assert!(fb.error);
497 assert!(fb.error_message.contains("device offline"));
498 assert!(!fb.busy);
499 assert_eq!(fb.state, State::Idle);
500 }
501
502 #[test]
503 fn clear_error_resets_flag() {
504 let mut fb = El3356::new("EL3356_0");
505 fb.error = true;
506 fb.error_message = "boom".to_string();
507 fb.clear_error();
508 assert!(!fb.error);
509 assert!(fb.error_message.is_empty());
510 }
511}