///|
/// Type of maintenance activity tracked for a field device.
pub(all) enum MaintenanceKind {
Inspection
Calibration
Cleaning
FirmwareUpdate
CableReplacement
PreventiveService
CorrectiveRepair
} derive(Debug, Eq)
///|
/// State of a maintenance work order.
pub(all) enum WorkOrderState {
Planned
Assigned
InProgress
Blocked
Completed
Cancelled
} derive(Debug, Eq)
///|
/// Priority assigned by an operator or by an automated due-date rule.
pub(all) enum MaintenancePriority {
Routine
Important
Urgent
Critical
} derive(Debug, Eq)
///|
/// A repeatable service plan for one device or profile point.
pub(all) struct ServicePlan {
id : String
asset_id : String
title : String
kind : MaintenanceKind
interval : Int
grace : Int
estimated_minutes : Int
mut enabled : Bool
mut last_completed : Int?
owner : String
}
///|
pub fn ServicePlan::new(
id : String,
asset_id : String,
title : String,
kind : MaintenanceKind,
interval : Int,
grace? : Int = 0,
estimated_minutes? : Int = 30,
owner? : String = "",
) -> Result[ServicePlan, ModbusError] {
if id == "" || asset_id == "" || title == "" {
return Err(InvalidData)
}
if interval < 1 || grace < 0 || estimated_minutes < 1 {
return Err(InvalidData)
}
Ok({
id,
asset_id,
title,
kind,
interval,
grace,
estimated_minutes,
enabled: true,
last_completed: None,
owner,
})
}
///|
pub fn ServicePlan::id(self : ServicePlan) -> String {
self.id
}
///|
pub fn ServicePlan::asset_id(self : ServicePlan) -> String {
self.asset_id
}
///|
pub fn ServicePlan::title(self : ServicePlan) -> String {
self.title
}
///|
pub fn ServicePlan::kind(self : ServicePlan) -> MaintenanceKind {
self.kind
}
///|
pub fn ServicePlan::interval(self : ServicePlan) -> Int {
self.interval
}
///|
pub fn ServicePlan::grace(self : ServicePlan) -> Int {
self.grace
}
///|
pub fn ServicePlan::estimated_minutes(self : ServicePlan) -> Int {
self.estimated_minutes
}
///|
pub fn ServicePlan::owner(self : ServicePlan) -> String {
self.owner
}
///|
pub fn ServicePlan::enabled(self : ServicePlan) -> Bool {
self.enabled
}
///|
pub fn ServicePlan::last_completed(self : ServicePlan) -> Int? {
self.last_completed
}
///|
pub fn ServicePlan::set_enabled(self : ServicePlan, enabled : Bool) -> Unit {
self.enabled = enabled
}
///|
pub fn ServicePlan::due_at(self : ServicePlan) -> Int? {
match self.last_completed {
Some(timestamp) => Some(timestamp + self.interval)
None => None
}
}
///|
pub fn ServicePlan::mark_completed(
self : ServicePlan,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if timestamp < 0 {
return Err(InvalidData)
}
match self.last_completed {
Some(previous) => if timestamp < previous { return Err(InvalidData) }
None => ()
}
self.last_completed = Some(timestamp)
Ok(())
}
///|
pub fn ServicePlan::is_due(
self : ServicePlan,
now : Int,
) -> Result[Bool, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
if !self.enabled {
return Ok(false)
}
match self.due_at() {
Some(due) => Ok(now >= due)
None => Ok(true)
}
}
///|
pub fn ServicePlan::is_overdue(
self : ServicePlan,
now : Int,
) -> Result[Bool, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
if !self.enabled {
return Ok(false)
}
match self.due_at() {
Some(due) => Ok(now > due + self.grace)
None => Ok(false)
}
}
///|
pub fn ServicePlan::priority(
self : ServicePlan,
now : Int,
) -> Result[MaintenancePriority, ModbusError] {
match self.is_overdue(now) {
Err(error) => Err(error)
Ok(true) => Ok(Critical)
Ok(false) =>
match self.is_due(now) {
Err(error) => Err(error)
Ok(true) => Ok(Important)
Ok(false) => Ok(Routine)
}
}
}
///|
pub fn maintenance_kind_name(kind : MaintenanceKind) -> String {
match kind {
Inspection => "inspection"
Calibration => "calibration"
Cleaning => "cleaning"
FirmwareUpdate => "firmware-update"
CableReplacement => "cable-replacement"
PreventiveService => "preventive-service"
CorrectiveRepair => "corrective-repair"
}
}
///|
pub fn maintenance_priority_name(priority : MaintenancePriority) -> String {
match priority {
Routine => "routine"
Important => "important"
Urgent => "urgent"
Critical => "critical"
}
}
///|
pub fn work_order_state_name(state : WorkOrderState) -> String {
match state {
Planned => "planned"
Assigned => "assigned"
InProgress => "in-progress"
Blocked => "blocked"
Completed => "completed"
Cancelled => "cancelled"
}
}
///|
/// A historical service record including measurements and the operator note.
pub(all) struct ServiceRecord {
id : String
plan_id : String
asset_id : String
started_at : Int
finished_at : Int
technician : String
result : String
readings : Array[Int]
parts : Array[String]
}
///|
pub fn ServiceRecord::new(
id : String,
plan_id : String,
asset_id : String,
started_at : Int,
finished_at : Int,
technician : String,
result : String,
) -> Result[ServiceRecord, ModbusError] {
if id == "" ||
plan_id == "" ||
asset_id == "" ||
technician == "" ||
result == "" {
return Err(InvalidData)
}
if started_at < 0 || finished_at < started_at {
return Err(InvalidData)
}
Ok({
id,
plan_id,
asset_id,
started_at,
finished_at,
technician,
result,
readings: [],
parts: [],
})
}
///|
pub fn ServiceRecord::id(self : ServiceRecord) -> String {
self.id
}
///|
pub fn ServiceRecord::plan_id(self : ServiceRecord) -> String {
self.plan_id
}
///|
pub fn ServiceRecord::asset_id(self : ServiceRecord) -> String {
self.asset_id
}
///|
pub fn ServiceRecord::started_at(self : ServiceRecord) -> Int {
self.started_at
}
///|
pub fn ServiceRecord::finished_at(self : ServiceRecord) -> Int {
self.finished_at
}
///|
pub fn ServiceRecord::technician(self : ServiceRecord) -> String {
self.technician
}
///|
pub fn ServiceRecord::result(self : ServiceRecord) -> String {
self.result
}
///|
pub fn ServiceRecord::duration(self : ServiceRecord) -> Int {
self.finished_at - self.started_at
}
///|
pub fn ServiceRecord::add_reading(self : ServiceRecord, reading : Int) -> Unit {
self.readings.push(reading)
}
///|
pub fn ServiceRecord::add_part(
self : ServiceRecord,
part : String,
) -> Result[Unit, ModbusError] {
if part == "" {
Err(InvalidData)
} else {
self.parts.push(part)
Ok(())
}
}
///|
pub fn ServiceRecord::reading_count(self : ServiceRecord) -> Int {
self.readings.length()
}
///|
pub fn ServiceRecord::part_count(self : ServiceRecord) -> Int {
self.parts.length()
}
///|
pub fn ServiceRecord::average_reading(
self : ServiceRecord,
) -> Result[Int, ModbusError] {
if self.readings.length() == 0 {
return Err(Incomplete)
}
let mut sum = 0
for value in self.readings {
sum += value
}
Ok(sum / self.readings.length())
}
///|
/// An actionable unit of work generated from a service plan or alarm.
pub(all) struct MaintenanceWorkOrder {
id : String
asset_id : String
plan_id : String?
title : String
priority : MaintenancePriority
mut state : WorkOrderState
created_at : Int
due_at : Int
mut assigned_to : String?
mut started_at : Int?
mut finished_at : Int?
mut note : String
}
///|
pub fn MaintenanceWorkOrder::new(
id : String,
asset_id : String,
title : String,
priority : MaintenancePriority,
created_at : Int,
due_at : Int,
plan_id? : String = "",
) -> Result[MaintenanceWorkOrder, ModbusError] {
if id == "" ||
asset_id == "" ||
title == "" ||
created_at < 0 ||
due_at < created_at {
return Err(InvalidData)
}
let selected_plan = if plan_id == "" { None } else { Some(plan_id) }
Ok({
id,
asset_id,
plan_id: selected_plan,
title,
priority,
state: Planned,
created_at,
due_at,
assigned_to: None,
started_at: None,
finished_at: None,
note: "",
})
}
///|
pub fn MaintenanceWorkOrder::id(self : MaintenanceWorkOrder) -> String {
self.id
}
///|
pub fn MaintenanceWorkOrder::asset_id(self : MaintenanceWorkOrder) -> String {
self.asset_id
}
///|
pub fn MaintenanceWorkOrder::plan_id(self : MaintenanceWorkOrder) -> String? {
self.plan_id
}
///|
pub fn MaintenanceWorkOrder::title(self : MaintenanceWorkOrder) -> String {
self.title
}
///|
pub fn MaintenanceWorkOrder::priority(
self : MaintenanceWorkOrder,
) -> MaintenancePriority {
self.priority
}
///|
pub fn MaintenanceWorkOrder::state(
self : MaintenanceWorkOrder,
) -> WorkOrderState {
self.state
}
///|
pub fn MaintenanceWorkOrder::created_at(self : MaintenanceWorkOrder) -> Int {
self.created_at
}
///|
pub fn MaintenanceWorkOrder::due_at(self : MaintenanceWorkOrder) -> Int {
self.due_at
}
///|
pub fn MaintenanceWorkOrder::assigned_to(
self : MaintenanceWorkOrder,
) -> String? {
self.assigned_to
}
///|
pub fn MaintenanceWorkOrder::note(self : MaintenanceWorkOrder) -> String {
self.note
}
///|
pub fn MaintenanceWorkOrder::assign(
self : MaintenanceWorkOrder,
technician : String,
) -> Result[Unit, ModbusError] {
if technician == "" || self.state == Completed || self.state == Cancelled {
return Err(Busy)
}
self.assigned_to = Some(technician)
self.state = Assigned
Ok(())
}
///|
pub fn MaintenanceWorkOrder::start(
self : MaintenanceWorkOrder,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if timestamp < self.created_at ||
self.state == Completed ||
self.state == Cancelled {
return Err(InvalidData)
}
match self.assigned_to {
None => return Err(Busy)
Some(_) => ()
}
self.started_at = Some(timestamp)
self.state = InProgress
Ok(())
}
///|
pub fn MaintenanceWorkOrder::block(
self : MaintenanceWorkOrder,
note : String,
) -> Result[Unit, ModbusError] {
if note == "" || self.state == Completed || self.state == Cancelled {
return Err(InvalidData)
}
self.note = note
self.state = Blocked
Ok(())
}
///|
pub fn MaintenanceWorkOrder::continue_work(
self : MaintenanceWorkOrder,
) -> Result[Unit, ModbusError] {
if self.state != Blocked {
return Err(Busy)
}
self.state = match self.assigned_to {
Some(_) => InProgress
None => Planned
}
Ok(())
}
///|
pub fn MaintenanceWorkOrder::complete(
self : MaintenanceWorkOrder,
timestamp : Int,
note? : String = "",
) -> Result[Unit, ModbusError] {
if timestamp < self.created_at || self.state == Cancelled {
return Err(InvalidData)
}
match self.started_at {
Some(started) => if timestamp < started { return Err(InvalidData) }
None => return Err(Busy)
}
self.finished_at = Some(timestamp)
if note != "" {
self.note = note
}
self.state = Completed
Ok(())
}
///|
pub fn MaintenanceWorkOrder::cancel(
self : MaintenanceWorkOrder,
reason : String,
) -> Result[Unit, ModbusError] {
if reason == "" || self.state == Completed {
return Err(Busy)
}
self.note = reason
self.state = Cancelled
Ok(())
}
///|
pub fn MaintenanceWorkOrder::is_due(
self : MaintenanceWorkOrder,
now : Int,
) -> Bool {
now >= self.due_at && self.state != Completed && self.state != Cancelled
}
///|
pub fn MaintenanceWorkOrder::is_overdue(
self : MaintenanceWorkOrder,
now : Int,
) -> Bool {
now > self.due_at && self.state != Completed && self.state != Cancelled
}
///|
pub fn MaintenanceWorkOrder::duration(self : MaintenanceWorkOrder) -> Int? {
match (self.started_at, self.finished_at) {
(Some(started), Some(finished)) => Some(finished - started)
_ => None
}
}
///|
/// Plan and audit maintenance across multiple devices.
pub(all) struct MaintenancePlanner {
plans : Array[ServicePlan]
records : Array[ServiceRecord]
work_orders : Array[MaintenanceWorkOrder]
max_records : Int
max_orders : Int
}
///|
pub fn MaintenancePlanner::new(
max_records? : Int = 4096,
max_orders? : Int = 4096,
) -> Result[MaintenancePlanner, ModbusError] {
if max_records < 1 || max_orders < 1 {
Err(InvalidData)
} else {
Ok({ plans: [], records: [], work_orders: [], max_records, max_orders })
}
}
///|
pub fn MaintenancePlanner::plan_count(self : MaintenancePlanner) -> Int {
self.plans.length()
}
///|
pub fn MaintenancePlanner::record_count(self : MaintenancePlanner) -> Int {
self.records.length()
}
///|
pub fn MaintenancePlanner::work_order_count(self : MaintenancePlanner) -> Int {
self.work_orders.length()
}
///|
pub fn MaintenancePlanner::add_plan(
self : MaintenancePlanner,
plan : ServicePlan,
) -> Result[Unit, ModbusError] {
for existing in self.plans {
if existing.id() == plan.id() {
return Err(InvalidData)
}
}
self.plans.push(plan)
Ok(())
}
///|
pub fn MaintenancePlanner::plan(
self : MaintenancePlanner,
id : String,
) -> ServicePlan? {
for plan in self.plans {
if plan.id() == id {
return Some(plan)
}
}
None
}
///|
pub fn MaintenancePlanner::add_record(
self : MaintenancePlanner,
record : ServiceRecord,
) -> Result[Unit, ModbusError] {
if self.records.length() >= self.max_records {
return Err(CapacityExceeded)
}
self.records.push(record)
Ok(())
}
///|
pub fn MaintenancePlanner::add_work_order(
self : MaintenancePlanner,
order : MaintenanceWorkOrder,
) -> Result[Unit, ModbusError] {
if self.work_orders.length() >= self.max_orders {
return Err(CapacityExceeded)
}
for existing in self.work_orders {
if existing.id() == order.id() {
return Err(InvalidData)
}
}
self.work_orders.push(order)
Ok(())
}
///|
pub fn MaintenancePlanner::work_order(
self : MaintenancePlanner,
id : String,
) -> MaintenanceWorkOrder? {
for order in self.work_orders {
if order.id() == id {
return Some(order)
}
}
None
}
///|
/// Generate due work orders for plans that have not been scheduled at `now`.
pub fn MaintenancePlanner::generate_due(
self : MaintenancePlanner,
now : Int,
) -> Result[Int, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let mut created = 0
for plan in self.plans {
match plan.is_due(now) {
Err(error) => return Err(error)
Ok(false) => ()
Ok(true) => {
let mut already_open = false
for order in self.work_orders {
match order.plan_id() {
Some(plan_id) =>
if plan_id == plan.id() &&
order.state() != Completed &&
order.state() != Cancelled {
already_open = true
}
None => ()
}
}
if !already_open &&
self.work_orders.length() + created < self.max_orders {
let priority = match plan.priority(now) {
Ok(value) => value
Err(error) => return Err(error)
}
let order_id = "auto-" + plan.id() + "-" + now.to_string()
let order = match
MaintenanceWorkOrder::new(
order_id,
plan.asset_id(),
plan.title(),
priority,
now,
now,
plan_id=plan.id(),
) {
Ok(value) => value
Err(error) => return Err(error)
}
self.work_orders.push(order)
created += 1
}
}
}
}
Ok(created)
}
///|
/// Complete a linked order and update the corresponding service plan.
pub fn MaintenancePlanner::complete_order(
self : MaintenancePlanner,
order_id : String,
timestamp : Int,
note? : String = "",
) -> Result[Unit, ModbusError] {
for order in self.work_orders {
if order.id() == order_id {
match order.plan_id() {
Some(plan_id) =>
for plan in self.plans {
if plan.id() == plan_id {
ignore(plan.mark_completed(timestamp))
}
}
None => ()
}
return order.complete(timestamp, note~)
}
}
Err(InvalidData)
}
///|
/// Return plans due or overdue at a timestamp.
pub fn MaintenancePlanner::due_plans(
self : MaintenancePlanner,
now : Int,
) -> Result[Array[String], ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let result : Array[String] = []
for plan in self.plans {
match plan.is_due(now) {
Ok(true) => result.push(plan.id())
Err(error) => return Err(error)
_ => ()
}
}
Ok(result)
}
///|
/// Return the IDs of open work orders, optionally limited to overdue work.
pub fn MaintenancePlanner::open_orders(
self : MaintenancePlanner,
now : Int,
overdue_only? : Bool = false,
) -> Result[Array[String], ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let result : Array[String] = []
for order in self.work_orders {
if order.state() != Completed && order.state() != Cancelled {
if !overdue_only || order.is_overdue(now) {
result.push(order.id())
}
}
}
Ok(result)
}
///|
/// Count service records for an asset within a time range.
pub fn MaintenancePlanner::history_count(
self : MaintenancePlanner,
asset_id : String,
start : Int,
end : Int,
) -> Result[Int, ModbusError] {
if asset_id == "" || start < 0 || end < start {
return Err(InvalidData)
}
let mut count = 0
for record in self.records {
if record.asset_id() == asset_id &&
record.finished_at() >= start &&
record.finished_at() <= end {
count += 1
}
}
Ok(count)
}
///|
/// Summary for maintenance dashboards and export jobs.
pub(all) struct MaintenanceSummary {
plans : Int
due : Int
overdue : Int
completed : Int
open : Int
records : Int
}
///|
pub fn MaintenancePlanner::summary(
self : MaintenancePlanner,
now : Int,
) -> Result[MaintenanceSummary, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let mut due = 0
for plan in self.plans {
match plan.is_due(now) {
Ok(true) => due += 1
_ => ()
}
}
let mut overdue = 0
let mut completed = 0
let mut open = 0
for order in self.work_orders {
match order.state() {
Completed => completed += 1
Cancelled => ()
_ => {
open += 1
if order.is_overdue(now) {
overdue += 1
}
}
}
}
Ok({
plans: self.plans.length(),
due,
overdue,
completed,
open,
records: self.records.length(),
})
}
///|
pub fn MaintenanceSummary::plans(self : MaintenanceSummary) -> Int {
self.plans
}
///|
pub fn MaintenanceSummary::due(self : MaintenanceSummary) -> Int {
self.due
}
///|
pub fn MaintenanceSummary::overdue(self : MaintenanceSummary) -> Int {
self.overdue
}
///|
pub fn MaintenanceSummary::completed(self : MaintenanceSummary) -> Int {
self.completed
}
///|
pub fn MaintenanceSummary::open(self : MaintenanceSummary) -> Int {
self.open
}
///|
pub fn MaintenanceSummary::records(self : MaintenanceSummary) -> Int {
self.records
}
///|
pub fn MaintenanceSummary::summary(self : MaintenanceSummary) -> String {
"plans=" +
self.plans.to_string() +
" due=" +
self.due.to_string() +
" overdue=" +
self.overdue.to_string() +
" open=" +
self.open.to_string() +
" completed=" +
self.completed.to_string() +
" records=" +
self.records.to_string()
}