From 1778c7155b2c43ec01d698cc502077917d42dd47 Mon Sep 17 00:00:00 2001 From: Eisverygoodletter <76079042+Eisverygoodletter@users.noreply.github.com> Date: Sun, 8 Dec 2024 07:53:57 +1100 Subject: [PATCH] Add the RegisterRepresentable trait and related functions (#37) * added RegisterRepresentable trait to allow writing/reading arbitrary types * Added tests for RegisterRepresentable and related representations * Added RegisterBuffer trait and tests for it * Added representable::representations re-export to crate::server * Add more documentation for representable * Converted new function implementation into defaults in the trait. This means this is no longer a breaking change --- README.md | 8 ++ src/server/context.rs | 58 ++++++++++ src/server/mod.rs | 2 + src/server/representable.rs | 209 ++++++++++++++++++++++++++++++++++++ src/tests/test_std.rs | 177 ++++++++++++++++++++++++++++++ 5 files changed, 454 insertions(+) create mode 100644 src/server/representable.rs diff --git a/README.md b/README.md index 02b8839..bebe2f0 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,14 @@ let context = ModbusStorage::<128, 16, 0, 100>::new(); Starting from the version 0.9 it is allowed to provide custom server implementation by implementing `use rmodbus::server::context::ModbusContext` on custom struct. For sample implementation have a look at `src/server/storage.rs` + +## Custom type representations in `u16` sized registers + +Starting from version \, you can implement +`server::RegisterRepresentable` on your own types and use +`ModbusContext::set_*_as_representable` and `ModbusContext::get_*_as_representable` +methods to directly store and read your own types in the registers. + ## Vectors Some of rmodbus functions use vectors to store result. Different vector types can be used: diff --git a/src/server/context.rs b/src/server/context.rs index 800b091..9e43cfa 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -1,5 +1,7 @@ use crate::{ErrorKind, VectorTrait}; +use super::representable::RegisterRepresentable; + #[allow(clippy::module_name_repetitions)] pub trait ModbusContext { /// Get inputs as Vec of u8 @@ -218,4 +220,60 @@ pub trait ModbusContext { /// Set IEEE 754 f32 to two holding registers fn set_holdings_from_f32(&mut self, reg: u16, value: f32) -> Result<(), ErrorKind>; + + /// Get N inputs represented as some [`RegisterRepresentable`] type T + /// + /// Returns the [`RegisterRepresentable`] once converted using + /// [`RegisterRepresentable::from_registers_sequential`] + fn get_inputs_as_representable>( + &self, + reg: u16, + ) -> Result { + let mut regs: [u16; N] = [0u16; N]; + for i in 0..N { + regs[i] = self.get_input(reg + i as u16)?; + } + Ok(T::from_registers_sequential(®s)) + } + + /// Get N holdings represented as some [`RegisterRepresentable`] type T. + /// + /// Returns the [`RegisterRepresentable`] once converted using + /// [`RegisterRepresentable::from_registers_sequential`] + fn get_holdings_as_representable>( + &self, + reg: u16, + ) -> Result { + let mut regs: [u16; N] = [0u16; N]; + for i in 0..N { + regs[i] = self.get_holding(reg + i as u16)?; + } + Ok(T::from_registers_sequential(®s)) + } + + /// Set N inputs using a [`RegisterRepresentable`]. + /// + /// Uses [`RegisterRepresentable::to_registers_sequential`] to convert + /// type T into a sequence of [`u16`] registers. + fn set_inputs_from_representable>( + &mut self, + reg: u16, + value: &T, + ) -> Result<(), ErrorKind> { + let regs = value.to_registers_sequential(); + self.set_inputs_bulk(reg, ®s) + } + + /// Set N holdings using a [`RegisterRepresentable`]. + /// + /// Uses [`RegisterRepresentable::to_registers_sequential`] to convert + /// type T into a sequence of [`u16`] registers. + fn set_holdings_from_representable>( + &mut self, + reg: u16, + value: &T, + ) -> Result<(), ErrorKind> { + let regs = value.to_registers_sequential(); + self.set_holdings_bulk(reg, ®s) + } } diff --git a/src/server/mod.rs b/src/server/mod.rs index cf250ea..69ea5ce 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,7 +1,9 @@ pub mod context; +pub mod representable; pub mod storage; use core::slice; +pub use representable::representations; #[allow(clippy::wildcard_imports)] use crate::consts::*; diff --git a/src/server/representable.rs b/src/server/representable.rs new file mode 100644 index 0000000..ac7c59f --- /dev/null +++ b/src/server/representable.rs @@ -0,0 +1,209 @@ +/// Implemented for structs that can be represented using u16 registers. +/// It is highly recommended that implementors of this type ensure that +/// [`RegisterRepresentable::to_registers_sequential`] and +/// [`RegisterRepresentable::from_registers_sequential`] are exact +/// inverses of each other. +pub trait RegisterRepresentable { + /// Convert this type into a sequence of `u16`s which can be loaded + /// into modbus registers. (From lower to higher addresses) + fn to_registers_sequential(&self) -> [u16; N]; + /// Extract this type from a sequence of `u16`s taken from sequential + /// modbus registers. (From lower to higher addresses) + fn from_registers_sequential(value: &[u16; N]) -> Self; +} + +/// The other side of [`RegisterRepresentable`], similar to how the +/// [`Into`] trait is the other side of Rust's [`From`] trait. This +/// trait is implemented on u16 buffers that can be converted to/from a +/// [`RegisterRepresentable`] type. +/// +/// This trait is automatically implemented using a blanket impl. Do not +/// implement this trait manually. +pub trait RegisterBuffer> { + /// Convert this buffer into the represented type. + fn to_represented(&self) -> T; + /// Convert the represented type into an instance of this buffer. + fn from_represented(value: &T) -> Self; + /// Convert the represented type to its u16 registers representation, + /// then copy that value into this buffer. + fn copy_from_represented(&mut self, value: &T); +} + +impl> RegisterBuffer for [u16; N] { + fn to_represented(&self) -> T { + T::from_registers_sequential(self) + } + fn from_represented(value: &T) -> Self { + value.to_registers_sequential() + } + fn copy_from_represented(&mut self, value: &T) { + self.copy_from_slice(value.to_registers_sequential().as_slice()); + } +} + +pub mod representations { + //! This module contains little and big endian implementations of + //! storing [`u32`] and [`u64`]s in [`u16`] sized registers. + //! + //! Note that "Big Endian" and "Little Endian" in this context means that + //! the value is big/small endian with respect to 16 bit words + //! (registers). This means a `u32` like `0x1122_3344` would be `3344 1122` + //! in little endian, not `4433 2211`. + //! The bytes in each word are big endian with respect to each other in the + //! word. + //! + //! # Example + //! ```rust + //! # use rmodbus::server::storage::ModbusStorageFull; + //! # use rmodbus::server::representations::{ U32LittleEndian, U32BigEndian }; + //! # use rmodbus::server::representable::*; + //! # use rmodbus::server::context::ModbusContext; + //! // note: in an actual application you would initialise the context globally + //! // or in some other way. + //! let mut ctx = ModbusStorageFull::default(); + //! let to_be_stored: u32 = 123u32; + //! // store the value as little endian at address 1. + //! ctx.set_inputs_from_representable(1, &U32LittleEndian(to_be_stored)).unwrap(); + //! // we can read it back from address 1 + //! let read: U32LittleEndian = ctx.get_inputs_as_representable(1).unwrap(); + //! assert_eq!(U32LittleEndian(to_be_stored), read); + //! // we can read it back using big endian as well, but this would give us the + //! // value with first and last 16 bits swapped + //! let read: U32BigEndian = ctx.get_inputs_as_representable(1).unwrap(); + //! assert_eq!(to_be_stored, (read.0 << 16) | (read.0 >> 16)); + //! ``` + use super::RegisterRepresentable; + #[cfg(feature = "with_bincode")] + use bincode::{Decode, Encode}; + #[cfg(feature = "with_serde")] + use serde::{Deserialize, Serialize}; + + /// A [`u32`] represented in 2 [`u16`] registers in big endian. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "with_serde", derive(Deserialize, Serialize))] + #[cfg_attr(feature = "with_bincode", derive(Decode, Encode))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct U32BigEndian(pub u32); + impl RegisterRepresentable<2> for U32BigEndian { + fn to_registers_sequential(&self) -> [u16; 2] { + [(self.0 >> 16) as u16, self.0 as u16] + } + fn from_registers_sequential(value: &[u16; 2]) -> Self { + Self(((value[0] as u32) << 16) | (value[1] as u32)) + } + } + + /// A [`u32`] represented in 2 [`u16`] registers in little endian. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "with_serde", derive(Deserialize, Serialize))] + #[cfg_attr(feature = "with_bincode", derive(Decode, Encode))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct U32LittleEndian(pub u32); + impl RegisterRepresentable<2> for U32LittleEndian { + fn to_registers_sequential(&self) -> [u16; 2] { + [self.0 as u16, (self.0 >> 16) as u16] + } + fn from_registers_sequential(value: &[u16; 2]) -> Self { + Self((value[0] as u32) | ((value[1] as u32) << 16)) + } + } + + /// A [`u64`] represented in 4 [`u16`] registers in big endian. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "with_serde", derive(Deserialize, Serialize))] + #[cfg_attr(feature = "with_bincode", derive(Decode, Encode))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct U64BigEndian(pub u64); + impl RegisterRepresentable<4> for U64BigEndian { + fn to_registers_sequential(&self) -> [u16; 4] { + [ + ((self.0 & 0xFFFF_0000_0000_0000) >> 48) as u16, + ((self.0 & 0x0000_FFFF_0000_0000) >> 32) as u16, + ((self.0 & 0x0000_0000_FFFF_0000) >> 16) as u16, + self.0 as u16, + ] + } + fn from_registers_sequential(value: &[u16; 4]) -> Self { + Self( + (value[0] as u64) << 48 + | (value[1] as u64) << 32 + | (value[2] as u64) << 16 + | (value[3] as u64), + ) + } + } + /// A [`u64`] represented in 4 [`u16`] registers in little endian. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "with_serde", derive(Deserialize, Serialize))] + #[cfg_attr(feature = "with_bincode", derive(Decode, Encode))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct U64LittleEndian(pub u64); + impl RegisterRepresentable<4> for U64LittleEndian { + fn to_registers_sequential(&self) -> [u16; 4] { + [ + self.0 as u16, + ((self.0 & 0x0000_0000_FFFF_0000) >> 16) as u16, + ((self.0 & 0x0000_FFFF_0000_0000) >> 32) as u16, + ((self.0 & 0xFFFF_0000_0000_0000) >> 48) as u16, + ] + } + fn from_registers_sequential(value: &[u16; 4]) -> Self { + Self( + (value[0] as u64) + | (value[1] as u64) << 16 + | (value[2] as u64) << 32 + | (value[3] as u64) << 48, + ) + } + } + + /// Tests specifically for the 4 representations provided + #[cfg(test)] + mod tests { + use super::super::RegisterBuffer; + #[allow(clippy::wildcard_imports)] + use super::*; + #[test] + fn test_u32_big_small_endian() { + let value: u32 = 0x1111_2222; + let big_endian = U32BigEndian(value).to_registers_sequential(); + assert_eq!( + <[u16; 2] as RegisterBuffer<2, U32BigEndian>>::to_represented(&big_endian), + U32BigEndian(value) + ); + let little_endian = U32LittleEndian(value).to_registers_sequential(); + assert_eq!( + <[u16; 2] as RegisterBuffer<2, U32LittleEndian>>::to_represented(&little_endian), + U32LittleEndian(value) + ); + assert_eq!(big_endian[0], little_endian[1]); + assert_eq!(big_endian[1], little_endian[0]); + + assert_eq!(little_endian[0], 0x2222u16); + assert_eq!(little_endian[1], 0x1111u16); + } + #[test] + fn test_u64_big_small_endian() { + let value: u64 = 0x1111_2222_3333_4444; + let big_endian = U64BigEndian(value).to_registers_sequential(); + assert_eq!( + <[u16; 4] as RegisterBuffer<4, U64BigEndian>>::to_represented(&big_endian), + U64BigEndian(value) + ); + let little_endian = U64LittleEndian(value).to_registers_sequential(); + assert_eq!( + <[u16; 4] as RegisterBuffer<4, U64LittleEndian>>::to_represented(&little_endian), + U64LittleEndian(value) + ); + assert_eq!(big_endian[0], little_endian[3]); + assert_eq!(big_endian[1], little_endian[2]); + assert_eq!(big_endian[2], little_endian[1]); + assert_eq!(big_endian[3], little_endian[0]); + + assert_eq!(little_endian[0], 0x4444u16); + assert_eq!(little_endian[1], 0x3333u16); + assert_eq!(little_endian[2], 0x2222u16); + assert_eq!(little_endian[3], 0x1111u16); + } + } +} diff --git a/src/tests/test_std.rs b/src/tests/test_std.rs index 184775b..e5651dd 100644 --- a/src/tests/test_std.rs +++ b/src/tests/test_std.rs @@ -9,6 +9,7 @@ use crate::*; #[allow(clippy::wildcard_imports)] use crc16::*; use once_cell::sync::Lazy; +use representable::representations; use std::sync::RwLock; static CTX: Lazy> = Lazy::new(<_>::default); @@ -214,6 +215,62 @@ fn test_std_read_inputs_as_bytes_oob() { } ctx.set_inputs_from_u64(u16::try_from(STORAGE_SIZE - 4).unwrap(), 0x9999) .unwrap(); + if ctx + .set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 1).unwrap(), + &representations::U32LittleEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 2).unwrap(), + &representations::U32LittleEndian(0x55), + ) + .unwrap(); + if ctx + .set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 1).unwrap(), + &representations::U32BigEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 2).unwrap(), + &representations::U32BigEndian(0x55), + ) + .unwrap(); + if ctx + .set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 3).unwrap(), + &representations::U64LittleEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 4).unwrap(), + &representations::U64LittleEndian(0x55), + ) + .unwrap(); + if ctx + .set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 3).unwrap(), + &representations::U64BigEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_inputs_from_representable( + u16::try_from(STORAGE_SIZE - 4).unwrap(), + &representations::U64BigEndian(0x55), + ) + .unwrap(); } #[test] @@ -252,6 +309,38 @@ fn test_std_get_set_inputs() { ); ctx.set_inputs_from_f32(200, 1234.567).unwrap(); assert_eq!(ctx.get_inputs_as_f32(200).unwrap(), 1234.567f32); + + ctx.set_inputs_from_representable(300, &representations::U32LittleEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_inputs_as_representable::<2, representations::U32LittleEndian>(300) + .unwrap(), + representations::U32LittleEndian(1234567) + ); + + ctx.set_inputs_from_representable(300, &representations::U32BigEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_inputs_as_representable::<2, representations::U32BigEndian>(300) + .unwrap(), + representations::U32BigEndian(1234567) + ); + + ctx.set_inputs_from_representable(300, &representations::U64BigEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_inputs_as_representable::<4, representations::U64BigEndian>(300) + .unwrap(), + representations::U64BigEndian(1234567) + ); + + ctx.set_inputs_from_representable(300, &representations::U64LittleEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_inputs_as_representable::<4, representations::U64LittleEndian>(300) + .unwrap(), + representations::U64LittleEndian(1234567) + ); } #[test] @@ -310,6 +399,62 @@ fn test_std_read_holdings_as_bytes_oob() { } ctx.set_holdings_from_u64(u16::try_from(STORAGE_SIZE - 4).unwrap(), 0x9999) .unwrap(); + if ctx + .set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 1).unwrap(), + &representations::U32LittleEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 2).unwrap(), + &representations::U32LittleEndian(0x55), + ) + .unwrap(); + if ctx + .set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 1).unwrap(), + &representations::U32BigEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 2).unwrap(), + &representations::U32BigEndian(0x55), + ) + .unwrap(); + if ctx + .set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 3).unwrap(), + &representations::U64LittleEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 4).unwrap(), + &representations::U64LittleEndian(0x55), + ) + .unwrap(); + if ctx + .set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 3).unwrap(), + &representations::U64BigEndian(0x55), + ) + .is_ok() + { + panic!("{}", "oob failed MAX RegisterRepresentable U32BigEndian") + } + ctx.set_holdings_from_representable( + u16::try_from(STORAGE_SIZE - 4).unwrap(), + &representations::U64BigEndian(0x55), + ) + .unwrap(); } #[test] @@ -348,6 +493,38 @@ fn test_std_get_set_holdings() { ); ctx.set_holdings_from_f32(200, 1234.567).unwrap(); assert_eq!(ctx.get_holdings_as_f32(200).unwrap(), 1234.567f32); + + ctx.set_holdings_from_representable(300, &representations::U32LittleEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_holdings_as_representable::<2, representations::U32LittleEndian>(300) + .unwrap(), + representations::U32LittleEndian(1234567) + ); + + ctx.set_holdings_from_representable(300, &representations::U32BigEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_holdings_as_representable::<2, representations::U32BigEndian>(300) + .unwrap(), + representations::U32BigEndian(1234567) + ); + + ctx.set_holdings_from_representable(300, &representations::U64BigEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_holdings_as_representable::<4, representations::U64BigEndian>(300) + .unwrap(), + representations::U64BigEndian(1234567) + ); + + ctx.set_holdings_from_representable(300, &representations::U64LittleEndian(1234567)) + .unwrap(); + assert_eq!( + ctx.get_holdings_as_representable::<4, representations::U64LittleEndian>(300) + .unwrap(), + representations::U64LittleEndian(1234567) + ); } #[test]