Skip to main content

pk_command/
util.rs

1#[cfg(feature = "std")]
2use std::sync::{Arc, RwLock};
3#[cfg(feature = "std")]
4use std::{cell::RefCell, pin::Pin};
5
6pub mod msg_id {
7    //! Module for handling PK Command Message IDs.
8    //!
9    //! ## Overview
10    //! Message ID is a 2-character string used to uniquely identify commands and support
11    //! the ACK mechanism and retransmission detection in the PK Command protocol.
12    //!
13    //! ## Base-94 Encoding Scheme
14    //! - **Character Set**: Printable ASCII characters from `0x21` (`!`) to `0x7E` (`~`)
15    //! - **Total Characters**: 94 characters
16    //! - **Encoding Formula**: `ID = (c1 - 0x21) * 94 + (c2 - 0x21)`
17    //!   - `c1` and `c2` are the two characters of the MSG ID string
18    //!
19    //! ## ID Range and Rollover
20    //! - **Minimum Value**: 0 (represented as `!!`)
21    //! - **Maximum Value**: 8835 (represented as `~~`)
22    //! - **Rollover Mechanism**: When ID reaches 8835, the next increment returns 0
23    //!
24    //! ## Scope and Lifetime
25    //! - IDs are **cumulative throughout the entire session**
26    //! - IDs are **NOT reset** by `ENDTR` commands
27    //! - Used for:
28    //!   1. Command tracking: Each command is uniquely identified by its MSG ID
29    //!   2. ACK validation: Receiver must return the same MSG ID in ACKNO response
30    //!   3. Retransmission detection: Receiving the same MSG ID indicates a retransmitted packet
31    //!
32    //! ## Special Case: ERROR Command
33    //! - ERROR command's MSG ID is fixed as two space characters (`0x20 0x20`)
34    //! - Its acknowledgement (`ACKNO ERROR`) also has MSG ID fixed as two spaces
35
36    #[cfg(not(feature = "std"))]
37    use alloc::{format, string::String};
38
39    const BASE: u16 = 94;
40    const OFFSET: u8 = b'!';
41    const MAX_ID: u16 = BASE * BASE - 1;
42
43    /// Converts a 2-character string ID into its u16 integer representation.
44    ///
45    /// The input string must consist of two characters within the printable ASCII
46    /// range (0x21 to 0x7E). Uses Base-94 encoding:
47    /// `ID = (c1 - 0x21) * 94 + (c2 - 0x21)`
48    ///
49    /// # Examples
50    /// ```
51    /// use pk_command::msg_id;
52    /// assert_eq!(msg_id::to_u16("!!"), Ok(0)); // minimum
53    /// assert_eq!(msg_id::to_u16("!\""), Ok(1));
54    /// assert_eq!(msg_id::to_u16("\"!"), Ok(94));
55    /// assert_eq!(msg_id::to_u16("~~"), Ok(8835)); // maximum
56    ///
57    /// assert!(msg_id::to_u16("!").is_err()); // invalid length
58    /// ```
59    pub fn to_u16(id_str: &str) -> Result<u16, &'static str> {
60        if id_str.len() != 2 {
61            // This is an internal utility, so a simple error message is fine.
62            // For a public API, more descriptive errors might be preferred.
63            // However, given its use within the PK Command protocol, this is likely sufficient.
64            // The primary validation for msg_id format happens during command parsing.
65            // This function assumes the input string *should* be a valid 2-char ID.
66            return Err("Input string must be exactly 2 characters long.");
67        }
68
69        let bytes = id_str.as_bytes();
70        let c1 = bytes[0];
71        let c2 = bytes[1];
72
73        if !((b'!'..=b'~').contains(&c1) && (b'!'..=b'~').contains(&c2)) {
74            return Err("Input string contains invalid characters.");
75        }
76
77        let val1 = (c1 - OFFSET) as u16;
78        let val2 = (c2 - OFFSET) as u16;
79
80        Ok(val1 * BASE + val2)
81    }
82
83    /// Converts a u16 integer ID back into its 2-character string representation.
84    ///
85    /// The ID must be within the valid range (0 to 8835, inclusive).
86    /// Uses the inverse of Base-94 encoding:
87    /// `c1 = (id / 94) + 0x21`, `c2 = (id % 94) + 0x21`
88    ///
89    /// # Arguments
90    /// * `id`: The u16 integer ID to convert (0-8835).
91    ///
92    /// # Returns
93    /// A `Result` containing the 2-character string ID, or an error message if the ID is out of range.
94    ///
95    /// # Examples
96    /// ```
97    /// use pk_command::msg_id;
98    /// assert_eq!(msg_id::from_u16(0), Ok("!!".to_string()));
99    /// assert_eq!(msg_id::from_u16(1), Ok("!\"".to_string()));
100    /// assert_eq!(msg_id::from_u16(94), Ok("\"!".to_string()));
101    /// assert_eq!(msg_id::from_u16(8835), Ok("~~".to_string()));
102    ///
103    /// assert!(msg_id::from_u16(8836).is_err()); // out of range
104    /// ```
105    pub fn from_u16(id: u16) -> Result<String, &'static str> {
106        if id > MAX_ID {
107            return Err("Input number is out of the valid range (0-8835).");
108        }
109
110        let val1 = id / BASE;
111        let val2 = id % BASE;
112
113        let c1 = (val1 as u8 + OFFSET) as char;
114        let c2 = (val2 as u8 + OFFSET) as char;
115
116        Ok(format!("{}{}", c1, c2))
117    }
118
119    /// Increments a message ID, handling rollover.
120    ///
121    /// When the ID reaches its maximum value (8835), it rolls over to 0.
122    /// This ensures IDs cycle through the entire range [0, 8835] in a continuous session.
123    ///
124    /// Implemented as: `(id + 1) % 8836`
125    ///
126    /// # Arguments
127    /// * `id`: The current u16 message ID.
128    ///
129    /// # Returns
130    /// The next message ID in the sequence.
131    ///
132    /// # Examples
133    /// ```
134    /// use pk_command::msg_id;
135    /// assert_eq!(msg_id::increment(0), 1);
136    /// assert_eq!(msg_id::increment(100), 101);
137    /// assert_eq!(msg_id::increment(8835), 0); // rollover
138    /// ```
139    pub fn increment(id: u16) -> u16 {
140        (id + 1) % (MAX_ID + 1)
141    }
142
143    #[cfg(test)]
144    mod tests {
145        use super::*;
146        #[cfg(not(feature = "std"))]
147        use alloc::string::ToString;
148
149        #[test]
150        fn test_msg_id_to_u16_valid() {
151            assert_eq!(to_u16("!!"), Ok(0));
152            assert_eq!(to_u16("!\""), Ok(1));
153            assert_eq!(to_u16("\"!"), Ok(BASE));
154            assert_eq!(to_u16("~~"), Ok(MAX_ID));
155        }
156
157        #[test]
158        fn test_msg_id_to_u16_invalid_length() {
159            assert!(to_u16("!").is_err());
160            assert!(to_u16("!!!").is_err());
161        }
162
163        #[test]
164        fn test_msg_id_to_u16_invalid_chars() {
165            assert!(to_u16(" !").is_err()); // Space is not allowed
166        }
167
168        #[test]
169        fn test_msg_id_from_u16_valid() {
170            assert_eq!(from_u16(0), Ok("!!".to_string()));
171            assert_eq!(from_u16(1), Ok("!\"".to_string()));
172            assert_eq!(from_u16(BASE), Ok("\"!".to_string()));
173            assert_eq!(from_u16(MAX_ID), Ok("~~".to_string()));
174        }
175
176        #[test]
177        fn test_msg_id_from_u16_out_of_range() {
178            assert!(from_u16(MAX_ID + 1).is_err());
179        }
180
181        #[test]
182        fn test_msg_id_increment() {
183            assert_eq!(increment(0), 1);
184            assert_eq!(increment(MAX_ID), 0); // Rollover
185            assert_eq!(increment(100), 101);
186        }
187    }
188}
189
190pub mod async_adapters {
191    #[cfg(all(feature = "std", feature = "tokio-runtime"))]
192    pub mod tokio {
193        //! Tokio runtime adapters.
194        //!
195        //! This module provides a [`Pollable`](crate::Pollable) wrapper that runs a `Future`
196        //! on the Tokio runtime and exposes its completion through the poll-based PK Command
197        //! interface.
198        //!
199        //! # Example
200        //! ```no_run
201        //! use pk_command::{PkHashmapMethod, tokio_adapter::TokioFuturePollable};
202        //!
203        //! // use current_thread flavor just for simplifying the dependencies,
204        //! // use whatever you want in your actual application
205        //! #[tokio::main(flavor = "current_thread")]
206        //! async fn main() {
207        //!     tokio::spawn(async {
208        //!         let method_accessor = PkHashmapMethod::new(vec![(
209        //!             String::from("ECHOO"),
210        //!             Box::new(move |param: Option<Vec<u8>>| {
211        //!                 TokioFuturePollable::from_future(async move {
212        //!                     // Note the `async` here     ^^^^^
213        //!                     Ok(param) // Echo back the input
214        //!                 })
215        //!             }),
216        //!         )]);
217        //!         // let pk=....;
218        //!         loop {
219        //!             // now you can call pk.poll() as usual.
220        //!         }
221        //!     });
222        //!
223        //!     // The async tasks you registered above, when called by the other side,
224        //!     // will run in the tokio runtime.
225        //! }
226        //! ```
227        use std::future::Future;
228        use std::pin::Pin;
229        use std::sync::{Arc, RwLock};
230
231        /// A `Pollable` adapter that spawns a `Future` onto the Tokio runtime and
232        /// exposes its completion through the `Pollable` interface.
233        ///
234        ///
235        ///
236        /// # Example with [`PkHashmapMethod`](crate::PkHashmapMethod)
237        /// ```
238        /// use pk_command::{PkCommand, PkHashmapMethod, tokio_adapter::TokioFuturePollable};
239        /// let method_impl = Box::new(move |param: Option<Vec<u8>>| {
240        ///     TokioFuturePollable::from_future(async move {
241        ///         // do_something_with(param);
242        ///         Ok(Some(b"async result".to_vec()))
243        ///     })
244        /// });
245        /// let methods = PkHashmapMethod::new(vec![("ASYNC".to_string(), method_impl)]);
246        /// ```
247        #[allow(clippy::type_complexity)]
248        pub struct TokioFuturePollable {
249            state: Arc<RwLock<Option<Result<Option<Vec<u8>>, String>>>>,
250        }
251
252        impl TokioFuturePollable {
253            /// Spawn a future onto the Tokio runtime and return a `Pollable` that
254            /// becomes ready when the future completes.
255            ///
256            /// The provided `Future` must output `Result<Option<Vec<u8>>, String>`.
257            pub fn from_future<F>(fut: F) -> Pin<Box<dyn crate::Pollable>>
258            where
259                F: Future<Output = Result<Option<Vec<u8>>, String>> + Send + 'static,
260            {
261                let state = Arc::new(RwLock::new(None));
262                let state_cloned = state.clone();
263                tokio::spawn(async move {
264                    let res = fut.await;
265                    *state_cloned.write().unwrap() = Some(res);
266                });
267                Box::pin(TokioFuturePollable { state })
268            }
269        }
270
271        #[cfg(all(feature = "std", feature = "tokio-runtime"))] // for documentation
272        impl crate::Pollable for TokioFuturePollable {
273            fn poll(&self) -> std::task::Poll<Result<Option<Vec<u8>>, String>> {
274                match self.state.read().unwrap().as_ref() {
275                    Some(r) => std::task::Poll::Ready(r.clone()),
276                    None => std::task::Poll::Pending,
277                }
278            }
279        }
280    }
281
282    #[cfg(all(feature = "std", feature = "smol-runtime"))]
283    pub mod smol {
284        //! Smol runtime adapters.
285        //!
286        //! This module provides a [`Pollable`](crate::Pollable) wrapper that runs a `Future`
287        //! on the smol executor and exposes its completion through the poll-based PK Command
288        //! interface.
289        //!
290        //! # Example
291        //! ```no_run
292        //! use pk_command::{PkHashmapMethod, smol_adapter::SmolFuturePollable};
293        //!
294        //! let method_accessor = PkHashmapMethod::new(vec![(
295        //!     String::from("ECHOO"),
296        //!     Box::new(move |param: Option<Vec<u8>>| {
297        //!         SmolFuturePollable::from_future(async move {
298        //!             // Note the `async` here    ^^^^^
299        //!             Ok(param) // Echo back the input as the result
300        //!         })
301        //!     }),
302        //! )]);
303        //!
304        //! // When using smol, you need to run the code within a smol executor context.
305        //! // The async tasks you registered above, when called by the other side,
306        //! // will run in the smol executor.
307        //! ```
308        use std::future::Future;
309        use std::pin::Pin;
310        use std::sync::{Arc, RwLock};
311
312        /// A `Pollable` adapter that spawns a `Future` onto the smol executor and
313        /// exposes its completion through the `Pollable` interface.
314        ///
315        ///
316        #[allow(clippy::type_complexity)]
317        pub struct SmolFuturePollable {
318            state: Arc<RwLock<Option<Result<Option<Vec<u8>>, String>>>>,
319        }
320
321        impl SmolFuturePollable {
322            /// Spawn a future onto the smol runtime and return a `Pollable` that
323            /// becomes ready when the future completes.
324            pub fn from_future<F>(fut: F) -> Pin<Box<dyn crate::Pollable>>
325            where
326                F: Future<Output = Result<Option<Vec<u8>>, String>> + Send + 'static,
327            {
328                let state = Arc::new(RwLock::new(None));
329                let state_cloned = state.clone();
330                // smol::spawn returns a Task which can be detached; detach so it runs in background
331                smol::spawn(async move {
332                    let res = fut.await;
333                    *state_cloned.write().unwrap() = Some(res);
334                })
335                .detach();
336                Box::pin(SmolFuturePollable { state })
337            }
338        }
339
340        #[cfg(all(feature = "std", feature = "smol-runtime"))]
341        impl crate::Pollable for SmolFuturePollable {
342            fn poll(&self) -> std::task::Poll<Result<Option<Vec<u8>>, String>> {
343                match self.state.read().unwrap().as_ref() {
344                    Some(r) => std::task::Poll::Ready(r.clone()),
345                    None => std::task::Poll::Pending,
346                }
347            }
348        }
349    }
350    #[cfg(feature = "embassy-runtime")]
351    pub mod embassy {
352        //! Embassy runtime adapters.
353        //!
354        //! This module provides [`Pollable`](crate::Pollable) and [`PkMethodAccessor`](crate::PkMethodAccessor)
355        //! helpers that integrate PK Command with the [Embassy](https://embassy.dev/) async runtime.
356        //!
357        //! See more at the [`embassy_method_accessor!`](crate::embassy_method_accessor) macro.
358
359        // Typically `std` is not available here
360        extern crate alloc;
361        use alloc::boxed::Box;
362        use alloc::string::String;
363        use alloc::sync::Arc;
364        use alloc::vec::Vec;
365        use embassy_sync::once_lock::OnceLock;
366
367        /// A [`Pollable`](crate::Pollable) backed by an Embassy [`OnceLock`].
368        ///
369        ///
370        ///
371        /// This is typically created by the [`embassy_method_accessor!`](crate::embassy_method_accessor) macro and
372        /// becomes ready when the async task resolves.
373        ///
374        /// # Example
375        /// ```no_run
376        /// extern crate alloc;
377        /// use alloc::sync::Arc;
378        /// use core::task::Poll;
379        /// use embassy_sync::once_lock::OnceLock;
380        /// use pk_command::embassy_adapter::EmbassyPollable;
381        ///
382        /// let lock = Arc::new(OnceLock::new());
383        /// let pollable = EmbassyPollable(lock);
384        /// let _ = pollable; // pass to PK Command state machine
385        /// ```
386        pub struct EmbassyPollable(pub Arc<OnceLock<Vec<u8>>>);
387        impl crate::Pollable for EmbassyPollable {
388            fn poll(&self) -> core::task::Poll<Result<Option<Vec<u8>>, String>> {
389                match self.0.try_get() {
390                    Some(data) => core::task::Poll::Ready(Ok(Some(data.clone()))),
391                    None => core::task::Poll::Pending,
392                }
393            }
394        }
395
396        /// Callback type used by Embassy tasks to resolve a method call.
397        ///
398        ///
399        ///
400        /// The callback should be invoked **exactly once** with the method's return data. Usually
401        /// right after your task is finished.
402        ///
403        /// # Panics
404        ///
405        /// Usually you would get this function as the second parameter of the task functions
406        /// (i.e. the ones marked with [`#[embassy_executor::task]`](embassy_executor::task))
407        /// that you brought into the [`embassy_method_accessor!`](crate::embassy_method_accessor).
408        ///
409        /// Inside, that function calls [`OnceLock::init()`](embassy_sync::once_lock::OnceLock::init),
410        /// right followed by an [`.unwrap()`](core::result::Result::unwrap). So this function panics
411        /// **when it is called multiple times**. This usually indicates a tragic logic failure.
412        ///
413        /// # Example
414        /// ```no_run
415        /// use embassy_time::Timer;
416        /// use pk_command::embassy_adapter::TaskCallback;
417        ///
418        /// #[embassy_executor::task]
419        /// async fn async_echo(param: Vec<u8>, callback: TaskCallback) {
420        ///     // You get the callback function^^^^^^^^ here.
421        ///
422        ///     Timer::after_millis(10).await;
423        ///     callback(param);
424        /// }
425        /// ```
426        pub type TaskCallback = Box<dyn Fn(Vec<u8>) + Send>;
427
428        /// Helper macro for creating a [`PkMethodAccessor`](crate::PkMethodAccessor)
429        /// backed by Embassy tasks.
430        ///
431        ///
432        ///
433        /// # What This Macro Does
434        ///
435        /// It provides a struct definition and implements the [`PkMethodAccessor`](crate::PkMethodAccessor)
436        /// trait for it. The macro accepts several name-function pairs (5-character string literals paired
437        /// with async functions) and internally expands them into a simple mapping implementation based on
438        /// `match` statements. When invoked by [`PkCommand`](crate::PkCommand), it constructs an [`EmbassyPollable`]
439        ///  struct and spawns the provided async task in the Embassy executor.
440        ///
441        /// # Why a Macro is Needed
442        ///
443        /// In embedded scenarios, memory and performance are typically constrained, making it wasteful to
444        /// maintain a [`HashMap`](std::collections::HashMap) resident in memory. Moreover, for async tasks
445        /// in the Embassy environment, the functions generated by [`#[task]`](embassy_executor::task) return
446        /// a [`SpawnToken<S>`](embassy_executor::SpawnToken) (where `S` is an opaque type that differs for
447        /// each generated task — we only know it implements the [`Sized`] trait). If we followed a `HashMap`-like
448        /// approach and used wrappers like `Box` to force them into the same variable, it would introduce unnecessary
449        /// and relatively expensive runtime overhead and code complexity.
450        ///
451        /// However, with a macro, we can simplify the complex hash matching logic into Rust's built-in pattern
452        /// matching and hide the differences in [`SpawnToken<S>`](embassy_executor::SpawnToken) generic parameters through
453        /// different execution paths (from the compiler's perspective). This not only significantly reduces runtime overhead
454        /// but also seamlessly integrates the Embassy environment into PK Command.
455        ///
456        /// # How to Use This Macro
457        ///
458        /// You can invoke this macro like:
459        ///
460        /// ```no_run
461        /// # #[macro_use] extern crate pk_command;
462        /// # use pk_command::embassy_adapter::TaskCallback;
463        /// # use embassy_time::Timer;
464        /// #
465        /// # #[embassy_executor::task]
466        /// # async fn async_task_1(param:Vec<u8>, callback:TaskCallback)
467        /// # {
468        /// #    // do_something_with(param);
469        /// #    callback(param);
470        /// # }
471        /// # #[embassy_executor::task]
472        /// # async fn async_task_2(param:Vec<u8>, callback:TaskCallback)
473        /// # {
474        /// #    // do_something_with(param);
475        /// #    callback(param);
476        /// # }
477        /// # extern crate alloc;
478        /// embassy_method_accessor!(
479        ///     MyMethodAccessor,
480        ///     ("TASK1", async_task_1),
481        ///     ("TASK2", async_task_2)
482        /// );
483        /// ```
484        ///
485        /// The macro accepts parameters consisting of an identifier and several tuples (at least one). Specifically:
486        ///
487        /// - Identifier: The name of the [`PkMethodAccessor`](crate::PkMethodAccessor) struct to be generated.
488        /// - Tuples:
489        ///   - First element: A 5-character string literal indicating the method name.
490        ///   - Second element: A function marked with the [`#[embassy_executor::task]`](embassy_executor::task) macro. This function must have the following form:
491        ///
492        /// ```no_run
493        /// # use pk_command::embassy_adapter::TaskCallback;
494        /// #[embassy_executor::task]
495        /// async fn async_task (param: Vec<u8>, callback: TaskCallback)
496        /// # {}
497        /// ```
498        ///
499        /// See [`TaskCallback`] for more details.
500        ///
501        /// <div class="warning">
502        ///
503        /// The macro does not perform compile-time checks for this, but you should still note:
504        /// method names must be 5-character strings containing only ASCII characters. If this constraint
505        /// is not met, the code will still compile, but your registered methods may not be callable by
506        /// PkCommand, which is a serious logical error.
507        ///
508        /// </div>
509        ///
510        /// # Complete Example
511        ///
512        /// ```no_run
513        /// # #[macro_use] extern crate pk_command;
514        /// use embassy_executor::Spawner;
515        /// use embassy_time::Timer;
516        /// use pk_command::embassy_adapter::TaskCallback;
517        /// use pk_command::{EmbassyInstant, PkCommand, PkCommandConfig, PkHashmapVariable};
518        ///
519        /// #[embassy_executor::task]
520        /// async fn async_task(param: Vec<u8>, callback: TaskCallback) {
521        ///     // do_something_with(param);
522        ///     callback(param);
523        /// }
524        ///
525        /// // This is required for the macro to work.
526        /// extern crate alloc;
527        /// pk_command::embassy_method_accessor!(MyMethodAccessor, ("TASK1", async_task));
528        ///
529        /// #[embassy_executor::task]
530        /// async fn pk_command(pk: PkCommand<PkHashmapVariable, MyMethodAccessor, EmbassyInstant>) {
531        ///     loop {
532        ///         let cmd = pk.poll();
533        ///         if let Some(cmd) = cmd {
534        ///             // send to the transport layer..
535        ///         }
536        ///         Timer::after_millis(10).await;
537        ///     }
538        /// }
539        ///
540        /// #[embassy_executor::main]
541        /// async fn main(spawner: Spawner) {
542        ///     let send_spawner = spawner.make_send();
543        ///     let ma = MyMethodAccessor::new(send_spawner);
544        ///     // Here you can use `ma` as the MethodAccessor of PkCommand
545        ///     let pk = PkCommand::<_, _, EmbassyInstant>::new(
546        ///         PkCommandConfig::default(64),
547        ///         PkHashmapVariable::new(vec![]),
548        ///         ma,
549        ///     );
550        ///     spawner.spawn(pk_command(pk)).unwrap();
551        /// }
552        /// ```
553        #[cfg_attr(docsrs, doc(cfg(feature = "embassy-runtime")))]
554        #[macro_export]
555        macro_rules! embassy_method_accessor {
556            (
557                $struct_name: ident,
558                $(
559                    (
560                        $method_name: literal,
561                        $function: path
562                    )
563                )
564                , +
565            ) => {
566                #[derive(Clone, Copy)]
567                struct $struct_name
568                {
569                    spawner: ::embassy_executor::SendSpawner,
570                }
571
572                impl ::pk_command::PkMethodAccessor for $struct_name
573                {
574                    fn call(
575                        &self,
576                        key: ::alloc::string::String,
577                        param: ::alloc::vec::Vec<u8>
578                    ) -> ::core::result::Result<::core::pin::Pin<::alloc::boxed::Box<dyn ::pk_command::Pollable>>, ::alloc::string::String>
579                    {
580                        let lock=::alloc::sync::Arc::new(::embassy_sync::once_lock::OnceLock::new());
581                        let lock_clone=lock.clone();
582                        let pollable=::alloc::boxed::Box::pin(::pk_command::embassy_adapter::EmbassyPollable(lock_clone));
583                        let lock_clone_clone=lock.clone();
584                        let callback = ::alloc::boxed::Box::new(move |data: Vec<u8>| {
585                            lock_clone_clone.init(data).unwrap();
586                        });
587                        match key.as_str() {
588                            $(
589                                $method_name => {
590                                    let token=$function(param, callback);
591                                    self.spawner.spawn(token)
592                                        .map_err(|x| x.to_string())?;
593                                    Ok(pollable)
594                                },
595                            )*
596                            _ => {
597                                let mut err_msg = ::alloc::string::String::from("No method named ");
598                                err_msg.push_str(&key);
599                                err_msg.push_str(" found");
600                                Err(err_msg)
601                            }
602                        }
603                    }
604                }
605                impl $struct_name
606                {
607                    fn new(spawner: ::embassy_executor::SendSpawner) -> Self
608                    {
609                        Self {spawner}
610                    }
611                }
612            };
613        }
614    }
615}
616
617/// Type alias for a variable change listener function.
618/// This function takes a `Vec<u8>` as input and returns nothing.
619/// It is used in `PkHashmapVariable`, and is called whenever a variable is updated.
620///
621///
622#[cfg(feature = "std")]
623pub type VariableChangeListener = Box<dyn Fn(Vec<u8>)>;
624
625/// A wrapper for `std::collections::HashMap` that implements the `PkVariableAccessor` trait.
626///
627///
628///
629/// This implementation provides internal mutability and a listener mechanism for variable updates.
630///
631/// **Note**: This is only available when the `std` feature is enabled.
632#[cfg(feature = "std")]
633pub struct PkHashmapVariable {
634    hashmap: std::collections::HashMap<String, (RefCell<Vec<u8>>, VariableChangeListener)>,
635}
636
637#[cfg(feature = "std")]
638impl crate::PkVariableAccessor for PkHashmapVariable {
639    fn get(&self, key: String) -> Option<Vec<u8>> {
640        self.hashmap.get(&key).map(|v| v.0.borrow().clone())
641    }
642    fn set(&self, key: String, value: Vec<u8>) -> Result<(), String> {
643        if self.hashmap.contains_key(&key) {
644            let v = self.hashmap.get(&key).unwrap();
645            v.0.replace(value.clone());
646            v.1(value);
647            Ok(())
648        } else {
649            Err(String::from("Key not found"))
650        }
651    }
652}
653#[cfg(feature = "std")]
654impl PkHashmapVariable {
655    /// Creates a new `PkHashmapVariable` instance.
656    ///
657    /// # Arguments
658    /// * `init_vec`: A vector of tuples, where each tuple contains:
659    ///     - `String`: The variable key.
660    ///     - `Option<Vec<u8>>`: The initial value of the variable. Defaults to an empty `Vec<u8>` if `None`.
661    ///     - `VariableChangeListener`: A listener function called when the variable is set.
662    ///
663    /// **IMPORTANT**: The listener is executed synchronously in the same thread as `PkCommand::poll()`.
664    /// If the listener performs heavy computation, it may block the protocol state machine.
665    pub fn new(init_vec: Vec<(String, Option<Vec<u8>>, VariableChangeListener)>) -> Self {
666        let mut hashmap = std::collections::HashMap::new();
667        for i in init_vec.into_iter() {
668            let (key, value, listener) = i;
669            hashmap.insert(key, (RefCell::new(value.unwrap_or_default()), listener));
670        }
671        PkHashmapVariable { hashmap }
672    }
673}
674
675/// Type alias for a method implementation function.
676/// This function takes an optional [`Vec<u8>`] as input and returns a pinned [`Pollable`](crate::Pollable).
677/// It is used in `PkHashmapMethod` to define method behaviors.
678///
679///
680#[cfg(feature = "std")]
681pub type MethodImplementation = Box<dyn Fn(Option<Vec<u8>>) -> Pin<Box<dyn crate::Pollable>>>;
682
683/// A wrapper for `std::collections::HashMap` that implements the [`PkMethodAccessor`](crate::PkMethodAccessor) trait.
684///
685///
686///
687/// **Note**: This is only available when the `std` feature is enabled.
688///
689/// # Note for Method Implementation type
690///
691/// The type `Box<dyn Fn(Option<Vec<u8>>) -> Pin<Box<dyn Pollable>>>` (as appeared in the second field of the tuple that [`new()`](PkHashmapMethod::new) accepts) is a boxed closure
692/// that takes an optional [`Vec<u8>`] as input, and returns a pinned [`Pollable`](crate::Pollable).
693/// This allows method implementations to perform background work and return a [`Pollable`](crate::Pollable) that becomes ready when the work is complete.
694///
695/// The library provides some helper types for the [`Pollable`](crate::Pollable) trait:
696/// - Sync, based on threads: [`PkPromise`].
697/// - Async with [Tokio](https://tokio.rs/): [`TokioFuturePollable`](crate::tokio_adapter::TokioFuturePollable).
698/// - Async with [Smol](https://github.com/smol-rs/smol): [`SmolFuturePollable`](crate::smol_adapter::SmolFuturePollable).
699///
700/// And simply boxing them should work.
701///
702/// # Example with [`PkPromise`]
703/// ```
704/// use pk_command::{PkHashmapMethod, PkPromise};
705/// let methods = PkHashmapMethod::new(vec![(
706///     String::from("LONGT"),
707///     Box::new(|param| {
708///         PkPromise::execute(|resolve| {
709///             // do_something_with(param);
710///             resolve(b"task complete".to_vec());
711///         })
712///     }),
713/// )]);
714/// ```
715#[cfg(feature = "std")]
716pub struct PkHashmapMethod {
717    hashmap: std::collections::HashMap<String, MethodImplementation>,
718}
719
720#[cfg(feature = "std")]
721impl crate::PkMethodAccessor for PkHashmapMethod {
722    fn call(&self, key: String, param: Vec<u8>) -> Result<Pin<Box<dyn crate::Pollable>>, String> {
723        if self.hashmap.contains_key(&key) {
724            let f = self.hashmap.get(&key).unwrap();
725            Ok(f(Some(param)))
726        } else {
727            Err(String::from("Method not found"))
728        }
729    }
730}
731
732#[cfg(feature = "std")]
733impl PkHashmapMethod {
734    /// Creates a new `PkHashmapMethod` instance.
735    ///
736    /// # Arguments
737    /// * `init_vec`: A vector of tuples containing method keys and their corresponding implementation closures.
738    pub fn new(init_vec: Vec<(String, MethodImplementation)>) -> Self {
739        let mut hashmap = std::collections::HashMap::new();
740        for i in init_vec.into_iter() {
741            let (key, method) = i;
742            hashmap.insert(key, method);
743        }
744        PkHashmapMethod { hashmap }
745    }
746}
747
748/// A simple implementation of `Pollable` that executes tasks in a background thread.
749///
750///
751///
752/// This is similar to a JavaScript Promise. It allows offloading work to another thread
753/// and polling for its completion within the PK protocol's `poll()` cycle.
754///
755/// **Note**: This is only available when the `std` feature is enabled.
756#[derive(Clone)]
757#[cfg(feature = "std")]
758pub struct PkPromise {
759    return_value: Arc<RwLock<Option<Vec<u8>>>>,
760}
761#[cfg(feature = "std")]
762impl PkPromise {
763    /// Executes a closure in a new thread and returns a `Pollable` handle.
764    ///
765    /// The provided closure `function` receives a `resolve` callback. Call `resolve(data)`
766    /// when the task is complete to make the data available.
767    ///
768    /// # Arguments
769    /// * `function`: A closure that performs the task and calls the provided `resolve` callback.
770    ///
771    /// # Example
772    /// ```
773    /// use pk_command::PkPromise;
774    /// let promise = PkPromise::execute(|resolve| {
775    ///     // Do expensive work...
776    ///     resolve(b"done".to_vec());
777    /// });
778    /// ```
779    ///
780    /// # Example with PK Command
781    /// ```
782    /// use pk_command::{PkCommand, PkCommandConfig, PkHashmapMethod, PkHashmapVariable, PkPromise};
783    /// let vars = PkHashmapVariable::new(vec![]);
784    /// let methods = PkHashmapMethod::new(vec![(
785    ///     "LONGT".to_string(),
786    ///     Box::new(|param| {
787    ///         PkPromise::execute(|resolve| {
788    ///             // do_something_with(param);
789    ///             resolve(b"task complete".to_vec());
790    ///         })
791    ///     }),
792    /// )]);
793    /// let config = PkCommandConfig::default(64);
794    /// let pk = PkCommand::<_, _, std::time::Instant>::new(config, vars, methods);
795    /// // main loop...
796    /// ```
797    #[cfg(feature = "std")]
798    pub fn execute<T>(function: T) -> Pin<Box<Self>>
799    where
800        T: FnOnce(Box<dyn FnOnce(Vec<u8>) + Send + 'static>) + Send + 'static,
801    {
802        let return_value_arc = Arc::new(RwLock::new(None));
803        let return_value_clone = return_value_arc.clone();
804        std::thread::spawn(move || {
805            let resolve: Box<dyn FnOnce(Vec<u8>) + Send + 'static> =
806                Box::new(move |ret: Vec<u8>| {
807                    // This resolve function is called by the user's function
808                    *return_value_clone.write().unwrap() = Some(ret);
809                });
810            function(Box::new(resolve));
811        });
812        Box::pin(PkPromise {
813            return_value: return_value_arc,
814        })
815    }
816}
817#[cfg(feature = "std")]
818impl crate::Pollable for PkPromise {
819    fn poll(&self) -> std::task::Poll<Result<Option<Vec<u8>>, String>> {
820        let read_guard = self.return_value.read().unwrap();
821        match read_guard.as_ref() {
822            Some(data) => std::task::Poll::Ready(Ok(Some(data.clone()))),
823            None => std::task::Poll::Pending,
824        }
825    }
826}