chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

M5Stamp C3+SCD41で温湿度を計測、ThingSpeakにMQTT Publish

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

*1:理由は、データ保存に関する制約が一番緩い、MATLABを使ってデータ分析可能