diff --git a/.github/actions/common/setup-rust-environment/action.yml b/.github/actions/common/setup-rust-environment/action.yml index 76c22f6..2574059 100644 --- a/.github/actions/common/setup-rust-environment/action.yml +++ b/.github/actions/common/setup-rust-environment/action.yml @@ -6,17 +6,22 @@ description: Install deps, ARM toolchain, and Rust toolchain runs: using: composite steps: - - name: Cache/Restore Build Files - id: cache-check + - name: Rust build cache (crates + target) + uses: Swatinem/rust-cache@v2 + with: + # Cache is scoped by OS, Rust version, target triple, and workspace hash + save-if: true + cache-all-crates: true + workspaces: | + . -> target + + - name: Cache Rustup Toolchains uses: actions/cache@v3 with: path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + ~/.rustup/toolchains/ + ~/.rustup/update-hashes/ + key: ${{ runner.os }}-rustup-stable-thumbv7em-none-eabihf - name: Disable man-db (skip Manual Page Installs) shell: bash @@ -29,7 +34,7 @@ runs: shell: bash run: | sudo apt update - sudo apt install -y cmake protobuf-compiler + sudo apt-get install -y --no-install-recommends cmake protobuf-compiler - name: Install Arm GNU Toolchain (arm-none-eabi-gcc) uses: carlosperate/arm-none-eabi-gcc-action@v1 @@ -41,9 +46,22 @@ runs: with: toolchain: stable target: thumbv7em-none-eabihf + profile: minimal + override: true - - name: Install Cargo Dependencies - uses: actions-rs/cargo@v1 - with: - command: install - args: --force cargo-make cargo-sort + - name: Install Cargo Binaries (idempotent) + shell: bash + run: | + set -euo pipefail + ensure_bin() { + local bin="$1"; shift + if ! command -v "$bin" >/dev/null 2>&1; then + echo "Installing $bin..." + cargo install "$@" + else + echo "$bin already installed; skipping" + fi + } + # Pin versions for reproducibility and better cache hits + ensure_bin cargo-make cargo-make@0.37.7 + ensure_bin cargo-sort cargo-sort@1.0.9 diff --git a/.github/workflows/argus-build.yml b/.github/workflows/argus-build.yml index 77f9e64..f789873 100644 --- a/.github/workflows/argus-build.yml +++ b/.github/workflows/argus-build.yml @@ -1,18 +1,18 @@ # SHOULD DO: dedupe these and clean them up on: push: - branches: [ main ] + branches: [main] pull_request: paths: - - '.github/workflows/argus/**' - - 'boards/argus/**' - - 'common/**' # because argus has some dependencies on common + - ".github/workflows/argus/**" + - "boards/argus/**" + - "common/**" # because argus has some dependencies on common workflow_dispatch: name: Build Verification jobs: - formatting: - name: Check for Formatting Issues + lint: + name: Check for Formatting/Linting/Semantic Issues runs-on: ubuntu-latest steps: - name: Checkout @@ -24,13 +24,20 @@ jobs: - name: Install Formatting Dependencies run: | cargo install cargo-sort - rustup component add rustfmt + rustup component add rustfmt clippy + working-directory: boards/argus - name: Check Formatting run: cargo fmt -- --check + working-directory: boards/argus - name: Check Cargo.toml Sorting run: cargo sort --check + working-directory: boards/argus + + - name: Check for Clippy Warnings + run: cargo clippy -p argus --all-features --no-deps + working-directory: boards/argus build_pressure: name: Build with Pressure Feature @@ -90,4 +97,4 @@ jobs: name: "Run Host Tests" with: command: make - args: test-host \ No newline at end of file + args: test-host diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index a9ccbdd..e49f667 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -34,12 +34,15 @@ jobs: - name: Build project with Pressure Feature run: cargo build --release --features pressure + working-directory: boards/argus - name: Build project with Temperature Feature run: cargo build --release --features temperature + working-directory: boards/argus - name: Build project with Strain Feature run: cargo build --release --features strain + working-directory: boards/argus - name: Run semantic-release id: semantic-release diff --git a/Cargo.lock b/Cargo.lock index 85d156b..0da6bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,8 +64,10 @@ dependencies = [ "cortex-m", "cortex-m-rt", "critical-section", + "csv", "defmt 0.3.100", "defmt-rtt", + "derive_more 2.0.1", "embassy-embedded-hal 0.3.2", "embassy-executor", "embassy-futures", @@ -90,15 +92,15 @@ dependencies = [ "libm", "messages-prost", "micromath", + "num-traits", "panic-probe", "pid", "serde", - "serde-csv-core", "smlang", "static_cell", "stm32-fmc", "straingauge-converter", - "thermocouple-converter", + "strum", ] [[package]] @@ -233,7 +235,7 @@ dependencies = [ "cortex-m", "defmt 0.3.100", "defmt-rtt", - "derive_more", + "derive_more 0.99.20", "embassy-embedded-hal 0.3.2", "embassy-time 0.4.0", "embedded-hal 1.0.0", @@ -258,6 +260,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cortex-m" version = "0.7.7" @@ -303,6 +314,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "csv" +version = "0.1.0" +dependencies = [ + "heapless 0.9.1", + "serde", + "serde-csv-core", +] + [[package]] name = "csv-core" version = "0.1.12" @@ -404,13 +424,35 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.1", "syn 2.0.106", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + [[package]] name = "document-features" version = "0.2.11" @@ -1394,6 +1436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1896,6 +1939,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "svgbobdoc" version = "0.3.0" @@ -1953,10 +2017,6 @@ dependencies = [ "libc", ] -[[package]] -name = "thermocouple-converter" -version = "0.1.0" - [[package]] name = "thiserror" version = "1.0.69" @@ -2026,12 +2086,24 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "usb-device" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 38266c1..7bbaddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,5 +48,9 @@ version = "1.0.150" default-features = false features = ["derive"] +[workspace.dependencies.serde-csv-core] +version = "0.3.2" +features = ["defmt"] + [workspace.dependencies.smlang] version = "0.8.0" diff --git a/Makefile.toml b/Makefile.toml index ea34191..278433e 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -9,8 +9,7 @@ default_to_workspace = false # ----------------------- [tasks.test-host] -dependencies = ["test-thermocouple-converter"] - +dependencies = [] # ----------------------- # Embedded Testing # ----------------------- @@ -33,14 +32,3 @@ args = ["test", "-p", "argus", "--features", "pressure", "${@}"] [tasks.test-argus-strain] command = "cargo" args = ["test", "-p", "argus", "--features", "strain", "${@}"] - -[tasks.test-thermocouple-converter] -command = "cargo" -args = [ - "test", - "-p", - "thermocouple-converter", - "--target", - "${CARGO_MAKE_RUST_TARGET_TRIPLE}", -] -env = { RUST_MIN_STACK = "8388608" } diff --git a/boards/argus/Cargo.toml b/boards/argus/Cargo.toml index 50187c9..933699b 100644 --- a/boards/argus/Cargo.toml +++ b/boards/argus/Cargo.toml @@ -16,8 +16,12 @@ chrono = { workspace = true } cortex-m = { workspace = true } cortex-m-rt = { workspace = true } critical-section = "1.1" +csv = { path = "../../common/csv" } defmt = { workspace = true } defmt-rtt = { workspace = true } +derive_more = { version = "2.0.1", default-features = false, features = [ + "full", +] } embassy-embedded-hal = { version = "0.3.0" } embassy-executor = { version = "0.7.0", features = [ "nightly", @@ -70,15 +74,17 @@ itoa = { version = "1.0.15", features = ["no-panic"] } libm = { version = "0.2.15", default-features = false } messages-prost = { workspace = true } micromath = "2.0.0" +num-traits = { version = "0.2.19", default-features = false, features = [ + "libm", +] } panic-probe = { workspace = true } pid = "4.0.0" serde = { workspace = true, features = ["derive", "serde_derive"] } -serde-csv-core = { version = "0.3.2", features = ["defmt"] } smlang = { workspace = true } static_cell = "2" stm32-fmc = "0.3.0" straingauge-converter = { path = "../../common/straingauge-converter" } -thermocouple-converter = { path = "../../common/thermocouple-converter" } +strum = { version = "0.27.2", default-features = false, features = ["derive"] } [[test]] name = "sd" diff --git a/boards/argus/src/adc/config.rs b/boards/argus/src/adc/config.rs deleted file mode 100644 index bfeb16e..0000000 --- a/boards/argus/src/adc/config.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Number of ADC devices in the system -pub const ADC_COUNT: usize = 2; diff --git a/boards/argus/src/adc/driver/mod.rs b/boards/argus/src/adc/driver/mod.rs index 39d485c..ae3bf5a 100644 --- a/boards/argus/src/adc/driver/mod.rs +++ b/boards/argus/src/adc/driver/mod.rs @@ -6,7 +6,7 @@ pub mod types; use embassy_time::Timer; use embedded_hal::digital::{InputPin, OutputPin}; use embedded_hal_async::spi::SpiDevice; -use types::{AnalogChannel, Command, DataRate, Filter, Gain, ReferenceRange, Register}; +use types::{AnalogChannel, Command, DataRate, Filter, Gain, ReferenceRange, Register, Voltage}; use crate::adc::driver::config::MAX_SIGNED_CODE_SIZE; @@ -63,7 +63,7 @@ where pub async fn read_single_ended( &mut self, channel: AnalogChannel, - ) -> Result { + ) -> Result { self.set_channels(channel, AnalogChannel::AINCOM).await?; self.wait_for_next_data().await; let code = self.read_data_code().await?; @@ -74,7 +74,7 @@ where &mut self, positive: AnalogChannel, negative: AnalogChannel, - ) -> Result { + ) -> Result { self.set_channels(positive, negative).await?; self.wait_for_next_data().await; let code = self.read_data_code().await?; @@ -90,7 +90,7 @@ where Ok(()) } - async fn send_command( + pub async fn send_command( &mut self, command: Command, ) -> Result<(), E> { @@ -98,7 +98,7 @@ where Ok(()) } - async fn set_channels( + pub async fn set_channels( &mut self, positive: AnalogChannel, negative: AnalogChannel, @@ -120,7 +120,7 @@ where Ok(()) } - async fn read_data_code(&mut self) -> Result { + pub async fn read_data_code(&mut self) -> Result { // Send the RDATA1 command followed by 4 dummy bytes to read the 32-bit result 4 * 8 = 32 bits let tx = [Command::RDATA1 as u8, 0, 0, 0, 0]; @@ -142,18 +142,18 @@ where code: i32, ) -> f32 { // Convert a 32‑bit two’s‑complement code to volts, using current VREF and PGA gain. - let full_scale_range: f32 = self.reference_range.to_volts() / (self.gain as u8 as f32); + let full_scale_range: f32 = self.reference_range.to_volts() / self.gain.to_multiplier(); (code as f64 / MAX_SIGNED_CODE_SIZE) as f32 * full_scale_range } - async fn write_register( + pub async fn write_register( &mut self, register: Register, value: u8, ) -> Result<(), E> { // Mask to 5 bits just in case, to remove the leading bits let mut address = register as u8; - address = address & 0x1F; + address &= 0x1F; // Add the write register opcode prefix 010rrrrr (40h+000rrrrr) let op1 = 0x40 | address; @@ -166,13 +166,13 @@ where Ok(()) } - async fn read_register( + pub async fn read_register( &mut self, register: Register, ) -> Result { let mut address = register as u8; // Mask to 5 bits just in case, to remove the leading bits - address = address & 0x1F; + address &= 0x1F; // Add the read register opcode prefix 001rrrrr (20h+000rrrrr) let op1 = 0x20 | address; @@ -189,7 +189,7 @@ where Ok(rx[2]) } - async fn wait_for_next_data(&mut self) { + pub async fn wait_for_next_data(&mut self) { loop { if self.data_ready.is_low().unwrap_or(false) { break; @@ -224,7 +224,7 @@ where Ok(()) } - async fn apply_reference_range_configuration(&mut self) -> Result<(), E> { + pub async fn apply_reference_range_configuration(&mut self) -> Result<(), E> { let mut register_value: u8 = 0x00; match self.reference_range { @@ -242,7 +242,7 @@ where Ok(()) } - async fn apply_internal_reference_configuration(&mut self) -> Result<(), E> { + pub async fn apply_internal_reference_configuration(&mut self) -> Result<(), E> { let mut register_value: u8 = 0x00; if self.enable_internal_reference { @@ -253,13 +253,13 @@ where Ok(()) } - async fn apply_filter_configuration(&mut self) -> Result<(), E> { + pub async fn apply_filter_configuration(&mut self) -> Result<(), E> { let mut register_value: u8 = 0x0; register_value |= (self.filter as u8) << 5; self.write_register(Register::MODE1, register_value).await } - async fn apply_gain_and_data_rate_configuration(&mut self) -> Result<(), E> { + pub async fn apply_gain_and_data_rate_configuration(&mut self) -> Result<(), E> { let mut register_value: u8 = 0x0; register_value |= (self.gain as u8) << 4; register_value |= self.data_rate as u8; @@ -268,7 +268,7 @@ where Ok(()) } - async fn apply_offset_calibration_configuration(&mut self) -> Result<(), E> { + pub async fn apply_offset_calibration_configuration(&mut self) -> Result<(), E> { // SHOULD DO: implement self.write_register(Register::OFCAL0, 0x00).await?; self.write_register(Register::OFCAL1, 0x00).await?; @@ -283,7 +283,7 @@ where Ok((device_id, revision_id)) } - async fn apply_full_scale_calibration_configuration(&mut self) -> Result<(), E> { + pub async fn apply_full_scale_calibration_configuration(&mut self) -> Result<(), E> { // SHOULD DO: implement Ok(()) } diff --git a/boards/argus/src/adc/driver/types/gain.rs b/boards/argus/src/adc/driver/types/gain.rs index 37f3af3..04880d8 100644 --- a/boards/argus/src/adc/driver/types/gain.rs +++ b/boards/argus/src/adc/driver/types/gain.rs @@ -1,6 +1,6 @@ /// Preset Gain values from ADS126x datasheet #[repr(u8)] -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Gain { G1 = 0b000, G2 = 0b001, @@ -9,3 +9,16 @@ pub enum Gain { G16 = 0b100, G32 = 0b101, } + +impl Gain { + pub fn to_multiplier(&self) -> f32 { + match self { + Gain::G1 => 1.0, + Gain::G2 => 2.0, + Gain::G4 => 4.0, + Gain::G8 => 8.0, + Gain::G16 => 16.0, + Gain::G32 => 32.0, + } + } +} diff --git a/boards/argus/src/adc/driver/types/mod.rs b/boards/argus/src/adc/driver/types/mod.rs index bbbcd2d..f4d0243 100644 --- a/boards/argus/src/adc/driver/types/mod.rs +++ b/boards/argus/src/adc/driver/types/mod.rs @@ -13,3 +13,5 @@ pub use filter::*; pub use gain::*; pub use reference_range::*; pub use register::*; + +pub type Voltage = f32; diff --git a/boards/argus/src/adc/mod.rs b/boards/argus/src/adc/mod.rs index 6aa33c1..af8121d 100644 --- a/boards/argus/src/adc/mod.rs +++ b/boards/argus/src/adc/mod.rs @@ -1,4 +1,3 @@ -pub mod config; pub mod driver; pub mod service; pub mod types; diff --git a/boards/argus/src/adc/service.rs b/boards/argus/src/adc/service.rs index 1747b79..1200b9e 100644 --- a/boards/argus/src/adc/service.rs +++ b/boards/argus/src/adc/service.rs @@ -7,7 +7,6 @@ use embassy_stm32::{gpio, mode, spi, time::mhz}; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; use static_cell::StaticCell; -use crate::adc::config::ADC_COUNT; use crate::adc::driver::Ads1262; // HACK: Use a static cell to hold the SPI bus shared between multiple ADC instances since we can't have self-referencing structs @@ -15,11 +14,11 @@ use crate::adc::driver::Ads1262; static ADC_SPI_BUS: StaticCell>> = StaticCell::new(); /// Acts as an orchestration layer for multiple ADC drivers. -pub struct AdcService { +pub struct AdcService { pub drivers: [AdcDriver; ADC_COUNT], } -impl AdcService { +impl AdcService { pub fn new( peri: impl Peripheral

+ 'static, sck: impl Peripheral

> + 'static, diff --git a/boards/argus/src/adc/types/device.rs b/boards/argus/src/adc/types/device.rs index f91eab9..c942383 100644 --- a/boards/argus/src/adc/types/device.rs +++ b/boards/argus/src/adc/types/device.rs @@ -1,8 +1,9 @@ use defmt::Format; use serde::{Deserialize, Serialize}; +use strum::EnumCount; // Called AdcDevice to not clash with embassy::adc::Adc -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Format, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Format, Serialize, Deserialize, EnumCount)] pub enum AdcDevice { Adc1 = 0, Adc2 = 1, diff --git a/boards/argus/src/lib.rs b/boards/argus/src/lib.rs index 6421d37..7f4dd90 100644 --- a/boards/argus/src/lib.rs +++ b/boards/argus/src/lib.rs @@ -3,6 +3,7 @@ #![no_main] pub mod adc; +pub mod linear_transformation; pub mod sd; pub mod serial; pub mod state_machine; @@ -10,3 +11,6 @@ pub mod utils; #[cfg(feature = "temperature")] pub mod temperature; + +#[cfg(feature = "pressure")] +pub mod pressure; diff --git a/boards/argus/src/linear_transformation/mod.rs b/boards/argus/src/linear_transformation/mod.rs new file mode 100644 index 0000000..8cf2a98 --- /dev/null +++ b/boards/argus/src/linear_transformation/mod.rs @@ -0,0 +1,2 @@ +pub mod service; +pub mod types; diff --git a/boards/argus/src/linear_transformation/service.rs b/boards/argus/src/linear_transformation/service.rs new file mode 100644 index 0000000..2719723 --- /dev/null +++ b/boards/argus/src/linear_transformation/service.rs @@ -0,0 +1,115 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::str::FromStr; + +use csv::SerializeCSV; +use defmt::{info, Format}; +use heapless::LinearMap; +use num_traits::Float; +use serde::{Deserialize, Serialize}; +use strum::EnumCount; + +use crate::adc::types::AdcDevice; +use crate::linear_transformation::types::LinearTransformation; +use crate::sd::service::SDCardService; +use crate::sd::types::{FileName, OperationScope, SdCardError}; +use crate::utils::types::AsyncMutex; + +pub struct LinearTransformationService +where + Channel: EnumCount + Default + Debug + Clone + Copy + Eq + PartialEq + Hash + Format + Serialize + for<'de> Deserialize<'de>, + ChannelValue: Float + Serialize + for<'de> Deserialize<'de>, { + pub sd_card_service: &'static AsyncMutex, + pub file_name: &'static str, + + // Linear transformations that are applied on top of the raw readings for each ADC and channel + pub transformations: LinearMap, CHANNEL_COUNT>, ADC_COUNT>, +} + +impl + LinearTransformationService +where + Channel: EnumCount + Default + Debug + Clone + Copy + Eq + PartialEq + Hash + Format + Serialize + for<'de> Deserialize<'de>, + ChannelValue: Float + Serialize + for<'de> Deserialize<'de>, +{ + pub fn new( + sd_card_service: &'static AsyncMutex, + file_name: &'static str, + ) -> Self { + Self { + sd_card_service, + file_name, + transformations: LinearMap::default(), + } + } + + pub async fn load_transformations(&mut self) -> Result<(), SdCardError> { + let result = self + .sd_card_service + .lock() + .await + .read(OperationScope::Root, FileName::from_str(self.file_name).unwrap(), |line| { + if *line == LinearTransformation::::get_csv_header() { + return true; // Skip header line + } + let transformation = LinearTransformation::::from_csv_line(line); + self.register_transformation(transformation); + true // Continue reading + }); + + match result { + Ok(_) => (), + Err(SdCardError::NotFound) => { + // If transformations not found, keep using the defaults and ignore this error. + info!("Linear transformations file not found, using defaults. Gain = 1, Offset = 0"); + } + Err(e) => return Err(e), + } + Ok(()) + } + + pub fn register_transformation( + &mut self, + transformation: LinearTransformation, + ) { + if !self.transformations.contains_key(&transformation.adc) { + let _ = self.transformations.insert(transformation.adc, LinearMap::new()); + } + let map = self.transformations.get_mut(&transformation.adc).unwrap(); + let _ = map.insert(transformation.channel, transformation); + } + + pub fn ensure_transformation_applied( + &self, + adc: AdcDevice, + channel: Channel, + raw_value: ChannelValue, + ) -> ChannelValue { + if let Some(channel_map) = self.transformations.get(&adc) { + if let Some(transformation) = channel_map.get(&channel) { + return transformation.apply(raw_value); + } + } + raw_value // If no transformation found, return the raw value + } + + pub async fn save_transformation( + &mut self, + transformation: LinearTransformation, + ) -> Result<(), SdCardError> { + let mut sd_card_service = self.sd_card_service.lock().await; + let path = FileName::from_str(self.file_name).unwrap(); + if !(sd_card_service.file_exists(OperationScope::Root, path.clone())?) { + sd_card_service.write( + OperationScope::Root, + path.clone(), + LinearTransformation::::get_csv_header(), + )?; + } + + sd_card_service.write(OperationScope::Root, path.clone(), transformation.to_csv_line())?; + self.register_transformation(transformation); + + Ok(()) + } +} diff --git a/boards/argus/src/linear_transformation/types.rs b/boards/argus/src/linear_transformation/types.rs new file mode 100644 index 0000000..2eec176 --- /dev/null +++ b/boards/argus/src/linear_transformation/types.rs @@ -0,0 +1,61 @@ +use core::fmt::Debug; +use core::hash::Hash; +use core::str::FromStr; + +use csv::SerializeCSV; +use defmt::Format; +use num_traits::Float; +use serde::{Deserialize, Serialize}; +use strum::EnumCount; + +use crate::adc::types::AdcDevice; +use crate::sd::config::MAX_LINE_LENGTH; +use crate::sd::types::Line; + +// Represents a linear transformation applied to a sensor reading +// corrected_value = value_with_error * gain + offset +#[derive(Debug, Clone, Copy, Format, Serialize, Deserialize)] +pub struct LinearTransformation { + pub adc: AdcDevice, + pub channel: Channel, + pub gain: ChannelValue, + pub offset: ChannelValue, +} + +impl LinearTransformation +where + Channel: EnumCount + Default + Debug + Clone + Copy + Eq + PartialEq + Hash + Format + Serialize + for<'de> Deserialize<'de>, + ChannelValue: Float + Serialize + for<'de> Deserialize<'de>, +{ + pub fn apply( + &self, + raw_value: ChannelValue, + ) -> ChannelValue { + raw_value * self.gain + self.offset + } +} + +impl Default for LinearTransformation +where + Channel: EnumCount + Default + Debug + Clone + Copy + Eq + PartialEq + Hash + Format + Serialize + for<'de> Deserialize<'de>, + ChannelValue: Float + Serialize + for<'de> Deserialize<'de>, +{ + fn default() -> Self { + Self { + adc: AdcDevice::Adc1, // Default to ADC1 + channel: Channel::default(), + gain: ChannelValue::one(), // Default to unity gain + offset: ChannelValue::zero(), // Default to zero offset + } + } +} + +impl SerializeCSV for LinearTransformation +where + Channel: EnumCount + Default + Debug + Clone + Copy + Eq + PartialEq + Hash + Format + Serialize + for<'de> Deserialize<'de>, + ChannelValue: Float + Serialize + for<'de> Deserialize<'de>, +{ + fn get_csv_header() -> Line { + Line::from_str("ADC Index,Channel Index,Gain,Offset\n").unwrap() + } +} diff --git a/boards/argus/src/main.rs b/boards/argus/src/main.rs index 3a27ddf..93020ba 100644 --- a/boards/argus/src/main.rs +++ b/boards/argus/src/main.rs @@ -8,6 +8,7 @@ // ); use argus::adc::service::{AdcConfig, AdcService}; +use argus::adc::types::AdcDevice; use argus::sd::service::SDCardService; use argus::sd::task::sd_card_task; use argus::serial::service::SerialService; @@ -20,9 +21,9 @@ use defmt_rtt as _; use embassy_executor::Spawner; use embassy_stm32::gpio::Pin; use embassy_stm32::{bind_interrupts, peripherals, usart}; -use embassy_time::Timer; use panic_probe as _; use static_cell::StaticCell; +use strum::EnumCount; // Mapping of NVIC interrupts to Embassy interrupt handlers bind_interrupts!(struct InterruptRequests { @@ -32,13 +33,16 @@ bind_interrupts!(struct InterruptRequests { // All services are singletons held in a static cell to initialize after peripherals are available // And wrapped around a mutex so they can be accessed safely from multiple async tasks static SD_CARD_SERVICE: StaticCell> = StaticCell::new(); -static ADC_SERVICE: StaticCell> = StaticCell::new(); +static ADC_SERVICE: StaticCell>> = StaticCell::new(); static SERIAL_SERVICE: StaticCell> = StaticCell::new(); static STATE_MACHINE_ORCHESTRATOR: StaticCell> = StaticCell::new(); #[cfg(feature = "temperature")] -static TEMPERATURE_SERVICE: StaticCell> = StaticCell::new(); +static TEMPERATURE_SERVICE: StaticCell>> = StaticCell::new(); + +#[cfg(feature = "pressure")] +static PRESSURE_SERVICE: StaticCell>> = StaticCell::new(); #[embassy_executor::main] async fn main(spawner: Spawner) { @@ -99,17 +103,46 @@ async fn main(spawner: Spawner) { use argus::temperature::tasks; let temperature_service = TEMPERATURE_SERVICE.init(AsyncMutex::new(TemperatureService::new(adc_service, sd_card_service, serial_service))); - spawner.must_spawn(tasks::measure(StateMachineWorker::new(state_machine_orchestrator), temperature_service)); + + // Setup the temperature service before starting the tasks + temperature_service.lock().await.setup().await.unwrap(); + + spawner.must_spawn(tasks::measure_rtds( + StateMachineWorker::new(state_machine_orchestrator), + temperature_service, + )); + spawner.must_spawn(tasks::measure_thermocouples( + StateMachineWorker::new(state_machine_orchestrator), + temperature_service, + )); spawner.must_spawn(tasks::log_measurements( StateMachineWorker::new(state_machine_orchestrator), sd_card_service, )); } - // Immediately request to start recording - state_machine_orchestrator.lock().await.dispatch_event(Events::StartRecordingRequested); + // Spawn tasks needed for pressure board + #[cfg(feature = "pressure")] + { + // Imported inside the block to avoid unused leaking the import when the feature is not enabled + use argus::pressure::service::PressureService; + use argus::pressure::tasks; - Timer::after_secs(30).await; + let pressure_service = PRESSURE_SERVICE.init(AsyncMutex::new(PressureService::new(adc_service, sd_card_service, serial_service))); - state_machine_orchestrator.lock().await.dispatch_event(Events::StopRecordingRequested); + // Setup the pressure service before starting the tasks + pressure_service.lock().await.setup().await.unwrap(); + + spawner.must_spawn(tasks::measure_pressure( + StateMachineWorker::new(state_machine_orchestrator), + pressure_service, + )); + spawner.must_spawn(tasks::log_measurements( + StateMachineWorker::new(state_machine_orchestrator), + sd_card_service, + )); + } + + // Immediately request to start recording + state_machine_orchestrator.lock().await.dispatch_event(Events::StartRecordingRequested); } diff --git a/boards/argus/src/pressure/config.rs b/boards/argus/src/pressure/config.rs new file mode 100644 index 0000000..aebb574 --- /dev/null +++ b/boards/argus/src/pressure/config.rs @@ -0,0 +1,9 @@ +// Size of the queue used to send pressure readings from the pressure service to the SD card service +pub const PRESSURE_READING_QUEUE_SIZE: usize = 16; + +// Maximum number of calibration data points allowed to be collected during a calibration session per thermocouple channel +pub const MAX_CALIBRATION_DATA_POINTS: usize = 10; + +// File name used to read/write linear transformations that applied to thermocouple readings to/from the SD card +// Linear transformations are stored in CSV format +pub const LINEAR_TRANSFORMATIONS_FILE_NAME: &str = "t_pres.csv"; // Cannot be longer than 12 characters diff --git a/boards/argus/src/pressure/mod.rs b/boards/argus/src/pressure/mod.rs new file mode 100644 index 0000000..80faf82 --- /dev/null +++ b/boards/argus/src/pressure/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod service; +pub mod tasks; +pub mod types; diff --git a/boards/argus/src/pressure/service.rs b/boards/argus/src/pressure/service.rs new file mode 100644 index 0000000..18f0714 --- /dev/null +++ b/boards/argus/src/pressure/service.rs @@ -0,0 +1,80 @@ +use embassy_time::Instant; +use strum::EnumCount; + +use crate::adc::driver::types::{DataRate, Filter, Gain, ReferenceRange}; +use crate::adc::service::AdcService; +use crate::adc::types::AdcDevice; +use crate::linear_transformation::service::LinearTransformationService; +use crate::pressure::config::LINEAR_TRANSFORMATIONS_FILE_NAME; +use crate::pressure::types::{PressureChannel, PressureReading, PressureReadingQueue, PressureServiceError}; +use crate::sd::service::SDCardService; +use crate::serial::service::SerialService; +use crate::utils::types::AsyncMutex; + +// A channel for buffering the pressure readings and decoupling the logging to sd task from the measurement task +pub static PRESSURE_READING_QUEUE: PressureReadingQueue = PressureReadingQueue::new(); + +pub struct PressureService { + // Other services are passed by a mutex to ensure safe concurrent access + pub adc_service: &'static AsyncMutex>, + pub sd_card_service: &'static AsyncMutex, + pub serial_service: &'static AsyncMutex, + + // Linear transformation service to apply error corrections obtained from calibration to raw readings + pub linear_transformation_service: LinearTransformationService, +} + +impl PressureService { + pub fn new( + adc_service: &'static AsyncMutex>, + sd_card_service: &'static AsyncMutex, + serial_service: &'static AsyncMutex, + ) -> Self { + Self { + adc_service, + sd_card_service, + serial_service, + linear_transformation_service: LinearTransformationService::new(sd_card_service, LINEAR_TRANSFORMATIONS_FILE_NAME), + } + } + + pub async fn setup(&mut self) -> Result<(), PressureServiceError> { + for driver in self.adc_service.lock().await.drivers.iter_mut() { + driver.reference_range = ReferenceRange::Avdd; + driver.data_rate = DataRate::Sps100; + driver.filter = Filter::Sinc3; + driver.enable_internal_reference = true; + driver.gain = Gain::G32; + driver.delay_after_setting_channel = 50; // 50 ms delay to allow the ADC to stabilize after switching channels + driver.apply_configurations().await?; + } + + self.linear_transformation_service.load_transformations().await?; + Ok(()) + } + + pub async fn read_pressure_sensor( + &mut self, + adc: AdcDevice, + channel: PressureChannel, + ) -> Result { + let mut adc_service = self.adc_service.lock().await; + + // Get the respective "adc channel" pair for the "thermocouple channel" + let (positive_channel, negative_channel) = channel.to_analog_input_channel_pair(); + + // Read the voltage from the ADC in millivolts + let voltage = adc_service.drivers[adc as usize] + .read_differential(positive_channel, negative_channel) + .await? * 1000.0; // Convert to millivolts + + let thermocouple_reading = PressureReading { + timestamp: Instant::now().as_millis(), + voltage, + pressure: 0.0, // Placeholder, actual pressure calculation can be added later + temperature: 0.0, // Placeholder, actual temperature calculation can be added + }; + + Ok(thermocouple_reading) + } +} diff --git a/boards/argus/src/pressure/tasks/log_measurements.rs b/boards/argus/src/pressure/tasks/log_measurements.rs new file mode 100644 index 0000000..61454c4 --- /dev/null +++ b/boards/argus/src/pressure/tasks/log_measurements.rs @@ -0,0 +1,57 @@ +use csv::SerializeCSV; +use embassy_executor::task; +use heapless::format; +use strum::EnumCount; + +use crate::adc::types::AdcDevice; +use crate::pressure::service::PRESSURE_READING_QUEUE; +use crate::pressure::types::pressure_reading::PressureReading; +use crate::pressure::types::PressureChannel; +use crate::sd::service::SDCardService; +use crate::sd::types::{FileName, OperationScope}; +use crate::state_machine::service::StateMachineWorker; +use crate::state_machine::types::States; +use crate::utils::types::AsyncMutex; + +// Task for picking up the readings from the channel and logging them to the SD card +#[task] +pub async fn log_measurements( + mut worker: StateMachineWorker, + sd_card_service_mutex: &'static AsyncMutex, +) { + initialize_csv_files(sd_card_service_mutex).await; + + worker + .run_while(States::Recording, async |_| -> Result<(), ()> { + let (adc, channel, pressure_reading) = PRESSURE_READING_QUEUE.receive().await; + let path = get_path_from_adc_and_channel(adc as usize, channel as usize); + let line = pressure_reading.to_csv_line(); + SDCardService::enqueue_write(OperationScope::CurrentSession, path, line).await; + Ok(()) + }) + .await + .unwrap(); +} + +// Create the files and write the CSV headers before starting the logging loop +async fn initialize_csv_files(sd_card_service_mutex: &'static AsyncMutex) { + let mut sd_card_service = sd_card_service_mutex.lock().await; + + // Ignore because if the SD card isn't mounted we don't want to panic + let _ = sd_card_service.ensure_session_created(); + for adc_index in 0..AdcDevice::COUNT { + for channel in 0..PressureChannel::COUNT { + let path = get_path_from_adc_and_channel(adc_index, channel); + + // Ignore because if the SD card isn't mounted we don't want to panic + let _ = sd_card_service.write(OperationScope::CurrentSession, path, PressureReading::get_csv_header()); + } + } +} + +fn get_path_from_adc_and_channel( + adc_index: usize, + channel: usize, +) -> FileName { + format!("P_{}_{}.csv", adc_index, channel).unwrap() as FileName +} diff --git a/boards/argus/src/pressure/tasks/measure_pressure.rs b/boards/argus/src/pressure/tasks/measure_pressure.rs new file mode 100644 index 0000000..ad2d2d7 --- /dev/null +++ b/boards/argus/src/pressure/tasks/measure_pressure.rs @@ -0,0 +1,43 @@ +use defmt::{debug, error}; +use embassy_executor::task; +use strum::EnumCount; + +use crate::adc::types::AdcDevice; +use crate::pressure::service::{PressureService, PRESSURE_READING_QUEUE}; +use crate::pressure::types::PressureChannel; +use crate::state_machine::service::StateMachineWorker; +use crate::state_machine::types::States; +use crate::utils::types::AsyncMutex; + +// Task that iterates through the ADCs and channels, measures the pressure, and enqueues the readings to a channel +#[task] +pub async fn measure_pressure( + mut worker: StateMachineWorker, + pressure_service_mutex: &'static AsyncMutex>, +) { + worker + .run_while(States::Recording, async |_| -> Result<(), ()> { + let mut pressure_service = pressure_service_mutex.lock().await; + + for adc_index in 0..AdcDevice::COUNT { + for channel_index in 0..PressureChannel::COUNT { + let adc = AdcDevice::from(adc_index); + let channel = PressureChannel::from(channel_index); + let data = pressure_service.read_pressure_sensor(adc, channel).await; + match data { + Ok(data) => { + debug!("ADC {} Channel {}: {}", adc, channel, data); + PRESSURE_READING_QUEUE.send((adc, channel, data)).await; + } + Err(err) => { + error!("Error reading ADC {} Channel {}: {:?}", adc, channel, err); + continue; + } + } + } + } + Ok(()) + }) + .await + .unwrap(); +} diff --git a/boards/argus/src/pressure/tasks/mod.rs b/boards/argus/src/pressure/tasks/mod.rs new file mode 100644 index 0000000..9103d4f --- /dev/null +++ b/boards/argus/src/pressure/tasks/mod.rs @@ -0,0 +1,5 @@ +mod log_measurements; +mod measure_pressure; + +pub use log_measurements::*; +pub use measure_pressure::*; diff --git a/boards/argus/src/pressure/types/error.rs b/boards/argus/src/pressure/types/error.rs new file mode 100644 index 0000000..4256147 --- /dev/null +++ b/boards/argus/src/pressure/types/error.rs @@ -0,0 +1,13 @@ +use defmt::Format; +use derive_more::From; + +use crate::adc::service::AdcError; +use crate::sd::types::SdCardError; +use crate::serial::service::UsartError; + +#[derive(Debug, Format, From)] +pub enum PressureServiceError { + AdcError(AdcError), + UsartError(UsartError), + SdCardError(SdCardError), +} diff --git a/boards/argus/src/pressure/types/mod.rs b/boards/argus/src/pressure/types/mod.rs new file mode 100644 index 0000000..3a9a7a0 --- /dev/null +++ b/boards/argus/src/pressure/types/mod.rs @@ -0,0 +1,9 @@ +pub mod error; +pub mod pressure_channel; +pub mod pressure_reading; +pub mod queue; + +pub use error::*; +pub use pressure_channel::*; +pub use pressure_reading::*; +pub use queue::*; diff --git a/boards/argus/src/pressure/types/pressure_channel.rs b/boards/argus/src/pressure/types/pressure_channel.rs new file mode 100644 index 0000000..ca8c2ee --- /dev/null +++ b/boards/argus/src/pressure/types/pressure_channel.rs @@ -0,0 +1,39 @@ +use defmt::Format; +use serde::{Deserialize, Serialize}; +use strum::EnumCount; + +use crate::adc::driver::types::AnalogChannel; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Format, Serialize, Deserialize, EnumCount, Default)] +pub enum PressureChannel { + #[default] + Channel1 = 0, + Channel2 = 1, + Channel3 = 2, + Channel4 = 3, +} + +// Support for implicit conversion from usize to PressureChannel +impl From for PressureChannel { + fn from(value: usize) -> Self { + match value { + 0 => PressureChannel::Channel1, + 1 => PressureChannel::Channel2, + 2 => PressureChannel::Channel3, + 3 => PressureChannel::Channel4, + _ => panic!("Invalid pressure channel index: {}", value), + } + } +} + +// Configure which analog input channel pair each pressure channel uses +impl PressureChannel { + pub fn to_analog_input_channel_pair(&self) -> (AnalogChannel, AnalogChannel) { + match self { + PressureChannel::Channel1 => (AnalogChannel::AIN0, AnalogChannel::AIN1), + PressureChannel::Channel2 => (AnalogChannel::AIN2, AnalogChannel::AIN3), + PressureChannel::Channel3 => (AnalogChannel::AIN4, AnalogChannel::AIN5), + PressureChannel::Channel4 => (AnalogChannel::AIN6, AnalogChannel::AIN7), + } + } +} diff --git a/boards/argus/src/pressure/types/pressure_reading.rs b/boards/argus/src/pressure/types/pressure_reading.rs new file mode 100644 index 0000000..42e31aa --- /dev/null +++ b/boards/argus/src/pressure/types/pressure_reading.rs @@ -0,0 +1,36 @@ +use core::str::FromStr; + +use csv::SerializeCSV; +use defmt::Format; +use serde::{Deserialize, Serialize}; + +use crate::sd::config::MAX_LINE_LENGTH; +use crate::sd::types::Line; + +// Represents a single pressure reading from a pressure channel +#[derive(Debug, Clone, Copy, Format, Serialize, Deserialize)] +pub struct PressureReading { + // Timestamp of the reading in milliseconds since epoch + pub timestamp: u64, + + // pressure voltage difference measured in millivolts + pub voltage: f32, + + // pressure calculated in psi + pub pressure: f32, + + // manifold temperature at which this pressure reading was taken + pub temperature: f32, +} + +impl SerializeCSV for PressureReading { + fn get_csv_header() -> Line { + Line::from_str( + "Timestamp (ms),\ + Voltage (mV),\ + Pressure (psi),\ + Manifold Temperature (C)\n", + ) + .unwrap() + } +} diff --git a/boards/argus/src/pressure/types/queue.rs b/boards/argus/src/pressure/types/queue.rs new file mode 100644 index 0000000..93e1e07 --- /dev/null +++ b/boards/argus/src/pressure/types/queue.rs @@ -0,0 +1,10 @@ +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::channel::Channel; + +use crate::adc::types::AdcDevice; +use crate::pressure::config::PRESSURE_READING_QUEUE_SIZE; +use crate::pressure::types::pressure_channel::PressureChannel; +use crate::pressure::types::pressure_reading::PressureReading; + +// Type alias for the pressure reading queue used to decouple reading from ADC and writing to logging pipes +pub type PressureReadingQueue = Channel; diff --git a/boards/argus/src/sd/config.rs b/boards/argus/src/sd/config.rs index 2c712cc..db34325 100644 --- a/boards/argus/src/sd/config.rs +++ b/boards/argus/src/sd/config.rs @@ -5,7 +5,7 @@ pub const MAX_DIRS: usize = 4; pub const MAX_FILES: usize = 4; // Max number of messages allowed in the sd operation queue channel before it locks up until the channel clears -pub const QUEUE_SIZE: usize = 8; +pub const SD_WRITING_QUEUE_SIZE: usize = 8; // Agreed upon value for maximum length of a line that can be written to the SD card pub const MAX_LINE_LENGTH: usize = 255; diff --git a/boards/argus/src/sd/csv/mod.rs b/boards/argus/src/sd/csv/mod.rs deleted file mode 100644 index cd40856..0000000 --- a/boards/argus/src/sd/csv/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod types; diff --git a/boards/argus/src/sd/mod.rs b/boards/argus/src/sd/mod.rs index a189761..5b51a7f 100644 --- a/boards/argus/src/sd/mod.rs +++ b/boards/argus/src/sd/mod.rs @@ -1,5 +1,4 @@ pub mod config; -pub mod csv; pub mod service; pub mod task; pub mod types; diff --git a/boards/argus/src/sd/service.rs b/boards/argus/src/sd/service.rs index 97b51ac..0c7f3fd 100644 --- a/boards/argus/src/sd/service.rs +++ b/boards/argus/src/sd/service.rs @@ -1,6 +1,6 @@ // SHOULD DO: use embedded_hal traits instead of embassy_stm32 types directly -use defmt::debug; +use defmt::trace; use embassy_stm32::spi::{MisoPin, MosiPin, SckPin}; use embassy_stm32::{gpio, spi, time, Peripheral}; use embassy_time::Delay; @@ -54,10 +54,10 @@ impl SDCardService { let sd_card = SDCardInstance::new(spi_device, Delay); let volume_manager: SDCardVolumeManager = SDCardVolumeManager::new_with_limits(sd_card, FakeTimeSource::new(), 0); - return SDCardService { + SDCardService { volume_manager, current_session: None, - }; + } } // Closure that handles accessing root directory @@ -65,10 +65,10 @@ impl SDCardService { &mut self, f: impl for<'b> FnOnce(SDCardDirectory<'b, MAX_DIRS, MAX_FILES>) -> Result, ) -> Result { - debug!("Opening root directory"); + trace!("Opening root directory"); let volume = self.volume_manager.open_volume(VolumeIdx(0))?; let root_dir = volume.open_root_dir()?; - return f(root_dir); + f(root_dir) } // Non-blocking write that queues the message to be written by the async task @@ -77,7 +77,7 @@ impl SDCardService { path: FileName, line: Line, ) { - debug!("Enqueuing write to SD card: {:?}, {:?}, {:?}", scope, path.as_str(), line.as_str()); + trace!("Enqueuing write to SD card: {:?}, {:?}, {:?}", scope, path.as_str(), line.as_str()); SD_CARD_WRITE_QUEUE.send((scope, path, line)).await; } @@ -86,7 +86,7 @@ impl SDCardService { scope: OperationScope, path: FileName, ) -> Result<(), SdCardError> { - debug!("Deleting from SD card: {:?}, {:?}", scope, path.as_str()); + trace!("Deleting from SD card: {:?}, {:?}", scope, path.as_str()); // Setup all variables needed from self since we cannot access self inside the self.with_root closure let session = match scope { @@ -117,7 +117,7 @@ impl SDCardService { path: FileName, mut line: Line, ) -> Result<(), SdCardError> { - debug!("Writing to SD card: {:?}, {:?}, {:?}", scope, path.as_str(), line.as_str()); + trace!("Writing to SD card: {:?}, {:?}, {:?}", scope, path.as_str(), line.as_str()); // Ensure line ends with newline if !line.as_str().ends_with("\n") { @@ -155,9 +155,9 @@ impl SDCardService { self.read(scope, path, |line| { if lines.len() < LINES_COUNT { lines.push(line.clone()).ok(); // Ignore capacity error - return true; + true } else { - return false; + false } })?; Ok(lines) @@ -195,7 +195,7 @@ impl SDCardService { ) -> Result<(), SdCardError> { // Setup all variables needed from self since we cannot access self inside the self.with_root closure - debug!("Reading from SD card: {:?}, {:?}", scope, path.as_str()); + trace!("Reading from SD card: {:?}, {:?}", scope, path.as_str()); let session = match scope { OperationScope::CurrentSession => Some(self.current_session.as_ref().unwrap().clone()), @@ -231,7 +231,7 @@ impl SDCardService { match read_byte { b'\n' => { // End of line (LF). Emit and clear. - if handle_line(&line) == false { + if !handle_line(&line) { return Ok(()); // Stop reading if handler returns false } line.clear(); @@ -243,7 +243,7 @@ impl SDCardService { // Push char if capacity allows; if full, emit as a line-chunk and continue if line.push(read_byte as char).is_err() { // Buffer full — emit current chunk as a line - if handle_line(&line) == false { + if !handle_line(&line) { return Ok(()); // Stop reading if handler returns false } line.clear(); @@ -260,7 +260,7 @@ impl SDCardService { } pub fn ensure_session_created(&mut self) -> Result<(), SdCardError> { - debug!("Ensuring session directory is created"); + trace!("Ensuring session directory is created"); if self.current_session.is_some() { // Session directory already created @@ -277,18 +277,18 @@ impl SDCardService { self.current_session.as_mut().unwrap().push_str(current_session_str).ok(); // Ignore capacity error self.with_root::<(), SdCardError>(|root_dir| { - debug!("Creating session directory: {}", current_session_str); - return root_dir.make_dir_in_dir(current_session_str); + trace!("Creating session directory: {}", current_session_str); + root_dir.make_dir_in_dir(current_session_str) }) } /// Infer the last session based on the largest directory in the SD Card fn get_last_session_number(&mut self) -> Result { - debug!("Getting last session number"); + trace!("Getting last session number"); // Sessions are directories generated on root directory: numbers starting from 0 autoincrementing let mut last_session: u8 = 0; - return self.with_root::(|root_dir| { + self.with_root::(|root_dir| { root_dir.iterate_dir(|entry| { if !entry.attributes.is_directory() { return; @@ -301,18 +301,18 @@ impl SDCardService { } })?; - debug!("Last session number: {}", last_session); - return Ok(last_session); - }); + trace!("Last session number: {}", last_session); + Ok(last_session) + }) } } /// Get the name of a file or directory from its basename i.e. remove the extension /// Example: foo.txt -> foo -fn get_name_from_basename<'b>(bytes: &'b [u8]) -> &'b str { +fn get_name_from_basename(bytes: &[u8]) -> &str { let mut end = bytes.len(); while end > 0 && bytes[end - 1] == b' ' { end -= 1; } - return core::str::from_utf8(&bytes[..end]).unwrap(); + core::str::from_utf8(&bytes[..end]).unwrap() } diff --git a/boards/argus/src/sd/types/queue.rs b/boards/argus/src/sd/types/queue.rs index 07cc6bb..68c610e 100644 --- a/boards/argus/src/sd/types/queue.rs +++ b/boards/argus/src/sd/types/queue.rs @@ -1,7 +1,7 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::Channel; -use crate::sd::config::QUEUE_SIZE; +use crate::sd::config::SD_WRITING_QUEUE_SIZE; use crate::sd::types::{files::OperationScope, FileName, Line}; -pub type SdCardWriteQueue = Channel; +pub type SdCardWriteQueue = Channel; diff --git a/boards/argus/src/sd/types/time_source.rs b/boards/argus/src/sd/types/time_source.rs index ab4338c..e608ed2 100644 --- a/boards/argus/src/sd/types/time_source.rs +++ b/boards/argus/src/sd/types/time_source.rs @@ -7,6 +7,12 @@ pub struct FakeTimeSource { _marker: PhantomData<*const ()>, } +impl Default for FakeTimeSource { + fn default() -> Self { + Self::new() + } +} + impl FakeTimeSource { pub fn new() -> Self { FakeTimeSource { _marker: PhantomData } diff --git a/boards/argus/src/state_machine/service.rs b/boards/argus/src/state_machine/service.rs index 45cdaa7..be7cea6 100644 --- a/boards/argus/src/state_machine/service.rs +++ b/boards/argus/src/state_machine/service.rs @@ -16,6 +16,12 @@ static CURRENT_STATE: StateWatch = StateWatch::new(); pub struct StateMachineOrchestrator { state_machine: StateMachine, } +impl Default for StateMachineOrchestrator { + fn default() -> Self { + Self::new() + } +} + impl StateMachineOrchestrator { pub fn new() -> Self { Self { diff --git a/boards/argus/src/temperature/calibration.rs b/boards/argus/src/temperature/calibration.rs index 2181d5f..8d96a36 100644 --- a/boards/argus/src/temperature/calibration.rs +++ b/boards/argus/src/temperature/calibration.rs @@ -3,24 +3,23 @@ use core::str::FromStr; use defmt::Format; use heapless::{format, String, Vec}; +use strum::EnumCount; -use crate::adc::config::ADC_COUNT; use crate::adc::types::AdcDevice; -use crate::sd::csv::types::SerializeCSV; -use crate::sd::types::{FileName, OperationScope}; +use crate::linear_transformation::types::LinearTransformation; use crate::serial::service::UsartError; -use crate::temperature::config::{CHANNEL_COUNT, LINEAR_TRANSFORMATIONS_FILE_NAME, MAX_CALIBRATION_DATA_POINTS}; +use crate::temperature::config::MAX_CALIBRATION_DATA_POINTS; use crate::temperature::service::TemperatureService; -use crate::temperature::types::{LinearTransformation, TemperatureServiceError, ThermocoupleChannel}; +use crate::temperature::types::{TemperatureServiceError, ThermocoupleChannel}; // Calibration logic has been separated into its own file for clarity -impl TemperatureService { +impl TemperatureService { pub async fn calibrate(&mut self) -> Result<(), TemperatureServiceError> { // Prompt for ADC index let adc_index: usize = self .prompt("Starting temperature calibration. Enter ADC index (Starts from 0):\"\n") .await?; - if adc_index >= ADC_COUNT { + if adc_index >= AdcDevice::COUNT { self.send_message("Invalid ADC index.\n").await?; return Ok(()); } @@ -28,7 +27,7 @@ impl TemperatureService { // Prompt for channel let channel_index: usize = self.prompt("Enter thermocouple channel index (Starts from 0):\"\n").await?; - if channel_index >= CHANNEL_COUNT { + if channel_index >= ThermocoupleChannel::COUNT { self.send_message("Invalid channel index.\n").await?; return Ok(()); } @@ -50,8 +49,8 @@ impl TemperatureService { let mut calibration_data_points: Vec = Vec::new(); for data_point_index in 0..data_points_count { let message: String<64> = format!("Data Point #{}. Enter expected value in degrees celsius:\n", data_point_index + 1).unwrap(); - let expected_temperature: f32 = self.prompt(message.as_str()).await?; - let measured_temperature: f32 = self.read_thermocouple(adc, channel).await?.compensated_temperature_in_celsius.unwrap(); + let expected_temperature: f64 = self.prompt(message.as_str()).await?; + let measured_temperature: f64 = self.read_thermocouple(adc, channel).await?.compensated_temperature; let data_point = CalibrationDataPoint { expected_temperature, measured_temperature, @@ -76,7 +75,7 @@ impl TemperatureService { self.send_message(result_message.as_str()).await?; // Update calibration for the channel - self.save_transformation(transformation).await?; + self.linear_transformation_service.save_transformation(transformation).await?; Ok(()) } @@ -85,15 +84,15 @@ impl TemperatureService { adc: AdcDevice, channel: ThermocoupleChannel, data_points: Vec, - ) -> LinearTransformation { - // Keeping all types as f32 - let data_points_count: f32 = data_points.len() as f32; + ) -> LinearTransformation { + // Keeping all types as f64 + let data_points_count: f64 = data_points.len() as f64; // Accumulate sums - let mut sum_x: f32 = 0.0; // measured - let mut sum_y: f32 = 0.0; // expected - let mut sum_xx: f32 = 0.0; // measured^2 - let mut sum_xy: f32 = 0.0; // measured * expected + let mut sum_x: f64 = 0.0; // measured + let mut sum_y: f64 = 0.0; // expected + let mut sum_xx: f64 = 0.0; // measured^2 + let mut sum_xy: f64 = 0.0; // measured * expected for data_point in data_points.iter() { let x = data_point.measured_temperature; @@ -112,21 +111,6 @@ impl TemperatureService { LinearTransformation { adc, channel, gain, offset } } - async fn save_transformation( - &mut self, - transformation: LinearTransformation, - ) -> Result<(), TemperatureServiceError> { - let mut sd_card_service = self.sd_card_service.lock().await; - let path = FileName::from_str(LINEAR_TRANSFORMATIONS_FILE_NAME).unwrap(); - if !(sd_card_service.file_exists(OperationScope::Root, path.clone())?) { - sd_card_service.write(OperationScope::Root, path.clone(), LinearTransformation::get_csv_header())?; - } - - sd_card_service.write(OperationScope::Root, path.clone(), transformation.to_csv_line())?; - - Ok(()) - } - async fn prompt( &mut self, prompt: &str, @@ -154,8 +138,8 @@ impl TemperatureService { #[derive(Debug, Clone, Copy, Format)] pub struct CalibrationDataPoint { // Expected temperature value in degrees Celsius measured by the calibration instrument - pub expected_temperature: f32, + pub expected_temperature: f64, // Value measured by the ADC - pub measured_temperature: f32, + pub measured_temperature: f64, } diff --git a/boards/argus/src/temperature/config.rs b/boards/argus/src/temperature/config.rs index c57324f..3b72c51 100644 --- a/boards/argus/src/temperature/config.rs +++ b/boards/argus/src/temperature/config.rs @@ -1,14 +1,15 @@ -// Number of thermocouple channels per ADC -// Note: Not to get confused with the number of analog input channels on each ADC -// Each thermocouple channel uses a pair of analog input channels (differential measurement) -pub const CHANNEL_COUNT: usize = 4; - // Size of the queue used to send temperature readings from the temperature service to the SD card service -pub const QUEUE_SIZE: usize = 16; +pub const TEMPERATURE_READING_QUEUE_SIZE: usize = 16; // Maximum number of calibration data points allowed to be collected during a calibration session per thermocouple channel pub const MAX_CALIBRATION_DATA_POINTS: usize = 10; // File name used to read/write linear transformations that applied to thermocouple readings to/from the SD card // Linear transformations are stored in CSV format -pub const LINEAR_TRANSFORMATIONS_FILE_NAME: &str = "t.csv"; // Cannot be longer than 12 characters +pub const LINEAR_TRANSFORMATIONS_FILE_NAME: &str = "t_temp.csv"; // Cannot be longer than 12 characters + +// Resistance of the RTD at 0 °C. +pub const RTD_RESISTANCE_AT_0C: f32 = 1000.0; // Ohms + +// Measure RTDs at a slower interval than the thermocouples +pub const RTD_MEASUREMENT_INTERVAL: u64 = 5000; // milliseconds diff --git a/boards/argus/src/temperature/mod.rs b/boards/argus/src/temperature/mod.rs index 7c1f41a..7ec4a1c 100644 --- a/boards/argus/src/temperature/mod.rs +++ b/boards/argus/src/temperature/mod.rs @@ -1,5 +1,7 @@ pub mod calibration; pub mod config; +pub mod rtd; pub mod service; pub mod tasks; +pub mod thermocouple; pub mod types; diff --git a/boards/argus/src/temperature/rtd.rs b/boards/argus/src/temperature/rtd.rs new file mode 100644 index 0000000..5d56e24 --- /dev/null +++ b/boards/argus/src/temperature/rtd.rs @@ -0,0 +1,75 @@ +use libm::sqrtf; + +// For RTDs (PT100, PT1000, etc). +// Reference: IEC 60751:2016 +// Uses the callandar-van dusen equation to convert resistance to temperature. +// Valid for -200 °C to +850 °C +// See https://www.ti.com/lit/an/sbaa275a/sbaa275a.pdf?ts=1758561034572 +pub fn convert_resistance_to_temperature( + resistance_at_0c: f32, + measured_resistance: f32, +) -> f32 { + // IEC 60751 constants for α = 0.00385 + const A: f32 = 3.9083e-3; + const B: f32 = -5.775e-7; + const C: f32 = -4.183e-12; // used only below 0 °C + + // Normalize measured resistance to ratio against R0. + // This keeps numbers close to 1.0 and improves numeric behavior. + let resistance_ratio = measured_resistance / resistance_at_0c; + + // ----- Case 1: Temperature ≥ 0 °C ----- + // Quadratic model: R(T) = R0 * (1 + A*T + B*T^2) + // Inverse: T = (-A + sqrt(A^2 - 4*B*(1 - R/R0))) / (2*B) + // Use only if the discriminant is non-negative and the result is ≥ 0. + let discriminant: f32 = A * A - 4.0 * B * (1.0 - resistance_ratio); + + if discriminant >= 0.0 { + let temperature_celsius = (-A + sqrtf(discriminant)) / (2.0 * B); // B < 0 + + if temperature_celsius >= 0.0 && temperature_celsius.is_finite() { + return temperature_celsius; + } + } + + // ----- Case 2: Temperature < 0 °C ----- + // Full Callendar–Van Dusen (includes coefficient C): + // R(T) = R0 * (1 + A*T + B*T^2 + C*(T - 100)*T^3) + // Solve numerically with Newton–Raphson. + + // Initial guess: linearized around 0 °C (good for small negative temps). + let mut temperature_estimate = (resistance_ratio - 1.0) / A; + + // Perform a small, fixed number of iterations similar to gradient descent + // for <0 °C over the usual range. Stops early if the correction is tiny. + for _ in 0..8 { + let t = temperature_estimate; + let t2 = t * t; + let t3 = t2 * t; + + // Predicted resistance from current temperature estimate + let predicted_measured_resistance = resistance_at_0c * (1.0 + A * t + B * t2 + C * (t - 100.0) * t3); + + // Error relative to the measured resistance + let resistance_error = predicted_measured_resistance - measured_resistance; + + // Derivative dR/dT (expanded; avoids powi) + // d/dT [R0*(1 + A*T + B*T^2 + C*(T - 100)*T^3)] + // = R0*(A + 2*B*T + C*(4*T^3 - 300*T^2)) + let derivative_ohms_per_c = resistance_at_0c * (A + 2.0 * B * t + C * (4.0 * t3 - 300.0 * t2)); + + // Guard against pathological derivative values + if derivative_ohms_per_c == 0.0 || !derivative_ohms_per_c.is_finite() { + break; + } + + let correction_step_celsius = resistance_error / derivative_ohms_per_c; + temperature_estimate -= correction_step_celsius; + + if correction_step_celsius.abs() < 1e-4 { + break; + } + } + + temperature_estimate +} diff --git a/boards/argus/src/temperature/service.rs b/boards/argus/src/temperature/service.rs index 5d12603..1543506 100644 --- a/boards/argus/src/temperature/service.rs +++ b/boards/argus/src/temperature/service.rs @@ -1,37 +1,39 @@ -use core::str::FromStr; - -use defmt::info; use embassy_time::Instant; -use heapless::LinearMap; +use strum::EnumCount; -use crate::adc::config::ADC_COUNT; -use crate::adc::driver::types::{DataRate, Filter, Gain, ReferenceRange}; +use crate::adc::driver::types::{AnalogChannel, DataRate, Filter, Gain, ReferenceRange}; use crate::adc::service::AdcService; use crate::adc::types::AdcDevice; -use crate::sd::csv::types::SerializeCSV; +use crate::linear_transformation::service::LinearTransformationService; use crate::sd::service::SDCardService; -use crate::sd::types::{FileName, OperationScope, SdCardError}; use crate::serial::service::SerialService; -use crate::temperature::config::{CHANNEL_COUNT, LINEAR_TRANSFORMATIONS_FILE_NAME}; -use crate::temperature::types::{LinearTransformation, TemperatureServiceError, ThermocoupleChannel, ThermocoupleReading, ThermocoupleReadingQueue}; +use crate::temperature::config::{LINEAR_TRANSFORMATIONS_FILE_NAME, RTD_RESISTANCE_AT_0C}; +use crate::temperature::rtd; +use crate::temperature::thermocouple::type_k; +use crate::temperature::types::{TemperatureServiceError, ThermocoupleChannel, ThermocoupleReading, ThermocoupleReadingQueue}; use crate::utils::types::AsyncMutex; // A channel for buffering the temperature readings and decoupling the logging to sd task from the measurement task pub static THERMOCOUPLE_READING_QUEUE: ThermocoupleReadingQueue = ThermocoupleReadingQueue::new(); -pub struct TemperatureService { +pub struct TemperatureService { // Other services are passed by a mutex to ensure safe concurrent access - pub adc_service: &'static AsyncMutex, + pub adc_service: &'static AsyncMutex>, pub sd_card_service: &'static AsyncMutex, pub serial_service: &'static AsyncMutex, + // Store the last RTD reading in Celsius to use for cold junction compensation + // This is cached here to avoid reading the RTD multiple times when reading multiple thermocouples + // We have one RTD per ADC, so we store an array of last readings + pub last_rtd_reading: [Option; ADC_COUNT], + // Linear transformations that are applied on top of the raw readings for each ADC and channel - pub transformations: LinearMap, ADC_COUNT>, + pub linear_transformation_service: LinearTransformationService, } -impl TemperatureService { +impl TemperatureService { pub fn new( - adc_service: &'static AsyncMutex, + adc_service: &'static AsyncMutex>, sd_card_service: &'static AsyncMutex, serial_service: &'static AsyncMutex, ) -> Self { @@ -39,7 +41,8 @@ impl TemperatureService { adc_service, sd_card_service, serial_service, - transformations: LinearMap::new(), + last_rtd_reading: [None; ADC_COUNT], + linear_transformation_service: LinearTransformationService::new(sd_card_service, LINEAR_TRANSFORMATIONS_FILE_NAME), } } @@ -50,11 +53,11 @@ impl TemperatureService { driver.filter = Filter::Sinc3; driver.enable_internal_reference = true; driver.gain = Gain::G32; - driver.delay_after_setting_channel = 100; // 100 ms delay to allow the ADC to stabilize after switching channels + driver.delay_after_setting_channel = 50; // 50 ms delay to allow the ADC to stabilize after switching channels driver.apply_configurations().await?; } - self.load_transformations().await?; + self.linear_transformation_service.load_transformations().await?; Ok(()) } @@ -68,54 +71,58 @@ impl TemperatureService { // Get the respective "adc channel" pair for the "thermocouple channel" let (positive_channel, negative_channel) = channel.to_analog_input_channel_pair(); + // Read the voltage from the ADC in millivolts let voltage = adc_service.drivers[adc as usize] .read_differential(positive_channel, negative_channel) - .await?; + .await? * 1000.0; // Convert to millivolts + + // Get the cold junction temperature from the last RTD reading for this ADC + let cold_junction_temperature = self.last_rtd_reading[adc as usize].unwrap_or(0.0); let thermocouple_reading = ThermocoupleReading { - timestamp_in_milliseconds: Instant::now().as_millis(), - voltage_in_millivolts: voltage * 1000.0, - uncompensated_temperature_in_celsius: None, // Placeholder for actual reading - compensated_temperature_in_celsius: None, // Placeholder for actual compensation logic - cold_junction_temperature_in_celsius: None, // Placeholder for actual cold junction temperature + timestamp: Instant::now().as_millis(), + voltage, + uncompensated_temperature: type_k::convert_voltage_to_temperature(voltage as f64)?, + compensated_temperature: type_k::convert_voltage_to_temperature_with_cold_junction_compensation( + voltage as f64, + cold_junction_temperature as f64, + )?, + cold_junction_temperature, }; Ok(thermocouple_reading) } - pub async fn load_transformations(&mut self) -> Result<(), TemperatureServiceError> { - let result = self.sd_card_service.lock().await.read( - OperationScope::Root, - FileName::from_str(LINEAR_TRANSFORMATIONS_FILE_NAME).unwrap(), - |line| { - if *line == LinearTransformation::get_csv_header() { - return true; // Skip header line - } - let transformation = LinearTransformation::from_csv_line(line); - self.load_transformation(transformation); - return true; // Continue reading - }, - ); - - match result { - Ok(_) => (), - Err(SdCardError::NotFound) => { - // If transformations not found, keep using the defaults and ignore this error. - info!("Linear transformations file not found, using defaults. Gain = 1, Offset = 0"); - } - Err(e) => return Err(TemperatureServiceError::SdCardError(e)), - } - Ok(()) - } - - pub fn load_transformation( + pub async fn read_rtd( &mut self, - transformation: LinearTransformation, - ) { - if !self.transformations.contains_key(&transformation.adc) { - let _ = self.transformations.insert(transformation.adc, LinearMap::new()); - } - let map = self.transformations.get_mut(&transformation.adc).unwrap(); - let _ = map.insert(transformation.channel, transformation); + adc: AdcDevice, + ) -> Result { + let mut adc_service = self.adc_service.lock().await; + let driver = &mut adc_service.drivers[adc as usize]; + let previous_gain = driver.gain; + + // Set the gain to 1 for RTD measurement to avoid saturating the ADC + driver.gain = Gain::G1; + driver.apply_gain_and_data_rate_configuration().await?; + driver.wait_for_next_data().await; + + // Perform the measurement at the gain of 1 + + // Note: This is based on Argus V2 design as of September 22, 2025 + // The AIN8-9 sequence is flipped accidentally so AIN9 is before the RTD and AIN8 is after the RTD + let voltage_before_rtd = driver.read_single_ended(AnalogChannel::AIN9).await?; + let voltage_after_rtd = driver.read_single_ended(AnalogChannel::AIN8).await?; + + // Restore the previous gain + driver.gain = previous_gain; + driver.apply_gain_and_data_rate_configuration().await?; + + // I = voltage_after_rtd / R6 + // measured_resistance = V_RTD / I = R6 * (voltage_before_rtd - voltage_after_rtd) / voltage_after_rtd + const R6: f32 = 1000.0; + let measured_resistance = R6 * (voltage_before_rtd - voltage_after_rtd) / voltage_after_rtd; + let estimated_temperature = rtd::convert_resistance_to_temperature(RTD_RESISTANCE_AT_0C, measured_resistance); + + Ok(estimated_temperature) } } diff --git a/boards/argus/src/temperature/tasks/log_measurements.rs b/boards/argus/src/temperature/tasks/log_measurements.rs index bb55086..bf3f93e 100644 --- a/boards/argus/src/temperature/tasks/log_measurements.rs +++ b/boards/argus/src/temperature/tasks/log_measurements.rs @@ -1,15 +1,15 @@ +use csv::SerializeCSV; use embassy_executor::task; use heapless::format; +use strum::EnumCount; -use crate::adc::config::ADC_COUNT; -use crate::sd::csv::types::SerializeCSV; +use crate::adc::types::AdcDevice; use crate::sd::service::SDCardService; use crate::sd::types::{FileName, OperationScope}; use crate::state_machine::service::StateMachineWorker; use crate::state_machine::types::States; -use crate::temperature::config::CHANNEL_COUNT; use crate::temperature::service::THERMOCOUPLE_READING_QUEUE; -use crate::temperature::types::ThermocoupleReading; +use crate::temperature::types::{ThermocoupleChannel, ThermocoupleReading}; use crate::utils::types::AsyncMutex; // Task for picking up the readings from the channel and logging them to the SD card @@ -38,8 +38,8 @@ async fn initialize_csv_files(sd_card_service_mutex: &'static AsyncMutex>, +) { + worker + .run_while(States::Recording, async |_| -> Result<(), ()> { + for adc_index in 0..AdcDevice::COUNT { + let mut temperature_service = temperature_service_mutex.lock().await; + let adc = AdcDevice::from(adc_index); + let result = temperature_service.read_rtd(adc).await; + match result { + Ok(data) => { + debug!("RTD Temperature {}: {}C", adc, data); + temperature_service.last_rtd_reading[adc_index] = Some(data); + } + Err(err) => { + error!("Error reading RTD for {}: {:?}", adc, err); + } + } + } + + // Delay the RTD measurement because it's not as critical as the thermocouples. We just need to read every once in a while + Timer::after_millis(RTD_MEASUREMENT_INTERVAL).await; + + Ok(()) + }) + .await + .unwrap(); +} diff --git a/boards/argus/src/temperature/tasks/measure.rs b/boards/argus/src/temperature/tasks/measure_thermocouples.rs similarity index 63% rename from boards/argus/src/temperature/tasks/measure.rs rename to boards/argus/src/temperature/tasks/measure_thermocouples.rs index 9bde667..c8dfe1b 100644 --- a/boards/argus/src/temperature/tasks/measure.rs +++ b/boards/argus/src/temperature/tasks/measure_thermocouples.rs @@ -1,45 +1,43 @@ -use defmt::debug; +use defmt::{debug, error}; use embassy_executor::task; +use embassy_futures::yield_now; +use strum::EnumCount; -use crate::adc::config::ADC_COUNT; use crate::adc::types::AdcDevice; use crate::state_machine::service::StateMachineWorker; use crate::state_machine::types::States; -use crate::temperature::config::CHANNEL_COUNT; use crate::temperature::service::{TemperatureService, THERMOCOUPLE_READING_QUEUE}; use crate::temperature::types::ThermocoupleChannel; use crate::utils::types::AsyncMutex; // Task that iterates through the ADCs and channels, measures the temperature, and enqueues the readings to a channel #[task] -pub async fn measure( +pub async fn measure_thermocouples( mut worker: StateMachineWorker, - temperature_service_mutex: &'static AsyncMutex, + temperature_service_mutex: &'static AsyncMutex>, ) { - // Configure the ADCs for temperature measurement - temperature_service_mutex.lock().await.setup().await.unwrap(); - worker .run_while(States::Recording, async |_| -> Result<(), ()> { - let mut temperature_service = temperature_service_mutex.lock().await; - - for adc_index in 0..ADC_COUNT { - for channel_index in 0..CHANNEL_COUNT { + for adc_index in 0..AdcDevice::COUNT { + for channel_index in 0..ThermocoupleChannel::COUNT { let adc = AdcDevice::from(adc_index); let channel = ThermocoupleChannel::from(channel_index); - let data = temperature_service.read_thermocouple(adc, channel).await; + let data = temperature_service_mutex.lock().await.read_thermocouple(adc, channel).await; match data { Ok(data) => { debug!("ADC {} Channel {}: {}", adc, channel, data); THERMOCOUPLE_READING_QUEUE.send((adc, channel, data)).await; } - Err(error) => { - debug!("Error reading ADC {} Channel {}: {:?}", adc, channel, error); + Err(err) => { + error!("Error reading ADC {} Channel {}: {:?}", adc, channel, err); continue; } } } } + + // Yield to allow other tasks to run, especially the RTD measurement task + yield_now().await; Ok(()) }) .await diff --git a/boards/argus/src/temperature/tasks/mod.rs b/boards/argus/src/temperature/tasks/mod.rs index 1447d91..b968a01 100644 --- a/boards/argus/src/temperature/tasks/mod.rs +++ b/boards/argus/src/temperature/tasks/mod.rs @@ -1,5 +1,7 @@ mod log_measurements; -mod measure; +mod measure_rtds; +mod measure_thermocouples; pub use log_measurements::*; -pub use measure::*; +pub use measure_rtds::*; +pub use measure_thermocouples::*; diff --git a/boards/argus/src/temperature/thermocouple/mod.rs b/boards/argus/src/temperature/thermocouple/mod.rs new file mode 100644 index 0000000..5e561e1 --- /dev/null +++ b/boards/argus/src/temperature/thermocouple/mod.rs @@ -0,0 +1 @@ +pub mod type_k; diff --git a/boards/argus/src/temperature/thermocouple/type_k/error.rs b/boards/argus/src/temperature/thermocouple/type_k/error.rs new file mode 100644 index 0000000..e6c4f55 --- /dev/null +++ b/boards/argus/src/temperature/thermocouple/type_k/error.rs @@ -0,0 +1,10 @@ +use defmt::Format; + +/// Errors that can occur during conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Format)] +pub enum ThermocoupleError { + /// The provided millivolt value is outside the supported ITS-90 range for Type K. + MillivoltsOutOfRange, + /// The provided cold-junction temperature is outside the supported ITS-90 range for Type K. + ColdJunctionTemperatureOutOfRange, +} diff --git a/boards/argus/src/temperature/thermocouple/type_k/forward.rs b/boards/argus/src/temperature/thermocouple/type_k/forward.rs new file mode 100644 index 0000000..9f4e6ac --- /dev/null +++ b/boards/argus/src/temperature/thermocouple/type_k/forward.rs @@ -0,0 +1,70 @@ +// ──────────────────────────────────────────────────────────────────────────── +// ITS-90: Type K reference function E(t) (voltage from temperature) +// Two ranges: +// +// A) -270 °C ≤ t ≤ 0 °C +// E = Σ c_i * t^i (i = 0..10) +// +// B) 0 °C < t ≤ 1372 °C +// E = Σ c_i * t^i (i = 0..9) + a0 * exp( a1 * (t - a2)^2 ) +// +// Coefficients are from the official tables (units: E in mV, t in °C). +// ──────────────────────────────────────────────────────────────────────────── + +const K_E_OF_T_NEG_COEFFICIENTS: [f64; 11] = [ + 0.000_000_000_000e+00, + 0.394_501_280_250e-01, + 0.236_223_735_980e-04, + -0.328_589_067_840e-06, + -0.499_048_287_770e-08, + -0.675_090_591_730e-10, + -0.574_103_274_280e-12, + -0.310_888_728_940e-14, + -0.104_516_093_650e-16, + -0.198_892_668_780e-19, + -0.163_226_974_860e-22, +]; + +const K_E_OF_T_POS_COEFFICIENTS: [f64; 10] = [ + -0.176_004_136_860e-01, + 0.389_212_049_750e-01, + 0.185_587_700_320e-04, + -0.994_575_928_740e-07, + 0.318_409_457_190e-09, + -0.560_728_448_890e-12, + 0.560_750_590_590e-15, + -0.320_207_200_030e-18, + 0.971_511_471_520e-22, + -0.121_047_212_750e-25, +]; + +const K_E_OF_T_POS_A0: f64 = 0.118_597_600_000e+00; +const K_E_OF_T_POS_A1: f64 = -0.118_343_200_000e-03; +const K_E_OF_T_POS_A2: f64 = 0.126_968_600_000e+03; + +/// Returns E(t) in voltage for a Type K thermocouple, or None if t is out of range. +pub fn convert_temperature_to_voltage(temperature: f64) -> Option { + if !(-270.0..=1372.0).contains(&temperature) { + return None; + } + + // Evaluate polynomial with Horner’s method (kept readable). + let polynomial = |coefficients: &[f64]| -> f64 { + let mut accumulator = 0.0; + // Highest order first for Horner’s method: + for &coefficient in coefficients.iter().rev() { + accumulator = accumulator * temperature + coefficient; + } + accumulator + }; + + if temperature <= 0.0 { + Some(polynomial(&K_E_OF_T_NEG_COEFFICIENTS)) + } else { + // Above 0 °C we add the exponential correction term. + let base = polynomial(&K_E_OF_T_POS_COEFFICIENTS); + let dt = temperature - K_E_OF_T_POS_A2; + let exp_term = K_E_OF_T_POS_A0 * libm::exp(K_E_OF_T_POS_A1 * (dt * dt)); + Some(base + exp_term) + } +} diff --git a/boards/argus/src/temperature/thermocouple/type_k/inverse.rs b/boards/argus/src/temperature/thermocouple/type_k/inverse.rs new file mode 100644 index 0000000..08f10a0 --- /dev/null +++ b/boards/argus/src/temperature/thermocouple/type_k/inverse.rs @@ -0,0 +1,85 @@ +use crate::temperature::thermocouple::type_k::error::ThermocoupleError; + +// ──────────────────────────────────────────────────────────────────────────── +// ITS-90: Type K inverse function t90(E) (temperature from voltage) +// Three ranges: +// +// 1) −5.891 mV ≤ E ≤ 0.000 mV +// t = Σ d_i * E^i (i = 0..8) +// +// 2) 0.000 mV < E ≤ 20.644 mV +// t = Σ d_i * E^i (i = 0..10) +// +// 3) 20.644 mV < E ≤ 54.886 mV +// t = Σ d_i * E^i (i = 0..6) +// +// Coefficients below are the official ITS-90 values (E in mV, t in °C). +// ──────────────────────────────────────────────────────────────────────────── + +const K_T_OF_E_D_NEG: [f64; 9] = [ + 0.000_000_0e+00, + 2.517_346_2e+01, + -1.166_287_8e+00, + -1.083_363_8e+00, + -8.977_354_0e-01, + -3.734_237_7e-01, + -8.663_264_3e-02, + -1.045_059_8e-02, + -5.192_057_7e-04, +]; +// Note: The published list for the negative range has zero for higher orders; the above length (0..8) matches the table. + +const K_T_OF_E_D_MID: [f64; 11] = [ + 0.000_000_0e+00, + 2.508_355_0e+01, + 7.860_106_0e-02, + -2.503_131_0e-01, + 8.315_270_0e-02, + -1.228_034_0e-02, + 9.804_036_0e-04, + -4.413_030_0e-05, + 1.057_734_0e-06, + -1.052_755_0e-08, + 0.000_000_0e+00, // table shows up to d9; any higher terms are zero. +]; + +const K_T_OF_E_D_HIGH: [f64; 7] = [ + -1.318_058_0e+02, + 4.830_222_0e+01, + -1.646_031_0e+00, + 5.464_731_0e-02, + -9.650_715_0e-04, + 8.802_193_0e-06, + -3.110_810_0e-08, +]; + +const K_E_MIN_MV: f64 = -5.891; +const K_E_MID_MAX_MV: f64 = 20.644; +const K_E_MAX_MV: f64 = 54.886; + +/// Returns t90(E) in °C for Type K, or an error if E is out of range. +pub fn convert_voltage_to_temperature(voltage: f64) -> Result { + if !(K_E_MIN_MV..=K_E_MAX_MV).contains(&voltage) { + return Err(ThermocoupleError::MillivoltsOutOfRange); + } + + if voltage <= 0.0 { + Ok(evaluate_power_series(voltage, &K_T_OF_E_D_NEG)) + } else if voltage <= K_E_MID_MAX_MV { + Ok(evaluate_power_series(voltage, &K_T_OF_E_D_MID)) + } else { + Ok(evaluate_power_series(voltage, &K_T_OF_E_D_HIGH)) + } +} + +/// Evaluate a power series t = Σ c_i * x^i using Horner’s method, kept legible. +fn evaluate_power_series( + x: f64, + coefficients: &[f64], +) -> f64 { + let mut accumulator = 0.0; + for &c in coefficients.iter().rev() { + accumulator = accumulator * x + c; + } + accumulator +} diff --git a/boards/argus/src/temperature/thermocouple/type_k/mod.rs b/boards/argus/src/temperature/thermocouple/type_k/mod.rs new file mode 100644 index 0000000..19210c0 --- /dev/null +++ b/boards/argus/src/temperature/thermocouple/type_k/mod.rs @@ -0,0 +1,16 @@ +pub mod error; +pub mod forward; +pub mod inverse; + +pub use error::*; +pub use forward::*; +pub use inverse::*; + +pub fn convert_voltage_to_temperature_with_cold_junction_compensation( + measured_voltage: f64, + cold_junction_temperature: f64, +) -> Result { + let cj_mv = convert_temperature_to_voltage(cold_junction_temperature).ok_or(ThermocoupleError::ColdJunctionTemperatureOutOfRange)?; + let compensated_mv = measured_voltage + cj_mv; + convert_voltage_to_temperature(compensated_mv) +} diff --git a/boards/argus/src/temperature/types/error.rs b/boards/argus/src/temperature/types/error.rs index d7818cb..549df7c 100644 --- a/boards/argus/src/temperature/types/error.rs +++ b/boards/argus/src/temperature/types/error.rs @@ -1,27 +1,15 @@ use defmt::Format; +use derive_more::From; use crate::adc::service::AdcError; use crate::sd::types::SdCardError; use crate::serial::service::UsartError; +use crate::temperature::thermocouple::type_k::ThermocoupleError; -#[derive(Debug, Format)] +#[derive(Debug, Format, From)] pub enum TemperatureServiceError { AdcError(AdcError), UsartError(UsartError), SdCardError(SdCardError), -} -impl From for TemperatureServiceError { - fn from(err: AdcError) -> Self { - TemperatureServiceError::AdcError(err) - } -} -impl From for TemperatureServiceError { - fn from(err: UsartError) -> Self { - TemperatureServiceError::UsartError(err) - } -} -impl From for TemperatureServiceError { - fn from(err: SdCardError) -> Self { - TemperatureServiceError::SdCardError(err) - } + ThermocoupleError(ThermocoupleError), } diff --git a/boards/argus/src/temperature/types/linear_transformation.rs b/boards/argus/src/temperature/types/linear_transformation.rs deleted file mode 100644 index 03e41d6..0000000 --- a/boards/argus/src/temperature/types/linear_transformation.rs +++ /dev/null @@ -1,34 +0,0 @@ -use core::str::FromStr; - -use defmt::Format; -use serde::{Deserialize, Serialize}; - -use crate::adc::types::AdcDevice; -use crate::sd::csv::types::SerializeCSV; -use crate::sd::types::Line; -use crate::temperature::types::thermocouple_channel::ThermocoupleChannel; - -// Represents a linear transformation applied to the thermocouple readings -// corrected_value = value_with_error * gain + offset -#[derive(Debug, Clone, Copy, Format, Serialize, Deserialize)] -pub struct LinearTransformation { - pub adc: AdcDevice, - pub channel: ThermocoupleChannel, - pub gain: f32, - pub offset: f32, -} -impl Default for LinearTransformation { - fn default() -> Self { - Self { - adc: AdcDevice::Adc1, - channel: ThermocoupleChannel::Channel1, - gain: 1.0, // Default to unity gain - offset: 0.0, // Default to zero offset - } - } -} -impl SerializeCSV for LinearTransformation { - fn get_csv_header() -> Line { - Line::from_str("ADC Index,Channel Index,Gain,Offset\n").unwrap() - } -} diff --git a/boards/argus/src/temperature/types/mod.rs b/boards/argus/src/temperature/types/mod.rs index 213f8b5..7988feb 100644 --- a/boards/argus/src/temperature/types/mod.rs +++ b/boards/argus/src/temperature/types/mod.rs @@ -1,11 +1,9 @@ pub mod error; -pub mod linear_transformation; pub mod queue; pub mod thermocouple_channel; pub mod thermocouple_reading; pub use error::*; -pub use linear_transformation::*; pub use queue::*; pub use thermocouple_channel::*; pub use thermocouple_reading::*; diff --git a/boards/argus/src/temperature/types/queue.rs b/boards/argus/src/temperature/types/queue.rs index bf1268d..627ab9f 100644 --- a/boards/argus/src/temperature/types/queue.rs +++ b/boards/argus/src/temperature/types/queue.rs @@ -2,9 +2,10 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::Channel; use crate::adc::types::AdcDevice; -use crate::sd::config::QUEUE_SIZE; +use crate::temperature::config::TEMPERATURE_READING_QUEUE_SIZE; use crate::temperature::types::thermocouple_channel::ThermocoupleChannel; use crate::temperature::types::thermocouple_reading::ThermocoupleReading; // Type alias for the thermocouple reading queue used to decouple reading from ADC and writing to logging pipes -pub type ThermocoupleReadingQueue = Channel; +pub type ThermocoupleReadingQueue = + Channel; diff --git a/boards/argus/src/temperature/types/thermocouple_channel.rs b/boards/argus/src/temperature/types/thermocouple_channel.rs index 4f41849..8127269 100644 --- a/boards/argus/src/temperature/types/thermocouple_channel.rs +++ b/boards/argus/src/temperature/types/thermocouple_channel.rs @@ -1,10 +1,12 @@ use defmt::Format; use serde::{Deserialize, Serialize}; +use strum::EnumCount; use crate::adc::driver::types::AnalogChannel; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Format, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Format, Serialize, Deserialize, EnumCount, Default)] pub enum ThermocoupleChannel { + #[default] Channel1 = 0, Channel2 = 1, Channel3 = 2, diff --git a/boards/argus/src/temperature/types/thermocouple_reading.rs b/boards/argus/src/temperature/types/thermocouple_reading.rs index f74810b..676d689 100644 --- a/boards/argus/src/temperature/types/thermocouple_reading.rs +++ b/boards/argus/src/temperature/types/thermocouple_reading.rs @@ -1,31 +1,32 @@ use core::str::FromStr; +use csv::SerializeCSV; use defmt::Format; use serde::{Deserialize, Serialize}; -use crate::sd::csv::types::SerializeCSV; +use crate::sd::config::MAX_LINE_LENGTH; use crate::sd::types::Line; // Represents a single temperature reading from a thermocouple channel #[derive(Debug, Clone, Copy, Format, Serialize, Deserialize)] pub struct ThermocoupleReading { // Timestamp of the reading in milliseconds since epoch - pub timestamp_in_milliseconds: u64, + pub timestamp: u64, // Thermocouple voltage difference measured in millivolts - pub voltage_in_millivolts: f32, + pub voltage: f32, // Cold-junction-compensated temperature of the thermocouple in degrees Celsius - pub compensated_temperature_in_celsius: Option, + pub compensated_temperature: f64, // Uncompensated temperature of the thermocouple in degrees Celsius - pub uncompensated_temperature_in_celsius: Option, + pub uncompensated_temperature: f64, // Temperature of the cold junction in degrees Celsius - pub cold_junction_temperature_in_celsius: Option, + pub cold_junction_temperature: f32, } -impl SerializeCSV for ThermocoupleReading { +impl SerializeCSV for ThermocoupleReading { fn get_csv_header() -> Line { Line::from_str( "Timestamp (ms),\ diff --git a/boards/argus/src/utils/hal.rs b/boards/argus/src/utils/hal.rs index 8146d95..b8dd897 100644 --- a/boards/argus/src/utils/hal.rs +++ b/boards/argus/src/utils/hal.rs @@ -49,5 +49,5 @@ pub fn configure_hal() -> Peripherals { config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz config.rcc.voltage_scale = VoltageScale::Scale1; - return init(config); + init(config) } diff --git a/boards/argus/tests/sd.rs b/boards/argus/tests/sd.rs index 7b3479c..3c7952b 100644 --- a/boards/argus/tests/sd.rs +++ b/boards/argus/tests/sd.rs @@ -10,6 +10,8 @@ mod tests { // But with assert_eq, etc we don't need to see panic messages anyway // So for now this is acceptable, but we should try to fix this in the future + use core::str::FromStr; + use argus::sd::service::SDCardService; use argus::sd::types::{FileName, Line, OperationScope}; use argus::utils::hal::configure_hal; @@ -19,20 +21,20 @@ mod tests { #[init] fn init() -> SDCardService { let peripherals = configure_hal(); - let sd_card_service = SDCardService::new(peripherals.SPI1, peripherals.PA5, peripherals.PA7, peripherals.PA6, peripherals.PC4); - sd_card_service + + SDCardService::new(peripherals.SPI1, peripherals.PA5, peripherals.PA7, peripherals.PA6, peripherals.PC4) } #[test] fn writing_directly_to_sd_card(mut sd_card_service: SDCardService) { - let path: String<12> = FileName::from("test.txt"); - let text = Line::from("Hello, world!"); + let path: String<12> = FileName::from_str("test.txt").unwrap(); + let text = Line::from_str("Hello, world!").unwrap(); sd_card_service.delete(OperationScope::Root, path.clone()).unwrap(); sd_card_service.write(OperationScope::Root, path.clone(), text.clone()).unwrap(); sd_card_service .read(OperationScope::Root, path.clone(), |line| { assert_eq!(line.as_str(), text.as_str()); - return false; + false }) .unwrap(); diff --git a/common/csv/Cargo.toml b/common/csv/Cargo.toml new file mode 100644 index 0000000..4bac8bc --- /dev/null +++ b/common/csv/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "csv" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" +harness = false + +[dependencies] +serde = { workspace = true, features = ["derive", "serde_derive"] } +serde-csv-core = { workspace = true } +heapless = { workspace = true } diff --git a/common/csv/README.md b/common/csv/README.md new file mode 100644 index 0000000..7e4fdb0 --- /dev/null +++ b/common/csv/README.md @@ -0,0 +1,2 @@ +# CSV +Provides a trait that is agreed to be used for any CSV-serializable struct. \ No newline at end of file diff --git a/boards/argus/src/sd/csv/types.rs b/common/csv/src/lib.rs similarity index 57% rename from boards/argus/src/sd/csv/types.rs rename to common/csv/src/lib.rs index bdf85ed..291e7b4 100644 --- a/boards/argus/src/sd/csv/types.rs +++ b/common/csv/src/lib.rs @@ -1,24 +1,25 @@ +#![no_std] +#![no_main] + use core::str::FromStr; use heapless::String; use serde::{Deserialize, Serialize}; use serde_csv_core::{Reader, Writer}; -use crate::sd::types::Line; - -pub trait SerializeCSV: Serialize + for<'d> Deserialize<'d> { - fn get_csv_header() -> Line; - fn from_csv_line(line: &Line) -> Self { +pub trait SerializeCSV: Serialize + for<'d> Deserialize<'d> { + fn get_csv_header() -> String; + fn from_csv_line(line: &String) -> Self { let mut reader = Reader::<255>::new(); let (record, _n) = reader.deserialize::(line.as_bytes()).unwrap(); record } - fn to_csv_line(&self) -> Line { + fn to_csv_line(&self) -> String { let mut writer = Writer::new(); let mut line = [0u8; 255]; writer.serialize(&self, &mut line).unwrap(); let line_str = core::str::from_utf8(&line).unwrap(); - let line_string = String::from_str(line_str).unwrap(); - line_string + + String::from_str(line_str).unwrap() } } diff --git a/common/thermocouple-converter/Cargo.toml b/common/thermocouple-converter/Cargo.toml deleted file mode 100644 index b9f42db..0000000 --- a/common/thermocouple-converter/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[package] -name = "thermocouple-converter" -version = "0.1.0" -edition = "2021" - diff --git a/common/thermocouple-converter/src/lib.rs b/common/thermocouple-converter/src/lib.rs deleted file mode 100644 index 473a4d6..0000000 --- a/common/thermocouple-converter/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! This crate contains code to convert type K thermocouple voltages to temperatures. - -#![no_std] - -/// Type K thermocouple coefficients for polynomial voltage to temperature conversion. -/// See https://www.eevblog.com/forum/metrology/a-dive-into-k-type-thermocouple-maths/ -pub const TYPE_K_COEF: [[f64; 10]; 3] = [ - [ - // Coefficients for -5.891 <= voltage <= 0.0 - 0.0000000E+00, - 2.5173462E+01, - -1.1662878E+00, - -1.0833638E+00, - -8.9773540E-01, - -3.7342377E-01, - -8.6632643E-02, - -1.0450598E-02, - -5.1920577E-04, - 0.0000000E+00, - ], - [ - // Coefficients for 0.0 <= voltage <= 20.644 - 0.000000E+00, - 2.508355E+01, - 7.860106E-02, - -2.503131E-01, - 8.315270E-02, - -1.228034E-02, - 9.804036E-04, - -4.413030E-05, - 1.057734E-06, - -1.052755E-08, - ], - [ - // Coefficients for 20.644 <= voltage <= 54.886 - -1.318058E+02, - 4.830222E+01, - -1.646031E+00, - 5.464731E-02, - -9.650715E-04, - 8.802193E-06, - -3.110810E-08, - 0.000000E+00, - 0.000000E+00, - 0.000000E+00, - ], -]; - -/// Converts a 32-bit ADC reading to a temperature in celsius. -pub fn adc_to_celsius(adc_reading: i32) -> Option { - voltage_to_celsius(adc_to_voltage(adc_reading)) -} - -/// Converts a 32-bit ADC reading to a voltage. -pub fn adc_to_voltage(adc_reading: i32) -> f64 { - const REFERENCE_VOLTAGE: f64 = 5.0; - const MAX_ADC_VALUE: f64 = 4_294_967_296.0; - // change 32 to be waht the gain is - const V_SCALE: f64 = (REFERENCE_VOLTAGE / MAX_ADC_VALUE) / 32.0; - - adc_reading as f64 * V_SCALE -} - -/// Converts voltage to celsius for type K thermocouples. -pub fn voltage_to_celsius(mut voltage: f64) -> Option { - voltage *= 1000.0; - return match voltage { - -5.891..=0.0 => Some(calc_temp_exponential(voltage, &TYPE_K_COEF[0])), - 0.0..=20.644 => Some(calc_temp_exponential(voltage, &TYPE_K_COEF[1])), - 20.644..=54.886 => Some(calc_temp_exponential(voltage, &TYPE_K_COEF[2])), - - // Insane temperature ranges that should never be reached. - // Hitting this is a strong indicator of a bug in the Argus system. - _ => None, - }; -} - -/// Calculates temperature using the NIST's exponential polynomial. -fn calc_temp_exponential( - voltage: f64, - coef: &[f64], -) -> f64 { - let mut result = 0.0; - for k in 0..coef.len() { - result += coef[k] * pow(voltage, k as i32); - } - return result; -} - -/// Floating point exponentiation function. -/// Cannot access std::f64::powi in no_std environment. -fn pow( - base: f64, - exp: i32, -) -> f64 { - if exp < 0 { - return 1.0 / pow(base, -exp); - } - - let mut result = 1.0; - for _ in 0..exp { - result *= base; - } - return result; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn voltage_to_celsius_converts_expected_ranges() { - let result: f64 = voltage_to_celsius(20.644 / 1000.0).unwrap(); - assert!(499.97 <= result && 500.0 >= result); - - let result: f64 = voltage_to_celsius(6.138 / 1000.0).unwrap(); - assert!(150.01 <= result && 150.03 >= result); - - let result: f64 = voltage_to_celsius(0.039 / 1000.0).unwrap(); - assert!(0.97 <= result && 0.98 >= result); - - let result: f64 = voltage_to_celsius(-0.778 / 1000.0).unwrap(); - assert!(-20.03 <= result && -20.01 >= result); - - let result: f64 = voltage_to_celsius(10.0 / 1000.0).unwrap(); - assert!(246.1 <= result && 246.3 >= result); - } - - #[test] - fn voltage_to_celsius_panics_on_temp_too_cold() { - assert!(voltage_to_celsius(-6.0).is_none()); - } - - #[test] - fn voltage_to_celsius_panics_on_temp_too_hot() { - assert!(voltage_to_celsius(-6.0).is_none()); - } -} diff --git a/common/thermocouple-converter/src/volts_to_celcius.rs b/common/thermocouple-converter/src/volts_to_celcius.rs deleted file mode 100644 index e5adac8..0000000 --- a/common/thermocouple-converter/src/volts_to_celcius.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Thermoelectric Voltages in mV for Type K Thermocouples. -/// Index 0 is -270C, index 1 is -269C, etc. -/// Contains voltages for -270C to 1372C. -const K: [f32; 1643] = [-6.458, -6.457, -6.456, -6.455, -6.453, -6.452, -6.450, -6.448, -6.446, -6.444, -6.441, -6.438, -6.435, -6.432, -6.429, -6.425, -6.421, -6.417, -6.413, -6.408, -6.404, -6.399, -6.393, -6.388, -6.382, -6.377, -6.370, -6.364, -6.358, -6.351, -6.344, -6.337, -6.329, -6.322, -6.314, -6.306, -6.297, -6.289, -6.280, -6.271, -6.262, -6.252, -6.243, -6.233, -6.223, -6.213, -6.202, -6.192, -6.181, -6.170, -6.158, -6.147, -6.135, -6.123, -6.111, -6.099, -6.087, -6.074, -6.061, -6.048, -6.035, -6.021, -6.007, -5.994, -5.980, -5.965, -5.951, -5.936, -5.922, -5.907, -5.891, -5.876, -5.861, -5.845, -5.829, -5.813, -5.797, -5.780, -5.763, -5.747, -5.730, -5.713, -5.695, -5.678, -5.660, -5.642, -5.624, -5.606, -5.588, -5.569, -5.550, -5.531, -5.512, -5.493, -5.474, -5.454, -5.435, -5.415, -5.395, -5.374, -5.354, -5.333, -5.313, -5.292, -5.271, -5.250, -5.228, -5.207, -5.185, -5.163, -5.141, -5.119, -5.097, -5.074, -5.052, -5.029, -5.006, -4.983, -4.960, -4.936, -4.913, -4.889, -4.865, -4.841, -4.817, -4.793, -4.768, -4.744, -4.719, -4.694, -4.669, -4.644, -4.618, -4.593, -4.567, -4.542, -4.516, -4.490, -4.463, -4.437, -4.411, -4.384, -4.357, -4.330, -4.303, -4.276, -4.249, -4.221, -4.194, -4.166, -4.138, -4.110, -4.082, -4.054, -4.025, -3.997, -3.968, -3.939, -3.911, -3.882, -3.852, -3.823, -3.794, -3.764, -3.734, -3.705, -3.675, -3.645, -3.614, -3.584, -3.554, -3.523, -3.492, -3.462, -3.431, -3.400, -3.368, -3.337, -3.306, -3.274, -3.243, -3.211, -3.179, -3.147, -3.115, -3.083, -3.050, -3.018, -2.986, -2.953, -2.920, -2.887, -2.854, -2.821, -2.788, -2.755, -2.721, -2.688, -2.654, -2.620, -2.587, -2.553, -2.519, -2.485, -2.450, -2.416, -2.382, -2.347, -2.312, -2.278, -2.243, -2.208, -2.173, -2.138, -2.103, -2.067, -2.032, -1.996, -1.961, -1.925, -1.889, -1.854, -1.818, -1.782, -1.745, -1.709, -1.673, -1.637, -1.600, -1.564, -1.527, -1.490, -1.453, -1.417, -1.380, -1.343, -1.305, -1.268, -1.231, -1.194, -1.156, -1.119, -1.081, -1.043, -1.006, -0.968, -0.930, -0.892, -0.854, -0.816, -0.778, -0.739, -0.701, -0.663, -0.624, -0.586, -0.547, -0.508, -0.470, -0.431, -0.392, -0.353, -0.314, -0.275, -0.236, -0.197, -0.157, -0.118, -0.079, -0.039, 0.000, 0.039, 0.079, 0.119, 0.158, 0.198, 0.238, 0.277, 0.317, 0.357, 0.397, 0.437, 0.477, 0.517, 0.557, 0.597, 0.637, 0.677, 0.718, 0.758, 0.798, 0.838, 0.879, 0.919, 0.960, 1.000, 1.041, 1.081, 1.122, 1.163, 1.203, 1.244, 1.285, 1.326, 1.366, 1.407, 1.448, 1.489, 1.530, 1.571, 1.612, 1.653, 1.694, 1.735, 1.776, 1.817, 1.858, 1.899, 1.941, 1.982, 2.023, 2.064, 2.106, 2.147, 2.188, 2.230, 2.271, 2.312, 2.354, 2.395, 2.436, 2.478, 2.519, 2.561, 2.602, 2.644, 2.685, 2.727, 2.768, 2.810, 2.851, 2.893, 2.934, 2.976, 3.017, 3.059, 3.100, 3.142, 3.184, 3.225, 3.267, 3.308, 3.350, 3.391, 3.433, 3.474, 3.516, 3.557, 3.599, 3.640, 3.682, 3.723, 3.765, 3.806, 3.848, 3.889, 3.931, 3.972, 4.013, 4.055, 4.096, 4.138, 4.179, 4.220, 4.262, 4.303, 4.344, 4.385, 4.427, 4.468, 4.509, 4.550, 4.591, 4.633, 4.674, 4.715, 4.756, 4.797, 4.838, 4.879, 4.920, 4.961, 5.002, 5.043, 5.084, 5.124, 5.165, 5.206, 5.247, 5.288, 5.328, 5.369, 5.410, 5.450, 5.491, 5.532, 5.572, 5.613, 5.653, 5.694, 5.735, 5.775, 5.815, 5.856, 5.896, 5.937, 5.977, 6.017, 6.058, 6.098, 6.138, 6.179, 6.219, 6.259, 6.299, 6.339, 6.380, 6.420, 6.460, 6.500, 6.540, 6.580, 6.620, 6.660, 6.701, 6.741, 6.781, 6.821, 6.861, 6.901, 6.941, 6.981, 7.021, 7.060, 7.100, 7.140, 7.180, 7.220, 7.260, 7.300, 7.340, 7.380, 7.420, 7.460, 7.500, 7.540, 7.579, 7.619, 7.659, 7.699, 7.739, 7.779, 7.819, 7.859, 7.899, 7.939, 7.979, 8.019, 8.059, 8.099, 8.138, 8.178, 8.218, 8.258, 8.298, 8.338, 8.378, 8.418, 8.458, 8.499, 8.539, 8.579, 8.619, 8.659, 8.699, 8.739, 8.779, 8.819, 8.860, 8.900, 8.940, 8.980, 9.020, 9.061, 9.101, 9.141, 9.181, 9.222, 9.262, 9.302, 9.343, 9.383, 9.423, 9.464, 9.504, 9.545, 9.585, 9.626, 9.666, 9.707, 9.747, 9.788, 9.828, 9.869, 9.909, 9.950, 9.991, 10.031, 10.072, 10.113, 10.153, 10.194, 10.235, 10.276, 10.316, 10.357, 10.398, 10.439, 10.480, 10.520, 10.561, 10.602, 10.643, 10.684, 10.725, 10.766, 10.807, 10.848, 10.889, 10.930, 10.971, 11.012, 11.053, 11.094, 11.135, 11.176, 11.217, 11.259, 11.300, 11.341, 11.382, 11.423, 11.465, 11.506, 11.547, 11.588, 11.630, 11.671, 11.712, 11.753, 11.795, 11.836, 11.877, 11.919, 11.960, 12.001, 12.043, 12.084, 12.126, 12.167, 12.209, 12.250, 12.291, 12.333, 12.374, 12.416, 12.457, 12.499, 12.540, 12.582, 12.624, 12.665, 12.707, 12.748, 12.790, 12.831, 12.873, 12.915, 12.956, 12.998, 13.040, 13.081, 13.123, 13.165, 13.206, 13.248, 13.290, 13.331, 13.373, 13.415, 13.457, 13.498, 13.540, 13.582, 13.624, 13.665, 13.707, 13.749, 13.791, 13.833, 13.874, 13.916, 13.958, 14.000, 14.042, 14.084, 14.126, 14.167, 14.209, 14.251, 14.293, 14.335, 14.377, 14.419, 14.461, 14.503, 14.545, 14.587, 14.629, 14.671, 14.713, 14.755, 14.797, 14.839, 14.881, 14.923, 14.965, 15.007, 15.049, 15.091, 15.133, 15.175, 15.217, 15.259, 15.301, 15.343, 15.385, 15.427, 15.469, 15.511, 15.554, 15.596, 15.638, 15.680, 15.722, 15.764, 15.806, 15.849, 15.891, 15.933, 15.975, 16.017, 16.059, 16.102, 16.144, 16.186, 16.228, 16.270, 16.313, 16.355, 16.397, 16.439, 16.482, 16.524, 16.566, 16.608, 16.651, 16.693, 16.735, 16.778, 16.820, 16.862, 16.904, 16.947, 16.989, 17.031, 17.074, 17.116, 17.158, 17.201, 17.243, 17.285, 17.328, 17.370, 17.413, 17.455, 17.497, 17.540, 17.582, 17.624, 17.667, 17.709, 17.752, 17.794, 17.837, 17.879, 17.921, 17.964, 18.006, 18.049, 18.091, 18.134, 18.176, 18.218, 18.261, 18.303, 18.346, 18.388, 18.431, 18.473, 18.516, 18.558, 18.601, 18.643, 18.686, 18.728, 18.771, 18.813, 18.856, 18.898, 18.941, 18.983, 19.026, 19.068, 19.111, 19.154, 19.196, 19.239, 19.281, 19.324, 19.366, 19.409, 19.451, 19.494, 19.537, 19.579, 19.622, 19.664, 19.707, 19.750, 19.792, 19.835, 19.877, 19.920, 19.962, 20.005, 20.048, 20.090, 20.133, 20.175, 20.218, 20.261, 20.303, 20.346, 20.389, 20.431, 20.474, 20.516, 20.559, 20.602, 20.644, 20.687, 20.730, 20.772, 20.815, 20.857, 20.900, 20.943, 20.985, 21.028, 21.071, 21.113, 21.156, 21.199, 21.241, 21.284, 21.326, 21.369, 21.412, 21.454, 21.497, 21.540, 21.582, 21.625, 21.668, 21.710, 21.753, 21.796, 21.838, 21.881, 21.924, 21.966, 22.009, 22.052, 22.094, 22.137, 22.179, 22.222, 22.265, 22.307, 22.350, 22.393, 22.435, 22.478, 22.521, 22.563, 22.606, 22.649, 22.691, 22.734, 22.776, 22.819, 22.862, 22.904, 22.947, 22.990, 23.032, 23.075, 23.117, 23.160, 23.203, 23.245, 23.288, 23.331, 23.373, 23.416, 23.458, 23.501, 23.544, 23.586, 23.629, 23.671, 23.714, 23.757, 23.799, 23.842, 23.884, 23.927, 23.970, 24.012, 24.055, 24.097, 24.140, 24.182, 24.225, 24.267, 24.310, 24.353, 24.395, 24.438, 24.480, 24.523, 24.565, 24.608, 24.650, 24.693, 24.735, 24.778, 24.820, 24.863, 24.905, 24.948, 24.990, 25.033, 25.075, 25.118, 25.160, 25.203, 25.245, 25.288, 25.330, 25.373, 25.415, 25.458, 25.500, 25.543, 25.585, 25.627, 25.670, 25.712, 25.755, 25.797, 25.840, 25.882, 25.924, 25.967, 26.009, 26.052, 26.094, 26.136, 26.179, 26.221, 26.263, 26.306, 26.348, 26.390, 26.433, 26.475, 26.517, 26.560, 26.602, 26.644, 26.687, 26.729, 26.771, 26.814, 26.856, 26.898, 26.940, 26.983, 27.025, 27.067, 27.109, 27.152, 27.194, 27.236, 27.278, 27.320, 27.363, 27.405, 27.447, 27.489, 27.531, 27.574, 27.616, 27.658, 27.700, 27.742, 27.784, 27.826, 27.869, 27.911, 27.953, 27.995, 28.037, 28.079, 28.121, 28.163, 28.205, 28.247, 28.289, 28.332, 28.374, 28.416, 28.458, 28.500, 28.542, 28.584, 28.626, 28.668, 28.710, 28.752, 28.794, 28.835, 28.877, 28.919, 28.961, 29.003, 29.045, 29.087, 29.129, 29.171, 29.213, 29.255, 29.297, 29.338, 29.380, 29.422, 29.464, 29.506, 29.548, 29.589, 29.631, 29.673, 29.715, 29.757, 29.798, 29.840, 29.882, 29.924, 29.965, 30.007, 30.049, 30.090, 30.132, 30.174, 30.216, 30.257, 30.299, 30.341, 30.382, 30.424, 30.466, 30.507, 30.549, 30.590, 30.632, 30.674, 30.715, 30.757, 30.798, 30.840, 30.881, 30.923, 30.964, 31.006, 31.047, 31.089, 31.130, 31.172, 31.213, 31.255, 31.296, 31.338, 31.379, 31.421, 31.462, 31.504, 31.545, 31.586, 31.628, 31.669, 31.710, 31.752, 31.793, 31.834, 31.876, 31.917, 31.958, 32.000, 32.041, 32.082, 32.124, 32.165, 32.206, 32.247, 32.289, 32.330, 32.371, 32.412, 32.453, 32.495, 32.536, 32.577, 32.618, 32.659, 32.700, 32.742, 32.783, 32.824, 32.865, 32.906, 32.947, 32.988, 33.029, 33.070, 33.111, 33.152, 33.193, 33.234, 33.275, 33.316, 33.357, 33.398, 33.439, 33.480, 33.521, 33.562, 33.603, 33.644, 33.685, 33.726, 33.767, 33.808, 33.848, 33.889, 33.930, 33.971, 34.012, 34.053, 34.093, 34.134, 34.175, 34.216, 34.257, 34.297, 34.338, 34.379, 34.420, 34.460, 34.501, 34.542, 34.582, 34.623, 34.664, 34.704, 34.745, 34.786, 34.826, 34.867, 34.908, 34.948, 34.989, 35.029, 35.070, 35.110, 35.151, 35.192, 35.232, 35.273, 35.313, 35.354, 35.394, 35.435, 35.475, 35.516, 35.556, 35.596, 35.637, 35.677, 35.718, 35.758, 35.798, 35.839, 35.879, 35.920, 35.960, 36.000, 36.041, 36.081, 36.121, 36.162, 36.202, 36.242, 36.282, 36.323, 36.363, 36.403, 36.443, 36.484, 36.524, 36.564, 36.604, 36.644, 36.685, 36.725, 36.765, 36.805, 36.845, 36.885, 36.925, 36.965, 37.006, 37.046, 37.086, 37.126, 37.166, 37.206, 37.246, 37.286, 37.326, 37.366, 37.406, 37.446, 37.486, 37.526, 37.566, 37.606, 37.646, 37.686, 37.725, 37.765, 37.805, 37.845, 37.885, 37.925, 37.965, 38.005, 38.044, 38.084, 38.124, 38.164, 38.204, 38.243, 38.283, 38.323, 38.363, 38.402, 38.442, 38.482, 38.522, 38.561, 38.601, 38.641, 38.680, 38.720, 38.760, 38.799, 38.839, 38.878, 38.918, 38.958, 38.997, 39.037, 39.076, 39.116, 39.155, 39.195, 39.235, 39.274, 39.314, 39.353, 39.393, 39.432, 39.471, 39.511, 39.550, 39.590, 39.629, 39.669, 39.708, 39.747, 39.787, 39.826, 39.866, 39.905, 39.944, 39.984, 40.023, 40.062, 40.101, 40.141, 40.180, 40.219, 40.259, 40.298, 40.337, 40.376, 40.415, 40.455, 40.494, 40.533, 40.572, 40.611, 40.651, 40.690, 40.729, 40.768, 40.807, 40.846, 40.885, 40.924, 40.963, 41.002, 41.042, 41.081, 41.120, 41.159, 41.198, 41.237, 41.276, 41.315, 41.354, 41.393, 41.431, 41.470, 41.509, 41.548, 41.587, 41.626, 41.665, 41.704, 41.743, 41.781, 41.820, 41.859, 41.898, 41.937, 41.976, 42.014, 42.053, 42.092, 42.131, 42.169, 42.208, 42.247, 42.286, 42.324, 42.363, 42.402, 42.440, 42.479, 42.518, 42.556, 42.595, 42.633, 42.672, 42.711, 42.749, 42.788, 42.826, 42.865, 42.903, 42.942, 42.980, 43.019, 43.057, 43.096, 43.134, 43.173, 43.211, 43.250, 43.288, 43.327, 43.365, 43.403, 43.442, 43.480, 43.518, 43.557, 43.595, 43.633, 43.672, 43.710, 43.748, 43.787, 43.825, 43.863, 43.901, 43.940, 43.978, 44.016, 44.054, 44.092, 44.130, 44.169, 44.207, 44.245, 44.283, 44.321, 44.359, 44.397, 44.435, 44.473, 44.512, 44.550, 44.588, 44.626, 44.664, 44.702, 44.740, 44.778, 44.816, 44.853, 44.891, 44.929, 44.967, 45.005, 45.043, 45.081, 45.119, 45.157, 45.194, 45.232, 45.270, 45.308, 45.346, 45.383, 45.421, 45.459, 45.497, 45.534, 45.572, 45.610, 45.647, 45.685, 45.723, 45.760, 45.798, 45.836, 45.873, 45.911, 45.948, 45.986, 46.024, 46.061, 46.099, 46.136, 46.174, 46.211, 46.249, 46.286, 46.324, 46.361, 46.398, 46.436, 46.473, 46.511, 46.548, 46.585, 46.623, 46.660, 46.697, 46.735, 46.772, 46.809, 46.847, 46.884, 46.921, 46.958, 46.995, 47.033, 47.070, 47.107, 47.144, 47.181, 47.218, 47.256, 47.293, 47.330, 47.367, 47.404, 47.441, 47.478, 47.515, 47.552, 47.589, 47.626, 47.663, 47.700, 47.737, 47.774, 47.811, 47.848, 47.884, 47.921, 47.958, 47.995, 48.032, 48.069, 48.105, 48.142, 48.179, 48.216, 48.252, 48.289, 48.326, 48.363, 48.399, 48.436, 48.473, 48.509, 48.546, 48.582, 48.619, 48.656, 48.692, 48.729, 48.765, 48.802, 48.838, 48.875, 48.911, 48.948, 48.984, 49.021, 49.057, 49.093, 49.130, 49.166, 49.202, 49.239, 49.275, 49.311, 49.348, 49.384, 49.420, 49.456, 49.493, 49.529, 49.565, 49.601, 49.637, 49.674, 49.710, 49.746, 49.782, 49.818, 49.854, 49.890, 49.926, 49.962, 49.998, 50.034, 50.070, 50.106, 50.142, 50.178, 50.214, 50.250, 50.286, 50.322, 50.358, 50.393, 50.429, 50.465, 50.501, 50.537, 50.572, 50.608, 50.644, 50.680, 50.715, 50.751, 50.787, 50.822, 50.858, 50.894, 50.929, 50.965, 51.000, 51.036, 51.071, 51.107, 51.142, 51.178, 51.213, 51.249, 51.284, 51.320, 51.355, 51.391, 51.426, 51.461, 51.497, 51.532, 51.567, 51.603, 51.638, 51.673, 51.708, 51.744, 51.779, 51.814, 51.849, 51.885, 51.920, 51.955, 51.990, 52.025, 52.060, 52.095, 52.130, 52.165, 52.200, 52.235, 52.270, 52.305, 52.340, 52.375, 52.410, 52.445, 52.480, 52.515, 52.550, 52.585, 52.620, 52.654, 52.689, 52.724, 52.759, 52.794, 52.828, 52.863, 52.898, 52.932, 52.967, 53.002, 53.037, 53.071, 53.106, 53.140, 53.175, 53.210, 53.244, 53.279, 53.313, 53.348, 53.382, 53.417, 53.451, 53.486, 53.520, 53.555, 53.589, 53.623, 53.658, 53.692, 53.727, 53.761, 53.795, 53.830, 53.864, 53.898, 53.932, 53.967, 54.001, 54.035, 54.069, 54.104, 54.138, 54.172, 54.206, 54.240, 54.274, 54.308, 54.343, 54.377, 54.411, 54.445, 54.479, 54.513, 54.547, 54.581, 54.615, 54.649, 54.683, 54.717, 54.751, 54.785, 54.819, 54.852, 54.886]; \ No newline at end of file