Persistent state
RoboPLC provides a helper module to save and load the program persistent state. The state can be shared between program instances on restart/reload events.
The state is saved into /var/roboplc/data folder, so it is purged on purge manager command.
If the state file extension is .json, the state is saved in JSON format. Otherwise, the state is saved in MessagePack.
Required features and 3rd party crates
To enable JSON support, enable json feature of roboplc crate.
To enable MessagePack support, enable msgpack feature of roboplc crate.
Both serialization formats require serde::Serialize and serde::Deserialize traits to be implemented for the persistent state structure. Add serde crate to the dependencies:
cargo add serde --features derive
Example
Here is an example of a program which saves and loads the persistent state. In case if the state is not found or failed to load, the default state is used.
use roboplc::controller::prelude::*;
use roboplc::prelude::*;
use rtsc::pi::Mutex;
use rtsc::time::interval;
use serde::{Deserialize, Serialize};
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
const STATE_FILE: &str = "current.json";
type Message = ();
#[derive(Default)]
struct Variables {
// This is a persistent data that is saved to disk
persistent_data: Mutex<Data>,
// This is a runtime data that is not saved to disk
_runtime_data: u32,
}
// The persistent data structure
#[derive(Serialize, Deserialize, Default)]
struct Data {
value1: u32,
value2: u32,
}
// This worker automatically saves the persistent data to disk every 10 seconds
#[derive(WorkerOpts)]
#[worker_opts(cpu = 0, priority = 50, scheduling = "fifo", blocking = true)]
struct StateSaver {}
impl Worker<Message, Variables> for StateSaver {
fn run(&mut self, context: &Context<Message, Variables>) -> WResult {
for _ in interval(Duration::from_secs(10)).take_while(|_| context.is_online()) {
roboplc::state::save(STATE_FILE, &*context.variables().persistent_data.lock())?;
}
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
roboplc::setup_panic();
roboplc::configure_logger(roboplc::LevelFilter::Info);
if !roboplc::is_production() {
roboplc::set_simulated();
}
roboplc::thread_rt::prealloc_heap(10_000_000)?;
let vars = Variables {
// Load the persistent data from disk or use the default value
persistent_data: Mutex::new(roboplc::state::load(STATE_FILE).unwrap_or_default()),
..Variables::default()
};
let mut controller = Controller::<Message, Variables>::new_with_variables(vars);
controller.spawn_worker(StateSaver {})?;
controller.register_signals(SHUTDOWN_TIMEOUT)?;
controller.block();
// Save the persistent data to disk before exiting
roboplc::state::save(STATE_FILE, &*controller.variables().persistent_data.lock())?;
Ok(())
}
Real-time safety
State load/save I/O operations may block the program execution if a mutex is held. Consider testing the configuration before deploying it to the production. The operation time depends on the number / size of variables and on the disk device I/O speed.
Here are several ways to mitigate the issue:
Use high-quality SSD, SD card or eMMC module with fast I/O speed.
Minimize the number of variables in the persistent state.
Clone the state before saving it to the disk.
Serialize the state into a temporary serde_json::Value before calling roboplc::state:save.
Use a custom save function which releases the mutex right after the data is serialized but before writing it to the disk.