diff --git a/Cargo.lock b/Cargo.lock index e27d5ce..03ab4e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -50,6 +61,21 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dht-hal" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5423e6132fe67189d1aeac8a7412e7d09816a7374e22201a49a2d5081c8549" +dependencies = [ + "embedded-hal", +] + [[package]] name = "display-interface" version = "0.4.1" @@ -110,6 +136,18 @@ dependencies = [ "void", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "float-cmp" version = "0.8.0" @@ -129,6 +167,46 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gpio-cdev" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da59d440ef8c26208b3960ed58b71b896db7dfd6bb138622bba733b66d978c3a" +dependencies = [ + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + [[package]] name = "i2cdev" version = "0.4.4" @@ -152,9 +230,20 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.65" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] [[package]] name = "linux-embedded-hal" @@ -178,6 +267,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8197ad28c61566fb74b2167563865906215fbc32c61fd866bd54e7c195eb84ed" +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "micromath" version = "1.1.1" @@ -207,7 +302,7 @@ checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" dependencies = [ "bitflags", "cc", - "cfg-if", + "cfg-if 0.1.10", "libc", "void", ] @@ -221,16 +316,48 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "raspi-oled" version = "0.1.0" dependencies = [ + "dht-hal", "embedded-graphics", + "embedded-hal", + "gpio-cdev", + "libc", "linux-embedded-hal", "machine-ip", + "rusqlite", "ssd1306", ] +[[package]] +name = "rusqlite" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57adcf67c8faaf96f3248c2a7b419a0dbc52ebe36ba83dd57fe83827c1ea4eb3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "serial-core" version = "0.4.0" @@ -252,6 +379,12 @@ dependencies = [ "termios", ] +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "spidev" version = "0.4.1" @@ -301,6 +434,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.3" @@ -312,3 +451,9 @@ name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" diff --git a/Cargo.toml b/Cargo.toml index 50f2ce9..6b7bf5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,13 @@ edition = "2018" [dependencies] embedded-graphics = "0.7.1" linux-embedded-hal = "0.3.0" +embedded-hal = "0.2.5" machine-ip = "0.2.1" ssd1306 = "0.6.0" +libc = "0.2.98" +gpio-cdev = "0.4" +dht-hal = "0.0.1" +rusqlite = { version = "0.25.3", features = ["bundled"] } [profile.release] codegen-units = 1 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9a904ed --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,140 @@ +use std::{thread::sleep, time::{self, Duration}}; + +use gpio_cdev::{EventType, Line, LineRequestFlags}; + +fn read_events(line: &gpio_cdev::Line, timeout: std::time::Duration) -> Result, SensorError> { + let input = line.request( + LineRequestFlags::INPUT, + 0, + "read-data")?; + + let mut last_state = 1; + let start = time::Instant::now(); + + let mut events = Vec::with_capacity(81); + while start.elapsed() < timeout && events.len() < 81 { + let new_state = input.get_value()?; + if new_state != last_state { + let timestamp = start.elapsed(); + let event_type = if last_state < new_state { + EventType::RisingEdge + } else { + EventType::FallingEdge + }; + events.push((timestamp.as_micros() as u64, event_type)); + last_state = new_state; + } + } + if events.len() < 81 { + return Err(SensorError::Timeout); + } + Ok(events) +} + +fn events_to_data(events: Vec<(u64, EventType)>) -> Vec { + events[1..] + .windows(2) + .map(|pair| { + let prev = pair.get(0).unwrap(); + let next = pair.get(1).unwrap(); + match next.1 { + EventType::FallingEdge => Some(next.0 - prev.0), + EventType::RisingEdge => None, + } + }) + .filter(|&d| d.is_some()) + .map(|elapsed| { + if elapsed.unwrap() > 35 { 1 } else { 0 } + }).collect() +} + +const MAX_HUMIDITY: u16 = 1000; + +fn process_data(mut bits: &[u8]) -> Result<(u16, u16), SensorError> { + if bits[0] == 1 { + // definitely incorrect first bit + // (the humidity can't be this big..) + bits = &bits[1..]; + } + let bytes: Vec = bits + .chunks(8) + .map(|chunk| chunk.iter() + .enumerate() + // 8 bits, starting with the MSB + .map(|(bit_idx, &x)| x << (7 - bit_idx)) + .sum() + ).collect(); + let rh = (bytes[0] as u16) << 8 | bytes[1] as u16; + if rh > MAX_HUMIDITY { + return Err(SensorError::HumidityTooHigh); + } + let celsius = (bytes[2] as u16) << 8 | bytes[3] as u16; + + if bits.len() >= 40 { + let cksum: u8 = bits[32..40] + .iter() + .enumerate() + .map(|(idx, &x)| x << (7 - idx)) + .sum(); + let actual_sum = (bytes[0].wrapping_add(bytes[1]).wrapping_add(bytes[2]).wrapping_add(bytes[3])) & 0xff; + if actual_sum != cksum { + return Err(SensorError::ChecksumMismatch); + } + } + Ok((rh, celsius)) +} + +#[test] +fn test_process_data() { + let x = process_data(&[1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1]).unwrap(); + assert_eq!(471, x.0); + assert_eq!(268, x.1); +} + +#[derive(Debug)] +pub enum SensorError { + Io(gpio_cdev::Error), + ChecksumMismatch, + HumidityTooHigh, + Timeout +} + +impl From for SensorError { + fn from(error: gpio_cdev::Error) -> Self { + SensorError::Io(error) + } +} + +pub fn am2302_reading(line: &Line) -> Result<(u16, u16), SensorError> { + line.request(LineRequestFlags::OUTPUT, 1, "rust-am2302").unwrap(); + sleep(Duration::from_millis(500)); + set_max_priority(); + // set low for 20 ms + if let Err(e) = line.request(LineRequestFlags::OUTPUT, 0, "rust-am2302") { + set_normal_priority(); + return Err(SensorError::Io(e)); + } + sleep(Duration::from_millis(3)); + + let events = read_events(&line, Duration::from_secs(1)); + println!("{:?} {:?}", events, events.as_ref().map(|x| x.len())); + set_normal_priority(); + let events = events?; + let data = events_to_data(events); + process_data(&data) +} + +fn set_max_priority() { + unsafe { + let mut sched_para: libc::sched_param = std::mem::transmute([0u8; std::mem::size_of::()]); + sched_para.sched_priority = libc::sched_get_priority_max(libc::SCHED_FIFO); + libc::sched_setscheduler(0, libc::SCHED_FIFO, (&sched_para) as *const libc::sched_param); + } +} + +fn set_normal_priority() { + unsafe { + let sched_para: libc::sched_param = std::mem::transmute([0u8; std::mem::size_of::()]); + libc::sched_setscheduler(0, libc::SCHED_OTHER, (&sched_para) as *const libc::sched_param); + } +} diff --git a/src/main.rs b/src/main.rs index e16aadd..528b33d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ +use dht_hal::{Dht22, Reading}; use embedded_graphics::image::{Image, ImageRaw}; +use embedded_graphics::mono_font::iso_8859_7::FONT_9X18; use embedded_graphics::pixelcolor::BinaryColor; use embedded_graphics::prelude::*; use embedded_graphics::primitives::{Circle, Line, PrimitiveStyle, Rectangle}; @@ -7,17 +9,144 @@ use embedded_graphics::{ mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder}, text::Text, }; +use embedded_hal::digital::v2::{InputPin, OutputPin}; +use embedded_hal::prelude::_embedded_hal_blocking_i2c_Write; +use gpio_cdev::{Chip, EventRequestFlags, EventType, LineRequestFlags}; +use linux_embedded_hal::i2cdev::core::I2CDevice; +use linux_embedded_hal::i2cdev::linux::LinuxI2CError; +use rusqlite::{Connection, params}; use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306}; -use linux_embedded_hal::I2cdev; +use linux_embedded_hal::{Delay, I2cdev}; use machine_ip; +use std::intrinsics::transmute; +use std::{env, mem, time}; use std::thread::sleep; -use std::time::Duration; +use std::time::{Duration, SystemTime}; static IMG_DATA: &[u8; 512] = include_bytes!("../rust.raw"); -fn main() { - let i2c = I2cdev::new("/dev/i2c-1").unwrap(); +const CCS811_ADDR: u8 = 0x5A; // or 0x5B + +const CCS811_STATUS: u8 = 0x00; +const CCS811_MEAS_MODE: u8 = 0x01; +const CCS811_ALG_RESULT_DATA: u8 = 0x02; +const CCS811_RAW_DATA: u8 = 0x03; +const CCS811_ENV_DATA: u8 = 0x05; +const CCS811_NTC: u8 = 0x06; +const CCS811_THRESHOLDS: u8 = 0x10; +const CCS811_BASELINE: u8 = 0x11; +const CCS811_HW_ID: u8 = 0x20; +const CCS811_HW_VERSION: u8 = 0x21; +const CCS811_FW_BOOT_VERSION: u8 = 0x23; +const CCS811_FW_APP_VERSION: u8 = 0x24; +const CCS811_ERROR_ID: u8 = 0xE0; +const CCS811_APP_START: u8 = 0xF4; +const CCS811_SW_RESET: u8 = 0xFF; + +struct CCS811 { + i2c: I2cdev, + addr: u8 +} + +impl CCS811 { + fn new(mut i2c: I2cdev, addr: u8) -> Self { + i2c.set_slave_address(addr as u16).unwrap(); + Self { + i2c, + addr + } + } + + fn check_for_error(&mut self) -> Option { + let x = self.i2c.smbus_read_byte_data(CCS811_STATUS).unwrap(); + if (x & 1) != 0 { + let err_code = self.i2c.smbus_read_byte_data(CCS811_ERROR_ID).unwrap(); + Some(err_code) + } else { + None + } + } + + fn hardware_id(&mut self) -> u8 { + self.i2c.smbus_read_byte_data(CCS811_HW_ID).unwrap() + } + + fn app_valid(&mut self) -> bool { + let x = self.i2c.smbus_read_byte_data(CCS811_STATUS).unwrap(); + x & 1 << 4 != 0 + } + + fn set_drive_mode(&mut self, mode: CCS811DriveMode) -> Result<(), Option> { + self.i2c.smbus_write_byte(CCS811_APP_START).map_err(Some)?; + if let Some(x) = self.check_for_error() { + println!("error ignored {:b}", x); + } + let mut setting = self.i2c.smbus_read_byte_data(CCS811_MEAS_MODE).map_err(Some)?; + setting &= !(0b00000111 << 4); + setting |= (mode as u8) << 4; + self.i2c.smbus_write_byte_data(CCS811_MEAS_MODE, setting).map_err(Some)?; + Ok(()) + } + + fn get_baseline(&mut self) -> u16 { + let x = self.i2c.smbus_read_i2c_block_data(CCS811_BASELINE, 2).unwrap(); + ((x[0] as u16) << 8) | (x[1] as u16) + } + + /// Returns (eCO2, tVOC) + fn get_reading(&mut self) -> (u16, u16) { + let x = self.i2c.smbus_read_i2c_block_data(CCS811_ALG_RESULT_DATA, 4).unwrap(); + (((x[0] as u16) << 8) | (x[1] as u16), ((x[2] as u16) << 8) | (x[3] as u16)) + } +} + +enum CCS811DriveMode { + Idle = 0, + EverySecond = 1, + Every10Seconds = 2, + Every60Seconds = 3, + /// Note the English manual states this is calculated every 10 ms! + Every250Milliseconds = 4, +} + +fn main() { + let args = env::args().collect::>(); + if args.len() < 2 { + panic!("missing argument: database path"); + } + let database = Connection::open(&args[1]).expect("failed to open database"); + database.execute(" + CREATE TABLE IF NOT EXISTS sensor_readings( + time INTEGER PRIMARY KEY, + humidity INTEGER NOT NULL, + celsius INTEGER NOT NULL + )", []).unwrap(); + + /* + let mut ccs = CCS811::new(i2c, CCS811_ADDR); + println!("HW ID, should be 0x81 {:x}", ccs.hardware_id()); + println!("Error code, should be None: {:?}", ccs.check_for_error()); + println!("app valid = {:?}", ccs.app_valid()); + println!("baseline = {:x}", ccs.get_baseline()); + println!("reading {:?}", ccs.get_reading()); + println!("setting drive mode to 1: {:?}", ccs.set_drive_mode(CCS811DriveMode::EverySecond)); + */ + let mut chip = Chip::new("/dev/gpiochip0").unwrap(); + let line = chip.get_line(26).unwrap(); + for _attempt in 0..5 { + let time = std::time::SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + if let Ok((rh, temp)) = raspi_oled::am2302_reading(&line) { + database.execute("INSERT INTO sensor_readings (time, humidity, celsius) VALUES (?1, ?2, ?3)", params![time, rh, temp]).unwrap(); + display_on_ssd1306(rh, temp); + break; + } + } +} + + +fn display_on_ssd1306(rh: u16, temp: u16) { + let i2c = I2cdev::new("/dev/i2c-1").unwrap(); let interface = I2CDisplayInterface::new(i2c); let mut disp = Ssd1306::new( interface, @@ -26,53 +155,63 @@ fn main() { ).into_buffered_graphics_mode(); disp.init().unwrap(); - disp.flush().unwrap(); let text_style = MonoTextStyleBuilder::new() - .font(&FONT_6X10) + .font(&FONT_9X18) .text_color(BinaryColor::On) .build(); - - let text = "0123456789012345678901"; + + let text = format!("{}.{}% {}.{}°C", rh / 10, rh % 10, temp / 10, temp % 10); Text::new(&text, Point::new(0, 10), text_style) .draw(&mut disp) .unwrap(); disp.flush().unwrap(); + /* sleep(Duration::from_secs(2)); disp.clear(); + let base_y = 0.0; + let max_dy = 32.0; + let mut tick = 0; loop { - Line::new(Point::new(8, 16 + 16), Point::new(8 + 16, 16 + 16)) + let y = if tick % 32 < 16 { + base_y + (tick % 16) as f32 / 16.0 * max_dy + } else { + base_y + max_dy - (tick % 16) as f32 / 16.0 * max_dy + } as i32; + tick += 1; + Line::new(Point::new(8, y + 16), Point::new(8 + 16, y + 16)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut disp).unwrap(); - Line::new(Point::new(8, 16 + 16), Point::new(8 + 8, 16)) + Line::new(Point::new(8, y + 16), Point::new(8 + 8, y)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut disp).unwrap(); - Line::new(Point::new(8 + 16, 16 + 16), Point::new(8 + 8, 16)) + Line::new(Point::new(8 + 16, y + 16), Point::new(8 + 8, y)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut disp).unwrap(); - Rectangle::new(Point::new(48, 16), Size::new(16, 16)) + Rectangle::new(Point::new(48, y), Size::new(16, 16)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut disp).unwrap(); - Circle::new(Point::new(88, 16), 17) + Circle::new(Point::new(88, y), 16) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut disp).unwrap(); - let local_addr = machine_ip::get().unwrap(); - - Text::new(&format!("IP: {}", local_addr.to_string()), Point::new(0, 56), text_style) + /* + Text::new(&format!("Hello from frame {}", tick), Point::new(0, 56), text_style) .draw(&mut disp) .unwrap(); + */ disp.flush().unwrap(); - sleep(Duration::from_secs(2)); + sleep(Duration::from_millis(10)); disp.clear(); + /* let im: ImageRaw = ImageRaw::new(IMG_DATA, 64); let img = Image::new(&im, Point::new(32, 0)); img.draw(&mut disp).unwrap(); @@ -80,5 +219,35 @@ fn main() { sleep(Duration::from_secs(2)); disp.clear(); + */ + } + */ +} + +struct LineWrapper(gpio_cdev::Line); + +impl InputPin for LineWrapper { + type Error = gpio_cdev::Error; + + fn is_high(&self) -> Result { + let handle = self.0.request(LineRequestFlags::INPUT, 0, "rust-line-wrapper")?; + Ok(handle.get_value()? == 1) + } + + fn is_low(&self) -> Result { + let handle = self.0.request(LineRequestFlags::INPUT, 0, "rust-line-wrapper")?; + Ok(handle.get_value()? == 0) } } + +impl OutputPin for LineWrapper { + type Error = gpio_cdev::Error; + + fn set_low(&mut self) -> Result<(), Self::Error> { + self.0.request(LineRequestFlags::OUTPUT, 1, "rust-line-wrapper")?.set_value(0) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + self.0.request(LineRequestFlags::OUTPUT, 1, "rust-line-wrapper")?.set_value(1) + } +} \ No newline at end of file