Reworked code to host API using picoserve
This commit is contained in:
parent
0fef57c37d
commit
148943ff42
1330
Cargo.lock
generated
1330
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
|
@ -8,73 +8,75 @@ cortex-m = { version = "0.7", features = ["inline-asm"] }
|
|||
cortex-m-rt = "0.7"
|
||||
defmt = "0.3"
|
||||
defmt-rtt = "0.4"
|
||||
embassy-embedded-hal = { version = "0.1.0", features = ["defmt"] }
|
||||
embassy-executor = { version = "0.5", features = [
|
||||
"nightly",
|
||||
"arch-cortex-m",
|
||||
"executor-thread",
|
||||
"executor-interrupt",
|
||||
"defmt",
|
||||
"integrated-timers",
|
||||
embassy-embedded-hal = { version = "0.3.0", features = ["defmt"] }
|
||||
embassy-executor = { version = "0.7", features = [
|
||||
"nightly",
|
||||
"arch-cortex-m",
|
||||
"executor-thread",
|
||||
"executor-interrupt",
|
||||
"defmt",
|
||||
] }
|
||||
embassy-rp = { version = "0.1", features = [
|
||||
"defmt",
|
||||
"unstable-pac",
|
||||
"time-driver",
|
||||
"critical-section-impl",
|
||||
"intrinsics",
|
||||
"rom-v2-intrinsics",
|
||||
embassy-rp = { version = "0.3", features = [
|
||||
"rp2040",
|
||||
"defmt",
|
||||
"unstable-pac",
|
||||
"time-driver",
|
||||
"critical-section-impl",
|
||||
"intrinsics",
|
||||
"rom-v2-intrinsics",
|
||||
] }
|
||||
embassy-boot-rp = { version = "0.2", features = ["defmt"] }
|
||||
embassy-boot = { version = "0.2", features = ["defmt"] }
|
||||
embassy-time = { version = "0.3", features = [
|
||||
"defmt",
|
||||
"defmt-timestamp-uptime",
|
||||
embassy-boot-rp = { version = "0.4", features = ["defmt"] }
|
||||
embassy-boot = { version = "0.4", features = ["defmt"] }
|
||||
embassy-time = { version = "0.4", features = [
|
||||
"defmt",
|
||||
"defmt-timestamp-uptime",
|
||||
] }
|
||||
embassy-net = { version = "0.4", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
embassy-net = { version = "0.6", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"dhcpv4-hostname",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
] }
|
||||
embassy-sync = { version = "0.5", features = ["defmt"] }
|
||||
embassy-sync = { version = "0.6", features = ["defmt"] }
|
||||
embassy-futures = { version = "0.1", features = ["defmt"] }
|
||||
panic-probe = { version = "0.3", features = ["print-defmt"] }
|
||||
cfg-if = "1.0"
|
||||
static_cell = { version = "2", features = ["nightly"] }
|
||||
cyw43 = { version = "0.1", features = ["defmt", "firmware-logs"] }
|
||||
cyw43-pio = { version = "0.1", features = ["defmt"] }
|
||||
cyw43 = { version = "0.3", features = ["defmt", "firmware-logs"] }
|
||||
cyw43-pio = { version = "0.3", features = ["defmt"] }
|
||||
rand = { version = "0.8", features = [
|
||||
"nightly",
|
||||
"small_rng",
|
||||
"std_rng",
|
||||
"nightly",
|
||||
"small_rng",
|
||||
"std_rng",
|
||||
], default-features = false }
|
||||
rust-mqtt = { version = "0.2", features = [
|
||||
"defmt",
|
||||
"no_std",
|
||||
"tls",
|
||||
rust-mqtt = { version = "0.3", features = [
|
||||
"defmt",
|
||||
"no_std",
|
||||
"tls",
|
||||
], default-features = false }
|
||||
const_format = "0.2"
|
||||
git-version = "0.3"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
heapless = { version = "0.7", features = ["defmt", "serde"] }
|
||||
serde-json-core = "0.5"
|
||||
heapless = { version = "0.8", features = ["defmt-03", "serde"] }
|
||||
nourl = { version = "0.1", features = ["defmt"] }
|
||||
# Embassy harfcodes a max of 6 dns servers, if there are more it crashes. This is a workaround
|
||||
# Embassy hardcodes a max of 6 dns servers, if there are more it crashes. This is a workaround
|
||||
# Ideally embassy returns an error instead of crashing...
|
||||
# Interestingly though, I only get 2 DNS servers...
|
||||
smoltcp = { version = "0.11", default-features = false, features = [
|
||||
"dns-max-server-count-4",
|
||||
smoltcp = { version = "0.12", default-features = false, features = [
|
||||
"dns-max-server-count-4",
|
||||
] }
|
||||
updater = { version = "0.1.0", path = "../iot_tools/updater" }
|
||||
portable-atomic = { version = "1.6", features = ["critical-section"] }
|
||||
bme280 = { version = "0.5.0", features = ["async", "defmt"] }
|
||||
picoserve = { version = "0.13.3", features = ["defmt", "embassy"] }
|
||||
embedded-storage = "0.3"
|
||||
|
||||
[patch.crates-io]
|
||||
# Make mqtt:// and mqtts:// actually work
|
||||
nourl = { git = "https://git.huizinga.dev/Dreaded_X/nourl" }
|
||||
bme280 = { git = "https://github.com/Remmirad/bme280-rs/", branch = "fix_reset_setup_time" }
|
||||
picoserve = { git = "https://github.com/hodasemi/picoserve" }
|
||||
|
||||
[features]
|
||||
include_firmwares = []
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
mkdir -p target/firmware
|
||||
cargo objcopy --release --features=include_firmwares -- -O binary target/firmware/firmware
|
||||
cargo objcopy --release -- -O binary target/firmware/firmware
|
||||
shasum -a 512 -b target/firmware/firmware | dd ibs=128 count=1 | xxd -p -r > target/firmware/checksum
|
||||
signify -S -m target/firmware/checksum -s ~/Projects/crypt/R0/private/keys/firmware/airfilter.sec -x target/firmware/checksum.sig
|
||||
tail -n1 target/firmware/checksum.sig | base64 -d -i | dd ibs=10 skip=1 > target/firmware/signed
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2024-03-01"
|
||||
channel = "nightly-2025-01-09"
|
||||
targets = ["thumbv6m-none-eabi"]
|
||||
components = ["rustfmt", "clippy", "llvm-tools"]
|
||||
components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools"]
|
||||
|
|
57
src/lib.rs
Normal file
57
src/lib.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
#![no_std]
|
||||
|
||||
use core::fmt::Debug;
|
||||
|
||||
use bme280::Measurements;
|
||||
use defmt::Format;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Format, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FanSpeed {
|
||||
Off,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FanState {
|
||||
pub speed: FanSpeed,
|
||||
pub manual: bool,
|
||||
}
|
||||
|
||||
impl FanState {
|
||||
pub fn new(speed: FanSpeed, manual: bool) -> Self {
|
||||
Self { speed, manual }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetFanSpeed {
|
||||
speed: FanSpeed,
|
||||
}
|
||||
|
||||
impl SetFanSpeed {
|
||||
pub fn speed(&self) -> FanSpeed {
|
||||
self.speed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SensorData {
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
pressure: f32,
|
||||
}
|
||||
|
||||
impl SensorData {
|
||||
pub fn new<E: Debug>(measurements: Measurements<E>) -> Self {
|
||||
Self {
|
||||
temperature: measurements.temperature,
|
||||
humidity: measurements.humidity,
|
||||
pressure: measurements.pressure,
|
||||
}
|
||||
}
|
||||
}
|
509
src/main.rs
509
src/main.rs
|
@ -1,118 +1,67 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
|
||||
use core::fmt::Debug;
|
||||
use core::{cell::RefCell, str::from_utf8};
|
||||
use core::{cell::RefCell, str::FromStr};
|
||||
|
||||
use bme280::{i2c::AsyncBME280, Measurements};
|
||||
use const_format::formatcp;
|
||||
use cyw43::PowerManagementMode;
|
||||
use cyw43_pio::PioSpi;
|
||||
use defmt::{debug, error, info, warn, Display2Format, Format};
|
||||
use bme280::i2c::AsyncBME280;
|
||||
use cyw43::{JoinOptions, PowerManagementMode};
|
||||
use cyw43_pio::{PioSpi, DEFAULT_CLOCK_DIVIDER};
|
||||
use defmt::{debug, info, warn};
|
||||
use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater, FirmwareUpdaterConfig};
|
||||
use embassy_embedded_hal::flash::partition::BlockingPartition;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_futures::select::{select3, select4, Either3};
|
||||
use embassy_net::{dns::DnsQueryType, tcp::TcpSocket, Config, Stack, StackResources};
|
||||
use embassy_net::{Config, DhcpConfig, StackResources};
|
||||
use embassy_rp::{
|
||||
bind_interrupts,
|
||||
clocks::RoscRng,
|
||||
flash::{Flash, WRITE_SIZE},
|
||||
gpio::{Flex, Input, Level, Output, Pin, Pull},
|
||||
i2c,
|
||||
peripherals::{DMA_CH1, I2C0, PIN_23, PIN_25, PIO0},
|
||||
i2c::{self, Async},
|
||||
peripherals::{DMA_CH1, FLASH, I2C0, PIO0},
|
||||
pio::{self, Pio},
|
||||
Peripheral,
|
||||
};
|
||||
use embassy_sync::blocking_mutex::Mutex;
|
||||
use embassy_time::{Delay, Duration, Ticker, Timer};
|
||||
use heapless::Vec;
|
||||
use nourl::Url;
|
||||
use rand::{
|
||||
rngs::{SmallRng, StdRng},
|
||||
RngCore, SeedableRng,
|
||||
};
|
||||
use rust_mqtt::{
|
||||
client::{
|
||||
client::MqttClient,
|
||||
client_config::{ClientConfig, MqttVersion},
|
||||
use embassy_sync::{
|
||||
blocking_mutex::{
|
||||
self,
|
||||
raw::{CriticalSectionRawMutex, NoopRawMutex},
|
||||
},
|
||||
packet::v5::publish_packet::QualityOfService,
|
||||
mutex::Mutex,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_cell::make_static;
|
||||
use embassy_time::{Delay, Duration, Timer};
|
||||
use heapless::String;
|
||||
use picoserve::{
|
||||
extract, make_static,
|
||||
routing::{get, PathRouter},
|
||||
Router,
|
||||
};
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
use static_cell::StaticCell;
|
||||
use updater::firmware_router;
|
||||
|
||||
use {defmt_rtt as _, panic_probe as _};
|
||||
|
||||
use air_filter::{FanSpeed, FanState, SensorData, SetFanSpeed};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
PIO0_IRQ_0 => pio::InterruptHandler<PIO0>;
|
||||
I2C0_IRQ => i2c::InterruptHandler<I2C0>;
|
||||
});
|
||||
|
||||
const ID: &str = env!("ID");
|
||||
const TOPIC_BASE: &str = formatcp!("pico/{}", ID);
|
||||
const TOPIC_STATUS: &str = formatcp!("{}/status", TOPIC_BASE);
|
||||
const TOPIC_UPDATE: &str = formatcp!("{}/update", TOPIC_BASE);
|
||||
const TOPIC_SET: &str = formatcp!("{}/set", TOPIC_BASE);
|
||||
const VERSION: &str = git_version::git_version!();
|
||||
const PUBLIC_SIGNING_KEY: &[u8; 32] = include_bytes!("../key.pub");
|
||||
const FLASH_SIZE: usize = 2 * 1024 * 1024;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetMessage {
|
||||
state: State,
|
||||
struct Controller<'a> {
|
||||
off: Input<'a>,
|
||||
low: Flex<'a>,
|
||||
medium: Flex<'a>,
|
||||
high: Flex<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StateMessage {
|
||||
state: State,
|
||||
manual: bool,
|
||||
temperature: f32,
|
||||
humidity: f32,
|
||||
pressure: f32,
|
||||
}
|
||||
|
||||
impl StateMessage {
|
||||
pub fn new<E: Debug>((state, manual): (State, bool), measurements: Measurements<E>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
manual,
|
||||
temperature: measurements.temperature,
|
||||
humidity: measurements.humidity,
|
||||
pressure: measurements.pressure,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vec(&self) -> Vec<u8, 128> {
|
||||
serde_json_core::to_vec(self)
|
||||
.expect("The buffer should be large enough to contain all the data")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Format, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum State {
|
||||
Off,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
impl SetMessage {
|
||||
fn get_state(&self) -> State {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
struct Controller<'a, O: Pin, L: Pin, M: Pin, H: Pin> {
|
||||
off: Input<'a, O>,
|
||||
low: Flex<'a, L>,
|
||||
medium: Flex<'a, M>,
|
||||
high: Flex<'a, H>,
|
||||
}
|
||||
|
||||
impl<'a, O: Pin, L: Pin, M: Pin, H: Pin> Controller<'a, O, L, M, H> {
|
||||
pub fn new(
|
||||
impl<'a> Controller<'a> {
|
||||
pub fn new<O: Pin, L: Pin, M: Pin, H: Pin>(
|
||||
off: impl Peripheral<P = O> + 'a,
|
||||
low: impl Peripheral<P = L> + 'a,
|
||||
medium: impl Peripheral<P = M> + 'a,
|
||||
|
@ -140,86 +89,143 @@ impl<'a, O: Pin, L: Pin, M: Pin, H: Pin> Controller<'a, O, L, M, H> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&mut self) -> (State, bool) {
|
||||
pub fn get_state(&mut self) -> FanState {
|
||||
let manual = self.off.is_high();
|
||||
let state = match (self.low.is_low(), self.medium.is_low(), self.high.is_low()) {
|
||||
(false, false, false) => State::Off,
|
||||
(true, false, false) => State::Low,
|
||||
(false, true, false) => State::Medium,
|
||||
(false, false, true) => State::High,
|
||||
let speed = match (self.low.is_low(), self.medium.is_low(), self.high.is_low()) {
|
||||
(false, false, false) => FanSpeed::Off,
|
||||
(true, false, false) => FanSpeed::Low,
|
||||
(false, true, false) => FanSpeed::Medium,
|
||||
(false, false, true) => FanSpeed::High,
|
||||
(a, b, c) => {
|
||||
// This happens if the user turns the knob, in this case we should turn off remote
|
||||
// control
|
||||
debug!("Unknown state: ({}, {}, {})", a, b, c);
|
||||
self.set_state(State::Off);
|
||||
State::Off
|
||||
self.set_speed(FanSpeed::Off);
|
||||
FanSpeed::Off
|
||||
}
|
||||
};
|
||||
|
||||
(state, manual)
|
||||
FanState { speed, manual }
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: State) {
|
||||
pub fn set_speed(&mut self, speed: FanSpeed) -> bool {
|
||||
let manual = self.off.is_high();
|
||||
|
||||
if manual && state != State::Off {
|
||||
if manual && speed != FanSpeed::Off {
|
||||
warn!("Filter is manual controlled, cannot control remotely");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
debug!("Setting state: {}", state);
|
||||
debug!("Setting state: {}", speed);
|
||||
|
||||
match state {
|
||||
State::Off => {
|
||||
match speed {
|
||||
FanSpeed::Off => {
|
||||
self.low.set_as_input();
|
||||
self.medium.set_as_input();
|
||||
self.high.set_as_input();
|
||||
}
|
||||
State::Low => {
|
||||
FanSpeed::Low => {
|
||||
self.low.set_as_output();
|
||||
self.low.set_drive_strength(embassy_rp::gpio::Drive::_12mA);
|
||||
self.medium.set_as_input();
|
||||
self.high.set_as_input();
|
||||
}
|
||||
State::Medium => {
|
||||
FanSpeed::Medium => {
|
||||
self.low.set_as_input();
|
||||
self.medium.set_as_output();
|
||||
self.medium
|
||||
.set_drive_strength(embassy_rp::gpio::Drive::_12mA);
|
||||
self.high.set_as_input();
|
||||
}
|
||||
State::High => {
|
||||
FanSpeed::High => {
|
||||
self.low.set_as_input();
|
||||
self.medium.set_as_input();
|
||||
self.high.set_as_output();
|
||||
self.high.set_drive_strength(embassy_rp::gpio::Drive::_12mA);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn watch(&mut self) -> (State, bool) {
|
||||
// Wait for change on any of the pins
|
||||
select4(
|
||||
self.off.wait_for_any_edge(),
|
||||
self.low.wait_for_any_edge(),
|
||||
self.medium.wait_for_any_edge(),
|
||||
self.high.wait_for_any_edge(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Give it some time to stabilze
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
|
||||
pub fn check_for_manual(&mut self) {
|
||||
// If off is high, that means that the knob is used to override the state
|
||||
if self.off.is_high() {
|
||||
// If the filter is in manual mode, set the pico outputs to off
|
||||
self.set_state(State::Off);
|
||||
self.set_speed(FanSpeed::Off);
|
||||
}
|
||||
|
||||
// Get the current state
|
||||
self.get_state()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SharedController(&'static Mutex<CriticalSectionRawMutex, Controller<'static>>);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SharedBME280(
|
||||
&'static Mutex<CriticalSectionRawMutex, AsyncBME280<i2c::I2c<'static, I2C0, Async>>>,
|
||||
);
|
||||
|
||||
struct AppState {
|
||||
shared_controller: SharedController,
|
||||
shared_bme280: SharedBME280,
|
||||
}
|
||||
|
||||
impl picoserve::extract::FromRef<AppState> for SharedController {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.shared_controller
|
||||
}
|
||||
}
|
||||
|
||||
impl picoserve::extract::FromRef<AppState> for SharedBME280 {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.shared_bme280
|
||||
}
|
||||
}
|
||||
|
||||
fn state_router() -> Router<impl PathRouter<AppState>, AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/fan",
|
||||
get(
|
||||
|extract::State(SharedController(controller)): extract::State<
|
||||
SharedController,
|
||||
>| async {
|
||||
let state = controller.lock().await.get_state();
|
||||
picoserve::response::Json(state)
|
||||
},
|
||||
)
|
||||
.put(
|
||||
|extract::State(SharedController(controller)): extract::State<
|
||||
SharedController,
|
||||
>,
|
||||
extract::Json(message): extract::Json<SetFanSpeed>| async move {
|
||||
let success = controller.lock().await.set_speed(message.speed());
|
||||
|
||||
if success {
|
||||
picoserve::response::StatusCode::OK
|
||||
} else {
|
||||
picoserve::response::StatusCode::SERVICE_UNAVAILABLE
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/sensor",
|
||||
get(
|
||||
|extract::State(SharedBME280(bme280)): extract::State<SharedBME280>| async {
|
||||
let measurement = bme280
|
||||
.lock()
|
||||
.await
|
||||
.measure(&mut Delay {})
|
||||
.await
|
||||
.expect("Measurement should work");
|
||||
let sensor_data = SensorData::new(measurement);
|
||||
|
||||
picoserve::response::Json(sensor_data)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the cyw43 firmware blobs
|
||||
///
|
||||
/// # Safety
|
||||
|
@ -250,36 +256,30 @@ unsafe fn get_firmware() -> (&'static [u8], &'static [u8]) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn wait_for_config(
|
||||
stack: &'static Stack<cyw43::NetDriver<'static>>,
|
||||
) -> embassy_net::StaticConfigV4 {
|
||||
for _ in 0..120 {
|
||||
// We are essentially busy looping here since there is no Async API for this
|
||||
if let Some(config) = stack.config_v4() {
|
||||
return config;
|
||||
}
|
||||
|
||||
Timer::after_secs(1).await;
|
||||
}
|
||||
|
||||
info!("Restarting...");
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn wifi_task(
|
||||
runner: cyw43::Runner<
|
||||
'static,
|
||||
Output<'static, PIN_23>,
|
||||
PioSpi<'static, PIN_25, PIO0, 0, DMA_CH1>,
|
||||
>,
|
||||
async fn cyw43_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
|
||||
async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! {
|
||||
runner.run().await
|
||||
}
|
||||
|
||||
type Thingy = BlockingPartition<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
Flash<'static, FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>,
|
||||
>;
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn fan_task(shared_controller: SharedController) -> ! {
|
||||
loop {
|
||||
shared_controller.0.lock().await.check_for_manual();
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::main]
|
||||
|
@ -287,18 +287,15 @@ async fn main(spawner: Spawner) {
|
|||
info!("Starting...");
|
||||
let p = embassy_rp::init(Default::default());
|
||||
|
||||
// TODO: Ideally we use async flash
|
||||
// This has issues with alignment right now
|
||||
let flash = Flash::<_, _, FLASH_SIZE>::new_blocking(p.FLASH);
|
||||
let flash = Mutex::new(RefCell::new(flash));
|
||||
let flash = make_static!(blocking_mutex::Mutex<NoopRawMutex, RefCell<Flash<'static, FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>>>, blocking_mutex::Mutex::new(RefCell::new(flash)));
|
||||
|
||||
let config = FirmwareUpdaterConfig::from_linkerfile_blocking(&flash);
|
||||
let mut aligned = AlignedBuffer([0; WRITE_SIZE]);
|
||||
let config = FirmwareUpdaterConfig::from_linkerfile_blocking(flash, flash);
|
||||
let aligned = make_static!(AlignedBuffer<WRITE_SIZE>, AlignedBuffer([0; WRITE_SIZE]));
|
||||
let updater = BlockingFirmwareUpdater::new(config, &mut aligned.0);
|
||||
let updater = make_static!(Mutex<CriticalSectionRawMutex, BlockingFirmwareUpdater<'static, Thingy, Thingy>>, Mutex::new(updater));
|
||||
|
||||
let mut updater = updater::Updater::new(updater, TOPIC_STATUS, VERSION, PUBLIC_SIGNING_KEY);
|
||||
|
||||
let mut controller = Controller::new(p.PIN_28, p.PIN_27, p.PIN_26, p.PIN_22);
|
||||
let controller = Controller::new(p.PIN_28, p.PIN_27, p.PIN_26, p.PIN_22);
|
||||
|
||||
let i2c = i2c::I2c::new_async(p.I2C0, p.PIN_9, p.PIN_8, Irqs, i2c::Config::default());
|
||||
let mut bme280 = AsyncBME280::new_primary(i2c);
|
||||
|
@ -312,6 +309,7 @@ async fn main(spawner: Spawner) {
|
|||
let spi = PioSpi::new(
|
||||
&mut pio.common,
|
||||
pio.sm0,
|
||||
DEFAULT_CLOCK_DIVIDER,
|
||||
pio.irq0,
|
||||
cs,
|
||||
p.PIN_24,
|
||||
|
@ -321,9 +319,10 @@ async fn main(spawner: Spawner) {
|
|||
|
||||
let (fw, clm) = unsafe { get_firmware() };
|
||||
|
||||
let state = make_static!(cyw43::State::new());
|
||||
static STATE: StaticCell<cyw43::State> = StaticCell::new();
|
||||
let state = STATE.init(cyw43::State::new());
|
||||
let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await;
|
||||
spawner.spawn(wifi_task(runner)).unwrap();
|
||||
spawner.spawn(cyw43_task(runner)).unwrap();
|
||||
|
||||
control.init(clm).await;
|
||||
control
|
||||
|
@ -333,25 +332,30 @@ async fn main(spawner: Spawner) {
|
|||
// Turn LED on while trying to connect
|
||||
control.gpio_set(0, true).await;
|
||||
|
||||
let config = Config::dhcpv4(Default::default());
|
||||
let mut config: DhcpConfig = Default::default();
|
||||
config.hostname = Some(String::from_str("airfilter").expect("Is valid"));
|
||||
let config = Config::dhcpv4(config);
|
||||
|
||||
// Use the Ring Oscillator of the RP2040 as a source of true randomness to seed the
|
||||
// cryptographically secure PRNG
|
||||
let mut rng = StdRng::from_rng(&mut RoscRng).unwrap();
|
||||
|
||||
let stack = make_static!(Stack::new(
|
||||
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||
let (stack, runner) = embassy_net::new(
|
||||
net_device,
|
||||
config,
|
||||
make_static!(StackResources::<6>::new()),
|
||||
RESOURCES.init(StackResources::new()),
|
||||
rng.next_u64(),
|
||||
));
|
||||
|
||||
spawner.spawn(net_task(stack)).unwrap();
|
||||
);
|
||||
spawner.spawn(net_task(runner)).unwrap();
|
||||
|
||||
// Connect to wifi
|
||||
loop {
|
||||
match control
|
||||
.join_wpa2(env!("WIFI_NETWORK"), env!("WIFI_PASSWORD"))
|
||||
.join(
|
||||
env!("WIFI_NETWORK"),
|
||||
JoinOptions::new(env!("WIFI_PASSWORD").as_bytes()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => break,
|
||||
|
@ -361,145 +365,66 @@ async fn main(spawner: Spawner) {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Waiting for DHCP...");
|
||||
let cfg = wait_for_config(stack).await;
|
||||
info!("IP Address: {}", cfg.address.address());
|
||||
// Mark the update as successful after connecting to the wifi
|
||||
updater.lock().await.mark_booted().unwrap();
|
||||
|
||||
let mut rx_buffer = [0; 1024];
|
||||
let mut tx_buffer = [0; 1024];
|
||||
info!("Waiting for link up...");
|
||||
stack.wait_link_up().await;
|
||||
info!("Link is up!");
|
||||
|
||||
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
||||
// socket.set_timeout(Some(Duration::from_secs(10)));
|
||||
let url = Url::parse(env!("MQTT_ADDRESS")).unwrap();
|
||||
debug!("MQTT URL: {}", url);
|
||||
let ip = stack.dns_query(url.host(), DnsQueryType::A).await.unwrap()[0];
|
||||
let addr = (ip, url.port_or_default());
|
||||
debug!("MQTT ADDR: {}", addr);
|
||||
|
||||
while let Err(e) = socket.connect(addr).await {
|
||||
warn!("Connect error: {:?}", e);
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
info!("TCP Connected!");
|
||||
|
||||
let mut config = ClientConfig::new(
|
||||
MqttVersion::MQTTv5,
|
||||
// Use fast and simple PRNG to generate packet identifiers, there is no need for this to be
|
||||
// cryptographically secure
|
||||
SmallRng::from_rng(&mut RoscRng).unwrap(),
|
||||
);
|
||||
|
||||
config.add_username(env!("MQTT_USERNAME"));
|
||||
config.add_password(env!("MQTT_PASSWORD"));
|
||||
config.add_max_subscribe_qos(QualityOfService::QoS1);
|
||||
config.add_client_id(ID);
|
||||
updater.add_will(&mut config);
|
||||
|
||||
let mut recv_buffer = [0; 1024];
|
||||
let mut write_buffer = [0; 1024];
|
||||
|
||||
let mut client = MqttClient::<_, 5, _>::new(
|
||||
socket,
|
||||
&mut write_buffer,
|
||||
1024,
|
||||
&mut recv_buffer,
|
||||
1024,
|
||||
config,
|
||||
);
|
||||
|
||||
info!("Connecting to MQTT...");
|
||||
if client.connect_to_broker().await.is_err() {
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
};
|
||||
info!("MQTT Connected!");
|
||||
|
||||
// We wait with marking as booted until everything is connected
|
||||
client.subscribe_to_topic(TOPIC_UPDATE).await.unwrap();
|
||||
client.subscribe_to_topic(TOPIC_SET).await.unwrap();
|
||||
updater.ready(&mut client).await.unwrap();
|
||||
info!("Waiting for stack to be up...");
|
||||
stack.wait_config_up().await;
|
||||
info!("Stack is up!");
|
||||
|
||||
// Turn LED off when connected
|
||||
control.gpio_set(0, false).await;
|
||||
|
||||
let mut keep_alive = Ticker::every(Duration::from_secs(30));
|
||||
loop {
|
||||
let message = match select3(
|
||||
keep_alive.next(),
|
||||
client.receive_message(),
|
||||
controller.watch(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either3::First(_) => Some(StateMessage::new(
|
||||
controller.get_state(),
|
||||
bme280.measure(&mut delay).await.unwrap(),
|
||||
)),
|
||||
Either3::Second(message) => match message {
|
||||
Ok((TOPIC_UPDATE, url)) => {
|
||||
let url: Vec<_, 256> = match Vec::from_slice(url) {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
error!("URL is longer then buffer size");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let url = match from_utf8(&url) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
error!("Url is not valid utf-8 string: {}", Display2Format(&err));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let url = match Url::parse(url) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
error!("Failed to parse url: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let cfg = stack.config_v4().expect("Config is up");
|
||||
info!("IP Address: {}", cfg.address.address());
|
||||
|
||||
if let Err(err) = updater.update(url, stack, &mut rng, &mut client).await {
|
||||
error!("Update failed: {}", err);
|
||||
}
|
||||
let config = make_static!(
|
||||
picoserve::Config<Duration>,
|
||||
picoserve::Config::new(picoserve::Timeouts {
|
||||
start_read_request: Some(Duration::from_secs(5)),
|
||||
read_request: Some(Duration::from_secs(1)),
|
||||
write: Some(Duration::from_secs(1)),
|
||||
})
|
||||
.keep_connection_alive()
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
Ok((TOPIC_SET, message)) => {
|
||||
let message: SetMessage = match serde_json_core::from_slice(message) {
|
||||
Ok((message, _)) => message,
|
||||
Err(_) => {
|
||||
error!("Unable to parse set message");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let app = Router::new().nest("/state", state_router()).nest(
|
||||
"/firmware",
|
||||
firmware_router(VERSION, updater, PUBLIC_SIGNING_KEY),
|
||||
);
|
||||
|
||||
controller.set_state(message.get_state());
|
||||
let shared_controller = SharedController(
|
||||
make_static!(Mutex<CriticalSectionRawMutex, Controller<'static>>, Mutex::new(controller)),
|
||||
);
|
||||
let shared_bme280 = SharedBME280(
|
||||
make_static!(Mutex<CriticalSectionRawMutex, AsyncBME280<i2c::I2c<'static, I2C0, Async>>>, Mutex::new(bme280)),
|
||||
);
|
||||
|
||||
Some(StateMessage::new(
|
||||
controller.get_state(),
|
||||
bme280.measure(&mut delay).await.unwrap(),
|
||||
))
|
||||
}
|
||||
Ok(_) => None,
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
info!("Restarting in 5s...");
|
||||
Timer::after(Duration::from_secs(5)).await;
|
||||
info!("Restarting...");
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
}
|
||||
},
|
||||
Either3::Third(state) => Some(StateMessage::new(
|
||||
state,
|
||||
bme280.measure(&mut delay).await.unwrap(),
|
||||
)),
|
||||
};
|
||||
let port = 80;
|
||||
let mut tcp_rx_buffer = [0; 1024];
|
||||
let mut tcp_tx_buffer = [0; 1024];
|
||||
let mut http_buffer = [0; 2048];
|
||||
|
||||
if let Some(message) = message {
|
||||
client
|
||||
.send_message(TOPIC_BASE, &message.vec(), QualityOfService::QoS1, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
spawner.must_spawn(fan_task(shared_controller));
|
||||
|
||||
// We can only handle one request at a time
|
||||
picoserve::listen_and_serve_with_state(
|
||||
0,
|
||||
&app,
|
||||
config,
|
||||
stack,
|
||||
port,
|
||||
&mut tcp_rx_buffer,
|
||||
&mut tcp_tx_buffer,
|
||||
&mut http_buffer,
|
||||
&AppState {
|
||||
shared_controller,
|
||||
shared_bme280,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user