diff --git a/Cargo.toml b/Cargo.toml index 9a082db8..f86ea661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ version = "0.1.0" [dependencies] libtock_adc = { path = "apis/adc"} +libtock_air_quality = { path = "apis/air_quality" } libtock_alarm = { path = "apis/alarm" } libtock_ambient_light = { path = "apis/ambient_light" } libtock_buttons = { path = "apis/buttons" } @@ -41,6 +42,7 @@ debug = true exclude = ["tock"] members = [ "apis/adc", + "apis/air_quality", "apis/alarm", "apis/gpio", "apis/buttons", diff --git a/apis/air_quality/Cargo.toml b/apis/air_quality/Cargo.toml new file mode 100644 index 00000000..856bfc4c --- /dev/null +++ b/apis/air_quality/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "libtock_air_quality" +version = "0.1.0" +authors = ["Tock Project Developers "] +license = "MIT/Apache-2.0" +edition = "2021" +repository = "https://www.github.com/tock/libtock-rs" +description = "libtock air quality driver" + +[dependencies] +libtock_platform = { path = "../../platform" } + +[dev-dependencies] +libtock_unittest = { path = "../../unittest" } diff --git a/apis/air_quality/src/lib.rs b/apis/air_quality/src/lib.rs new file mode 100644 index 00000000..715a6e4d --- /dev/null +++ b/apis/air_quality/src/lib.rs @@ -0,0 +1,128 @@ +#![no_std] + +use core::cell::Cell; +use libtock_platform::subscribe::OneId; +use libtock_platform::{ + share::scope, share::Handle, DefaultConfig, ErrorCode, Subscribe, Syscalls, Upcall, +}; +use Value::{Tvoc, CO2}; + +enum Value { + CO2 = READ_CO2 as isize, + Tvoc = READ_TVOC as isize, +} + +pub struct AirQuality(S); + +impl AirQuality { + /// Returns Ok() if the driver was present.This does not necessarily mean + /// that the driver is working. + pub fn exists() -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, EXISTS, 0, 0).to_result() + } + + /// Register an events listener + pub fn register_listener<'share, F: Fn(u32)>( + listener: &'share AirQualityListener, + subscribe: Handle>, + ) -> Result<(), ErrorCode> { + S::subscribe::<_, _, DefaultConfig, DRIVER_NUM, 0>(subscribe, listener) + } + + /// Unregister the events listener + pub fn unregister_listener() { + S::unsubscribe(DRIVER_NUM, 0) + } + + /// Initiate a CO2 measurement. + /// + /// This function is used both for synchronous and asynchronous readings + pub fn read_co2() -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, READ_CO2, 0, 0).to_result() + } + + /// Initiate a TVOC measurement. + /// + /// This function is used both for synchronous and asynchronous readings + pub fn read_tvoc() -> Result<(), ErrorCode> { + S::command(DRIVER_NUM, READ_TVOC, 0, 0).to_result() + } + + /// Public wrapper for `read_data_sync` for CO2 synchronous measurement + pub fn read_co2_sync() -> Result { + Self::read_data_sync(CO2) + } + + /// Public wrapper for `read_data_sync` for TVOC synchronous measurement + pub fn read_tvoc_sync() -> Result { + Self::read_data_sync(Tvoc) + } + + /// Read both CO2 and TVOC values synchronously + pub fn read_sync() -> Result<(u32, u32), ErrorCode> { + match (Self::read_data_sync(CO2), Self::read_data_sync(Tvoc)) { + (Ok(co2_value), Ok(tvoc_value)) => Ok((co2_value, tvoc_value)), + (Err(co2_error), _) => Err(co2_error), + (_, Err(tvoc_error)) => Err(tvoc_error), + } + } + + /// Initiate a synchronous CO2 or TVOC measurement, based on the `read_type`. + /// Returns Ok(value) if the operation was successful + fn read_data_sync(read_type: Value) -> Result { + let data_cell: Cell> = Cell::new(None); + let listener = AirQualityListener(|data_val| { + data_cell.set(Some(data_val)); + }); + + scope(|subscribe| { + Self::register_listener(&listener, subscribe)?; + match read_type { + CO2 => { + Self::read_co2()?; + while data_cell.get() == None { + S::yield_wait(); + } + + match data_cell.get() { + None => Err(ErrorCode::Fail), + Some(co2_value) => Ok(co2_value), + } + } + Tvoc => { + Self::read_tvoc()?; + while data_cell.get() == None { + S::yield_wait(); + } + + match data_cell.get() { + None => Err(ErrorCode::Fail), + Some(tvoc_value) => Ok(tvoc_value), + } + } + } + }) + } +} + +pub struct AirQualityListener(pub F); +impl Upcall> for AirQualityListener { + fn upcall(&self, data_val: u32, _arg1: u32, _arg2: u32) { + self.0(data_val) + } +} + +#[cfg(test)] +mod tests; + +// ----------------------------------------------------------------------------- +// Driver number and command IDs +// ----------------------------------------------------------------------------- + +const DRIVER_NUM: u32 = 0x60007; + +// Command IDs + +const EXISTS: u32 = 0; +const READ_CO2: u32 = 2; +const READ_TVOC: u32 = 3; diff --git a/apis/air_quality/src/tests.rs b/apis/air_quality/src/tests.rs new file mode 100644 index 00000000..de761683 --- /dev/null +++ b/apis/air_quality/src/tests.rs @@ -0,0 +1,120 @@ +use crate::AirQualityListener; +use core::cell::Cell; +use libtock_platform::{share::scope, ErrorCode, Syscalls, YieldNoWaitReturn}; +use libtock_unittest::fake; + +type AirQuality = super::AirQuality; + +#[test] +fn no_driver() { + let _kernel = fake::Kernel::new(); + assert_eq!(AirQuality::exists(), Err(ErrorCode::NoDevice)); +} + +#[test] +fn driver_check() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + assert_eq!(AirQuality::exists(), Ok(())); +} + +#[test] +fn read_co2() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + assert_eq!(AirQuality::read_co2(), Ok(())); + assert!(driver.is_busy()); + + assert_eq!(AirQuality::read_co2(), Err(ErrorCode::Busy)); + assert_eq!(AirQuality::read_co2_sync(), Err(ErrorCode::Busy)); +} + +#[test] +fn read_tvoc() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + assert_eq!(AirQuality::read_tvoc(), Ok(())); + assert!(driver.is_busy()); + + assert_eq!(AirQuality::read_tvoc(), Err(ErrorCode::Busy)); + assert_eq!(AirQuality::read_tvoc_sync(), Err(ErrorCode::Busy)); +} + +#[test] +fn register_unregister_listener() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + let data_cell: Cell> = Cell::new(None); + let listener = AirQualityListener(|data_val| { + data_cell.set(Some(data_val)); + }); + + scope(|subscribe| { + assert_eq!(AirQuality::read_co2(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + + assert_eq!(AirQuality::read_tvoc(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + + assert_eq!(AirQuality::register_listener(&listener, subscribe), Ok(())); + + assert_eq!(AirQuality::read_co2(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert_eq!(data_cell.get(), Some(100)); + + assert_eq!(AirQuality::read_tvoc(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert_eq!(data_cell.get(), Some(100)); + + AirQuality::unregister_listener(); + assert_eq!(AirQuality::read_co2(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + + assert_eq!(AirQuality::read_tvoc(), Ok(())); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + }); +} + +#[test] +fn read_co2_sync() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + driver.set_value_sync(100); + assert_eq!(AirQuality::read_co2_sync(), Ok(100)); +} + +#[test] +fn read_tvoc_sync() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + driver.set_value_sync(100); + assert_eq!(AirQuality::read_tvoc_sync(), Ok(100)); +} + +#[test] +fn read_sync() { + let kernel = fake::Kernel::new(); + let driver = fake::AirQuality::new(); + kernel.add_driver(&driver); + + driver.set_values_sync(100, 200); + assert_eq!(AirQuality::read_sync(), Ok((100, 200))) +} diff --git a/src/lib.rs b/src/lib.rs index d0334838..5d6f32e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,13 @@ pub mod adc { pub type Adc = adc::Adc; pub use adc::ADCListener; } + +pub mod air_quality { + use libtock_air_quality as air_quality; + pub type AirQuality = air_quality::AirQuality; + pub use air_quality::AirQualityListener; +} + pub mod alarm { use libtock_alarm as alarm; pub type Alarm = alarm::Alarm; diff --git a/unittest/src/fake/air_quality/mod.rs b/unittest/src/fake/air_quality/mod.rs new file mode 100644 index 00000000..95f17495 --- /dev/null +++ b/unittest/src/fake/air_quality/mod.rs @@ -0,0 +1,119 @@ +use crate::{DriverInfo, DriverShareRef}; +use libtock_platform::{CommandReturn, ErrorCode}; +use std::cell::Cell; + +pub struct AirQuality { + busy: Cell, + co2_available: Cell, + tvoc_available: Cell, + upcall_on_read: Cell>, + upcall_on_tuple_read: Cell>, + share_ref: DriverShareRef, +} + +impl AirQuality { + pub fn new() -> std::rc::Rc { + std::rc::Rc::new(AirQuality { + busy: Cell::new(false), + co2_available: Cell::new(true), + tvoc_available: Cell::new(true), + upcall_on_read: Cell::new(None), + upcall_on_tuple_read: Cell::new(None), + share_ref: Default::default(), + }) + } + + pub fn set_co2_available(&self, co2_available: bool) { + self.co2_available.set(co2_available); + } + + pub fn set_tvoc_available(&self, tvoc_available: bool) { + self.tvoc_available.set(tvoc_available); + } + + pub fn is_busy(&self) -> bool { + self.busy.get() + } + + pub fn set_value(&self, value: u32) { + if self.busy.get() { + self.share_ref + .schedule_upcall(0, (value as u32, 0, 0)) + .expect("Unable to schedule upcall"); + self.busy.set(false); + } + } + pub fn set_value_sync(&self, value: u32) { + self.upcall_on_read.set(Some(value)); + } + pub fn set_values_sync(&self, co2_value: u32, tvoc_value: u32) { + self.upcall_on_tuple_read.set(Some((co2_value, tvoc_value))); + } +} + +impl crate::fake::SyscallDriver for AirQuality { + fn info(&self) -> DriverInfo { + DriverInfo::new(DRIVER_NUM).upcall_count(1) + } + + fn register(&self, share_ref: DriverShareRef) { + self.share_ref.replace(share_ref); + } + + fn command(&self, command_id: u32, _argument0: u32, _argument1: u32) -> CommandReturn { + match command_id { + EXISTS => crate::command_return::success(), + READ_CO2 => { + if !self.co2_available.get() { + return crate::command_return::failure(ErrorCode::NoSupport); + } + if self.busy.get() { + return crate::command_return::failure(ErrorCode::Busy); + } + + self.busy.set(true); + if let Some(val) = self.upcall_on_read.take() { + self.set_value(val); + } + if let Some((co2_val, _)) = self.upcall_on_tuple_read.get() { + self.set_value(co2_val); + } + + crate::command_return::success() + } + READ_TVOC => { + if !self.tvoc_available.get() { + return crate::command_return::failure(ErrorCode::NoSupport); + } + if self.busy.get() { + return crate::command_return::failure(ErrorCode::Busy); + } + + self.busy.set(true); + if let Some(val) = self.upcall_on_read.take() { + self.set_value(val); + } + if let Some((_, tvoc_val)) = self.upcall_on_tuple_read.take() { + self.set_value(tvoc_val); + } + + crate::command_return::success() + } + _ => crate::command_return::failure(ErrorCode::NoSupport), + } + } +} + +#[cfg(test)] +mod tests; +// ----------------------------------------------------------------------------- +// Driver number and command IDs +// ----------------------------------------------------------------------------- + +const DRIVER_NUM: u32 = 0x60007; + +// Command IDs + +const EXISTS: u32 = 0; +const READ_CO2: u32 = 2; +const READ_TVOC: u32 = 3; diff --git a/unittest/src/fake/air_quality/tests.rs b/unittest/src/fake/air_quality/tests.rs new file mode 100644 index 00000000..ccbf9977 --- /dev/null +++ b/unittest/src/fake/air_quality/tests.rs @@ -0,0 +1,116 @@ +use crate::fake::{self, SyscallDriver}; +use fake::air_quality::*; +use libtock_platform::{share::scope, DefaultConfig, YieldNoWaitReturn}; + +//Test the `command` implementation +#[test] +fn command() { + let driver = AirQuality::new(); + + assert!(driver.command(EXISTS, 0, 0).is_success()); + + driver.set_co2_available(false); + assert_eq!( + driver.command(READ_CO2, 0, 0).get_failure(), + Some(ErrorCode::NoSupport) + ); + driver.set_tvoc_available(false); + assert_eq!( + driver.command(READ_TVOC, 0, 0).get_failure(), + Some(ErrorCode::NoSupport) + ); + + driver.set_co2_available(true); + driver.set_tvoc_available(true); + + assert!(driver.command(READ_CO2, 0, 0).is_success()); + assert_eq!( + driver.command(READ_CO2, 0, 0).get_failure(), + Some(ErrorCode::Busy) + ); + driver.set_value(100); + assert!(driver.command(READ_CO2, 0, 0).is_success()); + driver.set_value(100); + + assert!(driver.command(READ_TVOC, 0, 0).is_success()); + assert_eq!( + driver.command(READ_TVOC, 0, 0).get_failure(), + Some(ErrorCode::Busy) + ); + driver.set_value(100); + assert!(driver.command(READ_TVOC, 0, 0).is_success()); + driver.set_value(100); + + driver.set_value_sync(100); + assert!(driver.command(READ_CO2, 0, 0).is_success()); + assert!(driver.command(READ_TVOC, 0, 0).is_success()); +} + +// Integration test that verifies Temperature works with fake::Kernel and +// libtock_platform::Syscalls. +#[test] +fn kernel_integration() { + use libtock_platform::Syscalls; + let kernel = fake::Kernel::new(); + let driver = AirQuality::new(); + kernel.add_driver(&driver); + + assert!(fake::Syscalls::command(DRIVER_NUM, EXISTS, 0, 0).is_success()); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).get_failure(), + Some(ErrorCode::Busy) + ); + driver.set_value(100); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + driver.set_value(100); + + assert!(fake::Syscalls::command(DRIVER_NUM, READ_TVOC, 0, 0).is_success()); + assert_eq!( + fake::Syscalls::command(DRIVER_NUM, READ_TVOC, 0, 0).get_failure(), + Some(ErrorCode::Busy) + ); + driver.set_value(100); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_TVOC, 0, 0).is_success()); + + let listener = Cell::>::new(None); + scope(|subscribe| { + assert_eq!( + fake::Syscalls::subscribe::<_, _, DefaultConfig, DRIVER_NUM, 0>(subscribe, &listener), + Ok(()) + ); + + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert_eq!(listener.get(), Some((100,))); + + driver.set_value(200); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + assert_eq!(listener.get(), Some((100,))); + + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + driver.set_value(200); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert_eq!(listener.get(), Some((200,))); + + driver.set_value_sync(200); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + + assert!(fake::Syscalls::command(DRIVER_NUM, READ_TVOC, 0, 0).is_success()); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + driver.set_value_sync(200); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + driver.set_value(100); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert_eq!(listener.get(), Some((100,))); + + driver.set_values_sync(100, 200); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_TVOC, 0, 0).is_success()); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::Upcall); + assert!(fake::Syscalls::command(DRIVER_NUM, READ_CO2, 0, 0).is_success()); + assert_eq!(fake::Syscalls::yield_no_wait(), YieldNoWaitReturn::NoUpcall); + }); +} diff --git a/unittest/src/fake/mod.rs b/unittest/src/fake/mod.rs index 2e71b9a3..72d3f5b8 100644 --- a/unittest/src/fake/mod.rs +++ b/unittest/src/fake/mod.rs @@ -10,6 +10,7 @@ //! (e.g. `fake::Console`). mod adc; +mod air_quality; mod alarm; mod ambient_light; mod buttons; @@ -27,6 +28,7 @@ mod syscalls; mod temperature; pub use adc::Adc; +pub use air_quality::AirQuality; pub use alarm::Alarm; pub use ambient_light::AmbientLight; pub use buttons::Buttons;