///|
/// The role of a node in a Modbus deployment topology.
pub(all) enum TopologyNodeKind {
SiteNode
DeviceNode
GatewayNode
ServiceNode
} derive(Debug, Eq)
///|
/// Operational health used by route selection and dashboards.
pub(all) enum TopologyHealth {
Healthy
Degraded
Unreachable
Maintenance
UnknownHealth
} derive(Debug, Eq)
///|
/// A graph node representing a site, device, gateway, or consumer.
pub(all) struct TopologyNode {
id : String
label : String
kind : TopologyNodeKind
unit_id : Byte?
mut health : TopologyHealth
mut load : Int
zone : String
tags : Array[String]
}
///|
pub fn TopologyNode::new(
id : String,
label : String,
kind : TopologyNodeKind,
unit_id? : Byte = 0,
zone? : String = "",
) -> Result[TopologyNode, ModbusError] {
if id == "" || label == "" {
return Err(InvalidData)
}
if unit_id != 0 && !is_valid_unit_id(unit_id) {
return Err(InvalidUnitId)
}
let selected_unit = if unit_id == 0 { None } else { Some(unit_id) }
Ok({
id,
label,
kind,
unit_id: selected_unit,
health: UnknownHealth,
load: 0,
zone,
tags: [],
})
}
///|
pub fn TopologyNode::id(self : TopologyNode) -> String {
self.id
}
///|
pub fn TopologyNode::label(self : TopologyNode) -> String {
self.label
}
///|
pub fn TopologyNode::kind(self : TopologyNode) -> TopologyNodeKind {
self.kind
}
///|
pub fn TopologyNode::unit_id(self : TopologyNode) -> Byte? {
self.unit_id
}
///|
pub fn TopologyNode::health(self : TopologyNode) -> TopologyHealth {
self.health
}
///|
pub fn TopologyNode::load(self : TopologyNode) -> Int {
self.load
}
///|
pub fn TopologyNode::zone(self : TopologyNode) -> String {
self.zone
}
///|
pub fn TopologyNode::set_health(
self : TopologyNode,
health : TopologyHealth,
) -> Unit {
self.health = health
}
///|
pub fn TopologyNode::set_load(
self : TopologyNode,
load : Int,
) -> Result[Unit, ModbusError] {
if load < 0 || load > 100 {
Err(InvalidData)
} else {
self.load = load
Ok(())
}
}
///|
pub fn TopologyNode::add_tag(
self : TopologyNode,
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 TopologyNode::has_tag(self : TopologyNode, tag : String) -> Bool {
for existing in self.tags {
if existing == tag {
return true
}
}
false
}
///|
pub fn TopologyNode::tag_count(self : TopologyNode) -> Int {
self.tags.length()
}
///|
pub fn TopologyNode::is_available(self : TopologyNode) -> Bool {
self.health == Healthy || self.health == Degraded
}
///|
pub fn topology_node_kind_name(kind : TopologyNodeKind) -> String {
match kind {
SiteNode => "site"
DeviceNode => "device"
GatewayNode => "gateway"
ServiceNode => "service"
}
}
///|
pub fn topology_health_name(health : TopologyHealth) -> String {
match health {
Healthy => "healthy"
Degraded => "degraded"
Unreachable => "unreachable"
Maintenance => "maintenance"
UnknownHealth => "unknown"
}
}
///|
/// A directed or bidirectional connection between topology nodes.
pub(all) struct TopologyLink {
id : String
from : String
to : String
latency : Int
capacity : Int
mut utilization : Int
bidirectional : Bool
transport : String
}
///|
pub fn TopologyLink::new(
id : String,
from : String,
to : String,
latency : Int,
capacity : Int,
bidirectional? : Bool = true,
transport? : String = "modbus",
) -> Result[TopologyLink, ModbusError] {
if id == "" || from == "" || to == "" || from == to {
return Err(InvalidData)
}
if latency < 0 || capacity < 1 {
return Err(InvalidData)
}
if transport == "" {
return Err(InvalidData)
}
Ok({
id,
from,
to,
latency,
capacity,
utilization: 0,
bidirectional,
transport,
})
}
///|
pub fn TopologyLink::id(self : TopologyLink) -> String {
self.id
}
///|
pub fn TopologyLink::from(self : TopologyLink) -> String {
self.from
}
///|
pub fn TopologyLink::to(self : TopologyLink) -> String {
self.to
}
///|
pub fn TopologyLink::latency(self : TopologyLink) -> Int {
self.latency
}
///|
pub fn TopologyLink::capacity(self : TopologyLink) -> Int {
self.capacity
}
///|
pub fn TopologyLink::utilization(self : TopologyLink) -> Int {
self.utilization
}
///|
pub fn TopologyLink::transport(self : TopologyLink) -> String {
self.transport
}
///|
pub fn TopologyLink::is_bidirectional(self : TopologyLink) -> Bool {
self.bidirectional
}
///|
pub fn TopologyLink::set_utilization(
self : TopologyLink,
value : Int,
) -> Result[Unit, ModbusError] {
if value < 0 || value > 100 {
Err(InvalidData)
} else {
self.utilization = value
Ok(())
}
}
///|
pub fn TopologyLink::available(self : TopologyLink) -> Bool {
self.utilization < 100
}
///|
pub fn TopologyLink::cost(self : TopologyLink) -> Int {
self.latency + self.utilization + 100 / self.capacity
}
///|
pub fn TopologyLink::connects(self : TopologyLink, node_id : String) -> Bool {
self.from == node_id || (self.bidirectional && self.to == node_id)
}
///|
pub fn TopologyLink::other(self : TopologyLink, node_id : String) -> String? {
if self.from == node_id {
Some(self.to)
} else if self.bidirectional && self.to == node_id {
Some(self.from)
} else {
None
}
}
///|
/// A selected path with aggregate latency and link identifiers.
pub(all) struct TopologyRoute {
source : String
destination : String
nodes : Array[String]
links : Array[String]
cost : Int
reachable : Bool
}
///|
pub fn TopologyRoute::not_reachable(
source : String,
destination : String,
) -> TopologyRoute {
{ source, destination, nodes: [], links: [], cost: 0, reachable: false }
}
///|
pub fn TopologyRoute::new(
source : String,
destination : String,
nodes : Array[String],
links : Array[String],
cost : Int,
) -> TopologyRoute {
{ source, destination, nodes, links, cost, reachable: true }
}
///|
pub fn TopologyRoute::source(self : TopologyRoute) -> String {
self.source
}
///|
pub fn TopologyRoute::destination(self : TopologyRoute) -> String {
self.destination
}
///|
pub fn TopologyRoute::nodes(self : TopologyRoute) -> Array[String] {
self.nodes
}
///|
pub fn TopologyRoute::links(self : TopologyRoute) -> Array[String] {
self.links
}
///|
pub fn TopologyRoute::cost(self : TopologyRoute) -> Int {
self.cost
}
///|
pub fn TopologyRoute::reachable(self : TopologyRoute) -> Bool {
self.reachable
}
///|
pub fn TopologyRoute::hops(self : TopologyRoute) -> Int {
if self.nodes.length() == 0 {
0
} else {
self.nodes.length() - 1
}
}
///|
pub fn TopologyRoute::summary(self : TopologyRoute) -> String {
self.source +
"->" +
self.destination +
" hops=" +
self.hops().to_string() +
" cost=" +
self.cost.to_string() +
" reachable=" +
self.reachable.to_string()
}
///|
/// A mutable topology graph used by gateway routing and deployment tools.
pub(all) struct NetworkTopology {
nodes : Array[TopologyNode]
links : Array[TopologyLink]
max_nodes : Int
max_links : Int
}
///|
pub fn NetworkTopology::new(
max_nodes? : Int = 1024,
max_links? : Int = 4096,
) -> Result[NetworkTopology, ModbusError] {
if max_nodes < 1 || max_links < 1 {
Err(InvalidData)
} else {
Ok({ nodes: [], links: [], max_nodes, max_links })
}
}
///|
pub fn NetworkTopology::node_count(self : NetworkTopology) -> Int {
self.nodes.length()
}
///|
pub fn NetworkTopology::link_count(self : NetworkTopology) -> Int {
self.links.length()
}
///|
pub fn NetworkTopology::max_nodes(self : NetworkTopology) -> Int {
self.max_nodes
}
///|
pub fn NetworkTopology::max_links(self : NetworkTopology) -> Int {
self.max_links
}
///|
pub fn NetworkTopology::has_node(self : NetworkTopology, id : String) -> Bool {
for node in self.nodes {
if node.id() == id {
return true
}
}
false
}
///|
pub fn NetworkTopology::node(
self : NetworkTopology,
id : String,
) -> TopologyNode? {
for node in self.nodes {
if node.id() == id {
return Some(node)
}
}
None
}
///|
pub fn NetworkTopology::add_node(
self : NetworkTopology,
node : TopologyNode,
) -> Result[Unit, ModbusError] {
if self.nodes.length() >= self.max_nodes {
return Err(CapacityExceeded)
}
if self.has_node(node.id()) {
return Err(InvalidData)
}
self.nodes.push(node)
Ok(())
}
///|
pub fn NetworkTopology::remove_node(
self : NetworkTopology,
id : String,
) -> Result[TopologyNode, ModbusError] {
let mut index = -1
for i in 0.. Result[Unit, ModbusError] {
if self.links.length() >= self.max_links {
return Err(CapacityExceeded)
}
if !self.has_node(link.from()) || !self.has_node(link.to()) {
return Err(UnitMismatch)
}
for existing in self.links {
if existing.id() == link.id() {
return Err(InvalidData)
}
}
self.links.push(link)
Ok(())
}
///|
pub fn NetworkTopology::link(
self : NetworkTopology,
id : String,
) -> TopologyLink? {
for link in self.links {
if link.id() == id {
return Some(link)
}
}
None
}
///|
pub fn NetworkTopology::set_node_health(
self : NetworkTopology,
id : String,
health : TopologyHealth,
) -> Result[Unit, ModbusError] {
for node in self.nodes {
if node.id() == id {
node.set_health(health)
return Ok(())
}
}
Err(InvalidData)
}
///|
pub fn NetworkTopology::set_link_utilization(
self : NetworkTopology,
id : String,
utilization : Int,
) -> Result[Unit, ModbusError] {
for link in self.links {
if link.id() == id {
return link.set_utilization(utilization)
}
}
Err(InvalidData)
}
///|
fn NetworkTopology::topology_node_available(
self : NetworkTopology,
id : String,
source : String,
destination : String,
) -> Bool {
if id == source || id == destination {
return true
}
match self.node(id) {
Some(node) => node.is_available()
None => false
}
}
///|
/// Find a low-cost path with a bounded Bellman-Ford relaxation.
pub fn NetworkTopology::route(
self : NetworkTopology,
source : String,
destination : String,
) -> Result[TopologyRoute, ModbusError] {
if !self.has_node(source) || !self.has_node(destination) {
return Err(InvalidData)
}
if source == destination {
return Ok(TopologyRoute::new(source, destination, [source], [], 0))
}
let count = self.nodes.length()
let distances : Array[Int] = []
let previous : Array[String?] = []
let previous_link : Array[String?] = []
for _ in 0..= 1_000_000_000 {
continue
}
let from_id = self.nodes[from_index].id()
for link in self.links {
let target = if link.from() == from_id && link.available() {
Some(link.to())
} else if link.is_bidirectional() &&
link.to() == from_id &&
link.available() {
Some(link.from())
} else {
None
}
match target {
None => ()
Some(target_id) => {
if !self.topology_node_available(target_id, source, destination) {
continue
}
let target_index = self.node_index(target_id)
let candidate = distances[from_index] + link.cost()
if candidate < distances[target_index] {
distances[target_index] = candidate
previous[target_index] = Some(from_id)
previous_link[target_index] = Some(link.id())
changed = true
}
}
}
}
}
if !changed {
break
}
}
let destination_index = self.node_index(destination)
if distances[destination_index] >= 1_000_000_000 {
return Ok(TopologyRoute::not_reachable(source, destination))
}
let mut path_nodes : Array[String] = []
let mut path_links : Array[String] = []
let mut current = destination
path_nodes.push(current)
while current != source {
let index = self.node_index(current)
match previous_link[index] {
Some(link_id) => path_links.push(link_id)
None => return Ok(TopologyRoute::not_reachable(source, destination))
}
match previous[index] {
Some(node_id) => {
current = node_id
path_nodes.push(current)
}
None => return Ok(TopologyRoute::not_reachable(source, destination))
}
}
path_nodes = topology_reverse_strings(path_nodes)
path_links = topology_reverse_strings(path_links)
Ok(
TopologyRoute::new(
source,
destination,
path_nodes,
path_links,
distances[destination_index],
),
)
}
///|
fn topology_reverse_strings(values : Array[String]) -> Array[String] {
let result : Array[String] = []
for i in 0.. Int {
for i in 0.. Array[String] {
let result : Array[String] = []
for node in self.nodes {
if node.health() != Healthy {
result.push(node.id())
}
}
result
}
///|
/// Return links at or above a utilization threshold.
pub fn NetworkTopology::busy_links(
self : NetworkTopology,
threshold : Int,
) -> Result[Array[String], ModbusError] {
if threshold < 0 || threshold > 100 {
return Err(InvalidData)
}
let result : Array[String] = []
for link in self.links {
if link.utilization() >= threshold {
result.push(link.id())
}
}
Ok(result)
}
///|
/// Validate graph references, duplicate identifiers, and unit assignments.
pub fn NetworkTopology::validate(self : NetworkTopology) -> Array[ModbusError] {
let errors : Array[ModbusError] = []
for i in 0..
if !is_valid_unit_id(unit_id) {
errors.push(InvalidUnitId)
}
None => ()
}
}
for link in self.links {
if !self.has_node(link.from()) || !self.has_node(link.to()) {
errors.push(UnitMismatch)
}
if link.utilization() > 100 {
errors.push(InvalidData)
}
}
errors
}
///|
/// A compact health report suitable for a gateway status endpoint.
pub(all) struct TopologyHealthReport {
node_count : Int
link_count : Int
healthy : Int
degraded : Int
unavailable : Int
average_load : Int
invalid_edges : Int
}
///|
pub fn TopologyHealthReport::new(
topology : NetworkTopology,
) -> TopologyHealthReport {
let mut healthy = 0
let mut degraded = 0
let mut unavailable = 0
let mut load = 0
for node in topology.nodes {
load += node.load()
match node.health() {
Healthy => healthy += 1
Degraded => degraded += 1
Unreachable | Maintenance | UnknownHealth => unavailable += 1
}
}
let invalid_edges = topology.validate().length()
{
node_count: topology.node_count(),
link_count: topology.link_count(),
healthy,
degraded,
unavailable,
average_load: if topology.node_count() == 0 {
0
} else {
load / topology.node_count()
},
invalid_edges,
}
}
///|
pub fn TopologyHealthReport::node_count(self : TopologyHealthReport) -> Int {
self.node_count
}
///|
pub fn TopologyHealthReport::link_count(self : TopologyHealthReport) -> Int {
self.link_count
}
///|
pub fn TopologyHealthReport::healthy(self : TopologyHealthReport) -> Int {
self.healthy
}
///|
pub fn TopologyHealthReport::degraded(self : TopologyHealthReport) -> Int {
self.degraded
}
///|
pub fn TopologyHealthReport::unavailable(self : TopologyHealthReport) -> Int {
self.unavailable
}
///|
pub fn TopologyHealthReport::average_load(self : TopologyHealthReport) -> Int {
self.average_load
}
///|
pub fn TopologyHealthReport::invalid_edges(self : TopologyHealthReport) -> Int {
self.invalid_edges
}
///|
pub fn TopologyHealthReport::summary(self : TopologyHealthReport) -> String {
"nodes=" +
self.node_count.to_string() +
" links=" +
self.link_count.to_string() +
" healthy=" +
self.healthy.to_string() +
" degraded=" +
self.degraded.to_string() +
" unavailable=" +
self.unavailable.to_string() +
" load=" +
self.average_load.to_string()
}