feat: various attempts

This commit is contained in:
Kat Inskip 2025-10-27 02:20:07 -07:00
parent 065c6dfae9
commit 81700d30b6
Signed by: kat
GPG key ID: 465E64DECEA8CF0F
6 changed files with 854 additions and 25 deletions

185
src/http.rs Normal file
View file

@ -0,0 +1,185 @@
// Copyright Claudio Mattera 2024-2025.
//
// Distributed under the MIT License or the Apache 2.0 License at your option.
// See the accompanying files LICENSE-MIT.txt and LICENSE-APACHE-2.0.txt, or
// online at
// https://opensource.org/licenses/MIT
// https://opensource.org/licenses/Apache-2.0
//! HTTP client
use alloc::format;
use alloc::string::FromUtf8Error;
use alloc::string::String;
use embassy_net::dns::DnsSocket;
use alloc::vec::Vec;
use embassy_net::dns::Error as DnsError;
use embassy_net::tcp::client::TcpClient;
use embassy_net::tcp::client::TcpClientState;
use embassy_net::tcp::ConnectError as TcpConnectError;
use embassy_net::tcp::Error as TcpError;
use embassy_net::Stack;
use log::debug;
use reqwless::client::HttpClient;
use reqwless::client::TlsConfig;
use reqwless::client::TlsVerify;
use reqwless::headers::ContentType;
use reqwless::request::Method;
use reqwless::request::RequestBuilder;
use reqwless::Error as ReqlessError;
use rand_core::RngCore as _;
use crate::RngWrapper;
use crate::NTFY_TOKEN;
/// Response size
const RESPONSE_SIZE: usize = 4096;
/// HTTP client
///
/// This trait exists to be extended with requests to specific sites, like in
/// [`WorldTimeApiClient`][crate::worldtimeapi::WorldTimeApiClient].
pub trait ClientTrait {
/// Send an HTTP request
async fn send_request(&mut self, url: &str) -> Result<String, Error>;
}
/// HTTP client
pub struct Client {
/// Wifi stack
stack: Stack<'static>,
/// Random numbers generator
rng: RngWrapper,
/// TCP client state
tcp_client_state: TcpClientState<1, 4096, 4096>,
/// Buffer for received TLS data
read_record_buffer: [u8; 16640],
/// Buffer for transmitted TLS data
write_record_buffer: [u8; 16640],
}
impl Client {
/// Create a new client
pub fn new(stack: Stack<'static>, rng: RngWrapper) -> Self {
debug!("Create TCP client state");
let tcp_client_state = TcpClientState::<1, 4096, 4096>::new();
Self {
stack,
rng,
tcp_client_state,
read_record_buffer: [0_u8; 16640],
write_record_buffer: [0_u8; 16640],
}
}
}
impl ClientTrait for Client {
async fn send_request(&mut self, url: &str) -> Result<String, Error> {
debug!("Send HTTPs request to {url}");
debug!("Create DNS socket");
let dns_socket = DnsSocket::new(self.stack);
let seed = self.rng.next_u64();
let tls_config = TlsConfig::new(
seed,
&mut self.read_record_buffer,
&mut self.write_record_buffer,
TlsVerify::None,
);
debug!("Create TCP client");
let tcp_client = TcpClient::new(self.stack, &self.tcp_client_state);
debug!("Create HTTP client");
let mut client = HttpClient::new_with_tls(&tcp_client, &dns_socket, tls_config);
debug!("Create HTTP request");
let mut buffer = [0_u8; 4096];
let mut request = client.request(Method::GET, url).await?;
debug!("Send HTTP request");
let auth = format!("Bearer {}", NTFY_TOKEN);
let auth_str = auth.as_str();
let headers = [("Authorization", auth_str)];
// -H "Authorization: Bearer ''${SSH_NOTIFY_TOKEN}" \=
let mut response = request
//.host("ntfy.kittywit.ch")
.content_type(ContentType::TextPlain)
.headers(&headers);
let mut response = response
.send(&mut buffer).await?;
debug!("Response status: {:?}", response.status);
let buffer = response.body().read_to_end().await?;
debug!("Read {} bytes", buffer.len());
let mut vecy = Vec::new();
vecy.extend_from_slice(buffer);
let output = String::from_utf8(vecy)?;
Ok(output)
}
}
/// An error within an HTTP request
#[derive(Debug)]
pub enum Error {
// Error turning it into a utf-8 string
FromUtf8Error(FromUtf8Error),
/// Response was too large
ResponseTooLarge,
/// Error within TCP streams
Tcp(TcpError),
/// Error within TCP connection
TcpConnect(#[expect(unused, reason = "Never read directly")] TcpConnectError),
/// Error within DNS system
Dns(#[expect(unused, reason = "Never read directly")] DnsError),
/// Error in HTTP client
Reqless(#[expect(unused, reason = "Never read directly")] ReqlessError),
}
impl From<FromUtf8Error> for Error {
fn from(error: FromUtf8Error) -> Self {
Self::FromUtf8Error(error)
}
}
impl From<TcpError> for Error {
fn from(error: TcpError) -> Self {
Self::Tcp(error)
}
}
impl From<TcpConnectError> for Error {
fn from(error: TcpConnectError) -> Self {
Self::TcpConnect(error)
}
}
impl From<DnsError> for Error {
fn from(error: DnsError) -> Self {
Self::Dns(error)
}
}
impl From<ReqlessError> for Error {
fn from(error: ReqlessError) -> Self {
Self::Reqless(error)
}
}

View file

@ -5,13 +5,20 @@
extern crate alloc;
use alloc::format;
use alloc::{format, string::{String, ToString}, vec::Vec};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, once_lock::OnceLock, rwlock::RwLock};
use esp_alloc as _;
use esp_backtrace as _;
use esp_hal::{peripherals::RSA, rsa::Rsa};
use crate::http::ClientTrait;
use {
embassy_net::Runner,
core::error::Error,
embassy_net::{
Runner,
},
embassy_time::{Duration, Timer},
display_interface_spi::SPIInterface, embassy_executor::Spawner, embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyle},
@ -36,7 +43,14 @@ use {
esp_println::println
};
use embassy_net::{Stack, StackResources};
mod rng;
mod http;
use {
rng::RngWrapper,
};
use embassy_net::{dns::DnsSocket, tcp::client::{TcpClient, TcpClientState}, IpAddress, Stack, StackResources};
#[cfg(target_arch = "riscv32")]
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_radio::{
@ -47,6 +61,10 @@ use esp_radio::{
const SSID: &str = env!("WIFI_SSID");
const PASSWORD: &str = env!("WIFI_PASSWORD");
const NTFY_TOKEN: &str = env!("NTFY_TOKEN");
const NTFY_SCHEME: &str = env!("NTFY_SCHEME");
const NTFY_UPSTREAM: &str = env!("NTFY_UPSTREAM");
const NTFY_SUBPATH: &str = env!("NTFY_SUBPATH");
esp_bootloader_esp_idf::esp_app_desc!();
@ -320,30 +338,44 @@ struct Controller<'tft> {
}
impl <'tft>Controller<'tft> {
async fn init(spawner: Spawner, mut display: TFT<'tft>, stack: Stack<'static>) -> Self {
let wifi = ControllerWifi::init_wifi(spawner, &mut display, stack).await;
async fn init(mut display: TFT<'tft>, stack: Stack<'static>) -> Self {
let mut wifi = ControllerWifi::init_wifi(&mut display, stack).await;
let mut controller = Self {
display,
wifi,
};
if let Some(config) = controller.wifi.stack.config_v4() {
controller.display.fullscreen_alert(&format!("Controller initialized!\nCurrent IP address: {}", config.address), true);
let dns_servers = config.dns_servers
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("\n");
controller.display.fullscreen_alert(&format!("Controller initialized!\nCurrent IP address: {}\nDNS: {}", config.address, dns_servers), true);
}
controller
}
async fn req(&mut self) {
//let url = format!("{}://{}/{}/raw", NTFY_SCHEME, NTFY_UPSTREAM, NTFY_SUBPATH);
let url = "https://ntfy.kittywit.ch/alerts/raw";
match self.wifi.client.send_request(&url).await {
Ok(dat) => self.display.fullscreen_alert(&dat, true),
Err(err) => {
let out = format!("Error in request: {:?}", err);
log::error!("{}", out);
self.display.fullscreen_alert(&out, true)
}
}
}
}
struct ControllerWifi {
stack: Stack<'static>,
client: http::Client,
}
impl ControllerWifi {
async fn init_wifi(spawner: Spawner, display: &mut TFT<'_>, stack: Stack<'static>) -> Self {
let mut rx_buffer = [0; 4096];
let mut tx_buffer = [0; 4096];
async fn init_wifi(display: &mut TFT<'_>, stack: Stack<'static>) -> Self {
println!("Waiting to get IP address...");
display.fullscreen_alert("Waiting to obtain an IP address", true);
loop {
@ -354,11 +386,14 @@ impl ControllerWifi {
}
Timer::after(Duration::from_millis(500)).await;
}
let rng = Rng::new();
let client = http::Client::new(stack, RngWrapper::from(rng));
Self {
stack
stack,
client,
}
}
}
#[embassy_executor::task]
@ -420,7 +455,7 @@ async fn main(spawner: Spawner) {
#[cfg(feature = "log")]
// The default log level can be specified here.
// You can see the esp-println documentation https://docs.rs/esp-println
esp_println::logger::init_logger(log::LevelFilter::Info);
esp_println::logger::init_logger(log::LevelFilter::Debug);
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = init(config);
@ -467,7 +502,11 @@ async fn main(spawner: Spawner) {
spawner.spawn(connection(wifi_controller)).ok();
spawner.spawn(net_task(runner)).ok();
let controller = Controller::init(spawner, display, stack).await;
let mut controller = Controller::init(display, stack).await;
Timer::after(Duration::from_millis(5000)).await;
controller.req().await;
loop {
// your business logic

56
src/rng.rs Normal file
View file

@ -0,0 +1,56 @@
// Copyright Claudio Mattera 2024-2025.
//
// Distributed under the MIT License or the Apache 2.0 License at your option.
// See the accompanying files LICENSE-MIT.txt and LICENSE-APACHE-2.0.txt, or
// online at
// https://opensource.org/licenses/MIT
// https://opensource.org/licenses/Apache-2.0
//! Random numbers generator
use rand_core::CryptoRng;
use rand_core::RngCore;
use esp_hal::rng::Rng;
/// A wrapper for ESP random number generator that implement traits form
/// `rand_core`
#[derive(Clone)]
pub struct RngWrapper(Rng);
impl From<Rng> for RngWrapper {
fn from(rng: Rng) -> Self {
Self(rng)
}
}
impl RngCore for RngWrapper {
fn next_u32(&mut self) -> u32 {
self.0.random()
}
fn next_u64(&mut self) -> u64 {
u32_pair_to_u64(self.next_u32(), self.next_u32())
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
for value in dest.iter_mut() {
let [random_value, _, _, _] = self.next_u32().to_ne_bytes();
*value = random_value;
}
}
}
impl CryptoRng for RngWrapper {}
/// Join a pair of `u32` into a `u64`
#[allow(
clippy::many_single_char_names,
clippy::min_ident_chars,
reason = "This is still readable"
)]
fn u32_pair_to_u64(first: u32, second: u32) -> u64 {
let [a, b, c, d] = first.to_ne_bytes();
let [e, f, g, h] = second.to_ne_bytes();
u64::from_ne_bytes([a, b, c, d, e, f, g, h])
}