zproto/binary.rs
1//! Types and traits for communicating with Zaber products with Zaber's [Binary protocol](https://www.zaber.com/protocol-manual?protocol=Binary).
2//!
3//! The binary protocol is **deprecated** and Zaber recommends the [`ascii`](crate::ascii) protocol instead.
4//!
5//! ## Communicating with a Device
6//!
7//! All communication with Zaber products starts with a [`Port`], which can be either a serial or TCP port:
8//!
9//! ```
10//! # use zproto::binary::Port;
11//! # fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
12//! let mut port = Port::open_serial("/dev/ttyUSB0")?;
13//! // OR
14//! let mut port = Port::open_tcp("192.168.0.1:55550")?;
15//! # Ok(())
16//! # }
17//! ```
18//!
19//! You can then transmit a command to and receive a reply from devices
20//! connected to that port with the [`tx_recv`](Port::tx_recv) method, or any of the
21//! other `tx_recv*` methods. Commands are constructed as tuples:
22//! * Commands that do not require data take the form `(u8, Command)`, where
23//! * `u8` is the device address and
24//! * `Command` is some type that implements [`Command`](traits::Command) (the
25//! constants in the [`command`] module are highly recommended but you can
26//! also use a `u8`).
27//!
28//! ```
29//! # use zproto::{
30//! # binary::Port,
31//! # backend::Backend,
32//! # };
33//! # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
34//! // Send the Home command to all devices (address 0)
35//! use zproto::binary::command::HOME;
36//! let reply = port.tx_recv((0, HOME))?;
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! * Commands that do require data take a form similar to above, `(u8, Command, Data)`,
42//! where `u8` and `Command` are the same and `Data` is some type that implements
43//! [`Data`](traits::Data). This includes types like `i32`, `bool`, [`command`],
44//! and other types.
45//!
46//! ```
47//! # use zproto::{
48//! # binary::Port,
49//! # backend::Backend,
50//! # };
51//! # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
52//! // Move device 1 to the absolute position 10,000.
53//! use zproto::binary::command::*;
54//! let reply = port.tx_recv((1, MOVE_ABSOLUTE, 10000))?;
55//! // OR
56//! let reply = port.tx_recv((0, RETURN_SETTING, SET_TARGET_SPEED))?;
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! If the response is an [`ERROR`](command::ERROR) or is otherwise unexpected
62//! (e.g., from a different device) an error will be returned.
63//!
64//! **NOTE**: Address aliases other than 0 (all devices) are presently not
65//! supported. Responses to such messages will be treated as unexpected by the
66//! [`tx_recv*`](Port::tx_recv) methods.
67//!
68//! ## Other `Port` Methods
69//!
70//! [`tx_recv`](Port::tx_recv) works great when you need to send one command and
71//! receive one response. But, when you have a chain of devices, you may need to
72//! read a response from all devices in the chain. When you know the number of
73//! devices in the chain, you can use [`tx_recv_n`](Port::tx_recv_n) to read `n`
74//! responses to a command. If you don't know the number of devices in the chain
75//! you can use [`tx_recv_until_timeout`](Port::tx_recv_until_timeout) to read as
76//! many responses as possible.
77//!
78//! Sometimes you only want to transmit or receive data, but not both. In those
79//! cases the [`tx`](Port::tx), [`recv`](Port::recv), [`recv_n`](Port::recv_n),
80//! and [`recv_until_timeout`](Port::recv_until_timeout) are very helpful.
81//!
82//! Finally, a common pattern is to poll a device until it is in a desired
83//! state, which you can do with the [`poll_until`](Port::poll_until) and
84//! [`poll_until_idle`](Port::poll_until_idle) methods.
85//!
86//! ## Reading Data
87//!
88//! Reading data from a response is as simple as calling [`data()`](Message::data)
89//! on the response. If the command was sent using the strongly typed commands
90//! in the [`command`] module, the library will pick the correct type conversion
91//! for you at compile time.
92//!
93//! ```
94//! # use zproto::{
95//! # binary::Port,
96//! # backend::Backend,
97//! # };
98//! # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
99//! use zproto::binary::command::*;
100//! let reply = port.tx_recv((0, RETURN_SETTING, SET_TARGET_SPEED))?;
101//! let speed = reply.data()?; // `speed` is an `i32`
102//! let reply = port.tx_recv((0, RETURN_SETTING, SET_HOME_STATUS))?;
103//! let homed = reply.data()?; // `homed` is a `bool`.
104//! # Ok(())
105//! # }
106//! ```
107//!
108//! ## Sending a Port between threads
109//!
110//! By default a [`Port`] does not implement `Send`, so cannot be sent to another
111//! thread. If you're application requires this, use the [`Port::try_into_send`]
112//! method to convert it to one that can be. Doing so places [`Send`] bounds on
113//! any [packet](Port::set_packet_handler) handlers.
114//!
115//! ```
116//! # use zproto::ascii::Port;
117//! # fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
118//! let sendable_port = Port::open_serial("...")?
119//! .try_into_send()
120//! .unwrap();
121//! # Ok(())
122//! # }
123//! ```
124//!
125//! ## Type Safety
126//!
127//! The library uses Rust's type system and the strongly-typed commands in the
128//! [`command`] module to help enforce proper message structure and avoid
129//! run-time bugs. For instance, not passing data to commands that require data,
130//! or passing the wrong data type, will result in a compile time error.
131//!
132//! ```compile_fail
133//! # use zproto::{
134//! # binary::Port,
135//! # backend::Backend,
136//! # };
137//! # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
138//! use zproto::binary::command::*;
139//! let reply = port.tx_recv((0, MOVE_ABSOLUTE))?; // ERROR: the data field is missing!
140//! let reply = port.tx_recv((0, MOVE_ABSOLUTE, true))?; // ERROR: the data has the incorrect type!
141//! let reply = port.tx_recv((0, RESET)); // ERROR: Devices do not respond to the RESET command!
142//! # Ok(())
143//! # }
144//! ```
145//!
146//! To do this, each command in the [`command`] module is given a unique type
147//! that implement a set of [`traits`]. These tell the compiler what kind of
148//! data the command takes, what kind of data it returns, and if it returns a
149//! response at all. However, this means that the commands cannot be swapped out
150//! for one another at run time or stored in a collection.
151//!
152//! ```compile_fail
153//! // This will not compile because SET_TARGET_SPEED and SET_ACCELERATION
154//! // are different types.
155//! use zproto::binary::command::*;
156//! let commands = [(0, SET_TARGET_SPEED, 10000), (0, SET_ACCELERATION, 5000)];
157//! ```
158//!
159//! To accommodate situations like this, the library provides "untyped" versions
160//! of the command in the [`command::untyped`] module. The commands in that
161//! module are simply `u8`s and can be used anywhere their strongly-typed
162//! counterparts can.
163//!
164//! ```
165//! // This works now.
166//! use zproto::binary::command::untyped::*;
167//! let commands = [(0, SET_TARGET_SPEED, 10000), (0, SET_ACCELERATION, 5000)];
168//! ```
169//!
170//! However, you will not have any of the nice compile time type safety or
171//! automatic type conversion -- you will have to pick the types yourself.
172//!
173//! ```
174//! # use zproto::{
175//! # binary::Port,
176//! # backend::Backend,
177//! # };
178//! # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
179//! use zproto::binary::command::untyped::*;
180//! let reply = port.tx_recv((0, RETURN_SETTING, SET_TARGET_SPEED))?;
181//! let speed: i32 = reply.data()?; // Notice that the type must be specified.
182//! // The compiler cannot tell if you have picked the wrong type so this will
183//! // compile, but the data conversion will fail at run time.
184//! let speed: bool = reply.data()?;
185//! # Ok(())
186//! # }
187//! ```
188//!
189//! ### Handling Compile-time Errors
190//!
191//! The traits in this crate are named so that their meaning is hopefully clear,
192//! but, nevertheless, the compiler errors can sometimes be confusing. Here are
193//! some errors that you might see and suggestions for correcting them.
194//!
195//! #### `TakesData` is not Satisfied
196//!
197//! ```sh
198//! the trait `TakesData<{integer}>` is not implemented for ...
199//! ```
200//!
201//! There are two reasons for this error:
202//! 1. The command does not take any data argument. Try restructuring the message from `(address, command, data)` to `(address, command)`.
203//! 2. The command requires a data argument with a different type. Consult the [`documentation for the command`](command) and use the data type listed there.
204//!
205//! #### `TakesNoData` is not Satisfied
206//!
207//! ```sh
208//! the trait `TakesNoData` is not implemented for ...
209//! ```
210//!
211//! The command likely requires a data argument. Try restructuring the message from `(address, command)` to `(address, command, data)`.
212//!
213//! #### `ElicitsResponse` is not Satisfied
214//!
215//! ```sh
216//! the trait `ElicitsResponse` is not implemented for ...
217//! ```
218//!
219//! This means the command does not elicit a response from a device and the
220//! [`Port::tx_recv`] family of functions would eventually time out waiting for
221//! one. Try transmitting the command without reading a reply with
222//! [`tx`](Port::tx) instead.
223//!
224
225pub mod command;
226pub mod handlers;
227mod port;
228pub mod traits;
229
230use std::convert::Infallible;
231
232use crate::error;
233pub use port::*;
234
235/// A Binary Protocol message.
236#[derive(Debug, Copy, Clone, PartialEq, Eq)]
237pub struct Message<C = u8> {
238 /// The targeted device
239 target: u8,
240 /// The command code
241 command: C,
242 /// The command data
243 data: [u8; 4],
244 /// The message ID
245 id: Option<u8>,
246}
247
248impl<C> Message<C> {
249 /// Get the message target.
250 pub const fn target(&self) -> u8 {
251 self.target
252 }
253
254 /// Get the raw message data without decoding it.
255 pub const fn raw_data(&self) -> [u8; 4] {
256 self.data
257 }
258
259 /// Get the message id, if there is one.
260 pub const fn id(&self) -> Option<u8> {
261 self.id
262 }
263}
264
265impl<C: traits::Command> Message<C> {
266 /// Get the message command code.
267 pub fn command(&self) -> u8 {
268 self.command.command()
269 }
270
271 /// Get the decoded message data.
272 ///
273 /// The type `T` to decode to is determined by the implementation of
274 /// `ReplyData` for the command type `C`. For the types defined in
275 /// [`command`], there is only one data type. For other command types, like
276 /// `u8` (the type of the commands in [`command::untyped`]) any data type is
277 /// supported (though the conversion may fail at run time), so you will need
278 /// to specify the appropriate data type to decode.
279 ///
280 /// ## Example
281 ///
282 /// ```
283 /// # use zproto::{
284 /// # binary::Port,
285 /// # backend::Backend,
286 /// # };
287 /// # fn wrapper<B: Backend>(mut port: Port<B>) -> Result<(), Box<dyn std::error::Error>> {
288 /// use zproto::binary::command::*;
289 /// let reply = port.tx_recv((0, RETURN_SETTING, SET_HOME_STATUS))?;
290 /// let homed = reply.data()?; // `homed` is a `bool`
291 /// let reply = port.tx_recv((0, RETURN_SETTING, untyped::SET_HOME_STATUS))?;
292 /// let homed: bool = reply.data()?; // The data type must be explicitly defined here
293 /// # Ok(())
294 /// # }
295 /// ```
296 pub fn data<T>(&self) -> Result<T, T::Error>
297 where
298 T: traits::Data,
299 C: traits::ReplyData<T>,
300 {
301 T::try_from_data(self.data)
302 }
303
304 /// Get the message without any type constraints on it.
305 pub fn to_untyped(&self) -> Message<u8> {
306 Message {
307 target: self.target(),
308 command: self.command.command(),
309 data: self.data,
310 id: self.id,
311 }
312 }
313
314 /// Try to construct a strongly-typed `Message` from an "untyped" one.
315 pub fn try_from_untyped(
316 other: Message,
317 ) -> Result<Message<C>, error::BinaryUnexpectedCommandError> {
318 if let Ok(command) = C::try_from(other.command()) {
319 Ok(Message {
320 target: other.target,
321 command,
322 data: other.data,
323 id: other.id,
324 })
325 } else {
326 Err(error::BinaryUnexpectedCommandError::new(other))
327 }
328 }
329}
330
331impl Message<u8> {
332 /// Parse an array of 6 bytes into a [`Message`].
333 ///
334 /// Set `id` to `true` if the response is expected to contain a message ID.
335 pub(crate) const fn from_bytes(bytes: [u8; 6], id: bool) -> Message<u8> {
336 Message {
337 target: bytes[0],
338 command: bytes[1],
339 data: [bytes[2], bytes[3], bytes[4], if id { 0 } else { bytes[5] }],
340 id: if id { Some(bytes[5]) } else { None },
341 }
342 }
343}
344
345impl<C> std::fmt::Display for Message<C>
346where
347 C: traits::Command,
348{
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 write!(
351 f,
352 "[{}, {}, {}",
353 self.target,
354 self.command.command(),
355 i32::from_le_bytes(self.raw_data())
356 )?;
357 if let Some(id) = self.id() {
358 write!(f, "{id}]")
359 } else {
360 write!(f, "]")
361 }
362 }
363}
364
365/// Represents a version of firmware.
366///
367/// For convenience it is comparable to [`f32`].
368///
369/// ```
370/// # use zproto::binary::Version;
371/// let version = Version::new(7, 24).unwrap();
372/// assert_eq!(version, 7.24);
373/// ```
374#[derive(Debug, PartialEq, Eq, Copy, Clone)]
375pub struct Version(i32);
376
377impl Version {
378 /// Create a new version
379 pub fn new(major: i32, minor: i32) -> Result<Self, std::num::TryFromIntError> {
380 if minor % 100 == minor {
381 Ok(Version(major * 100 + minor % 100))
382 } else {
383 Err(u8::try_from(-1).unwrap_err())
384 }
385 }
386
387 /// Get the major version
388 pub fn major(&self) -> i32 {
389 self.0 / 100
390 }
391 /// Get the minor version
392 pub fn minor(&self) -> i32 {
393 self.0 % 100
394 }
395}
396
397impl traits::Data for Version {
398 type Error = Infallible;
399 fn fill_data(&self, _: &mut [u8]) {
400 // This type can never be sent
401 unimplemented!();
402 }
403 fn try_from_data(buffer: [u8; 4]) -> Result<Self, Self::Error>
404 where
405 Self: Sized,
406 {
407 Ok(Version(i32::try_from_data(buffer)?))
408 }
409}
410
411impl PartialEq<f32> for Version {
412 // Ignore clippy warnings about allocating just for comparison -- it is a
413 // false positive in this case.
414 #[allow(clippy::cmp_owned)]
415 fn eq(&self, other: &f32) -> bool {
416 f32::from(*self) == *other
417 }
418}
419
420impl PartialEq<Version> for f32 {
421 // Ignore clippy warnings about allocating just for comparison -- it is a
422 // false positive in this case.
423 #[allow(clippy::cmp_owned)]
424 fn eq(&self, other: &Version) -> bool {
425 *self == f32::from(*other)
426 }
427}
428
429impl From<Version> for f32 {
430 fn from(other: Version) -> Self {
431 // The major and minor versions are always going to be small enough
432 // that we don't need to worry about precision loss.
433 #![allow(clippy::cast_precision_loss)]
434 other.major() as f32 + other.minor() as f32 / 100f32
435 }
436}
437
438/// Define an enum for device status and all associated traits.
439macro_rules! define_status {
440 (pub enum $name:ident { $($variant:ident = $value:literal),+ $(,)? }) => {
441 /// The status of a device.
442 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
443 #[non_exhaustive]
444 #[allow(missing_docs)]
445 pub enum $name {
446 $($variant = $value),+
447 }
448
449 impl From<$name> for i32 {
450 fn from(value: $name) -> i32 {
451 value as i32
452 }
453 }
454
455 impl PartialEq<i32> for $name {
456 fn eq(&self, other: &i32) -> bool {
457 *self as i32 == *other
458 }
459 }
460
461 impl PartialEq<$name> for i32 {
462 fn eq(&self, other: &$name) -> bool {
463 other == self
464 }
465 }
466
467 impl TryFrom<i32> for $name {
468 type Error = std::num::TryFromIntError;
469
470 fn try_from(value: i32) -> Result<Self, Self::Error> {
471 match value {
472 $($value => Ok($name::$variant)),+
473 ,
474 _ => Err(u8::try_from(-1).unwrap_err()),
475 }
476 }
477 }
478
479 impl traits::Data for $name {
480 type Error = std::num::TryFromIntError;
481 fn fill_data(&self, _: &mut[u8]) {
482 // This data type should never be sent to a device.
483 unimplemented!();
484 }
485
486 fn try_from_data(buffer: [u8; 4]) -> Result<Self, Self::Error> {
487 $name::try_from(i32::from_le_bytes(buffer))
488 }
489 }
490 };
491}
492
493define_status! {
494 pub enum Status {
495 Idle = 0,
496 ExecutingHoming = 1,
497 ExecutingManualVelocityMove = 10,
498 ExecutingManualDisplacementMove = 11,
499 StalledStoppedOrDisplacedWhileStationary = 13,
500 ExecutingMoveStored = 18,
501 ExecutingMoveAbsolute = 20,
502 ExecutingMoveRelative = 21,
503 ExecutingMoveConstantSpeed = 22,
504 ExecutingStop = 23,
505 Parked = 65,
506 ExecutingMoveIndex = 78,
507 DriverDisabled = 90,
508 PeripheralInactive = 93,
509 ExecutingMotion = 99,
510 }
511}
512
513/// The state of multiple digital I/O channels.
514#[derive(Debug, PartialEq, Eq, Copy, Clone)]
515pub struct IoStates(u32);
516
517impl IoStates {
518 /// Return whether a channel is high.
519 ///
520 /// The `channel` index is one-based.
521 ///
522 /// # Panics
523 ///
524 /// This function will panic if `channel` is 0 or greater than 32.
525 pub fn is_high(&self, channel: usize) -> bool {
526 self.is_high_checked(channel)
527 .expect("`channel` must be in the range (1, 32)")
528 }
529
530 /// Return whether a channel is high.
531 ///
532 /// The `channel` index is one-based. `None` is returned if `channel` is 0
533 /// or greater than 32.
534 pub const fn is_high_checked(&self, channel: usize) -> Option<bool> {
535 if channel > 0 && channel <= 32 {
536 Some(self.0 & (1 << (channel - 1)) != 0)
537 } else {
538 None
539 }
540 }
541}
542
543impl traits::Data for IoStates {
544 type Error = Infallible;
545 fn fill_data(&self, _: &mut [u8]) {
546 // This type can never be sent
547 unimplemented!();
548 }
549 fn try_from_data(buffer: [u8; 4]) -> Result<Self, Self::Error>
550 where
551 Self: Sized,
552 {
553 // Data in a binary packet is always signed, so parse it as such first.
554 // It will always be a positive value so there is not concern about loss
555 // when converting it to a u32.
556 #[allow(clippy::cast_sign_loss)]
557 Ok(IoStates(i32::try_from_data(buffer)? as u32))
558 }
559}
560
561#[cfg(test)]
562mod test {
563 #[allow(clippy::wildcard_imports)]
564 use super::*;
565
566 #[test]
567 fn version() {
568 let version = Version(723);
569 assert_eq!(version.major(), 7);
570 assert_eq!(version.minor(), 23);
571
572 assert_eq!(version, 7.23);
573 assert_eq!(version, Version::new(7, 23).unwrap());
574
575 assert!(Version::new(7, 100).is_err());
576 }
577
578 #[test]
579 fn io_states() {
580 let states = IoStates(0b101);
581 assert!(states.is_high(1));
582 assert!(!states.is_high(2));
583 assert!(states.is_high(3));
584 assert!(!states.is_high(4));
585 assert!(!states.is_high(32));
586 }
587
588 #[test]
589 #[should_panic]
590 fn io_state_index_0() {
591 let states = IoStates(1);
592 states.is_high(0);
593 }
594
595 #[test]
596 #[should_panic]
597 fn io_state_index_gt_32() {
598 let states = IoStates(1);
599 states.is_high(33);
600 }
601
602 #[test]
603 fn device_message_parsing_invalid_command_code() {
604 use crate::binary::command::{types::*, untyped};
605
606 // Incorrect command value results in an error.
607 let err = Message::<SetHomeSpeed>::try_from_untyped(Message::from_bytes(
608 [3, untyped::SET_ACCELERATION, 0, 0, 0, 1],
609 false,
610 ))
611 .unwrap_err();
612 assert_eq!(
613 Message::from(err),
614 Message::from_bytes([3, untyped::SET_ACCELERATION, 0, 0, 0, 1], false)
615 );
616 }
617
618 #[test]
619 fn device_message_passing_with_id() {
620 use crate::binary::command::{types::*, untyped};
621
622 // Parsing a message with an ID works properly.
623 let message: Message<SetHomeSpeed> = Message::try_from_untyped(Message::from_bytes(
624 [3, untyped::SET_HOME_SPEED, 2, 0, 0, 1],
625 true,
626 ))
627 .unwrap();
628 assert_eq!(message.target(), 3);
629 assert_eq!(message.id(), Some(1));
630 assert_eq!(message.command(), untyped::SET_HOME_SPEED);
631 assert_eq!(message.data().unwrap(), 2);
632 }
633
634 #[test]
635 fn device_message_passing_without_id() {
636 use crate::binary::command::{types::*, untyped};
637
638 // Parsing a message without an ID works properly.
639 let message: Message<SetHomeSpeed> = Message::try_from_untyped(Message::from_bytes(
640 [3, untyped::SET_HOME_SPEED, 2, 0, 0, 1],
641 false,
642 ))
643 .unwrap();
644 assert_eq!(message.target(), 3);
645 assert_eq!(message.id(), None);
646 assert_eq!(message.command(), untyped::SET_HOME_SPEED);
647 assert_eq!(message.data().unwrap(), i32::from_le_bytes([2, 0, 0, 1]));
648 }
649}