diff --git a/Cargo.lock b/Cargo.lock index 9ead0189c..cede27761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,6 +950,14 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "device-metadata" +version = "0.1.0" +dependencies = [ + "riot-rs", + "riot-rs-boards", +] + [[package]] name = "diff" version = "0.1.13" @@ -3607,6 +3615,7 @@ dependencies = [ "riot-rs-boards", "riot-rs-debug", "riot-rs-embassy", + "riot-rs-embassy-common", "riot-rs-macros", "riot-rs-random", "riot-rs-rt", @@ -3760,6 +3769,10 @@ dependencies = [ "embassy-executor", "embassy-nrf", "embedded-hal-async", + "nrf52832-pac", + "nrf52833-pac", + "nrf52840-pac", + "nrf5340-app-pac", "paste", "portable-atomic", "riot-rs-debug", diff --git a/examples/device-metadata/Cargo.toml b/examples/device-metadata/Cargo.toml new file mode 100644 index 000000000..941693b19 --- /dev/null +++ b/examples/device-metadata/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "device-metadata" +version = "0.1.0" +authors.workspace = true +license.workspace = true +edition.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +riot-rs = { path = "../../src/riot-rs" } +riot-rs-boards = { path = "../../src/riot-rs-boards" } diff --git a/examples/device-metadata/README.md b/examples/device-metadata/README.md new file mode 100644 index 000000000..46c92e726 --- /dev/null +++ b/examples/device-metadata/README.md @@ -0,0 +1,13 @@ +# device-metadata + +## About + +This application prints all that a device knows +about the device and the running program +at startup to the debug console. + +## How to run + +In this folder, run + + laze build -b nrf52840dk run diff --git a/examples/device-metadata/laze.yml b/examples/device-metadata/laze.yml new file mode 100644 index 000000000..2746a72b7 --- /dev/null +++ b/examples/device-metadata/laze.yml @@ -0,0 +1,7 @@ +apps: + - name: device-metadata + selects: + - ?release + - sw/threading + depends: + - debug-console diff --git a/examples/device-metadata/src/main.rs b/examples/device-metadata/src/main.rs new file mode 100644 index 000000000..69b9b1699 --- /dev/null +++ b/examples/device-metadata/src/main.rs @@ -0,0 +1,18 @@ +#![no_main] +#![no_std] +#![feature(used_with_arg)] + +use riot_rs::debug::{exit, log::*}; + +#[riot_rs::thread(autostart)] +fn main() { + info!("Available information:"); + info!("Board type: {}", riot_rs::buildinfo::BOARD); + if let Ok(id) = riot_rs::identity::device_id_bytes() { + info!("Device ID: {=[u8]:x}", id.as_ref()); + } else { + info!("Device ID is unavailable."); + } + + exit(Ok(())); +} diff --git a/examples/laze.yml b/examples/laze.yml index a69997482..261453b03 100644 --- a/examples/laze.yml +++ b/examples/laze.yml @@ -8,6 +8,7 @@ subdirs: - blinky - core-sizes - coap + - device-metadata - embassy - embassy-http-server - embassy-net-tcp diff --git a/src/riot-rs-embassy-common/src/identity.rs b/src/riot-rs-embassy-common/src/identity.rs new file mode 100644 index 000000000..ae1ca7f26 --- /dev/null +++ b/src/riot-rs-embassy-common/src/identity.rs @@ -0,0 +1,96 @@ +//! Tools and traits for describing device identities. +//! +//! See `riot_rs::identity` for general documentation; that module also represents the public parts +//! of this API. +#![deny(missing_docs)] + +/// Trait describing the unique identifier available on a device. +/// +/// See the module level documentation on the characteristics of the identifier. +/// +/// # Evolution +/// +/// In its current state, this type is mainly a wrapper around a binary identifier. +/// +/// As it is used more, additional methods can be provided for concrete types of identifiers, such +/// as MAC addresses. By default, those would be generated in some way from what is available in +/// the identifier -- but devices where the identifier already *is* a MAC address (or possibly a +/// range thereof) can provide their official addresses. +pub trait DeviceId: Sized { + /// Some `[u8; N]` type, returned by [`.bytes()`][Self::bytes]. + /// + /// This may not represent all the identifying information available on the device, but can + /// represent a unique portion thereof. + /// + /// (For example, if a device has two consecutive MAC addresses assigned, the type as a whole + /// may represent both, but the conventional serialized identity of the device may just be one + /// of them). + /// + /// # Evolution + /// + /// In the long run, it will be preferable to add a `const BYTES_LEN: usize;` and enforce the + /// type `[u8; Self::BYTES_LEN]` as the return value of [`.bytes(_)]`][Self::bytes]. This can + /// not be done yet as it depends on the `generic_const_exprs` feature. + type Bytes: AsRef<[u8]>; + + /// Obtains a unique identifier of the device. + /// + /// # Errors + /// + /// This produces an error if no device ID is available on this board, or is not implemented. + /// It is encouraged to use [`core::convert::Infallible`] where possible. + fn get() -> Result; + + /// The device identifier in serialized bytes format. + fn bytes(&self) -> Self::Bytes; +} + +/// An uninhabited type implementing [`DeviceId`] that always errs. +/// +/// This can be used both on architectures that do not have a unique identifier on their boards, +/// and when it has not yet been implemented. +/// +/// Typical types for `E` are [`NotImplemented`] or [`NotAvailable`]. +#[derive(Debug)] +pub struct NoDeviceId( + core::convert::Infallible, + core::marker::PhantomData, +); + +impl DeviceId for NoDeviceId { + // We could also come up with a custom never type that AsRef's into [u8], but that won't fly + // once there is a BYTES_LEN. + type Bytes = [u8; 0]; + + fn get() -> Result { + Err::<_, E>(Default::default()) + } + + fn bytes(&self) -> [u8; 0] { + match self.0 {} + } +} + +/// Error indicating that a [`DeviceId`] may be available on this device, but is not implemented. +#[derive(Debug, Default)] +pub struct NotImplemented; + +impl core::fmt::Display for NotImplemented { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("Device ID not implemented on this device") + } +} + +impl core::error::Error for NotImplemented {} + +/// Error indicating that a [`DeviceId`] is not available on this device. +#[derive(Debug, Default)] +pub struct NotAvailable; + +impl core::fmt::Display for NotAvailable { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("Device ID not available on this device") + } +} + +impl core::error::Error for NotAvailable {} diff --git a/src/riot-rs-embassy-common/src/lib.rs b/src/riot-rs-embassy-common/src/lib.rs index 41002b5f4..297de5bf0 100644 --- a/src/riot-rs-embassy-common/src/lib.rs +++ b/src/riot-rs-embassy-common/src/lib.rs @@ -13,6 +13,8 @@ pub mod executor_swi; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity; + pub mod reexports { //! Crate re-exports. diff --git a/src/riot-rs-embassy/src/arch/mod.rs b/src/riot-rs-embassy/src/arch/mod.rs index 32f55fa52..6ef48f082 100644 --- a/src/riot-rs-embassy/src/arch/mod.rs +++ b/src/riot-rs-embassy/src/arch/mod.rs @@ -15,6 +15,12 @@ pub mod hwrng; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity { + use riot_rs_embassy_common::identity; + + pub type DeviceId = identity::NoDeviceId; +} + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-esp/src/lib.rs b/src/riot-rs-esp/src/lib.rs index d5ee5cd9f..d2d4ca86c 100644 --- a/src/riot-rs-esp/src/lib.rs +++ b/src/riot-rs-esp/src/lib.rs @@ -9,6 +9,12 @@ pub mod gpio; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity { + use riot_rs_embassy_common::identity; + + pub type DeviceId = identity::NoDeviceId; +} + #[cfg(feature = "wifi")] pub mod wifi; diff --git a/src/riot-rs-nrf/Cargo.toml b/src/riot-rs-nrf/Cargo.toml index 821c0fd3c..3ea664d61 100644 --- a/src/riot-rs-nrf/Cargo.toml +++ b/src/riot-rs-nrf/Cargo.toml @@ -30,16 +30,20 @@ riot-rs-random = { workspace = true, optional = true } [target.'cfg(context = "nrf52832")'.dependencies] embassy-nrf = { workspace = true, features = ["nrf52832"] } +nrf52832-pac = "0.12.2" [target.'cfg(context = "nrf52833")'.dependencies] # Disable NFC support for now, as we do not support it yet. embassy-nrf = { workspace = true, features = ["nfc-pins-as-gpio", "nrf52833"] } +nrf52833-pac = "0.12.2" [target.'cfg(context = "nrf52840")'.dependencies] embassy-nrf = { workspace = true, features = ["nrf52840"] } +nrf52840-pac = "0.12.2" [target.'cfg(context = "nrf5340")'.dependencies] embassy-nrf = { workspace = true, features = ["nrf5340-app-s"] } +nrf5340-app-pac = "0.12.2" [features] ## Enables GPIO interrupt support. diff --git a/src/riot-rs-nrf/src/identity.rs b/src/riot-rs-nrf/src/identity.rs new file mode 100644 index 000000000..ba83b2090 --- /dev/null +++ b/src/riot-rs-nrf/src/identity.rs @@ -0,0 +1,33 @@ +pub struct DeviceId(u64); + +impl riot_rs_embassy_common::identity::DeviceId for DeviceId { + #[allow( + refining_impl_trait_reachable, + reason = "Making this fallible would be a breaking API change for RIOT-rs." + )] + fn get() -> Result { + // Embassy does not wrap the FICR register, and given that all we need from there is a register + // read that is perfectly fine to do through a stolen register, let's do that rather than + // thread the access through several layers. + + // SAFETY: The register is used for read-only operations on constant values. + #[cfg(context = "nrf52840")] + let ficr = unsafe { nrf52840_pac::Peripherals::steal().FICR }; + #[cfg(context = "nrf52832")] + let ficr = unsafe { nrf52832_pac::Peripherals::steal().FICR }; + #[cfg(context = "nrf52833")] + let ficr = unsafe { nrf52833_pac::Peripherals::steal().FICR }; + #[cfg(context = "nrf5340")] + let ficr = &unsafe { nrf5340_app_pac::Peripherals::steal().FICR_S }.info; + + let low = ficr.deviceid[0].read().bits(); + let high = ficr.deviceid[1].read().bits(); + Ok(Self((u64::from(high) << u32::BITS) | u64::from(low))) + } + + type Bytes = [u8; 8]; + + fn bytes(&self) -> Self::Bytes { + self.0.to_le_bytes() + } +} diff --git a/src/riot-rs-nrf/src/lib.rs b/src/riot-rs-nrf/src/lib.rs index 94a139915..db8ebf1e4 100644 --- a/src/riot-rs-nrf/src/lib.rs +++ b/src/riot-rs-nrf/src/lib.rs @@ -16,6 +16,8 @@ pub mod hwrng; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity; + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-rp/src/lib.rs b/src/riot-rs-rp/src/lib.rs index bb1d5b879..ed1003b8a 100644 --- a/src/riot-rs-rp/src/lib.rs +++ b/src/riot-rs-rp/src/lib.rs @@ -21,6 +21,12 @@ pub mod hwrng; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity { + use riot_rs_embassy_common::identity; + + pub type DeviceId = identity::NoDeviceId; +} + #[cfg(feature = "usb")] pub mod usb; diff --git a/src/riot-rs-stm32/src/identity.rs b/src/riot-rs-stm32/src/identity.rs new file mode 100644 index 000000000..641b32fd2 --- /dev/null +++ b/src/riot-rs-stm32/src/identity.rs @@ -0,0 +1,17 @@ +pub struct DeviceId(&'static [u8; 12]); + +impl riot_rs_embassy_common::identity::DeviceId for DeviceId { + type Bytes = &'static [u8; 12]; + + #[allow( + refining_impl_trait_reachable, + reason = "Making this fallible would be a breaking API change for RIOT-rs." + )] + fn get() -> Result { + Ok(Self(embassy_stm32::uid::uid())) + } + + fn bytes(&self) -> Self::Bytes { + self.0 + } +} diff --git a/src/riot-rs-stm32/src/lib.rs b/src/riot-rs-stm32/src/lib.rs index 207bd5596..7228b0512 100644 --- a/src/riot-rs-stm32/src/lib.rs +++ b/src/riot-rs-stm32/src/lib.rs @@ -14,6 +14,8 @@ pub mod extint_registry; #[cfg(feature = "i2c")] pub mod i2c; +pub mod identity; + use embassy_stm32::Config; pub use embassy_stm32::{interrupt, peripherals, OptionalPeripherals, Peripherals}; diff --git a/src/riot-rs/Cargo.toml b/src/riot-rs/Cargo.toml index da6ffc527..bc6c56e36 100644 --- a/src/riot-rs/Cargo.toml +++ b/src/riot-rs/Cargo.toml @@ -15,6 +15,7 @@ riot-rs-bench = { workspace = true, optional = true } riot-rs-boards = { path = "../riot-rs-boards" } riot-rs-debug = { workspace = true } riot-rs-embassy = { path = "../riot-rs-embassy" } +riot-rs-embassy-common = { workspace = true } riot-rs-macros = { path = "../riot-rs-macros" } riot-rs-random = { workspace = true, optional = true } riot-rs-rt = { path = "../riot-rs-rt" } diff --git a/src/riot-rs/src/identity.rs b/src/riot-rs/src/identity.rs new file mode 100644 index 000000000..5b48cd7a6 --- /dev/null +++ b/src/riot-rs/src/identity.rs @@ -0,0 +1,32 @@ +//! Access to unique identifiers provided by the device. +//! +//! This module provides [`device_id_bytes()`], which returns an identifier for the +//! concrete piece of hardware that the software is running on in byte serialized form. +//! +//! Concrete properties of a device identity are: +//! +//! * Identifiers are reasonably unique: They are either unique by construction (serial number, MAC +//! address) or random identifiers (>= 64 bit). +//! +//! * The scope of the identifier is within a RIOT-rs board. Their scope may be broader, eg. when +//! a identifier is unique per MCU family, or even globally. +//! +//! * Identifiers do not change during regular development with a device, which includes the use of +//! a programmer. Identifiers may change under deliberate conditions, eg. when a device has a +//! one-time programmable identity, or when there is a custom functionality to overwrite the +//! built-in identifier that is not triggered by the device erase that is performed as part of +//! programming the device. +//! +//! Constructing an identifier fails rather than produce a dummy identifier. +//! +//! It is considered a breaking change in RIOT-rs if a device's identifier changes or becomes an +//! error. Errors changing to valid identifiers is a compatible change. + +/// Obtains a unique identifier of the device in its byte serialized form. +/// +/// See module level documentation for that identifier's properties. +pub fn device_id_bytes() -> Result, impl core::error::Error> { + use riot_rs_embassy_common::identity::DeviceId; + + riot_rs_embassy::arch::identity::DeviceId::get().map(|d| d.bytes()) +} diff --git a/src/riot-rs/src/lib.rs b/src/riot-rs/src/lib.rs index e88a610cf..d51628f20 100644 --- a/src/riot-rs/src/lib.rs +++ b/src/riot-rs/src/lib.rs @@ -13,6 +13,8 @@ pub mod buildinfo; +pub mod identity; + #[cfg(feature = "bench")] #[doc(inline)] pub use riot_rs_bench as bench;