initial commit
This commit is contained in:
commit
17a8e573c0
40 changed files with 4009 additions and 0 deletions
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.
|
||||
|
||||
 _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");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue