///|
/// Direction of a frame in a transport capture.
pub(all) enum CaptureDirection {
Received
Transmitted
Internal
} derive(Debug, Eq)
///|
/// Integrity state retained with a captured byte sequence.
pub(all) enum CaptureStatus {
Decoded
Undecoded
Corrupt
Truncated
} derive(Debug, Eq)
///|
/// A single timestamped wire record.
pub(all) struct CaptureRecord {
id : Int
timestamp : Int
mode : Mode
transaction_id : UInt16
direction : CaptureDirection
bytes : Array[Byte]
frame : Frame?
status : CaptureStatus
note : String
}
///|
pub fn CaptureRecord::new(
id : Int,
timestamp : Int,
mode : Mode,
transaction_id : UInt16,
direction : CaptureDirection,
bytes : Array[Byte],
note? : String = "",
) -> Result[CaptureRecord, ModbusError] {
if id < 0 ||
timestamp < 0 ||
bytes.length() == 0 ||
bytes.length() > limits_for(mode).max_adu {
return Err(InvalidLength)
}
let decoded = decode_transaction(mode, bytes)
match decoded {
Ok((observed_id, frame)) =>
Ok({
id,
timestamp,
mode,
transaction_id: if mode == Tcp {
observed_id
} else {
transaction_id
},
direction,
bytes: copy_bytes(bytes),
frame: Some(frame),
status: Decoded,
note,
})
Err(InvalidChecksum) | Err(InvalidAscii) | Err(InvalidMbap) =>
Ok({
id,
timestamp,
mode,
transaction_id,
direction,
bytes: copy_bytes(bytes),
frame: None,
status: Corrupt,
note,
})
Err(Incomplete) =>
Ok({
id,
timestamp,
mode,
transaction_id,
direction,
bytes: copy_bytes(bytes),
frame: None,
status: Truncated,
note,
})
Err(_) =>
Ok({
id,
timestamp,
mode,
transaction_id,
direction,
bytes: copy_bytes(bytes),
frame: None,
status: Undecoded,
note,
})
}
}
///|
pub fn CaptureRecord::from_frame(
id : Int,
timestamp : Int,
mode : Mode,
transaction_id : UInt16,
direction : CaptureDirection,
frame : Frame,
note? : String = "",
) -> Result[CaptureRecord, ModbusError] {
let bytes = encode_mode(mode, frame)
match
CaptureRecord::new(
id,
timestamp,
mode,
transaction_id,
direction,
bytes,
note~,
) {
Ok(record) => Ok(record)
Err(error) => Err(error)
}
}
///|
pub fn CaptureRecord::id(self : CaptureRecord) -> Int {
self.id
}
///|
pub fn CaptureRecord::timestamp(self : CaptureRecord) -> Int {
self.timestamp
}
///|
pub fn CaptureRecord::mode(self : CaptureRecord) -> Mode {
self.mode
}
///|
pub fn CaptureRecord::transaction_id(self : CaptureRecord) -> UInt16 {
self.transaction_id
}
///|
pub fn CaptureRecord::direction(self : CaptureRecord) -> CaptureDirection {
self.direction
}
///|
pub fn CaptureRecord::status(self : CaptureRecord) -> CaptureStatus {
self.status
}
///|
pub fn CaptureRecord::note(self : CaptureRecord) -> String {
self.note
}
///|
pub fn CaptureRecord::bytes(self : CaptureRecord) -> Array[Byte] {
copy_bytes(self.bytes)
}
///|
pub fn CaptureRecord::frame(self : CaptureRecord) -> Frame? {
self.frame
}
///|
pub fn CaptureRecord::unit_id(self : CaptureRecord) -> Byte? {
match self.frame {
Some(frame) => Some(frame.unit_id)
None => None
}
}
///|
pub fn CaptureRecord::function(self : CaptureRecord) -> Byte? {
match self.frame {
Some(frame) => Some(frame.pdu.function)
None => None
}
}
///|
pub fn CaptureRecord::is_valid(self : CaptureRecord) -> Bool {
self.status == Decoded
}
///|
pub fn CaptureRecord::summary(self : CaptureRecord) -> String {
"#" +
self.id.to_string() +
" t=" +
self.timestamp.to_string() +
" " +
mode_name(self.mode) +
" " +
capture_status_name(self.status) +
" bytes=" +
self.bytes.length().to_string()
}
///|
pub fn capture_status_name(status : CaptureStatus) -> String {
match status {
Decoded => "decoded"
Undecoded => "undecoded"
Corrupt => "corrupt"
Truncated => "truncated"
}
}
///|
pub fn capture_direction_name(direction : CaptureDirection) -> String {
match direction {
Received => "rx"
Transmitted => "tx"
Internal => "internal"
}
}
///|
/// Filter used to query a capture ring without decoding records again.
pub(all) struct CaptureFilter {
mode : Mode?
unit_id : Byte?
function : Byte?
direction : CaptureDirection?
status : CaptureStatus?
start_time : Int?
end_time : Int?
}
///|
pub fn CaptureFilter::new() -> CaptureFilter {
{
mode: None,
unit_id: None,
function: None,
direction: None,
status: None,
start_time: None,
end_time: None,
}
}
///|
pub fn CaptureFilter::with_mode(
self : CaptureFilter,
mode : Mode,
) -> CaptureFilter {
{ ..self, mode: Some(mode) }
}
///|
pub fn CaptureFilter::with_unit(
self : CaptureFilter,
unit_id : Byte,
) -> CaptureFilter {
{ ..self, unit_id: Some(unit_id) }
}
///|
pub fn CaptureFilter::with_function(
self : CaptureFilter,
function : Byte,
) -> CaptureFilter {
{ ..self, function: Some(function) }
}
///|
pub fn CaptureFilter::with_direction(
self : CaptureFilter,
direction : CaptureDirection,
) -> CaptureFilter {
{ ..self, direction: Some(direction) }
}
///|
pub fn CaptureFilter::with_status(
self : CaptureFilter,
status : CaptureStatus,
) -> CaptureFilter {
{ ..self, status: Some(status) }
}
///|
pub fn CaptureFilter::between(
self : CaptureFilter,
start : Int,
end : Int,
) -> Result[CaptureFilter, ModbusError] {
if start < 0 || end < start {
Err(InvalidData)
} else {
Ok({ ..self, start_time: Some(start), end_time: Some(end) })
}
}
///|
pub fn CaptureFilter::matches(
self : CaptureFilter,
record : CaptureRecord,
) -> Bool {
let mode_ok = match self.mode {
Some(mode) => record.mode() == mode
None => true
}
let unit_ok = match (self.unit_id, record.unit_id()) {
(Some(unit), Some(actual)) => unit == actual
(Some(_), None) => false
(None, _) => true
}
let function_ok = match (self.function, record.function()) {
(Some(function), Some(actual)) =>
function == actual || function == ordinary_function(actual)
(Some(_), None) => false
(None, _) => true
}
let direction_ok = match self.direction {
Some(direction) => record.direction() == direction
None => true
}
let status_ok = match self.status {
Some(status) => record.status() == status
None => true
}
let start_ok = match self.start_time {
Some(start) => record.timestamp() >= start
None => true
}
let end_ok = match self.end_time {
Some(end) => record.timestamp() <= end
None => true
}
mode_ok &&
unit_ok &&
function_ok &&
direction_ok &&
status_ok &&
start_ok &&
end_ok
}
///|
/// A bounded rolling store for protocol captures.
pub(all) struct CaptureRing {
records : Array[CaptureRecord]
max_records : Int
mut next_id : Int
mut lost : Int
}
///|
pub fn CaptureRing::new(
max_records? : Int = 4096,
) -> Result[CaptureRing, ModbusError] {
if max_records < 1 {
Err(CapacityExceeded)
} else {
Ok({ records: [], max_records, next_id: 1, lost: 0 })
}
}
///|
pub fn CaptureRing::length(self : CaptureRing) -> Int {
self.records.length()
}
///|
pub fn CaptureRing::capacity(self : CaptureRing) -> Int {
self.max_records
}
///|
pub fn CaptureRing::lost(self : CaptureRing) -> Int {
self.lost
}
///|
pub fn CaptureRing::push(
self : CaptureRing,
timestamp : Int,
mode : Mode,
transaction_id : UInt16,
direction : CaptureDirection,
bytes : Array[Byte],
note? : String = "",
) -> Result[CaptureRecord, ModbusError] {
let record = match
CaptureRecord::new(
self.next_id,
timestamp,
mode,
transaction_id,
direction,
bytes,
note~,
) {
Ok(value) => value
Err(error) => return Err(error)
}
self.next_id += 1
if self.records.length() >= self.max_records {
ignore(self.records.remove(0))
self.lost += 1
}
self.records.push(record)
Ok(record)
}
///|
pub fn CaptureRing::push_frame(
self : CaptureRing,
timestamp : Int,
mode : Mode,
transaction_id : UInt16,
direction : CaptureDirection,
frame : Frame,
note? : String = "",
) -> Result[CaptureRecord, ModbusError] {
let bytes = encode_mode(mode, frame)
self.push(timestamp, mode, transaction_id, direction, bytes, note~)
}
///|
pub fn CaptureRing::get(
self : CaptureRing,
index : Int,
) -> Result[CaptureRecord, ModbusError] {
if index < 0 || index >= self.records.length() {
Err(InvalidAddress)
} else {
Ok(self.records[index])
}
}
///|
pub fn CaptureRing::latest(self : CaptureRing) -> CaptureRecord? {
if self.records.length() == 0 {
None
} else {
Some(self.records[self.records.length() - 1])
}
}
///|
pub fn CaptureRing::snapshot(self : CaptureRing) -> Array[CaptureRecord] {
let result : Array[CaptureRecord] = []
for record in self.records {
result.push(record)
}
result
}
///|
pub fn CaptureRing::query(
self : CaptureRing,
filter : CaptureFilter,
) -> Array[CaptureRecord] {
let result : Array[CaptureRecord] = []
for record in self.records {
if filter.matches(record) {
result.push(record)
}
}
result
}
///|
pub fn CaptureRing::clear(self : CaptureRing) -> Unit {
self.records.clear()
self.lost = 0
}
///|
pub fn CaptureRing::decoded_count(self : CaptureRing) -> Int {
let mut count = 0
for record in self.records {
if record.status() == Decoded {
count += 1
}
}
count
}
///|
pub fn CaptureRing::corrupt_count(self : CaptureRing) -> Int {
let mut count = 0
for record in self.records {
if record.status() == Corrupt {
count += 1
}
}
count
}
///|
pub fn CaptureRing::byte_count(self : CaptureRing) -> Int {
let mut count = 0
for record in self.records {
count += record.bytes().length()
}
count
}
///|
pub fn CaptureRing::summary(self : CaptureRing) -> String {
"records=" +
self.length().to_string() +
" bytes=" +
self.byte_count().to_string() +
" decoded=" +
self.decoded_count().to_string() +
" lost=" +
self.lost.to_string()
}
///|
/// A deterministic replay item generated from a capture record.
pub(all) struct ReplayItem {
record_id : Int
mut due_at : Int
bytes : Array[Byte]
mut attempts : Int
mut sent : Bool
}
///|
pub fn ReplayItem::new(
record : CaptureRecord,
due_at : Int,
) -> Result[ReplayItem, ModbusError] {
if due_at < 0 {
Err(InvalidData)
} else {
Ok({
record_id: record.id(),
due_at,
bytes: record.bytes(),
attempts: 0,
sent: false,
})
}
}
///|
pub fn ReplayItem::record_id(self : ReplayItem) -> Int {
self.record_id
}
///|
pub fn ReplayItem::due_at(self : ReplayItem) -> Int {
self.due_at
}
///|
pub fn ReplayItem::bytes(self : ReplayItem) -> Array[Byte] {
copy_bytes(self.bytes)
}
///|
pub fn ReplayItem::attempts(self : ReplayItem) -> Int {
self.attempts
}
///|
pub fn ReplayItem::sent(self : ReplayItem) -> Bool {
self.sent
}
///|
pub fn ReplayItem::is_due(self : ReplayItem, now : Int) -> Bool {
!self.sent && now >= self.due_at
}
///|
pub fn ReplayItem::mark_sent(self : ReplayItem) -> Result[Unit, ModbusError] {
if self.sent {
Err(InvalidData)
} else {
self.attempts += 1
self.sent = true
Ok(())
}
}
///|
pub fn ReplayItem::reset(
self : ReplayItem,
due_at : Int,
) -> Result[Unit, ModbusError] {
if due_at < 0 {
return Err(InvalidData)
}
self.due_at = due_at
self.attempts = 0
self.sent = false
Ok(())
}
///|
/// A replay plan for deterministic integration tests and diagnostics.
pub(all) struct ReplayPlan {
items : Array[ReplayItem]
max_items : Int
mut started_at : Int?
}
///|
pub fn ReplayPlan::new(
max_items? : Int = 1024,
) -> Result[ReplayPlan, ModbusError] {
if max_items < 1 {
Err(CapacityExceeded)
} else {
Ok({ items: [], max_items, started_at: None })
}
}
///|
pub fn ReplayPlan::add(
self : ReplayPlan,
record : CaptureRecord,
delay : Int,
) -> Result[Unit, ModbusError] {
if delay < 0 || self.items.length() >= self.max_items {
return Err(CapacityExceeded)
}
let start = match self.started_at {
Some(value) => value
None => 0
}
match ReplayItem::new(record, start + delay) {
Ok(item) => {
self.items.push(item)
Ok(())
}
Err(error) => Err(error)
}
}
///|
pub fn ReplayPlan::length(self : ReplayPlan) -> Int {
self.items.length()
}
///|
pub fn ReplayPlan::start(
self : ReplayPlan,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if timestamp < 0 {
Err(InvalidData)
} else {
self.started_at = Some(timestamp)
Ok(())
}
}
///|
pub fn ReplayPlan::next_due(self : ReplayPlan, now : Int) -> ReplayItem? {
for item in self.items {
if item.is_due(now) {
return Some(item)
}
}
None
}
///|
pub fn ReplayPlan::mark_sent(
self : ReplayPlan,
record_id : Int,
) -> Result[Unit, ModbusError] {
for index, item in self.items {
if item.record_id() == record_id {
match item.mark_sent() {
Ok(_) => {
self.items[index] = item
return Ok(())
}
Err(error) => return Err(error)
}
}
}
Err(InvalidData)
}
///|
pub fn ReplayPlan::pending(self : ReplayPlan) -> Int {
let mut count = 0
for item in self.items {
if !item.sent() {
count += 1
}
}
count
}
///|
pub fn ReplayPlan::completed(self : ReplayPlan) -> Int {
self.length() - self.pending()
}
///|
pub fn ReplayPlan::snapshot(self : ReplayPlan) -> Array[ReplayItem] {
let result : Array[ReplayItem] = []
for item in self.items {
result.push(item)
}
result
}
///|
pub fn ReplayPlan::reset(
self : ReplayPlan,
timestamp : Int,
) -> Result[Unit, ModbusError] {
if timestamp < 0 {
return Err(InvalidData)
}
self.started_at = Some(timestamp)
for index, item in self.items {
match item.reset(timestamp) {
Ok(_) => self.items[index] = item
Err(error) => return Err(error)
}
}
Ok(())
}