diff --git a/src/cli/bin.rs b/src/cli/bin.rs index dd402f178b..813f15cab8 100644 --- a/src/cli/bin.rs +++ b/src/cli/bin.rs @@ -319,6 +319,8 @@ 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; + let mut nb_eol_test_snapshots: u32 = 0; loop { match rx.try_recv() { @@ -343,6 +345,12 @@ fn stats(cfg: Stats) { TelemetryMessage::ControlAck(_) => { nb_control_ack += 1; } + TelemetryMessage::FatalError(_) => { + nb_fatal_error += 1; + } + TelemetryMessage::EolTestSnapshot(_) => { + nb_eol_test_snapshots += 1; + } } telemetry_messages.push(message); } @@ -358,6 +366,8 @@ 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!("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 a7e92f3071..e64dfc07c5 100644 --- a/src/cli/convert.rs +++ b/src/cli/convert.rs @@ -162,6 +162,12 @@ 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 + } + 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/cli/statistics.rs b/src/cli/statistics.rs index db33f9f33f..0a497162fa 100644 --- a/src/cli/statistics.rs +++ b/src/cli/statistics.rs @@ -136,6 +136,8 @@ mod tests { inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, + locale: None, }, )]; @@ -180,6 +182,8 @@ mod tests { inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, + locale: None, })); assert_eq!(compute_duration(vect), 100); @@ -275,6 +279,8 @@ mod tests { inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, + locale: None, }, )); @@ -312,6 +318,8 @@ mod tests { inspiratory_duration_command: None, 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 ddcbfdaf38..ac97fd0b93 100644 --- a/src/control.rs +++ b/src/control.rs @@ -5,6 +5,8 @@ use std::ops::RangeInclusive; +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; @@ -71,6 +73,10 @@ pub enum ControlSetting { TargetInspiratoryFlow = 25, /// Duration of inspiration in ms (value bounds must be between 200 and 3000) InspiratoryDuration = 26, + /// 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, } impl ControlSetting { @@ -105,6 +111,8 @@ impl ControlSetting { Self::LeakAlarmThreshold => 200, Self::TargetInspiratoryFlow => 40, Self::InspiratoryDuration => 800, + Self::Locale => Locale::default().as_usize(), + Self::PatientHeight => 160, } } @@ -139,6 +147,8 @@ impl ControlSetting { Self::LeakAlarmThreshold => RangeInclusive::new(0, 10_000), Self::TargetInspiratoryFlow => RangeInclusive::new(5, 80), Self::InspiratoryDuration => RangeInclusive::new(200, 3_000), + Self::Locale => Locale::bounds(), + Self::PatientHeight => RangeInclusive::new(100, 250), } } } @@ -175,6 +185,8 @@ impl std::convert::TryFrom for ControlSetting { 24 => Ok(ControlSetting::LeakAlarmThreshold), 25 => Ok(ControlSetting::TargetInspiratoryFlow), 26 => Ok(ControlSetting::InspiratoryDuration), + 27 => Ok(ControlSetting::Locale), + 28 => Ok(ControlSetting::PatientHeight), _ => Err("Invalid setting number"), } } diff --git a/src/lib.rs b/src/lib.rs index 2a72697bb8..7923c3452f 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 @@ -279,6 +281,18 @@ pub fn display_message(message: TelemetryChannelType) { }))) => { info!("← {:?} = {}", &setting, &value); } + Ok(TelemetryMessageOrError::Message(TelemetryMessage::FatalError(FatalError { + error, + .. + }))) => { + 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/locale.rs b/src/locale.rs new file mode 100644 index 0000000000..8e7f25b0f3 --- /dev/null +++ b/src/locale.rs @@ -0,0 +1,117 @@ +// 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 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 + } + + /// Language code as a usize + pub fn as_usize(&self) -> usize { + self.0.into() + } + + /// Allowed value bounds (this is not really correct/useful) + pub fn bounds() -> RangeInclusive { + RangeInclusive::new( + Self::try_from("aa").unwrap().as_usize(), + Self::try_from("zz").unwrap().as_usize(), + ) + } +} + +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(Locale(w)) + } else { + Err("language code must be exactly 2 characters, according to ISO 639-1") + } + } +} + +impl Default for Locale { + fn default() -> Self { + Locale::try_from("en").unwrap() + } +} + +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); + f.write_str(&str) + } +} + +#[cfg(test)] +mod tests { + use super::Locale; + + 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 = Locale(code); + if ui_locale.to_string().is_ascii() { + Some(ui_locale) + } else { + None + } + }) + } + + #[test] + fn from_str_fr() { + assert_eq!(Locale::try_from("fr").map(|code| code.as_u16()), Ok(FR)); + } + + #[test] + fn from_str_empty() { + assert!(Locale::try_from("").is_err()) + } + + #[test] + fn from_str_too_long() { + assert!(Locale::try_from("fra").is_err()) + } + + #[test] + fn to_str() { + 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!(Locale::try_from(str.as_str()).map(|code| code.as_u16()), Ok(ui_locale.as_u16())) + } + } +} diff --git a/src/parsers/v1.rs b/src/parsers/v1.rs index b519959821..3a125eb78b 100644 --- a/src/parsers/v1.rs +++ b/src/parsers/v1.rs @@ -139,6 +139,8 @@ named!( inspiratory_duration_command: None, battery_level: None, current_alarm_codes: None, + patient_height: None, + locale: None, }) }) ) @@ -280,6 +282,8 @@ named!( inspiratory_duration_command: None, previous_inspiratory_duration: None, battery_level: None, + patient_height: None, + locale: None, })) ) ); @@ -522,6 +526,8 @@ mod tests { inspiratory_duration_command: None, 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 @@ -671,6 +677,8 @@ mod tests { inspiratory_duration_command: None, 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 cb924991e0..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; @@ -53,6 +54,84 @@ 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!( + 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!( @@ -159,6 +238,10 @@ named!( >> battery_level: be_u16 >> sep >> current_alarm_codes: u8_array + >> sep + >> patient_height: be_u8 + >> sep + >> locale: be_u16 >> end >> ({ TelemetryMessage::StoppedMessage(StoppedMessage { @@ -207,6 +290,8 @@ 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), + locale: Locale::try_from_u16(locale), }) }) ) @@ -352,6 +437,10 @@ named!( >> previous_inspiratory_duration: be_u16 >> sep >> battery_level: be_u16 + >> sep + >> patient_height: be_u8 + >> sep + >> locale: be_u16 >> end >> (TelemetryMessage::MachineStateSnapshot(MachineStateSnapshot { telemetry_version: VERSION, @@ -406,6 +495,8 @@ 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), + locale: Locale::try_from_u16(locale), })) ) ); @@ -497,6 +588,65 @@ 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, + })) + ) +); + +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. @@ -510,6 +660,8 @@ pub fn message(input: &[u8]) -> IResult<&[u8], TelemetryMessage, TelemetryError< machine_state_snapshot, alarm_trap, control_ack, + fatal_error, + eol_test_snapshot, ))(input) .map_err(nom::Err::convert) } @@ -571,6 +723,61 @@ 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 } + } + } + + 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( @@ -651,6 +858,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, @@ -686,6 +894,8 @@ 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), + locale: Some(Locale::default()), }; // This needs to be consistent with sendStoppedMessage() defined in src/software/firmware/srcs/telemetry.cpp @@ -758,6 +968,10 @@ 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"\t", + &msg.locale.unwrap_or_default().as_u16().to_be_bytes(), b"\n", ]); @@ -882,6 +1096,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, @@ -924,6 +1139,8 @@ 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), + locale: Some(Locale::default()), }; // This needs to be consistent with sendMachineStateSnapshot() defined in src/software/firmware/srcs/telemetry.cpp @@ -1010,6 +1227,10 @@ 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"\t", + &msg.locale.unwrap_or_default().as_u16().to_be_bytes(), b"\n", ]); @@ -1136,4 +1357,129 @@ 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))); + } + } + + 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 beb377f862..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)] @@ -175,6 +176,127 @@ 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 mmH2O + pressure: u16, + }, +} + +/// 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))] @@ -265,6 +387,10 @@ 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, + /// [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) @@ -387,6 +513,10 @@ 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, + /// [protocol v2] Language of the system + pub locale: Option, } /// A telemetry message that is sent every time an alarm is triggered or stopped @@ -445,6 +575,40 @@ 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, +} + +/// [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))] @@ -462,6 +626,10 @@ 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), + /// [protocol v2] A message sent during end of line tests + EolTestSnapshot(EolTestSnapshot), } impl TelemetryMessage { @@ -486,6 +654,12 @@ impl TelemetryMessage { Self::ControlAck(ControlAck { telemetry_version, .. }) => telemetry_version, + Self::FatalError(FatalError { + telemetry_version, .. + }) => telemetry_version, + Self::EolTestSnapshot(EolTestSnapshot { + telemetry_version, .. + }) => telemetry_version, }; *val } @@ -499,6 +673,8 @@ impl TelemetryMessage { Self::MachineStateSnapshot(MachineStateSnapshot { version, .. }) => version, Self::AlarmTrap(AlarmTrap { version, .. }) => version, Self::ControlAck(ControlAck { version, .. }) => version, + Self::FatalError(FatalError { version, .. }) => version, + Self::EolTestSnapshot(EolTestSnapshot { version, .. }) => version, }; val.clone() } @@ -512,6 +688,8 @@ 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, + Self::EolTestSnapshot(EolTestSnapshot { device_id, .. }) => device_id, }; val.clone() } @@ -525,6 +703,8 @@ impl TelemetryMessage { Self::MachineStateSnapshot(MachineStateSnapshot { systick, .. }) => systick, Self::AlarmTrap(AlarmTrap { systick, .. }) => systick, Self::ControlAck(ControlAck { systick, .. }) => systick, + Self::FatalError(FatalError { systick, .. }) => systick, + Self::EolTestSnapshot(EolTestSnapshot { systick, .. }) => systick, }; *val }