diff --git a/IO_CMRI.cpp b/IO_CMRI.cpp
new file mode 100644
index 0000000..eb5e4fd
--- /dev/null
+++ b/IO_CMRI.cpp
@@ -0,0 +1,324 @@
+/*
+ * © 2023, Neil McKechnie. 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_CMRI.h"
+#include "defines.h"
+
+/************************************************************
+ * CMRIbus implementation
+ ************************************************************/
+
+// Constructor for CMRIbus
+CMRIbus::CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin) {
+ _busNo = busNo;
+ _serial = &serial;
+ _baud = baud;
+ _cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds.
+ _transmitEnablePin = transmitEnablePin;
+ if (_transmitEnablePin != VPIN_NONE) {
+ pinMode(_transmitEnablePin, OUTPUT);
+ ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); // transmitter initially off
+ }
+
+ // Max message length is 256+6=262 bytes.
+ // Each byte is one start bit, 8 data bits and 1 or 2 stop bits, assume 11 bits per byte.
+ // Calculate timeout based on treble this time.
+ _timeoutPeriod = 3 * 11 * 262 * 1000UL / (_baud / 1000UL);
+#if defined(ARDUINOCMRI_COMPATIBLE)
+ // NOTE: The ArduinoCMRI library, unless modified, contains a 'delay(50)' between
+ // receiving the end of the prompt message and starting to send the response. This
+ // is allowed for below.
+ _timeoutPeriod += 50000UL;
+#endif
+
+ // Calculate the time in microseconds to transmit one byte (11 bits max).
+ _byteTransmitTime = 1000000UL * 11 / _baud;
+ // Postdelay is only required if we need to allow for data still being sent when
+ // we want to switch off the transmitter. The flush() method of HardwareSerial
+ // ensures that the data has completed being sent over the line.
+ _postDelay = 0;
+
+ // Add device to HAL device chain
+ IODevice::addDevice(this);
+
+ // Add bus to CMRIbus chain.
+ _nextBus = _busList;
+ _busList = this;
+}
+
+
+// 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 CMRIbus::_loop(unsigned long currentMicros) {
+
+ _currentMicros = currentMicros;
+
+ while (_serial->available())
+ processIncoming();
+
+ // Send any data that needs sending.
+ processOutgoing();
+
+}
+
+// Send output data to the bus for nominated CMRInode
+uint16_t CMRIbus::sendData(CMRInode *node) {
+ uint16_t numDataBytes = (node->getNumOutputs()+7)/8;
+ _serial->write(SYN);
+ _serial->write(SYN);
+ _serial->write(STX);
+ _serial->write(node->getNodeID() + 65);
+ _serial->write('T'); // T for Transmit data message
+ uint16_t charsSent = 6; // include header and trailer
+ for (uint8_t index=0; indexgetOutputStates(index);
+ if (value == DLE || value == STX || value == ETX) {
+ _serial->write(DLE);
+ charsSent++;
+ }
+ _serial->write(value);
+ charsSent++;
+ }
+ _serial->write(ETX);
+ return charsSent; // number of characters sent
+}
+
+// Send request for input data to nominated CMRInode.
+uint16_t CMRIbus::requestData(CMRInode *node) {
+ _serial->write(SYN);
+ _serial->write(SYN);
+ _serial->write(STX);
+ _serial->write(node->getNodeID() + 65);
+ _serial->write('P'); // P for Poll message
+ _serial->write(ETX);
+ return 6; // number of characters sent
+}
+
+// Send initialisation message
+uint16_t CMRIbus::sendInitialisation(CMRInode *node) {
+ _serial->write(SYN);
+ _serial->write(SYN);
+ _serial->write(STX);
+ _serial->write(node->getNodeID() + 65);
+ _serial->write('I'); // I for initialise message
+ _serial->write(node->getType()); // NDP
+ _serial->write((uint8_t)0); // dH
+ _serial->write((uint8_t)0); // dL
+ _serial->write((uint8_t)0); // NS
+ _serial->write(ETX);
+ return 10; // number of characters sent
+}
+
+void CMRIbus::processOutgoing() {
+ uint16_t charsSent = 0;
+ 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("CMRInode: 138 _nodeListEnd:%d "), _nodeListEnd);
+ DIAG(F("CMRInode: 139 _currentNode:%d "), _currentNode);
+ _currentNode = _nodeListStart;
+ DIAG(F("CMRInode: 141 _currentNode:%d "), _currentNode);
+ _transmitState = TD_INIT;
+ _cycleStartTime = _currentMicros;
+ }
+ if (_currentNode == NULL) return;
+ switch (_transmitState) {
+ case TD_IDLE:
+ case TD_INIT:
+ enableTransmitter();
+ if (!_currentNode->isInitialised()) {
+ charsSent = sendInitialisation(_currentNode);
+ _currentNode->setInitialised();
+ DIAG(F("CMRInode: 153 _currentNode:%d "), _currentNode);
+ _transmitState = TD_TRANSMIT;
+ delayUntil(_currentMicros+_byteTransmitTime*charsSent);
+ break;
+ }
+ /* fallthrough */
+ case TD_TRANSMIT:
+ charsSent = sendData(_currentNode);
+ _transmitState = TD_PROMPT;
+ // Defer next entry for as long as it takes to transmit the characters,
+ // to allow output queue to empty. Allow 2 bytes extra.
+ delayUntil(_currentMicros+_byteTransmitTime*(charsSent+2));
+ break;
+ case TD_PROMPT:
+ charsSent = requestData(_currentNode);
+ disableTransmitter();
+ _transmitState = TD_RECEIVE;
+ _timeoutStart = _currentMicros; // Start timeout on response
+ break;
+ case TD_RECEIVE: // Waiting for response / timeout
+ if (_currentMicros - _timeoutStart > _timeoutPeriod) {
+ // End of time slot allocated for responses.
+ _transmitState = TD_IDLE;
+ // Reset state of receiver
+ _receiveState = RD_SYN1;
+ // Move to next node
+ DIAG(F("CMRInode: 179 node:%d "), _currentNode);
+ _currentNode = _currentNode->getNext();
+ DIAG(F("CMRInode: 181 node:%d "), _currentNode);
+ }
+ break;
+ }
+}
+
+// Process any data bytes received from a CMRInode.
+void CMRIbus::processIncoming() {
+ int data = _serial->read();
+ if (data < 0) return; // No characters to read
+
+ DIAG(F("CMRInode: 192 node:%d "), _currentNode);
+ if (_transmitState != TD_RECEIVE || !_currentNode) return; // Not waiting for input, so ignore.
+
+ uint8_t nextState = RD_SYN1; // default to resetting state machine
+ switch(_receiveState) {
+ case RD_SYN1:
+ if (data == SYN) nextState = RD_SYN2;
+ break;
+ case RD_SYN2:
+ if (data == SYN) nextState = RD_STX; else nextState = RD_SYN2;
+ break;
+ case RD_STX:
+ if (data == STX) nextState = RD_ADDR;
+ break;
+ case RD_ADDR:
+ // If nodeID doesn't match, then ignore everything until next SYN-SYN-STX.
+ if (data == _currentNode->getNodeID() + 65) nextState = RD_TYPE;
+ break;
+ case RD_TYPE:
+ _receiveDataIndex = 0; // Initialise data pointer
+ if (data == 'R') nextState = RD_DATA;
+ break;
+ case RD_DATA: // data body
+ if (data == DLE) // escape next character
+ nextState = RD_ESCDATA;
+ else if (data == ETX) { // end of data
+ // End of data message. Protocol has all data in one
+ // message, so we don't need to wait any more. Allow
+ // transmitter to proceed with next node in list.
+ DIAG(F("CMRInode: 221 node:%d "), _currentNode);
+ _currentNode = _currentNode->getNext();
+ DIAG(F("CMRInode: 223 node:%d "), _currentNode);
+ _transmitState = TD_IDLE;
+ } else {
+ // Not end yet, so save data byte
+ _currentNode->saveIncomingData(_receiveDataIndex++, data);
+ nextState = RD_DATA; // wait for more data
+ }
+ break;
+ case RD_ESCDATA: // escaped data byte
+ _currentNode->saveIncomingData(_receiveDataIndex++, data);
+ nextState = RD_DATA;
+ break;
+ }
+ _receiveState = nextState;
+}
+
+// If configured for half duplex RS485, switch RS485 interface
+// into transmit mode.
+void CMRIbus::enableTransmitter() {
+ if (_transmitEnablePin != VPIN_NONE)
+ ArduinoPins::fastWriteDigital(_transmitEnablePin, 1);
+ // If we need a delay before we start the packet header,
+ // we can send a character or two to synchronise the
+ // transmitter and receiver.
+ // SYN characters should be used, but a bug in the
+ // ArduinoCMRI library causes it to ignore the packet if
+ // it's preceded by an odd number of SYN characters.
+ // So send a SYN followed by a NUL in that case.
+ _serial->write(SYN);
+#if defined(ARDUINOCMRI_COMPATIBLE)
+ _serial->write(NUL); // Reset the ArduinoCMRI library's parser
+#endif
+}
+
+// If configured for half duplex RS485, switch RS485 interface
+// into receive mode.
+void CMRIbus::disableTransmitter() {
+ // Wait until all data has been transmitted. On the standard
+ // AVR driver, this waits until the FIFO is empty and all
+ // data has been sent over the link.
+ _serial->flush();
+ // If we don't trust the 'flush' function and think the
+ // data's still in transit, then wait a bit longer.
+ if (_postDelay > 0)
+ delayMicroseconds(_postDelay);
+ // Hopefully, we can now safely switch off the transmitter.
+ if (_transmitEnablePin != VPIN_NONE)
+ ArduinoPins::fastWriteDigital(_transmitEnablePin, 0);
+}
+
+// Link to chain of CMRI bus instances
+CMRIbus *CMRIbus::_busList = NULL;
+
+
+/************************************************************
+ * CMRInode implementation
+ ************************************************************/
+
+// Constructor for CMRInode object
+CMRInode::CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, char type, uint16_t inputs, uint16_t outputs) {
+ _firstVpin = firstVpin;
+ _nPins = nPins;
+ _busNo = busNo;
+ _nodeID = nodeID;
+ _type = type;
+
+ switch (_type) {
+ case 'M': // SMINI, fixed 24 inputs and 48 outputs
+ _numInputs = 24;
+ _numOutputs = 48;
+ break;
+ case 'C': // CPNODE with 16 to 144 inputs/outputs using 8-bit cards
+ _numInputs = inputs;
+ _numOutputs = outputs;
+ break;
+ case 'N': // Classic USIC and SUSIC using 24 bit i/o cards
+ case 'X': // SUSIC using 32 bit i/o cards
+ default:
+ DIAG(F("CMRInode: bus:%d nodeID:%d ERROR unsupported type %c"), _busNo, _nodeID, _type);
+ return; // Don't register device.
+ }
+ if ((unsigned int)_nPins < _numInputs + _numOutputs)
+ DIAG(F("CMRInode: bus:%d nodeID:%d WARNING number of Vpins does not cover all inputs and outputs"), _busNo, _nodeID);
+
+ // Allocate memory for states
+ _inputStates = (uint8_t *)calloc((_numInputs+7)/8, 1);
+ _outputStates = (uint8_t *)calloc((_numOutputs+7)/8, 1);
+ if (!_inputStates || !_outputStates) {
+ DIAG(F("CMRInode: ERROR insufficient memory"));
+ return;
+ }
+
+ // Add this device to HAL device list
+ IODevice::addDevice(this);
+
+ // Add CMRInode to CMRIbus object.
+ CMRIbus *bus = CMRIbus::findBus(_busNo);
+ if (bus != NULL) {
+ bus->addNode(this);
+ return;
+ }
+}
diff --git a/IO_CMRI.h b/IO_CMRI.h
new file mode 100644
index 0000000..1aa9235
--- /dev/null
+++ b/IO_CMRI.h
@@ -0,0 +1,292 @@
+/*
+ * © 2023, Neil McKechnie. 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 .
+ */
+
+/*
+ * CMRIbus
+ * =======
+ * To define a CMRI bus, example syntax:
+ * CMRIbus::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.
+ *
+ * IMPORTANT: If you are using ArduinoCMRI library code by Michael Adams, at the time of writing this library
+ * is not compliant with the LCS-9.10.1 specification for CMRInet protocol.
+ * Various work-arounds may be enabled within the driver by adding the following line to your config.h file,
+ * to allow nodes running the ArduinoCMRI library to communicate:
+ *
+ * #define ARDUINOCMRI_COMPATIBLE
+ *
+ * CMRINode
+ * ========
+ * 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
+ * type = 'M' for SMINI (fixed 24 inputs and 48 outputs)
+ * 'C' for CPNODE (16 to 144 inputs/outputs in groups of 8)
+ * (other types are not supported at this time).
+ * inputs = number of inputs (CPNODE only)
+ * outputs = number of outputs (CPNODE only)
+ *
+ * Reference: "LCS-9.10.1
+ * Layout Control Specification: CMRInet Protocol
+ * Version 1.1 December 2014."
+ */
+
+#ifndef IO_CMRI_H
+#define IO_CMRI_H
+
+#include "IODevice.h"
+
+/**********************************************************************
+ * CMRInode class
+ *
+ * This encapsulates the state associated with a single CMRI node,
+ * which includes the nodeID type, number of inputs and outputs, and
+ * the states of the inputs and outputs.
+ **********************************************************************/
+class CMRInode : public IODevice {
+private:
+ uint8_t _busNo;
+ uint8_t _nodeID;
+ char _type;
+ CMRInode *_next = NULL;
+ uint8_t *_inputStates = NULL;
+ uint8_t *_outputStates = NULL;
+ uint16_t _numInputs = 0;
+ uint16_t _numOutputs = 0;
+ bool _initialised = false;
+
+public:
+ static void create(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, char type, uint16_t inputs=0, uint16_t outputs=0) {
+ if (checkNoOverlap(firstVpin, nPins)) new CMRInode(firstVpin, nPins, busNo, nodeID, type, inputs, outputs);
+ }
+ CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t nodeID, char type, uint16_t inputs=0, uint16_t outputs=0);
+
+ uint8_t getNodeID() {
+ return _nodeID;
+ }
+ CMRInode *getNext() {
+ return _next;
+ }
+ void setNext(CMRInode *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 < _numInputs) {
+ uint8_t mask = 1 << (pin & 0x7);
+ int index = pin / 8;
+ return (_inputStates[index] & mask) != 0;
+ } 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 - _numInputs;
+ if (pin < _numOutputs) {
+ uint8_t mask = 1 << (pin & 0x7);
+ int index = pin / 8;
+ if (value)
+ _outputStates[index] |= mask;
+ else
+ _outputStates[index] &= ~mask;
+ }
+ }
+
+ void saveIncomingData(uint8_t index, uint8_t data) {
+ if (index < (_numInputs+7)/8)
+ _inputStates[index] = data;
+ }
+
+ uint8_t getOutputStates(uint8_t index) {
+ if (index < (_numOutputs+7)/8)
+ return _outputStates[index];
+ else
+ return 0;
+ }
+
+ uint16_t getNumInputs() {
+ return _numInputs;
+ }
+
+ uint16_t getNumOutputs() {
+ return _numOutputs;
+ }
+
+ char getType() {
+ return _type;
+ }
+
+ uint8_t getBusNumber() {
+ return _busNo;
+ }
+
+ void _display() override {
+ DIAG(F("CMRInode type:'%c' configured on bus:%d nodeID:%d VPINs:%u-%u (in) %u-%u (out)"),
+ _type, _busNo, _nodeID, _firstVpin, _firstVpin+_numInputs-1,
+ _firstVpin+_numInputs, _firstVpin+_numInputs+_numOutputs-1);
+ }
+
+};
+
+/**********************************************************************
+ * CMRIbus class
+ *
+ * This encapsulates the properties state of the bus and the
+ * transmission and reception of data across that bus. Each CMRIbus
+ * object owns a set of CMRInode objects which represent the nodes
+ * attached to that bus.
+ **********************************************************************/
+class CMRIbus : public IODevice {
+private:
+ // Here we define the device-specific variables.
+ uint8_t _busNo;
+ HardwareSerial *_serial;
+ unsigned long _baud;
+ VPIN _transmitEnablePin = VPIN_NONE;
+ CMRInode *_nodeListStart = NULL, *_nodeListEnd = NULL;
+ CMRInode *_currentNode = NULL;
+
+ // Transmitter state machine states
+ enum {TD_IDLE, TD_PRETRANSMIT, TD_INIT, TD_TRANSMIT, TD_PROMPT, TD_RECEIVE};
+ uint8_t _transmitState = TD_IDLE;
+ // Receiver state machine states.
+ enum {RD_SYN1, RD_SYN2, RD_STX, RD_ADDR, RD_TYPE,
+ RD_DATA, RD_ESCDATA, RD_SKIPDATA, RD_SKIPESCDATA, RD_ETX};
+ uint8_t _receiveState = RD_SYN1;
+ uint16_t _receiveDataIndex = 0; // Index of next data byte to be received.
+ CMRIbus *_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 CMRIbus *_busList; // linked list of defined bus instances
+
+ // Definition of special characters in CMRInet protocol
+ enum : uint8_t {
+ NUL = 0x00,
+ STX = 0x02,
+ ETX = 0x03,
+ DLE = 0x10,
+ SYN = 0xff,
+ };
+
+public:
+ static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS=500, VPIN transmitEnablePin=VPIN_NONE) {
+ new CMRIbus(busNo, serial, baud, cycleTimeMS, transmitEnablePin);
+ }
+
+ // Device-specific initialisation
+ void _begin() override {
+ // CMRInet spec states one stop bit, JMRI and ArduinoCMRI use two stop bits
+#if defined(ARDUINOCMRI_COMPATIBLE)
+ _serial->begin(_baud, SERIAL_8N2);
+#else
+ _serial->begin(_baud, SERIAL_8N1);
+#endif
+ #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("CMRIbus %d configured, speed=%d baud, cycle=%d ms"), _busNo, _baud, _cycleTime/1000);
+ }
+
+ // Locate CMRInode object with specified nodeID.
+ CMRInode *findNode(uint8_t nodeID) {
+ for (CMRInode *node = _nodeListStart; node != NULL; node = node->getNext()) {
+ if (node->getNodeID() == nodeID)
+ return node;
+ }
+ return NULL;
+ }
+
+ // Add new CMRInode to the list of nodes for this bus.
+ void addNode(CMRInode *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:
+ CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin);
+ uint16_t sendData(CMRInode *node);
+ uint16_t requestData(CMRInode *node);
+ uint16_t sendInitialisation(CMRInode *node);
+
+ // Process any data bytes received from a CMRInode.
+ void processIncoming();
+ // Process any outgoing traffic that is due.
+ void processOutgoing();
+ // Enable transmitter
+ void enableTransmitter();
+ // Disable transmitter and enable receiver
+ void disableTransmitter();
+
+
+public:
+ uint8_t getBusNumber() {
+ return _busNo;
+ }
+
+ static CMRIbus *findBus(uint8_t busNo) {
+ for (CMRIbus *bus=_busList; bus!=NULL; bus=bus->_nextBus) {
+ if (bus->_busNo == busNo) return bus;
+ }
+ return NULL;
+ }
+};
+
+#endif // IO_CMRI_H