Compare commits
35 changed files with 3616 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)
|
||||
17
firmware/.cargo/config.toml
Normal file
17
firmware/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||
runner = [
|
||||
"probe-rs",
|
||||
"run",
|
||||
"--chip",
|
||||
"STM32G070KB",
|
||||
"--log-format",
|
||||
"{t} {m} {s}",
|
||||
]
|
||||
|
||||
[build]
|
||||
target = "thumbv6m-none-eabi"
|
||||
rustdocflags = ["--document-private-items"]
|
||||
|
||||
[env]
|
||||
# for tracing inside Embassy crates
|
||||
DEFMT_LOG = "trace"
|
||||
1
firmware/.gitignore
vendored
Normal file
1
firmware/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1111
firmware/Cargo.lock
generated
Normal file
1111
firmware/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
74
firmware/Cargo.toml
Normal file
74
firmware/Cargo.toml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
[package]
|
||||
name = "muart-failsafe"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
embassy-executor = { version = "0.7.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 = "0.7.0"
|
||||
embassy-time = { version = "0.4.0", features = ["tick-hz-32_768"] }
|
||||
|
||||
byteorder = { version = "1.5.0", default-features = false }
|
||||
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.0"
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-io = "0.6.1"
|
||||
embedded-io-async = { version = "0.6.1" }
|
||||
embedded-resources = { version = "0.2.1", features = ["stm32"] }
|
||||
portable-atomic = { version = "1.5", features = ["unsafe-assume-single-core"] }
|
||||
static_cell = "2.1.0"
|
||||
|
||||
thiserror = { version = "2.0.12", default-features = false }
|
||||
micropb = { version = "0.1.1", default-features = false, features = [ "encode", "decode", "container-heapless" ] }
|
||||
|
||||
defmt = { version = "1.0.0", optional = true }
|
||||
defmt-rtt = { version = "1.0.0", optional = true }
|
||||
panic-probe = { version = "1.0.0", optional = true }
|
||||
panic-halt = "1.0.0"
|
||||
|
||||
[build-dependencies]
|
||||
micropb-gen = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = ["board-nucleo64", "status-led", "trace"]
|
||||
board-nucleo64 = []
|
||||
status-led = []
|
||||
copy-within = []
|
||||
trace = [
|
||||
"dep:defmt",
|
||||
"dep:defmt-rtt",
|
||||
"dep:panic-probe",
|
||||
"panic-probe/print-defmt",
|
||||
"embassy-stm32/defmt",
|
||||
"embassy-executor/defmt",
|
||||
"embassy-time/defmt",
|
||||
"embassy-time/defmt-timestamp-uptime",
|
||||
"embassy-futures/defmt"
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
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
|
||||
|
||||
[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
|
||||
42
firmware/build.rs
Normal file
42
firmware/build.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use micropb_gen::{Config, Generator};
|
||||
|
||||
fn proto_generate() {
|
||||
let firmware_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let root_dir = firmware_dir.parent().unwrap();
|
||||
|
||||
let proto_dir = Path::new(root_dir).join("proto");
|
||||
|
||||
let target_dir = firmware_dir.join("src");
|
||||
|
||||
let mut g = Generator::new();
|
||||
g.use_container_heapless()
|
||||
.configure(".test.TestResponse.f2", Config::new().max_bytes(16))
|
||||
.configure(".test.TestResponse.f4", Config::new().max_bytes(8))
|
||||
.add_protoc_arg(format!("-I{}", proto_dir.display()))
|
||||
.compile_protos(
|
||||
&[
|
||||
proto_dir.join("api.proto"),
|
||||
proto_dir.join("echo.proto"),
|
||||
proto_dir.join("test.proto"),
|
||||
],
|
||||
target_dir.join("proto.rs"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", proto_dir.display());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-arg-bins=--nmagic");
|
||||
println!("cargo:rustc-link-arg-bins=-Tlink.x");
|
||||
|
||||
if env::var("CARGO_FEATURE_TRACE").is_ok() {
|
||||
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
|
||||
}
|
||||
|
||||
proto_generate();
|
||||
}
|
||||
5
firmware/memory.x
Normal file
5
firmware/memory.x
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
MEMORY
|
||||
{
|
||||
FLASH : ORIGIN = 0x08000000, LENGTH = 128K /* BANK_1 */
|
||||
RAM : ORIGIN = 0x20000000, LENGTH = 36K /* SRAM */
|
||||
}
|
||||
5
firmware/rust-toolchain.toml
Normal file
5
firmware/rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
profile = "default"
|
||||
components = [ "llvm-tools" ]
|
||||
targets = [ "thumbv6m-none-eabi" ]
|
||||
2
firmware/rustfmt.toml
Normal file
2
firmware/rustfmt.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
edition = "2024"
|
||||
style_edition = "2024"
|
||||
64
firmware/src/crc.rs
Normal file
64
firmware/src/crc.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use embassy_stm32::crc;
|
||||
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||
use embassy_sync::mutex;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
struct Shared<'a> {
|
||||
inner: mutex::Mutex<ThreadModeRawMutex, crc::Crc<'a>>,
|
||||
}
|
||||
|
||||
static STORAGE: StaticCell<Shared> = StaticCell::new();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Handle {
|
||||
shared: &'static Shared<'static>,
|
||||
}
|
||||
|
||||
struct Stream<'a> {
|
||||
guard: mutex::MutexGuard<'a, ThreadModeRawMutex, crc::Crc<'a>>,
|
||||
}
|
||||
|
||||
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 shared_ref = STORAGE.init(Shared {
|
||||
inner: mutex::Mutex::new(crc::Crc::new(r.crc_engine, cfg)),
|
||||
});
|
||||
|
||||
Handle { shared: shared_ref }
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub async fn compute(&self, data: &[u8]) -> u16 {
|
||||
let mut guard = self.shared.inner.lock().await;
|
||||
let engine = &mut *guard;
|
||||
|
||||
engine.reset();
|
||||
engine.feed_bytes(data) as u16
|
||||
}
|
||||
|
||||
pub async fn stream(&self) -> Stream {
|
||||
let mut guard = self.shared.inner.lock().await;
|
||||
let engine = &mut *guard;
|
||||
|
||||
engine.reset();
|
||||
Stream { guard }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream<'_> {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn compute(&mut self, data: &[u8]) -> u16 {
|
||||
let engine = &mut *self.guard;
|
||||
|
||||
engine.feed_bytes(data) as u16
|
||||
}
|
||||
}
|
||||
142
firmware/src/io/framed_rx.rs
Normal file
142
firmware/src/io/framed_rx.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use crate::io;
|
||||
use crate::println;
|
||||
use crate::utils;
|
||||
use embassy_time::Duration;
|
||||
use embedded_io::ReadReady;
|
||||
use embedded_io_async::Read;
|
||||
use micropb::heapless::Vec;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FramingError<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 FrameReceiver<const N: usize, T>
|
||||
where
|
||||
T: Read + ReadReady,
|
||||
{
|
||||
pub buf: &'static mut Vec<u8, N>,
|
||||
rx: T,
|
||||
sync: u8,
|
||||
first_byte_timeout: Duration,
|
||||
between_bytes_timeout: Duration,
|
||||
}
|
||||
|
||||
impl<const N: usize, T> FrameReceiver<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,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_sync(&mut self) -> Result<(), FramingError<T>> {
|
||||
// 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 {
|
||||
println!("waiting for sync byte");
|
||||
match self.rx.read(&mut buf[..]).await {
|
||||
Ok(_) => {
|
||||
println!("rx byte {=u8:#x}", buf[0]);
|
||||
if buf[0] == self.sync {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("read error");
|
||||
return Err(FramingError::Other(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_frame_fragment(
|
||||
&mut self,
|
||||
fragment_pos: usize,
|
||||
fragment_len: usize,
|
||||
) -> Result<(), FramingError<T>> {
|
||||
// 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(FramingError::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(FramingError::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(FramingError::Other(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
firmware/src/io/mod.rs
Normal file
104
firmware/src/io/mod.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use embassy_futures::select::*;
|
||||
use embassy_time::{Duration, Timer};
|
||||
use embedded_io::ReadReady;
|
||||
use embedded_io_async::Read;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod framed_rx;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
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(())
|
||||
}
|
||||
212
firmware/src/lib.rs
Normal file
212
firmware/src/lib.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#![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 embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||
use embassy_sync::channel;
|
||||
use embedded_resources::resource_group;
|
||||
use static_cell::StaticCell;
|
||||
use tasks::*;
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
pub use defmt::println;
|
||||
#[cfg(feature = "trace")]
|
||||
use defmt_rtt as _;
|
||||
#[cfg(not(feature = "trace"))]
|
||||
#[macro_export]
|
||||
// Empty version of 'println' macro for use when the tracing feature
|
||||
// is not enabled; this ensures that all calls to 'println' are
|
||||
// compiled out.
|
||||
macro_rules! println {
|
||||
() => {};
|
||||
($($arg:tt)*) => {};
|
||||
}
|
||||
|
||||
#[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>;
|
||||
});
|
||||
|
||||
type TargetMessageChannel = channel::Channel<ThreadModeRawMutex, proto::api_::TargetMessage, 3>;
|
||||
type TargetMessageGenerator =
|
||||
channel::Sender<'static, ThreadModeRawMutex, proto::api_::TargetMessage, 3>;
|
||||
type TargetMessagePublisher =
|
||||
channel::Receiver<'static, ThreadModeRawMutex, proto::api_::TargetMessage, 3>;
|
||||
static TARGET_MESSAGE_CHANNEL: StaticCell<TargetMessageChannel> = StaticCell::new();
|
||||
|
||||
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));
|
||||
|
||||
let tgt_msg_channel = TARGET_MESSAGE_CHANNEL.init(TargetMessageChannel::new());
|
||||
|
||||
host_interface::start(
|
||||
host_interface_resources!(p),
|
||||
spawner,
|
||||
crc_handle,
|
||||
tgt_msg_channel.receiver(),
|
||||
tgt_msg_channel.sender(),
|
||||
);
|
||||
|
||||
heat_pump_interface::start(heat_pump_interface_resources!(p), spawner);
|
||||
|
||||
thermostat_interface::start(thermostat_interface_resources!(p), spawner);
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
15
firmware/src/main.rs
Normal file
15
firmware/src/main.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
|
||||
#[cfg(not(feature = "trace"))]
|
||||
use panic_halt as _;
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
use panic_probe as _;
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
muart_failsafe::spawn_tasks(spawner);
|
||||
}
|
||||
664
firmware/src/proto.rs
Normal file
664
firmware/src/proto.rs
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
pub mod echo_ {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EchoRequest {
|
||||
pub r#data: u32,
|
||||
}
|
||||
impl ::core::default::Default for EchoRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r#data: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for EchoRequest {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret &= (self.r#data == other.r#data);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl EchoRequest {
|
||||
///Return a reference to `data`
|
||||
#[inline]
|
||||
pub fn r#data(&self) -> &u32 {
|
||||
&self.r#data
|
||||
}
|
||||
///Return a mutable reference to `data`
|
||||
#[inline]
|
||||
pub fn mut_data(&mut self) -> &mut u32 {
|
||||
&mut self.r#data
|
||||
}
|
||||
///Set the value of `data`
|
||||
#[inline]
|
||||
pub fn set_data(&mut self, value: u32) -> &mut Self {
|
||||
self.r#data = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `data`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_data(mut self, value: u32) -> Self {
|
||||
self.r#data = value.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
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),
|
||||
1u32 => {
|
||||
let mut_ref = &mut self.r#data;
|
||||
{
|
||||
let val = decoder.decode_varint32()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref != 0 {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
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};
|
||||
{
|
||||
let val_ref = &self.r#data;
|
||||
if *val_ref != 0 {
|
||||
encoder.encode_varint32(8u32)?;
|
||||
encoder.encode_varint32(*val_ref as _)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
{
|
||||
let val_ref = &self.r#data;
|
||||
if *val_ref != 0 {
|
||||
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EchoResponse {
|
||||
pub r#data: u32,
|
||||
}
|
||||
impl ::core::default::Default for EchoResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r#data: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for EchoResponse {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret &= (self.r#data == other.r#data);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl EchoResponse {
|
||||
///Return a reference to `data`
|
||||
#[inline]
|
||||
pub fn r#data(&self) -> &u32 {
|
||||
&self.r#data
|
||||
}
|
||||
///Return a mutable reference to `data`
|
||||
#[inline]
|
||||
pub fn mut_data(&mut self) -> &mut u32 {
|
||||
&mut self.r#data
|
||||
}
|
||||
///Set the value of `data`
|
||||
#[inline]
|
||||
pub fn set_data(&mut self, value: u32) -> &mut Self {
|
||||
self.r#data = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `data`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_data(mut self, value: u32) -> Self {
|
||||
self.r#data = value.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
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),
|
||||
1u32 => {
|
||||
let mut_ref = &mut self.r#data;
|
||||
{
|
||||
let val = decoder.decode_varint32()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref != 0 {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
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};
|
||||
{
|
||||
let val_ref = &self.r#data;
|
||||
if *val_ref != 0 {
|
||||
encoder.encode_varint32(8u32)?;
|
||||
encoder.encode_varint32(*val_ref as _)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
{
|
||||
let val_ref = &self.r#data;
|
||||
if *val_ref != 0 {
|
||||
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod test_ {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestResponse {
|
||||
pub r#f1: u32,
|
||||
pub r#f2: ::micropb::heapless::String<16>,
|
||||
pub r#f3: bool,
|
||||
pub r#f4: ::micropb::heapless::Vec<u8, 8>,
|
||||
}
|
||||
impl ::core::default::Default for TestResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r#f1: ::core::default::Default::default(),
|
||||
r#f2: ::core::default::Default::default(),
|
||||
r#f3: ::core::default::Default::default(),
|
||||
r#f4: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ::core::cmp::PartialEq for TestResponse {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let mut ret = true;
|
||||
ret &= (self.r#f1 == other.r#f1);
|
||||
ret &= (self.r#f2 == other.r#f2);
|
||||
ret &= (self.r#f3 == other.r#f3);
|
||||
ret &= (self.r#f4 == other.r#f4);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl TestResponse {
|
||||
///Return a reference to `f1`
|
||||
#[inline]
|
||||
pub fn r#f1(&self) -> &u32 {
|
||||
&self.r#f1
|
||||
}
|
||||
///Return a mutable reference to `f1`
|
||||
#[inline]
|
||||
pub fn mut_f1(&mut self) -> &mut u32 {
|
||||
&mut self.r#f1
|
||||
}
|
||||
///Set the value of `f1`
|
||||
#[inline]
|
||||
pub fn set_f1(&mut self, value: u32) -> &mut Self {
|
||||
self.r#f1 = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `f1`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_f1(mut self, value: u32) -> Self {
|
||||
self.r#f1 = value.into();
|
||||
self
|
||||
}
|
||||
///Return a reference to `f2`
|
||||
#[inline]
|
||||
pub fn r#f2(&self) -> &::micropb::heapless::String<16> {
|
||||
&self.r#f2
|
||||
}
|
||||
///Return a mutable reference to `f2`
|
||||
#[inline]
|
||||
pub fn mut_f2(&mut self) -> &mut ::micropb::heapless::String<16> {
|
||||
&mut self.r#f2
|
||||
}
|
||||
///Set the value of `f2`
|
||||
#[inline]
|
||||
pub fn set_f2(&mut self, value: ::micropb::heapless::String<16>) -> &mut Self {
|
||||
self.r#f2 = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `f2`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_f2(mut self, value: ::micropb::heapless::String<16>) -> Self {
|
||||
self.r#f2 = value.into();
|
||||
self
|
||||
}
|
||||
///Return a reference to `f3`
|
||||
#[inline]
|
||||
pub fn r#f3(&self) -> &bool {
|
||||
&self.r#f3
|
||||
}
|
||||
///Return a mutable reference to `f3`
|
||||
#[inline]
|
||||
pub fn mut_f3(&mut self) -> &mut bool {
|
||||
&mut self.r#f3
|
||||
}
|
||||
///Set the value of `f3`
|
||||
#[inline]
|
||||
pub fn set_f3(&mut self, value: bool) -> &mut Self {
|
||||
self.r#f3 = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `f3`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_f3(mut self, value: bool) -> Self {
|
||||
self.r#f3 = value.into();
|
||||
self
|
||||
}
|
||||
///Return a reference to `f4`
|
||||
#[inline]
|
||||
pub fn r#f4(&self) -> &::micropb::heapless::Vec<u8, 8> {
|
||||
&self.r#f4
|
||||
}
|
||||
///Return a mutable reference to `f4`
|
||||
#[inline]
|
||||
pub fn mut_f4(&mut self) -> &mut ::micropb::heapless::Vec<u8, 8> {
|
||||
&mut self.r#f4
|
||||
}
|
||||
///Set the value of `f4`
|
||||
#[inline]
|
||||
pub fn set_f4(&mut self, value: ::micropb::heapless::Vec<u8, 8>) -> &mut Self {
|
||||
self.r#f4 = value.into();
|
||||
self
|
||||
}
|
||||
///Builder method that sets the value of `f4`. Useful for initializing the message.
|
||||
#[inline]
|
||||
pub fn init_f4(mut self, value: ::micropb::heapless::Vec<u8, 8>) -> Self {
|
||||
self.r#f4 = value.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageDecode for TestResponse {
|
||||
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#f1;
|
||||
{
|
||||
let val = decoder.decode_varint32()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref != 0 {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
2u32 => {
|
||||
let mut_ref = &mut self.r#f2;
|
||||
{
|
||||
decoder
|
||||
.decode_string(mut_ref, ::micropb::Presence::Implicit)?;
|
||||
};
|
||||
}
|
||||
3u32 => {
|
||||
let mut_ref = &mut self.r#f3;
|
||||
{
|
||||
let val = decoder.decode_bool()?;
|
||||
let val_ref = &val;
|
||||
if *val_ref {
|
||||
*mut_ref = val as _;
|
||||
}
|
||||
};
|
||||
}
|
||||
4u32 => {
|
||||
let mut_ref = &mut self.r#f4;
|
||||
{
|
||||
decoder
|
||||
.decode_bytes(mut_ref, ::micropb::Presence::Implicit)?;
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
decoder.skip_wire_value(tag.wire_type())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ::micropb::MessageEncode for TestResponse {
|
||||
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#f1;
|
||||
if *val_ref != 0 {
|
||||
encoder.encode_varint32(8u32)?;
|
||||
encoder.encode_varint32(*val_ref as _)?;
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f2;
|
||||
if !val_ref.is_empty() {
|
||||
encoder.encode_varint32(18u32)?;
|
||||
encoder.encode_string(val_ref)?;
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f3;
|
||||
if *val_ref {
|
||||
encoder.encode_varint32(24u32)?;
|
||||
encoder.encode_bool(*val_ref)?;
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f4;
|
||||
if !val_ref.is_empty() {
|
||||
encoder.encode_varint32(34u32)?;
|
||||
encoder.encode_bytes(val_ref)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
{
|
||||
let val_ref = &self.r#f1;
|
||||
if *val_ref != 0 {
|
||||
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f2;
|
||||
if !val_ref.is_empty() {
|
||||
size += 1usize + ::micropb::size::sizeof_len_record(val_ref.len());
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f3;
|
||||
if *val_ref {
|
||||
size += 1usize + 1;
|
||||
}
|
||||
}
|
||||
{
|
||||
let val_ref = &self.r#f4;
|
||||
if !val_ref.is_empty() {
|
||||
size += 1usize + ::micropb::size::sizeof_len_record(val_ref.len());
|
||||
}
|
||||
}
|
||||
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#msg: ::core::option::Option<HostMessage_::Msg>,
|
||||
}
|
||||
impl ::core::default::Default for HostMessage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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#msg == other.r#msg);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl HostMessage {}
|
||||
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 = 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};
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
HostMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
encoder.encode_varint32(10u32)?;
|
||||
val_ref.encode_len_delimited(encoder)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn compute_size(&self) -> usize {
|
||||
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
|
||||
let mut size = 0;
|
||||
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),
|
||||
Test(super::super::test_::TestResponse),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TargetMessage {
|
||||
pub r#msg: ::core::option::Option<TargetMessage_::Msg>,
|
||||
}
|
||||
impl ::core::default::Default for TargetMessage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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#msg == other.r#msg);
|
||||
ret
|
||||
}
|
||||
}
|
||||
impl TargetMessage {}
|
||||
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 = 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)?;
|
||||
}
|
||||
2u32 => {
|
||||
let mut_ref = loop {
|
||||
if let ::core::option::Option::Some(variant) = &mut self
|
||||
.r#msg
|
||||
{
|
||||
if let TargetMessage_::Msg::Test(variant) = &mut *variant {
|
||||
break &mut *variant;
|
||||
}
|
||||
}
|
||||
self.r#msg = ::core::option::Option::Some(
|
||||
TargetMessage_::Msg::Test(
|
||||
::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};
|
||||
if let Some(oneof) = &self.r#msg {
|
||||
match &*oneof {
|
||||
TargetMessage_::Msg::Echo(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
encoder.encode_varint32(10u32)?;
|
||||
val_ref.encode_len_delimited(encoder)?;
|
||||
}
|
||||
TargetMessage_::Msg::Test(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;
|
||||
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(),
|
||||
);
|
||||
}
|
||||
TargetMessage_::Msg::Test(val_ref) => {
|
||||
let val_ref = &*val_ref;
|
||||
size
|
||||
+= 1usize
|
||||
+ ::micropb::size::sizeof_len_record(
|
||||
val_ref.compute_size(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
4
firmware/src/tasks.rs
Normal file
4
firmware/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;
|
||||
51
firmware/src/tasks/heat_pump_interface.rs
Normal file
51
firmware/src/tasks/heat_pump_interface.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::usart;
|
||||
use micropb::heapless::Vec;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
type UartRxBuf = Vec<u8, 32>;
|
||||
type UartTxBuf = Vec<u8, 32>;
|
||||
|
||||
static UART_RX_BUF: StaticCell<UartRxBuf> = StaticCell::new();
|
||||
static UART_TX_BUF: StaticCell<UartTxBuf> = 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 rx_buf = UART_RX_BUF.init(UartRxBuf::new());
|
||||
let tx_buf = UART_TX_BUF.init(UartTxBuf::new());
|
||||
|
||||
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,
|
||||
tx_buf,
|
||||
rx_buf,
|
||||
crate::Irqs,
|
||||
config,
|
||||
)
|
||||
.unwrap()
|
||||
.split();
|
||||
|
||||
spawner.must_spawn(tx_task(Tx { uart: uart_tx }));
|
||||
spawner.must_spawn(rx_task(Rx { uart: uart_rx }));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn tx_task(_tx: Tx) {}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn rx_task(_rx: Rx) {}
|
||||
305
firmware/src/tasks/host_interface/mod.rs
Normal file
305
firmware/src/tasks/host_interface/mod.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
use crate::{crc, io, println, utils};
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::{gpio, usart};
|
||||
use embassy_time::Duration;
|
||||
use embedded_io_async::Write;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use micropb::{DecodeError, MessageDecode, MessageEncode, PbDecoder, PbEncoder, heapless::Vec};
|
||||
|
||||
use crate::proto::api_::*;
|
||||
use crate::proto::echo_::*;
|
||||
|
||||
const SYNC_BYTE: u8 = 0xfc;
|
||||
const MESSAGE_LENGTH_SIZE: usize = 2;
|
||||
const MESSAGE_CRC_SIZE: usize = 2;
|
||||
const MAX_INBOUND_MESSAGE_SIZE: usize = MESSAGE_LENGTH_SIZE + 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_INBOUND_MESSAGE_SIZE>;
|
||||
|
||||
static MESSAGE_BUF: StaticCell<MessageBuf> = StaticCell::new();
|
||||
|
||||
struct Tx {
|
||||
uart: usart::BufferedUartTx<'static>,
|
||||
channel: crate::TargetMessagePublisher,
|
||||
crc: crc::Handle,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Rx {
|
||||
message_buf: &'static mut MessageBuf,
|
||||
uart: usart::BufferedUartRx<'static>,
|
||||
channel: crate::TargetMessageGenerator,
|
||||
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,
|
||||
publisher: crate::TargetMessagePublisher,
|
||||
generator: crate::TargetMessageGenerator,
|
||||
) {
|
||||
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(tx_task(Tx {
|
||||
uart: uart_tx,
|
||||
channel: publisher,
|
||||
crc,
|
||||
}));
|
||||
spawner.must_spawn(rx_task(Rx {
|
||||
message_buf,
|
||||
uart: uart_rx,
|
||||
channel: generator,
|
||||
crc,
|
||||
clr_host_transmit,
|
||||
set_host_transmit,
|
||||
host_active_led,
|
||||
}));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn tx_task(mut tx: Tx) {
|
||||
println!("tx_task started");
|
||||
let mut buf = Vec::<u8, 128>::new();
|
||||
|
||||
loop {
|
||||
let msg = tx.channel.receive().await;
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let encoded_size = (msg.compute_size() as u16).to_le_bytes();
|
||||
|
||||
buf.clear();
|
||||
|
||||
unsafe {
|
||||
// sync byte
|
||||
buf.push_unchecked(SYNC_BYTE);
|
||||
// two bytes for length of message (not including CRC)
|
||||
buf.push_unchecked(encoded_size[0]);
|
||||
buf.push_unchecked(encoded_size[1]);
|
||||
}
|
||||
|
||||
let mut encoder = PbEncoder::new(&mut buf);
|
||||
msg.encode(&mut encoder).unwrap();
|
||||
|
||||
let crc = (tx.crc.compute(&buf[3..]).await).to_le_bytes();
|
||||
buf.push(crc[0]).unwrap();
|
||||
buf.push(crc[1]).unwrap();
|
||||
|
||||
tx.uart.write_all(&buf).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn rx_task(rx: Rx) {
|
||||
println!("rx_task started");
|
||||
|
||||
let mut frx = io::framed_rx::FrameReceiver::new(
|
||||
rx.message_buf,
|
||||
rx.uart,
|
||||
SYNC_BYTE,
|
||||
Duration::from_millis(250),
|
||||
Duration::from_millis(50),
|
||||
);
|
||||
|
||||
loop {
|
||||
if frx.wait_for_sync().await.is_err() {
|
||||
println!("error while waiting for sync");
|
||||
continue;
|
||||
}
|
||||
println!("received sync byte from host");
|
||||
|
||||
// sync byte was seen, read the length of the message; if
|
||||
// there aren't enough bytes in the buffer, wait for them,
|
||||
// with timeouts
|
||||
match frx.read_frame_fragment(0, MESSAGE_LENGTH_SIZE).await {
|
||||
Ok(()) => {}
|
||||
Err(io::framed_rx::FramingError::BufferCapacity) => {
|
||||
println!("insufficient space to read message length");
|
||||
frx.buf.clear();
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Timeout) => {
|
||||
// restart loop, since the sync byte seen wasn't
|
||||
// the beginning of a message, or the sender
|
||||
// stopped sending
|
||||
println!("timeout waiting for message length");
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Other(_)) => {
|
||||
// restart loop when a USART error occurs
|
||||
println!("usart error waiting for message length");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// get the message length from the buffer
|
||||
let message_len = LittleEndian::read_u16(&frx.buf[..MESSAGE_LENGTH_SIZE]) as usize;
|
||||
println!("receiving message of {=u16} bytes", message_len as u16);
|
||||
|
||||
// read the message; if there aren't enough bytes in the
|
||||
// buffer for the message, wait for them, with timeouts
|
||||
let message_pos = MESSAGE_LENGTH_SIZE;
|
||||
match frx.read_frame_fragment(message_pos, message_len).await {
|
||||
Ok(()) => {}
|
||||
Err(io::framed_rx::FramingError::BufferCapacity) => {
|
||||
println!("insufficient space to read message body");
|
||||
frx.buf.clear();
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Timeout) => {
|
||||
// restart loop, since the sync byte seen wasn't
|
||||
// the beginning of a message, or the sender
|
||||
// stopped sending
|
||||
println!("timeout waiting for message body");
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Other(_)) => {
|
||||
// restart loop when a USART error occurs
|
||||
println!("usart error waiting for message length");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// read the CRC-16 of the message; if there aren't enough
|
||||
// bytes in the buffer, wait for them, with timeouts
|
||||
let crc_pos = message_pos + message_len;
|
||||
match frx.read_frame_fragment(crc_pos, MESSAGE_CRC_SIZE).await {
|
||||
Ok(()) => {}
|
||||
Err(io::framed_rx::FramingError::BufferCapacity) => {
|
||||
println!("insufficient space to read CRC-16");
|
||||
frx.buf.clear();
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Timeout) => {
|
||||
// restart loop, since the sync byte seen wasn't
|
||||
// the beginning of a message, or the sender
|
||||
// stopped sending
|
||||
println!("timeout waiting for message CRC-16");
|
||||
continue;
|
||||
}
|
||||
Err(io::framed_rx::FramingError::Other(_)) => {
|
||||
// restart loop when a USART error occurs
|
||||
println!("usart error waiting for message length");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = rx
|
||||
.crc
|
||||
.compute(&frx.buf[message_pos..message_pos + message_len])
|
||||
.await;
|
||||
|
||||
if computed_crc != expected_crc {
|
||||
println!(
|
||||
"CRC-16 mismatch: expected {=u16:#x} - computed {=u16:#x}",
|
||||
expected_crc, computed_crc
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("message frame is 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 = HostMessage::default();
|
||||
match msg.decode(&mut decoder, message_len) {
|
||||
Ok(()) => {
|
||||
println!("message decoded");
|
||||
handle_host_message(msg, rx.channel).await;
|
||||
}
|
||||
Err(DecodeError::VarIntLimit) => {
|
||||
println!("decoder error: VarInt too long");
|
||||
}
|
||||
Err(DecodeError::UnexpectedEof) => {
|
||||
println!("decoder error: unexpected EOF");
|
||||
}
|
||||
Err(DecodeError::Deprecation) => {
|
||||
println!("decoder error: deprecated wire type found");
|
||||
}
|
||||
Err(DecodeError::UnknownWireType) => {
|
||||
println!("decoder error: unknown wire type found");
|
||||
}
|
||||
Err(DecodeError::ZeroField) => {
|
||||
println!("decoder error: invalid field number 0");
|
||||
}
|
||||
Err(DecodeError::Utf8) => {
|
||||
println!("decoder error: invalid UTF-8 string");
|
||||
}
|
||||
Err(DecodeError::Capacity) => {
|
||||
println!("decoder error: capacity of fixed-length field exceeed");
|
||||
}
|
||||
Err(DecodeError::WrongLen) => {
|
||||
println!("decoder error: length-delimited-record too long");
|
||||
}
|
||||
Err(_) => {
|
||||
println!("decoder error: unknown");
|
||||
}
|
||||
}
|
||||
|
||||
utils::remove_leading_bytes(frx.buf, crc_pos + MESSAGE_CRC_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_host_message(host: HostMessage, generator: crate::TargetMessageGenerator) {
|
||||
match host.msg {
|
||||
None => {
|
||||
println!("message is empty, ignoring");
|
||||
}
|
||||
Some(HostMessage_::Msg::Echo(echo)) => {
|
||||
println!("replying to echo request with data: {=u32:#x}", echo.data);
|
||||
generator
|
||||
.send(TargetMessage {
|
||||
msg: Some(TargetMessage_::Msg::Echo(EchoResponse { data: echo.data })),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
firmware/src/tasks/supervisor.rs
Normal file
28
firmware/src/tasks/supervisor.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#[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();
|
||||
}
|
||||
}
|
||||
51
firmware/src/tasks/thermostat_interface.rs
Normal file
51
firmware/src/tasks/thermostat_interface.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::usart;
|
||||
use micropb::heapless::Vec;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
type UartRxBuf = Vec<u8, 32>;
|
||||
type UartTxBuf = Vec<u8, 32>;
|
||||
|
||||
static UART_RX_BUF: StaticCell<UartRxBuf> = StaticCell::new();
|
||||
static UART_TX_BUF: StaticCell<UartTxBuf> = 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::ThermostatInterfaceResources, spawner: Spawner) {
|
||||
let rx_buf = UART_RX_BUF.init(UartRxBuf::new());
|
||||
let tx_buf = UART_TX_BUF.init(UartTxBuf::new());
|
||||
|
||||
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,
|
||||
tx_buf,
|
||||
rx_buf,
|
||||
crate::Irqs,
|
||||
config,
|
||||
)
|
||||
.unwrap()
|
||||
.split();
|
||||
|
||||
spawner.must_spawn(tx_task(Tx { uart: uart_tx }));
|
||||
spawner.must_spawn(rx_task(Rx { uart: uart_rx }));
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn tx_task(_tx: Tx) {}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn rx_task(_rx: Rx) {}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue