Files
Embedded-Hacking/drivers/0x09_dht11_rust/src/dht11.rs
T
2026-04-02 11:36:06 -04:00

348 lines
9.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! @file dht11.rs
//! @brief Implementation of DHT11 temperature and humidity sensor driver
//! @author Kevin Thomas
//! @date 2025
//!
//! MIT License
//!
//! Copyright (c) 2025 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
/// Bit-duration threshold in microseconds.
///
/// If the high pulse is longer than this value, the bit is a 1; otherwise 0.
pub const BIT_THRESHOLD_US: u32 = 40;
/// Timeout in spin-loop iterations for waiting on a pin level change.
pub const LEVEL_WAIT_TIMEOUT: u32 = 10_000;
/// Accumulate a single received bit into the 5-byte data array.
///
/// Shifts `data[byte_index]` left by one and sets the LSB to 1 if the
/// measured high-pulse `duration_us` exceeds [`BIT_THRESHOLD_US`].
///
/// # Arguments
///
/// * `data` - 5-byte array accumulating the received bits.
/// * `bit_index` - Bit index (0..39).
/// * `duration_us` - Measured duration of the high pulse in microseconds.
pub fn accumulate_bit(data: &mut [u8; 5], bit_index: usize, duration_us: u32) {
let byte = bit_index / 8;
data[byte] <<= 1;
if duration_us > BIT_THRESHOLD_US {
data[byte] |= 1;
}
}
/// Verify the DHT11 checksum byte.
///
/// The checksum (byte 4) must equal the lower 8 bits of the sum of
/// bytes 0 through 3.
///
/// # Arguments
///
/// * `data` - 5-byte received data (bytes 03 plus checksum in byte 4).
///
/// # Returns
///
/// `true` if the checksum matches, `false` otherwise.
pub fn validate_checksum(data: &[u8; 5]) -> bool {
data[4] == (data[0].wrapping_add(data[1]).wrapping_add(data[2]).wrapping_add(data[3]))
}
/// Parse humidity from the raw DHT11 data bytes.
///
/// Humidity is encoded as an integer part in byte 0 and a fractional
/// part (tenths) in byte 1.
///
/// # Arguments
///
/// * `data` - 5-byte received data.
///
/// # Returns
///
/// Humidity as a percentage (e.g. 65.3 for 65.3%).
pub fn parse_humidity(data: &[u8; 5]) -> f32 {
data[0] as f32 + data[1] as f32 * 0.1
}
/// Parse temperature from the raw DHT11 data bytes.
///
/// Temperature is encoded as an integer part in byte 2 and a fractional
/// part (tenths) in byte 3.
///
/// # Arguments
///
/// * `data` - 5-byte received data.
///
/// # Returns
///
/// Temperature in degrees Celsius.
pub fn parse_temperature(data: &[u8; 5]) -> f32 {
data[2] as f32 + data[3] as f32 * 0.1
}
/// Format a sensor reading as `"Humidity: XX.X% Temperature: XX.X C"`.
///
/// # Arguments
///
/// * `buf` - Mutable byte slice (must be at least 40 bytes).
/// * `humidity` - Humidity percentage.
/// * `temperature` - Temperature in degrees Celsius.
///
/// # Returns
///
/// Number of bytes written into the buffer.
pub fn format_reading(buf: &mut [u8], humidity: f32, temperature: f32) -> usize {
let mut pos = copy_slice(buf, 0, b"Humidity: ");
pos += format_f32_1(&mut buf[pos..], humidity);
pos += copy_slice(buf, pos, b"% Temperature: ");
pos += format_f32_1(&mut buf[pos..], temperature);
pos += copy_slice(buf, pos, b" C");
pos
}
/// Copy a byte slice into `buf` at the given offset, returning bytes written.
fn copy_slice(buf: &mut [u8], offset: usize, src: &[u8]) -> usize {
buf[offset..offset + src.len()].copy_from_slice(src);
src.len()
}
/// Format a failed-read error message.
///
/// # Arguments
///
/// * `buf` - Mutable byte slice (must be at least 42 bytes).
/// * `gpio` - GPIO pin number to include in the message.
///
/// # Returns
///
/// Number of bytes written into the buffer.
pub fn format_error(buf: &mut [u8], gpio: u8) -> usize {
let mut pos = 0;
let prefix = b"DHT11 read failed - check wiring on GPIO ";
buf[pos..pos + prefix.len()].copy_from_slice(prefix);
pos += prefix.len();
pos += format_u8(&mut buf[pos..], gpio);
pos
}
/// Format a `u8` as decimal ASCII digits.
///
/// # Arguments
///
/// * `buf` - Output buffer.
/// * `val` - Value to format.
///
/// # Returns
///
/// Number of bytes written.
fn format_u8(buf: &mut [u8], val: u8) -> usize {
if val >= 100 {
buf[0] = b'0' + val / 100;
buf[1] = b'0' + (val / 10) % 10;
buf[2] = b'0' + val % 10;
3
} else if val >= 10 {
buf[0] = b'0' + val / 10;
buf[1] = b'0' + val % 10;
2
} else {
buf[0] = b'0' + val;
1
}
}
/// Format an `f32` with one decimal place (e.g. `"25.3"`).
///
/// # Arguments
///
/// * `buf` - Output buffer.
/// * `val` - Value to format.
///
/// # Returns
///
/// Number of bytes written.
fn format_f32_1(buf: &mut [u8], val: f32) -> usize {
let scaled = (val * 10.0) as u32;
let integer = scaled / 10;
let frac = (scaled % 10) as u8;
let mut pos = format_u32_minimal(buf, integer);
buf[pos] = b'.';
pos += 1;
buf[pos] = b'0' + frac;
pos += 1;
pos
}
/// Format a u32 as minimal decimal digits (no leading zeros).
fn format_u32_minimal(buf: &mut [u8], value: u32) -> usize {
let mut pos = 0;
if value >= 100 {
buf[pos] = b'0' + (value / 100) as u8;
pos += 1;
}
if value >= 10 {
buf[pos] = b'0' + ((value / 10) % 10) as u8;
pos += 1;
}
buf[pos] = b'0' + (value % 10) as u8;
pos + 1
}
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
#[test]
fn accumulate_bit_zero_short_pulse() {
let mut data = [0u8; 5];
accumulate_bit(&mut data, 0, 30);
assert_eq!(data[0], 0);
}
#[test]
fn accumulate_bit_one_long_pulse() {
let mut data = [0u8; 5];
accumulate_bit(&mut data, 0, 70);
assert_eq!(data[0], 1);
}
#[test]
fn accumulate_bit_threshold_exact() {
let mut data = [0u8; 5];
accumulate_bit(&mut data, 0, BIT_THRESHOLD_US);
assert_eq!(data[0], 0);
}
#[test]
fn accumulate_bit_two_bits() {
let mut data = [0u8; 5];
accumulate_bit(&mut data, 0, 70);
accumulate_bit(&mut data, 1, 30);
assert_eq!(data[0], 0b10);
}
#[test]
fn accumulate_bit_crosses_byte() {
let mut data = [0u8; 5];
accumulate_bit(&mut data, 7, 70);
accumulate_bit(&mut data, 8, 70);
assert_eq!(data[0], 1);
assert_eq!(data[1], 1);
}
#[test]
fn validate_checksum_valid() {
let data = [0x28, 0x00, 0x1A, 0x00, 0x42];
assert!(validate_checksum(&data));
}
#[test]
fn validate_checksum_invalid() {
let data = [0x28, 0x00, 0x1A, 0x00, 0xFF];
assert!(!validate_checksum(&data));
}
#[test]
fn validate_checksum_wraps() {
let data = [0xFF, 0x01, 0x00, 0x00, 0x00];
assert!(validate_checksum(&data));
}
#[test]
fn parse_humidity_integer_only() {
let data = [0x41, 0x00, 0x00, 0x00, 0x41];
let h = parse_humidity(&data);
assert!((h - 65.0).abs() < 0.01);
}
#[test]
fn parse_humidity_with_fraction() {
let data = [0x41, 0x03, 0x00, 0x00, 0x44];
let h = parse_humidity(&data);
assert!((h - 65.3).abs() < 0.01);
}
#[test]
fn parse_temperature_integer_only() {
let data = [0x00, 0x00, 0x19, 0x00, 0x19];
let t = parse_temperature(&data);
assert!((t - 25.0).abs() < 0.01);
}
#[test]
fn parse_temperature_with_fraction() {
let data = [0x00, 0x00, 0x19, 0x05, 0x1E];
let t = parse_temperature(&data);
assert!((t - 25.5).abs() < 0.01);
}
#[test]
fn format_reading_typical() {
let mut buf = [0u8; 48];
let n = format_reading(&mut buf, 65.0, 25.0);
assert_eq!(&buf[..n], b"Humidity: 65.0% Temperature: 25.0 C");
}
#[test]
fn format_reading_with_fractions() {
let mut buf = [0u8; 48];
let n = format_reading(&mut buf, 65.3, 25.5);
assert_eq!(&buf[..n], b"Humidity: 65.3% Temperature: 25.5 C");
}
#[test]
fn format_error_gpio4() {
let mut buf = [0u8; 48];
let n = format_error(&mut buf, 4);
assert_eq!(&buf[..n], b"DHT11 read failed - check wiring on GPIO 4");
}
#[test]
fn format_error_gpio15() {
let mut buf = [0u8; 48];
let n = format_error(&mut buf, 15);
assert_eq!(&buf[..n], b"DHT11 read failed - check wiring on GPIO 15");
}
#[test]
fn format_u8_single_digit() {
let mut buf = [0u8; 4];
let n = format_u8(&mut buf, 4);
assert_eq!(&buf[..n], b"4");
}
#[test]
fn format_u8_two_digits() {
let mut buf = [0u8; 4];
let n = format_u8(&mut buf, 15);
assert_eq!(&buf[..n], b"15");
}
#[test]
fn format_u8_three_digits() {
let mut buf = [0u8; 4];
let n = format_u8(&mut buf, 255);
assert_eq!(&buf[..n], b"255");
}
}