Skip to content

Commit

Permalink
Add Modbus TCP
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
b0661 committed Jul 14, 2024
1 parent e541a88 commit f0fa80f
Show file tree
Hide file tree
Showing 16 changed files with 1,121 additions and 0 deletions.
8 changes: 8 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ struct CONFIG_T {
double Latitude;
uint8_t SunsetType;
} Ntp;
struct {
bool TCPEnabled;
uint32_t Port;
uint32_t Clients;
uint32_t IDDTUPro;
uint32_t IDTotal;
uint32_t IDMeter;
} Modbus;

struct {
bool Enabled;
Expand Down
79 changes: 79 additions & 0 deletions include/ModbusDtu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <vector>

#include <TaskSchedulerDeclarations.h>

// eModbus
#include "ModbusMessage.h"
#include "ModbusServerTCPasync.h"

class ModbusDTUMessage : public ModbusMessage {
private:
// Value cache, mostly for conversion
union Value {
float val_float;
int16_t val_i16;
uint16_t val_u16;
int32_t val_i32;
uint32_t val_u32;
uint64_t val_u64;
uint32_t val_ip;
} value;

// Conversion cache
union Conversion {
// fixed point converted to u32
uint32_t fixed_point_u32;
// fixed point converted to u16
uint16_t fixed_point_u16;
// uint64 converted to hex string
char u64_hex_str[sizeof(uint64_t) * 8 + 1];
// ip address converted to String
char ip_str[12];
} conv;

public:
// Default empty message Constructor - optionally takes expected size of MM_data
explicit ModbusDTUMessage(uint16_t dataLen);

// Special message Constructor - takes a std::vector<uint8_t>
explicit ModbusDTUMessage(std::vector<uint8_t> s);

// Add float to Modbus register
uint16_t addFloat32(const float_t &val, const size_t reg_offset);

// Add float as 32 bit decimal fixed point to Modbus register
uint16_t addFloatAsDecimalFixedPoint32(const float_t &val, const float &precision, const size_t reg_offset);

// Add float as 16 bit decimal fixed point to Modbus register
uint16_t addFloatAsDecimalFixedPoint16(const float_t &val, const float &precision);

// Add string to Modbus register
uint16_t addString(const char * const str, const size_t length, const size_t reg_offset);

// Add string to Modbus register
uint16_t addString(const String &str, const size_t reg_offset);

// Add uint16 to Modbus register
uint16_t addUInt16(const uint16_t val);

// Add uint32 to Modbus register
uint16_t addUInt32(const uint32_t val, const size_t reg_offset);

// Add uint64 to Modbus register
uint16_t addUInt64(const uint64_t val, const size_t reg_offset);

// Convert uint64 to hex string and add to Modbus register
uint16_t addUInt64AsHexString(const uint64_t val, const size_t reg_offset);

// Convert IP address to string and add to Modbus register
uint16_t addIPAddressAsString(const IPAddress val, const size_t reg_offset);
};

ModbusMessage DTUPro(ModbusMessage request);
ModbusMessage OpenDTUTotal(ModbusMessage request);
ModbusMessage OpenDTUMeter(ModbusMessage request);

extern ModbusServerTCPasync ModbusTCPServer;
17 changes: 17 additions & 0 deletions include/ModbusSettings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

class ModbusSettingsClass {
public:
ModbusSettingsClass();
void init();

void performConfig();

private:
void startTCP();

void stopTCP();
};

extern ModbusSettingsClass ModbusSettings;
2 changes: 2 additions & 0 deletions include/WebApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "WebApi_inverter.h"
#include "WebApi_limit.h"
#include "WebApi_maintenance.h"
#include "WebApi_modbus.h"
#include "WebApi_mqtt.h"
#include "WebApi_network.h"
#include "WebApi_ntp.h"
Expand Down Expand Up @@ -55,6 +56,7 @@ class WebApiClass {
WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit;
WebApiMaintenanceClass _webApiMaintenance;
WebApiModbusClass _webApiModbus;
WebApiMqttClass _webApiMqtt;
WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp;
Expand Down
15 changes: 15 additions & 0 deletions include/WebApi_modbus.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>

class WebApiModbusClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);

private:
void onModbusStatus(AsyncWebServerRequest* request);
void onModbusAdminGet(AsyncWebServerRequest* request);
void onModbusAdminPost(AsyncWebServerRequest* request);
};
7 changes: 7 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
#define NTP_LATITUDE 51.1657f
#define NTP_SUNSETTYPE 1U

#define MODBUS_TCP_ENABLED false
#define MODBUS_PORT 502
#define MODBUS_CLIENTS 1
#define MODBUS_ID_DTUPRO 1
#define MODBUS_ID_TOTAL 125
#define MODBUS_ID_METER 243

#define MQTT_ENABLED false
#define MQTT_HOST ""
#define MQTT_PORT 1883U
Expand Down
7 changes: 7 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ build_flags =
build_unflags =
-std=gnu++11

; Ignore dependencies of eModbus as they are fulfilled by other library variants
lib_ignore =
AsyncTCP
ESPAsyncTCP
custom-Ethernet

lib_deps =
mathieucarbou/ESP Async WebServer @ 2.10.8
bblanchon/ArduinoJson @ 7.0.4
Expand All @@ -45,6 +51,7 @@ lib_deps =
olikraus/U8g2 @ 2.35.19
buelowp/sunset @ 1.1.7
https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/eModbus/eModbus.git

extra_scripts =
pre:pio-scripts/auto_firmware_version.py
Expand Down
16 changes: 16 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ bool ConfigurationClass::write()
ntp["longitude"] = config.Ntp.Longitude;
ntp["sunsettype"] = config.Ntp.SunsetType;

JsonObject modbus = doc["modbus"].to<JsonObject>();
modbus["tcp_enabled"] = config.Modbus.TCPEnabled;
modbus["port"] = config.Modbus.Port;
modbus["clients"] = config.Modbus.Clients;
modbus["id_dtupro"] = config.Modbus.IDDTUPro;
modbus["id_total"] = config.Modbus.IDTotal;
modbus["id_meter"] = config.Modbus.IDMeter;

JsonObject mqtt = doc["mqtt"].to<JsonObject>();
mqtt["enabled"] = config.Mqtt.Enabled;
mqtt["hostname"] = config.Mqtt.Hostname;
Expand Down Expand Up @@ -230,6 +238,14 @@ bool ConfigurationClass::read()
config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE;
config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE;

JsonObject modbus = doc["modbus"];
config.Modbus.TCPEnabled = modbus["tcp_enabled"] | MODBUS_TCP_ENABLED;
config.Modbus.Port = modbus["port"] | MODBUS_PORT;
config.Modbus.Clients = modbus["clients"] | MODBUS_CLIENTS;
config.Modbus.IDDTUPro = modbus["id_dtupro"] | MODBUS_ID_DTUPRO;
config.Modbus.IDTotal = modbus["id_total"] | MODBUS_ID_TOTAL;
config.Modbus.IDMeter = modbus["id_meter"] | MODBUS_ID_METER;

JsonObject mqtt = doc["mqtt"];
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
Expand Down
123 changes: 123 additions & 0 deletions src/ModbusDtu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Bobby Noelte
*/
#include <array>
#include <cstring>
#include <string>

// OpenDTU
#include "ModbusDtu.h"


ModbusDTUMessage::ModbusDTUMessage(uint16_t dataLen = 0) : ModbusMessage(dataLen) {
value.val_float = NAN;
}

ModbusDTUMessage::ModbusDTUMessage(std::vector<uint8_t> s) : ModbusMessage(s) {
value.val_float = NAN;
}

uint16_t ModbusDTUMessage::addFloat32(const float_t &val, const size_t reg_offset) {
// Use union to convert from float to uint32
value.val_float = val;

return addUInt32(value.val_u32, reg_offset);
}

uint16_t ModbusDTUMessage::addFloatAsDecimalFixedPoint32(const float_t &val, const float &precision, const size_t reg_offset) {
// Check if value is already converted to fixed point
if (value.val_float != val) {
// Multiply by 10^precision to shift the decimal point
// Round the scaled value to the nearest integer
// Use union to convert from fixed point to uint32
value.val_i32 = round(val * std::pow(10, precision));
// remember converted value
conv.fixed_point_u32 = value.val_u32;
// mark conversion
value.val_float = val;
}

return addUInt32(conv.fixed_point_u32, reg_offset);
}

uint16_t ModbusDTUMessage::addFloatAsDecimalFixedPoint16(const float_t &val, const float &precision) {
// Multiply by 10^precision to shift the decimal point
// Round the scaled value to the nearest integer
// Use union to convert from fixed point to uint16
value.val_i16 = round(val * std::pow(10, precision));

add(value.val_u16);
return value.val_u16;
}

uint16_t ModbusDTUMessage::addString(const char * const str, const size_t length, const size_t reg_offset) {
// Check if the position is within the bounds of the string
size_t offset = reg_offset * sizeof(uint16_t);
if (offset + sizeof(uint16_t) <= length) {
// Reinterpret the memory at position 'offset' as uint16_t
std::memcpy(&value.val_u16, str + offset, sizeof(uint16_t));
} else {
value.val_u16 = 0;
}

add(value.val_u16);
return value.val_u16;
}

uint16_t ModbusDTUMessage::addString(const String &str, const size_t reg_offset) {
return addString(str.c_str(), str.length(), reg_offset);
}

uint16_t ModbusDTUMessage::addUInt16(const uint16_t val) {
add(val);
return val;
}

uint16_t ModbusDTUMessage::addUInt32(const uint32_t val, const size_t reg_offset) {
if (reg_offset <= 1) {
value.val_u16 = val >> (16 * (1 - reg_offset));
} else {
value.val_u16 = 0;
}
add(value.val_u16);
return value.val_u16;
}

uint16_t ModbusDTUMessage::addUInt64(const uint64_t val, const size_t reg_offset) {
if (reg_offset <= 3) {
value.val_u16 = val >> (16 * (3 - reg_offset));
} else {
value.val_u16 = 0;
}
add(value.val_u16);
return value.val_u16;
}

uint16_t ModbusDTUMessage::addUInt64AsHexString(const uint64_t val, const size_t reg_offset) {
// Check if value is already converted to hex string
if (val != value.val_u64) {
snprintf(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), "%0x%08x",
((uint32_t)((val >> 32) & 0xFFFFFFFFUL)),
((uint32_t)(val & 0xFFFFFFFFUL)));
// mark conversion
value.val_u64 = val;
}

return addString(&conv.u64_hex_str[0], sizeof(conv.u64_hex_str), reg_offset);
}

uint16_t ModbusDTUMessage::addIPAddressAsString(const IPAddress val, const size_t reg_offset) {
// Check if value is already converted to hex string
if (val != value.val_ip) {
String str(val.toString());
std::memcpy(&conv.ip_str, str.c_str(), std::min(sizeof(conv.ip_str), str.length()));
// mark conversion
value.val_ip = val;
}

return addString(&conv.ip_str[0], sizeof(conv.ip_str), reg_offset);
}

// Create server(s)
ModbusServerTCPasync ModbusTCPServer;
Loading

0 comments on commit f0fa80f

Please sign in to comment.