--> -->
#blog2navi() *RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす [#j7e67dba] #title(RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす) ソーラーパネルのチャージコントローラー、[[RENOGY ROVER ELITE(現在は廃版の模様):https://renogy.jp/products/charge-controller/]]など、RS-485の口を持っている機器の情報を吸い出してサーバーに飛ばす仕組みの作り方です。~ あちこちに情報が散在しているので、まとめメモです。~ ~ * 発端 [#z066a463] オフグリッド(商用電源と連携しない)の太陽光発電を小規模に始めました。~ ソーラーパネルの出力を適切にバッテリーに送り込むために必要なのがチャージコントローラーで、[[RENOGY ROVER ELITE:https://renogy.jp/products/charge-controller/]]は別売りの[[BT-2 Bluetoothモジュール:https://renogy.jp/bt-2-bluetooth/]]を接続することで、充電状況をスマホアプリでも確認することができます。~ #ref(P171329.jpg)~ ~ しかしこれが、機能の割に結構いい値段がします。 チャージコントローラー本体の液晶パネルで確認できる情報を、4,000円も出してスマホに出すのはなんとなくオーバーコストな気がします。~ #ref(171728.png)~ ~ このBT-02は、チャージコントローラのRS-485端子(コネクタはRJ45)から電源と信号を貰っています。つまり、RS-485の規格に則って通信してあげれば、必要な情報は取れるはずです。~ というわけで、なんとか安く上げたい時のAliExpress頼み、見つけたのがこちらです。~ ■MAX485 モジュール、 RS485 モジュール、 TTL ターン RS-485 モジュール、 MCU 開発アクセサリー~ https://ja.aliexpress.com/item/32427910931.html #ref(154433.png); ■ESP32 開発ボード無線lan + bluetooth超低消費電力デュアルコア(30pin)~ https://ja.aliexpress.com/item/32959541446.html #ref(154004.png); 同じショップで買うと送料が節約になります。私が購入した時は二つ合わせて日本への送料込みで''$6.18''でした。 * ボード接続 [#t8132434] この2つのボードの接続は以下の参考サイトの通りに接続します。~ 実際の接続状態は後述の写真を参考にしてください。~ ■Inexpensive RS485 module with ESP32 (hardware serial)~ https://www.bizkit.ru/en/2019/02/21/12563/~ ~ 小さいボードでRS-485をTTLに変換し、大きいボードでプログラマブルに信号を処理してWifiで飛ばします。この僅か300円ちょっとのボードがプログラムで動作し、WifiやBluetooth通信をしてくれるとは驚きです。世の中の進歩は凄い・・・(笑)~ * RS-485と電源のPIN配列と接続 [#c000a2d5] 電源は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本だけ使います。~ -- VCC(+5V)は小さい基板ではなく(こちらは3.3V!)、ESP32ボードのUSBmicro-B側に入れます。 -- GNDも同様にUSBmicro-Bに繋ぎます。 -- AとBは小さいボードに繋ぎます。~ 私はねじ止めコネクタ(?)が折角ついているので、こちらに繋ぎました。 ESP32ボードは5Vを3.3Vに変換してくれますので、小さいボードの電源は変換後の3.3Vを繋ぎます(上記参考URL内の図参照)。~ ~ USBmicro-Bの電源位置(ピンアサイン)は以下のサイトの通りです。~ ■ユニバーサル・シリアル・バス - Wikipedia~ [[https://ja.wikipedia.org/wiki/ユニバーサル・シリアル・バス#ピン配置:https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB%E3%83%BB%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%AB%E3%83%BB%E3%83%90%E3%82%B9#%E3%83%94%E3%83%B3%E9%85%8D%E7%BD%AE]]~ * 接続写真 [#m14f40b0] 実際に配線した状態が以下です。~ #ref(P20210514.jpg)~ ~ #ref(P43908.jpg)~ 全てのGNDは互いに接続します。~ 5VはRJ45からUSBへ、3.3VはESP32から基板小へと接続します。~ 余った線がそのままになっていますが、ちゃんと絶縁しましょう(^^;)~ ~ ハードウェアは以上です。 * ソフト制御 [#g6f7092a] ソフトウェアは、Arduinoという開発環境を使います。~ 私の全然知らない世界でしたが、サンプルやライブラリが豊富で日本語の情報も多く、色々と面白いこともできそうです。~ とっても参考になる神サイトがこちらです。~ ■Arduinoで遊ぶページ~ https://garretlab.web.fc2.com/arduino/introduction/~ ~ 色々ググればすぐ分かるので簡単に書きます。~ まずはIDEを落としてきてインストールし、[[ESP32用のライブラリをインストール:https://garretlab.web.fc2.com/arduino/esp32/installation/]]して、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~ 実際のソースコードは以下です。動作確認とデータアップロードが混じってますが、単純ですので適当に読み取ってください(笑)~ #code2(c,gutter:false,toolbar:false){{{ #include "ModbusMaster.h" //https://github.com/4-20ma/ModbusMaster #include <WiFi.h> #include <WiFiMulti.h> #include <HTTPClient.h> #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++) { 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ぐらいになると思います。~ *表示イメージ [#o2116556] あとはサーバー側の受け取りや表示が必要です。~ 私の場合はDBなども使用しているのでここには上げません(説明が大変(^^;))が、どこかでGitHubに上げるかも知れません。~ 要はPOSTされたデータをサーバー上に保存し、表示ページで表示するだけです。~ ~ 実際のWEBページが以下です。~ 【PC画面】~ #ref(screen.png)~ ~ 【スマホ画面】~ #ref(phone.png)~ ~ これで外出先からも常に自宅の充電状況が確認できます。 #htmlinsert(twitterbutton.html) RIGHT:Category: [[[修理>日記/Category/修理]]] - 15:13:01 ---- #htmlinsert(20210528_arduino.html) ---- RIGHT:&blog2trackback(); #comment(above) #blog2navi()