Compare commits
No commits in common. "main" and "crc-stream" have entirely different histories.
main
...
crc-stream
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) {}
|
||||
12
firmware/src/utils.rs
Normal file
12
firmware/src/utils.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use micropb::heapless::Vec;
|
||||
|
||||
pub fn remove_leading_bytes<const N: usize>(buf: &mut Vec<u8, N>, remove: usize) {
|
||||
let retain_bytes = buf.len() - remove;
|
||||
#[cfg(not(feature = "copy-within"))]
|
||||
for i in 0..retain_bytes {
|
||||
buf[i] = buf[remove + i];
|
||||
}
|
||||
#[cfg(feature = "copy-within")]
|
||||
buf.copy_within(remove.., 0);
|
||||
buf.truncate(retain_bytes);
|
||||
}
|
||||
83
hw-notes.md
Normal file
83
hw-notes.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
UART1
|
||||
|
||||
TX - PA9 (pin 19 - FT_fd), PB6 (pin 30 - FT_fa)
|
||||
RX - PA10 (pin 21 - FT_fd), PB7 (pin 31 - FT_fa)
|
||||
|
||||
UART2
|
||||
|
||||
on Nucleo64 board this UART is connected to the ST-LINK interface
|
||||
|
||||
TX - PA2 (pin 9 - FT_a), PA14-BOOT0 (pin 25 - FT)
|
||||
RX - PA3 (pin 10 - FT_a), PA15 (pin 26 - FT)
|
||||
|
||||
UART3
|
||||
|
||||
TX - PA5 (pin 12 - TT_a), PB2 (pin 17 - FT_a), PB8 (pin 32 - FT_f)
|
||||
RX - PB0 (pin 15 - FT_a), PB9 (pin 1 - FT_f)
|
||||
|
||||
UART4
|
||||
|
||||
TX - PA0 (pin 7 - FT_a)
|
||||
RX - PA1 (pin 8 - FT_a)
|
||||
|
||||
All UART pins are FT except for PA5, so 5V-to-3V3 level translators will not be required.
|
||||
|
||||
UART assignments
|
||||
|
||||
Host - UART1 on PA9/PA10 (allows use of embedded boot loader)
|
||||
HP - UART4 on PA0/PA1
|
||||
TH - UART3 on PB2/PB0
|
||||
|
||||
# Pin Usage
|
||||
|
||||
|Pin|Function|Name|Configuration/Connection|
|
||||
|:---:|:---:|:---:|:---|
|
||||
|01|PB9|unused|digital input with internal pull-up|
|
||||
|02|PC14|CLR\_HOST\_TRANSMIT|open-drain output with external pull-up to 5V, reset combiner AND gate|
|
||||
|03|PC15|SET\_HOST\_TRANSMIT|open-drain output with external pull-up to 5V, S input of S/R latch|
|
||||
|04|VDD|||
|
||||
|05|VSS|||
|
||||
|06|NRST||internal pull-up, SWD header and host|
|
||||
|07|PA0 - USART4\_TX|MCU\_TO\_HP|open-drain output with external pull-up to 5V, 4-channel UART buffer|
|
||||
|08|PA1 - USART4\_RX|FROM\_HP|digital input, HP CN105 connector|
|
||||
|09|PA2|unused|digital input with internal pull-up|
|
||||
|10|PA3|unused|digital input with internal pull-up|
|
||||
|11|PA4|unused|digital input with internal pull-up|
|
||||
|12|PA5|STATUS\_LED|output, LED on Nucleo64|
|
||||
|13|PA6|unused|digital input with internal pull-up|
|
||||
|14|PA7|unused|digital input with internal pull-up|
|
||||
|15|PB0 - USART3\_RX|FROM\_TH|digital input, TH CN105 connector|
|
||||
|16|PB1|unused|digital input with internal pull-up|
|
||||
|17|PB2 - USART3\_TX|MCU\_TO\_TH|open-drain output with external pull-up to 5V, 4-channel UART buffer|
|
||||
|18|PA8|unused|digital input with internal pull-up|
|
||||
|19|PA9 - USART1\_TX|MCU\_TO\_HOST|digital output, host connector|
|
||||
|20|PC6|unused|digital input with internal pull-up|
|
||||
|21|PA10 - USART1\_RX|HOST\_TO\_MCU|digital input, host connector|
|
||||
|22|PA11|unused|digital input with internal pull-up|
|
||||
|23|PA12|unused|digital input with internal pull-up|
|
||||
|24|PA13 - SWDIO|SWDIO|SWD header|
|
||||
|25|PA14 - BOOT0|SWCLK-BOOT0|SWD header and host|
|
||||
|26|PA15|unused|digital input with internal pull-up|
|
||||
|27|PB3|unused|digital input with internal pull-up|
|
||||
|28|PB4|unused|digital input with internal pull-up|
|
||||
|29|PB5|unused|digital input with internal pull-up|
|
||||
|30|PB6|unused|digital input with internal pull-up|
|
||||
|31|PB7|HOST\_ACTIVE|open-drain output|
|
||||
|32|PB8|unused|digital input with internal pull-up|
|
||||
|
||||
# Power Budget
|
||||
|
||||
All estimates using worst-case conditions from datasheets.
|
||||
|
||||
|Device|5V current (μA)|3V3 current (μA)|Notes|
|
||||
|---:|---:|---:|:---:|
|
||||
|STM32G070KB||9,400|Computed using STM32CubeMX|
|
||||
|SN74AHCT125|20||all inputs at 5V or GND|
|
||||
|SN74AHCT1G00|10||all inputs at 5V or GND|
|
||||
|SN74AHCT1G00|10||all inputs at 5V or GND|
|
||||
|SN74AHCT1G08|1,510||10μA for base consumption plus 1,500μA for one input at 3V3|
|
||||
|LTST-C193KFKT-5A|3,000||power|
|
||||
|LTST-C193KFKT-5A|3,000||HOST\_TRANSMIT|
|
||||
|LTST-C193KFKT-5A|3,000||HOST\_ACTIVE|
|
||||
|TLV70033|9,475||75μA for regulator current plus 9,400μA for output current|
|
||||
|Totals|20,025|9,400||
|
||||
19
proto/api.proto
Normal file
19
proto/api.proto
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package api;
|
||||
|
||||
import "echo.proto";
|
||||
import "test.proto";
|
||||
|
||||
message HostMessage {
|
||||
oneof msg {
|
||||
echo.EchoRequest echo = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message TargetMessage {
|
||||
oneof msg {
|
||||
echo.EchoResponse echo = 1;
|
||||
test.TestResponse test = 2;
|
||||
}
|
||||
}
|
||||
11
proto/echo.proto
Normal file
11
proto/echo.proto
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package echo;
|
||||
|
||||
message EchoRequest {
|
||||
uint32 data = 1;
|
||||
}
|
||||
|
||||
message EchoResponse {
|
||||
uint32 data = 1;
|
||||
}
|
||||
10
proto/test.proto
Normal file
10
proto/test.proto
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package test;
|
||||
|
||||
message TestResponse {
|
||||
uint32 f1 = 1;
|
||||
string f2 = 2;
|
||||
bool f3 = 3;
|
||||
bytes f4 = 4;
|
||||
}
|
||||
29
prototest/.mypy.ini
Normal file
29
prototest/.mypy.ini
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[mypy]
|
||||
files = prototest.py
|
||||
|
||||
check_untyped_defs = true
|
||||
|
||||
no_implicit_optional = true
|
||||
|
||||
strict_equality = true
|
||||
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
[mypy-api_pb2]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-test_pb2]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-echo_pb2]
|
||||
ignore_errors = True
|
||||
46
prototest/.ruff.toml
Normal file
46
prototest/.ruff.toml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
target-version = "py313"
|
||||
line-length = 100
|
||||
exclude = [ "*_pb2.py", "*_pb2.pyi" ]
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"A",
|
||||
"ASYNC",
|
||||
"B",
|
||||
"C4",
|
||||
"E",
|
||||
"F",
|
||||
"FBT",
|
||||
"FURB",
|
||||
"G",
|
||||
"I",
|
||||
"ICN",
|
||||
"ISC",
|
||||
"LOG",
|
||||
"N",
|
||||
"PERF",
|
||||
"PIE",
|
||||
"PL",
|
||||
"PTH",
|
||||
"PYI",
|
||||
"Q",
|
||||
"RSE",
|
||||
"RET",
|
||||
"RUF",
|
||||
"SIM",
|
||||
"SLF",
|
||||
"TID",
|
||||
"TRY",
|
||||
"UP",
|
||||
]
|
||||
ignore = [
|
||||
"PLR0912",
|
||||
"PLR0915",
|
||||
]
|
||||
unfixable= [
|
||||
"F401",
|
||||
]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
21
prototest/Makefile
Normal file
21
prototest/Makefile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
PROTO_DIR = ../proto
|
||||
|
||||
proto_inputs = $(wildcard $(PROTO_DIR)/*.proto)
|
||||
proto_outputs = $(patsubst %.proto,%_pb2.py,$(notdir $(proto_inputs)))
|
||||
|
||||
all: $(proto_outputs)
|
||||
|
||||
%_pb2.py: $(PROTO_DIR)/%.proto
|
||||
@protoc --proto_path=$(PROTO_DIR) --python_out=. --pyi_out=. $^
|
||||
|
||||
.PHONY: lint lint-fix
|
||||
|
||||
lint:
|
||||
@ruff format --diff
|
||||
@ruff check
|
||||
@mypy
|
||||
|
||||
lint-fix:
|
||||
@ruff format
|
||||
@ruff check --fix
|
||||
@mypy
|
||||
29
prototest/api_pb2.py
Normal file
29
prototest/api_pb2.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: api.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import echo_pb2 as echo__pb2
|
||||
import test_pb2 as test__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tapi.proto\x12\x03\x61pi\x1a\necho.proto\x1a\ntest.proto\"7\n\x0bHostMessage\x12!\n\x04\x65\x63ho\x18\x01 \x01(\x0b\x32\x11.echo.EchoRequestH\x00\x42\x05\n\x03msg\"^\n\rTargetMessage\x12\"\n\x04\x65\x63ho\x18\x01 \x01(\x0b\x32\x12.echo.EchoResponseH\x00\x12\"\n\x04test\x18\x02 \x01(\x0b\x32\x12.test.TestResponseH\x00\x42\x05\n\x03msgb\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'api_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_HOSTMESSAGE._serialized_start=42
|
||||
_HOSTMESSAGE._serialized_end=97
|
||||
_TARGETMESSAGE._serialized_start=99
|
||||
_TARGETMESSAGE._serialized_end=193
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
21
prototest/api_pb2.pyi
Normal file
21
prototest/api_pb2.pyi
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import echo_pb2 as _echo_pb2
|
||||
import test_pb2 as _test_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class HostMessage(_message.Message):
|
||||
__slots__ = ["echo"]
|
||||
ECHO_FIELD_NUMBER: _ClassVar[int]
|
||||
echo: _echo_pb2.EchoRequest
|
||||
def __init__(self, echo: _Optional[_Union[_echo_pb2.EchoRequest, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class TargetMessage(_message.Message):
|
||||
__slots__ = ["echo", "test"]
|
||||
ECHO_FIELD_NUMBER: _ClassVar[int]
|
||||
TEST_FIELD_NUMBER: _ClassVar[int]
|
||||
echo: _echo_pb2.EchoResponse
|
||||
test: _test_pb2.TestResponse
|
||||
def __init__(self, echo: _Optional[_Union[_echo_pb2.EchoResponse, _Mapping]] = ..., test: _Optional[_Union[_test_pb2.TestResponse, _Mapping]] = ...) -> None: ...
|
||||
27
prototest/echo_pb2.py
Normal file
27
prototest/echo_pb2.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: echo.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\necho.proto\x12\x04\x65\x63ho\"\x1b\n\x0b\x45\x63hoRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\r\"\x1c\n\x0c\x45\x63hoResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\rb\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'echo_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_ECHOREQUEST._serialized_start=20
|
||||
_ECHOREQUEST._serialized_end=47
|
||||
_ECHORESPONSE._serialized_start=49
|
||||
_ECHORESPONSE._serialized_end=77
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
17
prototest/echo_pb2.pyi
Normal file
17
prototest/echo_pb2.pyi
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class EchoRequest(_message.Message):
|
||||
__slots__ = ["data"]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
data: int
|
||||
def __init__(self, data: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class EchoResponse(_message.Message):
|
||||
__slots__ = ["data"]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
data: int
|
||||
def __init__(self, data: _Optional[int] = ...) -> None: ...
|
||||
344
prototest/prototest.py
Executable file
344
prototest/prototest.py
Executable file
|
|
@ -0,0 +1,344 @@
|
|||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.13"
|
||||
# dependencies = [
|
||||
# "fastcrc",
|
||||
# "prompt-toolkit",
|
||||
# "protobuf",
|
||||
# "pyserial",
|
||||
# "pyserial-asyncio-fast",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import struct
|
||||
|
||||
import fastcrc
|
||||
import serial
|
||||
import serial_asyncio_fast
|
||||
|
||||
from google.protobuf.message import DecodeError
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
from prompt_toolkit.formatted_text import AnyFormattedText
|
||||
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from api_pb2 import HostMessage, TargetMessage
|
||||
|
||||
|
||||
SYNC_BYTE = b"\xfc"
|
||||
MESSAGE_LENGTH_SIZE = 2
|
||||
CRC_SIZE = 2
|
||||
|
||||
uint16_le = struct.Struct("<H")
|
||||
|
||||
incoming_messages = asyncio.Queue[TargetMessage]()
|
||||
outgoing_messages = asyncio.Queue[HostMessage]()
|
||||
echo_response_queue = asyncio.Queue[int](maxsize=1)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def proto_listener(stream: asyncio.StreamReader) -> None:
|
||||
input_buf = bytearray()
|
||||
|
||||
try:
|
||||
while True:
|
||||
# look for a sync byte in the buffer; the buffer may have
|
||||
# leftover bytes from a previous cycle through this loop
|
||||
# (due to an incomplete message, or a message which failed
|
||||
# validation)
|
||||
pre, sync, post = input_buf.partition(SYNC_BYTE)
|
||||
# if a sync byte was found, reset the buffer to the data
|
||||
# following it; otherwise, wait for a sync byte to arrive
|
||||
if len(sync) != 0:
|
||||
input_buf = post
|
||||
else:
|
||||
b = await stream.readexactly(1)
|
||||
if b != SYNC_BYTE:
|
||||
continue
|
||||
|
||||
logger.debug("got sync byte")
|
||||
|
||||
# sync byte was seen, read the length of the message
|
||||
# payload; if there aren't enough bytes in the buffer,
|
||||
# wait for them, with a timeout.
|
||||
if (buf_bytes := len(input_buf)) < MESSAGE_LENGTH_SIZE:
|
||||
try:
|
||||
b = await asyncio.wait_for(
|
||||
stream.readexactly(MESSAGE_LENGTH_SIZE - buf_bytes), timeout=0.5
|
||||
)
|
||||
|
||||
except TimeoutError:
|
||||
# restart loop, since the sync byte seen wasn't
|
||||
# the beginning of a message, or the sender
|
||||
# stopped sending
|
||||
#
|
||||
# Note: this assumes that stream.readexactly will
|
||||
# not consume any bytes from the stream's internal
|
||||
# buffer if the operation is cancelled
|
||||
logger.debug("timeout reading message length")
|
||||
continue
|
||||
|
||||
else:
|
||||
input_buf.extend(b)
|
||||
|
||||
# get the message length from the first bytes of the
|
||||
# buffer
|
||||
message_length = uint16_le.unpack(input_buf[0:MESSAGE_LENGTH_SIZE])[0]
|
||||
|
||||
# if there aren't enough bytes in the buffer for the
|
||||
# message, wait for them to arrive, with a timeout
|
||||
if (buf_bytes := len(input_buf[MESSAGE_LENGTH_SIZE:])) < message_length:
|
||||
try:
|
||||
# a loop is necessary here so that the timeout
|
||||
# will apply to each byte, not to the entire
|
||||
# sequence of bytes needed
|
||||
for _i in range(message_length - buf_bytes):
|
||||
b = await asyncio.wait_for(stream.readexactly(1), timeout=0.25)
|
||||
input_buf.extend(b)
|
||||
|
||||
except TimeoutError:
|
||||
# restart loop, since the sync byte seen wasn't
|
||||
# the beginning of a message, or the sender
|
||||
# stopped sending
|
||||
#
|
||||
# Note: this assumes that stream.readexactly will
|
||||
# not consume any bytes from the stream's internal
|
||||
# buffer if the operation is cancelled
|
||||
logger.debug("timeout reading message payload")
|
||||
continue
|
||||
|
||||
# message payload has been read, read the CRC; if there
|
||||
# aren't enough bytes in the buffer, wait for them, with a
|
||||
# timeout.
|
||||
if (buf_bytes := len(input_buf[MESSAGE_LENGTH_SIZE + message_length :])) < CRC_SIZE:
|
||||
try:
|
||||
b = await asyncio.wait_for(
|
||||
stream.readexactly(CRC_SIZE - buf_bytes), timeout=0.5
|
||||
)
|
||||
|
||||
except TimeoutError:
|
||||
# restart loop, since the sync byte seen wasn't
|
||||
# the beginning of a message, or the sender
|
||||
# stopped sending
|
||||
#
|
||||
# Note: this assumes that stream.readexactly will
|
||||
# not consume any bytes from the stream's internal
|
||||
# buffer if the operation is cancelled
|
||||
logger.debug("timeout reading message CRC")
|
||||
continue
|
||||
|
||||
else:
|
||||
input_buf.extend(b)
|
||||
|
||||
# at this point there is a potentially-complete-and-valid
|
||||
# message in input_buf, so check its CRC to confirm
|
||||
message_bytes = bytes(input_buf[MESSAGE_LENGTH_SIZE:-CRC_SIZE])
|
||||
expected_crc = uint16_le.unpack(input_buf[-CRC_SIZE:])[0]
|
||||
message_crc = fastcrc.crc16.spi_fujitsu(message_bytes)
|
||||
if message_crc != expected_crc:
|
||||
logger.error(
|
||||
"CRC mismatch: expected 0x%04x - received 0x%04x", expected_crc, message_crc
|
||||
)
|
||||
continue
|
||||
|
||||
input_buf.clear()
|
||||
|
||||
msg = TargetMessage()
|
||||
|
||||
try:
|
||||
msg.ParseFromString(message_bytes)
|
||||
|
||||
except DecodeError:
|
||||
logger.error("Message could not be decoded, dropping") # noqa: TRY400
|
||||
continue
|
||||
|
||||
logger.debug("Received: %s", msg)
|
||||
await incoming_messages.put(msg)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Protobuf Listener task cancelled.")
|
||||
|
||||
|
||||
async def proto_sender(stream: asyncio.StreamWriter) -> None:
|
||||
try:
|
||||
while True:
|
||||
msg = await outgoing_messages.get()
|
||||
|
||||
logger.debug("Sending: %s", msg)
|
||||
|
||||
output_buf = bytearray()
|
||||
|
||||
output_buf.extend(SYNC_BYTE)
|
||||
|
||||
msg_buf = msg.SerializeToString()
|
||||
|
||||
output_buf.extend(uint16_le.pack(len(msg_buf)))
|
||||
|
||||
output_buf.extend(msg_buf)
|
||||
|
||||
message_crc = fastcrc.crc16.spi_fujitsu(msg_buf)
|
||||
output_buf.extend(uint16_le.pack(message_crc))
|
||||
|
||||
stream.write(output_buf)
|
||||
await stream.drain()
|
||||
|
||||
outgoing_messages.task_done()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Protobuf Sender task cancelled.")
|
||||
|
||||
|
||||
class InvalidMessageError(Exception):
|
||||
"""Indicates that a malformed or unsupported Protocol Buffers message was received."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Invalid Message: {super()}"
|
||||
|
||||
|
||||
def invalid_message(msg: str) -> None:
|
||||
raise InvalidMessageError(msg)
|
||||
|
||||
|
||||
async def message_handler() -> None:
|
||||
try:
|
||||
while True:
|
||||
msg = await incoming_messages.get()
|
||||
|
||||
try:
|
||||
if not msg.HasField("msg"):
|
||||
invalid_message("missing 'msg' field")
|
||||
|
||||
await handle_target_message(msg)
|
||||
|
||||
except InvalidMessageError as ex:
|
||||
logger.error(ex) # noqa: TRY400
|
||||
|
||||
incoming_messages.task_done()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Message Handler task cancelled.")
|
||||
|
||||
|
||||
async def handle_target_message(tgt: TargetMessage) -> None:
|
||||
match tgt.WhichOneof("msg"):
|
||||
case "echo":
|
||||
await echo_response_queue.put(tgt.echo.data)
|
||||
|
||||
case "test":
|
||||
pass
|
||||
|
||||
case _:
|
||||
invalid_message("response: unknown type")
|
||||
|
||||
|
||||
async def message_sender() -> None:
|
||||
try:
|
||||
while True:
|
||||
msg = HostMessage()
|
||||
msg.echo.data = random.randint(0, 2**22)
|
||||
|
||||
await outgoing_messages.put(msg)
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(echo_response_queue.get(), timeout=2)
|
||||
|
||||
except TimeoutError:
|
||||
logger.error("Timeout waiting for echo response") # noqa: TRY400
|
||||
|
||||
else:
|
||||
if response != msg.echo.data:
|
||||
logger.error(
|
||||
"Incorrect echo response: expected %d - got %d", msg.echo.data, response
|
||||
)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Message Sender task cancelled.")
|
||||
|
||||
|
||||
async def shell() -> None:
|
||||
bindings = KeyBindings()
|
||||
|
||||
@bindings.add("f4")
|
||||
def _(event: KeyPressEvent) -> None:
|
||||
app = event.app
|
||||
|
||||
if app.editing_mode == EditingMode.VI:
|
||||
app.editing_mode = EditingMode.EMACS
|
||||
else:
|
||||
app.editing_mode = EditingMode.VI
|
||||
|
||||
@bindings.add("f12")
|
||||
def _(event: KeyPressEvent) -> None:
|
||||
if logger.level == logging.DEBUG:
|
||||
logger.setLevel(logging.INFO)
|
||||
else:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
def bottom_toolbar() -> AnyFormattedText:
|
||||
edit_mode = "Vi" if get_app().editing_mode == EditingMode.VI else "Emacs"
|
||||
debug_mode = "Debug" if logger.level == logging.DEBUG else "Info"
|
||||
return [("class:toolbar", f" [F4] {edit_mode} | [F12] {debug_mode} ")]
|
||||
|
||||
session = PromptSession[str](
|
||||
"prototest> ",
|
||||
enable_history_search=True,
|
||||
key_bindings=bindings,
|
||||
bottom_toolbar=bottom_toolbar,
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
result = await session.prompt_async()
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
raise SystemExit(0) from None
|
||||
|
||||
else:
|
||||
print(f'You said: "{result}"')
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace) -> None:
|
||||
with patch_stdout() as out:
|
||||
logging.basicConfig(
|
||||
stream=out, format="%(asctime)s %(levelname)s:%(message)s", level=logging.INFO
|
||||
)
|
||||
|
||||
reader, writer = await serial_asyncio_fast.open_serial_connection(
|
||||
url=args.protobuf_interface,
|
||||
baudrate=38400,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
)
|
||||
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tg.create_task(proto_listener(reader))
|
||||
tg.create_task(proto_sender(writer))
|
||||
tg.create_task(message_handler())
|
||||
tg.create_task(message_sender())
|
||||
tg.create_task(shell())
|
||||
|
||||
logger.info("Quitting.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="prototest",
|
||||
description="Tool for testing muart-failsafe protobuf interface.",
|
||||
epilog="",
|
||||
)
|
||||
|
||||
parser.add_argument("protobuf_interface", help="Serial port connected to protobuf interface")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args))
|
||||
25
prototest/test_pb2.py
Normal file
25
prototest/test_pb2.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: test.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ntest.proto\x12\x04test\">\n\x0cTestResponse\x12\n\n\x02\x66\x31\x18\x01 \x01(\r\x12\n\n\x02\x66\x32\x18\x02 \x01(\t\x12\n\n\x02\x66\x33\x18\x03 \x01(\x08\x12\n\n\x02\x66\x34\x18\x04 \x01(\x0c\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'test_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_TESTRESPONSE._serialized_start=20
|
||||
_TESTRESPONSE._serialized_end=82
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
25
prototest/test_pb2.pyi
Normal file
25
prototest/test_pb2.pyi
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from typing import ClassVar as _ClassVar
|
||||
from typing import Optional as _Optional
|
||||
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class TestResponse(_message.Message):
|
||||
__slots__ = ["f1", "f2", "f3", "f4"]
|
||||
F1_FIELD_NUMBER: _ClassVar[int]
|
||||
F2_FIELD_NUMBER: _ClassVar[int]
|
||||
F3_FIELD_NUMBER: _ClassVar[int]
|
||||
F4_FIELD_NUMBER: _ClassVar[int]
|
||||
f1: int
|
||||
f2: str
|
||||
f3: bool
|
||||
f4: bytes
|
||||
def __init__(
|
||||
self,
|
||||
f1: int | None = ...,
|
||||
f2: str | None = ...,
|
||||
f3: bool = ...,
|
||||
f4: bytes | None = ...,
|
||||
) -> None: ...
|
||||
Loading…
Add table
Add a link
Reference in a new issue