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