diff --git a/CommandDistributor.cpp b/CommandDistributor.cpp index 1651771..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 @@ -248,27 +249,123 @@ void CommandDistributor::broadcastLoco(byte slot) { } void CommandDistributor::broadcastPower() { + char pstr[] = "? x"; + for(byte t=0; t\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 d54ef31..e4dff5d 100644 --- a/CommandDistributor.h +++ b/CommandDistributor.h @@ -55,10 +55,20 @@ public : 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/DCCEXParser.cpp b/DCCEXParser.cpp index d79136f..f3cdfb2 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -115,6 +115,7 @@ Once a new OPCODE is decided upon, update this list. #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. @@ -159,6 +160,7 @@ 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'; @@ -210,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; @@ -304,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') @@ -553,131 +562,66 @@ 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; - bool singletrack=false; - //byte t=0; - 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; + TrackManager::setJoin(true); + TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON); } else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG> - prog=true; + TrackManager::setJoin(false); + TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON); } #endif - //else if (p[0] >= 'A' && p[0] <= 'H') { // <1 A-H> else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> - byte t = (p[0] - 'A'); - //DIAG(F("Processing track - %d "), t); - if (TrackManager::isProg(t)) { - main = false; - prog = true; - } - else - { - main=true; - prog=false; - } - singletrack=true; - if (main) TrackManager::setTrackPower(false, false, POWERMODE::ON, t); - if (prog) TrackManager::setTrackPower(true, false, POWERMODE::ON, t); - - StringFormatter::send(stream, F("<1 %c>\n"), t+'A'); - //CommandDistributor::broadcastPower(); - //TrackManager::streamTrackState(NULL,t); - return; + byte t = (p[0] - 'A'); + TrackManager::setTrackPower(POWERMODE::ON, t); + //StringFormatter::send(stream, F("\n"), t+'A'); } - else break; // will reply - } - - if (!singletrack) { - TrackManager::setJoin(join); - if (join) TrackManager::setJoinPower(POWERMODE::ON); - else { - if (main) TrackManager::setMainPower(POWERMODE::ON); - if (prog) TrackManager::setProgPower(POWERMODE::ON); - } - CommandDistributor::broadcastPower(); + } + CommandDistributor::broadcastPower(); + //TrackManager::streamTrackState(NULL,t); - return; - } + return; + } - } - case '0': // POWEROFF <0 [MAIN | PROG] > { - bool main=false; - bool prog=false; - bool singletrack=false; - //byte t=0; - 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; + TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off + TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF); } #endif - //else if (p[0] >= 'A' && p[0] <= 'H') { // <1 A-H> - else if (p[0] >= HASH_KEYWORD_A && p[0] <= HASH_KEYWORD_H) { // <1 A-H> - byte t = (p[0] - 'A'); - //DIAG(F("Processing track - %d "), t); - if (TrackManager::isProg(t)) { - main = false; - prog = true; - } - else - { - main=true; - prog=false; - } - singletrack=true; - TrackManager::setJoin(false); - if (main) TrackManager::setTrackPower(false, false, POWERMODE::OFF, t); - if (prog) { - TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off - TrackManager::setTrackPower(true, false, POWERMODE::OFF, t); - } - StringFormatter::send(stream, F("<0 %c>\n"), t+'A'); - //CommandDistributor::broadcastPower(); - //TrackManager::streamTrackState(NULL, t); - return; - } - - 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'); } - - if (!singletrack) { - 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; - } - } + else break; // will reply + } + CommandDistributor::broadcastPower(); + return; + } case '!': // ESTOP ALL DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1. @@ -724,8 +668,8 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream) return; break; #endif - case '=': // TACK MANAGER CONTROL <= [params]> - if (TrackManager::parseJ(stream, params, p)) + case '=': // TRACK MANAGER CONTROL <= [params]> + if (TrackManager::parseEqualSign(stream, params, p)) return; break; @@ -781,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(" 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")); } @@ -1119,9 +1065,9 @@ bool DCCEXParser::parseS(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]) { #ifndef DISABLE_PROG @@ -1159,6 +1105,8 @@ bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) { LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // } } else { + 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; } diff --git a/DCCTimerSTM32.cpp b/DCCTimerSTM32.cpp index 17d4b44..ca02a41 100644 --- a/DCCTimerSTM32.cpp +++ b/DCCTimerSTM32.cpp @@ -50,11 +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_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F446ZE) +#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 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 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 c902708..3e55620 100644 --- a/EXRAIL2.cpp +++ b/EXRAIL2.cpp @@ -55,22 +55,6 @@ #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. @@ -86,7 +70,7 @@ RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE. // and all others will have their locos stopped, then resumed after the pausing task resumes. byte RMFT2::flags[MAX_FLAGS]; Print * RMFT2::LCCSerial=0; -LookList * RMFT2::sequenceLookup=NULL; +LookList * RMFT2::routeLookup=NULL; LookList * RMFT2::onThrowLookup=NULL; LookList * RMFT2::onCloseLookup=NULL; LookList * RMFT2::onActivateLookup=NULL; @@ -100,9 +84,10 @@ LookList * RMFT2::onClockLookup=NULL; LookList * RMFT2::onRotateLookup=NULL; #endif LookList * RMFT2::onOverloadLookup=NULL; - -#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter) -#define SKIPOP progCounter+=3 +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) { @@ -120,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]; @@ -137,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); @@ -216,6 +234,12 @@ if (compileFeatures & FEATURE_SIGNAL) { 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: @@ -295,8 +319,14 @@ if (compileFeatures & FEATURE_SIGNAL) { } } 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; @@ -314,238 +344,15 @@ void RMFT2::setTurntableHiddenState(Turntable * tto) { #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; - case 'L': - if (compileFeatures & FEATURE_LCC) { - // This entire code block is compiled out if LLC macros not used - 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; - - default: // other commands pass through - break; - } - if (reject) { - opcode=0; - StringFormatter::send(stream,F("\n")); - } -} - -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; @@ -594,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; @@ -843,10 +650,10 @@ void RMFT2::loop2() { //byte thistrack=getOperand(1); switch (operand) { case TRACK_POWER_0: - TrackManager::setTrackPower(TrackManager::isProg(getOperand(1)), false, POWERMODE::OFF, getOperand(1)); + TrackManager::setTrackPower(POWERMODE::OFF, getOperand(1)); break; case TRACK_POWER_1: - TrackManager::setTrackPower(TrackManager::isProg(getOperand(1)), false, POWERMODE::ON, getOperand(1)); + TrackManager::setTrackPower(POWERMODE::ON, getOperand(1)); break; } @@ -857,7 +664,7 @@ void RMFT2::loop2() { // 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; @@ -995,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; @@ -1005,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; @@ -1068,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); } @@ -1076,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; @@ -1130,7 +937,47 @@ void RMFT2::loop2() { 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: @@ -1218,9 +1065,9 @@ int16_t RMFT2::getSignalSlot(int16_t id) { // 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; @@ -1289,26 +1136,26 @@ 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) handleEvent(F("ROTATE"),onRotateLookup,turntableId); + if (change) onRotateLookup->handleEvent(F("ROTATE"),turntableId); } #endif @@ -1317,8 +1164,8 @@ void RMFT2::clockEvent(int16_t clocktime, bool change) { 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); } } @@ -1327,16 +1174,10 @@ void RMFT2::powerEvent(int16_t track, bool overload) { if (Diag::CMD) DIAG(F("Looking for Power event on track : %c"), track); if (overload) { - handleEvent(F("POWER"),onOverloadLookup,track); + onOverloadLookup->handleEvent(F("POWER"),track); } } - -void RMFT2::handleEvent(const FSH* reason,LookList* handlers, int16_t id) { - int pc= handlers->find(id); - if (pc>=0) startNonRecursiveTask(reason,id,pc); -} - void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) { // Check we dont already have a task running this handler RMFT2 * task=loopTask; @@ -1453,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 de14f11..30a2f45 100644 --- a/EXRAIL2.h +++ b/EXRAIL2.h @@ -68,6 +68,9 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, 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 @@ -99,6 +102,8 @@ enum thrunger: byte { 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 @@ -119,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 { @@ -159,7 +171,8 @@ class LookList { 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[]) ; @@ -176,9 +189,7 @@ private: #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 void startNonRecursiveTask(const FSH* reason, int16_t id,int pc); static RMFT2 * loopTask; static RMFT2 * pausingTask; void delayMe(long millisecs); @@ -194,11 +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 Print * LCCSerial; - static LookList * sequenceLookup; + static LookList * routeLookup; static LookList * onThrowLookup; static LookList * onCloseLookup; static LookList * onActivateLookup; @@ -216,6 +227,12 @@ private: 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 @@ -237,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 d44b244..7811a0d 100644 --- a/EXRAIL2MacroReset.h +++ b/EXRAIL2MacroReset.h @@ -39,6 +39,8 @@ #undef AUTOSTART #undef BROADCAST #undef CALL +#undef CLEAR_STASH +#undef CLEAR_ALL_STASH #undef CLOSE #undef DCC_SIGNAL #undef DCC_TURNTABLE @@ -108,6 +110,7 @@ #undef ONCHANGE #undef PARSE #undef PAUSE +#undef PICKUP_STASH #undef PIN_TURNOUT #undef PRINT #ifndef DISABLE_PROG @@ -126,6 +129,11 @@ #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 @@ -147,6 +155,8 @@ #undef SIGNALH #undef SPEED #undef START +#undef STASH +#undef STEALTH #undef STOP #undef THROW #undef TT_ADDPOSITION @@ -179,7 +189,9 @@ #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) @@ -251,6 +263,7 @@ #define PIN_TURNOUT(id,pin,description...) #define PRINT(msg) #define PARSE(msg) +#define PICKUP_STASH(id) #ifndef DISABLE_PROG #define POM(cv,value) #endif @@ -267,6 +280,11 @@ #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) @@ -287,7 +305,9 @@ #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 TT_ADDPOSITION(turntable_id,position,value,angle,description...) 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 8e7fbb9..f79693d 100644 --- a/EXRAILMacros.h +++ b/EXRAILMacros.h @@ -102,6 +102,25 @@ void exrailHalSetup() { #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" @@ -111,7 +130,7 @@ const byte RMFT2::compileFeatures = 0 #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 @@ -153,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 @@ -185,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) @@ -204,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 @@ -335,6 +364,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; #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), @@ -393,6 +424,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; 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), @@ -417,6 +449,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; #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), @@ -438,6 +471,11 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; #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) @@ -459,6 +497,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; #define SIGNALH(redpin,amberpin,greenpin) #define SPEED(speed) OPCODE_SPEED,V(speed), #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 @@ -480,7 +519,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup]; // 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 d2a7fd1..23c6a6e 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "devel-202310230944Z" +#define GITHUB_SHA "devel-202311270714Z" diff --git a/IODevice.h b/IODevice.h index 74fe49b..d12fafd 100644 --- a/IODevice.h +++ b/IODevice.h @@ -542,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_CMRI.cpp b/IO_CMRI.cpp new file mode 100644 index 0000000..1dfa2fc --- /dev/null +++ b/IO_CMRI.cpp @@ -0,0 +1,316 @@ +/* + * © 2023, 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 . + */ + +#include "IO_CMRI.h" +#include "defines.h" + +/************************************************************ + * CMRIbus implementation + ************************************************************/ + +// Constructor for CMRIbus +CMRIbus::CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin) { + _busNo = busNo; + _serial = &serial; + _baud = baud; + _cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds. + _transmitEnablePin = transmitEnablePin; + if (_transmitEnablePin != VPIN_NONE) { + pinMode(_transmitEnablePin, OUTPUT); + ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); // transmitter initially off + } + + // Max message length is 256+6=262 bytes. + // Each byte is one start bit, 8 data bits and 1 or 2 stop bits, assume 11 bits per byte. + // Calculate timeout based on treble this time. + _timeoutPeriod = 3 * 11 * 262 * 1000UL / (_baud / 1000UL); +#if defined(ARDUINOCMRI_COMPATIBLE) + // NOTE: The ArduinoCMRI library, unless modified, contains a 'delay(50)' between + // receiving the end of the prompt message and starting to send the response. This + // is allowed for below. + _timeoutPeriod += 50000UL; +#endif + + // Calculate the time in microseconds to transmit one byte (11 bits max). + _byteTransmitTime = 1000000UL * 11 / _baud; + // Postdelay is only required if we need to allow for data still being sent when + // we want to switch off the transmitter. The flush() method of HardwareSerial + // ensures that the data has completed being sent over the line. + _postDelay = 0; + + // Add device to HAL device chain + IODevice::addDevice(this); + + // Add bus to CMRIbus chain. + _nextBus = _busList; + _busList = this; +} + + +// Main loop function for CMRIbus. +// Work through list of nodes. For each node, in separate loop entries +// send initialisation message (once only); then send +// output message; then send prompt for input data, and +// process any response data received. +// When the slot time has finished, move on to the next device. +void CMRIbus::_loop(unsigned long currentMicros) { + + _currentMicros = currentMicros; + + while (_serial->available()) + processIncoming(); + + // Send any data that needs sending. + processOutgoing(); + +} + +// Send output data to the bus for nominated CMRInode +uint16_t CMRIbus::sendData(CMRInode *node) { + uint16_t numDataBytes = (node->getNumOutputs()+7)/8; + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('T'); // T for Transmit data message + uint16_t charsSent = 6; // include header and trailer + for (uint8_t index=0; indexgetOutputStates(index); + if (value == DLE || value == STX || value == ETX) { + _serial->write(DLE); + charsSent++; + } + _serial->write(value); + charsSent++; + } + _serial->write(ETX); + return charsSent; // number of characters sent +} + +// Send request for input data to nominated CMRInode. +uint16_t CMRIbus::requestData(CMRInode *node) { + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('P'); // P for Poll message + _serial->write(ETX); + return 6; // number of characters sent +} + +// Send initialisation message +uint16_t CMRIbus::sendInitialisation(CMRInode *node) { + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('I'); // I for initialise message + _serial->write(node->getType()); // NDP + _serial->write((uint8_t)0); // dH + _serial->write((uint8_t)0); // dL + _serial->write((uint8_t)0); // NS + _serial->write(ETX); + return 10; // number of characters sent +} + +void CMRIbus::processOutgoing() { + uint16_t charsSent = 0; + if (_currentNode == NULL) { + // If we're between read/write cycles then don't do anything else. + if (_currentMicros - _cycleStartTime < _cycleTime) return; + // ... otherwise start processing the first node in the list + _currentNode = _nodeListStart; + _transmitState = TD_INIT; + _cycleStartTime = _currentMicros; + } + if (_currentNode == NULL) return; + switch (_transmitState) { + case TD_IDLE: + case TD_INIT: + enableTransmitter(); + if (!_currentNode->isInitialised()) { + charsSent = sendInitialisation(_currentNode); + _currentNode->setInitialised(); + _transmitState = TD_TRANSMIT; + delayUntil(_currentMicros+_byteTransmitTime*charsSent); + break; + } + /* fallthrough */ + case TD_TRANSMIT: + charsSent = sendData(_currentNode); + _transmitState = TD_PROMPT; + // Defer next entry for as long as it takes to transmit the characters, + // to allow output queue to empty. Allow 2 bytes extra. + delayUntil(_currentMicros+_byteTransmitTime*(charsSent+2)); + break; + case TD_PROMPT: + charsSent = requestData(_currentNode); + disableTransmitter(); + _transmitState = TD_RECEIVE; + _timeoutStart = _currentMicros; // Start timeout on response + break; + case TD_RECEIVE: // Waiting for response / timeout + if (_currentMicros - _timeoutStart > _timeoutPeriod) { + // End of time slot allocated for responses. + _transmitState = TD_IDLE; + // Reset state of receiver + _receiveState = RD_SYN1; + // Move to next node + _currentNode = _currentNode->getNext(); + } + break; + } +} + +// Process any data bytes received from a CMRInode. +void CMRIbus::processIncoming() { + int data = _serial->read(); + if (data < 0) return; // No characters to read + + if (_transmitState != TD_RECEIVE || !_currentNode) return; // Not waiting for input, so ignore. + + uint8_t nextState = RD_SYN1; // default to resetting state machine + switch(_receiveState) { + case RD_SYN1: + if (data == SYN) nextState = RD_SYN2; + break; + case RD_SYN2: + if (data == SYN) nextState = RD_STX; else nextState = RD_SYN2; + break; + case RD_STX: + if (data == STX) nextState = RD_ADDR; + break; + case RD_ADDR: + // If address doesn't match, then ignore everything until next SYN-SYN-STX. + if (data == _currentNode->getAddress() + 65) nextState = RD_TYPE; + break; + case RD_TYPE: + _receiveDataIndex = 0; // Initialise data pointer + if (data == 'R') nextState = RD_DATA; + break; + case RD_DATA: // data body + if (data == DLE) // escape next character + nextState = RD_ESCDATA; + else if (data == ETX) { // end of data + // End of data message. Protocol has all data in one + // message, so we don't need to wait any more. Allow + // transmitter to proceed with next node in list. + _currentNode = _currentNode->getNext(); + _transmitState = TD_IDLE; + } else { + // Not end yet, so save data byte + _currentNode->saveIncomingData(_receiveDataIndex++, data); + nextState = RD_DATA; // wait for more data + } + break; + case RD_ESCDATA: // escaped data byte + _currentNode->saveIncomingData(_receiveDataIndex++, data); + nextState = RD_DATA; + break; + } + _receiveState = nextState; +} + +// If configured for half duplex RS485, switch RS485 interface +// into transmit mode. +void CMRIbus::enableTransmitter() { + if (_transmitEnablePin != VPIN_NONE) + ArduinoPins::fastWriteDigital(_transmitEnablePin, 1); + // If we need a delay before we start the packet header, + // we can send a character or two to synchronise the + // transmitter and receiver. + // SYN characters should be used, but a bug in the + // ArduinoCMRI library causes it to ignore the packet if + // it's preceded by an odd number of SYN characters. + // So send a SYN followed by a NUL in that case. + _serial->write(SYN); +#if defined(ARDUINOCMRI_COMPATIBLE) + _serial->write(NUL); // Reset the ArduinoCMRI library's parser +#endif +} + +// If configured for half duplex RS485, switch RS485 interface +// into receive mode. +void CMRIbus::disableTransmitter() { + // Wait until all data has been transmitted. On the standard + // AVR driver, this waits until the FIFO is empty and all + // data has been sent over the link. + _serial->flush(); + // If we don't trust the 'flush' function and think the + // data's still in transit, then wait a bit longer. + if (_postDelay > 0) + delayMicroseconds(_postDelay); + // Hopefully, we can now safely switch off the transmitter. + if (_transmitEnablePin != VPIN_NONE) + ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); +} + +// Link to chain of CMRI bus instances +CMRIbus *CMRIbus::_busList = NULL; + + +/************************************************************ + * CMRInode implementation + ************************************************************/ + +// Constructor for CMRInode object +CMRInode::CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs, uint16_t outputs) { + _firstVpin = firstVpin; + _nPins = nPins; + _busNo = busNo; + _address = address; + _type = type; + + switch (_type) { + case 'M': // SMINI, fixed 24 inputs and 48 outputs + _numInputs = 24; + _numOutputs = 48; + break; + case 'C': // CPNODE with 16 to 144 inputs/outputs using 8-bit cards + _numInputs = inputs; + _numOutputs = outputs; + break; + case 'N': // Classic USIC and SUSIC using 24 bit i/o cards + case 'X': // SUSIC using 32 bit i/o cards + default: + DIAG(F("CMRInode: bus:%d address:%d ERROR unsupported type %c"), _busNo, _address, _type); + return; // Don't register device. + } + if ((unsigned int)_nPins < _numInputs + _numOutputs) + DIAG(F("CMRInode: bus:%d address:%d WARNING number of Vpins does not cover all inputs and outputs"), _busNo, _address); + + // Allocate memory for states + _inputStates = (uint8_t *)calloc((_numInputs+7)/8, 1); + _outputStates = (uint8_t *)calloc((_numOutputs+7)/8, 1); + if (!_inputStates || !_outputStates) { + DIAG(F("CMRInode: ERROR insufficient memory")); + return; + } + + // Add this device to HAL device list + IODevice::addDevice(this); + + // Add CMRInode to CMRIbus object. + CMRIbus *bus = CMRIbus::findBus(_busNo); + if (bus != NULL) { + bus->addNode(this); + return; + } +} + diff --git a/IO_CMRI.h b/IO_CMRI.h new file mode 100644 index 0000000..ef647b8 --- /dev/null +++ b/IO_CMRI.h @@ -0,0 +1,293 @@ +/* + * © 2023, 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 . + */ + +/* + * CMRIbus + * ======= + * To define a CMRI bus, example syntax: + * CMRIbus::create(bus, serial, baud[, cycletime[, pin]]); + * + * bus = 0-255 + * serial = serial port to be used (e.g. Serial3) + * baud = baud rate (9600, 19200, 28800, 57600 or 115200) + * cycletime = minimum time between successive updates/reads of a node in millisecs (default 500ms) + * pin = pin number connected to RS485 module's DE and !RE terminals for half-duplex operation (default VPIN_NONE) + * + * Each bus must use a different serial port. + * + * IMPORTANT: If you are using ArduinoCMRI library code by Michael Adams, at the time of writing this library + * is not compliant with the LCS-9.10.1 specification for CMRInet protocol. + * Various work-arounds may be enabled within the driver by adding the following line to your config.h file, + * to allow nodes running the ArduinoCMRI library to communicate: + * + * #define ARDUINOCMRI_COMPATIBLE + * + * CMRINode + * ======== + * To define a CMRI node and associate it with a CMRI bus, + * CMRInode::create(firstVPIN, numVPINs, bus, address, type [, inputs, outputs]); + * + * firstVPIN = first vpin in block allocated to this device + * numVPINs = number of vpins (e.g. 72 for an SMINI node) + * bus = 0-255 + * address = 0-127 + * type = 'M' for SMINI (fixed 24 inputs and 48 outputs) + * 'C' for CPNODE (16 to 144 inputs/outputs in groups of 8) + * (other types are not supported at this time). + * inputs = number of inputs (CPNODE only) + * outputs = number of outputs (CPNODE only) + * + * Reference: "LCS-9.10.1 + * Layout Control Specification: CMRInet Protocol + * Version 1.1 December 2014." + */ + +#ifndef IO_CMRI_H +#define IO_CMRI_H + +#include "IODevice.h" + +/********************************************************************** + * CMRInode class + * + * This encapsulates the state associated with a single CMRI node, + * which includes the address type, number of inputs and outputs, and + * the states of the inputs and outputs. + **********************************************************************/ +class CMRInode : public IODevice { +private: + uint8_t _busNo; + uint8_t _address; + char _type; + CMRInode *_next = NULL; + uint8_t *_inputStates = NULL; + uint8_t *_outputStates = NULL; + uint16_t _numInputs = 0; + uint16_t _numOutputs = 0; + bool _initialised = false; + +public: + static void create(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs=0, uint16_t outputs=0) { + if (checkNoOverlap(firstVpin, nPins)) new CMRInode(firstVpin, nPins, busNo, address, type, inputs, outputs); + } + CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs=0, uint16_t outputs=0); + + uint8_t getAddress() { + return _address; + } + CMRInode *getNext() { + return _next; + } + void setNext(CMRInode *node) { + _next = node; + } + bool isInitialised() { + return _initialised; + } + void setInitialised() { + _initialised = true; + } + + void _begin() { + _initialised = false; + } + + int _read(VPIN vpin) { + // Return current state from this device + uint16_t pin = vpin - _firstVpin; + if (pin < _numInputs) { + uint8_t mask = 1 << (pin & 0x7); + int index = pin / 8; + return (_inputStates[index] & mask) != 0; + } else + return 0; + } + + void _write(VPIN vpin, int value) { + // Update current state for this device, in preparation the bus transmission + uint16_t pin = vpin - _firstVpin - _numInputs; + if (pin < _numOutputs) { + uint8_t mask = 1 << (pin & 0x7); + int index = pin / 8; + if (value) + _outputStates[index] |= mask; + else + _outputStates[index] &= ~mask; + } + } + + void saveIncomingData(uint8_t index, uint8_t data) { + if (index < (_numInputs+7)/8) + _inputStates[index] = data; + } + + uint8_t getOutputStates(uint8_t index) { + if (index < (_numOutputs+7)/8) + return _outputStates[index]; + else + return 0; + } + + uint16_t getNumInputs() { + return _numInputs; + } + + uint16_t getNumOutputs() { + return _numOutputs; + } + + char getType() { + return _type; + } + + uint8_t getBusNumber() { + return _busNo; + } + + void _display() override { + DIAG(F("CMRInode type:'%c' configured on bus:%d address:%d VPINs:%u-%u (in) %u-%u (out)"), + _type, _busNo, _address, _firstVpin, _firstVpin+_numInputs-1, + _firstVpin+_numInputs, _firstVpin+_numInputs+_numOutputs-1); + } + +}; + +/********************************************************************** + * CMRIbus class + * + * This encapsulates the properties state of the bus and the + * transmission and reception of data across that bus. Each CMRIbus + * object owns a set of CMRInode objects which represent the nodes + * attached to that bus. + **********************************************************************/ +class CMRIbus : public IODevice { +private: + // Here we define the device-specific variables. + uint8_t _busNo; + HardwareSerial *_serial; + unsigned long _baud; + VPIN _transmitEnablePin = VPIN_NONE; + CMRInode *_nodeListStart = NULL, *_nodeListEnd = NULL; + CMRInode *_currentNode = NULL; + + // Transmitter state machine states + enum {TD_IDLE, TD_PRETRANSMIT, TD_INIT, TD_TRANSMIT, TD_PROMPT, TD_RECEIVE}; + uint8_t _transmitState = TD_IDLE; + // Receiver state machine states. + enum {RD_SYN1, RD_SYN2, RD_STX, RD_ADDR, RD_TYPE, + RD_DATA, RD_ESCDATA, RD_SKIPDATA, RD_SKIPESCDATA, RD_ETX}; + uint8_t _receiveState = RD_SYN1; + uint16_t _receiveDataIndex = 0; // Index of next data byte to be received. + CMRIbus *_nextBus = NULL; // Pointer to next bus instance in list. + unsigned long _cycleStartTime = 0; + unsigned long _timeoutStart = 0; + unsigned long _cycleTime; // target time between successive read/write cycles, microseconds + unsigned long _timeoutPeriod; // timeout on read responses, in microseconds. + unsigned long _currentMicros; // last value of micros() from _loop function. + unsigned long _postDelay; // delay time after transmission before switching off transmitter (in us) + unsigned long _byteTransmitTime; // time in us for transmission of one byte + + static CMRIbus *_busList; // linked list of defined bus instances + + // Definition of special characters in CMRInet protocol + enum : uint8_t { + NUL = 0x00, + STX = 0x02, + ETX = 0x03, + DLE = 0x10, + SYN = 0xff, + }; + +public: + static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS=500, VPIN transmitEnablePin=VPIN_NONE) { + new CMRIbus(busNo, serial, baud, cycleTimeMS, transmitEnablePin); + } + + // Device-specific initialisation + void _begin() override { + // CMRInet spec states one stop bit, JMRI and ArduinoCMRI use two stop bits +#if defined(ARDUINOCMRI_COMPATIBLE) + _serial->begin(_baud, SERIAL_8N2); +#else + _serial->begin(_baud, SERIAL_8N1); +#endif + #if defined(DIAG_IO) + _display(); + #endif + } + + // Loop function (overriding IODevice::_loop(unsigned long)) + void _loop(unsigned long currentMicros) override; + + // Display information about the device + void _display() override { + DIAG(F("CMRIbus %d configured, speed=%d baud, cycle=%d ms"), _busNo, _baud, _cycleTime/1000); + } + + // Locate CMRInode object with specified address. + CMRInode *findNode(uint8_t address) { + for (CMRInode *node = _nodeListStart; node != NULL; node = node->getNext()) { + if (node->getAddress() == address) + return node; + } + return NULL; + } + + // Add new CMRInode to the list of nodes for this bus. + void addNode(CMRInode *newNode) { + if (!_nodeListStart) + _nodeListStart = newNode; + if (!_nodeListEnd) + _nodeListEnd = newNode; + else { + _nodeListEnd->setNext(newNode); + _nodeListEnd = newNode; + } + } + +protected: + CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin); + uint16_t sendData(CMRInode *node); + uint16_t requestData(CMRInode *node); + uint16_t sendInitialisation(CMRInode *node); + + // Process any data bytes received from a CMRInode. + void processIncoming(); + // Process any outgoing traffic that is due. + void processOutgoing(); + // Enable transmitter + void enableTransmitter(); + // Disable transmitter and enable receiver + void disableTransmitter(); + + +public: + uint8_t getBusNumber() { + return _busNo; + } + + static CMRIbus *findBus(uint8_t busNo) { + for (CMRIbus *bus=_busList; bus!=NULL; bus=bus->_nextBus) { + if (bus->_busNo == busNo) return bus; + } + return NULL; + } +}; + +#endif // IO_CMRI_H \ No newline at end of file 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; + #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 20a91d3..07ff93f 100644 --- a/MotorDriver.h +++ b/MotorDriver.h @@ -3,7 +3,7 @@ * © 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 @@ -148,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); } @@ -168,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); @@ -232,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; @@ -263,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 b40de1c..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); @@ -230,4 +256,3 @@ void StringFormatter::printHex(Print * stream,uint16_t value) { result[4]='\0'; stream->print(result); } - \ No newline at end of file diff --git a/StringFormatter.h b/StringFormatter.h index 1231d54..25d15e2 100644 --- a/StringFormatter.h +++ b/StringFormatter.h @@ -54,6 +54,5 @@ class StringFormatter 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 dedf45e..7d2b36b 100644 --- a/TrackManager.cpp +++ b/TrackManager.cpp @@ -1,6 +1,6 @@ /* * © 2022 Chris Harlow - * © 2022 Harald Barth + * © 2022,2023 Harald Barth * © 2023 Colin Murdoch * All rights reserved. * @@ -45,11 +45,15 @@ 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; @@ -87,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; @@ -197,17 +201,20 @@ void TrackManager::setPROGSignal( bool on) { void TrackManager::setDCSignal(int16_t cab, byte speedbyte) { FOR_EACH_TRACK(t) { if (trackDCAddr[t]!=cab && cab != 0) 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 (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; @@ -223,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 { @@ -249,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 @@ -274,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 @@ -282,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; @@ -300,32 +326,33 @@ 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 @@ -353,49 +380,79 @@ 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(""); - bool pstate = TrackManager::isPowerOn(t); +const FSH* TrackManager::getModeName(TRACK_MODE tm) { + const FSH *modename=F("---"); - switch(track[t]->getMode()) { - case TRACK_MODE_MAIN: - if (pstate) {format=F("<= %c MAIN ON>\n");} else {format = F("<= %c MAIN OFF>\n");} - break; -#ifndef DISABLE_PROG - case TRACK_MODE_PROG: - if (pstate) {format=F("<= %c PROG ON>\n");} else {format=F("<= %c PROG OFF>\n");} - break; -#endif - case TRACK_MODE_NONE: - if (pstate) {format=F("<= %c NONE ON>\n");} else {format=F("<= %c NONE OFF>\n");} - break; - case TRACK_MODE_EXT: - if (pstate) {format=F("<= %c EXT ON>\n");} else {format=F("<= %c EXT OFF>\n");} - break; - case TRACK_MODE_DC: - if (pstate) {format=F("<= %c DC %d ON>\n");} else {format=F("<= %c DC %d OFF>\n");} - break; - case TRACK_MODE_DCX: - if (pstate) {format=F("<= %c DCX %d ON>\n");} else {format=F("<= %c DCX %d OFF>\n");} - break; - default: - break; // unknown, dont care + 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"); } +#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; +} - if (stream) StringFormatter::send(stream,format,'A'+t, trackDCAddr[t]); - else CommandDistributor::broadcastTrackState(format,'A'+t, trackDCAddr[t]); +// 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(); + } } @@ -411,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; } @@ -425,77 +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,bool setJoin, POWERMODE mode) { - if (!setProg) mainPowerGuess=mode; - FOR_EACH_TRACK(t) { - - TrackManager::setTrackPower(setProg, setJoin, mode, t); - +// 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); } - return; -} - -void TrackManager::setTrackPower(bool setProg, bool setJoin, POWERMODE mode, byte thistrack) { - - //DIAG(F("SetTrackPower Processing Track %d"), thistrack); - MotorDriver * driver=track[thistrack]; - if (!driver) return; - - switch (track[thistrack]->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: - //DIAG(F("Processing track - %d setProg %d"), thistrack, setProg); - if (setProg || setJoin) break; - driver->setBrake(true); // DC starts with brake on - applyDCSpeed(thistrack); // speed match DCC throttles - driver->setPower(mode); - break; - case TRACK_MODE_PROG: - if (!setProg && !setJoin) 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; - } - - } - - void TrackManager::reportPowerChange(Print* stream, byte thistrack) { - // This function is for backward JMRI compatibility only - // It reports the first track only, as main, regardless of track settings. - // - int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue()); - StringFormatter::send(stream, F("\n"), - track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent); -} - -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 @@ -537,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; @@ -566,12 +636,12 @@ bool TrackManager::isPowerOn(byte t) { } bool TrackManager::isProg(byte t) { - if (track[t]->getMode()==TRACK_MODE_PROG) + if (track[t]->getMode() & TRACK_MODE_PROG) return true; return false; } -byte TrackManager::returnMode(byte t) { +TRACK_MODE TrackManager::getMode(byte t) { return (track[t]->getMode()); } @@ -579,18 +649,3 @@ int16_t TrackManager::returnDCAddr(byte t) { return (trackDCAddr[t]); } -const char* TrackManager::getModeName(byte Mode) { - - //DIAG(F("PowerMode %d"), Mode); - -switch (Mode) - { - case 1: return "NONE"; - case 2: return "MAIN"; - case 4: return "PROG"; - case 8: return "DC"; - case 16: return "DCX"; - case 32: return "EXT"; - default: return "----"; - } -} diff --git a/TrackManager.h b/TrackManager.h index d197751..6310030 100644 --- a/TrackManager.h +++ b/TrackManager.h @@ -62,37 +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,bool joinTrack,POWERMODE mode); static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);} - static void setMainPower(POWERMODE mode) {setPower2(false,false,mode);} - static void setProgPower(POWERMODE mode) {setPower2(true,false,mode);} - static void setJoinPower(POWERMODE mode) {setPower2(false,true,mode);} - static void setTrackPower(bool setProg, bool setJoin, POWERMODE mode, byte thistrack); - + 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 reportPowerChange(Print* stream, byte thistrack); static void reportObsoleteCurrent(Print* stream); static void streamTrackState(Print* stream, byte t); static bool isPowerOn(byte t); static bool isProg(byte t); - static byte returnMode(byte t); + static TRACK_MODE getMode(byte t); static int16_t returnDCAddr(byte t); - static const char* getModeName(byte Mode); + static const FSH* getModeName(TRACK_MODE Mode); static int16_t joinRelay; static bool progTrackSyncMain; // true when prog track is a siding switched to main @@ -109,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/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 ab36957..8b2251a 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -201,17 +201,19 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password, // Display the AT version information StringFormatter::send(wifiStream, F("AT+GMR\r\n")); if (checkForOK(2000, F("AT version:"), true, false)) { - char version[] = "0.0.0.0"; - for (int i=0; i<8;i++) { + 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')) { - SSid = F("DCCEX_SAYS_BROKEN_FIRMWARE"); - forceAP = true; - } + } + 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); 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 e90d7f4..14dd1c5 100644 --- a/defines.h +++ b/defines.h @@ -219,11 +219,10 @@ // The HAL is disabled by default on Nano and Uno platforms, because of limited flash space. // #if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO) - #if defined(DISABLE_DIAG) && defined(DISABLE_EEPROM) && defined(DISABLE_PROG) - #warning you have sacrificed DIAG for HAL - #else - #define IO_NO_HAL - #endif +#define IO_NO_HAL // HAL too big whatever you disable otherwise +#ifndef ENABLE_VDPY +#define DISABLE_VDPY +#endif #endif #if __has_include ( "myAutomation.h") diff --git a/myHal.cpp_example.txt b/myHal.cpp_example.txt index 5533554..541c018 100644 --- a/myHal.cpp_example.txt +++ b/myHal.cpp_example.txt @@ -25,6 +25,16 @@ //#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 + +//========================================================================== +// also for CMRI connection using RS485 TTL module +//========================================================================== +// define UARt2 pins for ESP32 Rx=16, Tx=17 -- can conflict if sabertooth defined +//HardwareSerial mySerial2(2); // use UART2 +// +// for SERIAL_8N2 include this in config.h +// #define ARDUINOCMRI_COMPATIBLE //========================================================================== // The function halSetup() is invoked from CS if it exists within the build. @@ -34,6 +44,36 @@ void halSetup() { +//========================================================================== +// CMRI bus and nodes defined +//========================================================================== +// further explanation in IO_CMRI.h +// this example is being used to test connection of existing CMRI device +// add lines to myHal.cpp within halSetup() + +// for ESP32 +//mySerial2.begin(9600, SERIAL_8N2, 16, 17); // ESP32 to define pins also check DCCTimerESP.cpp +//CMRIbus::create(0, mySerial2, 9600, 500, 4); // for ESP32 + +// for Mega +//CMRIbus::create(0, Serial3, 9600, 500, 38); // for Mega - Serial3 already defined + // bus=0 always, unless multiple serial ports are used + // baud=9600 to match setting in existing CMRI nodes + // cycletime.. 500ms is default -- more frequent might be needed on master + // pin.. DE/!RE pins tied together on TTL RS485 module. + // pin 38 should work on Mega and F411RE (pin D38 aka PB12 on CN10_16) + +//CMRInode::create(900, 72, 0, 4, 'M'); +//CMRInode::create(1000, 72, 0, 5, 'M'); + // bus=0 must agree with bus in CMRIbus + // node=4 number to agree with node numbering + // 'M' is for SMINI. + // Starting VPin, Number of VPins=72 for SMINI +//========================================================================== +// end of CMRI +//========================================================================== + + //======================================================================= // The following directives define auxiliary display devices. // These can be defined in addition to the system display (display diff --git a/mySetup_h_cmri.txt b/mySetup_h_cmri.txt new file mode 100644 index 0000000..405c3b1 --- /dev/null +++ b/mySetup_h_cmri.txt @@ -0,0 +1,128 @@ +// mySetup.h +// defining CMRI accessories +// CMRI connections defined in myHal.cpp +// +// this is for testing. +SETUP("D CMD 1"); +// Turnouts defined in myAutomation.h can include descriptions which will appear in Engine Driver +// Sensors and digital outputs do not require pre-definition for use in EXRAIL automation +// +// SMINI emulation node 24-input/48-outputs +// the sketch I use +// 16 or 24 input pins +// 32 or 48 output pins +// +// Define 16 input pins 1000-1015 +SETUP("S 1000 1000 1"); +SETUP("S 1001 1001 1"); +SETUP("S 1002 1002 1"); +SETUP("S 1003 1003 1"); +SETUP("S 1004 1004 1"); +SETUP("S 1005 1005 1"); +SETUP("S 1006 1006 1"); +SETUP("S 1007 1007 1"); +SETUP("S 1008 1008 1"); +SETUP("S 1009 1009 1"); +SETUP("S 1010 1010 1"); +SETUP("S 1011 1011 1"); +SETUP("S 1012 1012 1"); +SETUP("S 1013 1013 1"); +SETUP("S 1014 1014 1"); +SETUP("S 1015 1015 1"); +// +// define 16 turnouts using VPIN (for Throw/Close commands via CMRI) +SETUP("T 1024 VPIN 1024"); +SETUP("T 1025 VPIN 1025"); +SETUP("T 1026 VPIN 1026"); +SETUP("T 1027 VPIN 1027"); +SETUP("T 1028 VPIN 1028"); +SETUP("T 1029 VPIN 1029"); +SETUP("T 1030 VPIN 1030"); +SETUP("T 1031 VPIN 1031"); +SETUP("T 1032 VPIN 1032"); +SETUP("T 1033 VPIN 1033"); +SETUP("T 1034 VPIN 1034"); +SETUP("T 1035 VPIN 1035"); +SETUP("T 1036 VPIN 1036"); +SETUP("T 1037 VPIN 1037"); +SETUP("T 1038 VPIN 1038"); +SETUP("T 1039 VPIN 1039"); +// +// define 16 pins for digital outputs +SETUP("Z 1040 1040 0"); +SETUP("Z 1041 1041 0"); +SETUP("Z 1042 1042 0"); +SETUP("Z 1043 1043 0"); +SETUP("Z 1044 1044 0"); +SETUP("Z 1045 1045 0"); +SETUP("Z 1046 1046 0"); +SETUP("Z 1047 1047 0"); +SETUP("Z 1048 1048 0"); +SETUP("Z 1049 1049 0"); +SETUP("Z 1050 1050 0"); +SETUP("Z 1051 1051 0"); +SETUP("Z 1052 1052 0"); +SETUP("Z 1053 1053 0"); +SETUP("Z 1054 1054 0"); +SETUP("Z 1055 1055 0"); +// +// additional 16 outputs available 1056-1071 +//SETUP("Z 1056 1056 0"); +// +// CMRI sketch used for testing available here +// https://www.trainboard.com/highball/index.php?threads/24-in-48-out-card-for-jmri.116454/page-2#post-1141569 +// + +// Define 16 input pins 900-915 +SETUP("S 900 900 1"); +SETUP("S 901 901 1"); +SETUP("S 902 902 1"); +SETUP("S 903 903 1"); +SETUP("S 904 904 1"); +SETUP("S 905 905 1"); +SETUP("S 906 906 1"); +SETUP("S 907 907 1"); +SETUP("S 908 908 1"); +SETUP("S 909 909 1"); +SETUP("S 910 910 1"); +SETUP("S 911 911 1"); +SETUP("S 912 912 1"); +SETUP("S 913 913 1"); +SETUP("S 914 914 1"); +SETUP("S 915 915 1"); +// +// define 16 turnouts using VPIN (for Throw/Close commands via CMRI) +SETUP("T 924 VPIN 924"); +SETUP("T 925 VPIN 925"); +SETUP("T 926 VPIN 926"); +SETUP("T 927 VPIN 927"); +SETUP("T 928 VPIN 928"); +SETUP("T 929 VPIN 929"); +SETUP("T 930 VPIN 930"); +SETUP("T 931 VPIN 931"); +SETUP("T 932 VPIN 932"); +SETUP("T 933 VPIN 933"); +SETUP("T 934 VPIN 934"); +SETUP("T 935 VPIN 935"); +SETUP("T 936 VPIN 936"); +SETUP("T 937 VPIN 937"); +SETUP("T 938 VPIN 938"); +SETUP("T 939 VPIN 939"); +// +// define 16 pins for digital outputs +SETUP("Z 940 940 0"); +SETUP("Z 941 941 0"); +SETUP("Z 942 942 0"); +SETUP("Z 943 943 0"); +SETUP("Z 944 944 0"); +SETUP("Z 945 945 0"); +SETUP("Z 946 946 0"); +SETUP("Z 947 947 0"); +SETUP("Z 948 948 0"); +SETUP("Z 949 949 0"); +SETUP("Z 950 950 0"); +SETUP("Z 951 951 0"); +SETUP("Z 952 952 0"); +SETUP("Z 953 953 0"); +SETUP("Z 954 954 0"); +SETUP("Z 955 955 0"); diff --git a/version.h b/version.h index 3c98c21..2e12d24 100644 --- a/version.h +++ b/version.h @@ -3,8 +3,37 @@ #include "StringFormatter.h" -#define VERSION "5.1.17eth" -// 5.1.17e - Initial ethernet code for STM32F429ZI and F439ZI boards +#define VERSION "5.2.14eth" +// 5.2.14eth - Initial ethernet code for STM32F429ZI and F439ZI boards +// - CMRI RS485 connection +// 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.