Compare commits
5 Commits
719723565f
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
b8e543b972
|
|||
|
ce96dd0a4e
|
|||
|
7ee0ef0bad
|
|||
|
30eda090b1
|
|||
|
85a4e4e7c5
|
@@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-03-01"
|
||||
channel = "nightly-2025-01-09"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
targets = ["thumbv6m-none-eabi"]
|
||||
|
||||
888
updater/Cargo.lock
generated
888
updater/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,27 @@
|
||||
[package]
|
||||
name = "updater"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cortex-m = { version = "0.7", features = ["inline-asm"] }
|
||||
cortex-m-rt = "0.7"
|
||||
defmt = "0.3"
|
||||
defmt-rtt = "0.4"
|
||||
embassy-net = { version = "0.4", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
embassy-net = { version = "0.6", features = [
|
||||
"tcp",
|
||||
"dhcpv4",
|
||||
"medium-ethernet",
|
||||
"defmt",
|
||||
"dns",
|
||||
] }
|
||||
embassy-boot = { version = "0.2", features = ["defmt", "ed25519-salty"] }
|
||||
embassy-time = { version = "0.3", features = [
|
||||
"defmt",
|
||||
"defmt-timestamp-uptime",
|
||||
embassy-boot = { version = "0.4", features = ["defmt", "ed25519-salty"] }
|
||||
embassy-time = { version = "0.4", features = [
|
||||
"defmt",
|
||||
"defmt-timestamp-uptime",
|
||||
] }
|
||||
embassy-futures = { version = "0.1", features = ["defmt"] }
|
||||
rand_core = "0.6"
|
||||
embedded-io-async = { version = "0.6", features = ["defmt-03"] }
|
||||
embedded-storage = "0.3"
|
||||
rust-mqtt = { version = "0.2", features = [
|
||||
"defmt",
|
||||
"no_std",
|
||||
"tls",
|
||||
], default-features = false }
|
||||
nourl = { version = "0.1", features = ["defmt"] }
|
||||
heapless = { version = "0.7", features = ["defmt", "serde"] }
|
||||
heapless = { version = "0.8", features = ["defmt-03", "serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
serde-json-core = "0.5"
|
||||
embedded-tls = { version = "0.17", default-features = false, features = [
|
||||
"defmt",
|
||||
] }
|
||||
reqwless = { version = "0.11", features = ["defmt"] }
|
||||
static_cell = { version = "2", features = ["nightly"] }
|
||||
impl-tools = "0.10"
|
||||
portable-atomic = { version = "1.6", features = ["critical-section"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Make mqtt:// and mqtts:// actually work
|
||||
nourl = { git = "https://git.huizinga.dev/Dreaded_X/nourl" }
|
||||
picoserve = { version = "0.14", features = ["defmt", "embassy"] }
|
||||
embassy-sync = { version = "0.6.1", features = ["defmt"] }
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
use core::fmt::{Display, Write};
|
||||
|
||||
use defmt::{Format, Formatter};
|
||||
use embassy_boot::FirmwareUpdaterError;
|
||||
use embassy_net::{dns, tcp::ConnectError};
|
||||
use embedded_io_async::ReadExactError;
|
||||
use embedded_storage::nor_flash::NorFlashError;
|
||||
use embedded_tls::TlsError;
|
||||
use heapless::String;
|
||||
use rust_mqtt::packet::v5::reason_codes::ReasonCode;
|
||||
|
||||
impl_tools::impl_scope! {
|
||||
#[derive(Debug)]
|
||||
pub enum Error<FE: NorFlashError + defmt::Format> {
|
||||
InvalidScheme,
|
||||
Mqtt(ReasonCode),
|
||||
Dns(dns::Error),
|
||||
Connect(ConnectError),
|
||||
Tls(TlsError),
|
||||
Reqwless(reqwless::Error),
|
||||
FirmwareUpdater(FirmwareUpdaterError),
|
||||
FlashError(FE),
|
||||
UnexpectedEof,
|
||||
}
|
||||
|
||||
impl Self {
|
||||
pub fn string(&self) -> String<256> {
|
||||
let mut error = String::new();
|
||||
core::write!(error, "{}", self).expect("Formatting the error should not fail");
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReasonCode> for Self {
|
||||
fn from(error: ReasonCode) -> Self {
|
||||
Self::Mqtt(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<dns::Error> for Self {
|
||||
fn from(error: dns::Error) -> Self {
|
||||
Self::Dns(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConnectError> for Self {
|
||||
fn from(error: ConnectError) -> Self {
|
||||
Self::Connect(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TlsError> for Self {
|
||||
fn from(error: TlsError) -> Self {
|
||||
Self::Tls(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwless::Error> for Self {
|
||||
fn from(error: reqwless::Error) -> Self {
|
||||
Self::Reqwless(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FirmwareUpdaterError> for Self {
|
||||
fn from(error: FirmwareUpdaterError) -> Self {
|
||||
Self::FirmwareUpdater(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReadExactError<reqwless::Error>> for Self {
|
||||
fn from(error: ReadExactError<reqwless::Error>) -> Self {
|
||||
match error {
|
||||
ReadExactError::UnexpectedEof => Self::UnexpectedEof,
|
||||
ReadExactError::Other(error) => Self::Reqwless(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for Self {
|
||||
fn format(&self, f: Formatter) {
|
||||
match self {
|
||||
Error::InvalidScheme => defmt::write!(f, "Invalid URL scheme"),
|
||||
Error::Mqtt(error) => defmt::write!(f, "Mqtt: {}", error),
|
||||
Error::Dns(error) => defmt::write!(f, "Dns: {}", error),
|
||||
Error::Connect(error) => defmt::write!(f, "Connect: {}", error),
|
||||
Error::Tls(error) => defmt::write!(f, "Tls: {}", error),
|
||||
Error::Reqwless(error) => defmt::write!(f, "Reqwless: {}", error),
|
||||
Error::FirmwareUpdater(error) => defmt::write!(f, "FirmwareUpdater: {}", error),
|
||||
Error::FlashError(error) => defmt::write!(f, "FlashError: {:?}", error),
|
||||
Error::UnexpectedEof => defmt::write!(f, "UnexpectedEof"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Self {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
|
||||
match self {
|
||||
Error::InvalidScheme => core::write!(f, "Invalid URL scheme"),
|
||||
Error::Mqtt(error) => core::write!(f, "Mqtt: {}", error),
|
||||
Error::Dns(error) => core::write!(f, "Dns: {:?}", error),
|
||||
Error::Connect(error) => core::write!(f, "Connect: {:?}", error),
|
||||
Error::Tls(error) => core::write!(f, "Tls: {:?}", error),
|
||||
Error::Reqwless(error) => core::write!(f, "Reqwless: {:?}", error),
|
||||
Error::FirmwareUpdater(error) => core::write!(f, "FirmwareUpdater: {:?}", error),
|
||||
Error::FlashError(error) => core::write!(f, "FlashError: {:?}", error),
|
||||
Error::UnexpectedEof => core::write!(f, "UnexpectedEof"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,249 +4,142 @@
|
||||
|
||||
use defmt::*;
|
||||
use embassy_boot::{AlignedBuffer, BlockingFirmwareUpdater};
|
||||
use embassy_net::{dns::DnsQueryType, driver::Driver, tcp::TcpSocket, Stack};
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||
use embassy_time::{Duration, Timer};
|
||||
use embedded_io_async::{Read, Write};
|
||||
use embedded_io_async::Read;
|
||||
use embedded_storage::nor_flash::NorFlash;
|
||||
use embedded_tls::{Aes128GcmSha256, NoVerify, TlsConfig, TlsConnection, TlsContext};
|
||||
use heapless::Vec;
|
||||
use nourl::{Url, UrlScheme};
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
use reqwless::{
|
||||
request::{Method, Request, RequestBuilder},
|
||||
response::Response,
|
||||
use picoserve::{
|
||||
response::{self, IntoResponse, StatusCode},
|
||||
routing::{get, put_service, PathRouter},
|
||||
Router,
|
||||
};
|
||||
use rust_mqtt::{
|
||||
client::{client::MqttClient, client_config::ClientConfig},
|
||||
packet::v5::publish_packet::QualityOfService,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
mod error;
|
||||
|
||||
pub use crate::error::Error;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "status")]
|
||||
pub enum Status<'a> {
|
||||
Connected { version: &'a str },
|
||||
Disconnected,
|
||||
PreparingUpdate,
|
||||
Erasing,
|
||||
Writing { progress: u32 },
|
||||
Verifying,
|
||||
UpdateFailed { error: &'a str },
|
||||
UpdateComplete,
|
||||
}
|
||||
|
||||
impl Status<'_> {
|
||||
fn json(&self) -> Vec<u8, 512> {
|
||||
serde_json_core::to_vec(self)
|
||||
.expect("This buffers size should be large enough to contain the serialized status")
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a wrapper around `BlockingFirmwareUpdater` that downloads signed updates
|
||||
/// from a HTTPS url.
|
||||
/// It also provides the current device status over MQTT
|
||||
// TODO: Make this the owner of the blocking firmware updater
|
||||
// TODO: When fixed, use the async firmware updater
|
||||
pub struct Updater<'a, DFU, STATE>
|
||||
#[derive(Clone, Copy)]
|
||||
struct UpdaterService<DFU, STATE>
|
||||
where
|
||||
DFU: NorFlash,
|
||||
STATE: NorFlash,
|
||||
DFU: NorFlash + 'static,
|
||||
STATE: NorFlash + 'static,
|
||||
DFU::Error: Format,
|
||||
{
|
||||
updater: BlockingFirmwareUpdater<'a, DFU, STATE>,
|
||||
|
||||
topic_status: &'static str,
|
||||
version: &'static str,
|
||||
updater: &'static Mutex<CriticalSectionRawMutex, BlockingFirmwareUpdater<'static, DFU, STATE>>,
|
||||
public_key: &'static [u8; 32],
|
||||
}
|
||||
|
||||
impl<'a, DFU, STATE> Updater<'a, DFU, STATE>
|
||||
impl<DFU, STATE> UpdaterService<DFU, STATE>
|
||||
where
|
||||
DFU: NorFlash,
|
||||
STATE: NorFlash,
|
||||
DFU::Error: Format,
|
||||
{
|
||||
/// Wrap the `BlockingFirmwareUpdater`
|
||||
pub fn new(
|
||||
updater: BlockingFirmwareUpdater<'a, DFU, STATE>,
|
||||
topic_status: &'static str,
|
||||
version: &'static str,
|
||||
fn new(
|
||||
updater: &'static Mutex<
|
||||
CriticalSectionRawMutex,
|
||||
BlockingFirmwareUpdater<'static, DFU, STATE>,
|
||||
>,
|
||||
public_key: &'static [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
updater,
|
||||
topic_status,
|
||||
version,
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set MQTT connection up to notify over MQTT when the device loses connection
|
||||
pub fn add_will<const MAX_PROPERTIES: usize>(
|
||||
impl<S, DFU, STATE> picoserve::routing::RequestHandlerService<S> for UpdaterService<DFU, STATE>
|
||||
where
|
||||
DFU: NorFlash + 'static,
|
||||
STATE: NorFlash + 'static,
|
||||
DFU::Error: Format,
|
||||
{
|
||||
async fn call_request_handler_service<
|
||||
R: Read,
|
||||
W: picoserve::response::ResponseWriter<Error = R::Error>,
|
||||
>(
|
||||
&self,
|
||||
config: &mut ClientConfig<'_, MAX_PROPERTIES, impl RngCore>,
|
||||
) {
|
||||
static MSG: StaticCell<Vec<u8, 512>> = StaticCell::new();
|
||||
let msg = MSG.init(Status::Disconnected.json());
|
||||
config.add_will(self.topic_status, msg, true);
|
||||
}
|
||||
|
||||
/// Mark the device is ready and booted, will notify over MQTT that the device is connected and the
|
||||
/// currently running firmware version
|
||||
pub async fn ready<const MAX_PROPERTIES: usize>(
|
||||
&mut self,
|
||||
client: &mut MqttClient<'_, impl Write + Read, MAX_PROPERTIES, impl RngCore>,
|
||||
) -> Result<(), Error<DFU::Error>> {
|
||||
let status = Status::Connected {
|
||||
version: self.version,
|
||||
}
|
||||
.json();
|
||||
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, true)
|
||||
.await?;
|
||||
|
||||
self.updater.mark_booted()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download signed update from specified url and notify progress over MQTT
|
||||
pub async fn update<const MAX_PROPERTIES: usize>(
|
||||
&mut self,
|
||||
url: Url<'_>,
|
||||
stack: &'static Stack<impl Driver>,
|
||||
rng: &mut (impl RngCore + CryptoRng),
|
||||
client: &mut MqttClient<'_, impl Write + Read, MAX_PROPERTIES, impl RngCore>,
|
||||
) -> Result<!, Error<DFU::Error>> {
|
||||
let result = self._update(url, stack, rng, client).await;
|
||||
|
||||
if let Err(err) = &result {
|
||||
let status = Status::UpdateFailed {
|
||||
error: &err.string(),
|
||||
_state: &S,
|
||||
_path_parameters: (),
|
||||
mut request: picoserve::request::Request<'_, R>,
|
||||
response_writer: W,
|
||||
) -> Result<picoserve::ResponseSent, W::Error> {
|
||||
let mut updater = self.updater.lock().await;
|
||||
let writer = match updater.prepare_update() {
|
||||
Ok(writer) => writer,
|
||||
Err(err) => {
|
||||
return response::Response::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format_args!("{err:?}"),
|
||||
)
|
||||
.write_to(request.body_connection.finalize().await?, response_writer)
|
||||
.await;
|
||||
}
|
||||
.json();
|
||||
};
|
||||
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
}
|
||||
let mut reader = request.body_connection.body().reader();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn _update<const MAX_PROPERTIES: usize>(
|
||||
&mut self,
|
||||
url: Url<'_>,
|
||||
stack: &'static Stack<impl Driver>,
|
||||
rng: &mut (impl RngCore + CryptoRng),
|
||||
client: &mut MqttClient<'_, impl Write + Read, MAX_PROPERTIES, impl RngCore>,
|
||||
) -> Result<!, Error<DFU::Error>> {
|
||||
info!("Preparing for OTA...");
|
||||
let status = Status::PreparingUpdate.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
|
||||
debug!("Making sure url is HTTPS");
|
||||
if url.scheme() != UrlScheme::HTTPS {
|
||||
return Err(Error::InvalidScheme);
|
||||
}
|
||||
|
||||
// TODO: Clear out retained update message, currently gives implementation specific error
|
||||
|
||||
let ip = stack.dns_query(url.host(), DnsQueryType::A).await?[0];
|
||||
|
||||
let mut rx_buffer = [0; 1024];
|
||||
let mut tx_buffer = [0; 1024];
|
||||
|
||||
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
||||
|
||||
let addr = (ip, url.port_or_default());
|
||||
debug!("Addr: {}", addr);
|
||||
socket.connect(addr).await?;
|
||||
|
||||
let mut read_record_buffer = [0; 16384 * 2];
|
||||
let mut write_record_buffer = [0; 16384];
|
||||
let mut tls: TlsConnection<TcpSocket, Aes128GcmSha256> =
|
||||
TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer);
|
||||
tls.open::<_, NoVerify>(TlsContext::new(&TlsConfig::new(), rng))
|
||||
.await?;
|
||||
|
||||
debug!("Path: {}", url.path());
|
||||
Request::get(url.path())
|
||||
.host(url.host())
|
||||
.build()
|
||||
.write(&mut tls)
|
||||
.await?;
|
||||
|
||||
let mut headers = [0; 1024];
|
||||
let resp = Response::read(&mut tls, Method::GET, &mut headers).await?;
|
||||
|
||||
let mut body = resp.body().reader();
|
||||
|
||||
debug!("Erasing flash...");
|
||||
let status = Status::Erasing.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
|
||||
let writer = self.updater.prepare_update()?;
|
||||
|
||||
debug!("Writing...");
|
||||
let status = Status::Writing { progress: 0 }.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
|
||||
// The first 64 bytes of the file contain the signature
|
||||
let mut signature = [0; 64];
|
||||
body.read_exact(&mut signature).await?;
|
||||
if let Err(err) = reader.read_exact(&mut signature).await {
|
||||
return response::Response::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format_args!("{err:?}"),
|
||||
)
|
||||
.write_to(request.body_connection.finalize().await?, response_writer)
|
||||
.await;
|
||||
}
|
||||
|
||||
trace!("Signature: {:?}", signature);
|
||||
|
||||
let mut buffer = AlignedBuffer([0; 4096]);
|
||||
let mut size = 0;
|
||||
while let Ok(read) = body.read(&mut buffer.0).await {
|
||||
while let Ok(read) = reader.read(&mut buffer.0).await {
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
debug!("Writing chunk: {}", read);
|
||||
writer
|
||||
.write(size, &buffer.0[..read])
|
||||
.map_err(Error::FlashError)?;
|
||||
if let Err(err) = writer.write(size, &buffer.0[..read]) {
|
||||
return response::Response::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format_args!("{err:?}"),
|
||||
)
|
||||
.write_to(request.body_connection.finalize().await?, response_writer)
|
||||
.await;
|
||||
}
|
||||
size += read as u32;
|
||||
|
||||
let status = Status::Writing { progress: size }.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
}
|
||||
debug!("Total size: {}", size);
|
||||
|
||||
let status = Status::Verifying.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
let public_key = self.public_key;
|
||||
if let Err(err) = updater.verify_and_mark_updated(public_key, &signature, size) {
|
||||
return response::Response::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format_args!("{err:?}"),
|
||||
)
|
||||
.write_to(request.body_connection.finalize().await?, response_writer)
|
||||
.await;
|
||||
}
|
||||
|
||||
"Update complete"
|
||||
.write_to(request.body_connection.finalize().await?, response_writer)
|
||||
.await?;
|
||||
|
||||
self.updater
|
||||
.verify_and_mark_updated(self.public_key, &signature, size)?;
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
|
||||
let status = Status::UpdateComplete.json();
|
||||
client
|
||||
.send_message(self.topic_status, &status, QualityOfService::QoS1, false)
|
||||
.await?;
|
||||
|
||||
client.disconnect().await?;
|
||||
|
||||
info!("Restarting in 5 seconds...");
|
||||
Timer::after(Duration::from_secs(5)).await;
|
||||
|
||||
cortex_m::peripheral::SCB::sys_reset()
|
||||
cortex_m::peripheral::SCB::sys_reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn firmware_router<S, DFU, STATE>(
|
||||
version: &'static str,
|
||||
updater: &'static Mutex<CriticalSectionRawMutex, BlockingFirmwareUpdater<'static, DFU, STATE>>,
|
||||
public_key: &'static [u8; 32],
|
||||
) -> Router<impl PathRouter<S>, S>
|
||||
where
|
||||
DFU: NorFlash + 'static,
|
||||
STATE: NorFlash + 'static,
|
||||
DFU::Error: Format,
|
||||
{
|
||||
let updater_service = UpdaterService::new(updater, public_key);
|
||||
|
||||
Router::new()
|
||||
.route("/update", put_service(updater_service))
|
||||
.route("/version", get(move || async move { version }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user