Compare commits
2 Commits
b9b9a0bdd0
...
8bd69dce67
Author | SHA1 | Date | |
---|---|---|---|
8bd69dce67 | |||
8ee32a8949 |
111
README.org
Normal file
111
README.org
Normal 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
|
2
pmme-device/rust-version/export-esp.sh
Normal file
2
pmme-device/rust-version/export-esp.sh
Normal 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"
|
@ -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);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@ -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(())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user