/* * © 2024 Paul M. Antoine * © 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 . */ /* EXRAILPlus planned FEATURE additions F1. [DONE] DCC accessory packet opcodes (short and long form) F2. [DONE] ONAccessory catchers F3. [DONE] Turnout descriptions for Withrottle F4. [DONE] Oled announcements (depends on HAL) F5. [DONE] Withrottle roster info F6. Multi-occupancy semaphore F7. [DONE see AUTOSTART] Self starting sequences F8. Park/unpark F9. [DONE] Analog drive F10. [DONE] Alias anywhere F11. [DONE]EXRAIL/ENDEXRAIL unnecessary F12. [DONE] Allow guarded code (as effect of ALIAS anywhere) F13. [DONE] IFGTE/IFLT function */ /* EXRAILPlus planned TRANSPARENT additions T1. [DONE] RAM based fast lookup for sequences ON* event catchers and signals. T2. Extend to >64k */ #include #include "defines.h" #include "EXRAIL2.h" #include "DCC.h" #include "DCCWaveform.h" #include "DIAG.h" #include "WiThrottle.h" #include "DCCEXParser.h" #include "Turnouts.h" #include "CommandDistributor.h" #include "TrackManager.h" #include "Turntables.h" #include "IODevice.h" #include "EXRAILSensor.h" // 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. // The threads exist in a ring, each time through loop() the next thread in the ring is serviced. // Statics const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prog track bool RMFT2::diag=false; // RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring. RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE. // when pausingTask is set, that is the ONLY task that gets any service, // and all others will have their locos stopped, then resumed after the pausing task resumes. byte RMFT2::flags[MAX_FLAGS]; Print * RMFT2::LCCSerial=0; LookList * RMFT2::routeLookup=NULL; LookList * RMFT2::signalLookup=NULL; LookList * RMFT2::onThrowLookup=NULL; LookList * RMFT2::onCloseLookup=NULL; LookList * RMFT2::onActivateLookup=NULL; LookList * RMFT2::onDeactivateLookup=NULL; LookList * RMFT2::onRedLookup=NULL; LookList * RMFT2::onAmberLookup=NULL; LookList * RMFT2::onGreenLookup=NULL; LookList * RMFT2::onChangeLookup=NULL; LookList * RMFT2::onClockLookup=NULL; #ifndef IO_NO_HAL LookList * RMFT2::onRotateLookup=NULL; #endif LookList * RMFT2::onOverloadLookup=NULL; LookList * RMFT2::onBlockEnterLookup=NULL; LookList * RMFT2::onBlockExitLookup=NULL; byte * RMFT2::routeStateArray=nullptr; const FSH * * RMFT2::routeCaptionArray=nullptr; int16_t * RMFT2::stashArray=nullptr; int16_t RMFT2::maxStashId=0; // getOperand instance version, uses progCounter from instance. uint16_t RMFT2::getOperand(byte n) { return getOperand(progCounter,n); } // getOperand static version, must be provided prog counter from loop etc. uint16_t RMFT2::getOperand(int progCounter,byte n) { int offset=progCounter+1+(n*3); byte lsb=GETHIGHFLASH(RouteCode,offset); byte msb=GETHIGHFLASH(RouteCode,offset+1); return msb<<8|lsb; } 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]; } } void LookList::add(int16_t lookup, int16_t result) { if (m_loaded==m_size) return; // and forget m_lookupArray[m_loaded]=lookup; m_resultArray[m_loaded]=result; m_loaded++; } 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, int16_t loco) { // 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;iadd(getOperand(progCounter,0),progCounter); } return list; } /* static */ void RMFT2::begin() { //DIAG(F("EXRAIL RoutCode at =%P"),RouteCode); bool saved_diag=diag; diag=true; DCCEXParser::setRMFTFilter(RMFT2::ComandFilter); for (int f=0;fchain(LookListLoader(OPCODE_SEQUENCE)); if (compileFeatures && FEATURE_ROUTESTATE) { routeStateArray=(byte *)calloc(routeLookup->size(),sizeof(byte)); routeCaptionArray=(const FSH * *)calloc(routeLookup->size(),sizeof(const FSH *)); } onThrowLookup=LookListLoader(OPCODE_ONTHROW); onCloseLookup=LookListLoader(OPCODE_ONCLOSE); onActivateLookup=LookListLoader(OPCODE_ONACTIVATE); onDeactivateLookup=LookListLoader(OPCODE_ONDEACTIVATE); onChangeLookup=LookListLoader(OPCODE_ONCHANGE); onClockLookup=LookListLoader(OPCODE_ONTIME); #ifndef IO_NO_HAL onRotateLookup=LookListLoader(OPCODE_ONROTATE); #endif onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD); if (compileFeatures & FEATURE_BLOCK) { onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER); onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT); } // onLCCLookup is not the same so not loaded here. // Second pass startup, define any turnouts or servos, set signals red // add sequences onRoutines to the lookups if (compileFeatures & FEATURE_SIGNAL) { onRedLookup=LookListLoader(OPCODE_ONRED); onAmberLookup=LookListLoader(OPCODE_ONAMBER); onGreenLookup=LookListLoader(OPCODE_ONGREEN); // Load the signal lookup with slot numbers in the signal table int signalCount=0; for (int16_t slot=0;;slot++) { SIGNAL_DEFINITION signal=getSignalSlot(slot); DIAG(F("Signal s=%d id=%d t=%d"),slot,signal.id,signal.type); if (signal.type==sigtypeNoMoreSignals) break; if (signal.type==sigtypeContinuation) continue; signalCount++; } signalLookup=new LookList(signalCount); for (int16_t slot=0;;slot++) { SIGNAL_DEFINITION signal=getSignalSlot(slot); if (signal.type==sigtypeNoMoreSignals) break; if (signal.type==sigtypeContinuation) continue; signalLookup->add(signal.id,slot); doSignal(signal.id, SIGNAL_RED); } } int progCounter; for (progCounter=0;; SKIPOP){ byte opcode=GET_OPCODE; if (opcode==OPCODE_ENDEXRAIL) break; VPIN operand=getOperand(progCounter,0); switch (opcode) { case OPCODE_AT: case OPCODE_ATTIMEOUT2: case OPCODE_AFTER: case OPCODE_IF: case OPCODE_IFNOT: { int16_t pin = (int16_t)operand; if (pin<0) pin = -pin; DIAG(F("EXRAIL input VPIN %u"),pin); 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: case OPCODE_IFGTE: case OPCODE_IFLT: case OPCODE_DRIVE: { DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand); IODevice::configureAnalogIn((VPIN)operand); break; } case OPCODE_ONSENSOR: if (compileFeatures & FEATURE_SENSOR) new EXRAILSensor(operand,progCounter+3,true ); break; case OPCODE_ONBUTTON: if (compileFeatures & FEATURE_SENSOR) new EXRAILSensor(operand,progCounter+3,false ); break; case OPCODE_TURNOUT: { VPIN id=operand; int addr=getOperand(progCounter,1); byte subAddr=getOperand(progCounter,2); setTurnoutHiddenState(DCCTurnout::create(id,addr,subAddr)); break; } case OPCODE_SERVOTURNOUT: { VPIN id=operand; VPIN pin=getOperand(progCounter,1); int activeAngle=getOperand(progCounter,2); int inactiveAngle=getOperand(progCounter,3); int profile=getOperand(progCounter,4); setTurnoutHiddenState(ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile)); break; } case OPCODE_PINTURNOUT: { VPIN id=operand; VPIN pin=getOperand(progCounter,1); setTurnoutHiddenState(VpinTurnout::create(id,pin)); break; } #ifndef IO_NO_HAL case OPCODE_DCCTURNTABLE: { VPIN id=operand; int home=getOperand(progCounter,1); setTurntableHiddenState(DCCTurntable::create(id)); Turntable *tto=Turntable::get(id); tto->addPosition(0,0,home); break; } case OPCODE_EXTTTURNTABLE: { VPIN id=operand; VPIN pin=getOperand(progCounter,1); int home=getOperand(progCounter,2); setTurntableHiddenState(EXTTTurntable::create(id,pin)); Turntable *tto=Turntable::get(id); tto->addPosition(0,0,home); break; } case OPCODE_TTADDPOSITION: { VPIN id=operand; int position=getOperand(progCounter,1); int value=getOperand(progCounter,2); int angle=getOperand(progCounter,3); Turntable *tto=Turntable::get(id); tto->addPosition(position,value,angle); break; } #endif case OPCODE_AUTOSTART: // automatically create a task from here at startup. // Removed if (progCounter>0) check 4.2.31 because // default start it top of file is now removed. . new RMFT2(progCounter); break; default: // Ignore break; } } SKIPOP; // include ENDROUTES opcode 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; } void RMFT2::setTurnoutHiddenState(Turnout * t) { // turnout descriptions are in low flash F strings const FSH *desc = getTurnoutDescription(t->getId()); if (desc) t->setHidden(GETFLASH(desc)==0x01); } #ifndef IO_NO_HAL void RMFT2::setTurntableHiddenState(Turntable * tto) { const FSH *desc = getTurntableDescription(tto->getId()); if (desc) tto->setHidden(GETFLASH(desc)==0x01); } #endif char RMFT2::getRouteType(int16_t id) { 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'; } RMFT2::RMFT2(int progCtr, int16_t _loco) { progCounter=progCtr; // get an unused task id from the flags table taskId=255; // in case of overflow for (int f=0;fnext; loopTask->next=this; } } RMFT2::~RMFT2() { // estop my loco if this is not an ONevent // (prevents DONE stopping loco at the end of an // ONBLOCKENTER or ONBLOCKEXIT ) if (loco>0 && this->onEventStartPosition==-1) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco)); setFlag(taskId,0,TASK_FLAG); // we are no longer using this id if (next==this) loopTask=NULL; else for (RMFT2* ring=next;;ring=ring->next) if (ring->next == this) { ring->next=next; loopTask=next; break; } } void RMFT2::createNewTask(int route, uint16_t cab) { int pc=routeLookup->find(route); if (pc<0) return; new RMFT2(pc,cab); } bool RMFT2::readSensor(uint16_t sensorId) { // Exrail operands are unsigned but we need the signed version as inserted by the macros. int16_t sId=(int16_t) sensorId; VPIN vpin=abs(sId); if (getFlag(vpin,LATCH_FLAG)) return true; // latched on // negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting) bool s= IODevice::read(vpin) ^ (sId<0); if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sId); return s; } // This skips to the end of an if block, or to the ELSE within it. bool RMFT2::skipIfBlock() { // returns false if killed short nest = 1; while (nest > 0) { SKIPOP; byte opcode = GET_OPCODE; // all other IF type commands increase the nesting level if (opcode>IF_TYPE_OPCODES) nest++; else switch(opcode) { case OPCODE_ENDEXRAIL: kill(F("missing ENDIF"), nest); return false; case OPCODE_ENDIF: nest--; break; case OPCODE_ELSE: // if nest==1 then this is the ELSE for the IF we are skipping if (nest==1) nest=0; // cause loop exit and return after ELSE break; default: break; } } return true; } /* static */ void RMFT2::readLocoCallback(int16_t cv) { if (cv <= 0) { DIAG(F("CV read error")); progtrackLocoId = -1; return; } if (cv & LONG_ADDR_MARKER) { // maker bit indicates long addr progtrackLocoId = cv ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr if (progtrackLocoId <= HIGHEST_SHORT_ADDR ) { // out of range for long addr DIAG(F("Long addr %d <= %d unsupported\n"), progtrackLocoId, HIGHEST_SHORT_ADDR); progtrackLocoId = -1; } } else { progtrackLocoId=cv; } } void RMFT2::pause() { if (loco) pauseSpeed=DCC::getThrottleSpeedByte(loco); } void RMFT2::resume() { if (loco) DCC::setThrottle(loco,pauseSpeed & 0x7f, pauseSpeed & 0x80); } void RMFT2::loop() { if (compileFeatures & FEATURE_SENSOR) EXRAILSensor::checkAll(); // Round Robin call to a RMFT task each time if (loopTask==NULL) return; loopTask=loopTask->next; if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2(); } void RMFT2::loop2() { if (delayTime!=0 && millis()-delayStart < delayTime) return; // special stand alone blink task if (compileFeatures & FEATURE_BLINK) { if (blinkState==blink_low) { IODevice::write(blinkPin,HIGH); blinkState=blink_high; delayMe(getOperand(1)); return; } if (blinkState==blink_high) { IODevice::write(blinkPin,LOW); blinkState=blink_low; delayMe(getOperand(2)); return; } } // Normal progstep following tasks continue here. byte opcode = GET_OPCODE; int16_t operand = getOperand(0); // skipIf will get set to indicate a failing IF condition bool skipIf=false; // if (diag) DIAG(F("RMFT2 %d %d"),opcode,operand); // Attention: Returning from this switch leaves the program counter unchanged. // This is used for unfinished waits for timers or sensors. // Breaking from this switch will step to the next step in the route. switch ((OPCODE)opcode) { case OPCODE_THROW: Turnout::setClosed(operand, false); break; case OPCODE_CLOSE: Turnout::setClosed(operand, true); break; case OPCODE_TOGGLE_TURNOUT: Turnout::setClosed(operand, Turnout::isThrown(operand)); break; #ifndef IO_NO_HAL case OPCODE_ROTATE: uint8_t activity; activity=getOperand(2); Turntable::setPosition(operand,getOperand(1),activity); break; #endif case OPCODE_REV: if (loco) DCC::setThrottle(loco,operand,invert); break; case OPCODE_FWD: if (loco) DCC::setThrottle(loco,operand,!invert); break; case OPCODE_SPEED: if (loco) DCC::setThrottle(loco,operand,DCC::getThrottleDirection(loco)); break; case OPCODE_FORGET: if (loco!=0) { DCC::forgetLoco(loco); loco=0; } break; case OPCODE_INVERT_DIRECTION: invert= !invert; break; case OPCODE_RESERVE: if (getFlag(operand,SECTION_FLAG)) { if (loco) DCC::setThrottle(loco,0,DCC::getThrottleDirection(loco)); delayMe(500); return; } setFlag(operand,SECTION_FLAG); break; case OPCODE_FREE: setFlag(operand,0,SECTION_FLAG); break; case OPCODE_AT: blinkState=not_blink_task; if (readSensor(operand)) break; delayMe(50); return; case OPCODE_ATGTE: // wait for analog sensor>= value blinkState=not_blink_task; if (IODevice::readAnalogue(operand) >= (int)(getOperand(1))) break; delayMe(50); return; case OPCODE_ATLT: // wait for analog sensor < value blinkState=not_blink_task; if (IODevice::readAnalogue(operand) < (int)(getOperand(1))) break; delayMe(50); return; case OPCODE_ATTIMEOUT1: // ATTIMEOUT(vpin,timeout) part 1 timeoutStart=millis(); blinkState=not_blink_task; break; case OPCODE_ATTIMEOUT2: if (readSensor(operand)) break; // success without timeout if (millis()-timeoutStart > 100*getOperand(1)) { blinkState=at_timeout; break; // and drop through } delayMe(50); return; case OPCODE_IFTIMEOUT: // do next operand if timeout flag set skipIf=blinkState!=at_timeout; break; case OPCODE_AFTER: // waits for sensor to hit and then remain off for x mS. // Note, this must come after an AT operation, which is // automatically inserted by the AFTER macro. if (readSensor(operand)) { // reset timer and keep waiting waitAfter=millis(); delayMe(50); return; } if (millis()-waitAfter < getOperand(1) ) return; break; case OPCODE_AFTEROVERLOAD: // waits for the power to be turned back on - either by power routine or button if (!TrackManager::isPowerOn(operand)) { // reset timer to half a second and keep waiting waitAfter=millis(); delayMe(50); return; } if (millis()-waitAfter < 500 ) return; break; case OPCODE_LATCH: setFlag(operand,LATCH_FLAG); break; case OPCODE_UNLATCH: setFlag(operand,0,LATCH_FLAG); break; case OPCODE_SET: case OPCODE_RESET: { auto count=getOperand(1); for (uint16_t i=0;iblinkPin=operand; newtask->blinkState=blink_low; // will go high on first call } break; case OPCODE_PAUSE: // all tasks save their speed bytes pause(); for (RMFT2 * t=next; t!=this;t=t->next) t->pause(); DCC::setThrottle(0,1,true); // pause all locos on the track pausingTask=this; break; case OPCODE_POM: if (loco) DCC::writeCVByteMain(loco, operand, getOperand(1)); break; case OPCODE_POWEROFF: TrackManager::setPower(POWERMODE::OFF); TrackManager::setJoin(false); break; case OPCODE_SET_POWER: // operand is TRACK_POWER , trackid //byte thistrack=getOperand(1); switch (operand) { case TRACK_POWER_0: TrackManager::setTrackPower(POWERMODE::OFF, getOperand(1)); break; case TRACK_POWER_1: TrackManager::setTrackPower(POWERMODE::ON, getOperand(1)); break; } break; case OPCODE_SET_TRACK: // operand is trackmode<<8 | track id // If DC/DCX use my loco for DC address { TRACK_MODE mode = (TRACK_MODE)(operand>>8); int16_t cab=(mode & TRACK_MODE_DC) ? loco : 0; TrackManager::setTrackMode(operand & 0x0F, mode, cab); } break; case OPCODE_SETFREQ: // Frequency is default 0, or 1, 2,3 DCC::setDCFreq(loco,operand); break; case OPCODE_RESUME: pausingTask=NULL; resume(); for (RMFT2 * t=next; t!=this;t=t->next) t->resume(); break; case OPCODE_IF: // do next operand if sensor set skipIf=!readSensor(operand); break; case OPCODE_ELSE: // skip to matching ENDIF skipIf=true; break; case OPCODE_IFGTE: // do next operand if sensor>= value skipIf=IODevice::readAnalogue(operand)<(int)(getOperand(1)); break; case OPCODE_IFLT: // do next operand if sensor< value skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1)); break; case OPCODE_IFLOCO: // do if the loco is the active one skipIf=loco!=(uint16_t)operand; // bad luck if someone enters negative loco numbers into EXRAIL break; case OPCODE_IFNOT: // do next operand if sensor not set skipIf=readSensor(operand); break; case OPCODE_IFRE: // do next operand if rotary encoder != position skipIf=IODevice::readAnalogue(operand)!=(int)(getOperand(1)); break; case OPCODE_IFRANDOM: // do block on random percentage skipIf=(uint8_t)micros() >= operand * 255/100; break; case OPCODE_IFRESERVE: // do block if we successfully RERSERVE if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG); else skipIf=true; break; case OPCODE_IFRED: // do block if signal as expected skipIf=!isSignal(operand,SIGNAL_RED); break; case OPCODE_IFAMBER: // do block if signal as expected skipIf=!isSignal(operand,SIGNAL_AMBER); break; case OPCODE_IFGREEN: // do block if signal as expected skipIf=!isSignal(operand,SIGNAL_GREEN); break; case OPCODE_IFTHROWN: skipIf=Turnout::isClosed(operand); break; case OPCODE_IFCLOSED: skipIf=Turnout::isThrown(operand); break; #ifndef IO_NO_HAL case OPCODE_IFTTPOSITION: // do block if turntable at this position skipIf=Turntable::getPosition(operand)!=(int)getOperand(1); break; #endif case OPCODE_ENDIF: break; case OPCODE_DELAYMS: delayMe(operand); break; case OPCODE_DELAY: delayMe(operand*100L); break; case OPCODE_DELAYMINS: delayMe(operand*60L*1000L); break; case OPCODE_RANDWAIT: delayMe(operand==0 ? 0 : (micros()%operand) *100L); break; case OPCODE_RED: doSignal(operand,SIGNAL_RED); break; case OPCODE_AMBER: doSignal(operand,SIGNAL_AMBER); break; case OPCODE_GREEN: doSignal(operand,SIGNAL_GREEN); break; case OPCODE_FON: if (loco) DCC::setFn(loco,operand,true); break; case OPCODE_FOFF: if (loco) DCC::setFn(loco,operand,false); break; case OPCODE_FTOGGLE: if (loco) DCC::changeFn(loco,operand); break; case OPCODE_DRIVE: { // Non functional but reserved break; } case OPCODE_XFON: DCC::setFn(operand,getOperand(1),true); break; case OPCODE_XFOFF: DCC::setFn(operand,getOperand(1),false); break; case OPCODE_XFTOGGLE: DCC::changeFn(operand,getOperand(1)); break; case OPCODE_DCCACTIVATE: { // operand is address<<3 | subaddr<<1 | active int16_t addr=operand>>3; int16_t subaddr=(operand>>1) & 0x03; bool active=operand & 0x01; DCC::setAccessory(addr,subaddr,active); break; } case OPCODE_ASPECT: { // operand is address<<5 | value int16_t address=operand>>5; byte aspect=operand & 0x1f; if (!signalAspectEvent(address,aspect)) DCC::setExtendedAccessory(address,aspect); break; } case OPCODE_FOLLOW: progCounter=routeLookup->find(operand); if (progCounter<0) kill(F("FOLLOW unknown"), operand); return; case OPCODE_CALL: if (stackDepth==MAX_STACK_DEPTH) { kill(F("CALL stack"), stackDepth); return; } callStack[stackDepth++]=progCounter+3; progCounter=routeLookup->find(operand); if (progCounter<0) kill(F("CALL unknown"),operand); return; case OPCODE_RETURN: if (stackDepth==0) { kill(F("RETURN stack")); return; } progCounter=callStack[--stackDepth]; return; case OPCODE_ENDTASK: case OPCODE_ENDEXRAIL: kill(); return; case OPCODE_KILLALL: while(loopTask) loopTask->kill(F("KILLALL")); return; #ifndef DISABLE_PROG case OPCODE_JOIN: TrackManager::setPower(POWERMODE::ON); TrackManager::setJoin(true); break; case OPCODE_UNJOIN: TrackManager::setJoin(false); break; case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet DCC::getLocoId(readLocoCallback); break; case OPCODE_READ_LOCO2: if (progtrackLocoId==LOCO_ID_WAITING) { delayMe(100); return; // still waiting for callback } // At failed read will result in loco == -1 // which is intended so it can be checked // from within EXRAIL loco=progtrackLocoId; invert=false; break; #endif case OPCODE_POWERON: TrackManager::setMainPower(POWERMODE::ON); TrackManager::setJoin(false); break; case OPCODE_START: { int newPc=routeLookup->find(operand); if (newPc<0) break; new RMFT2(newPc); } break; case OPCODE_SENDLOCO: // cab, route { int newPc=routeLookup->find(getOperand(1)); if (newPc<0) break; new RMFT2(newPc,operand); // create new task } break; case OPCODE_SETLOCO: { loco=operand; invert=false; } break; case OPCODE_LCC: // short form LCC if ((compileFeatures & FEATURE_LCC) && LCCSerial) StringFormatter::send(LCCSerial,F(""),(uint16_t)operand); break; case OPCODE_ACON: // MERG adapter case OPCODE_ACOF: if ((compileFeatures & FEATURE_LCC) && LCCSerial) StringFormatter::send(LCCSerial,F(""), opcode==OPCODE_ACON?'0':'1', (uint16_t)operand,getOperand(progCounter,1)); break; case OPCODE_LCCX: // long form LCC if ((compileFeatures & FEATURE_LCC) && LCCSerial) StringFormatter::send(LCCSerial,F("\n"), getOperand(progCounter,1), getOperand(progCounter,2), getOperand(progCounter,3), getOperand(progCounter,0) ); break; case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration) IODevice::writeAnalogue(operand,getOperand(1),getOperand(2),getOperand(3)); break; case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin) if (IODevice::isBusy(operand)) { delayMe(100); return; } break; #ifndef IO_NO_HAL case OPCODE_NEOPIXEL: // OPCODE_NEOPIXEL,V([-]vpin),OPCODE_PAD,V(colour_RG),OPCODE_PAD,V(colour_B),OPCODE_PAD,V(count) { VPIN vpin=operand>0?operand:-operand; auto count=getOperand(3); killBlinkOnVpin(vpin,count); IODevice::writeAnalogueRange(vpin,getOperand(1),operand>0,getOperand(2),count); } break; case OPCODE_WAITFORTT: // OPCODE_WAITFOR,V(turntable_id) if (Turntable::ttMoving(operand)) { delayMe(100); return; } break; #endif case OPCODE_PRINT: printMessage(operand); break; case OPCODE_ROUTE_HIDDEN: manageRouteState(operand,2); break; case OPCODE_ROUTE_INACTIVE: manageRouteState(operand,0); break; case OPCODE_ROUTE_ACTIVE: manageRouteState(operand,1); break; case OPCODE_ROUTE_DISABLED: manageRouteState(operand,4); break; case OPCODE_STASH: if (compileFeatures & FEATURE_STASH) stashArray[operand] = invert? -loco : loco; break; case OPCODE_CLEAR_STASH: if (compileFeatures & FEATURE_STASH) stashArray[operand] = 0; break; case OPCODE_CLEAR_ALL_STASH: if (compileFeatures & FEATURE_STASH) for (int i=0;i<=maxStashId;i++) stashArray[operand]=0; break; case OPCODE_PICKUP_STASH: if (compileFeatures & FEATURE_STASH) { int16_t x=stashArray[operand]; if (x>=0) { loco=x; invert=false; break; } loco=-x; invert=true; } break; case OPCODE_ROUTE: case OPCODE_AUTOMATION: case OPCODE_SEQUENCE: //if (diag) DIAG(F("EXRAIL begin(%d)"),operand); break; case OPCODE_AUTOSTART: // Handled only during begin process case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte. case OPCODE_TURNOUT: // Turnout definition ignored at runtime case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime case OPCODE_ONCLOSE: // Turnout event catchers ignored here case OPCODE_ONLCC: // LCC event catchers ignored here case OPCODE_ONACON: // MERG event catchers ignored here case OPCODE_ONACOF: // MERG event catchers ignored here case OPCODE_ONTHROW: case OPCODE_ONACTIVATE: // Activate event catchers ignored here case OPCODE_ONDEACTIVATE: case OPCODE_ONRED: case OPCODE_ONAMBER: case OPCODE_ONGREEN: case OPCODE_ONCHANGE: case OPCODE_ONTIME: case OPCODE_ONBUTTON: case OPCODE_ONSENSOR: #ifndef IO_NO_HAL case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime case OPCODE_TTADDPOSITION: // Turntable position definition ignored at runtime case OPCODE_ONROTATE: #endif case OPCODE_ONOVERLOAD: case OPCODE_ONBLOCKENTER: case OPCODE_ONBLOCKEXIT: break; default: kill(F("INVOP"),operand); } // Falling out of the switch means move on to the next opcode // but if we are skipping a false IF or else if (skipIf) if (!skipIfBlock()) return; SKIPOP; } void RMFT2::delayMe(long delay) { delayTime=delay; delayStart=millis(); } bool RMFT2::setFlag(VPIN id,byte onMask, byte offMask) { if (FLAGOVERFLOW(id)) return false; // Outside range limit byte f=flags[id]; f &= ~offMask; f |= onMask; flags[id]=f; return true; } bool RMFT2::getFlag(VPIN id,byte mask) { if (FLAGOVERFLOW(id)) return 0; // Outside range limit return flags[id]&mask; } void RMFT2::kill(const FSH * reason, int operand) { if (reason) DIAG(F("EXRAIL ERROR pc=%d, cab=%d, %S %d"), progCounter,loco, reason, operand); else if (diag) DIAG(F("ENDTASK at pc=%d"), progCounter); delete this; } SIGNAL_DEFINITION RMFT2::getSignalSlot(int16_t slot) { SIGNAL_DEFINITION signal; COPYHIGHFLASH(&signal,SignalDefinitions,slot*sizeof(SIGNAL_DEFINITION),sizeof(SIGNAL_DEFINITION)); return signal; } /* static */ void RMFT2::doSignal(int16_t id,char rag) { if (!(compileFeatures & FEATURE_SIGNAL)) return; // dont compile code below //if (diag) DIAG(F(" doSignal %d %x"),id,rag); // Schedule any event handler for this signal change. // This will work even without a signal definition. 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); auto sigslot=signalLookup->find(id); if (sigslot<0) return; // keep track of signal state setFlag(sigslot,rag,SIGNAL_MASK); // Correct signal definition found, get the rag values auto signal=getSignalSlot(sigslot); switch (signal.type) { case sigtypeSERVO: { auto servopos = rag==SIGNAL_RED? signal.redpin: (rag==SIGNAL_GREEN? signal.greenpin : signal.amberpin); //if (diag) DIAG(F("sigA %d %d"),id,servopos); if (servopos!=0) IODevice::writeAnalogue(id,servopos,PCA9685::Bounce); return; } case sigtypeDCC: { // redpin,amberpin are the DCC addr,subaddr DCC::setAccessory(signal.redpin,signal.amberpin, rag!=SIGNAL_RED); return; } case sigtypeDCCX: { // redpin,amberpin,greenpin are the 3 aspects auto value=signal.redpin; if (rag==SIGNAL_AMBER) value=signal.amberpin; if (rag==SIGNAL_GREEN) value=signal.greenpin; DCC::setExtendedAccessory(id, value); return; } case sigtypeNEOPIXEL: { // redpin,amberpin,greenpin are the 3 RG values but with no blue permitted. . (code limitation hack) auto colour_RG=signal.redpin; if (rag==SIGNAL_AMBER) colour_RG=signal.amberpin; if (rag==SIGNAL_GREEN) colour_RG=signal.greenpin; // blue channel is in followng signal slot (a continuation) auto signal2=getSignalSlot(sigslot+1); auto colour_B=signal2.redpin; if (rag==SIGNAL_AMBER) colour_B=signal2.amberpin; if (rag==SIGNAL_GREEN) colour_B=signal2.greenpin; IODevice::writeAnalogue(id, colour_RG,true,colour_B); return; } case sigtypeSIGNAL: case sigtypeSIGNALH: { // LED or similar 3 pin signal, (all pins zero would be a virtual signal) // If amberpin is zero, synthesise amber from red+green const byte SIMAMBER=0x00; if (rag==SIGNAL_AMBER && (signal.amberpin==0)) rag=SIMAMBER; // special case this func only // Manage invert (HIGH on) pins bool aHigh=signal.type==sigtypeSIGNALH; // set the three pins if (signal.redpin) { bool redval=(rag==SIGNAL_RED || rag==SIMAMBER); if (!aHigh) redval=!redval; killBlinkOnVpin(signal.redpin); IODevice::write(signal.redpin,redval); } if (signal.amberpin) { bool amberval=(rag==SIGNAL_AMBER); if (!aHigh) amberval=!amberval; killBlinkOnVpin(signal.amberpin); IODevice::write(signal.amberpin,amberval); } if (signal.greenpin) { bool greenval=(rag==SIGNAL_GREEN || rag==SIMAMBER); if (!aHigh) greenval=!greenval; killBlinkOnVpin(signal.greenpin); IODevice::write(signal.greenpin,greenval); } } case sigtypeVIRTUAL: break; case sigtypeContinuation: break; case sigtypeNoMoreSignals: break; } } /* static */ bool RMFT2::isSignal(int16_t id,char rag) { if (!(compileFeatures & FEATURE_SIGNAL)) return false; int16_t sigslot=signalLookup->find(id); if (sigslot<0) return false; return (flags[sigslot] & SIGNAL_MASK) == rag; } // signalAspectEvent returns true if the aspect is destined // for a defined DCCX_SIGNAL which will handle all the RAG flags // and ON* handlers. // Otherwise false so the parser should send the command directly bool RMFT2::signalAspectEvent(int16_t address, byte aspect ) { if (!(compileFeatures & FEATURE_SIGNAL)) return false; auto sigslot=signalLookup->find(address); if (sigslot<0) return false; // this is not a defined signal auto signal=getSignalSlot(sigslot); if (signal.type!=sigtypeDCCX) return false; // not a DCCX signal // Turn an aspect change into a RED/AMBER/GREEN setting if (aspect==signal.redpin) { doSignal(address,SIGNAL_RED); return true; } if (aspect==signal.amberpin) { doSignal(address,SIGNAL_AMBER); return true; } if (aspect==signal.greenpin) { doSignal(address,SIGNAL_GREEN); return true; } return false; // aspect is not a defined one } void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) { // Hunt for an ONTHROW/ONCLOSE for this turnout 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) onActivateLookup->handleEvent(F("ACTIVATE"),addr); else onDeactivateLookup->handleEvent(F("DEACTIVATE"),addr); } void RMFT2::blockEvent(int16_t block, int16_t loco, bool entering) { if (compileFeatures & FEATURE_BLOCK) { // Hunt for an ONBLOCKENTER/ONBLOCKEXIT for this accessory if (entering) onBlockEnterLookup->handleEvent(F("BLOCKENTER"),block,loco); else onBlockExitLookup->handleEvent(F("BLOCKEXIT"),block,loco); } } void RMFT2::changeEvent(int16_t vpin, bool change) { // Hunt for an ONCHANGE for this sensor if (change) onChangeLookup->handleEvent(F("CHANGE"),vpin); } #ifndef IO_NO_HAL void RMFT2::rotateEvent(int16_t turntableId, bool change) { // Hunt or an ONROTATE for this turntable if (change) onRotateLookup->handleEvent(F("ROTATE"),turntableId); } #endif void RMFT2::clockEvent(int16_t clocktime, bool change) { // Hunt for an ONTIME for this time if (Diag::CMD) DIAG(F("clockEvent at : %d"), clocktime); if (change) { onClockLookup->handleEvent(F("CLOCK"),clocktime); onClockLookup->handleEvent(F("CLOCK"),25*60+clocktime%60); } } void RMFT2::powerEvent(int16_t track, bool overload) { // Hunt for an ONOVERLOAD for this item if (Diag::CMD) DIAG(F("powerEvent : %c"), track); if (overload) { onOverloadLookup->handleEvent(F("POWER"),track); } } // This function is used when setting pins so that a SET or RESET // will cause any blink task on that pin to terminate. // It will be compiled out of existence if no BLINK feature is used. void RMFT2::killBlinkOnVpin(VPIN pin, uint16_t count) { if (!(compileFeatures & FEATURE_BLINK)) return; RMFT2 * stoptask=loopTask; // stop when we get back to here RMFT2 * task=loopTask; VPIN lastPin=pin+count-1; while(task) { auto nextTask=task->next; if ( (task->blinkState==blink_high || task->blinkState==blink_low) && task->blinkPin>=pin && task->blinkPin<=lastPin ) { if (diag) DIAG(F("kill blink %d"),task->blinkPin,lastPin); task->kill(); } task=nextTask; if (task==stoptask) return; } } void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco) { // Check we dont already have a task running this handler RMFT2 * task=loopTask; while(task) { if (task->onEventStartPosition==pc && task->loco==loco) { DIAG(F("Recursive ON%S(%d)"),reason, id); return; } task=task->next; if (task==loopTask) break; } task=new RMFT2(pc,loco); // new task starts at this instruction task->onEventStartPosition=pc; // flag for recursion detector } void RMFT2::printMessage2(const FSH * msg) { DIAG(F("EXRAIL(%d) %S"),loco,msg); } static StringBuffer * buffer=NULL; /* thrungeString is used to stream a HIGHFLASH string to a suitable Serial and handle the oddities like LCD, BROADCAST and PARSE */ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) { //DIAG(F("thrunge addr=%l mode=%d id=%d"), strfar,mode,id); Print * stream=NULL; // Find out where the string is going switch (mode) { case thrunge_print: StringFormatter::send(&USB_SERIAL,F("<* EXRAIL(%d) "),loco); stream=&USB_SERIAL; break; case thrunge_serial: stream=&USB_SERIAL; break; case thrunge_serial1: #ifdef SERIAL1_COMMANDS stream=&Serial1; #endif break; case thrunge_serial2: #ifdef SERIAL2_COMMANDS stream=&Serial2; #endif break; case thrunge_serial3: #ifdef SERIAL3_COMMANDS stream=&Serial3; #endif break; case thrunge_serial4: #ifdef SERIAL4_COMMANDS stream=&Serial4; #endif break; case thrunge_serial5: #ifdef SERIAL5_COMMANDS stream=&Serial5; #endif break; case thrunge_serial6: #ifdef SERIAL6_COMMANDS stream=&Serial6; #endif break; case thrunge_lcn: #if defined(LCN_SERIAL) stream=&LCN_SERIAL; #endif break; case thrunge_parse: case thrunge_broadcast: case thrunge_message: case thrunge_lcd: default: // thrunge_lcd+1, ... if (!buffer) buffer=new StringBuffer(); buffer->flush(); stream=buffer; break; } if (!stream) return; #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) // if mega stream it out for (;;strfar++) { char c=pgm_read_byte_far(strfar); if (c=='\0') break; stream->write(c); } #else // UNO/NANO CPUs dont have high memory // 32 bit cpus dont care anyway stream->print((FSH *)strfar); #endif // and decide what to do next switch (mode) { case thrunge_print: StringFormatter::send(&USB_SERIAL,F(" *>\n")); break; // TODO more serials for SAMx case thrunge_serial4: stream=&Serial4; break; case thrunge_parse: DCCEXParser::parseOne(&USB_SERIAL,(byte*)buffer->getString(),NULL); break; case thrunge_broadcast: CommandDistributor::broadcastRaw(CommandDistributor::COMMAND_TYPE,buffer->getString()); break; case thrunge_withrottle: CommandDistributor::broadcastRaw(CommandDistributor::WITHROTTLE_TYPE,buffer->getString()); break; case thrunge_message: CommandDistributor::broadcastMessage(buffer->getString()); break; case thrunge_lcd: LCD(id,F("%s"),buffer->getString()); break; default: // thrunge_lcd+1, ... if (mode > thrunge_lcd) SCREEN(mode-thrunge_lcd, id, F("%s"),buffer->getString()); // print to other display 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); } }