Skip to content

Commit 043a205

Browse files
authored
Report Thread 1.4 as the supported protocol (esp-rs#103)
* Report Thread 1.4 as the supported protocol * Address code review feedback
1 parent 0e9229e commit 043a205

11 files changed

Lines changed: 334 additions & 5 deletions

File tree

examples/std/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,8 @@ required-features = ["ftd"]
6363
path = "./src/bin/joiner.rs"
6464
name = "joiner"
6565
required-features = ["joiner"]
66+
67+
[[bin]]
68+
path = "./src/bin/become_router.rs"
69+
name = "become_router"
70+
required-features = ["ftd"]
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Host example driving a remote OpenThread RCP over serial that attaches as a
2+
//! child and then **forces a router upgrade** via [`OpenThread::become_router`],
3+
//! instead of waiting for OpenThread's jittered automatic upgrade.
4+
//!
5+
//! Its purpose is to deterministically exercise the child-to-router role
6+
//! transition — which is when OpenThread calls `otPlatRadioSetAlternateShortAddress`
7+
//! with the node's old (child) RLOC16 (the alternate short address), then clears
8+
//! it ~8 s later. Run with `RUST_LOG=info` and watch for the
9+
//! `Plat radio set alternate short address callback` line.
10+
//!
11+
//! Needs the `ftd` feature (only a Full Thread Device can become a router):
12+
//! `cargo run --features ftd --bin become_router`
13+
//!
14+
//! Set the serial device with `RCP_SERIAL` (default `/dev/ttyACM0`), the baud
15+
//! rate with `RCP_BAUD` (default 115200) and, optionally, `THREAD_DATASET`.
16+
17+
use embassy_executor::{Executor, Spawner};
18+
19+
use log::{info, warn};
20+
21+
use openthread::spinel::{
22+
SerialPort, SpinelRadio, SpinelRadioResources, UartSpinelTransport, UartTransportResources,
23+
};
24+
use openthread::{DeviceRole, OpenThread, OtResources, SimpleRamSettings};
25+
26+
use rand::rngs::StdRng;
27+
use rand::{RngCore, SeedableRng};
28+
29+
use static_cell::{ConstStaticCell, StaticCell};
30+
31+
// Linked for its `utoa`/`strtoul` C symbols, which OpenThread's C references.
32+
use tinyrlibc as _;
33+
34+
const DEFAULT_SERIAL: &str = "/dev/ttyACM0";
35+
const DEFAULT_BAUD: u32 = 115_200;
36+
37+
const THREAD_DATASET: &str = match option_env!("THREAD_DATASET") {
38+
Some(dataset) => dataset,
39+
None => "000300001901020fd80208b566147d38e384200e080000639c5d67a3bd0510c490f58d4be0d5eaeb0f09b395d1ae17030d4e4553542d50414e2d304644380708fd7d4f8232cb00000410a7e08419ae47c177fb91bcfcec789aa50c0402a0f77835060004001fffe0",
40+
};
41+
42+
static EXECUTOR: StaticCell<Executor> = StaticCell::new();
43+
44+
fn main() {
45+
env_logger::builder()
46+
.filter_level(log::LevelFilter::Info)
47+
.parse_default_env()
48+
.init();
49+
50+
let executor = EXECUTOR.init(Executor::new());
51+
executor.run(|spawner| spawner.spawn(main_task(spawner).unwrap()));
52+
}
53+
54+
#[embassy_executor::task]
55+
async fn main_task(spawner: Spawner) {
56+
let serial_path = std::env::var("RCP_SERIAL").unwrap_or_else(|_| DEFAULT_SERIAL.into());
57+
let baud = std::env::var("RCP_BAUD")
58+
.ok()
59+
.and_then(|v| v.parse().ok())
60+
.unwrap_or(DEFAULT_BAUD);
61+
62+
info!("Starting; opening RCP serial {serial_path} @ {baud} baud");
63+
64+
static RNG: StaticCell<StdRng> = StaticCell::new();
65+
let rng = RNG.init(StdRng::from_os_rng());
66+
67+
let mut ieee_eui64 = [0u8; 8];
68+
rng.fill_bytes(&mut ieee_eui64);
69+
70+
static OT_RESOURCES: StaticCell<OtResources> = StaticCell::new();
71+
static OT_SETTINGS_BUF: StaticCell<[u8; 1024]> = StaticCell::new();
72+
static OT_SETTINGS: StaticCell<SimpleRamSettings> = StaticCell::new();
73+
74+
let ot_resources = OT_RESOURCES.init(OtResources::new());
75+
let ot_settings_buf = OT_SETTINGS_BUF.init([0; 1024]);
76+
let ot_settings = OT_SETTINGS.init(SimpleRamSettings::new(ot_settings_buf));
77+
78+
let ot = OpenThread::new(ieee_eui64, rng, ot_settings, ot_resources).unwrap();
79+
80+
static RADIO_RESOURCES: ConstStaticCell<SpinelRadioResources> =
81+
ConstStaticCell::new(SpinelRadioResources::new());
82+
static UART_RESOURCES: ConstStaticCell<UartTransportResources> =
83+
ConstStaticCell::new(UartTransportResources::new());
84+
85+
let serial = SerialPort::open(&serial_path, baud).expect("open RCP serial");
86+
let radio = SpinelRadio::new(
87+
UartSpinelTransport::new(serial, UART_RESOURCES.take()),
88+
RADIO_RESOURCES.take(),
89+
);
90+
91+
spawner.spawn(run_ot(ot.clone(), radio).unwrap());
92+
93+
info!("Dataset: {THREAD_DATASET}");
94+
95+
ot.set_active_dataset_tlv_hexstr(THREAD_DATASET).unwrap();
96+
ot.enable_ipv6(true).unwrap();
97+
ot.enable_thread(true).unwrap();
98+
99+
// Wait until we have attached as a child.
100+
loop {
101+
let role = ot.device_role();
102+
info!("Role: {role:?} (eligible: {})", ot.router_eligible());
103+
if matches!(
104+
role,
105+
DeviceRole::Child | DeviceRole::Router | DeviceRole::Leader
106+
) {
107+
break;
108+
}
109+
ot.wait_changed().await;
110+
}
111+
112+
// Force the router upgrade. If we are already a router/leader this is a
113+
// harmless no-op error; if we are a child, it kicks off the child->router
114+
// transition (which drives `otPlatRadioSetAlternateShortAddress`).
115+
if matches!(ot.device_role(), DeviceRole::Child) {
116+
info!("Child attached — forcing router upgrade via become_router()...");
117+
match ot.become_router() {
118+
Ok(()) => info!("become_router(): Address Solicit sent"),
119+
Err(e) => warn!("become_router() failed: {e:?}"),
120+
}
121+
}
122+
123+
// Report every subsequent role change.
124+
loop {
125+
ot.wait_changed().await;
126+
info!("Role now: {:?}", ot.device_role());
127+
}
128+
}
129+
130+
#[embassy_executor::task]
131+
async fn run_ot(
132+
ot: OpenThread<'static>,
133+
radio: SpinelRadio<'static, UartSpinelTransport<'static, SerialPort>>,
134+
) -> ! {
135+
ot.run(radio).await
136+
}

openthread-sys/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9-
* The default MbedTLS backend is now OpenThread's own **bundled** MbedTLS, not the external `mbedtls-rs-sys` crate. A default-features build reuses the committed prebuilt libraries and needs **no C toolchain** (clang/cmake/ninja).
9+
* Advertise **Thread 1.4** instead of Thread 1.1 (#103)
10+
* The stack now reports Thread version 1.4 (`OpenThread::thread_version` returns 4; the highest the vendored OpenThread supports). Thread 1.3+ is the floor Matter-over-Thread expects; for a plain node (no Border Router, no TREL) the on-air/radio contract is unchanged past 1.2, so this is effectively a version bump plus a few benign internal behaviors (e.g. a more thorough parent search at attach).
11+
* CSL is deliberately **not** compiled in: both `OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE` (which otherwise defaults on at >= 1.2) and `OT_CSL_RECEIVER` are forced off. This keeps the radio-platform contract identical to 1.1 — no `EnableCsl` / `ReceiveAt` / `GetCslAccuracy` callbacks are referenced — so every existing `Radio` driver keeps working unchanged. Low-power CSL (SSED) remains a future opt-in.
12+
* HW-validated over an RCP: attach, SRP registration, and a `ping-stress` datapath sweep against a live Border Router.
13+
* NRF52: Fix a corruption issue where Clang compiled the C code with `short-enums = false`, while bindgen generated bindings with `short-enums = true` (#102)
14+
* The default MbedTLS backend is now OpenThread's own **bundled** MbedTLS, not the external `mbedtls-rs-sys` crate. A default-features build reuses the committed prebuilt libraries and needs **no C toolchain** (clang/cmake/ninja). (#101)
1015
* To build OpenThread against the external `mbedtls-rs-sys` instead, enable the `mbedtls-rs-sys` feature. Do this when another crate in the graph already provides `mbedtls-rs-sys` (e.g. `rs-matter`), so a single MbedTLS serves both, or when you need the HW accel capabilities of `mbedtls-rs-sys`.
1116
* WARNING: do not combine a default (bundled-MbedTLS) OpenThread with a separate `mbedtls-rs-sys` in the same firmware — that links two MbedTLS copies. If your graph needs `mbedtls-rs-sys`, enable this feature so OpenThread reuses it.
1217
* Remote Radio Support (Spinel-RCP) (#98)

openthread-sys/gen/builder.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,35 @@ impl OpenThreadBuilder {
232232
});
233233
}
234234

235+
// Thread protocol version advertised and spoken by the stack.
236+
//
237+
// 1.4 (the latest; 1.3.1 is an alias) — Matter-over-Thread requires >= 1.3,
238+
// and for a plain node (no Border Router, no TREL) the on-air/radio
239+
// contract does NOT change anywhere past 1.2: no new `otPlatRadio*`
240+
// callback is referenced at 1.3/1.4, and the only deltas are benign
241+
// internal C++ behaviors (a more thorough parent search at attach,
242+
// delay-aware tx-queue management). See `thread-version-and-frame-support`.
243+
//
244+
// The two CSL flags below are the important part of pinning ">=1.2
245+
// WITHOUT the CSL machinery". `OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE`
246+
// otherwise DEFAULTS ON at >= 1.2 (see the vendored `src/core/config/mac.h`)
247+
// and is a SEPARATE axis from the receiver: the transmitter is the
248+
// *parent* side (an FTD scheduling indirect frames to CSL children),
249+
// the receiver is the *child* (SSED) side. We want neither yet:
250+
// - CSL_RECEIVER off -> this node is never a CSL sleepy child.
251+
// - CSL_TRANSMITTER off -> an FTD here never tries to parent CSL
252+
// children, so OT never calls the CSL-parent radio hooks
253+
// (`otPlatRadioGetCslAccuracy`/`GetCslUncertainty`) at runtime.
254+
// Together they keep the radio-platform contract identical to 1.1: no
255+
// `EnableCsl`/`ReceiveAt`/`GetCsl*` callbacks are referenced or invoked,
256+
// so every existing `Radio` driver keeps working unchanged. Enabling CSL
257+
// (low-power SSED) is a deliberate future opt-in that also needs the
258+
// `Radio` trait to grow the CSL/enh-ACK-security surface.
235259
config
236-
.define("OT_THREAD_VERSION", "1.1")
260+
.define("OT_THREAD_VERSION", "1.4")
261+
.cflag("-DOPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE=0")
262+
.cxxflag("-DOPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE=0")
263+
.define("OT_CSL_RECEIVER", "OFF")
237264
.define("OT_LOG_LEVEL", "NOTE")
238265
// Build BOTH device types so the prebuilt cache covers MTD and FTD.
239266
// The actual archives shipped/linked are chosen by the umbrella

openthread/CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9-
* The default MbedTLS backend is now OpenThread's own **bundled** MbedTLS, not the external `mbedtls-rs-sys` crate. A default-features build reuses the committed prebuilt libraries and needs **no C toolchain** (clang/cmake/ninja).
9+
* Advertise **Thread 1.4** instead of Thread 1.1 (#103)
10+
* The stack now reports Thread version 1.4 (`OpenThread::thread_version` returns 4; the highest the vendored OpenThread supports). Thread 1.3+ is the floor Matter-over-Thread expects; for a plain node (no Border Router, no TREL) the on-air/radio contract is unchanged past 1.2, so this is effectively a version bump plus a few benign internal behaviors (e.g. a more thorough parent search at attach).
11+
* CSL is deliberately **not** compiled in: both `OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE` (which otherwise defaults on at >= 1.2) and `OT_CSL_RECEIVER` are forced off. This keeps the radio-platform contract identical to 1.1 — no `EnableCsl` / `ReceiveAt` / `GetCslAccuracy` callbacks are referenced — so every existing `Radio` driver keeps working unchanged. Low-power CSL (SSED) remains a future opt-in.
12+
* HW-validated over an RCP: attach, SRP registration, and a `ping-stress` datapath sweep against a live Border Router.
13+
* Alternate short address support (Thread 1.4 FTD) (#103)
14+
* Implements `otPlatRadioSetAlternateShortAddress`, which OpenThread calls during a child-to-router role transition so the node keeps receiving frames addressed to its previous RLOC16 for a short window (~8s). Surfaced as `Config::alt_short_addr`.
15+
* Honored by the software `MacRadio` filter, so radios with no MAC offload (e.g. `NrfRadio`) accept both the primary and alternate short address for free. `SpinelRadio` programs the RCP's alternate-short-address property, but only when the RCP advertises the `ALT_SHORT_ADDR` capability (stock RCPs without it fall back to the primary only). `EspRadio` does not yet honor it: `esp-radio` exposes no public multi-PAN / second-short-address API, so the alternate is dropped by the hardware filter for now. In all unsupported cases the alternate is a reliability optimization — peers relearn the new RLOC16 within the window and higher layers retransmit.
16+
* New APIs for calling into OpenThread directly (#103)
17+
* `OpenThread::with_instance`: an `unsafe` escape hatch that runs a closure with the raw `*mut otInstance`, inside an active state scope, for calling OpenThread C APIs this crate does not yet wrap.
18+
* `OpenThread::become_router` (`ftd`): request an immediate router upgrade (`otThreadBecomeRouter`) instead of waiting for OpenThread's jittered automatic one. New std example `become_router` demonstrates it (and deterministically exercises the alternate-short-address transition).
19+
* NRF52: Fix a corruption issue where Clang compiled the C code with `short-enums = false`, while bindgen generated bindings with `short-enums = true` (#102)
20+
* The default MbedTLS backend is now OpenThread's own **bundled** MbedTLS, not the external `mbedtls-rs-sys` crate. A default-features build reuses the committed prebuilt libraries and needs **no C toolchain** (clang/cmake/ninja). (#101)
1021
* To build OpenThread against the external `mbedtls-rs-sys` instead, enable the `mbedtls-rs-sys` feature. Do this when another crate in the graph already provides `mbedtls-rs-sys` (e.g. `rs-matter`), so a single MbedTLS serves both, or when you need the HW accel capabilities of `mbedtls-rs-sys`.
1122
* WARNING: do not combine a default (bundled-MbedTLS) OpenThread with a separate `mbedtls-rs-sys` in the same firmware — that links two MbedTLS copies. If your graph needs `mbedtls-rs-sys`, enable this feature so OpenThread reuses it.
1223
* Address the Tier 2 API gaps:

openthread/src/lib.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,24 @@ impl<'a> OpenThread<'a> {
621621
}
622622
}
623623

624+
/// Request that this node become a router (`otThreadBecomeRouter`).
625+
///
626+
/// Sends an Address Solicit to the leader to obtain a router ID, promoting a
627+
/// router-eligible child to the router role without waiting for OpenThread's
628+
/// automatic (jittered) router upgrade. Only meaningful on a Full Thread
629+
/// Device that is currently a child and [`router_eligible`](Self::router_eligible).
630+
///
631+
/// Returns an error if the node is not eligible or not in a state from which
632+
/// it can become a router (e.g. detached, disabled, or already a router or
633+
/// leader) — OpenThread reports `OT_ERROR_INVALID_STATE` / `OT_ERROR_NOT_CAPABLE`.
634+
#[cfg(feature = "ftd")]
635+
pub fn become_router(&self) -> Result<(), OtError> {
636+
let mut ot = self.activate();
637+
let state = ot.state();
638+
639+
ot!(unsafe { sys::otThreadBecomeRouter(state.ot.instance) })
640+
}
641+
624642
/// Return whether this node is router-eligible (`otThreadIsRouterEligible`).
625643
///
626644
/// Only available on a Full Thread Device (`ftd` feature); a Minimal Thread
@@ -1042,6 +1060,36 @@ impl<'a> OpenThread<'a> {
10421060
f(None)
10431061
}
10441062

1063+
/// Run a closure with direct access to the raw `otInstance` pointer.
1064+
///
1065+
/// An escape hatch for calling OpenThread C APIs (`otXxx`) this crate does
1066+
/// not yet wrap. The closure runs inside an *active* scope — the same state
1067+
/// activation every high-level method uses — so the platform callbacks
1068+
/// OpenThread may invoke during the call (alarm, radio, settings, …) are
1069+
/// wired to this instance. Because activation is scoped to the call, the
1070+
/// pointer must NOT escape the closure: it is only valid for the duration of
1071+
/// `f`. The closure's return value is passed back out.
1072+
///
1073+
/// # Safety
1074+
///
1075+
/// The pointer is a live `*mut otInstance`; misusing the C API through it can
1076+
/// violate the invariants the safe wrapper upholds. In particular:
1077+
/// - Do not stash the pointer for later use (it is only valid within `f`).
1078+
/// - Do not re-enter this crate's API from within `f` (e.g. call another
1079+
/// `OpenThread` method), as the state is already active — that would
1080+
/// attempt a re-entrant activation.
1081+
/// - Calling an OpenThread API that expects a different device build (e.g. an
1082+
/// FTD-only API on an MTD image) is undefined, exactly as in C.
1083+
pub fn with_instance<F, R>(&self, f: F) -> R
1084+
where
1085+
F: FnOnce(*mut otInstance) -> R,
1086+
{
1087+
let mut ot = self.activate();
1088+
let state = ot.state();
1089+
1090+
f(state.ot.instance)
1091+
}
1092+
10451093
/// Wait for the OpenThread stack to change its state.
10461094
///
10471095
/// NOTE:
@@ -2629,6 +2677,24 @@ impl<'a> OtContext<'a> {
26292677
}
26302678
}
26312679

2680+
fn plat_radio_set_alternate_short_address(&mut self, address: u16) {
2681+
// OpenThread clears the alternate with `OT_RADIO_INVALID_SHORT_ADDR`
2682+
// (0xfffe); map that to `None` (no alternate). Any other value is the
2683+
// second short address the radio should also accept.
2684+
let alt = (address != crate::sys::OT_RADIO_INVALID_SHORT_ADDR as u16).then_some(address);
2685+
2686+
info!(
2687+
"Plat radio set alternate short address callback, addr: {:?}",
2688+
alt
2689+
);
2690+
2691+
let state = self.state();
2692+
2693+
if state.ot.radio_conf.alt_short_addr != alt {
2694+
state.ot.radio_conf.alt_short_addr = alt;
2695+
}
2696+
}
2697+
26322698
fn plat_radio_set_pan_id(&mut self, pan_id: u16) {
26332699
info!("Plat radio set PAN ID callback, PAN ID: 0x{:02x}", pan_id);
26342700

openthread/src/platform.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ extern "C" fn otPlatRadioSetShortAddress(instance: *const otInstance, address: u
148148
OtContext::callback(instance).plat_radio_set_short_address(address);
149149
}
150150

151+
// Alternate short address (FTD, Thread >= 1.2).
152+
//
153+
// During a child-to-router role transition an FTD is briefly reachable at BOTH
154+
// its old (child) RLOC16 and its new (router) RLOC16. OpenThread hands the radio
155+
// the old address here so frames addressed to it keep being received for a short
156+
// window (`kAlternateRloc16Timeout`, ~8s), after which the stack clears it (calls
157+
// this with `OT_RADIO_INVALID_SHORT_ADDR`). It is invoked unconditionally in the
158+
// FTD `Mac` path (never on MTD, where `--gc-sections` drops it).
159+
#[no_mangle]
160+
extern "C" fn otPlatRadioSetAlternateShortAddress(instance: *const otInstance, address: u16) {
161+
OtContext::callback(instance).plat_radio_set_alternate_short_address(address);
162+
}
163+
151164
#[no_mangle]
152165
extern "C" fn otPlatRadioSetPanId(instance: *const otInstance, pan_id: u16) {
153166
OtContext::callback(instance).plat_radio_set_pan_id(pan_id);

0 commit comments

Comments
 (0)