目的:自宅の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で動かすと何か分かるかも