--> -->

skimemo


skimemo - 日記/2021-05-14/RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす のバックアップの現在との差分(No.1)


  • 追加された行はこの色です。
  • 削除された行はこの色です。
#blog2navi()
*RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす [#j7e67dba]

#title(RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす);
#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>

/*!
  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ぐらいになると思います。~

*表示イメージ [#o2116556]
あとはサーバー側の受け取りや表示が必要です。~
私の場合はDBなども使用しているのでここには上げません(説明が大変(^^;))が、どこかでGitHubに上げるかも知れません。~
要はPOSTされたデータをサーバー上に保存し、表示ページで表示するだけです。~
~
実際のWEBページが以下です。~
【PC画面】~
#ref(screen.png)~
~
【スマホ画面】~
#ref(phone.png)~
~
これで外出先からも常に自宅の充電状況が確認できます。


#htmlinsert(twitterbutton.html)
RIGHT:Category: &#x5b;[[修理>日記/Category/修理]]&#x5d; - 15:13:01
----
#htmlinsert(20210528_arduino.html)
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()