pub struct PkCommand<VA, MA, Instant>where
VA: PkVariableAccessor,
MA: PkMethodAccessor,
Instant: PkInstant + Add<Duration, Output = Instant> + PartialOrd + Copy,{ /* private fields */ }Expand description
The main state machine for handling the PK Command protocol.
It manages the lifecycle of a transaction, including:
- Parsing incoming raw bytes into commands.
- Generating response commands (ACKs, data slices, etc.).
- Handling timeouts and retransmissions.
- Managing data slicing for large transfers.
This struct is generic over:
VA: APkVariableAccessorfor variable storage.MA: APkMethodAccessorfor method invocation.Instant: APkInstantfor time tracking (allowing forno_stdtimer implementations). Typicallystd::time::Instantinstdenvironments, orEmbassyInstantin Embassy environments.
§Usage Pattern
- Feed received data into
incoming_command(). - Regularly call
poll()to progress the state machine and check for commands to send. - If
poll()returnsSome(Command), serialize it withto_bytes()and send it over your transport.
§Host vs Device
They are not actual device types, but rather roles in a transaction.
- Host is the one who calls
perform()to initiate a transaction (e.g.,SENDV,INVOK). - Device is the one who reacts against the transaction and automatically responds to incoming root commands using the provided accessors.
§Example
use pk_command::{PkCommand, PkCommandConfig, PkHashmapMethod, PkHashmapVariable};
let config = PkCommandConfig::default(64);
let vars = PkHashmapVariable::new(vec![]);
let methods = PkHashmapMethod::new(vec![]);
let pk = PkCommand::<_, _, std::time::Instant>::new(config, vars, methods);
loop {
// 1. Receive data from transport...
if let Some(received_bytes) = transport.recv() {
pk.incoming_command(received_bytes);
}
// 2. Drive the state machine
if let Some(cmd) = pk.poll() {
let bytes = cmd.to_bytes();
transport.send(bytes);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}Implementations§
Source§impl<VA: PkVariableAccessor, MA: PkMethodAccessor, Instant: PkInstant + Add<Duration, Output = Instant> + PartialOrd + Copy> PkCommand<VA, MA, Instant>
impl<VA: PkVariableAccessor, MA: PkMethodAccessor, Instant: PkInstant + Add<Duration, Output = Instant> + PartialOrd + Copy> PkCommand<VA, MA, Instant>
Sourcepub fn incoming_command(
&self,
command_bytes: Vec<u8>,
) -> Result<(), &'static str>
pub fn incoming_command( &self, command_bytes: Vec<u8>, ) -> Result<(), &'static str>
Ingests a raw command received from the other party.
This should be called whenever new bytes arrive on your transport layer. The
state machine will parse the bytes and update its internal buffers for the
next poll() cycle.
§Arguments
command_bytes: The raw bytes of the received command.
§Returns
Ok(()) if the command was successfully parsed and buffered.
Err(&'static str) if parsing failed (e.g., invalid format, unknown operation).
Sourcepub fn poll(&self) -> Option<Command>
pub fn poll(&self) -> Option<Command>
Polls the state machine for progress and pending actions.
See PkCommand for more details.
This method must be called frequently in your main loop. It handles:
- Processing: Consuming commands received via
incoming_command. - Execution: Running device-side logic (variable access, method polling).
- Protocol Flow: Automatically generating ACKs, data slices, and ENDTRs.
- Reliability: Handling timeouts and retransmitting lost packets.
§Returns
Some(Command): A command that needs to be sent to the peer. Serialize it withto_bytes()and transmit it.None: No action required at this time.
Sourcepub fn perform(
&self,
operation: Operation,
object: Option<String>,
data: Option<Vec<u8>>,
) -> Result<(), &'static str>
pub fn perform( &self, operation: Operation, object: Option<String>, data: Option<Vec<u8>>, ) -> Result<(), &'static str>
Initiates a new root operation from the Host side.
This starts a new transaction chain. It can only be called when the state machine is Idle.
The actual protocol exchange (beginning with a START packet) is driven by subsequent poll() calls.
§Arguments
operation: The root operation to perform (SENDV,REQUV,INVOK, orPKVER).object: The target name (e.g., variable name forREQUV, method name forINVOK).data: Optional parameter data (e.g., the value to set forSENDV).
§Returns
Ok(()): The transaction was successfully queued.Err(&'static str): The request was invalid (e.g., already in a transaction, not a root op).
Sourcepub fn is_complete(&self) -> bool
pub fn is_complete(&self) -> bool
Returns true if the state machine is currently Idle (no active transaction).
Sourcepub fn get_return_data(&self) -> Option<Vec<u8>>
pub fn get_return_data(&self) -> Option<Vec<u8>>
Retrieves the return data from a finished transaction and resets the transaction state.
This should be called by the Host after is_complete() returns true for a root
operation that expects return data (e.g., REQUV or INVOK).
§Returns
Some(Vec<u8>): The returned payload.None: If there was no data or the state machine is not in a completed host state.
Sourcepub fn wait_for_complete_and<F>(&self, callback: F) -> bool
pub fn wait_for_complete_and<F>(&self, callback: F) -> bool
Checks for transaction completion and executes a callback with the resulting data.
This is a convenience method for polling for completion on the Host side.
If the transaction is complete, it calls the callback with the return data
(if any) and returns true.
§Note
This function is also poll-based. You should also call it regularly (e.g., in your main loop), and when the transaction completes, it would call the provided function.
§Returns
true if the transaction was complete and the callback was executed.
§Example
use pk_command::{PkCommand, PkCommandConfig, PkHashmapVariable, PkHashmapMethod};
let config = PkCommandConfig::default(64);
let vars = PkHashmapVariable::new(vec![]);
let methods = PkHashmapMethod::new(vec![]);
let pk = PkCommand::<_, _, std::time::Instant>::new(config, vars, methods);
loop {
// 1. Receive data from transport...
if let Some(received_data) = transport.recv() {
pk.incoming_command(received_data);
}
// 3. perform some operation
if some_condition && pk.is_complete() {
pk.perform(operation, object, data).unwrap();
}
// 4. poll
let cmd=pk.poll();
// 5. check for completion and handle return data
// We can see that this function is poll-based as well, and should be called regularly. (typically
// right after calling `poll()`) When the transaction completes, it would call the provided function
// with the return data (if any).
let mut should_break=false;
pk.wait_for_complete_and(|data_opt| {
println!("Transaction complete! Return data: {:?}", data_opt);
should_break=true;
});
// 6. Send cmd back via transport...
if let Some(cmd_to_send) = cmd {
transport.send(cmd_to_send.to_bytes());
}
// 7. break if needed
if should_break {
break;
}
}Sourcepub fn new(
config: PkCommandConfig,
variable_accessor: VA,
method_accessor: MA,
) -> Self
pub fn new( config: PkCommandConfig, variable_accessor: VA, method_accessor: MA, ) -> Self
Creates a new PkCommand state machine.
§Arguments
config: Configuration defining timeouts and packet limits.variable_accessor: Provider for reading/writing variables.method_accessor: Provider for invoking methods.
§Note
§The Instant Type Parameter
The Instant type parameter must implement PkInstant, Copy, PartialOrd, and Add<Duration>.
We provide a default implementation for std::time::Instant in std environments,
so that could be used directly.
For no_std environments, users can implement their own PkInstant and use it here.
§The VA, MA Type Parameters
In std environments, the library provides two convenient implementations for variable
and method accessors: PkHashmapVariable and PkHashmapMethod, which use
HashMaps internally. You can use them directly or implement
your own if you have different storage needs.
In no_std environments, you must provide your own implementations of VA, MA,
and Instant.
§For Embassy Users
The library provides no_std utilities, based on the Embassy ecosystem,
to help you integrate PK Command into a Embassy-based application.
They are:
EmbassyInstantforInstant.EmbassyPollableforPollable, andembassy_method_accessor!forPkMethodAccessor.
Unfortunately still, you may need to provide your own implementation of PkVariableAccessor.
§Example
use pk_command::{PkCommand, PkCommandConfig, PkHashmapVariable, PkHashmapMethod};
// The third type parameter here is the PkInstant type. This can't be usually inferred so you must
// specify it explicitly. If you are using std, just use std::time::Instant. If you are using no_std,
// implement your own PkInstant and specify it here.
let pk = PkCommand::<_, _, std::time::Instant>::new(
PkCommandConfig::default(64),
PkHashmapVariable::new(vec![]),
PkHashmapMethod::new(vec![]),
);