From 843711c101ce3f46919dce8d7900751970ed39c3 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sat, 9 Jan 2021 12:28:06 +0100 Subject: [PATCH 1/8] Add setting to persist UI locale Fix #31 --- src/control.rs | 7 +++++ src/lib.rs | 2 ++ src/locale.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/locale.rs diff --git a/src/control.rs b/src/control.rs index ddcbfdaf38..9bb1ade6dc 100644 --- a/src/control.rs +++ b/src/control.rs @@ -5,6 +5,8 @@ use std::ops::RangeInclusive; +use crate::locale::UiLocale; + /// Special value that can be used in a heartbeat control message to disable RPi watchdog pub const DISABLE_RPI_WATCHDOG: u16 = 43_690; @@ -71,6 +73,8 @@ pub enum ControlSetting { TargetInspiratoryFlow = 25, /// Duration of inspiration in ms (value bounds must be between 200 and 3000) InspiratoryDuration = 26, + /// Language of the UI; this should be two letters (see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1)) in ASCII representation as two u8 + UiLocale = 27, } impl ControlSetting { @@ -105,6 +109,7 @@ impl ControlSetting { Self::LeakAlarmThreshold => 200, Self::TargetInspiratoryFlow => 40, Self::InspiratoryDuration => 800, + Self::UiLocale => UiLocale::default().as_usize(), } } @@ -139,6 +144,7 @@ impl ControlSetting { Self::LeakAlarmThreshold => RangeInclusive::new(0, 10_000), Self::TargetInspiratoryFlow => RangeInclusive::new(5, 80), Self::InspiratoryDuration => RangeInclusive::new(200, 3_000), + Self::UiLocale => UiLocale::bounds(), } } } @@ -175,6 +181,7 @@ impl std::convert::TryFrom for ControlSetting { 24 => Ok(ControlSetting::LeakAlarmThreshold), 25 => Ok(ControlSetting::TargetInspiratoryFlow), 26 => Ok(ControlSetting::InspiratoryDuration), + 27 => Ok(ControlSetting::UiLocale), _ => Err("Invalid setting number"), } } diff --git a/src/lib.rs b/src/lib.rs index 2a72697bb8..b5fe80e369 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub mod alarm; /// Structures to represent control messages pub mod control; +/// Tools to manipulate ISO 639-1 language codes to be used in the control protocol +pub mod locale; /// Underlying parsers for telemetry messages pub mod parsers; /// Structures to represent telemetry messages diff --git a/src/locale.rs b/src/locale.rs new file mode 100644 index 0000000000..9916944b54 --- /dev/null +++ b/src/locale.rs @@ -0,0 +1,77 @@ +// MakAir Telemetry +// +// Copyright: 2020, Makers For Life +// License: Public Domain License + +use std::convert::TryFrom; +use std::ops::RangeInclusive; + +/// An ISO 639-1 language code to be used to choose language for the ControlUI +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UiLocale(u16); + +impl UiLocale { + /// Language code as a u16 + pub fn as_u16(&self) -> u16 { + self.0 + } + + /// Language code as a usize + pub fn as_usize(&self) -> usize { + self.0.into() + } + + /// Allowed value bounds + pub fn bounds() -> RangeInclusive { + RangeInclusive::new( + Self::try_from("aa").unwrap().as_usize(), + Self::try_from("zz").unwrap().as_usize(), + ) + } +} + +impl TryFrom<&str> for UiLocale { + type Error = &'static str; + + fn try_from(value: &str) -> Result { + if value.len() == 2 { + let bytes = value.as_bytes(); + let w = ((bytes[0] as u16) << 8) | bytes[1] as u16; + Ok(UiLocale(w)) + } else { + Err("language code must be exactly 2 characters, according to ISO 639-1") + } + } +} + +impl Default for UiLocale { + fn default() -> Self { + UiLocale::try_from("en").unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::UiLocale; + + use std::convert::TryFrom; + + #[test] + fn fr() { + let expected: u16 = 0x6672; + assert_eq!( + UiLocale::try_from("fr").map(|code| code.as_u16()), + Ok(expected) + ); + } + + #[test] + fn empty() { + assert!(UiLocale::try_from("").is_err()) + } + + #[test] + fn too_long() { + assert!(UiLocale::try_from("fra").is_err()) + } +} From 3f94ab1f1ebd410f65ec103562590470bb44662d Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sat, 9 Jan 2021 14:09:44 +0100 Subject: [PATCH 2/8] Improve UiLocale --- src/locale.rs | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/locale.rs b/src/locale.rs index 9916944b54..045f4b9a53 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -21,7 +21,7 @@ impl UiLocale { self.0.into() } - /// Allowed value bounds + /// Allowed value bounds (this is not really correct/useful) pub fn bounds() -> RangeInclusive { RangeInclusive::new( Self::try_from("aa").unwrap().as_usize(), @@ -50,28 +50,59 @@ impl Default for UiLocale { } } +impl std::fmt::Display for UiLocale { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = self.0.to_be_bytes(); + let str = String::from_utf8_lossy(&bytes); + f.write_str(&str) + } +} + #[cfg(test)] mod tests { use super::UiLocale; + use proptest::prelude::*; use std::convert::TryFrom; + const FR: u16 = 0x6672; + + fn ui_locale_strategy() -> impl Strategy { + proptest::num::u16::ANY.prop_filter_map("Invalid UI locale code", |code| { + let ui_locale = UiLocale(code); + if ui_locale.to_string().is_ascii() { + Some(ui_locale) + } else { + None + } + }) + } + #[test] - fn fr() { - let expected: u16 = 0x6672; - assert_eq!( - UiLocale::try_from("fr").map(|code| code.as_u16()), - Ok(expected) - ); + fn from_str_fr() { + assert_eq!(UiLocale::try_from("fr").map(|code| code.as_u16()), Ok(FR)); } #[test] - fn empty() { + fn from_str_empty() { assert!(UiLocale::try_from("").is_err()) } #[test] - fn too_long() { + fn from_str_too_long() { assert!(UiLocale::try_from("fra").is_err()) } + + #[test] + fn to_str() { + assert_eq!(UiLocale(FR).to_string().as_str(), "fr") + } + + proptest! { + #[test] + fn back_and_forth(ui_locale in ui_locale_strategy()) { + let str = ui_locale.to_string(); + assert_eq!(UiLocale::try_from(str.as_str()).map(|code| code.as_u16()), Ok(ui_locale.as_u16())) + } + } } From 0b01d671a50a1758ccea5285bb334528d7345bb8 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sat, 9 Jan 2021 18:04:21 +0100 Subject: [PATCH 3/8] Rename UiLocale to Locale --- src/control.rs | 10 +++++----- src/locale.rs | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/control.rs b/src/control.rs index 9bb1ade6dc..fd1740250f 100644 --- a/src/control.rs +++ b/src/control.rs @@ -5,7 +5,7 @@ use std::ops::RangeInclusive; -use crate::locale::UiLocale; +use crate::locale::Locale; /// Special value that can be used in a heartbeat control message to disable RPi watchdog pub const DISABLE_RPI_WATCHDOG: u16 = 43_690; @@ -74,7 +74,7 @@ pub enum ControlSetting { /// Duration of inspiration in ms (value bounds must be between 200 and 3000) InspiratoryDuration = 26, /// Language of the UI; this should be two letters (see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1)) in ASCII representation as two u8 - UiLocale = 27, + Locale = 27, } impl ControlSetting { @@ -109,7 +109,7 @@ impl ControlSetting { Self::LeakAlarmThreshold => 200, Self::TargetInspiratoryFlow => 40, Self::InspiratoryDuration => 800, - Self::UiLocale => UiLocale::default().as_usize(), + Self::Locale => Locale::default().as_usize(), } } @@ -144,7 +144,7 @@ impl ControlSetting { Self::LeakAlarmThreshold => RangeInclusive::new(0, 10_000), Self::TargetInspiratoryFlow => RangeInclusive::new(5, 80), Self::InspiratoryDuration => RangeInclusive::new(200, 3_000), - Self::UiLocale => UiLocale::bounds(), + Self::Locale => Locale::bounds(), } } } @@ -181,7 +181,7 @@ impl std::convert::TryFrom for ControlSetting { 24 => Ok(ControlSetting::LeakAlarmThreshold), 25 => Ok(ControlSetting::TargetInspiratoryFlow), 26 => Ok(ControlSetting::InspiratoryDuration), - 27 => Ok(ControlSetting::UiLocale), + 27 => Ok(ControlSetting::Locale), _ => Err("Invalid setting number"), } } diff --git a/src/locale.rs b/src/locale.rs index 045f4b9a53..4d828dfca1 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -6,11 +6,11 @@ use std::convert::TryFrom; use std::ops::RangeInclusive; -/// An ISO 639-1 language code to be used to choose language for the ControlUI +/// An ISO 639-1 language code to be used to choose language for the whole system #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct UiLocale(u16); +pub struct Locale(u16); -impl UiLocale { +impl Locale { /// Language code as a u16 pub fn as_u16(&self) -> u16 { self.0 @@ -30,27 +30,27 @@ impl UiLocale { } } -impl TryFrom<&str> for UiLocale { +impl TryFrom<&str> for Locale { type Error = &'static str; fn try_from(value: &str) -> Result { if value.len() == 2 { let bytes = value.as_bytes(); let w = ((bytes[0] as u16) << 8) | bytes[1] as u16; - Ok(UiLocale(w)) + Ok(Locale(w)) } else { Err("language code must be exactly 2 characters, according to ISO 639-1") } } } -impl Default for UiLocale { +impl Default for Locale { fn default() -> Self { - UiLocale::try_from("en").unwrap() + Locale::try_from("en").unwrap() } } -impl std::fmt::Display for UiLocale { +impl std::fmt::Display for Locale { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let bytes = self.0.to_be_bytes(); let str = String::from_utf8_lossy(&bytes); @@ -60,16 +60,16 @@ impl std::fmt::Display for UiLocale { #[cfg(test)] mod tests { - use super::UiLocale; + use super::Locale; use proptest::prelude::*; use std::convert::TryFrom; const FR: u16 = 0x6672; - fn ui_locale_strategy() -> impl Strategy { + fn ui_locale_strategy() -> impl Strategy { proptest::num::u16::ANY.prop_filter_map("Invalid UI locale code", |code| { - let ui_locale = UiLocale(code); + let ui_locale = Locale(code); if ui_locale.to_string().is_ascii() { Some(ui_locale) } else { @@ -80,29 +80,29 @@ mod tests { #[test] fn from_str_fr() { - assert_eq!(UiLocale::try_from("fr").map(|code| code.as_u16()), Ok(FR)); + assert_eq!(Locale::try_from("fr").map(|code| code.as_u16()), Ok(FR)); } #[test] fn from_str_empty() { - assert!(UiLocale::try_from("").is_err()) + assert!(Locale::try_from("").is_err()) } #[test] fn from_str_too_long() { - assert!(UiLocale::try_from("fra").is_err()) + assert!(Locale::try_from("fra").is_err()) } #[test] fn to_str() { - assert_eq!(UiLocale(FR).to_string().as_str(), "fr") + assert_eq!(Locale(FR).to_string().as_str(), "fr") } proptest! { #[test] fn back_and_forth(ui_locale in ui_locale_strategy()) { let str = ui_locale.to_string(); - assert_eq!(UiLocale::try_from(str.as_str()).map(|code| code.as_u16()), Ok(ui_locale.as_u16())) + assert_eq!(Locale::try_from(str.as_str()).map(|code| code.as_u16()), Ok(ui_locale.as_u16())) } } } From 4a93a1f8291145ddf6824f09115b9466f8d0c01f Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sat, 9 Jan 2021 22:58:33 +0100 Subject: [PATCH 4/8] Add a new telemetry message for fatal errors Fix #33 --- src/cli/bin.rs | 5 ++ src/cli/convert.rs | 3 + src/lib.rs | 6 ++ src/parsers/v2.rs | 190 +++++++++++++++++++++++++++++++++++++++++++++ src/structures.rs | 57 ++++++++++++++ 5 files changed, 261 insertions(+) diff --git a/src/cli/bin.rs b/src/cli/bin.rs index dd402f178b..697008fc08 100644 --- a/src/cli/bin.rs +++ b/src/cli/bin.rs @@ -319,6 +319,7 @@ fn stats(cfg: Stats) { let mut nb_machine_state_snapshots: u32 = 0; let mut nb_stopped_messages: u32 = 0; let mut nb_control_ack: u32 = 0; + let mut nb_fatal_error: u32 = 0; loop { match rx.try_recv() { @@ -343,6 +344,9 @@ fn stats(cfg: Stats) { TelemetryMessage::ControlAck(_) => { nb_control_ack += 1; } + TelemetryMessage::FatalError(_) => { + nb_fatal_error += 1; + } } telemetry_messages.push(message); } @@ -358,6 +362,7 @@ fn stats(cfg: Stats) { println!("Nb MachineStateSnapshot: {}", nb_machine_state_snapshots); println!("Nb StoppedMessage: {}", nb_stopped_messages); println!("Nb ControlAck: {}", nb_control_ack); + println!("Nb FatalError: {}", nb_fatal_error); println!( "Estimated duration: {:.3} seconds", compute_duration(telemetry_messages) as f32 / 1000_f32 diff --git a/src/cli/convert.rs b/src/cli/convert.rs index a7e92f3071..1f818b7d5a 100644 --- a/src/cli/convert.rs +++ b/src/cli/convert.rs @@ -162,6 +162,9 @@ pub fn telemetry_to_gts(message: &TelemetryMessage, source_label: &Option { // Do nothing: we don't want this kind of messages } + TelemetryMessage::FatalError(_) => { + // Do nothing: we don't want this kind of messages + } }; output.iter().fold(String::new(), |mut acc, cur| { acc.push_str(cur); diff --git a/src/lib.rs b/src/lib.rs index b5fe80e369..d2e35729c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -281,6 +281,12 @@ pub fn display_message(message: TelemetryChannelType) { }))) => { info!("← {:?} = {}", &setting, &value); } + Ok(TelemetryMessageOrError::Message(TelemetryMessage::FatalError(FatalError { + error, + .. + }))) => { + info!("***** FATAL ERROR ***** {:?}", &error); + } Ok(TelemetryMessageOrError::Error(e)) => { warn!("a high-level error occurred: {:?}", e); } diff --git a/src/parsers/v2.rs b/src/parsers/v2.rs index cb924991e0..842593b61e 100644 --- a/src/parsers/v2.rs +++ b/src/parsers/v2.rs @@ -53,6 +53,60 @@ named!( map_res!(be_u8, |num| VentilationMode::try_from(num)) ); +fn fatal_error_details(input: &[u8]) -> IResult<&[u8], FatalErrorDetails> { + use nom::error::{Error, ErrorKind}; + use nom::Err::Failure; + use FatalErrorDetails::*; + + let (input, error_type) = be_u8(input)?; + match error_type { + 1 => Ok((input, WatchdogRestart)), + 2 => { + do_parse!( + input, + sep >> pressure_offset: be_i16 + >> sep + >> min_pressure: be_i16 + >> sep + >> max_pressure: be_i16 + >> sep + >> flow_at_starting: be_i16 + >> sep + >> flow_with_blower_on: be_i16 + >> (CalibrationError { + pressure_offset, + min_pressure, + max_pressure, + flow_at_starting: if flow_at_starting == i16::MAX { + None + } else { + Some(flow_at_starting) + }, + flow_with_blower_on: if flow_with_blower_on == i16::MAX { + None + } else { + Some(flow_with_blower_on) + }, + }) + ) + } + 3 => { + do_parse!( + input, + sep >> battery_level: be_u16 >> (BatteryDeeplyDischarged { battery_level }) + ) + } + 4 => Ok((input, MassFlowMeterError)), + 5 => { + do_parse!( + input, + sep >> pressure: be_u16 >> (InconsistentPressure { pressure }) + ) + } + _ => Err(Failure(Error::new(input, ErrorKind::Switch))), + } +} + named!( boot, do_parse!( @@ -497,6 +551,34 @@ named!( ) ); +named!( + fatal_error, + do_parse!( + tag!("E:") + >> tag!([VERSION]) + >> software_version_len: be_u8 + >> software_version: + map_res!(take!(software_version_len), |bytes| std::str::from_utf8( + bytes + )) + >> device_id1: be_u32 + >> device_id2: be_u32 + >> device_id3: be_u32 + >> sep + >> systick: be_u64 + >> sep + >> error: fatal_error_details + >> end + >> (TelemetryMessage::FatalError(FatalError { + telemetry_version: VERSION, + version: software_version.to_string(), + device_id: format!("{}-{}-{}", device_id1, device_id2, device_id3), + systick, + error, + })) + ) +); + /// Transform bytes into a structured telemetry message /// /// * `input` - Bytes to parse. @@ -510,6 +592,7 @@ pub fn message(input: &[u8]) -> IResult<&[u8], TelemetryMessage, TelemetryError< machine_state_snapshot, alarm_trap, control_ack, + fatal_error, ))(input) .map_err(nom::Err::convert) } @@ -571,6 +654,41 @@ mod tests { m.into() } + fn fatal_error_details_strategy() -> BoxedStrategy { + prop_oneof![ + Just(FatalErrorDetails::WatchdogRestart), + fatal_error_details_calibration_error_strategy(), + fatal_error_details_battery_deeply_discharged_strategy(), + Just(FatalErrorDetails::MassFlowMeterError), + fatal_error_details_inconsistent_pressure_strategy(), + ] + .boxed() + } + + prop_compose! { + fn fatal_error_details_calibration_error_strategy()( + pressure_offset in num::i16::ANY, + min_pressure in num::i16::ANY, + max_pressure in num::i16::ANY, + flow_at_starting in option::of(num::i16::ANY), + flow_with_blower_on in option::of(num::i16::ANY), + ) -> FatalErrorDetails { + FatalErrorDetails::CalibrationError { pressure_offset, min_pressure, max_pressure, flow_at_starting, flow_with_blower_on } + } + } + + prop_compose! { + fn fatal_error_details_battery_deeply_discharged_strategy()(battery_level in num::u16::ANY) -> FatalErrorDetails { + FatalErrorDetails::BatteryDeeplyDischarged { battery_level } + } + } + + prop_compose! { + fn fatal_error_details_inconsistent_pressure_strategy()(pressure in num::u16::ANY) -> FatalErrorDetails { + FatalErrorDetails::InconsistentPressure { pressure } + } + } + proptest! { #[test] fn test_boot_message_parser( @@ -1136,4 +1254,76 @@ mod tests { assert_eq!(nom::dbg_dmp(control_ack, "control_ack")(input), Ok((&[][..], expected))); } } + + proptest! { + #[test] + fn test_fatal_error_message_parser( + version in ".*", + device_id1 in (0u32..), + device_id2 in (0u32..), + device_id3 in (0u32..), + systick in (0u64..), + error in fatal_error_details_strategy(), + ) { + let msg = FatalError { + telemetry_version: VERSION, + version, + device_id: format!("{}-{}-{}", device_id1, device_id2, device_id3), + systick, + error, + }; + + let fatal_error_details: Vec = match msg.error { + FatalErrorDetails::WatchdogRestart => vec![1], + FatalErrorDetails::CalibrationError { + pressure_offset, + min_pressure, + max_pressure, + flow_at_starting, + flow_with_blower_on + } => flat(&[ + &[2], + b"\t", + &pressure_offset.to_be_bytes(), + b"\t", + &min_pressure.to_be_bytes(), + b"\t", + &max_pressure.to_be_bytes(), + b"\t", + &flow_at_starting.unwrap_or(i16::MAX).to_be_bytes(), + b"\t", + &flow_with_blower_on.unwrap_or(i16::MAX).to_be_bytes(), + ]), + FatalErrorDetails::BatteryDeeplyDischarged { battery_level } => flat(&[ + &[3], + b"\t", + &battery_level.to_be_bytes(), + ]), + FatalErrorDetails::MassFlowMeterError => vec![4], + FatalErrorDetails::InconsistentPressure { pressure } => flat(&[ + &[5], + b"\t", + &pressure.to_be_bytes(), + ]), + }; + + let input = &flat(&[ + b"E:", + &[VERSION], + &[msg.version.len() as u8], + &msg.version.as_bytes(), + &device_id1.to_be_bytes(), + &device_id2.to_be_bytes(), + &device_id3.to_be_bytes(), + b"\t", + &msg.systick.to_be_bytes(), + b"\t", + &fatal_error_details, + b"\n", + ]); + + let expected = TelemetryMessage::FatalError(msg); + assert_eq!(nom::dbg_dmp(fatal_error, "fatal_error")(input), Ok((&[][..], expected))); + } + } } diff --git a/src/structures.rs b/src/structures.rs index beb377f862..a5ef2da604 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -175,6 +175,39 @@ impl VentilationMode { } } +/// Details of fatal errors +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] +pub enum FatalErrorDetails { + /// MCU was restarted by watchdog + WatchdogRestart, + /// Calibration failed + CalibrationError { + /// Measured pressure offset in mmH2O + pressure_offset: i16, + /// Minimum presure measured during calibration in mmH2O + min_pressure: i16, + /// Maximum presure measured during calibration in mmH2O + max_pressure: i16, + /// Air flow measured at starting in cL/min (SLM * 100) + flow_at_starting: Option, + /// Air flow measured with blower ON in cL/min (SLM * 100) + flow_with_blower_on: Option, + }, + /// Battery is too discharged + BatteryDeeplyDischarged { + /// Battery level in centivolts + battery_level: u16, + }, + /// Could not read mass flow meter + MassFlowMeterError, + /// Read an inconsistent pressure + InconsistentPressure { + /// Measured pressure in ùùH2O + pressure: u16, + }, +} + /// A telemetry message that is sent once every time the MCU boots #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] @@ -445,6 +478,22 @@ pub struct ControlAck { pub value: u16, } +/// [protocol v2] A message sent when a fatal error occurs +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] +pub struct FatalError { + /// Version of the telemetry protocol + pub telemetry_version: u8, + /// Version of the MCU firmware + pub version: String, + /// Internal ID of the MCU + pub device_id: String, + /// Number of microseconds since the MCU booted + pub systick: u64, + /// Details of the error + pub error: FatalErrorDetails, +} + /// Supported telemetry messages #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] @@ -462,6 +511,8 @@ pub enum TelemetryMessage { AlarmTrap(AlarmTrap), /// An ACK message that is sent every time a setting is changed using the control protocol ControlAck(ControlAck), + /// [protocol v2] A message sent when a fatal error occurs + FatalError(FatalError), } impl TelemetryMessage { @@ -486,6 +537,9 @@ impl TelemetryMessage { Self::ControlAck(ControlAck { telemetry_version, .. }) => telemetry_version, + Self::FatalError(FatalError { + telemetry_version, .. + }) => telemetry_version, }; *val } @@ -499,6 +553,7 @@ impl TelemetryMessage { Self::MachineStateSnapshot(MachineStateSnapshot { version, .. }) => version, Self::AlarmTrap(AlarmTrap { version, .. }) => version, Self::ControlAck(ControlAck { version, .. }) => version, + Self::FatalError(FatalError { version, .. }) => version, }; val.clone() } @@ -512,6 +567,7 @@ impl TelemetryMessage { Self::MachineStateSnapshot(MachineStateSnapshot { device_id, .. }) => device_id, Self::AlarmTrap(AlarmTrap { device_id, .. }) => device_id, Self::ControlAck(ControlAck { device_id, .. }) => device_id, + Self::FatalError(FatalError { device_id, .. }) => device_id, }; val.clone() } @@ -525,6 +581,7 @@ impl TelemetryMessage { Self::MachineStateSnapshot(MachineStateSnapshot { systick, .. }) => systick, Self::AlarmTrap(AlarmTrap { systick, .. }) => systick, Self::ControlAck(ControlAck { systick, .. }) => systick, + Self::FatalError(FatalError { systick, .. }) => systick, }; *val } From d484262856b38df72372b0a87cdc7c5dab88c376 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sat, 9 Jan 2021 23:58:43 +0100 Subject: [PATCH 5/8] Add a new telemetry message for EOL tests Fix #34 --- src/cli/bin.rs | 5 ++ src/cli/convert.rs | 3 ++ src/lib.rs | 6 +++ src/parsers/v2.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++ src/structures.rs | 114 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 257 insertions(+) diff --git a/src/cli/bin.rs b/src/cli/bin.rs index 697008fc08..813f15cab8 100644 --- a/src/cli/bin.rs +++ b/src/cli/bin.rs @@ -320,6 +320,7 @@ fn stats(cfg: Stats) { let mut nb_stopped_messages: u32 = 0; let mut nb_control_ack: u32 = 0; let mut nb_fatal_error: u32 = 0; + let mut nb_eol_test_snapshots: u32 = 0; loop { match rx.try_recv() { @@ -347,6 +348,9 @@ fn stats(cfg: Stats) { TelemetryMessage::FatalError(_) => { nb_fatal_error += 1; } + TelemetryMessage::EolTestSnapshot(_) => { + nb_eol_test_snapshots += 1; + } } telemetry_messages.push(message); } @@ -363,6 +367,7 @@ fn stats(cfg: Stats) { println!("Nb StoppedMessage: {}", nb_stopped_messages); println!("Nb ControlAck: {}", nb_control_ack); println!("Nb FatalError: {}", nb_fatal_error); + println!("Nb EolTestSnapshot: {}", nb_eol_test_snapshots); println!( "Estimated duration: {:.3} seconds", compute_duration(telemetry_messages) as f32 / 1000_f32 diff --git a/src/cli/convert.rs b/src/cli/convert.rs index 1f818b7d5a..e64dfc07c5 100644 --- a/src/cli/convert.rs +++ b/src/cli/convert.rs @@ -165,6 +165,9 @@ pub fn telemetry_to_gts(message: &TelemetryMessage, source_label: &Option { // Do nothing: we don't want this kind of messages } + TelemetryMessage::EolTestSnapshot(_) => { + // Do nothing: we don't want this kind of messages + } }; output.iter().fold(String::new(), |mut acc, cur| { acc.push_str(cur); diff --git a/src/lib.rs b/src/lib.rs index d2e35729c4..7923c3452f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -287,6 +287,12 @@ pub fn display_message(message: TelemetryChannelType) { }))) => { info!("***** FATAL ERROR ***** {:?}", &error); } + Ok(TelemetryMessageOrError::Message(TelemetryMessage::EolTestSnapshot(_))) => { + info!( + " {:?}", + &message.expect("failed unwrapping message for EOL test snapshot") + ); + } Ok(TelemetryMessageOrError::Error(e)) => { warn!("a high-level error occurred: {:?}", e); } diff --git a/src/parsers/v2.rs b/src/parsers/v2.rs index 842593b61e..b38a2f6d17 100644 --- a/src/parsers/v2.rs +++ b/src/parsers/v2.rs @@ -107,6 +107,30 @@ fn fatal_error_details(input: &[u8]) -> IResult<&[u8], FatalErrorDetails> { } } +named!( + eol_test_step, + map_res!(be_u8, |step| EolTestStep::try_from(step)) +); + +fn eol_test_snapshot_content(input: &[u8]) -> IResult<&[u8], EolTestSnapshotContent> { + use nom::error::{Error, ErrorKind}; + use nom::Err::Failure; + use EolTestSnapshotContent::*; + + let (input, content_type) = be_u8(input)?; + match content_type { + 0 => Ok((input, InProgress)), + 1 => { + do_parse!( + input, + sep >> error: u8_array >> (Error(String::from_utf8_lossy(&error).into_owned())) + ) + } + 2 => Ok((input, Success)), + _ => Err(Failure(Error::new(input, ErrorKind::Switch))), + } +} + named!( boot, do_parse!( @@ -579,6 +603,37 @@ named!( ) ); +named!( + eol_test_snapshot, + do_parse!( + tag!("L:") + >> tag!([VERSION]) + >> software_version_len: be_u8 + >> software_version: + map_res!(take!(software_version_len), |bytes| std::str::from_utf8( + bytes + )) + >> device_id1: be_u32 + >> device_id2: be_u32 + >> device_id3: be_u32 + >> sep + >> systick: be_u64 + >> sep + >> current_step: eol_test_step + >> sep + >> content: eol_test_snapshot_content + >> end + >> (TelemetryMessage::EolTestSnapshot(EolTestSnapshot { + telemetry_version: VERSION, + version: software_version.to_string(), + device_id: format!("{}-{}-{}", device_id1, device_id2, device_id3), + systick, + current_step, + content, + })) + ) +); + /// Transform bytes into a structured telemetry message /// /// * `input` - Bytes to parse. @@ -593,6 +648,7 @@ pub fn message(input: &[u8]) -> IResult<&[u8], TelemetryMessage, TelemetryError< alarm_trap, control_ack, fatal_error, + eol_test_snapshot, ))(input) .map_err(nom::Err::convert) } @@ -689,6 +745,26 @@ mod tests { } } + fn eol_test_step_strategy() -> impl Strategy { + proptest::num::u8::ANY + .prop_filter_map("Invalid test step", |n| EolTestStep::try_from(n).ok()) + } + + fn eol_test_snapshot_content_strategy() -> BoxedStrategy { + prop_oneof![ + Just(EolTestSnapshotContent::InProgress), + eol_test_snapshot_content_error_strategy(), + Just(EolTestSnapshotContent::Success), + ] + .boxed() + } + + prop_compose! { + fn eol_test_snapshot_content_error_strategy()(reason in ".+") -> EolTestSnapshotContent { + EolTestSnapshotContent::Error(reason) + } + } + proptest! { #[test] fn test_boot_message_parser( @@ -1326,4 +1402,57 @@ mod tests { assert_eq!(nom::dbg_dmp(fatal_error, "fatal_error")(input), Ok((&[][..], expected))); } } + + proptest! { + #[test] + fn test_eol_test_snapshot_message_parser( + version in ".*", + device_id1 in (0u32..), + device_id2 in (0u32..), + device_id3 in (0u32..), + systick in (0u64..), + current_step in eol_test_step_strategy(), + content in eol_test_snapshot_content_strategy(), + ) { + let msg = EolTestSnapshot { + telemetry_version: VERSION, + version, + device_id: format!("{}-{}-{}", device_id1, device_id2, device_id3), + systick, + current_step, + content, + }; + + let eol_test_snapshot_content: Vec = match msg.content { + EolTestSnapshotContent::InProgress => vec![0], + EolTestSnapshotContent::Error(ref reason) => flat(&[ + &[1], + b"\t", + &[reason.len() as u8], + &reason.as_bytes(), + ]), + EolTestSnapshotContent::Success => vec![2], + }; + + let input = &flat(&[ + b"L:", + &[VERSION], + &[msg.version.len() as u8], + &msg.version.as_bytes(), + &device_id1.to_be_bytes(), + &device_id2.to_be_bytes(), + &device_id3.to_be_bytes(), + b"\t", + &msg.systick.to_be_bytes(), + b"\t", + &[current_step as u8], + b"\t", + &eol_test_snapshot_content, + b"\n", + ]); + + let expected = TelemetryMessage::EolTestSnapshot(msg); + assert_eq!(nom::dbg_dmp(eol_test_snapshot, "eol_test_snapshot")(input), Ok((&[][..], expected))); + } + } } diff --git a/src/structures.rs b/src/structures.rs index a5ef2da604..fe6472b332 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -208,6 +208,94 @@ pub enum FatalErrorDetails { }, } +/// Step of the end of line test +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] +#[allow(non_camel_case_types, missing_docs)] +pub enum EolTestStep { + START, + SUPPLY_TO_EXPANDER_NOT_CONNECTED, + CHECK_FAN, + TEST_BAT_DEAD, + BATTERY_DEEP_DISCHARGE, + DISCONNECT_MAINS, + CONNECT_MAINS, + CHECK_BUZZER, + CHECK_ALL_BUTTONS, + CHECK_UI_SCREEN, + PLUG_AIR_TEST_SYTEM, + REACH_MAX_PRESSURE, + MAX_PRESSURE_REACHED_OK, + MAX_PRESSURE_NOT_REACHED, + START_LEAK_MESURE, + LEAK_IS_TOO_HIGH, + REACH_NULL_PRESSURE, + MIN_PRESSURE_NOT_REACHED, + USER_CONFIRMATION_BEFORE_O2_TEST, + START_O2_TEST, + O2_PRESSURE_NOT_REACH, + WAIT_USER_BEFORE_LONG_RUN, + START_LONG_RUN_BLOWER, + PRESSURE_NOT_STABLE, + FLOW_NOT_STABLE, + END_SUCCESS, + DISPLAY_PRESSURE, + DISPLAY_FLOW, +} + +impl TryFrom for EolTestStep { + type Error = io::Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::START), + 1 => Ok(Self::SUPPLY_TO_EXPANDER_NOT_CONNECTED), + 2 => Ok(Self::CHECK_FAN), + 3 => Ok(Self::TEST_BAT_DEAD), + 4 => Ok(Self::BATTERY_DEEP_DISCHARGE), + 5 => Ok(Self::DISCONNECT_MAINS), + 6 => Ok(Self::CONNECT_MAINS), + 7 => Ok(Self::CHECK_BUZZER), + 8 => Ok(Self::CHECK_ALL_BUTTONS), + 9 => Ok(Self::CHECK_UI_SCREEN), + 10 => Ok(Self::PLUG_AIR_TEST_SYTEM), + 11 => Ok(Self::REACH_MAX_PRESSURE), + 12 => Ok(Self::MAX_PRESSURE_REACHED_OK), + 13 => Ok(Self::MAX_PRESSURE_NOT_REACHED), + 14 => Ok(Self::START_LEAK_MESURE), + 15 => Ok(Self::LEAK_IS_TOO_HIGH), + 16 => Ok(Self::REACH_NULL_PRESSURE), + 17 => Ok(Self::MIN_PRESSURE_NOT_REACHED), + 18 => Ok(Self::USER_CONFIRMATION_BEFORE_O2_TEST), + 19 => Ok(Self::START_O2_TEST), + 20 => Ok(Self::O2_PRESSURE_NOT_REACH), + 21 => Ok(Self::WAIT_USER_BEFORE_LONG_RUN), + 22 => Ok(Self::START_LONG_RUN_BLOWER), + 23 => Ok(Self::PRESSURE_NOT_STABLE), + 24 => Ok(Self::FLOW_NOT_STABLE), + 25 => Ok(Self::END_SUCCESS), + 26 => Ok(Self::DISPLAY_PRESSURE), + 27 => Ok(Self::DISPLAY_FLOW), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid EOL test step {}", value), + )), + } + } +} + +/// Content of end of line test snapshots +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] +pub enum EolTestSnapshotContent { + /// Test is in progress + InProgress, + /// There was an error during test + Error(String), + /// End of line test succeeded + Success, +} + /// A telemetry message that is sent once every time the MCU boots #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] @@ -494,6 +582,24 @@ pub struct FatalError { pub error: FatalErrorDetails, } +/// [protocol v2] A message sent during end of line tests +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] +pub struct EolTestSnapshot { + /// Version of the telemetry protocol + pub telemetry_version: u8, + /// Version of the MCU firmware + pub version: String, + /// Internal ID of the MCU + pub device_id: String, + /// Number of microseconds since the MCU booted + pub systick: u64, + /// Current step + pub current_step: EolTestStep, + /// Content of the snapshot + pub content: EolTestSnapshotContent, +} + /// Supported telemetry messages #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] @@ -513,6 +619,8 @@ pub enum TelemetryMessage { ControlAck(ControlAck), /// [protocol v2] A message sent when a fatal error occurs FatalError(FatalError), + /// [protocol v2] A message sent during end of line tests + EolTestSnapshot(EolTestSnapshot), } impl TelemetryMessage { @@ -540,6 +648,9 @@ impl TelemetryMessage { Self::FatalError(FatalError { telemetry_version, .. }) => telemetry_version, + Self::EolTestSnapshot(EolTestSnapshot { + telemetry_version, .. + }) => telemetry_version, }; *val } @@ -554,6 +665,7 @@ impl TelemetryMessage { Self::AlarmTrap(AlarmTrap { version, .. }) => version, Self::ControlAck(ControlAck { version, .. }) => version, Self::FatalError(FatalError { version, .. }) => version, + Self::EolTestSnapshot(EolTestSnapshot { version, .. }) => version, }; val.clone() } @@ -568,6 +680,7 @@ impl TelemetryMessage { Self::AlarmTrap(AlarmTrap { device_id, .. }) => device_id, Self::ControlAck(ControlAck { device_id, .. }) => device_id, Self::FatalError(FatalError { device_id, .. }) => device_id, + Self::EolTestSnapshot(EolTestSnapshot { device_id, .. }) => device_id, }; val.clone() } @@ -582,6 +695,7 @@ impl TelemetryMessage { Self::AlarmTrap(AlarmTrap { systick, .. }) => systick, Self::ControlAck(ControlAck { systick, .. }) => systick, Self::FatalError(FatalError { systick, .. }) => systick, + Self::EolTestSnapshot(EolTestSnapshot { systick, .. }) => systick, }; *val } From 8d08a3afccad08aaf84e3ebd52b736e2838a8f96 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Sun, 10 Jan 2021 19:12:07 +0100 Subject: [PATCH 6/8] Add new setting: patient's height --- src/cli/statistics.rs | 4 ++++ src/control.rs | 5 +++++ src/parsers/v1.rs | 4 ++++ src/parsers/v2.rs | 14 ++++++++++++++ src/structures.rs | 4 ++++ 5 files changed, 31 insertions(+) diff --git a/src/cli/statistics.rs b/src/cli/statistics.rs index db33f9f33f..6be4359af0 100644 --- a/src/cli/statistics.rs +++ b/src/cli/statistics.rs @@ -136,6 +136,7 @@ mod tests { inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, }, )]; @@ -180,6 +181,7 @@ mod tests { inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, })); assert_eq!(compute_duration(vect), 100); @@ -275,6 +277,7 @@ mod tests { inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, }, )); @@ -312,6 +315,7 @@ mod tests { inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, })); assert_eq!(compute_duration(vect), 110); diff --git a/src/control.rs b/src/control.rs index fd1740250f..ac9cd82d13 100644 --- a/src/control.rs +++ b/src/control.rs @@ -75,6 +75,8 @@ pub enum ControlSetting { InspiratoryDuration = 26, /// Language of the UI; this should be two letters (see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1)) in ASCII representation as two u8 Locale = 27, + /// Patient's height in centimeters + PatientHeight = 28, } impl ControlSetting { @@ -110,6 +112,7 @@ impl ControlSetting { Self::TargetInspiratoryFlow => 40, Self::InspiratoryDuration => 800, Self::Locale => Locale::default().as_usize(), + Self::PatientHeight => 160, } } @@ -145,6 +148,7 @@ impl ControlSetting { Self::TargetInspiratoryFlow => RangeInclusive::new(5, 80), Self::InspiratoryDuration => RangeInclusive::new(200, 3_000), Self::Locale => Locale::bounds(), + Self::PatientHeight => RangeInclusive::new(100, 250), } } } @@ -182,6 +186,7 @@ impl std::convert::TryFrom for ControlSetting { 25 => Ok(ControlSetting::TargetInspiratoryFlow), 26 => Ok(ControlSetting::InspiratoryDuration), 27 => Ok(ControlSetting::Locale), + 28 => Ok(ControlSetting::PatientHeight), _ => Err("Invalid setting number"), } } diff --git a/src/parsers/v1.rs b/src/parsers/v1.rs index b519959821..f022334dcd 100644 --- a/src/parsers/v1.rs +++ b/src/parsers/v1.rs @@ -139,6 +139,7 @@ named!( inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, }) }) ) @@ -280,6 +281,7 @@ named!( inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, })) ) ); @@ -522,6 +524,7 @@ mod tests { inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, }; // This needs to be consistent with sendStoppedMessage() defined in src/software/firmware/srcs/telemetry.cpp @@ -671,6 +674,7 @@ mod tests { inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, }; // This needs to be consistent with sendMachineStateSnapshot() defined in makair-firmware/srcs/telemetry.cpp diff --git a/src/parsers/v2.rs b/src/parsers/v2.rs index b38a2f6d17..86502c0b13 100644 --- a/src/parsers/v2.rs +++ b/src/parsers/v2.rs @@ -237,6 +237,8 @@ named!( >> battery_level: be_u16 >> sep >> current_alarm_codes: u8_array + >> sep + >> patient_height: be_u8 >> end >> ({ TelemetryMessage::StoppedMessage(StoppedMessage { @@ -285,6 +287,7 @@ named!( inspiratory_duration_command: Some(inspiratory_duration_command), battery_level: Some(battery_level), current_alarm_codes: Some(current_alarm_codes), + patient_height: Some(patient_height), }) }) ) @@ -430,6 +433,8 @@ named!( >> previous_inspiratory_duration: be_u16 >> sep >> battery_level: be_u16 + >> sep + >> patient_height: be_u8 >> end >> (TelemetryMessage::MachineStateSnapshot(MachineStateSnapshot { telemetry_version: VERSION, @@ -484,6 +489,7 @@ named!( inspiratory_duration_command: Some(inspiratory_duration_command), previous_inspiratory_duration: Some(previous_inspiratory_duration), battery_level: Some(battery_level), + patient_height: Some(patient_height), })) ) ); @@ -845,6 +851,7 @@ mod tests { inspiratory_duration_command in num::u16::ANY, battery_level in num::u16::ANY, current_alarm_codes in collection::vec(0u8.., 0..100), + patient_height in num::u8::ANY, ) { let msg = StoppedMessage { telemetry_version: VERSION, @@ -880,6 +887,7 @@ mod tests { inspiratory_duration_command: Some(inspiratory_duration_command), battery_level: Some(battery_level), current_alarm_codes: Some(current_alarm_codes), + patient_height: Some(patient_height), }; // This needs to be consistent with sendStoppedMessage() defined in src/software/firmware/srcs/telemetry.cpp @@ -952,6 +960,8 @@ mod tests { b"\t", &[msg.current_alarm_codes.clone().unwrap_or_default().len() as u8], &msg.current_alarm_codes.clone().unwrap_or_default(), + b"\t", + &msg.patient_height.unwrap_or_default().to_be_bytes(), b"\n", ]); @@ -1076,6 +1086,7 @@ mod tests { inspiratory_duration_command in num::u16::ANY, previous_inspiratory_duration in num::u16::ANY, battery_level in num::u16::ANY, + patient_height in num::u8::ANY, ) { let msg = MachineStateSnapshot { telemetry_version: VERSION, @@ -1118,6 +1129,7 @@ mod tests { inspiratory_duration_command: Some(inspiratory_duration_command), previous_inspiratory_duration: Some(previous_inspiratory_duration), battery_level: Some(battery_level), + patient_height: Some(patient_height), }; // This needs to be consistent with sendMachineStateSnapshot() defined in src/software/firmware/srcs/telemetry.cpp @@ -1204,6 +1216,8 @@ mod tests { &msg.previous_inspiratory_duration.unwrap_or_default().to_be_bytes(), b"\t", &msg.battery_level.unwrap_or_default().to_be_bytes(), + b"\t", + &msg.patient_height.unwrap_or_default().to_be_bytes(), b"\n", ]); diff --git a/src/structures.rs b/src/structures.rs index fe6472b332..f904d9bb87 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -386,6 +386,8 @@ pub struct StoppedMessage { pub battery_level: Option, /// [protocol v2] Codes of the alarms that are currently triggered pub current_alarm_codes: Option>, + /// [protocol v2] Patient's height in centimeters + pub patient_height: Option, } /// A telemetry message that is sent every time the firmware does a control iteration (every 10 ms) @@ -508,6 +510,8 @@ pub struct MachineStateSnapshot { pub previous_inspiratory_duration: Option, /// [protocol v2] Measured battery level value in centivolts (precise value) pub battery_level: Option, + /// [protocol v2] Patient's height in centimeters + pub patient_height: Option, } /// A telemetry message that is sent every time an alarm is triggered or stopped From 2f826bced7a081404b05084cd049638e37061b19 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Wed, 13 Jan 2021 20:01:46 +0100 Subject: [PATCH 7/8] Fix a typo --- src/structures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures.rs b/src/structures.rs index f904d9bb87..c3ebf3fc1f 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -203,7 +203,7 @@ pub enum FatalErrorDetails { MassFlowMeterError, /// Read an inconsistent pressure InconsistentPressure { - /// Measured pressure in ùùH2O + /// Measured pressure in mmH2O pressure: u16, }, } From af57f23463d0b5385b89c0fe7152f420c1e3f599 Mon Sep 17 00:00:00 2001 From: David Sferruzza Date: Wed, 13 Jan 2021 20:16:27 +0100 Subject: [PATCH 8/8] Add locale in stopped and machine state messages --- src/cli/statistics.rs | 4 ++++ src/control.rs | 2 +- src/locale.rs | 9 +++++++++ src/parsers/v1.rs | 4 ++++ src/parsers/v2.rs | 13 +++++++++++++ src/structures.rs | 5 +++++ 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/cli/statistics.rs b/src/cli/statistics.rs index 6be4359af0..0a497162fa 100644 --- a/src/cli/statistics.rs +++ b/src/cli/statistics.rs @@ -137,6 +137,7 @@ mod tests { previous_inspiratory_duration: None, battery_level: None, patient_height: None, + locale: None, }, )]; @@ -182,6 +183,7 @@ mod tests { battery_level: None, current_alarm_codes: None, patient_height: None, + locale: None, })); assert_eq!(compute_duration(vect), 100); @@ -278,6 +280,7 @@ mod tests { previous_inspiratory_duration: None, battery_level: None, patient_height: None, + locale: None, }, )); @@ -316,6 +319,7 @@ mod tests { battery_level: None, current_alarm_codes: None, patient_height: None, + locale: None, })); assert_eq!(compute_duration(vect), 110); diff --git a/src/control.rs b/src/control.rs index ac9cd82d13..ac97fd0b93 100644 --- a/src/control.rs +++ b/src/control.rs @@ -73,7 +73,7 @@ pub enum ControlSetting { TargetInspiratoryFlow = 25, /// Duration of inspiration in ms (value bounds must be between 200 and 3000) InspiratoryDuration = 26, - /// Language of the UI; this should be two letters (see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1)) in ASCII representation as two u8 + /// Language of the system; this should be two letters (see [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1)) in ASCII representation as two u8 Locale = 27, /// Patient's height in centimeters PatientHeight = 28, diff --git a/src/locale.rs b/src/locale.rs index 4d828dfca1..8e7f25b0f3 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -8,9 +8,18 @@ use std::ops::RangeInclusive; /// An ISO 639-1 language code to be used to choose language for the whole system #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serialize-messages", derive(serde::Serialize))] pub struct Locale(u16); impl Locale { + /// Create a locale from a u16 + pub fn try_from_u16(num: u16) -> Option { + match Self::try_from(Self(num).to_string().as_str()) { + Ok(locale) => Some(locale), + Err(_) => None, + } + } + /// Language code as a u16 pub fn as_u16(&self) -> u16 { self.0 diff --git a/src/parsers/v1.rs b/src/parsers/v1.rs index f022334dcd..3a125eb78b 100644 --- a/src/parsers/v1.rs +++ b/src/parsers/v1.rs @@ -140,6 +140,7 @@ named!( battery_level: None, current_alarm_codes: None, patient_height: None, + locale: None, }) }) ) @@ -282,6 +283,7 @@ named!( previous_inspiratory_duration: None, battery_level: None, patient_height: None, + locale: None, })) ) ); @@ -525,6 +527,7 @@ mod tests { battery_level: None, current_alarm_codes: None, patient_height: None, + locale: None, }; // This needs to be consistent with sendStoppedMessage() defined in src/software/firmware/srcs/telemetry.cpp @@ -675,6 +678,7 @@ mod tests { previous_inspiratory_duration: None, battery_level: None, patient_height: None, + locale: None, }; // This needs to be consistent with sendMachineStateSnapshot() defined in makair-firmware/srcs/telemetry.cpp diff --git a/src/parsers/v2.rs b/src/parsers/v2.rs index 86502c0b13..efa9fb261e 100644 --- a/src/parsers/v2.rs +++ b/src/parsers/v2.rs @@ -3,6 +3,7 @@ use nom::IResult; use nom::{alt, do_parse, length_data, map, map_res, named, tag, take}; use std::convert::TryFrom; +use super::super::locale::Locale; use super::super::structures::*; const VERSION: u8 = 2; @@ -239,6 +240,8 @@ named!( >> current_alarm_codes: u8_array >> sep >> patient_height: be_u8 + >> sep + >> locale: be_u16 >> end >> ({ TelemetryMessage::StoppedMessage(StoppedMessage { @@ -288,6 +291,7 @@ named!( battery_level: Some(battery_level), current_alarm_codes: Some(current_alarm_codes), patient_height: Some(patient_height), + locale: Locale::try_from_u16(locale), }) }) ) @@ -435,6 +439,8 @@ named!( >> battery_level: be_u16 >> sep >> patient_height: be_u8 + >> sep + >> locale: be_u16 >> end >> (TelemetryMessage::MachineStateSnapshot(MachineStateSnapshot { telemetry_version: VERSION, @@ -490,6 +496,7 @@ named!( previous_inspiratory_duration: Some(previous_inspiratory_duration), battery_level: Some(battery_level), patient_height: Some(patient_height), + locale: Locale::try_from_u16(locale), })) ) ); @@ -888,6 +895,7 @@ mod tests { battery_level: Some(battery_level), current_alarm_codes: Some(current_alarm_codes), patient_height: Some(patient_height), + locale: Some(Locale::default()), }; // This needs to be consistent with sendStoppedMessage() defined in src/software/firmware/srcs/telemetry.cpp @@ -962,6 +970,8 @@ mod tests { &msg.current_alarm_codes.clone().unwrap_or_default(), b"\t", &msg.patient_height.unwrap_or_default().to_be_bytes(), + b"\t", + &msg.locale.unwrap_or_default().as_u16().to_be_bytes(), b"\n", ]); @@ -1130,6 +1140,7 @@ mod tests { previous_inspiratory_duration: Some(previous_inspiratory_duration), battery_level: Some(battery_level), patient_height: Some(patient_height), + locale: Some(Locale::default()), }; // This needs to be consistent with sendMachineStateSnapshot() defined in src/software/firmware/srcs/telemetry.cpp @@ -1218,6 +1229,8 @@ mod tests { &msg.battery_level.unwrap_or_default().to_be_bytes(), b"\t", &msg.patient_height.unwrap_or_default().to_be_bytes(), + b"\t", + &msg.locale.unwrap_or_default().as_u16().to_be_bytes(), b"\n", ]); diff --git a/src/structures.rs b/src/structures.rs index c3ebf3fc1f..9ceba5c3c5 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -8,6 +8,7 @@ use std::convert::TryFrom; use std::io; pub use crate::control::ControlSetting; +use crate::locale::Locale; /// Variants of the MakAir firmware #[derive(Debug, Clone, PartialEq, Eq)] @@ -388,6 +389,8 @@ pub struct StoppedMessage { pub current_alarm_codes: Option>, /// [protocol v2] Patient's height in centimeters pub patient_height: Option, + /// [protocol v2] Language of the system + pub locale: Option, } /// A telemetry message that is sent every time the firmware does a control iteration (every 10 ms) @@ -512,6 +515,8 @@ pub struct MachineStateSnapshot { pub battery_level: Option, /// [protocol v2] Patient's height in centimeters pub patient_height: Option, + /// [protocol v2] Language of the system + pub locale: Option, } /// A telemetry message that is sent every time an alarm is triggered or stopped