From 4f0c80a503739d596654f7ff223c3322d68d27c2 Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Fri, 16 Jul 2021 10:24:11 +0200 Subject: [PATCH 001/125] Turnout states according to RCN-123 where Thrown is 0 and Closed is 1. Additional protection against invalid chars in protocol --- WiThrottle.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/WiThrottle.cpp b/WiThrottle.cpp index f3664a8..6c85056 100644 --- a/WiThrottle.cpp +++ b/WiThrottle.cpp @@ -140,7 +140,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { } else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle int id=getInt(cmd+4); - bool newstate=false; + byte newstate=2; // newstate can be 0,1 or 2. 2 is "invalid". Turnout * tt=Turnout::get(id); if (!tt) { // If turnout does not exist, create it @@ -150,12 +150,16 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { StringFormatter::send(stream, F("HmTurnout %d created\n"),id); } switch (cmd[3]) { - case 'T': newstate=true; break; - case 'C': newstate=false; break; - case '2': newstate=!Turnout::isActive(id); + // T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging. + case 'T': newstate=0; break; + case 'C': newstate=1; break; + case '2': newstate=!Turnout::isActive(id); break; + default : /* newstate still invalid */ break; } - Turnout::activate(id,newstate); - StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',id ); + if (newstate != 2) { + Turnout::activate(id,newstate); + StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',id ); + } } break; case 'N': // Heartbeat (2), only send if connection completed by 'HU' message From 5eff4c5ee546d8519265b05bb151ccaa48cbc924 Mon Sep 17 00:00:00 2001 From: FrightRisk <37218136+FrightRisk@users.noreply.github.com> Date: Tue, 3 Aug 2021 17:12:25 -0400 Subject: [PATCH 002/125] Squash all commits on RMFT branch to create EX-RAIL branch --- .gitignore | 6 + CommandStation-EX.ino | 25 +- DCC.cpp | 4 + DCCEX.h | 9 +- DCCEXParser.cpp | 36 +- EEStore.cpp | 1 + EEStore.h | 2 +- I2CManager.cpp | 190 ++++-- I2CManager.h | 222 +++++- I2CManager_AVR.h | 198 ++++++ I2CManager_Mega4809.h | 160 +++++ I2CManager_NonBlocking.h | 215 ++++++ I2CManager_Wire.h | 128 ++++ IODevice.cpp | 446 ++++++++++++ IODevice.h | 351 ++++++++++ IO_DCCAccessory.cpp | 63 ++ IO_ExampleSerial.cpp | 121 ++++ IO_ExampleSerial.h | 43 ++ IO_GPIOBase.h | 221 ++++++ IO_MCP23008.h | 96 +++ IO_MCP23017.h | 108 +++ IO_PCA9685.cpp | 258 +++++++ IO_PCF8574.h | 81 +++ LCDDisplay.cpp | 4 + LCDDisplay.h | 10 +- LCD_LCD.h | 33 - LCD_NONE.h | 27 - LCD_OLED.h | 73 -- LCN.cpp | 9 +- LiquidCrystal_I2C.cpp | 12 +- LiquidCrystal_I2C.h | 8 +- Outputs.cpp | 98 ++- Outputs.h | 34 +- PWMServoDriver.cpp | 109 --- PWMServoDriver.h | 39 -- RMFT.h | 23 + RMFT2.cpp | 646 ++++++++++++++++++ RMFT2.h | 111 +++ RMFTMacros.h | 223 ++++++ .../RMFT Reference.docx | Bin 0 -> 27380 bytes Release - Architecture Doc/RMFT.docx | Bin 0 -> 80441 bytes SSD1306Ascii.cpp | 11 +- SSD1306Ascii.h | 9 +- Sensors.cpp | 239 +++++-- Sensors.h | 77 ++- Turnouts.cpp | 310 +++++++-- Turnouts.h | 86 ++- WiThrottle.cpp | 32 +- WiThrottle.h | 4 +- WifiInboundHandler.cpp | 6 +- config.example.h | 12 +- defines.h | 11 +- myAutomation.example.h | 86 +++ platformio.ini | 65 +- 54 files changed, 4777 insertions(+), 614 deletions(-) create mode 100644 I2CManager_AVR.h create mode 100644 I2CManager_Mega4809.h create mode 100644 I2CManager_NonBlocking.h create mode 100644 I2CManager_Wire.h create mode 100644 IODevice.cpp create mode 100644 IODevice.h create mode 100644 IO_DCCAccessory.cpp create mode 100644 IO_ExampleSerial.cpp create mode 100644 IO_ExampleSerial.h create mode 100644 IO_GPIOBase.h create mode 100644 IO_MCP23008.h create mode 100644 IO_MCP23017.h create mode 100644 IO_PCA9685.cpp create mode 100644 IO_PCF8574.h delete mode 100644 LCD_LCD.h delete mode 100644 LCD_NONE.h delete mode 100644 LCD_OLED.h delete mode 100644 PWMServoDriver.cpp delete mode 100644 PWMServoDriver.h create mode 100644 RMFT.h create mode 100644 RMFT2.cpp create mode 100644 RMFT2.h create mode 100644 RMFTMacros.h create mode 100644 Release - Architecture Doc/RMFT Reference.docx create mode 100644 Release - Architecture Doc/RMFT.docx create mode 100644 myAutomation.example.h diff --git a/.gitignore b/.gitignore index d768dbf..b0b8666 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ Release/* config.h .vscode/extensions.json mySetup.h +mySetup.cpp +myAutomation.h +myFilter.cpp +myAutomation.h +myFilter.cpp +myLayout.h diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index c8a3aed..cd6db16 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -44,7 +44,6 @@ * along with CommandStation. If not, see . */ - #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 diff --git a/DCC.cpp b/DCC.cpp index 226425b..dec6646 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -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("\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(); diff --git a/DCCEX.h b/DCCEX.h index 1504490..cf6eb66 100644 --- a/DCCEX.h +++ b/DCCEX.h @@ -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 diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 2c3885a..9401796 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -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: // - 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("\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("\n"), tt->data.id, tt->data.pin, tt->data.iFlag, tt->data.oStatus); + StringFormatter::send(stream, F("\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("\n"), tt->data.id, tt->data.address, - tt->data.subAddress, (tt->data.tStatus & STATUS_ACTIVE)!=0); + tt->print(stream); } return gotOne; // will 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("\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0); + StringFormatter::send(stream, F("\n"), p[0], tt->data.active); } return true; - case 3: // 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("\n")); return true; - - default: - return false; // will } } @@ -713,13 +707,13 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[]) return true; case 0: // list sensor definitions - if (Sensor::firstSensor == NULL) - return false; - for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor) - { - StringFormatter::send(stream, F("\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("\n"), tt->data.snum, tt->data.pin, tt->data.pullUp); + } + return true; default: // invalid number of arguments break; diff --git a/EEStore.cpp b/EEStore.cpp index d1a301e..cf2531c 100644 --- a/EEStore.cpp +++ b/EEStore.cpp @@ -72,6 +72,7 @@ void EEStore::store(){ Sensor::store(); Output::store(); EEPROM.put(0,eeStore->data); + DIAG(F("EEPROM used: %d bytes"), EEStore::pointer()); } /////////////////////////////////////////////////////////////////////////////// diff --git a/EEStore.h b/EEStore.h index 77136ae..247e30a 100644 --- a/EEStore.h +++ b/EEStore.h @@ -29,7 +29,7 @@ extern ExternalEEPROM EEPROM; #include #endif -#define EESTORE_ID "DCC++" +#define EESTORE_ID "DCC++0" struct EEStoreData{ char id[sizeof(EESTORE_ID)]; diff --git a/I2CManager.cpp b/I2CManager.cpp index 27e85f1..82f5f46 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -18,14 +18,40 @@ */ #include -#include #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(); \ No newline at end of file diff --git a/I2CManager.h b/I2CManager.h index d93e7ed..b17accd 100644 --- a/I2CManager.h +++ b/I2CManager.h @@ -17,13 +17,16 @@ * along with CommandStation. If not, see . */ -#ifndef I2CManager_h -#define I2CManager_h +#ifndef I2CMANAGER_H +#define I2CMANAGER_H +#include #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 \ No newline at end of file +#endif diff --git a/I2CManager_AVR.h b/I2CManager_AVR.h new file mode 100644 index 0000000..9de9bf2 --- /dev/null +++ b/I2CManager_AVR.h @@ -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 . + */ + +#ifndef I2CMANAGER_AVR_H +#define I2CMANAGER_AVR_H + +#include +#include "I2CManager.h" + +#include +#include + +/**************************************************************************** + 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<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<writeBuffer + (txCount++)); + else + TWDR = currentRequest->writeBuffer[txCount++]; + bytesToSend--; + TWCR = (1< 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< 0) { + currentRequest->readBuffer[rxCount++] = TWDR; + bytesToReceive--; + } + TWCR = (1<i2cAddress << 1) | 1; // SLA+R + else + TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W + TWCR = (1<. + */ + +#ifndef I2CMANAGER_MEGA4809_H +#define I2CMANAGER_MEGA4809_H + +#include +#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 \ No newline at end of file diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h new file mode 100644 index 0000000..920cecd --- /dev/null +++ b/I2CManager_NonBlocking.h @@ -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 . + */ + +#ifndef I2CMANAGER_NONBLOCKING_H +#define I2CMANAGER_NONBLOCKING_H + +#include +#include "I2CManager.h" +#if defined(I2C_USE_INTERRUPTS) +#include +#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 \ No newline at end of file diff --git a/I2CManager_Wire.h b/I2CManager_Wire.h new file mode 100644 index 0000000..fb41f86 --- /dev/null +++ b/I2CManager_Wire.h @@ -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 . + */ + +#ifndef I2CMANAGER_WIRE_H +#define I2CMANAGER_WIRE_H + +#include +#include +#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 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 \ No newline at end of file diff --git a/IODevice.cpp b/IODevice.cpp new file mode 100644 index 0000000..0f833e8 --- /dev/null +++ b/IODevice.cpp @@ -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 . + */ + + +#include +#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= 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 diff --git a/IODevice.h b/IODevice.h new file mode 100644 index 0000000..38453ce --- /dev/null +++ b/IODevice.h @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp new file mode 100644 index 0000000..fefebb5 --- /dev/null +++ b/IO_DCCAccessory.cpp @@ -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 . + */ + +#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); +} + diff --git a/IO_ExampleSerial.cpp b/IO_ExampleSerial.cpp new file mode 100644 index 0000000..8ee8f13 --- /dev/null +++ b/IO_ExampleSerial.cpp @@ -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 . + */ + +#include +#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); +} + diff --git a/IO_ExampleSerial.h b/IO_ExampleSerial.h new file mode 100644 index 0000000..1273a95 --- /dev/null +++ b/IO_ExampleSerial.h @@ -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 . + */ + +#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 \ No newline at end of file diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h new file mode 100644 index 0000000..366d0fc --- /dev/null +++ b/IO_GPIOBase.h @@ -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 . + */ + +#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 for 8 pins, GPIOBase for 16 pins etc. +// A module with up to 64 pins can be handled in this way (uint64_t). + +template +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 +GPIOBase::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 +void GPIOBase::_begin() {} + +// Configuration parameters for inputs: +// params[0]: enable pullup +// params[1]: invert input (optional) +template +bool GPIOBase::_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 +void GPIOBase::_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 +void GPIOBase::_display() { + DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d"), _deviceName, _I2CAddress, + _firstVpin, _firstVpin+_nPins-1); +} + +template +void GPIOBase::_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 +int GPIOBase::_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 \ No newline at end of file diff --git a/IO_MCP23008.h b/IO_MCP23008.h new file mode 100644 index 0000000..c04712a --- /dev/null +++ b/IO_MCP23008.h @@ -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 . + */ + +#ifndef IO_MCP23008_H +#define IO_MCP23008_H + +#include "IO_GPIOBase.h" + +class MCP23008 : public GPIOBase { +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((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 \ No newline at end of file diff --git a/IO_MCP23017.h b/IO_MCP23017.h new file mode 100644 index 0000000..2c56ea7 --- /dev/null +++ b/IO_MCP23017.h @@ -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 . + */ + +#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 { +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((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 \ No newline at end of file diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp new file mode 100644 index 0000000..8ddf409 --- /dev/null +++ b/IO_PCA9685.cpp @@ -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 . + */ + +#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}; diff --git a/IO_PCF8574.h b/IO_PCF8574.h new file mode 100644 index 0000000..be9ead7 --- /dev/null +++ b/IO_PCF8574.h @@ -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 . + */ + +#ifndef IO_PCF8574_H +#define IO_PCF8574_H + +#include "IO_GPIOBase.h" + +class PCF8574 : public GPIOBase { +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((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 \ No newline at end of file diff --git a/LCDDisplay.cpp b/LCDDisplay.cpp index cd5b10b..c6609ce 100644 --- a/LCDDisplay.cpp +++ b/LCDDisplay.cpp @@ -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) { diff --git a/LCDDisplay.h b/LCDDisplay.h index 4454532..2791083 100644 --- a/LCDDisplay.h +++ b/LCDDisplay.h @@ -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; diff --git a/LCD_LCD.h b/LCD_LCD.h deleted file mode 100644 index 2f30c97..0000000 --- a/LCD_LCD.h +++ /dev/null @@ -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 . - */ - #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(); } diff --git a/LCD_NONE.h b/LCD_NONE.h deleted file mode 100644 index a76ade4..0000000 --- a/LCD_NONE.h +++ /dev/null @@ -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 . - */ - -// 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(){} - diff --git a/LCD_OLED.h b/LCD_OLED.h deleted file mode 100644 index 811b9be..0000000 --- a/LCD_OLED.h +++ /dev/null @@ -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 . - */ - -// 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() {} diff --git a/LCN.cpp b/LCN.cpp index 6cdf1c1..df1b4ea 100644 --- a/LCN.cpp +++ b/LCN.cpp @@ -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 diff --git a/LiquidCrystal_I2C.cpp b/LiquidCrystal_I2C.cpp index 1697d70..e6b3cdb 100644 --- a/LiquidCrystal_I2C.cpp +++ b/LiquidCrystal_I2C.cpp @@ -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); } \ No newline at end of file diff --git a/LiquidCrystal_I2C.h b/LiquidCrystal_I2C.h index 0eb9eae..6d65541 100644 --- a/LiquidCrystal_I2C.h +++ b/LiquidCrystal_I2C.h @@ -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 diff --git a/Outputs.cpp b/Outputs.cpp index 52cafb8..5910172 100644 --- a/Outputs.cpp +++ b/Outputs.cpp @@ -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 "" -// 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("\n"), tt->data.id, tt->data.oStatus); + StringFormatter::send(stream, F("\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;idata.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;idata.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;idata.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); - } /////////////////////////////////////////////////////////////////////////////// diff --git a/Outputs.h b/Outputs.h index 6132102..58be7e9 100644 --- a/Outputs.h +++ b/Outputs.h @@ -20,37 +20,43 @@ #define Outputs_h #include +#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 diff --git a/PWMServoDriver.cpp b/PWMServoDriver.cpp deleted file mode 100644 index cbf8432..0000000 --- a/PWMServoDriver.cpp +++ /dev/null @@ -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 . - */ -/*! - * @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 -#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<> 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); -} diff --git a/PWMServoDriver.h b/PWMServoDriver.h deleted file mode 100644 index aa8dab2..0000000 --- a/PWMServoDriver.h +++ /dev/null @@ -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 . - */ -/*! - * @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 diff --git a/RMFT.h b/RMFT.h new file mode 100644 index 0000000..98ef2cb --- /dev/null +++ b/RMFT.h @@ -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 diff --git a/RMFT2.cpp b/RMFT2.cpp new file mode 100644 index 0000000..3b615be --- /dev/null +++ b/RMFT2.cpp @@ -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 . + */ +#include +#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; // +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 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) { // + diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1); + opcode=0; + } + break; + + case 't': // THROTTLE + // 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("")); + } +} + +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\n")); + return true; + } + switch (p[0]) { + case HASH_KEYWORD_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: // + 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: // + 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; + } + } diff --git a/RMFT2.h b/RMFT2.h new file mode 100644 index 0000000..f253804 --- /dev/null +++ b/RMFT2.h @@ -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 . + */ +#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 diff --git a/RMFTMacros.h b/RMFTMacros.h new file mode 100644 index 0000000..420ff35 --- /dev/null +++ b/RMFTMacros.h @@ -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 . + */ +#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 diff --git a/Release - Architecture Doc/RMFT Reference.docx b/Release - Architecture Doc/RMFT Reference.docx new file mode 100644 index 0000000000000000000000000000000000000000..21ad7527ae9d733360d24db18592bb01ae015f08 GIT binary patch literal 27380 zcmeFXQ)=2@s6bF4IwJOVE@pNv2CAM8X3l!_9st`v zg^0>BF|}3A_^hy!8YGH8l}`B_63ZJz{W}q$|96)u zpuEyw5}Z9DF~j>dV=c4a62Ph@96qrg`7%jBW*Cw*F+=^q=1D_96%nB`-rS@(4=>7% zv$F$7j*FO(zcCUyqw=5-Eye^nXmZZ0S-nJ$m3zlFm414VHS53q>V+uFnblWHY=+2< z*CjZO$(^vu=4U6_x0KAw;mcK7IAQwTF^2A@h$=WLKz!4!9!OE$RFd*{ED~QWVgjI$ zO+$EFzb`3Vi-)*&&VT0)K^740DS5kPh^f>5C&R_qNKq@dg{$xULL1uhd3_p^;*U5l zMbTu7aa*JJPZ6G*Q7Mw5r)T_12P$!COsI!v;7BVEfwFKv8<5*f4irtgpMK0inkR*1 zOgk$7vV&*ze;nMu@IP4>$TG2Urbgiz?SBuiqWm(s`raA40D*i#O62yv+z|c8@6A6( zBNfOSKiv}L`34FE^!p16r1<~oRDuMY)>|MTAbAiVAXp$M5CbPOTW1FP|ET}lq5qrz zNOfYr^&lgh;BC-%(0sSjMn6`OJcG&HCe}J6l#Y}v>gJmD>d!at+8T)7xsk-=!eY{# zmvgqb>t3?n9Zsqmd}KHD%Deuc&S#egP)bNw36sZ)!vS2@$&2~-1c?;YL{O9#dgLq) zM8aEm`g9-aF}3K+aT%-?G1csn329SNuA|!OYq}pxeg@O>inXMUH)JKBSWhT^#|*X~ zjJZHV8Vg-~+`vww-jM1=QX6xOHPtgMjx8etS$2YPH4M&O*ZhT7QE%CBL|Fli9lO%gu|Je_^FxgP?c?kl0Cm}PrC9IWhSiT`nQ_~M-{eaYL!z~eXo*M_ z4Jp2Iix2xo^pw+o4ZCI-uOOyMAZyl6wfrCpB9J*auD@8)I8`r0_`MsoUyOK;KN8EF z-XYvabulGtQev04aKDY~(Uq{zA*rG~T<6R+Z94j%S5e0Mz8@6$86+gwjcJ-U&YY5l zm7I!nk9(Z|Ec$HB?&aa56Mr3NrI#-~r<{$vYT?B;TU@+)VaF#ci__xgCxasS2IN{4 z6;tkc3h{b}M-^pmoo(7Jjd*@RanJ-_xwsIXnmqu-Kn1d(1Y8wzT!l>$h37;9SEc4H zl2FPj)og;-^n>%&Y?9ynlG$D4uOuR>RT^K5L|3V}K9#hC7G3c7)_F7f$2V#7M##RV zZsp4ye_WaV!Umr@X6zIv{qq6Ilw>tMaCvc!!|2zOfl=Z1V+a3q$&5*Y#^+FNe3gju zd8zIBy?*dpesW*_ewJ{S=#lxY(iza^Jj=~H83*%7n2!8(>B-(-^sVS0{|p@)Y?0@c zrI>b&bM88HdC&ELp;51ex>@+j{9TkXU4ESMmL>kdwv?Q2t|k+Ygf8JnSSNMbr;_QQArRQ@^WG|&0lQH@_N zKjfC+VEnVlAW?H&_wDRykWfBt^-SP6Y*JyqnOpn9c{#&PhEzg-&+=dc$6I6S5c$qH zmqIL2fK-C8y7%|Fmo7PuqW?( z;*!&4m<#7nzP3fIP{@cx3CDA!<)2_TisPB4#Dj!?vWbk&x$srNbZNNt%nL_BQOT`Y81@NGoF8 zZBIB=N#2^@4@r-Ppr1ytNtIb;MAtAk&FMrr*x~EsJ|BM#;8;i*O*bxd#1Wz}J~s_- zce|eZHbzcS-=fN7(4CWx)1ZLk$wWm)=c}_f9Yjh$0HHpX(HF8eDYQw7(mKJvB*cp* z%zBybhS^y9AuFKn1+vBDRc8%pqxFF)hR$7OhOy1cnzwkorKi3|Ru$R0bt)Q*KZ=bP zOAK$Ae9Q2O^NBd3OPvtiQ8i3MQ*}aAN%TpTBT||ewGpAQz4~}FG*WuVkf;p zd$h-t+ra9m2Hx{r^EMco7K9XeKtN@m1<;{{{1z+z*=--!^3xQdeXg+6a(Z|@7 zaM5TiiXnS)Qc-F~G1 zn(9e?7o12w(B;lh=&>2j-RTlD?MxIMy|81aoQr_Sffo@zbqtK77~pX?Ttf@>an{v; zc2n@~P_V=clJP=_3xG~HPnwFzO3#YcWdlQN4ErWyXV$irQX4(o3#j7jK=Gy9n zjhswj_P5CYa_Lxt*%l2HUpG47rgW;xsm1GQ7u!T=vZ6GU9}`l>ueTA@7=J!;x*@H< zbQR+PN)%bVU_+#V?2C}ObQ^2pzfK*UvSt!b$DkS0ejtzj0uOvw$S&0ZI41p>?K_Xw znjr}znIZ0>Yj4#17tFW9a^`L!1KW(|#Cr99SB3rtGb!!{|J-D-e{ifhrK?ck7cQ;E z@_Wa{+$k7+E zE}EW$2#Uo?7x-cWb0$lI@WP+fN_RHzGx4}=w zUd4<_s*N^}{MekOqbp;HFogxwfSZ1{*=fyhrvGv!2)gWVkV@Q~>u}726@m(-e)9Um z(=Gu-Lqm2dWL;HP`X;HQ&McTNg#C47KHtxm~^ULA(_X zMGFU*^-c2++I=R?n@^h__{KrA1a=FD(fD_2nlTs>!d|0#n*>R=%+W6Ey1Fd!oJ}|o z+!1bAV9~e7Xe7VaKEOUi`#>oYpnLB~n4X+(^~(;$Umxw0=K;ZAekmav8mb`;kkZ&$ zLj-flO*uBD;-RtE&APs~OrA>Vue@U%7uUMxuYgU)eW;y9c`gVleT}}TPG>jCL^iZah+x22c2!Vw7*jZFFxmfzR z@@-EH%8!(!-+oq1aA`Zew<|w0ta#*t$bo+Y0q0u&c#i?k>=IAy>gaj%S%|9!z9n_w zs|`d9Iur&o8Y0oyp6?GC+z!Vlw~_D9@BDbM!ZLIqrMzz=VzJ6E3WuTR136@-!k8Ge zAvr7(4?$sb(5k>-8^svQHO}qByjGY+&mQ3u5??Dog(>6buBW``Jqp$xziaUtSZZkK z?mgENI=R^B{BdH721j+6MIPECf+=|-yQCN8Eh9A2o}#!{DPWWCNuRC+vdA4Wp~+FZ z46Q&eT*rjSen9%jhr8`JGL#6er{>@FVlRNg8B*^tXJDxv5x-9<2@%24|jm_d5 z?9I?n4@-gC%^2ymFTU@7D~WuA3(_yHF9^^LTE#aF6`qpasI z_JoFP7I|Cl0Pgd-mYPS|71IQ(L!qzdyWrtu7LLTadJb<8K?k@{g$0G-LnE3jz6i!W0Hqs{w?n(|H(4BEv}Z7e6LqR=f`-7a4IBe`wbpi%5}o}4zorZU z=SC*|euXi%16LF(q4`c>?{30Zz^990*6*hz4>+tsj!L4Ac`q0?i)YBk4ePBoEcr~p zw6E^yS&^B5E7yhCwv8CeoFj2VVcWj1zwSr7F6D)J35#geeLy4P62@RdS$Z(FugAgh zU+?Hr;~NC7?Zsd{N>Y?)dm+)fnsg1~OIn8VdM85bM>$3x9J@;B)f~(E6$+XoK9C-p zd@y_TSn0^JMQTi^(pY6WOWbW@CeLJQ4HKmUF$R(eJ5m-)7R0tgRS>ud4g8w5$ZNEI z7F$+{#$g|a%!BC{_QrA0;7jvMA$`>qXmekiOKxNmyse&zA*JsO@#4udmOIyNCLSc) zk9QT{YeZ}gnnK8g^Tdf1K~?P8R$GQVjcSHrk*&JKliG{maV5sz-^<@zO=f~^1`>K} zPfO-;RUD6`qSBf3YTg_+6+SUXD<##FBj4&a^I@@sm<3I-hkuZjn5YZN33#XHph&z8 zwM*2F-&f>hwfyVGA20skZ+UX+OOVUac`9*gbJC=T+gnjU<36rB(lAz<%({=JJ zfqyspLU)U(!75R))7Ik}W|SM5oBg*PEHUpKN0FCELV-;s3gAN6$MrFM^w<9gOE_Xa za#T*veU2x7&P^iwR5S_DxL8444m-dCWeBiQ*~&r0Ylr_W8ZE_MN4*@{$liYct@?nE zn6CQZmte2-9WUu}1v6GYwf|A9S$%yig;P4}#LE&QJpcodcX^&}JhutI&RvgQLrlXo zs6AeevT>IWkt1baMC%%}sIA+d(+I1iBn+>A;aw*s>&X0zDawefgcq5Eq4Oyoe7zRl z+0)S22gpMyPJ8|o8)`Vw?*OE9FqC(S5})HBQw8`4GhV`+4sy3>acVe7sqx#FY{+(3 z{?HbUv1#lSwFW=xGY;#G50rCp)0URgZ6R@uG$C8ysr2IB3LvTIyU;Pj(Xa`CmD}2) z3M~0r22H%0?%@R7fqK8_RI}^SnwxQ3O<^e>O>`-*$NmvcB(|bq!J{EPKG3>Xrsj9-lW1U@CXYJ@P*+F;Ic$xMRY9M zu0OHyyUp5)jFmM#RuL&3yrIEb+ga_4b`0FqYU8bSqDyZu#aeNzxH&R74Vka)E4@*3 zLj(Z9`fkRPtf7w8IL&^fkfiN+dfcC5wXF&so8tV%MPSDR&sWRHh(7tujr4k9(Cx#6 zxaG;xHshA7&MNki?Mn~138RZB8W8LYc`49_%6ST8n2GPpeWmt_HBOWJ zGY;WVJ<0d&gDZvy&xSNa+JyxwdP+(bEze{Y%!{Gn@6wwW0T01p(jS}qK}86U+pbi> zJ3ptZf6BVTX+76&)VX+X!ZL>s@=k>(;4vaGlTrX7sIavg4Tmtp* zh(xxVU(QRUL1q(JY+sIq64V5aYlIrA01P9q?qMhX+upzq$17;u;C z7#%34>q>i8FF*cC(3iHQ7$`kO_0q?}-uy%WlTxL)oqjwbn#LHWZgSo-bV__`q;`X`>h%Jzs5O19Lx0pHI}Vofu7|Se z9T&EDHLE8#uFFh8C%SOR6;CH~ax3UhoW~fsaNX)ZbRbRMHDtZzmv5nq+;@ZrKZp!h zKi!mWPF&(MTu1qe4BmWWuUz}3Pfzt(b}at!#@w(8kTy^v_DgWAsponij$1WRlo(S% z*WqS%MktJeDh*|C&=DDLmWvk# z@bJKfCZ8>$(r|p3lC#U+U1MDL*h-aujRnaldEy1|HF82<6R?m}#$f^@qYv&0qh03? zu&_rE`j5z$niSyq!~9n=xK~XS|4WhUy z{%9`6J7q?S2w1Y_P_TvF!reE}zf)k}&G^>B!)3#ZxFjWRh|k}9{t9OBDj2Q}8&z~r zVDGGgRoM9YI1$JQ4P*ePDeGN@5j6`+Fy-BcTFYtg#N=gfutgek8-FL~Uq`%#KQ3b% zru;>lX?wYHT=}qK1I?pw1#p9{S_DDrzGC0^DW}1M~w8bw4L3cjaYMkavx3 zNMeWF5*TlHYTrku*}X~5>8h_4Khm^oXuOa*l6DV683ESmKUqPuC%8)epun$M%b=c) z$wczRQ`2IEQ&(f7ZMcOV&`t8*K-3C+YCMHi7YM1;Kn5w$=PtkZpH#b95v@+6=*e=% z=vh_Lq#c^roaN4Hj>2{K6?zGi+$K27`D6Cc9ZB9%vi(K=8QjKLV6YkvcgisqEDpC& zEuUg{$HTL$Y5I3%yyxmuLzM1REX3&pEg6IdIfLXm*6XtYjR^H2LVtdjapXo* zYfZ!GmLIP>BYHry%tST@Mu`$g<}Qm5KQ!seG8<$^?}-Vp3%+jE@pk*R0rlgIRD0hANBO zp7cm-qdJ1SMrc(=5vlo5pWT@AA~B~e!2Ukm6SsLdwm}Iz_!8D=y~>8hS_D5K22Q?K zl2`C4r0l`;U?{rsF(X|7i=jVLoBiDrJR3%DUOI!9d%TSK3-BXTm(2D zr@!n-^M+U#OJ}y zEB|WU5Oa0bI2Wo0+ZM6|wX9O)2|6t6;5~8z*oCZ#Ueg0H^D(0T*{blsy4cE!F;fR_ zkl|Or7gzd?kGH7X-i$TsU4&>(d@!ifRk8b4a>$gs(V0*WM<`*AUftMJzr7~YRZA7N z6pYB}EbnNzYTC;Kav#JAGwCB6=_yp)yc=$#MPqM2!XH-C6KEYMTUXuU!WgBjmh@;j zHD#+bK;bYQF?f-P7nDo3S-`qRxDmb^7wLgSk0*qyi{@IB-fE>;!Pu`FR2!fUIB>r&;^_&>$I6xpvI(ho$~)9!pr8s#p4YBq z$v`bM)3zE*aTQHAQdjH<6zrZW9hL;HJ~lM^c<97}C|er#&Or3`rH-)~6pTa^>6fn` zTc+6y?45~M!&$+5vmU`=^FtgE6BjfHylH)FOGHu|l~!S%JBdu|+?==`M~x8N5SRQk zZU3_9+p70g!_$)9eyG|X2=GPGK8Kcm;Mt~6S77T$y|LI z-UB-^k+UdPmTB=$&dT!hbEMnV`f!E4DABpbgpN(hvMtc(GS5-FHI1SO(eKjJpHF_b z)h{T?S&Vv&``eOeZ&hGG*m`BvnZMAuKK_?>Aq`Eg`_g76nmG`Dj%Vr)mi+(o-Uv95uSpJ%Wnalaw6GX*8OP&3dfuJDe=n8fU> z7P~gW0x%h7Ph@?5EUN#!aVU~cCx-9toQVLIM~Rt{>qM_Pm zvJ%v7eakWu_qNb4+nZu_cW+WaVfwGdZ_lW?e;)|ZJLpFM(G~9Y__S$|nDm*Tz2&Bm z9%xyzfXjj3fa0As=e%RIrgC^s?1iPKVicT-#s>iu_R1L-{A1e*Kp?%(GoX2X293dB zz_fhQLQ2dE`=e-ywz@9fy)UeuqFw37<{I)0^Cf3oK^s^3b2pA?RLIY+rf^FC%*6S$ zT?<77II$@LET?q1;&v?XnMgK_x>uS$ogzr;;OFe>eOs_WV0B8;jC-fJUCnhE-hJJu zRNFzAXqI@dZuTv7emXrrlG9Auc6{(j+38r^8WE5I*>&O^Zqm}|2;5?n{6tia#xzO+ zOI#wQc8C)aSdggYYR=1Bj9aKdh;(<0?7D2YDE;joA5r*#-D*_4Mg6|}3-mwfo~E8C zd~jeOpcYUdAUq%_;Q!P;|K&vgyY%@#eiWE89|-vWc1u{DG;KY|h!lJq(i61iO~(Te zflhFy3eeSg1vd5m7I`))mecO_6_Z8@gIElTWihts30d}ftU>3=f9z%hX(R;?Ep2qQ z*9ImWe%S|4r?9fFs1^by@mf22b-(0LO&o~7{pfFsH}3L}wbXRHsX}Oeiol->P8;E- zqmo>G5K_-;6zh>(By(Sc8>?epuf^DNdnTzj#=8?(Z?XD-F#t5LTAqo6ze=L9KN6{Q zU`X|iO9^A#Z9+wrL$Ok4gWx^u`eHQgT)9Tj+xif3>Nq1!rX!)5--9?L_nNbh-r*$` zG=#!vRYH%hX9jshLf?lRf954+r&A#LJ#Rh`{*Twl!(&3V7#s-55*Y{x{r^})fSIY4 z5rY-L$ij??-V9(qrJ-l5s)qD)vkmB^qqDsB1{X=l2zvZ}_JKSzL5TTf3r@u5l(&D%G-nE8lZTPW**!|jDc z)S4jq_S{}>79#)$u@@-ABH{L_42;M3vFmA@Z;~k**W^X7mYT4Mi{nE;AB#q#>tzdV z27v6FC0!hXIXv?I5l z0;910$0ia`HB+2-)tLqLqoU1iyyg`n{7+G@{QX#N<)^?(nYY! zoGG#PD;%&*i1yAj?=8EbQYXH;f?nQ=jOo%6phpKfEJKj=JS@S%okG{6m=1iY4H-!x zQt|Ya>C+A>BCQZ}HhUgf%h$3R9WT+1lDn%*>vUR2B$(M@H9Gry$YS)*tm_$c_pKNF z05>#JAjGpZ^UWWsq)c-2>*9Vb^XF#a1h$ zXn*Qlr)WiFyl1eVhbIXI@tQAhv8VgWoj&=fDbk`r-|3ayw$JVT2NJ0*CBcL`?7=*- zOXbdL286n-F-ys_L~3bM|EuWnez6)o`=kFV9#Kg|^$uIY?qDi71t+h5>{N%$9-H}+ zEN671->2Ts1}@{H+dbG-Q8O6~UJa6uHd6~kF_#TN07lOX2OCnC+mxt$b*VXxld)?p zu;8-Tb|urI0;}*9vl%3JW2&aTtAl^T+;aev{>u=bZ@7(MY z$M=uBPmKxvYMq1S4qI-2{$6MLeU6o{6*q2%HO-@MaCINt?TjzS&R_H#SWAXykNv#q zWhz>pUYZ%#T;2sj`=4R~eh<&F+F#ZsbSZe%^Z|kj6?%f3IGY}-MxItZ96~D9LmIOw zgz8w|qvdJo0^O&@)#P1T=B3jv$Dm7&@Tu@K`mPP!I4JhWnYjhjW&_yUGKS?X8DYZj z9u9qS6C>Yp=XHB3~aGLG5LboAt;#q*=70 z8qqCi(5cY;f8O$&>`ZZR!}GYfd$UF$V&@_32_855xR{>YMtnZHZ*5BdviI=dUp7wL zFCGJJj5IxsB5u#7{9v3j^dajs1)lF-$?nBrK3QN**eyEkR$sxsa$x6_3*gA%;;-xe+19TP z-ss?9;|wSG&`$$KVB#+=noEg$+j`_MT8;UE;gF6B&ORBe~#su zkACW-?@Vhs*RsyOneO}~&0A_&F_(j+c(C+IQHt0Vp;s$@jmUk6$7jMGdw=LGE{f%^ z>|y?SqjV9)Z!}i!#aa zlyaGTYd>853<$CN7ylZ2x6&Qsy1eZT5o*kH!I{e0T6q)HS}#BAo&$9ZDcogqC_92= z1bj16VBxmpXcuS_kZ-u|I#i7)CVRTUSaWP41B#Vb($uIKEKhh@6$Ti4faNo;cP@(y7W}oCX zKolP@(wCaGr$uRRxP(e5wcEIV{C$&62V~d>^Vw)1NAIC~`hgeZvM+c#@QZ*k?~w z4H-msLK3~yzrpn5H@d-b=SX&cfrML`t!1bCyT(b&V1TE!1m&nZvDwnn!o60b{<`oB zW!i(^ejB!qhm{v7-#IGSeLb}$;o-ZFSyU&sqGa5X>5Xdlc^Sf*UvS()?FtPSlG;|d z&jl4`C;&}7#tg;@=j95Hfab+=LS3>wZ_40lc8vv;>`JXyv#ly|iQd*NKx5mp{7#!) zd^k*INbt6lR@h4g1O=xW!DY2CX6W4~tNrWtWLJgX0{XS(l;Dd(iTQ!cGndYSEYUdC z##i51J+je$qR0imtU&yja$3Nncpb$SU0!s4jOK{&?7kPxT|cZb@yvBskp8t`W$?)P;MU zX`>Trjn9MHkcT2SzQ&zdbcsE^_1gL&EPNFbSloU3+`Vs}Y1i%DDDqtOfcOM|HCo_Y z{8(^dXI7$M;U|{2Ol*5R9Qo3iZ}xvtj)VTSt&ulAUYo&M^?Y*pYkMy7-7s`n^C%bb zVI>ufM%JLSLxqX(Jj%L@vEC2Bln9Gjdeg>sMy;|S^n4DHQVsX}$b8Td3;pQAYHj;5 zPQb?47Sr%DISz~|e7vgjF5HE=EvsAFiV`OLF5qC785uE+J_LmLL|waF?|YY9b#auA za&I?;V5~AdLI*TVrIUl%1b7NeYXcrC6i$m^%Dn}5^a5MKR{u2BUOIh^qhI*w7-1iuCr-@n1w9irLO zU1=>rPgOlFFE|cu8vnA7Kr0 z;LKXD-Kr$<;fy_0RjkU{LglO$>?I`2MR2*;td|rhNDpq6AypiYssY|DXFZ>R^*TKBoF3<4G}f0sk4*FcWbf^ zOmrC=-RR(8bN=bOeo^O29k8n1A;h$h$(-ipA`mN1zq|^4>5MtuUFsShB|zKdHS61J zx$pF8we?QVz!$CyqD0OfRsato1Kv6R-?{z%w|?g;$Fq&%hOO;>R*K!O4UPwVB&AfT z&ROJcIb_v#EfK0XgU&u+7nj{e#v_#aw2&r6!BnYw_>ACC1jzD~SPDgg-rOTl+#eQ+ zEG(O|lQjCzojIiuNGMJ7M7?s@@5SMH>I)@zoRT~@K2Agp(eNlTiM)B^p+0zx9m2!K za8z1cZm-YN=j3Rd0sn_<`%BA~S#q#vJBNVp>ZMWzlK0=om|qx2;;F}Pr3Qs>>9AxEwz9MA6}C=MC$i%TJx?q<3q223(Zfq+(;Q& zW)B19ky@y3AJ!xr^yI7O=d;4ZEpLrw=b3O!uC{uN2b(gC9;2U@p&BR5Y@4RlOh+m& zC^Q9YiX(BOOd=SC-3Y^~{bzTKN$zPCQAqTQ-#?b%GJthm8`$*EEv<`{2bK$(liG-k zP5y>x+l!Au(JW)Wc<4C{{=Ib?5j@#}9NT+M4H){FY2gxF()`bi9RSO<00?7w2Tz#y3pTHpRjWga zEV)qRWFgOYSQu2_who**=VM8#>l$F4pz5?FS?!F265W0-? zPxK{3X&;tFafOXIvA&}teVUOE5!)OnELp;AoHLNA>pMRB^DMdbsw|FVhqPdFT&C)G zysh%;lE5?#Sf$;xp{Xr9vAfcOZWPz?{28pR(kDG(rSnre&pB`riNEnD~=6*az5 zL=mYz-4;xW)@fc*_5KnaZ>Gu1J$};I8TVlBC(&X|bTy(^GtS{7(sW(Jlf0EUG&zl* zd8Y#leaKQX>tjN80!%aQx9`nVwFHXhZ2i9NRdM2@{?WZ5!WbES zP?=ZD7fkr+(%@rXH`=*0@}Mw!0(bn0{_zS+-C!)T_Vl~C9Pjnc-Fd6QmRqq+HKy- zcGrQJ))Jai*Q0T=+~xKI{y%+a#C|Jg)R2IHgk}HRcDu8gi;I<=h4X(rfd6!L?6*eH z{TSwc|D3f)p74vX}jzZk%G`pN8lGum}T{NU~C z+jrb=AKq9q*}852>(HsuSbS_YoYKuL#n#v3kKN2#MAOJxHpY7y{ANXL$G*AMi28Y& z^&EO5yCeuZGP-X&Z(TGRzHBf+g&%bLt>3twZ36tPu0#27Dm?tO5*&P+`}W*MG%1BxMxI%KfSuURGE0b%@X{w z25CAI`0C->vWm6Ac|Yf^I=;;s^~tt)9?-P=;V|aUFx>s^p?b=Q{eC<-j(E9bNVrG- zFmmN^JDd-c2ic*u>+$ZyAGxuUJBrU|kW|Nnyg2z?B$@ri^IOQ=jVJo$h0BU6%hD5| zm&_W6Wpwl81D*Wo@zzza@95mLTAQc4{nqcVpR{U-}9Bcx;Aa0KTJGi&a`L_o44F{L!A8e}c=xW9fiMG#H7K1W?8 z+vnpHZNb z$HXLB4nd#-y_spmI_?|6oz_aW8TCjUN3uduq^)V#aNP5RS{9F0;(;({>u32Rh6qzaql zSd2aZ1J5NbIZU7~X^wL#SpfGBKoryOs3WOEnwqWLBj}47N&F`@djZ;;RC<`U1*M>d zIvr#}kM;xsyw87?DU^zL1Uifba-XyW0gH*s82+A^TQ;B-;u9ZvjG$mSDxdl#f*320 z2|S^T_eAGt$4Dm}&63HTcpPmu)z)&1HA!KJdAblo)|E&g)Kii2)aK6yXS8ZAE^m3E zF(OSRJIqBuars_;T_RclNw%C52J91>G89Gu+L3K+3{#{`IgfH8xnNbAw9OQL1?6S% zRzfEE5Sk@f{f#6F$jnN~K8^-pAv`oLP24fg6Hr|-hf|V5ZFr+e{jXj&2MT5J1n!+Q zS~H{@mZ)^^^nOBZio>3=HAr#~Tmzh0^1R?XUa)55Qgjl%l$(j+F1=895QCbA^E62f z>k8FO4aB0g@j`sNynJPl3*4+*OOQ{|xdS0j!coT|MFhGbT+vAyD#Wt}>|MXyURqP@ z9;?D9B(+3xAC#b|VyusME}71xve~%Jn#BE21BJ#0qb?+k3FoM)%5^M_pzTBNNUH@f>RPvLx$2u+BOQ&@z`Ns z7-Q`zO{Zb65J160x)Gdu3NspQ9(B7V5xoe3;&3r-v48v)j)Yv7L{?t>cq5Gi!C zMv@U%v}Oj`3FW%6718`Yg{vvdn-u;kXVaqJjkR$wuYpPQL=01a%@#OWv9Zpa>M{o_ zlQPl}Z@y%dvnGB?Uvp`!Hb;3ZIrZ7D3-N1Ib6VvqhH>EFY!X>o`i1a0aLXdGwFqid zH0R&N6rF5%oCN~N>tthDl1yxRDRd%Y%I09dC8$Z5C4Aww5aoPXTBh>u&;|J^B@%$4 zB8<2aI|Lv$e#(%SEt8E|O@#c42_TNSRmZGuYFm~pDzPQP6w7e|5H9R{a2z<)FLP7{ zHrmLp?P-Wr!coqB_J*LC5 zrNJ)6*65m4AwG&no4pOdn97lLOo+_6b}0w-*JATRh)Y)a$a)Mn!0pyMVr zE=qD>$wkiLI%^uZ7ZWwkct2~rCF3sTE;nKM8{R&igrIP;RkYHApl?xE*5SvJ_U2km zy?odRmc%^RWHPniR#Yi3+aBHuM8#chF8om%u1R#9rc(l2qh*$8i)LL?1A1MOmI+}z zwM86X$h7He43lX^I*(XzsBA@Ic>1au4g&IM@nFKreA5Aaz@a!5C4i!BcxM4xiSB7t zUQr$a&@WYR@0XgSE_sG%4klqy<}s8KOv2O5= zD9rkT zrXSxL+>;g`?70W72JR6}>g>ODuUrr38io`F$Xc+8=JxaO%iF++%y$TDq#x+T#{+wMuv~&E_HMlP;8852@_;P zojD95L1C=pt83fE9ShsLqEZAJSXPDNJ=_6=S0+S-4AJbA!H;`%eWEm)Su zS&ts^4pW+%0U^;fyp9oez-uaGgwA`9P^QpL6ScyxH7i^|aF;K#TgIAqh&4Iv+75Rv zwj5Iv+>ThW#A_-v0e{6SOoA9)l+*&+L5$UB>j0a-968^>O7Hvej~{9G$D&FYxr`?` zpqGy>w6uI?s`Nj!l9fF`T})t>2{=w_8$6YOu4r$FwS2lVPQ*JcVyYPNWD-W%AtEI` zWRYcRl_wjWV%cdX9z1#b4v|bD;D5%EsTv6n(ZRbkNkP9=8mYvwU_@a_c4mu@p4}$5 z!}U}vT!8!JK#A+$E+kDEEYqh_I(ggTCPF`jl)4WEpeEkoLR#^wL6fB=&qJ#Ssv)!( zI(dVv#z@hGvXLr%4;HvfZQ%HLv0(2zre(&9Lu6Dluf38#pu)e$V-8|F5;KH$n>}po ze^7+H(_)Id6fMX!)mi@;*>jGk+6a7QLW#ijUQ7Nr=*<-@>SNABR&+;InOuQ3!aF9t z$5E+_4A(1^aAtLP{-laNm;p)z?cJkVL*vjgRWekj4;{fdgPiE_l1%kFss`LH8u1v! z`)Y(6{>+Ixl?=Z3BhQ{H4H2$MbU(gp%QotL%S9PhHHW7)LQ%*}rrs8oNL8bhe(??< zmepCPSs%Mr(@M*6TZb}8yj zs%p{X4{3|jy9BtS~W7OdL$rNA_ zIsE2hWjNsL;x(-eVmzdPNOy8Bb5`l#sfDG3SitYtlNRs6^?DREgwg3+`Q+AlEY@G6 zwAZBDKYsdS6&!(oRwhOdK3fre_IrZ|n{JWNYpp=kQ7H`ZC&c>~Ia75x+SQ|(+^<6k z2M*xTl^(G8hY+LAEJy>)+(LN61KBmQZs|2@*;X#8;YN|`rIYTnbkrWBKOxnkA`jWy z@nPhUn5qxtNgDi(V(LO+_+*=zz;DUbCSi7zs?)YvrM@hR0$seG2QjH$6n!z2?sxm*$H#ki^@DKFQdkf@i0eX>swfg?VRo%U$fBB%qnzYI?P?MlCR) z9KkPfv0s@s+xUe)?R$2%rVN(eM?HSn{^lPVJL4sEOmDYc(Cg3cZnfv$w&Cs$FTX_Z zKEDpG{f*q>-^uB(sv@c1b-8rK?;OVF47YG@(!F+X?-lsc@4XpmWq8cJ!P;Lo`}_Mn z+J3@N@pQO5vhV0Fnm1zS{&hFKu3LBU)aK(j zlgohRu{rEiPoP#~)4h&f;3vCo7xD7>Zi%7SxBsu}`B?C`NbBX}eH44K{^0$AvH~a5oG4ZrA5A+@#%wJRxoS&+uMQ z_LW8i&NLkqv zmF3areO$i9eQ!$A*SwFDgXdcAn(f_BttKf=b*}8)!=nx>d1Shyl(BOl+1b24} z!GilhfS`lB6Wm>bdvMp_7F@oSdvo8-<=yuWzP(nT)zjU3SM^#o-Bs0f&dGRWkYvd- zXb$b(rVYLH@tU<$5jhscw645-6cYs%k@des>hY|q_D*#*2NeGQj#FBgO~v4>q~R- z-%F_y8f%Pg+U*(IZJ zc}8w@swGV9GqD;Hw}Kp^6JyS;ld-|B)zZtHW^R&^Lq5wpB`Y&D$86PcRG@+%XdGOv zgx_^QxDKZ5Os9;ExlbOY_0MB%8FUF-YU90Blm% zP|)nS%rA zf*O^1#R3YGTvFop2uil7Y6eq_ED>)TEyl4DD=}9Qc+X}o93HGFQ&6n->Zlm76JG99 zMCBoTh+}c6%GUQH{{eGhPozlRR2OGVwvQgT!UoCY+Q<-t(MN3#rzh!V#;#Pg4yvs9 z8vX(0P$Bc1OURP*G7s|Qf$CwH_cOH(S>Wb zBU4r-7C~5R@PU3TG%Ie+wmxv~3~gY@hbF(TCu90FOH$i!mMNL8ijx7o7sUCE5BNsTUsFeE_YU_nFbR1}(` z&BC}BQTJtUF+mg5?@H%c!i{8^Gw1NDip7ffX~~W{mVd=TG$}!AVdkunW?RrE&>+v!lwlE;9%o*-ThQD`zUueM1EpN zD?2s4ssh=ovbvJfwtw(Zg_zb@ZOWJ9%k3sa}?_ z{#KF>v7DL0#T~q<4bi23oL4m1Tc{))VF-41)}W1@aakA}tqST7Y>wZr6-Z{#&{WjY zOz||cmmx7|BK;7!4kmWJt6T3S6tGdUCls(zvnLcdys~c$-TP^79j?CUAw*o;+is-Z zrmoXG&Yv1Odg$NqU9Qrvhh?mHrj=P;8EN)-&GR1Bgcrrmm?vK7@=@4;tXha97oywm zj$VIpong#)CKYl3Le z&O5?v%U&?rGyMLnopPj>SWcG3Ti7t#v$uwAm1>$U0yHd;3#4KO5xdU2@`u%u1J(2~ zx5}v2SMA=&CsZK?luT#HF($T#nMX@q$Ha<3zD8SN*i=SeECgH(j_W0}q?zCnI}yBW zO7I5-;K~4~QSW1otx~*upJja@SJ3ici}mgvBeDyG35U|#oxmnoZ#IC~fl@HwxrVFO z{C#@9?1jUBrAD<^y&!Xs=ngh`Tt5)*P^<{$3Yq`9ZgD}bewTKsdPnxSq5J5=W7T#i z)j-)uJ<`>C&Y(g(MRh&GE_FKS{vg33fPCcE%EgQ7#>0sX!a}{mm8`fUMFOq&MOoI!z> z=^6c3Lh#g2$~YDdR;gx>=gX^n4?m(d!YEZ9iazjF%l~{g@INI>}5| znnPdkab|SGQq1rEM7PY?qX>q7p5pFkVpjRX+gLa?M*hW6tM(B}k(rG!wBW8<*VPvL z*AHpv9Ex#f(aZ9rin*L`M7|zop&62VGS9L$IYjMYXbrH~IWNIWYElt4wVfLMx!bHF ze6(Ch5^^1f5&#m^4Hfhs0_-w_N$|!9gv&Fy1J#8bku!X^a7dwsn7Q6086F$5^{(VT zmmT(q!L}IovN*nQIXvO+0K0ogmah|~LG9P|VzdxmzW1tKCz9KCSy-Q)2fOoXtk-ud z4m57+$89ucSq-VDmP0;pdw!QALon@oGmauB{ejJyMdDckX=Oo39p@0$>6tz7(_0IH zwC&8cvza-~AYfyxNi&aZ3>DrN`X3T#cB#U8l4sf$qrnk!c}_A)Nmr9OhK+uqlA}^C zC;dLQHhq4qSTfMPyR;#)(L$S}ONGQ7{RaYX77N1 zkSuZShe~h$?b0Htfx&jGt3ceUu+SBB`lD0Ug>*AdbaYUx6=&+qXPcf~!=4&e)Al0Q zBLmqYiPCRB#znC_CiN@!%0}^qRP0oE#aC5M2;kJa_lOPBu30qbMr{@*SqBzrY?0rA zb}_4@&<{f4tRKzD+`NJF&)S?5OoltsarfH%T7x%>nexI0YmbZ{!3P@mox4o5+y|*b zZ+48k?!-CGueO|}lyu_wE-1AtsSQTh?8LRUUekKW0I$EWf%2kSLu>J)11eMtNgCvS zZ1TY@nN}R|r8frqhu}AAH-~|6+zwUII4{!#+%BzX`=v8*INP`f9I@87CD zx=geq>A+HzfyLZc&DeZWBK%BI&zL$?7Y-3~WAR zY3)jyyVgAn_~Vs6qQ89?BM0p8CPc0Y+!L6=8=lg`e$iJk`W|5_Y1^WXV&upBTWs&p zUedTy*H1ifae}6JbUwq%Uo*8KMhxO~Zm7@b+)8`*Mvciw+T$P__pyOKtcDT^v{DiY zw5$>dwD=MUN-*cG!s32ET4jD3wY-nq@1V_?)V#KwZd{!{i~H4|vLZNcnxb#@kB|?3 zPD|1G;MBYorst{@9_X~`h&R3{60^a!R1uq9298pD;rR-cLjq||yZ_tTB|hD2n>Ck@ znC_FGI=fwb9E~>wcKS7@VF!#`3HF1dsSO6lmgc-VK1Kgh?+piBk;tHdDGZwNs|Z4J=o(g?Q$^ ztqU6fcMvN{%QYfNMV2DSfsu~X(AoPc!O`VI-|hSEY3eDt_oxx(J(d*|d(34j=!qCW zH#0lWD>)tuEO`CShUS{`(QT}1hCb2sQDQ@?-4y7Rn~;{V)sWJi8@j_aS~QOnHklQS za`ig;q`woWnTTk9ycbIoVy-ld&b2CQaEIRTtPehn<@R3uVzAue(q3>I*5V4&>utux#R2YOo~8L~?tZg#5eLJ(1iJDKB&sUg3o9~(qX0?tZgxC5tSw0{_*oA>8KCQbQmA> z%l>ufm;LjwG}$w-G}-BYocLMPPhBC{pE57=1s7=KN#On?XK|t=d@C4mS|=EA%Hf0f z`semL;jatfXwaS%$cT0ET$wr&NtxOfBR|RjBR|UbKNb90qdyB)PKUNH5)8onRe{VQ zfhh{f3F`~8yVAT9@cm(YsdApzgm-^QmTY5IzfPjYqIVdR>He7I-Rf#VF=xZu4i!w6 zdQt1T;!?*=`WN{~X{~f~-m6&??)*-mdn_RxR=8u38mK&v#oWD8p2rpY(h4Bz?FFO=`M zLN+|8J-;c)P3%4>3Ys~JM=-Hs%q^hL6-xIqF*V=~N$*(jv1SzE4#CFqMc_2)4^dYI z3{mqabux7(&GE|*!4?;gwD(T}3~eofD>&FiW)tx!#fl3^pe!wJH>@bt6Y=!+Wl$8p zlG5;`6D)F5a8-b7)+5$I*?Q(&S@i5RR8FKE;wK`3ngBzA= zwo*^HOEpO?mugwoif(lc&G&E**^}ml(!Q-%l;?#~h1$fn_1k4v5mGVW0I{_6WhB7s(SdfhBlK9|p0o-IPp-@F1wI%lh&nQUh>nAte)v$k`VNfP zbFZE%kgI_cn-|piea-282}5mRd_13G!+pnmd~eq?8{(usx!cR6k}lTnI`|%p>v`HA zNvtocir02avfeqZ^Q@z` zC;>aj4%u$(ICzzOISwa>lKXj&y|-ixs{#4VHS$C}hcf(}iK&Q;)=do^=N z)t&ryGDW(lPoX#zZ<5}&Xs^iJN(|Hb(A&pj=5@W-d>TWQ#c)LVeBLetg`;e;ZfXP; zb~OY`@)7OZOIM6S-p#O}#-ovn+s8z5_7)49S znuFe3oShsywN9;b#R{dhOqGGH1MgX)i*`H1?eY_aON!3A&zde9_Vz>JXP|Yh6;Z&ckc1qLlI^JO;0)60QdG%F0gyUZkNX-%`)7) za!Jb_QkrhN=e?^S7XkWmfs;>3pU9F4?|Bxk{t}9apXZyD&slpkKhv&fbIG@>AHrU_ zp8VyL5(mLkxr}auDV@ZH^+R5s6Gewq;FNoBg}u+<%UkoHxy7`0^C@kDaH<%fnnjmn ze077v*T+WH&9zcqYMpB3NxrYKV@Yl>b(68rk}Jio1=&}Og-nj}x2k^}JGj)#BZ<81 z*o&?8r0RJXbRVR}iW_;29j&r9A-ZNOqM+R(Xy^ETrJ6>v_Dm2u3{hq}5$9BQ8^&!+ zj2xl}RQ!Hhr8s3PZnU{9SW|dV{EmTEAS_pugDyx`)xj_JQL~~e%6~M@crAxyH`M1l zQVgpGQAodHCfH;SZa#-ZrkU|v1IhiKdP$%5a)Dg01to9>~eRrM3GW#gp9&Jl(G4XuU;B9^dNXdc)Sk`_V#9dgh{XzDoMrXB(+HRm*vC?X~Td~rq zw_CB&E+M#kQ%2xq`Jk)2K~l;4NFc3)hZ}7kXH?yNojI-K zTl@G0rxx}%*RaHxa}bo9*HUegbg|l-`v=Zr$>roC`bZgLLcz$d2C#!a zxdi%S;Sc(<&Tz6oaN0+7R$%vSA_AE`a828?(O7Y zX=YsJ`P;eD@CP`t#gD59OmXE0CekkOd4q=HFBc&v?F!wW&RPw?9|4QOV=Z9$6UnH)(8Ar_Db{=@pdvkSMw_RVI zCCU1lbUx+Pe5UW$gb=ohWu0kP$N6yzEau^y^iQ7Yb6#3r59jjq=&vQM+uvtkLquFu zWft|6wol7JaABr)>hP$n`-m0alv*gB6P@K&F?(*M{h(Z77sWgEvoz zrm4>dcKH)rQVE{bFq7t)g09e|aieaD1qo9Sl&;+{N9Ys^9G0RI_M8h>q}4_SJK~C! z4u4WoufHl5)-fHgCD8;CfP>G zH_JwlRsx}K7cgzEi*=+h@|Gi!2*U_n(Q#L^b~P%=aUW*H2@fWoazmB5u=uD7ul$9t z-|bxBm$k@>c4O(!J7b^9`6Ar45Pyf$70aI&6`Uo_mTj2e*^-D+9Uv!;gR2| zi3_*DbA&KrzRCOGG)VI0kLFJr-N8|*n`gf^Uf)im|-CW*(LuIXhd~m>l zBkOj2u$1cP-N<9jFYuLS&d*t>2v<@JRx~|fOUT+}$GJKb`l9 z525&aRv$>;kZ|vPGBGW}YQqggB&R;IW05@W-fNe1bxv3D;>tM|hfymr{o+at*@^68)@aTS9kfkh?5NKBGV1G zJKOo(+AFkv9_2meT3-XxNdNrRs(rPfsN^^~<$8j&+IKD~39+x@l}maL(6M=N4{}ZT z=p!E)GFCC@TmH3~VzAN6uRs9+D=+{6+V9O&-_Gvmlk@*u>7RfowcE@yAvXhO1aKNS zD=Kr5phdf*40iN2VY;kGlBB6Rj6hK{>9f<375F2wE=h8ca_8$9ppg0LVJz|BzW22z zQ^iJbTnG5;7nT?5-YxGcNHZKFDwf)lQeJ)0ynaZT5hH$;>7yLwFJh)g>i!!MXO z9u6i!t7IRDM8?(8N9Cdfl4j*~#=g%-?^3J%?ZN($>Jf+71Hm{Q+J?EEs;S@%=6K{4*UeLvcsT)3^iscIo zZW67Iq#=kd?%%gV?v|xR!r%R9^;zcCm95Z%U;o4+O$;HCy)$eCKUUve_VuvFi8jSo zPtD4#P%&;J;W@PPnLvMqZ}9Zyk@mnmb0w=%<0&e%I_Z7CC74wySI>a+(ecG&Vw0kR z*`-cBX9LsSeXVuiRK$_5!=yex>F#mlIgW*QeC>KkQ2d6`@d!MyF`W&?SYen^RnLLk zrAtVdl`x0}eJ!;V&6+{*v4M|m8`26qRpdhfs;Y8UMz?r{4HH;*Tt#m%{;RK@w-C!( z2LJ%bfdT*s|LVv68CB&UANv0YtI`xTfcQs35Z^~M6Wa+F64Kno;1$)~XUKh3C>+3O z>YGb=k@A&}&jX38sqP8d?wlBn3c+(4P$~-0WV9*cE@*a#s6{B{38OR+2n7@{bd+PD z`%nZUmmoCj;*)V`1awdAUECS9I13KacHfrNk#Bo!FOyy5sV~e)1Kl*Q2TOV)^Q;2G zb8!`zJ)%@E{l=E*=nGjLl-ctfU(1+ARq57QW*n)G2R*!djR#zFIfv zzq(UU(DZ=+l?~@9{v|h#{1btHCh7Pcg)jiXZz(&T5}#%-_#6H3|B}n#DgB?(aDGSq zt6%j05gX^J($jz_zk%$R($nxLPbHo@-~Wwi>i?Gb!xjIj#8aQozmdi8OX8^q=u?TO z9y5OU-WlgXS z;OeQu)1krNXx9Fv@Q;DQQ~p1Dalb=R4*>YDKHXFLKi{su d{Hpx_dM3$B!NLE$90&jc#$f;eWYeGI{{vKQR%ZYJ literal 0 HcmV?d00001 diff --git a/Release - Architecture Doc/RMFT.docx b/Release - Architecture Doc/RMFT.docx new file mode 100644 index 0000000000000000000000000000000000000000..9dd7d5e0cdc336cd9b6f1fe15c12ca0ef0505a8e GIT binary patch literal 80441 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%0ADQ7mIi8IkvRlXU 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 ZKq$+>!9U)H1Hb{O!vO#joR7}?{|}exgL41? literal 0 HcmV?d00001 diff --git a/SSD1306Ascii.cpp b/SSD1306Ascii.cpp index adf5898..07a7883 100644 --- a/SSD1306Ascii.cpp +++ b/SSD1306Ascii.cpp @@ -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; } diff --git a/SSD1306Ascii.h b/SSD1306Ascii.h index 5841c82..90a7e7e 100644 --- a/SSD1306Ascii.h +++ b/SSD1306Ascii.h @@ -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[]; diff --git a/Sensors.cpp b/Sensors.cpp index 33c3fd6..198d8d9 100644 --- a/Sensors.cpp +++ b/Sensors.cpp @@ -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 return and only react to 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 / 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;idata.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 \ No newline at end of file diff --git a/Sensors.h b/Sensors.h index 36e8157..d6288e0 100644 --- a/Sensors.h +++ b/Sensors.h @@ -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 / 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 diff --git a/Turnouts.cpp b/Turnouts.cpp index 4c46a68..aeebd5b 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -18,67 +18,154 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ +#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 "" + void Turnout::printAll(Print *stream){ for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout) - StringFormatter::send(stream, F("\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0); + StringFormatter::send(stream, F("\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: +// +// +// +// + +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("\n"), data.id, state); + break; + case TURNOUT_DCC: + // DCC Turnout + StringFormatter::send(stream, F("\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("\n"), data.id, data.vpinData.vpin, state); + break; +#ifndef IO_NO_HAL + case TURNOUT_SERVO: + // Servo Turnout + StringFormatter::send(stream, F("\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;idata.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 +// and +// and + +Turnout *Turnout::create(int id, int params, int16_t p[]) { +#ifndef IO_NO_HAL + if (p[0] == HASH_KEYWORD_SERVO) { // + 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) { // + 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) // + return createDCC(id, p[1], p[2]); + else if (params==2 && p[1]>0 && p[1]<=512*4) // + return createDCC(id, (p[1]-1)/4+1, (p[1]-1)%4); + else + return NULL; + } else if (params==2) { // for DCC or LCN + return createDCC(id, p[0], p[1]); + } +#ifndef IO_NO_HAL + else if (params==3) { // legacy 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 diff --git a/Turnouts.h b/Turnouts.h index db97590..a89b68a 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -16,43 +16,103 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ + +/* + * 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 #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. diff --git a/WiThrottle.cpp b/WiThrottle.cpp index f3664a8..bd34c12 100644 --- a/WiThrottle.cpp +++ b/WiThrottle.cpp @@ -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 +#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;loconextThrottle) @@ -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;locoV%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); } diff --git a/WiThrottle.h b/WiThrottle.h index 0f9b573..3969737 100644 --- a/WiThrottle.h +++ b/WiThrottle.h @@ -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); diff --git a/WifiInboundHandler.cpp b/WifiInboundHandler.cpp index 61fa335..a3768ad 100644 --- a/WifiInboundHandler.cpp +++ b/WifiInboundHandler.cpp @@ -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; diff --git a/config.example.h b/config.example.h index 1d1977a..0debbc2 100644 --- a/config.example.h +++ b/config.example.h @@ -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 + ///////////////////////////////////////////////////////////////////////////////////// diff --git a/defines.h b/defines.h index b018c54..d52ae4a 100644 --- a/defines.h +++ b/defines.h @@ -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 diff --git a/myAutomation.example.h b/myAutomation.example.h new file mode 100644 index 0000000..aecaeac --- /dev/null +++ b/myAutomation.example.h @@ -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: + * + * + * 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 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. + + diff --git a/platformio.ini b/platformio.ini index a05a191..953a658 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 From 270b9df523331c2eb9436eab67b1253133856a45 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Wed, 4 Aug 2021 08:54:29 +0100 Subject: [PATCH 003/125] Remove obsolete docs --- Release - Architecture Doc/RMFT Reference.docx | Bin 27380 -> 0 bytes Release - Architecture Doc/RMFT.docx | Bin 80441 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Release - Architecture Doc/RMFT Reference.docx delete mode 100644 Release - Architecture Doc/RMFT.docx diff --git a/Release - Architecture Doc/RMFT Reference.docx b/Release - Architecture Doc/RMFT Reference.docx deleted file mode 100644 index 21ad7527ae9d733360d24db18592bb01ae015f08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27380 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 diff --git a/Release - Architecture Doc/RMFT.docx b/Release - Architecture Doc/RMFT.docx deleted file mode 100644 index 9dd7d5e0cdc336cd9b6f1fe15c12ca0ef0505a8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80441 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%0ADQ7mIi8IkvRlXU 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 ZKq$+>!9U)H1Hb{O!vO#joR7}?{|}exgL41? From 67be436898f2e915a98d3ab915f5a9dc90f4b281 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 8 Aug 2021 18:48:51 +0100 Subject: [PATCH 004/125] Correct throw/close re haba --- RMFT2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 3b615be..da15ebb 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -357,11 +357,11 @@ void RMFT2::loop2() { switch ((OPCODE)opcode) { case OPCODE_THROW: - Turnout::activate(operand, true); + Turnout::activate(operand, false); break; case OPCODE_CLOSE: - Turnout::activate(operand, false); + Turnout::activate(operand, true); break; case OPCODE_REV: From b06db69b536fe3301a7846e9c53ed31909242bf0 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Tue, 10 Aug 2021 10:41:35 +0100 Subject: [PATCH 005/125] correct example --- myAutomation.example.h | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/myAutomation.example.h b/myAutomation.example.h index aecaeac..7ade902 100644 --- a/myAutomation.example.h +++ b/myAutomation.example.h @@ -8,52 +8,52 @@ * - 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 + * instruction after E$XRAIL. + * This task may simply follow a route, or may START * 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: - * + * * * 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 + * ROUTE(n,"name") - as Route_n .. to setup a route through a layout + * AUTOMATION(n,"name") 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) +EXRAIL // myAutomation must start with the EXRAIL instruction + // This is the default starting route, AKA SEQUENCE(0) SETLOCO(3) // set current loco id... - SCHEDULE(1) // send current loco off along route 1 + START(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. + START(2) // send current loco off along route 2 + DONE // This just ends the startup thread, leaving 2 others running. -/* ROUTE(1) is a simple shuttle between 2 sensors +/* SEQUENCE(1) is a simple shuttle between 2 sensors * S10 and S11 are sensors pre-defined with the command * S10 S11 * === START->================ */ - AUTOMATION(1) - DELAY(100) // wait 10 seconds + SEQUENCE(1) + DELAY(10000) // wait 10 seconds FON(3) // Set Loco Function 3, Horn on - DELAY(10) // wait 1 second + DELAY(1000) // 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 + DELAY(5000) // 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 + FOLLOW(1) // and follow sequence 1 again -/* AUTOMATION(2) is an automation example for a single loco Y shaped journey +/* SEQUENCE(2) is an automation example for a single loco Y shaped journey * S1,S2,S3 are sensors, T4 is a turnout * * S3 T4 S1 @@ -64,23 +64,23 @@ ROUTES // myAutomation must start with the ROUTES instruction * * Train runs from START to S1, back to S2, again to S1, Back to start. */ - AUTOMATION(2) + SEQUENCE(2) FWD(60) // go forward at DCC speed 60 AT(1) STOP // when we get to sensor 1 - DELAY(100) // wait 10 seconds + DELAY(10000) // 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 + DELAY(5000) // wait 5 seconds FWD(50) // go forwards at speed 50 AT(1) STOP // and stop at sensor 1 - DELAY(50) // wait 5 seconds + DELAY(5000) // 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 + DELAY(20000) // wait 20 seconds + FOLLOW(2) // follow sequence 2... ie repeat the process - ENDROUTES // marks the end of the ROUTES program. + ENDEXRAIL // marks the end of the EXRAIL program. From a93f88d3b71948a611bd5bcd17182c830f04a7d1 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Tue, 10 Aug 2021 16:32:23 +0100 Subject: [PATCH 006/125] PRINT command --- RMFT2.cpp | 17 +++- RMFT2.h | 5 +- RMFTMacros.h | 262 +++++++++++++++++++++++++++------------------------ 3 files changed, 158 insertions(+), 126 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index da15ebb..ab0f41f 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -570,7 +570,11 @@ void RMFT2::loop2() { 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_PRINT: + printMessage(operand); + break; + case OPCODE_ROUTE: case OPCODE_AUTOMATION: case OPCODE_SEQUENCE: @@ -632,8 +636,8 @@ void RMFT2::kill(const FSH * reason, int operand) { return; } } - void RMFT2::turnoutEvent(VPIN id, bool thrown) { - byte huntFor=thrown? OPCODE_ONTHROW : OPCODE_ONCLOSE; + void RMFT2::turnoutEvent(VPIN id, bool state) { + byte huntFor=state ? OPCODE_ONCLOSE : OPCODE_ONTHROW ; // caution hides class progCounter; for (int progCounter=0;; SKIPOP){ byte opcode=GET_OPCODE; @@ -643,4 +647,9 @@ void RMFT2::kill(const FSH * reason, int operand) { new RMFT2(progCounter); // new task starts at this instruction return; } - } + } + + void RMFT2::printMessage2(const FSH * msg) { + DIAG(F("EXRAIL(%d) %S"),loco,msg); + } + diff --git a/RMFT2.h b/RMFT2.h index f253804..2dc02d4 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -41,7 +41,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, + OPCODE_PRINT, OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL }; @@ -89,6 +90,8 @@ private: bool doManual(); void loop2(); void kill(const FSH * reason=NULL,int operand=0); + void printMessage(uint16_t id); // Built by RMFTMacros.h + void printMessage2(const FSH * msg); static bool diag; static const FLASH byte RouteCode[]; diff --git a/RMFTMacros.h b/RMFTMacros.h index 420ff35..dceeb4b 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -33,12 +33,148 @@ // but since the C preprocessor is such a wimp, we have to pass over the myAutomation.h 2 times with // different macros. - +// PRINT(msg) is implemented in a separate pass to create a getMessageText(id) function #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 + +// Pass 1 Macros convert descriptions to a flash string constant in withrottle format. +// Most macros are simply ignored in this pass. +#define ROUTE(id, description) "]\\[R" #id "}|{" description "}|{2" +#define AUTOMATION(id, description) "]\\[A" #id "}|{" description "}|{4" +#define EXRAIL const FLASH char RMFT2::RouteDescription[]= +#define ENDEXRAIL ""; + +#define ALIAS(name,value) +#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 DONE +#define ENDIF +#define ENDTASK +#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 PRINT(msg) +#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 SEQUENCE(id) +#define SPEED(speed) +#define STOP +#undef SIGNAL +#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" + +// setup for pass 2... Create getMessageText function +#undef ROUTE +#undef AUTOMATION +#define ROUTE(id, description) +#define AUTOMATION(id, description) + +#undef EXRAIL +#undef PRINT +#undef ENDEXRAIL +const int PrintMacroTracker1=__COUNTER__; +#define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) { +#define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,PrintMacroTracker1); return ; }} +#define PRINT(msg) case (__COUNTER__ - PrintMacroTracker1) : printMessage2(F(msg));break; +#include "myAutomation.h" + +#undef ALIAS +#undef AFTER +#undef AMBER +#undef AT +#undef AUTOMATION +#undef CALL +#undef CLOSE +#undef DELAY +#undef DELAYMINS +#undef DELAYRANDOM +#undef DONE +#undef ENDIF +#undef ENDEXRAIL +#undef ENDTASK +#undef ESTOP +#undef EXRAIL +#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 PRINT +#undef READ_LOCO +#undef RED +#undef RESERVE +#undef RESET +#undef RESUME +#undef RETURN +#undef REV +#undef ROUTE +#undef START +#undef SEQUENCE +#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 + +// Define macros or route code creation #define ALIAS(name,value) const int name=value; #define EXRAIL const FLASH byte RMFT2::RouteCode[] = { #define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id), @@ -88,136 +224,20 @@ #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 PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - PrintMacroTracker2), #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 +// PASS2 Build RouteCode +const int PrintMacroTracker2=__COUNTER__; #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 From a0791b041c5d3a6461a331e4c8c83097c086d944 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 12 Aug 2021 08:25:51 +0100 Subject: [PATCH 007/125] Fix status display --- RMFT2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index ab0f41f..09fbe8a 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -139,7 +139,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { 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"), + StringFormatter::send(stream,F("\nPC=%d,DT=%l,LOCO=%d%c,SPEED=%d%c"), task->progCounter,task->delayTime,task->loco, task->invert?'I':' ', task->speedo, From 22b5d5e4c4286d99f40ef185a936ec5f05c848ea Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 12 Aug 2021 08:32:48 +0100 Subject: [PATCH 008/125] default off DIAG_LOOPTIMES --- IODevice.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IODevice.h b/IODevice.h index 38453ce..eda76ab 100644 --- a/IODevice.h +++ b/IODevice.h @@ -24,7 +24,7 @@ //#define DIAG_IO Y // Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported -#define DIAG_LOOPTIMES +//#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. @@ -348,4 +348,4 @@ private: // #include "IO_MCP23017.h" // #include "IO_PCF8574.h" -#endif // iodevice_h \ No newline at end of file +#endif // iodevice_h From 23291b499f385ffe08dcca4e609549d1eb197c7a Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 12 Aug 2021 08:53:52 +0100 Subject: [PATCH 009/125] EXRAIL LCD macro --- RMFTMacros.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index dceeb4b..88a6b71 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -36,6 +36,8 @@ // PRINT(msg) is implemented in a separate pass to create a getMessageText(id) function #define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF #define NOP 0,0 +// remove normal code LCD macro (will be restored later) +#undef LCD // CAUTION: The macros below are triple passed over myAutomation.h @@ -72,6 +74,7 @@ #define INVERT_DIRECTION #define JOIN #define LATCH(sensor_id) +#define LCD(row,msg) #define ONCLOSE(turnout_id) #define ONTHROW(turnout_id) #define PAUSE @@ -111,10 +114,12 @@ #undef EXRAIL #undef PRINT #undef ENDEXRAIL -const int PrintMacroTracker1=__COUNTER__; +#undef LCD +const int StringMacroTracker1=__COUNTER__; #define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) { -#define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,PrintMacroTracker1); return ; }} -#define PRINT(msg) case (__COUNTER__ - PrintMacroTracker1) : printMessage2(F(msg));break; +#define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }} +#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break; +#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" #undef ALIAS @@ -146,6 +151,7 @@ const int PrintMacroTracker1=__COUNTER__; #undef INVERT_DIRECTION #undef JOIN #undef LATCH +#undef LCD #undef ONCLOSE #undef ONTHROW #undef PAUSE @@ -207,10 +213,12 @@ const int PrintMacroTracker1=__COUNTER__; #define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,NOP, #define JOIN OPCODE_JOIN,NOP, #define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id), +#define LCD(id,msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2), #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 PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2), #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), @@ -227,17 +235,17 @@ const int PrintMacroTracker1=__COUNTER__; #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 PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - PrintMacroTracker2), #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), // PASS2 Build RouteCode -const int PrintMacroTracker2=__COUNTER__; +const int StringMacroTracker2=__COUNTER__; #include "myAutomation.h" -//================== - +// Restore normal code LCD macro +#undef LCD +#define LCD StringFormatter::lcd #endif From 2a79f67308033a859fdcfd546433a991c447110a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 11:59:32 +0100 Subject: [PATCH 010/125] Fix EEPROM handling for outputs. Output definitions in EEPROM were being lost once the output was activated or deactivated. The handling has been corrected and tested. --- Outputs.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Outputs.cpp b/Outputs.cpp index 5910172..7287446 100644 --- a/Outputs.cpp +++ b/Outputs.cpp @@ -105,7 +105,7 @@ void Output::activate(uint16_t s){ // Update EEPROM if output has been stored. if(EEStore::eeStore->data.nOutputs > 0 && num > 0) - EEPROM.put(num, data.flags); + EEPROM.put(num, data.oStatus); } /////////////////////////////////////////////////////////////////////////////// @@ -149,7 +149,9 @@ void Output::load(){ for(uint16_t i=0;idata.nOutputs;i++){ 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); + tt=create(data.id, data.pin, data.flags); + uint8_t state = data.setDefault ? data.defaultValue : data.active; + tt->activate(state); if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM EEStore::advance(sizeof(tt->data)); @@ -177,6 +179,8 @@ void Output::store(){ /////////////////////////////////////////////////////////////////////////////// // Static function to create an Output object +// The obscurely named parameter 'v' is 0 if called from the load() function +// and 1 if called from the command processing. Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){ Output *tt; From ec503e7d3ee21101e0bf2567e65aca846a5ce64d Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 12:01:10 +0100 Subject: [PATCH 011/125] Make IODevice::read function return type consistent with underlying _read calls. IODevice::read() now returns int, instead of bool. This is consistent with the IODevice::_read() return and also allows for future devices that return a non-boolean value. --- IODevice.cpp | 4 ++-- IODevice.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 0f833e8..d49eedc 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -261,7 +261,7 @@ bool IODevice::owns(VPIN id) { // } // Read value from virtual pin. -bool IODevice::read(VPIN vpin) { +int IODevice::read(VPIN vpin) { for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { if (dev->owns(vpin)) return dev->_read(vpin); @@ -302,7 +302,7 @@ bool IODevice::hasCallback(VPIN vpin) { (void)vpin; // Avoid compiler warnings return false; } -bool IODevice::read(VPIN vpin) { +int IODevice::read(VPIN vpin) { pinMode(vpin, INPUT_PULLUP); return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1) } diff --git a/IODevice.h b/IODevice.h index eda76ab..ca16285 100644 --- a/IODevice.h +++ b/IODevice.h @@ -102,7 +102,7 @@ public: static bool hasCallback(VPIN vpin); // read invokes the IODevice instance's _read method. - static bool read(VPIN vpin); + static int read(VPIN vpin); // loop invokes the IODevice instance's _loop method. static void loop(); @@ -348,4 +348,4 @@ private: // #include "IO_MCP23017.h" // #include "IO_PCF8574.h" -#endif // iodevice_h +#endif // iodevice_h \ No newline at end of file From 5932b4d1014a978916719b43f59ffb026b307bb9 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 12:02:18 +0100 Subject: [PATCH 012/125] Remove unnecessary servo output demands. If start and end position of a servo movement are identical, skip all but the last step. --- IO_PCA9685.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 8ddf409..a6bde29 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -196,6 +196,10 @@ 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 == 0 && s->fromPosition == s->toPosition) { + // No movement required, so go straight to final step + s->stepNumber = s->numSteps; + } if (s->stepNumber < s->numSteps) { // Animation in progress, reposition servo s->stepNumber++; From 329df3a3ee627dba89915b065a51767c51586f30 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 12 Aug 2021 20:35:56 +0100 Subject: [PATCH 013/125] correct example sensors --- myAutomation.example.h | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/myAutomation.example.h b/myAutomation.example.h index 7ade902..7353821 100644 --- a/myAutomation.example.h +++ b/myAutomation.example.h @@ -33,8 +33,8 @@ EXRAIL // myAutomation must start with the EXRAIL instruction DONE // This just ends the startup thread, leaving 2 others running. /* SEQUENCE(1) is a simple shuttle between 2 sensors - * S10 and S11 are sensors pre-defined with the command - * S10 S11 + * S20 and S21 are sensors on arduino pins 20 and 21 + * S20 S21 * === START->================ */ SEQUENCE(1) @@ -43,41 +43,41 @@ EXRAIL // myAutomation must start with the EXRAIL instruction DELAY(1000) // wait 1 second FOFF(3) // Horn off FWD(80) // Move forward at speed 80 - AT(11) // until we hit sensor id 11 + AT(21) // until we hit sensor id 21 STOP // then stop DELAY(5000) // Wait 5 seconds FON(2) // ring bell REV(60) // reverse at speed 60 - AT(10) // until we get to S10 + AT(20) // until we get to S20 STOP // then stop FOFF(2) // Bell off FOLLOW(1) // and follow sequence 1 again /* SEQUENCE(2) is an automation example for a single loco Y shaped journey - * S1,S2,S3 are sensors, T4 is a turnout + * S31,S32,S33 are sensors, T4 is a turnout * - * S3 T4 S1 + * S33 T4 S31 * ===-START->============================================= * // - * S2 // + * S32 // * ======================// * - * Train runs from START to S1, back to S2, again to S1, Back to start. + * Train runs from START to S31, back to S32, again to S31, Back to start. */ SEQUENCE(2) FWD(60) // go forward at DCC speed 60 - AT(1) STOP // when we get to sensor 1 + AT(31) STOP // when we get to sensor 31 DELAY(10000) // wait 10 seconds - THROW(4) // throw turnout for route to S2 + THROW(4) // throw turnout for route to S32 REV(45) // go backwards at speed 45 - AT(2) STOP // until we arrive at sensor 2 + AT(32) STOP // until we arrive at sensor 32 DELAY(5000) // wait 5 seconds FWD(50) // go forwards at speed 50 - AT(1) STOP // and stop at sensor 1 + AT(31) STOP // and stop at sensor 31 DELAY(5000) // wait 5 seconds CLOSE(4) // set turnout closed REV(50) // reverse back to S3 - AT(3) STOP + AT(33) STOP DELAY(20000) // wait 20 seconds FOLLOW(2) // follow sequence 2... ie repeat the process From f86a14ceabc9a491a197416dea015472bb18f10e Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 21:17:40 +0100 Subject: [PATCH 014/125] Servo profile - avoid overrun of array bounds. --- IO_PCA9685.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index a6bde29..dd2a607 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -129,7 +129,7 @@ void PCA9685::_write(VPIN vpin, int value) { s->numSteps = profile==Fast ? 10 : profile==Medium ? 20 : profile==Slow ? 40 : - profile==Bounce ? sizeof(_bounceProfile) : + profile==Bounce ? sizeof(_bounceProfile)-1 : 1; s->state = value; s->stepNumber = 0; @@ -163,7 +163,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { s->numSteps = profile==Fast ? 10 : profile==Medium ? 20 : profile==Slow ? 40 : - profile==Bounce ? sizeof(_bounceProfile) : + profile==Bounce ? sizeof(_bounceProfile)-1 : 1; s->stepNumber = 0; s->toPosition = min(value, 4095); From 5f5efa7d23e83869d69cc45f9efad13280a4c939 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 21:18:06 +0100 Subject: [PATCH 015/125] Correct number of usable pins for ArduinoPins class. --- IODevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IODevice.cpp b/IODevice.cpp index d49eedc..81bea0e 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -42,7 +42,7 @@ // and PCA9685. void IODevice::begin() { // Initialise the IO subsystem - ArduinoPins::create(2, NUM_DIGITAL_PINS-3); // Reserve pins for direct access + ArduinoPins::create(2, NUM_DIGITAL_PINS-2); // Reserve pins for direct access // Predefine two PCA9685 modules 0x40-0x41 // Allocates 32 pins 100-131 PCA9685::create(100, 16, 0x40); From 3b1759a88e300d145dffab8cb61e4cd96c7c378c Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 12 Aug 2021 21:18:46 +0100 Subject: [PATCH 016/125] Update IODevice.h to include other device includes files.. --- IODevice.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IODevice.h b/IODevice.h index ca16285..6717c8b 100644 --- a/IODevice.h +++ b/IODevice.h @@ -344,8 +344,8 @@ private: ///////////////////////////////////////////////////////////////////////////////////////////////////// -// #include "IO_MCP23008.h" -// #include "IO_MCP23017.h" -// #include "IO_PCF8574.h" +#include "IO_MCP23008.h" +#include "IO_MCP23017.h" +#include "IO_PCF8574.h" #endif // iodevice_h \ No newline at end of file From f1e84330caff19bd9a02991a529697e02e1735ef Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sat, 14 Aug 2021 16:42:56 +0100 Subject: [PATCH 017/125] PIN and SERVO turnout ids --- RMFT2.cpp | 22 ++++++++++------------ RMFTMacros.h | 8 ++++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 09fbe8a..97392f8 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -57,7 +57,7 @@ byte RMFT2::flags[MAX_FLAGS]; DCCEXParser::setRMFTFilter(RMFT2::ComandFilter); for (int f=0;f commands to do the following: +// This filter intercepts <> 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[]) { @@ -114,10 +116,6 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16 opcode=0; } break; - - case 't': // THROTTLE - // TODO - Monitor throttle commands and reject any that are in current automation - break; case '/': // New EXRAIL command reject=!parseSlash(stream,paramCount,p); diff --git a/RMFTMacros.h b/RMFTMacros.h index 88a6b71..ca7acad 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -96,8 +96,8 @@ #define STOP #undef SIGNAL #define SIGNAL(redpin,amberpin,greenpin) -#define SERVO_TURNOUT(pin,activeAngle,inactiveAngle) -#define PIN_TURNOUT(pin) +#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle) +#define PIN_TURNOUT(id,pin) #define THROW(id) #define TURNOUT(id,addr,subaddr) #define UNJOIN @@ -233,8 +233,8 @@ const int StringMacroTracker1=__COUNTER__; #define SPEED(speed) OPCODE_SPEED,V(speed), #define STOP OPCODE_SPEED,V(0), #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 SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle), +#define PIN_TURNOUT(id,pin) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,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, From 3ee7ba0b53e54a6cf18e77a371e1b87bd12763c1 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 15 Aug 2021 16:39:21 +0100 Subject: [PATCH 018/125] Servo profiles --- RMFT2.cpp | 3 ++- RMFTMacros.h | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 97392f8..944bcfe 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -85,7 +85,8 @@ byte RMFT2::flags[MAX_FLAGS]; VPIN pin=GET_OPERAND(1); int activeAngle=GET_OPERAND(2); int inactiveAngle=GET_OPERAND(3); - Turnout::createServo(id,pin,activeAngle,inactiveAngle); + int profile=GET_OPERAND(4); + Turnout::createServo(id,pin,activeAngle,inactiveAngle,profile); continue; } diff --git a/RMFTMacros.h b/RMFTMacros.h index ca7acad..e3af537 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -96,7 +96,7 @@ #define STOP #undef SIGNAL #define SIGNAL(redpin,amberpin,greenpin) -#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle) +#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile) #define PIN_TURNOUT(id,pin) #define THROW(id) #define TURNOUT(id,addr,subaddr) @@ -227,13 +227,13 @@ const int StringMacroTracker1=__COUNTER__; #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 SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::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), #define SIGNAL(redpin,amberpin,greenpin) OPCODE_SIGNAL,V(redpin),OPCODE_PAD,V(amberpin),OPCODE_PAD,V(greenpin), -#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle), +#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile), #define PIN_TURNOUT(id,pin) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,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), From 1e61c2cd61cce2274fd284ecad7528aa72b50c0b Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 15 Aug 2021 17:17:41 +0100 Subject: [PATCH 019/125] SENDLOCO/START mixup --- RMFT2.cpp | 16 +++++++++++----- RMFT2.h | 2 +- RMFTMacros.h | 3 +++ myAutomation.example.h | 6 ++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 944bcfe..32e0ded 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -547,12 +547,18 @@ void RMFT2::loop2() { case OPCODE_START: { - // Create new task and transfer loco..... - // but cheat by swapping prog counters with new task - int newPc=locateRouteStart(operand); + int newPc=locateRouteStart(GET_OPERAND(1)); 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 + new RMFT2(newPc); + } + break; + + case OPCODE_SENDLOCO: // cab, route + { + int newPc=locateRouteStart(GET_OPERAND(1)); + if (newPc<0) break; + RMFT2* newtask=new RMFT2(newPc); // create new task + newtask->loco=operand; } break; diff --git a/RMFT2.h b/RMFT2.h index 2dc02d4..90253f3 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -39,7 +39,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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_START,OPCODE_SETLOCO,OPCODE_SENDLOCO, OPCODE_PAUSE, OPCODE_RESUME, OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, OPCODE_PRINT, diff --git a/RMFTMacros.h b/RMFTMacros.h index e3af537..652613a 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -88,6 +88,7 @@ #define RETURN #define REV(speed) #define START(route) +#define SENDLOCO(cab,route) #define SERVO(id,position,profile) #define SETLOCO(loco) #define SET(sensor_id) @@ -168,6 +169,7 @@ const int StringMacroTracker1=__COUNTER__; #undef START #undef SEQUENCE #undef SERVO +#undef SENDLOCO #undef SETLOCO #undef SET #undef SPEED @@ -226,6 +228,7 @@ const int StringMacroTracker1=__COUNTER__; #define RESUME OPCODE_RESUME,NOP, #define RETURN OPCODE_RETURN,NOP, #define REV(speed) OPCODE_REV,V(speed), +#define SENDLOCO(cab,route) OPCODE_START,V(cab),OPCODE_PAD,V(route), #define START(route) OPCODE_START,V(route), #define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::profile), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco), diff --git a/myAutomation.example.h b/myAutomation.example.h index 7353821..9ba9e20 100644 --- a/myAutomation.example.h +++ b/myAutomation.example.h @@ -26,10 +26,8 @@ EXRAIL // myAutomation must start with the EXRAIL instruction // This is the default starting route, AKA SEQUENCE(0) - SETLOCO(3) // set current loco id... - START(1) // send current loco off along route 1 - SETLOCO(10) // set current loco id... - START(2) // send current loco off along route 2 + SENDLOCO(3,1) // send loco 3 off along route 1 + SENDLOCO(10,2) // send loco 10 off along route 2 DONE // This just ends the startup thread, leaving 2 others running. /* SEQUENCE(1) is a simple shuttle between 2 sensors From 3a6e41ac49851e5c6aa063cc2f051c5a1393aea8 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 15 Aug 2021 20:38:55 +0100 Subject: [PATCH 020/125] Ptevent accidental broadcast throttles --- RMFT2.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 32e0ded..13b2f13 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -286,11 +286,10 @@ int RMFT2::locateRouteStart(int16_t _route) { void RMFT2::driveLoco(byte speed) { - if (loco<0) return; // Caution, allows broadcast! + if (loco<=0) return; // Prevent 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) { @@ -431,9 +430,7 @@ void RMFT2::loop2() { break; case OPCODE_POM: - if (loco!=0) { - DCC::writeCVByteMain(loco, operand, GET_OPERAND(1)); - } + if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1)); break; case OPCODE_RESUME: @@ -487,11 +484,11 @@ void RMFT2::loop2() { break; case OPCODE_FON: - DCC::setFn(loco,operand,true); + if (loco) DCC::setFn(loco,operand,true); break; case OPCODE_FOFF: - DCC::setFn(loco,operand,false); + if (loco) DCC::setFn(loco,operand,false); break; case OPCODE_FOLLOW: From 7227a0696e5099a84a23a980d69fa7c9094b9657 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 15 Aug 2021 23:15:02 +0100 Subject: [PATCH 021/125] task Id and KILL Also fixes a long standing flags bug no idea where that line went! --- RMFT2.cpp | 39 +++++++++++++++++++++++++++++++++------ RMFT2.h | 10 +++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 13b2f13..ea7b7c1 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -35,6 +35,7 @@ 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; +const int16_t HASH_KEYWORD_KILL=5218; // 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. @@ -44,7 +45,7 @@ const int16_t HASH_KEYWORD_RESUME=27609; int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck bool RMFT2::diag=false; // RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring. -RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE. +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]; @@ -138,8 +139,8 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { StringFormatter::send(stream, F("<* EXRAIL STATUS")); RMFT2 * task=loopTask; while(task) { - StringFormatter::send(stream,F("\nPC=%d,DT=%l,LOCO=%d%c,SPEED=%d%c"), - task->progCounter,task->delayTime,task->loco, + StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"), + (int)(task->taskId),task->progCounter,task->loco, task->invert?'I':' ', task->speedo, task->forward?'F':'R' @@ -150,7 +151,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { // Now stream the flags for (int id=0;id=MAX_FLAGS) return false; - switch (p[0]) { + switch (p[0]) { + case HASH_KEYWORD_KILL: // Kill taskid + { + RMFT2 * task=loopTask; + while(task) { + if (task->taskId==p[1]) { + delete task; + return true; + } + task=task->next; + if (task==loopTask) break; + } + } + return false; + case HASH_KEYWORD_RESERVE: // force reserve a section setFlag(p[1],SECTION_FLAG); return true; @@ -234,6 +249,16 @@ void RMFT2::emitWithrottleRouteList(Print* stream) { RMFT2::RMFT2(int progCtr) { progCounter=progCtr; + + // get an unused task id from the flags table + taskId=255; // in case of overflow + for (int f=0;fnext) if (ring->next == this) { ring->next=next; @@ -609,9 +635,10 @@ void RMFT2::setFlag(VPIN id,byte onMask, byte offMask) { byte f=flags[id]; f &= ~offMask; f |= onMask; + flags[id]=f; } -byte RMFT2::getFlag(VPIN id,byte mask) { +bool RMFT2::getFlag(VPIN id,byte mask) { if (FLAGOVERFLOW(id)) return 0; // Outside range limit return flags[id]&mask; } diff --git a/RMFT2.h b/RMFT2.h index 90253f3..8ab26c3 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -51,6 +51,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, // Flag bits for status of hardware and TPL static const byte SECTION_FLAG = 0x01; static const byte LATCH_FLAG = 0x02; + static const byte TASK_FLAG = 0x04; static const byte MAX_STACK_DEPTH=4; @@ -73,7 +74,7 @@ private: 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 bool 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); @@ -85,9 +86,6 @@ private: 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); void printMessage(uint16_t id); // Built by RMFTMacros.h @@ -98,12 +96,14 @@ private: static const FLASH char RouteDescription[]; static byte flags[MAX_FLAGS]; - // Local variables - exist for each instance/task + // 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; + byte taskId; + int loco; bool forward; bool invert; From d8f23c58da042a7175555259852c7c63a8d8f3f3 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 16 Aug 2021 16:30:30 +0100 Subject: [PATCH 022/125] SENDLOCO/START bug --- RMFT2.cpp | 2 +- RMFTMacros.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index ea7b7c1..6324711 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -570,7 +570,7 @@ void RMFT2::loop2() { case OPCODE_START: { - int newPc=locateRouteStart(GET_OPERAND(1)); + int newPc=locateRouteStart(operand); if (newPc<0) break; new RMFT2(newPc); } diff --git a/RMFTMacros.h b/RMFTMacros.h index 652613a..2225426 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -228,7 +228,7 @@ const int StringMacroTracker1=__COUNTER__; #define RESUME OPCODE_RESUME,NOP, #define RETURN OPCODE_RETURN,NOP, #define REV(speed) OPCODE_REV,V(speed), -#define SENDLOCO(cab,route) OPCODE_START,V(cab),OPCODE_PAD,V(route), +#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), #define START(route) OPCODE_START,V(route), #define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::profile), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco), From 71486beb16821fb1f674dee18d5065ec602607e4 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 16 Aug 2021 22:27:50 +0100 Subject: [PATCH 023/125] Stop loco on DONE/KILL --- RMFT2.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/RMFT2.cpp b/RMFT2.cpp index 6324711..735d016 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -279,6 +279,7 @@ RMFT2::RMFT2(int progCtr) { RMFT2::~RMFT2() { + driveLoco(1); // ESTOP my loco if any setFlag(taskId,0,TASK_FLAG); // we are no longer using this id if (next==this) loopTask=NULL; else for (RMFT2* ring=next;;ring=ring->next) if (ring->next == this) { From edefd638f19f8e122cbcb80cb7f5cbbd725b77c7 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Tue, 17 Aug 2021 18:32:11 +0100 Subject: [PATCH 024/125] Handoff immediately after acquire --- WiThrottle.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/WiThrottle.cpp b/WiThrottle.cpp index bd34c12..c4b04b9 100644 --- a/WiThrottle.cpp +++ b/WiThrottle.cpp @@ -263,6 +263,7 @@ void WiThrottle::multithrottle(RingStream * stream, byte * cmd){ if (myLocos[loco].throttle=='\0') { myLocos[loco].throttle=throttleChar; myLocos[loco].cab=locoid; + mostRecentCab=locoid; StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid); //tell client to add loco //Get known Fn states from DCC for(int fKey=0; fKey<=28; fKey++) { From 9dacd24d2777b46a33958e5d89a976daba531578 Mon Sep 17 00:00:00 2001 From: Neil McKechnie <75813993+Neil-McK@users.noreply.github.com> Date: Tue, 17 Aug 2021 23:41:34 +0100 Subject: [PATCH 025/125] Various HAL enhancements. (#182) * Add command Allow a PWM servo to be driven to any arbitrary position. * Enhancements for HAL drivers Add state change notification for external GPIO module drivers; Allow drivers to be installed statically by declaration (as an alternative to the 'create' call). * Create IO_HCSR04.h HAL driver for HC-SR04 ultrasonic distance sensor (sonar). * Enable servo commands in NO-HAL mode, but return error. Avoid compile errors in RMFT.cpp when compiled with basic HAL by including the Turnout::createServo function as a stub that returns NULL. * Update IO_HCSR04.h Minor changes * Change Give the command an optional parameter of the profile. For example, will slowly move the servo on pin 100 to PWM position corresponding to 200. If omitted, the servo will move immediately (no animation). * IODevice (HAL) changes 1) Put new devices on the end of the chain instead of the beginning. This will give better performance for devices created first (ArduinoPins and extender GPIO devices, typically). 2) Remove unused functions. * Update IO_HCSR04.h Allow thresholds for ON and OFF to be separately configured at creation. * Update IODevice.cpp Fix compile error on IO_NO_HAL minimal HAL version. * Update IO_PCA9685.cpp Remove unnecessary duplicated call to min() function. --- DCCEXParser.cpp | 5 ++ IODevice.cpp | 131 +++++++++++--------------------- IODevice.h | 74 +++++++++--------- IO_DCCAccessory.cpp | 5 ++ IO_ExampleSerial.cpp | 44 ++++++----- IO_ExampleSerial.h | 21 +++++- IO_GPIOBase.h | 44 +++++++---- IO_HCSR04.h | 173 +++++++++++++++++++++++++++++++++++++++++++ IO_MCP23008.h | 2 +- IO_MCP23017.h | 3 +- IO_PCA9685.cpp | 10 +-- IO_PCF8574.h | 7 +- Sensors.cpp | 5 +- Sensors.h | 1 - Turnouts.cpp | 24 ++---- 15 files changed, 358 insertions(+), 191 deletions(-) create mode 100644 IO_HCSR04.h diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 9401796..1111810 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -56,6 +56,7 @@ const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_RESET = 26133; const int16_t HASH_KEYWORD_SPEED28 = -17064; const int16_t HASH_KEYWORD_SPEED128 = 25816; +const int16_t HASH_KEYWORD_SERVO = 27709; int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS]; bool DCCEXParser::stashBusy; @@ -800,6 +801,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) StringFormatter::send(stream, F("128 Speedsteps")); return true; + case HASH_KEYWORD_SERVO: + IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); + break; + default: // invalid/unknown break; } diff --git a/IODevice.cpp b/IODevice.cpp index 81bea0e..43db2f6 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -56,6 +56,7 @@ void IODevice::begin() { for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) { dev->_begin(); } + _initPhase = false; } // Overarching static loop() method for the IODevice subsystem. Works through the @@ -114,37 +115,6 @@ bool IODevice::hasCallback(VPIN vpin) { 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); @@ -200,22 +170,25 @@ void IODevice::setGPIOInterruptPin(int16_t pinNumber) { _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; + // Link new object to the end of the chain. Thereby, the first devices to be declared/created + // will be located faster by findDevice than those which are created later. + // Ideally declare/create the digital IO pins first, then servos, then more esoteric devices. + IODevice *lastDevice; + if (_firstDevice == 0) + _firstDevice = newDevice; + else { + for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) + lastDevice = dev; + lastDevice->_nextDevice = newDevice; + } + newDevice->_nextDevice = 0; - // Initialise device - newDevice->_begin(); + // If the IODevice::begin() method has already been called, initialise device here. If not, + // the device's _begin() method will be called by IODevice::begin(). + if (!_initPhase) + newDevice->_begin(); } // Private helper function to locate a device by VPIN. Returns NULL if not found @@ -231,7 +204,17 @@ IODevice *IODevice::findDevice(VPIN vpin) { // Static data //------------------------------------------------------------------------------------------------------------------ -IONotifyStateChangeCallback *IODevice::_notifyCallbackChain = 0; +// Chain of callback blocks (identifying registered callback functions for state changes) +IONotifyCallback *IONotifyCallback::first = 0; + +// Start of chain of devices. +IODevice *IODevice::_firstDevice = 0; + +// Reference to next device to be called on _loop() method. +IODevice *IODevice::_nextLoopDevice = 0; + +// Flag which is reset when IODevice::begin has been called. +bool IODevice::_initPhase = true; //================================================================================================================== @@ -243,23 +226,6 @@ 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. int IODevice::read(VPIN vpin) { for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { @@ -272,15 +238,6 @@ int IODevice::read(VPIN vpin) { 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) @@ -298,6 +255,9 @@ void IODevice::write(VPIN vpin, int value) { digitalWrite(vpin, value); pinMode(vpin, OUTPUT); } +void IODevice::writeAnalogue(VPIN vpin, int value, int profile) { + (void)vpin; (void)value; (void)profile; // Avoid compiler warnings +} bool IODevice::hasCallback(VPIN vpin) { (void)vpin; // Avoid compiler warnings return false; @@ -311,16 +271,13 @@ 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; -} + +// Chain of callback blocks (identifying registered callback functions for state changes) +// Not used in IO_NO_HAL but must be declared. +IONotifyCallback *IONotifyCallback::first = 0; #endif // IO_NO_HAL @@ -373,11 +330,7 @@ void ArduinoPins::_write(VPIN vpin, int value) { 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; @@ -400,11 +353,7 @@ int ArduinoPins::_read(VPIN vpin) { 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); @@ -418,9 +367,9 @@ void ArduinoPins::_display() { ///////////////////////////////////////////////////////////////////////////////////////////////////// -#if defined(USE_FAST_IO) void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) { +#if defined(USE_FAST_IO) if (pin >= NUM_DIGITAL_PINS) return; uint8_t mask = digitalPinToBitMask(pin); uint8_t port = digitalPinToPort(pin); @@ -431,16 +380,22 @@ void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) { else *outPortAdr &= ~mask; interrupts(); +#else + digitalWrite(pin, value); +#endif } bool ArduinoPins::fastReadDigital(uint8_t pin) { +#if defined(USE_FAST_IO) 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; +#else + bool result = digitalRead(pin); +#endif return result; } -#endif diff --git a/IODevice.h b/IODevice.h index 6717c8b..a542f56 100644 --- a/IODevice.h +++ b/IODevice.h @@ -49,9 +49,32 @@ typedef uint16_t VPIN; #define VPIN_MAX 32767 #define VPIN_NONE 65535 +/* + * Callback support for state change notification from an IODevice subclass to a + * handler, e.g. Sensor object handling. + */ -typedef void IONotifyStateChangeCallback(VPIN vpin, int value); - +class IONotifyCallback { +public: + typedef void IONotifyCallbackFunction(VPIN vpin, int value); + static void add(IONotifyCallbackFunction *function) { + IONotifyCallback *blk = new IONotifyCallback(function); + if (first) blk->next = first; + first = blk; + } + static void invokeAll(VPIN vpin, int value) { + for (IONotifyCallback *blk = first; blk != NULL; blk = blk->next) + blk->invoke(vpin, value); + } + static bool hasCallback() { + return first != NULL; + } +private: + IONotifyCallback(IONotifyCallbackFunction *function) { invoke = function; }; + IONotifyCallback *next = 0; + IONotifyCallbackFunction *invoke = 0; + static IONotifyCallback *first; +}; /* * IODevice class @@ -82,7 +105,8 @@ public: // Static functions to find the device and invoke its member functions - // begin is invoked to create any standard IODevice subclass instances + // begin is invoked to create any standard IODevice subclass instances. + // Also, the _begin method of any existing instances is called from here. static void begin(); // configure is used invoke an IODevice instance's _configure method @@ -112,9 +136,6 @@ public: // 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 @@ -123,23 +144,6 @@ public: // 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: @@ -200,23 +204,18 @@ protected: // 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 number of interrupt pin for GPIO extender devices. The extender module 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; - + // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; private: @@ -229,6 +228,7 @@ private: static IODevice *_firstDevice; static IODevice *_nextLoopDevice; + static bool _initPhase; }; @@ -240,6 +240,8 @@ private: class PCA9685 : public IODevice { public: static void create(VPIN vpin, int nPins, uint8_t I2CAddress); + // Constructor + PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress); enum ProfileType { Instant = 0, // Moves immediately between positions Fast = 1, // Takes around 500ms end-to-end @@ -249,8 +251,6 @@ public: }; 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; @@ -301,11 +301,12 @@ private: 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); + +private: // Device-specific write function. + void _begin() override; void _write(VPIN vpin, int value) override; void _display() override; int _packedAddress; @@ -326,6 +327,9 @@ public: // Constructor ArduinoPins(VPIN firstVpin, int nPins); + static void fastWriteDigital(uint8_t pin, uint8_t value); + static bool fastReadDigital(uint8_t pin); + private: // Device-specific pin configuration bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override; @@ -335,8 +339,6 @@ private: 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 diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index fefebb5..139d900 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -39,7 +39,12 @@ DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, i _firstVpin = vpin; _nPins = nPins; _packedAddress = (DCCAddress << 2) + DCCSubaddress; +} + +void DCCAccessoryDecoder::_begin() { int endAddress = _packedAddress + _nPins - 1; + int DCCAddress = _packedAddress >> 2; + int DCCSubaddress = _packedAddress & 3; 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); diff --git a/IO_ExampleSerial.cpp b/IO_ExampleSerial.cpp index 8ee8f13..6954e55 100644 --- a/IO_ExampleSerial.cpp +++ b/IO_ExampleSerial.cpp @@ -25,21 +25,25 @@ IO_ExampleSerial::IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) { _firstVpin = firstVpin; _nPins = nPins; + _pinValues = (uint16_t *)calloc(_nPins, sizeof(uint16_t)); + _baud = baud; // Save reference to serial port driver _serial = serial; - _serial->begin(baud); - DIAG(F("ExampleSerial configured Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1); + + addDevice(this); } // 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); + new IO_ExampleSerial(firstVpin, nPins, serial, baud); } // Device-specific initialisation void IO_ExampleSerial::_begin() { + _serial->begin(_baud); + DIAG(F("ExampleSerial configured Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1); + // Send a few # characters to the output for (uint8_t i=0; i<3; i++) _serial->write('#'); @@ -65,9 +69,8 @@ void IO_ExampleSerial::_write(VPIN vpin, int 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 a value for the specified vpin. + int result = _pinValues[vpin-_firstVpin]; return result; } @@ -80,35 +83,38 @@ void IO_ExampleSerial::_loop(unsigned long currentMicros) { if (_serial->available()) { // Input data available to read. Read a character. char c = _serial->read(); - switch (inputState) { + switch (_inputState) { case 0: // Waiting for start of command if (c == '#') // Start of command received. - inputState = 1; + _inputState = 1; break; case 1: // Expecting command character if (c == 'N') { // 'Notify' character received - inputState = 2; - inputValue = inputIndex = 0; + _inputState = 2; + _inputValue = _inputIndex = 0; } else - inputState = 0; // Unexpected char, reset + _inputState = 0; // Unexpected char, reset break; case 2: // reading first parameter (index) if (isdigit(c)) - inputIndex = inputIndex * 10 + (c-'0'); + _inputIndex = _inputIndex * 10 + (c-'0'); else if (c==',') - inputState = 3; + _inputState = 3; else - inputState = 0; // Unexpected char, reset + _inputState = 0; // Unexpected char, reset break; case 3: // reading reading second parameter (value) if (isdigit(c)) - inputValue = inputValue * 10 - (c-'0'); + _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. + DIAG(F("ExampleSerial Received command, p1=%d, p2=%d"), _inputIndex, _inputValue); + if (_inputIndex < _nPins) { // Store value + _pinValues[_inputIndex] = _inputValue; + } + _inputState = 0; // Done, start again. } else - inputState = 0; // Unexpected char, reset + _inputState = 0; // Unexpected char, reset break; } } diff --git a/IO_ExampleSerial.h b/IO_ExampleSerial.h index 1273a95..582a51c 100644 --- a/IO_ExampleSerial.h +++ b/IO_ExampleSerial.h @@ -17,6 +17,18 @@ * along with CommandStation. If not, see . */ +/* + * To declare a device instance, + * IO_ExampleSerial myDevice(1000, 10, Serial3, 9600); + * or to create programmatically, + * IO_ExampleSerial::create(1000, 10, Serial3, 9600); + * + * (uses VPINs 1000-1009, talke on Serial 3 at 9600 baud.) + * + * See IO_ExampleSerial.cpp for the protocol used over the serial line. + * + */ + #ifndef IO_EXAMPLESERIAL_H #define IO_EXAMPLESERIAL_H @@ -27,6 +39,7 @@ public: IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud); static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud); +protected: void _begin() override; void _loop(unsigned long currentMicros) override; void _write(VPIN vpin, int value) override; @@ -35,9 +48,11 @@ public: private: HardwareSerial *_serial; - uint8_t inputState = 0; - int inputIndex = 0; - int inputValue = 0; + uint8_t _inputState = 0; + int _inputIndex = 0; + int _inputValue = 0; + uint16_t *_pinValues; // Pointer to block of memory containing pin values + unsigned long _baud; }; #endif // IO_EXAMPLESERIAL_H \ No newline at end of file diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 366d0fc..7179f9f 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -45,6 +45,10 @@ protected: int _read(VPIN vpin) override; void _display() override; void _loop(unsigned long currentMicros) override; + bool _hasCallback(VPIN vpin) { + (void)vpin; // suppress compiler warning + return true; // Enable callback if caller wants to use it. + } // Data fields uint8_t _I2CAddress; @@ -82,29 +86,29 @@ GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2 _nPins = nPins; _I2CAddress = I2CAddress; _gpioInterruptPin = interruptPin; - _notifyCallbackChain = 0; // Add device to list of devices. addDevice(this); +} +template +void GPIOBase::_begin() { // 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)) { + if (I2CManager.exists(_I2CAddress)) { _display(); _portMode = 0; // default to input mode _portPullup = -1; // default to pullup enabled - _portInputState = 0; + _portInputState = -1; } + _setupDevice(); _deviceState = DEVSTATE_NORMAL; _lastLoopEntry = micros(); } -template -void GPIOBase::_begin() {} - // Configuration parameters for inputs: // params[0]: enable pullup // params[1]: invert input (optional) @@ -134,9 +138,7 @@ bool GPIOBase::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCoun // Periodically read the input port template void GPIOBase::_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) { @@ -146,7 +148,27 @@ void GPIOBase::_loop(unsigned long currentMicros) { DIAG(F("%S I2C:x%x Error:%d"), _deviceName, _I2CAddress, status); } _processCompletion(status); + + // Scan for changes in input states and invoke callback (if present) + T differences = lastPortStates ^ _portInputState; + if (differences && IONotifyCallback::hasCallback()) { + // Scan for differences bit by bit + T mask = 1; + for (int pin=0; pin<_nPins; pin++) { + if (differences & mask) { + // Change detected. + IONotifyCallback::invokeAll(_firstVpin+pin, (_portInputState & mask) == 0); + } + mask <<= 1; + } + } + + #ifdef DIAG_IO + if (differences) + DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState); + #endif } + // Check if interrupt configured. If so, and pin is not pulled down, finish. if (_gpioInterruptPin >= 0) { if (digitalRead(_gpioInterruptPin)) return; @@ -162,12 +184,6 @@ void GPIOBase::_loop(unsigned long currentMicros) { _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 diff --git a/IO_HCSR04.h b/IO_HCSR04.h new file mode 100644 index 0000000..5234fe1 --- /dev/null +++ b/IO_HCSR04.h @@ -0,0 +1,173 @@ +/* + * © 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 . + */ + +/* + * The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver. + * It is operated through two signal pins. When the transmit pin is set to 1 for + * 10us, on the falling edge the transmitter sends a short transmission of + * 8 pulses (like a sonar 'ping'). This is reflected off objects and received + * by the receiver. A pulse is sent on the receive pin whose length is equal + * to the delay between the transmission of the pulse and the detection of + * its echo. The distance of the reflecting object is calculated by halving + * the time (to allow for the out and back distance), then multiplying by the + * speed of sound (assumed to be constant). + * + * This driver polls the HC-SR04 by sending the trigger pulse and then measuring + * the length of the received pulse. If the calculated distance is less than the + * threshold, the output changes to 1. If it is greater than the threshold plus + * a hysteresis margin, the output changes to 0. + * + * The measurement would be more reliable if interrupts were disabled while the + * pulse is being timed. However, this would affect other functions in the CS + * so the measurement is being performed with interrupts enabled. Also, we could + * use an interrupt pin in the Arduino for the timing, but the same consideration + * applies. + * + * Note: The timing accuracy required by this means that the pins have to be + * direct Arduino pins; GPIO pins on an IO Extender cannot provide the required + * accuracy. + */ + +#ifndef IO_HCSR04_H +#define IO_HCSR04_H + +#include "IODevice.h" + +class HCSR04 : public IODevice { + +private: + // pins must be arduino GPIO pins, not extender pins or HAL pins. + int _transmitPin = -1; + int _receivePin = -1; + // Thresholds for setting active state in cm. + uint8_t _onThreshold; // cm + uint8_t _offThreshold; // cm + // Active=1/inactive=0 state + uint8_t _value = 0; + // Time of last loop execution + unsigned long _lastExecutionTime; + // Factor for calculating the distance (cm) from echo time (ms). + // Based on a speed of sound of 345 metres/second. + const uint16_t factor = 58; // ms/cm + +public: + // Constructor perfroms static initialisation of the device object + HCSR04 (VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { + _firstVpin = vpin; + _nPins = 1; + _transmitPin = transmitPin; + _receivePin = receivePin; + _onThreshold = onThreshold; + _offThreshold = offThreshold; + addDevice(this); + } + + // Static create function provides alternative way to create object + static void create(VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { + new HCSR04(vpin, transmitPin, receivePin, onThreshold, offThreshold); + } + +protected: + // _begin function called to perform dynamic initialisation of the device + void _begin() override { + pinMode(_transmitPin, OUTPUT); + pinMode(_receivePin, INPUT); + ArduinoPins::fastWriteDigital(_transmitPin, 0); + _lastExecutionTime = micros(); + DIAG(F("HCSR04 configured on VPIN:%d TXpin:%d RXpin:%d On:%dcm Off:%dcm"), + _firstVpin, _transmitPin, _receivePin, _onThreshold, _offThreshold); + } + + // _read function - just return _value (calculated in _loop). + int _read(VPIN vpin) override { + (void)vpin; // avoid compiler warning + return _value; + } + + // _loop function - read HC-SR04 once every 50 milliseconds. + void _loop(unsigned long currentMicros) override { + if (currentMicros - _lastExecutionTime > 50000) { + _lastExecutionTime = currentMicros; + + _value = read_HCSR04device(); + } + } + +private: + // This polls the HC-SR04 device by sending a pulse and measuring the duration of + // the pulse observed on the receive pin. In order to be kind to the rest of the CS + // software, no interrupts are used and interrupts are not disabled. The pulse duration + // is measured in a loop, using the micros() function. Therefore, interrupts from other + // sources may affect the result. However, interrupts response code in CS typically takes + // much less than the 58us frequency for the DCC interrupt, and 58us corresponds to only 1cm + // in the HC-SR04. + // To reduce chatter on the output, hysteresis is applied on reset: the output is set to 1 when the + // measured distance is less than the onThreshold, and is set to 0 if the measured distance is + // greater than the offThreshold. + // + uint8_t read_HCSR04device() { + // uint16 enough to time up to 65ms + uint16_t startTime, waitTime, currentTime, maxTime; + + // If receive pin is still set on from previous call, abort the read. + if (ArduinoPins::fastReadDigital(_receivePin)) return _value; + + // Send 10us pulse to trigger transmitter + ArduinoPins::fastWriteDigital(_transmitPin, 1); + delayMicroseconds(10); + ArduinoPins::fastWriteDigital(_transmitPin, 0); + + // Wait for receive pin to be set + startTime = currentTime = micros(); + maxTime = factor * _offThreshold * 2; + while (!ArduinoPins::fastReadDigital(_receivePin)) { + // lastTime = currentTime; + currentTime = micros(); + waitTime = currentTime - startTime; + if (waitTime > maxTime) { + // Timeout waiting for pulse start, abort the read + return _value; + } + } + + // Wait for receive pin to reset, and measure length of pulse + startTime = currentTime = micros(); + maxTime = factor * _offThreshold; + while (ArduinoPins::fastReadDigital(_receivePin)) { + currentTime = micros(); + waitTime = currentTime - startTime; + // If pulse is too long then set return value to zero, + // and finish without waiting for end of pulse. + if (waitTime > maxTime) { + // Pulse length longer than maxTime, reset value. + return 0; + } + } + // Check if pulse length is below threshold, if so set value. + //DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance); + uint16_t distance = waitTime / factor; // in centimetres + if (distance < _onThreshold) + return 1; + + return _value; + } + +}; + +#endif //IO_HCSR04_H \ No newline at end of file diff --git a/IO_MCP23008.h b/IO_MCP23008.h index c04712a..3557b49 100644 --- a/IO_MCP23008.h +++ b/IO_MCP23008.h @@ -28,7 +28,6 @@ public: new MCP23008(firstVpin, nPins, I2CAddress, interruptPin); } -private: // Constructor MCP23008(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) : GPIOBase((FSH *)F("MCP23008"), firstVpin, min(nPins, 8), I2CAddress, interruptPin) { @@ -38,6 +37,7 @@ private: outputBuffer[0] = REG_GPIO; } +private: void _writeGpioPort() override { I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState); } diff --git a/IO_MCP23017.h b/IO_MCP23017.h index 2c56ea7..d7c27ce 100644 --- a/IO_MCP23017.h +++ b/IO_MCP23017.h @@ -34,7 +34,6 @@ public: new MCP23017(vpin, min(nPins,16), I2CAddress, interruptPin); } -private: // Constructor MCP23017(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) : GPIOBase((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin) @@ -42,9 +41,9 @@ private: requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer), outputBuffer, sizeof(outputBuffer)); outputBuffer[0] = REG_GPIOA; - _setupDevice(); } +private: void _writeGpioPort() override { I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8); } diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index dd2a607..1c24335 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -82,6 +82,10 @@ PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) { // Initialise structure used for setting pulse rate requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer)); +} + +// Device-specific initialisation +void PCA9685::_begin() { I2CManager.begin(); I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C // In reality, other devices including the Arduino will limit @@ -100,10 +104,6 @@ PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) { } } -// Device-specific initialisation -void PCA9685::_begin() { -} - // Device-specific write function, invoked from IODevice::write(). void PCA9685::_write(VPIN vpin, int value) { #ifdef DIAG_IO @@ -166,7 +166,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { profile==Bounce ? sizeof(_bounceProfile)-1 : 1; s->stepNumber = 0; - s->toPosition = min(value, 4095); + s->toPosition = value; s->fromPosition = s->currentPosition; } diff --git a/IO_PCF8574.h b/IO_PCF8574.h index be9ead7..2a8d363 100644 --- a/IO_PCF8574.h +++ b/IO_PCF8574.h @@ -28,13 +28,13 @@ public: new PCF8574(firstVpin, nPins, I2CAddress, interruptPin); } -private: PCF8574(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) : GPIOBase((FSH *)F("PCF8574"), firstVpin, min(nPins, 8), I2CAddress, interruptPin) { requestBlock.setReadParams(_I2CAddress, inputBuffer, 1); } +private: // 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); @@ -73,7 +73,10 @@ private: _portInputState = 0xff; } - void _setupDevice() override { } + // Set up device ports + void _setupDevice() override { + _writePortModes(); + } uint8_t inputBuffer[1]; }; diff --git a/Sensors.cpp b/Sensors.cpp index 198d8d9..fc8abb5 100644 --- a/Sensors.cpp +++ b/Sensors.cpp @@ -91,7 +91,7 @@ void Sensor::checkAll(Print *stream){ #ifdef USE_NOTIFY // Register the event handler ONCE! if (!inputChangeCallbackRegistered) - nextInputChangeCallback = IODevice::registerInputChangeNotification(inputChangeCallback); + IONotifyCallback::add(inputChangeCallback); inputChangeCallbackRegistered = true; #endif @@ -192,8 +192,6 @@ void Sensor::inputChangeCallback(VPIN vpin, int state) { if (tt != NULL) { // Sensor found tt->inputState = (state != 0); } - // Call next registered callback function - if (nextInputChangeCallback) nextInputChangeCallback(vpin, state); } #endif @@ -345,6 +343,5 @@ unsigned long Sensor::lastReadCycle=0; Sensor *Sensor::firstPollSensor = NULL; Sensor *Sensor::lastSensor = NULL; bool Sensor::pollSignalPhase = false; -IONotifyStateChangeCallback *Sensor::nextInputChangeCallback = 0; bool Sensor::inputChangeCallbackRegistered = false; #endif \ No newline at end of file diff --git a/Sensors.h b/Sensors.h index d6288e0..60e414f 100644 --- a/Sensors.h +++ b/Sensors.h @@ -92,7 +92,6 @@ public: #ifdef USE_NOTIFY static bool pollSignalPhase; static void inputChangeCallback(VPIN vpin, int state); - static IONotifyStateChangeCallback *nextInputChangeCallback; static bool inputChangeCallbackRegistered; #endif diff --git a/Turnouts.cpp b/Turnouts.cpp index aeebd5b..79a2d65 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -18,7 +18,7 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ -#define EESTOREDEBUG +//#define EESTOREDEBUG #include "defines.h" #include "Turnouts.h" #include "EEStore.h" @@ -72,13 +72,11 @@ void Turnout::print(Print *stream){ // VPIN Digital output StringFormatter::send(stream, F("\n"), data.id, data.vpinData.vpin, state); break; -#ifndef IO_NO_HAL case TURNOUT_SERVO: // Servo Turnout StringFormatter::send(stream, F("\n"), data.id, data.servoData.vpin, data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, state); break; -#endif default: break; } @@ -89,9 +87,6 @@ void Turnout::print(Print *stream){ // 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) return false; tt->activate(state); @@ -136,11 +131,11 @@ void Turnout::activate(bool state) { DCC::setAccessory((((data.dccAccessoryData.address-1) >> 2) + 1), ((data.dccAccessoryData.address-1) & 3), state); break; -#ifndef IO_NO_HAL case TURNOUT_SERVO: +#ifndef IO_NO_HAL IODevice::write(data.servoData.vpin, state); - break; #endif + break; case TURNOUT_VPIN: IODevice::write(data.vpinData.vpin, state); break; @@ -205,12 +200,10 @@ void Turnout::load(){ 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; @@ -294,11 +287,11 @@ Turnout *Turnout::createVpin(int id, VPIN vpin, uint8_t state){ 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){ +#ifndef IO_NO_HAL if (activePosition > 511 || inactivePosition > 511 || profile > 4) return NULL; Turnout *tt=create(id); @@ -317,8 +310,11 @@ Turnout *Turnout::createServo(int id, VPIN vpin, uint16_t activePosition, uint16 return NULL; } return(tt); -} +#else + (void)id; (void)vpin; (void)activePosition; (void)inactivePosition; (void)profile; (void)state; // avoid compiler warnings + return NULL; #endif +} /////////////////////////////////////////////////////////////////////////////// // Support for @@ -326,14 +322,12 @@ Turnout *Turnout::createServo(int id, VPIN vpin, uint16_t activePosition, uint16 // and Turnout *Turnout::create(int id, int params, int16_t p[]) { -#ifndef IO_NO_HAL if (p[0] == HASH_KEYWORD_SERVO) { // 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) { // if (params==2) return createVpin(id, p[1]); @@ -350,11 +344,9 @@ Turnout *Turnout::create(int id, int params, int16_t p[]) { } else if (params==2) { // for DCC or LCN return createDCC(id, p[0], p[1]); } -#ifndef IO_NO_HAL else if (params==3) { // legacy for Servo return createServo(id, (VPIN)p[0], (uint16_t)p[1], (uint16_t)p[2]); } -#endif return NULL; } From 36f6e2f9ce0f324311f929eee5aab1123078d393 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Wed, 18 Aug 2021 18:55:22 +0100 Subject: [PATCH 026/125] Narrowing Turnout publics --- DCCEXParser.cpp | 12 ++++-------- LCN.cpp | 2 +- RMFT2.cpp | 8 ++++---- RMFT2.h | 2 +- Turnouts.cpp | 27 ++++++++++++++++++++++++++- Turnouts.h | 20 ++++++++++++-------- WiThrottle.cpp | 31 ++++++++++++++++++------------- 7 files changed, 66 insertions(+), 36 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 1111810..816cf85 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -672,14 +672,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) StringFormatter::send(stream, F("\n")); return true; - case 2: // activate turnout - { - Turnout *tt = Turnout::get(p[0]); - if (!tt) - return false; - tt->activate(p[1]); - StringFormatter::send(stream, F("\n"), p[0], tt->data.active); - } + case 2: // turnout 0=CLOSE,1=THROW + if (p[1]>1 || p[1]<0 ) return false; + if (!Turnout::setClosed(p[0],p[1]==0)) return false; + StringFormatter::send(stream, F("\n"), p[0], p[1]); return true; default: // Anything else is handled by Turnout class. diff --git a/LCN.cpp b/LCN.cpp index df1b4ea..e9d0886 100644 --- a/LCN.cpp +++ b/LCN.cpp @@ -50,7 +50,7 @@ void LCN::loop() { if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch); Turnout * tt = Turnout::get(id); if (!tt) tt=Turnout::createLCN(id); - tt->setActive(ch=='t'); + Turnout::setClosedStateOnly(id,ch=='t'); Turnout::turnoutlistHash++; // signals ED update of turnout data id = 0; } diff --git a/RMFT2.cpp b/RMFT2.cpp index 735d016..5e4fd62 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -382,11 +382,11 @@ void RMFT2::loop2() { switch ((OPCODE)opcode) { case OPCODE_THROW: - Turnout::activate(operand, false); + Turnout::setClosed(operand, false); break; case OPCODE_CLOSE: - Turnout::activate(operand, true); + Turnout::setClosed(operand, true); break; case OPCODE_REV: @@ -666,8 +666,8 @@ void RMFT2::kill(const FSH * reason, int operand) { return; } } - void RMFT2::turnoutEvent(VPIN id, bool state) { - byte huntFor=state ? OPCODE_ONCLOSE : OPCODE_ONTHROW ; + void RMFT2::turnoutEvent(VPIN id, bool closed) { + byte huntFor=closed ? OPCODE_ONCLOSE : OPCODE_ONTHROW ; // caution hides class progCounter; for (int progCounter=0;; SKIPOP){ byte opcode=GET_OPCODE; diff --git a/RMFT2.h b/RMFT2.h index 8ab26c3..77e21bc 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -68,7 +68,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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); + static void turnoutEvent(VPIN id, bool closed); private: static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int p[]); static bool parseSlash(Print * stream, byte & paramCount, int p[]) ; diff --git a/Turnouts.cpp b/Turnouts.cpp index 79a2d65..c18337b 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -18,6 +18,12 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ + +// >>>>>> ATTENTION: This class requires major cleaning. +// The public interface has been narrowed to avoid the ambuguity of "activated". + + + //#define EESTOREDEBUG #include "defines.h" #include "Turnouts.h" @@ -40,6 +46,10 @@ enum unit8_t { TURNOUT_LCN = 4, }; + + + + /////////////////////////////////////////////////////////////////////////////// // Static function to print all Turnout states to stream in form "" @@ -82,10 +92,25 @@ void Turnout::print(Print *stream){ } } + +// Public interface to turnout throw/close +bool Turnout::setClosed(int id, bool closed) { + // hides the internal activate argument to a single place + return activate(id, closed? false: true ); /// Needs cleaning up +} +bool Turnout::isClosed(int id) { + // hides the internal activate argument to a single place + return !isActive(id); /// Needs cleaning up +} +int Turnout::getId() { + return data.id; +} + /////////////////////////////////////////////////////////////////////////////// // Static function to activate/deactivate Turnout with ID 'n'. // Returns false if turnout not found. + bool Turnout::activate(int n, bool state){ Turnout * tt=get(n); if (!tt) return false; @@ -145,7 +170,7 @@ void Turnout::activate(bool state) { EEPROM.put(num, data.tStatus); #if defined(RMFT_ACTIVE) - RMFT2::turnoutEvent(data.id, state); + RMFT2::turnoutEvent(data.id, !state); #endif } diff --git a/Turnouts.h b/Turnouts.h index a89b68a..db73214 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -33,6 +33,7 @@ #include "LCN.h" #include "IODevice.h" + const byte STATUS_ACTIVE=0x80; // Flag as activated in tStatus field const byte STATUS_TYPE = 0x7f; // Mask for turnout type in tStatus field @@ -91,13 +92,13 @@ class Turnout { public: static Turnout *firstTurnout; static int turnoutlistHash; - TurnoutData data; Turnout *nextTurnout; - 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 bool isClosed(int); + static bool setClosed(int n, bool closed); // return false if not found. + static void setClosedStateOnly(int n, bool closed); + int getId(); static void load(); static void store(); static Turnout *createServo(int id , VPIN vpin , uint16_t activeAngle, uint16_t inactiveAngle, uint8_t profile=1, uint8_t initialState=0); @@ -106,9 +107,6 @@ public: 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 @@ -116,6 +114,12 @@ public: #endif private: int num; // EEPROM address of tStatus in TurnoutData struct, or zero if not stored. -}; // Turnout + TurnoutData data; + static bool activate(int n, bool thrown); + static bool isActive(int); + bool isActive(); + void activate(bool state); + void setActive(bool state); + }; // Turnout #endif diff --git a/WiThrottle.cpp b/WiThrottle.cpp index 3aab78e..e9cfdfd 100644 --- a/WiThrottle.cpp +++ b/WiThrottle.cpp @@ -121,7 +121,8 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { if (turnoutListHash != Turnout::turnoutlistHash) { StringFormatter::send(stream,F("PTL")); for(Turnout *tt=Turnout::firstTurnout;tt!=NULL;tt=tt->nextTurnout){ - StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), tt->data.id, tt->data.id, Turnout::isActive(tt->data.id)?'4':'2'); + int id=tt->getId(); + StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), id, id, Turnout::isClosed(id)?'2':'4'); } StringFormatter::send(stream,F("\n")); turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison @@ -160,7 +161,6 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { #endif else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle int id=getInt(cmd+4); - byte newstate=2; // newstate can be 0,1 or 2. 2 is "invalid". Turnout * tt=Turnout::get(id); if (!tt) { // If turnout does not exist, create it @@ -170,17 +170,22 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { StringFormatter::send(stream, F("HmTurnout %d created\n"),id); } switch (cmd[3]) { - // T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging. - case 'T': newstate=0; break; - case 'C': newstate=1; break; - case '2': newstate=!Turnout::isActive(id); break; - default : /* newstate still invalid */ break; + // T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging. + case 'T': + Turnout::setClosed(id,false); + break; + case 'C': + Turnout::setClosed(id,true); + break; + case '2': + Turnout::setClosed(id,!Turnout::isClosed(id)); + break; + default : + Turnout::setClosed(id,true); + break; } - if (newstate != 2) { - Turnout::activate(id,newstate); - StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',id ); - } - } + StringFormatter::send(stream, F("PTA%c%d\n"),Turnout::isClosed(id)?'2':'4',id ); + } break; case 'N': // Heartbeat (2), only send if connection completed by 'HU' message if (initSent) { @@ -194,7 +199,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)); - StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{4\n")); + StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{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); From 776a098a724f941001a1c7ec8239122a3a508818 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 19 Aug 2021 20:17:48 +0100 Subject: [PATCH 027/125] Bump EESTORE_ID version. TurnoutData struct size has been reduced by one byte during rewrite of Turnout class. Consequently, this renders any previous turnout definitions in EEPROM incompatible with the new format. For safety, the version is increased so that incompatible EEPROM contents are discarded. --- EEStore.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EEStore.h b/EEStore.h index 247e30a..8fc98bd 100644 --- a/EEStore.h +++ b/EEStore.h @@ -29,7 +29,7 @@ extern ExternalEEPROM EEPROM; #include #endif -#define EESTORE_ID "DCC++0" +#define EESTORE_ID "DCC++1" struct EEStoreData{ char id[sizeof(EESTORE_ID)]; From fd36ca2b92abb54ac31bc121aaf757ee659e6574 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 19 Aug 2021 21:22:59 +0100 Subject: [PATCH 028/125] Restructure Turnout class. Turnout class split into a base class for common code and specific subclasses for Servo, DCC, VPIN and LCN turnouts. Interface further narrowed to reduce direct access to member variables. Turnout creation command handling has been moved into the DCCEXParser class. Turnout function and parameter names changed to make the Throw and Close functionality explicit. Turnout commands (close) and (throw) added. --- DCCEXParser.cpp | 66 +++++- LCN.cpp | 3 +- RMFT2.cpp | 6 +- Turnouts.cpp | 482 ++++++++++++------------------------------ Turnouts.h | 549 +++++++++++++++++++++++++++++++++++++++--------- WiThrottle.cpp | 7 +- 6 files changed, 645 insertions(+), 468 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 816cf85..4a27514 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -56,7 +56,8 @@ const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_RESET = 26133; const int16_t HASH_KEYWORD_SPEED28 = -17064; const int16_t HASH_KEYWORD_SPEED128 = 25816; -const int16_t HASH_KEYWORD_SERVO = 27709; +const int16_t HASH_KEYWORD_SERVO=27709; +const int16_t HASH_KEYWORD_VPIN=-415; int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS]; bool DCCEXParser::stashBusy; @@ -658,7 +659,7 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) case 0: // list turnout definitions { bool gotOne = false; - for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout) + for (Turnout *tt = Turnout::first(); tt != NULL; tt = tt->next()) { gotOne = true; tt->print(stream); @@ -672,17 +673,62 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) StringFormatter::send(stream, F("\n")); return true; - case 2: // turnout 0=CLOSE,1=THROW - if (p[1]>1 || p[1]<0 ) return false; - if (!Turnout::setClosed(p[0],p[1]==0)) return false; + case 2: // + switch (p[1]) { +#ifdef TURNOUT_LEGACY_BEHAVIOUR + // turnout 1 or T=THROW, 0 or C=CLOSE + case 1: case 0x54: // 1 or T + if (!Turnout::setClosed(p[0], false)) return false; + break; + case 0: case 0x43: // 0 or C + if (!Turnout::setClosed(p[0], true)) return false; + break; +#else + // turnout 0 or T=THROW,1 or C=CLOSE + case 0: case 0x54: // 0 or T + if (!Turnout::setClosed(p[0], false)) return false; + break; + case 1: case 0x43: // 1 or C + if (!Turnout::setClosed(p[0], true)) return false; + break; +#endif + default: + return false; + } + // Send acknowledgement to caller, and to Serial. StringFormatter::send(stream, F("\n"), p[0], p[1]); + if (stream != &Serial) StringFormatter::send(Serial, F("\n"), p[0], p[1]); return true; - default: // Anything else is handled by Turnout class. - if (!Turnout::create(p[0], params-1, &p[1])) + default: // Anything else is some kind of create function. + if (p[1] == HASH_KEYWORD_SERVO) { // + if (params == 6) { + if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5])) return false; - StringFormatter::send(stream, F("\n")); - return true; + } else + return false; + } else + if (p[1] == HASH_KEYWORD_VPIN) { // + if (params==3) { + if (VpinTurnout::create(p[0], p[2])) return false; + } else + return false; + } else + if (p[1]==HASH_KEYWORD_DCC) { + if (params==4 && p[2]>0 && p[2]<=512 && p[3]>=0 && p[3]<4) { // + if (!DCCTurnout::create(p[0], p[2], p[3])) return false; + } else if (params==3 && p[2]>0 && p[2]<=512*4) { // + if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false; + } else + return false; + } else if (params==3) { // for DCC or LCN + if (!DCCTurnout::create(p[0], p[1], p[2])) return false; + } + else if (params==3) { // legacy for Servo + if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false; + } + StringFormatter::send(stream, F("\n")); + return true; } } @@ -797,7 +843,7 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) StringFormatter::send(stream, F("128 Speedsteps")); return true; - case HASH_KEYWORD_SERVO: + case HASH_KEYWORD_SERVO: // IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); break; diff --git a/LCN.cpp b/LCN.cpp index e9d0886..16b3f3f 100644 --- a/LCN.cpp +++ b/LCN.cpp @@ -48,8 +48,7 @@ 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) tt=Turnout::createLCN(id); + if (!Turnout::exists(id)) LCNTurnout::create(id); Turnout::setClosedStateOnly(id,ch=='t'); Turnout::turnoutlistHash++; // signals ED update of turnout data id = 0; diff --git a/RMFT2.cpp b/RMFT2.cpp index 5e4fd62..8e155e4 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -77,7 +77,7 @@ byte RMFT2::flags[MAX_FLAGS]; VPIN id=GET_OPERAND(0); int addr=GET_OPERAND(1); byte subAddr=GET_OPERAND(2); - Turnout::createDCC(id,addr,subAddr); + DCCTurnout::create(id,addr,subAddr); continue; } @@ -87,14 +87,14 @@ byte RMFT2::flags[MAX_FLAGS]; int activeAngle=GET_OPERAND(2); int inactiveAngle=GET_OPERAND(3); int profile=GET_OPERAND(4); - Turnout::createServo(id,pin,activeAngle,inactiveAngle,profile); + ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile); continue; } if (opcode==OPCODE_PINTURNOUT) { int16_t id=GET_OPERAND(0); VPIN pin=GET_OPERAND(1); - Turnout::createVpin(id,pin); + VpinTurnout::create(id,pin); continue; } // other opcodes are not needed on this pass diff --git a/Turnouts.cpp b/Turnouts.cpp index c18337b..bd5fd5b 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -1,4 +1,5 @@ /* + * © 2021 Restructured Neil McKechnie * © 2013-2016 Gregg E. Berman * © 2020, Chris Harlow. All rights reserved. * © 2020, Harald Barth. @@ -19,17 +20,12 @@ * along with CommandStation. If not, see . */ -// >>>>>> ATTENTION: This class requires major cleaning. -// The public interface has been narrowed to avoid the ambuguity of "activated". - - -//#define EESTOREDEBUG #include "defines.h" -#include "Turnouts.h" #include "EEStore.h" #include "StringFormatter.h" #include "RMFT2.h" +#include "Turnouts.h" #ifdef EESTOREDEBUG #include "DIAG.h" #endif @@ -39,370 +35,150 @@ 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, -}; + /* + * Protected static data + */ + Turnout *Turnout::_firstTurnout = 0; - - -/////////////////////////////////////////////////////////////////////////////// -// Static function to print all Turnout states to stream in form "" - -void Turnout::printAll(Print *stream){ - for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout) - StringFormatter::send(stream, F("\n"), tt->data.id, tt->data.active); -} // Turnout::printAll - -/////////////////////////////////////////////////////////////////////////////// -// Object method to print configuration of one Turnout to stream, in one of the following forms: -// -// -// -// - -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("\n"), data.id, state); - break; - case TURNOUT_DCC: - // DCC Turnout - StringFormatter::send(stream, F("\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("\n"), data.id, data.vpinData.vpin, state); - break; - case TURNOUT_SERVO: - // Servo Turnout - StringFormatter::send(stream, F("\n"), data.id, data.servoData.vpin, - data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, state); - break; - default: - break; - } -} - - -// Public interface to turnout throw/close -bool Turnout::setClosed(int id, bool closed) { - // hides the internal activate argument to a single place - return activate(id, closed? false: true ); /// Needs cleaning up -} -bool Turnout::isClosed(int id) { - // hides the internal activate argument to a single place - return !isActive(id); /// Needs cleaning up -} -int Turnout::getId() { - return data.id; -} - -/////////////////////////////////////////////////////////////////////////////// -// Static function to activate/deactivate Turnout with ID 'n'. -// Returns false if turnout not found. - + /* + * Public static data + */ + int Turnout::turnoutlistHash = 0; -bool Turnout::activate(int n, bool state){ - Turnout * tt=get(n); - if (!tt) return false; - tt->activate(state); - turnoutlistHash++; - return true; -} + /* + * Protected static functions + */ -/////////////////////////////////////////////////////////////////////////////// -// 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) 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.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. - } - data.active = state; - switch (data.type) { - case TURNOUT_DCC: - DCC::setAccessory((((data.dccAccessoryData.address-1) >> 2) + 1), - ((data.dccAccessoryData.address-1) & 3), state); - break; - case TURNOUT_SERVO: -#ifndef IO_NO_HAL - IODevice::write(data.servoData.vpin, state); -#endif - break; - 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; - - for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextTurnout); - - if(tt==NULL) return false; - - if(tt==firstTurnout) - firstTurnout=tt->nextTurnout; - else - pp->nextTurnout=tt->nextTurnout; - - free(tt); - turnoutlistHash++; - return true; -} - -/////////////////////////////////////////////////////////////////////////////// -// 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=NULL; - - for(uint16_t i=0;idata.nTurnouts;i++){ - // 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; - case TURNOUT_SERVO: - tt=createServo(data.id, data.servoData.vpin, - data.servoData.activePosition, data.servoData.inactivePosition, data.servoData.profile, lastKnownState); - break; - 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 - if (tt) print(tt); -#endif - } -} - -/////////////////////////////////////////////////////////////////////////////// -// Static function to store all Turnout definitions to EEPROM - -void Turnout::store(){ - Turnout *tt; - - tt=firstTurnout; - EEStore::eeStore->data.nTurnouts=0; - - while(tt!=NULL){ - // LCN turnouts aren't saved to EEPROM - if (tt->data.type != TURNOUT_LCN) { -#ifdef EESTOREDEBUG - 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(tt->data.size); - EEStore::eeStore->data.nTurnouts++; - } - tt=tt->nextTurnout; - } -} - -/////////////////////////////////////////////////////////////////////////////// -// Static function for creating a DCC-controlled Turnout. - -Turnout *Turnout::createDCC(int id, uint16_t add, uint8_t subAdd){ - if (add > 511 || subAdd > 3) return NULL; - Turnout *tt=create(id); - 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); -} - -/////////////////////////////////////////////////////////////////////////////// -// Static function for creating a LCN-controlled Turnout. - -Turnout *Turnout::createLCN(int id, uint8_t state) { - Turnout *tt=create(id); - 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); -} - -/////////////////////////////////////////////////////////////////////////////// -// 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){ -#ifndef IO_NO_HAL - 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); + Turnout *Turnout::get(uint16_t id) { + // Find turnout object from list. + for (Turnout *tt = _firstTurnout; tt != NULL; tt = tt->_nextTurnout) + if (tt->_turnoutData.id == id) return tt; return NULL; } - return(tt); -#else - (void)id; (void)vpin; (void)activePosition; (void)inactivePosition; (void)profile; (void)state; // avoid compiler warnings - return NULL; -#endif -} -/////////////////////////////////////////////////////////////////////////////// -// Support for -// and -// and + // Add new turnout to end of chain + void Turnout::add(Turnout *tt) { + if (!_firstTurnout) + _firstTurnout = tt; + else { + // Find last object on chain + Turnout *ptr = _firstTurnout; + for ( ; ptr->_nextTurnout!=0; ptr=ptr->_nextTurnout) {} + // Line new object to last object. + ptr->_nextTurnout = tt; + } + turnoutlistHash++; + } + + // Remove nominated turnout from turnout linked list and delete the object. + bool Turnout::remove(uint16_t id) { + Turnout *tt,*pp=NULL; -Turnout *Turnout::create(int id, int params, int16_t p[]) { - if (p[0] == HASH_KEYWORD_SERVO) { // - 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 - if (p[0] == HASH_KEYWORD_VPIN) { // - if (params==2) - return createVpin(id, p[1]); + for(tt=_firstTurnout; tt!=NULL && tt->_turnoutData.id!=id; pp=tt, tt=tt->_nextTurnout) {} + if (tt == NULL) return false; + + if (tt == _firstTurnout) + _firstTurnout = tt->_nextTurnout; 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) // - return createDCC(id, p[1], p[2]); - else if (params==2 && p[1]>0 && p[1]<=512*4) // - return createDCC(id, (p[1]-1)/4+1, (p[1]-1)%4); - else - return NULL; - } else if (params==2) { // for DCC or LCN - return createDCC(id, p[0], p[1]); + pp->_nextTurnout = tt->_nextTurnout; + + delete (ServoTurnout *)tt; + + turnoutlistHash++; + return true; } - else if (params==3) { // legacy for Servo - return createServo(id, (VPIN)p[0], (uint16_t)p[1], (uint16_t)p[2]); + + + /* + * Public static functions + */ + + bool Turnout::isClosed(uint16_t id) { + Turnout *tt = get(id); + if (tt) + return tt->isClosed(); + else + return false; } - return NULL; -} + // Static activate function is invoked from close(), throw() etc. to perform the + // common parts of the turnout operation. Code which is specific to a turnout + // type should be placed in the polymorphic virtual function activate(bool) which is + // called from here. + bool Turnout::activate(uint16_t id, bool closeFlag) { + #ifdef EESTOREDEBUG + if (closeFlag) + DIAG(F("Turnout::close(%d)"), id); + else + DIAG(F("Turnout::throw(%d)"), id); + #endif + Turnout *tt = Turnout::get(id); + if (!tt) return false; + bool ok = tt->activate(closeFlag); -/////////////////////////////////////////////////////////////////////////////// -// Create basic Turnout object. The details of what sort of object it is -// controlling are not set here. + // Write new closed/thrown state to EEPROM if required. Note that eepromAddress + // is always zero for LCN turnouts. + if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress) + EEPROM.put(tt->_eepromAddress, tt->_turnoutData.closed); -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; + #if defined(RMFT_ACTIVE) + // TODO: Check that the inversion is correct here! + RMFT2::turnoutEvent(id, !closeFlag); + #endif + + return ok; } - turnoutlistHash++; - return tt; -} -/////////////////////////////////////////////////////////////////////////////// -// -// Object method to print debug info about the state of a Turnout object -// + // Load all turnout objects + void Turnout::load() { + for (uint16_t i=0; idata.nTurnouts; i++) { + Turnout::loadTurnout(); + } + } + + // Save all turnout objects + void Turnout::store() { + EEStore::eeStore->data.nTurnouts=0; + for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) { + tt->save(); + EEStore::eeStore->data.nTurnouts++; + } + } + + // Load one turnout from EEPROM + Turnout *Turnout::loadTurnout () { + Turnout *tt; + // Read turnout type from EEPROM + struct TurnoutData turnoutData; + int eepromAddress = EEStore::pointer(); // Address of byte containing the _closed flag. + EEPROM.get(EEStore::pointer(), turnoutData); + EEStore::advance(sizeof(turnoutData)); + + switch (turnoutData.turnoutType) { + case TURNOUT_SERVO: + // Servo turnout + tt = ServoTurnout::load(&turnoutData); + break; + case TURNOUT_DCC: + // DCC Accessory turnout + tt = DCCTurnout::load(&turnoutData); + break; + case TURNOUT_VPIN: + // VPIN turnout + tt = VpinTurnout::load(&turnoutData); + break; + } + if (!tt) { + // Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero. + tt->_eepromAddress = eepromAddress; + add(tt); + } + #ifdef EESTOREDEBUG -void Turnout::print(Turnout *tt) { - tt->print(StringFormatter::diagSerial); -} + printAll(&Serial); #endif + return tt; + } -/////////////////////////////////////////////////////////////////////////////// -Turnout *Turnout::firstTurnout=NULL; -int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists diff --git a/Turnouts.h b/Turnouts.h index db73214..152583e 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -1,4 +1,6 @@ /* + * © 2021 Restructured Neil McKechnie + * © 2013-2016 Gregg E. Berman * © 2020, Chris Harlow. All rights reserved. * * This file is part of Asbelos DCC API @@ -17,109 +19,464 @@ * along with CommandStation. If not, see . */ -/* - * 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 +//#define EESTOREDEBUG +#include "defines.h" +#include "EEStore.h" +#include "StringFormatter.h" +#include "RMFT2.h" +#ifdef EESTOREDEBUG +#include "DIAG.h" +#endif -#include #include "DCC.h" #include "LCN.h" -#include "IODevice.h" - -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 { - // 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; - }; +// Turnout type definitions +enum { + TURNOUT_DCC = 1, + TURNOUT_SERVO = 2, + TURNOUT_VPIN = 3, + TURNOUT_LCN = 4, }; +/************************************************************************************* + * Turnout - Base class for turnouts. + * + *************************************************************************************/ + class Turnout { -public: - static Turnout *firstTurnout; - static int turnoutlistHash; - Turnout *nextTurnout; - static Turnout* get(int); - static bool remove(int); - static bool isClosed(int); - static bool setClosed(int n, bool closed); // return false if not found. - static void setClosedStateOnly(int n, bool closed); - int getId(); - static void load(); - static void store(); - 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); - static void printAll(Print *); - void print(Print *stream); -#ifdef EESTOREDEBUG - static void print(Turnout *tt); -#endif -private: - int num; // EEPROM address of tStatus in TurnoutData struct, or zero if not stored. - TurnoutData data; - static bool activate(int n, bool thrown); - static bool isActive(int); - bool isActive(); - void activate(bool state); - void setActive(bool state); - }; // Turnout +protected: + /* + * Object data + */ + + // The TurnoutData struct contains data common to all turnout types, that + // is written to EEPROM when the turnout is saved. + // The first byte of this struct contains the 'closed' flag which is + // updated whenever the turnout changes from thrown to closed and + // vice versa. If the turnout has been saved, then this byte is rewritten + // when changed in RAM. The 'closed' flag must be located in the first byte. + struct TurnoutData { + bool closed : 1; + bool _rfu: 2; + uint8_t turnoutType : 5; + uint16_t id; + } _turnoutData; // 3 bytes + + // Address in eeprom of first byte of the _turnoutData struct (containing the closed flag). + // Set to zero if the object has not been saved in EEPROM, e.g. for newly created Turnouts, and + // for all LCN turnouts. + uint16_t _eepromAddress = 0; + + // Pointer to next turnout on linked list. + Turnout *_nextTurnout = 0; + + /* + * Constructor + */ + Turnout(uint16_t id, uint8_t turnoutType, bool closed) { + _turnoutData.id = id; + _turnoutData.turnoutType = turnoutType; + _turnoutData.closed = closed; + add(this); + } + + /* + * Static data + */ + + static Turnout *_firstTurnout; + static int _turnoutlistHash; + + /* + * Virtual functions + */ + + virtual bool activate(bool close) = 0; // Mandatory in subclass + virtual void save() {} + /* + * Static functions + */ + + static Turnout *get(uint16_t id); + + static void add(Turnout *tt); + +public: + /* + * Static data + */ + static int turnoutlistHash; + + /* + * Public base class functions + */ + inline bool isClosed() { return _turnoutData.closed; }; + inline bool isThrown() { return !_turnoutData.closed; } + inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; } + inline uint16_t getId() { return _turnoutData.id; } + inline Turnout *next() { return _nextTurnout; } + /* + * Virtual functions + */ + virtual void print(Print *stream) {} + virtual ~Turnout() {} // Destructor + + /* + * Public static functions + */ + inline static bool exists(uint16_t id) { return get(id) != 0; } + + static bool remove(uint16_t id); + + static bool isClosed(uint16_t id); + + inline static bool isThrown(uint16_t id) { + return !isClosed(id); + } + + static bool activate(uint16_t id, bool closeFlag); + + inline static bool setClosed(uint16_t id) { + return activate(id, true); + } + + inline static bool setThrown(uint16_t id) { + return activate(id, false); + } + + inline static bool setClosed(uint16_t id, bool close) { + return activate(id, close); + } + + static bool setClosedStateOnly(uint16_t id, bool close) { + Turnout *tt = get(id); + if (tt) return false; + tt->_turnoutData.closed = close; + return true; + } + + inline static Turnout *first() { return _firstTurnout; } + + // Load all turnout definitions. + static void load(); + // Load one turnout definition + static Turnout *loadTurnout(); + // Save all turnout definitions + static void store(); + + static void printAll(Print *stream) { + for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) + tt->print(stream); + } +}; + + +/************************************************************************************* + * ServoTurnout - Turnout controlled by servo device. + * + *************************************************************************************/ +class ServoTurnout : public Turnout { +private: + // ServoTurnoutData contains data specific to this subclass that is + // written to EEPROM when the turnout is saved. + struct ServoTurnoutData { + VPIN vpin; + uint16_t closedPosition : 12; + uint16_t thrownPosition : 12; + uint8_t profile; + } _servoTurnoutData; // 6 bytes + +public: + // Constructor + ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) : + Turnout(id, TURNOUT_SERVO, closed) + { + _servoTurnoutData.vpin = vpin; + _servoTurnoutData.thrownPosition = thrownPosition; + _servoTurnoutData.closedPosition = closedPosition; + _servoTurnoutData.profile = profile; + } + + // Create function + static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) { +#ifndef IO_NO_HAL + Turnout *tt = get(id); + if (!tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_SERVO)) { + // Yes, so set parameters + ServoTurnout *st = (ServoTurnout *)tt; + st->_servoTurnoutData.vpin = vpin; + st->_servoTurnoutData.thrownPosition = thrownPosition; + st->_servoTurnoutData.closedPosition = closedPosition; + st->_servoTurnoutData.profile = profile; + // Don't touch the _closed parameter, retain the original value. + + // We don't really need to do the following, since a call to IODevice::_writeAnalogue + // will provide all the data that is required! + // int params[] = {(int)thrownPosition, (int)closedPosition, profile, closed}; + // IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, params); + + // Set position to saved position + IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); + + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed); + IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); + return tt; +#else + return NULL; #endif + } + + bool activate(bool close) override { +#ifndef IO_NO_HAL + IODevice::writeAnalogue(_servoTurnoutData.vpin, + close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile); + _turnoutData.closed = close; +#endif + return true; + } + + void save() override { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _servoTurnoutData); + EEStore::advance(sizeof(_servoTurnoutData)); + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, + _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, _turnoutData.closed); + } + + // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + ServoTurnoutData servoTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), servoTurnoutData); + EEStore::advance(sizeof(servoTurnoutData)); + + // Create new object + ServoTurnout *tt = new ServoTurnout(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition, + servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed); + + return tt; + } +}; + +/************************************************************************************* + * DCCTurnout - Turnout controlled by DCC Accessory Controller. + * + *************************************************************************************/ +class DCCTurnout : public Turnout { +private: + // DCCTurnoutData contains data specific to this subclass that is + // written to EEPROM when the turnout is saved. + struct DCCTurnoutData { + // 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. + } _dccTurnoutData; // 2 bytes + +public: + // Constructor + DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : + Turnout(id, TURNOUT_DCC, false) + { + _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; + } + + // Create function + static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd) { + Turnout *tt = get(id); + if (!tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_DCC)) { + // Yes, so set parameters + DCCTurnout *dt = (DCCTurnout *)tt; + dt->_dccTurnoutData.address = ((add-1) << 2) + subAdd + 1; + // Don't touch the _closed parameter, retain the original value. + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new DCCTurnout(id, add, subAdd); + return tt; + } + + bool activate(bool close) override { + DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), + ((_dccTurnoutData.address-1) & 3), close); + _turnoutData.closed = close; + return true; + } + + void save() override { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _dccTurnoutData); + EEStore::advance(sizeof(_dccTurnoutData)); + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), _turnoutData.closed); + } + + // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + DCCTurnoutData dccTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), dccTurnoutData); + EEStore::advance(sizeof(dccTurnoutData)); + + // Create new object + DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); + + return tt; + } +}; + + +/************************************************************************************* + * VpinTurnout - Turnout controlled through a HAL vpin. + * + *************************************************************************************/ +class VpinTurnout : public Turnout { +private: + // VpinTurnoutData contains data specific to this subclass that is + // written to EEPROM when the turnout is saved. + struct VpinTurnoutData { + VPIN vpin; + } _vpinTurnoutData; // 2 bytes + +public: + // Constructor + VpinTurnout(uint16_t id, VPIN vpin, bool closed=true) : + Turnout(id, TURNOUT_VPIN, closed) + { + _vpinTurnoutData.vpin = vpin; + } + + // Create function + static Turnout *create(uint16_t id, VPIN vpin, bool closed=true) { + Turnout *tt = get(id); + if (!tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_VPIN)) { + // Yes, so set parameters + VpinTurnout *vt = (VpinTurnout *)tt; + vt->_vpinTurnoutData.vpin = vpin; + // Don't touch the _closed parameter, retain the original value. + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new VpinTurnout(id, vpin, closed); + return tt; + } + + bool activate(bool close) override { + IODevice::write(_vpinTurnoutData.vpin, close); + _turnoutData.closed = close; + return true; + } + + void save() override { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _vpinTurnoutData); + EEStore::advance(sizeof(_vpinTurnoutData)); + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, + _vpinTurnoutData.vpin, _turnoutData.closed); + } + + // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + VpinTurnoutData vpinTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), vpinTurnoutData); + EEStore::advance(sizeof(vpinTurnoutData)); + + // Create new object + VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed); + + return tt; + } +}; + + +/************************************************************************************* + * LCNTurnout - Turnout controlled by Loconet + * + *************************************************************************************/ +class LCNTurnout : public Turnout { +private: + // LCNTurnout has no specific data, and in any case is not written to EEPROM! + // struct LCNTurnoutData { + // } _lcnTurnoutData; // 0 bytes + +public: + // Constructor + LCNTurnout(uint16_t id, bool closed=true) : + Turnout(id, TURNOUT_LCN, closed) + { } + + // Create function + static Turnout *create(uint16_t id, bool closed=true) { + Turnout *tt = get(id); + if (!tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_LCN)) { + // Yes, so return this object + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new LCNTurnout(id, closed); + return tt; + } + + bool activate(bool close) override { + LCN::send('T', _turnoutData.id, close); + // The _turnoutData.closed flag should be updated by a message from the LCN master, later. + return true; + } + + // LCN turnouts not saved to EEPROM. + //void save() override { } + //static Turnout *load(struct TurnoutData *turnoutData) { + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _turnoutData.closed); + } + +}; + diff --git a/WiThrottle.cpp b/WiThrottle.cpp index e9cfdfd..07017ba 100644 --- a/WiThrottle.cpp +++ b/WiThrottle.cpp @@ -120,7 +120,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { // Send turnout list if changed since last sent (will replace list on client) if (turnoutListHash != Turnout::turnoutlistHash) { StringFormatter::send(stream,F("PTL")); - for(Turnout *tt=Turnout::firstTurnout;tt!=NULL;tt=tt->nextTurnout){ + for(Turnout *tt=Turnout::first();tt!=NULL;tt=tt->next()){ int id=tt->getId(); StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), id, id, Turnout::isClosed(id)?'2':'4'); } @@ -161,12 +161,11 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) { #endif else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle int id=getInt(cmd+4); - Turnout * tt=Turnout::get(id); - if (!tt) { + if (!Turnout::exists(id)) { // If turnout does not exist, create it int addr = ((id - 1) / 4) + 1; int subaddr = (id - 1) % 4; - Turnout::createDCC(id,addr,subaddr); + DCCTurnout::create(id,addr,subaddr); StringFormatter::send(stream, F("HmTurnout %d created\n"),id); } switch (cmd[3]) { From 7f6173825fa264b82d87e430adcbd27018278278 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 19 Aug 2021 21:43:55 +0100 Subject: [PATCH 029/125] Various corrections to Turnout code. --- DCCEXParser.cpp | 2 +- Turnouts.cpp | 14 ++++++++------ Turnouts.h | 9 +++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 4a27514..388cf60 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -710,7 +710,7 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) } else if (p[1] == HASH_KEYWORD_VPIN) { // if (params==3) { - if (VpinTurnout::create(p[0], p[2])) return false; + if (!VpinTurnout::create(p[0], p[2])) return false; } else return false; } else diff --git a/Turnouts.cpp b/Turnouts.cpp index bd5fd5b..0869a58 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -105,7 +105,7 @@ const int16_t HASH_KEYWORD_VPIN=-415; // Static activate function is invoked from close(), throw() etc. to perform the // common parts of the turnout operation. Code which is specific to a turnout - // type should be placed in the polymorphic virtual function activate(bool) which is + // type should be placed in the virtual function activate(bool) which is // called from here. bool Turnout::activate(uint16_t id, bool closeFlag) { #ifdef EESTOREDEBUG @@ -124,8 +124,7 @@ const int16_t HASH_KEYWORD_VPIN=-415; EEPROM.put(tt->_eepromAddress, tt->_turnoutData.closed); #if defined(RMFT_ACTIVE) - // TODO: Check that the inversion is correct here! - RMFT2::turnoutEvent(id, !closeFlag); + RMFT2::turnoutEvent(id, closeFlag); #endif return ok; @@ -149,10 +148,10 @@ const int16_t HASH_KEYWORD_VPIN=-415; // Load one turnout from EEPROM Turnout *Turnout::loadTurnout () { - Turnout *tt; + Turnout *tt = 0; // Read turnout type from EEPROM struct TurnoutData turnoutData; - int eepromAddress = EEStore::pointer(); // Address of byte containing the _closed flag. + int eepromAddress = EEStore::pointer(); // Address of byte containing the closed flag. EEPROM.get(EEStore::pointer(), turnoutData); EEStore::advance(sizeof(turnoutData)); @@ -169,11 +168,14 @@ const int16_t HASH_KEYWORD_VPIN=-415; // VPIN turnout tt = VpinTurnout::load(&turnoutData); break; + default: + // If we find anything else, then we don't know what it is or how long it is, + // so we can't go any further through the EEPROM! + return NULL; } if (!tt) { // Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero. tt->_eepromAddress = eepromAddress; - add(tt); } #ifdef EESTOREDEBUG diff --git a/Turnouts.h b/Turnouts.h index 152583e..6f6c408 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -204,7 +204,7 @@ public: static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) { #ifndef IO_NO_HAL Turnout *tt = get(id); - if (!tt) { + if (tt) { // Object already exists, check if it is usable if (tt->isType(TURNOUT_SERVO)) { // Yes, so set parameters @@ -237,6 +237,7 @@ public: #endif } + // ServoTurnout-specific code for throwing or closing a servo turnout. bool activate(bool close) override { #ifndef IO_NO_HAL IODevice::writeAnalogue(_servoTurnoutData.vpin, @@ -301,7 +302,7 @@ public: // Create function static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd) { Turnout *tt = get(id); - if (!tt) { + if (tt) { // Object already exists, check if it is usable if (tt->isType(TURNOUT_DCC)) { // Yes, so set parameters @@ -378,7 +379,7 @@ public: // Create function static Turnout *create(uint16_t id, VPIN vpin, bool closed=true) { Turnout *tt = get(id); - if (!tt) { + if (tt) { // Object already exists, check if it is usable if (tt->isType(TURNOUT_VPIN)) { // Yes, so set parameters @@ -450,7 +451,7 @@ public: // Create function static Turnout *create(uint16_t id, bool closed=true) { Turnout *tt = get(id); - if (!tt) { + if (tt) { // Object already exists, check if it is usable if (tt->isType(TURNOUT_LCN)) { // Yes, so return this object From b4a3b503bcc3f6c5b6dd4ac3eeec5e8e65feb616 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 20 Aug 2021 00:07:50 +0100 Subject: [PATCH 030/125] Turnout notification handling enhanced. Ensure that the message is sent on the serial USB (to JMRI) whenever the turnout is closed or thrown, even if the request didn't originate on the serial USB. --- DCCEXParser.cpp | 10 +++++++--- Turnouts.cpp | 7 ++++++- Turnouts.h | 3 ++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 388cf60..6779178 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -679,25 +679,29 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) // turnout 1 or T=THROW, 0 or C=CLOSE case 1: case 0x54: // 1 or T if (!Turnout::setClosed(p[0], false)) return false; + p[1] = 1; break; case 0: case 0x43: // 0 or C if (!Turnout::setClosed(p[0], true)) return false; + p[1] = 0; break; #else // turnout 0 or T=THROW,1 or C=CLOSE case 0: case 0x54: // 0 or T if (!Turnout::setClosed(p[0], false)) return false; + p[1] = 0; break; case 1: case 0x43: // 1 or C if (!Turnout::setClosed(p[0], true)) return false; + p[1] = 1; break; #endif default: return false; } - // Send acknowledgement to caller, and to Serial. - StringFormatter::send(stream, F("\n"), p[0], p[1]); - if (stream != &Serial) StringFormatter::send(Serial, F("\n"), p[0], p[1]); + // Send acknowledgement to caller if the command was not received over Serial (acknowledgement + // messages on Serial are sent by the Turnout class). + if (stream != &Serial) StringFormatter::send(stream, F("\n"), p[0], p[1]); return true; default: // Anything else is some kind of create function. diff --git a/Turnouts.cpp b/Turnouts.cpp index 0869a58..8a77f57 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -125,7 +125,12 @@ const int16_t HASH_KEYWORD_VPIN=-415; #if defined(RMFT_ACTIVE) RMFT2::turnoutEvent(id, closeFlag); - #endif + #endif + + // Send message to JMRI etc. over Serial USB. This is done here + // to ensure that the message is sent when the turnout operation + // is not initiated by a Serial command. + StringFormatter::send(Serial, F("\n"), id, closeFlag); return ok; } diff --git a/Turnouts.h b/Turnouts.h index 6f6c408..6659c24 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -466,7 +466,8 @@ public: } bool activate(bool close) override { - LCN::send('T', _turnoutData.id, close); + // Assume that the LCN command still uses 1 for throw and 0 for close... + LCN::send('T', _turnoutData.id, !close); // The _turnoutData.closed flag should be updated by a message from the LCN master, later. return true; } From 482f4b1c79539d5d3615f00eb1979184888855be Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 20 Aug 2021 14:36:18 +0100 Subject: [PATCH 031/125] Tidy up recent changes to Turnout class. --- DCCEXParser.cpp | 97 +++++++++++++++++++++++++------------------------ Turnouts.cpp | 27 ++++++++++---- Turnouts.h | 16 +++++--- 3 files changed, 79 insertions(+), 61 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 6779178..2c50ac1 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -58,6 +58,8 @@ const int16_t HASH_KEYWORD_SPEED28 = -17064; const int16_t HASH_KEYWORD_SPEED128 = 25816; const int16_t HASH_KEYWORD_SERVO=27709; const int16_t HASH_KEYWORD_VPIN=-415; +const int16_t HASH_KEYWORD_C=67; +const int16_t HASH_KEYWORD_T=84; int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS]; bool DCCEXParser::stashBusy; @@ -674,63 +676,62 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) return true; case 2: // - switch (p[1]) { -#ifdef TURNOUT_LEGACY_BEHAVIOUR - // turnout 1 or T=THROW, 0 or C=CLOSE - case 1: case 0x54: // 1 or T - if (!Turnout::setClosed(p[0], false)) return false; - p[1] = 1; - break; - case 0: case 0x43: // 0 or C - if (!Turnout::setClosed(p[0], true)) return false; - p[1] = 0; - break; -#else - // turnout 0 or T=THROW,1 or C=CLOSE - case 0: case 0x54: // 0 or T - if (!Turnout::setClosed(p[0], false)) return false; - p[1] = 0; - break; - case 1: case 0x43: // 1 or C - if (!Turnout::setClosed(p[0], true)) return false; - p[1] = 1; - break; -#endif - default: - return false; - } - // Send acknowledgement to caller if the command was not received over Serial (acknowledgement - // messages on Serial are sent by the Turnout class). - if (stream != &Serial) StringFormatter::send(stream, F("\n"), p[0], p[1]); - return true; + { + bool state = false; + switch (p[1]) { + // By default turnout command uses 0=throw, 1=close, + // but legacy DCC++ behaviour is 1=throw, 0=close. + case 0: + state = Turnout::useLegacyTurnoutBehaviour; + break; + case 1: + state = !Turnout::useLegacyTurnoutBehaviour; + break; + case HASH_KEYWORD_C: + state = true; + break; + case HASH_KEYWORD_T: + state= false; + break; + default: + return false; + } + if (!Turnout::setClosed(p[0], state)) return false; - default: // Anything else is some kind of create function. - if (p[1] == HASH_KEYWORD_SERVO) { // - if (params == 6) { - if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5])) - return false; - } else + // Send acknowledgement to caller if the command was not received over Serial + // (acknowledgement messages on Serial are sent by the Turnout class). + if (stream != &Serial) Turnout::printState(p[0], stream); + return true; + } + + default: // Anything else is some kind of turnout create function. + if (params == 6 && p[1] == HASH_KEYWORD_SERVO) { // + if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5])) return false; } else - if (p[1] == HASH_KEYWORD_VPIN) { // - if (params==3) { - if (!VpinTurnout::create(p[0], p[2])) return false; - } else - return false; - } else - if (p[1]==HASH_KEYWORD_DCC) { - if (params==4 && p[2]>0 && p[2]<=512 && p[3]>=0 && p[3]<4) { // + if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // + if (!VpinTurnout::create(p[0], p[2])) return false; + } else + if (params >= 3 && p[1] == HASH_KEYWORD_DCC) { + if (params==4 && p[2]>0 && p[2]<=512 && p[3]>=0 && p[3]<4) { // if (!DCCTurnout::create(p[0], p[2], p[3])) return false; - } else if (params==3 && p[2]>0 && p[2]<=512*4) { // + } else if (params==3 && p[2]>0 && p[2]<=512*4) { // , 1<=nn<=2048 if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false; } else return false; - } else if (params==3) { // for DCC or LCN - if (!DCCTurnout::create(p[0], p[1], p[2])) return false; + } else + if (params==3) { // legacy for DCC accessory + if (p[1]>0 && p[1]<=512 && p[2]>=0 && p[2]<4) { + if (!DCCTurnout::create(p[0], p[1], p[2])) return false; + } else + return false; } - else if (params==3) { // legacy for Servo + else + if (params==4) { // legacy for Servo if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false; - } + } else + return false; + StringFormatter::send(stream, F("\n")); return true; } diff --git a/Turnouts.cpp b/Turnouts.cpp index 8a77f57..86607f4 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -20,6 +20,14 @@ * along with CommandStation. If not, see . */ +#ifndef TURNOUTS_CPP +#define TURNOUTS_CPP + +// Set the following definition to true for = throw and = close +// or to false for = close and = throw (the original way). +#ifndef USE_LEGACY_TURNOUT_BEHAVIOUR +#define USE_LEGACY_TURNOUT_BEHAVIOUR false +#endif #include "defines.h" #include "EEStore.h" @@ -30,12 +38,6 @@ #include "DIAG.h" #endif -// 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; - - /* * Protected static data */ @@ -46,6 +48,7 @@ const int16_t HASH_KEYWORD_VPIN=-415; * Public static data */ int Turnout::turnoutlistHash = 0; + bool Turnout::useLegacyTurnoutBehaviour = USE_LEGACY_TURNOUT_BEHAVIOUR; /* * Protected static functions @@ -130,8 +133,7 @@ const int16_t HASH_KEYWORD_VPIN=-415; // Send message to JMRI etc. over Serial USB. This is done here // to ensure that the message is sent when the turnout operation // is not initiated by a Serial command. - StringFormatter::send(Serial, F("\n"), id, closeFlag); - + printState(id, &Serial); return ok; } @@ -189,3 +191,12 @@ const int16_t HASH_KEYWORD_VPIN=-415; return tt; } + // Display, on the specified stream, the current state of the turnout (1 or 0). + void Turnout::printState(uint16_t id, Print *stream) { + Turnout *tt = get(id); + if (!tt) + StringFormatter::send(stream, F("\n"), + id, tt->_turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + +#endif \ No newline at end of file diff --git a/Turnouts.h b/Turnouts.h index 6659c24..32a8a94 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -109,6 +109,7 @@ public: * Static data */ static int turnoutlistHash; + static bool useLegacyTurnoutBehaviour; /* * Public base class functions @@ -171,6 +172,8 @@ public: for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) tt->print(stream); } + + static void printState(uint16_t id, Print *stream); }; @@ -259,7 +262,8 @@ public: void print(Print *stream) override { StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, - _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, _turnoutData.closed); + _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); } // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. @@ -338,7 +342,8 @@ public: void print(Print *stream) override { StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), _turnoutData.closed); + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); } // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. @@ -413,8 +418,8 @@ public: } void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, - _vpinTurnoutData.vpin, _turnoutData.closed); + StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); } // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. @@ -477,7 +482,8 @@ public: //static Turnout *load(struct TurnoutData *turnoutData) { void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, _turnoutData.closed); + StringFormatter::send(stream, F("\n"), _turnoutData.id, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); } }; From 133c65bc4246236889eccffa18e73820c433eb8a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 20 Aug 2021 15:43:03 +0100 Subject: [PATCH 032/125] Report Turnout configuration in old and new formats. JMRI currently isn't aware of the newer types of turnout in DCC++EX, so when it receives the definitions of turnouts it barfs on them. It still knows a turnout exists, but isn't able to display its full configuration. For DCC Accessory turnouts, the configuration message has changed so that it includes the DCC string (to distinguish them from other types of turnout). To enable current and older versions of JMRI to continue working with DCC turnouts, CS now reports the old and new formats, i.e. and . It currently accepts the first one and ignores the second one, but in the fullness of time it might accept the second one too. --- Turnouts.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Turnouts.h b/Turnouts.h index 32a8a94..0a05dca 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -344,6 +344,10 @@ public: StringFormatter::send(stream, F("\n"), _turnoutData.id, (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), _turnoutData.closed ^ useLegacyTurnoutBehaviour); + // Also report using classic DCC++ syntax for DCC accessory turnouts + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); } // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. From ddcd40860f375de92cf6a8404b360494eafe9264 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Fri, 20 Aug 2021 19:18:30 +0100 Subject: [PATCH 033/125] UNTESTED Allow ALIAS of ROUTE/AUTOMATION id Runs ALIAS on first pass and creates a routine to emit the route stuff to withrottle because previous technique wouldnt compile for aliased ids. --- RMFT2.cpp | 10 +++++++++- RMFT2.h | 6 ++++-- RMFTMacros.h | 50 +++++++++++++++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 5e4fd62..07ba06e 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -243,7 +243,9 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { // 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); + StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL")); + emitWithrottleDescriptions(stream); + StringFormatter::send(stream,F("\n")); } @@ -682,4 +684,10 @@ void RMFT2::kill(const FSH * reason, int operand) { void RMFT2::printMessage2(const FSH * msg) { DIAG(F("EXRAIL(%d) %S"),loco,msg); } + +// This is called by emitRouteDescriptions to emit a withrottle description for a route or autoomation. +void RMFT2::emitRouteDescription(Print * stream, char type, int id, const FSH * description) { + StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"), + type,id,description, type=='R'?'2':'4'); +} diff --git a/RMFT2.h b/RMFT2.h index 77e21bc..e69696d 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -78,7 +78,9 @@ private: static int locateRouteStart(int16_t _route); static int progtrackLocoId; static void doSignal(VPIN id,bool red, bool amber, bool green); - + static void emitRouteDescription(Print * stream, char type, int id, const FSH * description); + static void emitWithrottleDescriptions(Print * stream); + static RMFT2 * loopTask; static RMFT2 * pausingTask; void delayMe(long millisecs); @@ -90,10 +92,10 @@ private: void kill(const FSH * reason=NULL,int operand=0); void printMessage(uint16_t id); // Built by RMFTMacros.h void printMessage2(const FSH * msg); + 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 diff --git a/RMFTMacros.h b/RMFTMacros.h index 2225426..7c6c9d2 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -19,6 +19,14 @@ #ifndef RMFTMacros_H #define RMFTMacros_H +// remove normal code LCD macro (will be restored later) +#undef LCD + + +// This file will include and build the EXRAIL script and associated helper tricks. +// It does this by incliding myAutomation.h several times, each with a set of macros to +// extract the relevant parts. + // 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. @@ -29,26 +37,25 @@ // - 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. +// Descriptive texts for routes and animations are created in a sepaerate function which +// can be called to emit a list of routes/automatuions in a form suitable for Withrottle. -// PRINT(msg) is implemented in a separate pass to create a getMessageText(id) function -#define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF -#define NOP 0,0 -// remove normal code LCD macro (will be restored later) -#undef LCD +// PRINT(msg) and LCD(row,msg) is implemented in a separate pass to create +// a getMessageText(id) function. -// CAUTION: The macros below are triple passed over myAutomation.h +// CAUTION: The macros below are multiple passed over myAutomation.h -// Pass 1 Macros convert descriptions to a flash string constant in withrottle format. +// Pass 1 Implements aliases and +// converts descriptions to withrottle format emitter function // Most macros are simply ignored in this pass. -#define ROUTE(id, description) "]\\[R" #id "}|{" description "}|{2" -#define AUTOMATION(id, description) "]\\[A" #id "}|{" description "}|{4" -#define EXRAIL const FLASH char RMFT2::RouteDescription[]= -#define ENDEXRAIL ""; + + +#define ALIAS(name,value) const int name=value; +#define EXRAIL void RMFT2::emitWithrottleDescriptions(Print * stream) { +#define ROUTE(id, description) emitRouteDescription(stream,'R',id,F(description)); +#define AUTOMATION(id, description) emitRouteDescription(stream,'A',id,F(description)); +#define ENDEXRAIL } -#define ALIAS(name,value) #define AFTER(sensor_id) #define AMBER(signal_id) #define AT(sensor_id) @@ -107,6 +114,7 @@ #include "myAutomation.h" // setup for pass 2... Create getMessageText function +#undef ALIAS #undef ROUTE #undef AUTOMATION #define ROUTE(id, description) @@ -117,13 +125,14 @@ #undef ENDEXRAIL #undef LCD const int StringMacroTracker1=__COUNTER__; +#define ALIAS(name,value) #define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) { #define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }} #define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break; #define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" - -#undef ALIAS + +// Setup for Pass 3: create main routes table #undef AFTER #undef AMBER #undef AT @@ -182,8 +191,11 @@ const int StringMacroTracker1=__COUNTER__; #undef UNJOIN #undef UNLATCH -// Define macros or route code creation -#define ALIAS(name,value) const int name=value; +// Define macros for route code creation +#define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF +#define NOP 0,0 + +#define ALIAS(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), From d8366f33c89521d451d39d77d0ca12e13314d20d Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 21 Aug 2021 00:25:00 +0100 Subject: [PATCH 034/125] Make output turnout state rather than full turnout definition. command currently prints the current states for outputs and for sensors, but prints the full configuration of turnouts. This change makes the turnout output consistent, i.e. just is output for each turnout. The command still outputs the full turnout definition. --- Turnouts.cpp | 4 +--- Turnouts.h | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index 86607f4..2647e1a 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -194,9 +194,7 @@ // Display, on the specified stream, the current state of the turnout (1 or 0). void Turnout::printState(uint16_t id, Print *stream) { Turnout *tt = get(id); - if (!tt) - StringFormatter::send(stream, F("\n"), - id, tt->_turnoutData.closed ^ useLegacyTurnoutBehaviour); + if (!tt) tt->printState(stream); } #endif \ No newline at end of file diff --git a/Turnouts.h b/Turnouts.h index 0a05dca..5ab7b07 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -119,6 +119,10 @@ public: inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; } inline uint16_t getId() { return _turnoutData.id; } inline Turnout *next() { return _nextTurnout; } + inline void printState(Print *stream) { + StringFormatter::send(stream, F("\n"), + _turnoutData.id, _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } /* * Virtual functions */ @@ -170,7 +174,7 @@ public: static void printAll(Print *stream) { for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) - tt->print(stream); + tt->printState(stream); } static void printState(uint16_t id, Print *stream); From 071389a04b4d9abba92d58d3770da4af7ef63382 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 21 Aug 2021 00:34:28 +0100 Subject: [PATCH 035/125] Remove compiler warnings in Turnout.h --- Turnouts.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Turnouts.h b/Turnouts.h index 5ab7b07..258c6d8 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -126,7 +126,9 @@ public: /* * Virtual functions */ - virtual void print(Print *stream) {} + virtual void print(Print *stream) { + (void)stream; // avoid compiler warnings. + } virtual ~Turnout() {} // Destructor /* @@ -240,6 +242,8 @@ public: IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); return tt; #else + (void)id; (void)vpin; (void)thrownPosition; (void)closedPosition; + (void)profile; (void)closed; // avoid compiler warnings. return NULL; #endif } @@ -250,6 +254,8 @@ public: IODevice::writeAnalogue(_servoTurnoutData.vpin, close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile); _turnoutData.closed = close; +#else + (void)close; // avoid compiler warnings #endif return true; } From 60718f5eaccadafdc0f6d0c0dfe674db11b6c4b0 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sat, 21 Aug 2021 13:17:14 +0100 Subject: [PATCH 036/125] int->int16_t to keep pedantic compilers happy --- RMFT2.cpp | 2 +- RMFT2.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 07ba06e..f358a70 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -357,7 +357,7 @@ bool RMFT2::skipIfBlock() { -/* static */ void RMFT2::readLocoCallback(int cv) { +/* static */ void RMFT2::readLocoCallback(int16_t cv) { progtrackLocoId=cv; } diff --git a/RMFT2.h b/RMFT2.h index e69696d..0619828 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -65,18 +65,18 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, RMFT2(int progCounter); RMFT2(int route, uint16_t cab); ~RMFT2(); - static void readLocoCallback(int cv); + static void readLocoCallback(int16_t cv); static void emitWithrottleRouteList(Print* stream); static void createNewTask(int route, uint16_t cab); static void turnoutEvent(VPIN id, bool closed); private: - static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int p[]); - static bool parseSlash(Print * stream, byte & paramCount, int p[]) ; + static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]); + static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ; static void streamFlags(Print* stream); static void setFlag(VPIN id,byte onMask, byte OffMask=0); static bool getFlag(VPIN id,byte mask); static int locateRouteStart(int16_t _route); - static int progtrackLocoId; + static int16_t progtrackLocoId; static void doSignal(VPIN id,bool red, bool amber, bool green); static void emitRouteDescription(Print * stream, char type, int id, const FSH * description); static void emitWithrottleDescriptions(Print * stream); From dbabfdca80c4bb27cca3c121112784a8c16e2dc6 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 21 Aug 2021 23:13:34 +0100 Subject: [PATCH 037/125] Improvements to PCA9685 operation Rationalise duplicated code; improve initialisation; --- IODevice.h | 2 +- IO_PCA9685.cpp | 61 ++++++++++++++++++++------------------------------ 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/IODevice.h b/IODevice.h index a542f56..eaffec8 100644 --- a/IODevice.h +++ b/IODevice.h @@ -274,7 +274,7 @@ private: 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; + uint8_t currentProfile; // profile being used for current animation. }; // 12 bytes per element, i.e. per pin in use struct ServoData *_servoData [16]; diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 1c24335..9e97234 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -53,19 +53,20 @@ bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i int8_t pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; - if (!s) { + if (s == NULL) { _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->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]); + int state = params[3]; + if (state != -1) { + // Position servo to initial state + _writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, Instant); + } return true; } @@ -75,6 +76,8 @@ PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) { _firstVpin = firstVpin; _nPins = min(nPins, 16); _I2CAddress = I2CAddress; + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. for (int i=0; i<_nPins; i++) _servoData[i] = NULL; @@ -113,30 +116,12 @@ void PCA9685::_write(VPIN vpin, int value) { if (value) value = 1; struct ServoData *s = _servoData[pin]; - if (!s) { + if (s == NULL) { // Pin not configured, just write default positions to servo controller - if (value) - writeDevice(pin, _defaultActivePosition); - else - writeDevice(pin, _defaultInactivePosition); + writeDevice(pin, value ? _defaultActivePosition : _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 : - 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; + _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile); } } @@ -150,16 +135,18 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { else if (value < 0) value = 0; struct ServoData *s = _servoData[pin]; - - if (!s) { - // Servo pin not configured, so configure now. + if (s == NULL) { + // Servo pin not configured, so configure now using defaults s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure s->activePosition = _defaultActivePosition; s->inactivePosition = _defaultInactivePosition; - s->currentPosition = value; // Don't know where we're moving from. + s->currentPosition = value; + s->profile = Instant; } - s->profile = profile; + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; s->numSteps = profile==Fast ? 10 : profile==Medium ? 20 : profile==Slow ? 40 : @@ -175,7 +162,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { bool PCA9685::_isActive(VPIN vpin) { int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; - if (!s) + if (s == NULL) return false; // No structure means no animation! else return (s->numSteps != 0); @@ -194,16 +181,16 @@ void PCA9685::_loop(unsigned long currentMicros) { // 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 == NULL) return; // No pin configuration/state data if (s->numSteps == 0) return; // No animation in progress if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { - // No movement required, so go straight to final step - s->stepNumber = s->numSteps; + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; } if (s->stepNumber < s->numSteps) { // Animation in progress, reposition servo s->stepNumber++; - if (s->profile == Bounce) { + if (s->currentProfile == Bounce) { // Retrieve step positions from array in flash byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); From 39a69e340eead439b20127f86cdcbea93404002e Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 21 Aug 2021 23:16:52 +0100 Subject: [PATCH 038/125] Turnout EEPROM improvements. Ensure state is saved and restored from EEPROM as expected. Make constructors for turnouts private. Otherwise, a statically created turnout may be initialising itself before the underlying HAL device has been created. By requiring the create() call be used, there is more control over the timing of the turnout object's creation. --- Turnouts.cpp | 26 +++++----- Turnouts.h | 138 +++++++++++++++++++++++++++------------------------ 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index 2647e1a..3c8c35a 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -121,19 +121,21 @@ if (!tt) return false; bool ok = tt->activate(closeFlag); - // Write new closed/thrown state to EEPROM if required. Note that eepromAddress - // is always zero for LCN turnouts. - if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress) - EEPROM.put(tt->_eepromAddress, tt->_turnoutData.closed); + if (ok) { + // Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress + // is always zero for LCN turnouts. + if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0) + EEPROM.put(tt->_eepromAddress, *((uint8_t *) &tt->_turnoutData)); - #if defined(RMFT_ACTIVE) - RMFT2::turnoutEvent(id, closeFlag); - #endif + #if defined(RMFT_ACTIVE) + RMFT2::turnoutEvent(id, closeFlag); + #endif - // Send message to JMRI etc. over Serial USB. This is done here - // to ensure that the message is sent when the turnout operation - // is not initiated by a Serial command. - printState(id, &Serial); + // Send message to JMRI etc. over Serial USB. This is done here + // to ensure that the message is sent when the turnout operation + // is not initiated by a Serial command. + printState(id, &Serial); + } return ok; } @@ -180,7 +182,7 @@ // so we can't go any further through the EEPROM! return NULL; } - if (!tt) { + if (tt) { // Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero. tt->_eepromAddress = eepromAddress; } diff --git a/Turnouts.h b/Turnouts.h index 258c6d8..11754a4 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -198,7 +198,6 @@ private: uint8_t profile; } _servoTurnoutData; // 6 bytes -public: // Constructor ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) : Turnout(id, TURNOUT_SERVO, closed) @@ -209,6 +208,7 @@ public: _servoTurnoutData.profile = profile; } +public: // Create function static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) { #ifndef IO_NO_HAL @@ -229,7 +229,7 @@ public: // int params[] = {(int)thrownPosition, (int)closedPosition, profile, closed}; // IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, params); - // Set position to saved position + // Set position directly to specified position - we don't know where it is moving from. IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); return tt; @@ -248,6 +248,26 @@ public: #endif } + // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + ServoTurnoutData servoTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), servoTurnoutData); + EEStore::advance(sizeof(servoTurnoutData)); + + // Create new object + Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition, + servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed); + return tt; + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, + _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + +protected: // ServoTurnout-specific code for throwing or closing a servo turnout. bool activate(bool close) override { #ifndef IO_NO_HAL @@ -269,26 +289,6 @@ public: EEPROM.put(EEStore::pointer(), _servoTurnoutData); EEStore::advance(sizeof(_servoTurnoutData)); } - - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, - _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } - - // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - ServoTurnoutData servoTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), servoTurnoutData); - EEStore::advance(sizeof(servoTurnoutData)); - - // Create new object - ServoTurnout *tt = new ServoTurnout(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition, - servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed); - - return tt; - } }; /************************************************************************************* @@ -305,7 +305,6 @@ private: // That's DCC accessory address 1-512 and subaddress 0-3. } _dccTurnoutData; // 2 bytes -public: // Constructor DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : Turnout(id, TURNOUT_DCC, false) @@ -313,6 +312,7 @@ public: _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; } +public: // Create function static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd) { Turnout *tt = get(id); @@ -333,9 +333,36 @@ public: return tt; } + // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + DCCTurnoutData dccTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), dccTurnoutData); + EEStore::advance(sizeof(dccTurnoutData)); + + // Create new object + DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); + + return tt; + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + // Also report using classic DCC++ syntax for DCC accessory turnouts + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + +protected: bool activate(bool close) override { + // DCC++ Classic behaviour is that Throw writes a 1 in the packet, + // and Close writes a 0. + // RCN-214 specifies that Throw is 0 and Close is 1. DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), - ((_dccTurnoutData.address-1) & 3), close); + ((_dccTurnoutData.address-1) & 3), close ^ useLegacyTurnoutBehaviour); _turnoutData.closed = close; return true; } @@ -350,28 +377,6 @@ public: EEStore::advance(sizeof(_dccTurnoutData)); } - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - // Also report using classic DCC++ syntax for DCC accessory turnouts - StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } - - // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - DCCTurnoutData dccTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), dccTurnoutData); - EEStore::advance(sizeof(dccTurnoutData)); - - // Create new object - DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); - - return tt; - } }; @@ -387,7 +392,6 @@ private: VPIN vpin; } _vpinTurnoutData; // 2 bytes -public: // Constructor VpinTurnout(uint16_t id, VPIN vpin, bool closed=true) : Turnout(id, TURNOUT_VPIN, closed) @@ -395,6 +399,7 @@ public: _vpinTurnoutData.vpin = vpin; } +public: // Create function static Turnout *create(uint16_t id, VPIN vpin, bool closed=true) { Turnout *tt = get(id); @@ -415,6 +420,25 @@ public: return tt; } + // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData) { + VpinTurnoutData vpinTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), vpinTurnoutData); + EEStore::advance(sizeof(vpinTurnoutData)); + + // Create new object + VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed); + + return tt; + } + + void print(Print *stream) override { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + +protected: bool activate(bool close) override { IODevice::write(_vpinTurnoutData.vpin, close); _turnoutData.closed = close; @@ -430,24 +454,6 @@ public: EEPROM.put(EEStore::pointer(), _vpinTurnoutData); EEStore::advance(sizeof(_vpinTurnoutData)); } - - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } - - // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - VpinTurnoutData vpinTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), vpinTurnoutData); - EEStore::advance(sizeof(vpinTurnoutData)); - - // Create new object - VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed); - - return tt; - } }; @@ -461,12 +467,12 @@ private: // struct LCNTurnoutData { // } _lcnTurnoutData; // 0 bytes -public: // Constructor LCNTurnout(uint16_t id, bool closed=true) : Turnout(id, TURNOUT_LCN, closed) { } +public: // Create function static Turnout *create(uint16_t id, bool closed=true) { Turnout *tt = get(id); From 0875d27b0a009babccf89d3b476691bd02402ff8 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 22 Aug 2021 14:07:16 +0100 Subject: [PATCH 039/125] Remove 'activate' functions from turnout classes. Remove the static 'activate' function and rename the virtual 'activate' function to 'setClosedInternal'. --- Turnouts.cpp | 8 ++++---- Turnouts.h | 20 ++++++++------------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index 3c8c35a..22bc175 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -106,11 +106,11 @@ return false; } - // Static activate function is invoked from close(), throw() etc. to perform the + // Static setClosed function is invoked from close(), throw() etc. to perform the // common parts of the turnout operation. Code which is specific to a turnout - // type should be placed in the virtual function activate(bool) which is + // type should be placed in the virtual function setClosedInternal(bool) which is // called from here. - bool Turnout::activate(uint16_t id, bool closeFlag) { + bool Turnout::setClosed(uint16_t id, bool closeFlag) { #ifdef EESTOREDEBUG if (closeFlag) DIAG(F("Turnout::close(%d)"), id); @@ -119,7 +119,7 @@ #endif Turnout *tt = Turnout::get(id); if (!tt) return false; - bool ok = tt->activate(closeFlag); + bool ok = tt->setClosedInternal(closeFlag); if (ok) { // Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress diff --git a/Turnouts.h b/Turnouts.h index 11754a4..bb2c57e 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -93,7 +93,7 @@ protected: * Virtual functions */ - virtual bool activate(bool close) = 0; // Mandatory in subclass + virtual bool setClosedInternal(bool close) = 0; // Mandatory in subclass virtual void save() {} /* @@ -144,18 +144,14 @@ public: return !isClosed(id); } - static bool activate(uint16_t id, bool closeFlag); + static bool setClosed(uint16_t id, bool closeFlag); inline static bool setClosed(uint16_t id) { - return activate(id, true); + return setClosed(id, true); } inline static bool setThrown(uint16_t id) { - return activate(id, false); - } - - inline static bool setClosed(uint16_t id, bool close) { - return activate(id, close); + return setClosed(id, false); } static bool setClosedStateOnly(uint16_t id, bool close) { @@ -269,7 +265,7 @@ public: protected: // ServoTurnout-specific code for throwing or closing a servo turnout. - bool activate(bool close) override { + bool setClosedInternal(bool close) override { #ifndef IO_NO_HAL IODevice::writeAnalogue(_servoTurnoutData.vpin, close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile); @@ -357,7 +353,7 @@ public: } protected: - bool activate(bool close) override { + bool setClosedInternal(bool close) override { // DCC++ Classic behaviour is that Throw writes a 1 in the packet, // and Close writes a 0. // RCN-214 specifies that Throw is 0 and Close is 1. @@ -439,7 +435,7 @@ public: } protected: - bool activate(bool close) override { + bool setClosedInternal(bool close) override { IODevice::write(_vpinTurnoutData.vpin, close); _turnoutData.closed = close; return true; @@ -490,7 +486,7 @@ public: return tt; } - bool activate(bool close) override { + bool setClosedInternal(bool close) override { // Assume that the LCN command still uses 1 for throw and 0 for close... LCN::send('T', _turnoutData.id, !close); // The _turnoutData.closed flag should be updated by a message from the LCN master, later. From b35ce88fdd807434013e447345317f71e98087a5 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sun, 22 Aug 2021 17:01:55 +0100 Subject: [PATCH 040/125] Deeay long values --- RMFT2.cpp | 6 +++--- RMFTMacros.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index f358a70..d3ffd75 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -489,15 +489,15 @@ void RMFT2::loop2() { break; case OPCODE_DELAY: - delayMe(operand*100); + delayMe(operand*100L); break; case OPCODE_DELAYMINS: - delayMe(operand*60*1000); + delayMe(operand*60L*1000L); break; case OPCODE_RANDWAIT: - delayMe((long)random(operand*100)); + delayMe(random(operand)*100L); break; case OPCODE_RED: diff --git a/RMFTMacros.h b/RMFTMacros.h index 7c6c9d2..3e81ee1 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -209,9 +209,9 @@ const int StringMacroTracker1=__COUNTER__; #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 DELAY(ms) OPCODE_DELAY,V(ms/100L), #define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay), -#define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100), +#define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100L),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L), #define ENDIF OPCODE_ENDIF,NOP, #define ESTOP OPCODE_SPEED,V(1), #define FOFF(func) OPCODE_FOFF,V(func), From 3e50a6bdad8b2a2515013bcc375ed70410ada5c7 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 22 Aug 2021 22:23:08 +0100 Subject: [PATCH 041/125] Add include guard to defines.h Ensure that defines.h is only process once, even if included multiple times. --- defines.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/defines.h b/defines.h index d52ae4a..1b1d3eb 100644 --- a/defines.h +++ b/defines.h @@ -18,6 +18,9 @@ */ +#ifndef DEFINES_H +#define DEFINES_H + //////////////////////////////////////////////////////////////////////////////// // // WIFI_ON: All prereqs for running with WIFI are met @@ -55,3 +58,5 @@ #if __has_include ( "myAutomation.h") && defined(BIG_RAM) #define RMFT_ACTIVE #endif + +#endif \ No newline at end of file From 7b47b861437fea18366b70d85214ec35e51e696b Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 22 Aug 2021 22:25:23 +0100 Subject: [PATCH 042/125] Turnouts: adjust the split of code between .h and .cpp file. No functional changes. --- IO_PCA9685.cpp | 4 + Turnouts.cpp | 309 ++++++++++++++++++++++++++++++++++++++++++++++++- Turnouts.h | 272 +++++-------------------------------------- 3 files changed, 339 insertions(+), 246 deletions(-) diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 9e97234..a3ab48c 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -181,12 +181,16 @@ void PCA9685::_loop(unsigned long currentMicros) { // 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 == NULL) return; // No pin configuration/state data + if (s->numSteps == 0) return; // No animation in progress + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { // Go straight to end of sequence, output final position. s->stepNumber = s->numSteps-1; } + if (s->stepNumber < s->numSteps) { // Animation in progress, reposition servo s->stepNumber++; diff --git a/Turnouts.cpp b/Turnouts.cpp index 22bc175..aa183cc 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -20,9 +20,6 @@ * along with CommandStation. If not, see . */ -#ifndef TURNOUTS_CPP -#define TURNOUTS_CPP - // Set the following definition to true for = throw and = close // or to false for = close and = throw (the original way). #ifndef USE_LEGACY_TURNOUT_BEHAVIOUR @@ -34,6 +31,8 @@ #include "StringFormatter.h" #include "RMFT2.h" #include "Turnouts.h" +#include "DCC.h" +#include "LCN.h" #ifdef EESTOREDEBUG #include "DIAG.h" #endif @@ -106,6 +105,14 @@ return false; } + bool Turnout::setClosedStateOnly(uint16_t id, bool close) { + Turnout *tt = get(id); + if (tt) return false; + tt->_turnoutData.closed = close; + return true; + } + + // Static setClosed function is invoked from close(), throw() etc. to perform the // common parts of the turnout operation. Code which is specific to a turnout // type should be placed in the virtual function setClosedInternal(bool) which is @@ -199,4 +206,298 @@ if (!tt) tt->printState(stream); } -#endif \ No newline at end of file + +/************************************************************************************* + * ServoTurnout - Turnout controlled by servo device. + * + *************************************************************************************/ + + // Private Constructor + ServoTurnout::ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) : + Turnout(id, TURNOUT_SERVO, closed) + { + _servoTurnoutData.vpin = vpin; + _servoTurnoutData.thrownPosition = thrownPosition; + _servoTurnoutData.closedPosition = closedPosition; + _servoTurnoutData.profile = profile; + } + + // Create function + Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) { +#ifndef IO_NO_HAL + Turnout *tt = get(id); + if (tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_SERVO)) { + // Yes, so set parameters + ServoTurnout *st = (ServoTurnout *)tt; + st->_servoTurnoutData.vpin = vpin; + st->_servoTurnoutData.thrownPosition = thrownPosition; + st->_servoTurnoutData.closedPosition = closedPosition; + st->_servoTurnoutData.profile = profile; + // Don't touch the _closed parameter, retain the original value. + + // We don't really need to do the following, since a call to IODevice::_writeAnalogue + // will provide all the data that is required! + // int params[] = {(int)thrownPosition, (int)closedPosition, profile, closed}; + // IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, params); + + // Set position directly to specified position - we don't know where it is moving from. + IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); + + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed); + IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); + return tt; +#else + (void)id; (void)vpin; (void)thrownPosition; (void)closedPosition; + (void)profile; (void)closed; // avoid compiler warnings. + return NULL; +#endif + } + + // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. + Turnout *ServoTurnout::load(struct TurnoutData *turnoutData) { + ServoTurnoutData servoTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), servoTurnoutData); + EEStore::advance(sizeof(servoTurnoutData)); + + // Create new object + Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition, + servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed); + return tt; + } + + void ServoTurnout::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, + _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + + // ServoTurnout-specific code for throwing or closing a servo turnout. + bool ServoTurnout::setClosedInternal(bool close) { +#ifndef IO_NO_HAL + IODevice::writeAnalogue(_servoTurnoutData.vpin, + close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile); + _turnoutData.closed = close; +#else + (void)close; // avoid compiler warnings +#endif + return true; + } + + void ServoTurnout::save() { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _servoTurnoutData); + EEStore::advance(sizeof(_servoTurnoutData)); + } + +/************************************************************************************* + * DCCTurnout - Turnout controlled by DCC Accessory Controller. + * + *************************************************************************************/ + + // DCCTurnoutData contains data specific to this subclass that is + // written to EEPROM when the turnout is saved. + struct DCCTurnoutData { + // 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. + } _dccTurnoutData; // 2 bytes + + // Constructor + DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : + Turnout(id, TURNOUT_DCC, false) + { + _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; + } + + // Create function + Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) { + Turnout *tt = get(id); + if (tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_DCC)) { + // Yes, so set parameters + DCCTurnout *dt = (DCCTurnout *)tt; + dt->_dccTurnoutData.address = ((add-1) << 2) + subAdd + 1; + // Don't touch the _closed parameter, retain the original value. + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new DCCTurnout(id, add, subAdd); + return tt; + } + + // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. + Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) { + DCCTurnoutData dccTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), dccTurnoutData); + EEStore::advance(sizeof(dccTurnoutData)); + + // Create new object + DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); + + return tt; + } + + void DCCTurnout::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + // Also report using classic DCC++ syntax for DCC accessory turnouts + StringFormatter::send(stream, F("\n"), _turnoutData.id, + (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + + bool DCCTurnout::setClosedInternal(bool close) { + // DCC++ Classic behaviour is that Throw writes a 1 in the packet, + // and Close writes a 0. + // RCN-214 specifies that Throw is 0 and Close is 1. + DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), + ((_dccTurnoutData.address-1) & 3), close ^ useLegacyTurnoutBehaviour); + _turnoutData.closed = close; + return true; + } + + void DCCTurnout::save() { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _dccTurnoutData); + EEStore::advance(sizeof(_dccTurnoutData)); + } + + + +/************************************************************************************* + * VpinTurnout - Turnout controlled through a HAL vpin. + * + *************************************************************************************/ + + // Constructor + VpinTurnout::VpinTurnout(uint16_t id, VPIN vpin, bool closed) : + Turnout(id, TURNOUT_VPIN, closed) + { + _vpinTurnoutData.vpin = vpin; + } + + // Create function + Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) { + Turnout *tt = get(id); + if (tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_VPIN)) { + // Yes, so set parameters + VpinTurnout *vt = (VpinTurnout *)tt; + vt->_vpinTurnoutData.vpin = vpin; + // Don't touch the _closed parameter, retain the original value. + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new VpinTurnout(id, vpin, closed); + return tt; + } + + // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. + Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) { + VpinTurnoutData vpinTurnoutData; + // Read class-specific data from EEPROM + EEPROM.get(EEStore::pointer(), vpinTurnoutData); + EEStore::advance(sizeof(vpinTurnoutData)); + + // Create new object + VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed); + + return tt; + } + + void VpinTurnout::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + + bool VpinTurnout::setClosedInternal(bool close) { + IODevice::write(_vpinTurnoutData.vpin, close); + _turnoutData.closed = close; + return true; + } + + void VpinTurnout::save() { + // Write turnout definition and current position to EEPROM + // First write common servo data, then + // write the servo-specific data + EEPROM.put(EEStore::pointer(), _turnoutData); + EEStore::advance(sizeof(_turnoutData)); + EEPROM.put(EEStore::pointer(), _vpinTurnoutData); + EEStore::advance(sizeof(_vpinTurnoutData)); + } + + +/************************************************************************************* + * LCNTurnout - Turnout controlled by Loconet + * + *************************************************************************************/ + + // LCNTurnout has no specific data, and in any case is not written to EEPROM! + // struct LCNTurnoutData { + // } _lcnTurnoutData; // 0 bytes + + // Constructor + LCNTurnout::LCNTurnout(uint16_t id, bool closed) : + Turnout(id, TURNOUT_LCN, closed) + { } + + // Create function + Turnout *LCNTurnout::create(uint16_t id, bool closed) { + Turnout *tt = get(id); + if (tt) { + // Object already exists, check if it is usable + if (tt->isType(TURNOUT_LCN)) { + // Yes, so return this object + return tt; + } else { + // Incompatible object, delete and recreate + remove(id); + } + } + tt = (Turnout *)new LCNTurnout(id, closed); + return tt; + } + + bool LCNTurnout::setClosedInternal(bool close) { + // Assume that the LCN command still uses 1 for throw and 0 for close... + LCN::send('T', _turnoutData.id, !close); + // The _turnoutData.closed flag should be updated by a message from the LCN master, later. + return true; + } + + // LCN turnouts not saved to EEPROM. + //void save() override { } + //static Turnout *load(struct TurnoutData *turnoutData) { + + void LCNTurnout::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turnoutData.id, + _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + diff --git a/Turnouts.h b/Turnouts.h index bb2c57e..2d1b9e8 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -19,18 +19,13 @@ * along with CommandStation. If not, see . */ +#ifndef TURNOUTS_H +#define TURNOUTS_H //#define EESTOREDEBUG -#include "defines.h" -#include "EEStore.h" -#include "StringFormatter.h" -#include "RMFT2.h" -#ifdef EESTOREDEBUG -#include "DIAG.h" -#endif +#include "Arduino.h" +#include "IODevice.h" -#include "DCC.h" -#include "LCN.h" // Turnout type definitions enum { @@ -154,12 +149,7 @@ public: return setClosed(id, false); } - static bool setClosedStateOnly(uint16_t id, bool close) { - Turnout *tt = get(id); - if (tt) return false; - tt->_turnoutData.closed = close; - return true; - } + static bool setClosedStateOnly(uint16_t id, bool close); inline static Turnout *first() { return _firstTurnout; } @@ -195,96 +185,21 @@ private: } _servoTurnoutData; // 6 bytes // Constructor - ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) : - Turnout(id, TURNOUT_SERVO, closed) - { - _servoTurnoutData.vpin = vpin; - _servoTurnoutData.thrownPosition = thrownPosition; - _servoTurnoutData.closedPosition = closedPosition; - _servoTurnoutData.profile = profile; - } + ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true); public: // Create function - static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true) { -#ifndef IO_NO_HAL - Turnout *tt = get(id); - if (tt) { - // Object already exists, check if it is usable - if (tt->isType(TURNOUT_SERVO)) { - // Yes, so set parameters - ServoTurnout *st = (ServoTurnout *)tt; - st->_servoTurnoutData.vpin = vpin; - st->_servoTurnoutData.thrownPosition = thrownPosition; - st->_servoTurnoutData.closedPosition = closedPosition; - st->_servoTurnoutData.profile = profile; - // Don't touch the _closed parameter, retain the original value. - - // We don't really need to do the following, since a call to IODevice::_writeAnalogue - // will provide all the data that is required! - // int params[] = {(int)thrownPosition, (int)closedPosition, profile, closed}; - // IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, params); - - // Set position directly to specified position - we don't know where it is moving from. - IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); - - return tt; - } else { - // Incompatible object, delete and recreate - remove(id); - } - } - tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed); - IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); - return tt; -#else - (void)id; (void)vpin; (void)thrownPosition; (void)closedPosition; - (void)profile; (void)closed; // avoid compiler warnings. - return NULL; -#endif - } + static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true); // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - ServoTurnoutData servoTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), servoTurnoutData); - EEStore::advance(sizeof(servoTurnoutData)); - - // Create new object - Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition, - servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed); - return tt; - } - - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, - _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } + static Turnout *load(struct TurnoutData *turnoutData); + void print(Print *stream) override; protected: // ServoTurnout-specific code for throwing or closing a servo turnout. - bool setClosedInternal(bool close) override { -#ifndef IO_NO_HAL - IODevice::writeAnalogue(_servoTurnoutData.vpin, - close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile); - _turnoutData.closed = close; -#else - (void)close; // avoid compiler warnings -#endif - return true; - } + bool setClosedInternal(bool close) override; + void save() override; - void save() override { - // Write turnout definition and current position to EEPROM - // First write common servo data, then - // write the servo-specific data - EEPROM.put(EEStore::pointer(), _turnoutData); - EEStore::advance(sizeof(_turnoutData)); - EEPROM.put(EEStore::pointer(), _servoTurnoutData); - EEStore::advance(sizeof(_servoTurnoutData)); - } }; /************************************************************************************* @@ -302,76 +217,18 @@ private: } _dccTurnoutData; // 2 bytes // Constructor - DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : - Turnout(id, TURNOUT_DCC, false) - { - _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; - } + DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd); public: // Create function - static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd) { - Turnout *tt = get(id); - if (tt) { - // Object already exists, check if it is usable - if (tt->isType(TURNOUT_DCC)) { - // Yes, so set parameters - DCCTurnout *dt = (DCCTurnout *)tt; - dt->_dccTurnoutData.address = ((add-1) << 2) + subAdd + 1; - // Don't touch the _closed parameter, retain the original value. - return tt; - } else { - // Incompatible object, delete and recreate - remove(id); - } - } - tt = (Turnout *)new DCCTurnout(id, add, subAdd); - return tt; - } - - // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - DCCTurnoutData dccTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), dccTurnoutData); - EEStore::advance(sizeof(dccTurnoutData)); - - // Create new object - DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); - - return tt; - } - - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - // Also report using classic DCC++ syntax for DCC accessory turnouts - StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } + static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd); + // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. + static Turnout *load(struct TurnoutData *turnoutData); + void print(Print *stream) override; protected: - bool setClosedInternal(bool close) override { - // DCC++ Classic behaviour is that Throw writes a 1 in the packet, - // and Close writes a 0. - // RCN-214 specifies that Throw is 0 and Close is 1. - DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), - ((_dccTurnoutData.address-1) & 3), close ^ useLegacyTurnoutBehaviour); - _turnoutData.closed = close; - return true; - } - - void save() override { - // Write turnout definition and current position to EEPROM - // First write common servo data, then - // write the servo-specific data - EEPROM.put(EEStore::pointer(), _turnoutData); - EEStore::advance(sizeof(_turnoutData)); - EEPROM.put(EEStore::pointer(), _dccTurnoutData); - EEStore::advance(sizeof(_dccTurnoutData)); - } + bool setClosedInternal(bool close) override; + void save() override; }; @@ -389,67 +246,20 @@ private: } _vpinTurnoutData; // 2 bytes // Constructor - VpinTurnout(uint16_t id, VPIN vpin, bool closed=true) : - Turnout(id, TURNOUT_VPIN, closed) - { - _vpinTurnoutData.vpin = vpin; - } + VpinTurnout(uint16_t id, VPIN vpin, bool closed=true); public: // Create function - static Turnout *create(uint16_t id, VPIN vpin, bool closed=true) { - Turnout *tt = get(id); - if (tt) { - // Object already exists, check if it is usable - if (tt->isType(TURNOUT_VPIN)) { - // Yes, so set parameters - VpinTurnout *vt = (VpinTurnout *)tt; - vt->_vpinTurnoutData.vpin = vpin; - // Don't touch the _closed parameter, retain the original value. - return tt; - } else { - // Incompatible object, delete and recreate - remove(id); - } - } - tt = (Turnout *)new VpinTurnout(id, vpin, closed); - return tt; - } + static Turnout *create(uint16_t id, VPIN vpin, bool closed=true); // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. - static Turnout *load(struct TurnoutData *turnoutData) { - VpinTurnoutData vpinTurnoutData; - // Read class-specific data from EEPROM - EEPROM.get(EEStore::pointer(), vpinTurnoutData); - EEStore::advance(sizeof(vpinTurnoutData)); - - // Create new object - VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed); - - return tt; - } - - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } + static Turnout *load(struct TurnoutData *turnoutData); + void print(Print *stream) override; protected: - bool setClosedInternal(bool close) override { - IODevice::write(_vpinTurnoutData.vpin, close); - _turnoutData.closed = close; - return true; - } + bool setClosedInternal(bool close) override; + void save() override; - void save() override { - // Write turnout definition and current position to EEPROM - // First write common servo data, then - // write the servo-specific data - EEPROM.put(EEStore::pointer(), _turnoutData); - EEStore::advance(sizeof(_turnoutData)); - EEPROM.put(EEStore::pointer(), _vpinTurnoutData); - EEStore::advance(sizeof(_vpinTurnoutData)); - } }; @@ -464,43 +274,21 @@ private: // } _lcnTurnoutData; // 0 bytes // Constructor - LCNTurnout(uint16_t id, bool closed=true) : - Turnout(id, TURNOUT_LCN, closed) - { } + LCNTurnout(uint16_t id, bool closed=true); public: // Create function - static Turnout *create(uint16_t id, bool closed=true) { - Turnout *tt = get(id); - if (tt) { - // Object already exists, check if it is usable - if (tt->isType(TURNOUT_LCN)) { - // Yes, so return this object - return tt; - } else { - // Incompatible object, delete and recreate - remove(id); - } - } - tt = (Turnout *)new LCNTurnout(id, closed); - return tt; - } + static Turnout *create(uint16_t id, bool closed=true); - bool setClosedInternal(bool close) override { - // Assume that the LCN command still uses 1 for throw and 0 for close... - LCN::send('T', _turnoutData.id, !close); - // The _turnoutData.closed flag should be updated by a message from the LCN master, later. - return true; - } + + bool setClosedInternal(bool close) override; // LCN turnouts not saved to EEPROM. //void save() override { } //static Turnout *load(struct TurnoutData *turnoutData) { - void print(Print *stream) override { - StringFormatter::send(stream, F("\n"), _turnoutData.id, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } + void print(Print *stream) override; }; +#endif From fdaa7b51b949a929ca6435b8b1a6b4d822a146d9 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 22 Aug 2021 22:30:09 +0100 Subject: [PATCH 043/125] Move Turnout code from .h to .cpp. Move implementation of Turnout::printState from Turnouts.h to Turnouts.cpp. No functional changes. --- Turnouts.cpp | 5 +++++ Turnouts.h | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index aa183cc..7b87153 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -74,6 +74,11 @@ turnoutlistHash++; } + void Turnout::printState(Print *stream) { + StringFormatter::send(stream, F("\n"), + _turnoutData.id, _turnoutData.closed ^ useLegacyTurnoutBehaviour); + } + // Remove nominated turnout from turnout linked list and delete the object. bool Turnout::remove(uint16_t id) { Turnout *tt,*pp=NULL; diff --git a/Turnouts.h b/Turnouts.h index 2d1b9e8..45f60a6 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -114,10 +114,7 @@ public: inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; } inline uint16_t getId() { return _turnoutData.id; } inline Turnout *next() { return _nextTurnout; } - inline void printState(Print *stream) { - StringFormatter::send(stream, F("\n"), - _turnoutData.id, _turnoutData.closed ^ useLegacyTurnoutBehaviour); - } + void printState(Print *stream); /* * Virtual functions */ From 00138be90d4aaac2afe12e8a9369117812091b41 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 22 Aug 2021 22:39:00 +0100 Subject: [PATCH 044/125] Increase default display line length to 20 (from 16). --- LCDDisplay.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LCDDisplay.h b/LCDDisplay.h index 2791083..15ba524 100644 --- a/LCDDisplay.h +++ b/LCDDisplay.h @@ -27,7 +27,7 @@ // Allow maximum message length to be overridden from config.h #if !defined(MAX_MSG_SIZE) -#define MAX_MSG_SIZE 16 +#define MAX_MSG_SIZE 20 #endif // Set default scroll mode (overridable in config.h) From ca55834051db9198c1aa16bb4bf541edddbf0297 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Mon, 23 Aug 2021 10:46:12 +0100 Subject: [PATCH 045/125] Update defines.h Add #include config.h (on which defines.h is reliant). --- defines.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/defines.h b/defines.h index 1b1d3eb..95102f9 100644 --- a/defines.h +++ b/defines.h @@ -21,6 +21,13 @@ #ifndef DEFINES_H #define DEFINES_H +// defines.h relies on macros defined in config.h +#if __has_include ( "config.h") + #include "config.h" +#else + #include "config.example.h" +#endif + //////////////////////////////////////////////////////////////////////////////// // // WIFI_ON: All prereqs for running with WIFI are met From 50a9e08d1f907338618c39e376eee85038c2a020 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 23 Aug 2021 11:55:42 +0100 Subject: [PATCH 046/125] defines/configig include tidy now just 2 places where config is included... 1) in defines.h 2) At the start of the .ino so it can be made obvious to the user what is happening. --- EthernetInterface.cpp | 6 ------ EthernetInterface.h | 8 ++------ LCDDisplay.h | 5 +---- defines.h | 11 +++++++---- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/EthernetInterface.cpp b/EthernetInterface.cpp index 22b61f3..c6f80b3 100644 --- a/EthernetInterface.cpp +++ b/EthernetInterface.cpp @@ -17,12 +17,6 @@ * along with CommandStation. If not, see . * */ -#if __has_include ( "config.h") - #include "config.h" -#else - #warning config.h not found. Using defaults from config.example.h - #include "config.example.h" -#endif #include "defines.h" #if ETHERNET_ON == true #include "EthernetInterface.h" diff --git a/EthernetInterface.h b/EthernetInterface.h index e97ebfc..ab21f6a 100644 --- a/EthernetInterface.h +++ b/EthernetInterface.h @@ -22,12 +22,8 @@ #ifndef EthernetInterface_h #define EthernetInterface_h -#if __has_include ( "config.h") - #include "config.h" -#else - #warning config.h not found. Using defaults from config.example.h - #include "config.example.h" -#endif + +#include "defines.h") #include "DCCEXParser.h" #include #include diff --git a/LCDDisplay.h b/LCDDisplay.h index 15ba524..6e2c69d 100644 --- a/LCDDisplay.h +++ b/LCDDisplay.h @@ -19,12 +19,9 @@ #ifndef LCDDisplay_h #define LCDDisplay_h #include +#include "defines.h" #include "DisplayInterface.h" -#if __has_include ( "config.h") - #include "config.h" -#endif - // Allow maximum message length to be overridden from config.h #if !defined(MAX_MSG_SIZE) #define MAX_MSG_SIZE 20 diff --git a/defines.h b/defines.h index 95102f9..1d76f95 100644 --- a/defines.h +++ b/defines.h @@ -22,10 +22,13 @@ #define DEFINES_H // defines.h relies on macros defined in config.h -#if __has_include ( "config.h") - #include "config.h" -#else - #include "config.example.h" +// but it may have already been included (for cosmetic convenence) by the .ino +#ifndef MOTOR_SHIELD_TYPE + #if __has_include ( "config.h") + #include "config.h" + #else + #include "config.example.h" + #endif #endif //////////////////////////////////////////////////////////////////////////////// From 214e6c643f464c52a9860b4a53b18b0a95255247 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 23 Aug 2021 11:58:48 +0100 Subject: [PATCH 047/125] Squashed commit of the following: commit b34205b1428aa72b6ad736f4cd95d3e292ba7004 Merge: 8703248 2829716 Author: Neil McKechnie <75813993+Neil-McK@users.noreply.github.com> Date: Mon Aug 23 10:05:54 2021 +0100 Merge branch 'EX-RAIL' into ackRetry commit 8703248c49a831a0a9d7d1b897550f646ff72f2a Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun Aug 22 16:47:38 2021 -0500 ACK RETRY max 255 with fallback to 3 if greater And includes LCD lines for power and ACK diags. commit f5d4522ed777926c38fcaab69ebaa811f29fcc7c Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun Aug 22 16:40:13 2021 -0500 ACK RETRY updated datatypes commit 1dbf23669740d47839f31bd4af6758ffd8fc9829 Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun Aug 22 16:35:14 2021 -0500 ACK RETRY updated datatypes commit d93584e9a4be81e685fdd606e055fdac1902fe5c Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun Aug 22 13:16:24 2021 -0500 ACK RETRY updated default is 2 retries. commit f58ebac6703e36afb20290d75a12c285101f09ca Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sat Aug 21 16:43:21 2021 -0500 ACK RETRY is 3 or less (default is 1) commit 08350b215a0f1fe832cf862f72cece37dfd7c9da Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sat Aug 21 11:55:17 2021 -0500 ACK RETRY LCD display update. lcd(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); commit 11cd216017bcf3c843789f0553ff6de04c80dd4f Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sat Aug 21 00:54:28 2021 -0500 ACK RETRY ACK retry code added to ackManagerSetup and callback. The default is . For ACK tuning, set retry to zero. Retry count is captured on the LCD display, and lines in the serial monitor. commit b67027a1ed45856c79d60599b56b5599f7dc7b4d Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sat Aug 21 00:33:01 2021 -0500 ACK RETRY variables added commit 34d2ab3543e8603d9f2d3aafb971791fe51b89aa Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sat Aug 21 00:23:34 2021 -0500 Update DCCEXParser.cpp LCD lines added to display power commands and ACK settings, when updated. Also new command . commit 8ca4011cb0e991c4816f4e4ec2dd086bdadc9024 Author: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Fri Aug 20 23:58:13 2021 -0500 Update CommandStation-EX.ino Update LCD row number for Ready and Free RAM. commit 65711383892333796ce55cd4088dee9b3ad4568d Author: Harald Barth Date: Sun Aug 1 22:08:34 2021 +0200 optimize command parser for size commit c4f659243e07293dc25379d41156f30ae36d75e5 Author: Harald Barth Date: Sun Aug 1 15:07:06 2021 +0200 optimize for loops for size (and speed) commit 55b7091d5a53c1b2e9cbdc8c102f7641a44066fb Author: Harald Barth Date: Sun Aug 1 12:45:29 2021 +0200 take less progmem for messages commit 6d7c1925b0f9b8ca267d947822d730297d425020 Author: Harald Barth Date: Sun Aug 1 11:56:12 2021 +0200 only pragma -O3 critical functions --- CommandStation-EX.ino | 4 ++-- DCC.cpp | 15 ++++++++++++++ DCC.h | 12 ++++++++++- DCCEXParser.cpp | 47 ++++++++++++++++++++++++++++++++++--------- DCCWaveform.cpp | 29 ++++++++++++++++---------- Sensors.cpp | 3 ++- 6 files changed, 85 insertions(+), 25 deletions(-) diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index cd6db16..026e763 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -109,7 +109,7 @@ void setup() LCN::init(LCN_SERIAL); #endif - LCD(1,F("Ready")); + LCD(3,F("Ready")); } void loop() @@ -149,6 +149,6 @@ void loop() if (freeNow < ramLowWatermark) { ramLowWatermark = freeNow; - LCD(2,F("Free RAM=%5db"), ramLowWatermark); + LCD(3,F("Free RAM=%5db"), ramLowWatermark); } } diff --git a/DCC.cpp b/DCC.cpp index dec6646..25cd118 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -683,9 +683,13 @@ int DCC::nextLoco = 0; //ACK MANAGER ackOp const * DCC::ackManagerProg; +ackOp const * DCC::ackManagerProgStart; byte DCC::ackManagerByte; byte DCC::ackManagerStash; int DCC::ackManagerWord; +byte DCC::ackManagerRetry; +byte DCC::ackRetry = 2; +int16_t DCC::ackRetrySum; int DCC::ackManagerCv; byte DCC::ackManagerBitNum; bool DCC::ackReceived; @@ -718,6 +722,8 @@ void DCC::ackManagerSetup(int cv, byte byteValueOrBitnum, ackOp const program[] ackManagerCv = cv; ackManagerProg = program; + ackManagerProgStart = program; + ackManagerRetry = ackRetry; ackManagerByte = byteValueOrBitnum; ackManagerBitNum=byteValueOrBitnum; ackManagerCallback = callback; @@ -901,6 +907,15 @@ void DCC::ackManagerLoop() { } void DCC::callback(int value) { + // check for automatic retry + if (value == -1 && ackManagerRetry > 0) { + ackRetrySum ++; + StringFormatter::lcd(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); + ackManagerRetry --; + ackManagerProg = ackManagerProgStart; + return; + } + static unsigned long callbackStart; // We are about to leave programming mode // Rule 1: If we have written to a decoder we must maintain power for 100mS diff --git a/DCC.h b/DCC.h index 1bdd5f0..cf1680f 100644 --- a/DCC.h +++ b/DCC.h @@ -64,8 +64,10 @@ enum CALLBACK_STATE : byte { // Allocations with memory implications..! // Base system takes approx 900 bytes + 8 per loco. Turnouts, Sensors etc are dynamically created -#ifdef ARDUINO_AVR_UNO +#if defined(ARDUINO_AVR_UNO) const byte MAX_LOCOS = 20; +#elif defined(ARDUINO_AVR_NANO) +const byte MAX_LOCOS = 30; #else const byte MAX_LOCOS = 50; #endif @@ -113,6 +115,10 @@ public: static inline void setGlobalSpeedsteps(byte s) { globalSpeedsteps = s; }; + static inline void setAckRetry(byte retry) { + ackRetry = retry; + ackRetrySum = 0; // reset running total + }; private: struct LOCO @@ -141,9 +147,13 @@ private: // ACK MANAGER static ackOp const *ackManagerProg; + static ackOp const *ackManagerProgStart; static byte ackManagerByte; static byte ackManagerBitNum; static int ackManagerCv; + static byte ackManagerRetry; + static byte ackRetry; + static int16_t ackRetrySum; static int ackManagerWord; static byte ackManagerStash; static bool ackReceived; diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 2c50ac1..ef25606 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -32,6 +32,16 @@ #include "DIAG.h" #include +//////////////////////////////////////////////////////////////////////////////// +// +// Figure out if we have enough memory for advanced features +// +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) +// nope +#else +#define HAS_ENOUGH_MEMORY +#endif + // These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter. // To discover new keyword numbers , use the <$ YOURKEYWORD> command const int16_t HASH_KEYWORD_PROG = -29718; @@ -40,8 +50,6 @@ const int16_t HASH_KEYWORD_JOIN = -30750; const int16_t HASH_KEYWORD_CABS = -11981; const int16_t HASH_KEYWORD_RAM = 25982; const int16_t HASH_KEYWORD_CMD = 9962; -const int16_t HASH_KEYWORD_WIT = 31594; -const int16_t HASH_KEYWORD_WIFI = -5583; const int16_t HASH_KEYWORD_ACK = 3113; const int16_t HASH_KEYWORD_ON = 2657; const int16_t HASH_KEYWORD_DCC = 6436; @@ -49,17 +57,22 @@ const int16_t HASH_KEYWORD_SLOW = -17209; const int16_t HASH_KEYWORD_PROGBOOST = -6353; const int16_t HASH_KEYWORD_EEPROM = -7168; const int16_t HASH_KEYWORD_LIMIT = 27413; -const int16_t HASH_KEYWORD_ETHERNET = -30767; const int16_t HASH_KEYWORD_MAX = 16244; const int16_t HASH_KEYWORD_MIN = 15978; -const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_RESET = 26133; +const int16_t HASH_KEYWORD_RETRY = 25704; const int16_t HASH_KEYWORD_SPEED28 = -17064; const int16_t HASH_KEYWORD_SPEED128 = 25816; const int16_t HASH_KEYWORD_SERVO=27709; const int16_t HASH_KEYWORD_VPIN=-415; const int16_t HASH_KEYWORD_C=67; const int16_t HASH_KEYWORD_T=84; +const int16_t HASH_KEYWORD_LCN = 15137; +#ifdef HAS_ENOUGH_MEMORY +const int16_t HASH_KEYWORD_WIFI = -5583; +const int16_t HASH_KEYWORD_ETHERNET = -30767; +const int16_t HASH_KEYWORD_WIT = 31594; +#endif int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS]; bool DCCEXParser::stashBusy; @@ -257,6 +270,7 @@ void DCCEXParser::parse(const FSH * cmd) { } // See documentation on DCC class for info on this section + void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) { (void)EEPROM; // tell compiler not to warn this is unused @@ -455,6 +469,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) if (mode == POWERMODE::OFF) DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off StringFormatter::send(stream, F("\n"), opcode); + StringFormatter::lcd(2, F("p%c"), opcode); return; } switch (p[0]) @@ -462,6 +477,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) case HASH_KEYWORD_MAIN: DCCWaveform::mainTrack.setPowerMode(mode); StringFormatter::send(stream, F("\n"), opcode); + StringFormatter::lcd(2, F("p%c MAIN"), opcode); return; case HASH_KEYWORD_PROG: @@ -469,6 +485,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) if (mode == POWERMODE::OFF) DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off StringFormatter::send(stream, F("\n"), opcode); + StringFormatter::lcd(2, F("p%c PROG"), opcode); return; case HASH_KEYWORD_JOIN: DCCWaveform::mainTrack.setPowerMode(mode); @@ -477,9 +494,13 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) { DCC::setProgTrackSyncMain(true); StringFormatter::send(stream, F("\n"), opcode); + StringFormatter::lcd(2, F("p1 JOIN")); } else + { StringFormatter::send(stream, F("\n")); + StringFormatter::lcd(2, F("p0")); + } return; } break; @@ -784,17 +805,21 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) StringFormatter::send(stream, F("Free memory=%d\n"), minimumFreeMemory()); break; - case HASH_KEYWORD_ACK: // + case HASH_KEYWORD_ACK: // if (params >= 3) { if (p[1] == HASH_KEYWORD_LIMIT) { DCCWaveform::progTrack.setAckLimit(p[2]); - StringFormatter::send(stream, F("Ack limit=%dmA\n"), p[2]); + StringFormatter::lcd(1, F("Ack Limit=%dmA"), p[2]); // } else if (p[1] == HASH_KEYWORD_MIN) { DCCWaveform::progTrack.setMinAckPulseDuration(p[2]); - StringFormatter::send(stream, F("Ack min=%dus\n"), p[2]); + StringFormatter::lcd(0, F("Ack Min=%dus"), p[2]); // } else if (p[1] == HASH_KEYWORD_MAX) { DCCWaveform::progTrack.setMaxAckPulseDuration(p[2]); - StringFormatter::send(stream, F("Ack max=%dus\n"), p[2]); + StringFormatter::lcd(0, F("Ack Max=%dus"), p[2]); // + } else if (p[1] == HASH_KEYWORD_RETRY) { + if (p[2] >255) p[2]=3; + DCC::setAckRetry(p[2]); + StringFormatter::lcd(0, F("Ack Retry=%d"), p[2]); // } } else { StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off")); @@ -806,21 +831,23 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) Diag::CMD = onOff; return true; +#ifdef HAS_ENOUGH_MEMORY case HASH_KEYWORD_WIFI: // Diag::WIFI = onOff; return true; - case HASH_KEYWORD_ETHERNET: // + case HASH_KEYWORD_ETHERNET: // Diag::ETHERNET = onOff; return true; case HASH_KEYWORD_WIT: // Diag::WITHROTTLE = onOff; return true; - + case HASH_KEYWORD_LCN: // Diag::LCN = onOff; return true; +#endif case HASH_KEYWORD_PROGBOOST: DCC::setProgTrackBoost(true); diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index df88e5d..0d23897 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ - #pragma GCC optimize ("-O3") + #include #include "DCCWaveform.h" @@ -46,10 +46,8 @@ void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) { && (mainDriver->getFaultPin() != UNUSED_PIN)); // Only use PWM if both pins are PWM capable. Otherwise JOIN does not work MotorDriver::usePWM= mainDriver->isPWMCapable() && progDriver->isPWMCapable(); - if (MotorDriver::usePWM) - DIAG(F("Signal pin config: high accuracy waveform")); - else - DIAG(F("Signal pin config: normal accuracy waveform")); + DIAG(F("Signal pin config: %S accuracy waveform"), + MotorDriver::usePWM ? F("high") : F("normal") ); DCCTimer::begin(DCCWaveform::interruptHandler); } @@ -58,6 +56,8 @@ void DCCWaveform::loop(bool ackManagerActive) { progTrack.checkPowerOverload(ackManagerActive); } +#pragma GCC push_options +#pragma GCC optimize ("-O3") void DCCWaveform::interruptHandler() { // call the timer edge sensitive actions for progtrack and maintrack // member functions would be cleaner but have more overhead @@ -79,7 +79,7 @@ void DCCWaveform::interruptHandler() { else if (progTrack.ackPending) progTrack.checkAck(); } - +#pragma GCC push_options // An instance of this class handles the DCC transmissions for one track. (main or prog) // Interrupts are marshalled via the statics. @@ -124,6 +124,8 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) { if (!isMainTrack && !ackManagerActive && !progTrackSyncMain && !progTrackBoosted) tripValue=progTripValue; + // Trackname for diag messages later + const FSH*trackname = isMainTrack ? F("MAIN") : F("PROG"); switch (powerMode) { case POWERMODE::OFF: sampleDelay = POWER_SAMPLE_OFF_WAIT; @@ -141,9 +143,9 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) { } // Write this after the fact as we want to turn on as fast as possible // because we don't know which output actually triggered the fault pin - DIAG(F("*** COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S ***"), isMainTrack ? F("MAIN") : F("PROG")); + DIAG(F("COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S"), trackname); } else { - DIAG(F("*** %S FAULT PIN ACTIVE - OVERLOAD ***"), isMainTrack ? F("MAIN") : F("PROG")); + DIAG(F("%S FAULT PIN ACTIVE - OVERLOAD"), trackname); if (lastCurrent < tripValue) { lastCurrent = tripValue; // exaggerate } @@ -161,7 +163,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) { unsigned int maxmA=motorDriver->raw2mA(tripValue); power_good_counter=0; sampleDelay = power_sample_overload_wait; - DIAG(F("*** %S TRACK POWER OVERLOAD current=%d max=%d offtime=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), mA, maxmA, sampleDelay); + DIAG(F("%S TRACK POWER OVERLOAD current=%d max=%d offtime=%d"), trackname, mA, maxmA, sampleDelay); if (power_sample_overload_wait >= 10000) power_sample_overload_wait = 10000; else @@ -173,7 +175,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) { setPowerMode(POWERMODE::ON); sampleDelay = POWER_SAMPLE_ON_WAIT; // Debug code.... - DIAG(F("*** %S TRACK POWER RESET delay=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), sampleDelay); + DIAG(F("%S TRACK POWER RESET delay=%d"), trackname, sampleDelay); break; default: sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning. @@ -197,6 +199,8 @@ const bool DCCWaveform::signalTransform[]={ /* WAVE_LOW_0 -> */ LOW, /* WAVE_PENDING (should not happen) -> */ LOW}; +#pragma GCC push_options +#pragma GCC optimize ("-O3") void DCCWaveform::interrupt2() { // calculate the next bit to be sent: // set state WAVE_MID_1 for a 1=bit @@ -252,7 +256,7 @@ void DCCWaveform::interrupt2() { } } } - +#pragma GCC pop_options // Wait until there is no packet pending, then make this pending @@ -306,6 +310,8 @@ byte DCCWaveform::getAck() { return(0); // pending set off but not detected means no ACK. } +#pragma GCC push_options +#pragma GCC optimize ("-O3") void DCCWaveform::checkAck() { // This function operates in interrupt() time so must be fast and can't DIAG if (sentResetsSincePacket > 6) { //ACK timeout @@ -355,3 +361,4 @@ void DCCWaveform::checkAck() { } ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge } +#pragma GCC pop_options diff --git a/Sensors.cpp b/Sensors.cpp index fc8abb5..4d3283a 100644 --- a/Sensors.cpp +++ b/Sensors.cpp @@ -310,7 +310,8 @@ void Sensor::load(){ struct SensorData data; Sensor *tt; - for(uint16_t i=0;idata.nSensors;i++){ + uint16_t i=EEStore::eeStore->data.nSensors; + while(i--){ EEPROM.get(EEStore::pointer(),data); tt=create(data.snum, data.pin, data.pullUp); EEStore::advance(sizeof(tt->data)); From 161b35ae8496bb2698935fbacaa82053dbabadbc Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 23 Aug 2021 12:35:42 +0100 Subject: [PATCH 048/125] indentation and LCD macro use No actual code change. --- DCC.cpp | 2 +- DCCEXParser.cpp | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/DCC.cpp b/DCC.cpp index 25cd118..e3dd921 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -910,7 +910,7 @@ void DCC::callback(int value) { // check for automatic retry if (value == -1 && ackManagerRetry > 0) { ackRetrySum ++; - StringFormatter::lcd(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); + LCD(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); ackManagerRetry --; ackManagerProg = ackManagerProgStart; return; diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index ef25606..27be7f3 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -469,7 +469,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) if (mode == POWERMODE::OFF) DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off StringFormatter::send(stream, F("\n"), opcode); - StringFormatter::lcd(2, F("p%c"), opcode); + LCD(2, F("p%c"), opcode); return; } switch (p[0]) @@ -477,7 +477,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) case HASH_KEYWORD_MAIN: DCCWaveform::mainTrack.setPowerMode(mode); StringFormatter::send(stream, F("\n"), opcode); - StringFormatter::lcd(2, F("p%c MAIN"), opcode); + LCD(2, F("p%c MAIN"), opcode); return; case HASH_KEYWORD_PROG: @@ -485,7 +485,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) if (mode == POWERMODE::OFF) DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off StringFormatter::send(stream, F("\n"), opcode); - StringFormatter::lcd(2, F("p%c PROG"), opcode); + LCD(2, F("p%c PROG"), opcode); return; case HASH_KEYWORD_JOIN: DCCWaveform::mainTrack.setPowerMode(mode); @@ -494,13 +494,13 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream) { DCC::setProgTrackSyncMain(true); StringFormatter::send(stream, F("\n"), opcode); - StringFormatter::lcd(2, F("p1 JOIN")); + LCD(2, F("p1 JOIN")); } else - { + { StringFormatter::send(stream, F("\n")); - StringFormatter::lcd(2, F("p0")); - } + LCD(2, F("p0")); + } return; } break; @@ -809,17 +809,17 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) if (params >= 3) { if (p[1] == HASH_KEYWORD_LIMIT) { DCCWaveform::progTrack.setAckLimit(p[2]); - StringFormatter::lcd(1, F("Ack Limit=%dmA"), p[2]); // + LCD(1, F("Ack Limit=%dmA"), p[2]); // } else if (p[1] == HASH_KEYWORD_MIN) { DCCWaveform::progTrack.setMinAckPulseDuration(p[2]); - StringFormatter::lcd(0, F("Ack Min=%dus"), p[2]); // + LCD(0, F("Ack Min=%dus"), p[2]); // } else if (p[1] == HASH_KEYWORD_MAX) { DCCWaveform::progTrack.setMaxAckPulseDuration(p[2]); - StringFormatter::lcd(0, F("Ack Max=%dus"), p[2]); // + LCD(0, F("Ack Max=%dus"), p[2]); // } else if (p[1] == HASH_KEYWORD_RETRY) { if (p[2] >255) p[2]=3; DCC::setAckRetry(p[2]); - StringFormatter::lcd(0, F("Ack Retry=%d"), p[2]); // + LCD(0, F("Ack Retry=%d"), p[2]); // } } else { StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off")); From f0cd96fed3985a8dd3ebd54537aeb047c48473d2 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Mon, 23 Aug 2021 12:43:14 +0100 Subject: [PATCH 049/125] Changes associated with RCN-213 DCC Accessory Packet format --- DCCEXParser.cpp | 4 ++-- LCDDisplay.h | 4 +--- Turnouts.cpp | 42 ++++++++++++++++++++++++++---------------- Turnouts.h | 23 ++++++++++++++--------- config.example.h | 23 +++++++++++++++++++++++ 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 2c50ac1..a6a68d8 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -682,10 +682,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) // By default turnout command uses 0=throw, 1=close, // but legacy DCC++ behaviour is 1=throw, 0=close. case 0: - state = Turnout::useLegacyTurnoutBehaviour; + state = Turnout::useClassicTurnoutCommands; break; case 1: - state = !Turnout::useLegacyTurnoutBehaviour; + state = !Turnout::useClassicTurnoutCommands; break; case HASH_KEYWORD_C: state = true; diff --git a/LCDDisplay.h b/LCDDisplay.h index 15ba524..6c6e150 100644 --- a/LCDDisplay.h +++ b/LCDDisplay.h @@ -21,9 +21,7 @@ #include #include "DisplayInterface.h" -#if __has_include ( "config.h") - #include "config.h" -#endif +#include "defines.h" // includes config.h as well // Allow maximum message length to be overridden from config.h #if !defined(MAX_MSG_SIZE) diff --git a/Turnouts.cpp b/Turnouts.cpp index 7b87153..a959cdd 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -20,13 +20,8 @@ * along with CommandStation. If not, see . */ -// Set the following definition to true for = throw and = close -// or to false for = close and = throw (the original way). -#ifndef USE_LEGACY_TURNOUT_BEHAVIOUR -#define USE_LEGACY_TURNOUT_BEHAVIOUR false -#endif -#include "defines.h" +#include "defines.h" // includes config.h #include "EEStore.h" #include "StringFormatter.h" #include "RMFT2.h" @@ -47,7 +42,12 @@ * Public static data */ int Turnout::turnoutlistHash = 0; - bool Turnout::useLegacyTurnoutBehaviour = USE_LEGACY_TURNOUT_BEHAVIOUR; + + #if defined(USE_RCN_213_TURNOUT_COMMANDS) + const bool Turnout::useClassicTurnoutCommands = false; + #else + const bool Turnout::useClassicTurnoutCommands = true; + #endif /* * Protected static functions @@ -74,9 +74,11 @@ turnoutlistHash++; } + // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed; + // if consistency with RCN-213 has been selected, it is 0 for thrown and 1 for closed. void Turnout::printState(Print *stream) { StringFormatter::send(stream, F("\n"), - _turnoutData.id, _turnoutData.closed ^ useLegacyTurnoutBehaviour); + _turnoutData.id, _turnoutData.closed ^ useClassicTurnoutCommands); } // Remove nominated turnout from turnout linked list and delete the object. @@ -279,10 +281,12 @@ return tt; } + // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed; + // if consistency with RCN-213 has been selected, it is 0 for thrown and 1 for closed. void ServoTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); + _turnoutData.closed ^ useClassicTurnoutCommands); } // ServoTurnout-specific code for throwing or closing a servo turnout. @@ -312,6 +316,12 @@ * *************************************************************************************/ +#if defined(DCC_TURNOUTS_RCN_213) + const bool DCCTurnout::rcn213Compliant = true; +#else + const bool DCCTurnout::rcn213Compliant = false; +#endif + // DCCTurnoutData contains data specific to this subclass that is // written to EEPROM when the turnout is saved. struct DCCTurnoutData { @@ -363,19 +373,19 @@ void DCCTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); - // Also report using classic DCC++ syntax for DCC accessory turnouts + _turnoutData.closed ^ useClassicTurnoutCommands); + // Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this. StringFormatter::send(stream, F("\n"), _turnoutData.id, (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useLegacyTurnoutBehaviour); + _turnoutData.closed ^ useClassicTurnoutCommands); } bool DCCTurnout::setClosedInternal(bool close) { // DCC++ Classic behaviour is that Throw writes a 1 in the packet, // and Close writes a 0. - // RCN-214 specifies that Throw is 0 and Close is 1. + // RCN-213 specifies that Throw is 0 and Close is 1. DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), - ((_dccTurnoutData.address-1) & 3), close ^ useLegacyTurnoutBehaviour); + ((_dccTurnoutData.address-1) & 3), close ^ !rcn213Compliant); _turnoutData.closed = close; return true; } @@ -439,7 +449,7 @@ void VpinTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); + _turnoutData.closed ^ useClassicTurnoutCommands); } bool VpinTurnout::setClosedInternal(bool close) { @@ -503,6 +513,6 @@ void LCNTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, - _turnoutData.closed ^ useLegacyTurnoutBehaviour); + _turnoutData.closed ^ useClassicTurnoutCommands); } diff --git a/Turnouts.h b/Turnouts.h index 45f60a6..27b1444 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -104,8 +104,8 @@ public: * Static data */ static int turnoutlistHash; - static bool useLegacyTurnoutBehaviour; - + static const bool useClassicTurnoutCommands; + /* * Public base class functions */ @@ -182,11 +182,12 @@ private: } _servoTurnoutData; // 6 bytes // Constructor - ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true); + ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed); public: // Create function - static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = true); + // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. + static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = !useClassicTurnoutCommands); // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. static Turnout *load(struct TurnoutData *turnoutData); @@ -223,6 +224,8 @@ public: static Turnout *load(struct TurnoutData *turnoutData); void print(Print *stream) override; + static const bool rcn213Compliant; + protected: bool setClosedInternal(bool close) override; void save() override; @@ -243,11 +246,12 @@ private: } _vpinTurnoutData; // 2 bytes // Constructor - VpinTurnout(uint16_t id, VPIN vpin, bool closed=true); + VpinTurnout(uint16_t id, VPIN vpin, bool closed); public: // Create function - static Turnout *create(uint16_t id, VPIN vpin, bool closed=true); + // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. + static Turnout *create(uint16_t id, VPIN vpin, bool closed=!useClassicTurnoutCommands); // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. static Turnout *load(struct TurnoutData *turnoutData); @@ -270,12 +274,13 @@ private: // struct LCNTurnoutData { // } _lcnTurnoutData; // 0 bytes - // Constructor - LCNTurnout(uint16_t id, bool closed=true); + // Constructor + LCNTurnout(uint16_t id, bool closed); public: // Create function - static Turnout *create(uint16_t id, bool closed=true); + // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. + static Turnout *create(uint16_t id, bool closed=!useClassicTurnoutCommands); bool setClosedInternal(bool close) override; diff --git a/config.example.h b/config.example.h index 0debbc2..3575cb0 100644 --- a/config.example.h +++ b/config.example.h @@ -129,4 +129,27 @@ The configuration file for DCC-EX Command Station #define SCROLLMODE 1 ///////////////////////////////////////////////////////////////////////////////////// +// +// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213 +// +// According to norm RCN-213 a DCC packet with a 1 is closed/straight +// and one with a 0 is thrown/diverging. In DCC++ Classic, and in previous +// versions of DCC++EX, a throw command was implemented in the packet as +// '1' and a close command as '0'. The #define below makes the states +// match with the norm. But we don't want to cause havoc on existent layouts, +// so we define this only for new installations. If you don't want this, +// don't add it to your config.h. +#define DCC_TURNOUTS_RCN_213 +// In addition to the above, there is an option to allow the values in the commands +// sent and received from JMRI to be changed to be consistent with the definition in +// RCN-213. In DCC++ Classic and in previous versions of DCC++EX, a command +// requested a 'throw' and requested a 'close'. +// The macro below, when present, allows this behaviour to be reversed so that a +// requests the turnout to 'close' and requests it to 'throw'. +// This should only be used if the command processor (JMRI) writing to the serial port +// supports it, otherwise turnout operation commands received over the serial port +// will be reversed. +//#define USE_RCN_213_TURNOUT_COMMANDS + +///////////////////////////////////////////////////////////////////////////////////// From 69c4733f2b2c94837cdde5b14a5073977ae1eea0 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Mon, 23 Aug 2021 15:26:23 +0100 Subject: [PATCH 050/125] Initialise turnouts to Closed by default Ensure that the servo, VPIN and LCN turnouts are all initialised to closed if no initial state is provided in the create call or in EEPROM. This applies irrespective of the RCN-213 configuration settings. --- Turnouts.h | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Turnouts.h b/Turnouts.h index 27b1444..e1cf075 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -186,8 +186,7 @@ private: public: // Create function - // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. - static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed = !useClassicTurnoutCommands); + static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed=true); // Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point. static Turnout *load(struct TurnoutData *turnoutData); @@ -223,7 +222,7 @@ public: // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. static Turnout *load(struct TurnoutData *turnoutData); void print(Print *stream) override; - + // Flag whether DCC Accessory packets are to contain 1=close/0=throw(RCN-213) or 1=throw/0-close (DCC++ Classic) static const bool rcn213Compliant; protected: @@ -250,8 +249,7 @@ private: public: // Create function - // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. - static Turnout *create(uint16_t id, VPIN vpin, bool closed=!useClassicTurnoutCommands); + static Turnout *create(uint16_t id, VPIN vpin, bool closed=true); // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. static Turnout *load(struct TurnoutData *turnoutData); @@ -279,8 +277,7 @@ private: public: // Create function - // If the initial state isn't specified, use true for RCN-213 consistency and false for DCC++ classic compatibility. - static Turnout *create(uint16_t id, bool closed=!useClassicTurnoutCommands); + static Turnout *create(uint16_t id, bool closed=true); bool setClosedInternal(bool close) override; From 0d235b65d3e91a539dd57bb925d16e933ed26534 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Mon, 23 Aug 2021 17:36:50 +0100 Subject: [PATCH 051/125] Turnouts - make code clearer. Overlay of flags bits added in struct TurnoutData,, called flags. This simplifies the the EEPROM update code. --- Turnouts.cpp | 6 +++--- Turnouts.h | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index a959cdd..8c022d0 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -139,7 +139,7 @@ // Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress // is always zero for LCN turnouts. if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0) - EEPROM.put(tt->_eepromAddress, *((uint8_t *) &tt->_turnoutData)); + EEPROM.put(tt->_eepromAddress, tt->_turnoutData.flags); #if defined(RMFT_ACTIVE) RMFT2::turnoutEvent(id, closeFlag); @@ -174,7 +174,7 @@ Turnout *tt = 0; // Read turnout type from EEPROM struct TurnoutData turnoutData; - int eepromAddress = EEStore::pointer(); // Address of byte containing the closed flag. + int eepromAddress = EEStore::pointer() + offsetof(struct TurnoutData, flags); // Address of byte containing the closed flag. EEPROM.get(EEStore::pointer(), turnoutData); EEStore::advance(sizeof(turnoutData)); @@ -198,7 +198,7 @@ } if (tt) { // Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero. - tt->_eepromAddress = eepromAddress; + tt->_eepromAddress = eepromAddress + offsetof(struct TurnoutData, flags); } #ifdef EESTOREDEBUG diff --git a/Turnouts.h b/Turnouts.h index e1cf075..9c14089 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -53,9 +53,14 @@ protected: // vice versa. If the turnout has been saved, then this byte is rewritten // when changed in RAM. The 'closed' flag must be located in the first byte. struct TurnoutData { - bool closed : 1; - bool _rfu: 2; - uint8_t turnoutType : 5; + union { + struct { + bool closed : 1; + bool _rfu: 2; + uint8_t turnoutType : 5; + }; + uint8_t flags; + }; uint16_t id; } _turnoutData; // 3 bytes From 425de3fcc79fd8fe4262b8ec8aee617e05580d97 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Mon, 23 Aug 2021 20:41:30 +0100 Subject: [PATCH 052/125] Create mySetup.cpp_example.txt Provide an example showing directives for HAL device configuration. --- mySetup.cpp_example.txt | 139 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 mySetup.cpp_example.txt diff --git a/mySetup.cpp_example.txt b/mySetup.cpp_example.txt new file mode 100644 index 0000000..949088a --- /dev/null +++ b/mySetup.cpp_example.txt @@ -0,0 +1,139 @@ +// Sample mySetup.cpp file. +// +// To use this file, copy it to mySetup.cpp and uncomment the directives and/or +// edit them to satisfy your requirements. + +// Note that if the file has a .cpp extension it WILL be compiled into the build +// and the mySetup() function WILL be invoked. +// +// To prevent this, temporarily rename it to mySetup.txt or similar. +// + +#include "IODevice.h" +#include "Turnouts.h" +#include "Sensors.h" +#include "IO_HCSR04.h" + + +// The #if directive prevent compile errors for Uno and Nano by excluding the +// HAL directives from the build. +#if !defined(IO_NO_HAL) + + +// Examples of statically defined HAL directives (alternative to the create() call). +// These have to be outside of the mySetup() function. + + +// The following directive defines a PCA9685 PWM Servo driver module. +// The parameters are: +// First Vpin=100 +// Number of VPINs=16 (numbered 100-115) +// I2C address of module=0x40 + +//PCA9685 pwmModule1(100, 16, 0x40); + + +// The following directive defines an MCP23017 16-port I2C GPIO Extender module. +// The parameters are: +// First Vpin=164 +// Number of VPINs=16 (numbered 164-179) +// I2C address of module=0x20 + +//MCP23017 gpioModule2(164, 16, 0x20); + + +// Alternative form, which allows the INT pin of the module to request a scan +// by pulling Arduino pin 40 to ground. Means that the I2C isn't being polled +// all the time, only when a change takes place. Multiple modules' INT pins +// may be connected to the same Arduino pin. + +//MCP23017 gpioModule2(164, 16, 0x20, 40); + + +// The following directive defines an MCP23008 8-port I2C GPIO Extender module. +// The parameters are: +// First Vpin=300 +// Number of VPINs=8 (numbered 300-307) +// I2C address of module=0x22 + +//MCP23017 gpioModule3(300, 8, 0x22); + + +// The following directive defines a PCF8574 8-port I2C GPIO Extender module. +// The parameters are: +// First Vpin=200 +// Number of VPINs=8 (numbered 200-207) +// I2C address of module=0x23 + +//PCF8574 gpioModule4(200, 8, 0x23); + + +// Alternative form using INT pin (see above) + +//PCF8574 gpioModule4(200, 8, 0x23, 40); + + +// The following directive defines an HCSR04 ultrasonic module. +// The parameters are: +// Vpin=2000 (only one VPIN per directive) +// Number of VPINs=1 +// Arduino pin connected to TRIG=30 +// Arduino pin connected to ECHO=31 +// Minimum trigger range=20cm (VPIN goes to 1 when <20cm) +// Maximum trigger range=25cm (VPIN goes to 0 when >25cm) +// Note: Multiple devices can be configured by using a different ECHO pin +// for each one. The TRIG pin can be shared between multiple devices. +// Be aware that the 'ping' of one device may be received by another +// device and position them accordingly! + +//HCSR04 sonarModule1(2000, 30, 31, 20, 25); +//HCSR04 sonarModule2(2001, 30, 32, 20, 25); + + +// The function mySetup() is invoked from CS if it exists within the build. +// It is called just before mysetup.h is executed, so things set up within here can be +// referenced by commands in mySetup.h. + +void mySetup() { + + // Alternative way of creating MCP23017, which has to be within the mySetup() function + // The other devices can also be created in this way. The parameter lists for the + // create() function are identical to the parameter lists for the declarations. + + //MCP23017::create(180, 16, 0x21); + + + // Creating a Turnout + // Parameters: same as command for Servo turnouts + // ID and VPIN are 100, sonar moves between positions 102 and 490 with slow profile. + // Profile may be Instant, Fast, Medium, Slow or Bounce. + + //ServoTurnout::create(100, 100, 490, 102, PCA9685::Slow); + + + // DCC Accessory turnout + // Parameters: same as command for DCC Accessory turnouts + // ID=3000 + // Decoder address=23 + // Decoder subaddress = 1 + + //DCCTurnout::create(3000, 23, 1); + + + // Creating a Sensor + // Parameters: As for the command, + // id = 164, + // Vpin = 164 (configured above as pin 0 of an MCP23017) + // Pullup enable = 1 (enabled) + + //Sensor::create(164, 164, 1); + + + // Way of creating lots of identical sensors in a range + + //for (int i=165; i<180; i++) + // Sensor::create(i, i, 1); + +} + +#endif From 8b498b8b495063d1ae40c2b70a1e528964144ae4 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Tue, 24 Aug 2021 09:45:11 +0100 Subject: [PATCH 053/125] cmd for JMRI/Withrottle --- RMFT2.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 1ccedf9..13627bd 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -36,6 +36,7 @@ const int16_t HASH_KEYWORD_UNLATCH=1353; const int16_t HASH_KEYWORD_PAUSE=-4142; const int16_t HASH_KEYWORD_RESUME=27609; const int16_t HASH_KEYWORD_KILL=5218; +const int16_t HASH_KEYWORD_ROUTES=-3702; // 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. @@ -192,7 +193,14 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { task->loco=cab; } return true; - + + case HASH_KEYWORD_ROUTES: // JMRI withrottle support + if (paramCount>1) return false; + StringFormatter::send(stream,F("")); + return true; + default: break; } From c45337d5d488048bea04c721fdea77b793791085 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 24 Aug 2021 22:13:52 +0100 Subject: [PATCH 054/125] Enable pullups for Arduino input pins as a default (to match GPIO Extender modules). --- IODevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IODevice.cpp b/IODevice.cpp index 43db2f6..f151923 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -292,7 +292,7 @@ ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) { _pinPullups = (uint8_t *)calloc(2, arrayLen); _pinModes = (&_pinPullups[0]) + arrayLen; for (int i=0; i Date: Tue, 24 Aug 2021 22:15:50 +0100 Subject: [PATCH 055/125] Enable pullups for Arduino input pins as a default --- IODevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IODevice.cpp b/IODevice.cpp index f151923..e7b9dbd 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -292,7 +292,7 @@ ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) { _pinPullups = (uint8_t *)calloc(2, arrayLen); _pinModes = (&_pinPullups[0]) + arrayLen; for (int i=0; i Date: Tue, 24 Aug 2021 22:18:51 +0100 Subject: [PATCH 056/125] Revert to original DCC++ Classic Turnout command polarity. Revert to command being 'throw' and being 'close', for turnouts. --- DCCEXParser.cpp | 11 +++-------- Turnouts.cpp | 26 ++++++++++---------------- config.example.h | 11 ----------- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index a6a68d8..f44df2f 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -679,22 +679,17 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) { bool state = false; switch (p[1]) { - // By default turnout command uses 0=throw, 1=close, - // but legacy DCC++ behaviour is 1=throw, 0=close. + // Turnout messages use 1=throw, 0=close. case 0: - state = Turnout::useClassicTurnoutCommands; - break; - case 1: - state = !Turnout::useClassicTurnoutCommands; - break; case HASH_KEYWORD_C: state = true; break; + case 1: case HASH_KEYWORD_T: state= false; break; default: - return false; + return false; // Invalid parameter } if (!Turnout::setClosed(p[0], state)) return false; diff --git a/Turnouts.cpp b/Turnouts.cpp index 8c022d0..552cd9d 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -42,12 +42,6 @@ * Public static data */ int Turnout::turnoutlistHash = 0; - - #if defined(USE_RCN_213_TURNOUT_COMMANDS) - const bool Turnout::useClassicTurnoutCommands = false; - #else - const bool Turnout::useClassicTurnoutCommands = true; - #endif /* * Protected static functions @@ -75,10 +69,9 @@ } // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed; - // if consistency with RCN-213 has been selected, it is 0 for thrown and 1 for closed. void Turnout::printState(Print *stream) { StringFormatter::send(stream, F("\n"), - _turnoutData.id, _turnoutData.closed ^ useClassicTurnoutCommands); + _turnoutData.id, !_turnoutData.closed); } // Remove nominated turnout from turnout linked list and delete the object. @@ -207,7 +200,7 @@ return tt; } - // Display, on the specified stream, the current state of the turnout (1 or 0). + // Display, on the specified stream, the current state of the turnout (1=thrown or 0=closed). void Turnout::printState(uint16_t id, Print *stream) { Turnout *tt = get(id); if (!tt) tt->printState(stream); @@ -281,12 +274,11 @@ return tt; } - // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed; - // if consistency with RCN-213 has been selected, it is 0 for thrown and 1 for closed. + // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed void ServoTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, _servoTurnoutData.vpin, _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, - _turnoutData.closed ^ useClassicTurnoutCommands); + !_turnoutData.closed); } // ServoTurnout-specific code for throwing or closing a servo turnout. @@ -373,11 +365,11 @@ void DCCTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useClassicTurnoutCommands); + !_turnoutData.closed); // Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this. StringFormatter::send(stream, F("\n"), _turnoutData.id, (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - _turnoutData.closed ^ useClassicTurnoutCommands); + !_turnoutData.closed); } bool DCCTurnout::setClosedInternal(bool close) { @@ -447,9 +439,10 @@ return tt; } + // Report 1 for thrown, 0 for closed. void VpinTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, _vpinTurnoutData.vpin, - _turnoutData.closed ^ useClassicTurnoutCommands); + !_turnoutData.closed); } bool VpinTurnout::setClosedInternal(bool close) { @@ -511,8 +504,9 @@ //void save() override { } //static Turnout *load(struct TurnoutData *turnoutData) { + // Report 1 for thrown, 0 for closed. void LCNTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, - _turnoutData.closed ^ useClassicTurnoutCommands); + !_turnoutData.closed); } diff --git a/config.example.h b/config.example.h index 3575cb0..6c5c69a 100644 --- a/config.example.h +++ b/config.example.h @@ -141,15 +141,4 @@ The configuration file for DCC-EX Command Station // don't add it to your config.h. #define DCC_TURNOUTS_RCN_213 -// In addition to the above, there is an option to allow the values in the commands -// sent and received from JMRI to be changed to be consistent with the definition in -// RCN-213. In DCC++ Classic and in previous versions of DCC++EX, a command -// requested a 'throw' and requested a 'close'. -// The macro below, when present, allows this behaviour to be reversed so that a -// requests the turnout to 'close' and requests it to 'throw'. -// This should only be used if the command processor (JMRI) writing to the serial port -// supports it, otherwise turnout operation commands received over the serial port -// will be reversed. -//#define USE_RCN_213_TURNOUT_COMMANDS - ///////////////////////////////////////////////////////////////////////////////////// From d0fed2dd3859795fc4cbfc4b3e0b3fc4ef4f77b7 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 24 Aug 2021 23:02:24 +0100 Subject: [PATCH 057/125] Make LCD output to I2C synchronous. Temporary work-around to problems with LCD driver, until I can look at it in depth. --- LiquidCrystal_I2C.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/LiquidCrystal_I2C.cpp b/LiquidCrystal_I2C.cpp index e6b3cdb..865868a 100644 --- a/LiquidCrystal_I2C.cpp +++ b/LiquidCrystal_I2C.cpp @@ -59,6 +59,8 @@ LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols, backlight(); lcdDisplay = this; } + // Initialise request block for comms. + requestBlock.setWriteParams(lcd_Addr, outputBuffer, sizeof(outputBuffer)); } void LiquidCrystal_I2C::begin() { @@ -190,13 +192,15 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) { mode |= _backlightval; uint8_t highnib = (((value >> 4) & 0x0f) << BACKPACK_DATA_BITS) | mode; uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | mode; + // Wait for previous request to complete before writing to outputbuffer. + requestBlock.wait(); // Send both nibbles uint8_t len = 0; outputBuffer[len++] = highnib|En; outputBuffer[len++] = highnib; outputBuffer[len++] = lownib|En; outputBuffer[len++] = lownib; - I2CManager.write(_Addr, outputBuffer, len, &requestBlock); + I2CManager.write(_Addr, outputBuffer, len); } // write 4 data bits to the HD44780 LCD controller. @@ -210,7 +214,7 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) { uint8_t len = 0; outputBuffer[len++] = _data|En; outputBuffer[len++] = _data; - I2CManager.write(_Addr, outputBuffer, len, &requestBlock); + I2CManager.write(_Addr, outputBuffer, len); } // write a byte to the PCF8574 I2C interface. We don't need to set @@ -219,5 +223,5 @@ 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, &requestBlock); + I2CManager.write(_Addr, outputBuffer, 1); } \ No newline at end of file From 80fc9e8a686293cc8c2009755efc73fe44d34e66 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 25 Aug 2021 00:29:57 +0100 Subject: [PATCH 058/125] Make LCD Display I2C calls synchronous. --- LiquidCrystal_I2C.cpp | 6 +++--- LiquidCrystal_I2C.h | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LiquidCrystal_I2C.cpp b/LiquidCrystal_I2C.cpp index 865868a..d7953f7 100644 --- a/LiquidCrystal_I2C.cpp +++ b/LiquidCrystal_I2C.cpp @@ -200,7 +200,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); // Write command synchronously } // write 4 data bits to the HD44780 LCD controller. @@ -214,7 +214,7 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) { uint8_t len = 0; outputBuffer[len++] = _data|En; outputBuffer[len++] = _data; - I2CManager.write(_Addr, outputBuffer, len); + I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously } // write a byte to the PCF8574 I2C interface. We don't need to set @@ -223,5 +223,5 @@ 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); // Write command synchronously } \ No newline at end of file diff --git a/LiquidCrystal_I2C.h b/LiquidCrystal_I2C.h index 6d65541..6cd4384 100644 --- a/LiquidCrystal_I2C.h +++ b/LiquidCrystal_I2C.h @@ -90,7 +90,8 @@ private: I2CRB requestBlock; uint8_t outputBuffer[4]; - bool isBusy() { return requestBlock.isBusy(); } + // I/O is synchronous, so if this is called we're not busy! + bool isBusy() { return false; } }; #endif From fa04fa508448e49042828c5f3812bc8c132a942a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 25 Aug 2021 00:34:19 +0100 Subject: [PATCH 059/125] I2C Manager, adjust loop code. loop() contains startTransaction which is called after handleInterrupt(). However, startTransaction is called within handleInterrupt so remove the extra call. This appears to solve strange problems encountered with the LCD display. --- I2CManager_NonBlocking.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h index 920cecd..41e9283 100644 --- a/I2CManager_NonBlocking.h +++ b/I2CManager_NonBlocking.h @@ -164,8 +164,6 @@ void I2CManagerClass::loop() { #if !defined(I2C_USE_INTERRUPTS) handleInterrupt(); #endif - // If free, initiate next transaction - startTransaction(); checkForTimeout(); } From 2469629cbb8f111a43fb18d194c5f050909880f2 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 25 Aug 2021 10:26:45 +0100 Subject: [PATCH 060/125] Temporarily use Wire for I2C. --- I2CManager.h | 4 ++-- I2CManager_AVR.h | 8 ++++---- I2CManager_NonBlocking.h | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/I2CManager.h b/I2CManager.h index b17accd..342d8d4 100644 --- a/I2CManager.h +++ b/I2CManager.h @@ -111,7 +111,7 @@ * */ -//#define I2C_USE_WIRE +#define I2C_USE_WIRE #ifndef I2C_NO_INTERRUPTS #define I2C_USE_INTERRUPTS #endif @@ -235,7 +235,7 @@ private: // 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 volatile uint8_t state; static I2CRB * volatile currentRequest; static volatile uint8_t txCount; diff --git a/I2CManager_AVR.h b/I2CManager_AVR.h index 9de9bf2..310afa2 100644 --- a/I2CManager_AVR.h +++ b/I2CManager_AVR.h @@ -136,7 +136,7 @@ void I2CManagerClass::I2C_handleInterrupt() { } else { // Nothing left to send or receive TWDR = 0xff; // Default condition = SDA released TWCR = (1< 0) { + if (state==I2C_STATE_ACTIVE && t!=0 && timeout > 0) { // Check for timeout if (currentMicros - startTime > timeout) { // Excessive time. Dequeue request @@ -148,7 +148,7 @@ void I2CManagerClass::checkForTimeout() { // Try close and init, not entirely satisfactory but sort of works... I2C_close(); // Shutdown and restart twi interface I2C_init(); - status = I2C_STATE_FREE; + state = I2C_STATE_FREE; // Initiate next queued request startTransaction(); @@ -178,7 +178,7 @@ void I2CManagerClass::handleInterrupt() { // Experimental -- perform the post processing with interrupts enabled. //interrupts(); - if (status!=I2C_STATUS_PENDING) { + if (state!=I2C_STATE_ACTIVE && state != I2C_STATE_FREE) { // Remove completed request from head of queue I2CRB * t; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { @@ -187,10 +187,10 @@ void I2CManagerClass::handleInterrupt() { queueHead = t->nextRequest; if (!queueHead) queueTail = queueHead; t->nBytes = rxCount; - t->status = status; + t->status = state; } // I2C state machine is now free for next request - status = I2C_STATE_FREE; + state = I2C_STATE_FREE; } // Start next request (if any) I2CManager.startTransaction(); @@ -201,7 +201,7 @@ void I2CManagerClass::handleInterrupt() { 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::state = I2C_STATE_FREE; volatile uint8_t I2CManagerClass::txCount; volatile uint8_t I2CManagerClass::rxCount; volatile uint8_t I2CManagerClass::operation; From 5e30740c5be370fec9201fb53893fd9cdd1637aa Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 26 Aug 2021 21:49:44 +0100 Subject: [PATCH 061/125] fix EXRAIL CALL/RETURN --- RMFT2.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 13627bd..af0040b 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -440,6 +440,7 @@ void RMFT2::loop2() { if (readSensor(operand)) { // reset timer to half a second and keep waiting waitAfter=millis(); + delayMe(50); return; } if (millis()-waitAfter < 500 ) return; @@ -538,7 +539,7 @@ void RMFT2::loop2() { kill(F("CALL stack"), stackDepth); return; } - callStack[stackDepth++]=progCounter; + callStack[stackDepth++]=progCounter+3; progCounter=locateRouteStart(operand); if (progCounter<0) kill(F("CALL unknown"),operand); return; From 0a9fcf6ebccf01269d6e98c3d101adb23e314708 Mon Sep 17 00:00:00 2001 From: Neil McKechnie <75813993+Neil-McK@users.noreply.github.com> Date: Thu, 26 Aug 2021 23:04:13 +0100 Subject: [PATCH 062/125] Neil bugfixes. (#186) * Re-enable native I2C driver. * Minor non-functional changes to native I2C Manager. * Minor changes to make variable types explicit in comparisons. * Fix IODevice::loop() to avoid null pointer dereference. Strange problems with LCD driver tracked down to being caused by a call to p->_loop() when p is NULL. * Correct sense of comparison in LCN support function Turnout::setClosedStateOnly() * Remove code (now unused) from LCD driver. * Add I2C textual error messages. * Add I2C textual error messages. * Fix compile error in 4809 I2C driver. * Remove init function call from SSD1306 driver. --- I2CManager.cpp | 38 +++++++++++++++++++++++++++++++++++--- I2CManager.h | 25 ++++++++++++++++++------- I2CManager_Mega4809.h | 10 +++++----- I2CManager_NonBlocking.h | 35 ++++++++++++++++++----------------- IODevice.cpp | 6 ++++-- IO_GPIOBase.h | 5 +++-- IO_HCSR04.h | 2 +- LiquidCrystal_I2C.cpp | 8 -------- LiquidCrystal_I2C.h | 5 +---- SSD1306Ascii.cpp | 3 --- Turnouts.cpp | 2 +- 11 files changed, 86 insertions(+), 53 deletions(-) diff --git a/I2CManager.cpp b/I2CManager.cpp index 82f5f46..94c4baf 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -147,6 +147,25 @@ uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) { return status; } +/*************************************************************************** + * Get a message corresponding to the error status + ***************************************************************************/ +const FSH *I2CManagerClass::getErrorMessage(uint8_t status) { + switch (status) { + case I2C_STATUS_OK: return F("OK"); + case I2C_STATUS_TRUNCATED: return F("Transmission truncated"); + case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F("No response from device (address NAK)"); + case I2C_STATUS_TRANSMIT_ERROR: return F("Transmit error (data NAK)"); + case I2C_STATUS_OTHER_TWI_ERROR: return F("Other Wire/TWI error"); + case I2C_STATUS_TIMEOUT: return F("Timeout"); + case I2C_STATUS_ARBITRATION_LOST: return F("Arbitration lost"); + case I2C_STATUS_BUS_ERROR: return F("I2C bus error"); + case I2C_STATUS_UNEXPECTED_ERROR: return F("Unexpected error"); + case I2C_STATUS_PENDING: return F("Request pending"); + default: return F("Error code not recognised"); + } +} + /*************************************************************************** * Declare singleton class instance. ***************************************************************************/ @@ -158,12 +177,25 @@ I2CManagerClass I2CManager = I2CManagerClass(); ///////////////////////////////////////////////////////////////////////////// /*************************************************************************** - * Block waiting for request block to complete, and return completion status + * Block waiting for request block to complete, and return completion status. + * Since such a loop could potentially last for ever if the RB status doesn't + * change, we set a high limit (0.1sec, 100ms) on the wait time and, if it + * hasn't changed by that time we assume it's not going to, and just return + * a timeout status. This means that CS will not lock up. ***************************************************************************/ uint8_t I2CRB::wait() { - do + unsigned long waitStart = millis(); + do { I2CManager.loop(); - while (status==I2C_STATUS_PENDING); + // Rather than looping indefinitely, let's set a very high timeout (100ms). + if ((millis() - waitStart) > 100UL) { + DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this); + status = I2C_STATUS_TIMEOUT; + // Note that, although the timeout is posted, the request may yet complete. + // TODO: Ideally we would like to cancel the request. + return status; + } + } while (status==I2C_STATUS_PENDING); return status; } diff --git a/I2CManager.h b/I2CManager.h index 342d8d4..25ab004 100644 --- a/I2CManager.h +++ b/I2CManager.h @@ -111,19 +111,22 @@ * */ -#define I2C_USE_WIRE +//#define I2C_USE_WIRE #ifndef I2C_NO_INTERRUPTS #define I2C_USE_INTERRUPTS #endif // Status codes for I2CRB structures. enum : uint8_t { + // Codes used by Wire and by native drivers I2C_STATUS_OK=0, I2C_STATUS_TRUNCATED=1, - I2C_STATUS_DEVICE_NOT_PRESENT=2, + I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2, I2C_STATUS_TRANSMIT_ERROR=3, - I2C_STATUS_NEGATIVE_ACKNOWLEDGE=4, I2C_STATUS_TIMEOUT=5, + // Code used by Wire only + I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error + // Codes used by native drivers only I2C_STATUS_ARBITRATION_LOST=6, I2C_STATUS_BUS_ERROR=7, I2C_STATUS_UNEXPECTED_ERROR=8, @@ -151,14 +154,16 @@ typedef enum : uint8_t #define I2C_FREQ 400000L #endif -// Struct defining a request context for an I2C operation. -struct I2CRB { +// Class defining a request context for an I2C operation. +class I2CRB { +public: volatile uint8_t status; // Completion status, or pending flag (updated from IRC) volatile uint8_t nBytes; // Number of bytes read (updated from IRC) + inline I2CRB() { status = I2C_STATUS_OK; }; 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); @@ -213,6 +218,10 @@ public: // Loop method void loop(); + // Expand error codes into text. Note that they are in flash so + // need to be printed using FSH. + static const FSH *getErrorMessage(uint8_t status); + private: bool _beginCompleted = false; bool _clockSpeedFixed = false; @@ -258,7 +267,9 @@ private: static void I2C_close(); public: - void setTimeout(unsigned long value) { timeout = value;}; + // setTimeout sets the timout value for I2C transactions. + // TODO: Get I2C timeout working before uncommenting the code below. + void setTimeout(unsigned long value) { (void)value; /* timeout = value; */ }; // handleInterrupt needs to be public to be called from the ISR function! static void handleInterrupt(); diff --git a/I2CManager_Mega4809.h b/I2CManager_Mega4809.h index 18f33e5..551b4f9 100644 --- a/I2CManager_Mega4809.h +++ b/I2CManager_Mega4809.h @@ -105,14 +105,14 @@ void I2CManagerClass::I2C_handleInterrupt() { I2C_sendStart(); // Reinitiate request } else if (currentStatus & TWI_BUSERR_bm) { // Bus error - status = I2C_STATUS_BUS_ERROR; + state = 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; + state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE; } else if (bytesToSend) { // Acked, so send next byte if (currentRequest->operation == OPERATION_SEND_P) @@ -126,7 +126,7 @@ void I2CManagerClass::I2C_handleInterrupt() { } else { // No more data to send/receive. Initiate a STOP condition. TWI0.MCTRLB = TWI_MCMD_STOP_gc; - status = I2C_STATUS_OK; // Done + state = I2C_STATUS_OK; // Done } } else if (currentStatus & TWI_RIF_bm) { // Master read completed without errors @@ -136,7 +136,7 @@ void I2CManagerClass::I2C_handleInterrupt() { } else { // Buffer full, issue nack/stop TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc; - status = I2C_STATUS_OK; + state = I2C_STATUS_OK; } if (bytesToReceive) { // More bytes to receive, issue ack and start another read @@ -144,7 +144,7 @@ void I2CManagerClass::I2C_handleInterrupt() { } else { // Transaction finished, issue NACK and STOP. TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc; - status = I2C_STATUS_OK; + state = I2C_STATUS_OK; } } } diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h index 036504e..3bdfc38 100644 --- a/I2CManager_NonBlocking.h +++ b/I2CManager_NonBlocking.h @@ -59,10 +59,9 @@ void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) { ***************************************************************************/ void I2CManagerClass::startTransaction() { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - I2CRB *t = queueHead; - if ((state == I2C_STATE_FREE) && (t != NULL)) { + if ((state == I2C_STATE_FREE) && (queueHead != NULL)) { state = I2C_STATE_ACTIVE; - currentRequest = t; + currentRequest = queueHead; rxCount = txCount = 0; // Copy key fields to static data for speed. operation = currentRequest->operation; @@ -85,9 +84,9 @@ void I2CManagerClass::queueRequest(I2CRB *req) { queueHead = queueTail = req; // Only item on queue else queueTail = queueTail->nextRequest = req; // Add to end + startTransaction(); } - startTransaction(); } /*************************************************************************** @@ -135,7 +134,7 @@ void I2CManagerClass::checkForTimeout() { unsigned long currentMicros = micros(); ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { I2CRB *t = queueHead; - if (state==I2C_STATE_ACTIVE && t!=0 && timeout > 0) { + if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && timeout > 0) { // Check for timeout if (currentMicros - startTime > timeout) { // Excessive time. Dequeue request @@ -150,7 +149,7 @@ void I2CManagerClass::checkForTimeout() { I2C_init(); state = I2C_STATE_FREE; - // Initiate next queued request + // Initiate next queued request if any. startTransaction(); } } @@ -173,27 +172,29 @@ void I2CManagerClass::loop() { ***************************************************************************/ void I2CManagerClass::handleInterrupt() { + // Update hardware state machine I2C_handleInterrupt(); - // Experimental -- perform the post processing with interrupts enabled. - //interrupts(); - - if (state!=I2C_STATE_ACTIVE && state != I2C_STATE_FREE) { + // Check if current request has completed. If there's a current request + // and state isn't active then state contains the completion status of the request. + if (state != I2C_STATE_ACTIVE && currentRequest != NULL) { // Remove completed request from head of queue - I2CRB * t; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - t = queueHead; - if (t != NULL) { + I2CRB * t = queueHead; + if (t == queueHead) { queueHead = t->nextRequest; if (!queueHead) queueTail = queueHead; t->nBytes = rxCount; t->status = state; + + // I2C state machine is now free for next request + currentRequest = NULL; + state = I2C_STATE_FREE; + + // Start next request (if any) + I2CManager.startTransaction(); } - // I2C state machine is now free for next request - state = I2C_STATE_FREE; } - // Start next request (if any) - I2CManager.startTransaction(); } } diff --git a/IODevice.cpp b/IODevice.cpp index e7b9dbd..3269abe 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -69,8 +69,10 @@ 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; + if (_nextLoopDevice) { + _nextLoopDevice->_loop(currentMicros); + _nextLoopDevice = _nextLoopDevice->_nextDevice; + } // Report loop time if diags enabled #if defined(DIAG_LOOPTIMES) diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 7179f9f..6e4a032 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -145,7 +145,8 @@ void GPIOBase::_loop(unsigned long currentMicros) { _deviceState = DEVSTATE_NORMAL; } else { _deviceState = DEVSTATE_FAILED; - DIAG(F("%S I2C:x%x Error:%d"), _deviceName, _I2CAddress, status); + DIAG(F("%S I2C:x%x Error:%d %S"), _deviceName, _I2CAddress, status, + I2CManager.getErrorMessage(status)); } _processCompletion(status); @@ -174,7 +175,7 @@ void GPIOBase::_loop(unsigned long currentMicros) { if (digitalRead(_gpioInterruptPin)) return; } else // No interrupt pin. Check if tick has elapsed. If not, finish. - if (currentMicros - _lastLoopEntry < _portTickTime) return; + if (currentMicros - _lastLoopEntry < (unsigned long)_portTickTime) return; // TODO: Could suppress reads if there are no pins configured as inputs! diff --git a/IO_HCSR04.h b/IO_HCSR04.h index 5234fe1..e66c343 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -102,7 +102,7 @@ protected: // _loop function - read HC-SR04 once every 50 milliseconds. void _loop(unsigned long currentMicros) override { - if (currentMicros - _lastExecutionTime > 50000) { + if (currentMicros - _lastExecutionTime > 50000UL) { _lastExecutionTime = currentMicros; _value = read_HCSR04device(); diff --git a/LiquidCrystal_I2C.cpp b/LiquidCrystal_I2C.cpp index d7953f7..e036b98 100644 --- a/LiquidCrystal_I2C.cpp +++ b/LiquidCrystal_I2C.cpp @@ -59,8 +59,6 @@ LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols, backlight(); lcdDisplay = this; } - // Initialise request block for comms. - requestBlock.setWriteParams(lcd_Addr, outputBuffer, sizeof(outputBuffer)); } void LiquidCrystal_I2C::begin() { @@ -192,8 +190,6 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) { mode |= _backlightval; uint8_t highnib = (((value >> 4) & 0x0f) << BACKPACK_DATA_BITS) | mode; uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | mode; - // Wait for previous request to complete before writing to outputbuffer. - requestBlock.wait(); // Send both nibbles uint8_t len = 0; outputBuffer[len++] = highnib|En; @@ -209,8 +205,6 @@ 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; @@ -220,8 +214,6 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) { // 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); // Write command synchronously } \ No newline at end of file diff --git a/LiquidCrystal_I2C.h b/LiquidCrystal_I2C.h index 6cd4384..6881a69 100644 --- a/LiquidCrystal_I2C.h +++ b/LiquidCrystal_I2C.h @@ -75,10 +75,8 @@ public: void backlight(); void command(uint8_t); - void init(); private: - void init_priv(); void send(uint8_t, uint8_t); void write4bits(uint8_t); void expanderWrite(uint8_t); @@ -88,10 +86,9 @@ private: uint8_t _displaymode; uint8_t _backlightval; - I2CRB requestBlock; uint8_t outputBuffer[4]; // I/O is synchronous, so if this is called we're not busy! - bool isBusy() { return false; } + bool isBusy() override { return false; } }; #endif diff --git a/SSD1306Ascii.cpp b/SSD1306Ascii.cpp index 07a7883..17fd5a2 100644 --- a/SSD1306Ascii.cpp +++ b/SSD1306Ascii.cpp @@ -99,9 +99,6 @@ 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++) { diff --git a/Turnouts.cpp b/Turnouts.cpp index 552cd9d..e309604 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -107,7 +107,7 @@ bool Turnout::setClosedStateOnly(uint16_t id, bool close) { Turnout *tt = get(id); - if (tt) return false; + if (!tt) return false; tt->_turnoutData.closed = close; return true; } From 0c218e1e13e814d78ccf1a0f5fae9f1afec2128f Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 10:58:00 +0100 Subject: [PATCH 063/125] Add HAL function configureInput(vpin,...) and configureServo(vpin,...). --- IODevice.h | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/IODevice.h b/IODevice.h index eaffec8..691322c 100644 --- a/IODevice.h +++ b/IODevice.h @@ -112,6 +112,18 @@ public: // configure is used invoke an IODevice instance's _configure method static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]); + // User-friendly function for configuring an input pin. + inline static bool configureInput(VPIN vpin, bool pullupEnable) { + int params[] = {pullupEnable}; + return IODevice::configure(vpin, CONFIGURE_INPUT, 1, params); + } + + // User-friendly function for configuring a servo pin. + inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint8_t initialState=0) { + int params[] = {(int)activePosition, (int)inactivePosition, profile, initialState}; + return IODevice::configure(vpin, CONFIGURE_SERVO, 4, params); + } + // write invokes the IODevice instance's _write method. static void write(VPIN vpin, int value); @@ -161,16 +173,11 @@ protected: (void)vpin; (void)value; }; - // Method to write an analogue value (optionally implemented within device class) + // 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 From 0aea9169b137118e36634dae3c8f3772ab663b1a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 11:18:15 +0100 Subject: [PATCH 064/125] Rename IODevice::isActive(vpin) to isBusy(vpin). --- IODevice.cpp | 6 +++--- IODevice.h | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 3269abe..3f33e57 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -156,12 +156,12 @@ void IODevice::writeAnalogue(VPIN vpin, int value, int profile) { #endif } -// isActive returns true if the device is currently in an animation of some sort, e.g. is changing +// isBusy 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) { +bool IODevice::isBusy(VPIN vpin) { IODevice *dev = findDevice(vpin); if (dev) - return dev->_isActive(vpin); + return dev->_isBusy(vpin); else return false; } diff --git a/IODevice.h b/IODevice.h index 691322c..d54b6fa 100644 --- a/IODevice.h +++ b/IODevice.h @@ -130,9 +130,9 @@ public: // 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 + // isBusy 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); + static bool isBusy(VPIN vpin); // check whether the pin supports notification. If so, then regular _read calls are not required. static bool hasCallback(VPIN vpin); @@ -193,9 +193,9 @@ protected: return 0; }; - // _isActive returns true if the device is currently in an animation of some sort, e.g. is changing + // _isBusy 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) { + virtual bool _isBusy(VPIN vpin) { (void)vpin; return false; } @@ -264,7 +264,7 @@ private: // 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; + bool _isBusy(VPIN vpin) override; void _loop(unsigned long currentMicros) override; void updatePosition(uint8_t pin); void writeDevice(uint8_t pin, int value); From 1dd574dc03cbe83e3513098a2b193199f9abe786 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 12:56:27 +0100 Subject: [PATCH 065/125] On commmand, output EEPROM size and amount used. Also, formatting and indentation fixed. --- EEStore.cpp | 100 +++++++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/EEStore.cpp b/EEStore.cpp index cf2531c..3a13be7 100644 --- a/EEStore.cpp +++ b/EEStore.cpp @@ -19,89 +19,87 @@ * along with CommandStation. If not, see . */ #include "EEStore.h" -#include "Turnouts.h" -#include "Sensors.h" -#include "Outputs.h" + #include "DIAG.h" +#include "Outputs.h" +#include "Sensors.h" +#include "Turnouts.h" #if defined(ARDUINO_ARCH_SAMD) ExternalEEPROM EEPROM; #endif -void EEStore::init(){ +void EEStore::init() { #if defined(ARDUINO_ARCH_SAMD) - EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three A pins grounded (0b1010000 = 0x50) + EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three + // A pins grounded (0b1010000 = 0x50) #endif - eeStore=(EEStore *)calloc(1,sizeof(EEStore)); - - EEPROM.get(0,eeStore->data); // get eeStore data + eeStore = (EEStore *)calloc(1, sizeof(EEStore)); - if(strncmp(eeStore->data.id,EESTORE_ID,sizeof(EESTORE_ID))!=0){ // check to see that eeStore contains valid DCC++ ID - sprintf(eeStore->data.id,EESTORE_ID); // if not, create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM - eeStore->data.nTurnouts=0; - eeStore->data.nSensors=0; - eeStore->data.nOutputs=0; - EEPROM.put(0,eeStore->data); - } + EEPROM.get(0, eeStore->data); // get eeStore data - reset(); // set memory pointer to first free EEPROM space - Turnout::load(); // load turnout definitions - Sensor::load(); // load sensor definitions - Output::load(); // load output definitions + // check to see that eeStore contains valid DCC++ ID + if (strncmp(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)) != 0) { + // if not, create blank eeStore structure (no + // turnouts, no sensors) and save it back to EEPROM + strncpy(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)); + eeStore->data.nTurnouts = 0; + eeStore->data.nSensors = 0; + eeStore->data.nOutputs = 0; + EEPROM.put(0, eeStore->data); + } + reset(); // set memory pointer to first free EEPROM space + Turnout::load(); // load turnout definitions + Sensor::load(); // load sensor definitions + Output::load(); // load output definitions } /////////////////////////////////////////////////////////////////////////////// -void EEStore::clear(){ - - sprintf(eeStore->data.id,EESTORE_ID); // create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM - eeStore->data.nTurnouts=0; - eeStore->data.nSensors=0; - eeStore->data.nOutputs=0; - EEPROM.put(0,eeStore->data); - +void EEStore::clear() { + sprintf(eeStore->data.id, + EESTORE_ID); // create blank eeStore structure (no turnouts, no + // sensors) and save it back to EEPROM + eeStore->data.nTurnouts = 0; + eeStore->data.nSensors = 0; + eeStore->data.nOutputs = 0; + EEPROM.put(0, eeStore->data); } /////////////////////////////////////////////////////////////////////////////// -void EEStore::store(){ - reset(); - Turnout::store(); - Sensor::store(); - Output::store(); - EEPROM.put(0,eeStore->data); - DIAG(F("EEPROM used: %d bytes"), EEStore::pointer()); +void EEStore::store() { + reset(); + Turnout::store(); + Sensor::store(); + Output::store(); + EEPROM.put(0, eeStore->data); + DIAG(F("EEPROM used: %d/%d bytes"), EEStore::pointer(), EEPROM.length()); } /////////////////////////////////////////////////////////////////////////////// -void EEStore::advance(int n){ - eeAddress+=n; -} +void EEStore::advance(int n) { eeAddress += n; } /////////////////////////////////////////////////////////////////////////////// -void EEStore::reset(){ - eeAddress=sizeof(EEStore); -} +void EEStore::reset() { eeAddress = sizeof(EEStore); } /////////////////////////////////////////////////////////////////////////////// -int EEStore::pointer(){ - return(eeAddress); -} +int EEStore::pointer() { return (eeAddress); } /////////////////////////////////////////////////////////////////////////////// void EEStore::dump(int num) { - byte b; - DIAG(F("Addr 0x char")); - for (int n=0 ; n Date: Fri, 27 Aug 2021 15:42:47 +0100 Subject: [PATCH 066/125] HAL writeAnalogue function change. IODevice::writeAnalogue() has an additional optional parameter "duration", specifying the time taken for the animation in units of 100ms (max 3276 seconds, or about 54 minutes). --- IODevice.cpp | 30 ++++++++------------------ IODevice.h | 23 +++++++++++--------- IO_PCA9685.cpp | 57 ++++++++++++++++++++++++++++++++------------------ 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 3f33e57..408162d 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -144,11 +144,12 @@ void IODevice::write(VPIN vpin, int value) { } // 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) { +// then only the first one found will be used. Duration is the time that the +// operation is to be performed over (e.g. as an animation) in deciseconds (0-3276 sec) +void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { IODevice *dev = findDevice(vpin); if (dev) { - dev->_writeAnalogue(vpin, value, profile); + dev->_writeAnalogue(vpin, value, profile, duration); return; } #ifdef DIAG_IO @@ -246,24 +247,13 @@ int IODevice::read(VPIN vpin) { // 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; -} +bool IODevice::configure(VPIN, ConfigTypeEnum, int, int []) { return true; } void IODevice::write(VPIN vpin, int value) { digitalWrite(vpin, value); pinMode(vpin, OUTPUT); } -void IODevice::writeAnalogue(VPIN vpin, int value, int profile) { - (void)vpin; (void)value; (void)profile; // Avoid compiler warnings -} -bool IODevice::hasCallback(VPIN vpin) { - (void)vpin; // Avoid compiler warnings - return false; -} +void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {} +bool IODevice::hasCallback(VPIN) { return false; } int IODevice::read(VPIN vpin) { pinMode(vpin, INPUT_PULLUP); return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1) @@ -272,10 +262,8 @@ void IODevice::loop() {} void IODevice::DumpAll() { DIAG(F("NO HAL CONFIGURED!")); } -bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < 49); } -void IODevice::setGPIOInterruptPin(int16_t pinNumber) { - (void) pinNumber; // Avoid compiler warning -} +bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < NUM_DIGITAL_PINS); } +void IODevice::setGPIOInterruptPin(int16_t) {} // Chain of callback blocks (identifying registered callback functions for state changes) // Not used in IO_NO_HAL but must be declared. diff --git a/IODevice.h b/IODevice.h index d54b6fa..673a9d2 100644 --- a/IODevice.h +++ b/IODevice.h @@ -42,6 +42,7 @@ #include "DIAG.h" #include "FSH.h" #include "I2CManager.h" +#include "inttypes.h" typedef uint16_t VPIN; // Limit VPIN number to max 32767. Above this number, printing often gives negative values. @@ -128,7 +129,7 @@ public: 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); + static void writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration=0); // isBusy returns true if the device is currently in an animation of some sort, e.g. is changing // the output over a period of time. @@ -174,8 +175,8 @@ protected: }; // 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; + virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + (void)vpin; (void)value; (void) profile; (void)duration; }; // Function called to check whether callback notification is supported by this pin. @@ -249,12 +250,14 @@ public: static void create(VPIN vpin, int nPins, uint8_t I2CAddress); // Constructor PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress); - enum ProfileType { - Instant = 0, // Moves immediately between positions + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration Fast = 1, // Takes around 500ms end-to-end Medium = 2, // 1 second end-to-end Slow = 3, // 2 seconds end-to-end - Bounce = 4 // For semaphores/turnouts with a bit of bounce!! + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. }; private: @@ -263,7 +266,7 @@ private: 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; + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override; bool _isBusy(VPIN vpin) override; void _loop(unsigned long currentMicros) override; void updatePosition(uint8_t pin); @@ -279,10 +282,10 @@ private: 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. + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. uint8_t currentProfile; // profile being used for current animation. - }; // 12 bytes per element, i.e. per pin in use + }; // 14 bytes per element, i.e. per pin in use struct ServoData *_servoData [16]; diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index a3ab48c..1b7436d 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -65,7 +65,7 @@ bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i int state = params[3]; if (state != -1) { // Position servo to initial state - _writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, Instant); + _writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0); } return true; @@ -96,7 +96,6 @@ void PCA9685::_begin() { // 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); @@ -104,10 +103,14 @@ void PCA9685::_begin() { // 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. + #if defined(DIAG_IO) + _display(); + #endif } } -// Device-specific write function, invoked from IODevice::write(). +// Device-specific write function, invoked from IODevice::write(). +// For this function, the configured profile is used. void PCA9685::_write(VPIN vpin, int value) { #ifdef DIAG_IO DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value); @@ -121,14 +124,24 @@ void PCA9685::_write(VPIN vpin, int value) { writeDevice(pin, value ? _defaultActivePosition : _defaultInactivePosition); } else { // Use configured parameters for advanced transitions - _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile); + _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, 0); } } // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). -void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { +// Profile is as follows: +// Bit 7: 0=Set PWM to 0% to power off servo motor when finished +// 1=Keep PWM pulses on (better when using PWM to drive an LED) +// Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) +// 1 (Fast) Move servo in 0.5 seconds +// 2 (Medium) Move servo in 1.0 seconds +// 3 (Slow) Move servo in 2.0 seconds +// 4 (Bounce) Servo 'bounces' at extremes. +// +void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { #ifdef DIAG_IO - DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d"), vpin, value, profile); + DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d"), + vpin, value, profile, duration); #endif int pin = vpin - _firstVpin; if (value > 4095) value = 4095; @@ -142,30 +155,32 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, int profile) { s->activePosition = _defaultActivePosition; s->inactivePosition = _defaultInactivePosition; s->currentPosition = value; - s->profile = Instant; + s->profile = Instant; // Use instant profile (but not this time) } // Animated profile. Initiate the appropriate action. s->currentProfile = profile; - s->numSteps = profile==Fast ? 10 : - profile==Medium ? 20 : - profile==Slow ? 40 : - profile==Bounce ? sizeof(_bounceProfile)-1 : - 1; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Instant ? 1 : + profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2; // Convert from deciseconds (100ms) to refresh cycles (50ms) s->stepNumber = 0; s->toPosition = value; s->fromPosition = s->currentPosition; } -// _isActive returns true if the device is currently in executing an animation, +// _isBusy returns true if the device is currently in executing an animation, // changing the output over a period of time. -bool PCA9685::_isActive(VPIN vpin) { +bool PCA9685::_isBusy(VPIN vpin) { int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; if (s == NULL) return false; // No structure means no animation! else - return (s->numSteps != 0); + return (s->stepNumber < s->numSteps); } void PCA9685::_loop(unsigned long currentMicros) { @@ -194,7 +209,7 @@ void PCA9685::updatePosition(uint8_t pin) { if (s->stepNumber < s->numSteps) { // Animation in progress, reposition servo s->stepNumber++; - if (s->currentProfile == Bounce) { + if ((s->currentProfile & ~NoPowerOff) == Bounce) { // Retrieve step positions from array in flash byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); @@ -208,10 +223,12 @@ void PCA9685::updatePosition(uint8_t pin) { // 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) { + && s->currentPosition != 0) { #ifdef IO_SWITCH_OFF_SERVO - // Wait has finished, so switch off PWM to prevent annoying servo buzz - writeDevice(pin, 0); + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + writeDevice(pin, 0); + } #endif s->numSteps = 0; // Done now. } @@ -236,7 +253,7 @@ void PCA9685::writeDevice(uint8_t pin, int value) { // Display details of this device. void PCA9685::_display() { - DIAG(F("PCA9685 I2C:x%x Vpins:%d-%d"), _I2CAddress, (int)_firstVpin, + DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d"), _I2CAddress, (int)_firstVpin, (int)_firstVpin+_nPins-1); } From 93dfdcce5307b4ebec0e65a93c6eb35e42e3b97a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 15:44:26 +0100 Subject: [PATCH 067/125] Add command to list HAL device configuration. Also, only display HAL device configurations at startup if DIAG_IO is #defined. --- DCCEXParser.cpp | 9 +++++++++ IO_DCCAccessory.cpp | 11 ++++------- IO_ExampleSerial.cpp | 6 ++++-- IO_GPIOBase.h | 2 ++ IO_HCSR04.h | 10 ++++++++-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 429e1dc..e4b4d73 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -68,6 +68,8 @@ const int16_t HASH_KEYWORD_VPIN=-415; const int16_t HASH_KEYWORD_C=67; const int16_t HASH_KEYWORD_T=84; const int16_t HASH_KEYWORD_LCN = 15137; +const int16_t HASH_KEYWORD_HAL = 10853; +const int16_t HASH_KEYWORD_SHOW = -21309; #ifdef HAS_ENOUGH_MEMORY const int16_t HASH_KEYWORD_WIFI = -5583; const int16_t HASH_KEYWORD_ETHERNET = -30767; @@ -874,6 +876,13 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); break; +#if !defined(IO_MINIMAL_HAL) + case HASH_KEYWORD_HAL: + if (p[1] == HASH_KEYWORD_SHOW) + IODevice::DumpAll(); + break; +#endif + default: // invalid/unknown break; } diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index 139d900..7f3dd93 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -42,12 +42,9 @@ DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, i } void DCCAccessoryDecoder::_begin() { - int endAddress = _packedAddress + _nPins - 1; - int DCCAddress = _packedAddress >> 2; - int DCCSubaddress = _packedAddress & 3; - 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); +#if defined(DIAG_IO) + _display(); +#endif } // Device-specific write function. @@ -61,7 +58,7 @@ void DCCAccessoryDecoder::_write(VPIN id, int 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, + DIAG(F("DCCAccessoryDecoder Configured on 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); } diff --git a/IO_ExampleSerial.cpp b/IO_ExampleSerial.cpp index 6954e55..efcc3bf 100644 --- a/IO_ExampleSerial.cpp +++ b/IO_ExampleSerial.cpp @@ -42,7 +42,9 @@ void IO_ExampleSerial::create(VPIN firstVpin, int nPins, HardwareSerial *serial, // Device-specific initialisation void IO_ExampleSerial::_begin() { _serial->begin(_baud); - DIAG(F("ExampleSerial configured Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1); +#if defined(DIAG_IO) + _display(); +#endif // Send a few # characters to the output for (uint8_t i=0; i<3; i++) @@ -121,7 +123,7 @@ void IO_ExampleSerial::_loop(unsigned long currentMicros) { } void IO_ExampleSerial::_display() { - DIAG(F("IO_ExampleSerial VPins:%d-%d"), (int)_firstVpin, + DIAG(F("IO_ExampleSerial Configured on VPins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1); } diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 6e4a032..4ca8cce 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -99,7 +99,9 @@ void GPIOBase::_begin() { I2CManager.begin(); I2CManager.setClock(400000); if (I2CManager.exists(_I2CAddress)) { +#if defined(DIAG_IO) _display(); +#endif _portMode = 0; // default to input mode _portPullup = -1; // default to pullup enabled _portInputState = -1; diff --git a/IO_HCSR04.h b/IO_HCSR04.h index e66c343..98340ff 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -90,8 +90,9 @@ protected: pinMode(_receivePin, INPUT); ArduinoPins::fastWriteDigital(_transmitPin, 0); _lastExecutionTime = micros(); - DIAG(F("HCSR04 configured on VPIN:%d TXpin:%d RXpin:%d On:%dcm Off:%dcm"), - _firstVpin, _transmitPin, _receivePin, _onThreshold, _offThreshold); +#if defined(DIAG_IO) + _display(); +#endif } // _read function - just return _value (calculated in _loop). @@ -109,6 +110,11 @@ protected: } } + void _display() override { + DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"), + _firstVpin, _transmitPin, _receivePin, _onThreshold, _offThreshold); + } + private: // This polls the HC-SR04 device by sending a pulse and measuring the duration of // the pulse observed on the receive pin. In order to be kind to the rest of the CS From 6ebf908802dc0a7391064d8146edce33be01a258 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 15:45:22 +0100 Subject: [PATCH 068/125] Ensure Turnout changes are notified on LCN activity. Also, some comment updates. --- Turnouts.cpp | 49 ++++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index e309604..7019fc4 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -36,18 +36,18 @@ * Protected static data */ - Turnout *Turnout::_firstTurnout = 0; + /* static */ Turnout *Turnout::_firstTurnout = 0; /* * Public static data */ - int Turnout::turnoutlistHash = 0; + /* static */ int Turnout::turnoutlistHash = 0; /* * Protected static functions */ - Turnout *Turnout::get(uint16_t id) { + /* static */ Turnout *Turnout::get(uint16_t id) { // Find turnout object from list. for (Turnout *tt = _firstTurnout; tt != NULL; tt = tt->_nextTurnout) if (tt->_turnoutData.id == id) return tt; @@ -55,7 +55,7 @@ } // Add new turnout to end of chain - void Turnout::add(Turnout *tt) { + /* static */ void Turnout::add(Turnout *tt) { if (!_firstTurnout) _firstTurnout = tt; else { @@ -75,7 +75,7 @@ } // Remove nominated turnout from turnout linked list and delete the object. - bool Turnout::remove(uint16_t id) { + /* static */ bool Turnout::remove(uint16_t id) { Turnout *tt,*pp=NULL; for(tt=_firstTurnout; tt!=NULL && tt->_turnoutData.id!=id; pp=tt, tt=tt->_nextTurnout) {} @@ -97,7 +97,7 @@ * Public static functions */ - bool Turnout::isClosed(uint16_t id) { + /* static */ bool Turnout::isClosed(uint16_t id) { Turnout *tt = get(id); if (tt) return tt->isClosed(); @@ -105,10 +105,21 @@ return false; } - bool Turnout::setClosedStateOnly(uint16_t id, bool close) { + /* static */ bool Turnout::setClosedStateOnly(uint16_t id, bool closeFlag) { Turnout *tt = get(id); if (!tt) return false; - tt->_turnoutData.closed = close; + tt->_turnoutData.closed = closeFlag; + + // I know it says setClosedStateOnly, but we need to tell others + // that the state has changed too. + #if defined(RMFT_ACTIVE) + RMFT2::turnoutEvent(id, closeFlag); + #endif + + // Send message to JMRI etc. over Serial USB. This is done here + // to ensure that the message is sent when the turnout operation + // is not initiated by a Serial command. + printState(id, &Serial); return true; } @@ -117,7 +128,7 @@ // common parts of the turnout operation. Code which is specific to a turnout // type should be placed in the virtual function setClosedInternal(bool) which is // called from here. - bool Turnout::setClosed(uint16_t id, bool closeFlag) { + /* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) { #ifdef EESTOREDEBUG if (closeFlag) DIAG(F("Turnout::close(%d)"), id); @@ -147,14 +158,14 @@ } // Load all turnout objects - void Turnout::load() { + /* static */ void Turnout::load() { for (uint16_t i=0; idata.nTurnouts; i++) { Turnout::loadTurnout(); } } // Save all turnout objects - void Turnout::store() { + /* static */ void Turnout::store() { EEStore::eeStore->data.nTurnouts=0; for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) { tt->save(); @@ -163,7 +174,7 @@ } // Load one turnout from EEPROM - Turnout *Turnout::loadTurnout () { + /* static */ Turnout *Turnout::loadTurnout () { Turnout *tt = 0; // Read turnout type from EEPROM struct TurnoutData turnoutData; @@ -201,7 +212,7 @@ } // Display, on the specified stream, the current state of the turnout (1=thrown or 0=closed). - void Turnout::printState(uint16_t id, Print *stream) { + /* static */ void Turnout::printState(uint16_t id, Print *stream) { Turnout *tt = get(id); if (!tt) tt->printState(stream); } @@ -223,7 +234,7 @@ } // Create function - Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) { + /* static */ Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) { #ifndef IO_NO_HAL Turnout *tt = get(id); if (tt) { @@ -330,7 +341,7 @@ } // Create function - Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) { + /* static */ Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) { Turnout *tt = get(id); if (tt) { // Object already exists, check if it is usable @@ -350,7 +361,7 @@ } // Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point. - Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) { + /* static */ Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) { DCCTurnoutData dccTurnoutData; // Read class-specific data from EEPROM EEPROM.get(EEStore::pointer(), dccTurnoutData); @@ -407,7 +418,7 @@ } // Create function - Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) { + /* static */ Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) { Turnout *tt = get(id); if (tt) { // Object already exists, check if it is usable @@ -427,7 +438,7 @@ } // Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point. - Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) { + /* static */ Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) { VpinTurnoutData vpinTurnoutData; // Read class-specific data from EEPROM EEPROM.get(EEStore::pointer(), vpinTurnoutData); @@ -477,7 +488,7 @@ { } // Create function - Turnout *LCNTurnout::create(uint16_t id, bool closed) { + /* static */ Turnout *LCNTurnout::create(uint16_t id, bool closed) { Turnout *tt = get(id); if (tt) { // Object already exists, check if it is usable From f8858b952efa3bd9bdfcd2b45a9e4444bc2cbcbf Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 16:59:04 +0100 Subject: [PATCH 069/125] Servo positioning - correct handling of profile 0. Ensure that profile 0 uses the duration parameter to calculate the number of steps. --- IO_PCA9685.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 1b7436d..79c022c 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -161,8 +161,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur // Animated profile. Initiate the appropriate action. s->currentProfile = profile; uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. - s->numSteps = profileValue==Instant ? 1 : - profileValue==Fast ? 10 : // 0.5 seconds + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds profileValue==Medium ? 20 : // 1.0 seconds profileValue==Slow ? 40 : // 2.0 seconds profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds From b2ddb3427393ca527f87b991a1870f90b6a5dd3f Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 17:01:18 +0100 Subject: [PATCH 070/125] RMFT: Add new FADE command for LED LED FADE command allows an LED to be attached to a PCA9685 PWM module and controlled to any arbitrary brightness (0-4095), changing over a specified period of time in milliseconds. FADE(vpin,value,ms) --- RMFT2.cpp | 4 ++-- RMFTMacros.h | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index af0040b..f57d1d7 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -607,8 +607,8 @@ void RMFT2::loop2() { 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)); + case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration) + IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2),GET_OPERAND(3)); break; case OPCODE_PRINT: diff --git a/RMFTMacros.h b/RMFTMacros.h index 3e81ee1..7352513 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -68,6 +68,7 @@ #define ENDIF #define ENDTASK #define ESTOP +#define FADE(pin,value,ms) #define FOFF(func) #define FOLLOW(route) #define FON(func) @@ -178,6 +179,7 @@ const int StringMacroTracker1=__COUNTER__; #undef START #undef SEQUENCE #undef SERVO +#undef FADE #undef SENDLOCO #undef SETLOCO #undef SET @@ -214,6 +216,7 @@ const int StringMacroTracker1=__COUNTER__; #define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100L),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L), #define ENDIF OPCODE_ENDIF,NOP, #define ESTOP OPCODE_SPEED,V(1), +#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L), #define FOFF(func) OPCODE_FOFF,V(func), #define FOLLOW(route) OPCODE_FOLLOW,V(route), #define FON(func) OPCODE_FON,V(func), @@ -242,7 +245,7 @@ const int StringMacroTracker1=__COUNTER__; #define REV(speed) OPCODE_REV,V(speed), #define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), #define START(route) OPCODE_START,V(route), -#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::profile), +#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::profile),OPCODE_PAD,V(0), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco), #define SET(sensor_id) OPCODE_SET,V(sensor_id), #define SPEED(speed) OPCODE_SPEED,V(speed), From 23ed4e61af2a59b3b02cc6c1e944eb718cb1ef17 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 17:09:40 +0100 Subject: [PATCH 071/125] Remove compiler warning If no route or automation definitions were present, the compiler warned that parameter stream is not used in function RMFT2::emitWithrottleDescriptions. --- RMFTMacros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index 7352513..b912540 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -51,7 +51,7 @@ #define ALIAS(name,value) const int name=value; -#define EXRAIL void RMFT2::emitWithrottleDescriptions(Print * stream) { +#define EXRAIL void RMFT2::emitWithrottleDescriptions(Print * stream) {(void)stream; #define ROUTE(id, description) emitRouteDescription(stream,'R',id,F(description)); #define AUTOMATION(id, description) emitRouteDescription(stream,'A',id,F(description)); #define ENDEXRAIL } From fb6ab85c4af5d21a2de4d4391050692439a9979e Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 21:43:24 +0100 Subject: [PATCH 072/125] Add flag to invert DCC Accessory command behaviour command puts a D=1 into the DCC packet for a DCC Accessory Decoder. This was previously though to correspond to a 'throw' request and a D=0 to a 'close' request. RCN-213 standard identifies that D=1 is 'close' and D=0 is 'throw', so this change allows CS to be configured to invert the states to conform to the RCN-213 definition. --- DCC.cpp | 3 +++ DCCEXParser.cpp | 7 ++++++- config.example.h | 10 ++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/DCC.cpp b/DCC.cpp index e3dd921..79c5b19 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -239,6 +239,9 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) { } void DCC::setAccessory(int address, byte number, bool activate) { + #ifdef DIAG_IO + DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, number, activate); + #endif // use masks to detect wrong values and do nothing if(address != (address & 511)) return; diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index e4b4d73..91039f7 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -27,6 +27,7 @@ #include "freeMemory.h" #include "GITHUB_SHA.h" #include "version.h" +#include "defines.h" #include "EEStore.h" #include "DIAG.h" @@ -364,8 +365,12 @@ 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. + // Honour the configuration option (config.h) which allows the command to be reversed +#ifdef DCC_ACCESSORY_RCN_213 + DCC::setAccessory(address, subaddress,p[activep]==0); +#else DCC::setAccessory(address, subaddress,p[activep]==1); +#endif } return; diff --git a/config.example.h b/config.example.h index 6c5c69a..9b1855f 100644 --- a/config.example.h +++ b/config.example.h @@ -134,11 +134,17 @@ The configuration file for DCC-EX Command Station // // According to norm RCN-213 a DCC packet with a 1 is closed/straight // and one with a 0 is thrown/diverging. In DCC++ Classic, and in previous -// versions of DCC++EX, a throw command was implemented in the packet as +// versions of DCC++EX, a turnout throw command was implemented in the packet as // '1' and a close command as '0'. The #define below makes the states // match with the norm. But we don't want to cause havoc on existent layouts, // so we define this only for new installations. If you don't want this, // don't add it to your config.h. -#define DCC_TURNOUTS_RCN_213 +//#define DCC_TURNOUTS_RCN_213 + +// The following #define likewise inverts the behaviour of the command +// for triggering DCC Accessory Decoders, so that generates a +// DCC packet with D=1 (close turnout) and generates D=0 +// (throw turnout). +//#define DCC_ACCESSORY_RCN_213 ///////////////////////////////////////////////////////////////////////////////////// From 7dea284ba8c682e56c499ff9a5d3a626e2281b48 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 21:47:13 +0100 Subject: [PATCH 073/125] DCCAccessoryDecoder class tidy. RCN-213 option. Rationalise address calculation into three macros. Ensure device is added to device chain. Allow inversion of the DCC packet to match definition of packet D bit in RCN-213, D=0 for 'throw' (rather than the DCC++ usage of D=1 for 'throw'). --- IO_DCCAccessory.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index 7f3dd93..5e1c8f4 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -20,25 +20,30 @@ #include "DCC.h" #include "IODevice.h" #include "DIAG.h" +#include "defines.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'. +// in different ways. DCC++EX uses a range of 1-2044 which excludes decoder address 0. +// Linear address 1 corresponds to address 1 subaddress 0. + +#define LINEARADDRESS(addr, subaddr) (((addr-1) << 2) + subaddr + 1) +#define ADDRESS(linearaddr) (((linearaddr-1) >> 2) + 1) +#define SUBADDRESS(linearaddr) ((linearaddr-1) % 4) void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress); } -// Constructor +// Constructors DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { _firstVpin = vpin; _nPins = nPins; - _packedAddress = (DCCAddress << 2) + DCCSubaddress; + _packedAddress = LINEARADDRESS(DCCAddress, DCCSubaddress); + addDevice(this); } void DCCAccessoryDecoder::_begin() { @@ -47,19 +52,22 @@ void DCCAccessoryDecoder::_begin() { #endif } -// Device-specific write function. +// Device-specific write function. State 1=closed, 0=thrown. Adjust for RCN-213 compliance 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); +#if !defined(DCC_ACCESSORY_RCN_213) + state = !state; +#endif + DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state); } void DCCAccessoryDecoder::_display() { int endAddress = _packedAddress + _nPins - 1; DIAG(F("DCCAccessoryDecoder Configured on 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); + ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress)); } From 40c6bb7f2ea617aa944a74c2ea5dad8f9feb0441 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 27 Aug 2021 21:47:48 +0100 Subject: [PATCH 074/125] Output Turnout state change diagnostic if DIAG_IO #defined. --- Turnouts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index 7019fc4..9170fa8 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -129,7 +129,7 @@ // type should be placed in the virtual function setClosedInternal(bool) which is // called from here. /* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) { - #ifdef EESTOREDEBUG + #if defined(DIAG_IO) if (closeFlag) DIAG(F("Turnout::close(%d)"), id); else From 0f55835b8be2495f5042c3734d8dd5521c2f3bdb Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 28 Aug 2021 17:39:48 +0100 Subject: [PATCH 075/125] Add RMFT WAITFOR() and SERVO2() commands. WAITFOR(pin) waits until the corresponding pin is not busy (e.g. has finished moving the servo). SERVO2(pin, value, ms) moves to the nominated position in a time given in milliseconds by ms. --- IODevice.cpp | 1 + RMFT2.cpp | 7 +++++++ RMFT2.h | 4 ++-- RMFTMacros.h | 18 ++++++++++++------ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 408162d..e19ddef 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -253,6 +253,7 @@ void IODevice::write(VPIN vpin, int value) { pinMode(vpin, OUTPUT); } void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {} +bool IODevice::isBusy(VPIN) { return false; } bool IODevice::hasCallback(VPIN) { return false; } int IODevice::read(VPIN vpin) { pinMode(vpin, INPUT_PULLUP); diff --git a/RMFT2.cpp b/RMFT2.cpp index f57d1d7..526c194 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -610,6 +610,13 @@ void RMFT2::loop2() { case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration) IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2),GET_OPERAND(3)); break; + + case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin) + if (IODevice::isBusy(operand)) { + delayMe(100); + return; + } + break; case OPCODE_PRINT: printMessage(operand); diff --git a/RMFT2.h b/RMFT2.h index 0619828..10c147a 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -36,13 +36,13 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_RANDWAIT, OPCODE_FON,OPCODE_FOFF, OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER, - OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT, + OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR, 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_SENDLOCO, OPCODE_PAUSE, OPCODE_RESUME, OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, - OPCODE_PRINT, + OPCODE_PRINT, OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL }; diff --git a/RMFTMacros.h b/RMFTMacros.h index b912540..3e1cde1 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -78,7 +78,7 @@ #define IF(sensor_id) #define IFNOT(sensor_id) #define IFRANDOM(percent) -#define IFRESERVE(block) +#define IFRESERVE(block) #define INVERT_DIRECTION #define JOIN #define LATCH(sensor_id) @@ -91,15 +91,16 @@ #define READ_LOCO #define RED(signal_id) #define RESERVE(blockid) -#define RESET(sensor_id) +#define RESET(pin) #define RESUME #define RETURN #define REV(speed) #define START(route) #define SENDLOCO(cab,route) #define SERVO(id,position,profile) +#define SERVO2(id,position,duration) #define SETLOCO(loco) -#define SET(sensor_id) +#define SET(pin) #define SEQUENCE(id) #define SPEED(speed) #define STOP @@ -111,6 +112,7 @@ #define TURNOUT(id,addr,subaddr) #define UNJOIN #define UNLATCH(sensor_id) +#define WAITFOR(pin) #include "myAutomation.h" @@ -179,6 +181,7 @@ const int StringMacroTracker1=__COUNTER__; #undef START #undef SEQUENCE #undef SERVO +#undef SERVO2 #undef FADE #undef SENDLOCO #undef SETLOCO @@ -192,6 +195,7 @@ const int StringMacroTracker1=__COUNTER__; #undef TURNOUT #undef UNJOIN #undef UNLATCH +#undef WAITFOR // Define macros for route code creation #define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF @@ -239,15 +243,16 @@ const int StringMacroTracker1=__COUNTER__; #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 RESET(pin) OPCODE_RESET,V(pin), #define RESUME OPCODE_RESUME,NOP, #define RETURN OPCODE_RETURN,NOP, #define REV(speed) OPCODE_REV,V(speed), #define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), #define START(route) OPCODE_START,V(route), -#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::ProfileType::profile),OPCODE_PAD,V(0), +#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0), +#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco), -#define SET(sensor_id) OPCODE_SET,V(sensor_id), +#define SET(pin) OPCODE_SET,V(pin), #define SPEED(speed) OPCODE_SPEED,V(speed), #define STOP OPCODE_SPEED,V(0), #define SIGNAL(redpin,amberpin,greenpin) OPCODE_SIGNAL,V(redpin),OPCODE_PAD,V(amberpin),OPCODE_PAD,V(greenpin), @@ -257,6 +262,7 @@ const int StringMacroTracker1=__COUNTER__; #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), +#define WAITFOR(pin) OPCODE_WAITFOR,V(pin), // PASS2 Build RouteCode const int StringMacroTracker2=__COUNTER__; From 09eae0ea918dbc239b4304c74aa3a104fe2c1f4a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 29 Aug 2021 01:10:47 +0100 Subject: [PATCH 076/125] Fix FADE(pin,0,0) operation in RMFT --- IO_PCA9685.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 79c022c..aaa56ea 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -165,7 +165,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur profileValue==Medium ? 20 : // 1.0 seconds profileValue==Slow ? 40 : // 2.0 seconds profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds - duration * 2; // Convert from deciseconds (100ms) to refresh cycles (50ms) + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) s->stepNumber = 0; s->toPosition = value; s->fromPosition = s->currentPosition; From 1bb7b5cc77497163ea531d9d505bf33b8765d078 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 29 Aug 2021 12:04:13 +0100 Subject: [PATCH 077/125] Make defaults for PWM (servo) positions 0 (PWM off) if not configured. When writing to a PWM device (servo or LED for example), it is possible to request the target position in the call, or to ask for a SET or RESET position. In the latter case, the positions corresponding to SET and RESET must be known, i.e. preconfigured. Defaults were assigned for this, but because the correct values will depend on the hardware device being driven, the defaults have been removed. In addition, the command, when defining a servo turnout, now configures the PWM positions (not required by commands, but desirable for consistency with other commands). --- IODevice.h | 10 ++++------ IO_PCA9685.cpp | 26 ++++++++++++-------------- Turnouts.cpp | 7 ++++--- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/IODevice.h b/IODevice.h index 673a9d2..65f5d35 100644 --- a/IODevice.h +++ b/IODevice.h @@ -120,9 +120,9 @@ public: } // User-friendly function for configuring a servo pin. - inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint8_t initialState=0) { - int params[] = {(int)activePosition, (int)inactivePosition, profile, initialState}; - return IODevice::configure(vpin, CONFIGURE_SERVO, 4, params); + inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint16_t duration, uint8_t initialState=0) { + int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState}; + return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params); } // write invokes the IODevice instance's _write method. @@ -285,13 +285,11 @@ private: uint16_t stepNumber; // Index of current step (starting from 0) uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 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]; diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index aaa56ea..ddc45d8 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -45,10 +45,10 @@ void PCA9685::create(VPIN firstVpin, int nPins, uint8_t 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; + if (paramCount != 5) 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]); + DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); #endif int8_t pin = vpin - _firstVpin; @@ -62,12 +62,13 @@ bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i s->activePosition = params[0]; s->inactivePosition = params[1]; s->profile = params[2]; - int state = params[3]; + s->duration = params[3]; + int state = params[4]; + if (state != -1) { // Position servo to initial state _writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0); } - return true; } @@ -119,13 +120,10 @@ void PCA9685::_write(VPIN vpin, int value) { if (value) value = 1; struct ServoData *s = _servoData[pin]; - if (s == NULL) { - // Pin not configured, just write default positions to servo controller - writeDevice(pin, value ? _defaultActivePosition : _defaultInactivePosition); - } else { - // Use configured parameters for advanced transitions - _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, 0); - } + if (s != NULL) { + // Use configured parameters + _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } // else { /* ignorethe request */ } } // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). @@ -152,8 +150,8 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur // Servo pin not configured, so configure now using defaults s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); if (s == NULL) return; // Check for memory allocation failure - s->activePosition = _defaultActivePosition; - s->inactivePosition = _defaultInactivePosition; + s->activePosition = 0; + s->inactivePosition = 0; s->currentPosition = value; s->profile = Instant; // Use instant profile (but not this time) } diff --git a/Turnouts.cpp b/Turnouts.cpp index 9170fa8..41b6b39 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -249,9 +249,10 @@ // Don't touch the _closed parameter, retain the original value. // We don't really need to do the following, since a call to IODevice::_writeAnalogue - // will provide all the data that is required! - // int params[] = {(int)thrownPosition, (int)closedPosition, profile, closed}; - // IODevice::configure(vpin, IODevice::CONFIGURE_SERVO, 4, params); + // will provide all the data that is required! However, if someone has configured + // a Turnout, we should ensure that the SET() RESET() and other commands that use write() + // behave consistently with the turnout commands. + IODevice::configureServo(vpin, thrownPosition, closedPosition, profile, 0, closed); // Set position directly to specified position - we don't know where it is moving from. IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant); From afe914167158db5fc451dd0dd81906f82871db2a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 29 Aug 2021 12:14:23 +0100 Subject: [PATCH 078/125] RMFT SIGNAL macro to allow for RGB LEDs. The SIGNAL macro has been changed to allow for use of RGB LEDs. Connect R and G pins, and assign as SIGNAL(redpin,0,greenpin). Then if amber is requested, the macro will set red and green on at the same time. --- RMFT2.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 526c194..0d7d133 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -678,9 +678,10 @@ void RMFT2::kill(const FSH * reason, int operand) { if (redpin!=id)continue; byte amberpin=GET_OPERAND(2); byte greenpin=GET_OPERAND(3); - IODevice::write(redpin,red); + // If amberpin is zero, synthesise amber from red+green + IODevice::write(redpin,red || (amber && (amberpin==0))); if (amberpin) IODevice::write(amberpin,amber); - if (greenpin) IODevice::write(amberpin,green); + if (greenpin) IODevice::write(amberpin,green || (amber && (amberpin==0))); return; } } From 08810dafd7eedc7cdb52c10626afb8a4b3ede97d Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:37:06 -0500 Subject: [PATCH 079/125] Update retry counter will also display running total prior to its reset. RCOUNT step included in Verify program will count when Verify fails --- DCC.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DCC.h b/DCC.h index cf1680f..c7dcbe0 100644 --- a/DCC.h +++ b/DCC.h @@ -41,6 +41,7 @@ enum ackOp : byte ITCB7, // If True callback(byte &0x7F) NAKFAIL, // if false callback(-1) FAIL, // callback(-1) + RCOUNT, // increment ackRetry counter STARTMERGE, // Clear bit and byte settings ready for merge pass MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes) SETBIT, // sets bit number to next prog byte @@ -115,9 +116,11 @@ public: static inline void setGlobalSpeedsteps(byte s) { globalSpeedsteps = s; }; - static inline void setAckRetry(byte retry) { + static inline int16_t setAckRetry(byte retry) { ackRetry = retry; + ackRetryPSum = ackRetrySum; ackRetrySum = 0; // reset running total + return ackRetryPSum; }; private: @@ -154,6 +157,7 @@ private: static byte ackManagerRetry; static byte ackRetry; static int16_t ackRetrySum; + static int16_t ackRetryPSum; static int ackManagerWord; static byte ackManagerStash; static bool ackReceived; From 4b87c879a9d5dc32c97d205a08b222a9a692cb4b Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:52:50 -0500 Subject: [PATCH 080/125] RCOUNT step added to Verify byte program --- DCC.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DCC.cpp b/DCC.cpp index 79c5b19..f5470b2 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -359,6 +359,7 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = { BASELINE, VB,WACK, // validate byte ITCB, // if ok callback value + RCOUNT, // increment ackRetry counter STARTMERGE, //clear bit and byte values ready for merge pass // each bit is validated against 0 and the result inverted in MERGE // this is because there tend to be more zeros in cv values than ones. @@ -693,6 +694,7 @@ int DCC::ackManagerWord; byte DCC::ackManagerRetry; byte DCC::ackRetry = 2; int16_t DCC::ackRetrySum; +int16_t DCC::ackRetryPSum; int DCC::ackManagerCv; byte DCC::ackManagerBitNum; bool DCC::ackReceived; @@ -844,7 +846,11 @@ void DCC::ackManagerLoop() { case FAIL: // callback(-1) callback(-1); return; - + + case RCOUNT: // ackRetry counter + ackRetrySum++; + break; + case STARTMERGE: ackManagerBitNum=7; ackManagerByte=0; From b4fb76b6c865461318ca0923818401e383f12d0c Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:02:05 -0500 Subject: [PATCH 081/125] Display running total ackRetrySum RCOUNT added to Verify program to report if Read step occurs. Report ackRetrySum on LCD when is sent --- DCCEXParser.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 91039f7..d09aa1b 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -820,8 +820,7 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) LCD(0, F("Ack Max=%dus"), p[2]); // } else if (p[1] == HASH_KEYWORD_RETRY) { if (p[2] >255) p[2]=3; - DCC::setAckRetry(p[2]); - LCD(0, F("Ack Retry=%d"), p[2]); // + LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCC::setAckRetry(p[2])); // } } else { StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off")); From 9d5781a87ce1c7283722944c27228db93c2f514f Mon Sep 17 00:00:00 2001 From: Asbelos Date: Fri, 3 Sep 2021 21:33:53 +0100 Subject: [PATCH 082/125] Signal pin corrections --- RMFT2.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 0d7d133..8c22b43 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -674,14 +674,14 @@ void RMFT2::kill(const FSH * reason, int operand) { byte opcode=GET_OPCODE; if (opcode==OPCODE_ENDEXRAIL) return; if (opcode!=OPCODE_SIGNAL) continue; - byte redpin=GET_OPERAND(1); + byte redpin=GET_OPERAND(0); if (redpin!=id)continue; - byte amberpin=GET_OPERAND(2); - byte greenpin=GET_OPERAND(3); + byte amberpin=GET_OPERAND(1); + byte greenpin=GET_OPERAND(2); // If amberpin is zero, synthesise amber from red+green IODevice::write(redpin,red || (amber && (amberpin==0))); if (amberpin) IODevice::write(amberpin,amber); - if (greenpin) IODevice::write(amberpin,green || (amber && (amberpin==0))); + if (greenpin) IODevice::write(greenpin,green || (amber && (amberpin==0))); return; } } From 99222bd37f5533c0128efae06a7abd9ebebfb667 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Fri, 3 Sep 2021 22:39:13 +0100 Subject: [PATCH 083/125] Turnout recursion test --- RMFT2.cpp | 20 +++++++++++++++++--- RMFT2.h | 5 +++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 8c22b43..2018532 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -275,6 +275,7 @@ RMFT2::RMFT2(int progCtr) { forward=true; invert=false; stackDepth=0; + onTurnoutId=0; // Not handling an ONTHROW/ONCLOSE // chain into ring of RMFTs if (loopTask==NULL) { @@ -685,15 +686,28 @@ void RMFT2::kill(const FSH * reason, int operand) { return; } } - void RMFT2::turnoutEvent(VPIN id, bool closed) { + void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) { + + // Check we dont already have a task running this turnout + RMFT2 * task=loopTask; + while(task) { + if (task->onTurnoutId==turnoutId) { + DIAG(F("Recursive ONTHROW/ONCLOSE for Turnout %d"),turnoutId); + return; + } + task=task->next; + if (task==loopTask) break; + } + // Hunt for an ONTHROW/ONCLOSE for this turnout byte huntFor=closed ? OPCODE_ONCLOSE : OPCODE_ONTHROW ; // 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 + if (turnoutId!=(int16_t)GET_OPERAND(0)) continue; + task=new RMFT2(progCounter); // new task starts at this instruction + task->onTurnoutId=turnoutId; // flag for recursion detector return; } } diff --git a/RMFT2.h b/RMFT2.h index 10c147a..bb205c3 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -68,7 +68,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, static void readLocoCallback(int16_t cv); static void emitWithrottleRouteList(Print* stream); static void createNewTask(int route, uint16_t cab); - static void turnoutEvent(VPIN id, bool closed); + static void turnoutEvent(int16_t id, bool closed); private: static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]); static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ; @@ -106,10 +106,11 @@ private: unsigned long delayTime; byte taskId; - int loco; + int16_t loco; bool forward; bool invert; int speedo; + int16_t onTurnoutId; byte stackDepth; int callStack[MAX_STACK_DEPTH]; }; From 9ba13a62c9f53dfda3b99a4f614342e89022fa4e Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sat, 4 Sep 2021 10:38:38 +0100 Subject: [PATCH 084/125] Negative sensor ids --- RMFT2.cpp | 13 +++++++++---- RMFT2.h | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 2018532..bc9c01e 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -330,11 +330,16 @@ void RMFT2::driveLoco(byte speed) { speedo=speed; } -bool RMFT2::readSensor(int16_t sensorId) { - VPIN vpin=abs(sensorId); +bool RMFT2::readSensor(uint16_t sensorId) { + // Exrail operands are unsigned but we need the signed version as inserted by the macros. + int16_t sId=(int16_t) sensorId; + + VPIN vpin=abs(sId); 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); + + // negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting) + bool s= IODevice::read(vpin) ^ (sId<0); + if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sId); return s; } diff --git a/RMFT2.h b/RMFT2.h index bb205c3..26cdf1d 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -85,7 +85,7 @@ private: static RMFT2 * pausingTask; void delayMe(long millisecs); void driveLoco(byte speedo); - bool readSensor(int16_t sensorId); + bool readSensor(uint16_t sensorId); bool skipIfBlock(); bool readLoco(); void loop2(); @@ -106,10 +106,10 @@ private: unsigned long delayTime; byte taskId; - int16_t loco; + uint16_t loco; bool forward; bool invert; - int speedo; + byte speedo; int16_t onTurnoutId; byte stackDepth; int callStack[MAX_STACK_DEPTH]; From 8d471d9f3f1e3e016c5f1909c69e178a1bd53719 Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun, 5 Sep 2021 16:19:03 -0500 Subject: [PATCH 085/125] Restore ackManagerByte before retry Verify --- DCC.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DCC.h b/DCC.h index c7dcbe0..8cb5d97 100644 --- a/DCC.h +++ b/DCC.h @@ -38,10 +38,11 @@ enum ackOp : byte ITC1, // If True Callback(1) (if prevous WACK got an ACK) ITC0, // If True callback(0); ITCB, // If True callback(byte) + ITCBV, // If True callback(byte) - end of Verify Byte ITCB7, // If True callback(byte &0x7F) NAKFAIL, // if false callback(-1) FAIL, // callback(-1) - RCOUNT, // increment ackRetry counter + BIV, // Set ackManagerByte to initial value for Verify retry STARTMERGE, // Clear bit and byte settings ready for merge pass MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes) SETBIT, // sets bit number to next prog byte @@ -152,6 +153,7 @@ private: static ackOp const *ackManagerProg; static ackOp const *ackManagerProgStart; static byte ackManagerByte; + static byte ackManagerByteVerify; static byte ackManagerBitNum; static int ackManagerCv; static byte ackManagerRetry; From 4dff8a2b50ac23ce6aaba81fac0b870e9047d1ce Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Sun, 5 Sep 2021 16:43:24 -0500 Subject: [PATCH 086/125] Restore ackManagerByte before retry Verify Identify where initial value was not verified, but initial value returned with subsequent Read. RCOUNT removed. BIV and ITCBV added. --- DCC.cpp | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/DCC.cpp b/DCC.cpp index f5470b2..5165f72 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -357,9 +357,9 @@ const ackOp FLASH WRITE_BYTE_PROG[] = { const ackOp FLASH VERIFY_BYTE_PROG[] = { BASELINE, + BIV, // ackManagerByte initial value VB,WACK, // validate byte - ITCB, // if ok callback value - RCOUNT, // increment ackRetry counter + ITCB, // if ok callback value STARTMERGE, //clear bit and byte values ready for merge pass // each bit is validated against 0 and the result inverted in MERGE // this is because there tend to be more zeros in cv values than ones. @@ -377,7 +377,7 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = { V0, WACK, MERGE, V0, WACK, MERGE, V0, WACK, MERGE, - VB, WACK, ITCB, // verify merged byte and return it if acked ok + VB, WACK, ITCBV, // verify merged byte and return it if acked ok - with retry report FAIL }; @@ -689,6 +689,7 @@ int DCC::nextLoco = 0; ackOp const * DCC::ackManagerProg; ackOp const * DCC::ackManagerProgStart; byte DCC::ackManagerByte; +byte DCC::ackManagerByteVerify; byte DCC::ackManagerStash; int DCC::ackManagerWord; byte DCC::ackManagerRetry; @@ -730,6 +731,7 @@ void DCC::ackManagerSetup(int cv, byte byteValueOrBitnum, ackOp const program[] ackManagerProgStart = program; ackManagerRetry = ackRetry; ackManagerByte = byteValueOrBitnum; + ackManagerByteVerify = byteValueOrBitnum; ackManagerBitNum=byteValueOrBitnum; ackManagerCallback = callback; } @@ -828,7 +830,18 @@ void DCC::ackManagerLoop() { return; } break; - + + case ITCBV: // If True callback(byte) - Verify + if (ackReceived) { + if (ackManagerByte == ackManagerByteVerify) { + ackRetrySum ++; + LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum); + } + callback(ackManagerByte); + return; + } + break; + case ITCB7: // If True callback(byte & 0x7F) if (ackReceived) { callback(ackManagerByte & 0x7F); @@ -847,8 +860,8 @@ void DCC::ackManagerLoop() { callback(-1); return; - case RCOUNT: // ackRetry counter - ackRetrySum++; + case BIV: // ackManagerByte initial value + ackManagerByte = ackManagerByteVerify; break; case STARTMERGE: @@ -919,7 +932,7 @@ void DCC::callback(int value) { // check for automatic retry if (value == -1 && ackManagerRetry > 0) { ackRetrySum ++; - LCD(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); + LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum); ackManagerRetry --; ackManagerProg = ackManagerProgStart; return; From 222eca6524778c5346000432138ed331b6bd4ca7 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 6 Sep 2021 12:27:21 +0100 Subject: [PATCH 087/125] XFON/XFOFF macros --- RMFT2.cpp | 14 +++++++++++--- RMFT2.h | 2 +- RMFTMacros.h | 6 ++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index bc9c01e..842b460 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -530,11 +530,19 @@ void RMFT2::loop2() { case OPCODE_FON: if (loco) DCC::setFn(loco,operand,true); break; - + case OPCODE_FOFF: if (loco) DCC::setFn(loco,operand,false); break; - + + case OPCODE_XFON: + DCC::setFn(operand,GET_OPERAND(1),true); + break; + + case OPCODE_XFOFF: + DCC::setFn(operand,GET_OPERAND(1),false); + break; + case OPCODE_FOLLOW: progCounter=locateRouteStart(operand); if (progCounter<0) kill(F("FOLLOW unknown"), operand); @@ -631,7 +639,7 @@ void RMFT2::loop2() { case OPCODE_ROUTE: case OPCODE_AUTOMATION: case OPCODE_SEQUENCE: - DIAG(F("EXRAIL begin(%d)"),operand); + if (diag) DIAG(F("EXRAIL begin(%d)"),operand); break; case OPCODE_PAD: // Just a padding for previous opcode needing >1 operad byte. diff --git a/RMFT2.h b/RMFT2.h index 26cdf1d..a254070 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -34,7 +34,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF, OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER, OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR, OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN, diff --git a/RMFTMacros.h b/RMFTMacros.h index 3e1cde1..f638001 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -113,6 +113,8 @@ #define UNJOIN #define UNLATCH(sensor_id) #define WAITFOR(pin) +#define XFOFF(cab,func) +#define XFON(cab,func) #include "myAutomation.h" @@ -196,6 +198,8 @@ const int StringMacroTracker1=__COUNTER__; #undef UNJOIN #undef UNLATCH #undef WAITFOR +#undef XFOFF +#undef XFON // Define macros for route code creation #define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF @@ -263,6 +267,8 @@ const int StringMacroTracker1=__COUNTER__; #define UNJOIN OPCODE_UNJOIN,NOP, #define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id), #define WAITFOR(pin) OPCODE_WAITFOR,V(pin), +#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func), +#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func), // PASS2 Build RouteCode const int StringMacroTracker2=__COUNTER__; From 81dc512c8608a54813a1afcb8206da6e950aef41 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Mon, 6 Sep 2021 12:30:25 +0100 Subject: [PATCH 088/125] Turnout print state and tell withrottle --- Turnouts.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Turnouts.cpp b/Turnouts.cpp index 41b6b39..6dbc402 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -119,7 +119,7 @@ // Send message to JMRI etc. over Serial USB. This is done here // to ensure that the message is sent when the turnout operation // is not initiated by a Serial command. - printState(id, &Serial); + tt->printState(&Serial); return true; } @@ -140,6 +140,8 @@ bool ok = tt->setClosedInternal(closeFlag); if (ok) { + turnoutlistHash++; // let withrottle know something changed + // Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress // is always zero for LCN turnouts. if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0) @@ -152,7 +154,7 @@ // Send message to JMRI etc. over Serial USB. This is done here // to ensure that the message is sent when the turnout operation // is not initiated by a Serial command. - printState(id, &Serial); + tt->printState(&Serial); } return ok; } @@ -214,7 +216,7 @@ // Display, on the specified stream, the current state of the turnout (1=thrown or 0=closed). /* static */ void Turnout::printState(uint16_t id, Print *stream) { Turnout *tt = get(id); - if (!tt) tt->printState(stream); + if (tt) tt->printState(stream); } From 9b3c6fe8969c3a714c0136ad9e57e3944701aa80 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Wed, 8 Sep 2021 16:21:04 +0100 Subject: [PATCH 089/125] LCN and SERIAL/1/2/3 --- RMFTMacros.h | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index f638001..e02aece 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -83,6 +83,7 @@ #define JOIN #define LATCH(sensor_id) #define LCD(row,msg) +#define LCN(msg) #define ONCLOSE(turnout_id) #define ONTHROW(turnout_id) #define PAUSE @@ -97,6 +98,9 @@ #define REV(speed) #define START(route) #define SENDLOCO(cab,route) +#define SERIAL1(msg) +#define SERIAL2(msg) +#define SERIAL3(msg) #define SERVO(id,position,profile) #define SERVO2(id,position,duration) #define SETLOCO(loco) @@ -127,6 +131,10 @@ #undef EXRAIL #undef PRINT +#undef LCN +#undef SERIAL1 +#undef SERIAL2 +#undef SERIAL3 #undef ENDEXRAIL #undef LCD const int StringMacroTracker1=__COUNTER__; @@ -134,6 +142,10 @@ const int StringMacroTracker1=__COUNTER__; #define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) { #define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }} #define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break; +#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break; +#define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break; +#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(L&Serial2,F(msg));break; +#define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break; #define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" @@ -167,6 +179,7 @@ const int StringMacroTracker1=__COUNTER__; #undef JOIN #undef LATCH #undef LCD +#undef LCN #undef ONCLOSE #undef ONTHROW #undef PAUSE @@ -186,6 +199,9 @@ const int StringMacroTracker1=__COUNTER__; #undef SERVO2 #undef FADE #undef SENDLOCO +#undef SERIAL1 +#undef SERIAL2 +#undef SERIAL3 #undef SETLOCO #undef SET #undef SPEED @@ -238,7 +254,8 @@ const int StringMacroTracker1=__COUNTER__; #define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,NOP, #define JOIN OPCODE_JOIN,NOP, #define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id), -#define LCD(id,msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2), +#define LCD(id,msg) PRINT(msg) +#define LCN(msg) PRINT(msg) #define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id), #define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id), #define PAUSE OPCODE_PAUSE,NOP, @@ -252,6 +269,9 @@ const int StringMacroTracker1=__COUNTER__; #define RETURN OPCODE_RETURN,NOP, #define REV(speed) OPCODE_REV,V(speed), #define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), +#define SERIAL1(msg) PRINT(msg) +#define SERIAL2(msg) PRINT(msg) +#define SERIAL3(msg) PRINT(msg) #define START(route) OPCODE_START,V(route), #define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0), #define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L), From f38bf512abc5b9efd8bda5addf037dbe9ccd0cc6 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Wed, 8 Sep 2021 16:29:58 +0100 Subject: [PATCH 090/125] Include SERIAL --- RMFTMacros.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index e02aece..fc5140f 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -98,6 +98,7 @@ #define REV(speed) #define START(route) #define SENDLOCO(cab,route) +#define SERIAL(msg) #define SERIAL1(msg) #define SERIAL2(msg) #define SERIAL3(msg) @@ -132,6 +133,7 @@ #undef EXRAIL #undef PRINT #undef LCN +#undef SERIAL #undef SERIAL1 #undef SERIAL2 #undef SERIAL3 @@ -141,12 +143,13 @@ const int StringMacroTracker1=__COUNTER__; #define ALIAS(name,value) #define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) { #define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }} -#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break; -#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break; +#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break; +#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break; +#define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break; #define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break; #define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(L&Serial2,F(msg));break; #define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break; -#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; +#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" // Setup for Pass 3: create main routes table @@ -199,6 +202,7 @@ const int StringMacroTracker1=__COUNTER__; #undef SERVO2 #undef FADE #undef SENDLOCO +#undef SERIAL #undef SERIAL1 #undef SERIAL2 #undef SERIAL3 @@ -269,6 +273,7 @@ const int StringMacroTracker1=__COUNTER__; #define RETURN OPCODE_RETURN,NOP, #define REV(speed) OPCODE_REV,V(speed), #define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), +#define SERIAL(msg) PRINT(msg) #define SERIAL1(msg) PRINT(msg) #define SERIAL2(msg) PRINT(msg) #define SERIAL3(msg) PRINT(msg) From f8311b8c56a7793db210884cde7c385c475b2cbb Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Wed, 8 Sep 2021 14:00:42 -0500 Subject: [PATCH 091/125] line added sentResetsSincePacket in DCCWaveform::setPowerMode(POWERMODE mode) -- to pause while power is off due to PROG TRACK POWER OVERLOAD and line added after case BASELINE in DCC.cpp --- DCCWaveform.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index 0d23897..a82d36c 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -114,6 +114,7 @@ void DCCWaveform::setPowerMode(POWERMODE mode) { powerMode = mode; bool ison = (mode == POWERMODE::ON); motorDriver->setPower( ison); + sentResetsSincePacket=0; } From ebabbbe59e07685fad80941b34440cd23ce68f78 Mon Sep 17 00:00:00 2001 From: Ash-4 <81280775+Ash-4@users.noreply.github.com> Date: Wed, 8 Sep 2021 14:06:39 -0500 Subject: [PATCH 092/125] pause program steps if OVERLOAD line added to pause program steps during OVERLOAD. case BASELINE if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return; -- also added a line in DCCWaveform.cpp --- DCC.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DCC.cpp b/DCC.cpp index 5165f72..f747362 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -757,6 +757,7 @@ void DCC::ackManagerLoop() { // (typically waiting for a reset counter or ACK waiting, or when all finished.) switch (opcode) { case BASELINE: + if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return; if (checkResets(DCCWaveform::progTrack.autoPowerOff || ackManagerRejoin ? 20 : 3)) return; DCCWaveform::progTrack.setAckBaseline(); callbackState=READY; From 254d83b6fc6e1e6cec06842331eb18d9b1c144e7 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 9 Sep 2021 10:12:27 +0100 Subject: [PATCH 093/125] Remove SERIAL warning --- RMFTMacros.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index fc5140f..c66dc3c 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -19,8 +19,9 @@ #ifndef RMFTMacros_H #define RMFTMacros_H -// remove normal code LCD macro (will be restored later) +// remove normal code LCD & SERIAL macros (will be restored later) #undef LCD +#undef SERIAL // This file will include and build the EXRAIL script and associated helper tricks. @@ -299,8 +300,9 @@ const int StringMacroTracker1=__COUNTER__; const int StringMacroTracker2=__COUNTER__; #include "myAutomation.h" -// Restore normal code LCD macro +// Restore normal code LCD & SERIAL macro #undef LCD #define LCD StringFormatter::lcd - +#undef SERIAL +#define SERIAL 0x0 #endif From 70b59d491c547096a091af7b92fba6f70ece0a73 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 9 Sep 2021 10:23:27 +0100 Subject: [PATCH 094/125] Ash's OVERLOAD check Makes prog track accesses wait if track in overload --- DCC.cpp | 1 + DCCWaveform.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/DCC.cpp b/DCC.cpp index 79c5b19..0674fce 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -753,6 +753,7 @@ void DCC::ackManagerLoop() { // (typically waiting for a reset counter or ACK waiting, or when all finished.) switch (opcode) { case BASELINE: + if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return; if (checkResets(DCCWaveform::progTrack.autoPowerOff || ackManagerRejoin ? 20 : 3)) return; DCCWaveform::progTrack.setAckBaseline(); callbackState=READY; diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index 0d23897..dd447ca 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -114,6 +114,7 @@ void DCCWaveform::setPowerMode(POWERMODE mode) { powerMode = mode; bool ison = (mode == POWERMODE::ON); motorDriver->setPower( ison); + sentResetsSincePacket=0; } From 2ed578821ffec6103342285e6b8a2ebf132a2fad Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 11 Sep 2021 13:35:11 +0100 Subject: [PATCH 095/125] Add analogue inputs to HAL. Add ability to read analogue inputs on arduino and on external ADS1115 I2C modules. --- IODevice.cpp | 54 +++++++++++++++++- IODevice.h | 14 ++++- IO_AnalogueInputs.h | 131 ++++++++++++++++++++++++++++++++++++++++++++ platformio.ini | 3 +- 4 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 IO_AnalogueInputs.h diff --git a/IODevice.cpp b/IODevice.cpp index e19ddef..98584ca 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -241,6 +241,18 @@ int IODevice::read(VPIN vpin) { return false; } +// Read analogue value from virtual pin. +int IODevice::readAnalogue(VPIN vpin) { + for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { + if (dev->owns(vpin)) + return dev->_readAnalogue(vpin); + } +#ifdef DIAG_IO + //DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); +#endif + return false; +} + #else // !defined(IO_NO_HAL) @@ -259,6 +271,13 @@ int IODevice::read(VPIN vpin) { pinMode(vpin, INPUT_PULLUP); return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1) } +int IODevice::readAnalogue(VPIN vpin) { + pinMode(vpin, INPUT); + noInterrupts(); + int value = analogRead(vpin); + interrupts(); + return value; +} void IODevice::loop() {} void IODevice::DumpAll() { DIAG(F("NO HAL CONFIGURED!")); @@ -330,7 +349,7 @@ void ArduinoPins::_write(VPIN vpin, int value) { } } -// Device-specific read function. +// Device-specific read function (digital input). int ArduinoPins::_read(VPIN vpin) { int pin = vpin; uint8_t mask = 1 << ((pin-_firstVpin) % 8); @@ -352,6 +371,39 @@ int ArduinoPins::_read(VPIN vpin) { return value; } +// Device-specific readAnalogue function (analogue input) +int ArduinoPins::_readAnalogue(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); + } + // Since AnalogRead is also called from interrupt code, disable interrupts + // while we're using it. There's only one ADC shared by all analogue inputs + // on the Arduino, so we don't want interruptions. + //****************************************************************************** + // NOTE: If the HAL is running on a computer without the DCC signal generator, + // then interrupts needn't be disabled. Also, the DCC signal generator puts + // the ADC into fast mode, so if it isn't present, analogueRead calls will be much + // slower!! + //****************************************************************************** + noInterrupts(); + int value = analogRead(pin); + interrupts(); + + #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); } diff --git a/IODevice.h b/IODevice.h index 65f5d35..58de64b 100644 --- a/IODevice.h +++ b/IODevice.h @@ -141,6 +141,9 @@ public: // read invokes the IODevice instance's _read method. static int read(VPIN vpin); + // read invokes the IODevice instance's _readAnalogue method. + static int readAnalogue(VPIN vpin); + // loop invokes the IODevice instance's _loop method. static void loop(); @@ -188,12 +191,18 @@ protected: return false; } - // Method to read pin state (optionally implemented within device class) + // Method to read digital pin state (optionally implemented within device class) virtual int _read(VPIN vpin) { (void)vpin; return 0; }; + // Method to read analogue pin state (optionally implemented within device class) + virtual int _readAnalogue(VPIN vpin) { + (void)vpin; + return 0; + }; + // _isBusy 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 _isBusy(VPIN vpin) { @@ -343,8 +352,9 @@ private: 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. + // Device-specific read functions. int _read(VPIN vpin) override; + int _readAnalogue(VPIN vpin) override; void _display() override; diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h new file mode 100644 index 0000000..4c73329 --- /dev/null +++ b/IO_AnalogueInputs.h @@ -0,0 +1,131 @@ +/* + * © 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 . + */ + +#ifndef io_analogueinputs_h +#define io_analogueinputs_h + +// Uncomment following line to slow the scan cycle down to 1second ADC samples, with +// diagnostic output of scanned values. +//#define IO_ANALOGUE_SLOW + +#include "IODevice.h" +#include "I2CManager.h" +#include "DIAG.h" +#include "FSH.h" + +/********************************************************************************************** + * ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115. + * + * ADS1113 and ADS1114 are restricted to 1 input. ADS1115 has a multiplexer which allows + * any of four input pins to be read by its ADC. + * + * The driver polls the device in accordance with the constant 'scanInterval' below. On first loop + * entry, the multiplexer is set to pin A0 and the ADC is triggered. On second and subsequent + * entries, the analogue value is read from the conversion register and then the multiplexer and + * ADC are set up to read the next pin. + * + * The ADS111x is set up as follows: + * Single-shot scan + * Data rate 128 samples/sec (7.8ms/sample) + * Comparator off + * Gain FSR=6.144V + * The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading + * of 32767*(5.0/6.144) = 26666. + * + * Note: The device is simple and does not need initial configuration, so it should recover from + * temporary loss of communications or power. + **********************************************************************************************/ +class ADS111x: public IODevice { +public: + ADS111x(VPIN firstVpin, int nPins, uint8_t i2cAddress) { + _firstVpin = firstVpin; + _nPins = min(nPins,4); + _i2cAddress = i2cAddress; + _currentPin = _nPins; // Suppress read on first loop entry. + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) { + new ADS111x(firstVpin, nPins, i2cAddress); + } + void _begin() { + // Initialise ADS device + if (I2CManager.exists(_i2cAddress)) { +#ifdef DIAG_IO + _display(); +#endif + } else { + DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); + } + } + void _loop(unsigned long currentMicros) { + + if (currentMicros - _lastMicros >= scanInterval) { + // Check that previous non-blocking write has completed, if not then wait + _i2crb.wait(); + + // If _currentPin is in the valid range, continue reading the pin values + if (_currentPin < _nPins) { + _outBuffer[0] = 0x00; // Conversion register address + uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, 1, _outBuffer); // Read register + if (status == I2C_STATUS_OK) { + _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; + #ifdef IO_ANALOGUE_SLOW + DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); + #endif + } + } + // Move to next pin + if (++_currentPin >= _nPins) _currentPin = 0; + + // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details + // of configuration register settings. + _outBuffer[0] = 0x01; // Config register address + _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n + _outBuffer[2] = 0x83; // 128 samples/sec, comparator off + // Write command, without waiting for completion. + I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); + + _lastMicros = currentMicros; + } + } + int _readAnalogue(VPIN vpin) { + int pin = vpin - _firstVpin; + return _value[pin]; + } + void _display() { + DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1); + } + +protected: + // With ADC set to 128 samples/sec, that's 7.8ms/sample. So set the period between updates to 10ms + #ifndef IO_ANALOGUE_SLOW + const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds. + #else + const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds. + #endif + uint16_t _value[4]; + uint8_t _i2cAddress; + uint8_t _outBuffer[3]; + uint8_t _inBuffer[2]; + uint8_t _currentPin; // ADC pin currently being scanned + unsigned long _lastMicros = 0; + I2CRB _i2crb; +}; + +#endif // io_analogueinputs_h \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 953a658..8bba297 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,6 +16,7 @@ default_envs = unowifiR2 nano src_dir = . +include_dir = . [env] build_flags = -Wall -Wextra @@ -41,7 +42,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_flags = --echo -build_flags = -DDIAG_IO +build_flags = -DDIAG_IO -DDIAG_LOOPTIMES [env:mega2560-no-HAL] platform = atmelavr From d316b720697d47e91e70ea293212efa80a5eacff Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 14 Sep 2021 12:34:31 +0100 Subject: [PATCH 096/125] VL53L0X Time-Of-Flight sensor driver HAL Driver for VL53L0X Time-Of-Flight sensor. Basic implementation, which doesn't include most of the calibration etc. so is very lean on memory and CPU but not as accurate as it could be. --- IO_VL53L0X.h | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 IO_VL53L0X.h diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h new file mode 100644 index 0000000..a0ac902 --- /dev/null +++ b/IO_VL53L0X.h @@ -0,0 +1,245 @@ +/* + * © 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 . + */ + +/* + * The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting + * the reflection of the pulse. The time between the pulse and the receipt of reflections + * is measured and used to determine the distance to the reflecting object. + * + * For economy of memory and processing time, this driver includes only part of the code + * that ST provide in their API. Also, the API code isn't very clear and it is not easy + * to identify what operations are useful and what are not. + * The operation shown here doesn't include any calibration, so is probably not as accurate + * as using the full driver, but it's probably accurate enough for the purpose. + * + * The device driver allocates up to 3 vpins to the device. A digital read on any of the pins + * will return a value that indicates whether the object is within the threshold range (1) + * or not (0). An analogue read on the first pin returns the last measured distance (in mm), + * the second pin returns the signal strength, and the third pin returns detected + * ambient light level. + * + * The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module, + * you can use this address. However, the address can be modified by software. If + * you select another address, that address will be written to the device and used until the device is reset. + * + * If you have more than one module, then you will need to specify a digital VPIN (Arduino + * digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now, + * when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once + * all VL53L0X modules are turned off, the driver works through each module in turn by + * setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address. + * In this way, many VL53L0X modules can be connected to the one I2C bus, each one + * using with a distinct I2C address. + * + * The driver is configured as follows: + * + * Single VL53L0X module: + * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold); + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is 1, 2 or 3, + * i2cAddress is the address of the device (normally 0x29), + * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm), + * and highThreshold is the distance at which the digital vpin state is set to 0 (in mm). + * + * Multiple VL53L0X modules: + * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin); + * ... + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is 1, 2 or 3, + * i2cAddress is the address of the device (any valid address except 0x29), + * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm), + * highThreshold is the distance at which the digital vpin state is set to 0 (in mm), + * and xshutPin is the VPIN number corresponding to a digital output that is connected to the + * XSHUT terminal on the module. + * + * Example: + * In mySetup function within mySetup.cpp: + * VL53L0X::create(4000, 3, 0x29, 200, 250); + * Sensor::create(4000, 4000, 0); // Create a sensor + * + * When an object comes within 200mm of the sensor, a message + * + * will be sent over the serial USB, and when the object moves more than 250mm from the sensor, + * a message + * + * will be sent. + * + */ + +#ifndef IO_VL53L0X_h +#define IO_VL53L0X_h + +#include "IODevice.h" + +class VL53L0X : public IODevice { +private: + uint8_t _i2cAddress; + uint16_t _ambient; + uint16_t _distance; + uint16_t _signal; + uint16_t _onThreshold; + uint16_t _offThreshold; + uint8_t _xshutPin; + bool _value; + bool _initialising = true; + uint8_t _entryCount = 0; + unsigned long _lastEntryTime = 0; + bool _scanInProgress = false; + // Register addresses + enum : uint8_t { + VL53L0X_REG_SYSRANGE_START=0x00, + VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13, + VL53L0X_REG_RESULT_RANGE_STATUS=0x14, + VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A, + }; + const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29; + +public: + VL53L0X(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) { + _firstVpin = firstVpin; + _nPins = min(nPins, 3); + _i2cAddress = i2cAddress; + _onThreshold = onThreshold; + _offThreshold = offThreshold; + _xshutPin = xshutPin; + _value = 0; + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) { + new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin); + } + +protected: + void _begin() override { + _initialising = true; + // Check if device is already responding on the nominated address. + if (I2CManager.exists(_i2cAddress)) { + // Yes, it's already on this address, so skip the address initialisation. + _entryCount = 3; + } else { + _entryCount = 0; + } + } + void _loop(unsigned long currentMicros) override { + if (_initialising) { + switch (_entryCount++) { + case 0: + // On first entry to loop, reset this module by pulling XSHUT low. All modules + // will be reset in turn. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0); + break; + case 1: + // On second entry, set XSHUT pin high to allow the module to restart. + // On the module, there is a diode in series with the XSHUT pin to + // protect the low-voltage pin against +5V. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1); + // Allow the module time to restart + delay(10); + // Then write the desired I2C address to the device, while this is the only + // module responding to the default address. + I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress); + break; + case 3: + if (I2CManager.exists(_i2cAddress)) { + _display(); + } + _initialising = false; + _entryCount = 0; + break; + default: + break; + } + } else if (_lastEntryTime - currentMicros > 10000UL) { + // Service device every 10ms + _lastEntryTime = currentMicros; + + if (!_scanInProgress) { + // Not scanning, so initiate a scan + write_reg(VL53L0X_REG_SYSRANGE_START, 0x01); + _scanInProgress = true; + + } else { + // Scan in progress, so check for completion. + uint8_t status = read_reg(VL53L0X_REG_RESULT_RANGE_STATUS); + if (status & 1) { + // Completed. Retrieve data + uint8_t inBuffer[12]; + read_registers(VL53L0X_REG_RESULT_RANGE_STATUS, inBuffer, 12); + uint8_t deviceRangeStatus = ((inBuffer[0] & 0x78) >> 3); + if (deviceRangeStatus == 0x0b) { + // Range status OK, so use data + _ambient = makeuint16(inBuffer[7], inBuffer[6]); + _signal = makeuint16(inBuffer[9], inBuffer[8]); + _distance = makeuint16(inBuffer[11], inBuffer[10]); + if (_distance <= _onThreshold) + _value = true; + else if (_distance > _offThreshold) + _value = false; + } + _scanInProgress = false; + } + } + } + } + // For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level. + int _readAnalogue(VPIN vpin) override { + int pin = vpin - _firstVpin; + switch (pin) { + case 0: + return _distance; + case 1: + return _signal; + case 2: + return _ambient; + default: + return -1; + } + } + // For digital read, return the same value for all pins. + int _read(VPIN vpin) override { + return _value; + } + void _display() override { + DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm"), + _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold); + } + + +private: + inline uint16_t makeuint16(byte lsb, byte msb) { + return (((uint16_t)msb) << 8) | lsb; + } + void write_reg(uint8_t reg, uint8_t data) { + // write byte to register + uint8_t outBuffer[2]; + outBuffer[0] = reg; + outBuffer[1] = data; + I2CManager.write(_i2cAddress, outBuffer, 2); + } + uint8_t read_reg(uint8_t reg) { + // read byte from register register + uint8_t inBuffer[1]; + I2CManager.read(_i2cAddress, inBuffer, 1, ®, 1); + return inBuffer[0]; + } + void read_registers(uint8_t reg, uint8_t buffer[], uint8_t size) { + I2CManager.read(_i2cAddress, buffer, size, ®, 1); + } +}; + +#endif // IO_VL53L0X_h From f7d34b92ee0e81fa46e4a71e3a76164922ae1c1b Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 14 Sep 2021 17:14:29 +0100 Subject: [PATCH 097/125] Update mySetup.cpp_example.txt --- mySetup.cpp_example.txt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mySetup.cpp_example.txt b/mySetup.cpp_example.txt index 949088a..0e2be50 100644 --- a/mySetup.cpp_example.txt +++ b/mySetup.cpp_example.txt @@ -13,6 +13,7 @@ #include "Turnouts.h" #include "Sensors.h" #include "IO_HCSR04.h" +#include "IO_VL53L0X.h" // The #if directive prevent compile errors for Uno and Nano by excluding the @@ -89,6 +90,27 @@ //HCSR04 sonarModule1(2000, 30, 31, 20, 25); //HCSR04 sonarModule2(2001, 30, 32, 20, 25); +// The following directive defines a single VL53L0X Time-of-Flight sensor. +// The parameters are: +// VPIN=5000 +// Number of VPINs=1 +// I2C address=0x29 (default for this chip) +// Minimum trigger range=200mm (VPIN goes to 1 when <20cm) +// Maximum trigger range=250mm (VPIN goes to 0 when >25cm) + +//VL53L0X tofModule1(5000, 1, 0x29, 200, 250); + +// For multiple VL53L0X modules, add another parameter which is a VPIN connected to the +// module's XSHUT pin. This allows the modules to be configured, at start, +// with distinct I2C addresses. In this case, the address 0x29 is only used during +// initialisation to configure each device with the desired unique I2C address. +// The examples below have one module's XSHUT pin connected to Arduino pin 34, +// and the other's connected to the second pin on the first MCP23017 module (VPIN 165). +// The first module is given I2C address 0x30 and the second is 0x31. + +//VL53L0X tofModule1(5000, 1, 0x30, 200, 250, 34); +//VL53L0X tofModule2(5001, 1, 0x31, 200, 250, 165); + // The function mySetup() is invoked from CS if it exists within the build. // It is called just before mysetup.h is executed, so things set up within here can be From 02a715d54d29cbc2d35a62221d37e53d722869f1 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 15 Sep 2021 00:23:24 +0100 Subject: [PATCH 098/125] New DFPlayer MP3 device, and tidy comments in other drivers. --- IO_AnalogueInputs.h | 8 +- IO_DFPlayer.h | 219 ++++++++++++++++++++++++++++++++++++++++++++ IO_HCSR04.h | 68 ++++++++------ IO_VL53L0X.h | 8 +- 4 files changed, 273 insertions(+), 30 deletions(-) create mode 100644 IO_DFPlayer.h diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 4c73329..0628bc9 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -42,12 +42,18 @@ * * The ADS111x is set up as follows: * Single-shot scan - * Data rate 128 samples/sec (7.8ms/sample) + * Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms) * Comparator off * Gain FSR=6.144V * The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading * of 32767*(5.0/6.144) = 26666. * + * A device is configured by the following: + * ADS111x::create(firstVpin, nPins, i2cAddress); + * for example + * ADS111x::create(300, 1, 0x48); // single-input ADS1113 + * ADS111x::create(300, 4, 0x48); // four-input ADS1115 + * * Note: The device is simple and does not need initial configuration, so it should recover from * temporary loss of communications or power. **********************************************************************************************/ diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h new file mode 100644 index 0000000..bb0e5c7 --- /dev/null +++ b/IO_DFPlayer.h @@ -0,0 +1,219 @@ +/* + * © 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 . + */ + +/* + * DFPlayer is an MP3 player module with an SD card holder. It also has an integrated + * amplifier, so it only needs a power supply and a speaker. + * + * This driver allows the device to be controlled through IODevice::write() and + * IODevice::writeAnalogue() calls. + * + * The driver is configured as follows: + * + * DFPlayer::create(firstVpin, nPins, Serialn); + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is the number of pins to be allocated (max 5) + * and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial2). + * + * + * Example: + * In mySetup function within mySetup.cpp: + * DFPlayer::create(3500, 5, Serial2); + * Writing a value 0-2999 to the first pin will select a numbered file from the SD card; + * Writing a value 0-30 to the second pin will set the volume of the output; + * Writing a digital value to the first pin will play or stop the file; + * + * From EX-RAIL, the following commands may be used: + * SET(3500) -- starts playing the first file on the SD card + * SET(3501) -- starts playing the second file on the SD card + * etc. + * RESET(3500) -- stops all playing on the player + * WAITFOR(3500) -- wait for the file currently being played by the player to complete + * + * NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly + * to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse. + * A 1k resistor in series with the module's RX terminal will alleviate this. + */ + +#ifndef IO_DFPlayer_h +#define IO_DFPlayer_h + +#include "IODevice.h" + +class DFPlayer : public IODevice { +private: + HardwareSerial *_serial; + bool _playing = false; + uint8_t _inputIndex = 0; + +public: + DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) { + _firstVpin = firstVpin; + _nPins = min(nPins, 3); + _serial = &serial; + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) { + new DFPlayer(firstVpin, nPins, serial); + } + +protected: + void _begin() override { + _serial->begin(9600); + _display(); + } + + void _loop(unsigned long) override { + // Check for incoming data on _serial, and update busy flag accordingly. + // Expected message is in the form "7F FF 06 3D xx xx xx xx xx EF" + while (_serial->available()) { + int c = _serial->read(); +// DIAG(F("Received: %x"), c); + if (c == 0x7E) + _inputIndex = 1; + else if ((c==0xFF && _inputIndex==1) || (c==0x06 && _inputIndex==2) + || (c==0x3D && _inputIndex==3) || (_inputIndex >=4 && _inputIndex <= 8)) + _inputIndex++; + else if (c==0xEF && _inputIndex==9) { + // End of play + #ifdef DIAG_IO + DIAG(F("DFPlayer: Finished")); + #endif + _playing = false; + _inputIndex = 0; + } + } + } + + // Write with value 1 starts playing a song. The relative pin number is the file number. + // Write with value 0 stops playing. + void _write(VPIN vpin, int value) override { + int pin = vpin - _firstVpin; + if (value) { + // Value 1, start playing + #ifdef DIAG_IO + DIAG(F("DFPlayer: Play %d"), pin+1); + #endif + sendPacket(0x03, pin+1); + _playing = true; + } else { + // Value 0, stop playing + #ifdef DIAG_IO + DIAG(F("DFPlayer: Stop")); + #endif + sendPacket(0x16); + _playing = false; + } + } + + // WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0. + // If value is zero, it stops playing. + // WriteAnalogue on second pin sets the output volume. + void _writeAnalogue(VPIN vpin, int value, uint8_t, uint16_t) override { + uint8_t pin = _firstVpin - vpin; + switch (pin) { + case 0: + if (value > 0) { + // Play global track + if (value > 2999) value = 2999; + #ifdef DIAG_IO + DIAG(F("DFPlayer: Play %d"), value); + #endif + sendPacket(0x03, value); + _playing = true; + } else { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Stop")); + #endif + sendPacket(0x16); + _playing = false; + } + break; + case 1: + // Set volume (0-30) + if (value > 30) value = 30; + else if (value < 0) value = 0; + #ifdef DIAG_IO + DIAG(F("DFPlayer: Volume %d"), value); + #endif + sendPacket(0x06, value); + break; + default: + break; + } + } + + bool _isBusy(VPIN vpin) override { + (void)vpin; // avoid compiler warning. + return _playing; + } + + void _display() override { + DIAG(F("DFPlayer Configured on Vpins:%d-%d")); + } + +private: + // 7E FF 06 0F 00 01 01 xx xx EF + // 0 -> 7E is start code + // 1 -> FF is version + // 2 -> 06 is length + // 3 -> 0F is command + // 4 -> 00 is no receive + // 5~6 -> 01 01 is argument + // 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 ) + // 9 -> EF is end code + + void sendPacket(uint8_t command, uint16_t arg = 0) + { + uint8_t out[] = { 0x7E, + 0xFF, + 06, + command, + 00, + static_cast(arg >> 8), + static_cast(arg & 0x00ff), + 00, + 00, + 0xEF }; + + setChecksum(out); + + _serial->write(out, sizeof(out)); + } + + uint16_t calcChecksum(uint8_t* packet) + { + uint16_t sum = 0; + for (int i = 1; i < 7; i++) + { + sum += packet[i]; + } + return -sum; + } + + void setChecksum(uint8_t* out) + { + uint16_t sum = calcChecksum(out); + + out[7] = (sum >> 8); + out[8] = (sum & 0xff); + } +}; + +#endif // IO_DFPlayer_h diff --git a/IO_HCSR04.h b/IO_HCSR04.h index 98340ff..76b8493 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -17,31 +17,37 @@ * along with CommandStation. If not, see . */ -/* +/* * The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver. - * It is operated through two signal pins. When the transmit pin is set to 1 for - * 10us, on the falling edge the transmitter sends a short transmission of + * It is operated through two signal pins. When the transmit pin is set to 1 + * for 10us, on the falling edge the transmitter sends a short transmission of * 8 pulses (like a sonar 'ping'). This is reflected off objects and received * by the receiver. A pulse is sent on the receive pin whose length is equal * to the delay between the transmission of the pulse and the detection of * its echo. The distance of the reflecting object is calculated by halving * the time (to allow for the out and back distance), then multiplying by the * speed of sound (assumed to be constant). - * + * * This driver polls the HC-SR04 by sending the trigger pulse and then measuring - * the length of the received pulse. If the calculated distance is less than the - * threshold, the output changes to 1. If it is greater than the threshold plus - * a hysteresis margin, the output changes to 0. - * - * The measurement would be more reliable if interrupts were disabled while the - * pulse is being timed. However, this would affect other functions in the CS - * so the measurement is being performed with interrupts enabled. Also, we could - * use an interrupt pin in the Arduino for the timing, but the same consideration - * applies. - * - * Note: The timing accuracy required by this means that the pins have to be - * direct Arduino pins; GPIO pins on an IO Extender cannot provide the required - * accuracy. + * the length of the received pulse. If the calculated distance is less than + * the threshold, the output state returned by a read() call changes to 1. If + * the distance is greater than the threshold plus a hysteresis margin, the + * output changes to 0. The device also supports readAnalogue(), which returns + * the measured distance in cm, or 32767 if the distance exceeds the + * offThreshold. + * + * It might be thought that the measurement would be more reliable if interrupts + * were disabled while the pulse is being timed. However, this would affect + * other functions in the CS so the measurement is being performed with + * interrupts enabled. Also, we could use an interrupt pin in the Arduino for + * the timing, but the same consideration applies. In any case, the DCC + * interrupt occurs once every 58us, so any IRC code is much faster than that. + * And 58us corresponds to 1cm in the calculation, so the effect of + * interrupts is negligible. + * + * Note: The timing accuracy required for measuring the pulse length means that + * the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot + * provide the required accuracy. */ #ifndef IO_HCSR04_H @@ -58,6 +64,8 @@ private: // Thresholds for setting active state in cm. uint8_t _onThreshold; // cm uint8_t _offThreshold; // cm + // Last measured distance in cm. + uint16_t _distance; // Active=1/inactive=0 state uint8_t _value = 0; // Time of last loop execution @@ -101,12 +109,17 @@ protected: return _value; } + int _readAnalogue(VPIN vpin) override { + (void)vpin; // avoid compiler warning + return _distance; + } + // _loop function - read HC-SR04 once every 50 milliseconds. void _loop(unsigned long currentMicros) override { if (currentMicros - _lastExecutionTime > 50000UL) { _lastExecutionTime = currentMicros; - _value = read_HCSR04device(); + read_HCSR04device(); } } @@ -127,12 +140,13 @@ private: // measured distance is less than the onThreshold, and is set to 0 if the measured distance is // greater than the offThreshold. // - uint8_t read_HCSR04device() { + void read_HCSR04device() { // uint16 enough to time up to 65ms uint16_t startTime, waitTime, currentTime, maxTime; // If receive pin is still set on from previous call, abort the read. - if (ArduinoPins::fastReadDigital(_receivePin)) return _value; + if (ArduinoPins::fastReadDigital(_receivePin)) + return; // Send 10us pulse to trigger transmitter ArduinoPins::fastWriteDigital(_transmitPin, 1); @@ -148,7 +162,7 @@ private: waitTime = currentTime - startTime; if (waitTime > maxTime) { // Timeout waiting for pulse start, abort the read - return _value; + return; } } @@ -162,16 +176,16 @@ private: // and finish without waiting for end of pulse. if (waitTime > maxTime) { // Pulse length longer than maxTime, reset value. - return 0; + _value = 0; + _distance = 32767; + return; } } // Check if pulse length is below threshold, if so set value. //DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance); - uint16_t distance = waitTime / factor; // in centimetres - if (distance < _onThreshold) - return 1; - - return _value; + _distance = waitTime / factor; // in centimetres + if (_distance < _onThreshold) + _value = 1; } }; diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h index a0ac902..08de1aa 100644 --- a/IO_VL53L0X.h +++ b/IO_VL53L0X.h @@ -94,7 +94,7 @@ private: uint16_t _signal; uint16_t _onThreshold; uint16_t _offThreshold; - uint8_t _xshutPin; + VPIN _xshutPin; bool _value; bool _initialising = true; uint8_t _entryCount = 0; @@ -105,6 +105,7 @@ private: VL53L0X_REG_SYSRANGE_START=0x00, VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13, VL53L0X_REG_RESULT_RANGE_STATUS=0x14, + VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A, }; const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29; @@ -157,6 +158,9 @@ protected: case 3: if (I2CManager.exists(_i2cAddress)) { _display(); + // Set 2.8V mode + write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, + read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); } _initialising = false; _entryCount = 0; @@ -211,7 +215,7 @@ protected: } } // For digital read, return the same value for all pins. - int _read(VPIN vpin) override { + int _read(VPIN) override { return _value; } void _display() override { From 592f87303e9e506833dc69e04c1b24682acedbf9 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 15 Sep 2021 10:44:43 +0100 Subject: [PATCH 099/125] Update IO_AnalogueInputs.h Increase frequency of ADC conversions to 4ms, since 10ms driver cycle isn't enough time for a 7.8ms conversion to complete reliably. --- IO_AnalogueInputs.h | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 0628bc9..7d24586 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -69,6 +69,7 @@ public: static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) { new ADS111x(firstVpin, nPins, i2cAddress); } +private: void _begin() { // Initialise ADS device if (I2CManager.exists(_i2cAddress)) { @@ -79,22 +80,25 @@ public: DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); } } - void _loop(unsigned long currentMicros) { + void _loop(unsigned long currentMicros) override { if (currentMicros - _lastMicros >= scanInterval) { // Check that previous non-blocking write has completed, if not then wait - _i2crb.wait(); - - // If _currentPin is in the valid range, continue reading the pin values - if (_currentPin < _nPins) { - _outBuffer[0] = 0x00; // Conversion register address - uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, 1, _outBuffer); // Read register - if (status == I2C_STATUS_OK) { - _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; - #ifdef IO_ANALOGUE_SLOW - DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); - #endif + uint8_t status = _i2crb.wait(); + if (status == I2C_STATUS_OK) { + // If _currentPin is in the valid range, continue reading the pin values + if (_currentPin < _nPins) { + _outBuffer[0] = 0x00; // Conversion register address + uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1); // Read register + if (status == I2C_STATUS_OK) { + _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; + #ifdef IO_ANALOGUE_SLOW + DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); + #endif + } } + if (status != I2C_STATUS_OK) + DIAG(F("ADS111x I2C:x%d Error:%d"), _i2cAddress, status); } // Move to next pin if (++_currentPin >= _nPins) _currentPin = 0; @@ -103,23 +107,23 @@ public: // of configuration register settings. _outBuffer[0] = 0x01; // Config register address _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n - _outBuffer[2] = 0x83; // 128 samples/sec, comparator off + _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off // Write command, without waiting for completion. I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); _lastMicros = currentMicros; } } - int _readAnalogue(VPIN vpin) { + int _readAnalogue(VPIN vpin) override { int pin = vpin - _firstVpin; return _value[pin]; } - void _display() { + void _display() override { DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1); } -protected: - // With ADC set to 128 samples/sec, that's 7.8ms/sample. So set the period between updates to 10ms + // ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms. + // This is enough to allow the conversion to reliably complete in time. #ifndef IO_ANALOGUE_SLOW const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds. #else From 3dc0b1619c53d2b78863fd6cba287e0fe3e6f003 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 15 Sep 2021 21:37:38 +0100 Subject: [PATCH 100/125] Update IO_DFPlayer.h --- IO_DFPlayer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index bb0e5c7..4072cae 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -131,13 +131,13 @@ protected: case 0: if (value > 0) { // Play global track - if (value > 2999) value = 2999; + if (value > 2999) return; #ifdef DIAG_IO DIAG(F("DFPlayer: Play %d"), value); #endif sendPacket(0x03, value); _playing = true; - } else { + } else if (value == 0){ #ifdef DIAG_IO DIAG(F("DFPlayer: Stop")); #endif From f3658aaee715f8b1505bf29d650f2532e662ce3a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 16 Sep 2021 00:17:26 +0100 Subject: [PATCH 101/125] Update IO_HCSR04.h Change transmitPin to trigPin and receivePin to echoPin to match the markings on the device module. --- IO_HCSR04.h | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/IO_HCSR04.h b/IO_HCSR04.h index 76b8493..2df3733 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -59,8 +59,8 @@ class HCSR04 : public IODevice { private: // pins must be arduino GPIO pins, not extender pins or HAL pins. - int _transmitPin = -1; - int _receivePin = -1; + int _trigPin = -1; + int _echoPin = -1; // Thresholds for setting active state in cm. uint8_t _onThreshold; // cm uint8_t _offThreshold; // cm @@ -76,27 +76,27 @@ private: public: // Constructor perfroms static initialisation of the device object - HCSR04 (VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { + HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { _firstVpin = vpin; _nPins = 1; - _transmitPin = transmitPin; - _receivePin = receivePin; + _trigPin = trigPin; + _echoPin = echoPin; _onThreshold = onThreshold; _offThreshold = offThreshold; addDevice(this); } // Static create function provides alternative way to create object - static void create(VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { - new HCSR04(vpin, transmitPin, receivePin, onThreshold, offThreshold); + static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold); } protected: // _begin function called to perform dynamic initialisation of the device void _begin() override { - pinMode(_transmitPin, OUTPUT); - pinMode(_receivePin, INPUT); - ArduinoPins::fastWriteDigital(_transmitPin, 0); + pinMode(_trigPin, OUTPUT); + pinMode(_echoPin, INPUT); + ArduinoPins::fastWriteDigital(_trigPin, 0); _lastExecutionTime = micros(); #if defined(DIAG_IO) _display(); @@ -120,12 +120,14 @@ protected: _lastExecutionTime = currentMicros; read_HCSR04device(); + // Delay next loop entry until 50ms have elapsed. + //delayUntil(currentMicros + 50000UL); } } void _display() override { DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"), - _firstVpin, _transmitPin, _receivePin, _onThreshold, _offThreshold); + _firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold); } private: @@ -145,18 +147,18 @@ private: uint16_t startTime, waitTime, currentTime, maxTime; // If receive pin is still set on from previous call, abort the read. - if (ArduinoPins::fastReadDigital(_receivePin)) + if (ArduinoPins::fastReadDigital(_echoPin)) return; // Send 10us pulse to trigger transmitter - ArduinoPins::fastWriteDigital(_transmitPin, 1); + ArduinoPins::fastWriteDigital(_trigPin, 1); delayMicroseconds(10); - ArduinoPins::fastWriteDigital(_transmitPin, 0); + ArduinoPins::fastWriteDigital(_trigPin, 0); // Wait for receive pin to be set startTime = currentTime = micros(); maxTime = factor * _offThreshold * 2; - while (!ArduinoPins::fastReadDigital(_receivePin)) { + while (!ArduinoPins::fastReadDigital(_echoPin)) { // lastTime = currentTime; currentTime = micros(); waitTime = currentTime - startTime; @@ -169,7 +171,7 @@ private: // Wait for receive pin to reset, and measure length of pulse startTime = currentTime = micros(); maxTime = factor * _offThreshold; - while (ArduinoPins::fastReadDigital(_receivePin)) { + while (ArduinoPins::fastReadDigital(_echoPin)) { currentTime = micros(); waitTime = currentTime - startTime; // If pulse is too long then set return value to zero, From 07cc45d86113533bd47500d6bc8929b06b71b073 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 16 Sep 2021 12:39:51 +0100 Subject: [PATCH 102/125] Update IO_DFPlayer.h Fix volume control command. --- IO_DFPlayer.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index 4072cae..061cd3f 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -126,7 +126,7 @@ protected: // If value is zero, it stops playing. // WriteAnalogue on second pin sets the output volume. void _writeAnalogue(VPIN vpin, int value, uint8_t, uint16_t) override { - uint8_t pin = _firstVpin - vpin; + uint8_t pin = vpin - _firstVpin; switch (pin) { case 0: if (value > 0) { @@ -159,13 +159,12 @@ protected: } } - bool _isBusy(VPIN vpin) override { - (void)vpin; // avoid compiler warning. + bool _isBusy(VPIN) override { return _playing; } void _display() override { - DIAG(F("DFPlayer Configured on Vpins:%d-%d")); + DIAG(F("DFPlayer Configured on Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1); } private: From d077e3a2ffae500d5b097bb6f3a51f26f097f9ad Mon Sep 17 00:00:00 2001 From: Asbelos Date: Thu, 16 Sep 2021 16:47:47 +0100 Subject: [PATCH 103/125] Auto power on and POWEROFF macro --- RMFT2.cpp | 10 ++++++++++ RMFT2.h | 2 +- RMFTMacros.h | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 842b460..9ab89f6 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -19,6 +19,7 @@ #include #include "RMFT2.h" #include "DCC.h" +#include "DCCWaveform.h" #include "DIAG.h" #include "WiThrottle.h" #include "DCCEXParser.h" @@ -326,6 +327,8 @@ int RMFT2::locateRouteStart(int16_t _route) { void RMFT2::driveLoco(byte speed) { if (loco<=0) return; // Prevent broadcast! if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert); + if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) + DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); DCC::setThrottle(loco,speed, forward^invert); speedo=speed; } @@ -477,6 +480,11 @@ void RMFT2::loop2() { if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1)); break; + case OPCODE_POWEROFF: + DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF); + DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF); + break; + case OPCODE_RESUME: pausingTask=NULL; driveLoco(speedo); @@ -572,6 +580,8 @@ void RMFT2::loop2() { return; case OPCODE_JOIN: + DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); + DCCWaveform::progTrack.setPowerMode(POWERMODE::ON); DCC::setProgTrackSyncMain(true); break; diff --git a/RMFT2.h b/RMFT2.h index a254070..90080b7 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -40,7 +40,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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_SENDLOCO, - OPCODE_PAUSE, OPCODE_RESUME, + OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF, OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, OPCODE_PRINT, OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL diff --git a/RMFTMacros.h b/RMFTMacros.h index c66dc3c..9fd1c22 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -90,6 +90,7 @@ #define PAUSE #define PRINT(msg) #define POM(cv,value) +#define POWEROFF #define READ_LOCO #define RED(signal_id) #define RESERVE(blockid) @@ -188,6 +189,7 @@ const int StringMacroTracker1=__COUNTER__; #undef ONTHROW #undef PAUSE #undef POM +#undef POWEROFF #undef PRINT #undef READ_LOCO #undef RED @@ -265,6 +267,7 @@ const int StringMacroTracker1=__COUNTER__; #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 POWEROFF OPCODE_POWEROFF,NOP, #define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2), #define READ_LOCO OPCODE_READ_LOCO1,NOP,OPCODE_READ_LOCO2,NOP, #define RED(signal_id) OPCODE_RED,V(signal_id), From ad7cd5f401d5c01f5ecb6f700d661d5d3fbbfa68 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 17 Sep 2021 11:36:08 +0100 Subject: [PATCH 104/125] Remove virtual _isBusy() function in favor of _read(). When writing to analogue outputs pins, the digital _read() function now returns the 'busy' status of the analogue pin. Consequently, the _isBusy() function becomes superfluous and has been removed. The static IODevice::isBusy() function now calls the object's _read() function instead. Also, limit in DFPlayer of 3 pins has been removed. --- IODevice.cpp | 19 ++++++++++++++----- IODevice.h | 19 +++++++++---------- IO_DFPlayer.h | 5 +++-- IO_PCA9685.cpp | 4 ++-- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 98584ca..cf90efe 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -53,7 +53,9 @@ void IODevice::begin() { MCP23017::create(180, 16, 0x21); // Call the begin() methods of each configured device in turn + unsigned long currentMicros = micros(); for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) { + dev->_nextEntryTime = currentMicros; dev->_begin(); } _initPhase = false; @@ -69,8 +71,14 @@ void IODevice::loop() { unsigned long currentMicros = micros(); // Call every device's loop function in turn, one per entry. if (!_nextLoopDevice) _nextLoopDevice = _firstDevice; - if (_nextLoopDevice) { + // Check if device exists, and is due to run + if (_nextLoopDevice /* && ((long)(currentMicros-_nextLoopDevice->_nextEntryTime) >= 0) */ ) { + // Move _nextEntryTime on, so that we can guarantee that the device will continue to + // be serviced if it doesn't update _nextEntryTime. + _nextLoopDevice->_nextEntryTime = currentMicros; + // Invoke device's _loop function _nextLoopDevice->_loop(currentMicros); + // Move to next device. _nextLoopDevice = _nextLoopDevice->_nextDevice; } @@ -157,12 +165,13 @@ void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur #endif } -// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing -// the output over a period of time. +// isBusy, when called for a device pin is always a digital output or analogue output, +// returns input feedback state of the pin, i.e. whether the pin is busy performing +// an animation or fade over a period of time. bool IODevice::isBusy(VPIN vpin) { IODevice *dev = findDevice(vpin); if (dev) - return dev->_isBusy(vpin); + return dev->_read(vpin); else return false; } @@ -248,7 +257,7 @@ int IODevice::readAnalogue(VPIN vpin) { return dev->_readAnalogue(vpin); } #ifdef DIAG_IO - //DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); + DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); #endif return false; } diff --git a/IODevice.h b/IODevice.h index 58de64b..7e45816 100644 --- a/IODevice.h +++ b/IODevice.h @@ -129,7 +129,7 @@ public: 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, uint8_t profile, uint16_t duration=0); + static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0); // isBusy returns true if the device is currently in an animation of some sort, e.g. is changing // the output over a period of time. @@ -178,7 +178,7 @@ protected: }; // Method to write an 'analogue' value (optionally implemented within device class) - virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { (void)vpin; (void)value; (void) profile; (void)duration; }; @@ -203,13 +203,6 @@ protected: return 0; }; - // _isBusy 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 _isBusy(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. @@ -220,6 +213,11 @@ protected: // Destructor virtual ~IODevice() {}; + + // Non-virtual function + void delayUntil(unsigned long futureMicrosCount) { + _nextEntryTime = futureMicrosCount; + } // Common object fields. VPIN _firstVpin; @@ -242,6 +240,7 @@ private: static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; + unsigned long _nextEntryTime; static IODevice *_firstDevice; static IODevice *_nextLoopDevice; @@ -276,7 +275,7 @@ private: // Device-specific write functions. void _write(VPIN vpin, int value) override; void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override; - bool _isBusy(VPIN vpin) override; + int _read(VPIN vpin) override; // returns the busy status of the device void _loop(unsigned long currentMicros) override; void updatePosition(uint8_t pin); void writeDevice(uint8_t pin, int value); diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index 061cd3f..f7a1a9b 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -65,7 +65,7 @@ private: public: DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) { _firstVpin = firstVpin; - _nPins = min(nPins, 3); + _nPins = nPins; _serial = &serial; addDevice(this); } @@ -159,7 +159,8 @@ protected: } } - bool _isBusy(VPIN) override { + // A read on any pin indicates whether the player is still playing. + int _read(VPIN) override { return _playing; } diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index ddc45d8..d8c9795 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -169,9 +169,9 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur s->fromPosition = s->currentPosition; } -// _isBusy returns true if the device is currently in executing an animation, +// _read returns true if the device is currently in executing an animation, // changing the output over a period of time. -bool PCA9685::_isBusy(VPIN vpin) { +int PCA9685::_read(VPIN vpin) { int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; if (s == NULL) From fa650673eb7c6cf1201c9ec7efa99f54c99383a3 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 17 Sep 2021 12:31:28 +0100 Subject: [PATCH 105/125] DFPlayer: allow volume to be set in play command. --- IO_DFPlayer.h | 80 +++++++++++++++++++++----------------- mySetup.cpp_example.txt | 85 +++++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 51 deletions(-) diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index f7a1a9b..4c133b5 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -27,17 +27,19 @@ * The driver is configured as follows: * * DFPlayer::create(firstVpin, nPins, Serialn); + * * Where firstVpin is the first vpin reserved for reading the device, * nPins is the number of pins to be allocated (max 5) - * and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial2). - * + * and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1). * * Example: * In mySetup function within mySetup.cpp: - * DFPlayer::create(3500, 5, Serial2); - * Writing a value 0-2999 to the first pin will select a numbered file from the SD card; - * Writing a value 0-30 to the second pin will set the volume of the output; - * Writing a digital value to the first pin will play or stop the file; + * DFPlayer::create(3500, 5, Serial1); + * + * Writing an analogue value 0-2999 to the first pin will select a numbered file from the SD card; + * Writing an analogue value 0-30 to the second pin will set the volume of the output; + * Writing a digital value to the first pin will play or stop the file; + * Reading a digital value from any pin will return true(1) if the player is playing, false(0) otherwise. * * From EX-RAIL, the following commands may be used: * SET(3500) -- starts playing the first file on the SD card @@ -45,6 +47,9 @@ * etc. * RESET(3500) -- stops all playing on the player * WAITFOR(3500) -- wait for the file currently being played by the player to complete + * SERVO(3500,23,0) -- plays file 23 at current volume + * SERVO(3500,23,30) -- plays file 23 at volume 30 (maximum) + * SERVO(3501,20,0) -- Sets the volume to 20 * * NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly * to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse. @@ -123,39 +128,44 @@ protected: } // WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0. - // If value is zero, it stops playing. + // Volume may be specified as second parameter to writeAnalogue. + // If value is zero, the player stops playing. // WriteAnalogue on second pin sets the output volume. - void _writeAnalogue(VPIN vpin, int value, uint8_t, uint16_t) override { + void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override { uint8_t pin = vpin - _firstVpin; - switch (pin) { - case 0: - if (value > 0) { - // Play global track - if (value > 2999) return; - #ifdef DIAG_IO - DIAG(F("DFPlayer: Play %d"), value); - #endif - sendPacket(0x03, value); - _playing = true; - } else if (value == 0){ - #ifdef DIAG_IO - DIAG(F("DFPlayer: Stop")); - #endif - sendPacket(0x16); - _playing = false; - } - break; - case 1: - // Set volume (0-30) - if (value > 30) value = 30; - else if (value < 0) value = 0; + + // Validate parameter. + volume = min(30,volume); + + if (pin == 0) { + // Play track + if (value > 0) { #ifdef DIAG_IO - DIAG(F("DFPlayer: Volume %d"), value); + DIAG(F("DFPlayer: Play %d"), value); #endif - sendPacket(0x06, value); - break; - default: - break; + sendPacket(0x03, value); // Play track + _playing = true; + if (volume > 0) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Volume %d"), volume); + #endif + sendPacket(0x06, volume); // Set volume + } + } else { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Stop")); + #endif + sendPacket(0x16); // Stop play + _playing = false; + } + } else if (pin == 1) { + // Set volume (0-30) + if (value > 30) value = 30; + else if (value < 0) value = 0; + #ifdef DIAG_IO + DIAG(F("DFPlayer: Volume %d"), value); + #endif + sendPacket(0x06, value); } } diff --git a/mySetup.cpp_example.txt b/mySetup.cpp_example.txt index 0e2be50..0efc771 100644 --- a/mySetup.cpp_example.txt +++ b/mySetup.cpp_example.txt @@ -24,8 +24,9 @@ // Examples of statically defined HAL directives (alternative to the create() call). // These have to be outside of the mySetup() function. - +//======================================================================= // The following directive defines a PCA9685 PWM Servo driver module. +//======================================================================= // The parameters are: // First Vpin=100 // Number of VPINs=16 (numbered 100-115) @@ -34,13 +35,15 @@ //PCA9685 pwmModule1(100, 16, 0x40); +//======================================================================= // The following directive defines an MCP23017 16-port I2C GPIO Extender module. +//======================================================================= // The parameters are: -// First Vpin=164 -// Number of VPINs=16 (numbered 164-179) -// I2C address of module=0x20 +// First Vpin=196 +// Number of VPINs=16 (numbered 196-211) +// I2C address of module=0x22 -//MCP23017 gpioModule2(164, 16, 0x20); +//MCP23017 gpioModule2(196, 16, 0x22); // Alternative form, which allows the INT pin of the module to request a scan @@ -48,19 +51,23 @@ // all the time, only when a change takes place. Multiple modules' INT pins // may be connected to the same Arduino pin. -//MCP23017 gpioModule2(164, 16, 0x20, 40); +//MCP23017 gpioModule2(196, 16, 0x22, 40); +//======================================================================= // The following directive defines an MCP23008 8-port I2C GPIO Extender module. +//======================================================================= // The parameters are: // First Vpin=300 // Number of VPINs=8 (numbered 300-307) // I2C address of module=0x22 -//MCP23017 gpioModule3(300, 8, 0x22); +//MCP23008 gpioModule3(300, 8, 0x22); +//======================================================================= // The following directive defines a PCF8574 8-port I2C GPIO Extender module. +//======================================================================= // The parameters are: // First Vpin=200 // Number of VPINs=8 (numbered 200-207) @@ -74,7 +81,9 @@ //PCF8574 gpioModule4(200, 8, 0x23, 40); -// The following directive defines an HCSR04 ultrasonic module. +//======================================================================= +// The following directive defines an HCSR04 ultrasonic ranging module. +//======================================================================= // The parameters are: // Vpin=2000 (only one VPIN per directive) // Number of VPINs=1 @@ -90,7 +99,10 @@ //HCSR04 sonarModule1(2000, 30, 31, 20, 25); //HCSR04 sonarModule2(2001, 30, 32, 20, 25); -// The following directive defines a single VL53L0X Time-of-Flight sensor. + +//======================================================================= +// The following directive defines a single VL53L0X Time-of-Flight range sensor. +//======================================================================= // The parameters are: // VPIN=5000 // Number of VPINs=1 @@ -103,29 +115,33 @@ // For multiple VL53L0X modules, add another parameter which is a VPIN connected to the // module's XSHUT pin. This allows the modules to be configured, at start, // with distinct I2C addresses. In this case, the address 0x29 is only used during -// initialisation to configure each device with the desired unique I2C address. -// The examples below have one module's XSHUT pin connected to Arduino pin 34, -// and the other's connected to the second pin on the first MCP23017 module (VPIN 165). -// The first module is given I2C address 0x30 and the second is 0x31. +// initialisation to configure each device in turn with the desired unique I2C address. +// The examples below have the modules' XSHUT pins connected to the first two pins of +// the first MCP23017 module (164 and 165), but Arduino pins may be used instead. +// The first module here is given I2C address 0x30 and the second is 0x31. -//VL53L0X tofModule1(5000, 1, 0x30, 200, 250, 34); +//VL53L0X tofModule1(5000, 1, 0x30, 200, 250, 164); //VL53L0X tofModule2(5001, 1, 0x31, 200, 250, 165); +//======================================================================= // The function mySetup() is invoked from CS if it exists within the build. // It is called just before mysetup.h is executed, so things set up within here can be // referenced by commands in mySetup.h. +//======================================================================= void mySetup() { - // Alternative way of creating MCP23017, which has to be within the mySetup() function + // Alternative way of creating a module driver, which has to be within the mySetup() function // The other devices can also be created in this way. The parameter lists for the // create() function are identical to the parameter lists for the declarations. - //MCP23017::create(180, 16, 0x21); + //MCP23017::create(196, 16, 0x22); + //======================================================================= // Creating a Turnout + //======================================================================= // Parameters: same as command for Servo turnouts // ID and VPIN are 100, sonar moves between positions 102 and 490 with slow profile. // Profile may be Instant, Fast, Medium, Slow or Bounce. @@ -133,7 +149,9 @@ void mySetup() { //ServoTurnout::create(100, 100, 490, 102, PCA9685::Slow); + //======================================================================= // DCC Accessory turnout + //======================================================================= // Parameters: same as command for DCC Accessory turnouts // ID=3000 // Decoder address=23 @@ -142,7 +160,9 @@ void mySetup() { //DCCTurnout::create(3000, 23, 1); + //======================================================================= // Creating a Sensor + //======================================================================= // Parameters: As for the command, // id = 164, // Vpin = 164 (configured above as pin 0 of an MCP23017) @@ -151,11 +171,44 @@ void mySetup() { //Sensor::create(164, 164, 1); + //======================================================================= // Way of creating lots of identical sensors in a range + //======================================================================= //for (int i=165; i<180; i++) // Sensor::create(i, i, 1); + + //======================================================================= + // Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module. + //======================================================================= + // Parameters: + // 10000 = first VPIN allocated. + // 10 = number of VPINs allocated. + // Serial1 = name of serial port (usually Serial1 or Serial2). + // With these parameters, up to 10 files may be played on pins 10000-10009. + // Play is started from EX-RAIL with SET(10000) for first mp3 file, SET(10001) + // for second file, etc. Play may also be initiated by writing an analogue + // value to the first pin, e.g. SERVO(10000,23,0) will play the 23rd mp3 file. + // SERVO(10000,23,30) will do the same thing, as well as setting the volume to + // 30 (maximum value). + // Play is stopped by RESET(10000) (or any other allocated VPIN). + // Volume may also be set by writing an analogue value to the second pin for the player, + // e.g. SERVO(10001,30,0) sets volume to maximum (30). + // The EX-RAIL script may check for completion of play by calling WAITFOR(pin), which will only proceed to the + // following line when the player is no longer busy. + // E.g. + // SEQUENCE(1) + // AT(164) // Wait for sensor attached to pin 164 to activate + // SET(10003) // Play fourth MP3 file + // LCD(4, "Playing") // Display message on LCD/OLED + // WAITFOR(10003) // Wait for playing to finish + // LCD(4, " ") // Clear LCD/OLED line + // FOLLOW(1) // Go back to start + + // DFPlayer::create(10000, 10, Serial1); + + } #endif From afe2ecdc1483e7e5b37d059644974fedc8ce0ec9 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 17 Sep 2021 12:44:27 +0100 Subject: [PATCH 106/125] Update IODevice.cpp Remove potentially irritating diag messages --- IODevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IODevice.cpp b/IODevice.cpp index cf90efe..a48564b 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -257,7 +257,7 @@ int IODevice::readAnalogue(VPIN vpin) { return dev->_readAnalogue(vpin); } #ifdef DIAG_IO - DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); + //DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); #endif return false; } From bda3c052656c34881985cf6ed801c8ee5710ed52 Mon Sep 17 00:00:00 2001 From: Asbelos Date: Sat, 18 Sep 2021 13:10:13 +0100 Subject: [PATCH 107/125] Auto power on tell JMR --- RMFT2.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index 9ab89f6..a117318 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -327,8 +327,10 @@ int RMFT2::locateRouteStart(int16_t _route) { void RMFT2::driveLoco(byte speed) { if (loco<=0) return; // Prevent broadcast! if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert); - if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) + if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) { DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); + Serial.println(F("")); // tell JMRI + } DCC::setThrottle(loco,speed, forward^invert); speedo=speed; } @@ -483,6 +485,8 @@ void RMFT2::loop2() { case OPCODE_POWEROFF: DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF); DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF); + DCC::setProgTrackSyncMain(false); + Serial.println(F("")); // Tell JMRI break; case OPCODE_RESUME: @@ -583,6 +587,7 @@ void RMFT2::loop2() { DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); DCCWaveform::progTrack.setPowerMode(POWERMODE::ON); DCC::setProgTrackSyncMain(true); + Serial.println(F("")); // Tell JMRI break; case OPCODE_UNJOIN: From 302b16547ef9918c49c4eeef1de4482d19710543 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 21 Sep 2021 11:02:23 +0100 Subject: [PATCH 108/125] HAL driver enhancements Performance enhancements in IODevice::loop() function. Improved error handling, device is placed off line if not responding. Improved error reporting, device shown as offline if not operational (faulty or not present). --- IODevice.cpp | 52 ++++++++++++++++++++++--------------- IODevice.h | 16 ++++++++---- IO_AnalogueInputs.h | 63 ++++++++++++++++++++++++--------------------- IO_DFPlayer.h | 33 +++++++++++++++++++----- IO_GPIOBase.h | 37 +++++++++++++------------- IO_HCSR04.h | 13 +++------- IO_PCA9685.cpp | 39 ++++++++++++++++------------ IO_VL53L0X.h | 45 ++++++++++++++++++++++---------- 8 files changed, 177 insertions(+), 121 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index a48564b..fc16bb0 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -53,9 +53,7 @@ void IODevice::begin() { MCP23017::create(180, 16, 0x21); // Call the begin() methods of each configured device in turn - unsigned long currentMicros = micros(); for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) { - dev->_nextEntryTime = currentMicros; dev->_begin(); } _initPhase = false; @@ -69,18 +67,24 @@ void IODevice::begin() { // 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; - // Check if device exists, and is due to run - if (_nextLoopDevice /* && ((long)(currentMicros-_nextLoopDevice->_nextEntryTime) >= 0) */ ) { - // Move _nextEntryTime on, so that we can guarantee that the device will continue to - // be serviced if it doesn't update _nextEntryTime. - _nextLoopDevice->_nextEntryTime = currentMicros; - // Invoke device's _loop function - _nextLoopDevice->_loop(currentMicros); - // Move to next device. - _nextLoopDevice = _nextLoopDevice->_nextDevice; - } + + IODevice *lastLoopDevice = _nextLoopDevice; // So we know when to stop... + // Loop through devices until we find one ready to be serviced. + do { + if (!_nextLoopDevice) _nextLoopDevice = _firstDevice; + if (_nextLoopDevice) { + if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED + && ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) { + // Found one ready to run, so invoke its _loop method. + _nextLoopDevice->_nextEntryTime = currentMicros; + _nextLoopDevice->_loop(currentMicros); + _nextLoopDevice = _nextLoopDevice->_nextDevice; + break; + } + // Not this one, move to next one + _nextLoopDevice = _nextLoopDevice->_nextDevice; + } + } while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all. // Report loop time if diags enabled #if defined(DIAG_LOOPTIMES) @@ -127,7 +131,8 @@ bool IODevice::hasCallback(VPIN vpin) { // Display (to diagnostics) details of the device. void IODevice::_display() { - DIAG(F("Unknown device Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1); + DIAG(F("Unknown device Vpins:%d-%d %S"), + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F("")); } // Find device associated with nominated Vpin and pass configuration values on to it. @@ -151,13 +156,18 @@ void IODevice::write(VPIN vpin, int value) { #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. Duration is the time that the -// operation is to be performed over (e.g. as an animation) in deciseconds (0-3276 sec) -void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { +// Write analogue value to virtual pin(s). If multiple devices are allocated +// the same pin then only the first one found will be used. +// +// The significance of param1 and param2 may vary from device to device. +// For servo controllers, param1 is the profile of the transition and param2 +// the duration, i.e. the time that the operation is to be animated over +// in deciseconds (0-3276 sec) +// +void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) { IODevice *dev = findDevice(vpin); if (dev) { - dev->_writeAnalogue(vpin, value, profile, duration); + dev->_writeAnalogue(vpin, value, param1, param2); return; } #ifdef DIAG_IO @@ -257,7 +267,7 @@ int IODevice::readAnalogue(VPIN vpin) { return dev->_readAnalogue(vpin); } #ifdef DIAG_IO - //DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); + DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); #endif return false; } diff --git a/IODevice.h b/IODevice.h index 7e45816..3e00c10 100644 --- a/IODevice.h +++ b/IODevice.h @@ -163,7 +163,14 @@ public: protected: - // Method to perform initialisation of the device (optionally implemented within device class) + // Constructor + IODevice(VPIN firstVpin=0, int nPins=0) { + _firstVpin = firstVpin; + _nPins = nPins; + _nextEntryTime = 0; + } + + // Method to perform initialisation of the device (optionally implemented within device class) virtual void _begin() {} // Method to configure device (optionally implemented within device class) @@ -178,8 +185,8 @@ protected: }; // Method to write an 'analogue' value (optionally implemented within device class) - virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { - (void)vpin; (void)value; (void) profile; (void)duration; + virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) { + (void)vpin; (void)value; (void) param1; (void)param2; }; // Function called to check whether callback notification is supported by this pin. @@ -275,7 +282,7 @@ private: // Device-specific write functions. void _write(VPIN vpin, int value) override; void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override; - int _read(VPIN vpin) override; // returns the busy status of the device + int _read(VPIN vpin) override; // returns the digital state or busy status of the device void _loop(unsigned long currentMicros) override; void updatePosition(uint8_t pin); void writeDevice(uint8_t pin, int value); @@ -302,7 +309,6 @@ private: 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; diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 7d24586..725f300 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -78,48 +78,52 @@ private: #endif } else { DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); + _deviceState = DEVSTATE_FAILED; } } void _loop(unsigned long currentMicros) override { - if (currentMicros - _lastMicros >= scanInterval) { - // Check that previous non-blocking write has completed, if not then wait - uint8_t status = _i2crb.wait(); - if (status == I2C_STATUS_OK) { - // If _currentPin is in the valid range, continue reading the pin values - if (_currentPin < _nPins) { - _outBuffer[0] = 0x00; // Conversion register address - uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1); // Read register - if (status == I2C_STATUS_OK) { - _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; - #ifdef IO_ANALOGUE_SLOW - DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); - #endif - } + // Check that previous non-blocking write has completed, if not then wait + uint8_t status = _i2crb.wait(); + if (status == I2C_STATUS_OK) { + // If _currentPin is in the valid range, continue reading the pin values + if (_currentPin < _nPins) { + _outBuffer[0] = 0x00; // Conversion register address + uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1); // Read register + if (status == I2C_STATUS_OK) { + _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; + #ifdef IO_ANALOGUE_SLOW + DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); + #endif } - if (status != I2C_STATUS_OK) - DIAG(F("ADS111x I2C:x%d Error:%d"), _i2cAddress, status); } - // Move to next pin - if (++_currentPin >= _nPins) _currentPin = 0; - - // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details - // of configuration register settings. - _outBuffer[0] = 0x01; // Config register address - _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n - _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off - // Write command, without waiting for completion. - I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); - - _lastMicros = currentMicros; } + if (status != I2C_STATUS_OK) { + DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); + _deviceState = DEVSTATE_FAILED; + } + // Move to next pin + if (++_currentPin >= _nPins) _currentPin = 0; + + // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details + // of configuration register settings. + _outBuffer[0] = 0x01; // Config register address + _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n + _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off + // Write command, without waiting for completion. + I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); + + delayUntil(currentMicros + scanInterval); } + int _readAnalogue(VPIN vpin) override { int pin = vpin - _firstVpin; return _value[pin]; } + void _display() override { - DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1); + DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d %S"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1, + _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } // ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms. @@ -134,7 +138,6 @@ private: uint8_t _outBuffer[3]; uint8_t _inBuffer[2]; uint8_t _currentPin; // ADC pin currently being scanned - unsigned long _lastMicros = 0; I2CRB _i2crb; }; diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index 4c133b5..5296ae0 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -66,6 +66,7 @@ private: HardwareSerial *_serial; bool _playing = false; uint8_t _inputIndex = 0; + unsigned long _commandSendTime; // Allows timeout processing public: DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) { @@ -81,21 +82,32 @@ public: protected: void _begin() override { _serial->begin(9600); - _display(); + _deviceState = DEVSTATE_INITIALISING; + + // Send a query to the device to see if it responds + sendPacket(0x42); + _commandSendTime = micros(); } - void _loop(unsigned long) override { + void _loop(unsigned long currentMicros) override { // Check for incoming data on _serial, and update busy flag accordingly. // Expected message is in the form "7F FF 06 3D xx xx xx xx xx EF" while (_serial->available()) { int c = _serial->read(); -// DIAG(F("Received: %x"), c); if (c == 0x7E) _inputIndex = 1; - else if ((c==0xFF && _inputIndex==1) || (c==0x06 && _inputIndex==2) - || (c==0x3D && _inputIndex==3) || (_inputIndex >=4 && _inputIndex <= 8)) + else if ((c==0xFF && _inputIndex==1) + || (c==0x3D && _inputIndex==3) + || (_inputIndex >=4 && _inputIndex <= 8)) _inputIndex++; - else if (c==0xEF && _inputIndex==9) { + else if (c==0x06 && _inputIndex==2) { + // Valid command prefix, so consider the device online. + _deviceState = DEVSTATE_NORMAL; + #ifdef DIAG_IO + _display(); + #endif + _inputIndex++; + } else if (c==0xEF && _inputIndex==9) { // End of play #ifdef DIAG_IO DIAG(F("DFPlayer: Finished")); @@ -104,6 +116,12 @@ protected: _inputIndex = 0; } } + // Check if the initial prompt to device has timed out. Allow 1 second + if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 1000000UL) { + DIAG(F("DFPlayer device not responding on serial port")); + _deviceState = DEVSTATE_FAILED; + } + delayUntil(currentMicros + 10000); // Only enter every 10ms } // Write with value 1 starts playing a song. The relative pin number is the file number. @@ -175,7 +193,8 @@ protected: } void _display() override { - DIAG(F("DFPlayer Configured on Vpins:%d-%d"), _firstVpin, _firstVpin+_nPins-1); + DIAG(F("DFPlayer Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } private: diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 4ca8cce..1a782b6 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -59,7 +59,6 @@ protected: 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; @@ -105,10 +104,12 @@ void GPIOBase::_begin() { _portMode = 0; // default to input mode _portPullup = -1; // default to pullup enabled _portInputState = -1; + _setupDevice(); + _deviceState = DEVSTATE_NORMAL; + } else { + DIAG(F("%S I2C:x%x Device not detected"), _deviceName, _I2CAddress); + _deviceState = DEVSTATE_FAILED; } - _setupDevice(); - _deviceState = DEVSTATE_NORMAL; - _lastLoopEntry = micros(); } // Configuration parameters for inputs: @@ -172,27 +173,25 @@ void GPIOBase::_loop(unsigned long currentMicros) { #endif } - // 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 < (unsigned long)_portTickTime) return; + // Check if interrupt configured. If not, or if it is active (pulled down), then + // initiate a scan. + if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) { + // TODO: Could suppress reads if there are no pins configured as inputs! - // 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; + // Read input + if (_deviceState == DEVSTATE_NORMAL) { + _readGpioPort(false); // Initiate non-blocking read + _deviceState= DEVSTATE_SCANNING; + } } + // Delay next entry until tick elapsed. + delayUntil(currentMicros + _portTickTime); } template void GPIOBase::_display() { - DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d"), _deviceName, _I2CAddress, - _firstVpin, _firstVpin+_nPins-1); + DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress, + _firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } template diff --git a/IO_HCSR04.h b/IO_HCSR04.h index 2df3733..9bbd2f8 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -68,8 +68,6 @@ private: uint16_t _distance; // Active=1/inactive=0 state uint8_t _value = 0; - // Time of last loop execution - unsigned long _lastExecutionTime; // Factor for calculating the distance (cm) from echo time (ms). // Based on a speed of sound of 345 metres/second. const uint16_t factor = 58; // ms/cm @@ -97,7 +95,6 @@ protected: pinMode(_trigPin, OUTPUT); pinMode(_echoPin, INPUT); ArduinoPins::fastWriteDigital(_trigPin, 0); - _lastExecutionTime = micros(); #if defined(DIAG_IO) _display(); #endif @@ -116,13 +113,9 @@ protected: // _loop function - read HC-SR04 once every 50 milliseconds. void _loop(unsigned long currentMicros) override { - if (currentMicros - _lastExecutionTime > 50000UL) { - _lastExecutionTime = currentMicros; - - read_HCSR04device(); - // Delay next loop entry until 50ms have elapsed. - //delayUntil(currentMicros + 50000UL); - } + read_HCSR04device(); + // Delay next loop entry until 50ms have elapsed. + delayUntil(currentMicros + 50000UL); } void _display() override { diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index d8c9795..009ff63 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -107,12 +107,14 @@ void PCA9685::_begin() { #if defined(DIAG_IO) _display(); #endif - } + } else + _deviceState = DEVSTATE_FAILED; } // Device-specific write function, invoked from IODevice::write(). // For this function, the configured profile is used. void PCA9685::_write(VPIN vpin, int value) { + if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value); #endif @@ -137,6 +139,7 @@ void PCA9685::_write(VPIN vpin, int value) { // 4 (Bounce) Servo 'bounces' at extremes. // void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d"), vpin, value, profile, duration); @@ -172,6 +175,7 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur // _read returns true if the device is currently in executing an animation, // changing the output over a period of time. int PCA9685::_read(VPIN vpin) { + if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; if (s == NULL) @@ -181,12 +185,10 @@ int PCA9685::_read(VPIN vpin) { } void PCA9685::_loop(unsigned long currentMicros) { - if (currentMicros - _lastRefreshTime >= refreshInterval * 1000) { - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); - } - _lastRefreshTime = currentMicros; + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); } + delayUntil(currentMicros + refreshInterval * 1000UL); } // Private function to reposition servo @@ -238,20 +240,25 @@ void PCA9685::writeDevice(uint8_t pin, int value) { 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); + uint8_t status = requestBlock.wait(); + if (status != I2C_STATUS_OK) { + _deviceState = DEVSTATE_FAILED; + DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, I2CManager.getErrorMessage(status)); + } else { + // 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 Configured on Vpins:%d-%d"), _I2CAddress, (int)_firstVpin, - (int)_firstVpin+_nPins-1); + DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (int)_firstVpin, + (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } // Internal helper function for this device diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h index 08de1aa..33c6a16 100644 --- a/IO_VL53L0X.h +++ b/IO_VL53L0X.h @@ -44,7 +44,11 @@ * all VL53L0X modules are turned off, the driver works through each module in turn by * setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address. * In this way, many VL53L0X modules can be connected to the one I2C bus, each one - * using with a distinct I2C address. + * using a distinct I2C address. + * + * WARNING: If the device's XSHUT pin is not connected, then it is very prone to noise, + * and the device may even reset when handled. If you're not using XSHUT, then it's + * best to tie it to +5V. * * The driver is configured as follows: * @@ -98,7 +102,6 @@ private: bool _value; bool _initialising = true; uint8_t _entryCount = 0; - unsigned long _lastEntryTime = 0; bool _scanInProgress = false; // Register addresses enum : uint8_t { @@ -134,8 +137,9 @@ protected: _entryCount = 3; } else { _entryCount = 0; - } - } + } + } + void _loop(unsigned long currentMicros) override { if (_initialising) { switch (_entryCount++) { @@ -156,11 +160,17 @@ protected: I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress); break; case 3: + // After two more loops, check if device has been configured. if (I2CManager.exists(_i2cAddress)) { + #ifdef DIAG_IO _display(); + #endif // Set 2.8V mode write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); + } else { + DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress); + _deviceState = DEVSTATE_FAILED; } _initialising = false; _entryCount = 0; @@ -168,14 +178,17 @@ protected: default: break; } - } else if (_lastEntryTime - currentMicros > 10000UL) { - // Service device every 10ms - _lastEntryTime = currentMicros; + } else { if (!_scanInProgress) { // Not scanning, so initiate a scan - write_reg(VL53L0X_REG_SYSRANGE_START, 0x01); - _scanInProgress = true; + uint8_t status = write_reg(VL53L0X_REG_SYSRANGE_START, 0x01); + if (status != I2C_STATUS_OK) { + DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); + _deviceState = DEVSTATE_FAILED; + _value = false; + } else + _scanInProgress = true; } else { // Scan in progress, so check for completion. @@ -198,8 +211,11 @@ protected: _scanInProgress = false; } } + // Next entry in 10 milliseconds. + delayUntil(currentMicros + 10000UL); } } + // For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level. int _readAnalogue(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -214,13 +230,16 @@ protected: return -1; } } + // For digital read, return the same value for all pins. int _read(VPIN) override { return _value; } + void _display() override { - DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm"), - _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold); + DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"), + _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } @@ -228,12 +247,12 @@ private: inline uint16_t makeuint16(byte lsb, byte msb) { return (((uint16_t)msb) << 8) | lsb; } - void write_reg(uint8_t reg, uint8_t data) { + uint8_t write_reg(uint8_t reg, uint8_t data) { // write byte to register uint8_t outBuffer[2]; outBuffer[0] = reg; outBuffer[1] = data; - I2CManager.write(_i2cAddress, outBuffer, 2); + return I2CManager.write(_i2cAddress, outBuffer, 2); } uint8_t read_reg(uint8_t reg) { // read byte from register register From e59e07b97168e0d64c0b5d5729b85f41335ee212 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 21 Sep 2021 13:43:52 +0100 Subject: [PATCH 109/125] Improved HAL diagnostics Looptime diagnostic enhanced, and duplicated diagnostic messages removed from DFPlayer class. --- IODevice.cpp | 22 +++++++++++++++------- IO_DFPlayer.h | 35 +++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index fc16bb0..e212cf5 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -89,24 +89,32 @@ void IODevice::loop() { // Report loop time if diags enabled #if defined(DIAG_LOOPTIMES) static unsigned long lastMicros = 0; - static unsigned long maxElapsed = 0; + // Measure time since loop() method started. + unsigned long halElapsed = micros() - currentMicros; + // Measure time between loop() method entries. + unsigned long elapsed = currentMicros - lastMicros; + static unsigned long maxElapsed = 0, maxHalElapsed = 0; static unsigned long lastOutputTime = 0; + static unsigned long halTotal = 0, total = 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; + if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed; + halTotal += halElapsed; + total += elapsed; + count++; } - count++; if (currentMicros - lastOutputTime > interval) { if (lastOutputTime > 0) - LCD(1,F("Loop=%lus,%lus max"), interval/count, maxElapsed); - maxElapsed = 0; - count = 0; + DIAG(F("Loop Total:%lus (%lus max) HAL:%lus (%lus max)"), + total/count, maxElapsed, halTotal/count, maxHalElapsed); + maxElapsed = maxHalElapsed = total = halTotal = count = 0; lastOutputTime = currentMicros; } - lastMicros = micros(); + lastMicros = currentMicros; #endif } diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index 5296ae0..bdf0626 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -69,12 +69,14 @@ private: unsigned long _commandSendTime; // Allows timeout processing public: - DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) { - _firstVpin = firstVpin; - _nPins = nPins; - _serial = &serial; + // Constructor + DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) : + IODevice(firstVpin, nPins), + _serial(&serial) + { addDevice(this); } + static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) { new DFPlayer(firstVpin, nPins, serial); } @@ -101,20 +103,25 @@ protected: || (_inputIndex >=4 && _inputIndex <= 8)) _inputIndex++; else if (c==0x06 && _inputIndex==2) { - // Valid command prefix, so consider the device online. - _deviceState = DEVSTATE_NORMAL; - #ifdef DIAG_IO - _display(); - #endif + // Valid message prefix, so consider the device online + if (_deviceState==DEVSTATE_INITIALISING) { + _deviceState = DEVSTATE_NORMAL; + #ifdef DIAG_IO + _display(); + #endif + } _inputIndex++; } else if (c==0xEF && _inputIndex==9) { // End of play - #ifdef DIAG_IO - DIAG(F("DFPlayer: Finished")); - #endif - _playing = false; + if (_playing) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Finished")); + #endif + _playing = false; + } _inputIndex = 0; - } + } else + _inputIndex = 0; // Unrecognised character sequence, start again! } // Check if the initial prompt to device has timed out. Allow 1 second if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 1000000UL) { From e287af83fffad1617e6d4f3205d725be9be97919 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 22 Sep 2021 10:38:11 +0100 Subject: [PATCH 110/125] DCC Turnouts: Store address/subaddress separately. Enable address 0. The range of accessory decoder addresses for the command is 0-511 in line with the DCC packet contents. The turnout command previously rejected address 0; this has been changed to the same range of addresses can be used by both commands, i.e. address 0-511 and subaddress 0-3. The linear address mapping remains so that linear address 1 is addr/subaddr 1/0; i.e. the first decoder address is not accessible by linear address. --- DCCEXParser.cpp | 8 +++++--- Turnouts.cpp | 17 ++++++++--------- Turnouts.h | 8 +++++--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index d09aa1b..b5f472f 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -736,15 +736,17 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) if (!VpinTurnout::create(p[0], p[2])) return false; } else if (params >= 3 && p[1] == HASH_KEYWORD_DCC) { - if (params==4 && p[2]>0 && p[2]<=512 && p[3]>=0 && p[3]<4) { // + // 0<=addr<=511, 0<=subadd<=3 (like command). + if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // if (!DCCTurnout::create(p[0], p[2], p[3])) return false; } else if (params==3 && p[2]>0 && p[2]<=512*4) { // , 1<=nn<=2048 + // Linearaddress 1 maps onto decoder address 1/0 (not 0/0!). if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false; } else return false; } else - if (params==3) { // legacy for DCC accessory - if (p[1]>0 && p[1]<=512 && p[2]>=0 && p[2]<4) { + if (params==3) { // legacy for DCC accessory + if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) { if (!DCCTurnout::create(p[0], p[1], p[2])) return false; } else return false; diff --git a/Turnouts.cpp b/Turnouts.cpp index 6dbc402..dcbff73 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -340,7 +340,8 @@ DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : Turnout(id, TURNOUT_DCC, false) { - _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; + _dccTurnoutData.address = address; + _dccTurnoutData.subAddress = subAdd; } // Create function @@ -351,7 +352,8 @@ if (tt->isType(TURNOUT_DCC)) { // Yes, so set parameters DCCTurnout *dt = (DCCTurnout *)tt; - dt->_dccTurnoutData.address = ((add-1) << 2) + subAdd + 1; + dt->_dccTurnoutData.address = add; + dt->_dccTurnoutData.subAddress = subAdd; // Don't touch the _closed parameter, retain the original value. return tt; } else { @@ -371,27 +373,24 @@ EEStore::advance(sizeof(dccTurnoutData)); // Create new object - DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); + DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress); return tt; } void DCCTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - !_turnoutData.closed); + _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); // Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this. StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - !_turnoutData.closed); + _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); } bool DCCTurnout::setClosedInternal(bool close) { // DCC++ Classic behaviour is that Throw writes a 1 in the packet, // and Close writes a 0. // RCN-213 specifies that Throw is 0 and Close is 1. - DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), - ((_dccTurnoutData.address-1) & 3), close ^ !rcn213Compliant); + DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant); _turnoutData.closed = close; return true; } diff --git a/Turnouts.h b/Turnouts.h index 9c14089..6a55c15 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -213,9 +213,11 @@ private: // DCCTurnoutData contains data specific to this subclass that is // written to EEPROM when the turnout is saved. struct DCCTurnoutData { - // 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. + // DCC address (Address in bits 15-2, subaddress in bits 1-0) + struct { + uint16_t address : 14; + uint8_t subAddress : 2; + }; } _dccTurnoutData; // 2 bytes // Constructor From 32eb8fe8c76c844a07d5289b20b7d7b7e6600c3b Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 22 Sep 2021 14:00:05 +0100 Subject: [PATCH 111/125] Update version.h (3.1.7draft) Added partial list of changes from 3.1.6 to 3.1.7draft. --- version.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/version.h b/version.h index 09211fa..1d76a7e 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,25 @@ #include "StringFormatter.h" -#define VERSION "3.1.6" +#define VERSION "3.1.7draft" +// 3.1.7 Major functional and non-functional changes. +// New HAL added for I/O (digital and analogue inputs and outputs, servos etc). +// Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules. +// Support for PCA9685 PWM (servo) control modules. +// Support for analogue inputs on Arduino pins and on ADS111x I2C modules. +// Support for MP3 sound playback via DFPlayer module. +// Support for HC-SR04 Ultrasonic range sensor module. +// Support for VL53L0X Laser range sensor module (Time-Of-Flight). +// Native non-blocking I2C drivers for AVR and Nano architectures (fallback +// to blocking Wire library for other platforms). +// EEPROM layout change - deletes EEPROM contents on first start following upgrade. +// New EX-RAIL automation capability. +// Turnout class revised to expand turnout capabilities, new commands added. +// Output class now allows ID > 255. +// Configuration options to globally flip polarity of DCC Accessory states when driven +// from command and command. +// Increased use of display for showing loco decoder programming information. +// ... // 3.1.6 Make output ID two bytes and guess format/size of registered outputs found in EEPROM // 3.1.5 Fix LCD corruption on power-up // 3.1.4 Refactor OLED and LCD drivers and remove unused code From e11fd188492c3a409960b68281b0b45954a7ff7d Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Wed, 22 Sep 2021 14:12:23 +0100 Subject: [PATCH 112/125] Update IO_DCCAccessory.cpp Ensure the full range of addresses including 0 are handled. --- IO_DCCAccessory.cpp | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index 5e1c8f4..e40198b 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -22,17 +22,9 @@ #include "DIAG.h" #include "defines.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. -// Linear address 1 corresponds to address 1 subaddress 0. - -#define LINEARADDRESS(addr, subaddr) (((addr-1) << 2) + subaddr + 1) -#define ADDRESS(linearaddr) (((linearaddr-1) >> 2) + 1) -#define SUBADDRESS(linearaddr) ((linearaddr-1) % 4) +#define PACKEDADDRESS(addr, subaddr) (((addr) << 2) + (subaddr)) +#define ADDRESS(packedaddr) ((packedaddr) >> 2) +#define SUBADDRESS(packedaddr) ((packedaddr) % 4) void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress); @@ -42,7 +34,7 @@ void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSu DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { _firstVpin = vpin; _nPins = nPins; - _packedAddress = LINEARADDRESS(DCCAddress, DCCSubaddress); + _packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress); addDevice(this); } @@ -66,8 +58,7 @@ void DCCAccessoryDecoder::_write(VPIN id, int state) { void DCCAccessoryDecoder::_display() { int endAddress = _packedAddress + _nPins - 1; - DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Linear Address:%d-%d (%d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, - _packedAddress, _packedAddress+_nPins-1, + DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress)); } From ffc5d91561a6e3cc359c437e2933775b43b7160d Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 23 Sep 2021 08:59:43 +0100 Subject: [PATCH 113/125] Update version.h --- version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.h b/version.h index 1d76a7e..bcf78f6 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,7 @@ #include "StringFormatter.h" -#define VERSION "3.1.7draft" +#define VERSION "3.1.7 draft" // 3.1.7 Major functional and non-functional changes. // New HAL added for I/O (digital and analogue inputs and outputs, servos etc). // Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules. From 9fc805831dcc01873652990879414560f281979a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 23 Sep 2021 10:54:27 +0100 Subject: [PATCH 114/125] HAL: Minor optimisations Remove virtual method hasCallback(). Optimise findDevice() method (used by read, write etc.). Simplify Sensor handling with regard to IO Devices that support callbacks. --- IODevice.cpp | 8 +++--- IODevice.h | 12 +++------ IO_GPIOBase.h | 11 +++------ Sensors.cpp | 67 ++++++++++++++------------------------------------- Sensors.h | 11 ++------- 5 files changed, 32 insertions(+), 77 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index e212cf5..5fd27ff 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -134,7 +134,7 @@ bool IODevice::exists(VPIN vpin) { bool IODevice::hasCallback(VPIN vpin) { IODevice *dev = findDevice(vpin); if (!dev) return false; - return dev->_hasCallback(vpin); + return dev->_hasCallback; } // Display (to diagnostics) details of the device. @@ -221,10 +221,12 @@ void IODevice::addDevice(IODevice *newDevice) { newDevice->_begin(); } -// Private helper function to locate a device by VPIN. Returns NULL if not found +// Private helper function to locate a device by VPIN. Returns NULL if not found. +// This is performance-critical, so minimises the calculation and function calls necessary. IODevice *IODevice::findDevice(VPIN vpin) { for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { - if (dev->owns(vpin)) + VPIN firstVpin = dev->_firstVpin; + if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins) return dev; } return NULL; diff --git a/IODevice.h b/IODevice.h index 3e00c10..fe1d3e6 100644 --- a/IODevice.h +++ b/IODevice.h @@ -189,15 +189,6 @@ protected: (void)vpin; (void)value; (void) param1; (void)param2; }; - // 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 digital pin state (optionally implemented within device class) virtual int _read(VPIN vpin) { (void)vpin; @@ -230,6 +221,9 @@ protected: VPIN _firstVpin; int _nPins; + // Flag whether the device supports callbacks. + bool _hasCallback = false; + // Pin number of interrupt pin for GPIO extender devices. The extender module will pull this // pin low if an input changes state. int16_t _gpioInterruptPin = -1; diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 1a782b6..9b1bee5 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -45,10 +45,6 @@ protected: int _read(VPIN vpin) override; void _display() override; void _loop(unsigned long currentMicros) override; - bool _hasCallback(VPIN vpin) { - (void)vpin; // suppress compiler warning - return true; // Enable callback if caller wants to use it. - } // Data fields uint8_t _I2CAddress; @@ -79,12 +75,13 @@ protected: // Constructor template -GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) { +GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) : + IODevice(firstVpin, nPins) +{ _deviceName = deviceName; - _firstVpin = firstVpin; - _nPins = nPins; _I2CAddress = I2CAddress; _gpioInterruptPin = interruptPin; + _hasCallback = true; // Add device to list of devices. addDevice(this); } diff --git a/Sensors.cpp b/Sensors.cpp index 4d3283a..d6d3d81 100644 --- a/Sensors.cpp +++ b/Sensors.cpp @@ -103,12 +103,6 @@ void Sensor::checkAll(Print *stream){ // 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; } } @@ -117,12 +111,6 @@ void Sensor::checkAll(Print *stream){ 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). @@ -130,10 +118,8 @@ void Sensor::checkAll(Print *stream){ // 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); + if (readingSensor->pollingRequired && pin != VPIN_NONE) + readingSensor->inputState = IODevice::read(pin); // Check if changed since last time, and process changes. if (readingSensor->inputState == readingSensor->active) { @@ -156,22 +142,12 @@ void Sensor::checkAll(Print *stream){ // 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. + // Currently process max of 16 sensors per entry. + // Performance measurements taken during development indicate that, with 128 sensors configured + // on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices + // within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond. sensorCount++; -#ifdef USE_NOTIFY - if (pollSignalPhase) { -#endif - if (sensorCount >= 16) pause = true; -#ifdef USE_NOTIFY - } else - { - if (sensorCount >= 16) pause = true; - } -#endif + if (sensorCount >= 16) pause = true; } } // Sensor::checkAll @@ -223,23 +199,18 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){ 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 + if (pin == VPIN_NONE) + tt->pollingRequired = false; + #ifdef USE_NOTIFY + else if (IODevice::hasCallback(pin)) + tt->pollingRequired = false; + #endif + else + tt->pollingRequired = true; + + // Add to the start of the list tt->nextSensor = firstSensor; firstSensor = tt; -#endif tt->data.snum = snum; tt->data.pin = pin; @@ -248,9 +219,8 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){ tt->inputState = 0; tt->latchDelay = minReadCount; - int params[] = {pullUp}; if (pin != VPIN_NONE) - IODevice::configure(pin, IODevice::CONFIGURE_INPUT, 1, params); + IODevice::configureInput(pin, pullUp); // 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 @@ -343,6 +313,5 @@ unsigned long Sensor::lastReadCycle=0; #ifdef USE_NOTIFY Sensor *Sensor::firstPollSensor = NULL; Sensor *Sensor::lastSensor = NULL; -bool Sensor::pollSignalPhase = false; bool Sensor::inputChangeCallbackRegistered = false; #endif \ No newline at end of file diff --git a/Sensors.h b/Sensors.h index 60e414f..7547784 100644 --- a/Sensors.h +++ b/Sensors.h @@ -45,14 +45,6 @@ struct SensorData { 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; @@ -74,6 +66,7 @@ public: // Constructor Sensor(); Sensor *nextSensor; + void setState(int state); static void load(); static void store(); @@ -88,9 +81,9 @@ public: 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 + bool pollingRequired = true; #ifdef USE_NOTIFY - static bool pollSignalPhase; static void inputChangeCallback(VPIN vpin, int state); static bool inputChangeCallbackRegistered; #endif From bfc2b75eb51b9c26faf1f2a16a106cf10652171d Mon Sep 17 00:00:00 2001 From: Asbelos Date: Fri, 1 Oct 2021 11:01:32 +0100 Subject: [PATCH 115/125] SERIAL2 Typo --- RMFTMacros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RMFTMacros.h b/RMFTMacros.h index 9fd1c22..01bfb46 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -149,7 +149,7 @@ const int StringMacroTracker1=__COUNTER__; #define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break; #define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break; #define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break; -#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(L&Serial2,F(msg));break; +#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break; #define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break; #define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" From 7aed7de6cd4305657032788385cddb2fb4f406ce Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sun, 3 Oct 2021 12:24:14 +0100 Subject: [PATCH 116/125] Change default LCD address. LCD Backpack Address in example config.h changed to 0x27 (to match the most commonly available PCF8574 device). --- config.example.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.example.h b/config.example.h index 9b1855f..5f26ca7 100644 --- a/config.example.h +++ b/config.example.h @@ -114,11 +114,11 @@ 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 Hitachi HD44780 -// controller and a PCF8574 based I2C 'backpack'. +// controller and a commonly available 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 +// define LCD_DRIVER for I2C address 0x27, 16 cols, 2 rows +// #define LCD_DRIVER 0x27,16,2 //OR define OLED_DRIVER width,height in pixels (address auto detected) // 128x32 or 128x64 I2C SSD1306-based devices are supported. From 6dde81127930388cb3892b39243aea86fd28a6dd Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 5 Oct 2021 12:48:45 +0100 Subject: [PATCH 117/125] Optimise HAL drivers for TOF sensor and Analogue Inputs Increased use of async I2C in HAL drivers to reduce overall loop time overhead. --- IO_AnalogueInputs.h | 65 ++++++++++----- IO_VL53L0X.h | 191 +++++++++++++++++++++++++------------------- 2 files changed, 154 insertions(+), 102 deletions(-) diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 725f300..85c224a 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -63,7 +63,9 @@ public: _firstVpin = firstVpin; _nPins = min(nPins,4); _i2cAddress = i2cAddress; - _currentPin = _nPins; // Suppress read on first loop entry. + _currentPin = 0; + for (int8_t i=0; i<_nPins; i++) + _value[i] = -1; addDevice(this); } static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) { @@ -73,6 +75,7 @@ private: void _begin() { // Initialise ADS device if (I2CManager.exists(_i2cAddress)) { + _nextState = STATE_STARTSCAN; #ifdef DIAG_IO _display(); #endif @@ -84,36 +87,48 @@ private: void _loop(unsigned long currentMicros) override { // Check that previous non-blocking write has completed, if not then wait - uint8_t status = _i2crb.wait(); + uint8_t status = _i2crb.status; + if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything. if (status == I2C_STATUS_OK) { - // If _currentPin is in the valid range, continue reading the pin values - if (_currentPin < _nPins) { - _outBuffer[0] = 0x00; // Conversion register address - uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1); // Read register - if (status == I2C_STATUS_OK) { + switch (_nextState) { + case STATE_STARTSCAN: + // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details + // of configuration register settings. + _outBuffer[0] = 0x01; // Config register address + _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n + _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off + // Write command, without waiting for completion. + I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); + + delayUntil(currentMicros + scanInterval); + _nextState = STATE_STARTREAD; + break; + + case STATE_STARTREAD: + // Reading the pin value + _outBuffer[0] = 0x00; // Conversion register address + I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register + _nextState = STATE_GETVALUE; + break; + + case STATE_GETVALUE: _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; #ifdef IO_ANALOGUE_SLOW DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); #endif - } + + // Move to next pin + if (++_currentPin >= _nPins) _currentPin = 0; + _nextState = STATE_STARTSCAN; + break; + + default: + break; } - } - if (status != I2C_STATUS_OK) { + } else { // error status DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); _deviceState = DEVSTATE_FAILED; } - // Move to next pin - if (++_currentPin >= _nPins) _currentPin = 0; - - // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details - // of configuration register settings. - _outBuffer[0] = 0x01; // Config register address - _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n - _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off - // Write command, without waiting for completion. - I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); - - delayUntil(currentMicros + scanInterval); } int _readAnalogue(VPIN vpin) override { @@ -133,12 +148,18 @@ private: #else const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds. #endif + enum : uint8_t { + STATE_STARTSCAN, + STATE_STARTREAD, + STATE_GETVALUE, + }; uint16_t _value[4]; uint8_t _i2cAddress; uint8_t _outBuffer[3]; uint8_t _inBuffer[2]; uint8_t _currentPin; // ADC pin currently being scanned I2CRB _i2crb; + uint8_t _nextState; }; #endif // io_analogueinputs_h \ No newline at end of file diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h index 33c6a16..bcdbc49 100644 --- a/IO_VL53L0X.h +++ b/IO_VL53L0X.h @@ -28,11 +28,12 @@ * The operation shown here doesn't include any calibration, so is probably not as accurate * as using the full driver, but it's probably accurate enough for the purpose. * - * The device driver allocates up to 3 vpins to the device. A digital read on any of the pins + * The device driver allocates up to 3 vpins to the device. A digital read on the first pin * will return a value that indicates whether the object is within the threshold range (1) * or not (0). An analogue read on the first pin returns the last measured distance (in mm), * the second pin returns the signal strength, and the third pin returns detected - * ambient light level. + * ambient light level. By default the device takes around 60ms to complete a ranging + * operation, so we do a 100ms cycle (10 samples per second). * * The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module, * you can use this address. However, the address can be modified by software. If @@ -100,9 +101,22 @@ private: uint16_t _offThreshold; VPIN _xshutPin; bool _value; - bool _initialising = true; - uint8_t _entryCount = 0; - bool _scanInProgress = false; + uint8_t _nextState = 0; + I2CRB _rb; + uint8_t _inBuffer[12]; + uint8_t _outBuffer[2]; + // State machine states. + enum : uint8_t { + STATE_INIT = 0, + STATE_CONFIGUREADDRESS = 1, + STATE_SKIP = 2, + STATE_CONFIGUREDEVICE = 3, + STATE_INITIATESCAN = 4, + STATE_CHECKSTATUS = 5, + STATE_GETRESULTS = 6, + STATE_DECODERESULTS = 7, + }; + // Register addresses enum : uint8_t { VL53L0X_REG_SYSRANGE_START=0x00, @@ -130,89 +144,107 @@ public: protected: void _begin() override { - _initialising = true; - // Check if device is already responding on the nominated address. - if (I2CManager.exists(_i2cAddress)) { - // Yes, it's already on this address, so skip the address initialisation. - _entryCount = 3; - } else { - _entryCount = 0; + if (_xshutPin == VPIN_NONE) { + // Check if device is already responding on the nominated address. + if (I2CManager.exists(_i2cAddress)) { + // Yes, it's already on this address, so skip the address initialisation. + _nextState = STATE_CONFIGUREDEVICE; + } else { + _nextState = STATE_INIT; + } } } void _loop(unsigned long currentMicros) override { - if (_initialising) { - switch (_entryCount++) { - case 0: - // On first entry to loop, reset this module by pulling XSHUT low. All modules - // will be reset in turn. - if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0); - break; - case 1: - // On second entry, set XSHUT pin high to allow the module to restart. - // On the module, there is a diode in series with the XSHUT pin to - // protect the low-voltage pin against +5V. - if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1); - // Allow the module time to restart - delay(10); - // Then write the desired I2C address to the device, while this is the only - // module responding to the default address. - I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress); - break; - case 3: - // After two more loops, check if device has been configured. - if (I2CManager.exists(_i2cAddress)) { - #ifdef DIAG_IO - _display(); - #endif - // Set 2.8V mode - write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, - read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); - } else { - DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress); - _deviceState = DEVSTATE_FAILED; - } - _initialising = false; - _entryCount = 0; - break; - default: - break; - } - } else { - - if (!_scanInProgress) { + uint8_t status; + switch (_nextState) { + case STATE_INIT: + // On first entry to loop, reset this module by pulling XSHUT low. All modules + // will be reset in turn. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0); + _nextState = STATE_CONFIGUREADDRESS; + break; + case STATE_CONFIGUREADDRESS: + // On second entry, set XSHUT pin high to allow the module to restart. + // On the module, there is a diode in series with the XSHUT pin to + // protect the low-voltage pin against +5V. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1); + // Allow the module time to restart + delay(10); + // Then write the desired I2C address to the device, while this is the only + // module responding to the default address. + I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress); + _nextState = STATE_SKIP; + break; + case STATE_SKIP: + // Do nothing on the third entry. + _nextState = STATE_CONFIGUREDEVICE; + break; + case STATE_CONFIGUREDEVICE: + // On next entry, check if device address has been set. + if (I2CManager.exists(_i2cAddress)) { + #ifdef DIAG_IO + _display(); + #endif + // Set 2.8V mode + write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, + read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); + } else { + DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress); + _deviceState = DEVSTATE_FAILED; + } + _nextState = STATE_INITIATESCAN; + break; + case STATE_INITIATESCAN: // Not scanning, so initiate a scan - uint8_t status = write_reg(VL53L0X_REG_SYSRANGE_START, 0x01); + _outBuffer[0] = VL53L0X_REG_SYSRANGE_START; + _outBuffer[1] = 0x01; + I2CManager.write(_i2cAddress, _outBuffer, 2, &_rb); + _nextState = STATE_CHECKSTATUS; + break; + case STATE_CHECKSTATUS: + status = _rb.status; + if (status == I2C_STATUS_PENDING) return; // try next time if (status != I2C_STATUS_OK) { DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); _deviceState = DEVSTATE_FAILED; _value = false; } else - _scanInProgress = true; - - } else { - // Scan in progress, so check for completion. - uint8_t status = read_reg(VL53L0X_REG_RESULT_RANGE_STATUS); - if (status & 1) { - // Completed. Retrieve data - uint8_t inBuffer[12]; - read_registers(VL53L0X_REG_RESULT_RANGE_STATUS, inBuffer, 12); - uint8_t deviceRangeStatus = ((inBuffer[0] & 0x78) >> 3); + _nextState = 2; + delayUntil(currentMicros + 95000); // wait for 95 ms before checking. + _nextState = STATE_GETRESULTS; + break; + case STATE_GETRESULTS: + // Ranging completed. Request results + _outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS; + I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb); + _nextState = 3; + delayUntil(currentMicros + 5000); // Allow 5ms to get data + _nextState = STATE_DECODERESULTS; + break; + case STATE_DECODERESULTS: + // If I2C write still busy, return. + status = _rb.status; + if (status == I2C_STATUS_PENDING) return; // try again next time + if (status == I2C_STATUS_OK) { + if (!(_inBuffer[0] & 1)) return; // device still busy + uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3); if (deviceRangeStatus == 0x0b) { // Range status OK, so use data - _ambient = makeuint16(inBuffer[7], inBuffer[6]); - _signal = makeuint16(inBuffer[9], inBuffer[8]); - _distance = makeuint16(inBuffer[11], inBuffer[10]); + _ambient = makeuint16(_inBuffer[7], _inBuffer[6]); + _signal = makeuint16(_inBuffer[9], _inBuffer[8]); + _distance = makeuint16(_inBuffer[11], _inBuffer[10]); if (_distance <= _onThreshold) _value = true; else if (_distance > _offThreshold) _value = false; } - _scanInProgress = false; } - } - // Next entry in 10 milliseconds. - delayUntil(currentMicros + 10000UL); + // Completed. Restart scan on next loop entry. + _nextState = STATE_INITIATESCAN; + break; + default: + break; } } @@ -231,9 +263,12 @@ protected: } } - // For digital read, return the same value for all pins. - int _read(VPIN) override { - return _value; + // For digital read, return zero for all but first pin. + int _read(VPIN vpin) override { + if (vpin == _firstVpin) + return _value; + else + return 0; } void _display() override { @@ -255,13 +290,9 @@ private: return I2CManager.write(_i2cAddress, outBuffer, 2); } uint8_t read_reg(uint8_t reg) { - // read byte from register register - uint8_t inBuffer[1]; - I2CManager.read(_i2cAddress, inBuffer, 1, ®, 1); - return inBuffer[0]; - } - void read_registers(uint8_t reg, uint8_t buffer[], uint8_t size) { - I2CManager.read(_i2cAddress, buffer, size, ®, 1); + // read byte from register and return value + I2CManager.read(_i2cAddress, _inBuffer, 1, ®, 1); + return _inBuffer[0]; } }; From 80472a76dc2e83c9c973ee5fe7924fea335c9f4b Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 8 Oct 2021 13:28:43 +0100 Subject: [PATCH 118/125] I2CManager - support slower I2C speeds. Previously the driver allowed speeds down to 32kHz but lower speeds were not implemented correctly. --- I2CManager.cpp | 6 +++--- I2CManager_AVR.h | 13 ++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/I2CManager.cpp b/I2CManager.cpp index 94c4baf..a3ea611 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -179,7 +179,7 @@ I2CManagerClass I2CManager = I2CManagerClass(); /*************************************************************************** * Block waiting for request block to complete, and return completion status. * Since such a loop could potentially last for ever if the RB status doesn't - * change, we set a high limit (0.1sec, 100ms) on the wait time and, if it + * change, we set a high limit (1sec, 1000ms) on the wait time and, if it * hasn't changed by that time we assume it's not going to, and just return * a timeout status. This means that CS will not lock up. ***************************************************************************/ @@ -187,8 +187,8 @@ uint8_t I2CRB::wait() { unsigned long waitStart = millis(); do { I2CManager.loop(); - // Rather than looping indefinitely, let's set a very high timeout (100ms). - if ((millis() - waitStart) > 100UL) { + // Rather than looping indefinitely, let's set a very high timeout (1s). + if ((millis() - waitStart) > 1000UL) { DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this); status = I2C_STATUS_TIMEOUT; // Note that, although the timeout is posted, the request may yet complete. diff --git a/I2CManager_AVR.h b/I2CManager_AVR.h index 310afa2..6492e00 100644 --- a/I2CManager_AVR.h +++ b/I2CManager_AVR.h @@ -62,7 +62,18 @@ * Set I2C clock speed register. ***************************************************************************/ void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) { - TWBR = ((F_CPU / i2cClockSpeed) - 16) / 2; + unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2; + for (uint8_t preScaler = 0; preScaler<=3; preScaler++) { + if (temp <= 255) { + TWBR = temp; + TWSR = (TWSR & 0xfc) | preScaler; + return; + } else + temp /= 4; + } + // Set slowest speed ~= 500 bits/sec + TWBR = 255; + TWSR |= 0x03; } /*************************************************************************** From 9097a62f42c9345c42f8defe7dd80ccee0939c27 Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 8 Oct 2021 13:30:23 +0100 Subject: [PATCH 119/125] Add new and commands. Alias for existing command added as (since not all analogue outputs are servos). Also, added to display the value of an analogue input pin. --- DCCEXParser.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index b5f472f..ed1f684 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -71,6 +71,8 @@ const int16_t HASH_KEYWORD_T=84; const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_HAL = 10853; const int16_t HASH_KEYWORD_SHOW = -21309; +const int16_t HASH_KEYWORD_ANIN = -10424; +const int16_t HASH_KEYWORD_ANOUT = -26399; #ifdef HAS_ENOUGH_MEMORY const int16_t HASH_KEYWORD_WIFI = -5583; const int16_t HASH_KEYWORD_ETHERNET = -30767; @@ -879,9 +881,14 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) return true; case HASH_KEYWORD_SERVO: // + case HASH_KEYWORD_ANOUT: // IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); break; + case HASH_KEYWORD_ANIN: // Display analogue input value + DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1])); + break; + #if !defined(IO_MINIMAL_HAL) case HASH_KEYWORD_HAL: if (p[1] == HASH_KEYWORD_SHOW) From 4f16a4ca06a8bd8c59ecb675a28470b5243b681b Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Fri, 15 Oct 2021 18:34:47 +0100 Subject: [PATCH 120/125] Fix GPIO Expander initial output state. Previously, pullups were enabled on GPIO Expander digital pins by default, even if the pin was only ever used as an output. This could lead to a spurious HIGH state being seen by external equipment before the output is initialised to LOW. To avoid this, the pin pullup is now not enabled until a configure or read operation is issued for the pin. --- IODevice.h | 4 ++-- IO_GPIOBase.h | 25 +++++++++++++++++++------ IO_MCP23008.h | 14 +++++++++----- IO_MCP23017.h | 15 ++++++++++----- IO_PCF8574.h | 20 +++++++++++++++++++- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/IODevice.h b/IODevice.h index fe1d3e6..5d6abc3 100644 --- a/IODevice.h +++ b/IODevice.h @@ -120,7 +120,7 @@ public: } // User-friendly function for configuring a servo pin. - inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile, uint16_t duration, uint8_t initialState=0) { + inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile=0, uint16_t duration=0, uint8_t initialState=0) { int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState}; return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params); } @@ -203,7 +203,7 @@ protected: // Method to perform updates on an ongoing basis (optionally implemented within device class) virtual void _loop(unsigned long currentMicros) { - (void)currentMicros; // Suppress compiler warning. + delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls. }; // Method for displaying info on DIAG output (optionally implemented within device class) diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 9b1bee5..4269a70 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -53,6 +53,7 @@ protected: T _portOutputState; T _portMode; T _portPullup; + T _portInUse; // Interval between refreshes of each input port static const int _portTickTime = 4000; @@ -100,7 +101,8 @@ void GPIOBase::_begin() { #endif _portMode = 0; // default to input mode _portPullup = -1; // default to pullup enabled - _portInputState = -1; + _portInputState = -1; + _portInUse = 0; _setupDevice(); _deviceState = DEVSTATE_NORMAL; } else { @@ -126,11 +128,15 @@ bool GPIOBase::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCoun _portPullup |= mask; else _portPullup &= ~mask; + // Mark that port has been accessed + _portInUse |= mask; + // Set input mode + _portMode &= ~mask; // Call subclass's virtual function to write to device + _writePortModes(); _writePullups(); - // Re-read port following change - _readGpioPort(); + // Port change will be notified on next loop entry. return true; } @@ -149,6 +155,8 @@ void GPIOBase::_loop(unsigned long currentMicros) { I2CManager.getErrorMessage(status)); } _processCompletion(status); + // Set unused pin and write mode pin value to 1 + _portInputState |= ~_portInUse | _portMode; // Scan for changes in input states and invoke callback (if present) T differences = lastPortStates ^ _portInputState; @@ -199,8 +207,9 @@ void GPIOBase::_write(VPIN vpin, int value) { DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value); #endif - // Set port mode output + // Set port mode output if currently not output mode if (!(_portMode & mask)) { + _portInUse |= mask; _portMode |= mask; _writePortModes(); } @@ -220,12 +229,16 @@ int GPIOBase::_read(VPIN vpin) { int pin = vpin - _firstVpin; T mask = 1 << pin; - // Set port mode to input - if (_portMode & mask) { + // Set port mode to input if currently output or first use + if ((_portMode | ~_portInUse) & mask) { _portMode &= ~mask; + _portInUse |= mask; + _writePullups(); _writePortModes(); // Port won't have been read yet, so read it now. _readGpioPort(); + // Set unused pin and write mode pin value to 1 + _portInputState |= ~_portInUse | _portMode; #ifdef DIAG_IO DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState); #endif diff --git a/IO_MCP23008.h b/IO_MCP23008.h index 3557b49..18ff12f 100644 --- a/IO_MCP23008.h +++ b/IO_MCP23008.h @@ -42,14 +42,18 @@ private: I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState); } void _writePullups() override { - I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup); + // Set pullups only for in-use pins. This prevents pullup being set for a pin that + // is intended for use as an output but hasn't been written to yet. + I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse); } 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) + // Write 0 to IODIR for in-use pins that are outputs, 1 for others. + uint8_t temp = ~(_portMode & _portInUse); + I2CManager.write(_I2CAddress, 2, REG_IODIR, temp); + // Enable interrupt-on-change for in-use pins that are inputs (_portMode=0) + temp = ~_portMode & _portInUse; I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00); - I2CManager.write(_I2CAddress, 2, REG_GPINTEN, ~_portMode); + I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp); } void _readGpioPort(bool immediate) override { if (immediate) { diff --git a/IO_MCP23017.h b/IO_MCP23017.h index d7c27ce..930b051 100644 --- a/IO_MCP23017.h +++ b/IO_MCP23017.h @@ -48,14 +48,19 @@ private: I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8); } void _writePullups() override { - I2CManager.write(_I2CAddress, 3, REG_GPPUA, _portPullup, _portPullup>>8); + // Set pullups only for in-use pins. This prevents pullup being set for a pin that + // is intended for use as an output but hasn't been written to yet. + uint16_t temp = _portPullup & _portInUse; + I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>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) + // Write 0 to IODIR for in-use pins that are outputs, 1 for others. + uint16_t temp = ~(_portMode & _portInUse); + I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8); + // Enable interrupt for in-use pins which are inputs (_portMode=0) + temp = ~_portMode & _portInUse; I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00); - I2CManager.write(_I2CAddress, 3, REG_GPINTENA, ~_portMode, (~_portMode)>>8); + I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8); } void _readGpioPort(bool immediate) override { if (immediate) { diff --git a/IO_PCF8574.h b/IO_PCF8574.h index 2a8d363..dea5e2c 100644 --- a/IO_PCF8574.h +++ b/IO_PCF8574.h @@ -17,6 +17,24 @@ * along with CommandStation. If not, see . */ +/* + * The PCF8574 is a simple device; it only has one register. The device + * input/output mode and pullup are configured through this, and the + * output state is written and the input state read through it too. + * + * This is accomplished by having a weak resistor in series with the output, + * and a read-back of the other end of the resistor. As an output, the + * pin state is set to 1 or 0, and the output voltage goes to +5V or 0V + * (through the weak resistor). + * + * In order to use the pin as an input, the output is written as + * a '1' in order to pull up the resistor. Therefore the input will be + * 1 unless the pin is pulled down externally, in which case it will be 0. + * + * As a consequence of this approach, it is not possible to use the device for + * inputs without pullups. + */ + #ifndef IO_PCF8574_H #define IO_PCF8574_H @@ -70,7 +88,7 @@ private: if (status == I2C_STATUS_OK) _portInputState = ((uint16_t)inputBuffer[0]) & 0xff; else - _portInputState = 0xff; + _portInputState = 0xff; } // Set up device ports From b7bcd133470581d7bbdf491b372d14d35edeb32a Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Thu, 21 Oct 2021 16:43:42 +0100 Subject: [PATCH 121/125] Fix Arduino pin pullup initial state. If an Arduino pin was used as an input (e.g. by EXRAIL) without previously configuring it, the default pullup wouldn't be set up. Now, on first call to the _read() method the pullup will be enabled. --- IODevice.cpp | 14 ++++++++++---- IODevice.h | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 5fd27ff..3456af5 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -327,12 +327,14 @@ IONotifyCallback *IONotifyCallback::first = 0; ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) { _firstVpin = firstVpin; _nPins = nPins; - uint8_t arrayLen = (_nPins+7)/8; - _pinPullups = (uint8_t *)calloc(2, arrayLen); + int arrayLen = (_nPins+7)/8; + _pinPullups = (uint8_t *)calloc(3, arrayLen); _pinModes = (&_pinPullups[0]) + arrayLen; + _pinInUse = (&_pinPullups[0]) + 2*arrayLen; for (int i=0; i Date: Thu, 21 Oct 2021 22:44:25 +0100 Subject: [PATCH 122/125] Fixup EXRAIL Read Loco issues --- RMFT2.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/RMFT2.cpp b/RMFT2.cpp index a117318..a6dd935 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -44,6 +44,7 @@ const int16_t HASH_KEYWORD_ROUTES=-3702; // The thrrads exist in a ring, each time through loop() the next thread in the ring is serviced. // Statics +const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck bool RMFT2::diag=false; // RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring. @@ -595,14 +596,20 @@ void RMFT2::loop2() { break; case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes + progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet DCC::getLocoId(readLocoCallback); break; case OPCODE_READ_LOCO2: - if (progtrackLocoId<0) { + if (progtrackLocoId==LOCO_ID_WAITING) { delayMe(100); return; // still waiting for callback } + if (progtrackLocoId<0) { + kill(F("No Loco Found"),progtrackLocoId); + return; // still waiting for callback + } + loco=progtrackLocoId; speedo=0; forward=true; From 055bc7bfe2ef2a31ffe794ef6b6da22a664730c1 Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Sun, 31 Oct 2021 22:20:59 +0100 Subject: [PATCH 123/125] unknown locos should have speed forward --- DCC.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DCC.cpp b/DCC.cpp index 70f30e2..f42ddf7 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -141,7 +141,7 @@ uint8_t DCC::getThrottleSpeed(int cab) { bool DCC::getThrottleDirection(int cab) { int reg=lookupSpeedTable(cab); - if (reg<0) return false ; + if (reg<0) return true; return (speedTable[reg].speedCode & 0x80) !=0; } From e3d771a24da09bb9c14d4b9b94c80f5c48bf6790 Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Sat, 6 Nov 2021 21:57:06 +0100 Subject: [PATCH 124/125] set default pullup in EXRAIL begin code --- RMFT2.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RMFT2.cpp b/RMFT2.cpp index a6dd935..a4f1758 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -66,6 +66,14 @@ byte RMFT2::flags[MAX_FLAGS]; byte opcode=GET_OPCODE; if (opcode==OPCODE_ENDEXRAIL) break; + switch (opcode) { + case OPCODE_AT: + case OPCODE_AFTER: + case OPCODE_IF: + case OPCODE_IFNOT: + IODevice::configureInput((VPIN)GET_OPERAND(0),true); + } + if (opcode==OPCODE_SIGNAL) { VPIN red=GET_OPERAND(0); VPIN amber=GET_OPERAND(1); From a16f6c8749093a9fab16faed6817e24cfab64456 Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Sat, 6 Nov 2021 22:12:32 +0100 Subject: [PATCH 125/125] configure pins correct even when HAL not used --- IODevice.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/IODevice.cpp b/IODevice.cpp index 3456af5..1f9f53f 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -160,7 +160,7 @@ void IODevice::write(VPIN vpin, int value) { return; } #ifdef DIAG_IO - //DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin); + DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin); #endif } @@ -179,7 +179,7 @@ void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t para return; } #ifdef DIAG_IO - //DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin); + DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin); #endif } @@ -265,7 +265,7 @@ int IODevice::read(VPIN vpin) { return dev->_read(vpin); } #ifdef DIAG_IO - //DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin); + DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin); #endif return false; } @@ -288,7 +288,17 @@ int IODevice::readAnalogue(VPIN vpin) { // 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, ConfigTypeEnum, int, int []) { return true; } +bool IODevice::configure(VPIN pin, ConfigTypeEnum, int, int p[]) { + #ifdef DIAG_IO + DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, p[0]); + #endif + if (p[0]) { + pinMode(pin, INPUT_PULLUP); + } else { + pinMode(pin, INPUT); + } + return true; +} void IODevice::write(VPIN vpin, int value) { digitalWrite(vpin, value); pinMode(vpin, OUTPUT); @@ -297,7 +307,6 @@ void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {} bool IODevice::isBusy(VPIN) { return false; } bool IODevice::hasCallback(VPIN) { return false; } int IODevice::read(VPIN vpin) { - pinMode(vpin, INPUT_PULLUP); return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1) } int IODevice::readAnalogue(VPIN vpin) { @@ -434,7 +443,7 @@ int ArduinoPins::_readAnalogue(VPIN vpin) { interrupts(); #ifdef DIAG_IO - //DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value); + DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value); #endif return value; }