use alloc::string::String;
use alloc::vec::Vec;
use core::cell::RefCell;
use embassy_futures::select::{select, Either};
use embassy_time::{Duration, Timer};
use zbus::Connection;
use crate::dm::clusters::net_comm::{
NetCtl, NetCtlError, NetworkScanInfo, NetworkType, WirelessCreds,
};
use crate::dm::clusters::thread_diag::{
MacCounters as DiagMacCounters, NeighborTable, RoutingRoleEnum, ThreadDiag,
};
use crate::dm::clusters::wifi_diag::WirelessDiag;
use crate::dm::networks::NetChangeNotif;
use crate::error::Error;
use crate::utils::sync::{blocking, DynBase};
use crate::utils::zbus_proxies::openthread::border_router::{
BorderRouterProxy, LeaderData, MacCounters, NeighborEntry,
};
extern crate alloc;
pub struct OtbrCtl<'a> {
connection: &'a Connection,
state: blocking::Mutex<RefCell<OtbrState>>,
}
impl<'a> OtbrCtl<'a> {
const CONNECT_TIMEOUT_SECS: u64 = 30;
pub const fn new(connection: &'a Connection) -> Self {
Self {
connection,
state: blocking::Mutex::new(RefCell::new(OtbrState::new())),
}
}
pub const fn connection(&self) -> &Connection {
self.connection
}
async fn proxy(&self) -> Result<BorderRouterProxy<'a>, zbus::Error> {
BorderRouterProxy::builder(self.connection)
.cache_properties(zbus::proxy::CacheProperties::No)
.build()
.await
}
fn role_from_str(role: &str) -> RoutingRoleEnum {
match role {
"child" => RoutingRoleEnum::EndDevice,
"router" => RoutingRoleEnum::Router,
"leader" => RoutingRoleEnum::Leader,
"detached" => RoutingRoleEnum::Unassigned,
_ => RoutingRoleEnum::Unspecified,
}
}
fn role_attached(role: RoutingRoleEnum) -> bool {
matches!(
role,
RoutingRoleEnum::EndDevice
| RoutingRoleEnum::SleepyEndDevice
| RoutingRoleEnum::REED
| RoutingRoleEnum::Router
| RoutingRoleEnum::Leader
)
}
async fn refresh(&self) -> Result<(bool, bool), zbus::Error> {
let proxy = self.proxy().await?;
let role = Self::role_from_str(&proxy.device_role().await?);
let state = OtbrState {
role,
channel: proxy.channel().await.ok(),
network_name: proxy.network_name().await.ok(),
pan_id: proxy.pan_id().await.ok(),
ext_pan_id: proxy.ext_pan_id().await.ok(),
ext_address: proxy.extended_address().await.ok(),
rloc16: proxy.rloc16().await.ok(),
leader_data: proxy.leader_data().await.ok(),
mac_counters: proxy
.mac_counters()
.await
.inspect_err(|e| debug!("Fetching the MAC counters failed: {:?}", e))
.ok(),
neighbors: proxy
.neighbor_table()
.await
.map(|entries| entries.iter().map(neighbor_table_entry).collect())
.unwrap_or_default(),
};
Ok(self.state.lock(|cached| {
let mut cached = cached.borrow_mut();
let changed = cached.role != state.role;
*cached = state;
(changed, Self::role_attached(role))
}))
}
}
impl NetCtl for OtbrCtl<'_> {
fn net_type(&self) -> NetworkType {
NetworkType::Thread
}
async fn scan<F>(&self, network: Option<&[u8]>, mut f: F) -> Result<(), NetCtlError>
where
F: FnMut(&NetworkScanInfo) -> Result<(), Error>,
{
let proxy = self.proxy().await.map_err(Error::from)?;
let results = proxy.scan().await.map_err(Error::from)?;
for result in &results {
if let Some(network) = network {
if network != result.ext_panid.to_be_bytes() {
continue;
}
}
f(&NetworkScanInfo::Thread {
pan_id: result.panid,
ext_pan_id: result.ext_panid,
network_name: &result.network_name,
channel: result.channel as u16,
version: result.version,
ext_addr: &result.ext_address.to_be_bytes(),
rssi: result.rssi.clamp(i8::MIN as i16, i8::MAX as i16) as i8,
lqi: result.lqi,
})?;
}
Ok(())
}
async fn connect(&self, creds: &WirelessCreds<'_>) -> Result<(), NetCtlError> {
let WirelessCreds::Thread { dataset_tlv } = creds else {
return Err(NetCtlError::Other(
crate::error::ErrorCode::InvalidAction.into(),
));
};
let proxy = self.proxy().await.map_err(Error::from)?;
let same = proxy
.active_dataset_tlvs()
.await
.map(|active| active == *dataset_tlv)
.unwrap_or(false);
if !same {
proxy.factory_reset().await.map_err(Error::from)?;
}
let connect = proxy.attach_all_nodes_to(dataset_tlv);
let timeout = Timer::after(Duration::from_secs(Self::CONNECT_TIMEOUT_SECS));
match select(connect, timeout).await {
Either::First(result) => {
result.map_err(Error::from)?;
}
Either::Second(_) => {
error!(
"Attaching to the Thread network timed out after {}s",
Self::CONNECT_TIMEOUT_SECS
);
return Err(NetCtlError::OtherConnectionFailure);
}
}
let _ = self.refresh().await;
info!("Attached to Thread network");
Ok(())
}
}
impl DynBase for OtbrCtl<'_> {}
impl WirelessDiag for OtbrCtl<'_> {
fn connected(&self) -> Result<bool, Error> {
Ok(self
.state
.lock(|state| Self::role_attached(state.borrow().role)))
}
}
impl ThreadDiag for OtbrCtl<'_> {
fn channel(&self) -> Result<Option<u16>, Error> {
Ok(self.state.lock(|state| state.borrow().channel))
}
fn routing_role(&self) -> Result<Option<RoutingRoleEnum>, Error> {
Ok(Some(self.state.lock(|state| state.borrow().role)))
}
fn network_name(
&self,
f: &mut dyn FnMut(Option<&str>) -> Result<(), Error>,
) -> Result<(), Error> {
self.state
.lock(|state| f(state.borrow().network_name.as_deref()))
}
fn pan_id(&self) -> Result<Option<u16>, Error> {
Ok(self.state.lock(|state| state.borrow().pan_id))
}
fn extended_pan_id(&self) -> Result<Option<u64>, Error> {
Ok(self.state.lock(|state| state.borrow().ext_pan_id))
}
fn ext_address(&self) -> Result<Option<u64>, Error> {
Ok(self.state.lock(|state| state.borrow().ext_address))
}
fn rloc_16(&self) -> Result<Option<u16>, Error> {
Ok(self.state.lock(|state| state.borrow().rloc16))
}
fn partition_id(&self) -> Result<Option<u32>, Error> {
Ok(self
.state
.lock(|state| state.borrow().leader_data.map(|data| data.partition_id)))
}
fn weighting(&self) -> Result<Option<u16>, Error> {
Ok(self
.state
.lock(|state| state.borrow().leader_data.map(|data| data.weighting as u16)))
}
fn data_version(&self) -> Result<Option<u16>, Error> {
Ok(self.state.lock(|state| {
state
.borrow()
.leader_data
.map(|data| data.data_version as u16)
}))
}
fn stable_data_version(&self) -> Result<Option<u16>, Error> {
Ok(self.state.lock(|state| {
state
.borrow()
.leader_data
.map(|data| data.stable_data_version as u16)
}))
}
fn leader_router_id(&self) -> Result<Option<u8>, Error> {
Ok(self
.state
.lock(|state| state.borrow().leader_data.map(|data| data.leader_router_id)))
}
fn mac_counters(
&self,
f: &mut dyn FnMut(Option<&DiagMacCounters>) -> Result<(), Error>,
) -> Result<(), Error> {
self.state
.lock(|state| match state.borrow().mac_counters.as_ref() {
Some(counters) => f(Some(&diag_mac_counters(counters))),
None => f(None),
})
}
fn neighbor_table(
&self,
f: &mut dyn FnMut(&NeighborTable) -> Result<(), Error>,
) -> Result<(), Error> {
self.state.lock(|state| {
for entry in &state.borrow().neighbors {
f(entry)?;
}
Ok(())
})
}
}
impl NetChangeNotif for OtbrCtl<'_> {
async fn wait_changed(&self) {
const POLL_PERIOD_SECS: u64 = 5;
loop {
Timer::after(Duration::from_secs(POLL_PERIOD_SECS)).await;
match self.refresh().await {
Ok((true, _)) => break,
Ok((false, _)) => {}
Err(e) => error!("Failed to refresh device role: {:?}", e),
}
}
}
}
#[derive(Debug, Clone)]
struct OtbrState {
role: RoutingRoleEnum,
channel: Option<u16>,
network_name: Option<String>,
pan_id: Option<u16>,
ext_pan_id: Option<u64>,
ext_address: Option<u64>,
rloc16: Option<u16>,
leader_data: Option<LeaderData>,
mac_counters: Option<MacCounters>,
neighbors: Vec<NeighborTable>,
}
impl OtbrState {
const fn new() -> Self {
Self {
role: RoutingRoleEnum::Unspecified,
channel: None,
network_name: None,
pan_id: None,
ext_pan_id: None,
ext_address: None,
rloc16: None,
leader_data: None,
mac_counters: None,
neighbors: Vec::new(),
}
}
}
fn diag_mac_counters(counters: &MacCounters) -> DiagMacCounters {
DiagMacCounters {
tx_total_count: counters.tx_total,
tx_unicast_count: counters.tx_unicast,
tx_broadcast_count: counters.tx_broadcast,
tx_ack_requested_count: counters.tx_ack_requested,
tx_acked_count: counters.tx_acked,
tx_no_ack_requested_count: counters.tx_no_ack_requested,
tx_data_count: counters.tx_data,
tx_data_poll_count: counters.tx_data_poll,
tx_beacon_count: counters.tx_beacon,
tx_beacon_request_count: counters.tx_beacon_request,
tx_other_count: counters.tx_other,
tx_retry_count: counters.tx_retry,
tx_direct_max_retry_expiry_count: None,
tx_indirect_max_retry_expiry_count: None,
tx_err_cca_count: counters.tx_err_cca,
tx_err_abort_count: counters.tx_err_abort,
tx_err_busy_channel_count: counters.tx_busy_channel,
rx_total_count: counters.rx_total,
rx_unicast_count: counters.rx_unicast,
rx_broadcast_count: counters.rx_broadcast,
rx_data_count: counters.rx_data,
rx_data_poll_count: counters.rx_data_poll,
rx_beacon_count: counters.rx_beacon,
rx_beacon_request_count: counters.rx_beacon_request,
rx_other_count: counters.rx_other,
rx_address_filtered_count: counters.rx_address_filtered,
rx_dest_addr_filtered_count: counters.rx_dest_address_filtered,
rx_duplicated_count: counters.rx_duplicated,
rx_err_no_frame_count: counters.rx_err_no_frame,
rx_err_unknown_neighbor_count: counters.rx_err_unknown_neighbor,
rx_err_invalid_src_addr_count: counters.rx_err_invalid_src_addr,
rx_err_sec_count: counters.rx_err_sec,
rx_err_fcs_count: counters.rx_err_fcs,
rx_err_other_count: counters.rx_err_other,
}
}
fn neighbor_table_entry(entry: &NeighborEntry) -> NeighborTable {
fn error_rate_percent(rate: u16) -> u8 {
(rate as u32 * 100 / u16::MAX as u32) as u8
}
fn rssi(raw: u8) -> Option<i8> {
let rssi = raw as i8;
(rssi != i8::MAX).then_some(rssi)
}
NeighborTable {
ext_address: entry.ext_address,
age: entry.age,
rloc16: entry.rloc16,
link_frame_counter: entry.link_frame_counter,
mle_frame_counter: entry.mle_frame_counter,
lqi: entry.link_quality_in,
average_rssi: rssi(entry.average_rssi),
last_rssi: rssi(entry.last_rssi),
frame_error_rate: error_rate_percent(entry.frame_error_rate),
message_error_rate: error_rate_percent(entry.message_error_rate),
rx_on_when_idle: entry.rx_on_when_idle,
full_thread_device: entry.full_thread_device,
full_network_data: entry.full_network_data,
is_child: entry.is_child,
}
}