diff --git a/Cargo.lock b/Cargo.lock index 9855b01..2dbc95e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -207,6 +216,25 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.3.0" @@ -228,6 +256,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -341,6 +379,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.28.1" @@ -356,8 +404,10 @@ dependencies = [ "chrono", "csv", "libc", + "mime_guess", "nb 1.1.0", "rppal", + "rust-embed", "rustyline", "serde", "tokio", @@ -551,6 +601,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -712,6 +772,40 @@ dependencies = [ "void", ] +[[package]] +name = "rust-embed" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -765,6 +859,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.198" @@ -827,6 +930,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -999,6 +1113,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1023,12 +1152,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1089,6 +1234,15 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index cf425c9..4c64001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ tokio = { "version" = "1.37" } chrono = "0.4" tower-http = { "version" = "0.5", features = ["cors"] } csv = "1.3.0" +rust-embed = { "version" = "8.3.0", features = ["debug-embed"] } +mime_guess = "=2.0.4" \ No newline at end of file diff --git a/html/growpi/index.html b/html/growpi/index.html index 99f583a..86fe5f0 100644 --- a/html/growpi/index.html +++ b/html/growpi/index.html @@ -4,7 +4,7 @@ - Vite App + GrowPi
diff --git a/html/growpi/public/favicon.ico b/html/growpi/public/favicon.ico index df36fcf..f5f3a5c 100644 Binary files a/html/growpi/public/favicon.ico and b/html/growpi/public/favicon.ico differ diff --git a/html/growpi/src/App.vue b/html/growpi/src/App.vue index b784a9e..8c16c4c 100644 --- a/html/growpi/src/App.vue +++ b/html/growpi/src/App.vue @@ -25,12 +25,12 @@ export default { this.soil_moisture = info.soil_moisture; }, async sendSwitch(name, state) { - await fetch("http://192.168.0.107:2205/switch/" + name + "/" + to_state(state)); + await fetch("/api/switch/" + name + "/" + to_state(state)); await this.updateInfo(); }, async pumpWater() { console.log("Pumping: " + this.pumpAmount); - await fetch("http://192.168.0.107:2205/pump/" + this.pumpAmount); + await fetch("/api/pump/" + this.pumpAmount); await this.updateInfo(); } }, @@ -41,11 +41,11 @@ export default { } async function switchDevice(state) { - await fetch("http://192.168.0.107:2205/info") + await fetch("/api/info") } async function receiveInfo() { - const response = await fetch("http://192.168.0.107:2205/info"); + const response = await fetch("/api/info"); const info = await response.json(); return info; } diff --git a/src/config.rs b/src/config.rs index e9fda03..137f6f6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,6 +60,11 @@ pub struct DataLoggingSettings { pub frequency_mins: u64, } +#[derive(Serialize, Deserialize, Clone)] +pub struct ServerSettings { + pub port: u16, +} + #[derive(Serialize, Deserialize)] pub struct Configuration { pub board_settings: BoardSettings, @@ -69,6 +74,7 @@ pub struct Configuration { pub water_pump_settings: WaterPumpSettings, pub controller_settings: ControllerSettings, pub data_logging_settings: DataLoggingSettings, + pub server_settings: ServerSettings, } impl Configuration { @@ -123,6 +129,7 @@ impl Default for Configuration { enabled: true, frequency_mins: 60, }, + server_settings: ServerSettings { port: 2205 }, } } } diff --git a/src/control/soil.rs b/src/control/soil.rs index 4682c3f..fb274e7 100644 --- a/src/control/soil.rs +++ b/src/control/soil.rs @@ -1,5 +1,7 @@ use std::time::Duration; +use chrono::{DateTime, Utc}; + use crate::{ actuators, error::GenericResult, @@ -26,6 +28,20 @@ fn soil_moisture_control(program_state: ProgramStateShared) -> GenericResult<()> let mut program_state = lock_state(&program_state)?; let config = &program_state.config.controller_settings; let watering_amount = config.watering_amount_grams; + let last_watering_time = program_state + .history + .watering_records + .iter() + .max_by_key(|x| x.time) + .and_then(|record| DateTime::from_timestamp(record.time, 0)); + if let Some(last_watering_time) = last_watering_time { + let hours_passed = (Utc::now() - last_watering_time).num_hours(); + if hours_passed as u64 <= config.watering_frequency_hours { + return Err("Watered too soon ago".into()); + } + } else { + return Err("Could not load last watering time".into()); + } actuators::pump_water( watering_amount.try_into().unwrap_or(100), &mut program_state, diff --git a/src/server.rs b/src/server.rs index 1620c43..c9ba691 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,7 @@ use axum::{ extract::{Path, State}, - http::{Method, StatusCode}, - response::IntoResponse, + http::{header, HeaderValue, Method, StatusCode}, + response::{IntoResponse, Response}, routing::get, Json, Router, }; @@ -17,8 +17,13 @@ use crate::{ }; pub async fn run_server(program_state: ProgramStateShared) { - let app: Router = setup_router(program_state); - let listener = tokio::net::TcpListener::bind("0.0.0.0:2205").await.unwrap(); + let app: Router = setup_router(program_state.clone()); + let port = lock_state(&program_state) + .map(|state| state.config.server_settings.port) + .unwrap_or(2205); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)) + .await + .unwrap(); axum::serve(listener, app).await.unwrap(); } @@ -28,9 +33,11 @@ fn setup_router(program_state: ProgramStateShared) -> Router { .allow_origin(Any); Router::new() - .route("/info", get(info_handler)) - .route("/switch/:device/:state", get(switch_handler)) - .route("/pump/:quantity", get(pump_handler)) + .route("/api/info", get(info_handler)) + .route("/api/switch/:device/:state", get(switch_handler)) + .route("/api/pump/:quantity", get(pump_handler)) + .route("/*path", get(site_handler)) + .route("/", get(root_handler)) .with_state(program_state) .layer(cors) } @@ -97,3 +104,38 @@ async fn info_handler( pump_state, })) } + +#[derive(rust_embed::RustEmbed)] +#[folder = "html/growpi/dist/"] +struct Asset; + +async fn site_handler(Path(path): Path) -> Response { + serve_site(Some(path)) +} +async fn root_handler() -> Response { + serve_site(None) +} + +fn serve_site(path: Option) -> Response { + let path = match path { + Some(path) => path, + None => "index.html".to_string(), + }; + let asset = Asset::get(&path).or_else(|| Asset::get("index.html")); + + let content = asset + .as_ref() + .and_then(|file| std::str::from_utf8(&file.data).ok()); + let content = content.unwrap_or("Sorry, couldn't load that asset :("); + + let mut response = content.to_string().into_response(); + + let mime_type = mime_guess::from_path(path).first_or_text_plain(); + let header_value = HeaderValue::from_str(mime_type.as_ref()); + if let Ok(header_value) = header_value { + response + .headers_mut() + .append(header::CONTENT_TYPE, header_value); + } + response +}