Nareshkumar Rao 2 weeks ago
parent
commit
cccf737c1a
  1. 3
      .vscode/tasks.json
  2. 331
      Cargo.lock
  3. 4
      Cargo.toml
  4. 2
      growpi.service
  5. 6
      growpi.toml
  6. 20
      html/growpi/src/App.vue
  7. 6
      src/config.rs
  8. 18
      src/control/data_logging.rs
  9. 46
      src/control/imaging.rs
  10. 6
      src/control/mod.rs
  11. 19
      src/control/temperature.rs
  12. 41
      src/io.rs
  13. 2
      src/main.rs
  14. 19
      src/server.rs

3
.vscode/tasks.json

@ -7,7 +7,8 @@
"args": [
"build",
"--target",
"arm-unknown-linux-gnueabihf"
"arm-unknown-linux-gnueabihf",
"--release"
],
"problemMatcher": [
"$rustc"

331
Cargo.lock

@ -42,6 +42,93 @@ dependencies = [
"libc",
]
[[package]]
name = "async-channel"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928"
dependencies = [
"concurrent-queue",
"event-listener 5.3.0",
"event-listener-strategy 0.5.2",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-io"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
dependencies = [
"async-lock",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "async-lock"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
dependencies = [
"event-listener 4.0.3",
"event-listener-strategy 0.4.0",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.3.0",
"futures-lite",
"rustix",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "async-signal"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.52.0",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.80"
@ -53,6 +140,12 @@ dependencies = [
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.2.0"
@ -157,6 +250,20 @@ dependencies = [
"generic-array",
]
[[package]]
name = "blocking"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88"
dependencies = [
"async-channel",
"async-lock",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -210,6 +317,15 @@ dependencies = [
"error-code",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@ -225,6 +341,12 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -320,6 +442,54 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
[[package]]
name = "event-listener"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
dependencies = [
"event-listener 4.0.3",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener 5.3.0",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "fd-lock"
version = "4.0.2"
@ -346,6 +516,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "futures-channel"
version = "0.3.30"
@ -361,6 +537,25 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-task"
version = "0.3.30"
@ -400,6 +595,7 @@ name = "growpi"
version = "0.1.0"
dependencies = [
"ads1x1x",
"async-process",
"axum",
"chrono",
"csv",
@ -410,6 +606,7 @@ dependencies = [
"rust-embed",
"rustyline",
"serde",
"tempdir",
"tokio",
"toml",
"tower-http",
@ -427,6 +624,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "home"
version = "0.5.9"
@ -691,6 +894,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -729,6 +938,32 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "polling"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
@ -757,6 +992,52 @@ dependencies = [
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "rppal"
version = "0.17.1"
@ -941,6 +1222,24 @@ dependencies = [
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@ -989,6 +1288,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand",
"remove_dir_all",
]
[[package]]
name = "tokio"
version = "1.37.0"
@ -1234,6 +1543,22 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
@ -1243,6 +1568,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"

4
Cargo.toml

@ -17,4 +17,6 @@ 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"
mime_guess = "=2.0.4"
tempdir = "0.3.7"
async-process = "2.2.2"

2
growpi.service

@ -10,4 +10,4 @@ ExecStart=/opt/growpi/growpi
WorkingDirectory=/opt/growpi
[Install]
WantedBy=mutli-user.target
WantedBy=multi-user.target

6
growpi.toml

@ -34,10 +34,14 @@ temperature_set_point_upper = 35.0
temperature_set_point_lower = 28.0
temperature_loop_mins = 60
sunlight_hours = 24
soil_loop_hours = 12
watering_frequency_hours = 30
watering_amount_grams = 200
[data_logging_settings]
enabled = true
frequency_mins = 60
imaging_frequency_minutes = 60
imaging_resolution = "R480p"
[server_settings]
port = 2205

20
html/growpi/src/App.vue

@ -1,5 +1,6 @@
<script>
import Button from 'primevue/button';
import Image from 'primevue/image';
import InputNumber from 'primevue/inputnumber';
import ToggleButton from 'primevue/togglebutton';
export default {
@ -12,9 +13,10 @@ export default {
water_primed: false,
fan_state: false,
pumpAmount: 150,
image_src: "/image"
};
},
components: { ToggleButton, Button, InputNumber },
components: { ToggleButton, Button, InputNumber, Image },
methods: {
async updateInfo() {
let info = await receiveInfo();
@ -24,6 +26,9 @@ export default {
this.temperature = info.temperature;
this.soil_moisture = info.soil_moisture;
},
async updateImage() {
this.image_src = "/image#" + new Date().getTime();
},
async sendSwitch(name, state) {
await fetch("/api/switch/" + name + "/" + to_state(state));
await this.updateInfo();
@ -32,11 +37,17 @@ export default {
console.log("Pumping: " + this.pumpAmount);
await fetch("/api/pump/" + this.pumpAmount);
await this.updateInfo();
},
async refreshImage() {
await fetch("/api/refresh_image");
this.updateImage();
}
},
created() {
this.updateInfo();
this.updateImage();
setInterval(this.updateInfo, 1000);
setInterval(this.updateImage, 10000);
}
}
@ -107,6 +118,13 @@ function to_bool(state) {
:min="100" :max="300" />
</td>
</tr>
<tr>
<td colspan="2">
<Image :src="image_src" />
<br /><br />
<Button @click="refreshImage();" label="Refresh Image" />
</td>
</tr>
</table>
</template>
<style>

6
src/config.rs

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::error::GenericResult;
use crate::{error::GenericResult, io::ImageResolution};
#[derive(Serialize, Deserialize)]
pub struct RelaySettings {
@ -58,6 +58,8 @@ pub struct ControllerSettings {
pub struct DataLoggingSettings {
pub enabled: bool,
pub frequency_mins: u64,
pub imaging_frequency_minutes: u64,
pub imaging_resolution: ImageResolution,
}
#[derive(Serialize, Deserialize, Clone)]
@ -128,6 +130,8 @@ impl Default for Configuration {
data_logging_settings: DataLoggingSettings {
enabled: true,
frequency_mins: 60,
imaging_frequency_minutes: 60,
imaging_resolution: ImageResolution::R480p,
},
server_settings: ServerSettings { port: 2205 },
}

18
src/control/data_logging.rs

@ -4,7 +4,6 @@ use chrono::Utc;
use serde::{Deserialize, Serialize};
use crate::{
config::DataLoggingSettings,
error::GenericResult,
sensors,
state::{lock_state, ProgramStateShared},
@ -43,15 +42,14 @@ impl DataRecords {
pub async fn data_logging_loop(program_state: ProgramStateShared) {
loop {
let DataLoggingSettings {
enabled,
frequency_mins,
} = lock_state(&program_state)
.map(|state| state.config.data_logging_settings.clone())
.unwrap_or(DataLoggingSettings {
enabled: true,
frequency_mins: 60,
});
let (enabled, frequency_mins) = lock_state(&program_state)
.map(|state| {
(
state.config.data_logging_settings.enabled,
state.config.data_logging_settings.frequency_mins,
)
})
.unwrap_or((true, 60));
if enabled {
let _ = DataRecords::push(program_state.clone());
}

46
src/control/imaging.rs

@ -0,0 +1,46 @@
use std::{path::Path, time::Duration};
use crate::{
error::GenericResult,
io,
state::{lock_state, ProgramStateShared},
};
pub const IMAGE_PATH: &str = "./growpi.image.jpeg";
pub async fn save_latest_image(program_state: ProgramStateShared) -> GenericResult<()> {
let resolution = lock_state(&program_state)
.map(|state| {
state
.config
.data_logging_settings
.imaging_resolution
.clone()
})
.unwrap_or(io::ImageResolution::R360p);
io::capture_image(&resolution, get_image_path()).await?;
Ok(())
}
pub fn get_image_path() -> &'static Path {
Path::new(IMAGE_PATH)
}
pub async fn imaging_loop(program_state: ProgramStateShared) {
loop {
let imaging_frequency = lock_state(&program_state)
.map(|state| state.config.data_logging_settings.imaging_frequency_minutes)
.map(|f| match f {
0 => None,
n => Some(n),
})
.unwrap_or(None);
match imaging_frequency {
Some(f) => {
let _ = save_latest_image(program_state.clone()).await;
tokio::time::sleep(Duration::from_mins(f)).await;
}
None => tokio::time::sleep(Duration::from_hours(24)).await,
};
}
}

6
src/control/mod.rs

@ -2,13 +2,14 @@ use tokio::join;
use crate::{
control::{
data_logging::data_logging_loop, light::light_control_loop,
data_logging::data_logging_loop, imaging::imaging_loop, light::light_control_loop,
soil::soil_moisture_control_loop, temperature::temperature_control_loop,
},
state::ProgramStateShared,
};
mod data_logging;
pub mod imaging;
mod light;
mod soil;
mod temperature;
@ -18,6 +19,7 @@ pub async fn control_thread(program_state: ProgramStateShared) {
light_control_loop(program_state.clone()),
temperature_control_loop(program_state.clone()),
soil_moisture_control_loop(program_state.clone()),
data_logging_loop(program_state.clone())
data_logging_loop(program_state.clone()),
imaging_loop(program_state.clone())
);
}

19
src/control/temperature.rs

@ -21,17 +21,16 @@ fn temperature_control(program_state: ProgramStateShared) -> GenericResult<()> {
}
pub async fn temperature_control_loop(program_state: ProgramStateShared) {
let loop_duration = program_state
.lock()
.map(|program_state| {
program_state
.config
.controller_settings
.temperature_loop_mins
})
.unwrap_or(1);
loop {
let loop_duration = program_state
.lock()
.map(|program_state| {
program_state
.config
.controller_settings
.temperature_loop_mins
})
.unwrap_or(1);
let _ = temperature_control(program_state.clone());
tokio::time::sleep(Duration::from_mins(loop_duration)).await;
}

41
src/io.rs

@ -1,6 +1,8 @@
use ads1x1x::{Ads1x1x, ChannelSelection, DynamicOneShot};
use async_process::Command;
use nb::block;
use rppal::gpio::{Gpio, OutputPin};
use serde::{Deserialize, Serialize};
use crate::{config::*, error::GenericResult};
@ -93,3 +95,42 @@ impl Relay {
.ok_or("Pin not configured.")?)
}
}
#[allow(dead_code)]
#[derive(Clone, Serialize, Deserialize)]
pub enum ImageResolution {
R1080p,
R720p,
R480p,
R360p,
}
impl ImageResolution {
fn get_width_height(&self) -> (u64, u64) {
match self {
ImageResolution::R1080p => (1920, 1080),
ImageResolution::R720p => (1280, 720),
ImageResolution::R480p => (640, 480),
ImageResolution::R360p => (480, 360),
}
}
}
pub async fn capture_image(
resolution: &ImageResolution,
path: &std::path::Path,
) -> GenericResult<()> {
let path = std::path::absolute(path)?;
let (width, height) = resolution.get_width_height();
Command::new("/usr/bin/libcamera-jpeg")
.arg("-o")
.arg(path.clone())
.arg("-t")
.arg("1")
.arg("--width")
.arg(width.to_string())
.arg("--height")
.arg(height.to_string())
.status()
.await?
.exit_ok()?;
Ok(())
}

2
src/main.rs

@ -1,4 +1,6 @@
#![feature(duration_constructors)]
#![feature(exit_status_error)]
#![feature(absolute_path)]
use cli_mode::run_cli;
use config::Configuration;

19
src/server.rs

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use crate::{
actuators,
actuators, control,
error::GenericResult,
io::RelaySwitchState,
sensors,
@ -36,6 +36,8 @@ fn setup_router(program_state: ProgramStateShared) -> Router {
.route("/api/info", get(info_handler))
.route("/api/switch/:device/:state", get(switch_handler))
.route("/api/pump/:quantity", get(pump_handler))
.route("/api/refresh_image", get(image_refresh_handler))
.route("/image", get(image_handler))
.route("/*path", get(site_handler))
.route("/", get(root_handler))
.with_state(program_state)
@ -139,3 +141,18 @@ fn serve_site(path: Option<String>) -> Response {
}
response
}
async fn image_handler() -> Response {
let bytes = std::fs::read(control::imaging::get_image_path());
let response = bytes.map(|bytes| {
let mut r = bytes.into_response();
r.headers_mut()
.append(header::CONTENT_TYPE, HeaderValue::from_static("image/jpeg"));
r
});
response.unwrap_or_else(|e| format!("Error: {}", e).into_response())
}
async fn image_refresh_handler(State(program_state): State<ProgramStateShared>) -> Response {
let _ = control::imaging::save_latest_image(program_state).await;
StatusCode::OK.into_response()
}

Loading…
Cancel
Save