はじめに
LILYGO T-7080G-S3ボードにPIR人感センサーを接続し、SORACOMのLTE-M通信のSIMにより、MQTT通信でさくらのVPSに10分単位でPIRセンサー感知回数データを送る手順について記録します。
このボードはLTE-Mモジュール(SIM7080G)とESP32-S3を搭載しており、WiFi通信ができない場所でも通信できることが特徴です。WiFi電波が飛んでない家に住む一人暮らしの老人の見守りを意識して作りました。
システム構成
- マイコンボード: LILYGO T-SIM7080G-S3 (ESP32-S3 + SIM7080G)
- センサー: PIRセンサー(M5Stack社) (PIR grove端子外側(IN)をボードのGPIO14に接続)
- 通信回線: SORACOM SIM (LTE-M / plan-D / nanoSIM)
- プロトコル: MQTT
- サーバー: さくらのVPS (MQTTブローカーMosquittoと時系列データベースInfluxDB+Grafanaが稼働中 参考:https://qiita.com/MikH/items/9acea2aa11a52b18589a
- 電源: 18650充電式リチウムリチウムイオン電池装着+USBケーブル(ACアダプター)接続
プログラムの解説
このシステムを実現するためのArduinoスケッチの主要部分を解説します。
1. ライブラリと定義
まず、必要なライブラリを読み込みます。このボード特有の電源管理チップ(PMU)であるAXP2101を制御するために XPowersLib が必要です。また、LTEモジュール制御には TinyGsmClient を使用します。
#define XPOWERS_CHIP_AXP2101
#include <XPowersLib.h>
#include <Arduino.h>
#include <ArduinoJson.h>
// TinyGSMの設定
#define TINY_GSM_RX_BUFFER 1024
#define TINY_GSM_MODEM_SIM7080
#include <TinyGsmClient.h>
#include <PubSubClient.h>
2. ピン定義とオブジェクト作成
ボードの回路図に基づき、モデムとの通信ピンやPMUのI2Cピンを定義します。PIRセンサーは今回はGPIO 14を使用しています。
// Pin Definitions
#define PMU_SDA 15
#define PMU_SCL 7
#define MODEM_TX 5
#define MODEM_RX 4
#define MODEM_PWRKEY 41
// ... (中略)
#define PIR_PIN 14
XPowersPMU PMU;
TinyGsm modem(Serial1);
TinyGsmClient client(modem, 0);
PubSubClient mqtt(client);
3. 電源管理 (PMU) の初期化
このボードで最も重要なのが電源管理です。SIM7080GモジュールやGPSへ電力を供給するために、PMU(Power Management Unit)の設定を行う必要があります。これを忘れるとモデムが起動しません。
// PMUの初期化
Wire.begin(PMU_SDA, PMU_SCL);
if (!PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, PMU_SDA, PMU_SCL)) {
Serial.println("Failed to initialize power.....");
while (1) delay(5000);
}
// ALDO/BLDOの設定(ボードの仕様に合わせて各電圧を設定)
PMU.setDC3Voltage(3000);
PMU.enableDC3();
// モデムとGPSへの給電をONにする
PMU.setBLDO2Voltage(3300); PMU.enableBLDO2();
PMU.setBLDO1Voltage(3300); PMU.enableBLDO1();
4. モデムの起動と接続の初期化
モデムの電源を入れる物理的な操作(PWRKEYの操作)をシミュレートし、ATコマンドに応答するまで待機します。
// モデムのPWRKEY等を設定
pinMode(MODEM_PWRKEY, OUTPUT);
// ...
// モデムが応答するまでリトライ
while (!modem.testAT(1000)) {
Serial.print(".");
if (retry++ > 6) {
// 応答がない場合はPWRKEYをパルス駆動して再起動を試みる
digitalWrite(MODEM_PWRKEY, LOW);
delay(100);
digitalWrite(MODEM_PWRKEY, HIGH);
delay(1000);
digitalWrite(MODEM_PWRKEY, LOW);
// ...
}
}
5. SORACOMへの接続設定
SORACOM Airを使用するためのAPN設定と、LTE-M (Cat-M) のネットワークモード設定を行います。
// ネットワーク設定をリセットするために一度RFをオフ
modem.sendAT("+CFUN=0"); modem.waitResponse(20000UL);
// Cat-M / LTE Only モードに設定
modem.setNetworkMode(38);
modem.setPreferredMode(1);
// APN設定 (soracom.io / sora / sora)
modem.sendAT("+CGDCONT=1,\"IP\",\"", apn, "\""); modem.waitResponse();
modem.sendAT("+CNCFG=0,1,\"", apn, "\""); modem.waitResponse();
// 電波をオン
modem.sendAT("+CFUN=1"); modem.waitResponse(20000UL);
この後、AT+CNACT=0,1 コマンド等でデータ通信をアクティブにします。
6. PIRセンサーの割り込み処理
PIRセンサーが反応した回数を取りこぼさずに数えるため、割り込み処理 (Interrupt) を使用しています。
// センサー反応回数を保持する変数(割り込み内で操作するためvolatile)
volatile unsigned long pirCount = 0;
// 割り込みハンドラ (IRAM_ATTRをつけてRAM上に配置推奨)
void IRAM_ATTR pirISR() {
pirCount++;
}
// setup()内での設定
pinMode(PIR_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirISR, RISING);
センサーが RISING(LoからHiになった瞬間)を検知すると、pirISR 関数が呼ばれてカウンタが増えます。
7. 定期的なデータ送信
メインループでは、MQTTの接続維持 (mqtt.loop()) を行いながら、設定した時間(10分)ごとにデータを送信します。送信データはJSON形式に整形します。
unsigned long now = millis();
if (now - lastMsg > interval) {
lastMsg = now;
// カウンタの値を読み出してリセット(割り込み禁止期間を設けて安全に)
noInterrupts();
unsigned long count = pirCount;
pirCount = 0;
interrupts();
// MQTT接続確認と再接続処理
if (!modem.isGprsConnected()) { /* ... */ }
if (!mqtt.connected()) { mqttReconnect(); }
// ArduinoJsonでJSONを作成
StaticJsonDocument<200> doc;
doc["count"] = count;
doc["device"] = device_name;
doc["signal"] = modem.getSignalQuality(); // 電波強度も送信
doc["batt_mv"] = PMU.getBattVoltage(); // バッテリー電圧も送信
char buffer[256];
serializeJson(doc, buffer);
// 送信
mqtt.publish(topic, buffer);
}
コード
/*
Final_PIR_MQTT.ino
LILYGO T-SIM7080G-S3 PIR Sensor Data Sender
Based on:
1. Qiita Article (Connectivity Logic) - PROVEN WORKING
2. Original PIR Sketch (Application Logic)
HARDWARE:
- Li-Po Battery REQUIRED (USB only will fail).
- PIR Sensor Pin: 14 (Check your wiring! If 14 doesn't work, try 48).
*/
#define XPOWERS_CHIP_AXP2101
#include <XPowersLib.h>
#include <Arduino.h>
#include <ArduinoJson.h>
// Macros for TinyGSM
#define TINY_GSM_RX_BUFFER 1024
#define TINY_GSM_MODEM_SIM7080
#include <TinyGsmClient.h>
#include <PubSubClient.h>
// Pin Definitions
#define PMU_SDA 15
#define PMU_SCL 7
#define MODEM_TX 5
#define MODEM_RX 4
#define MODEM_PWRKEY 41
#define MODEM_DTR 42
#define MODEM_RI 3
// PIR Pin (Matches original sketch define)
#define PIR_PIN 14
XPowersPMU PMU;
TinyGsm modem(Serial1);
TinyGsmClient client(modem, 0);
PubSubClient mqtt(client);
// Connection Config
const char apn[] = "soracom.io";
const char gprsUser[] = "sora";
const char gprsPass[] = "sora";
const char* broker = "xxx.xxx.xxx.xxx"; // Sakura VPSのIPを入力
const int port = 1883;
const char* topic = "sensor/lilygo_pir";
const char* device_name = "LILYGO-T-SIM7080G-S3";
// Variables
volatile unsigned long pirCount = 0;
unsigned long lastMsg = 0;
const unsigned long interval = 600000; // 10 minutes
// Interrupt
void IRAM_ATTR pirISR() {
pirCount++;
}
void setup() {
Serial.begin(115200);
// Wait for Serial optional, but good for debug
delay(3000);
Serial.println("\n--- Final PIR MQTT Start ---");
/*********************************
* POWER MANAGEMENT (Qiita Method)
***********************************/
Wire.begin(PMU_SDA, PMU_SCL);
if (!PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, PMU_SDA, PMU_SCL)) {
Serial.println("Failed to initialize power.....");
while (1) delay(5000);
}
// Set DC3 to 3000mV (Key setting from Qiita)
PMU.setDC3Voltage(3000);
PMU.enableDC3();
// Enable Modem & GPS Power
PMU.setBLDO2Voltage(3300); PMU.enableBLDO2();
PMU.setBLDO1Voltage(3300); PMU.enableBLDO1();
// Disable TS Pin
PMU.disableTSPinMeasure();
// Check Battery
int batt = PMU.getBattVoltage();
Serial.printf("Battery Voltage: %dmV\n", batt);
if (batt < 3300) {
Serial.println("!!! WARNING: Battery Low or Missing. Connection may fail !!!");
}
/*********************************
* MODEM STARTUP
***********************************/
Serial1.begin(115200, SERIAL_8N1, MODEM_RX, MODEM_TX);
pinMode(MODEM_PWRKEY, OUTPUT);
pinMode(MODEM_DTR, OUTPUT);
pinMode(MODEM_RI, INPUT);
// Wake Modem (Qiita Pulse Sequence)
Serial.println("Pulsing Modem...");
int retry = 0;
while (!modem.testAT(1000)) {
Serial.print(".");
if (retry++ > 6) {
digitalWrite(MODEM_PWRKEY, LOW);
delay(100);
digitalWrite(MODEM_PWRKEY, HIGH);
delay(1000);
digitalWrite(MODEM_PWRKEY, LOW);
retry = 0;
Serial.println("Retry start modem...");
}
}
Serial.println("\nModem Started!");
/*********************************
* NETWORK SETUP
***********************************/
modem.sendAT("+CFUN=0"); modem.waitResponse(20000UL); // Disable RF to config
// CAT-M / LTE Only (Qiita Config)
modem.setNetworkMode(38);
modem.setPreferredMode(1);
// APN Config
modem.sendAT("+CGDCONT=1,\"IP\",\"", apn, "\""); modem.waitResponse();
modem.sendAT("+CNCFG=0,1,\"", apn, "\""); modem.waitResponse();
modem.sendAT("+CFUN=1"); modem.waitResponse(20000UL); // Enable RF
Serial.println("Waiting for Registration...");
int16_t s; // Use int16_t to avoid RegStatus error
do {
s = modem.getRegistrationStatus();
if (s != 1 && s != 5) { Serial.print("."); delay(1000); }
} while (s != 1 && s != 5);
Serial.println("\nRegistered!");
// Activate Context
modem.sendAT("+CNACT=0,1"); modem.waitResponse();
if (modem.isGprsConnected()) {
Serial.println("GPRS Connected! IP: " + modem.getLocalIP());
} else {
Serial.println("GPRS Not showing connected yet, but proceeding...");
}
// MQTT Setup
mqtt.setServer(broker, port);
// PIR Setup
pinMode(PIR_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirISR, RISING);
// Force first publish
lastMsg = millis() - interval - 1000;
}
void mqttReconnect() {
while (!mqtt.connected()) {
Serial.println("Attempting MQTT connection...");
String clientId = "LILYGO-Client-" + String(random(0xffff), HEX);
if (mqtt.connect(clientId.c_str())) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(mqtt.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void loop() {
if (!mqtt.connected()) {
// Only verify network is still alive
if (!modem.isNetworkConnected()) {
Serial.println("Network lost, checking...");
}
// Only connect if we need to? Or keep alive?
// Publisher usually connects when needed or keeps open.
// We will try to keep open here for simplicity, or reconnect in the send block.
// Let's just put mqtt.loop() here and handle reconnect if fails.
}
mqtt.loop();
unsigned long now = millis();
if (now - lastMsg > interval) {
lastMsg = now;
// Snapshot count
noInterrupts();
unsigned long count = pirCount;
pirCount = 0;
interrupts();
Serial.print("PIR Count: "); Serial.println(count);
// Check/Restablish Connection
if (!modem.isGprsConnected()) {
Serial.println("Re-activating GPRS...");
modem.sendAT("+CNACT=0,1"); modem.waitResponse();
}
if (!mqtt.connected()) {
mqttReconnect();
}
// Create JSON
StaticJsonDocument<200> doc;
doc["count"] = count;
doc["device"] = device_name;
doc["signal"] = modem.getSignalQuality();
doc["batt_mv"] = PMU.getBattVoltage();
char buffer[256];
serializeJson(doc, buffer);
Serial.print("Publishing: "); Serial.println(buffer);
if (mqtt.publish(topic, buffer)) {
Serial.println("Published!");
} else {
Serial.println("Publish Failed.");
}
}
}
データを可視化
クラウド(さくらのVPS)に送られたデータを、Grafanaで可視化しました。
(ある一定条件でアラート送信を設定する予定)
参考:https://qiita.com/MikH/items/9acea2aa11a52b18589a
まとめ
以上のコードにより、LILYGO T-SIM7080G-S3上のPIRセンサーデータをLTE-M回線経由で定期的にクラウド(VPS)へ送信するシステムが構築できました。
バッテリー電圧や電波強度CSQ (Cellular Signal Quality)値(15以上で良好 20以上で強い 99は測定不能圏外の意味)も同時に送ることで、遠隔地での運用状態も監視できるようにしています。
参考資料
以下のコードおよび記事を参考にしました。
-
LILYGO 公式サンプルコード:
Github: Xinyuan-LilyGO/LilyGo-T-SIM7080G
特にexamples/ModemMqttPublishExampleおよびMinimalModemNBIOTExampleのモデム制御部分をベースにしています。
また、本ボード特有の電源管理(PMU)にはXPowersLibを使用しており、公式の推奨設定に従っています。 -
Qiita: ESP32-S3-WROOM1とSIM7080G(LILYGO T-SIM7080G-S3)でIOT開発(2.9日目):
https://qiita.com/noco/items/19a62fe931caa02b3f88
特に、SORACOM接続時のPMU設定(setDC3Voltage(3000))やモデムの初期化シーケンスにおいて、こちらの記事の知見が大変役立ちました。

