1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Copyright 2020 Brian J. Tarricone <brian@tarricone.org>
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! `sen0177` is a Rust library/crate that reads air quality data from the
//! SEN0177 air quality sensor.
//! 
//! ## Prerequisites
//! 
//! * You've connected the sensor to a UART on a Linux-based device, and
//!   that UART is enabled and available from the kernel as a TTY device
//!   node.
//! 
//! ## Installation
//! 
//! Include the following in your `Cargo.toml` file:
//! 
//! ```toml
//! [dependencies]
//! sen0177 = "0.1"
//! ```
//! 
//! ## Usage
//! 
//! ```rust,no_run
//! use sen0177::{Reading, Sen0177};
//! 
//! const SERIAL_PORT: &str = "/dev/ttyS0";
//! 
//! let mut sensor = Sen0177::open(SERIAL_PORT).expect("Failed to open device");
//! let reading = sensor.read().expect("Failed to read sensor data");
//! println!("PM1: {}µg/m³, PM2.5: {}µg/m³, PM10: {}µg/m³",
//!          reading.pm1(), reading.pm2_5(), reading.pm10());
//! ```
//! 
//! Note that the serial device occasionally returns bad data.  If you
//! receive [`Sen0177Error::InvalidData`] or [`Sen0177Error::ChecksumMismatch`]
//! from the [`Sen0177::read`] call, a second try will usually succeed.
//! 
//! ## Gotchas
//! 
//! ### Raspberry Pi
//! 
//! If you're using this with a Raspberry Pi, note that by default the
//! primary GPIO pins are set up as a Linux serial console.  You will need
//! to disable that (by editing `/boot/cmdline.txt`) before this will work.
//! Instead of using a specifiy TTY device node, you should use
//! `/dev/serial0`, which is a symlink to the proper device.
//! 
//! Alternatively, you can use the second UART, but you'll need to load an
//! overlay to assign it to GPIO pins.  See [UART
//! configuration](https://www.raspberrypi.org/documentation/configuration/uart.md)
//! and the [UART-related
//! overlays](https://www.raspberrypi.org/documentation/configuration/uart.md)
//! for more information.

use serial::{SystemPort, open as serial_open};
use serial_core::{BaudRate, CharSize, Error as SerialError, ErrorKind as SerialErrorKind, FlowControl, Parity, Result as SerialResult, SerialPort, StopBits};
use std::error::Error;
use std::fmt;
use std::io;
use std::io::Read;
use std::path::Path;
use std::time::Duration;

const BAUD_RATE: BaudRate = BaudRate::Baud9600;
const CHAR_SIZE: CharSize = CharSize::Bits8;
const PARITY: Parity = Parity::ParityNone;
const STOP_BITS: StopBits = StopBits::Stop1;
const FLOW_CONTROL: FlowControl = FlowControl::FlowNone;

const MAGIC_BYTE_0: u8 = 0x42;
const MAGIC_BYTE_1: u8 = 0x4d;
const PAYLOAD_LEN: usize = 30;  // 32 - magic bytes

/// A single air quality sensor reading
#[derive(Debug, Clone, Copy)]
pub struct Reading {
    pm1: f64,
    pm2_5: f64,
    pm10: f64,
}

impl Reading {
    /// Returns the PM1 concentration in µg/m³
    pub fn pm1(&self) -> f64 {
        self.pm1
    }

    /// Returns the PM2.5 concentration in µg/m³
    pub fn pm2_5(&self) -> f64 {
        self.pm2_5
    }

    /// Returns the PM10 concentration in µg/m³
    pub fn pm10(&self) -> f64 {
        self.pm10
    }
}

/// Describes errors returned by the SEN0177 sensor
#[derive(Debug)]
pub enum Sen0177Error {
    /// Device not found on the specified port
    DeviceNotFound,
    /// Device is in use or does not support required port configuration parameters
    DeviceUnavailable,
    /// Device returned invalid data
    InvalidData(String),
    /// The checksum provided in the sensor data did not match the checksum of the data itself
    ///
    /// Retrying the read will usually clear up the problem.
    ChecksumMismatch,
    /// An IO error occurred when communicating with the serial port
    IoError(io::Error),

}

impl fmt::Display for Sen0177Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use Sen0177Error::*;
        match self {
            DeviceNotFound => write!(f, "Device not found"),
            DeviceUnavailable => write!(f, "Device unavailable or does not support required parameters"),
            InvalidData(reason) => write!(f, "Invalid data: {}", reason),
            ChecksumMismatch => write!(f, "Data read was corrupt"),
            IoError(err) => write!(f, "IO error: {}", err),
        }
    }
}

impl Error for Sen0177Error {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Sen0177Error::IoError(ioerr) => Some(ioerr),
            _ => None,
        }
    }
}

impl From<SerialError> for Sen0177Error {
    fn from(err: SerialError) -> Self {
        use Sen0177Error::*;
        match err.kind() {
            SerialErrorKind::NoDevice => DeviceNotFound,
            SerialErrorKind::InvalidInput => DeviceUnavailable,
            SerialErrorKind::Io(kind) => IoError(io::Error::new(kind, format!("{:?}", kind))),
        }
    }
}

impl From<io::Error> for Sen0177Error {
    fn from(err: io::Error) -> Self {
        Sen0177Error::IoError(err)
    }
}

/// An instance of the SEN0177 air quality sensor
pub struct Sen0177 {
    serial_port: SystemPort,
}

impl Sen0177 {
    /// Opens the sensor on the specified port
    ///
    /// # Arguments
    ///
    /// * `serial_dev` - The serial device node the sensor is connected to
    pub fn open<P: AsRef<Path>>(serial_dev: P) -> Result<Sen0177, Sen0177Error> {
        let mut serial_port = serial_open(serial_dev.as_ref())?;
        Sen0177::configure_serial_port(&mut serial_port)?;
        Ok(Sen0177 {
            serial_port,
        })
    }

    /// Reads a single sensor measurement
    ///
    /// Note that this function will block until sufficient data is available.
    pub fn read(&mut self) -> Result<Reading, Sen0177Error> {
        let mut attempts_left = 10;
        let mut buf: [u8; 1] = [0; 1];
        while buf[0] != MAGIC_BYTE_1 && attempts_left > 0 && find_byte(&mut self.serial_port, MAGIC_BYTE_0, PAYLOAD_LEN as u32 * 4)? {
            self.serial_port.read_exact(&mut buf)?;
            attempts_left -= 1;
        }

        if buf[0] == MAGIC_BYTE_1 {
            let mut buf: [u8; PAYLOAD_LEN] = [0; PAYLOAD_LEN];
            self.serial_port.read_exact(&mut buf)?;
            validate_checksum(&buf)?;

            Ok(Reading {
                pm1: (((buf[2] as u16) << 8) | (buf[3] as u16)) as f64,
                pm2_5: (((buf[4] as u16) << 8) | (buf[5] as u16)) as f64,
                pm10: (((buf[6] as u16) << 8) | (buf[7] as u16)) as f64,
            })
        } else {
            Err(Sen0177Error::InvalidData(format!("Unable to find start magic 0x{:2x}{:2x}", MAGIC_BYTE_0, MAGIC_BYTE_1)))
        }
    }

    fn configure_serial_port(serial_port: &mut SystemPort) -> SerialResult<()> {
        serial_port.set_timeout(Duration::from_millis(1500))?;
        serial_port.reconfigure(&|settings| {
            settings.set_char_size(CHAR_SIZE);
            settings.set_parity(PARITY);
            settings.set_stop_bits(STOP_BITS);
            settings.set_flow_control(FLOW_CONTROL);
            settings.set_baud_rate(BAUD_RATE)
        })
    }
}

fn find_byte(serial_port: &mut SystemPort, byte: u8, attempts: u32) -> io::Result<bool> {
    let mut attempts_left = attempts;
    let mut buf: [u8; 1] = [0; 1];
    while buf[0] != byte && attempts_left > 0 {
        serial_port.read_exact(&mut buf)?;
        attempts_left -= 1;
    }
    Ok(buf[0] == byte)
}

fn validate_checksum(buf: &[u8; PAYLOAD_LEN]) -> Result<(), Sen0177Error> {
    let init: u16 = (MAGIC_BYTE_0 as u16) + (MAGIC_BYTE_1 as u16);
    let sum = buf[0..PAYLOAD_LEN-2].iter().fold(init, |accum, next| accum + *next as u16);
    let expected_sum: u16 = ((buf[PAYLOAD_LEN-2] as u16) << 8) | (buf[PAYLOAD_LEN-1] as u16);
    if expected_sum == sum {
        Ok(())
    } else {
        Err(Sen0177Error::ChecksumMismatch)
    }
}