製作概要
Sony Spresense と ELTRES アドオンボードを使い、取得した温湿度をクラウドに送信。数値の見える化を行いました。
また、提供されているAPIを使用して情報をメールで受信できるようにしてみました。
必要なもの
ハードウェア
- Spresense メインボード
- ELTRES アドオンボード
- DHT20 温湿度センサー
- ブレッドボード & ジャンパーワイヤー
ソフトウェア
- Arduino IDE(バージョン2.3.4で実施)
- ELTRES アドオンボード用ライブラリ(1.3.0)
- Adafruit AHTX0 ライブラリ
全体のイメージ図
使用するセンサについて
今回はセンサとしてDHT20を使用しました。
類似品としてDHT11というセンサもあります。
基本的には同じものですがセンサ精度がDHT20の方が良いものとなっています。
項目 | DHT11 | DHT20 |
---|---|---|
測定範囲(温度) | 0〜50°C | -40〜80°C |
測定範囲(湿度) | 20〜90% RH | 0〜100% RH |
精度(温度) | ±2°C | ±0.3°C |
精度(湿度) | ±5% RH | ±3% RH |
製作
回路の製作
↓配線した感じです。一部3Dプリンタで土台を作り、固定できるようにしました。
動作プログラムについて
ライブラリの追加
使用するライブラリについては、
Arduino IDE の「ツール」->「ライブラリの管理」を開いて入手していきます。
また、ELTRES アドオンボードについては、CLIP Viewer Liteコンテンツページにライブラリの記載がありますのでそこから入手を行いました。
Spresenseのソースコード
サンプルコードをもとに使用するセンサの情報を追記して組み立てていきました。
センサ情報の取得とデータ送信
#include <EltresAddonBoard.h>
// 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)
// DHT20(温湿度センサ)設定
#include <Adafruit_AHTX0.h> // DHT20用ライブラリ
Adafruit_AHTX0 dht20;
//確認用LEDのピン
#define SIGNAL_PIN1 22
// 直前の温度データ
float lastTemperature = 0.0;
// 測定間隔(1分ごと)
#define MEASURE_INTERVAL 60000
unsigned long lastMeasureTime = 0;
// プログラム内部状態
int program_sts = PROGRAM_STS_INIT;
// GNSS電波受信タイムアウト(GNSS受信エラー)発生フラグ
bool gnss_recevie_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;
// 初期値の設定
float last_temp = 0; // 最新値(温度)
float last_hum = 0; // 最新値(湿度)
uint16_t last_co2 = 0; // 最新値(CO2濃度)今回は使用なし
/**
* @brief イベント通知受信コールバック
* @param event イベント種別
*/
void eltres_event_cb(eltres_board_event event) {
switch (event) {
case ELTRES_BOARD_EVT_GNSS_TMOUT:
// GNSS電波受信タイムアウト
Serial.println("gnss wait timeout error.");
gnss_recevie_timeout = true;
break;
case ELTRES_BOARD_EVT_IDLE:
// アイドル状態
Serial.println("waiting sending timings.");
digitalWrite(LED_SND, LOW);
break;
case ELTRES_BOARD_EVT_SEND_READY:
// 送信直前通知(5秒前)
Serial.println("Shortly before sending, so setup payload if need.");
event_send_ready = true;
break;
case ELTRES_BOARD_EVT_SENDING:
// 送信開始
Serial.println("start sending.");
digitalWrite(LED_SND, HIGH);
break;
case ELTRES_BOARD_EVT_GNSS_UNRECEIVE:
// GNSS電波未受信
Serial.println("gnss wave has not been received.");
digitalWrite(LED_GNSS, LOW);
break;
case ELTRES_BOARD_EVT_GNSS_RECEIVE:
// GNSS電波受信
Serial.println("gnss wave has been received.");
digitalWrite(LED_GNSS, HIGH);
gnss_recevie_timeout = false;
break;
case ELTRES_BOARD_EVT_FAULT:
// 内部エラー発生
Serial.println("internal error.");
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("utc: ");
Serial.println((const char *)gga_info->m_utc);
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.print(", height: ");
Serial.print(gga_info->m_height);
Serial.print(" m, geoid: ");
Serial.print(gga_info->m_geoid);
Serial.println(" m");
} else {
// 非測位状態
// "invalid data"をシリアルモニタへ出力
Serial.println("invalid data.");
}
}
/**
* @brief setup()関数
*/
void setup() {
// シリアルモニタ出力設定
Serial.begin(115200);
Serial.println("initialize start.");
Serial.println("Serial OK. -->> NEXT ELTRES init");
Wire.begin();
// LED初期設定
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);
pinMode(SIGNAL_PIN1, OUTPUT);
digitalWrite(SIGNAL_PIN1, LOW);
delay(30);
// 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("cannot start eltres board (");
Serial.print(ret);
Serial.println(").");
} else {
// 正常
program_sts = PROGRAM_STS_RUNNING;
}
Serial.println("-->> NEXT DHt20 init");
// DHT20の初期化
if (!dht20.begin()) {
Serial.println("DHT20センサが見つかりません。接続を確認してください。");
while (1) delay(10);
}
Serial.println("DHT20センサが見つかりました。");
Serial.println("Initialization completed. Entering main loop.");
}
/**
* @brief loop()関数
*/
void loop() {
switch (program_sts) {
case PROGRAM_STS_RUNNING:
// プログラム内部状態:起動中
if (gnss_recevie_timeout) {
// GNSS電波受信タイムアウト(GNSS受信エラー)時の点滅処理
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;
setup_payload_gps(); //GPS情報の送信
setup_payload_temp_hum_co2(last_temp, last_hum, (float)last_co2); //温度湿度データの送信
// 送信ペイロードの設定
EltresAddonBoard.set_payload(payload);
}
//センサからのデータの更新
measure_scd41();
break;
case PROGRAM_STS_STOPPED:
// プログラム内部状態:終了
break;
}
// 次のループ処理まで100ミリ秒待機
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;
uint32_t gnss_time;
uint32_t utc_time;
// GNSS時刻(epoch秒)の取得
EltresAddonBoard.get_gnss_time(&gnss_time);
// UTC時刻を計算(閏秒補正)
utc_time = gnss_time - 18;
// 設定情報をシリアルモニタへ出力
Serial.print("[setup_payload_gps]");
Serial.print("lat:");
Serial.print(lat_string);
Serial.print(",lon:");
Serial.print(lon_string);
Serial.print(",utc:");
Serial.print(utc_time);
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);
// 時刻(EPOCH秒)設定
payload[10] = (uint8_t)((utc_time >> 24) & 0xff);
payload[11] = (uint8_t)((utc_time >> 16) & 0xff);
payload[12] = (uint8_t)((utc_time >> 8) & 0xff);
payload[13] = (uint8_t)(utc_time & 0xff);
// 拡張用領域(0固定)設定
payload[14] = 0x00;
// 品質設定
payload[15] = last_gga_info.m_pos_status;
}
/**
* @brief 温度・湿度・CO2 ペイロード設定
* @param temp 温度
* @param hum 湿度
* @param co2 CO2濃度
*/
void setup_payload_temp_hum_co2(float temp, float hum, float co2) {
// 設定情報をシリアルモニタへ出力
Serial.print("[setup_payload_temp_hum_co2]");
Serial.print("tem:");
Serial.print(temp, 6);
Serial.print(",hum:");
Serial.print(hum, 6);
Serial.print(",co2:");
Serial.print(co2);
Serial.println();
// ペイロード領域初期化
memset(payload, 0x00, sizeof(payload));
// ペイロード種別[温度・湿度・CO2ペイロード]設定
payload[0] = 0x82;
// 温度設定
uint32_t raw;
raw = *((uint32_t *)&temp);
payload[1] = (uint8_t)((raw >> 24) & 0xff);
payload[2] = (uint8_t)((raw >> 16) & 0xff);
payload[3] = (uint8_t)((raw >> 8) & 0xff);
payload[4] = (uint8_t)((raw >> 0) & 0xff);
// 湿度設定
raw = *((uint32_t *)&hum);
payload[5] = (uint8_t)((raw >> 24) & 0xff);
payload[6] = (uint8_t)((raw >> 16) & 0xff);
payload[7] = (uint8_t)((raw >> 8) & 0xff);
payload[8] = (uint8_t)((raw >> 0) & 0xff);
// CO2設定
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);
}
/**
* @brief SCD41から温度、湿度、CO2濃度を取得し、最新値を更新
*/
void measure_scd41() {
unsigned long currentTime = millis();
/*uint16_t error_scd4x;
bool data_ready_flag;
uint16_t co2;
float temp;
float hum;
error_scd4x = scd4x.getDataReadyFlag(data_ready_flag);
if (error_scd4x != NoError) {
Serial.print("cannot get data ready status (");
Serial.print(error_scd4x);
Serial.println(")");
return;
}
if (data_ready_flag == false) {
// センサの測定待ち
return;
}
error_scd4x = scd4x.readMeasurement(co2, temp, hum);
if (error_scd4x != NoError) {
Serial.print("cannot read measurement (");
Serial.print(error_scd4x);
Serial.println(")");
return;
}*/
if (currentTime - lastMeasureTime >= MEASURE_INTERVAL) {
lastMeasureTime = currentTime;
// 温度・湿度を取得
sensors_event_t humidity, temp;
dht20.getEvent(&humidity, &temp);
float temperature = temp.temperature;
float humidity_value = humidity.relative_humidity;
// シリアルに出力
Serial.print("温度: ");
Serial.print(temperature);
Serial.println(" °C");
Serial.print("湿度: ");
Serial.print(humidity_value);
Serial.println(" %");
// 最新値の更新
last_co2 = 0;
last_temp = temperature;
last_hum = humidity_value;
}
}
メール送信プログラム
次に提供されているAPIを使用したメール送信プログラムです。
ここでは、温湿度情報をメール受信するようにしてみました。
以下のソースコードは、jupyternotebook(Python環境)を使用したメール送信プログラムです。
APIを用いたメール送信プログラム
import requests
import smtplib
import json
import numpy as np
import matplotlib.pyplot as plt
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
import time
# 設定
API_KEY = "ここに発行済みのAPIキーを入力"
USERNAME = "登録メールアドレス"
PASSWORD = "登録パスワード"
DEVICE_ID = "デバイスID"
MAIL_SERVER = "smtp.gmail.com" # Gmailを使用する場合
MAIL_PORT = 587 #gmailのポート番号
MAIL_USERNAME = "送信に使用するメールアドレス"
MAIL_PASSWORD = アプリパスワード"
MAIL_TO = "受信用のメールアドレス"
SEND_INTERVAL = 600 # 送信間隔(秒で記載)(600=10分)
# 過去のデータを保存(最大10回まで)
history = {
"temperature": [],
"humidity": [],
"co2": [],
"timestamps": []
}
def get_rssi_quality(rssi):
"""RSSIの強度を判定"""
if rssi is None:
return "不明"
elif rssi >= 4:
return "📶 強い"
elif rssi >= 2:
return "📡 普通"
else:
return "⚠️ 弱い"
def get_api_token():
"""APIトークンを取得"""
url = "https://api.clip-viewer-lite.com/auth/token"
headers = {"X-API-Key": API_KEY}
data = {"username": USERNAME, "password": PASSWORD}
response = requests.post(url, headers=headers, json=data)
if response.status_code == 200:
return response.json().get("token")
else:
print("APIトークン取得エラー:", response.text)
return None
def get_latest_payload(token):
"""最新のペイロードデータを取得"""
url = f"https://api.clip-viewer-lite.com/payload/latest/{DEVICE_ID}"
headers = {
"X-API-Key": API_KEY,
"Authorization": f"Bearer {token}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
print("ペイロード取得エラー:", response.text)
return None
def update_history(data):
"""過去のデータを更新(最大10回分保存)"""
if len(history["temperature"]) >= 10:
history["temperature"].pop(0)
history["humidity"].pop(0)
history["co2"].pop(0)
history["timestamps"].pop(0)
history["temperature"].append(data.get("temperature", "N/A"))
history["humidity"].append(data.get("humidity", "N/A"))
history["co2"].append(data.get("carbonDioxide", "N/A"))
history["timestamps"].append(data.get("sendDateTime", "不明"))
def generate_graph():
"""温度・湿度・CO2の折れ線グラフを作成"""
plt.figure(figsize=(8, 5))
timestamps = history["timestamps"]
x_labels = np.arange(len(timestamps))
temp_data = [t if isinstance(t, (int, float)) else None for t in history["temperature"]]
hum_data = [h if isinstance(h, (int, float)) else None for h in history["humidity"]]
co2_data = [c if isinstance(c, (int, float)) and c > 0 else None for c in history["co2"]]
if any(temp_data):
plt.plot(x_labels, temp_data, marker='o', linestyle='-', label="温度 (°C)")
if any(hum_data):
plt.plot(x_labels, hum_data, marker='s', linestyle='-', label="湿度 (%)")
if any(co2_data):
plt.plot(x_labels, co2_data, marker='^', linestyle='-', label="CO2 (ppm)")
plt.xticks(x_labels, timestamps, rotation=45)
plt.xlabel("時間")
plt.ylabel("測定値")
plt.title("過去10分のデータ推移")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig("graph.png")
plt.close()
def format_payload(payload):
"""データを見やすくフォーマット"""
if not payload.get("payload"):
return "データがありません。"
data = payload["payload"][0]
update_history(data)
device_id = data.get("deviceId", "不明")
datetime = data.get("sendDateTime", "不明")
temperature = data.get("temperature")
humidity = data.get("humidity")
co2 = data.get("carbonDioxide")
rssi = data.get("rssi", "N/A")
rssi_quality = get_rssi_quality(rssi)
gps = data.get("gps")
latitude, longitude, map_link = None, None, ""
if gps and "latitude" in gps and "longitude" in gps:
latitude, longitude = gps["latitude"], gps["longitude"]
map_link = f'<a href="https://www.google.com/maps?q={latitude},{longitude}" target="_blank">📍 Google Mapsで確認</a>'
formatted_temp = f"{temperature:.2f}" if isinstance(temperature, (int, float)) else "N/A"
formatted_hum = f"{humidity:.2f}" if isinstance(humidity, (int, float)) else "N/A"
formatted_co2 = f"{co2}" if isinstance(co2, (int, float)) and co2 > 0 else "N/A"
email_body = f"""
<html>
<body>
<h2>📡 CLIP Viewer Lite データ受信 📡</h2>
<p><strong>📅 送信日時:</strong> {datetime}</p>
<p><strong>📟 デバイスID:</strong> {device_id}</p>
<p><strong>📡 通信ライン:</strong> {rssi_quality} (RSSI: {rssi})</p>
<h3>📊 測定データ</h3>
<p>🌡️ <strong>温度:</strong> {formatted_temp} °C</p>
<p>💧 <strong>湿度:</strong> {formatted_hum} %</p>
{f"<p>💨 <strong>CO2濃度:</strong> {formatted_co2} ppm</p>" if co2 not in ["N/A", None] else ""}
<h3>📍 位置情報</h3>
{map_link if latitude and longitude else '<p>位置情報なし</p>'}
</body>
</html>
"""
return email_body
def send_email(subject, body):
"""HTMLメール + グラフ画像を送信"""
msg = MIMEMultipart()
msg["Subject"] = subject
msg["From"] = MAIL_USERNAME
msg["To"] = MAIL_TO
msg.attach(MIMEText(body, "html"))
with open("graph.png", "rb") as img:
img_attachment = MIMEImage(img.read(), name="graph.png")
msg.attach(img_attachment)
server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT)
server.starttls()
server.login(MAIL_USERNAME, MAIL_PASSWORD)
server.sendmail(MAIL_USERNAME, MAIL_TO, msg.as_string())
server.quit()
def main():
token = get_api_token()
payload = get_latest_payload(token)
if payload:
email_body = format_payload(payload)
generate_graph()
send_email("📡 最新データ通知", email_body)
def main_loop():
while True:
main()
print(f"⌛ 次の送信まで {SEND_INTERVAL} 秒待機...")
time.sleep(SEND_INTERVAL)
if __name__ == "__main__":
main_loop()
上記プログラムのうち、この部分
記入の補足
# 設定
API_KEY = "ここに発行済みのAPIキーを入力"
USERNAME = "登録メールアドレス"
PASSWORD = "登録パスワード"
DEVICE_ID = "デバイスID"
MAIL_SERVER = "smtp.gmail.com" # Gmailを使用する場合
MAIL_PORT = 587 #gmailのポート番号
MAIL_USERNAME = "送信に使用するメールアドレス"
MAIL_PASSWORD = "アプリパスワード"
MAIL_TO = "受信用のメールアドレス"
は情報を書き換えてください。
Gmailの場合はアプリやデバイスからアクセスできるようにする 16 桁のパスコード(2段階認証アプリパスワード)が必要になりました。
このあたりが少々分かりづらかったので、URLとキャプチャー画像を添付します。
下のリンク先から説明ページにジャンプします。
アプリ パスワードでログインする - Gmail ヘルプ
↓アプリ名を決める。(ここでは、CLIP Viewer Liteとした)
↓パスワードが表示されます。これをプログラムにコピペします。
実行結果
まずは、Spresenseからの送信について。
写真のようにデータの送信ができおり、クラウド上に温湿度の情報が反映。
目標としていた温湿度の可視化を行うことができました。
メールについても指定時間ごとに送信+一緒に温度グラフも送られることを確認できました。
↓メール送信プログラム実行中の様子
まとめ、謝辞
今回はSpresense と ELTRES を用いて温度、湿度を測定しクラウドに送信。併せてメールでも情報を共有するという事を行ってみました。
クラウドへの送信はWi-Fiでの送信が一般的ですが、ELTRES通信機能によって行うことができるという、今までとは違った手法は新鮮な感じでした。安定的、低消費電力などメリットが多いELTRES通信。手探りで行っていた部分も多いため、もっと触れてみたいと感じました。
これからますますIoT化が盛んになっていきます。その中で、どのようにデータを取得していくのかが重要だろうと製作を行う中で考えさせられました。温湿度以外にも送れる情報は多くあるので、試してみたいと思います。
終わりに、本コンテストにあたり機材提供をしていただいたこと、いろいろご質問に対応していたことなど支えられる場面が多くありました。
この場を借りて、お礼申し上げます。
投稿者の人気記事
-
Fuka
さんが
2025/02/12
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する