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;
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,
used: [bool; 256],
}
impl AddressClaim {
pub fn new(name: u64, preferred: u8, arbitrary: bool) -> Self {
Self {
name,
address: preferred,
preferred,
state: ClaimState::Pending,
deadline: 0,
arbitrary,
used: [false; 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 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.used[src as usize] = true;
return Vec::new();
}
if self.name < their_name {
self.state = ClaimState::Pending;
self.deadline = now + CLAIM_TIMEOUT_MS;
return vec![self.claim_frame()];
}
self.used[src as usize] = true;
if self.arbitrary
&& let Some(next) = self.pick_free()
{
self.address = next;
self.state = ClaimState::Pending;
self.deadline = now + CLAIM_TIMEOUT_MS;
return vec![self.claim_frame()];
}
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;
Vec::new()
}
_ => Vec::new(),
}
}
fn begin_claim(&mut self, now: u64) -> Vec<RawFrame> {
if self.used[self.preferred as usize] {
match self.pick_free() {
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.deadline = now + CLAIM_TIMEOUT_MS;
vec![self.claim_frame()]
}
fn pick_free(&self) -> Option<u8> {
(0..=ADDR_MAX).find(|a| !self.used[*a as usize])
}
fn claim_frame(&self) -> RawFrame {
RawFrame::new(
None,
6,
PGN_ISO_ADDRESS_CLAIM,
self.address,
ADDR_GLOBAL,
self.name.to_le_bytes(),
)
}
}
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![(0, 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);
let claims = claim_frames(&out);
assert_eq!(claims.len(), 1);
assert_ne!(claims[0].0, 42, "moved off the contested address");
assert_eq!(c.state(), ClaimState::Pending);
}
#[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);
}
}