diff --git a/.gitignore b/.gitignore index 6237359..759b739 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ myFilter.cpp my*.h !my*.example.h compile_commands.json +newcode.txt.old +UserAddin.txt diff --git a/CommandDistributor.cpp b/CommandDistributor.cpp index ab6b52f..6770e7a 100644 --- a/CommandDistributor.cpp +++ b/CommandDistributor.cpp @@ -105,6 +105,7 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream void CommandDistributor::forget(byte clientId) { if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId); clients[clientId]=NONE_TYPE; + if (virtualLCDClient==clientId) virtualLCDClient=RingStream::NO_CLIENT; } #endif @@ -161,6 +162,10 @@ void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) { #endif } +void CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) { + broadcastReply(COMMAND_TYPE, F("\n"), id, position, moving); +} + void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) { // The JMRI clock command is of the form : PFT65871<;>4 // The CS broadcast is of the form "\n"),pstr); + + byte trackcount=0; + byte oncount=0; + byte offcount=0; + for(byte t=0; t\n"),state,reason); + if (join) { + reason = F("JOIN"); + broadcastReply(COMMAND_TYPE, F("\n"),reason); + } else { + if (main) { + //reason = F("MAIN"); + broadcastReply(COMMAND_TYPE, F("\n")); + } + if (prog) { + //reason = F("PROG"); + broadcastReply(COMMAND_TYPE, F("\n")); + } + } + + if (state != '2') + broadcastReply(COMMAND_TYPE, F("\n"),state); #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 - 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) { broadcastReply(type, F("%s"),msg); } -void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) { - broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr); +void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) { + broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr); } + +void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) { + broadcastReply(COMMAND_TYPE, F("\n"),routeId,state); +} + +void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) { + broadcastReply(COMMAND_TYPE, F("\n"),routeId,caption); +} + +Print * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) { + Print * stream=virtualLCDSerial; + #ifdef CD_HANDLE_RING + rememberVLCDClient=RingStream::NO_CLIENT; + if (!stream && virtualLCDClient!=RingStream::NO_CLIENT) { + // If we are broadcasting from a wifi/eth process we need to complete its output + // before merging broadcasts in the ring, then reinstate it in case + // the process continues to output to its client. + if ((rememberVLCDClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) { + ring->commit(); + } + ring->mark(virtualLCDClient); + stream=ring; + } + #endif + if (stream) StringFormatter::send(stream,F("<@ %d %d \""), screen,row); + return stream; +} + +void CommandDistributor::commitVirtualLCDSerial() { + #ifdef CD_HANDLE_RING + if (virtualLCDClient!=RingStream::NO_CLIENT) { + StringFormatter::send(ring,F("\">\n")); + ring->commit(); + if (rememberVLCDClient!=RingStream::NO_CLIENT) ring->mark(rememberVLCDClient); + return; + } + #endif + StringFormatter::send(virtualLCDSerial,F("\">\n")); +} + +void CommandDistributor::setVirtualLCDSerial(Print * stream) { + #ifdef CD_HANDLE_RING + virtualLCDClient=RingStream::NO_CLIENT; + if (stream && stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM) { + virtualLCDClient=((RingStream *) stream)->peekTargetMark(); + virtualLCDSerial=nullptr; + return; + } + #endif + virtualLCDSerial=stream; +} + +Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL; +byte CommandDistributor::virtualLCDClient=0xFF; +byte CommandDistributor::rememberVLCDClient=0; + diff --git a/CommandDistributor.h b/CommandDistributor.h index 45f8147..e4dff5d 100644 --- a/CommandDistributor.h +++ b/CommandDistributor.h @@ -49,15 +49,26 @@ public : static void broadcastLoco(byte slot); static void broadcastSensor(int16_t id, bool value); static void broadcastTurnout(int16_t id, bool isClosed); + static void broadcastTurntable(int16_t id, uint8_t position, bool moving); static void broadcastClockTime(int16_t time, int8_t rate); static void setClockTime(int16_t time, int8_t rate, byte opt); static int16_t retClockTime(); static void broadcastPower(); 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 static void broadcastReply(clientType type, Targs... msg); static void forget(byte clientId); + static void broadcastRouteState(uint16_t routeId,byte state); + static void broadcastRouteCaption(uint16_t routeId,const FSH * caption); + // Handling code for virtual LCD receiver. + static Print * getVirtualLCDSerial(byte screen, byte row); + static void commitVirtualLCDSerial(); + static void setVirtualLCDSerial(Print * stream); + private: + static Print * virtualLCDSerial; + static byte virtualLCDClient; + static byte rememberVLCDClient; }; #endif diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index 77e8f40..205babf 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -87,7 +87,7 @@ void setup() DISPLAY_START ( // 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")); ); diff --git a/DCC.cpp b/DCC.cpp index 30fcf5f..60c07df 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -595,7 +595,7 @@ void DCC::loop() { void DCC::issueReminders() { // if the main track transmitter still has a pending packet, skip this time around. - if ( DCCWaveform::mainTrack.getPacketPending()) return; + if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return; // Move to next loco slot. If occupied, send a reminder. int reg = lastLocoReminder+1; if (reg > highestUsedReg) reg = 0; // Go to start of table @@ -620,14 +620,23 @@ bool DCC::issueReminder(int reg) { case 1: // remind function group 1 (F0-F4) if (flags & FN_GROUP_1) setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD +#ifdef DISABLE_FUNCTION_REMINDERS + flags&= ~FN_GROUP_1; // dont send them again +#endif break; case 2: // remind function group 2 F5-F8 if (flags & FN_GROUP_2) setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD +#ifdef DISABLE_FUNCTION_REMINDERS + flags&= ~FN_GROUP_2; // dont send them again +#endif break; case 3: // remind function group 3 F9-F12 if (flags & FN_GROUP_3) setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD +#ifdef DISABLE_FUNCTION_REMINDERS + flags&= ~FN_GROUP_3; // dont send them again +#endif break; case 4: // remind function group 4 F13-F20 if (flags & FN_GROUP_4) diff --git a/DCCACK.cpp b/DCCACK.cpp index 1b339ee..8a074b4 100644 --- a/DCCACK.cpp +++ b/DCCACK.cpp @@ -351,7 +351,7 @@ void DCCACK::callback(int value) { switch (callbackState) { case AFTER_READ: - if (ackManagerRejoin && autoPowerOff) { + if (ackManagerRejoin && !autoPowerOff) { progDriver->setPower(POWERMODE::OFF); callbackStart=millis(); callbackState=WAITING_30; diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index aaf733c..f3cdfb2 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -25,6 +25,79 @@ * You should have received a copy of the GNU General Public License * along with CommandStation. If not, see . */ + +/* +List of single character OPCODEs in use for reference. + +When determining a new OPCODE for a new feature, refer to this list as the source of truth. + +Once a new OPCODE is decided upon, update this list. + + Character, Usage + /, |EX-R| interactive commands + -, Remove from reminder table + =, |TM| configuration + !, Emergency stop + @, Reserved for future use - LCD messages to JMRI + #, Request number of supported cabs/locos; heartbeat + +, WiFi AT commands + ?, Reserved for future use + 0, Track power off + 1, Track power on + a, DCC accessory control + A, + b, Write CV bit on main + B, Write CV bit + c, Request current command + C, configure the CS + d, + D, Diagnostic commands + e, Erase EEPROM + E, Store configuration in EEPROM + f, Loco decoder function control (deprecated) + F, Loco decoder function control + g, + G, + h, + H, Turnout state broadcast + i, Server details string + I, Turntable object command, control, and broadcast + j, Throttle responses + J, Throttle queries + k, Reserved for future use - Potentially Railcom + K, Reserved for future use - Potentially Railcom + l, Loco speedbyte/function map broadcast + L, Reserved for LCC interface (implemented in EXRAIL) + m, + M, Write DCC packet + n, + N, + o, + O, Output broadcast + p, Broadcast power state + P, Write DCC packet + q, Sensor deactivated + Q, Sensor activated + r, Broadcast address read on programming track + R, Read CVs + s, Display status + S, Sensor configuration + t, Cab/loco update command + T, Turnout configuration/control + u, Reserved for user commands + U, Reserved for user commands + v, + V, Verify CVs + w, Write CV on main + W, Write CV + x, + X, Invalid command + y, + Y, Output broadcast + z, + Z, Output configuration/control +*/ + #include "StringFormatter.h" #include "DCCEXParser.h" #include "DCC.h" @@ -41,6 +114,8 @@ #include "TrackManager.h" #include "DCCTimer.h" #include "EXRAIL2.h" +#include "Turntables.h" +#include "version.h" // This macro can't be created easily as a portable function because the // flashlist requires a far pointer for high flash access. @@ -48,7 +123,7 @@ for (int16_t i=0;;i+=sizeof(flashList[0])) { \ int16_t value=GETHIGHFLASHW(flashList,i); \ if (value==INT16_MAX) break; \ - if (value != 0) StringFormatter::send(stream,F(" %d"),value); \ + StringFormatter::send(stream,F(" %d"),value); \ } @@ -83,7 +158,11 @@ 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'; @@ -95,6 +174,8 @@ 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]; bool DCCEXParser::stashBusy; @@ -131,8 +212,10 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte case 1: // skipping spaces before a param if (hot == ' ') break; - if (hot == '\0' || hot == '>') - return parameterCount; + if (hot == '\0') + return -1; + if (hot == '>') + return parameterCount; state = 2; continue; @@ -225,14 +308,19 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) #ifndef DISABLE_EEPROM (void)EEPROM; // tell compiler not to warn this is unused #endif + byte params = 0; if (Diag::CMD) DIAG(F("PARSING:%s"), com); int16_t p[MAX_COMMAND_PARAMS]; while (com[0] == '<' || com[0] == ' ') com++; // strip off any number of < or spaces byte opcode = com[0]; - byte params = splitValues(p, com, opcode=='M' || opcode=='P'); - + int16_t splitnum = splitValues(p, com, opcode=='M' || opcode=='P'); + if (splitnum < 0 || splitnum >= MAX_COMMAND_PARAMS) // if arguments are broken, leave but via printing + goto out; + // Because of check above we are now inside byte size + params = splitnum; + if (filterCallback) filterCallback(stream, opcode, params, p); if (filterRMFTCallback && opcode!='\0') @@ -378,12 +466,16 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) #ifndef DISABLE_PROG case 'w': // WRITE CV on MAIN - DCC::writeCVByteMain(p[0], p[1], p[2]); - return; + if (params != 3) + break; + DCC::writeCVByteMain(p[0], p[1], p[2]); + return; case 'b': // WRITE CV BIT ON MAIN - DCC::writeCVBitMain(p[0], p[1], p[2], p[3]); - return; + if (params != 4) + break; + DCC::writeCVBitMain(p[0], p[1], p[2], p[3]); + return; #endif case 'M': // WRITE TRANSPARENT DCC PACKET MAIN @@ -406,14 +498,16 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) #ifndef DISABLE_PROG case 'W': // WRITE CV ON PROG - if (!stashCallback(stream, p, ringStream)) - break; + if (!stashCallback(stream, p, ringStream)) + break; if (params == 1) // Write new loco id (clearing consist and managing short/long) DCC::setLocoId(p[0],callback_Wloco); else if (params == 4) // WRITE CV ON PROG DCC::writeCVByte(p[0], p[1], callback_W4); - else // WRITE CV ON PROG + else if (params == 2) // WRITE CV ON PROG DCC::writeCVByte(p[0], p[1], callback_W); + else + break; return; case 'V': // VERIFY CV ON PROG @@ -433,9 +527,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) } break; - case 'B': // WRITE CV BIT ON PROG + case 'B': // WRITE CV BIT ON PROG or + if (params != 3 && params != 5) + break; if (!stashCallback(stream, p, ringStream)) - break; + break; DCC::writeCVBit(p[0], p[1], p[2], callback_B); return; @@ -466,70 +562,67 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) case '1': // POWERON <1 [MAIN|PROG|JOIN]> { - bool main=false; - bool prog=false; - bool join=false; - if (params > 1) break; - if (params==0) { // All - main=true; - prog=true; - } - if (params==1) { - if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN> - main=true; + if (params > 1) break; + if (params==0) { // All + TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON); } + if (params==1) { + if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN> + TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON); + } #ifndef DISABLE_PROG - else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN> - main=true; - prog=true; - join=true; - } - else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG> - prog=true; - } + else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN> + TrackManager::setJoin(true); + TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON); + } + else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG> + TrackManager::setJoin(false); + TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON); + } #endif - else break; // will reply + else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> + byte t = (p[0] - 'A'); + TrackManager::setTrackPower(POWERMODE::ON, t); + //StringFormatter::send(stream, F("\n"), t+'A'); + } + else break; // will reply + } + CommandDistributor::broadcastPower(); + //TrackManager::streamTrackState(NULL,t); + + return; } - TrackManager::setJoin(join); - if (main) TrackManager::setMainPower(POWERMODE::ON); - if (prog) TrackManager::setProgPower(POWERMODE::ON); - - CommandDistributor::broadcastPower(); - return; - } - + case '0': // POWEROFF <0 [MAIN | PROG] > { - bool main=false; - bool prog=false; - if (params > 1) break; - if (params==0) { // All - main=true; - prog=true; - } - if (params==1) { - if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN> - main=true; + if (params > 1) break; + if (params==0) { // All + TrackManager::setJoin(false); + TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF); } + if (params==1) { + if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN> + TrackManager::setJoin(false); + TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF); + } #ifndef DISABLE_PROG - else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG> - prog=true; - } + else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG> + TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off + TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF); + } #endif - else break; // will reply + else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> + byte t = (p[0] - 'A'); + TrackManager::setJoin(false); + TrackManager::setTrackPower(POWERMODE::OFF, t); + //StringFormatter::send(stream, F("\n"), t+'A'); + } + else break; // will reply + } + CommandDistributor::broadcastPower(); + return; } - TrackManager::setJoin(false); - if (main) TrackManager::setMainPower(POWERMODE::OFF); - if (prog) { - TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off - TrackManager::setProgPower(POWERMODE::OFF); - } - - CommandDistributor::broadcastPower(); - return; - } - case '!': // ESTOP ALL DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1. return; @@ -544,7 +637,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) Sensor::printAll(stream); return; - case 's': // + case 's': // STATUS StringFormatter::send(stream, F("\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA)); CommandDistributor::broadcastPower(); // is the only "get power status" command we have Turnout::printAll(stream); //send all Turnout states @@ -565,14 +658,18 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) case ' ': // < > StringFormatter::send(stream, F("\n")); return; - - case 'D': // < > + case 'C': // CONFIG + if (parseC(stream, params, p)) + return; + break; +#ifndef DISABLE_DIAG + case 'D': // DIAG if (parseD(stream, params, p)) return; - return; - - case '=': // <= Track manager control > - if (TrackManager::parseJ(stream, params, p)) + break; +#endif + case '=': // TRACK MANAGER CONTROL <= [params]> + if (TrackManager::parseEqualSign(stream, params, p)) return; break; @@ -628,27 +725,17 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) TrackManager::reportCurrent(stream); // return; - case HASH_KEYWORD_A: // returns automations/routes - StringFormatter::send(stream, F(" -#ifdef EXRAIL_ACTIVE - SENDFLASHLIST(stream,RMFT2::routeIdList) - SENDFLASHLIST(stream,RMFT2::automationIdList) -#endif - } - else { // - StringFormatter::send(stream,F(" %d %c \"%S\""), - id, -#ifdef EXRAIL_ACTIVE - RMFT2::getRouteType(id), // A/R - RMFT2::getRouteDescription(id) -#else - 'X',F("") -#endif - ); - } - StringFormatter::send(stream, F(">\n")); - return; + case HASH_KEYWORD_A: // intercepted by EXRAIL// returns automations/routes + if (params!=1) break; // + StringFormatter::send(stream, F("\n")); + return; + + case HASH_KEYWORD_M: // intercepted by EXRAIL + if (params>1) break; // invalid cant do + // requests stash size so say none. + StringFormatter::send(stream,F("\n")); + return; + case HASH_KEYWORD_R: // returns rosters StringFormatter::send(stream, F("\n")); return; @@ -689,20 +780,94 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) } StringFormatter::send(stream, F(">\n")); return; +// No turntables without HAL support +#ifndef IO_NO_HAL + case HASH_KEYWORD_O: // + for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) { + if (tto->isHidden()) continue; + StringFormatter::send(stream, F(" %d"),tto->getId()); + } + StringFormatter::send(stream, F(">\n")); + } else { // + Turntable *tto=Turntable::get(id); + if (!tto || tto->isHidden()) { + StringFormatter::send(stream, F(" %d X>\n"), id); + } else { + uint8_t pos = tto->getPosition(); + uint8_t type = tto->isEXTT(); + uint8_t posCount = tto->getPositionCount(); + const FSH *todesc = NULL; +#ifdef EXRAIL_ACTIVE + todesc = RMFT2::getTurntableDescription(id); +#endif + if (todesc == NULL) todesc = F(""); + StringFormatter::send(stream, F(" %d %d %d %d \"%S\">\n"), id, type, pos, posCount, todesc); + } + } + return; + case HASH_KEYWORD_P: // returns turntable position list for the turntable id + if (params==2) { // + Turntable *tto=Turntable::get(id); + if (!tto || tto->isHidden()) { + StringFormatter::send(stream, F(" %d X>\n"), id); + } else { + uint8_t posCount = tto->getPositionCount(); + const FSH *tpdesc = NULL; + for (uint8_t p = 0; p < posCount; p++) { + StringFormatter::send(stream, F("getPositionAngle(p); +#ifdef EXRAIL_ACTIVE + tpdesc = RMFT2::getTurntablePositionDescription(id, p); +#endif + if (tpdesc == NULL) tpdesc = F(""); + StringFormatter::send(stream, F(" %d %d %d \"%S\""), id, p, angle, tpdesc); + StringFormatter::send(stream, F(">\n")); + } + } + } else { + StringFormatter::send(stream, F("\n")); + } + return; +#endif default: break; } // switch(p[1]) break; // case J } +// No turntables without HAL support +#ifndef IO_NO_HAL + case 'I': // TURNTABLE + if (parseI(stream, params, p)) + return; + break; +#endif + + case 'L': // LCC interface implemented in EXRAIL parser + break; // Will if not intercepted by EXRAIL + +#ifndef DISABLE_VDPY + case '@': // JMRI saying "give me virtual LCD msgs" + CommandDistributor::setVirtualLCDSerial(stream); + StringFormatter::send(stream, + F("<@ 0 0 \"DCC-EX v" VERSION "\">\n" + "<@ 0 1 \"Lic GPLv3\">\n")); + return; +#endif default: //anything else will diagnose and drop out to + if (opcode >= ' ' && opcode <= '~') { DIAG(F("Opcode=%c params=%d"), opcode, params); for (int i = 0; i < params; i++) DIAG(F("p[%d]=%d (0x%x)"), i, p[i], p[i]); - break; + } else { + DIAG(F("Unprintable %x"), opcode); + } + break; } // end of opcode switch - // Any fallout here sends an +out:// Any fallout here sends an StringFormatter::send(stream, F("\n")); } @@ -899,20 +1064,29 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[]) return false; } -bool DCCEXParser::parseD(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) return false; - bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off switch (p[0]) { - case HASH_KEYWORD_CABS: // - DCC::displayCabList(stream); +#ifndef DISABLE_PROG + case HASH_KEYWORD_PROGBOOST: + TrackManager::progTrackBoosted=true; + return true; +#endif + case HASH_KEYWORD_RESET: + DCCTimer::reset(); + break; // and if we didnt restart + case HASH_KEYWORD_SPEED28: + DCC::setGlobalSpeedsteps(28); + DIAG(F("28 Speedsteps")); return true; - case HASH_KEYWORD_RAM: // - StringFormatter::send(stream, F("Free memory=%d\n"), DCCTimer::getMinimumFreeMemory()); - break; + case HASH_KEYWORD_SPEED128: + DCC::setGlobalSpeedsteps(128); + DIAG(F("128 Speedsteps")); + return true; #ifndef DISABLE_PROG case HASH_KEYWORD_ACK: // @@ -931,12 +1105,35 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // } } else { - StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off")); + bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off + + DIAG(F("Ack diag %S"), onOff ? F("on") : F("off")); Diag::ACK = onOff; } return true; #endif +default: // invalid/unknown + break; + } + return false; +} + +bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) +{ + if (params == 0) + return false; + bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off + switch (p[0]) + { + case HASH_KEYWORD_CABS: // + DCC::displayCabList(stream); + return true; + + case HASH_KEYWORD_RAM: // + DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory()); + return true; + case HASH_KEYWORD_CMD: // Diag::CMD = onOff; return true; @@ -958,34 +1155,14 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) Diag::LCN = onOff; return true; #endif -#ifndef DISABLE_PROG - case HASH_KEYWORD_PROGBOOST: - TrackManager::progTrackBoosted=true; - return true; -#endif - case HASH_KEYWORD_RESET: - DCCTimer::reset(); - break; // and if we didnt restart - - #ifndef DISABLE_EEPROM case HASH_KEYWORD_EEPROM: // if (params >= 2) EEStore::dump(p[1]); return true; #endif - - case HASH_KEYWORD_SPEED28: - DCC::setGlobalSpeedsteps(28); - StringFormatter::send(stream, F("28 Speedsteps")); - return true; - - case HASH_KEYWORD_SPEED128: - DCC::setGlobalSpeedsteps(128); - StringFormatter::send(stream, F("128 Speedsteps")); - return true; - case HASH_KEYWORD_SERVO: // + case HASH_KEYWORD_ANOUT: // IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); break; @@ -1008,11 +1185,104 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) break; default: // invalid/unknown - break; + return parseC(stream, params, p); } return false; } +// ========================== +// Turntable - no support if no HAL +// - list all +// - broadcast type and current position +// - create DCC - This is TBA +// - operate (DCC) +// - operate (EXTT) +// - add position +// - create EXTT +#ifndef IO_NO_HAL +bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[]) +{ + switch (params) + { + case 0: // list turntable objects + return Turntable::printAll(stream); + + case 1: // broadcast type and current position + { + Turntable *tto = Turntable::get(p[0]); + if (tto) { + bool type = tto->isEXTT(); + uint8_t position = tto->getPosition(); + StringFormatter::send(stream, F("\n"), type, position); + } else { + return false; + } + } + return true; + + case 2: // - rotate a DCC turntable + { + Turntable *tto = Turntable::get(p[0]); + if (tto && !tto->isEXTT()) { + if (!tto->setPosition(p[0], p[1])) return false; + } else { + return false; + } + } + return true; + + case 3: // | - rotate to position for EX-Turntable or create DCC turntable + { + Turntable *tto = Turntable::get(p[0]); + if (p[1] == HASH_KEYWORD_DCC) { + if (tto || p[2] < 0 || p[2] > 3600) return false; + if (!DCCTurntable::create(p[0])) return false; + Turntable *tto = Turntable::get(p[0]); + tto->addPosition(0, 0, p[2]); + StringFormatter::send(stream, F("\n")); + } else { + if (!tto) return false; + if (!tto->isEXTT()) return false; + if (!tto->setPosition(p[0], p[1], p[2])) return false; + } + } + return true; + + case 4: // create an EXTT turntable + { + Turntable *tto = Turntable::get(p[0]); + if (p[1] == HASH_KEYWORD_EXTT) { + if (tto || p[3] < 0 || p[3] > 3600) return false; + if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false; + Turntable *tto = Turntable::get(p[0]); + tto->addPosition(0, 0, p[3]); + StringFormatter::send(stream, F("\n")); + } else { + return false; + } + } + return true; + + case 5: // add a position + { + Turntable *tto = Turntable::get(p[0]); + if (p[1] == HASH_KEYWORD_ADD) { + // tto must exist, no more than 48 positions, angle 0 - 3600 + if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false; + tto->addPosition(p[2], p[3], p[4]); + StringFormatter::send(stream, F("\n")); + } else { + return false; + } + } + return true; + + default: // Anything else is invalid + return false; + } +} +#endif + // CALLBACKS must be static bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream) { diff --git a/DCCEXParser.h b/DCCEXParser.h index bb05ebf..3c3382c 100644 --- a/DCCEXParser.h +++ b/DCCEXParser.h @@ -24,6 +24,7 @@ #include #include "FSH.h" #include "RingStream.h" +#include "defines.h" typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]); typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command); @@ -45,13 +46,17 @@ struct DCCEXParser static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex); static bool parseT(Print * stream, int16_t params, int16_t p[]); - static bool parseZ(Print * stream, int16_t params, int16_t p[]); - static bool parseS(Print * stream, int16_t params, int16_t p[]); - static bool parsef(Print * stream, int16_t params, int16_t p[]); - static bool parseD(Print * stream, int16_t params, int16_t p[]); + static bool parseZ(Print * stream, int16_t params, int16_t p[]); + static bool parseS(Print * stream, int16_t params, int16_t p[]); + static bool parsef(Print * stream, int16_t params, int16_t p[]); + static bool parseC(Print * stream, int16_t params, int16_t p[]); + static bool parseD(Print * stream, int16_t params, int16_t p[]); +#ifndef IO_NO_HAL + static bool parseI(Print * stream, int16_t params, int16_t p[]); +#endif - static Print * getAsyncReplyStream(); - static void commitAsyncReplyStream(); + static Print * getAsyncReplyStream(); + static void commitAsyncReplyStream(); static bool stashBusy; static byte stashTarget; diff --git a/DCCTimer.h b/DCCTimer.h index 7402f16..3b14fd6 100644 --- a/DCCTimer.h +++ b/DCCTimer.h @@ -125,8 +125,13 @@ private: // On platforms that scan, it is called from waveform ISR // only on a regular basis. static void scan(); + #if defined (ARDUINO_ARCH_STM32) + // bit array of used pins (max 32) + static uint32_t usedpins; +#else // bit array of used pins (max 16) static uint16_t usedpins; +#endif static uint8_t highestPin; // cached analog values (malloc:ed to actual number of ADC channels) static int *analogvals; diff --git a/DCCTimerSTM32.cpp b/DCCTimerSTM32.cpp index 7e121be..ca02a41 100644 --- a/DCCTimerSTM32.cpp +++ b/DCCTimerSTM32.cpp @@ -1,6 +1,6 @@ /* * © 2023 Neil McKechnie - * © 2022-23 Paul M. Antoine + * © 2022-2023 Paul M. Antoine * © 2021 Mike S * © 2021, 2023 Harald Barth * © 2021 Fred Decker @@ -50,12 +50,16 @@ HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 // via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc. // On the F446RE, Serial3 and Serial5 are easy to use: HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE -HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE +HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 - F446RE // On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins -#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)|| defined(ARDUINO_NUCLEO_F412ZG) +#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) || \ + defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) // Nucleo-144 boards don't have Serial1 defined by default HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6 -HardwareSerial Serial5(PE7, PE8); // Rx=PE7, Tx=PE8 -- USART5 +HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART2 +#if !defined(ARDUINO_NUCLEO_F412ZG) // F412ZG does not have UART5 + HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 +#endif // Serial3 is defined to use USART3 by default, but is in fact used as the diag console // via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc. #else @@ -155,13 +159,28 @@ HardwareSerial Serial5(PE7, PE8); // Rx=PE7, Tx=PE8 -- USART5 /////////////////////////////////////////////////////////////////////////////////////////////// INTERRUPT_CALLBACK interruptHandler=0; -// Let's use STM32's timer #11 until disabused of this notion -// Timer #11 is used for "servo" library, but as DCC-EX is not using -// this libary, we should be free and clear. -HardwareTimer timer(TIM11); + +// On STM32F4xx models that have them, Timers 6 and 7 have no PWM output capability, +// so are good choices for general timer duties - they are used for tone and servo +// in stm32duino so we shall usurp those as DCC-EX doesn't use tone or servo libs. +// NB: the F401, F410 and F411 do **not** have Timer 6 or 7, so we use Timer 11 +#ifndef DCC_EX_TIMER +#if defined(TIM6) +#define DCC_EX_TIMER TIM6 +#elif defined(TIM7) +#define DCC_EX_TIMER TIM7 +#elif defined(TIM11) +#define DCC_EX_TIMER TIM11 +#else +#warning This STM32F4XX variant does not have Timers 6,7 or 11!! +#endif +#endif // ifndef DCC_EX_TIMER + +HardwareTimer dcctimer(DCC_EX_TIMER); +void DCCTimer_Handler() __attribute__((interrupt)); // Timer IRQ handler -void Timer11_Handler() { +void DCCTimer_Handler() { interruptHandler(); } @@ -169,22 +188,24 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) { interruptHandler=callback; noInterrupts(); - // adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES); - timer.pause(); - timer.setPrescaleFactor(1); + dcctimer.pause(); + dcctimer.setPrescaleFactor(1); // timer.setOverflow(CLOCK_CYCLES * 2); - timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); - timer.attachInterrupt(Timer11_Handler); - timer.refresh(); - timer.resume(); + dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT); + // dcctimer.attachInterrupt(Timer11_Handler); + dcctimer.attachInterrupt(DCCTimer_Handler); + dcctimer.setInterruptPriority(0, 0); // Set highest preemptive priority! + dcctimer.refresh(); + dcctimer.resume(); interrupts(); } bool DCCTimer::isPWMPin(byte pin) { - //TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM, + //TODO: STM32 whilst this call to digitalPinHasPWM will reveal which pins can do PWM, // there's no support yet for High Accuracy, so for now return false // return digitalPinHasPWM(pin); + (void) pin; return false; } @@ -199,9 +220,9 @@ void DCCTimer::clearPWM() { } void DCCTimer::getSimulatedMacAddress(byte mac[6]) { - volatile uint32_t *serno1 = (volatile uint32_t *)0x1FFF7A10; - volatile uint32_t *serno2 = (volatile uint32_t *)0x1FFF7A14; - // volatile uint32_t *serno3 = (volatile uint32_t *)0x1FFF7A18; + volatile uint32_t *serno1 = (volatile uint32_t *)UID_BASE; + volatile uint32_t *serno2 = (volatile uint32_t *)UID_BASE+4; + // volatile uint32_t *serno3 = (volatile uint32_t *)UID_BASE+8; volatile uint32_t m1 = *serno1; volatile uint32_t m2 = *serno2; @@ -236,22 +257,91 @@ void DCCTimer::reset() { while(true) {}; } -// TODO: may need to use uint32_t on STMF4xx variants with > 16 analog inputs! -#if defined(ARDUINO_NUCLEO_F446RE) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) -#warning STM32 board selected not fully supported - only use ADC1 inputs 0-15 for current sensing! -#endif -// For now, define the max of 16 ports - some variants have more, but this not **yet** supported -#define NUM_ADC_INPUTS 16 -// #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS +// TODO: rationalise the size of these... could really use sparse arrays etc. +static HardwareTimer * pin_timer[100] = {0}; +static uint32_t channel_frequency[100] = {0}; +static uint32_t pin_channel[100] = {0}; -uint16_t ADCee::usedpins = 0; -uint8_t ADCee::highestPin = 0; -int * ADCee::analogvals = NULL; -uint32_t * analogchans = NULL; -bool adc1configured = false; +// Using the HardwareTimer library API included in stm32duino core to handle PWM duties +// TODO: in order to use the HA code above which Neil kindly wrote, we may have to do something more +// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones +// currently used for HA so they don't interfere with one another. For now we'll just make PWM +// work well... then work backwards to integrate with HA mode if we can. +void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) +{ + if (pin_timer[pin] == NULL) { + // Automatically retrieve TIM instance and channel associated to pin + // This is used to be compatible with all STM32 series automatically. + TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM); + if (Instance == NULL) { + // We shouldn't get here (famous last words) as it ought to have been caught by brakeCanPWM()! + DIAG(F("DCCEXanalogWriteFrequency::Pin %d has no PWM function!"), pin); + return; + } + pin_channel[pin] = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(pin), PinMap_PWM)); -int16_t ADCee::ADCmax() { - return 4095; + // Instantiate HardwareTimer object. Thanks to 'new' instantiation, + // HardwareTimer is not destructed when setup function is finished. + pin_timer[pin] = new HardwareTimer(Instance); + // Configure and start PWM + // MyTim->setPWM(channel, pin, 5, 10, NULL, NULL); // No callback required, we can simplify the function call + if (pin_timer[pin] != NULL) + { + pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle + DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d, frequency %d"), pin, pin_channel[pin], frequency); + } + else + DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!")); + } + else + { + // Frequency change request + if (frequency != channel_frequency[pin]) + { + pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured! + pin_timer[pin]->setOverflow(frequency, HERTZ_FORMAT); // Just change the frequency if it's already running! + DIAG(F("DCCEXanalogWriteFrequency::setting frequency to %d"), frequency); + } + } + channel_frequency[pin] = frequency; + return; +} + +void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) { + // Calculate percentage duty cycle from value given + uint32_t duty_cycle = (value * 100 / 256) + 1; + if (pin_timer[pin] != NULL) { + // if (duty_cycle == 100) + // { + // pin_timer[pin]->pauseChannel(pin_channel[pin]); + // DIAG(F("DCCEXanalogWrite::Pausing timer channel on pin %d"), pin); + // } + // else + // { + pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured! + // pin_timer[pin]->resumeChannel(pin_channel[pin]); + pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle + DIAG(F("DCCEXanalogWrite::Pin %d, value %d, duty cycle %d"), pin, value, duty_cycle); + // } + } + else + DIAG(F("DCCEXanalogWrite::Pin %d is not configured for PWM!"), pin); +} + + +// Now we can handle more ADCs, maybe this works! +#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS + +uint32_t ADCee::usedpins = 0; // Max of 32 ADC input channels! +uint8_t ADCee::highestPin = 0; // Highest pin to scan +int * ADCee::analogvals = NULL; // Array of analog values last captured +uint32_t * analogchans = NULL; // Array of channel numbers to be scanned +// bool adc1configured = false; +ADC_TypeDef * * adcchans = NULL; // Array to capture which ADC is each input channel on + +int16_t ADCee::ADCmax() +{ + return 4095; } int ADCee::init(uint8_t pin) { @@ -262,11 +352,33 @@ int ADCee::init(uint8_t pin) { return -1024; // some silly value as error uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32) - uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC channel (only valid for ADC1!) - GPIO_TypeDef * gpioBase; + uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC input channel + ADC_TypeDef *adc = (ADC_TypeDef *)pinmap_find_peripheral(stmpin, PinMap_ADC); // find which ADC this pin is on ADC1/2/3 etc. + int adcnum = 1; + if (adc == ADC1) + DIAG(F("ADCee::init(): found pin %d on ADC1"), pin); +// Checking for ADC2 and ADC3 being defined helps cater for more variants later +#if defined(ADC2) + else if (adc == ADC2) + { + DIAG(F("ADCee::init(): found pin %d on ADC2"), pin); + adcnum = 2; + } +#endif +#if defined(ADC3) + else if (adc == ADC3) + { + DIAG(F("ADCee::init(): found pin %d on ADC3"), pin); + adcnum = 3; + } +#endif + else DIAG(F("ADCee::init(): found pin %d on unknown ADC!"), pin); - // Port config - find which port we're on and power it up - switch(stmgpio) { + // Port config - find which port we're on and power it up + GPIO_TypeDef *gpioBase; + + switch (stmgpio) + { case 0x00: RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA gpioBase = GPIOA; @@ -279,6 +391,20 @@ int ADCee::init(uint8_t pin) { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC gpioBase = GPIOC; break; + case 0x03: + RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; //Power up PORTD + gpioBase = GPIOD; + break; + case 0x04: + RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; //Power up PORTE + gpioBase = GPIOE; + break; +#if defined(GPIOF) + case 0x05: + RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; //Power up PORTF + gpioBase = GPIOF; + break; +#endif default: return -1023; // some silly value as error } @@ -294,31 +420,33 @@ int ADCee::init(uint8_t pin) { if (adcchan > 18) return -1022; // silly value as error if (adcchan < 10) - ADC1->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles + adc->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles else - ADC1->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles + adc->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles // Read the inital ADC value for this analog input - ADC1->SQR3 = adcchan; // 1st conversion in regular sequence - ADC1->CR2 |= (1 << 30); // Start 1st conversion SWSTART - while(!(ADC1->SR & (1 << 1))); // Wait until conversion is complete - value = ADC1->DR; // Read value from register + adc->SQR3 = adcchan; // 1st conversion in regular sequence + adc->CR2 |= ADC_CR2_SWSTART; //(1 << 30); // Start 1st conversion SWSTART + while(!(adc->SR & (1 << 1))); // Wait until conversion is complete + value = adc->DR; // Read value from register uint8_t id = pin - PNUM_ANALOG_BASE; - if (id > 15) { // today we have not enough bits in the mask to support more - return -1021; - } + // if (id > 15) { // today we have not enough bits in the mask to support more + // return -1021; + // } - if (analogvals == NULL) { // allocate analogvals and analogchans if this is the first invocation of init. + if (analogvals == NULL) { // allocate analogvals, analogchans and adcchans if this is the first invocation of init analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int)); analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t)); + adcchans = (ADC_TypeDef **)calloc(NUM_ADC_INPUTS+1, sizeof(ADC_TypeDef)); } analogvals[id] = value; // Store sampled value analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin - usedpins |= (1 << id); // This pin is now ready + adcchans[id] = adc; // Keep track of which ADC this channel is on + usedpins |= (1 << id); // This pin is now ready if (id > highestPin) highestPin = id; // Store our highest pin in use - DIAG(F("ADCee::init(): value=%d, channel=%d, id=%d"), value, adcchan, id); + DIAG(F("ADCee::init(): value=%d, ADC%d: channel=%d, id=%d"), value, adcnum, adcchan, id); return value; } @@ -345,13 +473,16 @@ void ADCee::scan() { static uint8_t id = 0; // id and mask are the same thing but it is faster to static uint16_t mask = 1; // increment and shift instead to calculate mask from id static bool waiting = false; + static ADC_TypeDef *adc; - if (waiting) { + adc = adcchans[id]; + if (waiting) + { // look if we have a result - if (!(ADC1->SR & (1 << 1))) + if (!(adc->SR & (1 << 1))) return; // no result, continue to wait // found value - analogvals[id] = ADC1->DR; + analogvals[id] = adc->DR; // advance at least one track #ifdef DEBUG_ADC if (id == 1) TrackManager::track[1]->setBrake(0); @@ -370,9 +501,10 @@ void ADCee::scan() { // look for a valid track to sample or until we are around while (true) { if (mask & usedpins) { - // start new ADC aquire on id - ADC1->SQR3 = analogchans[id]; //1st conversion in regular sequence - ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART + // start new ADC aquire on id + adc = adcchans[id]; + adc->SQR3 = analogchans[id]; // 1st conversion in regular sequence + adc->CR2 |= (1 << 30); // Start 1st conversion SWSTART #ifdef DEBUG_ADC if (id == 1) TrackManager::track[1]->setBrake(1); #endif @@ -393,19 +525,83 @@ void ADCee::scan() { void ADCee::begin() { noInterrupts(); //ADC1 config sequence - // TODO: currently defaults to ADC1, may need more to handle other members of STM32F4xx family - RCC->APB2ENR |= (1 << 8); //Enable ADC1 clock (Bit8) + RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // Enable ADC1 clock // Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8 ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8) ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00) ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion + // Disable the DMA controller for ADC1 + ADC1->CR2 &= ~ADC_CR2_DMA; ADC1->CR2 &= ~(1 << 1); //Single conversion ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0 ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register ADC1->CR2 |= (1 << 0); // Switch on ADC1 + // Wait for ADC1 to become ready (calibration complete) + while (!(ADC1->CR2 & ADC_CR2_ADON)) { + } +#if defined(ADC2) + // Enable the ADC2 clock + RCC->APB2ENR |= RCC_APB2ENR_ADC2EN; + + // Initialize ADC2 + ADC2->CR1 = 0; // Disable all channels + ADC2->CR2 = 0; // Clear CR2 register + + ADC2->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8) + ADC2->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00) + ADC2->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion + ADC2->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3 + ADC2->CR2 &= ~(1 << 1); //Single conversion + ADC2->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0 + ADC2->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + ADC2->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + ADC2->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + + // Enable the ADC + ADC2->CR2 |= ADC_CR2_ADON; + + // Wait for ADC2 to become ready (calibration complete) + while (!(ADC2->CR2 & ADC_CR2_ADON)) { + } + + // Perform ADC3 calibration (optional) + // ADC3->CR2 |= ADC_CR2_CAL; + // while (ADC3->CR2 & ADC_CR2_CAL) { + // } +#endif +#if defined(ADC3) + // Enable the ADC3 clock + RCC->APB2ENR |= RCC_APB2ENR_ADC3EN; + + // Initialize ADC3 + ADC3->CR1 = 0; // Disable all channels + ADC3->CR2 = 0; // Clear CR2 register + + ADC3->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8) + ADC3->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00) + ADC3->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion + ADC3->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3 + ADC3->CR2 &= ~(1 << 1); //Single conversion + ADC3->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0 + ADC3->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + ADC3->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + ADC3->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register + + // Enable the ADC + ADC3->CR2 |= ADC_CR2_ADON; + + // Wait for ADC3 to become ready (calibration complete) + while (!(ADC3->CR2 & ADC_CR2_ADON)) { + } + + // Perform ADC3 calibration (optional) + // ADC3->CR2 |= ADC_CR2_CAL; + // while (ADC3->CR2 & ADC_CR2_CAL) { + // } +#endif interrupts(); } #endif diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index 4a99997..93b21a2 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -106,6 +106,7 @@ void DCCWaveform::interruptHandler() { DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) { isMainTrack = isMain; packetPending = false; + reminderWindowOpen = false; memcpy(transmitPacket, idlePacket, sizeof(idlePacket)); state = WAVE_START; // The +1 below is to allow the preamble generator to create the stop bit @@ -127,9 +128,15 @@ void DCCWaveform::interrupt2() { if (remainingPreambles > 0 ) { state=WAVE_MID_1; // switch state to trigger LOW on next interrupt remainingPreambles--; + + // As we get to the end of the preambles, open the reminder window. + // This delays any reminder insertion until the last moment so + // that the reminder doesn't block a more urgent packet. + reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1; + if (remainingPreambles==1) promotePendingPacket(); // Update free memory diagnostic as we don't have anything else to do this time. // Allow for checkAck and its called functions using 22 bytes more. - DCCTimer::updateMinimumFreeMemoryISR(22); + else DCCTimer::updateMinimumFreeMemoryISR(22); return; } @@ -148,30 +155,9 @@ void DCCWaveform::interrupt2() { if (bytes_sent >= transmitLength) { // end of transmission buffer... repeat or switch to next message bytes_sent = 0; + // preamble for next packet will start... remainingPreambles = requiredPreambles; - - if (transmitRepeats > 0) { - transmitRepeats--; } - else if (packetPending) { - // Copy pending packet to transmit packet - // a fixed length memcpy is faster than a variable length loop for these small lengths - // for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b]; - memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket)); - - transmitLength = pendingLength; - transmitRepeats = pendingRepeats; - packetPending = false; - clearResets(); - } - else { - // Fortunately reset and idle packets are the same length - memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket)); - transmitLength = sizeof(idlePacket); - transmitRepeats = 0; - if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!) - } - } } } #pragma GCC pop_options @@ -193,8 +179,39 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea packetPending = true; clearResets(); } -bool DCCWaveform::getPacketPending() { - return packetPending; + +bool DCCWaveform::isReminderWindowOpen() { + return reminderWindowOpen && ! packetPending; +} + +void DCCWaveform::promotePendingPacket() { + // fill the transmission packet from the pending packet + + // Just keep going if repeating + if (transmitRepeats > 0) { + transmitRepeats--; + return; + } + + if (packetPending) { + // Copy pending packet to transmit packet + // a fixed length memcpy is faster than a variable length loop for these small lengths + // for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b]; + memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket)); + + transmitLength = pendingLength; + transmitRepeats = pendingRepeats; + packetPending = false; + clearResets(); + return; + } + + // nothing to do, just send idles or resets + // Fortunately reset and idle packets are the same length + memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket)); + transmitLength = sizeof(idlePacket); + transmitRepeats = 0; + if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!) } #endif @@ -266,15 +283,15 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea } } -bool DCCWaveform::getPacketPending() { +bool DCCWaveform::isReminderWindowOpen() { if(isMainTrack) { if (rmtMainChannel == NULL) - return true; - return rmtMainChannel->busy(); + return false; + return !rmtMainChannel->busy(); } else { if (rmtProgChannel == NULL) - return true; - return rmtProgChannel->busy(); + return false; + return !rmtProgChannel->busy(); } } void IRAM_ATTR DCCWaveform::loop() { diff --git a/DCCWaveform.h b/DCCWaveform.h index 1dad1b2..2202b53 100644 --- a/DCCWaveform.h +++ b/DCCWaveform.h @@ -76,11 +76,13 @@ class DCCWaveform { }; #endif void schedulePacket(const byte buffer[], byte byteCount, byte repeats); - bool getPacketPending(); + bool isReminderWindowOpen(); + void promotePendingPacket(); private: #ifndef ARDUINO_ARCH_ESP32 volatile bool packetPending; + volatile bool reminderWindowOpen; volatile byte sentResetsSincePacket; #else volatile uint32_t resetPacketBase; diff --git a/Display.h b/Display.h index af36d43..467424f 100644 --- a/Display.h +++ b/Display.h @@ -37,7 +37,9 @@ class Display : public DisplayInterface { public: Display(DisplayDevice *deviceDriver); +#if !defined (MAX_CHARACTER_ROWS) static const int MAX_CHARACTER_ROWS = 8; +#endif static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE; static const long DISPLAY_SCROLL_TIME = 3000; // 3 seconds diff --git a/Display_Implementation.h b/Display_Implementation.h index ca19bd7..6a3c995 100644 --- a/Display_Implementation.h +++ b/Display_Implementation.h @@ -54,7 +54,9 @@ xxx; \ t->refresh();} #else - #define DISPLAY_START(xxx) {} + #define DISPLAY_START(xxx) { \ + xxx; \ + } #endif #endif // LCD_Implementation_h diff --git a/EXRAIL2.cpp b/EXRAIL2.cpp index 0e17ea9..3e55620 100644 --- a/EXRAIL2.cpp +++ b/EXRAIL2.cpp @@ -2,7 +2,7 @@ * © 2021 Neil McKechnie * © 2021-2023 Harald Barth * © 2020-2023 Chris Harlow - * © 2022 Colin Murdoch + * © 2022-2023 Colin Murdoch * All rights reserved. * * This file is part of CommandStation-EX @@ -52,23 +52,9 @@ #include "Turnouts.h" #include "CommandDistributor.h" #include "TrackManager.h" +#include "Turntables.h" +#include "IODevice.h" -// Command parsing keywords -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; // One instance of RMFT clas is used for each "thread" in the automation. // Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation. @@ -83,8 +69,8 @@ RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE. // when pausingTask is set, that is the ONLY task that gets any service, // and all others will have their locos stopped, then resumed after the pausing task resumes. byte RMFT2::flags[MAX_FLAGS]; - -LookList * RMFT2::sequenceLookup=NULL; +Print * RMFT2::LCCSerial=0; +LookList * RMFT2::routeLookup=NULL; LookList * RMFT2::onThrowLookup=NULL; LookList * RMFT2::onCloseLookup=NULL; LookList * RMFT2::onActivateLookup=NULL; @@ -94,9 +80,14 @@ LookList * RMFT2::onAmberLookup=NULL; LookList * RMFT2::onGreenLookup=NULL; LookList * RMFT2::onChangeLookup=NULL; LookList * RMFT2::onClockLookup=NULL; - -#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter) -#define SKIPOP progCounter+=3 +#ifndef IO_NO_HAL +LookList * RMFT2::onRotateLookup=NULL; +#endif +LookList * RMFT2::onOverloadLookup=NULL; +byte * RMFT2::routeStateArray=nullptr; +const FSH * * RMFT2::routeCaptionArray=nullptr; +int16_t * RMFT2::stashArray=nullptr; +int16_t RMFT2::maxStashId=0; // getOperand instance version, uses progCounter from instance. uint16_t RMFT2::getOperand(byte n) { @@ -114,6 +105,7 @@ uint16_t RMFT2::getOperand(int progCounter,byte n) { LookList::LookList(int16_t size) { m_size=size; m_loaded=0; + m_chain=nullptr; if (size) { m_lookupArray=new int16_t[size]; m_resultArray=new int16_t[size]; @@ -131,8 +123,35 @@ int16_t LookList::find(int16_t value) { for (int16_t i=0;ifind(value) :-1; +} +void LookList::chain(LookList * chain) { + m_chain=chain; +} +void LookList::handleEvent(const FSH* reason,int16_t id) { + // New feature... create multiple ONhandlers + for (int i=0;iprint(" "); + _stream->print(m_lookupArray[i]); + } +} + +int16_t LookList::findPosition(int16_t value) { + for (int16_t i=0;ichain(LookListLoader(OPCODE_SEQUENCE)); + if (compileFeatures && FEATURE_ROUTESTATE) { + routeStateArray=(byte *)calloc(routeLookup->size(),sizeof(byte)); + routeCaptionArray=(const FSH * *)calloc(routeLookup->size(),sizeof(const FSH *)); + } onThrowLookup=LookListLoader(OPCODE_ONTHROW); onCloseLookup=LookListLoader(OPCODE_ONCLOSE); onActivateLookup=LookListLoader(OPCODE_ONACTIVATE); onDeactivateLookup=LookListLoader(OPCODE_ONDEACTIVATE); - onRedLookup=LookListLoader(OPCODE_ONRED); - onAmberLookup=LookListLoader(OPCODE_ONAMBER); - onGreenLookup=LookListLoader(OPCODE_ONGREEN); onChangeLookup=LookListLoader(OPCODE_ONCHANGE); onClockLookup=LookListLoader(OPCODE_ONTIME); - +#ifndef IO_NO_HAL + onRotateLookup=LookListLoader(OPCODE_ONROTATE); +#endif + onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD); + // onLCCLookup is not the same so not loaded here. // Second pass startup, define any turnouts or servos, set signals red // add sequences onRoutines to the lookups +if (compileFeatures & FEATURE_SIGNAL) { + onRedLookup=LookListLoader(OPCODE_ONRED); + onAmberLookup=LookListLoader(OPCODE_ONAMBER); + onGreenLookup=LookListLoader(OPCODE_ONGREEN); for (int sigslot=0;;sigslot++) { VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8); if (sigid==0) break; // end of signal list doSignal(sigid & SIGNAL_ID_MASK, SIGNAL_RED); } + } int progCounter; for (progCounter=0;; SKIPOP){ @@ -195,6 +225,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { case OPCODE_AT: case OPCODE_ATTIMEOUT2: case OPCODE_AFTER: + case OPCODE_AFTEROVERLOAD: case OPCODE_IF: case OPCODE_IFNOT: { int16_t pin = (int16_t)operand; @@ -203,6 +234,12 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { IODevice::configureInput((VPIN)pin,true); break; } + case OPCODE_STASH: + case OPCODE_CLEAR_STASH: + case OPCODE_PICKUP_STASH: { + maxStashId=max(maxStashId,((int16_t)operand)); + break; + } case OPCODE_ATGTE: case OPCODE_ATLT: @@ -238,7 +275,38 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { setTurnoutHiddenState(VpinTurnout::create(id,pin)); break; } - + +#ifndef IO_NO_HAL + case OPCODE_DCCTURNTABLE: { + VPIN id=operand; + int home=getOperand(progCounter,1); + setTurntableHiddenState(DCCTurntable::create(id)); + Turntable *tto=Turntable::get(id); + tto->addPosition(0,0,home); + break; + } + + case OPCODE_EXTTTURNTABLE: { + VPIN id=operand; + VPIN pin=getOperand(progCounter,1); + int home=getOperand(progCounter,3); + setTurntableHiddenState(EXTTTurntable::create(id,pin)); + Turntable *tto=Turntable::get(id); + tto->addPosition(0,0,home); + break; + } + + case OPCODE_TTADDPOSITION: { + VPIN id=operand; + int position=getOperand(progCounter,1); + int value=getOperand(progCounter,2); + int angle=getOperand(progCounter,3); + Turntable *tto=Turntable::get(id); + tto->addPosition(position,value,angle); + break; + } +#endif + case OPCODE_AUTOSTART: // automatically create a task from here at startup. // Removed if (progCounter>0) check 4.2.31 because @@ -251,8 +319,14 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) { } } 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 diag=saved_diag; @@ -263,185 +337,22 @@ void RMFT2::setTurnoutHiddenState(Turnout * t) { t->setHidden(GETFLASH(getTurnoutDescription(t->getId()))==0x01); } +#ifndef IO_NO_HAL +void RMFT2::setTurntableHiddenState(Turntable * tto) { + tto->setHidden(GETFLASH(getTurntableDescription(tto->getId()))==0x01); +} +#endif + char RMFT2::getRouteType(int16_t id) { - for (int16_t i=0;;i+=2) { - int16_t rid= GETHIGHFLASHW(routeIdList,i); - if (rid==INT16_MAX) break; - if (rid==id) return 'R'; - } - for (int16_t i=0;;i+=2) { - int16_t rid= GETHIGHFLASHW(automationIdList,i); - if (rid==INT16_MAX) break; - if (rid==id) return 'A'; + int16_t progCounter=routeLookup->find(id); + if (progCounter>=0) { + byte type=GET_OPCODE; + if (type==OPCODE_ROUTE) return 'R'; + if (type==OPCODE_AUTOMATION) return 'A'; } return 'X'; } -// This filter intercepts <> commands to do the following: -// - Implement RMFT specific commands/diagnostics -// - Reject/modify JMRI commands that would interfere with RMFT processing -void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) { - (void)stream; // avoid compiler warning if we don't access this parameter - bool reject=false; - switch(opcode) { - - case 'D': - if (p[0]==HASH_KEYWORD_EXRAIL) { // - diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1); - opcode=0; - } - break; - - case '/': // New EXRAIL command - reject=!parseSlash(stream,paramCount,p); - opcode=0; - break; - - default: // other commands pass through - break; - } - if (reject) { - opcode=0; - StringFormatter::send(stream,F("")); - } -} - -bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { - - if (paramCount==0) { // STATUS - StringFormatter::send(stream, F("<* EXRAIL STATUS")); - RMFT2 * task=loopTask; - while(task) { - StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"), - (int)(task->taskId),task->progCounter,task->loco, - task->invert?'I':' ', - task->speedo, - task->forward?'F':'R' - ); - task=task->next; - if (task==loopTask) break; - } - // Now stream the flags - for (int id=0;id\n")); - return true; - } - switch (p[0]) { - case HASH_KEYWORD_PAUSE: // - if (paramCount!=1) return false; - DCC::setThrottle(0,1,true); // pause all locos on the track - pausingTask=(RMFT2 *)1; // Impossible task address - return true; - - case HASH_KEYWORD_RESUME: // - if (paramCount!=1) return false; - pausingTask=NULL; - { - RMFT2 * task=loopTask; - while(task) { - if (task->loco) task->driveLoco(task->speedo); - task=task->next; - if (task==loopTask) break; - } - } - return true; - - - case HASH_KEYWORD_START: // - if (paramCount<2 || paramCount>3) return false; - { - int route=(paramCount==2) ? p[1] : p[2]; - uint16_t cab=(paramCount==2)? 0 : p[1]; - int pc=sequenceLookup->find(route); - if (pc<0) return false; - RMFT2* task=new RMFT2(pc); - task->loco=cab; - } - return true; - - default: - break; - } - - // check KILL ALL here, otherwise the next validation confuses ALL with a flag - if (p[0]==HASH_KEYWORD_KILL && p[1]==HASH_KEYWORD_ALL) { - while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask - return true; - } - - // all other / commands take 1 parameter - if (paramCount!=2 ) return false; - - switch (p[0]) { - case HASH_KEYWORD_KILL: // Kill taskid|ALL - { - if ( p[1]<0 || p[1]>=MAX_FLAGS) return false; - RMFT2 * task=loopTask; - while(task) { - if (task->taskId==p[1]) { - task->kill(F("KILL")); - return true; - } - task=task->next; - if (task==loopTask) break; - } - } - return false; - - case HASH_KEYWORD_RESERVE: // force reserve a section - return setFlag(p[1],SECTION_FLAG); - - case HASH_KEYWORD_FREE: // force free a section - return setFlag(p[1],0,SECTION_FLAG); - - case HASH_KEYWORD_LATCH: - return setFlag(p[1], LATCH_FLAG); - - case HASH_KEYWORD_UNLATCH: - return setFlag(p[1], 0, LATCH_FLAG); - - case HASH_KEYWORD_RED: - doSignal(p[1],SIGNAL_RED); - return true; - - case HASH_KEYWORD_AMBER: - doSignal(p[1],SIGNAL_AMBER); - return true; - - case HASH_KEYWORD_GREEN: - doSignal(p[1],SIGNAL_GREEN); - return true; - - default: - return false; - } -} - - -// This emits Routes and Automations to Withrottle -// Automations are given a state to set the button to "handoff" which implies -// handing over the loco to the automation. -// Routes are given "Set" buttons and do not cause the loco to be handed over. - - RMFT2::RMFT2(int progCtr) { progCounter=progCtr; @@ -490,7 +401,7 @@ RMFT2::~RMFT2() { } void RMFT2::createNewTask(int route, uint16_t cab) { - int pc=sequenceLookup->find(route); + int pc=routeLookup->find(route); if (pc<0) return; RMFT2* task=new RMFT2(pc); task->loco=cab; @@ -599,6 +510,14 @@ void RMFT2::loop2() { Turnout::setClosed(operand, true); break; +#ifndef IO_NO_HAL + case OPCODE_ROTATE: + uint8_t activity; + activity=getOperand(2); + Turntable::setPosition(operand,getOperand(1),activity); + break; +#endif + case OPCODE_REV: forward = false; driveLoco(operand); @@ -684,7 +603,17 @@ void RMFT2::loop2() { } if (millis()-waitAfter < 500 ) return; break; - + + case OPCODE_AFTEROVERLOAD: // waits for the power to be turned back on - either by power routine or button + if (!TrackManager::isPowerOn(operand)) { + // reset timer to half a second and keep waiting + waitAfter=millis(); + delayMe(50); + return; + } + if (millis()-waitAfter < 500 ) return; + break; + case OPCODE_LATCH: setFlag(operand,LATCH_FLAG); break; @@ -715,13 +644,27 @@ void RMFT2::loop2() { TrackManager::setJoin(false); CommandDistributor::broadcastPower(); break; + + case OPCODE_SET_POWER: + // operand is TRACK_POWER , trackid + //byte thistrack=getOperand(1); + switch (operand) { + case TRACK_POWER_0: + TrackManager::setTrackPower(POWERMODE::OFF, getOperand(1)); + break; + case TRACK_POWER_1: + TrackManager::setTrackPower(POWERMODE::ON, getOperand(1)); + break; + } + + break; case OPCODE_SET_TRACK: // operand is trackmode<<8 | track id // If DC/DCX use my loco for DC address { TRACK_MODE mode = (TRACK_MODE)(operand>>8); - int16_t cab=(mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) ? loco : 0; + int16_t cab=(mode & TRACK_MODE_DC) ? loco : 0; TrackManager::setTrackMode(operand & 0x0F, mode, cab); } break; @@ -788,7 +731,13 @@ void RMFT2::loop2() { case OPCODE_IFCLOSED: skipIf=Turnout::isThrown(operand); break; - + +#ifndef IO_NO_HAL + case OPCODE_IFTTPOSITION: // do block if turntable at this position + skipIf=Turntable::getPosition(operand)!=(int)getOperand(1); + break; +#endif + case OPCODE_ENDIF: break; @@ -853,7 +802,7 @@ void RMFT2::loop2() { } case OPCODE_FOLLOW: - progCounter=sequenceLookup->find(operand); + progCounter=routeLookup->find(operand); if (progCounter<0) kill(F("FOLLOW unknown"), operand); return; @@ -863,7 +812,7 @@ void RMFT2::loop2() { return; } callStack[stackDepth++]=progCounter+3; - progCounter=sequenceLookup->find(operand); + progCounter=routeLookup->find(operand); if (progCounter<0) kill(F("CALL unknown"),operand); return; @@ -926,7 +875,7 @@ void RMFT2::loop2() { case OPCODE_START: { - int newPc=sequenceLookup->find(operand); + int newPc=routeLookup->find(operand); if (newPc<0) break; new RMFT2(newPc); } @@ -934,7 +883,7 @@ void RMFT2::loop2() { case OPCODE_SENDLOCO: // cab, route { - int newPc=sequenceLookup->find(getOperand(1)); + int newPc=routeLookup->find(getOperand(1)); if (newPc<0) break; RMFT2* newtask=new RMFT2(newPc); // create new task newtask->loco=operand; @@ -949,7 +898,21 @@ void RMFT2::loop2() { invert=false; } break; - + + case OPCODE_LCC: // short form LCC + if ((compileFeatures & FEATURE_LCC) && LCCSerial) + StringFormatter::send(LCCSerial,F(""),(uint16_t)operand); + break; + + case OPCODE_LCCX: // long form LCC + if ((compileFeatures & FEATURE_LCC) && LCCSerial) + StringFormatter::send(LCCSerial,F("\n"), + getOperand(progCounter,1), + getOperand(progCounter,2), + getOperand(progCounter,3), + getOperand(progCounter,0) + ); + break; case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration) IODevice::writeAnalogue(operand,getOperand(1),getOperand(2),getOperand(3)); @@ -961,11 +924,60 @@ void RMFT2::loop2() { return; } break; - + +#ifndef IO_NO_HAL + case OPCODE_WAITFORTT: // OPCODE_WAITFOR,V(turntable_id) + if (Turntable::ttMoving(operand)) { + delayMe(100); + return; + } + break; +#endif + case OPCODE_PRINT: printMessage(operand); break; - + case OPCODE_ROUTE_HIDDEN: + manageRouteState(operand,2); + break; + case OPCODE_ROUTE_INACTIVE: + manageRouteState(operand,0); + break; + case OPCODE_ROUTE_ACTIVE: + manageRouteState(operand,1); + break; + case OPCODE_ROUTE_DISABLED: + manageRouteState(operand,4); + 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_AUTOMATION: case OPCODE_SEQUENCE: @@ -978,6 +990,7 @@ void RMFT2::loop2() { case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime case OPCODE_ONCLOSE: // Turnout event catchers ignored here + case OPCODE_ONLCC: // LCC event catchers ignored here case OPCODE_ONTHROW: case OPCODE_ONACTIVATE: // Activate event catchers ignored here case OPCODE_ONDEACTIVATE: @@ -986,6 +999,13 @@ void RMFT2::loop2() { case OPCODE_ONGREEN: case OPCODE_ONCHANGE: case OPCODE_ONTIME: +#ifndef IO_NO_HAL + case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime + case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime + case OPCODE_TTADDPOSITION: // Turntable position definition ignored at runtime + case OPCODE_ONROTATE: +#endif + case OPCODE_ONOVERLOAD: break; @@ -1040,13 +1060,14 @@ int16_t RMFT2::getSignalSlot(int16_t id) { } /* static */ void RMFT2::doSignal(int16_t id,char rag) { + if (!(compileFeatures & FEATURE_SIGNAL)) return; // dont compile code below if (diag) DIAG(F(" doSignal %d %x"),id,rag); // Schedule any event handler for this signal change. // Thjis will work even without a signal definition. - if (rag==SIGNAL_RED) handleEvent(F("RED"),onRedLookup,id); - else if (rag==SIGNAL_GREEN) handleEvent(F("GREEN"), onGreenLookup,id); - else handleEvent(F("AMBER"), onAmberLookup,id); + if (rag==SIGNAL_RED) onRedLookup->handleEvent(F("RED"),id); + else if (rag==SIGNAL_GREEN) onGreenLookup->handleEvent(F("GREEN"),id); + else onAmberLookup->handleEvent(F("AMBER"),id); int16_t sigslot=getSignalSlot(id); if (sigslot<0) return; @@ -1107,6 +1128,7 @@ int16_t RMFT2::getSignalSlot(int16_t id) { } /* static */ bool RMFT2::isSignal(int16_t id,char rag) { + if (!(compileFeatures & FEATURE_SIGNAL)) return false; int16_t sigslot=getSignalSlot(id); if (sigslot<0) return false; return (flags[sigslot] & SIGNAL_MASK) == rag; @@ -1114,36 +1136,49 @@ int16_t RMFT2::getSignalSlot(int16_t id) { void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) { // Hunt for an ONTHROW/ONCLOSE for this turnout - if (closed) handleEvent(F("CLOSE"),onCloseLookup,turnoutId); - else handleEvent(F("THROW"),onThrowLookup,turnoutId); + if (closed) onCloseLookup->handleEvent(F("CLOSE"),turnoutId); + else onThrowLookup->handleEvent(F("THROW"),turnoutId); } void RMFT2::activateEvent(int16_t addr, bool activate) { // Hunt for an ONACTIVATE/ONDEACTIVATE for this accessory - if (activate) handleEvent(F("ACTIVATE"),onActivateLookup,addr); - else handleEvent(F("DEACTIVATE"),onDeactivateLookup,addr); + if (activate) onActivateLookup->handleEvent(F("ACTIVATE"),addr); + else onDeactivateLookup->handleEvent(F("DEACTIVATE"),addr); } void RMFT2::changeEvent(int16_t vpin, bool change) { // Hunt for an ONCHANGE for this sensor - if (change) handleEvent(F("CHANGE"),onChangeLookup,vpin); + if (change) onChangeLookup->handleEvent(F("CHANGE"),vpin); } +#ifndef IO_NO_HAL +void RMFT2::rotateEvent(int16_t turntableId, bool change) { + // Hunt or an ONROTATE for this turntable + if (change) onRotateLookup->handleEvent(F("ROTATE"),turntableId); +} +#endif + void RMFT2::clockEvent(int16_t clocktime, bool change) { // Hunt for an ONTIME for this time if (Diag::CMD) DIAG(F("Looking for clock event at : %d"), clocktime); if (change) { - handleEvent(F("CLOCK"),onClockLookup,clocktime); - handleEvent(F("CLOCK"),onClockLookup,25*60+clocktime%60); + onClockLookup->handleEvent(F("CLOCK"),clocktime); + onClockLookup->handleEvent(F("CLOCK"),25*60+clocktime%60); } } -void RMFT2::handleEvent(const FSH* reason,LookList* handlers, int16_t id) { - int pc= handlers->find(id); - if (pc<0) return; - +void RMFT2::powerEvent(int16_t track, bool overload) { + // Hunt for an ONOVERLOAD for this item + if (Diag::CMD) + DIAG(F("Looking for Power event on track : %c"), track); + if (overload) { + onOverloadLookup->handleEvent(F("POWER"),track); + } +} + +void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) { // Check we dont already have a task running this handler RMFT2 * task=loopTask; while(task) { @@ -1259,3 +1294,29 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) { break; } } + +void RMFT2::manageRouteState(uint16_t id, byte state) { + if (compileFeatures && FEATURE_ROUTESTATE) { + // Route state must be maintained for when new throttles connect. + // locate route id in the Routes lookup + int16_t position=routeLookup->findPosition(id); + if (position<0) return; + // set state beside it + if (routeStateArray[position]==state) return; + routeStateArray[position]=state; + CommandDistributor::broadcastRouteState(id,state); + } +} +void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) { + if (compileFeatures && FEATURE_ROUTESTATE) { + // Route state must be maintained for when new throttles connect. + // locate route id in the Routes lookup + int16_t position=routeLookup->findPosition(id); + if (position<0) return; + // set state beside it + if (routeCaptionArray[position]==caption) return; + routeCaptionArray[position]=caption; + CommandDistributor::broadcastRouteCaption(id,caption); + } +} + diff --git a/EXRAIL2.h b/EXRAIL2.h index 4d106e6..30a2f45 100644 --- a/EXRAIL2.h +++ b/EXRAIL2.h @@ -1,7 +1,7 @@ /* * © 2021 Neil McKechnie * © 2020-2022 Chris Harlow - * © 2022 Colin Murdoch + * © 2022-2023 Colin Murdoch * © 2023 Harald Barth * All rights reserved. * @@ -25,6 +25,7 @@ #include "FSH.h" #include "IODevice.h" #include "Turnouts.h" +#include "Turntables.h" // The following are the operation codes (or instructions) for a kind of virtual machine. // Each instruction is normally 3 bytes long with an operation code followed by a parameter. @@ -35,7 +36,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION, OPCODE_RESERVE,OPCODE_FREE, - OPCODE_AT,OPCODE_AFTER,OPCODE_AUTOSTART, + OPCODE_AT,OPCODE_AFTER, + OPCODE_AFTEROVERLOAD,OPCODE_AUTOSTART, OPCODE_ATGTE,OPCODE_ATLT, OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2, OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET, @@ -57,11 +59,18 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_ROSTER,OPCODE_KILLALL, OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE, OPCODE_ENDTASK,OPCODE_ENDEXRAIL, - OPCODE_SET_TRACK, + OPCODE_SET_TRACK,OPCODE_SET_POWER, OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN, OPCODE_ONCHANGE, OPCODE_ONCLOCKTIME, OPCODE_ONTIME, + OPCODE_TTADDPOSITION,OPCODE_DCCTURNTABLE,OPCODE_EXTTTURNTABLE, + OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_WAITFORTT, + OPCODE_LCC,OPCODE_LCCX,OPCODE_ONLCC, + OPCODE_ONOVERLOAD, + OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN, + OPCODE_ROUTE_DISABLED, + OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH, // OPcodes below this point are skip-nesting IF operations // placed here so that they may be skipped as a group @@ -74,7 +83,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_IFRANDOM,OPCODE_IFRESERVE, OPCODE_IFCLOSED,OPCODE_IFTHROWN, OPCODE_IFRE, - OPCODE_IFLOCO + OPCODE_IFLOCO, + OPCODE_IFTTPOSITION }; // Ensure thrunge_lcd is put last as there may be more than one display, @@ -88,7 +98,13 @@ enum thrunger: byte { thrunge_lcd, // Must be last!! }; - + // Flag bits for compile time features. + static const byte FEATURE_SIGNAL= 0x80; + static const byte FEATURE_LCC = 0x40; + static const byte FEATURE_ROSTER= 0x20; + static const byte FEATURE_ROUTESTATE= 0x10; + static const byte FEATURE_STASH = 0x08; + // Flag bits for status of hardware and TPL static const byte SECTION_FLAG = 0x80; @@ -108,13 +124,20 @@ enum thrunger: byte { class LookList { public: LookList(int16_t size); + void chain(LookList* chainTo); void add(int16_t lookup, int16_t result); - int16_t find(int16_t value); + int16_t find(int16_t value); // finds result value + int16_t findPosition(int16_t value); // finds index + int16_t size(); + void stream(Print * _stream); + void handleEvent(const FSH* reason,int16_t id); + private: int16_t m_size; int16_t m_loaded; int16_t * m_lookupArray; - int16_t * m_resultArray; + int16_t * m_resultArray; + LookList* m_chain; }; class RMFT2 { @@ -130,6 +153,8 @@ class LookList { static void activateEvent(int16_t addr, bool active); static void changeEvent(int16_t id, bool change); static void clockEvent(int16_t clocktime, bool change); + static void rotateEvent(int16_t id, bool change); + static void powerEvent(int16_t track, bool overload); static const int16_t SERVO_SIGNAL_FLAG=0x4000; static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000; static const int16_t DCC_SIGNAL_FLAG=0x1000; @@ -144,7 +169,10 @@ class LookList { static const FSH * getTurnoutDescription(int16_t id); static const FSH * getRosterName(int16_t id); static const FSH * getRosterFunctions(int16_t id); - + static const FSH * getTurntableDescription(int16_t id); + static const FSH * getTurntablePositionDescription(int16_t turntableId, uint8_t positionId); + static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc); + private: static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]); static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ; @@ -156,9 +184,11 @@ private: static bool isSignal(int16_t id,char rag); static int16_t getSignalSlot(int16_t id); static void setTurnoutHiddenState(Turnout * t); + #ifndef IO_NO_HAL + static void setTurntableHiddenState(Turntable * tto); + #endif static LookList* LookListLoader(OPCODE op1, OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL); - static void handleEvent(const FSH* reason,LookList* handlers, int16_t id); static uint16_t getOperand(int progCounter,byte n); static RMFT2 * loopTask; static RMFT2 * pausingTask; @@ -175,10 +205,11 @@ private: uint16_t getOperand(byte n); static bool diag; - static const HIGHFLASH byte RouteCode[]; + static const HIGHFLASH3 byte RouteCode[]; static const HIGHFLASH int16_t SignalDefinitions[]; static byte flags[MAX_FLAGS]; - static LookList * sequenceLookup; + static Print * LCCSerial; + static LookList * routeLookup; static LookList * onThrowLookup; static LookList * onCloseLookup; static LookList * onActivateLookup; @@ -188,6 +219,20 @@ private: static LookList * onGreenLookup; static LookList * onChangeLookup; static LookList * onClockLookup; +#ifndef IO_NO_HAL + static LookList * onRotateLookup; +#endif + static LookList * onOverloadLookup; + + static const int countLCCLookup; + static int onLCCLookup[]; + static const byte compileFeatures; + static void manageRouteState(uint16_t id, byte state); + static void manageRouteCaption(uint16_t id, const FSH* caption); + static byte * routeStateArray; + static const FSH ** routeCaptionArray; + static int16_t * stashArray; + static int16_t maxStashId; // Local variables - exist for each instance/task RMFT2 *next; // loop chain @@ -209,4 +254,8 @@ private: byte stackDepth; int callStack[MAX_STACK_DEPTH]; }; + +#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter) +#define SKIPOP progCounter+=3 + #endif diff --git a/EXRAIL2MacroReset.h b/EXRAIL2MacroReset.h index 588a417..7811a0d 100644 --- a/EXRAIL2MacroReset.h +++ b/EXRAIL2MacroReset.h @@ -1,6 +1,6 @@ /* * © 2020-2022 Chris Harlow. All rights reserved. - * © 2022 Colin Murdoch + * © 2022-2023 Colin Murdoch * © 2023 Harald Barth * * This file is part of CommandStation-EX @@ -27,6 +27,7 @@ #undef ACTIVATE #undef ACTIVATEL #undef AFTER +#undef AFTEROVERLOAD #undef ALIAS #undef AMBER #undef ANOUT @@ -38,8 +39,11 @@ #undef AUTOSTART #undef BROADCAST #undef CALL +#undef CLEAR_STASH +#undef CLEAR_ALL_STASH #undef CLOSE #undef DCC_SIGNAL +#undef DCC_TURNTABLE #undef DEACTIVATE #undef DEACTIVATEL #undef DELAY @@ -51,8 +55,9 @@ #undef ENDEXRAIL #undef ENDIF #undef ENDTASK -#undef ESTOP -#undef EXRAIL +#undef ESTOP +#undef EXRAIL +#undef EXTT_TURNTABLE #undef FADE #undef FOFF #undef FOLLOW @@ -75,6 +80,7 @@ #undef IFRESERVE #undef IFTHROWN #undef IFTIMEOUT +#undef IFTTPOSITION #undef IFRE #undef INVERT_DIRECTION #undef JOIN @@ -82,6 +88,8 @@ #undef LATCH #undef LCD #undef SCREEN +#undef LCC +#undef LCCX #undef LCN #undef MOVETT #undef ONACTIVATE @@ -90,15 +98,19 @@ #undef ONDEACTIVATE #undef ONDEACTIVATEL #undef ONCLOSE +#undef ONLCC #undef ONTIME #undef ONCLOCKTIME #undef ONCLOCKMINS +#undef ONOVERLOAD #undef ONGREEN #undef ONRED +#undef ONROTATE #undef ONTHROW #undef ONCHANGE #undef PARSE #undef PAUSE +#undef PICKUP_STASH #undef PIN_TURNOUT #undef PRINT #ifndef DISABLE_PROG @@ -113,8 +125,15 @@ #undef RESUME #undef RETURN #undef REV -#undef ROSTER +#undef ROSTER +#undef ROTATE +#undef ROTATE_DCC #undef ROUTE +#undef ROUTE_ACTIVE +#undef ROUTE_INACTIVE +#undef ROUTE_HIDDEN +#undef ROUTE_DISABLED +#undef ROUTE_CAPTION #undef SENDLOCO #undef SEQUENCE #undef SERIAL @@ -130,13 +149,17 @@ #undef SERVO_SIGNAL #undef SET #undef SET_TRACK +#undef SET_POWER #undef SETLOCO #undef SIGNAL #undef SIGNALH #undef SPEED #undef START +#undef STASH +#undef STEALTH #undef STOP -#undef THROW +#undef THROW +#undef TT_ADDPOSITION #undef TURNOUT #undef TURNOUTL #undef UNJOIN @@ -144,6 +167,9 @@ #undef VIRTUAL_SIGNAL #undef VIRTUAL_TURNOUT #undef WAITFOR +#ifndef IO_NO_HAL +#undef WAITFORTT +#endif #undef WITHROTTLE #undef XFOFF #undef XFON @@ -152,6 +178,7 @@ #define ACTIVATE(addr,subaddr) #define ACTIVATEL(addr) #define AFTER(sensor_id) +#define AFTEROVERLOAD(track_id) #define ALIAS(name,value...) #define AMBER(signal_id) #define ANOUT(vpin,value,param1,param2) @@ -162,9 +189,12 @@ #define AUTOMATION(id,description) #define AUTOSTART #define BROADCAST(msg) -#define CALL(route) +#define CALL(route) +#define CLEAR_STASH(id) +#define CLEAR_ALL_STASH(id) #define CLOSE(id) #define DCC_SIGNAL(id,add,subaddr) +#define DCC_TURNTABLE(id,home,description) #define DEACTIVATE(addr,subaddr) #define DEACTIVATEL(addr) #define DELAY(mindelay) @@ -177,7 +207,8 @@ #define ENDIF #define ENDTASK #define ESTOP -#define EXRAIL +#define EXRAIL +#define EXTT_TURNTABLE(id,vpin,home,description) #define FADE(pin,value,ms) #define FOFF(func) #define FOLLOW(route) @@ -200,11 +231,14 @@ #define IFTHROWN(turnout_id) #define IFRESERVE(block) #define IFTIMEOUT +#define IFTTPOSITION(turntable_id,position) #define IFRE(sensor_id,value) #define INVERT_DIRECTION #define JOIN #define KILLALL -#define LATCH(sensor_id) +#define LATCH(sensor_id) +#define LCC(eventid) +#define LCCX(senderid,eventid) #define LCD(row,msg) #define SCREEN(display,row,msg) #define LCN(msg) @@ -215,17 +249,21 @@ #define ONTIME(value) #define ONCLOCKTIME(hours,mins) #define ONCLOCKMINS(mins) +#define ONOVERLOAD(track_id) #define ONDEACTIVATE(addr,subaddr) #define ONDEACTIVATEL(linear) #define ONCLOSE(turnout_id) +#define ONLCC(sender,event) #define ONGREEN(signal_id) -#define ONRED(signal_id) +#define ONRED(signal_id) +#define ONROTATE(turntable_id) #define ONTHROW(turnout_id) #define ONCHANGE(sensor_id) #define PAUSE #define PIN_TURNOUT(id,pin,description...) #define PRINT(msg) #define PARSE(msg) +#define PICKUP_STASH(id) #ifndef DISABLE_PROG #define POM(cv,value) #endif @@ -238,8 +276,15 @@ #define RESUME #define RETURN #define REV(speed) -#define ROUTE(id,description) +#define ROTATE(turntable_id,position,activity) +#define ROTATE_DCC(turntable_id,position) #define ROSTER(cab,name,funcmap...) +#define ROUTE(id,description) +#define ROUTE_ACTIVE(id) +#define ROUTE_INACTIVE(id) +#define ROUTE_HIDDEN(id) +#define ROUTE_DISABLED(id) +#define ROUTE_CAPTION(id,caption) #define SENDLOCO(cab,route) #define SEQUENCE(id) #define SERIAL(msg) @@ -255,13 +300,17 @@ #define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) #define SET(pin) #define SET_TRACK(track,mode) +#define SET_POWER(track,onoff) #define SETLOCO(loco) #define SIGNAL(redpin,amberpin,greenpin) #define SIGNALH(redpin,amberpin,greenpin) #define SPEED(speed) -#define START(route) +#define START(route) +#define STASH(id) +#define STEALTH(code...) #define STOP -#define THROW(id) +#define THROW(id) +#define TT_ADDPOSITION(turntable_id,position,value,angle,description...) #define TURNOUT(id,addr,subaddr,description...) #define TURNOUTL(id,addr,description...) #define UNJOIN @@ -269,6 +318,9 @@ #define VIRTUAL_SIGNAL(id) #define VIRTUAL_TURNOUT(id,description...) #define WAITFOR(pin) +#ifndef IO_NO_HAL +#define WAITFORTT(turntable_id) +#endif #define WITHROTTLE(msg) #define XFOFF(cab,func) #define XFON(cab,func) diff --git a/EXRAIL2Parser.cpp b/EXRAIL2Parser.cpp new file mode 100644 index 0000000..b049699 --- /dev/null +++ b/EXRAIL2Parser.cpp @@ -0,0 +1,328 @@ +/* + * © 2021 Neil McKechnie + * © 2021-2023 Harald Barth + * © 2020-2023 Chris Harlow + * © 2022-2023 Colin Murdoch + * 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 . + */ + +// THIS file is an extension of the RMFT2 class +// normally found in EXRAIL2.cpp + +#include +#include "defines.h" +#include "EXRAIL2.h" +#include "DCC.h" +// Command parsing keywords +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: +// - Implement RMFT specific commands/diagnostics +// - Reject/modify JMRI commands that would interfere with RMFT processing + +void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) { + (void)stream; // avoid compiler warning if we don't access this parameter + bool reject=false; + switch(opcode) { + + case 'D': + if (p[0]==HASH_KEYWORD_EXRAIL) { // + diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1); + opcode=0; + } + break; + + case '/': // New EXRAIL command + reject=!parseSlash(stream,paramCount,p); + opcode=0; + break; + + case 'L': + // This entire code block is compiled out if LLC macros not used + if (!(compileFeatures & FEATURE_LCC)) return; + + if (paramCount==0) { // LCC adapter introducing self + LCCSerial=stream; // now we know where to send events we raise + + // loop through all possible sent events + for (int progCounter=0;; SKIPOP) { + byte opcode=GET_OPCODE; + if (opcode==OPCODE_ENDEXRAIL) break; + if (opcode==OPCODE_LCC) StringFormatter::send(stream,F("\n"),getOperand(progCounter,0)); + if (opcode==OPCODE_LCCX) { // long form LCC + StringFormatter::send(stream,F("\n"), + getOperand(progCounter,1), + getOperand(progCounter,2), + getOperand(progCounter,3), + getOperand(progCounter,0) + ); + }} + + // we stream the hex events we wish to listen to + // and at the same time build the event index looku. + + + int eventIndex=0; + for (int progCounter=0;; SKIPOP) { + byte opcode=GET_OPCODE; + if (opcode==OPCODE_ENDEXRAIL) break; + if (opcode==OPCODE_ONLCC) { + onLCCLookup[eventIndex]=progCounter; // TODO skip... + StringFormatter::send(stream,F("\n"), + eventIndex, + getOperand(progCounter,1), + getOperand(progCounter,2), + getOperand(progCounter,3), + getOperand(progCounter,0) + ); + eventIndex++; + } + } + StringFormatter::send(stream,F("\n")); // Ready to rumble + opcode=0; + break; + } + if (paramCount==1) { // LCC event arrived from adapter + int16_t eventid=p[0]; + reject=eventid<0 || eventid>=countLCCLookup; + if (!reject) startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]); + opcode=0; + } + break; + + case 'J': // throttle info commands + if (paramCount<1) return; + switch(p[0]) { + case HASH_KEYWORD_A: // returns automations/routes + if (paramCount==1) {// + StringFormatter::send(stream, F("stream(stream); + StringFormatter::send(stream, F(">\n")); + opcode=0; + return; + } + if (paramCount==2) { // + uint16_t id=p[1]; + StringFormatter::send(stream,F("\n"), + id, getRouteType(id), getRouteDescription(id)); + + if (compileFeatures & FEATURE_ROUTESTATE) { + // Send any non-default button states or captions + int16_t statePos=routeLookup->findPosition(id); + if (statePos>=0) { + if (routeStateArray[statePos]) + StringFormatter::send(stream,F("\n"), id, routeStateArray[statePos]); + if (routeCaptionArray[statePos]) + StringFormatter::send(stream,F("\n"), id,routeCaptionArray[statePos]); + } + } + opcode=0; + return; + } + break; + case HASH_KEYWORD_M: + // NOTE: we only need to handle valid calls here because + // DCCEXParser has to have code to handle the 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) { // + StringFormatter::send(stream,F("\n"),maxStashId); + opcode=0; + break; + } + if (paramCount==2) { // + if (p[1]<=0 || p[1]>maxStashId) break; + StringFormatter::send(stream,F("\n"), + p[1],stashArray[p[1]]); + opcode=0; + break; + } + if (paramCount==3) { // + if (p[1]<=0 || p[1]>maxStashId) break; + stashArray[p[1]]=p[2]; + opcode=0; + break; + } + break; + + default: + break; + } + default: // other commands pass through + break; + } +} + +bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) { + + if (paramCount==0) { // STATUS + StringFormatter::send(stream, F("<* EXRAIL STATUS")); + RMFT2 * task=loopTask; + while(task) { + StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"), + (int)(task->taskId),task->progCounter,task->loco, + task->invert?'I':' ', + task->speedo, + task->forward?'F':'R' + ); + task=task->next; + if (task==loopTask) break; + } + // Now stream the flags + for (int id=0;id\n")); + return true; + } + switch (p[0]) { + case HASH_KEYWORD_PAUSE: // + if (paramCount!=1) return false; + DCC::setThrottle(0,1,true); // pause all locos on the track + pausingTask=(RMFT2 *)1; // Impossible task address + return true; + + case HASH_KEYWORD_RESUME: // + if (paramCount!=1) return false; + pausingTask=NULL; + { + RMFT2 * task=loopTask; + while(task) { + if (task->loco) task->driveLoco(task->speedo); + task=task->next; + if (task==loopTask) break; + } + } + return true; + + + case HASH_KEYWORD_START: // + if (paramCount<2 || paramCount>3) return false; + { + int route=(paramCount==2) ? p[1] : p[2]; + uint16_t cab=(paramCount==2)? 0 : p[1]; + int pc=routeLookup->find(route); + if (pc<0) return false; + RMFT2* task=new RMFT2(pc); + task->loco=cab; + } + return true; + + default: + break; + } + + // check KILL ALL here, otherwise the next validation confuses ALL with a flag + if (p[0]==HASH_KEYWORD_KILL && p[1]==HASH_KEYWORD_ALL) { + while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask + return true; + } + + // all other / commands take 1 parameter + if (paramCount!=2 ) return false; + + switch (p[0]) { + case HASH_KEYWORD_KILL: // Kill taskid|ALL + { + if ( p[1]<0 || p[1]>=MAX_FLAGS) return false; + RMFT2 * task=loopTask; + while(task) { + if (task->taskId==p[1]) { + task->kill(F("KILL")); + return true; + } + task=task->next; + if (task==loopTask) break; + } + } + return false; + + case HASH_KEYWORD_RESERVE: // force reserve a section + return setFlag(p[1],SECTION_FLAG); + + case HASH_KEYWORD_FREE: // force free a section + return setFlag(p[1],0,SECTION_FLAG); + + case HASH_KEYWORD_LATCH: + return setFlag(p[1], LATCH_FLAG); + + case HASH_KEYWORD_UNLATCH: + return setFlag(p[1], 0, LATCH_FLAG); + + case HASH_KEYWORD_RED: + doSignal(p[1],SIGNAL_RED); + return true; + + case HASH_KEYWORD_AMBER: + doSignal(p[1],SIGNAL_AMBER); + return true; + + case HASH_KEYWORD_GREEN: + doSignal(p[1],SIGNAL_GREEN); + return true; + + default: + return false; + } +} + diff --git a/EXRAILMacros.h b/EXRAILMacros.h index 66b0111..f79693d 100644 --- a/EXRAILMacros.h +++ b/EXRAILMacros.h @@ -1,7 +1,7 @@ /* * © 2021 Neil McKechnie * © 2020-2022 Chris Harlow - * © 2022 Colin Murdoch + * © 2022-2023 Colin Murdoch * © 2023 Harald Barth * All rights reserved. * @@ -54,6 +54,8 @@ // helper macro for turnout descriptions, creates NULL for missing description #define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL; +// helper macro for turntable descriptions, creates NULL for missing description +#define T_DESC(tid,pid,desc) if(turntableId==tid && positionId==pid) return ("" desc)[0]?F("" desc):NULL; // helper macro for turnout description as HIDDEN #define HIDDEN "\x01" @@ -61,13 +63,19 @@ // (10#mins)%100) #define STRIP_ZERO(value) 10##value%100 +// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF). +//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0; +//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1; + + // Pass 1 Implements aliases #include "EXRAIL2MacroReset.h" #undef ALIAS #define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10; #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 #include "EXRAIL2MacroReset.h" #undef HAL #define HAL(haltype,params...) haltype::create(params); @@ -75,11 +83,54 @@ void exrailHalSetup() { #include "myAutomation.h" } +// Pass 1c detect compile time featurtes +#include "EXRAIL2MacroReset.h" +#undef SIGNAL +#define SIGNAL(redpin,amberpin,greenpin) | FEATURE_SIGNAL +#undef SIGNALH +#define SIGNALH(redpin,amberpin,greenpin) | FEATURE_SIGNAL +#undef SERVO_SIGNAL +#define SERVO_SIGNAL(vpin,redval,amberval,greenval) | FEATURE_SIGNAL +#undef DCC_SIGNAL +#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL +#undef VIRTUAL_SIGNAL +#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL + +#undef LCC +#define LCC(eventid) | FEATURE_LCC +#undef LCCX +#define LCCX(senderid,eventid) | FEATURE_LCC +#undef ONLCC +#define ONLCC(senderid,eventid) | FEATURE_LCC +#undef ROUTE_ACTIVE +#define ROUTE_ACTIVE(id) | FEATURE_ROUTESTATE +#undef ROUTE_INACTIVE +#define ROUTE_INACTIVE(id) | FEATURE_ROUTESTATE +#undef ROUTE_HIDDEN +#define ROUTE_HIDDEN(id) | FEATURE_ROUTESTATE +#undef ROUTE_DISABLED +#define ROUTE_DISABLED(id) | FEATURE_ROUTESTATE +#undef ROUTE_CAPTION +#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 + #include "myAutomation.h" +; + // Pass 2 create throttle route list #include "EXRAIL2MacroReset.h" #undef ROUTE #define ROUTE(id, description) id, -const int16_t HIGHFLASH RMFT2::routeIdList[]= { +const int16_t HIGHFLASH RMFT2::routeIdList[]= { #include "myAutomation.h" INT16_MAX}; // Pass 2a create throttle automation list @@ -121,6 +172,12 @@ const int StringMacroTracker1=__COUNTER__; #define PRINT(msg) THRUNGE(msg,thrunge_print) #undef LCN #define LCN(msg) THRUNGE(msg,thrunge_lcn) +#undef ROUTE_CAPTION +#define ROUTE_CAPTION(id,caption) \ +case (__COUNTER__ - StringMacroTracker1) : {\ + manageRouteCaption(id,F(caption));\ + return;\ + } #undef SERIAL #define SERIAL(msg) THRUNGE(msg,thrunge_serial) #undef SERIAL1 @@ -153,6 +210,8 @@ const int StringMacroTracker1=__COUNTER__; lcdid=id;\ break;\ } +#undef STEALTH +#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return; #undef WITHROTTLE #define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle) @@ -172,6 +231,8 @@ void RMFT2::printMessage(uint16_t id) { #include "EXRAIL2MacroReset.h" #undef TURNOUT #define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description) +#undef TURNOUTL +#define TURNOUTL(id,addr,description...) O_DESC(id,description) #undef PIN_TURNOUT #define PIN_TURNOUT(id,pin,description...) O_DESC(id,description) #undef SERVO_TURNOUT @@ -187,6 +248,31 @@ const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) { return NULL; } +// Pass to get turntable descriptions (optional) +#include "EXRAIL2MacroReset.h" +#undef DCC_TURNTABLE +#define DCC_TURNTABLE(id,home,description...) O_DESC(id,description) +#undef EXTT_TURNTABLE +#define EXTT_TURNTABLE(id,vpin,home,description...) O_DESC(id,description) + +const FSH * RMFT2::getTurntableDescription(int16_t turntableId) { + switch (turntableId) { + #include "myAutomation.h" + default:break; + } + return NULL; +} + +// Pass to get turntable position descriptions (optional) +#include "EXRAIL2MacroReset.h" +#undef TT_ADDPOSITION +#define TT_ADDPOSITION(turntable_id,position,value,home,description...) T_DESC(turntable_id,position,description) + +const FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, uint8_t positionId) { + #include "myAutomation.h" + return NULL; +} + // Pass 6: Roster IDs (count) #include "EXRAIL2MacroReset.h" #undef ROSTER @@ -243,6 +329,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #include "myAutomation.h" 0,0,0,0 }; +// Pass 9 ONLCC counter and lookup array +#include "EXRAIL2MacroReset.h" +#undef ONLCC +#define ONLCC(sender,event) +1 + +const int RMFT2::countLCCLookup=0 +#include "myAutomation.h" +; +int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; + // Last Pass : create main routes table // Only undef the macros, not dummy them. #define RMFT2_UNDEF_ONLY @@ -256,6 +352,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1), #define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1), #define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id), +#define AFTEROVERLOAD(track_id) OPCODE_AFTEROVERLOAD,V(TRACK_NUMBER_##track_id), #define ALIAS(name,value...) #define AMBER(signal_id) OPCODE_AMBER,V(signal_id), #define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2), @@ -267,7 +364,12 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define AUTOSTART OPCODE_AUTOSTART,0,0, #define BROADCAST(msg) PRINT(msg) #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), +#ifndef IO_NO_HAL +#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home), +#endif #define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1), #define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1), #define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)), @@ -281,7 +383,10 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define ENDIF OPCODE_ENDIF,0,0, #define ENDTASK OPCODE_ENDTASK,0,0, #define ESTOP OPCODE_SPEED,V(1), -#define EXRAIL +#define EXRAIL +#ifndef IO_NO_HAL +#define EXTT_TURNTABLE(id,vpin,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(home), +#endif #define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L), #define FOFF(func) OPCODE_FOFF,V(func), #define FOLLOW(route) OPCODE_FOLLOW,V(route), @@ -304,29 +409,47 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define IFRESERVE(block) OPCODE_IFRESERVE,V(block), #define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id), #define IFTIMEOUT OPCODE_IFTIMEOUT,0,0, +#ifndef IO_NO_HAL +#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position), +#endif #define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value), #define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0, #define JOIN OPCODE_JOIN,0,0, #define KILLALL OPCODE_KILLALL,0,0, #define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id), +#define LCC(eventid) OPCODE_LCC,V(eventid), +#define LCCX(sender,event) OPCODE_LCCX,V(event),\ + OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\ + OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\ + OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF), #define LCD(id,msg) PRINT(msg) #define SCREEN(display,id,msg) PRINT(msg) +#define STEALTH(code...) PRINT(dummy) #define LCN(msg) PRINT(msg) #define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0), #define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr), #define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3), #define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id), #define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id), +#define ONLCC(sender,event) OPCODE_ONLCC,V(event),\ + OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\ + OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\ + OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF), #define ONTIME(value) OPCODE_ONTIME,V(value), #define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)), #define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins) +#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id), #define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr), #define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3), #define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id), #define ONRED(signal_id) OPCODE_ONRED,V(signal_id), +#ifndef IO_NO_HAL +#define ONROTATE(id) OPCODE_ONROTATE,V(id), +#endif #define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id), #define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id), #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), #ifndef DISABLE_PROG #define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value), @@ -343,7 +466,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define RETURN OPCODE_RETURN,0,0, #define REV(speed) OPCODE_REV,V(speed), #define ROSTER(cabid,name,funcmap...) +#ifndef IO_NO_HAL +#define ROTATE(id,position,activity) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(EXTurntable::activity), +#define ROTATE_DCC(id,position) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(0), +#endif #define ROUTE(id, description) OPCODE_ROUTE, V(id), +#define ROUTE_ACTIVE(id) OPCODE_ROUTE_ACTIVE,V(id), +#define ROUTE_INACTIVE(id) OPCODE_ROUTE_INACTIVE,V(id), +#define ROUTE_HIDDEN(id) OPCODE_ROUTE_HIDDEN,V(id), +#define ROUTE_DISABLED(id) OPCODE_ROUTE_DISABLED,V(id), +#define ROUTE_CAPTION(id,caption) PRINT(caption) #define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route), #define SEQUENCE(id) OPCODE_SEQUENCE, V(id), #define SERIAL(msg) PRINT(msg) @@ -359,13 +491,18 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile), #define SET(pin) OPCODE_SET,V(pin), #define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track), +#define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track), #define SETLOCO(loco) OPCODE_SETLOCO,V(loco), #define SIGNAL(redpin,amberpin,greenpin) #define SIGNALH(redpin,amberpin,greenpin) #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 THROW(id) OPCODE_THROW,V(id), +#ifndef IO_NO_HAL +#define TT_ADDPOSITION(id,position,value,angle,description...) OPCODE_TTADDPOSITION,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(value),OPCODE_PAD,V(angle), +#endif #define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr), #define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description) #define UNJOIN OPCODE_UNJOIN,0,0, @@ -374,12 +511,15 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = { #define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0), #define WITHROTTLE(msg) PRINT(msg) #define WAITFOR(pin) OPCODE_WAITFOR,V(pin), +#ifndef IO_NO_HAL +#define WAITFORTT(turntable_id) OPCODE_WAITFORTT,V(turntable_id), +#endif #define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func), #define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func), // Build RouteCode const int StringMacroTracker2=__COUNTER__; -const HIGHFLASH byte RMFT2::RouteCode[] = { +const HIGHFLASH3 byte RMFT2::RouteCode[] = { #include "myAutomation.h" OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 }; diff --git a/FSH.h b/FSH.h index d031935..280d37e 100644 --- a/FSH.h +++ b/FSH.h @@ -56,6 +56,7 @@ typedef __FlashStringHelper FSH; #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) // AVR_MEGA memory deliberately placed at end of link may need _far functions #define HIGHFLASH __attribute__((section(".fini2"))) +#define HIGHFLASH3 __attribute__((section(".fini3"))) #define GETFARPTR(data) pgm_get_far_address(data) #define GETHIGHFLASH(data,offset) pgm_read_byte_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 // as there is no progmem above 32kb anyway. #define HIGHFLASH PROGMEM +#define HIGHFLASH3 PROGMEM #define GETFARPTR(data) ((uint32_t)(data)) #define GETHIGHFLASH(data,offset) pgm_read_byte_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; #define FLASH #define HIGHFLASH +#define HIGHFLASH3 #define GETFARPTR(data) ((uint32_t)(data)) #define GETFLASH(addr) (*(const byte *)(addr)) #define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset)) diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h index 77a75c5..23c6a6e 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "devel-202308020800Z" +#define GITHUB_SHA "devel-202311270714Z" diff --git a/I2CManager.cpp b/I2CManager.cpp index d0d8550..1d1387e 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -92,7 +92,7 @@ void I2CManagerClass::begin(void) { // Probe and list devices. Use standard mode // (clock speed 100kHz) for best device compatibility. _setClock(100000); - unsigned long originalTimeout = _timeout; + uint32_t originalTimeout = _timeout; setTimeout(1000); // use 1ms timeout for probes #if defined(I2C_EXTENDED_ADDRESS) diff --git a/I2CManager.h b/I2CManager.h index b1003e6..08d81d4 100644 --- a/I2CManager.h +++ b/I2CManager.h @@ -485,7 +485,7 @@ private: // When retries are enabled, the timeout applies to each // try, and failure from timeout does not get retried. // A value of 0 means disable timeout monitoring. - unsigned long _timeout = 100000UL; + uint32_t _timeout = 100000UL; // Finish off request block by waiting for completion and posting status. uint8_t finishRB(I2CRB *rb, uint8_t status); @@ -532,14 +532,15 @@ private: uint8_t bytesToSend = 0; uint8_t bytesToReceive = 0; uint8_t operation = 0; - unsigned long startTime = 0; + uint32_t startTime = 0; uint8_t muxPhase = 0; uint8_t muxAddress = 0; uint8_t muxData[1]; uint8_t deviceAddress; const uint8_t *sendBuffer; uint8_t *receiveBuffer; - + uint8_t transactionState = 0; + volatile uint32_t pendingClockSpeed = 0; void startTransaction(); diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h index fb5bae5..59bbcaf 100644 --- a/I2CManager_NonBlocking.h +++ b/I2CManager_NonBlocking.h @@ -172,6 +172,10 @@ void I2CManagerClass::startTransaction() { * Function to queue a request block and initiate operations. ***************************************************************************/ void I2CManagerClass::queueRequest(I2CRB *req) { + + if (((req->operation & OPERATION_MASK) == OPERATION_READ) && req->readLen == 0) + return; // Ignore null read + req->status = I2C_STATUS_PENDING; req->nextRequest = NULL; ATOMIC_BLOCK() { @@ -184,6 +188,7 @@ void I2CManagerClass::queueRequest(I2CRB *req) { } + /*************************************************************************** * Initiate a write to an I2C device (non-blocking operation) ***************************************************************************/ @@ -240,8 +245,8 @@ void I2CManagerClass::checkForTimeout() { I2CRB *t = queueHead; if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) { // Check for timeout - unsigned long elapsed = micros() - startTime; - if (elapsed > _timeout) { + int32_t elapsed = micros() - startTime; + if (elapsed > (int32_t)_timeout) { #ifdef DIAG_IO //DIAG(F("I2CManager Timeout on %s"), t->i2cAddress.toString()); #endif @@ -300,12 +305,12 @@ void I2CManagerClass::handleInterrupt() { // Check if current request has completed. If there's a current request // and state isn't active then state contains the completion status of the request. - if (state == I2C_STATE_COMPLETED && currentRequest != NULL) { + if (state == I2C_STATE_COMPLETED && currentRequest != NULL && currentRequest == queueHead) { // Operation has completed. if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES || currentRequest->operation & OPERATION_NORETRY) { - // Status is OK, or has failed and retry count exceeded, or retries disabled. + // Status is OK, or has failed and retry count exceeded, or failed and retries disabled. #if defined(I2C_EXTENDED_ADDRESS) if (muxPhase == MuxPhase_PROLOG ) { overallStatus = completionStatus; diff --git a/I2CManager_STM32.h b/I2CManager_STM32.h index a55fd2e..7e0f547 100644 --- a/I2CManager_STM32.h +++ b/I2CManager_STM32.h @@ -26,27 +26,44 @@ #include "I2CManager.h" #include "I2CManager_NonBlocking.h" // to satisfy intellisense -//#include -//#include #include +#include "stm32f4xx_hal_rcc.h" -/*************************************************************************** - * Interrupt handler. - * IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero - * compatible variants such as the Sparkfun SAMD21 Dev Breakout etc. - * Later we may wish to allow use of an alternate I2C bus, or more than one I2C - * bus on the SAMD architecture - ***************************************************************************/ +/***************************************************************************** + * STM32F4xx I2C native driver support + * + * Nucleo-64 and Nucleo-144 boards all use I2C1 as the default I2C peripheral + * Later we may wish to support other STM32 boards, allow use of an alternate + * I2C bus, or more than one I2C bus on the STM32 architecture + *****************************************************************************/ #if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32) -void I2C1_IRQHandler() { +#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \ + || defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) \ + || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) +// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely all Nucleo-64 +// and Nucleo-144 variants +I2C_TypeDef *s = I2C1; + +// In init we will ask the STM32 HAL layer for the configured APB1 clock frequency in Hz +uint32_t APB1clk1; // Peripheral Input Clock speed in Hz. +uint32_t i2c_MHz; // Peripheral Input Clock speed in MHz. + +// IRQ handler for I2C1, replacing the weak definition in the STM32 HAL +extern "C" void I2C1_EV_IRQHandler(void) { I2CManager.handleInterrupt(); } +extern "C" void I2C1_ER_IRQHandler(void) { + I2CManager.handleInterrupt(); +} +#else +#warning STM32 board selected is not yet supported - so I2C1 peripheral is not defined +#endif #endif -// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely Nucleo-64 variants -I2C_TypeDef *s = I2C1; -#define I2C_IRQn I2C1_EV_IRQn -#define I2C_BUSFREQ 16 +// Peripheral Input Clock speed in MHz. +// For STM32F446RE, the speed is 45MHz. Ideally, this should be determined +// at run-time from the APB1 clock, as it can vary from STM32 family to family. +// #define I2C_PERIPH_CLK 45 // I2C SR1 Status Register #1 bit definitions for convenience // #define I2C_SR1_SMBALERT (1<<15) // SMBus alert @@ -80,52 +97,66 @@ I2C_TypeDef *s = I2C1; // #define I2C_CR1_SMBUS (1<<1) // SMBus mode, 1=SMBus, 0=I2C // #define I2C_CR1_PE (1<<0) // I2C Peripheral enable +// States of the STM32 I2C driver state machine +enum {TS_IDLE,TS_START,TS_W_ADDR,TS_W_DATA,TS_W_STOP,TS_R_ADDR,TS_R_DATA,TS_R_STOP}; + + /*************************************************************************** * Set I2C clock speed register. This should only be called outside of * a transmission. The I2CManagerClass::_setClock() function ensures * that it is only called at the beginning of an I2C transaction. ***************************************************************************/ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) { - // Calculate a rise time appropriate to the requested bus speed - // Use 10x the rise time spec to enable integer divide of 62.5ns clock period + // Use 10x the rise time spec to enable integer divide of 50ns clock period uint16_t t_rise; uint32_t ccr_freq; - if (i2cClockSpeed < 200000L) { - // i2cClockSpeed = 100000L; - t_rise = 0x11; // (1000ns /62.5ns) + 1; - } - else if (i2cClockSpeed < 800000L) + + while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further + // writes to CR1 while STOP is being executed! + + // Disable the I2C device, as TRISE can only be programmed whilst disabled + s->CR1 &= ~(I2C_CR1_PE); // Disable I2C + s->CR1 |= I2C_CR1_SWRST; // reset the I2C + asm("nop"); // wait a bit... suggestion from online! + s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation + + if (i2cClockSpeed > 100000UL) { - i2cClockSpeed = 400000L; - t_rise = 0x06; // (300ns / 62.5ns) + 1; - // } else if (i2cClockSpeed < 1200000L) { - // i2cClockSpeed = 1000000L; - // t_rise = 120; + // if (i2cClockSpeed > 400000L) + // i2cClockSpeed = 400000L; + + t_rise = 300; // nanoseconds } else { - i2cClockSpeed = 100000L; - t_rise = 0x11; // (1000ns /62.5ns) + 1; + // i2cClockSpeed = 100000L; + t_rise = 1000; // nanoseconds } - - // Enable the I2C master mode - s->CR1 &= ~(I2C_CR1_PE); // Enable I2C - // Software reset the I2C peripheral - // s->CR1 |= I2C_CR1_SWRST; // reset the I2C - // Release reset - // s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation - - // Calculate baudrate - using a rise time appropriate for the speed - ccr_freq = I2C_BUSFREQ * 1000000 / i2cClockSpeed / 2; + // Configure the rise time register - max allowed tRISE is 1000ns, + // so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1. + s->TRISE = (t_rise * i2c_MHz / 1000) + 1; // Bit 15: I2C Master mode, 0=standard, 1=Fast Mode - // Bit 14: Duty, fast mode duty cycle - // Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns) - s->CCR = (uint16_t)ccr_freq; + // Bit 14: Duty, fast mode duty cycle (use 2:1) + // Bit 11-0: FREQR + // if (i2cClockSpeed > 400000UL) { + // // In fast mode plus, I2C period is 3 * CCR * TPCLK1. + // // s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value + // s->CCR = APB1clk1 / 3 / i2cClockSpeed; // Set I2C clockspeed to start! + // s->CCR |= 0xC000; // We need Fast Mode AND DUTY bits set + // } else { + // In standard and fast mode, I2C period is 2 * CCR * TPCLK1 + s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value + s->CCR |= (APB1clk1 / 2 / i2cClockSpeed); // Set I2C clockspeed to start! + // s->CCR |= (i2c_MHz * 500 / (i2cClockSpeed / 1000)); // Set I2C clockspeed to start! + // if (i2cClockSpeed > 100000UL) + // s->CCR |= 0xC000; // We need Fast Mode bits set as well + // } - // Configure the rise time register - s->TRISE = t_rise; // 1000 ns / 62.5 ns = 16 + 1 + // DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2); + // DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR); + // DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE); // Enable the I2C master mode s->CR1 |= I2C_CR1_PE; // Enable I2C @@ -136,32 +167,54 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) { ***************************************************************************/ void I2CManagerClass::I2C_init() { - //Setting up the clocks - RCC->APB1ENR |= (1<<21); // Enable I2C CLOCK - RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9 + // Query the clockspeed from the STM32 HAL layer + APB1clk1 = HAL_RCC_GetPCLK1Freq(); + i2c_MHz = APB1clk1 / 1000000UL; + // DIAG(F("I2C_init() peripheral clock speed is: %d"), i2c_MHz); + // Enable clocks + RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;//(1 << 21); // Enable I2C CLOCK + // Reset the I2C1 peripheral to initial state + RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST; + RCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST; // Standard I2C pins are SCL on PB8 and SDA on PB9 + RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9 // Bits (17:16)= 1:0 --> Alternate Function for Pin PB8; // Bits (19:18)= 1:0 --> Alternate Function for Pin PB9 + GPIOB->MODER &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all MODER bits for PB8 and PB9 GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2)); // PB8 and PB9 set to ALT function GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode + GPIOB->PUPDR &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all PUPDR bits for PB8 and PB9 GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability // Alt Function High register routing pins PB8 and PB9 for I2C1: // Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8 // Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9 + GPIOB->AFR[1] &= ~((15<<0) | (15<<4)); // Clear all AFR bits for PB8 on low nibble, PB9 on next nibble up GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up // Software reset the I2C peripheral + I2C1->CR1 &= ~I2C_CR1_PE; // Disable I2C1 peripheral s->CR1 |= I2C_CR1_SWRST; // reset the I2C - s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation + asm("nop"); // wait a bit... suggestion from online! + s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation - // Program the peripheral input clock in CR2 Register in order to generate correct timings - s->CR2 |= I2C_BUSFREQ; // PCLK1 FREQUENCY in MHz + // Clear all bits in I2C CR2 register except reserved bits + s->CR2 &= 0xE000; + + // Set I2C peripheral clock frequency + // s->CR2 |= I2C_PERIPH_CLK; + s->CR2 |= i2c_MHz; + // DIAG(F("I2C_init() peripheral clock is now: %d"), s->CR2); + + // set own address to 00 - not used in master mode + I2C1->OAR1 = (1 << 14); // bit 14 should be kept at 1 according to the datasheet #if defined(I2C_USE_INTERRUPTS) // Setting NVIC - NVIC_SetPriority(I2C_IRQn, 1); // Match default priorities - NVIC_EnableIRQ(I2C_IRQn); + NVIC_SetPriority(I2C1_EV_IRQn, 1); // Match default priorities + NVIC_EnableIRQ(I2C1_EV_IRQn); + NVIC_SetPriority(I2C1_ER_IRQn, 1); // Match default priorities + NVIC_EnableIRQ(I2C1_ER_IRQn); // CR2 Interrupt Settings // Bit 15-13: reserved @@ -172,23 +225,28 @@ void I2CManagerClass::I2C_init() // Bit 8: ITERREN - Error interrupt enable // Bit 7-6: reserved // Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz) - // s->CR2 |= 0x0700; // Enable Buffer, Event and Error interrupts - s->CR2 |= 0x0300; // Enable Event and Error interrupts + s->CR2 |= (I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable Buffer, Event and Error interrupts #endif + // DIAG(F("I2C_init() setting initial I2C clock to 100KHz")); // Calculate baudrate and set default rate for now // Configure the Clock Control Register for 100KHz SCL frequency // Bit 15: I2C Master mode, 0=standard, 1=Fast Mode // Bit 14: Duty, fast mode duty cycle - // Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns) - s->CCR = 0x0050; + // Bit 11-0: so CCR divisor would be clk / 2 / 100000 (where clk is in Hz) + // s->CCR = I2C_PERIPH_CLK * 5; + s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value + s->CCR |= (APB1clk1 / 2 / 100000UL); // Set a default of 100KHz I2C clockspeed to start! - // Configure the rise time register - max allowed in 1000ns - s->TRISE = 0x0011; // 1000 ns / 62.5 ns = 16 + 1 + // Configure the rise time register - max allowed is 1000ns, so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1. + s->TRISE = (1000 * i2c_MHz / 1000) + 1; + + // DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2); + // DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR); + // DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE); // Enable the I2C master mode s->CR1 |= I2C_CR1_PE; // Enable I2C - // Setting bus idle mode and wait for sync } /*************************************************************************** @@ -198,49 +256,30 @@ void I2CManagerClass::I2C_sendStart() { // Set counters here in case this is a retry. rxCount = txCount = 0; - uint8_t temp; - // On a single-master I2C bus, the start bit won't be sent until the bus - // state goes to IDLE so we can request it without waiting. On a - // multi-master bus, the bus may be BUSY under control of another master, + // On a single-master I2C bus, the start bit won't be sent until the bus + // state goes to IDLE so we can request it without waiting. On a + // multi-master bus, the bus may be BUSY under control of another master, // in which case we can avoid some arbitration failures by waiting until // the bus state is IDLE. We don't do that here. + //while (s->SR2 & I2C_SR2_BUSY) {} - // If anything to send, initiate write. Otherwise initiate read. - if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend)) - { - // Send start for read operation - s->CR1 |= I2C_CR1_ACK; // Enable the ACK - s->CR1 |= I2C_CR1_START; // Generate START - // Send address with read flag (1) or'd in - s->DR = (deviceAddress << 1) | 1; // send the address - while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set - // Special case for 1 byte reads! - if (bytesToReceive == 1) - { - s->CR1 &= ~I2C_CR1_ACK; // clear the ACK bit - temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition - s->CR1 |= I2C_CR1_STOP; // Stop I2C - } - else - temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit - } - else { - // Send start for write operation - s->CR1 |= I2C_CR1_ACK; // Enable the ACK - s->CR1 |= I2C_CR1_START; // Generate START - // Send address with write flag (0) or'd in - s->DR = (deviceAddress << 1) | 0; // send the address - while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set - temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit - } + // Check there's no STOP still in progress. If we OR the START bit into CR1 + // and the STOP bit is already set, we could output multiple STOP conditions. + while (s->CR1 & I2C_CR1_STOP) {} // Wait for STOP bit to reset + + s->CR2 |= (I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable interrupts + s->CR2 &= ~I2C_CR2_ITBUFEN; // Don't enable buffer interupts yet. + s->CR1 &= ~I2C_CR1_POS; // Clear the POS bit + s->CR1 |= (I2C_CR1_ACK | I2C_CR1_START); // Enable the ACK and generate START + transactionState = TS_START; } /*************************************************************************** * Initiate a stop bit for transmission (does not interrupt) ***************************************************************************/ void I2CManagerClass::I2C_sendStop() { - s->CR1 |= I2C_CR1_STOP; // Stop I2C + s->CR1 |= I2C_CR1_STOP; // Stop I2C } /*************************************************************************** @@ -252,9 +291,11 @@ void I2CManagerClass::I2C_close() { s->CR1 &= ~I2C_CR1_PE; // Disable I2C peripheral // Should never happen, but wait for up to 500us only. unsigned long startTime = micros(); - while ((s->CR1 && I2C_CR1_PE) != 0) { - if (micros() - startTime >= 500UL) break; + while ((s->CR1 & I2C_CR1_PE) != 0) { + if ((int32_t)(micros() - startTime) >= 500) break; } + NVIC_DisableIRQ(I2C1_EV_IRQn); + NVIC_DisableIRQ(I2C1_ER_IRQn); } /*************************************************************************** @@ -263,50 +304,217 @@ void I2CManagerClass::I2C_close() { * (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()). ***************************************************************************/ void I2CManagerClass::I2C_handleInterrupt() { + volatile uint16_t temp_sr1, temp_sr2; - if (s->SR1 && I2C_SR1_ARLO) { - // Arbitration lost, restart - I2C_sendStart(); // Reinitiate request - } else if (s->SR1 && I2C_SR1_BERR) { - // Bus error - completionStatus = I2C_STATUS_BUS_ERROR; - state = I2C_STATE_COMPLETED; - } else if (s->SR1 && I2C_SR1_TXE) { - // Master write completed - if (s->SR1 && (1<<10)) { - // Nacked, send stop. - I2C_sendStop(); + temp_sr1 = s->SR1; + + // Check for errors first + if (temp_sr1 & (I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR)) { + // Check which error flag is set + if (temp_sr1 & I2C_SR1_AF) + { + s->SR1 &= ~(I2C_SR1_AF); // Clear AF + I2C_sendStop(); // Clear the bus + transactionState = TS_IDLE; completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE; state = I2C_STATE_COMPLETED; - } else if (bytesToSend) { - // Acked, so send next byte - s->DR = sendBuffer[txCount++]; - bytesToSend--; - } else if (bytesToReceive) { - // Last sent byte acked and no more to send. Send repeated start, address and read bit. - // s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1; - } else { - // Check both TxE/BTF == 1 before generating stop - while (!(s->SR1 && I2C_SR1_TXE)); // Check TxE - while (!(s->SR1 && I2C_SR1_BTF)); // Check BTF - // No more data to send/receive. Initiate a STOP condition and finish - I2C_sendStop(); + } + else if (temp_sr1 & I2C_SR1_ARLO) + { + // Arbitration lost, restart + s->SR1 &= ~(I2C_SR1_ARLO); // Clear ARLO + I2C_sendStart(); // Reinitiate request + transactionState = TS_START; + } + else if (temp_sr1 & I2C_SR1_BERR) + { + // Bus error + s->SR1 &= ~(I2C_SR1_BERR); // Clear BERR + I2C_sendStop(); // Clear the bus + transactionState = TS_IDLE; + completionStatus = I2C_STATUS_BUS_ERROR; state = I2C_STATE_COMPLETED; } - } else if (s->SR1 && I2C_SR1_RXNE) { - // Master read completed without errors - if (bytesToReceive == 1) { -// s->I2CM.CTRLB.bit.ACKACT = 1; // NAK final byte - I2C_sendStop(); // send stop - receiveBuffer[rxCount++] = s->DR; // Store received byte - bytesToReceive = 0; - state = I2C_STATE_COMPLETED; - } else if (bytesToReceive) { -// s->I2CM.CTRLB.bit.ACKACT = 0; // ACK all but final byte - receiveBuffer[rxCount++] = s->DR; // Store received byte - bytesToReceive--; + } + else { + // No error flags, so process event according to current state. + switch (transactionState) { + case TS_START: + if (temp_sr1 & I2C_SR1_SB) { + // Event EV5 + // Start bit has been sent successfully and we have the bus. + // If anything to send, initiate write. Otherwise initiate read. + if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend)) { + // Send address with read flag (1) or'd in + s->DR = (deviceAddress << 1) | 1; // send the address + transactionState = TS_R_ADDR; + } else { + // Send address with write flag (0) or'd in + s->DR = (deviceAddress << 1) | 0; // send the address + transactionState = TS_W_ADDR; + } + } + // SB bit is cleared by writing to DR (already done). + break; + + case TS_W_ADDR: + if (temp_sr1 & I2C_SR1_ADDR) { + temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit + // Event EV6 + // Address sent successfully, device has ack'd in response. + if (!bytesToSend) { + I2C_sendStop(); + transactionState = TS_IDLE; + completionStatus = I2C_STATUS_OK; + state = I2C_STATE_COMPLETED; + } else { + // Put one byte into DR to load shift register. + s->DR = sendBuffer[txCount++]; + bytesToSend--; + if (bytesToSend) { + // Put another byte to load DR + s->DR = sendBuffer[txCount++]; + bytesToSend--; + } + if (!bytesToSend) { + // No more bytes to send. + // The TXE interrupt occurs when the DR is empty, and the BTF interrupt + // occurs when the shift register is also empty (one character later). + // To avoid repeated TXE interrupts during this time, we disable TXE interrupt. + s->CR2 &= ~I2C_CR2_ITBUFEN; // Wait for BTF interrupt, disable TXE interrupt + transactionState = TS_W_STOP; + } else { + // More data remaining to send after this interrupt, enable TXE interrupt. + s->CR2 |= I2C_CR2_ITBUFEN; + transactionState = TS_W_DATA; + } + } + } + break; + + case TS_W_DATA: + if (temp_sr1 & I2C_SR1_TXE) { + // Event EV8_1/EV8 + // Transmitter empty, write a byte to it. + if (bytesToSend) { + s->DR = sendBuffer[txCount++]; + bytesToSend--; + if (!bytesToSend) { + s->CR2 &= ~I2C_CR2_ITBUFEN; // Disable TXE interrupt + transactionState = TS_W_STOP; + } + } + } + break; + + case TS_W_STOP: + if (temp_sr1 & I2C_SR1_BTF) { + // Event EV8_2 + // Done, last character sent. Anything to receive? + if (bytesToReceive) { + I2C_sendStart(); + // NOTE: Three redundant BTF interrupts take place between the + // first BTF interrupt and the START interrupt. I've tried all sorts + // of ways to eliminate them, and the only thing that worked for + // me was to loop until the BTF bit becomes reset. Either way, + // it's a waste of processor time. Anyone got a solution? + //while (s->SR1 && I2C_SR1_BTF) {} + transactionState = TS_START; + } else { + I2C_sendStop(); + transactionState = TS_IDLE; + completionStatus = I2C_STATUS_OK; + state = I2C_STATE_COMPLETED; + } + s->SR1 &= I2C_SR1_BTF; // Clear BTF interrupt + } + break; + + case TS_R_ADDR: + if (temp_sr1 & I2C_SR1_ADDR) { + // Event EV6 + // Address sent for receive. + // The next bit is different depending on whether there are + // 1 byte, 2 bytes or >2 bytes to be received, in accordance with the + // Programmers Reference RM0390. + if (bytesToReceive == 1) { + // Receive 1 byte + s->CR1 &= ~I2C_CR1_ACK; // Disable ack + temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit + // Next step will occur after a RXNE interrupt, so enable it + s->CR2 |= I2C_CR2_ITBUFEN; + transactionState = TS_R_STOP; + } else if (bytesToReceive == 2) { + // Receive 2 bytes + s->CR1 &= ~I2C_CR1_ACK; // Disable ACK for final byte + s->CR1 |= I2C_CR1_POS; // set POS flag to delay effect of ACK flag + // Next step will occur after a BTF interrupt, so disable RXNE interrupt + s->CR2 &= ~I2C_CR2_ITBUFEN; + temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit + transactionState = TS_R_STOP; + } else { + // >2 bytes, just wait for bytes to come in and ack them for the time being + // (ack flag has already been set). + // Next step will occur after a BTF interrupt, so disable RXNE interrupt + s->CR2 &= ~I2C_CR2_ITBUFEN; + temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit + transactionState = TS_R_DATA; + } + } + break; + + case TS_R_DATA: + // Event EV7/EV7_1 + if (temp_sr1 & I2C_SR1_BTF) { + // Byte received in receiver - read next byte + if (bytesToReceive == 3) { + // Getting close to the last byte, so a specific sequence is recommended. + s->CR1 &= ~I2C_CR1_ACK; // Reset ack for next byte received. + transactionState = TS_R_STOP; + } + receiveBuffer[rxCount++] = s->DR; // Store received byte + bytesToReceive--; + } + break; + + case TS_R_STOP: + if (temp_sr1 & I2C_SR1_BTF) { + // Event EV7 (last one) + // When we've got here, the receiver has got the last two bytes + // (or one byte, if only one byte is being received), + // and NAK has already been sent, so we need to read from the receiver. + if (bytesToReceive) { + if (bytesToReceive > 1) + I2C_sendStop(); + while(bytesToReceive) { + receiveBuffer[rxCount++] = s->DR; // Store received byte(s) + bytesToReceive--; + } + // Finish. + transactionState = TS_IDLE; + completionStatus = I2C_STATUS_OK; + state = I2C_STATE_COMPLETED; + } + } else if (temp_sr1 & I2C_SR1_RXNE) { + if (bytesToReceive == 1) { + // One byte on a single-byte transfer. Ack has already been set. + I2C_sendStop(); + receiveBuffer[rxCount++] = s->DR; // Store received byte + bytesToReceive--; + // Finish. + transactionState = TS_IDLE; + completionStatus = I2C_STATUS_OK; + state = I2C_STATE_COMPLETED; + } else + s->SR1 &= I2C_SR1_RXNE; // Acknowledge interrupt + } + break; } + // If we've received an interrupt at any other time, we're not interested so clear it + // to prevent it recurring ad infinitum. + s->SR1 = 0; } + } #endif /* I2CMANAGER_STM32_H */ diff --git a/IODevice.cpp b/IODevice.cpp index 2ed21b6..e811fff 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -176,6 +176,13 @@ bool IODevice::exists(VPIN vpin) { return findDevice(vpin) != NULL; } +// Return the status of the device att vpin. +uint8_t IODevice::getStatus(VPIN vpin) { + IODevice *dev = findDevice(vpin); + if (!dev) return false; + return dev->_deviceState; +} + // check whether the pin supports notification. If so, then regular _read calls are not required. bool IODevice::hasCallback(VPIN vpin) { IODevice *dev = findDevice(vpin); diff --git a/IODevice.h b/IODevice.h index 769e111..d12fafd 100644 --- a/IODevice.h +++ b/IODevice.h @@ -27,12 +27,6 @@ // Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported //#define DIAG_LOOPTIMES -// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required -// 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) -#define IO_NO_HAL -#endif - // Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an // animation has completed. This switches off the servo motor, preventing // the continuous buzz sometimes found on servos, and reducing the @@ -160,6 +154,9 @@ public: // exists checks whether there is a device owning the specified vpin static bool exists(VPIN vpin); + // getStatus returns the state of the device at the specified vpin + static uint8_t getStatus(VPIN vpin); + // Enable shared interrupt on specified pin for GPIO extender modules. The extender module // should pull down this pin when requesting a scan. The pin may be shared by multiple modules. // Without the shared interrupt, input states are scanned periodically to detect changes on @@ -383,6 +380,7 @@ private: uint8_t *_pinInUse; }; +#ifndef IO_NO_HAL ///////////////////////////////////////////////////////////////////////////////////////////////////// /* * IODevice subclass for EX-Turntable. @@ -411,10 +409,14 @@ private: void _begin() override; void _loop(unsigned long currentMicros) override; int _read(VPIN vpin) override; + void _broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity); void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override; void _display() override; uint8_t _stepperStatus; + uint8_t _previousStatus; + uint8_t _currentActivity; }; +#endif ///////////////////////////////////////////////////////////////////////////////////////////////////// @@ -540,8 +542,10 @@ protected: #include "IO_MCP23017.h" #include "IO_PCF8574.h" #include "IO_PCF8575.h" +#include "IO_PCA9555.h" #include "IO_duinoNodes.h" #include "IO_EXIOExpander.h" +#include "IO_trainbrains.h" #endif // iodevice_h diff --git a/IO_EXTurntable.h b/IO_EXTurntable.cpp similarity index 75% rename from IO_EXTurntable.h rename to IO_EXTurntable.cpp index 29ce679..aeb935b 100644 --- a/IO_EXTurntable.h +++ b/IO_EXTurntable.cpp @@ -20,20 +20,21 @@ /* * The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C. * -* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/Turntable-EX) and contains the stepper motor logic. +* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/EX-Turntable) and contains the stepper motor logic. * -* This device driver sends a step position to Turntable-EX to indicate the step position to move to using either of these commands: +* This device driver sends a step position to EX-Turntable to indicate the step position to move to using either of these commands: * in the serial console * MOVETT(vpin, steps, activity) in EX-RAIL * Refer to the documentation for further information including the valid activities. */ -#ifndef IO_EXTurntable_h -#define IO_EXTurntable_h - #include "IODevice.h" #include "I2CManager.h" #include "DIAG.h" +#include "Turntables.h" +#include "CommandDistributor.h" + +#ifndef IO_NO_HAL void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) { new EXTurntable(firstVpin, nPins, I2CAddress); @@ -44,6 +45,8 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) { _firstVpin = firstVpin; _nPins = nPins; _I2CAddress = I2CAddress; + _stepperStatus = 0; + _previousStatus = 0; addDevice(this); } @@ -51,6 +54,7 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) { void EXTurntable::_begin() { I2CManager.begin(); if (I2CManager.exists(_I2CAddress)) { + DIAG(F("EX-Turntable device found, I2C:%s"), _I2CAddress.toString()); #ifdef DIAG_IO _display(); #endif @@ -67,15 +71,19 @@ void EXTurntable::_loop(unsigned long currentMicros) { uint8_t readBuffer[1]; I2CManager.read(_I2CAddress, readBuffer, 1); _stepperStatus = readBuffer[0]; - // DIAG(F("Turntable-EX returned status: %d"), _stepperStatus); - delayUntil(currentMicros + 500000); // Wait 500ms before checking again, turntables turn slowly + if (_stepperStatus != _previousStatus && _stepperStatus == 0) { // Broadcast when a rotation finishes + if ( _currentActivity < 4) { + _broadcastStatus(_firstVpin, _stepperStatus, _currentActivity); + } + _previousStatus = _stepperStatus; + } + delayUntil(currentMicros + 100000); // Wait 100ms before checking again } // Read returns status as obtained in our loop. // Return false if our status value is invalid. int EXTurntable::_read(VPIN vpin) { if (_deviceState == DEVSTATE_FAILED) return 0; - // DIAG(F("_read status: %d"), _stepperStatus); if (_stepperStatus > 1) { return false; } else { @@ -83,6 +91,17 @@ int EXTurntable::_read(VPIN vpin) { } } +// If a status change has occurred for a turntable object, broadcast it +void EXTurntable::_broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity) { + Turntable *tto = Turntable::getByVpin(vpin); + if (tto) { + if (activity < 4) { + tto->setMoving(status); + CommandDistributor::broadcastTurntable(tto->getId(), tto->getPosition(), status); + } + } +} + // writeAnalogue to send the steps and activity to Turntable-EX. // Sends 3 bytes containing the MSB and LSB of the step count, and activity. // value contains the steps, bit shifted to MSB + LSB. @@ -100,6 +119,7 @@ int EXTurntable::_read(VPIN vpin) { // Acc_Off = 9 // Turn accessory pin off void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) { if (_deviceState == DEVSTATE_FAILED) return; + if (value < 0) return; uint8_t stepsMSB = value >> 8; uint8_t stepsLSB = value & 0xFF; #ifdef DIAG_IO @@ -108,7 +128,10 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_ DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"), _I2CAddress.toString(), stepsMSB, stepsLSB, activity); #endif - _stepperStatus = 1; // Tell the device driver Turntable-EX is busy + if (activity < 4) _stepperStatus = 1; // Tell the device driver Turntable-EX is busy + _previousStatus = _stepperStatus; + _currentActivity = activity; + _broadcastStatus(vpin, _stepperStatus, activity); // Broadcast when the rotation starts I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity); } diff --git a/IO_PCA9555.h b/IO_PCA9555.h index 137e287..75f2c38 100644 --- a/IO_PCA9555.h +++ b/IO_PCA9555.h @@ -30,20 +30,19 @@ class PCA9555 : public GPIOBase { public: - static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) { - new PCA9555(vpin, min(nPins,16), I2CAddress, interruptPin); + static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9555(vpin,nPins, i2cAddress, interruptPin); } - + +private: // Constructor - PCA9555(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) + PCA9555(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1) : GPIOBase((FSH *)F("PCA9555"), vpin, nPins, I2CAddress, interruptPin) { requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer), outputBuffer, sizeof(outputBuffer)); outputBuffer[0] = REG_INPUT_P0; } - -private: void _writeGpioPort() override { I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8); } diff --git a/IO_trainbrains.h b/IO_trainbrains.h new file mode 100644 index 0000000..058fe02 --- /dev/null +++ b/IO_trainbrains.h @@ -0,0 +1,98 @@ +/* + * © 2023, Chris Harlow. All rights reserved. + * © 2021, Neil McKechnie. All rights reserved. + * + * This file is part of DCC++EX API + * + * 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 . + */ + +#ifndef io_trainbrains_h +#define io_trainbrains_h + +#include "IO_GPIOBase.h" +#include "FSH.h" + +///////////////////////////////////////////////////////////////////////////////////////////////////// +/* + * IODevice subclass for trainbrains 3-block occupancy detector. + * For details see http://trainbrains.eu + */ + + enum TrackUnoccupancy +{ + TRACK_UNOCCUPANCY_UNKNOWN = 0, + TRACK_OCCUPIED = 1, + TRACK_UNOCCUPIED = 2 +}; + +class Trainbrains02 : public GPIOBase { +public: + static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new Trainbrains02(vpin, nPins, i2cAddress); + } + +private: + // Constructor + Trainbrains02(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) + : GPIOBase((FSH *)F("Trainbrains02"), vpin, nPins, i2cAddress, interruptPin) + { + requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer), + outputBuffer, sizeof(outputBuffer)); + + outputBuffer[0] = (uint8_t)_I2CAddress; // strips away the mux part. + outputBuffer[1] =14; + outputBuffer[2] =1; + outputBuffer[3] =0; // This is the channel updated at each poling call + outputBuffer[4] =0; + outputBuffer[5] =0; + outputBuffer[6] =0; + outputBuffer[7] =0; + outputBuffer[8] =0; + outputBuffer[9] =0; + } + + void _writeGpioPort() override {} + + void _readGpioPort(bool immediate) override { + // cycle channel on device each time + outputBuffer[3]=channelInProgress+1; // 1-origin + channelInProgress++; + if(channelInProgress>=_nPins) channelInProgress=0; + + if (immediate) { + _processCompletion(I2CManager.read(_I2CAddress, inputBuffer, sizeof(inputBuffer), + outputBuffer, sizeof(outputBuffer))); + } else { + // Queue new request + requestBlock.wait(); // Wait for preceding operation to complete + // Issue new request to read GPIO register + I2CManager.queueRequest(&requestBlock); + } + } + + // This function is invoked when an I/O operation on the requestBlock completes. + void _processCompletion(uint8_t status) override { + if (status != I2C_STATUS_OK) inputBuffer[6]=TRACK_UNOCCUPANCY_UNKNOWN; + if (inputBuffer[6] == TRACK_UNOCCUPIED ) _portInputState |= 0x01 < POWER_SAMPLE_RETRY_MAX) - power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX; + power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX; + #ifdef EXRAIL_ACTIVE + DIAG(F("Calling EXRAIL")); + RMFT2::powerEvent(trackno, true); // Tell EXRAIL we have an overload + #endif // power on test DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc); setPower(POWERMODE::ALERT); diff --git a/MotorDriver.h b/MotorDriver.h index 21bceb6..07ff93f 100644 --- a/MotorDriver.h +++ b/MotorDriver.h @@ -1,9 +1,9 @@ /* - * © 2022 Paul M Antoine + * © 2022-2023 Paul M. Antoine * © 2021 Mike S * © 2021 Fred Decker * © 2020 Chris Harlow - * © 2022 Harald Barth + * © 2022,2023 Harald Barth * All rights reserved. * * This file is part of CommandStation-EX @@ -28,8 +28,15 @@ #include "DCCTimer.h" // use powers of two so we can do logical and/or on the track modes in if clauses. +// RACK_MODE_DCX is (TRACK_MODE_DC|TRACK_MODE_INV) +template inline T operator~ (T a) { return (T)~(int)a; } +template inline T operator| (T a, T b) { return (T)((int)a | (int)b); } +template inline T operator& (T a, T b) { return (T)((int)a & (int)b); } +template inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); } enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4, - TRACK_MODE_DC = 8, TRACK_MODE_DCX = 16, TRACK_MODE_EXT = 32}; + TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16, TRACK_MODE_BOOST = 32, + TRACK_MODE_ALL = 62, // only to operate all tracks + TRACK_MODE_INV = 64, TRACK_MODE_DCX = 72 /*DC + INV*/, TRACK_MODE_AUTOINV = 128}; #define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH #define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW @@ -60,6 +67,16 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO #define HAVE_PORTB(X) X #define PORTC GPIOC->ODR #define HAVE_PORTC(X) X +#define PORTD GPIOD->ODR +#define HAVE_PORTD(X) X +#if defined(GPIOE) +#define PORTE GPIOE->ODR +#define HAVE_PORTE(X) X +#endif +#if defined(GPIOF) +#define PORTF GPIOF->ODR +#define HAVE_PORTF(X) X +#endif #endif // if macros not defined as pass-through we define @@ -74,6 +91,15 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO #ifndef HAVE_PORTC #define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0 #endif +#ifndef HAVE_PORTD +#define HAVE_PORTD(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0 +#endif +#ifndef HAVE_PORTE +#define HAVE_PORTE(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0 +#endif +#ifndef HAVE_PORTF +#define HAVE_PORTF(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0 +#endif // Virtualised Motor shield 1-track hardware Interface @@ -110,6 +136,9 @@ struct FASTPIN { extern volatile portreg_t shadowPORTA; extern volatile portreg_t shadowPORTB; extern volatile portreg_t shadowPORTC; +extern volatile portreg_t shadowPORTD; +extern volatile portreg_t shadowPORTE; +extern volatile portreg_t shadowPORTF; enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT }; @@ -126,7 +155,11 @@ class MotorDriver { // otherwise the call from interrupt context can undo whatever we do // from outside interrupt 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) + high = !high; +#endif if (trackPWM) { DCCTimer::setPWM(signalPin,high); } @@ -146,6 +179,12 @@ class MotorDriver { pinMode(signalPin, OUTPUT); else pinMode(signalPin, INPUT); + if (signalPin2 != UNUSED_PIN) { + if (on) + pinMode(signalPin2, OUTPUT); + else + pinMode(signalPin2, INPUT); + } }; inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); }; void setDCSignal(byte speedByte); @@ -163,16 +202,16 @@ class MotorDriver { unsigned int raw2mA( int raw); unsigned int mA2raw( unsigned int mA); inline bool brakeCanPWM() { -#if defined(ARDUINO_ARCH_ESP32) || defined(__arm__) - // TODO: on ARM we can use digitalPinHasPWM, and may wish/need to - return true; -#else -#ifdef digitalPinToTimer +#if defined(ARDUINO_ARCH_ESP32) + return (brakePin != UNUSED_PIN); // This was just (true) but we probably do need to check for UNUSED_PIN! +#elif defined(__arm__) + // On ARM we can use digitalPinHasPWM + return ((brakePin!=UNUSED_PIN) && (digitalPinHasPWM(brakePin))); +#elif defined(digitalPinToTimer) return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin))); #else return (brakePin<14 && brakePin >1); -#endif //digitalPinToTimer -#endif //ESP32/ARM +#endif } inline int getRawCurrentTripValue() { return rawCurrentTripValue; @@ -210,6 +249,32 @@ class MotorDriver { #endif inline void setMode(TRACK_MODE m) { trackMode = m; + invertOutput(trackMode & TRACK_MODE_INV); + }; + inline void invertOutput() { // toggles output inversion + invertPhase = !invertPhase; + invertOutput(invertPhase); + }; + inline void invertOutput(bool b) { // sets output inverted or not + if (b) + invertPhase = 1; + else + invertPhase = 0; +#if defined(ARDUINO_ARCH_ESP32) + pinpair p = getSignalPin(); + uint32_t *outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.pin); + if (invertPhase) // set or clear the invert bit in the gpio out register + *outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S); + else + *outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S); + if (p.invpin != UNUSED_PIN) { + outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.invpin); + if (invertPhase) // clear or set the invert bit in the gpio out register + *outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S); + else + *outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S); + } +#endif }; inline TRACK_MODE getMode() { return trackMode; @@ -241,7 +306,7 @@ class MotorDriver { bool invertBrake; // brake pin passed as negative means pin is inverted bool invertPower; // power pin passed as negative means pin is inverted bool invertFault; // fault pin passed as negative means pin is inverted - + bool invertPhase = 0; // phase of out pin is inverted // Raw to milliamp conversion factors avoiding float data types. // Milliamps=rawADCreading * sensefactorInternal / senseScale // diff --git a/SerialManager.cpp b/SerialManager.cpp index bacfceb..88bc7cd 100644 --- a/SerialManager.cpp +++ b/SerialManager.cpp @@ -111,14 +111,15 @@ void SerialManager::loop2() { bufferLength = 0; buffer[0] = '\0'; } - else if (ch == '>') { - buffer[bufferLength] = '\0'; - DCCEXParser::parse(serial, buffer, NULL); - inCommandPayload = false; - break; - } - else if (inCommandPayload) { - if (bufferLength < (COMMAND_BUFFER_SIZE-1)) buffer[bufferLength++] = ch; + else if (inCommandPayload) { + if (bufferLength < (COMMAND_BUFFER_SIZE-1)) + buffer[bufferLength++] = ch; + if (ch == '>') { + buffer[bufferLength] = '\0'; + DCCEXParser::parse(serial, buffer, NULL); + inCommandPayload = false; + break; + } } } diff --git a/StringFormatter.cpp b/StringFormatter.cpp index c475ef0..9c69877 100644 --- a/StringFormatter.cpp +++ b/StringFormatter.cpp @@ -19,6 +19,7 @@ #include "StringFormatter.h" #include #include "DisplayInterface.h" +#include "CommandDistributor.h" bool Diag::ACK=false; bool Diag::CMD=false; @@ -38,13 +39,28 @@ void StringFormatter::diag( const FSH* input...) { void StringFormatter::lcd(byte row, const FSH* input...) { va_list args; - +#ifndef DISABLE_VDPY + Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row); +#else + Print * virtualLCD=NULL; +#endif // Issue the LCD as a diag first - send(&USB_SERIAL,F("<* LCD%d:"),row); - va_start(args, input); - send2(&USB_SERIAL,input,args); - send(&USB_SERIAL,F(" *>\n")); + // Unless the same serial is asking for the virtual @ respomnse + if (virtualLCD!=&USB_SERIAL) { + send(&USB_SERIAL,F("<* LCD%d:"),row); + va_start(args, input); + send2(&USB_SERIAL,input,args); + send(&USB_SERIAL,F(" *>\n")); + } +#ifndef DISABLE_VDPY + // send to virtual LCD collector (if any) + if (virtualLCD) { + va_start(args, input); + send2(virtualLCD,input,args); + CommandDistributor::commitVirtualLCDSerial(); + } +#endif DisplayInterface::setRow(row); va_start(args, input); send2(DisplayInterface::getDisplayHandler(),input,args); @@ -52,6 +68,16 @@ void StringFormatter::lcd(byte row, const FSH* input...) { void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) { va_list args; + + // send to virtual LCD collector (if any) +#ifndef DISABLE_VDPY + Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row); + if (virtualLCD) { + va_start(args, input); + send2(virtualLCD,input,args); + CommandDistributor::commitVirtualLCDSerial(); + } +#endif DisplayInterface::setRow(display, row); va_start(args, input); @@ -117,6 +143,7 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) { case 'o': stream->print(va_arg(args, int), OCT); break; case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break; case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break; + case 'h': printHex(stream,(unsigned int)va_arg(args, unsigned int)); break; case 'M': { // this prints a unsigned long microseconds time in readable format unsigned long time = va_arg(args, long); @@ -218,4 +245,14 @@ void StringFormatter::printPadded(Print* stream, long value, byte width, bool fo if (!formatLeft) stream->print(value, DEC); } - +// printHex prints the full 2 byte hex with leading zeros, unlike print(value,HEX) +const char FLASH hexchars[]="0123456789ABCDEF"; +void StringFormatter::printHex(Print * stream,uint16_t value) { + char result[5]; + for (int i=3;i>=0;i--) { + result[i]=GETFLASH(hexchars+(value & 0x0F)); + value>>=4; + } + result[4]='\0'; + stream->print(result); +} diff --git a/StringFormatter.h b/StringFormatter.h index 6923c10..25d15e2 100644 --- a/StringFormatter.h +++ b/StringFormatter.h @@ -49,10 +49,10 @@ class StringFormatter static void lcd2(uint8_t display, byte row, const FSH* input...); static void printEscapes(char * input); static void printEscape( char c); + static void printHex(Print * stream,uint16_t value); private: static void send2(Print * serial, const FSH* input,va_list args); static void printPadded(Print* stream, long value, byte width, bool formatLeft); - }; #endif diff --git a/TrackManager.cpp b/TrackManager.cpp index db5b6b9..7d2b36b 100644 --- a/TrackManager.cpp +++ b/TrackManager.cpp @@ -1,6 +1,7 @@ /* * © 2022 Chris Harlow - * © 2022 Harald Barth + * © 2022,2023 Harald Barth + * © 2023 Colin Murdoch * All rights reserved. * * This file is part of DCC++EX @@ -25,7 +26,8 @@ #include "MotorDriver.h" #include "DCCTimer.h" #include "DIAG.h" -#include"CommandDistributor.h" +#include "CommandDistributor.h" +#include "DCCEXParser.h" // Virtualised Motor shield multi-track hardware Interface #define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++) @@ -43,17 +45,21 @@ 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]; int16_t TrackManager::trackDCAddr[MAX_TRACKS]; -POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF; byte TrackManager::lastTrack=0; bool TrackManager::progTrackSyncMain=false; bool TrackManager::progTrackBoosted=false; int16_t TrackManager::joinRelay=UNUSED_PIN; #ifdef ARDUINO_ARCH_ESP32 -byte TrackManager::tempProgTrack=MAX_TRACKS+1; +byte TrackManager::tempProgTrack=MAX_TRACKS+1; // MAX_TRACKS+1 is the unused flag #endif #ifdef ANALOG_READ_INTERRUPT @@ -85,7 +91,7 @@ void TrackManager::sampleCurrent() { if (!waiting) { // look for a valid track to sample or until we are around while (true) { - if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_DCX|TRACK_MODE_EXT )) { + if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT )) { track[tr]->startCurrentFromHW(); // for scope debug track[1]->setBrake(1); waiting = true; @@ -153,10 +159,16 @@ void TrackManager::setDCCSignal( bool on) { HAVE_PORTA(shadowPORTA=PORTA); HAVE_PORTB(shadowPORTB=PORTB); HAVE_PORTC(shadowPORTC=PORTC); + HAVE_PORTD(shadowPORTD=PORTD); + HAVE_PORTE(shadowPORTE=PORTE); + HAVE_PORTF(shadowPORTF=PORTF); APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on)); HAVE_PORTA(PORTA=shadowPORTA); HAVE_PORTB(PORTB=shadowPORTB); HAVE_PORTC(PORTC=shadowPORTC); + HAVE_PORTD(PORTD=shadowPORTD); + HAVE_PORTE(PORTE=shadowPORTE); + HAVE_PORTF(PORTF=shadowPORTF); } void TrackManager::setCutout( bool on) { @@ -171,10 +183,16 @@ void TrackManager::setPROGSignal( bool on) { HAVE_PORTA(shadowPORTA=PORTA); HAVE_PORTB(shadowPORTB=PORTB); HAVE_PORTC(shadowPORTC=PORTC); + HAVE_PORTD(shadowPORTD=PORTD); + HAVE_PORTE(shadowPORTE=PORTE); + HAVE_PORTF(shadowPORTF=PORTF); APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on)); HAVE_PORTA(PORTA=shadowPORTA); HAVE_PORTB(PORTB=shadowPORTB); HAVE_PORTC(PORTC=shadowPORTC); + HAVE_PORTD(PORTD=shadowPORTD); + HAVE_PORTE(PORTE=shadowPORTE); + HAVE_PORTF(PORTF=shadowPORTF); } // setDCSignal(), called from normal context @@ -182,18 +200,21 @@ void TrackManager::setPROGSignal( bool on) { // with interrupts turned off around the critical section void TrackManager::setDCSignal(int16_t cab, byte speedbyte) { FOR_EACH_TRACK(t) { - if (trackDCAddr[t]!=cab) continue; - if (track[t]->getMode()==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte); - else if (track[t]->getMode()==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128); + if (trackDCAddr[t]!=cab && cab != 0) continue; + if (track[t]->getMode() & TRACK_MODE_DC) + track[t]->setDCSignal(speedbyte); } } bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) { 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); // DC tracks require a motorDriver that can set brake! - if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) { + if (mode & TRACK_MODE_DC) { #if defined(ARDUINO_AVR_UNO) DIAG(F("Uno has no PWM timers available for DC")); return false; @@ -209,25 +230,41 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr pinpair p = track[trackToSet]->getSignalPin(); //DIAG(F("Track=%c remove pin %d"),trackToSet+'A', p.pin); gpio_reset_pin((gpio_num_t)p.pin); - pinMode(p.pin, OUTPUT); // gpio_reset_pin may reset to input if (p.invpin != UNUSED_PIN) { //DIAG(F("Track=%c remove ^pin %d"),trackToSet+'A', p.invpin); gpio_reset_pin((gpio_num_t)p.invpin); - pinMode(p.invpin, OUTPUT); // gpio_reset_pin may reset to input } +#ifdef BOOSTER_INPUT + if (mode & TRACK_MODE_BOOST) { + //DIAG(F("Track=%c mode boost pin %d"),trackToSet+'A', p.pin); + pinMode(BOOSTER_INPUT, INPUT); + gpio_matrix_in(26, SIG_IN_FUNC228_IDX, false); //pads 224 to 228 available as loopback + gpio_matrix_out(p.pin, SIG_IN_FUNC228_IDX, false, false); + if (p.invpin != UNUSED_PIN) { + gpio_matrix_out(p.invpin, SIG_IN_FUNC228_IDX, true /*inverted*/, false); + } + } else // elseif clause continues +#endif + if (mode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_DC)) { + // gpio_reset_pin may reset to input + pinMode(p.pin, OUTPUT); + if (p.invpin != UNUSED_PIN) + pinMode(p.invpin, OUTPUT); + } + #endif #ifndef DISABLE_PROG - if (mode==TRACK_MODE_PROG) { + if (mode & TRACK_MODE_PROG) { #else if (false) { #endif // only allow 1 track to be prog FOR_EACH_TRACK(t) - if (track[t]->getMode()==TRACK_MODE_PROG && t != trackToSet) { + if ( (track[t]->getMode() & TRACK_MODE_PROG) && t != trackToSet) { track[t]->setPower(POWERMODE::OFF); track[t]->setMode(TRACK_MODE_NONE); track[t]->makeProgTrack(false); // revoke prog track special handling - streamTrackState(NULL,t); + streamTrackState(NULL,t); } track[trackToSet]->makeProgTrack(true); // set for prog track special handling } else { @@ -235,22 +272,25 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr } track[trackToSet]->setMode(mode); trackDCAddr[trackToSet]=dcAddr; - streamTrackState(NULL,trackToSet); // When a track is switched, we must clear any side effects of its previous // state, otherwise trains run away or just dont move. // This can be done BEFORE the PWM-Timer evaluation (methinks) - if (!(mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX)) { + if (!(mode & TRACK_MODE_DC)) { // DCC tracks need to have set the PWM to zero or they will not work. track[trackToSet]->detachDCSignal(); track[trackToSet]->setBrake(false); } - // EXT is a special case where the signal pin is - // turned off. So unless that is set, the signal - // pin should be turned on - track[trackToSet]->enableSignal(mode != TRACK_MODE_EXT); + // BOOST: + // Leave it as is + // otherwise: + // EXT is a special case where the signal pin is + // turned off. So unless that is set, the signal + // pin should be turned on + if (!(mode & TRACK_MODE_BOOST)) + track[trackToSet]->enableSignal(!(mode & TRACK_MODE_EXT)); #ifndef ARDUINO_ARCH_ESP32 // re-evaluate HighAccuracy mode @@ -260,7 +300,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr // DC tracks must not have the DCC PWM switched on // so we globally turn it off if one of the PWM // capable tracks is now DC or DCX. - if (track[t]->getMode()==TRACK_MODE_DC || track[t]->getMode()==TRACK_MODE_DCX) { + if (track[t]->getMode() & TRACK_MODE_DC) { if (track[t]->isPWMCapable()) { canDo=false; // this track is capable but can not run PWM break; // in this mode, so abort and prevent globally below @@ -268,7 +308,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr track[t]->trackPWM=false; // this track sure can not run with PWM //DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A'); } - } else if (track[t]->getMode()==TRACK_MODE_MAIN || track[t]->getMode()==TRACK_MODE_PROG) { + } else if (track[t]->getMode() & (TRACK_MODE_MAIN |TRACK_MODE_PROG)) { track[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here //DIAG(F("Track %c trackPWM %d"), t+'A', track[t]->trackPWM); canDo &= track[t]->trackPWM; @@ -286,38 +326,40 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr #else // For ESP32 we just reinitialize the DCC Waveform DCCWaveform::begin(); + // setMode() again AFTER Waveform::begin() of ESP32 fixes INVERTED signal + track[trackToSet]->setMode(mode); #endif // This block must be AFTER the PWM-Timer modifications - if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) { + if (mode & TRACK_MODE_DC) { // DC tracks need to be given speed of the throttle for that cab address // otherwise will not match other tracks on same cab. // This also needs to allow for inverted DCX applyDCSpeed(trackToSet); } - // Normal running tracks are set to the global power state - track[trackToSet]->setPower( - (mode==TRACK_MODE_MAIN || mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX || mode==TRACK_MODE_EXT) ? - mainPowerGuess : POWERMODE::OFF); + // Turn off power if we changed the mode of this track + if (mode != oldmode) + track[trackToSet]->setPower(POWERMODE::OFF); + streamTrackState(NULL,trackToSet); + //DIAG(F("TrackMode=%d"),mode); return true; } void TrackManager::applyDCSpeed(byte t) { uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]); - if (track[t]->getMode()==TRACK_MODE_DCX) - speedByte = speedByte ^ 128; // reverse direction bit 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 FOR_EACH_TRACK(t) streamTrackState(stream,t); return true; + } p[0]-=HASH_KEYWORD_A; // convert A... to 0.... @@ -338,46 +380,80 @@ bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[]) if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT> return setTrackMode(p[0],TRACK_MODE_EXT); +#ifdef BOOSTER_INPUT + if (params==2 && p[1]==HASH_KEYWORD_BOOST) // <= id BOOST> + return setTrackMode(p[0],TRACK_MODE_BOOST); +#endif + if (params==2 && p[1]==HASH_KEYWORD_AUTO) // <= id AUTO> + return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV); + + if (params==2 && p[1]==HASH_KEYWORD_INV) // <= id AUTO> + 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> return setTrackMode(p[0],TRACK_MODE_DC,p[2]); if (params==3 && p[1]==HASH_KEYWORD_DCX && p[2]>0) // <= id DCX cab> - return setTrackMode(p[0],TRACK_MODE_DCX,p[2]); + return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]); return false; } -void TrackManager::streamTrackState(Print* stream, byte t) { - // null stream means send to commandDistributor for broadcast - if (track[t]==NULL) return; - auto format=F(""); - switch(track[t]->getMode()) { - case TRACK_MODE_MAIN: - format=F("<= %c MAIN>\n"); - break; -#ifndef DISABLE_PROG - case TRACK_MODE_PROG: - format=F("<= %c PROG>\n"); - break; -#endif - case TRACK_MODE_NONE: - format=F("<= %c NONE>\n"); - break; - case TRACK_MODE_EXT: - format=F("<= %c EXT>\n"); - break; - case TRACK_MODE_DC: - format=F("<= %c DC %d>\n"); - break; - case TRACK_MODE_DCX: - format=F("<= %c DCX %d>\n"); - break; - default: - break; // unknown, dont care +const FSH* TrackManager::getModeName(TRACK_MODE tm) { + const FSH *modename=F("---"); + + if (tm & TRACK_MODE_MAIN) { + if(tm & TRACK_MODE_AUTOINV) + modename=F("MAIN A"); + else if (tm & TRACK_MODE_INV) + modename=F("MAIN I>\n"); + else + modename=F("MAIN"); } - if (stream) StringFormatter::send(stream,format,'A'+t,trackDCAddr[t]); - else CommandDistributor::broadcastTrackState(format,'A'+t,trackDCAddr[t]); +#ifndef DISABLE_PROG + else if (tm & TRACK_MODE_PROG) + modename=F("PROG"); +#endif + else if (tm & TRACK_MODE_NONE) + modename=F("NONE"); + else if(tm & TRACK_MODE_EXT) + modename=F("EXT"); + else if(tm & TRACK_MODE_BOOST) { + if(tm & TRACK_MODE_AUTOINV) + modename=F("B A"); + else if (tm & TRACK_MODE_INV) + modename=F("B I"); + else + modename=F("B"); + } + else if (tm & TRACK_MODE_DC) { + if (tm & TRACK_MODE_INV) + modename=F("DCX"); + else + modename=F("DC"); + } + return modename; +} + +// null stream means send to commandDistributor for broadcast +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 + 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(); + } + } byte TrackManager::nextCycleTrack=MAX_TRACKS; @@ -392,13 +468,13 @@ void TrackManager::loop() { if (nextCycleTrack>lastTrack) nextCycleTrack=0; if (track[nextCycleTrack]==NULL) return; MotorDriver * motorDriver=track[nextCycleTrack]; - bool useProgLimit=dontLimitProg? false: track[nextCycleTrack]->getMode()==TRACK_MODE_PROG; + bool useProgLimit=dontLimitProg ? false : (bool)(track[nextCycleTrack]->getMode() & TRACK_MODE_PROG); motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack); } MotorDriver * TrackManager::getProgDriver() { FOR_EACH_TRACK(t) - if (track[t]->getMode()==TRACK_MODE_PROG) return track[t]; + if (track[t]->getMode() & TRACK_MODE_PROG) return track[t]; return NULL; } @@ -406,56 +482,90 @@ MotorDriver * TrackManager::getProgDriver() { std::vectorTrackManager::getMainDrivers() { std::vector v; FOR_EACH_TRACK(t) - if (track[t]->getMode()==TRACK_MODE_MAIN) v.push_back(track[t]); + if (track[t]->getMode() & TRACK_MODE_MAIN) v.push_back(track[t]); return v; } #endif -void TrackManager::setPower2(bool setProg,POWERMODE mode) { - if (!setProg) mainPowerGuess=mode; - FOR_EACH_TRACK(t) { - MotorDriver * driver=track[t]; - if (!driver) continue; - switch (track[t]->getMode()) { - case TRACK_MODE_MAIN: - if (setProg) break; - // toggle brake before turning power on - resets overcurrent error - // on the Pololu board if brake is wired to ^D2. - // XXX see if we can make this conditional - driver->setBrake(true); - driver->setBrake(false); // DCC runs with brake off - driver->setPower(mode); - break; - case TRACK_MODE_DC: - case TRACK_MODE_DCX: - if (setProg) break; - driver->setBrake(true); // DC starts with brake on - applyDCSpeed(t); // speed match DCC throttles - driver->setPower(mode); - break; - case TRACK_MODE_PROG: - if (!setProg) break; - driver->setBrake(true); - driver->setBrake(false); - driver->setPower(mode); - break; - case TRACK_MODE_EXT: - driver->setBrake(true); - driver->setBrake(false); - driver->setPower(mode); - break; - case TRACK_MODE_NONE: - break; - } +// Set track power for all tracks with this mode +void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) { + FOR_EACH_TRACK(t) { + MotorDriver *driver=track[t]; + TRACK_MODE trackmodeOfTrack = driver->getMode(); + if (trackmodeToMatch & trackmodeOfTrack) { + if (powermode == POWERMODE::ON) { + if (trackmodeOfTrack & TRACK_MODE_DC) { + driver->setBrake(true); // DC starts with brake on + applyDCSpeed(t); // speed match DCC throttles + } else { + // toggle brake before turning power on - resets overcurrent error + // on the Pololu board if brake is wired to ^D2. + driver->setBrake(true); + driver->setBrake(false); // DCC runs with brake off + } + } + driver->setPower(powermode); } -} - -POWERMODE TrackManager::getProgPower() { - FOR_EACH_TRACK(t) - if (track[t]->getMode()==TRACK_MODE_PROG) - return track[t]->getPower(); - return POWERMODE::OFF; } +} + +// Set track power for this track, inependent of mode +void TrackManager::setTrackPower(POWERMODE powermode, byte t) { + MotorDriver *driver=track[t]; + TRACK_MODE trackmode = driver->getMode(); + 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) { + driver->setBrake(true); // DC starts with brake on + applyDCSpeed(t); // speed match DCC throttles + } + } else /* MAIN PROG EXT BOOST */ { + if (powermode == POWERMODE::ON) { + // toggle brake before turning power on - resets overcurrent error + // on the Pololu board if brake is wired to ^D2. + driver->setBrake(true); + driver->setBrake(false); // DCC runs with brake off + } + } + driver->setPower(powermode); +} + +// returns state of the one and only prog track +POWERMODE TrackManager::getProgPower() { + FOR_EACH_TRACK(t) + if (track[t]->getMode() & TRACK_MODE_PROG) + return track[t]->getPower(); // optimize: there is max one prog track + return POWERMODE::OFF; +} + +// returns on if all are on. returns off otherwise +POWERMODE TrackManager::getMainPower() { + POWERMODE result = POWERMODE::OFF; + FOR_EACH_TRACK(t) { + if (track[t]->getMode() & TRACK_MODE_MAIN) { + POWERMODE p = track[t]->getPower(); + if (p == POWERMODE::OFF) + return POWERMODE::OFF; // done and out + if (p == POWERMODE::ON) + result = POWERMODE::ON; + } + } + return result; +} + +bool TrackManager::getPower(byte t, char s[]) { + if (t > lastTrack) + return false; + if (track[t]) { + s[0] = track[t]->getPower() == POWERMODE::ON ? '1' : '0'; + s[2] = t + 'A'; + return true; + } + return false; +} + void TrackManager::reportObsoleteCurrent(Print* stream) { // This function is for backward JMRI compatibility only @@ -497,7 +607,7 @@ void TrackManager::setJoin(bool joined) { #ifdef ARDUINO_ARCH_ESP32 if (joined) { FOR_EACH_TRACK(t) { - if (track[t]->getMode()==TRACK_MODE_PROG) { + if (track[t]->getMode() & TRACK_MODE_PROG) { tempProgTrack = t; setTrackMode(t, TRACK_MODE_MAIN); break; @@ -505,7 +615,12 @@ void TrackManager::setJoin(bool joined) { } } else { if (tempProgTrack != MAX_TRACKS+1) { + // as setTrackMode with TRACK_MODE_PROG defaults to + // power off, we will take the current power state + // of our track and then preserve that state. + POWERMODE tPTmode = track[tempProgTrack]->getPower(); //get current power status of this track setTrackMode(tempProgTrack, TRACK_MODE_PROG); + track[tempProgTrack]->setPower(tPTmode); //set track status as it was before tempProgTrack = MAX_TRACKS+1; } } @@ -513,3 +628,24 @@ void TrackManager::setJoin(bool joined) { progTrackSyncMain=joined; if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,joined?HIGH:LOW); } + +bool TrackManager::isPowerOn(byte t) { + if (track[t]->getPower()!=POWERMODE::ON) + return false; + return true; + } + +bool TrackManager::isProg(byte t) { + if (track[t]->getMode() & TRACK_MODE_PROG) + return true; + return false; +} + +TRACK_MODE TrackManager::getMode(byte t) { + return (track[t]->getMode()); +} + +int16_t TrackManager::returnDCAddr(byte t) { + return (trackDCAddr[t]); +} + diff --git a/TrackManager.h b/TrackManager.h index 965cfa3..6310030 100644 --- a/TrackManager.h +++ b/TrackManager.h @@ -1,6 +1,8 @@ /* * © 2022 Chris Harlow * © 2022 Harald Barth + * © 2023 Colin Murdoch + * * All rights reserved. * * This file is part of CommandStation-EX @@ -37,6 +39,10 @@ const byte TRACK_NUMBER_5=5, TRACK_NUMBER_F=5; const byte TRACK_NUMBER_6=6, TRACK_NUMBER_G=6; const byte TRACK_NUMBER_7=7, TRACK_NUMBER_H=7; +// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF). +const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0; +const byte TRACK_POWER_1=1, TRACK_POWER_ON=1; + class TrackManager { public: static void Setup(const FSH * shieldName, @@ -56,27 +62,39 @@ class TrackManager { static void setDCSignal(int16_t cab, byte speedbyte); static MotorDriver * getProgDriver(); #ifdef ARDUINO_ARCH_ESP32 - static std::vectorgetMainDrivers(); + static std::vectorgetMainDrivers(); #endif - static void setPower2(bool progTrack,POWERMODE mode); + static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);} - static void setMainPower(POWERMODE mode) {setPower2(false,mode);} - static void setProgPower(POWERMODE mode) {setPower2(true,mode);} + static void setTrackPower(POWERMODE mode, byte t); + static void setTrackPower(TRACK_MODE trackmode, POWERMODE powermode); + static void setMainPower(POWERMODE mode) {setTrackPower(TRACK_MODE_MAIN, mode);} + static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);} static const int16_t MAX_TRACKS=8; 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 POWERMODE getMainPower() {return mainPowerGuess;} + static POWERMODE getMainPower(); static POWERMODE getProgPower(); + static inline POWERMODE getPower(byte t) { return track[t]->getPower(); } + static bool getPower(byte t, char s[]); static void setJoin(bool join); 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 sampleCurrent(); static void reportGauges(Print* stream); static void reportCurrent(Print* stream); static void reportObsoleteCurrent(Print* stream); static void streamTrackState(Print* stream, byte t); + static bool isPowerOn(byte t); + static bool isProg(byte t); + static TRACK_MODE getMode(byte t); + static int16_t returnDCAddr(byte t); + static const FSH* getModeName(TRACK_MODE Mode); static int16_t joinRelay; static bool progTrackSyncMain; // true when prog track is a siding switched to main @@ -93,10 +111,9 @@ class TrackManager { static void addTrack(byte t, MotorDriver* driver); static byte lastTrack; static byte nextCycleTrack; - static POWERMODE mainPowerGuess; static void applyDCSpeed(byte t); - static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX + static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC #ifdef ARDUINO_ARCH_ESP32 static byte tempProgTrack; // holds the prog track number during join #endif diff --git a/Turntables.cpp b/Turntables.cpp new file mode 100644 index 0000000..ba143cb --- /dev/null +++ b/Turntables.cpp @@ -0,0 +1,268 @@ +/* + * © 2023 Peter Cole + * 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 . + */ + +#include "defines.h" +#include +#include "Turntables.h" +#include "StringFormatter.h" +#include "CommandDistributor.h" +#include "EXRAIL2.h" +#include "DCC.h" + +// No turntable support without HAL +#ifndef IO_NO_HAL + +/* + * Protected static data + */ +Turntable *Turntable::_firstTurntable = 0; + + +/* + * Public static data + */ +int Turntable::turntablelistHash = 0; + + +/* + * Protected static functions + */ +// Add new turntable to end of list + +void Turntable::add(Turntable *tto) { + if (!_firstTurntable) { + _firstTurntable = tto; + } else { + Turntable *ptr = _firstTurntable; + for ( ; ptr->_nextTurntable!=0; ptr=ptr->_nextTurntable) {} + ptr->_nextTurntable = tto; + } + turntablelistHash++; +} + +// Add a position +void Turntable::addPosition(uint8_t idx, uint16_t value, uint16_t angle) { + _turntablePositions.insert(idx, value, angle); +} + +// Get value for position +uint16_t Turntable::getPositionValue(uint8_t position) { + TurntablePosition* currentPosition = _turntablePositions.getHead(); + while (currentPosition) { + if (currentPosition->index == position) { + return currentPosition->data; + } + currentPosition = currentPosition->next; + } + return false; +} + +// Get value for position +uint16_t Turntable::getPositionAngle(uint8_t position) { + TurntablePosition* currentPosition = _turntablePositions.getHead(); + while (currentPosition) { + if (currentPosition->index == position) { + return currentPosition->angle; + } + currentPosition = currentPosition->next; + } + return false; +} + +// Get the count of positions associated with the turntable +uint8_t Turntable::getPositionCount() { + TurntablePosition* currentPosition = _turntablePositions.getHead(); + uint8_t count = 0; + while (currentPosition) { + count++; + currentPosition = currentPosition->next; + } + return count; +} + +/* + * Public static functions + */ +// Find turntable from list +Turntable *Turntable::get(uint16_t id) { + for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable) + if (tto->_turntableData.id == id) return tto; + return NULL; +} + +// Find turntable via Vpin +Turntable *Turntable::getByVpin(VPIN vpin) { + for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable) { + if (tto->isEXTT()) { + EXTTTurntable *exttTto = static_cast(tto); + if (exttTto->getVpin() == vpin) { + return tto; + } + } + } + return nullptr; +} + +// Get the current position for turntable with the specified ID +uint8_t Turntable::getPosition(uint16_t id) { + Turntable *tto = get(id); + if (!tto) return false; + return tto->getPosition(); +} + +// Got the moving state of the specified turntable +bool Turntable::ttMoving(uint16_t id) { + Turntable *tto = get(id); + if (!tto) return false; + return tto->isMoving(); +} + +// Initiate a turntable move +bool Turntable::setPosition(uint16_t id, uint8_t position, uint8_t activity) { +#if defined(DIAG_IO) + DIAG(F("Rotate turntable %d to position %d, activity %d)"), id, position, activity); +#endif + Turntable *tto = Turntable::get(id); + if (!tto) return false; + if (tto->isMoving()) return false; + bool ok = tto->setPositionInternal(position, activity); + + if (ok) { + // We only deal with broadcasts for DCC turntables here, EXTT in the device driver + if (!tto->isEXTT()) { + CommandDistributor::broadcastTurntable(id, position, false); + } + // Trigger EXRAIL rotateEvent for both types here if changed +#if defined(EXRAIL_ACTIVE) + bool rotated = false; + if (position != tto->_previousPosition) rotated = true; + RMFT2::rotateEvent(id, rotated); +#endif + } + return ok; +} + +/************************************************************************************* + * EXTTTurntable - EX-Turntable device. + * + *************************************************************************************/ +// Private constructor +EXTTTurntable::EXTTTurntable(uint16_t id, VPIN vpin) : + Turntable(id, TURNTABLE_EXTT) +{ + _exttTurntableData.vpin = vpin; +} + +using DevState = IODevice::DeviceStateEnum; + +// Create function + Turntable *EXTTTurntable::create(uint16_t id, VPIN vpin) { +#ifndef IO_NO_HAL + Turntable *tto = get(id); + if (tto) { + if (tto->isType(TURNTABLE_EXTT)) { + EXTTTurntable *extt = (EXTTTurntable *)tto; + extt->_exttTurntableData.vpin = vpin; + return tto; + } + } + if (!IODevice::exists(vpin)) return nullptr; + if (IODevice::getStatus(vpin) == DevState::DEVSTATE_FAILED) return nullptr; + if (Turntable::getByVpin(vpin)) return nullptr; + tto = (Turntable *)new EXTTTurntable(id, vpin); + DIAG(F("Turntable 0x%x size %d size %d"), tto, sizeof(Turntable), sizeof(struct TurntableData)); + return tto; +#else + (void)id; + (void)vpin; + return NULL; +#endif + } + + void EXTTTurntable::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turntableData.id, _exttTurntableData.vpin); + } + + // EX-Turntable specific code for moving to the specified position + bool EXTTTurntable::setPositionInternal(uint8_t position, uint8_t activity) { +#ifndef IO_NO_HAL + int16_t value; + if (position == 0) { + value = 0; // Position 0 is just to send activities + } else { + if (activity > 1) return false; // If sending a position update, only phase changes valid (0|1) + value = getPositionValue(position); // Get position value from position list + } + if (position > 0 && !value) return false; // Return false if it's not a valid position + // Set position via device driver + _previousPosition = _turntableData.position; + _turntableData.position = position; + EXTurntable::writeAnalogue(_exttTurntableData.vpin, value, activity); +#else + (void)position; +#endif + return true; + } + +/************************************************************************************* + * DCCTurntable - DCC Turntable device. + * + *************************************************************************************/ +// Private constructor +DCCTurntable::DCCTurntable(uint16_t id) : Turntable(id, TURNTABLE_DCC) {} + +// Create function + Turntable *DCCTurntable::create(uint16_t id) { +#ifndef IO_NO_HAL + Turntable *tto = get(id); + if (!tto) { + tto = (Turntable *)new DCCTurntable(id); + DIAG(F("Turntable 0x%x size %d size %d"), tto, sizeof(Turntable), sizeof(struct TurntableData)); + } + return tto; +#else + (void)id; + return NULL; +#endif + } + + void DCCTurntable::print(Print *stream) { + StringFormatter::send(stream, F("\n"), _turntableData.id); + } + + // EX-Turntable specific code for moving to the specified position + bool DCCTurntable::setPositionInternal(uint8_t position, uint8_t activity) { +#ifndef IO_NO_HAL + int16_t value = getPositionValue(position); + if (position == 0 || !value) return false; // Return false if it's not a valid position + // Set position via device driver + int16_t addr=value>>3; + int16_t subaddr=(value>>1) & 0x03; + bool active=value & 0x01; + _previousPosition = _turntableData.position; + _turntableData.position = position; + DCC::setAccessory(addr, subaddr, active); +#else + (void)position; +#endif + return true; + } + +#endif diff --git a/Turntables.h b/Turntables.h new file mode 100644 index 0000000..aa089ef --- /dev/null +++ b/Turntables.h @@ -0,0 +1,243 @@ +/* + * © 2023 Peter Cole + * 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 . + */ + +#ifndef TURNTABLES_H +#define TURNTABLES_H + +#include +#include "IODevice.h" +#include "StringFormatter.h" + +// No turntable support without HAL +#ifndef IO_NO_HAL + +// Turntable type definitions +// EXTT = EX-Turntable +// DCC = DCC accessory turntables - to be added later +enum { + TURNTABLE_EXTT = 0, + TURNTABLE_DCC = 1, +}; + +/************************************************************************************* + * Turntable positions. + * + *************************************************************************************/ +struct TurntablePosition { + uint8_t index; + uint16_t data; + uint16_t angle; + TurntablePosition* next; + + TurntablePosition(uint8_t idx, uint16_t value, uint16_t angle) : index(idx), data(value), angle(angle), next(nullptr) {} +}; + +class TurntablePositionList { +public: + TurntablePositionList() : head(nullptr) {} + + void insert(uint8_t idx, uint16_t value, uint16_t angle) { + TurntablePosition* newPosition = new TurntablePosition(idx, value, angle); + if(!head) { + head = newPosition; + } else { + newPosition->next = head; + head = newPosition; + } + } + + TurntablePosition* getHead() { + return head; + } + +private: + TurntablePosition* head; + +}; + + +/************************************************************************************* + * Turntable - Base class for turntables. + * + *************************************************************************************/ + +class Turntable { +protected: + /* + * Object data + */ + + // Data common to all turntable types + struct TurntableData { + union { + struct { + bool hidden : 1; + bool turntableType : 1; + uint8_t position : 6; // Allows up to 63 positions including 0/home + }; + uint8_t flags; + }; + uint16_t id; + } _turntableData; + + // Pointer to next turntable object + Turntable *_nextTurntable = 0; + + // Linked list for positions + TurntablePositionList _turntablePositions; + + // Store the previous position to allow checking for changes + uint8_t _previousPosition = 0; + + // Store the current state of the turntable + bool _isMoving = false; + + /* + * Constructor + */ + Turntable(uint16_t id, uint8_t turntableType) { + _turntableData.id = id; + _turntableData.turntableType = turntableType; + _turntableData.hidden = false; + _turntableData.position = 0; + add(this); + } + + /* + * Static data + */ + static Turntable *_firstTurntable; + static int _turntablelistHash; + + /* + * Virtual functions + */ + virtual bool setPositionInternal(uint8_t position, uint8_t activity) = 0; + + /* + * Static functions + */ + static void add(Turntable *tto); + +public: + static Turntable *get(uint16_t id); + static Turntable *getByVpin(VPIN vpin); + + /* + * Static data + */ + static int turntablelistHash; + + /* + * Public base class functions + */ + inline uint8_t getPosition() { return _turntableData.position; } + inline bool isHidden() { return _turntableData.hidden; } + inline void setHidden(bool h) {_turntableData.hidden=h; } + inline bool isType(uint8_t type) { return _turntableData.turntableType == type; } + inline bool isEXTT() const { return _turntableData.turntableType == TURNTABLE_EXTT; } + inline uint16_t getId() { return _turntableData.id; } + inline Turntable *next() { return _nextTurntable; } + void printState(Print *stream); + void addPosition(uint8_t idx, uint16_t value, uint16_t angle); + uint16_t getPositionValue(uint8_t position); + uint16_t getPositionAngle(uint8_t position); + uint8_t getPositionCount(); + bool isMoving() { return _isMoving; } + void setMoving(bool moving) { _isMoving=moving; } + + /* + * Virtual functions + */ + virtual void print(Print *stream) { + (void)stream; // suppress compiler warnings + } + virtual ~Turntable() {} // Destructor + + + /* + * Public static functions + */ + inline static bool exists(uint16_t id) { return get(id) != 0; } + static bool setPosition(uint16_t id, uint8_t position, uint8_t activity=0); + static uint8_t getPosition(uint16_t id); + static bool ttMoving(uint16_t id); + inline static Turntable *first() { return _firstTurntable; } + static bool printAll(Print *stream) { + bool gotOne = false; + for (Turntable *tto = _firstTurntable; tto != 0; tto = tto->_nextTurntable) + if (!tto->isHidden()) { + gotOne = true; + StringFormatter::send(stream, F("\n"), tto->getId(), tto->getPosition()); + } + return gotOne; + } + +}; + +/************************************************************************************* + * EXTTTurntable - EX-Turntable device. + * + *************************************************************************************/ +class EXTTTurntable : public Turntable { +private: + // EXTTTurntableData contains device specific data + struct EXTTTurntableData { + VPIN vpin; + } _exttTurntableData; + + // Constructor + EXTTTurntable(uint16_t id, VPIN vpin); + +public: + // Create function + static Turntable *create(uint16_t id, VPIN vpin); + void print(Print *stream) override; + VPIN getVpin() const { return _exttTurntableData.vpin; } + +protected: + // EX-Turntable specific code for setting position + bool setPositionInternal(uint8_t position, uint8_t activity) override; + +}; + +/************************************************************************************* + * DCCTurntable - DCC accessory Turntable device. + * + *************************************************************************************/ +class DCCTurntable : public Turntable { +private: + // Constructor + DCCTurntable(uint16_t id); + +public: + // Create function + static Turntable *create(uint16_t id); + void print(Print *stream) override; + +protected: + // DCC specific code for setting position + bool setPositionInternal(uint8_t position, uint8_t activity=0) override; + +}; + +#endif + +#endif diff --git a/WifiESP32.cpp b/WifiESP32.cpp index 28a15fe..f0a857f 100644 --- a/WifiESP32.cpp +++ b/WifiESP32.cpp @@ -163,7 +163,9 @@ bool WifiESP::setup(const char *SSid, delay(500); } 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")); + LCD(7, F("IP: %s"), WiFi.softAPIP().toString().c_str()); wifiUp = true; } else { DIAG(F("Could not connect to Wifi SSID %s"),SSid); @@ -209,8 +211,12 @@ bool WifiESP::setup(const char *SSid, if (WiFi.softAP(strSSID.c_str(), havePassword ? password : strPass.c_str(), channel, false, 8)) { - DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str()); - DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str()); + // DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str()); + DIAG(F("Wifi in AP mode")); + LCD(5, F("Wifi: %s"), strSSID.c_str()); + LCD(6, F("PASS: %s"),havePassword ? password : strPass.c_str()); + // DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str()); + LCD(7, F("IP: %s"),WiFi.softAPIP().toString().c_str()); wifiUp = true; APmode = true; } else { diff --git a/WifiInterface.cpp b/WifiInterface.cpp index 7511af6..8b2251a 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -200,7 +200,23 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, // Display the AT version information StringFormatter::send(wifiStream, F("AT+GMR\r\n")); - checkForOK(2000, true, false); // Makes this visible on the console + if (checkForOK(2000, F("AT version:"), true, false)) { + char version[] = "0.0.0.0-xxx"; + for (int i=0; i<11;i++) { + while(!wifiStream->available()); + version[i]=wifiStream->read(); + StringFormatter::printEscape(version[i]); + } + if ((version[0] == '0') || + (version[0] == '2' && version[2] == '0') || + (version[0] == '2' && version[2] == '2' && version[4] == '0' && version[6] == '0' + && version[7] == '-' && version[8] == 'd' && version[9] == 'e' && version[10] == 'v')) { + DIAG(F("You need to up/downgrade the ESP firmware")); + SSid = F("UPDATE_ESP_FIRMWARE"); + forceAP = true; + } + } + checkForOK(2000, true, false); #ifdef DONT_TOUCH_WIFI_CONF DIAG(F("DONT_TOUCH_WIFI_CONF was set: Using existing config")); diff --git a/config.example.h b/config.example.h index 0f136f9..89d6c1f 100644 --- a/config.example.h +++ b/config.example.h @@ -167,6 +167,14 @@ The configuration file for DCC-EX Command Station // * #define SCROLLMODE 2 is by row (move up 1 row at a time). #define SCROLLMODE 1 +// In order to avoid wasting memory the current scroll buffer is limited +// to 8 lines. Some users wishing to display additional information +// such as TrackManager power states have requested additional rows aware +// of the warning that this will take extra RAM. if you wish to include additional rows +// uncomment the following #define and set the number of lines you need. +//#define MAX_CHARACTER_ROWS 12 + + ///////////////////////////////////////////////////////////////////////////////////// // DISABLE EEPROM // @@ -191,6 +199,18 @@ The configuration file for DCC-EX Command Station // // #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 // is 127 and the first long address is 128. There are manufacturers which have @@ -266,6 +286,12 @@ The configuration file for DCC-EX Command Station // //#define SERIAL_BT_COMMANDS +// BOOSTER PIN INPUT ON ESP32 +// On ESP32 you have the possibility to define a pin as booster input +// Arduio pin D2 is GPIO 26 on ESPDuino32 +// +//#define BOOSTER_INPUT 26 + // SABERTOOTH // // This is a very special option and only useful if you happen to have a diff --git a/defines.h b/defines.h index f3822ca..14dd1c5 100644 --- a/defines.h +++ b/defines.h @@ -144,9 +144,9 @@ #define DISABLE_EEPROM #endif // STM32 support for native I2C is awaiting development - #ifndef I2C_USE_WIRE - #define I2C_USE_WIRE - #endif + // #ifndef I2C_USE_WIRE + // #define I2C_USE_WIRE + // #endif /* TODO when ready #elif defined(ARDUINO_ARCH_RP2040) @@ -213,6 +213,18 @@ // #define WIFI_SERIAL_LINK_SPEED 115200 +//////////////////////////////////////////////////////////////////////////////// +// +// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required +// 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) +#define IO_NO_HAL // HAL too big whatever you disable otherwise +#ifndef ENABLE_VDPY +#define DISABLE_VDPY +#endif +#endif + #if __has_include ( "myAutomation.h") #if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) || defined(DISABLE_PROG) #define EXRAIL_ACTIVE diff --git a/myHal.cpp_example.txt b/myHal.cpp_example.txt index f3fa1b3..541c018 100644 --- a/myHal.cpp_example.txt +++ b/myHal.cpp_example.txt @@ -24,6 +24,7 @@ //#include "IO_TouchKeypad.h // Touch keypad with 16 keys //#include "IO_EXTurntable.h" // Turntable-EX turntable controller //#include "IO_EXFastClock.h" // FastClock driver +//#include "IO_PCA9555.h" // 16-bit I/O expander (NXP & Texas Instruments). //#include "IO_CMRI.h" // CMRI nodes //========================================================================== @@ -91,7 +92,7 @@ void halSetup() { // Create a 20x4 LCD display device as display number 2 // (line 0 is written by EX-RAIL 'SCREEN(2, 0, "text")'). - // HALDisplay(2, 0x27, 20, 4); + // HALDisplay::create(2, 0x27, 20, 4); //======================================================================= diff --git a/platformio.ini b/platformio.ini index 1a87770..8767ef1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -30,8 +30,7 @@ include_dir = . [env] build_flags = -Wall -Wextra -monitor_filters = time -; lib_deps = adafruit/Adafruit ST7735 and ST7789 Library @ ^1.10.0 +; monitor_filters = time [env:samd21-dev-usb] platform = atmelsam @@ -60,7 +59,7 @@ framework = arduino lib_deps = ${env.lib_deps} monitor_speed = 115200 monitor_echo = yes -build_flags = -std=c++17 ; -DI2C_USE_WIRE -DDIAG_LOOPTIMES -DDIAG_IO +build_flags = -std=c++17 [env:mega2560-debug] platform = atmelavr @@ -72,7 +71,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_echo = yes -build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES +build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES [env:mega2560-no-HAL] platform = atmelavr @@ -84,7 +83,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_echo = yes -build_flags = -DIO_NO_HAL +build_flags = -DIO_NO_HAL [env:mega2560-I2C-wire] platform = atmelavr @@ -108,7 +107,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_echo = yes -build_flags = ; -DDIAG_LOOPTIMES +build_flags = [env:mega328] platform = atmelavr @@ -190,10 +189,75 @@ platform = ststm32 board = nucleo_f446re framework = arduino lib_deps = ${env.lib_deps} -build_flags = -std=c++17 -Os -g2 -Wunused-variable ; -DDIAG_LOOPTIMES ; -DDIAG_IO +build_flags = -std=c++17 -Os -g2 -Wunused-variable monitor_speed = 115200 monitor_echo = yes +; Experimental - no reason this should not work, but not +; tested as yet +; +[env:Nucleo-F401RE] +platform = ststm32 +board = nucleo_f401re +framework = arduino +lib_deps = ${env.lib_deps} +build_flags = -std=c++17 -Os -g2 -Wunused-variable +monitor_speed = 115200 +monitor_echo = yes + +; Commented out by default as the F13ZH has variant files +; but NOT the nucleo_f413zh.json file which needs to be +; installed before you can let PlatformIO see this +; +; [env:Nucleo-F413ZH] +; platform = ststm32 +; board = nucleo_f413zh +; framework = arduino +; lib_deps = ${env.lib_deps} +; build_flags = -std=c++17 -Os -g2 -Wunused-variable +; monitor_speed = 115200 +; monitor_echo = yes + +; Commented out by default as the F446ZE needs variant files +; installed before you can let PlatformIO see this +; +; [env:Nucleo-F446ZE] +; platform = ststm32 +; board = nucleo_f446ze +; framework = arduino +; lib_deps = ${env.lib_deps} +; build_flags = -std=c++17 -Os -g2 -Wunused-variable +; monitor_speed = 115200 +; monitor_echo = yes + +; Commented out by default as the F412ZG needs variant files +; installed before you can let PlatformIO see this +; +; [env:Nucleo-F412ZG] +; platform = ststm32 +; board = blah_f412zg +; framework = arduino +; lib_deps = ${env.lib_deps} +; build_flags = -std=c++17 -Os -g2 -Wunused-variable +; monitor_speed = 115200 +; monitor_echo = yes +; upload_protocol = stlink + +; Experimental - Ethernet work still in progress +; +; [env:Nucleo-F429ZI] +; platform = ststm32 +; board = nucleo_f429zi +; framework = arduino +; lib_deps = ${env.lib_deps} +; arduino-libraries/Ethernet @ ^2.0.1 +; stm32duino/STM32Ethernet @ ^1.3.0 +; stm32duino/STM32duino LwIP @ ^2.1.2 +; build_flags = -std=c++17 -Os -g2 -Wunused-variable +; monitor_speed = 115200 +; monitor_echo = yes +; upload_protocol = stlink + [env:Teensy3_2] platform = teensy board = teensy31 @@ -232,5 +296,4 @@ board = teensy41 framework = arduino build_flags = -std=c++17 -Os -g2 lib_deps = ${env.lib_deps} -lib_ignore = - +lib_ignore = diff --git a/version.h b/version.h index 465f684..a6265d8 100644 --- a/version.h +++ b/version.h @@ -3,7 +3,66 @@ #include "StringFormatter.h" -#define VERSION "4.2.68" +#define VERSION "5.2.14" +// 5.2.14 - Reminder window DCC packet optimization +// - Optional #define DISABLE_FUNCTION_REMINDERS +// 5.2.13 - EXRAIL STEALTH +// 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for +// - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers +// - STM32 extra UART/USARTs for larger Nucleo models +// 5.2.11 - Change from TrackManager::returnMode to TrackManager::getMode +// 5.2.10 - Include trainbrains.eu block unoccupancy driver +// - include IO_PCA9555 +// 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.4 - LCD macro will not do diag if that duplicates @ to same target. +// - Added ROUTE_DISABLED macro in EXRAIL +// 5.2.3 - Bugfix: Catch stange input to parser +// 5.2.2 - Added option to allow MAX_CHARACTER_ROWS to be defined in config.h +// 5.2.1 - Trackmanager rework for simpler structure +// 5.2.0 - ESP32: Autoreverse and booster mode support +// 5.1.21 - EXRAIL invoke multiple ON handlers for same event +// 5.1.20 - EXRAIL Tidy and ROUTE_STATE, ROUTE_CAPTION +// 5.1.19 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0 +// 5.1.18 - TURNOUTL bugfix +// 5.1.17 - Divide out C for config and D for diag commands +// 5.1.16 - Remove I2C address from EXTT_TURNTABLE macro to work with MUX, requires separate HAL macro to create +// 5.1.15 - LCC/Adapter support and Exrail feature-compile-out. +// 5.1.14 - Fixed IFTTPOSITION +// 5.1.13 - Changed turntable broadcast from i to I due to server string conflict +// 5.1.12 - Added Power commands <0 A> & <1 A> etc. and update to <=> +// Added EXRAIL SET_POWER(track, ON/OFF) +// Fixed a problem whereby <1 MAIN> also powered on PROG track +// Added functions to TrackManager.cpp to allow UserAddin code for power display on OLED/LCD +// Added - returnMode(byte t), returnDCAddr(byte t) & getModeName(byte Mode) +// 5.1.11 - STM32F4xx revised I2C clock setup, no correctly sets clock and has fully variable frequency selection +// 5.1.10 - STM32F4xx DCCEXanalogWrite to handle PWM generation for TrackManager DC/DCX +// - STM32F4xx DCC 58uS timer now using non-PWM output timers where possible +// - ESP32 brakeCanPWM check now detects UNUSED_PIN +// - ARM architecture brakeCanPWM now uses digitalPinHasPWM() +// - STM32F4xx shadowpin extensions to handle pins on ports D, E and F +// 5.1.9 - Fixed IO_PCA9555'h to work with PCA9548 mux, tested OK +// 5.1.8 - STM32Fxx ADCee extension to support ADCs #2 and #3 +// 5.1.7 - Fix turntable broadcasts for non-movement activities and result +// 5.1.6 - STM32F4xx native I2C driver added +// 5.1.5 - Added turntable object and EXRAIL commands +// - , , - turntable commands +// - DCC_TURNTABLE, EXTT_TURNTABLE, IFTTPOSITION, ONROTATE, ROTATE, ROTATE_DCC, TT_ADDPOSITION, WAITFORTT EXRAIL +// 5.1.4 - Added ONOVERLOAD & AFTEROVERLOAD to EXRAIL +// 5.1.3 - Make parser more fool proof +// 5.1.2 - Bugfix: ESP32 30ms off time +// 5.1.1 - Check bad AT firmware version +// - Update IO_PCA9555.h reflecting IO_MCP23017.h changes to support PCA9548 mux +// 5.0.1 - Bugfix: execute 30ms off time before rejoin +// 5.0.0 - Make 4.2.69 the 5.0.0 release +// 4.2.69 - Bugfix: Make work in DC mode // 4.2.68 - Rename track mode OFF to NONE // 4.2.67 - AVR: Pin specific timer register seting // - Protect Uno user from choosing DC(X)