1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-11-22 23:56:13 +01:00

Squash all commits on RMFT branch to create EX-RAIL branch

This commit is contained in:
FrightRisk 2021-08-03 17:12:25 -04:00
parent f8fb08e331
commit 5eff4c5ee5
54 changed files with 4777 additions and 614 deletions

6
.gitignore vendored
View File

@ -9,3 +9,9 @@ Release/*
config.h
.vscode/extensions.json
mySetup.h
mySetup.cpp
myAutomation.h
myFilter.cpp
myAutomation.h
myFilter.cpp
myLayout.h

View File

@ -44,7 +44,6 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCCEX.h"
// Create a serial command parser for the USB connection,
@ -85,11 +84,20 @@ void setup()
// detailed pin mappings and may also require modified subclasses of the MotorDriver to implement specialist logic.
// STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h
DCC::begin(MOTOR_SHIELD_TYPE);
// Start RMFT (ignored if no automnation)
RMFT::begin();
// Link to and call mySetup() function (if defined in the build in mySetup.cpp).
// The contents will depend on the user's system hardware configuration.
// The mySetup.cpp file is a standard C++ module so has access to all of the DCC++EX APIs.
extern __attribute__((weak)) void mySetup();
if (mySetup) {
mySetup();
}
#if defined(RMFT_ACTIVE)
RMFT::begin();
#endif
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. throught the normal text commands.
#if __has_include ( "mySetup.h")
#define SETUP(cmd) serialParser.parse(F(cmd))
#include "mySetup.h"
@ -123,15 +131,16 @@ void loop()
EthernetInterface::loop();
#endif
#if defined(RMFT_ACTIVE)
RMFT::loop();
#endif
RMFT::loop(); // ignored if no automation
#if defined(LCN_SERIAL)
LCN::loop();
#endif
LCDDisplay::loop(); // ignored if LCD not in use
// Handle/update IO devices.
IODevice::loop();
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop

View File

@ -24,6 +24,7 @@
#include "GITHUB_SHA.h"
#include "version.h"
#include "FSH.h"
#include "IODevice.h"
// This module is responsible for converting API calls into
// messages to be sent to the waveform generator.
@ -52,6 +53,9 @@ void DCC::begin(const FSH * motorShieldName, MotorDriver * mainDriver, MotorDriv
shieldName=(FSH *)motorShieldName;
StringFormatter::send(Serial,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
// Initialise HAL layer before reading EEprom.
IODevice::begin();
// Load stuff from EEprom
(void)EEPROM; // tell compiler not to warn this is unused
EEStore::init();

View File

@ -37,10 +37,11 @@
#include "LCD_Implementation.h"
#include "LCN.h"
#include "freeMemory.h"
#include "IODevice.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "RMFT.h"
#if __has_include ( "myAutomation.h")
#include "RMFT.h"
#define RMFT_ACTIVE
#endif
#endif

View File

@ -344,7 +344,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|| ((subaddress & 0x03) != subaddress) // invalid subaddress (limit 2 bits )
|| ((p[activep] & 0x01) != p[activep]) // invalid activate 0|1
) break;
// TODO: Trigger configurable range of addresses on local VPins.
DCC::setAccessory(address, subaddress,p[activep]==1);
}
return;
@ -579,10 +579,8 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
return true;
case 3: // <Z ID PIN IFLAG>
if (p[0] < 0 ||
p[1] > 255 || p[1] <= 1 || // Pins 0 and 1 are Serial to USB
p[2] < 0 || p[2] > 7 )
return false;
if (p[0] < 0 || p[2] < 0 || p[2] > 7 )
return false;
if (!Output::create(p[0], p[1], p[2], 1))
return false;
StringFormatter::send(stream, F("<O>\n"));
@ -600,7 +598,7 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
{
gotone = true;
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.iFlag, tt->data.oStatus);
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.flags, tt->data.active);
}
return gotone;
}
@ -662,8 +660,7 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
{
gotOne = true;
StringFormatter::send(stream, F("<H %d %d %d %d>\n"), tt->data.id, tt->data.address,
tt->data.subAddress, (tt->data.tStatus & STATUS_ACTIVE)!=0);
tt->print(stream);
}
return gotOne; // will <X> if none found
}
@ -680,18 +677,15 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
if (!tt)
return false;
tt->activate(p[1]);
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
StringFormatter::send(stream, F("<H %d %d>\n"), p[0], tt->data.active);
}
return true;
case 3: // <T id addr subaddr> define turnout
if (!Turnout::create(p[0], p[1], p[2]))
default: // Anything else is handled by Turnout class.
if (!Turnout::create(p[0], params-1, &p[1]))
return false;
StringFormatter::send(stream, F("<O>\n"));
return true;
default:
return false; // will <x>
}
}
@ -713,13 +707,13 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
return true;
case 0: // <S> list sensor definitions
if (Sensor::firstSensor == NULL)
return false;
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
{
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
}
return true;
if (Sensor::firstSensor == NULL)
return false;
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
{
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
}
return true;
default: // invalid number of arguments
break;

View File

@ -72,6 +72,7 @@ void EEStore::store(){
Sensor::store();
Output::store();
EEPROM.put(0,eeStore->data);
DIAG(F("EEPROM used: %d bytes"), EEStore::pointer());
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -29,7 +29,7 @@ extern ExternalEEPROM EEPROM;
#include <EEPROM.h>
#endif
#define EESTORE_ID "DCC++"
#define EESTORE_ID "DCC++0"
struct EEStoreData{
char id[sizeof(EESTORE_ID)];

View File

@ -18,14 +18,40 @@
*/
#include <stdarg.h>
#include <Wire.h>
#include "I2CManager.h"
#include "DIAG.h"
// If not already initialised, initialise I2C (wire).
// Include target-specific portions of I2CManager class
#if defined(I2C_USE_WIRE)
#include "I2CManager_Wire.h"
#elif defined(ARDUINO_ARCH_AVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_AVR.h" // Uno/Nano/Mega2560
#elif defined(ARDUINO_ARCH_MEGAAVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi
#else
#define I2C_USE_WIRE
#include "I2CManager_Wire.h" // Other platforms
#endif
// If not already initialised, initialise I2C
void I2CManagerClass::begin(void) {
//setTimeout(25000); // 25 millisecond timeout
if (!_beginCompleted) {
Wire.begin();
_beginCompleted = true;
_initialise();
// Probe and list devices.
bool found = false;
for (byte addr=1; addr<127; addr++) {
if (exists(addr)) {
found = true;
DIAG(F("I2C Device found at x%x"), addr);
}
}
if (!found) DIAG(F("No I2C Devices found"));
}
}
@ -34,8 +60,8 @@ void I2CManagerClass::begin(void) {
void I2CManagerClass::setClock(uint32_t speed) {
if (speed < _clockSpeed && !_clockSpeedFixed) {
_clockSpeed = speed;
Wire.setClock(_clockSpeed);
}
_setClock(_clockSpeed);
}
// Force clock speed to that specified. It can then only
@ -44,39 +70,21 @@ void I2CManagerClass::forceClock(uint32_t speed) {
if (!_clockSpeedFixed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
Wire.setClock(_clockSpeed);
_setClock(_clockSpeed);
}
}
// Check if specified I2C address is responding.
// Returns 0 if OK, or error code.
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
uint8_t I2CManagerClass::checkAddress(uint8_t address) {
begin();
Wire.beginTransmission(address);
return Wire.endTransmission();
return write(address, NULL, 0);
}
bool I2CManagerClass::exists(uint8_t address) {
return checkAddress(address)==0;
}
// Write a complete transmission to I2C using a supplied buffer of data
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size) {
Wire.beginTransmission(address);
Wire.write(buffer, size);
return Wire.endTransmission();
}
// Write a complete transmission to I2C using a supplied buffer of data in Flash
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size) {
uint8_t ramBuffer[size];
memcpy_P(ramBuffer, buffer, size);
return write(address, ramBuffer, size);
}
// Write a complete transmission to I2C using a list of data
uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
/***************************************************************************
* Write a transmission to I2C using a list of data (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t address, uint8_t nBytes, ...) {
uint8_t buffer[nBytes];
va_list args;
va_start(args, nBytes);
@ -86,30 +94,38 @@ uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
return write(address, buffer, nBytes);
}
// Write a command and read response, returns number of bytes received.
// Different modules use different ways of accessing registers:
// PCF8574 I/O expander justs needs the address (no data);
// PCA9685 needs a two byte command to select the register(s) to be read;
// MCP23016 needs a one-byte command to select the register.
// Some devices use 8-bit registers exclusively and some have 16-bit registers.
// Therefore the following function is general purpose, to apply to any
// type of I2C device.
//
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeBuffer[], uint8_t writeSize) {
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
Wire.endTransmission(false); // Don't free bus yet
}
Wire.requestFrom(address, readSize);
uint8_t nBytes = 0;
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
return nBytes;
/***************************************************************************
* Initiate a write to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
I2CRB req;
uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
// Overload of read() to allow command to be specified as a series of bytes.
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * data, uint8_t dataLen) {
I2CRB req;
uint8_t status = write_P(i2cAddress, data, dataLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen)
{
I2CRB req;
uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...) {
va_list args;
@ -122,8 +138,72 @@ uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t rea
return read(address, readBuffer, readSize, writeBuffer, writeSize);
}
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize) {
return read(address, readBuffer, readSize, NULL, 0);
/***************************************************************************
* Finish off request block by posting status, etc. (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {
if ((status == I2C_STATUS_OK) && rb)
status = rb->wait();
return status;
}
/***************************************************************************
* Declare singleton class instance.
***************************************************************************/
I2CManagerClass I2CManager = I2CManagerClass();
/////////////////////////////////////////////////////////////////////////////
// Helper functions associated with I2C Request Block
/////////////////////////////////////////////////////////////////////////////
/***************************************************************************
* Block waiting for request block to complete, and return completion status
***************************************************************************/
uint8_t I2CRB::wait() {
do
I2CManager.loop();
while (status==I2C_STATUS_PENDING);
return status;
}
/***************************************************************************
* Check whether request is still in progress.
***************************************************************************/
bool I2CRB::isBusy() {
I2CManager.loop();
return (status==I2C_STATUS_PENDING);
}
/***************************************************************************
* Helper functions to fill the I2CRequest structure with parameters.
***************************************************************************/
void I2CRB::setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
this->i2cAddress = i2cAddress;
this->writeLen = 0;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_READ;
this->status = I2C_STATUS_OK;
}
void I2CRB::setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_REQUEST;
this->status = I2C_STATUS_OK;
}
void I2CRB::setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readLen = 0;
this->operation = OPERATION_SEND;
this->status = I2C_STATUS_OK;
}
I2CManagerClass I2CManager = I2CManagerClass();

View File

@ -17,13 +17,16 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CManager_h
#define I2CManager_h
#ifndef I2CMANAGER_H
#define I2CMANAGER_H
#include <inttypes.h>
#include "FSH.h"
/*
* Helper class to manage access to the I2C 'Wire' subsystem.
* Manager for I2C communications. For portability, it allows use
* of the Wire class, but also has a native implementation for AVR
* which supports non-blocking queued I/O requests.
*
* Helps to avoid calling Wire.begin() multiple times (which is not)
* entirely benign as it reinitialises).
@ -33,14 +36,148 @@
*
* Thirdly, it provides a convenient way to check whether there is a
* device on a particular I2C address.
*
* Non-blocking requests are issued by creating an I2C Request Block
* (I2CRB) which is then added to the I2C manager's queue. The
* application refers to this block to check for completion of the
* operation, and for reading completion status.
*
* Examples:
* I2CRB rb;
* uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb);
* ...
* if (!rb.isBusy()) {
* status = rb.status;
* // Repeat write
* I2CManager.queueRequest(&rb);
* ...
* status = rb.wait(); // Wait for completion and read status
* }
* ...
* I2CRB rb2;
* outbuffer[0] = 12; // Register number in I2C device to be read
* rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1);
* status = I2CManager.queueRequest(&rb2);
* if (status == I2C_STATUS_OK) {
* status = rb2.wait();
* if (status == I2C_STATUS_OK) {
* registerValue = inBuffer[0];
* }
* }
* ...
*
* Synchronous (blocking) calls are also possible, e.g.
* status = I2CManager.write(address, buffer, sizeof(buffer));
*
* When using non-blocking requests, neither the I2CRB nor the input or output
* buffers should be modified until the I2CRB is complete (not busy).
*
* Timeout monitoring is possible, but requires that the following call is made
* reasonably frequently in the program's loop() function:
* I2CManager.loop();
*
*/
/*
* Future enhancement possibility:
*
* I2C Multiplexer (e.g. TCA9547, TCA9548)
*
* A multiplexer offers a way of extending the address range of I2C devices. For example, GPIO extenders use address range 0x20-0x27
* to are limited to 8 on a bus. By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64.
* And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's
* as many as 8192 input/output pins!
* Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices.
* The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus. For example, with one
* multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders).
* Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be
* put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions.
*
* Changes required: Increase the size of the I2CAddress field in the IODevice class from uint8_t to uint16_t.
* The most significant byte would contain a '1' bit flag, the multiplexer number (0-7) and bus number (0-7). Then, when performing
* an I2C operation, the I2CManager would check this byte and, if zero, do what it currently does. If the byte is non-zero, then
* that means the device is connected via a multiplexer so the I2C transaction should be preceded by a select command issued to the
* relevant multiplexer.
*
* Non-interrupting I2C:
*
* I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state
* machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function.
* The speed at which I2C operations can be performed then becomes highly dependent on the frequency that
* the loop() function is called, and may be adequate under some circumstances.
* The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use)
* becomes almost zero.
* This mechanism is under evaluation and should not be relied upon as yet.
*
*/
//#define I2C_USE_WIRE
#ifndef I2C_NO_INTERRUPTS
#define I2C_USE_INTERRUPTS
#endif
// Status codes for I2CRB structures.
enum : uint8_t {
I2C_STATUS_OK=0,
I2C_STATUS_TRUNCATED=1,
I2C_STATUS_DEVICE_NOT_PRESENT=2,
I2C_STATUS_TRANSMIT_ERROR=3,
I2C_STATUS_NEGATIVE_ACKNOWLEDGE=4,
I2C_STATUS_TIMEOUT=5,
I2C_STATUS_ARBITRATION_LOST=6,
I2C_STATUS_BUS_ERROR=7,
I2C_STATUS_UNEXPECTED_ERROR=8,
I2C_STATUS_PENDING=253,
};
// Status codes for the state machine (not returned to caller).
enum : uint8_t {
I2C_STATE_ACTIVE=253,
I2C_STATE_FREE=254,
I2C_STATE_CLOSING=255,
};
typedef enum : uint8_t
{
OPERATION_READ = 1,
OPERATION_REQUEST = 2,
OPERATION_SEND = 3,
OPERATION_SEND_P = 4,
} OperationEnum;
// Default I2C frequency
#ifndef I2C_FREQ
#define I2C_FREQ 400000L
#endif
// Struct defining a request context for an I2C operation.
struct I2CRB {
volatile uint8_t status; // Completion status, or pending flag (updated from IRC)
volatile uint8_t nBytes; // Number of bytes read (updated from IRC)
uint8_t wait();
bool isBusy();
inline void init() { status = I2C_STATUS_OK; };
void setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen);
void setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);
void setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);
uint8_t writeLen;
uint8_t readLen;
uint8_t operation;
uint8_t i2cAddress;
uint8_t *readBuffer;
const uint8_t *writeBuffer;
#if !defined(I2C_USE_WIRE)
I2CRB *nextRequest;
#endif
};
// I2C Manager
class I2CManagerClass {
public:
I2CManagerClass() {}
// If not already initialised, initialise I2C (wire).
void begin(void);
// Set clock speed to the lowest requested one.
@ -49,28 +186,87 @@ public:
void forceClock(uint32_t speed);
// Check if specified I2C address is responding.
uint8_t checkAddress(uint8_t address);
bool exists(uint8_t address);
inline bool exists(uint8_t address) {
return checkAddress(address)==I2C_STATUS_OK;
}
// Write a complete transmission to I2C from an array in RAM
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
// Write a complete transmission to I2C from an array in Flash
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
// Write a transmission to I2C from a list of bytes.
uint8_t write(uint8_t address, int nBytes, ...);
uint8_t write(uint8_t address, uint8_t nBytes, ...);
// Write a command from an array in RAM and read response
uint8_t read(uint8_t address, uint8_t writeBuffer[], uint8_t writeSize,
uint8_t readBuffer[], uint8_t readSize);
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);
// Write a command from an arbitrary list of bytes and read response
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...);
// Write a null command and read the response.
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize);
void queueRequest(I2CRB *req);
// Function to abort long-running operations.
void checkForTimeout();
// Loop method
void loop();
private:
bool _beginCompleted = false;
bool _clockSpeedFixed = false;
uint32_t _clockSpeed = 400000L; // 400kHz max on Arduino.
// Finish off request block by waiting for completion and posting status.
uint8_t finishRB(I2CRB *rb, uint8_t status);
void _initialise();
void _setClock(unsigned long);
#if !defined(I2C_USE_WIRE)
// I2CRB structs are queued on the following two links.
// If there are no requests, both are NULL.
// If there is only one request, then queueHead and queueTail both point to it.
// Otherwise, queueHead is the pointer to the first request in the queue and
// queueTail is the pointer to the last request in the queue.
// Within the queue, each request's nextRequest field points to the
// next request, or NULL.
// Mark volatile as they are updated by IRC and read/written elsewhere.
static I2CRB * volatile queueHead;
static I2CRB * volatile queueTail;
static volatile uint8_t status;
static I2CRB * volatile currentRequest;
static volatile uint8_t txCount;
static volatile uint8_t rxCount;
static volatile uint8_t bytesToSend;
static volatile uint8_t bytesToReceive;
static volatile uint8_t operation;
static volatile unsigned long startTime;
static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled.
void startTransaction();
// Low-level hardware manipulation functions.
static void I2C_init();
static void I2C_setClock(unsigned long i2cClockSpeed);
static void I2C_handleInterrupt();
static void I2C_sendStart();
static void I2C_sendStop();
static void I2C_close();
public:
void setTimeout(unsigned long value) { timeout = value;};
// handleInterrupt needs to be public to be called from the ISR function!
static void handleInterrupt();
#endif
};
extern I2CManagerClass I2CManager;
#endif
#endif

198
I2CManager_AVR.h Normal file
View File

@ -0,0 +1,198 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef I2CMANAGER_AVR_H
#define I2CMANAGER_AVR_H
#include <Arduino.h>
#include "I2CManager.h"
#include <avr/io.h>
#include <avr/interrupt.h>
/****************************************************************************
TWI State codes
****************************************************************************/
// General TWI Master staus codes
#define TWI_START 0x08 // START has been transmitted
#define TWI_REP_START 0x10 // Repeated START has been transmitted
#define TWI_ARB_LOST 0x38 // Arbitration lost
// TWI Master Transmitter staus codes
#define TWI_MTX_ADR_ACK 0x18 // SLA+W has been tramsmitted and ACK received
#define TWI_MTX_ADR_NACK 0x20 // SLA+W has been tramsmitted and NACK received
#define TWI_MTX_DATA_ACK 0x28 // Data byte has been tramsmitted and ACK received
#define TWI_MTX_DATA_NACK 0x30 // Data byte has been tramsmitted and NACK received
// TWI Master Receiver staus codes
#define TWI_MRX_ADR_ACK 0x40 // SLA+R has been tramsmitted and ACK received
#define TWI_MRX_ADR_NACK 0x48 // SLA+R has been tramsmitted and NACK received
#define TWI_MRX_DATA_ACK 0x50 // Data byte has been received and ACK tramsmitted
#define TWI_MRX_DATA_NACK 0x58 // Data byte has been received and NACK tramsmitted
// TWI Miscellaneous status codes
#define TWI_NO_STATE 0xF8 // No relevant state information available
#define TWI_BUS_ERROR 0x00 // Bus error due to an illegal START or STOP condition
#define TWI_TWBR ((F_CPU / I2C_FREQ) - 16) / 2 // TWI Bit rate Register setting.
#if defined(I2C_USE_INTERRUPTS)
#define ENABLE_TWI_INTERRUPT (1<<TWIE)
#else
#define ENABLE_TWI_INTERRUPT 0
#endif
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
TWBR = ((F_CPU / i2cClockSpeed) - 16) / 2;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
TWSR = 0;
TWBR = TWI_TWBR; // Set bit rate register (Baudrate). Defined in header file.
TWDR = 0xFF; // Default content = SDA released.
TWCR = (1<<TWINT); // Clear interrupt flag
pinMode(SDA, INPUT_PULLUP);
pinMode(SCL, INPUT_PULLUP);
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// We may have initiated a stop bit before this without waiting for it.
// Wait for stop bit to be sent before sending start.
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
// disable TWI
I2C_sendStop();
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler or,
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (!(TWCR & (1<<TWINT))) return; // Nothing to do.
uint8_t twsr = TWSR & 0xF8;
// Cases are ordered so that the most frequently used ones are tested first.
switch (twsr) {
case TWI_MTX_DATA_ACK: // Data byte has been transmitted and ACK received
case TWI_MTX_ADR_ACK: // SLA+W has been transmitted and ACK received
if (bytesToSend) { // Send first.
if (operation == OPERATION_SEND_P)
TWDR = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWDR = currentRequest->writeBuffer[txCount++];
bytesToSend--;
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
} else if (bytesToReceive) { // All sent, anything to receive?
while (TWCR & (1<<TWSTO)) {} // Wait for stop to be sent
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
} else { // Nothing left to send or receive
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
status = I2C_STATUS_OK;
}
break;
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
/* fallthrough */
case TWI_MRX_ADR_ACK: // SLA+R has been sent and ACK received
if (bytesToReceive <= 1) {
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception
} else {
// send ack
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
}
break;
case TWI_MRX_DATA_NACK: // Data byte has been received and NACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
status = I2C_STATUS_OK;
break;
case TWI_START: // START has been transmitted
case TWI_REP_START: // Repeated START has been transmitted
// Set up address and R/W
if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))
TWDR = (currentRequest->i2cAddress << 1) | 1; // SLA+R
else
TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
break;
case TWI_MTX_ADR_NACK: // SLA+W has been transmitted and NACK received
case TWI_MRX_ADR_NACK: // SLA+R has been transmitted and NACK received
case TWI_MTX_DATA_NACK: // Data byte has been transmitted and NACK received
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
status = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
break;
case TWI_ARB_LOST: // Arbitration lost
// Restart transaction from start.
I2C_sendStart();
break;
case TWI_BUS_ERROR: // Bus error due to an illegal START or STOP condition
default:
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
status = I2C_STATUS_TRANSMIT_ERROR;
}
}
#if defined(I2C_USE_INTERRUPTS)
ISR(TWI_vect) {
I2CManagerClass::handleInterrupt();
}
#endif
#endif /* I2CMANAGER_AVR_H */

160
I2CManager_Mega4809.h Normal file
View File

@ -0,0 +1,160 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef I2CMANAGER_MEGA4809_H
#define I2CMANAGER_MEGA4809_H
#include <Arduino.h>
#include "I2CManager.h"
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
uint16_t t_rise;
if (i2cClockSpeed < 200000) {
i2cClockSpeed = 100000;
t_rise = 1000;
} else if (i2cClockSpeed < 800000) {
i2cClockSpeed = 400000;
t_rise = 300;
} else if (i2cClockSpeed < 1200000) {
i2cClockSpeed = 1000000;
t_rise = 120;
} else {
i2cClockSpeed = 100000;
t_rise = 1000;
}
uint32_t baud = (F_CPU_CORRECTED / i2cClockSpeed - F_CPU_CORRECTED / 1000 / 1000
* t_rise / 1000 - 10) / 2;
TWI0.MBAUD = (uint8_t)baud;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
pinMode(PIN_WIRE_SDA, INPUT_PULLUP);
pinMode(PIN_WIRE_SCL, INPUT_PULLUP);
PORTMUX.TWISPIROUTEA |= TWI_MUX;
#if defined(I2C_USE_INTERRUPTS)
TWI0.MCTRLA = TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm;
#else
TWI0.MCTRLA = TWI_ENABLE_bm;
#endif
I2C_setClock(I2C_FREQ);
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
}
/***************************************************************************
* Initiate a start bit for transmission, followed by address and R/W
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || (operation == OPERATION_REQUEST & !bytesToSend))
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
else
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;
}
/***************************************************************************
* Initiate a stop bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
I2C_sendStop();
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler.
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
uint8_t currentStatus = TWI0.MSTATUS;
if (currentStatus & TWI_ARBLOST_bm) {
// Arbitration lost, restart
TWI0.MSTATUS = currentStatus; // clear all flags
I2C_sendStart(); // Reinitiate request
} else if (currentStatus & TWI_BUSERR_bm) {
// Bus error
status = I2C_STATUS_BUS_ERROR;
TWI0.MSTATUS = currentStatus; // clear all flags
} else if (currentStatus & TWI_WIF_bm) {
// Master write completed
if (currentStatus & TWI_RXACK_bm) {
// Nacked, send stop.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
status = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
} else if (bytesToSend) {
// Acked, so send next byte
if (currentRequest->operation == OPERATION_SEND_P)
TWI0.MDATA = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWI0.MDATA = currentRequest->writeBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
} else {
// No more data to send/receive. Initiate a STOP condition.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
status = I2C_STATUS_OK; // Done
}
} else if (currentStatus & TWI_RIF_bm) {
// Master read completed without errors
if (bytesToReceive) {
currentRequest->readBuffer[rxCount++] = TWI0.MDATA; // Store received byte
bytesToReceive--;
} else {
// Buffer full, issue nack/stop
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
status = I2C_STATUS_OK;
}
if (bytesToReceive) {
// More bytes to receive, issue ack and start another read
TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc;
} else {
// Transaction finished, issue NACK and STOP.
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
status = I2C_STATUS_OK;
}
}
}
/***************************************************************************
* Interrupt handler.
***************************************************************************/
ISR(TWI0_TWIM_vect) {
I2CManagerClass::handleInterrupt();
}
#endif

215
I2CManager_NonBlocking.h Normal file
View File

@ -0,0 +1,215 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef I2CMANAGER_NONBLOCKING_H
#define I2CMANAGER_NONBLOCKING_H
#include <Arduino.h>
#include "I2CManager.h"
#if defined(I2C_USE_INTERRUPTS)
#include <util/atomic.h>
#else
#define ATOMIC_BLOCK(x)
#define ATOMIC_RESTORESTATE
#endif
// This module is only compiled if I2C_USE_WIRE is not defined, so undefine it here
// to get intellisense to work correctly.
#if defined(I2C_USE_WIRE)
#undef I2C_USE_WIRE
#endif
/***************************************************************************
* Initialise the I2CManagerAsync class.
***************************************************************************/
void I2CManagerClass::_initialise()
{
queueHead = queueTail = NULL;
status = I2C_STATE_FREE;
I2C_init();
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
I2C_setClock(i2cClockSpeed);
}
/***************************************************************************
* Helper function to start operations, if the I2C interface is free and
* there is a queued request to be processed.
***************************************************************************/
void I2CManagerClass::startTransaction() {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
I2CRB *t = queueHead;
if ((status == I2C_STATE_FREE) && (t != NULL)) {
status = I2C_STATE_ACTIVE;
currentRequest = t;
rxCount = txCount = 0;
// Copy key fields to static data for speed.
operation = currentRequest->operation;
// Start the I2C process going.
I2C_sendStart();
startTime = micros();
}
}
}
/***************************************************************************
* Function to queue a request block and initiate operations.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
req->status = I2C_STATUS_PENDING;
req->nextRequest = NULL;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if (!queueTail)
queueHead = queueTail = req; // Only item on queue
else
queueTail = queueTail->nextRequest = req; // Add to end
}
startTransaction();
}
/***************************************************************************
* Initiate a write to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
req->operation = OPERATION_SEND_P;
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a read from the I2C device, optionally preceded by a write
* (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req)
{
// Make sure previous request has completed.
req->wait();
req->setRequestParams(i2cAddress, readBuffer, readLen, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* checkForTimeout() function, called from isBusy() and wait() to cancel
* requests that are taking too long to complete.
***************************************************************************/
void I2CManagerClass::checkForTimeout() {
unsigned long currentMicros = micros();
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
I2CRB *t = queueHead;
if (t && timeout > 0) {
// Check for timeout
if (currentMicros - startTime > timeout) {
// Excessive time. Dequeue request
queueHead = t->nextRequest;
if (!queueHead) queueTail = NULL;
currentRequest = NULL;
// Post request as timed out.
t->status = I2C_STATUS_TIMEOUT;
// Reset TWI interface so it is able to continue
// Try close and init, not entirely satisfactory but sort of works...
I2C_close(); // Shutdown and restart twi interface
I2C_init();
status = I2C_STATE_FREE;
// Initiate next queued request
startTransaction();
}
}
}
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {
#if !defined(I2C_USE_INTERRUPTS)
handleInterrupt();
#endif
// If free, initiate next transaction
startTransaction();
checkForTimeout();
}
/***************************************************************************
* Interupt handler. Call I2C state machine, and dequeue request
* if completed.
***************************************************************************/
void I2CManagerClass::handleInterrupt() {
I2C_handleInterrupt();
// Experimental -- perform the post processing with interrupts enabled.
//interrupts();
if (status!=I2C_STATUS_PENDING) {
// Remove completed request from head of queue
I2CRB * t;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
t = queueHead;
if (t != NULL) {
queueHead = t->nextRequest;
if (!queueHead) queueTail = queueHead;
t->nBytes = rxCount;
t->status = status;
}
// I2C state machine is now free for next request
status = I2C_STATE_FREE;
}
// Start next request (if any)
I2CManager.startTransaction();
}
}
// Fields in I2CManager class specific to Non-blocking implementation.
I2CRB * volatile I2CManagerClass::queueHead = NULL;
I2CRB * volatile I2CManagerClass::queueTail = NULL;
I2CRB * volatile I2CManagerClass::currentRequest = NULL;
volatile uint8_t I2CManagerClass::status = I2C_STATE_FREE;
volatile uint8_t I2CManagerClass::txCount;
volatile uint8_t I2CManagerClass::rxCount;
volatile uint8_t I2CManagerClass::operation;
volatile uint8_t I2CManagerClass::bytesToSend;
volatile uint8_t I2CManagerClass::bytesToReceive;
volatile unsigned long I2CManagerClass::startTime;
unsigned long I2CManagerClass::timeout = 0;
#endif

128
I2CManager_Wire.h Normal file
View File

@ -0,0 +1,128 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef I2CMANAGER_WIRE_H
#define I2CMANAGER_WIRE_H
#include <Arduino.h>
#include <Wire.h>
#include "I2CManager.h"
// This module is only compiled if I2C_USE_WIRE is defined, so define it here
// to get intellisense to work correctly.
#if !defined(I2C_USE_WIRE)
#define I2C_USE_WIRE
#endif
/***************************************************************************
* Initialise I2C interface software
***************************************************************************/
void I2CManagerClass::_initialise() {
Wire.begin();
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
Wire.setClock(i2cClockSpeed);
}
/***************************************************************************
* Initiate a write to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
Wire.beginTransmission(address);
if (size > 0) Wire.write(buffer, size);
rb->status = Wire.endTransmission();
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
uint8_t ramBuffer[size];
const uint8_t *p1 = buffer;
for (uint8_t i=0; i<size; i++)
ramBuffer[i] = GETFLASH(p1++);
return write(address, ramBuffer, size, rb);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation on Wire)
* If fewer than the number of requested bytes are received, status is I2C_STATUS_TRUNCATED.
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)
{
uint8_t status = I2C_STATUS_OK;
uint8_t nBytes = 0;
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
status = Wire.endTransmission(false); // Don't free bus yet
}
if (status == I2C_STATUS_OK) {
Wire.requestFrom(address, (size_t)readSize);
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
}
rb->nBytes = nBytes;
rb->status = status;
return I2C_STATUS_OK;
}
/***************************************************************************
* Function to queue a request block and initiate operations.
*
* For the Wire version, this executes synchronously, but the status is
* returned in the I2CRB as for the asynchronous version.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
uint8_t status;
switch (req->operation) {
case OPERATION_READ:
status = read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
break;
case OPERATION_SEND:
status = write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_SEND_P:
status = write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_REQUEST:
status = read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
break;
}
req->status = status;
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {}
// Loop function
void I2CManagerClass::checkForTimeout() {}
#endif

446
IODevice.cpp Normal file
View File

@ -0,0 +1,446 @@
/*
* © 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/>.
*/
#include <Arduino.h>
#include "IODevice.h"
#include "DIAG.h"
#include "FSH.h"
#include "IO_MCP23017.h"
#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR)
#define USE_FAST_IO
#endif
//==================================================================================================================
// Static methods
//------------------------------------------------------------------------------------------------------------------
// Static functions
// Static method to initialise the IODevice subsystem.
#if !defined(IO_NO_HAL)
// Create any standard device instances that may be required, such as the Arduino pins
// and PCA9685.
void IODevice::begin() {
// Initialise the IO subsystem
ArduinoPins::create(2, NUM_DIGITAL_PINS-3); // Reserve pins for direct access
// Predefine two PCA9685 modules 0x40-0x41
// Allocates 32 pins 100-131
PCA9685::create(100, 16, 0x40);
PCA9685::create(116, 16, 0x41);
// Predefine two MCP23017 module 0x20/0x21
// Allocates 32 pins 164-195
MCP23017::create(164, 16, 0x20);
MCP23017::create(180, 16, 0x21);
// Call the begin() methods of each configured device in turn
for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) {
dev->_begin();
}
}
// Overarching static loop() method for the IODevice subsystem. Works through the
// list of installed devices and calls their individual _loop() method.
// Devices may or may not implement this, but if they do it is useful for things like animations
// or flashing LEDs.
// The current value of micros() is passed as a parameter, so the called loop function
// doesn't need to invoke it.
void IODevice::loop() {
unsigned long currentMicros = micros();
// Call every device's loop function in turn, one per entry.
if (!_nextLoopDevice) _nextLoopDevice = _firstDevice;
_nextLoopDevice->_loop(currentMicros);
_nextLoopDevice = _nextLoopDevice->_nextDevice;
// Report loop time if diags enabled
#if defined(DIAG_LOOPTIMES)
static unsigned long lastMicros = 0;
static unsigned long maxElapsed = 0;
static unsigned long lastOutputTime = 0;
static unsigned long count = 0;
const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec
unsigned long elapsed = currentMicros - lastMicros;
// Ignore long loop counts while message is still outputting
if (currentMicros - lastOutputTime > 3000UL) {
if (elapsed > maxElapsed) maxElapsed = elapsed;
}
count++;
if (currentMicros - lastOutputTime > interval) {
if (lastOutputTime > 0)
LCD(1,F("Loop=%lus,%lus max"), interval/count, maxElapsed);
maxElapsed = 0;
count = 0;
lastOutputTime = currentMicros;
}
lastMicros = micros();
#endif
}
// Display a list of all the devices on the diagnostic stream.
void IODevice::DumpAll() {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
dev->_display();
}
}
// Determine if the specified vpin is allocated to a device.
bool IODevice::exists(VPIN vpin) {
return findDevice(vpin) != NULL;
}
// check whether the pin supports notification. If so, then regular _read calls are not required.
bool IODevice::hasCallback(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (!dev) return false;
return dev->_hasCallback(vpin);
}
// Remove specified device if one exists. This is necessary if devices are
// created on-the-fly by Turnouts, Sensors or Outputs since they may have
// been saved to EEPROM and recreated on start.
void IODevice::remove(VPIN vpin) {
// Only works if the object is exclusive, i.e. only one VPIN.
IODevice *previousDev = 0;
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin)) {
// Found object
if (dev->_isDeletable()) {
// First check it isn't next one to be processed by loop().
// If so, skip to the following one.
if (dev == _nextLoopDevice)
_nextLoopDevice = _nextLoopDevice->_nextDevice;
// Now unlink
if (!previousDev)
_firstDevice = dev->_nextDevice;
else
previousDev->_nextDevice = dev->_nextDevice;
delete dev;
#ifdef DIAG_IO
DIAG(F("IODevice deleted Vpin:%d"), vpin);
#endif
return;
}
}
previousDev = dev;
}
}
// Display (to diagnostics) details of the device.
void IODevice::_display() {
DIAG(F("Unknown device Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1);
}
// Find device associated with nominated Vpin and pass configuration values on to it.
// Return false if not found.
bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
IODevice *dev = findDevice(vpin);
if (dev) return dev->_configure(vpin, configType, paramCount, params);
return false;
}
// Write value to virtual pin(s). If multiple devices are allocated the same pin
// then only the first one found will be used.
void IODevice::write(VPIN vpin, int value) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_write(vpin, value);
return;
}
#ifdef DIAG_IO
//DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin);
#endif
}
// Write analogue value to virtual pin(s). If multiple devices are allocated the same pin
// then only the first one found will be used.
void IODevice::writeAnalogue(VPIN vpin, int value, int profile) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_writeAnalogue(vpin, value, profile);
return;
}
#ifdef DIAG_IO
//DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin);
#endif
}
// isActive returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time.
bool IODevice::isActive(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (dev)
return dev->_isActive(vpin);
else
return false;
}
void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
if (pinNumber >= 0)
pinMode(pinNumber, INPUT_PULLUP);
_gpioInterruptPin = pinNumber;
}
IONotifyStateChangeCallback *IODevice::registerInputChangeNotification(IONotifyStateChangeCallback *callback) {
IONotifyStateChangeCallback *previousHead = _notifyCallbackChain;
_notifyCallbackChain = callback;
return previousHead;
}
// Private helper function to add a device to the chain of devices.
void IODevice::addDevice(IODevice *newDevice) {
// Link new object to the start of chain. Thereby,
// a write or read will act on the first device found.
newDevice->_nextDevice = _firstDevice;
_firstDevice = newDevice;
// Initialise device
newDevice->_begin();
}
// Private helper function to locate a device by VPIN. Returns NULL if not found
IODevice *IODevice::findDevice(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev;
}
return NULL;
}
//==================================================================================================================
// Static data
//------------------------------------------------------------------------------------------------------------------
IONotifyStateChangeCallback *IODevice::_notifyCallbackChain = 0;
//==================================================================================================================
// Instance members
//------------------------------------------------------------------------------------------------------------------
// Method to check whether the id corresponds to this device
bool IODevice::owns(VPIN id) {
return (id >= _firstVpin && id < _firstVpin + _nPins);
}
// Write to devices which are after the current one in the list; this
// function allows a device to have the same input and output VPIN number, and
// a write to the VPIN from outside the device is passed to the device, but a
// call to writeDownstream will pass it to another device with the same
// VPIN number if one exists.
// void IODevice::writeDownstream(VPIN vpin, int value) {
// for (IODevice *dev = _nextDevice; dev != 0; dev = dev->_nextDevice) {
// if (dev->owns(vpin)) {
// dev->_write(vpin, value);
// return;
// }
// }
// #ifdef DIAG_IO
// //DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin);
// #endif
// }
// Read value from virtual pin.
bool IODevice::read(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_read(vpin);
}
#ifdef DIAG_IO
//DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin);
#endif
return false;
}
bool IODevice::_isDeletable() {
return false;
}
// Start of chain of devices.
IODevice *IODevice::_firstDevice = 0;
// Reference to next device to be called on _loop() method.
IODevice *IODevice::_nextLoopDevice = 0;
#else // !defined(IO_NO_HAL)
// Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more.
void IODevice::begin() { DIAG(F("NO HAL CONFIGURED!")); }
bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
(void)vpin; (void)paramCount; (void)params; // Avoid compiler warnings
if (configType == CONFIGURE_INPUT || configType == CONFIGURE_OUTPUT)
return true;
else
return false;
}
void IODevice::write(VPIN vpin, int value) {
digitalWrite(vpin, value);
pinMode(vpin, OUTPUT);
}
bool IODevice::hasCallback(VPIN vpin) {
(void)vpin; // Avoid compiler warnings
return false;
}
bool IODevice::read(VPIN vpin) {
pinMode(vpin, INPUT_PULLUP);
return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1)
}
void IODevice::loop() {}
void IODevice::DumpAll() {
DIAG(F("NO HAL CONFIGURED!"));
}
bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < 49); }
void IODevice::remove(VPIN vpin) {
(void)vpin; // Avoid compiler warnings
}
void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
(void) pinNumber; // Avoid compiler warning
}
IONotifyStateChangeCallback *IODevice::registerInputChangeNotification(IONotifyStateChangeCallback *callback) {
(void)callback; // Avoid compiler warning
return NULL;
}
#endif // IO_NO_HAL
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor
ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) {
_firstVpin = firstVpin;
_nPins = nPins;
uint8_t arrayLen = (_nPins+7)/8;
_pinPullups = (uint8_t *)calloc(2, arrayLen);
_pinModes = (&_pinPullups[0]) + arrayLen;
for (int i=0; i<arrayLen; i++) {
_pinPullups[i] = 0;
_pinModes[i] = 0;
}
}
// Device-specific pin configuration. Configure should be called infrequently so simplify
// code by using the standard pinMode function.
bool ArduinoPins::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount != 1) return false;
bool pullup = params[0];
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, pullup);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
_pinModes[index] &= ~mask; // set to input mode
if (pullup) {
_pinPullups[index] |= mask;
pinMode(pin, INPUT_PULLUP);
} else {
_pinPullups[index] &= ~mask;
pinMode(pin, INPUT);
}
return true;
}
// Device-specific write function.
void ArduinoPins::_write(VPIN vpin, int value) {
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino Write Pin:%d Val:%d"), pin, value);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
// First update the output state, then set into write mode if not already.
#if defined(USE_FAST_IO)
fastWriteDigital(pin, value);
#else
digitalWrite(pin, value);
#endif
if (!(_pinModes[index] & mask)) {
// Currently in read mode, change to write mode
_pinModes[index] |= mask;
// Since mode changes should be infrequent, use standard pinMode function
pinMode(pin, OUTPUT);
}
}
// Device-specific read function.
int ArduinoPins::_read(VPIN vpin) {
int pin = vpin;
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
if (_pinModes[index] & mask) {
// Currently in write mode, change to read mode
_pinModes[index] &= ~mask;
// Since mode changes should be infrequent, use standard pinMode function
if (_pinPullups[index] & mask)
pinMode(pin, INPUT_PULLUP);
else
pinMode(pin, INPUT);
}
#if defined(USE_FAST_IO)
int value = !fastReadDigital(pin); // Invert (5v=0, 0v=1)
#else
int value = !digitalRead(pin); // Invert (5v=0, 0v=1)
#endif
#ifdef DIAG_IO
//DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
#endif
return value;
}
void ArduinoPins::_display() {
DIAG(F("Arduino Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
#if defined(USE_FAST_IO)
void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) {
if (pin >= NUM_DIGITAL_PINS) return;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *outPortAdr = portOutputRegister(port);
noInterrupts();
if (value)
*outPortAdr |= mask;
else
*outPortAdr &= ~mask;
interrupts();
}
bool ArduinoPins::fastReadDigital(uint8_t pin) {
if (pin >= NUM_DIGITAL_PINS) return false;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *inPortAdr = portInputRegister(port);
// read input
bool result = (*inPortAdr & mask) != 0;
return result;
}
#endif

351
IODevice.h Normal file
View File

@ -0,0 +1,351 @@
/*
* © 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/>.
*/
#ifndef iodevice_h
#define iodevice_h
// Define symbol DIAG_IO to enable diagnostic output
//#define DIAG_IO Y
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
#define DIAG_LOOPTIMES
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
#define IO_NO_HAL
#endif
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
// animation has completed. This switches off the servo motor, preventing
// the continuous buzz sometimes found on servos, and reducing the
// power consumption of the servo when inactive.
// It is recommended to enable this, unless it causes you problems.
#define IO_SWITCH_OFF_SERVO
#include "DIAG.h"
#include "FSH.h"
#include "I2CManager.h"
typedef uint16_t VPIN;
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
// This should be enough for 99% of users.
#define VPIN_MAX 32767
#define VPIN_NONE 65535
typedef void IONotifyStateChangeCallback(VPIN vpin, int value);
/*
* IODevice class
*
* This class is the basis of the Hardware Abstraction Layer (HAL) for
* the DCC++EX Command Station. All device classes derive from this.
*
*/
class IODevice {
public:
// Parameter values to identify type of call to IODevice::configure.
typedef enum : uint8_t {
CONFIGURE_INPUT = 1,
CONFIGURE_SERVO = 2,
CONFIGURE_OUTPUT = 3,
} ConfigTypeEnum;
typedef enum : uint8_t {
DEVSTATE_DORMANT = 0,
DEVSTATE_PROBING = 1,
DEVSTATE_INITIALISING = 2,
DEVSTATE_NORMAL = 3,
DEVSTATE_SCANNING = 4,
DEVSTATE_FAILED = 5,
} DeviceStateEnum;
// Static functions to find the device and invoke its member functions
// begin is invoked to create any standard IODevice subclass instances
static void begin();
// configure is used invoke an IODevice instance's _configure method
static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]);
// write invokes the IODevice instance's _write method.
static void write(VPIN vpin, int value);
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
static void writeAnalogue(VPIN vpin, int value, int profile);
// isActive returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time.
static bool isActive(VPIN vpin);
// check whether the pin supports notification. If so, then regular _read calls are not required.
static bool hasCallback(VPIN vpin);
// read invokes the IODevice instance's _read method.
static bool read(VPIN vpin);
// loop invokes the IODevice instance's _loop method.
static void loop();
static void DumpAll();
// exists checks whether there is a device owning the specified vpin
static bool exists(VPIN vpin);
// remove deletes the device associated with the vpin, if it is deletable
static void remove(VPIN vpin);
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
// Without the shared interrupt, input states are scanned periodically to detect changes on
// GPIO extender pins. If a shared interrupt pin is configured, then input states are scanned
// only when the shared interrupt pin is pulled low. The external GPIO module releases the pin
// once the GPIO port concerned has been read.
void setGPIOInterruptPin(int16_t pinNumber);
// Method to add a notification. it is the caller's responsibility to save the return value
// and invoke the event handler associate with it. Example:
//
// NotifyStateChangeCallback *nextEv = registerInputChangeNotification(myProc);
//
// void processChange(VPIN pin, int value) {
// // Do something
// // Pass on to next event handler
// if (nextEv) nextEv(pin, value);
// }
//
// Note that this implementation is rudimentary and assumes a small number of callbacks (typically one). If
// more than one callback is registered, then the calls to successive callback functions are
// nested, and stack usage will be impacted. If callbacks are extensively used, it is recommended that
// a class or struct be implemented to hold the callback address, which can be chained to avoid
// nested callbacks.
static IONotifyStateChangeCallback *registerInputChangeNotification(IONotifyStateChangeCallback *callback);
protected:
// Method to perform initialisation of the device (optionally implemented within device class)
virtual void _begin() {}
// Method to configure device (optionally implemented within device class)
virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
(void)vpin; (void)configType; (void)paramCount; (void)params; // Suppress compiler warning.
return false;
};
// Method to write new state (optionally implemented within device class)
virtual void _write(VPIN vpin, int value) {
(void)vpin; (void)value;
};
// Method to write an analogue value (optionally implemented within device class)
virtual void _writeAnalogue(VPIN vpin, int value, int profile) {
(void)vpin; (void)value; (void) profile;
};
// Method called from within a filter device to trigger its output (which may
// have the same VPIN id as the input to the filter). It works through the
// later devices in the chain only.
void writeDownstream(VPIN vpin, int value);
// Function called to check whether callback notification is supported by this pin.
// Defaults to no, if not overridden by the device.
// The same value should be returned by all pins on the device, so only one need
// be checked.
virtual bool _hasCallback(VPIN vpin) {
(void) vpin;
return false;
}
// Method to read pin state (optionally implemented within device class)
virtual int _read(VPIN vpin) {
(void)vpin;
return 0;
};
// _isActive returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time. Returns false unless overridden in sub class.
virtual bool _isActive(VPIN vpin) {
(void)vpin;
return false;
}
// Method to perform updates on an ongoing basis (optionally implemented within device class)
virtual void _loop(unsigned long currentMicros) {
(void)currentMicros; // Suppress compiler warning.
};
// Method for displaying info on DIAG output (optionally implemented within device class)
virtual void _display();
// Destructor
virtual ~IODevice() {};
// isDeletable returns true if object is deletable (i.e. is not a base device driver).
virtual bool _isDeletable();
// Common object fields.
VPIN _firstVpin;
int _nPins;
// Pin number of interrupt pin for GPIO extender devices. The device will pull this
// pin low if an input changes state.
int16_t _gpioInterruptPin = -1;
// Static support function for subclass creation
static void addDevice(IODevice *newDevice);
// Notification of change
static IONotifyStateChangeCallback *_notifyCallbackChain;
DeviceStateEnum _deviceState = DEVSTATE_DORMANT;
private:
// Method to check whether the vpin corresponds to this device
bool owns(VPIN vpin);
// Method to find device handling Vpin
static IODevice *findDevice(VPIN vpin);
IODevice *_nextDevice = 0;
static IODevice *_firstDevice;
static IODevice *_nextLoopDevice;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for PCA9685 16-channel PWM module.
*/
class PCA9685 : public IODevice {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress);
enum ProfileType {
Instant = 0, // Moves immediately between positions
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!!
};
private:
// Constructor
PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress);
// Device-specific initialisation
void _begin() override;
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write functions.
void _write(VPIN vpin, int value) override;
void _writeAnalogue(VPIN vpin, int value, int profile) override;
bool _isActive(VPIN vpin) override;
void _loop(unsigned long currentMicros) override;
void updatePosition(uint8_t pin);
void writeDevice(uint8_t pin, int value);
void _display() override;
uint8_t _I2CAddress; // 0x40-0x43 possible
struct ServoData {
uint16_t activePosition : 12; // Config parameter
uint16_t inactivePosition : 12; // Config parameter
uint16_t currentPosition : 12;
uint16_t fromPosition : 12;
uint16_t toPosition : 12;
uint8_t profile; // Config parameter
uint8_t stepNumber; // Index of current step (starting from 0)
uint8_t numSteps; // Number of steps in animation, or 0 if none in progress.
int8_t state;
}; // 12 bytes per element, i.e. per pin in use
struct ServoData *_servoData [16];
static const uint16_t _defaultActivePosition = 410;
static const uint16_t _defaultInactivePosition = 205;
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
static const byte FLASH _bounceProfile[30];
const unsigned int refreshInterval = 50; // refresh every 50ms
unsigned long _lastRefreshTime; // last seen value of micros() count
// structures for setting up non-blocking writes to servo controller
I2CRB requestBlock;
uint8_t outputBuffer[5];
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for DCC accessory decoder.
*/
class DCCAccessoryDecoder: public IODevice {
public:
static void create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
private:
// Constructor
DCCAccessoryDecoder(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
// Device-specific write function.
void _write(VPIN vpin, int value) override;
void _display() override;
int _packedAddress;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for arduino input/output pins.
*/
class ArduinoPins: public IODevice {
public:
static void create(VPIN firstVpin, int nPins) {
addDevice(new ArduinoPins(firstVpin, nPins));
}
// Constructor
ArduinoPins(VPIN firstVpin, int nPins);
private:
// Device-specific pin configuration
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write function.
void _write(VPIN vpin, int value) override;
// Device-specific read function.
int _read(VPIN vpin) override;
void _display() override;
void fastWriteDigital(uint8_t pin, uint8_t value);
bool fastReadDigital(uint8_t pin);
uint8_t *_pinPullups;
uint8_t *_pinModes; // each bit is 1 for output, 0 for input
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
// #include "IO_MCP23008.h"
// #include "IO_MCP23017.h"
// #include "IO_PCF8574.h"
#endif // iodevice_h

63
IO_DCCAccessory.cpp Normal file
View File

@ -0,0 +1,63 @@
/*
* © 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/>.
*/
#include "DCC.h"
#include "IODevice.h"
#include "DIAG.h"
// Note: For DCC Accessory Decoders, a particular output can be specified by
// a linear address, or by an address/subaddress pair, where the subaddress is
// in the range 0 to 3 and specifies an output within a group of 4.
// NMRA and DCC++EX accepts addresses in the range 0-511. Linear addresses
// are not specified by the NMRA and so different manufacturers may calculate them
// in different ways. DCC+EX uses a range of 1-2044 which excludes decoder address 0.
// Therefore, I've avoided using linear addresses here because of the ambiguities
// involved. Instead I've used the term 'packedAddress'.
void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress);
}
// Constructor
DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
_firstVpin = vpin;
_nPins = nPins;
_packedAddress = (DCCAddress << 2) + DCCSubaddress;
int endAddress = _packedAddress + _nPins - 1;
DIAG(F("DCC Accessory Decoder configured Vpins:%d-%d Linear Address:%d-%d (%d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
_packedAddress, _packedAddress+_nPins-1,
DCCAddress, DCCSubaddress, endAddress >> 2, endAddress % 4);
}
// Device-specific write function.
void DCCAccessoryDecoder::_write(VPIN id, int state) {
int packedAddress = _packedAddress + id - _firstVpin;
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
#endif
DCC::setAccessory(packedAddress >> 2, packedAddress % 4, state);
}
void DCCAccessoryDecoder::_display() {
int endAddress = _packedAddress + _nPins - 1;
DIAG(F("DCC Accessory Vpins:%d-%d Linear Address:%d-%d (%d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
_packedAddress, _packedAddress+_nPins-1,
_packedAddress >> 2, _packedAddress % 4, endAddress >> 2, endAddress % 4);
}

121
IO_ExampleSerial.cpp Normal file
View File

@ -0,0 +1,121 @@
/*
* © 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/>.
*/
#include <Arduino.h>
#include "IO_ExampleSerial.h"
#include "FSH.h"
// Constructor
IO_ExampleSerial::IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
_firstVpin = firstVpin;
_nPins = nPins;
// Save reference to serial port driver
_serial = serial;
_serial->begin(baud);
DIAG(F("ExampleSerial configured Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1);
}
// Static create method for one module.
void IO_ExampleSerial::create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
IO_ExampleSerial *dev = new IO_ExampleSerial(firstVpin, nPins, serial, baud);
addDevice(dev);
}
// Device-specific initialisation
void IO_ExampleSerial::_begin() {
// Send a few # characters to the output
for (uint8_t i=0; i<3; i++)
_serial->write('#');
}
// Device-specific write function. Write a string in the form "#Wm,n#"
// where m is the vpin number, and n is the value.
void IO_ExampleSerial::_write(VPIN vpin, int value) {
int pin = vpin -_firstVpin;
#ifdef DIAG_IO
DIAG(F("IO_ExampleSerial::_write Pin:%d Value:%d"), (int)vpin, value);
#endif
// Send a command string over the serial line
_serial->print('#');
_serial->print('W');
_serial->print(pin);
_serial->print(',');
_serial->print(value);
_serial->println('#');
DIAG(F("ExampleSerial Sent command, p1=%d, p2=%d"), vpin, value);
}
// Device-specific read function.
int IO_ExampleSerial::_read(VPIN vpin) {
// Return a value for the specified vpin. For illustration, return
// a value indicating whether the pin number is odd.
int result = (vpin & 1);
return result;
}
// Loop function to do background scanning of the input port. State
// machine parses the incoming command as it is received. Command
// is in the form "#Nm,n#" where m is the index and n is the value.
void IO_ExampleSerial::_loop(unsigned long currentMicros) {
(void)currentMicros; // Suppress compiler warnings
if (_serial->available()) {
// Input data available to read. Read a character.
char c = _serial->read();
switch (inputState) {
case 0: // Waiting for start of command
if (c == '#') // Start of command received.
inputState = 1;
break;
case 1: // Expecting command character
if (c == 'N') { // 'Notify' character received
inputState = 2;
inputValue = inputIndex = 0;
} else
inputState = 0; // Unexpected char, reset
break;
case 2: // reading first parameter (index)
if (isdigit(c))
inputIndex = inputIndex * 10 + (c-'0');
else if (c==',')
inputState = 3;
else
inputState = 0; // Unexpected char, reset
break;
case 3: // reading reading second parameter (value)
if (isdigit(c))
inputValue = inputValue * 10 - (c-'0');
else if (c=='#') { // End of command
// Complete command received, do something with it.
DIAG(F("ExampleSerial Received command, p1=%d, p2=%d"), inputIndex, inputValue);
inputState = 0; // Done, start again.
} else
inputState = 0; // Unexpected char, reset
break;
}
}
}
void IO_ExampleSerial::_display() {
DIAG(F("IO_ExampleSerial VPins:%d-%d"), (int)_firstVpin,
(int)_firstVpin+_nPins-1);
}

43
IO_ExampleSerial.h Normal file
View File

@ -0,0 +1,43 @@
/*
* © 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/>.
*/
#ifndef IO_EXAMPLESERIAL_H
#define IO_EXAMPLESERIAL_H
#include "IODevice.h"
class IO_ExampleSerial : public IODevice {
public:
IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
void _begin() override;
void _loop(unsigned long currentMicros) override;
void _write(VPIN vpin, int value) override;
int _read(VPIN vpin) override;
void _display() override;
private:
HardwareSerial *_serial;
uint8_t inputState = 0;
int inputIndex = 0;
int inputValue = 0;
};
#endif // IO_EXAMPLESERIAL_H

221
IO_GPIOBase.h Normal file
View File

@ -0,0 +1,221 @@
/*
* © 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/>.
*/
#ifndef IO_GPIOBASE_H
#define IO_GPIOBASE_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// GPIOBase is defined as a class template. This allows it to be instantiated by
// subclasses with different types, according to the number of pins on the GPIO module.
// For example, GPIOBase<uint8_t> for 8 pins, GPIOBase<uint16_t> for 16 pins etc.
// A module with up to 64 pins can be handled in this way (uint64_t).
template <class T>
class GPIOBase : public IODevice {
protected:
// Constructor
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin);
// Device-specific initialisation
void _begin() override;
// Device-specific pin configuration function.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Pin write function.
void _write(VPIN vpin, int value) override;
// Pin read function.
int _read(VPIN vpin) override;
void _display() override;
void _loop(unsigned long currentMicros) override;
// Data fields
uint8_t _I2CAddress;
// Allocate enough space for all input pins
T _portInputState;
T _portOutputState;
T _portMode;
T _portPullup;
// Interval between refreshes of each input port
static const int _portTickTime = 4000;
unsigned long _lastLoopEntry = 0;
// Virtual functions for interfacing with I2C GPIO Device
virtual void _writeGpioPort() = 0;
virtual void _readGpioPort(bool immediate=true) = 0;
virtual void _writePullups() {};
virtual void _writePortModes() {};
virtual void _setupDevice() {};
virtual void _processCompletion(uint8_t status) {
(void)status; // Suppress compiler warning
};
I2CRB requestBlock;
FSH *_deviceName;
};
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
// file as the class declaration (above). Otherwise it won't compile!
// Constructor
template <class T>
GPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) {
_deviceName = deviceName;
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = I2CAddress;
_gpioInterruptPin = interruptPin;
_notifyCallbackChain = 0;
// Add device to list of devices.
addDevice(this);
// Configure pin used for GPIO extender notification of change (if allocated)
if (_gpioInterruptPin >= 0)
pinMode(_gpioInterruptPin, INPUT_PULLUP);
I2CManager.begin();
I2CManager.setClock(400000);
if (I2CManager.exists(I2CAddress)) {
_display();
_portMode = 0; // default to input mode
_portPullup = -1; // default to pullup enabled
_portInputState = 0;
}
_deviceState = DEVSTATE_NORMAL;
_lastLoopEntry = micros();
}
template <class T>
void GPIOBase<T>::_begin() {}
// Configuration parameters for inputs:
// params[0]: enable pullup
// params[1]: invert input (optional)
template <class T>
bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount == 0 || paramCount > 1) return false;
bool pullup = params[0];
int pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Config Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, pullup);
#endif
uint16_t mask = 1 << pin;
if (pullup)
_portPullup |= mask;
else
_portPullup &= ~mask;
// Call subclass's virtual function to write to device
_writePullups();
// Re-read port following change
_readGpioPort();
return true;
}
// Periodically read the input port
template <class T>
void GPIOBase<T>::_loop(unsigned long currentMicros) {
#ifdef DIAG_IO
T lastPortStates = _portInputState;
#endif
if (_deviceState == DEVSTATE_SCANNING && !requestBlock.isBusy()) {
uint8_t status = requestBlock.status;
if (status == I2C_STATUS_OK) {
_deviceState = DEVSTATE_NORMAL;
} else {
_deviceState = DEVSTATE_FAILED;
DIAG(F("%S I2C:x%x Error:%d"), _deviceName, _I2CAddress, status);
}
_processCompletion(status);
}
// Check if interrupt configured. If so, and pin is not pulled down, finish.
if (_gpioInterruptPin >= 0) {
if (digitalRead(_gpioInterruptPin)) return;
} else
// No interrupt pin. Check if tick has elapsed. If not, finish.
if (currentMicros - _lastLoopEntry < _portTickTime) return;
// TODO: Could suppress reads if there are no pins configured as inputs!
// Read input
_lastLoopEntry = currentMicros;
if (_deviceState == DEVSTATE_NORMAL) {
_readGpioPort(false); // Initiate non-blocking read
_deviceState= DEVSTATE_SCANNING;
}
#ifdef DIAG_IO
T differences = lastPortStates ^ _portInputState;
if (differences)
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
#endif
}
template <class T>
void GPIOBase<T>::_display() {
DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d"), _deviceName, _I2CAddress,
_firstVpin, _firstVpin+_nPins-1);
}
template <class T>
void GPIOBase<T>::_write(VPIN vpin, int value) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value);
#endif
// Set port mode output
if (!(_portMode & mask)) {
_portMode |= mask;
_writePortModes();
}
// Update port output state
if (value)
_portOutputState |= mask;
else
_portOutputState &= ~mask;
// Call subclass's virtual function to write to device.
return _writeGpioPort();
}
template <class T>
int GPIOBase<T>::_read(VPIN vpin) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
// Set port mode to input
if (_portMode & mask) {
_portMode &= ~mask;
_writePortModes();
// Port won't have been read yet, so read it now.
_readGpioPort();
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
#endif
}
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)
}
#endif

96
IO_MCP23008.h Normal file
View File

@ -0,0 +1,96 @@
/*
* © 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/>.
*/
#ifndef IO_MCP23008_H
#define IO_MCP23008_H
#include "IO_GPIOBase.h"
class MCP23008 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
new MCP23008(firstVpin, nPins, I2CAddress, interruptPin);
}
private:
// Constructor
MCP23008(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("MCP23008"), firstVpin, min(nPins, 8), I2CAddress, interruptPin) {
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIO;
}
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState);
}
void _writePullups() override {
I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup);
}
void _writePortModes() override {
// Each bit is 1 for an input, 0 for an output, i.e. inverted.
I2CManager.write(_I2CAddress, 2, REG_IODIR, ~_portMode);
// Enable interrupt-on-change for pins that are inputs (_portMode=0)
I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPINTEN, ~_portMode);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer;
I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);
_portInputState = buffer;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0];
else
_portInputState = 0xff;
}
void _setupDevice() override {
// IOCON is set ODR=1 (open drain shared interrupt pin), INTPOL=0 (active-Low)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x04);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[1];
uint8_t outputBuffer[1];
enum {
// Register definitions for MCP23008
REG_IODIR=0x00,
REG_GPINTEN=0x02,
REG_INTCON=0x04,
REG_IOCON=0x05,
REG_GPPU=0x06,
REG_GPIO=0x09,
};
};
#endif

108
IO_MCP23017.h Normal file
View File

@ -0,0 +1,108 @@
/*
* © 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/>.
*/
#ifndef io_mcp23017_h
#define io_mcp23017_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for MCP23017 16-bit I/O expander.
*/
class MCP23017 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
new MCP23017(vpin, min(nPins,16), I2CAddress, interruptPin);
}
private:
// Constructor
MCP23017(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIOA;
_setupDevice();
}
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);
}
void _writePullups() override {
I2CManager.write(_I2CAddress, 3, REG_GPPUA, _portPullup, _portPullup>>8);
}
void _writePortModes() override {
// Write 1 to IODIR for pins that are inputs, 0 for outputs (i.e. _portMode inverted)
I2CManager.write(_I2CAddress, 3, REG_IODIRA, ~_portMode, (~_portMode)>>8);
// Enable interrupt for those pins which are inputs (_portMode=0)
I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);
I2CManager.write(_I2CAddress, 3, REG_GPINTENA, ~_portMode, (~_portMode)>>8);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = ((uint16_t)inputBuffer[1]<<8) | inputBuffer[0];
else
_portInputState = 0xffff;
}
void _setupDevice() override {
// IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[2];
uint8_t outputBuffer[1];
enum {
REG_IODIRA = 0x00,
REG_IODIRB = 0x01,
REG_GPINTENA = 0x04,
REG_GPINTENB = 0x05,
REG_INTCONA = 0x08,
REG_INTCONB = 0x09,
REG_IOCON = 0x0A,
REG_GPPUA = 0x0C,
REG_GPPUB = 0x0D,
REG_GPIOA = 0x12,
REG_GPIOB = 0x13,
};
};
#endif

258
IO_PCA9685.cpp Normal file
View File

@ -0,0 +1,258 @@
/*
* © 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/>.
*/
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// REGISTER ADDRESSES
static const byte PCA9685_MODE1=0x00; // Mode Register
static const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
static const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
static const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
static const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
static const byte MODE1_RESTART=0x80; /**< Restart enabled */
static const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
static const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
static const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
// Predeclare helper function
static void writeRegister(byte address, byte reg, byte value);
// Create device driver instance.
void PCA9685::create(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
new PCA9685(firstVpin, nPins, I2CAddress);
}
// Configure a port on the PCA9685.
bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_SERVO) return false;
if (paramCount != 4) return false;
#ifdef DIAG_IO
DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d state:%d"),
vpin, params[0], params[1], params[2], params[3]);
#endif
int8_t pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (!s) {
_servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));
s = _servoData[pin];
if (!s) return false; // Check for failed memory allocation
}
s->activePosition = params[0];
s->currentPosition = s->inactivePosition = params[1];
s->profile = params[2];
// Position servo to initial state
s->state = -1; // Set unknown state, to force reposition
_write(vpin, params[3]);
return true;
}
// Constructor
PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
_firstVpin = firstVpin;
_nPins = min(nPins, 16);
_I2CAddress = I2CAddress;
for (int i=0; i<_nPins; i++)
_servoData[i] = NULL;
addDevice(this);
// Initialise structure used for setting pulse rate
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
I2CManager.begin();
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
// In reality, other devices including the Arduino will limit
// the clock speed to a lower rate.
// Initialise I/O module here.
if (I2CManager.exists(_I2CAddress)) {
DIAG(F("PCA9685 I2C:%x configured Vpins:%d-%d"), _I2CAddress, _firstVpin, _firstVpin+_nPins-1);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(_I2CAddress, PCA9685_PRESCALE, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period.
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
// In theory, we should wait 500us before sending any other commands to each device, to allow
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
// plenty of other stuff to do before we will send a command.
}
}
// Device-specific initialisation
void PCA9685::_begin() {
}
// Device-specific write function, invoked from IODevice::write().
void PCA9685::_write(VPIN vpin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value);
#endif
int pin = vpin - _firstVpin;
if (value) value = 1;
struct ServoData *s = _servoData[pin];
if (!s) {
// Pin not configured, just write default positions to servo controller
if (value)
writeDevice(pin, _defaultActivePosition);
else
writeDevice(pin, _defaultInactivePosition);
} else {
// Use configured parameters for advanced transitions
uint8_t profile = s->profile;
// If current position not known, go straight to selected position.
if (s->state == -1) profile = Instant;
// Animated profile. Initiate the appropriate action.
s->numSteps = profile==Fast ? 10 :
profile==Medium ? 20 :
profile==Slow ? 40 :
profile==Bounce ? sizeof(_bounceProfile) :
1;
s->state = value;
s->stepNumber = 0;
// Update new from/to positions to initiate or change animation.
s->fromPosition = s->currentPosition;
s->toPosition = s->state ? s->activePosition : s->inactivePosition;
}
}
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) {
#ifdef DIAG_IO
DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d"), vpin, value, profile);
#endif
int pin = vpin - _firstVpin;
if (value > 4095) value = 4095;
else if (value < 0) value = 0;
struct ServoData *s = _servoData[pin];
if (!s) {
// Servo pin not configured, so configure now.
s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);
s->activePosition = _defaultActivePosition;
s->inactivePosition = _defaultInactivePosition;
s->currentPosition = value; // Don't know where we're moving from.
}
s->profile = profile;
// Animated profile. Initiate the appropriate action.
s->numSteps = profile==Fast ? 10 :
profile==Medium ? 20 :
profile==Slow ? 40 :
profile==Bounce ? sizeof(_bounceProfile) :
1;
s->stepNumber = 0;
s->toPosition = min(value, 4095);
s->fromPosition = s->currentPosition;
}
// _isActive returns true if the device is currently in executing an animation,
// changing the output over a period of time.
bool PCA9685::_isActive(VPIN vpin) {
int pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (!s)
return false; // No structure means no animation!
else
return (s->numSteps != 0);
}
void PCA9685::_loop(unsigned long currentMicros) {
if (currentMicros - _lastRefreshTime >= refreshInterval * 1000) {
for (int pin=0; pin<_nPins; pin++) {
updatePosition(pin);
}
_lastRefreshTime = currentMicros;
}
}
// Private function to reposition servo
// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.
void PCA9685::updatePosition(uint8_t pin) {
struct ServoData *s = _servoData[pin];
if (!s) return;
if (s->numSteps == 0) return; // No animation in progress
if (s->stepNumber < s->numSteps) {
// Animation in progress, reposition servo
s->stepNumber++;
if (s->profile == Bounce) {
// Retrieve step positions from array in flash
byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);
s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);
} else {
// All other profiles - calculate step by linear interpolation between from and to positions.
s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);
}
// Send servo command
writeDevice(pin, s->currentPosition);
} else if (s->stepNumber < s->numSteps + _catchupSteps) {
// We've finished animation, wait a little to allow servo to catch up
s->stepNumber++;
} else if (s->stepNumber == s->numSteps + _catchupSteps
&& s->currentPosition != 4095 && s->currentPosition != 0) {
#ifdef IO_SWITCH_OFF_SERVO
// Wait has finished, so switch off PWM to prevent annoying servo buzz
writeDevice(pin, 0);
#endif
s->numSteps = 0; // Done now.
}
}
// writeDevice takes a pin in range 0 to _nPins-1 within the device, and a value
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
void PCA9685::writeDevice(uint8_t pin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value);
#endif
// Wait for previous request to complete
requestBlock.wait();
// Set up new request.
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
outputBuffer[1] = 0;
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
outputBuffer[3] = value & 0xff;
outputBuffer[4] = value >> 8;
I2CManager.queueRequest(&requestBlock);
}
// Display details of this device.
void PCA9685::_display() {
DIAG(F("PCA9685 I2C:x%x Vpins:%d-%d"), _I2CAddress, (int)_firstVpin,
(int)_firstVpin+_nPins-1);
}
// Internal helper function for this device
static void writeRegister(byte address, byte reg, byte value) {
I2CManager.write(address, 2, reg, value);
}
// Profile for a bouncing signal or turnout
// The profile below is in the range 0-100% and should be combined with the desired limits
// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here,
// i.e. the bounce is the same on the down action as on the up action. First entry isn't used.
const byte FLASH PCA9685::_bounceProfile[30] =
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};

81
IO_PCF8574.h Normal file
View File

@ -0,0 +1,81 @@
/*
* © 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/>.
*/
#ifndef IO_PCF8574_H
#define IO_PCF8574_H
#include "IO_GPIOBase.h"
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
new PCF8574(firstVpin, nPins, I2CAddress, interruptPin);
}
private:
PCF8574(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, min(nPins, 8), I2CAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
}
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
// Therefore, writing '1' in _writePortModes is enough to set the module to input mode
// and enable pull-up.
void _writePullups() override { }
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
void _writePortModes() override {
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
}
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
// When not in immediate mode, it initiates a request using the request block and returns.
// When the request completes, _processCompletion finishes the operation.
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[1];
I2CManager.read(_I2CAddress, buffer, 1);
_portInputState = ((uint16_t)buffer) & 0xff;
} else {
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = ((uint16_t)inputBuffer[0]) & 0xff;
else
_portInputState = 0xff;
}
void _setupDevice() override { }
uint8_t inputBuffer[1];
};
#endif

View File

@ -74,6 +74,10 @@ void LCDDisplay::loop() {
LCDDisplay *LCDDisplay::loop2(bool force) {
if (!lcdDisplay) return NULL;
// If output device is busy, don't do anything on this loop
// This avoids blocking while waiting for the device to complete.
if (isBusy()) return NULL;
unsigned long currentMillis = millis();
if (!force) {

View File

@ -39,17 +39,18 @@
class LCDDisplay : public DisplayInterface {
public:
LCDDisplay() {};
static const int MAX_LCD_ROWS = 8;
static const int MAX_LCD_COLS = MAX_MSG_SIZE;
static const long LCD_SCROLL_TIME = 3000; // 3 seconds
// Internally handled functions
static void loop();
LCDDisplay* loop2(bool force);
void setRow(byte line);
void clear();
LCDDisplay* loop2(bool force) override;
void setRow(byte line) override;
void clear() override;
size_t write(uint8_t b);
size_t write(uint8_t b) override;
protected:
uint8_t lcdRows;
@ -63,6 +64,7 @@ protected:
virtual void clearNative() = 0;
virtual void setRowNative(byte line) = 0;
virtual size_t writeNative(uint8_t b) = 0;
virtual bool isBusy() = 0;
unsigned long lastScrollTime = 0;
int8_t hotRow = 0;

View File

@ -1,33 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C LCDDriver(LCD_DRIVER); // set the LCD address, cols, rows
// DEVICE SPECIFIC LCDDisplay Implementation for LCD_DRIVER
LCDDisplay::LCDDisplay() {
lcdDisplay=this;
LCDDriver.init();
LCDDriver.backlight();
interfake(LCD_DRIVER);
clear();
}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; lcdRows=p3; }
void LCDDisplay::clearNative() {LCDDriver.clear();}
void LCDDisplay::setRowNative(byte row) { LCDDriver.setCursor(0, row); }
void LCDDisplay::writeNative(char b){ LCDDriver.write(b); }
void LCDDisplay::displayNative() { LCDDriver.display(); }

View File

@ -1,27 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
// dummy LCD shim to keep linker happy
LCDDisplay::LCDDisplay() {}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; (void)p3;}
void LCDDisplay::setRowNative(byte row) { (void)row;}
void LCDDisplay::clearNative() {}
void LCDDisplay::writeNative(char b){ (void)b;} //
void LCDDisplay::displayNative(){}

View File

@ -1,73 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
// OLED Implementation of LCDDisplay class
// Note: this file is optionally included by LCD_Implementation.h
// It is NOT a .cpp file to prevent it being compiled and demanding libraries
// even when not needed.
#include "I2CManager.h"
#include "SSD1306Ascii.h"
SSD1306AsciiWire LCDDriver;
// DEVICE SPECIFIC LCDDisplay Implementation for OLED
LCDDisplay::LCDDisplay() {
// Scan for device on 0x3c and 0x3d.
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
// Device found
DIAG(F("OLED display found at 0x%x"), address);
interfake(OLED_DRIVER, 0);
const DevType *devType;
if (lcdCols == 132)
devType = &SH1106_128x64; // Actually 132x64 but treated as 128x64
else if (lcdCols == 128 && lcdRows == 4)
devType = &Adafruit128x32;
else
devType = &Adafruit128x64;
LCDDriver.begin(devType, address);
lcdDisplay = this;
LCDDriver.setFont(System5x7); // Normal 1:1 pixel scale, 8 bits high
clear();
return;
}
}
DIAG(F("OLED display not found"));
}
void LCDDisplay::interfake(int p1, int p2, int p3) {
lcdCols = p1;
lcdRows = p2 / 8;
(void)p3;
}
void LCDDisplay::clearNative() { LCDDriver.clear(); }
void LCDDisplay::setRowNative(byte row) {
// Positions text write to start of row 1..n
int y = row;
LCDDriver.setCursor(0, y);
}
void LCDDisplay::writeNative(char b) { LCDDriver.write(b); }
void LCDDisplay::displayNative() {}

View File

@ -49,17 +49,16 @@ void LCN::loop() {
else if (ch == 't' || ch == 'T') { // Turnout opcodes
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Turnout * tt = Turnout::get(id);
if (!tt) Turnout::create(id, LCN_TURNOUT_ADDRESS, 0);
if (ch == 't') tt->data.tStatus |= STATUS_ACTIVE;
else tt->data.tStatus &= ~STATUS_ACTIVE;
if (!tt) tt=Turnout::createLCN(id);
tt->setActive(ch=='t');
Turnout::turnoutlistHash++; // signals ED update of turnout data
id = 0;
}
else if (ch == 'S' || ch == 's') {
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Sensor * ss = Sensor::get(id);
if (!ss) ss = Sensor::create(id, 255,0); // impossible pin
ss->active = ch == 'S';
if (!ss) ss = Sensor::create(id, VPIN_NONE, 0); // impossible pin
ss->setState(ch == 'S');
id = 0;
}
else id = 0; // ignore any other garbage from LCN

View File

@ -119,7 +119,7 @@ void LiquidCrystal_I2C::clearNative() {
void LiquidCrystal_I2C::setRowNative(byte row) {
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row > lcdRows) {
if (row >= lcdRows) {
row = lcdRows - 1; // we count rows starting w/0
}
command(LCD_SETDDRAMADDR | (row_offsets[row]));
@ -196,7 +196,7 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
outputBuffer[len++] = highnib;
outputBuffer[len++] = lownib|En;
outputBuffer[len++] = lownib;
I2CManager.write(_Addr, outputBuffer, len);
I2CManager.write(_Addr, outputBuffer, len, &requestBlock);
}
// write 4 data bits to the HD44780 LCD controller.
@ -205,15 +205,19 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) {
// Enable must be set/reset for at least 450ns. This is well within the
// I2C clock cycle time of 2.5us at 400kHz. Data is clocked in to the
// HD44780 on the trailing edge of the Enable pin.
// Wait for previous request to complete before writing to outputbuffer.
requestBlock.wait();
uint8_t len = 0;
outputBuffer[len++] = _data|En;
outputBuffer[len++] = _data;
I2CManager.write(_Addr, outputBuffer, len);
I2CManager.write(_Addr, outputBuffer, len, &requestBlock);
}
// write a byte to the PCF8574 I2C interface. We don't need to set
// the enable pin for this.
void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
// Wait for previous request to complete before writing to outputbuffer.
requestBlock.wait();
outputBuffer[0] = value | _backlightval;
I2CManager.write(_Addr, outputBuffer, 1);
I2CManager.write(_Addr, outputBuffer, 1, &requestBlock);
}

View File

@ -66,9 +66,9 @@ class LiquidCrystal_I2C : public LCDDisplay {
public:
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
void begin();
void clearNative();
void setRowNative(byte line);
size_t writeNative(uint8_t c);
void clearNative() override;
void setRowNative(byte line) override;
size_t writeNative(uint8_t c) override;
void display();
void noBacklight();
@ -88,7 +88,9 @@ private:
uint8_t _displaymode;
uint8_t _backlightval;
I2CRB requestBlock;
uint8_t outputBuffer[4];
bool isBusy() { return requestBlock.isBusy(); }
};
#endif

View File

@ -84,28 +84,43 @@ the state of any outputs being monitored or controlled by a separate interface o
#include "Outputs.h"
#include "EEStore.h"
#include "StringFormatter.h"
#include "IODevice.h"
///////////////////////////////////////////////////////////////////////////////
// Static function to print all output states to stream in the form "<Y id state>"
// print all output states to stream
void Output::printAll(Print *stream){
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.oStatus);
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.active);
} // Output::printAll
void Output::activate(int s){
data.oStatus=(s>0); // if s>0, set status to active, else inactive
digitalWrite(data.pin,data.oStatus ^ bitRead(data.iFlag,0)); // set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
if(num>0)
EEPROM.put(num,data.oStatus);
///////////////////////////////////////////////////////////////////////////////
// Object method to activate / deactivate the Output state.
void Output::activate(uint16_t s){
s = (s>0); // Make 0 or 1
data.active = s; // if s>0, set status to active, else inactive
// set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
IODevice::write(data.pin, s ^ data.invert);
// Update EEPROM if output has been stored.
if(EEStore::eeStore->data.nOutputs > 0 && num > 0)
EEPROM.put(num, data.flags);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to locate Output object specified by ID 'n'.
// Return NULL if not found.
Output* Output::get(uint16_t n){
Output *tt;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to delete Output object specified by ID 'n'.
// Return false if not found.
bool Output::remove(uint16_t n){
Output *tt,*pp=NULL;
@ -125,52 +140,24 @@ bool Output::remove(uint16_t n){
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load configuration and state of all Outputs from EEPROM
void Output::load(){
struct BrokenOutputData bdata;
struct OutputData data;
Output *tt;
bool isBroken=1;
// This is a scary kluge. As we have two formats in EEPROM due to an
// earlier bug, we don't know which we encounter now. So we guess
// that if in all entries this byte has value of 7 or lower this is
// an iFlag and thus the broken format. Otherwise it would be a pin
// id. If someone uses only pins 0 to 7 of their arduino, they
// loose. This is (if you look at an arduino) however unlikely.
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer()+ i*sizeof(struct BrokenOutputData),bdata);
if (bdata.iFlag > 7) { // it's a pin and not an iFlag!
isBroken=0;
break;
}
}
if ( isBroken ) {
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer(),bdata);
tt=create(bdata.id,bdata.pin,bdata.iFlag);
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):bdata.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
tt->num=EEStore::pointer();
EEStore::advance(sizeof(struct BrokenOutputData));
}
} else {
struct OutputData data;
EEPROM.get(EEStore::pointer(),data);
// Create new object, set current state to default or to saved state from eeprom.
tt=create(data.id, data.pin, data.flags, data.setDefault ? data.defaultValue : data.active);
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer(),data);
tt=create(data.id,data.pin,data.iFlag);
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):data.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
tt->num=EEStore::pointer();
EEStore::advance(sizeof(struct OutputData));
}
if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
EEStore::advance(sizeof(tt->data));
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to store configuration and state of all Outputs to EEPROM
void Output::store(){
Output *tt;
@ -179,19 +166,23 @@ void Output::store(){
EEStore::eeStore->data.nOutputs=0;
while(tt!=NULL){
tt->num=EEStore::pointer();
EEPROM.put(EEStore::pointer(),tt->data);
tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
EEStore::advance(sizeof(tt->data));
tt=tt->nextOutput;
EEStore::eeStore->data.nOutputs++;
}
}
///////////////////////////////////////////////////////////////////////////////
Output *Output::create(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t v){
///////////////////////////////////////////////////////////////////////////////
// Static function to create an Output object
Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){
Output *tt;
if (pin > VPIN_MAX) return NULL;
if(firstOutput==NULL){
firstOutput=(Output *)calloc(1,sizeof(Output));
tt=firstOutput;
@ -204,20 +195,21 @@ Output *Output::create(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t v){
}
if(tt==NULL) return tt;
tt->num = 0; // make sure new object doesn't get written to EEPROM until store() command
tt->data.id=id;
tt->data.pin=pin;
tt->data.iFlag=iFlag;
tt->data.oStatus=0;
tt->data.flags=iFlag;
if(v==1){
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):0; // sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
// sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
if (tt->data.setDefault)
tt->data.active = tt->data.defaultValue;
else
tt->data.active = 0;
}
IODevice::write(tt->data.pin, tt->data.active ^ tt->data.invert);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -20,37 +20,43 @@
#define Outputs_h
#include <Arduino.h>
#include "IODevice.h"
struct OutputData {
uint8_t oStatus;
union {
uint8_t oStatus; // (Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state, Bit 7=active)
struct {
unsigned int flags : 7; // Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state
unsigned int : 1;
};
struct {
unsigned int invert : 1;
unsigned int setDefault : 1;
unsigned int defaultValue : 1;
unsigned int: 4;
unsigned int active : 1;
};
};
uint16_t id;
uint8_t pin;
uint8_t iFlag;
VPIN pin;
};
struct BrokenOutputData {
uint8_t oStatus;
uint8_t id;
uint8_t pin;
uint8_t iFlag;
};
class Output{
public:
void activate(int s);
void activate(uint16_t s);
bool isActive();
static Output* get(uint16_t);
static bool remove(uint16_t);
static void load();
static void store();
static Output *create(uint16_t, uint8_t, uint8_t, uint8_t=0);
static Output *create(uint16_t, VPIN, int, int=0);
static Output *firstOutput;
struct OutputData data;
Output *nextOutput;
static void printAll(Print *);
private:
int num; // EEPROM pointer (Chris has no idea what this is all about!)
uint16_t num; // EEPROM address of oStatus in OutputData struct, or zero if not stored.
}; // Output

View File

@ -1,109 +0,0 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
/*!
* @file PWMServoDriver.cpp
*
* @mainpage Adafruit 16-channel PWM & Servo driver, based on Adafruit_PWMServoDriver
*
* @section intro_sec Introduction
*
* This is a library for the 16-channel PWM & Servo driver.
*
* Designed specifically to work with the Adafruit PWM & Servo driver.
* This class contains a very small subset of the Adafruit version which
* is relevant to driving simple servos at 50Hz through a number of chained
* servo driver boards (ie servos 0-15 on board 0x40, 16-31 on board 0x41 etc.)
*
* @section author Author
* Chris Harlow (TPL)
*
*/
#include <Arduino.h>
#include "PWMServoDriver.h"
#include "DIAG.h"
#include "I2CManager.h"
// REGISTER ADDRESSES
const byte PCA9685_MODE1=0x00; // Mode Register
const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
const byte MODE1_RESTART=0x80; /**< Restart enabled */
const byte PCA9685_I2C_ADDRESS=0x40; /** First PCA9685 I2C Slave Address */
const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
/*!
* @brief Sets the PWM frequency for a chip to 50Hz for servos
*/
byte PWMServoDriver::setupFlags=0; // boards that have been initialised
byte PWMServoDriver::failFlags=0; // boards that have faild initialisation
bool PWMServoDriver::setup(int board) {
if (board>3 || (failFlags & (1<<board))) return false;
if (setupFlags & (1<<board)) return true;
I2CManager.begin();
I2CManager.setClock(MAX_I2C_SPEED);
uint8_t i2caddr=PCA9685_I2C_ADDRESS + board;
// Test if device is available
byte error = I2CManager.checkAddress(i2caddr);
if (error) {
DIAG(F("I2C Servo device 0x%x Not Found %d"),i2caddr, error);
failFlags|=1<<board;
return false;
}
//DIAG(F("PWMServoDriver::setup %x prescale=%d"),i2caddr,PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(i2caddr,PCA9685_PRESCALE, PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1,MODE1_AI);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
setupFlags|=1<<board;
return true;
}
/*!
* @brief Sets the PWM output to a servo
*/
void PWMServoDriver::setServo(byte servoNum, uint16_t value) {
int board=servoNum/16;
int pin=servoNum%16;
if (setup(board)) {
DIAG(F("SetServo %d %d"),servoNum,value);
uint8_t buffer[] = {(uint8_t)(PCA9685_FIRST_SERVO + 4 * pin), // 4 registers per pin
0, 0, (uint8_t)(value & 0xff), (uint8_t)(value >> 8)};
if (value == 4095) buffer[2] = 0x10; // Full on
byte error=I2CManager.write(PCA9685_I2C_ADDRESS + board, buffer, sizeof(buffer));
if (error!=0) DIAG(F("SetServo error %d"),error);
}
}
void PWMServoDriver::writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d) {
I2CManager.write(i2caddr, 2, hardwareRegister, d);
}

View File

@ -1,39 +0,0 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
/*!
* @file PWMServoDriver.h
*
* Used to set servo positions on an I2C bus with 1 or more PCA96685 boards.
*/
#ifndef PWMServoDriver_H
#define PWMServoDriver_H
class PWMServoDriver {
public:
static void setServo(byte servoNum, uint16_t pos);
private:
static byte setupFlags;
static byte failFlags;
static bool setup(int board);
static void writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d);
};
#endif

23
RMFT.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef RMFT_H
#define RMFT_H
#if defined(RMFT_ACTIVE)
#include "RMFT2.h"
class RMFT {
public:
static void inline begin() {RMFT2::begin();}
static void inline loop() {RMFT2::loop();}
};
#include "RMFTMacros.h"
#else
// Dummy RMFT
class RMFT {
public:
static void inline begin() {}
static void inline loop() {}
};
#endif
#endif

646
RMFT2.cpp Normal file
View File

@ -0,0 +1,646 @@
/*
* © 2020,2021 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#include <Arduino.h>
#include "RMFT2.h"
#include "DCC.h"
#include "DIAG.h"
#include "WiThrottle.h"
#include "DCCEXParser.h"
#include "Turnouts.h"
// Command parsing keywords
const int16_t HASH_KEYWORD_EXRAIL=15435;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_START=23232;
const int16_t HASH_KEYWORD_RESERVE=11392;
const int16_t HASH_KEYWORD_FREE=-23052;
const int16_t HASH_KEYWORD_LATCH=1618;
const int16_t HASH_KEYWORD_UNLATCH=1353;
const int16_t HASH_KEYWORD_PAUSE=-4142;
const int16_t HASH_KEYWORD_RESUME=27609;
// One instance of RMFT clas is used for each "thread" in the automation.
// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.
// The thrrads exist in a ring, each time through loop() the next thread in the ring is serviced.
// Statics
int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck
bool RMFT2::diag=false; // <D EXRAIL ON>
RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring.
RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
// when pausingTask is set, that is the ONLY task that gets any service,
// and all others will have their locos stopped, then resumed after the pausing task resumes.
byte RMFT2::flags[MAX_FLAGS];
#define GET_OPCODE GETFLASH(RMFT2::RouteCode+progCounter)
#define GET_OPERAND(n) GETFLASHW(RMFT2::RouteCode+progCounter+1+(n*3))
#define SKIPOP progCounter+=3
/* static */ void RMFT2::begin() {
DCCEXParser::setRMFTFilter(RMFT2::ComandFilter);
for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;
int progCounter;
// first pass startup, define any turnouts or servos and count size.
for (progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) break;
if (opcode==OPCODE_SIGNAL) {
VPIN red=GET_OPERAND(0);
VPIN amber=GET_OPERAND(1);
VPIN green=GET_OPERAND(2);
IODevice::write(red,true);
if (amber) IODevice::write(amber,false);
IODevice::write(green,false);
continue;
}
if (opcode==OPCODE_TURNOUT) {
VPIN id=GET_OPERAND(0);
int addr=GET_OPERAND(1);
byte subAddr=GET_OPERAND(2);
Turnout::createDCC(id,addr,subAddr);
continue;
}
if (opcode==OPCODE_SERVOTURNOUT) {
VPIN id=GET_OPERAND(0);
int activeAngle=GET_OPERAND(1);
int inactiveAngle=GET_OPERAND(2);
Turnout::createServo(id,id,activeAngle,inactiveAngle);
continue;
}
if (opcode==OPCODE_PINTURNOUT) {
VPIN id=GET_OPERAND(0);
Turnout::createVpin(id,id);
continue;
}
// other opcodes are not needed on this pass
}
SKIPOP; // include ENDROUTES opcode
DIAG(F("EXRAIL %db, MAX_FLAGS=%d"), progCounter,MAX_FLAGS);
new RMFT2(0); // add the startup route
}
// This filter intercepst <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
(void)stream; // avoid compiler warning if we don't access this parameter
bool reject=false;
switch(opcode) {
case 'D':
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1);
opcode=0;
}
break;
case 't': // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>
// TODO - Monitor throttle commands and reject any that are in current automation
break;
case '/': // New EXRAIL command
reject=!parseSlash(stream,paramCount,p);
opcode=0;
break;
default: // other commands pass through
break;
}
if (reject) {
opcode=0;
StringFormatter::send(stream,F("<X>"));
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
StringFormatter::send(stream,F("\nPC=%d,DT=%d,LOCO=%d%c,SPEED=%d%c"),
task->progCounter,task->delayTime,task->loco,
task->invert?'I':' ',
task->speedo,
task->forward?'F':'R'
);
task=task->next;
if (task==loopTask) break;
}
// Now stream the flags
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag) {
StringFormatter::send(stream,F("\nflags[%d} "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case HASH_KEYWORD_PAUSE: // </ PAUSE>
if (paramCount!=1) return false;
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
return true;
case HASH_KEYWORD_RESUME: // </ RESUME>
if (paramCount!=1) return false;
pausingTask=NULL;
{
RMFT2 * task=loopTask;
while(task) {
if (task->loco) task->driveLoco(task->speedo);
task=task->next;
if (task==loopTask) break;
}
}
return true;
case HASH_KEYWORD_START: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=locateRouteStart(route);
if (pc<0) return false;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
return true;
default:
break;
}
// all other / commands take 1 parameter 0 to MAX_FLAGS-1
if (paramCount!=2 || p[1]<0 || p[1]>=MAX_FLAGS) return false;
switch (p[0]) {
case HASH_KEYWORD_RESERVE: // force reserve a section
setFlag(p[1],SECTION_FLAG);
return true;
case HASH_KEYWORD_FREE: // force free a section
setFlag(p[1],0,SECTION_FLAG);
return true;
case HASH_KEYWORD_LATCH:
setFlag(p[1], LATCH_FLAG);
return true;
case HASH_KEYWORD_UNLATCH:
setFlag(p[1], 0, LATCH_FLAG);
return true;
default:
return false;
}
}
// This emits Routes and Automations to Withrottle
// Automations are given a state to set the button to "handoff" which implies
// handing over the loco to the automation.
// Routes are given "Set" buttons and do not cause the loco to be handed over.
void RMFT2::emitWithrottleRouteList(Print* stream) {
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL%S\n"),RouteDescription);
}
RMFT2::RMFT2(int progCtr) {
progCounter=progCtr;
delayTime=0;
loco=0;
speedo=0;
forward=true;
invert=false;
stackDepth=0;
// chain into ring of RMFTs
if (loopTask==NULL) {
loopTask=this;
next=this;
}
else {
next=loopTask->next;
loopTask->next=this;
}
}
RMFT2::~RMFT2() {
if (next==this) loopTask=NULL;
else for (RMFT2* ring=next;;ring=ring->next) if (ring->next == this) {
ring->next=next;
loopTask=next;
break;
}
}
void RMFT2::createNewTask(int route, uint16_t cab) {
int pc=locateRouteStart(route);
if (pc<0) return;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
int RMFT2::locateRouteStart(int16_t _route) {
if (_route==0) return 0; // Route 0 is always start of ROUTES for default startup
for (int progCounter=0;;SKIPOP) {
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) {
DIAG(F("RMFT2 sequence %d not found"), _route);
return -1;
}
if ((opcode==OPCODE_ROUTE || opcode==OPCODE_AUTOMATION || opcode==OPCODE_SEQUENCE)
&& _route==(int)GET_OPERAND(0)) return progCounter;
}
return -1;
}
void RMFT2::driveLoco(byte speed) {
if (loco<0) return; // Caution, allows broadcast!
if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
DCC::setThrottle(loco,speed, forward^invert);
speedo=speed;
// TODO... if broadcast speed 0 then pause all other tasks.
}
bool RMFT2::readSensor(int16_t sensorId) {
VPIN vpin=abs(sensorId);
if (getFlag(vpin,LATCH_FLAG)) return true; // latched on
bool s= IODevice::read(vpin) ^ (sensorId<0);
if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sensorId);
return s;
}
bool RMFT2::skipIfBlock() {
// returns false if killed
short nest = 1;
while (nest > 0) {
SKIPOP;
byte opcode = GET_OPCODE;
switch(opcode) {
case OPCODE_ENDEXRAIL:
kill(F("missing ENDIF"), nest);
return false;
case OPCODE_IF:
case OPCODE_IFNOT:
case OPCODE_IFRANDOM:
case OPCODE_IFRESERVE:
nest++;
break;
case OPCODE_ENDIF:
nest--;
break;
default:
break;
}
}
return true;
}
/* static */ void RMFT2::readLocoCallback(int cv) {
progtrackLocoId=cv;
}
void RMFT2::loop() {
// Round Robin call to a RMFT task each time
if (loopTask==NULL) return;
loopTask=loopTask->next;
if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2();
}
void RMFT2::loop2() {
if (delayTime!=0 && millis()-delayStart < delayTime) return;
byte opcode = GET_OPCODE;
int16_t operand = GET_OPERAND(0);
// if (diag) DIAG(F("RMFT2 %d %d"),opcode,operand);
// Attention: Returning from this switch leaves the program counter unchanged.
// This is used for unfinished waits for timers or sensors.
// Breaking from this switch will step to the next step in the route.
switch ((OPCODE)opcode) {
case OPCODE_THROW:
Turnout::activate(operand, true);
break;
case OPCODE_CLOSE:
Turnout::activate(operand, false);
break;
case OPCODE_REV:
forward = false;
driveLoco(operand);
break;
case OPCODE_FWD:
forward = true;
driveLoco(operand);
break;
case OPCODE_SPEED:
driveLoco(operand);
break;
case OPCODE_INVERT_DIRECTION:
invert= !invert;
driveLoco(speedo);
break;
case OPCODE_RESERVE:
if (getFlag(operand,SECTION_FLAG)) {
driveLoco(0);
delayMe(500);
return;
}
setFlag(operand,SECTION_FLAG);
break;
case OPCODE_FREE:
setFlag(operand,0,SECTION_FLAG);
break;
case OPCODE_AT:
if (readSensor(operand)) break;
delayMe(50);
return;
case OPCODE_AFTER: // waits for sensor to hit and then remain off for 0.5 seconds. (must come after an AT operation)
if (readSensor(operand)) {
// reset timer to half a second and keep waiting
waitAfter=millis();
return;
}
if (millis()-waitAfter < 500 ) return;
break;
case OPCODE_LATCH:
setFlag(operand,LATCH_FLAG);
break;
case OPCODE_UNLATCH:
setFlag(operand,0,LATCH_FLAG);
break;
case OPCODE_SET:
IODevice::write(operand,true);
break;
case OPCODE_RESET:
IODevice::write(operand,false);
break;
case OPCODE_PAUSE:
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=this;
break;
case OPCODE_POM:
if (loco!=0) {
DCC::writeCVByteMain(loco, operand, GET_OPERAND(1));
}
break;
case OPCODE_RESUME:
pausingTask=NULL;
driveLoco(speedo);
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
break;
case OPCODE_IF: // do next operand if sensor set
if (!readSensor(operand)) if (!skipIfBlock()) return;
break;
case OPCODE_IFNOT: // do next operand if sensor not set
if (readSensor(operand)) if (!skipIfBlock()) return;
break;
case OPCODE_IFRANDOM: // do block on random percentage
if (random(100)>=operand) if (!skipIfBlock()) return;
break;
case OPCODE_IFRESERVE: // do block if we successfully RERSERVE
if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG);
else if (!skipIfBlock()) return;
break;
case OPCODE_ENDIF:
break;
case OPCODE_DELAY:
delayMe(operand*100);
break;
case OPCODE_DELAYMINS:
delayMe(operand*60*1000);
break;
case OPCODE_RANDWAIT:
delayMe((long)random(operand*100));
break;
case OPCODE_RED:
doSignal(operand,true,false,false);
break;
case OPCODE_AMBER:
doSignal(operand,false,true,false);
break;
case OPCODE_GREEN:
doSignal(operand,false,false,true);
break;
case OPCODE_FON:
DCC::setFn(loco,operand,true);
break;
case OPCODE_FOFF:
DCC::setFn(loco,operand,false);
break;
case OPCODE_FOLLOW:
progCounter=locateRouteStart(operand);
if (progCounter<0) kill(F("FOLLOW unknown"), operand);
return;
case OPCODE_CALL:
if (stackDepth==MAX_STACK_DEPTH) {
kill(F("CALL stack"), stackDepth);
return;
}
callStack[stackDepth++]=progCounter;
progCounter=locateRouteStart(operand);
if (progCounter<0) kill(F("CALL unknown"),operand);
return;
case OPCODE_RETURN:
if (stackDepth==0) {
kill(F("RETURN stack"));
return;
}
progCounter=callStack[--stackDepth];
return;
case OPCODE_ENDTASK:
case OPCODE_ENDEXRAIL:
kill();
return;
case OPCODE_JOIN:
DCC::setProgTrackSyncMain(true);
break;
case OPCODE_UNJOIN:
DCC::setProgTrackSyncMain(false);
break;
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
DCC::getLocoId(readLocoCallback);
break;
case OPCODE_READ_LOCO2:
if (progtrackLocoId<0) {
delayMe(100);
return; // still waiting for callback
}
loco=progtrackLocoId;
speedo=0;
forward=true;
invert=false;
break;
case OPCODE_START:
{
// Create new task and transfer loco.....
// but cheat by swapping prog counters with new task
int newPc=locateRouteStart(operand);
if (newPc<0) break;
new RMFT2(progCounter+3); // give new task my prog counter
progCounter=newPc; // and I'll carry on from new task position
}
break;
case OPCODE_SETLOCO:
{
loco=operand;
speedo=0;
forward=true;
invert=false;
}
break;
case OPCODE_SERVO: // OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),
IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2));
break;
case OPCODE_ROUTE:
case OPCODE_AUTOMATION:
case OPCODE_SEQUENCE:
DIAG(F("EXRAIL begin(%d)"),operand);
break;
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operad byte.
case OPCODE_SIGNAL: // Signal definition ignore at run time
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime
case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime
case OPCODE_ONCLOSE: // Turnout event catcers ignored here
case OPCODE_ONTHROW: // Turnout definition ignored at runtime
break;
default:
kill(F("INVOP"),operand);
}
// Falling out of the switch means move on to the next opcode
SKIPOP;
}
void RMFT2::delayMe(long delay) {
delayTime=delay;
delayStart=millis();
}
void RMFT2::setFlag(VPIN id,byte onMask, byte offMask) {
if (FLAGOVERFLOW(id)) return; // Outside range limit
byte f=flags[id];
f &= ~offMask;
f |= onMask;
}
byte RMFT2::getFlag(VPIN id,byte mask) {
if (FLAGOVERFLOW(id)) return 0; // Outside range limit
return flags[id]&mask;
}
void RMFT2::kill(const FSH * reason, int operand) {
if (reason) DIAG(F("EXRAIL ERROR pc=%d, cab=%d, %S %d"), progCounter,loco, reason, operand);
else if (diag) DIAG(F("ENDTASK at pc=%d"), progCounter);
delete this;
}
/* static */ void RMFT2::doSignal(VPIN id,bool red, bool amber, bool green) {
// CAUTION: hides class member progCounter
for (int progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) return;
if (opcode!=OPCODE_SIGNAL) continue;
byte redpin=GET_OPERAND(1);
if (redpin!=id)continue;
byte amberpin=GET_OPERAND(2);
byte greenpin=GET_OPERAND(3);
IODevice::write(redpin,red);
if (amberpin) IODevice::write(amberpin,amber);
if (greenpin) IODevice::write(amberpin,green);
return;
}
}
void RMFT2::turnoutEvent(VPIN id, bool thrown) {
byte huntFor=thrown? OPCODE_ONTHROW : OPCODE_ONCLOSE;
// caution hides class progCounter;
for (int progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) return;
if (opcode!=huntFor) continue;
if (id!=GET_OPERAND(0)) continue;
new RMFT2(progCounter); // new task starts at this instruction
return;
}
}

111
RMFT2.h Normal file
View File

@ -0,0 +1,111 @@
/*
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef RMFT2_H
#define RMFT2_H
#include "FSH.h"
#include "IODevice.h"
// The following are the operation codes (or instructions) for a kind of virtual machine.
// Each instruction is normally 2 bytes long with an operation code followed by a parameter.
// In cases where more than one parameter is required, the first parameter is followed by one
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
// searching easier as a parameter can never be confused with an opcode.
//
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
OPCODE_RESERVE,OPCODE_FREE,
OPCODE_AT,OPCODE_AFTER,
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
OPCODE_IF,OPCODE_IFNOT,OPCODE_ENDIF,OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,
OPCODE_PAUSE, OPCODE_RESUME,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL
};
// Flag bits for status of hardware and TPL
static const byte SECTION_FLAG = 0x01;
static const byte LATCH_FLAG = 0x02;
static const byte MAX_STACK_DEPTH=4;
static const short MAX_FLAGS=256;
#define FLAGOVERFLOW(x) x>=MAX_FLAGS
class RMFT2 {
public:
static void begin();
static void loop();
RMFT2(int progCounter);
RMFT2(int route, uint16_t cab);
~RMFT2();
static void readLocoCallback(int cv);
static void emitWithrottleRouteList(Print* stream);
static void createNewTask(int route, uint16_t cab);
static void turnoutEvent(VPIN id, bool thrown);
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int p[]);
static bool parseSlash(Print * stream, byte & paramCount, int p[]) ;
static void streamFlags(Print* stream);
static void setFlag(VPIN id,byte onMask, byte OffMask=0);
static byte getFlag(VPIN id,byte mask);
static int locateRouteStart(int16_t _route);
static int progtrackLocoId;
static void doSignal(VPIN id,bool red, bool amber, bool green);
static RMFT2 * loopTask;
static RMFT2 * pausingTask;
void delayMe(long millisecs);
void driveLoco(byte speedo);
bool readSensor(int16_t sensorId);
bool skipIfBlock();
bool readLoco();
void showManual();
void showProg(bool progOn);
bool doManual();
void loop2();
void kill(const FSH * reason=NULL,int operand=0);
static bool diag;
static const FLASH byte RouteCode[];
static const FLASH char RouteDescription[];
static byte flags[MAX_FLAGS];
// Local variables - exist for each instance/task
RMFT2 *next; // loop chain
int progCounter; // Byte offset of next route opcode in ROUTES table
unsigned long delayStart; // Used by opcodes that must be recalled before completing
unsigned long waitAfter; // Used by OPCODE_AFTER
unsigned long delayTime;
int loco;
bool forward;
bool invert;
int speedo;
byte stackDepth;
int callStack[MAX_STACK_DEPTH];
};
#endif

223
RMFTMacros.h Normal file
View File

@ -0,0 +1,223 @@
/*
* © 2020,2021 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* 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/>.
*/
#ifndef RMFTMacros_H
#define RMFTMacros_H
// The entire automation script is contained within a byte array RMFT2::RouteCode[]
// made up of opcode and parameter pairs.
// ech opcode is a 1 byte operation plus 2 byte operand.
// The array is normally built using the macros below as this makes it easier
// to manage the cases where:
// - padding must be applied to ensure the correct alignment of the next instruction
// - large parameters must be split up
// - multiple parameters aligned correctly
// - a single macro requires multiple operations
// Descriptive texts for routes and animations are created in a sepaerate array RMFT2::RouteDescription[]
// but since the C preprocessor is such a wimp, we have to pass over the myAutomation.h 2 times with
// different macros.
#define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF
#define NOP 0,0
// CAUTION: The macros below are triple passed over myAutomation.h
// Adding a macro here must have equivalent macros or no-ops in pass 2 and 3
#define ALIAS(name,value) const int name=value;
#define EXRAIL const FLASH byte RMFT2::RouteCode[] = {
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define ENDTASK OPCODE_ENDTASK,NOP,
#define DONE OPCODE_ENDTASK,NOP,
#define ENDEXRAIL OPCODE_ENDTASK,NOP,OPCODE_ENDEXRAIL,NOP };
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
#define CALL(route) OPCODE_CALL,V(route),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define DELAY(ms) OPCODE_DELAY,V(ms/100),
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100),
#define ENDIF OPCODE_ENDIF,NOP,
#define ESTOP OPCODE_SPEED,V(1),
#define FOFF(func) OPCODE_FOFF,V(func),
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
#define FON(func) OPCODE_FON,V(func),
#define FREE(blockid) OPCODE_FREE,V(blockid),
#define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,NOP,
#define JOIN OPCODE_JOIN,NOP,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define PAUSE OPCODE_PAUSE,NOP,
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#define READ_LOCO OPCODE_READ_LOCO1,NOP,OPCODE_READ_LOCO2,NOP,
#define RED(signal_id) OPCODE_RED,V(signal_id),
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
#define RESET(sensor_id) OPCODE_RESET,V(sensor_id),
#define RESUME OPCODE_RESUME,NOP,
#define RETURN OPCODE_RETURN,NOP,
#define REV(speed) OPCODE_REV,V(speed),
#define START(route) OPCODE_START,V(route),
#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SET(sensor_id) OPCODE_SET,V(sensor_id),
#define SPEED(speed) OPCODE_SPEED,V(speed),
#define STOP OPCODE_SPEED,V(0),
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) OPCODE_SIGNAL,V(redpin),OPCODE_PAD,V(amberpin),OPCODE_PAD,V(greenpin),
#define SERVO_TURNOUT(pin,activeAngle,inactiveAngle) OPCODE_SERVOTURNOUT,V(pin),OPCODE_PAD,V(actibeAngle),OPCODE
#define PIN_TURNOUT(pin) OPCODE_PINTURNOUT,V(pin),
#define THROW(id) OPCODE_THROW,V(id),
#define TURNOUT(id,addr,subaddr) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
#define UNJOIN OPCODE_UNJOIN,NOP,
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
// PASS1 Build RouteCode
#include "myAutomation.h"
#undef ALIAS
#undef EXRAIL
#undef AUTOMATION
#undef ROUTE
#undef SEQUENCE
#undef ENDTASK
#undef DONE
#undef ENDEXRAIL
#undef AFTER
#undef AMBER
#undef AT
#undef CALL
#undef CLOSE
#undef DELAY
#undef DELAYMINS
#undef DELAYRANDOM
#undef ENDIF
#undef ESTOP
#undef FOFF
#undef FOLLOW
#undef FON
#undef FREE
#undef FWD
#undef GREEN
#undef IF
#undef IFNOT
#undef IFRANDOM
#undef IFRESERVE
#undef INVERT_DIRECTION
#undef JOIN
#undef LATCH
#undef ONCLOSE
#undef ONTHROW
#undef PAUSE
#undef POM
#undef READ_LOCO
#undef RED
#undef RESERVE
#undef RESET
#undef RESUME
#undef RETURN
#undef REV
#undef START
#undef SERVO
#undef SETLOCO
#undef SET
#undef SPEED
#undef STOP
#undef SIGNAL
#undef SERVO_TURNOUT
#undef PIN_TURNOUT
#undef THROW
#undef TURNOUT
#undef UNJOIN
#undef UNLATCH
//==================
// Pass2 Macros convert descriptions to a flash string constant in withrottle format.
// Most macros are simply ignored in this pass.
#define ALIAS(name,value)
#define EXRAIL const FLASH char RMFT2::RouteDescription[]=
#define AUTOMATION(id, description) "]\\[A" #id "}|{" description "}|{4"
#define ROUTE(id, description) "]\\[R" #id "}|{" description "}|{2"
#define SEQUENCE(id)
#define ENDTASK
#define DONE
#define ENDEXRAIL "";
#define AFTER(sensor_id)
#define AMBER(signal_id)
#define AT(sensor_id)
#define CALL(route)
#define CLOSE(id)
#define DELAY(mindelay)
#define DELAYMINS(mindelay)
#define DELAYRANDOM(mindelay,maxdelay)
#define ENDIF
#define ESTOP
#define FOFF(func)
#define FOLLOW(route)
#define FON(func)
#define FREE(blockid)
#define FWD(speed)
#define GREEN(signal_id)
#define IF(sensor_id)
#define IFNOT(sensor_id)
#define IFRANDOM(percent)
#define IFRESERVE(block)
#define INVERT_DIRECTION
#define JOIN
#define LATCH(sensor_id)
#define ONCLOSE(turnout_id)
#define ONTHROW(turnout_id)
#define PAUSE
#define POM(cv,value)
#define READ_LOCO
#define RED(signal_id)
#define RESERVE(blockid)
#define RESET(sensor_id)
#define RESUME
#define RETURN
#define REV(speed)
#define START(route)
#define SERVO(id,position,profile)
#define SETLOCO(loco)
#define SET(sensor_id)
#define SPEED(speed)
#define STOP
#define SIGNAL(redpin,amberpin,greenpin)
#define SERVO_TURNOUT(pin,activeAngle,inactiveAngle)
#define PIN_TURNOUT(pin)
#define THROW(id)
#define TURNOUT(id,addr,subaddr)
#define UNJOIN
#define UNLATCH(sensor_id)
#include "myAutomation.h"
#endif

Binary file not shown.

Binary file not shown.

View File

@ -99,6 +99,9 @@ SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
lcdRows = height / 8;
lcdCols = width / 6;
// Initialise request block for I2C
requestBlock.init();
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
@ -158,13 +161,15 @@ void SSD1306AsciiWire::setRowNative(uint8_t line) {
if (row < m_displayHeight) {
m_row = row;
m_col = m_colOffset;
// Before using buffer, wait for last request to complete
requestBlock.wait();
// Build output buffer for I2C
uint8_t len = 0;
outputBuffer[len++] = 0x00; // Set to command mode
outputBuffer[len++] = SSD1306_SETLOWCOLUMN | (m_col & 0XF);
outputBuffer[len++] = SSD1306_SETHIGHCOLUMN | (m_col >> 4);
outputBuffer[len++] = SSD1306_SETSTARTPAGE | (m_row/8);
I2CManager.write(m_i2cAddr, outputBuffer, len);
I2CManager.write(m_i2cAddr, outputBuffer, len, &requestBlock);
}
}
//------------------------------------------------------------------------------
@ -189,6 +194,8 @@ size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
#endif
ch -= m_fontFirstChar;
base += fontWidth * ch;
// Before using buffer, wait for last request to complete
requestBlock.wait();
// Build output buffer for I2C
outputBuffer[0] = 0x40; // set SSD1306 controller to data mode
uint8_t bufferPos = 1;
@ -200,7 +207,7 @@ size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
outputBuffer[bufferPos++] = 0;
// Write the data to I2C display
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos);
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);
m_col += fontWidth + letterSpacing;
return 1;
}

View File

@ -58,22 +58,24 @@ class SSD1306AsciiWire : public LCDDisplay {
void begin(const DevType* dev, uint8_t i2cAddr);
// Clear the display and set the cursor to (0, 0).
void clearNative();
void clearNative() override;
// Set cursor to start of specified text line
void setRowNative(byte line);
void setRowNative(byte line) override;
// Initialize the display controller.
void init(const DevType* dev);
// Write one character to OLED
size_t writeNative(uint8_t c);
size_t writeNative(uint8_t c) override;
// Display characteristics / initialisation
static const DevType FLASH Adafruit128x32;
static const DevType FLASH Adafruit128x64;
static const DevType FLASH SH1106_132x64;
bool isBusy() override { return requestBlock.isBusy(); }
private:
// Cursor column.
uint8_t m_col;
@ -97,6 +99,7 @@ class SSD1306AsciiWire : public LCDDisplay {
uint8_t m_i2cAddr;
I2CRB requestBlock;
uint8_t outputBuffer[fontWidth+letterSpacing+1];
static const uint8_t blankPixels[];

View File

@ -1,5 +1,6 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021, modified by Neil McKechnie. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@ -27,9 +28,9 @@ or be allowed to float HIGH if use of the Arduino Pin's internal pull-up resisto
To ensure proper voltage levels, some part of the Sensor circuitry
MUST be tied back to the same ground as used by the Arduino.
The Sensor code below utilizes exponential smoothing to "de-bounce" spikes generated by
The Sensor code below utilises "de-bounce" logic to remove spikes generated by
mechanical switches and transistors. This avoids the need to create smoothing circuitry
for each sensor. You may need to change these parameters through trial and error for your specific sensors.
for each sensor. You may need to change the parameters through trial and error for your specific sensors.
To have this sketch monitor one or more Arduino pins for sensor triggers, first define/edit/delete
sensor definitions using the following variation of the "S" command:
@ -68,45 +69,134 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
#include "StringFormatter.h"
#include "Sensors.h"
#include "EEStore.h"
#include "IODevice.h"
///////////////////////////////////////////////////////////////////////////////
//
// checks one defined sensors and prints _changed_ sensor state
// checks a number of defined sensors per entry and prints _changed_ sensor state
// to stream unless stream is NULL in which case only internal
// state is updated. Then advances to next sensor which will
// be checked att next invocation.
// be checked at next invocation. Each cycle of reading all sensors will
// be initiated no more frequently than the time set by 'cycleInterval' microseconds.
//
// The list of sensors is divided such that the first part of the list
// contains sensors that support change notification via callback, and the second
// part of the list contains sensors that require cyclic polling. The start of the
// second part of the list is determined from by the 'firstPollSensor' pointer.
///////////////////////////////////////////////////////////////////////////////
void Sensor::checkAll(Print *stream){
uint16_t sensorCount = 0;
if (firstSensor == NULL) return;
if (readingSensor == NULL) readingSensor=firstSensor;
#ifdef USE_NOTIFY
// Register the event handler ONCE!
if (!inputChangeCallbackRegistered)
nextInputChangeCallback = IODevice::registerInputChangeNotification(inputChangeCallback);
inputChangeCallbackRegistered = true;
#endif
bool sensorstate = digitalRead(readingSensor->data.pin);
if (!sensorstate == readingSensor->active) { // active==true means sensorstate=0/false so sensor unchanged
// no change
if (readingSensor->latchdelay != 0) {
// enable if you want to debug contact jitter
//if (stream != NULL) StringFormatter::send(stream, F("JITTER %d %d\n"),
// readingSensor->latchdelay, readingSensor->data.snum);
readingSensor->latchdelay=0; // reset
if (firstSensor == NULL) return; // No sensors to be scanned
if (readingSensor == NULL) {
// Not currently scanning sensor list
unsigned long thisTime = micros();
if (thisTime - lastReadCycle >= cycleInterval) {
// Required time elapsed since last read cycle started,
// so initiate new scan through the sensor list
readingSensor = firstSensor;
#ifdef USE_NOTIFY
if (firstSensor == firstPollSensor)
pollSignalPhase = true;
else
pollSignalPhase = false;
#endif
lastReadCycle = thisTime;
}
} else if (readingSensor->latchdelay < 127) { // byte, max 255, good value unknown yet
// change but first increase anti-jitter counter
readingSensor->latchdelay++;
} else {
// make the change
readingSensor->active = !sensorstate;
readingSensor->latchdelay=0; // reset
if (stream != NULL) StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
}
readingSensor=readingSensor->nextSensor;
// Loop until either end of list is encountered or we pause for some reason
bool pause = false;
while (readingSensor != NULL && !pause) {
#ifdef USE_NOTIFY
// Check if we have reached the start of the polled portion of the sensor list.
if (readingSensor == firstPollSensor)
pollSignalPhase = true;
#endif
// Where the sensor is attached to a pin, read pin status. For sources such as LCN,
// which don't have an input pin to read, the LCN class calls setState() to update inputState when
// a message is received. The IODevice::read() call returns 1 for active pins (0v) and 0 for inactive (5v).
// Also, on HAL drivers that support change notifications, the driver calls the notification callback
// routine when an input signal change is detected, and this updates the inputState directly,
// so these inputs don't need to be polled here.
VPIN pin = readingSensor->data.pin;
#ifdef USE_NOTIFY
if (pollSignalPhase)
#endif
if (pin!=VPIN_NONE) readingSensor->inputState = IODevice::read(pin);
// Check if changed since last time, and process changes.
if (readingSensor->inputState == readingSensor->active) {
// no change
readingSensor->latchDelay = minReadCount; // Reset counter
} else if (readingSensor->latchDelay > 0) {
// change detected, but first decrement delay
readingSensor->latchDelay--;
} else {
// change validated, act on it.
readingSensor->active = readingSensor->inputState;
readingSensor->latchDelay = minReadCount; // Reset counter
if (stream != NULL) {
StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
pause = true; // Don't check any more sensors on this entry
}
}
// Move to next sensor in list.
readingSensor = readingSensor->nextSensor;
// Currently process max of 16 sensors per entry for polled sensors, and
// 16 per entry for sensors notified by callback.
// Performance measurements taken during development indicate that, with 64 sensors configured
// on 8x 8-pin PCF8574 GPIO expanders, all inputs can be read within 1.4ms (400Mhz I2C bus speed), and a
// full cycle of scanning 64 sensors for changes takes between 1.9 and 3.2 milliseconds.
sensorCount++;
#ifdef USE_NOTIFY
if (pollSignalPhase) {
#endif
if (sensorCount >= 16) pause = true;
#ifdef USE_NOTIFY
} else
{
if (sensorCount >= 16) pause = true;
}
#endif
}
} // Sensor::checkAll
#ifdef USE_NOTIFY
// Callback from HAL (IODevice class) when a digital input change is recognised.
// Updates the inputState field, which is subsequently scanned for changes in the checkAll
// method. Ideally the <Q>/<q> message should be sent from here, instead of waiting for
// the checkAll method, but the output stream is not available at this point.
void Sensor::inputChangeCallback(VPIN vpin, int state) {
Sensor *tt;
// This bit is not ideal since it has, potentially, to look through the entire list of
// sensors to find the one that has changed. Ideally this should be improved somehow.
for (tt=firstSensor; tt!=NULL ; tt=tt->nextSensor) {
if (tt->data.pin == vpin) break;
}
if (tt != NULL) { // Sensor found
tt->inputState = (state != 0);
}
// Call next registered callback function
if (nextInputChangeCallback) nextInputChangeCallback(vpin, state);
}
#endif
///////////////////////////////////////////////////////////////////////////////
//
// prints all sensor states to stream
@ -115,40 +205,68 @@ void Sensor::checkAll(Print *stream){
void Sensor::printAll(Print *stream){
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
if (stream != NULL)
if (stream != NULL) {
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
StringFormatter::send(stream, F("<%c %d>\n"), tt->active ? 'Q' : 'q', tt->data.snum);
}
} // loop over all sensors
} // Sensor::printAll
///////////////////////////////////////////////////////////////////////////////
// Static Function to create/find Sensor object.
Sensor *Sensor::create(int snum, int pin, int pullUp){
Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
Sensor *tt;
if(firstSensor==NULL){
firstSensor=(Sensor *)calloc(1,sizeof(Sensor));
tt=firstSensor;
} else if((tt=get(snum))==NULL){
tt=firstSensor;
while(tt->nextSensor!=NULL)
tt=tt->nextSensor;
tt->nextSensor=(Sensor *)calloc(1,sizeof(Sensor));
tt=tt->nextSensor;
if (pin > VPIN_MAX && pin != VPIN_NONE) return NULL;
remove(snum); // Unlink and free any existing sensor with the same id, before creating the new one.
tt = (Sensor *)calloc(1,sizeof(Sensor));
if (!tt) return tt; // memory allocation failure
#ifdef USE_NOTIFY
if (pin == VPIN_NONE || IODevice::hasCallback(pin)) {
// Callback available, or no pin to read, so link sensor on to the start of the list
tt->nextSensor = firstSensor;
firstSensor = tt;
if (lastSensor == NULL) lastSensor = tt; // This is only item in list.
} else {
// No callback, so add to end of list so it's polled.
if (lastSensor != NULL) lastSensor->nextSensor = tt;
lastSensor = tt;
if (!firstSensor) firstSensor = tt;
if (!firstPollSensor) firstPollSensor = tt;
}
#else
tt->nextSensor = firstSensor;
firstSensor = tt;
#endif
if(tt==NULL) return tt; // problem allocating memory
tt->data.snum = snum;
tt->data.pin = pin;
tt->data.pullUp = pullUp;
tt->active = 0;
tt->inputState = 0;
tt->latchDelay = minReadCount;
tt->data.snum=snum;
tt->data.pin=pin;
tt->data.pullUp=(pullUp==0?LOW:HIGH);
tt->active=false;
tt->latchdelay=0;
pinMode(pin,INPUT); // set mode to input
digitalWrite(pin,pullUp); // don't use Arduino's internal pull-up resistors for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
int params[] = {pullUp};
if (pin != VPIN_NONE)
IODevice::configure(pin, IODevice::CONFIGURE_INPUT, 1, params);
// Generally, internal pull-up resistors are not, on their own, sufficient
// for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
return tt;
}
///////////////////////////////////////////////////////////////////////////////
// Object method to directly change the input state, for sensors such as LCN which are updated
// by means other than by polling an input.
void Sensor::setState(int value) {
// Trigger sensor change to be reported on next checkAll loop.
inputState = (value != 0);
latchDelay = 0; // Don't wait for anti-jitter logic
}
///////////////////////////////////////////////////////////////////////////////
@ -166,13 +284,23 @@ bool Sensor::remove(int n){
for(tt=firstSensor;tt!=NULL && tt->data.snum!=n;pp=tt,tt=tt->nextSensor);
if (tt==NULL) return false;
if(tt==firstSensor)
firstSensor=tt->nextSensor;
else
pp->nextSensor=tt->nextSensor;
// Unlink the sensor from the list
if(tt==firstSensor)
firstSensor=tt->nextSensor;
else
pp->nextSensor=tt->nextSensor;
#ifdef USE_NOTIFY
if (tt==lastSensor)
lastSensor = pp;
if (tt==firstPollSensor)
firstPollSensor = tt->nextSensor;
#endif
// Check if the sensor being deleted is the next one to be read. If so,
// make the following one the next one to be read.
if (readingSensor==tt) readingSensor=tt->nextSensor;
free(tt);
return true;
@ -186,7 +314,7 @@ void Sensor::load(){
for(uint16_t i=0;i<EEStore::eeStore->data.nSensors;i++){
EEPROM.get(EEStore::pointer(),data);
tt=create(data.snum,data.pin,data.pullUp);
tt=create(data.snum, data.pin, data.pullUp);
EEStore::advance(sizeof(tt->data));
}
}
@ -211,3 +339,12 @@ void Sensor::store(){
Sensor *Sensor::firstSensor=NULL;
Sensor *Sensor::readingSensor=NULL;
unsigned long Sensor::lastReadCycle=0;
#ifdef USE_NOTIFY
Sensor *Sensor::firstPollSensor = NULL;
Sensor *Sensor::lastSensor = NULL;
bool Sensor::pollSignalPhase = false;
IONotifyStateChangeCallback *Sensor::nextInputChangeCallback = 0;
bool Sensor::inputChangeCallbackRegistered = false;
#endif

View File

@ -20,29 +20,82 @@
#define Sensor_h
#include "Arduino.h"
#include "IODevice.h"
#define SENSOR_DECAY 0.03
// Uncomment the following #define statement to use callback notification
// where the driver supports it.
// The principle of callback notification is to avoid the Sensor class
// having to poll the device driver cyclically for input values, and then scan
// for changes. Instead, when the driver scans the inputs, if it detects
// a change it invokes a callback function in the Sensor class. In the current
// implementation, the advantages are limited because (a) the Sensor class
// performs debounce checks, and (b) the Sensor class does not have a
// static reference to the output stream for sending <Q>/<q> messages
// when a change is detected. These restrictions mean that the checkAll()
// method still has to iterate through all of the Sensor objects looking
// for changes.
#define USE_NOTIFY
struct SensorData {
int snum;
uint8_t pin;
VPIN pin;
uint8_t pullUp;
};
struct Sensor{
static Sensor *firstSensor;
static Sensor *readingSensor;
class Sensor{
// The sensor list is a linked list where each sensor's 'nextSensor' field points to the next.
// The pointer is null in the last on the list.
// To partition the sensor into those sensors which require polling through cyclic calls
// to 'IODevice::read(vpin)', and those which support callback on change, 'firstSensor'
// points to the start of the overall list, and 'lastSensor' points to the end of the list
// (the last sensor object). This structure allows sensors to be added to the start or the
// end of the list easily. So if an input pin supports change notification, it is placed at the
// end of the list. If not, it is placed at the beginning. And the pointer 'firstPollSensor'
// is set to the first of the sensor objects that requires scanning. Thus, we can iterate
// through the whole list, or just through the part that requires scanning.
public:
SensorData data;
boolean active;
byte latchdelay;
struct {
uint8_t active:1;
uint8_t inputState:1;
uint8_t latchDelay:6;
}; // bit 7=active; bit 6=input state; bits 5-0=latchDelay
static Sensor *firstSensor;
#ifdef USE_NOTIFY
static Sensor *firstPollSensor;
static Sensor *lastSensor;
#endif
// readingSensor points to the next sensor to be polled, or null if the poll cycle is completed for
// the period.
static Sensor *readingSensor;
// Constructor
Sensor();
Sensor *nextSensor;
void setState(int state);
static void load();
static void store();
static Sensor *create(int, int, int);
static Sensor* get(int);
static bool remove(int);
static void checkAll(Print *);
static void printAll(Print *);
static Sensor *create(int id, VPIN vpin, int pullUp);
static Sensor* get(int id);
static bool remove(int id);
static void checkAll(Print *stream);
static void printAll(Print *stream);
static unsigned long lastReadCycle; // value of micros at start of last read cycle
static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.
// should not be less than device scan cycle time.
static const unsigned int minReadCount = 1; // number of additional scans before acting on change
// E.g. 1 means that a change is ignored for one scan and actioned on the next.
// Max value is 63
#ifdef USE_NOTIFY
static bool pollSignalPhase;
static void inputChangeCallback(VPIN vpin, int state);
static IONotifyStateChangeCallback *nextInputChangeCallback;
static bool inputChangeCallbackRegistered;
#endif
}; // Sensor
#endif

View File

@ -18,67 +18,154 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#define EESTOREDEBUG
#include "defines.h"
#include "Turnouts.h"
#include "EEStore.h"
#include "PWMServoDriver.h"
#include "StringFormatter.h"
#include "RMFT2.h"
#ifdef EESTOREDEBUG
#include "DIAG.h"
#endif
// print all turnout states to stream
// Keywords used for turnout configuration.
const int16_t HASH_KEYWORD_SERVO=27709;
const int16_t HASH_KEYWORD_DCC=6436;
const int16_t HASH_KEYWORD_VPIN=-415;
enum unit8_t {
TURNOUT_DCC = 1,
TURNOUT_SERVO = 2,
TURNOUT_VPIN = 3,
TURNOUT_LCN = 4,
};
///////////////////////////////////////////////////////////////////////////////
// Static function to print all Turnout states to stream in form "<H id state>"
void Turnout::printAll(Print *stream){
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, tt->data.active);
} // Turnout::printAll
bool Turnout::activate(int n,bool state){
///////////////////////////////////////////////////////////////////////////////
// Object method to print configuration of one Turnout to stream, in one of the following forms:
// <H id SERVO vpin activePos inactivePos profile state>
// <H id LCN state>
// <H id VPIN vpin state>
// <H id DCC address subAddress state>
void Turnout::print(Print *stream){
uint8_t state = ((data.active) != 0);
uint8_t type = data.type;
switch (type) {
case TURNOUT_LCN:
// LCN Turnout
StringFormatter::send(stream, F("<H %d LCN %d>\n"), data.id, state);
break;
case TURNOUT_DCC:
// DCC Turnout
StringFormatter::send(stream, F("<H %d DCC %d %d %d>\n"), data.id,
(((data.dccAccessoryData.address-1) >> 2)+1), ((data.dccAccessoryData.address-1) & 3), state);
break;
case TURNOUT_VPIN:
// VPIN Digital output
StringFormatter::send(stream, F("<H %d VPIN %d %d>\n"), data.id, data.vpinData.vpin, state);
break;
#ifndef IO_NO_HAL
case TURNOUT_SERVO:
// Servo Turnout
StringFormatter::send(stream, F("<H %d SERVO %d %d %d %d %d>\n"), data.id, data.servoData.vpin,
data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, state);
break;
#endif
default:
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to activate/deactivate Turnout with ID 'n'.
// Returns false if turnout not found.
bool Turnout::activate(int n, bool state){
#ifdef EESTOREDEBUG
DIAG(F("Turnout::activate(%d,%d)"),n,state);
#endif
Turnout * tt=get(n);
if (tt==NULL) return false;
if (!tt) return false;
tt->activate(state);
turnoutlistHash++;
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Static function to check if the Turnout with ID 'n' is activated or not.
// Returns false if turnout not found.
bool Turnout::isActive(int n){
Turnout * tt=get(n);
if (tt==NULL) return false;
return tt->data.tStatus & STATUS_ACTIVE;
if (!tt) return false;
return tt->isActive();
}
///////////////////////////////////////////////////////////////////////////////
// Object function to check the status of Turnout is activated or not.
bool Turnout::isActive() {
return data.active;
}
///////////////////////////////////////////////////////////////////////////////
// Object method to activate or deactivate the Turnout.
// activate is virtual here so that it can be overridden by a non-DCC turnout mechanism
void Turnout::activate(bool state) {
#ifdef EESTOREDEBUG
DIAG(F("Turnout::activate(%d)"),state);
#endif
if (data.address==LCN_TURNOUT_ADDRESS) {
// A LCN turnout is transmitted to the LCN master.
LCN::send('T',data.id,state);
return; // The tStatus will be updated by a message from the LCN master, later.
if (data.type == TURNOUT_LCN) {
// A LCN turnout is transmitted to the LCN master.
LCN::send('T', data.id, state);
return; // The tStatus will be updated by a message from the LCN master, later.
}
if (state)
data.tStatus|=STATUS_ACTIVE;
else
data.tStatus &= ~STATUS_ACTIVE;
if (data.tStatus & STATUS_PWM)
PWMServoDriver::setServo(data.tStatus & STATUS_PWMPIN, (data.inactiveAngle+(state?data.moveAngle:0)));
else
DCC::setAccessory(data.address,data.subAddress, state);
// Save state if stored in EEPROM
if (EEStore::eeStore->data.nTurnouts > 0 && num > 0)
EEPROM.put(num, data.tStatus);
data.active = state;
switch (data.type) {
case TURNOUT_DCC:
DCC::setAccessory((((data.dccAccessoryData.address-1) >> 2) + 1),
((data.dccAccessoryData.address-1) & 3), state);
break;
#ifndef IO_NO_HAL
case TURNOUT_SERVO:
IODevice::write(data.servoData.vpin, state);
break;
#endif
case TURNOUT_VPIN:
IODevice::write(data.vpinData.vpin, state);
break;
}
// Save state if stored in EEPROM
if (EEStore::eeStore->data.nTurnouts > 0 && num > 0)
EEPROM.put(num, data.tStatus);
#if defined(RMFT_ACTIVE)
RMFT2::turnoutEvent(data.id, state);
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Static function to find Turnout object specified by ID 'n'. Return NULL if not found.
Turnout* Turnout::get(int n){
Turnout *tt;
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;tt=tt->nextTurnout);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to delete Turnout object specified by ID 'n'. Return false if not found.
bool Turnout::remove(int n){
Turnout *tt,*pp=NULL;
@ -98,25 +185,50 @@ bool Turnout::remove(int n){
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load all Turnout definitions from EEPROM
// TODO: Consider transmitting the initial state of the DCC/LCN turnout here.
// (already done for servo turnouts and VPIN turnouts).
void Turnout::load(){
struct TurnoutData data;
Turnout *tt;
Turnout *tt=NULL;
for(uint16_t i=0;i<EEStore::eeStore->data.nTurnouts;i++){
EEPROM.get(EEStore::pointer(),data);
if (data.tStatus & STATUS_PWM) tt=create(data.id,data.tStatus & STATUS_PWMPIN, data.inactiveAngle,data.moveAngle);
else tt=create(data.id,data.address,data.subAddress);
tt->data.tStatus=data.tStatus;
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to status byte within EEPROM
EEStore::advance(sizeof(tt->data));
// Retrieve data
EEPROM.get(EEStore::pointer(), data);
int lastKnownState = data.active;
switch (data.type) {
case TURNOUT_DCC:
tt=createDCC(data.id, ((data.dccAccessoryData.address-1)>>2)+1, (data.dccAccessoryData.address-1)&3); // DCC-based turnout
break;
case TURNOUT_LCN:
// LCN turnouts are created when the remote device sends a message.
break;
#ifndef IO_NO_HAL
case TURNOUT_SERVO:
tt=createServo(data.id, data.servoData.vpin,
data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, lastKnownState);
break;
#endif
case TURNOUT_VPIN:
tt=createVpin(data.id, data.vpinData.vpin, lastKnownState); // VPIN-based turnout
break;
default:
tt=NULL;
}
if (tt) tt->num = EEStore::pointer() + offsetof(TurnoutData, tStatus); // Save pointer to tStatus byte within EEPROM
// Advance by the actual size of the individual turnout struct.
EEStore::advance(data.size);
#ifdef EESTOREDEBUG
tt->print(tt);
if (tt) print(tt);
#endif
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to store all Turnout definitions to EEPROM
void Turnout::store(){
Turnout *tt;
@ -125,59 +237,155 @@ void Turnout::store(){
EEStore::eeStore->data.nTurnouts=0;
while(tt!=NULL){
// LCN turnouts aren't saved to EEPROM
if (tt->data.type != TURNOUT_LCN) {
#ifdef EESTOREDEBUG
tt->print(tt);
print(tt);
#endif
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to tstatus byte within EEPROM
EEPROM.put(EEStore::pointer(),tt->data);
EEStore::advance(sizeof(tt->data));
tt->num = EEStore::pointer() + offsetof(TurnoutData, tStatus); // Save pointer to tstatus byte within EEPROM
EEPROM.put(EEStore::pointer(),tt->data);
EEStore::advance(tt->data.size);
EEStore::eeStore->data.nTurnouts++;
}
tt=tt->nextTurnout;
EEStore::eeStore->data.nTurnouts++;
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function for creating a DCC-controlled Turnout.
Turnout *Turnout::create(int id, int add, int subAdd){
Turnout *Turnout::createDCC(int id, uint16_t add, uint8_t subAdd){
if (add > 511 || subAdd > 3) return NULL;
Turnout *tt=create(id);
tt->data.address=add;
tt->data.subAddress=subAdd;
tt->data.tStatus=0;
if (!tt) return(tt);
tt->data.type = TURNOUT_DCC;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.dccAccessoryData);
tt->data.active = 0;
tt->data.dccAccessoryData.address = ((add-1) << 2) + subAdd + 1;
return(tt);
}
Turnout *Turnout::create(int id, byte pin, int activeAngle, int inactiveAngle){
///////////////////////////////////////////////////////////////////////////////
// Static function for creating a LCN-controlled Turnout.
Turnout *Turnout::createLCN(int id, uint8_t state) {
Turnout *tt=create(id);
tt->data.tStatus= STATUS_PWM | (pin & STATUS_PWMPIN);
tt->data.inactiveAngle=inactiveAngle;
tt->data.moveAngle=activeAngle-inactiveAngle;
if (!tt) return(tt);
tt->data.type = TURNOUT_LCN;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.lcnData);
tt->data.active = (state != 0);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function for associating a Turnout id with a virtual pin in IODevice space.
// The actual creation and configuration of the pin must be done elsewhere,
// e.g. in mySetup.cpp during startup of the CS.
Turnout *Turnout::createVpin(int id, VPIN vpin, uint8_t state){
if (vpin > VPIN_MAX) return NULL;
Turnout *tt=create(id);
if(!tt) return(tt);
tt->data.type = TURNOUT_VPIN;;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.vpinData);
tt->data.active = (state != 0);
tt->data.vpinData.vpin = vpin;
IODevice::write(vpin, state); // Set initial state of output.
return(tt);
}
#ifndef IO_NO_HAL
///////////////////////////////////////////////////////////////////////////////
// Method for creating a Servo Turnout, e.g. connected to PCA9685 PWM device.
Turnout *Turnout::createServo(int id, VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint8_t state){
if (activePosition > 511 || inactivePosition > 511 || profile > 4) return NULL;
Turnout *tt=create(id);
if (!tt) return(tt);
if (tt->data.type != TURNOUT_SERVO) tt->data.active = (state != 0); // Retain current state if it's an existing servo turnout.
tt->data.type = TURNOUT_SERVO;
tt->data.size = sizeof(tt->data.header) + sizeof(tt->data.servoData);
tt->data.servoData.vpin = vpin;
tt->data.servoData.activePosition = activePosition;
tt->data.servoData.inactivePosition = inactivePosition;
tt->data.servoData.profile = profile;
// Configure PWM interface device
int deviceParams[] = {(int)activePosition, (int)inactivePosition, profile, tt->data.active};
if (!IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, deviceParams)) {
remove(id);
return NULL;
}
return(tt);
}
#endif
///////////////////////////////////////////////////////////////////////////////
// Support for <T id SERVO pin activepos inactive pos profile>
// and <T id DCC address subaddress>
// and <T id VPIN pin>
Turnout *Turnout::create(int id, int params, int16_t p[]) {
#ifndef IO_NO_HAL
if (p[0] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n>
if (params == 5)
return createServo(id, (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], (uint8_t)p[4]);
else
return NULL;
} else
#endif
if (p[0] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
if (params==2)
return createVpin(id, p[1]);
else
return NULL;
} else
if (p[0]==HASH_KEYWORD_DCC) {
if (params==3 && p[1]>0 && p[1]<=512 && p[2]>=0 && p[2]<4) // <T id DCC n n>
return createDCC(id, p[1], p[2]);
else if (params==2 && p[1]>0 && p[1]<=512*4) // <T id DCC nn>
return createDCC(id, (p[1]-1)/4+1, (p[1]-1)%4);
else
return NULL;
} else if (params==2) { // <T id n n> for DCC or LCN
return createDCC(id, p[0], p[1]);
}
#ifndef IO_NO_HAL
else if (params==3) { // legacy <T id n n n> for Servo
return createServo(id, (VPIN)p[0], (uint16_t)p[1], (uint16_t)p[2]);
}
#endif
return NULL;
}
///////////////////////////////////////////////////////////////////////////////
// Create basic Turnout object. The details of what sort of object it is
// controlling are not set here.
Turnout *Turnout::create(int id){
Turnout *tt=get(id);
if (tt==NULL) {
tt=(Turnout *)calloc(1,sizeof(Turnout));
if (!tt) return (tt);
tt->nextTurnout=firstTurnout;
firstTurnout=tt;
tt->data.id=id;
}
}
turnoutlistHash++;
return tt;
}
}
///////////////////////////////////////////////////////////////////////////////
//
// print debug info about the state of a turnout
// Object method to print debug info about the state of a Turnout object
//
#ifdef EESTOREDEBUG
void Turnout::print(Turnout *tt) {
if (tt->data.tStatus & STATUS_PWM )
DIAG(F("Turnout %d ZeroAngle %d MoveAngle %d Status %d"),tt->data.id, tt->data.inactiveAngle, tt->data.moveAngle,tt->data.tStatus & STATUS_ACTIVE);
else
DIAG(F("Turnout %d Addr %d Subaddr %d Status %d"),tt->data.id, tt->data.address, tt->data.subAddress,tt->data.tStatus & STATUS_ACTIVE);
tt->print(StringFormatter::diagSerial);
}
#endif
///////////////////////////////////////////////////////////////////////////////
Turnout *Turnout::firstTurnout=NULL;
int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists

View File

@ -16,43 +16,103 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Turnout data is stored in a structure whose length depends on the
* type of turnout. There is a common header of 3 bytes, followed by
* 2 bytes for DCC turnout, 5 bytes for servo turnout, 2 bytes for a
* VPIN turnout, or zero bytes for an LCN turnout.
* The variable length allows the limited space in EEPROM to be used effectively.
*/
#ifndef Turnouts_h
#define Turnouts_h
#include <Arduino.h>
#include "DCC.h"
#include "LCN.h"
#include "IODevice.h"
const byte STATUS_ACTIVE=0x80; // Flag as activated
const byte STATUS_PWM=0x40; // Flag as a PWM turnout
const byte STATUS_PWMPIN=0x3F; // PWM pin 0-63
const int LCN_TURNOUT_ADDRESS=-1; // spoof dcc address -1 indicates a LCN turnout
const byte STATUS_ACTIVE=0x80; // Flag as activated in tStatus field
const byte STATUS_TYPE = 0x7f; // Mask for turnout type in tStatus field
// The struct 'header' is used to determine the length of the
// overlaid data so must be at least as long as the anonymous fields it
// is overlaid with.
struct TurnoutData {
int id;
uint8_t tStatus; // has STATUS_ACTIVE, STATUS_PWM, STATUS_PWMPIN
union {uint8_t subAddress; char moveAngle;}; //DCC sub addrerss or PWM difference from inactiveAngle
union {int address; int inactiveAngle;}; // DCC address or PWM servo angle
// Header common to all turnouts
union {
struct {
int id;
uint8_t tStatus;
uint8_t size;
} header;
struct {
int id;
union {
uint8_t tStatus;
struct {
uint8_t active: 1;
uint8_t type: 5;
uint8_t :2;
};
};
uint8_t size; // set to actual total length of used structure
};
};
// Turnout-type-specific structure elements, different length depending
// on turnout type. This allows the data to be packed efficiently
// in the EEPROM.
union {
struct {
// DCC address (Address in bits 15-2, subaddress in bits 1-0
uint16_t address; // CS currently supports linear address 1-2048
// That's DCC accessory address 1-512 and subaddress 0-3.
} dccAccessoryData;
struct {
VPIN vpin;
uint16_t activePosition : 12; // 0-4095
uint16_t inactivePosition : 12; // 0-4095
uint8_t profile;
} servoData;
struct {
} lcnData;
struct {
VPIN vpin;
} vpinData;
};
};
class Turnout {
public:
public:
static Turnout *firstTurnout;
static int turnoutlistHash;
TurnoutData data;
Turnout *nextTurnout;
static bool activate(int n, bool state);
static bool activate(int n, bool state);
static Turnout* get(int);
static bool remove(int);
static bool isActive(int);
static void setActive(int n, bool state);
static void load();
static void store();
static Turnout *create(int id , int address , int subAddress);
static Turnout *create(int id , byte pin , int activeAngle, int inactiveAngle);
static Turnout *createServo(int id , VPIN vpin , uint16_t activeAngle, uint16_t inactiveAngle, uint8_t profile=1, uint8_t initialState=0);
static Turnout *createVpin(int id, VPIN vpin, uint8_t initialState=0);
static Turnout *createDCC(int id, uint16_t address, uint8_t subAddress);
static Turnout *createLCN(int id, uint8_t initialState=0);
static Turnout *create(int id, int params, int16_t p[]);
static Turnout *create(int id);
void activate(bool state);
void setActive(bool state);
bool isActive();
static void printAll(Print *);
void print(Print *stream);
#ifdef EESTOREDEBUG
void print(Turnout *tt);
static void print(Turnout *tt);
#endif
private:
int num; // EEPROM address of tStatus in TurnoutData struct, or zero if not stored.

View File

@ -40,6 +40,7 @@
* WiThrottle.h sets the max locos per client at 10, this is ok to increase but requires just an extra 3 bytes per loco per client.
*/
#include <Arduino.h>
#include "defines.h"
#include "WiThrottle.h"
#include "DCC.h"
#include "DCCWaveform.h"
@ -48,12 +49,13 @@
#include "DIAG.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "RMFT2.h"
#define LOOPLOCOS(THROTTLECHAR, CAB) for (int loco=0;loco<MAX_MY_LOCO;loco++) \
if ((myLocos[loco].throttle==THROTTLECHAR || '*'==THROTTLECHAR) && (CAB<0 || myLocos[loco].cab==CAB))
WiThrottle * WiThrottle::firstThrottle=NULL;
bool WiThrottle::annotateLeftRight=false;
WiThrottle* WiThrottle::getThrottle( int wifiClient) {
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
@ -83,6 +85,8 @@ WiThrottle::WiThrottle( int wificlientid) {
initSent=false; // prevent sending heartbeats before connection completed
heartBeatEnable=false; // until client turns it on
turnoutListHash = -1; // make sure turnout list is sent once
exRailSent=false;
mostRecentCab=0;
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
}
@ -122,6 +126,14 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
StringFormatter::send(stream,F("\n"));
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
}
else if (!exRailSent) {
// Send ExRail routes list if not already sent (but not at same time as turnouts above)
exRailSent=true;
#ifdef RMFT_ACTIVE
RMFT2::emitWithrottleRouteList(stream);
#endif
}
}
while (cmd[0]) {
@ -138,6 +150,14 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
}
#if defined(RMFT_ACTIVE)
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
// exrail routes are RA2Rn , Animations are RA2An
int route=getInt(cmd+5);
uint16_t cab=cmd[4]=='A' ? mostRecentCab : 0;
RMFT2::createNewTask(route, cab);
}
#endif
else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle
int id=getInt(cmd+4);
bool newstate=false;
@ -146,7 +166,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
// If turnout does not exist, create it
int addr = ((id - 1) / 4) + 1;
int subaddr = (id - 1) % 4;
Turnout::create(id,addr,subaddr);
Turnout::createDCC(id,addr,subaddr);
StringFormatter::send(stream, F("HmTurnout %d created\n"),id);
}
switch (cmd[3]) {
@ -170,8 +190,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
if (cmd[1] == 'U') {
StringFormatter::send(stream,F("VN2.0\nHTDCC-EX\nRL0\n"));
StringFormatter::send(stream,F("HtDCC-EX v%S, %S, %S, %S\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
if (annotateLeftRight) StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Left}|{2]\\[Right}|{4\n"));
else StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{4\n"));
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{4\n"));
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
@ -278,6 +297,7 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
{
int witSpeed=getInt(aval+1);
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, witSpeed);
}
@ -311,7 +331,8 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
case 'R':
{
bool forward=aval[1]!='0';
LOOPLOCOS(throttleChar, cab) {
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab), forward);
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, forward);
}
@ -327,6 +348,7 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
case 'I': // Idle, set speed to 0
case 'Q': // Quit, set speed to 0
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, 0, DCC::getThrottleDirection(myLocos[loco].cab));
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, 0);
}

View File

@ -31,7 +31,7 @@ class WiThrottle {
static void loop(RingStream * stream);
void parse(RingStream * stream, byte * cmd);
static WiThrottle* getThrottle( int wifiClient);
static bool annotateLeftRight;
private:
WiThrottle( int wifiClientId);
~WiThrottle();
@ -53,6 +53,8 @@ class WiThrottle {
bool heartBeatEnable;
unsigned long heartBeat;
bool initSent; // valid connection established
bool exRailSent; // valid connection established
uint16_t mostRecentCab;
int turnoutListHash; // used to check for changes to turnout list
bool lastPowerState; // last power state sent to this client
int DCCToWiTSpeed(int DCCSpeed);

View File

@ -85,7 +85,9 @@ void WifiInboundHandler::loop1() {
CommandDistributor::parse(clientId,cmd,outboundRing);
// The commit call will either write the lenbgth bytes
// OR rollback to the mark because the reply is empty or commend generated more than fits the buffer
outboundRing->commit();
if (!outboundRing->commit()) {
DIAG(F("OUTBOUND FULL processing cmd:%s"),cmd);
}
return;
}
}
@ -243,7 +245,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
void WifiInboundHandler::purgeCurrentCIPSEND() {
// A CIPSEND was sent but errored... or the client closed just toss it away
if (Diag::WIFI) DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
pendingCipsend=false;
clientPendingCIPSEND=-1;

View File

@ -113,18 +113,20 @@ The configuration file for DCC-EX Command Station
//
// DEFINE LCD SCREEN USAGE BY THE BASE STATION
//
// Note: This feature requires an I2C enabled LCD screen using a PCF8574 based chipset.
// or one using a Hitachi HD44780.
// OR an I2C Oled screen.
// To enable, uncomment one of the lines below
// Note: This feature requires an I2C enabled LCD screen using a Hitachi HD44780
// controller and a PCF8574 based I2C 'backpack'.
// To enable, uncomment one of the #define lines below
// define LCD_DRIVER for I2C LCD address 0x3f,16 cols, 2 rows
// #define LCD_DRIVER 0x3F,16,2
//OR define OLED_DRIVER width,height in pixels (address auto detected)
// 128x32 or 128x64 I2C SSD1306-based devices are supported.
// Also 132x64 I2C SH1106 devices.
// Also 132x64 I2C SH1106 devices
// #define OLED_DRIVER 128,32
// Define scroll mode as 0, 1 or 2
#define SCROLLMODE 1
/////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,7 +23,10 @@
// WIFI_ON: All prereqs for running with WIFI are met
// Note: WIFI_CHANNEL may not exist in early config.h files so is added here if needed.
#if ENABLE_WIFI && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#define BIG_RAM
#endif
#if ENABLE_WIFI && defined(BIG_RAM)
#define WIFI_ON true
#ifndef WIFI_CHANNEL
#define WIFI_CHANNEL 1
@ -32,7 +35,7 @@
#define WIFI_ON false
#endif
#if ENABLE_ETHERNET && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#if ENABLE_ETHERNET && defined(BIG_RAM)
#define ETHERNET_ON true
#else
#define ETHERNET_ON false
@ -48,3 +51,7 @@
// Currently only devices which can communicate at 115200 are supported.
//
#define WIFI_SERIAL_LINK_SPEED 115200
#if __has_include ( "myAutomation.h") && defined(BIG_RAM)
#define RMFT_ACTIVE
#endif

86
myAutomation.example.h Normal file
View File

@ -0,0 +1,86 @@
/* This is an automation example file.
* The presence of a file calle "myAutomation.h" brings EX-RAIL code into
* the command station.
* The auotomation may have multiple concurrent tasks.
* A task may
* - Act as a ROUTE setup macro for a user to drive over
* - drive a loco through an AUTOMATION
* - automate some cosmetic part of the layout without any loco.
*
* At startup, a single task is created to execute the first
* instruction after ROUTES.
* This task may simply follow a route, or may SCHEDULE
* further tasks (thats is.. send a loco out along a route).
*
* Where the loco id is not known at compile time, a new task
* can be creatd with the command:
* </ SCHEDULE [cab] route>
*
* A ROUTE, AUTOMATION or SEQUENCE are internally identical in ExRail terms
* but are just represented differently to a Withrottle user:
* ROUTE(n) - as Route_n .. to setup a route through a layout
* AUTOMATION(n) as Auto_n .. to send the current loco off along an automated journey
* SEQUENCE(n) is not visible to Withrottle.
*
*/
ROUTES // myAutomation must start with the ROUTES instruction
// This is the default starting route, AKA ROUTE(0)
SETLOCO(3) // set current loco id...
SCHEDULE(1) // send current loco off along route 1
SETLOCO(10) // set current loco id...
SCHEDULE(2) // send current loco off along route 2
ENDROUTE // This just ends the startup thread, leaving 2 others running.
/* ROUTE(1) is a simple shuttle between 2 sensors
* S10 and S11 are sensors pre-defined with the <S> command
* S10 S11
* === START->================
*/
AUTOMATION(1)
DELAY(100) // wait 10 seconds
FON(3) // Set Loco Function 3, Horn on
DELAY(10) // wait 1 second
FOFF(3) // Horn off
FWD(80) // Move forward at speed 80
AT(11) // until we hit sensor id 11
STOP // then stop
DELAY(50) // Wait 5 seconds
FON(2) // ring bell
REV(60) // reverse at speed 60
AT(10) // until we get to S10
STOP // then stop
FOFF(2) // Bell off
FOLLOW(1) // and follow route 1 again
/* AUTOMATION(2) is an automation example for a single loco Y shaped journey
* S1,S2,S3 are sensors, T4 is a turnout
*
* S3 T4 S1
* ===-START->=============================================
* //
* S2 //
* ======================//
*
* Train runs from START to S1, back to S2, again to S1, Back to start.
*/
AUTOMATION(2)
FWD(60) // go forward at DCC speed 60
AT(1) STOP // when we get to sensor 1
DELAY(100) // wait 10 seconds
THROW(4) // throw turnout for route to S2
REV(45) // go backwards at speed 45
AT(2) STOP // until we arrive at sensor 2
DELAY(50) // wait 5 seconds
FWD(50) // go forwards at speed 50
AT(1) STOP // and stop at sensor 1
DELAY(50) // wait 5 seconds
CLOSE(4) // set turnout closed
REV(50) // reverse back to S3
AT(3) STOP
DELAY(200) // wait 20 seconds
FOLLOW(2) // follow route 2... ie repeat the process
ENDROUTES // marks the end of the ROUTES program.

View File

@ -12,9 +12,13 @@
default_envs =
mega2560
uno
mega328
unowifiR2
nano
src_dir = .
[env]
build_flags = -Wall -Wextra
[env:samd21]
platform = atmelsam
@ -27,6 +31,42 @@ lib_deps =
monitor_speed = 115200
monitor_flags = --echo
[env:mega2560-debug]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DDIAG_IO
[env:mega2560-no-HAL]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DIO_NO_HAL
[env:mega2560-I2C-wire]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DI2C_USE_WIRE
[env:mega2560]
platform = atmelavr
board = megaatmega2560
@ -59,7 +99,20 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"g
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
[env:nanoevery]
platform = atmelmegaavr
board = nano_every
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
upload_speed = 19200
build_flags = -DDIAG_IO
[env:uno]
platform = atmelavr
@ -71,3 +124,13 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_flags = --echo
[env:nano]
platform = atmelavr
board = nanoatmega328new
board_upload.maximum_size = 32256
framework = arduino
lib_deps =
${env.lib_deps}
monitor_speed = 115200
monitor_flags = --echo