#![no_std]
#[cfg(feature = "blocking_trigger")]
use embassy_time::block_for;
#[cfg(not(feature = "blocking_trigger"))]
use embassy_time::Timer;
use embassy_time::{with_timeout, Duration, Instant};
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal_async::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 struct Hcsr04<TRIGPIN: OutputPin, ECHOPIN: InputPin + Wait> {
trigger: TRIGPIN,
echo: ECHOPIN,
config: Config,
}
impl<TRIGPIN: OutputPin, ECHOPIN: InputPin + Wait> Hcsr04<TRIGPIN, ECHOPIN> {
pub fn new(trigger: TRIGPIN, echo: ECHOPIN, config: Config) -> Self {
Self {
trigger,
echo,
config,
}
}
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().ok().unwrap() {
return Err("Echo pin is already high");
}
self.trigger.set_high().ok();
#[cfg(feature = "blocking_trigger")]
block_for(Duration::from_micros(10));
#[cfg(not(feature = "blocking_trigger"))]
Timer::after(Duration::from_micros(10)).await;
self.trigger.set_low().ok();
let start = match with_timeout(Duration::from_secs(2), self.echo.wait_for_high()).await {
Ok(_) => Instant::now(),
Err(_) => return Err("Timeout waiting for echo pin to go high"),
};
let end = match with_timeout(Duration::from_secs(2), self.echo.wait_for_low()).await {
Ok(_) => Instant::now(),
Err(_) => return Err("Timeout waiting for echo pin to go low"),
};
let pulse_duration_secs = (end - start).as_micros() 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(())
}
}
#[test]
fn speevd_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);
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);
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);
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);
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);
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);
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);
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);
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);
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));
}
}