3 Commits

Author SHA1 Message Date
  Johannes Schilling 422ddde6c7 basic working version 6 months ago
  Johannes Schilling 9d246e21b4 finish renaming from e-ink-hello example 7 months ago
  Johannes Schilling 860296eca0 README: add espflash, --locked to install lines 7 months ago
6 changed files with 1156 additions and 148 deletions
Unified View
  1. +2
    -2
      .cargo/config.toml
  2. +827
    -14
      Cargo.lock
  3. +43
    -7
      Cargo.toml
  4. +2
    -1
      README.md
  5. +26
    -0
      letsencrypt-root.pem
  6. +256
    -124
      src/bin/main.rs

+ 2
- 2
.cargo/config.toml View File

@ -1,5 +1,5 @@
[target.xtensa-esp32-none-elf]
runner = "espflash flash --monitor --chip esp32s3 --log-format defmt"
[target.xtensa-esp32s3-none-elf]
runner = "espflash flash --monitor --chip esp32s3 --log-format defmt -- "
[env] [env]
DEFMT_LOG="info" DEFMT_LOG="info"

+ 827
- 14
Cargo.lock
File diff suppressed because it is too large
View File


+ 43
- 7
Cargo.toml View File

@ -1,17 +1,17 @@
[package] [package]
edition = "2024" edition = "2024"
name = "e-ink-hello"
name = "e-ink-spotprices"
rust-version = "1.88" rust-version = "1.88"
version = "0.1.0" version = "0.1.0"
[[bin]] [[bin]]
name = "e-ink-hello"
name = "e-ink-spotprices"
path = "./src/bin/main.rs" path = "./src/bin/main.rs"
[dependencies] [dependencies]
esp-hal = { version = "1.0.0", default-features = false, features = ["defmt", "esp32s3", "unstable"] } esp-hal = { version = "1.0.0", default-features = false, features = ["defmt", "esp32s3", "unstable"] }
esp-rtos = { version = "0.2.0", features = ["defmt", "embassy", "esp32s3", "esp-radio"] }
esp-rtos = { version = "0.2.0", features = ["defmt", "embassy", "esp32s3", "esp-radio", "esp-alloc"] }
defmt = "1.0.1" defmt = "1.0.1"
esp-bootloader-esp-idf = { version = "0.4.0", features = ["defmt", "esp32s3"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["defmt", "esp32s3"] }
@ -21,18 +21,28 @@ embassy-time = { version = "0.5.0", features = ["defmt"] }
esp-println = { version = "0.16.1", features = ["defmt-espflash", "esp32s3"] } esp-println = { version = "0.16.1", features = ["defmt-espflash", "esp32s3"] }
critical-section = "1.2.0" critical-section = "1.2.0"
static_cell = "2.1.1"
static_cell = { version = "2.1.1", features = [ "nightly" ] }
epd-waveshare = { version = "0.6", features = [ "graphics" ] } epd-waveshare = { version = "0.6", features = [ "graphics" ] }
embedded-hal-bus = { version = "0.3" } embedded-hal-bus = { version = "0.3" }
embedded-graphics = "0.8.1" embedded-graphics = "0.8.1"
embedded-nal-async = "0.9.0"
esp-alloc = "0.9" esp-alloc = "0.9"
esp-radio = { version = "0.17.0", features = ["esp32s3", "wifi"] }
embassy-net = { version = "0.8.0", features = [
esp-radio = { version = "=0.17.0", features = [
"defmt",
"esp-alloc",
"esp32s3",
"smoltcp",
"unstable",
"wifi",
] }
embassy-net = { version = "0.9.0", features = [
"defmt",
"dhcpv4", "dhcpv4",
"medium-ethernet", "medium-ethernet",
"udp",
"tcp",
"dns", "dns",
] } ] }
sntpc = { version = "0.6.0", default-features = false, features = [ sntpc = { version = "0.6.0", default-features = false, features = [
@ -40,6 +50,32 @@ sntpc = { version = "0.6.0", default-features = false, features = [
] } ] }
jiff = { version = "0.2.10", default-features = false, features = ["static"] } jiff = { version = "0.2.10", default-features = false, features = ["static"] }
smoltcp = { version = "0.12.0", default-features = false, features = [
"defmt",
"medium-ethernet",
"multicast",
"proto-dhcpv4",
"proto-dns",
"proto-ipv4",
"socket-icmp",
"socket-raw",
"socket-tcp",
"dns-max-server-count-4",
]}
reqwless = { version = "0.14.0", default-features = false, features = [
"embedded-tls",
"defmt",
] }
embedded-tls = { version = "0.17.0", default-features = false, features = [
"defmt",
"alloc",
]}
der = { version = "0.8.0", default-features = false, features = ["heapless", "alloc", "bytes", "pem"]}
picojson = "0.2"
[profile.dev] [profile.dev]
# Rust debug is too slow. # Rust debug is too slow.

+ 2
- 1
README.md View File

@ -5,7 +5,8 @@
## Toolchain ## Toolchain
```sh ```sh
cargo install espup
cargo install espup --locked
cargo install espflash --locked
espup install --name book-1.0.0 --toolchain-version 1.90.0 espup install --name book-1.0.0 --toolchain-version 1.90.0
``` ```

+ 26
- 0
letsencrypt-root.pem View File

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
u1igv3OefnWjSQ==
-----END CERTIFICATE-----

+ 256
- 124
src/bin/main.rs View File

@ -6,13 +6,18 @@
holding buffers for the duration of a data transfer." holding buffers for the duration of a data transfer."
)] )]
use defmt::info;
#[macro_use]
extern crate alloc;
use defmt::{error, info};
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_net::{DhcpConfig, Stack};
use embassy_time::{Delay, Duration, Timer}; use embassy_time::{Delay, Duration, Timer};
use embedded_graphics::primitives::{Line, Polyline, PrimitiveStyle}; use embedded_graphics::primitives::{Line, Polyline, PrimitiveStyle};
use embedded_hal_bus::spi::ExclusiveDevice; use embedded_hal_bus::spi::ExclusiveDevice;
use esp_hal::clock::CpuClock; use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::rng::Rng;
use esp_hal::timer::timg::TimerGroup; use esp_hal::timer::timg::TimerGroup;
use esp_println as _; use esp_println as _;
@ -33,8 +38,44 @@ use embedded_graphics::mono_font::ascii::FONT_10X20;
use embedded_graphics::prelude::*; use embedded_graphics::prelude::*;
use embedded_graphics::text::{Baseline, Text}; use embedded_graphics::text::{Baseline, Text};
// wifi
use esp_radio::wifi::{ClientConfig, Config, ModeConfig, ScanConfig, WifiController, WifiDevice, WifiEvent};
use embassy_net::{
Runner,
StackResources,
dns::DnsSocket,
tcp::client::{TcpClient, TcpClientState},
};
// time handling
use jiff::{tz::{self, TimeZone}, Timestamp};
use picojson::{SliceParser, PullParser, Event};
// tls
use reqwless::client::{HttpClient, TlsConfig, TlsVerify};
const WLAN_SSID: &str = env!("WLAN_SSID");
const WLAN_PASS: &str = env!("WLAN_PASS");
// (a little clumsy to have to specify the byte length here, but currently no way around that..)
const LETSENCRYPT_ROOT_PEM: &'static [u8; 1566] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/letsencrypt-root.pem"));
static TZ_EUROPE_BERLIN: TimeZone = tz::include!("/usr/share/zoneinfo/Europe/Berlin");
// from: https://github.com/esp-rs/esp-hal/blob/8281e43ba17f2140dea5e0d7e0cac4a7fa7318f2/examples/wifi/embassy_dhcp/src/main.rs
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}}
}
#[panic_handler] #[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
fn panic(pi: &core::panic::PanicInfo) -> ! {
error!("panic: {}", pi);
loop {} loop {}
} }
@ -54,12 +95,126 @@ async fn main(spawner: Spawner) -> ! {
let timg0 = TimerGroup::new(peripherals.TIMG0); let timg0 = TimerGroup::new(peripherals.TIMG0);
esp_rtos::start(timg0.timer0); esp_rtos::start(timg0.timer0);
info!("Embassy initialized!"); info!("Embassy initialized!");
// TODO: Spawn some tasks
let _ = spawner;
// wlan setup
let controller = &*mk_static!(esp_radio::Controller<'static>, esp_radio::init().expect("init radio controller"));
let (wifi_controller, interfaces) =
esp_radio::wifi::new(controller, peripherals.WIFI, Config::default()).expect("wifi init");
let wifi_interface = interfaces.sta;
let dhcp_config = embassy_net::Config::dhcpv4(DhcpConfig::default());
let rng = Rng::new();
let net_seed = (rng.random() as u64) << 32 | rng.random() as u64;
let (stack, runner) = embassy_net::new(wifi_interface, dhcp_config, mk_static!(StackResources::<3>, StackResources::<3>::new()), net_seed);
spawner.spawn(connection(wifi_controller)).expect("spawn wifi connection task");
spawner.spawn(net_task(runner)).expect("spawn net task");
wait_for_network_up(stack).await;
let dns = DnsSocket::new(stack);
let tcp_state = TcpClientState::<1, 4096, 4096>::new();
let tcp = TcpClient::new(stack, &tcp_state);
info!("tcp client");
let tls_seed = (rng.random() as u64) << 32 | rng.random() as u64;
let mut tls_tx_buf = [0u8; 4096];
let mut tls_rx_buf = [0u8; 4096];
// TODO: tls verify
let tls_config = TlsConfig::new(tls_seed, &mut tls_rx_buf, &mut tls_tx_buf, TlsVerify::None);
let mut http_client = HttpClient::new_with_tls(&tcp, &dns, tls_config);
info!("http client");
let mut buf = [0u8; 16384];
let http_req = http_client
.request(reqwless::request::Method::GET, "https://e.q0b.de/price?bzn=DE-LU")
.await;
// reverse proxy e.q0b.de/.* -> https://api.energy-charts.info/$1 with tls1_3 because api.energy-charts.info only does tlsv1.2...
let mut http_req = match http_req {
Ok(r) => r,
Err(e) => {
error!("{}", e);
panic!("failed to make http req");
}
};
let rsp = http_req.send(&mut buf).await.expect("http send");
info!("rsp made");
let res = rsp.body().read_to_end().await.expect("read rsp body");
info!("energy price: {}", str::from_utf8(res).expect("utf8"));
// parsing the response
enum ParseNext {
Nothing,
Timestamps,
Prices,
}
let mut scratch = [0u8; 128];
let mut parser = SliceParser::with_buffer_from_slice(res, &mut scratch);
// 24 hours * 4 items/hour (15min resolution) = 96 items total per day
const MAX_ITEMS: usize = 96;
let mut timestamps = [0i64; MAX_ITEMS];
let mut timestamps_idx = 0;
let mut prices = [0f32; MAX_ITEMS];
let mut prices_idx = 0;
let mut parse_next = ParseNext::Nothing;
let mut max_price = f32::MIN;
let mut min_price = f32::MAX;
while let Some(ref parse_result) = parser.next() {
match parse_result {
Err(e) => {
// TODO: defmt error printing impl for picojson
error!("json parse error");
continue;
}
Ok(Event::Key(k)) => {
if k.as_str() == "unix_seconds" {
parse_next = ParseNext::Timestamps;
} else if k.as_str() == "price" {
parse_next = ParseNext::Prices;
}
}
Ok(Event::Number(n)) => {
match parse_next {
ParseNext::Timestamps => {
if timestamps_idx < MAX_ITEMS {
timestamps[timestamps_idx] = n.as_int().unwrap();
timestamps_idx += 1;
}
}
ParseNext::Prices => {
if prices_idx < MAX_ITEMS {
prices[prices_idx] = n.as_f64().unwrap() as f32 / 10.0;
max_price = max_price.max(prices[prices_idx]);
min_price = min_price.min(prices[prices_idx]);
prices_idx += 1;
}
}
_ => error!("got a number, but not in a number parser state"),
}
}
Ok(_) => {},
}
}
for i in 0..MAX_ITEMS {
info!("{}: {}", timestamps[i], prices[i]);
}
// spi display drawing
let spi_bus = Spi::new( let spi_bus = Spi::new(
peripherals.SPI2, peripherals.SPI2,
spi::master::Config::default() spi::master::Config::default()
@ -93,120 +248,30 @@ async fn main(spawner: Spawner) -> ! {
epd.update_and_display_frame(&mut spi_dev, display.buffer(), &mut Delay) epd.update_and_display_frame(&mut spi_dev, display.buffer(), &mut Delay)
.unwrap(); .unwrap();
let prices = [
124.51,
120.29,
117.14,
115.63,
122.15,
119.4,
119.33,
117.12,
120.92,
118.14,
115.3,
115.01,
116.5,
119.49,
117.0,
118.34,
117.67,
118.73,
120.93,
123.66,
119.8,
129.0,
143.99,
153.06,
152.2,
168.99,
179.1,
190.23,
203.74,
191.36,
170.92,
139.56,
194.19,
151.4,
136.77,
107.87,
136.41,
122.21,
109.38,
88.01,
101.21,
90.13,
86.53,
71.17,
85.31,
77.93,
68.83,
59.17,
70.74,
64.99,
61.01,
56.54,
61.21,
59.19,
57.9,
60.1,
58.27,
67.62,
82.3,
88.11,
78.83,
91.99,
95.32,
103.15,
90.64,
100.02,
100.98,
120.69,
105.61,
130.77,
146.59,
173.23,
145.81,
159.0,
163.46,
167.46,
192.98,
177.61,
158.25,
144.14,
174.9,
150.14,
127.52,
119.0,
124.13,
122.5,
119.78,
109.57,
123.1,
117.9,
117.59,
108.05,
113.65,
106.89,
105.25,
98.26,
];
let mut points = [Point::new(0, 0); 96 /* TODO: same size as prices? or assume+validate/truncate first 96? */];
let mut foo = [0u8; 2];
// draw values for min/max
draw_text(&mut display, &format!("{}", max_price), 10, 20);
draw_text(&mut display, &format!("{}", min_price), 10, 260);
// current date, probably from the first timestamp (i.e. we assume we get things starting from 00:00)
let ts = Timestamp::from_second(timestamps[0]).unwrap().to_zoned(TZ_EUROPE_BERLIN.clone());
draw_text(&mut display, &format!("{}", ts.date()), 180, 10);
// some padding for the min/max
max_price *= 1.1;
min_price *= 1.1;
let mut points = [Point::new(0, 0); MAX_ITEMS];
for (i, pr) in prices.iter().enumerate() { for (i, pr) in prices.iter().enumerate() {
points[i] = Point::new(i as i32 * 4, 300 - *pr as i32);
if i % 18 == 0 {
Line::new(Point::new(i as i32 *4, 300), Point::new(i as i32 * 4, 290))
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
.draw(&mut display)
.expect("draw x-axis tick");
// no proper write/format library..
foo[0] = b'0' + (i / 10) as u8;
foo[1] = b'0' + (i % 10) as u8;
draw_text(&mut display, str::from_utf8(&foo[..]).unwrap(), i as i32 * 4, 280);
// fancyer calculations
// scale from price range price_min..price_max to pixel coordinates 270..0
points[i] = Point::new(i as i32 * 4, ((max_price - *pr) / (max_price - min_price) * 270.0) as i32);
let ts = Timestamp::from_second(timestamps[i]).expect("timestamp");
let ts_tz = ts.to_zoned(TZ_EUROPE_BERLIN.clone());
let hour = ts_tz.hour();
if hour != 0 && hour % 3 == 0 && ts_tz.minute() == 0 {
draw_text(&mut display, &format!("{}", hour), i as i32 * 4, 280);
} }
} }
let line_style = PrimitiveStyle::with_stroke(Color::Black, 3); let line_style = PrimitiveStyle::with_stroke(Color::Black, 3);
@ -246,10 +311,77 @@ fn draw_text(display: &mut Display4in2, text: &str, x: i32, y: i32) {
// * rtc initialization, and/or NTP query, to print times. // * rtc initialization, and/or NTP query, to print times.
// - maybe we can just convert the time steps from the returned data set, and e.g. do x axis ticks on multiples of 3hours from that // - maybe we can just convert the time steps from the returned data set, and e.g. do x axis ticks on multiples of 3hours from that
// * wifi // * wifi
// * tls
// * json parser
// * tls: reqless, esp-mbedtls
// * json parser: https://lib.rs/crates/picojson maybe?
// => fetch data from https://api.energy-charts.info/price?bzn=DE-LU // => fetch data from https://api.energy-charts.info/price?bzn=DE-LU
//
//
// ## drawing prettier
// * chrono: timestamp -> datetime<utc> -> .hours() % 3 == 0, then x-axis tick
async fn wait_for_network_up(stack: Stack<'_>) {
loop {
if stack.is_link_up() {
info!("network link up");
break;
}
Timer::after(Duration::from_millis(500)).await;
}
info!("Waiting to get IP address...");
loop {
if let Some(config) = stack.config_v4() {
info!("Got IP: {}", config.address);
break;
}
Timer::after(Duration::from_millis(500)).await;
}
}
// from: https://github.com/esp-rs/esp-hal/blob/8281e43ba17f2140dea5e0d7e0cac4a7fa7318f2/examples/wifi/embassy_dhcp/src/main.rs
#[embassy_executor::task]
async fn connection(mut controller: WifiController<'static>) {
info!("start connection task");
loop {
if matches!(controller.is_connected(), Ok(true)) {
// wait until we're no longer connected
controller
.wait_for_event(WifiEvent::StaDisconnected)
.await;
Timer::after(Duration::from_millis(5000)).await
}
if !matches!(controller.is_started(), Ok(true)) {
let wlan_cfg = ModeConfig::Client(
ClientConfig::default()
.with_ssid(WLAN_SSID.into())
.with_password(WLAN_PASS.into())
);
controller.set_config(&wlan_cfg).unwrap();
info!("Starting wifi");
controller.start_async().await.unwrap();
info!("Wifi started!");
info!("Scan");
let scan_config = ScanConfig::default().with_max(10);
let result = controller
.scan_with_config_async(scan_config)
.await
.unwrap();
for ap in result {
info!("{:?}", ap);
}
}
info!("About to connect...");
match controller.connect_async().await {
Ok(_) => info!("Wifi connected!"),
Err(e) => {
info!("Failed to connect to wifi: {:?}", e);
Timer::after(Duration::from_millis(5000)).await
}
}
}
}
#[embassy_executor::task]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await
}

Loading…
Cancel
Save