usb-bpm-exporter 0.1.4

USB Blood Pressure Monitor data extraction library and CLI tool
Documentation
use byteorder::ReadBytesExt;
use chrono::{NaiveDate, NaiveDateTime};
use serialport::{DataBits, FlowControl, Parity, SerialPort, StopBits};
use std::io::{Read, Write};
use std::thread::sleep;
use std::time::Duration;
use thiserror::Error;

const STX: u8 = 0x02;
const ETX: u8 = 0x03;
const ENQ: u8 = 0x05;
const DATA_BYTES: usize = 3000;

#[derive(Error, Debug)]
pub enum BpmError {
    #[error("Serial port error: {0}")]
    SerialPort(#[from] serialport::Error),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("CSV error: {0}")]
    Csv(#[from] csv::Error),
    #[error("UTF-8 conversion error: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),
    #[error("CSV writer error: {0}")]
    CsvWriter(#[from] csv::IntoInnerError<csv::Writer<Vec<u8>>>),
    #[error("Data parsing error: {0}")]
    Parse(String),
    #[error("Device communication error: {0}")]
    Communication(String),
    #[error("Insufficient data received: expected {expected}, got {actual}")]
    InsufficientData { expected: usize, actual: usize },
}

pub type Result<T> = std::result::Result<T, BpmError>;

/// Represents a USB Blood Pressure Monitor device
pub struct Device {
    device_path: String,
    serial_port: Box<dyn SerialPort>,
}

impl Device {
    /// Create a new device connection
    pub fn new(device_path: &str) -> Result<Device> {
        let s = serialport::new(device_path, 9600)
            .data_bits(DataBits::Eight)
            .parity(Parity::None)
            .stop_bits(StopBits::One)
            .flow_control(FlowControl::None)
            .timeout(Duration::from_secs(10));
        
        let port = s.open()?;
        
        Ok(Device {
            device_path: device_path.to_string(),
            serial_port: port,
        })
    }

    /// Get the number of observations for a specific user
    pub fn count(&mut self, user: u8) -> Result<u32> {
        let response = self.send_command(&format!("?MRN{}", user))?;
        
        if response.len() < 8 {
            return Err(BpmError::InsufficientData {
                expected: 8,
                actual: response.len(),
            });
        }
        
        let count_bytes = &response[5..8];
        let count_str = std::str::from_utf8(count_bytes)
            .map_err(|e| BpmError::Parse(format!("Invalid UTF-8 in count: {}", e)))?;
        let count = count_str.parse::<u32>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse count: {}", e)))?;
        
        Ok(count)
    }

    /// Get all observations for a specific user
    pub fn observations(&mut self, user: u8) -> Result<Vec<Observation>> {
        let expected_observation_count = self.count(user)?;
        let raw_response = self.send_command(&format!("?MDR{}A", user))?;

        if raw_response.len() < 5 {
            return Err(BpmError::InsufficientData {
                expected: 5,
                actual: raw_response.len(),
            });
        }

        let mut observations = vec![];
        let mut rdr = &raw_response[5..];
        
        for _ in 0..expected_observation_count {
            if rdr.len() < 20 {
                break;
            }
            match Observation::read(&mut rdr) {
                Ok(obs) => observations.push(obs),
                Err(e) => {
                    log::warn!("Error reading observation: {}", e);
                    break;
                }
            }
        }
        
        Ok(observations)
    }

    fn read_from_port(&mut self, length: usize) -> Result<Vec<u8>> {
        let mut buf = vec![0; length];
        let bytes_read = self.serial_port.read(&mut buf)?;
        Ok(buf[..bytes_read].to_vec())
    }

    fn send_command(&mut self, command: &str) -> Result<Vec<u8>> {
        // Send command wrapped in STX/ETX
        self.serial_port.write(&[STX])?;
        self.serial_port.write(command.as_bytes())?;
        self.serial_port.write(&[ETX])?;
        sleep(Duration::from_secs(1));

        // Read acknowledgement
        let ack_response = self.read_from_port(1)?;
        if ack_response.is_empty() {
            return Err(BpmError::Communication("No acknowledgement received".to_string()));
        }
        
        let acknowledgement = ack_response[0];
        
        // Send ENQ to request data
        self.serial_port.write(&[ENQ])?;
        sleep(Duration::from_millis(500));

        if acknowledgement == 6 {
            let response = self.read_from_port(DATA_BYTES)?;
            Ok(response)
        } else {
            Err(BpmError::Communication(format!(
                "Negative acknowledgement received: {}", 
                acknowledgement
            )))
        }
    }

    /// Get the device path
    pub fn device_path(&self) -> &str {
        &self.device_path
    }
}

/// Represents a blood pressure observation/measurement
#[derive(Debug, Clone, PartialEq)]
pub struct Observation {
    pub year: u16,
    pub month: u8,
    pub day: u8,
    pub hour: u8,
    pub minute: u8,
    pub regular_heart_beat: u8,
    pub systolic: u16,
    pub diastolic: u16,
    pub pulse: u16,
    pub body_movement: u8,
    pub incorrect_cuff_wrapping: u8,
    pub unsuitable_temperature: u8,
    pub usable: u8,
}

impl Observation {
    /// Read an observation from binary data
    pub fn read<R: Read>(rdr: &mut R) -> Result<Observation> {
        let mut buf = [0; 2];
    
        rdr.read_exact(&mut buf)?;
        let year = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse year: {}", e)))?
            .parse::<u16>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse year: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let month = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse month: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse month: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let day = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse day: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse day: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let hour = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse hour: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse hour: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let minute = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse minute: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse minute: {}", e)))?;
    
        let mut buf = [0; 1];
        rdr.read_exact(&mut buf)?;
        let regular_heart_beat = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse regular_heart_beat: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse regular_heart_beat: {}", e)))?;
    
        let mut buf = [0; 3];
        rdr.read_exact(&mut buf)?;
        let systolic = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse systolic: {}", e)))?
            .parse::<u16>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse systolic: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let diastolic = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse diastolic: {}", e)))?
            .parse::<u16>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse diastolic: {}", e)))?;
    
        rdr.read_exact(&mut buf)?;
        let pulse = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse pulse: {}", e)))?
            .parse::<u16>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse pulse: {}", e)))?;
    
        rdr.read_exact(&mut buf[..1])?;
        let body_movement = std::str::from_utf8(&buf[..1])
            .map_err(|e| BpmError::Parse(format!("Failed to parse body_movement: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse body_movement: {}", e)))?;
    
        rdr.read_exact(&mut buf[..1])?;
        let incorrect_cuff_wrapping = std::str::from_utf8(&buf[..1])
            .map_err(|e| BpmError::Parse(format!("Failed to parse incorrect_cuff_wrapping: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse incorrect_cuff_wrapping: {}", e)))?;
    
        rdr.read_exact(&mut buf[..1])?;
        let unsuitable_temperature = std::str::from_utf8(&buf[..1])
            .map_err(|e| BpmError::Parse(format!("Failed to parse unsuitable_temperature: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse unsuitable_temperature: {}", e)))?;
    
        let _ = rdr.read_u8()?; // Skip padding byte
        
        let mut buf = [0; 1];
        rdr.read_exact(&mut buf)?;
        let usable = std::str::from_utf8(&buf)
            .map_err(|e| BpmError::Parse(format!("Failed to parse usable: {}", e)))?
            .parse::<u8>()
            .map_err(|e| BpmError::Parse(format!("Failed to parse usable: {}", e)))?;
    
        Ok(Observation {
            year,
            month,
            day,
            hour,
            minute,
            regular_heart_beat,
            systolic,
            diastolic,
            pulse,
            body_movement,
            incorrect_cuff_wrapping,
            unsuitable_temperature,
            usable,
        })
    }

    /// Get the date of this observation
    pub fn date(&self) -> Option<NaiveDate> {
        if self.year == 0 || self.month == 0 || self.day == 0 {
            None
        } else {
            NaiveDate::from_ymd_opt(self.year as i32 + 2000, self.month as u32, self.day as u32)
        }
    }

    /// Get the datetime of this observation
    pub fn datetime(&self) -> Option<NaiveDateTime> {
        if let Some(date) = self.date() {
            date.and_hms_opt(self.hour as u32, self.minute as u32, 0)
        } else {
            None
        }
    }

    /// Check if this observation is marked as usable
    pub fn is_usable(&self) -> bool {
        self.usable == 1
    }

    /// Check if there was body movement during measurement
    pub fn has_body_movement(&self) -> bool {
        self.body_movement == 1
    }

    /// Check if cuff wrapping was incorrect
    pub fn has_incorrect_cuff_wrapping(&self) -> bool {
        self.incorrect_cuff_wrapping == 1
    }

    /// Check if temperature was unsuitable
    pub fn has_unsuitable_temperature(&self) -> bool {
        self.unsuitable_temperature == 1
    }

    /// Check if heart beat was regular
    pub fn has_regular_heart_beat(&self) -> bool {
        self.regular_heart_beat == 1
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{Datelike, Timelike};

    #[test]
    fn test_observation_date() {
        let obs = Observation {
            year: 23,
            month: 12,
            day: 25,
            hour: 14,
            minute: 30,
            regular_heart_beat: 1,
            systolic: 120,
            diastolic: 80,
            pulse: 72,
            body_movement: 0,
            incorrect_cuff_wrapping: 0,
            unsuitable_temperature: 0,
            usable: 1,
        };

        let date = obs.date().unwrap();
        assert_eq!(date.year(), 2023);
        assert_eq!(date.month(), 12);
        assert_eq!(date.day(), 25);

        let datetime = obs.datetime().unwrap();
        assert_eq!(datetime.hour(), 14);
        assert_eq!(datetime.minute(), 30);
    }

    #[test]
    fn test_observation_flags() {
        let obs = Observation {
            year: 23,
            month: 1,
            day: 1,
            hour: 0,
            minute: 0,
            regular_heart_beat: 1,
            systolic: 120,
            diastolic: 80,
            pulse: 72,
            body_movement: 1,
            incorrect_cuff_wrapping: 0,
            unsuitable_temperature: 1,
            usable: 1,
        };

        assert!(obs.is_usable());
        assert!(obs.has_regular_heart_beat());
        assert!(obs.has_body_movement());
        assert!(!obs.has_incorrect_cuff_wrapping());
        assert!(obs.has_unsuitable_temperature());
    }
}