diff --git a/Cargo.lock b/Cargo.lock index 0162aef..9d2237d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ascii-canvas" @@ -1108,6 +1108,16 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libscd" +version = "0.5.0" +source = "git+https://github.com/SvetlinZarev/libscd.git#9fda7e0b0d6f074717095d0b1e3f44d316fd9e28" +dependencies = [ + "defmt", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "litrs" version = "0.4.1" @@ -1170,8 +1180,10 @@ dependencies = [ "fixed", "fixed-macro", "heapless 0.8.0", + "libscd", "log", "mipidsi", + "numtoa", "panic-probe", "picoserve", "portable-atomic", @@ -1277,6 +1289,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + [[package]] name = "once_cell" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index 49d5da4..eaa72d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] # web server picoserve = { version = "0.15.0", path = "../picoserve/picoserve/", features = ["embassy"] } +libscd = { version = "0.5.0", git = "https://github.com/SvetlinZarev/libscd.git", features = ["async", "sync", "scd4x", "defmt"] } embassy-embedded-hal = { version = "0.3.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] } embassy-sync = { version = "0.6.2", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] } embassy-executor = { version = "0.7.0", git = "https://github.com/embassy-rs/embassy.git", features = ["task-arena-size-98304", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } @@ -61,6 +62,7 @@ portable-atomic = { version = "1.5", features = ["critical-section"] } log = "0.4" rand = { version = "0.8.5", default-features = false } embedded-sdmmc = "0.7.0" +numtoa = "0.2.4" [profile.release] debug = 2 diff --git a/src/main.rs b/src/main.rs index e73f8d6..8b79696 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,28 +8,35 @@ use cyw43::JoinOptions; use cyw43_pio::{PioSpi, DEFAULT_CLOCK_DIVIDER}; use embassy_executor::Spawner; -use embassy_net::{Config, StackResources, Ipv4Cidr, Ipv4Address}; +use embassy_net::{StackResources, Ipv4Cidr, Ipv4Address}; use embassy_rp::bind_interrupts; use embassy_rp::clocks::RoscRng; use embassy_rp::gpio::{Level, Output}; +use embassy_rp::i2c::{self, Config}; use embassy_rp::peripherals::{DMA_CH0, PIO0}; use embassy_rp::peripherals::USB; +use embassy_rp::peripherals::I2C1; use embassy_rp::pio::{InterruptHandler, Pio}; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; use embassy_rp::usb::{Driver, InterruptHandler as USBInterruptHandler}; -use embassy_time::{Duration, Timer}; +use embassy_time::{Duration, Timer, Delay}; use heapless::Vec; +use libscd::synchronous::scd4x::Scd4x; use picoserve::{ make_static, - routing::{get_service, PathRouter}, - AppBuilder, AppRouter + routing::{get, get_service, PathRouter}, + AppWithStateBuilder, AppRouter, + response::DebugValue }; use picoserve::response::File; +use picoserve::extract::State; use rand::RngCore; use static_cell::StaticCell; use {defmt_rtt as _, panic_probe as _}; const WIFI_NETWORK: &str = "bendybogalow"; const WIFI_PASSWORD: &str = "parsnipcabbageonion"; +const INDEX: &str = include_str!("html/index.html"); bind_interrupts!(struct Irqs { PIO0_IRQ_0 => InterruptHandler; @@ -54,23 +61,85 @@ async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'sta runner.run().await } +/* +async fn co2ppm_to_str { + let mut buff : [u8; 20] = [0u8; 20]; + unsafe { + CO2PPM.numtoa(10, &mut buff); + } + let buffstr: &str = core::str::from_utf8(&mut buff).unwrap(); + log::info!("CO2PPM from build_app: {}", buffstr); + "1234" // works + // buffstr // fails for the obvious reason of lifetime +} +*/ +/* +impl Content for AtomicU16 { + fn content_type(&self) -> &'static str { + "text/plain; charset=utf-8" + } + fn content_length(&self) -> usize { + 5; + } + async fn write_content(self, writer: W) -> Result<(), W::Error> { + "fooo".as_bytes().write_content(writer).await + } +} +*/ +/* +struct Number { + value: u16; +} + +async fn get_number(Number { value }: Number) -> impl IntoResponse { + picoserve::response::DebugValue(value) +} +*/ + +struct CO2PPM { + co2ppm : u16, +} +#[derive(Clone, Copy)] +struct SharedPPM(&'static Mutex); +struct AppState { + shared_ppm : SharedPPM, +} +impl picoserve::extract::FromRef for SharedPPM { + fn from_ref(state: &AppState) -> Self { + state.shared_ppm + } +} // picoserve HTTP code kicked off using: https://github.com/sammhicks/picoserve/blob/main/examples/embassy/hello_world/src/main.rs struct AppProps; -impl AppBuilder for AppProps { - type PathRouter = impl PathRouter; +impl AppWithStateBuilder for AppProps { + type State = AppState; + type PathRouter = impl PathRouter; + + fn build_app(self) -> picoserve::Router { + //let Self { } = self; + + /*let mut buff : [u8; 20] = [0u8; 20]; + unsafe { + let _ = CO2PPM.numtoa(10, &mut buff); + } + let buffstr: &str = core::str::from_utf8(&mut buff).unwrap(); + log::info!("CO2PPM from build_app: {}", buffstr);*/ - fn build_app(self) -> picoserve::Router { picoserve::Router::new() .route( "/", - get_service(File::html(include_str!("html/index.html"))) + get_service(File::html(INDEX)) // .replace("%{CO2}%", CO2PPM.to_string()))) ) .route( "/main.css", get_service(File::css(include_str!("html/main.css"))) ) + .route( + "/data/co2", + get(|State(SharedPPM(co2ppm)): State| async move { DebugValue( co2ppm.lock().await.co2ppm ) }), + ) } } @@ -83,13 +152,14 @@ async fn web_task( stack: embassy_net::Stack<'static>, app: &'static AppRouter, config: &'static picoserve::Config, + 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( + picoserve::listen_and_serve_with_state( id, app, config, @@ -98,6 +168,7 @@ async fn web_task( &mut tcp_rx_buffer, &mut tcp_tx_buffer, &mut http_buffer, + &state, ) .await } @@ -154,8 +225,9 @@ async fn main(spawner: Spawner) { .await; //let config = Config::dhcpv4(Default::default()); + log::info!("main: configure static IP"); let config = embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { - address: Ipv4Cidr::new(Ipv4Address::new(192, 168, 3, 14), 24), + address: Ipv4Cidr::new(Ipv4Address::new(192, 168, 3, 15), 24), dns_servers: Vec::new(), gateway: Some(Ipv4Address::new(192, 168, 3, 1)), }); @@ -164,11 +236,13 @@ async fn main(spawner: Spawner) { let seed = rng.next_u64(); // Init network stack + log::info!("main: init network stack"); static RESOURCES: StaticCell> = StaticCell::new(); let (stack, runner) = embassy_net::new(net_device, config, RESOURCES.init(StackResources::new()), seed); defmt::unwrap!(spawner.spawn(net_task(runner))); + log::info!("main: await network join"); loop { match control .join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes())) @@ -179,6 +253,7 @@ async fn main(spawner: Spawner) { log::error!("join failed with status={}", err.status); } } + Timer::after_millis(100).await; } // Wait for DHCP, not necessary when using static IP @@ -188,7 +263,39 @@ async fn main(spawner: Spawner) { } log::info!("DHCP is now up!");*/ - // And now we can use it! + + log::info!("Starting I2C Comms with SCD40"); + Timer::after_secs(1).await; + // this code derived from: https://github.com/SvetlinZarev/libscd/blob/main/examples/embassy-scd4x/src/main.rs + let sda = p.PIN_26; + let scl = p.PIN_27; + let i2c = i2c::I2c::new_blocking(p.I2C1, scl, sda, Config::default()); + log::info!("Initialise Scd4x"); + Timer::after_secs(1).await; + let mut scd = Scd4x::new(i2c, Delay); + + // When re-programming, the controller will be restarted, + // but not the sensor. We try to stop it in order to + // prevent the rest of the commands failing. + log::info!("Stop periodic measurements"); + Timer::after_secs(1).await; + _ = scd.stop_periodic_measurement(); + + log::info!("Sensor serial number: {:?}", scd.serial_number()); + Timer::after_secs(1).await; + if let Err(e) = scd.start_periodic_measurement() { + log::error!("Failed to start periodic measurement: {:?}", e ); + } + + let co2ppm = 69; + let shared_ppm = SharedPPM( + make_static!(Mutex, Mutex::new(CO2PPM { co2ppm })), + ); + spawner.must_spawn(read_co2(scd, shared_ppm)); + + log::info!("Commence HTTP service"); + Timer::after_secs(1).await; + let app = make_static!(AppRouter, AppProps.build_app()); let config = make_static!( @@ -202,7 +309,13 @@ async fn main(spawner: Spawner) { ); for id in 0..WEB_TASK_POOL_SIZE { - spawner.must_spawn(web_task(id, stack, app, config)); + spawner.must_spawn(web_task( + id, + stack, + app, + config, + AppState{ shared_ppm }, + )); } /* let mut rx_buffer = [0; 4096]; @@ -258,3 +371,23 @@ async fn main(spawner: Spawner) { } */ } + +#[embassy_executor::task] +async fn read_co2( + mut scd: Scd4x, Delay>, + shared_ppm: SharedPPM +) { + log::info!("Enter sensor read loop"); + Timer::after_secs(1).await; + loop { + if scd.data_ready().unwrap() { + let m = scd.read_measurement().unwrap(); + shared_ppm.0.lock().await.co2ppm = m.co2; + log::info!( + "CO2: {}\nHumidity: {}\nTemperature: {}", m.co2, m.humidity, m.temperature + ) + } + Timer::after_secs(1).await; + } +} +