use core::fmt::Write as _;
use embassy_time::Duration;
use heapless::{String, Vec};
use crate::at::processor::AtProcessor;
use crate::bus::SpiTransport;
use crate::error::{Error, Result};
use crate::net::device::NetworkDevice;
use crate::sync::TmMutex;
use crate::types::SocketProtocol;
#[derive(Debug, Clone)]
pub struct ParsedUrl {
pub scheme: UrlScheme,
pub host: String<128>,
pub port: u16,
pub path: String<128>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UrlScheme {
Http,
Https,
}
impl UrlScheme {
pub const fn default_port(&self) -> u16 {
match self {
UrlScheme::Http => 80,
UrlScheme::Https => 443,
}
}
pub const fn socket_protocol(&self) -> SocketProtocol {
match self {
UrlScheme::Http => SocketProtocol::Tcp,
UrlScheme::Https => SocketProtocol::Ssl,
}
}
}
pub fn parse_url(url: &str) -> Result<ParsedUrl> {
let url = url.trim();
let (scheme, rest) = if url.starts_with("https://") {
(UrlScheme::Https, &url[8..])
} else if url.starts_with("http://") {
(UrlScheme::Http, &url[7..])
} else {
(UrlScheme::Http, url)
};
let (host_port, path_str) = if let Some(slash_pos) = rest.find('/') {
(&rest[..slash_pos], &rest[slash_pos..])
} else {
(rest, "/")
};
let (host_str, port) = if let Some(colon_pos) = host_port.find(':') {
let port_str = &host_port[colon_pos + 1..];
let port = port_str
.parse::<u16>()
.map_err(|_| Error::InvalidParameter)?;
(&host_port[..colon_pos], port)
} else {
(host_port, scheme.default_port())
};
let mut host = String::new();
host.push_str(host_str).map_err(|_| Error::BufferTooSmall)?;
let mut path = String::new();
path.push_str(path_str).map_err(|_| Error::BufferTooSmall)?;
Ok(ParsedUrl {
scheme,
host,
port,
path,
})
}
pub const MAX_URL_LEN: usize = 256;
pub const MAX_HEADERS: usize = 8;
pub const MAX_HEADER_LEN: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Head,
Options,
Patch,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Delete => "DELETE",
HttpMethod::Head => "HEAD",
HttpMethod::Options => "OPTIONS",
HttpMethod::Patch => "PATCH",
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct HttpHeader {
pub name: String<64>,
pub value: String<64>,
}
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub method: HttpMethod,
pub url: String<MAX_URL_LEN>,
pub headers: Vec<HttpHeader, MAX_HEADERS>,
pub body: Option<Vec<u8, 1024>>,
}
impl HttpRequest {
pub fn new(method: HttpMethod, url: &str) -> Result<Self> {
let mut url_buf = String::new();
url_buf.push_str(url).map_err(|_| Error::BufferTooSmall)?;
Ok(Self {
method,
url: url_buf,
headers: Vec::new(),
body: None,
})
}
pub fn with_header(mut self, name: &str, value: &str) -> Result<Self> {
let mut name_buf = String::new();
name_buf.push_str(name).map_err(|_| Error::BufferTooSmall)?;
let mut value_buf = String::new();
value_buf
.push_str(value)
.map_err(|_| Error::BufferTooSmall)?;
self.headers
.push(HttpHeader {
name: name_buf,
value: value_buf,
})
.map_err(|_| Error::BufferTooSmall)?;
Ok(self)
}
pub fn with_body(mut self, body: &[u8]) -> Result<Self> {
let mut body_buf = Vec::new();
body_buf
.extend_from_slice(body)
.map_err(|_| Error::BufferTooSmall)?;
self.body = Some(body_buf);
Ok(self)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct HttpResponse {
pub status_code: u16,
pub headers: Vec<HttpHeader, MAX_HEADERS>,
pub body: Vec<u8, 2048>,
}
pub struct HttpClient {
device: &'static NetworkDevice,
#[allow(dead_code)]
processor: &'static AtProcessor,
timeout: Duration,
}
impl HttpClient {
pub const fn new(
device: &'static NetworkDevice,
processor: &'static AtProcessor,
timeout: Duration,
) -> Self {
Self {
device,
processor,
timeout,
}
}
pub async fn request<SPI, CS>(
&self,
spi: &'static TmMutex<SpiTransport<SPI, CS>>,
request: &HttpRequest,
) -> Result<HttpResponse>
where
SPI: embedded_hal_async::spi::SpiDevice,
CS: embedded_hal::digital::OutputPin,
{
let parsed = parse_url(&request.url)?;
let socket_id = self
.device
.allocate_socket(parsed.scheme.socket_protocol())
.await?;
self.device
.connect_socket(spi, socket_id, &parsed.host, parsed.port, self.timeout)
.await
.map_err(|e| {
let _ = self.device.free_socket(socket_id);
e
})?;
let mut request_str = String::<1024>::new();
write!(
&mut request_str,
"{} {} HTTP/1.1\r\n",
request.method.as_str(),
parsed.path.as_str()
)
.map_err(|_| Error::BufferTooSmall)?;
write!(&mut request_str, "Host: {}\r\n", parsed.host.as_str())
.map_err(|_| Error::BufferTooSmall)?;
for header in &request.headers {
write!(
&mut request_str,
"{}: {}\r\n",
header.name.as_str(),
header.value.as_str()
)
.map_err(|_| Error::BufferTooSmall)?;
}
if let Some(ref body) = request.body {
write!(&mut request_str, "Content-Length: {}\r\n", body.len())
.map_err(|_| Error::BufferTooSmall)?;
}
request_str
.push_str("\r\n")
.map_err(|_| Error::BufferTooSmall)?;
self.device
.send_socket(spi, socket_id, request_str.as_bytes(), self.timeout)
.await
.map_err(|e| {
let _ = self.device.close_socket(spi, socket_id, self.timeout);
e
})?;
if let Some(ref body) = request.body {
self.device
.send_socket(spi, socket_id, body, self.timeout)
.await
.map_err(|e| {
let _ = self.device.close_socket(spi, socket_id, self.timeout);
e
})?;
}
let mut response_buffer = [0u8; 2048];
let mut total_received = 0;
let response_timeout = embassy_time::Instant::now() + self.timeout;
while embassy_time::Instant::now() < response_timeout
&& total_received < response_buffer.len()
{
match self
.device
.receive_socket(
spi,
socket_id,
&mut response_buffer[total_received..],
Duration::from_millis(500),
)
.await
{
Ok(n) if n > 0 => {
total_received += n;
break;
}
Ok(_) => {
embassy_time::Timer::after(Duration::from_millis(100)).await;
}
Err(_) => {
break;
}
}
}
let _ = self.device.close_socket(spi, socket_id, self.timeout).await;
self.parse_response(&response_buffer[..total_received])
}
fn parse_response(&self, data: &[u8]) -> Result<HttpResponse> {
let mut header_end = 0;
for i in 0..data.len().saturating_sub(3) {
if &data[i..i + 4] == b"\r\n\r\n" {
header_end = i + 4;
break;
}
}
if header_end == 0 {
return Err(Error::InvalidResponse);
}
let header_str =
core::str::from_utf8(&data[..header_end]).map_err(|_| Error::InvalidResponse)?;
let mut lines = header_str.lines();
let status_line = lines.next().ok_or(Error::InvalidResponse)?;
let mut status_parts = status_line.split_whitespace();
status_parts.next(); let status_code_str = status_parts.next().ok_or(Error::InvalidResponse)?;
let status_code = status_code_str
.parse::<u16>()
.map_err(|_| Error::InvalidResponse)?;
let mut headers = Vec::new();
for line in lines {
if line.is_empty() {
break;
}
if let Some(colon_pos) = line.find(':') {
let name_str = line[..colon_pos].trim();
let value_str = line[colon_pos + 1..].trim();
let mut name = String::new();
name.push_str(name_str).map_err(|_| Error::BufferTooSmall)?;
let mut value = String::new();
value
.push_str(value_str)
.map_err(|_| Error::BufferTooSmall)?;
if headers.push(HttpHeader { name, value }).is_err() {
break;
}
}
}
let mut body = Vec::new();
for &byte in &data[header_end..] {
if body.push(byte).is_err() {
break;
}
}
Ok(HttpResponse {
status_code,
headers,
body,
})
}
pub async fn get<SPI, CS>(
&self,
spi: &'static TmMutex<SpiTransport<SPI, CS>>,
url: &str,
) -> Result<HttpResponse>
where
SPI: embedded_hal_async::spi::SpiDevice,
CS: embedded_hal::digital::OutputPin,
{
let request = HttpRequest::new(HttpMethod::Get, url)?;
self.request(spi, &request).await
}
pub async fn post<SPI, CS>(
&self,
spi: &'static TmMutex<SpiTransport<SPI, CS>>,
url: &str,
body: &[u8],
) -> Result<HttpResponse>
where
SPI: embedded_hal_async::spi::SpiDevice,
CS: embedded_hal::digital::OutputPin,
{
let request = HttpRequest::new(HttpMethod::Post, url)?
.with_body(body)?
.with_header("Content-Type", "application/json")?;
self.request(spi, &request).await
}
}