├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── flash-firmware.rs └── linux.rs ├── release.toml ├── src ├── app_mode.rs ├── boot_mode.rs ├── common_impl.rs ├── lib.rs ├── prelude.rs ├── register_access.rs ├── traits.rs └── types.rs └── tests ├── app_mode.rs ├── boot_mode.rs ├── common └── mod.rs ├── device_errors.rs └── integration.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | env: 5 | RUSTFLAGS: '--deny warnings' 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: [stable, 1.62.0] 14 | TARGET: 15 | - x86_64-unknown-linux-gnu 16 | - x86_64-unknown-linux-musl 17 | - arm-unknown-linux-gnueabi # Raspberry Pi 1 18 | - armv7-unknown-linux-gnueabihf # Raspberry Pi 2, 3, etc 19 | # Bare metal 20 | - thumbv6m-none-eabi 21 | - thumbv7em-none-eabi 22 | - thumbv7em-none-eabihf 23 | - thumbv7m-none-eabi 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@master 28 | with: 29 | toolchain: ${{ matrix.rust }} 30 | targets: ${{ matrix.TARGET }} 31 | 32 | - name: Checkout CI scripts 33 | uses: actions/checkout@v4 34 | with: 35 | repository: 'eldruin/rust-driver-ci-scripts' 36 | ref: 'master' 37 | path: 'ci' 38 | 39 | - run: ./ci/patch-no-std.sh 40 | if: ${{ ! contains(matrix.TARGET, 'x86_64') }} 41 | 42 | - run: cargo build --target=${{ matrix.TARGET }} 43 | 44 | checks: 45 | name: Checks 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | targets: x86_64-unknown-linux-gnu 53 | components: rustfmt 54 | 55 | - run: cargo doc 56 | - run: cargo fmt --all -- --check 57 | 58 | clippy: 59 | name: Clippy 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: 1.85.0 67 | targets: x86_64-unknown-linux-gnu 68 | components: clippy 69 | 70 | - run: cargo clippy --all-targets 71 | 72 | test: 73 | name: Tests 74 | runs-on: ubuntu-latest 75 | strategy: 76 | matrix: 77 | rust: [stable] 78 | TARGET: [x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl] 79 | 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: dtolnay/rust-toolchain@master 83 | with: 84 | toolchain: ${{ matrix.rust }} 85 | targets: ${{ matrix.TARGET }} 86 | 87 | - name: Test 88 | run: cargo test --target=${{ matrix.TARGET }} 89 | 90 | - name: Build examples 91 | run: cargo build --target=${{ matrix.TARGET }} --examples 92 | 93 | coverage: 94 | name: Coverage 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: actions/checkout@v4 98 | - uses: dtolnay/rust-toolchain@stable 99 | 100 | - name: Install cargo-llvm-cov 101 | uses: taiki-e/install-action@cargo-llvm-cov 102 | 103 | - name: Generate code coverage 104 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 105 | 106 | - name: upload to Coveralls 107 | uses: coverallsapp/github-action@master 108 | with: 109 | github-token: ${{ secrets.GITHUB_TOKEN }} 110 | path-to-lcov: './lcov.info' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | .#* 3 | /target/ 4 | Cargo.lock 5 | .gdb_history 6 | 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [Unreleased] - ReleaseDate 10 | 11 | ## [1.0.0] - 2025-03-05 12 | 13 | ### Changed 14 | - [breaking-change] Upgrade to `embedded-hal` `1.0` 15 | - Updated dependencies 16 | - Updated MSRV to 1.62.0 17 | 18 | ## [0.2.0] - 2020-09-10 19 | 20 | This driver is now functionally complete. 21 | 22 | ### Changed 23 | - [breaking-change] Use struct to report device errors. 24 | 25 | ## [0.1.0] - 2020-01-27 26 | 27 | Initial release to crates.io. 28 | 29 | 30 | [Unreleased]: https://github.com/eldruin/embedded-ccs811-rs/compare/v1.0.0...HEAD 31 | [1.0.0]: https://github.com/eldruin/embedded-ccs811-rs/compare/v0.2.0...v1.0.0 32 | [0.2.0]: https://github.com/eldruin/embedded-ccs811-rs/compare/v0.1.0...v0.2.0 33 | [0.1.0]: https://github.com/eldruin/embedded-ccs811-rs/releases/tag/v0.1.0 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embedded-ccs811" 3 | version = "1.0.0" 4 | authors = ["Diego Barrios Romero "] 5 | repository = "https://github.com/eldruin/embedded-ccs811-rs" 6 | license = "MIT OR Apache-2.0" 7 | description = "Platform-agnostic Rust driver for the CCS811 ultra-low power digital gas sensor for monitoring indoor air quality." 8 | readme = "README.md" 9 | keywords = ["gas", "air", "quality", "sensor", "embedded-hal-driver"] 10 | categories = ["embedded", "hardware-support", "no-std"] 11 | homepage = "https://github.com/eldruin/embedded-ccs811-rs" 12 | documentation = "https://docs.rs/embedded-ccs811" 13 | include = [ 14 | "/**/*.rs", 15 | "/Cargo.toml", 16 | "/README.md", 17 | "/CHANGELOG.md", 18 | "/LICENSE-MIT", 19 | "/LICENSE-APACHE", 20 | ] 21 | edition = "2018" 22 | 23 | [dependencies] 24 | embedded-hal = "1.0.0" 25 | nb = "1" 26 | 27 | [dev-dependencies] 28 | embedded-hal-mock = { version = "0.11.1", features = ["eh1"] } 29 | linux-embedded-hal = { version = "0.4", features = ["gpio_cdev"] } 30 | 31 | [profile.release] 32 | lto = true 33 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019-2025 Diego Barrios Romero 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust CCS811 Driver: Ultra-low Power Digital Gas Sensor for Monitoring Indoor Air Quality 2 | 3 | [![crates.io](https://img.shields.io/crates/v/embedded-ccs811.svg)](https://crates.io/crates/embedded-ccs811) 4 | [![Docs](https://docs.rs/embedded-ccs811/badge.svg)](https://docs.rs/embedded-ccs811) 5 | ![Minimum Supported Rust Version](https://img.shields.io/badge/rustc-1.62+-blue.svg) 6 | [![Build Status](https://github.com/eldruin/embedded-ccs811-rs/workflows/Build/badge.svg)](https://github.com/eldruin/embedded-ccs811-rs/actions?query=workflow%3ABuild) 7 | [![Coverage Status](https://coveralls.io/repos/github/eldruin/embedded-ccs811-rs/badge.svg?branch=master)](https://coveralls.io/github/eldruin/embedded-ccs811-rs?branch=master) 8 | 9 | This is a platform-agnostic Rust driver for the CCS811 ultra-low power 10 | digital VOC sensor for monitoring indoor air quality (IAQ) using 11 | the [`embedded-hal`] traits. 12 | 13 | This driver allows you to: 14 | - In application mode: 15 | - Set the measurement mode. See: `set_mode()`. 16 | - Check if there is new data ready. See: `has_data_ready()`. 17 | - Get the algoritm and raw result data. See: `data()`. 18 | - Get the raw data. See: `raw_data()`. 19 | - Get the current baseline. See: `baseline()`. 20 | - Set the baseline. See: `set_baseline()`. 21 | - Set the environment temperature and relative humidity. See: `set_environment()`. 22 | - Set the interrupt mode. See: `set_interrupt_mode()`. 23 | - Set the eCO2 thresholds for interrupts. See: `set_eco2_thresholds()`. 24 | - In boot mode: 25 | - Start application. See: `start_application()`. 26 | - Reset, erase, download and verify new application. See: `update_application()`. 27 | - Erase application. See: `erase_application()`. 28 | - Verify application. See: `verify_application()`. 29 | - Download application. See: `download_application()`. 30 | - In either mode: 31 | - Get the firmware mode. See: `firmware_mode()`. 32 | - Check whether a valid application is loaded. See: `has_valid_app()`. 33 | - Get the hardware ID. See: `hardware_id()`. 34 | - Get the hardware version. See: `hardware_version()`. 35 | - Get the firmware bootloader version. See: `firmware_bootloader_version()`. 36 | - Get the firmware application version. See: `firmware_application_version()`. 37 | - Do a software reset. See: `software_reset()`. 38 | 39 | [Introductory blog post](https://blog.eldruin.com/ccs811-indoor-air-quality-sensor-driver-in-rust) 40 | 41 | ## The device 42 | 43 | The CCS811 is an ultra-low power digital gas sensor solution which 44 | integrates a metal oxide (MOX) gas sensor to detect a wide range of 45 | Volatile Organic Compounds (VOCs) for indoor air quality monitoring 46 | with a microcontroller unit (MCU), which includes an Analog-to-Digital 47 | converter (ADC), and an I²C interface. 48 | 49 | CCS811 is based on ams unique micro-hotplate technology which enables a 50 | highly reliable solution for gas sensors, very fast cycle times and a 51 | significant reduction in average power consumption. 52 | 53 | The integrated MCU manages the sensor driver modes and measurements. 54 | The I²C digital interface significantly simplifies the hardware and 55 | software design, enabling a faster time to market. 56 | 57 | CCS811 supports intelligent algorithms to process raw sensor measurements 58 | to output equivalent total VOC (eTVOC) and equivalent CO2 (eCO2) values, 59 | where the main cause of VOCs is from humans. 60 | 61 | CCS811 supports multiple measurement modes that have been optimized for 62 | low-power consumption during an active sensor measurement and idle mode 63 | extending battery life in portable applications. 64 | 65 | Documentation: 66 | - [Datasheet](https://www.sciosense.com/wp-content/uploads/2020/01/CCS811-Datasheet.pdf) 67 | - [Programming and interfacing guide](https://www.sciosense.com/wp-content/uploads/2020/01/CCS811-Application-Note-Programming-and-interfacing-guide.pdf) 68 | 69 | ## Usage 70 | 71 | To use this driver, import this crate and an `embedded_hal` implementation, 72 | then instantiate the device. 73 | 74 | Please find additional examples using hardware in this repository: [driver-examples] 75 | 76 | [driver-examples]: https://github.com/eldruin/driver-examples 77 | 78 | ```rust 79 | extern crate linux_embedded_hal as hal; 80 | use embedded_ccs811::{prelude::*, Ccs811, MeasurementMode, SlaveAddr}; 81 | use nb::block; 82 | 83 | fn main() { 84 | let dev = hal::I2cdev::new("/dev/i2c-1").unwrap(); 85 | let nwake = hal::Pin::new(17); 86 | let delay = hal::Delay {}; 87 | let address = SlaveAddr::default(); 88 | let sensor = Ccs811::new(dev, address, nwake, delay); 89 | let mut sensor = sensor.start_application().ok().unwrap(); 90 | sensor.set_mode(MeasurementMode::ConstantPower1s).unwrap(); 91 | loop { 92 | let data = block!(sensor.data()).unwrap(); 93 | println!("eCO2: {}, eTVOC: {}", data.eco2, data.etvoc); 94 | } 95 | } 96 | ``` 97 | 98 | ## Support 99 | 100 | For questions, issues, feature requests, and other changes, please file an 101 | [issue in the github project](https://github.com/eldruin/embedded-ccs811-rs/issues). 102 | 103 | ## Minimum Supported Rust Version (MSRV) 104 | 105 | This crate is guaranteed to compile on stable Rust 1.62 and up. It *might* 106 | compile with older versions but that may change in any new patch release. 107 | 108 | ## License 109 | 110 | Licensed under either of 111 | 112 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 113 | http://www.apache.org/licenses/LICENSE-2.0) 114 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 115 | http://opensource.org/licenses/MIT) 116 | 117 | at your option. 118 | 119 | ### Contributing 120 | 121 | Unless you explicitly state otherwise, any contribution intentionally submitted 122 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 123 | be dual licensed as above, without any additional terms or conditions. 124 | 125 | [`embedded-hal`]: https://github.com/rust-embedded/embedded-hal 126 | -------------------------------------------------------------------------------- /examples/flash-firmware.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{prelude::*, Ccs811Awake, Ccs811Device, SlaveAddr}; 2 | use linux_embedded_hal::{Delay, I2cdev}; 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::prelude::*; 6 | use std::path::PathBuf; 7 | 8 | fn main() { 9 | let dev = I2cdev::new("/dev/i2c-1").unwrap(); 10 | let mut delay = Delay {}; 11 | let address = SlaveAddr::default(); 12 | let mut sensor = Ccs811Awake::new(dev, address); 13 | println!("Current status:"); 14 | print_status(&mut sensor); 15 | 16 | let args: Vec = env::args().collect(); 17 | if args.len() != 2 { 18 | panic!( 19 | "Invalid number of arguments.\n\ 20 | The path to the firmware binary file must be provided for flashing.\n\ 21 | Probably called 'CCS811_SW000246_1-00.bin'." 22 | ); 23 | } 24 | let data = read_firmware(&PathBuf::from(&args[1])); 25 | println!("Read firmware file. Length: {} bytes", data.len()); 26 | 27 | println!("Starting update process: Reset, erase, download, verify..."); 28 | let result = sensor.update_application(&data, &mut delay); 29 | match result { 30 | Err(e) => println!("An error occurred: {:?}", e), 31 | Ok(_) => println!("Update was successful!"), 32 | } 33 | 34 | println!("Status:"); 35 | print_status(&mut sensor); 36 | } 37 | 38 | fn print_status>(sensor: &mut DEV) { 39 | let hw_id = sensor.hardware_id().unwrap(); 40 | let hw_ver = sensor.hardware_version().unwrap(); 41 | let fw_boot_ver = sensor.firmware_bootloader_version().unwrap(); 42 | let fw_app_ver = sensor.firmware_application_version().unwrap(); 43 | let valid_app = sensor.has_valid_app().unwrap(); 44 | 45 | println!("Hardware ID: {}, hardware version: {:?}", hw_id, hw_ver); 46 | println!("Firmware boot version: {:?}", fw_boot_ver); 47 | println!("Firmware application version: {:?}", fw_app_ver); 48 | println!("Has valid firmware application: {}", valid_app); 49 | } 50 | 51 | fn read_firmware(path: &PathBuf) -> Vec { 52 | let mut file = File::open(path).expect("Failed to open firmware file"); 53 | let mut data = Vec::::new(); 54 | file.read_to_end(&mut data) 55 | .expect("Failed to read firmware file"); 56 | data 57 | } 58 | -------------------------------------------------------------------------------- /examples/linux.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{prelude::*, Ccs811Awake, MeasurementMode, ModeChangeError, SlaveAddr}; 2 | use linux_embedded_hal::I2cdev; 3 | use nb::block; 4 | 5 | fn main() { 6 | let dev = I2cdev::new("/dev/i2c-1").unwrap(); 7 | let address = SlaveAddr::default(); 8 | let sensor = Ccs811Awake::new(dev, address); 9 | match sensor.start_application() { 10 | Err(ModeChangeError { dev: _, error }) => { 11 | println!("Error during application start: {:?}", error); 12 | } 13 | Ok(mut sensor) => { 14 | sensor.set_mode(MeasurementMode::ConstantPower1s).unwrap(); 15 | loop { 16 | let data = block!(sensor.data()).unwrap(); 17 | println!("eCO2: {}, eTVOC: {}", data.eco2, data.etvoc); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 3 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 4 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 5 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, 6 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/eldruin/{{crate_name}}-rs/compare/{{tag_name}}...HEAD", exactly=1}, 7 | ] 8 | -------------------------------------------------------------------------------- /src/app_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::hal::{delay::DelayNs, digital::OutputPin}; 2 | use crate::{ 3 | hal, mode, register_access::get_errors, AlgorithmResult, BitFlags, Ccs811, Ccs811AppMode, 4 | Ccs811Awake, Error, ErrorAwake, InterruptMode, MeasurementMode, ModeChangeError, Register, 5 | }; 6 | 7 | impl Ccs811AppMode for Ccs811Awake 8 | where 9 | I2C: hal::i2c::I2c, 10 | { 11 | type Error = ErrorAwake; 12 | type ModeChangeError = ModeChangeError, Self>; 13 | type BootModeType = Ccs811Awake; 14 | 15 | fn set_mode(&mut self, mode: MeasurementMode) -> Result<(), Self::Error> { 16 | let idle_mode = self.meas_mode_reg & 0b0000_1100; 17 | let meas_mode = match mode { 18 | MeasurementMode::Idle => idle_mode, 19 | MeasurementMode::ConstantPower1s => idle_mode | (1 << 4), 20 | MeasurementMode::PulseHeating10s => idle_mode | (2 << 4), 21 | MeasurementMode::LowPowerPulseHeating60s => idle_mode | (3 << 4), 22 | MeasurementMode::ConstantPower250ms => idle_mode | (4 << 4), 23 | }; 24 | self.write_register_1byte(Register::MEAS_MODE, meas_mode)?; 25 | self.meas_mode_reg = meas_mode; 26 | Ok(()) 27 | } 28 | 29 | fn has_data_ready(&mut self) -> Result { 30 | let status = self.read_status()?; 31 | Ok((status & BitFlags::DATA_READY) != 0) 32 | } 33 | 34 | fn raw_data(&mut self) -> Result<(u8, u16), Self::Error> { 35 | let data = self.read_register_2bytes(Register::RAW_DATA)?; 36 | Ok(handle_raw_data(data[0], data[1])) 37 | } 38 | 39 | fn data(&mut self) -> nb::Result { 40 | let mut data = [0; 8]; 41 | self.i2c 42 | .write_read(self.address, &[Register::ALG_RESULT_DATA], &mut data) 43 | .map_err(ErrorAwake::I2C)?; 44 | let status = data[4]; 45 | if (status & BitFlags::ERROR) != 0 { 46 | get_errors(data[5]).map_err(ErrorAwake::Device)?; 47 | } else if (status & BitFlags::DATA_READY) == 0 { 48 | return Err(nb::Error::WouldBlock); 49 | } 50 | let raw = handle_raw_data(data[6], data[7]); 51 | Ok(AlgorithmResult { 52 | eco2: (u16::from(data[0]) << 8) | u16::from(data[1]), 53 | etvoc: (u16::from(data[2]) << 8) | u16::from(data[3]), 54 | raw_current: raw.0, 55 | raw_voltage: raw.1, 56 | }) 57 | } 58 | 59 | #[allow(clippy::manual_range_contains)] // avoid creating range with exact floats 60 | fn set_environment( 61 | &mut self, 62 | humidity_percentage: f32, 63 | temperature_celsius: f32, 64 | ) -> Result<(), Self::Error> { 65 | if humidity_percentage < 0.0 66 | || humidity_percentage > 100.0 67 | || temperature_celsius > 254.998_05 68 | { 69 | return Err(ErrorAwake::InvalidInputData); 70 | } 71 | let raw_humidity = get_raw_humidity(humidity_percentage); 72 | let raw_temp = get_raw_temperature(temperature_celsius); 73 | let raw = [ 74 | Register::ENV_DATA, 75 | raw_humidity.0, 76 | raw_humidity.1, 77 | raw_temp.0, 78 | raw_temp.1, 79 | ]; 80 | self.i2c 81 | .write(self.address, &raw) 82 | .map_err(ErrorAwake::I2C)?; 83 | self.check_status_error() 84 | } 85 | 86 | fn baseline(&mut self) -> Result<[u8; 2], Self::Error> { 87 | self.read_register_2bytes(Register::BASELINE) 88 | } 89 | 90 | fn set_baseline(&mut self, baseline: [u8; 2]) -> Result<(), Self::Error> { 91 | self.i2c 92 | .write( 93 | self.address, 94 | &[Register::BASELINE, baseline[0], baseline[1]], 95 | ) 96 | .map_err(ErrorAwake::I2C)?; 97 | self.check_status_error() 98 | } 99 | 100 | fn set_eco2_thresholds( 101 | &mut self, 102 | low_to_medium: u16, 103 | medium_to_high: u16, 104 | ) -> Result<(), Self::Error> { 105 | self.i2c 106 | .write( 107 | self.address, 108 | &[ 109 | Register::THRESHOLDS, 110 | (low_to_medium >> 8) as u8, 111 | low_to_medium as u8, 112 | (medium_to_high >> 8) as u8, 113 | medium_to_high as u8, 114 | ], 115 | ) 116 | .map_err(ErrorAwake::I2C)?; 117 | self.check_status_error() 118 | } 119 | 120 | fn set_interrupt_mode(&mut self, mode: InterruptMode) -> Result<(), Self::Error> { 121 | let int_mask = match mode { 122 | InterruptMode::Disabled => 0, 123 | InterruptMode::OnDataReady => BitFlags::INTERRUPT, 124 | InterruptMode::OnThresholdCrossed => BitFlags::INTERRUPT | BitFlags::THRESH, 125 | }; 126 | let meas_mode = (self.meas_mode_reg & (0b111 << 4)) | int_mask; 127 | self.write_register_1byte(Register::MEAS_MODE, meas_mode)?; 128 | self.meas_mode_reg = meas_mode; 129 | Ok(()) 130 | } 131 | 132 | // Note: is_verifying is false after a reset 133 | fn software_reset(mut self) -> Result { 134 | match self.write_sw_reset() { 135 | Err(e) => Err(ModeChangeError::new(self, e)), 136 | Ok(_) => Ok(Ccs811Awake::create(self.i2c, self.address)), 137 | } 138 | } 139 | } 140 | 141 | fn get_raw_humidity(humidity_percentage: f32) -> (u8, u8) { 142 | get_raw_environment_data(humidity_percentage) 143 | } 144 | 145 | fn get_raw_temperature(temperature_celsius: f32) -> (u8, u8) { 146 | let value = temperature_celsius + 25.0; 147 | if value < 0.0 { 148 | (0, 0) 149 | } else { 150 | get_raw_environment_data(value) 151 | } 152 | } 153 | 154 | fn get_raw_environment_data(value: f32) -> (u8, u8) { 155 | let main = (value as u8) << 1; 156 | let rest = value - f32::from(value as u8); 157 | let rest = (rest * 512.0) as u16; 158 | (main | (((rest & (1 << 8)) >> 8) as u8), rest as u8) 159 | } 160 | 161 | fn handle_raw_data(data0: u8, data1: u8) -> (u8, u16) { 162 | (data1 >> 2, u16::from(data0) | (u16::from(data1 & 0x3) << 8)) 163 | } 164 | 165 | impl Ccs811AppMode for Ccs811 166 | where 167 | I2C: hal::i2c::I2c, 168 | NWAKE: OutputPin, 169 | WAKEDELAY: DelayNs, 170 | { 171 | type Error = Error; 172 | type ModeChangeError = ModeChangeError, Self>; 173 | type BootModeType = Ccs811; 174 | 175 | fn set_mode(&mut self, mode: MeasurementMode) -> Result<(), Self::Error> { 176 | self.on_awaken(|s| s.dev.set_mode(mode)) 177 | } 178 | 179 | fn has_data_ready(&mut self) -> Result { 180 | self.on_awaken(|s| s.dev.has_data_ready()) 181 | } 182 | 183 | fn raw_data(&mut self) -> Result<(u8, u16), Self::Error> { 184 | self.on_awaken(|s| s.dev.raw_data()) 185 | } 186 | 187 | fn data(&mut self) -> nb::Result { 188 | self.on_awaken_nb(|s| s.dev.data()) 189 | } 190 | 191 | fn baseline(&mut self) -> Result<[u8; 2], Self::Error> { 192 | self.on_awaken(|s| s.dev.baseline()) 193 | } 194 | 195 | fn set_baseline(&mut self, baseline: [u8; 2]) -> Result<(), Self::Error> { 196 | self.on_awaken(|s| s.dev.set_baseline(baseline)) 197 | } 198 | 199 | fn set_environment( 200 | &mut self, 201 | humidity_percentage: f32, 202 | temperature_celsius: f32, 203 | ) -> Result<(), Self::Error> { 204 | self.on_awaken(|s| { 205 | s.dev 206 | .set_environment(humidity_percentage, temperature_celsius) 207 | }) 208 | } 209 | 210 | fn set_eco2_thresholds( 211 | &mut self, 212 | low_to_medium: u16, 213 | medium_to_high: u16, 214 | ) -> Result<(), Self::Error> { 215 | self.on_awaken(|s| s.dev.set_eco2_thresholds(low_to_medium, medium_to_high)) 216 | } 217 | 218 | fn set_interrupt_mode(&mut self, mode: InterruptMode) -> Result<(), Self::Error> { 219 | self.on_awaken(|s| s.dev.set_interrupt_mode(mode)) 220 | } 221 | 222 | fn software_reset(self) -> Result { 223 | self.wrap_mode_change(|s| s.software_reset()) 224 | } 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | 231 | #[test] 232 | fn convert_humidity() { 233 | assert_eq!((0, 0), get_raw_humidity(0.0)); 234 | assert_eq!((0x64, 0), get_raw_humidity(50.0)); 235 | assert_eq!((0x61, 0), get_raw_humidity(48.5)); 236 | assert_eq!((0x60, 0x80), get_raw_humidity(48.25)); 237 | assert_eq!((0x60, 0x40), get_raw_humidity(48.125)); 238 | assert_eq!((0x60, 0x20), get_raw_humidity(48.0625)); 239 | assert_eq!((0x60, 0x10), get_raw_humidity(48.03125)); 240 | assert_eq!((0x60, 0x08), get_raw_humidity(48.015_625)); 241 | assert_eq!((0x60, 0x04), get_raw_humidity(48.007_813)); 242 | assert_eq!((0x60, 0x02), get_raw_humidity(48.003_906)); 243 | assert_eq!((0x60, 0x01), get_raw_humidity(48.001_953)); 244 | assert_eq!((0x61, 0xFF), get_raw_humidity(48.998_047)); 245 | } 246 | 247 | #[test] 248 | fn convert_temperature() { 249 | assert_eq!((0, 0), get_raw_temperature(-25.5)); 250 | assert_eq!((0, 0), get_raw_temperature(-25.0)); 251 | assert_eq!((0x64, 0), get_raw_temperature(25.0)); 252 | assert_eq!((0x61, 0), get_raw_temperature(23.5)); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/boot_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::hal::{delay::DelayNs, digital::OutputPin}; 2 | use crate::{ 3 | hal, mode, ActionInProgress, BitFlags, Ccs811, Ccs811Awake, Ccs811BootMode, Ccs811Device, 4 | Error, ErrorAwake, ModeChangeError, Register, 5 | }; 6 | 7 | impl Ccs811BootMode for Ccs811Awake 8 | where 9 | I2C: hal::i2c::I2c, 10 | { 11 | type Error = ErrorAwake; 12 | type ModeChangeError = ModeChangeError; 13 | type TargetType = Ccs811Awake; 14 | 15 | fn start_application(mut self) -> Result { 16 | match self.has_valid_app() { 17 | Err(e) => Err(ModeChangeError::new(self, e)), 18 | Ok(is_valid) => { 19 | if !is_valid { 20 | Err(ModeChangeError::new(self, ErrorAwake::NoValidApp)) 21 | } else { 22 | match self.write_register_no_data(Register::APP_START) { 23 | Err(e) => Err(ModeChangeError::new(self, e)), 24 | Ok(_) => Ok(Ccs811Awake::create(self.i2c, self.address)), 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | fn verify_application(&mut self) -> nb::Result<(), Self::Error> { 32 | let status = self.read_status().map_err(nb::Error::Other)?; 33 | let verified = (status & BitFlags::APP_VERIFY) != 0; 34 | if !verified { 35 | if self.in_progress == ActionInProgress::Verification { 36 | Err(nb::Error::WouldBlock) 37 | } else { 38 | let result = self 39 | .i2c 40 | .write(self.address, &[Register::APP_VERIFY]) 41 | .map_err(ErrorAwake::I2C); 42 | match result { 43 | Ok(_) => { 44 | self.in_progress = ActionInProgress::Verification; 45 | Err(nb::Error::WouldBlock) 46 | } 47 | Err(e) => Err(nb::Error::Other(e)), 48 | } 49 | } 50 | } else { 51 | self.in_progress = ActionInProgress::None; 52 | Ok(()) 53 | } 54 | } 55 | 56 | fn erase_application(&mut self) -> nb::Result<(), Self::Error> { 57 | let status = self.read_status().map_err(nb::Error::Other)?; 58 | let erased = (status & BitFlags::APP_ERASE) != 0; 59 | if !erased { 60 | if self.in_progress == ActionInProgress::Erase { 61 | Err(nb::Error::WouldBlock) 62 | } else { 63 | let result = self 64 | .i2c 65 | .write(self.address, &[Register::APP_ERASE, 0xE7, 0xA7, 0xE6, 0x09]) 66 | .map_err(ErrorAwake::I2C); 67 | match result { 68 | Ok(_) => { 69 | self.in_progress = ActionInProgress::Erase; 70 | Err(nb::Error::WouldBlock) 71 | } 72 | Err(e) => Err(nb::Error::Other(e)), 73 | } 74 | } 75 | } else { 76 | self.in_progress = ActionInProgress::None; 77 | Ok(()) 78 | } 79 | } 80 | 81 | fn download_application( 82 | &mut self, 83 | bin: &[u8], 84 | delay: &mut D, 85 | ) -> Result<(), Self::Error> { 86 | if bin.len() % 8 != 0 { 87 | return Err(ErrorAwake::InvalidInputData); 88 | } 89 | let mut data = [0; 9]; 90 | data[0] = Register::REG_BOOT_APP; 91 | for chunk in bin.chunks(8) { 92 | data[1..].copy_from_slice(chunk); 93 | self.i2c 94 | .write(self.address, &data) 95 | .map_err(ErrorAwake::I2C)?; 96 | delay.delay_ms(50); 97 | } 98 | self.check_status_error() 99 | } 100 | 101 | fn update_application( 102 | &mut self, 103 | bin: &[u8], 104 | delay: &mut D, 105 | ) -> Result<(), Self::Error> { 106 | self.write_sw_reset()?; 107 | delay.delay_ms(20); 108 | loop { 109 | match self.erase_application() { 110 | Err(nb::Error::WouldBlock) => delay.delay_ms(500), 111 | Err(nb::Error::Other(e)) => return Err(e), 112 | Ok(_) => break, 113 | } 114 | } 115 | self.download_application(bin, delay)?; 116 | loop { 117 | match self.verify_application() { 118 | Err(nb::Error::WouldBlock) => delay.delay_ms(70), 119 | Err(nb::Error::Other(e)) => return Err(e), 120 | Ok(_) => break, 121 | } 122 | } 123 | Ok(()) 124 | } 125 | 126 | // Note: is_verifying is false after a reset 127 | fn software_reset(&mut self) -> Result<(), Self::Error> { 128 | self.write_sw_reset() 129 | } 130 | } 131 | 132 | impl Ccs811BootMode 133 | for Ccs811 134 | where 135 | I2C: hal::i2c::I2c, 136 | NWAKE: OutputPin, 137 | WAKEDELAY: DelayNs, 138 | { 139 | type Error = Error; 140 | type ModeChangeError = ModeChangeError; 141 | type TargetType = Ccs811; 142 | 143 | fn start_application(self) -> Result { 144 | self.wrap_mode_change(|s| s.start_application()) 145 | } 146 | 147 | fn verify_application(&mut self) -> nb::Result<(), Self::Error> { 148 | self.on_awaken_nb(|s| s.dev.verify_application()) 149 | } 150 | 151 | fn erase_application(&mut self) -> nb::Result<(), Self::Error> { 152 | self.on_awaken_nb(|s| s.dev.erase_application()) 153 | } 154 | 155 | fn download_application( 156 | &mut self, 157 | bin: &[u8], 158 | delay: &mut D, 159 | ) -> Result<(), Self::Error> { 160 | self.on_awaken(|s| s.dev.download_application(bin, delay)) 161 | } 162 | 163 | fn update_application( 164 | &mut self, 165 | bin: &[u8], 166 | delay: &mut D, 167 | ) -> Result<(), Self::Error> { 168 | self.on_awaken(|s| s.dev.update_application(bin, delay)) 169 | } 170 | 171 | fn software_reset(&mut self) -> Result<(), Self::Error> { 172 | self.on_awaken(|s| s.dev.software_reset()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/common_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::hal::{delay::DelayNs, digital::OutputPin}; 2 | use crate::{ 3 | hal, mode, ActionInProgress, BitFlags, Ccs811, Ccs811Awake, Ccs811Device, Error, ErrorAwake, 4 | FirmwareMode, ModeChangeError, Register, SlaveAddr, 5 | }; 6 | use core::marker::PhantomData; 7 | 8 | impl Ccs811 { 9 | /// Create new instance of the CCS811 device. 10 | /// 11 | /// See `Ccs811Awake` for the case where the nWAKE pin is not used. 12 | pub fn new(i2c: I2C, address: SlaveAddr, n_wake_pin: NWAKE, wake_delay: WAKEDELAY) -> Self { 13 | Self::create(i2c, address.addr(), n_wake_pin, wake_delay) 14 | } 15 | } 16 | 17 | impl Ccs811 { 18 | pub(crate) fn create(i2c: I2C, address: u8, n_wake_pin: NWAKE, wake_delay: WAKEDELAY) -> Self { 19 | Self::from_awake_dev(Ccs811Awake::create(i2c, address), n_wake_pin, wake_delay) 20 | } 21 | 22 | pub(crate) fn from_awake_dev( 23 | dev: Ccs811Awake, 24 | n_wake_pin: NWAKE, 25 | wake_delay: WAKEDELAY, 26 | ) -> Self { 27 | Ccs811 { 28 | dev, 29 | n_wake_pin, 30 | wake_delay, 31 | _mode: PhantomData, 32 | } 33 | } 34 | } 35 | 36 | impl Ccs811Awake { 37 | /// Create new instance of an already awake CCS811 device. 38 | pub fn new(i2c: I2C, address: SlaveAddr) -> Self { 39 | Self::create(i2c, address.addr()) 40 | } 41 | } 42 | 43 | impl Ccs811Awake { 44 | pub(crate) fn create(i2c: I2C, address: u8) -> Self { 45 | Ccs811Awake { 46 | i2c, 47 | address, 48 | meas_mode_reg: 0, 49 | in_progress: ActionInProgress::None, 50 | _mode: PhantomData, 51 | } 52 | } 53 | } 54 | 55 | impl Ccs811Awake 56 | where 57 | I2C: hal::i2c::I2c, 58 | { 59 | /// Destroy driver instance, return I²C bus instance. 60 | pub fn destroy(self) -> I2C { 61 | self.i2c 62 | } 63 | } 64 | impl Ccs811Awake 65 | where 66 | I2C: hal::i2c::I2c, 67 | { 68 | pub(crate) fn write_sw_reset(&mut self) -> Result<(), ErrorAwake> { 69 | self.i2c 70 | .write(self.address, &[Register::SW_RESET, 0x11, 0xE5, 0x72, 0x8A]) 71 | .map_err(ErrorAwake::I2C) 72 | } 73 | } 74 | 75 | impl Ccs811 76 | where 77 | I2C: hal::i2c::I2c, 78 | NWAKE: OutputPin, 79 | WAKEDELAY: DelayNs, 80 | { 81 | /// Destroy driver instance, return I²C bus, nWAKE pin 82 | /// and wake delay instances. 83 | pub fn destroy(self) -> (I2C, NWAKE, WAKEDELAY) { 84 | (self.dev.destroy(), self.n_wake_pin, self.wake_delay) 85 | } 86 | 87 | pub(crate) fn on_awaken(&mut self, f: F) -> Result> 88 | where 89 | F: FnOnce(&mut Self) -> Result>, 90 | { 91 | self.n_wake_pin.set_low().map_err(Error::Pin)?; 92 | self.wake_delay.delay_us(50); 93 | let result = match f(self) { 94 | Ok(v) => Ok(v), 95 | Err(e) => Err(e.into()), 96 | }; 97 | self.n_wake_pin.set_high().map_err(Error::Pin)?; 98 | self.wake_delay.delay_us(20); 99 | result 100 | } 101 | 102 | pub(crate) fn on_awaken_nb(&mut self, f: F) -> nb::Result> 103 | where 104 | F: FnOnce(&mut Self) -> nb::Result>, 105 | { 106 | self.n_wake_pin 107 | .set_low() 108 | .map_err(Error::Pin) 109 | .map_err(nb::Error::Other)?; 110 | self.wake_delay.delay_us(50); 111 | let result = match f(self) { 112 | Ok(v) => Ok(v), 113 | Err(nb::Error::Other(e)) => Err(nb::Error::Other(e.into())), 114 | Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock), 115 | }; 116 | self.n_wake_pin 117 | .set_high() 118 | .map_err(Error::Pin) 119 | .map_err(nb::Error::Other)?; 120 | self.wake_delay.delay_us(20); 121 | result 122 | } 123 | 124 | // Note: defining a type for the result would require inherent 125 | // associated items: https://github.com/rust-lang/rust/issues/8995 126 | // Note 2: is_verifying is always false after a mode change 127 | #[allow(clippy::type_complexity)] 128 | pub(crate) fn wrap_mode_change( 129 | mut self, 130 | f: F, 131 | ) -> Result, ModeChangeError, Self>> 132 | where 133 | F: FnOnce( 134 | Ccs811Awake, 135 | ) -> Result< 136 | Ccs811Awake, 137 | ModeChangeError, Ccs811Awake>, 138 | >, 139 | { 140 | if let Err(e) = self.n_wake_pin.set_low() { 141 | return Err(ModeChangeError::new(self, Error::Pin(e))); 142 | } 143 | self.wake_delay.delay_us(50); 144 | let Ccs811 { 145 | dev, 146 | mut n_wake_pin, 147 | mut wake_delay, 148 | .. 149 | } = self; 150 | let result = f(dev); 151 | if let Err(e) = n_wake_pin.set_high() { 152 | return match result { 153 | Ok(Ccs811Awake { i2c, address, .. }) => Err(ModeChangeError { 154 | dev: Ccs811::create(i2c, address, n_wake_pin, wake_delay), 155 | error: Error::Pin(e), 156 | }), 157 | Err(ModeChangeError { dev, error }) => Err(ModeChangeError { 158 | dev: Ccs811::from_awake_dev(dev, n_wake_pin, wake_delay), 159 | error: error.into(), 160 | }), 161 | }; 162 | } 163 | wake_delay.delay_us(20); 164 | match result { 165 | Ok(dev) => Ok(Ccs811::from_awake_dev(dev, n_wake_pin, wake_delay)), 166 | Err(ModeChangeError { dev, error }) => Err(ModeChangeError { 167 | dev: Ccs811::from_awake_dev(dev, n_wake_pin, wake_delay), 168 | error: error.into(), 169 | }), 170 | } 171 | } 172 | } 173 | 174 | impl Ccs811Device for Ccs811Awake 175 | where 176 | I2C: hal::i2c::I2c, 177 | { 178 | type Error = ErrorAwake; 179 | 180 | fn firmware_mode(&mut self) -> Result { 181 | let status = self.read_status()?; 182 | let mode = if (status & BitFlags::FW_MODE) != 0 { 183 | FirmwareMode::Application 184 | } else { 185 | FirmwareMode::Boot 186 | }; 187 | Ok(mode) 188 | } 189 | 190 | fn has_valid_app(&mut self) -> Result { 191 | let status = self.read_status()?; 192 | Ok((status & BitFlags::APP_VALID) != 0) 193 | } 194 | 195 | fn hardware_id(&mut self) -> Result { 196 | self.read_register_1byte(Register::HW_ID) 197 | } 198 | 199 | fn hardware_version(&mut self) -> Result<(u8, u8), Self::Error> { 200 | let version = self.read_register_1byte(Register::HW_VERSION)?; 201 | Ok(((version & 0xF0) >> 4, version & 0xF)) 202 | } 203 | 204 | fn firmware_bootloader_version(&mut self) -> Result<(u8, u8, u8), Self::Error> { 205 | let version = self.read_register_2bytes(Register::FW_BOOT_VERSION)?; 206 | Ok(((version[0] & 0xF0) >> 4, version[0] & 0xF, version[1])) 207 | } 208 | 209 | fn firmware_application_version(&mut self) -> Result<(u8, u8, u8), Self::Error> { 210 | let version = self.read_register_2bytes(Register::FW_APP_VERSION)?; 211 | Ok(((version[0] & 0xF0) >> 4, version[0] & 0xF, version[1])) 212 | } 213 | } 214 | 215 | impl Ccs811Device for Ccs811 216 | where 217 | I2C: hal::i2c::I2c, 218 | NWAKE: OutputPin, 219 | WAKEDELAY: DelayNs, 220 | { 221 | type Error = Error; 222 | 223 | fn firmware_mode(&mut self) -> Result { 224 | self.on_awaken(|s| s.dev.firmware_mode()) 225 | } 226 | 227 | fn has_valid_app(&mut self) -> Result { 228 | self.on_awaken(|s| s.dev.has_valid_app()) 229 | } 230 | 231 | fn hardware_id(&mut self) -> Result { 232 | self.on_awaken(|s| s.dev.hardware_id()) 233 | } 234 | 235 | fn hardware_version(&mut self) -> Result<(u8, u8), Self::Error> { 236 | self.on_awaken(|s| s.dev.hardware_version()) 237 | } 238 | 239 | fn firmware_bootloader_version(&mut self) -> Result<(u8, u8, u8), Self::Error> { 240 | self.on_awaken(|s| s.dev.firmware_bootloader_version()) 241 | } 242 | 243 | fn firmware_application_version(&mut self) -> Result<(u8, u8, u8), Self::Error> { 244 | self.on_awaken(|s| s.dev.firmware_application_version()) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is a platform agnostic Rust driver for the CCS811 ultra-low power 2 | //! digital VOC sensor for monitoring indoor air quality (IAQ) using 3 | //! the [`embedded-hal`] traits. 4 | //! 5 | //! [`embedded-hal`]: https://github.com/rust-embedded/embedded-hal 6 | //! 7 | //! This driver allows you to: 8 | //! - In application mode: 9 | //! - Set the measurement mode. See: [`set_mode()`]. 10 | //! - Check if there is new data ready. See: [`has_data_ready()`]. 11 | //! - Get the algoritm and raw result data. See: [`data()`]. 12 | //! - Get the raw data. See: [`raw_data()`]. 13 | //! - Get the current baseline. See: [`baseline()`]. 14 | //! - Set the baseline. See: [`set_baseline()`]. 15 | //! - Set the environment temperature and relative humidity. See: [`set_environment()`]. 16 | //! - Set the interrupt mode. See: [`set_interrupt_mode()`]. 17 | //! - Set the eCO2 thresholds for interrupts. See: [`set_eco2_thresholds()`]. 18 | //! - In boot mode: 19 | //! - Start application. See: [`start_application()`]. 20 | //! - Reset, erase, download and verify new application. See: [`update_application()`]. 21 | //! - Erase application. See: [`erase_application()`]. 22 | //! - Verify application. See: [`verify_application()`]. 23 | //! - Download application. See: [`download_application()`]. 24 | //! - In either mode: 25 | //! - Get the firmware mode. See: [`firmware_mode()`]. 26 | //! - Check whether a valid application is loaded. See: [`has_valid_app()`]. 27 | //! - Get the hardware ID. See: [`hardware_id()`]. 28 | //! - Get the hardware version. See: [`hardware_version()`]. 29 | //! - Get the firmware bootloader version. See: [`firmware_bootloader_version()`]. 30 | //! - Get the firmware application version. See: [`firmware_application_version()`]. 31 | //! - Do a software reset. See: [`software_reset()`]. 32 | //! 33 | //! [`set_mode()`]: trait.Ccs811AppMode.html#tymethod.set_mode 34 | //! [`has_data_ready()`]: trait.Ccs811AppMode.html#tymethod.has_data_ready 35 | //! [`data()`]: trait.Ccs811AppMode.html#tymethod.data 36 | //! [`raw_data()`]: trait.Ccs811AppMode.html#tymethod.raw_data 37 | //! [`baseline()`]: trait.Ccs811AppMode.html#tymethod.baseline 38 | //! [`set_baseline()`]: trait.Ccs811AppMode.html#tymethod.set_baseline 39 | //! [`set_environment()`]: trait.Ccs811AppMode.html#tymethod.set_environment 40 | //! [`set_interrupt_mode()`]: trait.Ccs811AppMode.html#tymethod.set_interrupt_mode 41 | //! [`set_eco2_thresholds()`]: trait.Ccs811AppMode.html#tymethod.set_eco2_thresholds 42 | //! [`start_application()`]: trait.Ccs811BootMode.html#tymethod.start_application 43 | //! [`update_application()`]: trait.Ccs811BootMode.html#tymethod.update_application 44 | //! [`erase_application()`]: trait.Ccs811BootMode.html#tymethod.erase_application 45 | //! [`verify_application()`]: trait.Ccs811BootMode.html#tymethod.verify_application 46 | //! [`download_application()`]: trait.Ccs811BootMode.html#tymethod.download_application 47 | //! [`firmware_mode()`]: trait.Ccs811Device.html#tymethod.firmware_mode 48 | //! [`has_valid_app()`]: trait.Ccs811Device.html#tymethod.has_valid_app 49 | //! [`hardware_id()`]: trait.Ccs811Device.html#tymethod.hardware_id 50 | //! [`hardware_version()`]: trait.Ccs811Device.html#tymethod.hardware_version 51 | //! [`firmware_bootloader_version()`]: trait.Ccs811Device.html#tymethod.firmware_bootloader_version 52 | //! [`firmware_application_version()`]: trait.Ccs811Device.html#tymethod.firmware_application_version 53 | //! [`software_reset()`]: trait.Ccs811Device.html#tymethod.software_reset 54 | //! 55 | //! [Introductory blog post](https://blog.eldruin.com/ccs811-indoor-air-quality-sensor-driver-in-rust) 56 | //! 57 | //! ## The device 58 | //! 59 | //! The CCS811 is an ultra-low power digital gas sensor solution which 60 | //! integrates a metal oxide (MOX) gas sensor to detect a wide range of 61 | //! Volatile Organic Compounds (VOCs) for indoor air quality monitoring 62 | //! with a microcontroller unit (MCU), which includes an Analog-to-Digital 63 | //! converter (ADC), and an I²C interface. 64 | //! 65 | //! CCS811 is based on ams unique micro-hotplate technology which enables a 66 | //! highly reliable solution for gas sensors, very fast cycle times and a 67 | //! significant reduction in average power consumption. 68 | //! 69 | //! The integrated MCU manages the sensor driver modes and measurements. 70 | //! The I²C digital interface significantly simplifies the hardware and 71 | //! software design, enabling a faster time to market. 72 | //! 73 | //! CCS811 supports intelligent algorithms to process raw sensor measurements 74 | //! to output equivalent total VOC (eTVOC) and equivalent CO2 (eCO2) values, 75 | //! where the main cause of VOCs is from humans. 76 | //! 77 | //! CCS811 supports multiple measurement modes that have been optimized for 78 | //! low-power consumption during an active sensor measurement and idle mode 79 | //! extending battery life in portable applications. 80 | //! 81 | //! Documentation: 82 | //! - [Datasheet](https://www.sciosense.com/wp-content/uploads/2020/01/CCS811-Datasheet.pdf) 83 | //! - [Programming and interfacing guide](https://www.sciosense.com/wp-content/uploads/2020/01/CCS811-Application-Note-Programming-and-interfacing-guide.pdf) 84 | //! 85 | //! ## Usage examples (see also examples folder) 86 | //! 87 | //! To use this driver, import this crate and an `embedded_hal` implementation, 88 | //! then instantiate the appropriate device. 89 | //! 90 | //! The CCS811 can be placed in sleep and woken up only for communication. 91 | //! This driver provides two structures: `Ccs811Awake` and `Ccs811` depeding 92 | //! on the waking state. 93 | //! 94 | //! The `Ccs811Awake` assumes an awake device and handles only the I2C communication. 95 | //! This can be used when the waking up and sleep of the device is handled 96 | //! manually. 97 | //! Additionally a wrapper `Ccs811` is provided, which handles waking up 98 | //! the device before each operation and putting it to sleep afterwards. 99 | //! 100 | //! Please find additional examples using hardware in this repository: [driver-examples] 101 | //! 102 | //! [driver-examples]: https://github.com/eldruin/driver-examples 103 | //! 104 | //! ### Start the application and take measurements 105 | //! 106 | //! ```no_run 107 | //! use linux_embedded_hal::{I2cdev, CdevPin, Delay}; 108 | //! use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags}; 109 | //! use embedded_ccs811::{prelude::*, Ccs811, SlaveAddr, MeasurementMode}; 110 | //! use nb::block; 111 | //! 112 | //! let dev = I2cdev::new("/dev/i2c-1").unwrap(); 113 | //! let mut chip = Chip::new("/dev/gpiochip0").unwrap(); 114 | //! let handle = chip.get_line(17).unwrap() 115 | //! .request(LineRequestFlags::OUTPUT, 0, "output").unwrap(); 116 | //! let nwake = CdevPin::new(handle).unwrap(); 117 | //! let delay = Delay {}; 118 | //! let address = SlaveAddr::default(); 119 | //! let sensor = Ccs811::new(dev, address, nwake, delay); 120 | //! let mut sensor = sensor.start_application().ok().unwrap(); 121 | //! sensor.set_mode(MeasurementMode::ConstantPower1s).unwrap(); 122 | //! loop { 123 | //! let data = block!(sensor.data()).unwrap(); 124 | //! println!("eCO2: {}, eTVOC: {}", data.eco2, data.etvoc); 125 | //! } 126 | //! ``` 127 | //! 128 | //! ### Save and restore the baseline 129 | //! 130 | //! ```no_run 131 | //! use linux_embedded_hal::I2cdev; 132 | //! use embedded_ccs811::{prelude::*, Ccs811Awake, SlaveAddr}; 133 | //! 134 | //! let dev = I2cdev::new("/dev/i2c-1").unwrap(); 135 | //! let address = SlaveAddr::default(); 136 | //! let sensor = Ccs811Awake::new(dev, address); 137 | //! let mut sensor = sensor.start_application().ok().unwrap(); 138 | //! let baseline = sensor.baseline().unwrap(); 139 | //! // ... 140 | //! sensor.set_baseline(baseline).unwrap(); 141 | //! ``` 142 | //! 143 | //! ### Set the environment temperature and relative humidity 144 | //! 145 | //! ```no_run 146 | //! use linux_embedded_hal::I2cdev; 147 | //! use embedded_ccs811::{prelude::*, Ccs811Awake, SlaveAddr}; 148 | //! 149 | //! let dev = I2cdev::new("/dev/i2c-1").unwrap(); 150 | //! let address = SlaveAddr::default(); 151 | //! let sensor = Ccs811Awake::new(dev, address); 152 | //! let mut sensor = sensor.start_application().ok().unwrap(); 153 | //! let temp_c = 25.0; 154 | //! let rel_humidity = 50.0; 155 | //! sensor.set_environment(rel_humidity, temp_c).unwrap(); 156 | //! ``` 157 | //! 158 | //! ### Set the eCO2 thresholds and configure interrupts 159 | //! 160 | //! Only generate an interrupt when the thresholds are crossed. 161 | //! 162 | //! ```no_run 163 | //! use linux_embedded_hal::I2cdev; 164 | //! use embedded_ccs811::{prelude::*, Ccs811Awake, SlaveAddr, InterruptMode, MeasurementMode}; 165 | //! 166 | //! let dev = I2cdev::new("/dev/i2c-1").unwrap(); 167 | //! let address = SlaveAddr::default(); 168 | //! let sensor = Ccs811Awake::new(dev, address); 169 | //! let mut sensor = sensor.start_application().ok().unwrap(); 170 | //! sensor.set_eco2_thresholds(1500, 2500).unwrap(); 171 | //! sensor.set_interrupt_mode(InterruptMode::OnThresholdCrossed).unwrap(); 172 | //! sensor.set_mode(MeasurementMode::ConstantPower1s).unwrap(); 173 | //! ``` 174 | //! 175 | //! ### Get hardware and firmware information 176 | //! 177 | //! ```no_run 178 | //! use linux_embedded_hal::I2cdev; 179 | //! use embedded_ccs811::{prelude::*, Ccs811Awake, SlaveAddr}; 180 | //! 181 | //! let dev = I2cdev::new("/dev/i2c-1").unwrap(); 182 | //! let address = SlaveAddr::default(); 183 | //! let mut sensor = Ccs811Awake::new(dev, address); 184 | //! let hw_id = sensor.hardware_id().unwrap(); 185 | //! let hw_ver = sensor.hardware_version().unwrap(); 186 | //! let fw_boot_ver = sensor.firmware_bootloader_version().unwrap(); 187 | //! let fw_app_ver = sensor.firmware_application_version().unwrap(); 188 | //! println!( 189 | //! "HW ID: {}, HW version: {:#?}, FW bootloader version: {:#?}, FW app version: {:#?}", 190 | //! hw_id, hw_ver, fw_boot_ver, fw_app_ver 191 | //! ); 192 | //! ``` 193 | 194 | #![deny(unsafe_code, missing_docs)] 195 | #![no_std] 196 | 197 | extern crate embedded_hal as hal; 198 | use core::marker::PhantomData; 199 | 200 | mod common_impl; 201 | pub mod prelude; 202 | mod register_access; 203 | use crate::register_access::{BitFlags, Register}; 204 | mod app_mode; 205 | mod boot_mode; 206 | mod traits; 207 | pub use crate::traits::{Ccs811AppMode, Ccs811BootMode, Ccs811Device}; 208 | mod types; 209 | pub use crate::types::{ 210 | AlgorithmResult, DeviceErrors, Error, ErrorAwake, FirmwareMode, InterruptMode, MeasurementMode, 211 | ModeChangeError, SlaveAddr, 212 | }; 213 | pub use nb; 214 | 215 | /// CCS811 device driver 216 | /// 217 | /// Convenience wrapper arount `Ccs811Awake` which handles waking up the device on each operation. 218 | #[derive(Debug)] 219 | pub struct Ccs811 { 220 | dev: Ccs811Awake, 221 | n_wake_pin: NWAKE, 222 | wake_delay: WAKEDELAY, 223 | _mode: PhantomData, 224 | } 225 | 226 | /// Already awake CCS811 device driver 227 | /// 228 | /// This can be used when the nWAKE pin is connected directly to GND or when 229 | /// handling the device waking manually instead of using the `Ccs811` wrapper type. 230 | #[derive(Debug)] 231 | pub struct Ccs811Awake { 232 | /// The concrete I²C device implementation. 233 | i2c: I2C, 234 | address: u8, 235 | meas_mode_reg: u8, 236 | in_progress: ActionInProgress, 237 | _mode: PhantomData, 238 | } 239 | 240 | #[derive(Debug, PartialEq)] 241 | enum ActionInProgress { 242 | None, 243 | Verification, 244 | Erase, 245 | } 246 | 247 | /// Mode marker 248 | pub mod mode { 249 | /// Boot mode 250 | pub struct Boot(()); 251 | /// App mode 252 | pub struct App(()); 253 | } 254 | 255 | mod private { 256 | use super::{mode, Ccs811, Ccs811Awake}; 257 | pub trait Sealed {} 258 | 259 | impl Sealed for mode::Boot {} 260 | impl Sealed for mode::App {} 261 | impl Sealed for Ccs811 {} 262 | impl Sealed for Ccs811Awake {} 263 | } 264 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! The prelude is a collection of all the traits for the CCS811 in this crate 2 | //! 3 | //! The traits have been renamed to avoid collisions with other items when 4 | //! performing a glob import. 5 | 6 | pub use crate::Ccs811AppMode as _ccs811_ccs811appmode; 7 | pub use crate::Ccs811BootMode as _ccs811_ccs811bootmode; 8 | pub use crate::Ccs811Device as _ccs811_ccs811device; 9 | -------------------------------------------------------------------------------- /src/register_access.rs: -------------------------------------------------------------------------------- 1 | use crate::{hal, Ccs811Awake, DeviceErrors, ErrorAwake}; 2 | 3 | pub(crate) struct Register {} 4 | impl Register { 5 | pub const STATUS: u8 = 0x00; 6 | pub const MEAS_MODE: u8 = 0x01; 7 | pub const ALG_RESULT_DATA: u8 = 0x02; 8 | pub const RAW_DATA: u8 = 0x03; 9 | pub const ENV_DATA: u8 = 0x05; 10 | pub const THRESHOLDS: u8 = 0x10; 11 | pub const BASELINE: u8 = 0x11; 12 | pub const HW_ID: u8 = 0x20; 13 | pub const HW_VERSION: u8 = 0x21; 14 | pub const FW_BOOT_VERSION: u8 = 0x23; 15 | pub const FW_APP_VERSION: u8 = 0x24; 16 | pub const ERROR_ID: u8 = 0xE0; 17 | pub const APP_ERASE: u8 = 0xF1; 18 | pub const REG_BOOT_APP: u8 = 0xF2; 19 | pub const APP_VERIFY: u8 = 0xF3; 20 | pub const APP_START: u8 = 0xF4; 21 | pub const SW_RESET: u8 = 0xFF; 22 | } 23 | 24 | pub(crate) struct BitFlags {} 25 | impl BitFlags { 26 | pub const DATA_READY: u8 = 1 << 3; 27 | pub const APP_VALID: u8 = 1 << 4; 28 | pub const APP_VERIFY: u8 = 1 << 5; 29 | pub const APP_ERASE: u8 = 1 << 6; 30 | pub const FW_MODE: u8 = 1 << 7; 31 | pub const ERROR: u8 = 1; 32 | pub const WRITE_REG_INVALID: u8 = 1; 33 | pub const READ_REG_INVALID: u8 = 1 << 1; 34 | pub const MEASMODE_INVALID: u8 = 1 << 2; 35 | pub const MAX_RESISTANCE: u8 = 1 << 3; 36 | pub const HEATER_FAULT: u8 = 1 << 4; 37 | pub const HEATER_SUPPLY: u8 = 1 << 5; 38 | pub const INTERRUPT: u8 = 1 << 3; 39 | pub const THRESH: u8 = 1 << 2; 40 | } 41 | 42 | impl Ccs811Awake 43 | where 44 | I2C: hal::i2c::I2c, 45 | { 46 | pub(crate) fn check_status_error(&mut self) -> Result<(), ErrorAwake> { 47 | self.read_status().map(drop) 48 | } 49 | 50 | pub(crate) fn read_status(&mut self) -> Result> { 51 | let mut data = [0]; 52 | self.i2c 53 | .write_read(self.address, &[Register::STATUS], &mut data) 54 | .map_err(ErrorAwake::I2C)?; 55 | let status = data[0]; 56 | if (status & BitFlags::ERROR) != 0 { 57 | self.i2c 58 | .write_read(self.address, &[Register::ERROR_ID], &mut data) 59 | .map_err(ErrorAwake::I2C)?; 60 | get_errors(data[0]).map_err(ErrorAwake::Device)?; 61 | } 62 | Ok(status) 63 | } 64 | 65 | pub(crate) fn read_register_1byte(&mut self, register: u8) -> Result> { 66 | let mut data = [0]; 67 | self.read_register(register, &mut data).and(Ok(data[0])) 68 | } 69 | 70 | pub(crate) fn read_register_2bytes(&mut self, register: u8) -> Result<[u8; 2], ErrorAwake> { 71 | let mut data = [0; 2]; 72 | self.read_register(register, &mut data).and(Ok(data)) 73 | } 74 | 75 | pub(crate) fn read_register( 76 | &mut self, 77 | register: u8, 78 | data: &mut [u8], 79 | ) -> Result<(), ErrorAwake> { 80 | self.i2c 81 | .write_read(self.address, &[register], data) 82 | .map_err(ErrorAwake::I2C)?; 83 | self.check_status_error() 84 | } 85 | 86 | pub(crate) fn write_register_no_data(&mut self, register: u8) -> Result<(), ErrorAwake> { 87 | self.i2c 88 | .write(self.address, &[register]) 89 | .map_err(ErrorAwake::I2C)?; 90 | self.check_status_error() 91 | } 92 | 93 | pub(crate) fn write_register_1byte( 94 | &mut self, 95 | register: u8, 96 | data: u8, 97 | ) -> Result<(), ErrorAwake> { 98 | self.i2c 99 | .write(self.address, &[register, data]) 100 | .map_err(ErrorAwake::I2C)?; 101 | self.check_status_error() 102 | } 103 | } 104 | 105 | pub(crate) fn get_errors(error_id: u8) -> Result<(), DeviceErrors> { 106 | let mut has_error = false; 107 | let mut errors = DeviceErrors::default(); 108 | if (error_id & BitFlags::WRITE_REG_INVALID) != 0 { 109 | errors.invalid_register_write = true; 110 | has_error = true; 111 | } 112 | if (error_id & BitFlags::READ_REG_INVALID) != 0 { 113 | errors.invalid_register_read = true; 114 | has_error = true; 115 | } 116 | if (error_id & BitFlags::MEASMODE_INVALID) != 0 { 117 | errors.invalid_measurement = true; 118 | has_error = true; 119 | } 120 | if (error_id & BitFlags::MAX_RESISTANCE) != 0 { 121 | errors.max_resistance = true; 122 | has_error = true; 123 | } 124 | if (error_id & BitFlags::HEATER_FAULT) != 0 { 125 | errors.heater_fault = true; 126 | has_error = true; 127 | } 128 | if (error_id & BitFlags::HEATER_SUPPLY) != 0 { 129 | errors.heater_supply = true; 130 | has_error = true; 131 | } 132 | if has_error { 133 | Err(errors) 134 | } else { 135 | Ok(()) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::{private, AlgorithmResult, FirmwareMode, InterruptMode, MeasurementMode}; 2 | use embedded_hal::delay::DelayNs; 3 | 4 | /// General CCS811 methods available in either mode 5 | pub trait Ccs811Device: private::Sealed { 6 | /// Error type 7 | type Error; 8 | 9 | /// Get the firmware mode. 10 | fn firmware_mode(&mut self) -> Result; 11 | 12 | /// Check if a valid application firmware is loaded. 13 | fn has_valid_app(&mut self) -> Result; 14 | 15 | /// Get the hardware ID (0x81 for the CCS81x family of devices) 16 | fn hardware_id(&mut self) -> Result; 17 | 18 | /// Get the hardware version (major, minor) ((1,X) for the CCS81x family of devices) 19 | fn hardware_version(&mut self) -> Result<(u8, u8), Self::Error>; 20 | 21 | /// Get the firmware bootloader verion (major, minor, trivial) 22 | fn firmware_bootloader_version(&mut self) -> Result<(u8, u8, u8), Self::Error>; 23 | 24 | /// Get the firmware application verion (major, minor, trivial) 25 | fn firmware_application_version(&mut self) -> Result<(u8, u8, u8), Self::Error>; 26 | } 27 | 28 | /// Methods available when on application mode 29 | pub trait Ccs811AppMode: private::Sealed { 30 | /// Error type 31 | type Error; 32 | /// Boot/App mode change error 33 | type ModeChangeError; 34 | /// Boot mode type 35 | type BootModeType; 36 | 37 | /// Set the measurement mode 38 | /// 39 | /// NOTE: When changing to a new mode with a lower sample rate, 40 | /// place the device in `Idle` mode for at least 10 minutes before 41 | /// enabling the new mode. 42 | fn set_mode(&mut self, mode: MeasurementMode) -> Result<(), Self::Error>; 43 | 44 | /// Check if there is a new data sample ready. 45 | fn has_data_ready(&mut self) -> Result; 46 | 47 | /// Get the algorithm results data. 48 | /// 49 | /// Returns a tuple containing the current and voltage through the sensor in 50 | /// the format: (current, voltage). 51 | /// The current is a value between 0uA and 63uA. 52 | /// The voltage contains the value as computed in the ADC. (1023 = 1.65V) 53 | fn data(&mut self) -> nb::Result; 54 | 55 | /// Get the raw sensor data. 56 | /// 57 | /// Returns a tuple containing the current and voltage through the sensor in 58 | /// the format: (current, voltage). 59 | /// The current is a value between 0uA and 63uA. 60 | /// The voltage contains the value as computed in the ADC. (1023 = 1.65V) 61 | fn raw_data(&mut self) -> Result<(u8, u16), Self::Error>; 62 | 63 | /// Get the current baseline 64 | fn baseline(&mut self) -> Result<[u8; 2], Self::Error>; 65 | 66 | /// Set the baseline 67 | fn set_baseline(&mut self, baseline: [u8; 2]) -> Result<(), Self::Error>; 68 | 69 | /// Set the environment temperature and relative humidity. 70 | /// 71 | /// The humidity must be provided as percentage: [0.0..100.0]. 72 | /// The temperature must be provided in Celsius. (Theoretical max: 254.99805ºC) 73 | fn set_environment( 74 | &mut self, 75 | humidity_percentage: f32, 76 | temperature_celsius: f32, 77 | ) -> Result<(), Self::Error>; 78 | 79 | /// Configure the interrupt generation. 80 | fn set_interrupt_mode(&mut self, mode: InterruptMode) -> Result<(), Self::Error>; 81 | 82 | /// Set the eCO2 threshold values for interrupt generation (in ppm). 83 | /// 84 | /// An interrupt will be asserted if the value moved from the current 85 | /// range by 50 ppm. 86 | fn set_eco2_thresholds( 87 | &mut self, 88 | low_to_medium: u16, 89 | medium_to_high: u16, 90 | ) -> Result<(), Self::Error>; 91 | 92 | /// Restart the device in boot mode. 93 | /// 94 | /// 2ms should be waited before doing any other operation. 95 | fn software_reset(self) -> Result; 96 | } 97 | 98 | /// Methods available when on boot mode 99 | pub trait Ccs811BootMode: private::Sealed { 100 | /// Error type 101 | type Error; 102 | /// Boot/App mode change error 103 | type ModeChangeError; 104 | /// Application mode type 105 | type TargetType; 106 | 107 | /// Start application mode 108 | /// 109 | /// NOTE: after this call 1ms must be waited before sending application commands. 110 | fn start_application(self) -> Result; 111 | 112 | /// Reset, erase, download new application and verify it in one step. 113 | /// 114 | /// This resets the device via a software reset, erases the current application, 115 | /// flashes the new binary and verifies it. This takes at least 572ms + 50ms * (bin_size/8). 116 | /// Returns `Error::InvalidInputData` if the input binary lengh is not multiple of 8. 117 | fn update_application( 118 | &mut self, 119 | bin: &[u8], 120 | delay: &mut D, 121 | ) -> Result<(), Self::Error>; 122 | 123 | /// Verify application. 124 | /// 125 | /// NOTE: After the first call, 70ms must be waited before calling again to 126 | /// poll until completion. 127 | fn verify_application(&mut self) -> nb::Result<(), Self::Error>; 128 | 129 | /// Erase application. 130 | /// 131 | /// NOTE: After the first call, 500ms must be waited before calling again to 132 | /// poll until completion. 133 | fn erase_application(&mut self) -> nb::Result<(), Self::Error>; 134 | 135 | /// Download new application. 136 | /// 137 | /// Returns `Error::InvalidInputData` if the input binary lengh is not multiple of 8. 138 | /// This takes at least 50ms * (bin_size/8). 139 | fn download_application( 140 | &mut self, 141 | bin: &[u8], 142 | delay: &mut D, 143 | ) -> Result<(), Self::Error>; 144 | 145 | /// Restart the device in boot mode. 146 | /// 147 | /// 2ms should be waited before doing any other operation. 148 | fn software_reset(&mut self) -> Result<(), Self::Error>; 149 | } 150 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use core::convert::From; 2 | 3 | /// All possible errors generated when using the `Ccs811` type. 4 | #[derive(Debug)] 5 | pub enum Error { 6 | /// I²C bus error 7 | I2C(CommE), 8 | /// nWAKE pin set error 9 | Pin(PinE), 10 | /// Errors reported by device 11 | /// 12 | /// This can contain several errors at the same time. 13 | /// You can index this list by `DeviceError` to see if an specific error variant 14 | /// has been reported. See the documentation for usage examples. 15 | Device(DeviceErrors), 16 | /// No valid application loaded 17 | NoValidApp, 18 | /// Invalid input data provided to function 19 | InvalidInputData, 20 | } 21 | 22 | /// All possible errors when using an the `Ccs811Awake` type. 23 | #[derive(Debug)] 24 | pub enum ErrorAwake { 25 | /// I²C bus error 26 | I2C(E), 27 | /// Errors reported by device 28 | /// 29 | /// This can contain several errors at the same time. 30 | /// You can index this list by `DeviceError` to see if an specific error variant 31 | /// has been reported. See the documentation for usage examples. 32 | Device(DeviceErrors), 33 | /// No valid application loaded 34 | NoValidApp, 35 | /// Invalid input data provided to function 36 | InvalidInputData, 37 | } 38 | 39 | impl From> for Error { 40 | fn from(error: ErrorAwake) -> Self { 41 | match error { 42 | ErrorAwake::I2C(e) => Error::I2C(e), 43 | ErrorAwake::Device(e) => Error::Device(e), 44 | ErrorAwake::NoValidApp => Error::NoValidApp, 45 | ErrorAwake::InvalidInputData => Error::InvalidInputData, 46 | } 47 | } 48 | } 49 | 50 | /// Errors reported by the device. 51 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 52 | pub struct DeviceErrors { 53 | /// I2C write to an invalid register reported by device. 54 | pub invalid_register_write: bool, 55 | /// I2C read from an invalid register reported by device. 56 | pub invalid_register_read: bool, 57 | /// Invalid measurement reported by device. 58 | pub invalid_measurement: bool, 59 | /// Sensor resistance measurement reached or exceeded the maximum range reported by device. 60 | pub max_resistance: bool, 61 | /// Heater current not in range reported by device. 62 | pub heater_fault: bool, 63 | /// Heater current not applied correctly reported by device. 64 | pub heater_supply: bool, 65 | } 66 | 67 | /// Error type for mode changes when using `Ccs811`. 68 | /// 69 | /// This allows to retrieve the unchanged device in case of an error. 70 | #[derive(Debug)] 71 | pub struct ModeChangeError { 72 | /// Unchanged device. 73 | pub dev: DEV, 74 | /// Error occurred. 75 | pub error: E, 76 | } 77 | 78 | impl ModeChangeError { 79 | pub(crate) fn new(dev: DEV, error: E) -> Self { 80 | ModeChangeError { dev, error } 81 | } 82 | } 83 | 84 | /// Measurement modes. 85 | /// 86 | /// NOTE: When changing to a new mode with a lower sample rate, 87 | /// place the device in `Idle` mode for at least 10 minutes before 88 | /// enabling the new mode. 89 | #[derive(Debug, Clone, Copy, PartialEq)] 90 | pub enum MeasurementMode { 91 | /// Idle. Measurements are disabled. (Mode 0) 92 | Idle, 93 | /// Constant power mode. IAQ measurement every second. (Mode 1) 94 | ConstantPower1s, 95 | /// Pulse heating mode. IAQ measurement every 10 seconds. (Mode 2) 96 | PulseHeating10s, 97 | /// Low power pulse heating mode. IAQ measurement every 60 seconds. (Mode 3) 98 | LowPowerPulseHeating60s, 99 | /// Constant power mode. IAQ measurement every 250ms. (Mode 4) 100 | ConstantPower250ms, 101 | } 102 | 103 | /// Firmware mode 104 | #[derive(Debug, Clone, Copy, PartialEq)] 105 | pub enum FirmwareMode { 106 | /// Boot mode. New firmware can be loaded. 107 | Boot, 108 | /// Application mode. CCS811 can take measurements 109 | Application, 110 | } 111 | 112 | /// Interrupt generation modes. 113 | #[derive(Debug, Clone, Copy, PartialEq)] 114 | pub enum InterruptMode { 115 | /// Disable interrupt generation 116 | Disabled, 117 | /// Generate an interrupt every time there is new data ready. 118 | OnDataReady, 119 | /// Generate an interrupt if the measurement crosses a threshold by more 120 | /// than 50 ppm. (See `set_eco2_thresholds()`). 121 | OnThresholdCrossed, 122 | } 123 | 124 | /// Algorithm result 125 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 126 | pub struct AlgorithmResult { 127 | /// eCO2 result in ppm 128 | pub eco2: u16, 129 | /// eTVOC result in ppb 130 | pub etvoc: u16, 131 | /// Raw sensor current in uA 132 | pub raw_current: u8, 133 | /// Raw sensor voltage (1023 = 1.65V) 134 | pub raw_voltage: u16, 135 | } 136 | 137 | /// Possible slave addresses 138 | #[derive(Debug, Clone, Copy)] 139 | pub enum SlaveAddr { 140 | /// Default slave address 141 | Default, 142 | /// Alternative slave address providing bit value for the ADDR pin 143 | Alternative(bool), 144 | } 145 | 146 | impl Default for SlaveAddr { 147 | /// Default slave address 148 | fn default() -> Self { 149 | SlaveAddr::Default 150 | } 151 | } 152 | 153 | impl SlaveAddr { 154 | pub(crate) fn addr(self) -> u8 { 155 | match self { 156 | SlaveAddr::Default => 0x5A, 157 | SlaveAddr::Alternative(false) => 0x5A, 158 | SlaveAddr::Alternative(true) => 0x5B, 159 | } 160 | } 161 | } 162 | 163 | #[cfg(test)] 164 | mod tests { 165 | use super::*; 166 | 167 | #[test] 168 | fn can_get_default_address() { 169 | let addr = SlaveAddr::default(); 170 | assert_eq!(0x5A, addr.addr()); 171 | } 172 | 173 | #[test] 174 | fn can_generate_alternative_addresses() { 175 | assert_eq!(0x5A, SlaveAddr::Alternative(false).addr()); 176 | assert_eq!(0x5B, SlaveAddr::Alternative(true).addr()); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/app_mode.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{ 2 | mode, prelude::*, AlgorithmResult, Ccs811, Error, InterruptMode, MeasurementMode, 3 | }; 4 | use embedded_hal_mock::eh1::{ 5 | delay::NoopDelay as NoDelay, 6 | digital::{Mock as PinMock, State as PinState, Transaction as PinTrans}, 7 | i2c::{Mock as I2cMock, Transaction as I2cTrans}, 8 | }; 9 | mod common; 10 | use crate::common::{destroy, new, BitFlags as BF, Register, DEV_ADDR}; 11 | use nb::Error as NbError; 12 | 13 | pub fn new_app( 14 | transactions: &[I2cTrans], 15 | pin: PinMock, 16 | ) -> Ccs811 { 17 | new(transactions, pin).start_application().ok().unwrap() 18 | } 19 | 20 | macro_rules! set_test { 21 | ($name:ident, $method:ident, $value:expr, $reg:ident, $reg_value:expr) => { 22 | #[test] 23 | fn $name() { 24 | let nwake = PinMock::new(&[ 25 | PinTrans::set(PinState::Low), 26 | PinTrans::set(PinState::High), 27 | PinTrans::set(PinState::Low), 28 | PinTrans::set(PinState::High), 29 | ]); 30 | let transactions = [ 31 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 32 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 33 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 34 | // started 35 | I2cTrans::write(DEV_ADDR, vec![Register::$reg, $reg_value]), 36 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 37 | ]; 38 | let mut sensor = new_app(&transactions, nwake); 39 | sensor.$method($value).unwrap(); 40 | destroy(sensor); 41 | } 42 | }; 43 | } 44 | 45 | macro_rules! set_mode_test { 46 | ($name:ident, $mode:ident, $value:expr) => { 47 | set_test!($name, set_mode, MeasurementMode::$mode, MEAS_MODE, $value); 48 | }; 49 | } 50 | 51 | set_mode_test!(can_set_mode_0, Idle, 0); 52 | set_mode_test!(can_set_mode_1, ConstantPower1s, 1 << 4); 53 | set_mode_test!(can_set_mode_2, PulseHeating10s, 2 << 4); 54 | set_mode_test!(can_set_mode_3, LowPowerPulseHeating60s, 3 << 4); 55 | set_mode_test!(can_set_mode_4, ConstantPower250ms, 4 << 4); 56 | 57 | #[macro_export] 58 | macro_rules! read_status_app_test { 59 | ($name:ident, $method:ident, $expected:expr, $value: expr) => { 60 | #[test] 61 | fn $name() { 62 | let nwake = PinMock::new(&[ 63 | PinTrans::set(PinState::Low), 64 | PinTrans::set(PinState::High), 65 | PinTrans::set(PinState::Low), 66 | PinTrans::set(PinState::High), 67 | ]); 68 | let transactions = [ 69 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 70 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 71 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 72 | // started 73 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![$value]), 74 | ]; 75 | let mut sensor = new_app(&transactions, nwake); 76 | assert_eq!($expected, sensor.$method().unwrap()); 77 | destroy(sensor); 78 | } 79 | }; 80 | } 81 | 82 | read_status_app_test!(has_data_ready, has_data_ready, true, BF::DATA_READY); 83 | read_status_app_test!(has_no_data_ready, has_data_ready, false, 0); 84 | 85 | #[test] 86 | fn can_read_raw_data() { 87 | let nwake = PinMock::new(&[ 88 | PinTrans::set(PinState::Low), 89 | PinTrans::set(PinState::High), 90 | PinTrans::set(PinState::Low), 91 | PinTrans::set(PinState::High), 92 | ]); 93 | let transactions = [ 94 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 95 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 96 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 97 | // started 98 | I2cTrans::write_read(DEV_ADDR, vec![Register::RAW_DATA], vec![0x34, 0x52]), 99 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 100 | ]; 101 | let mut sensor = new_app(&transactions, nwake); 102 | assert_eq!((0x50 >> 2, 0x234), sensor.raw_data().unwrap()); 103 | destroy(sensor); 104 | } 105 | 106 | #[test] 107 | fn can_read_alg_result_data() { 108 | let nwake = PinMock::new(&[ 109 | PinTrans::set(PinState::Low), 110 | PinTrans::set(PinState::High), 111 | PinTrans::set(PinState::Low), 112 | PinTrans::set(PinState::High), 113 | PinTrans::set(PinState::Low), 114 | PinTrans::set(PinState::High), 115 | ]); 116 | let transactions = [ 117 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 118 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 119 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 120 | // started 121 | I2cTrans::write_read( 122 | DEV_ADDR, 123 | vec![Register::ALG_RESULT_DATA], 124 | vec![0, 0, 0, 0, 0, 0, 0, 0], 125 | ), 126 | I2cTrans::write_read( 127 | DEV_ADDR, 128 | vec![Register::ALG_RESULT_DATA], 129 | vec![0x12, 0x34, 0x56, 0x78, BF::DATA_READY, 0, 0x91, 0x52], 130 | ), 131 | ]; 132 | let mut sensor = new_app(&transactions, nwake); 133 | let expected = AlgorithmResult { 134 | eco2: 0x1234, 135 | etvoc: 0x5678, 136 | raw_current: 0x50 >> 2, 137 | raw_voltage: 0x291, 138 | }; 139 | assert_error!(sensor.data(), NbError::WouldBlock); 140 | assert_eq!(expected, sensor.data().unwrap()); 141 | destroy(sensor); 142 | } 143 | 144 | #[test] 145 | fn can_read_baseline() { 146 | let nwake = PinMock::new(&[ 147 | PinTrans::set(PinState::Low), 148 | PinTrans::set(PinState::High), 149 | PinTrans::set(PinState::Low), 150 | PinTrans::set(PinState::High), 151 | ]); 152 | let transactions = [ 153 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 154 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 155 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 156 | // started 157 | I2cTrans::write_read(DEV_ADDR, vec![Register::BASELINE], vec![0x34, 0x52]), 158 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 159 | ]; 160 | let mut sensor = new_app(&transactions, nwake); 161 | let data = sensor.baseline().unwrap(); 162 | assert_eq!([0x34, 0x52], data); 163 | destroy(sensor); 164 | } 165 | 166 | #[test] 167 | fn can_set_baseline() { 168 | let nwake = PinMock::new(&[ 169 | PinTrans::set(PinState::Low), 170 | PinTrans::set(PinState::High), 171 | PinTrans::set(PinState::Low), 172 | PinTrans::set(PinState::High), 173 | ]); 174 | let transactions = [ 175 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 176 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 177 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 178 | // started 179 | I2cTrans::write(DEV_ADDR, vec![Register::BASELINE, 0x34, 0x52]), 180 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 181 | ]; 182 | let mut sensor = new_app(&transactions, nwake); 183 | sensor.set_baseline([0x34, 0x52]).unwrap(); 184 | destroy(sensor); 185 | } 186 | 187 | macro_rules! invalid_env_test { 188 | ($name:ident, $rh:expr, $temp:expr) => { 189 | #[test] 190 | fn $name() { 191 | let nwake = PinMock::new(&[ 192 | PinTrans::set(PinState::Low), 193 | PinTrans::set(PinState::High), 194 | PinTrans::set(PinState::Low), 195 | PinTrans::set(PinState::High), 196 | ]); 197 | let mut sensor = new_app( 198 | &[ 199 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 200 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 201 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 202 | ], 203 | nwake, 204 | ); 205 | assert_error!(sensor.set_environment($rh, $temp), Error::InvalidInputData); 206 | destroy(sensor); 207 | } 208 | }; 209 | } 210 | 211 | invalid_env_test!(cannot_set_negative_humidity, -1.0, 0.0); 212 | invalid_env_test!(cannot_set_too_high_humidity, 100.1, 0.0); 213 | invalid_env_test!(cannot_set_too_high_temp, 0.0, 255.0); 214 | 215 | #[test] 216 | fn can_set_environment_params() { 217 | let nwake = PinMock::new(&[ 218 | PinTrans::set(PinState::Low), 219 | PinTrans::set(PinState::High), 220 | PinTrans::set(PinState::Low), 221 | PinTrans::set(PinState::High), 222 | ]); 223 | let transactions = [ 224 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 225 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 226 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 227 | // started 228 | I2cTrans::write(DEV_ADDR, vec![Register::ENV_DATA, 0x60, 0x80, 0x64, 0x40]), 229 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 230 | ]; 231 | let mut sensor = new_app(&transactions, nwake); 232 | sensor.set_environment(48.25, 25.125).unwrap(); 233 | destroy(sensor); 234 | } 235 | 236 | #[test] 237 | fn can_set_thresholds() { 238 | let nwake = PinMock::new(&[ 239 | PinTrans::set(PinState::Low), 240 | PinTrans::set(PinState::High), 241 | PinTrans::set(PinState::Low), 242 | PinTrans::set(PinState::High), 243 | ]); 244 | let transactions = [ 245 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 246 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 247 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 248 | // started 249 | I2cTrans::write(DEV_ADDR, vec![Register::THRESHOLDS, 0x05, 0xDC, 0x09, 0xC4]), 250 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 251 | ]; 252 | let mut sensor = new_app(&transactions, nwake); 253 | sensor.set_eco2_thresholds(1500, 2500).unwrap(); 254 | destroy(sensor); 255 | } 256 | 257 | macro_rules! set_int_test { 258 | ($name:ident, $mode:ident, $value:expr) => { 259 | set_test!( 260 | $name, 261 | set_interrupt_mode, 262 | InterruptMode::$mode, 263 | MEAS_MODE, 264 | $value 265 | ); 266 | }; 267 | } 268 | 269 | set_int_test!(disable_int, Disabled, 0); 270 | set_int_test!(enable_int_data, OnDataReady, BF::INTERRUPT); 271 | set_int_test!(en_int_th, OnThresholdCrossed, BF::INTERRUPT | BF::THRESH); 272 | 273 | #[test] 274 | fn can_do_software_reset() { 275 | let nwake = PinMock::new(&[ 276 | PinTrans::set(PinState::Low), 277 | PinTrans::set(PinState::High), 278 | PinTrans::set(PinState::Low), 279 | PinTrans::set(PinState::High), 280 | ]); 281 | let transactions = [ 282 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 283 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 284 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 285 | // started 286 | I2cTrans::write(DEV_ADDR, vec![Register::SW_RESET, 0x11, 0xE5, 0x72, 0x8A]), 287 | ]; 288 | let sensor = new_app(&transactions, nwake); 289 | let sensor = sensor.software_reset().ok().unwrap(); 290 | destroy(sensor); 291 | } 292 | -------------------------------------------------------------------------------- /tests/boot_mode.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{prelude::*, Error}; 2 | use embedded_hal_mock::eh1::{ 3 | delay::NoopDelay as NoDelay, 4 | digital::{Mock as PinMock, State as PinState, Transaction as PinTrans}, 5 | i2c::Transaction as I2cTrans, 6 | }; 7 | mod common; 8 | use crate::common::{destroy, new, BitFlags as BF, Register, DEV_ADDR}; 9 | 10 | #[test] 11 | fn can_start_app_mode() { 12 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 13 | let transactions = [ 14 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VALID]), 15 | I2cTrans::write(DEV_ADDR, vec![Register::APP_START]), 16 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 17 | ]; 18 | let sensor = new(&transactions, nwake); 19 | let sensor = sensor.start_application().ok().unwrap(); 20 | destroy(sensor); 21 | } 22 | 23 | #[test] 24 | fn cannot_start_app_mode_invalid_app() { 25 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 26 | let transactions = [I2cTrans::write_read( 27 | DEV_ADDR, 28 | vec![Register::STATUS], 29 | vec![0], 30 | )]; 31 | let sensor = new(&transactions, nwake); 32 | let result = sensor.start_application().err().unwrap(); 33 | match result.error { 34 | Error::NoValidApp => (), 35 | _ => panic!("Invalid error"), 36 | } 37 | destroy(result.dev); 38 | } 39 | 40 | #[test] 41 | fn can_verify_app() { 42 | let nwake = PinMock::new(&[ 43 | PinTrans::set(PinState::Low), 44 | PinTrans::set(PinState::High), 45 | PinTrans::set(PinState::Low), 46 | PinTrans::set(PinState::High), 47 | PinTrans::set(PinState::Low), 48 | PinTrans::set(PinState::High), 49 | ]); 50 | let transactions = [ 51 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 52 | I2cTrans::write(DEV_ADDR, vec![Register::APP_VERIFY]), 53 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 54 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VERIFY]), 55 | ]; 56 | let mut sensor = new(&transactions, nwake); 57 | sensor 58 | .verify_application() 59 | .expect_err("Should have returned nb::Error::WouldBlock"); 60 | sensor 61 | .verify_application() 62 | .expect_err("Should have returned nb::Error::WouldBlock"); 63 | sensor.verify_application().unwrap(); 64 | destroy(sensor); 65 | } 66 | 67 | #[test] 68 | fn can_erase_app() { 69 | let nwake = PinMock::new(&[ 70 | PinTrans::set(PinState::Low), 71 | PinTrans::set(PinState::High), 72 | PinTrans::set(PinState::Low), 73 | PinTrans::set(PinState::High), 74 | PinTrans::set(PinState::Low), 75 | PinTrans::set(PinState::High), 76 | ]); 77 | let transactions = [ 78 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 79 | I2cTrans::write(DEV_ADDR, vec![Register::APP_ERASE, 0xE7, 0xA7, 0xE6, 0x09]), 80 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 81 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_ERASE]), 82 | ]; 83 | let mut sensor = new(&transactions, nwake); 84 | sensor 85 | .erase_application() 86 | .expect_err("Should have returned nb::Error::WouldBlock"); 87 | sensor 88 | .erase_application() 89 | .expect_err("Should have returned nb::Error::WouldBlock"); 90 | sensor.erase_application().unwrap(); 91 | destroy(sensor); 92 | } 93 | 94 | #[test] 95 | fn cannot_download_wrong_size_app() { 96 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 97 | let mut sensor = new(&[], nwake); 98 | assert_error!( 99 | sensor.download_application(&[0, 1, 2, 3, 4, 5, 6, 7, 8], &mut NoDelay::new()), 100 | Error::InvalidInputData 101 | ); 102 | destroy(sensor); 103 | } 104 | 105 | #[test] 106 | fn can_download_app() { 107 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 108 | let transactions = [ 109 | I2cTrans::write( 110 | DEV_ADDR, 111 | vec![Register::REG_BOOT_APP, 0, 1, 2, 3, 4, 5, 6, 7], 112 | ), 113 | I2cTrans::write( 114 | DEV_ADDR, 115 | vec![Register::REG_BOOT_APP, 8, 9, 10, 11, 12, 13, 14, 15], 116 | ), 117 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 118 | ]; 119 | let mut sensor = new(&transactions, nwake); 120 | sensor 121 | .download_application( 122 | &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 123 | &mut NoDelay::new(), 124 | ) 125 | .unwrap(); 126 | destroy(sensor); 127 | } 128 | 129 | #[test] 130 | fn can_update_app() { 131 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 132 | let transactions = [ 133 | I2cTrans::write(DEV_ADDR, vec![Register::SW_RESET, 0x11, 0xE5, 0x72, 0x8A]), 134 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 135 | I2cTrans::write(DEV_ADDR, vec![Register::APP_ERASE, 0xE7, 0xA7, 0xE6, 0x09]), 136 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 137 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_ERASE]), 138 | I2cTrans::write( 139 | DEV_ADDR, 140 | vec![Register::REG_BOOT_APP, 0, 1, 2, 3, 4, 5, 6, 7], 141 | ), 142 | I2cTrans::write( 143 | DEV_ADDR, 144 | vec![Register::REG_BOOT_APP, 8, 9, 10, 11, 12, 13, 14, 15], 145 | ), 146 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 147 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 148 | I2cTrans::write(DEV_ADDR, vec![Register::APP_VERIFY]), 149 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 150 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::APP_VERIFY]), 151 | ]; 152 | let mut sensor = new(&transactions, nwake); 153 | sensor 154 | .update_application( 155 | &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 156 | &mut NoDelay::new(), 157 | ) 158 | .unwrap(); 159 | destroy(sensor); 160 | } 161 | 162 | #[test] 163 | fn can_do_software_reset() { 164 | let nwake = PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 165 | let transactions = [I2cTrans::write( 166 | DEV_ADDR, 167 | vec![Register::SW_RESET, 0x11, 0xE5, 0x72, 0x8A], 168 | )]; 169 | let mut sensor = new(&transactions, nwake); 170 | sensor.software_reset().unwrap(); 171 | destroy(sensor); 172 | } 173 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{mode, Ccs811, SlaveAddr}; 2 | use embedded_hal_mock::eh1::{ 3 | delay::NoopDelay as NoDelay, 4 | digital::Mock as PinMock, 5 | i2c::{Mock as I2cMock, Transaction as I2cTrans}, 6 | }; 7 | 8 | pub const DEV_ADDR: u8 = 0x5A; 9 | pub struct Register {} 10 | #[allow(unused)] 11 | impl Register { 12 | pub const STATUS: u8 = 0x00; 13 | pub const MEAS_MODE: u8 = 0x01; 14 | pub const ALG_RESULT_DATA: u8 = 0x02; 15 | pub const RAW_DATA: u8 = 0x03; 16 | pub const ENV_DATA: u8 = 0x05; 17 | pub const THRESHOLDS: u8 = 0x10; 18 | pub const BASELINE: u8 = 0x11; 19 | pub const HW_ID: u8 = 0x20; 20 | pub const HW_VERSION: u8 = 0x21; 21 | pub const FW_BOOT_VERSION: u8 = 0x23; 22 | pub const FW_APP_VERSION: u8 = 0x24; 23 | pub const ERROR_ID: u8 = 0xE0; 24 | pub const APP_ERASE: u8 = 0xF1; 25 | pub const REG_BOOT_APP: u8 = 0xF2; 26 | pub const APP_VERIFY: u8 = 0xF3; 27 | pub const APP_START: u8 = 0xF4; 28 | pub const SW_RESET: u8 = 0xFF; 29 | } 30 | 31 | pub struct BitFlags {} 32 | #[allow(unused)] 33 | impl BitFlags { 34 | pub const DATA_READY: u8 = 1 << 3; 35 | pub const APP_VALID: u8 = 1 << 4; 36 | pub const APP_VERIFY: u8 = 1 << 5; 37 | pub const APP_ERASE: u8 = 1 << 6; 38 | pub const FW_MODE: u8 = 1 << 7; 39 | pub const ERROR: u8 = 1; 40 | pub const WRITE_REG_INVALID: u8 = 1; 41 | pub const READ_REG_INVALID: u8 = 1 << 1; 42 | pub const MEASMODE_INVALID: u8 = 1 << 2; 43 | pub const MAX_RESISTANCE: u8 = 1 << 3; 44 | pub const HEATER_FAULT: u8 = 1 << 4; 45 | pub const HEATER_SUPPLY: u8 = 1 << 5; 46 | pub const INTERRUPT: u8 = 1 << 3; 47 | pub const THRESH: u8 = 1 << 2; 48 | } 49 | 50 | pub fn new( 51 | transactions: &[I2cTrans], 52 | pin: PinMock, 53 | ) -> Ccs811 { 54 | Ccs811::new( 55 | I2cMock::new(transactions), 56 | SlaveAddr::default(), 57 | pin, 58 | NoDelay::new(), 59 | ) 60 | } 61 | 62 | pub fn destroy(sensor: Ccs811) { 63 | let (mut i2c, mut pin, _delay) = sensor.destroy(); 64 | i2c.done(); 65 | pin.done(); 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! read_status_test { 70 | ($name:ident, $method:ident, $expected:expr, $value: expr) => { 71 | #[test] 72 | fn $name() { 73 | let nwake = 74 | PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 75 | let transactions = [I2cTrans::write_read( 76 | DEV_ADDR, 77 | vec![Register::STATUS], 78 | vec![$value], 79 | )]; 80 | let mut sensor = new(&transactions, nwake); 81 | assert_eq!($expected, sensor.$method().unwrap()); 82 | destroy(sensor); 83 | } 84 | }; 85 | } 86 | 87 | #[macro_export] 88 | macro_rules! assert_error { 89 | ($result:expr, $error:ident::$variant:ident) => { 90 | match $result { 91 | Err($error::$variant) => (), 92 | _ => panic!("Error not returned."), 93 | } 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /tests/device_errors.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{prelude::*, Error}; 2 | use embedded_hal_mock::eh1::{ 3 | digital::{Mock as PinMock, State as PinState, Transaction as PinTrans}, 4 | i2c::Transaction as I2cTrans, 5 | }; 6 | mod common; 7 | use crate::common::{destroy, new, BitFlags as BF, Register, DEV_ADDR}; 8 | 9 | macro_rules! expect_err { 10 | ($name:ident, $error_id:expr, $invalid_write:expr, $invalid_read:expr, $invalid_meas:expr, 11 | $max_resistance:expr, $heater_fault:expr, $heater_supply:expr) => { 12 | #[test] 13 | fn $name() { 14 | let nwake = 15 | PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 16 | let transactions = [ 17 | I2cTrans::write_read(DEV_ADDR, vec![Register::HW_ID], vec![0x81]), 18 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![BF::ERROR]), 19 | I2cTrans::write_read(DEV_ADDR, vec![Register::ERROR_ID], vec![$error_id]), 20 | ]; 21 | let mut sensor = new(&transactions, nwake); 22 | match sensor.hardware_id() { 23 | Err(Error::Device(errors)) => { 24 | assert_eq!($invalid_write, errors.invalid_register_write); 25 | assert_eq!($invalid_read, errors.invalid_register_read); 26 | assert_eq!($invalid_meas, errors.invalid_measurement); 27 | assert_eq!($max_resistance, errors.max_resistance); 28 | assert_eq!($heater_fault, errors.heater_fault); 29 | assert_eq!($heater_supply, errors.heater_supply); 30 | } 31 | _ => panic!("Wrong result"), 32 | } 33 | destroy(sensor); 34 | } 35 | }; 36 | } 37 | 38 | expect_err!( 39 | invalid_write, 40 | BF::WRITE_REG_INVALID, 41 | true, 42 | false, 43 | false, 44 | false, 45 | false, 46 | false 47 | ); 48 | 49 | expect_err!( 50 | invalid_read, 51 | BF::READ_REG_INVALID, 52 | false, 53 | true, 54 | false, 55 | false, 56 | false, 57 | false 58 | ); 59 | 60 | expect_err!( 61 | invalid_measurement, 62 | BF::MEASMODE_INVALID, 63 | false, 64 | false, 65 | true, 66 | false, 67 | false, 68 | false 69 | ); 70 | 71 | expect_err!( 72 | max_resistence, 73 | BF::MAX_RESISTANCE, 74 | false, 75 | false, 76 | false, 77 | true, 78 | false, 79 | false 80 | ); 81 | 82 | expect_err!( 83 | heater_fault, 84 | BF::HEATER_FAULT, 85 | false, 86 | false, 87 | false, 88 | false, 89 | true, 90 | false 91 | ); 92 | 93 | expect_err!( 94 | heater_supply, 95 | BF::HEATER_SUPPLY, 96 | false, 97 | false, 98 | false, 99 | false, 100 | false, 101 | true 102 | ); 103 | 104 | expect_err!( 105 | heater_supply_and_heater_fault, 106 | BF::HEATER_SUPPLY | BF::HEATER_FAULT, 107 | false, 108 | false, 109 | false, 110 | false, 111 | true, 112 | true 113 | ); 114 | 115 | expect_err!( 116 | all, 117 | BF::WRITE_REG_INVALID 118 | | BF::READ_REG_INVALID 119 | | BF::MEASMODE_INVALID 120 | | BF::MAX_RESISTANCE 121 | | BF::HEATER_SUPPLY 122 | | BF::HEATER_FAULT, 123 | true, 124 | true, 125 | true, 126 | true, 127 | true, 128 | true 129 | ); 130 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use embedded_ccs811::{prelude::*, FirmwareMode as FwMode}; 2 | use embedded_hal_mock::eh1::{ 3 | digital::{Mock as PinMock, State as PinState, Transaction as PinTrans}, 4 | i2c::Transaction as I2cTrans, 5 | }; 6 | mod common; 7 | use crate::common::{destroy, new, BitFlags as BF, Register, DEV_ADDR}; 8 | 9 | #[test] 10 | fn can_create_and_destroy() { 11 | let nwake = PinMock::new(&[]); 12 | let sensor = new(&[], nwake); 13 | destroy(sensor); 14 | } 15 | 16 | macro_rules! get_test { 17 | ($name:ident, $method:ident, $reg:ident, $value:expr, $expected:expr) => { 18 | #[test] 19 | fn $name() { 20 | let nwake = 21 | PinMock::new(&[PinTrans::set(PinState::Low), PinTrans::set(PinState::High)]); 22 | let transactions = [ 23 | I2cTrans::write_read(DEV_ADDR, vec![Register::$reg], $value), 24 | I2cTrans::write_read(DEV_ADDR, vec![Register::STATUS], vec![0]), 25 | ]; 26 | let mut sensor = new(&transactions, nwake); 27 | assert_eq!($expected, sensor.$method().unwrap()); 28 | destroy(sensor); 29 | } 30 | }; 31 | } 32 | 33 | get_test!(can_get_hw_id, hardware_id, HW_ID, vec![0x81], 0x81); 34 | get_test!( 35 | can_get_hw_version, 36 | hardware_version, 37 | HW_VERSION, 38 | vec![0x12], 39 | (1, 2) 40 | ); 41 | get_test!( 42 | can_get_fw_boot_version, 43 | firmware_bootloader_version, 44 | FW_BOOT_VERSION, 45 | vec![0x12, 0x34], 46 | (1, 2, 0x34) 47 | ); 48 | get_test!( 49 | can_get_fw_app_version, 50 | firmware_application_version, 51 | FW_APP_VERSION, 52 | vec![0x12, 0x34], 53 | (1, 2, 0x34) 54 | ); 55 | 56 | read_status_test!(can_get_invalid_app, has_valid_app, false, 0); 57 | read_status_test!(can_get_valid_app, has_valid_app, true, BF::APP_VALID); 58 | read_status_test!(fw_mode_boot, firmware_mode, FwMode::Boot, 0); 59 | read_status_test!(fw_mode_app, firmware_mode, FwMode::Application, BF::FW_MODE); 60 | --------------------------------------------------------------------------------