Skip to main content

embedded_wg_display/runtime/
http_sync.rs

1//! Channel-based bridge allowing synchronous HTTP calls from Wasmtime host function [runtime::host_api::http](crate::runtime::host_api::http).
2//!
3//! Due to potenial memory corruption issues with multiple [EspHttpClient](crate::http_client::EspHttpClient) instances, all http request are processed by [http_handler_task] even for async functions.
4//!
5//!
6//! ```text
7//! Core 1 (widget)          Core 0 (embassy)
8//! ───────────────          ────────────────
9//! http_request_sync()  ──► HTTP_REQUEST_CHANNEL
10//!   polls with 10ms         http_handler_task() dequeues
11//!   RTOS yields       ◄──  HTTP_RESPONSE_CHANNEL
12//! ```
13//!
14//! Timeout on both sides: **30 seconds**.
15use crate::{http_client::EspHttpClient, runtime::widget::widget::http};
16use alloc::string::String;
17use alloc::vec::Vec;
18use defmt::{error, info};
19use embassy_net::Stack;
20use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
21use embassy_sync::channel::Channel;
22use embassy_time::{Duration, with_timeout};
23use esp_hal::time::Duration as HalDuration;
24use esp_rtos::CurrentThreadHandle;
25
26// HTTP request/response bridge for sync-to-async communication
27/// An HTTP request queued from the synchronous widget side to the async handler.
28#[derive(Clone)]
29pub struct HttpRequest {
30    pub method: reqwless::request::Method,
31    pub url: String,
32    pub body: Option<Vec<u8>>,
33}
34
35pub type HttpResponse = Result<http::Response, ()>;
36
37static HTTP_REQUEST_CHANNEL: Channel<CriticalSectionRawMutex, HttpRequest, 1> = Channel::new();
38static HTTP_RESPONSE_CHANNEL: Channel<CriticalSectionRawMutex, HttpResponse, 1> = Channel::new();
39const ASYNC_BRIDGE_TIMEOUT: Duration = Duration::from_secs(30);
40
41/// Asynchronous HTTP request function callable from async context.
42pub async fn http_request_async(request: HttpRequest) -> Result<http::Response, ()> {
43    match with_timeout(ASYNC_BRIDGE_TIMEOUT, HTTP_REQUEST_CHANNEL.send(request)).await {
44        Ok(()) => {}
45        Err(_) => {
46            error!("HTTP async bridge timed out while enqueueing request");
47            return Err(());
48        }
49    }
50
51    match with_timeout(ASYNC_BRIDGE_TIMEOUT, HTTP_RESPONSE_CHANNEL.receive()).await {
52        Ok(response) => response,
53        Err(_) => {
54            error!("HTTP async bridge timed out while waiting for response");
55            Err(())
56        }
57    }
58}
59
60/// Synchronous HTTP request function called from WIT host functions
61/// Widget execution is halted until this function returns or times out.
62pub fn http_request_sync(request: HttpRequest) -> Result<http::Response, ()> {
63    info!(
64        "http_request_sync: sending request to {}",
65        request.url.as_str()
66    );
67
68    // Send request to async handler task.
69    match HTTP_REQUEST_CHANNEL.try_send(request) {
70        Ok(_) => {
71            info!("HTTP request sent to channel, waiting for response...");
72
73            let mut iterations = 0u32;
74            let current_thread = CurrentThreadHandle::get();
75            const MAX_WAIT_ITERATIONS: u32 = 3000; // ~30s @ 10ms delay
76
77            loop {
78                match HTTP_RESPONSE_CHANNEL.try_receive() {
79                    Ok(response) => {
80                        info!("HTTP response received after {} iterations", iterations);
81                        return response;
82                    }
83                    Err(_) => {
84                        iterations += 1;
85                        if iterations >= MAX_WAIT_ITERATIONS {
86                            error!("HTTP request timed out while waiting for handler response");
87                            return Err(());
88                        }
89                        if iterations.is_multiple_of(20) {
90                            info!("Still waiting... iteration {}", iterations);
91                        }
92                        current_thread.delay(HalDuration::from_millis(10));
93                    }
94                }
95            }
96        }
97        Err(_) => {
98            error!("HTTP request channel full");
99            Err(())
100        }
101    }
102}
103
104/// Async task that handles HTTP requests from the channel
105/// Run on Core 0.
106#[embassy_executor::task]
107pub async fn http_handler_task(stack: Stack<'static>, _tls_seed: u64) {
108    defmt::info!("HTTP handler task started");
109    let http_client = EspHttpClient::new(stack, _tls_seed);
110
111    loop {
112        defmt::debug!("HTTP handler: waiting for request...");
113
114        // Wait for incoming request
115        let request = HTTP_REQUEST_CHANNEL.receive().await;
116
117        defmt::info!("Processing HTTP request to: {=str}", request.url.as_str());
118
119        // execute request
120        defmt::info!("HTTP handler: executing request");
121        let response_result = http_client
122            .request(request.method, &request.url, request.body.as_deref())
123            .await
124            .map_err(|e| {
125                defmt::error!("HTTP handler request failed: {:?}", defmt::Debug2Format(&e));
126            });
127
128        // Send response back
129        HTTP_RESPONSE_CHANNEL.send(response_result).await;
130        defmt::info!("HTTP response sent back to caller");
131    }
132}