///|
/// Category assigned to a diagnostic finding.
pub(all) enum DiagnosticCategory {
Framing
Addressing
FunctionSupport
Payload
Timing
Capacity
Configuration
Interoperability
} derive(Debug, Eq)
///|
/// Severity of a diagnostic finding.
pub(all) enum DiagnosticSeverity {
Note
WarningFinding
ErrorFinding
} derive(Debug, Eq)
///|
pub fn diagnostic_category_name(value : DiagnosticCategory) -> String {
match value {
Framing => "framing"
Addressing => "addressing"
FunctionSupport => "function-support"
Payload => "payload"
Timing => "timing"
Capacity => "capacity"
Configuration => "configuration"
Interoperability => "interoperability"
}
}
///|
pub fn diagnostic_severity_name(value : DiagnosticSeverity) -> String {
match value {
Note => "note"
WarningFinding => "warning"
ErrorFinding => "error"
}
}
///|
/// One actionable finding from a diagnostic pass.
pub(all) struct DiagnosticIssue {
code : String
category : DiagnosticCategory
severity : DiagnosticSeverity
message : String
context : String
suggestion : String
}
///|
pub fn DiagnosticIssue::new(
code : String,
category : DiagnosticCategory,
severity : DiagnosticSeverity,
message : String,
context? : String = "",
suggestion? : String = "",
) -> Result[DiagnosticIssue, ModbusError] {
if code == "" || message == "" {
Err(InvalidData)
} else {
Ok({ code, category, severity, message, context, suggestion })
}
}
///|
pub fn DiagnosticIssue::code(self : DiagnosticIssue) -> String {
self.code
}
///|
pub fn DiagnosticIssue::category(self : DiagnosticIssue) -> DiagnosticCategory {
self.category
}
///|
pub fn DiagnosticIssue::severity(self : DiagnosticIssue) -> DiagnosticSeverity {
self.severity
}
///|
pub fn DiagnosticIssue::message(self : DiagnosticIssue) -> String {
self.message
}
///|
pub fn DiagnosticIssue::context(self : DiagnosticIssue) -> String {
self.context
}
///|
pub fn DiagnosticIssue::suggestion(self : DiagnosticIssue) -> String {
self.suggestion
}
///|
pub fn DiagnosticIssue::summary(self : DiagnosticIssue) -> String {
self.code + " " + diagnostic_severity_name(self.severity) + " " + self.message
}
///|
/// A report collected from one or more protocol checks.
pub(all) struct DiagnosticReport {
name : String
issues : Array[DiagnosticIssue]
max_issues : Int
mut inspected : Int
}
///|
pub fn DiagnosticReport::new(
name? : String = "protocol-diagnostics",
max_issues? : Int = 1024,
) -> Result[DiagnosticReport, ModbusError] {
if name == "" || max_issues < 1 {
Err(InvalidData)
} else {
Ok({ name, issues: [], max_issues, inspected: 0 })
}
}
///|
pub fn DiagnosticReport::name(self : DiagnosticReport) -> String {
self.name
}
///|
pub fn DiagnosticReport::inspected(self : DiagnosticReport) -> Int {
self.inspected
}
///|
pub fn DiagnosticReport::add(
self : DiagnosticReport,
issue : DiagnosticIssue,
) -> Result[Unit, ModbusError] {
if self.issues.length() >= self.max_issues {
Err(CapacityExceeded)
} else {
self.issues.push(issue)
Ok(())
}
}
///|
pub fn DiagnosticReport::inspect_one(
self : DiagnosticReport,
frame : Frame,
) -> Result[Unit, ModbusError] {
self.inspected += 1
match validate_wire_frame(frame, true) {
Ok(_) => ()
Err(error) => {
let issue = DiagnosticIssue::new(
"FRAME-" + error_name(error),
Framing,
ErrorFinding,
"wire frame validation failed: " + error_name(error),
context=format_frame(frame),
suggestion="check unit id, function code and PDU length",
)
match issue {
Ok(value) => ignore(self.add(value))
Err(value) => return Err(value)
}
}
}
if frame.unit_id == 0 &&
frame.pdu.function != 5 &&
frame.pdu.function != 6 &&
frame.pdu.function != 15 &&
frame.pdu.function != 16 {
let issue = DiagnosticIssue::new(
"BROADCAST-READ",
Addressing,
WarningFinding,
"broadcast is only valid for write requests",
context=format_frame(frame),
suggestion="use a unit id from 1 through 247 for reads",
).unwrap()
ignore(self.add(issue))
}
match validate_pdu(frame.pdu) {
Ok(_) => ()
Err(error) => {
let issue = DiagnosticIssue::new(
"PDU-" + error_name(error),
Payload,
ErrorFinding,
"PDU shape is not valid for its function code",
context=function_name(frame.pdu.function),
suggestion="construct the request with a checked function builder",
).unwrap()
ignore(self.add(issue))
}
}
Ok(())
}
///|
pub fn DiagnosticReport::inspect_wire(
self : DiagnosticReport,
mode : Mode,
bytes : Array[Byte],
) -> Unit {
self.inspected += 1
match decode_transaction(mode, bytes) {
Ok(_) => ()
Err(error) => {
let category = match error {
InvalidChecksum | InvalidAscii | InvalidMbap => Framing
Incomplete => Capacity
_ => Payload
}
let issue = DiagnosticIssue::new(
"WIRE-" + error_name(error),
category,
ErrorFinding,
"wire bytes could not be decoded",
context=mode_name(mode),
suggestion="capture a complete ADU and verify the checksum",
).unwrap()
ignore(self.add(issue))
}
}
}
///|
pub fn DiagnosticReport::issues(
self : DiagnosticReport,
) -> Array[DiagnosticIssue] {
let result : Array[DiagnosticIssue] = []
for issue in self.issues {
result.push(issue)
}
result
}
///|
pub fn DiagnosticReport::length(self : DiagnosticReport) -> Int {
self.issues.length()
}
///|
pub fn DiagnosticReport::count_severity(
self : DiagnosticReport,
severity : DiagnosticSeverity,
) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity() == severity {
count += 1
}
}
count
}
///|
pub fn DiagnosticReport::count_category(
self : DiagnosticReport,
category : DiagnosticCategory,
) -> Int {
let mut count = 0
for issue in self.issues {
if issue.category() == category {
count += 1
}
}
count
}
///|
pub fn DiagnosticReport::has_errors(self : DiagnosticReport) -> Bool {
self.count_severity(ErrorFinding) > 0
}
///|
pub fn DiagnosticReport::has_warnings(self : DiagnosticReport) -> Bool {
self.count_severity(WarningFinding) > 0
}
///|
pub fn DiagnosticReport::ok(self : DiagnosticReport) -> Bool {
!self.has_errors()
}
///|
pub fn DiagnosticReport::clear(self : DiagnosticReport) -> Unit {
self.issues.clear()
self.inspected = 0
}
///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
self.name +
" inspected=" +
self.inspected.to_string() +
" issues=" +
self.length().to_string() +
" errors=" +
self.count_severity(ErrorFinding).to_string() +
" warnings=" +
self.count_severity(WarningFinding).to_string()
}
///|
/// A set of capabilities expected from a server or gateway.
pub(all) struct CapabilitySet {
functions : Array[Byte]
modes : Array[Mode]
max_pdu : Int
broadcasts : Bool
}
///|
pub fn CapabilitySet::new(
max_pdu? : Int = 253,
broadcasts? : Bool = true,
) -> Result[CapabilitySet, ModbusError] {
if max_pdu < 1 || max_pdu > 253 {
Err(InvalidLength)
} else {
Ok({ functions: [], modes: [], max_pdu, broadcasts })
}
}
///|
pub fn CapabilitySet::add_function(
self : CapabilitySet,
function : Byte,
) -> Result[Unit, ModbusError] {
if !supported_function(function) {
return Err(Unsupported)
}
for current in self.functions {
if current == function {
return Ok(())
}
}
self.functions.push(function)
Ok(())
}
///|
pub fn CapabilitySet::add_mode(self : CapabilitySet, mode : Mode) -> Unit {
for current in self.modes {
if current == mode {
return
}
}
self.modes.push(mode)
}
///|
pub fn CapabilitySet::supports_function(
self : CapabilitySet,
function : Byte,
) -> Bool {
for current in self.functions {
if current == function {
return true
}
}
false
}
///|
pub fn CapabilitySet::supports_mode(self : CapabilitySet, mode : Mode) -> Bool {
for current in self.modes {
if current == mode {
return true
}
}
false
}
///|
pub fn CapabilitySet::functions(self : CapabilitySet) -> Array[Byte] {
let result : Array[Byte] = []
for value in self.functions {
result.push(value)
}
result
}
///|
pub fn CapabilitySet::modes(self : CapabilitySet) -> Array[Mode] {
let result : Array[Mode] = []
for value in self.modes {
result.push(value)
}
result
}
///|
pub fn CapabilitySet::max_pdu(self : CapabilitySet) -> Int {
self.max_pdu
}
///|
pub fn CapabilitySet::broadcasts(self : CapabilitySet) -> Bool {
self.broadcasts
}
///|
pub fn CapabilitySet::validate_frame(
self : CapabilitySet,
frame : Frame,
) -> Result[Unit, ModbusError] {
if frame.pdu.data.length() > self.max_pdu {
return Err(InvalidLength)
}
if !self.supports_function(ordinary_function(frame.pdu.function)) {
return Err(Unsupported)
}
if frame.unit_id == 0 && !self.broadcasts {
return Err(InvalidUnitId)
}
Ok(())
}
///|
pub fn CapabilitySet::coverage(
self : CapabilitySet,
expected : Array[Byte],
) -> Int {
if expected.length() == 0 {
return 100
}
let mut found = 0
for function in expected {
if self.supports_function(function) {
found += 1
}
}
found * 100 / expected.length()
}
///|
pub fn default_server_capabilities() -> CapabilitySet {
let result = CapabilitySet::new().unwrap()
let functions : Array[Byte] = [
1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 15, 16, 17, 20, 21, 22, 23, 24, 43,
]
for function in functions {
ignore(result.add_function(function))
}
result.add_mode(Rtu)
result.add_mode(Ascii)
result.add_mode(Tcp)
result
}
///|
pub fn diagnose_frames(frames : Array[Frame]) -> DiagnosticReport {
let report = DiagnosticReport::new().unwrap()
for frame in frames {
ignore(report.inspect_one(frame))
}
report
}
///|
pub fn diagnose_capability_gap(
capabilities : CapabilitySet,
expected_functions : Array[Byte],
) -> DiagnosticReport {
let report = DiagnosticReport::new(name="capability-gap").unwrap()
for function in expected_functions {
if !capabilities.supports_function(function) {
let issue = DiagnosticIssue::new(
"FUNC-" + function.to_string(),
FunctionSupport,
ErrorFinding,
"required function is not supported",
context=function_name(function),
suggestion="add a handler or remove the function from the deployment profile",
).unwrap()
ignore(report.add(issue))
}
}
report
}