Compare commits

..

No commits in common. "9cc84258eb3cd4ed9ea8328f1aa53130a154b6e5" and "adfc7925c70917d89eb943f4b624fc1a3101579b" have entirely different histories.

19 changed files with 2130 additions and 400 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
/target
**/*.rs.bk
events_weekly.json
events.json
sensors.db

1129
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package]
name = "raspi-oled"
version = "0.1.0"
authors = ["chux0519 <chuxdesign@hotmail.com>"]
authors = ["FliegendeWurst <2012gdwu@posteo.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,12 +10,19 @@ edition = "2018"
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 = "0.25.3"
time = { version = "0.3.9", features = ["parsing"] }
time-tz = "1.0.1"
image = "0.24.1"
serde_json = "1.0.79"
serde_derive = "1.0.136"
serde = "1.0.136"
rppal = { version = "0.13.1", features = ["hal"] }
ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust" }
display-interface-spi = "0.4.1"
ureq = { version = "2.4.0", default-features = false }
[profile.release]
codegen-units = 1

42
LICENSE
View File

@ -1,29 +1,19 @@
BSD 3-Clause License
Copyright (c) 2022 Arne Keller
Copyright (c) 2020, Yongsheng Xu
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,17 +1,24 @@
# raspi demo for oled ssd1306 display
# raspi demo for OLED ssd1351 display
https://www.waveshare.com/wiki/1.5inch_RGB_OLED_Module
## Quick start
```bash
> nix-shell
>
> rustup target add arm-unknown-linux-musleabihf
>
> cargo build --release --target arm-unknown-linux-musleabihf
Then scp the release file to your raspi.
> scp target/arm-unknown-linux-musleabihf/release/{display_all,display_off,refresh_json,take_measurement} 'pi@raspberrypi:~'
> # on the Pi, create sensors.db and events.json
> patchelf --set-interpreter /lib/ld-linux-armhf.so.3 display_all
> ./display_off on
> ./display_all sensors.db events.json temps
```
## Example
![picture](./images/01.jpg)
![temperature graph](./images/temps.png)
![primitive](./images/02.jpg)
![events](./images/events.png)
(the second blue text is brighter on the OLED)

16
events_weekly.json Normal file
View File

@ -0,0 +1,16 @@
"weekly": [
{
"name": "Example 1",
"day": 0,
"hour": 9,
"minute": 45,
"duration": 90
},
{
"name": "Example 2",
"day": 3,
"hour": 11,
"minute": 15,
"duration": 45
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

BIN
images/events.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/temps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

BIN
rust.raw

Binary file not shown.

3
rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
hard_tabs = true
match_block_trailing_comma = true
max_width = 120

392
src/bin/display_all.rs Normal file
View File

@ -0,0 +1,392 @@
use std::{fs, ops::Sub, time::Duration, fmt::Debug};
use display_interface_spi::SPIInterfaceNoCS;
use embedded_graphics::{
draw_target::DrawTarget,
mono_font::{
ascii::{FONT_10X20, FONT_5X8, FONT_6X9, FONT_9X15},
MonoTextStyleBuilder,
},
pixelcolor::{Rgb565},
prelude::{Point, Primitive},
primitives::{PrimitiveStyleBuilder, Rectangle},
text::{Text, renderer::CharacterStyle},
Drawable,
};
use raspi_oled::FrameOutput;
use rppal::{
gpio::Gpio,
spi::{Bus, Mode, SlaveSelect, Spi},
};
use rusqlite::Connection;
use serde_derive::Deserialize;
//use ssd1306::{I2CDisplayInterface, Ssd1306, size::DisplaySize128x64, rotation::DisplayRotation, mode::DisplayConfig};
use time::{format_description, OffsetDateTime, PrimitiveDateTime};
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt, PrimitiveDateTimeExt};
#[derive(Deserialize)]
struct Events {
events: Vec<Event>,
weekly: Vec<Weekly>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Event {
name: String,
start_time: String,
end_time: Option<String>
}
#[derive(Deserialize)]
struct Weekly {
name: String,
day: i32,
hour: i32,
minute: i32,
duration: i32,
}
fn main() {
let args = std::env::args().collect::<Vec<_>>();
if args.len() < 4 {
panic!("missing argument: database path, event JSON data file, events / temps");
}
let database = Connection::open(&args[1]).expect("failed to open database");
let events = fs::read_to_string(&args[2]).expect("failed to read events.json");
let events: Events = serde_json::from_str(&events).unwrap();
let (rh, temp): (i64, i64) = database
.query_row(
"SELECT humidity, celsius FROM sensor_readings ORDER BY sensor_readings.time DESC LIMIT 1",
[],
|row| Ok((row.get(0).unwrap(), row.get(1).unwrap())),
)
.unwrap();
let time = OffsetDateTime::now_utc().to_timezone(BERLIN);
let mut query = database
.prepare("SELECT celsius FROM sensor_readings ORDER BY sensor_readings.time DESC LIMIT 288")
.unwrap();
let mut temps: Vec<i32> = query
.query_map([], |r| Ok(r.get(0)))
.unwrap()
.map(Result::unwrap)
.map(Result::unwrap)
.collect();
let mut global_min = 1000;
let mut global_max = 0;
let mut vals: Vec<(i32, i32)> = vec![];
for hour in temps.chunks_mut(6) {
hour.sort();
let mut min = hour[1];
let mut max = hour[hour.len() - 2];
//println!("min {} max {}", min, max);
// sanity check value
if max > 300 {
if vals.is_empty() {
max = min;
} else {
max = vals.last().unwrap().1;
}
min = min.min(max);
}
global_min = min.min(global_min);
global_max = max.max(global_max);
vals.push((min, max));
}
//println!("global {} | {}", global_min, global_max);
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 19660800, Mode::Mode0).unwrap();
let gpio = Gpio::new().unwrap();
let dc = gpio.get(25).unwrap().into_output();
let spii = SPIInterfaceNoCS::new(spi, dc);
let disp = ssd1351::display::display::Ssd1351::new(spii);
//let mut disp = FrameOutput::new(128, 128);
let mut disp = draw(disp, time, rh, temp, events, &args, global_min, global_max, vals);
let _ = disp.flush();
//disp.buffer.save("/tmp/x.png");
}
fn draw<D: DrawTarget<Color = Rgb565>>(mut disp: D, time: OffsetDateTime, rh: i64, temp: i64, events: Events, args: &[String], global_min: i32, global_max: i32, mut vals: Vec<(i32, i32)>) -> D where <D as DrawTarget>::Error: Debug {
let hour = time.hour();
let minute = time.minute();
let text_style_clock = MonoTextStyleBuilder::new()
.font(&FONT_10X20)
.text_color(Rgb565::new(0xff, 0xff, 0xff))
.build();
let text_style2 = MonoTextStyleBuilder::new()
.font(&FONT_9X15)
.text_color(Rgb565::new(0xff, 0xff, 0xff))
.build();
let mut text_style_6x9 = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(Rgb565::new(0xff, 0xff, 0xff))
.build();
let text_style4 = MonoTextStyleBuilder::new()
.font(&FONT_5X8)
.text_color(Rgb565::new(0xff, 0xff, 0xff))
.build();
let rect_style = PrimitiveStyleBuilder::new()
.fill_color(Rgb565::new(0xff, 0xff, 0xff))
.build();
//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();
let hour = format!("{:02}", hour);
Text::new(&hour, Point::new(64 + 10, 6 + 20), text_style_clock)
.draw(&mut disp)
.unwrap();
let minute = format!("{:02}", minute);
Text::new(&minute, Point::new(64 + 10 + 20 + 4, 6 + 20), text_style_clock)
.draw(&mut disp)
.unwrap();
Rectangle::new((95, 17).into(), (2, 2).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
Rectangle::new((95, 22).into(), (2, 2).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
let rh = format!("{:02}", rh / 10);
Text::new(&rh, Point::new(64 + 3, 64 - 4), text_style2)
.draw(&mut disp)
.unwrap();
Text::new("%", Point::new(64 + 3 + 18, 64 - 4), text_style_6x9)
.draw(&mut disp)
.unwrap();
let temp_int = format!("{:02}", temp / 10);
Text::new(&temp_int, Point::new(64 + 32 + 3, 64 - 4), text_style2)
.draw(&mut disp)
.unwrap();
Rectangle::new((64 + 32 + 3 + 18, 64 - 4).into(), (1, 1).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
let temp_fract = format!("{}", temp % 10);
Text::new(&temp_fract, Point::new(64 + 32 + 3 + 18 + 2, 64 - 4), text_style_6x9)
.draw(&mut disp)
.unwrap();
for (x, y) in [
(118, 49),
(119, 49),
(117, 50),
(117, 51),
(120, 50),
(120, 51),
(118, 52),
(119, 52),
(122, 50),
(122, 51),
(122, 52),
(123, 49),
(124, 49),
(123, 53),
(124, 53),
] {
Rectangle::new((x, y).into(), (1, 1).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
}
let x = 0;
let y = 0;
Rectangle::new((x + 2, y + 8).into(), (1, 24).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
Rectangle::new((x + 1, y + 16).into(), (1, 1).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
Rectangle::new((x + 1, y + 28).into(), (1, 1).into())
.into_styled(rect_style)
.draw(&mut disp)
.unwrap();
let mut day = time.weekday().number_days_from_monday() as usize;
let days = [
("M", "o"),
("D", "i"),
("M", "i"),
("D", "o"),
("F", "r"),
("S", "a"),
("S", "o"),
];
for i in 0..5 {
Text::new(days[day].0, (x + 12 * i + 4, y + 6).into(), text_style_6x9)
.draw(&mut disp)
.unwrap();
Text::new(days[day].1, (x + 12 * i + 10, y + 6).into(), text_style4)
.draw(&mut disp)
.unwrap();
day += 1;
day %= days.len();
}
let mut bits = vec![];
// events
let mut all_events = vec![];
for event in events.weekly {
let mut event_time = time.clone();
while event_time.weekday().number_days_from_monday() as i32 != event.day {
event_time += Duration::from_secs(24 * 60 * 60);
}
all_events.push((event.day, event.hour, event.minute, event.duration, event.name, event_time.to_julian_day()));
}
let format = format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]").unwrap();
for event in events.events {
let dt = PrimitiveDateTime::parse(&event.start_time, &format)
.unwrap()
.assume_timezone(BERLIN)
.unwrap();
let julian_day = dt.to_julian_day();
if dt < time {
continue;
}
let duration = if let Some(end_time) = event.end_time.as_ref() {
let dt2 = PrimitiveDateTime::parse(end_time, &format)
.unwrap()
.assume_timezone(BERLIN)
.unwrap();
(dt2.sub(dt).as_seconds_f32() / 60.0) as i32
} else {
30
};
all_events.push((
dt.weekday().number_days_from_monday() as _,
dt.hour() as _,
dt.minute() as _,
duration,
event.name,
julian_day,
));
}
let today = time.date().to_julian_day();
let weekday = time.weekday().number_days_from_monday() as i32;
all_events.sort_by_key(|x| (x.5, ((x.0 + 7) - weekday) % 7, x.1, x.2));
println!("{:?}", all_events);
let mut time_until_first = None;
let colors = vec![
Rgb565::new(0xff >> 3, 0xff >> 2, 0x00 >> 3),
Rgb565::new(0xff >> 3, 0x00 >> 2, 0xff >> 3),
Rgb565::new(0x00 >> 3, 0xff >> 2, 0xff >> 3),
Rgb565::new(0xff >> 3, 0x00 >> 2, 0x00 >> 3),
Rgb565::new(0x00 >> 3, 0xff >> 2, 0x00 >> 3),
Rgb565::new(0x00 >> 3, 0x00 >> 2, 0xff >> 3),
Rgb565::new(0xff >> 3, 0xff >> 2, 0xff >> 3),
];
for i in 0..5 {
let day = (weekday + i) % 7;
for hour in 0..24 {
for minute in 0..60 {
if minute % 6 != 0 {
continue;
}
if i == 0 && hour == time.hour() as i32 && minute == (time.minute() as i32 / 6) * 6 {
bits.push((i, hour, minute / 6, Some(Rgb565::new(0xff, 0x00, 0xff))));
}
for (event_idx, event) in all_events.iter().enumerate() {
if event.0 != day || event.5 < today || event.5 - today > 4 {
continue;
}
let event_start = event.1 * 60 + event.2;
let event_end = event_start + event.3;
let now = hour * 60 + minute;
let now2 = hour * 60 + minute + 6;
if now2 > event_start && now < event_end {
bits.push((i, hour, minute / 6, colors.get(event_idx).copied()));
}
if time_until_first.is_none() && (i > 0 || event.1 > time.hour() as i32 || (event.1 == time.hour() as i32 && event.2 >= time.minute() as i32)) {
time_until_first = Some(
((i * 24 + event.1) * 60 + event.2) * 60
- (time.hour() as i32 * 60 + time.minute() as i32) * 60,
);
}
}
}
}
}
for (d, h, m, color) in bits {
// calculate position
let x = x + 4 + d * 12 + m;
let y = y + 8 + h;
disp.fill_solid(&Rectangle::new((x, y).into(), (1, 1).into()), color.unwrap_or(Rgb565::new(0xff, 0xff, 0x10)))
.unwrap();
//Rectangle::new((x, y).into(), (1, 1).into()).into_styled(rect_style).draw(&mut disp).unwrap();
}
if args[3] == "events" {
for (i, event) in all_events.iter().take(7).enumerate() {
let text = if event.4.len() > 19 { &event.4[0..19] } else { &event.4 };
let day = event.0 as usize;
let y = y + 64 + 9 * i as i32 + 5;
text_style_6x9.set_text_color(Some(Rgb565::new(0xff, 0xff, 0xff)));
Text::new(days[day].0, (x, y).into(), text_style_6x9)
.draw(&mut disp)
.unwrap();
Text::new(days[day].1, (x + 6, y).into(), text_style4)
.draw(&mut disp)
.unwrap();
text_style_6x9.set_text_color(Some(colors[i]));
Text::new(text, (x + 14, y).into(), text_style_6x9)
.draw(&mut disp)
.unwrap();
}
} else {
let diff = global_max - global_min;
let x = 0;
let y = 64;
let scaley = 63;
let scalex = 2;
vals.reverse();
for (i, (a, b)) in vals.into_iter().enumerate() {
let x = x + i as i32 * scalex;
let y1 = y + (global_max - b) * scaley / diff;
let y2 = y + (global_max - a) * scaley / diff;
let height = y2 - y1 + 1;
let rect = Rectangle::new((x, y1).into(), (scalex as u32, height as u32).into());
disp.fill_solid(&rect, Rgb565::new(0xff, 0xff, 0xff)).unwrap();
}
Text::new(
&format!("{}", global_max as f32 / 10.0),
(100, 64 + 10).into(),
text_style_6x9,
)
.draw(&mut disp)
.unwrap();
Text::new(
&format!("{}", global_min as f32 / 10.0),
(100, 64 + 50).into(),
text_style_6x9,
)
.draw(&mut disp)
.unwrap();
}
if let Some(secs) = time_until_first {
let days = secs / (24 * 60 * 60);
let hours = secs / (60 * 60) % 24;
let minutes = secs / 60 % 60;
let text = if days > 0 {
String::new()
} else if hours > 0 {
format!("{}h{}m", hours, minutes)
} else if minutes > 0 {
format!("{}m", minutes)
} else {
"?".into()
};
Text::new(&text, (x + 2, y + 60).into(), text_style2)
.draw(&mut disp)
.unwrap();
}
disp
}

33
src/bin/display_off.rs Normal file
View File

@ -0,0 +1,33 @@
use display_interface_spi::SPIInterfaceNoCS;
use rppal::{
gpio::Gpio,
hal::Delay,
spi::{Bus, Mode, SlaveSelect, Spi},
};
fn main() {
let args = std::env::args().collect::<Vec<_>>();
if args.len() < 2 {
panic!("missing argument: on/off");
}
display_on_ssd1306(args[1] == "on");
}
fn display_on_ssd1306(on: bool) {
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 19660800, Mode::Mode0).unwrap();
let gpio = Gpio::new().unwrap();
let dc = gpio.get(25).unwrap().into_output();
let mut rst = gpio.get(27).unwrap().into_output();
// Init SPI
let spii = SPIInterfaceNoCS::new(spi, dc);
let mut disp = ssd1351::display::display::Ssd1351::new(spii);
// Reset & init
disp.reset(&mut rst, &mut Delay).unwrap();
if on {
disp.turn_on().unwrap();
} else {
disp.turn_off().unwrap();
}
}

22
src/bin/refresh_json.rs Normal file
View File

@ -0,0 +1,22 @@
use std::{error::Error, fs};
static WEEKLY: &'static str = include_str!("../../events_weekly.json");
fn main() {
let url = "http://nixos.fritz.box:12783/custom/event_alerts";
if let Ok(json) = get_json(url) {
let mut buf = String::new();
buf += "{";
buf += "\"events\": ";
buf += &json;
buf += ",";
buf += WEEKLY;
buf += "}";
fs::write("events.json", buf.as_bytes()).unwrap();
}
}
fn get_json(url: &str) -> Result<String, Box<dyn Error>> {
Ok(ureq::get(url).call()?.into_string()?)
}

102
src/bin/rgb_test.rs Normal file
View File

@ -0,0 +1,102 @@
use std::{
fs, thread,
time::{Duration, Instant},
};
use display_interface_spi::SPIInterfaceNoCS;
use embedded_graphics::{
draw_target::DrawTarget,
mono_font::{
ascii::{FONT_10X20, FONT_5X8, FONT_6X9, FONT_9X15},
MonoTextStyleBuilder,
},
pixelcolor::{BinaryColor, Rgb565},
prelude::{OriginDimensions, Point, Primitive, Size},
primitives::{PrimitiveStyleBuilder, Rectangle},
text::Text,
Drawable,
};
use embedded_hal::digital::v2::OutputPin;
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
use image::{ImageBuffer, Rgb};
use linux_embedded_hal::I2cdev;
use rppal::{
gpio::Gpio,
hal::Delay,
spi::{Bus, Mode, SlaveSelect, Spi},
};
//use ssd1351::{properties::DisplaySize, mode::{GraphicsMode, displaymode::DisplayModeTrait}};
use time::{format_description, OffsetDateTime, PrimitiveDateTime};
//use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt, PrimitiveDateTimeExt};
fn main() {
//let i2c = I2cdev::new("/dev/i2c-1").unwrap();
//let interface = I2CDisplayInterface::new(i2c);
//let mut disp = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0).into_buffered_graphics_mode();
//disp.init().unwrap();
// Configure gpio
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 19660800, Mode::Mode0).unwrap();
let gpio = Gpio::new().unwrap();
let dc = gpio.get(25).unwrap().into_output();
let mut rst = gpio.get(27).unwrap().into_output();
// Init SPI
let spii = SPIInterfaceNoCS::new(spi, dc);
let mut disp = ssd1351::display::display::Ssd1351::new(spii);
// Reset & init
disp.reset(&mut rst, &mut Delay).unwrap();
disp.turn_on().unwrap();
/*
thread::sleep(Duration::from_secs(5));
disp.reset(&mut rst, &mut Delay).unwrap();
disp.turn_off().unwrap();
panic!("done!");
*/
// Clear the display
disp.clear(Rgb565::new(0x00, 0x00, 0x00)).unwrap();
//disp.flush().unwrap();
//disp.flush().unwrap();
let text_style_clock = MonoTextStyleBuilder::new()
.font(&FONT_10X20)
.text_color(Rgb565::new(0xff, 0x00, 0x00))
.build();
//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();
Text::new("Abc", (0, 30).into(), text_style_clock)
.draw(&mut disp)
.unwrap();
let _ = disp.flush();
thread::sleep(Duration::from_secs(15));
Text::new("5 seconds to off!", (0, 60).into(), text_style_clock)
.draw(&mut disp)
.unwrap();
let start = Instant::now();
let _ = disp.flush();
println!("{:?} ms", start.elapsed().as_millis());
thread::sleep(Duration::from_secs(5));
disp.reset(&mut rst, &mut Delay).unwrap();
disp.turn_off().unwrap();
}
struct LineHandleWrapper(LineHandle);
impl OutputPin for LineHandleWrapper {
type Error = gpio_cdev::Error;
fn set_low(&mut self) -> Result<(), Self::Error> {
self.0.set_value(0)
}
fn set_high(&mut self) -> Result<(), Self::Error> {
self.0.set_value(1)
}
}

View File

@ -0,0 +1,53 @@
use std::time::{Duration, SystemTime};
use gpio_cdev::Chip;
use rusqlite::{params, Connection};
fn main() {
let args = std::env::args().collect::<Vec<_>>();
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 chip = Chip::new("/dev/gpiochip0").unwrap();
let line = chip.get_line(26).unwrap();
let mut attempts = 0;
let mut temps = vec![];
let mut rhs = vec![];
let time = std::time::SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
while temps.len() < 5 && attempts < 10 {
if let Ok((rh, temp)) = raspi_oled::am2302_reading(&line) {
if rh > 0 && temp < 500 {
rhs.push(rh);
temps.push(temp);
}
}
std::thread::sleep(Duration::from_secs(5));
attempts += 1;
}
if !temps.is_empty() {
// median = hopefully no faulty readings
temps.sort();
rhs.sort();
database
.execute(
"INSERT INTO sensor_readings (time, humidity, celsius) VALUES (?1, ?2, ?3)",
params![time.as_secs(), rhs[rhs.len() / 2], temps[temps.len() / 2]],
)
.unwrap();
}
}

View File

@ -1,12 +1,52 @@
use std::{thread::sleep, time::{self, Duration}};
use std::{
thread::sleep,
time::{self, Duration},
};
use embedded_graphics::{pixelcolor::Rgb565, draw_target::DrawTarget, prelude::{OriginDimensions, Size, RgbColor}};
use gpio_cdev::{EventType, Line, LineRequestFlags};
use image::{ImageBuffer, Rgb};
pub struct FrameOutput {
pub buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
}
impl FrameOutput {
pub fn new(width: u32, height: u32) -> Self {
FrameOutput {
buffer: ImageBuffer::new(width, height)
}
}
}
impl DrawTarget for FrameOutput {
type Color = Rgb565;
type Error = ();
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
{
for pos in pixels {
if pos.0.x < 0 || pos.0.y < 0 || pos.0.x as u32 >= self.buffer.width() || pos.0.y as u32 >= self.buffer.height() {
continue;
}
self.buffer.put_pixel(pos.0.x as u32, pos.0.y as u32, Rgb([pos.1.r() << 3, pos.1.g() << 2, pos.1.b() << 3]));
}
Ok(())
}
}
impl OriginDimensions for FrameOutput {
fn size(&self) -> Size {
Size::new(self.buffer.width(), self.buffer.height())
}
}
fn read_events(line: &gpio_cdev::Line, timeout: std::time::Duration) -> Result<Vec<(u64, EventType)>, SensorError> {
let input = line.request(
LineRequestFlags::INPUT,
0,
"read-data")?;
let input = line.request(LineRequestFlags::INPUT, 0, "read-data")?;
let mut last_state = 1;
let start = time::Instant::now();
@ -43,9 +83,8 @@ fn events_to_data(events: Vec<(u64, EventType)>) -> Vec<u8> {
}
})
.filter(|&d| d.is_some())
.map(|elapsed| {
if elapsed.unwrap() > 35 { 1 } else { 0 }
}).collect()
.map(|elapsed| if elapsed.unwrap() > 35 { 1 } else { 0 })
.collect()
}
const MAX_HUMIDITY: u16 = 1000;
@ -58,12 +97,15 @@ fn process_data(mut bits: &[u8]) -> Result<(u16, u16), SensorError> {
}
let bytes: Vec<u8> = bits
.chunks(8)
.map(|chunk| chunk.iter()
.map(|chunk| {
chunk
.iter()
.enumerate()
// 8 bits, starting with the MSB
.map(|(bit_idx, &x)| x << (7 - bit_idx))
.sum()
).collect();
})
.collect();
let rh = (bytes[0] as u16) << 8 | bytes[1] as u16;
if rh > MAX_HUMIDITY {
return Err(SensorError::HumidityTooHigh);
@ -71,12 +113,12 @@ fn process_data(mut bits: &[u8]) -> Result<(u16, u16), SensorError> {
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;
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);
}
@ -86,7 +128,11 @@ fn process_data(mut bits: &[u8]) -> Result<(u16, u16), SensorError> {
#[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();
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);
}
@ -96,7 +142,7 @@ pub enum SensorError {
Io(gpio_cdev::Error),
ChecksumMismatch,
HumidityTooHigh,
Timeout
Timeout,
}
impl From<gpio_cdev::Error> for SensorError {

View File

@ -1,30 +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};
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::{Delay, I2cdev};
use machine_ip;
use std::intrinsics::transmute;
use std::{env, mem, time};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
static IMG_DATA: &[u8; 512] = include_bytes!("../rust.raw");
use linux_embedded_hal::I2cdev;
const CCS811_ADDR: u8 = 0x5A; // or 0x5B
@ -46,16 +22,13 @@ const CCS811_SW_RESET: u8 = 0xFF;
struct CCS811 {
i2c: I2cdev,
addr: u8
addr: u8,
}
impl CCS811 {
fn new(mut i2c: I2cdev, addr: u8) -> Self {
i2c.set_slave_address(addr as u16).unwrap();
Self {
i2c,
addr
}
Self { i2c, addr }
}
fn check_for_error(&mut self) -> Option<u8> {
@ -85,7 +58,9 @@ impl CCS811 {
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)?;
self.i2c
.smbus_write_byte_data(CCS811_MEAS_MODE, setting)
.map_err(Some)?;
Ok(())
}
@ -97,7 +72,10 @@ impl CCS811 {
/// 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))
(
((x[0] as u16) << 8) | (x[1] as u16),
((x[2] as u16) << 8) | (x[3] as u16),
)
}
}
@ -110,18 +88,125 @@ enum CCS811DriveMode {
Every250Milliseconds = 4,
}
fn main() {
let i2c = I2cdev::new("/dev/i2c-1").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 args = std::env::args().collect::<Vec<_>>();
if args.len() < 2 {
panic!("missing argument: database path");
}
let mut disp = FrameOutput {
buffer: ImageBuffer::new(128, 64),
};
let database = Connection::open(&args[1]).expect("failed to open database");
let mut query = database
.prepare("SELECT celsius FROM sensor_readings ORDER BY sensor_readings.time DESC LIMIT 288")
.unwrap();
let mut temps: Vec<i32> = query
.query_map([], |r| Ok(r.get(0)))
.unwrap()
.map(Result::unwrap)
.map(Result::unwrap)
.collect();
let mut global_min = 1000;
let mut global_max = 0;
let mut vals: Vec<(i32, i32)> = vec![];
for hour in temps.chunks_mut(6) {
hour.sort();
let min = hour[1];
let mut max = hour[hour.len() - 2];
println!("min {} max {}", min, max);
// sanity check value
if max > 300 {
if vals.is_empty() {
max = min;
} else {
max = vals.last().unwrap().1;
}
}
global_min = min.min(global_min);
global_max = max.max(global_max);
vals.push((min, max));
}
println!("global {} | {}", global_min, global_max);
let diff = global_max - global_min;
let x = 1;
let y = 1;
let scaley = 64;
let scalex = 2;
vals.reverse();
for (i, (a, b)) in vals.into_iter().enumerate() {
let x = x + i as i32 * scalex;
let y1 = y + (global_max - b) * scaley / diff;
let y2 = y + (global_max - a) * scaley / diff;
let height = y2 - y1 + 1;
disp.fill_solid(
&Rectangle::new((x, y1).into(), (scalex as u32, height as u32).into()),
BinaryColor::On,
)
.unwrap();
}
disp.buffer.save("/tmp/frame.png").unwrap();
*/
}
/*
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};
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 linux_embedded_hal::{Delay, I2cdev};
use machine_ip;
use rusqlite::{params, Connection};
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
use std::intrinsics::transmute;
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use std::{env, mem, time};
static IMG_DATA: &[u8; 512] = include_bytes!("../rust.raw");
fn main() {
let args = env::args().collect::<Vec<_>>();
if args.len() < 2 {
panic!("missing argument: database path");
}
let database = Connection::open(&args[1]).expect("failed to open database");
database.execute("
database
.execute(
"
CREATE TABLE IF NOT EXISTS sensor_readings(
time INTEGER PRIMARY KEY,
humidity INTEGER NOT NULL,
celsius INTEGER NOT NULL
)", []).unwrap();
)",
[],
)
.unwrap();
/*
let mut ccs = CCS811::new(i2c, CCS811_ADDR);
@ -135,10 +220,17 @@ fn main() {
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();
let time = std::time::SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
if let Ok((rh, temp)) = raspi_oled::am2302_reading(&line) {
if rh > 0 && temp < 500 {
database.execute("INSERT INTO sensor_readings (time, humidity, celsius) VALUES (?1, ?2, ?3)", params![time.as_secs(), rh, temp]).unwrap();
database
.execute(
"INSERT INTO sensor_readings (time, humidity, celsius) VALUES (?1, ?2, ?3)",
params![time.as_secs(), rh, temp],
)
.unwrap();
display_on_ssd1306(rh, temp, time);
break;
}
@ -146,15 +238,10 @@ fn main() {
}
}
fn display_on_ssd1306(rh: u16, temp: u16, time: Duration) {
let i2c = I2cdev::new("/dev/i2c-1").unwrap();
let interface = I2CDisplayInterface::new(i2c);
let mut disp = Ssd1306::new(
interface,
DisplaySize128x64,
DisplayRotation::Rotate0,
).into_buffered_graphics_mode();
let mut disp = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0).into_buffered_graphics_mode();
disp.init().unwrap();
@ -164,14 +251,10 @@ fn display_on_ssd1306(rh: u16, temp: u16, time: Duration) {
.build();
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();
Text::new(&text, Point::new(0, 10), text_style).draw(&mut disp).unwrap();
let secs = time.as_secs();
let time = format!("{:02}:{:02} Uhr", (secs / 3600 + 2) % 24, secs / 60 % 60);
Text::new(&time, Point::new(0, 32), text_style)
.draw(&mut disp)
.unwrap();
Text::new(&time, Point::new(0, 32), text_style).draw(&mut disp).unwrap();
disp.flush().unwrap();
/*
sleep(Duration::from_secs(2));
@ -248,6 +331,7 @@ impl InputPin for LineWrapper {
}
impl OutputPin for LineWrapper {
<<<<<<< HEAD
type Error = gpio_cdev::Error;
fn set_low(&mut self) -> Result<(), Self::Error> {
@ -258,3 +342,30 @@ impl OutputPin for LineWrapper {
self.0.request(LineRequestFlags::OUTPUT, 1, "rust-line-wrapper")?.set_value(1)
}
}
||||||| parent of 683458d (Reformat code)
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)
}
}
=======
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)
}
}
*/