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::sync::Arc;
362 use alloc::vec::Vec;
363 use embassy_sync::once_lock::OnceLock;
364
365 /// A [`Pollable`](crate::Pollable) backed by an Embassy [`OnceLock`].
366 ///
367 ///
368 ///
369 /// This is typically created by the [`embassy_method_accessor!`](crate::embassy_method_accessor) macro and
370 /// becomes ready when the async task resolves.
371 ///
372 /// # Example
373 /// ```no_run
374 /// extern crate alloc;
375 /// use alloc::sync::Arc;
376 /// use core::task::Poll;
377 /// use embassy_sync::once_lock::OnceLock;
378 /// use pk_command::embassy_adapter::EmbassyPollable;
379 ///
380 /// let lock = Arc::new(OnceLock::new());
381 /// let pollable = EmbassyPollable(lock);
382 /// let _ = pollable; // pass to PK Command state machine
383 /// ```
384 pub struct EmbassyPollable(pub Arc<OnceLock<Vec<u8>>>);
385 impl crate::Pollable for EmbassyPollable {
386 fn poll(&self) -> core::task::Poll<Result<Option<Vec<u8>>, String>> {
387 match self.0.try_get() {
388 Some(data) => core::task::Poll::Ready(Ok(Some(data.clone()))),
389 None => core::task::Poll::Pending,
390 }
391 }
392 }
393
394 /// Callback type used by Embassy tasks to resolve a method call.
395 ///
396 ///
397 ///
398 /// The callback should be invoked **exactly once** with the method's return data. Usually
399 /// right after your task is finished.
400 ///
401 /// # Panics
402 ///
403 /// Usually you would get this function as the second parameter of the task functions
404 /// (i.e. the ones marked with [`#[embassy_executor::task]`](embassy_executor::task))
405 /// that you brought into the [`embassy_method_accessor!`](crate::embassy_method_accessor).
406 ///
407 /// Inside, that function calls [`OnceLock::init()`](embassy_sync::once_lock::OnceLock::init),
408 /// right followed by an [`.unwrap()`](core::result::Result::unwrap). So this function panics
409 /// **when it is called multiple times**. This usually indicates a tragic logic failure.
410 ///
411 /// # Example
412 /// ```no_run
413 /// use embassy_time::Timer;
414 /// use pk_command::embassy_adapter::TaskCallback;
415 ///
416 /// #[embassy_executor::task]
417 /// async fn async_echo(param: Vec<u8>, callback: TaskCallback) {
418 /// // You get the callback function^^^^^^^^ here.
419 ///
420 /// Timer::after_millis(10).await;
421 /// callback(param);
422 /// }
423 /// ```
424 pub type TaskCallback = Box<dyn Fn(Vec<u8>) + Send>;
425
426 /// Helper macro for creating a [`PkMethodAccessor`](crate::PkMethodAccessor)
427 /// backed by Embassy tasks.
428 ///
429 ///
430 ///
431 /// # What This Macro Does
432 ///
433 /// It provides a struct definition and implements the [`PkMethodAccessor`](crate::PkMethodAccessor)
434 /// trait for it. The macro accepts several name-function pairs (5-character string literals paired
435 /// with async functions) and internally expands them into a simple mapping implementation based on
436 /// `match` statements. When invoked by [`PkCommand`](crate::PkCommand), it constructs an [`EmbassyPollable`]
437 /// struct and spawns the provided async task in the Embassy executor.
438 ///
439 /// # Why a Macro is Needed
440 ///
441 /// In embedded scenarios, memory and performance are typically constrained, making it wasteful to
442 /// maintain a [`HashMap`](std::collections::HashMap) resident in memory. Moreover, for async tasks
443 /// in the Embassy environment, the functions generated by [`#[task]`](embassy_executor::task) return
444 /// a [`SpawnToken<S>`](embassy_executor::SpawnToken) (where `S` is an opaque type that differs for
445 /// each generated task — we only know it implements the [`Sized`] trait). If we followed a `HashMap`-like
446 /// approach and used wrappers like `Box` to force them into the same variable, it would introduce unnecessary
447 /// and relatively expensive runtime overhead and code complexity.
448 ///
449 /// However, with a macro, we can simplify the complex hash matching logic into Rust's built-in pattern
450 /// matching and hide the differences in [`SpawnToken<S>`](embassy_executor::SpawnToken) generic parameters through
451 /// different execution paths (from the compiler's perspective). This not only significantly reduces runtime overhead
452 /// but also seamlessly integrates the Embassy environment into PK Command.
453 ///
454 /// # How to Use This Macro
455 ///
456 /// You can invoke this macro like:
457 ///
458 /// ```no_run
459 /// # #[macro_use] extern crate pk_command;
460 /// # use pk_command::embassy_adapter::TaskCallback;
461 /// # use embassy_time::Timer;
462 /// #
463 /// # #[embassy_executor::task]
464 /// # async fn async_task_1(param:Vec<u8>, callback:TaskCallback)
465 /// # {
466 /// # // do_something_with(param);
467 /// # callback(param);
468 /// # }
469 /// # #[embassy_executor::task]
470 /// # async fn async_task_2(param:Vec<u8>, callback:TaskCallback)
471 /// # {
472 /// # // do_something_with(param);
473 /// # callback(param);
474 /// # }
475 /// # extern crate alloc;
476 /// embassy_method_accessor!(
477 /// MyMethodAccessor,
478 /// ("TASK1", async_task_1),
479 /// ("TASK2", async_task_2)
480 /// );
481 /// ```
482 ///
483 /// The macro accepts parameters consisting of an identifier and several tuples (at least one). Specifically:
484 ///
485 /// - Identifier: The name of the [`PkMethodAccessor`](crate::PkMethodAccessor) struct to be generated.
486 /// - Tuples:
487 /// - First element: A 5-character string literal indicating the method name.
488 /// - Second element: A function marked with the [`#[embassy_executor::task]`](embassy_executor::task) macro. This function must have the following form:
489 ///
490 /// ```no_run
491 /// # use pk_command::embassy_adapter::TaskCallback;
492 /// #[embassy_executor::task]
493 /// async fn async_task (param: Vec<u8>, callback: TaskCallback)
494 /// # {}
495 /// ```
496 ///
497 /// See [`TaskCallback`] for more details.
498 ///
499 /// <div class="warning">
500 ///
501 /// The macro does not perform compile-time checks for this, but you should still note:
502 /// method names must be 5-character strings containing only ASCII characters. If this constraint
503 /// is not met, the code will still compile, but your registered methods may not be callable by
504 /// PkCommand, which is a serious logical error.
505 ///
506 /// </div>
507 ///
508 /// # Complete Example
509 ///
510 /// ```no_run
511 /// # #[macro_use] extern crate pk_command;
512 /// use embassy_executor::Spawner;
513 /// use embassy_time::Timer;
514 /// use pk_command::embassy_adapter::TaskCallback;
515 /// use pk_command::{EmbassyInstant, PkCommand, PkCommandConfig, PkHashmapVariable};
516 ///
517 /// #[embassy_executor::task]
518 /// async fn async_task(param: Vec<u8>, callback: TaskCallback) {
519 /// // do_something_with(param);
520 /// callback(param);
521 /// }
522 ///
523 /// // This is required for the macro to work.
524 /// extern crate alloc;
525 /// pk_command::embassy_method_accessor!(MyMethodAccessor, ("TASK1", async_task));
526 ///
527 /// #[embassy_executor::task]
528 /// async fn pk_command(pk: PkCommand<PkHashmapVariable, MyMethodAccessor, EmbassyInstant>) {
529 /// loop {
530 /// let cmd = pk.poll();
531 /// if let Some(cmd) = cmd {
532 /// // send to the transport layer..
533 /// }
534 /// Timer::after_millis(10).await;
535 /// }
536 /// }
537 ///
538 /// #[embassy_executor::main]
539 /// async fn main(spawner: Spawner) {
540 /// let send_spawner = spawner.make_send();
541 /// let ma = MyMethodAccessor::new(send_spawner);
542 /// // Here you can use `ma` as the MethodAccessor of PkCommand
543 /// let pk = PkCommand::<_, _, EmbassyInstant>::new(
544 /// PkCommandConfig::default(64),
545 /// PkHashmapVariable::new(vec![]),
546 /// ma,
547 /// );
548 /// spawner.spawn(pk_command(pk)).unwrap();
549 /// }
550 /// ```
551 #[cfg_attr(docsrs, doc(cfg(feature = "embassy-runtime")))]
552 #[macro_export]
553 macro_rules! embassy_method_accessor {
554 (
555 $struct_name: ident,
556 $(
557 (
558 $method_name: literal,
559 $function: path
560 )
561 )
562 , +
563 ) => {
564 #[derive(Clone, Copy)]
565 struct $struct_name
566 {
567 spawner: ::embassy_executor::SendSpawner,
568 }
569
570 impl ::pk_command::PkMethodAccessor for $struct_name
571 {
572 fn call(
573 &self,
574 key: ::alloc::string::String,
575 param: ::alloc::vec::Vec<u8>
576 ) -> ::core::result::Result<::core::pin::Pin<::alloc::boxed::Box<dyn ::pk_command::Pollable>>, ::alloc::string::String>
577 {
578 let lock=::alloc::sync::Arc::new(::embassy_sync::once_lock::OnceLock::new());
579 let lock_clone=lock.clone();
580 let pollable=::alloc::boxed::Box::pin(::pk_command::embassy_adapter::EmbassyPollable(lock_clone));
581 let lock_clone_clone=lock.clone();
582 let callback = ::alloc::boxed::Box::new(move |data: Vec<u8>| {
583 lock_clone_clone.init(data).unwrap();
584 });
585 match key.as_str() {
586 $(
587 $method_name => {
588 let token=$function(param, callback);
589 self.spawner.spawn(token)
590 .map_err(|x| x.to_string())?;
591 Ok(pollable)
592 },
593 )*
594 _ => {
595 let mut err_msg = ::alloc::string::String::from("No method named ");
596 err_msg.push_str(&key);
597 err_msg.push_str(" found");
598 Err(err_msg)
599 }
600 }
601 }
602 }
603 impl $struct_name
604 {
605 fn new(spawner: ::embassy_executor::SendSpawner) -> Self
606 {
607 Self {spawner}
608 }
609 }
610 };
611 }
612 }
613}
614
615/// Type alias for a variable change listener function.
616/// This function takes a `Vec<u8>` as input and returns nothing.
617/// It is used in `PkHashmapVariable`, and is called whenever a variable is updated.
618///
619///
620#[cfg(feature = "std")]
621pub type VariableChangeListener = Box<dyn Fn(Vec<u8>)>;
622
623/// A wrapper for `std::collections::HashMap` that implements the `PkVariableAccessor` trait.
624///
625///
626///
627/// This implementation provides internal mutability and a listener mechanism for variable updates.
628///
629/// **Note**: This is only available when the `std` feature is enabled.
630#[cfg(feature = "std")]
631pub struct PkHashmapVariable {
632 hashmap: std::collections::HashMap<String, (RefCell<Vec<u8>>, VariableChangeListener)>,
633}
634
635#[cfg(feature = "std")]
636impl crate::PkVariableAccessor for PkHashmapVariable {
637 fn get(&self, key: String) -> Option<Vec<u8>> {
638 self.hashmap.get(&key).map(|v| v.0.borrow().clone())
639 }
640 fn set(&self, key: String, value: Vec<u8>) -> Result<(), String> {
641 if self.hashmap.contains_key(&key) {
642 let v = self.hashmap.get(&key).unwrap();
643 v.0.replace(value.clone());
644 v.1(value);
645 Ok(())
646 } else {
647 Err(String::from("Key not found"))
648 }
649 }
650}
651#[cfg(feature = "std")]
652impl PkHashmapVariable {
653 /// Creates a new `PkHashmapVariable` instance.
654 ///
655 /// # Arguments
656 /// * `init_vec`: A vector of tuples, where each tuple contains:
657 /// - `String`: The variable key.
658 /// - `Option<Vec<u8>>`: The initial value of the variable. Defaults to an empty `Vec<u8>` if `None`.
659 /// - `VariableChangeListener`: A listener function called when the variable is set.
660 ///
661 /// **IMPORTANT**: The listener is executed synchronously in the same thread as `PkCommand::poll()`.
662 /// If the listener performs heavy computation, it may block the protocol state machine.
663 pub fn new(init_vec: Vec<(String, Option<Vec<u8>>, VariableChangeListener)>) -> Self {
664 let mut hashmap = std::collections::HashMap::new();
665 for i in init_vec.into_iter() {
666 let (key, value, listener) = i;
667 hashmap.insert(key, (RefCell::new(value.unwrap_or_default()), listener));
668 }
669 PkHashmapVariable { hashmap }
670 }
671}
672
673/// Type alias for a method implementation function.
674/// This function takes an optional [`Vec<u8>`] as input and returns a pinned [`Pollable`](crate::Pollable).
675/// It is used in `PkHashmapMethod` to define method behaviors.
676///
677///
678#[cfg(feature = "std")]
679pub type MethodImplementation = Box<dyn Fn(Option<Vec<u8>>) -> Pin<Box<dyn crate::Pollable>>>;
680
681/// A wrapper for `std::collections::HashMap` that implements the [`PkMethodAccessor`](crate::PkMethodAccessor) trait.
682///
683///
684///
685/// **Note**: This is only available when the `std` feature is enabled.
686///
687/// # Note for Method Implementation type
688///
689/// 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
690/// that takes an optional [`Vec<u8>`] as input, and returns a pinned [`Pollable`](crate::Pollable).
691/// This allows method implementations to perform background work and return a [`Pollable`](crate::Pollable) that becomes ready when the work is complete.
692///
693/// The library provides some helper types for the [`Pollable`](crate::Pollable) trait:
694/// - Sync, based on threads: [`PkPromise`].
695/// - Async with [Tokio](https://tokio.rs/): [`TokioFuturePollable`](crate::tokio_adapter::TokioFuturePollable).
696/// - Async with [Smol](https://github.com/smol-rs/smol): [`SmolFuturePollable`](crate::smol_adapter::SmolFuturePollable).
697///
698/// And simply boxing them should work.
699///
700/// # Example with [`PkPromise`]
701/// ```
702/// use pk_command::{PkHashmapMethod, PkPromise};
703/// let methods = PkHashmapMethod::new(vec![(
704/// String::from("LONGT"),
705/// Box::new(|param| {
706/// PkPromise::execute(|resolve| {
707/// // do_something_with(param);
708/// resolve(b"task complete".to_vec());
709/// })
710/// }),
711/// )]);
712/// ```
713#[cfg(feature = "std")]
714pub struct PkHashmapMethod {
715 hashmap: std::collections::HashMap<String, MethodImplementation>,
716}
717
718#[cfg(feature = "std")]
719impl crate::PkMethodAccessor for PkHashmapMethod {
720 fn call(&self, key: String, param: Vec<u8>) -> Result<Pin<Box<dyn crate::Pollable>>, String> {
721 if self.hashmap.contains_key(&key) {
722 let f = self.hashmap.get(&key).unwrap();
723 Ok(f(Some(param)))
724 } else {
725 Err(String::from("Method not found"))
726 }
727 }
728}
729
730#[cfg(feature = "std")]
731impl PkHashmapMethod {
732 /// Creates a new `PkHashmapMethod` instance.
733 ///
734 /// # Arguments
735 /// * `init_vec`: A vector of tuples containing method keys and their corresponding implementation closures.
736 pub fn new(init_vec: Vec<(String, MethodImplementation)>) -> Self {
737 let mut hashmap = std::collections::HashMap::new();
738 for i in init_vec.into_iter() {
739 let (key, method) = i;
740 hashmap.insert(key, method);
741 }
742 PkHashmapMethod { hashmap }
743 }
744}
745
746/// A simple implementation of `Pollable` that executes tasks in a background thread.
747///
748///
749///
750/// This is similar to a JavaScript Promise. It allows offloading work to another thread
751/// and polling for its completion within the PK protocol's `poll()` cycle.
752///
753/// **Note**: This is only available when the `std` feature is enabled.
754#[derive(Clone)]
755#[cfg(feature = "std")]
756pub struct PkPromise {
757 return_value: Arc<RwLock<Option<Vec<u8>>>>,
758}
759#[cfg(feature = "std")]
760impl PkPromise {
761 /// Executes a closure in a new thread and returns a `Pollable` handle.
762 ///
763 /// The provided closure `function` receives a `resolve` callback. Call `resolve(data)`
764 /// when the task is complete to make the data available.
765 ///
766 /// # Arguments
767 /// * `function`: A closure that performs the task and calls the provided `resolve` callback.
768 ///
769 /// # Example
770 /// ```
771 /// use pk_command::PkPromise;
772 /// let promise = PkPromise::execute(|resolve| {
773 /// // Do expensive work...
774 /// resolve(b"done".to_vec());
775 /// });
776 /// ```
777 ///
778 /// # Example with PK Command
779 /// ```
780 /// use pk_command::{PkCommand, PkCommandConfig, PkHashmapMethod, PkHashmapVariable, PkPromise};
781 /// let vars = PkHashmapVariable::new(vec![]);
782 /// let methods = PkHashmapMethod::new(vec![(
783 /// "LONGT".to_string(),
784 /// Box::new(|param| {
785 /// PkPromise::execute(|resolve| {
786 /// // do_something_with(param);
787 /// resolve(b"task complete".to_vec());
788 /// })
789 /// }),
790 /// )]);
791 /// let config = PkCommandConfig::default(64);
792 /// let pk = PkCommand::<_, _, std::time::Instant>::new(config, vars, methods);
793 /// // main loop...
794 /// ```
795 #[cfg(feature = "std")]
796 pub fn execute<T>(function: T) -> Pin<Box<Self>>
797 where
798 T: FnOnce(Box<dyn FnOnce(Vec<u8>) + Send + 'static>) + Send + 'static,
799 {
800 let return_value_arc = Arc::new(RwLock::new(None));
801 let return_value_clone = return_value_arc.clone();
802 std::thread::spawn(move || {
803 let resolve: Box<dyn FnOnce(Vec<u8>) + Send + 'static> =
804 Box::new(move |ret: Vec<u8>| {
805 // This resolve function is called by the user's function
806 *return_value_clone.write().unwrap() = Some(ret);
807 });
808 function(Box::new(resolve));
809 });
810 Box::pin(PkPromise {
811 return_value: return_value_arc,
812 })
813 }
814}
815#[cfg(feature = "std")]
816impl crate::Pollable for PkPromise {
817 fn poll(&self) -> std::task::Poll<Result<Option<Vec<u8>>, String>> {
818 let read_guard = self.return_value.read().unwrap();
819 match read_guard.as_ref() {
820 Some(data) => std::task::Poll::Ready(Ok(Some(data.clone()))),
821 None => std::task::Poll::Pending,
822 }
823 }
824}