///|
/// Aggregated endpoint health used by operations dashboards.
pub(all) struct EndpointHealth {
unit_id : Byte
ready : Bool
event_count : UInt16
exceptions : Int
requests : Int
responses : Int
errors : Int
}
///|
pub fn endpoint_health(
device : Device,
metrics : ProtocolMetrics,
) -> EndpointHealth {
let health = device.health()
{
unit_id: health.unit_id,
ready: !health.busy,
event_count: health.event_count,
exceptions: health.exception_count,
requests: metrics.requests(),
responses: metrics.responses(),
errors: metrics.errors(),
}
}
///|
/// Return a score in [0, 100] suitable for a small status indicator.
pub fn endpoint_score(health : EndpointHealth) -> Int {
if !health.ready {
return 0
}
let total = health.requests
if total == 0 {
return 100
}
let good = health.responses - health.errors - health.exceptions
let score = good * 100 / total
if score < 0 {
0
} else if score > 100 {
100
} else {
score
}
}
///|
/// Validate that endpoint counters are internally consistent.
pub fn validate_endpoint_health(
health : EndpointHealth,
) -> Result[Unit, ModbusError] {
if health.requests < 0 ||
health.responses < 0 ||
health.errors < 0 ||
health.exceptions < 0 {
Err(InvalidData)
} else if health.responses > health.requests {
Err(InvalidData)
} else {
Ok(())
}
}
///|
/// Summarize health in stable key/value form for a CLI or metrics exporter.
pub fn endpoint_health_summary(health : EndpointHealth) -> String {
"unit=" +
health.unit_id.to_string() +
" ready=" +
(if health.ready { "true" } else { "false" }) +
" score=" +
endpoint_score(health).to_string() +
" requests=" +
health.requests.to_string() +
" responses=" +
health.responses.to_string() +
" errors=" +
health.errors.to_string()
}
///|
/// Compute an overall score for a fleet of devices.
pub fn fleet_score(health : Array[EndpointHealth]) -> Int {
if health.length() == 0 {
return 100
}
let mut total = 0
for item in health {
total += endpoint_score(item)
}
total / health.length()
}
///|
/// Return the least healthy endpoint, if any.
pub fn least_healthy(health : Array[EndpointHealth]) -> EndpointHealth? {
if health.length() == 0 {
return None
}
let mut result = health[0]
for item in health {
if endpoint_score(item) < endpoint_score(result) {
result = item
}
}
Some(result)
}