Full Code of DCC-EX/CommandStation-EX for AI

master 288ae34fdecc cached
226 files
1.7 MB
492.8k tokens
571 symbols
1 requests
Download .txt
Showing preview only (1,797K chars total). Download the full file or copy to clipboard to get everything.
Repository: DCC-EX/CommandStation-EX
Branch: master
Commit: 288ae34fdecc
Files: 226
Total size: 1.7 MB

Directory structure:
gitextract_3uy1hr_0/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── docs.yml
│       ├── label-sponsors.yml
│       ├── main.yml
│       └── sha.yml
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── CamParser.cpp
├── CamParser.h
├── CommandDistributor.cpp
├── CommandDistributor.h
├── CommandStation-EX.ino
├── DCC.cpp
├── DCC.h
├── DCCACK.cpp
├── DCCACK.h
├── DCCConsist.cpp
├── DCCConsist.h
├── DCCDecoder.cpp
├── DCCDecoder.h
├── DCCEX.h
├── DCCEXParser.cpp
├── DCCEXParser.h
├── DCCPacket.h
├── DCCQueue.cpp
├── DCCQueue.h
├── DCCRMT.cpp
├── DCCRMT.h
├── DCCTimer.h
├── DCCTimerAVR.cpp
├── DCCTimerESP.cpp
├── DCCTimerMEGAAVR.cpp
├── DCCTimerSAMD.cpp
├── DCCTimerSTM32.cpp
├── DCCTimerTEENSY.cpp
├── DCCWaveform.cpp
├── DCCWaveform.h
├── DCCWaveformRMT.cpp
├── DIAG.h
├── Display.cpp
├── Display.h
├── DisplayInterface.cpp
├── DisplayInterface.h
├── Display_Implementation.h
├── EEStore.cpp
├── EEStore.h
├── EXRAIL.h
├── EXRAIL2.cpp
├── EXRAIL2.h
├── EXRAIL2MacroBase.h
├── EXRAIL2MacroReset.h
├── EXRAIL2Parser.cpp
├── EXRAILAsserts.h
├── EXRAILMacros.h
├── EXRAILSensor.cpp
├── EXRAILSensor.h
├── EXRAILTest.h
├── EXmDNS.cpp
├── EXmDNS.h
├── EthernetInterface.cpp
├── EthernetInterface.h
├── FSH.h
├── GITHUB_SHA.h
├── I2CManager.cpp
├── I2CManager.h
├── I2CManager_AVR.h
├── I2CManager_Mega4809.h
├── I2CManager_NonBlocking.h
├── I2CManager_SAMD.h
├── I2CManager_STM32.h
├── I2CManager_Wire.h
├── IODevice.cpp
├── IODevice.h
├── IODeviceList.h
├── IO_AnalogueInputs.h
├── IO_Bitmap.h
├── IO_DCCAccessory.cpp
├── IO_DFPlayer.h
├── IO_DFPlayerBase.h
├── IO_DFPlayerI2C.h
├── IO_DFPlayerSerial.h
├── IO_DS1307.cpp
├── IO_DS1307.h
├── IO_EXFastclock.h
├── IO_EXIOExpander.h
├── IO_EXSensorCAM.h
├── IO_EXTurntable.cpp
├── IO_EncoderThrottle.cpp
├── IO_EncoderThrottle.h
├── IO_ExampleSerial.h
├── IO_GPIOBase.h
├── IO_HALDisplay.h
├── IO_HCSR04.h
├── IO_I2CDFPlayer-OBSOLETE.h
├── IO_I2CRailcom.cpp
├── IO_I2CRailcom.h
├── IO_MCP23008.h
├── IO_MCP23017.h
├── IO_NeoPixel.h
├── IO_PCA9554.h
├── IO_PCA9555.h
├── IO_PCA9685.cpp
├── IO_PCA9685pwm.h
├── IO_PCF8574.h
├── IO_PCF8575.h
├── IO_RotaryEncoder.h
├── IO_Servo.cpp
├── IO_Servo.h
├── IO_TCA8418.h
├── IO_TM1638.cpp
├── IO_TM1638.h
├── IO_TouchKeypad.h
├── IO_VL53L0X.h
├── IO_XL9535.h
├── IO_duinoNodes.h
├── IO_trainbrains.h
├── KeywordHasher.h
├── LCN.cpp
├── LCN.h
├── LICENSE
├── LiquidCrystal_I2C.cpp
├── LiquidCrystal_I2C.h
├── LocoSlot.cpp
├── LocoSlot.h
├── MotorDriver.cpp
├── MotorDriver.h
├── MotorDrivers.h
├── Outputs.cpp
├── Outputs.h
├── README.md
├── Railcom.cpp
├── Railcom.h
├── Release - Architecture Doc/
│   ├── CommandStation-EX-Arch-v1-0.vsd
│   ├── Prod-Release-Notes.md
│   └── Rough Release-Notes.md
├── Release_Notes/
│   ├── CommandRef.md
│   ├── DCC-EX v5.4 Release Notes.xlsx
│   ├── Exrail mods.txt
│   ├── IO_Bitmap.md
│   ├── NeoPixel.md
│   ├── Railcom.md
│   ├── Stash.md
│   ├── TCA8418.md
│   ├── TM1638.md
│   ├── ThrottleAssists.md
│   ├── TrackManager.md
│   ├── consists.md
│   ├── duinoNodes.md
│   ├── momentum.md
│   ├── release_notes_v3.0.0.md
│   ├── release_notes_v3.1.0.md
│   ├── release_notes_v4.0.0.md
│   └── websocketTester.html
├── RingStream.cpp
├── RingStream.h
├── SSD1306Ascii.cpp
├── SSD1306Ascii.h
├── STM32lwipopts.h.copyme
├── SensorGroup.cpp
├── SensorGroup.h
├── Sensors.cpp
├── Sensors.h
├── SerialManager.cpp
├── SerialManager.h
├── SerialUsbLog.cpp
├── SerialUsbLog.h
├── SerialUsbLog.html.h
├── SerialUsbLog.script1.js.h
├── SerialUsbLog.script2.js.h
├── SerialUsbLog.style.css.h
├── Sniffer.cpp
├── Sniffer.h
├── Stash.cpp
├── Stash.h
├── StringBuffer.cpp
├── StringBuffer.h
├── StringFormatter.cpp
├── StringFormatter.h
├── TemplateForEnums.h
├── TrackManager.cpp
├── TrackManager.h
├── Turnouts.cpp
├── Turnouts.h
├── Turntables.cpp
├── Turntables.h
├── Websockets.cpp
├── Websockets.h
├── WiThrottle.cpp
├── WiThrottle.h
├── WifiESP32.cpp
├── WifiESP32.h
├── WifiInboundHandler.cpp
├── WifiInboundHandler.h
├── WifiInterface.cpp
├── WifiInterface.h
├── Ztest.cpp
├── Ztest.h
├── config.example.h
├── defines.h
├── docs/
│   ├── DoxyfileEXRAIL
│   ├── Makefile
│   ├── _static/
│   │   └── css/
│   │       ├── dccex_theme.css
│   │       └── sphinx_design_overrides.css
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   ├── requirements.txt
│   └── robots.txt
├── install_via_powershell.cmd
├── installer.json
├── installer.ps1
├── installer.sh
├── libsha1.cpp
├── libsha1.h
├── myAutomation.example.h
├── myEX-Turntable.example.h
├── myHal.cpp.txt
├── myHal.cpp_example.txt
├── myTrackStatus.example.h
├── objdump.bat
├── objdump.sh
├── platformio.ini
├── release_notes.md
└── version.h

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
*.svg -text


================================================
FILE: .github/FUNDING.yml
================================================
github: DCC-EX
patreon: dccex


================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs

on:
  push:
    branches:
      - devel
  pull_request:
    branches: [ master ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4.1.1

      - name: Install Requirements
        run: |
          cd docs
          python -m pip install --upgrade pip
          pip3 install -r requirements.txt
          sudo apt-get install doxygen

      - name: Build Prod docs
        run: |
          cd docs
          make html
          touch _build/html/.nojekyll

      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@ba1486788b0490a235422264426c45848eac35c6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          branch: gh-pages  # The branch the action should deploy to.
          folder: docs/_build/html  # The folder the action should deploy.


================================================
FILE: .github/workflows/label-sponsors.yml
================================================
name: Label sponsors
on:
  pull_request:
    types: [opened]
  issues:
    types: [opened]
jobs:
  build:
    name: is-sponsor-label
    runs-on: ubuntu-latest
    steps:
      - uses: JasonEtco/is-sponsor-label-action@v1.2.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/main.yml
================================================
name: CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Install Python Wheel
      run: pip install wheel
    - name: Install PlatformIO Core
      run: pip install -U platformio
    - name: Copy generic config over
      run: cp config.example.h config.h
    - name: Compile Command Station (AVR)
      run: python -m platformio run


================================================
FILE: .github/workflows/sha.yml
================================================
name: SHA 

# Run this workflow ever time code is pushed to a branch
# other than `main` in your repository
on: push

jobs:
  # Set the job key. The key is displayed as the job name
  # when a job name is not provided
  sha:
    # Name the Job
    name: Commit SHA 
    # Set the type of machine to run on
    runs-on: ubuntu-latest

    if: github.ref == 'refs/heads/master'
    steps:
      # Checks out a copy of your repository on the ubuntu-latest machine
      - name: Checkout code
        uses: actions/checkout@v2
      
      - name: Create SHA File
        run: |
          sha=$(git rev-parse --short "$GITHUB_SHA")
          echo "#define GITHUB_SHA \"$sha\"" > GITHUB_SHA.h

      - uses: EndBug/add-and-commit@v8 # You can change this to use a specific version
        with:
          add: 'GITHUB_SHA.h'
          message: 'Committing a SHA'
          commit: --amend

        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged
          

================================================
FILE: .gitignore
================================================
Release/*
.ino.cpp
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json
.pio/
.vscode/
config.h
mySetup.cpp
myHal.cpp
myFilter.cpp
my*.h
!my*.example.h
compile_commands.json
newcode.txt.old
UserAddin.txt
_build
venv
.DS_Store


================================================
FILE: .travis.yml
================================================
# Continuous Integration (CI) is the practice, in software
# engineering, of merging all developer working copies with a shared mainline
# several times a day < https://docs.platformio.org/page/ci/index.html >
#
# Documentation:
#
# * Travis CI Embedded Builds with PlatformIO
#   < https://docs.travis-ci.com/user/integration/platformio/ >
#
# * PlatformIO integration with Travis CI
#   < https://docs.platformio.org/page/ci/travis.html >
#
# * User Guide for `platformio ci` command
#   < https://docs.platformio.org/page/userguide/cmd_ci.html >
#
#
# Please choose one of the following templates (proposed below) and uncomment
# it (remove "# " before each line) or use own configuration according to the
# Travis CI documentation (see above).
#


#
# Template #1: General project. Test it using existing `platformio.ini`.
#

# language: python
# python:
#     - "2.7"
#
# sudo: false
# cache:
#     directories:
#         - "~/.platformio"
#
# install:
#     - pip install -U platformio
#     - platformio update
#
# script:
#     - platformio run


#
# Template #2: The project is intended to be used as a library with examples.
#

# language: python
# python:
#     - "2.7"
#
# sudo: false
# cache:
#     directories:
#         - "~/.platformio"
#
# env:
#     - PLATFORMIO_CI_SRC=path/to/test/file.c
#     - PLATFORMIO_CI_SRC=examples/file.ino
#     - PLATFORMIO_CI_SRC=path/to/test/directory
#
# install:
#     - pip install -U platformio
#     - platformio update
#
# script:
#     - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Thanks for considering contributing to our project. Here is a guide for how to get started and and a list of our conventions. We will also walk you through the Github command line and Desktop commands necessary to download the code, make changes, and get it included in a next version of the sofware.

Before contributing to this repository, please first discuss the change you wish to make via issue, or any other method with the owners of this repository before making a change. 

Find us on our website at https://dcc-ex.com, on our Discord https://discord.gg/y2sB4Fp or on Trainboard: https://www.trainboard.com/highball/index.php?threads/dcc-update-project-2020.130071/

# Development Environment

We recommend using PlatformIO IDE for VSCode. If you haven't yet used it, it is an easy to learn and easy to use IDE that really shines for embedded development and the Arduino based hardware we use. For more information go to https://platformio.org/

* Download and install the latest version of the Arduino IDE
* Download and install the latest version of Visual Studio Code from Microsoft
* Run VSCode and click on the "extensions" icon on the left. Install "PlatformIO IDE for VSCode" and the "Arduino Framework" support

If you don't see C/C++ Installed in the list, install that too. We also recomment installing the Gitlens extension to make working with Git and GitHub even easier.

You may ask if you can use the Arduino IDE, Visual Studio, or even a text editor and the answer is "of course" if you know what you are doing. Since you are just changing text files, you can use whatever you like as long as your commits and pull requests can be merged in GitHub. However, it will be much easier to follow our coding standards if you have an IDE that can automatically format things for you.

# Coding Style Guidelines

We have adopted the Google style guidlines. In particular please make sure to adhere to these standards:

1. All header files should have #define guards to prevent multiple inclusion.
2. Use Unix style line endings
3. We indent using two spaces (soft tabs)
4. Braces

For more information just check our code or read https://google.github.io/styleguide/cppguide.html#C++_Version

## Using the Repository

1. Clone the repository on your local machine
2. Create a working branch using the format "username-featurename" ex: "git branch -b frightrisk-turnouts"
3. Commit offen, ex: "git add ." and then "git commit -m "description of your changes"
4. Push your changes to our repository "git push"
5. When you are ready, issue a pull request for your changes to be merged into the main branch

## Pull Request Process

1. Ensure any install or build dependencies are removed before the end of the layer when doing a build.

## Code of Conduct

Be Nice

### Enforcement

Contributors who do not follow the be nice rule in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## How Can I Contribute?

The DCC-EX Team has several projects and sub teams where you can help donate your epertise. See the sections below for the project or projects you are interested in.

### Development
### Documentation
### WebThrottle-EX
### Web Support
### Organization/Coordination

Links to external documentation goes here XXX


================================================
FILE: CamParser.cpp
================================================
/*
 *  © 2023-2025, Barry Daniel  
 *  © 2025       Chris Harlow
 *  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/>.
 */

//sensorCAM parser.cpp devel. version 3.07  August 2025
#include "DCCEXParser.h"
#include "CamParser.h"
#include "FSH.h"

// The CAMVPINS array will be filled by IO_EXSensorCam HAL drivers calling
// the CamParser::addVpin() function.
// The CAMBaseVpin is the one to be used when commands are given without a vpin. 
VPIN CamParser::CAMBaseVpin = 0; // no vpins yet known 
VPIN CamParser::CAMVPINS[] = {0,0,0,0};  // determines max # CAM's
int CamParser::vpcount=sizeof(CAMVPINS)/sizeof(CAMVPINS[0]);

void CamParser::parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
  if (opcode!='N') return; // this is not for us. 
  if (parseN(stream,paramCount,p)) opcode=0; // we have consumed this
  // If we fail, the caller will <X> the <N command. 
}
     
bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
  (void)stream;  // probably unused parameter 
  if (CAMBaseVpin==0) CAMBaseVpin=CAMVPINS[0];  // default to CAM 1.
  VPIN vpin=CAMBaseVpin;   //use current CAM selection

  if (paramCount==0) { 
    DIAG(F("Cam base vpin:%d"),CAMBaseVpin);
    for (auto i=0;i<vpcount;i++){
       if (CAMVPINS[i]==0) break;
       DIAG(F("EXSensorCam #%d vpin %d"),i+1,CAMVPINS[i]);
    }
    return true; 
  }
  uint8_t camop=p[0]; // cam oprerator 
  int param1=0;
  int16_t param3=9999;   // =0 could invoke parameter changes. & -1 gives later errors

  if(camop=='C'){ 
    if(p[1]>=100) CAMBaseVpin=p[1];
    if(p[1]<=vpcount && p[1]>0)    CAMBaseVpin=CAMVPINS[p[1]-1];
    DIAG(F("CAM base Vpin: %c %d "),p[0],CAMBaseVpin);
    return true;
  }

  if ((camop<='a') && (camop>='A')){   //switch CAM# if p[1] or p[2] dictates (beware 'k')
    vpin=p[1];
    if(camop != 'A')   
      if(p[2] < vpcount*100+99) { vpin=(p[1] > p[2]) ? p[1] : p[2] ;   //get the larger. 
        p[2]=p[2]%100;       //strip off any CAM #
      }
    if((vpin>=100) && (int(vpin)<=vpcount*100+99)) {    //limits to CAM# 1 to vpcount
      CAMBaseVpin=CAMVPINS[vpin/100-1];     
      DIAG(F("switching to CAM %d baseVpin:%d"),vpin/100,CAMBaseVpin);     
      p[1]=p[1]%100;       //strip off any CAM #
    } 
    vpin=CAMBaseVpin;
  }

  if (CAMBaseVpin==0) return false; // no cam defined 

      // send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser  
  switch(paramCount) {    
    case 1:                          //<N ver> produces '^'
      if (STRCHR_P((const char *)F("EFGMQRVW^"),camop) == nullptr) return false;
      if (camop=='Q') param3=10;     //<NQ> for activation state of all 10 banks of sensors
      if (camop=='F') camop=']';     //<NF> for Reset/Finish webCAM.
      break;    // F Coded as ']' else conflicts with <Nf %%>
    
    case 2:                          //<N camop p1>  
      if (STRCHR_P((const char *)F("ABFHILMNOPQRSTUV"),camop)==nullptr) return false;
      param1=p[1];
      break;
    
    case 3:              //<N vpin rowY colx > or <N cmd p1 p2>
      if (p[0]>=100) {   //vpin - i.e. NOT 'A' through 'Z'
        if (p[1]>236 || p[1]<0) return false;     //row
        if (p[2]>316 || p[2]<0) return false;     //column
        camop=0x80;      // special 'a' case for IO_SensorCAM
        vpin = p[0];
      }else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false; 
      camop=p[0];
      param1 = p[1];  
      param3 = p[2];
      break;
    
    case 4:          //<N a id row col> 
      if (camop!='A') return false;          //must start with 'a' 
      if (p[3]>316 || p[3]<0) return false;
      if (p[2]>236 || p[2]<0) return false;
      if (p[1]>97 || p[1]<0) return false;   //treat as bsNo.
      vpin = vpin + (p[1]/10)*8 + p[1]%10;   //translate p[1]
      camop=0x80;    // special 'a' case for IO_SensorCAM
      param1=p[2];   // row
      param3=p[3];   // col
      break;

    default:
      return false;
  }
  DIAG(F("CamParser: %d %c %d %d"),vpin,camop,param1,param3);
  IODevice::writeAnalogue(vpin,param1,camop,param3);
  return true;
}

void CamParser::addVpin(VPIN pin) {
  // called by IO_EXSensorCam starting up a camera on a vpin
  byte slot=255;
  for (auto i=0;i<vpcount && slot==255;i++) {
    if (CAMVPINS[i]==0) {
      slot=i;
      CAMVPINS[slot]=pin;
    }
  }
  if (slot==255) {
    DIAG(F("No more than %d cameras supported"),vpcount);
    return;
  }
  if (slot==0) CAMBaseVpin=pin;
  DIAG(F("CamParser Registered cam #%dvpin %d"),slot+1,pin);
  // tell the DCCEXParser that we wish to filter commands
  DCCEXParser::setCamParserFilter(&parse);
}

================================================
FILE: CamParser.h
================================================
/*
 *  © 2023-2025, Barry Daniel  
 *  © 2025       Chris Harlow
 *  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/>.
 */


#ifndef CamParser_H
#define CamParser_H
#include <Arduino.h>
#include "IODevice.h"

class CamParser {
   public:
   static void parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
   static void addVpin(VPIN pin);
   private:
    static bool parseN(Print * stream, byte paramCount, int16_t p[]);
    static VPIN CAMBaseVpin;
    static VPIN CAMVPINS[];
    static int vpcount;
};


#endif

================================================
FILE: CommandDistributor.cpp
================================================
/*
 *  © 2022 Harald Barth
 *  © 2020-2025 Chris Harlow
 *  © 2020 Gregor Baues
 *  © 2022 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/>.
 */

#include <Arduino.h>
#include "CommandDistributor.h"
#include "SerialManager.h"
#include "WiThrottle.h"
#include "DIAG.h"
#include "defines.h"
#include "DCCWaveform.h"
#include "DCC.h"
#include "TrackManager.h"
#include "StringFormatter.h"
#include "Websockets.h"
#include "LocoSlot.h"

// variables to hold clock time
int16_t lastclocktime;
int8_t lastclockrate;


#if WIFI_ON || ETHERNET_ON || defined(SERIAL1_COMMANDS) || defined(SERIAL2_COMMANDS) || defined(SERIAL3_COMMANDS) || defined(SERIAL4_COMMANDS) || defined(SERIAL5_COMMANDS) || defined(SERIAL6_COMMANDS)
// use a buffer to allow broadcast
StringBuffer * CommandDistributor::broadcastBufferWriter=new StringBuffer(256);
template<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){
  broadcastBufferWriter->flush();
  StringFormatter::send(broadcastBufferWriter, msg...);
  broadcastToClients(type);
  if (type==COMMAND_TYPE) broadcastToClients(WEBSOCKET_TYPE);
}
#else
// on a single USB connection config, write direct to Serial and ignore flush/shove
template<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){
  (void)type; //shut up compiler warning
  StringFormatter::send(&USB_SERIAL, msg...);
}
#endif 

#ifdef CD_HANDLE_RING
  // wifi or ethernet ring streams with multiple client types
  RingStream *  CommandDistributor::ring=0;
CommandDistributor::clientType  CommandDistributor::clients[MAX_NUM_TCP_CLIENTS]={ NONE_TYPE }; // 0 is and must be NONE_TYPE

// Parse is called by Withrottle or Ethernet interface to determine which
// protocol the client is using and call the appropriate part of dcc++Ex
void  CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
  if (clientId>=sizeof (clients)) {
    // Caution, diag dump of buffer could corrupt ringstream
    // if headed by websocket bytes. 
    DIAG(F("::parse invalid client=%d"),clientId);
    return;
  }
  ring=stream;

  // First check if the client is not known
  // yet and in that case determinine type
  // NOTE: First character of transmission determines if this
  // client is using the DCC++ protocol where all commands start
  // with '<'
  if (clients[clientId] == NONE_TYPE) {
    auto websock=Websockets::checkConnectionString(clientId,buffer,stream);
    if (websock) {
      clients[clientId]=WEBSOCK_CONNECTING_TYPE;
      // websockets will have replied already 
      return;
    }
    if (buffer[0] == '<')
      clients[clientId]=COMMAND_TYPE;
    else
      clients[clientId]=WITHROTTLE_TYPE;
  }

  // after first inbound transmission the websocket is connected
  if (clients[clientId]==WEBSOCK_CONNECTING_TYPE)
      clients[clientId]=WEBSOCKET_TYPE;
      
      
  // mark buffer that is sent to parser
  // When type is known, send the string
  // to the right parser
  if (clients[clientId] == COMMAND_TYPE) {
    ring->mark(clientId);
    DCCEXParser::parse(stream, buffer, ring);
  } else if (clients[clientId] == WITHROTTLE_TYPE) {
    ring->mark(clientId);
    WiThrottle::getThrottle(clientId)->parse(ring, buffer);
  }
  else if (clients[clientId] == WEBSOCKET_TYPE) {
    buffer=Websockets::unmask(clientId,ring, buffer);
    if (!buffer) return; // unmask may have handled it alrerday (ping/pong)
    // mark ring with client flagged as websocket for transmission later
    ring->mark(clientId | Websockets::WEBSOCK_CLIENT_MARKER);
    DCCEXParser::parse(stream, buffer, ring);
    }

  if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {
    // The commit call will either write the length bytes
    // OR rollback to the mark because the reply is empty
    // or the command generated more output than fits in
    // the buffer
    if (!ring->commit()) {
      DIAG(F("OUTBOUND FULL for client %d processing cmd:%s"),clientId, buffer);
    }
  } else {
    DIAG(F("CD parse: was alredy committed")); //XXX Could have been committed by broadcastClient?!
  }
}

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 

// This will not be called on a uno 
void CommandDistributor::broadcastToClients(clientType type) {

  byte rememberClient;
  (void)rememberClient; // shut up compiler warning

  // Broadcast to Serials
  if (type==COMMAND_TYPE) SerialManager::broadcast(broadcastBufferWriter->getString());

#ifdef CD_HANDLE_RING
  // 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 (ring) {
    if ((rememberClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {
      //DIAG(F("CD precommit client %d"), rememberClient);
      ring->commit();
    }
    // loop through ring clients
    for (byte clientId=0; clientId<sizeof(clients); clientId++) {
      if (clients[clientId]==type)  {
	//DIAG(F("CD mark client %d"), clientId);
	ring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));
	ring->print(broadcastBufferWriter->getString());
	//DIAG(F("CD commit client %d"), clientId);
	ring->commit();
      }
    }
    // at this point ring is committed (NO_CLIENT) either from
    // 4 or 13 lines above.
    if (rememberClient != RingStream::NO_CLIENT) {
      //DIAG(F("CD postmark client %d"), rememberClient);
      ring->mark(rememberClient);
    }
  }
#endif
}

// Public broadcast functions below 
void  CommandDistributor::broadcastSensor(int16_t id, bool on ) {
  broadcastReply(COMMAND_TYPE, F("<%c %d>\n"), on?'Q':'q', id);
}

void  CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
  // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
  // The string below contains serial and Withrottle protocols which should
  // be safe for both types.
  broadcastReply(COMMAND_TYPE, F("<H %d %d>\n"),id, !isClosed);
#ifdef CD_HANDLE_RING
  broadcastReply(WITHROTTLE_TYPE, F("PTA%c%d\n"), isClosed?'2':'4', id);
#endif
}

void CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) {
  broadcastReply(COMMAND_TYPE, F("<I %d %d %d>\n"), id, position, moving);
}

void  CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
  // The JMRI clock command is of the form : PFT65871<;>4
  // The CS broadcast is of the form "<jC mmmm nn" where mmmm is time minutes and dd speed
  // The string below contains serial and Withrottle protocols which should
  // be safe for both types.
  broadcastReply(COMMAND_TYPE, F("<jC %d %d>\n"),time, rate);
#ifdef CD_HANDLE_RING
  broadcastReply(WITHROTTLE_TYPE, F("PFT%l<;>%d\n"), (int32_t)time*60, rate);
#endif
}

void CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate) {
  // save the latest time if changed
      if (clocktime != lastclocktime){
        auto difference = clocktime - lastclocktime;
        if (difference<0) difference+=1440;
        DCC::setTime(clocktime,clockrate,difference>2);
        byte hh=clocktime/60;
        byte mm=clocktime%60;
        if (hh>23) hh=0;
        LCD(6,clockrate<=1?F("Time %d%d:%d%d"):F("Time %d%d:%d%d (%d)"),
             hh/10, hh%10, mm/10, mm%10, clockrate);

        // look for an event for this time
#ifdef EXRAIL_ACTIVE        
        RMFT2::clockEvent(clocktime,1);
#endif        
        // Now tell everyone else what the time is.
        CommandDistributor::broadcastClockTime(clocktime, clockrate);
        lastclocktime = clocktime;
        lastclockrate = clockrate;
      }
    }

int16_t CommandDistributor::retClockTime() {
  return lastclocktime;
}

void  CommandDistributor::broadcastLoco(LocoSlot *  sp) {
  if (!sp) {
    broadcastReply(COMMAND_TYPE,F("<l 0 -1 128 0>\n"));
    return;
	}

  // Broadcast the given loco.
  // If its a consist leader, broadcast all followers too.
  // A consist follower will only be broadcast because its functions have changed.
  
  bool isFollower=sp->isConsistFollower();
  
  #ifdef CD_HANDLE_RING
  // Use the buffer directly to avoid multiple transmits in the case of a consist
  broadcastBufferWriter->flush();
  for (auto slot=sp; slot; slot=slot->getConsistNext()) {
    StringFormatter::send(broadcastBufferWriter, F("<l %d 0 %d %l>\n"), 
      slot->getLoco(),slot->getTargetSpeed(),slot->getFunctions());
    if (isFollower) break;  // dont follow next chain if original call was for a follower
  }
  broadcastToClients(COMMAND_TYPE);
  broadcastToClients(WEBSOCKET_TYPE);
  
#else
  // no ring handling, just broadcast each separately
  for (auto slot=sp; slot; slot=slot->getConsistNext()) {
    broadcastReply(COMMAND_TYPE, F("<l %d 0 %d %l>\n"), 
      slot->getLoco(),slot->getTargetSpeed(),slot->getFunctions());
    if (isFollower) break;  // dont follow next chain if original call was for a follower
  }
  #endif

  #ifdef SABERTOOTH
  if (Serial2 && sp->loco == SABERTOOTH) {
    static uint8_t rampingmode = 0;
    auto speedCode=sp->getSpeedCode();
    bool direction = (speedCode & 0x80) !=0; // true for forward
    int32_t speed = speedCode & 0x7f;
    if (speed == 1) { // emergency stop
      if (rampingmode != 1) {
	rampingmode = 1;
	Serial2.print("R1: 0\r\n");
	Serial2.print("R2: 0\r\n");
      }
      Serial2.print("MD: 0\r\n");
    } else {
      if (speed != 0) {
        // speed is here 2 to 127
	speed = (speed - 1) * 1625 / 100;
	speed = speed * (direction ? 1 : -1);
	// speed is here -2047 to 2047
      }
      if (rampingmode != 2) {
	rampingmode = 2;
	Serial2.print("R1: 2047\r\n");
	Serial2.print("R2: 2047\r\n");
      }
      Serial2.print("M1: ");
      Serial2.print(speed);
      Serial2.print("\r\n");
      Serial2.print("M2: ");
      Serial2.print(speed);
      Serial2.print("\r\n");
    }
  }
#endif
#ifdef CD_HANDLE_RING
  WiThrottle::markForBroadcast(sp->getLoco());
#endif
}

void  CommandDistributor::broadcastForgetLoco(int16_t loco) {
  broadcastReply(COMMAND_TYPE, F("<l %d 0 1 0>\n<- %d>\n"), loco,loco);
}

void  CommandDistributor::broadcastPower() {
  char pstr[] = "? x";
  byte trackcount=0;
  byte oncount=0;
  byte offcount=0;
  uint8_t numTracks = TrackManager::numTracks();
  {
    char trackLetter[numTracks+1];
    trackLetter[numTracks] = '\0';

    for(byte t=0; t<numTracks; t++) {
      if (TrackManager::getPower(t, pstr))
	broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr);
      if (TrackManager::isActive(t)) {
	trackcount++;
	// do not call getPower(t) unless isActive(t)!
	if (TrackManager::getPower(t) == POWERMODE::ON) {
	  oncount++;
	  trackLetter[t] = t + 'A';
	} else if (TrackManager::getPower(t) == POWERMODE::OFF) {
	  offcount++;
	  trackLetter[t] = t + 'a';
	} else {
	  trackLetter[t] = 'X';
	}
      } else {
	trackLetter[t] = '_';
      }
    }
    //DIAG(F("t=%d on=%d off=%d"), trackcount, oncount, offcount);

    char state='2';
    if (oncount==0 || offcount == trackcount) // none on or all active off
      state = '0';
    else if (oncount == numTracks) {          // all on, no inactive tracks
      state = '1';
    }

    if (state != '2')
      broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);

    // additional info about MAIN, PROG and JOIN
    bool main=TrackManager::getMainPower()==POWERMODE::ON;
    bool prog=TrackManager::getProgPower()==POWERMODE::ON;
    bool join=TrackManager::isJoined();
    //DIAG(F("m=%d p=%d j=%d"), main, prog, join);
    const FSH * reason=F("");
    if (join) {
      reason = F(" JOIN"); // with space at start so we can append without space
      broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
    } else {
      if (main) {
	//reason = F("MAIN");
	broadcastReply(COMMAND_TYPE, F("<p1 MAIN>\n"));
      }
      if (prog) {
	//reason = F("PROG");
	broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
      }
    }
#ifdef CD_HANDLE_RING
    // 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
#if defined(HAS_ENOUGH_MEMORY)
    LCD(2,F("PWR %s%S"),state=='1'? "On" : ( state=='0'? "Off" : trackLetter ),reason);
#else
    LCD(2,F("PWR %s%S"),trackLetter ,reason);
#endif
  }
}

void CommandDistributor::broadcastRaw(clientType type, char * msg) {
  broadcastReply(type, F("%s"),msg);
}

void CommandDistributor::broadcastEstopLock(bool locked) {
  if (locked) {
    broadcastReply(COMMAND_TYPE, F("<!PAUSED>\n"));
    broadcastReply(WITHROTTLE_TYPE, F("HmESTOP PAUSED\n"));
  } else {
    broadcastReply(COMMAND_TYPE, F("<!RESUMED>\n"));
    broadcastReply(WITHROTTLE_TYPE, F("HmESTOP RESUMED\n"));
  }
}

void CommandDistributor::broadcastMessage(char * message) {
  broadcastReply(COMMAND_TYPE, F("<m \"%s\">\n"),message);
  broadcastReply(WITHROTTLE_TYPE, F("Hm%s\n"),message);
}

void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {
  broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
}

void  CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
  broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
}

void  CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
  broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\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;


================================================
FILE: CommandDistributor.h
================================================
/*
 *  © 2022 Harald Barth
 *  © 2020-2025 Chris Harlow
 *  © 2020 Gregor Baues
 *  © 2022 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/>.
 */
#ifndef CommandDistributor_h
#define CommandDistributor_h
#include "DCCEXParser.h"
#include "RingStream.h"
#include "StringBuffer.h"
#include "defines.h"
#include "EXRAIL2.h"
#include "DCC.h"

#if WIFI_ON | ETHERNET_ON 
  // Command Distributor must handle a RingStream of clients
  #define CD_HANDLE_RING
#endif 

class CommandDistributor {
public:
  enum clientType: byte {NONE_TYPE = 0,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE}; // independent of other types, NONE_TYPE must be 0
private:
  static void broadcastToClients(clientType type);
  static StringBuffer * broadcastBufferWriter;
  #ifdef CD_HANDLE_RING
    static RingStream * ring;
    static clientType clients[MAX_NUM_TCP_CLIENTS];
  #endif
public :
  static void parse(byte clientId,byte* buffer, RingStream * ring);
  static void broadcastLoco(LocoSlot * slot);
  static void broadcastForgetLoco(int16_t loco);
  static void broadcastSensor(int16_t id, bool value);
  static void broadcastTurnout(int16_t id, bool isClosed);
  static void broadcastTurntable(int16_t id, uint8_t position, bool moving);
  static void broadcastClockTime(int16_t time, int8_t rate);
  static void setClockTime(int16_t time, int8_t rate);
  static int16_t retClockTime();
  static void broadcastPower();
  static void broadcastRaw(clientType type,char * msg);
  static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
  template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
  static void forget(byte clientId);
  static void broadcastRouteState(int16_t routeId,byte state);
  static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
  static void broadcastMessage(char * message);
  static void broadcastEstopLock(bool locked); 
  
  // 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


================================================
FILE: CommandStation-EX.ino
================================================
////////////////////////////////////////////////////////////////////////////////////
//  DCC-EX CommandStation-EX   Please see https://DCC-EX.com
//
// This file is the main sketch for the Command Station.
//
// CONFIGURATION:
// Configuration is normally performed by editing a file called config.h.
// This file is NOT shipped with the code so that if you pull a later version
// of the code, your configuration will not be overwritten.
//
// If you used the automatic installer program, config.h will have been created automatically.
//
// If config.h is not found, the command station will build with no motor shield.
////////////////////////////////////////////////////////////////////////////////////

#if __has_include ( "config.h")
  #include "config.h"
#else 
  #warning config.h not found.
#endif 
#ifndef MOTOR_SHIELD_TYPE
  #warning MOTOR_SHIELD_TYPE not found. Building with no motor shield
  #define MOTOR_SHIELD_TYPE NO_SHIELD 
#endif

/*
 *  © 2021 Neil McKechnie
 *  © 2020-2025 Chris Harlow, Harald Barth, David Cutting,
 *  Fred Decker, Gregor Baues, Anthony W - Dayton
 *  © 2023 Nathan Kellenicki
 *  © 2025 Herb Morton
 *  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/>.
 */

#include "DCCEX.h"
#include "Display_Implementation.h"
#ifdef ARDUINO_ARCH_ESP32
#include "Sniffer.h"
#include "DCCDecoder.h"
Sniffer *dccSniffer = NULL;
bool DCCDecoder::active = false;
#endif // ARDUINO_ARCH_ESP32

#define QWRAP_(x) QWRAP__(x)
#define QWRAP__(x) #x
static_assert(MAX_LOCOS >1 && MAX_LOCOS<256, "#define MAX_LOCOS " QWRAP_(MAX_LOCOS) " must be >1 and <256");

#ifdef CPU_TYPE_ERROR
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
#endif

#ifdef WIFI_WARNING
#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED
#endif
#ifdef ETHERNET_WARNING
#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED
#endif
#ifdef EXRAIL_WARNING
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
// compile time check, passwords 1 to 7 chars do not work, so do not try to compile with them at all
// remember trailing '\0', sizeof("") == 1.
#define PASSWDCHECK(S) static_assert(sizeof(S) == 1 || sizeof(S) > 8, "Password shorter than 8 chars")

void setup()
{
  // The main sketch has responsibilities during setup()

  // Responsibility 1: Start the usb connection for diagnostics
  // This is normally Serial but uses SerialUSB on a SAMD processor
  SerialManager::init();

  DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));

// If user has defined a startup delay, delay here before starting IO
#if defined(STARTUP_DELAY)
  DIAG(F("Delaying startup for %dms"), STARTUP_DELAY);
  delay(STARTUP_DELAY);
#endif

// Initialise HAL layer before reading EEprom or setting up MotorDrivers 
  IODevice::begin();

  // As the setup of a motor shield may require a read of the current sense input from the ADC,
  // let's make sure to initialise the ADCee class!
  ADCee::begin();
  // Set up MotorDrivers early to initialize all pins
  TrackManager::Setup(MOTOR_SHIELD_TYPE);

  DISPLAY_START (
    // This block is still executed for DIAGS if display not in use
    LCD(0,F("DCC-EX v" VERSION));
    LCD(1,F("Lic GPLv3"));
  );

  // Responsibility 2: Start all the communications before the DCC engine
  // Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
  // Start Ethernet if it exists
#if WIFI_ON
  PASSWDCHECK(WIFI_PASSWORD); // compile time check
#ifndef ARDUINO_ARCH_ESP32
  WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#else
  WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif // ARDUINO_ARCH_ESP32
#endif // WIFI_ON

#if ETHERNET_ON
  EthernetInterface::setup();
#endif // ETHERNET_ON
  
  // Responsibility 3: Start the DCC engine.
  DCC::begin();

  // Start RMFT aka EX-RAIL (ignored if no automnation)
  RMFT::begin();

#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
  dccSniffer = new Sniffer(BOOSTER_INPUT);
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32

  // Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
  //  This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
  #if __has_include ( "mySetup.h")
    #define SETUP(cmd) DCCEXParser::parse(F(cmd))
    #include "mySetup.h"
    #undef SETUP
  #endif

  #if defined(LCN_SERIAL)
  LCN_SERIAL.begin(115200);
  LCN::init(LCN_SERIAL);
  #endif
  LCD(3, F("Ready"));
  CommandDistributor::broadcastPower();
}

void loop()
{
  #ifdef ENABLE_SERIAL_LOG
    SerialLog.loop();
  #endif

#ifdef ARDUINO_ARCH_ESP32

#ifdef BOOSTER_INPUT
  static bool oldactive = false;
  if (dccSniffer) {
    bool newactive = dccSniffer->inputActive();
    if (oldactive != newactive) {
      RMFT2::railsyncEvent(newactive);
      oldactive = newactive;
    }
    DCCPacket p = dccSniffer->fetchPacket();
    if (p.len() != 0) {
      if (DCCDecoder::parse(p)) {
	if (Diag::SNIFFER)
	  p.print();
      }
    }
  }
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32

  // The main sketch has responsibilities during loop()

  // Responsibility 1: Handle DCC background processes
  //                   (loco reminders and power checks)
  DCC::loop();
 
  // Responsibility 2: handle any incoming commands on USB connection
  SerialManager::loop();
 
  // Responsibility 3: Optionally handle any incoming WiFi traffic
#ifndef ARDUINO_ARCH_ESP32
#if WIFI_ON
  WifiInterface::loop();
 
#endif //WIFI_ON
#else  //ARDUINO_ARCH_ESP32
#if WIFI_ON
#ifndef WIFI_TASK_ON_CORE0
  WifiESP::loop();
#endif
#endif //WIFI_ON
#endif //ARDUINO_ARCH_ESP32
#if ETHERNET_ON
  EthernetInterface::loop();
#endif

  RMFT::loop();  // ignored if no automation

  #if defined(LCN_SERIAL)
  LCN::loop();
  #endif

  // Display refresh
  DisplayInterface::loop();

  // Handle/update IO devices.
  IODevice::loop();

  Sensor::checkAll(); // Update and print changes

  // Report any decrease in memory (will automatically trigger on first call)
  static int ramLowWatermark = __INT_MAX__; // replaced on first loop

  #ifdef ARDUINO_ARCH_AVR
  // count every byte of free RAM on AVR
  int freeNow = DCCTimer::getMinimumFreeMemory();
  if (freeNow < ramLowWatermark) {
    ramLowWatermark = freeNow;
    LCD(3,F("Free RAM=%5db"), ramLowWatermark);
  }
  #else
  // on other platforms, just report every 4kb
  int freeNow = DCCTimer::getMinimumFreeMemory() / 4096;
  if (freeNow < ramLowWatermark) {
    ramLowWatermark = freeNow;
    LCD(3,F("Free RAM=%5dKb"), ramLowWatermark*4);
  }
  #endif
}


================================================
FILE: DCC.cpp
================================================
/*
 *  © 2021 Neil McKechnie
 *  © 2021 Mike S
 *  © 2021 Fred Decker
 *  © 2021 Herb Morton
 *  © 2020-2022 Harald Barth
 *  © 2020-2021 M Steve Todd
 *  © 2020-2026 Chris Harlow
 *  All rights reserved.
 *
 *  This file is part of DCC-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/>.
 */
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "DCCConsist.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#endif
#include "GITHUB_SHA.h"
#include "version.h"
#include "FSH.h"
#include "IODevice.h"
#include "EXRAIL2.h"
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "Railcom.h"
#include "DCCQueue.h"

// This module is responsible for converting API calls into
// messages to be sent to the waveform generator.
// It has no visibility of the hardware, timers, interrupts
// nor of the waveform issues such as preambles, start bits checksums or cutouts.
//
// Nor should it have to deal with JMRI responsess other than the OK/FAIL
// or cv value returned. I will move that back to the JMRI interface later
//
// The interface to the waveform generator is narrowed down to merely:
//   Scheduling a message on the prog or main track using a function
//   Obtaining ACKs from the prog track using a function
//   There are no volatiles here.

const byte FN_GROUP_1=0x01;
const byte FN_GROUP_2=0x02;
const byte FN_GROUP_3=0x04;
const byte FN_GROUP_4=0x08;
const byte FN_GROUP_5=0x10;

FSH* DCC::shieldName=NULL;
byte DCC::globalSpeedsteps=128;

#define SLOTLOOP for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext())

void DCC::begin() {
  StringFormatter::send(&USB_SERIAL,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
#ifndef DISABLE_EEPROM
  // Load stuff from EEprom
  (void)EEPROM; // tell compiler not to warn this is unused
  EEStore::init();
#endif
#ifndef ARDUINO_ARCH_ESP32 /* On ESP32 started in TrackManager::setTrackMode() */
  DCCWaveform::begin();
#endif
}

byte DCC::defaultMomentumA=0;
byte DCC::defaultMomentumD=0;
bool DCC::linearAcceleration=false; 

byte DCC::getMomentum(LocoSlot * slot) {
   auto target=slot->getTargetSpeed() & 0x7f;
   auto current=slot->getSpeedCode() & 0x7f;
   if (target > current) {
    // accelerating
    auto momentum=(slot->getMomentumA() == MOMENTUM_USE_DEFAULT) ? defaultMomentumA : slot->getMomentumA();
    // if nonlinear acceleration, momentum is reduced according to 
    // gap between throttle and speed.
    // ie. Loco takes accelerates faster if high throttle
    if (momentum==0 || linearAcceleration) return momentum;
    auto powerDifference= (target-current)/8;
    if (momentum-powerDifference <0) return 0;
    return momentum-powerDifference; 
   }
   return (slot->getMomentumD() == MOMENTUM_USE_DEFAULT) ? defaultMomentumD : slot->getMomentumD();
}

bool DCC::setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection)  {
  if (tSpeed==1) {
    if (cab==0) {
      estopAll(); // ESTOP broadcast fix 
      return true;
    }
  } 
  byte speedCode = (tSpeed & 0x7F)  + tDirection * 128;
  auto slot=LocoSlot::getSlot(cab, true);
  if (!slot) return false; // speed table full, can not do anything

  // Ignore any throttle command to consist follower.
  if (slot->isConsistFollower()) return true; 
  
  if (slot->getTargetSpeed()==speedCode) // speed has been reached
    return true;
  slot->setTargetSpeed(speedCode);

  // copy target speed to consist followers
  for (auto follower=slot->getConsistNext(); follower; follower=follower->getConsistNext()) {
    follower->setTargetSpeed(speedCode ^ (follower->isConsistReverse() ? 0x80 : 0) );
  }

  byte momentum=getMomentum(slot);
  if (momentum && tSpeed!=1) { // not ESTOP
    // we dont throttle speed, we just let the reminders take it to target
    slot->setMomentumBase(millis());
  } 
  else {  // Momentum not involved, throttle now.
    setThrottle2(slot,speedCode);
    if (!estopIsLocked) TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr
  }
  CommandDistributor::broadcastLoco(slot);
  return true;
}

// set speedCode directly to DCC and make followers same
void DCC::setThrottle2( LocoSlot * slot, byte speedCode)  {
  for (auto follower=slot; follower; follower=follower->getConsistNext()) {
    follower->setSpeedCode(speedCode);
    setThrottleDCC(follower,speedCode);
  } 
}

void DCC::setThrottleDCC( LocoSlot * slot, byte speedCode)  {
  uint16_t cab=0;
  if (slot) {
    cab=slot->getLoco();
    if (slot->isConsistReverse()) speedCode^=0x80;
  }
  // any throttle traffic is blocked when estop is locked
  if (estopIsLocked) {
    // replace speed with emergency stop but keep direction
    speedCode = (speedCode & 0x80) | 0x01;
  }
  uint8_t b[4];
  uint8_t nB = 0;
  // DIAG(F("setSpeedInternal %d %x"),cab,speedCode);
  if (cab > HIGHEST_SHORT_ADDR)
    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address
  b[nB++] = lowByte(cab);

  if (globalSpeedsteps <= 28) {

    uint8_t speed128 = speedCode & 0x7F;
    uint8_t speed28;
    uint8_t code28;

    if (speed128 == 0 || speed128 == 1) { // stop or emergency stop
      code28 = speed128;
    } else {
      speed28= (speed128*10+36)/46;                 // convert 2-127 to 1-28
/*
      if (globalSpeedsteps <= 14)                   // Don't want to do 14 steps, to get F0 there is ugly
        code28 = (speed28+3)/2 | (Value of F0);     // convert 1-28 to DCC 14 step speed code
      else
*/
      code28 = (speed28+3)/2 | ( (speed28 & 1) ? 0 : 0b00010000 ); // convert 1-28 to DCC 28 step speed code
    }
    //        Construct command byte from:
    //        command      speed    direction
    b[nB++] = 0b01000000 | code28 | ((speedCode & 0x80) ? 0b00100000 : 0);

  } else { // 128 speedsteps

    b[nB++] = SET_SPEED;                      // 128-step speed control byte
    b[nB++] = speedCode; // for encoding see setThrottle

  }
  if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority
  else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
}

void DCC::setFunctionInternal(int cab, byte group, byte byte1, byte byte2) {
  // DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
  byte b[4];
  byte nB = 0;

  if (cab > HIGHEST_SHORT_ADDR)
    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address
  b[nB++] = lowByte(cab);
  if (byte1!=0) b[nB++] = byte1;
  b[nB++] = byte2;
  
  DCCQueue::scheduleDCCFunctionPacket(b, nB, cab,group);
}

// returns speed steps 0 to 127 (1 == emergency stop)
// or -1 on "loco not found"
int8_t DCC::getThrottleSpeed(int cab) {
  return getThrottleSpeedByte(cab) & 0x7F;
}

// returns speed code byte
// or 128 (speed 0, dir forward) on "loco not found".
// This is the throttle set speed... which may be different
// from the actual loco speed if momentum is active or
// speed limiting is applied.
uint8_t DCC::getThrottleSpeedByte(int cab) {
  auto slot=LocoSlot::getSlot(cab,false);
  return slot?slot->getTargetSpeed():128;
}
// returns speed code byte for loco. 
// This is the most recently send DCC speed packet byte
// or 128 (speed 0, dir forward) on "loco not found".
uint8_t DCC::getLocoSpeedByte(int cab) {
  auto slot=LocoSlot::getSlot(cab,false);
  return slot?slot->getSpeedCode():128;
}

// returns 0 to 7 for frequency
uint8_t DCC::getThrottleFrequency(int cab) {
#if defined(ARDUINO_AVR_UNO)
  (void)cab;
  return 0;
#else
 auto slot=LocoSlot::getSlot(cab,false);
 if (slot == nullptr)  // speed table full, can not do anything
    return 0;           // return value for default frequency
  // shift out first 29 bits so we have the 3 "frequency bits" left
  uint8_t res = (uint8_t)(slot->getFunctions() >>29);
  //DIAG(F("Speed table %d functions %l shifted %d"), reg, slot->functions, res);
  return res;
#endif
}

// returns direction on loco
// or true/forward on "loco not found"
bool DCC::getThrottleDirection(int cab) {
  return getThrottleSpeedByte(cab) & 0x80;
}

// Set function to value on or off
bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
  if (cab<=0 ) return false;
  if (functionNumber < 0) return false;

  if (functionNumber>28) {
    //non reminding advanced binary bit set
    byte b[5];
    byte nB = 0;
    if (cab > HIGHEST_SHORT_ADDR)
      b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address
    b[nB++] = lowByte(cab);
    if (functionNumber <= 127) {
       b[nB++] = 0b11011101;   // Binary State Control Instruction short form
       b[nB++] = functionNumber | (on ? 0x80 : 0);
    }
    else  {
       b[nB++] = 0b11000000;   // Binary State Control Instruction long form
       b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0);  // low order bits and state flag
       b[nB++] = functionNumber >>7 ;  // high order bits
    }
    DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
  }
  // We use the reminder table up to 28 for normal functions.
  // We use 29 to 31 for DC frequency as well so up to 28
  // are "real" functions and 29 to 31 are frequency bits
  // controlled by function buttons
  if (functionNumber > 31)
    return true;
  
   auto slot=LocoSlot::getSlot(cab,true);
   if (!slot) return false; // speed table full, can not do anything

   // Take care of functions:
  // Set state of function
  uint32_t previous=slot->getFunctions();
  uint32_t newset=previous;
  uint32_t funcmask = (1UL<<functionNumber);
  if (on) {
      newset |= funcmask;
  } else {
      newset &= ~funcmask;
  }
  if (newset != previous) {
    slot->setFunctions(newset);
    if (functionNumber <= 28)
      updateGroupflags(slot, functionNumber);
    CommandDistributor::broadcastLoco(slot);
  }
  return true;
}

// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
  auto currentValue=getFn(cab,functionNumber);
  if (currentValue<0) return;  // function not valid for change  
  setFn(cab,functionNumber, currentValue?false:true);
}

// Report function state (used from withrottle protocol)
// returns 0 false, 1 true or -1 for do not know
int8_t DCC::getFn( int cab, int16_t functionNumber) {
  if (cab<=0 || functionNumber>31)
    return -1;  // unknown
  auto slot = LocoSlot::getSlot(cab, false);
  if (slot == nullptr) // not found
    return -1;
  unsigned long funcmask = (1UL<<functionNumber);
  return  (slot->getFunctions() & funcmask)? 1 : 0;
}

// Set the group flag to say we have touched the particular group.
// A group will be reminded only if it has been touched.
void DCC::updateGroupflags(LocoSlot * slot, int16_t functionNumber) {
  byte groupMask;
  if (functionNumber<=4)       groupMask=FN_GROUP_1;
  else if (functionNumber<=8)  groupMask=FN_GROUP_2;
  else if (functionNumber<=12) groupMask=FN_GROUP_3;
  else if (functionNumber<=20) groupMask=FN_GROUP_4;
  else                         groupMask=FN_GROUP_5;
  slot->setGroupFlags((slot->getGroupFlags()) | groupMask);
}

uint32_t DCC::getFunctionMap(int cab) {
  if (cab<=0) return 0;  // unknown pretend all functions off
  auto slot = LocoSlot::getSlot(cab,false);
  return slot?slot->getFunctions():0;
}

// saves DC frequency (0..3) in spare functions 29,30,31
void DCC::setDCFreq(int cab,byte freq) {
  if (cab==0 || freq>3) return;
  auto slot=LocoSlot::getSlot(cab,true);
  if (!slot) return; // speed table full, can not do anything
  
  // drop and replace F29,30,31 (top 3 bits) 
  auto newFunctions=slot->getFunctions() & 0x1FFFFFFFUL; // get and clear relevant bits
  if (freq != 0) newFunctions |= (1UL<<(28 + freq));     // 1->29, 2->30, 3->31
  if (newFunctions==slot->getFunctions()) return; // no change 
  slot->setFunctions(newFunctions);
  CommandDistributor::broadcastLoco(slot);
}

void DCC::saveSpeed(int cab) {
  auto slot=LocoSlot::getSlot(cab,false);
  if (slot == nullptr)  // speed table full, can not do anything
    return;
  slot->saveSpeed();
}

void DCC::restoreSpeed(int cab) {
  auto slot=LocoSlot::getSlot(cab,false);
  if (slot == nullptr)  // speed table full, can not do anything
    return;
  auto speedCode=slot->getSavedSpeedCode();  
  setThrottle(cab, speedCode& 0x7f, (speedCode & 0x80)!=0);
}

void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
  // onoff is tristate:
  // 0  => send off packet
  // 1  => send on packet
  // >1 => send both on and off packets.

  // An accessory has an address, 4 ports and 2 gates (coils) each. That's how
  // the initial decoders were orgnized and that influenced how the DCC
  // standard was made.
  #ifdef DIAG_IO
  DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
  #endif
  // use masks to detect wrong values and do nothing
  if(address != (address & 511))
    return;
  if(port != (port & 3))
    return;
  byte b[2];

  // first byte is of the form 10AAAAAA, where AAAAAA represent 6 least signifcant bits of accessory address
  // second byte is of the form 1AAACPPG, where C is 1 for on, PP the ports 0 to 3 and G the gate (coil).
  b[0] = address % 64 + 128;
  b[1] = ((((address / 64) % 8) << 4) + (port % 4 << 1) + gate % 2) ^ 0xF8;
  if (onoff==0) {   // off packet only
    b[1] &= ~0x08; // set C to 0
    DCCQueue::scheduleDCCPacket(b, 2, 3);
  } else if (onoff==1) { // on packet only
    DCCQueue::scheduleDCCPacket(b, 2, 3);
  } else { // auto timed on then off  
    DCCQueue::scheduleAccOnOffPacket(b, 2, 3, 100); // On then off after 100mS
  } 
#if defined(EXRAIL_ACTIVE)
  if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate);
#endif
}

bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {

/* From https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf

The Extended Accessory Decoder Control Packet is included for the purpose of transmitting aspect control to signal 
decoders or data bytes to more complex accessory decoders. Each signal head can display one aspect at a time. 
{preamble} 0 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1

XXXXX is for a single head. A value of 00000 for XXXXX indicates the absolute stop aspect. All other aspects 
represented by the values for XXXXX are determined by the signaling system used and the prototype being 
modeled.

From https://normen.railcommunity.de/RCN-213.pdf:

More information is in RCN-213 about how the address bits are organized.
preamble -0- 1 0 A7 A6 A5 A4 A3 A2 -0- 0 ^A10 ^A9 ^A8 0 A1 A0 1 -0- ....

Thus in byte packet form the format is 10AAAAAA, 0AAA0AA1, 000XXXXX

Die Adresse für den ersten erweiterten Zubehördecoder ist wie bei den einfachen
Zubehördecodern die Adresse 4 = 1000-0001 0111-0001 . Diese Adresse wird in
Anwenderdialogen als Adresse 1 dargestellt.

This means that the first address shown to the user as "1" is mapped
to internal address 4.

Note that the Basic accessory format mentions "By convention these
bits (bits 4-6 of the second data byte) are in ones complement" but
this note is absent from the advanced packet description. The
english translation does not mention that the address format for
the advanced packet follows the one for the basic packet but
according to the RCN-213 this is the case.

We allow for addresses from -3 to 2047-3 as that allows to address the
whole range of the 11 bits sent to track.
*/
  if ((address > 2044) || (address < -3)) return false; // 2047-3, 11 bits but offset 3
  if (value != (value & 0x1F)) return false;            // 5 bits

  address+=3;                        // +3 offset according to RCN-213
  byte b[3];
  b[0]= 0x80                         // bits always on
    | ((address>>2) & 0x3F);         // shift out 2, mask out used bits
  b[1]= 0x01                         // bits always on
    | (((~(address>>8)) & 0x07)<<4)  // shift out 8, invert, mask 3 bits, shift up 4
    | ((address & 0x03)<<1);         // mask 2 bits, shift up 1
  b[2]=value;
  DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);
  return true;
}

//
// writeCVByteMain: Write a byte with PoM on main. This writes
// the 5 byte sized packet to implement this DCC function
//
void DCC::writeCVByteMain(int cab, int cv, byte bValue)  {
  byte b[5];
  byte nB = 0;
  if (cab > HIGHEST_SHORT_ADDR)
    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address

  b[nB++] = lowByte(cab);
  b[nB++] = cv1(WRITE_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03
  b[nB++] = cv2(cv);
  b[nB++] = bValue;

  DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}

//
// readCVByteMain: Read a byte with PoM on main.
// This requires Railcom active 
//
void DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback)  {
  byte b[5];
  byte nB = 0;
  if (cab > HIGHEST_SHORT_ADDR)
    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address

  b[nB++] = lowByte(cab);
  b[nB++] = cv1(READ_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03
  b[nB++] = cv2(cv);
  b[nB++] = 0;

  DCCQueue::scheduleDCCPacket(b, nB, 2,cab);
  Railcom::anticipate(cab,cv,callback);
}

//
// writeCVBitMain: Write a bit of a byte with PoM on main. This writes
// the 5 byte sized packet to implement this DCC function
//
void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue)  {
  byte b[5];
  byte nB = 0;
  bValue = bValue % 2;
  bNum = bNum % 8;

  if (cab > HIGHEST_SHORT_ADDR)
    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address

  b[nB++] = lowByte(cab);
  b[nB++] = cv1(WRITE_BIT_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03
  b[nB++] = cv2(cv);
  b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;

  DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}

bool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) {
  /* see rcn-122
  5 Global commands 
These commands are sent and begin exclusively with a broadcast address 0 
always with {synchronous bits} 0 0000-0000 … and end with the checksum 
... PPPPPPPP 1. Therefore, only the bytes of the commands and not that of 
shown below whole package shown. The commands can be used by vehicle and 
accessory decoders alike. 

  5.1 Time command 
This command is four bytes long and has the format: 
1100-0001 CCxx-xxxx xxxx-xxxxx xxxx-xxxx 
CC indicates what data is transmitted in the packet: 
CC = 00 Model Time 
1100-0001 00MM-MMMM WWWH-HHHH U0BB-BBBB with: 
MMMMMM = Minutes, Value range: 0..59 
WWW =     Day of the Week, Value range: 0 = Monday, 1 = Tuesday, 2 = Wednesday, 
3 = Thursday, 4 = Friday, 5 = Saturday, 6 = Sunday, 7 = Weekday 
is not supported. 
HHHHH =   Hours, value range: 0..23 
U =         
Update, i.e. the time has changed suddenly, e.g. by a new one timetable to start.        
Up to 4 can occur per sudden change commands can be marked like this. 
BBBBBB =    Acceleration factor, value range 0..63. An acceleration factor of 0 means the  
model clock has been stopped, a factor of 1 corresponds to real time, at 2 the 
clock runs twice as fast, at three times as fast as real time, etc. 
*/
if (minutes>=1440 || speed>63 ) return false; 
byte b[5];
b[0]=0; // broadcast address
b[1]=0b11000001; // 1100-0001 (model time)
b[2]=minutes % 60 ;  // MM
b[3]= 0b11100000 | (minutes/60); // 111H-HHHH weekday not supported
b[4]= (suddenChange ? 0b10000000 : 0) | speed;
DCCQueue::scheduleDCCPacket(b, sizeof(b), 2);
return true; 
}

FSH* DCC::getMotorShieldName() {
  return shieldName;
}

const ackOp FLASH WRITE_BIT0_PROG[] = {
     BASELINE,
     W0,WACK,
     V0, WACK,  // validate bit is 0
     ITC1,      // if acked, callback(1)
     CALLFAIL  // callback (-1)
};
const ackOp FLASH WRITE_BIT1_PROG[] = {
     BASELINE,
     W1,WACK,
     V1, WACK,  // validate bit is 1
     ITC1,      // if acked, callback(1)
     CALLFAIL  // callback (-1)
};

const ackOp FLASH VERIFY_BIT0_PROG[] = {
     BASELINE,
     V0, WACK,  // validate bit is 0
     ITC0,      // if acked, callback(0)
     V1, WACK,  // validate bit is 1
     ITC1,
     CALLFAIL  // callback (-1)
};
const ackOp FLASH VERIFY_BIT1_PROG[] = {
     BASELINE,
     V1, WACK,  // validate bit is 1
     ITC1,      // if acked, callback(1)
     V0, WACK,
     ITC0,
     CALLFAIL  // callback (-1)
};

const ackOp FLASH READ_BIT_PROG[] = {
     BASELINE,
     V1, WACK,  // validate bit is 1
     ITC1,      // if acked, callback(1)
     V0, WACK,  // validate bit is zero
     ITC0,      // if acked callback 0
     CALLFAIL       // bit not readable
     };

const ackOp FLASH WRITE_BYTE_PROG[] = {
      BASELINE,
      WB,WACK,ITC1,    // Write and callback(1) if ACK
      // handle decoders that dont ack a write
      VB,WACK,ITC1,    // validate byte and callback(1) if correct
      CALLFAIL        // callback (-1)
      };

const ackOp FLASH VERIFY_BYTE_PROG[] = {
      BASELINE,
      BIV,         // ackManagerByte initial value
      VB,WACK,     // validate byte
      ITCB,       // if ok callback value	
      STARTMERGE,    //clear bit and byte values ready for merge pass
      // each bit is validated against 0 and the result inverted in MERGE
      // this is because there tend to be more zeros in cv values than ones.
      // There is no need for one validation as entire byte is validated at the end
      V0, WACK, MERGE,        // read and merge first tested bit (7)
      ITSKIP,                 // do small excursion if there was no ack
        SETBIT,(ackOp)7,
        V1, WACK, NAKFAIL,    // test if there is an ack on the inverse of this bit (7)
        SETBIT,(ackOp)6,      // and abort whole test if not else continue with bit (6)
      SKIPTARGET,
      V0, WACK, MERGE,        // read and merge second tested bit (6)
      V0, WACK, MERGE,        // read and merge third  tested bit (5) ...
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCBV,  // verify merged byte and return it if acked ok - with retry report
      CALLFAIL };


const ackOp FLASH READ_CV_PROG[] = {
      BASELINE,
      STARTMERGE,    //clear bit and byte values ready for merge pass
      // each bit is validated against 0 and the result inverted in MERGE
      // this is because there tend to be more zeros in cv values than ones.
      // There is no need for one validation as entire byte is validated at the end
      V0, WACK, MERGE,        // read and merge first tested bit (7)
      ITSKIP,                 // do small excursion if there was no ack
        SETBIT,(ackOp)7,
        V1, WACK, NAKFAIL,    // test if there is an ack on the inverse of this bit (7)
        SETBIT,(ackOp)6,      // and abort whole test if not else continue with bit (6)
      SKIPTARGET,
      V0, WACK, MERGE,        // read and merge second tested bit (6)
      V0, WACK, MERGE,        // read and merge third  tested bit (5) ...
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCB,  // verify merged byte and return it if acked ok
      CALLFAIL };          // verification failed

// returns loco id or consist id or -1 on failure
const ackOp FLASH LOCO_DRIVEAWAY_PROG[] = {
      BASELINE,
      // first check cv20 for extended addressing
      SETCV, (ackOp)20,     // CV 19 is extended
      SETBYTE, (ackOp)0,
      VB, WACK, ITSKIP,     // skip past extended section if cv20 is zero
      // read cv20 and 19 and merge 
      STARTMERGE,           // Setup to read cv 20
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKSKIP, // bad read of cv20, assume its 0 
      BAD20SKIP,     // detect invalid cv20 value and ignore 
      STASHLOCOID,   // keep cv 20 until we have cv19 as well.
      SETCV, (ackOp)19, 
      STARTMERGE,           // Setup to read cv 19
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // cant recover if cv 19 unreadable
      COMBINE1920,  // Combile byte with stash and callback
// end of advanced 20,19 check
      SKIPTARGET,
      SETCV, (ackOp)19,     // CV 19 is consist setting
      SETBYTE, (ackOp)0,
      VB, WACK, ITSKIP,     // ignore consist if cv19 is zero (no consist)
      SETBYTE, (ackOp)128,
      VB, WACK, ITSKIP,     // ignore consist if cv19 is 128 (no consist, direction bit set)
      STARTMERGE,           // Setup to read cv 19
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCB7,  // return 7 bits only, No_ACK means CV19 not supported so ignore it

      SKIPTARGET,     // continue here if CV 19 is zero or fails all validation
      SETCV,(ackOp)29,
      SETBIT,(ackOp)5,
      V0, WACK, ITSKIP,  // Skip to SKIPTARGET if bit 5 of CV29 is zero

      // Long locoid
      SETCV, (ackOp)17,       // CV 17 is part of locoid
      STARTMERGE,
      V0, WACK, MERGE,  // read and merge bit 1 etc
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok
      STASHLOCOID,         // keep stashed cv 17 for later
      // Read 2nd part from CV 18
      SETCV, (ackOp)18,
      STARTMERGE,
      V0, WACK, MERGE,  // read and merge bit 1 etc
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok
      COMBINELOCOID,        // Combile byte with stash to make long locoid and callback

      // ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that
      SKIPTARGET,
      SETCV, (ackOp)1,
      STARTMERGE,
      SETBIT, (ackOp)6,  // skip over first bit as we know its a zero
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCB,  // verify merged byte and callback
      CALLFAIL
      };

// returns loco id regardless of consist 
const ackOp FLASH LOCO_READ_ID_PROG[] = {
      BASELINE,
      SETCV,(ackOp)29,
      SETBIT,(ackOp)5,
      V0, WACK, ITSKIP,  // Skip to SKIPTARGET if bit 5 of CV29 is zero

      // Long locoid
      SETCV, (ackOp)17,       // CV 17 is part of locoid
      STARTMERGE,
      V0, WACK, MERGE,  // read and merge bit 1 etc
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok
      STASHLOCOID,         // keep stashed cv 17 for later
      // Read 2nd part from CV 18
      SETCV, (ackOp)18,
      STARTMERGE,
      V0, WACK, MERGE,  // read and merge bit 1 etc
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok
      COMBINELOCOID,        // Combile byte with stash to make long locoid and callback

      // ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that
      SKIPTARGET,
      SETCV, (ackOp)1,
      STARTMERGE,
      SETBIT, (ackOp)6,  // skip over first bit as we know its a zero
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCB,  // verify merged byte and callback
      CALLFAIL
      };

// returns consist id or -1 on failure
const ackOp FLASH LOCO_READ_CONSIST_PROG[] = {
      BASELINE,
      // first check cv20 for extended addressing
      SETCV, (ackOp)20,     // CV 19 is extended
      SETBYTE, (ackOp)0,
      VB, WACK, ITSKIP,     // skip past extended section if cv20 is zero
      // read cv20 and 19 and merge 
      STARTMERGE,           // Setup to read cv 20
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKSKIP, // bad read of cv20, assume its 0 
      BAD20SKIP,     // detect invalid cv20 value and ignore 
      STASHLOCOID,   // keep cv 20 until we have cv19 as well.
      SETCV, (ackOp)19, 
      STARTMERGE,           // Setup to read cv 19
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, NAKFAIL,  // cant recover if cv 19 unreadable
      COMBINE1920,  // Combile byte with stash and callback
// end of advanced 20,19 check
      SKIPTARGET,
      SETCV, (ackOp)19,     // CV 19 is consist setting
      SETBYTE, (ackOp)0,
      VB, WACK, ITC0,     // rerturn 0 if cv19 is zero (no consist)
      SETBYTE, (ackOp)128,
      VB, WACK, ITC0,     // return 0 if cv19 is 128 (no consist, direction bit set)
      STARTMERGE,           // Setup to read cv 19
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      V0, WACK, MERGE,
      VB, WACK, ITCB7,  // return 7 bits only, No_ACK means CV19 not supported so ignore it
      CALLFAIL // return -1 if cv19 wont verify
      };

const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
      BASELINE,
      // Clear consist CV 19,20
      SETCV,(ackOp)20,
      SETBYTE, (ackOp)0,
      WB,WACK,     // ignore dedcoder without cv20 support
      SETCV,(ackOp)19,
      SETBYTE, (ackOp)0,
      WB,WACK,     // ignore dedcoder without cv19 support
      // Turn off long address flag
      SETCV,(ackOp)29,
      SETBIT,(ackOp)5,
      W0,WACK,
      V0,WACK,NAKFAIL,
      SETCV, (ackOp)1,
      SETBYTEL,   // low byte of word
      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok
      VB,WACK,ITC1,   // Some decoders do not ack and need verify
      CALLFAIL
};

// for CONSIST_ID_PROG the 20,19 values are already calculated
const ackOp FLASH CONSIST_ID_PROG[] = {
      BASELINE,
      SETCV,(ackOp)20,
      SETBYTEH,    // high byte to CV 20
      WB,WACK,ITSKIP,
      FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
      SKIPTARGET,
      SETCV,(ackOp)19,
      SETBYTEL,   // low byte of word
      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok
      VB,WACK,ITC1,   // Some decoders do not ack and need verify
      CALLFAIL
};

const ackOp FLASH LONG_LOCO_ID_PROG[] = {
      BASELINE,
      // Clear consist CV 19,20
      SETCV,(ackOp)20,
      SETBYTE, (ackOp)0,
      WB,WACK,     // ignore dedcoder without cv20 support
      SETCV,(ackOp)19,
      SETBYTE, (ackOp)0,
      WB,WACK,     // ignore decoder without cv19 support
      // Turn on long address flag cv29 bit 5
      SETCV,(ackOp)29,
      SETBIT,(ackOp)5,
      W1,WACK,
      V1,WACK,NAKFAIL,
      // Store high byte of address in cv 17
      SETCV, (ackOp)17,
      SETBYTEH, // high byte of word
      WB,WACK,      // do write
      ITSKIP,       // if ACK, jump to SKIPTARGET
        VB,WACK,    // try verify instead
        ITSKIP,     // if ACK, jump to SKIPTARGET
          CALLFAIL, // if still here, fail
      SKIPTARGET,
      // store
      SETCV, (ackOp)18,
      SETBYTEL,   // low byte of word
      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok
      VB,WACK,ITC1,   // Some decoders do not ack and need verify
      CALLFAIL
};

void  DCC::writeCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback)  {
  DCCACK::Setup(cv, byteValue,  WRITE_BYTE_PROG, callback);
}

void DCC::writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback)  {
  if (bitNum >= 8) callback(-1);
  else DCCACK::Setup(cv, bitNum, bitValue?WRITE_BIT1_PROG:WRITE_BIT0_PROG, callback);
}

void  DCC::verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback)  {
  DCCACK::Setup(cv, byteValue,  VERIFY_BYTE_PROG, callback);
}

void DCC::verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback)  {
  if (bitNum >= 8) callback(-1);
  else DCCACK::Setup(cv, bitNum, bitValue?VERIFY_BIT1_PROG:VERIFY_BIT0_PROG, callback);
}


void DCC::readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback)  {
  if (bitNum >= 8) callback(-1);
  else DCCACK::Setup(cv, bitNum,READ_BIT_PROG, callback);
}

void DCC::readCV(int16_t cv, ACK_CALLBACK callback)  {
  DCCACK::Setup(cv, 0,READ_CV_PROG, callback);
}

void DCC::getDriveawayLocoId(ACK_CALLBACK callback) {
  DCCACK::Setup(0,0, LOCO_DRIVEAWAY_PROG, callback);
}

void DCC::getLocoId(ACK_CALLBACK callback) {
  DCCACK::Setup(0,0, LOCO_READ_ID_PROG, callback);
}

void DCC::getConsistId(ACK_CALLBACK callback) {
  DCCACK::Setup(0,0, LOCO_READ_CONSIST_PROG, callback);
}

void DCC::setLocoId(int id,ACK_CALLBACK callback) {
  if (id<1 || id>10239) { //0x27FF according to standard
    callback(-1);
    return;
  }
  if (id<=HIGHEST_SHORT_ADDR)
      DCCACK::Setup(id, SHORT_LOCO_ID_PROG, callback);
  else
      DCCACK::Setup(id | 0xc000,LONG_LOCO_ID_PROG, callback);
}

void DCC::setConsistId(int id,bool reverse,ACK_CALLBACK callback) {
  if (id<0 || id>10239) { //0x27FF according to standard
    callback(-1);
    return;
  }
  byte cv20;
  byte cv19;

  if (id<=HIGHEST_SHORT_ADDR) {
    cv19=id;
    cv20=0;
  }
  else {
    cv20=id/100;
    cv19=id%100;
  }
  if (reverse) cv19|=0x80;
  DCCACK::Setup((cv20<<8)|cv19, CONSIST_ID_PROG, callback);
}

void DCC::forgetLoco(int cab) {  // removes any speed reminders for this loco
  auto slot=LocoSlot::getSlot(cab, false);
  if (slot) {
     setThrottleDCC(slot,1); // ESTOP this loco if still on track
     if (nextLocoReminder==slot) nextLocoReminder=slot->getNext(); // move on if we are about to forget this one
    if (slot->isConsistLead() || slot->isConsistFollower()) 
      DCCConsist::deleteAnyConsist(cab); // unchain any consist
    slot->forget(); // no longer used but not end of world
    CommandDistributor::broadcastForgetLoco(cab);
  }
}
void DCC::forgetAllLocos() {  // removes all speed reminders
  setThrottleDCC(nullptr,1); // ESTOP all locos still on track
  SLOTLOOP {
     CommandDistributor::broadcastForgetLoco(slot->getLoco());
  }
  LocoSlot::forgetAll(); // will effectively forget all consist chains 
  nextLocoReminder=nullptr;  
}

byte DCC::loopStatus=0;

void DCC::loop()  {
  TrackManager::loop(); // power overload checks
  if (DCCWaveform::mainTrack.isReminderWindowOpen()) {
    // Now is a good time to choose a packet to be sent
    // Either highest priority from the queues or a reminder
    if (!DCCQueue::scheduleNext(false)) {
      // none pending, 
      issueReminders();
      DCCQueue::scheduleNext(true); // send any pending and force an idle if none 
    }

  }
}

void DCC::issueReminders() {

  // note, chainModified is set whenever a loco is added or removed
  // so we dont accidentally follow a stale pointer.

  if (LocoSlot::chainModified || nextLocoReminder==nullptr) {
    LocoSlot::chainModified=false;
    nextLocoReminder=LocoSlot::getFirst(); // start at the beginning
    if (nextLocoReminder==nullptr) return; // no locos at all
    // we have reached the end of the table, so we can move on to 
    // the next loop state and start from the top.
    // There are 0-9 loop states..  speed,f1,speed,f2,speed,f3,speed,f4,speed,f5
    loopStatus++;
    if (loopStatus>9) loopStatus=0; // reset to 0
  }

  for (auto slot=nextLocoReminder;slot;slot=slot->getNext()) {
    if (issueReminder(slot)) {
      nextLocoReminder=slot->getNext(); // remember next one to check
      return; // reminder sent, exit
      }
    }
  
    // if we get to her, we have reached the end of the table.
    // The next call will move on to restart 
    nextLocoReminder=nullptr;
  
}

int16_t normalize(byte speed) {
   if (speed & 0x80) return speed & 0x7F;
   return 0-1-speed; 
}

byte dccalize(int16_t speed) {
   if (speed>127) return 0xFF;  // 127 forward
   if (speed<-127) return 0x7F;  // 127 reverse
   if (speed >=0) return speed | 0x80;
   // negative speeds... -1==dcc 0, -2==dcc 1 
   return (int16_t)-1 - speed; 
}

bool DCC::issueReminder(LocoSlot * slot) {
  unsigned long functions=slot->getFunctions();
  int loco=slot->getLoco();
  byte flags=slot->getGroupFlags();

  switch (loopStatus) {
        case 0:
        case 2:
        case 4:
        case 6:
        case 8: {
         // dont remind consist followers here 
         if (slot->isConsistFollower()) break; 

         // calculate any momentum change going on
         auto sc=slot->getSpeedCode();
         if (slot->getTargetSpeed() != sc) {
            // calculate new speed code 
            auto now=millis();
            int16_t delay=now-slot->getMomentumBase();
            auto millisPerNotch=MOMENTUM_FACTOR * (int16_t)getMomentum(slot);
            // allow for momentum change to 0 while accelerating/slowing
            auto ticks=(millisPerNotch>0)?(delay/millisPerNotch):500;
            if (ticks>0) {
              auto current=normalize(sc);  // -128..+127
              auto target=normalize(slot->getTargetSpeed());
              // DIAG(F("Momentum l=%d ti=%d sc=%d c=%d t=%d"),loco,ticks,sc,current,target);
              if (current<target) { // accelerate
                current+=ticks;
                if (current>target) current=target;
              }
              else  { // slow
                current-=ticks;
                if (current<target) current=target;
              }
              sc=dccalize(current);
              //DIAG(F("c=%d newsc=%d"),current,sc);
              slot->setSpeedCode(sc);
              if (!estopIsLocked) TrackManager::setDCSignal(loco,sc); // in case this is a dcc track on this addr
              slot->setMomentumBase(now);  
            }
          }
          // DIAG(F("Reminder %d speed %d"),loco,slot->speedCode);
          setThrottle2(slot, sc); // includes consist followers
        }
        return true; // reminder sent
       case 1: // remind function group 1 (F0-F4)
          if (flags & FN_GROUP_1) {
            setFunctionInternal(loco,1,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
            return true;  // reminder sent
          }
          break;
       case 3: // remind function group 2 F5-F8
          if (flags & FN_GROUP_2) {
  	        setFunctionInternal(loco,2,0, 176 | ((functions>>5)& 0x0F));      // 1011 DDDD
            return true;  // reminder sent
          }
          break;
       case 5: // remind function group 3 F9-F12
          if (flags & FN_GROUP_3) {
	          setFunctionInternal(loco,3,0, 160 | ((functions>>9)& 0x0F));    // 1010 DDDD
            return true;  // reminder sent
          }
          break;
       case 7: // remind function group 4 F13-F20
          if (flags & FN_GROUP_4) {
	          setFunctionInternal(loco,4,222, ((functions>>13)& 0xFF));
            return true; 
          }
          break;
       case 9: // remind function group 5 F21-F28
          if (flags & FN_GROUP_5) {
	          setFunctionInternal(loco,5,223, ((functions>>21)& 0xFF));
            return true;  // reminder sent
          }
          break;
      }
      return false; // no reminder sent 
    }




///// Private helper functions below here /////////////////////

byte DCC::cv1(byte opcode, int cv)  {
  cv--;
  return (highByte(cv) & (byte)0x03) | opcode;
}
byte DCC::cv2(int cv)  {
  cv--;
  return lowByte(cv);
}


bool DCC::setMomentum(int locoId,int16_t accelerating, int16_t decelerating) {
  if (locoId<0) return false;
  if (locoId==0) {
    if (accelerating<0 || decelerating<0) return false;
    defaultMomentumA=accelerating/MOMENTUM_FACTOR;
    defaultMomentumD=decelerating/MOMENTUM_FACTOR;
    return true;
  }
  // -1 is ok and means this loco should use the default.
  if (accelerating<-1 || decelerating<-1) return false;
  if (accelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT || 
      decelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT) return false;
  
  // Values stored are 255=MOMENTUM_USE_DEFAULT, or millis/MOMENTUM_FACTOR.
  // This is to keep the values in a byte rather than int16
  // thus saving 2 bytes RAM per loco slot.   
  auto slot=LocoSlot::getSlot(locoId,true);
  if (!slot) return false; // speed table full, can not do anything
  
  slot->setMomentumA((accelerating<0)? MOMENTUM_USE_DEFAULT: (accelerating/MOMENTUM_FACTOR));
  slot->setMomentumD((decelerating<0)? MOMENTUM_USE_DEFAULT: (decelerating/MOMENTUM_FACTOR));
  return true; 
}

// ESTOP functions:
// estopAll() - estop all locos (broadcast dcc) and set their reminders
//              to speed 0 with their current direction preserved.
//              Throttles will be told, but can immediately start driving again.
// estopLock(true) - estop all locos (broadcast dcc) and lock the estop state. 
//              While locked, no loco can be moved because any throttle packets will
//              be blocked at the DCC/DC level and replaced by the estop broadcast packet.
//              Reminders are unchanged so retain the original speed/direction ready for the restart.
//              Throttles will show original speed and can be changed to affect the speed that
//              will be used when the estop is unlocked. 
// estopLock(false) - unlocks the estop state and all locos will resume their 
//              previous (or changed since locking) speeds as the reminder loop continues.

void  DCC::estopAll() {
  setThrottleDCC(nullptr,1); // estop all locos
  TrackManager::setDCSignal(0,1); 
    
  // remind stop/estop but dont change direction
  for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext()) {
    byte newspeed=(slot->getTargetSpeed() & 0x80) |  0x01;
    slot->setSpeedCode(newspeed);
    slot->setTargetSpeed(newspeed);
    CommandDistributor::broadcastLoco(slot);  
  }
}

bool DCC::estopIsLocked=false;

bool DCC::isEstopLocked() {
  return estopIsLocked;
}

void DCC::estopLock( bool lock) {
  // see notes above about estop functions. 
  if (estopIsLocked==lock) return; // no change
  estopIsLocked=lock;
  if (lock) {
    setThrottleDCC(nullptr, 1); // broadcast estop to DCC
    TrackManager::setDCSignal(0, 1); // stop DC signal on all tracks
  }
  CommandDistributor::broadcastEstopLock(estopIsLocked); // tell throttle users
}


LocoSlot  *  DCC::nextLocoReminder = nullptr;


void DCC::displayCabList(Print * stream) {
    LocoSlot::dumpTable(stream);
     StringFormatter::send(stream,F("<* Default momentum=%d/%d *>\n"),
              DCC::defaultMomentumA,DCC::defaultMomentumD);
}


================================================
FILE: DCC.h
================================================
/*
 *  © 2021 Mike S
 *  © 2021 Fred Decker
 *  © 2021 Herb Morton
 *  © 2020-2021 Harald Barth
 *  © 2020-2025 Chris Harlow
 *  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 <https://www.gnu.org/licenses/>.
 */
#ifndef DCC_h
#define DCC_h
#include <Arduino.h>
#include "MotorDriver.h"
#include "MotorDrivers.h"
#include "FSH.h"

#include "defines.h"
#ifndef HIGHEST_SHORT_ADDR
#define HIGHEST_SHORT_ADDR 127
#else
#if HIGHEST_SHORT_ADDR > 127
#error short addr greater than 127 does not make sense
#endif
#endif
#include "DCCACK.h"
#include "LocoSlot.h"
const uint16_t LONG_ADDR_MARKER = 0x4000;

class DCC
{
public:
  static inline void setShieldName(const FSH * motorShieldName) {
    shieldName=(FSH *)motorShieldName;
  };
  static void begin();
  static void loop();

  // Public DCC API functions
  static bool setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
  static void estopAll();
  static void estopLock(bool lock);
  static bool isEstopLocked();

  static int8_t getThrottleSpeed(int cab);
  static uint8_t getThrottleSpeedByte(int cab);
  static uint8_t getLocoSpeedByte(int cab); // may lag throttle 
  static uint8_t getThrottleFrequency(int cab);
  static bool getThrottleDirection(int cab);
  static void writeCVByteMain(int cab, int cv, byte bValue);
  static void readCVByteMain(int cab, int cv, ACK_CALLBACK callback);
  
  static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
  static void setFunction(int cab, byte fByte, byte eByte);
  static bool setFn(int cab, int16_t functionNumber, bool on);
  static void changeFn(int cab, int16_t functionNumber);
  static int8_t getFn(int cab, int16_t functionNumber);
  static uint32_t getFunctionMap(int cab);
  static void setDCFreq(int cab,byte freq);
  static void updateGroupflags(LocoSlot * slot , int16_t functionNumber);
  static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
  static bool setExtendedAccessory(int16_t address, int16_t value, byte repeats=3);
  static bool writeTextPacket(byte *b, int nBytes);
  
  // ACKable progtrack calls  bitresults callback 0,0 or -1, cv returns value or -1
  static void readCV(int16_t cv, ACK_CALLBACK callback);
  static void readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback); // -1 for error
  static void writeCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);
  static void writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
  static void verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);
  static void verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
  static bool setTime(uint16_t minutes,uint8_t speed, bool suddenChange);
  static void getDriveawayLocoId(ACK_CALLBACK callback);
  static void getLocoId(ACK_CALLBACK callback);
  static void getConsistId(ACK_CALLBACK callback);
  static void setLocoId(int id,ACK_CALLBACK callback);
  static void setConsistId(int id,bool reverse,ACK_CALLBACK callback);
  // Enhanced API functions
  static void forgetLoco(int cab); // removes any speed reminders for this loco
  static void forgetAllLocos();    // removes all speed reminders
  static void saveSpeed(int cab);   // saves speed for later restore
  static void restoreSpeed(int cab); // restores saved speed  
  static void displayCabList(Print *stream);
  static FSH *getMotorShieldName();
  static inline void setGlobalSpeedsteps(byte s) {
    globalSpeedsteps = s;
  };
  
  static inline byte getGlobalSpeedsteps() {
    return globalSpeedsteps;
  };

 static const int16_t MOMENTUM_FACTOR=7;  
 
 static bool linearAcceleration;  
 
 static byte cv1(byte opcode, int cv);
 static byte cv2(int cv);
 static bool setMomentum(int locoId,int16_t accelerating, int16_t decelerating);
 static byte getMomentum(LocoSlot * slot);
private:
  static byte loopStatus;
  static byte defaultMomentumA;  // Accelerating
  static byte defaultMomentumD;  // Accelerating
  static void setThrottle2(LocoSlot * slot, uint8_t speedCode);
  static void setThrottleDCC(LocoSlot * slot, uint8_t speedCode);
  static void setFunctionInternal(int cab, byte group, byte fByte, byte eByte);
  static bool issueReminder(LocoSlot  * slot);
  static LocoSlot * nextLocoReminder;
  static FSH *shieldName;
  static byte globalSpeedsteps;
  static bool estopIsLocked;
  static void issueReminders();
  static void callback(int value);

  
  // NMRA codes #
  static const byte SET_SPEED = 0x3f;
  static const byte WRITE_BYTE_MAIN = 0xEC;
  static const byte READ_BYTE_MAIN = 0xE4;
  static const byte WRITE_BIT_MAIN = 0xE8;
  static const byte WRITE_BYTE = 0x7C;
  static const byte VERIFY_BYTE = 0x74;
  static const byte BIT_MANIPULATE = 0x78;
  static const byte WRITE_BIT = 0xF0;
  static const byte VERIFY_BIT = 0xE0;
  static const byte BIT_ON = 0x08;
  static const byte BIT_OFF = 0x00;
};

#endif


================================================
FILE: DCCACK.cpp
================================================
/*
 *  © 2021 M Steve Todd
 *  © 2021 Mike S
 *  © 2021 Fred Decker
 *  © 2020-2025 Harald Barth
 *  © 2020-2022 Chris Harlow
 *  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/>.
 */
#include "DCCACK.h"
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "TrackManager.h"

unsigned long DCCACK::minAckPulseDuration = 2000; // micros
unsigned long DCCACK::maxAckPulseDuration = 20000; // micros
  
MotorDriver *  DCCACK::progDriver=NULL;
ackOp  const *  DCCACK::ackManagerProg;
ackOp  const *  DCCACK::ackManagerProgStart;
byte   DCCACK::ackManagerByte;
byte   DCCACK::ackManagerByteVerify;
byte   DCCACK::ackManagerStash;
int    DCCACK::ackManagerWord;
byte   DCCACK::ackManagerRetry;
byte   DCCACK::ackRetry = 2;
int16_t  DCCACK::ackRetrySum;
int16_t  DCCACK::ackRetryPSum;
int    DCCACK::ackManagerCv;
byte   DCCACK::ackManagerBitNum;
bool   DCCACK::ackReceived;
bool   DCCACK::ackManagerRejoin;
volatile uint8_t DCCACK::numAckGaps=0;
volatile uint8_t DCCACK::numAckSamples=0;
uint8_t DCCACK::trailingEdgeCounter=0;


unsigned long DCCACK::ackPulseDuration;  // micros
unsigned long DCCACK::ackPulseStart; // micros
 volatile bool DCCACK::ackDetected;
 unsigned long DCCACK::ackCheckStart; // millis
 volatile bool DCCACK::ackPending;
  bool   DCCACK::autoPowerOff;
   int  DCCACK::ackThreshold; 
   int  DCCACK::ackLimitmA = 50;
     int DCCACK::ackMaxCurrent;
      unsigned int DCCACK::ackCheckDuration; // millis       
    
    
CALLBACK_STATE DCCACK::callbackState=READY;

ACK_CALLBACK DCCACK::ackManagerCallback;

void  DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
  // On ESP32 the joined track is hidden from sight (it has type MAIN)
  // and because of that we need first check if track was joined and
  // then unjoin if necessary. This requires that the joined flag is
  // cleared when the prog track is removed.
  ackManagerRejoin=TrackManager::isJoined();
  //DIAG(F("Joined is %d"), ackManagerRejoin);
  if (ackManagerRejoin) {
    // Change from JOIN must zero resets packet.
    TrackManager::setJoin(false);
    DCCWaveform::progTrack.clearResets();
  }
  progDriver=TrackManager::getProgDriver();
  //DIAG(F("Progdriver is %d"), progDriver);
  if (progDriver==NULL) {
    if (ackManagerRejoin) {
      DIAG(F("Joined but no Prog track"));
      TrackManager::setJoin(false);
    }
    callback(-3); // we dont have a prog track!
    return;
  }
  if (!progDriver->canMeasureCurrent()) {
    TrackManager::setJoin(ackManagerRejoin);
    callback(-2); // our prog track cant measure current
    return;
  }

   autoPowerOff=false;
   if (progDriver->getPower() == POWERMODE::OFF) {
        autoPowerOff=true;  // power off afterwards
        if (Diag::ACK) DIAG(F("Auto Prog power on"));
        progDriver->setPower(POWERMODE::ON);
	
    /* TODO !!! in MotorDriver surely!
    if (MotorDriver::commonFaultPin)
	  DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
        DCCWaveform::progTrack.clearResets();
   **/
      }


  ackManagerCv = cv;
  ackManagerProg = program;
  ackManagerProgStart = program;
  ackManagerRetry = ackRetry;
  ackManagerByte = byteValueOrBitnum;
  ackManagerByteVerify = byteValueOrBitnum;
  ackManagerBitNum=byteValueOrBitnum;
  ackManagerCallback = callback;
}

void  DCCACK::Setup(int wordval, ackOp const program[], ACK_CALLBACK callback) {
  ackManagerWord=wordval;
  Setup(0, 0, program, callback);
  }

const byte RESET_MIN=8;  // tuning of reset counter before sending message

// checkRessets return true if the caller should yield back to loop and try later.
bool DCCACK::checkResets(uint8_t numResets) {
  return DCCWaveform::progTrack.getResets() < numResets;
}
// Operations applicable to PROG track ONLY.
// (yes I know I could have subclassed the main track but...) 

void DCCACK::setAckBaseline() {
      int baseline=progDriver->getCurrentRaw();
      ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);
      if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %lus and %lus"),
			  baseline,progDriver->raw2mA(baseline),
			  ackThreshold,progDriver->raw2mA(ackThreshold),
                          minAckPulseDuration, maxAckPulseDuration);
}

void DCCACK::setAckPending() {
      ackMaxCurrent=0;
      ackPulseStart=0;
      ackPulseDuration=0;
      ackDetected=false;
      ackCheckStart=millis();
      numAckSamples=0;
      numAckGaps=0;
      ackPending=true;  // interrupt routines will now take note
}

byte DCCACK::getAck() {
      if (ackPending) return (2);  // still waiting
      if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%luS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
			  ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
      if (ackDetected) return (1); // Yes we had an ack
      return(0);  // pending set off but not detected means no ACK.   
}

#ifndef DISABLE_PROG
void DCCACK::loop() {
  while (ackManagerProg) {
    byte opcode=GETFLASH(ackManagerProg);

    // breaks from this switch will step to next prog entry
    // returns from this switch will stay on same entry
    // (typically waiting for a reset counter or ACK waiting, or when all finished.)
    switch (opcode) {
      case BASELINE:
          if (progDriver->getPower()==POWERMODE::OVERLOAD) return;
      	  if (checkResets(autoPowerOff || ackManagerRejoin  ? 20 : 3)) return;
          setAckBaseline();
          callbackState=AFTER_READ;
          break;
      case W0:    // write 0 bit
      case W1:    // write 1 bit
            {
	      if (checkResets(RESET_MIN)) return;
              if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
              byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;
              byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
              DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
              setAckPending();
             callbackState=AFTER_WRITE;
         }
            break;

      case WB:   // write byte
            {
	      if (checkResets( RESET_MIN)) return;
              if (Diag::ACK) DIAG(F("WB cv=%d value=%d"),ackManagerCv,ackManagerByte);
              byte message[] = {DCC::cv1(WRITE_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
              DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
              setAckPending();
              callbackState=AFTER_WRITE;
            }
            break;

      case   VB:     // Issue validate Byte packet
        {
	  if (checkResets( RESET_MIN)) return;
          if (Diag::ACK) DIAG(F("VB cv=%d value=%d"),ackManagerCv,ackManagerByte);
          byte message[] = { DCC::cv1(VERIFY_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
          DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
          setAckPending();
        }
        break;

      case V0:
      case V1:      // Issue validate bit=0 or bit=1  packet
        {
	  if (checkResets(RESET_MIN)) return;
          if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
          byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;
          byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
          DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
          setAckPending();
        }
        break;

      case WACK:   // wait for ack (or absence of ack)
         {
          byte ackState=2; // keep polling

          ackState=getAck();
          if (ackState==2) return; // keep polling
          ackReceived=ackState==1;
          break;  // we have a genuine ACK result
         }
     case ITC0:
     case ITC1:   // If True Callback(0 or 1)  (if prevous WACK got an ACK)
        if (ackReceived) {
            callback(opcode==ITC0?0:1);
            return;
          }
        break;

      case ITCB:   // If True callback(byte)
          if (ackReceived) {
            callback(ackManagerByte);
            return;
          }
        break;
		
      case ITCBV:   // If True callback(byte) - Verify
          if (ackReceived) {
            if (ackManagerByte == ackManagerByteVerify) {
               ackRetrySum ++;
               LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum);
            }
            callback(ackManagerByte);
            return;
          }
        break;
		
      case ITCB7:   // If True callback(byte & 0x7F)
          if (ackReceived) {
            callback(ackManagerByte & 0x7F);
            return;
          }
        break;

      case NAKFAIL:   // If nack callback(-1)
          if (!ackReceived) {
            callback(-1);
            return;
          }
        break;

      case CALLFAIL:  // callback(-1)
           callback(-1);
           return;

      case BIV:     // ackManagerByte initial value
           ackManagerByte = ackManagerByteVerify;
           break;

      case STARTMERGE:
           ackManagerBitNum=7;
           ackManagerByte=0;
          break;

      case MERGE:  // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)
          ackManagerByte <<= 1;
          // ackReceived means bit is zero.
          if (!ackReceived) ackManagerByte |= 1;
          ackManagerBitNum--;
          break;

      case SETBIT:
          ackManagerProg++;
          ackManagerBitNum=GETFLASH(ackManagerProg);
          break;

     case SETCV:
          ackManagerProg++;
          ackManagerCv=GETFLASH(ackManagerProg);
          break;

     case SETBYTE:
          ackManagerProg++;
          ackManagerByte=GETFLASH(ackManagerProg);
          break;

    case SETBYTEH:
          ackManagerByte=highByte(ackManagerWord);
          break;

    case SETBYTEL:
          ackManagerByte=lowByte(ackManagerWord);
          break;

     case STASHLOCOID:
          ackManagerStash=ackManagerByte;  // stash value from CV17
          break;

     case COMBINELOCOID:
          // ackManagerStash is  cv17, ackManagerByte is CV 18
          callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
          return;

     case COMBINE1920:
          // ackManagerStash is  cv20, ackManagerByte is CV 19
          // This will not be called if cv20==0
          ackManagerByte &= 0x7F;  // ignore direction marker
          ackManagerByte %=100;    // take last 2 decimal digits 
          callback( ackManagerStash*100+ackManagerByte);
          return;

     case ITSKIP:
          if (!ackReceived) break;
          // SKIP opcodes until SKIPTARGET found
          while (opcode!=SKIPTARGET) {
            ackManagerProg++;
            opcode=GETFLASH(ackManagerProg);
          }
          break;

     case NAKSKIP:
          if (ackReceived) break;
          // SKIP opcodes until SKIPTARGET found
          while (opcode!=SKIPTARGET) {
            ackManagerProg++;
            opcode=GETFLASH(ackManagerProg);
          }
          break;
     case BAD20SKIP:
          if (ackManagerByte > 120) {
            // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
            if (Diag::ACK) DIAG(F("XX cv20=%d "),ackManagerByte);
            while (opcode!=SKIPTARGET) {
              ackManagerProg++;
              opcode=GETFLASH(ackManagerProg);
            }
          }
          break; 
     case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it
          if (ackManagerByte==0) break;
          callback(-4);  
          return;           
     case SKIPTARGET:
          break;
     default:
          DIAG(F("ackOp %d FAULT"),opcode);
          callback( -1);
          return;

      }  // end of switch
    ackManagerProg++;
  }
}

void DCCACK::callback(int value) {
    // check for automatic retry
    if (value == -1 && ackManagerRetry > 0) {
      ackRetrySum ++;
      LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum);
      ackManagerRetry --;
      ackManagerProg = ackManagerProgStart;
      return;
    }

    static unsigned long callbackStart;
    // We are about to leave programming mode
    // Rule 1: If we have written to a decoder we must maintain power for 100mS
    // Rule 2: If we are re-joining the main track we must power off for 30mS

    const FSH *off30ms = F("OFF 30mS");
    switch (callbackState) {
      case AFTER_READ:
         if (ackManagerRejoin && !autoPowerOff) {
                progDriver->setPower(POWERMODE::OFF);
                callbackStart=millis();
                callbackState=WAITING_30;
                if (Diag::ACK) DIAG(off30ms);
        } else {
               callbackState=READY;
        }
        break;

      case AFTER_WRITE:  // first attempt to callback after a write operation
	    if (!ackManagerRejoin && !autoPowerOff) {
               callbackState=READY;
               break;
            }                              // lines 906-910 added. avoid wait after write. use 1 PROG
            callbackStart=millis();
            callbackState=WAITING_100;
            if (Diag::ACK) DIAG(F("Stable 100mS"));
            break;

       case WAITING_100:  // waiting for 100mS
            if (millis()-callbackStart < 100) break;
            // stable after power maintained for 100mS

            // If we are going to power off anyway, it doesnt matter
            // but if we will keep the power on, we must off it for 30mS
            if (autoPowerOff) callbackState=READY;
            else { // Need to cycle power off and on
                progDriver->setPower(POWERMODE::OFF);
                callbackStart=millis();
                callbackState=WAITING_30;
                if (Diag::ACK) DIAG(off30ms);
            }
            break;

        case WAITING_30:  // waiting for 30mS with power off
            if (millis()-callbackStart < 30) break;
            //power has been off for 30mS
            progDriver->setPower(POWERMODE::ON);
            callbackState=READY;
            break;

       case READY:  // ready after read, or write after power delay and off period.
            // power off if we powered it on
           if (autoPowerOff) {
              if (Diag::ACK) DIAG(F("Auto Prog power off"));
              progDriver->setPower(POWERMODE::OFF);
              /* TODO 
	      if (MotorDriver::commonFaultPin)
		DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
        **/
           }
          // Restore <1 JOIN> to state before BASELINE
          if (ackManagerRejoin) {
              TrackManager::setJoin(true);
              if (Diag::ACK) DIAG(F("Auto JOIN"));
          }

          ackManagerProg=NULL;  // no more steps to execute
          if (Diag::ACK) DIAG(F("Callback(%d)"),value);
          (ackManagerCallback)( value);
    }
}
#endif

void DCCACK::checkAck(byte sentResetsSincePacket) {
    if (!ackPending) return; 
    // This function operates in interrupt() time so must be fast and can't DIAG 
    if (sentResetsSincePacket > 6) {  //ACK timeout
        ackCheckDuration=millis()-ackCheckStart;
        ackPending = false;
        return; 
    }
      
    int current=progDriver->getCurrentRaw(true); // true means "from interrupt"
    numAckSamples++;
    if (current > ackMaxCurrent) ackMaxCurrent=current;
    // An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
        
    if (current>ackThreshold) {
       if (trailingEdgeCounter > 0) {
	 numAckGaps++;
	 trailingEdgeCounter = 0;
       }
       if (ackPulseStart==0) ackPulseStart=micros();    // leading edge of pulse detected
       return;
    }
    
    // not in pulse
    if (ackPulseStart==0) return; // keep waiting for leading edge 
    
    // if we reach to this point, we have
    // detected trailing edge of pulse
    if (trailingEdgeCounter == 0) {
      ackPulseDuration=micros()-ackPulseStart;
    }

    // but we do not trust it yet and return (which will force another
    // measurement) and first the third time around with low current
    // the ack detection will be finalized. 
    if (trailingEdgeCounter < 2) {
      trailingEdgeCounter++;
      return;
    }
    trailingEdgeCounter = 0;

    if (ackPulseDuration>=minAckPulseDuration
	&& (ackPulseDuration<=maxAckPulseDuration || maxAckPulseDuration == 0)) {
        ackCheckDuration=millis()-ackCheckStart;
        ackDetected=true;
        ackPending=false;
        DCCWaveform::progTrack.clearRepeats();  // shortcut remaining repeat packets 
        return;  // we have a genuine ACK result
    }      
    ackPulseStart=0;  // We have detected a too-short or too-long pulse so ignore and wait for next leading edge 
}


================================================
FILE: DCCACK.h
================================================
/*
 *  © 2021 M Steve Todd
 *  © 2021 Mike S
 *  © 2021 Fred Decker
 *  © 2020-2021 Harald Barth
 *  © 2020-2022 Chris Harlow
 *  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/>.
 */
#ifndef DCCACK_h
#define DCCACK_h

#include "MotorDriver.h"

typedef void (*ACK_CALLBACK)(int16_t result);

enum ackOp : byte
{           // Program opcodes for the ack Manager
  BASELINE, // ensure enough resets sent before starting and obtain baseline current
  W0,
  W1,               // issue write bit (0..1) packet
  WB,               // issue write byte packet
  VB,               // Issue validate Byte packet
  V0,               // Issue validate bit=0 packet
  V1,               // issue validate bit=1 packlet
  WACK,             // wait for ack (or absence of ack)
  ITC1,             // If True Callback(1)  (if prevous WACK got an ACK)
  ITC0,             // If True callback(0);
  ITCB,             // If True callback(byte)
  ITCBV,            // If True callback(byte) - end of Verify Byte
  ITCB7,            // If True callback(byte &0x7F)
  NAKFAIL,          // if false callback(-1)
  CALLFAIL,         // callback(-1)
  BIV,              // Set ackManagerByte to initial value for Verify retry
  STARTMERGE,       // Clear bit and byte settings ready for merge pass
  MERGE,            // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)
  SETBIT,           // sets bit number to next prog byte
  SETCV,            // sets cv number to next prog byte
  SETBYTE,          // sets current byte to next prog byte
  SETBYTEH,         // sets current byte to word high byte
  SETBYTEL,         // sets current byte to word low byte
  STASHLOCOID,      // keeps current byte value for later
  COMBINELOCOID,    // combines current value with stashed value and returns it
  ITSKIP,           // skip to SKIPTARGET if ack true
  NAKSKIP,          // skip to SKIPTARGET if ack false
  COMBINE1920,      // combine cvs 19 and 20 and callback
  BAD20SKIP,        // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255) 
  FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
  SKIPTARGET = 0xFF // jump to target
};

enum   CALLBACK_STATE : byte {

  AFTER_READ,   // Start callback sequence after something was read from the decoder  
  AFTER_WRITE,  // Start callback sequence after something was written to the decoder  
  WAITING_100,        // Waiting for 100mS of stable power 
  WAITING_30,         // waiting to 30ms of power off gap. 
  READY,              // Ready to complete callback  
  }; 



class DCCACK {
  public:
    static byte getAck();               //prog track only 0=NACK, 1=ACK 2=keep waiting
    static void checkAck(byte sentResetsSincePacket); // Interrupt time ack checker
    static inline void setAckLimit(int mA) {
	ackLimitmA = mA;
    }
    static inline void setMinAckPulseDuration(unsigned long i) {
	minAckPulseDuration = i;
    }
    static inline void setMaxAckPulseDuration(unsigned long i) {
	maxAckPulseDuration = i;
    }

    static void  Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback);
    static void  Setup(int wordval, ackOp const program[], ACK_CALLBACK callback);
    static void loop();
    static bool isActive() { return ackManagerProg!=NULL;}
  static inline int16_t setAckRetry(byte retry) {
    ackRetry = retry;
    ackRetryPSum = ackRetrySum;
    ackRetrySum = 0;  // reset running total
    return ackRetryPSum;
  };


  private:
    static const byte SET_SPEED = 0x3f;
    static const byte WRITE_BYTE = 0x7C;
    static const byte VERIFY_BYTE = 0x74;
    static const byte BIT_MANIPULATE = 0x78;
    static const byte WRITE_BIT = 0xF0;
    static const byte VERIFY_BIT = 0xE0;
    static const byte BIT_ON = 0x08;
    static const byte BIT_OFF = 0x00;
 
    static void setAckBaseline();
    static void setAckPending();  
    static void callback(int value);
    
    static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)
    
    // ACK management (Prog track only)  
    static void checkAck();
    static bool checkResets(uint8_t numResets);

    static volatile bool ackPending;
    static volatile bool ackDetected;
    static int  ackThreshold; 
    static int  ackLimitmA;
    static int ackMaxCurrent;
    static unsigned long ackCheckStart; // millis
    static unsigned int ackCheckDuration; // millis       
    
    static unsigned long ackPulseDuration;  // micros
    static unsigned long ackPulseStart; // micros

    static unsigned long minAckPulseDuration ; // micros
    static unsigned long maxAckPulseDuration ; // micros
    static MotorDriver* progDriver;
    static volatile uint8_t numAckGaps;
    static volatile uint8_t numAckSamples;
    static uint8_t trailingEdgeCounter;
    static ackOp  const *  ackManagerProg;
static ackOp  const *  ackManagerProgStart;
static byte   ackManagerByte;
static byte   ackManagerByteVerify;
static byte   ackManagerStash;
static int    ackManagerWord;
static byte   ackManagerRetry;
static byte   ackRetry;
static int16_t ackRetrySum;
static int16_t  ackRetryPSum;
static int    ackManagerCv;
static byte   ackManagerBitNum;
static bool   ackReceived;
static bool   ackManagerRejoin;
static bool   autoPowerOff;
static CALLBACK_STATE callbackState;
static ACK_CALLBACK ackManagerCallback;


};
#endif


================================================
FILE: DCCConsist.cpp
================================================
#include "DCCConsist.h"
#include "LocoSlot.h"
#include "StringFormatter.h"
#include "DIAG.h"

bool DCCConsist::parse(Print * stream, byte params, int16_t p[]) {
    if (params==0) {
          for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext()) {
            if (slot->isConsistLead()) {
              StringFormatter::send(stream,F("<^"));
              for (auto cslot=slot;cslot;cslot=cslot->getConsistNext()) {
                StringFormatter::send(stream,
                    cslot->isConsistReverse() ? F(" -%d") : F(" %d"),
                    cslot->getLoco());
              }                 
              StringFormatter::send(stream,F(" >\n"));
            }
          }
        return true;
    }
    // Detect any invalid locoids or duplicates 
    for (byte i=0;i<params;i++) {
        uint16_t locoId=abs(p[i]);
        if (locoId<1 || locoId>10239) {
            StringFormatter::send(stream,F("<* Invalid locoid %d *>"),p[i]);
            return false;
        }
        for (byte j=i+1;j<params;j++) {
            if (abs(p[j])==locoId) {
                StringFormatter::send(stream,F("<* Duplicate locoid %d *>"),p[i]);
                return false;
            }
        }
    }
    // Cross check other consists  
    // if p[0] is a consist lead, kill its consist first
    auto slot=LocoSlot::getSlot(abs(p[0]),false);
    if (slot && slot->isConsistLead()) {
        DCCConsist::deleteAnyConsist(p[0]);
    }
    if (params<2) return true; // we only had to delete 
     
    for (byte i=0;i<params;i++) {
        auto slot=LocoSlot::getSlot(abs(p[i]),false);
        if (slot && (slot->isConsistLead() || slot->isConsistFollower())) {
            StringFormatter::send(stream,F("<* Loco %d in another consist *>"),abs(p[i]));
            return false;
        }
    }
    
    auto leadLoco=LocoSlot::getSlot(abs(p[0]),true);
    leadLoco->setConsistReverse(p[0]<0);
    LocoSlot *  prev=leadLoco;
    for (byte i=1;i<params;i++) {
        auto slot=LocoSlot::getSlot(abs(p[i]),true);
        slot->setConsistLead(leadLoco);
        slot->setConsistReverse(p[i]<0);
        prev->setConsistNext(slot);        
        prev=slot;
    }
    return true;
}

void DCCConsist::deleteAnyConsist(int16_t locoid) {
    locoid=abs(locoid); // in case of (valid) negative
    auto slot=LocoSlot::getSlot(locoid,false);
    if (!slot) return; // no such loco, nothing to do
    if (!slot->isConsistLead()) slot=slot->getConsistLead();
    if (!slot) return; // not in consist, nothing to do

    while(slot) {
      auto next=slot->getConsistNext();
      slot->setConsistLead(nullptr);
      slot->setConsistNext(nullptr);
      slot->setConsistReverse(false);
      slot=next;
    } 
}

bool DCCConsist::addLocoToConsist(uint16_t consistId,uint16_t locoId, bool revesed) {
    if (consistId<1 || consistId>10239) return false;
    if (locoId<1 || locoId>10239) return false;
    if (locoId==consistId) return true; // cant add lead to itself
    
    auto leadSlot=LocoSlot::getSlot(consistId,true);
    if (!leadSlot) return false; // no ram
    // back up to lead of existing consist
    while(leadSlot->getConsistLead()) {
        leadSlot=leadSlot->getConsistLead();
    }

    // find tail of consist
    auto tailSlot=leadSlot;
    while(tailSlot->getConsistNext()) {
        tailSlot=tailSlot->getConsistNext();
    }

    auto addSlot=LocoSlot::getSlot(locoId,true);
    if (!addSlot) return false; // no ram
    
    if (addSlot->isConsistLead() || addSlot->isConsistFollower()) {
        DIAG(F("Loco %d already in a consist"),locoId);
        return false; // already in consist
    }
    
    // All OK so chain in. (belt and braces prevent self reference loop)
    addSlot->setConsistReverse(revesed);
    if (addSlot!=leadSlot) addSlot->setConsistLead(leadSlot);    
    if (tailSlot!=addSlot) tailSlot->setConsistNext(addSlot);
    return true;
}
 


================================================
FILE: DCCConsist.h
================================================
#ifndef DCCConsist_h
#define DCCConsist_h

#include <Arduino.h>
class DCCConsist {

    public:
      static bool parse(Print * stream, byte paramCount, int16_t p[]);

      // deteles any consist chain containing locoid 
      static void deleteAnyConsist(int16_t locoid);
      
      //add loco anywhere in consist (may create consist) see EXRAIL.
      static bool addLocoToConsist(uint16_t consistId,uint16_t locoid, bool revesed);

};
#endif


================================================
FILE: DCCDecoder.cpp
================================================
/*
 *  © 2025 Harald Barth
 *  
 *  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/>.
 */
#ifdef ARDUINO_ARCH_ESP32
#include "DCCDecoder.h"
#include "LocoSlot.h"
#include "DCCEXParser.h"
#include "DIAG.h"
#include "DCC.h"

bool DCCDecoder::parse(DCCPacket &p) {
  if (!active)
    return false;
  const byte DECODER_MOBILE = 1;
  const byte DECODER_ACCESSORY = 2;
  byte decoderType = 0; // use 0 as none
  byte *d = p.data();
  byte *instr = 0;      // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
  uint16_t addr;        // will be set to decoder addr (long/shor mobile or accessory) 
  bool locoInfoChanged = false;

  if (d[0] ==  0B11111111) {  // Idle packet
    return false;
  }
  // CRC verification here
  byte checksum = 0;
  for (byte n = 0; n < p.len(); n++)
    checksum ^= d[n];
  if (checksum) {  // Result should be zero, if not it's an error!
    if (Diag::SNIFFER) {
      DIAG(F("Checksum error:"));
      p.print();
    }
    return false;
  }

/*
  Serial.print("< ");
  for(int n=0; n<8; n++) {
    Serial.print(d[0]&(1<<n)?"1":"0");
  }
  Serial.println(" >");
*/
  if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
    decoderType = DECODER_MOBILE;
    instr = d+1;
    addr = d[0];
  } else {
    if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
      decoderType = DECODER_MOBILE;
      instr = d+2;
      addr = 256 * (d[0] & 0B00111111) + d[1];
    } else { // bit7 == 1 and bit 6 == 0
      decoderType = DECODER_ACCESSORY;
      instr = d+1;
      addr = d[0] &  0B00111111;
    }
  }
  if (decoderType == DECODER_MOBILE) {
    switch (instr[0] & 0xE0) {
    case 0x20: // 001x-xxxx Extended commands
      if (instr[0] == 0B00111111) { // 128 speed steps
	if ((locoInfoChanged = updateLoco(addr, instr[1])) == true) {
	  byte speed = instr[1] & 0B01111111;
	  byte direction = instr[1] & 0B10000000;
	  DCC::setThrottle(addr, speed, direction);
	  //DIAG(F("UPDATE"));
	  // send speed change to DCCEX here
	}
      }
      break;
    case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
    case 0x60: // 011x-xxxx
      if ((locoInfoChanged = updateLoco(addr, instr[0] & 0B00111111)) == true) {
	byte speed = instr[0] & 0B00001111; // first only look at 4 bits
	if (speed > 1) {               // neither stop nor emergency stop, recalculate speed
	  speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
	  speed = (speed - 3) * 9/2;
	}
	byte direction = instr[0] & 0B00100000;
	DCC::setThrottle(addr, speed, direction);
      }
      break;
    case 0x80: // 100x-xxxx Function group 1
      if ((locoInfoChanged = updateFunc(addr, instr[0], 1)) == true) {
	byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
	DCCEXParser::funcmap(addr, normalized, 0, 4);
      }
      break;
    case 0xA0: // 101x-xxxx Function group 3 and 2
    {
      byte low, high;
      if (bitRead(instr[0], 4)) {
	low = 5;
	high = 8;
      } else {
	low = 9;
	high = 12;
      }
      if ((locoInfoChanged = updateFunc(addr, instr[0], low)) == true) {
	DCCEXParser::funcmap(addr, instr[0], low, high);
      }
    }
    break;
    case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
      switch (instr[0] & 0B00011111) {
      case 0B00011110:  // F13-F20 Function Control
	if ((locoInfoChanged = updateFunc(addr, instr[0], 13)) == true) {
	  DCCEXParser::funcmap(addr, instr[1], 13, 20);
	}
	if ((locoInfoChanged = updateFunc(addr, instr[0], 17)) == true) {
	  DCCEXParser::funcmap(addr, instr[1], 13, 20);
	}
      break;
      case 0B00011111:  // F21-F28 Function Control
	if ((locoInfoChanged = updateFunc(addr, instr[1], 21)) == true) {
	  DCCEXParser::funcmap(addr, instr[1], 21, 28);
	}  // updateFunc handles only the 4 low bits as that is the most common case
	if ((locoInfoChanged = updateFunc(addr, instr[1]>>4, 25)) == true) {
	  DCCEXParser::funcmap(addr, instr[1], 21, 28);
	}
	break;
	/* do that later
      case 0B00011000:  // F29-F36 Function Control
	break;
      case 0B00011001:  // F37-F44 Function Control
	break;
      case 0B00011010:  // F45-F52 Function Control
	break;
      case 0B00011011:  // F53-F60 Function Control
	break;
      case 0B00011100:  // F61-F68 Function Control
	break;
	*/
      }
      break;
    case 0xE0: // 111x-xxxx Config vars
      break;
    }
    return locoInfoChanged;
  }
  if (decoderType == DECODER_ACCESSORY) {
      if (instr[0] & 0B10000000) {  // Basic Accessory
	addr = (((~instr[0]) & 0B01110000) << 2) + addr;
	byte port = (instr[0] & 0B00000110) >> 1;
	byte activate = (instr[0] & 0B00001000) >> 3;
	byte coil = (instr[0] & 0B00000001);
	locoInfoChanged = true;
	//(void)addr; (void)port; (void)coil; (void)activate;
	//DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
	DCC::setAccessory(addr, port, coil, activate);
      } else { // Accessory Extended NMRA spec, do we need to decode this?
	/*
	addr = (addr << 5) +
	  ((instr[0] & 0B01110000) >> 2) +
	  ((instr[0] & 0B00000110) >> 1);
	*/
      }
    return locoInfoChanged;
  }
  return false;
}

// returns false only if loco existed but nothing was changed
bool DCCDecoder::updateLoco(uint16_t loco, byte speedCode) {
  if (loco==0) return true; // its a broadcast
  
  // determine speed reg for this loco
  auto slot=LocoSlot::getSlot(loco, true);
  if (speedCode == slot->getSnifferSpeedCode()) {
    return false; // nothing changed
  }

  slot->setSnifferSpeedCode(speedCode);
  return true; // loco speed changed
  
}

bool DCCDecoder::updateFunc(uint16_t loco, byte func, int shift) {
  unsigned long previous;
  unsigned long newfunc;
  auto slot= LocoSlot::getSlot(loco, true);
  previous=slot->getSnifferFunctions();
  newfunc=previous;
  if(shift == 1) { // special case for light
    newfunc &= ~1UL;
    newfunc |= ((func & 0B10000) >> 4);
  }
  newfunc &= ~(0B1111UL << shift);
  newfunc |=  ((func & 0B1111) << shift);

  if (newfunc == previous) return false; // nothing changed
  slot->setSnifferFunctions(newfunc);
  return true;
}

#endif // ARDUINO_ARCH_ESP32


================================================
FILE: DCCDecoder.h
================================================
/*
 *  © 2025 Harald Barth
 *  
 *  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/>.
 */
#ifdef ARDUINO_ARCH_ESP32
#include <Arduino.h>
#include "DCCPacket.h"

class DCCDecoder {
public:
  static bool parse(DCCPacket &p);
  static inline void onoff(bool on) {active = on;};
private:
  static bool active;
  static bool updateLoco(uint16_t loco, byte speedCode);
  static bool updateFunc(uint16_t loco, byte func, int shift);
};
#endif // ARDUINO_ARCH_ESP32


================================================
FILE: DCCEX.h
================================================
/*
 *  © 2021 Fred Decker
 *  © 2020-2021 Harald Barth
 *  © 2020-2021 Chris Harlow
 *  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/>.
 */

// This include is intended to visually simplify the .ino for the end users.
// If there were any #ifdefs required they are much better handled in here.  

#ifndef DCCEX_h
#define DCCEX_h

#include "defines.h"
#include "DCC.h"
#include "DIAG.h"
#include "DCCEXParser.h"
#include "SerialManager.h"
#include "version.h"
#ifndef ARDUINO_ARCH_ESP32
#include "WifiInterface.h"
#else
#include "WifiESP32.h"
#endif
#if ETHERNET_ON == true
#include "EthernetInterface.h"
#endif
#include "Display_Implementation.h"
#include "LCN.h"
#include "IODevice.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"    
#include "KeywordHasher.h"
#include "EXRAIL.h"
    
#endif


================================================
FILE: DCCEXParser.cpp
================================================
/*
 *  © 2022 Paul M Antoine
 *  © 2021 Neil McKechnie
 *  © 2021 Mike S
 *  © 2021-2025 Herb Morton
 *  © 2020-2025 Harald Barth
 *  © 2020-2021 M Steve Todd
 *  © 2020-2021 Fred Decker
 *  © 2020-2025 Chris Harlow
 *  © 2022 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/>.
 */

/*
List of single character OPCODEs in use for reference.

When determining a new OPCODE for a new feature, refer to this list as the source of truth.

Once a new OPCODE is decided upon, update this list.

  Character, Usage
  /, |EX-R| interactive commands
  -, Remove from reminder table
  =, |TM| configuration
  !, Emergency stop
  @, LCD messages to/from JMRI
  #, Request number of supported cabs/locos; heartbeat
  +, WiFi AT commands
  ?, Reserved for future use
  ^, Consist commands
  0, Track power off
  1, Track power on
  a, DCC accessory control
  A, DCC extended accessory control
  b, Write CV bit on main
  B, Write CV bit
  c, Request current command
  C, configure the CS
  d,
  D, Diagnostic commands
  e, Erase EEPROM
  E, Store configuration in EEPROM
  f, Loco decoder function control (deprecated)
  F, Loco decoder function control
  g,
  G,
  h,
  H, Turnout state broadcast
  i, Server details string
  I, Turntable object command, control, and broadcast
  j, Throttle responses
  J, Throttle queries
  k, Block exit  (Railcom)
  K, Block enter (Railcom)
  l, Loco speedbyte/function map broadcast
  L, Reserved for LCC interface (implemented in EXRAIL)
  m, message to throttles (broadcast output) 
  m, set momentum  
  M, Write DCC packet
  n, Reserved for SensorCam
  N, Reserved for Sensorcam 
  o, Neopixel driver (see also IO_NeoPixel.h)
  O, Output broadcast
  p, Broadcast power state
  P, Write DCC packet
  q, Sensor deactivated
  Q, Sensor activated
  r, Broadcast address read on programming track
  R, Read CVs
  s, Display status
  S, Sensor configuration
  t, Cab/loco update command
  T, Turnout configuration/control
  u, Reserved for user commands
  U, Reserved for user commands
  v,
  V, Verify CVs
  w, Write CV on main
  W, Write CV
  x,
  X, Invalid command response
  y, Output Sound
  Y, Output broadcast
  z, Direct output
  Z, Output configuration/control
*/

#include "StringFormatter.h"
#include "DCCEXParser.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "Turnouts.h"
#include "Outputs.h"
#include "Sensors.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "defines.h"
#include "CommandDistributor.h"
#include "EEStore.h"
#include "DIAG.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "EXRAIL2.h"
#include "Turntables.h"
#include "version.h"
#include "KeywordHasher.h"
#include "CamParser.h"
#include "Stash.h"
#include "DCCConsist.h"
#ifdef ARDUINO_ARCH_ESP32
#include "WifiESP32.h"
#include "DCCDecoder.h"
#endif

// This macro can't be created easily as a portable function because the
// flashlist requires a far pointer for high flash access. 
#define SENDFLASHLIST(stream,flashList)                 \
    for (int16_t i=0;;i+=sizeof(flashList[0])) {                            \
        int16_t value=GETHIGHFLASHW(flashList,i);       \
        if (value==INT16_MAX) break;                            \
        StringFormatter::send(stream,F(" %d"),value);	\
    }                                   

int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy;
Print *DCCEXParser::stashStream = NULL;
RingStream *DCCEXParser::stashRingStream = NULL;
byte DCCEXParser::stashTarget=0;

// This is a JMRI command parser.
// It doesnt know how the string got here, nor how it gets back.
// It knows nothing about hardware or tracks... it just parses strings and
// calls the corresponding DCC api.
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.


int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd, bool usehex)
{
    byte state = 1;
    byte parameterCount = 0;
    int16_t runningValue = 0;
    byte *remainingCmd = cmd + 1; // skips the opcode
    bool signNegative = false;

    // clear all parameters in case not enough found
    for (int16_t i = 0; i < MAX_COMMAND_PARAMS; i++)
        result[i] = 0;

    while (parameterCount < MAX_COMMAND_PARAMS)
    {
        byte hot = *remainingCmd;
        switch (state)
        {

        case 1: // skipping spaces before a param
            if (hot == ' ')
                break;
            if (hot == '\0')
	      return -1;
	    if (hot == '>') {
	      *remainingCmd = '\0';  // terminate the cmd string with 0 instead of '>'
	      return parameterCount;
	    }
            state = 2;
            continue;

        case 2: // checking sign or quoted string
#ifdef HAS_ENOUGH_MEMORY
	    if (hot == '"') {
	      // this inserts an extra parameter 0x7777 in front
	      // of each string parameter as a marker that can
	      // be checked that a string parameter follows
	      // This clashes of course with the real value
	      // 0x7777 which we hope is used seldom
	      result[parameterCount] = (int16_t)0x7777;
	      parameterCount++;
	      result[parameterCount] = (int16_t)(remainingCmd - cmd + 1);
	      parameterCount++;
	      state = 4;
	      break;
	    }
#endif
            signNegative = false;
            runningValue = 0;
            state = 3;
            if (hot != '-')
                continue;
            signNegative = true;
            break;
        case 3: // building a parameter
            if (hot >= '0' && hot <= '9')
            {
                runningValue = (usehex?16:10) * runningValue + (hot - '0');
                break;
            }
            if (hot >= 'a' && hot <= 'z') hot=hot-'a'+'A'; // uppercase a..z
            if (usehex && hot>='A' && hot<='F') {
                // treat A..F as hex not keyword
                runningValue = 16 * runningValue + (hot - 'A' + 10);
                break;
            }
            if (hot=='_' || (hot >= 'A' && hot <= 'Z'))
            {
                // Since JMRI got modified to send keywords in some rare cases, we need this
                // Super Kluge to turn keywords into a hash value that can be recognised later
                runningValue = ((runningValue << 5) + runningValue) ^ hot;
                break;
            }
            result[parameterCount] = runningValue * (signNegative ? -1 : 1);
            parameterCount++;
            state = 1;
            continue;
#ifdef HAS_ENOUGH_MEMORY
	case 4: // skipover text
	  if (hot == '\0')        // We did run to end of buffer without finding the "
	    return -1;
	  if (hot == '"') {
	    *remainingCmd = '\0'; // overwrite " in command buffer with the end-of-string
	    state = 1;
	  }
	  break;
#endif
        }
        remainingCmd++;
    }
    return parameterCount;
}

extern __attribute__((weak))  void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
FILTER_CALLBACK DCCEXParser::filterCamParserCallback = 0;
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;

// deprecated
void DCCEXParser::setFilter(FILTER_CALLBACK filter)
{
    filterCallback = filter;
}
void DCCEXParser::setRMFTFilter(FILTER_CALLBACK filter)
{
    filterRMFTCallback = filter;
}
void DCCEXParser::setCamParserFilter(FILTER_CALLBACK filter)
{
    filterCamParserCallback = filter;
}
void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
{
    atCommandCallback = callback;
}

// Parse an F() string 
void DCCEXParser::parse(const FSH * cmd) {
      DIAG(F("SETUP(\"%S\")"),cmd);
      int size=STRLEN_P((char *)cmd)+1; 
      char buffer[size];
      STRCPY_P(buffer,(char *)cmd);
      parse(&USB_SERIAL,(byte *)buffer,NULL);
}

// See documentation on DCC class for info on this section

void DCCEXParser::parse(Print *stream,  byte *com,  RingStream *ringStream) {
  // This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
  // found is true first after the leading "<" has been passed which results
  // in parseOne() getting c="C OMM AND>"
  byte *cForLater = NULL;
  bool found = (com[0] != '<');
  for (byte *c=com; c[0] != '\0'; c++) {
    if (found) {
      cForLater = c;
      found=false;
    }
    if (c[0] == '<') {
      if (cForLater) parseOne(stream, cForLater, ringStream);
      found = true;
    }
  }
  if (cForLater) parseOne(stream, cForLater, ringStream);
}

void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
{
#ifdef DISABLE_PROG
    (void)ringStream;
#endif
#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];
    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 <X>
      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')
        filterRMFTCallback(stream, opcode, params, p);
    if (filterCamParserCallback && opcode!='\0')
        filterCamParserCallback(stream, opcode, params, p);

    // Functions return from this switch if complete, break from switch implies error <X> to send
    switch (opcode)
    {
    case '\0':
        return; // filterCallback asked us to ignore
    case 't':   // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>
    {
        int16_t cab;
        int16_t tspeed;
        int16_t direction;

        if (params==1) {  // <t cab>  display state
          if (p[0]<0 || p[0]>10239) break; // beyond DCC range
          // send current state of this cab
	      auto slot=LocoSlot::getSlot(p[0],false);
	      if (slot)
	        CommandDistributor::broadcastLoco(slot);
	      else // send dummy state speed 0 fwd no functions.
            StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
	      return;
        }

        if (params == 4)
        { // <t REGISTER CAB SPEED DIRECTION>
	    // ignore register p[0]
            cab = p[1];
            tspeed = p[2];
            direction = p[3];
        }
        else if (params == 3)
        { // <t CAB SPEED DIRECTION>
            cab = p[0];
            tspeed = p[1];
            direction = p[2];
        }
        else
            break;

        // Convert DCC-EX protocol speed steps where
        // -1=emergency stop, 0-126 as speeds
        // to DCC 0=stop, 1= emergency stop, 2-127 speeds
        if (tspeed > 126 || tspeed < -1)
            break; // invalid JMRI speed code
        if (tspeed < 0)
            tspeed = 1; // emergency stop DCC speed
        else if (tspeed > 0)
            tspeed++; // map 1-126 -> 2-127
        if (cab == 0 && tspeed > 1)
            break; // ignore broadcasts of speed>1

        if (direction < 0 || direction > 1)
            break; // invalid direction code
	if (cab > 10239 || cab < 0)
	    break; // beyond DCC range

        if (DCC::setThrottle(cab, tspeed, direction)) {
	  if (params == 4) // send obsolete format T response
            StringFormatter::send(stream, F("<T %d %d %d>\n"), p[0], p[2], p[3]);
	  // speed change will be broadcast anyway in new <l > format
	  return;
	} else {
	  break; // setThrottle() failed means slot table was full.
	}
    }
    case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>
        if (parsef(stream, params, p))
            return;
        break;

    case 'a': // ACCESSORY <a ADDRESS SUBADDRESS ACTIVATE [ONOFF]> or <a LINEARADDRESS ACTIVATE>
        { 
          int address;
          byte subaddress;
          byte activep;
          byte onoff;
          if (params==2) { // <a LINEARADDRESS ACTIVATE>
              address=(p[0] - 1) / 4 + 1;
              subaddress=(p[0] - 1)  % 4;
              activep=1;
              onoff=2; // send both
          }
          else if (params==3) { // <a ADDRESS SUBADDRESS ACTIVATE>
              address=p[0];
              subaddress=p[1];
              activep=2;
              onoff=2; // send both
          }
          else if (params==4) { // <a ADDRESS SUBADDRESS ACTIVATE ONOFF>
              address=p[0];
              subaddress=p[1];
              activep=2;
	      if ((p[3] < 0) || (p[3] > 1))        // invalid onoff    0|1
		break;
              onoff=p[3];
          }
          else break; // invalid no of parameters
          
          if (
	      ((address & 0x01FF) != address)      // invalid address (limit 9 bits)
           || ((subaddress & 0x03) != subaddress)  // invalid subaddress (limit 2 bits)
           || (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1
	      ) break;
          // Honour the configuration option (config.h) which allows the <a> command to be reversed
	  // Because of earlier confusion we need to do the same thing under both defines
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
          DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
#else
          DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
#endif
        }
        return;
    
    case 'A': // EXTENDED ACCESSORY <A address value> 
        // Note: if this happens to match a defined EXRAIL 
        // DCCX_SIGNAL, then EXRAIL will have intercepted
        // this command alrerady.   
        if (params==2 && DCC::setExtendedAccessory(p[0],p[1])) return;
        break;
     
    case 'T': // TURNOUT  <T ...>
        if (parseT(stream, params, p))
            return;
        break;

#ifndef IO_NO_HAL
    case 'o':  // Neopixel pin manipulation
        if (p[0]==0) break;
        {  
          VPIN vpin=p[0]>0 ? p[0]:-p[0];
          bool setON=p[0]>0;
          if (params==1) {  // <o [-]vpin> 
            IODevice::write(vpin,setON);
            return;
          }
          if (params==2) {  // <o [-]vpin count> 
            IODevice::writeRange(vpin,setON,p[1]);
            return;
          }
          if (params==4 || params==5) { // <z [-]vpin r g b [count]>
             auto count=p[4]?p[4]:1;  
             if (p[1]<0 || p[1]>0xFF) break;  
            if (p[2]<0 || p[2]>0xFF) break;  
            if (p[3]<0 || p[3]>0xFF) break;  
            // strange parameter mangling... see IO_NeoPixel.h NeoPixel::_writeAnalogue
            int colour_RG=(p[1]<<8)  | p[2];
            uint16_t colour_B=p[3];
            IODevice::writeAnalogueRange(vpin,colour_RG,setON,colour_B,count);
            return;
            }
        }
        break;
#endif        

  case 'z':  // direct pin manipulation
        if (p[0]==0) break; 
        if (params==1) {  // <z vpin | -vpin> 
            if (p[0]>0) IODevice::write(p[0],HIGH);
            else IODevice::write(-p[0],LOW);
            return;
        }
        if (params>=2 && params<=4) { // <z vpin analog profile duration> 
            // unused params default to 0           
            IODevice::writeAnalogue(p[0],p[1],p[2],p[3]);
            return;
        }
        break; 

    case 'y': // OUTPUT SOUND <y ...>
        if (parsey(stream, params, p))
            return;
        break;
    case 'Z': // OUTPUT <Z ...>
        if (parseZ(stream, params, p))
            return;
        break;

    case 'S': // SENSOR <S ...>
        if (parseS(stream, params, p))
            return;
        break;

#ifndef DISABLE_PROG
    case 'w': // WRITE CV on MAIN <w CAB CV VALUE>
      if (params != 3)
	break;
      DCC::writeCVByteMain(p[0], p[1], p[2]);
      return;

#ifdef HAS_ENOUGH_MEMORY    
    case 'r': // READ CV on MAIN <r CAB CV>  Requires Railcom
      if (params != 2)
	break;
      if (!DCCWaveform::isRailcom()) break;
      if (!stashCallback(stream, p, ringStream)) break;
      DCC::readCVByteMain(p[0], p[1],callback_r);
      return;
#endif      

    case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>
      if (params != 4)
	break;
      DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);
      return;
#endif
    
    case 'm': // <m cabid momentum [braking]>
              // <m LINEAR|POWER>
      if (params==1) {
        if (p[0]=="LINEAR"_hk) DCC::linearAcceleration=true;
        else if (p[0]=="POWER"_hk) DCC::linearAcceleration=false;
        else break;
        return; 
      }        
      if (params<2 || params>3) break;
      if (params==2) p[2]=p[1];
      if (DCC::setMomentum(p[0],p[1],p[2])) return; 
      break; 

    case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
#ifndef DISABLE_PROG
    case 'P': // WRITE TRANSPARENT DCC PACKET PROG <P REG X1 ... X9>
#endif
        // NOTE: this command was parsed in HEX instead of decimal
        params--; // drop REG
        if (params<1) break;
	if (params > MAX_PACKET_SIZE) break;
        {
          byte packet[params];
          for (int i=0;i<params;i++) {
            packet[i]=(byte)p[i+1];
            if (Diag::CMD) DIAG(F("packet[%d]=%d (0x%x)"), i, packet[i], packet[i]);
          }
          (opcode=='M'?DCCWaveform::mainTrack:DCCWaveform::progTrack).schedulePacket(packet,params,3);  
        }
        return;
        
#ifndef DISABLE_PROG
    case 'W': // WRITE CV ON PROG <W CV VALUE CALLBACKNUM CALLBACKSUB>
        if (!stashCallback(stream, p, ringStream))
	    break;
        if (params == 1) // <W id> Write new loco id (clearing consist and managing short/long)
            DCC::setLocoId(p[0],callback_Wloco);
        else if (params == 4)  // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
            DCC::writeCVByte(p[0], p[1], callback_W4);
        else if ((params==2 || params==3 ) && p[0]=="CONSIST"_hk ) {
            DCC::setConsistId(p[1],p[2]=="REVERSE"_hk,callback_Wconsist);
        }    
        else if (params == 2)  // WRITE CV ON PROG <W CV VALUE>
            DCC::writeCVByte(p[0], p[1], callback_W);
	else
            break;
        return;

    case 'V': // VERIFY CV ON PROG <V CV VALUE> <V CV BIT 0|1>
        if (params == 2)
        { // <V CV VALUE>
            if (!stashCallback(stream, p, ringStream))
                break;
            DCC::verifyCVByte(p[0], p[1], callback_Vbyte);
            return;
        }
        if (params == 3)
        {
            if (!stashCallback(stream, p, ringStream))
                break;
            DCC::verifyCVBit(p[0], p[1], p[2], callback_Vbit);
            return;
        }
        break;

    case 'B': // WRITE CV BIT ON PROG  <B CV BIT VALUE CALLBACKNUM CALLBACKSUB> or <B CV BIT VALUE>
        if (params != 3 && params != 5)
	  break;
        if (!stashCallback(stream, p, ringStream))
	  break;
        DCC::writeCVBit(p[0], p[1], p[2], callback_B);
        return;

    case 'R': // READ CV ON PROG
        if (params == 1) {
            if (!stashCallback(stream, p, ringStream)) break;
            if (p[0]=="LOCOID"_hk) { // <R LOCOID> read consist id
                DCC::getLocoId(callback_Rloco);
                return;
            }
            if (p[0]=="CONSIST"_hk) { // <R CONSIST> read consist id
                DCC::getConsistId(callback_Rloco);
                return;
            }
            
            // <R CV> -- uses verify callback
            DCC::verifyCVByte(p[0], 0, callback_Vbyte);
            return;
        }
        if (params == 3)
        { // <R CV CALLBACKNUM CALLBACKSUB>
            if (!stashCallback(stream, p, ringStream))
                break;
            DCC::readCV(p[0], callback_R);
            return;
        }
        if (params == 0)
        { // <R> New read driveaway loco id
            if (!stashCallback(stream, p, ringStream))
                break;
            DCC::getDriveawayLocoId(callback_Rloco);
            return;
        }
        break;
#endif

    case '1': // POWERON <1   [MAIN|PROG|JOIN]>
        {
	  if (params > 1) break;
	  if (params==0) { // All
	    TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);
	  }
	  if (params==1) {
	    if (p[0]=="MAIN"_hk) { // <1 MAIN>
	      TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
            }
#ifndef DISABLE_PROG
            else if (p[0] == "JOIN"_hk) {  // <1 JOIN>
	      TrackManager::setJoin(true);
	      TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
            }
            else if (p[0]=="PROG"_hk) { // <1 PROG>
	      TrackManager::setJoin(false);
	      TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
            }
#endif
            else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
	      byte t = (p[0] - 'A');
	      TrackManager::setTrackPower(POWERMODE::ON, t);
	      //StringFormatter::send(stream, F("<p1 %c>\n"), t+'A');
            }
	    else break; // will reply <X>
	  }
	  //TrackManager::streamTrackState(NULL,t);
          
	  return;
	}
            
    case '0': // POWEROFF <0 [MAIN | PROG] >
        {
	  if (params > 1) break;
	  if (params==0) { // All
	    TrackManager::setJoin(false);
	    TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);
	  }
	  if (params==1) {
	    if (p[0]=="MAIN"_hk) { // <0 MAIN>
	      TrackManager::setJoin(false);
	      TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
	    }
#ifndef DISABLE_PROG
            else if (p[0]=="PROG"_hk) { // <0 PROG>
          TrackManager::setJoin(false);
	      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"_hk && p[0] <= "H"_hk) { // <1 A-H>
	      byte t = (p[0] - 'A');
	      TrackManager::setJoin(false);
	      TrackManager::setTrackPower(POWERMODE::OFF, t);
	      //StringFormatter::send(stream, F("<p0 %c>\n"), t+'A');
	    }
	    else break; // will reply <X>
	  }
	  return;
	}

    case '!': // ESTOPALL  <!>
        if (p[0]=="P"_hk) DCC::estopLock(true); // <!P>
        else if (p[0]=="R"_hk) DCC::estopLock(false); // <!R
        else if (p[0]=="Q"_hk) StringFormatter::send(stream, 
                  DCC::isEstopLocked() ? F("<!PAUSED>\n"): F("<!RESUMED>\n")); // <!Q>
        
        else DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
        return;

#ifdef HAS_ENOUGH_MEMORY
    case 'c': // SEND METER RESPONSES <c>
        // No longer useful because of multiple tracks See <JG> and <JI>
        if (params>0) break;
        TrackManager::reportObsoleteCurrent(stream);
        return;
#endif
    case 'Q': // SENSORS <Q>
        Sensor::printAll(stream);
        return;

    case 's': // STATUS <s>
        StringFormatter::send(stream, F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
        CommandDistributor::broadcastPower(); // <s> is the only "get power status" command we have
        Turnout::printAll(stream); //send all Turnout states
        Sensor::printAll(stream);  //send all Sensor  states
        return;       

#ifndef DISABLE_EEPROM
    case 'E': // STORE EPROM <E>
        EEStore::store();
        StringFormatter::send(stream, F("<e %d %d %d>\n"), EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs);
        return;

    case 'e': // CLEAR EPROM <e>
        EEStore::clear();
        StringFormatter::send(stream, F("<O>\n"));
        return;
#endif
    case ' ': // < >
        StringFormatter::send(stream, F("\n"));
        return;
    case 'C': // CONFIG <C [params]>
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
      if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
	bool on = false;
	if (params>1 && p[1] == "ON"_hk) {
	  on = true;
	}
	DCCDecoder::onoff(on);
	return;
      }
#if WIFI_ON
      if (p[0] == "WIFI"_hk) { 	// <C WIFI SSID PASSWORD>
	if (params != 5)        // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
	  break;
	if (p[1] == 0x7777 && p[3] == 0x7777) {
	  WifiESP::setup((const char*)(com + p[2]), (const char*)(com + p[4]), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
	}
	return;
      }
#endif
#endif //ESP32
      if (parseC(stream, params, p))
	return;
      break;
#ifndef DISABLE_DIAG
    case 'D': // DIAG <D [params]>
        if (parseD(stream, params, p))
            return;
        break;
#endif
    case '=': // TRACK MANAGER CONTROL <= [params]>
        if (TrackManager::parseEqualSign(stream, params, p))
            return;
        break;

    case '#': // NUMBER OF LOCOSLOTS <#>
        {
	  int free = DCCTimer::getMinimumFreeMemory();
	  int freeSlotGuess = free/sizeof(LocoSlot);
	  freeSlotGuess = freeSlotGuess - 2; // be conservative
	  if (freeSlotGuess > MAX_LOCOS)
	    freeSlotGuess = MAX_LOCOS;
	  if (freeSlotGuess < 0)
	    freeSlotGuess = 0;
	  StringFormatter::send(stream, F("<# %d>\n"), freeSlotGuess);
	}
        return;

    case '-': // Forget Loco <- [cab]>
        if (params > 1 || p[0]<0) break;
        if (p[0]==0) DCC::forgetAllLocos();
        else  DCC::forgetLoco(p[0]);
        return;

    case 'F': // New command to call the new Loco Function API <F cab func 1|0>
        if(params!=3) break; 
        
        if (p[1]=="DCFREQ"_hk) { // <F cab DCFREQ 0..3>
          if (p[2]<0 || p[2]>3) break;
          DCC::setDCFreq(p[0],p[2]);
          return;    
        }

        if (Diag::CMD)
            DIAG(F("Setting loco %d F%d %S"), p[0], p[1], p[2] ? F("ON") : F("OFF"));
        if (DCC::setFn(p[0], p[1], p[2] == 1)) return;
	break;

    case '^': // Consist  <^ [cab0..7]>
        if (DCCConsist::parse(stream,params,p)) return;
        break;

#if WIFI_ON
    case '+': // Complex Wifi interface command (not usual parse)
        if (atCommandCallback && !ringStream) {
          TrackManager::setPower(POWERMODE::OFF);
          atCommandCallback((HardwareSerial *)stream,com);
          return;
        }
        break;
#endif 

    case 'J' : // throttle info access
        {
            if (params<1) break; // <J>
            //if ((params<1) | (params>2)) break; // <J>
            int16_t id=(params==2)?p[1]:0;
            switch(p[0]) {
                case "C"_hk: // <JC mmmm nn> sets time and speed
                    if (params==1) { // <JC> returns latest time
                        int16_t x = CommandDistributor::retClockTime();
                        StringFormatter::send(stream, F("<jC %d>\n"), x);
                        return;
                    }
                    CommandDistributor::setClockTime(p[1], p[2]);
                    return;
                
                case "G"_hk: // <JG> current gauge limits
                    if (params>1) break;
                    TrackManager::reportGauges(stream);   // <g limit...limit>     
                    return;
                
                case "I"_hk: // <JI> current values
                    if (params>1) break;
                    TrackManager::reportCurrent(stream);   // <g limit...limit>     
                    return;

                case "L"_hk: // <JL display row> track state and mA value on display
                    if (params<3) break;
                    TrackManager::reportCurrentLCD(p[1], p[2]);   // Track power status     
                    return;                    

                case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
                    if (params!=1) break; // <JA>
                    StringFormatter::send(stream, F("<jA>\n"));
                    return;
 
                case "M"_hk: // <JM> Stash management
                    if (parseJM(stream, params, p))
                        return;
                    break;
 
            case "R"_hk: // <JR> returns rosters 
                StringFormatter::send(stream, F("<jR"));
#ifdef EXRAIL_ACTIVE
                if (params==1) {
                    SENDFLASHLIST(stream,RMFT2::rosterIdList)
                }
                else {
                    auto rosterName= RMFT2::getRosterName(id);
                    if (!rosterName) rosterName=F("");

                    auto functionNames= RMFT2::getRosterFunctions(id);
                    if (!functionNames) functionNames=RMFT2::getRosterFunctions(0);
                    if (!functionNames) functionNames=F("");
                    StringFormatter::send(stream,F(" %d \"%S\" \"%S\""), 
					                            id, rosterName, functionNames);
                }
#endif          
                StringFormatter::send(stream, F(">\n"));      
                return; 
            case "T"_hk: // <JT> returns turnout list 
                StringFormatter::send(stream, F("<jT"));
                if (params==1) { // <JT>
                    for ( Turnout * t=Turnout::first(); t; t=t->next()) { 
                        if (t->isHidden()) continue;          
                        StringFormatter::send(stream, F(" %d"),t->getId());
                    }
                }
                else { // <JT id>
                    Turnout * t=Turnout::get(id);
                    if (!t || t->isHidden()) StringFormatter::send(stream, F(" %d X"),id);
                    else {
		      const FSH *tdesc = NULL;
#ifdef EXRAIL_ACTIVE
		      tdesc = RMFT2::getTurnoutDescription(id);
#endif
		      if (tdesc == NULL)
			tdesc = F("");
		      StringFormatter::send(stream, F(" %d %c \"%S\""),
					    id,t->isThrown()?'T':'C',
					    tdesc);
		    }
                }
                StringFormatter::send(stream, F(">\n"));
                return;
// No turntables without HAL support
#ifndef IO_NO_HAL
            case "O"_hk: // <JO returns turntable list
                StringFormatter::send(stream, F("<jO"));
                if (params==1) { // <JO>
                    for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) { 
                        if (tto->isHidden()) continue;          
                        StringFormatter::send(stream, F(" %d"),tto->getId());
                    }
                    StringFormatter::send(stream, F(">\n"));
                } else {    // <JO id>
                    Turntable *tto=Turntable::get(id);
                    if (!tto || tto->isHidden()) {
                        StringFormatter::send(stream, F(" %d X>\n"), id);
                    } else {
                        uint8_t pos = tto->getPosition();
                        uint8_t type = tto->isEXTT();
                        uint8_t posCount = tto->getPositionCount();
                        const FSH *todesc = NULL;
#ifdef EXRAIL_ACTIVE
                        todesc = RMFT2::getTurntableDescription(id);
#endif
                        if (todesc == NULL) todesc = F("");
                        StringFormatter::send(stream, F(" %d %d %d %d \"%S\">\n"), id, type, pos, posCount, todesc);
                    }
                }
                return;
            case "P"_hk: // <JP id> returns turntable position list for the turntable id
                if (params==2) { // <JP id>
                    Turntable *tto=Turntable::get(id);
                    if (!tto || tto->isHidden()) {
                        StringFormatter::send(stream, F(" %d X>\n"), id);
                    } else {
                        uint8_t posCount = tto->getPositionCount();
                        const FSH *tpdesc = NULL;
                        for (uint8_t p = 0; p < posCount; p++) {
                            StringFormatter::send(stream, F("<jP"));
                            int16_t angle = tto->getPositionAngle(p);
#ifdef EXRAIL_ACTIVE
                            tpdesc = RMFT2::getTurntablePositionDescription(id, p);
#endif
                            if (tpdesc == NULL) tpdesc = F("");
                            StringFormatter::send(stream, F(" %d %d %d \"%S\""), id, p, angle, tpdesc);
                            StringFormatter::send(stream, F(">\n"));
                        }
                    }
                } else {
                    StringFormatter::send(stream, F("<jP X>\n"));
                }
                return;
#endif
            default: break;    
            }  // switch(p[1])
        break; // case J
        }

// No turntables without HAL support
#ifndef IO_NO_HAL
    case 'I': // TURNTABLE  <I ...>
        if (parseI(stream, params, p))
            return;
        break;
#endif
    case '/': // implemented in EXRAIL parser
    case 'L': // LCC interface implemented in EXRAIL parser
    case 'N': // interface implemented in CamParser
        break; // Will <X> if not intercepted by filters

    case '@': 
#ifndef DISABLE_VDPY
      if (params==0) {  // <@> 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            
        if (params==4 && p[2]==0x7777) { // <@ x  y "string">
            // p[2] will be 0x7777 string marker. 
            StringFormatter::lcd2(p[0],p[1], F("%s"),(const char*)(com + p[3]));
            return; 
        }
        break; // will <X>    
 
default: //anything else will diagnose and drop out to <X>
      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]);
      } else {
	DIAG(F("Unprintable %x"), opcode);
      }
      break;

    } // end of opcode switch

out:// Any fallout here sends an <X>
    StringFormatter::send(stream, F("<X>\n"));
}

bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
{

    switch (params)
    {
    
    case 2: // <Z ID ACTIVATE>
    {
        Output *o = Output::get(p[0]);
        if (o == NULL)
            return false;
        o->activate(p[1]);
        StringFormatter::send(stream, F("<Y %d %d>\n"), p[0], p[1]);
    }
        return true;

    case 3: // <Z ID PIN IFLAG>
        if (p[0] < 0 || p[2] < 0 || p[2] > 7 )
	        return false;
        if (!Output::create(p[0], p[1], p[2], 1))
          return false;
        StringFormatter::send(stream, F("<O>\n"));
        return true;

    case 1: // <Z ID>
        if (!Output::remove(p[0]))
          return false;
        StringFormatter::send(stream, F("<O>\n"));
        return true;

    case 0: // <Z> list Output definitions
    {
        bool gotone = false;
        for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
        {
            gotone = true;
            StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.flags, tt->data.active);
        }
        return gotone;
    }
    default:
        return false;
    }
}

#include "IO_DFPlayerBase.h"

bool DCCEXParser::parsey(Print *stream, int16_t params, int16_t p[])
{
    (void)stream; // unused parameter
     
    // <y vpin PLAY track [volume]>
    // <y vpin REPEAT track [volume]>
    // <y vpin FOLDER folder>
    // <y vpin STOP>
    // <y vpin VOL volume>
    // <y vpin PAUSE>
    // <y vpin RESUME>
    // <y vpin EQ eq>
    // <y vpin RESET>
    
    if (params<2) return false;

    int16_t v1=0;
    uint8_t v2=0;
    uint16_t cmd=0; 

    

    switch (p[1])
    {
      case "PLAY"_hk:
        if (params<3) return false;
        v1=p[2]; // track
        cmd=DFPlayerBase::DF_PLAY;
        if (params>=4) v2=p[3]; // volume
        break;
    
      case "REPEAT"_hk:
        if (params<3) return false;
        v1=p[2]; // track
        cmd=DFPlayerBase::DF_REPEATPLAY;
        if (params>=4) v2=p[3]; // volume
        break;
      
      case "FOLDER"_hk:
        if (params!=3) return false;
        v2=p[2]; // folder
        cmd=DFPlayerBase::DF_FOLDER;
        break;

      case "STOP"_hk:
        if (params!=2) return false;
        cmd=DFPlayerBase::DF_STOPPLAY;
        break;

      case "PAUSE"_hk:
        if (params!=2) return false;
        cmd=DFPlayerBase::DF_PAUSE;
        break;

      case "RESUME"_hk:
        if (params!=2) return false;
        cmd=DFPlayerBase::DF_RESUME;
        break;

      case "RESET"_hk:
        if (params!=2) return false;
        cmd=DFPlayerBase::DF_RESET;
        break;

      case "VOL"_hk:
        if (params!=3) return false;
        cmd=DFPlayerBase::DF_VOL;
        v2=p[2]; // volume
        break;
    
      case "EQ"_hk:
        if (params!=3) return false;
        cmd=DFPlayerBase::DF_EQ;
        v2=p[2]; // EQ type
        break;
        
      default:
        return false;
    }
    IODevice::writeAnalogue((VPIN)p[0],v1,v2,cmd);
    return true;
}

//===================================
bool DCCEXParser::parsef(Print *stream, int16_t params, int16_t p[])
{
  // JMRI sends this info in DCC message format but it's not exactly
  // convenient for other processing
  if (params == 2) {
    byte instructionField = p[1] & 0xE0;   // 1110 0000
    if (instructionField == 0x80) {        // 1000 0000 Function group 1
      // Shuffle bits from order F0 F4 F3 F2 F1 to F4 F3 F2 F1 F0
      byte normalized = (p[1] << 1 & 0x1e) | (p[1] >> 4 & 0x01);
      return (funcmap(p[0], normalized, 0, 4));
    } else if (instructionField == 0xA0) { // 1010 0000 Function group 2
      if (p[1] & 0x10)                     // 0001 0000 Bit selects F5toF8 / F9toF12
	return (funcmap(p[0], p[1], 5, 8));
      else
	return (funcmap(p[0], p[1], 9, 12));
    } 
  }
  if (params == 3) {
    if (p[1] == 222) {
      return (funcmap(p[0], p[2], 13, 20));
    } else if (p[1] == 223) {
      return (funcmap(p[0], p[2], 21, 28));
    } 
  }
  (void)stream; // NO RESPONSE
  return false;
}

bool DCCEXParser::funcmap(int16_t cab, byte value, byte fstart, byte fstop)
{
  for (int16_t i = fstart; i <= fstop; i++) {
    if (! DCC::setFn(cab, i, value & 1)) return false;
    value >>= 1;
  }
  return true;
}

//===================================
bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
{
    switch (params)
    {
    case 0: // <T>  list turnout definitions
        return Turnout::printAll(stream); // will <X> if none found

    case 1: // <T id>  delete turnout
        if (!Turnout::remove(p[0]))
            return false;
        StringFormatter::send(stream, F("<O>\n"));
        return true;

    case 2: // <T id 0|1|T|C> 
        {
          bool state = false;
          switch (p[1]) {
            // Turnout messages use 1=throw, 0=close.
            case 0:
            case "C"_hk:
              state = true;
              break;
            case 1:
            case "T"_hk:
              state= false;
              break;
            case "X"_hk:
	    {
              Turnout *tt = Turnout::get(p[0]);
              if (tt) {
                tt->print(stream);
                return true;
              }
              return false;
	    }
            default: // Invalid parameter
	      return false;
          }
          if (!Turnout::setClosed(p[0], state)) return false;
          return true;
        }

    default: // Anything else is some kind of turnout create function.
      if (params == 6 && p[1] == "SERVO"_hk) { // <T id SERVO n n n n>
        if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
          return false;
      } else 
      if (params == 3 && p[1] == "VPIN"_hk) { // <T id VPIN n>
        if (!VpinTurnout::create(p[0], p[2])) return false;
      } else 
      if (params >= 3 && p[1] == "DCC"_hk) {
        // <T id DCC addr subadd>   0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
        if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
          if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
        } else if (params==3 && p[2]>0 && p[2]<=512*4) { // <T id DCC nn>, 1<=nn<=2048
          // Linearaddress 1 maps onto decoder address 1/0 (not 0/0!).
          if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false;
        } else
          return false;
      } else 
      if (params==3) { // legacy <T id addr subadd> for DCC accessory
        if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) {
          if (!DCCTurnout::create(p[0], p[1], p[2])) return false;
        } else
          return false;
      } 
      else 
      if (params==4) { // legacy <T id n n n> for Servo
        if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false;
      } else
        return false;

      StringFormatter::send(stream, F("<O>\n"));
      return true;
    }
}

bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
{

    switch (params)
    {
    case 3: // <S id pin pullup>  create sensor. pullUp indicator (0=LOW/1=HIGH)
        if (!Sensor::create(p[0], p[1], p[2]))
          return false;
        StringFormatter::send(stream, F("<O>\n"));
        return true;

    case 1: // S id> remove sensor
        if (!Sensor::remove(p[0]))
          return false;
        StringFormatter::send(stream, F("<O>\n"));
        return true;

    case 0: // <S> list sensor definitions
      Sensor::dumpAll(stream);
      return true;

    default: // invalid number of arguments
        break;
    }
    return false;
}

bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
    (void)stream; // arg not used, maybe later?
    if (params == 0)
        return false;
    switch (p[0])
    {
#ifndef DISABLE_PROG
    case "PROGBOOST"_hk:
        TrackManager::progTrackBoosted=true;
	    return true;
#endif
    case "RESET"_hk:
        DCCTimer::reset();
        break; // and <X> if we didnt restart
    case "SPEED28"_hk:
        DCC::setGlobalSpeedsteps(28);
	DIAG(F("28 Speedsteps"));
        return true;

    case "SPEED128"_hk:
        DCC::setGlobalSpeedsteps(128);
	DIAG(F("128 Speedsteps"));
        return true;
#if defined(HAS_ENOUGH_MEMORY) && !defined(ARDUINO_ARCH_UNO)
    case "RAILCOM"_hk:
        {   // <C RAILCOM ON|OFF|DEBUG >
            if (params<2) return false;
            bool on=false;
            switch (p[1]) {
                case "ON"_hk:
                case 1:
                    on=true;
                    break;
                case "OFF"_hk:
                case 0:
                     break;
                default:
                 return false;
            }              
        DIAG(F("Railcom %S")
            ,DCCWaveform::setRailcom(on)?F("ON"):F("OFF"));
        return true;     
        }
#endif
#ifndef DISABLE_PROG
    case "ACK"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
	if (params >= 3) {
            long duration;
	    if (p[1] == "LIMIT"_hk) {
	      DCCACK::setAckLimit(p[2]);
	      LCD(1, F("Ack Limit=%dmA"), p[2]);       // <D ACK LIMIT 42>
	    } else if (p[1] == "MIN"_hk) {
	      if (params == 4 && p[3] == "MS"_hk)
		duration = p[2] * 1000L;
	      else
		duration = p[2];
	      DCCACK::setMinAckPulseDuration(duration);
	      LCD(0, F("Ack Min=%lus"), duration);     // <D ACK MIN 1500>
	    } else if (p[1] == "MAX"_hk) {
	      if (params == 4 && p[3] == "MS"_hk)      // <D ACK MAX 80 MS>
		duration = p[2] * 1000L;
	      else
		duration = p[2];
	      DCCACK::setMaxAckPulseDuration(duration);
	      LCD(0, F("Ack Max=%lus"), duration);     // <D ACK MAX 9000>
	    } else if (p[1] == "RETRY"_hk) {
	      if (p[2] >255) p[2]=3;
	      LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2]));  //   <D ACK RETRY 2>
	    }
	} else {
      bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
    
	  DIAG(F("Ack diag %S"), onOff ? F("on") : F("off"));
	  Diag::ACK = onOff;
	}
        return true;
#endif
    default: // invalid/unknown
      break;
    }
    return false;
}

bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
{
    if (params == 0)
        return false;
    bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
    switch (p[0])
    {
    case "CABS"_hk: // <D CABS>
        DCC::displayCabList(stream);
        return true;

    case "RAM"_hk: // <D RAM>
        DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
        return true;

    case "CMD"_hk: // <D CMD ON/OFF>
        Diag::CMD = onOff;
        return true;

#ifdef HAS_ENOUGH_MEMORY
    case "RAILCOM"_hk: // <D RAILCOM ON/OFF>
        Diag::RAILCOM = onOff;
        return true;

    case "WIFI"_hk: // <D WIFI ON/OFF>
        Diag::WIFI = onOff;
        return true;

    case "ETHERNET"_hk: // <D ETHERNET ON/OFF>
        Diag::ETHERNET = onOff;
        return true;

    case "WIT"_hk: // <D WIT ON/OFF>
        Diag::WITHROTTLE = onOff;
        return true;

    case "LCN"_hk: // <D LCN ON/OFF>
        Diag::LCN = onOff;
        return true;

    case "SNIFFER"_hk: // <D SNIFFER ON/OFF>
        Diag::SNIFFER = onOff;
        return true;

    case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
        Diag::WEBSOCKET = onOff;
        return true;
#endif
#ifndef DISABLE_EEPROM
    case "EEPROM"_hk: // <D EEPROM NumEntries>
	if (params >= 2)
	    EEStore::dump(p[1]);
	return true;
#endif
    case "SERVO"_hk:  // <D SERVO vpin position [profile]>

    case "ANOUT"_hk:  // <D ANOUT vpin position [profile]>
        IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
        return true;

    case "ANIN"_hk:   // <D ANIN vpin>  Display analogue input value
        DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1]));
        return true;

#if !defined(IO_NO_HAL)
    case "HAL"_hk: 
        if (p[1] == "SHOW"_hk) {
          I2CManager.scanForDevices(&USB_SERIAL);
          IODevice::DumpAll();
        }
        else if (p[1] == "RESET"_hk)
          IODevice::reset();
        return true;
#endif

    case "TT"_hk:     // <D TT vpin steps activity>
        IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
        return true;

    default: // invalid/unknown
        return parseC(stream, params, p);
    }
    return false;
}

// ==========================
// Turntable - no support if no HAL
// <I> - list all
// <I id> - broadcast type and current position
// <I id DCC> - create DCC - This is TBA
// <I id steps> - operate (DCC)
// <I id steps activity> - operate (EXTT)
// <I id ADD position value> - add position
// <I id EXTT i2caddress vpin home> - create EXTT
#ifndef IO_NO_HAL
bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
{
    switch (params)
    {
    case 0: // <I> list turntable objects
        return Turntable::printAll(stream);

    case 1: // <I id> broadcast type and current position
        {    
            Turntable *tto = Turntable::get(p[0]);
            if (tto) {
                bool type = tto->isEXTT();
                uint8_t position = tto->getPosition();
                StringFormatter::send(stream, F("<I %d %d>\n"), type, position);
            } else {
                return false;
            }
        }
        return true;
    
    case 2: // <I id position> - rotate a DCC turntable
        {
            Turntable *tto = Turntable::get(p[0]);
            if (tto && !tto->isEXTT()) {
                if (!tto->setPosition(p[0], p[1])) return false;
            } else {
                return false;
            }
        }
        return true;

    case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable
        {
            Turntable *tto = Turntable::get(p[0]);
            if (p[1] == "DCC"_hk) {
                if (tto || p[2] < 0 || p[2] > 3600) return false;
                if (!DCCTurntable::create(p[0])) return false;
                Turntable *tto = Turntable::get(p[0]);
                tto->addPosition(0, 0, p[2]);
                StringFormatter::send(stream, F("<I>\n"));
            } else {
                if (!tto) return false;
                if (!tto->isEXTT()) return false;
                if (!tto->setPosition(p[0], p[1], p[2])) return false;
            }
        }
        return true;
    
    case 4: // <I id EXTT vpin home> create an EXTT turntable
        {
            Turntable *tto = Turntable::get(p[0]);
            if (p[1] == "EXTT"_hk) {
                if (tto || p[3] < 0 || p[3] > 3600) return false;
                if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false;
                Turntable *tto = Turntable::get(p[0]);
                tto->addPosition(0, 0, p[3]);
                StringFormatter::send(stream, F("<I>\n"));
            } else {
                return false;
            }
        }
        return true;
    
    case 5: // <I id ADD position value angle> add a position
        {
            Turntable *tto = Turntable::get(p[0]);
            if (p[1] == "ADD"_hk) {
                // tto must exist, no more than 48 positions, angle 0 - 3600
                if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false;
                tto->addPosition(p[2], p[3], p[4]);
                StringFormatter::send(stream, F("<I>\n"));
            } else {
                return false;
            }
        }
        return true;
    
    default:    // Anything else is invalid
        return false;
    }
}
#endif

bool DCCEXParser::parseJM(Print *stream, int16_t params, int16_t p[]) {
  switch (params) {
    case 1: // <JM> list all stashed automations
        Stash::list(stream);
        return true; 
        
    case 2: // <JM id> get stash value 
        Stash::list(stream, p[1]);
        return true;

    case 3: // 
        if (p[1]=="CLEAR"_hk) {
            if (p[2]=="ALL"_hk) { // <JM CLEAR ALL>
                Stash::clearAll();
                return true;
            }
            Stash::clear(p[2]); // <JM CLEAR id>
            return true;
        }
        Stash::set(p[1], p[2]);  // <JM id loco>
        return true;
    
    case 4: // <JM CLEAR ANY id>
    if (p[1]=="CLEAR"_hk && p[2]=="ANY"_hk) { 
        // <JM CLEAR ANY id>
        Stash::clearAny(p[3]);
        return true;
    }
    
    default: break;
}
return false;
}

// CALLBACKS must be static
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
{
    if (stashBusy )
        return false;
    stashBusy = true;
    stashStream = stream;
    stashRingStream=ringStream;
    if (ringStream) stashTarget= ringStream->peekTargetMark();
    memcpy(stashP, p, MAX_COMMAND_PARAMS * sizeof(p[0]));
    return true;
}

Print * DCCEXParser::getAsyncReplyStream() {
       if (stashRingStream) {
           stashRingStream->mark(stashTarget);
           return stashRingStream;
       }
       return stashStream;
}

void DCCEXParser::commitAsyncReplyStream() {
     if (stashRingStream) stashRingStream->commit();
     stashBusy = false;
}

void DCCEXParser::callback_W(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(),
          F("<r %d %d>\n"), stashP[0], result == 1 ? stashP[1] : -1);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_W4(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(),
	  F("<r%d|%d|%d %d>\n"), stashP[2], stashP[3], stashP[0], result == 1 ? stashP[1] : -1);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_B(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(), 
          F("<r%d|%d|%d %d %d>\n"), stashP[3], stashP[4], stashP[0], stashP[1], result == 1 ? stashP[2] : -1);
    commitAsyncReplyStream();
}
void DCCEXParser::callback_Vbit(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(), F("<v %d %d %d>\n"), stashP[0], stashP[1], result);
    commitAsyncReplyStream();
}
void DCCEXParser::callback_Vbyte(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(), F("<v %d %d>\n"), stashP[0], result);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_R(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(), F("<r%d|%d|%d %d>\n"), stashP[1], stashP[2], stashP[0], result);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_r(int16_t result)
{
    StringFormatter::send(getAsyncReplyStream(), F("<r %d %d %d >\n"), stashP[0], stashP[1], result);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_Rloco(int16_t result) {
  // determine type of <R command from stashP[0]
  const FSH * typetag;
  if (stashP[0]=="LOCOID"_hk) {
    typetag=F("LOCOID ");
  } else if (stashP[0]=="CONSIST"_hk) {
    typetag=F("CONSIST ");
  } else {
    typetag=F("");
  }

  const FSH * detail;
  if (result<=0) {
    detail=F("<r %S%d>\n");
  } else {
    bool longAddr=result & LONG_ADDR_MARKER;        //long addr
    if (longAddr)
      result = result^LONG_ADDR_MARKER;
    if (longAddr && result <= HIGHEST_SHORT_ADDR)
      detail=F("<r LONG %S%d UNSUPPORTED>\n");
    else
      detail=F("<r %S%d>\n");
  }
  StringFormatter::send(getAsyncReplyStream(), detail, typetag,result);
  commitAsyncReplyStream();
}

void DCCEXParser::callback_Wloco(int16_t result)
{
    if (result==1) result=stashP[0]; // pick up original requested id from command
    StringFormatter::send(getAsyncReplyStream(), F("<w %d>\n"), result);
    commitAsyncReplyStream();
}

void DCCEXParser::callback_Wconsist(int16_t result)
{
    if (result==-4) DIAG(F("Long Consist %d not supported by decoder"),stashP[1]);
    if (result==1) result=stashP[1]; // pick up original requested id from command
    StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
     result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));
    commitAsyncReplyStream();
}


================================================
FILE: DCCEXParser.h
================================================
/*
 *  © 2021 Mike S
 *  © 2021 Fred Decker
 *  © 2020-2025 Chris Harlow
 *  All rights reserved.
 *  
 *  This file is part of Asbelos DCC 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 <https://www.gnu.org/licenses/>.
 */
#ifndef DCCEXParser_h
#define DCCEXParser_h
#include <Arduino.h>
#include "FSH.h"
#include "RingStream.h"
#include "defines.h"

typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);

struct DCCEXParser
{
   
   static void parse(Print * stream,  byte * command,  RingStream * ringStream);
   static void parse(const FSH * cmd);
   static void parseOne(Print * stream,  byte * command,  RingStream * ringStream);
   static void setFilter(FILTER_CALLBACK filter);
   static void setRMFTFilter(FILTER_CALLBACK filter);
   static void setCamParserFilter(FILTER_CALLBACK filter);
   static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
   static const int MAX_COMMAND_PARAMS=10;  // Must not exceed this
   static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
 
   private:
  
    static const int16_t MAX_BUFFER=50;  // longest command sent in
    static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], byte * command, bool usehex);
     
    static bool parseT(Print * stream, int16_t params, int16_t p[]);
    static bool parseZ(Print * stream, int16_t params, int16_t p[]);
    static bool parsey(Print * stream, int16_t params, int16_t p[]);
    static bool parseS(Print * stream, int16_t params, int16_t p[]);
    static bool parsef(Print * stream, int16_t params, int16_t p[]);
    static bool parseC(Print * stream, int16_t params, int16_t p[]);
    static bool parseD(Print * stream, int16_t params, int16_t p[]);
    static bool parseJM(Print * stream, int16_t params, int16_t p[]);
#ifndef IO_NO_HAL
    static bool parseI(Print * stream, int16_t params, int16_t p[]);
#endif

    static Print * getAsyncReplyStream();
    static void commitAsyncReplyStream();

    static bool stashBusy;
    static byte stashTarget;
    static Print * stashStream;
    static RingStream * stashRingStream;
    
    static int16_t stashP[MAX_COMMAND_PARAMS];
    static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
    static void callback_W(int16_t result);
    static void callback_W4(int16_t result);
    static void callback_B(int16_t result);        
    static void callback_R(int16_t result); // prog
    static void callback_r(int16_t result); // main
    static void callback_Rloco(int16_t result);
    static void callback_Wloco(int16_t result);
    static void callback_Wconsist(int16_t result);
    static void callback_Vbit(int16_t result);
    static void callback_Vbyte(int16_t result);
    static FILTER_CALLBACK  filterCallback;
    static FILTER_CALLBACK  filterRMFTCallback;
    static FILTER_CALLBACK  filterCamParserCallback;
    static AT_COMMAND_CALLBACK  atCommandCallback;
    static void sendFlashList(Print * stream,const int16_t flashList[]);

};

#endif


================================================
FILE: DCCPacket.h
================================================
/*
 *  © 2025 Harald Barth
 *  
 *  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/>.
 */
#include <Arduino.h>
#ifndef DCCPacket_h
#define DCCPacket_h
#include <strings.h>
#include "defines.h"

class DCCPacket {
public:
  DCCPacket() {
    _len = 0;
    _data = NULL;
  };
  DCCPacket(byte *d, byte l) {
    _len = l;
    _data = new byte[_len];
    for (byte n = 0; n<_len; n++)
      _data[n] = d[n];
  };
  DCCPacket(const DCCPacket &old) {
    _len = old._len;
    _data = new byte[_len];
    for (byte n = 0; n<_len; n++)
      _data[n] = old._data[n];
  };
  DCCPacket &operator=(const DCCPacket &rhs) {
    if (this == &rhs)
      return *this;
    delete[]_data;
    _len = rhs._len;
    _data = new byte[_len];
    for (byte n = 0; n<_len; n++)
      _data[n] = rhs._data[n];
    return *this;
  };
  ~DCCPacket() {
    if (_len) {
      delete[]_data;
      _len = 0;
      _data = NULL;
    }
  };
  inline bool operator==(const DCCPacket &right) {
    if (_len != right._len)
      return false;
    if (_len == 0)
      return true;
    return (bcmp(_data, right._data, _len) == 0);
  };
  void print() {
    static const char hexchars[]="0123456789ABCDEF";
    USB_SERIAL.print(F("<* DCCPACKET "));
    for (byte n = 0; n< _len; n++) {
      USB_SERIAL.print(hexchars[_data[n]>>4]);
      USB_SERIAL.print(hexchars[_data[n] & 0x0f]);
      USB_SERIAL.print(' ');
    }
    USB_SERIAL.print(F("*>\n"));
  };
  inline byte len() {return _len;};
  inline byte *data() {return _data;};
private:
  byte _len = 0;
  byte *_data = NULL;
};
#endif


================================================
FILE: DCCQueue.cpp
================================================
/*
 *  © 2025 Chris Harlow
 *  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/>.
 */

/* What does this queue manager do:
  1. It provides a high priority queue and a low priority queue.
  2. It manages situations where multiple loco speed commands are in the queue.
  3. It allows an ESTOP to jump the queue and eliminate any outstanding speed commands that would later undo the stop.
  4. It allows for coil on/off accessory commands to be synchronized to a given time delay.
  5. It prevents transmission of sequential packets to the same loco id 
  */
#include "Arduino.h"
#include "defines.h"
#include "DCCQueue.h"
#include "DCCWaveform.h"
#include "DIAG.h"

// create statics 
DCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue();
DCCQueue* DCCQueue::highPriorityQueue=new DCCQueue();
PendingSlot* DCCQueue::recycleList=nullptr;
uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the same loco in a row


    DCCQueue::DCCQueue() {
        head=nullptr;
        tail=nullptr;
    }
    
    void DCCQueue::addQueue(PendingSlot* p) {
        if (tail) tail->next=p;
        else head=p;
        tail=p;
        p->next=nullptr;          
    }

    void DCCQueue::jumpQueue(PendingSlot* p) {
        p->next=head;
        head=p;
        if (!tail) tail=p; 
    }

    
    void DCCQueue::recycle(PendingSlot* p) {
        p->next=recycleList;
        recycleList=p;
    }
    
    void DCCQueue::remove(PendingSlot* premove) {
        PendingSlot* previous=nullptr;
        for (auto p=head;p;previous=p,p=p->next) {
            if (p==premove) {
                // remove this slot from the queue 
                if (previous) previous->next=p->next;
                else head=p->next;
                if (p==tail) tail=previous; // if last packet, update tail
                return;
            }
        }
        DIAG(F("DCCQueue::remove slot not found"));
      
    }

    // Packet joins end of low priority queue.
    void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
        lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
    }
    
    // Packet replaces existing loco speed packet or joins end of high priority queue. 
    
    void DCCQueue::scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
        for (auto p=highPriorityQueue->head;p;p=p->next) {
            if (p->locoId==loco) {
                // replace existing packet
                memcpy(p->packet,packet,length);
                p->packetLength=length;
                p->packetRepeat=repeats;
                return;
            }
        }
        highPriorityQueue->addQueue(getSlot(SPEED_PACKET,packet,length,repeats,loco));
    }
    
    // Packet replaces existing loco function packet or joins end of high priority queue. 
    
    void DCCQueue::scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group) {
        PendingType type=DEAD_PACKET;
        switch(group) {
            case 1: type=FUNCTION1_PACKET; break;
            case 2: type=FUNCTION2_PACKET; break;
            case 3: type=FUNCTION3_PACKET; break;
            case 4: type=FUNCTION4_PACKET; break;
            case 5: type=FUNCTION5_PACKET; break;
            default:
                DIAG(F("DCCQueue::scheduleDCCFunctionPacket invalid group %d"),group);
                return; // invalid group
        }

        for (auto p=lowPriorityQueue->head;p;p=p->next) {
            if (p->locoId==loco && p->type==type) {
                // replace existing packet for same loco and function group
                memcpy(p->packet,packet,length);
                p->packetLength=length;
                p->packetRepeat=0;
                return;
            }
        }
        lowPriorityQueue->addQueue(getSlot(type,packet,length,0,loco));
    }
    
    // ESTOP -  
    // any outstanding throttle packet for this loco (all if loco=0) discarded
    // Packet joins start of queue,
   
    
    void DCCQueue::scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco) {
        
        // DIAG(F("DCC ESTOP loco=%d"),loco);
       
        // kill any existing throttle packets for this loco (or all locos if broadcast)
        // this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
        PendingSlot * pNext=nullptr;
        for (auto p=highPriorityQueue->head;p;p=pNext) {
            pNext=p->next; // save next packet in case we recycle this one                
            if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
                // remove this slot from the queue or it will interfere with our ESTOP
                highPriorityQueue->remove(p);
                recycle(p);  // recycle this slot
            }
        }
        // add the estop packet to the start of the queue
        highPriorityQueue->jumpQueue(getSlot(SPEED_PACKET,packet,length,repeats,0));
    }

    // Accessory coil-On Packet joins end of queue as normal.
    // When dequeued, packet is retained at start of queue 
    // but modified to coil-off and given the delayed start.
    // getNext will ignore this packet until the requested start time. 
    void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {
        auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);
        p->delayOff=delayms;
        lowPriorityQueue->addQueue(p);
    };

    
    // Schedule the next dcc packet from the queues or an idle packet if none pending.
    const byte idlePacket[] = {0xFF, 0x00};

    bool DCCQueue::scheduleNext(bool force) {
        if (highPriorityQueue->scheduleNextInternal()) return true;
        if (lowPriorityQueue->scheduleNextInternal()) return true;
        if (force) {
            // This will arise when there is nothing available to be sent that will not compromise the rules
            // typically this will only happen when there is only one loco in the reminders as the closely queued 
            // speed and function reminders must be separated by at least one packet not sent to that loco.
            DCCWaveform::mainTrack.schedulePacket(idlePacket,sizeof(idlePacket),0);
            lastSentPacketLocoId=0;
            return true;
        }
        return false;
    }

    bool DCCQueue::scheduleNextInternal() {

        for (auto p=head;p;p=p->next) {
            // skip over pending ACC_OFF packets which are still delayed
            if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
            if (p->locoId) {
                // Prevent two consecutive packets to the same loco. 
                // this also means repeats cant be done by waveform 
                if (p->locoId==lastSentPacketLocoId) continue;  // try again later 
                DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,0);
                lastSentPacketLocoId=p->locoId;
                if (p->packetRepeat) {
                    p->packetRepeat--;
                    return true; // leave this packet in the queue
                }
            }
            else {
                // Non loco packets can repeat automatically
                DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
                lastSentPacketLocoId=0;
            }

            // remove this slot from the queue 
            remove(p);
            
            // special cases handling 
            if (p->type == ACC_ON_PACKET) {
                // convert to a delayed off packet and jump the high priority queue
                p->type= ACC_OFF_PACKET;
                p->packet[1]  &= ~0x08; // set C to 0 (gate off) 
                p->startTime=millis()+p->delayOff;
                highPriorityQueue->jumpQueue(p);
            }
            else recycle(p); 
            return true;
        }
        
        // No packets found
        return false;
    }
    
    // obtain and initialise slot for a PendingSlot.
    PendingSlot*  DCCQueue::getSlot(PendingType type, byte* packet, byte length, byte repeats,uint16_t loco) {
        PendingSlot * p; 
        if (recycleList) {
            p=recycleList;
            recycleList=p->next;
        }
        else { 
            static int16_t created=0;
            int16_t q1=0;
            int16_t q2=0;
            for (auto p=highPriorityQueue->head;p;p=p->next) q1++;
            for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
            bool leak=(q1+q2)!=created;
            DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
                   (int16_t)type,length,loco,q1,q2, created);
            if (leak) {
                for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
                for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
            }       
            p=new Pend
Download .txt
gitextract_3uy1hr_0/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── docs.yml
│       ├── label-sponsors.yml
│       ├── main.yml
│       └── sha.yml
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── CamParser.cpp
├── CamParser.h
├── CommandDistributor.cpp
├── CommandDistributor.h
├── CommandStation-EX.ino
├── DCC.cpp
├── DCC.h
├── DCCACK.cpp
├── DCCACK.h
├── DCCConsist.cpp
├── DCCConsist.h
├── DCCDecoder.cpp
├── DCCDecoder.h
├── DCCEX.h
├── DCCEXParser.cpp
├── DCCEXParser.h
├── DCCPacket.h
├── DCCQueue.cpp
├── DCCQueue.h
├── DCCRMT.cpp
├── DCCRMT.h
├── DCCTimer.h
├── DCCTimerAVR.cpp
├── DCCTimerESP.cpp
├── DCCTimerMEGAAVR.cpp
├── DCCTimerSAMD.cpp
├── DCCTimerSTM32.cpp
├── DCCTimerTEENSY.cpp
├── DCCWaveform.cpp
├── DCCWaveform.h
├── DCCWaveformRMT.cpp
├── DIAG.h
├── Display.cpp
├── Display.h
├── DisplayInterface.cpp
├── DisplayInterface.h
├── Display_Implementation.h
├── EEStore.cpp
├── EEStore.h
├── EXRAIL.h
├── EXRAIL2.cpp
├── EXRAIL2.h
├── EXRAIL2MacroBase.h
├── EXRAIL2MacroReset.h
├── EXRAIL2Parser.cpp
├── EXRAILAsserts.h
├── EXRAILMacros.h
├── EXRAILSensor.cpp
├── EXRAILSensor.h
├── EXRAILTest.h
├── EXmDNS.cpp
├── EXmDNS.h
├── EthernetInterface.cpp
├── EthernetInterface.h
├── FSH.h
├── GITHUB_SHA.h
├── I2CManager.cpp
├── I2CManager.h
├── I2CManager_AVR.h
├── I2CManager_Mega4809.h
├── I2CManager_NonBlocking.h
├── I2CManager_SAMD.h
├── I2CManager_STM32.h
├── I2CManager_Wire.h
├── IODevice.cpp
├── IODevice.h
├── IODeviceList.h
├── IO_AnalogueInputs.h
├── IO_Bitmap.h
├── IO_DCCAccessory.cpp
├── IO_DFPlayer.h
├── IO_DFPlayerBase.h
├── IO_DFPlayerI2C.h
├── IO_DFPlayerSerial.h
├── IO_DS1307.cpp
├── IO_DS1307.h
├── IO_EXFastclock.h
├── IO_EXIOExpander.h
├── IO_EXSensorCAM.h
├── IO_EXTurntable.cpp
├── IO_EncoderThrottle.cpp
├── IO_EncoderThrottle.h
├── IO_ExampleSerial.h
├── IO_GPIOBase.h
├── IO_HALDisplay.h
├── IO_HCSR04.h
├── IO_I2CDFPlayer-OBSOLETE.h
├── IO_I2CRailcom.cpp
├── IO_I2CRailcom.h
├── IO_MCP23008.h
├── IO_MCP23017.h
├── IO_NeoPixel.h
├── IO_PCA9554.h
├── IO_PCA9555.h
├── IO_PCA9685.cpp
├── IO_PCA9685pwm.h
├── IO_PCF8574.h
├── IO_PCF8575.h
├── IO_RotaryEncoder.h
├── IO_Servo.cpp
├── IO_Servo.h
├── IO_TCA8418.h
├── IO_TM1638.cpp
├── IO_TM1638.h
├── IO_TouchKeypad.h
├── IO_VL53L0X.h
├── IO_XL9535.h
├── IO_duinoNodes.h
├── IO_trainbrains.h
├── KeywordHasher.h
├── LCN.cpp
├── LCN.h
├── LICENSE
├── LiquidCrystal_I2C.cpp
├── LiquidCrystal_I2C.h
├── LocoSlot.cpp
├── LocoSlot.h
├── MotorDriver.cpp
├── MotorDriver.h
├── MotorDrivers.h
├── Outputs.cpp
├── Outputs.h
├── README.md
├── Railcom.cpp
├── Railcom.h
├── Release - Architecture Doc/
│   ├── CommandStation-EX-Arch-v1-0.vsd
│   ├── Prod-Release-Notes.md
│   └── Rough Release-Notes.md
├── Release_Notes/
│   ├── CommandRef.md
│   ├── DCC-EX v5.4 Release Notes.xlsx
│   ├── Exrail mods.txt
│   ├── IO_Bitmap.md
│   ├── NeoPixel.md
│   ├── Railcom.md
│   ├── Stash.md
│   ├── TCA8418.md
│   ├── TM1638.md
│   ├── ThrottleAssists.md
│   ├── TrackManager.md
│   ├── consists.md
│   ├── duinoNodes.md
│   ├── momentum.md
│   ├── release_notes_v3.0.0.md
│   ├── release_notes_v3.1.0.md
│   ├── release_notes_v4.0.0.md
│   └── websocketTester.html
├── RingStream.cpp
├── RingStream.h
├── SSD1306Ascii.cpp
├── SSD1306Ascii.h
├── STM32lwipopts.h.copyme
├── SensorGroup.cpp
├── SensorGroup.h
├── Sensors.cpp
├── Sensors.h
├── SerialManager.cpp
├── SerialManager.h
├── SerialUsbLog.cpp
├── SerialUsbLog.h
├── SerialUsbLog.html.h
├── SerialUsbLog.script1.js.h
├── SerialUsbLog.script2.js.h
├── SerialUsbLog.style.css.h
├── Sniffer.cpp
├── Sniffer.h
├── Stash.cpp
├── Stash.h
├── StringBuffer.cpp
├── StringBuffer.h
├── StringFormatter.cpp
├── StringFormatter.h
├── TemplateForEnums.h
├── TrackManager.cpp
├── TrackManager.h
├── Turnouts.cpp
├── Turnouts.h
├── Turntables.cpp
├── Turntables.h
├── Websockets.cpp
├── Websockets.h
├── WiThrottle.cpp
├── WiThrottle.h
├── WifiESP32.cpp
├── WifiESP32.h
├── WifiInboundHandler.cpp
├── WifiInboundHandler.h
├── WifiInterface.cpp
├── WifiInterface.h
├── Ztest.cpp
├── Ztest.h
├── config.example.h
├── defines.h
├── docs/
│   ├── DoxyfileEXRAIL
│   ├── Makefile
│   ├── _static/
│   │   └── css/
│   │       ├── dccex_theme.css
│   │       └── sphinx_design_overrides.css
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   ├── requirements.txt
│   └── robots.txt
├── install_via_powershell.cmd
├── installer.json
├── installer.ps1
├── installer.sh
├── libsha1.cpp
├── libsha1.h
├── myAutomation.example.h
├── myEX-Turntable.example.h
├── myHal.cpp.txt
├── myHal.cpp_example.txt
├── myTrackStatus.example.h
├── objdump.bat
├── objdump.sh
├── platformio.ini
├── release_notes.md
└── version.h
Download .txt
SYMBOL INDEX (571 symbols across 129 files)

FILE: CamParser.h
  function class (line 28) | class CamParser {

FILE: CommandDistributor.cpp
  function Print (line 409) | Print * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) {

FILE: CommandDistributor.h
  function class (line 38) | class CommandDistributor {

FILE: DCC.cpp
  function byte (line 84) | byte DCC::getMomentum(LocoSlot * slot) {
  function FSH (line 554) | FSH* DCC::getMotorShieldName() {
  function normalize (line 1058) | int16_t normalize(byte speed) {
  function byte (line 1063) | byte dccalize(int16_t speed) {
  function byte (line 1156) | byte DCC::cv1(byte opcode, int cv)  {
  function byte (line 1160) | byte DCC::cv2(int cv)  {

FILE: DCC.h
  function class (line 43) | class DCC
  function setGlobalSpeedsteps (line 98) | static inline void setGlobalSpeedsteps(byte s) {
  function byte (line 102) | static inline byte getGlobalSpeedsteps() {

FILE: DCCACK.cpp
  function byte (line 155) | byte DCCACK::getAck() {

FILE: DCCACK.h
  type ackOp (line 31) | enum ackOp
  type CALLBACK_STATE (line 66) | enum   CALLBACK_STATE
  function class (line 77) | class DCCACK {

FILE: DCCConsist.h
  function class (line 5) | class DCCConsist {

FILE: DCCDecoder.h
  function class (line 23) | class DCCDecoder {

FILE: DCCEXParser.cpp
  function Print (line 1589) | Print * DCCEXParser::getAsyncReplyStream() {

FILE: DCCEXParser.h
  type DCCEXParser (line 32) | struct DCCEXParser

FILE: DCCPacket.h
  function class (line 25) | class DCCPacket {
  function right (line 60) | inline bool operator==(const DCCPacket &right) {
  function print (line 67) | void print() {
  function byte (line 77) | inline byte len() {return _len;}
  function byte (line 78) | inline byte *data() {return _data;}

FILE: DCCQueue.cpp
  function PendingSlot (line 221) | PendingSlot*  DCCQueue::getSlot(PendingType type, byte* packet, byte len...

FILE: DCCQueue.h
  type PendingType (line 26) | enum PendingType
  type PendingSlot (line 30) | struct PendingSlot {
  function class (line 44) | class DCCQueue {

FILE: DCCRMT.cpp
  function setDCCBit1 (line 67) | void setDCCBit1(rmt_item32_t* item) {
  function setDCCBit0 (line 74) | void setDCCBit0(rmt_item32_t* item) {
  function setDCCBit0Long (line 82) | void setDCCBit0Long(rmt_item32_t* item) {
  function setEOT (line 89) | void setEOT(rmt_item32_t* item) {
  function interrupt (line 99) | void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {

FILE: DCCRMT.h
  function class (line 33) | class RMTChannel {

FILE: DCCTimer.h
  function class (line 58) | class DCCTimer {
  function class (line 113) | class ADCee {

FILE: DCCTimerAVR.cpp
  function ISR (line 123) | ISR(TIMER1_OVF_vect){ interruptHandler(); }

FILE: DCCTimerESP.cpp
  function local_adc1_get_raw (line 100) | int IRAM_ATTR local_adc1_get_raw(int channel) {

FILE: DCCTimerMEGAAVR.cpp
  function ISR (line 78) | ISR(TCB0_INT_vect){

FILE: DCCTimerSAMD.cpp
  function TCC0_Handler (line 90) | void TCC0_Handler() {
  function TCC1_Handler (line 97) | void TCC1_Handler() {
  function TCC2_Handler (line 104) | void TCC2_Handler() {

FILE: DCCTimerSTM32.cpp
  function DCCTimer_Handler (line 197) | void DCCTimer_Handler() {

FILE: DCCTimerTEENSY.cpp
  function teensyRead (line 82) | void teensyRead(uint8_t word, uint8_t *mac, uint8_t offset) {

FILE: DCCWaveform.cpp
  function else (line 159) | else if (isMainTrack && railcomActive) {

FILE: DCCWaveform.h
  type WAVE_STATE (line 40) | enum  WAVE_STATE
  function class (line 52) | class DCCWaveform {

FILE: Display.cpp
  function Display (line 101) | Display *Display::loop2(bool force) {

FILE: Display.h
  function class (line 37) | class Display : public DisplayInterface {

FILE: DisplayInterface.h
  function class (line 27) | class DisplayInterface : public Print {
  function DisplayInterface (line 41) | static DisplayInterface *getDisplayHandler() {
  function getDisplayNo (line 44) | uint8_t getDisplayNo() {
  function refresh (line 50) | static void refresh() { refresh(0); }
  function setRow (line 51) | static void setRow(uint8_t line) { setRow(0, line); }
  function clear (line 52) | static void clear() { clear(0); }
  function setRow (line 57) | static void setRow(uint8_t displayNo, uint8_t line) {
  function write (line 63) | size_t write (uint8_t c) override {
  function clear (line 68) | static void clear(uint8_t displayNo) {
  function refresh (line 72) | static void refresh(uint8_t displayNo) {
  function loop (line 76) | static void loop() {
  function virtual (line 81) | virtual void begin() {}
  function virtual (line 82) | virtual size_t _write(uint8_t c) { (void)c; return 0; }
  function virtual (line 83) | virtual void _setRow(uint8_t line) { (void)line; }
  function virtual (line 84) | virtual void _clear() {}
  function virtual (line 85) | virtual void _refresh() {}
  function virtual (line 86) | virtual void _displayLoop() {}
  function class (line 89) | class DisplayDevice {

FILE: EEStore.h
  type EEStoreData (line 38) | struct EEStoreData{
  type EEStore (line 45) | struct EEStore{

FILE: EXRAIL.h
  function class (line 27) | class RMFT {
  function loop (line 30) | static void inline loop() {RMFT2::loop();}
  function class (line 37) | class RMFT {

FILE: EXRAIL2.cpp
  function LookList (line 167) | LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
  function SIGNAL_DEFINITION (line 1380) | SIGNAL_DEFINITION RMFT2::getSignalSlot(int16_t slot) {

FILE: EXRAIL2.h
  type OPCODE (line 37) | enum OPCODE
  type thrunger (line 117) | enum thrunger
  type BlinkState (line 127) | enum BlinkState: byte {
  type SignalType (line 133) | enum SignalType {
  type SIGNAL_DEFINITION (line 145) | struct SIGNAL_DEFINITION {
  function class (line 177) | class LookList {
  function class (line 196) | class RMFT2 {

FILE: EXRAILMacros.h
  function exrailHalSetup1 (line 126) | bool exrailHalSetup1() {
  function doExrailSensorGroup (line 151) | void SensorGroup::doExrailSensorGroup(GroupProcess action, Print * strea...
  function exrailHalSetup2 (line 163) | void exrailHalSetup2() {
  function FSH (line 255) | const FSH * RMFT2::getRouteDescription(int16_t id) {
  function printMessage (line 371) | void  RMFT2::printMessage(uint16_t id) {
  function FSH (line 396) | const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
  function FSH (line 411) | const FSH * RMFT2::getTurntableDescription(int16_t turntableId) {
  function FSH (line 424) | const FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, ...
  function FSH (line 451) | const FSH * RMFT2::getRosterName(int16_t id) {
  function FSH (line 462) | const FSH * RMFT2::getRosterFunctions(int16_t id) {

FILE: EXRAILSensor.h
  function class (line 24) | class EXRAILSensor {

FILE: EXmDNS.cpp
  function dotToLen (line 38) | static void dotToLen(char *str, char **substr) {
  type _DNSHeader_t (line 108) | struct _DNSHeader_t

FILE: EXmDNS.h
  type MDNSServiceProtocol_t (line 29) | typedef enum _MDNSServiceProtocol_t
  function class (line 35) | class MDNS {

FILE: EthernetInterface.h
  type netif (line 50) | struct netif
  function class (line 70) | class EthernetInterface {

FILE: FSH.h
  type __FlashStringHelper (line 47) | typedef __FlashStringHelper FSH;
  type FSH (line 87) | typedef char FSH;

FILE: I2CManager.cpp
  function FSH (line 48) | static const FSH * guessI2CDeviceType(uint8_t address) {
  function FSH (line 277) | const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {

FILE: I2CManager.h
  type I2CBus (line 143) | enum I2CBus : uint8_t {
  type I2CMux (line 149) | enum I2CMux : uint8_t {
  type I2CSubBus (line 161) | enum I2CSubBus : uint8_t {
  function I2CAddress (line 190) | struct I2CAddress {
  function operator (line 275) | int operator == (I2CAddress &a) const {
  function I2CMux (line 289) | I2CMux muxNumber() { return _muxNumber; }
  function I2CSubBus (line 290) | I2CSubBus subBus() { return _subBus; }
  function deviceAddress (line 291) | uint8_t deviceAddress() { return _deviceAddress; }
  function I2CAddress (line 299) | struct I2CAddress {
  function operator (line 337) | int operator == (I2CAddress &a) const {
  function addressWarning (line 345) | void addressWarning() {
  function class (line 398) | class I2CRB {
  function class (line 424) | class I2CManagerClass {

FILE: I2CManager_AVR.h
  function I2C_setClock (line 65) | void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
  function I2C_init (line 83) | void I2CManagerClass::I2C_init()
  function I2C_sendStart (line 97) | void I2CManagerClass::I2C_sendStart() {
  function I2C_sendStop (line 110) | void I2CManagerClass::I2C_sendStop() {
  function I2C_close (line 118) | void I2CManagerClass::I2C_close() {
  function I2C_handleInterrupt (line 130) | void I2CManagerClass::I2C_handleInterrupt() {
  function ISR (line 225) | ISR(TWI_vect) {

FILE: I2CManager_Mega4809.h
  function I2C_setClock (line 29) | void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
  function I2C_init (line 52) | void I2CManagerClass::I2C_init()
  function I2C_sendStart (line 70) | void I2CManagerClass::I2C_sendStart() {
  function I2C_sendStop (line 84) | void I2CManagerClass::I2C_sendStop() {
  function I2C_close (line 91) | void I2CManagerClass::I2C_close() {
  function I2C_handleInterrupt (line 101) | void I2CManagerClass::I2C_handleInterrupt() {
  function ISR (line 155) | ISR(TWI0_TWIM_vect) {

FILE: I2CManager_NonBlocking.h
  function _deferInterrupts (line 39) | static inline uint8_t _deferInterrupts(void) {
  function _conditionalEnableInterrupts (line 48) | static inline void _conditionalEnableInterrupts(bool *wasEnabled) {
  function _getInterruptState (line 66) | static inline bool _getInterruptState(void) {
  function _getInterruptState (line 70) | static inline bool _getInterruptState( void ) {
  function _getInterruptState (line 76) | static inline bool _getInterruptState( void ) {
  type MuxPhase (line 96) | enum MuxPhase: uint8_t {
  function _initialise (line 107) | void I2CManagerClass::_initialise()
  function _setClock (line 122) | void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
  function startTransaction (line 132) | void I2CManagerClass::startTransaction() {
  function queueRequest (line 195) | void I2CManagerClass::queueRequest(I2CRB *req) {
  function write (line 216) | uint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t *wri...
  function write_P (line 227) | uint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * ...
  function read (line 240) | uint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer...
  function setTimeout (line 255) | void I2CManagerClass::setTimeout(unsigned long value) {
  function checkForTimeout (line 264) | void I2CManagerClass::checkForTimeout() {
  function loop (line 310) | void I2CManagerClass::loop() {
  function handleInterrupt (line 322) | void I2CManagerClass::handleInterrupt() {

FILE: I2CManager_SAMD.h
  function SERCOM3_Handler (line 40) | void SERCOM3_Handler() {
  function I2C_setClock (line 53) | void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
  function I2C_init (line 93) | void I2CManagerClass::I2C_init()
  function I2C_sendStart (line 153) | void I2CManagerClass::I2C_sendStart() {
  function I2C_sendStop (line 180) | void I2CManagerClass::I2C_sendStop() {
  function I2C_close (line 187) | void I2CManagerClass::I2C_close() {
  function I2C_handleInterrupt (line 203) | void I2CManagerClass::I2C_handleInterrupt() {

FILE: I2CManager_STM32.h
  function I2C1_EV_IRQHandler (line 53) | void I2C1_EV_IRQHandler(void) {
  function I2C1_ER_IRQHandler (line 56) | void I2C1_ER_IRQHandler(void) {
  function I2C_setClock (line 110) | void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
  function I2C_init (line 168) | void I2CManagerClass::I2C_init()
  function I2C_sendStart (line 265) | void I2CManagerClass::I2C_sendStart() {
  function I2C_sendStop (line 291) | void I2CManagerClass::I2C_sendStop() {
  function I2C_close (line 298) | void I2CManagerClass::I2C_close() {
  function I2C_handleInterrupt (line 316) | void I2CManagerClass::I2C_handleInterrupt() {

FILE: I2CManager_Wire.h
  function _initialise (line 41) | void I2CManagerClass::_initialise() {
  function _setClock (line 52) | void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
  function setTimeout (line 61) | void I2CManagerClass::setTimeout(unsigned long value) {
  function muxSelect (line 72) | static uint8_t muxSelect(I2CAddress address) {
  function write (line 100) | uint8_t I2CManagerClass::write(I2CAddress address, const uint8_t buffer[...
  function write_P (line 134) | uint8_t I2CManagerClass::write_P(I2CAddress address, const uint8_t buffe...
  function read (line 146) | uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], ...
  function queueRequest (line 212) | void I2CManagerClass::queueRequest(I2CRB *req) {
  function loop (line 232) | void I2CManagerClass::loop() {}

FILE: IODevice.cpp
  function IODevice (line 369) | IODevice *IODevice::findDevice(VPIN vpin) {
  function IODevice (line 380) | IODevice *IODevice::findDeviceFollowing(VPIN vpin) {

FILE: IODevice.h
  type VPIN (line 43) | typedef uint16_t VPIN;
  function class (line 54) | class IONotifyCallback {
  function class (line 84) | class IODevice {
  function virtual (line 201) | virtual VPIN _writeAnalogueRange(VPIN vpin, int value, uint8_t param1, u...
  function virtual (line 208) | virtual int _read(VPIN vpin) {
  function virtual (line 214) | virtual int _readAnalogue(VPIN vpin) {
  function virtual (line 230) | virtual void _begin() {}
  function virtual (line 236) | virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramC...
  function virtual (line 241) | virtual int _configureAnalogIn(VPIN vpin) {
  function virtual (line 247) | virtual void _loop(unsigned long currentMicros) {
  function virtual (line 255) | virtual ~IODevice() {}
  function delayUntil (line 258) | void delayUntil(unsigned long futureMicrosCount) {
  function class (line 296) | class PCA9685 : public IODevice {
  function _read (line 540) | int _read(VPIN vpin) override {
  function _write (line 547) | void _write(VPIN vpin, int value) override {
  function _display (line 557) | void _display() override {

FILE: IO_AnalogueInputs.h
  function class (line 60) | class ADS111x: public IODevice {
  function _begin (line 75) | void _begin() {
  function _loop (line 92) | void _loop(unsigned long currentMicros) override {
  function _readAnalogue (line 139) | int _readAnalogue(VPIN vpin) override {
  function _display (line 144) | void _display() override {

FILE: IO_Bitmap.h
  function class (line 40) | class Bitmap : public IODevice {
  function _begin (line 55) | void _begin() override {
  function _read (line 60) | int _read(VPIN vpin) override {
  function _write (line 65) | void _write(VPIN vpin, int value) override {
  function _readAnalogue (line 70) | int _readAnalogue(VPIN vpin) override {
  function _writeAnalogue (line 75) | void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dura...
  function _display (line 82) | void _display() override {

FILE: IO_DFPlayer.h
  function class (line 41) | class DFPlayer : public IODevice {
  function create (line 54) | static void create(VPIN f, HardwareSerial &s, int8_t rxPin, int8_t txPin) {
  function create (line 57) | static void create(VPIN firstVpin, int nPins, HardwareSerial &serial, in...
  function create (line 64) | static void create(VPIN f, HardwareSerial &s) { create(f, 1, s); }
  function create (line 65) | static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) {

FILE: IO_DFPlayerBase.h
  function class (line 61) | class DFPlayerBase : public IODevice {
  function forceUpdate (line 108) | void forceUpdate(int val) {
  function virtual (line 124) | virtual void detectXtal() { }
  function _loop (line 126) | void _loop(unsigned long currentMicros) override {
  function processIncomingByte (line 162) | void processIncomingByte(byte c) {
  function _write (line 177) | void _write(VPIN vpin, int value) override {
  function _read (line 191) | int _read(VPIN vpin) override { (void)vpin; return _playing ? 1 : 0; }

FILE: IO_DFPlayerI2C.h
  function class (line 89) | class DFPlayerI2C : public DFPlayerBase {
  function _begin (line 118) | void _begin() override {
  function detectXtal (line 129) | void detectXtal() override {
  function _display (line 165) | void _display() override {
  function transmitCommandBuffer (line 173) | void transmitCommandBuffer(const uint8_t b[], size_t s) override {
  function processIncoming (line 181) | bool processIncoming() override {

FILE: IO_DFPlayerSerial.h
  function class (line 43) | class DFPlayerSerial : public DFPlayerBase {
  function _begin (line 53) | void _begin() override {
  function _display (line 61) | void _display() override {
  function transmitCommandBuffer (line 67) | void transmitCommandBuffer(const uint8_t buffer[], size_t bytes) override {
  function processIncoming (line 72) | bool processIncoming() override {

FILE: IO_DS1307.cpp
  function d2b (line 35) | uint8_t d2b(uint8_t d) {

FILE: IO_DS1307.h
  function class (line 36) | class DS1307 : public IODevice {

FILE: IO_EXFastclock.h
  function class (line 40) | class EXFastClock : public IODevice {
  function create (line 48) | static void create(I2CAddress i2cAddress) {
  function _loop (line 94) | void _loop(unsigned long currentMicros) override{
  function _display (line 120) | void _display() override {

FILE: IO_EXIOExpander.h
  function class (line 53) | class EXIOExpander : public IODevice {
  function _begin (line 81) | void _begin() {
  function _configure (line 178) | bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, in...
  function _configureAnalogIn (line 207) | int _configureAnalogIn(VPIN vpin) override {
  function _loop (line 226) | void _loop(unsigned long currentMicros) override {
  function _readAnalogue (line 282) | int _readAnalogue(VPIN vpin) override {
  function _read (line 296) | int _read(VPIN vpin) override {
  function _write (line 311) | void _write(VPIN vpin, int value) override {
  function _writeAnalogue (line 332) | void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dura...
  function _display (line 361) | void _display() override {

FILE: IO_EXSensorCAM.h
  function class (line 66) | class EXSensorCAM : public IODevice {
  function _begin (line 87) | void _begin() {
  function _configure (line 125) | bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, in...
  function _configureAnalogIn (line 134) | int _configureAnalogIn(VPIN vpin) override {
  function _loop (line 140) | void _loop(unsigned long currentMicros) override {
  function _readAnalogue (line 188) | int _readAnalogue(VPIN vpin) override {
  function _read (line 194) | int _read(VPIN vpin) override {
  function _write (line 201) | void _write(VPIN vpin, int value) override {
  function ioESP32 (line 210) | int ioESP32(uint8_t i2cAddr,uint8_t *rBuf,int inbytes,uint8_t *outBuff,i...
  function processIncomingPkt (line 228) | int processIncomingPkt(uint8_t *rBuf,uint8_t sensorCmd) {
  function _writeAnalogue (line 333) | void _writeAnalogue(VPIN vpin, int param1, uint8_t camop, uint16_t param...
  function _display (line 387) | void _display() override {

FILE: IO_EncoderThrottle.h
  function class (line 30) | class EncoderThrottle : public IODevice {

FILE: IO_ExampleSerial.h
  function class (line 37) | class IO_ExampleSerial : public IODevice {
  function _begin (line 71) | void _begin() override {
  function _write (line 84) | void _write(VPIN vpin, int value) {
  function _read (line 100) | int _read(VPIN vpin) {
  function _loop (line 111) | void _loop(unsigned long currentMicros) {
  function _display (line 155) | void _display() {

FILE: IO_GPIOBase.h
  function _begin (line 39) | void _begin() override;
  function virtual (line 64) | virtual void _writePortModes() {}
  function virtual (line 65) | virtual void _setupDevice() {}
  function virtual (line 66) | virtual void _processCompletion(uint8_t status) {

FILE: IO_HALDisplay.h
  type SSD1306AsciiWire (line 62) | typedef SSD1306AsciiWire OLED;
  type LiquidCrystal_I2C (line 63) | typedef LiquidCrystal_I2C LiquidCrystal;
  function create (line 89) | static void create(uint8_t displayNo, I2CAddress i2cAddress, int width, ...
  function screenUpdate (line 140) | void screenUpdate() {
  function _begin (line 178) | void _begin() override {
  function _loop (line 195) | void _loop(unsigned long) override {
  function _display (line 200) | void _display() {
  function _setRow (line 218) | void _setRow(byte line) override {
  function virtual (line 247) | virtual size_t _write(uint8_t c) override {
  function _clear (line 256) | void _clear() {

FILE: IO_HCSR04.h
  function class (line 72) | class HCSR04 : public IODevice {
  function _begin (line 122) | void _begin() override {
  function _read (line 133) | int _read(VPIN vpin) override {
  function _readAnalogue (line 138) | int _readAnalogue(VPIN vpin) override {
  function _loop (line 144) | void _loop(unsigned long currentMicros) override {
  function _display (line 236) | void _display() override {

FILE: IO_I2CDFPlayer-OBSOLETE.h
  function class (line 64) | class I2CDFPlayer : public IODevice {
  function _loop (line 160) | void _loop(unsigned long currentMicros) override {
  function processIncoming (line 199) | void processIncoming(unsigned long currentMicros) {
  function processOutgoing (line 302) | void processOutgoing(unsigned long currentMicros) {
  function _write (line 410) | void _write(VPIN vpin, int value) override {
  function _read (line 512) | int _read(VPIN vpin) override {
  function _display (line 521) | void _display() override {
  function calcChecksum (line 583) | uint16_t calcChecksum(uint8_t* packet)
  function setChecksum (line 593) | void setChecksum(uint8_t* out)
  function Init_SC16IS752 (line 616) | void Init_SC16IS752(){ // Return value is in _deviceState
  function RX_fifo_lvl (line 676) | void RX_fifo_lvl(){
  function resetRX_fifo (line 692) | void resetRX_fifo(){
  function setGPIO (line 703) | void setGPIO(){
  function TX_fifo_lvl (line 728) | void TX_fifo_lvl(){
  function UART_WriteRegister (line 738) | void UART_WriteRegister(uint8_t UART_REG, uint8_t Val){
  function UART_ReadRegister (line 748) | void UART_ReadRegister(uint8_t UART_REG){

FILE: IO_I2CRailcom.h
  function class (line 39) | class I2CRailcom : public IODevice {

FILE: IO_MCP23008.h
  function class (line 26) | class MCP23008 : public GPIOBase<uint8_t> {
  function _writeGpioPort (line 42) | void _writeGpioPort() override {
  function _writePullups (line 45) | void _writePullups() override {
  function _writePortModes (line 50) | void _writePortModes() override {
  function _readGpioPort (line 59) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 72) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 78) | void _setupDevice() override {

FILE: IO_MCP23017.h
  function class (line 31) | class MCP23017 : public GPIOBase<uint16_t> {
  function _writeGpioPort (line 46) | void _writeGpioPort() override {
  function _writePullups (line 49) | void _writePullups() override {
  function _writePortModes (line 55) | void _writePortModes() override {
  function _readGpioPort (line 64) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 77) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 84) | void _setupDevice() override {

FILE: IO_NeoPixel.h
  function class (line 128) | class NeoPixel : public IODevice {
  function _begin (line 182) | void _begin() {
  function _loop (line 209) | void _loop(unsigned long currentMicros) override {
  function _read (line 219) | int _read(VPIN vpin) override {
  function _write (line 225) | void _write(VPIN vpin, int value) override {
  function VPIN (line 239) | VPIN _writeRange(VPIN vpin,int value, int count) {
  function _writeAnalogue (line 253) | void _writeAnalogue(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t co...
  function VPIN (line 263) | VPIN _writeAnalogueRange(VPIN vpin, int colour_RG, uint8_t onoff, uint16...
  function _display (line 274) | void _display() override {
  function isPixelOn (line 283) | bool isPixelOn(int16_t pixel) {return stateBuffer[pixel/8] & (0x80>>(pix...
  function setPixelOn (line 284) | void setPixelOn(int16_t pixel) {stateBuffer[pixel/8] |= (0x80>>(pixel%8));}
  function setPixelOff (line 285) | void setPixelOff(int16_t pixel) {stateBuffer[pixel/8] &= ~(0x80>>(pixel%...
  function transmit (line 296) | void transmit(uint16_t pixel) {
  type RGB (line 324) | struct RGB {

FILE: IO_PCA9554.h
  function class (line 32) | class PCA9554 : public GPIOBase<uint8_t> {
  function _writeGpioPort (line 47) | void _writeGpioPort() override {
  function _writePullups (line 50) | void _writePullups() override {
  function _writePortModes (line 55) | void _writePortModes() override {
  function _readGpioPort (line 61) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 74) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 81) | void _setupDevice() override {

FILE: IO_PCA9555.h
  function class (line 32) | class PCA9555 : public GPIOBase<uint16_t> {
  function _writeGpioPort (line 47) | void _writeGpioPort() override {
  function _writePullups (line 50) | void _writePullups() override {
  function _writePortModes (line 55) | void _writePortModes() override {
  function _readGpioPort (line 61) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 81) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 88) | void _setupDevice() override {
  function class (line 112) | class TCA9555 {  // ALIAS for PCA9555 as they are identical for our purp...

FILE: IO_PCA9685.cpp
  type ServoData (line 53) | struct ServoData
  type ServoData (line 55) | struct ServoData
  type ServoData (line 55) | struct ServoData
  type ServoData (line 125) | struct ServoData
  type ServoData (line 155) | struct ServoData
  type ServoData (line 158) | struct ServoData
  type ServoData (line 158) | struct ServoData
  type ServoData (line 184) | struct ServoData
  type ServoData (line 201) | struct ServoData

FILE: IO_PCA9685pwm.h
  function class (line 55) | class PCA9685pwm : public IODevice {
  function _begin (line 97) | void _begin() override {
  function _writeAnalogue (line 121) | void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param...
  function _display (line 136) | void _display() override {
  function writeDevice (line 143) | void writeDevice(uint8_t pin, int value) {
  function writeRegister (line 164) | static void writeRegister(I2CAddress address, uint8_t reg, uint8_t value) {

FILE: IO_PCF8574.h
  function class (line 45) | class PCF8574 : public GPIOBase<uint8_t> {
  function _writeGpioPort (line 67) | void _writeGpioPort() override {
  function _writePullups (line 74) | void _writePullups() override { }
  function _writePortModes (line 76) | void _writePortModes() override {
  function _readGpioPort (line 83) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 96) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 104) | void _setupDevice() override {

FILE: IO_PCF8575.h
  function class (line 45) | class PCF8575 : public GPIOBase<uint16_t> {
  function _writeGpioPort (line 61) | void _writeGpioPort() override {
  function _writePullups (line 69) | void _writePullups() override { }
  function _writePortModes (line 72) | void _writePortModes() override {
  function _readGpioPort (line 79) | void _readGpioPort(bool immediate) override {
  function _processCompletion (line 92) | void _processCompletion(uint8_t status) override {
  function _setupDevice (line 100) | void _setupDevice() override {

FILE: IO_RotaryEncoder.h
  function class (line 60) | class RotaryEncoder : public IODevice {
  function _begin (line 81) | void _begin() {
  function _loop (line 116) | void _loop(unsigned long currentMicros) override {
  function _readAnalogue (line 138) | int _readAnalogue(VPIN vpin) override {
  function _write (line 145) | void _write(VPIN vpin, int value) override {
  function _writeAnalogue (line 156) | void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t d...
  function _display (line 168) | void _display() override {

FILE: IO_Servo.h
  function class (line 52) | class Servo : IODevice {
  function _begin (line 157) | void _begin() override {
  function _write (line 165) | void _write(VPIN vpin, int value) override {
  function _writeAnalogue (line 194) | void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dura...
  function _read (line 230) | int _read(VPIN vpin) override {
  function _loop (line 240) | void _loop(unsigned long currentMicros) override {
  function updatePosition (line 250) | void updatePosition(uint8_t pin) {
  function _display (line 290) | void _display() override {

FILE: IO_TCA8418.h
  function class (line 77) | class TCA8418 : public IODevice {
  function _begin (line 116) | void _begin() {
  function _read (line 180) | int _read(VPIN vpin) override {
  function _loop (line 190) | void _loop(unsigned long currentMicros) override {
  function _display (line 266) | void _display() override {
  type tca8418_registers (line 283) | enum tca8418_registers
  type tca8418_config_reg_fields (line 335) | enum tca8418_config_reg_fields
  type tca8418_int_status_fields (line 348) | enum tca8418_int_status_fields
  type tca8418_lock_ec_fields (line 358) | enum tca8418_lock_ec_fields

FILE: IO_TM1638.h
  function class (line 26) | class TM1638 : public IODevice {

FILE: IO_TouchKeypad.h
  function class (line 49) | class TouchKeypad : public IODevice {
  function _begin (line 74) | void _begin() override {
  function _read (line 87) | int _read(VPIN vpin) {
  function _loop (line 104) | void _loop(unsigned long currentMicros) {
  function _display (line 126) | void _display() {

FILE: IO_VL53L0X.h
  function class (line 98) | class VL53L0X : public IODevice {
  function _begin (line 153) | void _begin() override {
  function _loop (line 162) | void _loop(unsigned long currentMicros) override {
  function reportError (line 292) | void reportError(uint8_t status) {
  function _readAnalogue (line 299) | int _readAnalogue(VPIN vpin) override {
  function _read (line 314) | int _read(VPIN vpin) override {
  function _display (line 321) | void _display() override {
  function write_reg (line 332) | uint8_t write_reg(uint8_t reg, uint8_t data) {
  function read_reg (line 339) | uint8_t read_reg(uint8_t reg) {

FILE: IO_XL9535.h
  function class (line 33) | class XL9535 : public GPIOBase<uint16_t> {
  function _writeGpioPort (line 46) | void _writeGpioPort() override {
  function _writePullups (line 50) | void _writePullups() override {}
  function _writePortModes (line 52) | void _writePortModes() override {
  function _readGpioPort (line 57) | void _readGpioPort(bool immediate) override {
  function _setupDevice (line 62) | void _setupDevice() override {

FILE: IO_duinoNodes.h
  function class (line 33) | class IO_duinoNodes : public IODevice {
  function _begin (line 52) | void _begin() override {
  function _loop (line 62) | void _loop(unsigned long currentMicros) override {
  function _loopInput (line 67) | void _loopInput(unsigned long currentMicros)  {
  function _loopOutput (line 96) | void _loopOutput()  {
  function _read (line 108) | int _read(VPIN vpin) override {
  function _write (line 114) | void _write(VPIN vpin, int value) override {
  function _display (line 124) | void _display() override {
  function class (line 141) | class IO_DNIN8  {
  function class (line 153) | class IO_DNIN8K  {
  function class (line 164) | class IO_DNOU8 {

FILE: IO_trainbrains.h
  type TrackUnoccupancy (line 38) | enum TrackUnoccupancy
  function class (line 46) | class Trainbrains : public IODevice {
  function _dumpBuffers (line 100) | void _dumpBuffers() {
  function _begin (line 112) | void _begin() override {
  function _display (line 126) | void _display() {
  function _write (line 133) | void _write(VPIN vpin,int value) override {
  function _read (line 140) | int _read(VPIN vpin) override {
  function class (line 162) | class Trainbrains02 {

FILE: KeywordHasher.h
  function CompiletimeSeg7 (line 80) | constexpr uint32_t CompiletimeSeg7(const char *  sv, uint32_t running, s...

FILE: LCN.h
  function class (line 25) | class LCN {

FILE: LiquidCrystal_I2C.h
  function class (line 65) | class LiquidCrystal_I2C : public DisplayDevice {

FILE: LocoSlot.cpp
  function LocoSlot (line 49) | LocoSlot *  LocoSlot::getSlot(uint16_t locoId, bool autoCreate) {
  function byte (line 140) | byte LocoSlot::getSavedSpeedCode() {

FILE: LocoSlot.h
  function class (line 27) | class LocoSlot {

FILE: MotorDriver.h
  function byte (line 34) | enum TRACK_MODE : byte {
  type portreg_t (line 152) | typedef uint32_t portreg_t;
  type portreg_t (line 154) | typedef uint8_t portreg_t;
  type FASTPIN (line 157) | struct FASTPIN {
  type class (line 175) | enum class
  function class (line 177) | class MotorDriver {

FILE: Outputs.cpp
  function Output (line 122) | Output* Output::get(uint16_t n){
  type OutputData (line 153) | struct OutputData
  function Output (line 193) | Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){

FILE: Outputs.h
  type OutputData (line 28) | struct OutputData {
  function class (line 48) | class Output{

FILE: Railcom.cpp
  type ResponseType (line 40) | enum ResponseType: byte {
  function byte (line 106) | byte Railcom::getCutout() {return cutoutCounter;}

FILE: Railcom.h
  function class (line 27) | class Railcom {

FILE: RingStream.cpp
  function byte (line 135) | byte RingStream::readRawByte() {

FILE: RingStream.h
  function class (line 26) | class RingStream : public Print {

FILE: SSD1306Ascii.h
  function class (line 39) | class SSD1306AsciiWire : public DisplayDevice {

FILE: SensorGroup.h
  type GroupProcess (line 12) | enum GroupProcess:byte {prepare,print,check,dump}
  function class (line 14) | class SensorGroup {

FILE: Sensors.cpp
  function Sensor (line 210) | Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
  function Sensor (line 267) | Sensor* Sensor::get(int n){
  type SensorData (line 305) | struct SensorData

FILE: Sensors.h
  type SensorData (line 43) | struct SensorData {
  function class (line 49) | class Sensor{

FILE: SerialManager.h
  function class (line 44) | class SerialManager {

FILE: SerialUsbLog.cpp
  function startsWithNoCase (line 79) | static inline bool startsWithNoCase(const String& s, const char* prefix) {
  function String (line 92) | static String urlPathOnly(const String& uri) {
  function String (line 97) | static String queryParamRaw(const String& uri, const char* key, String d...
  function queryParamInt (line 115) | static int queryParamInt(const String& uri, const char* key, int default...
  function String (line 121) | static String uriDecode(const String& str) {
  function String (line 139) | static String queryParamString(const String& uri, const char* key, Strin...
  function drainHttpHeaders (line 145) | static void drainHttpHeaders(Client& client) {

FILE: SerialUsbLog.script1.js.h
  function function (line 14) | function atBottom(){return (logEl.scrollHeight-logEl.scrollTop-logEl.cli...

FILE: SerialUsbLog.script2.js.h
  function catch (line 29) | catch(e){

FILE: SerialUsbLog.style.css.h
  function R (line 1) | R"???(
  function header (line 5) | header h1{font-size:1rem;margin:0;flex:1;min-width:220px;white-space:now...

FILE: Sniffer.cpp
  function packeterror (line 24) | static void packeterror() {
  function clear_packeterror (line 32) | static void clear_packeterror() {
  function halfbits2byte (line 40) | static bool halfbits2byte(uint16_t b, byte *dccbyte) {
  function blink_diag (line 70) | static void IRAM_ATTR blink_diag(int limit) {
  function cap_ISR_cb (line 84) | static bool IRAM_ATTR cap_ISR_cb(mcpwm_unit_t mcpwm, mcpwm_capture_chann...

FILE: Sniffer.h
  function class (line 29) | class Sniffer {

FILE: Stash.h
  function class (line 24) | class Stash {

FILE: StringBuffer.h
  function class (line 25) | class StringBuffer : public Print {

FILE: StringFormatter.h
  function class (line 25) | class Diag {
  function class (line 38) | class StringFormatter

FILE: TemplateForEnums.h
  function T (line 21) | T operator~ (T a) { return (T)~(int)a; }

FILE: TrackManager.cpp
  function FOR_EACH_TRACK (line 123) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 194) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 279) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 318) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 340) | FOR_EACH_TRACK(t) {
  function FSH (line 447) | const FSH* TrackManager::getModeName(TRACK_MODE tm) {
  function MotorDriver (line 520) | MotorDriver * TrackManager::getProgDriver() {
  function FOR_EACH_TRACK (line 538) | FOR_EACH_TRACK(t) {
  function POWERMODE (line 593) | POWERMODE TrackManager::getProgPower() {
  function POWERMODE (line 601) | POWERMODE TrackManager::getMainPower() {
  function FOR_EACH_TRACK (line 641) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 651) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 682) | FOR_EACH_TRACK(t) {
  function FOR_EACH_TRACK (line 700) | FOR_EACH_TRACK(t) {
  function TRACK_MODE (line 736) | TRACK_MODE TrackManager::getMode(byte t) {

FILE: TrackManager.h
  function class (line 47) | class TrackManager {

FILE: Turnouts.cpp
  function Turnout (line 56) | Turnout *Turnout::get(uint16_t id) {
  function Turnout (line 168) | Turnout *Turnout::loadTurnout () {
  function Turnout (line 222) | Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPos...
  function Turnout (line 263) | Turnout *ServoTurnout::load(struct TurnoutData *turnoutData) {
  type DCCTurnoutData (line 317) | struct DCCTurnoutData {
  function Turnout (line 332) | Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) {
  function Turnout (line 353) | Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) {
  function Turnout (line 416) | Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) {
  function Turnout (line 436) | Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) {
  function Turnout (line 492) | Turnout *LCNTurnout::create(uint16_t id, bool closed) {

FILE: Turnouts.h
  function class (line 47) | class Turnout {
  function virtual (line 105) | virtual void save() {}
  function isClosed (line 125) | inline bool isClosed() { return _turnoutData.closed; }
  function isThrown (line 126) | inline bool isThrown() { return !_turnoutData.closed; }
  function isHidden (line 127) | inline bool isHidden() { return _turnoutData.hidden; }
  function setHidden (line 128) | inline void setHidden(bool h) { _turnoutData.hidden=h; }
  function isType (line 129) | inline bool isType(uint8_t type) { return _turnoutData.turnoutType == ty...
  function getId (line 130) | inline uint16_t getId() { return _turnoutData.id; }
  function Turnout (line 131) | inline Turnout *next() { return _nextTurnout; }
  function virtual (line 136) | virtual void print(Print *stream) {
  function virtual (line 139) | virtual ~Turnout() {}
  function exists (line 144) | inline static bool exists(uint16_t id) { return get(id) != 0; }
  function isThrown (line 150) | inline static bool isThrown(uint16_t id) {
  function setClosed (line 156) | inline static bool setClosed(uint16_t id) {
  function setThrown (line 160) | inline static bool setThrown(uint16_t id) {
  function Turnout (line 166) | inline static Turnout *first() { return _firstTurnout; }
  function printAll (line 176) | static bool printAll(Print *stream) {
  function class (line 194) | class ServoTurnout : public Turnout {
  function class (line 227) | class DCCTurnout : public Turnout {
  function class (line 260) | class VpinTurnout : public Turnout {
  function class (line 290) | class LCNTurnout : public Turnout {

FILE: Turntables.cpp
  function Turntable (line 104) | Turntable *Turntable::get(uint16_t id) {
  function Turntable (line 111) | Turntable *Turntable::getByVpin(VPIN vpin) {
  function Turntable (line 176) | Turntable *EXTTTurntable::create(uint16_t id, VPIN vpin) {
  function Turntable (line 232) | Turntable *DCCTurntable::create(uint16_t id) {

FILE: Turntables.h
  function else (line 43) | struct TurntablePosition {
  function TurntablePosition (line 66) | TurntablePosition* getHead() {
  function class (line 81) | class Turntable {
  function class (line 199) | class EXTTTurntable : public Turntable {
  function class (line 225) | class DCCTurntable : public Turntable {

FILE: Websockets.cpp
  function byte (line 140) | byte * Websockets::unmask(byte clientId,RingStream *ring, byte * buffer) {

FILE: Websockets.h
  function class (line 24) | class Websockets {

FILE: WiThrottle.cpp
  function WiThrottle (line 66) | WiThrottle* WiThrottle::getThrottle( int wifiClient) {
  function LOOPLOCOS (line 312) | LOOPLOCOS(throttleChar, cab) {
  function LOOPLOCOS (line 323) | LOOPLOCOS(throttleChar, cab) {
  function LOOPLOCOS (line 334) | LOOPLOCOS(throttleChar, cab) {
  function LOOPLOCOS (line 343) | LOOPLOCOS(throttleChar, cab) {
  function LOOPLOCOS (line 354) | LOOPLOCOS(throttleChar, cab) {

FILE: WiThrottle.h
  type MYLOCO (line 26) | struct MYLOCO {
  function class (line 34) | class WiThrottle {

FILE: WifiESP32.cpp
  function feedTheDog0 (line 43) | void feedTheDog0(){
  class NetworkClient (line 76) | class NetworkClient {
    method NetworkClient (line 78) | NetworkClient(WiFiClient c) {
    method active (line 82) | bool active(byte clientId) {
    method recycle (line 94) | bool recycle(WiFiClient c) {
  function wifiLoop (line 124) | void wifiLoop(void *){
  function asciitolower (line 131) | char asciitolower(char in) {

FILE: WifiESP32.h
  function class (line 28) | class WifiESP

FILE: WifiInboundHandler.h
  type INBOUND_STATE (line 39) | enum INBOUND_STATE : byte {
  function byte (line 44) | enum LOOP_STATE : byte {

FILE: WifiInterface.cpp
  function wifiSerialState (line 150) | wifiSerialState WifiInterface::setup(Stream & setupStream,  const FSH* S...
  function wifiSerialState (line 183) | wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,

FILE: WifiInterface.h
  type wifiSerialState (line 29) | enum wifiSerialState { WIFI_NOAT, WIFI_DISCONNECTED, WIFI_CONNECTED }
  function class (line 31) | class WifiInterface

FILE: Ztest.h
  function class (line 5) | class Ztest {

FILE: libsha1.cpp
  function SHA1Transform (line 61) | void SHA1Transform(uint32_t state[5], const unsigned char buffer[64])
  function SHA1Init (line 122) | void SHA1Init(SHA1_CTX* context)
  function SHA1Update (line 136) | void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len)
  function SHA1Final (line 160) | void SHA1Final(unsigned char digest[20], SHA1_CTX* context)

FILE: libsha1.h
  type SHA1_CTX (line 15) | typedef struct {
Condensed preview — 226 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,831K chars).
[
  {
    "path": ".gitattributes",
    "chars": 78,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n*.svg -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 30,
    "preview": "github: DCC-EX\npatreon: dccex\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 866,
    "preview": "name: Docs\n\non:\n  push:\n    branches:\n      - devel\n  pull_request:\n    branches: [ master ]\n  workflow_dispatch:\n\njobs:"
  },
  {
    "path": ".github/workflows/label-sponsors.yml",
    "chars": 291,
    "preview": "name: Label sponsors\non:\n  pull_request:\n    types: [opened]\n  issues:\n    types: [opened]\njobs:\n  build:\n    name: is-s"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 399,
    "preview": "name: CI\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name:"
  },
  {
    "path": ".github/workflows/sha.yml",
    "chars": 988,
    "preview": "name: SHA \n\n# Run this workflow ever time code is pushed to a branch\n# other than `main` in your repository\non: push\n\njo"
  },
  {
    "path": ".gitignore",
    "chars": 226,
    "preview": "Release/*\n.ino.cpp\n.pioenvs\n.piolibdeps\n.clang_complete\n.gcc-flags.json\n.pio/\n.vscode/\nconfig.h\nmySetup.cpp\nmyHal.cpp\nmy"
  },
  {
    "path": ".travis.yml",
    "chars": 1557,
    "preview": "# Continuous Integration (CI) is the practice, in software\n# engineering, of merging all developer working copies with a"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3321,
    "preview": "# Contributing\n\nThanks for considering contributing to our project. Here is a guide for how to get started and and a lis"
  },
  {
    "path": "CamParser.cpp",
    "chars": 5276,
    "preview": "/*\n *  © 2023-2025, Barry Daniel  \n *  © 2025       Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of "
  },
  {
    "path": "CamParser.h",
    "chars": 1194,
    "preview": "/*\n *  © 2023-2025, Barry Daniel  \n *  © 2025       Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of "
  },
  {
    "path": "CommandDistributor.cpp",
    "chars": 15785,
    "preview": "/*\n *  © 2022 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2020 Gregor Baues\n *  © 2022 Colin Murdoch\n *  All rights "
  },
  {
    "path": "CommandDistributor.h",
    "chars": 2957,
    "preview": "/*\n *  © 2022 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2020 Gregor Baues\n *  © 2022 Colin Murdoch\n * \n *  All rig"
  },
  {
    "path": "CommandStation-EX.ino",
    "chars": 7449,
    "preview": "////////////////////////////////////////////////////////////////////////////////////\n//  DCC-EX CommandStation-EX   Plea"
  },
  {
    "path": "DCC.cpp",
    "chars": 43661,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2021 Herb Morton\n *  © 2020-2022 Harald Bart"
  },
  {
    "path": "DCC.h",
    "chars": 5466,
    "preview": "/*\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2021 Herb Morton\n *  © 2020-2021 Harald Barth\n *  © 2020-2025 Chris Ha"
  },
  {
    "path": "DCCACK.cpp",
    "chars": 17574,
    "preview": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2025 Harald Barth\n *  © 2020-2022 Chris H"
  },
  {
    "path": "DCCACK.h",
    "chars": 6082,
    "preview": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2022 Chris H"
  },
  {
    "path": "DCCConsist.cpp",
    "chars": 3893,
    "preview": "#include \"DCCConsist.h\"\n#include \"LocoSlot.h\"\n#include \"StringFormatter.h\"\n#include \"DIAG.h\"\n\nbool DCCConsist::parse(Pri"
  },
  {
    "path": "DCCConsist.h",
    "chars": 448,
    "preview": "#ifndef DCCConsist_h\n#define DCCConsist_h\n\n#include <Arduino.h>\nclass DCCConsist {\n\n    public:\n      static bool parse("
  },
  {
    "path": "DCCDecoder.cpp",
    "chars": 6704,
    "preview": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistr"
  },
  {
    "path": "DCCDecoder.h",
    "chars": 1091,
    "preview": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistr"
  },
  {
    "path": "DCCEX.h",
    "chars": 1558,
    "preview": "/*\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  Th"
  },
  {
    "path": "DCCEXParser.cpp",
    "chars": 54319,
    "preview": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021-2025 Herb Morton\n *  © 2020-2025 Har"
  },
  {
    "path": "DCCEXParser.h",
    "chars": 3673,
    "preview": "/*\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is "
  },
  {
    "path": "DCCPacket.h",
    "chars": 2187,
    "preview": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistr"
  },
  {
    "path": "DCCQueue.cpp",
    "chars": 10136,
    "preview": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free "
  },
  {
    "path": "DCCQueue.h",
    "chars": 3361,
    "preview": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free "
  },
  {
    "path": "DCCRMT.cpp",
    "chars": 10226,
    "preview": "/*\n *  © 2021-2024, Harald Barth.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribut"
  },
  {
    "path": "DCCRMT.h",
    "chars": 2233,
    "preview": "/*\n *  © 2021-2022, Harald Barth.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribut"
  },
  {
    "path": "DCCTimer.h",
    "chars": 6350,
    "preview": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021-2023 Harald Barth\n *  © 2021 Fred Decker\n *  All rights "
  },
  {
    "path": "DCCTimerAVR.cpp",
    "chars": 11750,
    "preview": "/*\n *  © 2021 Mike S\n *  © 2021-2023 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021-2025 Chris Harlow\n *  © 2021 David C"
  },
  {
    "path": "DCCTimerESP.cpp",
    "chars": 10023,
    "preview": "/*\n *  © 2020-2022 Harald Barth\n *\n *  This file is part of CommandStation-EX\n *  \n *  This is free software: you can re"
  },
  {
    "path": "DCCTimerMEGAAVR.cpp",
    "chars": 5127,
    "preview": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n "
  },
  {
    "path": "DCCTimerSAMD.cpp",
    "chars": 9376,
    "preview": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021-2022 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Har"
  },
  {
    "path": "DCCTimerSTM32.cpp",
    "chars": 24520,
    "preview": "/*\n *  © 2023 Neil McKechnie\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021, 2023 Harald Barth\n *  © 2021 "
  },
  {
    "path": "DCCTimerTEENSY.cpp",
    "chars": 5085,
    "preview": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Mike S\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n *"
  },
  {
    "path": "DCCWaveform.cpp",
    "chars": 8691,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris"
  },
  {
    "path": "DCCWaveform.h",
    "chars": 4651,
    "preview": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2024 Harald Barth\n *  © 2020-2025 Chris H"
  },
  {
    "path": "DCCWaveformRMT.cpp",
    "chars": 3874,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris"
  },
  {
    "path": "DIAG.h",
    "chars": 947,
    "preview": "/*\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-"
  },
  {
    "path": "Display.cpp",
    "chars": 7332,
    "preview": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  T"
  },
  {
    "path": "Display.h",
    "chars": 2380,
    "preview": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  T"
  },
  {
    "path": "DisplayInterface.cpp",
    "chars": 1008,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation"
  },
  {
    "path": "DisplayInterface.h",
    "chars": 3479,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation"
  },
  {
    "path": "Display_Implementation.h",
    "chars": 2252,
    "preview": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n * "
  },
  {
    "path": "EEStore.cpp",
    "chars": 3539,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  © 2013"
  },
  {
    "path": "EEStore.h",
    "chars": 1480,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020 Chris Harlow\n *  All rights "
  },
  {
    "path": "EXRAIL.h",
    "chars": 1138,
    "preview": "/*\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free s"
  },
  {
    "path": "EXRAIL2.cpp",
    "chars": 51122,
    "preview": "/*\n *  © 2024 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021-2023 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © "
  },
  {
    "path": "EXRAIL2.h",
    "chars": 12924,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 20"
  },
  {
    "path": "EXRAIL2MacroBase.h",
    "chars": 30433,
    "preview": "/*\n *  © 2020-2025 Chris Harlow. All rights reserved.\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 M"
  },
  {
    "path": "EXRAIL2MacroReset.h",
    "chars": 4781,
    "preview": "/*\n *  © 2020-2025 Chris Harlow. All rights reserved.\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 M"
  },
  {
    "path": "EXRAIL2Parser.cpp",
    "chars": 12165,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021-2023 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n * "
  },
  {
    "path": "EXRAILAsserts.h",
    "chars": 7809,
    "preview": "/*\n *  © 2025 Paul M. Antoine\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Comman"
  },
  {
    "path": "EXRAILMacros.h",
    "chars": 32133,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 20"
  },
  {
    "path": "EXRAILSensor.cpp",
    "chars": 3734,
    "preview": "/*\n *  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free "
  },
  {
    "path": "EXRAILSensor.h",
    "chars": 1771,
    "preview": "/*\n *  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free "
  },
  {
    "path": "EXRAILTest.h",
    "chars": 7159,
    "preview": "// This file contains various EXRAIL tests.\n// It will need to be #included in a myAutomation.h to be executed.\n\nROUTE(7"
  },
  {
    "path": "EXmDNS.cpp",
    "chars": 6420,
    "preview": "/*\n *  © 2024 Harald Barth\n *  © 2024 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStatio"
  },
  {
    "path": "EXmDNS.h",
    "chars": 1581,
    "preview": "/*\n *  © 2024 Harald Barth\n *  © 2024 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStatio"
  },
  {
    "path": "EthernetInterface.cpp",
    "chars": 8648,
    "preview": "/*\n *  © 2024 Morten \"Doc\" Nielsen\n *  © 2023-2024 Paul M. Antoine\n *  © 2022 Bruno Sanches\n *  © 2021 Fred Decker\n *  ©"
  },
  {
    "path": "EthernetInterface.h",
    "chars": 2684,
    "preview": "/*\n *  © 2023-2024 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2024 Ha"
  },
  {
    "path": "FSH.h",
    "chars": 3699,
    "preview": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  All rights re"
  },
  {
    "path": "GITHUB_SHA.h",
    "chars": 42,
    "preview": "#define GITHUB_SHA \"master-202605011818Z\"\n"
  },
  {
    "path": "I2CManager.cpp",
    "chars": 14604,
    "preview": "/*\n *  © 2023, Neil McKechnie\n *  © 2022 Paul M Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStat"
  },
  {
    "path": "I2CManager.h",
    "chars": 20862,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  © 2022 Paul M Antoine\n *\n *  This file is part of CommandStation"
  },
  {
    "path": "I2CManager_AVR.h",
    "chars": 9262,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "I2CManager_Mega4809.h",
    "chars": 5531,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "I2CManager_NonBlocking.h",
    "chars": 15233,
    "preview": "/*\n *  © 2023, Neil McKechnie\n *  © 2022 Paul M Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStat"
  },
  {
    "path": "I2CManager_SAMD.h",
    "chars": 9700,
    "preview": "/*\n *  © 2022 Paul M Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *\n *  This file is part of CommandStat"
  },
  {
    "path": "I2CManager_STM32.h",
    "chars": 23073,
    "preview": "/*\n *  © 2022-24 Paul M Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *\n *  This file is part of CommandS"
  },
  {
    "path": "I2CManager_Wire.h",
    "chars": 9065,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "IODevice.cpp",
    "chars": 22123,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX API\n"
  },
  {
    "path": "IODevice.h",
    "chars": 19232,
    "preview": "/*\n *  © 2023, Paul Antoine, Discord user @ADUBOURG\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file "
  },
  {
    "path": "IODeviceList.h",
    "chars": 1590,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free soft"
  },
  {
    "path": "IO_AnalogueInputs.h",
    "chars": 6364,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_Bitmap.h",
    "chars": 3036,
    "preview": "/*\n *  © 2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software:"
  },
  {
    "path": "IO_DCCAccessory.cpp",
    "chars": 2387,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_DFPlayer.h",
    "chars": 2691,
    "preview": "/*\n * © 2026, Nicola Malavasi. All rights reserved.\n * © 2025-26, Chris Harlow. All rights reserved.\n * © 2023, Neil McK"
  },
  {
    "path": "IO_DFPlayerBase.h",
    "chars": 9007,
    "preview": "/*\n * © 2026, Nicola Malavasi. All rights reserved.\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McK"
  },
  {
    "path": "IO_DFPlayerI2C.h",
    "chars": 7577,
    "preview": "/*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is pa"
  },
  {
    "path": "IO_DFPlayerSerial.h",
    "chars": 2880,
    "preview": "/*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is pa"
  },
  {
    "path": "IO_DS1307.cpp",
    "chars": 4693,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free soft"
  },
  {
    "path": "IO_DS1307.h",
    "chars": 1814,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free soft"
  },
  {
    "path": "IO_EXFastclock.h",
    "chars": 3536,
    "preview": "/*\n *  © 2022, Colin Murdoch. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free sof"
  },
  {
    "path": "IO_EXIOExpander.h",
    "chars": 17956,
    "preview": "/*\n *  © 2022, Peter Cole. All rights reserved.\n *  © 2024, Harald Barth. All rights reserved.\n *\n *  This file is part "
  },
  {
    "path": "IO_EXSensorCAM.h",
    "chars": 19518,
    "preview": "/*  2024/08/14 \n *  © 2024, Barry Daniel ESP32-CAM revision \n *\n *  This file is part of EX-CommandStation\n *\n *  This i"
  },
  {
    "path": "IO_EXTurntable.cpp",
    "chars": 5354,
    "preview": "/*\n *  © 2021, Peter Cole. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free softwa"
  },
  {
    "path": "IO_EncoderThrottle.cpp",
    "chars": 4770,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free soft"
  },
  {
    "path": "IO_EncoderThrottle.h",
    "chars": 1618,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free soft"
  },
  {
    "path": "IO_ExampleSerial.h",
    "chars": 5624,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_GPIOBase.h",
    "chars": 8130,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_HALDisplay.h",
    "chars": 9152,
    "preview": "/*\n *  © 2024, Paul Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX API"
  },
  {
    "path": "IO_HCSR04.h",
    "chars": 10150,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_I2CDFPlayer-OBSOLETE.h",
    "chars": 33549,
    "preview": "   /*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free sof"
  },
  {
    "path": "IO_I2CRailcom.cpp",
    "chars": 4184,
    "preview": "   /*\n *  © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.\n *  © 2023, Neil McKechnie. All rights reserved.\n"
  },
  {
    "path": "IO_I2CRailcom.h",
    "chars": 1829,
    "preview": "/*\n *  © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.\n *  © 2023, Neil McKechnie. All rights reserved.\n * "
  },
  {
    "path": "IO_MCP23008.h",
    "chars": 3460,
    "preview": "/*\n *  © 2022 Paul M Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n"
  },
  {
    "path": "IO_MCP23017.h",
    "chars": 3825,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_NeoPixel.h",
    "chars": 13550,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free soft"
  },
  {
    "path": "IO_PCA9554.h",
    "chars": 3232,
    "preview": "/*\n *  © 2025, Paul M. Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC-EX API"
  },
  {
    "path": "IO_PCA9555.h",
    "chars": 4212,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  © 2026, Chris Harlow. All rights reserved.\n *  \n *  This file is"
  },
  {
    "path": "IO_PCA9685.cpp",
    "chars": 11077,
    "preview": "/*\n *  © 2026 Paul M. Antoine\n *  © 2026 Filip Šilar\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file"
  },
  {
    "path": "IO_PCA9685pwm.h",
    "chars": 6719,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_PCF8574.h",
    "chars": 4210,
    "preview": "/*\n *  © 2025 Herb Morton\n *  © 2022 Paul M Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file "
  },
  {
    "path": "IO_PCF8575.h",
    "chars": 4156,
    "preview": "/*\n *  © 2023, Paul Antoine, and Discord user @ADUBOURG\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This f"
  },
  {
    "path": "IO_RotaryEncoder.h",
    "chars": 7914,
    "preview": "/*\n *  © 2023, Peter Cole. All rights reserved.\n *  © 2022, Peter Cole. All rights reserved.\n *\n *  This file is part of"
  },
  {
    "path": "IO_Servo.cpp",
    "chars": 1421,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_Servo.h",
    "chars": 11595,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_TCA8418.h",
    "chars": 15889,
    "preview": "/*\n *  © 2023-2024, Paul M. Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC-E"
  },
  {
    "path": "IO_TM1638.cpp",
    "chars": 6665,
    "preview": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software"
  },
  {
    "path": "IO_TM1638.h",
    "chars": 3649,
    "preview": "   /*\n *  © 2024, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softw"
  },
  {
    "path": "IO_TouchKeypad.h",
    "chars": 4955,
    "preview": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_VL53L0X.h",
    "chars": 14176,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free softwa"
  },
  {
    "path": "IO_XL9535.h",
    "chars": 2210,
    "preview": "/*\n *  © 2026, Terry Wheatcroft & Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n * "
  },
  {
    "path": "IO_duinoNodes.h",
    "chars": 5682,
    "preview": "/*\n *  © 2022, Chris Harlow. All rights reserved.\n *  Based on original by: Robin Simonds, Beagle Bay Inc\n *  \n *  This "
  },
  {
    "path": "IO_trainbrains.h",
    "chars": 5692,
    "preview": "/*\n *  © 2023-2025, Chris Harlow. All rights reserved.\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This fi"
  },
  {
    "path": "KeywordHasher.h",
    "chars": 3600,
    "preview": "/*\n *  © 2024 Vincent Hamp and Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n"
  },
  {
    "path": "LCN.cpp",
    "chars": 2325,
    "preview": "/*\n *  © 2021, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX \n *  \n *  This "
  },
  {
    "path": "LCN.h",
    "chars": 1064,
    "preview": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-"
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "LiquidCrystal_I2C.cpp",
    "chars": 8032,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  Based on the work by DFRobot, Frank de Brabander and Marco Schwa"
  },
  {
    "path": "LiquidCrystal_I2C.h",
    "chars": 2836,
    "preview": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  Based on the work by DFRobot, Frank de Brabander and Marco Schwa"
  },
  {
    "path": "LocoSlot.cpp",
    "chars": 3938,
    "preview": "/* Copyright (c) 2023 Harald Barth\n * Copyright (c) 2025 Chris Harlow\n * This source is free software: you can redistrib"
  },
  {
    "path": "LocoSlot.h",
    "chars": 4055,
    "preview": "/* Copyright (c) 2023 Harald Barth\n * Copyright (c) 2025 Chris Harlow\n *\n * This source is free software: you can redist"
  },
  {
    "path": "MotorDriver.cpp",
    "chars": 23362,
    "preview": "/*\n *  © 2022-2024 Paul M Antoine\n *  © 2024 Herb Morton\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2023 Harald"
  },
  {
    "path": "MotorDriver.h",
    "chars": 14166,
    "preview": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  © 2022,2023 Hara"
  },
  {
    "path": "MotorDrivers.h",
    "chars": 12561,
    "preview": "/*\n *  © 2022-2023 Paul M. Antoine\n *  © 2021 Fred Decker\n *  © 2020-2024 Harald Barth\n *  (c) 2020 Chris Harlow. All ri"
  },
  {
    "path": "Outputs.cpp",
    "chars": 9246,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  © 2020-2021 Fred Decker\n *  © 2020-2021 Chris Harlow\n *  All ri"
  },
  {
    "path": "Outputs.h",
    "chars": 1864,
    "preview": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is"
  },
  {
    "path": "README.md",
    "chars": 2429,
    "preview": "# What is DCC-EX?\nDCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your"
  },
  {
    "path": "Railcom.cpp",
    "chars": 3600,
    "preview": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: y"
  },
  {
    "path": "Railcom.h",
    "chars": 1477,
    "preview": "/*\n *  © 202 5Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: y"
  },
  {
    "path": "Release - Architecture Doc/Prod-Release-Notes.md",
    "chars": 5235,
    "preview": "The DCC-EX Team is pleased to release CommandStation-EX-v3.0.0 as a Production Release.  This release is a major re-writ"
  },
  {
    "path": "Release - Architecture Doc/Rough Release-Notes.md",
    "chars": 2255,
    "preview": "# CommandStation-EX Release Notes\n\n## v3.0.0\n\n - **WiFi Support** - AP and station modes supported. Auto-detection of an"
  },
  {
    "path": "Release_Notes/CommandRef.md",
    "chars": 4850,
    "preview": "This file is being used to consolidate the command reference information. \n\nGeneral points:\n  - Commands below have a si"
  },
  {
    "path": "Release_Notes/Exrail mods.txt",
    "chars": 5146,
    "preview": "// 5.2.49 \n\nWhich is a more efficient than the AT/AFTER/IF methods \nof handling buttons and switches, especially on MIMI"
  },
  {
    "path": "Release_Notes/IO_Bitmap.md",
    "chars": 3501,
    "preview": "Virtual Bitmap device pins.\n\na Bitmap device pin is a software representation of a virtual hardware device that has the "
  },
  {
    "path": "Release_Notes/NeoPixel.md",
    "chars": 4281,
    "preview": "NeoPixel support\n\nThe IO_NeoPixel.h driver supports the adafruit neopixel seesaw board. It turns each pixel into an indi"
  },
  {
    "path": "Release_Notes/Railcom.md",
    "chars": 4135,
    "preview": "Railcom implementation notes, Chris Harlow Oct 2024\n\nRailcom support is in 3 parts\n1. Generation of the DCC waveform wit"
  },
  {
    "path": "Release_Notes/Stash.md",
    "chars": 1628,
    "preview": "# The STASH feature of exrail.\n\nSTASH is used for scenarios where it is helpful to relate a loco id to where it is parke"
  },
  {
    "path": "Release_Notes/TCA8418.md",
    "chars": 2670,
    "preview": "## TCA8418 ##\n\nThe TCA8418 IC from Texas Instruments is a low cost and very capable GPIO and keyboard scanner. Used as a"
  },
  {
    "path": "Release_Notes/TM1638.md",
    "chars": 3831,
    "preview": "## TM1638 ##\n\nThe TM1638 board provides a very cheap way of implementing 8 buttons, 8 leds and an 8 digit 7segment displ"
  },
  {
    "path": "Release_Notes/ThrottleAssists.md",
    "chars": 3719,
    "preview": "Throttle Assist updates for versiuon 4.?\n\nChris Harlow April 2022\n\nThere are a number of additional throttle information"
  },
  {
    "path": "Release_Notes/TrackManager.md",
    "chars": 10055,
    "preview": "# DCC++EX Track Manager\n\nChris Harlow 2022/03/23\n\n**If you are only interested in a standard setup using just a DCC trac"
  },
  {
    "path": "Release_Notes/consists.md",
    "chars": 2131,
    "preview": "# Consist develeopment spec/checklist\n\n## creating and managing\n\n- ```<^ leadId followerId [id...] >```  creates a CS co"
  },
  {
    "path": "Release_Notes/duinoNodes.md",
    "chars": 1674,
    "preview": "Using Lew's Duino Gear boards:\n\n1. DNIN8 Input\n   This is a shift-register implementation of a digital input collector. "
  },
  {
    "path": "Release_Notes/momentum.md",
    "chars": 1618,
    "preview": "New Momentum feature notes:\n\nThe command station can apply momentum to throttle movements in the same way that a standar"
  },
  {
    "path": "Release_Notes/release_notes_v3.0.0.md",
    "chars": 5006,
    "preview": "\n\nThe DCC-EX Team is pleased to release CommandStation-EX-v3.0.0 as a Production Release.  This release is a major re-wr"
  },
  {
    "path": "Release_Notes/release_notes_v3.1.0.md",
    "chars": 10431,
    "preview": "The DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release.  Release v3.1.0 is a minor relea"
  },
  {
    "path": "Release_Notes/release_notes_v4.0.0.md",
    "chars": 15606,
    "preview": "Version 4.0 Release Notes\n*************************\n\nThe DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a"
  },
  {
    "path": "Release_Notes/websocketTester.html",
    "chars": 1260,
    "preview": "<html>\n  <!-- Minimalist test page for the DCCEX websocket API.-->\n<head>\n  <script>\n  let socket = new WebSocket(\"ws://"
  },
  {
    "path": "RingStream.cpp",
    "chars": 6432,
    "preview": "/*\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  T"
  },
  {
    "path": "RingStream.h",
    "chars": 2098,
    "preview": "#ifndef RingStream_h\n#define RingStream_h\n/*\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is"
  },
  {
    "path": "SSD1306Ascii.cpp",
    "chars": 24609,
    "preview": "/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman\n * Modifications (C) 2021 Neil McKechnie"
  },
  {
    "path": "SSD1306Ascii.h",
    "chars": 2897,
    "preview": "/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman\n * Modifications (C) 2021 Neil McKechnie"
  },
  {
    "path": "STM32lwipopts.h.copyme",
    "chars": 3332,
    "preview": "/*\n *  © 2024 Harald Barth\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "SensorGroup.cpp",
    "chars": 2108,
    "preview": "#include \"SensorGroup.h\"\n#include \"CommandDistributor.h\"\n\n#ifdef EXRAIL_ACTIVE\n\n// called in loop to check sensors\nvoid "
  },
  {
    "path": "SensorGroup.h",
    "chars": 861,
    "preview": "#ifndef SensorGroup_h\n#define SensorGroup_h\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"IODevice.h\"\n#include \"St"
  },
  {
    "path": "Sensors.cpp",
    "chars": 12976,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n * "
  },
  {
    "path": "Sensors.h",
    "chars": 3620,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n * "
  },
  {
    "path": "SerialManager.cpp",
    "chars": 4557,
    "preview": " /*\n *  © 2022 Paul M. Antoine\n *  © 2021 Chris Harlow\n *  © 2022 2024 Harald Barth\n *  All rights reserved.\n *  \n *  Th"
  },
  {
    "path": "SerialManager.h",
    "chars": 1666,
    "preview": " /*\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software:"
  },
  {
    "path": "SerialUsbLog.cpp",
    "chars": 12461,
    "preview": "/*\n *  © 2026 Chris Harlow\n *  © 2026 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of DCC-EX Comman"
  },
  {
    "path": "SerialUsbLog.h",
    "chars": 1862,
    "preview": "/*\n *  © 2026 Chris Harlow\n *  © 2026 Paul M. Antoine\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX Comm"
  },
  {
    "path": "SerialUsbLog.html.h",
    "chars": 903,
    "preview": "R\"???(\n<!doctype html><html><head>\n      <meta name=viewport content='width=device-width,initial-scale=1'>\n      <title>"
  },
  {
    "path": "SerialUsbLog.script1.js.h",
    "chars": 1228,
    "preview": "R\"???(\nfunction dog(tag) {return document.getElementById(tag);}\nconst logEl=dog('log');\nconst pauseBtn=dog('pause');\ncon"
  },
  {
    "path": "SerialUsbLog.script2.js.h",
    "chars": 1034,
    "preview": "R\"???(\nasync function tick(){\ntry{\nif(!paused && logEl.selectionStart===logEl.selectionEnd){\n  const chunk=2048;\n  let c"
  },
  {
    "path": "SerialUsbLog.style.css.h",
    "chars": 989,
    "preview": "R\"???(\nhtml,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial}\nbody{background:#0f1115"
  },
  {
    "path": "Sniffer.cpp",
    "chars": 6990,
    "preview": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistr"
  },
  {
    "path": "Sniffer.h",
    "chars": 2087,
    "preview": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistr"
  },
  {
    "path": "Stash.cpp",
    "chars": 2474,
    "preview": "/* \n*  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: y"
  },
  {
    "path": "Stash.h",
    "chars": 1254,
    "preview": "/* \n*  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: y"
  },
  {
    "path": "StringBuffer.cpp",
    "chars": 1404,
    "preview": "/*\n *  © 2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This i"
  },
  {
    "path": "StringBuffer.h",
    "chars": 1136,
    "preview": " /*\n *  © 2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software:"
  },
  {
    "path": "StringFormatter.cpp",
    "chars": 7859,
    "preview": "/*\n *  © 2020=2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free"
  },
  {
    "path": "StringFormatter.h",
    "chars": 1947,
    "preview": "/*\n *  © 2020-2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free"
  },
  {
    "path": "TemplateForEnums.h",
    "chars": 1102,
    "preview": "/*\n *  © 2024, Harald Barth. All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you"
  },
  {
    "path": "TrackManager.cpp",
    "chars": 25996,
    "preview": "/*\n *  © 2022-2025 Chris Harlow\n *  © 2022-2024 Harald Barth\n *  © 2023-2024 Paul M. Antoine\n *  © 2024-2025 Herb Morton"
  },
  {
    "path": "TrackManager.h",
    "chars": 4892,
    "preview": "/*\n *  © 2022 Chris Harlow\n *  © 2022-2024 Harald Barth\n *  © 2023 Colin Murdoch\n *  © 2025 Herb Morton\n * \n *  All righ"
  },
  {
    "path": "Turnouts.cpp",
    "chars": 17834,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 M Steve Todd\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2021"
  },
  {
    "path": "Turnouts.h",
    "chars": 8953,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 M Steve Todd\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2022"
  },
  {
    "path": "Turntables.cpp",
    "chars": 7913,
    "preview": "/*\n *  © 2023 Peter Cole\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "Turntables.h",
    "chars": 6547,
    "preview": "/*\n *  © 2023 Peter Cole\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "Websockets.cpp",
    "chars": 7987,
    "preview": "/*\n *  © 2023-2026 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is fr"
  },
  {
    "path": "Websockets.h",
    "chars": 1293,
    "preview": "/*\n *  © 2023 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free so"
  },
  {
    "path": "WiThrottle.cpp",
    "chars": 23236,
    "preview": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 M Steve Todd\n *  © 2020-2021"
  },
  {
    "path": "WiThrottle.h",
    "chars": 3412,
    "preview": "/*\n *  © 2021 Mike S\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API"
  },
  {
    "path": "WifiESP32.cpp",
    "chars": 13382,
    "preview": "/*\n    © 2023 Paul M. Antoine\n    © 2021 Harald Barth\n    © 2023 Nathan Kellenicki\n    © 2025 Chris Harlow\n    \n\n    Thi"
  },
  {
    "path": "WifiESP32.h",
    "chars": 1211,
    "preview": "/*\n *  © 2021 Harald Barth\n *  © 2023 Nathan Kellenicki\n *\n *  This file is part of CommandStation-EX\n *\n *  This is fre"
  },
  {
    "path": "WifiInboundHandler.cpp",
    "chars": 10084,
    "preview": "/*\n *  © 2021 Fred Decker\n *  © 2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  © 2020, Chris Harlow. All rights reser"
  },
  {
    "path": "WifiInboundHandler.h",
    "chars": 2903,
    "preview": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  (c) 2021 Fred Decker.  All rights reserved.\n *  (c) 2020-2025 Chri"
  },
  {
    "path": "WifiInterface.cpp",
    "chars": 18144,
    "preview": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2022 Chris Harlow\n *  "
  },
  {
    "path": "WifiInterface.h",
    "chars": 2093,
    "preview": "/*\n *  © 2020-2021 Chris Harlow\n *  © 2020, Harald Barth.\n *  © 2023 Nathan Kellenicki\n *  All rights reserved.\n *\n *  T"
  },
  {
    "path": "Ztest.cpp",
    "chars": 1668,
    "preview": "#include <Arduino.h>\n#include \"DIAG.h\"\n#include \"Ztest.h\"\n#include \"DCCEXParser.h\"\n#include \"StringFormatter.h\"\n \nRingSt"
  },
  {
    "path": "Ztest.h",
    "chars": 222,
    "preview": "#ifndef ztest_h\n#define ztest_h\n#include \"RingStream.h\"\n\nclass Ztest {\n  public:\n    static void parse(const FSH * comma"
  },
  {
    "path": "config.example.h",
    "chars": 16313,
    "preview": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Harald Barth\n *  © 2020-2021 Fred Decker\n *  © 2"
  },
  {
    "path": "defines.h",
    "chars": 8758,
    "preview": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald B"
  },
  {
    "path": "docs/DoxyfileEXRAIL",
    "chars": 126608,
    "preview": "# Doxyfile 1.9.8\n\n# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) "
  }
]

// ... and 26 more files (download for full content)

About this extraction

This page contains the full source code of the DCC-EX/CommandStation-EX GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 226 files (1.7 MB), approximately 492.8k tokens, and a symbol index with 571 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!