use heapless::Vec;
use panweave_types::{CryptoRng, ExtendedAddress, PanId, ShortAddress};
use crate::advertisement::Advertisement;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct BleAddress(pub [u8; 6]);
pub const KNOWN_ZDDS: usize = 4;
pub fn random_eui64<R: CryptoRng>(rng: &mut R) -> ExtendedAddress {
let mut bytes = [0u8; 8];
rng.fill_bytes(&mut bytes);
bytes[0] = (bytes[0] | 0x02) & !0x01;
ExtendedAddress(u64::from_be_bytes(bytes))
}
pub fn random_short_address<R: CryptoRng>(
rng: &mut R,
mut in_use: impl FnMut(ShortAddress) -> bool,
) -> Option<ShortAddress> {
for _ in 0..64 {
let mut b = [0u8; 2];
rng.fill_bytes(&mut b);
let candidate = ShortAddress(u16::from_le_bytes(b));
if candidate.is_assignable() && !in_use(candidate) {
return Some(candidate);
}
}
None
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum State {
Idle,
Discovering {
attempt: u8,
},
Connecting {
zdd: BleAddress,
},
EstablishingSession {
zdd: BleAddress,
},
Joining {
zdd: BleAddress,
},
Operating {
zdd: BleAddress,
},
Failed,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum JoinRequest {
InitialJoin,
SecureRejoin,
TrustCenterRejoin,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Action {
Scan,
Connect(BleAddress),
EstablishSession {
zdd: BleAddress,
},
SendCommissioningRequest(JoinRequest),
Disconnect(BleAddress),
GiveUp,
}
#[derive(Clone, Debug)]
pub struct TunnelClient {
pan_id: Option<PanId>,
short: ShortAddress,
known: Vec<BleAddress, KNOWN_ZDDS>,
rejected: Vec<BleAddress, KNOWN_ZDDS>,
max_attempts: u8,
provisioned: Option<u8>,
zdd_key_sequence: Option<u8>,
state: State,
pending: Vec<Action, 4>,
}
impl TunnelClient {
pub fn new(pan_id: Option<PanId>, short: ShortAddress, max_attempts: u8) -> Self {
TunnelClient {
pan_id,
short,
known: Vec::new(),
rejected: Vec::new(),
max_attempts: max_attempts.max(2),
provisioned: None,
zdd_key_sequence: None,
state: State::Idle,
pending: Vec::new(),
}
}
pub const fn state(&self) -> State {
self.state
}
pub const fn short_address(&self) -> ShortAddress {
self.short
}
pub fn set_basic_key_sequence(&mut self, sequence: u8) {
self.provisioned = Some(sequence);
}
pub const fn basic_key_sequence(&self) -> Option<u8> {
self.provisioned
}
pub fn next_action(&mut self) -> Option<Action> {
if self.pending.is_empty() {
None
} else {
Some(self.pending.remove(0))
}
}
fn push(&mut self, a: Action) {
let _ = self.pending.push(a);
}
pub fn start(&mut self) {
self.rejected.clear();
self.discover(1);
}
fn discover(&mut self, attempt: u8) {
if attempt > self.max_attempts {
self.state = State::Failed;
self.push(Action::GiveUp);
return;
}
self.state = State::Discovering { attempt };
if let Some(zdd) = self
.known
.iter()
.rev()
.copied()
.find(|z| !self.rejected.contains(z))
{
self.connect(zdd);
} else {
self.push(Action::Scan);
}
}
fn connect(&mut self, zdd: BleAddress) {
self.state = State::Connecting { zdd };
self.push(Action::Connect(zdd));
}
pub fn on_advertisement(&mut self, zdd: BleAddress, ad: &Advertisement) -> bool {
let State::Discovering { .. } = self.state else {
return false;
};
if !ad.tunnel_service || !ad.joined() || self.rejected.contains(&zdd) {
return false;
}
if self.pan_id.is_some_and(|p| p != ad.pan_id) {
return false;
}
if self.pan_id.is_none() {
self.pan_id = Some(ad.pan_id);
}
self.connect(zdd);
true
}
pub fn on_scan_finished(&mut self) {
if let State::Discovering { attempt } = self.state {
self.discover(attempt.saturating_add(1));
}
}
pub fn on_connected(&mut self) {
if let State::Connecting { zdd } = self.state {
self.state = State::EstablishingSession { zdd };
self.push(Action::EstablishSession { zdd });
}
}
pub fn on_connect_failed(&mut self) {
if let State::Connecting { zdd } = self.state {
let _ = self.rejected.push(zdd);
self.discover(1);
}
}
pub fn on_session_established(&mut self, zdd_key_sequence: Option<u8>) {
let State::EstablishingSession { zdd } = self.state else {
return;
};
self.zdd_key_sequence = zdd_key_sequence;
if !self.known.contains(&zdd) {
if self.known.is_full() {
self.known.remove(0);
}
let _ = self.known.push(zdd);
}
let request = match self.provisioned {
None => JoinRequest::InitialJoin,
Some(mine) => {
if zdd_key_sequence.is_some_and(|s| s != mine) {
JoinRequest::TrustCenterRejoin
} else {
JoinRequest::SecureRejoin
}
}
};
self.state = State::Joining { zdd };
self.push(Action::SendCommissioningRequest(request));
}
pub fn on_session_failed(&mut self) {
if let State::EstablishingSession { zdd } = self.state {
let _ = self.rejected.push(zdd);
self.push(Action::Disconnect(zdd));
self.discover(1);
}
}
pub fn on_commissioning_response(&mut self, success: bool, assigned: ShortAddress) {
let State::Joining { zdd } = self.state else {
return;
};
if success {
self.short = assigned;
self.state = State::Operating { zdd };
} else if assigned != self.short && assigned.is_assignable() {
self.short = assigned;
let request = match self.provisioned {
None => JoinRequest::InitialJoin,
Some(mine) if self.zdd_key_sequence.is_some_and(|s| s != mine) => {
JoinRequest::TrustCenterRejoin
}
Some(_) => JoinRequest::SecureRejoin,
};
self.push(Action::SendCommissioningRequest(request));
} else {
let _ = self.rejected.push(zdd);
self.push(Action::Disconnect(zdd));
self.discover(1);
}
}
pub fn on_disconnected(&mut self) {
match self.state {
State::Idle | State::Failed | State::Discovering { .. } => {}
_ => {
self.rejected.clear();
self.discover(1);
}
}
}
pub fn stop(&mut self) {
self.state = State::Idle;
self.pending.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Rng(panweave_types::rng::DeterministicRng);
impl panweave_types::rng::Rng for Rng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
}
impl CryptoRng for Rng {}
const A: BleAddress = BleAddress([1, 2, 3, 4, 5, 6]);
const B: BleAddress = BleAddress([9, 9, 9, 9, 9, 9]);
fn ad(pan: u16) -> Advertisement {
Advertisement {
tunnel_service: true,
permit_join: true,
pan_id: PanId(pan),
nwk_address: ShortAddress(0x1234),
}
}
#[test]
fn allocated_identifiers_follow_the_rules() {
let mut rng = Rng(panweave_types::rng::DeterministicRng::seed(7));
for _ in 0..32 {
let e = random_eui64(&mut rng);
let first = e.0.to_be_bytes()[0];
assert_eq!(first & 0x03, 0x02, "locally administered, unicast");
}
let taken = ShortAddress(0x1234);
for _ in 0..32 {
let s = random_short_address(&mut rng, |c| c == taken).unwrap();
assert!(s.is_assignable() && s != taken);
}
}
#[test]
fn the_client_joins_reconnects_and_rejoins() {
let mut c = TunnelClient::new(None, ShortAddress(0x4E21), 3);
c.start();
assert_eq!(c.next_action(), Some(Action::Scan));
let mut no_ts = ad(0x1A62);
no_ts.tunnel_service = false;
assert!(!c.on_advertisement(A, &no_ts));
assert!(c.on_advertisement(A, &ad(0x1A62)));
assert_eq!(c.next_action(), Some(Action::Connect(A)));
c.on_connected();
assert_eq!(c.next_action(), Some(Action::EstablishSession { zdd: A }));
c.on_session_established(Some(0));
assert_eq!(
c.next_action(),
Some(Action::SendCommissioningRequest(JoinRequest::InitialJoin))
);
c.on_commissioning_response(false, ShortAddress(0x4E22));
assert_eq!(c.short_address(), ShortAddress(0x4E22));
assert_eq!(
c.next_action(),
Some(Action::SendCommissioningRequest(JoinRequest::InitialJoin))
);
c.on_commissioning_response(true, ShortAddress(0x4E22));
assert_eq!(c.state(), State::Operating { zdd: A });
c.set_basic_key_sequence(0);
c.on_disconnected();
assert_eq!(c.next_action(), Some(Action::Connect(A)));
c.on_connected();
assert_eq!(c.next_action(), Some(Action::EstablishSession { zdd: A }));
c.on_session_established(Some(0));
assert_eq!(
c.next_action(),
Some(Action::SendCommissioningRequest(JoinRequest::SecureRejoin))
);
c.on_commissioning_response(true, ShortAddress(0x4E22));
c.on_disconnected();
assert_eq!(c.next_action(), Some(Action::Connect(A)));
c.on_connected();
assert_eq!(c.next_action(), Some(Action::EstablishSession { zdd: A }));
c.on_session_failed();
assert_eq!(c.next_action(), Some(Action::Disconnect(A)));
assert_eq!(c.next_action(), Some(Action::Scan));
assert!(!c.on_advertisement(B, &ad(0x2222)), "another network");
assert!(!c.on_advertisement(A, &ad(0x1A62)), "rejected this round");
assert!(c.on_advertisement(B, &ad(0x1A62)));
assert_eq!(c.next_action(), Some(Action::Connect(B)));
c.on_connected();
let _ = c.next_action();
c.on_session_established(Some(1));
assert_eq!(
c.next_action(),
Some(Action::SendCommissioningRequest(
JoinRequest::TrustCenterRejoin
))
);
c.on_commissioning_response(true, ShortAddress(0x4E22));
assert_eq!(c.state(), State::Operating { zdd: B });
}
#[test]
fn discovery_is_retried_at_least_once_then_given_up() {
let mut c = TunnelClient::new(Some(PanId(0x1A62)), ShortAddress(0x0001), 0);
c.start();
assert_eq!(c.next_action(), Some(Action::Scan));
c.on_scan_finished();
assert_eq!(c.state(), State::Discovering { attempt: 2 });
assert_eq!(c.next_action(), Some(Action::Scan));
c.on_scan_finished();
assert_eq!(c.state(), State::Failed);
assert_eq!(c.next_action(), Some(Action::GiveUp));
assert_eq!(c.next_action(), None);
}
}