Skip to content

Commit 345d3de

Browse files
committed
Add Modbus TCP
OpenDTU is extended by a Modbus server. The Modbus server serves TCP at port 502. At Modbus ID 1 the server mimicks the Modbus registers in the original DTUPro. At Modbus ID 125 the server serves a SunSpec compatible pseudo inverter that provides the OpenDTU aggregated data from all registered inverters. At Modbus ID 243 the server serves a Sunspec meter that provides aggregated AC power and AC yield values of all registered inverters. The OpenDTU Modbus sources were imspired by : https://github.com/ArekKubacki/OpenDTU. See tbnobody#582 for the orignal pull request. The Modbus library used for Modbus communication is: https://github.com/eModbus/eModbus. Documentation for the library is here: https://emodbus.github.io/. The library was choosen to achieve a lower memory footprint. Signed-off-by: Bobby Noelte <[email protected]>
1 parent 1f1227f commit 345d3de

16 files changed

+1121
-0
lines changed

include/Configuration.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ struct CONFIG_T {
8282
double Latitude;
8383
uint8_t SunsetType;
8484
} Ntp;
85+
struct {
86+
bool TCPEnabled;
87+
uint32_t Port;
88+
uint32_t Clients;
89+
uint32_t IDDTUPro;
90+
uint32_t IDTotal;
91+
uint32_t IDMeter;
92+
} Modbus;
8593

8694
struct {
8795
bool Enabled;

include/ModbusDtu.h

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
#pragma once
3+
4+
#include <vector>
5+
6+
#include <TaskSchedulerDeclarations.h>
7+
8+
// eModbus
9+
#include "ModbusMessage.h"
10+
#include "ModbusServerTCPasync.h"
11+
12+
class ModbusDTUMessage : public ModbusMessage {
13+
private:
14+
// Value cache, mostly for conversion
15+
union Value {
16+
float val_float;
17+
int16_t val_i16;
18+
uint16_t val_u16;
19+
int32_t val_i32;
20+
uint32_t val_u32;
21+
uint64_t val_u64;
22+
uint32_t val_ip;
23+
} value;
24+
25+
// Conversion cache
26+
union Conversion {
27+
// fixed point converted to u32
28+
uint32_t fixed_point_u32;
29+
// fixed point converted to u16
30+
uint16_t fixed_point_u16;
31+
// uint64 converted to hex string
32+
char u64_hex_str[sizeof(uint64_t) * 8 + 1];
33+
// ip address converted to String
34+
char ip_str[12];
35+
} conv;
36+
37+
public:
38+
// Default empty message Constructor - optionally takes expected size of MM_data
39+
explicit ModbusDTUMessage(uint16_t dataLen);
40+
41+
// Special message Constructor - takes a std::vector<uint8_t>
42+
explicit ModbusDTUMessage(std::vector<uint8_t> s);
43+
44+
// Add float to Modbus register
45+
uint16_t addFloat32(const float_t &val, const size_t reg_offset);
46+
47+
// Add float as 32 bit decimal fixed point to Modbus register
48+
uint16_t addFloatAsDecimalFixedPoint32(const float_t &val, const float &precision, const size_t reg_offset);
49+
50+
// Add float as 16 bit decimal fixed point to Modbus register
51+
uint16_t addFloatAsDecimalFixedPoint16(const float_t &val, const float &precision);
52+
53+
// Add string to Modbus register
54+
uint16_t addString(const char * const str, const size_t length, const size_t reg_offset);
55+
56+
// Add string to Modbus register
57+
uint16_t addString(const String &str, const size_t reg_offset);
58+
59+
// Add uint16 to Modbus register
60+
uint16_t addUInt16(const uint16_t val);
61+
62+
// Add uint32 to Modbus register
63+
uint16_t addUInt32(const uint32_t val, const size_t reg_offset);
64+
65+
// Add uint64 to Modbus register
66+
uint16_t addUInt64(const uint64_t val, const size_t reg_offset);
67+
68+
// Convert uint64 to hex string and add to Modbus register
69+
uint16_t addUInt64AsHexString(const uint64_t val, const size_t reg_offset);
70+
71+
// Convert IP address to string and add to Modbus register
72+
uint16_t addIPAddressAsString(const IPAddress val, const size_t reg_offset);
73+
};
74+
75+
ModbusMessage DTUPro(ModbusMessage request);
76+
ModbusMessage OpenDTUTotal(ModbusMessage request);
77+
ModbusMessage OpenDTUMeter(ModbusMessage request);
78+
79+
extern ModbusServerTCPasync ModbusTCPServer;

include/ModbusSettings.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
#pragma once
3+
4+
class ModbusSettingsClass {
5+
public:
6+
ModbusSettingsClass();
7+
void init();
8+
9+
void performConfig();
10+
11+
private:
12+
void startTCP();
13+
14+
void stopTCP();
15+
};
16+
17+
extern ModbusSettingsClass ModbusSettings;

include/WebApi.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "WebApi_inverter.h"
1313
#include "WebApi_limit.h"
1414
#include "WebApi_maintenance.h"
15+
#include "WebApi_modbus.h"
1516
#include "WebApi_mqtt.h"
1617
#include "WebApi_network.h"
1718
#include "WebApi_ntp.h"
@@ -55,6 +56,7 @@ class WebApiClass {
5556
WebApiInverterClass _webApiInverter;
5657
WebApiLimitClass _webApiLimit;
5758
WebApiMaintenanceClass _webApiMaintenance;
59+
WebApiModbusClass _webApiModbus;
5860
WebApiMqttClass _webApiMqtt;
5961
WebApiNetworkClass _webApiNetwork;
6062
WebApiNtpClass _webApiNtp;

include/WebApi_modbus.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
#pragma once
3+
4+
#include <ESPAsyncWebServer.h>
5+
#include <TaskSchedulerDeclarations.h>
6+
7+
class WebApiModbusClass {
8+
public:
9+
void init(AsyncWebServer& server, Scheduler& scheduler);
10+
11+
private:
12+
void onModbusStatus(AsyncWebServerRequest* request);
13+
void onModbusAdminGet(AsyncWebServerRequest* request);
14+
void onModbusAdminPost(AsyncWebServerRequest* request);
15+
};

include/defaults.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
#define NTP_LATITUDE 51.1657f
3131
#define NTP_SUNSETTYPE 1U
3232

33+
#define MODBUS_TCP_ENABLED false
34+
#define MODBUS_PORT 502
35+
#define MODBUS_CLIENTS 1
36+
#define MODBUS_ID_DTUPRO 1
37+
#define MODBUS_ID_TOTAL 125
38+
#define MODBUS_ID_METER 243
39+
3340
#define MQTT_ENABLED false
3441
#define MQTT_HOST ""
3542
#define MQTT_PORT 1883U

platformio.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,17 @@ build_flags =
3737
build_unflags =
3838
-std=gnu++11
3939

40+
; Ignore dependencies of eModbus as they are fulfilled by other library variants
41+
lib_ignore =
42+
AsyncTCP
43+
ESPAsyncTCP
44+
custom-Ethernet
45+
4046
lib_deps =
4147
mathieucarbou/ESP Async WebServer @ 2.9.5
4248
bblanchon/ArduinoJson @ 7.0.4
4349
https://github.com/bertmelis/espMqttClient.git#v1.6.0
50+
https://github.com/eModbus/eModbus.git
4451
nrf24/RF24 @ 1.4.8
4552
olikraus/U8g2 @ 2.35.19
4653
buelowp/sunset @ 1.1.7

src/Configuration.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ bool ConfigurationClass::write()
5454
ntp["longitude"] = config.Ntp.Longitude;
5555
ntp["sunsettype"] = config.Ntp.SunsetType;
5656

57+
JsonObject modbus = doc["modbus"].to<JsonObject>();
58+
modbus["tcp_enabled"] = config.Modbus.TCPEnabled;
59+
modbus["port"] = config.Modbus.Port;
60+
modbus["clients"] = config.Modbus.Clients;
61+
modbus["id_dtupro"] = config.Modbus.IDDTUPro;
62+
modbus["id_total"] = config.Modbus.IDTotal;
63+
modbus["id_meter"] = config.Modbus.IDMeter;
64+
5765
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
5866
mqtt["enabled"] = config.Mqtt.Enabled;
5967
mqtt["hostname"] = config.Mqtt.Hostname;
@@ -227,6 +235,14 @@ bool ConfigurationClass::read()
227235
config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE;
228236
config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE;
229237

238+
JsonObject modbus = doc["modbus"];
239+
config.Modbus.TCPEnabled = modbus["tcp_enabled"] | MODBUS_TCP_ENABLED;
240+
config.Modbus.Port = modbus["port"] | MODBUS_PORT;
241+
config.Modbus.Clients = modbus["clients"] | MODBUS_CLIENTS;
242+
config.Modbus.IDDTUPro = modbus["id_dtupro"] | MODBUS_ID_DTUPRO;
243+
config.Modbus.IDTotal = modbus["id_total"] | MODBUS_ID_TOTAL;
244+
config.Modbus.IDMeter = modbus["id_meter"] | MODBUS_ID_METER;
245+
230246
JsonObject mqtt = doc["mqtt"];
231247
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
232248
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));

src/ModbusDtu.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
/*
3+
* Copyright (C) 2024 Bobby Noelte
4+
*/
5+
#include <array>
6+
#include <cstring>
7+
#include <string>
8+
9+
// OpenDTU
10+
#include "ModbusDtu.h"
11+
12+
13+
ModbusDTUMessage::ModbusDTUMessage(uint16_t dataLen = 0) : ModbusMessage(dataLen) {
14+
value.val_float = NAN;
15+
}
16+
17+
ModbusDTUMessage::ModbusDTUMessage(std::vector<uint8_t> s) : ModbusMessage(s) {
18+
value.val_float = NAN;
19+
}
20+
21+
uint16_t ModbusDTUMessage::addFloat32(const float_t &val, const size_t reg_offset) {
22+
// Use union to convert from float to uint32
23+
value.val_float = val;
24+
25+
return addUInt32(value.val_u32, reg_offset);
26+
}
27+
28+
uint16_t ModbusDTUMessage::addFloatAsDecimalFixedPoint32(const float_t &val, const float &precision, const size_t reg_offset) {
29+
// Check if value is already converted to fixed point
30+
if (value.val_float != val) {
31+
// Multiply by 10^precision to shift the decimal point
32+
// Round the scaled value to the nearest integer
33+
// Use union to convert from fixed point to uint32
34+
value.val_i32 = round(val * std::pow(10, precision));
35+
// remember converted value
36+
conv.fixed_point_u32 = value.val_u32;
37+
// mark conversion
38+
value.val_float = val;
39+
}
40+
41+
return addUInt32(conv.fixed_point_u32, reg_offset);
42+
}
43+
44+
uint16_t ModbusDTUMessage::addFloatAsDecimalFixedPoint16(const float_t &val, const float &precision) {
45+
// Multiply by 10^precision to shift the decimal point
46+
// Round the scaled value to the nearest integer
47+
// Use union to convert from fixed point to uint16
48+
value.val_i16 = round(val * std::pow(10, precision));
49+
50+
add(value.val_u16);
51+
return value.val_u16;
52+
}
53+
54+
uint16_t ModbusDTUMessage::addString(const char * const str, const size_t length, const size_t reg_offset) {
55+
// Check if the position is within the bounds of the string
56+
size_t offset = reg_offset * sizeof(uint16_t);
57+
if (offset + sizeof(uint16_t) <= length) {
58+
// Reinterpret the memory at position 'offset' as uint16_t
59+
std::memcpy(&value.val_u16, str + offset, sizeof(uint16_t));
60+
} else {
61+
value.val_u16 = 0;
62+
}
63+
64+
add(value.val_u16);
65+
return value.val_u16;
66+
}
67+
68+
uint16_t ModbusDTUMessage::addString(const String &str, const size_t reg_offset) {
69+
return addString(str.c_str(), str.length(), reg_offset);
70+
}
71+
72+
uint16_t ModbusDTUMessage::addUInt16(const uint16_t val) {
73+
add(val);
74+
return val;
75+
}
76+
77+
uint16_t ModbusDTUMessage::addUInt32(const uint32_t val, const size_t reg_offset) {
78+
if (reg_offset <= 1) {
79+
value.val_u16 = val >> (16 * (1 - reg_offset));
80+
} else {
81+
value.val_u16 = 0;
82+
}
83+
add(value.val_u16);
84+
return value.val_u16;
85+
}
86+
87+
uint16_t ModbusDTUMessage::addUInt64(const uint64_t val, const size_t reg_offset) {
88+
if (reg_offset <= 3) {
89+
value.val_u16 = val >> (16 * (3 - reg_offset));
90+
} else {
91+
value.val_u16 = 0;
92+
}
93+
add(value.val_u16);
94+
return value.val_u16;
95+
}
96+
97+
uint16_t ModbusDTUMessage::addUInt64AsHexString(const uint64_t val, const size_t reg_offset) {
98+
// Check if value is already converted to hex string
99+
if (val != value.val_u64) {
100+
snprintf(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), "%0x%08x",
101+
((uint32_t)((val >> 32) & 0xFFFFFFFFUL)),
102+
((uint32_t)(val & 0xFFFFFFFFUL)));
103+
// mark conversion
104+
value.val_u64 = val;
105+
}
106+
107+
return addString(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), reg_offset);
108+
}
109+
110+
uint16_t ModbusDTUMessage::addIPAddressAsString(const IPAddress val, const size_t reg_offset) {
111+
// Check if value is already converted to hex string
112+
if (val != value.val_ip) {
113+
String str(val.toString());
114+
std::memcpy(&conv.ip_str, str.c_str(), std::min(sizeof(conv.ip_str), str.length()));
115+
// mark conversion
116+
value.val_ip = val;
117+
}
118+
119+
return addString(&conv.ip_str[0], sizeof(conv.ip_str), reg_offset);
120+
}
121+
122+
// Create server(s)
123+
ModbusServerTCPasync ModbusTCPServer;

0 commit comments

Comments
 (0)