目的:自宅のCO2濃度を計測してスマフォで確認するシステムを作る
取り組み:M5 Stamp C3+SCD41で温湿度を計測、ThingSpeakにMQTT PublishするアプリをRustで作成
詳細:
無料で使えるIoT プラットフォームはいろいろあるけど、データ分析用にはThingSpeakが適していると思っています*1。これまでMicroPythonでMQTT Publishはしてたけど、Rustの勉強も兼ねて、部屋の温湿度、CO2を計測して、ThingSpeakにPublishするシステムを試作した。
構成図は以下
センサとM5 Stamp C3の概観は以下
ThingSpeak自体はスマフォ用ダッシュボードを提供していませんが、ThingSpeak上のデータを閲覧するアプリ(Thingview)が個人開発?で提供されています。Thinviewを使うとスマフォでグラフ表示が可能になります。
以下は自宅の部屋のCO2濃度の遷移。M5Stampで動かす前は、XiaoESP32C3で動かしていた。グラフが0に張り付いているのはセンサの初期化がおかしい時。本来は異常データとして外すべきですが、そこまで前処理していません。
改善の余地ありですが、ご参考にmain.rsは以下
いろんなサンプルソースの寄せ集めなので、どこから引用したかすでに分からず。。無駄なuseもあるかもしれません。
// // IoT Gateway (publish mqtt w/TLS) // using IoT PF : Thingspeak // sensor : SCD41 // use std::thread; use std::thread::sleep; use std::time::Duration; use anyhow::bail; use embedded_svc::wifi::*; use esp_idf_svc::wifi::*; use esp_idf_svc::eventloop::*; use esp_idf_svc::netif::*; use esp_idf_hal::peripherals::Peripherals; use esp_idf_hal::peripheral; use esp_idf_hal::delay::FreeRtos; use esp_idf_hal::i2c::*; use esp_idf_hal::prelude::*; use esp_idf_sys as _; use anyhow::Result; use embedded_svc::mqtt::client::{Connection, QoS}; use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration}; mod scd41; //const THINGSPEAK_ENDPOINT: &str = "mqtts://mqtt3.thingspeak.com:8883"; const THINGSPEAK_CHANNEL_ID: &str = "<set_your_channel_id>"; const THINGSPEAK_ID: &str = "<set_your_id>"; const THINGSPEAK_PWD: &str = "<set_your_pwd>"; const SSID: &str = env!("RUST_ESP32_STD_DEMO_WIFI_SSID"); const PASS: &str = env!("RUST_ESP32_STD_DEMO_WIFI_PASS"); fn main() -> Result<()> { esp_idf_sys::link_patches(); #[allow(unused)] let peripherals = Peripherals::take().unwrap(); let sysloop = EspSystemEventLoop::take()?; #[allow(clippy::redundant_clone)] #[allow(unused_mut)] let mut _wifi = wifi(peripherals.modem, sysloop.clone())?; // enable I2C and setup SCD41 let i2c = peripherals.i2c0; let scl = peripherals.pins.gpio18; let sda = peripherals.pins.gpio19; let config = I2cConfig::new().baudrate(10.kHz().into()); let mut i2c = I2cDriver::new(i2c, sda, scl, &config)?; FreeRtos::delay_ms(500); // wait for stable if let Err(e) = scd41::setup(&mut i2c){ println!("Error in scd41 setup{}",e); } // https://github.com/esp-rs/esp-idf-svc/blob/master/src/mqtt/client.rs let conf = MqttClientConfiguration { crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach), keep_alive_interval: Some(Duration::from_secs(120)), client_id: Some(THINGSPEAK_ID), username: Some(THINGSPEAK_ID), password: Some(THINGSPEAK_PWD), ..Default::default() }; let (mut client, mut connection) = EspMqttClient::new_with_conn( "mqtts://mqtt3.thingspeak.com:8883", &conf, )?; thread::spawn(move || { println!("MQTT Listening for messages"); while let Some(msg) = connection.next() { match msg { Err(e) => println!("MQTT Message ERROR: {}", e), Ok(msg) => println!("MQTT Message: {:?}", msg), } } println!("MQTT connection loop exit"); }); println!("Connected to MQTT"); let mut temp = 0.0; let mut hum = 0.0; let mut co2 = 0; let press = 0; let topics = format!("channels/{}/publish", THINGSPEAK_CHANNEL_ID); loop { // get sensor value match scd41::read_measurement(&mut i2c){ Err(e) => println!("Error in read_measurement{}",e), Ok(v) => (temp, hum, co2) = v, } println!("---- Publish message ----"); println!("topic:{}", topics); let payload = format!("field1={}&field2={}&field3={}&field4={}", temp, hum, press, co2); println!("message{}", payload); client.publish(&topics, QoS::AtMostOnce, false, payload.as_bytes())?; println!("---- Publish done ----"); println!("sleep 10min"); sleep(Duration::from_millis(600_000)); // every 10min } } // // wifi function was created by referencing rust-esp32-std-demo // https://github.com/ivmarkov/rust-esp32-std-demo // fn wifi( modem: impl peripheral::Peripheral<P = esp_idf_hal::modem::Modem> + 'static, sysloop: EspSystemEventLoop,) -> Result<Box<EspWifi<'static>>> { use std::net::Ipv4Addr; let mut wifi = Box::new(EspWifi::new(modem, sysloop.clone(), None)?); let ap_infos = wifi.scan()?; let ours = ap_infos.into_iter().find(|a| a.ssid == SSID); let channel = if let Some(ours) = ours { Some(ours.channel) } else { None }; wifi.set_configuration(&Configuration::Mixed( ClientConfiguration { ssid: SSID.into(), password: PASS.into(), channel, ..Default::default() }, AccessPointConfiguration { ssid: "aptest".into(), channel: channel.unwrap_or(1), ..Default::default() }, ))?; wifi.start()?; if !WifiWait::new(&sysloop)? .wait_with_timeout(Duration::from_secs(20), || wifi.is_started().unwrap()) { bail!("Wifi did not start"); } wifi.connect()?; if !EspNetifWait::new::<EspNetif>(wifi.sta_netif(), &sysloop)?.wait_with_timeout( Duration::from_secs(20), || { wifi.is_connected().unwrap() && wifi.sta_netif().get_ip_info().unwrap().ip != Ipv4Addr::new(0, 0, 0, 0) }, ) { bail!("Wifi did not connect or did not receive a DHCP lease"); } let _ip_info = wifi.sta_netif().get_ip_info()?; Ok(wifi) }
動きはするものの分からない点
M5 Stamp C3はUSB/Serial変換を専用の外付けチップで行っている。M5 Stamp C3の前はXiao ESP32C3で動かしていた。こちらはUSBを直接収容している(はず)。異なるハード構成なのになぜ同じソースで動いてしまうのか??? それぞれの回路図とシリアル接続のソースを読み比べないと、理解できない。同じソースを使って、あきらかにUSBを直接収容しているM5 Stamp C3Uで動かすと何か分かるかも