Compare commits
43 changed files with 3790 additions and 85 deletions
85
README.md
85
README.md
|
|
@ -1,85 +0,0 @@
|
|||
# mUART Failsafe Interposer
|
||||
|
||||
<a href="https://opensource.org"><img height="150px" align="left" src="https://opensource.org/files/OSIApprovedCropped.png" alt="Open Source Initiative Approved License logo"></a>
|
||||
|
||||
This repository contains firmware and hardware designs for PCBs that
|
||||
can be used with the [mUART](https://muart-group.github.io/)
|
||||
components in [ESPHome](https://esphome.io/). The PCBs provide
|
||||
interfaces for the UART links to the heat pump/air handler and
|
||||
thermostat (using CN105-style connectors), and implement a 'failsafe'
|
||||
mode ensuring that the HP/AH and the thermostat can communicate with
|
||||
each other even if the ESPHome 'host' is not operating properly (or
|
||||
not connected at all). They are designed to be interposed between the
|
||||
heat pump/air handler and the thermostat.
|
||||
|
||||
##
|
||||
|
||||
## Design
|
||||
|
||||
The 'failsafe' feature is implemented by using four analog multiplexer
|
||||
chips (each chip can route either of two signals to its
|
||||
input/output). These are used to either connect the UART TX/RX signals
|
||||
between the HP/AH and thermostat (the 'bypass' mode), or to connect
|
||||
them to the GPIOs on the ESPHome device (the 'host' mode, so that the
|
||||
mUART software can communicate with the HP/AH and thermostat). At
|
||||
power-up, two of the multiplexers connect the UART signals in 'bypass'
|
||||
mode and the other two multiplexers are disabled.
|
||||
|
||||
In addition to the multiplexers there is an
|
||||
[MSP430](https://www.ti.com/microcontrollers-mcus-processors/msp430-microcontrollers/overview.html)
|
||||
microcontroller running a very small program (see Firmware below). The
|
||||
MSP430 can switch the multiplexers into 'host' mode or allow them to
|
||||
fall back to 'bypass' mode, depending on whether it receives periodic
|
||||
'alive' signals (pulses on a GPIO) from the ESPHome device. By default
|
||||
it requires the ESPHome device to send at least one pulse every four
|
||||
seconds; if no pulse is received during a four-second period, the
|
||||
multiplexers will switch back to 'bypass' mode. As long as the ESPHome
|
||||
device's software is operating correctly and sending periodic pulses,
|
||||
it will be able to communicate with the HP/AH and the thermostat. This
|
||||
also means that the ESPHome device must send an initial pulse when it
|
||||
starts up in order to switch the multiplexers into 'host' mode; the
|
||||
example configuration below shows one way to do that.
|
||||
|
||||
## Firmware
|
||||
|
||||
The [firmware](../firmware) is a C program which runs on the
|
||||
MSP430. It can be used with any variant of the MSP430 low-cost
|
||||
mixed-signal MCUs (FR2000, FR2100, FR2110, or FR2111) with no code
|
||||
changes.
|
||||
|
||||
## Hardware
|
||||
|
||||
### Olimex UEXT Adapter
|
||||
|
||||
<img height="250px" width="410px" src="../olimex-uext/olimex-uext-front-2024.5.png" alt="Ray-traced 3D rendering of the front side of the adapter board"><img height="250px" width="410px" src="../olimex-uext/olimex-uext-back-2024.5.png" alt="Ray-traced 3D rendering of the back side of the adapter board">
|
||||
|
||||
The [Olimex UEXT Adapter board](../olimex-uext) is designed to
|
||||
be used with an [Olimex](https://www.olimex.com/Products/IoT/)
|
||||
ESP32-based host board that has a
|
||||
[UEXT](https://www.olimex.com/Products/Modules/) connector. However,
|
||||
since the host connections are standard header pins, it can be used
|
||||
with any ESP32-based board by using jumper wires.
|
||||
|
||||
The board contains the above-mentioned CN105-style connectors, the
|
||||
analog multiplexers and MSP430, 3.3V-to-5V level shifters, and various
|
||||
status LEDs.
|
||||
|
||||
## ESPHome Example Configuration
|
||||
|
||||
(coming soon)
|
||||
|
||||
## Credits
|
||||
|
||||
["Standing on the shoulders of
|
||||
giants"](https://en.wikipedia.org/wiki/Standing_on_the_shoulders_of_giants)
|
||||
could not be more true than it is in the open source hardware and
|
||||
software community; this project relies on many wonderful tools and
|
||||
libraries produced by the global open source hardware and software
|
||||
community. I've listed many of them below, but if I've overlooked any
|
||||
please do not be offended :-)
|
||||
|
||||
* [Fabrication Toolkit](https://github.com/bennymeg/Fabrication-Toolkit)
|
||||
* [KiCAD](https://www.kicad.org/)
|
||||
* [MSP430 GCC](https://www.ti.com/tool/MSP430-GCC-OPENSOURCE)
|
||||
* [mspdebug](https://github.com/dlbeer/mspdebug)
|
||||
* [opensource-toolchain-msp430](https://github.com/cjacker/opensource-toolchain-msp430)
|
||||
2
firmware/.cargo/config.toml
Normal file
2
firmware/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
1
firmware/.gitignore
vendored
Normal file
1
firmware/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1291
firmware/Cargo.lock
generated
Normal file
1291
firmware/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
51
firmware/Cargo.toml
Normal file
51
firmware/Cargo.toml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["app", "boot"]
|
||||
|
||||
[workspace.dependencies]
|
||||
embassy-boot = { version = "0.6.0" }
|
||||
embassy-boot-stm32 = { version = "0.4.0" }
|
||||
embassy-executor = { version = "0.8.0", features = ["arch-cortex-m", "executor-thread"] }
|
||||
embassy-futures = { version = "0.1.1" }
|
||||
embassy-stm32 = { version = "0.2.0", features = ["time-driver-tim1", "stm32g070kb", "unstable-pac" ] }
|
||||
embassy-sync = { version = "0.7.0" }
|
||||
embassy-time = { version = "0.4.0", features = ["tick-hz-32_768", "defmt-timestamp-uptime"] }
|
||||
|
||||
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
||||
cortex-m-rt = { version = "0.7.0" }
|
||||
portable-atomic = { version = "1.5", features = ["unsafe-assume-single-core"] }
|
||||
|
||||
embedded-hal = { version = "1.0.0" }
|
||||
embedded-io = { version = "0.6.1" }
|
||||
embedded-io-async = { version = "0.6.1" }
|
||||
embedded-resources = { version = "0.2.1", features = ["stm32"] }
|
||||
embedded-storage = { version = "0.3.1" }
|
||||
embedded-storage-async = { version = "0.4.1" }
|
||||
|
||||
byteorder = { version = "1.5.0", default-features = false }
|
||||
static_cell = { version = "2.1.0" }
|
||||
never = { version = "0.1.0", default-features = false }
|
||||
thiserror = { version = "2.0.12", default-features = false }
|
||||
|
||||
defmt = { version = "1.0.0" }
|
||||
defmt-rtt = { version = "1.0.0" }
|
||||
panic-probe = { version = "1.0.0" }
|
||||
|
||||
[patch.crates-io]
|
||||
embassy-boot = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-boot-stm32 = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-executor = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-futures = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-sync = { git = "https://github.com/embassy-rs/embassy" }
|
||||
embassy-time = { git = "https://github.com/embassy-rs/embassy" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = "s"
|
||||
debug = 2
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
opt-level = "z"
|
||||
debug = 2
|
||||
6
firmware/app/.cargo/config.toml
Normal file
6
firmware/app/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[build]
|
||||
target = "thumbv6m-none-eabi"
|
||||
rustdocflags = ["--document-private-items"]
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "trace"
|
||||
50
firmware/app/Cargo.toml
Normal file
50
firmware/app/Cargo.toml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
embassy-boot = { workspace = true, features = ["defmt"] }
|
||||
embassy-boot-stm32 = { workspace = true, features = ["defmt"] }
|
||||
embassy-executor = { workspace = true, features = ["defmt"] }
|
||||
embassy-futures = { workspace = true, features = ["defmt"] }
|
||||
embassy-stm32 = { workspace = true, features = ["defmt"] }
|
||||
embassy-sync = { workspace = true }
|
||||
embassy-time = { workspace = true, features = ["defmt"] }
|
||||
|
||||
cortex-m = { workspace = true }
|
||||
cortex-m-rt = { workspace = true }
|
||||
portable-atomic = { workspace = true }
|
||||
|
||||
embedded-hal = { workspace = true }
|
||||
embedded-io = { workspace = true }
|
||||
embedded-io-async = { workspace = true }
|
||||
embedded-resources = { workspace = true }
|
||||
|
||||
byteorder = { workspace = true }
|
||||
static_cell = { workspace = true }
|
||||
never = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
defmt = { workspace = true }
|
||||
defmt-rtt = { workspace = true }
|
||||
panic-probe = { workspace = true, features = ["print-defmt"] }
|
||||
|
||||
micropb = { version = "0.1.1", default-features = false, features = ["encode", "decode", "container-heapless"] }
|
||||
|
||||
[build-dependencies]
|
||||
micropb-gen = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = ["board-nucleo64", "status-led"]
|
||||
board-nucleo64 = []
|
||||
status-led = []
|
||||
copy-within = []
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
wildcard_imports = "allow"
|
||||
used_underscore_binding = "allow" # necessary because Embassy macros 'use' task arguments
|
||||
cargo_common_metadata = "allow"
|
||||
multiple_crate_versions = "allow" # necessary because the Embassy crates... yeah
|
||||
62
firmware/app/build.rs
Normal file
62
firmware/app/build.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use std::{env, fs, path::Path};
|
||||
|
||||
use micropb_gen::Generator;
|
||||
|
||||
fn proto_generate(package_dir: &Path, project_dir: &Path) {
|
||||
let proto_dir = Path::new(project_dir).join("proto");
|
||||
let target_dir = package_dir.join("src");
|
||||
|
||||
let proto_files: Vec<_> = fs::read_dir(&proto_dir)
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|de| {
|
||||
if let Some(ext) = de.path().extension() {
|
||||
ext == "proto"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|de| de.file_name())
|
||||
.collect();
|
||||
|
||||
let mut g = Generator::new();
|
||||
g.use_container_heapless()
|
||||
.add_protoc_arg(format!("-I{}", proto_dir.display()))
|
||||
.compile_protos(&proto_files, target_dir.join("proto.rs"))
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", proto_dir.display());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let workspace_dir = Path::new(env!("CARGO_WORKSPACE_DIR"));
|
||||
let package_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let project_dir = workspace_dir.parent().unwrap();
|
||||
|
||||
// Make `all-memory.x` available to the linker.
|
||||
// Tell Cargo where to find the file.
|
||||
println!(
|
||||
"cargo:rustc-link-search={}",
|
||||
workspace_dir.join("common").display()
|
||||
);
|
||||
// Tell Cargo to rebuild if the file is updated.
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
workspace_dir.join("common").join("all-memory.x").display()
|
||||
);
|
||||
|
||||
// Make `memory.x` available to the linker.
|
||||
// Tell Cargo where to find the file.
|
||||
println!("cargo:rustc-link-search={}", package_dir.display());
|
||||
// Tell Cargo to rebuild if the file is updated.
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
package_dir.join("memory.x").display()
|
||||
);
|
||||
|
||||
println!("cargo:rustc-link-arg-bins=--nmagic");
|
||||
println!("cargo:rustc-link-arg-bins=-Tlink.x");
|
||||
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
|
||||
|
||||
proto_generate(package_dir, project_dir);
|
||||
}
|
||||
3
firmware/app/memory.x
Normal file
3
firmware/app/memory.x
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
INCLUDE all-memory.x
|
||||
|
||||
REGION_ALIAS("FLASH", APP)
|
||||
70
firmware/app/src/crc.rs
Normal file
70
firmware/app/src/crc.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use defmt::*;
|
||||
use embassy_stm32::crc;
|
||||
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||
use embassy_sync::mutex;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
type CrcEngine = mutex::Mutex<ThreadModeRawMutex, crc::Crc<'static>>;
|
||||
|
||||
static ENGINE: StaticCell<CrcEngine> = StaticCell::new();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Handle {
|
||||
engine: &'static CrcEngine,
|
||||
}
|
||||
|
||||
pub struct Stream {
|
||||
guard: mutex::MutexGuard<'static, ThreadModeRawMutex, crc::Crc<'static>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
pub fn new(r: crate::CrcEngineResources) -> Self {
|
||||
let cfg = crc::Config::new(
|
||||
crc::InputReverseConfig::None,
|
||||
false,
|
||||
crc::PolySize::Width16,
|
||||
0x1d0f,
|
||||
0x1021,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let engine_ref = ENGINE.init(mutex::Mutex::new(crc::Crc::new(r.crc_engine, cfg)));
|
||||
|
||||
Handle { engine: engine_ref }
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub async fn compute(&self, data: &[u8]) -> u16 {
|
||||
let mut guard = self.engine.lock().await;
|
||||
let engine = &mut *guard;
|
||||
|
||||
engine.reset();
|
||||
trace!("CRC computed");
|
||||
engine.feed_bytes(data) as u16
|
||||
}
|
||||
|
||||
pub async fn stream(&self) -> Stream {
|
||||
let mut guard = self.engine.lock().await;
|
||||
let engine = &mut *guard;
|
||||
|
||||
trace!("CRC stream started");
|
||||
engine.reset();
|
||||
Stream { guard }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn feed_bytes(&mut self, data: &[u8]) {
|
||||
let engine = &mut *self.guard;
|
||||
|
||||
engine.feed_bytes(data);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn result(&self) -> u16 {
|
||||
let engine = &*self.guard;
|
||||
|
||||
trace!("CRC stream completed");
|
||||
engine.read() as u16
|
||||
}
|
||||
}
|
||||
214
firmware/app/src/io/framed.rs
Normal file
214
firmware/app/src/io/framed.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
use crate::{io, utils};
|
||||
use defmt::*;
|
||||
use embassy_time::Duration;
|
||||
use embedded_io::{ReadReady, Write};
|
||||
use embedded_io_async::Read;
|
||||
use micropb::heapless::Vec;
|
||||
use thiserror::Error;
|
||||
|
||||
pub trait Sender {
|
||||
type Error;
|
||||
|
||||
fn send_sync(&mut self) -> Result<(), Self::Error>;
|
||||
fn send_frame_fragment(&mut self, buf: &[u8]) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Receiver {
|
||||
type Error;
|
||||
|
||||
fn buf(&self) -> &[u8];
|
||||
fn clear_buf(&mut self);
|
||||
fn remove_frame(&mut self, bytes: usize);
|
||||
|
||||
async fn receive_sync(&mut self) -> Result<(), Self::Error>;
|
||||
async fn receive_frame_fragment(
|
||||
&mut self,
|
||||
fragment_pos: usize,
|
||||
fragment_len: usize,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Error)]
|
||||
pub enum AsyncReadReceiverError<T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
#[error("insufficent buffer capacity")]
|
||||
BufferCapacity,
|
||||
#[error("timeout expired waiting for data")]
|
||||
Timeout,
|
||||
#[error("source error")]
|
||||
Other(T::Error),
|
||||
}
|
||||
|
||||
pub struct AsyncReadReceiver<const N: usize, T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
buf: &'static mut Vec<u8, N>,
|
||||
rx: T,
|
||||
sync: u8,
|
||||
first_byte_timeout: Duration,
|
||||
between_bytes_timeout: Duration,
|
||||
}
|
||||
|
||||
impl<const N: usize, T> AsyncReadReceiver<N, T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
pub fn new(
|
||||
buf: &'static mut Vec<u8, N>,
|
||||
rx: T,
|
||||
sync: u8,
|
||||
first_byte_timeout: Duration,
|
||||
between_bytes_timeout: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
rx,
|
||||
sync,
|
||||
first_byte_timeout,
|
||||
between_bytes_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, T> Receiver for AsyncReadReceiver<N, T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
type Error = AsyncReadReceiverError<T>;
|
||||
|
||||
fn buf(&self) -> &[u8] {
|
||||
self.buf.as_slice()
|
||||
}
|
||||
|
||||
fn clear_buf(&mut self) {
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
fn remove_frame(&mut self, bytes: usize) {
|
||||
utils::remove_leading_bytes(self.buf, bytes);
|
||||
}
|
||||
|
||||
async fn receive_sync(&mut self) -> Result<(), Self::Error> {
|
||||
// look for a sync byte in the buffer; the buffer may have
|
||||
// leftover bytes from a previous cycle (due to an incomplete
|
||||
// message, or a message which failed validation)
|
||||
if let Some(sync_pos) = self.buf.iter().position(|&x| x == self.sync) {
|
||||
// if a sync byte was found, reset the buffer to the data
|
||||
// following it
|
||||
//
|
||||
utils::remove_leading_bytes(self.buf, sync_pos + 1);
|
||||
} else {
|
||||
// remove any leftover data in the buffer, since a sync
|
||||
// byte will begin a new frame
|
||||
self.buf.clear();
|
||||
// wait for a sync byte to arrive
|
||||
let mut buf = [0_u8; 1];
|
||||
loop {
|
||||
debug!("waiting for sync byte");
|
||||
match self.rx.read(&mut buf[..]).await {
|
||||
Ok(_) => {
|
||||
trace!("rx byte {=u8:#x}", buf[0]);
|
||||
if buf[0] == self.sync {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(AsyncReadReceiverError::Other(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_frame_fragment(
|
||||
&mut self,
|
||||
fragment_pos: usize,
|
||||
fragment_len: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
// compute the number of bytes available in the buffer, and the
|
||||
// number of bytes to read
|
||||
let bytes_available = self.buf[fragment_pos..].len();
|
||||
let bytes_to_read = fragment_len.saturating_sub(bytes_available);
|
||||
// if there are enough bytes in the buffer, return
|
||||
if bytes_to_read == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// if the fragment will not fit in the buffer's available space,
|
||||
// return an error
|
||||
if bytes_to_read > (self.buf.capacity() - self.buf.len()) {
|
||||
return Err(AsyncReadReceiverError::BufferCapacity);
|
||||
}
|
||||
|
||||
// wait for the needed bytes to arrive, with timeouts
|
||||
let read_start = fragment_pos + bytes_available;
|
||||
let read_end = read_start + bytes_to_read;
|
||||
// make temporary space in the buffer for the bytes to be read
|
||||
unsafe {
|
||||
self.buf.set_len(read_end);
|
||||
}
|
||||
match io::read_exact_with_timeouts(
|
||||
&mut self.rx,
|
||||
&mut self.buf[read_start..read_end],
|
||||
self.first_byte_timeout,
|
||||
self.between_bytes_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(io::ReadError::Timeout { bytes_read }) => {
|
||||
// set the buffer length to include the number of bytes
|
||||
// that were actually read
|
||||
unsafe {
|
||||
self.buf.set_len(read_start + bytes_read);
|
||||
}
|
||||
Err(AsyncReadReceiverError::Timeout)
|
||||
}
|
||||
Err(io::ReadError::Other { bytes_read, source }) => {
|
||||
// set the buffer length to include the number of bytes
|
||||
// that were actually read
|
||||
unsafe {
|
||||
self.buf.set_len(read_start + bytes_read);
|
||||
}
|
||||
Err(AsyncReadReceiverError::Other(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WriteSender<T>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
tx: T,
|
||||
sync: u8,
|
||||
}
|
||||
|
||||
impl<T> WriteSender<T>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
pub fn new(tx: T, sync: u8) -> Self {
|
||||
Self { tx, sync }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sender for WriteSender<T>
|
||||
where
|
||||
T: Write,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn send_sync(&mut self) -> Result<(), Self::Error> {
|
||||
let buf = [self.sync; 1];
|
||||
self.tx.write_all(&buf)
|
||||
}
|
||||
|
||||
fn send_frame_fragment(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
|
||||
self.tx.write_all(buf)
|
||||
}
|
||||
}
|
||||
105
firmware/app/src/io/mod.rs
Normal file
105
firmware/app/src/io/mod.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use embassy_futures::select::*;
|
||||
use embassy_time::{Duration, Timer};
|
||||
use embedded_io::ReadReady;
|
||||
use embedded_io_async::Read;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod framed;
|
||||
pub mod proto;
|
||||
|
||||
#[derive(Error)]
|
||||
pub enum ReadError<T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
#[error("timeout expired waiting for data")]
|
||||
Timeout { bytes_read: usize },
|
||||
#[error("source error")]
|
||||
Other { bytes_read: usize, source: T::Error },
|
||||
}
|
||||
|
||||
/// Reads data from an object which implements both
|
||||
/// [`embedded_io_async::Read`] and [`embedded_io::ReadReady`], and
|
||||
/// supports two timeouts:
|
||||
///
|
||||
/// `first_byte`: if no data is available immediately, will wait this
|
||||
/// long for the first byte to arrive
|
||||
///
|
||||
/// `between_bytes`: after the first byte has been read, will wait
|
||||
/// this long between bytes
|
||||
///
|
||||
/// Returns [`ReadError::Timeout`] if a timeout expires; the
|
||||
/// `bytes_read` field of the error will contain the number of bytes
|
||||
/// which had been read before the timeout occurred, if any
|
||||
///
|
||||
/// Returns [`ReadError::Other`] if an error occurs in the source
|
||||
/// object; the `bytes_read` field of the error will contain the
|
||||
/// number of bytes which had been read before the error occurred, if
|
||||
/// any
|
||||
///
|
||||
/// Returns `Ok` if no error or timeout occurs
|
||||
async fn read_exact_with_timeouts<T>(
|
||||
rx: &mut T,
|
||||
mut buf: &mut [u8],
|
||||
first_byte: Duration,
|
||||
between_bytes: Duration,
|
||||
) -> Result<(), ReadError<T>>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
let mut bytes_read = 0;
|
||||
|
||||
while !buf.is_empty() {
|
||||
// check for immediately-available bytes, and read them
|
||||
match rx.read_ready() {
|
||||
Ok(false) => {}
|
||||
Ok(true) => match rx.read(buf).await {
|
||||
Ok(r) => {
|
||||
buf = &mut buf[r..];
|
||||
bytes_read += r;
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ReadError::Other {
|
||||
source: e,
|
||||
bytes_read,
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(ReadError::Other {
|
||||
source: e,
|
||||
bytes_read,
|
||||
});
|
||||
}
|
||||
}
|
||||
// set a timeout based on whether the first byte has been read
|
||||
let timeout = Timer::after(if bytes_read == 0 {
|
||||
first_byte
|
||||
} else {
|
||||
between_bytes
|
||||
});
|
||||
// setup a single-byte read for the next byte; if more than
|
||||
// one byte becomes available, the remainder will be read at
|
||||
// the top of the next loop iteration
|
||||
let src = rx.read(&mut buf[0..1]);
|
||||
// wait for either the read or the timeout to complete
|
||||
match select(src, timeout).await {
|
||||
Either::First(Ok(r)) => {
|
||||
buf = &mut buf[1..];
|
||||
bytes_read += r;
|
||||
}
|
||||
Either::First(Err(e)) => {
|
||||
return Err(ReadError::Other {
|
||||
source: e,
|
||||
bytes_read,
|
||||
});
|
||||
}
|
||||
Either::Second(()) => {
|
||||
return Err(ReadError::Timeout { bytes_read });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
143
firmware/app/src/io/proto.rs
Normal file
143
firmware/app/src/io/proto.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use defmt::*;
|
||||
use micropb::{DecodeError, MessageDecode, MessageEncode, PbDecoder, PbEncoder, PbWrite};
|
||||
use never;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::framed;
|
||||
use crate::{crc, proto};
|
||||
|
||||
pub const SYNC_BYTE: u8 = 0xfc;
|
||||
pub const MESSAGE_LENGTH_SIZE: usize = 2;
|
||||
pub const MESSAGE_CRC_SIZE: usize = 2;
|
||||
|
||||
struct Writer<'a, T>
|
||||
where
|
||||
T: framed::Sender,
|
||||
{
|
||||
ftx: &'a mut T,
|
||||
crc: &'a mut crc::Stream,
|
||||
}
|
||||
|
||||
impl<T> PbWrite for Writer<'_, T>
|
||||
where
|
||||
T: framed::Sender,
|
||||
{
|
||||
type Error = T::Error;
|
||||
|
||||
fn pb_write(&mut self, data: &[u8]) -> Result<(), T::Error> {
|
||||
self.ftx.send_frame_fragment(data)?;
|
||||
self.crc.feed_bytes(data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_target_message<T>(
|
||||
ftx: &mut T,
|
||||
msg: proto::api_::TargetMessage,
|
||||
crc: crc::Handle,
|
||||
) -> Result<(), T::Error>
|
||||
where
|
||||
T: framed::Sender,
|
||||
{
|
||||
debug!("sending sync");
|
||||
ftx.send_sync()?;
|
||||
|
||||
debug!("message is {=u16} bytes", msg.compute_size() as u16);
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let encoded_size = (msg.compute_size() as u16).to_le_bytes();
|
||||
ftx.send_frame_fragment(&encoded_size)?;
|
||||
|
||||
let mut crc_stream = crc.stream().await;
|
||||
let mut writer = Writer {
|
||||
ftx,
|
||||
crc: &mut crc_stream,
|
||||
};
|
||||
let mut encoder = PbEncoder::new(&mut writer);
|
||||
debug!("sending message");
|
||||
msg.encode(&mut encoder)?;
|
||||
|
||||
let crc_result = crc_stream.result().to_le_bytes();
|
||||
debug!("sending CRC");
|
||||
ftx.send_frame_fragment(&crc_result)
|
||||
}
|
||||
|
||||
#[derive(Error)]
|
||||
pub enum ReceiveError<T>
|
||||
where
|
||||
T: framed::Receiver,
|
||||
{
|
||||
#[error("sync error")]
|
||||
Sync,
|
||||
#[error("CRC-16 mismatch")]
|
||||
Crc,
|
||||
#[error("framing error")]
|
||||
Framing(T::Error),
|
||||
#[error("decode error")]
|
||||
Decode(DecodeError<never::Never>),
|
||||
}
|
||||
|
||||
pub async fn receive_host_message<T>(
|
||||
frx: &mut T,
|
||||
crc: crc::Handle,
|
||||
) -> Result<proto::api_::HostMessage, ReceiveError<T>>
|
||||
where
|
||||
T: framed::Receiver,
|
||||
{
|
||||
frx.receive_sync().await.or(Err(ReceiveError::Sync))?;
|
||||
|
||||
debug!("got sync");
|
||||
|
||||
// sync byte was seen, read the length of the message
|
||||
frx.receive_frame_fragment(0, MESSAGE_LENGTH_SIZE)
|
||||
.await
|
||||
.map_err(ReceiveError::Framing)?;
|
||||
|
||||
debug!("got length");
|
||||
|
||||
// get the message length from the buffer
|
||||
let message_len = LittleEndian::read_u16(&frx.buf()[..MESSAGE_LENGTH_SIZE]) as usize;
|
||||
|
||||
// read the message
|
||||
let message_pos = MESSAGE_LENGTH_SIZE;
|
||||
frx.receive_frame_fragment(message_pos, message_len)
|
||||
.await
|
||||
.map_err(ReceiveError::Framing)?;
|
||||
|
||||
debug!("got message");
|
||||
|
||||
// read the CRC-16 of the message
|
||||
let crc_pos = message_pos + message_len;
|
||||
frx.receive_frame_fragment(crc_pos, MESSAGE_CRC_SIZE)
|
||||
.await
|
||||
.map_err(ReceiveError::Framing)?;
|
||||
|
||||
debug!("got CRC");
|
||||
|
||||
// get the expected CRC-16 from the buffer
|
||||
let expected_crc = LittleEndian::read_u16(&frx.buf()[crc_pos..crc_pos + MESSAGE_CRC_SIZE]);
|
||||
// compute the actual CRC-16
|
||||
let computed_crc = crc
|
||||
.compute(&frx.buf()[message_pos..message_pos + message_len])
|
||||
.await;
|
||||
// check the CRC-16
|
||||
if computed_crc != expected_crc {
|
||||
return Err(ReceiveError::Crc);
|
||||
}
|
||||
|
||||
debug!("CRC valid");
|
||||
|
||||
// at this point the buffer has been confirmed to contain a
|
||||
// valid message frame, so any failures beyond this point can
|
||||
// only drop the message, it cannot be re-parsed
|
||||
let mut decoder = PbDecoder::new(&frx.buf()[message_pos..message_pos + message_len]);
|
||||
let mut msg = proto::api_::HostMessage::default();
|
||||
let result = msg
|
||||
.decode(&mut decoder, message_len)
|
||||
.map_err(ReceiveError::Decode)
|
||||
.map(|_x| msg);
|
||||
|
||||
frx.remove_frame(crc_pos + MESSAGE_CRC_SIZE);
|
||||
|
||||
result
|
||||
}
|
||||
185
firmware/app/src/lib.rs
Normal file
185
firmware/app/src/lib.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#![no_std]
|
||||
|
||||
extern crate core;
|
||||
|
||||
mod crc;
|
||||
mod io;
|
||||
mod tasks;
|
||||
mod utils;
|
||||
|
||||
mod proto {
|
||||
#![allow(clippy::all, clippy::pedantic)]
|
||||
#![allow(nonstandard_style, unused, irrefutable_let_patterns)]
|
||||
include!("proto.rs");
|
||||
}
|
||||
|
||||
use core::mem;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::peripherals::*;
|
||||
use embassy_stm32::{Peri, bind_interrupts, gpio, rcc, usart, wdg};
|
||||
use embedded_resources::resource_group;
|
||||
use tasks::*;
|
||||
|
||||
use defmt::*;
|
||||
use defmt_rtt as _;
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct SupervisorResources {
|
||||
wdg: IWDG,
|
||||
#[cfg(all(feature = "status-led", feature = "board-nucleo64"))]
|
||||
status_led: PA5,
|
||||
}
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct CrcEngineResources {
|
||||
crc_engine: CRC,
|
||||
}
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct HostInterfaceResources {
|
||||
usart: USART1,
|
||||
tx_pin: PA9,
|
||||
rx_pin: PA10,
|
||||
clr_host_transmit: PC14,
|
||||
set_host_transmit: PC15,
|
||||
host_active_led: PB7,
|
||||
}
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct HeatPumpInterfaceResources {
|
||||
usart: USART4,
|
||||
tx_pin: PA0,
|
||||
rx_pin: PA1,
|
||||
}
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct ThermostatInterfaceResources {
|
||||
usart: USART3,
|
||||
tx_pin: PB2,
|
||||
rx_pin: PB0,
|
||||
}
|
||||
|
||||
#[resource_group(no_aliases)]
|
||||
struct UnusedPinsResources {
|
||||
pa2: PA2,
|
||||
pa3: PA3,
|
||||
pa4: PA4,
|
||||
#[cfg(not(all(feature = "status-led", feature = "board-nucleo64")))]
|
||||
pa5: PA5,
|
||||
pa6: PA6,
|
||||
pa7: PA7,
|
||||
pa8: PA8,
|
||||
pa11: PA11,
|
||||
pa12: PA12,
|
||||
pa15: PA15,
|
||||
pb1: PB1,
|
||||
pb3: PB3,
|
||||
pb4: PB4,
|
||||
pb5: PB5,
|
||||
pb6: PB6,
|
||||
pb8: PB8,
|
||||
pb9: PB9,
|
||||
pc6: PC6,
|
||||
}
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USART1 => usart::BufferedInterruptHandler<USART1>;
|
||||
USART3_4 => usart::BufferedInterruptHandler<USART3>, usart::BufferedInterruptHandler<USART4>;
|
||||
});
|
||||
|
||||
pub fn spawn_tasks(spawner: Spawner) {
|
||||
let p = embassy_stm32::init(build_rcc_config());
|
||||
|
||||
// enable Prefetch and Instruction Cache in FLASH peripheral
|
||||
embassy_stm32::pac::FLASH.acr().modify(|w| {
|
||||
w.set_prften(true);
|
||||
w.set_icen(true);
|
||||
});
|
||||
|
||||
disable_unused_peripherals();
|
||||
|
||||
configure_unused_pins(unused_pins_resources!(p));
|
||||
|
||||
let r = supervisor_resources!(p);
|
||||
|
||||
#[cfg(feature = "status-led")]
|
||||
{
|
||||
let led = gpio::Output::new(r.status_led, gpio::Level::Low, gpio::Speed::Low);
|
||||
spawner.must_spawn(supervisor::status_task(led));
|
||||
}
|
||||
|
||||
let wdg = wdg::IndependentWatchdog::new(r.wdg, 4_000_000);
|
||||
spawner.must_spawn(supervisor::watchdog_task(wdg));
|
||||
|
||||
let crc_handle = crc::Handle::new(crc_engine_resources!(p));
|
||||
|
||||
host_interface::start(host_interface_resources!(p), spawner, crc_handle);
|
||||
|
||||
heat_pump_interface::start(heat_pump_interface_resources!(p), spawner);
|
||||
|
||||
thermostat_interface::start(thermostat_interface_resources!(p), spawner);
|
||||
|
||||
info!("startup complete");
|
||||
}
|
||||
|
||||
fn configure_unused_pin(p: Peri<'_, impl gpio::Pin>) {
|
||||
mem::forget(gpio::Input::new(p, gpio::Pull::Up));
|
||||
}
|
||||
|
||||
fn configure_unused_pins(r: UnusedPinsResources) {
|
||||
configure_unused_pin(r.pa2);
|
||||
configure_unused_pin(r.pa3);
|
||||
configure_unused_pin(r.pa4);
|
||||
#[cfg(not(all(feature = "status-led", feature = "board-nucleo64")))]
|
||||
configure_unused_pin(r.pa5);
|
||||
configure_unused_pin(r.pa6);
|
||||
configure_unused_pin(r.pa7);
|
||||
configure_unused_pin(r.pa8);
|
||||
configure_unused_pin(r.pa11);
|
||||
configure_unused_pin(r.pa12);
|
||||
configure_unused_pin(r.pa15);
|
||||
configure_unused_pin(r.pb1);
|
||||
configure_unused_pin(r.pb3);
|
||||
configure_unused_pin(r.pb4);
|
||||
configure_unused_pin(r.pb5);
|
||||
configure_unused_pin(r.pb6);
|
||||
configure_unused_pin(r.pb8);
|
||||
configure_unused_pin(r.pb9);
|
||||
configure_unused_pin(r.pc6);
|
||||
}
|
||||
|
||||
fn build_rcc_config() -> embassy_stm32::Config {
|
||||
let mut config = embassy_stm32::Config::default();
|
||||
config.rcc.hsi = Some(rcc::Hsi {
|
||||
sys_div: rcc::HsiSysDiv::DIV1,
|
||||
});
|
||||
config.rcc.pll = Some(rcc::Pll {
|
||||
source: rcc::PllSource::HSI,
|
||||
prediv: rcc::PllPreDiv::DIV1,
|
||||
mul: rcc::PllMul::MUL16,
|
||||
divp: None,
|
||||
divq: Some(rcc::PllQDiv::DIV2), // 16 / 1 * 16 / 2 = 128 MHz
|
||||
divr: Some(rcc::PllRDiv::DIV4), // 16 / 1 * 16 / 4 = 64 MHz
|
||||
});
|
||||
config.rcc.sys = rcc::Sysclk::PLL1_R;
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn disable_unused_peripherals() {
|
||||
rcc::disable::<ADC1>();
|
||||
rcc::disable::<I2C1>();
|
||||
rcc::disable::<I2C2>();
|
||||
rcc::disable::<I2C1>();
|
||||
rcc::disable::<I2C2>();
|
||||
rcc::disable::<SPI1>();
|
||||
rcc::disable::<SPI2>();
|
||||
rcc::disable::<TIM3>();
|
||||
rcc::disable::<TIM6>();
|
||||
rcc::disable::<TIM7>();
|
||||
rcc::disable::<TIM14>();
|
||||
rcc::disable::<TIM15>();
|
||||
rcc::disable::<TIM16>();
|
||||
rcc::disable::<TIM17>();
|
||||
rcc::disable::<USART2>();
|
||||
}
|
||||
11
firmware/app/src/main.rs
Normal file
11
firmware/app/src/main.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
|
||||
use panic_probe as _;
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
app::spawn_tasks(spawner);
|
||||
}
|
||||
390
firmware/app/src/proto.rs
Normal file
390
firmware/app/src/proto.rs
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
pub mod echo_ {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EchoRequest {}
|
||||
impl ::core::default::Default for EchoRequest {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for EchoRequest {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl EchoRequest {}
|
||||
impl ::micropb::MessageDecode for EchoRequest {
|
||||
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
|
||||
&mut self,
|
||||
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
|
||||
len: usize,
|
||||
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
|
||||
let before = decoder.bytes_read();
|
||||
while decoder.bytes_read() - before < len {
|
||||
let tag = decoder.decode_tag()?;
|
||||
match tag.field_num() {
|
||||
0 => return Err(::micropb::DecodeError::ZeroField),
|
||||
_ => {
|
||||
decoder.skip_wire_value(tag.wire_type())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageEncode for EchoRequest {
|
||||
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
|
||||
&self,
|
||||
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
|
||||
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
size
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EchoResponse {}
|
||||
impl ::core::default::Default for EchoResponse {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for EchoResponse {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl EchoResponse {}
|
||||
impl ::micropb::MessageDecode for EchoResponse {
|
||||
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
|
||||
&mut self,
|
||||
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
|
||||
len: usize,
|
||||
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
|
||||
let before = decoder.bytes_read();
|
||||
while decoder.bytes_read() - before < len {
|
||||
let tag = decoder.decode_tag()?;
|
||||
match tag.field_num() {
|
||||
0 => return Err(::micropb::DecodeError::ZeroField),
|
||||
_ => {
|
||||
decoder.skip_wire_value(tag.wire_type())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageEncode for EchoResponse {
|
||||
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
|
||||
&self,
|
||||
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
|
||||
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod api_ {
|
||||
pub mod HostMessage_ {
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Msg {
|
||||
Echo(super::super::echo_::EchoRequest),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostMessage {
|
||||
pub r#id: u32,
|
||||
pub r#msg: ::core::option::Option<HostMessage_::Msg>,
|
||||
}
|
||||
impl ::core::default::Default for HostMessage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r#id: ::core::default::Default::default(),
|
||||
r#msg: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for HostMessage {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret &= (self.r#id == other.r#id);
|
||||
ret &= (self.r#msg == other.r#msg);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl HostMessage {
|
||||
///Return a reference to `id`
|
||||
#[inline]
|
||||
pub fn r#id(&self) -> &u32 {
|
||||
&self.r#id
|
||||
}
|
||||
///Return a mutable reference to `id`
|
||||
#[inline]
|
||||
pub fn mut_id(&mut self) -> &mut u32 {
|
||||
&mut self.r#id
|
||||
}
|
||||
///Set the value of `id`
|
||||
#[inline]
|
||||
pub fn set_id(&mut self, value: u32) -> &mut Self {
|
||||
self.r#id = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `id`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_id(mut self, value: u32) -> Self {
|
||||
self.r#id = value.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageDecode for HostMessage {
|
||||
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
|
||||
&mut self,
|
||||
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
|
||||
len: usize,
|
||||
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
|
||||
let before = decoder.bytes_read();
|
||||
while decoder.bytes_read() - before < len {
|
||||
let tag = decoder.decode_tag()?;
|
||||
match tag.field_num() {
|
||||
0 => return Err(::micropb::DecodeError::ZeroField),
|
||||
1u32 => {
|
||||
let mut_ref = &mut self.r#id;
|
||||
{
|
||||
let val = decoder.decode_varint32()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref != 0 {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
2u32 => {
|
||||
let mut_ref = loop {
|
||||
if let ::core::option::Option::Some(variant) = &mut self
|
||||
.r#msg
|
||||
{
|
||||
if let HostMessage_::Msg::Echo(variant) = &mut *variant {
|
||||
break &mut *variant;
|
||||
}
|
||||
}
|
||||
self.r#msg = ::core::option::Option::Some(
|
||||
HostMessage_::Msg::Echo(::core::default::Default::default()),
|
||||
);
|
||||
};
|
||||
mut_ref.decode_len_delimited(decoder)?;
|
||||
}
|
||||
_ => {
|
||||
decoder.skip_wire_value(tag.wire_type())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageEncode for HostMessage {
|
||||
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
|
||||
&self,
|
||||
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
|
||||
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
{
|
||||
let val_ref = &self.r#id;
|
||||
if *val_ref != 0 {
|
||||
encoder.encode_varint32(8u32)?;
|
||||
encoder.encode_varint32(*val_ref as _)?;
|
||||
}
|
||||
}
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
HostMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
encoder.encode_varint32(18u32)?;
|
||||
val_ref.encode_len_delimited(encoder)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
{
|
||||
let val_ref = &self.r#id;
|
||||
if *val_ref != 0 {
|
||||
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
|
||||
}
|
||||
}
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
HostMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
size
|
||||
+= 1usize
|
||||
+ ::micropb::size::sizeof_len_record(
|
||||
val_ref.compute_size(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
pub mod TargetMessage_ {
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Msg {
|
||||
Echo(super::super::echo_::EchoResponse),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TargetMessage {
|
||||
pub r#id: u32,
|
||||
pub r#msg: ::core::option::Option<TargetMessage_::Msg>,
|
||||
}
|
||||
impl ::core::default::Default for TargetMessage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r#id: ::core::default::Default::default(),
|
||||
r#msg: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for TargetMessage {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret &= (self.r#id == other.r#id);
|
||||
ret &= (self.r#msg == other.r#msg);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl TargetMessage {
|
||||
///Return a reference to `id`
|
||||
#[inline]
|
||||
pub fn r#id(&self) -> &u32 {
|
||||
&self.r#id
|
||||
}
|
||||
///Return a mutable reference to `id`
|
||||
#[inline]
|
||||
pub fn mut_id(&mut self) -> &mut u32 {
|
||||
&mut self.r#id
|
||||
}
|
||||
///Set the value of `id`
|
||||
#[inline]
|
||||
pub fn set_id(&mut self, value: u32) -> &mut Self {
|
||||
self.r#id = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `id`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_id(mut self, value: u32) -> Self {
|
||||
self.r#id = value.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageDecode for TargetMessage {
|
||||
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
|
||||
&mut self,
|
||||
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
|
||||
len: usize,
|
||||
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
|
||||
let before = decoder.bytes_read();
|
||||
while decoder.bytes_read() - before < len {
|
||||
let tag = decoder.decode_tag()?;
|
||||
match tag.field_num() {
|
||||
0 => return Err(::micropb::DecodeError::ZeroField),
|
||||
1u32 => {
|
||||
let mut_ref = &mut self.r#id;
|
||||
{
|
||||
let val = decoder.decode_varint32()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref != 0 {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
2u32 => {
|
||||
let mut_ref = loop {
|
||||
if let ::core::option::Option::Some(variant) = &mut self
|
||||
.r#msg
|
||||
{
|
||||
if let TargetMessage_::Msg::Echo(variant) = &mut *variant {
|
||||
break &mut *variant;
|
||||
}
|
||||
}
|
||||
self.r#msg = ::core::option::Option::Some(
|
||||
TargetMessage_::Msg::Echo(
|
||||
::core::default::Default::default(),
|
||||
),
|
||||
);
|
||||
};
|
||||
mut_ref.decode_len_delimited(decoder)?;
|
||||
}
|
||||
_ => {
|
||||
decoder.skip_wire_value(tag.wire_type())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageEncode for TargetMessage {
|
||||
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
|
||||
&self,
|
||||
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
|
||||
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
{
|
||||
let val_ref = &self.r#id;
|
||||
if *val_ref != 0 {
|
||||
encoder.encode_varint32(8u32)?;
|
||||
encoder.encode_varint32(*val_ref as _)?;
|
||||
}
|
||||
}
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
TargetMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
encoder.encode_varint32(18u32)?;
|
||||
val_ref.encode_len_delimited(encoder)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
{
|
||||
let val_ref = &self.r#id;
|
||||
if *val_ref != 0 {
|
||||
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
|
||||
}
|
||||
}
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
TargetMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
size
|
||||
+= 1usize
|
||||
+ ::micropb::size::sizeof_len_record(
|
||||
val_ref.compute_size(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
4
firmware/app/src/tasks.rs
Normal file
4
firmware/app/src/tasks.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod heat_pump_interface;
|
||||
pub mod host_interface;
|
||||
pub mod supervisor;
|
||||
pub mod thermostat_interface;
|
||||
57
firmware/app/src/tasks/heat_pump_interface.rs
Normal file
57
firmware/app/src/tasks/heat_pump_interface.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::usart;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
struct UartBuf {
|
||||
buffer: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for UartBuf {
|
||||
fn default() -> UartBuf {
|
||||
unsafe { core::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
static UART_TX_BUF: StaticCell<UartBuf> = StaticCell::new();
|
||||
static UART_RX_BUF: StaticCell<UartBuf> = StaticCell::new();
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Tx {
|
||||
uart: usart::BufferedUartTx<'static>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Rx {
|
||||
uart: usart::BufferedUartRx<'static>,
|
||||
}
|
||||
|
||||
pub fn start(r: crate::HeatPumpInterfaceResources, spawner: Spawner) {
|
||||
let tx_buf = UART_TX_BUF.init(UartBuf::default());
|
||||
let rx_buf = UART_RX_BUF.init(UartBuf::default());
|
||||
|
||||
let mut config = usart::Config::default();
|
||||
config.baudrate = 2400;
|
||||
config.parity = usart::Parity::ParityEven;
|
||||
config.tx_config = usart::OutputConfig::OpenDrain;
|
||||
|
||||
let (uart_tx, uart_rx) = usart::BufferedUart::new(
|
||||
r.usart,
|
||||
r.rx_pin,
|
||||
r.tx_pin,
|
||||
&mut tx_buf.buffer,
|
||||
&mut rx_buf.buffer,
|
||||
crate::Irqs,
|
||||
config,
|
||||
)
|
||||
.unwrap()
|
||||
.split();
|
||||
|
||||
spawner.must_spawn(heat_pump_tx_task(Tx { uart: uart_tx }));
|
||||
spawner.must_spawn(heat_pump_rx_task(Rx { uart: uart_rx }));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn heat_pump_tx_task(_tx: Tx) {}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn heat_pump_rx_task(_rx: Rx) {}
|
||||
197
firmware/app/src/tasks/host_interface/mod.rs
Normal file
197
firmware/app/src/tasks/host_interface/mod.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
use crate::io::framed::Receiver;
|
||||
use crate::{crc, io, proto};
|
||||
use defmt::*;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::{gpio, usart};
|
||||
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||
use embassy_sync::channel;
|
||||
use embassy_time::Duration;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use micropb::{DecodeError, heapless::Vec};
|
||||
|
||||
use crate::proto::api_::*;
|
||||
use crate::proto::echo_::*;
|
||||
|
||||
const MAX_HOST_MESSAGE_SIZE: usize =
|
||||
io::proto::MESSAGE_LENGTH_SIZE + io::proto::MESSAGE_CRC_SIZE + 2048;
|
||||
|
||||
struct UartBuf {
|
||||
buffer: [u8; 128],
|
||||
}
|
||||
|
||||
impl Default for UartBuf {
|
||||
fn default() -> UartBuf {
|
||||
unsafe { core::mem::zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
static UART_TX_BUF: StaticCell<UartBuf> = StaticCell::new();
|
||||
static UART_RX_BUF: StaticCell<UartBuf> = StaticCell::new();
|
||||
|
||||
type MessageBuf = Vec<u8, MAX_HOST_MESSAGE_SIZE>;
|
||||
|
||||
static MESSAGE_BUF: StaticCell<MessageBuf> = StaticCell::new();
|
||||
|
||||
type TargetMessageChannel = channel::Channel<ThreadModeRawMutex, proto::api_::TargetMessage, 3>;
|
||||
static TARGET_MESSAGE_CHANNEL: TargetMessageChannel = channel::Channel::new();
|
||||
|
||||
struct Tx {
|
||||
uart: usart::BufferedUartTx<'static>,
|
||||
crc: crc::Handle,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Rx {
|
||||
message_buf: &'static mut MessageBuf,
|
||||
uart: usart::BufferedUartRx<'static>,
|
||||
crc: crc::Handle,
|
||||
clr_host_transmit: gpio::OutputOpenDrain<'static>,
|
||||
set_host_transmit: gpio::OutputOpenDrain<'static>,
|
||||
host_active_led: gpio::OutputOpenDrain<'static>,
|
||||
}
|
||||
|
||||
pub fn start(r: crate::HostInterfaceResources, spawner: Spawner, crc: crc::Handle) {
|
||||
let tx_buf = UART_TX_BUF.init(UartBuf::default());
|
||||
let rx_buf = UART_RX_BUF.init(UartBuf::default());
|
||||
let message_buf = MESSAGE_BUF.init(MessageBuf::new());
|
||||
|
||||
let mut config = usart::Config::default();
|
||||
config.baudrate = 38400;
|
||||
|
||||
let (uart_tx, uart_rx) = usart::BufferedUart::new(
|
||||
r.usart,
|
||||
r.rx_pin,
|
||||
r.tx_pin,
|
||||
&mut tx_buf.buffer,
|
||||
&mut rx_buf.buffer,
|
||||
crate::Irqs,
|
||||
config,
|
||||
)
|
||||
.unwrap()
|
||||
.split();
|
||||
|
||||
let clr_host_transmit =
|
||||
gpio::OutputOpenDrain::new(r.clr_host_transmit, gpio::Level::High, gpio::Speed::Low);
|
||||
let set_host_transmit =
|
||||
gpio::OutputOpenDrain::new(r.set_host_transmit, gpio::Level::High, gpio::Speed::Low);
|
||||
let host_active_led =
|
||||
gpio::OutputOpenDrain::new(r.host_active_led, gpio::Level::High, gpio::Speed::Low);
|
||||
|
||||
spawner.must_spawn(host_tx_task(Tx { uart: uart_tx, crc }));
|
||||
spawner.must_spawn(host_rx_task(Rx {
|
||||
message_buf,
|
||||
uart: uart_rx,
|
||||
crc,
|
||||
clr_host_transmit,
|
||||
set_host_transmit,
|
||||
host_active_led,
|
||||
}));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn host_tx_task(tx: Tx) {
|
||||
let mut ftx = io::framed::WriteSender::new(tx.uart, io::proto::SYNC_BYTE);
|
||||
let receiver = TARGET_MESSAGE_CHANNEL.receiver();
|
||||
|
||||
loop {
|
||||
let msg = receiver.receive().await;
|
||||
|
||||
debug!("sending message");
|
||||
let _ = io::proto::send_target_message(&mut ftx, msg, tx.crc).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn host_rx_task(rx: Rx) {
|
||||
let mut frx = io::framed::AsyncReadReceiver::new(
|
||||
rx.message_buf,
|
||||
rx.uart,
|
||||
io::proto::SYNC_BYTE,
|
||||
Duration::from_millis(250),
|
||||
Duration::from_millis(50),
|
||||
);
|
||||
|
||||
loop {
|
||||
match io::proto::receive_host_message(&mut frx, rx.crc).await {
|
||||
Ok(msg) => {
|
||||
debug!("message received");
|
||||
handle_host_message(msg).await;
|
||||
}
|
||||
Err(io::proto::ReceiveError::Sync) => {
|
||||
error!("sync error");
|
||||
}
|
||||
Err(io::proto::ReceiveError::Crc) => {
|
||||
error!("CRC error");
|
||||
}
|
||||
Err(io::proto::ReceiveError::Framing(e)) => {
|
||||
match e {
|
||||
io::framed::AsyncReadReceiverError::BufferCapacity => {
|
||||
// the host message will not fit in the
|
||||
// buffer, so clear the buffer and restart the
|
||||
// loop
|
||||
error!("message too large");
|
||||
frx.clear_buf();
|
||||
}
|
||||
io::framed::AsyncReadReceiverError::Timeout => {
|
||||
// restart loop, since the sync byte seen wasn't
|
||||
// the beginning of a message, or the sender
|
||||
// stopped sending
|
||||
error!("timeout");
|
||||
}
|
||||
io::framed::AsyncReadReceiverError::Other(_) => {
|
||||
// restart loop when a USART error occurs
|
||||
error!("USART error");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(io::proto::ReceiveError::Decode(e)) => match e {
|
||||
DecodeError::VarIntLimit => {
|
||||
error!("decoder: VarInt too long");
|
||||
}
|
||||
DecodeError::UnexpectedEof => {
|
||||
error!("decoder: unexpected EOF");
|
||||
}
|
||||
DecodeError::Deprecation => {
|
||||
error!("decoder: deprecated wire type found");
|
||||
}
|
||||
DecodeError::UnknownWireType => {
|
||||
error!("decoder: unknown wire type found");
|
||||
}
|
||||
DecodeError::ZeroField => {
|
||||
error!("decoder: invalid field number 0");
|
||||
}
|
||||
DecodeError::Utf8 => {
|
||||
error!("decoder: invalid UTF-8 string");
|
||||
}
|
||||
DecodeError::Capacity => {
|
||||
error!("decoder: capacity of fixed-length field exceeed");
|
||||
}
|
||||
DecodeError::WrongLen => {
|
||||
error!("decoder: length-delimited-record too long");
|
||||
}
|
||||
_ => {
|
||||
error!("decoder: unknown");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_host_message(host: HostMessage) {
|
||||
match host.msg {
|
||||
None => {
|
||||
error!("empty message");
|
||||
}
|
||||
Some(HostMessage_::Msg::Echo(_)) => {
|
||||
info!("echo request");
|
||||
TARGET_MESSAGE_CHANNEL
|
||||
.sender()
|
||||
.send(TargetMessage {
|
||||
id: host.id,
|
||||
msg: Some(TargetMessage_::Msg::Echo(EchoResponse {})),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
firmware/app/src/tasks/supervisor.rs
Normal file
30
firmware/app/src/tasks/supervisor.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use defmt::*;
|
||||
#[cfg(feature = "status-led")]
|
||||
use embassy_stm32::gpio;
|
||||
use embassy_stm32::{peripherals, wdg};
|
||||
#[cfg(feature = "status-led")]
|
||||
use embassy_time::Timer;
|
||||
use embassy_time::{Duration, Ticker};
|
||||
|
||||
#[cfg(feature = "status-led")]
|
||||
#[embassy_executor::task]
|
||||
pub async fn status_task(mut led: gpio::Output<'static>) {
|
||||
let mut ticker = Ticker::every(Duration::from_millis(1500));
|
||||
loop {
|
||||
ticker.next().await;
|
||||
led.toggle();
|
||||
Timer::after_millis(100).await;
|
||||
led.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn watchdog_task(mut wdg: wdg::IndependentWatchdog<'static, peripherals::IWDG>) {
|
||||
let mut ticker = Ticker::every(Duration::from_secs(1));
|
||||
wdg.unleash();
|
||||
loop {
|
||||
ticker.next().await;
|
||||
wdg.pet();
|
||||
trace!("watchdog");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue