Initial proof of concept
This commit is contained in:
2
src/adc/mod.rs
Normal file
2
src/adc/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "stm32")]
|
||||
pub mod stm32;
|
||||
9
src/adc/stm32.rs
Normal file
9
src/adc/stm32.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::quantity::{MilliVolts, Quantity};
|
||||
|
||||
pub fn reading_to_voltage(
|
||||
reading: u32,
|
||||
reference_voltage: MilliVolts<u32>,
|
||||
v_ref_int_scale: u32,
|
||||
) -> MilliVolts<u16> {
|
||||
MilliVolts((reading * v_ref_int_scale / reference_voltage.value()) as u16)
|
||||
}
|
||||
2
src/circuit/mod.rs
Normal file
2
src/circuit/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "resistive-divider")]
|
||||
pub mod resistive_divider;
|
||||
43
src/circuit/resistive_divider.rs
Normal file
43
src/circuit/resistive_divider.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::quantity::{Ohms, Volts};
|
||||
|
||||
/// Given the resistance of the second resistor in a resistive voltage divider, calculate the resistance of the first resistor.
|
||||
pub fn solve_r1(
|
||||
voltage_src: Volts<f32>,
|
||||
voltage_read: Volts<f32>,
|
||||
r2: Ohms<f32>,
|
||||
) -> Ohms<f32> {
|
||||
Ohms(r2.0 * (voltage_src.0 / voltage_read.0 - 1.0))
|
||||
}
|
||||
|
||||
/// Given the resistance of the first resistor in a resistive voltage divider, calculate the resistance of the second resistor.
|
||||
pub fn solve_r2(
|
||||
voltage_src: Volts<f32>,
|
||||
voltage_read: Volts<f32>,
|
||||
r1: Ohms<f32>,
|
||||
) -> Ohms<f32> {
|
||||
Ohms((r1.0 * voltage_read.0) / (voltage_src.0 - voltage_read.0))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::quantity::{OhmsVal, VoltsVal};
|
||||
use super::*;
|
||||
use float_cmp::assert_approx_eq;
|
||||
|
||||
#[test]
|
||||
fn solve_r1_test() {
|
||||
let resistance = solve_r1(3.3.volts(), 2.0.volts(), 1_000.0.ohms());
|
||||
|
||||
assert_approx_eq!(f32, 650.0, resistance.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_r2_test() {
|
||||
let resistance = solve_r2(3.3.volts(), 2.0.volts(), 1_000.0.ohms());
|
||||
|
||||
assert_approx_eq!(f32, 1538.462, resistance.0);
|
||||
}
|
||||
}
|
||||
2
src/control/mod.rs
Normal file
2
src/control/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "pid")]
|
||||
pub mod pid;
|
||||
363
src/control/pid.rs
Normal file
363
src/control/pid.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
//! A proportional-integral-derivative (PID) controller library.
|
||||
//!
|
||||
//! See [Pid] for the adjustable controller itself, as well as [ControlOutput] for the outputs and weights which you can use after setting up your controller. Follow the complete example below to setup your first controller!
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use physical::control::pid::Pid;
|
||||
//!
|
||||
//! // Create a new proportional-only PID controller with a setpoint of 15
|
||||
//! let mut pid = Pid::new(15.0, 100.0);
|
||||
//! pid.proportional_gain = 10.0;
|
||||
//! pid.proportional_limit = 100.0;
|
||||
//!
|
||||
//! // Input a measurement with an error of 5.0 from our setpoint
|
||||
//! let output = pid.next_control_output(10.0);
|
||||
//!
|
||||
//! // Show that the error is correct by multiplying by our kp
|
||||
//! assert_eq!(output, 50.0); // <--
|
||||
//!
|
||||
//! // It won't change on repeat; the controller is proportional-only
|
||||
//! let output = pid.next_control_output(10.0);
|
||||
//! assert_eq!(output, 50.0); // <--
|
||||
//!
|
||||
//! // Add a new integral term to the controller and input again
|
||||
//! pid.integral_gain = 1.0;
|
||||
//! pid.integral_limit = 100.0;
|
||||
//! let output = pid.next_control_output(10.0);
|
||||
//!
|
||||
//! // Now that the integral makes the controller stateful, it will change
|
||||
//! assert_eq!(output, 55.0); // <--
|
||||
//!
|
||||
//! // Add our final derivative term and match our setpoint target
|
||||
//! pid.derivative_gain = 2.0;
|
||||
//! pid.derivative_limit = 100.0;
|
||||
//! let output = pid.next_control_output(15.0);
|
||||
//!
|
||||
//! // The output will now say to go down due to the derivative
|
||||
//! assert_eq!(output, -5.0); // <--//!
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A trait for any numeric type usable in the PID controller
|
||||
///
|
||||
/// This trait is automatically implemented for all types that satisfy `PartialOrd + num_traits::Signed + Copy`. This includes all of the signed float types and builtin integer except for [isize]:
|
||||
/// - [i8]
|
||||
/// - [i16]
|
||||
/// - [i32]
|
||||
/// - [i64]
|
||||
/// - [i128]
|
||||
/// - [f32]
|
||||
/// - [f64]
|
||||
///
|
||||
/// As well as any user type that matches the requirements
|
||||
pub trait Number: PartialOrd + num_traits::Signed + Copy {}
|
||||
|
||||
// Implement `Number` for all types that
|
||||
// satisfy `PartialOrd + num_traits::Signed + Copy`.
|
||||
impl<T: PartialOrd + num_traits::Signed + Copy> Number for T {}
|
||||
|
||||
/// Adjustable proportional-integral-derivative (PID) controller.
|
||||
///
|
||||
/// This [`next_control_output`](Self::next_control_output) method is what's used to input new values into the controller to tell it what the current state of the system is. In the examples above it's only being used once, but realistically this will be a hot method. Please see [ControlOutput] for examples of how to handle these outputs; it's quite straight forward and mirrors the values of this structure in some ways.
|
||||
///
|
||||
/// The last item of note is that the gain and limit fields can be safely modified during use.
|
||||
///
|
||||
/// # Type Warning
|
||||
/// [Number] is abstract and can be used with anything from a [i32] to an [i128] (as well as user-defined types). Because of this, very small types might overflow during calculation in [`next_control_output`](Self::next_control_output). You probably don't want to use [i8] or user-defined types around that size so keep that in mind when designing your controller.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||||
pub struct Pid<T: Number> {
|
||||
/// Ideal setpoint to strive for.
|
||||
pub setpoint: T,
|
||||
/// Defines the overall output filter limit.
|
||||
pub output_limit: T,
|
||||
/// Proportional gain. The proportional component is dependant only on the error.
|
||||
/// It is the error * this value.
|
||||
pub proportional_gain: T,
|
||||
/// Integral gain. The integral component is dependent on the error and the integral term from the previous iteration.
|
||||
/// It is the previous integral + (error * this value).
|
||||
pub integral_gain: T,
|
||||
/// Derivative gain. The derivative component is dependent on the rate of change of the measurement.
|
||||
/// It is the (current measurement - previous measurement) * this value.
|
||||
pub derivative_gain: T,
|
||||
/// Limiter for the proportional term: `-p_limit <= P <= p_limit`.
|
||||
pub proportional_limit: T,
|
||||
/// Limiter for the integral term: `-i_limit <= I <= i_limit`.
|
||||
pub integral_limit: T,
|
||||
/// Limiter for the derivative term: `-d_limit <= D <= d_limit`.
|
||||
pub derivative_limit: T,
|
||||
/// Last calculated integral value if [Pid::i_gain] is used.
|
||||
integral_term: T,
|
||||
/// Previously found measurement whilst using the [Pid::next_control_output] method.
|
||||
prev_measurement: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Pid<T>
|
||||
where
|
||||
T: Number,
|
||||
{
|
||||
/// Creates a new controller with the target setpoint and the output limit
|
||||
pub fn new(setpoint: T, output_limit: T) -> Self {
|
||||
Self {
|
||||
setpoint,
|
||||
output_limit,
|
||||
proportional_gain: T::zero(),
|
||||
integral_gain: T::zero(),
|
||||
derivative_gain: T::zero(),
|
||||
proportional_limit: T::zero(),
|
||||
integral_limit: T::zero(),
|
||||
derivative_limit: T::zero(),
|
||||
integral_term: T::zero(),
|
||||
prev_measurement: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [Pid::setpoint] to target for this controller.
|
||||
pub fn setpoint(&mut self, setpoint: T) -> &mut Self {
|
||||
self.setpoint = setpoint;
|
||||
self
|
||||
}
|
||||
|
||||
/// Given a new measurement, calculates the next control setting.
|
||||
pub fn next_control_output(&mut self, measurement: T) -> T {
|
||||
// Calculate the error between the ideal setpoint and the current
|
||||
// measurement to compare against
|
||||
let error = self.setpoint - measurement;
|
||||
|
||||
// Calculate the proportional term and limit to it's individual limit
|
||||
let p_unbounded = error * self.proportional_gain;
|
||||
let p = apply_limit(self.proportional_limit, p_unbounded);
|
||||
|
||||
// Mitigate output jumps when ki(t) != ki(t-1).
|
||||
// While it's standard to use an error_integral that's a running sum of
|
||||
// just the error (no ki), because we support ki changing dynamically,
|
||||
// we store the entire term so that we don't need to remember previous
|
||||
// ki values.
|
||||
self.integral_term = self.integral_term + error * self.integral_gain;
|
||||
|
||||
// Mitigate integral windup: Don't want to keep building up error
|
||||
// beyond what i_limit will allow.
|
||||
self.integral_term = apply_limit(self.integral_limit, self.integral_term);
|
||||
|
||||
// Mitigate derivative kick: Use the derivative of the measurement
|
||||
// rather than the derivative of the error.
|
||||
let d_unbounded = -match self.prev_measurement {
|
||||
Some(prev_measurement) => measurement - prev_measurement,
|
||||
None => T::zero(),
|
||||
} * self.derivative_gain;
|
||||
self.prev_measurement = Some(measurement);
|
||||
let d = apply_limit(self.derivative_limit, d_unbounded);
|
||||
|
||||
// Calculate the final output by adding together the PID terms, then
|
||||
// apply the final defined output limit
|
||||
let output = p + self.integral_term + d;
|
||||
let output = apply_limit(self.output_limit, output);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Resets the integral term back to zero, this may drastically change the
|
||||
/// control output.
|
||||
pub fn reset_integral_term(&mut self) {
|
||||
self.integral_term = T::zero();
|
||||
}
|
||||
}
|
||||
|
||||
/// Saturating the input `value` according the absolute `limit` (`-abs(limit) <= output <= abs(limit)`).
|
||||
fn apply_limit<T: Number>(limit: T, value: T) -> T {
|
||||
num_traits::clamp(value, -limit.abs(), limit.abs())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Pid;
|
||||
|
||||
/// Proportional-only controller operation and limits
|
||||
#[test]
|
||||
fn proportional() {
|
||||
let mut pid = Pid::new(10.0, 100.0);
|
||||
pid.proportional_gain = 2.0;
|
||||
pid.proportional_limit = 100.0;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
assert_eq!(pid.setpoint, 10.0);
|
||||
|
||||
// Test simple proportional
|
||||
assert_eq!(pid.next_control_output(0.0), 20.0);
|
||||
|
||||
// Test proportional limit
|
||||
pid.proportional_limit = 10.0;
|
||||
assert_eq!(pid.next_control_output(0.0), 10.0);
|
||||
}
|
||||
|
||||
/// Derivative-only controller operation and limits
|
||||
#[test]
|
||||
fn derivative() {
|
||||
let mut pid = Pid::new(10.0, 100.0);
|
||||
pid.proportional_limit = 100.0;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
pid.derivative_gain = 2.0;
|
||||
|
||||
// Test that there's no derivative since it's the first measurement
|
||||
assert_eq!(pid.next_control_output(0.0), 0.0);
|
||||
|
||||
// Test that there's now a derivative
|
||||
assert_eq!(pid.next_control_output(5.0), -10.0);
|
||||
|
||||
// Test derivative limit
|
||||
pid.derivative_limit = 5.0;
|
||||
assert_eq!(pid.next_control_output(10.0), -5.0);
|
||||
}
|
||||
|
||||
/// Integral-only controller operation and limits
|
||||
#[test]
|
||||
fn integral() {
|
||||
let mut pid = Pid::new(10.0, 100.0);
|
||||
pid.proportional_limit = 0.0;
|
||||
pid.integral_gain = 2.0;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
|
||||
// Test basic integration
|
||||
assert_eq!(pid.next_control_output(0.0), 20.0);
|
||||
assert_eq!(pid.next_control_output(0.0), 40.0);
|
||||
assert_eq!(pid.next_control_output(5.0), 50.0);
|
||||
|
||||
// Test limit
|
||||
pid.integral_limit = 50.0;
|
||||
assert_eq!(pid.next_control_output(5.0), 50.0);
|
||||
// Test that limit doesn't impede reversal of error integral
|
||||
assert_eq!(pid.next_control_output(15.0), 40.0);
|
||||
|
||||
// Test that error integral accumulates negative values
|
||||
let mut pid2 = Pid::new(-10.0, 100.0);
|
||||
pid2.proportional_limit = 100.0;
|
||||
pid2.integral_gain = 2.0;
|
||||
pid2.integral_limit = 100.0;
|
||||
pid2.derivative_limit = 100.0;
|
||||
|
||||
assert_eq!(pid2.next_control_output(0.0), -20.0);
|
||||
assert_eq!(pid2.next_control_output(0.0), -40.0);
|
||||
|
||||
pid2.integral_limit = 50.0;
|
||||
assert_eq!(pid2.next_control_output(-5.0), -50.0);
|
||||
// Test that limit doesn't impede reversal of error integral
|
||||
assert_eq!(pid2.next_control_output(-15.0), -40.0);
|
||||
}
|
||||
|
||||
/// Checks that a full PID controller's limits work properly through multiple output iterations
|
||||
#[test]
|
||||
fn output_limit() {
|
||||
let mut pid = Pid::new(10.0, 1.0);
|
||||
pid.proportional_gain = 2.0;
|
||||
pid.proportional_limit = 100.0;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
|
||||
let out = pid.next_control_output(0.0);
|
||||
assert_eq!(out, 1.0);
|
||||
|
||||
let out = pid.next_control_output(20.0);
|
||||
assert_eq!(out, -1.0);
|
||||
}
|
||||
|
||||
/// Combined PID operation
|
||||
#[test]
|
||||
fn pid() {
|
||||
let mut pid = Pid::new(10.0, 100.0);
|
||||
pid.proportional_gain = 1.0;
|
||||
pid.proportional_limit = 100.0;
|
||||
pid.integral_gain = 0.1;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_gain = 1.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
|
||||
let out = pid.next_control_output(0.0);
|
||||
assert_eq!(out, 11.0);
|
||||
|
||||
let out = pid.next_control_output(5.0);
|
||||
assert_eq!(out, 1.5);
|
||||
|
||||
let out = pid.next_control_output(11.0);
|
||||
assert_eq!(out, -5.6);
|
||||
|
||||
let out = pid.next_control_output(10.0);
|
||||
assert_eq!(out, 2.4);
|
||||
}
|
||||
|
||||
// NOTE: use for new test in future: /// Full PID operation with mixed float checking to make sure they're equal
|
||||
/// PID operation with zero'd values, checking to see if different floats equal each other
|
||||
#[test]
|
||||
fn floats_zeros() {
|
||||
let mut pid_f32 = Pid::new(10.0f32, 100.0);
|
||||
pid_f32.proportional_limit = 100.0;
|
||||
pid_f32.integral_limit = 100.0;
|
||||
pid_f32.derivative_limit = 100.0;
|
||||
|
||||
let mut pid_f64 = Pid::new(10.0, 100.0f64);
|
||||
pid_f64.proportional_limit = 100.0;
|
||||
pid_f64.integral_limit = 100.0;
|
||||
pid_f64.derivative_limit = 100.0;
|
||||
|
||||
for _ in 0..5 {
|
||||
assert_eq!(pid_f32.next_control_output(0.0), pid_f64.next_control_output(0.0) as f32);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: use for new test in future: /// Full PID operation with mixed signed integer checking to make sure they're equal
|
||||
/// PID operation with zero'd values, checking to see if different floats equal each other
|
||||
#[test]
|
||||
fn signed_integers_zeros() {
|
||||
let mut pid_i8 = Pid::new(10i8, 100);
|
||||
pid_i8.proportional_limit = 100;
|
||||
pid_i8.integral_limit = 100;
|
||||
pid_i8.derivative_limit = 100;
|
||||
|
||||
let mut pid_i32 = Pid::new(10i32, 100);
|
||||
pid_i32.proportional_limit = 100;
|
||||
pid_i32.integral_limit = 100;
|
||||
pid_i32.derivative_limit = 100;
|
||||
|
||||
for _ in 0..5 {
|
||||
assert_eq!(pid_i32.next_control_output(0), pid_i8.next_control_output(0i8) as i32);
|
||||
}
|
||||
}
|
||||
|
||||
/// See if the controller can properly target to the setpoint after 2 output iterations
|
||||
#[test]
|
||||
fn setpoint() {
|
||||
let mut pid = Pid::new(10.0, 100.0);
|
||||
pid.proportional_gain = 1.0;
|
||||
pid.proportional_limit = 100.0;
|
||||
pid.integral_gain = 0.1;
|
||||
pid.integral_limit = 100.0;
|
||||
pid.derivative_gain = 1.0;
|
||||
pid.derivative_limit = 100.0;
|
||||
|
||||
let out = pid.next_control_output(0.0);
|
||||
assert_eq!(out, 11.0);
|
||||
|
||||
pid.setpoint(0.0);
|
||||
|
||||
assert_eq!(pid.next_control_output(0.0), 1.0);
|
||||
}
|
||||
|
||||
/// Make sure negative limits don't break the controller
|
||||
#[test]
|
||||
fn negative_limits() {
|
||||
let mut pid = Pid::new(10.0f32, -10.0);
|
||||
pid.proportional_gain = 1.0;
|
||||
pid.proportional_limit = -50.0;
|
||||
pid.integral_gain = 1.0;
|
||||
pid.integral_limit = -50.0;
|
||||
pid.derivative_gain = 1.0;
|
||||
pid.derivative_limit = -50.0;
|
||||
|
||||
let out = pid.next_control_output(0.0);
|
||||
assert_eq!(out, 10.0);
|
||||
}
|
||||
}
|
||||
41
src/error.rs
Normal file
41
src/error.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
/// Indicates the transducer value is known to be impossible.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InvalidValue;
|
||||
|
||||
/// Indicates that the encoded data is not valid for the type.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InvalidEncoding;
|
||||
/// An error that it is likely impossible to recover from. This error should only be created in
|
||||
/// situations where attempts to recover have already been attempted and have failed. Error handling
|
||||
/// should consist of attempting to alert another system for maintenance and attempting to shut down
|
||||
/// any systems that depend on the correct functionality of the component having errors.
|
||||
///
|
||||
/// In many systems there can be a single function for handling any critical error as a critical
|
||||
/// error always means everything needs to be stopped.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum CriticalError {
|
||||
/// Critical communication failed and retries are either impossible or also failed.
|
||||
Communication,
|
||||
InvalidValue(InvalidValue),
|
||||
}
|
||||
|
||||
/// A state of this type may mean the program has encountered an error that prevents it from continuing to run
|
||||
/// and should attempt to enter a safe terminal state.
|
||||
/// e.g. Certain [Err]s
|
||||
pub trait Terminal<T, E> {
|
||||
//TODO: Switch to using ! as the return type for the FnOnce when the feature is stabilized
|
||||
fn terminal(self, terminate: impl FnOnce(E)) -> T;
|
||||
}
|
||||
|
||||
impl <T, E> Terminal<T, E> for Result<T, E> {
|
||||
fn terminal(self, terminate: impl FnOnce(E)) -> T {
|
||||
match self {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
//TODO: Remove panic when terminate returns -> !
|
||||
terminate(error);
|
||||
panic!()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod transducer;
|
||||
pub mod control;
|
||||
pub mod error;
|
||||
|
||||
pub mod adc;
|
||||
pub mod circuit;
|
||||
pub mod quantity;
|
||||
|
||||
pub use error::CriticalError;
|
||||
5
src/quantity/irradiance.rs
Normal file
5
src/quantity/irradiance.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::quantity::{Quantity, Value};
|
||||
use generate_quantity::quantity_type;
|
||||
|
||||
//----- Watts per Square Meter ----------------------------------
|
||||
quantity_type! {WattsPerSquareMeter, "W/m²"}
|
||||
170
src/quantity/mod.rs
Normal file
170
src/quantity/mod.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
mod irradiance;
|
||||
mod resistance;
|
||||
mod temperature;
|
||||
mod voltage;
|
||||
mod volume;
|
||||
mod volume_rate;
|
||||
mod pressure;
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
pub use defmt_impl::*;
|
||||
|
||||
pub use irradiance::*;
|
||||
pub use resistance::*;
|
||||
pub use temperature::*;
|
||||
pub use voltage::*;
|
||||
pub use volume::*;
|
||||
pub use volume_rate::*;
|
||||
pub use pressure::*;
|
||||
|
||||
use core::fmt::Display;
|
||||
use core::marker::PhantomData;
|
||||
use core::ops::{Add, Sub};
|
||||
use num_traits::{FromPrimitive, Num, NumCast};
|
||||
|
||||
const DECA: u8 = 10;
|
||||
const DECI: u8 = 10;
|
||||
const HECTO: u8 = 100;
|
||||
const CENTI: u8 = 100;
|
||||
const KILO: u16 = 1_000;
|
||||
const MILLI: u16 = 1_000;
|
||||
const MEGA: u32 = 1_000_000;
|
||||
const MICRO: u32 = 1_000_000;
|
||||
const GIGA: u32 = 1_000_000_000;
|
||||
const NANO: u32 = 1_000_000_000;
|
||||
|
||||
pub trait Quantity<V: Value>: Copy + PartialEq + PartialOrd + Add + Sub {
|
||||
fn value(self) -> V;
|
||||
|
||||
fn symbol() -> &'static str;
|
||||
|
||||
/// Returns a wrapper that implements [Display] and [defmt::Format] for [Quantity]s with core number values.
|
||||
///
|
||||
/// - `rounding` - Sets the number of decimal places to round to when formatting (currently ignored with defmt).
|
||||
#[inline(always)]
|
||||
fn fmt(self, rounding: Option<usize>) -> Fmt<V, Self> {
|
||||
Fmt::new(self, rounding)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Value: Num + Copy + PartialOrd + FromPrimitive + NumCast + Display {}
|
||||
impl<V> Value for V where V: Num + Copy + PartialOrd + FromPrimitive + NumCast + Display {}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Fmt<V: Value, Q: Quantity<V>> {
|
||||
quantity: Q,
|
||||
rounding: Option<usize>,
|
||||
_phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V: Value, Q: Quantity<V>> Fmt<V, Q> {
|
||||
#[inline(always)]
|
||||
fn new(quantity: Q, rounding: Option<usize>) -> Self {
|
||||
Self {
|
||||
quantity,
|
||||
rounding,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Value, Q: Quantity<V>> Display for Fmt<V, Q> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self.rounding {
|
||||
Some(places) => {
|
||||
write!(f, "{:.prec$} {}", self.quantity.value(), Q::symbol(), prec = places)
|
||||
},
|
||||
None => write!(f, "{} {}", self.quantity.value(), Q::symbol()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "defmt")]
|
||||
mod defmt_impl {
|
||||
use super::*;
|
||||
use defmt::{write, Format, Formatter};
|
||||
|
||||
impl<Q: Quantity<u8>> Format for Fmt<u8, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<u16>> Format for Fmt<u16, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<u32>> Format for Fmt<u32, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<u64>> Format for Fmt<u64, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<u128>> Format for Fmt<u128, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<usize>> Format for Fmt<usize, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<i8>> Format for Fmt<i8, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<i16>> Format for Fmt<i16, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<i32>> Format for Fmt<i32, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<i64>> Format for Fmt<i64, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<i128>> Format for Fmt<i128, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<isize>> Format for Fmt<isize, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<f32>> Format for Fmt<f32, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Q: Quantity<f64>> Format for Fmt<f64, Q> {
|
||||
fn format(&self, fmt: Formatter) {
|
||||
write!(fmt, "{} {}", self.quantity.value(), Q::symbol());
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/quantity/pressure.rs
Normal file
99
src/quantity/pressure.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::quantity::{KILO, Quantity, Value};
|
||||
use generate_quantity::quantity_type;
|
||||
|
||||
const PASCALS_PER_TORR: f64 = 101325.0 / 760.0;
|
||||
const KILO_PASCALS_PER_PSI: f64 = 6.894757293168364;
|
||||
|
||||
//----- Pascals ----------------------------------
|
||||
quantity_type! {Pascals, "Pa"}
|
||||
|
||||
impl<V: Value> Pascals<V> {
|
||||
#[inline]
|
||||
pub fn to_kilo_pascals(self) -> KiloPascals<V> {
|
||||
let divisor = V::from_u16(KILO).unwrap();
|
||||
KiloPascals(self.0 / divisor)
|
||||
}
|
||||
|
||||
// Should this be in separate imple with `V` bound to `num_traits::Float`?
|
||||
#[inline]
|
||||
pub fn to_torr(self) -> Torr<V> {
|
||||
let divisor = V::from_f64(PASCALS_PER_TORR).unwrap();
|
||||
Torr(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Kilopascals ----------------------------------
|
||||
quantity_type! {KiloPascals, "kPa"}
|
||||
|
||||
impl<V: Value> KiloPascals<V> {
|
||||
#[inline]
|
||||
pub fn to_pascals(self) -> Pascals<V> {
|
||||
let multiplier = V::from_u16(KILO).unwrap();
|
||||
Pascals(self.0 * multiplier)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_psi(self) -> Psi<V> {
|
||||
let divisor = V::from_f64(KILO_PASCALS_PER_PSI).unwrap();
|
||||
Psi(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Torr ----------------------------------
|
||||
quantity_type! {Torr, "Torr"}
|
||||
|
||||
// Should this bound `V` to `num_traits::Float`?
|
||||
impl<V: Value> Torr<V> {
|
||||
#[inline]
|
||||
pub fn to_pascals(self) -> Pascals<V> {
|
||||
let multiplier = V::from_f64(PASCALS_PER_TORR).unwrap();
|
||||
Pascals(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
//----- PSI ----------------------------------
|
||||
quantity_type! {Psi, "PSI"}
|
||||
|
||||
impl<V: Value> Psi<V> {
|
||||
#[inline]
|
||||
pub fn to_kilo_pascals(self) -> KiloPascals<V> {
|
||||
let multiplier = V::from_f64(KILO_PASCALS_PER_PSI).unwrap();
|
||||
KiloPascals(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use float_cmp::assert_approx_eq;
|
||||
|
||||
#[test]
|
||||
fn pascals_kilo_pascals() {
|
||||
let pascals: Pascals<u32> = 1_000.pascals();
|
||||
let kilo_pascals: KiloPascals<u32> = 1.kilo_pascals();
|
||||
|
||||
assert_eq!(pascals.0, kilo_pascals.to_pascals().0);
|
||||
assert_eq!(kilo_pascals.0, pascals.to_kilo_pascals().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torr_pascals() {
|
||||
let torr: Torr<f32> = 7.5.torr();
|
||||
let pascals: Pascals<f32> = 999.9177631578947.pascals();
|
||||
|
||||
assert_approx_eq!(f32, pascals.to_torr().0, torr.0);
|
||||
assert_approx_eq!(f32, torr.to_pascals().0, pascals.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psi_kilo_pascals() {
|
||||
let psi: Psi<f32> = 2.5.psi();
|
||||
let kilo_pascals: KiloPascals<f32> = 17.23689323292091.kilo_pascals();
|
||||
|
||||
assert_approx_eq!(f32, psi.to_kilo_pascals().0, kilo_pascals.0);
|
||||
assert_approx_eq!(f32, kilo_pascals.to_psi().0, psi.0);
|
||||
}
|
||||
}
|
||||
5
src/quantity/resistance.rs
Normal file
5
src/quantity/resistance.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use generate_quantity::quantity_type;
|
||||
use crate::quantity::{Quantity, Value};
|
||||
|
||||
//----- Ohms ----------------------------------
|
||||
quantity_type! {Ohms, "Ω"}
|
||||
96
src/quantity/temperature.rs
Normal file
96
src/quantity/temperature.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use generate_quantity::quantity_type;
|
||||
use crate::quantity::{DECI, Quantity, Value};
|
||||
|
||||
const KELVIN_CELSIUS_OFFSET: f64 = 273.15;
|
||||
|
||||
//----- Kelvins ----------------------------------
|
||||
quantity_type! {Kelvins, "K"}
|
||||
|
||||
impl <V: Value> Kelvins<V> {
|
||||
#[inline]
|
||||
pub fn to_celsius(self) -> Celsius<V> {
|
||||
//TODO: Make const
|
||||
let offset = V::from_f64(KELVIN_CELSIUS_OFFSET).unwrap();
|
||||
Celsius(self.0 - offset)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_deci_kelvins(self) -> DeciKelvins<V> {
|
||||
let multiplier = V::from_u8(DECI).unwrap();
|
||||
DeciKelvins(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Decikelvins ----------------------------------
|
||||
quantity_type! {DeciKelvins, "dK"}
|
||||
|
||||
impl<V: Value> DeciKelvins<V> {
|
||||
#[inline]
|
||||
pub fn to_kelvins(self) -> Kelvins<V> {
|
||||
let divisor = V::from_u8(DECI).unwrap();
|
||||
Kelvins(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Degrees Celsius ----------------------------------
|
||||
quantity_type! {Celsius, "℃"}
|
||||
|
||||
impl <V: Value> Celsius<V> {
|
||||
#[inline]
|
||||
pub fn to_kelvins(self) -> Kelvins<V> {
|
||||
//TODO: Make const
|
||||
let offset = V::from_f64(KELVIN_CELSIUS_OFFSET).unwrap();
|
||||
Kelvins(self.0 + offset)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_deci_celsius(self) -> DeciCelsius<V> {
|
||||
let multiplier = V::from_u8(DECI).unwrap();
|
||||
DeciCelsius(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Deci Degrees Celsius ----------------------------------
|
||||
quantity_type! {DeciCelsius, "d℃"}
|
||||
|
||||
impl<V: Value> DeciCelsius<V> {
|
||||
#[inline]
|
||||
pub fn to_celsius(self) -> Celsius<V> {
|
||||
let divisor = V::from_u8(DECI).unwrap();
|
||||
Celsius(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use float_cmp::assert_approx_eq;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_f32() {
|
||||
let kelvins: Kelvins<f32> = 298.15.kelvins();
|
||||
let deci_kelvins: DeciKelvins<f32> = 2_981.5.deci_kelvins();
|
||||
let celsius: Celsius<f32> = 25.0.celsius();
|
||||
let deci_celsius: DeciCelsius<f32> = 250.0.deci_celsius();
|
||||
|
||||
assert_approx_eq!(f32, kelvins.to_celsius().0, celsius.0);
|
||||
assert_approx_eq!(f32, celsius.to_kelvins().0, kelvins.0);
|
||||
|
||||
assert_approx_eq!(f32, deci_kelvins.to_kelvins().0, kelvins.0);
|
||||
assert_approx_eq!(f32, kelvins.to_deci_kelvins().0, deci_kelvins.0);
|
||||
|
||||
assert_approx_eq!(f32, deci_celsius.to_celsius().0, celsius.0);
|
||||
assert_approx_eq!(f32, celsius.to_deci_celsius().0, deci_celsius.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_u16() {
|
||||
let kelvins: Kelvins<u16> = 298.kelvins();
|
||||
let celsius: Celsius<u16> = 25.celsius();
|
||||
|
||||
assert_eq!(celsius.0, kelvins.to_celsius().0);
|
||||
}
|
||||
}
|
||||
52
src/quantity/voltage.rs
Normal file
52
src/quantity/voltage.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use generate_quantity::quantity_type;
|
||||
use crate::quantity::{Quantity, Value};
|
||||
|
||||
const VOLT_MV_RATIO: u16 = 1_000;
|
||||
|
||||
//----- Volts ----------------------------------
|
||||
quantity_type! {Volts, "V"}
|
||||
|
||||
impl<V: Value> Volts<V> {
|
||||
#[inline]
|
||||
pub fn to_milli_volts(self) -> MilliVolts<V> {
|
||||
let multiplier = V::from_u16(VOLT_MV_RATIO).unwrap();
|
||||
MilliVolts(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Millivolts ----------------------------------
|
||||
quantity_type! {MilliVolts, "mV"}
|
||||
|
||||
impl<V: Value> MilliVolts<V> {
|
||||
pub fn to_volts(self) -> Volts<V> {
|
||||
let divisor = V::from_u16(VOLT_MV_RATIO).unwrap();
|
||||
Volts(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use float_cmp::assert_approx_eq;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_u32() {
|
||||
let volts: Volts<u32> = 3.volts();
|
||||
let millivolts: MilliVolts<u32> = 3_000.milli_volts();
|
||||
|
||||
assert_eq!(volts.to_milli_volts().0, millivolts.0);
|
||||
assert_eq!(millivolts.to_volts().0, volts.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_f64() {
|
||||
let volts: Volts<f64> = 3.0.volts();
|
||||
let millivolts: MilliVolts<f64> = 3_000.0.milli_volts();
|
||||
|
||||
assert_approx_eq!(f64, volts.to_milli_volts().0, millivolts.0);
|
||||
assert_approx_eq!(f64, millivolts.to_volts().0, volts.0);
|
||||
}
|
||||
}
|
||||
43
src/quantity/volume.rs
Normal file
43
src/quantity/volume.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::quantity::{Quantity, Value};
|
||||
use generate_quantity::quantity_type;
|
||||
|
||||
use crate::quantity::MILLI;
|
||||
|
||||
//----- Liters ----------------------------------
|
||||
quantity_type! {Liters, "L"}
|
||||
|
||||
impl<V: Value> Liters<V> {
|
||||
#[inline]
|
||||
pub fn to_milli_liters(self) -> MilliLiters<V> {
|
||||
let multiplier = V::from_u16(MILLI).unwrap();
|
||||
MilliLiters(self.0 * multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
//----- Milliliters ----------------------------------
|
||||
quantity_type! {MilliLiters, "mL"}
|
||||
|
||||
impl<V: Value> MilliLiters<V> {
|
||||
#[inline]
|
||||
pub fn to_liters(self) -> MilliLiters<V> {
|
||||
let divisor = V::from_u16(MILLI).unwrap();
|
||||
MilliLiters(self.0 / divisor)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_u32() {
|
||||
let liters: Liters<u32> = 12.liters();
|
||||
let milli_liters: MilliLiters<u32> = 12_000.milli_liters();
|
||||
|
||||
assert_eq!(liters.0, milli_liters.to_liters().0);
|
||||
assert_eq!(milli_liters.0, liters.to_milli_liters().0);
|
||||
}
|
||||
}
|
||||
5
src/quantity/volume_rate.rs
Normal file
5
src/quantity/volume_rate.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::quantity::{Quantity, Value};
|
||||
use generate_quantity::quantity_type;
|
||||
|
||||
//----- Liters per Minute ----------------------------------
|
||||
quantity_type! {LitersPerMinute, "L/min"}
|
||||
3
src/transducer/mod.rs
Normal file
3
src/transducer/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod part;
|
||||
|
||||
pub use part::*;
|
||||
16
src/transducer/part/lm35.rs
Normal file
16
src/transducer/part/lm35.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::error::InvalidValue;
|
||||
use crate::quantity::{DeciCelsius, MilliVolts, Quantity};
|
||||
|
||||
#[inline]
|
||||
pub fn convert(
|
||||
voltage: MilliVolts<i16>,
|
||||
) -> Result<DeciCelsius<i16>, InvalidValue> {
|
||||
const MIN_VOLTAGE: MilliVolts<i16> = MilliVolts(-550);
|
||||
const MAX_VOLTAGE: MilliVolts<i16> = MilliVolts(1_500);
|
||||
|
||||
if voltage >= MIN_VOLTAGE && voltage <= MAX_VOLTAGE {
|
||||
Ok(DeciCelsius(voltage.value()))
|
||||
} else {
|
||||
Err(InvalidValue)
|
||||
}
|
||||
}
|
||||
10
src/transducer/part/mod.rs
Normal file
10
src/transducer/part/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod thermocouple;
|
||||
|
||||
#[cfg(feature = "lm35")]
|
||||
pub mod lm35;
|
||||
|
||||
#[cfg(feature = "thermistor")]
|
||||
pub mod thermistor;
|
||||
|
||||
#[cfg(feature = "thermocouple-k")]
|
||||
pub use thermocouple::type_k as thermocouple_k;
|
||||
38
src/transducer/part/thermistor.rs
Normal file
38
src/transducer/part/thermistor.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::quantity::{Kelvins, Ohms};
|
||||
use libm::{log, logf};
|
||||
|
||||
/// Convert thermistor resistance to a temperature using beta parameter equation
|
||||
pub fn convert_beta(
|
||||
resistance: Ohms<f32>,
|
||||
beta: f32,
|
||||
reference_temp: Kelvins<f32>,
|
||||
reference_resist: Ohms<f32>,
|
||||
) -> Kelvins<f32> {
|
||||
let kelvins = 1.0 / ((logf(resistance.0 / reference_resist.0) / beta) + 1.0 / reference_temp.0);
|
||||
Kelvins(kelvins)
|
||||
}
|
||||
|
||||
/// Convert thermistor resistance to a temperature using Steinhart-Hart equation
|
||||
pub fn convert_steinhart(resistance: Ohms<f64>, a: f64, b: f64, c: f64) -> Kelvins<f32> {
|
||||
let log_omhs = log(resistance.0);
|
||||
let kelvins = 1.0 / (a + b * log_omhs + c * log_omhs * log_omhs * log_omhs);
|
||||
Kelvins(kelvins as f32)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Tests ------------------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use float_cmp::assert_approx_eq;
|
||||
|
||||
use crate::quantity::{OhmsVal, KelvinsVal};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_beta_test() {
|
||||
let temperature = convert_beta(1538.462.ohms(), 3950.0, 298.15.kelvins(), 100_000.0.ohms());
|
||||
|
||||
assert_approx_eq!(f32, 435.31073, temperature.0);
|
||||
}
|
||||
}
|
||||
2
src/transducer/part/thermocouple/mod.rs
Normal file
2
src/transducer/part/thermocouple/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "thermocouple-k")]
|
||||
pub mod type_k;
|
||||
169
src/transducer/part/thermocouple/type_k.rs
Normal file
169
src/transducer/part/thermocouple/type_k.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
//! Note - Thermocouple conversion uses [f64] arithmetic internally.
|
||||
|
||||
use libm::pow;
|
||||
use crate::error::InvalidValue;
|
||||
use crate::quantity::{Celsius, MilliVolts, Quantity};
|
||||
|
||||
fn _convert(
|
||||
voltage: MilliVolts<f64>,
|
||||
) -> Result<Celsius<f32>, InvalidValue> {
|
||||
let mv = voltage.value();
|
||||
let mv_pow2 = mv * mv;
|
||||
let mv_pow3 = mv_pow2 * mv;
|
||||
let mv_pow4 = mv_pow3 * mv;
|
||||
let mv_pow5 = mv_pow4 * mv;
|
||||
let mv_pow6 = mv_pow5 * mv;
|
||||
|
||||
if mv >= -5.891 && mv <= 0.0 {
|
||||
let mv_pow7 = mv_pow6 * mv;
|
||||
let mv_pow8 = mv_pow7 * mv;
|
||||
|
||||
let celsius = 2.5173462E+1 * mv
|
||||
+ -1.1662878 * mv_pow2
|
||||
+ -1.0833638 * mv_pow3
|
||||
+ -8.9773540E-1 * mv_pow4
|
||||
+ -3.7342377E-1 * mv_pow5
|
||||
+ -8.6632643E-2 * mv_pow6
|
||||
+ -1.0450598E-2 * mv_pow7
|
||||
+ -5.1920577E-4 * mv_pow8;
|
||||
|
||||
Ok(Celsius(celsius as f32))
|
||||
} else if mv > 0.0 && mv < 20.644 {
|
||||
let mv_pow7 = mv_pow6 * mv;
|
||||
let mv_pow8 = mv_pow7 * mv;
|
||||
let mv_pow9 = mv_pow8 * mv;
|
||||
|
||||
let celsius = 2.508355E+1 * mv
|
||||
+ 7.860106E-2 * mv_pow2
|
||||
+ -2.503131E-1 * mv_pow3
|
||||
+ 8.315270E-2 * mv_pow4
|
||||
+ -1.228034E-2 * mv_pow5
|
||||
+ 9.804036E-4 * mv_pow6
|
||||
+ -4.413030E-5 * mv_pow7
|
||||
+ 1.057734E-6 * mv_pow8
|
||||
+ -1.052755E-8 * mv_pow9;
|
||||
|
||||
Ok(Celsius(celsius as f32))
|
||||
} else if mv >= 20.644 && mv <= 54.886 {
|
||||
let celsius = 1.318058e2
|
||||
+ 4.830222E+1 * mv
|
||||
+ -1.646031 * mv_pow2
|
||||
+ 5.464731E-2 * mv_pow3
|
||||
+ -9.650715E-4 * mv_pow4
|
||||
+ 8.802193E-6 * mv_pow5
|
||||
+ -3.110810E-8 * mv_pow6;
|
||||
|
||||
Ok(Celsius(celsius as f32))
|
||||
} else {
|
||||
Err(InvalidValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and
|
||||
/// directly adding the reference junction temperature to the result for offset compensation.
|
||||
///
|
||||
/// Can be useful compared to [convert_seebeck] when the reference temperature or the temperature
|
||||
/// being read by the thermocouple is fairly close to 0.
|
||||
///
|
||||
/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html).
|
||||
#[inline]
|
||||
pub fn convert_direct(
|
||||
voltage: MilliVolts<f64>,
|
||||
r_junction: Celsius<f32>,
|
||||
) -> Result<Celsius<f32>, InvalidValue> {
|
||||
let base_temp = _convert(voltage)?;
|
||||
|
||||
Ok(base_temp + r_junction)
|
||||
}
|
||||
|
||||
/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and
|
||||
/// using a constant seebeck coefficient to correct the input voltage for offset compensation.
|
||||
///
|
||||
/// Probably the right choice most of the time.
|
||||
///
|
||||
/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html).
|
||||
#[inline]
|
||||
pub fn convert_seebeck(
|
||||
voltage: MilliVolts<f64>,
|
||||
r_junction: Celsius<f32>,
|
||||
) -> Result<Celsius<f32>, InvalidValue> {
|
||||
let voltage_correction = temp_to_voltage_seebeck(r_junction)?;
|
||||
_convert(MilliVolts(voltage.0 + voltage_correction.0 as f64))
|
||||
}
|
||||
|
||||
/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and
|
||||
/// using a polynomial to correct the input voltage for offset compensation.
|
||||
///
|
||||
/// This is the most accurate method but uses the most processor cycles by a wide margin.
|
||||
///
|
||||
/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html).
|
||||
#[inline]
|
||||
pub fn convert_polynomial(
|
||||
voltage: MilliVolts<f64>,
|
||||
r_junction: Celsius<f64>,
|
||||
) -> Result<Celsius<f32>, InvalidValue> {
|
||||
let voltage_correction = temp_to_voltage_poly(r_junction)?;
|
||||
_convert(MilliVolts(voltage.0 + voltage_correction.0 as f64))
|
||||
}
|
||||
|
||||
pub fn temp_to_voltage_poly(
|
||||
temperature: Celsius<f64>,
|
||||
) -> Result<MilliVolts<f32>, InvalidValue> {
|
||||
let celsius = temperature.value();
|
||||
let cel_pow2 = celsius * celsius;
|
||||
let cel_pow3 = cel_pow2 * celsius;
|
||||
let cel_pow4 = cel_pow3 * celsius;
|
||||
let cel_pow5 = cel_pow4 * celsius;
|
||||
let cel_pow6 = cel_pow5 * celsius;
|
||||
let cel_pow7 = cel_pow6 * celsius;
|
||||
let cel_pow8 = cel_pow7 * celsius;
|
||||
let cel_pow9 = cel_pow8 * celsius;
|
||||
|
||||
if celsius >= -270.0 && celsius < 0.0 {
|
||||
let cel_pow10 = cel_pow9 * celsius;
|
||||
|
||||
let mv = 0.394501280250E-01 * celsius
|
||||
+ 0.236223735980E-04 * cel_pow2
|
||||
+ -0.328589067840E-06 * cel_pow3
|
||||
+ -0.499048287770E-08 * cel_pow4
|
||||
+ -0.675090591730E-10 * cel_pow5
|
||||
+ -0.574103274280E-12 * cel_pow6
|
||||
+ -0.310888728940E-14 * cel_pow7
|
||||
+ -0.104516093650E-16 * cel_pow8
|
||||
+ -0.198892668780E-19 * cel_pow9
|
||||
+ -0.163226974860E-22 * cel_pow10;
|
||||
|
||||
Ok(MilliVolts(mv as f32))
|
||||
} else if celsius >= 0.0 && celsius <= 1372.0 {
|
||||
let base = celsius - 0.126968600000E+03;
|
||||
let exp = -0.118343200000E-03 * (base * base);
|
||||
let addition = pow(0.1185976, exp);
|
||||
|
||||
let mv = -0.176004136860E-01
|
||||
+ 0.389212049750E-01 * celsius
|
||||
+ 0.185587700320E-04 * cel_pow2
|
||||
+ -0.994575928740E-07 * cel_pow3
|
||||
+ 0.318409457190E-09 * cel_pow4
|
||||
+ -0.560728448890E-12 * cel_pow5
|
||||
+ 0.560750590590E-15 * cel_pow6
|
||||
+ -0.320207200030E-18 * cel_pow7
|
||||
+ 0.971511471520E-22 * cel_pow8
|
||||
+ -0.121047212750E-25 * cel_pow9
|
||||
+ addition;
|
||||
|
||||
Ok(MilliVolts(mv as f32))
|
||||
} else {
|
||||
Err(InvalidValue)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn temp_to_voltage_seebeck(
|
||||
temperature: Celsius<f32>,
|
||||
) -> Result<MilliVolts<f32>, InvalidValue> {
|
||||
if temperature.value() >= -2.0 && temperature.value() <= 800.0 {
|
||||
Ok(MilliVolts(0.041 * temperature.value()))
|
||||
} else {
|
||||
Err(InvalidValue)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user