Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions esp32/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ use esp_idf_svc::{
use log::info;
use std::time::Duration;

pub fn fetch_image_data(url: &str) -> Result<Vec<u8>> {
const HEADER_X_ESP_DEEP_SLEEP_SECONDS: &str = "x-esp-deep-sleep-seconds";

pub struct Response {
pub image_data: Vec<u8>,
pub deep_sleep_seconds: Option<u64>,
}

pub fn fetch_data(url: &str) -> Result<Response> {
let connection = EspHttpConnection::new(&Configuration {
timeout: Some(Duration::from_secs(5)),
use_global_ca_store: true,
Expand All @@ -30,10 +37,17 @@ pub fn fetch_image_data(url: &str) -> Result<Vec<u8>> {
bail!("Expected response code 200, got {status}");
}

let deep_sleep_seconds = response
.header(HEADER_X_ESP_DEEP_SLEEP_SECONDS)
.and_then(|value| value.parse().ok());

let mut buf = vec![0; display_buffer_size()];
let len = io::try_read_full(response, &mut buf).map_err(|err| err.0)?;

info!("Received {len} bytes");

Ok(buf[..len].to_vec())
Ok(Response {
image_data: buf[..len].to_vec(),
deep_sleep_seconds,
})
}
17 changes: 10 additions & 7 deletions esp32/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,21 @@ fn main() -> Result<()> {
let sysloop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;

if let Err(err) = run(peripherals, sysloop, nvs) {
let deep_sleep_seconds = run(peripherals, sysloop, nvs).unwrap_or_else(|err| {
error!("{err}");
}
None
});

let sleep_time = Duration::from_secs(deep_sleep_seconds.unwrap_or(CONFIG.deep_sleep_seconds));

enter_deep_sleep(Duration::from_secs(CONFIG.deep_sleep_seconds));
enter_deep_sleep(sleep_time);
}

fn run(
peripherals: Peripherals,
sysloop: EspSystemEventLoop,
nvs: EspDefaultNvsPartition,
) -> Result<()> {
) -> Result<Option<u64>> {
let wifi = wifi::connect(
CONFIG.wifi_ssid,
CONFIG.wifi_psk,
Expand All @@ -61,7 +64,7 @@ fn run(
)
.context("Could not connect to WiFi network")?;

let image_data = http::fetch_image_data(CONFIG.data_url)?;
let resp = http::fetch_data(CONFIG.data_url)?;

info!("Disconnecting WiFi");
drop(wifi);
Expand Down Expand Up @@ -91,7 +94,7 @@ fn run(
info!("E-Ink display init completed!");

info!("Drawing image");
epd.update_and_display_frame(&mut spi, &image_data, &mut delay)?;
epd.update_and_display_frame(&mut spi, &resp.image_data, &mut delay)?;

#[allow(clippy::absurd_extreme_comparisons)]
if CONFIG.clear_after_seconds > 0 {
Expand All @@ -104,7 +107,7 @@ fn run(
}

epd.sleep(&mut spi, &mut delay)?;
Ok(())
Ok(resp.deep_sleep_seconds)
}

fn enter_deep_sleep(sleep_time: Duration) -> ! {
Expand Down
1 change: 1 addition & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ thiserror = "1.0.64"
embedded-graphics = "0.8.1"
monsoon = "0.1.1"
tower = { version = "0.5.1", features = ["limit", "util"] }
jiff = "0.1.13"
jiff = { version = "0.1.13", features = ["serde"] }
sun = "0.3.1"
config = "0.14.0"
serde = { version = "1.0.210", features = ["derive"] }
Expand Down
38 changes: 38 additions & 0 deletions server/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,41 @@ altitude = 0
# If you don't like inverted colors at night, you can disable night mode by
# setting this to `true`.
disable_night_mode = false

# Configurable time-based presets.
#
# When an image request falls within one of the configured schedules, the
# server will perform certain actions like returning additional data to the ESP
# in HTTP headers to configure its behaviour.
#
# If multiple presets match at a given time, their settings are merged in the
# order they are defined.
#
# [presets.preset-name]
# # Enable or disable the preset.
# enabled = true
#
# # Start moment of the schedule. Can be a datetime, date or time.
# from = "20:00"
#
# # End moment of the schedule. Can be a datetime, date or time.
# to = "05:00"
#
# # Send the number of seconds the ESP should sleep after it requested a new
# # image. This field is optional. If unset, the ESP will sleep for its
# # configured default value.
# esp_deep_sleep_seconds = 600
#
# # Wether to add a lot of randomness into the weather data by default. This
# # field is optional.
# wreck_havoc = true
#
# The following preset instructs the ESP to only ask for a new image every 60
# minutes to save battery during night time when people would rarely look at it
# anyways:
#
# [presets.night-time]
# enabled = true
# from = "22:00"
# to = "06:00"
# esp_deep_sleep_seconds = 3600
5 changes: 4 additions & 1 deletion server/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{config::Config, error::Result, graphics::Renderer, weather::Weather};
use crate::{config::Config, error::Result, graphics::Renderer, preset::Presets, weather::Weather};
use prometheus::{
IntCounterVec, Registry,
core::{AtomicU64, GenericCounter},
Expand All @@ -9,6 +9,7 @@ use prometheus::{
#[derive(Clone)]
pub struct AppState {
pub metrics: Metrics,
pub presets: Presets,
pub renderer: Renderer,
pub weather: Weather,
}
Expand All @@ -18,9 +19,11 @@ impl AppState {
pub fn new(config: &Config, metrics: Metrics) -> Result<AppState> {
let weather = Weather::new(config.latitude, config.longitude, config.altitude)?;
let renderer = Renderer::new(config, metrics.clone());
let presets = Presets::new(&config.presets)?;

Ok(AppState {
metrics,
presets,
renderer,
weather,
})
Expand Down
17 changes: 15 additions & 2 deletions server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::Result;
use crate::{error::Result, preset::Moment};
use config::{Environment, File};
use serde::Deserialize;
use std::collections::BTreeMap;
use tracing::debug;

/// Application configuration sourced from env and/or config file.
Expand All @@ -11,6 +12,18 @@ pub struct Config {
pub altitude: Option<i32>,
#[serde(default)]
pub disable_night_mode: bool,
#[serde(default)]
pub presets: BTreeMap<String, PresetConfig>,
}

/// Configuration of a time-based preset.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PresetConfig {
pub enabled: bool,
pub from: Moment,
pub to: Moment,
pub wreck_havoc: Option<bool>,
pub esp_deep_sleep_seconds: Option<u64>,
}

impl Config {
Expand All @@ -20,7 +33,7 @@ impl Config {
// Configuration from `config.toml`.
.add_source(File::with_name("config").required(false))
// Config from environment variables.
.add_source(Environment::default().separator("_"))
.add_source(Environment::default().separator("__"))
.build()?
.try_deserialize()?;

Expand Down
25 changes: 16 additions & 9 deletions server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod app;
mod config;
mod error;
mod graphics;
mod preset;
mod sun;
mod weather;

Expand All @@ -13,27 +14,27 @@ use crate::{
};
use actix_web::{
App, HttpResponse, HttpServer, get,
http::header::ContentType,
middleware::Logger,
web::{Data, Path, Query},
};
use actix_web_prom::PrometheusMetricsBuilder;
use jiff::Zoned;
use rand::{SeedableRng, rngs::StdRng};
use serde::Deserialize;
use tracing::{debug, error};

#[derive(Deserialize, Clone, Debug)]
struct ImageQuery {
/// Adds a lot of randomness to the weather data to make the weather seem unpredictable.
#[serde(default)]
wreck_havoc: bool,
wreck_havoc: Option<bool>,
/// A seed for the RNG to produce stable randomness.
seed: Option<u64>,
}

impl ImageQuery {
fn seed_rng(&self) -> StdRng {
let seed = self.seed.unwrap_or_else(rand::random);
tracing::debug!(?seed, "seeding RNG used for image rendering");
debug!(?seed, "seeding RNG used for image rendering");
StdRng::seed_from_u64(seed)
}
}
Expand All @@ -49,10 +50,14 @@ async fn image(
format: Path<ImageFormat>,
query: Query<ImageQuery>,
) -> actix_web::Result<HttpResponse> {
let settings = state.presets.get_settings_for(Zoned::now().datetime());

let wreck_havoc = query.wreck_havoc.or(settings.wreck_havoc).unwrap_or(false);

let mut data = state.weather.get().await?;
let mut rng = query.seed_rng();

if query.wreck_havoc {
if wreck_havoc {
weather::wreck_havoc(&mut data, &mut rng);
}

Expand All @@ -61,9 +66,11 @@ async fn image(

state.metrics.image_counter(mime_type.essence_str()).inc();

Ok(HttpResponse::Ok()
.insert_header(ContentType(mime_type))
.body(body))
let mut resp = HttpResponse::Ok();

settings.configure_response(&mut resp);

Ok(resp.content_type(mime_type).body(body))
}

async fn run() -> Result<()> {
Expand Down Expand Up @@ -98,7 +105,7 @@ async fn main() {
tracing_subscriber::fmt::init();

if let Err(err) = run().await {
tracing::error!("{err}");
error!("{err}");
std::process::exit(1);
}
}
Loading
Loading