skimemo


skimemo - 日記/2021-05-14

_ RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす

ソーラーパネルのチャージコントローラー、RENOGY ROVER ELITE(現在は廃版の模様)など、RS-485の口を持っている機器の情報を吸い出してサーバーに飛ばす仕組みの作り方です。
あちこちに情報が散在しているので、まとめメモです。

_ 発端

オフグリッド(商用電源と連携しない)の太陽光発電を小規模に始めました。
ソーラーパネルの出力を適切にバッテリーに送り込むために必要なのがチャージコントローラーで、RENOGY ROVER ELITEは別売りのBT-2 Bluetoothモジュールを接続することで、充電状況をスマホアプリでも確認することができます。

P171329.jpg


しかしこれが、機能の割に結構いい値段がします。 チャージコントローラー本体の液晶パネルで確認できる情報を、4,000円も出してスマホに出すのはなんとなくオーバーコストな気がします。

171728.png


このBT-02は、チャージコントローラのRS-485端子(コネクタはRJ45)から電源と信号を貰っています。つまり、RS-485の規格に則って通信してあげれば、必要な情報は取れるはずです。
というわけで、なんとか安く上げたい時のAliExpress頼み、見つけたのがこちらです。

■MAX485 モジュール、 RS485 モジュール、 TTL ターン RS-485 モジュール、 MCU 開発アクセサリー
https://ja.aliexpress.com/item/32427910931.html

154433.png

■ESP32 開発ボード無線lan + bluetooth超低消費電力デュアルコア(30pin)
https://ja.aliexpress.com/item/32959541446.html

154004.png

同じショップで買うと送料が節約になります。私が購入した時は二つ合わせて日本への送料込みで$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通信をしてくれるとは驚きです。世の中の進歩は凄い・・・(笑)

_ RS-485と電源のPIN配列と接続

電源は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/ユニバーサル・シリアル・バス#ピン配置

_ 接続写真

実際に配線した状態が以下です。

P20210514.jpg


P43908.jpg

全ての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:
*[[/>skimemo - 日記/2021-05-14/RENOGY ROVER ELITEの情報をRS-485経由でESP32-WROOMに受けてWEBサーバーに飛ばす]]

#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画面】

screen.png


【スマホ画面】

phone.png


これで外出先からも常に自宅の充電状況が確認できます。

Category: [修理] - 15:13:01

Arduino、こんな事になってるんだ・・・知らんかった。。





 
Last-modified: 2021-05-14 (金) 15:13:01 (367d)