embedded_devices/devices/sensirion/
commands.rs

1use core::marker::PhantomData;
2
3use embedded_interfaces::{TransportError, commands::Command, define_executor};
4use heapless::Vec;
5
6/// An error representing CRC errors.
7#[cfg_attr(feature = "defmt", derive(defmt::Format))]
8#[derive(Debug, thiserror::Error)]
9pub enum Crc8Error {
10    #[error("the calculated crc checksum {calculated:#02x} did not match the expected value {expected:#02x}")]
11    CrcMismatch { calculated: u8, expected: u8 },
12}
13
14/// Metadata associated to any sensirion specific command
15pub trait SensirionCommand {
16    /// Length of the command id in bytes
17    const COMMAND_ID_LEN: usize;
18    /// The command id.
19    const COMMAND_ID: u64;
20    /// This reflects the execution time for this command in milliseconds, if specified in the
21    /// datasheet.
22    const EXECUTION_TIME_MS: u32;
23}
24
25/// This trait represents a send-command as specified by Sensirion. It consists of sending a
26/// command id followed by waiting for a pre-defined amount of time.
27pub trait SensirionSendCommand:
28    SensirionCommand + Command<I2cExecutor = SensirionSendCommandExecutor<Self>, In = (), Out = ()>
29{
30}
31
32/// This trait represents a write-command as specified by Sensirion. It consists of sending a
33/// command id plus some data followed by waiting for a pre-defined amount of time.
34pub trait SensirionWriteCommand:
35    SensirionCommand + Command<I2cExecutor = SensirionWriteCommandExecutor<Self>, Out = ()>
36{
37}
38
39/// This trait represents a read-command as specified by Sensirion. It consists of sending a
40/// command id followed by a wait period and a final read.
41pub trait SensirionReadCommand:
42    SensirionCommand + Command<I2cExecutor = SensirionReadCommandExecutor<Self>, In = ()>
43{
44}
45
46/// This trait represents a write-read-command (also called fetch-command) as specified by
47/// Sensirion. It consists of sending a command id plus some data followed by a wait period and a
48/// final read.
49pub trait SensirionWriteReadCommand:
50    SensirionCommand + Command<I2cExecutor = SensirionWriteReadCommandExecutor<Self>>
51{
52}
53
54// Crc8Error is technically not needed for send and write, but we use it anyway to make
55// the interface more usable. Otherwise we'd need a ton of fallible error conversions.
56// FIXME: try to rework this once the never type is stable and we can express that this can't error, ever.
57define_executor!(SensirionSendCommandExecutor, SensirionSendCommand, Crc8Error);
58define_executor!(SensirionWriteCommandExecutor, SensirionWriteCommand, Crc8Error);
59define_executor!(SensirionReadCommandExecutor, SensirionReadCommand, Crc8Error);
60define_executor!(SensirionWriteReadCommandExecutor, SensirionWriteReadCommand, Crc8Error);
61
62#[maybe_async_cfg::maybe(
63    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), Executor, I2cBoundBus),
64    sync(feature = "sync"),
65    async(feature = "async"),
66    keep_self
67)]
68impl<C: SensirionSendCommand> embedded_interfaces::commands::i2c::Executor for SensirionSendCommandExecutor<C> {
69    async fn execute<D, I, A>(
70        delay: &mut D,
71        bound_bus: &mut embedded_interfaces::i2c::I2cBoundBus<I, A>,
72        _input: C::In,
73    ) -> Result<C::Out, TransportError<Self::Error, I::Error>>
74    where
75        D: hal::delay::DelayNs,
76        I: hal::i2c::I2c<A> + hal::i2c::ErrorType,
77        A: hal::i2c::AddressMode + Copy + core::fmt::Debug,
78    {
79        #[cfg(feature = "trace-communication")]
80        log::trace!(
81            "I2C write i2c_addr={:?} SensirionSendCommand(id={id:0w$x})",
82            bound_bus.address,
83            id = C::COMMAND_ID,
84            w = C::COMMAND_ID_LEN
85        );
86
87        let header = &C::COMMAND_ID.to_be_bytes()[core::mem::size_of_val(&C::COMMAND_ID) - C::COMMAND_ID_LEN..];
88        bound_bus.interface.write(bound_bus.address, header).await?;
89        delay.delay_ms(C::EXECUTION_TIME_MS).await;
90        Ok(())
91    }
92}
93
94#[maybe_async_cfg::maybe(
95    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), Executor, I2cBoundBus),
96    sync(feature = "sync"),
97    async(feature = "async"),
98    keep_self
99)]
100impl<C: SensirionWriteCommand> embedded_interfaces::commands::i2c::Executor for SensirionWriteCommandExecutor<C> {
101    async fn execute<D, I, A>(
102        delay: &mut D,
103        bound_bus: &mut embedded_interfaces::i2c::I2cBoundBus<I, A>,
104        input: C::In,
105    ) -> Result<C::Out, TransportError<Self::Error, I::Error>>
106    where
107        D: hal::delay::DelayNs,
108        I: hal::i2c::I2c<A> + hal::i2c::ErrorType,
109        A: hal::i2c::AddressMode + Copy + core::fmt::Debug,
110    {
111        const CHUNK_SIZE: usize = 2;
112
113        #[cfg(feature = "trace-communication")]
114        log::trace!(
115            "I2C write i2c_addr={:?} SensirionWriteCommand(id={id:0w$x}):\n{}",
116            bound_bus.address,
117            embedded_interfaces::BitdumpFormattable::bitdump(&input),
118            id = C::COMMAND_ID,
119            w = C::COMMAND_ID_LEN,
120        );
121
122        let header = &C::COMMAND_ID.to_be_bytes()[core::mem::size_of_val(&C::COMMAND_ID) - C::COMMAND_ID_LEN..];
123        let mut array = Vec::<u8, 64>::new();
124        array
125            .extend_from_slice(header)
126            .map_err(|_| TransportError::Unexpected("command id: too large for command buffer"))?;
127
128        let crc = crc::Crc::<u8>::new(&crc::CRC_8_NRSC_5);
129        let data = bytemuck::bytes_of(&input);
130        for x in data.chunks(CHUNK_SIZE) {
131            array
132                .extend_from_slice(x)
133                .map_err(|_| TransportError::Unexpected("command data: too large for buffer"))?;
134            array
135                .push(crc.checksum(x))
136                .map_err(|_| TransportError::Unexpected("command data: too large for buffer"))?;
137        }
138
139        bound_bus.interface.write(bound_bus.address, &array).await?;
140        delay.delay_ms(C::EXECUTION_TIME_MS).await;
141        Ok(())
142    }
143}
144
145#[maybe_async_cfg::maybe(
146    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), Executor, I2cBoundBus),
147    sync(feature = "sync"),
148    async(feature = "async"),
149    keep_self
150)]
151impl<C: SensirionReadCommand> embedded_interfaces::commands::i2c::Executor for SensirionReadCommandExecutor<C> {
152    async fn execute<D, I, A>(
153        delay: &mut D,
154        bound_bus: &mut embedded_interfaces::i2c::I2cBoundBus<I, A>,
155        _input: C::In,
156    ) -> Result<C::Out, TransportError<Self::Error, I::Error>>
157    where
158        D: hal::delay::DelayNs,
159        I: hal::i2c::I2c<A> + hal::i2c::ErrorType,
160        A: hal::i2c::AddressMode + Copy + core::fmt::Debug,
161    {
162        use bytemuck::Zeroable;
163        const CHUNK_SIZE: usize = 2;
164
165        let header = &C::COMMAND_ID.to_be_bytes()[core::mem::size_of_val(&C::COMMAND_ID) - C::COMMAND_ID_LEN..];
166        bound_bus.interface.write(bound_bus.address, header).await?;
167        delay.delay_ms(C::EXECUTION_TIME_MS).await;
168
169        let mut array = Vec::<u8, 64>::new();
170        let mut out = C::Out::zeroed();
171        let data = bytemuck::bytes_of_mut(&mut out);
172        array
173            .resize_default(data.len() + data.len() / CHUNK_SIZE)
174            .map_err(|_| TransportError::Unexpected("command response data: too large for buffer"))?;
175
176        bound_bus.interface.read(bound_bus.address, &mut array).await?;
177
178        let crc = crc::Crc::<u8>::new(&crc::CRC_8_NRSC_5);
179        for (i, x) in array.chunks(CHUNK_SIZE + 1).enumerate() {
180            let value = &x[0..CHUNK_SIZE];
181            data[(i * CHUNK_SIZE)..((i + 1) * CHUNK_SIZE)].copy_from_slice(value);
182
183            let calculated = crc.checksum(value);
184            let expected = x[CHUNK_SIZE];
185            if expected != calculated {
186                return Err(TransportError::Codec(Crc8Error::CrcMismatch { calculated, expected }));
187            }
188        }
189
190        #[cfg(feature = "trace-communication")]
191        log::trace!(
192            "I2C read i2c_addr={:?} SensirionReadCommand(id={id:0w$x}):\n{}",
193            bound_bus.address,
194            embedded_interfaces::BitdumpFormattable::bitdump(&out),
195            id = C::COMMAND_ID,
196            w = C::COMMAND_ID_LEN,
197        );
198
199        Ok(out)
200    }
201}
202
203#[maybe_async_cfg::maybe(
204    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), Executor, I2cBoundBus),
205    sync(feature = "sync"),
206    async(feature = "async"),
207    keep_self
208)]
209impl<C: SensirionWriteReadCommand> embedded_interfaces::commands::i2c::Executor
210    for SensirionWriteReadCommandExecutor<C>
211{
212    async fn execute<D, I, A>(
213        delay: &mut D,
214        bound_bus: &mut embedded_interfaces::i2c::I2cBoundBus<I, A>,
215        input: C::In,
216    ) -> Result<C::Out, TransportError<Self::Error, I::Error>>
217    where
218        D: hal::delay::DelayNs,
219        I: hal::i2c::I2c<A> + hal::i2c::ErrorType,
220        A: hal::i2c::AddressMode + Copy + core::fmt::Debug,
221    {
222        use bytemuck::Zeroable;
223        const CHUNK_SIZE: usize = 2;
224
225        #[cfg(feature = "trace-communication")]
226        log::trace!(
227            "I2C write i2c_addr={:?} SensirionWriteReadCommand(id={id:0w$x}):\n{}",
228            bound_bus.address,
229            embedded_interfaces::BitdumpFormattable::bitdump(&input),
230            id = C::COMMAND_ID,
231            w = C::COMMAND_ID_LEN,
232        );
233
234        let header = &C::COMMAND_ID.to_be_bytes()[core::mem::size_of_val(&C::COMMAND_ID) - C::COMMAND_ID_LEN..];
235        let mut array = Vec::<u8, 64>::new();
236        array
237            .extend_from_slice(header)
238            .map_err(|_| TransportError::Unexpected("command id: too large for command buffer"))?;
239
240        let crc = crc::Crc::<u8>::new(&crc::CRC_8_NRSC_5);
241        let data = bytemuck::bytes_of(&input);
242        for x in data.chunks(CHUNK_SIZE) {
243            array
244                .extend_from_slice(x)
245                .map_err(|_| TransportError::Unexpected("command data: too large for buffer"))?;
246            array
247                .push(crc.checksum(x))
248                .map_err(|_| TransportError::Unexpected("command data: too large for buffer"))?;
249        }
250
251        bound_bus.interface.write(bound_bus.address, &array).await?;
252        delay.delay_ms(C::EXECUTION_TIME_MS).await;
253
254        let mut array = Vec::<u8, 64>::new();
255        let mut out = C::Out::zeroed();
256        let data = bytemuck::bytes_of_mut(&mut out);
257        array
258            .resize_default(data.len() + data.len() / CHUNK_SIZE)
259            .map_err(|_| TransportError::Unexpected("command response data: too large for buffer"))?;
260
261        bound_bus.interface.read(bound_bus.address, &mut array).await?;
262
263        let crc = crc::Crc::<u8>::new(&crc::CRC_8_NRSC_5);
264        for (i, x) in array.chunks(CHUNK_SIZE + 1).enumerate() {
265            let value = &x[0..CHUNK_SIZE];
266            data[(i * CHUNK_SIZE)..((i + 1) * CHUNK_SIZE)].copy_from_slice(value);
267
268            let calculated = crc.checksum(value);
269            let expected = x[CHUNK_SIZE];
270            if expected != calculated {
271                return Err(TransportError::Codec(Crc8Error::CrcMismatch { calculated, expected }));
272            }
273        }
274
275        #[cfg(feature = "trace-communication")]
276        log::trace!(
277            "I2C read i2c_addr={:?} SensirionWriteReadCommand(id={id:0w$x}):\n{}",
278            bound_bus.address,
279            embedded_interfaces::BitdumpFormattable::bitdump(&out),
280            id = C::COMMAND_ID,
281            w = C::COMMAND_ID_LEN,
282        );
283
284        Ok(out)
285    }
286}
287
288#[allow(unused)]
289macro_rules! define_send_command {
290    ($name:ident, id_len=$id_len:literal, id=$id:expr, time=$time_ms:expr) => {
291        impl embedded_interfaces::commands::Command for $name {
292            type In = ();
293            type Out = ();
294            type ExecutorError = crate::devices::sensirion::commands::Crc8Error;
295            type SpiExecutor = embedded_interfaces::commands::unsupported_executor::UnsupportedExecutor<
296                crate::devices::sensirion::commands::Crc8Error,
297                Self,
298            >;
299            type I2cExecutor = crate::devices::sensirion::commands::SensirionSendCommandExecutor<Self>;
300        }
301        impl crate::devices::sensirion::commands::SensirionCommand for $name {
302            const COMMAND_ID_LEN: usize = $id_len;
303            const COMMAND_ID: u64 = $id;
304            const EXECUTION_TIME_MS: u32 = $time_ms;
305        }
306        impl crate::devices::sensirion::commands::SensirionSendCommand for $name {}
307    };
308}
309#[allow(unused)]
310pub(crate) use define_send_command;
311
312#[allow(unused)]
313macro_rules! define_write_command {
314    ($name:ident, id_len=$id_len:literal, id=$id:expr, time=$time_ms:expr, in=$in:ty) => {
315        impl embedded_interfaces::commands::Command for $name {
316            type In = $in;
317            type Out = ();
318            type ExecutorError = crate::devices::sensirion::commands::Crc8Error;
319            type SpiExecutor = embedded_interfaces::commands::unsupported_executor::UnsupportedExecutor<
320                crate::devices::sensirion::commands::Crc8Error,
321                Self,
322            >;
323            type I2cExecutor = crate::devices::sensirion::commands::SensirionWriteCommandExecutor<Self>;
324        }
325        impl crate::devices::sensirion::commands::SensirionCommand for $name {
326            const COMMAND_ID_LEN: usize = $id_len;
327            const COMMAND_ID: u64 = $id;
328            const EXECUTION_TIME_MS: u32 = $time_ms;
329        }
330        impl crate::devices::sensirion::commands::SensirionWriteCommand for $name {}
331    };
332}
333#[allow(unused)]
334pub(crate) use define_write_command;
335
336#[allow(unused)]
337macro_rules! define_read_command {
338    ($name:ident, id_len=$id_len:literal, id=$id:expr, time=$time_ms:expr, out=$out:ty) => {
339        impl embedded_interfaces::commands::Command for $name {
340            type In = ();
341            type Out = $out;
342            type ExecutorError = crate::devices::sensirion::commands::Crc8Error;
343            type SpiExecutor = embedded_interfaces::commands::unsupported_executor::UnsupportedExecutor<
344                crate::devices::sensirion::commands::Crc8Error,
345                Self,
346            >;
347            type I2cExecutor = crate::devices::sensirion::commands::SensirionReadCommandExecutor<Self>;
348        }
349        impl crate::devices::sensirion::commands::SensirionCommand for $name {
350            const COMMAND_ID_LEN: usize = $id_len;
351            const COMMAND_ID: u64 = $id;
352            const EXECUTION_TIME_MS: u32 = $time_ms;
353        }
354        impl crate::devices::sensirion::commands::SensirionReadCommand for $name {}
355    };
356}
357#[allow(unused)]
358pub(crate) use define_read_command;
359
360#[allow(unused)]
361macro_rules! define_write_read_command {
362    ($name:ident, id_len=$id_len:literal, id=$id:expr, time=$time_ms:expr, in=$in:ty, out=$out:ty) => {
363        impl embedded_interfaces::commands::Command for $name {
364            type In = $in;
365            type Out = $out;
366            type ExecutorError = crate::devices::sensirion::commands::Crc8Error;
367            type SpiExecutor = embedded_interfaces::commands::unsupported_executor::UnsupportedExecutor<
368                crate::devices::sensirion::commands::Crc8Error,
369                Self,
370            >;
371            type I2cExecutor = crate::devices::sensirion::commands::SensirionWriteReadCommandExecutor<Self>;
372        }
373        impl crate::devices::sensirion::commands::SensirionCommand for $name {
374            const COMMAND_ID_LEN: usize = $id_len;
375            const COMMAND_ID: u64 = $id;
376            const EXECUTION_TIME_MS: u32 = $time_ms;
377        }
378        impl crate::devices::sensirion::commands::SensirionWriteReadCommand for $name {}
379    };
380}
381#[allow(unused)]
382pub(crate) use define_write_read_command;
383
384#[allow(unused)]
385macro_rules! define_sensirion_commands {
386    // Main entry point - parse the marker section first
387    (
388        id_len $id_len:literal;
389        marker [
390            $(($feature:literal, $trait_path:path)),* $(,)?
391        ];
392        $($rest:tt)*
393    ) => {
394        define_sensirion_commands! {
395            @id_len $id_len;
396            @markers [$(($feature, $trait_path)),*];
397            @parse [$($rest)*]
398        }
399    };
400
401    // Parse individual command definitions
402    (
403        @id_len $id_len:literal;
404        @markers [$($markers:tt)*];
405        @parse [
406            $(#[doc = $doc:literal])*
407            send $id:literal time_ms=$time:literal $name:ident();
408            $($rest:tt)*
409        ]
410    ) => {
411        define_sensirion_commands! { @emit_send { [$( #[doc = $doc] )*], $name, $id_len, $id, $time } }
412        define_sensirion_commands! { @emit_impls { $name, [$($markers)*] } }
413        define_sensirion_commands! {
414            @id_len $id_len;
415            @markers [$($markers)*];
416            @parse [$($rest)*]
417        }
418    };
419
420    (
421        @id_len $id_len:literal;
422        @markers [$($markers:tt)*];
423        @parse [
424            $(#[doc = $doc:literal])*
425            read $id:literal time_ms=$time:literal $name:ident() -> $out:ty;
426            $($rest:tt)*
427        ]
428    ) => {
429        define_sensirion_commands! { @emit_read { [$( #[doc = $doc] )*], $name, $id_len, $id, $time, $out } }
430        define_sensirion_commands! { @emit_impls { $name, [$($markers)*] } }
431        define_sensirion_commands! {
432            @id_len $id_len;
433            @markers [$($markers)*];
434            @parse [$($rest)*]
435        }
436    };
437
438    (
439        @id_len $id_len:literal;
440        @markers [$($markers:tt)*];
441        @parse [
442            $(#[doc = $doc:literal])*
443            write $id:literal time_ms=$time:literal $name:ident($in:ty);
444            $($rest:tt)*
445        ]
446    ) => {
447        define_sensirion_commands! { @emit_write { [$( #[doc = $doc] )*], $name, $id_len, $id, $time, $in } }
448        define_sensirion_commands! { @emit_impls { $name, [$($markers)*] } }
449        define_sensirion_commands! {
450            @id_len $id_len;
451            @markers [$($markers)*];
452            @parse [$($rest)*]
453        }
454    };
455
456    (
457        @id_len $id_len:literal;
458        @markers [$($markers:tt)*];
459        @parse [
460            $(#[doc = $doc:literal])*
461            write_read $id:literal time_ms=$time:literal $name:ident($in:ty) -> $out:ty;
462            $($rest:tt)*
463        ]
464    ) => {
465        define_sensirion_commands! { @emit_write_read { [$( #[doc = $doc] )*], $name, $id_len, $id, $time, $in, $out } }
466        define_sensirion_commands! { @emit_impls { $name, [$($markers)*] } }
467        define_sensirion_commands! {
468            @id_len $id_len;
469            @markers [$($markers)*];
470            @parse [$($rest)*]
471        }
472    };
473
474    // Base case - no more commands to parse
475    (
476        @id_len $id_len:literal;
477        @markers [$($markers:tt)*];
478        @parse []
479    ) => {};
480
481    // Emit the actual command definitions
482    (@emit_send { [$( $doc:tt )*], $name:ident, $id_len:literal, $id:literal, $time:literal }) => {
483        $( $doc )*
484        #[doc = ""]
485        #[doc = concat!("Execution time (ms): `", stringify!($time), "`")]
486        pub struct $name {}
487
488        crate::devices::sensirion::commands::define_send_command!($name, id_len=$id_len, id=$id, time=$time);
489    };
490
491    (@emit_read { [$( $doc:tt )*], $name:ident, $id_len:literal, $id:literal, $time:literal, $out:ty }) => {
492        $( $doc )*
493        #[doc = ""]
494        #[doc = concat!("Out: [`", stringify!($out), "`]")]
495        #[doc = ""]
496        #[doc = concat!("Execution time (ms): `", stringify!($time), "`")]
497        pub struct $name {}
498
499        crate::devices::sensirion::commands::define_read_command!($name, id_len=$id_len, id=$id, time=$time, out=$out);
500    };
501
502    (@emit_write { [$( $doc:tt )*], $name:ident, $id_len:literal, $id:literal, $time:literal, $in:ty }) => {
503        $( $doc )*
504        #[doc = ""]
505        #[doc = concat!("In: [`", stringify!($in), "`]")]
506        #[doc = ""]
507        #[doc = concat!("Execution time (ms): `", stringify!($time), "`")]
508        pub struct $name {}
509
510        crate::devices::sensirion::commands::define_write_command!($name, id_len=$id_len, id=$id, time=$time, in=$in);
511    };
512
513    (@emit_write_read { [$( $doc:tt )*], $name:ident, $id_len:literal, $id:literal, $time:literal, $in:ty, $out:ty }) => {
514        $( $doc )*
515        #[doc = ""]
516        #[doc = concat!("In: [`", stringify!($in), "`]")]
517        #[doc = ""]
518        #[doc = concat!("Out: [`", stringify!($out), "`]")]
519        #[doc = ""]
520        #[doc = concat!("Execution time (ms): `", stringify!($time), "`")]
521        pub struct $name {}
522
523        crate::devices::sensirion::commands::define_write_read_command!($name, id_len=$id_len, id=$id, time=$time, in=$in, out=$out);
524    };
525
526    // Emit trait implementations for all markers
527    (
528        @emit_impls { $name:ident, [$(($feature:literal, $trait_path:path)),*] }
529    ) => {
530        $(
531            #[cfg(feature = $feature)]
532            impl $trait_path for $name {}
533        )*
534    };
535}
536#[allow(unused)]
537pub(crate) use define_sensirion_commands;