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}