use alloc::string::String;
use alloc::vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Instant, Timer};
use picoserve::ResponseSent;
use picoserve::io::Read;
use picoserve::request::Request;
use picoserve::response::{IntoResponse, Json, ResponseWriter, StatusCode};
use picoserve::routing::RequestHandlerService;
use resident_fat::BlockDevice;
use resident_fat::counted::Counters;
use rpi_loader_ota::Format;
use rpi_loader_ota::apply::{self, Report};
use rpi_loader_ota::measure::{Measure, Phase, rate_kib_s};
use crate::logln;
use crate::storage::{Shared, Volume};
pub const DRAIN: Duration = Duration::from_secs(1);
pub const MAX_BUNDLE: usize = 16 * 1024 * 1024;
pub struct Reboot {
installed: Signal<CriticalSectionRawMutex, Report>,
}
impl Reboot {
pub const fn new() -> Self {
Reboot {
installed: Signal::new(),
}
}
pub fn request(&self, installed: Report) {
self.installed.signal(installed);
}
pub async fn wait(&self) -> Report {
let installed = self.installed.wait().await;
Timer::after(DRAIN).await;
logln!("ota: rebooting into the new image");
installed
}
}
impl Default for Reboot {
fn default() -> Self {
Reboot::new()
}
}
#[allow(async_fn_in_trait)]
pub trait Hooks<D: BlockDevice> {
type Receiving;
async fn receiving(&self) -> Self::Receiving;
async fn received(&self, before: Self::Receiving);
fn install<R>(&self, volume: &mut Volume<D>, install: impl FnOnce(&mut Volume<D>) -> R) -> R {
install(volume)
}
}
impl<D: BlockDevice> Hooks<D> for () {
type Receiving = ();
async fn receiving(&self) {}
async fn received(&self, _before: ()) {}
}
pub struct OtaUpload<'a, D: BlockDevice, H = ()> {
card: &'a Shared<D>,
format: &'a Format,
reboot: &'a Reboot,
max_bundle: usize,
counters: Option<&'a Counters>,
hooks: H,
}
impl<'a, D: BlockDevice> OtaUpload<'a, D> {
pub fn new(card: &'a Shared<D>, format: &'a Format, reboot: &'a Reboot) -> Self {
OtaUpload {
card,
format,
reboot,
max_bundle: MAX_BUNDLE,
counters: None,
hooks: (),
}
}
}
impl<'a, D: BlockDevice, H> OtaUpload<'a, D, H> {
pub fn max_bundle(mut self, bytes: usize) -> Self {
self.max_bundle = bytes;
self
}
pub fn counting(mut self, counters: &'a Counters) -> Self {
self.counters = Some(counters);
self
}
pub fn hooks<H2: Hooks<D>>(self, hooks: H2) -> OtaUpload<'a, D, H2> {
OtaUpload {
card: self.card,
format: self.format,
reboot: self.reboot,
max_bundle: self.max_bundle,
counters: self.counters,
hooks,
}
}
fn measure(&self) -> Measure<'a, impl FnMut() -> u64 + use<'a, D, H>> {
let now = || Instant::now().as_millis();
match self.counters {
Some(counters) => Measure::counting(now, counters),
None => Measure::new(now),
}
}
}
impl<D, H, State, PathParameters> RequestHandlerService<State, PathParameters>
for OtaUpload<'_, D, H>
where
D: BlockDevice,
H: Hooks<D>,
{
async fn call_request_handler_service<R: Read, W: ResponseWriter<Error = R::Error>>(
&self,
_state: &State,
_path_parameters: PathParameters,
mut request: Request<'_, R>,
response_writer: W,
) -> Result<ResponseSent, W::Error> {
let content_length = request.body_connection.body().content_length();
if content_length > self.max_bundle {
return refuse(
request,
response_writer,
413,
"bundle is larger than this board accepts",
)
.await;
}
let mut bundle = vec![0u8; content_length];
let before = self.hooks.receiving().await;
let started = Instant::now();
let mut stopped = "";
let received = {
let mut reader = request.body_connection.body().reader();
let mut received = 0;
while received < content_length {
match reader.read(&mut bundle[received..]).await {
Ok(0) => {
stopped = "the client stopped sending";
break;
}
Err(_) => {
stopped = "the read failed or ran out of time";
break;
}
Ok(n) => received += n,
}
}
received
};
let elapsed_ms = Instant::now()
.saturating_duration_since(started)
.as_millis();
self.hooks.received(before).await;
if received != content_length {
logln!(
"ota: received {received} of {content_length} bytes in {elapsed_ms} ms -- {stopped}"
);
return refuse(request, response_writer, 400, "upload incomplete").await;
}
logln!(
"ota: received {received} bytes in {elapsed_ms} ms ({} KiB/s)",
rate_kib_s(received, elapsed_ms)
);
let mut measure = self.measure();
let format = self.format;
let installed = self
.card
.with(|volume| {
self.hooks.install(volume, |volume| {
apply::apply(volume, format, &bundle, &mut measure)
})
})
.await;
for entry in measure.entries() {
logln!("ota: {entry}");
}
let connection = request.body_connection.finalize().await?;
let (status, outcome) = match installed {
None => {
logln!("ota: refused -- no card mounted");
(503, Outcome::refused("this board has no card mounted"))
}
Some(Err(error)) => {
logln!("ota: failed -- {error}");
(
error.http_status(),
Outcome::refused(&alloc::format!("{error}")),
)
}
Some(Ok(report)) => {
let elapsed_ms = measure.elapsed_ms();
logln!(
"ota: {} written, {} unchanged in {elapsed_ms} ms",
report.written,
report.skipped
);
self.reboot.request(report);
(
200,
Outcome::installed(&report, elapsed_ms, measure.kernel()),
)
}
};
Json(outcome)
.into_response()
.with_status_code(StatusCode::new(status))
.write_to(connection, response_writer)
.await
}
}
async fn refuse<R: Read, W: ResponseWriter<Error = R::Error>>(
request: Request<'_, R>,
response_writer: W,
status: u16,
reason: &str,
) -> Result<ResponseSent, W::Error> {
logln!("ota: refused -- {reason}");
Json(Outcome::refused(reason))
.into_response()
.with_status_code(StatusCode::new(status))
.write_to(request.body_connection.finalize().await?, response_writer)
.await
}
#[derive(serde::Serialize)]
struct Outcome {
ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
kernel: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
written: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
skipped: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
elapsed_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
kernel_timing: Option<KernelTiming>,
#[serde(skip_serializing_if = "Option::is_none")]
rebooting_in_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
impl Outcome {
fn refused(reason: &str) -> Self {
Outcome {
ok: false,
kernel: None,
written: None,
skipped: None,
elapsed_ms: None,
kernel_timing: None,
rebooting_in_seconds: None,
error: Some(String::from(reason)),
}
}
fn installed(report: &Report, elapsed_ms: u64, kernel: Option<(Phase, Phase)>) -> Self {
Outcome {
ok: true,
kernel: report.kernel_len,
written: Some(report.written),
skipped: Some(report.skipped),
elapsed_ms: Some(elapsed_ms),
kernel_timing: kernel.map(|(write, verify)| KernelTiming::of(&write, &verify)),
rebooting_in_seconds: Some(DRAIN.as_secs()),
error: None,
}
}
}
#[derive(serde::Serialize)]
struct KernelTiming {
write_ms: u64,
verify_ms: u64,
write_kib_s: u64,
verify_kib_s: u64,
#[serde(skip_serializing_if = "Option::is_none")]
write_calls: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
write_blocks: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
verify_calls: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
verify_blocks: Option<u32>,
}
impl KernelTiming {
fn of(write: &Phase, verify: &Phase) -> Self {
let (write_counts, verify_counts) = (write.counts, verify.counts);
KernelTiming {
write_ms: write.ms,
verify_ms: verify.ms,
write_kib_s: write.rate_kib_s(),
verify_kib_s: verify.rate_kib_s(),
write_calls: write_counts.map(|c| c.write_calls),
write_blocks: write_counts.map(|c| c.write_blocks),
verify_calls: verify_counts.map(|c| c.read_calls),
verify_blocks: verify_counts.map(|c| c.read_blocks),
}
}
}