kd_yutaのアイコン画像
kd_yuta 2025年01月31日作成 (2025年01月31日更新) © MIT
製作品 製作品 閲覧数 467
kd_yuta 2025年01月31日作成 (2025年01月31日更新) © MIT 製作品 製作品 閲覧数 467

SPRESENSEとELTRES通信でCO2濃度をマッピング

SPRESENSEとELTRES通信でCO2濃度をマッピング

初めに

本製品は
公益社団法人 計測自動制御学会 システムインテグレーション部門様
ソニーグループ株式会社様
が共催する、Sensing Solution ハッカソン2024にて作成した提案製品を改良したものです。

概要

本製品はSony SpresenseELTRES通信を活用し、CO₂濃度のリアルタイム測定・解析・可視化を行うIoT環境モニタリングシステムである。都市部と郊外の複数地点でCO₂濃度データを収集し、クラウド(CLIP Viewer Lite)と連携することで、遠隔地からのリアルタイム監視を実現する。また、MATLABを用いた解析により、CO₂濃度の地理的分布を視覚化し、環境政策や都市計画に活用可能な情報を提供する。

課題背景と希望

地球温暖化とCO₂濃度の急増

地球温暖化は、現在の社会における最も深刻な環境問題の一つである。その主要な要因とされるCO₂濃度の上昇は、産業革命以降急激に進行しており、現在では過去100万年で最高レベルに達していると報告されている。特に、都市部では交通量や産業活動の影響によりCO₂排出量が増加し、局所的なCO₂濃度の変動が気候や空気質に与える影響が大きくなっている。
一方で、過去の地球環境史を振り返ると、CO₂濃度が高かった時代は存在するが、問題の本質は「濃度の高さ」ではなく「増加のスピードの速さ」にある。地球の気候システムは変化に対して一定のバランスを保とうとするが、ある閾値を超えると急激な環境変化が生じ、生態系の破壊や異常気象の頻発を引き起こす可能性が高いとされている。

過去の成功事例:オゾン層破壊問題の解決

CO₂濃度の可視化が重要である理由の一つに、過去の環境問題解決の成功事例が挙げられる。
1980年代、オゾン層の破壊問題が世界的に認識された際、南極の「オゾンホール」の視覚的な提示が人々の意識を変える大きな要因となった。オゾン層破壊の危険性が明確になったことで、フロンガスの使用規制が実施され、現在ではオゾンホールの回復が進んでいる。

本製品はCO₂濃度の可視化によって社会に対し変化の必要性を認知して頂きたく作成した。

構成

本製品の構成は以下です。
システム構成 フロー図

要素

  • SPRESENSE
  • MH-Z19B
    高精度なCO₂測定
  • ELTRESアドオンボード
    広域通信(100km以上) が可能なソニーのIoT向けLPWA
    GNSS(GPS)機能を搭載し、位置情報とセットでデータを送信できる
  • CLIP Viewer Lite
    APIを利用してMATLABと連携し、データをリアルタイムで取得
    遠隔地からクラウド経由でデータを監視・分析
    キャプションを入力できます
  • MATLAB
    APIを利用してCLIP Viewer Liteのデータを取得
    CO₂濃度の時系列変化を解析
    都市部と郊外のCO₂濃度の違いをマッピング

実現のための工夫

単体のSpresenseで複数のデータを収集・送信

  • 課題
    CO₂濃度とGPS情報を同時に測定し、効率的にクラウドへ送信する必要がある。
  • 解決策
    送信フラグを設定 し、CO₂濃度データとGPSデータを交互に送信 することで、1台のSpresenseで両方の測定・送信を実現。
    UART通信を使用するELTRESアドオンボードと、PWM通信でCO₂データを取得するMH-Z19Bを組み合わせることで、通信干渉を回避。

MATLABとAPIを活用したリアルタイムデータ解析

  • 課題
    収集したCO₂データを 即時に可視化し、一般ユーザーにも分かりやすく表示 する必要がある。
  • 解決策
    CLIP Viewer LiteのAPIを活用 し、MATLABと連携することで、CO₂データをリアルタイムで取得・解析。
    CO₂データとGPSデータを統合し、地理情報付きで解析。
    緯度・経度情報を自動変換し、マッピング可能な形式へ処理。

マッピングによる「みえる化」

  • 課題
    専門的なデータを、自治体・一般市民・研究者にとって分かりやすく表現 する必要がある。
  • 解決策
    Leaflet.jsやGoogle Maps APIを活用し、Web上で視覚化
    MATLABの地理情報機能を活用し、CO₂濃度を地図上にプロット

導入事例

測定結果_昼
測定結果_夜

都市 vs 郊外におけるCO₂濃度の比較

測定地点

  • 都市部(12か所):新宿、渋谷、池袋、銀座、六本木、皇居周辺など
  • 郊外(10か所):青梅、多摩市、八王子、稲城市、立川市など

都市部では CO₂濃度が平均 700-800 ppm で推移し、交通量や建物の密集度が影響を与えていることが確認された。
一方、郊外では CO₂濃度が平均 400-500 ppm にとどまり、緑地の割合が多いことが影響していると考えられる。
特に、都市公園(皇居周辺・代々木公園)では、都市内であっても CO₂濃度が低い傾向 にあり、緑地のCO₂吸収効果が示唆された。

昼夜のCO₂濃度の変化

測定時間

  • 昼間(午前10時~午後3時)
  • 夜間(午後9時~午前1時)

昼間はCO₂濃度が高くなる傾向(都市部では最大 850 ppm)
夜間はCO₂濃度が低下(都市部では500 ppm前後、郊外では400 ppm以下)
これは、交通量の減少・人の活動の低下 によるものと考えられる。
逆に、夜間は植物の光合成が停止するため、緑地でも昼間より若干のCO₂濃度増加が見られた。

CO₂削減効果の推定

都心部の平均
合計 = 693 + 701 + 689 + 723 + 659 + 786 + 621 + 802 + 582 + 811 + 799 + 721 = 8587
平均 = 8587 / 12 ≈ 715.6 [ppm]
郊外の平均
合計 = 589 + 509 + 498 + 484 + 621 + 439 + 455 + 521 + 443 + 450 = 5009
平均 = 5009 / 10 = 500.9 [ppm]

  • 「都心部 → 郊外レベル」まで下げられた場合
    都心平均: 約 716 ppm
    郊外平均: 約 501 ppm
    差 = 716 - 501 = 215 ppm
    もし都心部全体が“郊外並みのCO₂濃度”まで下げられたら、理想的には200 ppm超の削減効果があると推定できます。

  • 「都心部の一部を“緑地豊富な都心地点”並みにする」場合
    都心平均: 約 716 ppm
    緑地が多いと思われる都心地点(9): 582 ppm
    差 = 716 - 582 = 134 ppm
    都心部でも、充分に緑地化・通風を良くしているエリアでは 582 ppm まで低下している例があります。
    そこに近い値まで引き下げられれば、約130 ppmの削減が見込める計算です。

遠隔地からのリアルタイム監視

ELTRES通信により、遠隔地からのリアルタイムデータ送信を実現。
APIを活用し、クラウドと連携することで、Web上でのリアルタイムモニタリングを可能にする。
遠隔地のCO₂濃度を即時に確認できるシステムを構築。
WEB上で動作

コード

  • Arduino
    センサーとELTRES通信を制御するコードです。
    測定から通信まで可能です。

CO2&ELTRES

/* * mhz19b_pwm_eltres_dual_payload.ino - MH-Z19B CO2センサーのPWM通信とELTRESアドオンボードを使用するコード * GPSとCO2ペイロードのデータを順番に送信するように改良。 */ #include <EltresAddonBoard.h> // ELTRESライブラリ #include <MHZ.h> // ピン設定 #define MH_Z19B_PWM_PIN 3 // MH-Z19BのPWM出力ピンを接続するArduinoピン // PIN定義:LED(プログラム状態) #define LED_RUN PIN_LED0 // PIN定義:LED(GNSS電波状態) #define LED_GNSS PIN_LED1 // PIN定義:LED(ELTRES状態) #define LED_SND PIN_LED2 // PIN定義:LED(エラー状態) #define LED_ERR PIN_LED3 // プログラム内部状態:初期状態 #define PROGRAM_STS_INIT (0) // プログラム内部状態:起動中 #define PROGRAM_STS_RUNNING (1) // プログラム内部状態:終了 #define PROGRAM_STS_STOPPED (3) // プログラム内部状態 int program_sts = PROGRAM_STS_INIT; // GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ bool gnss_receive_timeout = false; // 点滅処理で最後に変更した時間 uint64_t last_change_blink_time = 0; // イベント通知での送信直前通知(5秒前)受信フラグ bool event_send_ready = false; // ペイロードデータ格納場所 uint8_t payload[16]; // 最新のGGA情報 eltres_board_gga_info last_gga_info; // 最新センサー値を保持する変数(グローバルで宣言する) uint16_t last_co2 = 0; float last_temp = 0.0; // 温度のデータがないため0.0を設定 float last_hum = 0.0; // 湿度のデータがないため0.0を設定 // ペイロード送信のフラグ(trueならGPSを、falseならCO2を送信する) bool send_gps_payload = true; /** * @brief イベント通知受信コールバック * @param event イベント種別 */ void eltres_event_cb(eltres_board_event event) { switch (event) { case ELTRES_BOARD_EVT_GNSS_TMOUT: // GNSS電波受信タイムアウト Serial.println("GNSS電波受信タイムアウト."); gnss_receive_timeout = true; break; case ELTRES_BOARD_EVT_IDLE: // アイドル状態 Serial.println("アイドル状態."); digitalWrite(LED_SND, LOW); break; case ELTRES_BOARD_EVT_SEND_READY: // 送信直前通知(5秒前) Serial.println("5秒後に送信が開始されます."); event_send_ready = true; break; case ELTRES_BOARD_EVT_SENDING: // 送信開始 Serial.println("送信開始."); digitalWrite(LED_SND, HIGH); break; case ELTRES_BOARD_EVT_GNSS_UNRECEIVE: // GNSS電波未受信 Serial.println("GNSS電波未受信."); digitalWrite(LED_GNSS, LOW); break; case ELTRES_BOARD_EVT_GNSS_RECEIVE: // GNSS電波受信 Serial.println("GNSS電波受信."); digitalWrite(LED_GNSS, HIGH); gnss_receive_timeout = false; break; case ELTRES_BOARD_EVT_FAULT: // 内部エラー発生 Serial.println("内部エラー発生."); break; } } /** * @brief GGA情報受信コールバック * @param gga_info GGA情報のポインタ */ void gga_event_cb(const eltres_board_gga_info *gga_info) { Serial.print("[gga]"); last_gga_info = *gga_info; if (gga_info->m_pos_status) { // 測位状態 // GGA情報をシリアルモニタへ出力 Serial.print("lat: "); Serial.print((const char *)gga_info->m_n_s); Serial.print((const char *)gga_info->m_lat); Serial.print(", lon: "); Serial.print((const char *)gga_info->m_e_w); Serial.println((const char *)gga_info->m_lon); Serial.print("pos_status: "); Serial.print(gga_info->m_pos_status); Serial.print(", sat_used: "); Serial.println(gga_info->m_sat_used); Serial.print("hdop: "); Serial.print(gga_info->m_hdop); Serial.println(); } else { // 非測位状態 Serial.println("非測位状態."); } } /** * setup関数 */ void setup() { Serial.begin(115200); pinMode(MH_Z19B_PWM_PIN, INPUT); // PWM入力ピン設定 pinMode(LED_RUN, OUTPUT); digitalWrite(LED_RUN, HIGH); pinMode(LED_GNSS, OUTPUT); digitalWrite(LED_GNSS, LOW); pinMode(LED_SND, OUTPUT); digitalWrite(LED_SND, LOW); pinMode(LED_ERR, OUTPUT); digitalWrite(LED_ERR, LOW); // ELTRES起動処理 eltres_board_result ret = EltresAddonBoard.begin(ELTRES_BOARD_SEND_MODE_1MIN,eltres_event_cb, gga_event_cb); if (ret != ELTRES_BOARD_RESULT_OK) { // ELTRESエラー発生 digitalWrite(LED_RUN, LOW); digitalWrite(LED_ERR, HIGH); program_sts = PROGRAM_STS_STOPPED; Serial.print("ELTRESを起動出来ませんでした. ("); Serial.print(ret); Serial.println(")."); } else { // 正常 program_sts = PROGRAM_STS_RUNNING; } } /** * loop関数 */ void loop() { switch (program_sts) { case PROGRAM_STS_RUNNING: if (gnss_receive_timeout) { uint64_t now_time = millis(); if ((now_time - last_change_blink_time) >= 1000) { last_change_blink_time = now_time; bool set_value = digitalRead(LED_ERR); bool next_value = (set_value == LOW) ? HIGH : LOW; digitalWrite(LED_ERR, next_value); } } else { digitalWrite(LED_ERR, LOW); } if (event_send_ready) { event_send_ready = false; // GPSペイロードとCO2ペイロードを交互に送信 if (send_gps_payload) { setup_payload_gps(); } else { setup_payload_temp_hum_co2(last_temp, last_hum, (float)last_co2); } EltresAddonBoard.set_payload(payload); send_gps_payload = !send_gps_payload; // フラグを切り替えて次回は別のペイロードを送信 } read_mhz19b_pwm(); // CO2センサーを読み取る break; case PROGRAM_STS_STOPPED: break; } delay(100); } /** * @brief GPSペイロード設定 */ void setup_payload_gps() { String lat_string = String((char*)last_gga_info.m_lat); String lon_string = String((char*)last_gga_info.m_lon); int index; // 設定情報をシリアルモニタへ出力 Serial.print("[setup_payload_gps]"); Serial.print("lat:"); Serial.print(lat_string); Serial.print(",lon:"); Serial.print(lon_string); Serial.print(",pos:"); Serial.print(last_gga_info.m_pos_status); Serial.println(); // ペイロード領域初期化 memset(payload, 0x00, sizeof(payload)); // ペイロード種別[GPSペイロード]設定 payload[0] = 0x81; // 緯度設定 index = 0; payload[1] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[2] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; index += 1; // skip "." payload[3] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[4] = (uint8_t)(((lat_string.substring(index,index+1).toInt() << 4) + lat_string.substring(index+1,index+2).toInt()) & 0xff); // 経度設定 index = 0; payload[5] = (uint8_t)(lon_string.substring(index,index+1).toInt() & 0xff); index += 1; payload[6] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[7] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; index += 1; // skip "." payload[8] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); index += 2; payload[9] = (uint8_t)(((lon_string.substring(index,index+1).toInt() << 4) + lon_string.substring(index+1,index+2).toInt()) & 0xff); // 拡張用領域(0固定)設定 payload[14] = 0x00; // 品質設定 payload[15] = last_gga_info.m_pos_status; } /** * 温度・湿度・CO2ペイロードを設定する関数 */ void setup_payload_temp_hum_co2(float temp, float hum, float co2) { memset(payload, 0x00, sizeof(payload)); // ペイロード初期化 payload[0] = 0x82; // ペイロード種別 // CO2設定 uint32_t raw = *((uint32_t*)&co2); payload[9] = (uint8_t)((raw >> 24) & 0xff); payload[10] = (uint8_t)((raw >> 16) & 0xff); payload[11] = (uint8_t)((raw >> 8) & 0xff); payload[12] = (uint8_t)((raw >> 0) & 0xff); } /** * MH-Z19BからCO2濃度をPWMで取得する関数 */ void read_mhz19b_pwm() { unsigned long highLevelTime = pulseIn(MH_Z19B_PWM_PIN, HIGH, 2000000); // 最大2秒待機 unsigned long lowLevelTime = pulseIn(MH_Z19B_PWM_PIN, LOW, 2000000); if (highLevelTime == 0) { Serial.println("[MH-Z19B] PWM読み取りエラー"); return; } const int threshold = 1500; // CO2濃度の計算 (公式: ppm = 5000 * (th - 2ms) / (th + tl - 4ms)) float period = highLevelTime + lowLevelTime; last_co2 = 5000 * (highLevelTime - 2000) / (period - 4000); if (last_co2 > threshold) { Serial.println("異常な値を検出したため30秒間待機"); delay(30000); // 30秒間待機 } Serial.print("[MH-Z19B] CO2濃度: "); Serial.print(last_co2); Serial.println(" ppm"); }
  • MATLAB
    APIを使用してClipviewerLiteからデータを取得しマッピングします。

MATLAB

% API情報 apiKey = 'YqjQk2cjhd8qDYlBnqLkm9en6Us4jywi8DXUuIDG'; username = '2210930014u@hiro.kindai.ac.jp'; password = 'Yakko1228'; deviceId = '0001019959'; % グラフの初期設定 figure('Name', 'リアルタイムCO2濃度分布'); geobasemap('grayland'); % グレーベースマップに設定 title('リアルタイムCO2濃度分布'); colorbar; colormap(jet); % 地図プロットの初期化 latitude = NaN; longitude = NaN; CO2 = NaN; scatterPlot = geoscatter(latitude, longitude, 100, CO2, 'filled'); % カラーマップ設定 caxis([400, 800]); % 想定されるCO2濃度範囲での色スケール設定 % 関数: トークン取得 function token = getToken(apiKey, username, password) url = 'https://api.clip-viewer-lite.com/auth/token'; headers = weboptions('HeaderFields', {'X-API-Key', apiKey}, 'MediaType', 'application/json'); body = jsonencode(struct('username', username, 'password', password)); try response = webwrite(url, body, headers); token = response.token; catch ME disp('トークン取得失敗'); disp(ME.message); token = ''; end end % 関数: 最新ペイロード取得 function payloadData = getLatestPayload(apiKey, token, deviceId) url = ['https://api.clip-viewer-lite.com/payload/latest/', deviceId]; headers = weboptions('HeaderFields', {'X-API-Key', apiKey; 'Authorization', token}); try payloadData = webread(url, headers); catch ME disp('ペイロード取得失敗'); disp(ME.message); payloadData = []; end end % 定期的にデータを取得・表示するループ while true % トークンを取得 token = getToken(apiKey, username, password); if isempty(token) pause(60); % トークン取得失敗時の待機 continue; end % 最新のペイロードデータを取得 payloadData = getLatestPayload(apiKey, token, deviceId); if isempty(payloadData) || isempty(payloadData.payload) pause(60); % ペイロード取得失敗時の待機 continue; end % ペイロードデータの解析 gpsData = payloadData.payload.gps; co2Data = payloadData.payload.carbonDioxide; if isempty(gpsData) || isempty(co2Data) pause(60); continue; end % 緯度・経度を取得して度単位に変換 gpsString = string(gpsData); split_gps = split(gpsString, ' '); % 緯度の計算 lat_deg = floor(str2double(split_gps(1)) / 100); lat_min = mod(str2double(split_gps(1)), 100); latitude = lat_deg + (lat_min / 60); % 経度の計算 lon_deg = floor(str2double(split_gps(2)) / 100); lon_min = mod(str2double(split_gps(2)), 100); longitude = lon_deg + (lon_min / 60); % CO2濃度の取得 CO2 = co2Data; % 地図プロットを更新 set(scatterPlot, 'LatitudeData', latitude, 'LongitudeData', longitude, 'CData', CO2); % カラーマップ更新 colorbar; if min(CO2) == max(CO2) caxis([min(CO2) - 1, max(CO2) + 1]); else caxis([min(CO2), max(CO2)]); end drawnow; % 1分間待機してデータ再取得 pause(60); end

最後に

本製品を作成するうえで最も苦労したのは、MATLABの使用方法でした。
その際、建築学科で人の流動シミュレーションを行っている友人の力を借りることで、なんとか形にすることができました。
学びの形は人それぞれであり、幅広い知識を身につけることも、一つの分野に没頭することも、それぞれ成長の方法だと感じています。しかし、本製品の作成を通じて、人と人とが協力し合うことで、個人では達成が難しかったことも可能になると実感しました。
本コンテストやSensing Solution ハッカソン 2024も、同様に大きな成長の機会を提供していただいたことに感謝しています。
また、来年度も機会があれば、ぜひ挑戦したいと考えています。

1
2
ログインしてコメントを投稿する