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.

The OpenDTU Modbus sources were imspired by : https://github.com/ArekKubacki/OpenDTU.
See #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 #582

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
  • Loading branch information
b0661 committed Apr 14, 2024
1 parent d098193 commit 1f08b32
Show file tree
Hide file tree
Showing 15 changed files with 881 additions and 0 deletions.
6 changes: 6 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ struct CONFIG_T {
double Latitude;
uint8_t SunsetType;
} Ntp;
struct {
bool TCPEnabled;
uint32_t Port;
uint32_t IDDTUPro;
uint32_t IDTotal;
} Modbus;

struct {
bool Enabled;
Expand Down
74 changes: 74 additions & 0 deletions include/ModbusDtu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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;
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;
// uint64 converted to hex string
char u64_hex_str[sizeof(uint64_t) * 8 + 1];
// uint64 converted to 12 decimal digits (6 registers) in big endian
std::array<uint16_t, 6> u64_dec_digits;
// 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
void addFloat32(const float_t &val, const size_t reg_offset);

// Add float as decimal fixed point to Modbus register
void addFloat32AsDecimalFixedPoint(const float_t &val, const float &precision, const size_t reg_offset);

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

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

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

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

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

// Convert uint64 to 12 decimal digits (big endian) and add to Modbus register
void addUInt64AsDecimalDigits(const uint64_t val, const size_t reg_offset);

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

ModbusMessage OpenDTUTotal(ModbusMessage request);
ModbusMessage DTUPro(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);
};
5 changes: 5 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
#define NTP_LATITUDE 51.1657f
#define NTP_SUNSETTYPE 1U

#define MODBUS_TCP_ENABLED false
#define MODBUS_PORT 502
#define MODBUS_ID_DTUPRO 1
#define MODBUS_ID_TOTAL 125

#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 @@ -36,10 +36,17 @@ 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.9.0
bblanchon/ArduinoJson @ ^7.0.4
https://github.com/bertmelis/espMqttClient.git#v1.6.0
https://github.com/eModbus/eModbus.git
nrf24/RF24 @ ^1.4.8
olikraus/U8g2 @ ^2.35.15
buelowp/sunset @ ^1.1.7
Expand Down
12 changes: 12 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ 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["id_dtupro"] = config.Modbus.IDDTUPro;
modbus["id_total"] = config.Modbus.IDTotal;

JsonObject mqtt = doc["mqtt"].to<JsonObject>();
mqtt["enabled"] = config.Mqtt.Enabled;
mqtt["hostname"] = config.Mqtt.Hostname;
Expand Down Expand Up @@ -227,6 +233,12 @@ 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.IDDTUPro = modbus["id_dtupro"] | MODBUS_ID_DTUPRO;
config.Modbus.IDTotal = modbus["id_total"] | MODBUS_ID_TOTAL;

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
124 changes: 124 additions & 0 deletions src/ModbusDtu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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;
}

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

addUInt32(value.val_u32, reg_offset);
}

void ModbusDTUMessage::addFloat32AsDecimalFixedPoint(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;
}

addUInt32(conv.fixed_point_u32, reg_offset);
}

void 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);
}

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

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

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

void 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) & 0xFFFFFFFF)),
((uint32_t)(val & 0xFFFFFFFF)));
// mark conversion
value.val_u64 = val;
}

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

void ModbusDTUMessage::addUInt64AsDecimalDigits(const uint64_t val, const size_t reg_offset) {
if (val != value.val_u64) {
value.val_u64 = val;
// Extract digits from the number
for (int i = 6 - 1; i >= 0; i--) {
conv.u64_dec_digits[i] = value.val_u64 % 10; // Extract the least significant digit
value.val_u64 /= 10; // Remove the least significant digit
conv.u64_dec_digits[i] += (value.val_u64 % 10) << 8; // Extract the least significant digit
value.val_u64 /= 10; // Remove the least significant digit
}
// mark conversion
value.val_u64 = val;
}

if (reg_offset < 6) {
add(conv.u64_dec_digits[reg_offset]);
} else {
add((uint16_t)0);
}
}

void 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;
}

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

// Create server(s)
ModbusServerTCPasync ModbusTCPServer;
Loading

0 comments on commit 1f08b32

Please sign in to comment.