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}