Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions org/weather-station.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#+TITLE: Rust Weather Station Learning Project
#+STARTUP: overview
#+CATEGORY: WeatherStation
#+TODO: TODO IN-PROGRESS BLOCKED REVIEW DONE

* Project Overview
This is a hands-on Rust learning project using a Raspberry Pi Pico W. It reads environmental data (light, temperature, humidity, pressure), displays it on an LCD, and serves it via an embedded HTTP API.

You'll learn embedded Rust, async, traits, device abstraction, and Wi-Fi communication.

* Hardware Components
- GI5516 Light Sensor (analog)
- BMP180 Pressure Sensor (I2C)
- DHT11 Temp/Humidity (digital)
- I2C 1602 LCD Display
- Raspberry Pi Pico W

* Phase 1: Hardware Setup & Display

** TODO Solder LCD to protoboard
SCHEDULED: <2025-07-03 Thu>
DEADLINE: <2025-07-04 Fri>
- [ ] Pin layout check (VCC, GND, SDA, SCL)
- [ ] Solder and inspect joints
- [ ] Test I2C connection with LCD address

** TODO Write minimal LCD driver usage
SCHEDULED: <2025-07-05 Sat>
DEADLINE: <2025-07-06 Sun>
- [ ] Understand `lcd-lcm1602-i2c` API
- [ ] Use Embassy I2C & Delay
- [ ] Display static text on both lines
- [ ] Learn about lifetime annotations in `init_lcd`

* Rust Learning Goals
- Ownership of I2C and LCD structs
- Lifetimes and async init patterns
- Embassy HAL I2C basics

---

* Phase 2: Sensor Integration

** TODO Connect and read GI5516 Light Sensor (ADC)
SCHEDULED: <2025-07-07 Mon>
DEADLINE: <2025-07-08 Tue>
- [ ] Learn Embassy ADC usage
- [ ] Convert analog value to % light
- [ ] Display "Light: 78%" on LCD

** TODO Integrate BMP180 Pressure Sensor (I2C)
SCHEDULED: <2025-07-09 Wed>
DEADLINE: <2025-07-11 Fri>
- [ ] Connect sensor via I2C
- [ ] Use or write simple BMP180 driver
- [ ] Read & convert pressure
- [ ] Format string like "Pressure: 1013 hPa"

** TODO Read DHT11 Temp & Humidity Sensor
SCHEDULED: <2025-07-12 Sat>
DEADLINE: <2025-07-14 Mon>
- [ ] Learn DHT11 timing protocol
- [ ] Read data from GPIO pin
- [ ] Display "Temp: 26°C RH: 55%"

* Rust Learning Goals
- Embedded-hal traits: ADC, I2C, digital I/O
- Dealing with timing constraints
- Error handling (`Result`, `Option`)
- Converting raw sensor data

---

* Phase 3: Display System

** TODO Display sensor readings cyclically
SCHEDULED: <2025-07-15 Tue>
DEADLINE: <2025-07-16 Wed>
- [ ] Use Embassy `Timer::after` for delay
- [ ] Cycle Light → Pressure → Temp/Humidity
- [ ] Refresh LCD cleanly with `.clear()`

** TODO Add graceful error messages on LCD
SCHEDULED: <2025-07-17 Thu>
DEADLINE: <2025-07-18 Fri>
- [ ] Show "Sensor X Error" when reads fail
- [ ] Catch and log sensor errors

* Rust Learning Goals
- Timers & async/await logic
- `match`, pattern matching
- Display formatting with `core::fmt::Write`

---

* Phase 4: Code Architecture & Abstraction

** TODO Abstract sensors into a trait system
SCHEDULED: <2025-07-19 Sat>
DEADLINE: <2025-07-21 Mon>
- [ ] Define `WeatherSensor` trait
- [ ] Implement for LightSensor, Bmp180, Dht11
- [ ] Use boxed trait objects or generics

** TODO Create display controller module
SCHEDULED: <2025-07-22 Tue>
DEADLINE: <2025-07-23 Wed>
- [ ] Pass list of sensors to display manager
- [ ] Print current value of each
- [ ] Decouple logic from `main.rs`

* Rust Learning Goals
- Traits and dynamic dispatch
- Lifetimes in trait objects
- Modular code & reuse
- Separation of concerns

---

* Phase 5: Networking & API

** TODO Enable Wi-Fi on Pico W
SCHEDULED: <2025-07-24 Thu>
DEADLINE: <2025-07-26 Sat>
- [ ] Connect to local Wi-Fi
- [ ] Learn Embassy + CYW43 driver

** TODO Serve sensor values on HTTP endpoint
SCHEDULED: <2025-07-27 Sun>
DEADLINE: <2025-07-30 Wed>
- [ ] Serve `GET /api/weather`
- [ ] Return JSON: { "light": x, "temp": y, ... }
- [ ] Test with browser & curl

** TODO Optional: Serve basic HTML status page
SCHEDULED: <2025-07-31 Thu>
DEADLINE: <2025-08-02 Sat>
- [ ] Render sensor values in HTML
- [ ] Serve page on `/status`
- [ ] Embed refresh every 5s

* Rust Learning Goals
- Embassy networking stack (TCP, async)
- Wi-Fi provisioning
- Building & serializing JSON with `serde`
- Minimal embedded web server design

---

* Phase 6: Polish & Reflection

** TODO Test for long-term stability
SCHEDULED: <2025-08-03 Sun>
DEADLINE: <2025-08-05 Tue>
- [ ] Run for 12+ hours
- [ ] Check power draw, memory usage
- [ ] Handle disconnection gracefully

** TODO Final code cleanup & learning notes
SCHEDULED: <2025-08-06 Wed>
DEADLINE: <2025-08-08 Fri>
- [ ] Write README with build + wiring instructions
- [ ] Summarize what you learned
- [ ] Archive the repo and flash firmware

---

* Future Ideas (optional)
- Log to SD card (SPI)
- Bluetooth LE broadcast
- OTA firmware updates
- Integrate with MQTT/Home Assistant
22 changes: 22 additions & 0 deletions src/lcd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use embassy_rp::i2c::I2c;
use embassy_time::Delay;
use lcd_lcm1602_i2c::async_lcd::Lcd;

pub async fn init_lcd<'a>(
bus: &'a mut I2c<'a, embassy_rp::peripherals::I2C0, embassy_rp::i2c::Async>,
delay: &'a mut Delay,
) -> Result<Lcd<'a, I2c<'a, _, _>, Delay>, embassy_rp::i2c::Error> {
const LCD_ADDR: u8 = 0x27;
let lcd = Lcd::new(bus, delay)
.with_address(LCD_ADDR)
.with_cursor_on(false)
.with_rows(2);
Ok(lcd.init().await.unwrap())
}

pub async fn welcome_message(
lcd: &mut Lcd<'_, I2c<'_, _, _>, Delay>,
) {
lcd.clear().await.unwrap();
lcd.write_str("Rust Weather!").await.unwrap();
}
71 changes: 41 additions & 30 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,62 @@
//! This example shows how to communicate asynchronous using i2c with external chip.
//!
//! It's using embassy's functions directly instead of traits from embedded_hal_async::i2c::I2c.
//! While most of i2c devices are addressed using 7 bits, an extension allows 10 bits too.

#![no_std]
#![no_main]
use embassy_rp::i2c::InterruptHandler;

use embassy_rp::i2c::{I2c, InterruptHandler};
use embassy_rp::peripherals::I2C0;
use embassy_time::{Delay, Duration, Timer};
use lcd_lcm1602_i2c::async_lcd::Lcd;
use {defmt_rtt as _, panic_probe as _};

embassy_rp::bind_interrupts!(struct Irqs {
I2C0_IRQ => InterruptHandler<embassy_rp::peripherals::I2C0>;
I2C0_IRQ => InterruptHandler<I2C0>;
});

#[embassy_executor::main]
async fn main(_task_spawner: embassy_executor::Spawner) {
const LCD_ADDRESS: u8 = 0x26;
let p = embassy_rp::init(Default::default());
let sda = p.PIN_0;
let scl = p.PIN_1;
let config = embassy_rp::i2c::Config::default();
let mut bus = embassy_rp::i2c::I2c::new_async(p.I2C0, scl, sda, Irqs, config);
let mut delay = Delay;
async fn init_lcd<'a>(
bus: &'a mut I2c<'a, I2C0, embassy_rp::i2c::Async>,
delay: &'a mut Delay,
) -> Result<Lcd<'a, I2c<'a, I2C0, embassy_rp::i2c::Async>, Delay>, embassy_rp::i2c::Error> {
const LCD_ADDRESS: u8 = 0x27;

let mut lcd_scr = Lcd::new(&mut bus, &mut delay)
let lcd_scr = Lcd::new(bus, delay)
.with_address(LCD_ADDRESS)
.with_cursor_on(false)
.with_rows(2);

// Initialize the LCD
lcd_scr = lcd_scr.init().await.unwrap();
lcd_scr.clear().await.unwrap();
let lcd_scr = lcd_scr.init().await.unwrap(); // `init` returns ownership
Ok(lcd_scr)
}

// Write a message to the LCD
lcd_scr.write_str("Hello Rust!").await.unwrap();
async fn lcd_display<'a>(
lcd: &mut Lcd<'a, I2c<'a, I2C0, embassy_rp::i2c::Async>, Delay>,
msg: &str,
) {
lcd.clear().await.unwrap();
lcd.write_str("Hello Rust!").await.unwrap();

// Main loop: write messages with delays
loop {
lcd_scr.set_cursor(0, 0).await.unwrap(); // Move to the second line
lcd_scr.write_str("Async + LCD").await.unwrap();
lcd.set_cursor(1, 0).await.unwrap(); // Move to the second line
lcd.write_str(msg).await.unwrap();

Timer::after(Duration::from_secs(2)).await;
Timer::after(Duration::from_secs(2)).await;
}

lcd_scr.clear().await.unwrap();
lcd_scr.write_str("Embassy Rocks!").await.unwrap();
#[embassy_executor::main]
async fn main(_task_spawner: embassy_executor::Spawner) {
let p = embassy_rp::init(Default::default());

Timer::after(Duration::from_secs(2)).await;
let mut bus = I2c::new_async(
p.I2C0,
p.PIN_1,
p.PIN_0,
Irqs,
embassy_rp::i2c::Config::default(),
);
let mut delay = Delay;

// Initialize the LCD
let mut lcd = init_lcd(&mut bus, &mut delay).await.unwrap();

// Display messages
loop {
lcd_display(&mut lcd, "Hello World!").await;
}
}
4 changes: 4 additions & 0 deletions src/net/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// placeholder for embedding http server
pub async fn serve_api() {
// Future: handle GET /api/weather
}
23 changes: 23 additions & 0 deletions src/sensors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
mod light;
mod bmp180;
mod dht11;

use alloc::vec::Vec;
use async_trait::async_trait;
use embassy_rp::i2c::I2c;
use embassy_time::Delay;

#[async_trait]
pub trait WeatherSensor {
async fn read(&mut self) -> Result<String, &'static str>;
}

pub async fn init_all<'a>(
_i2c: &'a mut I2c<'a, embassy_rp::peripherals::I2C0, embassy_rp::i2c::Async>,
_delay: &'a mut Delay,
) -> Vec<Box<dyn WeatherSensor + 'a>> {
let mut sensors: Vec<Box<dyn WeatherSensor>> = Vec::new();
// Add real sensor instances here
// sensors.push(Box::new(light::LightSensor::new(...)));
sensors
}
17 changes: 17 additions & 0 deletions src/sensors/sensor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use super::WeatherSensor;
use async_trait::async_trait;

pub struct LightSensor {}

impl LightSensor {
pub fn new() -> Self {
Self {}
}
}

#[async_trait]
impl WeatherSensor for LightSensor {
async fn read(&mut self) -> Result<String, &'static str> {
Ok("Light: 75%".to_string())
}
}
Loading