initial commit
This commit is contained in:
commit
17a8e573c0
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
/.direnv
|
11
Cargo.lock
generated
Normal file
11
Cargo.lock
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mqtt-protocol"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial-comm"
|
||||||
|
version = "0.1.0"
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["serial-comm", "mqtt-protocol"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "rp2040 temperature sensor"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
8
controller/.cargo/config.toml
Normal file
8
controller/.cargo/config.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||||
|
runner = "probe-rs run --chip RP2040"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
||||||
|
|
||||||
|
[env]
|
||||||
|
DEFMT_LOG = "info"
|
1973
controller/Cargo.lock
generated
Normal file
1973
controller/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
controller/Cargo.toml
Normal file
68
controller/Cargo.toml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
[workspace]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
name = "controller"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
embassy-embedded-hal = { version = "0.2.0",features = ["defmt"] }
|
||||||
|
embassy-sync = { version = "0.6.0", features = ["defmt"] }
|
||||||
|
embassy-executor = { version = "0.6.3", features = [
|
||||||
|
"arch-cortex-m",
|
||||||
|
"task-arena-size-98304",
|
||||||
|
"executor-thread",
|
||||||
|
"executor-interrupt",
|
||||||
|
"defmt",
|
||||||
|
"integrated-timers",
|
||||||
|
] }
|
||||||
|
|
||||||
|
embassy-time = { version = "0.3.2", features = ["defmt", "defmt-timestamp-uptime"] }
|
||||||
|
embassy-rp = { version = "0.2.0", features = [
|
||||||
|
"defmt",
|
||||||
|
"unstable-pac",
|
||||||
|
"time-driver",
|
||||||
|
"critical-section-impl",
|
||||||
|
] }
|
||||||
|
embassy-usb = { version = "0.3.0", features = ["defmt"] }
|
||||||
|
embassy-net = { version = "0.4.0", features = [
|
||||||
|
"defmt",
|
||||||
|
"tcp",
|
||||||
|
"udp",
|
||||||
|
"dhcpv4",
|
||||||
|
"medium-ethernet",
|
||||||
|
] }
|
||||||
|
embassy-net-wiznet = { version = "0.1.0", features = ["defmt"] }
|
||||||
|
embassy-futures = "0.1.1"
|
||||||
|
cyw43 = { version = "0.2.0", features = ["defmt", "firmware-logs"] }
|
||||||
|
cyw43-pio = { version = "0.2.0", features = ["defmt", "overclock"] }
|
||||||
|
defmt = "0.3.8"
|
||||||
|
defmt-rtt = "0.4.1"
|
||||||
|
fixed = "1.28.0"
|
||||||
|
fixed-macro = "1.2.0"
|
||||||
|
|
||||||
|
cortex-m = { version = "0.7.7", features = ["inline-asm"] }
|
||||||
|
cortex-m-rt = "0.7.5"
|
||||||
|
panic-probe = { version = "0.3.2", features = ["print-defmt"] }
|
||||||
|
futures = { version = "0.3.31", default-features = false, features = [
|
||||||
|
"async-await",
|
||||||
|
"cfg-target-has-atomic",
|
||||||
|
] }
|
||||||
|
critical-section = "1.2.0"
|
||||||
|
portable-atomic = { version = "1.5", features = ["critical-section"] }
|
||||||
|
|
||||||
|
embedded-hal-async = "1.0.0"
|
||||||
|
embedded-hal-bus = { version = "0.2.0", features = ["async"] }
|
||||||
|
embedded-io-async = { version = "0.6.1", features = ["defmt-03"] }
|
||||||
|
embedded-storage = "0.3.1"
|
||||||
|
static_cell = "2.1.0"
|
||||||
|
log = "0.4"
|
||||||
|
pio-proc = "0.2.2"
|
||||||
|
pio = "0.2.1"
|
||||||
|
rand = { version = "0.8.5", default-features = false }
|
||||||
|
|
||||||
|
mqtt-protocol = { path = "../mqtt-protocol" }
|
||||||
|
serial-comm = { path = "../serial-comm" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = 2
|
110
controller/README.md
Normal file
110
controller/README.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Pico with Embassy
|
||||||
|
|
||||||
|
Read temperature values and write them to an MQTT topic.
|
||||||
|
|
||||||
|
The Raspberry Pi Pico is well supported by [embassy](https://embassy.dev), apart
|
||||||
|
from the bluetooth stack (for [legal]() reasons). As we do not use bluetooth for
|
||||||
|
communication we can ignore that.
|
||||||
|
|
||||||
|
## Flashing
|
||||||
|
|
||||||
|
There are two ways of getting the code onto the microcontroller.
|
||||||
|
|
||||||
|
### elf2uf2
|
||||||
|
|
||||||
|
Compile the binary as normal, convert it into a uf2 firmware which is flashable
|
||||||
|
to the pico with only an USB connection. Disadvantage of this apporach is that
|
||||||
|
it is more annoying to do and does not lend itself to debugging.
|
||||||
|
|
||||||
|
- Clone the [elf2uf2](https://github.com/rej696/elf2uf2) repository and follow
|
||||||
|
its instructions to compile the tool
|
||||||
|
- Compile the controller code with `cargo build --release`
|
||||||
|
- Convert the resulting binary with something like
|
||||||
|
`elf2uf2 target/thumbv6m-none-eabi/release/controller ./controller.uf2`
|
||||||
|
- Hold the bootselect button of the pico when plugging it in
|
||||||
|
- Copy the uf2 file to the mass storage device
|
||||||
|
|
||||||
|
### CMSIS-DAP
|
||||||
|
|
||||||
|
Use the [CMSIS-DAP](https://arm-software.github.io/CMSIS_5/DAP/html/index.html)
|
||||||
|
protocol for flashing and debugging.
|
||||||
|
|
||||||
|
The
|
||||||
|
[raspberry pi debug probe](https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html)
|
||||||
|
works well but anything implementing that protocol is fine.
|
||||||
|
|
||||||
|
![Debug Probe Setup](./debug-probe-setup.jpg) _Example setup with a Pico WH
|
||||||
|
(Pico W works as well, just a bit more annoying)_
|
||||||
|
|
||||||
|
- Setup debug probe
|
||||||
|
- Install [probe-rs](https://github.com/probe-rs/probe-rs/)
|
||||||
|
- Run `cargo run` and it should upload and logs be visible
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is done by sending commands across a serial connection. Only one
|
||||||
|
command is implemented for now: Set-config with two parameters.
|
||||||
|
|
||||||
|
A sample set-config command looks like this (not encoded yet): `SC ssid MyNet`.
|
||||||
|
It consists of three parts:
|
||||||
|
|
||||||
|
- `SC`: Command prefix, always the same.
|
||||||
|
- `ssid`: Configuration key.
|
||||||
|
- `MyNet`: Configuration value.
|
||||||
|
|
||||||
|
The following keys are recognized:
|
||||||
|
|
||||||
|
- `ssid`: Name of the network to connect to.
|
||||||
|
- `ssid_pw`: Password to connect to the network.
|
||||||
|
- `mqtt`: URL of the MQTT broker (must not use https).
|
||||||
|
- `client_id`: ID of the device (used for identifaction in MQTT).
|
||||||
|
|
||||||
|
A message needs to be encoded into its byte representation looking on a high
|
||||||
|
level as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
|<parameters>|<parameter-lengths>|<prefix>|<parameters>|
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Parameters` is an unsigned byte signifying the amount of parameters in the
|
||||||
|
message.
|
||||||
|
- `Parameter-Lengths`: Length of each parameter.
|
||||||
|
- `Prefix` is always two bytes long and encoded as UTF-8 (meaning it is
|
||||||
|
basically ASCII).
|
||||||
|
- `Parameters`: every parameter encodes is own length in its first byte,
|
||||||
|
followed by the actual data.
|
||||||
|
|
||||||
|
Taking the above ssid example this would lead to this (the prefix counts as its
|
||||||
|
own parameter):
|
||||||
|
|
||||||
|
```
|
||||||
|
0x03 0x02 0x04 0x05 0x53 0x43 0x73 0x73 0x69 0x64 0x4D 0x59 0x4E 0x65 0x74
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the COBS encoding is applied before sending it across the wire:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x10 0x03 0x02 0x04 0x05 0x53 0x43 0x73 0x73 0x69 0x64 0x4d 0x79 0x4e 0x65 0x74
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case (as in most) it merely adds two additional bytes (the last 0x00
|
||||||
|
byte is implied).
|
||||||
|
|
||||||
|
Sending this this command on linux, assuming the serial connection is accessible
|
||||||
|
on `/dev/ttyACM1`:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo -en "\x10\x03\x02\x04\x05\x53\x43\x73\x73\x69\x64\x4d\x79\x4e\x65\x74\x00" > /dev/ttyACM1
|
||||||
|
```
|
||||||
|
|
||||||
|
If listening to the serial connection, command acknowledgements like `OK` or
|
||||||
|
`ERROR` can be seen.
|
||||||
|
|
||||||
|
In order to decode a message, apply the above steps in reverse order.
|
||||||
|
|
||||||
|
All configuration is read on startup of the microcontroller.
|
||||||
|
|
||||||
|
## Outlook
|
||||||
|
|
||||||
|
- Pull the flash/config handling into separate library for testability
|
||||||
|
- Merge different ways of running tasks (join and tasks)
|
36
controller/build.rs
Normal file
36
controller/build.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//! This build script copies the `memory.x` file from the crate root into
|
||||||
|
//! a directory where the linker can always find it at build time.
|
||||||
|
//! For many projects this is optional, as the linker always searches the
|
||||||
|
//! project root directory -- wherever `Cargo.toml` is. However, if you
|
||||||
|
//! are using a workspace or have a more complicated build setup, this
|
||||||
|
//! build script becomes required. Additionally, by requesting that
|
||||||
|
//! Cargo re-run the build script whenever `memory.x` is changed,
|
||||||
|
//! updating `memory.x` ensures a rebuild of the application with the
|
||||||
|
//! new memory settings.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Put `memory.x` in our output directory and ensure it's
|
||||||
|
// on the linker search path.
|
||||||
|
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||||
|
File::create(out.join("memory.x"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(include_bytes!("memory.x"))
|
||||||
|
.unwrap();
|
||||||
|
println!("cargo:rustc-link-search={}", out.display());
|
||||||
|
|
||||||
|
// By default, Cargo will re-run a build script whenever
|
||||||
|
// any file in the project changes. By specifying `memory.x`
|
||||||
|
// here, we ensure the build script is only re-run when
|
||||||
|
// `memory.x` is changed.
|
||||||
|
println!("cargo:rerun-if-changed=memory.x");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-arg-bins=--nmagic");
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tlink.x");
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tlink-rp.x");
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
|
||||||
|
}
|
BIN
controller/cyw43-firmware/43439A0.bin
Normal file
BIN
controller/cyw43-firmware/43439A0.bin
Normal file
Binary file not shown.
BIN
controller/cyw43-firmware/43439A0_clm.bin
Normal file
BIN
controller/cyw43-firmware/43439A0_clm.bin
Normal file
Binary file not shown.
@ -0,0 +1,49 @@
|
|||||||
|
Permissive Binary License
|
||||||
|
|
||||||
|
Version 1.0, July 2019
|
||||||
|
|
||||||
|
Redistribution. Redistribution and use in binary form, without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1) Redistributions must reproduce the above copyright notice and the
|
||||||
|
following disclaimer in the documentation and/or other materials
|
||||||
|
provided with the distribution.
|
||||||
|
|
||||||
|
2) Unless to the extent explicitly permitted by law, no reverse
|
||||||
|
engineering, decompilation, or disassembly of this software is
|
||||||
|
permitted.
|
||||||
|
|
||||||
|
3) Redistribution as part of a software development kit must include the
|
||||||
|
accompanying file named <20>DEPENDENCIES<45> and any dependencies listed in
|
||||||
|
that file.
|
||||||
|
|
||||||
|
4) Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
Limited patent license. The copyright holders (and contributors) grant a
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free patent license to
|
||||||
|
make, have made, use, offer to sell, sell, import, and otherwise
|
||||||
|
transfer this software, where such license applies only to those patent
|
||||||
|
claims licensable by the copyright holders (and contributors) that are
|
||||||
|
necessarily infringed by this software. This patent license shall not
|
||||||
|
apply to any combinations that include this software. No hardware is
|
||||||
|
licensed hereunder.
|
||||||
|
|
||||||
|
If you institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the software
|
||||||
|
itself infringes your patent(s), then your rights granted under this
|
||||||
|
license shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
CONTRIBUTORS "AS IS." ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
9
controller/cyw43-firmware/README.md
Normal file
9
controller/cyw43-firmware/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# WiFi firmware
|
||||||
|
|
||||||
|
Firmware obtained from https://github.com/Infineon/wifi-host-driver/tree/master/WiFi_Host_Driver/resources/firmware/COMPONENT_43439
|
||||||
|
|
||||||
|
Licensed under the [Infineon Permissive Binary License](./LICENSE-permissive-binary-license-1.0.txt)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
* 2023-07-28: synced with `ad3bad0` - Update 43439 fw from 7.95.55 ot 7.95.62
|
BIN
controller/debug-probe-setup.jpg
Normal file
BIN
controller/debug-probe-setup.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 356 KiB |
5
controller/memory.x
Normal file
5
controller/memory.x
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MEMORY {
|
||||||
|
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
|
||||||
|
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
|
||||||
|
RAM : ORIGIN = 0x20000000, LENGTH = 256K
|
||||||
|
}
|
78
controller/src/byte_handler.rs
Normal file
78
controller/src/byte_handler.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use defmt::{error, info};
|
||||||
|
use embassy_rp::usb::{Driver, Instance};
|
||||||
|
use embassy_usb::{class::cdc_acm::CdcAcmClass, driver::EndpointError};
|
||||||
|
use serial_comm::{
|
||||||
|
cobs::SENTINEL,
|
||||||
|
protocol::{cmd::Cobs, error::ErrorCommand, ok::OkCommand},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cmd_handlers::{handle_cmd, PACKET_SIZE},
|
||||||
|
flash::Config,
|
||||||
|
usb_serial::DeviceClass,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Disconnected;
|
||||||
|
|
||||||
|
impl From<EndpointError> for Disconnected {
|
||||||
|
fn from(val: EndpointError) -> Self {
|
||||||
|
match val {
|
||||||
|
EndpointError::BufferOverflow => panic!("Buffer overflow"),
|
||||||
|
EndpointError::Disabled => Disconnected {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn cmd_task(class: &'static mut DeviceClass, config: &'static mut Config<'static>) -> ! {
|
||||||
|
loop {
|
||||||
|
class.wait_connection().await;
|
||||||
|
info!("Connected");
|
||||||
|
let _ = handle_bytes(class, config).await;
|
||||||
|
info!("Disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle incoming bytes over the usb-serial connection.
|
||||||
|
///
|
||||||
|
/// Always buffer data until you encounter the `SENTINEL` value. After that try to decode and
|
||||||
|
/// execute the received command.
|
||||||
|
pub async fn handle_bytes<'d, T: Instance + 'd>(
|
||||||
|
class: &mut CdcAcmClass<'d, Driver<'d, T>>,
|
||||||
|
config: &mut Config<'d>,
|
||||||
|
) -> Result<(), Disconnected> {
|
||||||
|
let mut cmd_buffer: [u8; PACKET_SIZE] = [0; PACKET_SIZE];
|
||||||
|
let mut cursor: usize = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut read_buf: [u8; 64] = [0; 64];
|
||||||
|
let n = class.read_packet(&mut read_buf).await?;
|
||||||
|
let data = &read_buf[..n];
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
cmd_buffer[cursor..cursor + n].copy_from_slice(data);
|
||||||
|
cursor += n;
|
||||||
|
if data[n - 1] == SENTINEL {
|
||||||
|
match handle_cmd(cmd_buffer, cursor, config) {
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Failed to handle command: {:?} - {:?}",
|
||||||
|
e,
|
||||||
|
cmd_buffer[0..cursor]
|
||||||
|
);
|
||||||
|
let err_cmd = ErrorCommand::new("Failed to handle command");
|
||||||
|
let (length, err_cmd): (usize, [u8; 128]) = err_cmd.as_cobs();
|
||||||
|
class.write_packet(&err_cmd[0..length]).await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let ok_cmd = OkCommand::new();
|
||||||
|
let (length, ok_cmd): (usize, [u8; 128]) = ok_cmd.as_cobs();
|
||||||
|
class.write_packet(&ok_cmd[0..length]).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cursor = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
controller/src/cmd_handlers/mod.rs
Normal file
72
controller/src/cmd_handlers/mod.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
//! Handle specific commands, comming in from usb-serial.
|
||||||
|
|
||||||
|
pub mod set_config;
|
||||||
|
|
||||||
|
use defmt::Format;
|
||||||
|
use serial_comm::{cobs, protocol::set_config::SetConfig};
|
||||||
|
|
||||||
|
use crate::flash::{Config, FlashError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Utf8Error(pub core::str::Utf8Error);
|
||||||
|
|
||||||
|
impl Format for Utf8Error {
|
||||||
|
fn format(&self, f: defmt::Formatter) {
|
||||||
|
defmt::write!(f, "Utf8Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Format)]
|
||||||
|
pub enum CmdError {
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
FlashError(FlashError),
|
||||||
|
NoSuchCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<core::str::Utf8Error> for CmdError {
|
||||||
|
fn from(err: core::str::Utf8Error) -> Self {
|
||||||
|
CmdError::Utf8Error(Utf8Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FlashError> for CmdError {
|
||||||
|
fn from(err: FlashError) -> Self {
|
||||||
|
CmdError::FlashError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// arbitrarily constrained, we know we don't need more for now
|
||||||
|
pub const PACKET_SIZE: usize = 256;
|
||||||
|
|
||||||
|
type CmdHandler = fn([u8; PACKET_SIZE], &mut Config) -> Result<(), CmdError>;
|
||||||
|
static CMD_HANDLERS: &[(&str, CmdHandler)] = &[(SetConfig::PREFIX, set_config::handle)];
|
||||||
|
|
||||||
|
/// Try decoding a command buffer and run it.
|
||||||
|
///
|
||||||
|
/// On failure return an ErrorCommand across the serial connection, otherwise an OkCommand.
|
||||||
|
pub fn handle_cmd(
|
||||||
|
cmd_buffer: [u8; PACKET_SIZE],
|
||||||
|
length: usize,
|
||||||
|
config: &mut Config<'_>,
|
||||||
|
) -> Result<(), CmdError> {
|
||||||
|
let (length, decoded_cmd): (usize, [u8; PACKET_SIZE]) = cobs::decode(cmd_buffer, length);
|
||||||
|
|
||||||
|
if length > 1 {
|
||||||
|
let header_length = (decoded_cmd[0] + 1) as usize;
|
||||||
|
let cmd_prefix = core::str::from_utf8(&decoded_cmd[header_length..header_length + 2])?;
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < CMD_HANDLERS.len() && CMD_HANDLERS[idx].0 != cmd_prefix {
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == CMD_HANDLERS.len() {
|
||||||
|
return Err(CmdError::NoSuchCmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = CMD_HANDLERS[idx].1;
|
||||||
|
handler(decoded_cmd, config)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
17
controller/src/cmd_handlers/set_config.rs
Normal file
17
controller/src/cmd_handlers/set_config.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use defmt::info;
|
||||||
|
use serial_comm::protocol::{cmd::Cmd, set_config::SetConfig};
|
||||||
|
|
||||||
|
use crate::{cmd_handlers::CmdError, flash::Config};
|
||||||
|
|
||||||
|
use super::PACKET_SIZE;
|
||||||
|
|
||||||
|
/// Handle a SetConfig command by writing the received key-value pair to flash storage.
|
||||||
|
pub fn handle(data: [u8; PACKET_SIZE], config: &mut Config<'_>) -> Result<(), CmdError> {
|
||||||
|
let mut cmd_buffer: [u8; PACKET_SIZE] = [0; PACKET_SIZE];
|
||||||
|
|
||||||
|
let set_config = SetConfig::from_bytes::<PACKET_SIZE>(data, &mut cmd_buffer)?;
|
||||||
|
config.write_config(set_config.key, set_config.value)?;
|
||||||
|
info!("set {} to {}", set_config.key, set_config.value);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
124
controller/src/flash.rs
Normal file
124
controller/src/flash.rs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
use defmt::{error, Format};
|
||||||
|
use embassy_rp::{
|
||||||
|
flash::{self, Async, Flash, ERASE_SIZE},
|
||||||
|
peripherals::{DMA_CH0, FLASH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::cmd_handlers::Utf8Error;
|
||||||
|
|
||||||
|
const ADDR_OFFSET: u32 = 0x100000;
|
||||||
|
const FLASH_SIZE: usize = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
pub const ENTRY_SIZE: u32 = ERASE_SIZE as u32;
|
||||||
|
const SSID_CONFIG: u32 = ADDR_OFFSET;
|
||||||
|
const SSID_PW_CONFIG: u32 = ADDR_OFFSET + ENTRY_SIZE;
|
||||||
|
const MQTT_CONFIG: u32 = ADDR_OFFSET + 2 * ENTRY_SIZE;
|
||||||
|
const CLIENT_ID_CONFIG: u32 = ADDR_OFFSET + 3 * ENTRY_SIZE;
|
||||||
|
|
||||||
|
pub const SSID_KEY: &str = "ssid";
|
||||||
|
pub const SSID_PW_KEY: &str = "ssid_pw";
|
||||||
|
pub const MQTT_KEY: &str = "mqtt";
|
||||||
|
pub const CLIENT_ID_KEY: &str = "client_id";
|
||||||
|
|
||||||
|
#[derive(Debug, Format)]
|
||||||
|
pub enum FlashError {
|
||||||
|
Internal(flash::Error),
|
||||||
|
Utf8Error(Utf8Error),
|
||||||
|
NoSuchKey,
|
||||||
|
ValueTooLong,
|
||||||
|
NoValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<flash::Error> for FlashError {
|
||||||
|
fn from(err: flash::Error) -> Self {
|
||||||
|
FlashError::Internal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<core::str::Utf8Error> for FlashError {
|
||||||
|
fn from(err: core::str::Utf8Error) -> Self {
|
||||||
|
FlashError::Utf8Error(Utf8Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write key-value data to flash storage for persistence and read it from there again.
|
||||||
|
pub struct Config<'a> {
|
||||||
|
flash: Flash<'a, FLASH, Async, FLASH_SIZE>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Config<'a> {
|
||||||
|
/// Initialize the flash module on the RP2040.
|
||||||
|
pub fn init(flash: FLASH, dma_ch0: DMA_CH0) -> Self {
|
||||||
|
let flash = embassy_rp::flash::Flash::<_, Async, FLASH_SIZE>::new(flash, dma_ch0);
|
||||||
|
|
||||||
|
Self { flash }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position on flash for a configuration key.
|
||||||
|
fn get_config_cursor(&self, key: &str) -> Option<u32> {
|
||||||
|
match key {
|
||||||
|
SSID_KEY => Some(SSID_CONFIG),
|
||||||
|
SSID_PW_KEY => Some(SSID_PW_CONFIG),
|
||||||
|
MQTT_KEY => Some(MQTT_CONFIG),
|
||||||
|
CLIENT_ID_KEY => Some(CLIENT_ID_CONFIG),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a key-value pair to flash storage.
|
||||||
|
///
|
||||||
|
/// The key must be one of the predefined ones and the value length can not be longer than
|
||||||
|
/// `ENTRY_SIZE`.
|
||||||
|
///
|
||||||
|
/// Before writing, the whole sector must be erased, otherwise garbage writes can occur. For
|
||||||
|
/// that reason `ENTRY_SIZE` is set to the ERASE_SIZE of the RP2040 (4096 bits), otherwise we
|
||||||
|
/// would get misaligned erasures.
|
||||||
|
///
|
||||||
|
/// Each key-vaue pair is encoded as [<value_length>,<value>], value_length being one byte
|
||||||
|
/// long.
|
||||||
|
pub fn write_config(&mut self, key: &str, val: &str) -> Result<(), FlashError> {
|
||||||
|
let entry_cursor = self.get_config_cursor(key).ok_or(FlashError::NoSuchKey)?;
|
||||||
|
let val_data = val.as_bytes();
|
||||||
|
|
||||||
|
if val_data.len() > ENTRY_SIZE as usize {
|
||||||
|
error!(
|
||||||
|
"data of length {} for {} is longer than the max entry size of {}",
|
||||||
|
val_data.len(),
|
||||||
|
key,
|
||||||
|
ENTRY_SIZE
|
||||||
|
);
|
||||||
|
return Err(FlashError::ValueTooLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flash
|
||||||
|
.blocking_erase(entry_cursor, entry_cursor + ERASE_SIZE as u32)?;
|
||||||
|
self.flash
|
||||||
|
.blocking_write(entry_cursor, &[val_data.len() as u8])?; // value length
|
||||||
|
self.flash.blocking_write(entry_cursor + 1, val_data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the value of a key from flash storage.
|
||||||
|
pub fn read_config<'b>(
|
||||||
|
&mut self,
|
||||||
|
key: &str,
|
||||||
|
buffer: &'b mut [u8; ENTRY_SIZE as usize],
|
||||||
|
) -> Result<&'b str, FlashError> {
|
||||||
|
let entry_cursor = self.get_config_cursor(key).ok_or(FlashError::NoSuchKey)?;
|
||||||
|
|
||||||
|
let mut length_buffer: [u8; 1] = [0; 1];
|
||||||
|
self.flash.blocking_read(entry_cursor, &mut length_buffer)?;
|
||||||
|
let length = length_buffer[0] as usize;
|
||||||
|
|
||||||
|
if length < 1 || length > ENTRY_SIZE as usize {
|
||||||
|
error!("there is no valid value for {}", key);
|
||||||
|
return Err(FlashError::NoValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flash.blocking_read(entry_cursor + 1, buffer)?;
|
||||||
|
let val = core::str::from_utf8(&buffer[0..length])?;
|
||||||
|
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
}
|
101
controller/src/main.rs
Normal file
101
controller/src/main.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//! Read the temperature at regular intervals and publish that data via MQTT:
|
||||||
|
//! Configure the microcontroller with commands sent across an usb-serial connection.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use byte_handler::cmd_task;
|
||||||
|
use cyw43::Control;
|
||||||
|
use defmt::{error, info};
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::Stack;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use flash::{Config, FlashError};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
use temperature::{temperature_task, Temperature};
|
||||||
|
use usb_serial::{init_usb, usb_task};
|
||||||
|
use wifi::connect;
|
||||||
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
|
use crate::{mqtt::BUFFER_SIZE, wifi::init_wifi};
|
||||||
|
|
||||||
|
mod byte_handler;
|
||||||
|
mod cmd_handlers;
|
||||||
|
mod flash;
|
||||||
|
mod mqtt;
|
||||||
|
mod temperature;
|
||||||
|
mod usb_serial;
|
||||||
|
mod wifi;
|
||||||
|
|
||||||
|
async fn connect_wifi<'a>(
|
||||||
|
config: &mut Config<'static>,
|
||||||
|
wifi: &mut Control<'a>,
|
||||||
|
net_stack: &'a Stack<cyw43::NetDriver<'a>>,
|
||||||
|
) -> Result<(), FlashError> {
|
||||||
|
let mut ssid_buffer = [0; flash::ENTRY_SIZE as usize];
|
||||||
|
let ssid = config.read_config(flash::SSID_KEY, &mut ssid_buffer)?;
|
||||||
|
let mut ssid_pw_buffer = [0; flash::ENTRY_SIZE as usize];
|
||||||
|
let ssid_pw = config.read_config(flash::SSID_PW_KEY, &mut ssid_pw_buffer)?;
|
||||||
|
|
||||||
|
if !ssid.is_empty() {
|
||||||
|
connect(wifi, net_stack, ssid, ssid_pw).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG: StaticCell<Config> = StaticCell::new();
|
||||||
|
|
||||||
|
static RX_BUFFER: StaticCell<[u8; BUFFER_SIZE]> = StaticCell::new();
|
||||||
|
static TX_BUFFER: StaticCell<[u8; BUFFER_SIZE]> = StaticCell::new();
|
||||||
|
|
||||||
|
static CLIENT_ID_BUFFER: StaticCell<[u8; flash::ENTRY_SIZE as usize]> = StaticCell::new();
|
||||||
|
static MQTT_IP_BUFFER: StaticCell<[u8; flash::ENTRY_SIZE as usize]> = StaticCell::new();
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(spawner: Spawner) {
|
||||||
|
let p = embassy_rp::init(Default::default());
|
||||||
|
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
|
||||||
|
let config = CONFIG.init(Config::init(p.FLASH, p.DMA_CH0));
|
||||||
|
|
||||||
|
let (mut wifi, net_stack) = init_wifi(
|
||||||
|
spawner, p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.PIO0, p.DMA_CH1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match connect_wifi(config, &mut wifi, net_stack).await {
|
||||||
|
Ok(_) => info!("Connected to WiFi."),
|
||||||
|
Err(_) => error!("Failed to connect to WiFi"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let rx_buffer = RX_BUFFER.init([0u8; BUFFER_SIZE]);
|
||||||
|
let tx_buffer = TX_BUFFER.init([0u8; BUFFER_SIZE]);
|
||||||
|
|
||||||
|
let client_id_buffer = CLIENT_ID_BUFFER.init([0u8; flash::ENTRY_SIZE as usize]);
|
||||||
|
let client_id = config
|
||||||
|
.read_config(flash::CLIENT_ID_KEY, client_id_buffer)
|
||||||
|
.unwrap_or("tomato");
|
||||||
|
|
||||||
|
let mqtt_ip_buffer = MQTT_IP_BUFFER.init([0u8; flash::ENTRY_SIZE as usize]);
|
||||||
|
let mqtt_ip = config
|
||||||
|
.read_config(flash::MQTT_KEY, mqtt_ip_buffer)
|
||||||
|
.unwrap_or("127.0.0.1");
|
||||||
|
|
||||||
|
let mqtt = mqtt::Client::new(client_id, net_stack, rx_buffer, tx_buffer);
|
||||||
|
let host_addr = embassy_net::IpAddress::from_str(mqtt_ip)
|
||||||
|
.unwrap_or(embassy_net::IpAddress::v4(127, 0, 1, 1));
|
||||||
|
let temps = Temperature::new(p.ADC, p.ADC_TEMP_SENSOR);
|
||||||
|
|
||||||
|
let (usb, class) = init_usb(p.USB);
|
||||||
|
spawner
|
||||||
|
.spawn(usb_task(usb))
|
||||||
|
.expect("failed to run usb task");
|
||||||
|
spawner
|
||||||
|
.spawn(cmd_task(class, config))
|
||||||
|
.expect("failed initializing usb command handler");
|
||||||
|
spawner
|
||||||
|
.spawn(temperature_task(client_id, host_addr, mqtt, temps))
|
||||||
|
.expect("failed starting temperature task");
|
||||||
|
}
|
73
controller/src/mqtt.rs
Normal file
73
controller/src/mqtt.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use defmt::Format;
|
||||||
|
use embassy_net::{
|
||||||
|
tcp::{ConnectError, Error, TcpSocket},
|
||||||
|
IpAddress, Stack,
|
||||||
|
};
|
||||||
|
use embassy_time::Duration;
|
||||||
|
use mqtt_protocol::{connect::Connect, publish::Publish};
|
||||||
|
|
||||||
|
pub const BUFFER_SIZE: usize = 4096;
|
||||||
|
const TCP_TIMEOUT_SECS: u64 = 5;
|
||||||
|
|
||||||
|
#[derive(Debug, Format)]
|
||||||
|
pub enum MqttError {
|
||||||
|
TcpError(Error),
|
||||||
|
ConnectError(ConnectError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ConnectError> for MqttError {
|
||||||
|
fn from(err: ConnectError) -> Self {
|
||||||
|
MqttError::ConnectError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for MqttError {
|
||||||
|
fn from(err: Error) -> Self {
|
||||||
|
MqttError::TcpError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Very simple MQTT client, capable of sending simple publish packets over unencrypted TCP/IP.
|
||||||
|
pub struct Client<'a> {
|
||||||
|
pub client_id: &'a str,
|
||||||
|
socket: TcpSocket<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Client<'a> {
|
||||||
|
/// Initialize the socket for sending MQTT data and set the socket timeout to `TCP_TIMEOUT_SECS`.
|
||||||
|
///
|
||||||
|
/// - `client_id`: Identification of this client.
|
||||||
|
/// - `net_stack`: network stack for sending data.
|
||||||
|
/// - `rx_buffer`: byte buffer for receiving data.
|
||||||
|
/// - `tx_buffer`: byte buffer for sending data.
|
||||||
|
pub fn new(
|
||||||
|
client_id: &'a str,
|
||||||
|
net_stack: &'a Stack<cyw43::NetDriver<'a>>,
|
||||||
|
rx_buffer: &'static mut [u8; BUFFER_SIZE],
|
||||||
|
tx_buffer: &'static mut [u8; BUFFER_SIZE],
|
||||||
|
) -> Self {
|
||||||
|
let mut socket = embassy_net::tcp::TcpSocket::new(net_stack, rx_buffer, tx_buffer);
|
||||||
|
socket.set_timeout(Some(Duration::from_secs(TCP_TIMEOUT_SECS)));
|
||||||
|
|
||||||
|
Self { client_id, socket }
|
||||||
|
}
|
||||||
|
|
||||||
|
//// Publish the payload `payload` to the MQTT topic `topic` on the broker running at `address`:1883.
|
||||||
|
pub async fn publish(
|
||||||
|
&mut self,
|
||||||
|
address: IpAddress,
|
||||||
|
topic: &str,
|
||||||
|
payload: &[u8],
|
||||||
|
) -> Result<(), MqttError> {
|
||||||
|
self.socket.connect((address, 1883)).await?;
|
||||||
|
let connect = Connect::new(self.client_id);
|
||||||
|
self.socket.write(&connect.data[0..connect.length]).await?;
|
||||||
|
|
||||||
|
let publish = Publish::new(topic, payload);
|
||||||
|
self.socket.write(&publish.data[0..publish.length]).await?;
|
||||||
|
|
||||||
|
self.socket.close();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
86
controller/src/temperature.rs
Normal file
86
controller/src/temperature.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use defmt::{error, info};
|
||||||
|
use embassy_net::IpAddress;
|
||||||
|
use embassy_rp::{
|
||||||
|
adc::{Adc, Async, Channel, Config, Error, InterruptHandler},
|
||||||
|
bind_interrupts,
|
||||||
|
peripherals::{ADC, ADC_TEMP_SENSOR},
|
||||||
|
};
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use mqtt_protocol::publish::MAX_TOPIC_LENGTH;
|
||||||
|
|
||||||
|
use crate::mqtt;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
ADC_IRQ_FIFO => InterruptHandler;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn temperature_task(
|
||||||
|
client_id: &'static str,
|
||||||
|
host_addr: IpAddress,
|
||||||
|
mut mqtt: mqtt::Client<'static>,
|
||||||
|
mut temps: Temperature<'static>,
|
||||||
|
) -> ! {
|
||||||
|
const TOPIC_PREFIX: &str = "temps/";
|
||||||
|
let mut topic_buffer = [0u8; MAX_TOPIC_LENGTH - TOPIC_PREFIX.len()];
|
||||||
|
topic_buffer[..TOPIC_PREFIX.len()].copy_from_slice(TOPIC_PREFIX.as_bytes());
|
||||||
|
|
||||||
|
let client_id_bytes = client_id.as_bytes();
|
||||||
|
topic_buffer[TOPIC_PREFIX.len()..TOPIC_PREFIX.len() + client_id_bytes.len()]
|
||||||
|
.copy_from_slice(client_id_bytes);
|
||||||
|
|
||||||
|
let topic_full =
|
||||||
|
core::str::from_utf8(&topic_buffer[..TOPIC_PREFIX.len() + client_id_bytes.len()])
|
||||||
|
.unwrap_or("temps/tomato");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match temps.read().await {
|
||||||
|
Ok(temp) => {
|
||||||
|
info!("publishing temperature data...");
|
||||||
|
if let Err(e) = mqtt
|
||||||
|
.publish(host_addr, topic_full, &temp.to_le_bytes())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("failed to publish temperature data: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => error!("failed to read temperature: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer::after_secs(10).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the raw temperature sensor reading into degrees celsius.
|
||||||
|
///
|
||||||
|
/// Taken from the [embassy examples](https://github.com/embassy-rs/embassy/blob/b6fc682117a41e8e63a9632e06da5a17f46d9ab0/examples/rp/src/bin/adc.rs#L43).
|
||||||
|
fn convert_to_celsius(raw_temp: u16) -> f32 {
|
||||||
|
// According to chapter 4.9.5. Temperature Sensor in RP2040 datasheet
|
||||||
|
let temp = 27.0 - (raw_temp as f32 * 3.3 / 4096.0 - 0.706) / 0.001721;
|
||||||
|
let sign = if temp < 0.0 { -1.0 } else { 1.0 };
|
||||||
|
let rounded_temp_x10: i16 = ((temp * 10.0) + 0.5 * sign) as i16;
|
||||||
|
(rounded_temp_x10 as f32) / 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Temperature<'a> {
|
||||||
|
adc: Adc<'a, Async>,
|
||||||
|
ts: Channel<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Temperature<'a> {
|
||||||
|
/// Initialize the temperature sensor.
|
||||||
|
pub fn new(adc_pin: ADC, adc_temp_pin: ADC_TEMP_SENSOR) -> Self {
|
||||||
|
let adc = Adc::new(adc_pin, Irqs, Config::default());
|
||||||
|
let ts = Channel::new_temp_sensor(adc_temp_pin);
|
||||||
|
|
||||||
|
Self { adc, ts }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read from the temperature sensor and return the value in degree celsius.
|
||||||
|
pub async fn read(&mut self) -> Result<f32, Error> {
|
||||||
|
let temp = self.adc.read(&mut self.ts).await?;
|
||||||
|
let temp = convert_to_celsius(temp);
|
||||||
|
|
||||||
|
Ok(temp)
|
||||||
|
}
|
||||||
|
}
|
74
controller/src/usb_serial.rs
Normal file
74
controller/src/usb_serial.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use embassy_rp::{
|
||||||
|
bind_interrupts,
|
||||||
|
peripherals::USB,
|
||||||
|
usb::{Driver, InterruptHandler},
|
||||||
|
};
|
||||||
|
use embassy_usb::{
|
||||||
|
class::cdc_acm::{CdcAcmClass, State},
|
||||||
|
Builder, Config, UsbDevice,
|
||||||
|
};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
USBCTRL_IRQ => InterruptHandler<USB>;
|
||||||
|
});
|
||||||
|
|
||||||
|
pub type SerialDevice = UsbDevice<'static, Driver<'static, USB>>;
|
||||||
|
pub type DeviceClass = CdcAcmClass<'static, Driver<'static, USB>>;
|
||||||
|
|
||||||
|
const DESCRIPTOR_SIZE: usize = 256;
|
||||||
|
type Descriptor = [u8; DESCRIPTOR_SIZE];
|
||||||
|
|
||||||
|
const BUFFER_SIZE: usize = 64;
|
||||||
|
type ControlBuffer = [u8; BUFFER_SIZE];
|
||||||
|
|
||||||
|
static DEVICE_DESCRIPTOR: StaticCell<Descriptor> = StaticCell::new();
|
||||||
|
static CONFIG_DESCRIPTOR: StaticCell<Descriptor> = StaticCell::new();
|
||||||
|
static BOS_DESCRIPTOR: StaticCell<Descriptor> = StaticCell::new();
|
||||||
|
static CONTROL_BUFFER: StaticCell<ControlBuffer> = StaticCell::new();
|
||||||
|
static STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
static CLASS: StaticCell<DeviceClass> = StaticCell::new();
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
pub async fn usb_task(mut usb: SerialDevice) -> ! {
|
||||||
|
usb.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the USB serial interface and return the device and CdcAcm class (Abstract Control Model).
|
||||||
|
pub fn init_usb(usb: USB) -> (SerialDevice, &'static mut DeviceClass) {
|
||||||
|
let driver = Driver::new(usb, Irqs);
|
||||||
|
|
||||||
|
let mut config = Config::new(0xc0de, 0xcafe);
|
||||||
|
config.manufacturer = Some("Embassy");
|
||||||
|
config.product = Some("USB-serial");
|
||||||
|
config.serial_number = Some("123456789");
|
||||||
|
config.max_power = 100;
|
||||||
|
config.max_packet_size_0 = 64;
|
||||||
|
|
||||||
|
// Required for windows 7 compatibility.
|
||||||
|
// https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
|
||||||
|
config.device_class = 0xEF;
|
||||||
|
config.device_sub_class = 0x02;
|
||||||
|
config.device_protocol = 0x01;
|
||||||
|
config.composite_with_iads = true;
|
||||||
|
|
||||||
|
let device_descriptor = DEVICE_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]);
|
||||||
|
let config_descriptor = CONFIG_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]);
|
||||||
|
let bos_descriptor = BOS_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]);
|
||||||
|
let control_buf = CONTROL_BUFFER.init([0u8; BUFFER_SIZE]);
|
||||||
|
|
||||||
|
let state = STATE.init(State::new());
|
||||||
|
let mut builder = Builder::new(
|
||||||
|
driver,
|
||||||
|
config,
|
||||||
|
device_descriptor,
|
||||||
|
config_descriptor,
|
||||||
|
bos_descriptor,
|
||||||
|
control_buf,
|
||||||
|
);
|
||||||
|
|
||||||
|
let class = CLASS.init(CdcAcmClass::new(&mut builder, state, 64));
|
||||||
|
let usb = builder.build();
|
||||||
|
|
||||||
|
(usb, &mut *class)
|
||||||
|
}
|
128
controller/src/wifi.rs
Normal file
128
controller/src/wifi.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use cyw43::{Control, ControlError, State};
|
||||||
|
use cyw43_pio::PioSpi;
|
||||||
|
use defmt::{error, info, unwrap};
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_net::{Config, Stack, StackResources};
|
||||||
|
use embassy_rp::{
|
||||||
|
bind_interrupts,
|
||||||
|
clocks::RoscRng,
|
||||||
|
gpio::{Level, Output},
|
||||||
|
peripherals::{DMA_CH1, PIN_23, PIN_24, PIN_25, PIN_29, PIO0},
|
||||||
|
pio::{InterruptHandler, Pio},
|
||||||
|
};
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use rand::RngCore;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
const MAX_WIFI_TRIES: u8 = 3;
|
||||||
|
const MAX_DHCP_TRIES: u8 = 20;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
PIO0_IRQ_0 => InterruptHandler<PIO0>;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn wifi_task(
|
||||||
|
runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH1>>,
|
||||||
|
) -> ! {
|
||||||
|
runner.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn net_task(stack: &'static Stack<cyw43::NetDriver<'static>>) -> ! {
|
||||||
|
stack.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
static STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
static STACK: StaticCell<Stack<cyw43::NetDriver<'static>>> = StaticCell::new();
|
||||||
|
const STACK_RESOURCES_COUNT: usize = 2;
|
||||||
|
static STACK_RESOURCES: StaticCell<StackResources<STACK_RESOURCES_COUNT>> = StaticCell::new();
|
||||||
|
|
||||||
|
// Initialise the wwifi chip of the RP2040 and return the control interface as well as the network
|
||||||
|
// stack.
|
||||||
|
pub async fn init_wifi(
|
||||||
|
spawner: Spawner,
|
||||||
|
pin_23: PIN_23,
|
||||||
|
pin_24: PIN_24,
|
||||||
|
pin_25: PIN_25,
|
||||||
|
pin_29: PIN_29,
|
||||||
|
pio0: PIO0,
|
||||||
|
dma_ch1: DMA_CH1,
|
||||||
|
) -> (Control<'static>, &'static Stack<cyw43::NetDriver<'static>>) {
|
||||||
|
let fw = include_bytes!("../cyw43-firmware/43439A0.bin");
|
||||||
|
let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin");
|
||||||
|
|
||||||
|
let pwr = Output::new(pin_23, Level::Low);
|
||||||
|
let cs = Output::new(pin_25, Level::High);
|
||||||
|
let mut pio = Pio::new(pio0, Irqs);
|
||||||
|
let spi = PioSpi::new(
|
||||||
|
&mut pio.common,
|
||||||
|
pio.sm0,
|
||||||
|
pio.irq0,
|
||||||
|
cs,
|
||||||
|
pin_24,
|
||||||
|
pin_29,
|
||||||
|
dma_ch1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = STATE.init(State::new());
|
||||||
|
let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await;
|
||||||
|
|
||||||
|
unwrap!(spawner.spawn(wifi_task(runner)));
|
||||||
|
|
||||||
|
control.init(clm).await;
|
||||||
|
control
|
||||||
|
.set_power_management(cyw43::PowerManagementMode::PowerSave)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let config = Config::dhcpv4(Default::default());
|
||||||
|
|
||||||
|
let mut rng = RoscRng;
|
||||||
|
let seed = rng.next_u64();
|
||||||
|
|
||||||
|
// Init network stack
|
||||||
|
let resources = STACK_RESOURCES.init(StackResources::<STACK_RESOURCES_COUNT>::new());
|
||||||
|
let stack = &*STACK.init(Stack::new(net_device, config, resources, seed));
|
||||||
|
|
||||||
|
unwrap!(spawner.spawn(net_task(stack)));
|
||||||
|
|
||||||
|
(control, stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to a WiFi network using WPA2 and try to get an IP from DHCP (IPv4).
|
||||||
|
pub async fn connect<'a>(
|
||||||
|
wifi: &mut Control<'a>,
|
||||||
|
net_stack: &Stack<cyw43::NetDriver<'a>>,
|
||||||
|
ssid: &str,
|
||||||
|
pw: &str,
|
||||||
|
) {
|
||||||
|
info!("trying to connect to {}...", ssid);
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
let mut status: Result<(), ControlError> = Err(ControlError { status: 0 });
|
||||||
|
|
||||||
|
while idx < MAX_WIFI_TRIES && status.is_err() {
|
||||||
|
status = wifi.join_wpa2(ssid, pw).await;
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = status {
|
||||||
|
error!("WiFi connection failed: {}", e.status);
|
||||||
|
} else {
|
||||||
|
info!("waiting for DHCP...");
|
||||||
|
|
||||||
|
idx = 0;
|
||||||
|
while idx < MAX_DHCP_TRIES && !net_stack.is_config_up() {
|
||||||
|
Timer::after_millis(1000).await;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if net_stack.is_config_up() {
|
||||||
|
info!("DHCP is up!");
|
||||||
|
} else {
|
||||||
|
error!("Failed to get an IP from DHCP");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
flake.lock
Normal file
114
flake.lock
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731738660,
|
||||||
|
"narHash": "sha256-tIXhc9lX1b030v812yVJanSR37OnpTb/OY5rU3TbShA=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "e10ba121773f754a30d31b6163919a3e404a434f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731319897,
|
||||||
|
"narHash": "sha256-PbABj4tnbWFMfBp6OcUK5iGy1QY+/Z96ZcLpooIbuEI=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "dc460ec76cbff0e66e269457d7b728432263166c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731319897,
|
||||||
|
"narHash": "sha256-PbABj4tnbWFMfBp6OcUK5iGy1QY+/Z96ZcLpooIbuEI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "dc460ec76cbff0e66e269457d7b728432263166c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731693936,
|
||||||
|
"narHash": "sha256-uHUUS1WPyW6ohp5Bt3dAZczUlQ22vOn7YZF8vaPKIEw=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "1b90e979aeee8d1db7fe14603a00834052505497",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
37
flake.nix
Normal file
37
flake.nix
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
fenix.url = "github:nix-community/fenix";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
fenix,
|
||||||
|
flake-utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
fx = fenix.packages.${system};
|
||||||
|
rust = fx.combine [
|
||||||
|
fx.stable.toolchain
|
||||||
|
fx.targets.thumbv6m-none-eabi.stable.rust-std
|
||||||
|
];
|
||||||
|
buildInputs = [
|
||||||
|
rust
|
||||||
|
pkgs.cargo-deny
|
||||||
|
pkgs.rust-analyzer
|
||||||
|
pkgs.probe-rs
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = buildInputs;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
8
mqtt-protocol/Cargo.toml
Normal file
8
mqtt-protocol/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "mqtt-protocol"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
128
mqtt-protocol/src/connect.rs
Normal file
128
mqtt-protocol/src/connect.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use crate::fixed_header::FixedHeader;
|
||||||
|
|
||||||
|
// [MQTT 5.0: 3.1.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242)
|
||||||
|
const ALLOWED_CLIENT_ID_CHARS: &str =
|
||||||
|
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
// [MQTT 5.0: 3.1.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242)
|
||||||
|
const MAX_CLIENT_ID_SIZE: usize = 23;
|
||||||
|
|
||||||
|
// [MQTT 5.0: 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_Fixed_Header)
|
||||||
|
const FIXED_CONNECT_TYPE: u8 = 0x01;
|
||||||
|
// [MQTT 5.0: 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_Fixed_Header)
|
||||||
|
const FIXED_CONNECT_FLAGS: u8 = 0x00;
|
||||||
|
|
||||||
|
// "MQTT" [MQTT 5.0: 3.1.2.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349225)
|
||||||
|
const PROTOCOL_NAME: [u8; 6] = [0x00, 0x04, 0x4d, 0x51, 0x54, 0x54];
|
||||||
|
// "version 5" [MQTT 5.0: 3.1.2.2](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349227)
|
||||||
|
const PROTOCOL_VERSION: [u8; 1] = [0x05];
|
||||||
|
// no user name, no will qos, no will, no clean start [MQTT 5.0: 3.1.2.3 - 3.1.2.9](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349229)
|
||||||
|
const CONNECT_FLAGS: [u8; 1] = [0x00];
|
||||||
|
// turn off keep alive mechanism [MQTT 5.0: 3.1.2.10](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Keep_Alive_1)
|
||||||
|
const KEEP_ALIVE: [u8; 2] = [0x00, 0x00];
|
||||||
|
// no properties [MQTT 5.0: 3.1.2.11](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988523)
|
||||||
|
const PROPERTIES: [u8; 1] = [0x00];
|
||||||
|
|
||||||
|
// calculate position in the data array (those that are constant)
|
||||||
|
const PROTOCOL_NAME_IDX: usize = 0;
|
||||||
|
const PROTOCOL_VERSION_IDX: usize = PROTOCOL_NAME_IDX + PROTOCOL_NAME.len();
|
||||||
|
const CONNECT_FLAGS_IDX: usize = PROTOCOL_VERSION_IDX + PROTOCOL_VERSION.len();
|
||||||
|
const KEEP_ALIVE_IDX: usize = CONNECT_FLAGS_IDX + CONNECT_FLAGS.len();
|
||||||
|
const PROPERTIES_IDX: usize = KEEP_ALIVE_IDX + KEEP_ALIVE.len();
|
||||||
|
const PAYLOAD_IDX: usize = PROPERTIES_IDX + PROPERTIES.len();
|
||||||
|
|
||||||
|
// max size the variable header + payload of a connect packet
|
||||||
|
const MAX_PACKET_LENGTH: usize = PROTOCOL_NAME.len()
|
||||||
|
+ PROTOCOL_VERSION.len()
|
||||||
|
+ CONNECT_FLAGS.len()
|
||||||
|
+ KEEP_ALIVE.len()
|
||||||
|
+ PROPERTIES.len()
|
||||||
|
+ MAX_CLIENT_ID_SIZE
|
||||||
|
+ 2; // two bytes for the client id length
|
||||||
|
|
||||||
|
// max size a whole connect packet can be
|
||||||
|
const MAX_CONNECT_LENGTH: usize = MAX_PACKET_LENGTH + 5; // 5 is the max length of the fixed header
|
||||||
|
|
||||||
|
pub struct Connect {
|
||||||
|
// byte representation of the connect packet
|
||||||
|
pub data: [u8; MAX_CONNECT_LENGTH],
|
||||||
|
// actual length of the whole connect packet
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connect {
|
||||||
|
/// Create an MQTT connect packet.
|
||||||
|
///
|
||||||
|
/// See [MQTT 5.0: 3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_%E2%80%93_Connection)
|
||||||
|
/// for the byte structure.
|
||||||
|
///
|
||||||
|
/// `client_id` is how the client identifies itself. According to
|
||||||
|
/// [MQTT 5.0: 3.1.3.1]((https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242)
|
||||||
|
/// this is not strictly necessary but a server does not have to accept this.
|
||||||
|
///
|
||||||
|
/// The same holds for the limitation of 23 bytes for the `client_id`.
|
||||||
|
pub fn new(client_id: &str) -> Self {
|
||||||
|
assert!(client_id
|
||||||
|
.chars()
|
||||||
|
.all(|x| ALLOWED_CLIENT_ID_CHARS.contains(x)));
|
||||||
|
|
||||||
|
let client_id_data = client_id.as_bytes();
|
||||||
|
assert!(!client_id_data.is_empty() && client_id_data.len() <= MAX_CLIENT_ID_SIZE);
|
||||||
|
|
||||||
|
let client_id_length = client_id_data.len() as u16;
|
||||||
|
let high_byte = ((client_id_length & 0xFF00) >> 8) as u8;
|
||||||
|
let low_byte = (client_id_length & 0x00FF) as u8;
|
||||||
|
|
||||||
|
let mut payload_data = [0u8; MAX_CLIENT_ID_SIZE + 2];
|
||||||
|
payload_data[0] = high_byte;
|
||||||
|
payload_data[1] = low_byte;
|
||||||
|
payload_data[2..2 + client_id_data.len()].copy_from_slice(client_id_data);
|
||||||
|
|
||||||
|
let packet_length = MAX_PACKET_LENGTH - MAX_CLIENT_ID_SIZE + client_id_data.len();
|
||||||
|
let fixed_header = FixedHeader::new(
|
||||||
|
FIXED_CONNECT_TYPE,
|
||||||
|
FIXED_CONNECT_FLAGS,
|
||||||
|
packet_length as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut data = [0u8; MAX_CONNECT_LENGTH];
|
||||||
|
data[0] = fixed_header.type_flags;
|
||||||
|
data[1..1 + fixed_header.length]
|
||||||
|
.copy_from_slice(&fixed_header.remaining_length[0..fixed_header.length]);
|
||||||
|
let fixed_offset = fixed_header.length + 1;
|
||||||
|
|
||||||
|
data[fixed_offset + PROTOCOL_NAME_IDX..fixed_offset + PROTOCOL_VERSION_IDX]
|
||||||
|
.copy_from_slice(&PROTOCOL_NAME);
|
||||||
|
data[fixed_offset + PROTOCOL_VERSION_IDX..fixed_offset + CONNECT_FLAGS_IDX]
|
||||||
|
.copy_from_slice(&PROTOCOL_VERSION);
|
||||||
|
data[fixed_offset + CONNECT_FLAGS_IDX..fixed_offset + KEEP_ALIVE_IDX]
|
||||||
|
.copy_from_slice(&CONNECT_FLAGS);
|
||||||
|
data[fixed_offset + KEEP_ALIVE_IDX..fixed_offset + PROPERTIES_IDX]
|
||||||
|
.copy_from_slice(&KEEP_ALIVE);
|
||||||
|
data[fixed_offset + PROPERTIES_IDX..fixed_offset + PAYLOAD_IDX]
|
||||||
|
.copy_from_slice(&PROPERTIES);
|
||||||
|
data[fixed_offset + PAYLOAD_IDX..fixed_offset + PAYLOAD_IDX + client_id_data.len() + 2]
|
||||||
|
.copy_from_slice(&payload_data[0..client_id_data.len() + 2]);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
length: packet_length + fixed_header.length + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Connect;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_length_1_byte() {
|
||||||
|
let connect = Connect::new("Tomato");
|
||||||
|
assert_eq!(
|
||||||
|
connect.data[0..connect.length],
|
||||||
|
[
|
||||||
|
0x10, 0x13, 0x00, 0x04, 0x4d, 0x51, 0x54, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x06, 0x54, 0x6f, 0x6d, 0x61, 0x74, 0x6f
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
28
mqtt-protocol/src/fixed_header.rs
Normal file
28
mqtt-protocol/src/fixed_header.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use super::variable_length::encode_length;
|
||||||
|
|
||||||
|
pub struct FixedHeader {
|
||||||
|
pub type_flags: u8,
|
||||||
|
// length of variable header + payload, encoded as a variable byte integer
|
||||||
|
// ([MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950))
|
||||||
|
pub remaining_length: [u8; 4],
|
||||||
|
// how many bytes in the remaining length field are relevant (left to right)
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedHeader {
|
||||||
|
/// Create a fixed header ([MQTT 5.0: 2.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988498)) for an MQTT packet.
|
||||||
|
///
|
||||||
|
/// - `packet_type`: [MQTT 5.0: 2.1.2](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc353481061)
|
||||||
|
/// - `flags`: [MQTT 5.0: 2.1.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc353481062)
|
||||||
|
/// - `remaining_length`: Length of the variable header + payload.
|
||||||
|
pub fn new(packet_type: u8, flags: u8, remaining_length: u32) -> Self {
|
||||||
|
let mut remaining_length_data = [0u8; 4];
|
||||||
|
let length = encode_length(remaining_length, &mut remaining_length_data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
type_flags: packet_type << 4 | (flags & 0x0F),
|
||||||
|
remaining_length: remaining_length_data,
|
||||||
|
length: length.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
mqtt-protocol/src/lib.rs
Normal file
10
mqtt-protocol/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//! Implement just enough of the MQTT protocol to be able to publish data with
|
||||||
|
//! QOS 0.
|
||||||
|
//! All formats are taken from [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html).
|
||||||
|
|
||||||
|
#![cfg_attr(not(test), no_std)]
|
||||||
|
|
||||||
|
pub mod connect;
|
||||||
|
pub mod fixed_header;
|
||||||
|
pub mod publish;
|
||||||
|
pub mod variable_length;
|
80
mqtt-protocol/src/publish.rs
Normal file
80
mqtt-protocol/src/publish.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use crate::fixed_header::FixedHeader;
|
||||||
|
|
||||||
|
// [MQTT 5.0: 3.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc359155681)
|
||||||
|
const FIXED_PUBLISH_TYPE: u8 = 0x03;
|
||||||
|
// no flags set [MQTT 5.0: 3.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc359155681)
|
||||||
|
const FIXED_PUBLISH_FLAGS: u8 = 0x00;
|
||||||
|
|
||||||
|
// no properties [MQTT 5.0: 3.3.2.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988586)
|
||||||
|
const PROPERTIES: [u8; 1] = [0x00];
|
||||||
|
|
||||||
|
// technically this is 65535 (see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_UTF-8_Encoded_String)
|
||||||
|
// but we do not need that here
|
||||||
|
pub const MAX_TOPIC_LENGTH: usize = 254;
|
||||||
|
|
||||||
|
// this is again arbitrarilly constricted, technically it could be as long as the variable length
|
||||||
|
// encoding allows (minus fixed and variable header)
|
||||||
|
const MAX_PAYLOAD_LENGTH: usize = 128;
|
||||||
|
// 7 is 5 + 2 which is max length of the fixed header and the two bytes used to encode the topic
|
||||||
|
// length
|
||||||
|
const MAX_PUBLISH_LENGTH: usize = MAX_TOPIC_LENGTH + 7 + MAX_PAYLOAD_LENGTH;
|
||||||
|
|
||||||
|
/// Encode a string to an MQTT length encoded string
|
||||||
|
/// [MQTT 5.0: 1.5.4](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc462729066)
|
||||||
|
fn encode_mqtt_str(topic: &str, encode_buffer: &mut [u8; MAX_TOPIC_LENGTH]) -> usize {
|
||||||
|
let topic_data = topic.as_bytes();
|
||||||
|
assert!(topic_data.len() <= MAX_TOPIC_LENGTH);
|
||||||
|
|
||||||
|
encode_buffer[0] = 0; // no need for the high bits as our max topic length is too small
|
||||||
|
encode_buffer[1] = topic_data.len() as u8;
|
||||||
|
encode_buffer[2..2 + topic_data.len()].copy_from_slice(topic_data);
|
||||||
|
|
||||||
|
topic_data.len() + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Publish {
|
||||||
|
// byte representation of the publish packet
|
||||||
|
pub data: [u8; MAX_PUBLISH_LENGTH],
|
||||||
|
// actual length of the whole publish packet
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Publish {
|
||||||
|
/// Create an MQTT publish packet.
|
||||||
|
///
|
||||||
|
/// See [MQTT 5.0: 3.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc384800410)
|
||||||
|
/// for the byte structure.
|
||||||
|
///
|
||||||
|
/// `topic`: defines the topic to publish to
|
||||||
|
/// `payload` value to publish as is
|
||||||
|
pub fn new(topic: &str, payload: &[u8]) -> Self {
|
||||||
|
let mut topic_data = [0u8; MAX_TOPIC_LENGTH];
|
||||||
|
let topic_length = encode_mqtt_str(topic, &mut topic_data);
|
||||||
|
|
||||||
|
let packet_length = PROPERTIES.len() + topic_length + 2 + payload.len();
|
||||||
|
let fixed_header = FixedHeader::new(
|
||||||
|
FIXED_PUBLISH_TYPE,
|
||||||
|
FIXED_PUBLISH_FLAGS,
|
||||||
|
packet_length as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut data = [0u8; MAX_PUBLISH_LENGTH];
|
||||||
|
data[0] = fixed_header.type_flags;
|
||||||
|
data[1..1 + fixed_header.length]
|
||||||
|
.copy_from_slice(&fixed_header.remaining_length[0..fixed_header.length]);
|
||||||
|
let fixed_offset = fixed_header.length + 1;
|
||||||
|
|
||||||
|
let topic_idx = fixed_offset;
|
||||||
|
let properties_idx = topic_idx + topic_length;
|
||||||
|
let payload_idx = properties_idx + PROPERTIES.len();
|
||||||
|
|
||||||
|
data[topic_idx..properties_idx].copy_from_slice(&topic_data[0..topic_length]);
|
||||||
|
data[properties_idx..payload_idx].copy_from_slice(&PROPERTIES);
|
||||||
|
data[payload_idx..payload_idx + payload.len()].copy_from_slice(payload);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
length: packet_length + fixed_header.length + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
mqtt-protocol/src/variable_length.rs
Normal file
94
mqtt-protocol/src/variable_length.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/// Encode an integer as a variable byte integer
|
||||||
|
/// [MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950)
|
||||||
|
pub fn encode_length(length: u32, encoded_length: &mut [u8; 4]) -> u8 {
|
||||||
|
let mut length = length;
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < encoded_length.len() && length > 0 {
|
||||||
|
let mut encoded_byte = (length % 128) as u8;
|
||||||
|
length /= 128;
|
||||||
|
|
||||||
|
if length > 0 {
|
||||||
|
encoded_byte |= 0x80; // Set the highest bit
|
||||||
|
}
|
||||||
|
encoded_length[idx] = encoded_byte;
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a variable byte integer as an integer.
|
||||||
|
/// [MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950)
|
||||||
|
pub fn decode_length(bytes: [u8; 4]) -> u32 {
|
||||||
|
let mut length = 0u32;
|
||||||
|
let mut multiplier = 1u32;
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
let mut byte = bytes[idx];
|
||||||
|
|
||||||
|
while idx < bytes.len() && byte & 0x80 != 0 {
|
||||||
|
length += ((byte & 0x7F) as u32) * multiplier;
|
||||||
|
multiplier *= 128;
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
byte = bytes[idx]
|
||||||
|
}
|
||||||
|
length += ((byte & 0x7F) as u32) * multiplier;
|
||||||
|
|
||||||
|
length
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::variable_length::{decode_length, encode_length};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_length_1_byte() {
|
||||||
|
let mut length = [0u8; 4];
|
||||||
|
let byte_length = encode_length(16, &mut length);
|
||||||
|
|
||||||
|
assert_eq!(byte_length, 1);
|
||||||
|
assert_eq!(length, [0x10, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
|
let length = decode_length(length);
|
||||||
|
assert_eq!(length, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_length_2_bytes() {
|
||||||
|
let mut length = [0u8; 4];
|
||||||
|
let byte_length = encode_length(568, &mut length);
|
||||||
|
|
||||||
|
assert_eq!(byte_length, 2);
|
||||||
|
assert_eq!(length, [0xb8, 0x04, 0x00, 0x00]);
|
||||||
|
|
||||||
|
let length = decode_length(length);
|
||||||
|
assert_eq!(length, 568);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_length_3_bytes() {
|
||||||
|
let mut length = [0u8; 4];
|
||||||
|
let byte_length = encode_length(85734, &mut length);
|
||||||
|
|
||||||
|
assert_eq!(byte_length, 3);
|
||||||
|
assert_eq!(length, [0xe6, 0x9d, 0x05, 0x00]);
|
||||||
|
|
||||||
|
let length = decode_length(length);
|
||||||
|
assert_eq!(length, 85734);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_length_4_bytes() {
|
||||||
|
let mut length = [0u8; 4];
|
||||||
|
let byte_length = encode_length(8573471, &mut length);
|
||||||
|
|
||||||
|
assert_eq!(byte_length, 4);
|
||||||
|
assert_eq!(length, [0x9f, 0xa4, 0x8b, 0x04]);
|
||||||
|
|
||||||
|
let length = decode_length(length);
|
||||||
|
assert_eq!(length, 8573471);
|
||||||
|
}
|
||||||
|
}
|
8
serial-comm/Cargo.toml
Normal file
8
serial-comm/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "serial-comm"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
150
serial-comm/src/cobs.rs
Normal file
150
serial-comm/src/cobs.rs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// sentinel value for backage boundaries
|
||||||
|
// could be anything but 0x00 is used as a default and makes sense
|
||||||
|
pub const SENTINEL: u8 = 0x00;
|
||||||
|
// max size of non SENTINEL values before a code split occurs
|
||||||
|
const SPLIT_BOUNDARY: u8 = 0xff;
|
||||||
|
|
||||||
|
/// Encode a byte array of size `INPUT` with an actual data length of`length`
|
||||||
|
/// to an output array of size `OUTPUT`, using the COBS algorithm.
|
||||||
|
///
|
||||||
|
/// Return a tuple of the structure (<encoded_length>, <output_array>)
|
||||||
|
pub fn encode<const INPUT: usize, const OUTPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
length: usize,
|
||||||
|
) -> (usize, [u8; OUTPUT]) {
|
||||||
|
let mut output: [u8; OUTPUT] = [0; OUTPUT];
|
||||||
|
|
||||||
|
let mut encode_idx = 1;
|
||||||
|
let mut code_idx = 0;
|
||||||
|
let mut code = 1;
|
||||||
|
|
||||||
|
for byte in input.iter().take(length) {
|
||||||
|
if *byte == SENTINEL {
|
||||||
|
output[code_idx] = code;
|
||||||
|
code = 1;
|
||||||
|
code_idx = encode_idx;
|
||||||
|
encode_idx += 1;
|
||||||
|
} else {
|
||||||
|
output[encode_idx] = *byte;
|
||||||
|
encode_idx += 1;
|
||||||
|
code += 1;
|
||||||
|
|
||||||
|
if code == SPLIT_BOUNDARY {
|
||||||
|
output[code_idx] = code;
|
||||||
|
code = 1;
|
||||||
|
code_idx = encode_idx;
|
||||||
|
|
||||||
|
if encode_idx < length {
|
||||||
|
encode_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code_idx < OUTPUT {
|
||||||
|
output[code_idx] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
(encode_idx, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a COBS encoded byte array of length `INPUT` and an actual data length of `length` to an
|
||||||
|
/// array of length `OUTPUT`.
|
||||||
|
///
|
||||||
|
/// Return a tuple of the structure (<decoded_length>, <output_array>)
|
||||||
|
pub fn decode<const INPUT: usize, const OUTPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
length: usize,
|
||||||
|
) -> (usize, [u8; OUTPUT]) {
|
||||||
|
let mut output: [u8; OUTPUT] = [0; OUTPUT];
|
||||||
|
|
||||||
|
let mut out_idx = 0;
|
||||||
|
let mut idx = 0;
|
||||||
|
|
||||||
|
while idx < length {
|
||||||
|
let code = input[idx];
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
for _ in 1..code {
|
||||||
|
if idx < length {
|
||||||
|
output[out_idx] = input[idx];
|
||||||
|
out_idx += 1;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != SPLIT_BOUNDARY && idx < length {
|
||||||
|
output[out_idx] = SENTINEL;
|
||||||
|
out_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(out_idx, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cobs::{decode, encode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paper_example() {
|
||||||
|
let input = [
|
||||||
|
0x45, 0x00, 0x00, 0x2c, 0x4c, 0x79, 0x00, 0x00, 0x40, 0x06, 0x4f, 0x37,
|
||||||
|
];
|
||||||
|
let output = [
|
||||||
|
0x02, 0x45, 0x01, 0x04, 0x2c, 0x4c, 0x79, 0x01, 0x05, 0x40, 0x06, 0x4f, 0x37,
|
||||||
|
];
|
||||||
|
assert_eq!(encode(input, 12), (13, output));
|
||||||
|
assert_eq!(decode(output, 13), (12, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_input() {
|
||||||
|
let input = [];
|
||||||
|
let output = [0x01];
|
||||||
|
assert_eq!(encode(input, 0), (1, output));
|
||||||
|
assert_eq!(decode(output, 1), (0, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_zeros() {
|
||||||
|
let input = [0x00, 0x00, 0x00];
|
||||||
|
let output = [0x01, 0x01, 0x01, 0x01];
|
||||||
|
assert_eq!(encode(input, 3), (4, output));
|
||||||
|
assert_eq!(decode(output, 4), (3, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_zeros() {
|
||||||
|
let input = [0x01, 0x02, 0x03];
|
||||||
|
let output = [0x04, 0x01, 0x02, 0x03];
|
||||||
|
assert_eq!(encode(input, 3), (4, output));
|
||||||
|
assert_eq!(decode(output, 4), (3, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_zeros_in_254_bytes() {
|
||||||
|
let input: [u8; 254] = [0x01; 254];
|
||||||
|
|
||||||
|
// expected output will have the first byte as 0xFF followed by the 255 non-zero bytes.
|
||||||
|
let mut output: [u8; 255] = [0x01; 255];
|
||||||
|
output[0] = 0xff;
|
||||||
|
|
||||||
|
assert_eq!(encode(input, 254), (255, output));
|
||||||
|
assert_eq!(decode(output, 255), (254, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_zeros_in_254_bytes_and_additional_block() {
|
||||||
|
let mut input: [u8; 259] = [0x01; 259];
|
||||||
|
input[254..259].copy_from_slice(&[0x02, 0x00, 0x03, 0x0c, 0x01]);
|
||||||
|
|
||||||
|
// expected output will have the first byte as 0xFF followed by the 254 non-zero bytes.
|
||||||
|
let mut output: [u8; 261] = [0x01; 261];
|
||||||
|
output[0] = 0xff;
|
||||||
|
output[255..261].copy_from_slice(&[0x02, 0x02, 0x04, 0x03, 0x0c, 0x01]);
|
||||||
|
|
||||||
|
assert_eq!(encode(input, 259), (261, output));
|
||||||
|
assert_eq!(decode(output, 261), (259, input));
|
||||||
|
}
|
||||||
|
}
|
8
serial-comm/src/lib.rs
Normal file
8
serial-comm/src/lib.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//! Simple communication protocol over a serial connection, leveraging the COBS algorithm for byte
|
||||||
|
//! stuffing
|
||||||
|
//! (S. Cheshire and M. Baker, "Consistent overhead byte stuffing," in IEEE/ACM Transactions on Networking, vol. 7, no. 2, pp. 159-172, April 1999, doi: 10.1109/90.769765).
|
||||||
|
|
||||||
|
#![cfg_attr(not(test), no_std)]
|
||||||
|
|
||||||
|
pub mod cobs;
|
||||||
|
pub mod protocol;
|
50
serial-comm/src/protocol/cmd.rs
Normal file
50
serial-comm/src/protocol/cmd.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use core::str::Utf8Error;
|
||||||
|
|
||||||
|
const MAX_PARAMS: usize = 8;
|
||||||
|
|
||||||
|
pub trait Cobs<'a> {
|
||||||
|
/// Encode a command with the COBS algorithm.
|
||||||
|
///
|
||||||
|
/// Return a tuple of the structure (<encoded_length>, <output_array>)
|
||||||
|
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Cmd<'a> {
|
||||||
|
fn from_bytes<const INPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
buffer: &'a mut [u8; INPUT],
|
||||||
|
) -> Result<Self, Utf8Error>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
/// Read parameters from a byte array (not COBS encoded).
|
||||||
|
///
|
||||||
|
/// Every parameter is interpreted as an UTF-8 string.
|
||||||
|
///
|
||||||
|
/// Return a string array of size `MAX_PARAMS`.
|
||||||
|
fn params_from_bytes<const INPUT: usize, const PARAMS: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
buffer: &mut [u8; INPUT],
|
||||||
|
) -> Result<[&str; MAX_PARAMS], Utf8Error> {
|
||||||
|
buffer.copy_from_slice(&input);
|
||||||
|
|
||||||
|
let param_count = buffer[0] as usize;
|
||||||
|
assert_eq!(param_count, PARAMS);
|
||||||
|
assert!(param_count <= MAX_PARAMS);
|
||||||
|
|
||||||
|
let param_offset = param_count + 1;
|
||||||
|
let mut param_idx = 0;
|
||||||
|
let mut params: [&str; MAX_PARAMS] = [""; MAX_PARAMS];
|
||||||
|
for i in 1..=param_count {
|
||||||
|
let param_length = buffer[i] as usize;
|
||||||
|
let param_start = param_idx + param_offset;
|
||||||
|
let param = &buffer[param_start..param_start + param_length];
|
||||||
|
let param = core::str::from_utf8(param)?;
|
||||||
|
params[i - 1] = param;
|
||||||
|
|
||||||
|
param_idx += param_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
}
|
83
serial-comm/src/protocol/error.rs
Normal file
83
serial-comm/src/protocol/error.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use core::str::Utf8Error;
|
||||||
|
|
||||||
|
use crate::cobs;
|
||||||
|
|
||||||
|
use super::cmd::{Cmd, Cobs};
|
||||||
|
|
||||||
|
/// The error command carries a message.
|
||||||
|
pub struct ErrorCommand<'a> {
|
||||||
|
pub prefix: &'a str,
|
||||||
|
pub msg: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ErrorCommand<'a> {
|
||||||
|
const PARAMS: u8 = 2;
|
||||||
|
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
|
||||||
|
|
||||||
|
pub fn new(msg: &'a str) -> Self {
|
||||||
|
Self { prefix: "ER", msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cobs<'a> for ErrorCommand<'a> {
|
||||||
|
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
|
||||||
|
let prefix_bytes = self.prefix.as_bytes();
|
||||||
|
let msg_bytes = self.msg.as_bytes();
|
||||||
|
|
||||||
|
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
|
||||||
|
let msg_end = prefix_bytes.len() + msg_bytes.len() + Self::HEADER_LENGTH;
|
||||||
|
|
||||||
|
let mut output: [u8; OUTPUT] = [0; OUTPUT];
|
||||||
|
|
||||||
|
output[0] = ErrorCommand::PARAMS;
|
||||||
|
output[1] = prefix_bytes.len() as u8;
|
||||||
|
output[2] = msg_bytes.len() as u8;
|
||||||
|
output[ErrorCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
|
||||||
|
output[prefix_end..msg_end].copy_from_slice(msg_bytes);
|
||||||
|
|
||||||
|
cobs::encode(output, msg_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cmd<'a> for ErrorCommand<'a> {
|
||||||
|
fn from_bytes<const INPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
buffer: &'a mut [u8; INPUT],
|
||||||
|
) -> Result<Self, Utf8Error> {
|
||||||
|
let params = <ErrorCommand<'a> as Cmd>::params_from_bytes::<
|
||||||
|
INPUT,
|
||||||
|
{ ErrorCommand::PARAMS as usize },
|
||||||
|
>(input, buffer)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
prefix: params[0],
|
||||||
|
msg: params[1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ErrorCommand;
|
||||||
|
use crate::cobs;
|
||||||
|
use crate::protocol::cmd::{Cmd, Cobs};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ok_cmd() {
|
||||||
|
let cmd = ErrorCommand::new("error");
|
||||||
|
let (length, cmd_cobs) = cmd.as_cobs::<64>();
|
||||||
|
|
||||||
|
assert_eq!(length, 11);
|
||||||
|
assert_eq!(
|
||||||
|
&cmd_cobs[0..11],
|
||||||
|
&[0x0b, 0x02, 0x02, 0x05, 0x45, 0x52, 0x65, 0x72, 0x72, 0x6f, 0x72]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 11);
|
||||||
|
|
||||||
|
let mut cmd_buffer: [u8; 64] = [0; 64];
|
||||||
|
let cmd = ErrorCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
|
||||||
|
assert_eq!(cmd.prefix, "ER");
|
||||||
|
assert_eq!(cmd.msg, "error");
|
||||||
|
}
|
||||||
|
}
|
6
serial-comm/src/protocol/mod.rs
Normal file
6
serial-comm/src/protocol/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
//! Specific commands for the protocol.
|
||||||
|
|
||||||
|
pub mod cmd;
|
||||||
|
pub mod error;
|
||||||
|
pub mod ok;
|
||||||
|
pub mod set_config;
|
76
serial-comm/src/protocol/ok.rs
Normal file
76
serial-comm/src/protocol/ok.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use core::str::Utf8Error;
|
||||||
|
|
||||||
|
use crate::cobs;
|
||||||
|
|
||||||
|
use super::cmd::{Cmd, Cobs};
|
||||||
|
|
||||||
|
// The Ok command has no parameters, it should merely signify something went ok.
|
||||||
|
pub struct OkCommand<'a> {
|
||||||
|
pub prefix: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OkCommand<'a> {
|
||||||
|
const PARAMS: u8 = 1;
|
||||||
|
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { prefix: "OK" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for OkCommand<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cobs<'a> for OkCommand<'a> {
|
||||||
|
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
|
||||||
|
let prefix_bytes = self.prefix.as_bytes();
|
||||||
|
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
|
||||||
|
|
||||||
|
let mut output: [u8; OUTPUT] = [0; OUTPUT];
|
||||||
|
|
||||||
|
output[0] = OkCommand::PARAMS;
|
||||||
|
output[1] = prefix_bytes.len() as u8;
|
||||||
|
output[OkCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
|
||||||
|
|
||||||
|
cobs::encode(output, prefix_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cmd<'a> for OkCommand<'a> {
|
||||||
|
fn from_bytes<const INPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
buffer: &'a mut [u8; INPUT],
|
||||||
|
) -> Result<Self, Utf8Error> {
|
||||||
|
let params = <OkCommand<'a> as Cmd>::params_from_bytes::<
|
||||||
|
INPUT,
|
||||||
|
{ OkCommand::PARAMS as usize },
|
||||||
|
>(input, buffer)?;
|
||||||
|
|
||||||
|
Ok(Self { prefix: params[0] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::OkCommand;
|
||||||
|
use crate::cobs;
|
||||||
|
use crate::protocol::cmd::{Cmd, Cobs};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ok_cmd() {
|
||||||
|
let cmd = OkCommand::new();
|
||||||
|
let (length, cmd_cobs) = cmd.as_cobs::<64>();
|
||||||
|
|
||||||
|
assert_eq!(length, 5);
|
||||||
|
assert_eq!(&cmd_cobs[0..5], &[0x05, 0x01, 0x02, 0x4f, 0x4b]);
|
||||||
|
|
||||||
|
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 5);
|
||||||
|
|
||||||
|
let mut cmd_buffer: [u8; 64] = [0; 64];
|
||||||
|
let cmd = OkCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
|
||||||
|
assert_eq!(cmd.prefix, "OK");
|
||||||
|
}
|
||||||
|
}
|
92
serial-comm/src/protocol/set_config.rs
Normal file
92
serial-comm/src/protocol/set_config.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use core::str::Utf8Error;
|
||||||
|
|
||||||
|
use crate::cobs;
|
||||||
|
|
||||||
|
use super::cmd::{Cmd, Cobs};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// The SetConfig command sets the value of a key.
|
||||||
|
pub struct SetConfig<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
pub value: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SetConfig<'a> {
|
||||||
|
pub const PREFIX: &'a str = "SC";
|
||||||
|
const PARAMS: u8 = 3;
|
||||||
|
const HEADER_LENGTH: usize = Self::PARAMS as usize + 1;
|
||||||
|
|
||||||
|
pub fn new(key: &'a str, value: &'a str) -> Self {
|
||||||
|
Self { key, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cobs<'a> for SetConfig<'a> {
|
||||||
|
/// Encode a SetConfig command with the COBS algorithm.
|
||||||
|
///
|
||||||
|
/// Return a tuple of the structure (<encoded_length>, <output_array>)
|
||||||
|
fn as_cobs<const OUTPUT: usize>(&self) -> (usize, [u8; OUTPUT]) {
|
||||||
|
let prefix_bytes = Self::PREFIX.as_bytes();
|
||||||
|
let key_bytes = self.key.as_bytes();
|
||||||
|
let val_bytes = self.value.as_bytes();
|
||||||
|
|
||||||
|
let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH;
|
||||||
|
let key_end = prefix_end + key_bytes.len();
|
||||||
|
let val_end = key_end + val_bytes.len();
|
||||||
|
|
||||||
|
let mut output: [u8; OUTPUT] = [0; OUTPUT];
|
||||||
|
|
||||||
|
output[0] = Self::PARAMS;
|
||||||
|
output[1] = prefix_bytes.len() as u8;
|
||||||
|
output[2] = key_bytes.len() as u8;
|
||||||
|
output[3] = val_bytes.len() as u8;
|
||||||
|
output[Self::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes);
|
||||||
|
output[prefix_end..key_end].copy_from_slice(key_bytes);
|
||||||
|
output[key_end..val_end].copy_from_slice(val_bytes);
|
||||||
|
|
||||||
|
cobs::encode(output, val_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cmd<'a> for SetConfig<'a> {
|
||||||
|
fn from_bytes<const INPUT: usize>(
|
||||||
|
input: [u8; INPUT],
|
||||||
|
buffer: &'a mut [u8; INPUT],
|
||||||
|
) -> Result<Self, Utf8Error> {
|
||||||
|
let params = <SetConfig<'a> as Cmd>::params_from_bytes::<
|
||||||
|
INPUT,
|
||||||
|
{ SetConfig::PARAMS as usize },
|
||||||
|
>(input, buffer)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
key: params[1],
|
||||||
|
value: params[2],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cobs;
|
||||||
|
use crate::protocol::cmd::{Cmd, Cobs};
|
||||||
|
use crate::protocol::set_config::SetConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_cmd() {
|
||||||
|
let cmd = SetConfig::new("ssid", "pw");
|
||||||
|
let (length, cmd_cobs) = cmd.as_cobs::<64>();
|
||||||
|
|
||||||
|
assert_eq!(length, 13);
|
||||||
|
assert_eq!(
|
||||||
|
&cmd_cobs[0..13],
|
||||||
|
&[0x0d, 0x03, 0x02, 0x04, 0x02, 0x53, 0x43, 0x73, 0x73, 0x69, 0x64, 0x70, 0x77]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 13);
|
||||||
|
|
||||||
|
let mut cmd_buffer: [u8; 64] = [0; 64];
|
||||||
|
let cmd = SetConfig::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap();
|
||||||
|
assert_eq!(cmd.key, "ssid");
|
||||||
|
assert_eq!(cmd.value, "pw");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user