1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-11-27 10:06:13 +01:00

Compare commits

...

43 Commits

Author SHA1 Message Date
Kcsmith0708
9e9586ddee
Merge 183b824a5d into 6d0740eab4 2024-01-29 22:55:29 +01:00
Harald Barth
6d0740eab4 version 5.2.28 2024-01-21 21:11:57 +01:00
Harald Barth
0a52a26d50 ESP32: Can all Wifi channels. Only write Wifi password to display if it is a well known one 2024-01-21 21:09:55 +01:00
Harald Barth
811bce4b2a tag 2024-01-20 22:16:26 +01:00
Harald Barth
cf1e1c92b3 Check "easy" check first 2024-01-20 22:15:47 +01:00
peteGSX
657c08c653 Update EX-IOExpander copyright 2024-01-18 18:56:15 +10:00
Harald Barth
bc37a2d2cf version 5.2.27 2024-01-18 08:22:28 +01:00
Harald Barth
3c0704dbd1 Bugfix: allocate enough bytes for digital pins. Add more sanity checks when allocating memory 2024-01-18 08:20:33 +01:00
Asbelos
95bf5aae38 HAL defaults control 2024-01-14 20:20:22 +00:00
Asbelos
8216579f62 5.2.25 <D> returns <X> bugs 2024-01-14 02:09:22 +00:00
Asbelos
a54a262f68 5.2.24 EXRAIL asserts 2024-01-14 02:03:42 +00:00
Asbelos
a508ee7055 Fix asserts for Teensy 2024-01-10 16:08:11 +00:00
Harald Barth
20ae915eaf remove unused ccr_reg variable 2024-01-10 15:23:52 +01:00
Harald Barth
35a0bde115 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-01-10 15:10:57 +01:00
Harald Barth
d24d09c37a subversion 2024-01-10 15:10:25 +01:00
Harald Barth
9ab6b3d4ea Bugfix: Ethernet fixed IP start 2024-01-10 15:09:22 +01:00
Asbelos
d8c282434c _hk in myAutomation 2024-01-10 12:11:14 +00:00
Asbelos
43648fd9f4 5.2.23 2024-01-10 12:01:40 +00:00
Asbelos
b5ddade2b3 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-01-10 11:58:37 +00:00
Asbelos
2e4995cab3 Keyword Hasher _hk 2024-01-10 11:58:30 +00:00
Harald Barth
796d5c4774 version 5.2.22 2024-01-10 08:20:14 +01:00
Harald Barth
27bd444884 Numbers for automations/routes can be negative 2024-01-10 08:18:14 +01:00
Harald Barth
ca380d11dc Do not crash on turnouts or turntables without description 2024-01-10 08:15:30 +01:00
peteGSX
c336ab0bb4
Merge pull request #389 from DCC-EX:add-startup-delay
Add-startup-delay
2024-01-09 16:43:41 +10:00
peteGSX
5ac26ce505 Add missing ; and DIAG message 2024-01-09 10:49:22 +10:00
peteGSX
b51a8fe126 Add STARTUP_DELAY 2024-01-09 10:41:29 +10:00
Harald Barth
718e78fca6 version 5.2.20 2024-01-08 13:20:29 +01:00
Harald Barth
70a1b9538c Check return of Ethernet.begin() in all code variants 2024-01-08 13:19:22 +01:00
Harald Barth
39d0cbb791 version 5.2.19 2024-01-07 22:24:15 +01:00
Harald Barth
4a3d3228a9 ESP32: Use SOC_RMT_MEM_WORDS_PER_CHANNEL to determine if the RMT hardware can handle DCC 2024-01-07 22:22:38 +01:00
Harald Barth
74f7af1675 Display network IP fix 2024-01-03 02:36:07 +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
Kcsmith0708
183b824a5d
Update version.h
Added v4.1.6 Support EX-MotorShield8874
2023-08-24 13:57:09 -04:00
Kcsmith0708
50863600da
Update version.h
Updated version
4.1 thru 4.1.5
2023-04-06 11:55:50 -04:00
25 changed files with 463 additions and 315 deletions

View File

@ -283,7 +283,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 +303,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) {

View File

@ -76,6 +76,12 @@ void setup()
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com")); DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
// If user has defined a startup delay, delay here before starting IO
#if defined(STARTUP_DELAY)
DIAG(F("Delaying startup for %dms"), STARTUP_DELAY);
delay(STARTUP_DELAY);
#endif
// Initialise HAL layer before reading EEprom or setting up MotorDrivers // Initialise HAL layer before reading EEprom or setting up MotorDrivers
IODevice::begin(); IODevice::begin();

26
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)
@ -619,33 +619,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
#ifdef 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 flags&= ~FN_GROUP_1; // dont send them again
#endif #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
#ifdef 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 flags&= ~FN_GROUP_2; // dont send them again
#endif #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
#ifdef 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 flags&= ~FN_GROUP_3; // dont send them again
#endif #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;
} }

2
DCC.h
View File

@ -109,7 +109,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

@ -49,6 +49,7 @@
#include "CommandDistributor.h" #include "CommandDistributor.h"
#include "TrackManager.h" #include "TrackManager.h"
#include "DCCTimer.h" #include "DCCTimer.h"
#include "KeywordHasher.h"
#include "EXRAIL.h" #include "EXRAIL.h"
#endif #endif

View File

@ -116,6 +116,7 @@ Once a new OPCODE is decided upon, update this list.
#include "EXRAIL2.h" #include "EXRAIL2.h"
#include "Turntables.h" #include "Turntables.h"
#include "version.h" #include "version.h"
#include "KeywordHasher.h"
// This macro can't be created easily as a portable function because the // This macro can't be created easily as a portable function because the
// flashlist requires a far pointer for high flash access. // flashlist requires a far pointer for high flash access.
@ -126,57 +127,6 @@ Once a new OPCODE is decided upon, update this list.
StringFormatter::send(stream,F(" %d"),value); \ StringFormatter::send(stream,F(" %d"),value); \
} }
// These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter.
// To discover new keyword numbers , use the <$ YOURKEYWORD> command
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_CABS = -11981;
const int16_t HASH_KEYWORD_RAM = 25982;
const int16_t HASH_KEYWORD_CMD = 9962;
const int16_t HASH_KEYWORD_ACK = 3113;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_DCC = 6436;
const int16_t HASH_KEYWORD_SLOW = -17209;
#ifndef DISABLE_PROG
const int16_t HASH_KEYWORD_JOIN = -30750;
const int16_t HASH_KEYWORD_PROG = -29718;
const int16_t HASH_KEYWORD_PROGBOOST = -6353;
#endif
#ifndef DISABLE_EEPROM
const int16_t HASH_KEYWORD_EEPROM = -7168;
#endif
const int16_t HASH_KEYWORD_LIMIT = 27413;
const int16_t HASH_KEYWORD_MAX = 16244;
const int16_t HASH_KEYWORD_MIN = 15978;
const int16_t HASH_KEYWORD_RESET = 26133;
const int16_t HASH_KEYWORD_RETRY = 25704;
const int16_t HASH_KEYWORD_SPEED28 = -17064;
const int16_t HASH_KEYWORD_SPEED128 = 25816;
const int16_t HASH_KEYWORD_SERVO=27709;
const int16_t HASH_KEYWORD_TT=2688;
const int16_t HASH_KEYWORD_VPIN=-415;
const int16_t HASH_KEYWORD_A='A';
const int16_t HASH_KEYWORD_C='C';
const int16_t HASH_KEYWORD_G='G';
const int16_t HASH_KEYWORD_H='H';
const int16_t HASH_KEYWORD_I='I';
const int16_t HASH_KEYWORD_M='M';
const int16_t HASH_KEYWORD_O='O';
const int16_t HASH_KEYWORD_P='P';
const int16_t HASH_KEYWORD_R='R';
const int16_t HASH_KEYWORD_T='T';
const int16_t HASH_KEYWORD_X='X';
const int16_t HASH_KEYWORD_LCN = 15137;
const int16_t HASH_KEYWORD_HAL = 10853;
const int16_t HASH_KEYWORD_SHOW = -21309;
const int16_t HASH_KEYWORD_ANIN = -10424;
const int16_t HASH_KEYWORD_ANOUT = -26399;
const int16_t HASH_KEYWORD_WIFI = -5583;
const int16_t HASH_KEYWORD_ETHERNET = -30767;
const int16_t HASH_KEYWORD_WIT = 31594;
const int16_t HASH_KEYWORD_EXTT = 8573;
const int16_t HASH_KEYWORD_ADD = 3201;
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS]; int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy; bool DCCEXParser::stashBusy;
Print *DCCEXParser::stashStream = NULL; Print *DCCEXParser::stashStream = NULL;
@ -567,27 +517,26 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON); TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON);
} }
if (params==1) { if (params==1) {
if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN> if (p[0]=="MAIN"_hk) { // <1 MAIN>
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON); TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
} }
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN> else if (p[0] == "JOIN"_hk) { // <1 JOIN>
TrackManager::setJoin(true); TrackManager::setJoin(true);
TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON); TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
} }
else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG> else if (p[0]=="PROG"_hk) { // <1 PROG>
TrackManager::setJoin(false); TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON); TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
} }
#endif #endif
else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
byte t = (p[0] - 'A'); byte t = (p[0] - 'A');
TrackManager::setTrackPower(POWERMODE::ON, t); TrackManager::setTrackPower(POWERMODE::ON, t);
//StringFormatter::send(stream, F("<p1 %c>\n"), t+'A'); //StringFormatter::send(stream, F("<p1 %c>\n"), t+'A');
} }
else break; // will reply <X> else break; // will reply <X>
} }
CommandDistributor::broadcastPower();
//TrackManager::streamTrackState(NULL,t); //TrackManager::streamTrackState(NULL,t);
return; return;
@ -601,17 +550,17 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF); TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF);
} }
if (params==1) { if (params==1) {
if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN> if (p[0]=="MAIN"_hk) { // <0 MAIN>
TrackManager::setJoin(false); TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF); TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
} }
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG> else if (p[0]=="PROG"_hk) { // <0 PROG>
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF); TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);
} }
#endif #endif
else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
byte t = (p[0] - 'A'); byte t = (p[0] - 'A');
TrackManager::setJoin(false); TrackManager::setJoin(false);
TrackManager::setTrackPower(POWERMODE::OFF, t); TrackManager::setTrackPower(POWERMODE::OFF, t);
@ -619,7 +568,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
} }
else break; // will reply <X> else break; // will reply <X>
} }
CommandDistributor::broadcastPower();
return; return;
} }
@ -706,7 +654,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
//if ((params<1) | (params>2)) break; // <J> //if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0; int16_t id=(params==2)?p[1]:0;
switch(p[0]) { switch(p[0]) {
case HASH_KEYWORD_C: // <JC mmmm nn> sets time and speed case "C"_hk: // <JC mmmm nn> sets time and speed
if (params==1) { // <JC> returns latest time if (params==1) { // <JC> returns latest time
int16_t x = CommandDistributor::retClockTime(); int16_t x = CommandDistributor::retClockTime();
StringFormatter::send(stream, F("<jC %d>\n"), x); StringFormatter::send(stream, F("<jC %d>\n"), x);
@ -715,28 +663,28 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
CommandDistributor::setClockTime(p[1], p[2], 1); CommandDistributor::setClockTime(p[1], p[2], 1);
return; return;
case HASH_KEYWORD_G: // <JG> current gauge limits case "G"_hk: // <JG> current gauge limits
if (params>1) break; if (params>1) break;
TrackManager::reportGauges(stream); // <g limit...limit> TrackManager::reportGauges(stream); // <g limit...limit>
return; return;
case HASH_KEYWORD_I: // <JI> current values case "I"_hk: // <JI> current values
if (params>1) break; if (params>1) break;
TrackManager::reportCurrent(stream); // <g limit...limit> TrackManager::reportCurrent(stream); // <g limit...limit>
return; return;
case HASH_KEYWORD_A: // <JA> intercepted by EXRAIL// <JA> returns automations/routes case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
if (params!=1) break; // <JA> if (params!=1) break; // <JA>
StringFormatter::send(stream, F("<jA>\n")); StringFormatter::send(stream, F("<jA>\n"));
return; return;
case HASH_KEYWORD_M: // <JM> intercepted by EXRAIL case "M"_hk: // <JM> intercepted by EXRAIL
if (params>1) break; // invalid cant do if (params>1) break; // invalid cant do
// <JM> requests stash size so say none. // <JM> requests stash size so say none.
StringFormatter::send(stream,F("<jM 0>\n")); StringFormatter::send(stream,F("<jM 0>\n"));
return; return;
case HASH_KEYWORD_R: // <JR> returns rosters case "R"_hk: // <JR> returns rosters
StringFormatter::send(stream, F("<jR")); StringFormatter::send(stream, F("<jR"));
#ifdef EXRAIL_ACTIVE #ifdef EXRAIL_ACTIVE
if (params==1) { if (params==1) {
@ -755,7 +703,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
#endif #endif
StringFormatter::send(stream, F(">\n")); StringFormatter::send(stream, F(">\n"));
return; return;
case HASH_KEYWORD_T: // <JT> returns turnout list case "T"_hk: // <JT> returns turnout list
StringFormatter::send(stream, F("<jT")); StringFormatter::send(stream, F("<jT"));
if (params==1) { // <JT> if (params==1) { // <JT>
for ( Turnout * t=Turnout::first(); t; t=t->next()) { for ( Turnout * t=Turnout::first(); t; t=t->next()) {
@ -782,7 +730,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
return; return;
// No turntables without HAL support // No turntables without HAL support
#ifndef IO_NO_HAL #ifndef IO_NO_HAL
case HASH_KEYWORD_O: // <JO returns turntable list case "O"_hk: // <JO returns turntable list
StringFormatter::send(stream, F("<jO")); StringFormatter::send(stream, F("<jO"));
if (params==1) { // <JO> if (params==1) { // <JO>
for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) { for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) {
@ -807,7 +755,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
} }
} }
return; return;
case HASH_KEYWORD_P: // <JP id> returns turntable position list for the turntable id case "P"_hk: // <JP id> returns turntable position list for the turntable id
if (params==2) { // <JP id> if (params==2) { // <JP id>
Turntable *tto=Turntable::get(id); Turntable *tto=Turntable::get(id);
if (!tto || tto->isHidden()) { if (!tto || tto->isHidden()) {
@ -974,14 +922,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
switch (p[1]) { switch (p[1]) {
// Turnout messages use 1=throw, 0=close. // Turnout messages use 1=throw, 0=close.
case 0: case 0:
case HASH_KEYWORD_C: case "C"_hk:
state = true; state = true;
break; break;
case 1: case 1:
case HASH_KEYWORD_T: case "T"_hk:
state= false; state= false;
break; break;
case HASH_KEYWORD_X: case "X"_hk:
{ {
Turnout *tt = Turnout::get(p[0]); Turnout *tt = Turnout::get(p[0]);
if (tt) { if (tt) {
@ -998,14 +946,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
} }
default: // Anything else is some kind of turnout create function. default: // Anything else is some kind of turnout create function.
if (params == 6 && p[1] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n> if (params == 6 && p[1] == "SERVO"_hk) { // <T id SERVO n n n n>
if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5])) if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
return false; return false;
} else } else
if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // <T id VPIN n> if (params == 3 && p[1] == "VPIN"_hk) { // <T id VPIN n>
if (!VpinTurnout::create(p[0], p[2])) return false; if (!VpinTurnout::create(p[0], p[2])) return false;
} else } else
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) { if (params >= 3 && p[1] == "DCC"_hk) {
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T> // <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m> if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
if (!DCCTurnout::create(p[0], p[2], p[3])) return false; if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
@ -1071,41 +1019,41 @@ bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
switch (p[0]) switch (p[0])
{ {
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
case HASH_KEYWORD_PROGBOOST: case "PROGBOOST"_hk:
TrackManager::progTrackBoosted=true; TrackManager::progTrackBoosted=true;
return true; return true;
#endif #endif
case HASH_KEYWORD_RESET: case "RESET"_hk:
DCCTimer::reset(); DCCTimer::reset();
break; // and <X> if we didnt restart break; // and <X> if we didnt restart
case HASH_KEYWORD_SPEED28: case "SPEED28"_hk:
DCC::setGlobalSpeedsteps(28); DCC::setGlobalSpeedsteps(28);
DIAG(F("28 Speedsteps")); DIAG(F("28 Speedsteps"));
return true; return true;
case HASH_KEYWORD_SPEED128: case "SPEED128"_hk:
DCC::setGlobalSpeedsteps(128); DCC::setGlobalSpeedsteps(128);
DIAG(F("128 Speedsteps")); DIAG(F("128 Speedsteps"));
return true; return true;
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value> case "ACK"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
if (params >= 3) { if (params >= 3) {
if (p[1] == HASH_KEYWORD_LIMIT) { if (p[1] == "LIMIT"_hk) {
DCCACK::setAckLimit(p[2]); DCCACK::setAckLimit(p[2]);
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42> LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
} else if (p[1] == HASH_KEYWORD_MIN) { } else if (p[1] == "MIN"_hk) {
DCCACK::setMinAckPulseDuration(p[2]); DCCACK::setMinAckPulseDuration(p[2]);
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500> LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
} else if (p[1] == HASH_KEYWORD_MAX) { } else if (p[1] == "MAX"_hk) {
DCCACK::setMaxAckPulseDuration(p[2]); DCCACK::setMaxAckPulseDuration(p[2]);
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000> LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
} else if (p[1] == HASH_KEYWORD_RETRY) { } else if (p[1] == "RETRY"_hk) {
if (p[2] >255) p[2]=3; if (p[2] >255) p[2]=3;
LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // <D ACK RETRY 2> LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // <D ACK RETRY 2>
} }
} else { } else {
bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
DIAG(F("Ack diag %S"), onOff ? F("on") : F("off")); DIAG(F("Ack diag %S"), onOff ? F("on") : F("off"));
Diag::ACK = onOff; Diag::ACK = onOff;
@ -1123,66 +1071,66 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
{ {
if (params == 0) if (params == 0)
return false; return false;
bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
switch (p[0]) switch (p[0])
{ {
case HASH_KEYWORD_CABS: // <D CABS> case "CABS"_hk: // <D CABS>
DCC::displayCabList(stream); DCC::displayCabList(stream);
return true; return true;
case HASH_KEYWORD_RAM: // <D RAM> case "RAM"_hk: // <D RAM>
DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory()); DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
return true; return true;
case HASH_KEYWORD_CMD: // <D CMD ON/OFF> case "CMD"_hk: // <D CMD ON/OFF>
Diag::CMD = onOff; Diag::CMD = onOff;
return true; return true;
#ifdef HAS_ENOUGH_MEMORY #ifdef HAS_ENOUGH_MEMORY
case HASH_KEYWORD_WIFI: // <D WIFI ON/OFF> case "WIFI"_hk: // <D WIFI ON/OFF>
Diag::WIFI = onOff; Diag::WIFI = onOff;
return true; return true;
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF> case "ETHERNET"_hk: // <D ETHERNET ON/OFF>
Diag::ETHERNET = onOff; Diag::ETHERNET = onOff;
return true; return true;
case HASH_KEYWORD_WIT: // <D WIT ON/OFF> case "WIT"_hk: // <D WIT ON/OFF>
Diag::WITHROTTLE = onOff; Diag::WITHROTTLE = onOff;
return true; return true;
case HASH_KEYWORD_LCN: // <D LCN ON/OFF> case "LCN"_hk: // <D LCN ON/OFF>
Diag::LCN = onOff; Diag::LCN = onOff;
return true; return true;
#endif #endif
#ifndef DISABLE_EEPROM #ifndef DISABLE_EEPROM
case HASH_KEYWORD_EEPROM: // <D EEPROM NumEntries> case "EEPROM"_hk: // <D EEPROM NumEntries>
if (params >= 2) if (params >= 2)
EEStore::dump(p[1]); EEStore::dump(p[1]);
return true; return true;
#endif #endif
case HASH_KEYWORD_SERVO: // <D SERVO vpin position [profile]> case "SERVO"_hk: // <D SERVO vpin position [profile]>
case HASH_KEYWORD_ANOUT: // <D ANOUT vpin position [profile]> case "ANOUT"_hk: // <D ANOUT vpin position [profile]>
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
break; return true;
case HASH_KEYWORD_ANIN: // <D ANIN vpin> Display analogue input value case "ANIN"_hk: // <D ANIN vpin> Display analogue input value
DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1])); DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1]));
break; return true;
#if !defined(IO_NO_HAL) #if !defined(IO_NO_HAL)
case HASH_KEYWORD_HAL: case "HAL"_hk:
if (p[1] == HASH_KEYWORD_SHOW) if (p[1] == "SHOW"_hk)
IODevice::DumpAll(); IODevice::DumpAll();
else if (p[1] == HASH_KEYWORD_RESET) else if (p[1] == "RESET"_hk)
IODevice::reset(); IODevice::reset();
break; return true;
#endif #endif
case HASH_KEYWORD_TT: // <D TT vpin steps activity> case "TT"_hk: // <D TT vpin steps activity>
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
break; return true;
default: // invalid/unknown default: // invalid/unknown
return parseC(stream, params, p); return parseC(stream, params, p);
@ -1234,7 +1182,7 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable
{ {
Turntable *tto = Turntable::get(p[0]); Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_DCC) { if (p[1] == "DCC"_hk) {
if (tto || p[2] < 0 || p[2] > 3600) return false; if (tto || p[2] < 0 || p[2] > 3600) return false;
if (!DCCTurntable::create(p[0])) return false; if (!DCCTurntable::create(p[0])) return false;
Turntable *tto = Turntable::get(p[0]); Turntable *tto = Turntable::get(p[0]);
@ -1251,7 +1199,7 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 4: // <I id EXTT vpin home> create an EXTT turntable case 4: // <I id EXTT vpin home> create an EXTT turntable
{ {
Turntable *tto = Turntable::get(p[0]); Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_EXTT) { if (p[1] == "EXTT"_hk) {
if (tto || p[3] < 0 || p[3] > 3600) return false; if (tto || p[3] < 0 || p[3] > 3600) return false;
if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false; if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false;
Turntable *tto = Turntable::get(p[0]); Turntable *tto = Turntable::get(p[0]);
@ -1266,7 +1214,7 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 5: // <I id ADD position value angle> add a position case 5: // <I id ADD position value angle> add a position
{ {
Turntable *tto = Turntable::get(p[0]); Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_ADD) { if (p[1] == "ADD"_hk) {
// tto must exist, no more than 48 positions, angle 0 - 3600 // tto must exist, no more than 48 positions, angle 0 - 3600
if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false; if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false;
tto->addPosition(p[2], p[3], p[4]); tto->addPosition(p[2], p[3], p[4]);

View File

@ -1,5 +1,5 @@
/* /*
* © 2021-2022, Harald Barth. * © 2021-2024, Harald Barth.
* *
* This file is part of DCC-EX * This file is part of DCC-EX
* *
@ -25,6 +25,18 @@
#include "DCCWaveform.h" // for MAX_PACKET_SIZE #include "DCCWaveform.h" // for MAX_PACKET_SIZE
#include "soc/gpio_sig_map.h" #include "soc/gpio_sig_map.h"
// check for right type of ESP32
#include "soc/soc_caps.h"
#ifndef SOC_RMT_MEM_WORDS_PER_CHANNEL
#error This symobol should be defined
#endif
#if SOC_RMT_MEM_WORDS_PER_CHANNEL < 64
#warning This is not an ESP32-WROOM but some other unsupported variant
#warning You are outside of the DCC-EX supported hardware
#endif
static const byte RMT_CHAN_PER_DCC_CHAN = 2;
// Number of bits resulting out of X bytes of DCC payload data // Number of bits resulting out of X bytes of DCC payload data
// Each byte has one bit extra and at the end we have one EOF marker // Each byte has one bit extra and at the end we have one EOF marker
#define DATA_LEN(X) ((X)*9+1) #define DATA_LEN(X) ((X)*9+1)
@ -75,12 +87,30 @@ void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {
RMTChannel::RMTChannel(pinpair pins, bool isMain) { RMTChannel::RMTChannel(pinpair pins, bool isMain) {
byte ch; byte ch;
byte plen; byte plen;
// Below we check if the DCC packet actually fits into the RMT hardware
// Currently MAX_PACKET_SIZE = 5 so with checksum there are
// MAX_PACKET_SIZE+1 data packets. Each need DATA_LEN (9) bits.
// To that we add the preamble length, the fencepost DCC end bit
// and the RMT EOF marker.
// SOC_RMT_MEM_WORDS_PER_CHANNEL is either 64 (original WROOM) or
// 48 (all other ESP32 like the -C3 or -S2
// The formula to get the possible MAX_PACKET_SIZE is
//
// ALLOCATED = RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL
// MAX_PACKET_SIZE = floor((ALLOCATED - PREAMBLE_LEN - 2)/9 - 1)
//
if (isMain) { if (isMain) {
ch = 0; ch = 0;
plen = PREAMBLE_BITS_MAIN; plen = PREAMBLE_BITS_MAIN;
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_MAIN + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
"Number of DCC packet bits greater than ESP32 RMT memory available");
} else { } else {
ch = 2; ch = RMT_CHAN_PER_DCC_CHAN; // number == offset
plen = PREAMBLE_BITS_PROG; plen = PREAMBLE_BITS_PROG;
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_PROG + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
"Number of DCC packet bits greater than ESP32 RMT memory available");
} }
// preamble // preamble
@ -123,20 +153,10 @@ RMTChannel::RMTChannel(pinpair pins, bool isMain) {
config.channel = channel = (rmt_channel_t)ch; config.channel = channel = (rmt_channel_t)ch;
config.clk_div = RMT_CLOCK_DIVIDER; config.clk_div = RMT_CLOCK_DIVIDER;
config.gpio_num = (gpio_num_t)pins.pin; config.gpio_num = (gpio_num_t)pins.pin;
config.mem_block_num = 2; // With longest DCC packet 11 inc checksum (future expansion) config.mem_block_num = RMT_CHAN_PER_DCC_CHAN;
// number of bits needed is 22preamble + start + // use config
// 11*9 + extrazero + EOT = 124
// 2 mem block of 64 RMT items should be enough
ESP_ERROR_CHECK(rmt_config(&config)); ESP_ERROR_CHECK(rmt_config(&config));
addPin(pins.invpin, true); addPin(pins.invpin, true);
/*
// test: config another gpio pin
gpio_num_t gpioNum = (gpio_num_t)(pin-1);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX, 0, 0);
*/
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask // NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED)); ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));

View File

@ -2,7 +2,7 @@
* © 2021 M Steve Todd * © 2021 M Steve Todd
* © 2021 Mike S * © 2021 Mike S
* © 2021 Fred Decker * © 2021 Fred Decker
* © 2020-2021 Harald Barth * © 2020-2024 Harald Barth
* © 2020-2021 Chris Harlow * © 2020-2021 Chris Harlow
* All rights reserved. * All rights reserved.
* *
@ -33,8 +33,8 @@
// Number of preamble bits. // Number of preamble bits.
const int PREAMBLE_BITS_MAIN = 16; const byte PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22; const byte PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum. const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.

View File

@ -334,12 +334,14 @@ if (compileFeatures & FEATURE_SIGNAL) {
void RMFT2::setTurnoutHiddenState(Turnout * t) { void RMFT2::setTurnoutHiddenState(Turnout * t) {
// turnout descriptions are in low flash F strings // turnout descriptions are in low flash F strings
t->setHidden(GETFLASH(getTurnoutDescription(t->getId()))==0x01); const FSH *desc = getTurnoutDescription(t->getId());
if (desc) t->setHidden(GETFLASH(desc)==0x01);
} }
#ifndef IO_NO_HAL #ifndef IO_NO_HAL
void RMFT2::setTurntableHiddenState(Turntable * tto) { void RMFT2::setTurntableHiddenState(Turntable * tto) {
tto->setHidden(GETFLASH(getTurntableDescription(tto->getId()))==0x01); const FSH *desc = getTurntableDescription(tto->getId());
if (desc) tto->setHidden(GETFLASH(desc)==0x01);
} }
#endif #endif
@ -414,7 +416,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 +643,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:
@ -837,12 +837,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 +868,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

@ -67,6 +67,7 @@
#undef FWD #undef FWD
#undef GREEN #undef GREEN
#undef HAL #undef HAL
#undef HAL_IGNORE_DEFAULTS
#undef IF #undef IF
#undef IFAMBER #undef IFAMBER
#undef IFCLOSED #undef IFCLOSED
@ -218,6 +219,7 @@
#define FWD(speed) #define FWD(speed)
#define GREEN(signal_id) #define GREEN(signal_id)
#define HAL(haltype,params...) #define HAL(haltype,params...)
#define HAL_IGNORE_DEFAULTS
#define IF(sensor_id) #define IF(sensor_id)
#define IFAMBER(signal_id) #define IFAMBER(signal_id)
#define IFCLOSED(turnout_id) #define IFCLOSED(turnout_id)

View File

@ -28,25 +28,7 @@
#include "defines.h" #include "defines.h"
#include "EXRAIL2.h" #include "EXRAIL2.h"
#include "DCC.h" #include "DCC.h"
// Command parsing keywords #include "KeywordHasher.h"
const int16_t HASH_KEYWORD_EXRAIL=15435;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_START=23232;
const int16_t HASH_KEYWORD_RESERVE=11392;
const int16_t HASH_KEYWORD_FREE=-23052;
const int16_t HASH_KEYWORD_LATCH=1618;
const int16_t HASH_KEYWORD_UNLATCH=1353;
const int16_t HASH_KEYWORD_PAUSE=-4142;
const int16_t HASH_KEYWORD_RESUME=27609;
const int16_t HASH_KEYWORD_KILL=5218;
const int16_t HASH_KEYWORD_ALL=3457;
const int16_t HASH_KEYWORD_ROUTES=-3702;
const int16_t HASH_KEYWORD_RED=26099;
const int16_t HASH_KEYWORD_AMBER=18713;
const int16_t HASH_KEYWORD_GREEN=-31493;
const int16_t HASH_KEYWORD_A='A';
const int16_t HASH_KEYWORD_M='M';
// This filter intercepts <> commands to do the following: // This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics // - Implement RMFT specific commands/diagnostics
@ -58,8 +40,8 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
switch(opcode) { switch(opcode) {
case 'D': case 'D':
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF> if (p[0]=="EXRAIL"_hk) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1); diag = paramCount==2 && (p[1]=="ON"_hk || p[1]==1);
opcode=0; opcode=0;
} }
break; break;
@ -125,7 +107,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
case 'J': // throttle info commands case 'J': // throttle info commands
if (paramCount<1) return; if (paramCount<1) return;
switch(p[0]) { switch(p[0]) {
case HASH_KEYWORD_A: // <JA> returns automations/routes case "A"_hk: // <JA> returns automations/routes
if (paramCount==1) {// <JA> if (paramCount==1) {// <JA>
StringFormatter::send(stream, F("<jA")); StringFormatter::send(stream, F("<jA"));
routeLookup->stream(stream); routeLookup->stream(stream);
@ -134,7 +116,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
return; return;
} }
if (paramCount==2) { // <JA id> if (paramCount==2) { // <JA id>
uint16_t id=p[1]; int16_t id=p[1];
StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"), StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"),
id, getRouteType(id), getRouteDescription(id)); id, getRouteType(id), getRouteDescription(id));
@ -152,7 +134,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
return; return;
} }
break; break;
case HASH_KEYWORD_M: case "M"_hk:
// NOTE: we only need to handle valid calls here because // NOTE: we only need to handle valid calls here because
// DCCEXParser has to have code to handle the <J<> cases where // DCCEXParser has to have code to handle the <J<> cases where
// exrail isnt involved anyway. // exrail isnt involved anyway.
@ -236,13 +218,13 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
return true; return true;
} }
switch (p[0]) { switch (p[0]) {
case HASH_KEYWORD_PAUSE: // </ PAUSE> case "PAUSE"_hk: // </ PAUSE>
if (paramCount!=1) return false; if (paramCount!=1) return false;
DCC::setThrottle(0,1,true); // pause all locos on the track DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address pausingTask=(RMFT2 *)1; // Impossible task address
return true; return true;
case HASH_KEYWORD_RESUME: // </ RESUME> case "RESUME"_hk: // </ RESUME>
if (paramCount!=1) return false; if (paramCount!=1) return false;
pausingTask=NULL; pausingTask=NULL;
{ {
@ -256,7 +238,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
return true; return true;
case HASH_KEYWORD_START: // </ START [cab] route > case "START"_hk: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false; if (paramCount<2 || paramCount>3) return false;
{ {
int route=(paramCount==2) ? p[1] : p[2]; int route=(paramCount==2) ? p[1] : p[2];
@ -273,7 +255,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
} }
// check KILL ALL here, otherwise the next validation confuses ALL with a flag // check KILL ALL here, otherwise the next validation confuses ALL with a flag
if (p[0]==HASH_KEYWORD_KILL && p[1]==HASH_KEYWORD_ALL) { if (p[0]=="KILL"_hk && p[1]=="ALL"_hk) {
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
return true; return true;
} }
@ -282,7 +264,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount!=2 ) return false; if (paramCount!=2 ) return false;
switch (p[0]) { switch (p[0]) {
case HASH_KEYWORD_KILL: // Kill taskid|ALL case "KILL"_hk: // Kill taskid|ALL
{ {
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false; if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
RMFT2 * task=loopTask; RMFT2 * task=loopTask;
@ -297,27 +279,27 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
} }
return false; return false;
case HASH_KEYWORD_RESERVE: // force reserve a section case "RESERVE"_hk: // force reserve a section
return setFlag(p[1],SECTION_FLAG); return setFlag(p[1],SECTION_FLAG);
case HASH_KEYWORD_FREE: // force free a section case "FREE"_hk: // force free a section
return setFlag(p[1],0,SECTION_FLAG); return setFlag(p[1],0,SECTION_FLAG);
case HASH_KEYWORD_LATCH: case "LATCH"_hk:
return setFlag(p[1], LATCH_FLAG); return setFlag(p[1], LATCH_FLAG);
case HASH_KEYWORD_UNLATCH: case "UNLATCH"_hk:
return setFlag(p[1], 0, LATCH_FLAG); return setFlag(p[1], 0, LATCH_FLAG);
case HASH_KEYWORD_RED: case "RED"_hk:
doSignal(p[1],SIGNAL_RED); doSignal(p[1],SIGNAL_RED);
return true; return true;
case HASH_KEYWORD_AMBER: case "AMBER"_hk:
doSignal(p[1],SIGNAL_AMBER); doSignal(p[1],SIGNAL_AMBER);
return true; return true;
case HASH_KEYWORD_GREEN: case "GREEN"_hk:
doSignal(p[1],SIGNAL_GREEN); doSignal(p[1],SIGNAL_GREEN);
return true; return true;

View File

@ -74,13 +74,81 @@
#define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10; #define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10;
#include "myAutomation.h" #include "myAutomation.h"
// Pass 1d Detect sequence duplicates.
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
#undef ROUTE
#define ROUTE(id, description) id,
#undef SEQUENCE
#define SEQUENCE(id) id,
constexpr int16_t compileTimeSequenceList[]={
#include "myAutomation.h"
0
};
constexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;
// Compile time function to check for sequence nos.
constexpr bool hasseq(const int16_t value, const uint16_t pos=0 ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1);
}
// Compile time function to check for duplicate sequence nos.
constexpr bool hasdup(const int16_t value, const uint16_t pos ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1)
|| hasdup(compileTimeSequenceList[pos],pos+1);
}
static_assert(!hasdup(compileTimeSequenceList[0],1),"Duplicate SEQUENCE/ROUTE/AUTOMATION detected");
//pass 1s static asserts to
// - check call and follows etc for existing sequence numbers
// - check range on LATCH/UNLATCH
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef CALL
#define CALL(id) static_assert(hasseq(id),"Sequence not found");
#undef FOLLOW
#define FOLLOW(id) static_assert(hasseq(id),"Sequence not found");
#undef START
#define START(id) static_assert(hasseq(id),"Sequence not found");
#undef SENDLOCO
#define SENDLOCO(cab,id) static_assert(hasseq(id),"Sequence not found");
#undef LATCH
#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef UNLATCH
#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef RESERVE
#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef FREE
#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef SPEED
#define SPEED(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef FWD
#define FWD(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef REV
#define REV(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#include "myAutomation.h"
// Pass 1h Implements HAL macro by creating exrailHalSetup function // Pass 1h Implements HAL macro by creating exrailHalSetup function
// Also allows creating EXTurntable object // Also allows creating EXTurntable object
#include "EXRAIL2MacroReset.h" #include "EXRAIL2MacroReset.h"
#undef HAL #undef HAL
#define HAL(haltype,params...) haltype::create(params); #define HAL(haltype,params...) haltype::create(params);
void exrailHalSetup() { #undef HAL_IGNORE_DEFAULTS
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
bool exrailHalSetup() {
bool ignore_defaults=false;
#include "myAutomation.h" #include "myAutomation.h"
return ignore_defaults;
} }
// Pass 1c detect compile time featurtes // Pass 1c detect compile time featurtes
@ -396,6 +464,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define FWD(speed) OPCODE_FWD,V(speed), #define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id), #define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define HAL(haltype,params...) #define HAL(haltype,params...)
#define HAL_IGNORE_DEFAULTS
#define IF(sensor_id) OPCODE_IF,V(sensor_id), #define IF(sensor_id) OPCODE_IF,V(sensor_id),
#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id), #define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id), #define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),

View File

@ -47,6 +47,10 @@ void EthernetInterface::setup()
}; };
#ifdef IP_ADDRESS
static IPAddress myIP(IP_ADDRESS);
#endif
/** /**
* @brief Aquire IP Address from DHCP and start server * @brief Aquire IP Address from DHCP and start server
* *
@ -59,15 +63,15 @@ EthernetInterface::EthernetInterface()
DCCTimer::getSimulatedMacAddress(mac); DCCTimer::getSimulatedMacAddress(mac);
connected=false; connected=false;
#ifdef IP_ADDRESS #ifdef IP_ADDRESS
Ethernet.begin(mac, IP_ADDRESS); Ethernet.begin(mac, myIP);
#else #else
if (Ethernet.begin(mac) == 0) if (Ethernet.begin(mac) == 0)
{ {
DIAG(F("Ethernet.begin FAILED")); DIAG(F("Ethernet.begin FAILED"));
return; return;
} }
#endif #endif
if (Ethernet.hardwareStatus() == EthernetNoHardware) { if (Ethernet.hardwareStatus() == EthernetNoHardware) {
DIAG(F("Ethernet shield not found or W5100")); DIAG(F("Ethernet shield not found or W5100"));
} }
@ -136,7 +140,7 @@ bool EthernetInterface::checkLink() {
DIAG(F("Ethernet cable connected")); DIAG(F("Ethernet cable connected"));
connected=true; connected=true;
#ifdef IP_ADDRESS #ifdef IP_ADDRESS
Ethernet.setLocalIP(IP_ADDRESS); // for static IP, set it again Ethernet.setLocalIP(myIP); // for static IP, set it again
#endif #endif
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static) IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT

View File

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

View File

@ -110,7 +110,6 @@ 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;
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

@ -33,7 +33,7 @@
// Link to halSetup function. If not defined, the function reference will be NULL. // Link to halSetup function. If not defined, the function reference will be NULL.
extern __attribute__((weak)) void halSetup(); extern __attribute__((weak)) void halSetup();
extern __attribute__((weak)) void exrailHalSetup(); extern __attribute__((weak)) bool exrailHalSetup();
//================================================================================================================== //==================================================================================================================
// Static methods // Static methods
@ -60,33 +60,30 @@ void IODevice::begin() {
halSetup(); halSetup();
// include any HAL devices defined in exrail. // include any HAL devices defined in exrail.
bool ignoreDefaults=false;
if (exrailHalSetup) if (exrailHalSetup)
exrailHalSetup(); ignoreDefaults=exrailHalSetup();
if (ignoreDefaults) return;
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts // Predefine two PCA9685 modules 0x40-0x41 if no conflicts
// Allocates 32 pins 100-131 // Allocates 32 pins 100-131
if (checkNoOverlap(100, 16, 0x40)) { const bool silent=true; // no message if these conflict
if (checkNoOverlap(100, 16, 0x40, silent)) {
PCA9685::create(100, 16, 0x40); PCA9685::create(100, 16, 0x40);
} else {
DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device"));
} }
if (checkNoOverlap(116, 16, 0x41)) {
if (checkNoOverlap(116, 16, 0x41, silent)) {
PCA9685::create(116, 16, 0x41); PCA9685::create(116, 16, 0x41);
} else {
DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device"));
} }
// Predefine two MCP23017 module 0x20/0x21 if no conflicts // Predefine two MCP23017 module 0x20/0x21 if no conflicts
// Allocates 32 pins 164-195 // Allocates 32 pins 164-195
if (checkNoOverlap(164, 16, 0x20)) { if (checkNoOverlap(164, 16, 0x20, silent)) {
MCP23017::create(164, 16, 0x20); MCP23017::create(164, 16, 0x20);
} else {
DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device"));
} }
if (checkNoOverlap(180, 16, 0x21)) {
if (checkNoOverlap(180, 16, 0x21, silent)) {
MCP23017::create(180, 16, 0x21); MCP23017::create(180, 16, 0x21);
} else {
DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device"));
} }
} }
@ -339,7 +336,10 @@ IODevice *IODevice::findDeviceFollowing(VPIN vpin) {
// returns true if pins DONT overlap with existing device // returns true if pins DONT overlap with existing device
// TODO: Move the I2C address reservation and checks into the I2CManager code. // TODO: Move the I2C address reservation and checks into the I2CManager code.
// That will enable non-HAL devices to reserve I2C addresses too. // That will enable non-HAL devices to reserve I2C addresses too.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) { // Silent is used by the default setup so that there is no message if the default
// device has already been handled by the user setup.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins,
I2CAddress i2cAddress, bool silent) {
#ifdef DIAG_IO #ifdef DIAG_IO
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString()); DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
#endif #endif
@ -352,14 +352,14 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddres
VPIN lastDevPin=firstDevPin+dev->_nPins-1; VPIN lastDevPin=firstDevPin+dev->_nPins-1;
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin; bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
if (!noOverlap) { if (!noOverlap) {
DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."), if (!silent) DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
firstPin, lastPin); firstPin, lastPin);
return false; return false;
} }
} }
// Check for overlapping I2C address // Check for overlapping I2C address
if (i2cAddress && dev->_I2CAddress==i2cAddress) { if (i2cAddress && dev->_I2CAddress==i2cAddress) {
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString()); if (!silent) DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
return false; return false;
} }
} }

View File

@ -166,7 +166,8 @@ public:
void setGPIOInterruptPin(int16_t pinNumber); void setGPIOInterruptPin(int16_t pinNumber);
// Method to check if pins will overlap before creating new device. // Method to check if pins will overlap before creating new device.
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=0); static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1,
I2CAddress i2cAddress=0, bool silent=false);
// Method used by IODevice filters to locate slave pins that may be overlayed by their own // Method used by IODevice filters to locate slave pins that may be overlayed by their own
// pin range. // pin range.

View File

@ -1,5 +1,6 @@
/* /*
* © 2022, Peter Cole. All rights reserved. * © 2022, Peter Cole. All rights reserved.
* © 2024, Harald Barth. All rights reserved.
* *
* This file is part of EX-CommandStation * This file is part of EX-CommandStation
* *
@ -22,13 +23,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,13 +96,23 @@ private:
_numAnaloguePins = receiveBuffer[2]; _numAnaloguePins = receiveBuffer[2];
// See if we already have suitable buffers assigned // See if we already have suitable buffers assigned
if (_numDigitalPins>0) {
size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8; size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;
if (_digitalPinBytes < digitalBytesNeeded) { if (_digitalPinBytes < digitalBytesNeeded) {
// Not enough space, free any existing buffer and allocate a new one // Not enough space, free any existing buffer and allocate a new one
if (_digitalPinBytes > 0) free(_digitalInputStates); if (_digitalPinBytes > 0) free(_digitalInputStates);
_digitalInputStates = (byte*) calloc(_digitalPinBytes, 1); if ((_digitalInputStates = (byte*) calloc(digitalBytesNeeded, 1)) != NULL) {
_digitalPinBytes = digitalBytesNeeded; _digitalPinBytes = digitalBytesNeeded;
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR alloc %d bytes"), _I2CAddress.toString(), digitalBytesNeeded);
_deviceState = DEVSTATE_FAILED;
_digitalPinBytes = 0;
return;
} }
}
}
if (_numAnaloguePins>0) {
size_t analogueBytesNeeded = _numAnaloguePins * 2; size_t analogueBytesNeeded = _numAnaloguePins * 2;
if (_analoguePinBytes < analogueBytesNeeded) { if (_analoguePinBytes < analogueBytesNeeded) {
// Free any existing buffers and allocate new ones. // Free any existing buffers and allocate new ones.
@ -116,7 +124,17 @@ private:
_analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1); _analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1); _analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
if (_analogueInputStates != NULL &&
_analogueInputBuffer != NULL &&
_analoguePinMap != NULL) {
_analoguePinBytes = analogueBytesNeeded; _analoguePinBytes = analogueBytesNeeded;
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR alloc analog pin bytes"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
_analoguePinBytes = 0;
return;
}
}
} }
} 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 +142,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 +257,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 (_numDigitalPins>0 && currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh
// Issue new read request for digital states. As the request is non-blocking, the buffer has to // 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 +265,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 (_numAnaloguePins>0 && currentMicros - _lastAnalogueRead > _analogueRefresh) { // 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,
@ -362,14 +380,14 @@ private:
uint8_t _minorVer = 0; uint8_t _minorVer = 0;
uint8_t _patchVer = 0; uint8_t _patchVer = 0;
uint8_t* _digitalInputStates; uint8_t* _digitalInputStates = NULL;
uint8_t* _analogueInputStates; uint8_t* _analogueInputStates = NULL;
uint8_t* _analogueInputBuffer; // buffer for I2C input transfers uint8_t* _analogueInputBuffer = NULL; // buffer for I2C input transfers
uint8_t _readCommandBuffer[1]; uint8_t _readCommandBuffer[1];
uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed) uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t _analoguePinBytes = 0; // Size of allocated memory buffers (may be longer than needed) uint8_t _analoguePinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t* _analoguePinMap; uint8_t* _analoguePinMap = NULL;
I2CRB _i2crb; I2CRB _i2crb;
enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states

57
KeywordHasher.h Normal file
View File

@ -0,0 +1,57 @@
/*
* © 2024 Vincent Hamp and Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* Reader be aware:
This function implements the _hk data type so that a string keyword
is hashed to the same value as the DCCEXParser uses to hash incoming
keywords.
Thus "MAIN"_hk generates exactly the same run time vakue
as const int16_t HASH_KEYWORD_MAIN=11339
*/
#ifndef KeywordHAsher_h
#define KeywordHasher_h
#include <Arduino.h>
constexpr uint16_t CompiletimeKeywordHasher(const char * sv, uint16_t running=0) {
return (*sv==0) ? running : CompiletimeKeywordHasher(sv+1,
(*sv >= '0' && *sv <= '9')
? (10*running+*sv-'0') // Numeric hash
: ((running << 5) + running) ^ *sv
); //
}
constexpr int16_t operator""_hk(const char * keyword, size_t len)
{
return (int16_t) CompiletimeKeywordHasher(keyword,len*0);
}
/* Some historical values for testing:
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_SLOW = -17209;
const int16_t HASH_KEYWORD_SPEED28 = -17064;
const int16_t HASH_KEYWORD_SPEED128 = 25816;
*/
static_assert("MAIN"_hk == 11339,"Keyword hasher error");
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
#endif

View File

@ -28,6 +28,7 @@
#include "DIAG.h" #include "DIAG.h"
#include "CommandDistributor.h" #include "CommandDistributor.h"
#include "DCCEXParser.h" #include "DCCEXParser.h"
#include "KeywordHasher.h"
// Virtualised Motor shield multi-track hardware Interface // Virtualised Motor shield multi-track hardware Interface
#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++) #define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)
@ -35,21 +36,6 @@
FOR_EACH_TRACK(t) \ FOR_EACH_TRACK(t) \
if (track[t]->getMode()==findmode) \ if (track[t]->getMode()==findmode) \
track[t]->function; track[t]->function;
#ifndef DISABLE_PROG
const int16_t HASH_KEYWORD_PROG = -29718;
#endif
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_OFF = 22479;
const int16_t HASH_KEYWORD_NONE = -26550;
const int16_t HASH_KEYWORD_DC = 2183;
const int16_t HASH_KEYWORD_DCX = 6463; // DC reversed polarity
const int16_t HASH_KEYWORD_EXT = 8201; // External DCC signal
const int16_t HASH_KEYWORD_A = 65; // parser makes single chars the ascii.
const int16_t HASH_KEYWORD_AUTO = -5457;
#ifdef BOOSTER_INPUT
const int16_t HASH_KEYWORD_BOOST = 11269;
#endif
const int16_t HASH_KEYWORD_INV = 11857;
MotorDriver * TrackManager::track[MAX_TRACKS]; MotorDriver * TrackManager::track[MAX_TRACKS];
int16_t TrackManager::trackDCAddr[MAX_TRACKS]; int16_t TrackManager::trackDCAddr[MAX_TRACKS];
@ -362,38 +348,38 @@ bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
} }
p[0]-=HASH_KEYWORD_A; // convert A... to 0.... p[0]-="A"_hk; // convert A... to 0....
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS)) if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
return false; return false;
if (params==2 && p[1]==HASH_KEYWORD_MAIN) // <= id MAIN> if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
return setTrackMode(p[0],TRACK_MODE_MAIN); return setTrackMode(p[0],TRACK_MODE_MAIN);
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
if (params==2 && p[1]==HASH_KEYWORD_PROG) // <= id PROG> if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
return setTrackMode(p[0],TRACK_MODE_PROG); return setTrackMode(p[0],TRACK_MODE_PROG);
#endif #endif
if (params==2 && (p[1]==HASH_KEYWORD_OFF || p[1]==HASH_KEYWORD_NONE)) // <= id OFF> <= id NONE> if (params==2 && (p[1]=="OFF"_hk || p[1]=="NONE"_hk)) // <= id OFF> <= id NONE>
return setTrackMode(p[0],TRACK_MODE_NONE); return setTrackMode(p[0],TRACK_MODE_NONE);
if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT> if (params==2 && p[1]=="EXT"_hk) // <= id EXT>
return setTrackMode(p[0],TRACK_MODE_EXT); return setTrackMode(p[0],TRACK_MODE_EXT);
#ifdef BOOSTER_INPUT #ifdef BOOSTER_INPUT
if (params==2 && p[1]==HASH_KEYWORD_BOOST) // <= id BOOST> if (params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
return setTrackMode(p[0],TRACK_MODE_BOOST); return setTrackMode(p[0],TRACK_MODE_BOOST);
#endif #endif
if (params==2 && p[1]==HASH_KEYWORD_AUTO) // <= id AUTO> if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV); return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV);
if (params==2 && p[1]==HASH_KEYWORD_INV) // <= id AUTO> if (params==2 && p[1]=="INV"_hk) // <= id AUTO>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_INV); return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_INV);
if (params==3 && p[1]==HASH_KEYWORD_DC && p[2]>0) // <= id DC cab> if (params==3 && p[1]=="DC"_hk && p[2]>0) // <= id DC cab>
return setTrackMode(p[0],TRACK_MODE_DC,p[2]); return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
if (params==3 && p[1]==HASH_KEYWORD_DCX && p[2]>0) // <= id DCX cab> if (params==3 && p[1]=="DCX"_hk && p[2]>0) // <= id DCX cab>
return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]); return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]);
return false; return false;
@ -489,10 +475,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 +496,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 +522,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

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) {

View File

@ -74,25 +74,39 @@ class NetworkClient {
public: public:
NetworkClient(WiFiClient c) { NetworkClient(WiFiClient c) {
wifi = c; wifi = c;
inUse = true;
}; };
bool ok() { bool active(byte clientId) {
return (inUse && wifi.connected()); if (!inUse)
};
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; return false;
if(!wifi.connected()) {
DIAG(F("Remove client %d"), clientId);
CommandDistributor::forget(clientId);
wifi.stop();
inUse = false;
return false;
}
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; wifi = c;
inUse = true; inUse = true;
return 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
@ -150,6 +164,8 @@ bool WifiESP::setup(const char *SSid,
if (haveSSID && havePassword && !forceAP) { if (haveSSID && havePassword && !forceAP) {
WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE! WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE!
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // Scan all channels so we find strongest
// (default in Wifi library is first match)
#ifdef SERIAL_BT_COMMANDS #ifdef SERIAL_BT_COMMANDS
WiFi.setSleep(true); WiFi.setSleep(true);
#else #else
@ -165,7 +181,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);
@ -190,7 +206,7 @@ bool WifiESP::setup(const char *SSid,
if (!haveSSID || forceAP) { if (!haveSSID || forceAP) {
// prepare all strings // prepare all strings
String strSSID(forceAP ? SSid : "DCCEX_"); String strSSID(forceAP ? SSid : "DCCEX_");
String strPass(forceAP ? password : "PASS_"); String strPass( (forceAP && havePassword) ? password : "PASS_");
if (!forceAP) { if (!forceAP) {
String strMac = WiFi.macAddress(); String strMac = WiFi.macAddress();
strMac.remove(0,9); strMac.remove(0,9);
@ -214,7 +230,8 @@ bool WifiESP::setup(const char *SSid,
// DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str()); // DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
DIAG(F("Wifi in AP mode")); DIAG(F("Wifi in AP mode"));
LCD(5, F("Wifi: %s"), strSSID.c_str()); LCD(5, F("Wifi: %s"), strSSID.c_str());
LCD(6, F("PASS: %s"),havePassword ? password : strPass.c_str()); if (!havePassword)
LCD(6, F("PASS: %s"),strPass.c_str());
// DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str()); // DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
LCD(7, F("IP: %s"),WiFi.softAPIP().toString().c_str()); LCD(7, F("IP: %s"),WiFi.softAPIP().toString().c_str());
wifiUp = true; wifiUp = true;
@ -282,37 +299,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 +356,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 +389,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

@ -222,6 +222,14 @@ The configuration file for DCC-EX Command Station
// We do not support to use the same address, for example 100(long) and 100(short) // We do not support to use the same address, for example 100(long) and 100(short)
// at the same time, there must be a border. // at the same time, there must be a border.
/////////////////////////////////////////////////////////////////////////////////////
// Some newer 32bit microcontrollers boot very quickly, so powering on I2C and other
// peripheral devices at the same time may result in the CommandStation booting too
// quickly to detect them.
// To work around this, uncomment the STARTUP_DELAY line below and set a value in
// milliseconds that works for your environment, default is 3000 (3 seconds).
// #define STARTUP_DELAY 3000
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// //
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213 // DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213

View File

@ -3,7 +3,27 @@
#include "StringFormatter.h" #include "StringFormatter.h"
#define VERSION "5.2.14" #define VERSION "5.2.28"
// 5.2.28 - ESP32: Can all Wifi channels.
// - ESP32: Only write Wifi password to display if it is a well known one
// 5.2.27 - Bugfix: IOExpander memory allocation
// 5.2.26 - Silently ignore overridden HAL defaults
// - include HAL_IGNORE_DEFAULTS macro in EXRAIL
// 5.2.25 - Fix bug causing <X> after working <D commands
// 5.2.24 - Exrail macro asserts to catch
// : duplicate/missing automation/route/sequence/call ids
// : latches and reserves out of range
// : speeds out of range
// 5.2.23 - KeywordHasher _hk (no functional change)
// 5.2.22 - Bugfixes: Empty turnout descriptions ok; negative route numbers valid.
// 5.2.21 - Add STARTUP_DELAY config option to delay CS bootup
// 5.2.20 - Check return of Ethernet.begin()
// 5.2.19 - ESP32: Determine if the RMT hardware can handle DCC
// 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 // 5.2.14 - Reminder window DCC packet optimization
// - Optional #define DISABLE_FUNCTION_REMINDERS // - Optional #define DISABLE_FUNCTION_REMINDERS
// 5.2.13 - EXRAIL STEALTH // 5.2.13 - EXRAIL STEALTH
@ -204,9 +224,18 @@
// TrackManager DCC & DC up to 8 Districts Architecture // TrackManager DCC & DC up to 8 Districts Architecture
// Automatic ALIAS(name) // Automatic ALIAS(name)
// Command Parser now accepts Underscore in Alias Names // Command Parser now accepts Underscore in Alias Names
// 4.1.6 Support for new EX-MotorShield8874 Dual 5Amp Shield
// 4.1.5 Bugfix LCN number parsing
// 4.1.4 Bugfix for issue #299 TurnoutDescription NULL
// 4.1.3 Bugfix: Ethernet init order
// 4.1.2 Bugfix: Ethernet shield W5100 does not report HW or link level
// 4.1.1 Bugfix: preserve turnout format // 4.1.1 Bugfix: preserve turnout format
// Bugfix: parse multiple commands in one buffer string correct // Bugfix: parse multiple commands in one buffer string correctly (ex: <s><Q>)
// Bugfix: </> command signal status in Exrail // Bugfix: </> command signal status of EX-RAIL tasks or threads
// Bugfix: EX-RAIL read long loco addr
// Bugfix: Add space character after version string 4.1.1 for JMRI parsing.
// Improved display and loop time for signals make service start to be outside the DONT_TOUCH_WIFI_CONF area
// Improve WiFi startup by making service start to be outside the DONT_TOUCH_WIFI_CONF area
// 4.1.0 ... // 4.1.0 ...
// //
// 4.0.2 EXRAIL additions: // 4.0.2 EXRAIL additions: