mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2024-11-27 01:56:14 +01:00
473 lines
19 KiB
C++
473 lines
19 KiB
C++
/*
|
|
* © 2021, 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* Each node on the network is configured with a node number in the range 0-254.
|
|
* The remoting configuration defines, for each pin to be available remotely,
|
|
* the node number and the VPIN number on that node. The configuration must
|
|
* match in all nodes, since it is used by the sending node to identify the node
|
|
* and VPIN to which a write command is to be sent, and the VPIN number for a
|
|
* sensor/input, and on the receiving node to identify the node from which a
|
|
* sensor/input value is being sourced.
|
|
*
|
|
* The node number is also used in the network driver's address
|
|
* field. Number 255 is treated as a multicast address. All stations listen on
|
|
* their own address and on the multicast address.
|
|
*
|
|
* All nodes send regular multicast packets containing the latest values of the
|
|
* sensors as they know them. On receipt of such a packet, each node extracts
|
|
* the states of the sensors which are sourced by the originating node, and
|
|
* updates the values in its own local data. Thus, each node has a copy of the
|
|
* states of all digital input pin values that are defined in the remoting
|
|
* configuration. Multicasts are sent frequently, so if one is missed
|
|
* then, like a London bus, another will be along shortly.
|
|
*
|
|
* Commands (originating from write() or writeAnalogue() calls) are sent
|
|
* immediately, directly from the originating node to the target node. This
|
|
* is done with acknowlegements enabled to maximise the probability of
|
|
* successful delivery.
|
|
*
|
|
* Usage:
|
|
* First declare, for each remote pin in the common area, the mapping onto
|
|
* a node and VPIN number. The array below assumes that the first remote
|
|
* VPIN is 4000. The REMOTEPINS definition
|
|
* should be the same on all nodes in the network. For outputs, it is the
|
|
* definition in the sending node that dictates which node and VPIN the
|
|
* action is performed on. For inputs, the value is placed into the
|
|
* VPIN location defined in the sending node (that scans the input value),
|
|
* but the value is only accepted in the receiving node if its definition
|
|
* shows that the signal originates in the sending node.
|
|
*
|
|
* Example to go into mySetup() function in mySetup.cpp:
|
|
* REMOTEPINS rpins[] = {
|
|
* {0,30,RPIN_OUT}, //4000 Node 0 GPIO pin 30 (output)
|
|
* {1,30,RPIN_IN}, //4001 Node 1 GPIO pin 30 (input)
|
|
* {1,100,RPIN_INOUT}, //4002 Node 1 Servo (PCA9685) pin (output to servo, input busy flag)
|
|
* {1,164,RPIN_IN}, //4003 Node 1 GPIO extender (MCP23017) pin (input)
|
|
* {2,164,RPIN_IN} //4004 Node 2 GPIO extender (MCP23017) pin (input)
|
|
* }
|
|
* // FirstVPIN, nPins, thisNode, pinDefs, CEPin, CSNPin
|
|
* Network::create(4000, NUMREMOTEPINS(rpins), 0, rpins, new RF24Driver(48, 49));
|
|
*
|
|
* This example defines VPINs 4000-4004 which map onto pins on nodes 0, 1 and 2.
|
|
* The network device in this case is an nRF24L01, which has to be connected to the hardware
|
|
* MISO, MOSI, SCK and CS pins of the microcontroller; in addition, the CE and
|
|
* CSN pins on the nRF24 are connected to two pins (48 and 49 above).
|
|
*
|
|
* If any of pins 4000-4004 are referenced by turnouts, outputs or sensors, or by EX-RAIL,
|
|
* then the corresponding remote pin state will be retrieved or updated.
|
|
* For example, in EX-RAIL,
|
|
* SET(4000) on node 1 or 2 will set pin 30 on Node 0 to +5V (pin is put into output mode on first write).
|
|
* AT(4001) on node 0 or 2 will wait until the sensor attached to pin 30 on Node 1 activates.
|
|
* SERVO(4002,300,2) on node 0 or 2 will reposition the servo on Node 1 PCA9685 module to position 300, and
|
|
* AT(-4002) will wait until the servo has finished moving.
|
|
*
|
|
* The following sensor definition on node 0 will map onto VPIN 4004, i.e. Node 2 VPIN 164,
|
|
* which is the first pin on the first MCP23017:
|
|
* <S 1 4004 0>
|
|
* and when a sensor attached to the pin on node 2 is activated (pin pulled down to 0V) the following
|
|
* message will be generated on node 0:
|
|
* <Q 1>
|
|
* When the sensor deactivates, the following message will be generated on node 0:
|
|
* <q 1>
|
|
*/
|
|
|
|
#ifndef IO_NETWORK_H
|
|
#define IO_NETWORK_H
|
|
|
|
#include "IODevice.h"
|
|
#include "RF24.h"
|
|
|
|
// Macros and type for creating the remote pin definitions.
|
|
// The definitions are stored in PROGMEM to reduce RAM requirements.
|
|
// The flags byte contains, in the low 2 bits, RPIN_IN, RPIN_OUT or RPIN_INOUT.
|
|
typedef struct { uint8_t node; VPIN vpin; uint8_t flags; } RPIN;
|
|
#define REMOTEPINS static const RPIN PROGMEM
|
|
#define NUMREMOTEPINS(x) (sizeof(x)/sizeof(RPIN))
|
|
enum {
|
|
RPIN_IN=1,
|
|
RPIN_OUT=2,
|
|
RPIN_INOUT=RPIN_IN|RPIN_OUT,
|
|
};
|
|
|
|
// Define interface for network driver. This should be implemented for each supported
|
|
// network type.
|
|
// class NetInterface {
|
|
// public:
|
|
// bool begin();
|
|
// bool sendCommand(uint8_t node, const uint8_t buffer[], uint8_t size);
|
|
// bool available();
|
|
// uint8_t read(uint8_t buffer[], uint8_t size);
|
|
// void loop();
|
|
// };
|
|
|
|
// Class implementing the Application-layer network functionality.
|
|
// This is implemented as an IODevice instance so it can be easily
|
|
// plugged in to the HAL framwork.
|
|
template <class NetInterface>
|
|
class Network : public IODevice {
|
|
|
|
private:
|
|
const RPIN *_pinDefs; // May need to become a far pointer!
|
|
// Time of last loop execution
|
|
unsigned long _lastExecutionTime;
|
|
// Current digital values for remoted pins, stored as a bit field
|
|
uint8_t *_pinValues;
|
|
// Number of the current node (1-254)
|
|
uint8_t _thisNode;
|
|
// Maximum size of payload (must be 32 or less for RF24)
|
|
static const uint8_t maxPayloadSize = 32;
|
|
bool _updatePending;
|
|
int _nextSendPin;
|
|
unsigned long _lastMulticastTime;
|
|
int _firstPinToSend; // must be a multiple of 8
|
|
int _numPinsToSend; // need not be a multiple of 8
|
|
NetInterface *_netDriver;
|
|
|
|
// List of network commands
|
|
enum : uint8_t {
|
|
NET_CMD_WRITE = 0,
|
|
NET_CMD_WRITEANALOGUE = 1,
|
|
NET_CMD_VALUEUPDATE = 2,
|
|
};
|
|
|
|
// Field Positions in Network Header
|
|
enum NetHeader {
|
|
IONET_SENDNODE = 0, // for VALUEUPDATE
|
|
IONET_DESTNODE = 0, // for WRITE/WRITEANALOGUE
|
|
IONET_CMDTYPE = 1,
|
|
IONET_VPIN = 2,
|
|
IONET_VPIN_H = 2,
|
|
IONET_VPIN_L = 3,
|
|
IONET_DATA = 4,
|
|
};
|
|
|
|
public:
|
|
// Constructor performs static initialisation of the device object
|
|
Network (VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], NetInterface *netDriver) {
|
|
_firstVpin = firstVpin;
|
|
_nPins = nPins;
|
|
_thisNode = thisNode;
|
|
_pinDefs = pinDefs;
|
|
_pinValues = (uint8_t *)calloc((nPins+7)/8, 1); // Allocate space for input values.
|
|
_netDriver = netDriver;
|
|
addDevice(this);
|
|
|
|
// Identify which pins are allocated to this node.
|
|
_firstPinToSend = -1;
|
|
int lastPinToSend = 0;
|
|
for (int pin=0; pin<_nPins; pin++) {
|
|
uint8_t node = GETFLASH(&_pinDefs[pin].node);
|
|
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
|
|
// Check if the pin is an input on this node?
|
|
if (node == _thisNode && (flags & RPIN_IN)) {
|
|
if (_firstPinToSend==-1) _firstPinToSend = pin;
|
|
lastPinToSend = pin;
|
|
}
|
|
//DIAG(F("Node=%d FirstPin=%d, NumPins=%d"), node, _firstPinToSend, _numPinsToSend);
|
|
}
|
|
// Round down to multiple of 8 (byte boundary).
|
|
_firstPinToSend /= 8;
|
|
_firstPinToSend *= 8;
|
|
_numPinsToSend = lastPinToSend - _firstPinToSend + 1;
|
|
// Restrict to the max that fit in a packet
|
|
_numPinsToSend = min(8*(MAX_MSG_SIZE-IONET_DATA),_numPinsToSend);
|
|
//DIAG(F("FirstPin=%d, NumPins=%d"), _firstPinToSend, _numPinsToSend);
|
|
|
|
// Prepare for first transmission
|
|
_nextSendPin = _firstPinToSend;
|
|
}
|
|
|
|
// Static create function provides alternative way to create object
|
|
static void create(VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], NetInterface *netDriver) {
|
|
new Network(firstVpin, nPins, thisNode, pinDefs, netDriver);
|
|
}
|
|
|
|
protected:
|
|
// _begin function called to perform dynamic initialisation of the device
|
|
void _begin() override {
|
|
if (_netDriver->begin(_thisNode)) {
|
|
_display();
|
|
_deviceState = DEVSTATE_NORMAL;
|
|
_lastMulticastTime = _lastExecutionTime = micros();
|
|
_updatePending = true;
|
|
} else {
|
|
// Error in initialising
|
|
DIAG(F("Network Failed to initialise"));
|
|
_deviceState = DEVSTATE_FAILED;
|
|
}
|
|
}
|
|
|
|
// _read function - just return pin value (updated in _loop when message received from remote node)
|
|
int _read(VPIN vpin) override {
|
|
int pin = vpin - _firstVpin;
|
|
uint8_t mask = 1 << (pin & 7);
|
|
int byteIndex = pin / 8;
|
|
return (_pinValues[byteIndex] & mask) ? 1 : 0;
|
|
}
|
|
|
|
// _write (digital) - send command directly to the appropriate remote node.
|
|
void _write(VPIN vpin, int value) override {
|
|
// Send message
|
|
int pin = vpin - _firstVpin;
|
|
uint8_t node = GETFLASH(&_pinDefs[pin].node);
|
|
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
|
|
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
|
|
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
|
|
#ifdef DIAG_IO
|
|
DIAG(F("Network: write(%d,%d)=>send(%d,\"write(%d,%d)\")"), vpin, value, node, remoteVpin, value);
|
|
#endif
|
|
|
|
netBuffer[IONET_DESTNODE] = node;
|
|
netBuffer[IONET_CMDTYPE] = NET_CMD_WRITE;
|
|
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
|
|
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
|
|
netBuffer[IONET_DATA] = (uint8_t)value;
|
|
// Set up to send to the specified node address
|
|
_netDriver->sendCommand(node, netBuffer, IONET_DATA+1);
|
|
}
|
|
}
|
|
|
|
// _writeAnalogue - send command directly to the appropriate remote node.
|
|
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
|
|
// Send message
|
|
int pin = vpin - _firstVpin;
|
|
uint8_t node = GETFLASH(&_pinDefs[pin].node);
|
|
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
|
|
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
|
|
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
|
|
#ifdef DIAG_IO
|
|
DIAG(F("Network: writeAnalogue(%d,%d,%d,%d)=>send(%d,\"writeAnalogue(%d,%d,...)\")"),
|
|
vpin, value, param1, param2, node, remoteVpin, value);
|
|
#endif
|
|
|
|
netBuffer[IONET_DESTNODE] = node;
|
|
netBuffer[IONET_CMDTYPE] = NET_CMD_WRITEANALOGUE;
|
|
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
|
|
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
|
|
netBuffer[IONET_DATA+0] = getMsb(value);
|
|
netBuffer[IONET_DATA+1] = getLsb(value);
|
|
netBuffer[IONET_DATA+2] = param1;
|
|
netBuffer[IONET_DATA+3] = getMsb(param2);
|
|
netBuffer[IONET_DATA+4] = getLsb(param2);
|
|
// Set up to send to the specified node address
|
|
_netDriver->sendCommand(node, netBuffer, IONET_DATA+5);
|
|
}
|
|
}
|
|
|
|
// _loop function - check for, and process, received data from RF24, and send any
|
|
// updates that are due.
|
|
void _loop(unsigned long currentMicros) override {
|
|
|
|
// Perform cyclic netdriver functions, including switching back to receive mode
|
|
// (for half-duplex network drivers) and receiving input packets.
|
|
_netDriver->loop();
|
|
|
|
// Check for incoming data
|
|
if (_netDriver->available())
|
|
processReceivedData();
|
|
|
|
// Force a data update broadcast every 1000ms irrespective of whether there are
|
|
// data changes or not.
|
|
if (currentMicros - _lastMulticastTime > (1000 * 1000UL))
|
|
_updatePending = true;
|
|
|
|
// Send out data update broadcasts once every 20ms if there are changes
|
|
if (currentMicros - _lastExecutionTime > (20 * 1000UL)) {
|
|
// Broadcast updates to all other nodes. The preparation is done in a number of
|
|
// successive calls, and when sendSensorUpdates() returns true it has completed.
|
|
if (sendSensorUpdates()) {
|
|
_lastExecutionTime = currentMicros; // Send complete, wait for next time
|
|
}
|
|
}
|
|
}
|
|
|
|
void _display() override {
|
|
DIAG(F("Network Configured on Vpins:%d-%d Node:%d%S"),
|
|
_firstVpin, _firstVpin+_nPins-1, _thisNode, (_deviceState==DEVSTATE_FAILED) ? F(" OFFLINE") : F(""));
|
|
}
|
|
|
|
private:
|
|
// Send sensor updates only if one or more locally sourced inputs that
|
|
// are mapped to remote VPINs have changed state.
|
|
//
|
|
bool sendSensorUpdates() {
|
|
// This loop is split into multiple loop() entries, so as not to hog
|
|
// the cpu for too long.
|
|
|
|
if (_numPinsToSend == 0) return true; // No pins to send from this node.
|
|
|
|
// Update the _pinValues bitfield to reflect the current values of local pins.
|
|
// Process maximum of 5 pins per entry.
|
|
uint8_t count = 5;
|
|
bool state;
|
|
// First time through, _nextSendPin is equal to _firstPinToSend.
|
|
for (int pin=_nextSendPin; pin<_firstPinToSend+_numPinsToSend; pin++) {
|
|
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
|
|
// Is the pin an input on this node?
|
|
if ((flags & RPIN_IN) && GETFLASH(&_pinDefs[pin].node) == _thisNode) {
|
|
// Local input pin, read and update current state of input
|
|
VPIN localVpin = GETFLASHW(&_pinDefs[pin].vpin);
|
|
if (localVpin != VPIN_NONE) {
|
|
state = IODevice::read(localVpin);
|
|
uint16_t byteIndex = pin / 8;
|
|
uint8_t bitMask = 1 << (pin & 7);
|
|
uint8_t byteValue = _pinValues[byteIndex];
|
|
bool oldState = byteValue & bitMask;
|
|
if (state != oldState) {
|
|
// Store state in remote values array
|
|
if (state)
|
|
byteValue |= bitMask;
|
|
else
|
|
byteValue &= ~bitMask;
|
|
_pinValues[byteIndex] = byteValue;
|
|
_updatePending = true;
|
|
}
|
|
if (--count == 0) {
|
|
// Done enough checks for this entry, resume on next one.
|
|
_nextSendPin = pin+1;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// When we get here, we've updated the _pinValues array. See if an
|
|
// update is due.
|
|
if (_updatePending) {
|
|
// On master and on slave, send pin states to other nodes
|
|
netBuffer[IONET_SENDNODE] = _thisNode; // Originating node
|
|
netBuffer[IONET_CMDTYPE] = NET_CMD_VALUEUPDATE;
|
|
// The packet size is 32 bytes, header is 4 bytes, so 28 bytes of data.
|
|
// We can therefore send up to 224 binary states per packet.
|
|
int byteCount = (_numPinsToSend+7)/8;
|
|
VPIN remoteVpin = _firstVpin+_firstPinToSend;
|
|
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
|
|
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
|
|
|
|
// Copy from pinValues array into buffer. This is why _firstPinToSend must be a multiple of 8.
|
|
memcpy(&netBuffer[IONET_DATA], &_pinValues[_firstPinToSend/8], byteCount);
|
|
|
|
// Broadcast update
|
|
_netDriver->sendCommand(255, netBuffer, IONET_DATA + byteCount);
|
|
|
|
//DIAG(F("Sent %d bytes: %x %x ..."), byteCount, netBuffer[4], netBuffer[5]);
|
|
_lastMulticastTime = micros();
|
|
_updatePending = false;
|
|
}
|
|
// Set next pin ready for next entry.
|
|
_nextSendPin = _firstPinToSend;
|
|
|
|
return true; // Done all we need to for this cycle.
|
|
}
|
|
|
|
// Read next packet from the device's input buffers. Decode the message,
|
|
// and take the appropriate action.
|
|
// The packet may be a command to do an output write (digital or analogue), or
|
|
// it may be an update for digital input signals.
|
|
// For digital input signals, the values are broadcast from the node that is
|
|
// the pin source to all the other nodes.
|
|
void processReceivedData() {
|
|
// Read packet
|
|
uint8_t size = _netDriver->read(netBuffer, sizeof(netBuffer));
|
|
if (size < IONET_DATA) return; // packet too short.
|
|
// Extract command type from packet.
|
|
uint8_t command = netBuffer[IONET_CMDTYPE];
|
|
//DIAG(F("Received %d bytes, type=%d"), size, command);
|
|
// Process received data
|
|
switch (command) {
|
|
case NET_CMD_WRITE: // Digital write command
|
|
{
|
|
uint8_t targetNode = netBuffer[IONET_DESTNODE];
|
|
if (targetNode == _thisNode && size == IONET_DATA+1) {
|
|
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
|
|
uint8_t state = netBuffer[IONET_DATA];
|
|
IODevice::write(vpin, state);
|
|
}
|
|
}
|
|
break;
|
|
case NET_CMD_WRITEANALOGUE: // Analogue write command
|
|
{
|
|
uint8_t targetNode = netBuffer[IONET_DESTNODE];
|
|
if (targetNode == _thisNode && size == IONET_DATA+5) {
|
|
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
|
|
int value = makeWord(netBuffer[IONET_DATA], netBuffer[IONET_DATA+1]);
|
|
uint8_t param1 = netBuffer[IONET_DATA+2];
|
|
uint16_t param2 = makeWord(netBuffer[IONET_DATA+3], netBuffer[IONET_DATA+4]);
|
|
IODevice::writeAnalogue(vpin, value, param1, param2);
|
|
// Set the local value for the pin, used by isBusy(),
|
|
// and subsequently updated by the remote node.
|
|
_pinValues[vpin-_firstVpin] = true;
|
|
}
|
|
}
|
|
break;
|
|
case NET_CMD_VALUEUPDATE: // Updates of input states (sensors etc).
|
|
{
|
|
uint8_t sendingNode = netBuffer[IONET_SENDNODE];
|
|
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
|
|
//DIAG(F("Node %d Size %d VPIN %d Rx States %x"), sendingNode, size, vpin, netBuffer[IONET_DATA]);
|
|
|
|
// Read through the buffer one byte at a time.
|
|
uint8_t *buffPtr = &netBuffer[IONET_DATA];
|
|
uint8_t *bitFieldPtr = &_pinValues[(vpin-_firstVpin)/8];
|
|
|
|
int currentPin = vpin - _firstVpin;
|
|
for (int byteNo=0; byteNo<size-4 && currentPin<_nPins; byteNo++) {
|
|
// Now work through the received byte examining each bit.
|
|
uint8_t byteValue = *buffPtr++;
|
|
uint8_t bitFieldValue = *bitFieldPtr;
|
|
uint8_t bitMask = 1;
|
|
for (int bitNo=0; bitNo<8 && currentPin<_nPins; bitNo++) {
|
|
// Process incoming value if it's come from the pin source node
|
|
uint8_t pinSource = GETFLASH(&_pinDefs[currentPin].node);
|
|
if (sendingNode == pinSource) {
|
|
if (byteValue & bitMask)
|
|
bitFieldValue |= bitMask;
|
|
else
|
|
bitFieldValue &= ~bitMask;
|
|
}
|
|
bitMask <<= 1;
|
|
currentPin++;
|
|
}
|
|
// Store the modified byte back
|
|
*bitFieldPtr++ = bitFieldValue;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Helper functions for packing/unpacking buffers.
|
|
inline uint16_t makeWord(uint8_t msb, uint8_t lsb) {
|
|
return ((uint16_t)msb << 8) | lsb;
|
|
}
|
|
inline uint8_t getMsb(uint16_t w) {
|
|
return w >> 8;
|
|
}
|
|
inline uint8_t getLsb(uint16_t w) {
|
|
return w & 0xff;
|
|
}
|
|
// Data space for actual input and output buffer.
|
|
uint8_t netBuffer[maxPayloadSize];
|
|
|
|
};
|
|
|
|
#endif //IO_NETWORK_H
|