Compare commits

...

2 Commits

4 changed files with 152 additions and 162 deletions

111
README.org Normal file
View File

@ -0,0 +1,111 @@
#+TITLE: PMME - Particulate Matter Monitoring Ecosystem
#+OPTIONS: toc:nil
#+AUTHOR:
#+DATE:
* Overview
A complete IoT solution for monitoring air quality using PM2.5 sensors, built with Rust across embedded, cloud, and mobile platforms.
* Architecture Overview
The PMME system consists of four main components working together to provide real-time air quality monitoring:
#+BEGIN_EXAMPLE
[ESP32 Device] --WiFi--> [Cloud Server] <--HTTP--> [Mobile App]
| |
[Admin Panel] <--WiFi--> [PostgreSQL DB]
#+END_EXAMPLE
* Components
** 🔧 ESP32 Device (~pmme-device/rust-version/~)
- *Platform*: ESP32 microcontroller running esp-idf with Rust
- *Sensors*: PM2.5 particulate matter sensor (PMS7003)
- *Display*: SSD1306 OLED for local readings
- *Connectivity*: WiFi for data transmission to cloud
- *Features*:
- Real-time sensor data collection
- Local display of current readings
- Automatic cloud data transmission
** ☁️ Cloud Server (~pmme-cloud/~)
- *Framework*: Rocket web framework (Rust)
- *Database*: PostgreSQL (planned integration)
- *Purpose*:
- Receive and store sensor data from ESP32 devices
- Provide REST API for mobile app data access
- Future: Historical data analysis and graphing
** 📱 Mobile App (~pmme-mobile/~)
- *Framework*: Tauri (Rust + Web frontend)
- *Platform*: Cross-platform mobile application
- *Features*:
- Connect to cloud server via HTTP
- Display current PM2.5 readings
- Future: Historical data visualization and graphs
- Future: Potential direct ESP32 admin panel access
** 🌐 Web UI & Admin Panel (~pmme-ui/~)
- *Framework*: ClojureScript with Shadow CLJS
- *Purpose*:
- Data visualization interface
- Admin panel for ESP32 device management and provisioning
* Getting Started
** ESP32 Device Setup
#+BEGIN_SRC bash
cd pmme-device/rust-version/
# Set up ESP-IDF environment
./export-esp.sh
cargo build
cargo run
#+END_SRC
** Cloud Server
#+BEGIN_SRC bash
cd pmme-cloud/
cargo run
#+END_SRC
** Mobile App
#+BEGIN_SRC bash
cd pmme-mobile/
npm install
npm run tauri dev
#+END_SRC
** Web UI
#+BEGIN_SRC bash
cd pmme-ui/
npm install
npm run dev
#+END_SRC
* Future Features
- *PostgreSQL Integration*: Historical data storage and analysis
- *Data Visualization*: Graphs and trends in mobile app
- *Multi-sensor Support*: Expansion beyond PM2.5 to other environmental sensors
- *ESP32 HTTP Server*: Device-hosted admin interface
* Development Notes
- The project previously included a C version implementation which has been deprecated in favor of the Rust implementation
- The system is designed to be scalable for multiple ESP32 devices reporting to a single cloud instance
* Tech Stack
- *Embedded*: Rust + esp-idf
- *Backend*: Rust + Rocket + PostgreSQL
- *Mobile*: Rust + Tauri
- *Web/Admin*: ClojureScript + Shadow CLJS

View File

@ -0,0 +1,2 @@
export PATH="/home/joe/.local/share/rustup/toolchains/esp/xtensa-esp-elf/esp-14.2.0_20240906/xtensa-esp-elf/bin:$PATH"
export LIBCLANG_PATH="/home/joe/.local/share/rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-19.1.2_20250225/esp-clang/lib"

View File

@ -70,48 +70,6 @@ fn main() -> Result<()> {
let sensors_arc = Arc::clone(&pm_data);
// let wifi_arc = Arc::clone(&pm_data);
// let ssid = "Bad Math Bird";
// let pass = "shocktop";
// let mut auth_method = AuthMethod::WPA2Personal;
// if ssid.is_empty() {
// bail!("Missing WiFi name")
// }
// if pass.is_empty() {
// auth_method = AuthMethod::None;
// info!("Wifi password is empty");
// }
// let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), Some(nvs))?;
// let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
// wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
// info!("Starting wifi...");
// wifi.set_configuration(&Configuration::Client(ClientConfiguration {
// ssid: ssid
// .try_into()
// .expect("Could not parse the given SSID into WiFi config"),
// password: pass
// .try_into()
// .expect("Could not parse the given password into WiFi config"),
// channel: None,
// auth_method,
// ..Default::default()
// }))?;
// wifi.start()?;
// info!("Connecting wifi...");
// wifi.connect()?;
// info!("Waiting for DHCP lease...");
// wifi.wait_netif_up()?;
// let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
// info!("Wifi DHCP info: {:?}", ip_info);
let handles = vec![
thread::spawn(move || {
if let Err(e) = oled_task(oled_arc, i2c) {
@ -157,6 +115,8 @@ fn main() -> Result<()> {
}
if is_pressed {
FreeRtos::delay_ms(100);
} else {
FreeRtos::delay_ms(10);
}
}
}),

View File

@ -2,142 +2,59 @@ use anyhow::{bail, Result};
use log::info;
use esp_idf_svc::{
eventloop::EspSystemEventLoop,
hal::peripheral,
nvs::EspDefaultNvsPartition,
sys::{
eventloop::EspSystemEventLoop, hal::{delay::FreeRtos, peripheral}, http::{server::EspHttpServer, Method}, io::Write, nvs::EspDefaultNvsPartition, sys::{
esp, esp_err_t, wifi_prov_event_handler_t, wifi_prov_mgr_config_t, wifi_prov_mgr_deinit,
wifi_prov_mgr_init, wifi_prov_mgr_is_provisioned, wifi_prov_mgr_start_provisioning,
wifi_prov_mgr_stop_provisioning, wifi_prov_mgr_wait, wifi_prov_scheme_ble,
wifi_prov_security_WIFI_PROV_SECURITY_1, wifi_prov_security_t, EspError,
},
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi},
}, wifi::{AccessPointConfiguration, AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi}
};
use std::ffi::c_void;
use std::ffi::CString;
use std::ptr;
pub struct WifiProvisioning;
impl WifiProvisioning {
pub fn new() -> Result<Self, EspError> {
unsafe {
// Updated struct initialization
let config = wifi_prov_mgr_config_t {
scheme: wifi_prov_scheme_ble, // ble provisioning
scheme_event_handler: wifi_prov_event_handler_t {
event_cb: None, // No custom callback
user_data: ptr::null_mut(),
},
app_event_handler: wifi_prov_event_handler_t {
event_cb: None, // No custom callback
user_data: ptr::null_mut(),
},
};
esp!(wifi_prov_mgr_init(config))?;
}
Ok(WifiProvisioning)
}
pub fn start_provisioning(
&self,
security: wifi_prov_security_t,
pop: &str,
service_name: &str,
service_key: Option<&str>,
) -> Result<(), EspError> {
let pop = CString::new(pop).unwrap();
let service_name = CString::new(service_name).unwrap();
let service_key = service_key.map(|key| CString::new(key).unwrap());
let pop_ptr: *const c_void = pop.as_ptr() as *const c_void;
unsafe {
esp!(wifi_prov_mgr_start_provisioning(
security,
pop_ptr,
service_name.as_ptr(),
service_key.map_or(ptr::null(), |k| k.as_ptr()),
))?;
}
Ok(())
}
pub fn wait(&self) {
unsafe {
wifi_prov_mgr_wait();
}
}
pub fn is_provisioned(&self) -> Result<bool, EspError> {
let mut provisioned: bool = false;
let result: esp_err_t = unsafe { wifi_prov_mgr_is_provisioned(&mut provisioned) };
if result == 0 {
Ok(provisioned)
} else {
Err(EspError::from(result).unwrap())
}
}
pub fn stop(&self) {
unsafe {
wifi_prov_mgr_stop_provisioning();
}
}
}
impl Drop for WifiProvisioning {
fn drop(&mut self) {
unsafe {
wifi_prov_mgr_deinit();
}
}
}
// NOTE: We decided against standard BLE provisioning. Refer to this in case we reconsider
// https://github.com/mzakharo/esp32_rust_provisioning/blob/master/src/main.rs
pub fn wifi_task(
modem: impl peripheral::Peripheral<P = esp_idf_svc::hal::modem::Modem> + 'static,
sysloop: EspSystemEventLoop,
nvs: EspDefaultNvsPartition,
// esp_wifi: EspWifi,
) -> Result<()> {
// info!("Got wifi");
// let ssid = "Bad Math Bird";
// let pass = "shocktop";
info!("Provisioning device!");
let wifi = esp_idf_svc::wifi::EspWifi::new(modem, sysloop.clone(), Some(nvs))?;
let mut wifi = BlockingWifi::wrap(wifi, sysloop)?;
info!("Make blocking wifi");
let prov = WifiProvisioning::new()?;
info!("New Provision");
if !prov.is_provisioned()? {
info!("Not provisioned");
let wifi_configuration: Configuration = Configuration::Client(ClientConfiguration {
..Default::default()
});
info!("Got configuration");
wifi.set_configuration(&wifi_configuration)?;
info!("Set configuration");
wifi.start()?;
info!("Started wifi");
prov.start_provisioning(
wifi_prov_security_WIFI_PROV_SECURITY_1,
"88888888", // Proof of Possession (POP)
"PROV_ESP32", // Service Name
None, // No Service Key
info!("Enabling Hotspot");
let mut wifi = BlockingWifi::wrap(
EspWifi::new(modem, sysloop.clone(), Some(nvs))?,
sysloop,
)?;
info!("Start provisioning");
println!("Waiting for Wi-Fi provisioning...");
prov.wait();
wifi.set_configuration(&Configuration::AccessPoint(AccessPointConfiguration {
ssid: "PMME-Wifi".try_into().unwrap(),
password: "88888888".try_into().unwrap(),
ssid_hidden: false,
channel: 1,
secondary_channel: None,
protocols: Default::default(),
auth_method: AuthMethod::WPA2Personal,
max_connections: 2,
}))?;
println!("Provisioning completed. Stopping...");
prov.stop();
} else {
info!("Provisioned!");
wifi.start()?;
wifi.connect()?;
}
info!("Wifi Started");
wifi.wait_netif_up()?;
let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
// println!("Wifi DHCP info: {:?}", ip_info);
info!("Wifi netif up");
Ok(())
let server_conf = esp_idf_svc::http::server::Configuration::default();
let mut server = EspHttpServer::new(&server_conf)?;
info!("Started web server on port {}", server_conf.http_port);
server.fn_handler("/api/provision", Method::Get, |req| {
req.into_ok_response()?
.write_all(b"<html><body>Hello world!</body></html>")
.map(|_| ())
})?;
loop {
FreeRtos::delay_ms(500);
}
// Ok(())
}