///|
/// Local Modbus TCP/RTU framing simulation for W2B.
///|
pub(all) enum ModbusPduKind {
ReadHoldingRegisters
ReadInputRegisters
WriteSingleHoldingRegister
WriteMultipleHoldingRegisters
} derive(Eq, Debug)
///|
pub fn ModbusPduKind::function_code(self : ModbusPduKind) -> Int {
match self {
ReadHoldingRegisters => 3
ReadInputRegisters => 4
WriteSingleHoldingRegister => 6
WriteMultipleHoldingRegisters => 16
}
}
///|
pub(all) struct ModbusApplicationPdu {
kind : ModbusPduKind
address : UInt
quantity : Int
value_digest : Int
} derive(Eq, Debug)
///|
pub fn ModbusApplicationPdu::request_length(self : ModbusApplicationPdu) -> Int {
match self.kind {
ReadHoldingRegisters | ReadInputRegisters | WriteSingleHoldingRegister => 5
WriteMultipleHoldingRegisters => 6 + self.quantity * 2
}
}
///|
pub fn ModbusApplicationPdu::digest(self : ModbusApplicationPdu) -> Int {
(
(self.kind.function_code() * 131 + self.address.reinterpret_as_int()) * 131 +
self.quantity
) *
131 +
self.value_digest
}
///|
pub(all) struct ModbusMbapEnvelope {
transaction_id : Int
protocol_id : Int
length : Int
unit_id : Int
} derive(Eq, Debug)
///|
pub fn ModbusMbapEnvelope::for_pdu(
transaction_id~ : Int,
unit_id~ : Int,
pdu : ModbusApplicationPdu,
) -> ModbusMbapEnvelope {
{ transaction_id, protocol_id: 0, length: 1 + pdu.request_length(), unit_id }
}
///|
pub fn ModbusMbapEnvelope::length_matches(
self : ModbusMbapEnvelope,
pdu : ModbusApplicationPdu,
) -> Bool {
self.protocol_id == 0 && self.length == 1 + pdu.request_length()
}
///|
pub(all) struct ModbusTcpFrame {
mbap : ModbusMbapEnvelope
pdu : ModbusApplicationPdu
payload_digest : Int
} derive(Eq, Debug)
///|
pub fn ModbusTcpFrame::make(
transaction_id~ : Int,
unit_id~ : Int,
pdu~ : ModbusApplicationPdu,
) -> ModbusTcpFrame {
let mbap = ModbusMbapEnvelope::for_pdu(transaction_id~, unit_id~, pdu)
{ mbap, pdu, payload_digest: mbap.transaction_id * 257 + pdu.digest() }
}
///|
pub fn ModbusTcpFrame::mbap_valid(self : ModbusTcpFrame) -> Bool {
self.mbap.length_matches(self.pdu)
}
///|
pub(all) struct ModbusRtuTiming {
baud_rate : Int
char_time_ns : Int64
t35_ns : Int64
} derive(Eq, Debug)
///|
pub fn ModbusRtuTiming::for_baud(baud_rate? : Int = 19_200) -> ModbusRtuTiming {
let baud_rate = if baud_rate <= 0 { 19_200 } else { baud_rate }
let char_time_ns = 11_000_000_000L / Int64::from_int(baud_rate)
{ baud_rate, char_time_ns, t35_ns: char_time_ns * 35L / 10L }
}
///|
pub(all) struct ModbusRtuFrameBoundary {
timing : ModbusRtuTiming
silent_before_ns : Int64
frame_time_ns : Int64
silent_after_ns : Int64
} derive(Eq, Debug)
///|
pub fn ModbusRtuFrameBoundary::passes(self : ModbusRtuFrameBoundary) -> Bool {
self.silent_before_ns >= self.timing.t35_ns &&
self.silent_after_ns >= self.timing.t35_ns
}
///|
pub(all) struct ModbusRtuFrame {
unit_id : Int
pdu : ModbusApplicationPdu
crc_placeholder : Int
boundary : ModbusRtuFrameBoundary
} derive(Eq, Debug)
///|
pub fn ModbusRtuFrame::make(
unit_id~ : Int,
pdu~ : ModbusApplicationPdu,
timing~ : ModbusRtuTiming,
silent_before_ns~ : Int64,
silent_after_ns~ : Int64,
) -> ModbusRtuFrame {
{
unit_id,
pdu,
crc_placeholder: (unit_id * 257 + pdu.digest()) & 0xffff,
boundary: {
timing,
silent_before_ns,
frame_time_ns: timing.char_time_ns *
Int64::from_int(1 + pdu.request_length() + 2),
silent_after_ns,
},
}
}
///|
pub fn ModbusRtuFrame::delimited(self : ModbusRtuFrame) -> Bool {
self.boundary.passes()
}
///|
pub(all) struct ModbusFramingSimulationReport {
tcp_mbap_valid : Bool
rtu_t35_delimited : Bool
p2p_event_count : Int
fabric_event_count : Int
p2p_replay_count : Int
fabric_replay_count : Int
p2p_digest : @core.SimDigest
fabric_digest : @core.SimDigest
live_device_evidence : Bool
} derive(Eq, Debug)
///|
pub fn ModbusFramingSimulationReport::passes(
self : ModbusFramingSimulationReport,
) -> Bool {
self.tcp_mbap_valid &&
self.rtu_t35_delimited &&
self.p2p_event_count == 6 &&
self.fabric_event_count > self.p2p_event_count &&
self.p2p_replay_count == self.p2p_event_count &&
self.fabric_replay_count == self.fabric_event_count &&
!self.live_device_evidence
}
///|
pub fn modbus_read_holding_pdu(
address? : UInt = 0x0000U,
quantity? : Int = 2,
) -> ModbusApplicationPdu {
{ kind: ReadHoldingRegisters, address, quantity, value_digest: 0 }
}
///|
pub fn modbus_read_input_pdu(
address? : UInt = 0x0000U,
quantity? : Int = 2,
) -> ModbusApplicationPdu {
{ kind: ReadInputRegisters, address, quantity, value_digest: 0 }
}
///|
pub fn modbus_tcp_read_holding_fixture(
transaction_id? : Int = 1,
unit_id? : Int = 1,
) -> ModbusTcpFrame {
ModbusTcpFrame::make(transaction_id~, unit_id~, pdu=modbus_read_holding_pdu())
}
///|
pub fn modbus_rtu_read_input_fixture(
unit_id? : Int = 1,
baud_rate? : Int = 19_200,
) -> ModbusRtuFrame {
let timing = ModbusRtuTiming::for_baud(baud_rate~)
ModbusRtuFrame::make(
unit_id~,
pdu=modbus_read_input_pdu(),
timing~,
silent_before_ns=timing.t35_ns,
silent_after_ns=timing.t35_ns,
)
}
///|
pub fn modbus_rtu_boundary_fixture(
baud_rate? : Int = 19_200,
) -> ModbusRtuFrameBoundary {
let timing = ModbusRtuTiming::for_baud(baud_rate~)
{
timing,
silent_before_ns: timing.t35_ns,
frame_time_ns: timing.char_time_ns * 8L,
silent_after_ns: timing.t35_ns,
}
}
///|
pub fn modbus_p2p_framing_trace(seed? : Int = 52) -> @trace.TraceLog {
let tcp = modbus_tcp_read_holding_fixture()
let rtu = modbus_rtu_read_input_fixture()
let log = @trace.TraceLog::new()
append_modbus_frame_event(
log,
1,
"modbus.tcp.mbap.tx",
@trace.Tx,
0L,
tcp.payload_digest,
"modbus.tcp.p2p",
seed~,
)
append_modbus_frame_event(
log,
2,
"modbus.tcp.mbap.rx",
@trace.Rx,
5L,
tcp.payload_digest,
"modbus.tcp.p2p",
seed~,
)
append_modbus_frame_event(
log,
3,
"modbus.rtu.t35.silent-before",
@trace.Meta,
rtu.boundary.timing.t35_ns,
rtu.boundary.timing.t35_ns.to_int(),
"modbus.rtu.p2p",
seed~,
)
append_modbus_frame_event(
log,
4,
"modbus.rtu.frame.tx",
@trace.Tx,
rtu.boundary.timing.t35_ns + 10L,
rtu.crc_placeholder,
"modbus.rtu.p2p",
seed~,
)
append_modbus_frame_event(
log,
5,
"modbus.rtu.t35.silent-after",
@trace.Meta,
rtu.boundary.timing.t35_ns + rtu.boundary.frame_time_ns,
rtu.boundary.timing.t35_ns.to_int(),
"modbus.rtu.p2p",
seed~,
)
append_modbus_frame_event(
log,
6,
"modbus.rtu.frame.rx",
@trace.Rx,
rtu.boundary.timing.t35_ns +
rtu.boundary.frame_time_ns +
rtu.boundary.timing.t35_ns,
rtu.crc_placeholder,
"modbus.rtu.p2p",
seed~,
)
log
}
///|
pub fn modbus_fabric_framing_trace(seed? : Int = 52) -> @trace.TraceLog {
let fabric = @sim.run_fabric_partition_heal(seed~)
let tcp = modbus_tcp_read_holding_fixture(transaction_id=2, unit_id=1)
let log = @trace.TraceLog::new()
append_modbus_frame_event(
log,
1,
"modbus.tcp.fabric.mbap.tx",
@trace.Tx,
0L,
tcp.payload_digest,
"modbus.tcp.fabric",
seed~,
)
let mut event_id = 2
for event in fabric.trace.events {
append_fabric_envelope_event(log, event, event_id~, seed~)
event_id += 1
}
append_modbus_frame_event(
log,
event_id,
"modbus.tcp.fabric.mbap.rx",
@trace.Rx,
Int64::from_int(event_id) * 10L,
tcp.payload_digest,
"modbus.tcp.fabric",
seed~,
)
log
}
///|
pub fn modbus_framing_simulation_report(
seed? : Int = 52,
) -> ModbusFramingSimulationReport {
let p2p = modbus_p2p_framing_trace(seed~)
let fabric = modbus_fabric_framing_trace(seed~)
{
tcp_mbap_valid: modbus_tcp_read_holding_fixture().mbap_valid(),
rtu_t35_delimited: modbus_rtu_read_input_fixture().delimited(),
p2p_event_count: p2p.len(),
fabric_event_count: fabric.len(),
p2p_replay_count: replay_count(p2p),
fabric_replay_count: replay_count(fabric),
p2p_digest: p2p.portable_digest(seed~),
fabric_digest: fabric.portable_digest(seed~),
live_device_evidence: false,
}
}
///|
fn append_modbus_frame_event(
log : @trace.TraceLog,
event_id : Int,
label : String,
direction : @trace.TraceDirection,
time_ns : Int64,
payload_digest : Int,
medium_id : String,
seed~ : Int,
) -> Unit {
log.append(
@trace.TraceEvent::make(
event_id~,
parent_id=None,
vtime=@core.VTime::from_ns(time_ns),
clock_domain="sim",
raw_ns=time_ns,
node_id="controller",
medium_id~,
channel_id=None,
direction~,
payload_digest=Some(payload_digest),
rng_step=event_id,
seed~,
backend=@core.SimNative,
label~,
),
)
}
///|
fn append_fabric_envelope_event(
log : @trace.TraceLog,
event : @trace.TraceEvent,
event_id~ : Int,
seed~ : Int,
) -> Unit {
let direction = match event.direction {
@trace.Fault => @trace.Probe
other => other
}
let time_ns = Int64::from_int(event_id) * 10L
log.append(
@trace.TraceEvent::make(
event_id~,
parent_id=event.parent_id,
vtime=@core.VTime::from_ns(time_ns),
clock_domain="sim",
raw_ns=time_ns,
node_id="fabric",
medium_id="modbus.tcp.fabric",
channel_id=event.channel_id,
direction~,
payload_digest=event.payload_digest,
rng_step=event_id,
seed~,
backend=@core.SimNative,
label="modbus.tcp.fabric." + event.label,
),
)
}
///|
fn replay_count(log : @trace.TraceLog) -> Int {
let replay = @trace.Replay::from_log(log)
let mut count = 0
while !replay.is_exhausted() {
ignore(replay.next_event())
count += 1
}
count
}