diff --git a/.gitignore b/.gitignore index c8e40c2..6237359 100644 --- a/.gitignore +++ b/.gitignore @@ -7,15 +7,9 @@ Release/* .pio/ .vscode/ config.h -.vscode/* -# mySetup.h mySetup.cpp myHal.cpp -# myAutomation.h myFilter.cpp -# myAutomation.h -# myLayout.h my*.h !my*.example.h -.vscode/extensions.json -.vscode/extensions.json +compile_commands.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 080e70d..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "platformio.platformio-ide" - ], - "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ffa498a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "files.associations": { - "array": "cpp", - "deque": "cpp", - "string": "cpp", - "unordered_map": "cpp", - "vector": "cpp", - "string_view": "cpp", - "initializer_list": "cpp", - "cstdint": "cpp" - } -} diff --git a/CommandDistributor.cpp b/CommandDistributor.cpp index 0c9185d..1714b73 100644 --- a/CommandDistributor.cpp +++ b/CommandDistributor.cpp @@ -233,3 +233,7 @@ void CommandDistributor::broadcastText(const FSH * msg) { broadcastReply(WITHROTTLE_TYPE, F("Hm%S\n"), msg); #endif } + +void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) { + broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr); +} diff --git a/CommandDistributor.h b/CommandDistributor.h index bbbc44c..e1dbfec 100644 --- a/CommandDistributor.h +++ b/CommandDistributor.h @@ -51,6 +51,7 @@ public : static int16_t retClockTime(); static void broadcastPower(); static void broadcastText(const FSH * msg); + static void broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr); template static void broadcastReply(clientType type, Targs... msg); static void forget(byte clientId); diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index 3c40bba..7363566 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -75,6 +75,9 @@ void setup() DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com")); +// Initialise HAL layer before reading EEprom or setting up MotorDrivers + IODevice::begin(); + DISPLAY_START ( // This block is still executed for DIAGS if display not in use LCD(0,F("DCC-EX v%S"),F(VERSION)); @@ -96,10 +99,7 @@ void setup() #if ETHERNET_ON EthernetInterface::setup(); #endif // ETHERNET_ON - - // Initialise HAL layer before reading EEprom or setting up MotorDrivers - IODevice::begin(); - + // As the setup of a motor shield may require a read of the current sense input from the ADC, // let's make sure to initialise the ADCee class! ADCee::begin(); diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 9e2cf5f..102d271 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -344,6 +344,20 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) return; break; + case 'z': // direct pin manipulation + if (p[0]==0) break; + if (params==1) { // + if (p[0]>0) IODevice::write(p[0],HIGH); + else IODevice::write(-p[0],LOW); + return; + } + if (params>=2 && params<=4) { // + // unused params default to 0 + IODevice::writeAnalogue(p[0],p[1],p[2],p[3]); + return; + } + break; + case 'Z': // OUTPUT if (parseZ(stream, params, p)) return; @@ -516,9 +530,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) StringFormatter::send(stream, F("\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA)); CommandDistributor::broadcastPower(); // is the only "get power status" command we have Turnout::printAll(stream); //send all Turnout states - Output::printAll(stream); //send all Output states Sensor::printAll(stream); //send all Sensor states - // TODO Send stats of speed reminders table return; #ifndef DISABLE_EEPROM @@ -955,7 +967,7 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) break; case HASH_KEYWORD_ANIN: // Display analogue input value - DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1])); + DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1])); break; #if !defined(IO_NO_HAL) diff --git a/DCCTimer.h b/DCCTimer.h index 2214851..7a9d940 100644 --- a/DCCTimer.h +++ b/DCCTimer.h @@ -1,7 +1,7 @@ /* - * © 2022 Paul M. Antoine + * © 2022-2023 Paul M. Antoine * © 2021 Mike S - * © 2021-2022 Harald Barth + * © 2021-2023 Harald Barth * © 2021 Fred Decker * All rights reserved. * @@ -62,6 +62,9 @@ class DCCTimer { static bool isPWMPin(byte pin); static void setPWM(byte pin, bool high); static void clearPWM(); + static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency); + static void DCCEXanalogWrite(uint8_t pin, int value); + // Update low ram level. Allow for extra bytes to be specified // by estimation or inspection, that may be used by other // called subroutines. Must be called with interrupts disabled. diff --git a/DCCTimerESP.cpp b/DCCTimerESP.cpp index caaae9d..cf5978e 100644 --- a/DCCTimerESP.cpp +++ b/DCCTimerESP.cpp @@ -150,6 +150,45 @@ int DCCTimer::freeMemory() { void DCCTimer::reset() { ESP.restart(); } + +#include "esp32-hal.h" +#include "soc/soc_caps.h" + + +#ifdef SOC_LEDC_SUPPORT_HS_MODE +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1) +#else +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM) +#endif + +static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 }; +static int cnt_channel = LEDC_CHANNELS; + +void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) { + if (pin < SOC_GPIO_PIN_COUNT) { + if (pin_to_channel[pin] != 0) { + ledcSetup(pin_to_channel[pin], frequency, 8); + } + } +} + +void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) { + if (pin < SOC_GPIO_PIN_COUNT) { + if (pin_to_channel[pin] == 0) { + if (!cnt_channel) { + log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS); + return; + } + pin_to_channel[pin] = --cnt_channel; + ledcAttachPin(pin, cnt_channel); + ledcSetup(cnt_channel, 1000, 8); + } else { + ledcAttachPin(pin, pin_to_channel[pin]); + } + ledcWrite(pin_to_channel[pin], value); + } +} + int ADCee::init(uint8_t pin) { pinMode(pin, ANALOG); adc1_config_width(ADC_WIDTH_BIT_12); diff --git a/DCCTimerSAMD.cpp b/DCCTimerSAMD.cpp index 463cc64..f878ae5 100644 --- a/DCCTimerSAMD.cpp +++ b/DCCTimerSAMD.cpp @@ -162,7 +162,7 @@ uint16_t ADCee::usedpins = 0; int * ADCee::analogvals = NULL; int ADCee::init(uint8_t pin) { - uint id = pin - A0; + uint8_t id = pin - A0; int value = 0; if (id > NUM_ADC_INPUTS) @@ -210,7 +210,7 @@ int ADCee::read(uint8_t pin, bool fromISR) { #pragma GCC push_options #pragma GCC optimize ("-O3") void ADCee::scan() { - static uint id = 0; // id and mask are the same thing but it is faster to + static uint8_t id = 0; // id and mask are the same thing but it is faster to static uint16_t mask = 1; // increment and shift instead to calculate mask from id static bool waiting = false; diff --git a/DCCTimerSTM32.cpp b/DCCTimerSTM32.cpp index 2504a26..4f9cbdd 100644 --- a/DCCTimerSTM32.cpp +++ b/DCCTimerSTM32.cpp @@ -1,8 +1,8 @@ /* * © 2023 Neil McKechnie - * © 2022 Paul M. Antoine + * © 2022-23 Paul M. Antoine * © 2021 Mike S - * © 2021 Harald Barth + * © 2021, 2023 Harald Barth * © 2021 Fred Decker * © 2021 Chris Harlow * © 2021 David Cutting @@ -40,27 +40,123 @@ HardwareSerial Serial1(PB7, PA15); // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F411RE #elif defined(ARDUINO_NUCLEO_F446RE) // Nucleo-64 boards don't have Serial1 defined by default -HardwareSerial Serial1(PA10, PB6); // Rx=PA10, Tx=PB6 -- CN10 pins 33 and 17 - F446RE +HardwareSerial Serial1(PA10, PB6); // Rx=PA10 (D2), Tx=PB6 (D10) -- CN10 pins 17 and 9 - F446RE // Serial2 is defined to use USART2 by default, but is in fact used as the diag console // via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc. +// NB: USART3 and USART6 are available but as yet undefined #elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) // Nucleo-144 boards don't have Serial1 defined by default HardwareSerial Serial1(PG9, PG14); // Rx=PG9, Tx=PG14 -- D0, D1 - F412ZG/F446ZE +// Serial2 is defined to use USART2 by default, but is in fact used as the diag console +// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc. +// NB: +// On all of the above, USART3, and USART6 are available but as yet undefined +// On F446ZE and F429ZI, UART4, UART5 are also available but as yet undefined +// On F429ZI, UART7 and UART8 are also available but as yet undefined #else -#warning Serial1 not defined +#error STM32 board selected is not yet explicitly supported - so Serial1 peripheral is not defined #endif +/////////////////////////////////////////////////////////////////////////////////////////////// +// Experimental code for High Accuracy (HA) DCC Signal mode +// Warning - use of TIM2 and TIM3 can affect the use of analogWrite() function on certain pins, +// which is used by the DC motor types. +/////////////////////////////////////////////////////////////////////////////////////////////// + +// INTERRUPT_CALLBACK interruptHandler=0; +// // Let's use STM32's timer #2 which supports hardware pulse generation on pin D13. +// // Also, timer #3 will do hardware pulses on pin D12. This gives +// // accurate timing, independent of the latency of interrupt handling. +// // We only need to interrupt on one of these (TIM2), the other will just generate +// // pulses. +// HardwareTimer timer(TIM2); +// HardwareTimer timerAux(TIM3); +// static bool tim2ModeHA = false; +// static bool tim3ModeHA = false; + +// // Timer IRQ handler +// void Timer_Handler() { +// interruptHandler(); +// } + +// void DCCTimer::begin(INTERRUPT_CALLBACK callback) { +// interruptHandler=callback; +// noInterrupts(); + +// // adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES); +// timer.pause(); +// timerAux.pause(); +// timer.setPrescaleFactor(1); +// timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); +// timer.attachInterrupt(Timer_Handler); +// timer.refresh(); +// timerAux.setPrescaleFactor(1); +// timerAux.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); +// timerAux.refresh(); + +// timer.resume(); +// timerAux.resume(); + +// interrupts(); +// } + +// bool DCCTimer::isPWMPin(byte pin) { +// // Timer 2 Channel 1 controls pin D13, and Timer3 Channel 1 controls D12. +// // Enable the appropriate timer channel. +// switch (pin) { +// case 12: +// return true; +// case 13: +// return true; +// default: +// return false; +// } +// } + +// void DCCTimer::setPWM(byte pin, bool high) { +// // Set the timer so that, at the next counter overflow, the requested +// // pin state is activated automatically before the interrupt code runs. +// // TIM2 is timer, TIM3 is timerAux. +// switch (pin) { +// case 12: +// if (!tim3ModeHA) { +// timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D12); +// tim3ModeHA = true; +// } +// if (high) +// TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0; +// else +// TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1; +// break; +// case 13: +// if (!tim2ModeHA) { +// timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D13); +// tim2ModeHA = true; +// } +// if (high) +// TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0; +// else +// TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1; +// break; +// } +// } + +// void DCCTimer::clearPWM() { +// timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC); +// tim2ModeHA = false; +// timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC); +// tim3ModeHA = false; +// } +/////////////////////////////////////////////////////////////////////////////////////////////// + INTERRUPT_CALLBACK interruptHandler=0; -// Let's use STM32's timer #2 which supports hardware pulse generation on pin D13. -// Also, timer #3 will do hardware pulses on pin D12. This gives -// accurate timing, independent of the latency of interrupt handling. -// We only need to interrupt on one of these (TIM2), the other will just generate -// pulses. -HardwareTimer timer(TIM2); -HardwareTimer timerAux(TIM3); +// Let's use STM32's timer #11 until disabused of this notion +// Timer #11 is used for "servo" library, but as DCC-EX is not using +// this libary, we should be free and clear. +HardwareTimer timer(TIM11); // Timer IRQ handler -void Timer_Handler() { +void Timer11_Handler() { interruptHandler(); } @@ -70,59 +166,31 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) { // adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES); timer.pause(); - timerAux.pause(); timer.setPrescaleFactor(1); +// timer.setOverflow(CLOCK_CYCLES * 2); timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); - timer.attachInterrupt(Timer_Handler); + timer.attachInterrupt(Timer11_Handler); timer.refresh(); - timerAux.setPrescaleFactor(1); - timerAux.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); - timerAux.refresh(); - timer.resume(); - timerAux.resume(); interrupts(); } bool DCCTimer::isPWMPin(byte pin) { - // Timer 2 Channel 1 controls pin D13, and Timer3 Channel 1 controls D12. - // Enable the appropriate timer channel. - switch (pin) { - case 12: - timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D12); - return true; - case 13: - timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D13); - return true; - default: - return false; - } + //TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM, + // there's no support yet for High Accuracy, so for now return false + // return digitalPinHasPWM(pin); + return false; } void DCCTimer::setPWM(byte pin, bool high) { - // Set the timer so that, at the next counter overflow, the requested - // pin state is activated automatically before the interrupt code runs. - // TIM2 is timer, TIM3 is timerAux. - switch (pin) { - case 12: - if (high) - TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0; - else - TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1; - break; - case 13: - if (high) - TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0; - else - TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1; - break; - } + // TODO: High Accuracy mode is not supported as yet, and may never need to be + (void) pin; + (void) high; } void DCCTimer::clearPWM() { - timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC); - timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC); + return; } void DCCTimer::getSimulatedMacAddress(byte mac[6]) { @@ -166,6 +234,9 @@ void DCCTimer::reset() { #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS // TODO: may need to use uint32_t on STMF4xx variants with > 16 analog inputs! +#if defined(ARDUINO_NUCLEO_F446RE) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) +#warning STM32 board selected not fully supported - only use ADC1 inputs 0-15 for current sensing! +#endif uint16_t ADCee::usedpins = 0; int * ADCee::analogvals = NULL; uint32_t * analogchans = NULL; @@ -176,10 +247,13 @@ int16_t ADCee::ADCmax() { } int ADCee::init(uint8_t pin) { - uint id = pin - A0; + int value = 0; - PinName stmpin = digitalPin[analogInputPin[id]]; - uint32_t stmgpio = stmpin / 16; // 16-bits per GPIO port group on STM32 + PinName stmpin = analogInputToPinName(pin); + if (stmpin == NC) // do not continue if this is not an analog pin at all + return -1024; // some silly value as error + + uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32) uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC channel (only valid for ADC1!) GPIO_TypeDef * gpioBase; @@ -197,12 +271,20 @@ int ADCee::init(uint8_t pin) { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC gpioBase = GPIOC; break; + default: + return -1023; // some silly value as error } - // Set pin mux mode to analog input - gpioBase->MODER |= (0b011 << (stmpin << 1)); // Set pin mux to analog mode + // Set pin mux mode to analog input, the 32 bit port mode register has 2 bits per pin + gpioBase->MODER |= (0b011 << (STM_PIN(stmpin) << 1)); // Set pin mux to analog mode (binary 11) // Set the sampling rate for that analog input + // This is F411x specific! Different on for example F334 + // STM32F11xC/E Reference manual + // 11.12.4 ADC sample time register 1 (ADC_SMPR1) (channels 10 to 18) + // 11.12.5 ADC sample time register 2 (ADC_SMPR2) (channels 0 to 9) + if (adcchan > 18) + return -1022; // silly value as error if (adcchan < 10) ADC1->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles else @@ -214,8 +296,12 @@ int ADCee::init(uint8_t pin) { while(!(ADC1->SR & (1 << 1))); // Wait until conversion is complete value = ADC1->DR; // Read value from register - if (analogvals == NULL) - { + uint8_t id = pin - PNUM_ANALOG_BASE; + if (id > 15) { // today we have not enough bits in the mask to support more + return -1021; + } + + if (analogvals == NULL) { // allocate analogvals and analogchans if this is the first invocation of init. analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int)); analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t)); } @@ -230,7 +316,7 @@ int ADCee::init(uint8_t pin) { * Read function ADCee::read(pin) to get value instead of analogRead(pin) */ int ADCee::read(uint8_t pin, bool fromISR) { - uint8_t id = pin - A0; + uint8_t id = pin - PNUM_ANALOG_BASE; // Was this pin initialised yet? if ((usedpins & (1<CR2 |= (1 << 0); // Switch on ADC1 interrupts(); } -#endif \ No newline at end of file +#endif diff --git a/Display.cpp b/Display.cpp index 2c44778..d581149 100644 --- a/Display.cpp +++ b/Display.cpp @@ -36,7 +36,7 @@ * not held up significantly. The exception to this is when * the loop2() function is called with force=true, where * a screen update is executed to completion. This is normally - * only noMoreRowsToDisplay during start-up. + * only done during start-up. * The scroll mode is selected by defining SCROLLMODE as 0, 1 or 2 * in the config.h. * #define SCROLLMODE 0 is scroll continuous (fill screen if poss), @@ -51,11 +51,10 @@ Display::Display(DisplayDevice *deviceDriver) { _deviceDriver = deviceDriver; // Get device dimensions in characters (e.g. 16x2). - numCharacterColumns = _deviceDriver->getNumCols(); - numCharacterRows = _deviceDriver->getNumRows();; + numScreenColumns = _deviceDriver->getNumCols(); + numScreenRows = _deviceDriver->getNumRows(); for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++) rowBuffer[row][0] = '\0'; - topRow = ROW_INITIAL; // loop2 will fill from row 0 addDisplay(0); // Add this display as display number 0 }; @@ -69,20 +68,19 @@ void Display::_clear() { _deviceDriver->clearNative(); for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++) rowBuffer[row][0] = '\0'; - topRow = ROW_INITIAL; // loop2 will fill from row 0 } void Display::_setRow(uint8_t line) { hotRow = line; hotCol = 0; - rowBuffer[hotRow][0] = 0; // Clear existing text + rowBuffer[hotRow][0] = '\0'; // Clear existing text } size_t Display::_write(uint8_t b) { if (hotRow >= MAX_CHARACTER_ROWS || hotCol >= MAX_CHARACTER_COLS) return -1; rowBuffer[hotRow][hotCol] = b; hotCol++; - rowBuffer[hotRow][hotCol] = 0; + rowBuffer[hotRow][hotCol] = '\0'; return 1; } @@ -109,8 +107,8 @@ Display *Display::loop2(bool force) { return NULL; } else { // force full screen update from the beginning. - rowFirst = ROW_INITIAL; - rowNext = ROW_INITIAL; + rowFirst = 0; + rowCurrent = 0; bufferPointer = 0; noMoreRowsToDisplay = false; slot = 0; @@ -118,15 +116,20 @@ Display *Display::loop2(bool force) { do { if (bufferPointer == 0) { - // Find a line of data to write to the screen. - if (rowFirst == ROW_INITIAL) rowFirst = rowNext; - if (findNextNonBlankRow()) { + // Search for non-blank row + while (!noMoreRowsToDisplay) { + if (!isCurrentRowBlank()) break; + moveToNextRow(); + if (rowCurrent == rowFirst) noMoreRowsToDisplay = true; + } + + if (noMoreRowsToDisplay) { + // No non-blank lines left, so draw blank line + buffer[0] = '\0'; + } else { // Non-blank line found, so copy it (including terminator) for (uint8_t i = 0; i <= MAX_CHARACTER_COLS; i++) - buffer[i] = rowBuffer[rowNext][i]; - } else { - // No non-blank lines left, so draw a blank line - buffer[0] = 0; + buffer[i] = rowBuffer[rowCurrent][i]; } _deviceDriver->setRowNative(slot); // Set position for display charIndex = 0; @@ -142,21 +145,49 @@ Display *Display::loop2(bool force) { } if (++charIndex >= MAX_CHARACTER_COLS) { - // Screen slot completed, move to next slot on screen + // Screen slot completed, move to next nonblank row bufferPointer = 0; + for (;;) { + moveToNextRow(); + if (rowCurrent == rowFirst) { + noMoreRowsToDisplay = true; + break; + } + if (!isCurrentRowBlank()) break; + } + // Move to next screen slot, if available slot++; - if (slot >= numCharacterRows) { - // Last slot on screen written, reset ready for next screen update. -#if SCROLLMODE==2 - if (!noMoreRowsToDisplay) { - // On next refresh, restart one row on from previous start. - rowNext = rowFirst; - findNextNonBlankRow(); + if (slot >= numScreenRows) { + // Last slot on screen written, so get ready for next screen update. +#if SCROLLMODE==0 + // Scrollmode 0 scrolls continuously. If the rows fit on the screen, + // then restart at row 0, but otherwise continue with the row + // after the last one displayed. + if (countNonBlankRows() <= numScreenRows) + rowCurrent = 0; + rowFirst = rowCurrent; +#elif SCROLLMODE==1 + // Scrollmode 1 scrolls by page, so if the last page has just completed then + // next time restart with row 0. + if (noMoreRowsToDisplay) + rowFirst = rowCurrent = 0; +#else + // Scrollmode 2 scrolls by row. If the rows don't fit on the screen, + // then start one row further on next time. If they do fit, then + // show them in order and start next page at row 0. + if (countNonBlankRows() <= numScreenRows) { + rowFirst = rowCurrent = 0; + } else { + // Find first non-blank row after the previous first row + rowCurrent = rowFirst; + do { + moveToNextRow(); + } while (isCurrentRowBlank()); + rowFirst = rowCurrent; } #endif noMoreRowsToDisplay = false; slot = 0; - rowFirst = ROW_INITIAL; lastScrollTime = currentMillis; return NULL; } @@ -167,30 +198,22 @@ Display *Display::loop2(bool force) { return NULL; } -bool Display::findNextNonBlankRow() { - while (!noMoreRowsToDisplay) { - if (rowNext == ROW_INITIAL) - rowNext = 0; - else - rowNext = rowNext + 1; - if (rowNext >= MAX_CHARACTER_ROWS) rowNext = ROW_INITIAL; -#if SCROLLMODE == 1 - // Finished if we've looped back to start - if (rowNext == ROW_INITIAL) { - noMoreRowsToDisplay = true; - return false; - } -#else - // Finished if we're back to the first one shown - if (rowNext == rowFirst) { - noMoreRowsToDisplay = true; - return false; - } -#endif - if (rowBuffer[rowNext][0] != 0) { - // Found non-blank row - return true; - } +bool Display::isCurrentRowBlank() { + return (rowBuffer[rowCurrent][0] == '\0'); +} + +void Display::moveToNextRow() { + // Skip blank rows + if (++rowCurrent >= MAX_CHARACTER_ROWS) + rowCurrent = 0; +} + +uint8_t Display::countNonBlankRows() { + uint8_t count = 0; + for (uint8_t rowNumber=0; rowNumber. - */ -#ifdef ARDUINO_ARCH_ESP32 -#include -#include "ESP32-fixes.h" - -#include "esp32-hal.h" -#include "soc/soc_caps.h" - - -#ifdef SOC_LEDC_SUPPORT_HS_MODE -#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1) -#else -#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM) -#endif - -static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 }; -static int cnt_channel = LEDC_CHANNELS; - -void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) { - if (pin < SOC_GPIO_PIN_COUNT) { - if (pin_to_channel[pin] != 0) { - ledcSetup(pin_to_channel[pin], frequency, 8); - } - } -} - -void DCCEXanalogWrite(uint8_t pin, int value) { - if (pin < SOC_GPIO_PIN_COUNT) { - if (pin_to_channel[pin] == 0) { - if (!cnt_channel) { - log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS); - return; - } - pin_to_channel[pin] = --cnt_channel; - ledcAttachPin(pin, cnt_channel); - ledcSetup(cnt_channel, 1000, 8); - } else { - ledcAttachPin(pin, pin_to_channel[pin]); - } - ledcWrite(pin_to_channel[pin], value); - } -} -#endif diff --git a/ESP32-fixes.h b/ESP32-fixes.h deleted file mode 100644 index 0fd4a7f..0000000 --- a/ESP32-fixes.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * © 2022 Harald Barth - * 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 . - */ -#ifdef ARDUINO_ARCH_ESP32 -#pragma once -#include -void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency); -void DCCEXanalogWrite(uint8_t pin, int value); -#endif - diff --git a/EX-CommandStation-installer.exe b/EX-CommandStation-installer.exe new file mode 100644 index 0000000..88f12a2 Binary files /dev/null and b/EX-CommandStation-installer.exe differ diff --git a/EXRAIL2.cpp b/EXRAIL2.cpp index 3bd8da7..31a0de6 100644 --- a/EXRAIL2.cpp +++ b/EXRAIL2.cpp @@ -1,7 +1,7 @@ /* * © 2021 Neil McKechnie * © 2021-2023 Harald Barth - * © 2020-2022 Chris Harlow + * © 2020-2023 Chris Harlow * All rights reserved. * * This file is part of CommandStation-EX @@ -24,8 +24,8 @@ F1. [DONE] DCC accessory packet opcodes (short and long form) F2. [DONE] ONAccessory catchers F3. [DONE] Turnout descriptions for Withrottle - F4. Oled announcements (depends on HAL) - F5. Withrottle roster info + F4. [DONE] Oled announcements (depends on HAL) + F5. [DONE] Withrottle roster info F6. Multi-occupancy semaphore F7. [DONE see AUTOSTART] Self starting sequences F8. Park/unpark @@ -105,12 +105,9 @@ uint16_t RMFT2::getOperand(byte n) { // getOperand static version, must be provided prog counter from loop etc. uint16_t RMFT2::getOperand(int progCounter,byte n) { int offset=progCounter+1+(n*3); - if (offset&1) { - byte lsb=GETHIGHFLASH(RouteCode,offset); - byte msb=GETHIGHFLASH(RouteCode,offset+1); - return msb<<8|lsb; - } - return GETHIGHFLASHW(RouteCode,offset); + byte lsb=GETHIGHFLASH(RouteCode,offset); + byte msb=GETHIGHFLASH(RouteCode,offset+1); + return msb<<8|lsb; } LookList::LookList(int16_t size) { @@ -201,7 +198,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { case OPCODE_IFNOT: { int16_t pin = (int16_t)operand; if (pin<0) pin = -pin; - DIAG(F("EXRAIL input vpin %d"),pin); + DIAG(F("EXRAIL input VPIN %u"),pin); IODevice::configureInput((VPIN)pin,true); break; } @@ -211,7 +208,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { case OPCODE_IFGTE: case OPCODE_IFLT: case OPCODE_DRIVE: { - DIAG(F("EXRAIL analog input vpin %d"),(VPIN)operand); + DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand); IODevice::configureAnalogIn((VPIN)operand); break; } @@ -243,8 +240,9 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { case OPCODE_AUTOSTART: // automatically create a task from here at startup. - // but we will do one at 0 anyway by default. - if (progCounter>0) new RMFT2(progCounter); + // Removed if (progCounter>0) check 4.2.31 because + // default start it top of file is now removed. . + new RMFT2(progCounter); break; default: // Ignore @@ -255,7 +253,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS); - new RMFT2(0); // add the startup route + // Removed for 4.2.31 new RMFT2(0); // add the startup route diag=saved_diag; } @@ -1131,7 +1129,10 @@ void RMFT2::clockEvent(int16_t clocktime, bool change) { // Hunt for an ONTIME for this time if (Diag::CMD) DIAG(F("Looking for clock event at : %d"), clocktime); - if (change) handleEvent(F("CLOCK"),onClockLookup,clocktime); + if (change) { + handleEvent(F("CLOCK"),onClockLookup,clocktime); + handleEvent(F("CLOCK"),onClockLookup,25*60+clocktime%60); + } } void RMFT2::handleEvent(const FSH* reason,LookList* handlers, int16_t id) { diff --git a/EXRAIL2MacroReset.h b/EXRAIL2MacroReset.h index b4ffc6d..6ea195e 100644 --- a/EXRAIL2MacroReset.h +++ b/EXRAIL2MacroReset.h @@ -91,6 +91,7 @@ #undef ONCLOSE #undef ONTIME #undef ONCLOCKTIME +#undef ONCLOCKMINS #undef ONGREEN #undef ONRED #undef ONTHROW @@ -134,6 +135,7 @@ #undef STOP #undef THROW #undef TURNOUT +#undef TURNOUTL #undef UNJOIN #undef UNLATCH #undef VIRTUAL_SIGNAL @@ -208,6 +210,7 @@ #define ONAMBER(signal_id) #define ONTIME(value) #define ONCLOCKTIME(hours,mins) +#define ONCLOCKMINS(mins) #define ONDEACTIVATE(addr,subaddr) #define ONDEACTIVATEL(linear) #define ONCLOSE(turnout_id) @@ -254,6 +257,7 @@ #define STOP #define THROW(id) #define TURNOUT(id,addr,subaddr,description...) +#define TURNOUTL(id,addr,description...) #define UNJOIN #define UNLATCH(sensor_id) #define VIRTUAL_SIGNAL(id) diff --git a/EXRAILMacros.h b/EXRAILMacros.h index ee20c1f..446b388 100644 --- a/EXRAILMacros.h +++ b/EXRAILMacros.h @@ -316,6 +316,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id), #define ONTIME(value) OPCODE_ONTIME,V(value), #define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)), +#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins) #define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr), #define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3), #define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id), @@ -361,6 +362,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define STOP OPCODE_SPEED,V(0), #define THROW(id) OPCODE_THROW,V(id), #define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr), +#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description) #define UNJOIN OPCODE_UNJOIN,0,0, #define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id), #define VIRTUAL_SIGNAL(id) diff --git a/EthernetInterface.cpp b/EthernetInterface.cpp index 7c5d768..5cf531c 100644 --- a/EthernetInterface.cpp +++ b/EthernetInterface.cpp @@ -136,7 +136,7 @@ bool EthernetInterface::checkLink() { DIAG(F("Ethernet cable connected")); connected=true; #ifdef IP_ADDRESS - setLocalIP(IP_ADDRESS); // for static IP, set it again + Ethernet.setLocalIP(IP_ADDRESS); // for static IP, set it again #endif IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static) server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h index 75a9be6..0ac9c5f 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "devel-202302281422Z" +#define GITHUB_SHA "devel-202304111013Z" diff --git a/I2CManager.cpp b/I2CManager.cpp index 6d5db41..d0d8550 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -72,18 +72,23 @@ static const FSH * guessI2CDeviceType(uint8_t address) { void I2CManagerClass::begin(void) { if (!_beginCompleted) { _beginCompleted = true; + + // Check for short-circuit or floating lines (no pull-up) on I2C before enabling I2C + const FSH *message = F("WARNING: Check I2C %S line for short/pullup"); + pinMode(SDA, INPUT); + if (!digitalRead(SDA)) + DIAG(message, F("SDA")); + pinMode(SCL, INPUT); + if (!digitalRead(SCL)) + DIAG(message, F("SCL")); + + // Now initialise I2C _initialise(); #if defined(I2C_USE_WIRE) DIAG(F("I2CManager: Using Wire library")); #endif - // Check for short-circuits on I2C - if (!digitalRead(SDA)) - DIAG(F("WARNING: Possible short-circuit on I2C SDA line")); - if (!digitalRead(SCL)) - DIAG(F("WARNING: Possible short-circuit on I2C SCL line")); - // Probe and list devices. Use standard mode // (clock speed 100kHz) for best device compatibility. _setClock(100000); diff --git a/IODevice.cpp b/IODevice.cpp index e907c23..2ed21b6 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -63,15 +63,31 @@ void IODevice::begin() { if (exrailHalSetup) exrailHalSetup(); - // Predefine two PCA9685 modules 0x40-0x41 + // Predefine two PCA9685 modules 0x40-0x41 if no conflicts // Allocates 32 pins 100-131 - PCA9685::create(100, 16, 0x40); - PCA9685::create(116, 16, 0x41); + if (checkNoOverlap(100, 16, 0x40)) { + PCA9685::create(100, 16, 0x40); + } else { + DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device")); + } + if (checkNoOverlap(116, 16, 0x41)) { + PCA9685::create(116, 16, 0x41); + } else { + DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device")); + } - // Predefine two MCP23017 module 0x20/0x21 + // Predefine two MCP23017 module 0x20/0x21 if no conflicts // Allocates 32 pins 164-195 - MCP23017::create(164, 16, 0x20); - MCP23017::create(180, 16, 0x21); + if (checkNoOverlap(164, 16, 0x20)) { + MCP23017::create(164, 16, 0x20); + } else { + DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device")); + } + if (checkNoOverlap(180, 16, 0x21)) { + MCP23017::create(180, 16, 0x21); + } else { + DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device")); + } } // reset() function to reinitialise all devices @@ -169,7 +185,7 @@ bool IODevice::hasCallback(VPIN vpin) { // Display (to diagnostics) details of the device. void IODevice::_display() { - DIAG(F("Unknown device Vpins:%d-%d %S"), + DIAG(F("Unknown device Vpins:%u-%u %S"), (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F("")); } @@ -179,7 +195,7 @@ bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i IODevice *dev = findDevice(vpin); if (dev) return dev->_configure(vpin, configType, paramCount, params); #ifdef DIAG_IO - DIAG(F("IODevice::configure(): Vpin ID %d not found!"), (int)vpin); + DIAG(F("IODevice::configure(): VPIN %u not found!"), (int)vpin); #endif return false; } @@ -191,7 +207,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 %u not found!"), (int)vpin); #endif return false; } @@ -203,7 +219,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 %u not found!"), (int)vpin); #endif return -1023; } @@ -213,7 +229,7 @@ int IODevice::configureAnalogIn(VPIN vpin) { return dev->_configureAnalogIn(vpin); } #ifdef DIAG_IO - DIAG(F("IODevice::configureAnalogIn(): Vpin %d not found!"), (int)vpin); + DIAG(F("IODevice::configureAnalogIn(): VPIN %u not found!"), (int)vpin); #endif return -1023; } @@ -227,7 +243,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 %u not found!"), (int)vpin); #endif } @@ -246,7 +262,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 %u not found!"), (int)vpin); #endif } @@ -314,9 +330,11 @@ IODevice *IODevice::findDeviceFollowing(VPIN vpin) { // Private helper function to check for vpin overlap. Run during setup only. // returns true if pins DONT overlap with existing device +// TODO: Move the I2C address reservation and checks into the I2CManager code. +// That will enable non-HAL devices to reserve I2C addresses too. bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) { #ifdef DIAG_IO - DIAG(F("Check no overlap %d %d %s"), firstPin,nPins,i2cAddress.toString()); + DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString()); #endif VPIN lastPin=firstPin+nPins-1; for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { @@ -327,7 +345,7 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddres VPIN lastDevPin=firstDevPin+dev->_nPins-1; bool noOverlap= firstPin>lastDevPin || lastPin= NUM_DIGITAL_PINS) return false; #ifdef DIAG_IO - DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, p[0]); + DIAG(F("Arduino _configurePullup pin:%d Val:%d"), pin, p[0]); #endif pinMode(pin, p[0] ? INPUT_PULLUP : INPUT); return true; @@ -528,7 +546,7 @@ int ArduinoPins::_configureAnalogIn(VPIN vpin) { } void ArduinoPins::_display() { - DIAG(F("Arduino Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1); + DIAG(F("Arduino Vpins:%u-%u"), (int)_firstVpin, (int)_firstVpin+_nPins-1); } ///////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/IODevice.h b/IODevice.h index 51b5aa0..769e111 100644 --- a/IODevice.h +++ b/IODevice.h @@ -467,6 +467,75 @@ protected: } }; +///////////////////////////////////////////////////////////////////////////////////////////////////// +// +// This HAL device driver is intended for communication in automation +// sequences. A VPIN can be SET or RESET within a sequence, and its +// current state checked elsewhere using IF, IFNOT, AT etc. or monitored +// from JMRI using a Sensor object (DCC-EX command). +// Alternatively, the flag can be set from JMRI and other interfaces +// using the command, to enable or disable actions within a sequence. +// +// Example of configuration in halSetup.h: +// +// FLAGS::create(32000, 128); +// +// or in myAutomation.h: +// +// HAL(FLAGS, 32000, 128); +// +// Both create 128 flags numbered with VPINs 32000-32127. +// +// + +class FLAGS : IODevice { +private: + uint8_t *_states = NULL; + +public: + static void create(VPIN firstVpin, unsigned int nPins) { + if (checkNoOverlap(firstVpin, nPins)) + new FLAGS(firstVpin, nPins); + } + +protected: + // Constructor performs static initialisation of the device object + FLAGS (VPIN firstVpin, int nPins) { + _firstVpin = firstVpin; + _nPins = nPins; + _states = (uint8_t *)calloc(1, (_nPins+7)/8); + if (!_states) { + DIAG(F("FLAGS: ERROR Memory Allocation Failure")); + return; + } + + addDevice(this); + } + + int _read(VPIN vpin) override { + int pin = vpin - _firstVpin; + if (pin >= _nPins || pin < 0) return 0; + uint8_t mask = 1 << (pin & 7); + return (_states[pin>>3] & mask) ? 1 : 0; + } + + void _write(VPIN vpin, int value) override { + int pin = vpin - _firstVpin; + if (pin >= _nPins || pin < 0) return; + uint8_t mask = 1 << (pin & 7); + if (value) + _states[pin>>3] |= mask; + else + _states[pin>>3] &= ~mask; + } + + void _display() override { + DIAG(F("FLAGS configured on VPINs %u-%u"), + _firstVpin, _firstVpin+_nPins-1); + } + +}; + #include "IO_MCP23008.h" #include "IO_MCP23017.h" #include "IO_PCF8574.h" diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 8ff8683..a78503f 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -119,7 +119,7 @@ private: 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]); + DIAG(F("ADS111x VPIN:%u value:%d"), _currentPin, _value[_currentPin]); #endif // Move to next pin @@ -142,7 +142,7 @@ private: } void _display() override { - DIAG(F("ADS111x I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, + DIAG(F("ADS111x I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index 214a2f9..3e60e3b 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -62,7 +62,7 @@ void DCCAccessoryDecoder::_write(VPIN id, int state) { void DCCAccessoryDecoder::_display() { int endAddress = _packedAddress + _nPins - 1; - DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, + DIAG(F("DCCAccessoryDecoder Configured on Vpins:%u-%u Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress)); } diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index 4439fbc..edd416d 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -1,5 +1,5 @@ /* - * © 2022, Neil McKechnie. All rights reserved. + * © 2023, Neil McKechnie. All rights reserved. * * This file is part of DCC++EX API * @@ -33,10 +33,13 @@ * and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1). * * Example: - * In mySetup function within mySetup.cpp: + * In halSetup function within myHal.cpp: * DFPlayer::create(3500, 5, Serial1); + * or in myAutomation.h: + * HAL(DFPlayer, 3500, 5, Serial1) * - * Writing an analogue value 1-2999 to the first pin (3500) will play the numbered file from the SD card; + * Writing an analogue value 1-2999 to the first pin (3500) will play the numbered file from the + * SD card; e.g. a value of 1 will play the first file, 2 for the second file etc. * Writing an analogue value 0 to the first pin (3500) will stop the file playing; * Writing an analogue value 0-30 to the second pin (3501) will set the volume; * Writing a digital value of 1 to a pin will play the file corresponding to that pin, e.g. @@ -61,6 +64,10 @@ * card (as listed by the DIR command in Windows). This may not match the order of the files * as displayed by Windows File Manager, which sorts the file names. It is suggested that * files be copied into an empty SDcard in the desired order, one at a time. + * + * The driver now polls the device for its current status every second. Should the device + * fail to respond it will be marked off-line and its busy indicator cleared, to avoid + * lock-ups in automation scripts that are executing for a WAITFOR(). */ #ifndef IO_DFPlayer_h @@ -74,21 +81,13 @@ private: HardwareSerial *_serial; bool _playing = false; uint8_t _inputIndex = 0; - unsigned long _commandSendTime; // Allows timeout processing - uint8_t _lastVolumeLevel = MAXVOLUME; - - // When two commands are sent in quick succession, the device sometimes - // fails to execute one. A delay is required between successive commands. - // This could be implemented by buffering commands and outputting them - // from the loop() function, but it would somewhat complicate the - // driver. A simpler solution is to output a number of NUL pad characters - // between successive command strings if there isn't sufficient elapsed time - // between them. At 9600 baud, each pad character takes approximately - // 1ms to complete. Experiments indicate that the minimum number of pads - // for reliable operation is 17. This gives 17.7ms between the end of one - // command and the beginning of the next, or 28ms between successive commands - // being completed. I've allowed 20 characters, which is almost 21ms. - const int numPadCharacters = 20; // Number of pad characters between commands + unsigned long _commandSendTime; // Time (us) that last transmit took place. + unsigned long _timeoutTime; + uint8_t _recvCMD; // Last received command code byte + bool _awaitingResponse = false; + uint8_t _requestedVolumeLevel = MAXVOLUME; + uint8_t _currentVolume = MAXVOLUME; + int _requestedSong = -1; // -1=none, 0=stop, >0=file number public: @@ -113,66 +112,151 @@ protected: // Send a query to the device to see if it responds sendPacket(0x42); - _commandSendTime = micros(); + _timeoutTime = micros() + 5000000UL; // 5 second timeout + _awaitingResponse = true; } void _loop(unsigned long currentMicros) override { - // Check for incoming data on _serial, and update busy flag accordingly. - // Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF" - while (_serial->available()) { - int c = _serial->read(); - if (c == 0x7E && _inputIndex == 0) - _inputIndex = 1; - else if ((c==0xFF && _inputIndex==1) - || (c==0x3D && _inputIndex==3) - || (_inputIndex >=4 && _inputIndex <= 8)) - _inputIndex++; - else if (c==0x06 && _inputIndex==2) { - // 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 - 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 5 seconds - if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 5000000UL) { + + // Read responses from device + processIncoming(); + + // Check if a command sent to device has timed out. Allow 0.5 second for response + if (_awaitingResponse && (int32_t)(currentMicros - _timeoutTime) > 0) { DIAG(F("DFPlayer device not responding on serial port")); _deviceState = DEVSTATE_FAILED; + _awaitingResponse = false; + _playing = false; } + + // Send any commands that need to go. + processOutgoing(currentMicros); + delayUntil(currentMicros + 10000); // Only enter every 10ms } + // Check for incoming data on _serial, and update busy flag and other state accordingly + void processIncoming() { + // Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF" + bool ok = false; + while (_serial->available()) { + int c = _serial->read(); + switch (_inputIndex) { + case 0: + if (c == 0x7E) ok = true; + break; + case 1: + if (c == 0xFF) ok = true; + break; + case 2: + if (c== 0x06) ok = true; + break; + case 3: + _recvCMD = c; // CMD byte + ok = true; + break; + case 6: + switch (_recvCMD) { + case 0x42: + // Response to status query + _playing = (c != 0); + // Mark the device online and cancel timeout + if (_deviceState==DEVSTATE_INITIALISING) { + _deviceState = DEVSTATE_NORMAL; + #ifdef DIAG_IO + _display(); + #endif + } + _awaitingResponse = false; + break; + case 0x3d: + // End of play + if (_playing) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Finished")); + #endif + _playing = false; + } + break; + case 0x40: + // Error code + DIAG(F("DFPlayer: Error %d returned from device"), c); + _playing = false; + break; + } + ok = true; + break; + case 4: case 5: case 7: case 8: + ok = true; // Skip over these bytes in message. + break; + case 9: + if (c==0xef) { + // Message finished + } + break; + default: + break; + } + if (ok) + _inputIndex++; // character as expected, so increment index + else + _inputIndex = 0; // otherwise reset. + } + } + + // Send any commands that need to be sent + void processOutgoing(unsigned long currentMicros) { + + // When two commands are sent in quick succession, the device will often fail to + // execute one. Testing has indicated that a delay of 100ms or more is required + // between successive commands to get reliable operation. + // If 100ms has elapsed since the last thing sent, then check if there's some output to do. + if (((int32_t)currentMicros - _commandSendTime) > 100000) { + if (_currentVolume > _requestedVolumeLevel) { + // Change volume before changing song if volume is reducing. + _currentVolume = _requestedVolumeLevel; + sendPacket(0x06, _currentVolume); + } else if (_requestedSong > 0) { + // Change song + sendPacket(0x03, _requestedSong); + _requestedSong = -1; + } else if (_requestedSong == 0) { + sendPacket(0x16); // Stop playing + _requestedSong = -1; + } else if (_currentVolume < _requestedVolumeLevel) { + // Change volume after changing song if volume is increasing. + _currentVolume = _requestedVolumeLevel; + sendPacket(0x06, _currentVolume); + } else if ((int32_t)currentMicros - _commandSendTime > 1000000) { + // Poll device every second that other commands aren't being sent, + // to check if it's still connected and responding. + sendPacket(0x42); + if (!_awaitingResponse) { + _timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds + _awaitingResponse = true; + } + } + } + } + // 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 { + if (_deviceState == DEVSTATE_FAILED) return; 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); + _requestedSong = pin+1; _playing = true; } else { // Value 0, stop playing #ifdef DIAG_IO DIAG(F("DFPlayer: Stop")); #endif - sendPacket(0x16); + _requestedSong = 0; // No song _playing = false; } } @@ -181,16 +265,13 @@ protected: // 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. - // If starting a new file and setting volume, then avoid a short burst of loud noise by - // the following strategy: - // - If the volume is increasing, start playing the song before setting the volume, - // - If the volume is decreasing, decrease it and then start playing. // void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override { + if (_deviceState == DEVSTATE_FAILED) return; uint8_t pin = vpin - _firstVpin; #ifdef DIAG_IO - DIAG(F("DFPlayer: VPIN:%d FileNo:%d Volume:%d"), vpin, value, volume); + DIAG(F("DFPlayer: VPIN:%u FileNo:%d Volume:%d"), vpin, value, volume); #endif // Validate parameter. @@ -199,37 +280,28 @@ protected: if (pin == 0) { // Play track if (value > 0) { - if (volume != 0) { - if (volume <= _lastVolumeLevel) - sendPacket(0x06, volume); // Set volume before starting - sendPacket(0x03, value); // Play track - _playing = true; - if (volume > _lastVolumeLevel) - sendPacket(0x06, volume); // Set volume after starting - _lastVolumeLevel = volume; - } else { - // Volume not changed, just play - sendPacket(0x03, value); - _playing = true; - } + if (volume > 0) + _requestedVolumeLevel = volume; + _requestedSong = value; + _playing = true; } else { - sendPacket(0x16); // Stop play + _requestedSong = 0; // stop playing _playing = false; } } else if (pin == 1) { // Set volume (0-30) - sendPacket(0x06, value); - _lastVolumeLevel = volume; + _requestedVolumeLevel = value; } } // A read on any pin indicates whether the player is still playing. int _read(VPIN) override { + if (_deviceState == DEVSTATE_FAILED) return false; return _playing; } void _display() override { - DIAG(F("DFPlayer Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1, + DIAG(F("DFPlayer Configured on Vpins:%u-%u %S"), _firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } @@ -246,7 +318,6 @@ private: void sendPacket(uint8_t command, uint16_t arg = 0) { - unsigned long currentMillis = millis(); uint8_t out[] = { 0x7E, 0xFF, 06, @@ -260,19 +331,10 @@ private: setChecksum(out); - // Check how long since the last command was sent. - // Each character takes approx 1ms at 9600 baud - unsigned long minimumGap = numPadCharacters + sizeof(out); - if (currentMillis - _commandSendTime < minimumGap) { - // Output some pad characters to add an - // artificial delay between commands - for (int i=0; iwrite((uint8_t)0); - } - - // Now output the command + // Output the command _serial->write(out, sizeof(out)); - _commandSendTime = currentMillis; + + _commandSendTime = micros(); } uint16_t calcChecksum(uint8_t* packet) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 063b5dd..675d66c 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -34,11 +34,16 @@ * device in use. There is no way for the device driver to sanity check pins are used for the * correct purpose, however the EX-IOExpander device's pin map will prevent pins being used * incorrectly (eg. A6/7 on Nano cannot be used for digital input/output). +* +* The total number of pins cannot exceed 256 because of the communications packet format. +* The number of analogue inputs cannot exceed 16 because of a limit on the maximum +* I2C packet size of 32 bytes (in the Wire library). */ #ifndef IO_EX_IOEXPANDER_H #define IO_EX_IOEXPANDER_H +#include "IODevice.h" #include "I2CManager.h" #include "DIAG.h" #include "FSH.h" @@ -64,116 +69,209 @@ public: if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } -private: +private: // Constructor EXIOExpander(VPIN firstVpin, int nPins, I2CAddress i2cAddress) { _firstVpin = firstVpin; + // Number of pins cannot exceed 256 (1 byte) because of I2C message structure. + if (nPins > 256) nPins = 256; _nPins = nPins; - _i2cAddress = i2cAddress; + _I2CAddress = i2cAddress; addDevice(this); } void _begin() { + uint8_t status; // Initialise EX-IOExander device I2CManager.begin(); - if (I2CManager.exists(_i2cAddress)) { - _command4Buffer[0] = EXIOINIT; - _command4Buffer[1] = _nPins; - _command4Buffer[2] = _firstVpin & 0xFF; - _command4Buffer[3] = _firstVpin >> 8; + if (I2CManager.exists(_I2CAddress)) { // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command4Buffer, 4); - if (_receive3Buffer[0] == EXIOPINS) { - _numDigitalPins = _receive3Buffer[1]; - _numAnaloguePins = _receive3Buffer[2]; - _digitalPinBytes = (_numDigitalPins + 7)/8; - _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); - _analoguePinBytes = _numAnaloguePins * 2; - _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); - _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { - DIAG(F("ERROR configuring EX-IOExpander device, I2C:%s"), _i2cAddress.toString()); - _deviceState = DEVSTATE_FAILED; - return; - } + // NB The I2C calls here are done as blocking calls, as they're not time-critical + // during initialisation and the reads require waiting for a response anyway. + // Hence we can allocate I/O buffers from the stack. + uint8_t receiveBuffer[3]; + uint8_t commandBuffer[4] = {EXIOINIT, (uint8_t)_nPins, (uint8_t)(_firstVpin & 0xFF), (uint8_t)(_firstVpin >> 8)}; + status = I2CManager.read(_I2CAddress, receiveBuffer, sizeof(receiveBuffer), commandBuffer, sizeof(commandBuffer)); + if (status == I2C_STATUS_OK) { + if (receiveBuffer[0] == EXIOPINS) { + _numDigitalPins = receiveBuffer[1]; + _numAnaloguePins = receiveBuffer[2]; + + // See if we already have suitable buffers assigned + size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8; + if (_digitalPinBytes < digitalBytesNeeded) { + // Not enough space, free any existing buffer and allocate a new one + if (_digitalPinBytes > 0) free(_digitalInputStates); + _digitalInputStates = (byte*) calloc(_digitalPinBytes, 1); + _digitalPinBytes = digitalBytesNeeded; + } + size_t analogueBytesNeeded = _numAnaloguePins * 2; + if (_analoguePinBytes < analogueBytesNeeded) { + // Free any existing buffers and allocate new ones. + if (_analoguePinBytes > 0) { + free(_analogueInputBuffer); + free(_analogueInputStates); + free(_analoguePinMap); + } + _analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1); + _analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + _analoguePinBytes = analogueBytesNeeded; + } + } else { + DIAG(F("EX-IOExpander I2C:%s ERROR configuring device"), _I2CAddress.toString()); + _deviceState = DEVSTATE_FAILED; + return; + } + } // We now need to retrieve the analogue pin map - _command1Buffer[0] = EXIOINITA; - I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); - // Attempt to get version, if we don't get it, we don't care, don't go offline - _command1Buffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); - _majorVer = _versionBuffer[0]; - _minorVer = _versionBuffer[1]; - _patchVer = _versionBuffer[2]; - DIAG(F("EX-IOExpander device found, I2C:%s, Version v%d.%d.%d"), - _i2cAddress.toString(), _versionBuffer[0], _versionBuffer[1], _versionBuffer[2]); + if (status == I2C_STATUS_OK) { + commandBuffer[0] = EXIOINITA; + status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1); + } + if (status == I2C_STATUS_OK) { + // Attempt to get version, if we don't get it, we don't care, don't go offline + uint8_t versionBuffer[3]; + commandBuffer[0] = EXIOVER; + if (I2CManager.read(_I2CAddress, versionBuffer, sizeof(versionBuffer), commandBuffer, 1) == I2C_STATUS_OK) { + _majorVer = versionBuffer[0]; + _minorVer = versionBuffer[1]; + _patchVer = versionBuffer[2]; + } + DIAG(F("EX-IOExpander device found, I2C:%s, Version v%d.%d.%d"), + _I2CAddress.toString(), _majorVer, _minorVer, _patchVer); + #ifdef DIAG_IO - _display(); + _display(); #endif + } + if (status != I2C_STATUS_OK) + reportError(status); + } else { - DIAG(F("EX-IOExpander device not found, I2C:%s"), _i2cAddress.toString()); + DIAG(F("EX-IOExpander I2C:%s device not found"), _I2CAddress.toString()); _deviceState = DEVSTATE_FAILED; } } - // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested. + // Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can + // be allocated from the stack to reduce RAM allocation. bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; int pin = vpin - _firstVpin; if (configType == CONFIGURE_INPUT) { - bool pullup = params[0]; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); - if (_command1Buffer[0] == EXIORDY) { - return true; - } else { - DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); - return false; - } - } else { + uint8_t pullup = params[0]; + uint8_t outBuffer[] = {EXIODPUP, (uint8_t)pin, pullup}; + uint8_t responseBuffer[1]; + uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer), + outBuffer, sizeof(outBuffer)); + if (status == I2C_STATUS_OK) { + if (responseBuffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("EXIOVpin %u cannot be used as a digital input pin"), (int)vpin); + } + } else + reportError(status); + } else if (configType == CONFIGURE_ANALOGINPUT) { + // TODO: Consider moving code from _configureAnalogIn() to here and remove _configureAnalogIn + // from IODevice class definition. Not urgent, but each virtual function defined + // means increasing the RAM requirement of every HAL device driver, whether it's relevant + // to the driver or not. return false; } + return false; } - // Analogue input pin configuration, used to enable on EX-IOExpander device + // Analogue input pin configuration, used to enable an EX-IOExpander device. + // Use I2C blocking calls and allocate buffers from stack to save RAM. int _configureAnalogIn(VPIN vpin) override { int pin = vpin - _firstVpin; - _command2Buffer[0] = EXIOENAN; - _command2Buffer[1] = pin; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _command2Buffer, 2); - if (_command1Buffer[0] == EXIORDY) { - return true; - } else { - DIAG(F("Vpin %d cannot be used as an analogue input pin"), (int)vpin); - return false; - } - return true; + uint8_t commandBuffer[] = {EXIOENAN, (uint8_t)pin}; + uint8_t responseBuffer[1]; + uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer), + commandBuffer, sizeof(commandBuffer)); + if (status == I2C_STATUS_OK) { + if (responseBuffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("EX-IOExpander: Vpin %u cannot be used as an analogue input pin"), (int)vpin); + } + } else + reportError(status); + + return false; } // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { - (void)currentMicros; // remove warning - if (_deviceState == DEVSTATE_FAILED) return; - _command1Buffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); - _command1Buffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); + if (_deviceState == DEVSTATE_FAILED) return; // If device failed, return + + // Request block is used for analogue and digital reads from the IOExpander, which are performed + // on a cyclic basis. Writes are performed synchronously as and when requested. + + if (_readState != RDS_IDLE) { + if (_i2crb.isBusy()) return; // If I2C operation still in progress, return + + uint8_t status = _i2crb.status; + if (status == I2C_STATUS_OK) { // If device request ok, read input data + + // First check if we need to process received data + if (_readState == RDS_ANALOGUE) { + // Read of analogue values was in progress, so process received values + // Here we need to copy the values from input buffer to the analogue value array. We need to + // do this to avoid tearing of the values (i.e. one byte of a two-byte value being changed + // while the value is being read). + memcpy(_analogueInputStates, _analogueInputBuffer, _analoguePinBytes); // Copy I2C input buffer to states + + } else if (_readState == RDS_DIGITAL) { + // Read of digital states was in progress, so process received values + // The received digital states are placed directly into the digital buffer on receipt, + // so don't need any further processing at this point (unless we want to check for + // changes and notify them to subscribers, to avoid the need for polling - see IO_GPIOBase.h). + } + } else + reportError(status, false); // report eror but don't go offline. + + _readState = RDS_IDLE; + } + + // If we're not doing anything now, check to see if a new input transfer is due. + if (_readState == RDS_IDLE) { + if (currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh + // Issue new read request for digital states. As the request is non-blocking, the buffer has to + // be allocated from heap (object state). + _readCommandBuffer[0] = EXIORDD; + I2CManager.read(_I2CAddress, _digitalInputStates, (_numDigitalPins+7)/8, _readCommandBuffer, 1, &_i2crb); + // non-blocking read + _lastDigitalRead = currentMicros; + _readState = RDS_DIGITAL; + } else if (currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh + // Issue new read for analogue input states + _readCommandBuffer[0] = EXIORDAN; + I2CManager.read(_I2CAddress, _analogueInputBuffer, + _numAnaloguePins * 2, _readCommandBuffer, 1, &_i2crb); + _lastAnalogueRead = currentMicros; + _readState = RDS_ANALOGUE; + } + } } + // Obtain the correct analogue input value, with reference to the analogue + // pin map. // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; - uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { if (_analoguePinMap[aPin] == pin) { - _pinLSBByte = aPin * 2; + uint8_t _pinLSBByte = aPin * 2; + uint8_t _pinMSBByte = _pinLSBByte + 1; + return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } } - uint8_t _pinMSBByte = _pinLSBByte + 1; - return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; + return -1; // pin not found in table } // Obtain the correct digital input value @@ -185,63 +283,102 @@ private: return value; } + // Write digital value. We could have an output buffer of states, that is periodically + // written to the device if there are any changes; this would reduce the I2C overhead + // if lots of output requests are being made. We could also cache the last value + // sent so that we don't write the same value over and over to the output. + // However, for the time being, we just write the current value (blocking I2C) to the + // IOExpander node. As it is a blocking request, we can use buffers allocated from + // the stack to save RAM allocation. void _write(VPIN vpin, int value) override { + uint8_t digitalOutBuffer[3]; + uint8_t responseBuffer[1]; if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIOWRD; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = value; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); - if (_command1Buffer[0] != EXIORDY) { - DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + digitalOutBuffer[0] = EXIOWRD; + digitalOutBuffer[1] = pin; + digitalOutBuffer[2] = value; + uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, digitalOutBuffer, 3); + if (status != I2C_STATUS_OK) { + reportError(status); + } else { + if (responseBuffer[0] != EXIORDY) { + DIAG(F("Vpin %u cannot be used as a digital output pin"), (int)vpin); + } } } + // Write analogue (integer) value. Write the parameters (blocking I2C) to the + // IOExpander node. As it is a blocking request, we can use buffers allocated from + // the stack to reduce RAM allocation. void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + uint8_t servoBuffer[7]; + uint8_t responseBuffer[1]; + if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; #ifdef DIAG_IO - DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + DIAG(F("Servo: WriteAnalogue Vpin:%u Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); #endif - _servoBuffer[0] = EXIOWRAN; - _servoBuffer[1] = pin; - _servoBuffer[2] = value & 0xFF; - _servoBuffer[3] = value >> 8; - _servoBuffer[4] = profile; - _servoBuffer[5] = duration & 0xFF; - _servoBuffer[6] = duration >> 8; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _servoBuffer, 7); - if (_command1Buffer[0] != EXIORDY) { - DIAG(F("Vpin %d cannot be used as a servo/PWM pin"), (int)vpin); + servoBuffer[0] = EXIOWRAN; + servoBuffer[1] = pin; + servoBuffer[2] = value & 0xFF; + servoBuffer[3] = value >> 8; + servoBuffer[4] = profile; + servoBuffer[5] = duration & 0xFF; + servoBuffer[6] = duration >> 8; + uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, servoBuffer, 7); + if (status != I2C_STATUS_OK) { + DIAG(F("EX-IOExpander I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status)); + _deviceState = DEVSTATE_FAILED; + } else { + if (responseBuffer[0] != EXIORDY) { + DIAG(F("Vpin %u cannot be used as a servo/PWM pin"), (int)vpin); + } } } + // Display device information and status. void _display() override { - DIAG(F("EX-IOExpander I2C:%s v%d.%d.%d Vpins %d-%d %S"), - _i2cAddress.toString(), _majorVer, _minorVer, _patchVer, + DIAG(F("EX-IOExpander I2C:%s v%d.%d.%d Vpins %u-%u %S"), + _I2CAddress.toString(), _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } - I2CAddress _i2cAddress; + // Helper function for error handling + void reportError(uint8_t status, bool fail=true) { + DIAG(F("EX-IOExpander I2C:%s Error:%d (%S)"), _I2CAddress.toString(), + status, I2CManager.getErrorMessage(status)); + if (fail) + _deviceState = DEVSTATE_FAILED; + } + uint8_t _numDigitalPins = 0; uint8_t _numAnaloguePins = 0; - byte _digitalOutBuffer[3]; - uint8_t _versionBuffer[3]; + uint8_t _majorVer = 0; uint8_t _minorVer = 0; uint8_t _patchVer = 0; - byte* _digitalInputStates; - byte* _analogueInputStates; - uint8_t _digitalPinBytes = 0; - uint8_t _analoguePinBytes = 0; - byte _command1Buffer[1]; - byte _command2Buffer[2]; - byte _command4Buffer[4]; - byte _receive3Buffer[3]; - byte _servoBuffer[7]; + + uint8_t* _digitalInputStates; + uint8_t* _analogueInputStates; + uint8_t* _analogueInputBuffer; // buffer for I2C input transfers + uint8_t _readCommandBuffer[1]; + + uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed) + uint8_t _analoguePinBytes = 0; // Size of allocated memory buffers (may be longer than needed) uint8_t* _analoguePinMap; + I2CRB _i2crb; + + enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states + uint8_t _readState = RDS_IDLE; + + unsigned long _lastDigitalRead = 0; + unsigned long _lastAnalogueRead = 0; + const unsigned long _digitalRefresh = 10000UL; // Delay refreshing digital inputs for 10ms + const unsigned long _analogueRefresh = 50000UL; // Delay refreshing analogue inputs for 50ms // EX-IOExpander protocol flags enum { diff --git a/IO_EXTurntable.h b/IO_EXTurntable.h index 02a87e3..40c02ee 100644 --- a/IO_EXTurntable.h +++ b/IO_EXTurntable.h @@ -103,7 +103,7 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_ uint8_t stepsMSB = value >> 8; uint8_t stepsLSB = value & 0xFF; #ifdef DIAG_IO - DIAG(F("EX-Turntable WriteAnalogue Vpin:%d Value:%d Activity:%d Duration:%d"), + DIAG(F("EX-Turntable WriteAnalogue VPIN:%u Value:%d Activity:%d Duration:%d"), vpin, value, activity, duration); DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"), _I2CAddress.toString(), stepsMSB, stepsLSB, activity); @@ -114,7 +114,7 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_ // Display Turnetable-EX device driver info. void EXTurntable::_display() { - DIAG(F("EX-Turntable I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), (int)_firstVpin, + DIAG(F("EX-Turntable I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin, (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_ExampleSerial.h b/IO_ExampleSerial.h index da421c5..5c70eb4 100644 --- a/IO_ExampleSerial.h +++ b/IO_ExampleSerial.h @@ -84,7 +84,7 @@ protected: void _write(VPIN vpin, int value) { int pin = vpin -_firstVpin; #ifdef DIAG_IO - DIAG(F("IO_ExampleSerial::_write Pin:%d Value:%d"), (int)vpin, value); + DIAG(F("IO_ExampleSerial::_write VPIN:%u Value:%d"), (int)vpin, value); #endif // Send a command string over the serial line _serial->print('#'); @@ -153,10 +153,10 @@ protected: // Display information about the device, and perhaps its current condition (e.g. active, disabled etc). // Here we display the current values held for the pins. void _display() { - DIAG(F("IO_ExampleSerial Configured on VPins:%d-%d"), (int)_firstVpin, + DIAG(F("IO_ExampleSerial Configured on Vpins:%u-%u"), (int)_firstVpin, (int)_firstVpin+_nPins-1); for (int i=0; i<_nPins; i++) - DIAG(F(" VPin %2d: %d"), _firstVpin+i, _pinValues[i]); + DIAG(F(" VPin %2u: %d"), _firstVpin+i, _pinValues[i]); } diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 66b9ff6..94265c3 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -196,7 +196,7 @@ void GPIOBase::_loop(unsigned long currentMicros) { template void GPIOBase::_display() { - DIAG(F("%S I2C:%s Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress.toString(), + DIAG(F("%S I2C:%s Configured on Vpins:%u-%u %S"), _deviceName, _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_HALDisplay.h b/IO_HALDisplay.h index 5ef9efc..f2ca3af 100644 --- a/IO_HALDisplay.h +++ b/IO_HALDisplay.h @@ -82,10 +82,10 @@ private: public: // Static function to handle "HALDisplay::create(...)" calls. static void create(I2CAddress i2cAddress, int width, int height) { - /* if (checkNoOverlap(i2cAddress)) */ new HALDisplay(0, i2cAddress, width, height); + if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(0, i2cAddress, width, height); } static void create(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) { - /* if (checkNoOverlap(i2cAddress)) */ new HALDisplay(displayNo, i2cAddress, width, height); + if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(displayNo, i2cAddress, width, height); } protected: diff --git a/IO_HCSR04.h b/IO_HCSR04.h index e7b5622..be1d303 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -30,7 +30,7 @@ * * 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 state returned by a read() call changes to 1. If + * 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 @@ -48,6 +48,20 @@ * 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. + * + * Example configuration: + * HCSR04::create(23000, 32, 33, 80, 85); + * + * Where 23000 is the VPIN allocated, + * 32 is the pin connected to the HCSR04 trigger terminal, + * 33 is the pin connected to the HCSR04 echo terminal, + * 80 is the distance in cm below which pin 23000 will be active, + * and 85 is the distance in cm above which pin 23000 will be inactive. + * + * Alternative configuration, which hogs the processor until the measurement is complete + * (old behaviour, more accurate but higher impact on other CS tasks): + * HCSR04::create(23000, 32, 33, 80, 85, HCSR04::LOOP); + * */ #ifndef IO_HCSR04_H @@ -61,38 +75,52 @@ private: // pins must be arduino GPIO pins, not extender pins or HAL pins. int _trigPin = -1; int _echoPin = -1; - // Thresholds for setting active state in cm. + // 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 + // Active=1/inactive=0 _state uint8_t _value = 0; - // Factor for calculating the distance (cm) from echo time (ms). + // Factor for calculating the distance (cm) from echo time (us). // Based on a speed of sound of 345 metres/second. - const uint16_t factor = 58; // ms/cm + const uint16_t factor = 58; // us/cm + // Limit the time spent looping by dropping out when the expected + // worst case threshold value is greater than an arbitrary value. + const uint16_t maxPermittedLoopTime = 10 * factor; // max in us + unsigned long _startTime = 0; + unsigned long _maxTime = 0; + enum {DORMANT, MEASURING}; // _state values + uint8_t _state = DORMANT; + uint8_t _counter = 0; + uint16_t _options = 0; public: + enum Options { + LOOP = 1, // Option HCSR04::LOOP reinstates old behaviour, i.e. complete measurement in one loop entry. + }; // Static create function provides alternative way to create object - static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options = 0) { if (checkNoOverlap(vpin)) - new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold); + new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold, options); } protected: - // Constructor perfroms static initialisation of the device object - HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + // Constructor performs static initialisation of the device object + HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options) { _firstVpin = vpin; _nPins = 1; _trigPin = trigPin; _echoPin = echoPin; _onThreshold = onThreshold; _offThreshold = offThreshold; + _options = options; addDevice(this); } // _begin function called to perform dynamic initialisation of the device void _begin() override { + _state = 0; pinMode(_trigPin, OUTPUT); pinMode(_echoPin, INPUT); ArduinoPins::fastWriteDigital(_trigPin, 0); @@ -112,78 +140,104 @@ protected: return _distance; } - // _loop function - read HC-SR04 once every 50 milliseconds. + // _loop function - read HC-SR04 once every 100 milliseconds. void _loop(unsigned long currentMicros) override { - read_HCSR04device(); - // Delay next loop entry until 50ms have elapsed. - delayUntil(currentMicros + 50000UL); + unsigned long waitTime; + switch(_state) { + case DORMANT: // Issue pulse + // If receive pin is still set on from previous call, do nothing till next entry. + if (ArduinoPins::fastReadDigital(_echoPin)) return; + + // Send 10us pulse to trigger transmitter + ArduinoPins::fastWriteDigital(_trigPin, 1); + delayMicroseconds(10); + ArduinoPins::fastWriteDigital(_trigPin, 0); + + // Wait, with timeout, for echo pin to become set. + // Measured time delay is just under 500us, so + // wait for max of 1000us. + _startTime = micros(); + _maxTime = 1000; + + while (!ArduinoPins::fastReadDigital(_echoPin)) { + // Not set yet, see if we've timed out. + waitTime = micros() - _startTime; + if (waitTime > _maxTime) { + // Timeout waiting for pulse start, abort the read and start again + _state = DORMANT; + return; + } + } + + // Echo pulse started, so wait for echo pin to reset, and measure length of pulse + _startTime = micros(); + _maxTime = factor * _offThreshold; + _state = MEASURING; + // If maximum measurement time is high, then skip until next loop entry before + // starting to look for pulse end. + // This gives better accuracy at shorter distance thresholds but without extending + // loop execution time for longer thresholds. If LOOP option is set on, then + // the entire measurement will be done in one loop entry, i.e. the code will fall + // through into the measuring phase. + if (!(_options & LOOP) && _maxTime > maxPermittedLoopTime) break; + /* fallthrough */ + + case MEASURING: // Check if echo pulse has finished + do { + waitTime = micros() - _startTime; + if (!ArduinoPins::fastReadDigital(_echoPin)) { + // Echo pulse completed; check if pulse length is below threshold and if so set value. + if (waitTime <= factor * _onThreshold) { + // Measured time is within the onThreshold, so value is one. + _value = 1; + // If the new distance value is less than the current, use it immediately. + // But if the new distance value is longer, then it may be erroneously long + // (because of extended loop times delays), so apply a delay to distance increases. + uint16_t estimatedDistance = waitTime / factor; + if (estimatedDistance < _distance) + _distance = estimatedDistance; + else + _distance += 1; // Just increase distance slowly. + _counter = 0; + //DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, _distance); + } + _state = DORMANT; + } else { + // Echo pulse hasn't finished, so check if maximum time has elapsed + // 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, value is provisionally zero. + // But don't change _value unless provisional value is zero for 10 consecutive measurements + if (_value == 1) { + if (++_counter >= 10) { + _value = 0; + _distance = 32767; + _counter = 0; + } + } + _state = DORMANT; // start again + } + } + // If there's lots of time remaining before the expected completion time, + // then exit and wait for next loop entry. Otherwise, loop until we finish. + // If option LOOP is set, then we loop until finished anyway. + uint32_t remainingTime = _maxTime - waitTime; + if (!(_options & LOOP) && remainingTime < maxPermittedLoopTime) return; + } while (_state == MEASURING) ; + break; + } + // Datasheet recommends a wait of at least 60ms between measurement cycles + if (_state == DORMANT) + delayUntil(currentMicros+60000UL); // wait 60ms till next measurement + } void _display() override { - DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"), + DIAG(F("HCSR04 Configured on VPIN:%u TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"), _firstVpin, _trigPin, _echoPin, _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 - // 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. - // - void read_HCSR04device() { - // uint16 enough to time up to 65ms - uint16_t startTime, waitTime = 0, currentTime, maxTime; - - // If receive pin is still set on from previous call, abort the read. - if (ArduinoPins::fastReadDigital(_echoPin)) - return; - - // Send 10us pulse to trigger transmitter - ArduinoPins::fastWriteDigital(_trigPin, 1); - delayMicroseconds(10); - ArduinoPins::fastWriteDigital(_trigPin, 0); - - // Wait for receive pin to be set - startTime = currentTime = micros(); - maxTime = factor * _offThreshold * 2; - while (!ArduinoPins::fastReadDigital(_echoPin)) { - // lastTime = currentTime; - currentTime = micros(); - waitTime = currentTime - startTime; - if (waitTime > maxTime) { - // Timeout waiting for pulse start, abort the read - return; - } - } - - // Wait for receive pin to reset, and measure length of pulse - startTime = currentTime = micros(); - maxTime = factor * _offThreshold; - while (ArduinoPins::fastReadDigital(_echoPin)) { - 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. - _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); - _distance = waitTime / factor; // in centimetres - if (_distance < _onThreshold) - _value = 1; - } - }; #endif //IO_HCSR04_H diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 71f3661..3c8b37a 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -46,7 +46,7 @@ bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i if (configType != CONFIGURE_SERVO) return false; if (paramCount != 5) return false; #ifdef DIAG_IO - DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + DIAG(F("PCA9685 Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), vpin, params[0], params[1], params[2], params[3], params[4]); #endif @@ -118,7 +118,7 @@ void PCA9685::_begin() { // 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); + DIAG(F("PCA9685 Write VPIN:%u Value:%d"), vpin, value); #endif int pin = vpin - _firstVpin; if (value) value = 1; @@ -145,7 +145,7 @@ void PCA9685::_write(VPIN vpin, int value) { // 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 Duration:%d %S"), + DIAG(F("PCA9685 WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); #endif if (_deviceState == DEVSTATE_FAILED) return; @@ -262,7 +262,7 @@ void PCA9685::writeDevice(uint8_t pin, int value) { // Display details of this device. void PCA9685::_display() { - DIAG(F("PCA9685 I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), (int)_firstVpin, + DIAG(F("PCA9685 I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin, (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_PCA9685pwm.h b/IO_PCA9685pwm.h index 042516b..a12cb84 100644 --- a/IO_PCA9685pwm.h +++ b/IO_PCA9685pwm.h @@ -121,7 +121,7 @@ private: void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { (void)param1; (void)param2; // suppress compiler warning #ifdef DIAG_IO - DIAG(F("PCA9685pwm WriteAnalogue Vpin:%d Value:%d %S"), + DIAG(F("PCA9685pwm WriteAnalogue VPIN:%u Value:%d %S"), vpin, value, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); #endif if (_deviceState == DEVSTATE_FAILED) return; @@ -134,7 +134,7 @@ private: // Display details of this device. void _display() override { - DIAG(F("PCA9685pwm I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), (int)_firstVpin, + DIAG(F("PCA9685pwm I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin, (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_RotaryEncoder.h b/IO_RotaryEncoder.h index b271b48..00a8249 100644 --- a/IO_RotaryEncoder.h +++ b/IO_RotaryEncoder.h @@ -98,13 +98,14 @@ private: void _write(VPIN vpin, int value) override { if (vpin == _firstVpin + 1) { - byte _feedbackBuffer[2] = {RE_OP, value}; + if (value != 0) value = 0x01; + byte _feedbackBuffer[2] = {RE_OP, (byte)value}; I2CManager.write(_I2CAddress, _feedbackBuffer, 2); } } void _display() override { - DIAG(F("Rotary Encoder I2C:%s v%d.%d.%d Configured on Vpin:%d-%d %S"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer, + DIAG(F("Rotary Encoder I2C:%s v%d.%d.%d Configured on VPIN:%u-%d %S"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer, (int)_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_Servo.h b/IO_Servo.h index 8f95463..9e3211e 100644 --- a/IO_Servo.h +++ b/IO_Servo.h @@ -98,7 +98,7 @@ private: if (configType != CONFIGURE_SERVO) return false; if (paramCount != 5) return false; #ifdef DIAG_IO - DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + DIAG(F("Servo: Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), vpin, params[0], params[1], params[2], params[3], params[4]); #endif @@ -140,12 +140,12 @@ private: // Get reference to slave device. _slaveDevice = findDevice(_firstSlavePin); if (!_slaveDevice) { - DIAG(F("Servo: Slave device not found on pins %d-%d"), + DIAG(F("Servo: Slave device not found on Vpins %u-%u"), _firstSlavePin, _firstSlavePin+_nPins-1); _deviceState = DEVSTATE_FAILED; } if (_slaveDevice != findDevice(_firstSlavePin+_nPins-1)) { - DIAG(F("Servo: Slave device does not cover all pins %d-%d"), + DIAG(F("Servo: Slave device does not cover all Vpins %u-%u"), _firstSlavePin, _firstSlavePin+_nPins-1); _deviceState = DEVSTATE_FAILED; } @@ -165,7 +165,7 @@ private: void _write(VPIN vpin, int value) override { if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO - DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); + DIAG(F("Servo Write VPIN:%u Value:%d"), vpin, value); #endif int pin = vpin - _firstVpin; if (value) value = 1; @@ -193,7 +193,7 @@ private: // void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { #ifdef DIAG_IO - DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + DIAG(F("Servo: WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); #endif if (_deviceState == DEVSTATE_FAILED) return; @@ -288,7 +288,7 @@ private: // Display details of this device. void _display() override { - DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), + DIAG(F("Servo Configured on Vpins:%u-%u, slave pins:%d-%d %S"), (int)_firstVpin, (int)_firstVpin+_nPins-1, (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); diff --git a/IO_TouchKeypad.h b/IO_TouchKeypad.h index 8984bce..48e3b25 100644 --- a/IO_TouchKeypad.h +++ b/IO_TouchKeypad.h @@ -124,7 +124,7 @@ protected: // Display information about the device, and perhaps its current condition (e.g. active, disabled etc). void _display() { - DIAG(F("TouchKeypad Configured on VPins:%d-%d SCL=%d SDO=%d"), (int)_firstVpin, + DIAG(F("TouchKeypad Configured on Vpins:%u-%u SCL=%d SDO=%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1, _clockPin, _dataPin); } diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h index 7c80518..08fe44e 100644 --- a/IO_VL53L0X.h +++ b/IO_VL53L0X.h @@ -319,7 +319,7 @@ protected: } void _display() override { - DIAG(F("VL53L0X I2C:%s Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"), + DIAG(F("VL53L0X I2C:%s Configured on Vpins:%u-%u On:%dmm Off:%dmm %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } diff --git a/IO_duinoNodes.h b/IO_duinoNodes.h index 73de1a1..c764db2 100644 --- a/IO_duinoNodes.h +++ b/IO_duinoNodes.h @@ -55,6 +55,7 @@ public: pinMode(_clockPin,OUTPUT); pinMode(_dataPin,_pinMap?INPUT_PULLUP:OUTPUT); _display(); + if (!_pinMap) _loopOutput(); } // loop called by HAL supervisor @@ -121,7 +122,7 @@ void _loopOutput() { } void _display() override { - DIAG(F("IO_duinoNodes %SPUT Configured on VPins:%d-%d shift=%d"), + DIAG(F("IO_duinoNodes %SPUT Configured on Vpins:%u-%u shift=%d"), _pinMap?F("IN"):F("OUT"), (int)_firstVpin, (int)_firstVpin+_nPins-1, _nShiftBytes*8); diff --git a/MotorDriver.cpp b/MotorDriver.cpp index 7415b36..b8068f3 100644 --- a/MotorDriver.cpp +++ b/MotorDriver.cpp @@ -1,8 +1,8 @@ /* - * © 2022 Paul M Antoine + * © 2022-2023 Paul M Antoine * © 2021 Mike S * © 2021 Fred Decker - * © 2020-2022 Harald Barth + * © 2020-2023 Harald Barth * © 2020-2021 Chris Harlow * All rights reserved. * @@ -27,10 +27,6 @@ #include "DCCTimer.h" #include "DIAG.h" -#if defined(ARDUINO_ARCH_ESP32) -#include "ESP32-fixes.h" -#endif - bool MotorDriver::commonFaultPin=false; volatile portreg_t shadowPORTA; @@ -75,6 +71,23 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i dualSignal=true; getFastPin(F("SIG2"),signalPin2,fastSignalPin2); pinMode(signalPin2, OUTPUT); + + fastSignalPin2.shadowinout = NULL; + if (HAVE_PORTA(fastSignalPin2.inout == &PORTA)) { + DIAG(F("Found PORTA pin %d"),signalPin2); + fastSignalPin2.shadowinout = fastSignalPin2.inout; + fastSignalPin2.inout = &shadowPORTA; + } + if (HAVE_PORTB(fastSignalPin2.inout == &PORTB)) { + DIAG(F("Found PORTB pin %d"),signalPin2); + fastSignalPin2.shadowinout = fastSignalPin2.inout; + fastSignalPin2.inout = &shadowPORTB; + } + if (HAVE_PORTC(fastSignalPin2.inout == &PORTC)) { + DIAG(F("Found PORTC pin %d"),signalPin2); + fastSignalPin2.shadowinout = fastSignalPin2.inout; + fastSignalPin2.inout = &shadowPORTC; + } } else dualSignal=false; @@ -123,8 +136,7 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i if (currentPin==UNUSED_PIN) DIAG(F("** WARNING ** No current or short detection")); else { - DIAG(F("CurrentPin=A%d, TripValue=%d"), - currentPin-A0, rawCurrentTripValue); + DIAG(F("Track %c, TripValue=%d"), trackLetter, rawCurrentTripValue); // self testing diagnostic for the non-float converters... may be removed when happy // DIAG(F("senseFactorInternal=%d raw2mA(1000)=%d mA2Raw(1000)=%d"), @@ -149,7 +161,7 @@ void MotorDriver::setPower(POWERMODE mode) { // when switching a track On, we need to check the crrentOffset with the pin OFF if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) { senseOffset = ADCee::read(currentPin); - DIAG(F("CurrentPin A%d sensOffset=%d"),currentPin-A0,senseOffset); + DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset); } IODevice::write(powerPin,invertPower ? LOW : HIGH); @@ -197,7 +209,10 @@ int MotorDriver::getCurrentRaw(bool fromISR) { (void)fromISR; if (currentPin==UNUSED_PIN) return 0; int current; - current = ADCee::read(currentPin, fromISR)-senseOffset; + current = ADCee::read(currentPin, fromISR); + // here one can diag raw value + // if (fromISR == false) DIAG(F("%c: %d"), trackLetter, current); + current = current-senseOffset; // adjust with offset if (current<0) current=0-current; if ((faultPin != UNUSED_PIN) && powerMode==POWERMODE::ON) { if (invertFault && isLOW(fastFaultPin)) @@ -274,7 +289,7 @@ void MotorDriver::setDCSignal(byte speedcode) { f = taurustones[ (tSpeed-2)/2 ] ; } } - DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup + DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup } #endif if (tSpeed <= 1) brake = 255; @@ -283,7 +298,7 @@ void MotorDriver::setDCSignal(byte speedcode) { if (invertBrake) brake=255-brake; #if defined(ARDUINO_ARCH_ESP32) - DCCEXanalogWrite(brakePin,brake); + DCCTimer::DCCEXanalogWrite(brakePin,brake); #else analogWrite(brakePin,brake); #endif diff --git a/MotorDriver.h b/MotorDriver.h index 5f36184..743e579 100644 --- a/MotorDriver.h +++ b/MotorDriver.h @@ -182,11 +182,15 @@ class MotorDriver { isProgTrack = on; } void checkPowerOverload(bool useProgLimit, byte trackno); + inline void setTrackLetter(char c) { + trackLetter = c; + }; #ifdef ANALOG_READ_INTERRUPT bool sampleCurrentFromHW(); void startCurrentFromHW(); #endif private: + char trackLetter = '?'; bool isProgTrack = false; // tells us if this is a prog track void getFastPin(const FSH* type,int pin, bool input, FASTPIN & result); void getFastPin(const FSH* type,int pin, FASTPIN & result) { diff --git a/MotorDrivers.h b/MotorDrivers.h index c066052..cc13f54 100644 --- a/MotorDrivers.h +++ b/MotorDrivers.h @@ -73,12 +73,12 @@ #elif defined(ARDUINO_ARCH_ESP32) // STANDARD shield on an ESPDUINO-32 (ESP32 in Uno form factor). The shield must be eiter the // 3.3V compatible R3 version or it has to be modified to not supply more than 3.3V to the -// analog inputs. Here we use analog inputs A4 and A5 as A0 and A1 are wired in a way so that +// analog inputs. Here we use analog inputs A2 and A3 as A0 and A1 are wired in a way so that // they are not useable at the same time as WiFi (what a bummer). The numbers below are the // actual GPIO numbers. In comments the numbers the pins have on an Uno. #define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \ - new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 36/*A4*/, 0.70, 1500, UNUSED_PIN), \ - new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 39/*A5*/, 0.70, 1500, UNUSED_PIN) + new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 0.70, 1500, UNUSED_PIN), \ + new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 0.70, 1500, UNUSED_PIN) #else // STANDARD shield on any Arduino Uno or Mega compatible with the original specification. diff --git a/Release_Notes/CommandRef.md b/Release_Notes/CommandRef.md index 6420471..6bade54 100644 --- a/Release_Notes/CommandRef.md +++ b/Release_Notes/CommandRef.md @@ -4,24 +4,37 @@ General points: - Commands below have a single character opcode and parameters. Even is actually read as - Keyword parameters are shown in upper case but may be entered in mixed case. - - value parameters are numeric. + - value parameters are decimal numeric (unless otherwise noted) - [something] indicates its optional. - - Not all commands have a response, and not all responses come from the last commands that you have issued. + - Not all commands have a response, and broadcasts mean that not all responses come from the last commands that you have issued. Startup status - + Return status like + + also returns defined turnout list: + 1=thrown -Track power management -<1> -<1 MAIN|PROG|JOIN> -<0> -<0 MAIN|PROG> +Track power management. After power commands a power state is broadcast to all throttles. + +<1> Power on all +<1 MAIN|PROG|JOIN> Power on MAIN or PROG track +<1 JOIN> Power on MAIN and PROG track but send main track data on both. +<0> Power off all tracks +<0 MAIN|PROG> Power off main or prog track + +Basic manual loco control + Throttle loco. + speed in JMRI-form (-1=ESTOP, 0=STOP, 1..126 = DCC speeds 2..127) + direction 1=forward, 0=reverse + For response see broadcast + + Set loco function 1=ON, 0-OFF + For response see broadcast + + emergency stop all locos + Control turnout id, 0=C=Closed, 1=T=Thrown + response broadcast -Basic manual control - - - - DCC accessory control @@ -35,25 +48,33 @@ Note: Turnouts are best defined in myAutomation.h where a turnout description ca + Valid commands respond with + +Direct pin manipulation (replaces Set pin HIGH + Set pin LOW + Set pin analog value + Set pin analog with profile + set pin analog with profile and value -Outputs - - +Sensors (Used by JMRI, not required by EXRAIL) + define a sensor to be monitored. + Responses and as sensor changes -Sensors - +Decoder programming - main track + POM write value to cv on loco + POM write bit to cv on loco -Decoder programming - - - - - - - - - +Decoder Programming - prog track + Clear consist and write new cab id (includes long/short settings) + Responds or for error + Write value to cv + + Read cv value, much faster if prediction is correct. + Read CV bit + + Read drive-away loco id. (May be a consist id) @@ -152,6 +173,11 @@ Obsolete commands/formats + V command is much faster if prediction is correct. + V command is much faster if prediction is correct. + (use (use setPower(POWERMODE::OFF); + track[t]->setTrackLetter('A'+t); lastTrack=t; } } @@ -203,6 +205,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr track[t]->setPower(POWERMODE::OFF); trackMode[t]=TRACK_MODE_OFF; track[t]->makeProgTrack(false); // revoke prog track special handling + streamTrackState(NULL,t); } track[trackToSet]->makeProgTrack(true); // set for prog track special handling } else { @@ -210,7 +213,8 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr } trackMode[trackToSet]=mode; trackDCAddr[trackToSet]=dcAddr; - + streamTrackState(NULL,trackToSet); + // When a track is switched, we must clear any side effects of its previous // state, otherwise trains run away or just dont move. @@ -290,36 +294,7 @@ bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[]) if (params==0) { // <=> List track assignments FOR_EACH_TRACK(t) - if (track[t]!=NULL) { - StringFormatter::send(stream,F("<= %c "),'A'+t); - switch(trackMode[t]) { - case TRACK_MODE_MAIN: - StringFormatter::send(stream,F("MAIN")); - if (track[t]->trackPWM) - StringFormatter::send(stream,F("+")); - break; - case TRACK_MODE_PROG: - StringFormatter::send(stream,F("PROG")); - if (track[t]->trackPWM) - StringFormatter::send(stream,F("+")); - break; - case TRACK_MODE_OFF: - StringFormatter::send(stream,F("OFF")); - break; - case TRACK_MODE_EXT: - StringFormatter::send(stream,F("EXT")); - break; - case TRACK_MODE_DC: - StringFormatter::send(stream,F("DC %d"),trackDCAddr[t]); - break; - case TRACK_MODE_DCX: - StringFormatter::send(stream,F("DCX %d"),trackDCAddr[t]); - break; - default: - break; // unknown, dont care - } - StringFormatter::send(stream,F(">\n")); - } + streamTrackState(stream,t); return true; } @@ -349,6 +324,36 @@ bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[]) return false; } +void TrackManager::streamTrackState(Print* stream, byte t) { + // null stream means send to commandDistributor for broadcast + if (track[t]==NULL) return; + auto format=F(""); + switch(trackMode[t]) { + case TRACK_MODE_MAIN: + format=F("<= %c MAIN>\n"); + break; + case TRACK_MODE_PROG: + format=F("<= %c PROG>\n"); + break; + case TRACK_MODE_OFF: + format=F("<= %c OFF>\n"); + break; + case TRACK_MODE_EXT: + format=F("<= %c EXT>\n"); + break; + case TRACK_MODE_DC: + format=F("<= %c DC %d>\n"); + break; + case TRACK_MODE_DCX: + format=F("<= %c DCX %d>\n"); + break; + default: + break; // unknown, dont care + } + if (stream) StringFormatter::send(stream,format,'A'+t,trackDCAddr[t]); + else CommandDistributor::broadcastTrackState(format,'A'+t,trackDCAddr[t]); +} + byte TrackManager::nextCycleTrack=MAX_TRACKS; void TrackManager::loop() { diff --git a/TrackManager.h b/TrackManager.h index 4a548cc..ef4a47c 100644 --- a/TrackManager.h +++ b/TrackManager.h @@ -80,6 +80,8 @@ class TrackManager { static void reportGauges(Print* stream); static void reportCurrent(Print* stream); static void reportObsoleteCurrent(Print* stream); + static void streamTrackState(Print* stream, byte t); + static int16_t joinRelay; static bool progTrackSyncMain; // true when prog track is a siding switched to main static bool progTrackBoosted; // true when prog track is not current limited diff --git a/Turnouts.cpp b/Turnouts.cpp index 582ea7d..e8f55af 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -110,49 +110,40 @@ /* static */ bool Turnout::setClosedStateOnly(uint16_t id, bool closeFlag) { Turnout *tt = get(id); if (!tt) return false; - tt->_turnoutData.closed = closeFlag; - // I know it says setClosedStateOnly, but we need to tell others - // that the state has changed too. - #if defined(EXRAIL_ACTIVE) - RMFT2::turnoutEvent(id, closeFlag); - #endif - - CommandDistributor::broadcastTurnout(id, closeFlag); + // that the state has changed too. But we only broadcast if there + // really has been a change. + if (tt->_turnoutData.closed != closeFlag) { + tt->_turnoutData.closed = closeFlag; + CommandDistributor::broadcastTurnout(id, closeFlag); + } +#if defined(EXRAIL_ACTIVE) + RMFT2::turnoutEvent(id, closeFlag); +#endif return true; } - +#define DIAG_IO // 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 // called from here. /* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) { - #if defined(DIAG_IO) - if (closeFlag) - DIAG(F("Turnout::close(%d)"), id); - else - DIAG(F("Turnout::throw(%d)"), id); - #endif +#if defined(DIAG_IO) + DIAG(F("Turnout(%d,%c)"), id, closeFlag ? 'c':'t'); +#endif Turnout *tt = Turnout::get(id); if (!tt) return false; bool ok = tt->setClosedInternal(closeFlag); if (ok) { - + tt->setClosedStateOnly(id, closeFlag); #ifndef DISABLE_EEPROM // 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, tt->_turnoutData.flags); #endif - - #if defined(EXRAIL_ACTIVE) - RMFT2::turnoutEvent(id, closeFlag); - #endif - - // Send message to JMRI etc. - CommandDistributor::broadcastTurnout(id, closeFlag); } return ok; } @@ -298,7 +289,6 @@ #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 @@ -396,7 +386,6 @@ // and Close writes a 0. // RCN-213 specifies that Throw is 0 and Close is 1. DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant); - _turnoutData.closed = close; return true; } @@ -472,7 +461,6 @@ bool VpinTurnout::setClosedInternal(bool close) { IODevice::write(_vpinTurnoutData.vpin, close); - _turnoutData.closed = close; return true; } @@ -523,7 +511,10 @@ 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. + // The _turnoutData.closed flag should be updated by a message from the LCN master. + // but in this implementation it is updated in setClosedStateOnly() instead. + // If the LCN master updates this, setClosedStateOnly() and all setClosedInternal() + // have to be updated accordingly so that the closed flag is only set once. return true; } diff --git a/WifiInterface.cpp b/WifiInterface.cpp index f69cc2f..21d474b 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -125,17 +125,18 @@ wifiSerialState WifiInterface::setup(Stream & setupStream, const FSH* SSid, con wifiState = setup2( SSid, password, hostname, port, channel); if (wifiState == WIFI_NOAT) { - DIAG(F("++ Wifi Setup NO AT ++")); - return wifiState; + LCD(4, F("WiFi no AT chip")); + return wifiState; } if (wifiState == WIFI_CONNECTED) { StringFormatter::send(wifiStream, F("ATE0\r\n")); // turn off the echo - checkForOK(200, true); + checkForOK(200, true); + DIAG(F("WiFi CONNECTED")); + // LCD already shows IP + } else { + LCD(4,F("WiFi DISCON.")); } - - - DIAG(F("++ Wifi Setup %S ++"), wifiState == WIFI_CONNECTED ? F("CONNECTED") : F("DISCONNECTED")); return wifiState; } diff --git a/config.example.h b/config.example.h index b253ab3..2a8a11a 100644 --- a/config.example.h +++ b/config.example.h @@ -125,10 +125,10 @@ The configuration file for DCC-EX Command Station // 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) +//OR define OLED_DRIVER width,height[,address] in pixels (address auto detected if not supplied) // 128x32 or 128x64 I2C SSD1306-based devices are supported. // Use 132,64 for a SH1106-based I2C device with a 128x64 display. -// #define OLED_DRIVER 128,32 +// #define OLED_DRIVER 128,32,0x3c // Define scroll mode as 0, 1 or 2 // * #define SCROLLMODE 0 is scroll continuous (fill screen if poss), diff --git a/config.h.txt b/config.h.txt new file mode 100644 index 0000000..98cc3fc --- /dev/null +++ b/config.h.txt @@ -0,0 +1,169 @@ +/********************************************************************** + +Config.h +COPYRIGHT (c) 2013-2016 Gregg E. Berman +COPYRIGHT (c) 2020 Fred Decker + +The configuration file for DCC++ EX Command Station + +**********************************************************************/ +///////////////////////////////////////////////////////////////////////////////////// +// NOTE: Before connecting these boards and selecting one in this software +// check the quick install guides!!! Some of these boards require a voltage +// generating resitor on the current sense pin of the device. Failure to select +// the correct resistor could damage the sense pin on your Arduino or destroy +// the device. +// +// DEFINE MOTOR_SHIELD_TYPE BELOW ACCORDING TO THE FOLLOWING TABLE: +// +// STANDARD_MOTOR_SHIELD : Arduino Motor shield Rev3 based on the L298 with 18V 2A per channel +// POLOLU_MOTOR_SHIELD : Pololu MC33926 Motor Driver (not recommended for prog track) +// FUNDUMOTO_SHIELD : Fundumoto Shield, no current sensing (not recommended, no short protection) +// FIREBOX_MK1 : The Firebox MK1 +// FIREBOX_MK1S : The Firebox MK1S +// | +// +-----------------------v +// +// #define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), +// new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 0.488, 1500, UNUSED_PIN), +// new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 0.488, 1500, UNUSED_PIN) + +#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD + +///////////////////////////////////////////////////////////////////////////////////// +// +// The IP port to talk to a WIFI or Ethernet shield. +// +#define IP_PORT 2560 + +///////////////////////////////////////////////////////////////////////////////////// +// +// NOTE: Only supported on Arduino Mega +// Set to false if you not even want it on the Arduino Mega +// +//#define ENABLE_WIFI true + +///////////////////////////////////////////////////////////////////////////////////// +// +// DEFINE WiFi Parameters (only in effect if WIFI is on) +// +// If DONT_TOUCH_WIFI_CONF is set, all WIFI config will be done with +// the <+> commands and this sketch will not change anything over +// AT commands and the other WIFI_* defines below do not have any effect. +//#define DONT_TOUCH_WIFI_CONF +// +// WIFI_SSID is the network name IF you want to use your existing home network. +// Do NOT change this if you want to use the WiFi in Access Point (AP) mode. +// +// If you do NOT set the WIFI_SSID, the WiFi chip will first try +// to connect to the previously configured network and if that fails +// fall back to Access Point mode. The SSID of the AP will be +// automatically set to DCCEX_*. +// +// Your SSID may not conain ``"'' (double quote, ASCII 0x22). +#define WIFI_SSID "Your network name" +// +// WIFI_PASSWORD is the network password for your home network or if +// you want to change the password from default AP mode password +// to the AP password you want. +// Your password may not conain ``"'' (double quote, ASCII 0x22). +#define WIFI_PASSWORD "deadcafe" +// +// WIFI_HOSTNAME: You probably don't need to change this +#define WIFI_HOSTNAME "dccex" +// +///////////////////////////////////////////////////////////////////////////////////// +// +// Wifi connect timeout in milliseconds. Default is 14000 (14 seconds). You only need +// to set this if you have an extremely slow Wifi router. +// +#define WIFI_CONNECT_TIMEOUT 14000 + +///////////////////////////////////////////////////////////////////////////////////// +// +// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired). This +// is not for Wifi. You will then need the Arduino Ethernet library as well +// +//#define ENABLE_ETHERNET true + + +///////////////////////////////////////////////////////////////////////////////////// +// +// DEFINE STATIC IP ADDRESS *OR* COMMENT OUT TO USE DHCP +// +//#define IP_ADDRESS { 192, 168, 1, 31 } + +///////////////////////////////////////////////////////////////////////////////////// +// +// DEFINE MAC ADDRESS ARRAY FOR ETHERNET COMMUNICATIONS INTERFACE +// +// Uncomment to use with Ethernet Shields +// +// Ethernet Shields do not have have a MAC address in hardware. There may be one on +// a sticker on the Shield that you should use. Otherwise choose one of the ones below +// Be certain that no other device on your network has this same MAC address! +// +// 52:b8:8a:8e:ce:21 +// e3:e9:73:e1:db:0d +// 54:2b:13:52:ac:0c + +// NOTE: This is not used with ESP8266 WiFi modules. + +//#define MAC_ADDRESS { 0x52, 0xB8, 0x8A, 0x8E, 0xCE, 0x21 } // MAC address of your networking card found on the sticker on your card or take one from above + +// +// #define MAC_ADDRESS { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEF } + +///////////////////////////////////////////////////////////////////////////////////// +// +// 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', +// OR an I2C Oled screen based on SSD1306 (128x64 or 128x32) controller, +// OR an I2C Oled screen based on SH1106 (132x64) controller. +// To enable, uncomment one of the lines below + +// define LCD_DRIVER for I2C LCD address 0x3f,16 cols, 2 rows +//#define LCD_DRIVER {SubBus_4,0x27},20,4 + +//OR define OLED_DRIVER width,height in pixels (address auto detected) +#if defined(ARDUINO_ARCH_STM32) +#define OLED_DRIVER 0x3c, 128, 64 +#else +#define OLED_DRIVER {SubBus_0,0x3c}, 128, 32 +#endif + +#define SCROLLMODE 1 + +///////////////////////////////////////////////////////////////////////////////////// +// DISABLE EEPROM +// +// If you do not need the EEPROM at all, you can disable all the code that saves +// data in the EEPROM. You might want to do that if you are in a Arduino UNO +// and want to use the EX-RAIL automation. Otherwise you do not have enough RAM +// to do that. Of course, then none of the EEPROM related commands work. +// +#define DISABLE_EEPROM + + +///////////////////////////////////////////////////////////////////////////////////// +// +// 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 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 + +// 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 + +///////////////////////////////////////////////////////////////////////////////////// diff --git a/install_via_powershell.cmd b/install_via_powershell.cmd new file mode 100644 index 0000000..c3a7e03 --- /dev/null +++ b/install_via_powershell.cmd @@ -0,0 +1,13 @@ +@ECHO OFF + +FOR /f "tokens=*" %%a IN ('powershell Get-ExecutionPolicy -Scope CurrentUser') DO SET PS_POLICY=%%a + +IF NOT %PS_POLICY=="Bypass" ( + powershell Set-ExecutionPolicy -Scope CurrentUser Bypass +) + +powershell %~dp0%installer.ps1 + +IF NOT %PS_POLICY=="Bypass" ( + powershell Set-ExecutionPolicy -Scope CurrentUser %PS_POLICY% +) diff --git a/installer.ps1 b/installer.ps1 new file mode 100644 index 0000000..b690824 --- /dev/null +++ b/installer.ps1 @@ -0,0 +1,540 @@ +<# +# © 2023 Peter Cole +# +# This file is part of EX-CommandStation +# +# 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 . +#> + +<############################################ +For script errors set ExecutionPolicy: +Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Bypass +############################################> + +<############################################ +Optional command line parameters: + $buildDirectory - specify an existing directory rather than generating a new unique one + $configDirectory - specify a directory containing existing files as per $configFiles +############################################> +Param( + [Parameter()] + [String]$buildDirectory, + [Parameter()] + [String]$configDirectory +) + +<############################################ +Define global parameters here such as known URLs etc. +############################################> +$installerVersion = "v0.0.8" +$configFiles = @("config.h", "myAutomation.h", "myHal.cpp", "mySetup.h") +$wifiBoards = @("arduino:avr:mega", "esp32:esp32:esp32") +$userDirectory = $env:USERPROFILE + "\" +$gitHubAPITags = "https://api.github.com/repos/DCC-EX/CommandStation-EX/git/refs/tags" +$gitHubURLPrefix = "https://github.com/DCC-EX/CommandStation-EX/archive/" +if ((Get-WmiObject win32_operatingsystem | Select-Object osarchitecture).osarchitecture -eq "64-bit") { + $arduinoCLIURL = "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Windows_64bit.zip" + $arduinoCLIZip = $userDirectory + "Downloads\" + "arduino-cli_latest_Windows_64bit.zip" +} else { + $arduinoCLIURL = "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Windows_32bit.zip" + $arduinoCLIZip = $userDirectory + "Downloads\" + "arduino-cli_latest_Windows_32bit.zip" +} +$arduinoCLIDirectory = $userDirectory + "arduino-cli" +$arduinoCLI = $arduinoCLIDirectory + "\arduino-cli.exe" + +<############################################ +List of supported devices with FQBN in case clones used that aren't detected +############################################> +$supportedDevices = @( + @{ + name = "Arduino Mega or Mega 2560" + fqbn = "arduino:avr:mega" + }, + @{ + name = "Arduino Nano" + fqbn = "arduino:avr:nano" + }, + @{ + name = "Arduino Uno" + fqbn = "arduino:avr:uno" + }, + @{ + name = "ESP32 Dev Module" + fqbn = "esp32:esp32:esp32" + } +) + +<############################################ +List of supported displays +############################################> +$displayList = @( + @{ + option = "LCD 16 columns x 2 rows" + configLine = "#define LCD_DRIVER 0x27,16,2" + }, + @{ + option = "LCD 16 columns x 4 rows" + configLine = "#define LCD_DRIVER 0x27,16,4" + }, + @{ + option = "OLED 128 x 32" + configLine = "#define OLED_DRIVER 128,32" + }, + @{ + option = "OLED 128 x 64" + configLine = "#define OLED_DRIVER 128,64" + } +) + +<############################################ +Basics of config.h +############################################> +$configLines = @( + "/*", + "This config.h file was generated by the DCC-EX PowerShell installer $installerVersion", + "*/", + "", + "// Define standard motor shield", + "#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD", + "" +) + +<############################################ +Set default action for progress indicators, warnings, and errors +############################################> +$global:ProgressPreference = "SilentlyContinue" +$global:WarningPreference = "SilentlyContinue" +$global:ErrorActionPreference = "SilentlyContinue" + +<############################################ +If $buildDirectory not provided, generate a new time/date stamp based directory to use +############################################> +if (!$PSBoundParameters.ContainsKey('buildDirectory')) { + $buildDate = Get-Date -Format 'yyyyMMdd-HHmmss' + $buildDirectory = $userDirectory + "EX-CommandStation-Installer\" + $buildDate +} +$commandStationDirectory = $buildDirectory + "\CommandStation-EX" + +<############################################ +Write out intro message and prompt to continue +############################################> +@" +Welcome to the DCC-EX PowerShell installer for EX-CommandStation ($installerVersion) + +Current installer options: + +- EX-CommandStation will be built in $commandStationDirectory +- Arduino CLI will downloaded and extracted to $arduinoCLIDirectory + +Before continuing, please ensure: + +- Your computer is connected to the internet +- The device you wish to install EX-CommandStation on is connected to a USB port + +This installer will obtain the Arduino CLI (if not already present), and then download and install your chosen version of EX-CommandStation + +"@ + +<############################################ +Prompt user to confirm all is ready to proceed +############################################> +$confirmation = Read-Host "Enter 'Y' or 'y' then press to confirm you are ready to proceed, any other key to exit" +if ($confirmation -ne "Y" -and $confirmation -ne "y") { + Exit +} + +<############################################ +See if we have the Arduino CLI already, otherwise download and extract it +############################################> +if (!(Test-Path -PathType Leaf -Path $arduinoCLI)) { + if (!(Test-Path -PathType Container -Path $arduinoCLIDirectory)) { + try { + New-Item -ItemType Directory -Path $arduinoCLIDirectory | Out-Null + } + catch { + Write-Output "Arduino CLI does not exist and cannot create directory $arduinoCLIDirectory" + Exit + } + } + Write-Output "`r`nDownloading and extracting Arduino CLI" + try { + Invoke-WebRequest -Uri $arduinoCLIURL -OutFile $arduinoCLIZip + } + catch { + Write-Output "Failed to download Arduino CLI" + Exit + } + try { + Expand-Archive -Path $arduinoCLIZip -DestinationPath $arduinoCLIDirectory -Force + } + catch { + Write-Output "Failed to extract Arduino CLI" + } +} else { + Write-Output "`r`nArduino CLI already downloaded, ensuring it is up to date and you have a board connected" +} + +<############################################ +Make sure Arduino CLI core index updated and list of boards populated +############################################> +# Need to do an initial board list to download everything first +try { + & $arduinoCLI core update-index | Out-Null +} +catch { + Write-Output "Failed to update Arduino CLI core index" + Exit +} +# Need to do an initial board list to download everything first +try { + & $arduinoCLI board list | Out-Null +} +catch { + Write-Output "Failed to update Arduino CLI board list" + Exit +} + +<############################################ +Identify available board(s) +############################################> +try { + $boardList = & $arduinoCLI board list --format jsonmini | ConvertFrom-Json +} +catch { + Write-Output "Failed to obtain list of boards" + Exit +} + +<############################################ +Get user to select board +############################################> +if ($boardList.count -eq 0) { + Write-Output "Could not find any attached devices, please ensure your device is plugged in to a USB port and Windows recognises it" + Exit +} else { +@" + +Devices attached to COM ports: +------------------------------ +"@ + + $boardSelect = 1 + foreach ($board in $boardList) { + if ($board.matching_boards.name) { + $boardName = $board.matching_boards.name + } else { + $boardName = "Unknown device" + } + $port = $board.port.address + Write-Output "$boardSelect - $boardName on port $port" + $boardSelect++ + } + Write-Output "$boardSelect - Exit" + $userSelection = 0 + do { + [int]$userSelection = Read-Host "`r`nSelect the device to use from the list above" + } until ( + (($userSelection -ge 1) -and ($userSelection -le ($boardList.count + 1))) + ) + if ($userSelection -eq ($boardList.count + 1)) { + Write-Output "Exiting installer" + Exit + } else { + $selectedBoard = $userSelection - 1 + } +} + +<############################################ +If the board is unknown, need to choose which one +############################################> +if ($null -eq $boardList[$selectedBoard].matching_boards.name) { + Write-Output "The device selected is unknown, these boards are supported:`r`n" + $deviceSelect = 1 + foreach ($device in $supportedDevices) { + Write-Output "$deviceSelect - $($supportedDevices[$deviceSelect - 1].name)" + $deviceSelect++ + } + Write-Output "$deviceSelect - Exit" + $userSelection = 0 + do { + [int]$userSelection = Read-Host "Select the board type from the list above" + } until ( + (($userSelection -ge 1) -and ($userSelection -le ($supportedDevices.count + 1))) + ) + if ($userSelection -eq ($supportedDevices.count + 1)) { + Write-Output "Exiting installer" + Exit + } else { + $deviceName = $supportedDevices[$userSelection - 1].name + $deviceFQBN = $supportedDevices[$userSelection - 1].fqbn + $devicePort = $boardList[$selectedBoard].port.address + } +} else { + $deviceName = $boardList[$selectedBoard].matching_boards.name + $deviceFQBN = $boardList[$selectedBoard].matching_boards.fqbn + $devicePort = $boardList[$selectedBoard].port.address +} + +<############################################ +Get the list of tags +############################################> +try { + $gitHubTags = Invoke-RestMethod -Uri $gitHubAPITags +} +catch { + Write-Output "Failed to obtain list of available EX-CommandStation versions" + Exit +} + +<############################################ +Get our GitHub tag list in a hash so we can sort by version numbers and extract just the ones we want +############################################> +$versionMatch = ".*?v(\d+)\.(\d+).(\d+)-(.*)" +$tagList = @{} +foreach ($tag in $gitHubTags) { + $tagHash = @{} + $tagHash["Ref"] = $tag.ref + $version = $tag.ref.split("/")[2] + $null = $version -match $versionMatch + $tagHash["Major"] = [int]$Matches[1] + $tagHash["Minor"] = [int]$Matches[2] + $tagHash["Patch"] = [int]$Matches[3] + $tagHash["Type"] = $Matches[4] + $tagList.Add($version, $tagHash) +} + +<############################################ +Get latest two Prod and Devel for user to select +############################################> +$userList = @{} +$prodCount = 1 +$devCount = 1 +$select = 1 +foreach ($tag in $tagList.Keys | Sort-Object {$tagList[$_]["Major"]},{$tagList[$_]["Minor"]},{$tagList[$_]["Patch"]} -Descending) { + if (($tagList[$tag]["Type"] -eq "Prod") -and $prodCount -le 2) { + $userList[$select] = $tag + $select++ + $prodCount++ + } elseif (($tagList[$tag]["Type"] -eq "Devel") -and $devCount -le 2) { + $userList[$select] = $tag + $select++ + $devCount++ + } +} + +<############################################ +Display options for user to select and get the selection +############################################> +@" + +Available EX-CommandStation versions: +------------------------------------- +"@ +foreach ($selection in $userList.Keys | Sort-Object $selection) { + Write-Output "$selection - $($userList[$selection])" +} +Write-Output "5 - Exit" +$userSelection = 0 +do { + [int]$userSelection = Read-Host "`r`nSelect the version to install from the list above (1 - 5)" +} until ( + (($userSelection -ge 1) -and ($userSelection -le 5)) +) +if ($userSelection -eq 5) { + Write-Output "Exiting installer" + Exit +} else { + $downloadURL = $gitHubURLPrefix + $tagList[$userList[$userSelection]]["Ref"] + ".zip" +} + +<############################################ +Create build directory if it doesn't exist, or fail +############################################> +if (!(Test-Path -PathType Container -Path $buildDirectory)) { + try { + New-Item -ItemType Directory -Path $buildDirectory | Out-Null + } + catch { + Write-Output "Could not create build directory $buildDirectory" + Exit + } +} + +<############################################ +Download the chosen version to the build directory +############################################> +$downladFile = $buildDirectory + "\CommandStation-EX.zip" +Write-Output "Downloading and extracting $($userList[$userSelection])" +try { + Invoke-WebRequest -Uri $downloadURL -OutFile $downladFile +} +catch { + Write-Output "Error downloading EX-CommandStation zip file" + Exit +} + +<############################################ +If folder exists, bail out and tell user +############################################> +if (Test-Path -PathType Container -Path "$buildDirectory\CommandStation-EX") { + Write-Output "EX-CommandStation directory already exists, please ensure you have copied any user files then delete manually: $buildDirectory\CommandStation-EX" + Exit +} + +<############################################ +Extract and rename to CommandStation-EX to allow building +############################################> +try { + Expand-Archive -Path $downladFile -DestinationPath $buildDirectory -Force +} +catch { + Write-Output "Failed to extract EX-CommandStation zip file" + Exit +} + +$folderName = $buildDirectory + "\CommandStation-EX-" + ($userList[$userSelection] -replace "^v", "") +try { + Rename-Item -Path $folderName -NewName $commandStationDirectory +} +catch { + Write-Output "Could not rename folder" + Exit +} + +<############################################ +If config directory provided, copy files here +############################################> +if ($PSBoundParameters.ContainsKey('configDirectory')) { + if (Test-Path -PathType Container -Path $configDirectory) { + foreach ($file in $configFiles) { + if (Test-Path -PathType Leaf -Path "$configDirectory\$file") { + Copy-Item -Path "$configDirectory\$file" -Destination "$commandStationDirectory\$file" + } + } + } else { + Write-Output "User provided configuration directory $configDirectory does not exist, skipping" + } +} else { + +<############################################ +If no config directory provided, prompt for display option +############################################> + Write-Output "`r`nIf you have an LCD or OLED display connected, you can configure it here`r`n" + Write-Output "1 - I have no display, skip this step" + $displaySelect = 2 + foreach ($display in $displayList) { + Write-Output "$displaySelect - $($displayList[$displaySelect - 2].option)" + $displaySelect++ + } + Write-Output "$($displayList.Count + 2) - Exit" + do { + [int]$displayChoice = Read-Host "`r`nSelect a display option" + } until ( + ($displayChoice -ge 1 -and $displayChoice -le ($displayList.Count + 2)) + ) + if ($displayChoice -eq ($displayList.Count + 2)) { + Exit + } elseif ($displayChoice -ge 2) { + $configLines+= "// Display configuration" + $configLines+= "$($displayList[$displayChoice - 2].configLine)" + $configLines+= "#define SCROLLMODE 1 // Alternate between pages" + } +<############################################ +If device supports WiFi, prompt to configure +############################################> + if ($wifiBoards.Contains($deviceFQBN)) { + Write-Output "`r`nYour chosen board supports WiFi`r`n" + Write-Output "1 - I don't want WiFi, skip this step +2 - Configure my device as an access point I will connect to directly +3 - Configure my device to connect to my home WiFi network +4 - Exit" + do { + [int]$wifiChoice = Read-Host "`r`nSelect a WiFi option" + } until ( + ($wifiChoice -ge 1 -and $wifiChoice -le 4) + ) + if ($wifiChoice -eq 4) { + Exit + } elseif ($wifiChoice -ne 1) { + $configLines+= "" + $configLines+= "// WiFi configuration" + $configLines+= "#define ENABLE_WIFI true" + $configLines+= "#define IP_PORT 2560" + $configLines+= "#define WIFI_HOSTNAME ""dccex""" + $configLines+= "#define WIFI_CHANNEL 1" + if ($wifiChoice -eq 2) { + $configLines+= "#define WIFI_SSID ""Your network name""" + $configLines+= "#define WIFI_PASSWORD ""Your network passwd""" + } + if ($wifiChoice -eq 3) { + $wifiSSID = Read-Host "Please enter the SSID of your home network here" + $wifiPassword = Read-Host "Please enter your home network WiFi password here" + $configLines+= "#define WIFI_SSID ""$($wifiSSID)""" + $configLines+= "#define WIFI_PASSWORD ""$($wifiPassword)""" + } + } + } + +<############################################ +Write out config.h to a file here only if config directory not provided +############################################> + $configH = $commandStationDirectory + "\config.h" + try { + $configLines | Out-File -FilePath $configH -Encoding ascii + } + catch { + Write-Output "Error writing config file to $configH" + Exit + } +} + +<############################################ +Install core libraries for the platform +############################################> +$platformArray = $deviceFQBN.split(":") +$platform = $platformArray[0] + ":" + $platformArray[1] +try { + & $arduinoCLI core install $platform +} +catch { + Write-Output "Error install core libraries" + Exit +} + +<############################################ +Upload the sketch to the selected board +############################################> +#$arduinoCLI upload -b fqbn -p port $commandStationDirectory +Write-Output "Compiling and uploading to $deviceName on $devicePort" +try { + $output = & $arduinoCLI compile -b $deviceFQBN -u -t -p $devicePort $commandStationDirectory --format jsonmini | ConvertFrom-Json +} +catch { + Write-Output "Failed to compile" + Exit +} +if ($output.success -eq "True") { + Write-Output "`r`nCongratulations! DCC-EX EX-CommandStation $($userList[$userSelection]) has been installed on your $deviceName`r`n" +} else { + Write-Output "`r`nThere was an error installing $($userList[$userSelection]) on your $($deviceName), please take note of the errors provided:`r`n" + if ($null -ne $output.compiler_err) { + Write-Output "Compiler error: $($output.compiler_err)`r`n" + } + if ($null -ne $output.builder_result) { + Write-Output "Builder result: $($output.builder_result)`r`n" + } +} + +Write-Output "`r`nPress any key to exit the installer" +[void][System.Console]::ReadKey($true) diff --git a/installer.sh b/installer.sh index 98de771..17a9c04 100755 --- a/installer.sh +++ b/installer.sh @@ -69,10 +69,10 @@ else # need to do this config better cp -p config.example.h config.h fi -need curl if test -x "$ACLI" ; then : all well else + need curl curl "$ACLIINSTALL" > acliinstall.sh chmod +x acliinstall.sh ./acliinstall.sh diff --git a/myAutomation.example.h b/myAutomation.example.h index 1382a52..8338452 100644 --- a/myAutomation.example.h +++ b/myAutomation.example.h @@ -23,7 +23,9 @@ * */ -// This is the startup sequence, AKA SEQUENCE(0) +// This is the startup sequence, +AUTOSTART +POWERON // turn on track power 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. diff --git a/myHal.cpp.txt b/myHal.cpp.txt new file mode 100644 index 0000000..b5b612e --- /dev/null +++ b/myHal.cpp.txt @@ -0,0 +1,465 @@ +#include "defines.h" +#include "IODevice.h" + +#ifndef IO_NO_HAL + +#include "IO_VL53L0X.h" +#include "IO_HCSR04.h" +#include "Sensors.h" +#include "Turnouts.h" +#include "IO_DFPlayer.h" +//#include "IO_Wire.h" +#include "IO_AnalogueInputs.h" +#if __has_include("IO_Servo.h") +#include "IO_Servo.h" +#include "IO_PCA9685pwm.h" +#endif + +#include "IO_HALDisplay.h" +#include "LiquidCrystal_I2C.h" + +#if __has_include("IO_CMRI.h") +#include "IO_CMRI.h" +#endif + +//#include "IO_ExampleSerial.h" + +//#include "IO_EXFastclock.h" +//#include "IO_EXTurntable.h" + +#if __has_include("IO_ExternalEEPROM.h") +#include "IO_ExternalEEPROM.h" +#endif + +#if __has_include("IO_Network.h") +#include "IO_Network.h" +#include "Net_RF24.h" +#include "Net_ENC28J60.h" +#include "Net_Ethernet.h" +#define NETWORK_PRESENT +#endif + +#include "IO_TouchKeypad.h" + +#define WIRE_TEST 0 +#define TESTHARNESS 1 +#define I2C_STRESS_TEST 0 +#define I2C_SETCLOCK 0 + +#include "DCC.h" + + +#if 0 // Long Strings +#define s10 "0123456789" +#define s100 s10 s10 s10 s10 s10 s10 s10 s10 s10 s10 +#define s1k s100 s100 s100 s100 s100 s100 s100 s100 s100 s100 +#define s10k s1k s1k s1k s1k s1k s1k s1k s1k s1k s1k +#define s32k s10k s10k s10k s1k s1k +volatile const char PROGMEM ss1[] = s32k; +#endif + + +#if TESTHARNESS + +// Function to be invoked by test harness +void myTest() { + // DIAG(F("VL53L0X #1 Test: dist=%d signal=%d ambient=%d value=%d"), + // IODevice::readAnalogue(5000), + // IODevice::readAnalogue(5001), + // IODevice::readAnalogue(5002), + // IODevice::read(5000)); + // DIAG(F("VL53L0X #2 Test: dist=%d signal=%d ambient=%d value=%d"), + // IODevice::readAnalogue(5003), + // IODevice::readAnalogue(5004), + // IODevice::readAnalogue(5005), + // IODevice::read(5003)); + // DIAG(F("HCSR04 Test: dist=%d value=%d"), + // IODevice::readAnalogue(2000), + // IODevice::read(2000)); + // DIAG(F("ADS111x Test: %d %d %d %d %d"), + // IODevice::readAnalogue(4500), + // IODevice::readAnalogue(4501), + // IODevice::readAnalogue(4502), + // IODevice::readAnalogue(4503), + // IODevice::readAnalogue(A5) + // ); + // DIAG(F("RF24 Test: 4000:%d 4002:%d"), + // IODevice::read(4000), + // IODevice::read(4002) + // ); + DIAG(F("EXPANDER: 2212:%d 2213:%d 2214:%d"), + IODevice::readAnalogue(2212), + IODevice::readAnalogue(2213), + IODevice::readAnalogue(2214)); +} +#endif + +#if I2C_STRESS_TEST +static bool initialised = false; +static uint8_t lastStatus = 0; +static const int nRBs = 3; // request blocks concurrently +static const int I2cTestPeriod = 1; // milliseconds +static I2CAddress testDevice = {SubBus_6, 0x27}; +static I2CRB rb[nRBs]; +static uint8_t readBuffer[nRBs*32]; // nRB x 32-byte input buffer +static uint8_t writeBuffer[nRBs]; // nRB x 1-byte output buffer +static unsigned long count = 0; +static unsigned long errors = 0; +static unsigned long lastOutput = millis(); + +void I2CTest() { + if (!initialised) { + // I2C Loading for stress test. + // Write value then read back 32 times + for (int i=0; i 60000) { // 1 minute + DIAG(F("I2CTest: Count=%l Errors=%l"), count, errors); + count = errors = 0; + lastOutput = millis(); + } +} +#endif + +void updateLocoScreen() { + for (int i=0; i<8; i++) { + if (DCC::speedTable[i].loco > 0) { + int speed = DCC::speedTable[i].speedCode; + char direction = (speed & 0x80) ? 'R' : 'F'; + speed = speed & 0x7f; + if (speed > 0) speed = speed - 1; + SCREEN(3, i, F("Loco:%4d %3d %c"), DCC::speedTable[i].loco, + speed, direction); + } + } +} + +void updateTime() { + uint8_t buffer[20]; + I2CAddress rtc = {SubBus_1, 0x68}; // Real-time clock I2C address + buffer[0] = 0; + + // Set time - only needs to be done once if battery is ok. + static bool timeSet = false; + if (!timeSet) { + // I2CManager.read(rtc, buffer+1, sizeof(buffer)-1); + // uint8_t year = 23; // 2023 + // uint8_t day = 2; // tuesday + // uint8_t date = 21; // 21st + // uint8_t month = 2; // feb + // uint8_t hours = 23; // xx: + // uint8_t minutes = 25; // :xx + // buffer[1] = 0; // seconds + // buffer[2] = ((minutes / 10) << 4) | (minutes % 10); + // buffer[3] = ((hours / 10) << 4) | (hours % 10); + // buffer[4] = day; + // buffer[5] = ((date/10) << 4) + date%10; // 24th + // buffer[6] = ((month/10) << 4) + month%10; // feb + // buffer[7] = ((year/10) << 4) + year%10; // xx23 + // for (uint8_t i=8; i> 4; + uint8_t seconds1 = buffer[1] & 0xf; + uint8_t minutes10 = buffer[2] >> 4; + uint8_t minutes1 = buffer[2] & 0xf; + uint8_t hours10 = buffer[3] >> 4; + uint8_t hours1 = buffer[3] & 0xf; + SCREEN(10, 0, F("Departures %d%d:%d%d:%d%d"), + hours10, hours1, minutes10, minutes1, seconds10, seconds1); + } +} + +void showCharacterSet() { + if (millis() < 3000) return; + const uint8_t lineLen = 20; + char buffer[lineLen+1]; + static uint8_t nextChar = 0x20; + for (uint8_t row=0; row<8; row+=1) { + for (uint8_t col=0; col::create(10, {SubBus_5, 0x3c}, 132, 64); // SH1106 + // UserAddin::create(updateLocoScreen, 1000); + // UserAddin::create(showCharacterSet, 5000); + // UserAddin::create(updateTime, 1000); + + HALDisplay::create(10, {SubBus_4, 0x3c}, 128, 32); + HALDisplay::create(10, {SubBus_7, 0x3c}, 128, 32); + + //HALDisplay::create(10, {SubBus_4, 0x27}, 20, 4); + + // Draw double boxes with X O O X inside. + // SCREEN(3, 2, F("\xc9\xcd\xcd\xcd\xcb\xcd\xcd\xcd\xcb\xcd\xcd\xcd\xcb\xcd\xcd\xcd\xcb\xcd\xcd\xcd\xbb")); + // SCREEN(3, 3, F("\xba X \xba O \xba O \xba O \xba X \xba")); + // SCREEN(3, 4, F("\xcc\xcd\xcd\xcd\xce\xcd\xcd\xcd\xce\xcd\xcd\xcd\xce\xcd\xcd\xcd\xce\xcd\xcd\xcd\xb9")); + // SCREEN(3, 5, F("\xba X \xba O \xba O \xba O \xba X \xba")); + // SCREEN(3, 6, F("\xc8\xcd\xcd\xcd\xca\xcd\xcd\xcd\xca\xcd\xcd\xcd\xca\xcd\xcd\xcd\xca\xcd\xcd\xcd\xbc")); + + // Draw single boxes with X O O X inside. + // SCREEN(3, 0, F("Summary Data:")); + // SCREEN(3, 1, F("\xda\xc4\xc4\xc4\xc2\xc4\xc4\xc4\xc2\xc4\xc4\xc4\xc2\xc4\xc4\xc4\xc2\xc4\xc4\xc4\xbf")); + // SCREEN(3, 2, F("\xb3 X \xb3 O \xb3 O \xb3 O \xb3 X \xb3")); + // SCREEN(3, 3, F("\xc3\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xb4")); + // SCREEN(3, 4, F("\xb3 X \xb3 O \xb3 O \xb3 O \xb3 X \xb3")); + // SCREEN(3, 5, F("\xc3\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xc5\xc4\xc4\xc4\xb4")); + // SCREEN(3, 6, F("\xb3 X \xb3 O \xb3 O \xb3 O \xb3 X \xb3")); + // SCREEN(3, 7, F("\xc0\xc4\xc4\xc4\xc1\xc4\xc4\xc4\xc1\xc4\xc4\xc4\xc1\xc4\xc4\xc4\xc1\xc4\xc4\xc4\xd9")); + + // Blocks of different greyness + // SCREEN(3, 0, F("\xb0\xb0\xb0\xb0\xb1\xb1\xb1\xb1\xb2\xb2\xb2\xb2\xdb\xdb\xdb\xdb")); + // SCREEN(3, 1, F("\xb0\xb0\xb0\xb0\xb1\xb1\xb1\xb1\xb2\xb2\xb2\xb2\xdb\xdb\xdb\xdb")); + // SCREEN(3, 2, F("\xb0\xb0\xb0\xb0\xb1\xb1\xb1\xb1\xb2\xb2\xb2\xb2\xdb\xdb\xdb\xdb")); + + // DCCEX logo + // SCREEN(3, 1, F("\xb0\xb0\x20\x20\x20\xb0\x20\x20\x20\xb0\x20\x20\x20\x20\xb0\xb0\xb0\x20\xb0\x20\xb0")); + // SCREEN(3, 2, F("\xb0\x20\xb0\x20\xb0\x20\xb0\x20\xb0\x20\xb0\x20\x20\x20\xb0\x20\x20\x20\xb0\x20\xb0")); + // SCREEN(3, 3, F("\xb0\x20\xb0\x20\xb0\x20\x20\x20\xb0\x20\x20\x20\xb0\x20\xb0\xb0\x20\x20\x20\xb0\x20")); + // SCREEN(3, 4, F("\xb0\x20\xb0\x20\xb0\x20\xb0\x20\xb0\x20\xb0\x20\x20\x20\xb0\x20\x20\x20\xb0\x20\xb0")); + // SCREEN(3, 5, F("\xb0\xb0\x20\x20\x20\xb0\x20\x20\x20\xb0\x20\x20\x20\x20\xb0\xb0\xb0\x20\xb0\x20\xb0")); + // SCREEN(3, 7, F("\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1\xb1")); + +#if 0 + // List versions of devices that respond to the version request + for (uint8_t address = 8; address<0x78; address++) { + uint8_t buffer[3]; + uint8_t status = I2CManager.read(0x7c, buffer, sizeof(buffer), 1, address); + if (status == I2C_STATUS_OK) { + uint16_t manufacturer = ((uint16_t)buffer[0] << 4 ) | (buffer[1] >> 4); + uint16_t deviceID = ((uint16_t)(buffer[1] & 0x0f) << 5) | (buffer[2] >> 3); + uint16_t dieRevision = buffer[2] & 0x1f; + DIAG(F("Addr %s version: %x %x %x"), address.toString(), manufacturer, deviceID, dieRevision); + } + } +#endif + +#if I2C_STRESS_TEST + UserAddin::create(I2CTest, I2cTestPeriod); +#endif + +#if WIRE_TEST + // Test of Wire-I2CManager interface + Wire.begin(); + Wire.setClock(400000); + Wire.beginTransmission(0x23); + Wire.print("Hello"); + uint8_t status = Wire.endTransmission(); + if (status==0) DIAG(F("Wire: device Found on 0x23")); + + Wire.beginTransmission(0x23); + Wire.write(0xde); + Wire.endTransmission(false); // don't send stop + Wire.requestFrom(0x23, 1); + if (Wire.available()) { + DIAG(F("Wire: value=x%x"), Wire.read()); + } + uint8_t st = I2CManager.write(0x33, 0, 0); + DIAG(F("I2CManager 0x33 st=%d \"%S\""), st, + I2CManager.getErrorMessage(st)); +#endif + +#if I2C_SETCLOCK + // Test I2C clock changes + // Set up two I2C request blocks + I2CRB rb1, rb2; + uint8_t readBuff[32]; + rb1.setRequestParams(0x23, readBuff, sizeof(readBuff), readBuff, sizeof(readBuff)); + rb2.setRequestParams(0x23, readBuff, sizeof(readBuff), readBuff, sizeof(readBuff)); + // First set clock to 400kHz and then issue requests + I2CManager.forceClock(400000); + I2CManager.queueRequest(&rb1); + I2CManager.queueRequest(&rb2); + // Wait a little to allow the first transaction to start + delayMicroseconds(2); + // ... then request a clock speed change + I2CManager.forceClock(100000); + DIAG(F("I2CClock: rb1 status=%d"), rb1.wait()); + DIAG(F("I2CClock: rb2 status=%d"), rb2.wait()); + // Reset clock speed + I2CManager.forceClock(400000); +#endif + + EXIOExpander::create(2200, 18, {SubBus_0, 0x65}); + //UserAddin::create(myTest, 1000); + // ServoTurnout::create(2200, 2200, 400, 200, 0); + // ServoTurnout::create(2200, 2200, 400, 200, 0); + + TouchKeypad::create(2300, 16, 25, 24); + + // GPIO + PCF8574::create(800, 8, {SubBus_1, 0x23}); + //PCF8574::create(808, 8, {SubBus_2, 0x27}); + PCF8574::create(65000, 8, 0x27); + + MCP23017::create(164,16,{SubBus_3, 0x20}); + //MCP23017::create(180,16,{SubBus_0, 0x27}); + Sensor::create(170, 170, 1); // Hall effect, enable pullup. + Sensor::create(171, 171, 1); + + // PWM (LEDs and Servos) + // For servos, use default 50Hz pulses. + PCA9685::create(100, 16, {SubBus_1, 0x41}); + // For LEDs, use 1kHz pulses. + PCA9685::create(116, 16, {SubBus_1, 0x40}, 1000); + + // 4-pin Analogue Input Module + //ADS111x::create(4500, 4, 0x48); + + // Laser Time-Of-Flight Sensors + VL53L0X::create(5000, 3, {SubBus_0, 0x60}, 300, 310, 46); + //VL53L0X::create(5003, 3, {SubBus_6, 0x61}, 300, 310, 47); + Sensor::create(5000, 5000, 0); + Sensor::create(5003, 5003, 0); + // Monitor reset digital on first TOF + //Sensor::create(46,46,0); + + // // External 24C256 EEPROM (256kBytes) on I2C address 0x50. + // ExternalEEPROM::create({SubBus_0, 0x50}, 256); + + // Play up to 10 sounds on pins 10000-10009. Player is connected to Serial1 or Serial2. + #if defined(HAVE_HWSERIAL1) && !defined(ARDUINO_ARCH_STM32) + DFPlayer::create(10000, 14, Serial1); + #elif defined(ARDUINO_ARCH_STM32) + DFPlayer::create(10000, 10, Serial3); // Pins PC11 (RX) and PC10 (TX) + #endif + + // Ultrasound echo device + HCSR04::create(2000, 32, 33, 80, 85 /*, HCSR04::LOOP */); + Sensor::create(2000, 2000, 0); + +#if __has_include("IO_CMRI.h") + CMRIbus::create(0, Serial2, 115200, 50, 40); // 50ms cycle, pin 40 for DE/!RE pins + CMRInode::create(25000, 72, 0, 0, 'M'); // SMINI address 0 + for (int pin=0; pin<24; pin++) { + Sensor::create(25000+pin, 25000+pin, 0); + } +#endif + + //CMRInode::create(25072, 72, 0, 13, 'M'); // SMINI address 13 + //CMRInode::create(25144, 288, 0, 14, 'C', 144, 144); // CPNODE address 14 + +#ifdef NETWORK_PRESENT + // Define remote pins to be used. The range of remote pins is like a common data area shared + // between all nodes. + // For outputs, a write to a remote VPIN causes a message to be sent to another node, which then performs + // the write operation on the device VPIN that is local to that node. + // For inputs, the state of remote input VPIN is read on the node where it is connected, and then + // sent to other nodes in the system where the state is saved and processed. Updates are sent on change, and + // also periodically if no changes. + // + // Each definition is a triple of remote node, remote pin, indexed by relative pin. Up to 224 rpins can + // be configured (per node). This is to fit into a 32-byte packet. + REMOTEPINS rpins[] = { + {30,164,RPIN_IN} , //4000 Node 30, first MCP23017 pin, input + {30,165,RPIN_IN}, //4001 Node 30, second MCP23017 pin, input + {30,166,RPIN_OUT}, //4002 Node 30, third MCP23017 pin, output + {30,166,RPIN_OUT}, //4003 Node 30, fourth MCP23017 pin, output + {30,100,RPIN_INOUT}, //4004 Node 30, first PCA9685 servo pin + {30,101,RPIN_INOUT}, //4005 Node 30, second PCA9685 servo pin + {30,102,RPIN_INOUT}, //4006 Node 30, third PCA9685 servo pin + {30,103,RPIN_INOUT}, //4007 Node 30, fourth PCA9685 servo pin + {30,24,RPIN_IN}, //4008 Node 30, Arduino pin D24 + {30,25,RPIN_IN}, //4009 Node 30, Arduino pin D25 + {30,26,RPIN_IN}, //4010 Node 30, Arduino pin D26 + {30,27,RPIN_IN}, //4011 Node 30, Arduino pin D27 + {30,1000,RPIN_OUT}, //4012 Node 30, DFPlayer playing flag (when read) / Song selector (when written) + {30,5000,RPIN_IN}, //4013 Node 30, VL53L0X detect pin + {30,VPIN_NONE,0}, //4014 Node 30, spare + {30,VPIN_NONE,0}, //4015 Node 30, spare + + {31,164,RPIN_IN} , //4016 Node 31, first MCP23017 pin, input + {31,165,RPIN_IN}, //4017 Node 31, second MCP23017 pin, input + {31,166,RPIN_OUT}, //4018 Node 31, third MCP23017 pin, output + {31,166,RPIN_OUT}, //4019 Node 31, fourth MCP23017 pin, output + {31,100,RPIN_INOUT}, //4020 Node 31, first PCA9685 servo pin + {31,101,RPIN_INOUT}, //4021 Node 31, second PCA9685 servo pin + {31,102,RPIN_INOUT}, //4022 Node 31, third PCA9685 servo pin + {31,103,RPIN_INOUT}, //4023 Node 31, fourth PCA9685 servo pin + {31,24,RPIN_IN}, //4024 Node 31, Arduino pin D24 + {31,25,RPIN_IN}, //4025 Node 31, Arduino pin D25 + {31,26,RPIN_IN}, //4026 Node 31, Arduino pin D26 + {31,27,RPIN_IN}, //4027 Node 31, Arduino pin D27 + {31,3,RPIN_IN}, //4028 Node 31, Arduino pin D3 + {31,VPIN_NONE,0}, //4029 Node 31, spare + {31,VPIN_NONE,0}, //4030 Node 31, spare + {31,VPIN_NONE,0} //4031 Node 31, spare + }; + // FirstVPIN, nPins, thisNode, pinDefs, CEPin, CSNPin + // Net_RF24 *rf24Driver = new Net_RF24(48, 49); + // Network::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, rf24Driver); + #if NODE==30 + //Net_ENC28J60 *encDriver = new Net_ENC28J60(49); + //Network::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, encDriver); + #elif NODE==31 + Net_ENC28J60 *encDriver = new Net_ENC28J60(53); + Network::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, encDriver); + #else + Net_Ethernet *etherDriver = new Net_Ethernet(); + Network::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, etherDriver); + #endif + for (int i=0; i<=32; i++) + Sensor::create(4000+i, 4000+i, 0); +#endif + +#ifdef ARDUINO_ARCH_STM32 +//PCF8574::create(1900, 8, 0x27); +Sensor::create(1900,100,1); +Sensor::create(1901,101,1); +#endif + +} +#endif // IO_NO_HAL diff --git a/myHal.cpp_example.txt b/myHal.cpp_example.txt index b9e68b7..d93ea5c 100644 --- a/myHal.cpp_example.txt +++ b/myHal.cpp_example.txt @@ -17,9 +17,11 @@ // Include devices you need. #include "IODevice.h" -#include "IO_HCSR04.h" // Ultrasonic range sensor -#include "IO_VL53L0X.h" // Laser time-of-flight sensor -#include "IO_DFPlayer.h" // MP3 sound player +//#include "IO_HALDisplay.h" // Auxiliary display devices (LCD/OLED) +//#include "IO_HCSR04.h" // Ultrasonic range sensor +//#include "IO_VL53L0X.h" // Laser time-of-flight sensor +//#include "IO_DFPlayer.h" // MP3 sound player +//#include "IO_TouchKeypad.h // Touch keypad with 16 keys //#include "IO_EXTurntable.h" // Turntable-EX turntable controller //#include "IO_EXFastClock.h" // FastClock driver @@ -31,6 +33,61 @@ void halSetup() { + //======================================================================= + // The following directives define auxiliary display devices. + // These can be defined in addition to the system display (display + // number 0) that is defined in config.h. + // A write to a line which is beyond the length of the screen will overwrite + // the bottom line, unless the line number is 255 in which case the + // screen contents will scroll up before the text is written to the + // bottom line. + //======================================================================= + // + // Create a 128x32 OLED display device as display number 1 + // (line 0 is written by EX-RAIL 'SCREEN(1, 0, "text")'). + + //HALDisplay::create(1, 0x3d, 128, 32); + + // Create a 20x4 LCD display device as display number 2 + // (line 0 is written by EX-RAIL 'SCREEN(2, 0, "text")'). + + // HALDisplay(2, 0x27, 20, 4); + + + //======================================================================= + // User Add-ins + //======================================================================= + // User add-ins can be created when you want to do something that + // can't be done in EX-RAIL but does not merit a HAL driver. The + // user add-in is a C++ function that is executed periodically by the + // HAL subsystem. + + // Example: The function will be executed once per second and will display, + // on screen #3, the first eight entries (assuming an 8-line display) + // from the loco speed table. + + // Put the following block of code in myHal.cpp OUTSIDE of the + // halSetup() function: + // + // void updateLocoScreen() { + // for (int i=0; i<8; i++) { + // if (DCC::speedTable[i].loco > 0) { + // int speed = DCC::speedTable[i].speedCode; + // char direction = (speed & 0x80) ? 'R' : 'F'; + // speed = speed & 0x7f; + // if (speed > 0) speed = speed - 1; + // SCREEN(3, i, F("Loco:%4d %3d %c"), DCC::speedTable[i].loco, + // speed, direction); + // } + // } + // } + // + // Put the following line INSIDE the halSetup() function: + // + // UserAddin::create(updateLocoScreen, 1000); + // + + //======================================================================= // The following directive defines a PCA9685 PWM Servo driver module. //======================================================================= @@ -176,6 +233,21 @@ void halSetup() { // DFPlayer::create(10000, 10, Serial1); + //======================================================================= + // 16-pad capacitative touch key pad based on TP229 IC. + //======================================================================= + // Parameters below: + // 11000 = first VPIN allocated + // 16 = number of VPINs allocated + // 25 = local GPIO pin number for clock signal + // 24 = local GPIO pin number for data signal + // + // Pressing the key pads numbered 1-16 cause each of the nominated digital VPINs + // (11000-11015 in this case) to be activated. + + // TouchKeypad::create(11000, 16, 25, 24); + + //======================================================================= // The following directive defines an EX-Turntable turntable instance. //======================================================================= diff --git a/platformio.ini b/platformio.ini index 1fc7291..d85c76c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,11 +20,11 @@ default_envs = ESP32 Nucleo-F411RE Nucleo-F446RE - Teensy3.2 - Teensy3.5 - Teensy3.6 - Teensy4.0 - Teensy4.1 + Teensy3_2 + Teensy3_5 + Teensy3_6 + Teensy4_0 + Teensy4_1 src_dir = . include_dir = . @@ -53,7 +53,7 @@ monitor_speed = 115200 monitor_echo = yes build_flags = -std=c++17 -[env:Arduino M0] +[env:Arduino-M0] platform = atmelsam board = mzeroUSB framework = arduino @@ -192,7 +192,7 @@ build_flags = -std=c++17 -Os -g2 -Wunused-variable -DDIAG_LOOPTIMES ; -DDIAG_IO monitor_speed = 115200 monitor_echo = yes -[env:Teensy3.2] +[env:Teensy3_2] platform = teensy board = teensy31 framework = arduino @@ -200,7 +200,7 @@ build_flags = -std=c++17 -Os -g2 lib_deps = ${env.lib_deps} lib_ignore = NativeEthernet -[env:Teensy3.5] +[env:Teensy3_5] platform = teensy board = teensy35 framework = arduino @@ -208,7 +208,7 @@ build_flags = -std=c++17 -Os -g2 lib_deps = ${env.lib_deps} lib_ignore = NativeEthernet -[env:Teensy3.6] +[env:Teensy3_6] platform = teensy board = teensy36 framework = arduino @@ -216,7 +216,7 @@ build_flags = -std=c++17 -Os -g2 lib_deps = ${env.lib_deps} lib_ignore = NativeEthernet -[env:Teensy4.0] +[env:Teensy4_0] platform = teensy board = teensy40 framework = arduino @@ -224,7 +224,7 @@ build_flags = -std=c++17 -Os -g2 lib_deps = ${env.lib_deps} lib_ignore = NativeEthernet -[env:Teensy4.1] +[env:Teensy4_1] platform = teensy board = teensy41 framework = arduino diff --git a/version.h b/version.h index a97aaec..15c6d0b 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,40 @@ #include "StringFormatter.h" -#define VERSION "4.2.20" +#define VERSION "4.2.45" +// 4.2.45 - Add ONCLOCKMINS to FastClock to allow hourly repeat events +// 4.2.44 - Add PowerShell installer EX-CommandStation-installer.exe +// 4.2.43 - Fix STM32 set right port mode bits for analog +// 4.2.42 - Added EXRAIL TURNOUTL Macro definition +// 4.2.41 - Move HAl startup to ASAP in setup() +// - Fix DNOU8 output pin setup to all LOW +// 4.2.40 - Automatically detect conflicting default I2C devices and disable +// 4.2.39 - DFplayer driver now polls device to detect failures and errors. +// 4.2.38 - Clean up compiler warning when IO_RotaryEncoder.h included +// 4.2.37 - Add new FLAGS HAL device for communications to/from EX-RAIL; +// - Fix diag display of high VPINs within IODevice class. +// 4.2.36 - do not broadcast a turnout state that has not changed +// - Use A2/A3 for current sensing on ESP32 + Motor Shield +// 4.2.35 - add direct pin manipulation command +// 4.2.34 - Completely fix EX-IOExpander analogue inputs +// 4.2.33 - Fix EX-IOExpander non-working analogue inputs +// 4.2.32 - Fix LCD/Display bugfixes from 4.2.29 +// 4.2.31 - Removes EXRAIL statup from top of file. (BREAKING CHANGE !!) +// Just add AUTOSTART to the top of your myAutomation.h to restore this function. +// 4.2.30 - Fixes/enhancements to EX-IOExpander device driver. +// 4.2.29 - Bugfix Scroll LCD without empty lines and consistent +// 4.2.28 - Reinstate use of timer11 in STM32 - remove HA mode. +// - Update IO_DFPlayer to work with MP3-TF-16P rev3. +// 4.2.27 - Bugfix LCD showed random characters in SCROLLMODE 2 +// 4.2.26 - EX-IOExpander device driver enhancements +// - Enhance I2C error checking +// - Introduce delays to _loop to allow room for other I2C device comms +// - Improve analogue read reliability +// 4.2.25 - Bugfix SAMD21 Exrail odd byte boundary +// 4.2.24 - Bugfix Ethernet shield: Static IP now possible +// 4.2.23 - Bugfix signalpin2 was not set up in shadow port +// 4.2.22 - Implement broadcast of Track Manager changes +// 4.2.21 - Implement non-blocking I2C for EX-IOExpander device driver // 4.2.20 - & commands for multi-track gauges // - Reinstate but remember its a bit useless when TM involved. // 4.2.19 - Bugfix for analog reading of track current sensor offset. @@ -43,6 +76,10 @@ // 4.2.11 Exrail IFLOCO feature added // 4.2.10 SIGNAL/SIGNALH bug fix as they were inverted // IO_EXIOExpander.h input speed optimisation +// ONCLOCK and ONCLOCKTIME command added to EXRAIL for EX-FastCLock +// Serial command added for EX-FastClock +// Broadcast added for EX-FastClock +// IO_EXFastClock.h added for I2C FastClock connection // 4.2.9 duinoNodes support // 4.2.8 HIGHMEM (EXRAIL support beyond 64kb) // Withrottle connect/disconnect improvements