diff --git a/IO_Modbus.cpp b/IO_Modbus.cpp new file mode 100644 index 0000000..4c7df51 --- /dev/null +++ b/IO_Modbus.cpp @@ -0,0 +1,114 @@ +/* + * © 2024, Travis Farmer. All rights reserved. + * + * This file is part of DCC++EX API + * + * This is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * It is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CommandStation. If not, see . + */ + +#include "IO_Modbus.h" +#include "defines.h" + + +/************************************************************ + * Modbus implementation + ************************************************************/ + +// Constructor for Modbus +Modbus::Modbus(uint8_t busNo, HardwareSerial serial, unsigned long baud, uint16_t cycleTimeMS, int16_t transmitEnablePin) { + _busNo = busNo; + _baud = baud; + _serial = &serial; + _cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds. + _transmitEnablePin = transmitEnablePin; + if (_transmitEnablePin != VPIN_NONE) { + pinMode(_transmitEnablePin, OUTPUT); + ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); // transmitter initially off + } + ModbusRTUMaster modbusmaster(*_serial, _transmitEnablePin); + + // Add device to HAL device chain + IODevice::addDevice(this); + + // Add bus to CMRIbus chain. + _nextBus = _busList; + _busList = this; + const char* errorStrings[] = {"success", "invalid id", "invalid buffer", "invalid quantity", "response timeout", "frame error", "crc error", "unknown comm error", "unexpected id", "exception response", "unexpected function code", "unexpected response length", "unexpected byte count", "unexpected address", "unexpected value", "unexpected quantity"}; +} + + +// Main loop function for CMRIbus. +// Work through list of nodes. For each node, in separate loop entries +// send initialisation message (once only); then send +// output message; then send prompt for input data, and +// process any response data received. +// When the slot time has finished, move on to the next device. +void Modbus::_loop(unsigned long currentMicros) { + + _currentMicros = currentMicros; + if (_currentNode == NULL) { + // If we're between read/write cycles then don't do anything else. + if (_currentMicros - _cycleStartTime < _cycleTime) return; + // ... otherwise start processing the first node in the list + DIAG(F("Modbusnode: 138 _nodeListEnd:%d "), _nodeListEnd); + DIAG(F("Modbusnode: 139 _currentNode:%d "), _currentNode); + _currentNode = _nodeListStart; + DIAG(F("Modbusnode: 141 _currentNode:%d "), _currentNode); + _cycleStartTime = _currentMicros; + } + if (_currentNode == NULL) return; + + uint8_t error; + error = modbusmaster->writeMultipleCoils(_currentNode->getNodeID(), 0, _currentNode->coils, _currentNode->getNumCoils()); + if (error != 0) DIAG(F("%02d %04d %04d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumCoils(), errorStrings[error]); + + error = modbusmaster->readDiscreteInputs(_currentNode->getNodeID(), 0, _currentNode->discreteInputs, _currentNode->getNumDisInputs()); + if (error != 0) DIAG(F("%02d %04d %04d %s"), _currentNode->getNodeID(), 0, _currentNode->getNumDisInputs(), errorStrings[error]); +} + +// Link to chain of CMRI bus instances +Modbus *Modbus::_busList = NULL; + + +/************************************************************ + * Modbusnode implementation + ************************************************************/ + +// Constructor for Modbusnode object +Modbusnode::Modbusnode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils, uint8_t numDiscreteInputs) { + _firstVpin = firstVpin; + _nPins = nPins; + _busNo = busNo; + _nodeID = nodeID; + coils[numCoils]; + discreteInputs[numDiscreteInputs]; + + if ((unsigned int)_nPins < numDiscreteInputs + numCoils) + DIAG(F("Modbusnode: bus:%d nodeID:%d WARNING number of Vpins does not cover all inputs and outputs"), _busNo, _nodeID); + + if (!discreteInputs || !coils) { + DIAG(F("Modbusnode: ERROR insufficient memory")); + return; + } + + // Add this device to HAL device list + IODevice::addDevice(this); + + // Add Modbusnode to Modbus object. + Modbus *bus = Modbus::findBus(_busNo); + if (bus != NULL) { + bus->addNode(this); + return; + } +} diff --git a/IO_Modbus.h b/IO_Modbus.h new file mode 100644 index 0000000..4933eeb --- /dev/null +++ b/IO_Modbus.h @@ -0,0 +1,255 @@ +/* + * © 2024, Travis Farmer. All rights reserved. + * + * This file is part of DCC++EX API + * + * This is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * It is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CommandStation. If not, see . + */ + +/* + * Modbus + * ======= + * To define a Modbus, example syntax: + * Modbus::create(bus, serial, baud[, cycletime[, pin]]); + * + * bus = 0-255 + * serial = serial port to be used (e.g. Serial3) + * baud = baud rate (9600, 19200, 28800, 57600 or 115200) + * cycletime = minimum time between successive updates/reads of a node in millisecs (default 500ms) + * pin = pin number connected to RS485 module's DE and !RE terminals for half-duplex operation (default VPIN_NONE) + * + * Each bus must use a different serial port. + * + * ModbusNode + * ======== + * To define a CMRI node and associate it with a CMRI bus, + * CMRInode::create(firstVPIN, numVPINs, bus, nodeID, type [, inputs, outputs]); + * + * firstVPIN = first vpin in block allocated to this device + * numVPINs = number of vpins (e.g. 72 for an SMINI node) + * bus = 0-255 + * nodeID = 0-127 + * numDiscreteInputs = number of discrete inputs + * numCoils = number of coils + * + * Reference: "LCS-9.10.1 + * Layout Control Specification: CMRInet Protocol + * Version 1.1 December 2014." + */ + +#ifndef IO_MODBUS_H +#define IO_MODBUS_H + +#include "IODevice.h" +#include "ModbusRTUMaster.h" +/********************************************************************** + * Modbusnode class + * + * This encapsulates the state associated with a single Modbus node, + * which includes the nodeID, number of discrete inputs and coils, and + * the states of the discrete inputs and coils. + **********************************************************************/ +class Modbusnode : public IODevice { +private: + uint8_t _busNo; + uint8_t _nodeID; + char _type; + Modbusnode *_next = NULL; + bool _initialised = false; + uint8_t numCoils; + uint8_t numDiscreteInputs; + +public: + static void create(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils=0, uint8_t numDiscreteInputs=0) { + if (checkNoOverlap(firstVpin, nPins)) new Modbusnode(firstVpin, nPins, busNo, nodeID, numCoils, numDiscreteInputs); + } + Modbusnode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, uint8_t numCoils=0, uint8_t numDiscreteInputs=0); + bool *coils; + bool *discreteInputs; + + uint8_t getNodeID() { + return _nodeID; + } + uint8_t getNumCoils() { + return numCoils; + } + uint8_t getNumDisInputs() { + return numDiscreteInputs; + } + + Modbusnode *getNext() { + return _next; + } + void setNext(Modbusnode *node) { + _next = node; + } + bool isInitialised() { + return _initialised; + } + void setInitialised() { + _initialised = true; + } + + void _begin() { + _initialised = false; + } + + int _read(VPIN vpin) { + // Return current state from this device + uint16_t pin = vpin - _firstVpin; + if (pin < numDiscreteInputs) { + return discreteInputs[pin]; + } else + return 0; + } + + void _write(VPIN vpin, int value) { + // Update current state for this device, in preparation the bus transmission + uint16_t pin = vpin - _firstVpin - numCoils; + if (pin < numCoils) { + if (value) + coils[pin] = value; + else + coils[pin]; + } + } + + void saveIncomingData(uint8_t index, uint8_t data) { + if (index < numDiscreteInputs) + discreteInputs[index] = data; + } + + uint8_t getOutputStates(uint8_t index) { + if (index < numCoils) + return coils[index]; + else + return 0; + } + + uint16_t getNumInputs() { + return numDiscreteInputs; + } + + uint16_t getNumOutputs() { + return numCoils; + } + + char getType() { + return _type; + } + + uint8_t getBusNumber() { + return _busNo; + } + + void _display() override { + DIAG(F("Modbusnode type:'%c' configured on bus:%d nodeID:%d VPINs:%u-%u (in) %u-%u (out)"), + _type, _busNo, _nodeID, _firstVpin, _firstVpin+numDiscreteInputs-1, + _firstVpin+numDiscreteInputs, _firstVpin+numDiscreteInputs+numCoils-1); + } + +}; + +/********************************************************************** + * Modbus class + * + * This encapsulates the properties state of the bus and the + * transmission and reception of data across that bus. Each Modbus + * object owns a set of Modbusnode objects which represent the nodes + * attached to that bus. + **********************************************************************/ +class Modbus : public IODevice { +private: + // Here we define the device-specific variables. + uint8_t _busNo; + + unsigned long _baud; + int16_t _transmitEnablePin = VPIN_NONE; + Modbusnode *_nodeListStart = NULL, *_nodeListEnd = NULL; + Modbusnode *_currentNode = NULL; + + uint16_t _receiveDataIndex = 0; // Index of next data byte to be received. + Modbus *_nextBus = NULL; // Pointer to next bus instance in list. + unsigned long _cycleStartTime = 0; + unsigned long _timeoutStart = 0; + unsigned long _cycleTime; // target time between successive read/write cycles, microseconds + unsigned long _timeoutPeriod; // timeout on read responses, in microseconds. + unsigned long _currentMicros; // last value of micros() from _loop function. + unsigned long _postDelay; // delay time after transmission before switching off transmitter (in us) + unsigned long _byteTransmitTime; // time in us for transmission of one byte + + static Modbus *_busList; // linked list of defined bus instances + +public: + static void create(uint8_t busNo, HardwareSerial& serial, unsigned long baud, uint16_t cycleTimeMS=500, int16_t transmitEnablePin=VPIN_NONE) { + new Modbus(busNo, serial, baud, cycleTimeMS, transmitEnablePin); + } + HardwareSerial *_serial; + ModbusRTUMaster *modbusmaster; + const char* errorStrings[]; + // Device-specific initialisation + void _begin() override { + _serial->begin(_baud, SERIAL_8N1); + modbusmaster->begin(_baud); + #if defined(DIAG_IO) + _display(); + #endif + } + + // Loop function (overriding IODevice::_loop(unsigned long)) + void _loop(unsigned long currentMicros) override; + + // Display information about the device + void _display() override { + DIAG(F("Modbus %d configured, speed=%d baud, cycle=%d ms"), _busNo, _baud, _cycleTime/1000); + } + + // Locate Modbusnode object with specified nodeID. + Modbusnode *findNode(uint8_t nodeID) { + for (Modbusnode *node = _nodeListStart; node != NULL; node = node->getNext()) { + if (node->getNodeID() == nodeID) + return node; + } + return NULL; + } + + // Add new Modbusnode to the list of nodes for this bus. + void addNode(Modbusnode *newNode) { + if (!_nodeListStart) + _nodeListStart = newNode; + if (!_nodeListEnd) + _nodeListEnd = newNode; + else + _nodeListEnd->setNext(newNode); + DIAG(F("bus: 260h nodeID: _nodeListStart:%d _nodeListEnd:%d"), _nodeListStart, _nodeListEnd); + } + +protected: + Modbus(uint8_t busNo, HardwareSerial serial, unsigned long baud, uint16_t cycleTimeMS, int16_t transmitEnablePin); + +public: + + uint8_t getBusNumber() { + return _busNo; + } + + static Modbus *findBus(uint8_t busNo) { + for (Modbus *bus=_busList; bus!=NULL; bus=bus->_nextBus) { + if (bus->_busNo == busNo) return bus; + } + return NULL; + } +}; + +#endif // IO_MODBUS_H diff --git a/ModbusADU.cpp b/ModbusADU.cpp new file mode 100644 index 0000000..b7752b2 --- /dev/null +++ b/ModbusADU.cpp @@ -0,0 +1,153 @@ +#include "ModbusADU.h" + +void ModbusADU::setTransactionId(uint16_t transactionId) { + _setRegister(tcp, 0, transactionId); +} + +void ModbusADU::setProtocolId(uint16_t protocolId) { + _setRegister(tcp, 2, protocolId); +} + +void ModbusADU::setLength(uint16_t length) { + if (length < 3 || length > 254) _setRegister(tcp, 4, 0); + else _setRegister(tcp, 4, length); +} + +void ModbusADU::setUnitId(uint8_t unitId) { + tcp[6] = unitId; +} + +void ModbusADU::setFunctionCode(uint8_t functionCode) { + pdu[0] = functionCode; +} + +void ModbusADU::setDataRegister(uint8_t index, uint16_t value) { + _setRegister(data, index, value); +} + + + +void ModbusADU::setRtuLen(uint16_t rtuLen) { + setLength(rtuLen - 2); +} + +void ModbusADU::setTcpLen(uint16_t tcpLen) { + setLength(tcpLen - 6); +} + +void ModbusADU::setPduLen(uint16_t pduLen) { + setLength(pduLen + 1); +} + +void ModbusADU::setDataLen(uint16_t dataLen) { + setLength(dataLen + 2); +} + + + +uint16_t ModbusADU::getTransactionId() { + return _getRegister(tcp, 0); +} + +uint16_t ModbusADU::getProtocolId() { + return _getRegister(tcp, 2); +} + +uint16_t ModbusADU::getLength() { + uint16_t length = _getRegister(tcp, 4); + if (length < 3 || length > 254) return 0; + else return length; +} + +uint8_t ModbusADU::getUnitId() { + return tcp[6]; +} + +uint8_t ModbusADU::getFunctionCode() { + return pdu[0]; +} + +uint16_t ModbusADU::getDataRegister(uint8_t index) { + return _getRegister(data, index); +} + + + +uint16_t ModbusADU::getRtuLen() { + uint16_t len = getLength(); + if (len == 0) return 0; + else return len + 2; +} + +uint16_t ModbusADU::getTcpLen() { + uint16_t len = getLength(); + if (len == 0) return 0; + else return len + 6; +} + +uint16_t ModbusADU::getPduLen() { + uint16_t len = getLength(); + if (len == 0) return 0; + else return len - 1; +} + +uint16_t ModbusADU::getDataLen() { + uint16_t len = getLength(); + if (len == 0) return 0; + else return len - 2; +} + + + +void ModbusADU::updateCrc() { + uint16_t len = getLength(); + uint16_t crc = _calculateCrc(len); + rtu[len] = lowByte(crc); + rtu[len + 1] = highByte(crc); +} + +bool ModbusADU::crcGood() { + uint16_t len = getLength(); + uint16_t aduCrc = rtu[len] | (rtu[len + 1] << 8); + uint16_t calculatedCrc = _calculateCrc(len); + if (aduCrc == calculatedCrc) return true; + else return false; +} + + + +void ModbusADU::prepareExceptionResponse(uint8_t exceptionCode) { + pdu[0] |= 0x80; + pdu[1] = exceptionCode; + setPduLen(2); +} + + + +void ModbusADU::_setRegister(uint8_t *buf, uint16_t index, uint16_t value) { + buf[index] = highByte(value); + buf[index + 1] = lowByte(value); +} + +uint16_t ModbusADU::_getRegister(uint8_t *buf, uint16_t index) { + return (buf[index] << 8) | buf[index + 1]; +} + +uint16_t ModbusADU::_calculateCrc(uint16_t len) { + uint16_t value = 0xFFFF; + for (uint16_t i = 0; i < len; i++) { + value ^= (uint16_t)rtu[i]; + for (uint8_t j = 0; j < 8; j++) { + bool lsb = value & 1; + value >>= 1; + if (lsb == true) value ^= 0xA001; + } + } + return value; +} + + + +uint16_t div8RndUp(uint16_t value) { + return (value + 7) >> 3; +} \ No newline at end of file diff --git a/ModbusADU.h b/ModbusADU.h new file mode 100644 index 0000000..2aa72d9 --- /dev/null +++ b/ModbusADU.h @@ -0,0 +1,52 @@ +#ifndef ModbusADU_h +#define ModbusADU_h + +#include "Arduino.h" + +class ModbusADU { + public: + uint8_t *rtu = _adu + 6; + uint8_t *tcp = _adu; + uint8_t *pdu = _adu + 7; + uint8_t *data = _adu + 8; + + void setTransactionId(uint16_t transactionId); + void setProtocolId(uint16_t protocolId); + void setLength(uint16_t length); + void setUnitId(uint8_t unitId); + void setFunctionCode(uint8_t functionCode); + void setDataRegister(uint8_t index, uint16_t value); + + void setRtuLen(uint16_t rtuLen); + void setTcpLen(uint16_t tcpLen); + void setPduLen(uint16_t pduLen); + void setDataLen(uint16_t dataLen); + + uint16_t getTransactionId(); + uint16_t getProtocolId(); + uint16_t getLength(); + uint8_t getUnitId(); + uint8_t getFunctionCode(); + uint16_t getDataRegister(uint8_t index); + + uint16_t getRtuLen(); + uint16_t getTcpLen(); + uint16_t getPduLen(); + uint16_t getDataLen(); + + void updateCrc(); + bool crcGood(); + + void prepareExceptionResponse(uint8_t exceptionCode); + + private: + uint8_t _adu[262]; + void _setRegister(uint8_t *buf, uint16_t index, uint16_t value); + uint16_t _getRegister(uint8_t *buf, uint16_t index); + uint16_t _calculateCrc(uint16_t len); + +}; + +uint16_t div8RndUp(uint16_t value); + +#endif \ No newline at end of file diff --git a/ModbusRTUComm.cpp b/ModbusRTUComm.cpp new file mode 100644 index 0000000..2abbe96 --- /dev/null +++ b/ModbusRTUComm.cpp @@ -0,0 +1,98 @@ +#include "ModbusRTUComm.h" + +ModbusRTUComm::ModbusRTUComm(Stream& serial, int8_t dePin, int8_t rePin) : _serial(serial) { + _dePin = dePin; + _rePin = rePin; +} + +void ModbusRTUComm::begin(unsigned long baud, uint32_t config) { + unsigned long bitsPerChar; + switch (config) { + case SERIAL_8E2: + case SERIAL_8O2: + bitsPerChar = 12; + break; + case SERIAL_8N2: + case SERIAL_8E1: + case SERIAL_8O1: + bitsPerChar = 11; + break; + case SERIAL_8N1: + default: + bitsPerChar = 10; + break; + } + if (baud <= 19200) { + _charTimeout = (bitsPerChar * 2500000) / baud; + _frameTimeout = (bitsPerChar * 4500000) / baud; + } + else { + _charTimeout = (bitsPerChar * 1000000) / baud + 750; + _frameTimeout = (bitsPerChar * 1000000) / baud + 1750; + } + #if defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_GIGA) || (defined(ARDUINO_NANO_RP2040_CONNECT) && defined(ARDUINO_ARCH_MBED)) + _postDelay = ((bitsPerChar * 1000000) / baud) + 2; + #endif + if (_dePin >= 0) { + pinMode(_dePin, OUTPUT); + digitalWrite(_dePin, LOW); + } + if (_rePin >= 0) { + pinMode(_rePin, OUTPUT); + digitalWrite(_rePin, LOW); + } + clearRxBuffer(); +} + +void ModbusRTUComm::setTimeout(unsigned long timeout) { + _readTimeout = timeout; +} + +ModbusRTUCommError ModbusRTUComm::readAdu(ModbusADU& adu) { + adu.setRtuLen(0); + unsigned long startMillis = millis(); + while (!_serial.available()) { + if (millis() - startMillis >= _readTimeout) return MODBUS_RTU_COMM_TIMEOUT; + } + uint16_t len = 0; + unsigned long startMicros = micros(); + do { + if (_serial.available()) { + startMicros = micros(); + adu.rtu[len] = _serial.read(); + len++; + } + } while (micros() - startMicros <= _charTimeout && len < 256); + adu.setRtuLen(len); + while (micros() - startMicros < _frameTimeout); + if (_serial.available()) { + adu.setRtuLen(0); + return MODBUS_RTU_COMM_FRAME_ERROR; + } + if (!adu.crcGood()) { + adu.setRtuLen(0); + return MODBUS_RTU_COMM_CRC_ERROR; + } + return MODBUS_RTU_COMM_SUCCESS; +} + +void ModbusRTUComm::writeAdu(ModbusADU& adu) { + adu.updateCrc(); + if (_dePin >= 0) digitalWrite(_dePin, HIGH); + if (_rePin >= 0) digitalWrite(_rePin, HIGH); + _serial.write(adu.rtu, adu.getRtuLen()); + _serial.flush(); + delayMicroseconds(_postDelay); + if (_dePin >= 0) digitalWrite(_dePin, LOW); + if (_rePin >= 0) digitalWrite(_rePin, LOW); +} + +void ModbusRTUComm::clearRxBuffer() { + unsigned long startMicros = micros(); + do { + if (_serial.available() > 0) { + startMicros = micros(); + _serial.read(); + } + } while (micros() - startMicros < _frameTimeout); +} diff --git a/ModbusRTUComm.h b/ModbusRTUComm.h new file mode 100644 index 0000000..cae7234 --- /dev/null +++ b/ModbusRTUComm.h @@ -0,0 +1,33 @@ +#ifndef ModbusRTUComm_h +#define ModbusRTUComm_h + +#include "Arduino.h" +#include "ModbusADU.h" + +enum ModbusRTUCommError : uint8_t { + MODBUS_RTU_COMM_SUCCESS = 0, + MODBUS_RTU_COMM_TIMEOUT = 1, + MODBUS_RTU_COMM_FRAME_ERROR = 2, + MODBUS_RTU_COMM_CRC_ERROR = 3 +}; + +class ModbusRTUComm { + public: + ModbusRTUComm(Stream& serial, int8_t dePin = -1, int8_t rePin = -1); + void begin(unsigned long baud, uint32_t config = SERIAL_8N1); + void setTimeout(unsigned long timeout); + ModbusRTUCommError readAdu(ModbusADU& adu); + void writeAdu(ModbusADU& adu); + void clearRxBuffer(); + + private: + Stream& _serial; + int8_t _dePin; + int8_t _rePin; + unsigned long _charTimeout; + unsigned long _frameTimeout; + unsigned long _postDelay = 0; + unsigned long _readTimeout = 0; +}; + +#endif \ No newline at end of file diff --git a/ModbusRTUMaster.cpp b/ModbusRTUMaster.cpp new file mode 100644 index 0000000..897ce5a --- /dev/null +++ b/ModbusRTUMaster.cpp @@ -0,0 +1,216 @@ +#include "ModbusRTUMaster.h" + +ModbusRTUMaster::ModbusRTUMaster(Stream& serial, int8_t dePin, int8_t rePin) : _rtuComm(serial, dePin, rePin) { + _rtuComm.setTimeout(500); +} + +void ModbusRTUMaster::setTimeout(unsigned long timeout) { + _rtuComm.setTimeout(timeout); +} + +void ModbusRTUMaster::begin(unsigned long baud, uint32_t config) { + _rtuComm.begin(baud, config); +} + + + +ModbusRTUMasterError ModbusRTUMaster::readCoils(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity) { + return _readValues(id, 1, startAddress, buf, quantity); +} + +ModbusRTUMasterError ModbusRTUMaster::readDiscreteInputs(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity) { + return _readValues(id, 2, startAddress, buf, quantity); +} + +ModbusRTUMasterError ModbusRTUMaster::readHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) { + return _readValues(id, 3, startAddress, buf, quantity); +} + +ModbusRTUMasterError ModbusRTUMaster::readInputRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) { + return _readValues(id, 4, startAddress, buf, quantity); +} + + + +ModbusRTUMasterError ModbusRTUMaster::writeSingleCoil(uint8_t id, uint16_t address, bool value) { + return _writeSingleValue(id, 5, address, ((value) ? 0xFF00 : 0x0000)); +} + +ModbusRTUMasterError ModbusRTUMaster::writeSingleHoldingRegister(uint8_t id, uint16_t address, uint16_t value) { + return _writeSingleValue(id, 6, address, value); +} + + + +ModbusRTUMasterError ModbusRTUMaster::writeMultipleCoils(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity) { + const uint8_t functionCode = 15; + if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID; + if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER; + if (quantity == 0 || quantity > 1968) return MODBUS_RTU_MASTER_INVALID_QUANTITY; + ModbusADU adu; + uint16_t byteCount = div8RndUp(quantity); + adu.setUnitId(id); + adu.setFunctionCode(functionCode); + adu.setDataRegister(0, startAddress); + adu.setDataRegister(2, quantity); + adu.data[4] = byteCount; + for (uint16_t i = 0; i < quantity; i++) { + bitWrite(adu.data[5 + (i >> 3)], i & 7, buf[i]); + } + for (uint16_t i = quantity; i < (byteCount * 8); i++) { + bitClear(adu.data[5 + (i >> 3)], i & 7); + } + adu.setDataLen(5 + byteCount); + _rtuComm.writeAdu(adu); + if (id == 0) return MODBUS_RTU_MASTER_SUCCESS; + ModbusRTUCommError commError = _rtuComm.readAdu(adu); + if (commError) return _translateCommError(commError); + if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID; + if (adu.getFunctionCode() == (functionCode + 0x80)) { + _exceptionResponse = adu.data[0]; + return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE; + } + if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE; + if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH; + if (adu.getDataRegister(0) != startAddress) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS; + if (adu.getDataRegister(2) != quantity) return MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY; + return MODBUS_RTU_MASTER_SUCCESS; +} + +ModbusRTUMasterError ModbusRTUMaster::writeMultipleHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity) { + uint8_t functionCode = 16; + if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID; + if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER; + if (quantity == 0 || quantity > 123) return MODBUS_RTU_MASTER_INVALID_QUANTITY; + uint16_t byteCount = quantity * 2; + ModbusADU adu; + adu.setUnitId(id); + adu.setFunctionCode(functionCode); + adu.setDataRegister(0, startAddress); + adu.setDataRegister(2, quantity); + adu.data[4] = byteCount; + for (uint16_t i = 0; i < quantity; i++) { + adu.setDataRegister(5 + (i * 2), buf[i]); + } + adu.setDataLen(5 + byteCount); + _rtuComm.writeAdu(adu); + if (id == 0) return MODBUS_RTU_MASTER_SUCCESS; + ModbusRTUCommError commError = _rtuComm.readAdu(adu); + if (commError) return _translateCommError(commError); + if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID; + if (adu.getFunctionCode() == (functionCode + 0x80)) { + _exceptionResponse = adu.data[0]; + return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE; + } + if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE; + if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH; + if (adu.getDataRegister(0) != startAddress) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS; + if (adu.getDataRegister(2) != quantity) return MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY; + return MODBUS_RTU_MASTER_SUCCESS; +} + + + +uint8_t ModbusRTUMaster::getExceptionResponse() { + return _exceptionResponse; +} + + + +ModbusRTUMasterError ModbusRTUMaster::_readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, bool buf[], uint16_t quantity) { + if (id < 1 || id > 247) return MODBUS_RTU_MASTER_INVALID_ID; + if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER; + if (quantity == 0 || quantity > 2000) return MODBUS_RTU_MASTER_INVALID_QUANTITY; + ModbusADU adu; + adu.setUnitId(id); + adu.setFunctionCode(functionCode); + adu.setDataRegister(0, startAddress); + adu.setDataRegister(2, quantity); + adu.setDataLen(4); + _rtuComm.writeAdu(adu); + ModbusRTUCommError commError = _rtuComm.readAdu(adu); + if (commError) return _translateCommError(commError); + if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID; + if (adu.getFunctionCode() == (functionCode + 0x80)) { + _exceptionResponse = adu.data[0]; + return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE; + } + if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE; + uint16_t byteCount = div8RndUp(quantity); + if (adu.getDataLen() != (1 + byteCount)) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH; + if (adu.data[0] != byteCount) return MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT; + for (uint16_t i = 0; i < quantity; i++) { + buf[i] = bitRead(adu.data[1 + (i >> 3)], i & 7); + } + return MODBUS_RTU_MASTER_SUCCESS; +} + +ModbusRTUMasterError ModbusRTUMaster::_readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, uint16_t buf[], uint16_t quantity) { + if (id < 1 || id > 247) return MODBUS_RTU_MASTER_INVALID_ID; + if (!buf) return MODBUS_RTU_MASTER_INVALID_BUFFER; + if (quantity == 0 || quantity > 125) return MODBUS_RTU_MASTER_INVALID_QUANTITY; + ModbusADU adu; + adu.setUnitId(id); + adu.setFunctionCode(functionCode); + adu.setDataRegister(0, startAddress); + adu.setDataRegister(2, quantity); + adu.setDataLen(4); + _rtuComm.writeAdu(adu); + ModbusRTUCommError commError = _rtuComm.readAdu(adu); + if (commError) return _translateCommError(commError); + if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID; + if (adu.getFunctionCode() == (functionCode + 0x80)) { + _exceptionResponse = adu.data[0]; + return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE; + } + if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE; + uint16_t byteCount = quantity * 2; + if (adu.getDataLen() != (1 + byteCount)) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH; + if (adu.data[0] != byteCount) return MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT; + for (uint16_t i = 0; i < quantity; i++) { + buf[i] = adu.getDataRegister(1 + (i * 2)); + } + return MODBUS_RTU_MASTER_SUCCESS; +} + +ModbusRTUMasterError ModbusRTUMaster::_writeSingleValue(uint8_t id, uint8_t functionCode, uint16_t address, uint16_t value) { + if (id > 247) return MODBUS_RTU_MASTER_INVALID_ID; + ModbusADU adu; + adu.setUnitId(id); + adu.setFunctionCode(functionCode); + adu.setDataRegister(0, address); + adu.setDataRegister(2, value); + adu.setDataLen(4); + _rtuComm.writeAdu(adu); + if (id == 0) return MODBUS_RTU_MASTER_SUCCESS; + ModbusRTUCommError commError = _rtuComm.readAdu(adu); + if (commError) return _translateCommError(commError); + if (adu.getUnitId() != id) return MODBUS_RTU_MASTER_UNEXPECTED_ID; + if (adu.getFunctionCode() == (functionCode + 0x80)) { + _exceptionResponse = adu.data[0]; + return MODBUS_RTU_MASTER_EXCEPTION_RESPONSE; + } + if (adu.getFunctionCode() != functionCode) return MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE; + if (adu.getDataLen() != 4) return MODBUS_RTU_MASTER_UNEXPECTED_LENGTH; + if (adu.getDataRegister(0) != address) return MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS; + if (adu.getDataRegister(2) != value) return MODBUS_RTU_MASTER_UNEXPECTED_VALUE; + return MODBUS_RTU_MASTER_SUCCESS; +} + + + +ModbusRTUMasterError ModbusRTUMaster::_translateCommError(ModbusRTUCommError commError) { + switch (commError) { + case MODBUS_RTU_COMM_SUCCESS: + return MODBUS_RTU_MASTER_SUCCESS; + case MODBUS_RTU_COMM_TIMEOUT: + return MODBUS_RTU_MASTER_RESPONSE_TIMEOUT; + case MODBUS_RTU_COMM_FRAME_ERROR: + return MODBUS_RTU_MASTER_FRAME_ERROR; + case MODBUS_RTU_COMM_CRC_ERROR: + return MODBUS_RTU_MASTER_CRC_ERROR; + default: + return MODBUS_RTU_MASTER_UNKNOWN_COMM_ERROR; + } +} + diff --git a/ModbusRTUMaster.h b/ModbusRTUMaster.h new file mode 100644 index 0000000..d9193c3 --- /dev/null +++ b/ModbusRTUMaster.h @@ -0,0 +1,57 @@ +#ifndef ModbusRTUMaster_h +#define ModbusRTUMaster_h + +#include "Arduino.h" +#include "ModbusADU.h" +#include "ModbusRTUComm.h" + +enum ModbusRTUMasterError : uint8_t { + MODBUS_RTU_MASTER_SUCCESS = 0, + MODBUS_RTU_MASTER_INVALID_ID = 1, + MODBUS_RTU_MASTER_INVALID_BUFFER = 2, + MODBUS_RTU_MASTER_INVALID_QUANTITY = 3, + MODBUS_RTU_MASTER_RESPONSE_TIMEOUT = 4, + MODBUS_RTU_MASTER_FRAME_ERROR = 5, + MODBUS_RTU_MASTER_CRC_ERROR = 6, + MODBUS_RTU_MASTER_UNKNOWN_COMM_ERROR = 7, + MODBUS_RTU_MASTER_UNEXPECTED_ID = 8, + MODBUS_RTU_MASTER_EXCEPTION_RESPONSE = 9, + MODBUS_RTU_MASTER_UNEXPECTED_FUNCTION_CODE = 10, + MODBUS_RTU_MASTER_UNEXPECTED_LENGTH = 11, + MODBUS_RTU_MASTER_UNEXPECTED_BYTE_COUNT = 12, + MODBUS_RTU_MASTER_UNEXPECTED_ADDRESS = 13, + MODBUS_RTU_MASTER_UNEXPECTED_VALUE = 14, + MODBUS_RTU_MASTER_UNEXPECTED_QUANTITY = 15 +}; + +class ModbusRTUMaster { + public: + ModbusRTUMaster(Stream& serial, int8_t dePin = -1, int8_t rePin = -1); + void setTimeout(unsigned long timeout); + void begin(unsigned long baud, uint32_t config = SERIAL_8N1); + + ModbusRTUMasterError readCoils(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity); + ModbusRTUMasterError readDiscreteInputs(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity); + ModbusRTUMasterError readHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity); + ModbusRTUMasterError readInputRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity); + + ModbusRTUMasterError writeSingleCoil(uint8_t id, uint16_t address, bool value); + ModbusRTUMasterError writeSingleHoldingRegister(uint8_t id, uint16_t address, uint16_t value); + ModbusRTUMasterError writeMultipleCoils(uint8_t id, uint16_t startAddress, bool buf[], uint16_t quantity); + ModbusRTUMasterError writeMultipleHoldingRegisters(uint8_t id, uint16_t startAddress, uint16_t buf[], uint16_t quantity); + + uint8_t getExceptionResponse(); + + private: + ModbusRTUComm _rtuComm; + uint8_t _exceptionResponse = 0; + + ModbusRTUMasterError _readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, bool buf[], uint16_t quantity); + ModbusRTUMasterError _readValues(uint8_t id, uint8_t functionCode, uint16_t startAddress, uint16_t buf[], uint16_t quantity); + ModbusRTUMasterError _writeSingleValue(uint8_t id, uint8_t functionCode, uint16_t address, uint16_t value); + + ModbusRTUMasterError _translateCommError(ModbusRTUCommError commError); + +}; + +#endif