diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index 7363566..77e8f40 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -30,6 +30,7 @@ * © 2021 Neil McKechnie * © 2020-2021 Chris Harlow, Harald Barth, David Cutting, * Fred Decker, Gregor Baues, Anthony W - Dayton + * © 2023 Nathan Kellenicki * All rights reserved. * * This file is part of CommandStation-EX @@ -78,6 +79,12 @@ void setup() // 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(); + // Set up MotorDrivers early to initialize all pins + TrackManager::Setup(MOTOR_SHIELD_TYPE); + DISPLAY_START ( // This block is still executed for DIAGS if display not in use LCD(0,F("DCC-EX v%S"),F(VERSION)); @@ -89,26 +96,19 @@ void setup() // Start Ethernet if it exists #ifndef ARDUINO_ARCH_ESP32 #if WIFI_ON - WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL); + WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP); #endif // WIFI_ON #else // ESP32 needs wifi on always - WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL); + WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP); #endif // ARDUINO_ARCH_ESP32 #if ETHERNET_ON EthernetInterface::setup(); #endif // ETHERNET_ON - // 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(); // Responsibility 3: Start the DCC engine. - // Note: this provides DCC with two motor drivers, main and prog, which handle the motor shield(s) - // Standard supported devices have pre-configured macros but custome hardware installations require - // detailed pin mappings and may also require modified subclasses of the MotorDriver to implement specialist logic. - // STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h - TrackManager::Setup(MOTOR_SHIELD_TYPE); + DCC::begin(); // Start RMFT aka EX-RAIL (ignored if no automnation) RMFT::begin(); diff --git a/DCC.cpp b/DCC.cpp index c9535b2..30fcf5f 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -60,8 +60,7 @@ const byte FN_GROUP_5=0x10; FSH* DCC::shieldName=NULL; byte DCC::globalSpeedsteps=128; -void DCC::begin(const FSH * motorShieldName) { - shieldName=(FSH *)motorShieldName; +void DCC::begin() { StringFormatter::send(&USB_SERIAL,F("\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA)); #ifndef DISABLE_EEPROM // Load stuff from EEprom diff --git a/DCC.h b/DCC.h index 15d5a4f..74b4e77 100644 --- a/DCC.h +++ b/DCC.h @@ -51,7 +51,10 @@ const byte MAX_LOCOS = 30; class DCC { public: - static void begin(const FSH * motorShieldName); + static inline void setShieldName(const FSH * motorShieldName) { + shieldName=(FSH *)motorShieldName; + }; + static void begin(); static void loop(); // Public DCC API functions diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 048ae2a..aa635ae 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -467,7 +467,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) bool prog=false; bool join=false; if (params > 1) break; - if (params==0 || MotorDriver::commonFaultPin) { // <1> or tracks can not be handled individually + if (params==0) { // All main=true; prog=true; } @@ -487,9 +487,9 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) #endif else break; // will reply } + TrackManager::setJoin(join); if (main) TrackManager::setMainPower(POWERMODE::ON); if (prog) TrackManager::setProgPower(POWERMODE::ON); - TrackManager::setJoin(join); CommandDistributor::broadcastPower(); return; @@ -500,7 +500,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) bool main=false; bool prog=false; if (params > 1) break; - if (params==0 || MotorDriver::commonFaultPin) { // <0> or tracks can not be handled individually + if (params==0) { // All main=true; prog=true; } @@ -516,12 +516,12 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) else break; // will reply } + TrackManager::setJoin(false); if (main) TrackManager::setMainPower(POWERMODE::OFF); if (prog) { TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off TrackManager::setProgPower(POWERMODE::OFF); } - TrackManager::setJoin(false); CommandDistributor::broadcastPower(); return; diff --git a/DCCRMT.cpp b/DCCRMT.cpp index 631cc16..cbd9af6 100644 --- a/DCCRMT.cpp +++ b/DCCRMT.cpp @@ -194,8 +194,10 @@ int RMTChannel::RMTfillData(const byte buffer[], byte byteCount, byte repeatCoun setDCCBit1(data + bitcounter-1); // overwrite previous zero bit with one bit setEOT(data + bitcounter++); // EOT marker dataLen = bitcounter; + noInterrupts(); // keep dataReady and dataRepeat consistnet to each other dataReady = true; dataRepeat = repeatCount+1; // repeatCount of 0 means send once + interrupts(); return 0; } @@ -212,6 +214,8 @@ void IRAM_ATTR RMTChannel::RMTinterrupt() { if (dataReady) { // if we have new data, fill while preamble is running rmt_fill_tx_items(channel, data, dataLen, preambleLen-1); dataReady = false; + if (dataRepeat == 0) // all data should go out at least once + DIAG(F("Channel %d DCC signal lost data"), channel); } if (dataRepeat > 0) // if a repeat count was specified, work on that dataRepeat--; diff --git a/DCCTimer.h b/DCCTimer.h index 7a9d940..7402f16 100644 --- a/DCCTimer.h +++ b/DCCTimer.h @@ -105,9 +105,14 @@ private: // that an offset can be initialized. class ADCee { public: - // init does add the pin to the list of scanned pins (if this + // begin is called for any setup that must be done before + // **init** can be called. On some architectures this involves ADC + // initialisation and clock routing, sampling times etc. + static void begin(); + // init adds the pin to the list of scanned pins (if this // platform's implementation scans pins) and returns the first - // read value. It is called before the regular scan is started. + // read value (which is why it required begin to have been called first!) + // It must be called before the regular scan is started. static int init(uint8_t pin); // read does read the pin value from the scanned cache or directly // if this is a platform that does not scan. fromISR is a hint if @@ -116,19 +121,15 @@ public: static int read(uint8_t pin, bool fromISR=false); // returns possible max value that the ADC can return static int16_t ADCmax(); - // begin is called for any setup that must be done before - // scan can be called. - static void begin(); private: // On platforms that scan, it is called from waveform ISR // only on a regular basis. static void scan(); // bit array of used pins (max 16) static uint16_t usedpins; + static uint8_t highestPin; // cached analog values (malloc:ed to actual number of ADC channels) static int *analogvals; - // ids to scan (new way) - static byte *idarr; // friend so that we can call scan() and begin() friend class DCCWaveform; }; diff --git a/DCCTimerAVR.cpp b/DCCTimerAVR.cpp index 40ce0fb..3e6c436 100644 --- a/DCCTimerAVR.cpp +++ b/DCCTimerAVR.cpp @@ -1,6 +1,6 @@ /* * © 2021 Mike S - * © 2021-2022 Harald Barth + * © 2021-2023 Harald Barth * © 2021 Fred Decker * © 2021 Chris Harlow * © 2021 David Cutting @@ -29,6 +29,9 @@ #include #include #include "DCCTimer.h" +#ifdef DEBUG_ADC +#include "TrackManager.h" +#endif INTERRUPT_CALLBACK interruptHandler=0; // Arduino nano, uno, mega etc @@ -128,8 +131,8 @@ void DCCTimer::reset() { #define NUM_ADC_INPUTS 8 #endif uint16_t ADCee::usedpins = 0; +uint8_t ADCee::highestPin = 0; int * ADCee::analogvals = NULL; -byte *ADCee::idarr = NULL; static bool ADCusesHighPort = false; /* @@ -139,28 +142,17 @@ static bool ADCusesHighPort = false; */ int ADCee::init(uint8_t pin) { uint8_t id = pin - A0; - byte n; if (id >= NUM_ADC_INPUTS) return -1023; if (id > 7) ADCusesHighPort = true; pinMode(pin, INPUT); int value = analogRead(pin); - if (analogvals == NULL) { + if (analogvals == NULL) analogvals = (int *)calloc(NUM_ADC_INPUTS, sizeof(int)); - for (n=0 ; n < NUM_ADC_INPUTS; n++) // set unreasonable value at startup as marker - analogvals[n] = -32768; // 16 bit int min value - idarr = (byte *)calloc(NUM_ADC_INPUTS+1, sizeof(byte)); // +1 for terminator value - for (n=0 ; n <= NUM_ADC_INPUTS; n++) - idarr[n] = 255; // set 255 as end of array marker - } - analogvals[id] = value; // store before enable by idarr[n] - for (n=0 ; n <= NUM_ADC_INPUTS; n++) { - if (idarr[n] == 255) { - idarr[n] = id; - break; - } - } + analogvals[id] = value; + usedpins |= (1< highestPin) highestPin = id; return value; } int16_t ADCee::ADCmax() { @@ -170,14 +162,14 @@ int16_t ADCee::ADCmax() { * Read function ADCee::read(pin) to get value instead of analogRead(pin) */ int ADCee::read(uint8_t pin, bool fromISR) { - (void)fromISR; // AVR does ignore this arg uint8_t id = pin - A0; - int a; + if ((usedpins & (1<setBrake(0); +#endif waiting = false; + id++; + mask = mask << 1; + if (id > highestPin) { + id = 0; + mask = 1; + } } if (!waiting) { - // cycle around in-use analogue pins - num++; - if (idarr[num] == 255) - num = 0; - // start new ADC aquire on id + if (usedpins == 0) // otherwise we would loop forever + return; + // look for a valid track to sample or until we are around + while (true) { + if (mask & usedpins) { + // start new ADC aquire on id #if defined(ADCSRB) && defined(MUX5) - if (ADCusesHighPort) { // if we ever have started to use high pins) - if (idarr[num] > 7) // if we use a high ADC pin - bitSet(ADCSRB, MUX5); // set MUX5 bit - else - bitClear(ADCSRB, MUX5); - } + if (ADCusesHighPort) { // if we ever have started to use high pins) + if (id > 7) // if we use a high ADC pin + bitSet(ADCSRB, MUX5); // set MUX5 bit + else + bitClear(ADCSRB, MUX5); + } #endif - ADMUX = (1 << REFS0) | (idarr[num] & 0x07); // select AVCC as reference and set MUX - bitSet(ADCSRA, ADSC); // start conversion - waiting = true; + ADMUX=(1<setBrake(1); +#endif + waiting = true; + return; + } + id++; + mask = mask << 1; + if (id > highestPin) { + id = 0; + mask = 1; + } + } } } #pragma GCC pop_options @@ -231,4 +247,4 @@ void ADCee::begin() { //bitSet(ADCSRA, ADSC); //do not start the ADC yet. Done when we have set the MUX interrupts(); } -#endif \ No newline at end of file +#endif diff --git a/DCCTimerSTM32.cpp b/DCCTimerSTM32.cpp index a45c4d4..cc60547 100644 --- a/DCCTimerSTM32.cpp +++ b/DCCTimerSTM32.cpp @@ -30,25 +30,31 @@ #ifdef ARDUINO_ARCH_STM32 #include "DCCTimer.h" +#ifdef DEBUG_ADC +#include "TrackManager.h" +#endif +#include "DIAG.h" #if defined(ARDUINO_NUCLEO_F411RE) -// Nucleo-64 boards don't have Serial1 defined by default +// Nucleo-64 boards don't have additional serial ports defined by default HardwareSerial Serial1(PB7, PA15); // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F411RE // 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. // Let's define Serial6 as an additional serial port (the only other option for the Nucleo-64s) -HardwareSerial Serial3(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F411RE +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 +// Nucleo-64 boards don't have additional serial ports defined by default +// On the F446RE, Serial1 isn't really useable as it's Rx/Tx pair sit on already used D2/D10 pins // 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. -HardwareSerial Serial1(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE -HardwareSerial Serial3(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE -// NB: USART3 and USART6 are available but as yet undefined +// On the F446RE, Serial3 and Serial5 are easy to use: +HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE +HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE +// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins #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 -- USART6 +HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6 // Serial3 is defined to use USART3 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. #else @@ -229,13 +235,16 @@ void DCCTimer::reset() { while(true) {}; } -#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 +// For now, define the max of 16 ports - some variants have more, but this not **yet** supported +#define NUM_ADC_INPUTS 16 +// #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS + uint16_t ADCee::usedpins = 0; +uint8_t ADCee::highestPin = 0; int * ADCee::analogvals = NULL; uint32_t * analogchans = NULL; bool adc1configured = false; @@ -306,6 +315,9 @@ int ADCee::init(uint8_t pin) { analogvals[id] = value; // Store sampled value analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin usedpins |= (1 << id); // This pin is now ready + if (id > highestPin) highestPin = id; // Store our highest pin in use + + DIAG(F("ADCee::init(): value=%d, channel=%d, id=%d"), value, adcchan, id); return value; } @@ -340,11 +352,13 @@ void ADCee::scan() { // found value analogvals[id] = ADC1->DR; // advance at least one track - // for scope debug TrackManager::track[1]->setBrake(0); +#ifdef DEBUG_ADC + if (id == 1) TrackManager::track[1]->setBrake(0); +#endif waiting = false; id++; mask = mask << 1; - if (id == NUM_ADC_INPUTS+1) { + if (id > highestPin) { // the 1 has been shifted out id = 0; mask = 1; } @@ -355,18 +369,20 @@ void ADCee::scan() { // look for a valid track to sample or until we are around while (true) { if (mask & usedpins) { - // start new ADC aquire on id + // start new ADC aquire on id ADC1->SQR3 = analogchans[id]; //1st conversion in regular sequence ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART - // for scope debug TrackManager::track[1]->setBrake(1); - waiting = true; - return; +#ifdef DEBUG_ADC + if (id == 1) TrackManager::track[1]->setBrake(1); +#endif + waiting = true; + return; } id++; mask = mask << 1; - if (id == NUM_ADC_INPUTS+1) { - id = 0; - mask = 1; + if (id > highestPin) { + id = 0; + mask = 1; } } } diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index e065648..4a99997 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -247,6 +247,9 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea pendingPacket[byteCount] = checksum; pendingLength = byteCount + 1; pendingRepeats = repeats; +// DIAG repeated commands (accesories) +// if (pendingRepeats > 0) +// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG"); // The resets will be zero not only now but as well repeats packets into the future clearResets(repeats+1); { diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h index 8c2daa8..977f449 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "devel-z21-202306082116Z" +#define GITHUB_SHA "devel-z21-202307082214Z" diff --git a/IO_EXTurntable.h b/IO_EXTurntable.h index 40c02ee..29ce679 100644 --- a/IO_EXTurntable.h +++ b/IO_EXTurntable.h @@ -50,12 +50,12 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) { // Initialisation of EXTurntable void EXTurntable::_begin() { I2CManager.begin(); - I2CManager.setClock(1000000); if (I2CManager.exists(_I2CAddress)) { #ifdef DIAG_IO _display(); #endif } else { + DIAG(F("EX-Turntable I2C:%s device not found"), _I2CAddress.toString()); _deviceState = DEVSTATE_FAILED; } } diff --git a/IO_RotaryEncoder.h b/IO_RotaryEncoder.h index 00a8249..9d40b34 100644 --- a/IO_RotaryEncoder.h +++ b/IO_RotaryEncoder.h @@ -1,4 +1,5 @@ /* + * © 2023, Peter Cole. All rights reserved. * © 2022, Peter Cole. All rights reserved. * * This file is part of EX-CommandStation @@ -28,9 +29,23 @@ * ONCHANGE(vpin) - flag when the rotary encoder position has changed from the previous position * IFRE(vpin, position) - test to see if specified rotary encoder position has been received * -* Further to this, feedback can be sent to the rotary encoder by using 2 Vpins, and sending a SET()/RESET() to the second Vpin. +* Feedback can also be sent to the rotary encoder by using 2 Vpins, and sending a SET()/RESET() to the second Vpin. * A SET(vpin) will flag that a turntable (or anything else) is in motion, and a RESET(vpin) that the motion has finished. * +* In addition, defining a third Vpin will allow a position number to be sent so that when an EXRAIL automation or some other +* activity has moved a turntable, the position can be reflected in the rotary encoder software. This can be accomplished +* using the EXRAIL SERVO(vpin, position, profile) command, where: +* - vpin = the third defined Vpin (any other is ignored) +* - position = the defined position in the DCC-EX Rotary Encoder software, 0 (Home) to 255 +* - profile = Must be defined as per the SERVO() command, but is ignored as it has no relevance +* +* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples: +* +* #include "IO_RotaryEncoder.h" +* HAL(RotaryEncoder, 700, 1, 0x70) // Define single Vpin, no feedback or position sent to rotary encoder software +* HAL(RotaryEncoder, 700, 2, 0x70) // Define two Vpins, feedback only sent to rotary encoder software +* HAL(RotaryEncoder, 700, 3, 0x70) // Define three Vpins, can send feedback and position update to rotary encoder software +* * Refer to the documentation for further information including the valid activities and examples. */ @@ -44,58 +59,88 @@ class RotaryEncoder : public IODevice { public: - // Constructor - RotaryEncoder(VPIN firstVpin, int nPins, I2CAddress i2cAddress){ - _firstVpin = firstVpin; - _nPins = nPins; - _I2CAddress = i2cAddress; - addDevice(this); - } + static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) { if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new RotaryEncoder(firstVpin, nPins, i2cAddress); } private: + // Constructor + RotaryEncoder(VPIN firstVpin, int nPins, I2CAddress i2cAddress){ + _firstVpin = firstVpin; + _nPins = nPins; + if (_nPins > 3) { + _nPins = 3; + DIAG(F("RotaryEncoder WARNING:%d vpins defined, only 3 supported"), _nPins); + } + _I2CAddress = i2cAddress; + addDevice(this); + } + // Initiate the device void _begin() { + uint8_t _status; + // Attempt to initilalise device I2CManager.begin(); if (I2CManager.exists(_I2CAddress)) { - byte _getVersion[1] = {RE_VER}; - I2CManager.read(_I2CAddress, _versionBuffer, 3, _getVersion, 1); - _majorVer = _versionBuffer[0]; - _minorVer = _versionBuffer[1]; - _patchVer = _versionBuffer[2]; - _buffer[0] = RE_OP; - I2CManager.write(_I2CAddress, _buffer, 1); + // Send RE_RDY, must receive RE_RDY to be online + _sendBuffer[0] = RE_RDY; + _status = I2CManager.read(_I2CAddress, _rcvBuffer, 1, _sendBuffer, 1); + if (_status == I2C_STATUS_OK) { + if (_rcvBuffer[0] == RE_RDY) { + _sendBuffer[0] = RE_VER; + if (I2CManager.read(_I2CAddress, _versionBuffer, 3, _sendBuffer, 1) == I2C_STATUS_OK) { + _majorVer = _versionBuffer[0]; + _minorVer = _versionBuffer[1]; + _patchVer = _versionBuffer[2]; + } + } else { + DIAG(F("RotaryEncoder I2C:%s garbage received: %d"), _I2CAddress.toString(), _rcvBuffer[0]); + _deviceState = DEVSTATE_FAILED; + return; + } + } else { + DIAG(F("RotaryEncoder I2C:%s ERROR connecting"), _I2CAddress.toString()); + _deviceState = DEVSTATE_FAILED; + return; + } #ifdef DIAG_IO _display(); #endif } else { - _deviceState = DEVSTATE_FAILED; + DIAG(F("RotaryEncoder I2C:%s device not found"), _I2CAddress.toString()); + _deviceState = DEVSTATE_FAILED; } } void _loop(unsigned long currentMicros) override { - I2CManager.read(_I2CAddress, _buffer, 1); - _position = _buffer[0]; - // This here needs to have a change check, ie. position is a different value. - #if defined(EXRAIL_ACTIVE) + if (_deviceState == DEVSTATE_FAILED) return; // Return if device has failed + if (_i2crb.isBusy()) return; // Return if I2C operation still in progress + + if (currentMicros - _lastPositionRead > _positionRefresh) { + _lastPositionRead = currentMicros; + _sendBuffer[0] = RE_READ; + I2CManager.read(_I2CAddress, _rcvBuffer, 1, _sendBuffer, 1, &_i2crb); // Read position from encoder + _position = _rcvBuffer[0]; + // If EXRAIL is active, we need to trigger the ONCHANGE() event handler if it's in use +#if defined(EXRAIL_ACTIVE) if (_position != _previousPosition) { _previousPosition = _position; - RMFT2::changeEvent(_firstVpin,1); + RMFT2::changeEvent(_firstVpin, 1); } else { - RMFT2::changeEvent(_firstVpin,0); + RMFT2::changeEvent(_firstVpin, 0); } - #endif - delayUntil(currentMicros + 100000); +#endif + } } - // Device specific read function + // Return the position sent by the rotary encoder software int _readAnalogue(VPIN vpin) override { if (_deviceState == DEVSTATE_FAILED) return 0; return _position; } + // Send the feedback value to the rotary encoder software void _write(VPIN vpin, int value) override { if (vpin == _firstVpin + 1) { if (value != 0) value = 0x01; @@ -103,6 +148,19 @@ private: I2CManager.write(_I2CAddress, _feedbackBuffer, 2); } } + + // Send a position update to the rotary encoder software + // To be valid, must be 0 to 255, and different to the current position + // If the current position is the same, it was initiated by the rotary encoder + void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t duration) override { + if (vpin == _firstVpin + 2) { + if (position >= 0 && position <= 255 && position != _position) { + byte newPosition = position & 0xFF; + byte _positionBuffer[2] = {RE_MOVE, newPosition}; + I2CManager.write(_I2CAddress, _positionBuffer, 2); + } + } + } void _display() override { DIAG(F("Rotary Encoder I2C:%s v%d.%d.%d Configured on VPIN:%u-%d %S"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer, @@ -112,14 +170,21 @@ private: int8_t _position; int8_t _previousPosition = 0; uint8_t _versionBuffer[3]; - uint8_t _buffer[1]; + uint8_t _sendBuffer[1]; + uint8_t _rcvBuffer[1]; uint8_t _majorVer = 0; uint8_t _minorVer = 0; uint8_t _patchVer = 0; + I2CRB _i2crb; + unsigned long _lastPositionRead = 0; + const unsigned long _positionRefresh = 100000UL; // Delay refreshing position for 100ms enum { - RE_VER = 0xA0, // Flag to retrieve rotary encoder version from the device - RE_OP = 0xA1, // Flag for normal operation + RE_RDY = 0xA0, // Flag to check if encoder is ready for operation + RE_VER = 0xA1, // Flag to retrieve rotary encoder software version + RE_READ = 0xA2, // Flag to read the current position of the encoder + RE_OP = 0xA3, // Flag for operation start/end, sent to when sending feedback on move start/end + RE_MOVE = 0xA4, // Flag for sending a position update from the device driver to the encoder }; }; diff --git a/MotorDriver.cpp b/MotorDriver.cpp index 678a28a..dc82044 100644 --- a/MotorDriver.cpp +++ b/MotorDriver.cpp @@ -27,7 +27,7 @@ #include "DCCTimer.h" #include "DIAG.h" -bool MotorDriver::commonFaultPin=false; +unsigned long MotorDriver::globalOverloadStart = 0; volatile portreg_t shadowPORTA; volatile portreg_t shadowPORTB; @@ -108,8 +108,13 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i } currentPin=current_pin; - if (currentPin!=UNUSED_PIN) - ADCee::init(currentPin); + if (currentPin!=UNUSED_PIN) { + int ret = ADCee::init(currentPin); + if (ret < -1010) { // XXX give value a name later + DIAG(F("ADCee::init error %d, disable current pin %d"), ret, currentPin); + currentPin = UNUSED_PIN; + } + } senseOffset=0; // value can not be obtained until waveform is activated if (fault_pin != UNUSED_PIN) { @@ -130,7 +135,11 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i // float calculations or libraray code. senseFactorInternal=sense_factor * senseScale; tripMilliamps=trip_milliamps; - rawCurrentTripValue=mA2raw(trip_milliamps); +#ifdef MAX_CURRENT + if (MAX_CURRENT > 0 && MAX_CURRENT < tripMilliamps) + tripMilliamps = MAX_CURRENT; +#endif + rawCurrentTripValue=mA2raw(tripMilliamps); if (rawCurrentTripValue + senseOffset > ADCee::ADCmax()) { // This would mean that the values obtained from the ADC never @@ -154,11 +163,7 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i // senseFactorInternal, raw2mA(1000),mA2raw(1000)); } - // prepare values for current detection - sampleDelay = 0; - lastSampleTaken = millis(); progTripValue = mA2raw(TRIP_CURRENT_PROG); - } bool MotorDriver::isPWMCapable() { @@ -167,7 +172,12 @@ bool MotorDriver::isPWMCapable() { void MotorDriver::setPower(POWERMODE mode) { - bool on=mode==POWERMODE::ON; + if (powerMode == mode) return; + //DIAG(F("Track %c POWERMODE=%d"), trackLetter, (int)mode); + lastPowerChange[(int)mode] = micros(); + if (mode == POWERMODE::OVERLOAD) + globalOverloadStart = lastPowerChange[(int)mode]; + bool on=(mode==POWERMODE::ON || mode ==POWERMODE::ALERT); if (on) { // when switching a track On, we need to check the crrentOffset with the pin OFF if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) { @@ -207,8 +217,8 @@ bool MotorDriver::canMeasureCurrent() { return currentPin!=UNUSED_PIN; } /* - * Return the current reading as pin reading 0 to 1023. If the fault - * pin is activated return a negative current to show active fault pin. + * Return the current reading as pin reading 0 to max resolution (1024 or 4096). + * If the fault pin is activated return a negative current to show active fault pin. * As there is no -0, cheat a little and return -1 in that case. * * senseOffset handles the case where a shield returns values above or below @@ -366,64 +376,166 @@ void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & res // DIAG(F(" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH); } +/////////////////////////////////////////////////////////////////////////////////////////// +// checkPowerOverload(useProgLimit, trackno) +// bool useProgLimit: Trackmanager knows if this track is in prog mode or in main mode +// byte trackno: trackmanager knows it's number (could be skipped?) +// +// Short ciruit handling strategy: +// +// There are the following power states: ON ALERT OVERLOAD OFF +// OFF state is only changed to/from manually. Power is on +// during ON and ALERT. Power is off during OVERLOAD and OFF. +// The overload mechanism changes between the other states like +// +// ON -1-> ALERT -2-> OVERLOAD -3-> ALERT -4-> ON +// or +// ON -1-> ALERT -4-> ON +// +// Times are in class MotorDriver (MotorDriver.h). +// +// 1. ON to ALERT: +// Transition on fault pin condition or current overload +// +// 2. ALERT to OVERLOAD: +// Transition happens if different timeouts have elapsed. +// If only the fault pin is active, timeout is +// POWER_SAMPLE_IGNORE_FAULT_LOW (100ms) +// If only overcurrent is detected, timeout is +// POWER_SAMPLE_IGNORE_CURRENT (100ms) +// If fault pin and overcurrent are active, timeout is +// POWER_SAMPLE_IGNORE_FAULT_HIGH (5ms) +// Transition to OVERLOAD turns off power to the affected +// output (unless fault pins are shared) +// If the transition conditions are not fullfilled, +// transition according to 4 is tested. +// +// 3. OVERLOAD to ALERT +// Transiton happens when timeout has elapsed, timeout +// is named power_sample_overload_wait. It is started +// at POWER_SAMPLE_OVERLOAD_WAIT (40ms) at first entry +// to OVERLOAD and then increased by a factor of 2 +// at further entries to the OVERLOAD condition. This +// happens until POWER_SAMPLE_RETRY_MAX (10sec) is reached. +// power_sample_overload_wait is reset by a poweroff or +// a POWER_SAMPLE_ALL_GOOD (5sec) period during ON. +// After timeout power is turned on again and state +// goes back to ALERT. +// +// 4. ALERT to ON +// Transition happens by watching the current and fault pin +// samples during POWER_SAMPLE_ALERT_GOOD (20ms) time. If +// values have been good during that time, transition is +// made back to ON. Note that even if state is back to ON, +// the power_sample_overload_wait time is first reset +// later (see above). +// +// The time keeping is handled by timestamps lastPowerChange[] +// which are set by each power change and by lastBadSample which +// keeps track if conditions during ALERT have been good enough +// to go back to ON. The time differences are calculated by +// microsSinceLastPowerChange(). +// + void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) { - if (millis() - lastSampleTaken < sampleDelay) return; - lastSampleTaken = millis(); - int tripValue= useProgLimit?progTripValue:getRawCurrentTripValue(); - - // Trackname for diag messages later + switch (powerMode) { - case POWERMODE::OFF: - sampleDelay = POWER_SAMPLE_OFF_WAIT; - break; - case POWERMODE::ON: - // Check current - lastCurrent=getCurrentRaw(); - if (lastCurrent < 0) { - // We have a fault pin condition to take care of - lastCurrent = -lastCurrent; - setPower(POWERMODE::OVERLOAD); // Turn off, decide later how fast to turn on again - if (commonFaultPin) { - if (lastCurrent < tripValue) { - setPower(POWERMODE::ON); // maybe other track - } - // Write this after the fact as we want to turn on as fast as possible - // because we don't know which output actually triggered the fault pin - DIAG(F("COMMON FAULT PIN ACTIVE: POWERTOGGLE TRACK %c"), trackno + 'A'); - } else { - DIAG(F("TRACK %c FAULT PIN ACTIVE - OVERLOAD"), trackno + 'A'); - if (lastCurrent < tripValue) { - lastCurrent = tripValue; // exaggerate - } - } - } - if (lastCurrent < tripValue) { - sampleDelay = POWER_SAMPLE_ON_WAIT; - if(power_good_counter<100) - power_good_counter++; - else - if (power_sample_overload_wait>POWER_SAMPLE_OVERLOAD_WAIT) power_sample_overload_wait=POWER_SAMPLE_OVERLOAD_WAIT; + + case POWERMODE::OFF: { + lastPowerMode = POWERMODE::OFF; + power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT; + break; + } + + case POWERMODE::ON: { + lastPowerMode = POWERMODE::ON; + bool cF = checkFault(); + bool cC = checkCurrent(useProgLimit); + if(cF || cC ) { + if (cC) { + unsigned int mA=raw2mA(lastCurrent); + DIAG(F("TRACK %c ALERT %s %dmA"), trackno + 'A', + cF ? "FAULT" : "", + mA); } else { - setPower(POWERMODE::OVERLOAD); - unsigned int mA=raw2mA(lastCurrent); - unsigned int maxmA=raw2mA(tripValue); - power_good_counter=0; - sampleDelay = power_sample_overload_wait; - DIAG(F("TRACK %c POWER OVERLOAD %dmA (limit %dmA) shutdown for %dms"), trackno + 'A', mA, maxmA, sampleDelay); - if (power_sample_overload_wait >= 10000) - power_sample_overload_wait = 10000; - else - power_sample_overload_wait *= 2; + DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A'); } + setPower(POWERMODE::ALERT); break; - case POWERMODE::OVERLOAD: - // Try setting it back on after the OVERLOAD_WAIT + } + // all well + if (microsSinceLastPowerChange(POWERMODE::ON) > POWER_SAMPLE_ALL_GOOD) { + power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT; + } + break; + } + + case POWERMODE::ALERT: { + // set local flags that handle how much is output to diag (do not output duplicates) + bool notFromOverload = (lastPowerMode != POWERMODE::OVERLOAD); + bool newPowerMode = (powerMode != lastPowerMode); + unsigned long now = micros(); + if (newPowerMode) + lastBadSample = now; + lastPowerMode = POWERMODE::ALERT; + // check how long we have been in this state + unsigned long mslpc = microsSinceLastPowerChange(POWERMODE::ALERT); + if(checkFault()) { + lastBadSample = now; + unsigned long timeout = checkCurrent(useProgLimit) ? POWER_SAMPLE_IGNORE_FAULT_HIGH : POWER_SAMPLE_IGNORE_FAULT_LOW; + if ( mslpc < timeout) { + if (newPowerMode) + DIAG(F("TRACK %c FAULT PIN (%M ignore)"), trackno + 'A', timeout); + break; + } + DIAG(F("TRACK %c FAULT PIN detected after %4M. Pause %4M)"), trackno + 'A', mslpc, power_sample_overload_wait); + setPower(POWERMODE::OVERLOAD); + break; + } + if (checkCurrent(useProgLimit)) { + lastBadSample = now; + if (mslpc < POWER_SAMPLE_IGNORE_CURRENT) { + if (newPowerMode) { + unsigned int mA=raw2mA(lastCurrent); + DIAG(F("TRACK %c CURRENT (%M ignore) %dmA"), trackno + 'A', POWER_SAMPLE_IGNORE_CURRENT, mA); + } + break; + } + unsigned int mA=raw2mA(lastCurrent); + unsigned int maxmA=raw2mA(tripValue); + DIAG(F("TRACK %c POWER OVERLOAD %4dmA (max %4dmA) detected after %4M. Pause %4M"), + trackno + 'A', mA, maxmA, mslpc, power_sample_overload_wait); + setPower(POWERMODE::OVERLOAD); + break; + } + // all well + unsigned long goodtime = micros() - lastBadSample; + if (goodtime > POWER_SAMPLE_ALERT_GOOD) { + if (true || notFromOverload) { // we did a RESTORE message XXX + unsigned int mA=raw2mA(lastCurrent); + DIAG(F("TRACK %c NORMAL (after %M/%M) %dmA"), trackno + 'A', goodtime, mslpc, mA); + } setPower(POWERMODE::ON); - sampleDelay = POWER_SAMPLE_ON_WAIT; - // Debug code.... - DIAG(F("TRACK %c POWER RESTORE (check %dms)"), trackno + 'A', sampleDelay); - break; - default: - sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning. + } + break; + } + + case POWERMODE::OVERLOAD: { + lastPowerMode = POWERMODE::OVERLOAD; + unsigned long mslpc = (commonFaultPin ? (micros() - globalOverloadStart) : microsSinceLastPowerChange(POWERMODE::OVERLOAD)); + if (mslpc > power_sample_overload_wait) { + // adjust next wait time + power_sample_overload_wait *= 2; + if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX) + power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX; + // power on test + DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc); + setPower(POWERMODE::ALERT); + } + break; + } + + default: + break; } } diff --git a/MotorDriver.h b/MotorDriver.h index be77d2d..454015d 100644 --- a/MotorDriver.h +++ b/MotorDriver.h @@ -107,7 +107,7 @@ extern volatile portreg_t shadowPORTA; extern volatile portreg_t shadowPORTB; extern volatile portreg_t shadowPORTC; -enum class POWERMODE : byte { OFF, ON, OVERLOAD }; +enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT }; class MotorDriver { public: @@ -175,7 +175,10 @@ class MotorDriver { bool isPWMCapable(); bool canMeasureCurrent(); bool trackPWM = false; // this track uses PWM timer to generate the DCC waveform - static bool commonFaultPin; // This is a stupid motor shield which has only a common fault pin for both outputs + bool commonFaultPin = false; // This is a stupid motor shield which has only a common fault pin for both outputs + inline byte setCommonFaultPin() { + return commonFaultPin = true; + } inline byte getFaultPin() { return faultPin; } @@ -186,6 +189,16 @@ class MotorDriver { inline void setTrackLetter(char c) { trackLetter = c; }; + // this returns how much time has passed since the last power change. If it + // was really long ago (approx > 52min) advance counter approx 35 min so that + // we are at 18 minutes again. Times for 32 bit unsigned long. + inline unsigned long microsSinceLastPowerChange(POWERMODE mode) { + unsigned long now = micros(); + unsigned long diff = now - lastPowerChange[(int)mode]; + if (diff > (1UL << (7 *sizeof(unsigned long)))) // 2^(4*7)us = 268.4 seconds + lastPowerChange[(int)mode] = now - 30000000UL; // 30 seconds ago + return diff; + }; #ifdef ANALOG_READ_INTERRUPT bool sampleCurrentFromHW(); void startCurrentFromHW(); @@ -194,9 +207,22 @@ class MotorDriver { 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) { + inline void getFastPin(const FSH* type,int pin, FASTPIN & result) { getFastPin(type, pin, 0, result); - } + }; + // side effect sets lastCurrent and tripValue + inline bool checkCurrent(bool useProgLimit) { + tripValue= useProgLimit?progTripValue:getRawCurrentTripValue(); + lastCurrent = getCurrentRaw(); + if (lastCurrent < 0) + lastCurrent = -lastCurrent; + return lastCurrent >= tripValue; + }; + // side effect sets lastCurrent + inline bool checkFault() { + lastCurrent = getCurrentRaw(); + return lastCurrent < 0; + }; VPIN powerPin; byte signalPin, signalPin2, currentPin, faultPin, brakePin; FASTPIN fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin; @@ -217,10 +243,14 @@ class MotorDriver { int rawCurrentTripValue; // current sampling POWERMODE powerMode; - unsigned long lastSampleTaken; - unsigned int sampleDelay; + POWERMODE lastPowerMode; + unsigned long lastPowerChange[4]; // timestamp in microseconds + unsigned long lastBadSample; // timestamp in microseconds + // used to sync restore time when common Fault pin detected + static unsigned long globalOverloadStart; // timestamp in microseconds int progTripValue; - int lastCurrent; + int lastCurrent; //temp value + int tripValue; //temp value #ifdef ANALOG_READ_INTERRUPT volatile unsigned long sampleCurrentTimestamp; volatile uint16_t sampleCurrent; @@ -228,10 +258,21 @@ class MotorDriver { int maxmA; int tripmA; - // Wait times for power management. Unit: milliseconds - static const int POWER_SAMPLE_ON_WAIT = 100; - static const int POWER_SAMPLE_OFF_WAIT = 1000; - static const int POWER_SAMPLE_OVERLOAD_WAIT = 20; + // Times for overload management. Unit: microseconds. + // Base for wait time until power is turned on again + static const unsigned long POWER_SAMPLE_OVERLOAD_WAIT = 40000UL; + // Time after we consider all faults old and forgotten + static const unsigned long POWER_SAMPLE_ALL_GOOD = 5000000UL; + // Time after which we consider a ALERT over + static const unsigned long POWER_SAMPLE_ALERT_GOOD = 20000UL; + // How long to ignore fault pin if current is under limit + static const unsigned long POWER_SAMPLE_IGNORE_FAULT_LOW = 100000UL; + // How long to ignore fault pin if current is higher than limit + static const unsigned long POWER_SAMPLE_IGNORE_FAULT_HIGH = 5000UL; + // How long to wait between overcurrent and turning off + static const unsigned long POWER_SAMPLE_IGNORE_CURRENT = 100000UL; + // Upper limit for retry period + static const unsigned long POWER_SAMPLE_RETRY_MAX = 10000000UL; // Trip current for programming track, 250mA. Change only if you really // need to be non-NMRA-compliant because of decoders that are not either. diff --git a/StringFormatter.cpp b/StringFormatter.cpp index 470972e..0ace1f4 100644 --- a/StringFormatter.cpp +++ b/StringFormatter.cpp @@ -120,6 +120,24 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) { case 'o': stream->print(va_arg(args, int), OCT); break; case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break; case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break; + case 'M': + { // this prints a unsigned long microseconds time in readable format + unsigned long time = va_arg(args, long); + if (time >= 2000) { + time = time / 1000; + if (time >= 2000) { + printPadded(stream, time/1000, formatWidth, formatLeft); + stream->print(F("sec")); + } else { + printPadded(stream,time, formatWidth, formatLeft); + stream->print(F("msec")); + } + } else { + printPadded(stream,time, formatWidth, formatLeft); + stream->print(F("usec")); + } + } + break; //case 'f': stream->print(va_arg(args, double), 2); break; //format width prefix case '-': diff --git a/TrackManager.cpp b/TrackManager.cpp index 9c0f0b0..8f66677 100644 --- a/TrackManager.cpp +++ b/TrackManager.cpp @@ -123,10 +123,18 @@ void TrackManager::Setup(const FSH * shieldname, setTrackMode(1,TRACK_MODE_MAIN); #endif - // TODO Fault pin config for odd motor boards (example pololu) - // MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin()) - // && (mainDriver->getFaultPin() != UNUSED_PIN)); - DCC::begin(shieldname); + // Fault pin config for odd motor boards (example pololu) + FOR_EACH_TRACK(t) { + for (byte s=t+1;s<=lastTrack;s++) { + if (track[t]->getFaultPin() != UNUSED_PIN && + track[t]->getFaultPin() == track[s]->getFaultPin()) { + track[t]->setCommonFaultPin(); + track[s]->setCommonFaultPin(); + DIAG(F("Common Fault pin tracks %c and %c"), t+'A', s+'A'); + } + } + } + DCC::setShieldName(shieldname); } void TrackManager::addTrack(byte t, MotorDriver* driver) { diff --git a/TrackManager.h b/TrackManager.h index ef4a47c..19e756d 100644 --- a/TrackManager.h +++ b/TrackManager.h @@ -84,8 +84,15 @@ class TrackManager { 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 - + static bool progTrackBoosted; // true when prog track is not current limited + +#ifdef DEBUG_ADC + public: +#else + private: +#endif + static MotorDriver* track[MAX_TRACKS]; + private: static void addTrack(byte t, MotorDriver* driver); static byte lastTrack; @@ -93,7 +100,6 @@ class TrackManager { static POWERMODE mainPowerGuess; static void applyDCSpeed(byte t); - static MotorDriver* track[MAX_TRACKS]; static TRACK_MODE trackMode[MAX_TRACKS]; static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX #ifdef ARDUINO_ARCH_ESP32 diff --git a/WifiESP32.cpp b/WifiESP32.cpp index cd10ffc..3826ba2 100644 --- a/WifiESP32.cpp +++ b/WifiESP32.cpp @@ -1,5 +1,7 @@ /* - © 2021, Harald Barth. + © 2023 Paul M. Antoine + © 2021 Harald Barth + © 2023 Nathan Kellenicki This file is part of CommandStation-EX @@ -20,6 +22,7 @@ #if defined(ARDUINO_ARCH_ESP32) #include #include "defines.h" +#include "ESPmDNS.h" #include #include "esp_wifi.h" #include "WifiESP32.h" @@ -106,11 +109,18 @@ void wifiLoop(void *){ } #endif +char asciitolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + bool WifiESP::setup(const char *SSid, const char *password, const char *hostname, int port, - const byte channel) { + const byte channel, + const bool forceAP) { bool havePassword = true; bool haveSSID = true; bool wifiUp = false; @@ -139,7 +149,8 @@ bool WifiESP::setup(const char *SSid, if (strncmp(yourNetwork, password, 13) == 0 || strncmp("", password, 13) == 0) havePassword = false; - if (haveSSID && havePassword) { + if (haveSSID && havePassword && !forceAP) { + WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE! WiFi.mode(WIFI_STA); #ifdef SERIAL_BT_COMMANDS WiFi.setSleep(true); @@ -176,16 +187,20 @@ bool WifiESP::setup(const char *SSid, } } } - if (!haveSSID) { + if (!haveSSID || forceAP) { // prepare all strings - String strSSID("DCC_"); - String strPass("PASS_"); - String strMac = WiFi.macAddress(); - strMac.remove(0,9); - strMac.replace(":",""); - strMac.replace(":",""); - strSSID.concat(strMac); - strPass.concat(strMac); + String strSSID(forceAP ? SSid : "DCCEX_"); + String strPass(forceAP ? password : "PASS_"); + if (!forceAP) { + String strMac = WiFi.macAddress(); + strMac.remove(0,9); + strMac.replace(":",""); + strMac.replace(":",""); + // convert mac addr hex chars to lower case to be compatible with AT software + std::transform(strMac.begin(), strMac.end(), strMac.begin(), asciitolower); + strSSID.concat(strMac); + strPass.concat(strMac); + } WiFi.mode(WIFI_AP); #ifdef SERIAL_BT_COMMANDS @@ -211,6 +226,15 @@ bool WifiESP::setup(const char *SSid, // no idea to go on return false; } + + // Now Wifi is up, register the mDNS service + if(!MDNS.begin(hostname)) { + DIAG(F("Wifi setup failed to start mDNS")); + } + if(!MDNS.addService("withrottle", "tcp", 2560)) { + DIAG(F("Wifi setup failed to add withrottle service to mDNS")); + } + server = new WiFiServer(port); // start listening on tcp port server->begin(); // server started here diff --git a/WifiESP32.h b/WifiESP32.h index 100e393..ec2f560 100644 --- a/WifiESP32.h +++ b/WifiESP32.h @@ -1,5 +1,6 @@ /* - * © 2021, Harald Barth. + * © 2021 Harald Barth + * © 2023 Nathan Kellenicki * * This file is part of CommandStation-EX * @@ -31,7 +32,8 @@ public: const char *wifiPassword, const char *hostname, const int port, - const byte channel); + const byte channel, + const bool forceAP); static void loop(); private: }; diff --git a/WifiInterface.cpp b/WifiInterface.cpp index 732b393..20dc235 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -2,6 +2,7 @@ * © 2021 Fred Decker * © 2020-2022 Harald Barth * © 2020-2022 Chris Harlow + * © 2023 Nathan Kellenicki * All rights reserved. * * This file is part of CommandStation-EX @@ -52,26 +53,39 @@ Stream * WifiInterface::wifiStream; #if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)) #define NUM_SERIAL 3 +#define SERIAL1 Serial1 +#define SERIAL3 Serial3 +#endif + +#if defined(ARDUINO_ARCH_STM32) +// Handle serial ports availability on STM32 for variants! +// #undef NUM_SERIAL +#if defined(ARDUINO_NUCLEO_F411RE) +#define NUM_SERIAL 3 +#define SERIAL1 Serial1 +#define SERIAL3 Serial6 +#elif defined(ARDUINO_NUCLEO_F446RE) +#define NUM_SERIAL 3 +#define SERIAL1 Serial3 +#define SERIAL3 Serial5 +#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) +#define NUM_SERIAL 2 +#define SERIAL1 Serial6 +#endif #endif #ifndef NUM_SERIAL #define NUM_SERIAL 1 +#define SERIAL1 Serial1 #endif -// For STM32 we need to define Serial3 in the platform specific -// DCCTimerSTM32.cpp file, we here make the assumption that it -// exists to link against. -#ifdef ARDUINO_ARCH_STM32 -#if NUM_SERIAL > 2 -extern HardwareSerial Serial3; -#endif -#endif bool WifiInterface::setup(long serial_link_speed, const FSH *wifiESSID, const FSH *wifiPassword, const FSH *hostname, const int port, - const byte channel) { + const byte channel, + const bool forceAP) { wifiSerialState wifiUp = WIFI_NOAT; @@ -83,21 +97,23 @@ bool WifiInterface::setup(long serial_link_speed, (void) hostname; (void) port; (void) channel; + (void) forceAP; #endif - + +// See if the WiFi is attached to the first serial port #if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS) - Serial1.begin(serial_link_speed); - wifiUp = setup(Serial1, wifiESSID, wifiPassword, hostname, port, channel); + SERIAL1.begin(serial_link_speed); + wifiUp = setup(SERIAL1, wifiESSID, wifiPassword, hostname, port, channel, forceAP); #endif // Other serials are tried, depending on hardware. -// Currently only the Arduino Mega 2560 has usable Serial2 +// Currently only the Arduino Mega 2560 has usable Serial2 (Nucleo-64 boards use Serial 2 for console!) #if defined(ARDUINO_AVR_MEGA2560) #if NUM_SERIAL > 1 && !defined(SERIAL2_COMMANDS) if (wifiUp == WIFI_NOAT) { Serial2.begin(serial_link_speed); - wifiUp = setup(Serial2, wifiESSID, wifiPassword, hostname, port, channel); + wifiUp = setup(Serial2, wifiESSID, wifiPassword, hostname, port, channel, forceAP); } #endif #endif @@ -107,8 +123,8 @@ bool WifiInterface::setup(long serial_link_speed, #if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS) if (wifiUp == WIFI_NOAT) { - Serial3.begin(serial_link_speed); - wifiUp = setup(Serial3, wifiESSID, wifiPassword, hostname, port, channel); + SERIAL3.begin(serial_link_speed); + wifiUp = setup(SERIAL3, wifiESSID, wifiPassword, hostname, port, channel, forceAP); } #endif @@ -126,7 +142,7 @@ bool WifiInterface::setup(long serial_link_speed, } wifiSerialState WifiInterface::setup(Stream & setupStream, const FSH* SSid, const FSH* password, - const FSH* hostname, int port, byte channel) { + const FSH* hostname, int port, byte channel, bool forceAP) { wifiSerialState wifiState; static uint8_t ntry = 0; ntry++; @@ -135,7 +151,7 @@ wifiSerialState WifiInterface::setup(Stream & setupStream, const FSH* SSid, con DIAG(F("++ Wifi Setup Try %d ++"), ntry); - wifiState = setup2( SSid, password, hostname, port, channel); + wifiState = setup2( SSid, password, hostname, port, channel, forceAP); if (wifiState == WIFI_NOAT) { LCD(4, F("WiFi no AT chip")); @@ -159,7 +175,7 @@ wifiSerialState WifiInterface::setup(Stream & setupStream, const FSH* SSid, con #pragma GCC diagnostic ignored "-Wunused-parameter" #endif wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, - const FSH* hostname, int port, byte channel) { + const FSH* hostname, int port, byte channel, bool forceAP) { bool ipOK = false; bool oldCmd = false; @@ -212,7 +228,7 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, if (!checkForOK(1000, F("0.0.0.0"), true,false)) ipOK = true; } - } else { + } else if (!forceAP) { // SSID was configured, so we assume station (client) mode. if (oldCmd) { // AT command early version supports CWJAP/CWSAP @@ -272,14 +288,19 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, i=0; do { - if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) { - // unconfigured - StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"DCCEX_%s\",\"PASS_%s\",%d,4\r\n"), - oldCmd ? "" : "_CUR", macTail, macTail, channel); + if (!forceAP) { + if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) { + // unconfigured + StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"DCCEX_%s\",\"PASS_%s\",%d,4\r\n"), + oldCmd ? "" : "_CUR", macTail, macTail, channel); + } else { + // password configured by user + StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"DCCEX_%s\",\"%S\",%d,4\r\n"), oldCmd ? "" : "_CUR", + macTail, password, channel); + } } else { - // password configured by user - StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"DCCEX_%s\",\"%S\",%d,4\r\n"), oldCmd ? "" : "_CUR", - macTail, password, channel); + StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"%S\",\"%S\",%d,4\r\n"), + oldCmd ? "" : "_CUR", SSid, password, channel); } } while (!checkForOK(WIFI_CONNECT_TIMEOUT, true) && i++<2); // do twice if necessary but ignore failure as AP mode may still be ok if (i >= 2) diff --git a/WifiInterface.h b/WifiInterface.h index 7c0a433..652156d 100644 --- a/WifiInterface.h +++ b/WifiInterface.h @@ -1,6 +1,7 @@ /* * © 2020-2021 Chris Harlow * © 2020, Harald Barth. + * © 2023 Nathan Kellenicki * All rights reserved. * * This file is part of CommandStation-EX @@ -36,17 +37,18 @@ public: const FSH *wifiPassword, const FSH *hostname, const int port, - const byte channel); + const byte channel, + const bool forceAP); static void loop(); static void ATCommand(HardwareSerial * stream,const byte *command); private: static wifiSerialState setup(Stream &setupStream, const FSH *SSSid, const FSH *password, - const FSH *hostname, int port, byte channel); + const FSH *hostname, int port, byte channel, bool forceAP); static Stream *wifiStream; static DCCEXParser parser; static wifiSerialState setup2(const FSH *SSSid, const FSH *password, - const FSH *hostname, int port, byte channel); + const FSH *hostname, int port, byte channel, bool forceAP); static bool checkForOK(const unsigned int timeout, bool echo, bool escapeEcho = true); static bool checkForOK(const unsigned int timeout, const FSH *waitfor, bool echo, bool escapeEcho = true); static bool connected; diff --git a/config.example.h b/config.example.h index 16402b6..0f136f9 100644 --- a/config.example.h +++ b/config.example.h @@ -4,6 +4,7 @@ * © 2020-2023 Harald Barth * © 2020-2021 Fred Decker * © 2020-2021 Chris Harlow + * © 2023 Nathan Kellenicki * * This file is part of CommandStation-EX * @@ -57,6 +58,21 @@ The configuration file for DCC-EX Command Station // +-----------------------v // #define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD +// +///////////////////////////////////////////////////////////////////////////////////// +// +// If you want to restrict the maximum current LOWER than what your +// motor shield can provide, you can do that here. For example if you +// have a motor shield that can provide 5A and your power supply can +// only provide 2.5A then you should restict the maximum current to +// 2.25A (90% of 2.5A) so that DCC-EX does shut off the track before +// your PS does shut DCC-EX. MAX_CURRENT is in mA so for this example +// it would be 2250, adjust the number according to your PS. If your +// PS has a higher rating than your motor shield you do not need this. +// You can use this as well if you are cautious and your trains do not +// need full current. +// #define MAX_CURRENT 2250 +// ///////////////////////////////////////////////////////////////////////////////////// // // The IP port to talk to a WIFI or Ethernet shield. @@ -108,6 +124,11 @@ The configuration file for DCC-EX Command Station // this line exists or not. If you need to use an alternate channel (we recommend // using only 1,6, or 11) you may change it here. #define WIFI_CHANNEL 1 +// +// WIFI_FORCE_AP: If you'd like to specify your own WIFI_SSID in AP mode, set this +// true. Otherwise it is assumed that you'd like to connect to an existing network +// with that SSID. +#define WIFI_FORCE_AP false ///////////////////////////////////////////////////////////////////////////////////// // diff --git a/defines.h b/defines.h index 3f5c3ba..f40ed2f 100644 --- a/defines.h +++ b/defines.h @@ -147,8 +147,6 @@ #ifndef I2C_USE_WIRE #define I2C_USE_WIRE #endif - #undef NUM_SERIAL - #define NUM_SERIAL 3 /* TODO when ready #elif defined(ARDUINO_ARCH_RP2040) @@ -207,7 +205,7 @@ #define WIFI_SERIAL_LINK_SPEED 115200 #if __has_include ( "myAutomation.h") - #if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) + #if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) || defined(DISABLE_PROG) #define EXRAIL_ACTIVE #else #define EXRAIL_WARNING diff --git a/installer.sh b/installer.sh index 17a9c04..857710e 100755 --- a/installer.sh +++ b/installer.sh @@ -1,7 +1,7 @@ #!/bin/bash # -# © 2022 Harald Barth +# © 2022,2023 Harald Barth # # This file is part of CommandStation-EX # @@ -29,14 +29,33 @@ ACLI="./bin/arduino-cli" function need () { type -p $1 > /dev/null && return + dpkg -l $1 2>&1 | egrep ^ii >/dev/null && return sudo apt-get install $1 type -p $1 > /dev/null && return echo "Could not install $1, abort" exit 255 } - need git + +if cat /etc/issue | egrep '^Raspbian' 2>&1 >/dev/null ; then + # we are on a raspi where we do not support graphical + unset DISPLAY +fi + +if [ x$DISPLAY != x ] ; then + # we have DISPLAY, do the graphic thing + need python3-tk + need python3.8-venv + mkdir -p ~/ex-installer/venv + python3 -m venv ~/ex-installer/venv + cd ~/ex-installer/venv || exit 255 + source ./bin/activate + git clone https://github.com/DCC-EX/EX-Installer + cd EX-Installer || exit 255 + pip3 install -r requirements.txt + exec python3 -m ex_installer +fi if test -d `basename "$DCCEXGITURL"` ; then : assume we are almost there cd `basename "$DCCEXGITURL"` || exit 255 diff --git a/platformio.ini b/platformio.ini index d85c76c..1a87770 100644 --- a/platformio.ini +++ b/platformio.ini @@ -173,6 +173,8 @@ board = esp32dev framework = arduino lib_deps = ${env.lib_deps} build_flags = -std=c++17 +monitor_speed = 115200 +monitor_echo = yes [env:Nucleo-F411RE] platform = ststm32 @@ -188,7 +190,7 @@ platform = ststm32 board = nucleo_f446re framework = arduino lib_deps = ${env.lib_deps} -build_flags = -std=c++17 -Os -g2 -Wunused-variable -DDIAG_LOOPTIMES ; -DDIAG_IO +build_flags = -std=c++17 -Os -g2 -Wunused-variable ; -DDIAG_LOOPTIMES ; -DDIAG_IO monitor_speed = 115200 monitor_echo = yes diff --git a/version.h b/version.h index e66e1be..7f6bdc7 100644 --- a/version.h +++ b/version.h @@ -3,9 +3,23 @@ #include "StringFormatter.h" - -#define VERSION "4.2.54-Z21" -// Z21 experiments ongoing +#define VERSION "4.2.64" +// 4.2.64 - new config WIFI_FORCE_AP option +// 4.2.63 - completely new overcurrent detection +// - ESP32 protect from race in RMT code +// 4.2.62 - Update IO_RotaryEncoder.h to ignore sending current position +// - Update IO_EXTurntable.h to remove forced I2C clock speed +// - Show device offline if EX-Turntable not connected +// 4.2.61 - MAX_CURRENT restriction (caps motor shield value) +// 4.2.60 - Add mDNS capability to ESP32 for autodiscovery +// 4.2.59 - Fix: AP SSID was DCC_ instead of DCCEX_ +// 4.2.58 - Start motordriver as soon as possible but without waveform +// 4.2.57 - New overload handling (faster and handles commonFaultPin again) +// - Optimize analog read STM32 +// 4.2.56 - Update IO_RotaryEncoder.h: +// - Improved I2C communication, non-blocking reads +// - Enable sending positions to the encoder from EXRAIL via SERVO() +// 4.2.55 - Optimize analog read for AVR // 4.2.54 - EX8874 shield in config.example.h // - Fix: Better warnings for pin number errors // - Fix: Default roster list possible in Withrottle and