Skip to content

Commit 1f08b32

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. 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. fixes tbnobody#582 Signed-off-by: Bobby Noelte <[email protected]>
1 parent d098193 commit 1f08b32

15 files changed

+881
-0
lines changed

include/Configuration.h

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

8692
struct {
8793
bool Enabled;

include/ModbusDtu.h

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
uint16_t val_u16;
18+
int32_t val_i32;
19+
uint32_t val_u32;
20+
uint64_t val_u64;
21+
uint32_t val_ip;
22+
} value;
23+
24+
// Conversion cache
25+
union Conversion {
26+
// fixed point converted to u32
27+
uint32_t fixed_point_u32;
28+
// uint64 converted to hex string
29+
char u64_hex_str[sizeof(uint64_t) * 8 + 1];
30+
// uint64 converted to 12 decimal digits (6 registers) in big endian
31+
std::array<uint16_t, 6> u64_dec_digits;
32+
// ip address converted to String
33+
char ip_str[12];
34+
} conv;
35+
36+
public:
37+
// Default empty message Constructor - optionally takes expected size of MM_data
38+
explicit ModbusDTUMessage(uint16_t dataLen);
39+
40+
// Special message Constructor - takes a std::vector<uint8_t>
41+
explicit ModbusDTUMessage(std::vector<uint8_t> s);
42+
43+
// Add float to Modbus register
44+
void addFloat32(const float_t &val, const size_t reg_offset);
45+
46+
// Add float as decimal fixed point to Modbus register
47+
void addFloat32AsDecimalFixedPoint(const float_t &val, const float &precision, const size_t reg_offset);
48+
49+
// Add string to Modbus register
50+
void addString(const char * const str, const size_t length, const size_t reg_offset);
51+
52+
// Add string to Modbus register
53+
void addString(const String &str, const size_t reg_offset);
54+
55+
// Add uint32 to Modbus register
56+
void addUInt32(const uint32_t val, const size_t reg_offset);
57+
58+
// Add uint64 to Modbus register
59+
void addUInt64(const uint64_t val, const size_t reg_offset);
60+
61+
// Convert uint64 to hex string and add to Modbus register
62+
void addUInt64AsHexString(const uint64_t val, const size_t reg_offset);
63+
64+
// Convert uint64 to 12 decimal digits (big endian) and add to Modbus register
65+
void addUInt64AsDecimalDigits(const uint64_t val, const size_t reg_offset);
66+
67+
// Convert IP address to string and add to Modbus register
68+
void addIPAddressAsString(const IPAddress val, const size_t reg_offset);
69+
};
70+
71+
ModbusMessage OpenDTUTotal(ModbusMessage request);
72+
ModbusMessage DTUPro(ModbusMessage request);
73+
74+
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
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_ID_DTUPRO 1
36+
#define MODBUS_ID_TOTAL 125
37+
3338
#define MQTT_ENABLED false
3439
#define MQTT_HOST ""
3540
#define MQTT_PORT 1883U

platformio.ini

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

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

src/Configuration.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ 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["id_dtupro"] = config.Modbus.IDDTUPro;
61+
modbus["id_total"] = config.Modbus.IDTotal;
62+
5763
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
5864
mqtt["enabled"] = config.Mqtt.Enabled;
5965
mqtt["hostname"] = config.Mqtt.Hostname;
@@ -227,6 +233,12 @@ bool ConfigurationClass::read()
227233
config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE;
228234
config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE;
229235

236+
JsonObject modbus = doc["modbus"];
237+
config.Modbus.TCPEnabled = modbus["tcp_enabled"] | MODBUS_TCP_ENABLED;
238+
config.Modbus.Port = modbus["port"] | MODBUS_PORT;
239+
config.Modbus.IDDTUPro = modbus["id_dtupro"] | MODBUS_ID_DTUPRO;
240+
config.Modbus.IDTotal = modbus["id_total"] | MODBUS_ID_TOTAL;
241+
230242
JsonObject mqtt = doc["mqtt"];
231243
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
232244
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));

src/ModbusDtu.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
void 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+
addUInt32(value.val_u32, reg_offset);
26+
}
27+
28+
void ModbusDTUMessage::addFloat32AsDecimalFixedPoint(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+
addUInt32(conv.fixed_point_u32, reg_offset);
42+
}
43+
44+
void ModbusDTUMessage::addString(const char * const str, const size_t length, const size_t reg_offset) {
45+
// Check if the position is within the bounds of the string
46+
size_t offset = reg_offset * sizeof(uint16_t);
47+
if (offset + sizeof(uint16_t) <= length) {
48+
// Reinterpret the memory at position 'offset' as uint16_t
49+
std::memcpy(&value.val_u16, str + offset, sizeof(uint16_t));
50+
} else {
51+
value.val_u16 = 0;
52+
}
53+
54+
add(value.val_u16);
55+
}
56+
57+
void ModbusDTUMessage::addString(const String &str, const size_t reg_offset) {
58+
addString(str.c_str(), str.length(), reg_offset);
59+
}
60+
61+
void ModbusDTUMessage::addUInt32(const uint32_t val, const size_t reg_offset) {
62+
if (reg_offset <= 1) {
63+
add((uint16_t)(val >> 16 * (1 - reg_offset)));
64+
} else {
65+
add((uint16_t)0);
66+
}
67+
}
68+
69+
void ModbusDTUMessage::addUInt64(const uint64_t val, const size_t reg_offset) {
70+
if (reg_offset <= 3) {
71+
add((uint16_t)(val >> 16 * (3 - reg_offset)));
72+
} else {
73+
add((uint16_t)0);
74+
}
75+
}
76+
77+
void ModbusDTUMessage::addUInt64AsHexString(const uint64_t val, const size_t reg_offset) {
78+
// Check if value is already converted to hex string
79+
if (val != value.val_u64) {
80+
snprintf(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), "%0x%08x",
81+
((uint32_t)((val >> 32) & 0xFFFFFFFF)),
82+
((uint32_t)(val & 0xFFFFFFFF)));
83+
// mark conversion
84+
value.val_u64 = val;
85+
}
86+
87+
addString(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), reg_offset);
88+
}
89+
90+
void ModbusDTUMessage::addUInt64AsDecimalDigits(const uint64_t val, const size_t reg_offset) {
91+
if (val != value.val_u64) {
92+
value.val_u64 = val;
93+
// Extract digits from the number
94+
for (int i = 6 - 1; i >= 0; i--) {
95+
conv.u64_dec_digits[i] = value.val_u64 % 10; // Extract the least significant digit
96+
value.val_u64 /= 10; // Remove the least significant digit
97+
conv.u64_dec_digits[i] += (value.val_u64 % 10) << 8; // Extract the least significant digit
98+
value.val_u64 /= 10; // Remove the least significant digit
99+
}
100+
// mark conversion
101+
value.val_u64 = val;
102+
}
103+
104+
if (reg_offset < 6) {
105+
add(conv.u64_dec_digits[reg_offset]);
106+
} else {
107+
add((uint16_t)0);
108+
}
109+
}
110+
111+
void ModbusDTUMessage::addIPAddressAsString(const IPAddress val, const size_t reg_offset) {
112+
// Check if value is already converted to hex string
113+
if (val != value.val_ip) {
114+
String str(val.toString());
115+
std::memcpy(&conv.ip_str, str.c_str(), std::min(sizeof(conv.ip_str), str.length()));
116+
// mark conversion
117+
value.val_ip = val;
118+
}
119+
120+
addString(&conv.ip_str[0], sizeof(conv.ip_str), reg_offset);
121+
}
122+
123+
// Create server(s)
124+
ModbusServerTCPasync ModbusTCPServer;

0 commit comments

Comments
 (0)