--> -->
ソーラーパネルのチャージコントローラー、RENOGY ROVER ELITE(現在は廃版の模様)など、RS-485の口を持っている機器の情報を吸い出してサーバーに飛ばす仕組みの作り方です。
あちこちに情報が散在しているので、まとめメモです。
オフグリッド(商用電源と連携しない)の太陽光発電を小規模に始めました。
ソーラーパネルの出力を適切にバッテリーに送り込むために必要なのがチャージコントローラーで、RENOGY ROVER ELITEは別売りのBT-2 Bluetoothモジュールを接続することで、充電状況をスマホアプリでも確認することができます。
しかしこれが、機能の割に結構いい値段がします。
チャージコントローラー本体の液晶パネルで確認できる情報を、4,000円も出してスマホに出すのはなんとなくオーバーコストな気がします。
このBT-02は、チャージコントローラのRS-485端子(コネクタはRJ45)から電源と信号を貰っています。つまり、RS-485の規格に則って通信してあげれば、必要な情報は取れるはずです。
というわけで、なんとか安く上げたい時のAliExpress頼み、見つけたのがこちらです。
■MAX485 モジュール、 RS485 モジュール、 TTL ターン RS-485 モジュール、 MCU 開発アクセサリー
https://ja.aliexpress.com/item/32427910931.html
■ESP32 開発ボード無線lan + bluetooth超低消費電力デュアルコア(30pin)
https://ja.aliexpress.com/item/32959541446.html
同じショップで買うと送料が節約になります。私が購入した時は二つ合わせて日本への送料込みで$6.18でした。
この2つのボードの接続は以下の参考サイトの通りに接続します。
実際の接続状態は後述の写真を参考にしてください。
■Inexpensive RS485 module with ESP32 (hardware serial)
https://www.bizkit.ru/en/2019/02/21/12563/
小さいボードでRS-485をTTLに変換し、大きいボードでプログラマブルに信号を処理してWifiで飛ばします。この僅か300円ちょっとのボードがプログラムで動作し、WifiやBluetooth通信をしてくれるとは驚きです。世の中の進歩は凄い・・・(笑)
電源はESP32ボードのUSBmicro-B端子から5Vを入れます。BT-02が別電源を不要としていることから分かるように、RS-485の端子に5Vが出ています。
RS-485の正しいピン配列というのは特に無いようで、製品によってまちまちです。RENOGY ROVER ELITEの配列は以下のサイトを参考にしました。
■Project: Solar/Wind PIC controlled battery array
https://forum.allaboutcircuits.com/threads/project-solar-wind-pic-controlled-battery-array.32879/page-5#post-1483807
若干情報がごちゃごちゃしているので整理すると、チャージコントローラのRS-485端子は穴を覗いた場合以下のようになります。
+----+ 8: VCC(+5V) +--+ +--+ 7: A | | 6: B +-87654321-+ 5: GND
左の4本だけ使います。
ESP32ボードは5Vを3.3Vに変換してくれますので、小さいボードの電源は変換後の3.3Vを繋ぎます(上記参考URL内の図参照)。
USBmicro-Bの電源位置(ピンアサイン)は以下のサイトの通りです。
■ユニバーサル・シリアル・バス - Wikipedia
https://ja.wikipedia.org/wiki/ユニバーサル・シリアル・バス#ピン配置
実際に配線した状態が以下です。
全てのGNDは互いに接続します。
5VはRJ45からUSBへ、3.3VはESP32から基板小へと接続します。
余った線がそのままになっていますが、ちゃんと絶縁しましょう(^^;)
ハードウェアは以上です。
ソフトウェアは、Arduinoという開発環境を使います。
私の全然知らない世界でしたが、サンプルやライブラリが豊富で日本語の情報も多く、色々と面白いこともできそうです。
とっても参考になる神サイトがこちらです。
■Arduinoで遊ぶページ
https://garretlab.web.fc2.com/arduino/introduction/
色々ググればすぐ分かるので簡単に書きます。
まずはIDEを落としてきてインストールし、ESP32用のライブラリをインストールして、USBmicro-BでPCと接続します。(上記で配線したUSBは電源のためだけのものですので外しておきます)。
USB接続するとCOMポートとして認識されます。
チャージコントローラとの通信はmodbusという(事実上の)標準仕様が使われています。
簡単に言うと、レジスタアドレスを指定してあげるとデータが降ってくるので、それを取りに行きます。
RENOGY ROVER ELITEのmodbusに関する公式資料は以下です。(正式な配布元は会員しか取れないので、有志が(?)別の所に上げたものです)
■OVER MODBUS.DOCX (314.26 KB) - workupload
https://workupload.com/file/yVYgPyyk
私はこのdocxを見つける前に、以下のJavaScriptモジュールのサンプルを参考にしました。こちらの方が分かりやすいかも。
■menloparkinnovation/renogy-rover - GitHub
https://github.com/menloparkinnovation/renogy-rover/blob/master/renogy-rover.js
実際のソースコードは以下です。動作確認とデータアップロードが混じってますが、単純ですので適当に読み取ってください(笑)
#include "ModbusMaster.h" //https://github.com/4-20ma/ModbusMaster
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
/*!
We're using a MAX485-compatible RS485 Transceiver.
Rx/Tx is hooked up to the hardware serial port at 'Serial'.
The Data Enable (DE) and Receiver Enable (RE) pins are hooked up as follows:
*/
#define MAX485_DE 3
#define MAX485_RE_NEG 4 //D4 RS485 has a enable/disable pin to transmit or receive data. Arduino Digital Pin 2 = Rx/Tx 'Enable'; High to Transmit, Low to Receive
#define Slave_ID 1
#define RX_PIN 16 //RX2
#define TX_PIN 17 //TX2
#define UPLOAD_INTERVAL 10000 // WEB upload interval(ms)
#define WIFI_SSID "YOUR WIFI ESSID"
#define WIFI_PASSWORD "WIFI PASSWORD"
// instantiate ModbusMaster object
ModbusMaster node;
WiFiMulti wifiMulti;
void preTransmission() {
// digitalWrite(MAX485_RE_NEG, HIGH); //Switch to transmit data
digitalWrite(MAX485_RE_NEG, 1); //Switch to transmit data
digitalWrite(MAX485_DE, 1);
}
void postTransmission() {
// digitalWrite(MAX485_RE_NEG, LOW); //Switch to receive data
digitalWrite(MAX485_RE_NEG, 0); //Switch to receive data
digitalWrite(MAX485_DE, 0);
}
void setup() {
pinMode(MAX485_RE_NEG, OUTPUT);
pinMode(MAX485_DE, OUTPUT);
// Init in receive mode
postTransmission();
// Modbus communication runs at 9600 baud
Serial.begin(9600, SERIAL_8N1);
Serial2.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
node.begin(Slave_ID, Serial2);
// Callbacks allow us to configure the RS485 transceiver correctly
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
// Wifi
wifiMulti.addAP(WIFI_SSID, WIFI_PASSWORD);
}
void loop() {
uint8_t j, result;
uint16_t data[16];
String post = "";
uint16_t adrs;
// Operating Parameters
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x000A, 2);
if (getResultMsg(&node, result)) {
post += "0x000A=" + String(node.getResponseBuffer(0)) + "&";
post += "0x000B=" + String(node.getResponseBuffer(2)) + "&";
}
// Model
post += "0x000C=";
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x000C, 16);
if (getResultMsg(&node, result)) {
for (j = 0; j < 16; j++) {
data[j] = node.getResponseBuffer(j);
}
for (j = 0; j < 16; j++) {
// Serial.print(data[j],HEX);
// Serial.print(" ");
if (data[j] < 0x20) break;
post += String(char(data[j]/256));
post += String(char(data[j]%256));
}
// Serial.println("");
}
post += "&";
// Serial No.
post += "0x0018=";
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x000C, 4);
if (getResultMsg(&node, result)) {
for (j = 0; j < 4; j++) {
data[j] = node.getResponseBuffer(j);
}
for (j = 0; j < 4; j++) {
if (data[j] < 0x20) break;
post += String(char(data[j]/256));
post += String(char(data[j]%256));
}
}
post += "&";
// Battery status
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x0100, 4);
if (getResultMsg(&node, result)) {
uint16_t stateOfCharge = node.getResponseBuffer(0); // 0=0%, 5=100%?
uint16_t voltage = node.getResponseBuffer(1);
uint16_t chargingCurrent = node.getResponseBuffer(2);
// int8_t controllerTemperature = node.getResponseBuffer(7);
int8_t batteryTemperature = node.getResponseBuffer(3);
Serial.print("stateOfCharge=");
Serial.print(stateOfCharge);
Serial.print(", voltage=");
Serial.print(voltage);
Serial.print(", chargingCurrent=");
Serial.print(chargingCurrent);
// Serial.print(", controllerTemperature=");
// Serial.print(controllerTemperature);
Serial.print(", batteryTemperature=");
Serial.print(batteryTemperature);
Serial.println("");
for(j=0; j<4; j++) {
adrs = 0x0100 + j;
post += "0x" + String(adrs, HEX) + "=" + String(node.getResponseBuffer(j)) + "&";
}
}
// Soloar status
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x0107, 14);
if (getResultMsg(&node, result)) {
uint16_t voltage = node.getResponseBuffer(0);
uint16_t current = node.getResponseBuffer(1);
uint16_t chargingPower = node.getResponseBuffer(2);
Serial.print("voltage=");
Serial.print(voltage);
Serial.print(", current=");
Serial.print(current);
Serial.print(", power=");
Serial.print(chargingPower);
Serial.println("");
for(j=0; j<14; j++) {
adrs = 0x0107 + j;
post += "0x" + String(adrs, HEX) + "=" + String(node.getResponseBuffer(j)) + "&";
}
}
// Historical Information
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x0115, 11);
if (getResultMsg(&node, result)) {
// 2 byte
for(j=0; j<3; j++) {
adrs = 0x0115 + j;
uint16_t val = node.getResponseBuffer(j*2);
post += "0x" + String(adrs, HEX) + "=" + String(val) + "&";
}
// 4 byte
for(j=3; j<11; j+=2) {
adrs = 0x0115 + j;
uint32_t val = node.getResponseBuffer(j) * 0x1000 + node.getResponseBuffer(j+1);
post += "0x" + String(adrs, HEX) + "=" + String(val) + "&";
}
}
// Charging status
// 00H: charging deactivated
// 01H: charging activated
// 02H: mppt charging mode
// 03H: equalizing charging mode
// 04H: boost charging mode
// 05H: floating charging mode
// 06H: current limiting (overpower)
node.clearResponseBuffer();
result = node.readHoldingRegisters(0x0120, 3);
if (getResultMsg(&node, result)) {
uint8_t stateOfCharging = node.getResponseBuffer(0);
Serial.print("stateOfCharging=");
Serial.print(stateOfCharging);
Serial.println("");
post += "0x0120=" + String(stateOfCharging) + "&";
uint32_t weinformation = node.getResponseBuffer(1);
post += "0x0121=" + String(weinformation) + "&";
}
// Setting information
node.clearResponseBuffer();
result = node.readHoldingRegisters(0xE002, 4);
if (getResultMsg(&node, result)) {
// 2 byte
for(j=0; j<4; j++) {
adrs = 0xE002 + j;
post += "0x" + String(adrs, HEX) + "=" + String(node.getResponseBuffer(j)) + "&";
}
}
// Setting information
node.clearResponseBuffer();
result = node.readHoldingRegisters(0xF000, 2);
if (getResultMsg(&node, result)) {
// 2 byte
for(j=0; j<2; j++) {
adrs = 0xF000 + j;
post += "0x" + String(adrs, HEX) + "=" + String(node.getResponseBuffer(j)) + "&";
}
}
Serial.println(post.length());
Serial.println(post);
// Wifi
if ((wifiMulti.run() == WL_CONNECTED)) {
char buf[512];
HTTPClient http;
uint8_t httpCode;
// POSTデータ準備
post.toCharArray(buf, post.length());
// POST
http.begin("http://SERVERNAME/post.php");
http.addHeader("Content-Type", "application/x-www-form-urlencoded", false, true);
httpCode = http.POST((uint8_t*)buf, strlen(buf));
// 結果確認
if(httpCode > 0) {
Serial.printf("[HTTP] POST... code: %d\n", httpCode);
// HTTP OK
if(httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
delay(UPLOAD_INTERVAL);
}
bool getResultMsg(ModbusMaster *node, uint8_t result) {
String tmpstr2 = "";
switch (result) {
case node->ku8MBSuccess:
return true;
break;
case node->ku8MBIllegalFunction:
tmpstr2 += "Illegal Function";
break;
case node->ku8MBIllegalDataAddress:
tmpstr2 += "Illegal Data Address";
break;
case node->ku8MBIllegalDataValue:
tmpstr2 += "Illegal Data Value";
break;
case node->ku8MBSlaveDeviceFailure:
tmpstr2 += "Slave Device Failure";
break;
case node->ku8MBInvalidSlaveID:
tmpstr2 += "Invalid Slave ID";
break;
case node->ku8MBInvalidFunction:
tmpstr2 += "Invalid Function";
break;
case node->ku8MBResponseTimedOut:
tmpstr2 += "Response Timed Out";
break;
case node->ku8MBInvalidCRC:
tmpstr2 += "Invalid CRC";
break;
default:
tmpstr2 += "Unknown error: " + String(result);
break;
}
Serial.println(tmpstr2);
return false;
}
私がC++初心者なので多分無駄な変数の使い方をしているのと、同じ処理の繰り返しをリファクタすれば、1/5ぐらいになると思います。
あとはサーバー側の受け取りや表示が必要です。
私の場合はDBなども使用しているのでここには上げません(説明が大変(^^;))が、どこかでGitHubに上げるかも知れません。
要はPOSTされたデータをサーバー上に保存し、表示ページで表示するだけです。
実際のWEBページが以下です。
【PC画面】
【スマホ画面】
これで外出先からも常に自宅の充電状況が確認できます。
Arduino、こんな事になってるんだ・・・知らんかった。。 |