センサデータをArduinoで取得して、ゲートウェイ経由でsyslogサーバに集めてみた

# モチベーション

とある環境下で、安価にセンサデータを収集する構成を検討してて、Arduino使ったプロトタイピングの忘備録

- 対象が多くて、LTEとか高くて使えない。
- 測定にリアルタイム性は必要ないし、わざわざ測定しに行きたくもない。
- インターネット接続はなくLANは伸びているが、無線LANを各部屋に用意するハードルが高いし、そもそもセンサ端末ごとにIPアドレス持たせるほどアドレス用意できない。


# 参考にしたもの
下記を参考に、足回りはBLEとし BLEゲートウェイでセンサデータ集約して、閉域網内にあるサーバ(下記ではAmbient)に投げることにした。

- [BLE環境センサー・ゲートウェイ(ESP32編)](https://ambidata.io/samples/m5stack/ble_gw/)

前記の通り、測定にリアルタイム性は不要で、センサデータ収集のためのサーバアプリを組むのも面倒なので、Arduinoから閉域網内にある既存のsyslogサーバにsyslogでセンサデータ投げ付けて、定期的にパースすることにした。

- [Syslog](https://github.com/arcao/Syslog)

上記「BLE環境センサー・ゲートウェイ」はWiFi接続だが、前記の通りWiFiなんか用意できないのでBLEゲートウェイだけは有線で接続

- [ENC28J60を使ってESP32でEthernet UDP通信する](https://hikoleaf.hatenablog.jp/entry/2019/06/22/190914)

よって、プロトタイビングとしては、センサ端末としては持ってた[Arduino Nano 33 BLE Sense](https://www.robotshop.com/jp/ja/arduino-nano-33-ble-sense-microcontroller-with-headers.html)、BLEゲートウェイとしてArduino互換?の[ESP-WROOM-32D](https://www.amazon.co.jp/gp/product/B08926W9NM)と、RJ45付きの10/100MbpsのEthernetコントローラ[ENC28J60](https://www.amazon.co.jp/gp/product/B07GPKTQ5M)を追加購入
全部国内で買ったので4995+1529+660=7184円しましたが、AliExpressとかで買えば送料込みで2000円以下で全部揃いますね。。。
数が多くなるのは、Arduino Nano 33 BLE Sense相当なのですが、互換装置は600円くらいでありそう。

(2021/12/12追記)
電圧電流測定のために、AliExpressで購入した[INA219](https://ja.aliexpress.com/item/1005001621733824.html)(送料込み370円)、[SCT-013-100](https://ja.aliexpress.com/item/4000680462559.html)(送料込み557円)を追加
SCT-013-100の方は3.5mmミニジャックなので、[TZT TRRS](https://ja.aliexpress.com/item/32709284953.html)(送料込み190円)で接続

# 作ったもの

- [https://github.com/kamotsuru/ble-sensor-gateway](https://github.com/kamotsuru/ble-sensor-gateway)

## センサ端末
Arduino標準のAruduinoBLEライブラリのPeripheralのサンプル、BatteryMonitorをいじって下記な感じに
[NON-INVASIVE SENSOR: YHDC SCT013-000 CT USED WITH ARDUINO. (SCT-013)](https://www.poweruc.pl/blogs/news/non-invasive-sensor-yhdc-sct013-000-ct-used-with-arduino-sct-013)を参考に(リンク先の記述間違ってるのですが、SCT013-000が電流出力、SCT013-100が電圧出力)、出力電圧は1.20~4.02VになるのでA0ピンのアナログ入力値0-1023を0-3300mVにマッピング(3.3V≒74A以上入力するような測定しない)、これを0~100Aに逆変換する。
電流 = (出力電圧 - 5.22V*461kΩ/(459kΩ+461kΩ))/20Ω*2000turn/√2
ミニジャックの接続は、下記も参考
https://www.poweruc.pl/blogs/news/non-invasive-sensor-yhdc-sct013-000-ct-used-with-arduino-sct-013
また、下記を参照して、INA219のライブラリをインストールし、バス電圧、シャント電圧、電流、負荷電圧？を取得

- [Arduino Code | Adafruit INA219 Current Sensor Breakout](https://learn.adafruit.com/adafruit-ina219-current-sensor-breakout/arduino-code)

7ピンにつなげたLEDをCT Sensorsの電流値に応じた明るさで光らせるとともに、CT Sensors, INA219のそれぞれの測定値を、BLEのアドバタイズパケットのManufacturer dataに100で割った切り捨て(小数点以上)と、剰余(少数点以下)を、それぞれ1バイトごとにマッピングして、定期的にアドバタイズ
![IMG_1182.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/ea2864d1-a1bd-f24c-1ef7-c2f78fcdf2cb.jpeg)
![Screen Shot 2021-12-08 at 16.54.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/bc0ed6d6-6900-3980-2607-b5a5a720936a.png)

`MyAdvertiser.ino`グローバルスコープ
最終的にはセンサ端末1台で、複数の測定対象の電流、電圧を収集し、同じアドバタイズパケットに詰めれるだけ詰めて送るつもり

```
#include <ArduinoBLE.h>

#define DATA_SIZE 7
#define LED 7        // the PWM pin the LED is attached to

int oldA0Level = 0;  // last a0 level reading from analog input
//    led = 7;         // the PWM pin the LED is attached to
byte data[DATA_SIZE] = {0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00};
```

`MyAdvertiser.ino`setup()

```
void setup() {
  // declare pin 7 to be an output:
  pinMode(LED, OUTPUT);
  
  Serial.begin(9600);    // initialize serial communication
  delay(500);

  // begin initialization
  if (!BLE.begin()) {
    if(Serial)
      Serial.println("starting BLE failed!");

    while (1);
  }

  /* Set a local name for the BLE device
     This name will appear in advertising packets
     and can be used by remote devices to identify this BLE device
     The name can be changed but maybe be truncated based on space left in advertisement packet
  */
  BLE.setLocalName("ArduinoNano33BLE");

  BLE.setManufacturerData(data, DATA_SIZE);
  BLE.setAdvertisingInterval(3200); // 2000ms = 3200*0.625ms

  /* Start advertising BLE.  It will start continuously transmitting BLE
     advertising packets and will be visible to remote BLE central devices
     until it receives a new connection */

  // start advertising
  BLE.advertise();

  if(Serial)
    Serial.println("Bluetooth device active, waiting for connections...");
}
```

`MyAdvertiser.ino`loop()

```
void loop() {
  /* Read the current voltage level on the A0 analog input pin.
     This is used here to simulate the charge level of a battery.
  */
  int a0val = analogRead(A0);
  int a0Level = map(a0val, 0, 1023, 0, 3300);
  float shuntvoltage = ina219.getShuntVoltage_mV();
  float busvoltage = ina219.getBusVoltage_V();
  float current_mA = ina219.getCurrent_mA();
  float loadvoltage = busvoltage + (shuntvoltage / 1000);
  float power_mW = ina219.getPower_mW();
  float current_A = ((float)a0Level/1000.0 - 2.61567)*70.71; // 5.22*461/(459+461), 1/20.0*2000.0/1.4142

  if(Serial) {
    Serial.print("Bus Voltage:   "); Serial.print(busvoltage); Serial.println(" V");
    Serial.print("Shunt Voltage: "); Serial.print(shuntvoltage); Serial.println(" mV");
    Serial.print("Load Voltage:  "); Serial.print(loadvoltage); Serial.println(" V");
    Serial.print("Current:       "); Serial.print(current_mA); Serial.println(" mA");
    Serial.print("Power:         "); Serial.print(power_mW); Serial.println(" mW");
    Serial.print("current:         "); Serial.print(current_A); Serial.println(" A");    
    Serial.println("");
  }
  
  analogWrite(LED, (int)map(a0Level, 0, 3300, 0, 255));
  if(Serial) {
    Serial.printf("a0 Level is now: %f\n", a0Level/1000.0);    
  }

  if (a0Level != oldA0Level) {      // if the battery level has changed
    oldA0Level = a0Level;           // save the level for next comparison
    
    BLE.stopAdvertise();
    data[2] = 0x01; // sensor id
    int val = floorf(current_mA); //current before decimal point
    data[7] = (byte)val;
    data[8] = (byte)floorf((current_mA - (float)val)*100.0); //current after decimal point
    val = floorf(busvoltage); //voltage before decimal point
    data[3] = (byte)val;
    data[4] = (byte)floorf((busvoltage - (float)val)*100.0); //voltage after decimal point
    val = floorf(current_A);
    data[5] = (byte)val;
    data[6] = (byte)floorf((current_A - (float)val)*100.0);
    if(Serial)      
//      Serial.printf("data: %d.%02d, %d.%02d, %d.%02d\n", (int)data[3], (int)data[4], (int)data[5], (int)data[6], (int)data[7], (int)data[8]);
    BLE.setManufacturerData(data, DATA_SIZE);
    BLE.advertise();
  }
  
  delay(random(2000,3000));
}
```

## BLEゲートウェイ
Arduino IDEでESP-WROOM-32Dを使えるようにすること自体は、購入した代理店？の下記サイトを参照した。

- [# ESP32-DevKitC-32Dボードの基本的な使い方 ～Arduino IDE編～](http://microtechnica.xyz/?p=271)

基板の幅が広いので[サンハヤト-SAD-101 ニューブレッドボード](https://www.amazon.co.jp/サンハヤト-SAD-101-ニューブレッドボード/dp/B00DSKCS68)とかじゃないと、両側にジャンパピンさせません。
RJ45の口のついたEthernetコントローラENC28J60とESP-WROOM-32Dの接続は[冒頭の記事](https://hikoleaf.hatenablog.jp/entry/2019/06/22/190914)を参照した。
ESP-WROOM-32DのライブラリのWiFiのサンプル WiFiClientをいじって下記な感じに。最初自宅のWiFi環境でsyslogまで送れるまで確認してたので、WiFiClientを拡張したのち、有線(ENC28J60)向けに再拡張しました。
グローバル変数として、UDP接続やらsyslog接続の初期化して、loop()の中で2~3秒(ランダム)に一回BLEのアドバタイズパケットをスキャン、センサ端末のアドバタイズパケットのManufacturerDataから前期A0ピンの測定値を取り出して、センサ端末のMACアドレスとともにsyslogサーバに投げつけてます。
![IMG_0961.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117100/6b19c36e-f6cc-9107-fd30-5134e1266d7e.jpeg)
`WiFiBLEClient.ino`グローバルスコープ

```
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Syslog.h>
#include <UIPEthernet.h>

#define MANUFACTURER_ID 0xffff // test manufacturer ID
#define SYSLOG_PORT 514
#define SCAN_TIME 5

#define ENC28J60

BLEScan* pBLEScan;
char* server = "192.168.0.11";
//char* server = "10.0.1.1";

#ifndef ENC28J60
char* ssid     = "Wifi Network";
char* password = "guest";
IPAddress wifiIp;
WiFiUDP udpClient;
#else
EthernetUDP udpClient;
uint8_t mac[] = { 0x00, 0x01, 0x02, 0x04, 0x04, 0x05 };
uint8_t IP[] = { 10, 0, 1, 2 };
uint8_t DNS[] = { 10, 0, 1, 1 };
uint8_t MASK[] = { 255, 255,255, 0 };
uint8_t GW[] = { 10, 0, 1, 1 };
#endif

Syslog syslog(udpClient, server, SYSLOG_PORT, "esp32", LOG_KERN);

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    }
};
```

`WiFiBLEClient.ino`setup()

```
void setup() {
  Serial.begin(115200);
  if(Serial)
    Serial.println("Scanning...");

  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setActiveScan(false); //active scan uses more power, but get results faster

#ifndef ENC28J60
  // We start by connecting to a WiFi network
  delay(10);
  if(Serial) {
    Serial.print("Connecting to ");
    Serial.println(ssid);
  }

  WiFi.begin(ssid, password);
  while(WiFi.status() != WL_CONNECTED) {
      delay(500);
      if(Serial)
        Serial.print(".");
  }
  
  wifiIp = WiFi.localIP();


  if(Serial) {
    Serial.println("");
    Serial.print("Wifi connected. IP address: ");
    Serial.println(WiFi.localIP());
  }
#else
//  Ethernet.begin(mac, IP, DNS, GW, MASK);
  Ethernet.begin(mac);
  if(Serial) {
    Serial.println("");
    Serial.print("ENC28J60 IP address: ");
    Serial.println(Ethernet.localIP());
    Serial.print("Subnet Mask: ");
    Serial.println(Ethernet.subnetMask());
    Serial.print("Gateway: ");
    Serial.println(Ethernet.gatewayIP());
  }
#endif

}
```

`WiFiBLEClient.ino`loop()他

```
void loop() {
  bool found = false;
  int manufacturerId, id;
  float current, busvoltage, current_mA;

  if (Serial.available() > 0) {
    String str = Serial.readStringUntil('\n');
    if (str[0] == 'R') {
#ifndef ENC28J60
      Serial.printf("%s ", ssid);
      Serial.println(wifiIp);
#else
      Serial.println(Ethernet.localIP());
#endif
    } else if (str[0] == 'A') {
      std::string para = myOffset(str, 2);
      const char* param = "testapp";
      Serial.println(strcmp(para.c_str(), param));
//      syslog.appName((const char*)(para.c_str()));
      syslog.appName(param);      
    }
  }
  
  // put your main code here, to run repeatedly:
  BLEScanResults foundDevices = pBLEScan->start(SCAN_TIME);
  int count = foundDevices.getCount();
  if(Serial) {
    Serial.print("Devices found: ");
    Serial.println(count);
  }
  for (int i = 0; i < count; i++) {
    BLEAdvertisedDevice d = foundDevices.getDevice(i);
    if (d.haveManufacturerData()) {
      std::string data = d.getManufacturerData();
      manufacturerId = data[1] << 8 | data[0];
      if (manufacturerId == MANUFACTURER_ID) {
        found = true;
        id = (int)data[2];
        current = (float)data[3] + (float)data[4]/100.0;
        if (current > 128)
          current = current - 256.0;
        busvoltage = (float)data[5] + (float)data[6]/100.0;
        if (busvoltage > 128)
          busvoltage = busvoltage - 256.0;
        current_mA = (float)data[7] + (float)data[8]/100.0;
        if (current_mA > 128)
          current_mA = current_mA - 256.0;
        if(Serial)
          Serial.printf("%s %d %f %f %f\n", d.getAddress().toString().c_str(), id, current, busvoltage, current_mA);    
        syslog.logf(LOG_INFO, "%s %d %f %f %f", d.getAddress().toString().c_str(), id, current, busvoltage, current_mA);
      }
    }
  }
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory
  
delay(random(2000,3000));
}

std::string myOffset (String string, int offset) {
  std::string para = "";
  for (int i = offset; i < string.length(); i++) {
     para += string[i];
  }
  return para;
}
```

途中にこういう場所ありますが、syslogの送信先とかattributeを動的に変える目的で、```std::string```にserial入力から受け取った文字列コピーしようとしているのですが、コメントアウトと入れ替えると変なアドレス触ってる(ログ5行目以降)ようで、syslogライブラリ作者に聞いているが返事こない。。。
[How can we change const char* arguments during the application running?](https://github.com/arcao/Syslog/issues/25)


```
      std::string para = myOffset(str, 2);
      const char* param = "testapp";
      Serial.println(strcmp(para.c_str(), param));
//      syslog.appName((const char*)(para.c_str()));
      syslog.appName(param);    
```

```
May  9 17:00:30 esp32 - ﻿15:45:05:42:da:6c 1 66.849998 66.849998
May  9 17:00:36 esp32 - ﻿15:45:05:42:da:6c 1 67.730003 67.730003
May  9 17:00:44 esp32 - ﻿15:45:05:42:da:6c 1 67.339996 67.339996
May  9 17:00:51 esp32 - ﻿15:45:05:42:da:6c 1 67.050003 67.050003
May  9 17:00:59 esp32 d8:9c:67:3a:98:14 ﻿15:45:05:42:da:6c 1 67.629997 67.629997
May  9 17:01:06 esp32 #002 ﻿15:45:05:42:da:6c 1 67.139999 67.139999
May  9 17:01:14 esp32 47:c6:10:36:97:82 ﻿15:45:05:42:da:6c 1 67.440002 67.440002
May  9 17:01:22 esp32 #002 ﻿15:45:05:42:da:6c 1 67.440002 67.440002
May  9 17:01:30 esp32 X??? ﻿15:45:05:42:da:6c 1 67.339996 67.339996
```


# syslog受信
今センサ端末が一台ですが、syslogサーバでは下記のように受信できた。(現状、測定値3つ送ってる)

```
Dec 12 13:04:52 esp32 - ﻿15:45:05:42:da:a8 1 3.260000 -2.029999 0.300000
Dec 12 13:05:00 esp32 - ﻿15:45:05:42:da:a8 1 3.250000 -0.479996 0.100000
Dec 12 13:05:07 esp32 - ﻿15:45:05:42:da:a8 1 3.240000 -2.320007 -0.199997
Dec 12 13:05:15 esp32 - ﻿15:45:05:42:da:a8 1 3.250000 -0.259995 0.100000
Dec 12 13:05:23 esp32 - ﻿15:45:05:42:da:a8 1 3.250000 -1.820007 0.100000
Dec 12 13:05:31 esp32 - ﻿15:45:05:42:da:a8 1 3.240000 0.230000 0.100000
Dec 12 13:05:39 esp32 - ﻿15:45:05:42:da:a8 1 3.260000 -1.610001 0.000000
Dec 12 13:05:46 esp32 - ﻿15:45:05:42:da:a8 1 3.230000 -1.610001 -0.199997
Dec 12 13:05:53 esp32 - ﻿15:45:05:42:da:a8 1 3.240000 0.650000 0.100000
Dec 12 13:06:00 esp32 - ﻿15:45:05:42:da:a8 1 3.240000 -0.259995 0.300000
Dec 12 13:06:08 esp32 - ﻿15:45:05:42:da:a8 1 3.240000 0.020000 0.000000
```

# sqliteに放り込む
cronで定期的にsyslog出力をsqliteに放り込む。
ログローテート対応は考えます。。。syslog出力行もDB上に持たせて、syslogファイルそのものの行数が下回ったら、ローテートされたファイルも見に行くとか

```sl2sql.sh 
#!/bin/sh

#cd to this script located directory
cd `dirname $0`

#Usage
#case $# in
# 1 ) ;;
# * ) echo "Usage: ${0##*/} syslog_file_name"; exit 1;;
#esac

#sqlite3 db file name created by this script file name
#dbfile=`echo ${0##*/} | sed -e s/\.[^\.]*$//`.sqlite3
dbfile="/home/pi/sl2sql.sqlite3"

cmd_sqlite3="/usr/bin/sqlite3"

#create db if the above db file doesn't exist
if [ ! -f ${dbfile} ]; then
  echo "${dbfile} doesn't exist!"
  ${cmd_sqlite3} ${dbfile} "CREATE TABLE sens_data (id INTEGER PRIMARY KEY AUTOINCREMENT, device VARCHAR(64), sensId INTEGER, current REAL, busvoltage REAL, current_mA REAL, timestamp TEXT, updated_at TEXT DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime')));" 
#else
#  echo "${dbfile} exists!"
fi

#file=$1
file="/var/log/messages"

#get the latest sensor data timestamp in the db
ex_data=`${cmd_sqlite3} ${dbfile} "SELECT max(id), timestamp FROM sens_data;"` 
ex_timestamp=`echo $ex_data | cut -d '|' -f 2`

flag=0

#read syslog file line by line
while read line
do
    app=`echo ${line} | awk '{print $4}'`
    if [ $app ] && [ $app != "esp32" ]; then
        continue
    fi  
    device=`echo ${line} | awk '{print $6}'`
    sensId=`echo ${line} | awk '{print $7}'`
    current=`echo ${line} | awk '{print $8}'`
    busvoltage=`echo ${line} | awk '{print $9}'`
    current_mA=`echo ${line} | awk '{print $10}'`    
    comp=`echo "$current > 128.0" | bc`
    if [ $comp -eq 1 ]; then
	current = `expr $current - 256.0`
    fi
    comp=`echo "$busvoltage > 128.0" | bc`    
    if [ $comp -eq 1 ]; then
       busvoltage = `expr $busvoltage - 256.0`     
    fi
    comp=`echo "$current_mA > 128.0" | bc`    
    if [ $comp -eq 1 ]; then
       current_mA = `expr $current_mA - 256.0`     
    fi
#    echo $device   
#    echo $sensId   
#    echo $current   
#    echo $busvoltage   
#    echo $timestamp
#flag if the latest sensor data found
    if [ $flag -eq 1 ] || [ `date -d"${ex_timestamp}" +%s` -lt `date -d"${timestamp}" +%s` ]; then
        flag=1
    fi
#insert sensor data
    if [ $flag -eq 1 ]; then
        ${cmd_sqlite3} ${dbfile} "INSERT INTO sens_data(device, sensId, current, busvoltage, current_mA, timestamp) VALUES('${device}', '${sensId}', '${current}', '${busvoltage}', '${current_mA}', '${timestamp}');"
    fi
done < $file
```