///|
/// Local Modbus register-backed object profile for W2A.
///|
pub fn package_id() -> String {
"fieldbus_core/modbus"
}
///|
pub fn modbus_medium_id() -> String {
"modbus"
}
///|
pub(all) enum ModbusRegisterArea {
HoldingRegister
InputRegister
} derive(Eq, Debug)
///|
pub fn ModbusRegisterArea::channel(self : ModbusRegisterArea) -> String {
match self {
HoldingRegister => "holding_register"
InputRegister => "input_register"
}
}
///|
pub fn ModbusRegisterArea::read_function_code(self : ModbusRegisterArea) -> Int {
match self {
HoldingRegister => 3
InputRegister => 4
}
}
///|
pub fn ModbusRegisterArea::write_function_code(
self : ModbusRegisterArea,
) -> Int? {
match self {
HoldingRegister => Some(6)
InputRegister => None
}
}
///|
pub(all) struct ModbusRegisterProfile {
object_id : String
label : String
area : ModbusRegisterArea
address : UInt
word_count : Int
direction : @trace.TraceDirection
object_kind : @isocontract.ObjectKind
behavior : String
} derive(Eq, Debug)
///|
pub(all) struct ModbusRegisterProfileReport {
object_count : Int
binding_count : Int
trace_event_count : Int
checked_events : Int
matched_steps : Int
eventless_lifecycle : Bool
live_device_evidence : Bool
conformance : @protocol_ast.ConformanceReport
} derive(Debug)
///|
pub fn ModbusRegisterProfileReport::passes(
self : ModbusRegisterProfileReport,
) -> Bool {
self.conformance.passes() &&
self.object_count == 6 &&
self.binding_count == 6 &&
self.trace_event_count == 6 &&
self.checked_events == 5 &&
self.matched_steps == 5 &&
self.eventless_lifecycle &&
!self.live_device_evidence
}
///|
pub fn modbus_register_profiles(
unit_id? : Int = 1,
) -> Array[ModbusRegisterProfile] {
[
register_profile(
object_id="modbus.lifecycle",
label="modbus.holding.lifecycle.write",
area=HoldingRegister,
address=0x0000U,
word_count=1,
direction=@trace.Tx,
object_kind=@isocontract.Lifecycle,
behavior="event-less lifecycle command encoded as a holding register write",
unit_id~,
),
register_profile(
object_id="modbus.command",
label="modbus.holding.command.write",
area=HoldingRegister,
address=0x0001U,
word_count=1,
direction=@trace.Tx,
object_kind=@isocontract.Command,
behavior="single-register command word written by the controller",
unit_id~,
),
register_profile(
object_id="modbus.setpoint",
label="modbus.holding.setpoint.write",
area=HoldingRegister,
address=0x0002U,
word_count=2,
direction=@trace.Tx,
object_kind=@isocontract.Command,
behavior="two-register setpoint write; framing is deferred to W2B",
unit_id~,
),
register_profile(
object_id="modbus.status",
label="modbus.input.status.read",
area=InputRegister,
address=0x0000U,
word_count=1,
direction=@trace.Rx,
object_kind=@isocontract.Signal,
behavior="status signal read from input register space",
unit_id~,
),
register_profile(
object_id="modbus.actual_value",
label="modbus.input.actual.read",
area=InputRegister,
address=0x0001U,
word_count=2,
direction=@trace.Rx,
object_kind=@isocontract.Signal,
behavior="actual value signal read from input registers",
unit_id~,
),
register_profile(
object_id="modbus.parameter",
label="modbus.holding.parameter.query",
area=HoldingRegister,
address=0x0010U,
word_count=1,
direction=@trace.Rx,
object_kind=@isocontract.Query,
behavior="register-backed parameter query response; request framing is deferred to W2B",
unit_id~,
),
]
}
///|
pub fn modbus_register_bindings(
unit_id? : Int = 1,
) -> Array[@isocontract.ObjectBinding] {
let bindings : Array[@isocontract.ObjectBinding] = []
for profile in modbus_register_profiles(unit_id~) {
bindings.push(modbus_binding(profile, unit_id~))
}
bindings
}
///|
pub fn modbus_register_contracts(
unit_id? : Int = 1,
) -> @isocontract.ContractRegistry {
let registry = @isocontract.ContractRegistry::new()
for profile in modbus_register_profiles(unit_id~) {
registry.register(contract_for_profile(profile, unit_id~))
}
registry
}
///|
pub fn modbus_register_protocol() -> @protocol_ast.GlobalProtocol {
let controller = @protocol_ast.role(name="controller")
let device = @protocol_ast.role(name="modbus-device")
@protocol_ast.GlobalProtocol::new(protocol_id="ModbusRegisterProfile")
.send(
from=controller,
to=device,
label="modbus.holding.lifecycle.write",
object_id="modbus.lifecycle",
)
.send(
from=controller,
to=device,
label="modbus.holding.command.write",
object_id="modbus.command",
)
.send(
from=controller,
to=device,
label="modbus.holding.setpoint.write",
object_id="modbus.setpoint",
)
.send(
from=device,
to=controller,
label="modbus.input.status.read",
object_id="modbus.status",
)
.send(
from=device,
to=controller,
label="modbus.input.actual.read",
object_id="modbus.actual_value",
)
}
///|
pub fn modbus_register_observer_backend(
unit_id? : Int = 1,
) -> @protocol_ast.ObserverBackend {
@protocol_ast.ObserverBackend::new(
registry=modbus_register_contracts(unit_id~),
protocol=modbus_register_protocol(),
roles=[@protocol_ast.role(name="controller")],
)
}
///|
pub fn modbus_register_trace_fixture(unit_id? : Int = 1) -> @trace.TraceLog {
let log = @trace.TraceLog::new()
log.append(
modbus_event(
1,
"modbus.lifecycle.eventless.register-model",
@trace.Meta,
0L,
None,
unit_id~,
),
)
log.append(
modbus_event(
2,
"modbus.holding.lifecycle.write",
@trace.Tx,
10L,
Some(1),
unit_id~,
),
)
log.append(
modbus_event(
3,
"modbus.holding.command.write",
@trace.Tx,
20L,
Some(0x0006),
unit_id~,
),
)
log.append(
modbus_event(
4,
"modbus.holding.setpoint.write",
@trace.Tx,
30L,
Some(12_345),
unit_id~,
),
)
log.append(
modbus_event(
5,
"modbus.input.status.read",
@trace.Rx,
40L,
Some(0x0021),
unit_id~,
),
)
log.append(
modbus_event(
6,
"modbus.input.actual.read",
@trace.Rx,
50L,
Some(12_300),
unit_id~,
),
)
log
}
///|
pub fn modbus_query_trace_fixture(unit_id? : Int = 1) -> @trace.TraceLog {
let log = @trace.TraceLog::new()
log.append(
modbus_event(
1,
"modbus.holding.parameter.query",
@trace.Rx,
0L,
Some(0x0042),
unit_id~,
),
)
log
}
///|
pub fn modbus_register_observer_report(
unit_id? : Int = 1,
) -> @protocol_ast.ConformanceReport {
modbus_register_observer_backend(unit_id~).check_trace(
modbus_register_trace_fixture(unit_id~),
)
}
///|
pub fn modbus_register_profile_report(
unit_id? : Int = 1,
) -> ModbusRegisterProfileReport {
let profiles = modbus_register_profiles(unit_id~)
let bindings = modbus_register_bindings(unit_id~)
let trace = modbus_register_trace_fixture(unit_id~)
let conformance = modbus_register_observer_backend(unit_id~).check_trace(
trace,
)
{
object_count: profiles.length(),
binding_count: bindings.length(),
trace_event_count: trace.len(),
checked_events: conformance.checked_events,
matched_steps: conformance.matched_steps,
eventless_lifecycle: true,
live_device_evidence: false,
conformance,
}
}
///|
fn register_profile(
object_id~ : String,
label~ : String,
area~ : ModbusRegisterArea,
address~ : UInt,
word_count~ : Int,
direction~ : @trace.TraceDirection,
object_kind~ : @isocontract.ObjectKind,
behavior~ : String,
unit_id~ : Int,
) -> ModbusRegisterProfile {
ignore(unit_id)
{
object_id,
label,
area,
address,
word_count,
direction,
object_kind,
behavior,
}
}
///|
fn contract_for_profile(
profile : ModbusRegisterProfile,
unit_id~ : Int,
) -> @isocontract.ObjectContract {
let binding = modbus_binding(profile, unit_id~)
match profile.object_kind {
@isocontract.Command =>
@isocontract.ObjectContract::make(
id=profile.object_id,
kind=@isocontract.Command,
schema=if profile.word_count == 1 {
@isocontract.U16
} else {
@isocontract.I32
},
timing=@isocontract.TimingContract::make(
max_age_ns=1_000_000L,
valid_for_ns=1_000_000L,
),
safety=@isocontract.SafetyContract::make(
authority=@isocontract.Role("controller"),
timeout_action="hold_last",
),
).with_binding(binding)
@isocontract.Signal =>
@isocontract.ObjectContract::make(
id=profile.object_id,
kind=@isocontract.Signal,
schema=if profile.word_count == 1 {
@isocontract.U16
} else {
@isocontract.I32
},
timing=@isocontract.TimingContract::make(
period_ns=10_000_000L,
max_age_ns=20_000_000L,
),
safety=@isocontract.SafetyContract::make(
authority=@isocontract.ReadOnly,
),
).with_binding(binding)
@isocontract.Query =>
@isocontract.ObjectContract::make(
id=profile.object_id,
kind=@isocontract.Query,
schema=@isocontract.U16,
timing=@isocontract.TimingContract::empty(),
safety=@isocontract.SafetyContract::make(
authority=@isocontract.ReadOnly,
),
).with_binding(binding)
@isocontract.Lifecycle =>
@isocontract.ObjectContract::make(
id=profile.object_id,
kind=@isocontract.Lifecycle,
schema=@isocontract.State,
timing=@isocontract.TimingContract::make(max_age_ns=20_000_000L),
safety=@isocontract.SafetyContract::make(
authority=@isocontract.Role("controller"),
),
).with_binding(binding)
@isocontract.Event =>
@isocontract.ObjectContract::make(
id=profile.object_id,
kind=@isocontract.Event,
schema=@isocontract.Bool,
timing=@isocontract.TimingContract::make(max_age_ns=20_000_000L),
safety=@isocontract.SafetyContract::make(
authority=@isocontract.ReadOnly,
),
).with_binding(binding)
}
}
///|
fn modbus_binding(
profile : ModbusRegisterProfile,
unit_id~ : Int,
) -> @isocontract.ObjectBinding {
@isocontract.ObjectBinding::make(
object_id=profile.object_id,
binding_id="modbus.unit" +
unit_id.to_string() +
"." +
profile.area.channel() +
"." +
profile.address.to_string(),
medium_id=modbus_medium_id(),
label=profile.label,
direction=profile.direction,
kind=@isocontract.ModbusRegister,
index=profile.address,
channel=profile.area.channel(),
authority=@isocontract.Role("controller"),
)
}
///|
fn modbus_event(
event_id : Int,
label : String,
direction : @trace.TraceDirection,
time_ns : Int64,
payload_digest : Int?,
unit_id~ : Int,
) -> @trace.TraceEvent {
@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=modbus_medium_id(),
channel_id=None,
direction~,
payload_digest~,
rng_step=event_id,
seed=520 + unit_id,
backend=@core.SimNative,
label~,
)
}