///|
/// Lifecycle state of an enrolled industrial device.
pub(all) enum InventoryState {
InventoryDiscovered
InventoryProvisioned
InventoryOnline
InventoryOffline
InventoryRetired
} derive(Debug, Eq)
///|
/// Firmware and identity metadata used by fleet operations.
pub(all) struct InventoryDevice {
id : String
label : String
vendor : String
model : String
serial : String
firmware : String
unit_id : Byte
transport : String
mut state : InventoryState
mut last_seen : Int?
tags : Array[String]
}
///|
pub fn InventoryDevice::new(
id : String,
label : String,
vendor : String,
model : String,
serial : String,
firmware : String,
unit_id : Byte,
transport : String,
) -> Result[InventoryDevice, ModbusError] {
if id == "" ||
label == "" ||
vendor == "" ||
model == "" ||
serial == "" ||
firmware == "" ||
transport == "" {
return Err(InvalidData)
}
if !is_valid_unit_id(unit_id) {
return Err(InvalidUnitId)
}
Ok({
id,
label,
vendor,
model,
serial,
firmware,
unit_id,
transport,
state: InventoryDiscovered,
last_seen: None,
tags: [],
})
}
///|
pub fn InventoryDevice::id(self : InventoryDevice) -> String {
self.id
}
///|
pub fn InventoryDevice::label(self : InventoryDevice) -> String {
self.label
}
///|
pub fn InventoryDevice::vendor(self : InventoryDevice) -> String {
self.vendor
}
///|
pub fn InventoryDevice::model(self : InventoryDevice) -> String {
self.model
}
///|
pub fn InventoryDevice::serial(self : InventoryDevice) -> String {
self.serial
}
///|
pub fn InventoryDevice::firmware(self : InventoryDevice) -> String {
self.firmware
}
///|
pub fn InventoryDevice::unit_id(self : InventoryDevice) -> Byte {
self.unit_id
}
///|
pub fn InventoryDevice::transport(self : InventoryDevice) -> String {
self.transport
}
///|
pub fn InventoryDevice::state(self : InventoryDevice) -> InventoryState {
self.state
}
///|
pub fn InventoryDevice::last_seen(self : InventoryDevice) -> Int? {
self.last_seen
}
///|
pub fn InventoryDevice::set_state(
self : InventoryDevice,
state : InventoryState,
) -> Unit {
self.state = state
}
///|
pub fn InventoryDevice::add_tag(
self : InventoryDevice,
tag : String,
) -> Result[Unit, ModbusError] {
if tag == "" {
return Err(InvalidData)
}
for existing in self.tags {
if existing == tag {
return Ok(())
}
}
self.tags.push(tag)
Ok(())
}
///|
pub fn InventoryDevice::has_tag(self : InventoryDevice, tag : String) -> Bool {
for existing in self.tags {
if existing == tag {
return true
}
}
false
}
///|
pub fn InventoryDevice::mark_seen(
self : InventoryDevice,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if timestamp < 0 {
return Err(InvalidData)
}
self.last_seen = Some(timestamp)
self.state = InventoryOnline
Ok(())
}
///|
pub fn InventoryDevice::stale(
self : InventoryDevice,
now : Int,
timeout : Int,
) -> Result[Bool, ModbusError] {
if now < 0 || timeout < 1 {
return Err(InvalidData)
}
match self.last_seen {
Some(timestamp) => Ok(now - timestamp > timeout)
None => Ok(true)
}
}
///|
pub fn inventory_state_name(state : InventoryState) -> String {
match state {
InventoryDiscovered => "discovered"
InventoryProvisioned => "provisioned"
InventoryOnline => "online"
InventoryOffline => "offline"
InventoryRetired => "retired"
}
}
///|
/// A fleet inventory with lookup and stale-device reporting.
pub(all) struct DeviceInventory {
devices : Array[InventoryDevice]
max_devices : Int
mut revision : Int
}
///|
pub fn DeviceInventory::new(
max_devices? : Int = 4096,
) -> Result[DeviceInventory, ModbusError] {
if max_devices < 1 {
Err(InvalidData)
} else {
Ok({ devices: [], max_devices, revision: 0 })
}
}
///|
pub fn DeviceInventory::count(self : DeviceInventory) -> Int {
self.devices.length()
}
///|
pub fn DeviceInventory::revision(self : DeviceInventory) -> Int {
self.revision
}
///|
pub fn DeviceInventory::add(
self : DeviceInventory,
device : InventoryDevice,
) -> Result[Unit, ModbusError] {
if self.devices.length() >= self.max_devices {
return Err(CapacityExceeded)
}
for existing in self.devices {
if existing.id() == device.id() || existing.serial() == device.serial() {
return Err(InvalidData)
}
}
self.devices.push(device)
self.revision += 1
Ok(())
}
///|
pub fn DeviceInventory::get(
self : DeviceInventory,
id : String,
) -> InventoryDevice? {
for device in self.devices {
if device.id() == id {
return Some(device)
}
}
None
}
///|
pub fn DeviceInventory::remove(
self : DeviceInventory,
id : String,
) -> Result[InventoryDevice, ModbusError] {
for i in 0.. Array[String] {
let result : Array[String] = []
for device in self.devices {
if device.vendor() == vendor {
result.push(device.id())
}
}
result
}
///|
pub fn DeviceInventory::by_tag(
self : DeviceInventory,
tag : String,
) -> Array[String] {
let result : Array[String] = []
for device in self.devices {
if device.has_tag(tag) {
result.push(device.id())
}
}
result
}
///|
/// Mark devices stale or offline using one common fleet timestamp.
pub fn DeviceInventory::mark_stale(
self : DeviceInventory,
now : Int,
timeout : Int,
) -> Result[Int, ModbusError] {
if now < 0 || timeout < 1 {
return Err(InvalidData)
}
let mut count = 0
for device in self.devices {
match device.stale(now, timeout) {
Err(error) => return Err(error)
Ok(false) => ()
Ok(true) => {
device.set_state(InventoryOffline)
count += 1
}
}
}
Ok(count)
}
///|
/// Return devices by lifecycle state.
pub fn DeviceInventory::by_state(
self : DeviceInventory,
state : InventoryState,
) -> Array[String] {
let result : Array[String] = []
for device in self.devices {
if device.state() == state {
result.push(device.id())
}
}
result
}
///|
/// Stable tabular export for provisioning and support tools.
pub fn DeviceInventory::export_lines(self : DeviceInventory) -> Array[String] {
let lines : Array[String] = []
for device in self.devices {
lines.push(
device.id() +
"\t" +
device.serial() +
"\t" +
device.vendor() +
"\t" +
device.model() +
"\tunit=" +
device.unit_id().to_int().to_string() +
"\ttransport=" +
device.transport() +
"\tstate=" +
inventory_state_name(device.state()),
)
}
lines
}
///|
/// A small firmware compatibility constraint.
pub(all) struct FirmwareConstraint {
vendor : String
model : String
minimum : String
recommended : String
}
///|
pub fn FirmwareConstraint::new(
vendor : String,
model : String,
minimum : String,
recommended : String,
) -> Result[FirmwareConstraint, ModbusError] {
if vendor == "" || model == "" || minimum == "" || recommended == "" {
Err(InvalidData)
} else {
Ok({ vendor, model, minimum, recommended })
}
}
///|
pub fn FirmwareConstraint::matches(
self : FirmwareConstraint,
device : InventoryDevice,
) -> Bool {
device.vendor() == self.vendor && device.model() == self.model
}
///|
pub fn FirmwareConstraint::minimum(self : FirmwareConstraint) -> String {
self.minimum
}
///|
pub fn FirmwareConstraint::recommended(self : FirmwareConstraint) -> String {
self.recommended
}