#![no_std]
use embedded_hal::{
delay::DelayNs,
digital::{InputPin, OutputPin},
};
use embedded_hal_async::{delay::DelayNs as DelayNsAsync, digital::Wait};
use libm::sqrt;
pub enum DistanceUnit {
Centimeters,
Inches,
}
pub enum TemperatureUnit {
Celsius,
Fahrenheit,
}
pub struct Config {
pub distance_unit: DistanceUnit,
pub temperature_unit: TemperatureUnit,
}
pub trait Now {
fn now_micros(&self) -> u64;
}
pub struct Hcsr04<TRIGPIN, ECHOPIN, CLOCK, DELAY> {
trigger: TRIGPIN,
echo: ECHOPIN,
config: Config,
clock: CLOCK,
delay: DELAY,
}
impl<TRIGPIN, ECHOPIN, CLOCK, DELAY> Hcsr04<TRIGPIN, ECHOPIN, CLOCK, DELAY>
where
TRIGPIN: OutputPin,
ECHOPIN: InputPin + Wait,
CLOCK: Now,
DELAY: DelayNs + DelayNsAsync,
{
pub const fn new(
trigger: TRIGPIN,
echo: ECHOPIN,
config: Config,
clock: CLOCK,
delay: DELAY,
) -> Self {
Self {
trigger,
echo,
config,
clock,
delay,
}
}
fn speed_of_sound_temperature_adjusted(&self, temperature: f64) -> f64 {
let temp = match self.config.temperature_unit {
TemperatureUnit::Celsius => temperature,
TemperatureUnit::Fahrenheit => (temperature - 32.0) * 5.0 / 9.0,
};
331.5 * sqrt(1.0 + (temp / 273.15))
}
fn distance(&self, speed_of_sound: f64, duration_secs: f64) -> f64 {
let distance = (speed_of_sound * 100.0 * duration_secs) / 2.0;
match self.config.distance_unit {
DistanceUnit::Centimeters => distance,
DistanceUnit::Inches => distance / 2.54,
}
}
pub async fn measure(&mut self, temperature: f64) -> Result<f64, &'static str> {
if self.echo.is_high().map_err(|_| "Error reading echo pin")? {
return Err("Echo pin is already high");
}
self.trigger
.set_high()
.map_err(|_| "Error setting trigger pin high")?;
#[cfg(feature = "blocking_trigger")]
DelayNs::delay_us(&mut self.delay, 10);
#[cfg(not(feature = "blocking_trigger"))]
DelayNsAsync::delay_us(&mut self.delay, 10).await;
self.trigger
.set_low()
.map_err(|_| "Error setting trigger pin low")?;
self.echo
.wait_for_high()
.await
.map_err(|_| "Error waiting for echo high")?;
let start = Now::now_micros(&self.clock);
self.echo
.wait_for_low()
.await
.map_err(|_| "Error waiting for echo low")?;
let end = Now::now_micros(&self.clock);
let pulse_duration_secs = (end - start) as f64 / 1_000_000.0;
Ok(self.distance(
self.speed_of_sound_temperature_adjusted(temperature),
pulse_duration_secs,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::sync::atomic::{AtomicU32, Ordering};
use defmt_rtt as _;
use embedded_hal::digital::{ErrorKind, ErrorType};
use libm::round;
static COUNT: AtomicU32 = AtomicU32::new(0);
defmt::timestamp!("{=u32:us}", COUNT.fetch_add(1, Ordering::Relaxed));
use critical_section::RawRestoreState;
struct CriticalSection;
unsafe impl critical_section::Impl for CriticalSection {
unsafe fn acquire() -> RawRestoreState {
}
unsafe fn release(_state: RawRestoreState) {
}
}
critical_section::set_impl!(CriticalSection);
struct OutputPinMock;
impl ErrorType for OutputPinMock {
type Error = ErrorKind;
}
impl OutputPin for OutputPinMock {
fn set_high(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn set_state(
&mut self,
_state: embedded_hal::digital::PinState,
) -> Result<(), Self::Error> {
Ok(())
}
}
struct InputPinMock;
impl ErrorType for InputPinMock {
type Error = ErrorKind;
}
impl InputPin for InputPinMock {
fn is_high(&mut self) -> Result<bool, Self::Error> {
Ok(true)
}
fn is_low(&mut self) -> Result<bool, Self::Error> {
Ok(true)
}
}
impl Wait for InputPinMock {
async fn wait_for_high(&mut self) -> Result<(), Self::Error> {
Ok(())
}
async fn wait_for_low(&mut self) -> Result<(), Self::Error> {
Ok(())
}
async fn wait_for_any_edge(&mut self) -> Result<(), Self::Error> {
Ok(())
}
async fn wait_for_falling_edge(&mut self) -> Result<(), Self::Error> {
Ok(())
}
async fn wait_for_rising_edge(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
struct ClockMock;
impl Now for ClockMock {
fn now_micros(&self) -> u64 {
0
}
}
struct DelayMock;
impl DelayNs for DelayMock {
fn delay_ns(&mut self, _ns: u32) {}
}
impl DelayNsAsync for DelayMock {
async fn delay_ns(&mut self, _ns: u32) {}
}
#[test]
fn speed_of_sound_m_per_s_temperature_adjusted_0() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(
round(sensor.speed_of_sound_temperature_adjusted(0.0)),
round(331.5)
);
}
#[test]
fn speed_of_sound_m_per_s_temperature_adjusted_20() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(
round(sensor.speed_of_sound_temperature_adjusted(20.0)),
round(343.42)
);
}
#[test]
fn speed_of_sound_m_per_s_temperature_adjusted_40() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(
round(sensor.speed_of_sound_temperature_adjusted(40.0)),
round(354.94)
);
}
#[test]
fn distance_cm_duration_0secs() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(sensor.distance(343.14, 0.0), 0.0);
}
#[test]
fn distance_cm_duration_5ms() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(sensor.distance(343.14, 0.005), 85.785);
}
#[test]
fn distance_cm_duration_10ms() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(sensor.distance(343.14, 0.01), 171.57);
}
#[test]
fn can_use_fahrenheit() {
let config = Config {
distance_unit: DistanceUnit::Centimeters,
temperature_unit: TemperatureUnit::Fahrenheit,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(
round(sensor.speed_of_sound_temperature_adjusted(32.0)),
round(331.5)
);
}
#[test]
fn can_use_inches() {
let config = Config {
distance_unit: DistanceUnit::Inches,
temperature_unit: TemperatureUnit::Celsius,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(round(sensor.distance(343.14, 0.01)), round(67.56));
}
#[test]
fn can_use_fahrenheit_and_inches() {
let config = Config {
distance_unit: DistanceUnit::Inches,
temperature_unit: TemperatureUnit::Fahrenheit,
};
let sensor = Hcsr04::new(OutputPinMock, InputPinMock, config, ClockMock, DelayMock);
assert_eq!(
round(sensor.speed_of_sound_temperature_adjusted(32.0)),
round(331.5)
);
assert_eq!(round(sensor.distance(343.14, 0.01)), round(67.56));
}
}