///|
/// Scenario-driven CiA 402 closed-loop fixture for offline conformance.
///|
pub fn package_id() -> String {
"fieldbus_core/cia402_scenario_loop"
}
///|
pub(all) enum Cia402ScenarioVariant {
Nominal
StaleCommand
OutOfOrderStatus
InjectedFault
} derive(Eq, Debug)
///|
pub fn Cia402ScenarioVariant::label(self : Cia402ScenarioVariant) -> String {
match self {
Nominal => "nominal"
StaleCommand => "stale_command"
OutOfOrderStatus => "out_of_order_status"
InjectedFault => "injected_fault"
}
}
///|
pub(all) struct Cia402ScenarioReport {
seed : Int
backend : @core.BackendProfile
variant : Cia402ScenarioVariant
trace : @trace.TraceLog
scenario_digest : @core.SimDigest
recording_digest : @core.SimDigest
replay_digest : @core.SimDigest
portable_digest : @core.SimDigest
fault_report : @fault.FaultReport
conformance : @protocol_ast.ConformanceReport
} derive(Debug)
///|
pub(all) struct Cia402ConformanceArtifact {
scenario : Cia402ScenarioReport
trace_artifact : @trace.TraceArtifact
summary : String
} derive(Debug)
///|
pub fn Cia402ScenarioReport::passes(self : Cia402ScenarioReport) -> Bool {
self.conformance.passes() && self.replay_digest_matches()
}
///|
pub fn Cia402ScenarioReport::replay_digest_matches(
self : Cia402ScenarioReport,
) -> Bool {
self.recording_digest == self.replay_digest
}
///|
pub fn Cia402ScenarioReport::conformance_summary(
self : Cia402ScenarioReport,
) -> String {
"scenario=cia402" +
"|variant=" +
self.variant.label() +
"|seed=" +
self.seed.to_string() +
"|backend=" +
@core.backend_profile_label(self.backend) +
"|passes=" +
bool_text(self.passes()) +
"|event_count=" +
self.trace.len().to_string() +
"|matched_steps=" +
self.conformance.matched_steps.to_string() +
"|replay_digest_matches=" +
bool_text(self.replay_digest_matches()) +
"|first_failure=" +
conformance_failure_text(self.conformance.first_failure)
}
///|
pub fn Cia402ScenarioReport::to_conformance_artifact(
self : Cia402ScenarioReport,
) -> Cia402ConformanceArtifact {
{
scenario: self,
trace_artifact: self.trace.to_artifact(seed=self.seed),
summary: self.conformance_summary(),
}
}
///|
pub fn Cia402ConformanceArtifact::passes(
self : Cia402ConformanceArtifact,
) -> Bool {
self.scenario.passes()
}
///|
pub fn Cia402ConformanceArtifact::artifact_replay_digest_matches(
self : Cia402ConformanceArtifact,
) -> Bool raise @trace.TraceArtifactError {
self.trace_artifact.replay_digest() == self.scenario.recording_digest
}
///|
pub fn run_cia402_conformance_artifact(
seed~ : Int,
backend? : @core.BackendProfile = @core.SimNative,
variant? : Cia402ScenarioVariant = Nominal,
) -> Cia402ConformanceArtifact {
run_cia402_scenario(seed~, backend~, variant~).to_conformance_artifact()
}
///|
pub fn run_cia402_scenario(
seed~ : Int,
backend? : @core.BackendProfile = @core.SimNative,
variant? : Cia402ScenarioVariant = Nominal,
) -> Cia402ScenarioReport {
let env = @scenario.SimEnv::new(seed~)
let recording = @trace.Recording::new()
let fault_plan = @fault.FaultPlan::new(seed~)
match variant {
OutOfOrderStatus => {
emit_status_read(env, recording, seed, backend, event_id=1, delay_ns=0L)
emit_shutdown_write(
env,
recording,
seed,
backend,
event_id=2,
delay_ns=10L,
)
}
_ => {
emit_shutdown_write(
env,
recording,
seed,
backend,
event_id=1,
delay_ns=0L,
raw_offset_ns=if variant is StaleCommand { 2_000_001L } else { 0L },
)
emit_status_read(env, recording, seed, backend, event_id=2, delay_ns=10L)
}
}
emit_mode_write(env, recording, seed, backend, event_id=3, delay_ns=10L)
emit_target_write(env, recording, seed, backend, event_id=4, delay_ns=10L)
emit_actual_read(env, recording, seed, backend, event_id=5, delay_ns=10L)
emit_profile_fault(env, recording, seed, backend, event_id=6, delay_ns=10L)
let next_id = if variant is InjectedFault {
fault_plan.add_rule(@fault.FaultRule::new(@fault.Drop, @fault.Once))
let outcome = fault_plan.apply(b"cia402.injected-fault")
match outcome.hit {
Some(hit) =>
recording.record(
hit.to_trace_event(
event_id=7,
vtime=env.now(),
clock_domain="sim",
node_id="gateway",
medium_id="trace",
backend~,
),
)
None => ()
}
8
} else {
7
}
emit_fault_reset(
env,
recording,
seed,
backend,
event_id=next_id,
delay_ns=10L,
)
emit_reset_status(
env,
recording,
seed,
backend,
event_id=next_id + 1,
delay_ns=10L,
)
let trace = recording.get_log()
let replay = @trace.Replay::from_log(trace)
let recording_digest = recording.digest(seed~)
let replay_digest = replay.digest(seed~)
let portable_digest = trace.portable_digest(seed~)
{
seed,
backend,
variant,
trace,
scenario_digest: env.checkpoint(),
recording_digest,
replay_digest,
portable_digest,
fault_report: fault_plan.report(),
conformance: @moonecat_cia402_adapter.moonecat_cia402_observer_backend().check_trace(
trace,
),
}
}
///|
fn emit_shutdown_write(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
raw_offset_ns? : Int64 = 0L,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
raw_offset_ns~,
label="cia402.controlword.write",
direction=@trace.Tx,
payload_digest=Some(0x0006),
)
}
///|
fn emit_status_read(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.statusword.read",
direction=@trace.Rx,
payload_digest=Some(0x0021),
)
}
///|
fn emit_mode_write(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.mode.write",
direction=@trace.Tx,
payload_digest=Some(0x08),
)
}
///|
fn emit_target_write(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.target_position.write",
direction=@trace.Tx,
payload_digest=Some(607_000),
)
}
///|
fn emit_actual_read(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.actual_position.read",
direction=@trace.Rx,
payload_digest=Some(606_400),
)
}
///|
fn emit_profile_fault(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.fault.detected",
direction=@trace.Rx,
payload_digest=Some(1),
)
}
///|
fn emit_fault_reset(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.controlword.write",
direction=@trace.Tx,
payload_digest=Some(0x0080),
)
}
///|
fn emit_reset_status(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
) -> Unit {
emit_profile_event(
env,
recording,
seed,
backend,
event_id~,
delay_ns~,
label="cia402.statusword.read",
direction=@trace.Rx,
payload_digest=Some(0x0040),
)
}
///|
fn emit_profile_event(
env : @scenario.SimEnv,
recording : @trace.Recording,
seed : Int,
backend : @core.BackendProfile,
event_id~ : Int,
delay_ns~ : Int64,
raw_offset_ns? : Int64 = 0L,
label~ : String,
direction~ : @trace.TraceDirection,
payload_digest~ : Int?,
) -> Unit {
ignore(
env.run_until_event(
@scenario.timeout(env, @core.Duration::from_ns(delay_ns)),
),
)
let time_ns = env.now().ns()
recording.record(
@trace.TraceEvent::make(
event_id~,
parent_id=None,
vtime=env.now(),
clock_domain="sim",
raw_ns=time_ns + raw_offset_ns,
node_id="controller",
medium_id="trace",
channel_id=None,
direction~,
payload_digest~,
rng_step=event_id,
seed~,
backend~,
label~,
),
)
}
///|
fn bool_text(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn conformance_failure_text(
failure : @protocol_ast.ConformanceFailure?,
) -> String {
match failure {
None => "none"
Some(failure) =>
failure.source.label() +
":" +
event_option_text(failure.event_id) +
":" +
role_option_text(failure.role) +
":" +
failure.reason
}
}
///|
fn event_option_text(value : Int?) -> String {
match value {
Some(v) => v.to_string()
None => "none"
}
}
///|
fn role_option_text(value : @protocol_ast.Role?) -> String {
match value {
Some(role) => role.label()
None => "none"
}
}