feat: add Modbus support

This commit is contained in:
mheyse
2024-06-30 15:24:46 +02:00
parent 217d90629a
commit 0c76a249e3
71 changed files with 8941 additions and 12 deletions

View File

@@ -332,7 +332,8 @@ const de: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Modbus aktivieren'
};
export default de;

View File

@@ -332,7 +332,8 @@ const en: Translation = {
MODULES_UPDATED: 'Modules updated',
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules',
MODULES_NONE: 'No external modules detected',
RENAME: 'Rename'
RENAME: 'Rename',
ENABLE_MODBUS: 'Enable Modbus'
};
export default en;

View File

@@ -332,7 +332,8 @@ const fr: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Activer Modbus'
};
export default fr;

View File

@@ -332,7 +332,8 @@ const it: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Abilita Modbus'
};
export default it;

View File

@@ -332,7 +332,8 @@ const nl: Translation = {
MODULES_UPDATED: 'Modules geüpdatet',
MODULES_DESCRIPTION: 'Klik op de module om EMS-ESP library modules te activeren of te deactiveren',
MODULES_NONE: 'Geen externe modules gedetecteerd',
RENAME: 'Hernoemen'
RENAME: 'Hernoemen',
ENABLE_MODBUS: 'Activeer Modbus'
};
export default nl;

View File

@@ -332,7 +332,8 @@ const no: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktiver Modbus'
};
export default no;

View File

@@ -332,7 +332,8 @@ const pl: BaseTranslation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktywuj Modbus'
};
export default pl;

View File

@@ -332,6 +332,8 @@ const sk: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Povoliť Modbus'
};
export default sk;

View File

@@ -332,7 +332,8 @@ const sv: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktivera Modbus'
};
export default sv;

View File

@@ -332,7 +332,8 @@ const tr: Translation = {
MODULES_UPDATED: 'Modules updated', // TODO translate
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', // TODO translate
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename' // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Enable Modbus' // TODO translate
};
export default tr;

View File

@@ -826,6 +826,75 @@ const ApplicationSettings: FC = () => {
</Grid>
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Modbus
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.modbus_enabled}
onChange={updateFormValue}
name="modbus_enabled"
disabled={saving}
/>
}
label={LL.ENABLE_MODBUS()}
/>
{data.modbus_enabled && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_max_clients"
label={LL.AP_MAX_CLIENTS()}
fullWidth
variant="outlined"
value={numberValue(data.modbus_max_clients)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.modbus_port)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_timeout"
label="Timeout"
InputProps={{
endAdornment: <InputAdornment position="end">ms</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.modbus_timeout)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
</Grid>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button

View File

@@ -38,6 +38,10 @@ export interface Settings {
eth_phy_addr: number;
eth_clock_mode: number;
platform: string;
modbus_enabled: boolean;
modbus_port: number;
modbus_max_clients: number;
modbus_timeout: number;
}
export enum busConnectionStatus {

26
lib/eModbus/README.md Normal file
View File

@@ -0,0 +1,26 @@
<img src=https://github.com/eModbus/eModbus/blob/master/eModbusLogo.png width="33%" alt="eModbus">
**Read the docs at http://emodbus.github.io!**
![eModbus](https://github.com/eModbus/eModbus/workflows/Building/badge.svg)
This is a library to provide Modbus client (formerly known as master), server (formerly slave) and bridge/gateway functionalities for Modbus RTU, ASCII and TCP protocols.
For Modbus protocol specifications, please refer to the [Modbus.org site](https://www.modbus.org/specs.php)!
Modbus communication is done in separate tasks, so Modbus requests and responses are non-blocking. Callbacks are provided to prepare or receive the responses asynchronously.
Key features:
- for use in the Arduino framework
- designed for ESP32, various interfaces supported; async versions run also on ESP8266
- non blocking / asynchronous API
- server, client and bridge modes
- TCP (Ethernet, WiFi and Async), ASCII and RTU interfaces
- all common and user-defined Modbus standard function codes
This has been developed by enthusiasts. While we do our utmost best to make robust software, do not expect any bullet-proof, industry deployable, guaranteed software. [**See the license**](https://github.com/eModbus/eModbus/blob/master/license.md) to learn about liabilities etc.
We do welcome any ideas, suggestions, bug reports or questions. Please use the "[Issues](https://github.com/eModbus/eModbus/issues)" tab to report bugs and request new features and visit the "[Discussions](https://github.com/eModbus/eModbus/discussions)" tab for all else.
Have fun!

257
lib/eModbus/keywords.txt Normal file
View File

@@ -0,0 +1,257 @@
#######################################
# Syntax Coloring Map For the current project.
# This file was generated by doxygen2keywords.xsl.
#######################################
#######################################
# Classes and structs (KEYWORD1)
#######################################
ModbusServerTCP::ClientData KEYWORD1
CoilData KEYWORD1
Modbus::FCT KEYWORD1
ModbusBridge KEYWORD1
ModbusClient KEYWORD1
ModbusClientTCP KEYWORD1
ModbusClientRTU KEYWORD1
ModbusClientTCPasync KEYWORD1
ModbusError KEYWORD1
ModbusMessage KEYWORD1
ModbusServer KEYWORD1
ModbusServerTCP KEYWORD1
ModbusServerRTU KEYWORD1
ModbusServerTCPasync KEYWORD1
RTUutils KEYWORD1
#######################################
# Methods (KEYWORD2)
#######################################
ClientData KEYWORD2
~ClientData KEYWORD2
CoilData KEYWORD2
~CoilData KEYWORD2
coils KEYWORD2
coilsSetON KEYWORD2
coilsSetOFF KEYWORD2
FCT KEYWORD2
getType KEYWORD2
redefineType KEYWORD2
ModbusBridge KEYWORD2
attachServer KEYWORD2
addFunctionCode KEYWORD2
denyFunctionCode KEYWORD2
bridgeWorker KEYWORD2
bridgeDenyWorker KEYWORD2
onDataHandler KEYWORD2
onErrorHandler KEYWORD2
onResponseHandler KEYWORD2
getMessageCount KEYWORD2
getErrorCount KEYWORD2
resetCounts KEYWORD2
addRequest KEYWORD2
syncRequest KEYWORD2
buildErrorMsg KEYWORD2
addRequest KEYWORD2
ModbusClient KEYWORD2
waitSync KEYWORD2
ModbusClientTCPasync KEYWORD2
setTimeout KEYWORD2
setIdleTimeout KEYWORD2
setMaxInflightRequests KEYWORD2
addToQueue KEYWORD2
ModbusError KEYWORD2
getText KEYWORD2
ModbusMessage KEYWORD2
data KEYWORD2
size KEYWORD2
push_back KEYWORD2
clear KEYWORD2
resize KEYWORD2
begin KEYWORD2
end KEYWORD2
append KEYWORD2
getServerID KEYWORD2
getFunctionCode KEYWORD2
getError KEYWORD2
setFunctionCode KEYWORD2
add KEYWORD2
get KEYWORD2
setMessage KEYWORD2
setError KEYWORD2
determineFloatOrder KEYWORD2
determineDoubleOrder KEYWORD2
swapFloat KEYWORD2
swapDouble KEYWORD2
getOne KEYWORD2
registerWorker KEYWORD2
getWorker KEYWORD2
unregisterWorker KEYWORD2
isServerFor KEYWORD2
getMessageCount KEYWORD2
getErrorCount KEYWORD2
resetCounts KEYWORD2
localRequest KEYWORD2
listServer KEYWORD2
ModbusServer KEYWORD2
ModbusServerTCP KEYWORD2
activeClients KEYWORD2
start KEYWORD2
stop KEYWORD2
clientAvailable KEYWORD2
ModbusServerTCPasync KEYWORD2
isRunning KEYWORD2
calcCRC KEYWORD2
validCRC KEYWORD2
addCRC KEYWORD2
calculateInterval KEYWORD2
prepareHardwareSerial KEYWORD2
RTUutils KEYWORD2
ServerData KEYWORD2
NIL_RESPONSE KEYWORD2
ECHO_RESPONSE KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
DISCONNECTED LITERAL1
CONNECTING LITERAL1
CONNECTED LITERAL1
ANY_FUNCTION_CODE LITERAL1
READ_COIL LITERAL1
READ_DISCR_INPUT LITERAL1
READ_HOLD_REGISTER LITERAL1
READ_INPUT_REGISTER LITERAL1
WRITE_COIL LITERAL1
WRITE_HOLD_REGISTER LITERAL1
READ_EXCEPTION_SERIAL LITERAL1
DIAGNOSTICS_SERIAL LITERAL1
READ_COMM_CNT_SERIAL LITERAL1
READ_COMM_LOG_SERIAL LITERAL1
WRITE_MULT_COILS LITERAL1
WRITE_MULT_REGISTERS LITERAL1
REPORT_SERVER_ID_SERIAL LITERAL1
READ_FILE_RECORD LITERAL1
WRITE_FILE_RECORD LITERAL1
MASK_WRITE_REGISTER LITERAL1
R_W_MULT_REGISTERS LITERAL1
READ_FIFO_QUEUE LITERAL1
ENCAPSULATED_INTERFACE LITERAL1
USER_DEFINED_41 LITERAL1
USER_DEFINED_42 LITERAL1
USER_DEFINED_43 LITERAL1
USER_DEFINED_44 LITERAL1
USER_DEFINED_45 LITERAL1
USER_DEFINED_46 LITERAL1
USER_DEFINED_47 LITERAL1
USER_DEFINED_48 LITERAL1
USER_DEFINED_64 LITERAL1
USER_DEFINED_65 LITERAL1
USER_DEFINED_66 LITERAL1
USER_DEFINED_67 LITERAL1
USER_DEFINED_68 LITERAL1
USER_DEFINED_69 LITERAL1
USER_DEFINED_6A LITERAL1
USER_DEFINED_6B LITERAL1
USER_DEFINED_6C LITERAL1
USER_DEFINED_6D LITERAL1
USER_DEFINED_6E LITERAL1
SUCCESS LITERAL1
ILLEGAL_FUNCTION LITERAL1
ILLEGAL_DATA_ADDRESS LITERAL1
ILLEGAL_DATA_VALUE LITERAL1
SERVER_DEVICE_FAILURE LITERAL1
ACKNOWLEDGE LITERAL1
SERVER_DEVICE_BUSY LITERAL1
NEGATIVE_ACKNOWLEDGE LITERAL1
MEMORY_PARITY_ERROR LITERAL1
GATEWAY_PATH_UNAVAIL LITERAL1
GATEWAY_TARGET_NO_RESP LITERAL1
TIMEOUT LITERAL1
INVALID_SERVER LITERAL1
CRC_ERROR LITERAL1
FC_MISMATCH LITERAL1
SERVER_ID_MISMATCH LITERAL1
PACKET_LENGTH_ERROR LITERAL1
PARAMETER_COUNT_ERROR LITERAL1
PARAMETER_LIMIT_ERROR LITERAL1
REQUEST_QUEUE_FULL LITERAL1
ILLEGAL_IP_OR_PORT LITERAL1
IP_CONNECTION_FAILED LITERAL1
TCP_HEAD_MISMATCH LITERAL1
EMPTY_MESSAGE LITERAL1
ASCII_FRAME_ERR LITERAL1
ASCII_CRC_ERR LITERAL1
ASCII_INVALID_CHAR LITERAL1
BROADCAST_ERROR LITERAL1
UNDEFINED_ERROR LITERAL1
FC01_TYPE LITERAL1
FC07_TYPE LITERAL1
FC0F_TYPE LITERAL1
FC10_TYPE LITERAL1
FC16_TYPE LITERAL1
FC18_TYPE LITERAL1
FCGENERIC LITERAL1
FCUSER LITERAL1
FCILLEGAL LITERAL1
PrintOut LITERAL1
LOG_LEVEL LITERAL1
LOCAL_LOG_LEVEL LITERAL1
LOG_LEVEL_NONE LITERAL1
LOG_LEVEL_CRITICAL LITERAL1
LOG_LEVEL_ERROR LITERAL1
LOG_LEVEL_WARNING LITERAL1
LOG_LEVEL_INFO LITERAL1
LOG_LEVEL_DEBUG LITERAL1
LOG_LEVEL_VERBOSE LITERAL1
LL_RED LITERAL1
LL_GREEN LITERAL1
LL_YELLOW LITERAL1
LL_BLUE LITERAL1
LL_MAGENTA LITERAL1
LL_CYAN LITERAL1
LL_NORM LITERAL1
LOG_HEADER LITERAL1
LOG_LINE_C LITERAL1
LOG_LINE_E LITERAL1
LOG_LINE_T LITERAL1
LOG_RAW_C LITERAL1
LOG_RAW_E LITERAL1
LOG_RAW_T LITERAL1
HEX_DUMP_T LITERAL1
LOG_N LITERAL1
LOGRAW_N LITERAL1
HEXDUMP_N LITERAL1
LOG_C LITERAL1
LOGRAW_C LITERAL1
HEXDUMP_C LITERAL1
LOG_E LITERAL1
LOGRAW_E LITERAL1
HEXDUMP_E LITERAL1
LOG_W LITERAL1
LOGRAW_W LITERAL1
HEXDUMP_W LITERAL1
LOG_I LITERAL1
LOGRAW_I LITERAL1
HEXDUMP_I LITERAL1
LOG_D LITERAL1
LOGRAW_D LITERAL1
HEXDUMP_D LITERAL1
LOG_V LITERAL1
LOGRAW_V LITERAL1
HEXDUMP_V LITERAL1
LOCAL_LOG_LEVEL LITERAL1
TCP_SERVER LITERAL1
RTU_SERVER LITERAL1
SERVER_END LITERAL1
LOCAL_LOG_LEVEL LITERAL1
DEFAULTTIMEOUT LITERAL1
DEFAULTIDLETIME LITERAL1
LOCAL_LOG_LEVEL LITERAL1
SERVER_END LITERAL1
SWAP_BYTES LITERAL1
SWAP_REGISTERS LITERAL1
SWAP_WORDS LITERAL1
SWAP_NIBBLES LITERAL1
LOCK_GUARD LITERAL1

46
lib/eModbus/library.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "eModbus",
"version": "1.7.0",
"keywords": "Arduino, ESP32, Modbus, RTU, ASCII, ModbusASCII, ModbusRTU, ModbusTCP",
"description": "ModbusRTU, ModbusASCII and ModbusTCP functions for ESP32",
"homepage": "https://emodbus.github.io",
"license": "MIT",
"authors": [
{
"name": "Bert Melis",
"url": "https://github.com/bertmelis",
"maintainer": true
},
{
"name": "Michael Harwerth",
"url": "https://github.com/Miq1",
"email": "miq1@gmx.de",
"maintainer": true
}
],
"repository": {
"type": "git",
"url": "https://github.com/eModbus/eModbus",
"branch": "master"
},
"export": {
"include":
[
"src/*.cpp",
"src/*.h",
"examples/*",
"Test/*",
".gitignore",
"README.md",
"license.md",
"keywords.txt",
"library.properties",
"library.json"
]
},
"frameworks": "arduino",
"platforms": [
"espressif32",
"espressif8266"
]
}

View File

@@ -0,0 +1,9 @@
name=eModbus
version=1.7.0
author=bertmelis,Miq1 <miq1@gmx.de>
maintainer=Miq1 <miq1@gmx.de>
sentence=eModbus provides Modbus RTU, ASCII and TCP functions for ESP32.
paragraph=This library is non-blocking for the program using it. Modbus requests and responses will be returned to user-supplied callback functions. All Modbus function codes are supported implicitly, the codes specified by the Modbus specs are parameter-checked.
category=Communication
url=https://github.com/eModbus/eModbus
architectures=esp32,FreeRTOS

7
lib/eModbus/license.md Normal file
View File

@@ -0,0 +1,7 @@
#### Copyright 2020 Michael Harwerth, Bert Melis and the contributors to eModbus (MIT license)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#### The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,554 @@
// =================================================================================================
// eModbus: Copyright 2020, 2021 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "CoilData.h"
#undef LOCAL_LOG_LEVEL
#include "Logging.h"
// Constructor: optional size in bits, optional initial value for all bits
// Maximum size is 2000 coils (=250 bytes)
CoilData::CoilData(uint16_t size, bool initValue) :
CDsize(0),
CDbyteSize(0),
CDbuffer(nullptr) {
// Limit the size to 2000 (Modbus rules)
if (size > 2000) size = 2000;
// Do we have a size?
if (size) {
// Calculate number of bytes needed
CDbyteSize = byteIndex(size - 1) + 1;
// Allocate and init buffer
CDbuffer = new uint8_t[CDbyteSize];
memset(CDbuffer, initValue ? 0xFF : 0, CDbyteSize);
if (initValue) {
CDbuffer[CDbyteSize - 1] &= CDfilter[bitIndex(size - 1)];
}
CDsize = size;
}
}
// Alternate constructor, taking a "1101..." bit image char array to init
CoilData::CoilData(const char *initVector) :
CDsize(0),
CDbyteSize(0),
CDbuffer(nullptr) {
// Init with bit image array.
setVector(initVector);
}
// Destructor: take care of cleaning up
CoilData::~CoilData() {
if (CDbuffer) {
delete CDbuffer;
}
}
// Assignment operator
CoilData& CoilData::operator=(const CoilData& m) {
// Remove old data
if (CDbuffer) {
delete CDbuffer;
}
// Are coils in source?
if (m.CDsize > 0) {
// Yes. Allocate new buffer and copy data
CDbuffer = new uint8_t[m.CDbyteSize];
memcpy(CDbuffer, m.CDbuffer, m.CDbyteSize);
CDsize = m.CDsize;
CDbyteSize = m.CDbyteSize;
} else {
// No, leave buffer empty
CDsize = 0;
CDbyteSize = 0;
CDbuffer = nullptr;
}
return *this;
}
// Copy constructor
CoilData::CoilData(const CoilData& m) :
CDsize(0),
CDbyteSize(0),
CDbuffer(nullptr) {
// Has the source coils at all?
if (m.CDsize > 0) {
// Yes. Allocate new buffer and copy data
CDbuffer = new uint8_t[m.CDbyteSize];
memcpy(CDbuffer, m.CDbuffer, m.CDbyteSize);
CDsize = m.CDsize;
CDbyteSize = m.CDbyteSize;
}
}
#ifndef NO_MOVE
// Move constructor
CoilData::CoilData(CoilData&& m) {
// Copy all data
CDbuffer = m.CDbuffer;
CDsize = m.CDsize;
CDbyteSize = m.CDbyteSize;
// Then clear source
m.CDbuffer = nullptr;
m.CDsize = 0;
m.CDbyteSize = 0;
}
// Move assignment
CoilData& CoilData::operator=(CoilData&& m) {
// Remove buffer, if already allocated
if (CDbuffer) {
delete CDbuffer;
}
// Are there coils in the source at all?
if (m.CDsize > 0) {
// Yes. Copy over all data
CDbuffer = m.CDbuffer;
CDsize = m.CDsize;
CDbyteSize = m.CDbyteSize;
// Then clear source
m.CDbuffer = nullptr;
m.CDsize = 0;
m.CDbyteSize = 0;
} else {
// No, leave object empty.
CDbuffer = nullptr;
CDsize = 0;
CDbyteSize = 0;
}
return *this;
}
#endif
// Comparison operators
bool CoilData::operator==(const CoilData& m) {
// Self-compare is always true
if (this == &m) return true;
// Different sizes are never equal
if (CDsize != m.CDsize) return false;
// Compare the data
if (CDsize > 0 && memcmp(CDbuffer, m.CDbuffer, CDbyteSize)) return false;
return true;
}
// Inequality: invert the result of the equality comparison
bool CoilData::operator!=(const CoilData& m) {
return !(*this == m);
}
// Assignment of a bit image char array to re-init
CoilData& CoilData::operator=(const char *initVector) {
// setVector() may be unsuccessful - then data is deleted!
setVector(initVector);
return *this;
}
// If used as vector<uint8_t>, return a complete slice
CoilData::operator vector<uint8_t> const () {
// Create new vector to return
vector<uint8_t> retval;
if (CDsize > 0) {
// Copy over all buffer content
retval.assign(CDbuffer, CDbuffer + CDbyteSize);
}
// return the copy (or an empty vector)
return retval;
}
// slice: return a CoilData object with coils shifted leftmost
// will return empty object if illegal parameters are detected
CoilData CoilData::slice(uint16_t start, uint16_t length) {
CoilData retval;
// Any slice of an empty coilset is an empty coilset ;)
if (CDsize == 0) return retval;
// If start is beyond the available coils, return empty slice
if (start > CDsize) return retval;
// length default is all up to the end
if (length == 0) length = CDsize - start;
// Does the requested slice fit in the buffer?
if ((start + length) <= CDsize) {
// Yes, it does. Extend return object
retval = CoilData(length);
// Loop over all requested bits
for (uint16_t i = start; i < start + length; ++i) {
if (CDbuffer[byteIndex(i)] & (1 << bitIndex(i))) {
retval.set(i - start, true);
}
}
}
return retval;
}
// operator[]: return value of a single coil
bool CoilData::operator[](uint16_t index) const {
if (index < CDsize) {
return (CDbuffer[byteIndex(index)] & (1 << bitIndex(index))) ? true : false;
}
// Wrong parameter -> always return false
return false;
}
// set functions to change coil value(s)
// Will return true if done, false if impossible (wrong address or data)
// set #1: alter one single coil
bool CoilData::set(uint16_t index, bool value) {
// Within coils?
if (index < CDsize) {
// Yes. Determine affected byte and bit therein
uint16_t by = byteIndex(index);
uint8_t mask = 1 << bitIndex(index);
// Stamp out bit
CDbuffer[by] &= ~mask;
// If required, set it to 1 now
if (value) {
CDbuffer[by] |= mask;
}
return true;
}
// Wrong parameter -> always return false
return false;
}
// set #2: alter a group of coils, overwriting it by the bits from vector newValue
bool CoilData::set(uint16_t start, uint16_t length, vector<uint8_t> newValue) {
// Does the vector contain enough data for the specified size?
if (newValue.size() >= (size_t)(byteIndex(length - 1) + 1)) {
// Yes, we safely may call set #3 with it
return set(start, length, newValue.data());
}
return false;
}
// set #3: alter a group of coils, overwriting it by the bits from uint8_t buffer newValue
// **** Watch out! ****
// This may be a potential risk if newValue is pointing to an array shorter than required.
// Then heap data behind the array may be used to set coils!
bool CoilData::set(uint16_t start, uint16_t length, uint8_t *newValue) {
// Does the requested slice fit in the buffer?
if (length && (start + length) <= CDsize) {
// Yes, it does.
// Prepare pointers to the source byte and the bit within
uint8_t *cp = newValue;
uint8_t bitPtr = 0;
// Loop over all bits to be set
for (uint16_t i = start; i < start + length; i++) {
// Get affected byte
uint8_t by = byteIndex(i);
// Calculate single-bit mask in target byte
uint8_t mask = 1 << bitIndex(i);
// Stamp out bit
CDbuffer[by] &= ~mask;
// is source bit set?
if (*cp & (1 << bitPtr)) {
// Yes. Set it in target as well
CDbuffer[by] |= mask;
}
// Advance source bit ptr
bitPtr++;
// Overflow?
if (bitPtr >= 8) {
// Yes. move pointers to first bit in next source byte
bitPtr = 0;
cp++;
}
}
return true;
}
return false;
}
// set #4: alter a group of coils, overwriting it by the coils in another CoilData object
// Setting stops when either target storage or source coils are exhausted
bool CoilData::set(uint16_t index, const CoilData& c) {
// if source object is empty, return false
if (c.empty()) return false;
// If target is empty, or index is beyond coils, return false
if (CDsize == 0 || index >= CDsize) return false;
// Take the minimum of remaining coils after index and the length of c
uint16_t length = CDsize - index;
if (c.coils() < length) length = c.coils();
// Loop over all coils to be copied
for (uint16_t i = index; i < index + length; ++i) {
set(i, c[i - index]);
}
return true;
}
// set #5: alter a group of coils, overwriting it by a bit image array
// Setting stops when either target storage or source bits are exhausted
bool CoilData::set(uint16_t index, const char *initVector) {
// if target is empty or index is beyond coils, return false
if (CDsize == 0 || index >= CDsize) return false;
// We do a single pass on the bit image array, until it ends or the target is exhausted
const char *cp = initVector; // pointer to source array
bool skipFlag = false; // Signal next character irrelevant
while (*cp && index < CDsize) {
switch (*cp) {
case '1': // A valid 1 bit
case '0': // A valid 0 bit
// Shall we ignore it?
if (skipFlag) {
// Yes. just reset the ignore flag
skipFlag = false;
} else {
// No, we can set it. First stamp out the existing bit
CDbuffer[byteIndex(index)] &= ~(1 << bitIndex(index));
// Do we have a 1 bit here?
if (*cp == '1') {
// Yes. set it in coil storage
CDbuffer[byteIndex(index)] |= (1 << bitIndex(index));
}
index++;
}
break;
case '_': // Skip next
skipFlag = true;
break;
default: // anything else
skipFlag = false;
break;
}
cp++;
}
return true;
}
// Comparison against bit image array
bool CoilData::operator==(const char *initVector) {
const char *cp = initVector; // pointer to source array
bool skipFlag = false; // Signal next character irrelevant
uint16_t index = 0;
// We do a single pass on the bit image array, until it ends or the target is exhausted
while (*cp && index < CDsize) {
switch (*cp) {
case '1': // A valid 1 bit
case '0': // A valid 0 bit
// Shall we ignore it?
if (skipFlag) {
// Yes. just reset the ignore flag
skipFlag = false;
} else {
// No, we can compare it
uint8_t value = CDbuffer[byteIndex(index)] & (1 << bitIndex(index));
// Do we have a 1 bit here?
if (*cp == '1') {
// Yes. Is the source different? Then we can stop
if (value == 0) return false;
} else {
// No, it is a 0. Different?
if (value) return false;
}
index++;
}
break;
case '_': // Skip next
skipFlag = true;
break;
default: // anything else
skipFlag = false;
break;
}
cp++;
}
// So far everything was equal, but we may have more bits in the image array!
if (*cp) {
// There is more. Check for more valid bits
while (*cp) {
switch (*cp) {
case '1': // A valid 1 bit
case '0': // A valid 0 bit
// Shall we ignore it?
if (skipFlag) {
// Yes. just reset the ignore flag
skipFlag = false;
} else {
// No, a valid bit that exceeds the target coils count
return false;
}
break;
case '_': // Skip next
skipFlag = true;
break;
default: // anything else
skipFlag = false;
break;
}
cp++;
}
}
return true;
}
bool CoilData::operator!=(const char *initVector) {
return !(*this == initVector);
}
// Init all coils by a readable bit image array
bool CoilData::setVector(const char *initVector) {
uint16_t length = 0; // resulting bit pattern length
const char *cp = initVector; // pointer to source array
bool skipFlag = false; // Signal next character irrelevant
// Do a first pass to count all valid bits in array
while (*cp) {
switch (*cp) {
case '1': // A valid 1 bit
case '0': // A valid 0 bit
// Shall we ignore it?
if (skipFlag) {
// Yes. just reset the ignore flag
skipFlag = false;
} else {
// No, we can count it
length++;
}
break;
case '_': // Skip next
skipFlag = true;
break;
default: // anything else
skipFlag = false;
break;
}
cp++;
}
// If there are coils already, trash them.
if (CDbuffer) {
delete CDbuffer;
}
CDsize = 0;
CDbyteSize = 0;
// Did we count a manageable number?
if (length && length <= 2000) {
// Yes. Init the coils
CDsize = length;
CDbyteSize = byteIndex(length - 1) + 1;
// Allocate new coil storage
CDbuffer = new uint8_t[CDbyteSize];
memset(CDbuffer, 0, CDbyteSize);
// Prepare second loop
uint16_t ptr = 0; // bit pointer in coil storage
skipFlag = false;
cp = initVector;
// Do a second pass, converting 1 and 0 into coils (bits)
// loop as above, only difference is setting the bits
while (*cp) {
switch (*cp) {
case '1':
case '0':
if (skipFlag) {
skipFlag = false;
} else {
// Do we have a 1 bit here?
if (*cp == '1') {
// Yes. set it in coil storage
CDbuffer[byteIndex(ptr)] |= (1 << bitIndex(ptr));
}
// advance bit pointer in any case 0 or 1
ptr++;
}
break;
case '_':
skipFlag = true;
break;
default:
skipFlag = false;
break;
}
cp++;
}
// We had content, so return true
return true;
}
// No valid bits found, return false
return false;
}
// init: set all coils to 1 or 0 (default)
void CoilData::init(bool value) {
if (CDsize > 0) {
memset(CDbuffer, value ? 0xFF : 0, CDbyteSize);
// Stamp out overhang bits
CDbuffer[CDbyteSize - 1] &= CDfilter[bitIndex(CDsize - 1)];
}
}
// Return number of coils set to 1 (or not)
// Uses Brian Kernighan's algorithm!
uint16_t CoilData::coilsSetON() const {
uint16_t count = 0;
// Do we have coils at all?
if (CDbyteSize) {
// Yes. Loop over all bytes summing up the '1' bits
for (uint8_t i = 0; i < CDbyteSize; ++i) {
uint8_t by = CDbuffer[i];
while (by) {
by &= by - 1; // this clears the LSB-most set bit
count++;
}
}
}
return count;
}
uint16_t CoilData::coilsSetOFF() const {
return CDsize - coilsSetON();
}
#if !IS_LINUX
// Not for Linux for the Print reference!
// Print out a coil storage in readable form to ease debugging
void CoilData::print(const char *label, Print& s) {
uint8_t bitptr = 0;
uint8_t labellen = strlen(label);
uint8_t pos = labellen;
// Put out the label
s.print(label);
// Print out all coils as "1" or "0"
for (uint16_t i = 0; i < CDsize; ++i) {
s.print((CDbuffer[byteIndex(i)] & (1 << bitptr)) ? "1" : "0");
pos++;
// Have a blank after every group of 4
if (i % 4 == 3) {
// Have a line break if > 80 characters, including the last group of 4
if (pos >= 80) {
s.println("");
pos = 0;
// Leave a nice empty space below the label
while (pos++ < labellen) {
s.print(" ");
}
} else {
s.print(" ");
pos++;
}
}
bitptr++;
bitptr &= 0x07;
}
s.println("");
}
#endif

122
lib/eModbus/src/CoilData.h Normal file
View File

@@ -0,0 +1,122 @@
// =================================================================================================
// eModbus: Copyright 2020, 2021 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _COILDATA_H
#define _COILDATA_H
#include <vector>
#include <cstdint>
#include "options.h"
using std::vector;
// CoilData: representing Modbus coil (=bit) values
class CoilData {
public:
// Constructor: optional size in bits, optional initial value for all bits
// Maximum size is 2000 coils (=250 bytes)
explicit CoilData(uint16_t size = 0, bool initValue = false);
// Alternate constructor, taking a "1101..." bit image char array to init
explicit CoilData(const char *initVector);
// Destructor: take care of cleaning up
~CoilData();
// Assignment operator
CoilData& operator=(const CoilData& m);
// Copy constructor
CoilData(const CoilData& m);
#ifndef NO_MOVE
// Move constructor
CoilData(CoilData&& m);
// Move assignment
CoilData& operator=(CoilData&& m);
#endif
// Comparison operators
bool operator==(const CoilData& m);
bool operator!=(const CoilData& m);
bool operator==(const char *initVector);
bool operator!=(const char *initVector);
// Assignment of a bit image char array to re-init
CoilData& operator=(const char *initVector);
// If used as vector<uint8_t>, return the complete set
operator vector<uint8_t> const ();
// slice: return a new CoilData object with coils shifted leftmost
// will return empty set if illegal parameters are detected
// Default start is first coil, default length all to the end
CoilData slice(uint16_t start = 0, uint16_t length = 0);
// operator[]: return value of a single coil
bool operator[](uint16_t index) const;
// Set functions to change coil value(s)
// Will return true if done, false if impossible (wrong address or data)
// set #1: alter one single coil
bool set(uint16_t index, bool value);
// set #2: alter a group of coils, overwriting it by the bits from newValue
bool set(uint16_t index, uint16_t length, vector<uint8_t> newValue);
// set #3: alter a group of coils, overwriting it by the bits from unit8_t buffer newValue
bool set(uint16_t index, uint16_t length, uint8_t *newValue);
// set #4: alter a group of coils, overwriting it by the coils in another CoilData object
// Setting stops when either target storage or source coils are exhausted
bool set(uint16_t index, const CoilData& c);
// set #5: alter a group of coils, overwriting it by a bit image array
// Setting stops when either target storage or source bits are exhausted
bool set(uint16_t index, const char *initVector);
// (Re-)init complete coil set to 1 or 0
void init(bool value = false);
// get size in coils
inline uint16_t coils() const { return CDsize; }
// Raw access to coil data buffer
inline uint8_t *data() const { return CDbuffer; };
inline uint8_t size() const { return CDbyteSize; };
// Test if there are any coils in object
inline bool empty() const { return (CDsize >0) ? true : false; }
inline operator bool () const { return empty(); }
// Return number of coils set to 1 (or ON)
uint16_t coilsSetON() const;
// Return number of coils set to 0 (or OFF)
uint16_t coilsSetOFF() const;
#if !LINUX
// Helper function to dump out coils in logical order
void print(const char *label, Print& s);
#endif
protected:
// bit masks for bits left of a bit index in a byte
const uint8_t CDfilter[8] = { 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF };
// Calculate byte index and bit index within that byte
inline uint8_t byteIndex(uint16_t index) const { return index >> 3; }
inline uint8_t bitIndex(uint16_t index) const { return index & 0x07; }
// Calculate reversed bit sequence for a byte (taken from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits)
inline uint8_t reverseBits(uint8_t b) { return ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16; }
// (Re-)init with bit image vector
bool setVector(const char *initVector);
uint16_t CDsize; // Size of the CoilData store in bits
uint8_t CDbyteSize; // Size in bytes
uint8_t *CDbuffer; // Pointer to bit storage
};
#endif

View File

@@ -0,0 +1,68 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "Logging.h"
#include <cinttypes>
int MBUlogLvl = LOG_LEVEL;
#if IS_LINUX
#define PrintOut printf
void logHexDump(const char *letter, const char *label, const uint8_t *data, const size_t length) {
#else
Print *LOGDEVICE = &Serial;
#define PrintOut output->printf
void logHexDump(Print *output, const char *letter, const char *label, const uint8_t *data, const size_t length) {
#endif
size_t cnt = 0;
size_t step = 0;
char limiter = '|';
// Use line buffer to speed up output
const uint16_t BUFLEN(80);
const uint16_t ascOffset(61);
char linebuf[BUFLEN];
char *cp = linebuf;
const char HEXDIGIT[] = "0123456789ABCDEF";
// Print out header
PrintOut ("[%s] %s: @%" PRIXPTR "/%" PRIu32 ":\n", letter, label, (uintptr_t)data, (uint32_t)(length & 0xFFFFFFFF));
// loop over data in steps of 16
for (cnt = 0; cnt < length; ++cnt) {
step = cnt % 16;
// New line?
if (step == 0) {
// Yes. Clear line and print address header
memset(linebuf, ' ', BUFLEN);
linebuf[60] = limiter;
linebuf[77] = limiter;
linebuf[78] = '\n';
linebuf[BUFLEN - 1] = 0;
snprintf(linebuf, BUFLEN, " %c %04X: ", limiter, (uint16_t)(cnt & 0xFFFF));
cp = linebuf + strlen(linebuf);
// No, but first block of 8 done?
} else if (step == 8) {
// Yes, put out additional space
cp++;
}
// Print data byte
uint8_t c = data[cnt];
*cp++ = HEXDIGIT[(c >> 4) & 0x0F];
*cp++ = HEXDIGIT[c & 0x0F];
*cp++ = ' ';
if (c >= 32 && c <= 127) linebuf[ascOffset + step] = c;
else linebuf[ascOffset + step] = '.';
// Line end?
if (step == 15) {
// Yes, print line
PrintOut ("%s", linebuf);
}
}
// Unfinished line?
if (length && step != 15) {
// Yes, print line
PrintOut ("%s", linebuf);
}
}

181
lib/eModbus/src/Logging.h Normal file
View File

@@ -0,0 +1,181 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_ERROR
#endif
#ifndef LOCAL_LOG_LEVEL
#define LOCAL_LOG_LEVEL LOG_LEVEL
#endif
// The following needs to be defined only once
#ifndef _MODBUS_LOGGING
#define _MODBUS_LOGGING
#include "options.h"
#define LOG_LEVEL_NONE (0)
#define LOG_LEVEL_CRITICAL (1)
#define LOG_LEVEL_ERROR (2)
#define LOG_LEVEL_WARNING (3)
#define LOG_LEVEL_INFO (4)
#define LOG_LEVEL_DEBUG (5)
#define LOG_LEVEL_VERBOSE (6)
#define LL_RED "\e[1;31m"
#define LL_GREEN "\e[32m"
#define LL_YELLOW "\e[1;33m"
#define LL_BLUE "\e[34m"
#define LL_MAGENTA "\e[35m"
#define LL_CYAN "\e[36m"
#define LL_NORM "\e[0m"
#define LOG_HEADER(x) "[" #x "] %lu| %-20s [%4d] %s: "
constexpr const char* str_end(const char *str) {
return *str ? str_end(str + 1) : str;
}
constexpr bool str_slant(const char *str) {
return ((*str == '/') || (*str == '\\')) ? true : (*str ? str_slant(str + 1) : false);
}
constexpr const char* r_slant(const char* str) {
return ((*str == '/') || (*str == '\\')) ? (str + 1) : r_slant(str - 1);
}
constexpr const char* file_name(const char* str) {
return str_slant(str) ? r_slant(str_end(str)) : str;
}
#if IS_LINUX
void logHexDump(const char *letter, const char *label, const uint8_t *data, const size_t length);
#else
extern Print *LOGDEVICE;
void logHexDump(Print *output, const char *letter, const char *label, const uint8_t *data, const size_t length);
#endif
extern int MBUlogLvl;
#endif // _MODBUS_LOGGING
// The remainder may need to be redefined if LOCAL_LOG_LEVEL was set differently before
#ifdef LOG_LINE_T
#undef LOG_LINE_C
#undef LOG_LINE_E
#undef LOG_LINE_T
#undef LOG_RAW_C
#undef LOG_RAW_E
#undef LOG_RAW_T
#undef HEX_DUMP_T
#undef LOG_N
#undef LOG_C
#undef LOG_E
#undef LOG_W
#undef LOG_I
#undef LOG_D
#undef LOG_V
#undef LOGRAW_N
#undef LOGRAW_C
#undef LOGRAW_E
#undef LOGRAW_W
#undef LOGRAW_I
#undef LOGRAW_D
#undef LOGRAW_V
#undef HEXDUMP_N
#undef HEXDUMP_C
#undef HEXDUMP_E
#undef HEXDUMP_W
#undef HEXDUMP_I
#undef HEXDUMP_D
#undef HEXDUMP_V
#endif
// Now we can define the macros based on LOCAL_LOG_LEVEL
#if IS_LINUX
#define LOG_LINE_C(level, x, format, ...) if (MBUlogLvl >= level) printf(LL_RED LOG_HEADER(x) format LL_NORM, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_LINE_E(level, x, format, ...) if (MBUlogLvl >= level) printf(LL_YELLOW LOG_HEADER(x) format LL_NORM, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_LINE_T(level, x, format, ...) if (MBUlogLvl >= level) printf(LOG_HEADER(x) format, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_RAW_C(level, x, format, ...) if (MBUlogLvl >= level) printf(LL_RED format LL_NORM, ##__VA_ARGS__)
#define LOG_RAW_E(level, x, format, ...) if (MBUlogLvl >= level) printf(LL_YELLOW format LL_NORM, ##__VA_ARGS__)
#define LOG_RAW_T(level, x, format, ...) if (MBUlogLvl >= level) printf(format, ##__VA_ARGS__)
#define HEX_DUMP_T(x, level, label, address, length) if (MBUlogLvl >= level) logHexDump(#x, label, address, length)
#else
#define LOG_LINE_C(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(LL_RED LOG_HEADER(x) format LL_NORM, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_LINE_E(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(LL_YELLOW LOG_HEADER(x) format LL_NORM, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_LINE_T(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(LOG_HEADER(x) format, millis(), file_name(__FILE__), __LINE__, __func__, ##__VA_ARGS__)
#define LOG_RAW_C(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(LL_RED format LL_NORM, ##__VA_ARGS__)
#define LOG_RAW_E(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(LL_YELLOW format LL_NORM, ##__VA_ARGS__)
#define LOG_RAW_T(level, x, format, ...) if (MBUlogLvl >= level) LOGDEVICE->printf(format, ##__VA_ARGS__)
#define HEX_DUMP_T(x, level, label, address, length) if (MBUlogLvl >= level) logHexDump(LOGDEVICE, #x, label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_NONE
#define LOG_N(format, ...) LOG_LINE_T(LOG_LEVEL_NONE, N, format, ##__VA_ARGS__)
#define LOGRAW_N(format, ...) LOG_RAW_T(LOG_LEVEL_NONE, N, format, ##__VA_ARGS__)
#define HEXDUMP_N(label, address, length) HEX_DUMP_T(N, LOG_LEVEL_NONE, label, address, length)
#else
#define LOG_N(format, ...)
#define LOGRAW_N(format, ...)
#define HEXDUMP_N(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_CRITICAL
#define LOG_C(format, ...) LOG_LINE_C(LOG_LEVEL_CRITICAL, C, format, ##__VA_ARGS__)
#define LOGRAW_C(format, ...) LOG_RAW_C(LOG_LEVEL_CRITICAL, C, format, ##__VA_ARGS__)
#define HEXDUMP_C(label, address, length) HEX_DUMP_T(C, LOG_LEVEL_CRITICAL, label, address, length)
#else
#define LOG_C(format, ...)
#define LOGRAW_C(format, ...)
#define HEXDUMP_C(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_ERROR
#define LOG_E(format, ...) LOG_LINE_E(LOG_LEVEL_ERROR, E, format, ##__VA_ARGS__)
#define LOGRAW_E(format, ...) LOG_RAW_E(LOG_LEVEL_ERROR, E, format, ##__VA_ARGS__)
#define HEXDUMP_E(label, address, length) HEX_DUMP_T(E, LOG_LEVEL_ERROR, label, address, length)
#else
#define LOG_E(format, ...)
#define LOGRAW_E(format, ...)
#define HEXDUMP_E(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_WARNING
#define LOG_W(format, ...) LOG_LINE_T(LOG_LEVEL_WARNING, W, format, ##__VA_ARGS__)
#define LOGRAW_W(format, ...) LOG_RAW_T(LOG_LEVEL_WARNING, W, format, ##__VA_ARGS__)
#define HEXDUMP_W(label, address, length) HEX_DUMP_T(W, LOG_LEVEL_WARNING, label, address, length)
#else
#define LOG_W(format, ...)
#define LOGRAW_W(format, ...)
#define HEXDUMP_W(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_INFO
#define LOG_I(format, ...) LOG_LINE_T(LOG_LEVEL_INFO, I, format, ##__VA_ARGS__)
#define LOGRAW_I(format, ...) LOG_RAW_T(LOG_LEVEL_INFO, I, format, ##__VA_ARGS__)
#define HEXDUMP_I(label, address, length) HEX_DUMP_T(I, LOG_LEVEL_INFO, label, address, length)
#else
#define LOG_I(format, ...)
#define LOGRAW_I(format, ...)
#define HEXDUMP_I(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_DEBUG
#define LOG_D(format, ...) LOG_LINE_T(LOG_LEVEL_DEBUG, D, format, ##__VA_ARGS__)
#define LOGRAW_D(format, ...) LOG_RAW_T(LOG_LEVEL_DEBUG, D, format, ##__VA_ARGS__)
#define HEXDUMP_D(label, address, length) HEX_DUMP_T(D, LOG_LEVEL_DEBUG, label, address, length)
#else
#define LOG_D(format, ...)
#define LOGRAW_D(format, ...)
#define HEXDUMP_D(label, address, length)
#endif
#if LOCAL_LOG_LEVEL >= LOG_LEVEL_VERBOSE
#define LOG_V(format, ...) LOG_LINE_T(LOG_LEVEL_VERBOSE, V, format, ##__VA_ARGS__)
#define LOGRAW_V(format, ...) LOG_RAW_T(LOG_LEVEL_VERBOSE, V, format, ##__VA_ARGS__)
#define HEXDUMP_V(label, address, length) HEX_DUMP_T(V, LOG_LEVEL_VERBOSE, label, address, length)
#else
#define LOG_V(format, ...)
#define LOGRAW_V(format, ...)
#define HEXDUMP_V(label, address, length)
#endif

View File

@@ -0,0 +1,21 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_ETHERNET_H
#define _MODBUS_BRIDGE_ETHERNET_H
#include "options.h"
#if HAS_ETHERNET == 1
#include <Ethernet.h>
#include <SPI.h>
#undef SERVER_END
#define SERVER_END // NIL for Ethernet
#include "ModbusServerTCPtemp.h"
#include "ModbusBridgeTemp.h"
using ModbusBridgeEthernet = ModbusBridge<ModbusServerTCP<EthernetServer, EthernetClient>>;
#endif
#endif

View File

@@ -0,0 +1,14 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_RTU_H
#define _MODBUS_BRIDGE_RTU_H
#include "options.h"
#include "ModbusServerRTU.h"
#include "ModbusBridgeTemp.h"
#include "RTUutils.h"
using ModbusBridgeRTU = ModbusBridge<ModbusServerRTU>;
#endif

View File

@@ -0,0 +1,199 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_TEMP_H
#define _MODBUS_BRIDGE_TEMP_H
#include <map>
#include <functional>
#include "ModbusClient.h"
#include "ModbusClientTCP.h" // Needed for client.setTarget()
#include "RTUutils.h" // Needed for RTScallback
#undef LOCAL_LOG_LEVEL
#define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
using std::bind;
using std::placeholders::_1;
// Known server types: TCP (client, host/port) and RTU (client)
enum ServerType : uint8_t { TCP_SERVER, RTU_SERVER };
// Bridge class template, takes one of ModbusServerRTU, ModbusServerWiFi, ModbusServerEthernet or ModbusServerTCPasync as parameter
template<typename SERVERCLASS>
class ModbusBridge : public SERVERCLASS {
public:
// Constructor for TCP server variants.
ModbusBridge();
// Constructors for the RTU variant. Parameters as are for ModbusServerRTU
ModbusBridge(HardwareSerial& serial, uint32_t timeout, int rtsPin = -1);
ModbusBridge(HardwareSerial& serial, uint32_t timeout, RTScallback rts);
// Destructor
~ModbusBridge();
// Method to link external servers to the bridge
bool attachServer(uint8_t aliasID, uint8_t serverID, uint8_t functionCode, ModbusClient *client, IPAddress host = IPAddress(0, 0, 0, 0), uint16_t port = 0);
// Link another function code to the server
bool addFunctionCode(uint8_t aliasID, uint8_t functionCode);
// Block a function code (respond with ILLEGAL_FUNCTION error)
bool denyFunctionCode(uint8_t aliasID, uint8_t functionCode);
protected:
// ServerData holds all data necessary to address a single server
struct ServerData {
uint8_t serverID; // External server id
ModbusClient *client; // client to be used to request the server
ServerType serverType; // TCP_SERVER or RTU_SERVER
IPAddress host; // TCP: host IP address, else 0.0.0.0
uint16_t port; // TCP: host port number, else 0
// RTU constructor
ServerData(uint8_t sid, ModbusClient *c) :
serverID(sid),
client(c),
serverType(RTU_SERVER),
host(IPAddress(0, 0, 0, 0)),
port(0) {}
// TCP constructor
ServerData(uint8_t sid, ModbusClient *c, IPAddress h, uint16_t p) :
serverID(sid),
client(c),
serverType(TCP_SERVER),
host(h),
port(p) {}
};
// Default worker functions
ModbusMessage bridgeWorker(ModbusMessage msg);
ModbusMessage bridgeDenyWorker(ModbusMessage msg);
// Map of servers attached
std::map<uint8_t, ServerData *> servers;
};
// Constructor for TCP variants
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge() :
SERVERCLASS() { }
// Constructors for RTU variant
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge(HardwareSerial& serial, uint32_t timeout, int rtsPin) :
SERVERCLASS(serial, timeout, rtsPin) { }
// Alternate constructors for RTU variant
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::ModbusBridge(HardwareSerial& serial, uint32_t timeout, RTScallback rts) :
SERVERCLASS(serial, timeout, rts) { }
// Destructor
template<typename SERVERCLASS>
ModbusBridge<SERVERCLASS>::~ModbusBridge() {
// Release ServerData storage in servers array
for (auto itr = servers.begin(); itr != servers.end(); itr++) {
delete (itr->second);
}
servers.clear();
}
// attachServer: memorize the access data for an external server with ID serverID under bridge ID aliasID
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::attachServer(uint8_t aliasID, uint8_t serverID, uint8_t functionCode, ModbusClient *client, IPAddress host, uint16_t port) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) == servers.end()) {
// No. Store server data in map.
// Do we have a port number?
if (port != 0) {
// Yes. Must be a TCP client
servers[aliasID] = new ServerData(serverID, static_cast<ModbusClient *>(client), host, port);
LOG_D("(TCP): %02X->%02X %d.%d.%d.%d:%d\n", aliasID, serverID, host[0], host[1], host[2], host[3], port);
} else {
// No - RTU client required
servers[aliasID] = new ServerData(serverID, static_cast<ModbusClient *>(client));
LOG_D("(RTU): %02X->%02X\n", aliasID, serverID);
}
}
// Register the server/FC combination for the bridgeWorker
addFunctionCode(aliasID, functionCode);
return true;
}
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::addFunctionCode(uint8_t aliasID, uint8_t functionCode) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) != servers.end()) {
// Yes. Link server to own worker function
this->registerWorker(aliasID, functionCode, std::bind(&ModbusBridge<SERVERCLASS>::bridgeWorker, this, std::placeholders::_1));
LOG_D("FC %02X added for server %02X\n", functionCode, aliasID);
} else {
LOG_E("Server %d not attached to bridge!\n", aliasID);
return false;
}
return true;
}
template<typename SERVERCLASS>
bool ModbusBridge<SERVERCLASS>::denyFunctionCode(uint8_t aliasID, uint8_t functionCode) {
// Is there already an entry for the aliasID?
if (servers.find(aliasID) != servers.end()) {
// Yes. Link server to own worker function
this->registerWorker(aliasID, functionCode, std::bind(&ModbusBridge<SERVERCLASS>::bridgeDenyWorker, this, std::placeholders::_1));
LOG_D("FC %02X blocked for server %02X\n", functionCode, aliasID);
} else {
LOG_E("Server %d not attached to bridge!\n", aliasID);
return false;
}
return true;
}
// bridgeWorker: default worker function to process bridge requests
template<typename SERVERCLASS>
ModbusMessage ModbusBridge<SERVERCLASS>::bridgeWorker(ModbusMessage msg) {
uint8_t aliasID = msg.getServerID();
uint8_t functionCode = msg.getFunctionCode();
ModbusMessage response;
// Find the (alias) serverID
if (servers.find(aliasID) != servers.end()) {
// Found it. We may use servers[aliasID] now without allocating a new map slot
// Set real target server ID
msg.setServerID(servers[aliasID]->serverID);
// Issue the request
LOG_D("Request (%02X/%02X) sent\n", servers[aliasID]->serverID, functionCode);
// TCP servers have a target host/port that needs to be set in the client
if (servers[aliasID]->serverType == TCP_SERVER) {
response = reinterpret_cast<ModbusClientTCP *>(servers[aliasID]->client)->syncRequestMT(msg, (uint32_t)millis(), servers[aliasID]->host, servers[aliasID]->port);
} else {
response = servers[aliasID]->client->syncRequestM(msg, (uint32_t)millis());
}
// Re-set the requested server ID
response.setServerID(aliasID);
} else {
// If we get here, something has gone wrong internally. We send back an error response anyway.
response.setError(aliasID, functionCode, INVALID_SERVER);
}
return response;
}
// bridgeDenyWorker: worker function to block function codes
template<typename SERVERCLASS>
ModbusMessage ModbusBridge<SERVERCLASS>::bridgeDenyWorker(ModbusMessage msg) {
ModbusMessage response;
response.setError(msg.getServerID(), msg.getFunctionCode(), ILLEGAL_FUNCTION);
return response;
}
#endif

View File

@@ -0,0 +1,18 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_BRIDGE_WIFI_H
#define _MODBUS_BRIDGE_WIFI_H
#include "options.h"
#include <WiFi.h>
#undef SERVER_END
#define SERVER_END server.end();
#include "ModbusServerTCPtemp.h"
#include "ModbusBridgeTemp.h"
using ModbusBridgeWiFi = ModbusBridge<ModbusServerTCP<WiFiServer, WiFiClient>>;
#endif

View File

@@ -0,0 +1,103 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClient.h"
#undef LOCAL_LOG_LEVEL
#include "Logging.h"
uint16_t ModbusClient::instanceCounter = 0;
// Default constructor: set the default timeout to 2000ms, zero out all other
ModbusClient::ModbusClient() :
messageCount(0),
errorCount(0),
#if HAS_FREERTOS
worker(NULL),
#elif IS_LINUX
worker(0),
#endif
onData(nullptr),
onError(nullptr),
onResponse(nullptr) { instanceCounter++; }
// onDataHandler: register callback for data responses
bool ModbusClient::onDataHandler(MBOnData handler) {
if (onData) {
LOG_W("onData handler was already claimed\n");
} else if (onResponse) {
LOG_E("onData handler is unavailable with an onResponse handler\n");
return false;
}
onData = handler;
return true;
}
// onErrorHandler: register callback for error responses
bool ModbusClient::onErrorHandler(MBOnError handler) {
if (onError) {
LOG_W("onError handler was already claimed\n");
} else if (onResponse) {
LOG_E("onError handler is unavailable with an onResponse handler\n");
return false;
}
onError = handler;
return true;
}
// onResponseHandler: register callback for error responses
bool ModbusClient::onResponseHandler(MBOnResponse handler) {
if (onError || onData) {
LOG_E("onResponse handler is unavailable with an onData or onError handler\n");
return false;
}
onResponse = handler;
return true;
}
// getMessageCount: return message counter value
uint32_t ModbusClient::getMessageCount() {
return messageCount;
}
// getErrorCount: return error counter value
uint32_t ModbusClient::getErrorCount() {
return errorCount;
}
// resetCounts: Set both message and error counts to zero
void ModbusClient::resetCounts() {
{
LOCK_GUARD(cntLock, countAccessM);
messageCount = 0;
errorCount = 0;
}
}
// waitSync: wait for response on syncRequest to arrive
ModbusMessage ModbusClient::waitSync(uint8_t serverID, uint8_t functionCode, uint32_t token) {
ModbusMessage response;
unsigned long lostPatience = millis();
// Default response is TIMEOUT
response.setError(serverID, functionCode, TIMEOUT);
// Loop 60 seconds, if unlucky
while (millis() - lostPatience < 60000) {
{
LOCK_GUARD(lg, syncRespM);
// Look for the token
auto sR = syncResponse.find(token);
// Is it there?
if (sR != syncResponse.end()) {
// Yes. get the response, delete it from the map and return
response = sR->second;
syncResponse.erase(sR);
break;
}
}
// Give the watchdog time to act
delay(10);
}
return response;
}

View File

@@ -0,0 +1,119 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_H
#define _MODBUS_CLIENT_H
#include <functional>
#include <map>
#include "options.h"
#include "ModbusMessage.h"
#if HAS_FREERTOS
extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
}
#elif IS_LINUX
#include <pthread.h>
#endif
#if USE_MUTEX
#include <mutex> // NOLINT
using std::mutex;
using std::lock_guard;
#endif
typedef std::function<void(ModbusMessage msg, uint32_t token)> MBOnData;
typedef std::function<void(Modbus::Error errorCode, uint32_t token)> MBOnError;
typedef std::function<void(ModbusMessage msg, uint32_t token)> MBOnResponse;
class ModbusClient {
public:
bool onDataHandler(MBOnData handler); // Accept onData handler
bool onErrorHandler(MBOnError handler); // Accept onError handler
bool onResponseHandler(MBOnResponse handler); // Accept onResponse handler
uint32_t getMessageCount(); // Informative: return number of messages created
uint32_t getErrorCount(); // Informative: return number of errors received
void resetCounts(); // Set both message and error counts to zero
inline Error addRequest(ModbusMessage m, uint32_t token) { return addRequestM(m, token); }
inline ModbusMessage syncRequest(ModbusMessage m, uint32_t token) { return syncRequestM(m, token); }
// Template function to generate syncRequest functions as long as there is a
// matching ModbusMessage::setMessage() call
template <typename... Args>
ModbusMessage syncRequest(uint32_t token, Args&&... args) {
Error rc = SUCCESS;
// Create request, if valid
ModbusMessage m;
rc = m.setMessage(std::forward<Args>(args) ...);
// Add it to the queue and wait for a response, if valid
if (rc == SUCCESS) {
return syncRequestM(m, token);
}
// Else return the error as a message
return buildErrorMsg(rc, std::forward<Args>(args) ...);
}
// Template function to create an error response message from a variadic pattern
template <typename... Args>
ModbusMessage buildErrorMsg(Error e, uint8_t serverID, uint8_t functionCode, Args&&... args) {
ModbusMessage m;
m.setError(serverID, functionCode, e);
return m;
}
// Template function to generate addRequest functions as long as there is a
// matching ModbusMessage::setMessage() call
template <typename... Args>
Error addRequest(uint32_t token, Args&&... args) {
Error rc = SUCCESS; // Return value
// Create request, if valid
ModbusMessage m;
rc = m.setMessage(std::forward<Args>(args) ...);
// Add it to the queue, if valid
if (rc == SUCCESS) {
return addRequestM(m, token);
}
// Else return the error
return rc;
}
protected:
ModbusClient(); // Default constructor
virtual void isInstance() = 0; // Make class abstract
ModbusMessage waitSync(uint8_t serverID, uint8_t functionCode, uint32_t token); // wait for syncRequest response to arrive
// Virtual addRequest variant needed internally. All others done by template!
virtual Error addRequestM(ModbusMessage msg, uint32_t token) = 0;
// Virtual syncRequest variant following the same pattern
virtual ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token) = 0;
// Prevent copy construction or assignment
ModbusClient(ModbusClient& other) = delete;
ModbusClient& operator=(ModbusClient& other) = delete;
uint32_t messageCount; // Number of requests generated. Used for transactionID in TCPhead
uint32_t errorCount; // Number of errors received
#if HAS_FREERTOS
TaskHandle_t worker; // Interface instance worker task
#elif IS_LINUX
pthread_t worker;
#endif
MBOnData onData; // Data response handler
MBOnError onError; // Error response handler
MBOnResponse onResponse; // Uniform response handler
static uint16_t instanceCounter; // Number of ModbusClients created
std::map<uint32_t, ModbusMessage> syncResponse; // Map to hold response messages on synchronous requests
#if USE_MUTEX
std::mutex syncRespM; // Mutex protecting syncResponse map against race conditions
std::mutex countAccessM; // Mutex protecting access to the message and error counts
#endif
// Let any ModbusBridge class use protected members
template<typename SERVERCLASS> friend class ModbusBridge;
};
#endif

View File

@@ -0,0 +1,349 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClientRTU.h"
#if HAS_FREERTOS
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
// Constructor takes an optional DE/RE pin and queue size
ModbusClientRTU::ModbusClientRTU(int8_t rtsPin, uint16_t queueLimit) :
ModbusClient(),
MR_serial(nullptr),
MR_lastMicros(micros()),
MR_interval(2000),
MR_rtsPin(rtsPin),
MR_qLimit(queueLimit),
MR_timeoutValue(DEFAULTTIMEOUT),
MR_useASCII(false),
MR_skipLeadingZeroByte(false) {
if (MR_rtsPin >= 0) {
pinMode(MR_rtsPin, OUTPUT);
MTRSrts = [this](bool level) {
digitalWrite(MR_rtsPin, level);
};
MTRSrts(LOW);
} else {
MTRSrts = RTUutils::RTSauto;
}
}
// Alternative constructor takes an RTS callback function
ModbusClientRTU::ModbusClientRTU(RTScallback rts, uint16_t queueLimit) :
ModbusClient(),
MR_serial(nullptr),
MR_lastMicros(micros()),
MR_interval(2000),
MTRSrts(rts),
MR_qLimit(queueLimit),
MR_timeoutValue(DEFAULTTIMEOUT),
MR_useASCII(false),
MR_skipLeadingZeroByte(false) {
MR_rtsPin = -1;
MTRSrts(LOW);
}
// Destructor: clean up queue, task etc.
ModbusClientRTU::~ModbusClientRTU() {
// Kill worker task and clean up request queue
end();
}
// begin: start worker task - general version
void ModbusClientRTU::begin(Stream& serial, uint32_t baudRate, int coreID) {
MR_serial = &serial;
doBegin(baudRate, coreID);
}
// begin: start worker task - HardwareSerial version
void ModbusClientRTU::begin(HardwareSerial& serial, int coreID) {
MR_serial = &serial;
uint32_t baudRate = serial.baudRate();
serial.setRxFIFOFull(1);
doBegin(baudRate, coreID);
}
void ModbusClientRTU::doBegin(uint32_t baudRate, int coreID) {
// Task already running? End it in case
end();
// Pull down RTS toggle, if necessary
MTRSrts(LOW);
// Set minimum interval time
MR_interval = RTUutils::calculateInterval(baudRate);
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "Modbus%02XRTU", instanceCounter);
// Start task to handle the queue
xTaskCreatePinnedToCore((TaskFunction_t)&handleConnection, taskName, CLIENT_TASK_STACK, this, 6, &worker, coreID >= 0 ? coreID : NULL);
LOG_D("Client task %d started. Interval=%d\n", (uint32_t)worker, MR_interval);
}
// end: stop worker task
void ModbusClientRTU::end() {
if (worker) {
// Clean up queue
{
// Safely lock access
LOCK_GUARD(lockGuard, qLock);
// Get all queue entries one by one
while (!requests.empty()) {
// Remove front entry
requests.pop();
}
}
// Kill task
vTaskDelete(worker);
LOG_D("Client task %d killed.\n", (uint32_t)worker);
worker = nullptr;
}
}
// setTimeOut: set/change the default interface timeout
void ModbusClientRTU::setTimeout(uint32_t TOV) {
MR_timeoutValue = TOV;
LOG_D("Timeout set to %d\n", TOV);
}
// Toggle protocol to ModbusASCII
void ModbusClientRTU::useModbusASCII(unsigned long timeout) {
MR_useASCII = true;
MR_timeoutValue = timeout; // Switch timeout to ASCII's value
LOG_D("Protocol mode: ASCII\n");
}
// Toggle protocol to ModbusRTU
void ModbusClientRTU::useModbusRTU() {
MR_useASCII = false;
LOG_D("Protocol mode: RTU\n");
}
// Inquire protocol mode
bool ModbusClientRTU::isModbusASCII() {
return MR_useASCII;
}
// Toggle skipping of leading 0x00 byte
void ModbusClientRTU::skipLeading0x00(bool onOff) {
MR_skipLeadingZeroByte = onOff;
LOG_D("Skip leading 0x00 mode = %s\n", onOff ? "ON" : "OFF");
}
// Return number of unprocessed requests in queue
uint32_t ModbusClientRTU::pendingRequests() {
return requests.size();
}
// Remove all pending request from queue
void ModbusClientRTU::clearQueue()
{
std::queue<RequestEntry> empty;
LOCK_GUARD(lockGuard, qLock);
std::swap(requests, empty);
}
// Base addRequest taking a preformatted data buffer and length as parameters
Error ModbusClientRTU::addRequestM(ModbusMessage msg, uint32_t token) {
Error rc = SUCCESS; // Return value
LOG_D("request for %02X/%02X\n", msg.getServerID(), msg.getFunctionCode());
// Add it to the queue, if valid
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// Base syncRequest follows the same pattern
ModbusMessage ModbusClientRTU::syncRequestM(ModbusMessage msg, uint32_t token) {
ModbusMessage response;
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg, true)) {
// No. Return error after deleting the allocated request.
response.setError(msg.getServerID(), msg.getFunctionCode(), REQUEST_QUEUE_FULL);
} else {
// Request is queued - wait for the result.
response = waitSync(msg.getServerID(), msg.getFunctionCode(), token);
}
} else {
response.setError(msg.getServerID(), msg.getFunctionCode(), EMPTY_MESSAGE);
}
return response;
}
// addBroadcastMessage: create a fire-and-forget message to all servers on the RTU bus
Error ModbusClientRTU::addBroadcastMessage(const uint8_t *data, uint8_t len) {
Error rc = SUCCESS; // Return value
LOG_D("Broadcast request of length %d\n", len);
// We do only accept requests with data, 0 byte, data and CRC must fit into 256 bytes.
if (len && len < 254) {
// Create a "broadcast token"
uint32_t token = (millis() & 0xFFFFFF) | 0xBC000000;
ModbusMessage msg;
// Server ID is 0x00 for broadcast
msg.add((uint8_t)0x00);
// Append data
msg.add(data, len);
// Queue add successful?
if (!addToQueue(token, msg)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
} else {
rc = BROADCAST_ERROR;
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// addToQueue: send freshly created request to queue
bool ModbusClientRTU::addToQueue(uint32_t token, ModbusMessage request, bool syncReq) {
bool rc = false;
// Did we get one?
if (request) {
RequestEntry re(token, request, syncReq);
if (requests.size()<MR_qLimit) {
// Yes. Safely lock queue and push request to queue
rc = true;
LOCK_GUARD(lockGuard, qLock);
requests.push(re);
}
{
LOCK_GUARD(cntLock, countAccessM);
messageCount++;
}
}
LOG_D("RC=%02X\n", rc);
return rc;
}
// handleConnection: worker task
// This was created in begin() to handle the queue entries
void ModbusClientRTU::handleConnection(ModbusClientRTU *instance) {
// initially clean the serial buffer
while (instance->MR_serial->available()) instance->MR_serial->read();
delay(100);
// Loop forever - or until task is killed
while (1) {
// Do we have a reuest in queue?
if (!instance->requests.empty()) {
// Yes. pull it.
RequestEntry request = instance->requests.front();
LOG_D("Pulled request from queue\n");
// Send it via Serial
RTUutils::send(*(instance->MR_serial), instance->MR_lastMicros, instance->MR_interval, instance->MTRSrts, request.msg, instance->MR_useASCII);
LOG_D("Request sent.\n");
// HEXDUMP_V("Data", request.msg.data(), request.msg.size());
// For a broadcast, we will not wait for a response
if (request.msg.getServerID() != 0 || ((request.token & 0xFF000000) != 0xBC000000)) {
// This is a regular request, Get the response - if any
ModbusMessage response = RTUutils::receive(
'C',
*(instance->MR_serial),
instance->MR_timeoutValue,
instance->MR_lastMicros,
instance->MR_interval,
instance->MR_useASCII,
instance->MR_skipLeadingZeroByte);
LOG_D("%s response (%d bytes) received.\n", response.size()>1 ? "Data" : "Error", response.size());
HEXDUMP_V("Data", response.data(), response.size());
// No error in receive()?
if (response.size() > 1) {
// No. Check message contents
// Does the serverID match the requested?
if (request.msg.getServerID() != response.getServerID()) {
// No. Return error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), SERVER_ID_MISMATCH);
// ServerID ok, but does the FC match as well?
} else if (request.msg.getFunctionCode() != (response.getFunctionCode() & 0x7F)) {
// No. Return error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), FC_MISMATCH);
}
} else {
// No, we got an error code from receive()
// Return it as error response
response.setError(request.msg.getServerID(), request.msg.getFunctionCode(), static_cast<Error>(response[0]));
}
LOG_D("Response generated.\n");
HEXDUMP_V("Response packet", response.data(), response.size());
// If we got an error, count it
if (response.getError() != SUCCESS) {
instance->errorCount++;
}
// Was it a synchronous request?
if (request.isSyncRequest) {
// Yes. Put it into the response map
{
LOCK_GUARD(sL, instance->syncRespM);
instance->syncResponse[request.token] = response;
}
// No, an async request. Do we have an onResponse handler?
} else if (instance->onResponse) {
// Yes. Call it
instance->onResponse(response, request.token);
} else {
// No, but we may have onData or onError handlers
// Did we get a normal response?
if (response.getError()==SUCCESS) {
// Yes. Do we have an onData handler registered?
if (instance->onData) {
// Yes. call it
instance->onData(response, request.token);
}
} else {
// No, something went wrong. All we have is an error
// Do we have an onError handler?
if (instance->onError) {
// Yes. Forward the error code to it
instance->onError(response.getError(), request.token);
}
}
}
}
// Clean-up time.
{
// Safely lock the queue
LOCK_GUARD(lockGuard, instance->qLock);
// Remove the front queue entry
instance->requests.pop();
}
} else {
delay(1);
}
}
}
#endif // HAS_FREERTOS

View File

@@ -0,0 +1,111 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_RTU_H
#define _MODBUS_CLIENT_RTU_H
#include "options.h"
#if HAS_FREERTOS
#include "ModbusClient.h"
#include "Stream.h"
#include "RTUutils.h"
#include <queue>
#include <vector>
using std::queue;
#define DEFAULTTIMEOUT 2000
class ModbusClientRTU : public ModbusClient {
public:
// Constructor takes an optional DE/RE pin and queue limit
explicit ModbusClientRTU(int8_t rtsPin = -1, uint16_t queueLimit = 100);
// Alternative Constructor takes an RTS line toggle callback
explicit ModbusClientRTU(RTScallback rts, uint16_t queueLimit = 100);
// Destructor: clean up queue, task etc.
~ModbusClientRTU();
// begin: start worker task
void begin(Stream& serial, uint32_t baudrate, int coreID = -1);
// Special variant for HardwareSerial
void begin(HardwareSerial& serial, int coreID = -1);
// end: stop the worker
void end();
// Set default timeout value for interface
void setTimeout(uint32_t TOV);
// Toggle protocol to ModbusASCII
void useModbusASCII(unsigned long timeout = 1000);
// Toggle protocol to ModbusRTU
void useModbusRTU();
// Inquire protocol mode
bool isModbusASCII();
// Toggle skipping of leading 0x00 byte
void skipLeading0x00(bool onOff = true);
// Return number of unprocessed requests in queue
uint32_t pendingRequests();
// Remove all pending request from queue
void clearQueue();
// addBroadcastMessage: create a fire-and-forget message to all servers on the RTU bus
Error addBroadcastMessage(const uint8_t *data, uint8_t len);
protected:
struct RequestEntry {
uint32_t token;
ModbusMessage msg;
bool isSyncRequest;
RequestEntry(uint32_t t, ModbusMessage m, bool syncReq = false) :
token(t),
msg(m),
isSyncRequest(syncReq) {}
};
// Base addRequest and syncRequest must be present
Error addRequestM(ModbusMessage msg, uint32_t token);
ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token);
// addToQueue: send freshly created request to queue
bool addToQueue(uint32_t token, ModbusMessage msg, bool syncReq = false);
// handleConnection: worker task method
static void handleConnection(ModbusClientRTU *instance);
// receive: get response via Serial
ModbusMessage receive(const ModbusMessage request);
// start background task
void doBegin(uint32_t baudRate, int coreID);
void isInstance() { return; } // make class instantiable
queue<RequestEntry> requests; // Queue to hold requests to be processed
#if USE_MUTEX
mutex qLock; // Mutex to protect queue
#endif
Stream *MR_serial; // Ptr to the serial interface used
unsigned long MR_lastMicros; // Microseconds since last bus activity
uint32_t MR_interval; // Modbus RTU bus quiet time
int8_t MR_rtsPin; // GPIO pin to toggle RS485 DE/RE line. -1 if none.
RTScallback MTRSrts; // RTS line callback function
uint16_t MR_qLimit; // Maximum number of requests to hold in the queue
uint32_t MR_timeoutValue; // Interface default timeout
bool MR_useASCII; // true=ModbusASCII, false=ModbusRTU
bool MR_skipLeadingZeroByte; // true=skip the first byte if it is 0x00, false=accept all bytes
};
#endif // HAS_FREERTOS
#endif // INCLUDE GUARD

View File

@@ -0,0 +1,428 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClientTCP.h"
#if HAS_FREERTOS || IS_LINUX
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
// Constructor takes reference to Client (EthernetClient or WiFiClient)
ModbusClientTCP::ModbusClientTCP(Client& client, uint16_t queueLimit) :
ModbusClient(),
MT_client(client),
MT_lastTarget(IPAddress(0, 0, 0, 0), 0, DEFAULTTIMEOUT, TARGETHOSTINTERVAL),
MT_target(IPAddress(0, 0, 0, 0), 0, DEFAULTTIMEOUT, TARGETHOSTINTERVAL),
MT_defaultTimeout(DEFAULTTIMEOUT),
MT_defaultInterval(TARGETHOSTINTERVAL),
MT_qLimit(queueLimit)
{ }
// Alternative Constructor takes reference to Client (EthernetClient or WiFiClient) plus initial target host
ModbusClientTCP::ModbusClientTCP(Client& client, IPAddress host, uint16_t port, uint16_t queueLimit) :
ModbusClient(),
MT_client(client),
MT_lastTarget(IPAddress(0, 0, 0, 0), 0, DEFAULTTIMEOUT, TARGETHOSTINTERVAL),
MT_target(host, port, DEFAULTTIMEOUT, TARGETHOSTINTERVAL),
MT_defaultTimeout(DEFAULTTIMEOUT),
MT_defaultInterval(TARGETHOSTINTERVAL),
MT_qLimit(queueLimit)
{ }
// Destructor: clean up queue, task etc.
ModbusClientTCP::~ModbusClientTCP() {
end();
}
// end: stop worker task
void ModbusClientTCP::end() {
// Clean up queue
{
// Safely lock access
LOCK_GUARD(lockGuard, qLock);
// Get all queue entries one by one
while (!requests.empty()) {
requests.pop();
}
}
LOG_D("TCP client worker killed.\n");
// Kill task
if (worker) {
#if IS_LINUX
pthread_cancel(worker);
worker = NULL;
#else
vTaskDelete(worker);
worker = nullptr;
#endif
}
}
// begin: start worker task
#if IS_LINUX
void *ModbusClientTCP::pHandle(void *p) {
handleConnection((ModbusClientTCP *)p);
return nullptr;
}
#endif
void ModbusClientTCP::begin(int coreID) {
if (!worker) {
#if IS_LINUX
int rc = pthread_create(&worker, NULL, &pHandle, this);
if (rc) {
LOG_E("Error creating TCP client thread: %d\n", rc);
} else {
LOG_D("TCP client worker started.\n");
}
#else
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "Modbus%02XTCP", instanceCounter);
// Start task to handle the queue
xTaskCreatePinnedToCore((TaskFunction_t)&handleConnection, taskName, CLIENT_TASK_STACK, this, 5, &worker, coreID >= 0 ? coreID : NULL);
LOG_D("TCP client worker %s started\n", taskName);
#endif
} else {
LOG_E("Worker thread has been already started!");
}
}
// Set default timeout value (and interval)
void ModbusClientTCP::setTimeout(uint32_t timeout, uint32_t interval) {
MT_defaultTimeout = timeout;
MT_defaultInterval = interval;
}
// Switch target host (if necessary)
// Return true, if host/port is different from last host/port used
bool ModbusClientTCP::setTarget(IPAddress host, uint16_t port, uint32_t timeout, uint32_t interval) {
MT_target.host = host;
MT_target.port = port;
MT_target.timeout = timeout ? timeout : MT_defaultTimeout;
MT_target.interval = interval ? interval : MT_defaultInterval;
LOG_D("Target set: %d.%d.%d.%d:%d\n", host[0], host[1], host[2], host[3], port);
if (MT_target.host == MT_lastTarget.host && MT_target.port == MT_lastTarget.port) return false;
return true;
}
// Return number of unprocessed requests in queue
uint32_t ModbusClientTCP::pendingRequests() {
return requests.size();
}
// Remove all pending request from queue
void ModbusClientTCP::clearQueue() {
std::queue<RequestEntry *> empty;
LOCK_GUARD(lockGuard, qLock);
std::swap(requests, empty);
}
// Base addRequest for preformatted ModbusMessage and last set target
Error ModbusClientTCP::addRequestM(ModbusMessage msg, uint32_t token) {
Error rc = SUCCESS; // Return value
// Add it to the queue, if valid
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg, MT_target)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
}
LOG_D("Add TCP request result: %02X\n", rc);
return rc;
}
// TCP addRequest for preformatted ModbusMessage and adhoc target
Error ModbusClientTCP::addRequestMT(ModbusMessage msg, uint32_t token, IPAddress targetHost, uint16_t targetPort) {
Error rc = SUCCESS; // Return value
// Add it to the queue, if valid
if (msg) {
// Set up adhoc target
TargetHost adhocTarget(targetHost, targetPort, MT_defaultTimeout, MT_defaultInterval);
// Queue add successful?
if (!addToQueue(token, msg, adhocTarget, true)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
}
LOG_D("Add TCP request result: %02X\n", rc);
return rc;
}
// Base syncRequest follows the same pattern
ModbusMessage ModbusClientTCP::syncRequestM(ModbusMessage msg, uint32_t token) {
ModbusMessage response;
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg, MT_target, true)) {
// No. Return error after deleting the allocated request.
response.setError(msg.getServerID(), msg.getFunctionCode(), REQUEST_QUEUE_FULL);
} else {
// Request is queued - wait for the result.
response = waitSync(msg.getServerID(), msg.getFunctionCode(), token);
}
} else {
response.setError(msg.getServerID(), msg.getFunctionCode(), EMPTY_MESSAGE);
}
return response;
}
// TCP syncRequest with adhoc target parameters
ModbusMessage ModbusClientTCP::syncRequestMT(ModbusMessage msg, uint32_t token, IPAddress targetHost, uint16_t targetPort) {
ModbusMessage response;
if (msg) {
// Set up adhoc target
TargetHost adhocTarget(targetHost, targetPort, MT_defaultTimeout, MT_defaultInterval);
// Queue add successful?
if (!addToQueue(token, msg, adhocTarget, true)) {
// No. Return error after deleting the allocated request.
response.setError(msg.getServerID(), msg.getFunctionCode(), REQUEST_QUEUE_FULL);
} else {
// Request is queued - wait for the result.
response = waitSync(msg.getServerID(), msg.getFunctionCode(), token);
}
} else {
response.setError(msg.getServerID(), msg.getFunctionCode(), EMPTY_MESSAGE);
}
return response;
}
// addToQueue: send freshly created request to queue
bool ModbusClientTCP::addToQueue(uint32_t token, ModbusMessage request, TargetHost target, bool syncReq) {
bool rc = false;
// Did we get one?
LOG_D("Queue size: %d\n", (uint32_t)requests.size());
HEXDUMP_D("Enqueue", request.data(), request.size());
if (request) {
if (requests.size()<MT_qLimit) {
RequestEntry *re = new RequestEntry(token, request, target, syncReq);
// inject proper transactionID
re->head.transactionID = messageCount++;
re->head.len = request.size();
// Safely lock queue and push request to queue
rc = true;
LOCK_GUARD(lockGuard, qLock);
requests.push(re);
}
}
return rc;
}
// handleConnection: worker task
// This was created in begin() to handle the queue entries
void ModbusClientTCP::handleConnection(ModbusClientTCP *instance) {
bool doNotPop;
unsigned long lastRequest = millis();
// Loop forever - or until task is killed
while (1) {
// Do we have a request in queue?
if (!instance->requests.empty()) {
// Yes. pull it.
RequestEntry *request = instance->requests.front();
doNotPop = false;
LOG_D("Got request from queue\n");
// Do we have a connection open?
if (instance->MT_client.connected()) {
// Empty the RX buffer in case there is a stray response left
while (instance->MT_client.read() != -1) {}
// check if lastHost/lastPort!=host/port off the queued request
if (instance->MT_lastTarget != request->target) {
// It is different. Disconnect it.
instance->MT_client.stop();
LOG_D("Target different, disconnect\n");
delay(1); // Give scheduler room to breathe
} else {
// it is the same host/port.
// Give it some slack to get ready again
while (millis() - lastRequest < request->target.interval) { delay(1); }
}
}
// if client is disconnected (we will have to switch hosts)
if (!instance->MT_client.connected()) {
// Serial.println("Client reconnecting");
// It is disconnected. connect to host/port from queue
instance->MT_client.connect(request->target.host, request->target.port);
LOG_D("Target connect (%d.%d.%d.%d:%d).\n", request->target.host[0], request->target.host[1], request->target.host[2], request->target.host[3], request->target.port);
delay(1); // Give scheduler room to breathe
}
ModbusMessage response;
// Are we connected (again)?
if (instance->MT_client.connected()) {
LOG_D("Is connected. Send request.\n");
// Yes. Send the request via IP
instance->send(request);
// Get the response - if any
response = instance->receive(request);
// Did we get a normal response?
if (response.getError()==SUCCESS) {
LOG_D("Data response.\n");
// Yes. Is it a synchronous request?
if (request->isSyncRequest) {
// Yes. Put the response into the response map
{
LOCK_GUARD(sL, instance->syncRespM);
instance->syncResponse[request->token] = response;
}
// No, async request. Do we have an onResponse handler?
} else if (instance->onResponse) {
// Yes. Call it.
instance->onResponse(response, request->token);
// No, but do we have an onData handler registered?
} else if (instance->onData) {
// Yes. call it
instance->onData(response, request->token);
} else {
LOG_D("No handler for response!\n");
}
} else {
// No, something went wrong. All we have is an error
LOG_D("Error response.\n");
// Count it
{
LOCK_GUARD(responseCnt, instance->countAccessM);
instance->errorCount++;
}
// Is it a synchronous request?
if (request->isSyncRequest) {
// Yes. Put the response into the response map
{
LOCK_GUARD(sL, instance->syncRespM);
instance->syncResponse[request->token] = response;
}
// No, but do we have an onResponse handler?
} else if (instance->onResponse) {
// Yes, call it.
instance->onResponse(response, request->token);
// No, but do we have an onError handler?
} else if (instance->onError) {
// Yes. Forward the error code to it
instance->onError(response.getError(), request->token);
} else {
LOG_D("No onError handler\n");
}
}
// set lastHost/lastPort tp host/port
instance->MT_lastTarget = request->target;
} else {
// Oops. Connection failed
response.setError(request->msg.getServerID(), request->msg.getFunctionCode(), IP_CONNECTION_FAILED);
// Is it a synchronous request?
if (request->isSyncRequest) {
// Yes. Put the response into the response map
{
LOCK_GUARD(sL, instance->syncRespM);
instance->syncResponse[request->token] = response;
}
// No, but do we have an onResponse handler?
} else if (instance->onResponse) {
// Yes, call it.
instance->onResponse(response, request->token);
// Finally, do we have an onError handler?
} else if (instance->onError) {
// Yes. Forward the error code to it
instance->onError(IP_CONNECTION_FAILED, request->token);
}
}
// Clean-up time.
if (!doNotPop)
{
// Safely lock the queue
LOCK_GUARD(lockGuard, instance->qLock);
// Remove the front queue entry
instance->requests.pop();
// Delete request
delete request;
LOG_D("Request popped from queue.\n");
}
lastRequest = millis();
} else {
delay(1); // Give scheduler room to breathe
}
}
}
// send: send request via Client connection
void ModbusClientTCP::send(RequestEntry *request) {
// We have a established connection here, so we can write right away.
// Move tcpHead and request into one continuous buffer, since the very first request tends to
// take too long to be sent to be recognized.
ModbusMessage m;
m.add((const uint8_t *)request->head, 6);
m.append(request->msg);
MT_client.write(m.data(), m.size());
// Done. Are we?
MT_client.flush();
HEXDUMP_V("Request packet", m.data(), m.size());
}
// receive: get response via Client connection
ModbusMessage ModbusClientTCP::receive(RequestEntry *request) {
unsigned long lastMillis = millis(); // Timer to check for timeout
bool hadData = false; // flag data received
const uint16_t dataLen(300); // Modbus Packet supposedly will fit (260<300)
uint8_t data[dataLen]; // Local buffer to collect received data
uint16_t dataPtr = 0; // Pointer into data
ModbusMessage response; // Response structure to be returned
// wait for packet data, overflow or timeout
while (millis() - lastMillis < request->target.timeout && dataPtr < dataLen && !hadData) {
// Is there data waiting?
if (MT_client.available()) {
// Yes. catch as much as is there and fits into buffer
while (MT_client.available() && dataPtr < dataLen) {
data[dataPtr++] = MT_client.read();
}
// Register data received
hadData = true;
// Rewind EOT and timeout timers
lastMillis = millis();
}
delay(1); // Give scheduler room to breathe
}
// Did we get some data?
if (hadData) {
LOG_D("Received response.\n");
HEXDUMP_V("Response packet", data, dataPtr);
// Yes. check it for validity
// First transactionID and protocolID shall be identical, length has to match the remainder.
ModbusTCPhead head(request->head.transactionID, request->head.protocolID, dataPtr - 6);
// Matching head?
if (memcmp((const uint8_t *)head, data, 6)) {
// No. return Error response
response.setError(request->msg.getServerID(), request->msg.getFunctionCode(), TCP_HEAD_MISMATCH);
// If the server id does not match that of the request, report error
} else if (data[6] != request->msg.getServerID()) {
response.setError(request->msg.getServerID(), request->msg.getFunctionCode(), SERVER_ID_MISMATCH);
// If the function code does not match that of the request, report error
} else if ((data[7] & 0x7F) != request->msg.getFunctionCode()) {
response.setError(request->msg.getServerID(), request->msg.getFunctionCode(), FC_MISMATCH);
} else {
// Looks good.
response.add(data + 6, dataPtr - 6);
}
} else {
// No, timeout must have struck
response.setError(request->msg.getServerID(), request->msg.getFunctionCode(), TIMEOUT);
}
return response;
}
#endif

View File

@@ -0,0 +1,195 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_TCP_H
#define _MODBUS_CLIENT_TCP_H
#include "options.h"
#if HAS_FREERTOS || IS_LINUX
#if HAS_FREERTOS
#include <Arduino.h>
#endif
#include "ModbusClient.h"
#include "Client.h"
#include <queue>
#include <vector>
using std::queue;
#define TARGETHOSTINTERVAL 10
#define DEFAULTTIMEOUT 2000
class ModbusClientTCP : public ModbusClient {
public:
// Constructor takes reference to Client (EthernetClient or WiFiClient)
explicit ModbusClientTCP(Client& client, uint16_t queueLimit = 100);
// Alternative Constructor takes reference to Client (EthernetClient or WiFiClient) plus initial target host
ModbusClientTCP(Client& client, IPAddress host, uint16_t port, uint16_t queueLimit = 100);
// Destructor: clean up queue, task etc.
~ModbusClientTCP();
// begin: start worker task
void begin(int coreID = -1);
// end: stop worker task
void end();
// Set default timeout value (and interval)
void setTimeout(uint32_t timeout = DEFAULTTIMEOUT, uint32_t interval = TARGETHOSTINTERVAL);
// Switch target host (if necessary)
bool setTarget(IPAddress host, uint16_t port, uint32_t timeout = 0, uint32_t interval = 0);
// Return number of unprocessed requests in queue
uint32_t pendingRequests();
// Remove all pending request from queue
void clearQueue();
protected:
// class describing a target server
struct TargetHost {
IPAddress host; // IP address
uint16_t port; // Port number
uint32_t timeout; // Time in ms waiting for a response
uint32_t interval; // Time in ms to wait between requests
inline TargetHost& operator=(TargetHost& t) {
host = t.host;
port = t.port;
timeout = t.timeout;
interval = t.interval;
return *this;
}
inline TargetHost(TargetHost& t) :
host(t.host),
port(t.port),
timeout(t.timeout),
interval(t.interval) {}
inline TargetHost() :
host(IPAddress(0, 0, 0, 0)),
port(0),
timeout(0),
interval(0)
{ }
inline TargetHost(IPAddress host, uint16_t port, uint32_t timeout, uint32_t interval) :
host(host),
port(port),
timeout(timeout),
interval(interval)
{ }
inline bool operator==(TargetHost& t) {
if (host != t.host) return false;
if (port != t.port) return false;
return true;
}
inline bool operator!=(TargetHost& t) {
if (host != t.host) return true;
if (port != t.port) return true;
return false;
}
};
// class describing the TCP header of Modbus packets
class ModbusTCPhead {
public:
ModbusTCPhead() :
transactionID(0),
protocolID(0),
len(0) {}
ModbusTCPhead(uint16_t tid, uint16_t pid, uint16_t _len) :
transactionID(tid),
protocolID(pid),
len(_len) {}
uint16_t transactionID; // Caller-defined identification
uint16_t protocolID; // const 0x0000
uint16_t len; // Length of remainder of TCP packet
inline explicit operator const uint8_t *() {
uint8_t *cp = headRoom;
*cp++ = (transactionID >> 8) & 0xFF;
*cp++ = transactionID & 0xFF;
*cp++ = (protocolID >> 8) & 0xFF;
*cp++ = protocolID & 0xFF;
*cp++ = (len >> 8) & 0xFF;
*cp++ = len & 0xFF;
return headRoom;
}
inline ModbusTCPhead& operator= (ModbusTCPhead& t) {
transactionID = t.transactionID;
protocolID = t.protocolID;
len = t.len;
return *this;
}
protected:
uint8_t headRoom[6]; // Buffer to hold MSB-first TCP header
};
struct RequestEntry {
uint32_t token;
ModbusMessage msg;
TargetHost target;
ModbusTCPhead head;
bool isSyncRequest;
RequestEntry(uint32_t t, ModbusMessage m, TargetHost tg, bool syncReq = false) :
token(t),
msg(m),
target(tg),
head(ModbusTCPhead()),
isSyncRequest(syncReq) {}
};
// Base addRequest and syncRequest must be present
Error addRequestM(ModbusMessage msg, uint32_t token);
ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token);
// TCP-specific addition "...MT()" including adhoc target - used by bridge
Error addRequestMT(ModbusMessage msg, uint32_t token, IPAddress targetHost, uint16_t targetPort);
ModbusMessage syncRequestMT(ModbusMessage msg, uint32_t token, IPAddress targetHost, uint16_t targetPort);
// addToQueue: send freshly created request to queue
bool addToQueue(uint32_t token, ModbusMessage request, TargetHost target, bool syncReq = false);
// handleConnection: worker task method
static void handleConnection(ModbusClientTCP *instance);
#if IS_LINUX
static void *pHandle(void *p);
#endif
// send: send request via Client connection
void send(RequestEntry *request);
// receive: get response via Client connection
ModbusMessage receive(RequestEntry *request);
void isInstance() { return; } // make class instantiable
queue<RequestEntry *> requests; // Queue to hold requests to be processed
#if USE_MUTEX
mutex qLock; // Mutex to protect queue
#endif
Client& MT_client; // Client reference for Internet connections (EthernetClient or WifiClient)
TargetHost MT_lastTarget; // last used server
TargetHost MT_target; // Description of target server
uint32_t MT_defaultTimeout; // Standard timeout value taken if no dedicated was set
uint32_t MT_defaultInterval; // Standard interval value taken if no dedicated was set
uint16_t MT_qLimit; // Maximum number of requests to accept in queue
// Let any ModbusBridge class use protected members
template<typename SERVERCLASS> friend class ModbusBridge;
};
#endif // HAS_FREERTOS
#endif // INCLUDE GUARD

View File

@@ -0,0 +1,401 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusClientTCPasync.h"
#define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
// #undef LOCAL_LOG_LEVEL
#include "Logging.h"
ModbusClientTCPasync::ModbusClientTCPasync(IPAddress address, uint16_t port, uint16_t queueLimit) :
ModbusClient(),
txQueue(),
rxQueue(),
MTA_client(),
MTA_timeout(DEFAULTTIMEOUT),
MTA_idleTimeout(DEFAULTIDLETIME),
MTA_qLimit(queueLimit),
MTA_maxInflightRequests(queueLimit),
MTA_lastActivity(0),
MTA_state(DISCONNECTED),
MTA_host(address),
MTA_port(port)
{
// attach all handlers on async tcp events
MTA_client.onConnect([](void* i, AsyncClient* c) { (static_cast<ModbusClientTCPasync*>(i))->onConnected(); }, this);
MTA_client.onDisconnect([](void* i, AsyncClient* c) { (static_cast<ModbusClientTCPasync*>(i))->onDisconnected(); }, this);
MTA_client.onError([](void* i, AsyncClient* c, int8_t error) { (static_cast<ModbusClientTCPasync*>(i))->onACError(c, error); }, this);
// MTA_client.onTimeout([](void* i, AsyncClient* c, uint32_t time) { (static_cast<ModbusClientTCPasync*>(i))->onTimeout(time); }, this);
// MTA_client.onAck([](void* i, AsyncClient* c, size_t len, uint32_t time) { (static_cast<ModbusClientTCPasync*>(i))->onAck(len, time); }, this);
MTA_client.onData([](void* i, AsyncClient* c, void* data, size_t len) { (static_cast<ModbusClientTCPasync*>(i))->onPacket(static_cast<uint8_t*>(data), len); }, this);
MTA_client.onPoll([](void* i, AsyncClient* c) { (static_cast<ModbusClientTCPasync*>(i))->onPoll(); }, this);
// disable nagle algorithm ref Modbus spec
MTA_client.setNoDelay(true);
}
// Destructor: clean up queue, task etc.
ModbusClientTCPasync::~ModbusClientTCPasync() {
// Clean up queue
{
// Safely lock access
LOCK_GUARD(lock1, qLock);
LOCK_GUARD(lock2, sLock);
// Delete all elements from queues
while (!txQueue.empty()) {
delete txQueue.front();
txQueue.pop_front();
}
for (auto it = rxQueue.cbegin(); it != rxQueue.cend();/* no increment */) {
delete it->second;
it = rxQueue.erase(it);
}
}
// force close client
MTA_client.close(true);
}
// optionally manually connect to modbus server. Otherwise connection will be made upon first request
void ModbusClientTCPasync::connect() {
LOG_D("connecting\n");
LOCK_GUARD(lock1, sLock);
// only connect if disconnected
if (MTA_state == DISCONNECTED) {
MTA_state = CONNECTING;
MTA_client.connect(MTA_host, MTA_port);
}
}
// connect to another modbus server.
void ModbusClientTCPasync::connect(IPAddress host, uint16_t port) {
// First disconnect, if connected
disconnect(true);
// Set new host and port
MTA_host = host;
MTA_port = port;
connect();
}
// manually disconnect from modbus server. Connection will also auto close after idle time
void ModbusClientTCPasync::disconnect(bool force) {
LOG_D("disconnecting\n");
MTA_client.close(force);
}
// Set timeout value
void ModbusClientTCPasync::setTimeout(uint32_t timeout) {
MTA_timeout = timeout;
}
// Set idle timeout value (time before connection auto closes after being idle)
void ModbusClientTCPasync::setIdleTimeout(uint32_t timeout) {
MTA_idleTimeout = timeout;
}
void ModbusClientTCPasync::setMaxInflightRequests(uint32_t maxInflightRequests) {
MTA_maxInflightRequests = maxInflightRequests;
}
// Remove all pending request from queue
void ModbusClientTCPasync::clearQueue()
{
LOCK_GUARD(lock1, qLock);
LOCK_GUARD(lock2, sLock);
// Delete all elements from queues
while (!txQueue.empty()) {
delete txQueue.front();
txQueue.pop_front();
}
}
// Base addRequest for preformatted ModbusMessage and last set target
Error ModbusClientTCPasync::addRequestM(ModbusMessage msg, uint32_t token) {
Error rc = SUCCESS; // Return value
// Add it to the queue, if valid
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg)) {
// No. Return error after deleting the allocated request.
rc = REQUEST_QUEUE_FULL;
}
}
LOG_D("Add TCP request result: %02X\n", rc);
return rc;
}
// Base syncRequest follows the same pattern
ModbusMessage ModbusClientTCPasync::syncRequestM(ModbusMessage msg, uint32_t token) {
ModbusMessage response;
if (msg) {
// Queue add successful?
if (!addToQueue(token, msg, true)) {
// No. Return error after deleting the allocated request.
response.setError(msg.getServerID(), msg.getFunctionCode(), REQUEST_QUEUE_FULL);
} else {
// Request is queued - wait for the result.
response = waitSync(msg.getServerID(), msg.getFunctionCode(), token);
}
} else {
response.setError(msg.getServerID(), msg.getFunctionCode(), EMPTY_MESSAGE);
}
return response;
}
// addToQueue: send freshly created request to queue
bool ModbusClientTCPasync::addToQueue(int32_t token, ModbusMessage request, bool syncReq) {
// Did we get one?
if (request) {
LOCK_GUARD(lock1, qLock);
if (txQueue.size() + rxQueue.size() < MTA_qLimit) {
HEXDUMP_V("Enqueue", request.data(), request.size());
RequestEntry *re = new RequestEntry(token, request, syncReq);
if (!re) return false; //TODO: proper error returning in case allocation fails
// inject proper transactionID
re->head.transactionID = messageCount++;
re->head.len = request.size();
// if we're already connected, try to send and push to rxQueue
// or else push to txQueue and (re)connect
if (MTA_state == CONNECTED && send(re)) {
re->sentTime = millis();
rxQueue[re->head.transactionID] = re;
} else {
txQueue.push_back(re);
if (MTA_state == DISCONNECTED) {
connect();
}
}
return true;
}
LOG_E("queue is full\n");
}
return false;
}
void ModbusClientTCPasync::onConnected() {
LOG_D("connected\n");
LOCK_GUARD(lock1, sLock);
MTA_state = CONNECTED;
MTA_lastActivity = millis();
// from now on onPoll will be called every 500 msec
}
void ModbusClientTCPasync::onDisconnected() {
LOG_D("disconnected\n");
LOCK_GUARD(lock1, sLock);
MTA_state = DISCONNECTED;
// empty queue on disconnect, calling errorcode on every waiting request
LOCK_GUARD(lock2, qLock);
while (!txQueue.empty()) {
RequestEntry* r = txQueue.front();
if (onError) {
onError(IP_CONNECTION_FAILED, r->token);
}
delete r;
txQueue.pop_front();
}
while (!rxQueue.empty()) {
RequestEntry *r = rxQueue.begin()->second;
if (onError) {
onError(IP_CONNECTION_FAILED, r->token);
}
delete r;
rxQueue.erase(rxQueue.begin());
}
}
void ModbusClientTCPasync::onACError(AsyncClient* c, int8_t error) {
// onDisconnect will alse be called, so nothing to do here
LOG_W("TCP error: %s\n", c->errorToString(error));
}
/*
void onTimeout(uint32_t time) {
// timeOut is handled by onPoll or onDisconnect
}
void onAck(size_t len, uint32_t time) {
// assuming we don't need this
}
*/
void ModbusClientTCPasync::onPacket(uint8_t* data, size_t length) {
LOG_D("packet received (len:%d)\n", length);
// reset idle timeout
MTA_lastActivity = millis();
if (length) {
LOG_D("parsing (len:%d)\n", length + 1);
}
while (length > 0) {
RequestEntry* request = nullptr;
ModbusMessage* response = nullptr;
uint16_t transactionID = 0;
uint16_t protocolID = 0;
uint16_t messageLength = 0;
bool isOkay = false;
// 1. Check for valid modbus message
// MBAP header is 6 bytes, we can't do anything with less
// total message should fit MBAP plus remaining bytes (in data[4], data[5])
if (length > 6) {
transactionID = (data[0] << 8) | data[1];
protocolID = (data[2] << 8) | data[3];
messageLength = (data[4] << 8) | data[5];
if (protocolID == 0 &&
length >= (uint32_t)messageLength + 6 &&
messageLength < 256) {
response = new ModbusMessage(messageLength);
response->add(&data[6], messageLength);
LOG_D("packet validated (len:%d)\n", messageLength);
// on next iteration: adjust remaining length and pointer to data
length -= 6 + messageLength;
data += 6 + messageLength;
isOkay = true;
}
}
if (!isOkay) {
// invalid packet, abort function
LOG_W("packet invalid\n");
return;
} else {
// 2. we got a valid response, match with a request
LOCK_GUARD(lock1, qLock);
auto i = rxQueue.find(transactionID);
if (i != rxQueue.end()) {
// found it, handle it and stop iterating
request = i->second;
i = rxQueue.erase(i);
LOG_D("matched request\n");
} else {
// TCP packet did not yield valid modbus response, abort function
LOG_W("no matching request found\n");
return;
}
}
// 3. we have a valid request and a valid response, call appropriate callback
if (request) {
// compare request with response
Error error = SUCCESS;
if (request->msg.getFunctionCode() != (response->getFunctionCode() & 0x7F)) {
error = FC_MISMATCH;
} else if (request->msg.getServerID() != response->getServerID()) {
error = SERVER_ID_MISMATCH;
} else {
error = response->getError();
}
if (error != SUCCESS) {
LOCK_GUARD(errorCntLock, countAccessM);
errorCount++;
}
if (request->isSyncRequest) {
{
LOCK_GUARD(sL ,syncRespM);
syncResponse[request->token] = *response;
}
} else if (onResponse) {
onResponse(*response, request->token);
} else {
if (error == SUCCESS) {
if (onData) {
onData(*response, request->token);
}
} else {
if (onError) {
onError(response->getError(), request->token);
}
}
}
delete request;
}
delete response;
} // end processing of incoming data
// check if we have to send the next request
LOCK_GUARD(lock1, qLock);
handleSendingQueue();
}
void ModbusClientTCPasync::onPoll() {
{
LOCK_GUARD(lock1, qLock);
// try to send whatever is waiting
handleSendingQueue();
// next check if timeout has struck for oldest request
if (!rxQueue.empty()) {
RequestEntry* request = rxQueue.begin()->second;
if (millis() - request->sentTime > MTA_timeout) {
LOG_D("request timeouts (now:%lu-sent:%u)\n", millis(), request->sentTime);
// oldest element timeouts, call onError and clean up
if (onError) {
// Handle timeout error
onError(TIMEOUT, request->token);
}
delete request;
rxQueue.erase(rxQueue.begin());
}
}
} // end lockguard scope
// if nothing happened during idle timeout, gracefully close connection
if (millis() - MTA_lastActivity > MTA_idleTimeout) {
disconnect();
}
}
void ModbusClientTCPasync::handleSendingQueue() {
// ATTENTION: This method does not have a lock guard.
// Calling sites must assure shared resources are protected
// by mutex.
// try to send everything we have waiting
std::list<RequestEntry*>::iterator it = txQueue.begin();
while (it != txQueue.end()) {
// get the actual element
if (send(*it)) {
// after sending, update timeout value, add to other queue and remove from this queue
(*it)->sentTime = millis();
rxQueue[(*it)->head.transactionID] = (*it); // push request to other queue
it = txQueue.erase(it); // remove from toSend queue and point i to next request
} else {
// sending didn't succeed, try next request
++it;
}
}
}
bool ModbusClientTCPasync::send(RequestEntry* re) {
// ATTENTION: This method does not have a lock guard.
// Calling sites must assure shared resources are protected
// by mutex.
if (rxQueue.size() >= MTA_maxInflightRequests) {
return false;
}
// check if TCP client is able to send
if (MTA_client.space() > ((uint32_t)re->msg.size() + 6)) {
// Write TCP header first
MTA_client.add(reinterpret_cast<const char *>((const uint8_t *)(re->head)), 6, ASYNC_WRITE_FLAG_COPY);
// Request comes next
MTA_client.add(reinterpret_cast<const char*>(re->msg.data()), re->msg.size(), ASYNC_WRITE_FLAG_COPY);
// done
MTA_client.send();
LOG_D("request sent (msgid:%d)\n", re->head.transactionID);
return true;
}
return false;
}

View File

@@ -0,0 +1,158 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_CLIENT_TCP_ASYNC_H
#define _MODBUS_CLIENT_TCP_ASYNC_H
#include <Arduino.h>
#if defined ESP32
#include <AsyncTCP.h>
#elif defined ESP8266
#include <ESPAsyncTCP.h>
#endif
#include "options.h"
#include "ModbusMessage.h"
#include "ModbusClient.h"
#include <list>
#include <map>
#include <vector>
#if USE_MUTEX
#include <mutex> // NOLINT
#endif
using std::vector;
#define DEFAULTTIMEOUT 10000
#define DEFAULTIDLETIME 60000
class ModbusClientTCPasync : public ModbusClient {
public:
// Constructor takes address and port
explicit ModbusClientTCPasync(IPAddress address, uint16_t port = 502, uint16_t queueLimit = 100);
// Destructor: clean up queue, task etc.
~ModbusClientTCPasync();
// optionally manually connect to modbus server. Otherwise connection will be made upon first request
void connect();
// Connect to another Modbus server
void connect(IPAddress host, uint16_t port = 502);
// manually disconnect from modbus server. Connection will also auto close after idle time
void disconnect(bool force = false);
// Set timeout value
void setTimeout(uint32_t timeout);
// Set idle timeout value (time before connection auto closes after being idle)
void setIdleTimeout(uint32_t timeout);
// Set maximum amount of messages awaiting a response. Subsequent messages will be queued.
void setMaxInflightRequests(uint32_t maxInflightRequests);
// Remove all pending request from queue
void clearQueue();
protected:
// class describing the TCP header of Modbus packets
class ModbusTCPhead {
public:
ModbusTCPhead() :
transactionID(0),
protocolID(0),
len(0) {}
ModbusTCPhead(uint16_t tid, uint16_t pid, uint16_t _len) :
transactionID(tid),
protocolID(pid),
len(_len) {}
uint16_t transactionID; // Caller-defined identification
uint16_t protocolID; // const 0x0000
uint16_t len; // Length of remainder of TCP packet
inline explicit operator const uint8_t *() {
uint8_t *cp = headRoom;
*cp++ = (transactionID >> 8) & 0xFF;
*cp++ = transactionID & 0xFF;
*cp++ = (protocolID >> 8) & 0xFF;
*cp++ = protocolID & 0xFF;
*cp++ = (len >> 8) & 0xFF;
*cp++ = len & 0xFF;
return headRoom;
}
inline ModbusTCPhead& operator= (ModbusTCPhead& t) {
transactionID = t.transactionID;
protocolID = t.protocolID;
len = t.len;
return *this;
}
protected:
uint8_t headRoom[6]; // Buffer to hold MSB-first TCP header
};
struct RequestEntry {
uint32_t token;
ModbusMessage msg;
ModbusTCPhead head;
uint32_t sentTime;
bool isSyncRequest;
RequestEntry(uint32_t t, ModbusMessage m, bool syncReq = false) :
token(t),
msg(m),
head(ModbusTCPhead()),
sentTime(0),
isSyncRequest(syncReq) {}
};
// Base addRequest and syncRequest both must be present
Error addRequestM(ModbusMessage msg, uint32_t token);
ModbusMessage syncRequestM(ModbusMessage msg, uint32_t token);
// addToQueue: send freshly created request to queue
bool addToQueue(int32_t token, ModbusMessage request, bool syncReq = false);
// send: send request via Client connection
bool send(RequestEntry *request);
// receive: get response via Client connection
// TCPResponse* receive(uint8_t* data, size_t length);
void isInstance() { return; } // make class instantiable
// TCP handling code, all static taking a class instancs as param
void onConnected();
void onDisconnected();
void onACError(AsyncClient* c, int8_t error);
// void onTimeout(uint32_t time);
// void onAck(size_t len, uint32_t time);
void onPacket(uint8_t* data, size_t length);
void onPoll();
void handleSendingQueue();
std::list<RequestEntry*> txQueue; // Queue to hold requests to be sent
std::map<uint16_t, RequestEntry*> rxQueue; // Queue to hold requests to be processed
#if USE_MUTEX
std::mutex sLock; // Mutex to protect state
std::mutex qLock; // Mutex to protect queues
#endif
AsyncClient MTA_client; // Async TCP client
uint32_t MTA_timeout; // Standard timeout value taken
uint32_t MTA_idleTimeout; // Standard timeout value taken
uint16_t MTA_qLimit; // Maximum number of requests to accept in queue
uint32_t MTA_maxInflightRequests; // Maximum number of inflight requests
uint32_t MTA_lastActivity; // Last time there was activity (disabled when queues are not empty)
enum {
DISCONNECTED,
CONNECTING,
CONNECTED
} MTA_state; // TCP connection state
IPAddress MTA_host;
uint16_t MTA_port;
};
#endif

View File

@@ -0,0 +1,138 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_ERROR_H
#define _MODBUS_ERROR_H
#include "ModbusTypeDefs.h"
using namespace Modbus; // NOLINT
class ModbusError {
public:
// Constructor with error code
inline explicit ModbusError(Error e) : err(e) {}
// Empty constructor defaults to 0
inline ModbusError() : err(SUCCESS) {}
// Assignment operators
inline ModbusError& operator=(const ModbusError& e) { err = e.err; return *this; }
inline ModbusError& operator=(const Error e) { err = e; return *this; }
// Copy constructor
inline ModbusError(const ModbusError& m) : err(m.err) {}
// Equality comparison
inline bool operator==(const ModbusError& m) { return (err == m.err); }
inline bool operator==(const Error e) { return (err == e); }
// Inequality comparison
inline bool operator!=(const ModbusError& m) { return (err != m.err); }
inline bool operator!=(const Error e) { return (err != e); }
inline explicit operator Error() { return err; }
inline operator int() { return static_cast<int>(err); }
#ifndef MINIMAL
inline explicit operator const char *() { return getText(err); }
#endif
private:
Error err; // The error code
#ifndef MINIMAL
// Return error as static text
inline static const char *getText(Error err) {
switch (err) {
case SUCCESS : // 0x00,
return "Success";
break;
case ILLEGAL_FUNCTION : // 0x01,
return "Illegal function code";
break;
case ILLEGAL_DATA_ADDRESS : // 0x02,
return "Illegal data address";
break;
case ILLEGAL_DATA_VALUE : // 0x03,
return "Illegal data value";
break;
case SERVER_DEVICE_FAILURE : // 0x04,
return "Server device failure";
break;
case ACKNOWLEDGE : // 0x05,
return "Acknowledge";
break;
case SERVER_DEVICE_BUSY : // 0x06,
return "Server device busy";
break;
case NEGATIVE_ACKNOWLEDGE : // 0x07,
return "Negative acknowledge";
break;
case MEMORY_PARITY_ERROR : // 0x08,
return "Memory parity error";
break;
case GATEWAY_PATH_UNAVAIL : // 0x0A,
return "Gateway path unavailable";
break;
case GATEWAY_TARGET_NO_RESP: // 0x0B,
return "Gateway target not responding";
break;
case TIMEOUT : // 0xE0,
return "Timeout";
break;
case INVALID_SERVER : // 0xE1,
return "Invalid server";
break;
case CRC_ERROR : // 0xE2, // only for Modbus-RTU
return "CRC check error";
break;
case FC_MISMATCH : // 0xE3,
return "Function code mismatch";
break;
case SERVER_ID_MISMATCH : // 0xE4,
return "Server ID mismatch";
break;
case PACKET_LENGTH_ERROR : // 0xE5,
return "Packet length error";
break;
case PARAMETER_COUNT_ERROR : // 0xE6,
return "Wrong # of parameters";
break;
case PARAMETER_LIMIT_ERROR : // 0xE7,
return "Parameter out of bounds";
break;
case REQUEST_QUEUE_FULL : // 0xE8,
return "Request queue full";
break;
case ILLEGAL_IP_OR_PORT : // 0xE9,
return "Illegal IP or port";
break;
case IP_CONNECTION_FAILED : // 0xEA,
return "IP connection failed";
break;
case TCP_HEAD_MISMATCH : // 0xEB,
return "TCP header mismatch";
break;
case EMPTY_MESSAGE : // 0xEC,
return "Incomplete request";
break;
case ASCII_FRAME_ERR : // 0xED,
return "Invalid ASCII frame";
break;
case ASCII_CRC_ERR : // 0xEE,
return "Invalid ASCII CRC";
break;
case ASCII_INVALID_CHAR : // 0xEF,
return "Invalid ASCII character";
break;
case BROADCAST_ERROR : // 0xF0,
return "Broadcast data invalid";
break;
case UNDEFINED_ERROR : // 0xFF // otherwise uncovered communication error
default:
return "Unspecified error";
break;
}
return "What?";
}
#endif
};
#endif

View File

@@ -0,0 +1,705 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusMessage.h"
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_ERROR
#include "Logging.h"
// Default Constructor - takes optional size of MM_data to allocate memory
ModbusMessage::ModbusMessage(uint16_t dataLen) {
if (dataLen) MM_data.reserve(dataLen);
}
// Special message Constructor - takes a std::vector<uint8_t>
ModbusMessage::ModbusMessage(std::vector<uint8_t> s) :
MM_data(s) { }
// Destructor
ModbusMessage::~ModbusMessage() {
// If paranoid, one can use the below :D
// std::vector<uint8_t>().swap(MM_data);
}
// Assignment operator
ModbusMessage& ModbusMessage::operator=(const ModbusMessage& m) {
// Do anything only if not self-assigning
if (this != &m) {
// Copy data from source to target
MM_data = m.MM_data;
}
return *this;
}
#ifndef NO_MOVE
// Move constructor
ModbusMessage::ModbusMessage(ModbusMessage&& m) {
MM_data = std::move(m.MM_data);
}
// Move assignment
ModbusMessage& ModbusMessage::operator=(ModbusMessage&& m) {
MM_data = std::move(m.MM_data);
return *this;
}
#endif
// Copy constructor
ModbusMessage::ModbusMessage(const ModbusMessage& m) :
MM_data(m.MM_data) { }
// Equality comparison
bool ModbusMessage::operator==(const ModbusMessage& m) {
// Prevent self-compare
if (this == &m) return true;
// If size is different, we assume inequality
if (MM_data.size() != m.MM_data.size()) return false;
// We will compare bytes manually - for uint8_t it should work out-of-the-box,
// but the data type might be changed later.
// If we find a difference byte, we found inequality
for (uint16_t i = 0; i < MM_data.size(); ++i) {
if (MM_data[i] != m.MM_data[i]) return false;
}
// Both tests passed ==> equality
return true;
}
// Inequality comparison
bool ModbusMessage::operator!=(const ModbusMessage& m) {
return (!(*this == m));
}
// Conversion to bool
ModbusMessage::operator bool() {
if (MM_data.size() >= 2) return true;
return false;
}
// Exposed methods of std::vector
const uint8_t *ModbusMessage::data() { return MM_data.data(); }
uint16_t ModbusMessage::size() { return MM_data.size(); }
void ModbusMessage::push_back(const uint8_t& val) { MM_data.push_back(val); }
void ModbusMessage::clear() { MM_data.clear(); }
// provide restricted operator[] interface
uint8_t ModbusMessage::operator[](uint16_t index) const {
if (index < MM_data.size()) {
return MM_data[index];
}
LOG_W("Index %d out of bounds (>=%d).\n", index, MM_data.size());
return 0;
}
// Resize internal MM_data
uint16_t ModbusMessage::resize(uint16_t newSize) {
MM_data.resize(newSize);
return MM_data.size();
}
// Add append() for two ModbusMessages or a std::vector<uint8_t> to be appended
void ModbusMessage::append(ModbusMessage& m) {
MM_data.reserve(size() + m.size());
MM_data.insert(MM_data.end(), m.begin(), m.end());
}
void ModbusMessage::append(std::vector<uint8_t>& m) {
MM_data.reserve(size() + m.size());
MM_data.insert(MM_data.end(), m.begin(), m.end());
}
uint8_t ModbusMessage::getServerID() const {
// Only if we have data and it is at least as long to fit serverID and function code, return serverID
if (MM_data.size() >= 2) { return MM_data[0]; }
// Else return 0 - normally the Broadcast serverID, but we will not support that. Full stop. :-D
return 0;
}
// Get MM_data[0] (server ID) and MM_data[1] (function code)
uint8_t ModbusMessage::getFunctionCode() const {
// Only if we have data and it is at least as long to fit serverID and function code, return FC
if (MM_data.size() >= 2) { return MM_data[1]; }
// Else return 0 - which is no valid Modbus FC.
return 0;
}
// getError() - returns error code
Error ModbusMessage::getError() const {
// Do we have data long enough?
if (MM_data.size() > 2) {
// Yes. Does it indicate an error?
if (MM_data[1] & 0x80)
{
// Yes. Get it.
return static_cast<Modbus::Error>(MM_data[2]);
}
}
// Default: everything OK - SUCCESS
return SUCCESS;
}
// Modbus data manipulation
void ModbusMessage::setServerID(uint8_t serverID) {
// We accept here that [0] may allocate a byte!
if (MM_data.empty()) {
MM_data.reserve(3); // At least an error message should fit
}
MM_data[0] = serverID;
}
void ModbusMessage::setFunctionCode(uint8_t FC) {
// We accept here that [0], [1] may allocate bytes!
if (MM_data.empty()) {
MM_data.reserve(3); // At least an error message should fit
}
// No serverID set yet? use a 0 to initialize it to an error-generating value
if (MM_data.size() < 2) MM_data[0] = 0; // intentional invalid server ID!
MM_data[1] = FC;
}
// add() variant to copy a buffer into MM_data. Returns updated size
uint16_t ModbusMessage::add(const uint8_t *arrayOfBytes, uint16_t count) {
// Copy it
while (count--) {
MM_data.push_back(*arrayOfBytes++);
}
// Return updated size (logical length of message so far)
return MM_data.size();
}
// determineFloatOrder: calculate the sequence of bytes in a float value
uint8_t ModbusMessage::determineFloatOrder() {
constexpr uint8_t floatSize = sizeof(float);
// Only do it if not done yet
if (floatOrder[0] == 0xFF) {
// We need to calculate it.
// This will only work for 32bit floats, so check that
if (floatSize != 4) {
// OOPS! we cannot proceed.
LOG_E("Oops. float seems to be %d bytes wide instead of 4.\n", floatSize);
return 0;
}
uint32_t i = 77230; // int value to go into a float without rounding error
float f = i; // assign it
uint8_t *b = (uint8_t *)&f; // Pointer to bytes of f
uint8_t expect[floatSize] = { 0x47, 0x96, 0xd7, 0x00 }; // IEEE754 representation
uint8_t matches = 0; // number of bytes successfully matched
// Loop over the bytes of the expected sequence
for (uint8_t inx = 0; inx < floatSize; ++inx) {
// Loop over the real bytes of f
for (uint8_t trg = 0; trg < floatSize; ++trg) {
if (expect[inx] == b[trg]) {
floatOrder[inx] = trg;
matches++;
break;
}
}
}
// All bytes found?
if (matches != floatSize) {
// No! There is something fishy...
LOG_E("Unable to determine float byte order (matched=%d of %d)\n", matches, floatSize);
floatOrder[0] = 0xFF;
return 0;
} else {
HEXDUMP_V("floatOrder", floatOrder, floatSize);
}
}
return floatSize;
}
// determineDoubleOrder: calculate the sequence of bytes in a double value
uint8_t ModbusMessage::determineDoubleOrder() {
constexpr uint8_t doubleSize = sizeof(double);
// Only do it if not done yet
if (doubleOrder[0] == 0xFF) {
// We need to calculate it.
// This will only work for 64bit doubles, so check that
if (doubleSize != 8) {
// OOPS! we cannot proceed.
LOG_E("Oops. double seems to be %d bytes wide instead of 8.\n", doubleSize);
return 0;
}
uint64_t i = 5791007487489389; // int64 value to go into a double without rounding error
double f = i; // assign it
uint8_t *b = (uint8_t *)&f; // Pointer to bytes of f
uint8_t expect[doubleSize] = { 0x43, 0x34, 0x92, 0xE4, 0x00, 0x2E, 0xF5, 0x6D }; // IEEE754 representation
uint8_t matches = 0; // number of bytes successfully matched
// Loop over the bytes of the expected sequence
for (uint8_t inx = 0; inx < doubleSize; ++inx) {
// Loop over the real bytes of f
for (uint8_t trg = 0; trg < doubleSize; ++trg) {
if (expect[inx] == b[trg]) {
doubleOrder[inx] = trg;
matches++;
break;
}
}
}
// All bytes found?
if (matches != doubleSize) {
// No! There is something fishy...
LOG_E("Unable to determine double byte order (matched=%d of %d)\n", matches, doubleSize);
doubleOrder[0] = 0xFF;
return 0;
} else {
HEXDUMP_V("doubleOrder", doubleOrder, doubleSize);
}
}
return doubleSize;
}
// swapFloat() and swapDouble() will re-order the bytes of a float or double value
// according a user-given pattern
float ModbusMessage::swapFloat(float& f, int swapRule) {
LOG_V("swap float, swapRule=%02X\n", swapRule);
// Make a byte pointer to the given float
uint8_t *src = (uint8_t *)&f;
// Define a "work bench" float and byte pointer to it
float interim;
uint8_t *dst = (uint8_t *)&interim;
// Loop over all bytes of a float
for (uint8_t i = 0; i < sizeof(float); ++i) {
// Get i-th byte from the spot the swap table tells
// (only the first 4 tables are valid for floats)
LOG_V("dst[%d] = src[%d]\n", i, swapTables[swapRule & 0x03][i]);
dst[i] = src[swapTables[swapRule & 0x03][i]];
// Does the swar rule require nibble swaps?
if (swapRule & 0x08) {
// Yes, it does.
uint8_t nib = ((dst[i] & 0x0f) << 4) | ((dst[i] >> 4) & 0x0F);
dst[i] = nib;
}
}
// Save and return result
f = interim;
return interim;
}
double ModbusMessage::swapDouble(double& f, int swapRule) {
LOG_V("swap double, swapRule=%02X\n", swapRule);
// Make a byte pointer to the given double
uint8_t *src = (uint8_t *)&f;
// Define a "work bench" double and byte pointer to it
double interim;
uint8_t *dst = (uint8_t *)&interim;
// Loop over all bytes of a double
for (uint8_t i = 0; i < sizeof(double); ++i) {
// Get i-th byte from the spot the swap table tells
LOG_V("dst[%d] = src[%d]\n", i, swapTables[swapRule & 0x07][i]);
dst[i] = src[swapTables[swapRule & 0x07][i]];
// Does the swar rule require nibble swaps?
if (swapRule & 0x08) {
// Yes, it does.
uint8_t nib = ((dst[i] & 0x0f) << 4) | ((dst[i] >> 4) & 0x0F);
dst[i] = nib;
}
}
// Save and return result
f = interim;
return interim;
}
// add() variant for a vector of uint8_t
uint16_t ModbusMessage::add(vector<uint8_t> v) {
for (auto& b: v) {
MM_data.push_back(b);
}
return MM_data.size();
}
// add() variants for float and double values
// values will be added in IEEE754 byte sequence (MSB first)
uint16_t ModbusMessage::add(float v, int swapRule) {
// First check if we need to determine byte order
LOG_V("add float, swapRule=%02X\n", swapRule);
HEXDUMP_V("float", (uint8_t *)&v, sizeof(float));
if (determineFloatOrder()) {
// If we get here, the floatOrder is known
float interim = 0;
uint8_t *dst = (uint8_t *)&interim;
uint8_t *src = (uint8_t *)&v;
// Put out the bytes of v in normalized sequence
for (uint8_t i = 0; i < sizeof(float); ++i) {
dst[i] = src[floatOrder[i]];
}
HEXDUMP_V("normalized float", (uint8_t *)&interim, sizeof(float));
// Do we need to apply a swap rule?
if (swapRule & 0x0B) {
// Yes, so do it.
swapFloat(interim, swapRule & 0x0B);
}
HEXDUMP_V("swapped float", (uint8_t *)&interim, sizeof(float));
// Put out the bytes of v in normalized (and swapped) sequence
for (uint8_t i = 0; i < sizeof(float); ++i) {
MM_data.push_back(dst[i]);
}
}
return MM_data.size();
}
uint16_t ModbusMessage::add(double v, int swapRule) {
// First check if we need to determine byte order
LOG_V("add double, swapRule=%02X\n", swapRule);
HEXDUMP_V("double", (uint8_t *)&v, sizeof(double));
if (determineDoubleOrder()) {
// If we get here, the doubleOrder is known
double interim = 0;
uint8_t *dst = (uint8_t *)&interim;
uint8_t *src = (uint8_t *)&v;
// Put out the bytes of v in normalized sequence
for (uint8_t i = 0; i < sizeof(double); ++i) {
dst[i] = src[doubleOrder[i]];
}
HEXDUMP_V("normalized double", (uint8_t *)&interim, sizeof(double));
// Do we need to apply a swap rule?
if (swapRule & 0x0F) {
// Yes, so do it.
swapDouble(interim, swapRule & 0x0F);
}
HEXDUMP_V("swapped double", (uint8_t *)&interim, sizeof(double));
// Put out the bytes of v in normalized (and swapped) sequence
for (uint8_t i = 0; i < sizeof(double); ++i) {
MM_data.push_back(dst[i]);
}
}
return MM_data.size();
}
// get() variants for float and double values
// values will be read in IEEE754 byte sequence (MSB first)
uint16_t ModbusMessage::get(uint16_t index, float& v, int swapRule) const {
// First check if we need to determine byte order
if (determineFloatOrder()) {
// If we get here, the floatOrder is known
// Will it fit?
if (index <= MM_data.size() - sizeof(float)) {
// Yes. Get the bytes of v in normalized sequence
uint8_t *bytes = (uint8_t *)&v;
for (uint8_t i = 0; i < sizeof(float); ++i) {
bytes[i] = MM_data[index + floatOrder[i]];
}
HEXDUMP_V("got float", (uint8_t *)&v, sizeof(float));
// Do we need to apply a swap rule?
if (swapRule & 0x0B) {
// Yes, so do it.
swapFloat(v, swapRule & 0x0B);
}
HEXDUMP_V("got float swapped", (uint8_t *)&v, sizeof(float));
index += sizeof(float);
}
}
return index;
}
uint16_t ModbusMessage::get(uint16_t index, double& v, int swapRule) const {
// First check if we need to determine byte order
if (determineDoubleOrder()) {
// If we get here, the doubleOrder is known
// Will it fit?
if (index <= MM_data.size() - sizeof(double)) {
// Yes. Get the bytes of v in normalized sequence
uint8_t *bytes = (uint8_t *)&v;
for (uint8_t i = 0; i < sizeof(double); ++i) {
bytes[i] = MM_data[index + doubleOrder[i]];
}
HEXDUMP_V("got double", (uint8_t *)&v, sizeof(double));
// Do we need to apply a swap rule?
if (swapRule & 0x0F) {
// Yes, so do it.
swapDouble(v, swapRule & 0x0F);
}
HEXDUMP_V("got double swapped", (uint8_t *)&v, sizeof(double));
index += sizeof(double);
}
}
return index;
}
// get() - read a byte array of a given size into a vector<uint8_t>. Returns updated index
uint16_t ModbusMessage::get(uint16_t index, vector<uint8_t>& v, uint8_t count) const {
// Clean target vector
v.clear();
// Loop until required count is complete or the source is exhausted
while (index < MM_data.size() && count--) {
v.push_back(MM_data[index++]);
}
return index;
}
// Data validation methods for the different factory calls
// 0. serverID and function code - used by all of the below
Error ModbusMessage::checkServerFC(uint8_t serverID, uint8_t functionCode) {
if (serverID == 0) return INVALID_SERVER; // Broadcast - not supported here
if (serverID > 247) return INVALID_SERVER; // Reserved server addresses
if (FCT::getType(functionCode) == FCILLEGAL) return ILLEGAL_FUNCTION; // FC 0 does not exist
return SUCCESS;
}
// 1. no additional parameter (FCs 0x07, 0x0b, 0x0c, 0x11)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode) {
LOG_V("Check data #1\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC07_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
}
}
return returnCode;
}
// 2. one uint16_t parameter (FC 0x18)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1) {
LOG_V("Check data #2\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC18_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
}
}
return returnCode;
}
// 3. two uint16_t parameters (FC 0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2) {
LOG_V("Check data #3\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC01_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
} else {
switch (functionCode) {
case 0x01:
case 0x02:
if ((p2 > 0x7d0) || (p2 == 0)) returnCode = PARAMETER_LIMIT_ERROR;
break;
case 0x03:
case 0x04:
if ((p2 > 0x7d) || (p2 == 0)) returnCode = PARAMETER_LIMIT_ERROR;
break;
case 0x05:
if ((p2 != 0) && (p2 != 0xff00)) returnCode = PARAMETER_LIMIT_ERROR;
break;
}
}
}
return returnCode;
}
// 4. three uint16_t parameters (FC 0x16)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint16_t p3) {
LOG_V("Check data #4\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC16_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
}
}
return returnCode;
}
// 5. two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of words (FC 0x10)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint16_t *arrayOfWords) {
LOG_V("Check data #5\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC10_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
} else {
if ((p2 == 0) || (p2 > 0x7b)) returnCode = PARAMETER_LIMIT_ERROR;
else if (count != (p2 * 2)) returnCode = ILLEGAL_DATA_VALUE;
}
}
return returnCode;
}
// 6. two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of bytes (FC 0x0f)
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint8_t *arrayOfBytes) {
LOG_V("Check data #6\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FC0F_TYPE && ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
} else {
if ((p2 == 0) || (p2 > 0x7b0)) returnCode = PARAMETER_LIMIT_ERROR;
else if (count != ((p2 / 8 + (p2 % 8 ? 1 : 0)))) returnCode = ILLEGAL_DATA_VALUE;
}
}
return returnCode;
}
// 7. generic constructor for preformatted data ==> count is counting bytes!
Error ModbusMessage::checkData(uint8_t serverID, uint8_t functionCode, uint16_t count, uint8_t *arrayOfBytes) {
LOG_V("Check data #7\n");
Error returnCode = checkServerFC(serverID, functionCode);
if (returnCode == SUCCESS)
{
FCType ft = FCT::getType(functionCode);
if (ft != FCUSER && ft != FCGENERIC) {
returnCode = PARAMETER_COUNT_ERROR;
}
}
return returnCode;
}
// Factory methods to create valid Modbus messages from the parameters
// 1. no additional parameter (FCs 0x07, 0x0b, 0x0c, 0x11)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(2);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode);
}
return returnCode;
}
// 2. one uint16_t parameter (FC 0x18)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, p1);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(4);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode, p1);
}
return returnCode;
}
// 3. two uint16_t parameters (FC 0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, p1, p2);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(6);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode, p1, p2);
}
return returnCode;
}
// 4. three uint16_t parameters (FC 0x16)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint16_t p3) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, p1, p2, p3);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(8);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode, p1, p2, p3);
}
return returnCode;
}
// 5. two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of words (FC 0x10)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint16_t *arrayOfWords) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, p1, p2, count, arrayOfWords);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(7 + count * 2);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode, p1, p2);
add(count);
for (uint8_t i = 0; i < (count >> 1); ++i) {
add(arrayOfWords[i]);
}
}
return returnCode;
}
// 6. two uint16_t parameters, a uint8_t length byte and a uint8_t* pointer to array of bytes (FC 0x0f)
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint8_t *arrayOfBytes) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, p1, p2, count, arrayOfBytes);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(7 + count);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode, p1, p2);
add(count);
for (uint8_t i = 0; i < count; ++i) {
add(arrayOfBytes[i]);
}
}
return returnCode;
}
// 7. generic constructor for preformatted data ==> count is counting bytes!
Error ModbusMessage::setMessage(uint8_t serverID, uint8_t functionCode, uint16_t count, uint8_t *arrayOfBytes) {
// Check parameter for validity
Error returnCode = checkData(serverID, functionCode, count, arrayOfBytes);
// No error?
if (returnCode == SUCCESS)
{
// Yes, all fine. Create new ModbusMessage
MM_data.reserve(2 + count);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, functionCode);
for (uint8_t i = 0; i < count; ++i) {
add(arrayOfBytes[i]);
}
}
return returnCode;
}
// 8. Error response generator
Error ModbusMessage::setError(uint8_t serverID, uint8_t functionCode, Error errorCode) {
// No error checking for server ID or function code here, as both may be the cause for the message!?
MM_data.reserve(3);
MM_data.shrink_to_fit();
MM_data.clear();
add(serverID, static_cast<uint8_t>((functionCode | 0x80) & 0xFF), static_cast<uint8_t>(errorCode));
return SUCCESS;
}
// Error output in case a message constructor will fail
void ModbusMessage::printError(const char *file, int lineNo, Error e, uint8_t serverID, uint8_t functionCode) {
LOG_E("(%s, line %d) Error in constructor: %02X - %s (%02X/%02X)\n", file_name(file), lineNo, e, (const char *)(ModbusError(e)), serverID, functionCode);
}
uint8_t ModbusMessage::floatOrder[] = { 0xFF };
uint8_t ModbusMessage::doubleOrder[] = { 0xFF };

View File

@@ -0,0 +1,216 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_MESSAGE_H
#define _MODBUS_MESSAGE_H
#include "ModbusTypeDefs.h"
#include "ModbusError.h"
#include <type_traits>
#include <vector>
using Modbus::Error;
using Modbus::FCType;
using Modbus::FCT;
using std::vector;
class ModbusMessage {
public:
// Default empty message Constructor - optionally takes expected size of MM_data
explicit ModbusMessage(uint16_t dataLen = 0);
// Special message Constructor - takes a std::vector<uint8_t>
explicit ModbusMessage(std::vector<uint8_t> s);
// Message constructors - internally setMessage() is called
// WARNING: if parameters are invalid, message will _NOT_ be set up!
template <typename... Args>
ModbusMessage(uint8_t serverID, uint8_t functionCode, Args&&... args) { // NOLINT
Error e = SUCCESS;
if ((e = setMessage(serverID, functionCode, std::forward<Args>(args) ...)) != SUCCESS) {
printError(__FILE__, __LINE__, e, serverID, functionCode);
}
}
// Destructor
~ModbusMessage();
// Assignment operator
ModbusMessage& operator=(const ModbusMessage& m);
// Copy constructor
ModbusMessage(const ModbusMessage& m);
#ifndef NO_MOVE
// Move constructor
ModbusMessage(ModbusMessage&& m);
// Move assignment
ModbusMessage& operator=(ModbusMessage&& m);
#endif
// Comparison operators
bool operator==(const ModbusMessage& m);
bool operator!=(const ModbusMessage& m);
operator bool();
// Exposed methods of std::vector
const uint8_t *data(); // address of MM_data
uint16_t size(); // used length in MM_data
uint8_t operator[](uint16_t index) const; // provide restricted operator[] interface
void push_back(const uint8_t& val); // add a byte at the end of MM_data
void clear(); // delete message contents
uint16_t resize(uint16_t newSize); // resize MM_data
// provide iterator interface on MM_data
typedef std::vector<uint8_t>::const_iterator const_iterator;
const_iterator begin() const { return MM_data.begin(); }
const_iterator end() const { return MM_data.end(); }
// Add append() for two ModbusMessages or a std::vector<uint8_t> to be appended
void append(ModbusMessage& m);
void append(std::vector<uint8_t>& m);
// Modbus data extraction
uint8_t getServerID() const; // returns Server ID or 0 if MM_data is shorter than 3
uint8_t getFunctionCode() const; // returns FC or 0 if MM_data is shorter than 3
Error getError() const; // getError() - returns error code (MM_data[2], if MM_data[1] > 0x7F, else SUCCESS)
// Modbus data manipulation
void setServerID(uint8_t serverID); // Change server ID
void setFunctionCode(uint8_t FC); // Change function code
// add() variant to copy a buffer into MM_data. Returns updated size
uint16_t add(const uint8_t *arrayOfBytes, uint16_t count);
// add() - add a single data element MSB first to MM_data. Returns updated size
template <class T> uint16_t add(T v) {
uint16_t sz = sizeof(T); // Size of value to be added
// Copy it MSB first
while (sz) {
sz--;
MM_data.push_back((v >> (sz << 3)) & 0xFF);
}
// Return updated size (logical length of message so far)
return MM_data.size();
}
// Template function to extend add(A) to add(A, B, C, ...)
template <class T, class... Args>
typename std::enable_if<!std::is_pointer<T>::value, uint16_t>::type
add(T v, Args... args) {
add(v);
return add(args...);
}
// get() - read a byte array of a given size into a vector<uint8_t>. Returns updated index
uint16_t get(uint16_t index, vector<uint8_t>& v, uint8_t count) const;
// get() - recursion stopper for template function below
inline uint16_t get(uint16_t index) const { return index; }
// Template function to extend getOne(index, A&) to get(index, A&, B&, C&, ...)
template <class T, class... Args>
typename std::enable_if<!std::is_pointer<T>::value, uint16_t>::type
get(uint16_t index, T& v, Args&... args) const {
uint16_t pos = getOne(index, v);
return get(pos, args...);
}
// add() variant for vectors of uint8_t
uint16_t add(vector<uint8_t> v);
// add() variants for float and double values
uint16_t add(float v, int swapRules = 0);
uint16_t add(double v, int swapRules = 0);
// get() variants for float and double values
uint16_t get(uint16_t index, float& v, int swapRules = 0) const;
uint16_t get(uint16_t index, double& v, int swapRules = 0) const;
// Message generation methods
// 1. no additional parameter (FCs 0x07, 0x0b, 0x0c, 0x11)
Error setMessage(uint8_t serverID, uint8_t functionCode);
// 2. one uint16_t parameter (FC 0x18)
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1);
// 3. two uint16_t parameters (FC 0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2);
// 4. three uint16_t parameters (FC 0x16)
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint16_t p3);
// 5. two uint16_t parameters, a uint8_t length byte and a uint8_t* pointer to array of words (FC 0x10)
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint16_t *arrayOfWords);
// 6. two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of bytes (FC 0x0f)
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint8_t *arrayOfBytes);
// 7. generic constructor for preformatted data ==> count is counting bytes!
Error setMessage(uint8_t serverID, uint8_t functionCode, uint16_t count, uint8_t *arrayOfBytes);
// 8. error response
Error setError(uint8_t serverID, uint8_t functionCode, Error errorCode);
protected:
// Data validation methods - used by the above!
// 0. serverID and function code - used by all of the below
static Error checkServerFC(uint8_t serverID, uint8_t functionCode);
// 1. no additional parameter (FCs 0x07, 0x0b, 0x0c, 0x11)
static Error checkData(uint8_t serverID, uint8_t functionCode);
// 2. one uint16_t parameter (FC 0x18)
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1);
// 3. two uint16_t parameters (FC 0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2);
// 4. three uint16_t parameters (FC 0x16)
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint16_t p3);
// 5. two uint16_t parameters, a uint8_t length byte and a uint8_t* pointer to array of words (FC 0x10)
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint16_t *arrayOfWords);
// 6. two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of bytes (FC 0x0f)
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t p1, uint16_t p2, uint8_t count, uint8_t *arrayOfBytes);
// 7. generic constructor for preformatted data ==> count is counting bytes!
static Error checkData(uint8_t serverID, uint8_t functionCode, uint16_t count, uint8_t *arrayOfBytes);
// Error output in case a message constructor will fail
static void printError(const char *file, int lineNo, Error e, uint8_t serverID, uint8_t functionCode);
std::vector<uint8_t> MM_data; // Message data buffer
static uint8_t floatOrder[sizeof(float)]; // order of bytes in a float variable
static uint8_t doubleOrder[sizeof(double)]; // order of bytes in a double variable
static uint8_t determineFloatOrder();
static uint8_t determineDoubleOrder();
static float swapFloat(float& f, int swapRule);
static double swapDouble(double& f, int swapRule);
// getOne() - read a MSB-first value starting at byte index. Returns updated index
template <typename T> uint16_t getOne(uint16_t index, T& retval) const {
uint16_t sz = sizeof(retval); // Size of value to be read
retval = 0; // return value
// Will it fit?
if (index <= MM_data.size() - sz) {
// Yes. Copy it MSB first
while (sz) {
sz--;
retval <<= 8;
retval |= MM_data[index++];
}
}
return index;
}
};
#endif

View File

@@ -0,0 +1,175 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include <Arduino.h>
#include "ModbusServer.h"
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
// registerWorker: register a worker function for a certain serverID/FC combination
// If there is one already, it will be overwritten!
void ModbusServer::registerWorker(uint8_t serverID, uint8_t functionCode, MBSworker worker) {
workerMap[serverID][functionCode] = worker;
LOG_D("Registered worker for %02X/%02X\n", serverID, functionCode);
}
// getWorker: if a worker function is registered, return its address, nullptr otherwise
MBSworker ModbusServer::getWorker(uint8_t serverID, uint8_t functionCode) {
// Search the FC map associated with the serverID
auto svmap = workerMap.find(serverID);
// Is there one?
if (svmap != workerMap.end()) {
// Yes. Now look for the function code in the inner map
auto fcmap = svmap->second.find(functionCode);;
// Found it?
if (fcmap != svmap->second.end()) {
// Yes. Return the function pointer for it.
LOG_D("Worker found for %02X/%02X\n", serverID, functionCode);
return fcmap->second;
// No, no explicit worker found, but may be there is one for ANY_FUNCTION_CODE?
} else {
fcmap = svmap->second.find(ANY_FUNCTION_CODE);;
// Found it?
if (fcmap != svmap->second.end()) {
// Yes. Return the function pointer for it.
LOG_D("Worker found for %02X/ANY\n", serverID);
return fcmap->second;
}
}
}
// No matching function pointer found
LOG_D("No matching worker found\n");
return nullptr;
}
// unregisterWorker; remove again all or part of the registered workers for a given server ID
// Returns true if the worker was found and removed
bool ModbusServer::unregisterWorker(uint8_t serverID, uint8_t functionCode) {
uint16_t numEntries = 0; // Number of entries removed
// Is there at least one entry for the serverID?
auto svmap = workerMap.find(serverID);
// Is there one?
if (svmap != workerMap.end()) {
// Yes. we may proceed with it
// Are we to look for a single serverID/FC combination?
if (functionCode) {
// Yes.
numEntries = svmap->second.erase(functionCode);
} else {
// No, the serverID shall be removed with all references
numEntries = workerMap.erase(serverID);
}
}
LOG_D("Removed %d worker entries for %d/%d\n", numEntries, serverID, functionCode);
return (numEntries ? true : false);
}
// isServerFor: if any worker function is registered for the given serverID, return true
bool ModbusServer::isServerFor(uint8_t serverID) {
// Search the FC map for the serverID
auto svmap = workerMap.find(serverID);
// Is it there? Then return true
if (svmap != workerMap.end()) return true;
// No, serverID was not found. Return false
return false;
}
// getMessageCount: read number of messages processed
uint32_t ModbusServer::getMessageCount() {
return messageCount;
}
// getErrorCount: read number of errors responded
uint32_t ModbusServer::getErrorCount() {
return errorCount;
}
// resetCounts: set both message and error counts to zero
void ModbusServer::resetCounts() {
{
LOCK_GUARD(cntLock, m);
messageCount = 0;
errorCount = 0;
}
}
// LocalRequest: get response from locally running server.
ModbusMessage ModbusServer::localRequest(ModbusMessage msg) {
ModbusMessage m;
uint8_t serverID = msg.getServerID();
uint8_t functionCode = msg.getFunctionCode();
LOG_D("Local request for %02X/%02X\n", serverID, functionCode);
HEXDUMP_V("Request", msg.data(), msg.size());
messageCount++;
// Try to get a worker for the request
MBSworker worker = getWorker(serverID, functionCode);
// Did we get one?
if (worker != nullptr) {
// Yes. call it and return the response
LOG_D("Call worker\n");
m = worker(msg);
LOG_D("Worker responded\n");
HEXDUMP_V("Worker response", m.data(), m.size());
// Process Response. Is it one of the predefined types?
if (m[0] == 0xFF && (m[1] == 0xF0 || m[1] == 0xF1)) {
// Yes. Check it
switch (m[1]) {
case 0xF0: // NIL
m.clear();
break;
case 0xF1: // ECHO
m.clear();
m.append(msg);
break;
default: // Will not get here, but lint likes it!
break;
}
}
HEXDUMP_V("Response", m.data(), m.size());
if (m.getError() != SUCCESS) {
errorCount++;
}
return m;
} else {
LOG_D("No worker found. Error response.\n");
// No. Is there at least one worker for the serverID?
if (isServerFor(serverID)) {
// Yes. Respond with "illegal function code"
m.setError(serverID, functionCode, ILLEGAL_FUNCTION);
} else {
// No. Respond with "Invalid server ID"
m.setError(serverID, functionCode, INVALID_SERVER);
}
errorCount++;
return m;
}
// We should never get here...
LOG_C("Internal problem: should not get here!\n");
m.setError(serverID, functionCode, UNDEFINED_ERROR);
errorCount++;
return m;
}
// Constructor
ModbusServer::ModbusServer() :
messageCount(0),
errorCount(0) { }
// Destructor
ModbusServer::~ModbusServer() {
}
// listServer: Print out all mapped server/FC combinations
void ModbusServer::listServer() {
for (auto it = workerMap.begin(); it != workerMap.end(); ++it) {
LOG_N("Server %3d: ", it->first);
for (auto it2 = it->second.begin(); it2 != it->second.end(); it2++) {
LOGRAW_N(" %02X", it2->first);
}
LOGRAW_N("\n");
}
}

View File

@@ -0,0 +1,86 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_H
#define _MODBUS_SERVER_H
#include "options.h"
#include <map>
#include <vector>
#include <functional>
#if USE_MUTEX
#include <mutex> // NOLINT
#endif
#include "ModbusTypeDefs.h"
#include "ModbusError.h"
#include "ModbusMessage.h"
#if USE_MUTEX
using std::mutex;
using std::lock_guard;
#endif
// Standard response variants for "no response" and "echo the request"
const ModbusMessage NIL_RESPONSE (std::vector<uint8_t>{0xFF, 0xF0});
const ModbusMessage ECHO_RESPONSE(std::vector<uint8_t>{0xFF, 0xF1});
// MBSworker: function signature for worker functions to handle single serverID/functionCode combinations
using MBSworker = std::function<ModbusMessage(ModbusMessage msg)>;
class ModbusServer {
public:
// registerWorker: register a worker function for a certain serverID/FC combination
// If there is one already, it will be overwritten!
void registerWorker(uint8_t serverID, uint8_t functionCode, MBSworker worker);
// getWorker: if a worker function is registered, return its address, nullptr otherwise
MBSworker getWorker(uint8_t serverID, uint8_t functionCode);
// unregisterWorker; remove again all or part of the registered workers for a given server ID
// Returns true if the worker was found and removed
bool unregisterWorker(uint8_t serverID, uint8_t functionCode = 0);
// isServerFor: if any worker function is registered for the given serverID, return true
bool isServerFor(uint8_t serverID);
// getMessageCount: read number of messages processed
uint32_t getMessageCount();
// getErrorCount: read number of errors responded
uint32_t getErrorCount();
// resetCounts: set both message and error counts to zero
void resetCounts();
// Local request to the server
ModbusMessage localRequest(ModbusMessage msg);
// listServer: print out all server/FC combinations served
void listServer();
protected:
// Constructor
ModbusServer();
// Destructor
~ModbusServer();
// Prevent copy construction or assignment
ModbusServer(ModbusServer& other) = delete;
ModbusServer& operator=(ModbusServer& other) = delete;
// Virtual function to prevent this class being instantiated
virtual void isInstance() = 0;
std::map<uint8_t, std::map<uint8_t, MBSworker>> workerMap; // map on serverID->functionCode->worker function
uint32_t messageCount; // Number of Requests processed
uint32_t errorCount; // Number of errors responded
#if USE_MUTEX
mutex m; // mutex to cover changes to messageCount and errorCount
#endif
};
#endif

View File

@@ -0,0 +1,19 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_ETHERNET_H
#define _MODBUS_SERVER_ETHERNET_H
#include "options.h"
#if HAS_ETHERNET == 1
#include <Ethernet.h>
#include <SPI.h>
#undef SERVER_END
#define SERVER_END // NIL for Ethernet
#include "ModbusServerTCPtemp.h"
using ModbusServerEthernet = ModbusServerTCP<EthernetServer, EthernetClient>;
#endif
#endif

View File

@@ -0,0 +1,260 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusServerRTU.h"
#if HAS_FREERTOS
#undef LOG_LEVEL_LOCAL
#include "Logging.h"
// Init number of created ModbusServerRTU objects
uint8_t ModbusServerRTU::instanceCounter = 0;
// Constructor with RTS pin GPIO (or -1)
ModbusServerRTU::ModbusServerRTU(uint32_t timeout, int rtsPin) :
ModbusServer(),
serverTask(nullptr),
serverTimeout(timeout),
MSRserial(nullptr),
MSRinterval(2000), // will be calculated in begin()!
MSRlastMicros(0),
MSRrtsPin(rtsPin),
MSRuseASCII(false),
MSRskipLeadingZeroByte(false),
listener(nullptr),
sniffer(nullptr) {
// Count instances one up
instanceCounter++;
// If we have a GPIO RE/DE pin, configure it.
if (MSRrtsPin >= 0) {
pinMode(MSRrtsPin, OUTPUT);
MRTSrts = [this](bool level) {
digitalWrite(MSRrtsPin, level);
};
MRTSrts(LOW);
} else {
MRTSrts = RTUutils::RTSauto;
}
}
// Constructor with RTS callback
ModbusServerRTU::ModbusServerRTU(uint32_t timeout, RTScallback rts) :
ModbusServer(),
serverTask(nullptr),
serverTimeout(timeout),
MSRserial(nullptr),
MSRinterval(2000), // will be calculated in begin()!
MSRlastMicros(0),
MRTSrts(rts),
MSRuseASCII(false),
MSRskipLeadingZeroByte(false),
listener(nullptr),
sniffer(nullptr) {
// Count instances one up
instanceCounter++;
// Configure RTS callback
MSRrtsPin = -1;
MRTSrts(LOW);
}
// Destructor
ModbusServerRTU::~ModbusServerRTU() {
}
// start: create task with RTU server - general version
void ModbusServerRTU::begin(Stream& serial, uint32_t baudRate, int coreID) {
MSRserial = &serial;
doBegin(baudRate, coreID);
}
// start: create task with RTU server - HardwareSerial versions
void ModbusServerRTU::begin(HardwareSerial& serial, int coreID) {
MSRserial = &serial;
uint32_t baudRate = serial.baudRate();
serial.setRxFIFOFull(1);
doBegin(baudRate, coreID);
}
void ModbusServerRTU::doBegin(uint32_t baudRate, int coreID) {
// Task already running? Stop it in case.
end();
// Set minimum interval time
MSRinterval = RTUutils::calculateInterval(baudRate);
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "MBsrv%02XRTU", instanceCounter);
// Start task to handle the client
xTaskCreatePinnedToCore((TaskFunction_t)&serve, taskName, SERVER_TASK_STACK, this, 8, &serverTask, coreID >= 0 ? coreID : NULL);
LOG_D("Server task %d started. Interval=%d\n", (uint32_t)serverTask, MSRinterval);
}
// end: kill server task
void ModbusServerRTU::end() {
if (serverTask != nullptr) {
vTaskDelete(serverTask);
LOG_D("Server task %d stopped.\n", (uint32_t)serverTask);
serverTask = nullptr;
}
}
// Toggle protocol to ModbusASCII
void ModbusServerRTU::useModbusASCII(unsigned long timeout) {
MSRuseASCII = true;
serverTimeout = timeout; // Set timeout to ASCII's value
LOG_D("Protocol mode: ASCII\n");
}
// Toggle protocol to ModbusRTU
void ModbusServerRTU::useModbusRTU() {
MSRuseASCII = false;
LOG_D("Protocol mode: RTU\n");
}
// Inquire protocol mode
bool ModbusServerRTU::isModbusASCII() {
return MSRuseASCII;
}
// Toggle skipping of leading 0x00 byte
void ModbusServerRTU::skipLeading0x00(bool onOff) {
MSRskipLeadingZeroByte = onOff;
LOG_D("Skip leading 0x00 mode = %s\n", onOff ? "ON" : "OFF");
}
// Special case: worker to react on broadcast requests
void ModbusServerRTU::registerBroadcastWorker(MSRlistener worker) {
// If there is one already, it will be overwritten!
listener = worker;
LOG_D("Registered worker for broadcast requests\n");
}
// Even more special: register a sniffer worker
void ModbusServerRTU::registerSniffer(MSRlistener worker) {
// If there is one already, it will be overwritten!
// This holds true for the broadcast worker as well,
// so a sniffer never will do else but to sniff on broadcast requests!
sniffer = worker;
LOG_D("Registered sniffer\n");
}
// serve: loop until killed and receive messages from the RTU interface
void ModbusServerRTU::serve(ModbusServerRTU *myServer) {
ModbusMessage request; // received request message
ModbusMessage m; // Application's response data
ModbusMessage response; // Response proper to be sent
// init microseconds timer
myServer->MSRlastMicros = micros();
while (true) {
// Initialize all temporary vectors
request.clear();
response.clear();
m.clear();
// Wait for and read an request
request = RTUutils::receive(
'S',
*(myServer->MSRserial),
myServer->serverTimeout,
myServer->MSRlastMicros,
myServer->MSRinterval,
myServer->MSRuseASCII,
myServer->MSRskipLeadingZeroByte);
// Request longer than 1 byte (that will signal an error in receive())?
if (request.size() > 1) {
LOG_D("Request received.\n");
// Yes.
// Do we have a sniffer listening?
if (myServer->sniffer) {
// Yes. call it
myServer->sniffer(request);
}
// Is it a broadcast?
if (request[0] == 0) {
// Yes. Do we have a listener?
if (myServer->listener) {
// Yes. call it
myServer->listener(request);
}
// else we simply ignore it
} else {
// No Broadcast.
// Do we have a callback function registered for it?
MBSworker callBack = myServer->getWorker(request[0], request[1]);
if (callBack) {
LOG_D("Callback found.\n");
// Yes, we do. Count the message
{
LOCK_GUARD(cntLock, myServer->m);
myServer->messageCount++;
}
// Get the user's response
LOG_D("Callback called.\n");
m = callBack(request);
HEXDUMP_V("Callback response", m.data(), m.size());
// Process Response. Is it one of the predefined types?
if (m[0] == 0xFF && (m[1] == 0xF0 || m[1] == 0xF1)) {
// Yes. Check it
switch (m[1]) {
case 0xF0: // NIL
response.clear();
break;
case 0xF1: // ECHO
response = request;
if (request.getFunctionCode() == WRITE_MULT_REGISTERS ||
request.getFunctionCode() == WRITE_MULT_COILS) {
response.resize(6);
}
break;
default: // Will not get here, but lint likes it!
break;
}
} else {
// No predefined. User provided data in free format
response = m;
}
} else {
// No callback. Is at least the serverID valid and no broadcast?
if (myServer->isServerFor(request[0]) && request[0] != 0x00) {
// Yes. Send back a ILLEGAL_FUNCTION error
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION);
}
// Else we will ignore the request, as it is not meant for us and we do not deal with broadcasts!
}
// Do we have gathered a valid response now?
if (response.size() >= 3) {
// Yes. send it back.
RTUutils::send(*(myServer->MSRserial), myServer->MSRlastMicros, myServer->MSRinterval, myServer->MRTSrts, response, myServer->MSRuseASCII);
LOG_D("Response sent.\n");
// Count it, in case we had an error response
if (response.getError() != SUCCESS) {
LOCK_GUARD(errorCntLock, myServer->m);
myServer->errorCount++;
}
}
}
} else {
// No, we got a 1-byte request, meaning an error has happened in receive()
// This is a server, so we will ignore TIMEOUT.
if (request[0] != TIMEOUT) {
// Any other error could be important for debugging, so print it
ModbusError me((Error)request[0]);
LOG_E("RTU receive: %02X - %s\n", (int)me, (const char *)me);
}
}
// Give scheduler room to breathe
delay(1);
}
}
#endif

View File

@@ -0,0 +1,92 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_RTU_H
#define _MODBUS_SERVER_RTU_H
#include "options.h"
#if HAS_FREERTOS
#include <Arduino.h>
#include "Stream.h"
#include "ModbusServer.h"
#include "RTUutils.h"
extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
}
// Specal function signature for broadcast or sniffer listeners
using MSRlistener = std::function<void(ModbusMessage msg)>;
class ModbusServerRTU : public ModbusServer {
public:
// Constructors
explicit ModbusServerRTU(uint32_t timeout, int rtsPin = -1);
ModbusServerRTU(uint32_t timeout, RTScallback rts);
// Destructor
~ModbusServerRTU();
// begin: create task with RTU server to accept requests
void begin(Stream& serial, uint32_t baudRate, int coreID = -1);
void begin(HardwareSerial& serial, int coreID = -1);
// end: kill server task
void end();
// Toggle protocol to ModbusASCII
void useModbusASCII(unsigned long timeout = 1000);
// Toggle protocol to ModbusRTU
void useModbusRTU();
// Inquire protocol mode
bool isModbusASCII();
// Toggle skipping of leading 0x00 byte
void skipLeading0x00(bool onOff = true);
// Special case: worker to react on broadcast requests
void registerBroadcastWorker(MSRlistener worker);
// Even more special: register a sniffer worker
void registerSniffer(MSRlistener worker);
protected:
// Prevent copy construction and assignment
ModbusServerRTU(ModbusServerRTU& m) = delete;
ModbusServerRTU& operator=(ModbusServerRTU& m) = delete;
inline void isInstance() { } // Make class instantiable
// internal common begin function
void doBegin(uint32_t baudRate, int coreID);
static uint8_t instanceCounter; // Number of RTU servers created (for task names)
TaskHandle_t serverTask; // task of the started server
uint32_t serverTimeout; // given timeout for receive. Does not really
// matter for a server, but is needed in
// RTUutils. After timeout without any message
// the server will pause ~1ms and start
// receive again.
Stream *MSRserial; // The serial interface to use
uint32_t MSRinterval; // Bus quiet time between messages
unsigned long MSRlastMicros; // microsecond time stamp of last bus activity
int8_t MSRrtsPin; // GPIO number of the RS485 module's RE/DE line
RTScallback MRTSrts; // Callback to set the RTS line to HIGH/LOW
bool MSRuseASCII; // true=ModbusASCII, false=ModbusRTU
bool MSRskipLeadingZeroByte; // true=first byte ignored if 0x00, false=all bytes accepted
MSRlistener listener; // Broadcast listener
MSRlistener sniffer; // Sniffer listener
// serve: loop function for server task
static void serve(ModbusServerRTU *myself);
};
#endif // HAS_FREERTOS
#endif // INCLUDE GUARD

View File

@@ -0,0 +1,267 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusServerTCPasync.h"
#define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
// #undef LOCAL_LOG_LEVEL
#include "Logging.h"
ModbusServerTCPasync::mb_client::mb_client(ModbusServerTCPasync * s, AsyncClient * c)
: server(s)
, client(c)
, lastActiveTime(millis())
, message(nullptr)
, error(SUCCESS)
, outbox() {
client->onData([](void * i, AsyncClient * c, void * data, size_t len) { (static_cast<mb_client *>(i))->onData(static_cast<uint8_t *>(data), len); }, this);
client->onPoll([](void * i, AsyncClient * c) { (static_cast<mb_client *>(i))->onPoll(); }, this);
client->onDisconnect([](void * i, AsyncClient * c) { (static_cast<mb_client *>(i))->onDisconnect(); }, this);
client->setNoDelay(true);
}
ModbusServerTCPasync::mb_client::~mb_client() {
// clear outbox, if data is left
while (!outbox.empty()) {
outbox.pop();
}
delete client; // will also close connection, if any
}
void ModbusServerTCPasync::mb_client::onData(uint8_t * data, size_t len) {
lastActiveTime = millis();
LOG_D("data len %d\n", len);
Error error = SUCCESS;
size_t i = 0;
while (i < len) {
// 0. start
if (!message) {
message = new ModbusMessage(8);
error = SUCCESS;
}
// 1. get minimal 8 bytes to move on
while (message->size() < 8 && i < len) {
message->push_back(data[i++]);
}
// 2. preliminary validation: protocol bytes and message length
if ((*message)[2] != 0 || (*message)[3] != 0) {
error = TCP_HEAD_MISMATCH;
LOG_D("invalid protocol\n");
}
size_t messageLength = (((*message)[4] << 8) | (*message)[5]) + 6;
if (messageLength > 262) { // 256 + MBAP(6) = 262
error = PACKET_LENGTH_ERROR;
LOG_D("max length error\n");
}
if (error != SUCCESS) {
ModbusMessage response;
response.setError(message->getServerID(), message->getFunctionCode(), error);
message->resize(4);
message->add(static_cast<uint16_t>(3));
message->append(response);
addResponseToOutbox(message); // outbox has pointer ownership now
// reset to starting values and process remaining data
message = nullptr;
return; // protocol validation, abort further parsing
}
// 3. receive until request is complete
while (message->size() < messageLength && i < len) {
message->push_back(data[i++]);
}
if (message->size() == messageLength) {
LOG_D("request complete (len:%d)\n", message->size());
} else {
LOG_D("request incomplete (len:%d), waiting for next TCP packet\n", message->size());
continue;
}
// 4. request complete, process
ModbusMessage request(messageLength - 6); // create request without MBAP, with server ID
request.add(message->data() + 6, message->size() - 6);
ModbusMessage userData;
if (server->isServerFor(request.getServerID())) {
MBSworker callback = server->getWorker(request.getServerID(), request.getFunctionCode());
if (callback) {
// request is well formed and is being served by user API
userData = callback(request);
// Process Response
// One of the predefined types?
if (userData[0] == 0xFF && (userData[1] == 0xF0 || userData[1] == 0xF1)) {
// Yes. Check it
switch (userData[1]) {
case 0xF0: // NIL
userData.clear();
LOG_D("NIL response\n");
break;
case 0xF1: // ECHO
userData = request;
if (request.getFunctionCode() == WRITE_MULT_REGISTERS || request.getFunctionCode() == WRITE_MULT_COILS) {
userData.resize(6);
}
LOG_D("ECHO response\n");
break;
default: // Will not get here!
break;
}
} else {
// No. User provided data response
LOG_D("Data response\n");
}
error = SUCCESS;
} else { // no worker found
error = ILLEGAL_FUNCTION;
}
} else { // mismatch server ID
error = INVALID_SERVER;
}
if (error != SUCCESS) {
userData.setError(request.getServerID(), request.getFunctionCode(), error);
}
// Keep transaction id and protocol id
message->resize(4);
// Add new payload length
message->add(static_cast<uint16_t>(userData.size()));
// Append payload
message->append(userData);
// Transfer message data to outbox
addResponseToOutbox(message);
message = nullptr;
} // end while loop iterating incoming data
}
void ModbusServerTCPasync::mb_client::onPoll() {
LOCK_GUARD(lock1, obLock);
handleOutbox();
if (server->idle_timeout > 0 && millis() - lastActiveTime > server->idle_timeout) {
LOG_D("client idle, closing\n");
client->close();
}
}
void ModbusServerTCPasync::mb_client::onDisconnect() {
LOG_D("client disconnected\n");
server->onClientDisconnect(this);
}
void ModbusServerTCPasync::mb_client::addResponseToOutbox(ModbusMessage * response) {
if (response->size() > 0) {
LOCK_GUARD(lock1, obLock);
outbox.push(response);
handleOutbox();
}
}
void ModbusServerTCPasync::mb_client::handleOutbox() {
while (!outbox.empty()) {
ModbusMessage * m = outbox.front();
if (m->size() <= client->space()) {
LOG_D("sending (%d)\n", m->size());
client->add(reinterpret_cast<const char *>(m->data()), m->size(), ASYNC_WRITE_FLAG_COPY);
client->send();
delete m;
outbox.pop();
} else {
return;
}
}
}
ModbusServerTCPasync::ModbusServerTCPasync()
: server(nullptr)
, clients()
, maxNoClients(5)
, idle_timeout(60000) {
// setup will be done in 'start'
}
ModbusServerTCPasync::~ModbusServerTCPasync() {
stop();
delete server;
}
uint16_t ModbusServerTCPasync::activeClients() {
LOCK_GUARD(lock1, cListLock);
return clients.size();
}
bool ModbusServerTCPasync::start(uint16_t port, uint8_t max_clients, uint32_t timeout, int coreID) {
// don't restart if already running
if (server) {
LOG_W("Server already running.\n");
return false;
}
maxNoClients = max_clients;
idle_timeout = timeout;
server = new AsyncServer(port);
if (server) {
server->setNoDelay(true);
server->onClient([](void * i, AsyncClient * c) { (static_cast<ModbusServerTCPasync *>(i))->onClientConnect(c); }, this);
server->begin();
LOG_D("Modbus server started\n");
return true;
}
LOG_E("Could not start server\n");
return false;
}
bool ModbusServerTCPasync::stop() {
if (!server) {
LOG_W("Server not running.\n");
return false;
}
// stop server to prevent new clients connecting
server->end();
// now close existing clients
LOCK_GUARD(lock1, cListLock);
while (!clients.empty()) {
// prevent onDisconnect handler to be called, resulting in deadlock
clients.front()->client->onDisconnect(nullptr, nullptr);
delete clients.front();
clients.pop_front();
}
delete server;
server = nullptr;
LOG_D("Modbus server stopped\n");
return true;
}
bool ModbusServerTCPasync::isRunning() {
if (server)
return true;
else
return false;
}
void ModbusServerTCPasync::onClientConnect(AsyncClient * client) {
LOG_D("new client\n");
LOCK_GUARD(lock1, cListLock);
if (clients.size() < maxNoClients) {
clients.emplace_back(new mb_client(this, client));
LOG_D("nr clients: %d\n", clients.size());
} else {
LOG_D("max number of clients reached, closing new\n");
client->close(true);
delete client;
}
}
void ModbusServerTCPasync::onClientDisconnect(mb_client * client) {
LOCK_GUARD(lock1, cListLock);
// delete mb_client from list
clients.remove_if([client](mb_client * i) { return i->client == client->client; });
// delete client itself
delete client;
LOG_D("nr clients: %d\n", clients.size());
}

View File

@@ -0,0 +1,92 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_TCP_ASYNC_H
#define _MODBUS_SERVER_TCP_ASYNC_H
#include "options.h"
#include <list>
#include <queue>
#if USE_MUTEX
#include <mutex> // NOLINT
#endif
#include <vector>
#include <Arduino.h> // for millis()
#if defined(ESP32)
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESPAsyncTCP.h>
#endif
#include "ModbusServer.h"
#if USE_MUTEX
using std::lock_guard;
#endif
class ModbusServerTCPasync : public ModbusServer {
private:
class mb_client {
friend class ModbusServerTCPasync;
public:
mb_client(ModbusServerTCPasync * s, AsyncClient * c);
~mb_client();
private:
void onData(uint8_t * data, size_t len);
void onPoll();
void onDisconnect();
void addResponseToOutbox(ModbusMessage * response);
void handleOutbox();
ModbusServerTCPasync * server;
AsyncClient * client;
uint32_t lastActiveTime;
ModbusMessage * message;
Modbus::Error error;
std::queue<ModbusMessage *> outbox;
#if USE_MUTEX
std::mutex obLock; // outbox protection
#endif
};
public:
// Constructor
ModbusServerTCPasync();
// Destructor: closes the connections
~ModbusServerTCPasync();
// activeClients: return number of clients currently employed
uint16_t activeClients();
// start: create task with TCP server to accept requests
bool start(uint16_t port, uint8_t max_clients, uint32_t timeout, int coreID = -1);
// stop: drop all connections and kill server task
bool stop();
// isRunning: return true is server is running
bool isRunning();
protected:
inline void isInstance() {
}
void onClientConnect(AsyncClient * client);
void onClientDisconnect(mb_client * client);
AsyncServer * server;
std::list<mb_client *> clients;
uint8_t maxNoClients;
uint32_t idle_timeout;
#if USE_MUTEX
std::mutex cListLock; // client list protection
#endif
};
#endif

View File

@@ -0,0 +1,433 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_TCP_TEMP_H
#define _MODBUS_SERVER_TCP_TEMP_H
#include <Arduino.h>
#include <mutex> // NOLINT
#include "ModbusServer.h"
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
}
using std::lock_guard;
using std::mutex;
using std::vector;
template <typename ST, typename CT>
class ModbusServerTCP : public ModbusServer {
public:
// Constructor
ModbusServerTCP();
// Destructor: closes the connections
~ModbusServerTCP();
// activeClients: return number of clients currently employed
uint16_t activeClients();
// start: create task with TCP server to accept requests
bool start(uint16_t port, uint8_t max_clients, uint32_t timeout, int coreID = -1);
// stop: drop all connections and kill server task
bool stop();
protected:
// Prevent copy construction and assignment
ModbusServerTCP(ModbusServerTCP & m) = delete;
ModbusServerTCP & operator=(ModbusServerTCP & m) = delete;
inline void isInstance() {
}
uint8_t numClients;
TaskHandle_t serverTask;
uint16_t serverPort;
uint32_t serverTimeout;
bool serverGoDown;
mutex clientLock;
struct ClientData {
ClientData()
: task(nullptr)
, client(0)
, timeout(0)
, parent(nullptr) {
}
ClientData(TaskHandle_t t, CT & c, uint32_t to, ModbusServerTCP<ST, CT> * p)
: task(t)
, client(c)
, timeout(to)
, parent(p) {
}
~ClientData() {
if (client) {
client.stop();
}
if (task != nullptr) {
vTaskDelete(task);
LOG_D("Killed client task %d\n", (uint32_t)task);
}
}
TaskHandle_t task;
CT client;
uint32_t timeout;
ModbusServerTCP<ST, CT> * parent;
};
ClientData ** clients;
// serve: loop function for server task
static void serve(ModbusServerTCP<ST, CT> * myself);
// worker: loop function for client tasks
static void worker(ClientData * myData);
// receive: read data from TCP
ModbusMessage receive(CT & client, uint32_t timeWait);
// accept: start a task to receive requests and respond to a given client
bool accept(CT & client, uint32_t timeout, int coreID = -1);
// clientAvailable: return true,. if a client slot is currently unused
bool clientAvailable() {
return (numClients - activeClients()) > 0;
}
};
// Constructor
template <typename ST, typename CT>
ModbusServerTCP<ST, CT>::ModbusServerTCP()
: ModbusServer()
, numClients(0)
, serverTask(nullptr)
, serverPort(502)
, serverTimeout(20000)
, serverGoDown(false) {
clients = new ClientData *[numClients]();
}
// Destructor: closes the connections
template <typename ST, typename CT>
ModbusServerTCP<ST, CT>::~ModbusServerTCP() {
for (uint8_t i = 0; i < numClients; ++i) {
if (clients[i] != nullptr) {
delete clients[i];
}
}
delete[] clients;
serverGoDown = true;
}
// activeClients: return number of clients currently employed
template <typename ST, typename CT>
uint16_t ModbusServerTCP<ST, CT>::activeClients() {
uint8_t cnt = 0;
for (uint8_t i = 0; i < numClients; ++i) {
// Current slot could have been previously used - look for cleared task handles
if (clients[i] != nullptr) {
// Empty task handle?
if (clients[i]->task == nullptr) {
// Yes. Delete entry and init client pointer
lock_guard<mutex> cL(clientLock);
delete clients[i];
LOG_V("Delete client %d\n", i);
clients[i] = nullptr;
}
}
if (clients[i] != nullptr)
cnt++;
}
return cnt;
}
// start: create task with TCP server to accept requests
template <typename ST, typename CT>
bool ModbusServerTCP<ST, CT>::start(uint16_t port, uint8_t max_clients, uint32_t timeout, int coreID) {
// Task already running?
if (serverTask != nullptr) {
// Yes. stop it first
stop();
}
// Does the required number of slots fit?
if (numClients != max_clients) {
// No. Drop array and allocate a new one
delete[] clients;
// Now allocate a new one
numClients = max_clients;
clients = new ClientData *[numClients]();
}
serverPort = port;
serverTimeout = timeout;
serverGoDown = false;
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "MBserve%04X", port);
// Start task to handle the client
xTaskCreatePinnedToCore((TaskFunction_t)&serve, taskName, SERVER_TASK_STACK, this, 5, &serverTask, coreID >= 0 ? coreID : NULL);
LOG_D("Server task %s started (%d).\n", taskName, (uint32_t)serverTask);
// Wait two seconds for it to establish
delay(2000);
return true;
}
// stop: drop all connections and kill server task
template <typename ST, typename CT>
bool ModbusServerTCP<ST, CT>::stop() {
// Check for clients still connected
for (uint8_t i = 0; i < numClients; ++i) {
// Client is alive?
if (clients[i] != nullptr) {
// Yes. Close the connection
delete clients[i];
clients[i] = nullptr;
}
}
if (serverTask != nullptr) {
// Signal server task to stop
serverGoDown = true;
delay(5000);
LOG_D("Killed server task %d\n", (uint32_t)(serverTask));
serverTask = nullptr;
serverGoDown = false;
}
return true;
}
// accept: start a task to receive requests and respond to a given client
template <typename ST, typename CT>
bool ModbusServerTCP<ST, CT>::accept(CT & client, uint32_t timeout, int coreID) {
// Look for an empty client slot
for (uint8_t i = 0; i < numClients; ++i) {
// Empty slot?
if (clients[i] == nullptr) {
// Yes. allocate new client data in slot
clients[i] = new ClientData(0, client, timeout, this);
// Create unique task name
char taskName[18];
snprintf(taskName, 18, "MBsrv%02Xclnt", i);
// Start task to handle the client
xTaskCreatePinnedToCore((TaskFunction_t)&worker, taskName, SERVER_TASK_STACK, clients[i], 5, &clients[i]->task, coreID >= 0 ? coreID : NULL);
LOG_D("Started client %d task %d\n", i, (uint32_t)(clients[i]->task));
return true;
}
}
LOG_D("No client slot available.\n");
return false;
}
template <typename ST, typename CT>
void ModbusServerTCP<ST, CT>::serve(ModbusServerTCP<ST, CT> * myself) {
// need a local scope here to delete the server at termination time
if (1) {
// Set up server with given port
ST server(myself->serverPort);
// Start it
server.begin();
// Loop until being killed
while (!myself->serverGoDown) {
// Do we have clients left to use?
if (myself->clientAvailable()) {
// Yes. accept one, when it connects
CT ec = server.accept();
// Did we get a connection?
if (ec) {
// Yes. Forward it to the Modbus server
myself->accept(ec, myself->serverTimeout, 0);
LOG_D("Accepted connection - %d clients running\n", myself->activeClients());
}
}
// Give scheduler room to breathe
delay(10);
}
LOG_E("Server going down\n");
// We must go down
SERVER_END;
}
vTaskDelete(NULL);
}
template <typename ST, typename CT>
void ModbusServerTCP<ST, CT>::worker(ClientData * myData) {
// Get own reference data in handier form
CT myClient = myData->client;
uint32_t myTimeOut = myData->timeout;
// TaskHandle_t myTask = myData->task;
ModbusServerTCP<ST, CT> * myParent = myData->parent;
unsigned long myLastMessage = millis();
LOG_D("Worker started, timeout=%d\n", myTimeOut);
// loop forever, if timeout is 0, or until timeout was hit
while (myClient.connected() && (!myTimeOut || (millis() - myLastMessage < myTimeOut))) {
ModbusMessage response; // Data buffer to hold prepared response
// Get a request
if (myClient.available()) {
response.clear();
ModbusMessage m = myParent->receive(myClient, 100);
// has it the minimal length (6 bytes TCP header plus serverID plus FC)?
if (m.size() >= 8) {
{
LOCK_GUARD(cntLock, myParent->m);
myParent->messageCount++;
}
// Extract request data
ModbusMessage request;
request.add(m.data() + 6, m.size() - 6);
// Protocol ID shall be 0x0000 - is it?
if (m[2] == 0 && m[3] == 0) {
// ServerID shall be at [6], FC at [7]. Check both
if (myParent->isServerFor(request.getServerID())) {
// Server is correct - in principle. Do we serve the FC?
MBSworker callBack = myParent->getWorker(request.getServerID(), request.getFunctionCode());
if (callBack) {
// Yes, we do.
// Invoke the worker method to get a response
ModbusMessage data = callBack(request);
// Process Response
// One of the predefined types?
if (data[0] == 0xFF && (data[1] == 0xF0 || data[1] == 0xF1)) {
// Yes. Check it
switch (data[1]) {
case 0xF0: // NIL
response.clear();
LOG_D("NIL response\n");
break;
case 0xF1: // ECHO
response = request;
if (request.getFunctionCode() == WRITE_MULT_REGISTERS || request.getFunctionCode() == WRITE_MULT_COILS) {
response.resize(6);
}
LOG_D("ECHO response\n");
break;
default: // Will not get here!
break;
}
} else {
// No. User provided data response
response = data;
LOG_D("Data response\n");
}
} else {
// No, function code is not served here
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION);
}
} else {
// No, serverID is not served here
response.setError(request.getServerID(), request.getFunctionCode(), INVALID_SERVER);
}
} else {
// No, protocol ID was something weird
response.setError(request.getServerID(), request.getFunctionCode(), TCP_HEAD_MISMATCH);
}
}
delay(1);
// Do we have a response to send?
if (response.size() >= 3) {
// Yes. Do it now.
// Cut off length and request data, then update TCP header
m.resize(4);
m.add(static_cast<uint16_t>(response.size()));
// Append response
m.append(response);
myClient.write(m.data(), m.size());
HEXDUMP_V("Response", m.data(), m.size());
// count error responses
if (response.getError() != SUCCESS) {
LOCK_GUARD(cntLock, myParent->m);
myParent->errorCount++;
}
}
// We did something communicationally - rewind timeout timer
myLastMessage = millis();
}
delay(1);
}
if (millis() - myLastMessage >= myTimeOut) {
// Timeout!
LOG_D("Worker stopping due to timeout.\n");
} else {
// Disconnected!
LOG_D("Worker stopping due to client disconnect.\n");
}
// Read away all that may still hang in the buffer
while (myClient.read() != -1) {
}
// Now stop the client
myClient.stop();
{
lock_guard<mutex> cL(myParent->clientLock);
myData->task = nullptr;
}
delay(50);
vTaskDelete(NULL);
}
// receive: get request via Client connection
template <typename ST, typename CT>
ModbusMessage ModbusServerTCP<ST, CT>::receive(CT & client, uint32_t timeWait) {
unsigned long lastMillis = millis(); // Timer to check for timeout
ModbusMessage m; // to take read data
uint16_t lengthVal = 0;
uint16_t cnt = 0;
const uint16_t BUFFERSIZE(300);
uint8_t buffer[BUFFERSIZE];
// wait for sufficient packet data or timeout
while ((millis() - lastMillis < timeWait) && ((cnt < 6) || (cnt < lengthVal)) && (cnt < BUFFERSIZE)) {
// Is there data waiting?
if (client.available()) {
buffer[cnt] = client.read();
// Are we at the TCP header length field byte #1?
if (cnt == 4)
lengthVal = buffer[cnt] << 8;
// Are we at the TCP header length field byte #2?
if (cnt == 5) {
lengthVal |= buffer[cnt];
lengthVal += 6;
}
cnt++;
// Rewind EOT and timeout timers
lastMillis = millis();
} else {
delay(1); // Give scheduler room to breathe
}
}
// Did we receive some data?
if (cnt) {
// Yes. Is it too much?
if (cnt >= BUFFERSIZE) {
// Yes, likely a buffer overflow of some sort
// Adjust message size in TCP header
buffer[4] = (cnt >> 8) & 0xFF;
buffer[5] = cnt & 0xFF;
LOG_E("Potential buffer overrun (>%d)!\n", cnt);
}
// Get as much buffer as was read
m.add(buffer, cnt);
}
return m;
}
#endif

View File

@@ -0,0 +1,16 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_SERVER_WIFI_H
#define _MODBUS_SERVER_WIFI_H
#include "options.h"
#include <WiFi.h>
#undef SERVER_END
#define SERVER_END server.end();
#include "ModbusServerTCPtemp.h"
using ModbusServerWiFi = ModbusServerTCP<WiFiServer, WiFiClient>;
#endif

View File

@@ -0,0 +1,66 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "ModbusTypeDefs.h"
#ifndef MINIMAL
using Modbus::FCType;
using Modbus::FCT;
// Initialize function code type table
FCType FCT::table[] = {
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FC01_TYPE, FC01_TYPE, FC01_TYPE, FC01_TYPE, FC01_TYPE, FC01_TYPE, FC07_TYPE, // 0x0.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCGENERIC, FCILLEGAL, FCILLEGAL, FC07_TYPE, FC07_TYPE, FCILLEGAL, FCILLEGAL, FC0F_TYPE, // 0x0.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FC10_TYPE, FC07_TYPE, FCILLEGAL, FCILLEGAL, FCGENERIC, FCGENERIC, FC16_TYPE, FCGENERIC, // 0x1.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FC18_TYPE, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x1.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x2.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCGENERIC, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x2.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x3.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x3.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, // 0x4.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCUSER, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x4.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x5.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x5.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCUSER, FCUSER, FCUSER, FCUSER, // 0x6.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, FCUSER, FCILLEGAL, // 0x6.
// 0x.0 0x.1 0x.2 0x.3 0x.4 0x.5 0x.6 0x.7
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x7.
// 0x.8 0x.9 0x.A 0x.B 0x.C 0x.D 0x.E 0x.F
FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, FCILLEGAL, // 0x7.
};
// FCT::getType: get the function code type for a given function code
FCType FCT::getType(uint8_t functionCode) {
return table[functionCode & 0x7F];
}
// setType: change the type of a function code.
// This is possible only for the codes undefined yet and will return
// the effective type
FCType FCT::redefineType(uint8_t functionCode, const FCType type) {
uint8_t fc = functionCode & 0x7F;
// Allow modifications for yet undefined codes only
if (table[fc] == FCILLEGAL) {
table[fc] = type;
}
return table[fc];
}
#endif

View File

@@ -0,0 +1,139 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _MODBUS_TYPEDEFS_H
#define _MODBUS_TYPEDEFS_H
#include <stdint.h>
#include <stddef.h>
#include <cstdint>
namespace Modbus {
enum FunctionCode : uint8_t {
ANY_FUNCTION_CODE = 0x00, // Only valid for server to register function codes
READ_COIL = 0x01,
READ_DISCR_INPUT = 0x02,
READ_HOLD_REGISTER = 0x03,
READ_INPUT_REGISTER = 0x04,
WRITE_COIL = 0x05,
WRITE_HOLD_REGISTER = 0x06,
READ_EXCEPTION_SERIAL = 0x07,
DIAGNOSTICS_SERIAL = 0x08,
READ_COMM_CNT_SERIAL = 0x0B,
READ_COMM_LOG_SERIAL = 0x0C,
WRITE_MULT_COILS = 0x0F,
WRITE_MULT_REGISTERS = 0x10,
REPORT_SERVER_ID_SERIAL = 0x11,
READ_FILE_RECORD = 0x14,
WRITE_FILE_RECORD = 0x15,
MASK_WRITE_REGISTER = 0x16,
R_W_MULT_REGISTERS = 0x17,
READ_FIFO_QUEUE = 0x18,
ENCAPSULATED_INTERFACE = 0x2B,
USER_DEFINED_41 = 0x41,
USER_DEFINED_42 = 0x42,
USER_DEFINED_43 = 0x43,
USER_DEFINED_44 = 0x44,
USER_DEFINED_45 = 0x45,
USER_DEFINED_46 = 0x46,
USER_DEFINED_47 = 0x47,
USER_DEFINED_48 = 0x48,
USER_DEFINED_64 = 0x64,
USER_DEFINED_65 = 0x65,
USER_DEFINED_66 = 0x66,
USER_DEFINED_67 = 0x67,
USER_DEFINED_68 = 0x68,
USER_DEFINED_69 = 0x69,
USER_DEFINED_6A = 0x6A,
USER_DEFINED_6B = 0x6B,
USER_DEFINED_6C = 0x6C,
USER_DEFINED_6D = 0x6D,
USER_DEFINED_6E = 0x6E,
};
enum Error : uint8_t {
SUCCESS = 0x00,
ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDRESS = 0x02,
ILLEGAL_DATA_VALUE = 0x03,
SERVER_DEVICE_FAILURE = 0x04,
ACKNOWLEDGE = 0x05,
SERVER_DEVICE_BUSY = 0x06,
NEGATIVE_ACKNOWLEDGE = 0x07,
MEMORY_PARITY_ERROR = 0x08,
GATEWAY_PATH_UNAVAIL = 0x0A,
GATEWAY_TARGET_NO_RESP = 0x0B,
TIMEOUT = 0xE0,
INVALID_SERVER = 0xE1,
CRC_ERROR = 0xE2, // only for Modbus-RTU
FC_MISMATCH = 0xE3,
SERVER_ID_MISMATCH = 0xE4,
PACKET_LENGTH_ERROR = 0xE5,
PARAMETER_COUNT_ERROR = 0xE6,
PARAMETER_LIMIT_ERROR = 0xE7,
REQUEST_QUEUE_FULL = 0xE8,
ILLEGAL_IP_OR_PORT = 0xE9,
IP_CONNECTION_FAILED = 0xEA,
TCP_HEAD_MISMATCH = 0xEB,
EMPTY_MESSAGE = 0xEC,
ASCII_FRAME_ERR = 0xED,
ASCII_CRC_ERR = 0xEE,
ASCII_INVALID_CHAR = 0xEF,
BROADCAST_ERROR = 0xF0,
UNDEFINED_ERROR = 0xFF // otherwise uncovered communication error
};
#ifndef MINIMAL
// Constants for float and double re-ordering
#define SWAP_BYTES 0x01
#define SWAP_REGISTERS 0x02
#define SWAP_WORDS 0x04
#define SWAP_NIBBLES 0x08
const uint8_t swapTables[8][8] = {
{ 0, 1, 2, 3, 4, 5, 6, 7 }, // no swap
{ 1, 0, 3, 2, 5, 4, 7, 6 }, // bytes only
{ 2, 3, 0, 1, 6, 7, 4, 5 }, // registers only
{ 3, 2, 1, 0, 7, 6, 5, 4 }, // registers and bytes
{ 4, 5, 6, 7, 0, 1, 2, 3 }, // words only (double)
{ 5, 4, 7, 6, 1, 0, 3, 2 }, // words and bytes (double)
{ 6, 7, 4, 5, 2, 3, 0, 1 }, // words and registers (double)
{ 7, 6, 5, 4, 3, 2, 1, 0 } // Words, registers and bytes (double)
};
enum FCType : uint8_t {
FC01_TYPE, // Two uint16_t parameters (FC 0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
FC07_TYPE, // no additional parameter (FCs 0x07, 0x0b, 0x0c, 0x11)
FC0F_TYPE, // two uint16_t parameters, a uint8_t length byte and a uint16_t* pointer to array of bytes (FC 0x0f)
FC10_TYPE, // two uint16_t parameters, a uint8_t length byte and a uint8_t* pointer to array of words (FC 0x10)
FC16_TYPE, // three uint16_t parameters (FC 0x16)
FC18_TYPE, // one uint16_t parameter (FC 0x18)
FCGENERIC, // for FCs not yet explicitly coded (or too complex)
FCUSER, // No checks except the server ID
FCILLEGAL, // not allowed function codes
};
// FCT: static class to hold the types of function codes
class FCT {
protected:
static FCType table[128]; // data table
FCT() = delete; // No instances allowed
FCT(const FCT&) = delete; // No copy constructor
FCT& operator=(const FCT& other) = delete; // No assignment either
public:
// getType: get the function code type for a given function code
static FCType getType(uint8_t functionCode);
// setType: change the type of a function code.
// This is possible only for the codes undefined yet and will return
// the effective type
static FCType redefineType(uint8_t functionCode, const FCType type = FCUSER);
};
#endif
} // namespace Modbus
#endif

View File

@@ -0,0 +1,467 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#include "options.h"
#include "ModbusMessage.h"
#include "RTUutils.h"
#undef LOCAL_LOG_LEVEL
// #define LOCAL_LOG_LEVEL LOG_LEVEL_VERBOSE
#include "Logging.h"
#if HAS_FREERTOS
// calcCRC: calculate Modbus CRC16 on a given array of bytes
uint16_t RTUutils::calcCRC(const uint8_t *data, uint16_t len) {
// CRC16 pre-calculated tables
const uint8_t crcHiTable[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40
};
const uint8_t crcLoTable[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40
};
uint8_t crcHi = 0xFF;
uint8_t crcLo = 0xFF;
uint8_t index;
while (len--) {
index = crcLo ^ *data++;
crcLo = crcHi ^ crcHiTable[index];
crcHi = crcLoTable[index];
}
return (crcHi << 8 | crcLo);
}
// calcCRC: calculate Modbus CRC16 on a given message
uint16_t RTUutils::calcCRC(ModbusMessage msg) {
return calcCRC(msg.data(), msg.size());
}
// validCRC #1: check the given CRC in a block of data for correctness
bool RTUutils::validCRC(const uint8_t *data, uint16_t len) {
return validCRC(data, len - 2, data[len - 2] | (data[len - 1] << 8));
}
// validCRC #2: check the CRC of a block of data against a given one for equality
bool RTUutils::validCRC(const uint8_t *data, uint16_t len, uint16_t CRC) {
uint16_t crc16 = calcCRC(data, len);
if (CRC == crc16) return true;
return false;
}
// validCRC #3: check the given CRC in a message for correctness
bool RTUutils::validCRC(ModbusMessage msg) {
return validCRC(msg.data(), msg.size() - 2, msg[msg.size() - 2] | (msg[msg.size() - 1] << 8));
}
// validCRC #4: check the CRC of a message against a given one for equality
bool RTUutils::validCRC(ModbusMessage msg, uint16_t CRC) {
return validCRC(msg.data(), msg.size(), CRC);
}
// addCRC: calculate the CRC for a given RTUMessage and add it to the end
void RTUutils::addCRC(ModbusMessage& raw) {
uint16_t crc16 = calcCRC(raw.data(), raw.size());
raw.push_back(crc16 & 0xff);
raw.push_back((crc16 >> 8) & 0xFF);
}
// calculateInterval: determine the minimal gap time between messages
uint32_t RTUutils::calculateInterval(uint32_t baudRate) {
uint32_t interval = 0;
// silent interval is at least 3.5x character time
interval = 35000000UL / baudRate; // 3.5 * 10 bits * 1000 µs * 1000 ms / baud
if (interval < 1750) interval = 1750; // lower limit according to Modbus RTU standard
LOG_V("Calc interval(%u)=%u\n", baudRate, interval);
return interval;
}
// send: send a message via Serial, watching interval times - including CRC!
void RTUutils::send(Stream& serial, unsigned long& lastMicros, uint32_t interval, RTScallback rts, const uint8_t *data, uint16_t len, bool ASCIImode) {
// Clear serial buffers
while (serial.available()) serial.read();
// Treat ASCII differently
if (ASCIImode) {
// Toggle rtsPin, if necessary
rts(HIGH);
// Yes, ASCII mode. Send lead-in
serial.write(':');
uint16_t cnt = len;
uint8_t crc = 0;
uint8_t *cp = (uint8_t *)data;
// Loop over all bytes of the message
while (cnt--) {
// Write two nibbles as ASCII characters
serial.write(ASCIIwrite[(*cp >> 4) & 0x0F]);
serial.write(ASCIIwrite[*cp & 0x0F]);
// Advance CRC
crc += *cp;
// Next byte
cp++;
}
// Finalize CRC (2's complement)
crc = ~crc;
crc++;
// Write ist - two nibbles as ASCII characters
serial.write(ASCIIwrite[(crc >> 4) & 0x0F]);
serial.write(ASCIIwrite[crc & 0x0F]);
// Send lead-out
serial.write("\r\n");
serial.flush();
// Toggle rtsPin, if necessary
rts(LOW);
} else {
// RTU mode
uint16_t crc16 = calcCRC(data, len);
// Respect interval - we must not toggle rtsPin before
if (micros() - lastMicros < interval) delayMicroseconds(interval - (micros() - lastMicros));
// Toggle rtsPin, if necessary
rts(HIGH);
// Write message
serial.write(data, len);
// Write CRC in LSB order
serial.write(crc16 & 0xff);
serial.write((crc16 >> 8) & 0xFF);
serial.flush();
// Toggle rtsPin, if necessary
rts(LOW);
}
HEXDUMP_D("Sent packet", data, len);
// Mark end-of-message time for next interval
lastMicros = micros();
}
// send: send a message via Serial, watching interval times - including CRC!
void RTUutils::send(Stream& serial, unsigned long& lastMicros, uint32_t interval, RTScallback rts, ModbusMessage raw, bool ASCIImode) {
send(serial, lastMicros, interval, rts, raw.data(), raw.size(), ASCIImode);
}
// receive: get (any) message from Serial, taking care of timeout and interval
ModbusMessage RTUutils::receive(uint8_t caller, Stream& serial, uint32_t timeout, unsigned long& lastMicros, uint32_t interval, bool ASCIImode, bool skipLeadingZeroBytes) {
// Allocate initial receive buffer size: 1 block of BUFBLOCKSIZE bytes
const uint16_t BUFBLOCKSIZE(512);
uint8_t *buffer = new uint8_t[BUFBLOCKSIZE];
ModbusMessage rv;
// Index into buffer
uint16_t bufferPtr = 0;
// Byte read
int b = 0;
// State machine states, RTU mode
enum STATES : uint8_t { WAIT_DATA = 0, IN_PACKET, DATA_READ, FINISHED };
// State machine states, ASCII mode
enum ASTATES : uint8_t { A_WAIT_DATA = 0, A_DATA, A_WAIT_LEAD_OUT, A_FINISHED };
uint8_t state;
// Timeout tracker
unsigned long TimeOut = millis();
// RTU mode?
if (!ASCIImode) {
// Yes.
state = WAIT_DATA;
// interval tracker
lastMicros = micros();
while (state != FINISHED) {
switch (state) {
// WAIT_DATA: await first data byte, but watch timeout
case WAIT_DATA:
// Blindly try to read a byte
b = serial.read();
// Did we get one?
if (b >= 0) {
// Yes. Note the time.
lastMicros = micros();
// Do we need to skip it, if it is zero?
if (b > 0 || !skipLeadingZeroBytes) {
// No, we can go process it regularly
buffer[bufferPtr++] = b;
state = IN_PACKET;
}
} else {
// No, we had no byte. Just check the timeout period
if (millis() - TimeOut >= timeout) {
rv.push_back(TIMEOUT);
state = FINISHED;
}
delay(1);
}
break;
// IN_PACKET: read data until a gap of at least _interval time passed without another byte arriving
case IN_PACKET:
// tight loop until finished reading or error
while (state == IN_PACKET) {
// Is there a byte?
while (serial.available()) {
// Yes, collect it
buffer[bufferPtr++] = serial.read();
// Mark time of last byte
lastMicros = micros();
// Buffer full?
if (bufferPtr >= BUFBLOCKSIZE) {
// Yes. Something fishy here - bail out!
rv.push_back(PACKET_LENGTH_ERROR);
state = FINISHED;
break;
}
}
// No more byte read
if (state == IN_PACKET) {
// Are we past the interval gap?
if (micros() - lastMicros >= interval) {
// Yes, terminate reading
LOG_V("%c/%ldus without data after %u\n", (const char)caller, micros() - lastMicros, bufferPtr);
state = DATA_READ;
break;
}
}
}
break;
// DATA_READ: successfully gathered some data. Prepare return object.
case DATA_READ:
// Did we get a sensible buffer length?
LOG_V("%c/", (const char)caller);
HEXDUMP_V("Raw buffer received", buffer, bufferPtr);
if (bufferPtr >= 4)
{
// Yes. Check CRC
if (!validCRC(buffer, bufferPtr)) {
// Ooops. CRC is wrong.
rv.push_back(CRC_ERROR);
} else {
// CRC was fine, Now allocate response object without the CRC
for (uint16_t i = 0; i < bufferPtr - 2; ++i) {
rv.push_back(buffer[i]);
}
}
} else {
// No, packet was too short for anything usable. Return error
rv.push_back(PACKET_LENGTH_ERROR);
}
state = FINISHED;
break;
// FINISHED: we are done, clean up.
case FINISHED:
// CLear serial buffer in case something is left trailing
// May happen with servers too slow!
while (serial.available()) serial.read();
break;
}
}
} else {
// We are in ASCII mode.
state = A_WAIT_DATA;
// Track nibbles in a byte
bool byteComplete = true;
// Track bytes read
bool hadBytes = false;
// ASCII crc byte
uint8_t crc = 0;
while (state != A_FINISHED) {
// Always watch timeout - 1s
if (millis() - TimeOut >= timeout) {
// Timeout! Bail out with error
rv.push_back(TIMEOUT);
state = A_FINISHED;
} else {
// Still in time. Check for another byte on serial
if (!hadBytes && serial.available()) {
b = serial.read();
if (b >= 0) {
hadBytes = true;
}
}
// Only use state machine with new data arrived
if (hadBytes) {
// First reset timeout
TimeOut = millis();
// Is it a valid character?
if ((b & 0x80) || ASCIIread[b] == 0xFF) {
// No. Report error and leave.
rv.clear();
rv.push_back(ASCII_INVALID_CHAR);
hadBytes = false;
state = A_FINISHED;
} else {
// Yes, is valid. Furtheron use interpreted byte
b = ASCIIread[b];
switch (state) {
// A_WAIT_DATA: await lead-in byte ':'
case A_WAIT_DATA:
// Is it the lead-in?
if (b == 0xF0) {
// Yes, proceed to data read state
state = A_DATA;
}
// byte was consumed in any case
hadBytes = false;
break;
// A_DATA: read data as it comes
case A_DATA:
// Lead-out byte 1 received?
if (b == 0xF1) {
// Yes. Was last buffer byte completed?
if (byteComplete) {
// Yes. Move to final state
state = A_WAIT_LEAD_OUT;
} else {
// No, signal with error
rv.push_back(PACKET_LENGTH_ERROR);
state = A_FINISHED;
}
} else {
// No lead-out, must be data byte.
// Is it valid?
if (b < 0xF0) {
// Yes. Add it into current buffer byte
buffer[bufferPtr] <<= 4;
buffer[bufferPtr] += (b & 0x0F);
// Advance nibble
byteComplete = !byteComplete;
// Was it the second of the byte?
if (byteComplete) {
// Yes. Advance CRC and move buffer pointer by one
crc += buffer[bufferPtr];
bufferPtr++;
buffer[bufferPtr] = 0;
}
} else {
// No, garbage. report error
rv.push_back(ASCII_INVALID_CHAR);
state = A_FINISHED;
}
}
hadBytes = false;
break;
// A_WAIT_LEAD_OUT: await \n
case A_WAIT_LEAD_OUT:
if (b == 0xF2) {
// Lead-out byte 2 received. Transfer buffer to returned message
LOG_V("%c/", (const char)caller);
HEXDUMP_V("Raw buffer received", buffer, bufferPtr);
// Did we get a sensible buffer length?
if (bufferPtr >= 3)
{
// Yes. Was the CRC calculated correctly?
if (crc == 0) {
// Yes, reduce buffer by 1 to get rid of CRC byte...
bufferPtr--;
// Move data into returned message
for (uint16_t i = 0; i < bufferPtr; ++i) {
rv.push_back(buffer[i]);
}
} else {
// No, CRC calculation seems to have failed
rv.push_back(ASCII_CRC_ERR);
}
} else {
// No, packet was too short for anything usable. Return error
rv.push_back(PACKET_LENGTH_ERROR);
}
} else {
// No lead out byte 2, but something else - report error.
rv.push_back(ASCII_FRAME_ERR);
}
state = A_FINISHED;
break;
// A_FINISHED: Message completed
case A_FINISHED:
// Clean up serial buffer
while (serial.available()) serial.read();
break;
}
}
} else {
// No data received, so give the task scheduler room to breathe
delay(1);
}
}
}
}
// Deallocate buffer
delete[] buffer;
LOG_D("%c/", (const char)caller);
HEXDUMP_D("Received packet", rv.data(), rv.size());
return rv;
}
// Lower 7 bit ASCII characters - all invalid are set to 0xFF
const char RTUutils::ASCIIread[] = {
/* 00-07 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 08-0F */ 0xFF, 0xFF, 0xF2, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF, // LF + CR
/* 10-17 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 18-1F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 20-27 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 28-2F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 30-37 */ 0, 1, 2, 3, 4, 5, 6, 7, // digits 0-7
/* 38-3F */ 8, 9, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // digits 8 + 9, :
/* 40-47 */ 0xFF, 10, 11, 12, 13, 14, 15, 0xFF, // digits A-F
/* 48-4F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 50-57 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 58-5F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 60-67 */ 0xFF, 10, 11, 12, 13, 14, 15, 0xFF, // digits a-f
/* 68-6F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 70-77 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
/* 78-7F */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
// Writable ASCII chars for hex digits
const char RTUutils::ASCIIwrite[] = { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46
};
#endif

View File

@@ -0,0 +1,76 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _RTU_UTILS_H
#define _RTU_UTILS_H
#include <stdint.h>
#if NEED_UART_PATCH
#include <soc/uart_struct.h>
#endif
#include <vector>
#include "Stream.h"
#include "ModbusTypeDefs.h"
#include <functional>
typedef std::function<void(bool level)> RTScallback;
using namespace Modbus; // NOLINT
// RTUutils is bundling the send, receive and CRC functions for Modbus RTU communications.
// RTU server and client will make use of it.
// All functions are static!
class RTUutils {
public:
friend class ModbusClientRTU;
friend class ModbusServerRTU;
// calcCRC: calculate the CRC16 value for a given block of data
static uint16_t calcCRC(const uint8_t *data, uint16_t len);
// calcCRC: calculate the CRC16 value for a given block of data
static uint16_t calcCRC(ModbusMessage msg);
// validCRC #1: check the CRC in a block of data for validity
static bool validCRC(const uint8_t *data, uint16_t len);
// validCRC #2: check the CRC of a block of data against a given one
static bool validCRC(const uint8_t *data, uint16_t len, uint16_t CRC);
// validCRC #1: check the CRC in a message for validity
static bool validCRC(ModbusMessage msg);
// validCRC #2: check the CRC of a message against a given one
static bool validCRC(ModbusMessage msg, uint16_t CRC);
// addCRC: extend a RTUMessage by a valid CRC
static void addCRC(ModbusMessage& raw);
// calculateInterval: determine the minimal gap time between messages
static uint32_t calculateInterval(uint32_t baudRate);
// RTSauto: dummy callback for auto half duplex RS485 boards
inline static void RTSauto(bool level) { return; } // NOLINT
// Necessary preparations for a HardwareSerial
static void prepareHardwareSerial(HardwareSerial& s, uint16_t bufferSize = 260) {
s.setRxBufferSize(bufferSize);
s.setTxBufferSize(bufferSize);
}
protected:
// Printable characters for ASCII protocol: 012345678ABCDEF
static const char ASCIIwrite[];
static const char ASCIIread[];
RTUutils() = delete;
// receive: get a Modbus message from serial, maintaining timeouts etc.
static ModbusMessage receive(uint8_t caller, Stream& serial, uint32_t timeout, unsigned long& lastMicros, uint32_t interval, bool ASCIImode, bool skipLeadingZeroBytes = false);
// send: send a Modbus message in either format (ModbusMessage or data/len)
static void send(Stream& serial, unsigned long& lastMicros, uint32_t interval, RTScallback r, const uint8_t *data, uint16_t len, bool ASCIImode);
static void send(Stream& serial, unsigned long& lastMicros, uint32_t interval, RTScallback r, ModbusMessage raw, bool ASCIImode);
};
#endif

61
lib/eModbus/src/options.h Normal file
View File

@@ -0,0 +1,61 @@
// =================================================================================================
// eModbus: Copyright 2020 by Michael Harwerth, Bert Melis and the contributors to eModbus
// MIT license - see license.md for details
// =================================================================================================
#ifndef _EMODBUS_OPTIONS_H
#define _EMODBUS_OPTIONS_H
/* === ESP32 DEFINITIONS AND MACROS === */
#if defined(ESP32)
#include <Arduino.h>
#define USE_MUTEX 1
#define HAS_FREERTOS 1
#define HAS_ETHERNET 1
#define IS_LINUX 0
#define NEED_UART_PATCH 1
const unsigned int SERVER_TASK_STACK = 4096;
const unsigned int CLIENT_TASK_STACK = 4096;
/* === ESP8266 DEFINITIONS AND MACROS === */
#elif defined(ESP8266)
#include <Arduino.h>
#define USE_MUTEX 0
#define HAS_FREERTOS 0
#define HAS_ETHERNET 0
#define IS_LINUX 0
#define NEED_UART_PATCH 0
/* === LINUX DEFINITIONS AND MACROS === */
#elif defined(__linux__)
#define USE_MUTEX 1
#define HAS_FREERTOS 0
#define HAS_ETHERNET 0
#define IS_LINUX 1
#define NEED_UART_PATCH 0
#include <cstdio> // for printf()
#include <cstring> // for memcpy(), strlen() etc.
#include <cinttypes> // for uint32_t etc.
#if IS_RASPBERRY
#include <wiringPi.h>
#else
#include <chrono> // NOLINT
// Use nanosleep() to avoid problems with pthreads (std::this_thread::sleep_for would interfere!)
#define delay(x) nanosleep((const struct timespec[]){{x/1000, (x%1000)*1000000L}}, NULL);
typedef std::chrono::steady_clock clk;
#define millis() std::chrono::duration_cast<std::chrono::milliseconds>(clk::now().time_since_epoch()).count()
#define micros() std::chrono::duration_cast<std::chrono::microseconds>(clk::now().time_since_epoch()).count()
#endif
/* === INVALID TARGET === */
#else
#error Define target in options.h
#endif
/* === COMMON MACROS === */
#if USE_MUTEX
#define LOCK_GUARD(x,y) std::lock_guard<std::mutex> x(y);
#else
#define LOCK_GUARD(x,y)
#endif
#endif // _EMODBUS_OPTIONS_H

View File

@@ -0,0 +1,118 @@
import fileinput
import csv
import sys
from itertools import groupby
# static data
tag_to_tagtype = {
-1: "TAG_TYPE_NONE",
0: "DEVICE_DATA",
1: "HC",
2: "HC",
3: "HC",
4: "HC",
5: "HC",
6: "HC",
7: "HC",
8: "HC",
9: "DHW",
10: "DHW",
11: "DHW",
12: "DHW",
13: "DHW",
14: "DHW",
15: "DHW",
16: "DHW",
17: "DHW",
18: "DHW",
19: "AHS",
20: "HS",
21: "HS",
22: "HS",
23: "HS",
24: "HS",
25: "HS",
26: "HS",
27: "HS",
28: "HS",
29: "HS",
30: "HS",
31: "HS",
32: "HS",
33: "HS",
34: "HS",
35: "HS"
}
# read entities csv from stdin
entities = []
with fileinput.input() as f_input:
entities_reader = csv.reader(f_input, delimiter=',', quotechar='"')
headers = next(entities_reader)
for row in entities_reader:
entity = {}
for i, val in enumerate(row):
entity[headers[i]] = val
entities.append(entity)
def device_name_key(e): return e["device name"]
def device_type_key(e): return e["device type"]
def grouped_by(list, key): return groupby(sorted(list, key=key), key)
# entities_by_device_type = grouped_by(entities, device_type_key)
def printDeviceEntities(device_name, device_entities):
print("### " + device_name)
print("| shortname | fullname | type | uom | writeable | tag type | register offset | register count | scale factor |")
print("|-|-|-|-|-|-|-|-|-|")
for de in device_entities:
print("| " + de["shortname"] + " | " + de["fullname"] + " | " + de["type [options...] \\| (min/max)"] + " | " + de["uom"] + " | " + de["writeable"] +
" | " + tag_to_tagtype[int(de["modbus block"])] + " | " + de["modbus offset"] + " | " + de["modbus count"] + " | " + de["modbus scale factor"] + " | ")
print()
def printDeviceTypeDevices(device_type, devices):
print("## Devices of type *" + device_type + "*")
for device_name, device_entities in grouped_by(devices, device_name_key):
printDeviceEntities(device_name, device_entities)
# write header
print("<!-- Use full browser width for this page, the tables are wide -->")
print("<style>")
print(".md-grid {")
print(" max-width: 100%; /* or 100%, if you want to stretch to full-width */")
print("}")
print("</style>")
print()
print("# Entity/Register Mapping")
print()
print("!!! note")
print()
print(" This file has been auto-generated. Do not edit.")
print()
for device_type, devices in grouped_by(entities, device_type_key):
printDeviceTypeDevices(device_type, devices)
# def printGroupedData(groupedData):
# for k, v in groupedData:
# # print("Group {} {}".format(k, list(v)))
# print(k)
# printGroupedData(grouped_entities)
# for e in entities:
# print(e)

View File

@@ -0,0 +1,6 @@
#!/bin/bash
make clean
make ARGS=-DEMSESP_STANDALONE
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py | python3 ./scripts/generate-modbus-register-doc.py

View File

@@ -0,0 +1,256 @@
#
# Update modbus parameters from entity definitions.
# This script generates c++ code for the modbus parameter definitions.
#
# Usage:
# - first, dump all entities to csv by running 'scripts/dump_entities.sh'
# - then run 'cat ../dump_entities.csv | python3 update_modbus_registers.py > ../src/modbus_entity_parameters.hpp'
# from the "scripts" folder
import fileinput
import csv
import json
import re
from string import Template
modbus_block_size = 1000 # block size of a register block for each tag
# string sizes including terminating NUL. Extracted from the source code.
string_sizes = {
"boiler/lastcode": 55,
"boiler/servicecode": 4,
"boiler/maintenancemessage": 4,
"boiler/maintenancedate": 12,
"boiler/hpin1opt": 16,
"boiler/hpin2opt": 16,
"boiler/hpin3opt": 16,
"boiler/hpin4opt": 16,
"thermostat/errorcode": 16,
"thermostat/lastcode": 50,
"thermostat/datetime": 25,
"thermostat/wwswitchtime": 16,
"thermostat/wwcircswitchtime": 16,
"thermostat/circswitchtime": 16,
"thermostat/wwholidays": 26,
"thermostat/wwvacations": 26,
"thermostat/vacations1": 22,
"thermostat/vacations2": 22,
"thermostat/vacations3": 22,
"thermostat/vacations4": 22,
"thermostat/vacations5": 22,
"thermostat/vacations6": 22,
"thermostat/vacations7": 22,
"thermostat/holidays": 26,
"thermostat/vacations": 26,
"thermostat/switchtime1": 16,
"thermostat/switchtime2": 16,
"thermostat/switchtime": 16,
"controller/datetime": 25
}
tag_to_tagtype = {
-1: "TAG_TYPE_NONE",
0: "TAG_TYPE_DEVICE_DATA",
1: "TAG_TYPE_HC",
2: "TAG_TYPE_HC",
3: "TAG_TYPE_HC",
4: "TAG_TYPE_HC",
5: "TAG_TYPE_HC",
6: "TAG_TYPE_HC",
7: "TAG_TYPE_HC",
8: "TAG_TYPE_HC",
9: "TAG_TYPE_DHW",
10: "TAG_TYPE_DHW",
11: "TAG_TYPE_DHW",
12: "TAG_TYPE_DHW",
13: "TAG_TYPE_DHW",
14: "TAG_TYPE_DHW",
15: "TAG_TYPE_DHW",
16: "TAG_TYPE_DHW",
17: "TAG_TYPE_DHW",
18: "TAG_TYPE_DHW",
19: "TAG_TYPE_AHS",
20: "TAG_TYPE_HS",
21: "TAG_TYPE_HS",
22: "TAG_TYPE_HS",
23: "TAG_TYPE_HS",
24: "TAG_TYPE_HS",
25: "TAG_TYPE_HS",
26: "TAG_TYPE_HS",
27: "TAG_TYPE_HS",
28: "TAG_TYPE_HS",
29: "TAG_TYPE_HS",
30: "TAG_TYPE_HS",
31: "TAG_TYPE_HS",
32: "TAG_TYPE_HS",
33: "TAG_TYPE_HS",
34: "TAG_TYPE_HS",
35: "TAG_TYPE_HS"
}
device_type_names = [
"SYSTEM",
"TEMPERATURESENSOR",
"ANALOGSENSOR",
"SCHEDULER",
"CUSTOM",
"BOILER",
"THERMOSTAT",
"MIXER",
"SOLAR",
"HEATPUMP",
"GATEWAY",
"SWITCH",
"CONTROLLER",
"CONNECT",
"ALERT",
"EXTENSION",
"GENERIC",
"HEATSOURCE",
"VENTILATION",
"WATER"
]
cpp_file_template = Template('''#include "modbus.h"
#include "emsdevice.h"
namespace emsesp {
using dt = EMSdevice::DeviceType;
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \\
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {
$entries};
} // namespace emsesp''')
# cpp_entry_template = Template(
# ' {std::make_tuple($devtype, $tagtype, std::string(\"$shortname\")), {$registeroffset, $registercount}},\n')
cpp_entry_template = Template(
' REGISTER_MAPPING($devtype, $tagtype, $shortname, $registeroffset, $registercount), // $entity_name\n')
# read translations
listNames = {}
transre = re.compile(r'^MAKE_TRANSLATION\(([^,\s]+)\s*,\s*\"([^\"]+)\"')
transf = open('./src/locale_translations.h', 'r')
while True:
line = transf.readline()
if not line:
break
m = transre.match(line)
if m is not None:
listNames[m.group(2)] = m.group(1)
transf.close()
entities = []
with fileinput.input() as f_input:
entities_reader = csv.reader(f_input, delimiter=',', quotechar='"')
headers = next(entities_reader)
for row in entities_reader:
entity = {}
for i, val in enumerate(row):
entity[headers[i]] = val
entities.append(entity)
# print(json.dumps(entities, indent=" "))
device_types = {}
string_entities = []
entity_modbus_property_names = [
"modbus block",
"modbus offset",
"modbus count"
]
for entity in entities:
device_type_name = entity['device type'].upper()
if device_type_name not in device_types:
device_types[device_type_name] = {}
device_type = device_types[device_type_name]
tag_name = entity['modbus block']
if tag_name not in device_type:
device_type[tag_name] = {}
tag = device_type[tag_name]
entity_shortname = entity['shortname']
entity_dev_name = (device_type_name + "/" + entity_shortname).lower()
# set size for string entities
if entity["modbus count"] == "0" and entity_dev_name in string_sizes:
entity["modbus count"] = -(-string_sizes[entity_dev_name] // 2) # divide and round up
if int(entity["modbus count"]) <= 0:
raise Exception('Entity "' + entity_shortname + '" does not have a size - string sizes need to be added manually to update_modbus_registers.py')
# if entity["modbus count"] == "0":
# print("ignoring " + entity_dev_name + " - it has a register length of zero")
if entity_shortname in tag:
for entity_property_name in entity_modbus_property_names:
if tag[entity_shortname][entity_property_name] != entity[entity_property_name]:
raise Exception(
"Property mismatch between instances of the same entity for property '" + entity_property_name + "':\n" +
json.dumps(tag[entity_shortname]) +
json.dumps(entity))
else:
tag[entity_shortname] = {}
for entity_property_name in entity_modbus_property_names:
tag[entity_shortname][entity_property_name] = entity[entity_property_name]
# print(json.dumps(device_types, indent=" "))
# ASSIGN REGISTERS
for device_type_name, device_type in device_types.items():
for tag, entities in device_type.items():
total_registers = 0
next_free_offset = 0
for entity_name, modbus_info in entities.items():
register_offset = int(modbus_info['modbus offset'])
register_count = int(modbus_info['modbus count'])
total_registers += register_count
if register_offset >= 0 and register_offset + register_count > next_free_offset:
next_free_offset = register_offset + register_count
# print(device_type_name + "/" + tag + ": total_registers=" + str(total_registers) + "; next_free_offset=" + str(
# next_free_offset))
for entity_name, modbus_info in entities.items():
register_offset = int(modbus_info['modbus offset'])
register_count = int(modbus_info['modbus count'])
if register_offset < 0 and register_count > 0:
# assign register
# print("assign " + entity_name + " -> " + str(next_free_offset))
modbus_info['modbus offset'] = str(next_free_offset)
next_free_offset += register_count
# OUTPUT
cpp_entries = ""
# traverse all elements in correct order so they are correctly sorted
for device_type_name in device_type_names:
if device_type_name in device_types:
device_type = device_types[device_type_name]
for ntag in range(0, 40):
tag = str(ntag)
if tag in device_type:
entities = device_type[tag]
for entity_name, modbus_info in sorted(entities.items(), key=lambda x: int(x[1]["modbus offset"])):
params = {
'devtype': "dt::" + device_type_name,
"tagtype": tag_to_tagtype[int(tag)], # re.sub(r"[0-9]+", "*", tag),
"shortname": 'FL_(' + listNames[entity_name] + ")",
"entity_name": entity_name,
'registeroffset': modbus_info["modbus offset"],
'registercount': modbus_info["modbus count"]
}
# print(entitypath + ": " + str(modbus_info))
cpp_entries += cpp_entry_template.substitute(params)
cpp_src = cpp_file_template.substitute({'entries': cpp_entries})
print(cpp_src)

View File

@@ -0,0 +1,55 @@
#!/bin/bash
# Update modbus parameters from entity definitions.
# This script generates c++ code for the modbus parameter definitions.
#
# Run this script from the EMS-ESP32 root directory with the command `scripts/update_modbus_registers.sh`.
while [[ $# -gt 0 ]]; do
case $1 in
--reset)
RESET=YES
shift
;;
--force)
FORCE=YES
shift
;;
*|-*|--*)
echo "Unknown option $1"
exit 1
;;
esac
done
if [ "$RESET" = "YES" ]; then
if [ "$FORCE" != "YES" ]; then
read -p "Are you sure you want to reset all modbus entity parameters, potentially generating new register mappings? WARNING: This might introduce breaking changes for end users. [yes/no] " CONFIRMATION
if [ "$CONFIRMATION" != "yes" ]; then
echo "Aborted."
exit 1
fi
fi
echo "Resetting modbus configuration"
cat >./src/modbus_entity_parameters.hpp <<EOL
#include "modbus.h"
#include "emsdevice.h"
namespace emsesp {
using dt = EMSdevice::DeviceType;
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \\
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {};
} // namespace emsesp
EOL
fi
make clean
make ARGS=-DEMSESP_STANDALONE
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py | python3 ./scripts/update_modbus_registers.py > ./src/modbus_entity_parameters.hpp
echo "Modbus entity parameters written to ./src/modbus_entity_parameters.hpp."

View File

@@ -121,6 +121,22 @@
#define EMSESP_DEFAULT_TELNET_ENABLED true
#endif
#ifndef EMSESP_DEFAULT_MODBUS_ENABLED
#define EMSESP_DEFAULT_MODBUS_ENABLED false
#endif
#ifndef EMSESP_DEFAULT_MODBUS_PORT
#define EMSESP_DEFAULT_MODBUS_PORT 502
#endif
#ifndef EMSESP_DEFAULT_MODBUS_MAX_CLIENTS
#define EMSESP_DEFAULT_MODBUS_MAX_CLIENTS 10
#endif
#ifndef EMSESP_DEFAULT_MODBUS_TIMEOUT
#define EMSESP_DEFAULT_MODBUS_TIMEOUT 10000
#endif
#ifndef EMSESP_DEFAULT_BOARD_PROFILE
#define EMSESP_DEFAULT_BOARD_PROFILE "default"
#endif

View File

@@ -1374,6 +1374,33 @@ void EMSdevice::dump_value_info() {
Serial.print(entityid);
}
Serial.print(",");
// modbus specific infos
Serial.print(device_type());
Serial.print(',');
Serial.print(dv.tag);
Serial.print(',');
// numeric operator -> scale factor
if (dv.numeric_operator == 0)
Serial.print("1");
else if (dv.numeric_operator > 0)
Serial.printf("1/%d", dv.numeric_operator);
else
Serial.print(-dv.numeric_operator);
Serial.print(",");
Serial.printf("%d", EMSESP::modbus_->getRegisterOffset(dv));
Serial.print(",");
Serial.printf("%d", EMSESP::modbus_->getRegisterCount(dv));
// /modbus specific infos
Serial.println();
}
}
@@ -1901,4 +1928,155 @@ std::string EMSdevice::name() {
return custom_name_;
}
// copy a raw value (i.e. without applying the numeric_operator) to the output buffer.
// returns true on success.
int EMSdevice::get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result) {
// find device value by shortname
// TODO linear search is inefficient
const auto & it = std::find_if(devicevalues_.begin(), devicevalues_.end(), [&](const DeviceValue & x) { return x.tag == tag && x.short_name == shortname; });
if (it == devicevalues_.end())
return -1;
auto & dv = *it;
// check if it exists, there is a value for the entity. Set the flag to ACTIVE
// not that this will override any previously removed states
(dv.hasValue()) ? dv.add_state(DeviceValueState::DV_ACTIVE) : dv.remove_state(DeviceValueState::DV_ACTIVE);
if (!dv.has_state(DeviceValueState::DV_ACTIVE))
return -2;
// handle Booleans
if (dv.type == DeviceValueType::BOOL) {
if (result.size() != 1)
return -3;
auto value_bool = *(uint8_t *)(dv.value_p);
if (!Helpers::hasValue(value_bool, EMS_VALUE_BOOL))
return -4;
result[0] = value_bool ? 1 : 0;
}
// handle TEXT strings
else if (dv.type == DeviceValueType::STRING) {
auto value_s = (char *)dv.value_p;
size_t length_s = strlen(value_s) + 1; // length including terminating zero in bytes
size_t register_length_s = (length_s + 1) / 2; // length including terminating zero in uint16_t-registers
if ((long)result.size() < (long)register_length_s) {
return -5;
}
for (auto i = 0; i < register_length_s; i++) {
auto hi = (uint8_t)value_s[2 * i];
auto lo = (uint8_t)(2 * i + 1 < length_s ? value_s[2 * i + 1] : 0);
result[i] = ((uint16_t)hi << 8) | lo;
}
}
// handle ENUMs
else if (dv.type == DeviceValueType::ENUM) {
if (result.size() != 1)
return -6;
auto value_enum = *(uint8_t *)(dv.value_p);
if (value_enum >= dv.options_size)
return -7;
result[0] = (uint16_t)value_enum;
}
// handle Numbers
else if (dv.type == DeviceValueType::INT8) {
if (result.size() != 1)
return -8;
result[0] = (uint16_t)(uint8_t)(*(int8_t *)(dv.value_p));
} else if (dv.type == DeviceValueType::UINT8) {
if (result.size() != 1)
return -9;
result[0] = (uint16_t)(*(uint8_t *)(dv.value_p));
} else if (dv.type == DeviceValueType::INT16) {
if (result.size() != 1)
return -10;
result[0] = (uint16_t)(*(int16_t *)(dv.value_p));
} else if (dv.type == DeviceValueType::UINT16) {
if (result.size() != 1)
return -11;
result[0] = *(uint16_t *)(dv.value_p);
} else if (dv.type == DeviceValueType::UINT24 || dv.type == DeviceValueType::UINT32 || dv.type == DeviceValueType::TIME) {
if (result.size() != 2)
return -12;
auto value_uint32 = *(uint32_t *)(dv.value_p);
result[0] = (uint16_t)(value_uint32 >> 16);
result[1] = (uint16_t)(value_uint32 & 0xffff);
}
else {
return -13;
}
return 0;
}
bool EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject & jsonValue) {
//Serial.printf("modbus_value_to_json(%d,%s,[%d bytes])\n", tag, shortname.c_str(), modbus_data.size());
// find device value by shortname
const auto & it = std::find_if(devicevalues_.begin(), devicevalues_.end(), [&](const DeviceValue & x) { return x.tag == tag && x.short_name == shortname; });
if (it == devicevalues_.end()) {
return false;
}
auto & dv = *it;
// handle Booleans
if (dv.type == DeviceValueType::BOOL) {
// bools are 1 16 bit register
if (modbus_data.size() != 2) {
return false;
}
jsonValue["value"] = modbus_data[0] || modbus_data[1];
}
// handle TEXT strings
else if (dv.type == DeviceValueType::STRING) {
// text is optionally nul terminated
// check if the data contains a null char
auto nul_or_end = std::find(modbus_data.begin(), modbus_data.end(), 0);
jsonValue["value"] = std::string(modbus_data.begin(), nul_or_end);
}
// handle ENUMs
else if (dv.type == DeviceValueType::ENUM) {
// these data types are 1 16 bit register
if (modbus_data.size() != 2) {
return false;
}
jsonValue["value"] = (uint16_t)modbus_data[0] << 8 | (uint16_t)modbus_data[1];
}
// handle Numbers
else if (dv.type == DeviceValueType::INT8 || dv.type == DeviceValueType::UINT8 || dv.type == DeviceValueType::INT16 || dv.type == DeviceValueType::UINT16) {
// these data types are 1 16 bit register
if (modbus_data.size() != 2) {
return false;
}
jsonValue["value"] = Helpers::numericoperator2scalefactor(dv.numeric_operator) * (float)((uint16_t)modbus_data[0] << 8 | (uint16_t)modbus_data[1]);
} else if (dv.type == DeviceValueType::UINT24 || dv.type == DeviceValueType::UINT32 || dv.type == DeviceValueType::TIME) {
// these data types are 2 16 bit register
if (modbus_data.size() != 4) {
return false;
}
jsonValue["value"] =
Helpers::numericoperator2scalefactor(dv.numeric_operator)
* (float)((uint32_t)modbus_data[0] << 24 | (uint32_t)modbus_data[1] << 16 | (uint32_t)modbus_data[2] << 8 | (uint32_t)modbus_data[3]);
}
else {
return false;
}
return true;
}
} // namespace emsesp

View File

@@ -202,6 +202,9 @@ class EMSdevice {
}
}
int get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result);
bool modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject & jsonValue);
const char * brand_to_char();
const std::string to_string();
const std::string to_string_short();

View File

@@ -72,6 +72,7 @@ uuid::syslog::SyslogService System::syslog_;
RxService EMSESP::rxservice_; // incoming Telegram Rx handler
TxService EMSESP::txservice_; // outgoing Telegram Tx handler
Mqtt EMSESP::mqtt_; // mqtt handler
Modbus * EMSESP::modbus_; // modbus handler
System EMSESP::system_; // core system services
TemperatureSensor EMSESP::temperaturesensor_; // Temperature sensors
AnalogSensor EMSESP::analogsensor_; // Analog sensors
@@ -334,8 +335,8 @@ void EMSESP::show_ems(uuid::console::Shell & shell) {
void EMSESP::dump_all_values(uuid::console::Shell & shell) {
Serial.println("---- CSV START ----"); // marker use by py script
// add header for CSV
Serial.println(
"device name,device type,product id,shortname,fullname,type [options...] \\| (min/max),uom,writeable,discovery entityid v3.4, discovery entityid");
Serial.println("device name,device type,product id,shortname,fullname,type [options...] \\| (min/max),uom,writeable,discovery entityid v3.4,discovery "
"entityid,modbus unit identifier,modbus block,modbus scale factor,modbus offset,modbus count");
for (const auto & device_class : EMSFactory::device_handlers()) {
// go through each device type so they are sorted
@@ -1655,6 +1656,11 @@ void EMSESP::start() {
#endif
}
if (system_.modbus_enabled()) {
modbus_ = new Modbus;
modbus_->start(1, system_.modbus_port(), system_.modbus_max_clients(), system_.modbus_timeout());
}
mqtt_.start(); // mqtt init
system_.start(); // starts commands, led, adc, button, network (sets hostname), syslog & uart
shower_.start(); // initialize shower timer and shower alert

View File

@@ -56,6 +56,7 @@
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "modbus.h"
#include "system.h"
#include "temperaturesensor.h"
#include "analogsensor.h"
@@ -216,6 +217,7 @@ class EMSESP {
// services
static Mqtt mqtt_;
static Modbus * modbus_;
static System system_;
static TemperatureSensor temperaturesensor_;
static AnalogSensor analogsensor_;

View File

@@ -837,4 +837,13 @@ uint16_t Helpers::string2minutes(const std::string & str) {
}
}
float Helpers::numericoperator2scalefactor(uint8_t numeric_operator) {
if (numeric_operator == 0)
return 1.0f;
else if (numeric_operator > 0)
return 1.0f / numeric_operator;
else
return -numeric_operator;
}
} // namespace emsesp

View File

@@ -50,6 +50,7 @@ class Helpers {
static bool check_abs(const int32_t i);
static uint32_t abs(const int32_t i);
static uint16_t string2minutes(const std::string & str);
static float numericoperator2scalefactor(uint8_t numeric_operator);
static float transformNumFloat(float value, const int8_t numeric_operator, const uint8_t fahrenheit = 0);

View File

@@ -52,6 +52,7 @@ MAKE_WORD(ems)
MAKE_WORD(devices)
MAKE_WORD(shower)
MAKE_WORD(mqtt)
MAKE_WORD(modbus)
MAKE_WORD(emsesp)
MAKE_WORD(connected)
MAKE_WORD(disconnected)

509
src/modbus.cpp Normal file
View File

@@ -0,0 +1,509 @@
/**
* TODO:
* - verwendete libs in readme hinzufügen
*/
#include "modbus.h"
#include "modbus_entity_parameters.hpp"
#include "emsesp.h"
#include "emsdevice.h"
#include <string>
#include <sstream>
#include <iomanip>
namespace emsesp {
#ifdef EMSESP_STANDALONE
// no eModbus lib in standalone build
enum FunctionCode : uint8_t { WRITE_HOLD_REGISTER = 0x06, WRITE_MULT_REGISTERS = 0x10 };
#endif
uuid::log::Logger Modbus::logger_{F_(modbus), uuid::log::Facility::DAEMON};
void Modbus::start(uint8_t systemServerId, uint16_t port, uint8_t max_clients, uint32_t timeout) {
#ifndef EMSESP_STANDALONE
if (!check_parameter_order()) {
LOG_ERROR("Unable to enable Modbus - the parameter list order is corrupt. This is a firmware bug.");
return;
}
modbusServer_ = new ModbusServerTCPasync();
modbusServer_->registerWorker(systemServerId, READ_INPUT_REGISTER, [this](auto && request) { return handleSystemRead(request); });
for (uint8_t i = EMSdevice::DeviceType::BOILER; i < EMSdevice::DeviceType::UNKNOWN; i++) {
if (i != systemServerId) {
modbusServer_->registerWorker(i, READ_INPUT_REGISTER, [this](auto && request) { return handleRead(request); });
modbusServer_->registerWorker(i, READ_HOLD_REGISTER, [this](auto && request) { return handleRead(request); });
modbusServer_->registerWorker(i, WRITE_HOLD_REGISTER, [this](auto && request) { return handleWrite(request); });
modbusServer_->registerWorker(i, WRITE_MULT_REGISTERS, [this](auto && request) { return handleWrite(request); });
}
}
modbusServer_->start(port, max_clients, timeout);
LOG_INFO("Modbus server with ID %d started on port %d", systemServerId, port);
#else
if (!check_parameter_order()) {
LOG_ERROR("Unable to enable Modbus - the parameter list order is corrupt. This is a firmware bug.");
}
LOG_INFO("Modbus deactivated in standalone build.");
#endif
}
// this is currently never called, just for good measure
void Modbus::stop() {
#ifndef EMSESP_STANDALONE
modbusServer_->stop();
delete modbusServer_;
modbusServer_ = nullptr;
#endif
}
// Check that the Modbus parameters defined in modbus_entity_parameters.cpp are correctly ordered
bool Modbus::check_parameter_order() {
EntityModbusInfo const * prev = nullptr;
bool isFirst = true;
for (const auto & mi : modbus_register_mappings) {
if (isFirst) {
isFirst = false;
} else if (prev == nullptr || !prev->isLessThan(mi)) {
LOG_ERROR("Error in modbus parameters: %s must be listed before %s.", mi.short_name, prev->short_name);
return false;
}
prev = &mi;
}
return true;
}
int8_t Modbus::tag_to_type(int8_t tag) {
// this coulod even be an array
switch (tag) {
case DeviceValue::TAG_NONE:
return TAG_TYPE_NONE;
case DeviceValue::TAG_DEVICE_DATA:
return TAG_TYPE_DEVICE_DATA;
case DeviceValue::TAG_HC1:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC2:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC3:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC4:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC5:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC6:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC7:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC8:
return TAG_TYPE_HC;
case DeviceValue::TAG_DHW1:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW2:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW3:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW4:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW5:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW6:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW7:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW8:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW9:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW10:
return TAG_TYPE_DHW;
case DeviceValue::TAG_AHS1:
return TAG_TYPE_AHS;
case DeviceValue::TAG_HS1:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS2:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS3:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS4:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS5:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS6:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS7:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS8:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS9:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS10:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS11:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS12:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS13:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS14:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS15:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS16:
return TAG_TYPE_HS;
default:
return INVALID_TAG_TYPE;
}
}
/* DEBUG
template <typename TInputIter>
std::string make_hex_string(TInputIter first, TInputIter last) {
std::ostringstream ss;
ss << std::hex << std::setfill('0');
while (first != last)
ss << std::setw(2) << static_cast<int>(*first++);
return ss.str();
}
*/
/**
*
* @param request
* @return
*/
ModbusMessage Modbus::handleSystemRead(const ModbusMessage & request) {
ModbusMessage response;
uint16_t start_address = 0;
uint16_t num_words = 0;
request.get(2, start_address);
request.get(4, num_words);
LOG_DEBUG("Got request for serverId %d, startAddress %d, numWords %d", request.getServerID(), start_address, num_words);
if (start_address < 1000) {
switch (start_address) {
case 1:
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)2);
response.add((uint16_t)EMSESP::emsdevices.size());
break;
default:
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
}
} else if (start_address < 1000 + EMSESP::emsdevices.size()) {
const auto & dev = EMSESP::emsdevices[start_address - 1000];
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)2);
response.add(dev->device_type());
response.add(dev->device_id());
} else {
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
}
return response;
}
ModbusMessage Modbus::handleRead(const ModbusMessage & request) {
ModbusMessage response;
uint8_t device_type = request.getServerID(); // the server ID is the same as the device type
uint16_t start_address = 0;
uint16_t num_words = 0;
request.get(2, start_address);
request.get(4, num_words);
LOG_DEBUG("Got read request for serverId %d, startAddress %d, numWords %d", device_type, start_address, num_words);
// each register block corresponds to a device value tag
auto tag = (uint8_t)(start_address / REGISTER_BLOCK_SIZE);
auto tag_type = tag_to_type(tag);
if (tag_type == INVALID_TAG_TYPE) {
// invalid register block, does not correspond to an existing tag
LOG_ERROR("invalid register block, does not correspond to an existing tag");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto register_offset = start_address - tag * REGISTER_BLOCK_SIZE;
const auto & dev_it =
std::find_if(EMSESP::emsdevices.begin(), EMSESP::emsdevices.end(), [&](const std::unique_ptr<EMSdevice> & x) { return x->device_type() == device_type; });
if (dev_it == EMSESP::emsdevices.end()) {
// device not found => invalid server ID
LOG_ERROR("device with type %d not found => invalid server ID", device_type);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
const auto & dev = *dev_it;
// binary search in modbus infos
auto key = EntityModbusInfoKey(dev->device_type(), tag_type, register_offset);
const auto & modbusInfo = std::lower_bound(std::begin(modbus_register_mappings),
std::end(modbus_register_mappings),
key,
[](const EntityModbusInfo & a, const EntityModbusInfoKey & b) { return a.isLessThan(b); });
if (modbusInfo == std::end(modbus_register_mappings) || !modbusInfo->equals(key)) {
// combination of device_type/tag_type/register_offset does not exist
LOG_ERROR("combination of device_type/tag_type/register_offset does not exist");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
// only reading a single value at a time is supported for now
if (num_words != modbusInfo->registerCount) {
// number of registers requested does not match actual register count for entity
LOG_ERROR("number of registers requested does not match actual register count for entity");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto buf = std::vector<uint16_t>(num_words);
if (dev->get_modbus_value(tag, modbusInfo->short_name, buf) == 0) {
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)(num_words * 2));
for (auto & value : buf)
response.add(value);
} else {
LOG_ERROR("Unable to read raw device value %s for tag=%d", modbusInfo->short_name, (int)tag);
response.setError(request.getServerID(), request.getFunctionCode(), SERVER_DEVICE_FAILURE);
}
return response;
}
ModbusMessage Modbus::handleWrite(const ModbusMessage & request) {
ModbusMessage response;
uint8_t device_type = request.getServerID(); // the server ID is the same as the device type
uint8_t function_code = request.getFunctionCode();
uint16_t start_address = 0;
uint16_t num_words = 0;
uint8_t byte_count = 0;
std::vector<uint8_t> data;
if (function_code == WRITE_MULT_REGISTERS) {
request.get(2, start_address);
request.get(4, num_words);
request.get(6, byte_count);
request.get(7, data, byte_count);
} else if (function_code == WRITE_HOLD_REGISTER) {
num_words = 1;
byte_count = 2;
request.get(2, start_address);
request.get(4, data, byte_count);
} else {
LOG_ERROR("Function code %d is not implemented", function_code);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION);
return response;
}
LOG_DEBUG("Got write request for serverId %d, startAddress %d, numWords %d, byteCount %d", device_type, start_address, num_words, byte_count);
// each register block corresponds to a device value tag
auto tag = (uint8_t)(start_address / REGISTER_BLOCK_SIZE);
auto tag_type = tag_to_type(tag);
if (tag_type == INVALID_TAG_TYPE) {
// invalid register block, does not correspond to an existing tag
LOG_ERROR("invalid register block (%d), does not correspond to an existing tag", tag);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto register_offset = start_address - tag * REGISTER_BLOCK_SIZE;
const auto & dev_it =
std::find_if(EMSESP::emsdevices.begin(), EMSESP::emsdevices.end(), [&](const std::unique_ptr<EMSdevice> & x) { return x->device_type() == device_type; });
if (dev_it == EMSESP::emsdevices.end()) {
// device not found => invalid server ID
LOG_ERROR("device_type (%d) not found => invalid server ID", device_type);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
const auto & dev = *dev_it;
// binary search in modbus infos
auto key = EntityModbusInfoKey(dev->device_type(), tag_type, register_offset);
auto modbusInfo = std::lower_bound(std::begin(modbus_register_mappings),
std::end(modbus_register_mappings),
key,
[](const EntityModbusInfo & mi, const EntityModbusInfoKey & k) { return mi.isLessThan(k); });
if (modbusInfo == std::end(modbus_register_mappings) || !modbusInfo->equals(key)) {
// combination of device_type/tag_type/register_offset does not exist
LOG_ERROR("combination of device_type (%d)/tag_type (%d)/register_offset (%d) does not exist",
key.device_type,
key.device_value_tag_type,
key.registerOffset);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
// only writing a single value at a time is supported for now
if (num_words != modbusInfo->registerCount) {
// number of registers requested does not match actual register count for entity
LOG_ERROR("number of registers (%d) requested does not match actual register count (%d) for entity", num_words, modbusInfo->registerCount);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
JsonDocument input_doc;
JsonObject input = input_doc.to<JsonObject>();
if (!dev->modbus_value_to_json(tag, modbusInfo->short_name, data, input)) {
// error getting modbus value as json
LOG_ERROR("error getting modbus value as json");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
std::string path;
if (tag < DeviceValueTAG::TAG_HC1) {
path = std::string("ems-esp/") + std::string(EMSdevice::device_type_2_device_name(dev->device_type())) + "/" + modbusInfo->short_name;
} else {
path = std::string("ems-esp/") + std::string(EMSdevice::device_type_2_device_name(dev->device_type())) + "/" + EMSdevice::tag_to_mqtt(tag) + "/"
+ modbusInfo->short_name;
}
LOG_DEBUG("path: %s\n", path.c_str());
std::string inputStr;
serializeJson(input, inputStr);
LOG_DEBUG("input: %s\n", inputStr.c_str());
JsonDocument output_doc;
JsonObject output = output_doc.to<JsonObject>();
uint8_t return_code = Command::process(path.c_str(), true, input, output); // modbus is always authenticated
if (return_code != CommandRet::OK) {
char error[100];
if (output.size()) {
snprintf(error,
sizeof(error),
"Modbus write command failed with error: %s (%s)",
(const char *)output["message"],
Command::return_code_string(return_code).c_str());
} else {
snprintf(error, sizeof(error), "Modbus write command failed with error code (%s)", Command::return_code_string(return_code).c_str());
}
LOG_ERROR(error);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_VALUE);
} else {
// all good
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add(start_address);
response.add(num_words);
}
return response;
}
#if defined(EMSESP_STANDALONE)
// return the relative register start offset for a DeviceValue, i.e. the address within the
// register block corresponding to the value's tag type.
int Modbus::getRegisterOffset(const DeviceValue & dv) {
auto it = std::find_if(std::begin(modbus_register_mappings), std::end(modbus_register_mappings), [&](const EntityModbusInfo & mi) {
return mi.device_type == dv.device_type && mi.device_value_tag_type == tag_to_type(dv.tag) && !strcmp(mi.short_name, dv.short_name);
});
if (it != std::end(modbus_register_mappings)) {
return it->registerOffset;
}
return -1;
}
// return the number of registers
int Modbus::getRegisterCount(const DeviceValue & dv) {
auto it = std::find_if(std::begin(modbus_register_mappings), std::end(modbus_register_mappings), [&](const EntityModbusInfo & mi) {
return mi.device_type == dv.device_type && mi.device_value_tag_type == tag_to_type(dv.tag) && !strcmp(mi.short_name, dv.short_name);
});
if (it != std::end(modbus_register_mappings)) {
// look up actual size
return it->registerCount;
} else {
// guess based on type
switch (dv.type) {
case DeviceValue::BOOL: // 8 bit
case DeviceValue::INT8:
case DeviceValue::UINT8:
case DeviceValue::INT16:
case DeviceValue::UINT16:
case DeviceValue::ENUM: // 8 bit
case DeviceValue::CMD:
return 1;
case DeviceValue::UINT24:
case DeviceValue::UINT32:
case DeviceValue::TIME: // 32 bit
return 2;
case DeviceValue::STRING:
break; // impossible to guess, needs to be hardcoded
}
}
return 0;
}
// return the absolute register start address for a DeviceValue
int Modbus::getRegisterStartAddress(const DeviceValue & dv) {
return dv.tag * REGISTER_BLOCK_SIZE + getRegisterOffset(dv);
}
#endif
} // namespace emsesp

112
src/modbus.h Normal file
View File

@@ -0,0 +1,112 @@
//
// Created by Michael Heyse on 08.02.24.
//
#ifndef EMSESP_MODBUS_H_
#define EMSESP_MODBUS_H_
#include "helpers.h"
#include "emsdevice.h"
#include "emsdevicevalue.h"
#include <string>
#include <map>
#include <utility>
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
#include <modbus_test.h>
#endif
#ifndef EMSESP_STANDALONE
#include <ModbusServerTCPasync.h>
#endif
namespace emsesp {
class Modbus {
public:
static const int REGISTER_BLOCK_SIZE = 1000;
void start(uint8_t systemServerId, uint16_t port, uint8_t max_clients, uint32_t timeout);
void stop();
#if defined(EMSESP_STANDALONE)
int getRegisterOffset(const DeviceValue & dv);
int getRegisterCount(const DeviceValue & dv);
int getRegisterStartAddress(const DeviceValue & dv);
#endif
private:
static uuid::log::Logger logger_;
struct EntityModbusInfoKey {
const uint8_t device_type;
const uint8_t device_value_tag_type;
const uint16_t registerOffset;
EntityModbusInfoKey(uint8_t deviceType, uint8_t deviceValueTagType, uint16_t registerOffset)
: device_type(deviceType)
, device_value_tag_type(deviceValueTagType)
, registerOffset(registerOffset) {
}
bool equals(const EntityModbusInfoKey & other) const {
return device_type == other.device_type && device_value_tag_type == other.device_value_tag_type && registerOffset == other.registerOffset;
}
};
struct EntityModbusInfo {
const uint8_t device_type;
const uint8_t device_value_tag_type;
const char * const short_name;
const uint16_t registerOffset;
const uint16_t registerCount;
bool equals(const EntityModbusInfoKey & other) const {
return device_type == other.device_type && device_value_tag_type == other.device_value_tag_type && registerOffset == other.registerOffset;
}
bool isLessThan(const EntityModbusInfoKey & other) const {
return device_type < other.device_type || ((device_type == other.device_type) && (device_value_tag_type < other.device_value_tag_type))
|| ((device_type == other.device_type) && (device_value_tag_type == other.device_value_tag_type) && (registerOffset < other.registerOffset));
}
bool isLessThan(const EntityModbusInfo & other) const {
return device_type < other.device_type || ((device_type == other.device_type) && (device_value_tag_type < other.device_value_tag_type))
|| ((device_type == other.device_type) && (device_value_tag_type == other.device_value_tag_type) && (registerOffset < other.registerOffset));
}
};
enum DeviceValueTAGType : int8_t {
TAG_TYPE_NONE = DeviceValue::DeviceValueTAG::TAG_NONE,
TAG_TYPE_DEVICE_DATA = DeviceValue::DeviceValueTAG::TAG_DEVICE_DATA,
TAG_TYPE_HC = DeviceValue::DeviceValueTAG::TAG_HC1,
TAG_TYPE_DHW = DeviceValue::DeviceValueTAG::TAG_DHW1,
TAG_TYPE_AHS = DeviceValue::DeviceValueTAG::TAG_AHS1,
TAG_TYPE_HS = DeviceValue::DeviceValueTAG::TAG_HS1,
INVALID_TAG_TYPE = -2
};
static const std::initializer_list<EntityModbusInfo> modbus_register_mappings;
static int8_t tag_to_type(int8_t tag);
static bool check_parameter_order();
#ifndef EMSESP_STANDALONE
ModbusServerTCPasync * modbusServer_;
#endif
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
public:
#endif
static ModbusMessage handleSystemRead(const ModbusMessage & request);
static ModbusMessage handleRead(const ModbusMessage & request);
static ModbusMessage handleWrite(const ModbusMessage & request);
};
} // namespace emsesp
#endif //EMSESP_MODBUS_H_

View File

@@ -0,0 +1,515 @@
#include "modbus.h"
#include "emsdevice.h"
namespace emsesp {
using dt = EMSdevice::DeviceType;
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(reset), 0, 1), // reset
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(forceHeatingOff), 1, 1), // heatingoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingActive), 2, 1), // heatingactive
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tapwaterActive), 3, 1), // tapwateractive
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(selFlowTemp), 4, 1), // selflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingPumpMod), 5, 1), // heatingpumpmod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(outdoorTemp), 6, 1), // outdoortemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curFlowTemp), 7, 1), // curflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(retTemp), 8, 1), // rettemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(switchTemp), 9, 1), // switchtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(sysPress), 10, 1), // syspress
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilTemp), 11, 1), // boiltemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(headertemp), 12, 1), // headertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingActivated), 13, 1), // heatingactivated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingTemp), 14, 1), // heatingtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingPump), 15, 1), // heatingpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpModMax), 16, 1), // pumpmodmax
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpModMin), 17, 1), // pumpmodmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpMode), 18, 1), // pumpmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpCharacter), 19, 1), // pumpcharacter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpDelay), 20, 1), // pumpdelay
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(setFlowTemp), 21, 1), // setflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(setBurnPow), 22, 1), // setburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(selBurnPow), 23, 1), // selburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curBurnPow), 24, 1), // curburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnStarts), 25, 2), // burnstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnWorkMin), 27, 2), // burnworkmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burn2WorkMin), 29, 2), // burn2workmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatWorkMin), 31, 2), // heatworkmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatStarts), 33, 2), // heatstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(UBAuptime), 35, 2), // ubauptime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(lastCode), 37, 28), // lastcode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(serviceCode), 65, 2), // servicecode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(serviceCodeNumber), 67, 1), // servicecodenumber
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceMessage), 68, 2), // maintenancemessage
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceType), 70, 1), // maintenance
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceTime), 71, 1), // maintenancetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceDate), 72, 6), // maintenancedate
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(emergencyOps), 78, 1), // emergencyops
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(emergencyTemp), 79, 1), // emergencytemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgTotal), 80, 2), // nrgtotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat), 82, 2), // nrgheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterTotal), 84, 2), // metertotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterComp), 86, 2), // metercomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterEHeat), 88, 2), // metereheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterHeat), 90, 2), // meterheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeTotal), 92, 2), // uptimetotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeControl), 94, 2), // uptimecontrol
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompHeating), 96, 2), // uptimecompheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompCooling), 98, 2), // uptimecompcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompPool), 100, 2), // uptimecomppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(totalCompStarts), 102, 2), // totalcompstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingStarts), 104, 2), // heatingstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(coolingStarts), 106, 2), // coolingstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(poolStarts), 108, 2), // poolstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsTotal), 110, 2), // nrgconstotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompTotal), 112, 2), // nrgconscomptotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompHeating), 114, 2), // nrgconscompheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompCooling), 116, 2), // nrgconscompcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompPool), 118, 2), // nrgconscomppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsTotal), 120, 2), // auxelecheatnrgconstotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsHeating), 122, 2), // auxelecheatnrgconsheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsPool), 124, 2), // auxelecheatnrgconspool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppTotal), 126, 2), // nrgsupptotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppHeating), 128, 2), // nrgsuppheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppCooling), 130, 2), // nrgsuppcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppPool), 132, 2), // nrgsupppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPower), 134, 1), // hppower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpMaxPower), 135, 1), // hpmaxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpSetDiffPress), 136, 1), // hpsetdiffpress
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCompOn), 137, 1), // hpcompon
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpActivity), 138, 1), // hpactivity
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrinePumpSpd), 139, 1), // hpbrinepumpspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpSwitchValve), 140, 1), // hpswitchvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCompSpd), 141, 1), // hpcompspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCircSpd), 142, 1), // hpcircspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrineIn), 143, 1), // hpbrinein
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrineOut), 144, 1), // hpbrineout
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc0), 145, 1), // hptc0
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc1), 146, 1), // hptc1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc3), 147, 1), // hptc3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr1), 148, 1), // hptr1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr3), 149, 1), // hptr3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr4), 150, 1), // hptr4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr5), 151, 1), // hptr5
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr6), 152, 1), // hptr6
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr7), 153, 1), // hptr7
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTl2), 154, 1), // hptl2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPl1), 155, 1), // hppl1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPh1), 156, 1), // hpph1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTa4), 157, 1), // hpta4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTw1), 158, 1), // hptw1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(poolSetTemp), 159, 1), // poolsettemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hp4wayValve), 160, 1), // hp4way
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput1), 161, 1), // hpin1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn1Opt), 162, 8), // hpin1opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput2), 170, 1), // hpin2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn2Opt), 171, 8), // hpin2opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput3), 179, 1), // hpin3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn3Opt), 180, 8), // hpin3opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput4), 188, 1), // hpin4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn4Opt), 189, 8), // hpin4opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maxHeatComp), 197, 1), // maxheatcomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maxHeatHeat), 198, 1), // maxheatheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(manDefrost), 199, 1), // mandefrost
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pvCooling), 200, 1), // pvcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterOnly), 201, 1), // auxheateronly
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterOff), 202, 1), // auxheateroff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterStatus), 203, 1), // auxheaterstatus
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterDelay), 204, 1), // auxheaterdelay
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxMaxLimit), 205, 1), // auxmaxlimit
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxLimitStart), 206, 1), // auxlimitstart
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeatMode), 207, 1), // auxheatrmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystHeat), 208, 1), // hphystheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystCool), 209, 1), // hphystcool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystPool), 210, 1), // hphystpool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentMode), 211, 1), // silentmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentFrom), 212, 1), // silentfrom
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentTo), 213, 1), // silentto
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(minTempSilent), 214, 1), // mintempsilent
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempParMode), 215, 1), // tempparmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeatMixValve), 216, 1), // auxheatmix
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempDiffHeat), 217, 1), // tempdiffheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempDiffCool), 218, 1), // tempdiffcool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(vp_cooling), 219, 1), // vpcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatCable), 220, 1), // heatcable
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(VC0valve), 221, 1), // vc0valve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(primePump), 222, 1), // primepump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(primePumpMod), 223, 1), // primepumpmod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hp3wayValve), 224, 1), // hp3way
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep1), 225, 1), // elheatstep1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep2), 226, 1), // elheatstep2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep3), 227, 1), // elheatstep3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpEA0), 228, 1), // hpea0
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPumpMode), 229, 1), // hppumpmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpFan), 230, 1), // fan
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpShutdown), 231, 1), // shutdown
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(exhaustTemp), 232, 1), // exhausttemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnGas), 233, 1), // burngas
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnGas2), 234, 1), // burngas2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(flameCurr), 235, 1), // flamecurr
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(fanWork), 236, 1), // fanwork
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(ignWork), 237, 1), // ignwork
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(oilPreHeat), 238, 1), // oilpreheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMinPower), 239, 1), // burnminpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMaxPower), 240, 1), // burnmaxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMinPeriod), 241, 1), // burnminperiod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(absBurnPow), 242, 1), // absburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatblock), 243, 1), // heatblock
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilHystOn), 244, 1), // boilhyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilHystOff), 245, 1), // boilhystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boil2HystOn), 246, 1), // boil2hyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boil2HystOff), 247, 1), // boil2hystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveOn), 248, 1), // curveon
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveBase), 249, 1), // curvebase
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveEnd), 250, 1), // curveend
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(summertemp), 251, 1), // summertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nofrostmode), 252, 1), // nofrostmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nofrosttemp), 253, 1), // nofrosttemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(gasMeterHeat), 254, 2), // gasmeterheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat2), 256, 2), // nrgheat2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nomPower), 258, 1), // nompower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(netFlowTemp), 259, 1), // netflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatValve), 260, 1), // heatvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(keepWarmTemp), 261, 1), // keepwarmtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(setReturnTemp), 262, 1), // setreturntemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingOn), 263, 1), // heating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw), 0, 2), // nrg
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(meterWw), 2, 2), // meter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(upTimeCompWw), 4, 2), // uptimecomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStarts2), 6, 2), // starts2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgConsCompWw), 8, 2), // nrgconscomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(auxElecHeatNrgConsWw), 10, 2), // auxelecheatnrgcons
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgSuppWw), 12, 2), // nrgsupp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(maxHeatDhw), 14, 1), // maxheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAlternatingOper), 15, 1), // alternatingop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAltOpPrioHeat), 16, 1), // altopprioheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAltOpPrioWw), 17, 1), // altopprio
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfOffTemp), 18, 1), // comfoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoOffTemp), 19, 1), // ecooff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusOffTemp), 20, 1), // ecoplusoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfDiffTemp), 21, 1), // comfdiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoDiffTemp), 22, 1), // ecodiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusDiffTemp), 23, 1), // ecoplusdiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfStopTemp), 24, 1), // comfstop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoStopTemp), 25, 1), // ecostop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusStopTemp), 26, 1), // ecoplusstop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(hpCircPumpWw), 27, 1), // hpcircpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwtapactivated), 28, 1), // tapactivated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSetTemp), 29, 1), // settemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(selRoomTemp), 30, 1), // seltemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempLow), 31, 1), // seltemplow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempEco), 32, 1), // tempecoplus
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempOff), 33, 1), // seltempoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempSingle), 34, 1), // seltempsingle
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSolarTemp), 35, 1), // solartemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwType), 36, 1), // type
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfort), 37, 1), // comfort
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfort1), 38, 1), // comfort1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(flowtempoffset), 39, 1), // flowtempoffset
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargeOptimization), 40, 1), // chargeoptimization
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMaxPower), 41, 1), // maxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMaxTemp), 42, 1), // maxtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircPump), 43, 1), // circpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargeType), 44, 1), // chargetype
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOn), 45, 1), // hyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOff), 46, 1), // hystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfectionTemp), 47, 1), // disinfectiontemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircMode), 48, 1), // circmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCirc), 49, 1), // circ
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp), 50, 1), // curtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp2), 51, 1), // curtemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurFlow), 52, 1), // curflow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp1), 53, 1), // storagetemp1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp2), 54, 1), // storagetemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(activated), 55, 1), // activated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwOneTime), 56, 1), // onetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfecting), 57, 1), // disinfecting
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCharging), 58, 1), // charging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwRecharging), 59, 1), // recharging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwTempOK), 60, 1), // tempok
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwActive), 61, 1), // active
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(ww3wayValve), 62, 1), // 3wayvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSetPumpPower), 63, 1), // setpumppower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMixerTemp), 64, 1), // mixertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(cylMiddleTemp), 65, 1), // cylmiddletemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStarts), 66, 2), // starts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwWorkM), 68, 2), // workm
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(gasMeterWw), 70, 2), // gasmeter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw2), 72, 2), // nrg2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwValve), 74, 1), // dhwvalve
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(errorCode), 0, 8), // errorcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(lastCode), 8, 25), // lastcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 33, 13), // datetime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaCalIntTemperature), 46, 1), // intoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(floordrystatus), 47, 1), // floordry
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(dampedoutdoortemp), 48, 1), // dampedoutdoortemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(floordrytemp), 49, 1), // floordrytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaBuildingType), 50, 1), // building
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaMinExtTemperature), 51, 1), // minexttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(damping), 52, 1), // damping
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(hybridStrategy), 53, 1), // hybridstrategy
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(switchOverTemp), 54, 1), // switchovertemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(energyCostRatio), 55, 1), // energycostratio
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(fossileFactor), 56, 1), // fossilefactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(electricFactor), 57, 1), // electricfactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(delayBoiler), 58, 1), // delayboiler
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempDiffBoiler), 59, 1), // tempdiffboiler
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvEnableWw), 60, 1), // pvenabledhw
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvRaiseHeat), 61, 1), // pvraiseheat
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvLowerCool), 62, 1), // pvlowercool
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaMainDisplay), 63, 1), // display
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaLanguage), 64, 1), // language
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaClockOffset), 65, 1), // clockoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempsensor1), 66, 1), // inttemp1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempsensor2), 67, 1), // inttemp2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(autodst), 68, 1), // autodst
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(backlight), 69, 1), // backlight
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(brightness), 70, 1), // brightness
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(mixingvalves), 71, 1), // mixingvalves
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(heatingPID), 72, 1), // heatingpid
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(preheating), 73, 1), // preheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(vacations), 74, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(selRoomTemp), 0, 1), // seltemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomTemp), 1, 1), // currtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(haclimate), 2, 1), // haclimate
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(mode), 3, 1), // mode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(modetype), 4, 1), // modetype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(ecotemp), 5, 1), // ecotemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(manualtemp), 6, 1), // manualtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(comforttemp), 7, 1), // comforttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summertemp), 8, 1), // summertemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(designtemp), 9, 1), // designtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(offsettemp), 10, 1), // offsettemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(minflowtemp), 11, 1), // minflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(maxflowtemp), 12, 1), // maxflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roominfluence), 13, 1), // roominfluence
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roominfl_factor), 14, 1), // roominflfactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(curroominfl), 15, 1), // curroominfl
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nofrostmode), 16, 1), // nofrostmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nofrosttemp), 17, 1), // nofrosttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(targetflowtemp), 18, 1), // targetflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatingtype), 19, 1), // heatingtype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summersetmode), 20, 1), // summersetmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpoperatingmode), 21, 1), // hpoperatingmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summermode), 22, 1), // summermode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpoperatingstate), 23, 1), // hpoperatingstate
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(controlmode), 24, 1), // controlmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(program), 25, 1), // program
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(tempautotemp), 26, 1), // tempautotemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remoteseltemp), 27, 1), // remoteseltemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(fastheatup), 28, 1), // fastheatup
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchonoptimization), 29, 1), // switchonoptimization
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducemode), 30, 1), // reducemode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(noreducetemp), 31, 1), // noreducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducetemp), 32, 1), // reducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(wwprio), 33, 1), // dhwprio
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpcooling), 34, 1), // cooling
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(coolingOn), 35, 1), // coolingon
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpmode), 36, 1), // hpmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(dewoffset), 37, 1), // dewoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomtempdiff), 38, 1), // roomtempdiff
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpminflowtemp), 39, 1), // hpminflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(control), 40, 1), // control
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remotetemp), 41, 1), // remotetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remotehum), 42, 1), // remotehum
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatondelay), 43, 1), // heatondelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatoffdelay), 44, 1), // heatoffdelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(instantstart), 45, 1), // instantstart
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(boost), 46, 1), // boost
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(boosttime), 47, 1), // boosttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daytemp), 48, 1), // daytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nighttemp2), 49, 1), // nighttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidaytemp), 50, 1), // holidaytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidaymode), 51, 1), // holidaymode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(flowtempoffset), 52, 1), // flowtempoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidays), 53, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations), 66, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(pause), 79, 1), // pause
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(party), 80, 1), // party
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacreducetemp), 81, 1), // vacreducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacreducemode), 82, 1), // vacreducemode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchtime1), 83, 8), // switchtime1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchtime2), 91, 8), // switchtime2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(offtemp), 99, 1), // offtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daylowtemp), 100, 1), // daytemp2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daymidtemp), 101, 1), // daytemp3
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(dayhightemp), 102, 1), // daytemp4
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(wwswitchtime), 103, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations1), 111, 11), // vacations1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations2), 122, 11), // vacations2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations3), 133, 11), // vacations3
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations4), 144, 11), // vacations4
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations5), 155, 11), // vacations5
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations6), 166, 11), // vacations6
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations7), 177, 11), // vacations7
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducehours), 188, 1), // reducehours
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reduceminutes), 189, 1), // reduceminutes
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heattemp), 190, 1), // heattemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomsensor), 191, 1), // roomsensor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatup), 192, 1), // heatup
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(mode), 0, 1), // mode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTemp), 1, 1), // settemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTempLow), 2, 1), // settemplow
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 3, 1), // circmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 4, 1), // chargeduration
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 5, 1), // charge
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 6, 1), // extra
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 7, 1), // disinfecting
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 8, 1), // disinfectday
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 9, 1), // disinfecttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 10, 1), // dailyheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 11, 1), // dailyheattime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 12, 1), // progmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 13, 1), // circprog
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 14, 1), // disinfecthour
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 15, 1), // maxtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 16, 1), // onetimekey
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwswitchtime), 17, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 25, 8), // circswitchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 33, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(vacations), 46, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwWhenModeOff), 59, 1), // whenmodeoff
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempHc), 0, 1), // flowtemphc
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(valveStatus), 1, 1), // valvestatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowSetTemp), 2, 1), // flowsettemp
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(wwPumpStatus), 3, 1), // pumpstatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(activated), 4, 1), // activated
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(mixerSetTime), 5, 1), // valvesettime
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempVf), 6, 1), // flowtempvf
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowtempoffset), 7, 1), // flowtempoffset
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(flowTempHc), 0, 1), // flowtemphc
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(valveStatus), 1, 1), // valvestatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(flowSetTemp), 2, 1), // flowsettemp
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(wwPumpStatus), 3, 1), // pumpstatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(activated), 4, 1), // activated
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(mixerSetTime), 5, 1), // valvesettime
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_DHW, FL_(flowtempoffset), 6, 1), // flowtempoffset
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorTemp), 0, 1), // collectortemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylBottomTemp), 1, 1), // cylbottomtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump), 2, 1), // solarpump
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pumpWorkTime), 3, 2), // pumpworktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylMaxTemp), 5, 1), // cylmaxtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorShutdown), 6, 1), // collectorshutdown
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylHeated), 7, 1), // cylheated
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpMod), 8, 1), // solarpumpmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pumpMinMod), 9, 1), // pumpminmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpTurnonDiff), 10, 1), // turnondiff
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpTurnoffDiff), 11, 1), // turnoffdiff
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPower), 12, 1), // solarpower
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyLastHour), 13, 2), // energylasthour
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(maxFlow), 15, 1), // maxflow
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarIsEnabled), 16, 1), // solarenabled
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylMiddleTemp), 17, 1), // cylmiddletemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(retHeatAssist), 18, 1), // retheatassist
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1Valve), 19, 1), // heatassistvalve
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Temp), 20, 1), // collector2temp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1Power), 21, 1), // heatassistpower
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2), 22, 1), // solarpump2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2Mod), 23, 1), // solarpump2mod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cyl2BottomTemp), 24, 1), // cyl2bottomtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatExchangerTemp), 25, 1), // heatexchangertemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylPumpMod), 26, 1), // cylpumpmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(valveStatus), 27, 1), // valvestatus
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(vs1Status), 28, 1), // vs1status
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorMaxTemp), 29, 1), // collectormaxtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorMinTemp), 30, 1), // collectormintemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyToday), 31, 2), // energytoday
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyTotal), 33, 2), // energytotal
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pump2WorkTime), 35, 2), // pump2worktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1WorkTime), 37, 2), // m1worktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatTransferSystem), 39, 1), // heattransfersystem
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(externalCyl), 40, 1), // externalcyl
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(thermalDisinfect), 41, 1), // thermaldisinfect
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatMetering), 42, 1), // heatmetering
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(activated), 43, 1), // activated
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpMode), 44, 1), // solarpumpmode
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpKick), 45, 1), // pumpkick
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(plainWaterMode), 46, 1), // plainwatermode
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(doubleMatchFlow), 47, 1), // doublematchflow
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pump2MinMod), 48, 1), // pump2minmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2TurnonDiff), 49, 1), // turnondiff2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2TurnoffDiff), 50, 1), // turnoffdiff2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2Kick), 51, 1), // pump2kick
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(climateZone), 52, 1), // climatezone
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector1Area), 53, 1), // collector1area
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector1Type), 54, 1), // collector1type
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Area), 55, 1), // collector2area
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Type), 56, 1), // collector2type
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylPriority), 57, 1), // cylpriority
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCntFlowTemp), 58, 1), // heatcntflowtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCntRetTemp), 59, 1), // heatcntrettemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCnt), 60, 1), // heatcnt
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapFlowTemp), 61, 1), // swapflowtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapRetTemp), 62, 1), // swaprettemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DHW, FL_(wwMinTemp), 0, 1), // mintemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(airHumidity), 0, 1), // airhumidity
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(dewTemperature), 1, 1), // dewtemperature
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(curFlowTemp), 2, 1), // curflowtemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(retTemp), 3, 1), // rettemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(sysRetTemp), 4, 1), // sysrettemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTa4), 5, 1), // hpta4
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr1), 6, 1), // hptr1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr3), 7, 1), // hptr3
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr4), 8, 1), // hptr4
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr5), 9, 1), // hptr5
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr6), 10, 1), // hptr6
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTl2), 11, 1), // hptl2
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpPl1), 12, 1), // hppl1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpPh1), 13, 1), // hpph1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatingPumpMod), 14, 1), // heatingpumpmod
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpCompSpd), 15, 1), // hpcompspd
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hybridStrategy), 16, 1), // hybridstrategy
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseMode), 17, 1), // lownoisemode
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseStart), 18, 1), // lownoisestart
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseStop), 19, 1), // lownoisestop
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hybridDHW), 20, 1), // hybriddhw
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPriceGas), 21, 1), // energypricegas
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPriceEl), 22, 1), // energypriceel
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPricePV), 23, 1), // energyfeedpv
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(switchOverTemp), 24, 1), // switchovertemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(airPurgeMode), 25, 1), // airpurgemode
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatPumpOutput), 26, 1), // heatpumpoutput
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(coolingCircuit), 27, 1), // coolingcircuit
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(compStartMod), 28, 1), // compstartmod
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatDrainPan), 29, 1), // heatdrainpan
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatCable), 30, 1), // heatcable
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(nrgTotal), 31, 2), // nrgtotal
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat), 33, 2), // nrgheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterTotal), 35, 2), // metertotal
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterComp), 37, 2), // metercomp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterEHeat), 39, 2), // metereheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterHeat), 41, 2), // meterheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(nrgWw), 0, 2), // nrg
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(meterWw), 2, 2), // meter
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(activated), 0, 1), // activated
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(flowTempHc), 1, 1), // flowtemphc
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(status), 2, 1), // status
REGISTER_MAPPING(dt::CONTROLLER, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 0, 13), // datetime
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setFlowTemp), 0, 1), // setflowtemp
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setBurnPow), 1, 1), // setburnpow
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(flowTempVf), 0, 1), // flowtempvf
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(input), 1, 1), // input
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(outPower), 2, 1), // outpow
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(setPower), 3, 1), // setpower
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(setPoint), 4, 1), // setpoint
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(minV), 5, 1), // minv
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(maxV), 6, 1), // maxv
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(minT), 7, 1), // mint
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(maxT), 8, 1), // maxt
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(mode), 9, 1), // mode
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(outFresh), 0, 1), // outfresh
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(inFresh), 1, 1), // infresh
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(outEx), 2, 1), // outexhaust
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(inEx), 3, 1), // inexhaust
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventInSpeed), 4, 1), // ventinspeed
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventOutSpeed), 5, 1), // ventoutspeed
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventMode), 6, 1), // ventmode
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(airquality), 7, 1), // airquality
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(airHumidity), 8, 1), // airhumidity
};
} // namespace emsesp

106
src/modbus_test.h Normal file
View File

@@ -0,0 +1,106 @@
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
#ifndef MODBUS_TEST_H
#define MODBUS_TEST_H
#include <cstdint>
#include <vector>
// Mocked ModbusMessage for tests
enum Error : uint8_t {
SUCCESS = 0x00,
ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDRESS = 0x02,
ILLEGAL_DATA_VALUE = 0x03,
SERVER_DEVICE_FAILURE = 0x04,
ACKNOWLEDGE = 0x05,
SERVER_DEVICE_BUSY = 0x06,
NEGATIVE_ACKNOWLEDGE = 0x07,
MEMORY_PARITY_ERROR = 0x08,
GATEWAY_PATH_UNAVAIL = 0x0A,
GATEWAY_TARGET_NO_RESP = 0x0B,
TIMEOUT = 0xE0,
INVALID_SERVER = 0xE1,
CRC_ERROR = 0xE2, // only for Modbus-RTU
FC_MISMATCH = 0xE3,
SERVER_ID_MISMATCH = 0xE4,
PACKET_LENGTH_ERROR = 0xE5,
PARAMETER_COUNT_ERROR = 0xE6,
PARAMETER_LIMIT_ERROR = 0xE7,
REQUEST_QUEUE_FULL = 0xE8,
ILLEGAL_IP_OR_PORT = 0xE9,
IP_CONNECTION_FAILED = 0xEA,
TCP_HEAD_MISMATCH = 0xEB,
EMPTY_MESSAGE = 0xEC,
ASCII_FRAME_ERR = 0xED,
ASCII_CRC_ERR = 0xEE,
ASCII_INVALID_CHAR = 0xEF,
BROADCAST_ERROR = 0xF0,
UNDEFINED_ERROR = 0xFF // otherwise uncovered communication error
};
class ModbusMessage {
public:
ModbusMessage() {
}
ModbusMessage(std::vector<uint8_t> data) {
_data = data;
}
void get(uint16_t index, uint8_t & value) const {
value = _data[index];
}
void get(uint16_t index, uint16_t & value) const {
value = (_data[index] << 8) + _data[index + 1];
}
void get(uint16_t index, std::vector<uint8_t> & data, uint8_t byte_count) const {
for (auto i = 0; i < byte_count; i++)
data.push_back(_data[i + index]);
}
void add(uint8_t value) {
_data.push_back(value);
}
void add(uint16_t value) {
_data.push_back(value >> 8);
_data.push_back(value & 0xff);
}
uint8_t getServerID() const {
return _data[0];
}
uint8_t getFunctionCode() const {
return _data[1];
}
Error getError() const {
if (_data.size() > 2) {
if (_data[1] & 0x80) {
return static_cast<Error>(_data[2]);
}
}
return SUCCESS;
}
Error setError(uint8_t serverID, uint8_t functionCode, Error errorCode) {
_data.reserve(3);
_data.shrink_to_fit();
_data.clear();
add(serverID);
add(static_cast<uint8_t>((functionCode | 0x80) & 0xFF));
add(static_cast<uint8_t>(errorCode));
return SUCCESS;
}
std::vector<uint8_t> _data;
};
#endif
#endif

View File

@@ -396,6 +396,11 @@ void System::reload_settings() {
board_profile_ = settings.board_profile;
telnet_enabled_ = settings.telnet_enabled;
modbus_enabled_ = settings.modbus_enabled;
modbus_port_ = settings.modbus_port;
modbus_max_clients_ = settings.modbus_max_clients;
modbus_timeout_ = settings.modbus_timeout;
rx_gpio_ = settings.rx_gpio;
tx_gpio_ = settings.tx_gpio;
dallas_gpio_ = settings.dallas_gpio;

View File

@@ -120,6 +120,22 @@ class System {
return telnet_enabled_;
}
bool modbus_enabled() {
return modbus_enabled_;
}
uint16_t modbus_port() {
return modbus_port_;
}
uint8_t modbus_max_clients() {
return modbus_max_clients_;
}
uint32_t modbus_timeout() {
return modbus_timeout_;
}
bool analog_enabled() {
return analog_enabled_;
}
@@ -339,6 +355,10 @@ class System {
uint8_t enum_format_;
bool readonly_mode_;
String version_;
bool modbus_enabled_;
uint16_t modbus_port_;
uint8_t modbus_max_clients_;
uint32_t modbus_timeout_;
// ethernet
uint8_t phy_type_;

View File

@@ -1759,6 +1759,215 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
ok = true;
}
if (command == "modbus") {
shell.printfln("Testing Modbus...");
System::test_set_all_active(true);
add_device(0x08, 172); // boiler: Enviline/Compress 6000AW/Hybrid 3000-7000iAW/SupraEco/Geo 5xx/WLW196i
const auto & it = std::find_if(EMSESP::emsdevices.begin(), EMSESP::emsdevices.end(), [&](const std::unique_ptr<EMSdevice> & dev) {
return dev && dev->device_id() == 0x08;
});
if (it == EMSESP::emsdevices.end()) {
EMSESP::logger().err("ERROR - can not find mocked heatpump device");
return;
}
const auto & device = *it;
{
auto test_int8 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("INT8 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("INT8 %s: %d ", shortname.c_str(), (int8_t)modbus_regs[0]);
if ((int8_t)modbus_regs[0] == (int8_t)EMS_VALUE_DEFAULT_INT8_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (int8_t)EMS_VALUE_DEFAULT_INT8_DUMMY, (int8_t)modbus_regs[0]);
}
};
auto test_uint8 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("UINT8 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("UINT8 %s: %d ", shortname.c_str(), (uint8_t)modbus_regs[0]);
if ((uint8_t)modbus_regs[0] == (uint8_t)EMS_VALUE_DEFAULT_UINT8_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint8_t)EMS_VALUE_DEFAULT_UINT8_DUMMY, (uint8_t)modbus_regs[0]);
}
};
auto test_int16 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("INT16 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("INT16 %s: %d ", shortname.c_str(), (int16_t)modbus_regs[0]);
if ((int16_t)modbus_regs[0] == (int16_t)EMS_VALUE_DEFAULT_INT16_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (int16_t)EMS_VALUE_DEFAULT_INT16_DUMMY, (int16_t)modbus_regs[0]);
}
};
auto test_uint16 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("UINT16 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("UINT16 %s: %d ", shortname.c_str(), (uint16_t)modbus_regs[0]);
if ((uint16_t)modbus_regs[0] == (uint16_t)EMS_VALUE_DEFAULT_UINT16_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint16_t)EMS_VALUE_DEFAULT_UINT16_DUMMY, (uint16_t)modbus_regs[0]);
}
};
auto test_uint24 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(2);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("UINT24 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
uint32_t value = ((uint32_t)modbus_regs[0] << 16) | (uint32_t)modbus_regs[1];
shell.printf("UINT24 %s: %d ", shortname.c_str(), value);
if (value == (uint32_t)EMS_VALUE_DEFAULT_UINT24_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint32_t)EMS_VALUE_DEFAULT_UINT24_DUMMY, value);
}
};
/* there seem to be no uint32 entities to run this test on.
auto test_uint32 = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(2);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("UINT32 %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
uint32_t value = ((uint32_t)modbus_regs[0] << 16) | (uint32_t)modbus_regs[1];
shell.printf("UINT32 %s: %d ", shortname.c_str(), value);
if (value == (uint32_t)EMS_VALUE_DEFAULT_UINT32_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint32_t)EMS_VALUE_DEFAULT_UINT32_DUMMY, value);
}
};
*/
auto test_bool = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("BOOL %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("BOOL %s: %d ", shortname.c_str(), (uint8_t)modbus_regs[0]);
if ((uint8_t)modbus_regs[0] == (uint8_t)EMS_VALUE_DEFAULT_BOOL_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint8_t)EMS_VALUE_DEFAULT_BOOL_DUMMY, (uint8_t)modbus_regs[0]);
}
};
auto test_enum = [&](const std::unique_ptr<EMSdevice> & device, uint8_t tag, const std::string & shortname) {
std::vector<uint16_t> modbus_regs(1);
if (auto result = device->get_modbus_value(tag, shortname, modbus_regs)) {
shell.printf("ENUM %s FAILED (ERROR %d)\n", shortname.c_str(), result);
} else {
shell.printf("ENUM %s: %d ", shortname.c_str(), (uint8_t)modbus_regs[0]);
if ((uint8_t)modbus_regs[0] == (uint8_t)EMS_VALUE_DEFAULT_ENUM_DUMMY)
shell.printfln("[OK]");
else
shell.printfln("[ERROR] - expected %d, got %d", (uint8_t)EMS_VALUE_DEFAULT_ENUM_DUMMY, (uint8_t)modbus_regs[0]);
}
};
shell.println();
shell.printfln("Testing device->get_modbus_value():");
test_int8(device, DeviceValueTAG::TAG_DEVICE_DATA, "mintempsilent");
test_uint8(device, DeviceValueTAG::TAG_DEVICE_DATA, "selflowtemp");
test_int16(device, DeviceValueTAG::TAG_DEVICE_DATA, "outdoortemp");
test_uint16(device, DeviceValueTAG::TAG_DEVICE_DATA, "rettemp");
// test_uint32(device, DeviceValueTAG::TAG_DEVICE_DATA, "heatstarts"); // apparently there are no uint32 entities?
test_uint24(device, DeviceValueTAG::TAG_DEVICE_DATA, "heatstarts");
test_bool(device, DeviceValueTAG::TAG_DEVICE_DATA, "heatingactivated");
test_enum(device, DeviceValueTAG::TAG_DEVICE_DATA, "pumpmode");
}
// modbus_value_to_json
{
shell.println();
shell.printfln("Testing device->modbus_value_to_json():");
std::vector<uint8_t> modbus_bytes(2);
JsonDocument input;
JsonObject inputObject = input.to<JsonObject>();
modbus_bytes[0] = 0;
modbus_bytes[1] = EMS_VALUE_DEFAULT_UINT8_DUMMY;
device->modbus_value_to_json(DeviceValueTAG::TAG_DEVICE_DATA, "selflowtemp", modbus_bytes, inputObject);
std::string jsonString;
serializeJson(inputObject, jsonString);
shell.printf("UINT8 %s: %s (%d) ", "selflowtemp", jsonString.c_str(), inputObject["value"].as<int>());
if (inputObject["value"] == (uint8_t)EMS_VALUE_DEFAULT_UINT8_DUMMY)
shell.println("[OK]");
else
shell.println("[ERROR]");
}
// handleRead
{
shell.println();
shell.printfln("Testing modbus->handleRead():");
uint16_t reg = Modbus::REGISTER_BLOCK_SIZE * DeviceValueTAG::TAG_DEVICE_DATA + 209; // mintempsilent is tag 2 (TAG_DEVICE_DATA), offset 209
ModbusMessage request({device->device_type(), 0x03, static_cast<unsigned char>(reg >> 8), static_cast<unsigned char>(reg & 0xff), 0, 1});
auto response = EMSESP::modbus_->handleRead(request);
if (response.getError() == SUCCESS) {
shell.print("mintempsilent MODBUS response:");
for (const auto & d : response._data) {
shell.printf(" %d", d);
}
if (response._data.size() == 5 && response._data[3] == 0 && response._data[4] == (uint8_t)EMS_VALUE_DEFAULT_INT8_DUMMY) {
shell.printf(" [OK]");
} else {
shell.printf(" [ERROR - invalid response]");
}
shell.println();
} else {
shell.printf("mintempsilent [MODBUS ERROR %d]\n", response.getError());
}
}
// handleWrite
{
shell.println();
shell.printfln("Testing modbus->handleWrite():");
uint16_t reg = Modbus::REGISTER_BLOCK_SIZE * DeviceValueTAG::TAG_DEVICE_DATA + 4; // selflowtemp is tag 2 (TAG_DEVICE_DATA), offset 4
ModbusMessage request({device->device_type(), 0x06, static_cast<unsigned char>(reg >> 8), static_cast<unsigned char>(reg & 0xff), 0, 1, 2, 0, 45});
auto response = EMSESP::modbus_->handleWrite(request);
if (response.getError() == SUCCESS) {
shell.print("selflowtemp MODBUS response:");
for (const auto & d : response._data) {
shell.printf(" %d", d);
}
shell.println(" [OK]");
} else {
shell.printf("selflowtemp [MODBUS ERROR %d]\n", response.getError());
}
}
ok = true;
}
if (command == "poll2") {
shell.printfln("Testing Tx Sending last message on queue...");

View File

@@ -77,6 +77,10 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["eth_clock_mode"] = settings.eth_clock_mode;
String platform = EMSESP_PLATFORM;
root["platform"] = (platform == "ESP32" && EMSESP::system_.PSram()) ? "ESP32R" : platform;
root["modbus_enabled"] = settings.modbus_enabled;
root["modbus_port"] = settings.modbus_port;
root["modbus_max_clients"] = settings.modbus_max_clients;
root["modbus_timeout"] = settings.modbus_timeout;
}
// call on initialization and also when settings are updated via web or console
@@ -272,6 +276,22 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
settings.low_clock = root["low_clock"];
check_flag(prev, settings.low_clock, ChangeFlags::RESTART);
prev = settings.modbus_enabled;
settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED;
check_flag(prev, settings.modbus_enabled, ChangeFlags::RESTART);
prev = settings.modbus_port;
settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT;
check_flag(prev, settings.modbus_port, ChangeFlags::RESTART);
prev = settings.modbus_max_clients;
settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS;
check_flag(prev, settings.modbus_max_clients, ChangeFlags::RESTART);
prev = settings.modbus_timeout;
settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT;
check_flag(prev, settings.modbus_timeout, ChangeFlags::RESTART);
//
// these may need mqtt restart to rebuild HA discovery topics
//

View File

@@ -68,6 +68,10 @@ class WebSettings {
uint8_t weblog_buffer;
bool weblog_compact;
bool fahrenheit;
bool modbus_enabled;
uint16_t modbus_port;
uint8_t modbus_max_clients;
uint32_t modbus_timeout;
uint8_t phy_type;
int8_t eth_power; // -1 means disabled