/*
 *  © 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 <https://www.gnu.org/licenses/>.
 */

/* 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 <Arduino.h>
#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;      // <D EXRAIL ON>  
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;
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;i<m_size;i++) {
    if (m_lookupArray[i]==value) return m_resultArray[i];
  }
  return m_chain ?  m_chain->find(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;i<m_size;i++) 
    if (m_lookupArray[i]==id)
       RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i]);
}


void LookList::stream(Print * _stream) {
  for (int16_t i=0;i<m_size;i++) {
    _stream->print(" ");
    _stream->print(m_lookupArray[i]);
  }
}

int16_t LookList::findPosition(int16_t value) {
  for (int16_t i=0;i<m_size;i++) {
    if (m_lookupArray[i]==value) return i;
  }
  return -1;
}
int16_t LookList::size() {
   return m_size;
}

LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
  int progCounter;
  int16_t count=0;
  // find size for list
  for (progCounter=0;; SKIPOP) {
    byte opcode=GET_OPCODE;
    if (opcode==OPCODE_ENDEXRAIL) break;
    if (opcode==op1 || opcode==op2 || opcode==op3) count++;
  }
  // create list 
  LookList* list=new LookList(count);
  if (count==0) return list;

  for (progCounter=0;; SKIPOP) {
    byte opcode=GET_OPCODE;
    if (opcode==OPCODE_ENDEXRAIL) break;
    if (opcode==op1 || opcode==op2 || opcode==op3)  list->add(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;f<MAX_FLAGS;f++) flags[f]=0;
  
  // create lookups
  routeLookup=LookListLoader(OPCODE_ROUTE, OPCODE_AUTOMATION);
  routeLookup->chain(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);
  // 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) {
  progCounter=progCtr;

  // get an unused  task id from the flags table
  taskId=255; // in case of overflow
  for (int f=0;f<MAX_FLAGS;f++) {
    if (!getFlag(f,TASK_FLAG)) {
      taskId=f;
      setFlag(f, TASK_FLAG);
      break;
    }
  }
  delayTime=0;
  loco=0;
  speedo=0;
  forward=true;
  invert=false;
  blinkState=not_blink_task;
  stackDepth=0;
  onEventStartPosition=-1; // Not handling an ONxxx 

  // chain into ring of RMFTs
  if (loopTask==NULL) {
    loopTask=this;
    next=this;
  } else {
    next=loopTask->next;
    loopTask->next=this;
  }
}


RMFT2::~RMFT2() {
  driveLoco(1); // ESTOP my loco if any
  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;
      RMFT2* task=new RMFT2(pc);
      task->loco=cab;
}

void RMFT2::driveLoco(byte speed) {
  if (loco<=0) return;  // Prevent broadcast!
  //if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
 /* TODO.....
 power on appropriate track if DC or main if dcc
  if (TrackManager::getMainPowerMode()==POWERMODE::OFF) {
    TrackManager::setMainPower(POWERMODE::ON);
  }
  **********/

  DCC::setThrottle(loco,speed, forward^invert);
  speedo=speed;
}

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"), progtrackLocoId, HIGHEST_SHORT_ADDR);
      progtrackLocoId = -1;
    }
  } else {
    progtrackLocoId=cv;
  }
}

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:
    forward = false;
    driveLoco(operand);
    break;
    
  case OPCODE_FWD:
      forward = true;
      driveLoco(operand);
      break;
      
  case OPCODE_SPEED:
    forward=DCC::getThrottleDirection(loco)^invert;
    driveLoco(operand);
    break;
    
  case OPCODE_FORGET:
    if (loco!=0) {
      DCC::forgetLoco(loco);
      loco=0; 
    } 
    break;

  case OPCODE_INVERT_DIRECTION:
    invert= !invert;
    driveLoco(speedo);
    break;
    
  case OPCODE_RESERVE:
    if (getFlag(operand,SECTION_FLAG)) {
      driveLoco(0);
      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;i<count;i++) {
        killBlinkOnVpin(operand+i);
        IODevice::write(operand+i,opcode==OPCODE_SET);
      }
    }
    break;
  
  case OPCODE_BLINK: 
     // Start a new task to blink this vpin
     killBlinkOnVpin(operand);
     {
      auto newtask=new RMFT2(progCounter);
      newtask->blinkPin=operand;
      newtask->blinkState=blink_low; // will go high on first call
     }
     break; 

  case OPCODE_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;
    driveLoco(speedo);
    for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
    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:
    {
      byte analogSpeed=IODevice::readAnalogue(operand) *127 / 1024;
      if (speedo!=analogSpeed) driveLoco(analogSpeed);
      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;
    speedo=0;
    forward=true;
    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;
      RMFT2* newtask=new RMFT2(newPc); // create new task
      newtask->loco=operand;
    }
    break;
    
  case OPCODE_SETLOCO:
    {
      loco=operand;
      speedo=0;
      forward=true;
      invert=false;
    }
    break;

  case OPCODE_LCC:  // short form LCC
      if ((compileFeatures & FEATURE_LCC) && LCCSerial) 
          StringFormatter::send(LCCSerial,F("<L x%h>"),(uint16_t)operand);
       break; 
  
  case OPCODE_ACON:  // MERG adapter 
  case OPCODE_ACOF: 
      if ((compileFeatures & FEATURE_LCC) && LCCSerial) 
          StringFormatter::send(LCCSerial,F("<L x%c%h%h>"),
          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("<L x%h%h%h%h>\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:
  
    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::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) {  
  // Check we dont already have a task running this handler
  RMFT2 * task=loopTask;
  while(task) {
    if (task->onEventStartPosition==pc) {
      DIAG(F("Recursive ON%S(%d)"),reason, id);
      return;
    }
    task=task->next;
    if (task==loopTask) break;
  }
  
  task=new RMFT2(pc);  // 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);
  }
}