Compare commits
No commits in common. "master" and "v0.0.3" have entirely different histories.
1354
Cargo.lock
generated
1354
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
101
Cargo.toml
101
Cargo.toml
|
@ -1,87 +1,80 @@
|
|||
[package]
|
||||
name = "air_filter"
|
||||
version = "0.4.5"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace.dependencies]
|
||||
defmt = "0.3"
|
||||
bme280 = { version = "0.5.0", features = ["async", "defmt"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
|
||||
[dependencies]
|
||||
air_filter_types = { path = "./air_filter_types/" }
|
||||
cortex-m = { version = "0.7", features = ["inline-asm"] }
|
||||
cortex-m-rt = "0.7"
|
||||
defmt = { workspace = true }
|
||||
defmt = "0.3"
|
||||
defmt-rtt = "0.4"
|
||||
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-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-rp = { version = "0.3", features = [
|
||||
"rp2040",
|
||||
"defmt",
|
||||
"unstable-pac",
|
||||
"time-driver",
|
||||
"critical-section-impl",
|
||||
"intrinsics",
|
||||
"rom-v2-intrinsics",
|
||||
embassy-rp = { version = "0.1", features = [
|
||||
"defmt",
|
||||
"unstable-pac",
|
||||
"time-driver",
|
||||
"critical-section-impl",
|
||||
"intrinsics",
|
||||
"rom-v2-intrinsics",
|
||||
] }
|
||||
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-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-net = { version = "0.6", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"dhcpv4-hostname",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
embassy-net = { version = "0.4", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
] }
|
||||
embassy-sync = { version = "0.6", features = ["defmt"] }
|
||||
embassy-sync = { version = "0.5", 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.3", features = ["defmt", "firmware-logs"] }
|
||||
cyw43-pio = { version = "0.3", features = ["defmt"] }
|
||||
cyw43 = { version = "0.1", features = ["defmt", "firmware-logs"] }
|
||||
cyw43-pio = { version = "0.1", features = ["defmt"] }
|
||||
rand = { version = "0.8", features = [
|
||||
"nightly",
|
||||
"small_rng",
|
||||
"std_rng",
|
||||
"nightly",
|
||||
"small_rng",
|
||||
"std_rng",
|
||||
], default-features = false }
|
||||
rust-mqtt = { version = "0.3", features = [
|
||||
"defmt",
|
||||
"no_std",
|
||||
"tls",
|
||||
rust-mqtt = { version = "0.2", features = [
|
||||
"defmt",
|
||||
"no_std",
|
||||
"tls",
|
||||
], default-features = false }
|
||||
const_format = "0.2"
|
||||
git-version = "0.3"
|
||||
serde = { workspace = true }
|
||||
heapless = { version = "0.8", features = ["defmt-03", "serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
heapless = { version = "0.7", features = ["defmt", "serde"] }
|
||||
serde-json-core = "0.5"
|
||||
nourl = { version = "0.1", features = ["defmt"] }
|
||||
# Embassy hardcodes a max of 6 dns servers, if there are more it crashes. This is a workaround
|
||||
# Embassy harfcodes 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.12", default-features = false, features = [
|
||||
"dns-max-server-count-4",
|
||||
smoltcp = { version = "0.11", default-features = false, features = [
|
||||
"dns-max-server-count-4",
|
||||
] }
|
||||
updater = { git = "https://git.huizinga.dev/Dreaded_X/iot_tools", tag = "v0.3.0" }
|
||||
updater = { version = "0.1.0", path = "../iot_tools/updater" }
|
||||
portable-atomic = { version = "1.6", features = ["critical-section"] }
|
||||
bme280 = { workspace = true }
|
||||
picoserve = { version = "0.14", features = ["defmt", "embassy"] }
|
||||
embedded-storage = "0.3"
|
||||
bme280 = { version = "0.5.0", features = ["async", "defmt"] }
|
||||
|
||||
[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" }
|
||||
|
||||
[features]
|
||||
include_firmwares = []
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
[package]
|
||||
name = "air_filter_types"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bme280 = { workspace = true }
|
||||
defmt = { workspace = true }
|
||||
serde = { workspace = true }
|
|
@ -1,81 +0,0 @@
|
|||
#![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, Deserialize)]
|
||||
#[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 }
|
||||
}
|
||||
|
||||
pub fn speed(&self) -> FanSpeed {
|
||||
self.speed
|
||||
}
|
||||
|
||||
pub fn manual(&self) -> bool {
|
||||
self.manual
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SetFanSpeed {
|
||||
speed: FanSpeed,
|
||||
}
|
||||
|
||||
impl SetFanSpeed {
|
||||
pub fn new(speed: FanSpeed) -> Self {
|
||||
Self { speed }
|
||||
}
|
||||
|
||||
pub fn speed(&self) -> FanSpeed {
|
||||
self.speed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn temperature(&self) -> f32 {
|
||||
self.temperature
|
||||
}
|
||||
|
||||
pub fn humidity(&self) -> f32 {
|
||||
self.humidity
|
||||
}
|
||||
|
||||
pub fn pressure(&self) -> f32 {
|
||||
self.pressure
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
mkdir -p target/firmware
|
||||
cargo objcopy --release -- -O binary target/firmware/firmware
|
||||
cargo objcopy --release --features=include_firmwares -- -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-2025-01-09"
|
||||
channel = "nightly-2024-03-01"
|
||||
targets = ["thumbv6m-none-eabi"]
|
||||
components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools"]
|
||||
components = ["rustfmt", "clippy", "llvm-tools"]
|
||||
|
|
616
src/main.rs
616
src/main.rs
|
@ -1,62 +1,118 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
|
||||
use core::{cell::RefCell, net::Ipv4Addr, str::FromStr};
|
||||
use core::fmt::Debug;
|
||||
use core::{cell::RefCell, str::from_utf8};
|
||||
|
||||
use approuter::{AppRouter, AppState};
|
||||
use bme280::i2c::AsyncBME280;
|
||||
use cyw43::{JoinOptions, PowerManagementMode};
|
||||
use cyw43_pio::{PioSpi, DEFAULT_CLOCK_DIVIDER};
|
||||
use defmt::{debug, info, warn};
|
||||
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 embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater, FirmwareUpdaterConfig};
|
||||
use embassy_embedded_hal::flash::partition::BlockingPartition;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_net::{tcp::TcpSocket, Config, DhcpConfig, Stack, StackResources};
|
||||
use embassy_futures::select::{select3, select4, Either3};
|
||||
use embassy_net::{dns::DnsQueryType, tcp::TcpSocket, Config, Stack, StackResources};
|
||||
use embassy_rp::{
|
||||
bind_interrupts,
|
||||
clocks::RoscRng,
|
||||
flash::{Flash, WRITE_SIZE},
|
||||
gpio::{Flex, Input, Level, Output, Pin, Pull},
|
||||
i2c::{self},
|
||||
peripherals::{DMA_CH1, FLASH, I2C0, PIO0},
|
||||
i2c,
|
||||
peripherals::{DMA_CH1, I2C0, PIN_23, PIN_25, PIO0},
|
||||
pio::{self, Pio},
|
||||
Peripheral,
|
||||
};
|
||||
use embassy_sync::{
|
||||
blocking_mutex::{
|
||||
self,
|
||||
raw::{CriticalSectionRawMutex, NoopRawMutex},
|
||||
},
|
||||
mutex::Mutex,
|
||||
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 embassy_time::{Delay, Duration, Timer};
|
||||
use heapless::String;
|
||||
use picoserve::{make_static, Router};
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
use static_cell::StaticCell;
|
||||
use rust_mqtt::{
|
||||
client::{
|
||||
client::MqttClient,
|
||||
client_config::{ClientConfig, MqttVersion},
|
||||
},
|
||||
packet::v5::publish_packet::QualityOfService,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_cell::make_static;
|
||||
|
||||
use {defmt_rtt as _, panic_probe as _};
|
||||
|
||||
use air_filter_types::{FanSpeed, FanState};
|
||||
|
||||
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;
|
||||
|
||||
struct Controller<'a> {
|
||||
off: Input<'a>,
|
||||
low: Flex<'a>,
|
||||
medium: Flex<'a>,
|
||||
high: Flex<'a>,
|
||||
#[derive(Deserialize)]
|
||||
struct SetMessage {
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl<'a> Controller<'a> {
|
||||
pub fn new<O: Pin, L: Pin, M: Pin, H: Pin>(
|
||||
#[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(
|
||||
off: impl Peripheral<P = O> + 'a,
|
||||
low: impl Peripheral<P = L> + 'a,
|
||||
medium: impl Peripheral<P = M> + 'a,
|
||||
|
@ -84,213 +140,83 @@ impl<'a> Controller<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&mut self) -> FanState {
|
||||
pub fn get_state(&mut self) -> (State, bool) {
|
||||
let manual = self.off.is_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,
|
||||
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,
|
||||
(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_speed(FanSpeed::Off);
|
||||
FanSpeed::Off
|
||||
self.set_state(State::Off);
|
||||
State::Off
|
||||
}
|
||||
};
|
||||
|
||||
FanState { speed, manual }
|
||||
(state, manual)
|
||||
}
|
||||
|
||||
pub fn set_speed(&mut self, speed: FanSpeed) -> bool {
|
||||
pub fn set_state(&mut self, state: State) {
|
||||
let manual = self.off.is_high();
|
||||
|
||||
if manual && speed != FanSpeed::Off {
|
||||
if manual && state != State::Off {
|
||||
warn!("Filter is manual controlled, cannot control remotely");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("Setting state: {}", speed);
|
||||
debug!("Setting state: {}", state);
|
||||
|
||||
match speed {
|
||||
FanSpeed::Off => {
|
||||
match state {
|
||||
State::Off => {
|
||||
self.low.set_as_input();
|
||||
self.medium.set_as_input();
|
||||
self.high.set_as_input();
|
||||
}
|
||||
FanSpeed::Low => {
|
||||
State::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();
|
||||
}
|
||||
FanSpeed::Medium => {
|
||||
State::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();
|
||||
}
|
||||
FanSpeed::High => {
|
||||
State::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 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() {
|
||||
self.set_speed(FanSpeed::Off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need this because otherwise the compiler yells at us
|
||||
mod approuter {
|
||||
use air_filter_types::{SensorData, SetFanSpeed};
|
||||
use bme280::i2c::AsyncBME280;
|
||||
use defmt::debug;
|
||||
use embassy_boot::BlockingFirmwareUpdater;
|
||||
use embassy_rp::{
|
||||
i2c::{Async, I2c},
|
||||
peripherals::I2C0,
|
||||
};
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use embassy_time::Delay;
|
||||
use picoserve::{
|
||||
extract, make_static,
|
||||
routing::{get, PathRouter},
|
||||
Router,
|
||||
};
|
||||
use updater::firmware_router;
|
||||
|
||||
use crate::{Controller, Partition};
|
||||
|
||||
const VERSION: &str = git_version::git_version!();
|
||||
const PUBLIC_SIGNING_KEY: &[u8; 32] = include_bytes!("../key.pub");
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SharedController(pub &'static Mutex<CriticalSectionRawMutex, Controller<'static>>);
|
||||
|
||||
impl SharedController {
|
||||
fn new(controller: Controller<'static>) -> Self {
|
||||
Self(
|
||||
make_static!(Mutex<CriticalSectionRawMutex, Controller<'static>>, Mutex::new(controller)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SharedBME280(
|
||||
pub &'static Mutex<CriticalSectionRawMutex, AsyncBME280<I2c<'static, I2C0, Async>>>,
|
||||
);
|
||||
|
||||
impl SharedBME280 {
|
||||
fn new(bme280: AsyncBME280<I2c<'static, I2C0, Async>>) -> Self {
|
||||
Self(
|
||||
make_static!(Mutex<CriticalSectionRawMutex, AsyncBME280<I2c<'static, I2C0, Async>>>, Mutex::new(bme280)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub shared_controller: SharedController,
|
||||
pub shared_bme280: SharedBME280,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(
|
||||
controller: Controller<'static>,
|
||||
bme280: AsyncBME280<I2c<'static, I2C0, Async>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
shared_controller: SharedController::new(controller),
|
||||
shared_bme280: SharedBME280::new(bme280),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
debug!("Getting fan state");
|
||||
|
||||
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 {
|
||||
debug!("Setting fan state");
|
||||
|
||||
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 {
|
||||
debug!("Getting sensor state");
|
||||
|
||||
let measurement = bme280
|
||||
.lock()
|
||||
.await
|
||||
.measure(&mut Delay {})
|
||||
.await
|
||||
.expect("Measurement should work");
|
||||
let sensor_data = SensorData::new(measurement);
|
||||
|
||||
picoserve::response::Json(sensor_data)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub type AppRouter = impl PathRouter<AppState>;
|
||||
pub fn make_app(
|
||||
updater: &'static Mutex<
|
||||
CriticalSectionRawMutex,
|
||||
BlockingFirmwareUpdater<'static, Partition, Partition>,
|
||||
>,
|
||||
) -> Router<AppRouter, AppState> {
|
||||
Router::new().nest("/state", state_router()).nest(
|
||||
"/firmware",
|
||||
firmware_router(VERSION, updater, PUBLIC_SIGNING_KEY),
|
||||
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;
|
||||
|
||||
if self.off.is_high() {
|
||||
// If the filter is in manual mode, set the pico outputs to off
|
||||
self.set_state(State::Off);
|
||||
}
|
||||
|
||||
// Get the current state
|
||||
self.get_state()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,59 +250,36 @@ unsafe fn get_firmware() -> (&'static [u8], &'static [u8]) {
|
|||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn cyw43_task(
|
||||
runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH1>>,
|
||||
) -> ! {
|
||||
runner.run().await
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! {
|
||||
runner.run().await
|
||||
}
|
||||
|
||||
type Partition = BlockingPartition<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
Flash<'static, FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>,
|
||||
>;
|
||||
|
||||
const WEB_TASK_POOL_SIZE: usize = 8;
|
||||
|
||||
#[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)]
|
||||
async fn web_task(
|
||||
id: usize,
|
||||
stack: Stack<'static>,
|
||||
app: &'static Router<AppRouter, AppState>,
|
||||
config: &'static picoserve::Config<Duration>,
|
||||
state: AppState,
|
||||
) -> ! {
|
||||
let port = 80;
|
||||
let mut tcp_rx_buffer = [0; 1024];
|
||||
let mut tcp_tx_buffer = [0; 1024];
|
||||
let mut http_buffer = [0; 2048];
|
||||
|
||||
picoserve::listen_and_serve_with_state(
|
||||
id,
|
||||
app,
|
||||
config,
|
||||
stack,
|
||||
port,
|
||||
&mut tcp_rx_buffer,
|
||||
&mut tcp_tx_buffer,
|
||||
&mut http_buffer,
|
||||
&state,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn controller_task(state: AppState) {
|
||||
loop {
|
||||
state.shared_controller.0.lock().await.check_for_manual();
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
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>,
|
||||
>,
|
||||
) -> ! {
|
||||
runner.run().await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn net_task(stack: &'static Stack<cyw43::NetDriver<'static>>) -> ! {
|
||||
stack.run().await
|
||||
}
|
||||
|
||||
#[embassy_executor::main]
|
||||
|
@ -384,15 +287,18 @@ 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 = make_static!(blocking_mutex::Mutex<NoopRawMutex, RefCell<Flash<'static, FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>>>, blocking_mutex::Mutex::new(RefCell::new(flash)));
|
||||
let flash = Mutex::new(RefCell::new(flash));
|
||||
|
||||
let config = FirmwareUpdaterConfig::from_linkerfile_blocking(flash, flash);
|
||||
let aligned = make_static!(AlignedBuffer<WRITE_SIZE>, AlignedBuffer([0; WRITE_SIZE]));
|
||||
let config = FirmwareUpdaterConfig::from_linkerfile_blocking(&flash);
|
||||
let mut aligned = AlignedBuffer([0; WRITE_SIZE]);
|
||||
let updater = BlockingFirmwareUpdater::new(config, &mut aligned.0);
|
||||
let updater = make_static!(Mutex<CriticalSectionRawMutex, BlockingFirmwareUpdater<'static, Partition, Partition>>, Mutex::new(updater));
|
||||
|
||||
let controller = Controller::new(p.PIN_28, p.PIN_27, p.PIN_26, p.PIN_22);
|
||||
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 i2c = i2c::I2c::new_async(p.I2C0, p.PIN_9, p.PIN_8, Irqs, i2c::Config::default());
|
||||
let mut bme280 = AsyncBME280::new_primary(i2c);
|
||||
|
@ -406,7 +312,6 @@ async fn main(spawner: Spawner) {
|
|||
let spi = PioSpi::new(
|
||||
&mut pio.common,
|
||||
pio.sm0,
|
||||
DEFAULT_CLOCK_DIVIDER,
|
||||
pio.irq0,
|
||||
cs,
|
||||
p.PIN_24,
|
||||
|
@ -416,10 +321,9 @@ async fn main(spawner: Spawner) {
|
|||
|
||||
let (fw, clm) = unsafe { get_firmware() };
|
||||
|
||||
static STATE: StaticCell<cyw43::State> = StaticCell::new();
|
||||
let state = STATE.init(cyw43::State::new());
|
||||
let state = make_static!(cyw43::State::new());
|
||||
let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await;
|
||||
spawner.spawn(cyw43_task(runner)).unwrap();
|
||||
spawner.spawn(wifi_task(runner)).unwrap();
|
||||
|
||||
control.init(clm).await;
|
||||
control
|
||||
|
@ -429,30 +333,25 @@ async fn main(spawner: Spawner) {
|
|||
// Turn LED on while trying to connect
|
||||
control.gpio_set(0, true).await;
|
||||
|
||||
let mut config: DhcpConfig = Default::default();
|
||||
config.hostname = Some(String::from_str("airfilter").expect("Is valid"));
|
||||
let config = Config::dhcpv4(config);
|
||||
let config = Config::dhcpv4(Default::default());
|
||||
|
||||
// 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();
|
||||
|
||||
static RESOURCES: StaticCell<StackResources<16>> = StaticCell::new();
|
||||
let (stack, runner) = embassy_net::new(
|
||||
let stack = make_static!(Stack::new(
|
||||
net_device,
|
||||
config,
|
||||
RESOURCES.init(StackResources::new()),
|
||||
make_static!(StackResources::<6>::new()),
|
||||
rng.next_u64(),
|
||||
);
|
||||
spawner.spawn(net_task(runner)).unwrap();
|
||||
));
|
||||
|
||||
spawner.spawn(net_task(stack)).unwrap();
|
||||
|
||||
// Connect to wifi
|
||||
loop {
|
||||
match control
|
||||
.join(
|
||||
env!("WIFI_NETWORK"),
|
||||
JoinOptions::new(env!("WIFI_PASSWORD").as_bytes()),
|
||||
)
|
||||
.join_wpa2(env!("WIFI_NETWORK"), env!("WIFI_PASSWORD"))
|
||||
.await
|
||||
{
|
||||
Ok(_) => break,
|
||||
|
@ -462,56 +361,145 @@ async fn main(spawner: Spawner) {
|
|||
}
|
||||
}
|
||||
|
||||
// Mark the update as successful after connecting to the wifi
|
||||
updater.lock().await.mark_booted().unwrap();
|
||||
info!("Waiting for DHCP...");
|
||||
let cfg = wait_for_config(stack).await;
|
||||
info!("IP Address: {}", cfg.address.address());
|
||||
|
||||
info!("Waiting for link up...");
|
||||
stack.wait_link_up().await;
|
||||
info!("Link is up!");
|
||||
let mut rx_buffer = [0; 1024];
|
||||
let mut tx_buffer = [0; 1024];
|
||||
|
||||
info!("Waiting for stack to be up...");
|
||||
stack.wait_config_up().await;
|
||||
info!("Stack 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();
|
||||
|
||||
// Turn LED off when connected
|
||||
control.gpio_set(0, false).await;
|
||||
|
||||
let cfg = stack.config_v4().expect("Config is up");
|
||||
info!("IP Address: {}", cfg.address.address());
|
||||
|
||||
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()
|
||||
);
|
||||
|
||||
let app = make_static!(Router<AppRouter, AppState>, approuter::make_app(updater));
|
||||
|
||||
let state = AppState::new(controller, bme280);
|
||||
|
||||
for id in 0..WEB_TASK_POOL_SIZE {
|
||||
spawner.must_spawn(web_task(id, stack, app, config, state.clone()));
|
||||
}
|
||||
|
||||
spawner.must_spawn(controller_task(state));
|
||||
|
||||
let mut rx_buffer = [0; 4096];
|
||||
let mut tx_buffer = [0; 4096];
|
||||
|
||||
let mut keep_alive = Ticker::every(Duration::from_secs(30));
|
||||
loop {
|
||||
// TODO: In the future, use reqless to push the current state to automation_rs
|
||||
control.gpio_set(0, true).await;
|
||||
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
||||
socket.set_timeout(Some(Duration::from_secs(1)));
|
||||
socket
|
||||
.connect(("10.0.0.2".parse::<Ipv4Addr>().unwrap(), 80))
|
||||
.await
|
||||
.ok();
|
||||
control.gpio_set(0, false).await;
|
||||
Timer::after(Duration::from_secs(60)).await;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = updater.update(url, stack, &mut rng, &mut client).await {
|
||||
error!("Update failed: {}", err);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
controller.set_state(message.get_state());
|
||||
|
||||
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(),
|
||||
)),
|
||||
};
|
||||
|
||||
if let Some(message) = message {
|
||||
client
|
||||
.send_message(TOPIC_BASE, &message.vec(), QualityOfService::QoS1, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user