モチベーション
とある環境下で、安価にセンサデータを収集する構成を検討してて、Arduino使ったプロトタイピングの忘備録
- 対象が多くて、LTEとか高くて使えない。
- 測定にリアルタイム性は必要ないし、わざわざ測定しに行きたくもない。
- インターネット接続はなくLANは伸びているが、無線LANを各部屋に用意するハードルが高いし、そもそもセンサ端末ごとにIPアドレス持たせるほどアドレス用意できない。
参考にしたもの
下記を参考に、足回りはBLEとし BLEゲートウェイでセンサデータ集約して、閉域網内にあるサーバ(下記ではAmbient)に投げることにした。
前記の通り、測定にリアルタイム性は不要で、センサデータ収集のためのサーバアプリを組むのも面倒なので、Arduinoから閉域網内にある既存のsyslogサーバにsyslogでセンサデータ投げ付けて、定期的にパースすることにした。
上記「BLE環境センサー・ゲートウェイ」はWiFi接続だが、前記の通りWiFiなんか用意できないのでBLEゲートウェイだけは有線で接続
よって、プロトタイビングとしては、センサ端末としては持ってたArduino Nano 33 BLE Sense、BLEゲートウェイとしてArduino互換?のESP-WROOM-32Dと、RJ45付きの10/100MbpsのEthernetコントローラENC28J60を追加購入
全部国内で買ったので4995+1529+660=7184円しましたが、AliExpressとかで買えば送料込みで2000円以下で全部揃いますね。。。
数が多くなるのは、Arduino Nano 33 BLE Sense相当なのですが、互換装置は600円くらいでありそう。
(2021/12/12追記)
電圧電流測定のために、AliExpressで購入したINA219(送料込み370円)、SCT-013-100(送料込み557円)を追加
SCT-013-100の方は3.5mmミニジャックなので、TZT TRRS(送料込み190円)で接続
作ったもの
センサ端末
Arduino標準のAruduinoBLEライブラリのPeripheralのサンプル、BatteryMonitorをいじって下記な感じに
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.22V461kΩ/(459kΩ+461kΩ))/20Ω2000turn/√2
ミニジャックの接続は、下記も参考
https://www.poweruc.pl/blogs/news/non-invasive-sensor-yhdc-sct013-000-ct-used-with-arduino-sct-013
また、下記を参照して、INA219のライブラリをインストールし、バス電圧、シャント電圧、電流、負荷電圧?を取得
7ピンにつなげたLEDをCT Sensorsの電流値に応じた明るさで光らせるとともに、CT Sensors, INA219のそれぞれの測定値を、BLEのアドバタイズパケットのManufacturer dataに100で割った切り捨て(小数点以上)と、剰余(少数点以下)を、それぞれ1バイトごとにマッピングして、定期的にアドバタイズ
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を使えるようにすること自体は、購入した代理店?の下記サイトを参照した。
基板の幅が広いのでサンハヤト-SAD-101 ニューブレッドボードとかじゃないと、両側にジャンパピンさせません。
RJ45の口のついたEthernetコントローラENC28J60とESP-WROOM-32Dの接続は冒頭の記事を参照した。
ESP-WROOM-32DのライブラリのWiFiのサンプル WiFiClientをいじって下記な感じに。最初自宅のWiFi環境でsyslogまで送れるまで確認してたので、WiFiClientを拡張したのち、有線(ENC28J60)向けに再拡張しました。
グローバル変数として、UDP接続やらsyslog接続の初期化して、loop()の中で2~3秒(ランダム)に一回BLEのアドバタイズパケットをスキャン、センサ端末のアドバタイズパケットのManufacturerDataから前期A0ピンの測定値を取り出して、センサ端末のMACアドレスとともにsyslogサーバに投げつけてます。
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?
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ファイルそのものの行数が下回ったら、ローテートされたファイルも見に行くとか
#!/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