Let op: Tweakers stopt per 2023 met Tweakblogs. In dit artikel leggen we uit waarom we hiervoor hebben gekozen.

DIY Modbus naar Wifi Bridge

Door ronaldmathies op donderdag 16 december 2021 16:42 - Reacties (4)
Categorie: -, Views: 6.142

Introductie



Een tijd geleden had ik een post met als titel "Meten is weten! De warmtepomp". Dit keer heb ik hier eigenlijk een vervolg op maar omdat het toepasbaar is op verschillende soorten apparaten met een Modbus interface houd ik het verhaal wat meer generiek.

Mijn warmtepomp is van het type Mitsubishi (specifiek binnenunit: ERSC-VM2CR2, en buitenunit is een PUHZ-SHW140 YHA). Deze kan op verschillende manieren uitgelezen worden maar op mijn lijstje stond al meer dan een jaar om een Procon MelcoBEMS MINI A1M aan te sluiten. Dit is een klein apparaatje wat een modbus interface toevoegt.

Echter, modbus heeft als nadeel dat het bedraad is en dat kwam mij niet handig uit. Ik wilde dus een oplossing vinden waarbij ik de modbus data beschikbaar kon stellen op mijn interne netwerk. Zodat ik deze kan uitlezen via een Python script wat bijvoorbeeld binnen een Docker container draait.

Om dit mogelijk te maken had ik het volgende idee, de modbus sluit ik aan op een ESP8266, en die heeft een API beschikbaar waarbij deze continue de vertaling doet tussen HTTP en Modbus.

Voordeel is, alle software eromheen zoals mijn python script hoeft alleen maar HTTP te kunnen doen, en de ESP haalt de data op, en vertaald dit terug naar een Json structuur.

We proberen hier een aantal vragen te beantwoorden:
  • Hoe een Modbus interface aansluiten op een ESP8266?
  • Software om de vertaling te doen tussen Modbus en HTTP
  • De API gebruiken om registers uit te lezen

Hoe een Modbus interface aansluiten op een ESP8266?



De ESP8266 kan standaard niet met Modbus communiceren. Dit komt mede omdat modbus op een compleet ander voltage bereik werkt dan een ESP8266.

Om dit probleem op te lossen voegen we een module toe die dit probleem oplost. Ik zelf heb gebruik gemaakt van een module met een Max485, deze modules kosten meestal rond de drie euro.



Daarnaast hebben we natuurlijk een ESP8266 nodig, ik zelf gebruik de NodeMCU 12E, de reden dat ik deze gebruik heeft er meer mee te maken dat ik ooit een redelijke hoeveelheid van deze modules in één keer had gekocht.



Om de twee modules aan elkaar aan te sluiten kan het volgende schema gebruikt worden:
  • Max485->VCC ---> ESP->VIN
  • Max485->GND ---> ESP->GND
  • Max485->DI ---> ESP->TX
  • Max485->DE ---> ESP->D2
  • Max485->RE ---> ESP->D2
  • Max485->RO ---> ESP->RX
Dat de DE en RE beide aan D2 aangesloten zijn klopt.


Software om de vertaling te doen tussen Modbus en HTTP



Om het geheel werkend te krijgen hebben we ook nog wat software nodig. In dit geval is er niet echt een kant en klaar stukje software te downloaden. Echter is de software niet heel groot of complex dus kunnen we het zelf maken. Als je alle blokken code hieronder kopieert en bij elkaar plakt in een enkel bestand heb je alles compleet. Ik heb het even in stukken gehakt zodat het wat makkelijker is om te bespreken wat welk deel doet.


code:
1
2
3
4
5
6
#include <ModbusRtu.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ESP8266WebServer.h>
#include <ArduinoJson.h>


Het bovenstaande deel zijn de standaard Arduino imports. De meeste zijn direct te downloaden via de Manage Libraries functie van de Arduino. De enige die daarin niet staat is de ModbusRtu.h bibliotheek. Deze moest ik handmatig downloaden van de volgende repository:

code:
1
https://github.com/smarmengol/Modbus-Master-Slave-for-Arduino


Download de zip en pak deze uit in de libraries map van je Arduino IDE (bij mij staat dit in mijn ~/Documents/Arduino/Libraries/ ).

code:
1
2
3
4
5
6
7
#define ENABLE_DEBUG true

#if ENABLE_DEBUG
  #define DEBUG_log(...) Serial.printf((const char *)__VA_ARGS__);
#else
  #define DEBUG_log(...) ((void) 0)
#endif


Een handigheid om makkelijk alle debug logging aan / uit te zetten.

code:
1
2
3
#define WIFI_HOSTNAME "ProconA1Bridge"
#define WIFI_SSID     "[MYSSID]
#define WIFI_PASSWORD "[MYPASSWORD]"


Vul hier je SSID en PASSWORD in van je WiFi netwerk, let op dat dit wel een 2,4Ghz Wifi netwerk is. Verder kan je hier eventueel een HOSTNAME opgeven, zodat je ESP8266 een bekende naam heeft op het netwerk.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ESP8266WebServer server(80);

Modbus master(0, 0, D2);
  
void setup() {
  Serial.begin(9600);
  Serial.println("Test");
  DEBUG_log("\nInitializing");
  master.begin(9600);
  master.setTimeOut(1000);

  setupWiFi();
  setupApi();
}


We zetten de seriële poort op 9600 baud, dit heeft te maken met dat mijn Modbus aansluiting standaard op 9600 baud staat. Bekijk de documentatie van jouw apparaat waar deze op ingesteld staat. Het is belangrijk dat zowel de Serial.begin als de master.begin dezelfde baud rate hebben omdat beide dezelfde tx / rx pins gebruiken van je ESP8266.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void setupWiFi() {
  DEBUG_log("Connecting with WiFi network\n");
  WiFi.setHostname(WIFI_HOSTNAME);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    DEBUG_log("Connection Failed! Rebooting...\n");
    delay(5000);
    ESP.restart();
  }
  
  DEBUG_log("WiFi connection established\n");
  DEBUG_log("Local IP Address:");
  IPAddress ipAddress = WiFi.localIP();
  DEBUG_log("%d.%d.%d.%d\n", ipAddress[0], ipAddress[1], ipAddress[2], ipAddress[3]);
}

void setupApi() {
  DEBUG_log("Initializing HTTP Web Server\n");
  server.on("/", handleGet);
  server.begin();
}


De bovenstaande code initialiseert het WiFi netwerk, en start de HTTP webserver op, alle requests die binnenkomen op / zullen doorverwezen worden naar de handleGet methode.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void handleGet() {
  StaticJsonDocument<1024> doc;

  int _time = -1, _slave = -1, _function = -1;
  int _registerIdx = 0;
  String _registers[32];
  for (int i = 0; i < server.args(); i++) {
    if (server.argName(i) == "s") {
      _slave = server.arg("s").toInt();
      DEBUG_log("Slave: %d\n", _slave);
    } else if (server.argName(i) == "t") {
      _time = server.arg("t").toInt();
      DEBUG_log("Time: %d\n", _time);
    } else if (server.argName(i).startsWith("r")) {
      _registers[_registerIdx] = server.arg(server.argName(i));
      DEBUG_log("Register entry: %s\n", _registers[_registerIdx]);
      _registerIdx++;
    }
  }


Het eerste deel van de code kijkt of je alle argumenten hebt meegegeven die nodig zijn om een uitlees actie te kunnen uitvoeren. Al deze argumenten worden wanneer debug logging aan staat naar de seriële poort weggeschreven zodat je deze in de console van je Arduino IDE kan terug zien,.

Wanneer je niet alle argumenten mee hebt gegeven die noodzakelijk zijn dan zal dit resulteren in een Json object met een error code:

code:
1
2
3
4
5
6
7
  if (_slave == -1) {
    doc["error"] = "Slave (param: s) has not been specified.";
  } else if (_time == -1) {
    doc["error"] = "Time (param: t (in ms)) has not been specified.";
  } else if (_registers[0] == "") {
    doc["error"] = "Register/Number of elements (param: r[i]) has not been specified.";
  }


Dit ziet er dan als volgt uit:

code:
1
{"error":"Slave (param: s) has not been specified."}


Wanneer er geen fouten zijn ontdekt dan zal de programmatuur de gevraagde registers proberen uit te lezen:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  if (!doc.containsKey("error")) {
    for (int idx = 0; idx < _registerIdx; idx++) {

      int _start_pos = 0;
      int _end_pos = _registers[idx].indexOf(":");
      int _function = _registers[idx].substring(_start_pos, _end_pos).toInt();

      _start_pos = _end_pos + 1;
      _end_pos = _registers[idx].indexOf(":", _start_pos);
      int _start = _registers[idx].substring(_start_pos, _end_pos).toInt();

      _start_pos = _end_pos + 1;
      int _length =  _registers[idx].substring(_start_pos).toInt();
      DEBUG_log("Decoded registry entry, function: %d, start: %d,  length: %d\n", _function, _start, _length);

      uint16_t _response[16];
      int _state = 0;
      while(_state != 2) {
        delay(_time);
        switch (_state) {
          case 0: // Request
            modbus_t telegram;
            telegram.u8id = _slave; // slave address
            telegram.u8fct = _function; // function code (this one is registers read)
            telegram.u16RegAdd = _start; // start address in slave
            telegram.u16CoilsNo = _length; // number of elements (coils or registers) to read
            telegram.au16reg = _response; // pointer to a memory array in the Arduino
            master.query(telegram); // send query (only once)
            _state++;
            break;
          case 1: // Response
            master.poll(); // check incoming messages
            if (master.getState() == COM_IDLE) {
              JsonVariant _functionObject = doc[String(_function)];
              if (_functionObject.isNull()) {
                _functionObject = doc.createNestedObject(String(_function));
              }
              for (int idx = 0; idx < _length; idx++) {
                _functionObject[String(_start + idx)] = _response[idx];
              }
              _state++;
            }
            break;
        }
      }
    }

  }
  
#if ENABLE_DEBUG
  serializeJson(doc, Serial);
#endif

  String output;
  serializeJson(doc, output);
  server.send(200, F("application/json"), output);
}


Als dit lukt dan zal er een resultaat gegeven worden van alle gevraagde registers en de bijbehorende waarden:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "4": {
    "32":0,
    "56":3050,
    "57":3050
  },
  "3": {
    "25":1,
    "26":2,
    "27":1,
    "37":0
  }
}




De API gebruiken om registers uit te lezen




Deze is redelijk simpel, de volgende argumenten moeten meegegeven worden:
  • s - Slave ID, dus het ID van het apparaat waarmee je wilt communiceren.
  • t - Timing, tijdens de communicatie met het modbus apparaat moeten geregeld vertragingen aangehouden worden, met deze timing argument kan je in milliseconden aangeven hoeveel vertraging er moet zijn.
  • r[x] - Functie, register ene aantal, met dit argument geef je aan met welke functie wat het start register is wat je wilt uitlezen en hoeveel register posities je in totaal wilt uitlezen. Het aantal posities is in de code gemaximaliseerd op 16 posities tegelijk. Het aantal start registers is gemaximaliseerd op 10.
Om even een voorbeeld te geven:

code:
1
http://192.168.1.1/?s=1&t=50&r1=4:32:1&r2=4:56:3&r3=3:60:5&r4=3:173:1


Heeft de volgende argumenten, de slave Id is ingesteld op "1".

We hebben een timing ingesteld van 50ms, dit getal werkt toevallig bij mij consistent,. Houd er rekening mee dat dit voor verschillende apparaten anders kan zijn. Als je niet de waarden krijg die je verwacht dan kan het handig zijn om dit gedaan iets te verhogen naar bijvoorbeeld 100, 200 of 300.

Als laatste geven we een aantal functie, start registers en lengte argumenten op:

r1 is functie 4, start register 32 met een lengte van 1, dus alleen register 32.
r2 is functie 4, start register 56 met een lengte van 3, dus registers 56, 57 en 58 zullen uitgelezen worden.
r3 is functie 3, start register 60 met een lengte van 5, dus 60, 61, 62, 63 en 64.
r4 is functie 3, start register 173 met een lengte van 1, dus alleen 173.

Als resultaat krijg ik in dit geval:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "4": {
    "32":0,
    "56":3050,
    "57":3050,
    "58": 10
  },
  "3": {
    "60":1,
    "61":2,
    "62":1,
    "63":0,
    "64":0,
    "173":0
  }
}


Ik hoop dat het allemaal een beetje duidelijk is zo.

Volgende: Meten is weten! De Warmtepomp 10-'18 Meten is weten! De Warmtepomp

Reacties


Door Tweakers user Infant, zaterdag 18 december 2021 10:03

Lekker bezig. Ik heb toevallig net iets soortgelijks gedaan:

De losse ESP-12S module, volgens mij bijna hetzelfde als de 12E op de Node MCU. Ik heb er Tasmota op draaien. Dat wordt elke keer dat ik er naar kijk beter, zo goed dat je ze nu direct vanuit de browser kun flashen. *mind blown* Dus dat duurde wel 2 seconden.

Ik heb inderdaad niet echt een makkelijke modbus feature in tasmota kunnen vinden, dus ik heb er een micro controller tussen die de modbus master is, en via serieel de handel op MQTT dumpt.

Ik lees er een ABB B23 energie en vooral stroom meter mee uit.

Door Tweakers user Eraser127, zaterdag 18 december 2021 10:05

Hhhmm, interessant. Wil m'n Brink Flair 300 WTW systeem ook integreren met Home Asisstant. Online zie je dit wel met een modbus-usb adapter en een Raspberry, maar als dit op deze manier kan scheelt dit weer een extra (grote) computer in het netwerk.

Het laatste stuk code met die registers is specifiek van het apparaat wat jij wil integreren begrijp ik? en die codes die je terugkrijgt zijn dus bepaalde functies van het apparaat die een bepaalde waarde hebben?

Door Tweakers user ronaldmathies, zaterdag 18 december 2021 11:58

Eraser127 schreef op zaterdag 18 december 2021 @ 10:05:
Hhhmm, interessant. Wil m'n Brink Flair 300 WTW systeem ook integreren met Home Asisstant. Online zie je dit wel met een modbus-usb adapter en een Raspberry, maar als dit op deze manier kan scheelt dit weer een extra (grote) computer in het netwerk.

Het laatste stuk code met die registers is specifiek van het apparaat wat jij wil integreren begrijp ik? en die codes die je terugkrijgt zijn dus bepaalde functies van het apparaat die een bepaalde waarde hebben?
Die registers en functie codes moet je terug zoeken in de documentstie van het betreffende apparaat, dit is namelijk verschillend.

Bij mijn warmtepomp is bijvoorbeeld functie code 3 en 4 voor lezen, 6 voor schrijven.

Als de documentstie niet te vinden is dan zag ik hier een bode-red voorbeeld waarin de registers waarschijnlijk wel genoemd worden.

https://medium.com/@clifc...ome-assistant-8ff6a41d19c

Door Tweakers user ronaldmathies, zaterdag 18 december 2021 12:00

Infant schreef op zaterdag 18 december 2021 @ 10:03:
Lekker bezig. Ik heb toevallig net iets soortgelijks gedaan:

De losse ESP-12S module, volgens mij bijna hetzelfde als de 12E op de Node MCU. Ik heb er Tasmota op draaien. Dat wordt elke keer dat ik er naar kijk beter, zo goed dat je ze nu direct vanuit de browser kun flashen. *mind blown* Dus dat duurde wel 2 seconden.

Ik heb inderdaad niet echt een makkelijke modbus feature in tasmota kunnen vinden, dus ik heb er een micro controller tussen die de modbus master is, en via serieel de handel op MQTT dumpt.

Ik lees er een ABB B23 energie en vooral stroom meter mee uit.
Dat is ook een aardige oplossing, ik wilde juist een HTTP APi ( gewoon een url aanroep) zodat er makkelijk te testen is met een standaard browser, en hij is enorm flexibel want je kan de op te halen waardes zo aanpassen. Met node-red kan het dan bijvoorbeeld uitgelezen worden en op een mqtt geplaatst worden, of rechtstreeks in een sql of time series database (influxdb)

Reageren is niet meer mogelijk