diff --git a/IO_EXIO485.cpp b/IO_EXIO485.cpp new file mode 100644 index 00000000..ebedc579 --- /dev/null +++ b/IO_EXIO485.cpp @@ -0,0 +1,427 @@ +/* + * © 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_EXIO485.h" +#include "defines.h" + +static const byte PAYLOAD_FALSE = 0; +static const byte PAYLOAD_NORMAL = 1; +static const byte PAYLOAD_STRING = 2; + + +/************************************************************ + * EXIO485 implementation + ************************************************************/ + +// Constructor for EXIO485 +EXIO485::EXIO485(uint8_t busNo, HardwareSerial &serial, unsigned long baud, int8_t txPin) { + _serial = &serial; + _baud = baud; + + _txPin = txPin; + _busNo = busNo; + _retryTime = 2000000UL; // 1 second + bufferLength=0; + inCommandPayload=PAYLOAD_FALSE; + // Add device to HAL device chain + IODevice::addDevice(this); + + // Add bus to EXIO485 chain. + _nextBus = _busList; + _busList = this; +} + +// CRC-16 implementation +uint16_t EXIO485::crc16(uint8_t *data, uint16_t length) { + uint16_t crc = 0xFFFF; + for (uint16_t i = 0; i < length; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + bool bit = ((crc & 0x0001) != 0); + crc >>= 1; + if (bit) { + crc ^= 0xA001; + } + } + } + return crc; +} + + +/* -= _loop =- +// +// Main loop function for EXIO485. +// Work through list of nodes. For each node, in separate loop entries +// When the slot time has finished, move on to the next device. +*/ + +void EXIO485::_loop(unsigned long currentMicros) { + _currentMicros = currentMicros; + if (_currentNode == NULL) _currentNode = _nodeListStart; + if (!hasTasks() && _currentNode->isInitialised()) { // no tasks? lets poll for data + uint8_t buffA[3]; + buffA[0] = (_currentNode->getNodeID()); + buffA[1] = (0); + buffA[2] = (EXIORDD); + addTask(buffA, 3, EXIORDD); + uint8_t buffB[3]; + buffB[0] = (_currentNode->getNodeID()); + buffB[1] = (0); + buffB[2] = (EXIORDAN); + addTask(buffB, 3, EXIORDAN); + _currentNode = _currentNode->getNext(); + } + + if ( hasTasks()){ // do we have any tasks on the docket + + if (CurrentTaskID == -1) CurrentTaskID = getNextTaskId(); + Task* currentTask = getTaskById(CurrentTaskID); + if (currentTask == nullptr) return; // dead task + + if (!currentTask->completed && _currentMicros - _cycleStartTimeA >= _retryTime) { // after CRC pass, timer is reset + + if (currentTask->currentRetryTimer == 0UL) { // first trigger + currentTask->currentRetryTimer = _currentMicros; // set timer + } else if (_currentMicros - currentTask->currentRetryTimer >= _retryTime) { + currentTask->currentRetryTimer = _currentMicros; // reset timer + currentTask->rxMode = false; // resend data + DIAG(F("EX-IOExplorer485: Fail RX, Retry TX. Task: %d"), CurrentTaskID); + } + } + + if (!currentTask->rxMode) { + currentTask->crcPassFail = 0; + uint16_t response_crc = crc16((uint8_t*)currentTask->commandArray, currentTask->byteCount-1); + + if (_txPin != -1) ArduinoPins::fastWriteDigital(_txPin,HIGH); + // Send response data with CRC + _serial->write(0xFE); + _serial->write(0xFE); + _serial->write(response_crc >> 8); + _serial->write(response_crc & 0xFF); + _serial->write(currentTask->byteCount); + for (int i = 0; i < currentTask->byteCount; i++) { + _serial->write(currentTask->commandArray[i]); + } + _serial->write(0xFD); + _serial->write(0xFD); + _serial->flush(); + if (_txPin != -1) ArduinoPins::fastWriteDigital(_txPin,LOW); + // delete task command after sending, for now + currentTask->rxMode = true; + + } else { + if ( _serial->available()) { + int curByte = _serial->read(); + + if (curByte == 0xFE && flagStart == false) flagStart = true; + else if ( curByte == 0xFE && flagStart == true) { + flagProc = false; + byteCounter = 0; + flagStarted = true; + flagStart = false; + flagEnded = false; + rxStart = true; + rxEnd = false; + crcPass = false; + memset(received_data, 0, ARRAY_SIZE); + }else if (flagStarted) { + crc[0] = curByte; + byteCounter++; + flagStarted = false; + } else if (byteCounter == 1) { + crc[1] = curByte; + received_crc = (crc[0] << 8) | crc[1]; + byteCounter++; + } else if (byteCounter == 2) { + byteCount = curByte; + byteCounter++; + } else if (flagEnded == false && byteCounter >= 3) { + received_data[byteCounter-3] = curByte; + byteCounter++; + } + if (curByte == 0xFD && flagEnd == false) flagEnd = true; + else if ( curByte == 0xFD && flagEnd == true) { + flagEnded = true; + flagEnd = false; + rxEnd = true; + byteCount = byteCounter; + byteCounter = 0; + } + if (flagEnded) { + calculated_crc = crc16((uint8_t*)received_data, byteCount-6); + if (received_crc == calculated_crc) { + crcPass = true; + } + flagEnded = false; + } + } + // Check CRC validity + if (crcPass) { + // Data received successfully, process it (e.g., print) + int nodeTo = received_data[0]; + if (nodeTo == 0) { // for master. + flagProc = true; + + } + } + + if (flagProc) { + crcPass = false; + int nodeFr = received_data[1]; + EXIO485node *node = findNode(nodeFr); + int AddrCode = received_data[2]; + + switch (AddrCode) { + case EXIOPINS: + {node->setnumDigitalPins(received_data[3]); + node->setnumAnalogPins(received_data[4]); + + // See if we already have suitable buffers assigned + if (node->getnumDigialPins()>0) { + size_t digitalBytesNeeded = (node->getnumDigialPins() + 7) / 8; + if (node->getdigitalPinBytes() < digitalBytesNeeded) { + // Not enough space, free any existing buffer and allocate a new one + if (node->cleandigitalPinStates(digitalBytesNeeded)) { + node->setdigitalPinBytes(digitalBytesNeeded); + } else { + DIAG(F("EX-IOExpander485 node:%d ERROR alloc %d bytes"), nodeFr, digitalBytesNeeded); + node->setdigitalPinBytes(0); + } + } + } + + if (node->getnumAnalogPins()>0) { + size_t analogueBytesNeeded = node->getnumAnalogPins() * 2; + if (node->getanalogPinBytes() < analogueBytesNeeded) { + // Free any existing buffers and allocate new ones. + + if (node->cleanAnalogStates(analogueBytesNeeded)) { + node->setanalogPinBytes(analogueBytesNeeded); + } else { + DIAG(F("EX-IOExpander485 node:%d ERROR alloc analog pin bytes"), nodeFr); + node->setanalogPinBytes(0); + } + } + } + markTaskCompleted(CurrentTaskID); + flagProc = false; + break;} + case EXIOINITA: { + for (int i = 0; i < node->getnumAnalogPins(); i++) { + node->setanalogPinMap(received_data[i+3], i); + } + + markTaskCompleted(CurrentTaskID); + flagProc = false; + break; + } + case EXIOVER: { + node->setMajVer(received_data[3]); + node->setMinVer(received_data[4]); + node->setPatVer(received_data[5]); + DIAG(F("EX-IOExpander485: Found node %d v%d.%d.%d"),node->getNodeID(), node->getMajVer(), node->getMinVer(), node->getPatVer()); + node->setInitialised(); + markTaskCompleted(CurrentTaskID); + flagProc = false; + break; + } + case EXIORDY: { + markTaskCompleted(CurrentTaskID); + flagProc = false; + break; + } + case EXIOERR: { + markTaskCompleted(CurrentTaskID); + DIAG(F("EX-IOExplorer485: Some sort of error was received...")); // ;-) + flagProc = false; + break; + } + case EXIORDAN: { + for (int i = 0; i < node->_numAnaloguePins; i++) { + node->setanalogInputBuffer(received_data[i+3], i); + } + markTaskCompleted(CurrentTaskID); + flagProc = false; + break; + } + case EXIORDD: { + for (int i = 0; i < (node->_numDigitalPins+7)/8; i++) { + node->setdigitalInputStates(received_data[i+3], i); + } + markTaskCompleted(CurrentTaskID); + flagProc = false; + break; + } + } + _cycleStartTimeA = currentMicros; // reset timer so we do not resend data + } + } + } +} + +// Link to chain of EXIO485 instances, left over from EXIO485 template. +EXIO485 *EXIO485::_busList = NULL; + + +/************************************************************ + * EXIO485node implementation + ************************************************************/ + +/* -= EXIO485node =- +// +// Constructor for EXIO485node object +*/ +EXIO485node::EXIO485node(VPIN firstVpin, int nPins, uint8_t nodeID) { + _firstVpin = firstVpin; + _nPins = nPins; + _busNo = 0; + _nodeID = nodeID; + _initialised = false; + memset(resFlag, 0, 255); + if (_nodeID > 252) _nodeID = 252; // cannot have a node with the frame flags + if (_nodeID < 1) _nodeID = 1; // cannot have a node with the master ID + + // Add this device to HAL device list + IODevice::addDevice(this); + _display(); + // Add EXIO485node to EXIO485 object. + EXIO485 *bus = EXIO485::findBus(_busNo); + if (bus != NULL) { + bus->addNode(this); + return; + } + +} + +bool EXIO485node::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { + if (paramCount != 1) return false; + int pin = vpin - _firstVpin; + + uint8_t pullup = (uint8_t)params[0]; + uint8_t buff[ARRAY_SIZE]; + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIODPUP); + buff[3] = (pin); + buff[4] = (pullup); + EXIO485 *bus = EXIO485::findBus(0); + bus->setBusy(); + bus->addTask(buff, 5, EXIODPUP); + + return true; + } + + int EXIO485node::_configureAnalogIn(VPIN vpin) { + int pin = vpin - _firstVpin; + uint8_t buff[ARRAY_SIZE]; + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOENAN); + buff[3] = (pin); + buff[4] = lowByte(_firstVpin); + buff[5] = highByte(_firstVpin); + EXIO485 *bus = EXIO485::findBus(0); + bus->setBusy(); + bus->addTask(buff, 6, EXIOENAN); + + return false; + } + +void EXIO485node::_begin() { + uint8_t buff[ARRAY_SIZE]; + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOINIT); + buff[3] = (_nPins); + buff[4] = ((_firstVpin & 0xFF)); + buff[5] = ((_firstVpin >> 8)); + EXIO485 *bus = EXIO485::findBus(0); + bus->setBusy(); + bus->addTask(buff, 6, EXIOINIT); + + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOINITA); + bus->setBusy(); + bus->addTask(buff, 3, EXIOINITA); + + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOVER); + bus->setBusy(); + bus->addTask(buff, 3, EXIOVER); + +#ifdef DIAG_IO + _display(); +#endif +} + +int EXIO485node::_read(VPIN vpin) { + if (_deviceState == DEVSTATE_FAILED) return 0; + int pin = vpin - _firstVpin; + uint8_t pinByte = pin / 8; + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); + return value; + } +void EXIO485node::_write(VPIN vpin, int value) { + if (_deviceState == DEVSTATE_FAILED) return; + int pin = vpin - _firstVpin; + uint8_t buff[ARRAY_SIZE]; + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOWRD); + buff[3] = (pin); + buff[4] = (value); + EXIO485 *bus = EXIO485::findBus(0); + bus->setBusy(); + bus->addTask(buff, 5, EXIOWRD); + + } + + int EXIO485node::_readAnalogue(VPIN vpin) { + if (_deviceState == DEVSTATE_FAILED) return 0; + int pin = vpin - _firstVpin; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + uint8_t _pinLSBByte = aPin * 2; + uint8_t _pinMSBByte = _pinLSBByte + 1; + return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; + } + } + return -1; // pin not found in table + } + + void EXIO485node::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + int pin = vpin - _firstVpin; + uint8_t buff[ARRAY_SIZE]; + buff[0] = (_nodeID); + buff[1] = (0); + buff[2] = (EXIOWRAN); + buff[3] = (pin); + buff[4] = lowByte(value); + buff[5] = highByte(value); + buff[6] = (profile); + buff[7] = lowByte(duration); + buff[8] = highByte(duration); + EXIO485 *bus = EXIO485::findBus(0); + bus->setBusy(); + bus->addTask(buff, 9, EXIOWRAN); + + } \ No newline at end of file diff --git a/IO_EXIO485.h b/IO_EXIO485.h new file mode 100644 index 00000000..52b1f096 --- /dev/null +++ b/IO_EXIO485.h @@ -0,0 +1,625 @@ +/* +* © 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 . +*/ + +/* + * EXIO485 + * ======= + * To define a EXIO485, example syntax: + * EXIO485::create(busNo, serial, baud[, TxPin]); + * + * busNo = the Bus no of the instance. should = 0, unless more than one bus configured for some reason. + * 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) + * TxPin = pin number connected to EXIO485 module's DE and !RE terminals for half-duplex operation (default -1) + * if omitted (default), hardware MUST support full-duplex opperation! + * + * + * EXIO485Node + * ======== + * To define a EXIO485 node and associate it with a EXIO485 bus, + * EXIO485node::create(firstVPIN, numVPINs, nodeID); + * + * firstVPIN = first vpin in block allocated to this device + * numVPINs = number of vpins + * nodeID = 1-252 + */ + +#ifndef IO_EXIO485_H +#define IO_EXIO485_H + +#include "IODevice.h" + +class EXIO485; +class EXIO485node; + + + +#ifndef COMMAND_BUFFER_SIZE + #define COMMAND_BUFFER_SIZE 900 +#endif + +/********************************************************************** + * Data Structure + * + * Data Frame: + * 0xFE : 0xFE : CRC : CRC : ByteCount : DataPacket : 0xFD : 0xFD + * -------------------------------------------------------------- + * Start Frame : CRC Bytes : Data Size : Data : End Frame + * + * Data frame must always start with the Start Frame bytes (two Bytes), + * follow with the CRC bytes (two bytes), the data byte count + * (one byte), the Data Packet (variable bytes), and the end Frame + * Bytes. + * + * + * Data Packet: + * NodeTo : NodeFrom : AddrCode : ~Command Params~ + * ----------------------------------------------- + * NodeTo = where the packet is destined for. + * NodeFrom = where the packet came from. + * Address Code = from EXIO enumeration. + * Command Params: + * + * EXIOINIT:TX CS + * -------- + * nPins : FirstPinL : FirstPinH + * ----------------------------- + * nPins = Number of allocated pins. + * FirstPinL = First VPIN lowByte. + * FirstPinH = First VPIN highByte. + * + * Sends the allocated pins. + * + * EXIOINITA: Tx CS + * -=no parameters, just a header=- + * + * requests the analog pin map from the node. + * + * EXIOVER: Tx CS + * -=no parameters=- + * + * requests the node software version, but as yet to do anything with it + * + * EXIODPUP: Tx CS + * pin : pullup + * + * pin = VPIN number + * pullup = 1 - Pullup, 0 - no pullup + * configures a digital pin for input + * + * EXIOENAN: TX CS + * pin : FirstPinL : FirstPinH + * + * pin = VPIN number + * FirstPinL = first pin lowByte + * FirstPinH = first pin highByte + * + * EXIOWRD: TX CS + * pin : value + * + * pin = VPIN number + * value = 1 or 0 + * + * EXIOWRAN: TX CS + * pin : valueL : valueH : profile : durationL : durationH + * + * pin = VPIN Number + * valueL = value lowByte + * valueH = value highByte + * profile = servo profile + * dueationL = duration lowByte + * durationH = duration highByte + * + * EXIORDD: TX CS + * -=No Parameters=- + * + * Requests digital pin states. + * + * EXIORDAN: TX CS + * -=no parameters=- + * + * Requests analog pin states. + * + * EXIOPINS: TX Node (EXIOINIT) + * numDigital : numAnalog + * + * numDigital = number of digital capable pins + * numAnalog = number of analog capable pins + * + * EXIOINITA: TX Node (EXIOINITA) + * ~analog pin map~ + * + * each byte is a analog pin map value, variable length. + * + * EXIORDY/EXIOERR: TX Node (EXIODPUP, EXIOWRD, EXIOENAN, EXIOWRAN) + * -=no parameters=- + * + * Responds EXIORDY for OK, and EXIOERR for FAIL. + * + * EXIORDAN: TX Node (EXIORDAN) + * ~analog pin states~ + * + * each byte is a pin state value, perhaps in lowByte/higeByte config. + * + * EXIORDD: TX Node (EXIORDD) + * ~digital pin states~ + * + * each byte is a 8-bit grouping of pinstates. + * + * EXIOVER: TX Node (EXIOVER) + * Major Version : Minor Version : Patch Version + * + * each byte represents a numeric version value. + **********************************************************************/ + + + + +/********************************************************************** + * EXIO485node class + * + * This encapsulates the state associated with a single EXIO485 node, + * which includes the nodeID, number of discrete inputs and coils, and + * the states of the discrete inputs and coils. + **********************************************************************/ +class EXIO485node : public IODevice { +private: + uint8_t _busNo; + uint8_t _nodeID; + char _type; + EXIO485node *_next = NULL; + bool _initialised; + EXIO485 *bus; + HardwareSerial* _serial; + enum { + EXIOINIT = 0xE0, // Flag to initialise setup procedure + EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup + EXIODPUP = 0xE2, // Flag we're sending digital pin pullup configuration + EXIOVER = 0xE3, // Flag to get version + EXIORDAN = 0xE4, // Flag to read an analogue input + EXIOWRD = 0xE5, // Flag for digital write + EXIORDD = 0xE6, // Flag to read digital input + EXIOENAN = 0xE7, // Flag to enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings + EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers + EXIOWRAN = 0xEA, // Flag we're sending an analogue write (PWM) + EXIOERR = 0xEF, // Flag we've received an error + }; + static const int ARRAY_SIZE = 254; +public: + static EXIO485node *_nodeList; + enum ProfileType : int { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + + uint8_t _numDigitalPins = 0; + uint8_t getnumDigialPins() { + return _numDigitalPins; + } + void setnumDigitalPins(uint8_t value) { + _numDigitalPins = value; + } + uint8_t _numAnaloguePins = 0; + uint8_t getnumAnalogPins() { + return _numAnaloguePins; + } + void setnumAnalogPins(uint8_t value) { + _numAnaloguePins = value; + } + uint8_t _majorVer = 0; + uint8_t getMajVer() { + return _majorVer; + } + void setMajVer(uint8_t value) { + _majorVer = value; + } + uint8_t _minorVer = 0; + uint8_t getMinVer() { + return _minorVer; + } + void setMinVer(uint8_t value) { + _minorVer = value; + } + uint8_t _patchVer = 0; + uint8_t getPatVer() { + return _patchVer; + } + void setPatVer(uint8_t value) { + _patchVer = value; + } + uint8_t* _digitalInputStates = NULL; + uint8_t getdigitalInputStates(int index) { + return _digitalInputStates[index]; + } + void setdigitalInputStates(uint8_t value, int index) { + _digitalInputStates[index] = value; + } + bool cleandigitalPinStates(int size) { + if (_digitalPinBytes > 0) free(_digitalInputStates); + if ((_digitalInputStates = (byte*) calloc(size, 1)) != NULL) { + return true; + } else return false; + } + uint8_t* _analogueInputStates = NULL; + uint8_t getanalogInputStates(int index) { + return _analogueInputStates[index]; + } + void setanalogInputStates(uint8_t value, int index) { + _analogueInputStates[index] = value; + } + + uint8_t* _analogueInputBuffer = NULL; // buffer for I2C input transfers + uint8_t getanalogInpuBuffer(int index) { + return _analogueInputBuffer[index]; + } + void setanalogInputBuffer(uint8_t value, int index) { + _analogueInputBuffer[index] = value; + memcpy(_analogueInputStates, _analogueInputBuffer, _analoguePinBytes); + } + uint8_t _readCommandBuffer[4]; // unused? + uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed) + uint8_t getdigitalPinBytes() { + return _digitalPinBytes; + } + void setdigitalPinBytes(uint8_t value) { + _digitalPinBytes = value; + } + uint8_t _analoguePinBytes = 0; // Size of allocated memory buffer (may be longer than needed) + uint8_t getanalogPinBytes() { + return _analoguePinBytes; + } + void setanalogPinBytes(uint8_t value) { + _analoguePinBytes = value; + } + uint8_t* _analoguePinMap = NULL; + uint8_t getanalogPinMap(int index) { + return _analoguePinMap[index]; + } + void setanalogPinMap(uint8_t value, int index) { + _analoguePinMap[index] = value; + } + bool cleanAnalogStates(int size) { + if (_analoguePinBytes > 0) { + free(_analogueInputBuffer); + free(_analogueInputStates); + free(_analoguePinMap); + } + _analogueInputStates = (uint8_t*) calloc(size, 1); + _analogueInputBuffer = (uint8_t*) calloc(size, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + if (_analogueInputStates != NULL && _analogueInputBuffer != NULL && _analoguePinMap != NULL) return true; + else return false; + } + int resFlag[255]; + bool _initalized; + static void create(VPIN firstVpin, int nPins, uint8_t nodeID) { + if (checkNoOverlap(firstVpin, nPins)) new EXIO485node(firstVpin, nPins, nodeID); + } + EXIO485node(VPIN firstVpin, int nPins, uint8_t nodeID); + + uint8_t getNodeID() { + return _nodeID; + } + + EXIO485node *getNext() { + return _next; + } + + void setNext(EXIO485node *node) { + _next = node; + } + bool isInitialised() { + return _initialised; + } + void setInitialised() { + _initialised = true; + } + + bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override; + int _configureAnalogIn(VPIN vpin) override; + void _begin() override; + int _read(VPIN vpin) override; + void _write(VPIN vpin, int value) override; + int _readAnalogue(VPIN vpin) override; + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override; + + uint8_t getBusNumber() { + return _busNo; + } + + void _display() override { + DIAG(F("EX-IOExpander485 node:%d Vpins %u-%u %S"), _nodeID, (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); + } + +}; + +/********************************************************************** + * EXIO485 class + * + * This encapsulates the properties state of the bus and the + * transmission and reception of data across that bus. Each EXIO485 + * object owns a set of EXIO485node objects which represent the nodes + * attached to that bus. + **********************************************************************/ +class EXIO485 : public IODevice { +private: + // Here we define the device-specific variables. + uint8_t _busNo; + unsigned long _cycleStartTime = 0; + unsigned long _cycleStartTimeA = 0; + unsigned long _timeoutStart = 0; + unsigned long _retryTime; // 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 + int _operationCount = 0; + int _refreshOperation = 0; + byte bufferLength; + static const int ARRAY_SIZE = 150; + int buffer[ARRAY_SIZE]; + byte inCommandPayload; + static EXIO485 *_busList; // linked list of defined bus instances + bool waitReceive = false; + int _waitCounter = 0; + int _waitCounterB = 0; + int _waitA; + unsigned long _charTimeout; + unsigned long _frameTimeout; + enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states + uint8_t _readState = RDS_IDLE; + + unsigned long _lastDigitalRead = 0; + unsigned long _lastAnalogueRead = 0; + const unsigned long _digitalRefresh = 10000UL; // Delay refreshing digital inputs for 10ms + const unsigned long _analogueRefresh = 50000UL; // Delay refreshing analogue inputs for 50ms + + + + + EXIO485node *_nodeListStart = NULL, *_nodeListEnd = NULL; + EXIO485node *_currentNode = NULL; + uint16_t _receiveDataIndex = 0; // Index of next data byte to be received. + EXIO485 *_nextBus = NULL; // Pointer to next bus instance in list. + + int byteCounter = 0; +public: +struct Task { + static const int ARRAY_SIZE = 150; + long taskID; + uint8_t commandArray[ARRAY_SIZE]; + int byteCount; + uint8_t retFlag; + bool gotCallback; + bool rxMode; + int crcPassFail; + bool completed; + bool processed; + unsigned long currentRetryTimer; + }; + static const int MAX_TASKS = 100; // we don't want to run out of task slots, but memory? + long taskIDCntr = 1; + long CurrentTaskID = -1; + int taskResendCount = 0; + Task taskBuffer[MAX_TASKS]; + int currentTaskIndex = 0; + + void addTask(const uint8_t* cmd, int byteCount, uint8_t retFlag) { + // Find an empty slot in the buffer + int emptySlot = -1; + for (int i = 0; i < MAX_TASKS; i++) { + if (taskBuffer[i].completed) { + emptySlot = i; + break; + } + } + // If no empty slot found, return (buffer full) + if (emptySlot == -1) { + DIAG(F("Task Buffer Full!")); + return; + } + for (int i = 0; i < byteCount; i++) taskBuffer[emptySlot].commandArray[i] = cmd[i]; + taskBuffer[emptySlot].byteCount = byteCount; + taskBuffer[emptySlot].retFlag = retFlag; + taskBuffer[emptySlot].rxMode = false; + taskBuffer[emptySlot].crcPassFail = 0; + taskBuffer[emptySlot].gotCallback = false; + taskBuffer[emptySlot].completed = false; + taskBuffer[emptySlot].processed = false; + taskBuffer[emptySlot].currentRetryTimer = 0UL; + taskIDCntr++; + if (taskIDCntr >= 5000000) taskIDCntr = 1; + taskBuffer[emptySlot].taskID = taskIDCntr; + currentTaskIndex = emptySlot; + } + bool hasTasks() { + for (int i = 0; i < MAX_TASKS; i++) { + if (!taskBuffer[i].completed) { + return true; // At least one task is not completed + } + } + return false; // All tasks are completed + } + // Function to get a specific task by ID + Task* getTaskById(int id) { + for (int i = 0; i < MAX_TASKS; i++) { + if (taskBuffer[i].taskID == id) { + return &taskBuffer[i]; // Return a pointer to the task + } + } + return nullptr; // Task not found + } + // Function to get the next task (optional) + long getNextTaskId() { + for (int i = 0; i < MAX_TASKS; i++) { + if (!taskBuffer[i].completed) { + return taskBuffer[i].taskID; + } + } + return -1; // No tasks available + } + // Function to mark a task as completed + void markTaskCompleted(int id) { + for (int i = 0; i < MAX_TASKS; i++) { + if (taskBuffer[i].taskID == id) { + taskBuffer[i].completed = true; // completed + taskBuffer[i].taskID = -1; // unassigned + taskBuffer[i].currentRetryTimer = 0UL; // stop timer + CurrentTaskID = getNextTaskId(); + break; + } + } + } + bool flagEnd = false; + bool flagEnded = false; + bool flagStart = false; + bool flagStarted = false; + bool rxStart = false; + bool rxEnd = false; + bool crcPass = false; + bool flagProc = false; + uint16_t calculated_crc; + int byteCount = 100; + uint8_t received_data[ARRAY_SIZE]; + uint16_t received_crc; + uint8_t crc[2]; + uint16_t crc16(uint8_t *data, uint16_t length); + + // EX-IOExpander protocol flags + enum { + EXIOINIT = 0xE0, // Flag to initialise setup procedure + EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup + EXIODPUP = 0xE2, // Flag we're sending digital pin pullup configuration + EXIOVER = 0xE3, // Flag to get version + EXIORDAN = 0xE4, // Flag to read an analogue input + EXIOWRD = 0xE5, // Flag for digital write + EXIORDD = 0xE6, // Flag to read digital input + EXIOENAN = 0xE7, // Flag to enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings + EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers + EXIOWRAN = 0xEA, // Flag we're sending an analogue write (PWM) + EXIOERR = 0xEF, // Flag we've received an error + }; + static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, int8_t txPin=-1) { + new EXIO485(busNo, serial, baud, txPin); + } + HardwareSerial* _serial; + int _CommMode = 0; + int _opperation = 0; + uint16_t _pullup; + uint16_t _pin; + int8_t _txPin; + int8_t getTxPin() { + return _txPin; + } + bool _busy = false; + void setBusy() { + _busy = true; + } + void clearBusy() { + _busy = false; + } + bool getBusy() { + return _busy; + } + unsigned long _baud; + int taskCnt = 0; + uint8_t initBuffer[1] = {0xFE}; + unsigned long taskCounter=0ul; + // Device-specific initialisation + void _begin() override { + _serial->begin(_baud, SERIAL_8N1); + if (_txPin >0) { + pinMode(_txPin, OUTPUT); + digitalWrite(_txPin, LOW); + + } + + #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("EX-IOExpander485 Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1, + _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("OK")); + } + + // Locate EXIO485node object with specified nodeID. + EXIO485node *findNode(uint8_t nodeID) { + for (EXIO485node *node = _nodeListStart; node != NULL; node = node->getNext()) { + if (node->getNodeID() == nodeID) + return node; + } + return NULL; + } + + bool nodesInitialized() { + bool retval = true; + for (EXIO485node *node = _nodeListStart; node != NULL; node = node->getNext()) { + if (node->_initalized == false) + retval = false; + } + return retval; + } + // Add new EXIO485node to the list of nodes for this bus. + void addNode(EXIO485node *newNode) { + if (!_nodeListStart) + _nodeListStart = newNode; + if (!_nodeListEnd) + _nodeListEnd = newNode; + else + _nodeListEnd->setNext(newNode); + //DIAG(F("EXIO485: 260h nodeID:%d _nodeListStart:%d _nodeListEnd:%d"), newNode, _nodeListStart, _nodeListEnd); + } + +protected: + EXIO485(uint8_t busNo, HardwareSerial &serial, unsigned long baud, int8_t txPin); + +public: + + uint8_t getBusNumber() { + return _busNo; + } + EXIO485 *getNext() { + return _nextBus; + } + static EXIO485 *findBus(uint8_t busNo) { + for (EXIO485 *bus = _busList; bus != NULL; bus = bus->getNext()) { + if (bus->getBusNumber() == busNo) + return bus; + } + return NULL; + } +}; + + +#endif // IO_EXIO485_H \ No newline at end of file