mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
feat: add Modbus support
This commit is contained in:
26
lib/eModbus/README.md
Normal file
26
lib/eModbus/README.md
Normal 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!**
|
||||
|
||||

|
||||
|
||||
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
257
lib/eModbus/keywords.txt
Normal 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
46
lib/eModbus/library.json
Normal 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"
|
||||
]
|
||||
}
|
||||
9
lib/eModbus/library.properties
Normal file
9
lib/eModbus/library.properties
Normal 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
7
lib/eModbus/license.md
Normal 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.
|
||||
554
lib/eModbus/src/CoilData.cpp
Normal file
554
lib/eModbus/src/CoilData.cpp
Normal 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
122
lib/eModbus/src/CoilData.h
Normal 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
|
||||
68
lib/eModbus/src/Logging.cpp
Normal file
68
lib/eModbus/src/Logging.cpp
Normal 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
181
lib/eModbus/src/Logging.h
Normal 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
|
||||
|
||||
21
lib/eModbus/src/ModbusBridgeEthernet.h
Normal file
21
lib/eModbus/src/ModbusBridgeEthernet.h
Normal 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
|
||||
14
lib/eModbus/src/ModbusBridgeRTU.h
Normal file
14
lib/eModbus/src/ModbusBridgeRTU.h
Normal 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
|
||||
199
lib/eModbus/src/ModbusBridgeTemp.h
Normal file
199
lib/eModbus/src/ModbusBridgeTemp.h
Normal 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
|
||||
18
lib/eModbus/src/ModbusBridgeWiFi.h
Normal file
18
lib/eModbus/src/ModbusBridgeWiFi.h
Normal 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
|
||||
103
lib/eModbus/src/ModbusClient.cpp
Normal file
103
lib/eModbus/src/ModbusClient.cpp
Normal 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;
|
||||
}
|
||||
119
lib/eModbus/src/ModbusClient.h
Normal file
119
lib/eModbus/src/ModbusClient.h
Normal 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
|
||||
349
lib/eModbus/src/ModbusClientRTU.cpp
Normal file
349
lib/eModbus/src/ModbusClientRTU.cpp
Normal 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
|
||||
111
lib/eModbus/src/ModbusClientRTU.h
Normal file
111
lib/eModbus/src/ModbusClientRTU.h
Normal 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
|
||||
428
lib/eModbus/src/ModbusClientTCP.cpp
Normal file
428
lib/eModbus/src/ModbusClientTCP.cpp
Normal 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
|
||||
195
lib/eModbus/src/ModbusClientTCP.h
Normal file
195
lib/eModbus/src/ModbusClientTCP.h
Normal 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
|
||||
401
lib/eModbus/src/ModbusClientTCPasync.cpp
Normal file
401
lib/eModbus/src/ModbusClientTCPasync.cpp
Normal 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;
|
||||
}
|
||||
158
lib/eModbus/src/ModbusClientTCPasync.h
Normal file
158
lib/eModbus/src/ModbusClientTCPasync.h
Normal 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
|
||||
138
lib/eModbus/src/ModbusError.h
Normal file
138
lib/eModbus/src/ModbusError.h
Normal 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
|
||||
705
lib/eModbus/src/ModbusMessage.cpp
Normal file
705
lib/eModbus/src/ModbusMessage.cpp
Normal 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 };
|
||||
216
lib/eModbus/src/ModbusMessage.h
Normal file
216
lib/eModbus/src/ModbusMessage.h
Normal 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
|
||||
175
lib/eModbus/src/ModbusServer.cpp
Normal file
175
lib/eModbus/src/ModbusServer.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
86
lib/eModbus/src/ModbusServer.h
Normal file
86
lib/eModbus/src/ModbusServer.h
Normal 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
|
||||
19
lib/eModbus/src/ModbusServerEthernet.h
Normal file
19
lib/eModbus/src/ModbusServerEthernet.h
Normal 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
|
||||
260
lib/eModbus/src/ModbusServerRTU.cpp
Normal file
260
lib/eModbus/src/ModbusServerRTU.cpp
Normal 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
|
||||
92
lib/eModbus/src/ModbusServerRTU.h
Normal file
92
lib/eModbus/src/ModbusServerRTU.h
Normal 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
|
||||
267
lib/eModbus/src/ModbusServerTCPasync.cpp
Normal file
267
lib/eModbus/src/ModbusServerTCPasync.cpp
Normal 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());
|
||||
}
|
||||
92
lib/eModbus/src/ModbusServerTCPasync.h
Normal file
92
lib/eModbus/src/ModbusServerTCPasync.h
Normal 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
|
||||
433
lib/eModbus/src/ModbusServerTCPtemp.h
Normal file
433
lib/eModbus/src/ModbusServerTCPtemp.h
Normal 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
|
||||
16
lib/eModbus/src/ModbusServerWiFi.h
Normal file
16
lib/eModbus/src/ModbusServerWiFi.h
Normal 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
|
||||
66
lib/eModbus/src/ModbusTypeDefs.cpp
Normal file
66
lib/eModbus/src/ModbusTypeDefs.cpp
Normal 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
|
||||
139
lib/eModbus/src/ModbusTypeDefs.h
Normal file
139
lib/eModbus/src/ModbusTypeDefs.h
Normal 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
|
||||
467
lib/eModbus/src/RTUutils.cpp
Normal file
467
lib/eModbus/src/RTUutils.cpp
Normal 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
|
||||
76
lib/eModbus/src/RTUutils.h
Normal file
76
lib/eModbus/src/RTUutils.h
Normal 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
61
lib/eModbus/src/options.h
Normal 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
|
||||
Reference in New Issue
Block a user