diff --git a/src/main.rs b/src/main.rs index c81995e..9bfcb5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,12 @@ #![feature(type_alias_impl_trait)] #![feature(impl_trait_in_assoc_type)] +use alloc::rc::Rc; use embassy_executor::Spawner; use embassy_net::Stack; use embassy_sync::{ blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, - channel::Channel, + mutex::Mutex, pubsub::{ PubSubChannel, Publisher, WaitResult::{Lagged, Message}, @@ -20,7 +21,11 @@ use esp_hal::{gpio::InputConfig, peripherals}; use log::{debug, info}; use static_cell::make_static; -use crate::store::{Date, IDStore, TallyID}; +use crate::{ + init::sd_card::SDCardPersistence, + store::{Date, IDStore, TallyID}, + webserver::start_webserver, +}; extern crate alloc; @@ -28,30 +33,34 @@ mod drivers; mod feedback; mod init; mod store; -//mod webserver; +mod webserver; static FEEDBACK_STATE: Signal = Signal::new(); type TallyChannel = PubSubChannel; type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>; +type UsedStore = IDStore; #[esp_hal_embassy::main] async fn main(mut spawner: Spawner) { let (uart_device, stack, _i2c, _led, buzzer_gpio, sd_det_gpio, persistence_layer) = init::hardware::hardware_init(&mut spawner).await; - wait_for_stack_up(stack).await; - info!("Starting up..."); - let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new()); - - //start_webserver(&mut spawner, stack); - - let publisher = chan.publisher().unwrap(); - let mut rtc = drivers::rtc::RTCClock::new(_i2c).await; + let store: UsedStore = IDStore::new_from_storage(persistence_layer).await; + let shared_store = Rc::new(Mutex::new(store)); + + let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new()); + let publisher = chan.publisher().unwrap(); + let mut sub = chan.subscriber().unwrap(); + + wait_for_stack_up(stack).await; + + start_webserver(&mut spawner, stack, shared_store.clone()); + /****************************** Spawning tasks ***********************************/ debug!("spawing NFC reader task..."); spawner.must_spawn(drivers::nfc_reader::rfid_reader_task( @@ -66,13 +75,9 @@ async fn main(mut spawner: Spawner) { spawner.must_spawn(sd_detect_task(sd_det_gpio)); /******************************************************************************/ - let mut sub = chan.subscriber().unwrap(); - debug!("everything spawned"); FEEDBACK_STATE.signal(feedback::FeedbackState::Startup); - let mut store = IDStore::new_from_storage(persistence_layer).await; - loop { let wait_result = sub.next_message().await; match wait_result { @@ -81,7 +86,7 @@ async fn main(mut spawner: Spawner) { debug!("Got message: {msg:?}"); let day: Date = rtc.get_date().await; - let added = store.add_id(msg, day).await; + let added = shared_store.lock().await.add_id(msg, day).await; if added { FEEDBACK_STATE.signal(feedback::FeedbackState::Ack); diff --git a/src/webserver/api.rs b/src/webserver/api.rs new file mode 100644 index 0000000..3afd134 --- /dev/null +++ b/src/webserver/api.rs @@ -0,0 +1,66 @@ +use alloc::string::String; +use picoserve::{ + extract::{Json, State}, + response::{self, IntoResponse}, +}; +use serde::Deserialize; + +use crate::{ + store::{Name, TallyID}, + webserver::app::AppState, +}; + +#[derive(Deserialize)] +pub struct NewMapping { + id: String, + name: Name, +} + +pub fn hex_string_to_tally_id(s: &str) -> Option { + let bytes = s.as_bytes(); + if bytes.len() != 24 { + return None; + } + + let mut out = [0u8; 12]; + for i in 0..12 { + let hi = hex_val(bytes[2 * i])?; + let lo = hex_val(bytes[2 * i + 1])?; + out[i] = (hi << 4) | lo; + } + Some(out) +} + +fn hex_val(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } +} + +/* + * #[get("/api/idevent")] + * #[get("/api/csv")] + * #[get("/api/mapping")] + * #[post("/api/mapping", format = "json", data = "")] + * struct NewMapping { + * id: String, + * name: Name, + * } +*/ + +pub async fn get_mapping(State(state): State) -> impl IntoResponse { + let store = state.store.lock().await; + response::Json(store.mapping.clone()) +} + +pub async fn add_mapping( + State(state): State, + Json(data): Json, +) -> impl IntoResponse { + let mut store = state.store.lock().await; + let tally_id = hex_string_to_tally_id(&data.id).unwrap(); + store.mapping.add_mapping(tally_id, data.name); +} diff --git a/src/webserver/app.rs b/src/webserver/app.rs new file mode 100644 index 0000000..30de5e7 --- /dev/null +++ b/src/webserver/app.rs @@ -0,0 +1,31 @@ +use alloc::rc::Rc; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; +use picoserve::{AppWithStateBuilder, routing::get}; + +use crate::{ + webserver::{ + api::{add_mapping, get_mapping}, + assets::Assets, + }, UsedStore, +}; + +#[derive(Clone)] +pub struct AppState { + pub store: Rc>, +} + +pub struct AppProps; + +impl AppWithStateBuilder for AppProps { + type State = AppState; + type PathRouter = impl picoserve::routing::PathRouter; + + fn build_app(self) -> picoserve::Router { + picoserve::Router::from_service(Assets) + .route("/api/mapping", get(get_mapping).post(add_mapping)) + // .route( + // "/api/idevent", + // get(move || response::EventStream(Events(self.chan))), + // ) + } +} diff --git a/src/webserver/mod.rs b/src/webserver/mod.rs index d9b3bed..0c71b1f 100644 --- a/src/webserver/mod.rs +++ b/src/webserver/mod.rs @@ -1,54 +1,57 @@ +use alloc::rc::Rc; use embassy_executor::Spawner; use embassy_net::Stack; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; use embassy_time::Duration; -use picoserve::{AppBuilder, AppRouter, routing::get}; +use picoserve::{AppRouter, AppWithStateBuilder}; use static_cell::make_static; -mod assets; +use crate::{ + UsedStore, + init::network::NETWORK_STACK_SIZE, + webserver::app::{AppProps, AppState}, +}; -pub fn start_webserver(spawner: &mut Spawner, stack: Stack<'static>) { +mod assets; +// mod sse; +mod api; +mod app; + +pub fn start_webserver( + spawner: &mut Spawner, + stack: Stack<'static>, + store: Rc>, +) { let app = make_static!(AppProps.build_app()); + let state = make_static!(AppState { store }); + let config = make_static!(picoserve::Config::new(picoserve::Timeouts { start_read_request: Some(Duration::from_secs(5)), - persistent_start_read_request: Some(Duration::from_secs(1)), - read_request: Some(Duration::from_secs(1)), - write: Some(Duration::from_secs(1)), + persistent_start_read_request: Some(Duration::from_secs(5)), + read_request: Some(Duration::from_secs(5)), + write: Some(Duration::from_secs(5)), })); - let _ = spawner.spawn(webserver_task(0, stack, app, config)); -} - -struct AppProps; - -impl AppBuilder for AppProps { - type PathRouter = impl picoserve::routing::PathRouter; - - fn build_app(self) -> picoserve::Router { - picoserve::Router::from_service(assets::Assets).route("/api/a", get(async move || "Hello")) + for task_id in 0..NETWORK_STACK_SIZE { + spawner.must_spawn(webserver_task(task_id, stack, app, config, state)); } } -#[embassy_executor::task] +#[embassy_executor::task(pool_size = NETWORK_STACK_SIZE)] async fn webserver_task( - id: usize, + task_id: usize, stack: embassy_net::Stack<'static>, app: &'static AppRouter, config: &'static picoserve::Config, + state: &'static AppState, ) -> ! { let mut tcp_rx_buffer = [0u8; 1024]; let mut tcp_tx_buffer = [0u8; 1024]; let mut http_buffer = [0u8; 2048]; - picoserve::listen_and_serve( - id, - app, - config, - stack, - 80, - &mut tcp_rx_buffer, - &mut tcp_tx_buffer, - &mut http_buffer, - ) - .await + picoserve::Server::new(&app.shared().with_state(state), config, &mut http_buffer) + .listen_and_serve(task_id, stack, 80, &mut tcp_rx_buffer, &mut tcp_tx_buffer) + .await + .into_never() } diff --git a/src/webserver/sse.rs b/src/webserver/sse.rs new file mode 100644 index 0000000..e09ddf3 --- /dev/null +++ b/src/webserver/sse.rs @@ -0,0 +1,29 @@ +use embassy_time::{Duration, Timer}; +use log::warn; +use picoserve::response; + +pub struct Events(pub TallySubscriber); + +impl response::sse::EventSource for Events { + async fn write_events( + mut self, + mut writer: response::sse::EventWriter, + ) -> Result<(), W::Error> { + loop { + let timeout = Timer::after(Duration::from_secs(15)); + let sel = embassy_futures::select::select(self.0.next_message(), timeout); + + match sel.await { + embassy_futures::select::Either::First(msg) => match msg { + embassy_sync::pubsub::WaitResult::Message(id) => { + writer.write_event("msg", id.to_string().as_str()).await? + } + embassy_sync::pubsub::WaitResult::Lagged(_) => { + warn!("SSE subscriber got lagged"); + } + }, + embassy_futures::select::Either::Second(_) => writer.write_keepalive().await?, + } + } + } +}