From 6fc828e8649432c16ab6272747a3b01bb88f9fdb Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 19 Jul 2023 18:09:13 +0000 Subject: [PATCH] Initial node implementation (#4) Reviewed-on: https://git.bfpower.io/BFPOWER/physical/pulls/4 Co-authored-by: Zack Co-committed-by: Zack --- .gitignore | 2 + Cargo.toml | 155 +++++++ README.md | 61 +-- commander/Cargo.toml | 13 + commander/src/lib.rs | 14 + examples/ads1256/.cargo/config.toml | 9 + examples/ads1256/Cargo.toml | 45 +++ examples/ads1256/build.rs | 5 + examples/ads1256/src/bin/multiplex.rs | 181 +++++++++ examples/ads1256/src/bin/poll.rs | 197 +++++++++ examples/ads1256/src/bin/thermocouple.rs | 126 ++++++ macros/node-poll-variants/Cargo.toml | 28 ++ macros/node-poll-variants/src/lib.rs | 294 ++++++++++++++ macros/node-poll-variants/tests/generate.rs | 31 ++ macros/node-poll-variants/tests/test_build.rs | 5 + node/Cargo.toml | 22 + node/src/lib.rs | 10 + node/src/transducer/input.rs | 1 + node/src/transducer/mod.rs | 8 + node/src/transducer/output.rs | 1 + node/src/transducer/sync/input.rs | 61 +++ node/src/transducer/sync/mod.rs | 58 +++ peripheral-components/ads1256/node/Cargo.toml | 31 ++ peripheral-components/ads1256/node/src/lib.rs | 4 + .../ads1256/node/src/standard/mod.rs | 2 + .../node/src/standard/multiplexer/mod.rs | 4 + .../node/src/standard/multiplexer/sync/mod.rs | 2 + .../src/standard/multiplexer/sync/poll.rs | 382 ++++++++++++++++++ rust-toolchain.toml | 14 + rustfmt.toml | 8 + src/cell.rs | 20 + src/error.rs | 41 ++ src/lib.rs | 8 + src/transducer/input.rs | 6 + src/transducer/mod.rs | 62 +++ src/transducer/output.rs | 8 + src/transducer/part/lm35.rs | 22 + src/transducer/part/mod.rs | 7 + src/transducer/part/thermocouple/mod.rs | 2 + src/transducer/part/thermocouple/type_k.rs | 173 ++++++++ 40 files changed, 2079 insertions(+), 44 deletions(-) create mode 100644 Cargo.toml create mode 100644 commander/Cargo.toml create mode 100644 commander/src/lib.rs create mode 100644 examples/ads1256/.cargo/config.toml create mode 100644 examples/ads1256/Cargo.toml create mode 100644 examples/ads1256/build.rs create mode 100644 examples/ads1256/src/bin/multiplex.rs create mode 100644 examples/ads1256/src/bin/poll.rs create mode 100644 examples/ads1256/src/bin/thermocouple.rs create mode 100644 macros/node-poll-variants/Cargo.toml create mode 100644 macros/node-poll-variants/src/lib.rs create mode 100644 macros/node-poll-variants/tests/generate.rs create mode 100644 macros/node-poll-variants/tests/test_build.rs create mode 100644 node/Cargo.toml create mode 100644 node/src/lib.rs create mode 100644 node/src/transducer/input.rs create mode 100644 node/src/transducer/mod.rs create mode 100644 node/src/transducer/output.rs create mode 100644 node/src/transducer/sync/input.rs create mode 100644 node/src/transducer/sync/mod.rs create mode 100644 peripheral-components/ads1256/node/Cargo.toml create mode 100644 peripheral-components/ads1256/node/src/lib.rs create mode 100644 peripheral-components/ads1256/node/src/standard/mod.rs create mode 100644 peripheral-components/ads1256/node/src/standard/multiplexer/mod.rs create mode 100644 peripheral-components/ads1256/node/src/standard/multiplexer/sync/mod.rs create mode 100644 peripheral-components/ads1256/node/src/standard/multiplexer/sync/poll.rs create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 src/cell.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/transducer/input.rs create mode 100644 src/transducer/mod.rs create mode 100644 src/transducer/output.rs create mode 100644 src/transducer/part/lm35.rs create mode 100644 src/transducer/part/mod.rs create mode 100644 src/transducer/part/thermocouple/mod.rs create mode 100644 src/transducer/part/thermocouple/type_k.rs diff --git a/.gitignore b/.gitignore index 8b6a8b5..6175e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ cmake-build-*/ # File-based project format *.iws + +*.stderr \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..87ff90b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,155 @@ +#--------------------------------------------------------------------------------------------------------------------- +#----- Workspace ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[workspace] +members = [ + # Device types + "node", + "commander", + # Peripherals + + # Peripheral components + "peripheral-components/ads1256/*", + # Macros + "macros/node-poll-variants", + # Examples + "examples/ads1256" +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +repository = "https://git.bfpower.io/BFPOWER/physical" +readme = "README.md" +license = "MIT" + +#----- no-std ---------------------------------- +# Math +[workspace.dependencies.libm] +version = "0.2.*" +# Units of measurement +[workspace.dependencies.uom] +version = "0.34.*" +default-features = false +features = ["f32", "si"] +# Logging +[workspace.dependencies.tracing] +version = "0.1.*" +[workspace.dependencies.defmt] +version = "0.3.*" +[workspace.dependencies.defmt-rtt] +version = "0.4.*" +# Serialization +[workspace.dependencies.parity-scale-codec] +version = "3.6.*" +default-features = false +# Embedded-HAL +[workspace.dependencies.embedded-hal] +version = "1.0.0-alpha.10" +[workspace.dependencies.embedded-hal-async] +version = "0.2.0-alpha.1" +# Memory +[workspace.dependencies.static_cell] +version = "1.1.*" +[workspace.dependencies.heapless] +version = "0.7.*" +# Other embedded utilities +[workspace.dependencies.cortex-m] +version = "0.7.*" +[workspace.dependencies.cortex-m-rt] +version = "0.7.*" +[workspace.dependencies.panic-probe] +version = "0.3.*" +features = ["print-defmt"] +# BFPOWER Drivers +[workspace.dependencies.ads1256-types] +git = "https://git.bfpower.io/BFPOWER/bfpower-drivers.git" +features = ["defmt"] +[workspace.dependencies.ads1256] +git = "https://git.bfpower.io/BFPOWER/bfpower-drivers.git" +features = ["uom"] +# Embassy +[workspace.dependencies.embassy-futures] +version = "0.1.*" +[workspace.dependencies.embassy-time] +version = "0.1.*" +features = ["defmt", "defmt-timestamp-uptime", "unstable-traits", "nightly", ] +[workspace.dependencies.embassy-sync] +version = "0.1.*" +features = ["defmt"] +[workspace.dependencies.embassy-embedded-hal] +version = "0.1.*" +features = ["nightly"] +[workspace.dependencies.embassy-executor] +version = "0.1.*" +features = ["defmt", "arch-cortex-m", "integrated-timers", "executor-interrupt", "executor-thread", "nightly"] +[workspace.dependencies.embassy-stm32] +version = "0.1.*" +features = ["defmt", "unstable-traits", "unstable-pac", "nightly"] +[workspace.dependencies.embassy-nrf] +version = "0.1.*" +features = ["defmt", "unstable-traits", "nightly"] +# Macros +[workspace.dependencies.syn] +version = "2.0.*" +features = ["extra-traits", "parsing"] +[workspace.dependencies.quote] +version = "1.0.*" +[workspace.dependencies.trybuild] +version = "1.0.*" + +#--------------------------------------------------------------------------------------------------------------------- +#----- Patch ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[patch.crates-io] +embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-net-driver = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-nrf = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } +embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "047ea9066f0d946fd4d706577b21df38fd3b1647" } + +#--------------------------------------------------------------------------------------------------------------------- +#----- Package ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[package] +name = "physical" +description = "Physical is a library for interacting with the physical world." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[features] +thermocouple_k = [] +lm35 = [] + +[dependencies] +uom = { workspace = true } +parity-scale-codec = { workspace = true } +libm = { workspace = true } + +#--------------------------------------------------------------------------------------------------------------------- +#----- Profiles ------------------------ +#--------------------------------------------------------------------------------------------------------------------- +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" + +[profile.dev] +opt-level = 3 +debug = true +debug-assertions = true +overflow-checks = true +lto = true +panic = "abort" +incremental = false +codegen-units = 1 \ No newline at end of file diff --git a/README.md b/README.md index 6d01fc3..f84712c 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,25 @@ # Physical -# Conceptual Design - -Physical is a library for interacting with the physical world from a computer. This can broadly be broken down into two categories: +Physical is a library for interacting with the physical world from a computer. This can broadly be broken down into two +categories: * Collecting and digitizing data from the physical world. * Controlling devices that take physical action. -As this is a broad purpose physical is intended to run on or interact with a wide range of computer hardware. This broad range of hardware can be categorized into two distinct "layers". +## Concepts -## Computer Hardware Layers +The main concepts of Physical are: -* Peripherals - A peripheral is a minimal set of hardware and firmware that directly interfaces with sensors or controllers (in fact it may be a single hardwired sensor or controller). - * Peripherals are always where analog to digital conversion takes place in the case of inputs and digital to analog conversion takes place in the case of outputs. - * Peripherals are things that run extremely minimal software, either with no dedicated operating system at all or a minimal RTOS such as [Embassy](https://embassy.dev/). - * Peripherals should do very little data processing, usually directly sending collected data to the host in the case of inputs or adjusting electronics components in response to output settings. Potentially a peripheral could have some very simple logic built in for things that need extremely fast response times such as closing a gate between two evacuated solar collectors when one loses vacuum. - * Physical is designed to work with peripherals running arbitrary firmware, i.e. peripherals do not need to be running any components from physical. However, it will eventually be possible for to make firmware for peripherals using firmware. - -* Hosts - A host is the computer peripherals are connected to. A host may be a fairly low power single board computer like a raspberry pi 0, all the way up to a 128 core supercomputer. - * Hosts are intended to run full fledged multi-process operating systems such as GNU/Linux. - * Hosts are intended to do the heavy lifting of data processing and decision making for the system being controlled. - -## Software Abstractions - -* Input - Individual unit for data collection (e.g. an analog input reads a voltage). -* Output - Individual unit for device control. -* Transformation - Some way of transforming data coming from inputs and going to outputs. E.g. transforming an analog input to a type K thermocouple. Transformations are for I/O where many different physical sensors or devices could be plugged into an input or output and need some algorithm applied to the direct data for it to be useful. E.g. analog input reads voltage whether a thermocouple or pressure sensor is connected to it. Transformation will transform the voltage to temperature or pressure. -* Peripheral - Software representation of what's described in "Computer Hardware Layers". - * Peripherals host inputs and outputs. -* Connection - Peripherals could be connected to the host in various ways (SPI, I²C, USB, etc.) different connection methods must be supported in physical. - -## Device Builder - -* In physical, all configuration must be done ahead of time. That is once the device build phase of the program is complete, there cannot be any change to the configuration. - * Peripherals cannot be added or removed. - * I/O settings cannot be changed. - * The only allowed "change" is to change the value of an output (like setting a digital output to high instead of low). - -## Roadmap - -- [ ] Minimal implementation of physical for hosts. Think about what components can be separated out and reused for peripherals. -- [ ] Add support for more peripherals (basically aiming to add support for what's needed for early BFPOWER systems). -- [ ] Common abstractions for system control (recording, PID control, etc.) -- [ ] Implementation of physical running directly on peripherals. -- [ ] Kotlin embedded DSL and general API to make simple programs to be run on a physical host (optional). -- [ ] GUI to be run on a physical host to do simple data collection, control, and analysis. - -## Questions - -* Nested hosts - Should there be abstractions built in to physical for setting one host to be the master of another. - * Leaning towards not having this. Would be better to have slightly more sophisticated peripherals running a RTOS then to have the added complexity of nested hosts. \ No newline at end of file +* Peripheral: A peripheral is a board that hosts physical I/O and usually does analog to digital conversion or + digital to analog conversion. A peripheral cannot function on its own, it must be connected to a node. This is more + narrow than the definition of a peripheral in embedded systems generally. Peripheral support is done on the basis + of complete boards, not individual components like an ADC. Abstractions for individual components should be made + separately, such as in BFPOWER drivers. +* Node: A node hosts peripherals. A node can have a commander but does not need one. A node can ignore or even override + commands from the commander. In a complex system, nodes are intended to be kept simple, less likely to + encounter an error than the commander, and in some cases should check for obvious problems in commands from the + commander. +* Commander: A commander hosts nodes. It is possible for a device to be both a node and a commander at the same time, + although it may not be the best idea to make such a setup. There is no concept of nesting commanders built into + Physical. If some kind of abstraction for a computer that commands multiple commanders, which command nodes, is + necessary, it should be made for that specific application. \ No newline at end of file diff --git a/commander/Cargo.toml b/commander/Cargo.toml new file mode 100644 index 0000000..70fdefb --- /dev/null +++ b/commander/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "physical-commander" +description = "A commander hosts nodes." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.physical] +path = ".." +[dependencies.tracing] +workspace = true \ No newline at end of file diff --git a/commander/src/lib.rs b/commander/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/commander/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/examples/ads1256/.cargo/config.toml b/examples/ads1256/.cargo/config.toml new file mode 100644 index 0000000..dbcc285 --- /dev/null +++ b/examples/ads1256/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +# replace STM32F429ZITx with your chip as listed in `probe-run --list-chips` +runner = "probe-rs-cli run --chip STM32F429ZITx" + +[build] +target = "thumbv7em-none-eabi" + +[env] +DEFMT_LOG = "trace" \ No newline at end of file diff --git a/examples/ads1256/Cargo.toml b/examples/ads1256/Cargo.toml new file mode 100644 index 0000000..b75e37d --- /dev/null +++ b/examples/ads1256/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "ads1256-examples" +description = "Examples using the ads1256." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.physical] +path = "../.." +features = ["thermocouple_k", "lm35"] +[dependencies.physical-node] +path = "../../node" +features = ["embassy-sync"] +[dependencies.physical-ads1256] +path = "../../peripheral-components/ads1256/node" +features = ["poll"] +[dependencies.ads1256] +workspace = true +[dependencies.uom] +workspace = true +[dependencies.defmt] +workspace = true +[dependencies.defmt-rtt] +workspace = true +[dependencies.cortex-m] +workspace = true +features = ["critical-section-single-core"] +[dependencies.cortex-m-rt] +workspace = true +[dependencies.embassy-stm32] +workspace = true +features = ["stm32f429zi", "memory-x", "time", "exti", "time-driver-any"] +[dependencies.embassy-executor] +workspace = true +[dependencies.embassy-futures] +workspace = true +[dependencies.embassy-time] +workspace = true +features = ["tick-hz-16_000_000"] +[dependencies.embassy-sync] +workspace = true +[dependencies.panic-probe] +workspace = true diff --git a/examples/ads1256/build.rs b/examples/ads1256/build.rs new file mode 100644 index 0000000..56127fd --- /dev/null +++ b/examples/ads1256/build.rs @@ -0,0 +1,5 @@ +fn main() { + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} \ No newline at end of file diff --git a/examples/ads1256/src/bin/multiplex.rs b/examples/ads1256/src/bin/multiplex.rs new file mode 100644 index 0000000..91f9bb9 --- /dev/null +++ b/examples/ads1256/src/bin/multiplex.rs @@ -0,0 +1,181 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait, async_fn_in_trait)] + +use core::cell::Cell; +use cortex_m::prelude::{ + _embedded_hal_blocking_delay_DelayMs, _embedded_hal_blocking_delay_DelayUs, +}; +use {defmt_rtt as _, panic_probe as _}; + +use {embassy_executor as executor, embassy_stm32 as stm32}; + +use ads1256::standard::input::SingleEnded; +use ads1256::{ + AdControl, Ads1256, AutoCal, BitOrder, BlockingDelay, Buffer, ClockOut, Config, DState, + DataRate, DigitalIo, DigitalIoDirection, DigitalIoState, DioDirection, Gain, Multiplexer, + MuxInput, OutputPin, Sdcs, SpiBus, Status, Wait, +}; +use embassy_time::{Delay, Duration, Timer}; +use executor::Spawner; +use stm32::dma::NoDma; +use stm32::exti::ExtiInput; +use stm32::gpio::{Input, Level, Output, Pull, Speed}; +use stm32::spi::Spi; +use stm32::time::Hertz; +use stm32::{pac, spi}; + +use uom::si::electric_potential::volt; +use uom::si::f32; + +use defmt::{debug, error, info, trace, unwrap}; +use embassy_executor::_export::StaticCell; +use embassy_stm32::peripherals::{EXTI6, PF6, PF7, SPI3}; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::pubsub::PubSubChannel; +use physical_node::transducer::{Publish, StatefulPublisher}; + +const AUTOCAL_CONF: Config = Config { + status: Status::setting(Buffer::Enabled, AutoCal::Enabled, BitOrder::MostSigFirst), + multiplexer: Multiplexer::setting(MuxInput::AIn0, MuxInput::Common), + ad_control: AdControl::setting(Gain::X2, Sdcs::Off, ClockOut::Off), + data_rate: DataRate::Sps2_5, + digital_io: DigitalIo::setting(DigitalIoState::default(), DigitalIoDirection::default()), +}; + +struct Ads1256Delay; + +impl ads1256::BlockingDelay for Ads1256Delay { + #[inline] + fn t6_delay(&mut self) { + Delay.delay_us(1u32); + } + + fn t11_1_delay(&mut self) {} + + fn t11_2_delay(&mut self) {} +} + +struct Inputs { + ai0: StatefulPublisher, + ai1: StatefulPublisher, + ai2: StatefulPublisher, +} + +// Inputs +static ANALOG_INPUTS: StaticCell = StaticCell::new(); +static ADS_1256: StaticCell, ExtiInput>> = StaticCell::new(); +static SPI: StaticCell> = StaticCell::new(); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + unsafe { + pac::FLASH.acr().modify(|v| { + v.set_prften(true); + v.set_icen(true); + v.set_dcen(true); + }); + } + + let p = embassy_stm32::init(Default::default()); + + let mut spi_conf = spi::Config::default(); + spi_conf.mode = spi::MODE_1; + spi_conf.bit_order = spi::BitOrder::MsbFirst; + + let ads1256_data_ready = ExtiInput::new(Input::new(p.PF6, Pull::Up), p.EXTI6); + let select_ads1256 = Output::new(p.PF7, Level::High, Speed::VeryHigh); + + let spi = SPI.init(Spi::new( + p.SPI3, + p.PC10, + p.PC12, + p.PC11, + NoDma, + NoDma, + Hertz(ads1256::defaults::SPI_CLK_HZ), + spi_conf, + )); + + let ads_1256 = ADS_1256.init(Ads1256::new(Ads1256Delay, select_ads1256, ads1256_data_ready)); + ads_1256.write_config(spi, AUTOCAL_CONF).unwrap(); + + let inputs = &*ANALOG_INPUTS.init(Inputs { + ai0: StatefulPublisher::new( + Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + PubSubChannel::new().into(), + ), + ai1: StatefulPublisher::new( + Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + PubSubChannel::new().into(), + ), + ai2: StatefulPublisher::new( + Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + PubSubChannel::new().into(), + ), + }); + + spawner.spawn(log_task(inputs)).unwrap(); + spawner + .spawn(drive_inputs_task(ads_1256, spi, inputs)) + .unwrap(); +} + +#[embassy_executor::task] +async fn drive_inputs_task( + ads_1256: &'static mut Ads1256, ExtiInput<'static, PF6>>, + spi: &'static mut Spi<'static, SPI3, NoDma, NoDma>, + inputs: &'static Inputs, +) { + let Inputs { ai0, ai1, ai2 } = inputs; + + loop { + let mut accumulator = f32::ElectricPotential::new::(0.0); + + let voltage = ads_1256 + .autocal_convert(spi, SingleEnded::AIn0.into(), None, None, None, false) + .await + .unwrap() + .to_voltage(AUTOCAL_CONF.ad_control.gain()); + ai0.update(voltage); + accumulator += voltage; + + let voltage = ads_1256 + .autocal_convert(spi, SingleEnded::AIn1.into(), None, None, None, false) + .await + .unwrap() + .to_voltage(AUTOCAL_CONF.ad_control.gain()); + ai1.update(voltage); + accumulator += voltage; + + let voltage = ads_1256 + .autocal_convert(spi, SingleEnded::AIn2.into(), None, None, None, false) + .await + .unwrap() + .to_voltage(AUTOCAL_CONF.ad_control.gain()); + ai2.update(voltage); + accumulator += voltage; + + let accum_volts = accumulator.get::(); + info!("Immediate loop iteration result, combined volts: {}", accum_volts); + } +} + +#[embassy_executor::task] +async fn log_task(inputs: &'static Inputs) { + let Inputs { ai0, ai1, ai2 } = inputs; + let mut ai0_sub = ai0.subscribe().unwrap(); + let mut ai1_sub = ai1.subscribe().unwrap(); + let mut ai2_sub = ai2.subscribe().unwrap(); + + loop { + let msg = ai0_sub.next_message_pure().await.get::(); + info!("Log task ai0: {}", msg); + + let msg = ai1_sub.next_message_pure().await.get::(); + info!("Log task ai1: {}", msg); + + let msg = ai2_sub.next_message_pure().await.get::(); + info!("Log task ai2: {}", msg); + } +} diff --git a/examples/ads1256/src/bin/poll.rs b/examples/ads1256/src/bin/poll.rs new file mode 100644 index 0000000..5c2cfd1 --- /dev/null +++ b/examples/ads1256/src/bin/poll.rs @@ -0,0 +1,197 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait, async_fn_in_trait)] + +use core::cell::Cell; +use core::ops::DerefMut; +use cortex_m::prelude::{ + _embedded_hal_blocking_delay_DelayMs, _embedded_hal_blocking_delay_DelayUs, +}; +use {defmt_rtt as _, panic_probe as _}; + +use {embassy_executor as executor, embassy_stm32 as stm32}; + +use ads1256::standard::input::SingleEnded; +use ads1256::{ + AdControl, Ads1256, AutoCal, BitOrder, BlockingDelay, Buffer, ClockOut, Config, DState, + DataRate, DigitalIo, DigitalIoDirection, DigitalIoState, DioDirection, Gain, Multiplexer, + MuxInput, OutputPin, Sdcs, SpiBus, Status, Wait, +}; +use embassy_time::{Delay, Duration, Timer}; +use executor::Spawner; +use stm32::dma::NoDma; +use stm32::exti::ExtiInput; +use stm32::gpio::{Input, Level, Output, Pull, Speed}; +use stm32::spi::Spi; +use stm32::time::Hertz; +use stm32::{pac, spi}; + +use uom::si::electric_potential::volt; +use uom::si::f32; + +use defmt::{debug, error, info, trace, unwrap}; +use embassy_executor::_export::StaticCell; +use embassy_stm32::peripherals::{EXTI6, PF6, PF7, SPI3}; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::mutex::Mutex; +use embassy_sync::pubsub::PubSubChannel; +use physical_ads1256::standard::multiplexer::poll::{ + AutocalPoll, AutocalPollStatePub, ModInputOnly, +}; +use physical_node::transducer::input::Poll; +use physical_node::transducer::{Publish, Publisher, StatefulPublisher}; + +const AUTOCAL_CONF: Config = Config { + status: Status::setting(Buffer::Enabled, AutoCal::Enabled, BitOrder::MostSigFirst), + multiplexer: Multiplexer::setting(MuxInput::AIn0, MuxInput::Common), + ad_control: AdControl::setting(Gain::X2, Sdcs::Off, ClockOut::Off), + data_rate: DataRate::Sps2_5, + digital_io: DigitalIo::setting(DigitalIoState::default(), DigitalIoDirection::default()), +}; + +struct Ads1256Delay; + +impl ads1256::BlockingDelay for Ads1256Delay { + #[inline] + fn t6_delay(&mut self) { + Delay.delay_us(1u32); + } + + fn t11_1_delay(&mut self) {} + + fn t11_2_delay(&mut self) {} +} + +type ExampleInput = AutocalPollStatePub< + 'static, + NoopRawMutex, + NoopRawMutex, + ModInputOnly, + Ads1256Delay, + Output<'static, PF7>, + ExtiInput<'static, PF6>, + Spi<'static, SPI3, NoDma, NoDma>, + 10, + 1, +>; + +struct Inputs { + ai1: ExampleInput, + ai2: ExampleInput, + ai3: ExampleInput, +} + +// Inputs +static ANALOG_INPUTS: StaticCell = StaticCell::new(); +static ADS_1256: StaticCell< + Mutex, ExtiInput>>, +> = StaticCell::new(); +static SPI: StaticCell>> = StaticCell::new(); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + unsafe { + pac::FLASH.acr().modify(|v| { + v.set_prften(true); + v.set_icen(true); + v.set_dcen(true); + }); + } + + let p = embassy_stm32::init(Default::default()); + + let mut spi_conf = spi::Config::default(); + spi_conf.mode = spi::MODE_1; + spi_conf.bit_order = spi::BitOrder::MsbFirst; + + let ads1256_data_ready = ExtiInput::new(Input::new(p.PF6, Pull::Up), p.EXTI6); + let select_ads1256 = Output::new(p.PF7, Level::High, Speed::VeryHigh); + + let spi = SPI.init(Mutex::new(Spi::new( + p.SPI3, + p.PC10, + p.PC12, + p.PC11, + NoDma, + NoDma, + Hertz(ads1256::defaults::SPI_CLK_HZ), + spi_conf, + ))); + + let ads_1256 = + ADS_1256.init(Mutex::new(Ads1256::new(Ads1256Delay, select_ads1256, ads1256_data_ready))); + + ads_1256 + .get_mut() + .write_config(spi.get_mut(), AUTOCAL_CONF) + .unwrap(); + + let inputs = &*ANALOG_INPUTS.init(Inputs { + ai1: AutocalPollStatePub { + poll: AutocalPoll::new( + ModInputOnly { + gain: Gain::X2, + multiplexer: SingleEnded::AIn1.into(), + }, + ads_1256, + spi, + ), + publisher: PubSubChannel::new().into(), + state: Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + }, + ai2: AutocalPollStatePub { + poll: AutocalPoll::new( + ModInputOnly { + gain: Gain::X2, + multiplexer: SingleEnded::AIn2.into(), + }, + ads_1256, + spi, + ), + publisher: PubSubChannel::new().into(), + state: Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + }, + ai3: AutocalPollStatePub { + poll: AutocalPoll::new( + ModInputOnly { + gain: Gain::X2, + multiplexer: SingleEnded::AIn3.into(), + }, + ads_1256, + spi, + ), + publisher: PubSubChannel::new().into(), + state: Cell::new(f32::ElectricPotential::new::(f32::NAN)).into(), + }, + }); + spawner.spawn(log_task(&inputs.ai1, 1)).unwrap(); + spawner.spawn(log_task(&inputs.ai2, 2)).unwrap(); + spawner.spawn(log_task(&inputs.ai3, 3)).unwrap(); + spawner + .spawn(poll_task(&inputs.ai1, 1, Duration::from_secs(5))) + .unwrap(); + spawner + .spawn(poll_task(&inputs.ai2, 2, Duration::from_secs(10))) + .unwrap(); + spawner + .spawn(poll_task(&inputs.ai3, 3, Duration::from_secs(20))) + .unwrap(); +} + +#[embassy_executor::task(pool_size = 3)] +async fn poll_task(input: &'static ExampleInput, input_num: u8, every: Duration) { + loop { + Timer::after(every).await; + let result = input.poll().await.unwrap().get::(); + info!("ai{} poll result: {} volts", input_num, result); + } +} + +#[embassy_executor::task(pool_size = 3)] +async fn log_task(input: &'static ExampleInput, input_num: u8) { + let mut subscriber = input.subscribe().unwrap(); + loop { + let msg = subscriber.next_message_pure().await.get::(); + info!("Log task ai{}: {}", input_num, msg); + } +} diff --git a/examples/ads1256/src/bin/thermocouple.rs b/examples/ads1256/src/bin/thermocouple.rs new file mode 100644 index 0000000..5fe194f --- /dev/null +++ b/examples/ads1256/src/bin/thermocouple.rs @@ -0,0 +1,126 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait, async_fn_in_trait)] + +use core::cell::Cell; +use cortex_m::prelude::{ + _embedded_hal_blocking_delay_DelayMs, _embedded_hal_blocking_delay_DelayUs, +}; +use {defmt_rtt as _, panic_probe as _}; + +use {embassy_executor as executor, embassy_stm32 as stm32}; + +use ads1256::standard::input::{Differential, SingleEnded}; +use ads1256::{ + AdControl, Ads1256, AutoCal, BitOrder, BlockingDelay, Buffer, ClockOut, Config, DState, + DataRate, DigitalIo, DigitalIoDirection, DigitalIoState, DioDirection, Gain, Multiplexer, + MuxInput, OutputPin, Sdcs, SpiBus, Status, Wait, +}; +use embassy_time::{Delay, Duration, Timer}; +use executor::Spawner; +use stm32::dma::NoDma; +use stm32::exti::ExtiInput; +use stm32::gpio::{Input, Level, Output, Pull, Speed}; +use stm32::spi::Spi; +use stm32::time::Hertz; +use stm32::{pac, spi}; + +use uom::si::electric_potential::{millivolt, volt}; +use uom::si::f32; + +use defmt::{debug, error, info, trace, unwrap}; +use physical::transducer::{lm35, thermocouple_k}; +use uom::si::thermodynamic_temperature::degree_celsius; + +const AUTOCAL_CONF: Config = Config { + status: Status::setting(Buffer::Enabled, AutoCal::Enabled, BitOrder::MostSigFirst), + multiplexer: Multiplexer::setting(MuxInput::AIn0, MuxInput::AIn1), + ad_control: AdControl::setting(Gain::X64, Sdcs::Off, ClockOut::Off), + data_rate: DataRate::Sps2_5, + digital_io: DigitalIo::setting(DigitalIoState::default(), DigitalIoDirection::default()), +}; + +struct Ads1256Delay; + +impl ads1256::BlockingDelay for Ads1256Delay { + #[inline] + fn t6_delay(&mut self) { + Delay.delay_us(1u32); + } + + fn t11_1_delay(&mut self) {} + + fn t11_2_delay(&mut self) {} +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + unsafe { + pac::FLASH.acr().modify(|v| { + v.set_prften(true); + v.set_icen(true); + v.set_dcen(true); + }); + } + + let p = embassy_stm32::init(Default::default()); + + let mut spi_conf = spi::Config::default(); + spi_conf.mode = spi::MODE_1; + spi_conf.bit_order = spi::BitOrder::MsbFirst; + + let ads1256_data_ready = ExtiInput::new(Input::new(p.PF6, Pull::Up), p.EXTI6); + let select_ads1256 = Output::new(p.PF7, Level::High, Speed::VeryHigh); + + let mut spi = Spi::new( + p.SPI3, + p.PC10, + p.PC12, + p.PC11, + NoDma, + NoDma, + Hertz(ads1256::defaults::SPI_CLK_HZ), + spi_conf, + ); + + let mut ads_1256 = Ads1256::new(Ads1256Delay, select_ads1256, ads1256_data_ready); + ads_1256.write_config(&mut spi, AUTOCAL_CONF).unwrap(); + ads_1256.self_calibrate(&mut spi).await.unwrap(); + + loop { + let gain = Gain::X4; + let reference = ads_1256 + .autocal_convert( + &mut spi, + SingleEnded::AIn2.into(), + None, + Some(AUTOCAL_CONF.ad_control.with_gain(gain)), + None, + false, + ) + .await + .unwrap() + .to_voltage(gain); + let reference = lm35::convert(reference).unwrap(); + let reference_celsius = reference.get::(); + info!("Reference junction temperature: {}°C", reference_celsius); + + let gain = Gain::X64; + let voltage = ads_1256 + .autocal_convert( + &mut spi, + Differential::AIn0.into(), + None, + Some(AUTOCAL_CONF.ad_control.with_gain(gain)), + None, + false, + ) + .await + .unwrap() + .to_voltage(AUTOCAL_CONF.ad_control.gain()); + let mv = voltage.get::(); + let temperature = thermocouple_k::convert_direct(voltage, reference).unwrap(); + let celsius = temperature.get::(); + info!("Thermocouple temperature: {}°C, millivolts: {}", celsius, mv); + } +} diff --git a/macros/node-poll-variants/Cargo.toml b/macros/node-poll-variants/Cargo.toml new file mode 100644 index 0000000..46ea342 --- /dev/null +++ b/macros/node-poll-variants/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "node-poll-variants" +description = "Macros for physical nodes." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/test_build.rs" + +[dependencies.syn] +workspace = true +[dependencies.quote] +workspace = true + +[dev-dependencies.trybuild] +workspace = true +[dev-dependencies.physical-node] +path = "../../node" +features = ["embassy-sync"] +[dev-dependencies.embassy-sync] +workspace = true \ No newline at end of file diff --git a/macros/node-poll-variants/src/lib.rs b/macros/node-poll-variants/src/lib.rs new file mode 100644 index 0000000..242c1cf --- /dev/null +++ b/macros/node-poll-variants/src/lib.rs @@ -0,0 +1,294 @@ +use proc_macro::TokenStream; +use quote::quote; +use std::ops::Deref; +use quote::__private::parse_spanned; +use syn::__private::{str, Span}; +use syn::punctuated::Punctuated; +use syn::token::{Colon, Comma, PathSep, Plus}; +use syn::{parse_macro_input, parse_quote, parse_quote_spanned, parse_str, Data, DeriveInput, Expr, GenericParam, Generics, Ident, Lit, LitStr, Meta, Path, PathSegment, Token, TraitBound, TraitBoundModifier, TypeParam, TypeParamBound, Type}; + +#[proc_macro_derive(PollVariants, attributes(value_type, error_type))] +pub fn poll_variant_macro(input: TokenStream) -> TokenStream { + // ----- Parse input ---------------------------------- + let input = parse_macro_input!(input as DeriveInput); + let attrs = &input.attrs; + let vis = &input.vis; + let ident = &input.ident; + let data = &input.data; + let og_generics = &input.generics; + let (og_impl_generics, og_type_generics, og_where_clause) = &og_generics.split_for_impl(); + + // ----- Check that item the macro was used on is a struct ---------------------------------- + match data { + Data::Struct(struct_data) => struct_data, + Data::Enum(_) => panic!("Stateful struct cannot be derived from an enum."), + Data::Union(_) => panic!("Stateful struct cannot be derived from a union."), + }; + + // ----- Extract attribute information ---------------------------------- + const VALUE_T_NAME: &str = "value_type"; + const ERROR_T_NAME: &str = "error_type"; + let mut value_type: Option = None; + let mut error_type: Option = None; + for attribute in attrs.iter() { + // if the attribute is a named value + if let Meta::NameValue(meta) = &attribute.meta { + // if the name of the attribute is value_type + if meta.path.segments[0].ident == VALUE_T_NAME { + // if the value of the attribute is a literal + if let Expr::Lit(lit) = &meta.value { + // if the literal is a string + if let Lit::Str(lit) = &lit.lit { + let span = lit.span(); + let string = lit.token().to_string(); + let string = string.trim_matches('"').to_string(); + let _value_type: Type = parse_str(string.deref()).unwrap(); + let _value_type: Type = parse_quote_spanned!(span=> #_value_type); + + value_type = Some(_value_type); + + } else { + panic!("{VALUE_T_NAME} must be set with a string literal.") + } + } else { + panic!("{VALUE_T_NAME} must be set with a literal.") + } + } + + // if the name of the attribute is error_type + if meta.path.segments[0].ident == ERROR_T_NAME { + // if the value of the attribute is a literal + if let Expr::Lit(lit) = &meta.value { + // if the literal is a string + if let Lit::Str(lit) = &lit.lit { + let span = lit.span(); + let string = lit.token().to_string(); + let string = string.trim_matches('"').to_string(); + let _error_type: Type = parse_str(string.deref()).unwrap(); + let _error_type: Type = parse_quote_spanned!(span=> #_error_type); + + error_type = Some(_error_type); + } else { + panic!("{ERROR_T_NAME} must be set with a string literal.") + } + } else { + panic!("{ERROR_T_NAME} must be set with a literal.") + } + } + } + } + let value_type = value_type + .expect(format!("Need attribute {VALUE_T_NAME}: #[{VALUE_T_NAME} = \"type\"]").deref()); + let error_type = error_type + .expect(format!("Need attribute {ERROR_T_NAME}: #[{ERROR_T_NAME} = \"type\"]").deref()); + + // ----- Add publisher generics ---------------------------------- + // MutexT + const MUTEX_T_NAME: &str = "PublishMutexT"; + let mutex_t_ident = Ident::new(MUTEX_T_NAME, Span::call_site()); + const CAPACITY_NAME: &str = "CAPACITY"; + let capacity_ident = Ident::new(CAPACITY_NAME, Span::call_site()); + const NUM_SUBS_NAME: &str = "NUM_SUBS"; + let num_subs_ident = Ident::new(NUM_SUBS_NAME, Span::call_site()); + let mut num_lifetimes: usize = 0; + let mut num_types: usize = 0; + let mut num_const: usize = 0; + let mut has_mutex_t = false; + for param in og_generics.params.iter() { + match param { + GenericParam::Lifetime(_) => num_lifetimes += 1, + GenericParam::Type(param) => { + num_types += 1; + + // If the generic parameter is MutexT + if param.ident == MUTEX_T_NAME { + has_mutex_t = true; + } + }, + GenericParam::Const(_) => num_const += 1, + } + } + + let mut publish_generics = og_generics.clone(); + // If MutexT is not a generic parameter, add it + if !has_mutex_t { + let mutex_t_param: GenericParam = + parse_quote!(#mutex_t_ident: embassy_sync::blocking_mutex::raw::RawMutex); + + let num_generics = num_lifetimes + num_types + num_const; + // If there are generics + if num_generics > 0 { + // If all generics are lifetimes + if num_lifetimes == num_generics { + // Add MutexT after the lifetimes + publish_generics.params.push(mutex_t_param); + // If no generics are lifetimes + } else if num_lifetimes == 0 { + // Insert MutexT at the front + publish_generics.params.insert(0, mutex_t_param); + // If there are lifetimes and other generics + } else { + // Insert MutexT after the lifetimes + publish_generics.params.insert(num_lifetimes, mutex_t_param); + } + // If there are no generics + } else { + // Add MutexT + publish_generics.params.push(mutex_t_param); + } + } + // const generics + let capacity_param: GenericParam = parse_quote!(const #capacity_ident: usize); + let num_subs_param: GenericParam = parse_quote!(const #num_subs_ident: usize); + publish_generics.params.push(capacity_param); + publish_generics.params.push(num_subs_param); + + let (publ_impl_generics, publ_type_generics, publ_where_clause) = + &publish_generics.split_for_impl(); + + let pubsub_error_path: Path = parse_quote!(embassy_sync::pubsub::Error); + let pubsub_sub_path: Path = parse_quote!(embassy_sync::pubsub::Subscriber); + + let stateful_variant_ident = Ident::new(format!("{ident}Stateful").deref(), ident.span()); + let publish_variant_ident = Ident::new(format!("{ident}Publish").deref(), ident.span()); + let state_pub_variant_ident = Ident::new(format!("{ident}StatePub").deref(), ident.span()); + + let poll_path: Path = parse_quote!(physical_node::transducer::input::Poll); + let stateful_path: Path = parse_quote!(physical_node::transducer::Stateful); + let publish_path: Path = parse_quote!(physical_node::transducer::Publish); + + let state_path: Path = parse_quote!(physical_node::transducer::State); + let publisher_path: Path = parse_quote!(physical_node::transducer::Publisher); + let cellview_path: Path = parse_quote!(physical_node::cell::CellView); + + let error_path: Path = parse_quote!(physical_node::CriticalError); + + let expanded = quote! { + // ----- Stateful struct ---------------------------------- + #vis struct #stateful_variant_ident #og_generics #og_where_clause { + pub poll: #ident #og_type_generics, + pub state: #state_path<#value_type>, + } + + // ----- Stateful impls ---------------------------------- + impl #og_impl_generics #poll_path for #stateful_variant_ident #og_type_generics #og_where_clause { + type Value = #value_type; + type Error = #error_type; + + #[inline] + async fn poll(&self) -> Result { + let result = self.poll.poll().await; + if let Ok(value) = result { + self.state.update(value); + } + result + } + } + + impl #og_impl_generics #stateful_path for #stateful_variant_ident #og_type_generics #og_where_clause { + type Value = #value_type; + + #[inline(always)] + fn state_cell(&self) -> #cellview_path { + self.state.state_cell() + } + + #[inline(always)] + fn state(&self) -> Self::Value { + self.state.state() + } + } + + // ----- Publish struct ---------------------------------- + #[cfg(feature = "embassy-sync")] + #vis struct #publish_variant_ident #publish_generics #publ_where_clause { + pub poll: #ident #og_type_generics, + pub publisher: #publisher_path<#value_type, #mutex_t_ident, #capacity_ident, #num_subs_ident>, + } + + // ----- Publish impl ---------------------------------- + #[cfg(feature = "embassy-sync")] + impl #publ_impl_generics #poll_path for #publish_variant_ident #publ_type_generics #publ_where_clause { + type Value = #value_type; + type Error = #error_type; + + #[inline] + async fn poll(&self) -> Result { + let result = self.poll.poll().await; + if let Ok(value) = result { + self.publisher.update(value); + } + result + } + } + + #[cfg(feature = "embassy-sync")] + impl #publ_impl_generics #publish_path<#capacity_ident, #num_subs_ident> for #publish_variant_ident #publ_type_generics #publ_where_clause { + type Value = #value_type; + type Mutex = #mutex_t_ident; + + #[inline(always)] + fn subscribe( + &self, + ) -> Result<#pubsub_sub_path, #pubsub_error_path> { + self.publisher.subscribe() + } + } + + // ----- StatePub struct ---------------------------------- + #[cfg(feature = "embassy-sync")] + #vis struct #state_pub_variant_ident #publish_generics #publ_where_clause { + pub poll: #ident #og_type_generics, + pub state: #state_path<#value_type>, + pub publisher: #publisher_path<#value_type, #mutex_t_ident, #capacity_ident, #num_subs_ident>, + } + + #[cfg(feature = "embassy-sync")] + impl #publ_impl_generics #poll_path for #state_pub_variant_ident #publ_type_generics #publ_where_clause { + type Value = #value_type; + type Error = #error_type; + + #[inline] + async fn poll(&self) -> Result { + let result = self.poll.poll().await; + if let Ok(value) = result { + self.state.update(value); + self.publisher.update(value); + } + result + } + } + + #[cfg(feature = "embassy-sync")] + impl #publ_impl_generics #stateful_path for #state_pub_variant_ident #publ_type_generics #publ_where_clause { + type Value = #value_type; + + #[inline(always)] + fn state_cell(&self) -> #cellview_path { + self.state.state_cell() + } + + #[inline(always)] + fn state(&self) -> Self::Value { + self.state.state() + } + } + + #[cfg(feature = "embassy-sync")] + impl #publ_impl_generics #publish_path<#capacity_ident, #num_subs_ident> for #state_pub_variant_ident #publ_type_generics #publ_where_clause { + type Value = #value_type; + type Mutex = #mutex_t_ident; + + #[inline(always)] + fn subscribe( + &self, + ) -> Result<#pubsub_sub_path, #pubsub_error_path> { + self.publisher.subscribe() + } + } + + + }; + + TokenStream::from(expanded) +} diff --git a/macros/node-poll-variants/tests/generate.rs b/macros/node-poll-variants/tests/generate.rs new file mode 100644 index 0000000..98df1d5 --- /dev/null +++ b/macros/node-poll-variants/tests/generate.rs @@ -0,0 +1,31 @@ +#![feature(async_fn_in_trait, impl_trait_projections, never_type)] + +use node_poll_variants::PollVariants; +use physical_node::transducer::input::Poll; + +#[derive(PollVariants)] +#[value_type = "SecondT"] +#[error_type = "!"] +struct ExamplePoll<'a, FirstT, SecondT> +where + SecondT: Copy, +{ + a: &'a i32, + b: i32, + first: FirstT, + second: SecondT, +} + +impl<'a, FirstT, SecondT> Poll for ExamplePoll<'a, FirstT, SecondT> +where + SecondT: Copy, +{ + type Value = SecondT; + type Error = !; + + async fn poll(&self) -> Result { + Ok(self.second) + } +} + +fn main() {} diff --git a/macros/node-poll-variants/tests/test_build.rs b/macros/node-poll-variants/tests/test_build.rs new file mode 100644 index 0000000..6661c9d --- /dev/null +++ b/macros/node-poll-variants/tests/test_build.rs @@ -0,0 +1,5 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/generate.rs"); +} \ No newline at end of file diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..644cde8 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "physical-node" +description = "A node hosts peripherals." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies.physical] +path = ".." +[dependencies.embedded-hal] +workspace = true +[dependencies.embedded-hal-async] +workspace = true +[dependencies.defmt] +workspace = true +[dependencies.uom] +workspace = true +[dependencies.embassy-sync] +workspace = true +optional = true \ No newline at end of file diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 0000000..ff5daeb --- /dev/null +++ b/node/src/lib.rs @@ -0,0 +1,10 @@ +#![no_std] +#![feature(async_fn_in_trait)] + +pub mod transducer; + +pub mod cell { + pub use physical::cell::*; +} + +pub use physical::CriticalError; diff --git a/node/src/transducer/input.rs b/node/src/transducer/input.rs new file mode 100644 index 0000000..84eb5d9 --- /dev/null +++ b/node/src/transducer/input.rs @@ -0,0 +1 @@ +pub use physical::transducer::input::*; diff --git a/node/src/transducer/mod.rs b/node/src/transducer/mod.rs new file mode 100644 index 0000000..e8e7c3d --- /dev/null +++ b/node/src/transducer/mod.rs @@ -0,0 +1,8 @@ +pub mod input; +pub mod output; +#[cfg(feature = "embassy-sync")] +mod sync; + +pub use physical::transducer::*; +#[cfg(feature = "embassy-sync")] +pub use sync::*; diff --git a/node/src/transducer/output.rs b/node/src/transducer/output.rs new file mode 100644 index 0000000..c441daa --- /dev/null +++ b/node/src/transducer/output.rs @@ -0,0 +1 @@ +pub use physical::transducer::output::*; \ No newline at end of file diff --git a/node/src/transducer/sync/input.rs b/node/src/transducer/sync/input.rs new file mode 100644 index 0000000..e66a306 --- /dev/null +++ b/node/src/transducer/sync/input.rs @@ -0,0 +1,61 @@ +use crate::cell::CellView; +use crate::transducer::{Publish, Publisher}; +use core::cell::Cell; +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::pubsub::{Error, PubSubChannel, Subscriber}; +use physical::transducer::{State, Stateful}; + +pub struct StatefulPublisher< + T: Copy, + MutexT: RawMutex, + const CAPACITY: usize, + const NUM_SUBS: usize, +> { + pub state: State, + pub publisher: Publisher, +} + +impl + StatefulPublisher +{ + #[inline(always)] + pub fn new(state: State, publisher: Publisher) -> Self { + Self { state, publisher } + } + + #[inline(always)] + pub fn update(&self, value: T) { + self.state.update(value); + self.publisher.update(value); + } +} + +impl Stateful + for StatefulPublisher +{ + type Value = T; + + #[inline(always)] + fn state_cell(&self) -> CellView { + self.state.state_cell() + } + + #[inline(always)] + fn state(&self) -> Self::Value { + self.state.state() + } +} + +impl + Publish for StatefulPublisher +{ + type Value = T; + type Mutex = MutexT; + + #[inline(always)] + fn subscribe( + &self, + ) -> Result, Error> { + self.publisher.subscribe() + } +} diff --git a/node/src/transducer/sync/mod.rs b/node/src/transducer/sync/mod.rs new file mode 100644 index 0000000..0ac16ba --- /dev/null +++ b/node/src/transducer/sync/mod.rs @@ -0,0 +1,58 @@ +mod input; + +pub use input::*; + +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::pubsub; +use embassy_sync::pubsub::{PubSubBehavior, PubSubChannel, Subscriber}; + +pub trait Publish { + type Value: Copy; + type Mutex: RawMutex; + + fn subscribe( + &self, + ) -> Result, pubsub::Error>; +} + +pub struct Publisher { + channel: PubSubChannel, +} + +impl +Publisher +{ + #[inline(always)] + pub fn new(channel: PubSubChannel) -> Self { + Self { channel } + } + + #[inline(always)] + pub fn update(&self, value: T) { + self.channel.publish_immediate(value); + } +} + +impl +Publish for Publisher +{ + type Value = T; + type Mutex = MutexT; + + #[inline(always)] + fn subscribe( + &self, + ) -> Result, pubsub::Error> { + self.channel.subscriber() + } +} + +impl +From> +for Publisher +{ + #[inline(always)] + fn from(channel: PubSubChannel) -> Self { + Publisher::new(channel) + } +} \ No newline at end of file diff --git a/peripheral-components/ads1256/node/Cargo.toml b/peripheral-components/ads1256/node/Cargo.toml new file mode 100644 index 0000000..5b64fff --- /dev/null +++ b/peripheral-components/ads1256/node/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "physical-ads1256" +description = "Shared node abstractions for ADS1256 components." +version.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true + +[features] +embassy-sync = ["dep:embassy-sync", "ads1256/embassy-sync", "physical-node/embassy-sync"] +poll = ["standard-multiplexer", "embassy-sync"] +standard-multiplexer = [] + +[dependencies.physical-node] +path = "../../../node" +[dependencies.node-poll-variants] +path = "../../../macros/node-poll-variants" +[dependencies.ads1256] +workspace = true +[dependencies.embedded-hal] +workspace = true +[dependencies.embedded-hal-async] +workspace = true +[dependencies.defmt] +workspace = true +[dependencies.uom] +workspace = true +[dependencies.embassy-sync] +workspace = true +optional = true \ No newline at end of file diff --git a/peripheral-components/ads1256/node/src/lib.rs b/peripheral-components/ads1256/node/src/lib.rs new file mode 100644 index 0000000..ed52f0c --- /dev/null +++ b/peripheral-components/ads1256/node/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] +#![feature(async_fn_in_trait, impl_trait_projections)] + +pub mod standard; \ No newline at end of file diff --git a/peripheral-components/ads1256/node/src/standard/mod.rs b/peripheral-components/ads1256/node/src/standard/mod.rs new file mode 100644 index 0000000..490e4c6 --- /dev/null +++ b/peripheral-components/ads1256/node/src/standard/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "standard-multiplexer")] +pub mod multiplexer; \ No newline at end of file diff --git a/peripheral-components/ads1256/node/src/standard/multiplexer/mod.rs b/peripheral-components/ads1256/node/src/standard/multiplexer/mod.rs new file mode 100644 index 0000000..2c842b4 --- /dev/null +++ b/peripheral-components/ads1256/node/src/standard/multiplexer/mod.rs @@ -0,0 +1,4 @@ +mod sync; + +#[cfg(feature = "embassy-sync")] +pub use sync::*; \ No newline at end of file diff --git a/peripheral-components/ads1256/node/src/standard/multiplexer/sync/mod.rs b/peripheral-components/ads1256/node/src/standard/multiplexer/sync/mod.rs new file mode 100644 index 0000000..5e18f2b --- /dev/null +++ b/peripheral-components/ads1256/node/src/standard/multiplexer/sync/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "poll")] +pub mod poll; \ No newline at end of file diff --git a/peripheral-components/ads1256/node/src/standard/multiplexer/sync/poll.rs b/peripheral-components/ads1256/node/src/standard/multiplexer/sync/poll.rs new file mode 100644 index 0000000..d180353 --- /dev/null +++ b/peripheral-components/ads1256/node/src/standard/multiplexer/sync/poll.rs @@ -0,0 +1,382 @@ +use ads1256::{ + AdControl, Ads1256, BlockingDelay, DataRate, Gain, Multiplexer, OutputPin, SpiBus, Status, Wait, +}; +use core::ops::DerefMut; +use embassy_sync::blocking_mutex::raw::RawMutex; +use embassy_sync::mutex::Mutex; +use node_poll_variants::PollVariants; +use physical_node::transducer::input::Poll; +use physical_node::CriticalError; +use uom::si::f32; + +#[derive(PollVariants)] +#[value_type = "f32::ElectricPotential"] +#[error_type = "CriticalError"] +pub struct AutocalPoll<'a, DeviceMutexT, ModInT, DelayerT, SST, DrdyT, SpiT> +where + DeviceMutexT: RawMutex, + ModInT: ModInput, + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, + SpiT: SpiBus, +{ + input_mod: ModInT, + ads1256: &'a Mutex>, + spi: &'a Mutex, +} + +impl<'a, DeviceMutexT, ModInT, DelayerT, SST, DrdyT, SpiT> + AutocalPoll<'a, DeviceMutexT, ModInT, DelayerT, SST, DrdyT, SpiT> +where + DeviceMutexT: RawMutex, + ModInT: ModInput, + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, + SpiT: SpiBus, +{ + #[inline(always)] + pub fn new( + input_mod: ModInT, + ads1256: &'a Mutex>, + spi: &'a Mutex, + ) -> Self { + Self { + input_mod, + ads1256, + spi, + } + } +} + +impl<'a, DeviceMutexT, ModInT, DelayerT, SST, DrdyT, SpiT> Poll + for AutocalPoll<'a, DeviceMutexT, ModInT, DelayerT, SST, DrdyT, SpiT> +where + DeviceMutexT: RawMutex, + ModInT: ModInput, + DelayerT: BlockingDelay, + SST: OutputPin, + DrdyT: Wait, + SpiT: SpiBus, +{ + type Value = f32::ElectricPotential; + type Error = CriticalError; + + async fn poll(&self) -> Result { + let mut ads1256_guard = self.ads1256.lock().await; + let ads1256 = ads1256_guard.deref_mut(); + //TODO: ADS1256 documentation seems to say we should be waiting for drdy low after + // issuing standby command but it never goes low. + + let result = ads1256 + .autocal_convert_m( + self.spi, + self.input_mod.multiplexer(), + self.input_mod.status(), + self.input_mod.ad_control(), + self.input_mod.data_rate(), + true, + ) + .await; + + match result { + Ok(conversion) => Ok(conversion.to_voltage(self.input_mod.gain())), + Err(_) => Err(CriticalError::Communication), + } + } +} + +pub trait ModInput: Copy { + fn multiplexer(self) -> Multiplexer; + + fn gain(self) -> Gain; + + fn status(self) -> Option; + + fn ad_control(self) -> Option; + + fn data_rate(self) -> Option; +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInputOnly { + pub multiplexer: Multiplexer, + /// Only used for converting to voltage, this value must match what is set on the ADS1256, it + /// will not *be* set unlike the other fields. + pub gain: Gain, +} + +impl ModInput for ModInputOnly { + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + fn gain(self) -> Gain { + self.gain + } + + fn status(self) -> Option { + None + } + + fn ad_control(self) -> Option { + None + } + + fn data_rate(self) -> Option { + None + } +} + +/// buffer +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInStatus { + pub multiplexer: Multiplexer, + /// Only used for converting to voltage, this value must match what is set on the ADS1256, it + /// will not *be* set unlike the other fields. + pub gain: Gain, + pub status: Status, +} + +impl ModInput for ModInStatus { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.gain + } + + #[inline(always)] + fn status(self) -> Option { + Some(self.status) + } + + #[inline(always)] + fn ad_control(self) -> Option { + None + } + + #[inline(always)] + fn data_rate(self) -> Option { + None + } +} + +/// gain +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInAdControl { + pub multiplexer: Multiplexer, + pub ad_control: AdControl, +} + +impl ModInput for ModInAdControl { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.ad_control.gain() + } + + #[inline(always)] + fn status(self) -> Option { + None + } + + #[inline(always)] + fn ad_control(self) -> Option { + Some(self.ad_control) + } + + #[inline(always)] + fn data_rate(self) -> Option { + None + } +} + +/// data rate +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInDataRate { + pub multiplexer: Multiplexer, + /// Only used for converting to voltage, this value must match what is set on the ADS1256, it + /// will not *be* set unlike the other fields. + pub gain: Gain, + pub data_rate: DataRate, +} + +impl ModInput for ModInDataRate { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.gain + } + + #[inline(always)] + fn status(self) -> Option { + None + } + + #[inline(always)] + fn ad_control(self) -> Option { + None + } + + #[inline(always)] + fn data_rate(self) -> Option { + Some(self.data_rate) + } +} + +/// buffer, gain +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInStatAdc { + pub multiplexer: Multiplexer, + pub status: Status, + pub ad_control: AdControl, +} + +impl ModInput for ModInStatAdc { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.ad_control.gain() + } + + #[inline(always)] + fn status(self) -> Option { + Some(self.status) + } + + #[inline(always)] + fn ad_control(self) -> Option { + Some(self.ad_control) + } + + #[inline(always)] + fn data_rate(self) -> Option { + None + } +} + +/// buffer, data rate +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInStatDrate { + pub multiplexer: Multiplexer, + /// Only used for converting to voltage, this value must match what is set on the ADS1256, it + /// will not *be* set unlike the other fields. + pub gain: Gain, + pub status: Status, + pub data_rate: DataRate, +} + +impl ModInput for ModInStatDrate { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.gain + } + + #[inline(always)] + fn status(self) -> Option { + Some(self.status) + } + + #[inline(always)] + fn ad_control(self) -> Option { + None + } + + #[inline(always)] + fn data_rate(self) -> Option { + Some(self.data_rate) + } +} + +// gain, data rate +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInAdcDrate { + pub multiplexer: Multiplexer, + pub ad_control: AdControl, + pub data_rate: DataRate, +} + +impl ModInput for ModInAdcDrate { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.ad_control.gain() + } + + #[inline(always)] + fn status(self) -> Option { + None + } + + #[inline(always)] + fn ad_control(self) -> Option { + Some(self.ad_control) + } + + #[inline(always)] + fn data_rate(self) -> Option { + Some(self.data_rate) + } +} + +/// buffer, gain, data rate +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ModInAll { + pub multiplexer: Multiplexer, + pub status: Status, + pub ad_control: AdControl, + pub data_rate: DataRate, +} + +impl ModInput for ModInAll { + #[inline(always)] + fn multiplexer(self) -> Multiplexer { + self.multiplexer + } + + #[inline(always)] + fn gain(self) -> Gain { + self.ad_control.gain() + } + + #[inline(always)] + fn status(self) -> Option { + Some(self.status) + } + + #[inline(always)] + fn ad_control(self) -> Option { + Some(self.ad_control) + } + + #[inline(always)] + fn data_rate(self) -> Option { + Some(self.data_rate) + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..2301ddc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,14 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "nightly-2023-04-18" +components = [ "rust-src", "rustfmt", "llvm-tools-preview" ] +targets = [ + "thumbv7em-none-eabi", + "thumbv7m-none-eabi", + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf", + "thumbv8m.main-none-eabihf", + "riscv32imac-unknown-none-elf", + "wasm32-unknown-unknown", +] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1a2c99f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +imports_granularity = "Module" +format_strings = true +wrap_comments = true +match_block_trailing_comma = true +enum_discrim_align_threshold = 25 +fn_call_width = 100 +comment_width = 120 +single_line_if_else_max_width = 100 \ No newline at end of file diff --git a/src/cell.rs b/src/cell.rs new file mode 100644 index 0000000..16ee00b --- /dev/null +++ b/src/cell.rs @@ -0,0 +1,20 @@ +use core::cell::Cell; + +/// Provides a view only reference to a [Cell]. +/// Useful alternative to `&Cell` when an API wants to control where a [Cell]s value can be set. +#[derive(Copy, Clone)] +pub struct CellView<'a, T: Copy>(&'a Cell); + +impl CellView<'_, T> { + #[inline(always)] + pub fn get(self) -> T { + self.0.get() + } +} + +impl<'a, T: Copy> From<&'a Cell> for CellView<'a, T> { + #[inline(always)] + fn from(value: &'a Cell) -> Self { + CellView(value) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..096e009 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,41 @@ +use crate::transducer::InvalidValue; + +/// An error that it is likely impossible to recover from. This error should only be created in +/// situations where attempts to recover have already been attempted and have failed. Error handling +/// should consist of attempting to alert another system for maintenance and attempting to shut down +/// any systems that depend on the correct functionality of the component having errors. +/// +/// In many systems there can be a single function for handling any critical error as a critical +/// error always means everything needs to be stopped. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CriticalError { + /// Critical communication failed and retries are either impossible or also failed. + Communication, + InvalidValue(InvalidValue), +} + +impl CriticalError { + pub fn emergency_procedure(self, procedure: impl FnOnce(CriticalError) -> !) -> ! { + procedure(self); + } +} + +/// [Result] where error type is [CriticalError]. +pub trait CriticalErrResult: Copy { + type Value: Copy; + + /// Execute emergency procedure in the event of a critical, the emergency procedure cannot + /// return. It should usually terminate the program, potentially rebooting the device in some sort of recovery mode. + fn err_emproc(self, procedure: impl FnOnce(CriticalError) -> !) -> Self::Value; +} + +impl CriticalErrResult for Result { + type Value = T; + + fn err_emproc(self, procedure: impl FnOnce(CriticalError) -> !) -> Self::Value { + match self { + Ok(val) => val, + Err(error) => error.emergency_procedure(procedure), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b064117 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] +#![feature(async_fn_in_trait, never_type)] + +pub mod transducer; +pub mod cell; +mod error; + +pub use error::CriticalError; diff --git a/src/transducer/input.rs b/src/transducer/input.rs new file mode 100644 index 0000000..40e3cce --- /dev/null +++ b/src/transducer/input.rs @@ -0,0 +1,6 @@ +pub trait Poll { + type Value: Copy; + type Error: Copy; + + async fn poll(&self) -> Result; +} diff --git a/src/transducer/mod.rs b/src/transducer/mod.rs new file mode 100644 index 0000000..d79c453 --- /dev/null +++ b/src/transducer/mod.rs @@ -0,0 +1,62 @@ +use core::cell::Cell; +use crate::cell::CellView; + +pub mod input; +pub mod output; +mod part; + +pub use part::*; + +// Initialisation will always be async and won't complete until a state is available for all +// stateful transducers. +pub trait Stateful { + type Value: Copy; + + fn state_cell(&self) -> CellView; + + fn state(&self) -> Self::Value; +} + +pub struct State { + state_cell: Cell, +} + +impl State { + #[inline(always)] + pub fn new(state_cell: Cell) -> Self { + Self { state_cell } + } + + #[inline(always)] + pub fn update(&self, value: T) { + self.state_cell.set(value); + } +} + +impl Stateful for State { + type Value = T; + + #[inline(always)] + fn state_cell(&self) -> CellView { + (&self.state_cell).into() + } + + #[inline(always)] + fn state(&self) -> Self::Value { + self.state_cell.get() + } +} + +impl From> for State { + #[inline(always)] + fn from(state_cell: Cell) -> Self { + State::new(state_cell) + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// ----- Error ------------------------ +// --------------------------------------------------------------------------------------------------------------------- +/// Indicates the transducer value is statically known to be impossible. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct InvalidValue; diff --git a/src/transducer/output.rs b/src/transducer/output.rs new file mode 100644 index 0000000..3512b8f --- /dev/null +++ b/src/transducer/output.rs @@ -0,0 +1,8 @@ +use crate::transducer::InvalidValue; + +pub trait Output { + type Value: Copy; + + //TODO: Should this be maybe async? + fn output(&mut self, setting: Self::Value) -> Result<(), InvalidValue>; +} diff --git a/src/transducer/part/lm35.rs b/src/transducer/part/lm35.rs new file mode 100644 index 0000000..0f492da --- /dev/null +++ b/src/transducer/part/lm35.rs @@ -0,0 +1,22 @@ +use crate::transducer::InvalidValue; +use uom::si::electric_potential::volt; +use uom::si::f32; +use uom::si::thermodynamic_temperature::degree_celsius; + +const MIN_VOLTS: f32 = -0.55; +const MAX_VOLTS: f32 = 1.50; +const SCALE_FACTOR: f32 = 100.0; + +#[inline] +pub fn convert( + voltage: f32::ElectricPotential, +) -> Result { + let volts = voltage.get::(); + + if volts >= MIN_VOLTS && volts <= MAX_VOLTS { + let celsius = volts * SCALE_FACTOR; + Ok(f32::ThermodynamicTemperature::new::(celsius)) + } else { + Err(InvalidValue) + } +} diff --git a/src/transducer/part/mod.rs b/src/transducer/part/mod.rs new file mode 100644 index 0000000..cd42413 --- /dev/null +++ b/src/transducer/part/mod.rs @@ -0,0 +1,7 @@ +mod thermocouple; + +#[cfg(feature = "lm35")] +pub mod lm35; + +#[cfg(feature = "thermocouple_k")] +pub use thermocouple::type_k as thermocouple_k; \ No newline at end of file diff --git a/src/transducer/part/thermocouple/mod.rs b/src/transducer/part/thermocouple/mod.rs new file mode 100644 index 0000000..9ce86f4 --- /dev/null +++ b/src/transducer/part/thermocouple/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "thermocouple_k")] +pub mod type_k; \ No newline at end of file diff --git a/src/transducer/part/thermocouple/type_k.rs b/src/transducer/part/thermocouple/type_k.rs new file mode 100644 index 0000000..44ff7da --- /dev/null +++ b/src/transducer/part/thermocouple/type_k.rs @@ -0,0 +1,173 @@ +use crate::transducer::InvalidValue; +use libm::powf; +use uom::si::electric_potential::{millivolt, volt}; +use uom::si::f32; +use uom::si::thermodynamic_temperature::degree_celsius; + +fn _convert( + voltage: f32::ElectricPotential, +) -> Result { + let mv = voltage.get::(); + let mv_pow2 = mv * mv; + let mv_pow3 = mv_pow2 * mv; + let mv_pow4 = mv_pow3 * mv; + let mv_pow5 = mv_pow4 * mv; + let mv_pow6 = mv_pow5 * mv; + + if mv >= -5.891 && mv <= 0.0 { + let mv_pow7 = mv_pow6 * mv; + let mv_pow8 = mv_pow7 * mv; + + let celsius = 2.5173462E+1 * mv + + -1.1662878 * mv_pow2 + + -1.0833638 * mv_pow3 + + -8.9773540E-1 * mv_pow4 + + -3.7342377E-1 * mv_pow5 + + -8.6632643E-2 * mv_pow6 + + -1.0450598E-2 * mv_pow7 + + -5.1920577E-4 * mv_pow8; + + Ok(f32::ThermodynamicTemperature::new::(celsius)) + } else if mv > 0.0 && mv < 20.644 { + let mv_pow7 = mv_pow6 * mv; + let mv_pow8 = mv_pow7 * mv; + let mv_pow9 = mv_pow8 * mv; + + let celsius = 2.508355E+1 * mv + + 7.860106E-2 * mv_pow2 + + -2.503131E-1 * mv_pow3 + + 8.315270E-2 * mv_pow4 + + -1.228034E-2 * mv_pow5 + + 9.804036E-4 * mv_pow6 + + -4.413030E-5 * mv_pow7 + + 1.057734E-6 * mv_pow8 + + -1.052755E-8 * mv_pow9; + + Ok(f32::ThermodynamicTemperature::new::(celsius)) + } else if mv >= 20.644 && mv <= 54.886 { + let celsius = 1.318058e2 + + 4.830222E+1 * mv + + -1.646031 * mv_pow2 + + 5.464731E-2 * mv_pow3 + + -9.650715E-4 * mv_pow4 + + 8.802193E-6 * mv_pow5 + + -3.110810E-8 * mv_pow6; + + Ok(f32::ThermodynamicTemperature::new::(celsius)) + } else { + Err(InvalidValue) + } +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// directly adding the reference junction temperature to the result for offset compensation. +/// +/// Can be useful compared to [convert_seebeck] when the reference temperature or the temperature +/// being read by the thermocouple is fairly close to 0. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_direct( + voltage: f32::ElectricPotential, + r_junction: f32::ThermodynamicTemperature, +) -> Result { + let base_celsius = _convert(voltage)?.get::(); + let r_junction_celsius = r_junction.get::(); + + Ok(f32::ThermodynamicTemperature::new::(base_celsius + r_junction_celsius)) +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// using a constant seebeck coefficient to correct the input voltage for offset compensation. +/// +/// Probably the right choice most of the time. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_seebeck( + voltage: f32::ElectricPotential, + r_junction: f32::ThermodynamicTemperature, +) -> Result { + let voltage_correction = temp_to_voltage_seebeck(r_junction)?; + _convert(voltage + voltage_correction) +} + +/// Convert from a voltage produced by a type k thermocouple to a temperature using polynomial and +/// using a polynomial to correct the input voltage for offset compensation. +/// +/// This is the most accurate method but uses the most processor cycles by a wide margin. +/// +/// This function uses the [NIST type K thermocouple linearisation polynomial](https://srdata.nist.gov/its90/type_k/kcoefficients_inverse.html). +#[inline] +pub fn convert_polynomial( + voltage: f32::ElectricPotential, + r_junction: f32::ThermodynamicTemperature, +) -> Result { + let voltage_correction = temp_to_voltage_poly(r_junction)?; + _convert(voltage + voltage_correction) +} + +//TODO: This is not working, check libm pow. +pub fn temp_to_voltage_poly( + temperature: f32::ThermodynamicTemperature, +) -> Result { + let celsius = temperature.get::(); + let cel_pow2 = celsius * celsius; + let cel_pow3 = cel_pow2 * celsius; + let cel_pow4 = cel_pow3 * celsius; + let cel_pow5 = cel_pow4 * celsius; + let cel_pow6 = cel_pow5 * celsius; + let cel_pow7 = cel_pow6 * celsius; + let cel_pow8 = cel_pow7 * celsius; + let cel_pow9 = cel_pow8 * celsius; + + if celsius >= -270.0 && celsius < 0.0 { + let cel_pow10 = cel_pow9 * celsius; + + let mv = 0.394501280250E-01 * celsius + + 0.236223735980E-04 * cel_pow2 + + -0.328589067840E-06 * cel_pow3 + + -0.499048287770E-08 * cel_pow4 + + -0.675090591730E-10 * cel_pow5 + + -0.574103274280E-12 * cel_pow6 + + -0.310888728940E-14 * cel_pow7 + + -0.104516093650E-16 * cel_pow8 + + -0.198892668780E-19 * cel_pow9 + + -0.163226974860E-22 * cel_pow10; + + Ok(f32::ElectricPotential::new::(mv)) + } else if celsius >= 0.0 && celsius <= 1372.0 { + let base = celsius - 0.126968600000E+03; + let exp = -0.118343200000E-03 * (base * base); + let addition = powf(0.1185976, exp); + + let mv = -0.176004136860E-01 + + 0.389212049750E-01 * celsius + + 0.185587700320E-04 * cel_pow2 + + -0.994575928740E-07 * cel_pow3 + + 0.318409457190E-09 * cel_pow4 + + -0.560728448890E-12 * cel_pow5 + + 0.560750590590E-15 * cel_pow6 + + -0.320207200030E-18 * cel_pow7 + + 0.971511471520E-22 * cel_pow8 + + -0.121047212750E-25 * cel_pow9 + + addition; + + Ok(f32::ElectricPotential::new::(mv)) + } else { + Err(InvalidValue) + } +} + +#[inline] +pub fn temp_to_voltage_seebeck( + temperature: f32::ThermodynamicTemperature, +) -> Result { + let celsius = temperature.get::(); + if celsius >= -2.0 && celsius <= 800.0 { + let mv = 0.041 * celsius; + Ok(f32::ElectricPotential::new::(mv)) + } else { + Err(InvalidValue) + } +}