1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-29 18:33:44 +02:00

Compare commits

...

34 Commits

Author SHA1 Message Date
Harald Barth
daa2ffc459 tag 2024-01-20 23:36:11 +01:00
Harald Barth
9728d19b19 eliminate warning 2024-01-20 23:35:30 +01:00
Harald Barth
99a09c713f To make usage easier, use F29 to F31 for frequencies 2024-01-20 23:34:17 +01:00
Colin Murdoch
a5b73c823a Added SETFREQ command
Added SETFREQ command to EXRAIL
2024-01-20 18:09:03 +00:00
Harald Barth
8036ba1c48 temp version tag 2024-01-03 02:44:15 +01:00
Harald Barth
6f076720f7 temp version tag 2024-01-01 22:17:47 +01:00
Harald Barth
d899da5898 Make return type of DCC::getFn int8_t 2024-01-01 22:08:59 +01:00
Harald Barth
3ce9d2ec88 DC frequency fix broadcast messages step #7 2024-01-01 22:08:04 +01:00
Harald Barth
9ebb1c5fb1 less debug diag 2024-01-01 21:25:43 +01:00
Harald Barth
19efa749b8 Typo fix HAS vs HAVE 2023-12-31 17:57:30 +01:00
Harald Barth
36cc46e88d DC frequency dummy functions for odd architectures step #6 2023-12-31 13:52:37 +01:00
Harald Barth
bba74a08f6 Do not support obsolete <c> on memory tight arch 2023-12-31 13:22:42 +01:00
Harald Barth
ab58c38e7b motordriver frequency diag 2023-12-31 13:22:34 +01:00
Harald Barth
d4f0a7c8f3 DC frequency uno does not have timers anyway step #5 2023-12-31 13:18:28 +01:00
Harald Barth
ba0a41b6f2 DC frequency fix bit shifting (debug code) step #4 2023-12-31 10:48:48 +01:00
Harald Barth
bf17f2018b fix type and static warning step #3 2023-12-30 22:20:41 +01:00
Harald Barth
67387d2dc3 function bits to freqency step #2 2023-12-30 22:09:01 +01:00
Harald Barth
adb8b56c92 variable frequency step #1 2023-12-30 21:23:44 +01:00
Harald Barth
bd44184f57 version 5.2.17 2023-12-25 17:49:16 +01:00
Harald Barth
e7d3d92c23 as no other tasks run on core1, yield() not necessary 2023-12-25 17:40:29 +01:00
Harald Barth
e3bab887a2 simplify WifiESP32 2023-12-25 17:32:39 +01:00
Harald Barth
041a6534da more diag and inUse tests 2023-12-24 12:03:42 +01:00
pmantoine
198d762a21 Add F439ZI serial setup in WifiInterface 2023-12-22 12:29:17 +08:00
peteGSX
1398cf1999 Merge pull request #373 from DCC-EX:ex-ioexpander-no-analogue-pins-fix
Ex-ioexpander-no-analogue-pins-fix
2023-12-19 18:49:18 +10:00
peteGSX
797028b223 Ready to test 2023-12-19 07:30:15 +10:00
Harald Barth
1881d4c9ad version 5.2.15 2023-12-13 11:41:57 +01:00
Harald Barth
18116a391c move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions 2023-12-13 11:40:15 +01:00
Harald Barth
a1accec79a add repeats to function packets that are not reminded in accordance with accessory packets 2023-12-13 10:55:58 +01:00
Asbelos
08f0a2b37d 5.2.13 2023-11-30 19:56:58 +00:00
Asbelos
6637ea6fe7 Merge branch 'devel_reminders' into devel 2023-11-30 19:54:20 +00:00
Asbelos
a69017f8bb Optional DISABLE_FUNCTION_REMINDERS 2023-11-30 19:48:02 +00:00
Asbelos
763c9d8ae6 STEALTH 2023-11-30 11:32:39 +00:00
Asbelos
3f4099520a ESP32 update for reminders 2023-11-28 19:57:14 +00:00
Asbelos
07fd4bc309 Window 2023-11-27 16:49:02 +00:00
27 changed files with 423 additions and 228 deletions

View File

@@ -209,7 +209,9 @@ int16_t CommandDistributor::retClockTime() {
void CommandDistributor::broadcastLoco(byte slot) { void CommandDistributor::broadcastLoco(byte slot) {
DCC::LOCO * sp=&DCC::speedTable[slot]; DCC::LOCO * sp=&DCC::speedTable[slot];
broadcastReply(COMMAND_TYPE, F("<l %d %d %d %l>\n"), sp->loco,slot,sp->speedCode,sp->functions); uint32_t func = sp->functions;
func = func & 0x1fffffff; // mask out bits 0-28
broadcastReply(COMMAND_TYPE, F("<l %d %d %d %l>\n"), sp->loco,slot,sp->speedCode,func);
#ifdef SABERTOOTH #ifdef SABERTOOTH
if (Serial2 && sp->loco == SABERTOOTH) { if (Serial2 && sp->loco == SABERTOOTH) {
static uint8_t rampingmode = 0; static uint8_t rampingmode = 0;
@@ -283,7 +285,7 @@ void CommandDistributor::broadcastPower() {
//DIAG(F("m=%d p=%d j=%d"), main, prog, join); //DIAG(F("m=%d p=%d j=%d"), main, prog, join);
const FSH * reason=F(""); const FSH * reason=F("");
if (join) { if (join) {
reason = F("JOIN"); reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason); broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
} else { } else {
if (main) { if (main) {
@@ -303,7 +305,7 @@ void CommandDistributor::broadcastPower() {
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state); broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
#endif #endif
LCD(2,F("Power %S %S"),state=='1'?F("On"): ( state=='0'? F("Off") : F("SC") ),reason); LCD(2,F("Power %S%S"),state=='1'?F("On"): ( state=='0'? F("Off") : F("SC") ),reason);
} }
void CommandDistributor::broadcastRaw(clientType type, char * msg) { void CommandDistributor::broadcastRaw(clientType type, char * msg) {

82
DCC.cpp
View File

@@ -122,7 +122,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
DCCWaveform::mainTrack.schedulePacket(b, nB, 0); DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
} }
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) { void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2); // DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
byte b[4]; byte b[4];
byte nB = 0; byte nB = 0;
@@ -133,7 +133,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
if (byte1!=0) b[nB++] = byte1; if (byte1!=0) b[nB++] = byte1;
b[nB++] = byte2; b[nB++] = byte2;
DCCWaveform::mainTrack.schedulePacket(b, nB, 0); DCCWaveform::mainTrack.schedulePacket(b, nB, count);
} }
// returns speed steps 0 to 127 (1 == emergency stop) // returns speed steps 0 to 127 (1 == emergency stop)
@@ -153,6 +153,22 @@ uint8_t DCC::getThrottleSpeedByte(int cab) {
return speedTable[reg].speedCode; return speedTable[reg].speedCode;
} }
// returns 0 to 7 for frequency
uint8_t DCC::getThrottleFrequency(int cab) {
#if defined(ARDUINO_AVR_UNO)
(void)cab;
return 0;
#else
int reg=lookupSpeedTable(cab);
if (reg<0)
return 0; // use default frequency
// shift out first 29 bits so we have the 3 "frequency bits" left
uint8_t res = (uint8_t)(speedTable[reg].functions >>29);
//DIAG(F("Speed table %d functions %l shifted %d"), reg, speedTable[reg].functions, res);
return res;
#endif
}
// returns direction on loco // returns direction on loco
// or true/forward on "loco not found" // or true/forward on "loco not found"
bool DCC::getThrottleDirection(int cab) { bool DCC::getThrottleDirection(int cab) {
@@ -183,43 +199,54 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
b[nB++] = functionNumber >>7 ; // high order bits b[nB++] = functionNumber >>7 ; // high order bits
} }
DCCWaveform::mainTrack.schedulePacket(b, nB, 4); DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
return true;
} }
// We use the reminder table up to 28 for normal functions.
// We use 29 to 31 for DC frequency as well so up to 28
// are "real" functions and 29 to 31 are frequency bits
// controlled by function buttons
if (functionNumber > 31)
return true;
int reg = lookupSpeedTable(cab); int reg = lookupSpeedTable(cab);
if (reg<0) return false; if (reg<0) return false;
// Take care of functions: // Take care of functions:
// Set state of function // Set state of function
unsigned long previous=speedTable[reg].functions; uint32_t previous=speedTable[reg].functions;
unsigned long funcmask = (1UL<<functionNumber); uint32_t funcmask = (1UL<<functionNumber);
if (on) { if (on) {
speedTable[reg].functions |= funcmask; speedTable[reg].functions |= funcmask;
} else { } else {
speedTable[reg].functions &= ~funcmask; speedTable[reg].functions &= ~funcmask;
} }
if (speedTable[reg].functions != previous) { if (speedTable[reg].functions != previous && functionNumber <= 28) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber); updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg); CommandDistributor::broadcastLoco(reg);
} }
return true; return true;
} }
// Flip function state // Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) { void DCC::changeFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28) return; if (cab<=0 || functionNumber>31) return;
int reg = lookupSpeedTable(cab); int reg = lookupSpeedTable(cab);
if (reg<0) return; if (reg<0) return;
unsigned long funcmask = (1UL<<functionNumber); unsigned long funcmask = (1UL<<functionNumber);
speedTable[reg].functions ^= funcmask; speedTable[reg].functions ^= funcmask;
updateGroupflags(speedTable[reg].groupFlags, functionNumber); if (functionNumber <= 28) {
CommandDistributor::broadcastLoco(reg); updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
}
} }
int DCC::getFn( int cab, int16_t functionNumber) { // Report function state (used from withrottle protocol)
if (cab<=0 || functionNumber>28) return -1; // unknown // returns 0 false, 1 true or -1 for do not know
int8_t DCC::getFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28)
return -1; // unknown
int reg = lookupSpeedTable(cab); int reg = lookupSpeedTable(cab);
if (reg<0) return -1; if (reg<0)
return -1;
unsigned long funcmask = (1UL<<functionNumber); unsigned long funcmask = (1UL<<functionNumber);
return (speedTable[reg].functions & funcmask)? 1 : 0; return (speedTable[reg].functions & funcmask)? 1 : 0;
@@ -595,7 +622,7 @@ void DCC::loop() {
void DCC::issueReminders() { void DCC::issueReminders() {
// if the main track transmitter still has a pending packet, skip this time around. // if the main track transmitter still has a pending packet, skip this time around.
if ( DCCWaveform::mainTrack.getPacketPending()) return; if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
// Move to next loco slot. If occupied, send a reminder. // Move to next loco slot. If occupied, send a reminder.
int reg = lastLocoReminder+1; int reg = lastLocoReminder+1;
if (reg > highestUsedReg) reg = 0; // Go to start of table if (reg > highestUsedReg) reg = 0; // Go to start of table
@@ -619,24 +646,39 @@ bool DCC::issueReminder(int reg) {
break; break;
case 1: // remind function group 1 (F0-F4) case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1) if (flags & FN_GROUP_1)
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD #ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
#else
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),2);
flags&= ~FN_GROUP_1; // dont send them again
#endif
break; break;
case 2: // remind function group 2 F5-F8 case 2: // remind function group 2 F5-F8
if (flags & FN_GROUP_2) if (flags & FN_GROUP_2)
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD #ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
#else
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),2);
flags&= ~FN_GROUP_2; // dont send them again
#endif
break; break;
case 3: // remind function group 3 F9-F12 case 3: // remind function group 3 F9-F12
if (flags & FN_GROUP_3) if (flags & FN_GROUP_3)
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD #ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
#else
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),2);
flags&= ~FN_GROUP_3; // dont send them again
#endif
break; break;
case 4: // remind function group 4 F13-F20 case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4) if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF)); setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again flags&= ~FN_GROUP_4; // dont send them again
break; break;
case 5: // remind function group 5 F21-F28 case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5) if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF)); setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again flags&= ~FN_GROUP_5; // dont send them again
break; break;
} }

7
DCC.h
View File

@@ -61,13 +61,14 @@ public:
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection); static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
static int8_t getThrottleSpeed(int cab); static int8_t getThrottleSpeed(int cab);
static uint8_t getThrottleSpeedByte(int cab); static uint8_t getThrottleSpeedByte(int cab);
static uint8_t getThrottleFrequency(int cab);
static bool getThrottleDirection(int cab); static bool getThrottleDirection(int cab);
static void writeCVByteMain(int cab, int cv, byte bValue); static void writeCVByteMain(int cab, int cv, byte bValue);
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue); static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
static void setFunction(int cab, byte fByte, byte eByte); static void setFunction(int cab, byte fByte, byte eByte);
static bool setFn(int cab, int16_t functionNumber, bool on); static bool setFn(int cab, int16_t functionNumber, bool on);
static void changeFn(int cab, int16_t functionNumber); static void changeFn(int cab, int16_t functionNumber);
static int getFn(int cab, int16_t functionNumber); static int8_t getFn(int cab, int16_t functionNumber);
static uint32_t getFunctionMap(int cab); static uint32_t getFunctionMap(int cab);
static void updateGroupflags(byte &flags, int16_t functionNumber); static void updateGroupflags(byte &flags, int16_t functionNumber);
static void setAccessory(int address, byte port, bool gate, byte onoff = 2); static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
@@ -98,7 +99,7 @@ public:
int loco; int loco;
byte speedCode; byte speedCode;
byte groupFlags; byte groupFlags;
unsigned long functions; uint32_t functions;
}; };
static LOCO speedTable[MAX_LOCOS]; static LOCO speedTable[MAX_LOCOS];
static int lookupSpeedTable(int locoId, bool autoCreate=true); static int lookupSpeedTable(int locoId, bool autoCreate=true);
@@ -109,7 +110,7 @@ private:
static byte loopStatus; static byte loopStatus;
static void setThrottle2(uint16_t cab, uint8_t speedCode); static void setThrottle2(uint16_t cab, uint8_t speedCode);
static void updateLocoReminder(int loco, byte speedCode); static void updateLocoReminder(int loco, byte speedCode);
static void setFunctionInternal(int cab, byte fByte, byte eByte); static void setFunctionInternal(int cab, byte fByte, byte eByte, byte count);
static bool issueReminder(int reg); static bool issueReminder(int reg);
static int lastLocoReminder; static int lastLocoReminder;
static int highestUsedReg; static int highestUsedReg;

View File

@@ -587,7 +587,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
} }
else break; // will reply <X> else break; // will reply <X>
} }
CommandDistributor::broadcastPower();
//TrackManager::streamTrackState(NULL,t); //TrackManager::streamTrackState(NULL,t);
return; return;
@@ -619,7 +618,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
} }
else break; // will reply <X> else break; // will reply <X>
} }
CommandDistributor::broadcastPower();
return; return;
} }
@@ -627,12 +625,13 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1. DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
return; return;
#ifdef HAS_ENOUGH_MEMORY
case 'c': // SEND METER RESPONSES <c> case 'c': // SEND METER RESPONSES <c>
// No longer useful because of multiple tracks See <JG> and <JI> // No longer useful because of multiple tracks See <JG> and <JI>
if (params>0) break; if (params>0) break;
TrackManager::reportObsoleteCurrent(stream); TrackManager::reportObsoleteCurrent(stream);
return; return;
#endif
case 'Q': // SENSORS <Q> case 'Q': // SENSORS <Q>
Sensor::printAll(stream); Sensor::printAll(stream);
return; return;

View File

@@ -85,6 +85,7 @@ class DCCTimer {
static void reset(); static void reset();
private: private:
static void DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency);
static int freeMemory(); static int freeMemory();
static volatile int minimum_free_memory; static volatile int minimum_free_memory;
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle

View File

@@ -29,6 +29,7 @@
#include <avr/boot.h> #include <avr/boot.h>
#include <avr/wdt.h> #include <avr/wdt.h>
#include "DCCTimer.h" #include "DCCTimer.h"
#include "DIAG.h"
#ifdef DEBUG_ADC #ifdef DEBUG_ADC
#include "TrackManager.h" #include "TrackManager.h"
#endif #endif
@@ -125,6 +126,81 @@ void DCCTimer::reset() {
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// Speed mapping is done like this:
// No functions buttons: 000 0 -> low 131Hz
// Only F29 pressed 001 1 -> mid 490Hz
// F30 with or w/o F29 01x 2-3 -> high 3400Hz
// F31 with or w/o F29/30 1xx 4-7 -> supersonic 62500Hz
uint8_t abits;
uint8_t bbits;
if (pin == 9 || pin == 10) { // timer 2 is different
if (fbits >= 4)
abits = B00000011;
else
abits = B00000001;
if (fbits >= 4)
bbits = B0001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0100;
else // fbits == 0
bbits = B0110;
TCCR2A = (TCCR2A & B11111100) | abits; // set WGM0 and WGM1
TCCR2B = (TCCR2B & B11110000) | bbits; // set WGM2 and 3 bits of prescaler
DIAG(F("Timer 2 A=%x B=%x"), TCCR2A, TCCR2B);
} else { // not timer 9 or 10
abits = B01;
if (fbits >= 4)
bbits = B1001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0011;
else
bbits = B0100;
switch (pin) {
// case 9 and 10 taken care of above by if()
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | abits; // set WGM0 and WGM1
TCCR4B = (TCCR4B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 4 A=%x B=%x"), TCCR4A, TCCR4B);
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | abits; // set WGM0 and WGM1
TCCR5B = (TCCR5B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 5 A=%x B=%x"), TCCR5A, TCCR5B);
break;
default:
break;
}
}
#endif
}
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define NUM_ADC_INPUTS 16 #define NUM_ADC_INPUTS 16
#else #else

View File

@@ -151,10 +151,26 @@ void DCCTimer::reset() {
ESP.restart(); ESP.restart();
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
else if (f == 7)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
#include "esp32-hal.h" #include "esp32-hal.h"
#include "soc/soc_caps.h" #include "soc/soc_caps.h"
#ifdef SOC_LEDC_SUPPORT_HS_MODE #ifdef SOC_LEDC_SUPPORT_HS_MODE
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1) #define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
#else #else
@@ -164,7 +180,7 @@ void DCCTimer::reset() {
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 }; static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
static int cnt_channel = LEDC_CHANNELS; static int cnt_channel = LEDC_CHANNELS;
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) { void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency) {
if (pin < SOC_GPIO_PIN_COUNT) { if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] != 0) { if (pin_to_channel[pin] != 0) {
ledcSetup(pin_to_channel[pin], frequency, 8); ledcSetup(pin_to_channel[pin], frequency, 8);

View File

@@ -125,6 +125,11 @@ void DCCTimer::reset() {
while(true){} while(true){}
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
int16_t ADCee::ADCmax() { int16_t ADCee::ADCmax() {
return 4095; return 4095;
} }

View File

@@ -156,6 +156,11 @@ void DCCTimer::reset() {
while(true) {}; while(true) {};
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
uint16_t ADCee::usedpins = 0; uint16_t ADCee::usedpins = 0;

View File

@@ -257,6 +257,23 @@ void DCCTimer::reset() {
while(true) {}; while(true) {};
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
else if (f == 7)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
// TODO: rationalise the size of these... could really use sparse arrays etc. // TODO: rationalise the size of these... could really use sparse arrays etc.
static HardwareTimer * pin_timer[100] = {0}; static HardwareTimer * pin_timer[100] = {0};
static uint32_t channel_frequency[100] = {0}; static uint32_t channel_frequency[100] = {0};
@@ -267,7 +284,7 @@ static uint32_t pin_channel[100] = {0};
// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones // sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones
// currently used for HA so they don't interfere with one another. For now we'll just make PWM // currently used for HA so they don't interfere with one another. For now we'll just make PWM
// work well... then work backwards to integrate with HA mode if we can. // work well... then work backwards to integrate with HA mode if we can.
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency)
{ {
if (pin_timer[pin] == NULL) { if (pin_timer[pin] == NULL) {
// Automatically retrieve TIM instance and channel associated to pin // Automatically retrieve TIM instance and channel associated to pin

View File

@@ -141,6 +141,11 @@ void DCCTimer::reset() {
SCB_AIRCR = 0x05FA0004; SCB_AIRCR = 0x05FA0004;
} }
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
int16_t ADCee::ADCmax() { int16_t ADCee::ADCmax() {
return 4095; return 4095;
} }

View File

@@ -106,6 +106,7 @@ void DCCWaveform::interruptHandler() {
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) { DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
isMainTrack = isMain; isMainTrack = isMain;
packetPending = false; packetPending = false;
reminderWindowOpen = false;
memcpy(transmitPacket, idlePacket, sizeof(idlePacket)); memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
state = WAVE_START; state = WAVE_START;
// The +1 below is to allow the preamble generator to create the stop bit // The +1 below is to allow the preamble generator to create the stop bit
@@ -127,9 +128,15 @@ void DCCWaveform::interrupt2() {
if (remainingPreambles > 0 ) { if (remainingPreambles > 0 ) {
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
remainingPreambles--; remainingPreambles--;
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
if (remainingPreambles==1) promotePendingPacket();
// Update free memory diagnostic as we don't have anything else to do this time. // Update free memory diagnostic as we don't have anything else to do this time.
// Allow for checkAck and its called functions using 22 bytes more. // Allow for checkAck and its called functions using 22 bytes more.
DCCTimer::updateMinimumFreeMemoryISR(22); else DCCTimer::updateMinimumFreeMemoryISR(22);
return; return;
} }
@@ -148,30 +155,9 @@ void DCCWaveform::interrupt2() {
if (bytes_sent >= transmitLength) { if (bytes_sent >= transmitLength) {
// end of transmission buffer... repeat or switch to next message // end of transmission buffer... repeat or switch to next message
bytes_sent = 0; bytes_sent = 0;
// preamble for next packet will start...
remainingPreambles = requiredPreambles; remainingPreambles = requiredPreambles;
if (transmitRepeats > 0) {
transmitRepeats--;
} }
else if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
}
else {
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
}
} }
} }
#pragma GCC pop_options #pragma GCC pop_options
@@ -193,8 +179,39 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
packetPending = true; packetPending = true;
clearResets(); clearResets();
} }
bool DCCWaveform::getPacketPending() {
return packetPending; bool DCCWaveform::isReminderWindowOpen() {
return reminderWindowOpen && ! packetPending;
}
void DCCWaveform::promotePendingPacket() {
// fill the transmission packet from the pending packet
// Just keep going if repeating
if (transmitRepeats > 0) {
transmitRepeats--;
return;
}
if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
return;
}
// nothing to do, just send idles or resets
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
} }
#endif #endif
@@ -266,15 +283,15 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
} }
} }
bool DCCWaveform::getPacketPending() { bool DCCWaveform::isReminderWindowOpen() {
if(isMainTrack) { if(isMainTrack) {
if (rmtMainChannel == NULL) if (rmtMainChannel == NULL)
return true; return false;
return rmtMainChannel->busy(); return !rmtMainChannel->busy();
} else { } else {
if (rmtProgChannel == NULL) if (rmtProgChannel == NULL)
return true; return false;
return rmtProgChannel->busy(); return !rmtProgChannel->busy();
} }
} }
void IRAM_ATTR DCCWaveform::loop() { void IRAM_ATTR DCCWaveform::loop() {

View File

@@ -76,11 +76,13 @@ class DCCWaveform {
}; };
#endif #endif
void schedulePacket(const byte buffer[], byte byteCount, byte repeats); void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
bool getPacketPending(); bool isReminderWindowOpen();
void promotePendingPacket();
private: private:
#ifndef ARDUINO_ARCH_ESP32 #ifndef ARDUINO_ARCH_ESP32
volatile bool packetPending; volatile bool packetPending;
volatile bool reminderWindowOpen;
volatile byte sentResetsSincePacket; volatile byte sentResetsSincePacket;
#else #else
volatile uint32_t resetPacketBase; volatile uint32_t resetPacketBase;

View File

@@ -414,7 +414,6 @@ void RMFT2::driveLoco(byte speed) {
power on appropriate track if DC or main if dcc power on appropriate track if DC or main if dcc
if (TrackManager::getMainPowerMode()==POWERMODE::OFF) { if (TrackManager::getMainPowerMode()==POWERMODE::OFF) {
TrackManager::setMainPower(POWERMODE::ON); TrackManager::setMainPower(POWERMODE::ON);
CommandDistributor::broadcastPower();
} }
**********/ **********/
@@ -642,7 +641,6 @@ void RMFT2::loop2() {
case OPCODE_POWEROFF: case OPCODE_POWEROFF:
TrackManager::setPower(POWERMODE::OFF); TrackManager::setPower(POWERMODE::OFF);
TrackManager::setJoin(false); TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break; break;
case OPCODE_SET_POWER: case OPCODE_SET_POWER:
@@ -669,6 +667,45 @@ void RMFT2::loop2() {
} }
break; break;
case OPCODE_SETFREQ:
// Frequency is default 0, or 1, 2,3
//if (loco) DCC::setFn(loco,operand,true);
switch (operand) {
case 0: // default - all F-s off
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,false);
}
break;
case 1:
if (loco) {
DCC::setFn(loco,29,true);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,false);
}
break;
case 2:
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,true);
DCC::setFn(loco,31,false);
}
break;
case 3:
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,true);
}
break;
default:
; // do nothing
break;
}
break;
case OPCODE_RESUME: case OPCODE_RESUME:
pausingTask=NULL; pausingTask=NULL;
driveLoco(speedo); driveLoco(speedo);
@@ -837,12 +874,10 @@ void RMFT2::loop2() {
case OPCODE_JOIN: case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON); TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true); TrackManager::setJoin(true);
CommandDistributor::broadcastPower();
break; break;
case OPCODE_UNJOIN: case OPCODE_UNJOIN:
TrackManager::setJoin(false); TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break; break;
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
@@ -870,7 +905,6 @@ void RMFT2::loop2() {
case OPCODE_POWERON: case OPCODE_POWERON:
TrackManager::setMainPower(POWERMODE::ON); TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setJoin(false); TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break; break;
case OPCODE_START: case OPCODE_START:

View File

@@ -51,7 +51,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2, OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,
#endif #endif
OPCODE_POM, OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET, OPCODE_START,OPCODE_SETLOCO,OPCODE_SETFREQ,OPCODE_SENDLOCO,OPCODE_FORGET,
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON, OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_PRINT,OPCODE_DCCACTIVATE, OPCODE_PRINT,OPCODE_DCCACTIVATE,

View File

@@ -151,11 +151,13 @@
#undef SET_TRACK #undef SET_TRACK
#undef SET_POWER #undef SET_POWER
#undef SETLOCO #undef SETLOCO
#undef SETFREQ
#undef SIGNAL #undef SIGNAL
#undef SIGNALH #undef SIGNALH
#undef SPEED #undef SPEED
#undef START #undef START
#undef STASH #undef STASH
#undef STEALTH
#undef STOP #undef STOP
#undef THROW #undef THROW
#undef TT_ADDPOSITION #undef TT_ADDPOSITION
@@ -301,11 +303,13 @@
#define SET_TRACK(track,mode) #define SET_TRACK(track,mode)
#define SET_POWER(track,onoff) #define SET_POWER(track,onoff)
#define SETLOCO(loco) #define SETLOCO(loco)
#define SETFREQ(loco,freq)
#define SIGNAL(redpin,amberpin,greenpin) #define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin) #define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) #define SPEED(speed)
#define START(route) #define START(route)
#define STASH(id) #define STASH(id)
#define STEALTH(code...)
#define STOP #define STOP
#define THROW(id) #define THROW(id)
#define TT_ADDPOSITION(turntable_id,position,value,angle,description...) #define TT_ADDPOSITION(turntable_id,position,value,angle,description...)

View File

@@ -210,6 +210,8 @@ case (__COUNTER__ - StringMacroTracker1) : {\
lcdid=id;\ lcdid=id;\
break;\ break;\
} }
#undef STEALTH
#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return;
#undef WITHROTTLE #undef WITHROTTLE
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle) #define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
@@ -422,6 +424,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF), OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
#define LCD(id,msg) PRINT(msg) #define LCD(id,msg) PRINT(msg)
#define SCREEN(display,id,msg) PRINT(msg) #define SCREEN(display,id,msg) PRINT(msg)
#define STEALTH(code...) PRINT(dummy)
#define LCN(msg) PRINT(msg) #define LCN(msg) PRINT(msg)
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0), #define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr), #define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
@@ -490,6 +493,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track), #define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
#define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track), #define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SETFREQ(loco,freq) OPCODE_SETLOCO,V(loco), OPCODE_SETFREQ,V(freq),
#define SIGNAL(redpin,amberpin,greenpin) #define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin) #define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed), #define SPEED(speed) OPCODE_SPEED,V(speed),

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202311270714Z" #define GITHUB_SHA "devel-202401202235Z"

View File

@@ -110,7 +110,7 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Calculate a rise time appropriate to the requested bus speed // Calculate a rise time appropriate to the requested bus speed
// Use 10x the rise time spec to enable integer divide of 50ns clock period // Use 10x the rise time spec to enable integer divide of 50ns clock period
uint16_t t_rise; uint16_t t_rise;
uint32_t ccr_freq; //uint32_t ccr_freq;
while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further
// writes to CR1 while STOP is being executed! // writes to CR1 while STOP is being executed!

View File

@@ -22,13 +22,10 @@
* This device driver will configure the device on startup, along with * This device driver will configure the device on startup, along with
* interacting with the device for all input/output duties. * interacting with the device for all input/output duties.
* *
* To create EX-IOExpander devices, these are defined in myHal.cpp: * To create EX-IOExpander devices, these are defined in myAutomation.h:
* (Note the device driver is included by default) * (Note the device driver is included by default)
* *
* void halSetup() { * HAL(EXIOExpander,800,18,0x65)
* // EXIOExpander::create(vpin, num_vpins, i2c_address);
* EXIOExpander::create(800, 18, 0x65);
* }
* *
* All pins on an EX-IOExpander device are allocated according to the pin map for the specific * All pins on an EX-IOExpander device are allocated according to the pin map for the specific
* device in use. There is no way for the device driver to sanity check pins are used for the * device in use. There is no way for the device driver to sanity check pins are used for the
@@ -98,25 +95,30 @@ private:
_numAnaloguePins = receiveBuffer[2]; _numAnaloguePins = receiveBuffer[2];
// See if we already have suitable buffers assigned // See if we already have suitable buffers assigned
size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8; if (_numDigitalPins>0) {
if (_digitalPinBytes < digitalBytesNeeded) { size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;
// Not enough space, free any existing buffer and allocate a new one if (_digitalPinBytes < digitalBytesNeeded) {
if (_digitalPinBytes > 0) free(_digitalInputStates); // Not enough space, free any existing buffer and allocate a new one
_digitalInputStates = (byte*) calloc(_digitalPinBytes, 1); if (_digitalPinBytes > 0) free(_digitalInputStates);
_digitalPinBytes = digitalBytesNeeded; _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) { if (_numAnaloguePins>0) {
free(_analogueInputBuffer); size_t analogueBytesNeeded = _numAnaloguePins * 2;
free(_analogueInputStates); if (_analoguePinBytes < analogueBytesNeeded) {
free(_analoguePinMap); // 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;
} }
_analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
_analoguePinBytes = analogueBytesNeeded;
} }
} else { } else {
DIAG(F("EX-IOExpander I2C:%s ERROR configuring device"), _I2CAddress.toString()); DIAG(F("EX-IOExpander I2C:%s ERROR configuring device"), _I2CAddress.toString());
@@ -124,8 +126,8 @@ private:
return; return;
} }
} }
// We now need to retrieve the analogue pin map // We now need to retrieve the analogue pin map if there are analogue pins
if (status == I2C_STATUS_OK) { if (status == I2C_STATUS_OK && _numAnaloguePins>0) {
commandBuffer[0] = EXIOINITA; commandBuffer[0] = EXIOINITA;
status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1); status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1);
} }
@@ -239,7 +241,7 @@ private:
// If we're not doing anything now, check to see if a new input transfer is due. // If we're not doing anything now, check to see if a new input transfer is due.
if (_readState == RDS_IDLE) { if (_readState == RDS_IDLE) {
if (currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh if (currentMicros - _lastDigitalRead > _digitalRefresh && _numDigitalPins>0) { // Delay for digital read refresh
// Issue new read request for digital states. As the request is non-blocking, the buffer has to // Issue new read request for digital states. As the request is non-blocking, the buffer has to
// be allocated from heap (object state). // be allocated from heap (object state).
_readCommandBuffer[0] = EXIORDD; _readCommandBuffer[0] = EXIORDD;
@@ -247,7 +249,7 @@ private:
// non-blocking read // non-blocking read
_lastDigitalRead = currentMicros; _lastDigitalRead = currentMicros;
_readState = RDS_DIGITAL; _readState = RDS_DIGITAL;
} else if (currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh } else if (currentMicros - _lastAnalogueRead > _analogueRefresh && _numAnaloguePins>0) { // Delay for analogue read refresh
// Issue new read for analogue input states // Issue new read for analogue input states
_readCommandBuffer[0] = EXIORDAN; _readCommandBuffer[0] = EXIORDAN;
I2CManager.read(_I2CAddress, _analogueInputBuffer, I2CManager.read(_I2CAddress, _analogueInputBuffer,

View File

@@ -325,49 +325,23 @@ uint16_t taurustones[28] = { 165, 175, 196, 220,
220, 196, 175, 165 }; 220, 196, 175, 165 };
#endif #endif
#endif #endif
void MotorDriver::setDCSignal(byte speedcode) { void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/) {
if (brakePin == UNUSED_PIN) if (brakePin == UNUSED_PIN)
return; return;
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is differnet)
TCCR2A = (TCCR2A & B11111100) | B00000001; // set WGM1=0 and WGM0=1 phase correct PWM
TCCR2B = (TCCR2B & B11110000) | B00000110; // set WGM2=0 ; set divisor on timer 2 to 1/256 for 122.55Hz
//DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
#endif
default:
break;
}
// spedcoode is a dcc speed & direction // spedcoode is a dcc speed & direction
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127 byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
byte tDir=speedcode & 0x80; byte tDir=speedcode & 0x80;
byte brake; byte brake;
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
{ // new block because of variable f
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32) #if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
{ int f = frequency;
int f = 131;
#ifdef VARIABLE_TONES #ifdef VARIABLE_TONES
if (tSpeed > 2) { if (tSpeed > 2) {
if (tSpeed <= 58) { if (tSpeed <= 58) {
@@ -375,19 +349,15 @@ void MotorDriver::setDCSignal(byte speedcode) {
} }
} }
#endif #endif
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup //DIAG(F("Brake pin %d freqency %d"), brakePin, f);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else // all AVR here
DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps
analogWrite(brakePin,brake);
#endif
} }
#endif
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else
analogWrite(brakePin,brake);
#endif
//DIAG(F("DCSignal %d"), speedcode); //DIAG(F("DCSignal %d"), speedcode);
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) { if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
noInterrupts(); noInterrupts();
@@ -436,58 +406,26 @@ void MotorDriver::throttleInrush(bool on) {
return; return;
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT))) if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT)))
return; return;
byte duty = on ? 208 : 0; byte duty = on ? 207 : 0; // duty of 81% at 62500Hz this gives pauses of 3usec
if (invertBrake) if (invertBrake)
duty = 255-duty; duty = 255-duty;
#if defined(ARDUINO_ARCH_ESP32) #if defined(ARDUINO_ARCH_ESP32)
if(on) { if(on) {
DCCTimer::DCCEXanalogWrite(brakePin,duty); DCCTimer::DCCEXanalogWrite(brakePin,duty);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500); DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
} else { } else {
ledcDetachPin(brakePin); ledcDetachPin(brakePin);
} }
#elif defined(ARDUINO_ARCH_STM32) #elif defined(ARDUINO_ARCH_STM32)
if(on) { if(on) {
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500); DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
DCCTimer::DCCEXanalogWrite(brakePin,duty); DCCTimer::DCCEXanalogWrite(brakePin,duty);
} else { } else {
pinMode(brakePin, OUTPUT); pinMode(brakePin, OUTPUT);
} }
#else #else // all AVR here
if(on){ if(on){
switch(brakePin) { DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is different)
TCCR2A = (TCCR2A & B11111100) | B00000011; // set WGM0=1 and WGM1=1 for fast PWM
TCCR2B = (TCCR2B & B11110000) | B00000001; // set WGM2=0 and prescaler div=1 (max)
DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
#endif
default:
break;
}
} }
analogWrite(brakePin,duty); analogWrite(brakePin,duty);
#endif #endif

View File

@@ -187,7 +187,7 @@ class MotorDriver {
} }
}; };
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); }; inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
void setDCSignal(byte speedByte); void setDCSignal(byte speedByte, uint8_t frequency=0);
void throttleInrush(bool on); void throttleInrush(bool on);
inline void detachDCSignal() { inline void detachDCSignal() {
#if defined(__arm__) #if defined(__arm__)

View File

@@ -19,6 +19,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>. * along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "defines.h"
#include "TrackManager.h" #include "TrackManager.h"
#include "FSH.h" #include "FSH.h"
#include "DCCWaveform.h" #include "DCCWaveform.h"
@@ -202,7 +203,7 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
FOR_EACH_TRACK(t) { FOR_EACH_TRACK(t) {
if (trackDCAddr[t]!=cab && cab != 0) continue; if (trackDCAddr[t]!=cab && cab != 0) continue;
if (track[t]->getMode() & TRACK_MODE_DC) if (track[t]->getMode() & TRACK_MODE_DC)
track[t]->setDCSignal(speedbyte); track[t]->setDCSignal(speedbyte, DCC::getThrottleFrequency(trackDCAddr[t]));
} }
} }
@@ -348,8 +349,8 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
} }
void TrackManager::applyDCSpeed(byte t) { void TrackManager::applyDCSpeed(byte t) {
uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]); track[t]->setDCSignal(DCC::getThrottleSpeedByte(trackDCAddr[t]),
track[t]->setDCSignal(speedByte); DCC::getThrottleFrequency(trackDCAddr[t]));
} }
bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[]) bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
@@ -489,10 +490,13 @@ std::vector<MotorDriver *>TrackManager::getMainDrivers() {
// Set track power for all tracks with this mode // Set track power for all tracks with this mode
void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) { void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) {
bool didChange=false;
FOR_EACH_TRACK(t) { FOR_EACH_TRACK(t) {
MotorDriver *driver=track[t]; MotorDriver *driver=track[t];
TRACK_MODE trackmodeOfTrack = driver->getMode(); TRACK_MODE trackmodeOfTrack = driver->getMode();
if (trackmodeToMatch & trackmodeOfTrack) { if (trackmodeToMatch & trackmodeOfTrack) {
if (powermode != driver->getPower())
didChange=true;
if (powermode == POWERMODE::ON) { if (powermode == POWERMODE::ON) {
if (trackmodeOfTrack & TRACK_MODE_DC) { if (trackmodeOfTrack & TRACK_MODE_DC) {
driver->setBrake(true); // DC starts with brake on driver->setBrake(true); // DC starts with brake on
@@ -507,12 +511,15 @@ void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermod
driver->setPower(powermode); driver->setPower(powermode);
} }
} }
if (didChange)
CommandDistributor::broadcastPower();
} }
// Set track power for this track, inependent of mode // Set track power for this track, inependent of mode
void TrackManager::setTrackPower(POWERMODE powermode, byte t) { void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
MotorDriver *driver=track[t]; MotorDriver *driver=track[t];
TRACK_MODE trackmode = driver->getMode(); TRACK_MODE trackmode = driver->getMode();
POWERMODE oldpower = driver->getPower();
if (trackmode & TRACK_MODE_NONE) { if (trackmode & TRACK_MODE_NONE) {
driver->setBrake(true); // Track is unused. Brake is good to have. driver->setBrake(true); // Track is unused. Brake is good to have.
powermode = POWERMODE::OFF; // Track is unused. Force it to OFF powermode = POWERMODE::OFF; // Track is unused. Force it to OFF
@@ -530,6 +537,8 @@ void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
} }
} }
driver->setPower(powermode); driver->setPower(powermode);
if (oldpower != driver->getPower())
CommandDistributor::broadcastPower();
} }
// returns state of the one and only prog track // returns state of the one and only prog track
@@ -566,14 +575,17 @@ bool TrackManager::getPower(byte t, char s[]) {
return false; return false;
} }
void TrackManager::reportObsoleteCurrent(Print* stream) { void TrackManager::reportObsoleteCurrent(Print* stream) {
// This function is for backward JMRI compatibility only // This function is for backward JMRI compatibility only
// It reports the first track only, as main, regardless of track settings. // It reports the first track only, as main, regardless of track settings.
// <c MeterName value C/V unit min max res warn> // <c MeterName value C/V unit min max res warn>
#ifdef HAS_ENOUGH_MEMORY
int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue()); int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"), StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"),
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent); track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
#else
(void)stream;
#endif
} }
void TrackManager::reportCurrent(Print* stream) { void TrackManager::reportCurrent(Print* stream) {

View File

@@ -150,7 +150,6 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF); DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
*/ */
CommandDistributor::broadcastPower();
} }
#if defined(EXRAIL_ACTIVE) #if defined(EXRAIL_ACTIVE)
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
@@ -496,7 +495,6 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
DIAG(F("LocoCallback commit success")); DIAG(F("LocoCallback commit success"));
stashStream->commit(); stashStream->commit();
CommandDistributor::broadcastPower();
} }
void WiThrottle::sendIntro(Print* stream) { void WiThrottle::sendIntro(Print* stream) {
@@ -620,7 +618,7 @@ void WiThrottle::sendFunctions(Print* stream, byte loco) {
#endif #endif
for(int fKey=0; fKey<fkeys; fKey++) { for(int fKey=0; fKey<fkeys; fKey++) {
int fstate=DCC::getFn(locoid,fKey); int8_t fstate=DCC::getFn(locoid,fKey);
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey); if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey);
} }
} }

View File

@@ -74,25 +74,39 @@ class NetworkClient {
public: public:
NetworkClient(WiFiClient c) { NetworkClient(WiFiClient c) {
wifi = c; wifi = c;
};
bool ok() {
return (inUse && wifi.connected());
};
bool recycle(WiFiClient c) {
if (inUse == true) return false;
// return false here until we have
// implemented a LRU timer
// if (LRU too recent) return false;
return false;
wifi = c;
inUse = true; inUse = true;
};
bool active(byte clientId) {
if (!inUse)
return false;
if(!wifi.connected()) {
DIAG(F("Remove client %d"), clientId);
CommandDistributor::forget(clientId);
wifi.stop();
inUse = false;
return false;
}
return true; return true;
}
bool recycle(WiFiClient c) {
if (wifi == c) {
if (inUse == true)
DIAG(F("WARNING: Duplicate"));
else
DIAG(F("Returning"));
inUse = true;
return true;
}
if (inUse == false) {
wifi = c;
inUse = true;
return true;
}
return false;
}; };
WiFiClient wifi; WiFiClient wifi;
bool inUse = true; private:
bool inUse;
}; };
static std::vector<NetworkClient> clients; // a list to hold all clients static std::vector<NetworkClient> clients; // a list to hold all clients
@@ -165,7 +179,7 @@ bool WifiESP::setup(const char *SSid,
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
// DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str()); // DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
DIAG(F("Wifi in STA mode")); DIAG(F("Wifi in STA mode"));
LCD(7, F("IP: %s"), WiFi.softAPIP().toString().c_str()); LCD(7, F("IP: %s"), WiFi.localIP().toString().c_str());
wifiUp = true; wifiUp = true;
} else { } else {
DIAG(F("Could not connect to Wifi SSID %s"),SSid); DIAG(F("Could not connect to Wifi SSID %s"),SSid);
@@ -282,37 +296,26 @@ void WifiESP::loop() {
// really no good way to check for LISTEN especially in AP mode? // really no good way to check for LISTEN especially in AP mode?
wl_status_t wlStatus; wl_status_t wlStatus;
if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) { if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) {
// loop over all clients and remove inactive
for (clientId=0; clientId<clients.size(); clientId++){
// check if client is there and alive
if(clients[clientId].inUse && !clients[clientId].wifi.connected()) {
DIAG(F("Remove client %d"), clientId);
CommandDistributor::forget(clientId);
clients[clientId].wifi.stop();
clients[clientId].inUse = false;
//Do NOT clients.erase(clients.begin()+clientId) as
//that would mix up clientIds for later.
}
}
if (server->hasClient()) { if (server->hasClient()) {
WiFiClient client; WiFiClient client;
while (client = server->available()) { while (client = server->available()) {
for (clientId=0; clientId<clients.size(); clientId++){ for (clientId=0; clientId<clients.size(); clientId++){
if (clients[clientId].recycle(client)) { if (clients[clientId].recycle(client)) {
DIAG(F("Recycle client %d %s"), clientId, client.remoteIP().toString().c_str()); DIAG(F("Recycle client %d %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
break; break;
} }
} }
if (clientId>=clients.size()) { if (clientId>=clients.size()) {
NetworkClient nc(client); NetworkClient nc(client);
clients.push_back(nc); clients.push_back(nc);
DIAG(F("New client %d, %s"), clientId, client.remoteIP().toString().c_str()); DIAG(F("New client %d, %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
} }
} }
} }
// loop over all connected clients // loop over all connected clients
// this removes as a side effect inactive clients when checking ::active()
for (clientId=0; clientId<clients.size(); clientId++){ for (clientId=0; clientId<clients.size(); clientId++){
if(clients[clientId].ok()) { if(clients[clientId].active(clientId)) {
int len; int len;
if ((len = clients[clientId].wifi.available()) > 0) { if ((len = clients[clientId].wifi.available()) > 0) {
// read data from client // read data from client
@@ -350,7 +353,7 @@ void WifiESP::loop() {
} }
// buffer filled, end with '\0' so we can use it as C string // buffer filled, end with '\0' so we can use it as C string
buffer[count]='\0'; buffer[count]='\0';
if((unsigned int)clientId <= clients.size() && clients[clientId].ok()) { if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
if (Diag::CMD || Diag::WITHROTTLE) if (Diag::CMD || Diag::WITHROTTLE)
DIAG(F("SEND %d:%s"), clientId, buffer); DIAG(F("SEND %d:%s"), clientId, buffer);
clients[clientId].wifi.write(buffer,count); clients[clientId].wifi.write(buffer,count);
@@ -383,8 +386,9 @@ void WifiESP::loop() {
// prio task. On core1 this is not a problem // prio task. On core1 this is not a problem
// as there the wdt is disabled by the // as there the wdt is disabled by the
// arduio IDE startup routines. // arduio IDE startup routines.
if (xPortGetCoreID() == 0) if (xPortGetCoreID() == 0) {
feedTheDog0(); feedTheDog0();
yield(); yield();
}
} }
#endif //ESP32 #endif //ESP32

View File

@@ -68,7 +68,9 @@ Stream * WifiInterface::wifiStream;
#define NUM_SERIAL 3 #define NUM_SERIAL 3
#define SERIAL1 Serial3 #define SERIAL1 Serial3
#define SERIAL3 Serial5 #define SERIAL3 Serial5
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) #elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) \
|| defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) \
|| defined(ARDUINO_NUCLEO_F439ZI)
#define NUM_SERIAL 2 #define NUM_SERIAL 2
#define SERIAL1 Serial6 #define SERIAL1 Serial6
#else #else

View File

@@ -3,7 +3,16 @@
#include "StringFormatter.h" #include "StringFormatter.h"
#define VERSION "5.2.12" #define VERSION "5.2.XX"
// 5.2.XX - Variable frequency for DC mode
// 5.2.18 - Display network IP fix
// 5.2.17 - ESP32 simplify network logic
// 5.2.16 - Bugfix to allow for devices using the EX-IOExpander protocol to have no analogue or no digital pins
// 5.2.15 - move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions
// - add repeats to function packets that are not reminded in accordance with accessory packets
// 5.2.14 - Reminder window DCC packet optimization
// - Optional #define DISABLE_FUNCTION_REMINDERS
// 5.2.13 - EXRAIL STEALTH
// 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for // 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for
// - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers // - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers
// - STM32 extra UART/USARTs for larger Nucleo models // - STM32 extra UART/USARTs for larger Nucleo models