Initial node implementation (#4)

Reviewed-on: #4
Co-authored-by: Zack <zack@bfpower.io>
Co-committed-by: Zack <zack@bfpower.io>
This commit is contained in:
2023-07-19 18:09:13 +00:00
committed by Zachary Sunforge
parent 886fbf0020
commit 6fc828e864
40 changed files with 2079 additions and 44 deletions

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ cmake-build-*/
# File-based project format # File-based project format
*.iws *.iws
*.stderr

155
Cargo.toml Normal file
View File

@ -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

View File

@ -1,52 +1,25 @@
# Physical # 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. * Collecting and digitizing data from the physical world.
* Controlling devices that take physical action. * 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). * Peripheral: A peripheral is a board that hosts physical I/O and usually does analog to digital conversion or
* 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. digital to analog conversion. A peripheral cannot function on its own, it must be connected to a node. This is more
* 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/). narrow than the definition of a peripheral in embedded systems generally. Peripheral support is done on the basis
* 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. of complete boards, not individual components like an ADC. Abstractions for individual components should be made
* 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. 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
* 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. commands from the commander. In a complex system, nodes are intended to be kept simple, less likely to
* Hosts are intended to run full fledged multi-process operating systems such as GNU/Linux. encounter an error than the commander, and in some cases should check for obvious problems in commands from the
* Hosts are intended to do the heavy lifting of data processing and decision making for the system being controlled. commander.
* Commander: A commander hosts nodes. It is possible for a device to be both a node and a commander at the same time,
## Software Abstractions 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
* Input - Individual unit for data collection (e.g. an analog input reads a voltage). necessary, it should be made for that specific application.
* 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.

13
commander/Cargo.toml Normal file
View File

@ -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

14
commander/src/lib.rs Normal file
View File

@ -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);
}
}

View File

@ -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"

View File

@ -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

View File

@ -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");
}

View File

@ -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<f32::ElectricPotential, NoopRawMutex, 10, 1>,
ai1: StatefulPublisher<f32::ElectricPotential, NoopRawMutex, 10, 1>,
ai2: StatefulPublisher<f32::ElectricPotential, NoopRawMutex, 10, 1>,
}
// Inputs
static ANALOG_INPUTS: StaticCell<Inputs> = StaticCell::new();
static ADS_1256: StaticCell<Ads1256<Ads1256Delay, Output<PF7>, ExtiInput<PF6>>> = StaticCell::new();
static SPI: StaticCell<Spi<SPI3, NoDma, NoDma>> = 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::<volt>(f32::NAN)).into(),
PubSubChannel::new().into(),
),
ai1: StatefulPublisher::new(
Cell::new(f32::ElectricPotential::new::<volt>(f32::NAN)).into(),
PubSubChannel::new().into(),
),
ai2: StatefulPublisher::new(
Cell::new(f32::ElectricPotential::new::<volt>(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<Ads1256Delay, Output<'static, PF7>, 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::<volt>(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::<volt>();
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::<volt>();
info!("Log task ai0: {}", msg);
let msg = ai1_sub.next_message_pure().await.get::<volt>();
info!("Log task ai1: {}", msg);
let msg = ai2_sub.next_message_pure().await.get::<volt>();
info!("Log task ai2: {}", msg);
}
}

View File

@ -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<Inputs> = StaticCell::new();
static ADS_1256: StaticCell<
Mutex<NoopRawMutex, Ads1256<Ads1256Delay, Output<PF7>, ExtiInput<PF6>>>,
> = StaticCell::new();
static SPI: StaticCell<Mutex<NoopRawMutex, Spi<SPI3, NoDma, NoDma>>> = 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::<volt>(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::<volt>(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::<volt>(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::<volt>();
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::<volt>();
info!("Log task ai{}: {}", input_num, msg);
}
}

View File

@ -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::<degree_celsius>();
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::<millivolt>();
let temperature = thermocouple_k::convert_direct(voltage, reference).unwrap();
let celsius = temperature.get::<degree_celsius>();
info!("Thermocouple temperature: {}°C, millivolts: {}", celsius, mv);
}
}

View File

@ -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

View File

@ -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<Type> = None;
let mut error_type: Option<Type> = 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<Self::Value, Self::Error> {
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::Value> {
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<Self::Value, Self::Error> {
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<Self::Mutex, Self::Value, #capacity_ident, #num_subs_ident, 0>, #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<Self::Value, Self::Error> {
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::Value> {
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<Self::Mutex, Self::Value, #capacity_ident, #num_subs_ident, 0>, #pubsub_error_path> {
self.publisher.subscribe()
}
}
};
TokenStream::from(expanded)
}

View File

@ -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<Self::Value, Self::Error> {
Ok(self.second)
}
}
fn main() {}

View File

@ -0,0 +1,5 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/generate.rs");
}

22
node/Cargo.toml Normal file
View File

@ -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

10
node/src/lib.rs Normal file
View File

@ -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;

View File

@ -0,0 +1 @@
pub use physical::transducer::input::*;

View File

@ -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::*;

View File

@ -0,0 +1 @@
pub use physical::transducer::output::*;

View File

@ -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<T>,
pub publisher: Publisher<T, MutexT, CAPACITY, NUM_SUBS>,
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize>
StatefulPublisher<T, MutexT, CAPACITY, NUM_SUBS>
{
#[inline(always)]
pub fn new(state: State<T>, publisher: Publisher<T, MutexT, CAPACITY, NUM_SUBS>) -> Self {
Self { state, publisher }
}
#[inline(always)]
pub fn update(&self, value: T) {
self.state.update(value);
self.publisher.update(value);
}
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize> Stateful
for StatefulPublisher<T, MutexT, CAPACITY, NUM_SUBS>
{
type Value = T;
#[inline(always)]
fn state_cell(&self) -> CellView<Self::Value> {
self.state.state_cell()
}
#[inline(always)]
fn state(&self) -> Self::Value {
self.state.state()
}
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize>
Publish<CAPACITY, NUM_SUBS> for StatefulPublisher<T, MutexT, CAPACITY, NUM_SUBS>
{
type Value = T;
type Mutex = MutexT;
#[inline(always)]
fn subscribe(
&self,
) -> Result<Subscriber<Self::Mutex, Self::Value, CAPACITY, NUM_SUBS, 0>, Error> {
self.publisher.subscribe()
}
}

View File

@ -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<const CAPACITY: usize, const NUM_SUBS: usize> {
type Value: Copy;
type Mutex: RawMutex;
fn subscribe(
&self,
) -> Result<Subscriber<Self::Mutex, Self::Value, CAPACITY, NUM_SUBS, 0>, pubsub::Error>;
}
pub struct Publisher<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize> {
channel: PubSubChannel<MutexT, T, CAPACITY, NUM_SUBS, 0>,
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize>
Publisher<T, MutexT, CAPACITY, NUM_SUBS>
{
#[inline(always)]
pub fn new(channel: PubSubChannel<MutexT, T, CAPACITY, NUM_SUBS, 0>) -> Self {
Self { channel }
}
#[inline(always)]
pub fn update(&self, value: T) {
self.channel.publish_immediate(value);
}
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize>
Publish<CAPACITY, NUM_SUBS> for Publisher<T, MutexT, CAPACITY, NUM_SUBS>
{
type Value = T;
type Mutex = MutexT;
#[inline(always)]
fn subscribe(
&self,
) -> Result<Subscriber<Self::Mutex, Self::Value, CAPACITY, NUM_SUBS, 0>, pubsub::Error> {
self.channel.subscriber()
}
}
impl<T: Copy, MutexT: RawMutex, const CAPACITY: usize, const NUM_SUBS: usize>
From<PubSubChannel<MutexT, T, CAPACITY, NUM_SUBS, 0>>
for Publisher<T, MutexT, CAPACITY, NUM_SUBS>
{
#[inline(always)]
fn from(channel: PubSubChannel<MutexT, T, CAPACITY, NUM_SUBS, 0>) -> Self {
Publisher::new(channel)
}
}

View File

@ -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

View File

@ -0,0 +1,4 @@
#![no_std]
#![feature(async_fn_in_trait, impl_trait_projections)]
pub mod standard;

View File

@ -0,0 +1,2 @@
#[cfg(feature = "standard-multiplexer")]
pub mod multiplexer;

View File

@ -0,0 +1,4 @@
mod sync;
#[cfg(feature = "embassy-sync")]
pub use sync::*;

View File

@ -0,0 +1,2 @@
#[cfg(feature = "poll")]
pub mod poll;

View File

@ -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<DeviceMutexT, Ads1256<DelayerT, SST, DrdyT>>,
spi: &'a Mutex<DeviceMutexT, SpiT>,
}
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<DeviceMutexT, Ads1256<DelayerT, SST, DrdyT>>,
spi: &'a Mutex<DeviceMutexT, SpiT>,
) -> 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<Self::Value, CriticalError> {
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<Status>;
fn ad_control(self) -> Option<AdControl>;
fn data_rate(self) -> Option<DataRate>;
}
#[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<Status> {
None
}
fn ad_control(self) -> Option<AdControl> {
None
}
fn data_rate(self) -> Option<DataRate> {
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<Status> {
Some(self.status)
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
None
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
None
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
Some(self.ad_control)
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
None
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
None
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
Some(self.status)
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
Some(self.ad_control)
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
Some(self.status)
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
None
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
None
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
Some(self.ad_control)
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
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<Status> {
Some(self.status)
}
#[inline(always)]
fn ad_control(self) -> Option<AdControl> {
Some(self.ad_control)
}
#[inline(always)]
fn data_rate(self) -> Option<DataRate> {
Some(self.data_rate)
}
}

14
rust-toolchain.toml Normal file
View File

@ -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",
]

8
rustfmt.toml Normal file
View File

@ -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

20
src/cell.rs Normal file
View File

@ -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<T>);
impl<T: Copy> CellView<'_, T> {
#[inline(always)]
pub fn get(self) -> T {
self.0.get()
}
}
impl<'a, T: Copy> From<&'a Cell<T>> for CellView<'a, T> {
#[inline(always)]
fn from(value: &'a Cell<T>) -> Self {
CellView(value)
}
}

41
src/error.rs Normal file
View File

@ -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<T: Copy> CriticalErrResult for Result<T, CriticalError> {
type Value = T;
fn err_emproc(self, procedure: impl FnOnce(CriticalError) -> !) -> Self::Value {
match self {
Ok(val) => val,
Err(error) => error.emergency_procedure(procedure),
}
}
}

8
src/lib.rs Normal file
View File

@ -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;

6
src/transducer/input.rs Normal file
View File

@ -0,0 +1,6 @@
pub trait Poll {
type Value: Copy;
type Error: Copy;
async fn poll(&self) -> Result<Self::Value, Self::Error>;
}

62
src/transducer/mod.rs Normal file
View File

@ -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<Self::Value>;
fn state(&self) -> Self::Value;
}
pub struct State<T: Copy> {
state_cell: Cell<T>,
}
impl<T: Copy> State<T> {
#[inline(always)]
pub fn new(state_cell: Cell<T>) -> Self {
Self { state_cell }
}
#[inline(always)]
pub fn update(&self, value: T) {
self.state_cell.set(value);
}
}
impl<T: Copy> Stateful for State<T> {
type Value = T;
#[inline(always)]
fn state_cell(&self) -> CellView<Self::Value> {
(&self.state_cell).into()
}
#[inline(always)]
fn state(&self) -> Self::Value {
self.state_cell.get()
}
}
impl<T: Copy> From<Cell<T>> for State<T> {
#[inline(always)]
fn from(state_cell: Cell<T>) -> 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;

8
src/transducer/output.rs Normal file
View File

@ -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>;
}

View File

@ -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<f32::ThermodynamicTemperature, InvalidValue> {
let volts = voltage.get::<volt>();
if volts >= MIN_VOLTS && volts <= MAX_VOLTS {
let celsius = volts * SCALE_FACTOR;
Ok(f32::ThermodynamicTemperature::new::<degree_celsius>(celsius))
} else {
Err(InvalidValue)
}
}

View File

@ -0,0 +1,7 @@
mod thermocouple;
#[cfg(feature = "lm35")]
pub mod lm35;
#[cfg(feature = "thermocouple_k")]
pub use thermocouple::type_k as thermocouple_k;

View File

@ -0,0 +1,2 @@
#[cfg(feature = "thermocouple_k")]
pub mod type_k;

View File

@ -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<f32::ThermodynamicTemperature, InvalidValue> {
let mv = voltage.get::<millivolt>();
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::<degree_celsius>(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::<degree_celsius>(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::<degree_celsius>(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<f32::ThermodynamicTemperature, InvalidValue> {
let base_celsius = _convert(voltage)?.get::<degree_celsius>();
let r_junction_celsius = r_junction.get::<degree_celsius>();
Ok(f32::ThermodynamicTemperature::new::<degree_celsius>(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<f32::ThermodynamicTemperature, InvalidValue> {
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<f32::ThermodynamicTemperature, InvalidValue> {
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<f32::ElectricPotential, InvalidValue> {
let celsius = temperature.get::<degree_celsius>();
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::<millivolt>(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::<millivolt>(mv))
} else {
Err(InvalidValue)
}
}
#[inline]
pub fn temp_to_voltage_seebeck(
temperature: f32::ThermodynamicTemperature,
) -> Result<f32::ElectricPotential, InvalidValue> {
let celsius = temperature.get::<degree_celsius>();
if celsius >= -2.0 && celsius <= 800.0 {
let mv = 0.041 * celsius;
Ok(f32::ElectricPotential::new::<millivolt>(mv))
} else {
Err(InvalidValue)
}
}