use crate::engine::{ADDR_GLOBAL, ADDR_NULL, RawFrame};
pub const ADDR_MAX: u8 = 253;
pub const CLAIM_TIMEOUT_MS: u64 = 250;
pub const SCAN_TIMEOUT_MS: u64 = 1000;
pub const USED_TTL_MS: u64 = 15 * 60 * 1000;
const PGN_ISO_REQUEST: u32 = 59904;
const PGN_ISO_ADDRESS_CLAIM: u32 = 60928;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ClaimState {
Disabled,
Scanning,
Pending,
Claimed,
Failed,
}
pub struct AddressClaim {
name: u64,
address: u8,
preferred: u8,
state: ClaimState,
deadline: u64,
arbitrary: bool,
defending: bool,
last_seen: [Option<u64>; 256],
}
impl AddressClaim {
pub fn new(name: u64, preferred: u8, arbitrary: bool) -> Self {
Self {
name,
address: preferred,
preferred,
state: ClaimState::Pending,
deadline: 0,
arbitrary,
defending: false,
last_seen: [None; 256],
}
}
pub fn for_name(name: &crate::io::name::Name, preferred: u8) -> Self {
Self::new(
name.to_u64(),
preferred,
name.is_arbitrary_address_capable(),
)
}
pub fn disabled(name: u64) -> Self {
Self {
state: ClaimState::Disabled,
..Self::new(name, 0, true)
}
}
pub fn state(&self) -> ClaimState {
self.state
}
pub fn is_claimed(&self) -> bool {
self.state == ClaimState::Claimed
}
pub fn address(&self) -> Option<u8> {
self.is_claimed().then_some(self.address)
}
pub fn send_address(&self) -> Option<u8> {
(self.state == ClaimState::Claimed || (self.state == ClaimState::Pending && self.defending))
.then_some(self.address)
}
pub fn name(&self) -> u64 {
self.name
}
pub fn deadline(&self) -> u64 {
self.deadline
}
pub fn is_timing(&self) -> bool {
matches!(self.state, ClaimState::Scanning | ClaimState::Pending)
}
pub fn start(&mut self, now: u64) -> Vec<RawFrame> {
if self.state == ClaimState::Disabled {
return Vec::new();
}
self.state = ClaimState::Scanning;
self.deadline = now + SCAN_TIMEOUT_MS;
vec![request_frame(ADDR_NULL, ADDR_GLOBAL, PGN_ISO_ADDRESS_CLAIM)]
}
pub fn on_address_claim(&mut self, now: u64, src: u8, their_name: u64) -> Vec<RawFrame> {
if src > ADDR_MAX || self.state == ClaimState::Disabled {
return Vec::new();
}
if their_name == self.name {
return Vec::new();
}
if self.state == ClaimState::Scanning || src != self.address {
self.last_seen[src as usize] = Some(now);
return Vec::new();
}
if self.name < their_name {
log::info!(
"address claim: kept address {src} against a higher NAME ({}); ours is {}",
describe_name(their_name),
describe_name(self.name)
);
self.defending = self.send_address().is_some();
self.state = ClaimState::Pending;
self.deadline = now + CLAIM_TIMEOUT_MS;
return vec![self.claim_frame()];
}
self.defending = false;
self.last_seen[src as usize] = Some(now);
if self.arbitrary
&& let Some(next) = self.pick_free(now)
{
log::warn!(
"address claim: lost address {src} to a lower NAME ({}); ours ({}) moves to {next}",
describe_name(their_name),
describe_name(self.name)
);
self.address = next;
self.state = ClaimState::Pending;
self.deadline = now + CLAIM_TIMEOUT_MS;
return vec![self.claim_frame()];
}
log::warn!(
"address claim: lost address {src} to a lower NAME ({}); ours ({}) has no other \
address to go to and goes silent",
describe_name(their_name),
describe_name(self.name)
);
self.address = ADDR_NULL;
self.state = ClaimState::Failed;
vec![self.claim_frame()] }
pub fn respond_to_claim_request(&self) -> Option<RawFrame> {
matches!(self.state, ClaimState::Claimed | ClaimState::Pending).then(|| self.claim_frame())
}
pub fn tick(&mut self, now: u64) -> Vec<RawFrame> {
match self.state {
ClaimState::Scanning if now >= self.deadline => self.begin_claim(now),
ClaimState::Pending if now >= self.deadline => {
self.state = ClaimState::Claimed;
self.defending = false;
Vec::new()
}
_ => Vec::new(),
}
}
fn begin_claim(&mut self, now: u64) -> Vec<RawFrame> {
if self.in_use(self.preferred, now) {
match self.pick_free(now) {
Some(next) => self.address = next,
None => {
self.address = ADDR_NULL;
self.state = ClaimState::Failed;
return vec![self.claim_frame()];
}
}
} else {
self.address = self.preferred;
}
self.state = ClaimState::Pending;
self.defending = false;
self.deadline = now + CLAIM_TIMEOUT_MS;
vec![self.claim_frame()]
}
fn in_use(&self, address: u8, now: u64) -> bool {
self.last_seen[address as usize].is_some_and(|seen| now.saturating_sub(seen) < USED_TTL_MS)
}
fn pick_free(&self, now: u64) -> Option<u8> {
(self.preferred..=ADDR_MAX)
.chain(0..self.preferred)
.find(|&a| !self.in_use(a, now))
}
fn claim_frame(&self) -> RawFrame {
RawFrame::new(
None,
6,
PGN_ISO_ADDRESS_CLAIM,
self.address,
ADDR_GLOBAL,
self.name.to_le_bytes(),
)
}
}
fn describe_name(name: u64) -> String {
format!(
"{name:#018x}: manufacturer {}, class {}, function {}, instance {}, \
system instance {}, industry {}, unique {:#x}",
(name >> 21) & 0x7ff,
(name >> 49) & 0x7f,
(name >> 40) & 0xff,
(name >> 32) & 0xff,
(name >> 56) & 0x0f,
(name >> 60) & 0x07,
name & 0x1f_ffff
)
}
fn request_frame(src: u8, dst: u8, pgn: u32) -> RawFrame {
RawFrame::new(
None,
6,
PGN_ISO_REQUEST,
src,
dst,
[pgn as u8, (pgn >> 8) as u8, (pgn >> 16) as u8],
)
}
#[cfg(test)]
mod tests {
use super::*;
const LOW: u64 = 0x00AA_BBCC_DDEE_FF00;
const HIGH: u64 = 0x80AA_BBCC_DDEE_FF00;
fn claim_frames(frames: &[RawFrame]) -> Vec<(u8, u8)> {
frames
.iter()
.filter(|f| f.pgn == PGN_ISO_ADDRESS_CLAIM)
.map(|f| (f.src, f.dst))
.collect()
}
#[test]
fn scan_then_claim_preferred() {
let mut c = AddressClaim::new(LOW, 42, true);
let out = c.start(0);
assert_eq!(out.len(), 1);
assert_eq!(out[0].pgn, PGN_ISO_REQUEST);
assert_eq!(c.state(), ClaimState::Scanning);
let out = c.tick(SCAN_TIMEOUT_MS);
assert_eq!(claim_frames(&out), vec![(42, ADDR_GLOBAL)]);
assert_eq!(c.state(), ClaimState::Pending);
assert_eq!(c.address(), None, "not owned until the claim settles");
let out = c.tick(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS);
assert!(out.is_empty());
assert!(c.is_claimed());
assert_eq!(c.address(), Some(42));
}
#[test]
fn scan_learns_used_and_avoids_preferred() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
assert!(c.on_address_claim(10, 42, HIGH).is_empty());
let out = c.tick(SCAN_TIMEOUT_MS);
assert_eq!(claim_frames(&out), vec![(43, ADDR_GLOBAL)]);
}
#[test]
fn wins_conflict_with_lower_name() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS); let out = c.on_address_claim(SCAN_TIMEOUT_MS, 42, HIGH);
assert_eq!(claim_frames(&out), vec![(42, ADDR_GLOBAL)]);
assert_eq!(c.state(), ClaimState::Pending);
}
#[test]
fn loses_conflict_and_moves_when_arbitrary() {
let mut c = AddressClaim::new(HIGH, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS); let out = c.on_address_claim(SCAN_TIMEOUT_MS, 42, LOW);
assert_eq!(
claim_frames(&out),
vec![(43, ADDR_GLOBAL)],
"moved up from the contested address, not down to 0"
);
assert_eq!(c.state(), ClaimState::Pending);
}
#[test]
fn the_search_for_a_free_address_wraps_round() {
let mut c = AddressClaim::new(LOW, ADDR_MAX, true);
c.start(0);
c.on_address_claim(10, ADDR_MAX, HIGH);
let out = c.tick(SCAN_TIMEOUT_MS);
assert_eq!(claim_frames(&out), vec![(0, ADDR_GLOBAL)]);
}
#[test]
fn an_address_not_heard_from_is_free_again() {
let mut c = AddressClaim::new(HIGH, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS); c.on_address_claim(SCAN_TIMEOUT_MS, 42, LOW); assert_eq!(c.pick_free(SCAN_TIMEOUT_MS + 1), Some(43));
assert_eq!(
c.pick_free(SCAN_TIMEOUT_MS + USED_TTL_MS),
Some(42),
"the winner has not re-claimed 42 for a TTL"
);
}
#[test]
fn a_name_is_described_by_the_fields_that_arbitrate() {
let name = crate::io::name::Name::new(381, 0x1234)
.device_function(140)
.device_class(60)
.to_u64();
assert_eq!(
describe_name(name),
format!(
"{name:#018x}: manufacturer 381, class 60, function 140, instance 0, \
system instance 0, industry 4, unique 0x1234"
)
);
}
#[test]
fn a_description_shows_the_system_instance() {
let base = crate::io::name::Name::new(999, 0x1234).device_class(25);
let a = describe_name(base.to_u64());
let b = describe_name(base.system_instance(15).to_u64());
assert_ne!(a, b);
assert!(b.contains("system instance 15"), "{b}");
}
#[test]
fn loses_conflict_and_fails_when_not_arbitrary() {
let mut c = AddressClaim::new(HIGH, 42, false);
c.start(0);
c.tick(SCAN_TIMEOUT_MS);
let out = c.on_address_claim(SCAN_TIMEOUT_MS, 42, LOW);
assert_eq!(claim_frames(&out), vec![(ADDR_NULL, ADDR_GLOBAL)]);
assert_eq!(c.state(), ClaimState::Failed);
assert_eq!(c.address(), None);
}
#[test]
fn responds_to_claim_request_only_once_owned_or_pending() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
assert!(
c.respond_to_claim_request().is_none(),
"scanning: nothing to answer with"
);
c.tick(SCAN_TIMEOUT_MS); assert!(c.respond_to_claim_request().is_some());
c.tick(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS); let f = c.respond_to_claim_request().unwrap();
assert_eq!(f.pgn, PGN_ISO_ADDRESS_CLAIM);
assert_eq!(f.src, 42);
}
#[test]
fn ignores_its_own_claim_echoed_back() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS); c.tick(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS); assert_eq!(c.address(), Some(42));
let out = c.on_address_claim(5000, 42, LOW);
assert!(out.is_empty());
assert_eq!(c.address(), Some(42), "still owns 42, did not move");
}
#[test]
fn disabled_never_claims() {
let mut c = AddressClaim::disabled(LOW);
assert!(c.start(0).is_empty());
assert!(c.on_address_claim(0, 42, HIGH).is_empty());
assert!(c.tick(10_000).is_empty());
assert_eq!(c.state(), ClaimState::Disabled);
assert_eq!(c.address(), None);
}
#[test]
fn no_send_address_until_claimed() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
assert_eq!(c.send_address(), None, "scanning");
c.tick(SCAN_TIMEOUT_MS);
assert_eq!(c.send_address(), None, "first claim settling");
c.tick(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS);
assert_eq!(c.send_address(), Some(42));
}
#[test]
fn keeps_sending_while_defending_an_owned_address() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS);
let now = SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS;
c.tick(now);
c.on_address_claim(now, 42, HIGH);
assert_eq!(c.state(), ClaimState::Pending);
assert_eq!(c.address(), None);
assert_eq!(c.send_address(), Some(42));
c.on_address_claim(now + 10, 42, HIGH);
assert_eq!(c.send_address(), Some(42));
c.tick(now + 10 + CLAIM_TIMEOUT_MS);
assert_eq!(c.address(), Some(42));
assert_eq!(c.send_address(), Some(42));
}
#[test]
fn winning_before_the_claim_settles_is_not_owning() {
let mut c = AddressClaim::new(LOW, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS);
c.on_address_claim(SCAN_TIMEOUT_MS, 42, HIGH);
assert_eq!(c.send_address(), None);
}
#[test]
fn losing_an_owned_address_stops_sending() {
let mut c = AddressClaim::new(HIGH, 42, true);
c.start(0);
c.tick(SCAN_TIMEOUT_MS);
let now = SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS;
c.tick(now);
c.on_address_claim(now, 42, u64::MAX); assert_eq!(c.send_address(), Some(42));
c.on_address_claim(now + 10, 42, LOW); assert_eq!(c.send_address(), None);
c.tick(now + 10 + CLAIM_TIMEOUT_MS);
assert_eq!(c.send_address(), Some(43));
}
#[test]
fn no_send_address_once_claiming_failed() {
let mut c = AddressClaim::new(HIGH, 42, false);
c.start(0);
c.tick(SCAN_TIMEOUT_MS);
c.tick(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS);
c.on_address_claim(SCAN_TIMEOUT_MS + CLAIM_TIMEOUT_MS, 42, LOW);
assert_eq!(c.state(), ClaimState::Failed);
assert_eq!(c.send_address(), None);
}
}