Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
16b1509
remove unused file_system.rs from espressif driver
AlixANNERAUD May 10, 2026
4c0b887
prevent decrementing reference count below zero in virtual file system
AlixANNERAUD May 10, 2026
23e49bd
add count_ones method to flags macro for counting set flags
AlixANNERAUD May 10, 2026
5fd4024
fix: remove trailing CRLF from status line in HttpResponseBuilder
AlixANNERAUD May 10, 2026
8544300
refactor: consolidate DNS functionality into stack and remove DnsSock…
AlixANNERAUD May 10, 2026
cb27561
refactor: streamline target resolution in PingCommand by removing sep…
AlixANNERAUD May 10, 2026
6e618eb
refactor: replace DnsSocket with network manager in DNS resolution
AlixANNERAUD May 10, 2026
6c7ae23
feat: implement sleep request handling in ABI context and update thre…
AlixANNERAUD May 10, 2026
3fc9e6e
feat: add textarea text retrieval functions in WASM bindings
AlixANNERAUD May 10, 2026
77b1cbb
feat: implement sleep request handling in WASM execution flow
AlixANNERAUD May 10, 2026
9baf6c7
feat: enhance file system error logging and add directory statistics …
AlixANNERAUD May 10, 2026
d042978
feat: refactor HTTP and HTTPS client to improve DNS resolution and he…
AlixANNERAUD May 10, 2026
0b6af42
feat: add HTTPS client device initialization and mounting in main and…
AlixANNERAUD May 10, 2026
294e905
feat(weather): add weather executable with API integration
AlixANNERAUD May 10, 2026
1505235
feat(weather): load and execute weather binary in WASM environment
AlixANNERAUD May 10, 2026
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ wasm = { path = "executables/wasm" }
terminal = { path = "executables/terminal" }
settings = { path = "executables/settings" }
calculator = { path = "executables/calculator" }
weather = { path = "executables/weather" }

# - External crates
miniserde = { version = "0.1" }
Expand Down Expand Up @@ -183,6 +184,7 @@ members = [
"modules/graphics/fonts_generator",
"modules/testing",
"modules/device",
"executables/weather",
]

[patch.crates-io]
Expand Down
Empty file.
16 changes: 6 additions & 10 deletions drivers/shared/src/devices/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,13 @@ async fn create_tcp_connection(host: &str, port: u16) -> Result<TcpSocket> {
log::information!("http_client: create session host='{}' port={}", host, port);
let manager = network::get_instance();

let dns_socket = manager
.new_dns_socket(None)
let address = manager
.resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa, true, None)
.await
.map_err(map_network_error)?;
let resolved = dns_socket
.resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa)
.await
.map_err(map_network_error)?;
dns_socket.close().await.map_err(map_network_error)?;

let address = resolved.into_iter().next().ok_or(Error::NotFound)?;
.map_err(map_network_error)?
.first()
.cloned()
.ok_or(Error::NotFound)?;

let mut socket = manager
.new_tcp_socket(4096, 4096, None)
Expand Down
34 changes: 30 additions & 4 deletions drivers/shared/src/devices/http_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,36 @@ pub fn build_serialized_response_headers(raw_headers: &[u8], output: &mut [u8])
.add_status_code(status_code)
.ok_or(Error::InternalError)?;

for (name, value) in parser.get_headers() {
builder
.add_header(name, value.as_bytes())
.ok_or(Error::FileTooLarge)?;
// Manually parse headers line by line to handle edge cases like
// servers that omit reason phrase (HTTP/1.1 200) causing an empty line after status
let lines = parser.split_lines();
let mut header_started = false;

for line in lines {
let trimmed = core::str::from_utf8(line).unwrap_or("").trim();

// Skip the status line (first non-empty line)
if !header_started && !trimmed.is_empty() {
header_started = true;
continue;
}

// Stop at empty line (end of headers)
if header_started && trimmed.is_empty() {
break;
}

// Skip empty lines in the header section
if trimmed.is_empty() {
continue;
}

// Parse and add the header
if let Some((name, value)) = shared::parse_header(line) {
builder
.add_header(name, value.as_bytes())
.ok_or(Error::FileTooLarge)?;
}
}

builder.add_line(b"").ok_or(Error::FileTooLarge)?;
Expand Down
141 changes: 65 additions & 76 deletions drivers/shared/src/devices/https_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use embassy_futures::select::{Either, select};
use embedded_io;
use embedded_io_async;
use embedded_tls::{Aes128GcmSha256, TlsConfig, TlsConnection, TlsContext, UnsecureProvider};
use file_system::{BaseOperations, CharacterDevice, Context, Error, MountOperations, Result, Size};
use file_system::{
BaseOperations, CharacterDevice, Context, DirectCharacterDevice, Error, MountOperations,
Result, Size,
};
use network::{DnsQueryKind, Duration as NetworkDuration, Port, TcpSocket};
use rand_core::{CryptoRng, RngCore};
use shared::HttpRequestParser;
Expand All @@ -15,36 +18,44 @@ use super::http_common::{
build_serialized_response_headers, compute_request_length, map_network_error, split_host_port,
};

const TLS_RECORD_BUFFER_SIZE: usize = 4096;
const RESPONSE_SCAN_BUFFER_SIZE: usize = 3072;
const RESPONSE_SERIALIZED_HEADER_SIZE: usize = 2048;
const TLS_RECORD_BUFFER_SIZE: usize = 16384; // TLS 1.2 max record size (2^14 bytes)
const RESPONSE_SCAN_BUFFER_SIZE: usize = 4096; // HTTP headers buffer (typically 2-5 KB)
const RESPONSE_SERIALIZED_HEADER_SIZE: usize = 2048; // Parsed header output buffer
const TLS_READ_CHUNK_SIZE: usize = 512;
const DEFAULT_HTTPS_PORT: u16 = 443;
const IO_TIMEOUT_SECONDS: u64 = 15;

struct SystemRng;
struct RandomNumberGenerator<T: DirectCharacterDevice + 'static>(&'static T);

impl CryptoRng for SystemRng {}
impl<T: DirectCharacterDevice + 'static> CryptoRng for RandomNumberGenerator<T> {}

impl RngCore for SystemRng {
impl<T: DirectCharacterDevice + 'static> RngCore for RandomNumberGenerator<T> {
fn next_u32(&mut self) -> u32 {
let mut bytes = [0u8; 4];
getrandom::fill(&mut bytes).expect("SystemRng failed to gather u32 entropy");
self.0
.read(&mut bytes, 0)
.expect("Random device read failed");
u32::from_le_bytes(bytes)
}

fn next_u64(&mut self) -> u64 {
let mut bytes = [0u8; 8];
getrandom::fill(&mut bytes).expect("SystemRng failed to gather u64 entropy");
self.0
.read(&mut bytes, 0)
.expect("Random device read failed");
u64::from_le_bytes(bytes)
}

fn fill_bytes(&mut self, dest: &mut [u8]) {
getrandom::fill(dest).expect("SystemRng failed to gather entropy");
self.0.read(dest, 0).expect("Random device read failed");
}

fn try_fill_bytes(&mut self, dest: &mut [u8]) -> core::result::Result<(), rand_core::Error> {
getrandom::fill(dest).map_err(|_| rand_core::Error::from(core::num::NonZeroU32::MIN))
self.0.read(dest, 0).map_err(|e| {
log::error!("Random device read failed: {:?}", e);
rand_core::Error::from(e.get_discriminant())
})?;
Ok(())
}
}

Expand Down Expand Up @@ -271,73 +282,56 @@ async fn write_tls_all(
Ok(())
}

async fn create_tls_connection<'a>(
async fn create_tls_connection<'a, T: DirectCharacterDevice + 'static>(
host: &str,
port: u16,
read_record: &'a mut [u8; TLS_RECORD_BUFFER_SIZE],
write_record: &'a mut [u8; TLS_RECORD_BUFFER_SIZE],
random_device: &'static T,
) -> Result<TlsConnection<'a, TcpSocketAdapter, Aes128GcmSha256>> {
log::information!("https_client: create session host='{}' port={}", host, port);
let manager = network::get_instance();

log::information!("https_client: resolving host='{}'", host);
let dns_socket = manager
.new_dns_socket(None)
.await
.map_err(map_network_error)?;
let resolved = dns_socket
.resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa)
let address = manager
.resolve(host, DnsQueryKind::A | DnsQueryKind::Aaaa, true, None)
.await
.map_err(map_network_error)?;
dns_socket.close().await.map_err(map_network_error)?;

let address = resolved.into_iter().next().ok_or(Error::NotFound)?;
log::information!("https_client: dns resolved host='{}'", host);
.map_err(map_network_error)?
.first()
.cloned()
.ok_or(Error::NotFound)?;

log::information!("https_client: creating tcp socket");
let mut socket = manager
.new_tcp_socket(4096, 4096, None)
.await
.map_err(map_network_error)?;
socket
.set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS)))
.await;
log::information!(
"https_client: connecting tcp socket to {}:{}",
address,
port
);

socket
.connect(address, Port::from_inner(port))
.await
.map_err(map_network_error)?;
socket
.set_timeout(Some(NetworkDuration::from_seconds(IO_TIMEOUT_SECONDS)))
.await;
log::information!("https_client: tcp connected");

let mut tls = TlsConnection::new(TcpSocketAdapter { socket }, read_record, write_record);

let configuration = TlsConfig::new().with_server_name(host);
let provider = UnsecureProvider::new::<Aes128GcmSha256>(SystemRng);
let provider = UnsecureProvider::new::<Aes128GcmSha256>(RandomNumberGenerator(random_device));
let context = TlsContext::new(&configuration, provider);

log::information!("https_client: starting tls handshake");
tls.open(context).await.map_err(map_tls_error)?;
log::information!("https_client: tls handshake done");

Ok(tls)
}

async fn run_request(
async fn run_request<T: DirectCharacterDevice + 'static>(
inner: Arc<Mutex<CriticalSectionRawMutex, HttpsClientInner>>,
request: Vec<u8>,
random_device: &'static T,
) {
let result = async {
log::information!(
"https_client: run_request begin (buffer_len={})",
request.len()
);
let parser = HttpRequestParser::from_buffer(&request);
let _ = parser.get_request().ok_or(Error::InvalidParameter)?;

Expand All @@ -348,18 +342,21 @@ async fn run_request(
.ok_or(Error::InvalidParameter)?;

let (host, port) = split_host_port(host_header, DEFAULT_HTTPS_PORT);
log::information!("https_client: parsed host='{}' port={}", host, port);

let mut read_record = [0u8; TLS_RECORD_BUFFER_SIZE];
let mut write_record = [0u8; TLS_RECORD_BUFFER_SIZE];
let mut tls =
create_tls_connection(host, port, &mut read_record, &mut write_record).await?;
let mut tls = create_tls_connection(
host,
port,
&mut read_record,
&mut write_record,
random_device,
)
.await?;

let request_length = compute_request_length(&request, parser)?;
let payload = &request[..request_length];
log::information!("https_client: request length computed = {}", request_length);

log::information!("https_client: tls write_all begin");
write_tls_all(&mut tls, payload).await?;

let has_header_terminator = payload.windows(4).any(|window| window == b"\r\n\r\n");
Expand All @@ -378,37 +375,23 @@ async fn run_request(
write_tls_all(&mut tls, suffix).await?;
}

log::information!("https_client: tls write_all done");

log::information!("https_client: tls flush begin");
tls.flush().await.map_err(map_tls_error)?;
log::information!("https_client: tls flush done");

let mut raw_headers = [0u8; RESPONSE_SCAN_BUFFER_SIZE];

log::information!("https_client: waiting response headers");
let (raw_headers_end, response_body) =
read_response_headers_and_body(&mut tls, &mut raw_headers).await?;
log::information!(
"https_client: response headers received (headers_end={}, body_size={})",
raw_headers_end,
response_body.len()
);

let mut response_headers = [0u8; RESPONSE_SERIALIZED_HEADER_SIZE];
let serialized_headers_len = build_serialized_response_headers(
&raw_headers[..raw_headers_end],
&mut response_headers,
)?;

match tls.close().await {
Ok(mut adapter) => {
adapter.socket.close().await;
}
Err((mut adapter, _)) => {
adapter.socket.close().await;
}
}
// Skip explicit TLS close as it may block indefinitely.
// The TLS connection and socket will be dropped naturally when this scope ends.
// This avoids deadlock while still cleaning up resources.
drop(tls);

Ok::<(usize, [u8; RESPONSE_SERIALIZED_HEADER_SIZE], Vec<u8>), Error>((
serialized_headers_len,
Expand All @@ -423,6 +406,7 @@ async fn run_request(
match result {
Ok((serialized_headers_len, response_headers, response_body)) => {
if !matches!(guard.state, State::InFlight) {
log::warning!("https_client: state is not InFlight, returning early");
return;
}

Expand All @@ -435,11 +419,6 @@ async fn run_request(
guard.response_body_cursor = 0;

guard.state = State::HeadersReady;

log::information!(
"https_client: request complete, headers ready len={}",
serialized_headers_len
);
}
Err(error) => {
if matches!(guard.state, State::InFlight) {
Expand All @@ -450,9 +429,15 @@ async fn run_request(
}
}

pub struct HttpsClientDevice;
pub struct HttpsClientDevice<T: DirectCharacterDevice + 'static>(&'static T);

impl BaseOperations for HttpsClientDevice {
impl<T: DirectCharacterDevice + 'static> HttpsClientDevice<T> {
pub const fn new(random_device: &'static T) -> Self {
Self(random_device)
}
}

impl<T: DirectCharacterDevice + 'static> BaseOperations for HttpsClientDevice<T> {
fn open(&self, context: &mut Context) -> Result<()> {
context.set_private_data(Box::new(HttpsClientContext::new()));
Ok(())
Expand All @@ -476,7 +461,6 @@ impl BaseOperations for HttpsClientDevice {
}

fn write(&self, context: &mut Context, buffer: &[u8], _: Size) -> Result<usize> {
log::information!("https_client: write called size={}", buffer.len());
let context = context
.get_private_data_mutable_of_type::<HttpsClientContext>()
.ok_or(Error::InvalidParameter)?;
Expand All @@ -489,12 +473,14 @@ impl BaseOperations for HttpsClientDevice {
let task_manager = task::get_instance();
let parent = task::Manager::ROOT_TASK_IDENTIFIER;

let random_device = self.0;

if let Err(spawn_error) =
task::block_on(
task_manager.spawn(parent, "HTTPS request worker", None, move |_| {
let inner_clone = inner.clone();
let request_owned = request;
async move { run_request(inner_clone, request_owned).await }
async move { run_request(inner_clone, request_owned, random_device).await }
}),
)
{
Expand All @@ -507,7 +493,6 @@ impl BaseOperations for HttpsClientDevice {
return Err(Error::RessourceBusy);
}

log::information!("https_client: write submitted successfully");
Ok(buffer.len())
}

Expand All @@ -522,9 +507,9 @@ impl BaseOperations for HttpsClientDevice {
}
}

impl MountOperations for HttpsClientDevice {}
impl<T: DirectCharacterDevice + 'static> MountOperations for HttpsClientDevice<T> {}

impl CharacterDevice for HttpsClientDevice {}
impl<T: DirectCharacterDevice + 'static> CharacterDevice for HttpsClientDevice<T> {}

fn write_state_gate(context: &mut HttpsClientContext) -> Result<()> {
let mut inner = task::block_on(context.inner.lock());
Expand Down Expand Up @@ -570,7 +555,11 @@ fn read_state_transition(context: &mut HttpsClientContext, buffer: &mut [u8]) ->
}
State::BodyStreaming => {
if inner.response_body_cursor >= inner.response_body.len() {
inner.reset();
// Only reset if we actually had a body (headers_len > 0).
// If headers_len is 0, we're being called prematurely during request setup.
if inner.response_headers_len > 0 {
inner.reset();
}
return Ok(0);
}

Expand Down
Loading
Loading