///|
/// A polling job with a deterministic logical-clock schedule.
pub(all) struct PollJob {
id : Int
name : String
request : Frame
interval : Int
timeout : Int
retries : Int
mut next_due : Int
mut attempts : Int
mut runs : Int
mut failures : Int
mut last_response : Frame?
}
///|
pub fn PollJob::new(
id : Int,
name : String,
request : Frame,
interval? : Int = 1000,
timeout? : Int = 250,
retries? : Int = 1,
) -> Result[PollJob, ModbusError] {
if id < 0 || interval < 1 || timeout < 1 || retries < 0 {
Err(InvalidData)
} else {
Ok({
id,
name,
request,
interval,
timeout,
retries,
next_due: 0,
attempts: 0,
runs: 0,
failures: 0,
last_response: None,
})
}
}
///|
pub fn PollJob::is_due(self : PollJob, now : Int) -> Bool {
now >= self.next_due
}
///|
pub fn PollJob::mark_success(
self : PollJob,
now : Int,
response : Frame,
) -> Unit {
self.runs += 1
self.attempts = 0
self.last_response = Some(response)
self.next_due = now + self.interval
}
///|
pub fn PollJob::mark_failure(self : PollJob, now : Int) -> Unit {
self.failures += 1
self.attempts += 1
if self.attempts > self.retries {
self.attempts = 0
self.next_due = now + self.interval
} else {
let backoff = self.timeout * (1 << (self.attempts - 1))
self.next_due = now + backoff
}
}
///|
pub fn PollJob::success_rate(self : PollJob) -> Float {
let total = self.runs + self.failures
if total == 0 {
1.0
} else {
Float::from_int(self.runs) / Float::from_int(total)
}
}
///|
/// A polling plan keeps jobs ordered and addresses them by stable ids.
pub struct PollPlan {
jobs : Array[PollJob]
max_jobs : Int
}
///|
pub fn PollPlan::new(max_jobs? : Int = 256) -> Result[PollPlan, ModbusError] {
if max_jobs < 1 {
Err(CapacityExceeded)
} else {
Ok({ jobs: [], max_jobs })
}
}
///|
pub fn PollPlan::add(
self : PollPlan,
job : PollJob,
) -> Result[Unit, ModbusError] {
if self.jobs.length() >= self.max_jobs {
return Err(CapacityExceeded)
}
for existing in self.jobs {
if existing.id == job.id || existing.name == job.name {
return Err(InvalidData)
}
}
self.jobs.push(job)
Ok(())
}
///|
pub fn PollPlan::remove(self : PollPlan, id : Int) -> Result[Unit, ModbusError] {
let found = self.find_index(id)
match found {
None => Err(InvalidAddress)
Some(index) => {
let remaining : Array[PollJob] = []
for current in 0.. Int {
self.jobs.length()
}
///|
pub fn PollPlan::job(self : PollPlan, id : Int) -> PollJob? {
match self.find_index(id) {
Some(index) => Some(self.jobs[index])
None => None
}
}
///|
pub fn PollPlan::due(self : PollPlan, now : Int) -> Array[PollJob] {
let out : Array[PollJob] = []
for job in self.jobs {
if job.is_due(now) {
out.push(job)
}
}
out
}
///|
pub fn PollPlan::mark_success(
self : PollPlan,
id : Int,
now : Int,
response : Frame,
) -> Result[Unit, ModbusError] {
match self.find_index(id) {
None => Err(InvalidAddress)
Some(index) => {
self.jobs[index].mark_success(now, response)
Ok(())
}
}
}
///|
pub fn PollPlan::mark_failure(
self : PollPlan,
id : Int,
now : Int,
) -> Result[Unit, ModbusError] {
match self.find_index(id) {
None => Err(InvalidAddress)
Some(index) => {
self.jobs[index].mark_failure(now)
Ok(())
}
}
}
///|
pub fn PollPlan::snapshot(self : PollPlan) -> Array[PollJob] {
let out : Array[PollJob] = []
for job in self.jobs {
out.push(job)
}
out
}
///|
fn PollPlan::find_index(self : PollPlan, id : Int) -> Int? {
for index in 0.. PollReport {
let due = self.due(now)
let responses : Array[Frame] = []
let mut succeeded = 0
let mut failed = 0
for job in due {
match device.handle(job.request) {
Ok(response) => {
let _ = self.mark_success(job.id, now, response)
responses.push(response)
succeeded += 1
}
Err(_) => {
let _ = self.mark_failure(job.id, now)
failed += 1
}
}
}
{ now, attempted: due.length(), succeeded, failed, responses }
}
///|
/// A scheduler that records the last report and applies a transport mode.
pub struct PollScheduler {
plan : PollPlan
device : Device
mode : Mode
mut last_report : PollReport?
}
///|
pub fn PollScheduler::new(
plan : PollPlan,
device : Device,
mode : Mode,
) -> PollScheduler {
{ plan, device, mode, last_report: None }
}
///|
pub fn PollScheduler::tick(self : PollScheduler, now : Int) -> PollReport {
let report = self.plan.run_once(self.device, now)
self.last_report = Some(report)
report
}
///|
pub fn PollScheduler::plan(self : PollScheduler) -> PollPlan {
self.plan
}
///|
pub fn PollScheduler::mode(self : PollScheduler) -> Mode {
self.mode
}
///|
pub fn PollScheduler::last_report(self : PollScheduler) -> PollReport? {
self.last_report
}
///|
/// Validate plan timing invariants before deployment.
pub fn validate_poll_plan(plan : PollPlan) -> Result[Unit, ModbusError] {
let jobs = plan.snapshot()
for job in jobs {
if job.interval < job.timeout || job.timeout < 1 {
return Err(InvalidData)
}
match validate_frame(job.request, false) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
Ok(())
}