From 6b54fb570a9c7b4e1d850c562ba09711e487d71d Mon Sep 17 00:00:00 2001 From: Jakob Dalsgaard Date: Wed, 22 Jan 2025 00:49:05 +0100 Subject: [PATCH] Added data, api/v1 and js --- Cargo.lock | 108 +++++++++++++++++- Cargo.toml | 15 ++- src/database.rs | 159 ++++++++++++++++++++++++++ src/httpd.rs | 173 +++++++++++++++++++++++++--- src/main.rs | 31 ++++- src/serial.rs | 205 ++++++++++++++++++++++++++++++++-- src/sntp_client.rs | 12 +- src/static/app.js | 23 ++++ src/static/espressif-logo.png | Bin 7727 -> 3923 bytes src/static/risc-v-logo.png | Bin 12169 -> 4981 bytes src/static/styles.css | 18 +++ src/static/wallas-logo.png | Bin 19114 -> 7399 bytes 12 files changed, 704 insertions(+), 40 deletions(-) create mode 100644 src/database.rs create mode 100644 src/static/app.js diff --git a/Cargo.lock b/Cargo.lock index 234e4d4..86b5e34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "anyhow" version = "1.0.95" @@ -59,6 +65,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.0" @@ -80,6 +92,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8a42181e0652c2997ae4d217f25b63c5337a52fd2279736e97b832fa0a3cff" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -604,6 +625,16 @@ dependencies = [ "vcell", ] +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -694,6 +725,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "include_file_compress" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6823f4ab0232f685fd0320c7210f39a5264bae4b223e45c1910e04d795df75" +dependencies = [ + "cast", + "flate2", + "quote", + "syn", + "thiserror", +] + [[package]] name = "indexmap" version = "2.7.0" @@ -755,6 +799,7 @@ version = "0.26.0" dependencies = [ "itoa", "maud_macros", + "picoserve", ] [[package]] @@ -784,6 +829,21 @@ dependencies = [ "serde", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + [[package]] name = "mutex-trait" version = "0.2.0" @@ -805,6 +865,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -845,6 +915,7 @@ dependencies = [ "futures-util", "heapless", "lhash", + "log", "ryu", "serde", "serde-json-core", @@ -1092,6 +1163,16 @@ dependencies = [ "managed", ] +[[package]] +name = "sntpc" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2927eedca9d3b301b1eb88b81a2e415666ec5a84eb24f1c2129c08fc98ff18be" +dependencies = [ + "cfg-if", + "embassy-net", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1155,6 +1236,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.19" @@ -1220,9 +1321,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] -name = "wallas-embassy" +name = "wallas-esp32c3" version = "0.1.0" dependencies = [ + "chrono", "critical-section", "embassy-executor", "embassy-net", @@ -1237,11 +1339,15 @@ dependencies = [ "esp-println", "esp-wifi", "heapless", + "include_file_compress", "log", "maud", + "nom", "picoserve", "rand_core", + "serde", "smoltcp 0.11.0", + "sntpc", "static_cell", ] diff --git a/Cargo.toml b/Cargo.toml index 3623531..192debc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ esp-alloc = { version = "0.5.0" } embedded-io = "0.6.1" embedded-io-async = "0.6.1" -embassy-net = { version = "0.5.0", features = [ "tcp", "udp", "dhcpv4", "medium-ethernet"] } +embassy-net = { version = "0.5.0", features = [ "tcp", "udp", "dhcpv4", "dns", "medium-ethernet"] } esp-wifi = { version = "0.11.0", default-features=false, features = [ "esp32c3", @@ -31,6 +31,7 @@ esp-wifi = { version = "0.11.0", default-features=false, features = [ ] } embassy-sync = "0.6.1" rand_core = "0.6.4" +nom = { version = "7.1.3", default-features = false, features = [ "alloc" ] } heapless = { version = "0.8.0", default-features = false } smoltcp = { version = "0.11.0", default-features = false, features = [ "medium-ethernet", @@ -44,16 +45,22 @@ smoltcp = { version = "0.11.0", default-features = false, features = [ "socket-udp", ] } embassy-executor = { version = "0.6.0", features = [ - "task-arena-size-40960" + "task-arena-size-163840" ] } embassy-time = { version = "0.3.1", features = ["generic-queue-8"] } esp-hal-embassy = { version = "0.5.0", features = ["esp32c3"] } static_cell = { version = "2.1.0", features = ["nightly"] } critical-section = "1.2.0" -maud = { path = "/home/jda/src/rust/maud/target/package/maud-0.26.0", features = ["alloc"] } +maud = { path = "/home/jda/src/rust/maud/target/package/maud-0.26.0", features = ["alloc", "picoserve"] } picoserve = { version = "0.13.3", default-features = false, features = [ - "embassy" + "alloc", + "embassy", + "log", ] } +sntpc = { version = "0.5.1", default-features = false, features = [ "embassy-socket" ] } +chrono = { version = "0.4.39", default-features = false, features = [ "alloc" ] } +serde = { version = "1.0.217", default-features = false } +include_file_compress = "0.1.3" [profile.dev] # Rust debug is too slow. diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..f05297f --- /dev/null +++ b/src/database.rs @@ -0,0 +1,159 @@ + + +use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex, RawMutex}; +use embassy_sync::blocking_mutex::Mutex; +use embassy_time::Instant; +use alloc::vec::Vec; +use core::cell::RefCell; +use crate::serial; + +pub struct DataPoint { + pub instant: Instant, + t1: i8, + t2: i8, + pub target: i8, + pub current: i8, +} + +impl Copy for DataPoint { } + +impl Clone for DataPoint { + fn clone(&self) -> Self { + *self + } +} + +impl DataPoint { + pub const fn default() -> Self { + DataPoint { instant: Instant::from_ticks(0), t1: 0, t2: 0, target: 0, current: 0 } + } +} + +struct RingBuffer { + buf: [T; CAP], + index: usize, + has_wrapped: bool, +} + +impl RingBuffer { + pub const fn new(def: T) -> Self { + Self { + buf: [def; CAP], index: 0, has_wrapped: false + } + } + + pub fn size(&self) -> usize { + if self.has_wrapped { + CAP + } else { self.index } + } + + /** + * Get latest pushed item (youngest item) + */ + pub fn get_latest(&self) -> Option { + if self.index == 0 && self.has_wrapped { + Some(self.buf[CAP-1]) + } else if self.index == 0 { + None + } else { + Some(self.buf[self.index-1]) + } + } + + /** + * Get oldest item in the ringbuffer + */ + pub fn get_first(&self) -> Option { + if self.has_wrapped { + Some(self.buf[self.index]) + } else if self.index > 0 { + Some(self.buf[0]) + } else { + None + } + } + + /** + * Get all items in the buffer, oldest at index 0, + * youngest at index CAP. + */ + pub fn get_all(&self) -> Vec { + let size = self.size(); + let mut res = Vec::::with_capacity(size); + let mut i = if self.has_wrapped { self.index } else { 0 }; + + for _ in 0..size { + res.push(self.buf[i]); + i = i + 1; + if i >= CAP { + i = 0; + } + } + + res + } + + /** + * Push an item to the buffer + */ + pub fn push(&mut self, val: T) { + self.buf[self.index] = val; + self.index = self.index + 1; + if self.index >= CAP { + self.has_wrapped = true; + self.index = 0; + } + } +} + +pub struct MutexRingBuffer { + inner: Mutex>>, +} + +impl MutexRingBuffer { + pub const fn new(def: T) -> Self { + Self { + inner: Mutex::new(RefCell::new(RingBuffer::new(def))), + } + } + + pub fn push(&self, val: T) { + self.inner.lock(|rc| { + let mut rb = rc.borrow_mut(); + rb.push(val); + }); + } + + pub fn get_all(&self) -> Vec { + self.inner.lock(|rc| { + let rb = rc.borrow(); + rb.get_all() + }) + } + + pub fn get_latest(&self) -> Option { + self.inner.lock(|rc| { + let rb = rc.borrow(); + rb.get_latest() + }) + } +} + +pub static DATAPOINT_BUFFER: MutexRingBuffer = MutexRingBuffer::new(DataPoint::default()); + +pub fn database_spawn(spawner: embassy_executor::Spawner) { + let subscriber = serial::DOMAIN_MESSAGE_CHANNEL.subscriber().unwrap(); + spawner.spawn(data_subscriber(subscriber)).ok(); +} + +#[embassy_executor::task] +async fn data_subscriber (mut subscriber: serial::DomainMessageSubscriber<'static>) { + loop { + let data = subscriber.next_message_pure().await; + if let serial::DomainMessage::WallasData(i, t1, t2, target, current) = data { + DATAPOINT_BUFFER.push(DataPoint{instant: i, t1: t1, t2: t2, target: target, current: current}); + } + } +} + diff --git a/src/httpd.rs b/src/httpd.rs index fac1c78..d77e45c 100644 --- a/src/httpd.rs +++ b/src/httpd.rs @@ -1,9 +1,16 @@ -use picoserve::{make_static, routing::get, AppBuilder, AppRouter, Router}; -use picoserve::routing::PathRouter; -use embassy_time::Duration; +use picoserve::routing::{get, get_service}; +use picoserve::response::{IntoResponse, File, Json}; +use embassy_time::{Duration, Instant}; use embassy_net::Stack; +use maud::{DOCTYPE, html, Markup}; +use alloc::vec::Vec; +use crate::sntp_client::get_now; +use crate::sntp_client::get_instant; +use serde::{Serialize, Serializer}; +use serde::ser::SerializeSeq; +use include_file_compress::include_file_compress_deflate; -use static_cell::StaticCell; +use crate::database::DATAPOINT_BUFFER; static PICO_CONFIG : picoserve::Config = picoserve::Config::new( picoserve::Timeouts { @@ -12,32 +19,44 @@ static PICO_CONFIG : picoserve::Config = picoserve::Config::new( write: Some(Duration::from_secs(1)), }).keep_connection_alive(); -/** -struct AppProps; - -impl AppBuilder for AppProps { - type PathRouter = impl picoserve::routing::PathRouter; - - fn build_app(self) -> picoserve::Router { - picoserve::Router::new().route("/", get(|| async move { "Hello World" })) - } -} -*/ pub fn httpd_spawn(spawner: embassy_executor::Spawner, size: usize, stack: Stack<'static>) -> Result<(), super::Error> { - for i in 0..size { + for i in 0..size { spawner.must_spawn(web_task(i, stack)); } Ok(()) } +static DEFLATE_CACHEABLE_CONTENT_HEADERS: &'static [(&'static str, &'static str); 2] = &[("Content-Encoding", "deflate"), ("Cache-Control", "public, s-maxage=28800, max-age=28800")]; +static CACHEABLE_IMAGE_HEADERS: &'static [(&'static str, &'static str); 1] = &[DEFLATE_CACHEABLE_CONTENT_HEADERS[1]]; +static PNG_CONTENT_TYPE: &'static str = "image/png"; +static CSS_CONTENT_TYPE: &'static str = "text/css"; +static JS_CONTENT_TYPE: &'static str = "text/javascript"; + #[embassy_executor::task(pool_size = super::MAX_CONCURRENT_SOCKETS)] async fn web_task( id: usize, stack: Stack<'static>, ) -> ! { + let api_router = picoserve::Router::new() + .route("/latest", get(|| async { latest() })) + .route("/allreadings", get(|| async { allreadings() })); + let image_router = picoserve::Router::new() + .route("/risc-v-logo.png", get_service(File::with_content_type_and_headers(&PNG_CONTENT_TYPE, + include_bytes!("static/risc-v-logo.png"), CACHEABLE_IMAGE_HEADERS))) + .route("/espressif-logo.png", get_service(File::with_content_type_and_headers(&PNG_CONTENT_TYPE, + include_bytes!("static/espressif-logo.png"), CACHEABLE_IMAGE_HEADERS))) + .route("/wallas-logo.png", get_service(File::with_content_type_and_headers(&PNG_CONTENT_TYPE, + include_bytes!("static/wallas-logo.png"), CACHEABLE_IMAGE_HEADERS))); let app = - picoserve::Router::new().route("/", get(|| async { "Hello World" })).route("/test", get(|| async { "Test" })); + picoserve::Router::new() + .nest("/api/v1", api_router) + .route("/", get(|| async { index() })) + .route("/styles.css", get_service(File::with_content_type_and_headers(&CSS_CONTENT_TYPE, + include_file_compress_deflate!("src/static/styles.css", 5), DEFLATE_CACHEABLE_CONTENT_HEADERS))) + .route("/app.js", get_service(File::with_content_type_and_headers(&JS_CONTENT_TYPE, + include_file_compress_deflate!("src/static/app.js", 5), DEFLATE_CACHEABLE_CONTENT_HEADERS))) + .nest("/images", image_router); let port = 80; let mut tcp_rx_buffer = [0; 1024]; @@ -56,3 +75,123 @@ async fn web_task( ) .await } + +fn page(heading: &str, content: Markup) -> Markup { + html! { + (DOCTYPE) + html { + head { + link rel="stylesheet" type="text/css" href="/styles.css"; + script src="app.js" {}; + title { (heading) } + } + body { + h1 { (heading) } + (content) + div #footer { + a href="https://riscv.org/" { + img src="/images/risc-v-logo.png"; + }; + a href="https://wallas.fi/" { + img src="/images/wallas-logo.png"; + }; + a href="https://www.espressif.com/" { + img src="/images/espressif-logo.png"; + }; + br; + "RISC-V is a registered trademark of RISC-V International · Wallas is a registered trademark of Wallas-Marin Oy · Espressif is a registered trademark of Espressif Systems" + } + } + + } + } +} + +#[derive(Serialize, Clone)] +struct TemperatureReading { + #[serde(serialize_with = "instant_to_string")] + time: Instant, + target: i8, + temperature: i8, +} + +fn instant_to_string(val: &Instant, s: S) -> Result +where + S: Serializer +{ + s.serialize_str(get_instant(val).as_str()) +} + +fn option_instant_to_string(oval: &Option, s: S) -> Result +where + S: Serializer +{ + match oval { + Some(val) => s.serialize_str(get_instant(val).as_str()), + None => s.serialize_str("none"), + } +} + + +#[derive(Serialize)] +#[serde(transparent)] +struct AllTemperatureReadings { + #[serde(serialize_with = "vec_temperature_reading")] + v: Vec, +} + +/** + * Implement serialization as alloc::vec::Vec does not implement 'Serialize' + */ +fn vec_temperature_reading(v: &Vec, s: S) -> Result +where + S: Serializer +{ + let mut seq = s.serialize_seq(Some(v.len())).unwrap(); + for e in v { + seq.serialize_element(e)? + } + seq.end() +} + +fn allreadings() -> impl IntoResponse { + Json(AllTemperatureReadings { + v: DATAPOINT_BUFFER.get_all().iter() + .map(|d| TemperatureReading { time: d.instant, target: d.target, temperature: d.current }) + .collect::>() }) +} + +#[derive(Serialize)] +struct LatestResponse { + #[serde(skip_serializing_if = "Option::is_none", serialize_with = "option_instant_to_string")] + time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + target: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, +} + +impl LatestResponse { + pub fn none () -> Self { + LatestResponse { time: None, target: None, temperature: None } + } + + pub fn some (time: Instant, target: i8, temperature: i8) -> Self { + LatestResponse { time: Some(time), target: Some(target), temperature: Some(temperature) } + } +} + +fn latest() -> impl IntoResponse { + match DATAPOINT_BUFFER.get_latest() { + None => Json(LatestResponse::none()), + Some(datapoint) => Json(LatestResponse::some(datapoint.instant, datapoint.target, datapoint.current)) + } +} + +fn index() -> impl IntoResponse { + page("Wallas 22GB Wifi Extension", html! { + p .intro { "Beta version of ESP32C3 based Wifi extension to the Wallas 361062 Control Panel for the DT/GB Heaters" } + p #latest { "Waiting for latest reading ..." } + // p { "Time is now " (get_now()) } + }) +} diff --git a/src/main.rs b/src/main.rs index 2eb30e3..50ae80f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use esp_hal::timer::systimer::SystemTimer; use esp_hal_embassy::init as initialize_embassy; use esp_hal::timer::systimer::Target; use embassy_executor::Spawner; -use embassy_time::{Duration, Timer}; +// use embassy_time::{Duration, Timer}; use esp_hal::timer::timg::TimerGroup; /** @@ -31,7 +31,7 @@ use self::random::RngWrapper; */ mod wifi; use self::wifi::connect as connect_to_wifi; -use self::wifi::Error as WifiError; +// use self::wifi::Error as WifiError; /** * httpd @@ -45,6 +45,20 @@ use httpd::httpd_spawn; mod serial; use serial::serial_spawn; +/** + * sntp client + */ +mod sntp_client; +use sntp_client::sntp_client_spawn; +pub use sntp_client::get_now; + +/** + * database + */ +mod database; +use database::database_spawn; + + /// SSID for WiFi network const WIFI_SSID: &str = env!("WIFI_SSID"); @@ -54,7 +68,12 @@ const WIFI_PASSWORD: &str = env!("WIFI_PASSWORD"); /// Size of heap for dynamically-allocated memory const HEAP_MEMORY_SIZE: usize = 72 * 1024; -const MAX_CONCURRENT_SOCKETS: usize = 5; +const HTTPD_SOCKETS: usize = 8; +const DHCP_SOCKETS: usize = 1; +const SNTP_SOCKETS: usize = 1; +const DNS_SOCKETS: usize = 1; +const MAX_CONCURRENT_SOCKETS: usize = HTTPD_SOCKETS + DHCP_SOCKETS + DNS_SOCKETS + SNTP_SOCKETS; + #[main] async fn main(spawner: Spawner) { @@ -92,10 +111,14 @@ async fn main_fallible( let stack = connect_to_wifi(spawner, TimerGroup::new(peripherals.TIMG0), rng, peripherals.WIFI, peripherals.RADIO_CLK, (ssid, password)).await.unwrap(); - httpd_spawn(spawner, MAX_CONCURRENT_SOCKETS-1, stack); + let _ = httpd_spawn(spawner, HTTPD_SOCKETS, stack); + + let _ = sntp_client_spawn(spawner, stack); serial_spawn(spawner, peripherals.UART0.into(), peripherals.GPIO20.into(), peripherals.GPIO21.into()); + let _ = database_spawn(spawner); + info!("firmware done booting"); // we got here - all is fine diff --git a/src/serial.rs b/src/serial.rs index 7665a8e..343da49 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -1,36 +1,221 @@ use esp_hal::{ // clock::ClockControl, - peripherals::{Peripherals}, - prelude::*, - uart::{AtCmdConfig, AnyUart, Uart, UartRx, UartTx, Config}, + // peripherals::{Peripherals}, + uart::{AnyUart, Uart, UartRx, UartTx, Config}, gpio::AnyPin, Async, }; -use log::{info, error}; +use log::{info, error, debug}; -const BUFFER_SIZE: usize = 64; +// Channel stuff +// use embassy_sync::channel::Channel; +use embassy_sync::pubsub::{PubSubChannel, Subscriber}; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_time::{Instant, Timer}; +use heapless::Vec; + +const BUFFER_SIZE: usize = 256; +type BaseMessage = heapless::String; + +#[derive(Debug, Clone)] +pub enum DomainMessage { + AtOk, + WallasData(Instant, i8, i8, i8, i8), +} + +#[derive(Debug, Clone)] +pub enum DomainCommand { + Start, + Stop, + Ventilate, + Temperature(i8), +} + +type DomainMessageChannel = PubSubChannel; +pub type DomainMessageSubscriber<'a> = Subscriber<'a, CriticalSectionRawMutex, DomainMessage, 3, 1, 1>; + +pub static DOMAIN_MESSAGE_CHANNEL: DomainMessageChannel = DomainMessageChannel::new(); + +type DomainCommandChannel = PubSubChannel; + +static DOMAIN_COMMAND_CHANNEL: DomainCommandChannel = DomainCommandChannel::new(); pub fn serial_spawn(spawner: embassy_executor::Spawner, peri_uart: AnyUart, rx_pin: AnyPin, tx_pin: AnyPin) { // Initialize and configure UART0 let config = Config::default().rx_fifo_full_threshold(BUFFER_SIZE as u16); let my_uart = Uart::new_with_config(peri_uart, config, rx_pin, tx_pin).unwrap().into_async(); // Split UART0 to create seperate Tx and Rx handles - let (rx, _tx) = my_uart.split(); + let (rx, tx) = my_uart.split(); spawner.spawn(reader(rx)).ok(); - //spawner.spawn(writer(tx)).ok(); + spawner.spawn(writer(tx)).ok(); + spawner.spawn(fakedata()).ok(); +} + +#[embassy_executor::task] +async fn fakedata() { + let domain_publisher = DOMAIN_MESSAGE_CHANNEL.publisher().unwrap(); + let mut target: i8 = 22; + let mut current: i8 = 10; + let mut direction: i8 = 1; + loop { + Timer::after_secs(15).await; + domain_publisher.publish_immediate(DomainMessage::WallasData(Instant::now(), 0, 0, target, current)); + current = current + direction; + if current >= 22 || current <= 5 { + direction = direction * -1; + if target == 22 { + target = 5; + } else { + target = 22; + } + } + } +} + + +fn at_ok_parser (i: &str) -> Option<()> { + match nom::sequence::tuple(( + nom::bytes::complete::tag("AT+OK"), + nom::character::complete::crlf::<&str, nom::error::Error<&str>> + ))(i) { + Ok((_residual, (_, _))) => { + Some(()) + } + Err(_e) => { + None + } + } +} + +fn at_wallas_parser (i: &str) -> Option<(i8, i8, i8, i8)> { + match nom::sequence::tuple(( + nom::bytes::complete::tag("AT+WALLAS="), + nom::character::complete::i8, + nom::character::complete::char(','), + nom::character::complete::i8, + nom::character::complete::char(','), + nom::character::complete::i8, + nom::character::complete::char(','), + nom::character::complete::i8, + nom::character::complete::crlf::<&str, nom::error::Error<&str>> + ))(i) { + Ok((_residual, (_, t1, _, t2, _, t3, _, t4, _))) => { + Some((t1, t2, t3, t4)) + } + Err(_e) => { + None + } + } +} + +fn serial_receive(msg: &str) { + let domain_publisher = DOMAIN_MESSAGE_CHANNEL.publisher().unwrap(); + loop { + if let Some(()) = at_ok_parser(&msg) { + domain_publisher.publish_immediate(DomainMessage::AtOk); + } else if let Some((t1, t2, t3, t4)) = at_wallas_parser(&msg) { + domain_publisher.publish_immediate(DomainMessage::WallasData(Instant::now(), t1, t2, t3, t4)); + } else { + error!("msg received but unmatched '{}'", &msg) + } + } +} + +#[embassy_executor::task] +async fn writer(mut tx: UartTx<'static, Async>) { + let mut wbuf: [u8; 32] = [0u8; 32]; + let mut domain_subscriber = DOMAIN_COMMAND_CHANNEL.subscriber().unwrap(); + loop { + let cmd = domain_subscriber.next_message_pure().await; + let _ = embedded_io_async::Write::write(&mut tx, + match &cmd { + DomainCommand::Start => b"START\r\n", + DomainCommand::Stop => b"STOP\r\n", + DomainCommand::Ventilate => b"VENT\r\n", + DomainCommand::Temperature(temp) => { + let msg = alloc::format!("TEMP={}\r\n", temp); + wbuf.copy_from_slice(msg.as_bytes()); + &wbuf[0..msg.len()] + } + } + ).await; + } } #[embassy_executor::task] async fn reader(mut rx: UartRx<'static, Async>) { let mut rbuf: [u8; BUFFER_SIZE] = [0u8; BUFFER_SIZE]; loop { - let r = embedded_io_async::Read::read(&mut rx, &mut rbuf[0..]).await; - match r { - Ok(len) => { + let mut offset: usize = 0; + let mut eaten: usize = 0; + let mut msg : Option = None; + loop { + if let Some(base_msg) = msg { + //BASE_CHANNEL.send(base_msg).await; + serial_receive(&base_msg); + msg = None; + } + if eaten != 0 { + for n in 0..offset { + rbuf[n] = rbuf[n+eaten]; + } + eaten = 0; + } + let r = embedded_io_async::Read::read(&mut rx, &mut rbuf[offset..]).await; + match r { + Ok(len) => { + let new_offset = len + offset; + // send_line will send two numbers: new offset (after eaten bytes have been + // cleared) - and 'eaten' how many bytes have been consumed + let send_line = |start: usize, end: usize| -> (usize, usize, Option) { + // send rbuf[start, end] somewhere + let msg : BaseMessage = BaseMessage::from_utf8(Vec::from_slice(&rbuf[start..end]).unwrap()).unwrap(); + if end == new_offset { + (0, 0, Some(msg)) + } else { + let residual = new_offset - end; + /* + for n in 0..residual + rbuf[n] = rbuf[end+n]; + } */ + (residual, end, Some(msg)) + } + }; + for i in offset..new_offset { + if rbuf[i] == 0x0d { + let next = i+1; + if next < new_offset && rbuf[next] == 0x0a { + (offset, eaten, msg) = send_line(0, offset+next); + continue; + } else { + (offset, eaten, msg) = send_line(0, offset+i); + continue; + } + } else if rbuf[i] == 0x0a { + let next = i+1; + if next < new_offset && rbuf[next] == 0x0a { + (offset, eaten, msg) = send_line(0, offset+next); + continue; + } else { + (offset, eaten, msg) = send_line(0, offset+1); + continue; + } + } + } + // if buffer is full... then consider this a line, even withour cr/lf + if new_offset == BUFFER_SIZE { + debug!("serial receive BUFFER_SIZE characters, but no cr/lf so far"); + send_line(0, new_offset); + offset = 0; + continue; + } + offset = offset + len; info!("Read: {len}, data: {:?}", &rbuf[..len]); } Err(e) => error!("RX Error: {:?}", e), } + } } +} diff --git a/src/sntp_client.rs b/src/sntp_client.rs index bc5a427..fe51570 100644 --- a/src/sntp_client.rs +++ b/src/sntp_client.rs @@ -20,18 +20,22 @@ struct Timestamp { static UTC_DATETIME: Watch = Watch::new(); pub fn get_now() -> String { + get_instant(&Instant::now()) +} + +pub fn get_instant(instant: &Instant) -> String { let offset = UTC_DATETIME.try_get(); match offset { None => { - alloc::format!("LAUNCH+{}s", Instant::now().as_secs()) + alloc::format!("LAUNCH+{}s", instant.as_secs()) }, Some(duration) => { - let now = Instant::now() + duration; - let micros: i64 = now.as_micros() as i64; + let time = *instant + duration; + let micros: i64 = time.as_micros() as i64; let dt = DateTime::::from_timestamp_micros(micros); match dt { Some(val) => alloc::format!("{}", val.format("%Y-%m-%d %H:%M:%S")), - None => alloc::format!("LAUNCH+{}s", Instant::now().as_secs()) + None => alloc::format!("LAUNCH+{}s", instant.as_secs()) } } } diff --git a/src/static/app.js b/src/static/app.js new file mode 100644 index 0000000..0a7e3dd --- /dev/null +++ b/src/static/app.js @@ -0,0 +1,23 @@ +/* app.js */ + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +document.addEventListener("DOMContentLoaded", () => DOMLoaded(), false); + +async function DOMLoaded() { + await sleep(1000); + while (true) { + fetch("/api/v1/latest").then((response) => response.json()).then((json) => { + if (json.time) { + time = json.time; + temperature = json.temperature; + target = json.target; + document.getElementById("latest").innerHTML = `Temperature was ${temperature}°C at ${time} UTC, target temperature was ${target}°C`; + } + }) + await sleep(10000); + } +}; + diff --git a/src/static/espressif-logo.png b/src/static/espressif-logo.png index 697cc2c4206b8ba7431c9fe717e7a9b3b39eeb90..dcb399feeaf2b05b8d2cb412c8ffdae91e83df3c 100644 GIT binary patch literal 3923 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 WHfhT5zX}}1LFOjb#(9T4&ixMv2QuRT diff --git a/src/static/risc-v-logo.png b/src/static/risc-v-logo.png index 0d8ff6455a5df69d4428d9bc167b895d8d31f6ed..6cd2e9dfdc310bf6c437b21e26e2b04c6a691b8e 100644 GIT binary patch delta 4610 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 literal 12169 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 IY=7(j0O{!LyZ`_I diff --git a/src/static/styles.css b/src/static/styles.css index 9d943b8..07053ac 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -1,3 +1,7 @@ +html, body { + margin: 0; + padding: 0; +} body { font-family: serif; text-align: center; @@ -16,7 +20,21 @@ p.intro { padding: 0.2em; margin: 0.3em 10vw 0.3em 10vw; } +p#latest { + border: 1px solid #B15C1B; + padding: 0.2em; + margin: 0.3em 10vw 0.3em 10vw; +} div#footer { position: absolute; bottom: 0px; + margin: 0; + text-align: center; + font-size: 70%; + width: 100%; + padding-bottom: 0.3em; +} +div#footer > a > img { + padding-left: 3vw; + padding-right: 3vw; } diff --git a/src/static/wallas-logo.png b/src/static/wallas-logo.png index 8773608961e35df5399cb39f8f262192daaad997..619726c16536f836ffd19a998133df163f6bfe13 100644 GIT binary patch delta 7092 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 Jm9RnJ{{v4uU~B*Y