1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-11-24 08:36:14 +01:00

Compare commits

...

26 Commits

Author SHA1 Message Date
Kcsmith0708
b41bb305dc
Merge 183b824a5d into ebaf1b984e 2023-11-24 17:07:43 -07:00
Harald Barth
ebaf1b984e version tag 2023-11-23 22:15:03 +01:00
Harald Barth
697f228a05 Save progmem with DISABLE_VDPY on Uno 2023-11-23 22:14:24 +01:00
Harald Barth
c8e307db7a remove unused TrackManager::reportPowerChange(...) 2023-11-23 22:11:00 +01:00
Asbelos
a5ccb2e29e EXRAIL STASH 2023-11-23 14:15:58 +00:00
Asbelos
42e2e69f5f Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-23 10:41:40 +00:00
Asbelos
2075bc50e8 EXRAIL basic stash implementation 2023-11-23 10:41:35 +00:00
Harald Barth
a16214790e version 5.2.8 2023-11-23 10:49:15 +01:00
Harald Barth
784934024e Bugfix: Do not turn off all tracks on change ; give better power messages 2023-11-23 10:47:43 +01:00
Asbelos
b478056a9f Fix @ reporting on startup 2023-11-23 09:00:49 +00:00
Harald Barth
ef47257d67 version tag 2023-11-22 10:54:01 +01:00
Harald Barth
03db06f2ee Bugfix: Do not turn on track with trackmode NONE 2023-11-22 10:53:34 +01:00
Harald Barth
4308739c2b version 5.2.7 2023-11-21 23:04:05 +01:00
Harald Barth
0cfea3e1a5 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-21 22:48:54 +01:00
Harald Barth
d0df9f3c33 version tag 2023-11-21 22:48:35 +01:00
Harald Barth
ac4af407aa On ESP32, the inversion is already done in HW 2023-11-21 22:47:48 +01:00
Asbelos
a236a205fe Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-21 21:18:11 +00:00
Asbelos
478e9661bb EXRAIL ling segment 2023-11-21 21:14:54 +00:00
Harald Barth
2c1b3e0a8f version tag 2023-11-21 21:16:42 +01:00
Harald Barth
e7c4af5d4a back out wrong const change 2023-11-21 21:16:20 +01:00
Harald Barth
263ed18b25 version tag 2023-11-21 15:37:47 +01:00
Harald Barth
4e1fad4832 Trackmanager consolidate getModeName 2023-11-21 15:37:08 +01:00
Harald Barth
29ea746062 version 5.2.6 2023-11-21 11:54:43 +01:00
Harald Barth
e6f33cfdee Trackmanager broadcast power state on track mode change 2023-11-21 11:51:26 +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
19 changed files with 280 additions and 95 deletions

View File

@ -254,29 +254,64 @@ void CommandDistributor::broadcastPower() {
if (TrackManager::getPower(t, pstr)) if (TrackManager::getPower(t, pstr))
broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr); broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr);
byte trackcount=0;
byte oncount=0;
byte offcount=0;
for(byte t=0; t<TrackManager::MAX_TRACKS; t++) {
if (TrackManager::isActive(t)) {
trackcount++;
// do not call getPower(t) unless isActive(t)!
if (TrackManager::getPower(t) == POWERMODE::ON)
oncount++;
else
offcount++;
}
}
//DIAG(F("t=%d on=%d off=%d"), trackcount, oncount, offcount);
char state='2';
if (oncount==0 || offcount == trackcount)
state = '0';
else if (oncount == trackcount) {
state = '1';
}
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON; bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON; bool prog=TrackManager::getProgPower()==POWERMODE::ON;
bool join=TrackManager::isJoined(); bool join=TrackManager::isJoined();
//DIAG(F("m=%d p=%d j=%d"), main, prog, join);
const FSH * reason=F(""); const FSH * reason=F("");
char state='1'; if (join) {
if (main && prog && join) reason=F(" JOIN"); reason = F("JOIN");
else if (main && prog); broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
else if (main) reason=F(" MAIN"); } else {
else if (prog) reason=F(" PROG"); if (main) {
else state='0'; //reason = F("MAIN");
broadcastReply(COMMAND_TYPE, F("<p%c%S>\n"),state,reason); broadcastReply(COMMAND_TYPE, F("<p1 MAIN>\n"));
}
if (prog) {
//reason = F("PROG");
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING #ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1':'0'); // send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
#endif #endif
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),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) {
broadcastReply(type, F("%s"),msg); broadcastReply(type, F("%s"),msg);
} }
void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter, int16_t dcAddr) { void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format, trackLetter, dcAddr); broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
} }
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) { void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
@ -330,7 +365,7 @@ void CommandDistributor::setVirtualLCDSerial(Print * stream) {
virtualLCDSerial=stream; virtualLCDSerial=stream;
} }
Print* CommandDistributor::virtualLCDSerial=nullptr; Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
byte CommandDistributor::virtualLCDClient=0xFF; byte CommandDistributor::virtualLCDClient=0xFF;
byte CommandDistributor::rememberVLCDClient=0; byte CommandDistributor::rememberVLCDClient=0;

View File

@ -55,7 +55,7 @@ public :
static int16_t retClockTime(); static int16_t retClockTime();
static void broadcastPower(); static void broadcastPower();
static void broadcastRaw(clientType type,char * msg); static void broadcastRaw(clientType type,char * msg);
static void broadcastTrackState(const FSH* format,byte trackLetter, int16_t dcAddr); static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg); template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId); static void forget(byte clientId);
static void broadcastRouteState(uint16_t routeId,byte state); static void broadcastRouteState(uint16_t routeId,byte state);

View File

@ -87,7 +87,7 @@ void setup()
DISPLAY_START ( DISPLAY_START (
// This block is still executed for DIAGS if display not in use // This block is still executed for DIAGS if display not in use
LCD(0,F("DCC-EX v%S"),F(VERSION)); LCD(0,F("DCC-EX v" VERSION));
LCD(1,F("Lic GPLv3")); LCD(1,F("Lic GPLv3"));
); );

View File

@ -160,6 +160,7 @@ const int16_t HASH_KEYWORD_C='C';
const int16_t HASH_KEYWORD_G='G'; const int16_t HASH_KEYWORD_G='G';
const int16_t HASH_KEYWORD_H='H'; const int16_t HASH_KEYWORD_H='H';
const int16_t HASH_KEYWORD_I='I'; 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_O='O';
const int16_t HASH_KEYWORD_P='P'; const int16_t HASH_KEYWORD_P='P';
const int16_t HASH_KEYWORD_R='R'; const int16_t HASH_KEYWORD_R='R';
@ -668,7 +669,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
break; break;
#endif #endif
case '=': // TRACK MANAGER CONTROL <= [params]> case '=': // TRACK MANAGER CONTROL <= [params]>
if (TrackManager::parseJ(stream, params, p)) if (TrackManager::parseEqualSign(stream, params, p))
return; return;
break; break;
@ -729,6 +730,12 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
StringFormatter::send(stream, F("<jA>\n")); StringFormatter::send(stream, F("<jA>\n"));
return; return;
case HASH_KEYWORD_M: // <JM> intercepted by EXRAIL
if (params>1) break; // invalid cant do
// <JM> requests stash size so say none.
StringFormatter::send(stream,F("<jM 0>\n"));
return;
case HASH_KEYWORD_R: // <JR> returns rosters case HASH_KEYWORD_R: // <JR> returns rosters
StringFormatter::send(stream, F("<jR")); StringFormatter::send(stream, F("<jR"));
#ifdef EXRAIL_ACTIVE #ifdef EXRAIL_ACTIVE
@ -840,13 +847,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'L': // LCC interface implemented in EXRAIL parser case 'L': // LCC interface implemented in EXRAIL parser
break; // Will <X> if not intercepted by EXRAIL break; // Will <X> if not intercepted by EXRAIL
#ifndef DISABLE_VDPY
case '@': // JMRI saying "give me virtual LCD msgs" case '@': // JMRI saying "give me virtual LCD msgs"
CommandDistributor::setVirtualLCDSerial(stream); CommandDistributor::setVirtualLCDSerial(stream);
StringFormatter::send(stream, StringFormatter::send(stream,
F("<@ 0 0 \"DCC-EX v" VERSION "\">\n" F("<@ 0 0 \"DCC-EX v" VERSION "\">\n"
"<@ 0 1 \"Lic GPLv3\">\n")); "<@ 0 1 \"Lic GPLv3\">\n"));
return; return;
#endif
default: //anything else will diagnose and drop out to <X> default: //anything else will diagnose and drop out to <X>
if (opcode >= ' ' && opcode <= '~') { if (opcode >= ' ' && opcode <= '~') {
DIAG(F("Opcode=%c params=%d"), opcode, params); DIAG(F("Opcode=%c params=%d"), opcode, params);
@ -1057,6 +1065,7 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
} }
bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) { bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
(void)stream; // arg not used, maybe later?
if (params == 0) if (params == 0)
return false; return false;
switch (p[0]) switch (p[0])

View File

@ -54,7 +54,9 @@
xxx; \ xxx; \
t->refresh();} t->refresh();}
#else #else
#define DISPLAY_START(xxx) {} #define DISPLAY_START(xxx) { \
xxx; \
}
#endif #endif
#endif // LCD_Implementation_h #endif // LCD_Implementation_h

View File

@ -86,6 +86,8 @@ LookList * RMFT2::onRotateLookup=NULL;
LookList * RMFT2::onOverloadLookup=NULL; LookList * RMFT2::onOverloadLookup=NULL;
byte * RMFT2::routeStateArray=nullptr; byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr; const FSH * * RMFT2::routeCaptionArray=nullptr;
int16_t * RMFT2::stashArray=nullptr;
int16_t RMFT2::maxStashId=0;
// getOperand instance version, uses progCounter from instance. // getOperand instance version, uses progCounter from instance.
uint16_t RMFT2::getOperand(byte n) { uint16_t RMFT2::getOperand(byte n) {
@ -232,6 +234,12 @@ if (compileFeatures & FEATURE_SIGNAL) {
IODevice::configureInput((VPIN)pin,true); IODevice::configureInput((VPIN)pin,true);
break; break;
} }
case OPCODE_STASH:
case OPCODE_CLEAR_STASH:
case OPCODE_PICKUP_STASH: {
maxStashId=max(maxStashId,((int16_t)operand));
break;
}
case OPCODE_ATGTE: case OPCODE_ATGTE:
case OPCODE_ATLT: case OPCODE_ATLT:
@ -312,7 +320,13 @@ if (compileFeatures & FEATURE_SIGNAL) {
} }
SKIPOP; // include ENDROUTES opcode SKIPOP; // include ENDROUTES opcode
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS); if (compileFeatures & FEATURE_STASH) {
// create the stash array from the highest id found
if (maxStashId>0) stashArray=(int16_t*)calloc(maxStashId+1, sizeof(int16_t));
//TODO check EEPROM and fetch stashArray
}
DIAG(F("EXRAIL %db, fl=%d, stash=%d"),progCounter,MAX_FLAGS, maxStashId);
// Removed for 4.2.31 new RMFT2(0); // add the startup route // Removed for 4.2.31 new RMFT2(0); // add the startup route
diag=saved_diag; diag=saved_diag;
@ -936,6 +950,34 @@ void RMFT2::loop2() {
manageRouteState(operand,4); manageRouteState(operand,4);
break; break;
case OPCODE_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = invert? -loco : loco;
break;
case OPCODE_CLEAR_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = 0;
break;
case OPCODE_CLEAR_ALL_STASH:
if (compileFeatures & FEATURE_STASH)
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
break;
case OPCODE_PICKUP_STASH:
if (compileFeatures & FEATURE_STASH) {
int16_t x=stashArray[operand];
if (x>=0) {
loco=x;
invert=false;
break;
}
loco=-x;
invert=true;
}
break;
case OPCODE_ROUTE: case OPCODE_ROUTE:
case OPCODE_AUTOMATION: case OPCODE_AUTOMATION:
case OPCODE_SEQUENCE: case OPCODE_SEQUENCE:

View File

@ -70,6 +70,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_ONOVERLOAD, OPCODE_ONOVERLOAD,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN, OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED, OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
// OPcodes below this point are skip-nesting IF operations // OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group // placed here so that they may be skipped as a group
@ -102,6 +103,7 @@ enum thrunger: byte {
static const byte FEATURE_LCC = 0x40; static const byte FEATURE_LCC = 0x40;
static const byte FEATURE_ROSTER= 0x20; static const byte FEATURE_ROSTER= 0x20;
static const byte FEATURE_ROUTESTATE= 0x10; static const byte FEATURE_ROUTESTATE= 0x10;
static const byte FEATURE_STASH = 0x08;
// Flag bits for status of hardware and TPL // Flag bits for status of hardware and TPL
@ -203,7 +205,7 @@ private:
uint16_t getOperand(byte n); uint16_t getOperand(byte n);
static bool diag; static bool diag;
static const HIGHFLASH byte RouteCode[]; static const HIGHFLASH3 byte RouteCode[];
static const HIGHFLASH int16_t SignalDefinitions[]; static const HIGHFLASH int16_t SignalDefinitions[];
static byte flags[MAX_FLAGS]; static byte flags[MAX_FLAGS];
static Print * LCCSerial; static Print * LCCSerial;
@ -229,6 +231,8 @@ private:
static void manageRouteCaption(uint16_t id, const FSH* caption); static void manageRouteCaption(uint16_t id, const FSH* caption);
static byte * routeStateArray; static byte * routeStateArray;
static const FSH ** routeCaptionArray; static const FSH ** routeCaptionArray;
static int16_t * stashArray;
static int16_t maxStashId;
// Local variables - exist for each instance/task // Local variables - exist for each instance/task
RMFT2 *next; // loop chain RMFT2 *next; // loop chain

View File

@ -39,6 +39,8 @@
#undef AUTOSTART #undef AUTOSTART
#undef BROADCAST #undef BROADCAST
#undef CALL #undef CALL
#undef CLEAR_STASH
#undef CLEAR_ALL_STASH
#undef CLOSE #undef CLOSE
#undef DCC_SIGNAL #undef DCC_SIGNAL
#undef DCC_TURNTABLE #undef DCC_TURNTABLE
@ -108,6 +110,7 @@
#undef ONCHANGE #undef ONCHANGE
#undef PARSE #undef PARSE
#undef PAUSE #undef PAUSE
#undef PICKUP_STASH
#undef PIN_TURNOUT #undef PIN_TURNOUT
#undef PRINT #undef PRINT
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
@ -152,6 +155,7 @@
#undef SIGNALH #undef SIGNALH
#undef SPEED #undef SPEED
#undef START #undef START
#undef STASH
#undef STOP #undef STOP
#undef THROW #undef THROW
#undef TT_ADDPOSITION #undef TT_ADDPOSITION
@ -185,6 +189,8 @@
#define AUTOSTART #define AUTOSTART
#define BROADCAST(msg) #define BROADCAST(msg)
#define CALL(route) #define CALL(route)
#define CLEAR_STASH(id)
#define CLEAR_ALL_STASH(id)
#define CLOSE(id) #define CLOSE(id)
#define DCC_SIGNAL(id,add,subaddr) #define DCC_SIGNAL(id,add,subaddr)
#define DCC_TURNTABLE(id,home,description) #define DCC_TURNTABLE(id,home,description)
@ -256,6 +262,7 @@
#define PIN_TURNOUT(id,pin,description...) #define PIN_TURNOUT(id,pin,description...)
#define PRINT(msg) #define PRINT(msg)
#define PARSE(msg) #define PARSE(msg)
#define PICKUP_STASH(id)
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
#define POM(cv,value) #define POM(cv,value)
#endif #endif
@ -298,6 +305,7 @@
#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 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

@ -45,6 +45,8 @@ const int16_t HASH_KEYWORD_RED=26099;
const int16_t HASH_KEYWORD_AMBER=18713; const int16_t HASH_KEYWORD_AMBER=18713;
const int16_t HASH_KEYWORD_GREEN=-31493; const int16_t HASH_KEYWORD_GREEN=-31493;
const int16_t HASH_KEYWORD_A='A'; 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
@ -150,6 +152,32 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
return; return;
} }
break; break;
case HASH_KEYWORD_M:
// NOTE: we only need to handle valid calls here because
// DCCEXParser has to have code to handle the <J<> cases where
// exrail isnt involved anyway.
// This entire code block is compiled out if STASH macros not used
if (!(compileFeatures & FEATURE_STASH)) return;
if (paramCount==1) { // <JM>
StringFormatter::send(stream,F("<jM %d>\n"),maxStashId);
opcode=0;
break;
}
if (paramCount==2) { // <JM id>
if (p[1]<=0 || p[1]>maxStashId) break;
StringFormatter::send(stream,F("<jM %d %d>\n"),
p[1],stashArray[p[1]]);
opcode=0;
break;
}
if (paramCount==3) { // <JM id cab>
if (p[1]<=0 || p[1]>maxStashId) break;
stashArray[p[1]]=p[2];
opcode=0;
break;
}
break;
default: default:
break; break;
} }
@ -195,6 +223,15 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
sigid & SIGNAL_ID_MASK); sigid & SIGNAL_ID_MASK);
} }
} }
if (compileFeatures & FEATURE_STASH) {
for (int i=1;i<=maxStashId;i++) {
if (stashArray[i])
StringFormatter::send(stream,F("\nSTASH[%d] Loco=%d"),
i, stashArray[i]);
}
}
StringFormatter::send(stream,F(" *>\n")); StringFormatter::send(stream,F(" *>\n"));
return true; return true;
} }

View File

@ -113,6 +113,15 @@ void exrailHalSetup() {
#undef ROUTE_CAPTION #undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE #define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
#undef CLEAR_STASH
#define CLEAR_STASH(id) | FEATURE_STASH
#undef CLEAR_ALL_STASH
#define CLEAR_ALL_STASH | FEATURE_STASH
#undef PICKUP_STASH
#define PICKUP_STASH(id) | FEATURE_STASH
#undef STASH
#define STASH(id) | FEATURE_STASH
const byte RMFT2::compileFeatures = 0 const byte RMFT2::compileFeatures = 0
#include "myAutomation.h" #include "myAutomation.h"
; ;
@ -353,6 +362,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define AUTOSTART OPCODE_AUTOSTART,0,0, #define AUTOSTART OPCODE_AUTOSTART,0,0,
#define BROADCAST(msg) PRINT(msg) #define BROADCAST(msg) PRINT(msg)
#define CALL(route) OPCODE_CALL,V(route), #define CALL(route) OPCODE_CALL,V(route),
#define CLEAR_STASH(id) OPCODE_CLEAR_STASH,V(id),
#define CLEAR_ALL_STASH OPCODE_CLEAR_ALL_STASH,V(0),
#define CLOSE(id) OPCODE_CLOSE,V(id), #define CLOSE(id) OPCODE_CLOSE,V(id),
#ifndef IO_NO_HAL #ifndef IO_NO_HAL
#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home), #define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home),
@ -435,6 +446,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id), #define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id), #define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
#define PAUSE OPCODE_PAUSE,0,0, #define PAUSE OPCODE_PAUSE,0,0,
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin), #define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value), #define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
@ -482,6 +494,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define SIGNALH(redpin,amberpin,greenpin) #define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed), #define SPEED(speed) OPCODE_SPEED,V(speed),
#define START(route) OPCODE_START,V(route), #define START(route) OPCODE_START,V(route),
#define STASH(id) OPCODE_STASH,V(id),
#define STOP OPCODE_SPEED,V(0), #define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id), #define THROW(id) OPCODE_THROW,V(id),
#ifndef IO_NO_HAL #ifndef IO_NO_HAL
@ -503,7 +516,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
// Build RouteCode // Build RouteCode
const int StringMacroTracker2=__COUNTER__; const int StringMacroTracker2=__COUNTER__;
const HIGHFLASH byte RMFT2::RouteCode[] = { const HIGHFLASH3 byte RMFT2::RouteCode[] = {
#include "myAutomation.h" #include "myAutomation.h"
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 }; OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };

3
FSH.h
View File

@ -56,6 +56,7 @@ typedef __FlashStringHelper FSH;
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// AVR_MEGA memory deliberately placed at end of link may need _far functions // AVR_MEGA memory deliberately placed at end of link may need _far functions
#define HIGHFLASH __attribute__((section(".fini2"))) #define HIGHFLASH __attribute__((section(".fini2")))
#define HIGHFLASH3 __attribute__((section(".fini3")))
#define GETFARPTR(data) pgm_get_far_address(data) #define GETFARPTR(data) pgm_get_far_address(data)
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset) #define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset) #define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
@ -63,6 +64,7 @@ typedef __FlashStringHelper FSH;
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent // AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
// as there is no progmem above 32kb anyway. // as there is no progmem above 32kb anyway.
#define HIGHFLASH PROGMEM #define HIGHFLASH PROGMEM
#define HIGHFLASH3 PROGMEM
#define GETFARPTR(data) ((uint32_t)(data)) #define GETFARPTR(data) ((uint32_t)(data))
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset)) #define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset)) #define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
@ -80,6 +82,7 @@ typedef __FlashStringHelper FSH;
typedef char FSH; typedef char FSH;
#define FLASH #define FLASH
#define HIGHFLASH #define HIGHFLASH
#define HIGHFLASH3
#define GETFARPTR(data) ((uint32_t)(data)) #define GETFARPTR(data) ((uint32_t)(data))
#define GETFLASH(addr) (*(const byte *)(addr)) #define GETFLASH(addr) (*(const byte *)(addr))
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset)) #define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))

View File

@ -1 +1 @@
#define GITHUB_SHA "devel-202311200731Z" #define GITHUB_SHA "devel-202311232114Z"

View File

@ -156,8 +156,10 @@ class MotorDriver {
// from outside interrupt // from outside interrupt
void setBrake( bool on, bool interruptContext=false); void setBrake( bool on, bool interruptContext=false);
__attribute__((always_inline)) inline void setSignal( bool high) { __attribute__((always_inline)) inline void setSignal( bool high) {
#ifndef ARDUINO_ARCH_ESP32
if (invertPhase) if (invertPhase)
high = !high; high = !high;
#endif
if (trackPWM) { if (trackPWM) {
DCCTimer::setPWM(signalPin,high); DCCTimer::setPWM(signalPin,high);
} }

View File

@ -39,8 +39,11 @@ void StringFormatter::diag( const FSH* input...) {
void StringFormatter::lcd(byte row, const FSH* input...) { void StringFormatter::lcd(byte row, const FSH* input...) {
va_list args; va_list args;
#ifndef DISABLE_VDPY
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row); Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row);
#else
Print * virtualLCD=NULL;
#endif
// Issue the LCD as a diag first // Issue the LCD as a diag first
// Unless the same serial is asking for the virtual @ respomnse // Unless the same serial is asking for the virtual @ respomnse
if (virtualLCD!=&USB_SERIAL) { if (virtualLCD!=&USB_SERIAL) {
@ -50,13 +53,14 @@ void StringFormatter::lcd(byte row, const FSH* input...) {
send(&USB_SERIAL,F(" *>\n")); send(&USB_SERIAL,F(" *>\n"));
} }
#ifndef DISABLE_VDPY
// send to virtual LCD collector (if any) // send to virtual LCD collector (if any)
if (virtualLCD) { if (virtualLCD) {
va_start(args, input); va_start(args, input);
send2(virtualLCD,input,args); send2(virtualLCD,input,args);
CommandDistributor::commitVirtualLCDSerial(); CommandDistributor::commitVirtualLCDSerial();
} }
#endif
DisplayInterface::setRow(row); DisplayInterface::setRow(row);
va_start(args, input); va_start(args, input);
send2(DisplayInterface::getDisplayHandler(),input,args); send2(DisplayInterface::getDisplayHandler(),input,args);
@ -66,12 +70,14 @@ void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {
va_list args; va_list args;
// send to virtual LCD collector (if any) // send to virtual LCD collector (if any)
#ifndef DISABLE_VDPY
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row); Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row);
if (virtualLCD) { if (virtualLCD) {
va_start(args, input); va_start(args, input);
send2(virtualLCD,input,args); send2(virtualLCD,input,args);
CommandDistributor::commitVirtualLCDSerial(); CommandDistributor::commitVirtualLCDSerial();
} }
#endif
DisplayInterface::setRow(display, row); DisplayInterface::setRow(display, row);
va_start(args, input); va_start(args, input);
@ -250,4 +256,3 @@ void StringFormatter::printHex(Print * stream,uint16_t value) {
result[4]='\0'; result[4]='\0';
stream->print(result); stream->print(result);
} }

View File

@ -54,7 +54,6 @@ 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];
POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF;
byte TrackManager::lastTrack=0; byte TrackManager::lastTrack=0;
bool TrackManager::progTrackSyncMain=false; bool TrackManager::progTrackSyncMain=false;
bool TrackManager::progTrackBoosted=false; bool TrackManager::progTrackBoosted=false;
@ -210,6 +209,9 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) { bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false; if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
// Remember track mode we came from for later
TRACK_MODE oldmode = track[trackToSet]->getMode();
//DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode); //DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode);
// DC tracks require a motorDriver that can set brake! // DC tracks require a motorDriver that can set brake!
if (mode & TRACK_MODE_DC) { if (mode & TRACK_MODE_DC) {
@ -270,7 +272,6 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
} }
track[trackToSet]->setMode(mode); track[trackToSet]->setMode(mode);
trackDCAddr[trackToSet]=dcAddr; trackDCAddr[trackToSet]=dcAddr;
streamTrackState(NULL,trackToSet);
// When a track is switched, we must clear any side effects of its previous // When a track is switched, we must clear any side effects of its previous
// state, otherwise trains run away or just dont move. // state, otherwise trains run away or just dont move.
@ -337,10 +338,11 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
applyDCSpeed(trackToSet); applyDCSpeed(trackToSet);
} }
// Normal running tracks are set to the global power state // Turn off power if we changed the mode of this track
track[trackToSet]->setPower( if (mode != oldmode)
(mode & (TRACK_MODE_MAIN | TRACK_MODE_DC | TRACK_MODE_EXT | TRACK_MODE_BOOST)) ? track[trackToSet]->setPower(POWERMODE::OFF);
mainPowerGuess : POWERMODE::OFF); streamTrackState(NULL,trackToSet);
//DIAG(F("TrackMode=%d"),mode); //DIAG(F("TrackMode=%d"),mode);
return true; return true;
} }
@ -350,7 +352,7 @@ void TrackManager::applyDCSpeed(byte t) {
track[t]->setDCSignal(speedByte); track[t]->setDCSignal(speedByte);
} }
bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[]) bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
{ {
if (params==0) { // <=> List track assignments if (params==0) { // <=> List track assignments
@ -397,46 +399,60 @@ bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[])
return false; return false;
} }
void TrackManager::streamTrackState(Print* stream, byte t) { const FSH* TrackManager::getModeName(TRACK_MODE tm) {
// null stream means send to commandDistributor for broadcast const FSH *modename=F("---");
if (track[t]==NULL) return;
auto format=F("<= %d XXX>\n");
TRACK_MODE tm = track[t]->getMode();
if (tm & TRACK_MODE_MAIN) { if (tm & TRACK_MODE_MAIN) {
if(tm & TRACK_MODE_AUTOINV) if(tm & TRACK_MODE_AUTOINV)
format=F("<= %c MAIN A>\n"); modename=F("MAIN A");
else if (tm & TRACK_MODE_INV) else if (tm & TRACK_MODE_INV)
format=F("<= %c MAIN I>\n"); modename=F("MAIN I>\n");
else else
format=F("<= %c MAIN>\n"); modename=F("MAIN");
} }
#ifndef DISABLE_PROG #ifndef DISABLE_PROG
else if (tm & TRACK_MODE_PROG) else if (tm & TRACK_MODE_PROG)
format=F("<= %c PROG>\n"); modename=F("PROG");
#endif #endif
else if (tm & TRACK_MODE_NONE) else if (tm & TRACK_MODE_NONE)
format=F("<= %c NONE>\n"); modename=F("NONE");
else if(tm & TRACK_MODE_EXT) else if(tm & TRACK_MODE_EXT)
format=F("<= %c EXT>\n"); modename=F("EXT");
else if(tm & TRACK_MODE_BOOST) { else if(tm & TRACK_MODE_BOOST) {
if(tm & TRACK_MODE_AUTOINV) if(tm & TRACK_MODE_AUTOINV)
format=F("<= %c B A>\n"); modename=F("B A");
else if (tm & TRACK_MODE_INV) else if (tm & TRACK_MODE_INV)
format=F("<= %c B I>\n"); modename=F("B I");
else else
format=F("<= %c B>\n"); modename=F("B");
} }
else if (tm & TRACK_MODE_DC) { else if (tm & TRACK_MODE_DC) {
if (tm & TRACK_MODE_INV) if (tm & TRACK_MODE_INV)
format=F("<= %c DCX %d>\n"); modename=F("DCX");
else else
format=F("<= %c DC %d>\n"); modename=F("DC");
} }
return modename;
}
if (stream) // null stream means send to commandDistributor for broadcast
StringFormatter::send(stream,format,'A'+t, trackDCAddr[t]); void TrackManager::streamTrackState(Print* stream, byte t) {
const FSH *format;
if (track[t]==NULL) return;
TRACK_MODE tm = track[t]->getMode();
if (tm & TRACK_MODE_DC)
format=F("<= %c %S %d>\n");
else else
CommandDistributor::broadcastTrackState(format,'A'+t, trackDCAddr[t]); format=F("<= %c %S>\n");
const FSH *modename=getModeName(tm);
if (stream) { // null stream means send to commandDistributor for broadcast
StringFormatter::send(stream,format,'A'+t, modename, trackDCAddr[t]);
} else {
CommandDistributor::broadcastTrackState(format,'A'+t, modename, trackDCAddr[t]);
CommandDistributor::broadcastPower();
}
} }
@ -497,12 +513,15 @@ void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermod
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();
if (trackmode & TRACK_MODE_DC) { if (trackmode & TRACK_MODE_NONE) {
driver->setBrake(true); // Track is unused. Brake is good to have.
powermode = POWERMODE::OFF; // Track is unused. Force it to OFF
} else if (trackmode & TRACK_MODE_DC) { // includes inverted DC (called DCX)
if (powermode == POWERMODE::ON) { if (powermode == POWERMODE::ON) {
driver->setBrake(true); // DC starts with brake on driver->setBrake(true); // DC starts with brake on
applyDCSpeed(t); // speed match DCC throttles applyDCSpeed(t); // speed match DCC throttles
} }
} else { } else /* MAIN PROG EXT BOOST */ {
if (powermode == POWERMODE::ON) { if (powermode == POWERMODE::ON) {
// toggle brake before turning power on - resets overcurrent error // toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2. // on the Pololu board if brake is wired to ^D2.
@ -513,15 +532,6 @@ void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
driver->setPower(powermode); driver->setPower(powermode);
} }
void TrackManager::reportPowerChange(Print* stream, byte thistrack) {
// This function is for backward JMRI compatibility only
// It reports the first track only, as main, regardless of track settings.
// <c MeterName value C/V unit min max res warn>
int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"),
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
}
// returns state of the one and only prog track // returns state of the one and only prog track
POWERMODE TrackManager::getProgPower() { POWERMODE TrackManager::getProgPower() {
FOR_EACH_TRACK(t) FOR_EACH_TRACK(t)
@ -639,18 +649,3 @@ int16_t TrackManager::returnDCAddr(byte t) {
return (trackDCAddr[t]); return (trackDCAddr[t]);
} }
const char* TrackManager::getModeName(byte Mode) {
//DIAG(F("PowerMode %d"), Mode);
switch (Mode)
{
case 1: return "NONE";
case 2: return "MAIN";
case 4: return "PROG";
case 8: return "DC";
case 16: return "DCX";
case 32: return "EXT";
default: return "----";
}
}

View File

@ -73,25 +73,28 @@ class TrackManager {
static const int16_t MAX_TRACKS=8; static const int16_t MAX_TRACKS=8;
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0); static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
static bool parseJ(Print * stream, int16_t params, int16_t p[]); static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
static void loop(); static void loop();
static POWERMODE getMainPower(); static POWERMODE getMainPower();
static POWERMODE getProgPower(); static POWERMODE getProgPower();
static inline POWERMODE getPower(byte t) { return track[t]->getPower(); }
static bool getPower(byte t, char s[]); static bool getPower(byte t, char s[]);
static void setJoin(bool join); static void setJoin(bool join);
static bool isJoined() { return progTrackSyncMain;} static bool isJoined() { return progTrackSyncMain;}
static inline bool isActive (byte tr) {
if (tr > lastTrack) return false;
return track[tr]->getMode() & (TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT);}
static void setJoinRelayPin(byte joinRelayPin); static void setJoinRelayPin(byte joinRelayPin);
static void sampleCurrent(); static void sampleCurrent();
static void reportGauges(Print* stream); static void reportGauges(Print* stream);
static void reportCurrent(Print* stream); static void reportCurrent(Print* stream);
static void reportPowerChange(Print* stream, byte thistrack);
static void reportObsoleteCurrent(Print* stream); static void reportObsoleteCurrent(Print* stream);
static void streamTrackState(Print* stream, byte t); static void streamTrackState(Print* stream, byte t);
static bool isPowerOn(byte t); static bool isPowerOn(byte t);
static bool isProg(byte t); static bool isProg(byte t);
static byte returnMode(byte t); static byte returnMode(byte t);
static int16_t returnDCAddr(byte t); static int16_t returnDCAddr(byte t);
static const char* getModeName(byte Mode); static const FSH* getModeName(TRACK_MODE Mode);
static int16_t joinRelay; static int16_t joinRelay;
static bool progTrackSyncMain; // true when prog track is a siding switched to main static bool progTrackSyncMain; // true when prog track is a siding switched to main
@ -108,7 +111,6 @@ class TrackManager {
static void addTrack(byte t, MotorDriver* driver); static void addTrack(byte t, MotorDriver* driver);
static byte lastTrack; static byte lastTrack;
static byte nextCycleTrack; static byte nextCycleTrack;
static POWERMODE mainPowerGuess;
static void applyDCSpeed(byte t); static void applyDCSpeed(byte t);
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC

View File

@ -199,6 +199,18 @@ The configuration file for DCC-EX Command Station
// //
// #define DISABLE_PROG // #define DISABLE_PROG
/////////////////////////////////////////////////////////////////////////////////////
// DISABLE / ENABLE VDPY
//
// The Virtual display "VDPY" feature is by default enabled everywhere
// but on Uno and Nano. If you think you can fit it (for example
// having disabled some of the features above) you can enable it with
// ENABLE_VDPY. You can even disable it on all other CPUs with
// DISABLE_VDPY
//
// #define DISABLE_VDPY
// #define ENABLE_VDPY
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address // REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address
// is 127 and the first long address is 128. There are manufacturers which have // is 127 and the first long address is 128. There are manufacturers which have

View File

@ -219,11 +219,10 @@
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space. // The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
// //
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO) #if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
#if defined(DISABLE_DIAG) && defined(DISABLE_EEPROM) && defined(DISABLE_PROG) #define IO_NO_HAL // HAL too big whatever you disable otherwise
#warning you have sacrificed DIAG for HAL #ifndef ENABLE_VDPY
#else #define DISABLE_VDPY
#define IO_NO_HAL #endif
#endif
#endif #endif
#if __has_include ( "myAutomation.h") #if __has_include ( "myAutomation.h")

View File

@ -3,7 +3,15 @@
#include "StringFormatter.h" #include "StringFormatter.h"
#define VERSION "5.2.5" #define VERSION "5.2.9"
// 5.2.9 - Bugfix LCD startup with no LCD, uses <@
// 5.2.9 - EXRAIL STASH feature
// 5.2.8 - Bugfix: Do not turn off all tracks on change
// give better power messages
// 5.2.7 - Bugfix: EXRAIL ling segment
// - Bugfix: Back out wrongly added const
// - Bugfix ESP32: Do not inverse DCX direction signal twice
// 5.2.6 - Trackmanager broadcast power state on track mode change
// 5.2.5 - Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC // 5.2.5 - Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC
// 5.2.4 - LCD macro will not do diag if that duplicates @ to same target. // 5.2.4 - LCD macro will not do diag if that duplicates @ to same target.
// - Added ROUTE_DISABLED macro in EXRAIL // - Added ROUTE_DISABLED macro in EXRAIL
@ -187,9 +195,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: