///|
/// Access permission for a deterministic ECU memory region.
pub enum EcuMemoryPermission {
EcuMemoryReadable
EcuMemoryWritable
EcuMemoryReadWrite
EcuMemoryLocked
}
///|
pub fn ecu_memory_permission_variants() -> Array[EcuMemoryPermission] {
[EcuMemoryReadable, EcuMemoryWritable, EcuMemoryReadWrite, EcuMemoryLocked]
}
///|
/// An addressable memory region exposed by an ECU simulator.
pub struct EcuMemoryRegion {
start : UInt
length : Int
permission : EcuMemoryPermission
bytes : Array[Byte]
}
///|
pub suberror EcuMemoryError {
EcuMemoryOutOfRange
EcuMemoryPermissionDenied
EcuMemoryInvalidLength
EcuMemoryOverlappingRegion
}
///|
/// Create a zero-initialized ECU memory region.
pub fn ecu_memory_region(
start : UInt,
length : Int,
permission? : EcuMemoryPermission = EcuMemoryReadWrite,
) -> EcuMemoryRegion raise EcuMemoryError {
if length < 0 {
raise EcuMemoryInvalidLength
}
{ start, length, permission, bytes: Array::make(length, 0) }
}
///|
pub fn EcuMemoryRegion::start(self : EcuMemoryRegion) -> UInt {
self.start
}
///|
pub fn EcuMemoryRegion::length(self : EcuMemoryRegion) -> Int {
self.length
}
///|
pub fn EcuMemoryRegion::permission(
self : EcuMemoryRegion,
) -> EcuMemoryPermission {
self.permission
}
///|
pub fn EcuMemoryRegion::contains(
self : EcuMemoryRegion,
address : UInt,
length : Int,
) -> Bool {
length >= 0 &&
address >= self.start &&
address - self.start <= self.length.reinterpret_as_uint() &&
length <= self.length - (address - self.start).reinterpret_as_int()
}
///|
pub fn EcuMemoryRegion::read(
self : EcuMemoryRegion,
address : UInt,
length : Int,
) -> Array[Byte] raise EcuMemoryError {
if !self.contains(address, length) {
raise EcuMemoryOutOfRange
}
match self.permission {
EcuMemoryWritable | EcuMemoryLocked => raise EcuMemoryPermissionDenied
_ =>
self.bytes[address.reinterpret_as_int() - self.start.reinterpret_as_int():address.reinterpret_as_int() -
self.start.reinterpret_as_int() +
length].to_owned()
}
}
///|
pub fn EcuMemoryRegion::write(
self : EcuMemoryRegion,
address : UInt,
data : Array[Byte],
) -> Unit raise EcuMemoryError {
if !self.contains(address, data.length()) {
raise EcuMemoryOutOfRange
}
match self.permission {
EcuMemoryReadable | EcuMemoryLocked => raise EcuMemoryPermissionDenied
_ => {
let offset = address.reinterpret_as_int() -
self.start.reinterpret_as_int()
for index, value in data {
self.bytes[offset + index] = value
}
}
}
}
///|
pub fn EcuMemoryRegion::fill(
self : EcuMemoryRegion,
value : Byte,
) -> Unit raise EcuMemoryError {
match self.permission {
EcuMemoryReadable | EcuMemoryLocked => raise EcuMemoryPermissionDenied
_ =>
for index in 0.. Array[Byte] {
self.bytes.copy()
}
///|
/// A collection of non-overlapping ECU memory regions.
pub struct EcuMemoryMap {
regions : Array[EcuMemoryRegion]
mut reads : Int
mut writes : Int
}
///|
pub fn new_ecu_memory_map() -> EcuMemoryMap {
{ regions: [], reads: 0, writes: 0 }
}
///|
pub fn EcuMemoryMap::add(
self : EcuMemoryMap,
region : EcuMemoryRegion,
) -> Unit raise EcuMemoryError {
for item in self.regions {
let overlaps = region.start() <
item.start() + item.length().reinterpret_as_uint() &&
item.start() < region.start() + region.length().reinterpret_as_uint()
if overlaps {
raise EcuMemoryOverlappingRegion
}
}
self.regions.push(region)
}
///|
pub fn EcuMemoryMap::find(
self : EcuMemoryMap,
address : UInt,
length : Int,
) -> EcuMemoryRegion? {
for region in self.regions {
if region.contains(address, length) {
return Some(region)
}
}
None
}
///|
pub fn EcuMemoryMap::read(
self : EcuMemoryMap,
address : UInt,
length : Int,
) -> Array[Byte] raise EcuMemoryError {
match self.find(address, length) {
Some(region) => {
self.reads += 1
region.read(address, length)
}
None => raise EcuMemoryOutOfRange
}
}
///|
pub fn EcuMemoryMap::write(
self : EcuMemoryMap,
address : UInt,
data : Array[Byte],
) -> Unit raise EcuMemoryError {
match self.find(address, data.length()) {
Some(region) => {
self.writes += 1
region.write(address, data)
}
None => raise EcuMemoryOutOfRange
}
}
///|
pub fn EcuMemoryMap::regions(self : EcuMemoryMap) -> Array[EcuMemoryRegion] {
self.regions.copy()
}
///|
pub fn EcuMemoryMap::length(self : EcuMemoryMap) -> Int {
self.regions.length()
}
///|
pub fn EcuMemoryMap::reads(self : EcuMemoryMap) -> Int {
self.reads
}
///|
pub fn EcuMemoryMap::writes(self : EcuMemoryMap) -> Int {
self.writes
}
///|
/// Security access state maintained by an ECU endpoint.
pub enum EcuSecurityState {
EcuSecurityLocked
EcuSecuritySeedIssued(Byte)
EcuSecurityUnlocked(Byte)
EcuSecurityDelay(UInt64)
}
///|
pub fn ecu_security_state_variants() -> Array[EcuSecurityState] {
[
EcuSecurityLocked,
EcuSecuritySeedIssued(1),
EcuSecurityUnlocked(1),
EcuSecurityDelay(0),
]
}
///|
/// Deterministic security access policy for tests and simulation.
pub struct EcuSecurityPolicy {
level : Byte
secret : UInt
max_attempts : Int
delay_us : UInt64
mut attempts : Int
mut state : EcuSecurityState
mut seed_counter : UInt
}
///|
pub fn ecu_security_policy(
level : Byte,
secret : UInt,
max_attempts? : Int = 3,
delay_us? : UInt64 = 1_000_000,
) -> EcuSecurityPolicy {
{
level,
secret,
max_attempts: if max_attempts < 1 {
1
} else {
max_attempts
},
delay_us,
attempts: 0,
state: EcuSecurityLocked,
seed_counter: 0x1357,
}
}
///|
pub fn EcuSecurityPolicy::level(self : EcuSecurityPolicy) -> Byte {
self.level
}
///|
pub fn EcuSecurityPolicy::attempts(self : EcuSecurityPolicy) -> Int {
self.attempts
}
///|
pub fn EcuSecurityPolicy::state(self : EcuSecurityPolicy) -> EcuSecurityState {
self.state
}
///|
pub fn EcuSecurityPolicy::is_unlocked(self : EcuSecurityPolicy) -> Bool {
self.state is EcuSecurityUnlocked(_)
}
///|
pub fn EcuSecurityPolicy::issue_seed(
self : EcuSecurityPolicy,
timestamp_us : UInt64,
) -> UInt {
match self.state {
EcuSecurityDelay(until) =>
if timestamp_us < until {
0
} else {
self.state = EcuSecurityLocked
self.issue_seed(timestamp_us)
}
_ => {
self.seed_counter = (self.seed_counter * 1103515245 + 12345) & 0x7FFFFFFF
self.state = EcuSecuritySeedIssued(self.level)
self.seed_counter
}
}
}
///|
pub fn EcuSecurityPolicy::expected_key(
self : EcuSecurityPolicy,
seed : UInt,
) -> UInt {
((seed ^ self.secret) + self.level.to_uint()) & 0xFFFFFFFF
}
///|
pub fn EcuSecurityPolicy::unlock(
self : EcuSecurityPolicy,
key : UInt,
seed : UInt,
timestamp_us : UInt64,
) -> Bool {
if self.state is EcuSecurityDelay(until) && timestamp_us < until {
false
} else if self.state is EcuSecuritySeedIssued(_) &&
key == self.expected_key(seed) {
self.state = EcuSecurityUnlocked(self.level)
self.attempts = 0
true
} else {
self.attempts += 1
if self.attempts >= self.max_attempts {
self.state = EcuSecurityDelay(timestamp_us + self.delay_us)
self.attempts = 0
}
false
}
}
///|
pub fn EcuSecurityPolicy::lock(self : EcuSecurityPolicy) -> Unit {
self.state = EcuSecurityLocked
}
///|
/// ECU reset kinds understood by the simulator.
pub enum EcuResetKind {
EcuHardReset
EcuKeyOffOnReset
EcuSoftReset
EcuResetBySupplier(Byte)
}
///|
pub fn ecu_reset_kind_variants() -> Array[EcuResetKind] {
[EcuHardReset, EcuKeyOffOnReset, EcuSoftReset, EcuResetBySupplier(0)]
}
///|
/// The result of handling a diagnostic request at an ECU endpoint.
pub enum EcuRequestOutcome {
EcuPositiveResponse(Array[Byte])
EcuNegativeResponse(Byte)
EcuRequestRejected(String)
}
///|
pub fn ecu_request_outcome_variants() -> Array[EcuRequestOutcome] {
[
EcuPositiveResponse([]),
EcuNegativeResponse(0x11),
EcuRequestRejected("example"),
]
}
///|
/// A deterministic ECU endpoint for integration testing.
pub struct EcuEndpoint {
address : UInt
name : String
mut session : DiagnosticSessionProfile
data : DiagnosticDataTable
dtcs : DiagnosticTroubleCodeStore
memory : EcuMemoryMap
security : EcuSecurityPolicy
mut reset_count : Int
mut request_count : Int
mut positive_count : Int
mut negative_count : Int
mut last_timestamp_us : UInt64
mut pending_reset : EcuResetKind?
}
///|
/// Create an ECU endpoint with empty data, DTC and memory tables.
pub fn new_ecu_endpoint(
address : UInt,
name : String,
security? : EcuSecurityPolicy = ecu_security_policy(1, 0xA5A55A5A),
) -> EcuEndpoint {
{
address,
name,
session: default_diagnostic_session(),
data: new_diagnostic_data_table(),
dtcs: new_diagnostic_trouble_code_store(64),
memory: new_ecu_memory_map(),
security,
reset_count: 0,
request_count: 0,
positive_count: 0,
negative_count: 0,
last_timestamp_us: 0,
pending_reset: None,
}
}
///|
pub fn EcuEndpoint::address(self : EcuEndpoint) -> UInt {
self.address
}
///|
pub fn EcuEndpoint::name(self : EcuEndpoint) -> String {
self.name
}
///|
pub fn EcuEndpoint::session(self : EcuEndpoint) -> DiagnosticSessionProfile {
self.session
}
///|
pub fn EcuEndpoint::data(self : EcuEndpoint) -> DiagnosticDataTable {
self.data
}
///|
pub fn EcuEndpoint::dtcs(self : EcuEndpoint) -> DiagnosticTroubleCodeStore {
self.dtcs
}
///|
pub fn EcuEndpoint::memory(self : EcuEndpoint) -> EcuMemoryMap {
self.memory
}
///|
pub fn EcuEndpoint::security(self : EcuEndpoint) -> EcuSecurityPolicy {
self.security
}
///|
pub fn EcuEndpoint::reset_count(self : EcuEndpoint) -> Int {
self.reset_count
}
///|
pub fn EcuEndpoint::request_count(self : EcuEndpoint) -> Int {
self.request_count
}
///|
pub fn EcuEndpoint::positive_count(self : EcuEndpoint) -> Int {
self.positive_count
}
///|
pub fn EcuEndpoint::negative_count(self : EcuEndpoint) -> Int {
self.negative_count
}
///|
pub fn EcuEndpoint::pending_reset(self : EcuEndpoint) -> EcuResetKind? {
self.pending_reset
}
///|
pub fn EcuEndpoint::register_data(
self : EcuEndpoint,
item : DiagnosticDataIdentifier,
) -> Bool {
self.data.add(item)
}
///|
pub fn EcuEndpoint::register_memory(
self : EcuEndpoint,
region : EcuMemoryRegion,
) -> Bool {
try {
self.memory.add(region)
true
} catch {
_ => false
}
}
///|
pub fn EcuEndpoint::record_dtc(
self : EcuEndpoint,
item : DiagnosticTroubleCode,
) -> Unit {
self.dtcs.record(item)
}
///|
pub fn EcuEndpoint::clear_dtcs(
self : EcuEndpoint,
timestamp_us : UInt64,
) -> Unit {
self.dtcs.clear(timestamp_us)
}
///|
/// Handle a request and update endpoint counters.
pub fn EcuEndpoint::handle(
self : EcuEndpoint,
request : DiagnosticRequest,
timestamp_us : UInt64,
) -> EcuRequestOutcome {
self.request_count += 1
self.last_timestamp_us = timestamp_us
let result = match request.service() {
DiagnosticSessionControl => self.handle_session(request.payload())
EcuReset => self.handle_reset(request.payload())
ReadDataByIdentifier => self.handle_read_data(request.payload())
WriteDataByIdentifier => self.handle_write_data(request.payload())
ReadMemoryByAddress => self.handle_read_memory(request.payload())
WriteMemoryByAddress => self.handle_write_memory(request.payload())
SecurityAccess => self.handle_security(request.payload(), timestamp_us)
ClearDiagnosticInformation =>
self.handle_clear_dtc(request.payload(), timestamp_us)
ReadDtcInformation => EcuPositiveResponse(self.dtcs.encode_report(AllDtc))
TesterPresent => EcuPositiveResponse([0x7E, 0x00])
_ => EcuNegativeResponse(0x11)
}
match result {
EcuPositiveResponse(_) => self.positive_count += 1
_ => self.negative_count += 1
}
result
}
///|
fn EcuEndpoint::handle_session(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 2 {
EcuNegativeResponse(0x13)
} else {
let kind = match payload[1].to_int() {
0x01 => DiagnosticSessionKind::DefaultSession
0x02 => DiagnosticSessionKind::ProgrammingSession
0x03 => DiagnosticSessionKind::ExtendedSession
0x04 => DiagnosticSessionKind::SafetySystemSession
value => DiagnosticSessionKind::SupplierSession(value.to_byte())
}
self.session = diagnostic_session_profile(
kind, 50_000, 5_000_000, 5_000_000,
)
EcuPositiveResponse([0x50, payload[1], 0x00, 0x32, 0x01, 0xF4])
}
}
///|
fn EcuEndpoint::handle_reset(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 2 {
EcuNegativeResponse(0x13)
} else {
let kind = match payload[1].to_int() {
0x01 => EcuHardReset
0x02 => EcuKeyOffOnReset
0x03 => EcuSoftReset
value => EcuResetBySupplier(value.to_byte())
}
self.pending_reset = Some(kind)
self.reset_count += 1
EcuPositiveResponse([0x51, payload[1]])
}
}
///|
fn EcuEndpoint::handle_read_data(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 3 {
EcuNegativeResponse(0x13)
} else {
let identifier = (payload[1].to_uint() << 8) | payload[2].to_uint()
match self.data.read(identifier) {
Some(value) => EcuPositiveResponse([0x62, payload[1], payload[2]] + value)
None => EcuNegativeResponse(0x31)
}
}
}
///|
fn EcuEndpoint::handle_write_data(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 3 {
EcuNegativeResponse(0x13)
} else {
let identifier = (payload[1].to_uint() << 8) | payload[2].to_uint()
if self.data.write(identifier, payload[3:].to_owned()) {
EcuPositiveResponse([0x6E, payload[1], payload[2]])
} else {
EcuNegativeResponse(0x31)
}
}
}
///|
fn EcuEndpoint::handle_read_memory(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 5 {
EcuNegativeResponse(0x13)
} else {
let address = (payload[1].to_uint() << 24) |
(payload[2].to_uint() << 16) |
(payload[3].to_uint() << 8) |
payload[4].to_uint()
let length = if payload.length() > 5 { payload[5].to_int() } else { 0 }
let data = self.memory.read(address, length) catch { _ => [] }
if data.length() != length {
EcuNegativeResponse(0x31)
} else {
EcuPositiveResponse([0x63] + data)
}
}
}
///|
fn EcuEndpoint::handle_write_memory(
self : EcuEndpoint,
payload : Array[Byte],
) -> EcuRequestOutcome {
if payload.length() < 6 {
EcuNegativeResponse(0x13)
} else {
let address = (payload[1].to_uint() << 24) |
(payload[2].to_uint() << 16) |
(payload[3].to_uint() << 8) |
payload[4].to_uint()
let data = payload[5:].to_owned()
if self.memory.find(address, data.length()) is None {
EcuNegativeResponse(0x31)
} else {
ignore(self.memory.write(address, data) catch { _ => () })
EcuPositiveResponse([0x7D])
}
}
}
///|
fn EcuEndpoint::handle_security(
self : EcuEndpoint,
payload : Array[Byte],
timestamp_us : UInt64,
) -> EcuRequestOutcome {
if payload.length() < 2 {
EcuNegativeResponse(0x13)
} else if (payload[1].to_int() & 1) == 1 {
let seed = self.security.issue_seed(timestamp_us)
EcuPositiveResponse([
0x67,
payload[1],
(seed >> 8).to_byte(),
seed.to_byte(),
])
} else if payload.length() < 4 {
EcuNegativeResponse(0x13)
} else {
let key = (payload[2].to_uint() << 8) | payload[3].to_uint()
let unlocked = self.security.unlock(key, 0, timestamp_us)
if unlocked {
EcuPositiveResponse([0x67, payload[1]])
} else {
EcuNegativeResponse(0x35)
}
}
}
///|
fn EcuEndpoint::handle_clear_dtc(
self : EcuEndpoint,
payload : Array[Byte],
timestamp_us : UInt64,
) -> EcuRequestOutcome {
if payload.length() < 4 {
EcuNegativeResponse(0x13)
} else {
self.dtcs.clear(timestamp_us)
EcuPositiveResponse([0x54])
}
}
///|
/// Return a compact endpoint health line for a simulation report.
pub fn EcuEndpoint::to_text(self : EcuEndpoint) -> String {
"ecu=\{self.name} address=\{self.address} session=\{diagnostic_session_name(self.session.kind())} requests=\{self.request_count} positive=\{self.positive_count} negative=\{self.negative_count} dtcs=\{self.dtcs.length()}"
}
///|
/// A deterministic fleet of ECU endpoints.
pub struct EcuFleet {
endpoints : Array[EcuEndpoint]
mut routed : Int
mut missed : Int
}
///|
pub fn new_ecu_fleet() -> EcuFleet {
{ endpoints: [], routed: 0, missed: 0 }
}
///|
pub fn EcuFleet::add(self : EcuFleet, endpoint : EcuEndpoint) -> Bool {
if self.find(endpoint.address()) is Some(_) {
false
} else {
self.endpoints.push(endpoint)
true
}
}
///|
pub fn EcuFleet::remove(self : EcuFleet, address : UInt) -> Bool {
match self.find_index(address) {
Some(index) => {
ignore(self.endpoints.remove(index))
true
}
None => false
}
}
///|
pub fn EcuFleet::find(self : EcuFleet, address : UInt) -> EcuEndpoint? {
match self.find_index(address) {
Some(index) => Some(self.endpoints[index])
None => None
}
}
///|
pub fn EcuFleet::handle(
self : EcuFleet,
address : UInt,
request : DiagnosticRequest,
timestamp_us : UInt64,
) -> EcuRequestOutcome {
match self.find(address) {
Some(endpoint) => {
self.routed += 1
endpoint.handle(request, timestamp_us)
}
None => {
self.missed += 1
EcuRequestRejected("unknown ECU address")
}
}
}
///|
pub fn EcuFleet::length(self : EcuFleet) -> Int {
self.endpoints.length()
}
///|
pub fn EcuFleet::routed(self : EcuFleet) -> Int {
self.routed
}
///|
pub fn EcuFleet::missed(self : EcuFleet) -> Int {
self.missed
}
///|
pub fn EcuFleet::endpoints(self : EcuFleet) -> Array[EcuEndpoint] {
self.endpoints.copy()
}
///|
pub fn EcuFleet::to_text(self : EcuFleet) -> String {
let lines : Array[String] = []
for endpoint in self.endpoints {
lines.push(endpoint.to_text())
}
lines.join("\n")
}
///|
fn EcuFleet::find_index(self : EcuFleet, address : UInt) -> Int? {
for index, endpoint in self.endpoints {
if endpoint.address() == address {
return Some(index)
}
}
None
}