diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index f2832f6db..e92c5787e 100644 --- a/.github/workflows/build-deploy-docs.yml +++ b/.github/workflows/build-deploy-docs.yml @@ -48,7 +48,7 @@ jobs: - name: Build rustdoc docs run: | - cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb + cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,sensors,threading,usb echo "" > target/doc/index.html mkdir -p ./_site/dev/docs/api && mv target/doc/* ./_site/dev/docs/api diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ce7bcf4d..42b1ed253 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -116,7 +116,7 @@ jobs: # TODO: we'll eventually want to enable relevant features - name: Run crate tests run: | - cargo test --no-default-features --features i2c,no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros + cargo test --no-default-features --features i2c,no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-runqueue -p riot-rs-sensors -p riot-rs-threads -p riot-rs-macros cargo test -p rbi -p ringbuffer -p coapcore # We need to set `RUSTDOCFLAGS` as well in the following jobs, because it @@ -176,7 +176,7 @@ jobs: - name: clippy uses: clechasseur/rs-clippy-check@v3 with: - args: --verbose --locked --features no-boards,external-interrupts -p riot-rs -p riot-rs-boards -p riot-rs-chips -p riot-rs-debug -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-threads -p riot-rs-utils -- --deny warnings + args: --verbose --locked --features no-boards,external-interrupts,sensors -p riot-rs -p riot-rs-boards -p riot-rs-chips -p riot-rs-debug -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-sensors -p riot-rs-threads -p riot-rs-utils -- --deny warnings - run: echo 'RUSTFLAGS=--cfg context="esp32c6"' >> $GITHUB_ENV - name: clippy for ESP32 @@ -206,7 +206,7 @@ jobs: - run: echo 'RUSTFLAGS=' >> $GITHUB_ENV - name: rustdoc - run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb + run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,sensors,threading,usb - name: rustdoc for ESP32 run: RUSTDOCFLAGS='-D warnings --cfg context="esp32c6"' cargo doc --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp diff --git a/Cargo.lock b/Cargo.lock index 9ead0189c..9978a4d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3262,6 +3262,26 @@ dependencies = [ "serde-json-core", ] +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -3610,6 +3630,7 @@ dependencies = [ "riot-rs-macros", "riot-rs-random", "riot-rs-rt", + "riot-rs-sensors", "riot-rs-threads", "riot-rs-utils", "static_cell", @@ -3822,6 +3843,17 @@ dependencies = [ "hax-lib", ] +[[package]] +name = "riot-rs-sensors" +version = "0.1.0" +dependencies = [ + "defmt", + "embassy-sync 0.6.0", + "linkme", + "pin-project", + "riot-rs-macros", +] + [[package]] name = "riot-rs-stm32" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 873713c71..e27194a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "src/riot-rs-nrf", "src/riot-rs-random", "src/riot-rs-rp", + "src/riot-rs-sensors", "src/riot-rs-stm32", "tests/benchmarks/bench_sched_flags", "tests/benchmarks/bench_sched_yield", @@ -85,9 +86,11 @@ riot-rs-boards = { path = "src/riot-rs-boards", default-features = false } riot-rs-debug = { path = "src/riot-rs-debug", default-features = false } riot-rs-embassy = { path = "src/riot-rs-embassy", default-features = false } riot-rs-embassy-common = { path = "src/riot-rs-embassy-common" } +riot-rs-macros = { path = "src/riot-rs-macros" } riot-rs-random = { path = "src/riot-rs-random" } riot-rs-rt = { path = "src/riot-rs-rt" } riot-rs-runqueue = { path = "src/riot-rs-runqueue" } +riot-rs-sensors = { path = "src/riot-rs-sensors" } riot-rs-stm32 = { path = "src/riot-rs-stm32" } riot-rs-utils = { path = "src/riot-rs-utils", default-features = false } @@ -102,6 +105,7 @@ once_cell = { version = "=1.19.0", default-features = false, features = [ "critical-section", ] } paste = { version = "1.0" } +pin-project = "1.1.6" static_cell = { version = "2.0.0", features = ["nightly"] } [profile.dev] diff --git a/src/riot-rs-macros/Cargo.toml b/src/riot-rs-macros/Cargo.toml index 9bfd250be..f7134b322 100644 --- a/src/riot-rs-macros/Cargo.toml +++ b/src/riot-rs-macros/Cargo.toml @@ -32,3 +32,13 @@ trybuild = "1.0.89" [lib] proc-macro = true + +[features] +# These features could be codegened +max-reading-value-min-count-2 = [] +max-reading-value-min-count-3 = [] +max-reading-value-min-count-4 = [] +max-reading-value-min-count-6 = [] +max-reading-value-min-count-7 = [] +max-reading-value-min-count-9 = [] +max-reading-value-min-count-12 = [] diff --git a/src/riot-rs-macros/src/define_count_adjusted_sensor_enums.rs b/src/riot-rs-macros/src/define_count_adjusted_sensor_enums.rs new file mode 100644 index 000000000..2c933f68e --- /dev/null +++ b/src/riot-rs-macros/src/define_count_adjusted_sensor_enums.rs @@ -0,0 +1,134 @@ +/// Generates sensor-related enums whose number of variants needs to be adjusted based on Cargo +/// features, to accommodate the sensor driver returning the largest number of values. +/// +/// One single type must be defined so that it can be used in the Future returned by sensor +/// drivers, which must be the same for every sensor driver so it can be part of the `Sensor` +/// trait. +#[proc_macro] +pub fn define_count_adjusted_sensor_enums(_item: TokenStream) -> TokenStream { + use quote::quote; + + #[allow(clippy::wildcard_imports)] + use define_count_adjusted_enum::*; + + // The order of these feature-gated statements is important as these features are not meant to + // be mutually exclusive. + #[allow(unused_variables, reason = "overridden by feature selection")] + let count = 1; + #[cfg(feature = "max-reading-value-min-count-2")] + let count = 2; + #[cfg(feature = "max-reading-value-min-count-3")] + let count = 3; + #[cfg(feature = "max-reading-value-min-count-4")] + let count = 4; + #[cfg(feature = "max-reading-value-min-count-6")] + let count = 6; + #[cfg(feature = "max-reading-value-min-count-7")] + let count = 7; + #[cfg(feature = "max-reading-value-min-count-9")] + let count = 9; + #[cfg(feature = "max-reading-value-min-count-12")] + let count = 12; + + let physical_values_variants = (1..=count).map(|i| { + let variant = variant_name(i); + quote! { #variant([Value; #i]) } + }); + let physical_values_first_value = (1..=count).map(|i| { + let variant = variant_name(i); + quote! { + Self::#variant(values) => { + if let Some(value) = values.first() { + *value + } else { + // NOTE(no-panic): there is always at least one value + unreachable!(); + } + } + } + }); + + let reading_axes_variants = (1..=count).map(|i| { + let variant = variant_name(i); + quote! { #variant([ReadingAxis; #i]) } + }); + + let values_iter = (1..=count) + .map(|i| { + let variant = variant_name(i); + quote! { Self::#variant(values) => values.iter().copied() } + }) + .collect::>(); + + let expanded = quote! { + /// Values returned by a sensor driver. + /// + /// This type implements [`Reading`] to iterate over the values. + /// + /// # Note + /// + /// This type is automatically generated, the number of variants is automatically adjusted. + #[derive(Debug, Copy, Clone)] + pub enum Values { + #[doc(hidden)] + #(#physical_values_variants),* + } + + impl Reading for Values { + fn value(&self) -> Value { + match self { + #(#physical_values_first_value),* + } + } + + fn values(&self) -> impl ExactSizeIterator { + match self { + #(#values_iter),* + } + } + } + + /// Metadata required to interpret values returned by [`Sensor::wait_for_reading()`]. + /// + /// # Note + /// + /// This type is automatically generated, the number of variants is automatically adjusted. + #[derive(Debug, Copy, Clone)] + pub enum ReadingAxes { + #[doc(hidden)] + #(#reading_axes_variants),*, + } + + impl ReadingAxes { + /// Returns an iterator over the underlying [`ReadingAxis`] items. + /// + /// For a given sensor driver, the number and order of items match the one of + /// [`Values`]. + /// [`Iterator::zip()`] can be useful to zip the returned iterator with the one + /// obtained with [`Reading::values()`]. + pub fn iter(&self) -> impl Iterator + '_ { + match self { + #(#values_iter),*, + } + } + + /// Returns the first [`ReadingAxis`]. + pub fn first(&self) -> ReadingAxis { + if let Some(value) = self.iter().next() { + value + } else { + // NOTE(no-panic): there is always at least one value. + unreachable!(); + } + } + } + }; + + TokenStream::from(expanded) +} + +mod define_count_adjusted_enum { + pub fn variant_name(index: usize) -> syn::Ident { + quote::format_ident!("V{index}") + } +} diff --git a/src/riot-rs-macros/src/lib.rs b/src/riot-rs-macros/src/lib.rs index d1a72394c..5c63aa236 100644 --- a/src/riot-rs-macros/src/lib.rs +++ b/src/riot-rs-macros/src/lib.rs @@ -5,6 +5,7 @@ mod utils; use proc_macro::TokenStream; include!("config.rs"); +include!("define_count_adjusted_sensor_enums.rs"); include!("spawner.rs"); include!("task.rs"); include!("thread.rs"); diff --git a/src/riot-rs-rt/linkme.x b/src/riot-rs-rt/linkme.x index 9a22004c0..099d975e0 100644 --- a/src/riot-rs-rt/linkme.x +++ b/src/riot-rs-rt/linkme.x @@ -5,6 +5,8 @@ SECTIONS { linkm2_EMBASSY_TASKS : { *(linkm2_EMBASSY_TASKS) } > FLASH linkme_USB_BUILDER_HOOKS : { *(linkme_USB_BUILDER_HOOKS) } > FLASH linkm2_USB_BUILDER_HOOKS : { *(linkm2_USB_BUILDER_HOOKS) } > FLASH + linkme_SENSOR_REFS : { *(linkme_SENSOR_REFS) } > FLASH + linkm2_SENSOR_REFS : { *(linkm2_SENSOR_REFS) } > FLASH linkme_THREAD_FNS : { *(linkme_THREAD_FNS) } > FLASH linkm2_THREAD_FNS : { *(linkm2_THREAD_FNS) } > FLASH } diff --git a/src/riot-rs-sensors/Cargo.toml b/src/riot-rs-sensors/Cargo.toml new file mode 100644 index 000000000..41f266f5d --- /dev/null +++ b/src/riot-rs-sensors/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "riot-rs-sensors" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embassy-sync = { workspace = true } +linkme = { workspace = true } +pin-project = { workspace = true } +riot-rs-macros = { workspace = true } + +[features] +defmt = ["dep:defmt"] + +# These features could be codegened +max-reading-value-min-count-2 = ["riot-rs-macros/max-reading-value-min-count-2"] +max-reading-value-min-count-3 = ["riot-rs-macros/max-reading-value-min-count-3"] +max-reading-value-min-count-4 = ["riot-rs-macros/max-reading-value-min-count-4"] +max-reading-value-min-count-6 = ["riot-rs-macros/max-reading-value-min-count-6"] +max-reading-value-min-count-7 = ["riot-rs-macros/max-reading-value-min-count-7"] +max-reading-value-min-count-9 = ["riot-rs-macros/max-reading-value-min-count-9"] +max-reading-value-min-count-12 = [ + "riot-rs-macros/max-reading-value-min-count-12", +] diff --git a/src/riot-rs-sensors/src/category.rs b/src/riot-rs-sensors/src/category.rs new file mode 100644 index 000000000..309ca41d1 --- /dev/null +++ b/src/riot-rs-sensors/src/category.rs @@ -0,0 +1,54 @@ +/// Categories a sensor driver can be part of. +/// +/// A sensor driver can be part of multiple categories. +/// +/// # For sensor driver implementors +/// +/// Many mechanical sensor devices (e.g., accelerometers) include a temperature sensor as +/// temperature may slightly affect the measurement results. +/// If temperature readings are not exposed by the sensor driver, the sensor driver must not be +/// considered part of a category that includes temperature ([`Category::Temperature`] or +/// [`Category::AccelerometerTemperature`] in the case of an accelerometer). +/// +/// Missing variants can be added when required. +/// Please open an issue to discuss it. +// Built upon https://doc.riot-os.org/group__drivers__saul.html#ga8f2dfec7e99562dbe5d785467bb71bbb +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Category { + /// Accelerometer. + Accelerometer, + /// Accelerometer & temperature sensor. + AccelerometerTemperature, + /// Accelerometer & magnetometer & temperature sensor. + AccelerometerMagnetometerTemperature, + /// Ammeter (ampere meter). + Ammeter, + /// CO₂ gas sensor. + Co2Gas, + /// Color sensor. + Color, + /// Gyroscope. + Gyroscope, + /// Humidity sensor. + Humidity, + /// Humidity & temperature sensor. + HumidityTemperature, + /// Light sensor. + Light, + /// Magnetometer. + Magnetometer, + /// pH sensor. + Ph, + /// Pressure sensor. + Pressure, + /// Push button. + PushButton, + /// Temperature sensor. + Temperature, + /// TVOC sensor. + Tvoc, + /// Voltage sensor. + Voltage, +} diff --git a/src/riot-rs-sensors/src/label.rs b/src/riot-rs-sensors/src/label.rs new file mode 100644 index 000000000..f09c340d2 --- /dev/null +++ b/src/riot-rs-sensors/src/label.rs @@ -0,0 +1,44 @@ +/// Label of a [`Value`](crate::sensor::Value) part of a +/// [`Values`](crate::sensor::Values) tuple. +/// +/// # For sensor driver implementors +/// +/// Missing variants can be added when required. +/// Please open an issue to discuss it. +/// +/// [`Label::Main`] must be used for sensor drivers returning a single +/// [`Value`](crate::sensor::Value), even if a more specific label exists for the +/// physical quantity. +/// This allows consumers displaying the label to ignore it for sensor drivers returning a single +/// [`Value`](crate::sensor::Value). +/// Other labels are reserved for sensor drivers returning multiple physical quantities. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Label { + /// Used for sensor drivers returning a single [`Value`](crate::sensor::Value). + Main, + /// Humidity. + Humidity, + /// Temperature. + Temperature, + /// X axis. + X, + /// Y axis. + Y, + /// Z axis. + Z, +} + +impl core::fmt::Display for Label { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Main => write!(f, ""), + Self::Humidity => write!(f, "Humidity"), + Self::Temperature => write!(f, "Temperature"), + Self::X => write!(f, "X"), + Self::Y => write!(f, "Y"), + Self::Z => write!(f, "Z"), + } + } +} diff --git a/src/riot-rs-sensors/src/lib.rs b/src/riot-rs-sensors/src/lib.rs new file mode 100644 index 000000000..cd1530f21 --- /dev/null +++ b/src/riot-rs-sensors/src/lib.rs @@ -0,0 +1,67 @@ +//! Provides a sensor abstraction layer. +//! +//! # Definitions +//! +//! In the context of this abstraction: +//! +//! - A *sensor device* is a device measuring one or multiple physical quantities and reporting +//! them as one or more digital values. - Sensor devices measuring the same physical quantity are +//! said to be part of the same *sensor category*. +//! A sensor device may be part of multiple sensor categories. +//! - A *measurement* is the physical operation of measuring one or several physical quantities. +//! - A *reading* is the digital result returned by a sensor device after carrying out a +//! measurement. +//! Values of different physical quantities can therefore be part of the same reading. +//! - A *sensor driver* refers to a sensor device as exposed by the sensor abstraction layer. +//! - A *sensor driver instance* is an instance of a sensor driver. +//! +//! # Accessing sensor driver instances +//! +//! Registered sensor driver instances can be accessed using +//! [`REGISTRY::sensors()`](registry::Registry::sensors). +//! Sensor drivers implement the [`Sensor`] trait, which allows to trigger measurements and obtain +//! the resulting readings. +//! +//! # Obtaining a sensor reading +//! +//! After triggering a measurement with [`Sensor::trigger_measurement()`], a reading can be +//! obtained using [`Sensor::wait_for_reading()`]. +//! It is additionally necessary to use [`Sensor::reading_axes()`] to make sense of the obtained +//! reading: +//! +//! - [`Sensor::wait_for_reading()`] returns a [`Values`](sensor::Values), a data "tuple" +//! containing values returned by the sensor driver. +//! - [`Sensor::reading_axes()`] returns a [`ReadingAxes`](sensor::ReadingAxes) which +//! indicates which physical quantity each [`Value`](value::Value) from that tuple corresponds +//! to, using a [`Label`]. +//! For instance, this allows to disambiguate the values provided by a temperature & humidity +//! sensor. +//! +//! To avoid handling floats, [`Value`](value::Value)s returned by [`Sensor::wait_for_reading()`] +//! are integers, and a fixed scaling value is provided in [`ReadingAxis`](sensor::ReadingAxis), +//! for each [`Value`](value::Value) returned. +//! See [`Value`](value::Value) for more details. +//! +//! # For implementors +//! +//! Sensor drivers must implement the [`Sensor`] trait. +//! +#![no_std] +// Required by linkme +#![feature(used_with_arg)] +#![deny(clippy::pedantic)] +#![deny(missing_docs)] + +mod category; +mod label; +mod measurement_unit; +pub mod registry; +pub mod sensor; +mod value; + +pub use category::Category; +pub use label::Label; +pub use measurement_unit::MeasurementUnit; +pub use registry::{REGISTRY, SENSOR_REFS}; +pub use sensor::Sensor; +pub use value::Reading; diff --git a/src/riot-rs-sensors/src/measurement_unit.rs b/src/riot-rs-sensors/src/measurement_unit.rs new file mode 100644 index 000000000..201c29c76 --- /dev/null +++ b/src/riot-rs-sensors/src/measurement_unit.rs @@ -0,0 +1,125 @@ +/// Represents a unit of measurement. +/// +/// # For sensor driver implementors +/// +/// Missing variants can be added when required. +/// Please open an issue to discuss it. +// Built upon https://doc.riot-os.org/phydat_8h_source.html +// and https://bthome.io/format/#sensor-data +// and https://www.iana.org/assignments/senml/senml.xhtml +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum MeasurementUnit { + /// [Acceleration *g*](https://en.wikipedia.org/wiki/G-force#Unit_and_measurement). + AccelG, + /// Ampere (A). + Ampere, + /// Becquerel (Bq). + Becquerel, + /// Logic boolean: `0` means `false` and `1` means `true`. + Bool, + /// Candela (cd). + Candela, + /// Degrees Celsius (°C). + Celsius, + /// Coulomb (C). + Coulomb, + /// Decibel (dB). + Decibel, + /// Farad (F). + Farad, + // FIXME: Kilogram as well? + /// Gram (g). + Gram, + /// Gray (Gy). + Gray, + /// Henry (H). + Henry, + /// Hertz (Hz). + Hertz, + /// Joule (J). + Joule, + /// Katal (kat). + Katal, + /// Kelvin (K). + Kelvin, + /// Lumen (lm). + Lumen, + /// Lux (lx). + Lux, + /// Meter (m) + Meter, + /// Mole (mol). + Mole, + /// Newton (N). + Newton, + /// Ohm (Ω). + Ohm, + /// Pascal (Pa). + Pascal, + /// Percent (%). + Percent, + /// %RH. + PercentageRelativeHumidity, + /// Radian (rad). + Radian, + /// Second (s). + Second, + /// Siemens (S). + Siemens, + /// Sievert (Sv). + Sievert, + /// Steradian (sr). + Steradian, + /// Tesla (T). + Tesla, + /// Volt (V). + Volt, + /// Watt (W). + Watt, + /// Weber (Wb). + Weber, +} + +impl core::fmt::Display for MeasurementUnit { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[expect(clippy::match_same_arms)] + match self { + Self::AccelG => write!(f, "g"), + Self::Ampere => write!(f, "A"), + Self::Becquerel => write!(f, "Bq"), + Self::Bool => write!(f, ""), + Self::Candela => write!(f, "cd"), + Self::Celsius => write!(f, "°C"), // The Unicode Standard v15 recommends using U+00B0 + U+0043. + Self::Coulomb => write!(f, "C"), + Self::Decibel => write!(f, "dB"), + Self::Farad => write!(f, "F"), + Self::Gram => write!(f, "g"), + Self::Gray => write!(f, "Gy"), + Self::Henry => write!(f, "H"), + Self::Hertz => write!(f, "Hz"), + Self::Joule => write!(f, "J"), + Self::Katal => write!(f, "kat"), + Self::Kelvin => write!(f, "K"), + Self::Lumen => write!(f, "lm"), + Self::Lux => write!(f, "lx"), + Self::Meter => write!(f, "m"), + Self::Mole => write!(f, "mol"), + Self::Newton => write!(f, "N"), + Self::Ohm => write!(f, "Ω"), + Self::Pascal => write!(f, "Pa"), + Self::Percent => write!(f, "%"), + Self::PercentageRelativeHumidity => write!(f, "%RH"), + Self::Radian => write!(f, "rad"), + Self::Second => write!(f, "s"), + Self::Siemens => write!(f, "S"), + Self::Sievert => write!(f, "Sv"), + Self::Steradian => write!(f, "sr"), + Self::Tesla => write!(f, "T"), + Self::Volt => write!(f, "V"), + Self::Watt => write!(f, "W"), + Self::Weber => write!(f, "Wb"), + } + } +} diff --git a/src/riot-rs-sensors/src/registry.rs b/src/riot-rs-sensors/src/registry.rs new file mode 100644 index 000000000..86732ed0e --- /dev/null +++ b/src/riot-rs-sensors/src/registry.rs @@ -0,0 +1,42 @@ +//! Provides a sensor driver instance registry, allowing to register sensor driver instances and +//! access them in a centralized location. + +use crate::Sensor; + +/// Stores references to registered sensor driver instances. +/// +/// To register a sensor driver instance, insert a `&'static` into this [distributed +/// slice](linkme). +/// The sensor driver will therefore need to be statically allocated, to be able to obtain a +/// `&'static`. +// Exclude this from the users' documentation, to force users to use `Registry::sensors()` instead, +// for easier forward compatibility with possibly non-static references. +#[doc(hidden)] +#[linkme::distributed_slice] +pub static SENSOR_REFS: [&'static dyn Sensor] = [..]; + +/// The global registry instance. +pub static REGISTRY: Registry = Registry::new(); + +/// The sensor driver instance registry. +/// +/// This is exposed as [`REGISTRY`]. +pub struct Registry { + // Prevents instantiation from outside this module. + _private: (), +} + +impl Registry { + // The constructor is private to make the registry a singleton. + const fn new() -> Self { + Self { _private: () } + } + + /// Returns an iterator over registered sensor driver instances. + pub fn sensors(&self) -> impl Iterator { + // Returning an iterator instead of the distributed slice directly would allow us to chain + // another source of sensor driver instances in the future, if we decided to support + // dynamically-allocated sensor driver instances. + SENSOR_REFS.iter().copied() + } +} diff --git a/src/riot-rs-sensors/src/sensor.rs b/src/riot-rs-sensors/src/sensor.rs new file mode 100644 index 000000000..f344d076d --- /dev/null +++ b/src/riot-rs-sensors/src/sensor.rs @@ -0,0 +1,326 @@ +//! Provides a [`Sensor`] trait abstracting over implementation details of a sensor driver. +use core::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::ReceiveFuture}; + +use crate::{Category, Label, MeasurementUnit}; + +pub use crate::{ + value::{Accuracy, Value}, + Reading, +}; + +riot_rs_macros::define_count_adjusted_sensor_enums!(); + +/// This trait must be implemented by sensor drivers. +/// +/// See [the module level documentation](crate) for more. +pub trait Sensor: Send + Sync { + /// Triggers a measurement. + /// Clears the previous reading. + /// + /// To obtain readings from every sensor drivers this method can be called in a loop over all + /// sensors returned by [`Registry::sensors()`](crate::registry::Registry::sensors), before + /// obtaining the readings with [`Self::wait_for_reading()`] in a second loop, so that the + /// measurements happen concurrently. + /// + /// # For implementors + /// + /// This method should return quickly. + /// + /// # Errors + /// + /// Returns [`TriggerMeasurementError::NonEnabled`] if the sensor driver is not enabled. + fn trigger_measurement(&self) -> Result<(), TriggerMeasurementError>; + + /// Waits for the reading and returns it asynchronously. + /// Depending on the sensor device and the sensor driver, this may use a sensor interrupt or + /// data polling. + /// Interpretation of the reading requires data from [`Sensor::reading_axes()`] as well. + /// See [the module level documentation](crate) for more. + /// + /// # Note + /// + /// It is necessary to trigger a measurement by calling [`Sensor::trigger_measurement()`] + /// beforehand, even if the sensor device carries out periodic measurements on its own. + /// + /// # Errors + /// + /// - Quickly returns [`ReadingError::NonEnabled`] if the sensor driver is not enabled. + /// - Quickly returns [`ReadingError::NotMeasuring`] if no measurement has been triggered + /// beforehand using [`Sensor::trigger_measurement()`]. + /// - Returns [`ReadingError::SensorAccess`] if the sensor device cannot be accessed. + fn wait_for_reading(&'static self) -> ReadingWaiter; + + /// Provides information about the reading returned by [`Sensor::wait_for_reading()`]. + #[must_use] + fn reading_axes(&self) -> ReadingAxes; + + /// Sets the sensor driver mode and returns the previous state. + /// Allows to put the sensor device to sleep if supported. + /// + /// # Errors + /// + /// Returns [`SetModeError::Uninitialized`] if the sensor driver is not initialized. + fn set_mode(&self, mode: Mode) -> Result; + + /// Returns the current sensor driver state. + #[must_use] + fn state(&self) -> State; + + /// Returns the categories the sensor device is part of. + #[must_use] + fn categories(&self) -> &'static [Category]; + + /// String label of the sensor driver *instance*. + /// For instance, in the case of a temperature sensor, this allows to specify whether this + /// specific sensor device is placed indoor or outdoor. + #[must_use] + fn label(&self) -> Option<&'static str>; + + /// Returns a human-readable name of the *sensor driver*. + /// For instance, "push button" and "3-axis accelerometer" are appropriate display names. + /// + /// # Note + /// + /// Different sensor drivers for the same sensor device may have different display names. + #[must_use] + fn display_name(&self) -> Option<&'static str>; + + /// Returns the sensor device part number. + /// Returns `None` when the sensor device does not have a part number. + /// For instance, "DS18B20" is a valid part number. + #[must_use] + fn part_number(&self) -> Option<&'static str>; + + /// Returns the sensor driver version number. + #[must_use] + fn version(&self) -> u8; +} + +/// Future returned by [`Sensor::wait_for_reading()`]. +#[must_use = "futures do nothing unless you `.await` or poll them"] +#[pin_project::pin_project(project = ReadingWaiterProj)] +pub enum ReadingWaiter { + #[doc(hidden)] + Waiter { + #[pin] + waiter: ReceiveFuture<'static, CriticalSectionRawMutex, ReadingResult, 1>, + }, + #[doc(hidden)] + Err(ReadingError), + #[doc(hidden)] + Resolved, +} + +impl Future for ReadingWaiter { + type Output = ReadingResult; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut().project(); + match this { + ReadingWaiterProj::Waiter { waiter } => waiter.poll(cx), + ReadingWaiterProj::Err(err) => { + // Replace the error with a dummy error value, crafted from thin air, and mark the + // future as resolved, so that we do not take this dummy value into account later. + // This avoids requiring `Clone` on `ReadingError`. + let err = core::mem::replace(err, ReadingError::NonEnabled); + *self = ReadingWaiter::Resolved; + + Poll::Ready(Err(err)) + } + ReadingWaiterProj::Resolved => unreachable!(), + } + } +} + +/// Mode of a sensor driver. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Mode { + /// The sensor driver is disabled. + Disabled, + /// The sensor driver is enabled. + Enabled, + /// The sensor driver is sleeping. + /// The sensor device may be in a low-power mode. + Sleeping, +} + +/// Possible errors when attempting to set the mode of a sensor driver. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SetModeError { + /// The sensor driver is uninitialized. + /// It has not been initialized yet, or initialization could not succeed. + Uninitialized, +} + +impl core::fmt::Display for SetModeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Uninitialized => write!(f, "sensor driver is not initialized"), + } + } +} + +impl core::error::Error for SetModeError {} + +/// State of a sensor driver. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum State { + /// The sensor driver is uninitialized. + /// It has not been initialized yet, or initialization could not succeed. + #[default] + Uninitialized = 0, + /// The sensor driver is disabled. + Disabled = 1, + /// The sensor driver is enabled. + Enabled = 2, + /// The sensor driver is enabled and a measurement has been triggered. + Measuring = 3, + /// The sensor driver is sleeping. + Sleeping = 4, +} + +impl From for State { + fn from(mode: Mode) -> Self { + match mode { + Mode::Disabled => Self::Disabled, + Mode::Enabled => Self::Enabled, + Mode::Sleeping => Self::Sleeping, + } + } +} + +impl TryFrom for State { + type Error = TryFromIntError; + + fn try_from(int: u8) -> Result { + match int { + 0 => Ok(Self::Uninitialized), + 1 => Ok(Self::Disabled), + 2 => Ok(Self::Enabled), + 3 => Ok(Self::Measuring), + 4 => Ok(Self::Sleeping), + _ => Err(TryFromIntError), + } + } +} + +/// The error type returned when a checked integral type conversion fails. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TryFromIntError; + +impl core::fmt::Display for TryFromIntError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "out of range integral type conversion attempted") + } +} + +impl core::error::Error for TryFromIntError {} + +/// Provides metadata about a [`Value`]. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +// NOTE(derive): we do not implement `Eq` on purpose: its would prevent us from possibly adding +// floats in the future. +pub struct ReadingAxis { + label: Label, + scaling: i8, + unit: MeasurementUnit, +} + +impl ReadingAxis { + /// Creates a new [`ReadingAxis`]. + /// + /// This constructor is intended for sensor driver implementors only. + #[must_use] + pub fn new(label: Label, scaling: i8, unit: MeasurementUnit) -> Self { + Self { + label, + scaling, + unit, + } + } + + /// Returns the [`Label`] for this axis. + #[must_use] + pub fn label(&self) -> Label { + self.label + } + + /// Returns the [scaling](Value) for this axis. + #[must_use] + pub fn scaling(&self) -> i8 { + self.scaling + } + + /// Returns the unit of measurement for this axis. + #[must_use] + pub fn unit(&self) -> MeasurementUnit { + self.unit + } +} + +/// Represents errors happening when *triggering* a sensor measurement. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum TriggerMeasurementError { + /// The sensor driver is not enabled (e.g., it may be disabled or sleeping). + NonEnabled, +} + +impl core::fmt::Display for TriggerMeasurementError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NonEnabled => write!(f, "sensor driver is not enabled"), + } + } +} + +impl core::error::Error for TriggerMeasurementError {} + +/// Represents errors happening when accessing a sensor reading. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ReadingError { + /// The sensor driver is not enabled (e.g., it may be disabled or sleeping). + NonEnabled, + /// Cannot access the sensor device (e.g., because of a bus error). + SensorAccess, + /// No measurement has been triggered before waiting for a reading. + /// It is necessary to call [`Sensor::trigger_measurement()`] before calling + /// [`Sensor::wait_for_reading()`]. + NotMeasuring, +} + +impl core::fmt::Display for ReadingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NonEnabled => write!(f, "sensor driver is not enabled"), + Self::SensorAccess => write!(f, "sensor device could not be accessed"), + Self::NotMeasuring => write!(f, "no measurement has been triggered"), + } + } +} + +impl core::error::Error for ReadingError {} + +/// A specialized [`Result`] type for [`Reading`] operations. +pub type ReadingResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + // Assert that the Sensor trait is object-safe + static _SENSOR_REFS: &[&dyn Sensor] = &[]; +} diff --git a/src/riot-rs-sensors/src/value.rs b/src/riot-rs-sensors/src/value.rs new file mode 100644 index 000000000..cc0ca2cb1 --- /dev/null +++ b/src/riot-rs-sensors/src/value.rs @@ -0,0 +1,130 @@ +#[expect(clippy::doc_markdown)] +/// Represents a value obtained from a sensor device, along with its accuracy. +/// +/// # Scaling +/// +/// The [scaling value](crate::sensor::ReadingAxis::scaling()) obtained from the sensor driver with +/// [`Sensor::reading_axes()`](crate::Sensor::reading_axes) must be taken into account using the +/// following formula: +/// +/// Value::value()·10scaling +/// +/// For instance, in the case of a temperature sensor, if [`Self::value()`] returns `2225` and the +/// scaling value is `-2`, this means that the temperature measured and returned by the sensor +/// device is `22.25` (the [measurement error](Accuracy) must additionally be taken into +/// account). +/// This is required to avoid handling floats. +/// +/// # Unit of measurement +/// +/// The unit of measurement can be obtained using +/// [`ReadingAxis::unit()`](crate::sensor::ReadingAxis::unit). +/// +/// # Accuracy +/// +/// The accuracy can be obtained with [`Self::accuracy()`]. +// NOTE(derive): we do not implement `Eq` or `PartialOrd` on purpose: `Eq` would prevent us from +// possibly adding floats in the future and `PartialOrd` does not make sense because interpreting +// the value requires the `ReadingAxis` associated with this `Value`. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Value { + value: i32, + accuracy: Accuracy, +} + +impl Value { + /// Creates a new value. + /// + /// This constructor is intended for sensor driver implementors only. + #[must_use] + pub const fn new(value: i32, accuracy: Accuracy) -> Self { + Self { value, accuracy } + } + + /// Returns the value. + #[must_use] + pub fn value(&self) -> i32 { + self.value + } + + /// Returns the measurement accuracy. + #[must_use] + pub fn accuracy(&self) -> Accuracy { + self.accuracy + } +} + +/// Specifies the accuracy of a measurement. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Accuracy { + /// Unknown accuracy. + Unknown, + /// No measurement error (e.g., boolean values from a push button). + NoError, + /// Measurement error symmetrical around the [`bias`](Accuracy::SymmetricalError::bias). + /// + /// The unit of measurement is provided by the [`ReadingAxis`](crate::sensor::ReadingAxis) + /// associated to the [`Value`]. + /// The `scaling` value is used for both `deviation` and `bias`. + /// The accuracy error is thus given by the following formulas: + /// + /// +(bias+deviation)·10scaling/-(bias-deviation)·10scaling + /// + /// # Examples + /// + /// The DS18B20 temperature sensor accuracy error is +0.05/-0.45 + /// at 20 °C (see Figure 1 of its datasheet). + /// [`Accuracy`] would thus be the following: + /// + /// ``` + /// # use riot_rs_sensors::sensor::Accuracy; + /// Accuracy::SymmetricalError { + /// deviation: 25, + /// bias: -20, + /// scaling: -2, + /// } + /// # ; + /// ``` + SymmetricalError { + /// Deviation around the bias value. + deviation: i8, + /// Bias (mean accuracy error). + bias: i8, + /// Scaling of [`deviation`](Accuracy::SymmetricalError::deviation) and + /// [`bias`](Accuracy::SymmetricalError::bias). + scaling: i8, + }, +} + +/// Implemented on [`Values`](crate::sensor::Values), returned by +/// [`Sensor::wait_for_reading()`](crate::Sensor::wait_for_reading). +pub trait Reading: core::fmt::Debug { + /// Returns the first value returned by [`Reading::values()`]. + fn value(&self) -> Value; + + /// Returns an iterator over [`Value`]s of a sensor reading. + /// + /// The order of [`Value`]s is not significant, but is fixed. + /// + /// # For implementors + /// + /// The default implementation must be overridden on types containing multiple + /// [`Value`]s. + fn values(&self) -> impl ExactSizeIterator { + [self.value()].into_iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assert_type_sizes() { + assert!(size_of::() <= size_of::()); + // Make sure the type is small enough. + assert!(size_of::() <= 2 * size_of::()); + } +} diff --git a/src/riot-rs/Cargo.toml b/src/riot-rs/Cargo.toml index da6ffc527..621ad042f 100644 --- a/src/riot-rs/Cargo.toml +++ b/src/riot-rs/Cargo.toml @@ -18,6 +18,7 @@ riot-rs-embassy = { path = "../riot-rs-embassy" } riot-rs-macros = { path = "../riot-rs-macros" } riot-rs-random = { workspace = true, optional = true } riot-rs-rt = { path = "../riot-rs-rt" } +riot-rs-sensors = { workspace = true, optional = true } riot-rs-threads = { path = "../riot-rs-threads", optional = true } riot-rs-utils = { workspace = true } static_cell = { workspace = true } @@ -43,6 +44,9 @@ random = ["riot-rs-random"] csprng = ["riot-rs-random/csprng"] ## Enables seeding the random number generator from hardware. hwrng = ["riot-rs-embassy/hwrng"] +## Enables support for sensors. +## *Currently experimental.* +sensors = ["dep:riot-rs-sensors"] #! ## Serial communication ## Enables I2C support. diff --git a/src/riot-rs/src/lib.rs b/src/riot-rs/src/lib.rs index e88a610cf..e21fcc42a 100644 --- a/src/riot-rs/src/lib.rs +++ b/src/riot-rs/src/lib.rs @@ -23,6 +23,9 @@ pub use riot_rs_debug as debug; pub use riot_rs_random as random; #[doc(inline)] pub use riot_rs_rt as rt; +#[cfg(feature = "sensors")] +#[doc(inline)] +pub use riot_rs_sensors as sensors; #[cfg(feature = "threading")] #[doc(inline)] pub use riot_rs_threads as thread;