ESP-NOWを使ってGateWay方式でSORACOMへ送信する～機械のIoT化を低コストで

##動機
私は稲作農家でコンバインで収穫した籾を穀物乾燥機で水分を乾燥させる必要があります。
日中収穫し夕方までに乾燥機内に張り込んで、夜通し灯油を燃やした熱風を当てて乾かします。
ほとんどは設定すれば翌朝の乾燥終了まで自動で行われますが、なにかトラブルがあると米の品質にも影響がありますし翌日の作業にも支障があります。
そこで夜に確認しに機械を見て廻ることがあり、こういった作業や異常警報を素速くわかるようにIoT化しようと思いました。

##構成

機械の稼働状況と警報をリモートで確認したい。
ただし複数あるので個々にSimを挿してではコストがかかる。
かといって有線を張り巡らすのは邪魔。

そこでESP32を一台を親機として複数の子機から送られてくるデータを無線で集めて、SORACOMに送信するゲートウェイ方式を考えました。
IoTデバイスはLTE-M Shield for ArduinoとESP32開発ボード、子機からの送信方式はEspressif独自の通信方式のESP-NOWが使いやすそうなので採用しました。仕様としては子機の数は20個以下payloadは250Byte以下の制約はありますが、使ってみた感じでは到達距離も問題なさそう。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/14c16f55-58bb-9cfe-b003-682573dc70ce.png)


##送信（子機　Sender）
温度センサーは[OneWire　DS18B20](https://www.amazon.co.jp/gp/product/B083TTCB9Q/) 
モーターのオンオフは電磁開閉器の補助接点から、
警報はDC12Vブザーの配線を分岐してフォトカプラを介して取りました。

![IMG_20211105_163810.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/ad7cffd1-3f2b-d8d2-24dc-c269a6c5e4d5.jpeg)



ESP-NOWのスケッチはこちらを参考にしました。

https://randomnerdtutorials.com/esp-now-esp32-arduino-ide/

[親機のMacアドレスを予め調べておき](https://randomnerdtutorials.com/get-change-esp32-esp8266-mac-address-arduino/)ペアさせます
送信データは構造体にして複数の数値を送ります

```
typedef struct struct_message {
  char name[4];//機体名
  uint8_t id;//機体ID
  uint8_t motorStatus;//モーター状態
  float temp;//温度
} struct_message;
```

motorStatus　は8bitのうち5bitで以下のON/OFFの状態を表しています

|bit5|bit4|bit3|bit2|bit1|bit0|||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|警報ブザー|スロワモーター|繰出モーター|送風モーター|搬送モーター|受信チェック||
|buzzer|thrower|kuridashi|soufu|hansou|receive|十進||
|0|0|0|0|0|0|0|受信異常|
|0|0|0|0|0|1|1|停止|
|0|0|0|0|1|1|3|運転中*|
|0|0|0|1|1|1|7|張込|
|0|0|1|1|1|1|15|通風/乾燥|
|0|1|1|x|1|1|27,31|排出|
|1|x|x|x|x|x|32~|警報|
*機械によってはモーターが1台のものもある

<details><summary>コード</summary><div>

```cpp:espSender.ino
#include <esp_now.h>
#include <WiFi.h>
#include <OneWire.h>
#include <DallasTemperature.h>
OneWire  oneWire(21); //21ピンにDS18B20のDQを4.7kΩでプルアップして接続
DallasTemperature sensors(&oneWire);
DeviceAddress insideThermometer;
uint8_t broadcastAddress[] = {0x30, 0xAE, 0xA4, 0x21, 0xB7, 0x78};//親機のMACアドレスに書き換える
#define NAME "A1"//子機機体名
#define ID 0 //子機ID
#define INTERVAL 60000

// 送信用構造体　受信用と一致させる
typedef struct struct_message {
  char name[4];
  uint8_t id;
  uint8_t motorStatus;
  float temp;
} struct_message;

struct_message myData;
bool ledstatus = false;;

// 送信時コールバック関数
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\nLast Packet Send Status:\t");
  if(status == ESP_NOW_SEND_SUCCESS){
    Serial.println("Delivery Success");
    digitalWrite(14, LOW);
    ledstatus = false;
  }else{
    Serial.println("Delivery Fail");
    digitalWrite(14, HIGH);
    ledstatus = true;
  }
}

void setup() {
  //GPIO初期設定
  for (uint8_t i =16; i < 20; i++ ){
    pinMode(i, INPUT_PULLUP);
  }
  pinMode(4, INPUT_PULLUP);
  pinMode(14, OUTPUT);

  Serial.begin(115200);
  //温度センサー初期設定
  sensors.begin();
  sensors.setResolution(insideThermometer, 9);
  if (!sensors.getAddress(insideThermometer, 0)) Serial.println("Unable to find address for Device 0"); 

  strcpy(myData.name, NAME);
  myData.id = ID;

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  //データの送信時にトリガーされるコールバック関数を登録します
  esp_now_register_send_cb(OnDataSent);
  
  // peer登録
  esp_now_peer_info_t peerInfo;
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
}
 
void loop() {
   //温度取得
  sensors.requestTemperatures(); 
  myData.temp = sensors.getTempC(insideThermometer);
  Serial.print("Temp C: ");
  Serial.println(myData.temp);
  //motorstatus
  bool buzzer = !digitalRead(4);
  bool thrower = !digitalRead(16);//16ピンとGNDをモーターの電磁開閉器の補助接点に接続
  bool kuridasi =!digitalRead(17);//以下同
  bool soufu = !digitalRead(18);
  bool hansou = !digitalRead(19);
  bitWrite(myData.motorStatus, 5, buzzer);
  bitWrite(myData.motorStatus, 4, thrower);
  bitWrite(myData.motorStatus, 3, kuridasi);
  bitWrite(myData.motorStatus, 2, soufu);
  bitWrite(myData.motorStatus, 1, hansou); 
  bitWrite(myData.motorStatus, 0, TRUE);//receive check
  Serial.print("MotorStatus: ");
  Serial.println(myData.motorStatus, BIN);

  // ESP-NOW 送信
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
   
  if (result == ESP_OK) {
    Serial.println("Sent with success");
  }else {
    Serial.println("Error sending the data");
  }
  delay(INTERVAL);
}
```
</div></details>

##受信（親機　Receiver)
arduinoUNO型のESP32搭載ボード[IotExpress](https://www.aitendo.com/product/16899) と[LTE-M Shield for Arduino](https://soracom.jp/store/5303/) を重ねてUART接続により子機から集めたデータをモデムを通してSORACOMへ送信します。
送信の際はこの記事を参考にバイナリーで送りバイナリーパーサーでJSON形式に戻すことで通信料を削減できます。

https://qiita.com/m_take/items/41dafef96857dd1b1b6a

稼働時間、警告フラグと子機1台につきmotorStatus=1Byte ＋ temp=2Byte をHEXにして送ります





<details><summary>コード</summary><div>

```cpp:espGateWay.ino

#include <esp_now.h>
#include <WiFi.h>

#define INTERVAL_MS 60000L
#define ENDPOINT "uni.soracom.io"
#define SKETCH_NAME "esp32_gateway_with_soracom"
#define VERSION "1.0"
/* for LTE-M Shield for Arduino */
#define RX 25  //D10
#define TX 23 //D11 SDカードを使用する場合は他のピンを使う
#define BAUDRATE 9600
#define BG96_RESET 33 //A1
#define LEDPIN 2
#define TINY_GSM_MODEM_BG96
#include <TinyGsmClient.h>
#define RESET_DURATION 86400000UL // 1 day
HardwareSerial LTE_M_shieldUART(1); 
TinyGsm modem(LTE_M_shieldUART);
TinyGsmClient ctx(modem);

// 受信用構造体　送信用と一致させる
typedef struct struct_message {
  char name[4];
  uint8_t id;
  uint8_t motorStatus;
  float temp;
} struct_message;

// Create a struct_message called myData
struct_message myData;
uint8_t recv_motorStatus[12];
float recv_temp[12];

// ESP-NOW受信コールバック関数
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
  memcpy(&myData, incomingData, sizeof(myData));
  recv_motorStatus[myData.id] = myData.motorStatus;
  recv_temp[myData.id] = myData.temp;

  Serial.print(len);
  Serial.println(" Bytes received");
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
          mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("mac addr: "); Serial.println(macStr);
  Serial.print("sender name: ");
  Serial.println(myData.name);
  Serial.print("Sender ID: ");
  Serial.println(myData.id);
  Serial.print("motor Status: ");
  Serial.print(myData.motorStatus,BIN);
  Serial.print(" temp: ");
  Serial.println(myData.temp);
  Serial.println();
}

void setup() {
  pinMode(BG96_RESET,OUTPUT);
  pinMode(LEDPIN,OUTPUT);
  digitalWrite(LEDPIN,LOW);
  Serial.begin(115200);
  Serial.println();
  Serial.print(SKETCH_NAME); Serial.println(VERSION);
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);
  // Init ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  Serial.print("resetting module ");
  digitalWrite(BG96_RESET,LOW);
  delay(300);
  digitalWrite(BG96_RESET,HIGH);
  delay(300);
  digitalWrite(BG96_RESET,LOW);
  Serial.println(F(" done."));

  LTE_M_shieldUART.begin(BAUDRATE, SERIAL_8N1, RX ,TX);
  Serial.print("modem.restart()");
  modem.restart();
  Serial.println(" done.");

  Serial.print("modem.getModemInfo(): ");
  String modemInfo = modem.getModemInfo();
  Serial.println(modemInfo);

  Serial.print("waitForNetwork()");
  while (!modem.waitForNetwork()) Serial.print(".");
  Serial.println(" Ok.");

  Serial.print("gprsConnect(soracom.io)");
  modem.gprsConnect("soracom.io", "sora", "sora");
  Serial.println(" done.");

  Serial.print("isNetworkConnected()");
  while (!modem.isNetworkConnected()) Serial.print(".");
  Serial.println(" Ok.");

  Serial.print("My IP addr: ");
  IPAddress ipaddr = modem.localIP();
  Serial.println(ipaddr);
//ESP-NOW受信コールバック関数登録
 esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  //初期化
  for(int i=0; i < 12; i++){
    recv_motorStatus[i] = 0;
  }
  //子機からの受信を待ちます 
  delay(INTERVAL_MS);
  bool errorflag = false;
  uint16_t uptime = millis();
  char hexPayload[180];
 
  for(int i=0; i < 20; i++){
    errorflag |= bitRead(recv_motorStatus[i], 5);// buzzerが一つでも鳴ったらtrue
  }

  sprintf(hexPayload,"%04X%02X", uptime, errorflag);//稼働時間と警報用フラグ
  //子機のDataを順に並べていく　温度は100倍してuint16_t 今回はマイナスの温度は考慮してません
  for(int i=0; i < 12; i++){
    char hex[10],str[10];
    sprintf(hex,"%02X%04X",recv_motorStatus[i],uint16_t(recv_temp[i]*100));
    strcat(hexPayload,hex);
  }  
  //モデム送信用ATコマンド
  char cmd[128];
  sprintf(cmd, "+QISENDEX=0,\"%s\"",hexPayload);
  Serial.println(cmd);
  Serial.print(strlen(cmd));
  Serial.print(F(" Bytes Send..."));

  /* connect */
  if (!ctx.connect(ENDPOINT, 23080)) {
    Serial.println(F("failed."));
    digitalWrite(LEDPIN,HIGH);
    delay(3000);
    return;
  }
  /*send AT command */
  modem.sendAT(cmd);
  /* receive response */
  ctx.stop();
  Serial.println(F("done."));
  digitalWrite(LEDPIN,LOW);

  
#ifdef RESET_DURATION
  if(millis() > RESET_DURATION ){
    Serial.println("Execute software reset...");
    delay(1000);
    ESP.restart();
  }
#endif
}
```
</div></details>


##バイナリーパーサー
数が増えて手入力だと面倒なのでpythonで作りました

```python:binaryparser.py
text="uptime:0:uint:16 error:2:uint:8 "
names = ["A1","A2","A3","A4","B1","B2","B3","B4","C1","C2","C3","C4"]
for i ,name in enumerate(names):
    text+=f"{name}:{i*3+3}:uint:8 t_{name}:{i*3+4}:uint:16:/100 "
print(text)
```

```
uptime:0:uint:16 error:2:uint:8 A1:3:uint:8 t_A1:4:uint:16:/100 A2:6:uint:8 t_A2:7:uint:16:/100 A3:9:uint:8 t_A3:10:uint:16:/100 A4:12:uint:8 t_A4:13:uint:16:/100 B1:15:uint:8 t_B1:16:uint:16:/100 B2:18:uint:8 t_B2:19:uint:16:/100 B3:21:uint:8 t_B3:22:uint:16:/100 B4:24:uint:8 t_B4:25:uint:16:/100 C1:27:uint:8 t_C1:28:uint:16:/100 C2:30:uint:8 t_C2:31:uint:16:/100 C3:33:uint:8 t_C3:34:uint:16:/100 C4:36:uint:8 t_C4:37:uint:16:/100

```

##SORACOM Lagoon　－　ダッシュボード
新しくなった[SORACOM Lagoon](https://soracom.jp/services/lagoon/)でこのようにひと目で機械の稼働状況を確認でき,警報時はLINEで通知もされるようになりました。
![スクリーンショット 2021-08-16 180544.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/0a285442-a0a7-aaff-108d-5e75c66f27bb.png)

便利と思った機能は

- **Table　表示される文字列を変更する**

https://users.soracom.io/ja-jp/docs/lagoon-v2/visualization-table/#%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%82%8B%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B

int型のmortorStatusから日本語へ変換ができわかりやすく表示できました。

![スクリーンショット 2021-08-16 180631.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/ce558c51-e26d-c13e-136e-321e499877c3.png)

- **LINE通知**

https://users.soracom.io/ja-jp/docs/lagoon-v2/alert-notification-channel/#line

警報が鳴ったらLINEで通知するようにした
![Screenshot_20210816_200523_jp.naver.line.android_LI (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261581/506bdbe1-053a-ec75-e1a4-6b4288f85733.jpeg)




#苦労した点、課題
・SDカードスロットが付いていたのでcsvファイルで保存できるようにしたら、IotExpressのSDカードに使用してるピンとモデムのUARTに使うピンがかぶっていた。

・はじめ子機はM5-ATOMを使うつもりだったが電波の到達距離が短かかったので、ESP開発ボードを使い端子台用にPCBを作った。

・配線が大変だった

・電磁開閉器の接点から取れない機械もあるので他の方法でモーターのON/OFFを検知する方法が必要

・機械ごとの稼働時間もグラフ化したい

・Wi-Fiの届くところではLTE-Mセルラー回線を使わずにESP32から[SORACOM　Arc](https://soracom.jp/services/arc/)が使えるそうなので試してみたい

https://github.com/ciniml/WireGuard-ESP32-Arduino

#制作費用　(2021/10/1追記）
・親機

||購入先|価格（税込み・送料別）|
|:-:|:-:|:-:|
|IoT Express MkII | https://www.aitendo.com/product/16899 | 3058円|
|ESPrOne 32|https://www.switch-science.com/catalog/3555/|3575円（~~上記が在庫無しなので~~似たようなボード　ピン配置等は未検証です)|
|LTE-M Shield for Arduino |https://soracom.jp/store/5303/ |7180円(1500円クーポン付き）|
|300MBパック (IoT SIM plan-D D-300MB)|https://soracom.jp/store/13380/ |初期902円 月額330円|
|SORACOM Harvest|https://soracom.jp/pricing/|日額5.5円～(無料枠あり）|

・子機

||購入先|価格（税込み・送料別）|
|:-:|:-:|:-:|
|ESP32-DevKitC　ESP-WROOM-32開発ボード|https://akizukidenshi.com/catalog/g/gM-11819/|~1480円(aliexpressで探せば安いのも）|
|PCB|https://www.fusionpcb.jp/|$4.9 50mmx100mm 20枚分|
|DS18B20 防水型温度センサ|https://www.amazon.co.jp/gp/product/B083TTCB9Q/|5個988円|
|フォトカプラTLP222GF|https://akizukidenshi.com/catalog/g/gI-07331/|60円|
|ピンソケット、ターミナルブロック、抵抗、LED、配線、端子等||13台分で5000円ほど|
|USB充電器、USBケーブル、ケース|百均|400円|