From f28f1da4dfbe0abe2a9038912b55f23408637632 Mon Sep 17 00:00:00 2001 From: Piotr Esden-Tempski Date: Thu, 28 Dec 2023 14:25:52 -0800 Subject: [PATCH 1/4] Added dfu request support. --- src/dfu.rs | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 289 insertions(+) create mode 100644 src/dfu.rs diff --git a/src/dfu.rs b/src/dfu.rs new file mode 100644 index 0000000..85862f7 --- /dev/null +++ b/src/dfu.rs @@ -0,0 +1,288 @@ +//! https://dev.blues.io/api-reference/notecard-api/dfu-requests/ + +#[allow(unused_imports)] +use defmt::{debug, error, info, trace, warn}; +use embedded_hal::blocking::i2c::{Read, SevenBitAddress, Write}; +use embedded_hal::blocking::delay::DelayMs; +use serde::{Deserialize, Serialize}; + +use super::{FutureResponse, NoteError, Notecard}; + +pub struct DFU<'a, IOM: Write + Read, const BS: usize> { + note: &'a mut Notecard, +} + +impl<'a, IOM: Write + Read, const BS: usize> DFU<'a, IOM, BS> { + pub fn from(note: &mut Notecard) -> DFU<'_, IOM, BS> { + DFU { note } + } + + /// Retrieves downloaded firmware data from the Notecard. + /// Note: this request is functional only when the Notecard has been set to + /// dfu mode with a `hub.set`, `mode:dfu` request. + pub fn get(self, delay: &mut impl DelayMs, length: usize, offset: Option) -> Result, IOM, BS>, NoteError> { + self.note.request(delay, req::Get { + req: "dfu.get", + length, + offset, + })?; + + Ok(FutureResponse::from(self.note)) + } + + /// Gets and sets the background download status of MCU host or Notecard + /// firmware updates. + pub fn status( + self, + delay: &mut impl DelayMs, + name: Option, + stop: Option, + status: Option<&str>, + version: Option<&str>, + vvalue: Option<&str>, // This is not JSON :( + on: Option, + err: Option<&str> + ) -> Result, NoteError> { + self.note.request(delay, req::Status::new( + name, + stop, + status, + version, + vvalue, + on, + err + ))?; + + Ok(FutureResponse::from(self.note)) + } +} + +mod req { + use super::*; + + #[derive(Serialize, Deserialize, defmt::Format, Default)] + pub struct Get { + pub req: &'static str, + + pub length: usize, + + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option + } + + + #[derive(Serialize, Deserialize, defmt::Format, PartialEq, Debug)] + #[serde(rename_all = "lowercase")] + pub enum StatusName { + User, + Card + } + + #[derive(Serialize, Deserialize, defmt::Format, Default)] + pub struct Status<'a> { + pub req: &'static str, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub stop: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub vvalue: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub on: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub off: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub err: Option<&'a str>, + } + + impl Status<'_> { + pub fn new<'a>( + name: Option, + stop: Option, + status: Option<&'a str>, + version: Option<&'a str>, + vvalue: Option<&'a str>, // This is not JSON :( + on: Option, + err: Option<&'a str> + ) -> Status<'a> { + // The `on`/`off` parameters are exclusive + // When on is `true` we set `on` to `Some(True)` and `off` to `None`. + // When on is `false` we set `on` to `None` and `off` to `Some(True)`. + // This way we are not sending the `on` and `off` parameters together. + Status { + req: "dfu.status", + name, + stop, + status, + version, + vvalue, + on: on.and_then(|v| v.then_some(true)), + off: on.and_then(|v| (!v).then_some(true)), + err, + } + } + } +} + +mod res { + use super::*; + + #[derive(Deserialize, defmt::Format)] + pub struct Get { + pub payload: heapless::String + } + + #[derive(Deserialize, defmt::Format, PartialEq, Debug)] + #[serde(rename_all = "lowercase")] + pub enum StatusMode { + Idle, + Error, + Downloading, + Ready + } + + #[derive(Deserialize, defmt::Format)] + pub struct StatusBody { + pub crc32: Option, + pub created: Option, + //pub info: JSON? + pub length: Option, + pub md5: Option>, + pub modified: Option, + pub name: Option>, + pub notes: Option>, + pub source: Option>, + #[serde(rename = "type")] + pub bin_type: Option>, + } + + #[derive(Deserialize, defmt::Format)] + pub struct Status { + pub mode: StatusMode, + pub status: Option>, + pub on: Option, + pub off: Option, + pub pending: Option, + pub body: Option + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get() { + let (res, _) = serde_json_core::from_str::>(r#"{"payload":"THISISALOTOFBINARYDATA="}"#).unwrap(); + assert_eq!(res.payload, r#"THISISALOTOFBINARYDATA="#); + } + + #[test] + fn test_status_name() { + let res: heapless::String<32> = serde_json_core::to_string(&req::StatusName::Card).unwrap(); + assert_eq!(res, r#""card""#); + let res: heapless::String<32> = serde_json_core::to_string(&req::StatusName::User).unwrap(); + assert_eq!(res, r#""user""#); + } + + #[test] + fn test_status_req() { + // Test basic request + let req = req::Status::new( + None, + None, + None, + None, + None, + None, + None + ); + let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap(); + assert_eq!(res, r#"{"req":"dfu.status"}"#); + + // Test a bunch of fields set + let req = req::Status::new( + Some(req::StatusName::User), + Some(true), + Some("test status"), + Some("1.0.0"), + Some("usb:1;high:1;normal:1;low:0;dead:0"), + Some(true), + Some("test error"), + ); + let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap(); + assert_eq!(res, r#"{"req":"dfu.status","name":"user","stop":true,"status":"test status","version":"1.0.0","vvalue":"usb:1;high:1;normal:1;low:0;dead:0","on":true,"err":"test error"}"#); + + // Test off set + let req = req::Status::new( + None, + None, + None, + None, + None, + Some(false), + None + ); + let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap(); + assert_eq!(res, r#"{"req":"dfu.status","off":true}"#); + } + + #[test] + fn test_status_mode() { + let (res, _) = serde_json_core::from_str::(r#""downloading""#).unwrap(); + assert_eq!(res, res::StatusMode::Downloading); + let (res, _) = serde_json_core::from_str::(r#""error""#).unwrap(); + assert_eq!(res, res::StatusMode::Error); + let (res, _) = serde_json_core::from_str::(r#""idle""#).unwrap(); + assert_eq!(res, res::StatusMode::Idle); + let (res, _) = serde_json_core::from_str::(r#""ready""#).unwrap(); + assert_eq!(res, res::StatusMode::Ready); + } + + #[test] + fn test_status() { + let (res, _) = serde_json_core::from_str::(r#"{ + "mode": "ready", + "status": "successfully downloaded", + "on": true, + "body": { + "crc32": 2525287425, + "created": 1599163431, + "info": {}, + "length": 42892, + "md5": "5a3f73a7f1b4bc8917b12b36c2532969", + "modified": 1599163431, + "name": "stm32-new-firmware$20200903200351.bin", + "notes": "Latest prod firmware", + "source": "stm32-new-firmware.bin", + "type": "firmware" + } + }"#).unwrap(); + + assert_eq!(res.mode, res::StatusMode::Ready); + assert_eq!(res.status.unwrap(), "successfully downloaded"); + assert_eq!(res.on.unwrap(), true); + let body = res.body.unwrap(); + assert_eq!(body.crc32.unwrap(), 2525287425); + assert_eq!(body.created.unwrap(), 1599163431); + assert_eq!(body.length.unwrap(), 42892); + assert_eq!(body.md5.unwrap(), "5a3f73a7f1b4bc8917b12b36c2532969"); + assert_eq!(body.modified.unwrap(), 1599163431); + assert_eq!(body.name.unwrap(), "stm32-new-firmware$20200903200351.bin"); + assert_eq!(body.notes.unwrap(), "Latest prod firmware"); + assert_eq!(body.source.unwrap(), "stm32-new-firmware.bin"); + assert_eq!(body.bin_type.unwrap(), "firmware"); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8fc37c9..b9e0318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; pub mod card; pub mod hub; pub mod note; +pub mod dfu; /// Delay between polling for new response. const RESPONSE_DELAY: u16 = 25; From 101344f676f7a286ac2f84e71d115a29c3912688 Mon Sep 17 00:00:00 2001 From: Piotr Esden-Tempski Date: Fri, 29 Dec 2023 23:03:19 -0800 Subject: [PATCH 2/4] Made dfu::req and dfu::res public. --- src/dfu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dfu.rs b/src/dfu.rs index 85862f7..2f6e1e7 100644 --- a/src/dfu.rs +++ b/src/dfu.rs @@ -57,7 +57,7 @@ impl<'a, IOM: Write + Read, const BS: usize> D } } -mod req { +pub mod req { use super::*; #[derive(Serialize, Deserialize, defmt::Format, Default)] @@ -136,7 +136,7 @@ mod req { } } -mod res { +pub mod res { use super::*; #[derive(Deserialize, defmt::Format)] From 4901acb42ff13d4b539144d275ef180f69931064 Mon Sep 17 00:00:00 2001 From: Piotr Esden-Tempski Date: Fri, 29 Dec 2023 23:03:51 -0800 Subject: [PATCH 3/4] Hooked up dfu requests to be usable. --- src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b9e0318..6e180de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -528,6 +528,11 @@ impl + Read, const BUF_SIZE: usize> pub fn hub(&mut self) -> hub::Hub { hub::Hub::from(self) } + + /// [dfu Requests](https://dev.blues.io/api-reference/notecard-api/dfu-requests/) + pub fn dfu(&mut self) -> dfu::DFU { + dfu::DFU::from(self) + } } /// A future response. From b9b10f695eacf35fe6501cd2c098eba8f8f368f4 Mon Sep 17 00:00:00 2001 From: Piotr Esden-Tempski Date: Fri, 29 Dec 2023 23:05:05 -0800 Subject: [PATCH 4/4] Added dfu::req::Version struct. This allows us to format the version string correctly so notehub can parse it. Version is expected to be a string that is actually JSON that notehub parses. --- src/dfu.rs | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/dfu.rs b/src/dfu.rs index 2f6e1e7..66204f2 100644 --- a/src/dfu.rs +++ b/src/dfu.rs @@ -78,6 +78,28 @@ pub mod req { Card } + #[derive(Serialize, Deserialize, defmt::Format)] + pub struct Version<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub org: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub product: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub firmware: Option<&'a str>, + pub version: &'a str, + pub ver_major: u32, + pub ver_minor: u32, + pub ver_patch: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub ver_build: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub built: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub builder: Option<&'a str>, + } + #[derive(Serialize, Deserialize, defmt::Format, Default)] pub struct Status<'a> { pub req: &'static str, @@ -213,17 +235,31 @@ mod tests { assert_eq!(res, r#"{"req":"dfu.status"}"#); // Test a bunch of fields set + let ver = req::Version { + org: Some("Organization"), + product: Some("Product"), + description: Some("Firmware Description"), + firmware: Some("Firmware Name"), + version: "Firmware Version 1.0.0", + ver_major: 1, + ver_minor: 0, + ver_patch: 0, + ver_build: Some(12345), + built: Some("Some Sunny Day In December"), + builder: Some("The Compnay"), + }; + let ver_str: heapless::String<512> = serde_json_core::to_string(&ver).unwrap(); let req = req::Status::new( Some(req::StatusName::User), Some(true), Some("test status"), - Some("1.0.0"), + Some(ver_str.as_str()), Some("usb:1;high:1;normal:1;low:0;dead:0"), Some(true), Some("test error"), ); - let res: heapless::String<256> = serde_json_core::to_string(&req).unwrap(); - assert_eq!(res, r#"{"req":"dfu.status","name":"user","stop":true,"status":"test status","version":"1.0.0","vvalue":"usb:1;high:1;normal:1;low:0;dead:0","on":true,"err":"test error"}"#); + let res: heapless::String<512> = serde_json_core::to_string(&req).unwrap(); + assert_eq!(res, r#"{"req":"dfu.status","name":"user","stop":true,"status":"test status","version":"{\"org\":\"Organization\",\"product\":\"Product\",\"description\":\"Firmware Description\",\"firmware\":\"Firmware Name\",\"version\":\"Firmware Version 1.0.0\",\"ver_major\":1,\"ver_minor\":0,\"ver_patch\":0,\"ver_build\":12345,\"built\":\"Some Sunny Day In December\",\"builder\":\"The Compnay\"}","vvalue":"usb:1;high:1;normal:1;low:0;dead:0","on":true,"err":"test error"}"#); // Test off set let req = req::Status::new(