[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n*.svg -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: DCC-EX\npatreon: dccex\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Docs\n\non:\n  push:\n    branches:\n      - devel\n  pull_request:\n    branches: [ master ]\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4.1.1\n\n      - name: Install Requirements\n        run: |\n          cd docs\n          python -m pip install --upgrade pip\n          pip3 install -r requirements.txt\n          sudo apt-get install doxygen\n\n      - name: Build Prod docs\n        run: |\n          cd docs\n          make html\n          touch _build/html/.nojekyll\n\n      - name: Deploy\n        uses: JamesIves/github-pages-deploy-action@ba1486788b0490a235422264426c45848eac35c6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          branch: gh-pages  # The branch the action should deploy to.\n          folder: docs/_build/html  # The folder the action should deploy.\n"
  },
  {
    "path": ".github/workflows/label-sponsors.yml",
    "content": "name: Label sponsors\non:\n  pull_request:\n    types: [opened]\n  issues:\n    types: [opened]\njobs:\n  build:\n    name: is-sponsor-label\n    runs-on: ubuntu-latest\n    steps:\n      - uses: JasonEtco/is-sponsor-label-action@v1.2.0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "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: Install Python Wheel\n      run: pip install wheel\n    - name: Install PlatformIO Core\n      run: pip install -U platformio\n    - name: Copy generic config over\n      run: cp config.example.h config.h\n    - name: Compile Command Station (AVR)\n      run: python -m platformio run\n"
  },
  {
    "path": ".github/workflows/sha.yml",
    "content": "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\njobs:\n  # Set the job key. The key is displayed as the job name\n  # when a job name is not provided\n  sha:\n    # Name the Job\n    name: Commit SHA \n    # Set the type of machine to run on\n    runs-on: ubuntu-latest\n\n    if: github.ref == 'refs/heads/master'\n    steps:\n      # Checks out a copy of your repository on the ubuntu-latest machine\n      - name: Checkout code\n        uses: actions/checkout@v2\n      \n      - name: Create SHA File\n        run: |\n          sha=$(git rev-parse --short \"$GITHUB_SHA\")\n          echo \"#define GITHUB_SHA \\\"$sha\\\"\" > GITHUB_SHA.h\n\n      - uses: EndBug/add-and-commit@v8 # You can change this to use a specific version\n        with:\n          add: 'GITHUB_SHA.h'\n          message: 'Committing a SHA'\n          commit: --amend\n\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged\n          "
  },
  {
    "path": ".gitignore",
    "content": "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\nmyFilter.cpp\nmy*.h\n!my*.example.h\ncompile_commands.json\nnewcode.txt.old\nUserAddin.txt\n_build\nvenv\n.DS_Store\n"
  },
  {
    "path": ".travis.yml",
    "content": "# Continuous Integration (CI) is the practice, in software\n# engineering, of merging all developer working copies with a shared mainline\n# several times a day < https://docs.platformio.org/page/ci/index.html >\n#\n# Documentation:\n#\n# * Travis CI Embedded Builds with PlatformIO\n#   < https://docs.travis-ci.com/user/integration/platformio/ >\n#\n# * PlatformIO integration with Travis CI\n#   < https://docs.platformio.org/page/ci/travis.html >\n#\n# * User Guide for `platformio ci` command\n#   < https://docs.platformio.org/page/userguide/cmd_ci.html >\n#\n#\n# Please choose one of the following templates (proposed below) and uncomment\n# it (remove \"# \" before each line) or use own configuration according to the\n# Travis CI documentation (see above).\n#\n\n\n#\n# Template #1: General project. Test it using existing `platformio.ini`.\n#\n\n# language: python\n# python:\n#     - \"2.7\"\n#\n# sudo: false\n# cache:\n#     directories:\n#         - \"~/.platformio\"\n#\n# install:\n#     - pip install -U platformio\n#     - platformio update\n#\n# script:\n#     - platformio run\n\n\n#\n# Template #2: The project is intended to be used as a library with examples.\n#\n\n# language: python\n# python:\n#     - \"2.7\"\n#\n# sudo: false\n# cache:\n#     directories:\n#         - \"~/.platformio\"\n#\n# env:\n#     - PLATFORMIO_CI_SRC=path/to/test/file.c\n#     - PLATFORMIO_CI_SRC=examples/file.ino\n#     - PLATFORMIO_CI_SRC=path/to/test/directory\n#\n# install:\n#     - pip install -U platformio\n#     - platformio update\n#\n# script:\n#     - platformio ci --lib=\".\" --board=ID_1 --board=ID_2 --board=ID_N\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThanks 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.\n\nBefore 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. \n\nFind 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/\n\n# Development Environment\n\nWe 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/\n\n* Download and install the latest version of the Arduino IDE\n* Download and install the latest version of Visual Studio Code from Microsoft\n* Run VSCode and click on the \"extensions\" icon on the left. Install \"PlatformIO IDE for VSCode\" and the \"Arduino Framework\" support\n\nIf 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.\n\nYou 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.\n\n# Coding Style Guidelines\n\nWe have adopted the Google style guidlines. In particular please make sure to adhere to these standards:\n\n1. All header files should have #define guards to prevent multiple inclusion.\n2. Use Unix style line endings\n3. We indent using two spaces (soft tabs)\n4. Braces\n\nFor more information just check our code or read https://google.github.io/styleguide/cppguide.html#C++_Version\n\n## Using the Repository\n\n1. Clone the repository on your local machine\n2. Create a working branch using the format \"username-featurename\" ex: \"git branch -b frightrisk-turnouts\"\n3. Commit offen, ex: \"git add .\" and then \"git commit -m \"description of your changes\"\n4. Push your changes to our repository \"git push\"\n5. When you are ready, issue a pull request for your changes to be merged into the main branch\n\n## Pull Request Process\n\n1. Ensure any install or build dependencies are removed before the end of the layer when doing a build.\n\n## Code of Conduct\n\nBe Nice\n\n### Enforcement\n\nContributors 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.\n\n## How Can I Contribute?\n\nThe 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.\n\n### Development\n### Documentation\n### WebThrottle-EX\n### Web Support\n### Organization/Coordination\n\nLinks to external documentation goes here XXX\n"
  },
  {
    "path": "CamParser.cpp",
    "content": "/*\n *  © 2023-2025, Barry Daniel  \n *  © 2025       Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n//sensorCAM parser.cpp devel. version 3.07  August 2025\n#include \"DCCEXParser.h\"\n#include \"CamParser.h\"\n#include \"FSH.h\"\n\n// The CAMVPINS array will be filled by IO_EXSensorCam HAL drivers calling\n// the CamParser::addVpin() function.\n// The CAMBaseVpin is the one to be used when commands are given without a vpin. \nVPIN CamParser::CAMBaseVpin = 0; // no vpins yet known \nVPIN CamParser::CAMVPINS[] = {0,0,0,0};  // determines max # CAM's\nint CamParser::vpcount=sizeof(CAMVPINS)/sizeof(CAMVPINS[0]);\n\nvoid CamParser::parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {\n  if (opcode!='N') return; // this is not for us. \n  if (parseN(stream,paramCount,p)) opcode=0; // we have consumed this\n  // If we fail, the caller will <X> the <N command. \n}\n     \nbool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {\n  (void)stream;  // probably unused parameter \n  if (CAMBaseVpin==0) CAMBaseVpin=CAMVPINS[0];  // default to CAM 1.\n  VPIN vpin=CAMBaseVpin;   //use current CAM selection\n\n  if (paramCount==0) { \n    DIAG(F(\"Cam base vpin:%d\"),CAMBaseVpin);\n    for (auto i=0;i<vpcount;i++){\n       if (CAMVPINS[i]==0) break;\n       DIAG(F(\"EXSensorCam #%d vpin %d\"),i+1,CAMVPINS[i]);\n    }\n    return true; \n  }\n  uint8_t camop=p[0]; // cam oprerator \n  int param1=0;\n  int16_t param3=9999;   // =0 could invoke parameter changes. & -1 gives later errors\n\n  if(camop=='C'){ \n    if(p[1]>=100) CAMBaseVpin=p[1];\n    if(p[1]<=vpcount && p[1]>0)    CAMBaseVpin=CAMVPINS[p[1]-1];\n    DIAG(F(\"CAM base Vpin: %c %d \"),p[0],CAMBaseVpin);\n    return true;\n  }\n\n  if ((camop<='a') && (camop>='A')){   //switch CAM# if p[1] or p[2] dictates (beware 'k')\n    vpin=p[1];\n    if(camop != 'A')   \n      if(p[2] < vpcount*100+99) { vpin=(p[1] > p[2]) ? p[1] : p[2] ;   //get the larger. \n        p[2]=p[2]%100;       //strip off any CAM #\n      }\n    if((vpin>=100) && (int(vpin)<=vpcount*100+99)) {    //limits to CAM# 1 to vpcount\n      CAMBaseVpin=CAMVPINS[vpin/100-1];     \n      DIAG(F(\"switching to CAM %d baseVpin:%d\"),vpin/100,CAMBaseVpin);     \n      p[1]=p[1]%100;       //strip off any CAM #\n    } \n    vpin=CAMBaseVpin;\n  }\n\n  if (CAMBaseVpin==0) return false; // no cam defined \n\n      // send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser  \n  switch(paramCount) {    \n    case 1:                          //<N ver> produces '^'\n      if (STRCHR_P((const char *)F(\"EFGMQRVW^\"),camop) == nullptr) return false;\n      if (camop=='Q') param3=10;     //<NQ> for activation state of all 10 banks of sensors\n      if (camop=='F') camop=']';     //<NF> for Reset/Finish webCAM.\n      break;    // F Coded as ']' else conflicts with <Nf %%>\n    \n    case 2:                          //<N camop p1>  \n      if (STRCHR_P((const char *)F(\"ABFHILMNOPQRSTUV\"),camop)==nullptr) return false;\n      param1=p[1];\n      break;\n    \n    case 3:              //<N vpin rowY colx > or <N cmd p1 p2>\n      if (p[0]>=100) {   //vpin - i.e. NOT 'A' through 'Z'\n        if (p[1]>236 || p[1]<0) return false;     //row\n        if (p[2]>316 || p[2]<0) return false;     //column\n        camop=0x80;      // special 'a' case for IO_SensorCAM\n        vpin = p[0];\n      }else if (STRCHR_P((const char *)F(\"IJMNT\"),camop) == nullptr) return false; \n      camop=p[0];\n      param1 = p[1];  \n      param3 = p[2];\n      break;\n    \n    case 4:          //<N a id row col> \n      if (camop!='A') return false;          //must start with 'a' \n      if (p[3]>316 || p[3]<0) return false;\n      if (p[2]>236 || p[2]<0) return false;\n      if (p[1]>97 || p[1]<0) return false;   //treat as bsNo.\n      vpin = vpin + (p[1]/10)*8 + p[1]%10;   //translate p[1]\n      camop=0x80;    // special 'a' case for IO_SensorCAM\n      param1=p[2];   // row\n      param3=p[3];   // col\n      break;\n\n    default:\n      return false;\n  }\n  DIAG(F(\"CamParser: %d %c %d %d\"),vpin,camop,param1,param3);\n  IODevice::writeAnalogue(vpin,param1,camop,param3);\n  return true;\n}\n\nvoid CamParser::addVpin(VPIN pin) {\n  // called by IO_EXSensorCam starting up a camera on a vpin\n  byte slot=255;\n  for (auto i=0;i<vpcount && slot==255;i++) {\n    if (CAMVPINS[i]==0) {\n      slot=i;\n      CAMVPINS[slot]=pin;\n    }\n  }\n  if (slot==255) {\n    DIAG(F(\"No more than %d cameras supported\"),vpcount);\n    return;\n  }\n  if (slot==0) CAMBaseVpin=pin;\n  DIAG(F(\"CamParser Registered cam #%dvpin %d\"),slot+1,pin);\n  // tell the DCCEXParser that we wish to filter commands\n  DCCEXParser::setCamParserFilter(&parse);\n}"
  },
  {
    "path": "CamParser.h",
    "content": "/*\n *  © 2023-2025, Barry Daniel  \n *  © 2025       Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n#ifndef CamParser_H\n#define CamParser_H\n#include <Arduino.h>\n#include \"IODevice.h\"\n\nclass CamParser {\n   public:\n   static void parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);\n   static void addVpin(VPIN pin);\n   private:\n    static bool parseN(Print * stream, byte paramCount, int16_t p[]);\n    static VPIN CAMBaseVpin;\n    static VPIN CAMVPINS[];\n    static int vpcount;\n};\n\n\n#endif"
  },
  {
    "path": "CommandDistributor.cpp",
    "content": "/*\n *  © 2022 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2020 Gregor Baues\n *  © 2022 Colin Murdoch\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <Arduino.h>\n#include \"CommandDistributor.h\"\n#include \"SerialManager.h\"\n#include \"WiThrottle.h\"\n#include \"DIAG.h\"\n#include \"defines.h\"\n#include \"DCCWaveform.h\"\n#include \"DCC.h\"\n#include \"TrackManager.h\"\n#include \"StringFormatter.h\"\n#include \"Websockets.h\"\n#include \"LocoSlot.h\"\n\n// variables to hold clock time\nint16_t lastclocktime;\nint8_t lastclockrate;\n\n\n#if WIFI_ON || ETHERNET_ON || defined(SERIAL1_COMMANDS) || defined(SERIAL2_COMMANDS) || defined(SERIAL3_COMMANDS) || defined(SERIAL4_COMMANDS) || defined(SERIAL5_COMMANDS) || defined(SERIAL6_COMMANDS)\n// use a buffer to allow broadcast\nStringBuffer * CommandDistributor::broadcastBufferWriter=new StringBuffer(256);\ntemplate<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){\n  broadcastBufferWriter->flush();\n  StringFormatter::send(broadcastBufferWriter, msg...);\n  broadcastToClients(type);\n  if (type==COMMAND_TYPE) broadcastToClients(WEBSOCKET_TYPE);\n}\n#else\n// on a single USB connection config, write direct to Serial and ignore flush/shove\ntemplate<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){\n  (void)type; //shut up compiler warning\n  StringFormatter::send(&USB_SERIAL, msg...);\n}\n#endif \n\n#ifdef CD_HANDLE_RING\n  // wifi or ethernet ring streams with multiple client types\n  RingStream *  CommandDistributor::ring=0;\nCommandDistributor::clientType  CommandDistributor::clients[MAX_NUM_TCP_CLIENTS]={ NONE_TYPE }; // 0 is and must be NONE_TYPE\n\n// Parse is called by Withrottle or Ethernet interface to determine which\n// protocol the client is using and call the appropriate part of dcc++Ex\nvoid  CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {\n  if (clientId>=sizeof (clients)) {\n    // Caution, diag dump of buffer could corrupt ringstream\n    // if headed by websocket bytes. \n    DIAG(F(\"::parse invalid client=%d\"),clientId);\n    return;\n  }\n  ring=stream;\n\n  // First check if the client is not known\n  // yet and in that case determinine type\n  // NOTE: First character of transmission determines if this\n  // client is using the DCC++ protocol where all commands start\n  // with '<'\n  if (clients[clientId] == NONE_TYPE) {\n    auto websock=Websockets::checkConnectionString(clientId,buffer,stream);\n    if (websock) {\n      clients[clientId]=WEBSOCK_CONNECTING_TYPE;\n      // websockets will have replied already \n      return;\n    }\n    if (buffer[0] == '<')\n      clients[clientId]=COMMAND_TYPE;\n    else\n      clients[clientId]=WITHROTTLE_TYPE;\n  }\n\n  // after first inbound transmission the websocket is connected\n  if (clients[clientId]==WEBSOCK_CONNECTING_TYPE)\n      clients[clientId]=WEBSOCKET_TYPE;\n      \n      \n  // mark buffer that is sent to parser\n  // When type is known, send the string\n  // to the right parser\n  if (clients[clientId] == COMMAND_TYPE) {\n    ring->mark(clientId);\n    DCCEXParser::parse(stream, buffer, ring);\n  } else if (clients[clientId] == WITHROTTLE_TYPE) {\n    ring->mark(clientId);\n    WiThrottle::getThrottle(clientId)->parse(ring, buffer);\n  }\n  else if (clients[clientId] == WEBSOCKET_TYPE) {\n    buffer=Websockets::unmask(clientId,ring, buffer);\n    if (!buffer) return; // unmask may have handled it alrerday (ping/pong)\n    // mark ring with client flagged as websocket for transmission later\n    ring->mark(clientId | Websockets::WEBSOCK_CLIENT_MARKER);\n    DCCEXParser::parse(stream, buffer, ring);\n    }\n\n  if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {\n    // The commit call will either write the length bytes\n    // OR rollback to the mark because the reply is empty\n    // or the command generated more output than fits in\n    // the buffer\n    if (!ring->commit()) {\n      DIAG(F(\"OUTBOUND FULL for client %d processing cmd:%s\"),clientId, buffer);\n    }\n  } else {\n    DIAG(F(\"CD parse: was alredy committed\")); //XXX Could have been committed by broadcastClient?!\n  }\n}\n\nvoid CommandDistributor::forget(byte clientId) {\n  if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);\n  clients[clientId]=NONE_TYPE;\n  if (virtualLCDClient==clientId) virtualLCDClient=RingStream::NO_CLIENT;\n}\n#endif \n\n// This will not be called on a uno \nvoid CommandDistributor::broadcastToClients(clientType type) {\n\n  byte rememberClient;\n  (void)rememberClient; // shut up compiler warning\n\n  // Broadcast to Serials\n  if (type==COMMAND_TYPE) SerialManager::broadcast(broadcastBufferWriter->getString());\n\n#ifdef CD_HANDLE_RING\n  // If we are broadcasting from a wifi/eth process we need to complete its output\n  // before merging broadcasts in the ring, then reinstate it in case\n  // the process continues to output to its client.\n  if (ring) {\n    if ((rememberClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {\n      //DIAG(F(\"CD precommit client %d\"), rememberClient);\n      ring->commit();\n    }\n    // loop through ring clients\n    for (byte clientId=0; clientId<sizeof(clients); clientId++) {\n      if (clients[clientId]==type)  {\n\t//DIAG(F(\"CD mark client %d\"), clientId);\n\tring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));\n\tring->print(broadcastBufferWriter->getString());\n\t//DIAG(F(\"CD commit client %d\"), clientId);\n\tring->commit();\n      }\n    }\n    // at this point ring is committed (NO_CLIENT) either from\n    // 4 or 13 lines above.\n    if (rememberClient != RingStream::NO_CLIENT) {\n      //DIAG(F(\"CD postmark client %d\"), rememberClient);\n      ring->mark(rememberClient);\n    }\n  }\n#endif\n}\n\n// Public broadcast functions below \nvoid  CommandDistributor::broadcastSensor(int16_t id, bool on ) {\n  broadcastReply(COMMAND_TYPE, F(\"<%c %d>\\n\"), on?'Q':'q', id);\n}\n\nvoid  CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {\n  // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;\n  // The string below contains serial and Withrottle protocols which should\n  // be safe for both types.\n  broadcastReply(COMMAND_TYPE, F(\"<H %d %d>\\n\"),id, !isClosed);\n#ifdef CD_HANDLE_RING\n  broadcastReply(WITHROTTLE_TYPE, F(\"PTA%c%d\\n\"), isClosed?'2':'4', id);\n#endif\n}\n\nvoid CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) {\n  broadcastReply(COMMAND_TYPE, F(\"<I %d %d %d>\\n\"), id, position, moving);\n}\n\nvoid  CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {\n  // The JMRI clock command is of the form : PFT65871<;>4\n  // The CS broadcast is of the form \"<jC mmmm nn\" where mmmm is time minutes and dd speed\n  // The string below contains serial and Withrottle protocols which should\n  // be safe for both types.\n  broadcastReply(COMMAND_TYPE, F(\"<jC %d %d>\\n\"),time, rate);\n#ifdef CD_HANDLE_RING\n  broadcastReply(WITHROTTLE_TYPE, F(\"PFT%l<;>%d\\n\"), (int32_t)time*60, rate);\n#endif\n}\n\nvoid CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate) {\n  // save the latest time if changed\n      if (clocktime != lastclocktime){\n        auto difference = clocktime - lastclocktime;\n        if (difference<0) difference+=1440;\n        DCC::setTime(clocktime,clockrate,difference>2);\n        byte hh=clocktime/60;\n        byte mm=clocktime%60;\n        if (hh>23) hh=0;\n        LCD(6,clockrate<=1?F(\"Time %d%d:%d%d\"):F(\"Time %d%d:%d%d (%d)\"),\n             hh/10, hh%10, mm/10, mm%10, clockrate);\n\n        // look for an event for this time\n#ifdef EXRAIL_ACTIVE        \n        RMFT2::clockEvent(clocktime,1);\n#endif        \n        // Now tell everyone else what the time is.\n        CommandDistributor::broadcastClockTime(clocktime, clockrate);\n        lastclocktime = clocktime;\n        lastclockrate = clockrate;\n      }\n    }\n\nint16_t CommandDistributor::retClockTime() {\n  return lastclocktime;\n}\n\nvoid  CommandDistributor::broadcastLoco(LocoSlot *  sp) {\n  if (!sp) {\n    broadcastReply(COMMAND_TYPE,F(\"<l 0 -1 128 0>\\n\"));\n    return;\n\t}\n\n  // Broadcast the given loco.\n  // If its a consist leader, broadcast all followers too.\n  // A consist follower will only be broadcast because its functions have changed.\n  \n  bool isFollower=sp->isConsistFollower();\n  \n  #ifdef CD_HANDLE_RING\n  // Use the buffer directly to avoid multiple transmits in the case of a consist\n  broadcastBufferWriter->flush();\n  for (auto slot=sp; slot; slot=slot->getConsistNext()) {\n    StringFormatter::send(broadcastBufferWriter, F(\"<l %d 0 %d %l>\\n\"), \n      slot->getLoco(),slot->getTargetSpeed(),slot->getFunctions());\n    if (isFollower) break;  // dont follow next chain if original call was for a follower\n  }\n  broadcastToClients(COMMAND_TYPE);\n  broadcastToClients(WEBSOCKET_TYPE);\n  \n#else\n  // no ring handling, just broadcast each separately\n  for (auto slot=sp; slot; slot=slot->getConsistNext()) {\n    broadcastReply(COMMAND_TYPE, F(\"<l %d 0 %d %l>\\n\"), \n      slot->getLoco(),slot->getTargetSpeed(),slot->getFunctions());\n    if (isFollower) break;  // dont follow next chain if original call was for a follower\n  }\n  #endif\n\n  #ifdef SABERTOOTH\n  if (Serial2 && sp->loco == SABERTOOTH) {\n    static uint8_t rampingmode = 0;\n    auto speedCode=sp->getSpeedCode();\n    bool direction = (speedCode & 0x80) !=0; // true for forward\n    int32_t speed = speedCode & 0x7f;\n    if (speed == 1) { // emergency stop\n      if (rampingmode != 1) {\n\trampingmode = 1;\n\tSerial2.print(\"R1: 0\\r\\n\");\n\tSerial2.print(\"R2: 0\\r\\n\");\n      }\n      Serial2.print(\"MD: 0\\r\\n\");\n    } else {\n      if (speed != 0) {\n        // speed is here 2 to 127\n\tspeed = (speed - 1) * 1625 / 100;\n\tspeed = speed * (direction ? 1 : -1);\n\t// speed is here -2047 to 2047\n      }\n      if (rampingmode != 2) {\n\trampingmode = 2;\n\tSerial2.print(\"R1: 2047\\r\\n\");\n\tSerial2.print(\"R2: 2047\\r\\n\");\n      }\n      Serial2.print(\"M1: \");\n      Serial2.print(speed);\n      Serial2.print(\"\\r\\n\");\n      Serial2.print(\"M2: \");\n      Serial2.print(speed);\n      Serial2.print(\"\\r\\n\");\n    }\n  }\n#endif\n#ifdef CD_HANDLE_RING\n  WiThrottle::markForBroadcast(sp->getLoco());\n#endif\n}\n\nvoid  CommandDistributor::broadcastForgetLoco(int16_t loco) {\n  broadcastReply(COMMAND_TYPE, F(\"<l %d 0 1 0>\\n<- %d>\\n\"), loco,loco);\n}\n\nvoid  CommandDistributor::broadcastPower() {\n  char pstr[] = \"? x\";\n  byte trackcount=0;\n  byte oncount=0;\n  byte offcount=0;\n  uint8_t numTracks = TrackManager::numTracks();\n  {\n    char trackLetter[numTracks+1];\n    trackLetter[numTracks] = '\\0';\n\n    for(byte t=0; t<numTracks; t++) {\n      if (TrackManager::getPower(t, pstr))\n\tbroadcastReply(COMMAND_TYPE, F(\"<p%s>\\n\"),pstr);\n      if (TrackManager::isActive(t)) {\n\ttrackcount++;\n\t// do not call getPower(t) unless isActive(t)!\n\tif (TrackManager::getPower(t) == POWERMODE::ON) {\n\t  oncount++;\n\t  trackLetter[t] = t + 'A';\n\t} else if (TrackManager::getPower(t) == POWERMODE::OFF) {\n\t  offcount++;\n\t  trackLetter[t] = t + 'a';\n\t} else {\n\t  trackLetter[t] = 'X';\n\t}\n      } else {\n\ttrackLetter[t] = '_';\n      }\n    }\n    //DIAG(F(\"t=%d on=%d off=%d\"), trackcount, oncount, offcount);\n\n    char state='2';\n    if (oncount==0 || offcount == trackcount) // none on or all active off\n      state = '0';\n    else if (oncount == numTracks) {          // all on, no inactive tracks\n      state = '1';\n    }\n\n    if (state != '2')\n      broadcastReply(COMMAND_TYPE, F(\"<p%c>\\n\"),state);\n\n    // additional info about MAIN, PROG and JOIN\n    bool main=TrackManager::getMainPower()==POWERMODE::ON;\n    bool prog=TrackManager::getProgPower()==POWERMODE::ON;\n    bool join=TrackManager::isJoined();\n    //DIAG(F(\"m=%d p=%d j=%d\"), main, prog, join);\n    const FSH * reason=F(\"\");\n    if (join) {\n      reason = F(\" JOIN\"); // with space at start so we can append without space\n      broadcastReply(COMMAND_TYPE, F(\"<p1%S>\\n\"),reason);\n    } else {\n      if (main) {\n\t//reason = F(\"MAIN\");\n\tbroadcastReply(COMMAND_TYPE, F(\"<p1 MAIN>\\n\"));\n      }\n      if (prog) {\n\t//reason = F(\"PROG\");\n\tbroadcastReply(COMMAND_TYPE, F(\"<p1 PROG>\\n\"));\n      }\n    }\n#ifdef CD_HANDLE_RING\n    // send '1' if all main are on, otherwise global state (which in that case is '0' or '2')\n    broadcastReply(WITHROTTLE_TYPE, F(\"PPA%c\\n\"), main?'1': state);\n#endif\n#if defined(HAS_ENOUGH_MEMORY)\n    LCD(2,F(\"PWR %s%S\"),state=='1'? \"On\" : ( state=='0'? \"Off\" : trackLetter ),reason);\n#else\n    LCD(2,F(\"PWR %s%S\"),trackLetter ,reason);\n#endif\n  }\n}\n\nvoid CommandDistributor::broadcastRaw(clientType type, char * msg) {\n  broadcastReply(type, F(\"%s\"),msg);\n}\n\nvoid CommandDistributor::broadcastEstopLock(bool locked) {\n  if (locked) {\n    broadcastReply(COMMAND_TYPE, F(\"<!PAUSED>\\n\"));\n    broadcastReply(WITHROTTLE_TYPE, F(\"HmESTOP PAUSED\\n\"));\n  } else {\n    broadcastReply(COMMAND_TYPE, F(\"<!RESUMED>\\n\"));\n    broadcastReply(WITHROTTLE_TYPE, F(\"HmESTOP RESUMED\\n\"));\n  }\n}\n\nvoid CommandDistributor::broadcastMessage(char * message) {\n  broadcastReply(COMMAND_TYPE, F(\"<m \\\"%s\\\">\\n\"),message);\n  broadcastReply(WITHROTTLE_TYPE, F(\"Hm%s\\n\"),message);\n}\n\nvoid CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {\n  broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);\n}\n\nvoid  CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {\n  broadcastReply(COMMAND_TYPE, F(\"<jB %d %d>\\n\"),routeId,state);\n}\n\nvoid  CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {\n  broadcastReply(COMMAND_TYPE, F(\"<jB %d \\\"%S\\\">\\n\"),routeId,caption);\n}\n\nPrint * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) {\n  Print * stream=virtualLCDSerial;\n  #ifdef  CD_HANDLE_RING\n  rememberVLCDClient=RingStream::NO_CLIENT;\n  if (!stream && virtualLCDClient!=RingStream::NO_CLIENT) {\n    // If we are broadcasting from a wifi/eth process we need to complete its output\n    // before merging broadcasts in the ring, then reinstate it in case\n    // the process continues to output to its client.\n    if ((rememberVLCDClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {\n      ring->commit();\n    }\n    ring->mark(virtualLCDClient);   \n    stream=ring; \n  }\n  #endif\n  if (stream) StringFormatter::send(stream,F(\"<@ %d %d \\\"\"), screen,row);\n  return stream;  \n}\n\nvoid CommandDistributor::commitVirtualLCDSerial() {\n  #ifdef  CD_HANDLE_RING\n  if (virtualLCDClient!=RingStream::NO_CLIENT) {\n    StringFormatter::send(ring,F(\"\\\">\\n\"));\n    ring->commit();\n    if (rememberVLCDClient!=RingStream::NO_CLIENT) ring->mark(rememberVLCDClient);\n    return;  \n   }\n  #endif\n  StringFormatter::send(virtualLCDSerial,F(\"\\\">\\n\"));  \n}\n\nvoid CommandDistributor::setVirtualLCDSerial(Print * stream) {\n  #ifdef  CD_HANDLE_RING\n  virtualLCDClient=RingStream::NO_CLIENT;\n  if (stream && stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM) {\n     virtualLCDClient=((RingStream *) stream)->peekTargetMark();\n     virtualLCDSerial=nullptr;\n     return;\n  }      \n    #endif\n  virtualLCDSerial=stream;\n}\n\nPrint* CommandDistributor::virtualLCDSerial=&USB_SERIAL;\nbyte CommandDistributor::virtualLCDClient=0xFF;\nbyte CommandDistributor::rememberVLCDClient=0;\n"
  },
  {
    "path": "CommandDistributor.h",
    "content": "/*\n *  © 2022 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2020 Gregor Baues\n *  © 2022 Colin Murdoch\n * \n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef CommandDistributor_h\n#define CommandDistributor_h\n#include \"DCCEXParser.h\"\n#include \"RingStream.h\"\n#include \"StringBuffer.h\"\n#include \"defines.h\"\n#include \"EXRAIL2.h\"\n#include \"DCC.h\"\n\n#if WIFI_ON | ETHERNET_ON \n  // Command Distributor must handle a RingStream of clients\n  #define CD_HANDLE_RING\n#endif \n\nclass CommandDistributor {\npublic:\n  enum clientType: byte {NONE_TYPE = 0,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE}; // independent of other types, NONE_TYPE must be 0\nprivate:\n  static void broadcastToClients(clientType type);\n  static StringBuffer * broadcastBufferWriter;\n  #ifdef CD_HANDLE_RING\n    static RingStream * ring;\n    static clientType clients[MAX_NUM_TCP_CLIENTS];\n  #endif\npublic :\n  static void parse(byte clientId,byte* buffer, RingStream * ring);\n  static void broadcastLoco(LocoSlot * slot);\n  static void broadcastForgetLoco(int16_t loco);\n  static void broadcastSensor(int16_t id, bool value);\n  static void broadcastTurnout(int16_t id, bool isClosed);\n  static void broadcastTurntable(int16_t id, uint8_t position, bool moving);\n  static void broadcastClockTime(int16_t time, int8_t rate);\n  static void setClockTime(int16_t time, int8_t rate);\n  static int16_t retClockTime();\n  static void broadcastPower();\n  static void broadcastRaw(clientType type,char * msg);\n  static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);\n  template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);\n  static void forget(byte clientId);\n  static void broadcastRouteState(int16_t routeId,byte state);\n  static void broadcastRouteCaption(int16_t routeId,const FSH * caption);\n  static void broadcastMessage(char * message);\n  static void broadcastEstopLock(bool locked); \n  \n  // Handling code for virtual LCD receiver.\n  static Print * getVirtualLCDSerial(byte screen, byte row);\n  static void commitVirtualLCDSerial();\n  static void setVirtualLCDSerial(Print * stream); \n  private:\n    static Print * virtualLCDSerial;\n    static byte virtualLCDClient;\n    static byte rememberVLCDClient;\n};\n\n#endif\n"
  },
  {
    "path": "CommandStation-EX.ino",
    "content": "////////////////////////////////////////////////////////////////////////////////////\n//  DCC-EX CommandStation-EX   Please see https://DCC-EX.com\n//\n// This file is the main sketch for the Command Station.\n//\n// CONFIGURATION:\n// Configuration is normally performed by editing a file called config.h.\n// This file is NOT shipped with the code so that if you pull a later version\n// of the code, your configuration will not be overwritten.\n//\n// If you used the automatic installer program, config.h will have been created automatically.\n//\n// If config.h is not found, the command station will build with no motor shield.\n////////////////////////////////////////////////////////////////////////////////////\n\n#if __has_include ( \"config.h\")\n  #include \"config.h\"\n#else \n  #warning config.h not found.\n#endif \n#ifndef MOTOR_SHIELD_TYPE\n  #warning MOTOR_SHIELD_TYPE not found. Building with no motor shield\n  #define MOTOR_SHIELD_TYPE NO_SHIELD \n#endif\n\n/*\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Chris Harlow, Harald Barth, David Cutting,\n *  Fred Decker, Gregor Baues, Anthony W - Dayton\n *  © 2023 Nathan Kellenicki\n *  © 2025 Herb Morton\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"DCCEX.h\"\n#include \"Display_Implementation.h\"\n#ifdef ARDUINO_ARCH_ESP32\n#include \"Sniffer.h\"\n#include \"DCCDecoder.h\"\nSniffer *dccSniffer = NULL;\nbool DCCDecoder::active = false;\n#endif // ARDUINO_ARCH_ESP32\n\n#define QWRAP_(x) QWRAP__(x)\n#define QWRAP__(x) #x\nstatic_assert(MAX_LOCOS >1 && MAX_LOCOS<256, \"#define MAX_LOCOS \" QWRAP_(MAX_LOCOS) \" must be >1 and <256\");\n\n#ifdef CPU_TYPE_ERROR\n#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h\n#endif\n\n#ifdef WIFI_WARNING\n#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED\n#endif\n#ifdef ETHERNET_WARNING\n#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED\n#endif\n#ifdef EXRAIL_WARNING\n#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED\n#endif\n// compile time check, passwords 1 to 7 chars do not work, so do not try to compile with them at all\n// remember trailing '\\0', sizeof(\"\") == 1.\n#define PASSWDCHECK(S) static_assert(sizeof(S) == 1 || sizeof(S) > 8, \"Password shorter than 8 chars\")\n\nvoid setup()\n{\n  // The main sketch has responsibilities during setup()\n\n  // Responsibility 1: Start the usb connection for diagnostics\n  // This is normally Serial but uses SerialUSB on a SAMD processor\n  SerialManager::init();\n\n  DIAG(F(\"License GPLv3 fsf.org (c) dcc-ex.com\"));\n\n// If user has defined a startup delay, delay here before starting IO\n#if defined(STARTUP_DELAY)\n  DIAG(F(\"Delaying startup for %dms\"), STARTUP_DELAY);\n  delay(STARTUP_DELAY);\n#endif\n\n// Initialise HAL layer before reading EEprom or setting up MotorDrivers \n  IODevice::begin();\n\n  // As the setup of a motor shield may require a read of the current sense input from the ADC,\n  // let's make sure to initialise the ADCee class!\n  ADCee::begin();\n  // Set up MotorDrivers early to initialize all pins\n  TrackManager::Setup(MOTOR_SHIELD_TYPE);\n\n  DISPLAY_START (\n    // This block is still executed for DIAGS if display not in use\n    LCD(0,F(\"DCC-EX v\" VERSION));\n    LCD(1,F(\"Lic GPLv3\"));\n  );\n\n  // Responsibility 2: Start all the communications before the DCC engine\n  // Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi\n  // Start Ethernet if it exists\n#if WIFI_ON\n  PASSWDCHECK(WIFI_PASSWORD); // compile time check\n#ifndef ARDUINO_ARCH_ESP32\n  WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);\n#else\n  WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);\n#endif // ARDUINO_ARCH_ESP32\n#endif // WIFI_ON\n\n#if ETHERNET_ON\n  EthernetInterface::setup();\n#endif // ETHERNET_ON\n  \n  // Responsibility 3: Start the DCC engine.\n  DCC::begin();\n\n  // Start RMFT aka EX-RAIL (ignored if no automnation)\n  RMFT::begin();\n\n#ifdef ARDUINO_ARCH_ESP32\n#ifdef BOOSTER_INPUT\n  dccSniffer = new Sniffer(BOOSTER_INPUT);\n#endif // BOOSTER_INPUT\n#endif // ARDUINO_ARCH_ESP32\n\n  // Invoke any DCC++EX commands in the form \"SETUP(\"xxxx\");\"\" found in optional file mySetup.h.\n  //  This can be used to create turnouts, outputs, sensors etc. through the normal text commands.\n  #if __has_include ( \"mySetup.h\")\n    #define SETUP(cmd) DCCEXParser::parse(F(cmd))\n    #include \"mySetup.h\"\n    #undef SETUP\n  #endif\n\n  #if defined(LCN_SERIAL)\n  LCN_SERIAL.begin(115200);\n  LCN::init(LCN_SERIAL);\n  #endif\n  LCD(3, F(\"Ready\"));\n  CommandDistributor::broadcastPower();\n}\n\nvoid loop()\n{\n  #ifdef ENABLE_SERIAL_LOG\n    SerialLog.loop();\n  #endif\n\n#ifdef ARDUINO_ARCH_ESP32\n\n#ifdef BOOSTER_INPUT\n  static bool oldactive = false;\n  if (dccSniffer) {\n    bool newactive = dccSniffer->inputActive();\n    if (oldactive != newactive) {\n      RMFT2::railsyncEvent(newactive);\n      oldactive = newactive;\n    }\n    DCCPacket p = dccSniffer->fetchPacket();\n    if (p.len() != 0) {\n      if (DCCDecoder::parse(p)) {\n\tif (Diag::SNIFFER)\n\t  p.print();\n      }\n    }\n  }\n#endif // BOOSTER_INPUT\n#endif // ARDUINO_ARCH_ESP32\n\n  // The main sketch has responsibilities during loop()\n\n  // Responsibility 1: Handle DCC background processes\n  //                   (loco reminders and power checks)\n  DCC::loop();\n \n  // Responsibility 2: handle any incoming commands on USB connection\n  SerialManager::loop();\n \n  // Responsibility 3: Optionally handle any incoming WiFi traffic\n#ifndef ARDUINO_ARCH_ESP32\n#if WIFI_ON\n  WifiInterface::loop();\n \n#endif //WIFI_ON\n#else  //ARDUINO_ARCH_ESP32\n#if WIFI_ON\n#ifndef WIFI_TASK_ON_CORE0\n  WifiESP::loop();\n#endif\n#endif //WIFI_ON\n#endif //ARDUINO_ARCH_ESP32\n#if ETHERNET_ON\n  EthernetInterface::loop();\n#endif\n\n  RMFT::loop();  // ignored if no automation\n\n  #if defined(LCN_SERIAL)\n  LCN::loop();\n  #endif\n\n  // Display refresh\n  DisplayInterface::loop();\n\n  // Handle/update IO devices.\n  IODevice::loop();\n\n  Sensor::checkAll(); // Update and print changes\n\n  // Report any decrease in memory (will automatically trigger on first call)\n  static int ramLowWatermark = __INT_MAX__; // replaced on first loop\n\n  #ifdef ARDUINO_ARCH_AVR\n  // count every byte of free RAM on AVR\n  int freeNow = DCCTimer::getMinimumFreeMemory();\n  if (freeNow < ramLowWatermark) {\n    ramLowWatermark = freeNow;\n    LCD(3,F(\"Free RAM=%5db\"), ramLowWatermark);\n  }\n  #else\n  // on other platforms, just report every 4kb\n  int freeNow = DCCTimer::getMinimumFreeMemory() / 4096;\n  if (freeNow < ramLowWatermark) {\n    ramLowWatermark = freeNow;\n    LCD(3,F(\"Free RAM=%5dKb\"), ramLowWatermark*4);\n  }\n  #endif\n}\n"
  },
  {
    "path": "DCC.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2021 Herb Morton\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 M Steve Todd\n *  © 2020-2026 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include \"DIAG.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"DCCConsist.h\"\n#ifndef DISABLE_EEPROM\n#include \"EEStore.h\"\n#endif\n#include \"GITHUB_SHA.h\"\n#include \"version.h\"\n#include \"FSH.h\"\n#include \"IODevice.h\"\n#include \"EXRAIL2.h\"\n#include \"CommandDistributor.h\"\n#include \"TrackManager.h\"\n#include \"DCCTimer.h\"\n#include \"Railcom.h\"\n#include \"DCCQueue.h\"\n\n// This module is responsible for converting API calls into\n// messages to be sent to the waveform generator.\n// It has no visibility of the hardware, timers, interrupts\n// nor of the waveform issues such as preambles, start bits checksums or cutouts.\n//\n// Nor should it have to deal with JMRI responsess other than the OK/FAIL\n// or cv value returned. I will move that back to the JMRI interface later\n//\n// The interface to the waveform generator is narrowed down to merely:\n//   Scheduling a message on the prog or main track using a function\n//   Obtaining ACKs from the prog track using a function\n//   There are no volatiles here.\n\nconst byte FN_GROUP_1=0x01;\nconst byte FN_GROUP_2=0x02;\nconst byte FN_GROUP_3=0x04;\nconst byte FN_GROUP_4=0x08;\nconst byte FN_GROUP_5=0x10;\n\nFSH* DCC::shieldName=NULL;\nbyte DCC::globalSpeedsteps=128;\n\n#define SLOTLOOP for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext())\n\nvoid DCC::begin() {\n  StringFormatter::send(&USB_SERIAL,F(\"<iDCC-EX V-%S / %S / %S G-%S>\\n\"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));\n#ifndef DISABLE_EEPROM\n  // Load stuff from EEprom\n  (void)EEPROM; // tell compiler not to warn this is unused\n  EEStore::init();\n#endif\n#ifndef ARDUINO_ARCH_ESP32 /* On ESP32 started in TrackManager::setTrackMode() */\n  DCCWaveform::begin();\n#endif\n}\n\nbyte DCC::defaultMomentumA=0;\nbyte DCC::defaultMomentumD=0;\nbool DCC::linearAcceleration=false; \n\nbyte DCC::getMomentum(LocoSlot * slot) {\n   auto target=slot->getTargetSpeed() & 0x7f;\n   auto current=slot->getSpeedCode() & 0x7f;\n   if (target > current) {\n    // accelerating\n    auto momentum=(slot->getMomentumA() == MOMENTUM_USE_DEFAULT) ? defaultMomentumA : slot->getMomentumA();\n    // if nonlinear acceleration, momentum is reduced according to \n    // gap between throttle and speed.\n    // ie. Loco takes accelerates faster if high throttle\n    if (momentum==0 || linearAcceleration) return momentum;\n    auto powerDifference= (target-current)/8;\n    if (momentum-powerDifference <0) return 0;\n    return momentum-powerDifference; \n   }\n   return (slot->getMomentumD() == MOMENTUM_USE_DEFAULT) ? defaultMomentumD : slot->getMomentumD();\n}\n\nbool DCC::setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection)  {\n  if (tSpeed==1) {\n    if (cab==0) {\n      estopAll(); // ESTOP broadcast fix \n      return true;\n    }\n  } \n  byte speedCode = (tSpeed & 0x7F)  + tDirection * 128;\n  auto slot=LocoSlot::getSlot(cab, true);\n  if (!slot) return false; // speed table full, can not do anything\n\n  // Ignore any throttle command to consist follower.\n  if (slot->isConsistFollower()) return true; \n  \n  if (slot->getTargetSpeed()==speedCode) // speed has been reached\n    return true;\n  slot->setTargetSpeed(speedCode);\n\n  // copy target speed to consist followers\n  for (auto follower=slot->getConsistNext(); follower; follower=follower->getConsistNext()) {\n    follower->setTargetSpeed(speedCode ^ (follower->isConsistReverse() ? 0x80 : 0) );\n  }\n\n  byte momentum=getMomentum(slot);\n  if (momentum && tSpeed!=1) { // not ESTOP\n    // we dont throttle speed, we just let the reminders take it to target\n    slot->setMomentumBase(millis());\n  } \n  else {  // Momentum not involved, throttle now.\n    setThrottle2(slot,speedCode);\n    if (!estopIsLocked) TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr\n  }\n  CommandDistributor::broadcastLoco(slot);\n  return true;\n}\n\n// set speedCode directly to DCC and make followers same\nvoid DCC::setThrottle2( LocoSlot * slot, byte speedCode)  {\n  for (auto follower=slot; follower; follower=follower->getConsistNext()) {\n    follower->setSpeedCode(speedCode);\n    setThrottleDCC(follower,speedCode);\n  } \n}\n\nvoid DCC::setThrottleDCC( LocoSlot * slot, byte speedCode)  {\n  uint16_t cab=0;\n  if (slot) {\n    cab=slot->getLoco();\n    if (slot->isConsistReverse()) speedCode^=0x80;\n  }\n  // any throttle traffic is blocked when estop is locked\n  if (estopIsLocked) {\n    // replace speed with emergency stop but keep direction\n    speedCode = (speedCode & 0x80) | 0x01;\n  }\n  uint8_t b[4];\n  uint8_t nB = 0;\n  // DIAG(F(\"setSpeedInternal %d %x\"),cab,speedCode);\n  if (cab > HIGHEST_SHORT_ADDR)\n    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n  b[nB++] = lowByte(cab);\n\n  if (globalSpeedsteps <= 28) {\n\n    uint8_t speed128 = speedCode & 0x7F;\n    uint8_t speed28;\n    uint8_t code28;\n\n    if (speed128 == 0 || speed128 == 1) { // stop or emergency stop\n      code28 = speed128;\n    } else {\n      speed28= (speed128*10+36)/46;                 // convert 2-127 to 1-28\n/*\n      if (globalSpeedsteps <= 14)                   // Don't want to do 14 steps, to get F0 there is ugly\n        code28 = (speed28+3)/2 | (Value of F0);     // convert 1-28 to DCC 14 step speed code\n      else\n*/\n      code28 = (speed28+3)/2 | ( (speed28 & 1) ? 0 : 0b00010000 ); // convert 1-28 to DCC 28 step speed code\n    }\n    //        Construct command byte from:\n    //        command      speed    direction\n    b[nB++] = 0b01000000 | code28 | ((speedCode & 0x80) ? 0b00100000 : 0);\n\n  } else { // 128 speedsteps\n\n    b[nB++] = SET_SPEED;                      // 128-step speed control byte\n    b[nB++] = speedCode; // for encoding see setThrottle\n\n  }\n  if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority\n  else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);\n}\n\nvoid DCC::setFunctionInternal(int cab, byte group, byte byte1, byte byte2) {\n  // DIAG(F(\"setFunctionInternal %d %x %x\"),cab,byte1,byte2);\n  byte b[4];\n  byte nB = 0;\n\n  if (cab > HIGHEST_SHORT_ADDR)\n    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n  b[nB++] = lowByte(cab);\n  if (byte1!=0) b[nB++] = byte1;\n  b[nB++] = byte2;\n  \n  DCCQueue::scheduleDCCFunctionPacket(b, nB, cab,group);\n}\n\n// returns speed steps 0 to 127 (1 == emergency stop)\n// or -1 on \"loco not found\"\nint8_t DCC::getThrottleSpeed(int cab) {\n  return getThrottleSpeedByte(cab) & 0x7F;\n}\n\n// returns speed code byte\n// or 128 (speed 0, dir forward) on \"loco not found\".\n// This is the throttle set speed... which may be different\n// from the actual loco speed if momentum is active or\n// speed limiting is applied.\nuint8_t DCC::getThrottleSpeedByte(int cab) {\n  auto slot=LocoSlot::getSlot(cab,false);\n  return slot?slot->getTargetSpeed():128;\n}\n// returns speed code byte for loco. \n// This is the most recently send DCC speed packet byte\n// or 128 (speed 0, dir forward) on \"loco not found\".\nuint8_t DCC::getLocoSpeedByte(int cab) {\n  auto slot=LocoSlot::getSlot(cab,false);\n  return slot?slot->getSpeedCode():128;\n}\n\n// returns 0 to 7 for frequency\nuint8_t DCC::getThrottleFrequency(int cab) {\n#if defined(ARDUINO_AVR_UNO)\n  (void)cab;\n  return 0;\n#else\n auto slot=LocoSlot::getSlot(cab,false);\n if (slot == nullptr)  // speed table full, can not do anything\n    return 0;           // return value for default frequency\n  // shift out first 29 bits so we have the 3 \"frequency bits\" left\n  uint8_t res = (uint8_t)(slot->getFunctions() >>29);\n  //DIAG(F(\"Speed table %d functions %l shifted %d\"), reg, slot->functions, res);\n  return res;\n#endif\n}\n\n// returns direction on loco\n// or true/forward on \"loco not found\"\nbool DCC::getThrottleDirection(int cab) {\n  return getThrottleSpeedByte(cab) & 0x80;\n}\n\n// Set function to value on or off\nbool DCC::setFn( int cab, int16_t functionNumber, bool on) {\n  if (cab<=0 ) return false;\n  if (functionNumber < 0) return false;\n\n  if (functionNumber>28) {\n    //non reminding advanced binary bit set\n    byte b[5];\n    byte nB = 0;\n    if (cab > HIGHEST_SHORT_ADDR)\n      b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n    b[nB++] = lowByte(cab);\n    if (functionNumber <= 127) {\n       b[nB++] = 0b11011101;   // Binary State Control Instruction short form\n       b[nB++] = functionNumber | (on ? 0x80 : 0);\n    }\n    else  {\n       b[nB++] = 0b11000000;   // Binary State Control Instruction long form\n       b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0);  // low order bits and state flag\n       b[nB++] = functionNumber >>7 ;  // high order bits\n    }\n    DCCQueue::scheduleDCCPacket(b, nB, 4,cab);\n  }\n  // We use the reminder table up to 28 for normal functions.\n  // We use 29 to 31 for DC frequency as well so up to 28\n  // are \"real\" functions and 29 to 31 are frequency bits\n  // controlled by function buttons\n  if (functionNumber > 31)\n    return true;\n  \n   auto slot=LocoSlot::getSlot(cab,true);\n   if (!slot) return false; // speed table full, can not do anything\n\n   // Take care of functions:\n  // Set state of function\n  uint32_t previous=slot->getFunctions();\n  uint32_t newset=previous;\n  uint32_t funcmask = (1UL<<functionNumber);\n  if (on) {\n      newset |= funcmask;\n  } else {\n      newset &= ~funcmask;\n  }\n  if (newset != previous) {\n    slot->setFunctions(newset);\n    if (functionNumber <= 28)\n      updateGroupflags(slot, functionNumber);\n    CommandDistributor::broadcastLoco(slot);\n  }\n  return true;\n}\n\n// Flip function state (used from withrottle protocol)\nvoid DCC::changeFn( int cab, int16_t functionNumber) {\n  auto currentValue=getFn(cab,functionNumber);\n  if (currentValue<0) return;  // function not valid for change  \n  setFn(cab,functionNumber, currentValue?false:true);\n}\n\n// Report function state (used from withrottle protocol)\n// returns 0 false, 1 true or -1 for do not know\nint8_t DCC::getFn( int cab, int16_t functionNumber) {\n  if (cab<=0 || functionNumber>31)\n    return -1;  // unknown\n  auto slot = LocoSlot::getSlot(cab, false);\n  if (slot == nullptr) // not found\n    return -1;\n  unsigned long funcmask = (1UL<<functionNumber);\n  return  (slot->getFunctions() & funcmask)? 1 : 0;\n}\n\n// Set the group flag to say we have touched the particular group.\n// A group will be reminded only if it has been touched.\nvoid DCC::updateGroupflags(LocoSlot * slot, int16_t functionNumber) {\n  byte groupMask;\n  if (functionNumber<=4)       groupMask=FN_GROUP_1;\n  else if (functionNumber<=8)  groupMask=FN_GROUP_2;\n  else if (functionNumber<=12) groupMask=FN_GROUP_3;\n  else if (functionNumber<=20) groupMask=FN_GROUP_4;\n  else                         groupMask=FN_GROUP_5;\n  slot->setGroupFlags((slot->getGroupFlags()) | groupMask);\n}\n\nuint32_t DCC::getFunctionMap(int cab) {\n  if (cab<=0) return 0;  // unknown pretend all functions off\n  auto slot = LocoSlot::getSlot(cab,false);\n  return slot?slot->getFunctions():0;\n}\n\n// saves DC frequency (0..3) in spare functions 29,30,31\nvoid DCC::setDCFreq(int cab,byte freq) {\n  if (cab==0 || freq>3) return;\n  auto slot=LocoSlot::getSlot(cab,true);\n  if (!slot) return; // speed table full, can not do anything\n  \n  // drop and replace F29,30,31 (top 3 bits) \n  auto newFunctions=slot->getFunctions() & 0x1FFFFFFFUL; // get and clear relevant bits\n  if (freq != 0) newFunctions |= (1UL<<(28 + freq));     // 1->29, 2->30, 3->31\n  if (newFunctions==slot->getFunctions()) return; // no change \n  slot->setFunctions(newFunctions);\n  CommandDistributor::broadcastLoco(slot);\n}\n\nvoid DCC::saveSpeed(int cab) {\n  auto slot=LocoSlot::getSlot(cab,false);\n  if (slot == nullptr)  // speed table full, can not do anything\n    return;\n  slot->saveSpeed();\n}\n\nvoid DCC::restoreSpeed(int cab) {\n  auto slot=LocoSlot::getSlot(cab,false);\n  if (slot == nullptr)  // speed table full, can not do anything\n    return;\n  auto speedCode=slot->getSavedSpeedCode();  \n  setThrottle(cab, speedCode& 0x7f, (speedCode & 0x80)!=0);\n}\n\nvoid DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {\n  // onoff is tristate:\n  // 0  => send off packet\n  // 1  => send on packet\n  // >1 => send both on and off packets.\n\n  // An accessory has an address, 4 ports and 2 gates (coils) each. That's how\n  // the initial decoders were orgnized and that influenced how the DCC\n  // standard was made.\n  #ifdef DIAG_IO\n  DIAG(F(\"DCC::setAccessory(%d,%d,%d,%d)\"), address, port, gate, onoff);\n  #endif\n  // use masks to detect wrong values and do nothing\n  if(address != (address & 511))\n    return;\n  if(port != (port & 3))\n    return;\n  byte b[2];\n\n  // first byte is of the form 10AAAAAA, where AAAAAA represent 6 least signifcant bits of accessory address\n  // second byte is of the form 1AAACPPG, where C is 1 for on, PP the ports 0 to 3 and G the gate (coil).\n  b[0] = address % 64 + 128;\n  b[1] = ((((address / 64) % 8) << 4) + (port % 4 << 1) + gate % 2) ^ 0xF8;\n  if (onoff==0) {   // off packet only\n    b[1] &= ~0x08; // set C to 0\n    DCCQueue::scheduleDCCPacket(b, 2, 3);\n  } else if (onoff==1) { // on packet only\n    DCCQueue::scheduleDCCPacket(b, 2, 3);\n  } else { // auto timed on then off  \n    DCCQueue::scheduleAccOnOffPacket(b, 2, 3, 100); // On then off after 100mS\n  } \n#if defined(EXRAIL_ACTIVE)\n  if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate);\n#endif\n}\n\nbool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {\n\n/* From https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf\n\nThe Extended Accessory Decoder Control Packet is included for the purpose of transmitting aspect control to signal \ndecoders or data bytes to more complex accessory decoders. Each signal head can display one aspect at a time. \n{preamble} 0 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1\n\nXXXXX is for a single head. A value of 00000 for XXXXX indicates the absolute stop aspect. All other aspects \nrepresented by the values for XXXXX are determined by the signaling system used and the prototype being \nmodeled.\n\nFrom https://normen.railcommunity.de/RCN-213.pdf:\n\nMore information is in RCN-213 about how the address bits are organized.\npreamble -0- 1 0 A7 A6 A5 A4 A3 A2 -0- 0 ^A10 ^A9 ^A8 0 A1 A0 1 -0- ....\n\nThus in byte packet form the format is 10AAAAAA, 0AAA0AA1, 000XXXXX\n\nDie Adresse für den ersten erweiterten Zubehördecoder ist wie bei den einfachen\nZubehördecodern die Adresse 4 = 1000-0001 0111-0001 . Diese Adresse wird in\nAnwenderdialogen als Adresse 1 dargestellt.\n\nThis means that the first address shown to the user as \"1\" is mapped\nto internal address 4.\n\nNote that the Basic accessory format mentions \"By convention these\nbits (bits 4-6 of the second data byte) are in ones complement\" but\nthis note is absent from the advanced packet description. The\nenglish translation does not mention that the address format for\nthe advanced packet follows the one for the basic packet but\naccording to the RCN-213 this is the case.\n\nWe allow for addresses from -3 to 2047-3 as that allows to address the\nwhole range of the 11 bits sent to track.\n*/\n  if ((address > 2044) || (address < -3)) return false; // 2047-3, 11 bits but offset 3\n  if (value != (value & 0x1F)) return false;            // 5 bits\n\n  address+=3;                        // +3 offset according to RCN-213\n  byte b[3];\n  b[0]= 0x80                         // bits always on\n    | ((address>>2) & 0x3F);         // shift out 2, mask out used bits\n  b[1]= 0x01                         // bits always on\n    | (((~(address>>8)) & 0x07)<<4)  // shift out 8, invert, mask 3 bits, shift up 4\n    | ((address & 0x03)<<1);         // mask 2 bits, shift up 1\n  b[2]=value;\n  DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);\n  return true;\n}\n\n//\n// writeCVByteMain: Write a byte with PoM on main. This writes\n// the 5 byte sized packet to implement this DCC function\n//\nvoid DCC::writeCVByteMain(int cab, int cv, byte bValue)  {\n  byte b[5];\n  byte nB = 0;\n  if (cab > HIGHEST_SHORT_ADDR)\n    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n\n  b[nB++] = lowByte(cab);\n  b[nB++] = cv1(WRITE_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03\n  b[nB++] = cv2(cv);\n  b[nB++] = bValue;\n\n  DCCQueue::scheduleDCCPacket(b, nB, 4,cab);\n}\n\n//\n// readCVByteMain: Read a byte with PoM on main.\n// This requires Railcom active \n//\nvoid DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback)  {\n  byte b[5];\n  byte nB = 0;\n  if (cab > HIGHEST_SHORT_ADDR)\n    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n\n  b[nB++] = lowByte(cab);\n  b[nB++] = cv1(READ_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03\n  b[nB++] = cv2(cv);\n  b[nB++] = 0;\n\n  DCCQueue::scheduleDCCPacket(b, nB, 2,cab);\n  Railcom::anticipate(cab,cv,callback);\n}\n\n//\n// writeCVBitMain: Write a bit of a byte with PoM on main. This writes\n// the 5 byte sized packet to implement this DCC function\n//\nvoid DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue)  {\n  byte b[5];\n  byte nB = 0;\n  bValue = bValue % 2;\n  bNum = bNum % 8;\n\n  if (cab > HIGHEST_SHORT_ADDR)\n    b[nB++] = highByte(cab) | 0xC0;    // convert train number into a two-byte address\n\n  b[nB++] = lowByte(cab);\n  b[nB++] = cv1(WRITE_BIT_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03\n  b[nB++] = cv2(cv);\n  b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;\n\n  DCCQueue::scheduleDCCPacket(b, nB, 4,cab);\n}\n\nbool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) {\n  /* see rcn-122\n  5 Global commands \nThese commands are sent and begin exclusively with a broadcast address 0 \nalways with {synchronous bits} 0 0000-0000 … and end with the checksum \n... PPPPPPPP 1. Therefore, only the bytes of the commands and not that of \nshown below whole package shown. The commands can be used by vehicle and \naccessory decoders alike. \n\n  5.1 Time command \nThis command is four bytes long and has the format: \n1100-0001 CCxx-xxxx xxxx-xxxxx xxxx-xxxx \nCC indicates what data is transmitted in the packet: \nCC = 00 Model Time \n1100-0001 00MM-MMMM WWWH-HHHH U0BB-BBBB with: \nMMMMMM = Minutes, Value range: 0..59 \nWWW =     Day of the Week, Value range: 0 = Monday, 1 = Tuesday, 2 = Wednesday, \n3 = Thursday, 4 = Friday, 5 = Saturday, 6 = Sunday, 7 = Weekday \nis not supported. \nHHHHH =   Hours, value range: 0..23 \nU =         \nUpdate, i.e. the time has changed suddenly, e.g. by a new one timetable to start.        \nUp to 4 can occur per sudden change commands can be marked like this. \nBBBBBB =    Acceleration factor, value range 0..63. An acceleration factor of 0 means the  \nmodel clock has been stopped, a factor of 1 corresponds to real time, at 2 the \nclock runs twice as fast, at three times as fast as real time, etc. \n*/\nif (minutes>=1440 || speed>63 ) return false; \nbyte b[5];\nb[0]=0; // broadcast address\nb[1]=0b11000001; // 1100-0001 (model time)\nb[2]=minutes % 60 ;  // MM\nb[3]= 0b11100000 | (minutes/60); // 111H-HHHH weekday not supported\nb[4]= (suddenChange ? 0b10000000 : 0) | speed;\nDCCQueue::scheduleDCCPacket(b, sizeof(b), 2);\nreturn true; \n}\n\nFSH* DCC::getMotorShieldName() {\n  return shieldName;\n}\n\nconst ackOp FLASH WRITE_BIT0_PROG[] = {\n     BASELINE,\n     W0,WACK,\n     V0, WACK,  // validate bit is 0\n     ITC1,      // if acked, callback(1)\n     CALLFAIL  // callback (-1)\n};\nconst ackOp FLASH WRITE_BIT1_PROG[] = {\n     BASELINE,\n     W1,WACK,\n     V1, WACK,  // validate bit is 1\n     ITC1,      // if acked, callback(1)\n     CALLFAIL  // callback (-1)\n};\n\nconst ackOp FLASH VERIFY_BIT0_PROG[] = {\n     BASELINE,\n     V0, WACK,  // validate bit is 0\n     ITC0,      // if acked, callback(0)\n     V1, WACK,  // validate bit is 1\n     ITC1,\n     CALLFAIL  // callback (-1)\n};\nconst ackOp FLASH VERIFY_BIT1_PROG[] = {\n     BASELINE,\n     V1, WACK,  // validate bit is 1\n     ITC1,      // if acked, callback(1)\n     V0, WACK,\n     ITC0,\n     CALLFAIL  // callback (-1)\n};\n\nconst ackOp FLASH READ_BIT_PROG[] = {\n     BASELINE,\n     V1, WACK,  // validate bit is 1\n     ITC1,      // if acked, callback(1)\n     V0, WACK,  // validate bit is zero\n     ITC0,      // if acked callback 0\n     CALLFAIL       // bit not readable\n     };\n\nconst ackOp FLASH WRITE_BYTE_PROG[] = {\n      BASELINE,\n      WB,WACK,ITC1,    // Write and callback(1) if ACK\n      // handle decoders that dont ack a write\n      VB,WACK,ITC1,    // validate byte and callback(1) if correct\n      CALLFAIL        // callback (-1)\n      };\n\nconst ackOp FLASH VERIFY_BYTE_PROG[] = {\n      BASELINE,\n      BIV,         // ackManagerByte initial value\n      VB,WACK,     // validate byte\n      ITCB,       // if ok callback value\t\n      STARTMERGE,    //clear bit and byte values ready for merge pass\n      // each bit is validated against 0 and the result inverted in MERGE\n      // this is because there tend to be more zeros in cv values than ones.\n      // There is no need for one validation as entire byte is validated at the end\n      V0, WACK, MERGE,        // read and merge first tested bit (7)\n      ITSKIP,                 // do small excursion if there was no ack\n        SETBIT,(ackOp)7,\n        V1, WACK, NAKFAIL,    // test if there is an ack on the inverse of this bit (7)\n        SETBIT,(ackOp)6,      // and abort whole test if not else continue with bit (6)\n      SKIPTARGET,\n      V0, WACK, MERGE,        // read and merge second tested bit (6)\n      V0, WACK, MERGE,        // read and merge third  tested bit (5) ...\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCBV,  // verify merged byte and return it if acked ok - with retry report\n      CALLFAIL };\n\n\nconst ackOp FLASH READ_CV_PROG[] = {\n      BASELINE,\n      STARTMERGE,    //clear bit and byte values ready for merge pass\n      // each bit is validated against 0 and the result inverted in MERGE\n      // this is because there tend to be more zeros in cv values than ones.\n      // There is no need for one validation as entire byte is validated at the end\n      V0, WACK, MERGE,        // read and merge first tested bit (7)\n      ITSKIP,                 // do small excursion if there was no ack\n        SETBIT,(ackOp)7,\n        V1, WACK, NAKFAIL,    // test if there is an ack on the inverse of this bit (7)\n        SETBIT,(ackOp)6,      // and abort whole test if not else continue with bit (6)\n      SKIPTARGET,\n      V0, WACK, MERGE,        // read and merge second tested bit (6)\n      V0, WACK, MERGE,        // read and merge third  tested bit (5) ...\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCB,  // verify merged byte and return it if acked ok\n      CALLFAIL };          // verification failed\n\n// returns loco id or consist id or -1 on failure\nconst ackOp FLASH LOCO_DRIVEAWAY_PROG[] = {\n      BASELINE,\n      // first check cv20 for extended addressing\n      SETCV, (ackOp)20,     // CV 19 is extended\n      SETBYTE, (ackOp)0,\n      VB, WACK, ITSKIP,     // skip past extended section if cv20 is zero\n      // read cv20 and 19 and merge \n      STARTMERGE,           // Setup to read cv 20\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKSKIP, // bad read of cv20, assume its 0 \n      BAD20SKIP,     // detect invalid cv20 value and ignore \n      STASHLOCOID,   // keep cv 20 until we have cv19 as well.\n      SETCV, (ackOp)19, \n      STARTMERGE,           // Setup to read cv 19\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // cant recover if cv 19 unreadable\n      COMBINE1920,  // Combile byte with stash and callback\n// end of advanced 20,19 check\n      SKIPTARGET,\n      SETCV, (ackOp)19,     // CV 19 is consist setting\n      SETBYTE, (ackOp)0,\n      VB, WACK, ITSKIP,     // ignore consist if cv19 is zero (no consist)\n      SETBYTE, (ackOp)128,\n      VB, WACK, ITSKIP,     // ignore consist if cv19 is 128 (no consist, direction bit set)\n      STARTMERGE,           // Setup to read cv 19\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCB7,  // return 7 bits only, No_ACK means CV19 not supported so ignore it\n\n      SKIPTARGET,     // continue here if CV 19 is zero or fails all validation\n      SETCV,(ackOp)29,\n      SETBIT,(ackOp)5,\n      V0, WACK, ITSKIP,  // Skip to SKIPTARGET if bit 5 of CV29 is zero\n\n      // Long locoid\n      SETCV, (ackOp)17,       // CV 17 is part of locoid\n      STARTMERGE,\n      V0, WACK, MERGE,  // read and merge bit 1 etc\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok\n      STASHLOCOID,         // keep stashed cv 17 for later\n      // Read 2nd part from CV 18\n      SETCV, (ackOp)18,\n      STARTMERGE,\n      V0, WACK, MERGE,  // read and merge bit 1 etc\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok\n      COMBINELOCOID,        // Combile byte with stash to make long locoid and callback\n\n      // ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that\n      SKIPTARGET,\n      SETCV, (ackOp)1,\n      STARTMERGE,\n      SETBIT, (ackOp)6,  // skip over first bit as we know its a zero\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCB,  // verify merged byte and callback\n      CALLFAIL\n      };\n\n// returns loco id regardless of consist \nconst ackOp FLASH LOCO_READ_ID_PROG[] = {\n      BASELINE,\n      SETCV,(ackOp)29,\n      SETBIT,(ackOp)5,\n      V0, WACK, ITSKIP,  // Skip to SKIPTARGET if bit 5 of CV29 is zero\n\n      // Long locoid\n      SETCV, (ackOp)17,       // CV 17 is part of locoid\n      STARTMERGE,\n      V0, WACK, MERGE,  // read and merge bit 1 etc\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok\n      STASHLOCOID,         // keep stashed cv 17 for later\n      // Read 2nd part from CV 18\n      SETCV, (ackOp)18,\n      STARTMERGE,\n      V0, WACK, MERGE,  // read and merge bit 1 etc\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // verify merged byte and return -1 it if not acked ok\n      COMBINELOCOID,        // Combile byte with stash to make long locoid and callback\n\n      // ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that\n      SKIPTARGET,\n      SETCV, (ackOp)1,\n      STARTMERGE,\n      SETBIT, (ackOp)6,  // skip over first bit as we know its a zero\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCB,  // verify merged byte and callback\n      CALLFAIL\n      };\n\n// returns consist id or -1 on failure\nconst ackOp FLASH LOCO_READ_CONSIST_PROG[] = {\n      BASELINE,\n      // first check cv20 for extended addressing\n      SETCV, (ackOp)20,     // CV 19 is extended\n      SETBYTE, (ackOp)0,\n      VB, WACK, ITSKIP,     // skip past extended section if cv20 is zero\n      // read cv20 and 19 and merge \n      STARTMERGE,           // Setup to read cv 20\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKSKIP, // bad read of cv20, assume its 0 \n      BAD20SKIP,     // detect invalid cv20 value and ignore \n      STASHLOCOID,   // keep cv 20 until we have cv19 as well.\n      SETCV, (ackOp)19, \n      STARTMERGE,           // Setup to read cv 19\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, NAKFAIL,  // cant recover if cv 19 unreadable\n      COMBINE1920,  // Combile byte with stash and callback\n// end of advanced 20,19 check\n      SKIPTARGET,\n      SETCV, (ackOp)19,     // CV 19 is consist setting\n      SETBYTE, (ackOp)0,\n      VB, WACK, ITC0,     // rerturn 0 if cv19 is zero (no consist)\n      SETBYTE, (ackOp)128,\n      VB, WACK, ITC0,     // return 0 if cv19 is 128 (no consist, direction bit set)\n      STARTMERGE,           // Setup to read cv 19\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      V0, WACK, MERGE,\n      VB, WACK, ITCB7,  // return 7 bits only, No_ACK means CV19 not supported so ignore it\n      CALLFAIL // return -1 if cv19 wont verify\n      };\n\nconst ackOp FLASH SHORT_LOCO_ID_PROG[] = {\n      BASELINE,\n      // Clear consist CV 19,20\n      SETCV,(ackOp)20,\n      SETBYTE, (ackOp)0,\n      WB,WACK,     // ignore dedcoder without cv20 support\n      SETCV,(ackOp)19,\n      SETBYTE, (ackOp)0,\n      WB,WACK,     // ignore dedcoder without cv19 support\n      // Turn off long address flag\n      SETCV,(ackOp)29,\n      SETBIT,(ackOp)5,\n      W0,WACK,\n      V0,WACK,NAKFAIL,\n      SETCV, (ackOp)1,\n      SETBYTEL,   // low byte of word\n      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok\n      VB,WACK,ITC1,   // Some decoders do not ack and need verify\n      CALLFAIL\n};\n\n// for CONSIST_ID_PROG the 20,19 values are already calculated\nconst ackOp FLASH CONSIST_ID_PROG[] = {\n      BASELINE,\n      SETCV,(ackOp)20,\n      SETBYTEH,    // high byte to CV 20\n      WB,WACK,ITSKIP,\n      FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it\n      SKIPTARGET,\n      SETCV,(ackOp)19,\n      SETBYTEL,   // low byte of word\n      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok\n      VB,WACK,ITC1,   // Some decoders do not ack and need verify\n      CALLFAIL\n};\n\nconst ackOp FLASH LONG_LOCO_ID_PROG[] = {\n      BASELINE,\n      // Clear consist CV 19,20\n      SETCV,(ackOp)20,\n      SETBYTE, (ackOp)0,\n      WB,WACK,     // ignore dedcoder without cv20 support\n      SETCV,(ackOp)19,\n      SETBYTE, (ackOp)0,\n      WB,WACK,     // ignore decoder without cv19 support\n      // Turn on long address flag cv29 bit 5\n      SETCV,(ackOp)29,\n      SETBIT,(ackOp)5,\n      W1,WACK,\n      V1,WACK,NAKFAIL,\n      // Store high byte of address in cv 17\n      SETCV, (ackOp)17,\n      SETBYTEH, // high byte of word\n      WB,WACK,      // do write\n      ITSKIP,       // if ACK, jump to SKIPTARGET\n        VB,WACK,    // try verify instead\n        ITSKIP,     // if ACK, jump to SKIPTARGET\n          CALLFAIL, // if still here, fail\n      SKIPTARGET,\n      // store\n      SETCV, (ackOp)18,\n      SETBYTEL,   // low byte of word\n      WB,WACK,ITC1,   // If ACK, we are done - callback(1) means Ok\n      VB,WACK,ITC1,   // Some decoders do not ack and need verify\n      CALLFAIL\n};\n\nvoid  DCC::writeCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback)  {\n  DCCACK::Setup(cv, byteValue,  WRITE_BYTE_PROG, callback);\n}\n\nvoid DCC::writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback)  {\n  if (bitNum >= 8) callback(-1);\n  else DCCACK::Setup(cv, bitNum, bitValue?WRITE_BIT1_PROG:WRITE_BIT0_PROG, callback);\n}\n\nvoid  DCC::verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback)  {\n  DCCACK::Setup(cv, byteValue,  VERIFY_BYTE_PROG, callback);\n}\n\nvoid DCC::verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback)  {\n  if (bitNum >= 8) callback(-1);\n  else DCCACK::Setup(cv, bitNum, bitValue?VERIFY_BIT1_PROG:VERIFY_BIT0_PROG, callback);\n}\n\n\nvoid DCC::readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback)  {\n  if (bitNum >= 8) callback(-1);\n  else DCCACK::Setup(cv, bitNum,READ_BIT_PROG, callback);\n}\n\nvoid DCC::readCV(int16_t cv, ACK_CALLBACK callback)  {\n  DCCACK::Setup(cv, 0,READ_CV_PROG, callback);\n}\n\nvoid DCC::getDriveawayLocoId(ACK_CALLBACK callback) {\n  DCCACK::Setup(0,0, LOCO_DRIVEAWAY_PROG, callback);\n}\n\nvoid DCC::getLocoId(ACK_CALLBACK callback) {\n  DCCACK::Setup(0,0, LOCO_READ_ID_PROG, callback);\n}\n\nvoid DCC::getConsistId(ACK_CALLBACK callback) {\n  DCCACK::Setup(0,0, LOCO_READ_CONSIST_PROG, callback);\n}\n\nvoid DCC::setLocoId(int id,ACK_CALLBACK callback) {\n  if (id<1 || id>10239) { //0x27FF according to standard\n    callback(-1);\n    return;\n  }\n  if (id<=HIGHEST_SHORT_ADDR)\n      DCCACK::Setup(id, SHORT_LOCO_ID_PROG, callback);\n  else\n      DCCACK::Setup(id | 0xc000,LONG_LOCO_ID_PROG, callback);\n}\n\nvoid DCC::setConsistId(int id,bool reverse,ACK_CALLBACK callback) {\n  if (id<0 || id>10239) { //0x27FF according to standard\n    callback(-1);\n    return;\n  }\n  byte cv20;\n  byte cv19;\n\n  if (id<=HIGHEST_SHORT_ADDR) {\n    cv19=id;\n    cv20=0;\n  }\n  else {\n    cv20=id/100;\n    cv19=id%100;\n  }\n  if (reverse) cv19|=0x80;\n  DCCACK::Setup((cv20<<8)|cv19, CONSIST_ID_PROG, callback);\n}\n\nvoid DCC::forgetLoco(int cab) {  // removes any speed reminders for this loco\n  auto slot=LocoSlot::getSlot(cab, false);\n  if (slot) {\n     setThrottleDCC(slot,1); // ESTOP this loco if still on track\n     if (nextLocoReminder==slot) nextLocoReminder=slot->getNext(); // move on if we are about to forget this one\n    if (slot->isConsistLead() || slot->isConsistFollower()) \n      DCCConsist::deleteAnyConsist(cab); // unchain any consist\n    slot->forget(); // no longer used but not end of world\n    CommandDistributor::broadcastForgetLoco(cab);\n  }\n}\nvoid DCC::forgetAllLocos() {  // removes all speed reminders\n  setThrottleDCC(nullptr,1); // ESTOP all locos still on track\n  SLOTLOOP {\n     CommandDistributor::broadcastForgetLoco(slot->getLoco());\n  }\n  LocoSlot::forgetAll(); // will effectively forget all consist chains \n  nextLocoReminder=nullptr;  \n}\n\nbyte DCC::loopStatus=0;\n\nvoid DCC::loop()  {\n  TrackManager::loop(); // power overload checks\n  if (DCCWaveform::mainTrack.isReminderWindowOpen()) {\n    // Now is a good time to choose a packet to be sent\n    // Either highest priority from the queues or a reminder\n    if (!DCCQueue::scheduleNext(false)) {\n      // none pending, \n      issueReminders();\n      DCCQueue::scheduleNext(true); // send any pending and force an idle if none \n    }\n\n  }\n}\n\nvoid DCC::issueReminders() {\n\n  // note, chainModified is set whenever a loco is added or removed\n  // so we dont accidentally follow a stale pointer.\n\n  if (LocoSlot::chainModified || nextLocoReminder==nullptr) {\n    LocoSlot::chainModified=false;\n    nextLocoReminder=LocoSlot::getFirst(); // start at the beginning\n    if (nextLocoReminder==nullptr) return; // no locos at all\n    // we have reached the end of the table, so we can move on to \n    // the next loop state and start from the top.\n    // There are 0-9 loop states..  speed,f1,speed,f2,speed,f3,speed,f4,speed,f5\n    loopStatus++;\n    if (loopStatus>9) loopStatus=0; // reset to 0\n  }\n\n  for (auto slot=nextLocoReminder;slot;slot=slot->getNext()) {\n    if (issueReminder(slot)) {\n      nextLocoReminder=slot->getNext(); // remember next one to check\n      return; // reminder sent, exit\n      }\n    }\n  \n    // if we get to her, we have reached the end of the table.\n    // The next call will move on to restart \n    nextLocoReminder=nullptr;\n  \n}\n\nint16_t normalize(byte speed) {\n   if (speed & 0x80) return speed & 0x7F;\n   return 0-1-speed; \n}\n\nbyte dccalize(int16_t speed) {\n   if (speed>127) return 0xFF;  // 127 forward\n   if (speed<-127) return 0x7F;  // 127 reverse\n   if (speed >=0) return speed | 0x80;\n   // negative speeds... -1==dcc 0, -2==dcc 1 \n   return (int16_t)-1 - speed; \n}\n\nbool DCC::issueReminder(LocoSlot * slot) {\n  unsigned long functions=slot->getFunctions();\n  int loco=slot->getLoco();\n  byte flags=slot->getGroupFlags();\n\n  switch (loopStatus) {\n        case 0:\n        case 2:\n        case 4:\n        case 6:\n        case 8: {\n         // dont remind consist followers here \n         if (slot->isConsistFollower()) break; \n\n         // calculate any momentum change going on\n         auto sc=slot->getSpeedCode();\n         if (slot->getTargetSpeed() != sc) {\n            // calculate new speed code \n            auto now=millis();\n            int16_t delay=now-slot->getMomentumBase();\n            auto millisPerNotch=MOMENTUM_FACTOR * (int16_t)getMomentum(slot);\n            // allow for momentum change to 0 while accelerating/slowing\n            auto ticks=(millisPerNotch>0)?(delay/millisPerNotch):500;\n            if (ticks>0) {\n              auto current=normalize(sc);  // -128..+127\n              auto target=normalize(slot->getTargetSpeed());\n              // DIAG(F(\"Momentum l=%d ti=%d sc=%d c=%d t=%d\"),loco,ticks,sc,current,target);\n              if (current<target) { // accelerate\n                current+=ticks;\n                if (current>target) current=target;\n              }\n              else  { // slow\n                current-=ticks;\n                if (current<target) current=target;\n              }\n              sc=dccalize(current);\n              //DIAG(F(\"c=%d newsc=%d\"),current,sc);\n              slot->setSpeedCode(sc);\n              if (!estopIsLocked) TrackManager::setDCSignal(loco,sc); // in case this is a dcc track on this addr\n              slot->setMomentumBase(now);  \n            }\n          }\n          // DIAG(F(\"Reminder %d speed %d\"),loco,slot->speedCode);\n          setThrottle2(slot, sc); // includes consist followers\n        }\n        return true; // reminder sent\n       case 1: // remind function group 1 (F0-F4)\n          if (flags & FN_GROUP_1) {\n            setFunctionInternal(loco,1,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD\n            return true;  // reminder sent\n          }\n          break;\n       case 3: // remind function group 2 F5-F8\n          if (flags & FN_GROUP_2) {\n  \t        setFunctionInternal(loco,2,0, 176 | ((functions>>5)& 0x0F));      // 1011 DDDD\n            return true;  // reminder sent\n          }\n          break;\n       case 5: // remind function group 3 F9-F12\n          if (flags & FN_GROUP_3) {\n\t          setFunctionInternal(loco,3,0, 160 | ((functions>>9)& 0x0F));    // 1010 DDDD\n            return true;  // reminder sent\n          }\n          break;\n       case 7: // remind function group 4 F13-F20\n          if (flags & FN_GROUP_4) {\n\t          setFunctionInternal(loco,4,222, ((functions>>13)& 0xFF));\n            return true; \n          }\n          break;\n       case 9: // remind function group 5 F21-F28\n          if (flags & FN_GROUP_5) {\n\t          setFunctionInternal(loco,5,223, ((functions>>21)& 0xFF));\n            return true;  // reminder sent\n          }\n          break;\n      }\n      return false; // no reminder sent \n    }\n\n\n\n\n///// Private helper functions below here /////////////////////\n\nbyte DCC::cv1(byte opcode, int cv)  {\n  cv--;\n  return (highByte(cv) & (byte)0x03) | opcode;\n}\nbyte DCC::cv2(int cv)  {\n  cv--;\n  return lowByte(cv);\n}\n\n\nbool DCC::setMomentum(int locoId,int16_t accelerating, int16_t decelerating) {\n  if (locoId<0) return false;\n  if (locoId==0) {\n    if (accelerating<0 || decelerating<0) return false;\n    defaultMomentumA=accelerating/MOMENTUM_FACTOR;\n    defaultMomentumD=decelerating/MOMENTUM_FACTOR;\n    return true;\n  }\n  // -1 is ok and means this loco should use the default.\n  if (accelerating<-1 || decelerating<-1) return false;\n  if (accelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT || \n      decelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT) return false;\n  \n  // Values stored are 255=MOMENTUM_USE_DEFAULT, or millis/MOMENTUM_FACTOR.\n  // This is to keep the values in a byte rather than int16\n  // thus saving 2 bytes RAM per loco slot.   \n  auto slot=LocoSlot::getSlot(locoId,true);\n  if (!slot) return false; // speed table full, can not do anything\n  \n  slot->setMomentumA((accelerating<0)? MOMENTUM_USE_DEFAULT: (accelerating/MOMENTUM_FACTOR));\n  slot->setMomentumD((decelerating<0)? MOMENTUM_USE_DEFAULT: (decelerating/MOMENTUM_FACTOR));\n  return true; \n}\n\n// ESTOP functions:\n// estopAll() - estop all locos (broadcast dcc) and set their reminders\n//              to speed 0 with their current direction preserved.\n//              Throttles will be told, but can immediately start driving again.\n// estopLock(true) - estop all locos (broadcast dcc) and lock the estop state. \n//              While locked, no loco can be moved because any throttle packets will\n//              be blocked at the DCC/DC level and replaced by the estop broadcast packet.\n//              Reminders are unchanged so retain the original speed/direction ready for the restart.\n//              Throttles will show original speed and can be changed to affect the speed that\n//              will be used when the estop is unlocked. \n// estopLock(false) - unlocks the estop state and all locos will resume their \n//              previous (or changed since locking) speeds as the reminder loop continues.\n\nvoid  DCC::estopAll() {\n  setThrottleDCC(nullptr,1); // estop all locos\n  TrackManager::setDCSignal(0,1); \n    \n  // remind stop/estop but dont change direction\n  for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext()) {\n    byte newspeed=(slot->getTargetSpeed() & 0x80) |  0x01;\n    slot->setSpeedCode(newspeed);\n    slot->setTargetSpeed(newspeed);\n    CommandDistributor::broadcastLoco(slot);  \n  }\n}\n\nbool DCC::estopIsLocked=false;\n\nbool DCC::isEstopLocked() {\n  return estopIsLocked;\n}\n\nvoid DCC::estopLock( bool lock) {\n  // see notes above about estop functions. \n  if (estopIsLocked==lock) return; // no change\n  estopIsLocked=lock;\n  if (lock) {\n    setThrottleDCC(nullptr, 1); // broadcast estop to DCC\n    TrackManager::setDCSignal(0, 1); // stop DC signal on all tracks\n  }\n  CommandDistributor::broadcastEstopLock(estopIsLocked); // tell throttle users\n}\n\n\nLocoSlot  *  DCC::nextLocoReminder = nullptr;\n\n\nvoid DCC::displayCabList(Print * stream) {\n    LocoSlot::dumpTable(stream);\n     StringFormatter::send(stream,F(\"<* Default momentum=%d/%d *>\\n\"),\n              DCC::defaultMomentumA,DCC::defaultMomentumD);\n}\n"
  },
  {
    "path": "DCC.h",
    "content": "/*\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2021 Herb Morton\n *  © 2020-2021 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DCC_h\n#define DCC_h\n#include <Arduino.h>\n#include \"MotorDriver.h\"\n#include \"MotorDrivers.h\"\n#include \"FSH.h\"\n\n#include \"defines.h\"\n#ifndef HIGHEST_SHORT_ADDR\n#define HIGHEST_SHORT_ADDR 127\n#else\n#if HIGHEST_SHORT_ADDR > 127\n#error short addr greater than 127 does not make sense\n#endif\n#endif\n#include \"DCCACK.h\"\n#include \"LocoSlot.h\"\nconst uint16_t LONG_ADDR_MARKER = 0x4000;\n\nclass DCC\n{\npublic:\n  static inline void setShieldName(const FSH * motorShieldName) {\n    shieldName=(FSH *)motorShieldName;\n  };\n  static void begin();\n  static void loop();\n\n  // Public DCC API functions\n  static bool setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);\n  static void estopAll();\n  static void estopLock(bool lock);\n  static bool isEstopLocked();\n\n  static int8_t getThrottleSpeed(int cab);\n  static uint8_t getThrottleSpeedByte(int cab);\n  static uint8_t getLocoSpeedByte(int cab); // may lag throttle \n  static uint8_t getThrottleFrequency(int cab);\n  static bool getThrottleDirection(int cab);\n  static void writeCVByteMain(int cab, int cv, byte bValue);\n  static void readCVByteMain(int cab, int cv, ACK_CALLBACK callback);\n  \n  static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);\n  static void setFunction(int cab, byte fByte, byte eByte);\n  static bool setFn(int cab, int16_t functionNumber, bool on);\n  static void changeFn(int cab, int16_t functionNumber);\n  static int8_t getFn(int cab, int16_t functionNumber);\n  static uint32_t getFunctionMap(int cab);\n  static void setDCFreq(int cab,byte freq);\n  static void updateGroupflags(LocoSlot * slot , int16_t functionNumber);\n  static void setAccessory(int address, byte port, bool gate, byte onoff = 2);\n  static bool setExtendedAccessory(int16_t address, int16_t value, byte repeats=3);\n  static bool writeTextPacket(byte *b, int nBytes);\n  \n  // ACKable progtrack calls  bitresults callback 0,0 or -1, cv returns value or -1\n  static void readCV(int16_t cv, ACK_CALLBACK callback);\n  static void readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback); // -1 for error\n  static void writeCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);\n  static void writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);\n  static void verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);\n  static void verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);\n  static bool setTime(uint16_t minutes,uint8_t speed, bool suddenChange);\n  static void getDriveawayLocoId(ACK_CALLBACK callback);\n  static void getLocoId(ACK_CALLBACK callback);\n  static void getConsistId(ACK_CALLBACK callback);\n  static void setLocoId(int id,ACK_CALLBACK callback);\n  static void setConsistId(int id,bool reverse,ACK_CALLBACK callback);\n  // Enhanced API functions\n  static void forgetLoco(int cab); // removes any speed reminders for this loco\n  static void forgetAllLocos();    // removes all speed reminders\n  static void saveSpeed(int cab);   // saves speed for later restore\n  static void restoreSpeed(int cab); // restores saved speed  \n  static void displayCabList(Print *stream);\n  static FSH *getMotorShieldName();\n  static inline void setGlobalSpeedsteps(byte s) {\n    globalSpeedsteps = s;\n  };\n  \n  static inline byte getGlobalSpeedsteps() {\n    return globalSpeedsteps;\n  };\n\n static const int16_t MOMENTUM_FACTOR=7;  \n \n static bool linearAcceleration;  \n \n static byte cv1(byte opcode, int cv);\n static byte cv2(int cv);\n static bool setMomentum(int locoId,int16_t accelerating, int16_t decelerating);\n static byte getMomentum(LocoSlot * slot);\nprivate:\n  static byte loopStatus;\n  static byte defaultMomentumA;  // Accelerating\n  static byte defaultMomentumD;  // Accelerating\n  static void setThrottle2(LocoSlot * slot, uint8_t speedCode);\n  static void setThrottleDCC(LocoSlot * slot, uint8_t speedCode);\n  static void setFunctionInternal(int cab, byte group, byte fByte, byte eByte);\n  static bool issueReminder(LocoSlot  * slot);\n  static LocoSlot * nextLocoReminder;\n  static FSH *shieldName;\n  static byte globalSpeedsteps;\n  static bool estopIsLocked;\n  static void issueReminders();\n  static void callback(int value);\n\n  \n  // NMRA codes #\n  static const byte SET_SPEED = 0x3f;\n  static const byte WRITE_BYTE_MAIN = 0xEC;\n  static const byte READ_BYTE_MAIN = 0xE4;\n  static const byte WRITE_BIT_MAIN = 0xE8;\n  static const byte WRITE_BYTE = 0x7C;\n  static const byte VERIFY_BYTE = 0x74;\n  static const byte BIT_MANIPULATE = 0x78;\n  static const byte WRITE_BIT = 0xF0;\n  static const byte VERIFY_BIT = 0xE0;\n  static const byte BIT_ON = 0x08;\n  static const byte BIT_OFF = 0x00;\n};\n\n#endif\n"
  },
  {
    "path": "DCCACK.cpp",
    "content": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2025 Harald Barth\n *  © 2020-2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include \"DCCACK.h\"\n#include \"DIAG.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"TrackManager.h\"\n\nunsigned long DCCACK::minAckPulseDuration = 2000; // micros\nunsigned long DCCACK::maxAckPulseDuration = 20000; // micros\n  \nMotorDriver *  DCCACK::progDriver=NULL;\nackOp  const *  DCCACK::ackManagerProg;\nackOp  const *  DCCACK::ackManagerProgStart;\nbyte   DCCACK::ackManagerByte;\nbyte   DCCACK::ackManagerByteVerify;\nbyte   DCCACK::ackManagerStash;\nint    DCCACK::ackManagerWord;\nbyte   DCCACK::ackManagerRetry;\nbyte   DCCACK::ackRetry = 2;\nint16_t  DCCACK::ackRetrySum;\nint16_t  DCCACK::ackRetryPSum;\nint    DCCACK::ackManagerCv;\nbyte   DCCACK::ackManagerBitNum;\nbool   DCCACK::ackReceived;\nbool   DCCACK::ackManagerRejoin;\nvolatile uint8_t DCCACK::numAckGaps=0;\nvolatile uint8_t DCCACK::numAckSamples=0;\nuint8_t DCCACK::trailingEdgeCounter=0;\n\n\nunsigned long DCCACK::ackPulseDuration;  // micros\nunsigned long DCCACK::ackPulseStart; // micros\n volatile bool DCCACK::ackDetected;\n unsigned long DCCACK::ackCheckStart; // millis\n volatile bool DCCACK::ackPending;\n  bool   DCCACK::autoPowerOff;\n   int  DCCACK::ackThreshold; \n   int  DCCACK::ackLimitmA = 50;\n     int DCCACK::ackMaxCurrent;\n      unsigned int DCCACK::ackCheckDuration; // millis       \n    \n    \nCALLBACK_STATE DCCACK::callbackState=READY;\n\nACK_CALLBACK DCCACK::ackManagerCallback;\n\nvoid  DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {\n  // On ESP32 the joined track is hidden from sight (it has type MAIN)\n  // and because of that we need first check if track was joined and\n  // then unjoin if necessary. This requires that the joined flag is\n  // cleared when the prog track is removed.\n  ackManagerRejoin=TrackManager::isJoined();\n  //DIAG(F(\"Joined is %d\"), ackManagerRejoin);\n  if (ackManagerRejoin) {\n    // Change from JOIN must zero resets packet.\n    TrackManager::setJoin(false);\n    DCCWaveform::progTrack.clearResets();\n  }\n  progDriver=TrackManager::getProgDriver();\n  //DIAG(F(\"Progdriver is %d\"), progDriver);\n  if (progDriver==NULL) {\n    if (ackManagerRejoin) {\n      DIAG(F(\"Joined but no Prog track\"));\n      TrackManager::setJoin(false);\n    }\n    callback(-3); // we dont have a prog track!\n    return;\n  }\n  if (!progDriver->canMeasureCurrent()) {\n    TrackManager::setJoin(ackManagerRejoin);\n    callback(-2); // our prog track cant measure current\n    return;\n  }\n\n   autoPowerOff=false;\n   if (progDriver->getPower() == POWERMODE::OFF) {\n        autoPowerOff=true;  // power off afterwards\n        if (Diag::ACK) DIAG(F(\"Auto Prog power on\"));\n        progDriver->setPower(POWERMODE::ON);\n\t\n    /* TODO !!! in MotorDriver surely!\n    if (MotorDriver::commonFaultPin)\n\t  DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);\n        DCCWaveform::progTrack.clearResets();\n   **/\n      }\n\n\n  ackManagerCv = cv;\n  ackManagerProg = program;\n  ackManagerProgStart = program;\n  ackManagerRetry = ackRetry;\n  ackManagerByte = byteValueOrBitnum;\n  ackManagerByteVerify = byteValueOrBitnum;\n  ackManagerBitNum=byteValueOrBitnum;\n  ackManagerCallback = callback;\n}\n\nvoid  DCCACK::Setup(int wordval, ackOp const program[], ACK_CALLBACK callback) {\n  ackManagerWord=wordval;\n  Setup(0, 0, program, callback);\n  }\n\nconst byte RESET_MIN=8;  // tuning of reset counter before sending message\n\n// checkRessets return true if the caller should yield back to loop and try later.\nbool DCCACK::checkResets(uint8_t numResets) {\n  return DCCWaveform::progTrack.getResets() < numResets;\n}\n// Operations applicable to PROG track ONLY.\n// (yes I know I could have subclassed the main track but...) \n\nvoid DCCACK::setAckBaseline() {\n      int baseline=progDriver->getCurrentRaw();\n      ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);\n      if (Diag::ACK) DIAG(F(\"ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %lus and %lus\"),\n\t\t\t  baseline,progDriver->raw2mA(baseline),\n\t\t\t  ackThreshold,progDriver->raw2mA(ackThreshold),\n                          minAckPulseDuration, maxAckPulseDuration);\n}\n\nvoid DCCACK::setAckPending() {\n      ackMaxCurrent=0;\n      ackPulseStart=0;\n      ackPulseDuration=0;\n      ackDetected=false;\n      ackCheckStart=millis();\n      numAckSamples=0;\n      numAckGaps=0;\n      ackPending=true;  // interrupt routines will now take note\n}\n\nbyte DCCACK::getAck() {\n      if (ackPending) return (2);  // still waiting\n      if (Diag::ACK) DIAG(F(\"%S after %dmS max=%d/%dmA pulse=%luS samples=%d gaps=%d\"),ackDetected?F(\"ACK\"):F(\"NO-ACK\"), ackCheckDuration,\n\t\t\t  ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);\n      if (ackDetected) return (1); // Yes we had an ack\n      return(0);  // pending set off but not detected means no ACK.   \n}\n\n#ifndef DISABLE_PROG\nvoid DCCACK::loop() {\n  while (ackManagerProg) {\n    byte opcode=GETFLASH(ackManagerProg);\n\n    // breaks from this switch will step to next prog entry\n    // returns from this switch will stay on same entry\n    // (typically waiting for a reset counter or ACK waiting, or when all finished.)\n    switch (opcode) {\n      case BASELINE:\n          if (progDriver->getPower()==POWERMODE::OVERLOAD) return;\n      \t  if (checkResets(autoPowerOff || ackManagerRejoin  ? 20 : 3)) return;\n          setAckBaseline();\n          callbackState=AFTER_READ;\n          break;\n      case W0:    // write 0 bit\n      case W1:    // write 1 bit\n            {\n\t      if (checkResets(RESET_MIN)) return;\n              if (Diag::ACK) DIAG(F(\"W%d cv=%d bit=%d\"),opcode==W1, ackManagerCv,ackManagerBitNum);\n              byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;\n              byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };\n              DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);\n              setAckPending();\n             callbackState=AFTER_WRITE;\n         }\n            break;\n\n      case WB:   // write byte\n            {\n\t      if (checkResets( RESET_MIN)) return;\n              if (Diag::ACK) DIAG(F(\"WB cv=%d value=%d\"),ackManagerCv,ackManagerByte);\n              byte message[] = {DCC::cv1(WRITE_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};\n              DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);\n              setAckPending();\n              callbackState=AFTER_WRITE;\n            }\n            break;\n\n      case   VB:     // Issue validate Byte packet\n        {\n\t  if (checkResets( RESET_MIN)) return;\n          if (Diag::ACK) DIAG(F(\"VB cv=%d value=%d\"),ackManagerCv,ackManagerByte);\n          byte message[] = { DCC::cv1(VERIFY_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};\n          DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);\n          setAckPending();\n        }\n        break;\n\n      case V0:\n      case V1:      // Issue validate bit=0 or bit=1  packet\n        {\n\t  if (checkResets(RESET_MIN)) return;\n          if (Diag::ACK) DIAG(F(\"V%d cv=%d bit=%d\"),opcode==V1, ackManagerCv,ackManagerBitNum);\n          byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;\n          byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };\n          DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);\n          setAckPending();\n        }\n        break;\n\n      case WACK:   // wait for ack (or absence of ack)\n         {\n          byte ackState=2; // keep polling\n\n          ackState=getAck();\n          if (ackState==2) return; // keep polling\n          ackReceived=ackState==1;\n          break;  // we have a genuine ACK result\n         }\n     case ITC0:\n     case ITC1:   // If True Callback(0 or 1)  (if prevous WACK got an ACK)\n        if (ackReceived) {\n            callback(opcode==ITC0?0:1);\n            return;\n          }\n        break;\n\n      case ITCB:   // If True callback(byte)\n          if (ackReceived) {\n            callback(ackManagerByte);\n            return;\n          }\n        break;\n\t\t\n      case ITCBV:   // If True callback(byte) - Verify\n          if (ackReceived) {\n            if (ackManagerByte == ackManagerByteVerify) {\n               ackRetrySum ++;\n               LCD(1, F(\"v %d %d Sum=%d\"), ackManagerCv, ackManagerByte, ackRetrySum);\n            }\n            callback(ackManagerByte);\n            return;\n          }\n        break;\n\t\t\n      case ITCB7:   // If True callback(byte & 0x7F)\n          if (ackReceived) {\n            callback(ackManagerByte & 0x7F);\n            return;\n          }\n        break;\n\n      case NAKFAIL:   // If nack callback(-1)\n          if (!ackReceived) {\n            callback(-1);\n            return;\n          }\n        break;\n\n      case CALLFAIL:  // callback(-1)\n           callback(-1);\n           return;\n\n      case BIV:     // ackManagerByte initial value\n           ackManagerByte = ackManagerByteVerify;\n           break;\n\n      case STARTMERGE:\n           ackManagerBitNum=7;\n           ackManagerByte=0;\n          break;\n\n      case MERGE:  // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)\n          ackManagerByte <<= 1;\n          // ackReceived means bit is zero.\n          if (!ackReceived) ackManagerByte |= 1;\n          ackManagerBitNum--;\n          break;\n\n      case SETBIT:\n          ackManagerProg++;\n          ackManagerBitNum=GETFLASH(ackManagerProg);\n          break;\n\n     case SETCV:\n          ackManagerProg++;\n          ackManagerCv=GETFLASH(ackManagerProg);\n          break;\n\n     case SETBYTE:\n          ackManagerProg++;\n          ackManagerByte=GETFLASH(ackManagerProg);\n          break;\n\n    case SETBYTEH:\n          ackManagerByte=highByte(ackManagerWord);\n          break;\n\n    case SETBYTEL:\n          ackManagerByte=lowByte(ackManagerWord);\n          break;\n\n     case STASHLOCOID:\n          ackManagerStash=ackManagerByte;  // stash value from CV17\n          break;\n\n     case COMBINELOCOID:\n          // ackManagerStash is  cv17, ackManagerByte is CV 18\n          callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));\n          return;\n\n     case COMBINE1920:\n          // ackManagerStash is  cv20, ackManagerByte is CV 19\n          // This will not be called if cv20==0\n          ackManagerByte &= 0x7F;  // ignore direction marker\n          ackManagerByte %=100;    // take last 2 decimal digits \n          callback( ackManagerStash*100+ackManagerByte);\n          return;\n\n     case ITSKIP:\n          if (!ackReceived) break;\n          // SKIP opcodes until SKIPTARGET found\n          while (opcode!=SKIPTARGET) {\n            ackManagerProg++;\n            opcode=GETFLASH(ackManagerProg);\n          }\n          break;\n\n     case NAKSKIP:\n          if (ackReceived) break;\n          // SKIP opcodes until SKIPTARGET found\n          while (opcode!=SKIPTARGET) {\n            ackManagerProg++;\n            opcode=GETFLASH(ackManagerProg);\n          }\n          break;\n     case BAD20SKIP:\n          if (ackManagerByte > 120) {\n            // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)\n            if (Diag::ACK) DIAG(F(\"XX cv20=%d \"),ackManagerByte);\n            while (opcode!=SKIPTARGET) {\n              ackManagerProg++;\n              opcode=GETFLASH(ackManagerProg);\n            }\n          }\n          break; \n     case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it\n          if (ackManagerByte==0) break;\n          callback(-4);  \n          return;           \n     case SKIPTARGET:\n          break;\n     default:\n          DIAG(F(\"ackOp %d FAULT\"),opcode);\n          callback( -1);\n          return;\n\n      }  // end of switch\n    ackManagerProg++;\n  }\n}\n\nvoid DCCACK::callback(int value) {\n    // check for automatic retry\n    if (value == -1 && ackManagerRetry > 0) {\n      ackRetrySum ++;\n      LCD(0, F(\"Retry %d %d Sum=%d\"), ackManagerCv, ackManagerRetry, ackRetrySum);\n      ackManagerRetry --;\n      ackManagerProg = ackManagerProgStart;\n      return;\n    }\n\n    static unsigned long callbackStart;\n    // We are about to leave programming mode\n    // Rule 1: If we have written to a decoder we must maintain power for 100mS\n    // Rule 2: If we are re-joining the main track we must power off for 30mS\n\n    const FSH *off30ms = F(\"OFF 30mS\");\n    switch (callbackState) {\n      case AFTER_READ:\n         if (ackManagerRejoin && !autoPowerOff) {\n                progDriver->setPower(POWERMODE::OFF);\n                callbackStart=millis();\n                callbackState=WAITING_30;\n                if (Diag::ACK) DIAG(off30ms);\n        } else {\n               callbackState=READY;\n        }\n        break;\n\n      case AFTER_WRITE:  // first attempt to callback after a write operation\n\t    if (!ackManagerRejoin && !autoPowerOff) {\n               callbackState=READY;\n               break;\n            }                              // lines 906-910 added. avoid wait after write. use 1 PROG\n            callbackStart=millis();\n            callbackState=WAITING_100;\n            if (Diag::ACK) DIAG(F(\"Stable 100mS\"));\n            break;\n\n       case WAITING_100:  // waiting for 100mS\n            if (millis()-callbackStart < 100) break;\n            // stable after power maintained for 100mS\n\n            // If we are going to power off anyway, it doesnt matter\n            // but if we will keep the power on, we must off it for 30mS\n            if (autoPowerOff) callbackState=READY;\n            else { // Need to cycle power off and on\n                progDriver->setPower(POWERMODE::OFF);\n                callbackStart=millis();\n                callbackState=WAITING_30;\n                if (Diag::ACK) DIAG(off30ms);\n            }\n            break;\n\n        case WAITING_30:  // waiting for 30mS with power off\n            if (millis()-callbackStart < 30) break;\n            //power has been off for 30mS\n            progDriver->setPower(POWERMODE::ON);\n            callbackState=READY;\n            break;\n\n       case READY:  // ready after read, or write after power delay and off period.\n            // power off if we powered it on\n           if (autoPowerOff) {\n              if (Diag::ACK) DIAG(F(\"Auto Prog power off\"));\n              progDriver->setPower(POWERMODE::OFF);\n              /* TODO \n\t      if (MotorDriver::commonFaultPin)\n\t\tDCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);\n        **/\n           }\n          // Restore <1 JOIN> to state before BASELINE\n          if (ackManagerRejoin) {\n              TrackManager::setJoin(true);\n              if (Diag::ACK) DIAG(F(\"Auto JOIN\"));\n          }\n\n          ackManagerProg=NULL;  // no more steps to execute\n          if (Diag::ACK) DIAG(F(\"Callback(%d)\"),value);\n          (ackManagerCallback)( value);\n    }\n}\n#endif\n\nvoid DCCACK::checkAck(byte sentResetsSincePacket) {\n    if (!ackPending) return; \n    // This function operates in interrupt() time so must be fast and can't DIAG \n    if (sentResetsSincePacket > 6) {  //ACK timeout\n        ackCheckDuration=millis()-ackCheckStart;\n        ackPending = false;\n        return; \n    }\n      \n    int current=progDriver->getCurrentRaw(true); // true means \"from interrupt\"\n    numAckSamples++;\n    if (current > ackMaxCurrent) ackMaxCurrent=current;\n    // An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)\n        \n    if (current>ackThreshold) {\n       if (trailingEdgeCounter > 0) {\n\t numAckGaps++;\n\t trailingEdgeCounter = 0;\n       }\n       if (ackPulseStart==0) ackPulseStart=micros();    // leading edge of pulse detected\n       return;\n    }\n    \n    // not in pulse\n    if (ackPulseStart==0) return; // keep waiting for leading edge \n    \n    // if we reach to this point, we have\n    // detected trailing edge of pulse\n    if (trailingEdgeCounter == 0) {\n      ackPulseDuration=micros()-ackPulseStart;\n    }\n\n    // but we do not trust it yet and return (which will force another\n    // measurement) and first the third time around with low current\n    // the ack detection will be finalized. \n    if (trailingEdgeCounter < 2) {\n      trailingEdgeCounter++;\n      return;\n    }\n    trailingEdgeCounter = 0;\n\n    if (ackPulseDuration>=minAckPulseDuration\n\t&& (ackPulseDuration<=maxAckPulseDuration || maxAckPulseDuration == 0)) {\n        ackCheckDuration=millis()-ackCheckStart;\n        ackDetected=true;\n        ackPending=false;\n        DCCWaveform::progTrack.clearRepeats();  // shortcut remaining repeat packets \n        return;  // we have a genuine ACK result\n    }      \n    ackPulseStart=0;  // We have detected a too-short or too-long pulse so ignore and wait for next leading edge \n}\n"
  },
  {
    "path": "DCCACK.h",
    "content": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DCCACK_h\n#define DCCACK_h\n\n#include \"MotorDriver.h\"\n\ntypedef void (*ACK_CALLBACK)(int16_t result);\n\nenum ackOp : byte\n{           // Program opcodes for the ack Manager\n  BASELINE, // ensure enough resets sent before starting and obtain baseline current\n  W0,\n  W1,               // issue write bit (0..1) packet\n  WB,               // issue write byte packet\n  VB,               // Issue validate Byte packet\n  V0,               // Issue validate bit=0 packet\n  V1,               // issue validate bit=1 packlet\n  WACK,             // wait for ack (or absence of ack)\n  ITC1,             // If True Callback(1)  (if prevous WACK got an ACK)\n  ITC0,             // If True callback(0);\n  ITCB,             // If True callback(byte)\n  ITCBV,            // If True callback(byte) - end of Verify Byte\n  ITCB7,            // If True callback(byte &0x7F)\n  NAKFAIL,          // if false callback(-1)\n  CALLFAIL,         // callback(-1)\n  BIV,              // Set ackManagerByte to initial value for Verify retry\n  STARTMERGE,       // Clear bit and byte settings ready for merge pass\n  MERGE,            // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)\n  SETBIT,           // sets bit number to next prog byte\n  SETCV,            // sets cv number to next prog byte\n  SETBYTE,          // sets current byte to next prog byte\n  SETBYTEH,         // sets current byte to word high byte\n  SETBYTEL,         // sets current byte to word low byte\n  STASHLOCOID,      // keeps current byte value for later\n  COMBINELOCOID,    // combines current value with stashed value and returns it\n  ITSKIP,           // skip to SKIPTARGET if ack true\n  NAKSKIP,          // skip to SKIPTARGET if ack false\n  COMBINE1920,      // combine cvs 19 and 20 and callback\n  BAD20SKIP,        // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255) \n  FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it\n  SKIPTARGET = 0xFF // jump to target\n};\n\nenum   CALLBACK_STATE : byte {\n\n  AFTER_READ,   // Start callback sequence after something was read from the decoder  \n  AFTER_WRITE,  // Start callback sequence after something was written to the decoder  \n  WAITING_100,        // Waiting for 100mS of stable power \n  WAITING_30,         // waiting to 30ms of power off gap. \n  READY,              // Ready to complete callback  \n  }; \n\n\n\nclass DCCACK {\n  public:\n    static byte getAck();               //prog track only 0=NACK, 1=ACK 2=keep waiting\n    static void checkAck(byte sentResetsSincePacket); // Interrupt time ack checker\n    static inline void setAckLimit(int mA) {\n\tackLimitmA = mA;\n    }\n    static inline void setMinAckPulseDuration(unsigned long i) {\n\tminAckPulseDuration = i;\n    }\n    static inline void setMaxAckPulseDuration(unsigned long i) {\n\tmaxAckPulseDuration = i;\n    }\n\n    static void  Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback);\n    static void  Setup(int wordval, ackOp const program[], ACK_CALLBACK callback);\n    static void loop();\n    static bool isActive() { return ackManagerProg!=NULL;}\n  static inline int16_t setAckRetry(byte retry) {\n    ackRetry = retry;\n    ackRetryPSum = ackRetrySum;\n    ackRetrySum = 0;  // reset running total\n    return ackRetryPSum;\n  };\n\n\n  private:\n    static const byte SET_SPEED = 0x3f;\n    static const byte WRITE_BYTE = 0x7C;\n    static const byte VERIFY_BYTE = 0x74;\n    static const byte BIT_MANIPULATE = 0x78;\n    static const byte WRITE_BIT = 0xF0;\n    static const byte VERIFY_BIT = 0xE0;\n    static const byte BIT_ON = 0x08;\n    static const byte BIT_OFF = 0x00;\n \n    static void setAckBaseline();\n    static void setAckPending();  \n    static void callback(int value);\n    \n    static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)\n    \n    // ACK management (Prog track only)  \n    static void checkAck();\n    static bool checkResets(uint8_t numResets);\n\n    static volatile bool ackPending;\n    static volatile bool ackDetected;\n    static int  ackThreshold; \n    static int  ackLimitmA;\n    static int ackMaxCurrent;\n    static unsigned long ackCheckStart; // millis\n    static unsigned int ackCheckDuration; // millis       \n    \n    static unsigned long ackPulseDuration;  // micros\n    static unsigned long ackPulseStart; // micros\n\n    static unsigned long minAckPulseDuration ; // micros\n    static unsigned long maxAckPulseDuration ; // micros\n    static MotorDriver* progDriver;\n    static volatile uint8_t numAckGaps;\n    static volatile uint8_t numAckSamples;\n    static uint8_t trailingEdgeCounter;\n    static ackOp  const *  ackManagerProg;\nstatic ackOp  const *  ackManagerProgStart;\nstatic byte   ackManagerByte;\nstatic byte   ackManagerByteVerify;\nstatic byte   ackManagerStash;\nstatic int    ackManagerWord;\nstatic byte   ackManagerRetry;\nstatic byte   ackRetry;\nstatic int16_t ackRetrySum;\nstatic int16_t  ackRetryPSum;\nstatic int    ackManagerCv;\nstatic byte   ackManagerBitNum;\nstatic bool   ackReceived;\nstatic bool   ackManagerRejoin;\nstatic bool   autoPowerOff;\nstatic CALLBACK_STATE callbackState;\nstatic ACK_CALLBACK ackManagerCallback;\n\n\n};\n#endif\n"
  },
  {
    "path": "DCCConsist.cpp",
    "content": "#include \"DCCConsist.h\"\n#include \"LocoSlot.h\"\n#include \"StringFormatter.h\"\n#include \"DIAG.h\"\n\nbool DCCConsist::parse(Print * stream, byte params, int16_t p[]) {\n    if (params==0) {\n          for (auto slot=LocoSlot::getFirst();slot;slot=slot->getNext()) {\n            if (slot->isConsistLead()) {\n              StringFormatter::send(stream,F(\"<^\"));\n              for (auto cslot=slot;cslot;cslot=cslot->getConsistNext()) {\n                StringFormatter::send(stream,\n                    cslot->isConsistReverse() ? F(\" -%d\") : F(\" %d\"),\n                    cslot->getLoco());\n              }                 \n              StringFormatter::send(stream,F(\" >\\n\"));\n            }\n          }\n        return true;\n    }\n    // Detect any invalid locoids or duplicates \n    for (byte i=0;i<params;i++) {\n        uint16_t locoId=abs(p[i]);\n        if (locoId<1 || locoId>10239) {\n            StringFormatter::send(stream,F(\"<* Invalid locoid %d *>\"),p[i]);\n            return false;\n        }\n        for (byte j=i+1;j<params;j++) {\n            if (abs(p[j])==locoId) {\n                StringFormatter::send(stream,F(\"<* Duplicate locoid %d *>\"),p[i]);\n                return false;\n            }\n        }\n    }\n    // Cross check other consists  \n    // if p[0] is a consist lead, kill its consist first\n    auto slot=LocoSlot::getSlot(abs(p[0]),false);\n    if (slot && slot->isConsistLead()) {\n        DCCConsist::deleteAnyConsist(p[0]);\n    }\n    if (params<2) return true; // we only had to delete \n     \n    for (byte i=0;i<params;i++) {\n        auto slot=LocoSlot::getSlot(abs(p[i]),false);\n        if (slot && (slot->isConsistLead() || slot->isConsistFollower())) {\n            StringFormatter::send(stream,F(\"<* Loco %d in another consist *>\"),abs(p[i]));\n            return false;\n        }\n    }\n    \n    auto leadLoco=LocoSlot::getSlot(abs(p[0]),true);\n    leadLoco->setConsistReverse(p[0]<0);\n    LocoSlot *  prev=leadLoco;\n    for (byte i=1;i<params;i++) {\n        auto slot=LocoSlot::getSlot(abs(p[i]),true);\n        slot->setConsistLead(leadLoco);\n        slot->setConsistReverse(p[i]<0);\n        prev->setConsistNext(slot);        \n        prev=slot;\n    }\n    return true;\n}\n\nvoid DCCConsist::deleteAnyConsist(int16_t locoid) {\n    locoid=abs(locoid); // in case of (valid) negative\n    auto slot=LocoSlot::getSlot(locoid,false);\n    if (!slot) return; // no such loco, nothing to do\n    if (!slot->isConsistLead()) slot=slot->getConsistLead();\n    if (!slot) return; // not in consist, nothing to do\n\n    while(slot) {\n      auto next=slot->getConsistNext();\n      slot->setConsistLead(nullptr);\n      slot->setConsistNext(nullptr);\n      slot->setConsistReverse(false);\n      slot=next;\n    } \n}\n\nbool DCCConsist::addLocoToConsist(uint16_t consistId,uint16_t locoId, bool revesed) {\n    if (consistId<1 || consistId>10239) return false;\n    if (locoId<1 || locoId>10239) return false;\n    if (locoId==consistId) return true; // cant add lead to itself\n    \n    auto leadSlot=LocoSlot::getSlot(consistId,true);\n    if (!leadSlot) return false; // no ram\n    // back up to lead of existing consist\n    while(leadSlot->getConsistLead()) {\n        leadSlot=leadSlot->getConsistLead();\n    }\n\n    // find tail of consist\n    auto tailSlot=leadSlot;\n    while(tailSlot->getConsistNext()) {\n        tailSlot=tailSlot->getConsistNext();\n    }\n\n    auto addSlot=LocoSlot::getSlot(locoId,true);\n    if (!addSlot) return false; // no ram\n    \n    if (addSlot->isConsistLead() || addSlot->isConsistFollower()) {\n        DIAG(F(\"Loco %d already in a consist\"),locoId);\n        return false; // already in consist\n    }\n    \n    // All OK so chain in. (belt and braces prevent self reference loop)\n    addSlot->setConsistReverse(revesed);\n    if (addSlot!=leadSlot) addSlot->setConsistLead(leadSlot);    \n    if (tailSlot!=addSlot) tailSlot->setConsistNext(addSlot);\n    return true;\n}\n \n"
  },
  {
    "path": "DCCConsist.h",
    "content": "#ifndef DCCConsist_h\n#define DCCConsist_h\n\n#include <Arduino.h>\nclass DCCConsist {\n\n    public:\n      static bool parse(Print * stream, byte paramCount, int16_t p[]);\n\n      // deteles any consist chain containing locoid \n      static void deleteAnyConsist(int16_t locoid);\n      \n      //add loco anywhere in consist (may create consist) see EXRAIL.\n      static bool addLocoToConsist(uint16_t consistId,uint16_t locoid, bool revesed);\n\n};\n#endif\n"
  },
  {
    "path": "DCCDecoder.cpp",
    "content": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ARDUINO_ARCH_ESP32\n#include \"DCCDecoder.h\"\n#include \"LocoSlot.h\"\n#include \"DCCEXParser.h\"\n#include \"DIAG.h\"\n#include \"DCC.h\"\n\nbool DCCDecoder::parse(DCCPacket &p) {\n  if (!active)\n    return false;\n  const byte DECODER_MOBILE = 1;\n  const byte DECODER_ACCESSORY = 2;\n  byte decoderType = 0; // use 0 as none\n  byte *d = p.data();\n  byte *instr = 0;      // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])\n  uint16_t addr;        // will be set to decoder addr (long/shor mobile or accessory) \n  bool locoInfoChanged = false;\n\n  if (d[0] ==  0B11111111) {  // Idle packet\n    return false;\n  }\n  // CRC verification here\n  byte checksum = 0;\n  for (byte n = 0; n < p.len(); n++)\n    checksum ^= d[n];\n  if (checksum) {  // Result should be zero, if not it's an error!\n    if (Diag::SNIFFER) {\n      DIAG(F(\"Checksum error:\"));\n      p.print();\n    }\n    return false;\n  }\n\n/*\n  Serial.print(\"< \");\n  for(int n=0; n<8; n++) {\n    Serial.print(d[0]&(1<<n)?\"1\":\"0\");\n  }\n  Serial.println(\" >\");\n*/\n  if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr\n    decoderType = DECODER_MOBILE;\n    instr = d+1;\n    addr = d[0];\n  } else {\n    if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr\n      decoderType = DECODER_MOBILE;\n      instr = d+2;\n      addr = 256 * (d[0] & 0B00111111) + d[1];\n    } else { // bit7 == 1 and bit 6 == 0\n      decoderType = DECODER_ACCESSORY;\n      instr = d+1;\n      addr = d[0] &  0B00111111;\n    }\n  }\n  if (decoderType == DECODER_MOBILE) {\n    switch (instr[0] & 0xE0) {\n    case 0x20: // 001x-xxxx Extended commands\n      if (instr[0] == 0B00111111) { // 128 speed steps\n\tif ((locoInfoChanged = updateLoco(addr, instr[1])) == true) {\n\t  byte speed = instr[1] & 0B01111111;\n\t  byte direction = instr[1] & 0B10000000;\n\t  DCC::setThrottle(addr, speed, direction);\n\t  //DIAG(F(\"UPDATE\"));\n\t  // send speed change to DCCEX here\n\t}\n      }\n      break;\n    case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28\n    case 0x60: // 011x-xxxx\n      if ((locoInfoChanged = updateLoco(addr, instr[0] & 0B00111111)) == true) {\n\tbyte speed = instr[0] & 0B00001111; // first only look at 4 bits\n\tif (speed > 1) {               // neither stop nor emergency stop, recalculate speed\n\t  speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits\n\t  speed = (speed - 3) * 9/2;\n\t}\n\tbyte direction = instr[0] & 0B00100000;\n\tDCC::setThrottle(addr, speed, direction);\n      }\n      break;\n    case 0x80: // 100x-xxxx Function group 1\n      if ((locoInfoChanged = updateFunc(addr, instr[0], 1)) == true) {\n\tbyte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);\n\tDCCEXParser::funcmap(addr, normalized, 0, 4);\n      }\n      break;\n    case 0xA0: // 101x-xxxx Function group 3 and 2\n    {\n      byte low, high;\n      if (bitRead(instr[0], 4)) {\n\tlow = 5;\n\thigh = 8;\n      } else {\n\tlow = 9;\n\thigh = 12;\n      }\n      if ((locoInfoChanged = updateFunc(addr, instr[0], low)) == true) {\n\tDCCEXParser::funcmap(addr, instr[0], low, high);\n      }\n    }\n    break;\n    case 0xC0: // 110x-xxxx Extended (here are functions F13 and up\n      switch (instr[0] & 0B00011111) {\n      case 0B00011110:  // F13-F20 Function Control\n\tif ((locoInfoChanged = updateFunc(addr, instr[0], 13)) == true) {\n\t  DCCEXParser::funcmap(addr, instr[1], 13, 20);\n\t}\n\tif ((locoInfoChanged = updateFunc(addr, instr[0], 17)) == true) {\n\t  DCCEXParser::funcmap(addr, instr[1], 13, 20);\n\t}\n      break;\n      case 0B00011111:  // F21-F28 Function Control\n\tif ((locoInfoChanged = updateFunc(addr, instr[1], 21)) == true) {\n\t  DCCEXParser::funcmap(addr, instr[1], 21, 28);\n\t}  // updateFunc handles only the 4 low bits as that is the most common case\n\tif ((locoInfoChanged = updateFunc(addr, instr[1]>>4, 25)) == true) {\n\t  DCCEXParser::funcmap(addr, instr[1], 21, 28);\n\t}\n\tbreak;\n\t/* do that later\n      case 0B00011000:  // F29-F36 Function Control\n\tbreak;\n      case 0B00011001:  // F37-F44 Function Control\n\tbreak;\n      case 0B00011010:  // F45-F52 Function Control\n\tbreak;\n      case 0B00011011:  // F53-F60 Function Control\n\tbreak;\n      case 0B00011100:  // F61-F68 Function Control\n\tbreak;\n\t*/\n      }\n      break;\n    case 0xE0: // 111x-xxxx Config vars\n      break;\n    }\n    return locoInfoChanged;\n  }\n  if (decoderType == DECODER_ACCESSORY) {\n      if (instr[0] & 0B10000000) {  // Basic Accessory\n\taddr = (((~instr[0]) & 0B01110000) << 2) + addr;\n\tbyte port = (instr[0] & 0B00000110) >> 1;\n\tbyte activate = (instr[0] & 0B00001000) >> 3;\n\tbyte coil = (instr[0] & 0B00000001);\n\tlocoInfoChanged = true;\n\t//(void)addr; (void)port; (void)coil; (void)activate;\n\t//DIAG(F(\"HL=%d LL=%d C=%d A=%d\"), addr, port, coil, activate);\n\tDCC::setAccessory(addr, port, coil, activate);\n      } else { // Accessory Extended NMRA spec, do we need to decode this?\n\t/*\n\taddr = (addr << 5) +\n\t  ((instr[0] & 0B01110000) >> 2) +\n\t  ((instr[0] & 0B00000110) >> 1);\n\t*/\n      }\n    return locoInfoChanged;\n  }\n  return false;\n}\n\n// returns false only if loco existed but nothing was changed\nbool DCCDecoder::updateLoco(uint16_t loco, byte speedCode) {\n  if (loco==0) return true; // its a broadcast\n  \n  // determine speed reg for this loco\n  auto slot=LocoSlot::getSlot(loco, true);\n  if (speedCode == slot->getSnifferSpeedCode()) {\n    return false; // nothing changed\n  }\n\n  slot->setSnifferSpeedCode(speedCode);\n  return true; // loco speed changed\n  \n}\n\nbool DCCDecoder::updateFunc(uint16_t loco, byte func, int shift) {\n  unsigned long previous;\n  unsigned long newfunc;\n  auto slot= LocoSlot::getSlot(loco, true);\n  previous=slot->getSnifferFunctions();\n  newfunc=previous;\n  if(shift == 1) { // special case for light\n    newfunc &= ~1UL;\n    newfunc |= ((func & 0B10000) >> 4);\n  }\n  newfunc &= ~(0B1111UL << shift);\n  newfunc |=  ((func & 0B1111) << shift);\n\n  if (newfunc == previous) return false; // nothing changed\n  slot->setSnifferFunctions(newfunc);\n  return true;\n}\n\n#endif // ARDUINO_ARCH_ESP32\n"
  },
  {
    "path": "DCCDecoder.h",
    "content": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ARDUINO_ARCH_ESP32\n#include <Arduino.h>\n#include \"DCCPacket.h\"\n\nclass DCCDecoder {\npublic:\n  static bool parse(DCCPacket &p);\n  static inline void onoff(bool on) {active = on;};\nprivate:\n  static bool active;\n  static bool updateLoco(uint16_t loco, byte speedCode);\n  static bool updateFunc(uint16_t loco, byte func, int shift);\n};\n#endif // ARDUINO_ARCH_ESP32\n"
  },
  {
    "path": "DCCEX.h",
    "content": "/*\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// This include is intended to visually simplify the .ino for the end users.\n// If there were any #ifdefs required they are much better handled in here.  \n\n#ifndef DCCEX_h\n#define DCCEX_h\n\n#include \"defines.h\"\n#include \"DCC.h\"\n#include \"DIAG.h\"\n#include \"DCCEXParser.h\"\n#include \"SerialManager.h\"\n#include \"version.h\"\n#ifndef ARDUINO_ARCH_ESP32\n#include \"WifiInterface.h\"\n#else\n#include \"WifiESP32.h\"\n#endif\n#if ETHERNET_ON == true\n#include \"EthernetInterface.h\"\n#endif\n#include \"Display_Implementation.h\"\n#include \"LCN.h\"\n#include \"IODevice.h\"\n#include \"Turnouts.h\"\n#include \"Sensors.h\"\n#include \"Outputs.h\"\n#include \"CommandDistributor.h\"\n#include \"TrackManager.h\"\n#include \"DCCTimer.h\"    \n#include \"KeywordHasher.h\"\n#include \"EXRAIL.h\"\n    \n#endif\n"
  },
  {
    "path": "DCCEXParser.cpp",
    "content": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021-2025 Herb Morton\n *  © 2020-2025 Harald Barth\n *  © 2020-2021 M Steve Todd\n *  © 2020-2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  © 2022 Colin Murdoch\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\nList of single character OPCODEs in use for reference.\n\nWhen determining a new OPCODE for a new feature, refer to this list as the source of truth.\n\nOnce a new OPCODE is decided upon, update this list.\n\n  Character, Usage\n  /, |EX-R| interactive commands\n  -, Remove from reminder table\n  =, |TM| configuration\n  !, Emergency stop\n  @, LCD messages to/from JMRI\n  #, Request number of supported cabs/locos; heartbeat\n  +, WiFi AT commands\n  ?, Reserved for future use\n  ^, Consist commands\n  0, Track power off\n  1, Track power on\n  a, DCC accessory control\n  A, DCC extended accessory control\n  b, Write CV bit on main\n  B, Write CV bit\n  c, Request current command\n  C, configure the CS\n  d,\n  D, Diagnostic commands\n  e, Erase EEPROM\n  E, Store configuration in EEPROM\n  f, Loco decoder function control (deprecated)\n  F, Loco decoder function control\n  g,\n  G,\n  h,\n  H, Turnout state broadcast\n  i, Server details string\n  I, Turntable object command, control, and broadcast\n  j, Throttle responses\n  J, Throttle queries\n  k, Block exit  (Railcom)\n  K, Block enter (Railcom)\n  l, Loco speedbyte/function map broadcast\n  L, Reserved for LCC interface (implemented in EXRAIL)\n  m, message to throttles (broadcast output) \n  m, set momentum  \n  M, Write DCC packet\n  n, Reserved for SensorCam\n  N, Reserved for Sensorcam \n  o, Neopixel driver (see also IO_NeoPixel.h)\n  O, Output broadcast\n  p, Broadcast power state\n  P, Write DCC packet\n  q, Sensor deactivated\n  Q, Sensor activated\n  r, Broadcast address read on programming track\n  R, Read CVs\n  s, Display status\n  S, Sensor configuration\n  t, Cab/loco update command\n  T, Turnout configuration/control\n  u, Reserved for user commands\n  U, Reserved for user commands\n  v,\n  V, Verify CVs\n  w, Write CV on main\n  W, Write CV\n  x,\n  X, Invalid command response\n  y, Output Sound\n  Y, Output broadcast\n  z, Direct output\n  Z, Output configuration/control\n*/\n\n#include \"StringFormatter.h\"\n#include \"DCCEXParser.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"Turnouts.h\"\n#include \"Outputs.h\"\n#include \"Sensors.h\"\n#include \"GITHUB_SHA.h\"\n#include \"version.h\"\n#include \"defines.h\"\n#include \"CommandDistributor.h\"\n#include \"EEStore.h\"\n#include \"DIAG.h\"\n#include \"TrackManager.h\"\n#include \"DCCTimer.h\"\n#include \"EXRAIL2.h\"\n#include \"Turntables.h\"\n#include \"version.h\"\n#include \"KeywordHasher.h\"\n#include \"CamParser.h\"\n#include \"Stash.h\"\n#include \"DCCConsist.h\"\n#ifdef ARDUINO_ARCH_ESP32\n#include \"WifiESP32.h\"\n#include \"DCCDecoder.h\"\n#endif\n\n// This macro can't be created easily as a portable function because the\n// flashlist requires a far pointer for high flash access. \n#define SENDFLASHLIST(stream,flashList)                 \\\n    for (int16_t i=0;;i+=sizeof(flashList[0])) {                            \\\n        int16_t value=GETHIGHFLASHW(flashList,i);       \\\n        if (value==INT16_MAX) break;                            \\\n        StringFormatter::send(stream,F(\" %d\"),value);\t\\\n    }                                   \n\nint16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];\nbool DCCEXParser::stashBusy;\nPrint *DCCEXParser::stashStream = NULL;\nRingStream *DCCEXParser::stashRingStream = NULL;\nbyte DCCEXParser::stashTarget=0;\n\n// This is a JMRI command parser.\n// It doesnt know how the string got here, nor how it gets back.\n// It knows nothing about hardware or tracks... it just parses strings and\n// calls the corresponding DCC api.\n// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.\n\n\nint16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd, bool usehex)\n{\n    byte state = 1;\n    byte parameterCount = 0;\n    int16_t runningValue = 0;\n    byte *remainingCmd = cmd + 1; // skips the opcode\n    bool signNegative = false;\n\n    // clear all parameters in case not enough found\n    for (int16_t i = 0; i < MAX_COMMAND_PARAMS; i++)\n        result[i] = 0;\n\n    while (parameterCount < MAX_COMMAND_PARAMS)\n    {\n        byte hot = *remainingCmd;\n        switch (state)\n        {\n\n        case 1: // skipping spaces before a param\n            if (hot == ' ')\n                break;\n            if (hot == '\\0')\n\t      return -1;\n\t    if (hot == '>') {\n\t      *remainingCmd = '\\0';  // terminate the cmd string with 0 instead of '>'\n\t      return parameterCount;\n\t    }\n            state = 2;\n            continue;\n\n        case 2: // checking sign or quoted string\n#ifdef HAS_ENOUGH_MEMORY\n\t    if (hot == '\"') {\n\t      // this inserts an extra parameter 0x7777 in front\n\t      // of each string parameter as a marker that can\n\t      // be checked that a string parameter follows\n\t      // This clashes of course with the real value\n\t      // 0x7777 which we hope is used seldom\n\t      result[parameterCount] = (int16_t)0x7777;\n\t      parameterCount++;\n\t      result[parameterCount] = (int16_t)(remainingCmd - cmd + 1);\n\t      parameterCount++;\n\t      state = 4;\n\t      break;\n\t    }\n#endif\n            signNegative = false;\n            runningValue = 0;\n            state = 3;\n            if (hot != '-')\n                continue;\n            signNegative = true;\n            break;\n        case 3: // building a parameter\n            if (hot >= '0' && hot <= '9')\n            {\n                runningValue = (usehex?16:10) * runningValue + (hot - '0');\n                break;\n            }\n            if (hot >= 'a' && hot <= 'z') hot=hot-'a'+'A'; // uppercase a..z\n            if (usehex && hot>='A' && hot<='F') {\n                // treat A..F as hex not keyword\n                runningValue = 16 * runningValue + (hot - 'A' + 10);\n                break;\n            }\n            if (hot=='_' || (hot >= 'A' && hot <= 'Z'))\n            {\n                // Since JMRI got modified to send keywords in some rare cases, we need this\n                // Super Kluge to turn keywords into a hash value that can be recognised later\n                runningValue = ((runningValue << 5) + runningValue) ^ hot;\n                break;\n            }\n            result[parameterCount] = runningValue * (signNegative ? -1 : 1);\n            parameterCount++;\n            state = 1;\n            continue;\n#ifdef HAS_ENOUGH_MEMORY\n\tcase 4: // skipover text\n\t  if (hot == '\\0')        // We did run to end of buffer without finding the \"\n\t    return -1;\n\t  if (hot == '\"') {\n\t    *remainingCmd = '\\0'; // overwrite \" in command buffer with the end-of-string\n\t    state = 1;\n\t  }\n\t  break;\n#endif\n        }\n        remainingCmd++;\n    }\n    return parameterCount;\n}\n\nextern __attribute__((weak))  void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);\nFILTER_CALLBACK DCCEXParser::filterCallback = myFilter;\nFILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;\nFILTER_CALLBACK DCCEXParser::filterCamParserCallback = 0;\nAT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;\n\n// deprecated\nvoid DCCEXParser::setFilter(FILTER_CALLBACK filter)\n{\n    filterCallback = filter;\n}\nvoid DCCEXParser::setRMFTFilter(FILTER_CALLBACK filter)\n{\n    filterRMFTCallback = filter;\n}\nvoid DCCEXParser::setCamParserFilter(FILTER_CALLBACK filter)\n{\n    filterCamParserCallback = filter;\n}\nvoid DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)\n{\n    atCommandCallback = callback;\n}\n\n// Parse an F() string \nvoid DCCEXParser::parse(const FSH * cmd) {\n      DIAG(F(\"SETUP(\\\"%S\\\")\"),cmd);\n      int size=STRLEN_P((char *)cmd)+1; \n      char buffer[size];\n      STRCPY_P(buffer,(char *)cmd);\n      parse(&USB_SERIAL,(byte *)buffer,NULL);\n}\n\n// See documentation on DCC class for info on this section\n\nvoid DCCEXParser::parse(Print *stream,  byte *com,  RingStream *ringStream) {\n  // This function can get stings of the form \"<C OMM AND>\" or \"C OMM AND>\"\n  // found is true first after the leading \"<\" has been passed which results\n  // in parseOne() getting c=\"C OMM AND>\"\n  byte *cForLater = NULL;\n  bool found = (com[0] != '<');\n  for (byte *c=com; c[0] != '\\0'; c++) {\n    if (found) {\n      cForLater = c;\n      found=false;\n    }\n    if (c[0] == '<') {\n      if (cForLater) parseOne(stream, cForLater, ringStream);\n      found = true;\n    }\n  }\n  if (cForLater) parseOne(stream, cForLater, ringStream);\n}\n\nvoid DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)\n{\n#ifdef DISABLE_PROG\n    (void)ringStream;\n#endif\n#ifndef DISABLE_EEPROM\n    (void)EEPROM; // tell compiler not to warn this is unused\n#endif\n    byte params = 0;\n    if (Diag::CMD)\n        DIAG(F(\"PARSING:%s\"), com);\n    int16_t p[MAX_COMMAND_PARAMS];\n    while (com[0] == '<' || com[0] == ' ')\n        com++; // strip off any number of < or spaces\n    byte opcode = com[0];\n    int16_t splitnum = splitValues(p, com, opcode=='M' || opcode=='P');\n    if (splitnum < 0 || splitnum >= MAX_COMMAND_PARAMS) // if arguments are broken, leave but via printing <X>\n      goto out;\n    // Because of check above we are now inside byte size\n    params = splitnum;\n\n    if (filterCallback)\n        filterCallback(stream, opcode, params, p);\n    if (filterRMFTCallback && opcode!='\\0')\n        filterRMFTCallback(stream, opcode, params, p);\n    if (filterCamParserCallback && opcode!='\\0')\n        filterCamParserCallback(stream, opcode, params, p);\n\n    // Functions return from this switch if complete, break from switch implies error <X> to send\n    switch (opcode)\n    {\n    case '\\0':\n        return; // filterCallback asked us to ignore\n    case 't':   // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>\n    {\n        int16_t cab;\n        int16_t tspeed;\n        int16_t direction;\n\n        if (params==1) {  // <t cab>  display state\n          if (p[0]<0 || p[0]>10239) break; // beyond DCC range\n          // send current state of this cab\n\t      auto slot=LocoSlot::getSlot(p[0],false);\n\t      if (slot)\n\t        CommandDistributor::broadcastLoco(slot);\n\t      else // send dummy state speed 0 fwd no functions.\n            StringFormatter::send(stream,F(\"<l %d -1 128 0>\\n\"),p[0]);\n\t      return;\n        }\n\n        if (params == 4)\n        { // <t REGISTER CAB SPEED DIRECTION>\n\t    // ignore register p[0]\n            cab = p[1];\n            tspeed = p[2];\n            direction = p[3];\n        }\n        else if (params == 3)\n        { // <t CAB SPEED DIRECTION>\n            cab = p[0];\n            tspeed = p[1];\n            direction = p[2];\n        }\n        else\n            break;\n\n        // Convert DCC-EX protocol speed steps where\n        // -1=emergency stop, 0-126 as speeds\n        // to DCC 0=stop, 1= emergency stop, 2-127 speeds\n        if (tspeed > 126 || tspeed < -1)\n            break; // invalid JMRI speed code\n        if (tspeed < 0)\n            tspeed = 1; // emergency stop DCC speed\n        else if (tspeed > 0)\n            tspeed++; // map 1-126 -> 2-127\n        if (cab == 0 && tspeed > 1)\n            break; // ignore broadcasts of speed>1\n\n        if (direction < 0 || direction > 1)\n            break; // invalid direction code\n\tif (cab > 10239 || cab < 0)\n\t    break; // beyond DCC range\n\n        if (DCC::setThrottle(cab, tspeed, direction)) {\n\t  if (params == 4) // send obsolete format T response\n            StringFormatter::send(stream, F(\"<T %d %d %d>\\n\"), p[0], p[2], p[3]);\n\t  // speed change will be broadcast anyway in new <l > format\n\t  return;\n\t} else {\n\t  break; // setThrottle() failed means slot table was full.\n\t}\n    }\n    case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>\n        if (parsef(stream, params, p))\n            return;\n        break;\n\n    case 'a': // ACCESSORY <a ADDRESS SUBADDRESS ACTIVATE [ONOFF]> or <a LINEARADDRESS ACTIVATE>\n        { \n          int address;\n          byte subaddress;\n          byte activep;\n          byte onoff;\n          if (params==2) { // <a LINEARADDRESS ACTIVATE>\n              address=(p[0] - 1) / 4 + 1;\n              subaddress=(p[0] - 1)  % 4;\n              activep=1;\n              onoff=2; // send both\n          }\n          else if (params==3) { // <a ADDRESS SUBADDRESS ACTIVATE>\n              address=p[0];\n              subaddress=p[1];\n              activep=2;\n              onoff=2; // send both\n          }\n          else if (params==4) { // <a ADDRESS SUBADDRESS ACTIVATE ONOFF>\n              address=p[0];\n              subaddress=p[1];\n              activep=2;\n\t      if ((p[3] < 0) || (p[3] > 1))        // invalid onoff    0|1\n\t\tbreak;\n              onoff=p[3];\n          }\n          else break; // invalid no of parameters\n          \n          if (\n\t      ((address & 0x01FF) != address)      // invalid address (limit 9 bits)\n           || ((subaddress & 0x03) != subaddress)  // invalid subaddress (limit 2 bits)\n           || (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1\n\t      ) break;\n          // Honour the configuration option (config.h) which allows the <a> command to be reversed\n\t  // Because of earlier confusion we need to do the same thing under both defines\n#if defined(DCC_ACCESSORY_COMMAND_REVERSE)\n          DCC::setAccessory(address, subaddress,p[activep]==0,onoff);\n#else\n          DCC::setAccessory(address, subaddress,p[activep]==1,onoff);\n#endif\n        }\n        return;\n    \n    case 'A': // EXTENDED ACCESSORY <A address value> \n        // Note: if this happens to match a defined EXRAIL \n        // DCCX_SIGNAL, then EXRAIL will have intercepted\n        // this command alrerady.   \n        if (params==2 && DCC::setExtendedAccessory(p[0],p[1])) return;\n        break;\n     \n    case 'T': // TURNOUT  <T ...>\n        if (parseT(stream, params, p))\n            return;\n        break;\n\n#ifndef IO_NO_HAL\n    case 'o':  // Neopixel pin manipulation\n        if (p[0]==0) break;\n        {  \n          VPIN vpin=p[0]>0 ? p[0]:-p[0];\n          bool setON=p[0]>0;\n          if (params==1) {  // <o [-]vpin> \n            IODevice::write(vpin,setON);\n            return;\n          }\n          if (params==2) {  // <o [-]vpin count> \n            IODevice::writeRange(vpin,setON,p[1]);\n            return;\n          }\n          if (params==4 || params==5) { // <z [-]vpin r g b [count]>\n             auto count=p[4]?p[4]:1;  \n             if (p[1]<0 || p[1]>0xFF) break;  \n            if (p[2]<0 || p[2]>0xFF) break;  \n            if (p[3]<0 || p[3]>0xFF) break;  \n            // strange parameter mangling... see IO_NeoPixel.h NeoPixel::_writeAnalogue\n            int colour_RG=(p[1]<<8)  | p[2];\n            uint16_t colour_B=p[3];\n            IODevice::writeAnalogueRange(vpin,colour_RG,setON,colour_B,count);\n            return;\n            }\n        }\n        break;\n#endif        \n\n  case 'z':  // direct pin manipulation\n        if (p[0]==0) break; \n        if (params==1) {  // <z vpin | -vpin> \n            if (p[0]>0) IODevice::write(p[0],HIGH);\n            else IODevice::write(-p[0],LOW);\n            return;\n        }\n        if (params>=2 && params<=4) { // <z vpin analog profile duration> \n            // unused params default to 0           \n            IODevice::writeAnalogue(p[0],p[1],p[2],p[3]);\n            return;\n        }\n        break; \n\n    case 'y': // OUTPUT SOUND <y ...>\n        if (parsey(stream, params, p))\n            return;\n        break;\n    case 'Z': // OUTPUT <Z ...>\n        if (parseZ(stream, params, p))\n            return;\n        break;\n\n    case 'S': // SENSOR <S ...>\n        if (parseS(stream, params, p))\n            return;\n        break;\n\n#ifndef DISABLE_PROG\n    case 'w': // WRITE CV on MAIN <w CAB CV VALUE>\n      if (params != 3)\n\tbreak;\n      DCC::writeCVByteMain(p[0], p[1], p[2]);\n      return;\n\n#ifdef HAS_ENOUGH_MEMORY    \n    case 'r': // READ CV on MAIN <r CAB CV>  Requires Railcom\n      if (params != 2)\n\tbreak;\n      if (!DCCWaveform::isRailcom()) break;\n      if (!stashCallback(stream, p, ringStream)) break;\n      DCC::readCVByteMain(p[0], p[1],callback_r);\n      return;\n#endif      \n\n    case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>\n      if (params != 4)\n\tbreak;\n      DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);\n      return;\n#endif\n    \n    case 'm': // <m cabid momentum [braking]>\n              // <m LINEAR|POWER>\n      if (params==1) {\n        if (p[0]==\"LINEAR\"_hk) DCC::linearAcceleration=true;\n        else if (p[0]==\"POWER\"_hk) DCC::linearAcceleration=false;\n        else break;\n        return; \n      }        \n      if (params<2 || params>3) break;\n      if (params==2) p[2]=p[1];\n      if (DCC::setMomentum(p[0],p[1],p[2])) return; \n      break; \n\n    case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>\n#ifndef DISABLE_PROG\n    case 'P': // WRITE TRANSPARENT DCC PACKET PROG <P REG X1 ... X9>\n#endif\n        // NOTE: this command was parsed in HEX instead of decimal\n        params--; // drop REG\n        if (params<1) break;\n\tif (params > MAX_PACKET_SIZE) break;\n        {\n          byte packet[params];\n          for (int i=0;i<params;i++) {\n            packet[i]=(byte)p[i+1];\n            if (Diag::CMD) DIAG(F(\"packet[%d]=%d (0x%x)\"), i, packet[i], packet[i]);\n          }\n          (opcode=='M'?DCCWaveform::mainTrack:DCCWaveform::progTrack).schedulePacket(packet,params,3);  \n        }\n        return;\n        \n#ifndef DISABLE_PROG\n    case 'W': // WRITE CV ON PROG <W CV VALUE CALLBACKNUM CALLBACKSUB>\n        if (!stashCallback(stream, p, ringStream))\n\t    break;\n        if (params == 1) // <W id> Write new loco id (clearing consist and managing short/long)\n            DCC::setLocoId(p[0],callback_Wloco);\n        else if (params == 4)  // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>\n            DCC::writeCVByte(p[0], p[1], callback_W4);\n        else if ((params==2 || params==3 ) && p[0]==\"CONSIST\"_hk ) {\n            DCC::setConsistId(p[1],p[2]==\"REVERSE\"_hk,callback_Wconsist);\n        }    \n        else if (params == 2)  // WRITE CV ON PROG <W CV VALUE>\n            DCC::writeCVByte(p[0], p[1], callback_W);\n\telse\n            break;\n        return;\n\n    case 'V': // VERIFY CV ON PROG <V CV VALUE> <V CV BIT 0|1>\n        if (params == 2)\n        { // <V CV VALUE>\n            if (!stashCallback(stream, p, ringStream))\n                break;\n            DCC::verifyCVByte(p[0], p[1], callback_Vbyte);\n            return;\n        }\n        if (params == 3)\n        {\n            if (!stashCallback(stream, p, ringStream))\n                break;\n            DCC::verifyCVBit(p[0], p[1], p[2], callback_Vbit);\n            return;\n        }\n        break;\n\n    case 'B': // WRITE CV BIT ON PROG  <B CV BIT VALUE CALLBACKNUM CALLBACKSUB> or <B CV BIT VALUE>\n        if (params != 3 && params != 5)\n\t  break;\n        if (!stashCallback(stream, p, ringStream))\n\t  break;\n        DCC::writeCVBit(p[0], p[1], p[2], callback_B);\n        return;\n\n    case 'R': // READ CV ON PROG\n        if (params == 1) {\n            if (!stashCallback(stream, p, ringStream)) break;\n            if (p[0]==\"LOCOID\"_hk) { // <R LOCOID> read consist id\n                DCC::getLocoId(callback_Rloco);\n                return;\n            }\n            if (p[0]==\"CONSIST\"_hk) { // <R CONSIST> read consist id\n                DCC::getConsistId(callback_Rloco);\n                return;\n            }\n            \n            // <R CV> -- uses verify callback\n            DCC::verifyCVByte(p[0], 0, callback_Vbyte);\n            return;\n        }\n        if (params == 3)\n        { // <R CV CALLBACKNUM CALLBACKSUB>\n            if (!stashCallback(stream, p, ringStream))\n                break;\n            DCC::readCV(p[0], callback_R);\n            return;\n        }\n        if (params == 0)\n        { // <R> New read driveaway loco id\n            if (!stashCallback(stream, p, ringStream))\n                break;\n            DCC::getDriveawayLocoId(callback_Rloco);\n            return;\n        }\n        break;\n#endif\n\n    case '1': // POWERON <1   [MAIN|PROG|JOIN]>\n        {\n\t  if (params > 1) break;\n\t  if (params==0) { // All\n\t    TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);\n\t  }\n\t  if (params==1) {\n\t    if (p[0]==\"MAIN\"_hk) { // <1 MAIN>\n\t      TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);\n            }\n#ifndef DISABLE_PROG\n            else if (p[0] == \"JOIN\"_hk) {  // <1 JOIN>\n\t      TrackManager::setJoin(true);\n\t      TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);\n            }\n            else if (p[0]==\"PROG\"_hk) { // <1 PROG>\n\t      TrackManager::setJoin(false);\n\t      TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);\n            }\n#endif\n            else if (p[0] >= \"A\"_hk && p[0] <= \"H\"_hk) { // <1 A-H>\n\t      byte t = (p[0] - 'A');\n\t      TrackManager::setTrackPower(POWERMODE::ON, t);\n\t      //StringFormatter::send(stream, F(\"<p1 %c>\\n\"), t+'A');\n            }\n\t    else break; // will reply <X>\n\t  }\n\t  //TrackManager::streamTrackState(NULL,t);\n          \n\t  return;\n\t}\n            \n    case '0': // POWEROFF <0 [MAIN | PROG] >\n        {\n\t  if (params > 1) break;\n\t  if (params==0) { // All\n\t    TrackManager::setJoin(false);\n\t    TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);\n\t  }\n\t  if (params==1) {\n\t    if (p[0]==\"MAIN\"_hk) { // <0 MAIN>\n\t      TrackManager::setJoin(false);\n\t      TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);\n\t    }\n#ifndef DISABLE_PROG\n            else if (p[0]==\"PROG\"_hk) { // <0 PROG>\n          TrackManager::setJoin(false);\n\t      TrackManager::progTrackBoosted=false;  // Prog track boost mode will not outlive prog track off\n\t      TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);\n            }\n#endif\n\t    else if (p[0] >= \"A\"_hk && p[0] <= \"H\"_hk) { // <1 A-H>\n\t      byte t = (p[0] - 'A');\n\t      TrackManager::setJoin(false);\n\t      TrackManager::setTrackPower(POWERMODE::OFF, t);\n\t      //StringFormatter::send(stream, F(\"<p0 %c>\\n\"), t+'A');\n\t    }\n\t    else break; // will reply <X>\n\t  }\n\t  return;\n\t}\n\n    case '!': // ESTOPALL  <!>\n        if (p[0]==\"P\"_hk) DCC::estopLock(true); // <!P>\n        else if (p[0]==\"R\"_hk) DCC::estopLock(false); // <!R\n        else if (p[0]==\"Q\"_hk) StringFormatter::send(stream, \n                  DCC::isEstopLocked() ? F(\"<!PAUSED>\\n\"): F(\"<!RESUMED>\\n\")); // <!Q>\n        \n        else DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.\n        return;\n\n#ifdef HAS_ENOUGH_MEMORY\n    case 'c': // SEND METER RESPONSES <c>\n        // No longer useful because of multiple tracks See <JG> and <JI>\n        if (params>0) break;\n        TrackManager::reportObsoleteCurrent(stream);\n        return;\n#endif\n    case 'Q': // SENSORS <Q>\n        Sensor::printAll(stream);\n        return;\n\n    case 's': // STATUS <s>\n        StringFormatter::send(stream, F(\"<iDCC-EX V-%S / %S / %S G-%S>\\n\"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));\n        CommandDistributor::broadcastPower(); // <s> is the only \"get power status\" command we have\n        Turnout::printAll(stream); //send all Turnout states\n        Sensor::printAll(stream);  //send all Sensor  states\n        return;       \n\n#ifndef DISABLE_EEPROM\n    case 'E': // STORE EPROM <E>\n        EEStore::store();\n        StringFormatter::send(stream, F(\"<e %d %d %d>\\n\"), EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs);\n        return;\n\n    case 'e': // CLEAR EPROM <e>\n        EEStore::clear();\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return;\n#endif\n    case ' ': // < >\n        StringFormatter::send(stream, F(\"\\n\"));\n        return;\n    case 'C': // CONFIG <C [params]>\n#if defined(ARDUINO_ARCH_ESP32)\n// currently this only works on ESP32\n      if (p[0] == \"SNIFFER\"_hk) { // <C SNIFFER ON|OFF>\n\tbool on = false;\n\tif (params>1 && p[1] == \"ON\"_hk) {\n\t  on = true;\n\t}\n\tDCCDecoder::onoff(on);\n\treturn;\n      }\n#if WIFI_ON\n      if (p[0] == \"WIFI\"_hk) { \t// <C WIFI SSID PASSWORD>\n\tif (params != 5)        // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD\n\t  break;\n\tif (p[1] == 0x7777 && p[3] == 0x7777) {\n\t  WifiESP::setup((const char*)(com + p[2]), (const char*)(com + p[4]), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);\n\t}\n\treturn;\n      }\n#endif\n#endif //ESP32\n      if (parseC(stream, params, p))\n\treturn;\n      break;\n#ifndef DISABLE_DIAG\n    case 'D': // DIAG <D [params]>\n        if (parseD(stream, params, p))\n            return;\n        break;\n#endif\n    case '=': // TRACK MANAGER CONTROL <= [params]>\n        if (TrackManager::parseEqualSign(stream, params, p))\n            return;\n        break;\n\n    case '#': // NUMBER OF LOCOSLOTS <#>\n        {\n\t  int free = DCCTimer::getMinimumFreeMemory();\n\t  int freeSlotGuess = free/sizeof(LocoSlot);\n\t  freeSlotGuess = freeSlotGuess - 2; // be conservative\n\t  if (freeSlotGuess > MAX_LOCOS)\n\t    freeSlotGuess = MAX_LOCOS;\n\t  if (freeSlotGuess < 0)\n\t    freeSlotGuess = 0;\n\t  StringFormatter::send(stream, F(\"<# %d>\\n\"), freeSlotGuess);\n\t}\n        return;\n\n    case '-': // Forget Loco <- [cab]>\n        if (params > 1 || p[0]<0) break;\n        if (p[0]==0) DCC::forgetAllLocos();\n        else  DCC::forgetLoco(p[0]);\n        return;\n\n    case 'F': // New command to call the new Loco Function API <F cab func 1|0>\n        if(params!=3) break; \n        \n        if (p[1]==\"DCFREQ\"_hk) { // <F cab DCFREQ 0..3>\n          if (p[2]<0 || p[2]>3) break;\n          DCC::setDCFreq(p[0],p[2]);\n          return;    \n        }\n\n        if (Diag::CMD)\n            DIAG(F(\"Setting loco %d F%d %S\"), p[0], p[1], p[2] ? F(\"ON\") : F(\"OFF\"));\n        if (DCC::setFn(p[0], p[1], p[2] == 1)) return;\n\tbreak;\n\n    case '^': // Consist  <^ [cab0..7]>\n        if (DCCConsist::parse(stream,params,p)) return;\n        break;\n\n#if WIFI_ON\n    case '+': // Complex Wifi interface command (not usual parse)\n        if (atCommandCallback && !ringStream) {\n          TrackManager::setPower(POWERMODE::OFF);\n          atCommandCallback((HardwareSerial *)stream,com);\n          return;\n        }\n        break;\n#endif \n\n    case 'J' : // throttle info access\n        {\n            if (params<1) break; // <J>\n            //if ((params<1) | (params>2)) break; // <J>\n            int16_t id=(params==2)?p[1]:0;\n            switch(p[0]) {\n                case \"C\"_hk: // <JC mmmm nn> sets time and speed\n                    if (params==1) { // <JC> returns latest time\n                        int16_t x = CommandDistributor::retClockTime();\n                        StringFormatter::send(stream, F(\"<jC %d>\\n\"), x);\n                        return;\n                    }\n                    CommandDistributor::setClockTime(p[1], p[2]);\n                    return;\n                \n                case \"G\"_hk: // <JG> current gauge limits\n                    if (params>1) break;\n                    TrackManager::reportGauges(stream);   // <g limit...limit>     \n                    return;\n                \n                case \"I\"_hk: // <JI> current values\n                    if (params>1) break;\n                    TrackManager::reportCurrent(stream);   // <g limit...limit>     \n                    return;\n\n                case \"L\"_hk: // <JL display row> track state and mA value on display\n                    if (params<3) break;\n                    TrackManager::reportCurrentLCD(p[1], p[2]);   // Track power status     \n                    return;                    \n\n                case \"A\"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes\n                    if (params!=1) break; // <JA>\n                    StringFormatter::send(stream, F(\"<jA>\\n\"));\n                    return;\n \n                case \"M\"_hk: // <JM> Stash management\n                    if (parseJM(stream, params, p))\n                        return;\n                    break;\n \n            case \"R\"_hk: // <JR> returns rosters \n                StringFormatter::send(stream, F(\"<jR\"));\n#ifdef EXRAIL_ACTIVE\n                if (params==1) {\n                    SENDFLASHLIST(stream,RMFT2::rosterIdList)\n                }\n                else {\n                    auto rosterName= RMFT2::getRosterName(id);\n                    if (!rosterName) rosterName=F(\"\");\n\n                    auto functionNames= RMFT2::getRosterFunctions(id);\n                    if (!functionNames) functionNames=RMFT2::getRosterFunctions(0);\n                    if (!functionNames) functionNames=F(\"\");\n                    StringFormatter::send(stream,F(\" %d \\\"%S\\\" \\\"%S\\\"\"), \n\t\t\t\t\t                            id, rosterName, functionNames);\n                }\n#endif          \n                StringFormatter::send(stream, F(\">\\n\"));      \n                return; \n            case \"T\"_hk: // <JT> returns turnout list \n                StringFormatter::send(stream, F(\"<jT\"));\n                if (params==1) { // <JT>\n                    for ( Turnout * t=Turnout::first(); t; t=t->next()) { \n                        if (t->isHidden()) continue;          \n                        StringFormatter::send(stream, F(\" %d\"),t->getId());\n                    }\n                }\n                else { // <JT id>\n                    Turnout * t=Turnout::get(id);\n                    if (!t || t->isHidden()) StringFormatter::send(stream, F(\" %d X\"),id);\n                    else {\n\t\t      const FSH *tdesc = NULL;\n#ifdef EXRAIL_ACTIVE\n\t\t      tdesc = RMFT2::getTurnoutDescription(id);\n#endif\n\t\t      if (tdesc == NULL)\n\t\t\ttdesc = F(\"\");\n\t\t      StringFormatter::send(stream, F(\" %d %c \\\"%S\\\"\"),\n\t\t\t\t\t    id,t->isThrown()?'T':'C',\n\t\t\t\t\t    tdesc);\n\t\t    }\n                }\n                StringFormatter::send(stream, F(\">\\n\"));\n                return;\n// No turntables without HAL support\n#ifndef IO_NO_HAL\n            case \"O\"_hk: // <JO returns turntable list\n                StringFormatter::send(stream, F(\"<jO\"));\n                if (params==1) { // <JO>\n                    for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) { \n                        if (tto->isHidden()) continue;          \n                        StringFormatter::send(stream, F(\" %d\"),tto->getId());\n                    }\n                    StringFormatter::send(stream, F(\">\\n\"));\n                } else {    // <JO id>\n                    Turntable *tto=Turntable::get(id);\n                    if (!tto || tto->isHidden()) {\n                        StringFormatter::send(stream, F(\" %d X>\\n\"), id);\n                    } else {\n                        uint8_t pos = tto->getPosition();\n                        uint8_t type = tto->isEXTT();\n                        uint8_t posCount = tto->getPositionCount();\n                        const FSH *todesc = NULL;\n#ifdef EXRAIL_ACTIVE\n                        todesc = RMFT2::getTurntableDescription(id);\n#endif\n                        if (todesc == NULL) todesc = F(\"\");\n                        StringFormatter::send(stream, F(\" %d %d %d %d \\\"%S\\\">\\n\"), id, type, pos, posCount, todesc);\n                    }\n                }\n                return;\n            case \"P\"_hk: // <JP id> returns turntable position list for the turntable id\n                if (params==2) { // <JP id>\n                    Turntable *tto=Turntable::get(id);\n                    if (!tto || tto->isHidden()) {\n                        StringFormatter::send(stream, F(\" %d X>\\n\"), id);\n                    } else {\n                        uint8_t posCount = tto->getPositionCount();\n                        const FSH *tpdesc = NULL;\n                        for (uint8_t p = 0; p < posCount; p++) {\n                            StringFormatter::send(stream, F(\"<jP\"));\n                            int16_t angle = tto->getPositionAngle(p);\n#ifdef EXRAIL_ACTIVE\n                            tpdesc = RMFT2::getTurntablePositionDescription(id, p);\n#endif\n                            if (tpdesc == NULL) tpdesc = F(\"\");\n                            StringFormatter::send(stream, F(\" %d %d %d \\\"%S\\\"\"), id, p, angle, tpdesc);\n                            StringFormatter::send(stream, F(\">\\n\"));\n                        }\n                    }\n                } else {\n                    StringFormatter::send(stream, F(\"<jP X>\\n\"));\n                }\n                return;\n#endif\n            default: break;    \n            }  // switch(p[1])\n        break; // case J\n        }\n\n// No turntables without HAL support\n#ifndef IO_NO_HAL\n    case 'I': // TURNTABLE  <I ...>\n        if (parseI(stream, params, p))\n            return;\n        break;\n#endif\n    case '/': // implemented in EXRAIL parser\n    case 'L': // LCC interface implemented in EXRAIL parser\n    case 'N': // interface implemented in CamParser\n        break; // Will <X> if not intercepted by filters\n\n    case '@': \n#ifndef DISABLE_VDPY\n      if (params==0) {  // <@> JMRI saying \"give me virtual LCD msgs\"\n        CommandDistributor::setVirtualLCDSerial(stream);\n        StringFormatter::send(stream,\n            F(\"<@ 0 0 \\\"DCC-EX v\" VERSION \"\\\">\\n\"\n               \"<@ 0 1 \\\"Lic GPLv3\\\">\\n\"));\n            return;   \n            }\n#endif            \n        if (params==4 && p[2]==0x7777) { // <@ x  y \"string\">\n            // p[2] will be 0x7777 string marker. \n            StringFormatter::lcd2(p[0],p[1], F(\"%s\"),(const char*)(com + p[3]));\n            return; \n        }\n        break; // will <X>    \n \ndefault: //anything else will diagnose and drop out to <X>\n      if (opcode >= ' ' && opcode <= '~') {\n        DIAG(F(\"Opcode=%c params=%d\"), opcode, params);\n        for (int i = 0; i < params; i++)\n            DIAG(F(\"p[%d]=%d (0x%x)\"), i, p[i], p[i]);\n      } else {\n\tDIAG(F(\"Unprintable %x\"), opcode);\n      }\n      break;\n\n    } // end of opcode switch\n\nout:// Any fallout here sends an <X>\n    StringFormatter::send(stream, F(\"<X>\\n\"));\n}\n\nbool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])\n{\n\n    switch (params)\n    {\n    \n    case 2: // <Z ID ACTIVATE>\n    {\n        Output *o = Output::get(p[0]);\n        if (o == NULL)\n            return false;\n        o->activate(p[1]);\n        StringFormatter::send(stream, F(\"<Y %d %d>\\n\"), p[0], p[1]);\n    }\n        return true;\n\n    case 3: // <Z ID PIN IFLAG>\n        if (p[0] < 0 || p[2] < 0 || p[2] > 7 )\n\t        return false;\n        if (!Output::create(p[0], p[1], p[2], 1))\n          return false;\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return true;\n\n    case 1: // <Z ID>\n        if (!Output::remove(p[0]))\n          return false;\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return true;\n\n    case 0: // <Z> list Output definitions\n    {\n        bool gotone = false;\n        for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)\n        {\n            gotone = true;\n            StringFormatter::send(stream, F(\"<Y %d %d %d %d>\\n\"), tt->data.id, tt->data.pin, tt->data.flags, tt->data.active);\n        }\n        return gotone;\n    }\n    default:\n        return false;\n    }\n}\n\n#include \"IO_DFPlayerBase.h\"\n\nbool DCCEXParser::parsey(Print *stream, int16_t params, int16_t p[])\n{\n    (void)stream; // unused parameter\n     \n    // <y vpin PLAY track [volume]>\n    // <y vpin REPEAT track [volume]>\n    // <y vpin FOLDER folder>\n    // <y vpin STOP>\n    // <y vpin VOL volume>\n    // <y vpin PAUSE>\n    // <y vpin RESUME>\n    // <y vpin EQ eq>\n    // <y vpin RESET>\n    \n    if (params<2) return false;\n\n    int16_t v1=0;\n    uint8_t v2=0;\n    uint16_t cmd=0; \n\n    \n\n    switch (p[1])\n    {\n      case \"PLAY\"_hk:\n        if (params<3) return false;\n        v1=p[2]; // track\n        cmd=DFPlayerBase::DF_PLAY;\n        if (params>=4) v2=p[3]; // volume\n        break;\n    \n      case \"REPEAT\"_hk:\n        if (params<3) return false;\n        v1=p[2]; // track\n        cmd=DFPlayerBase::DF_REPEATPLAY;\n        if (params>=4) v2=p[3]; // volume\n        break;\n      \n      case \"FOLDER\"_hk:\n        if (params!=3) return false;\n        v2=p[2]; // folder\n        cmd=DFPlayerBase::DF_FOLDER;\n        break;\n\n      case \"STOP\"_hk:\n        if (params!=2) return false;\n        cmd=DFPlayerBase::DF_STOPPLAY;\n        break;\n\n      case \"PAUSE\"_hk:\n        if (params!=2) return false;\n        cmd=DFPlayerBase::DF_PAUSE;\n        break;\n\n      case \"RESUME\"_hk:\n        if (params!=2) return false;\n        cmd=DFPlayerBase::DF_RESUME;\n        break;\n\n      case \"RESET\"_hk:\n        if (params!=2) return false;\n        cmd=DFPlayerBase::DF_RESET;\n        break;\n\n      case \"VOL\"_hk:\n        if (params!=3) return false;\n        cmd=DFPlayerBase::DF_VOL;\n        v2=p[2]; // volume\n        break;\n    \n      case \"EQ\"_hk:\n        if (params!=3) return false;\n        cmd=DFPlayerBase::DF_EQ;\n        v2=p[2]; // EQ type\n        break;\n        \n      default:\n        return false;\n    }\n    IODevice::writeAnalogue((VPIN)p[0],v1,v2,cmd);\n    return true;\n}\n\n//===================================\nbool DCCEXParser::parsef(Print *stream, int16_t params, int16_t p[])\n{\n  // JMRI sends this info in DCC message format but it's not exactly\n  // convenient for other processing\n  if (params == 2) {\n    byte instructionField = p[1] & 0xE0;   // 1110 0000\n    if (instructionField == 0x80) {        // 1000 0000 Function group 1\n      // Shuffle bits from order F0 F4 F3 F2 F1 to F4 F3 F2 F1 F0\n      byte normalized = (p[1] << 1 & 0x1e) | (p[1] >> 4 & 0x01);\n      return (funcmap(p[0], normalized, 0, 4));\n    } else if (instructionField == 0xA0) { // 1010 0000 Function group 2\n      if (p[1] & 0x10)                     // 0001 0000 Bit selects F5toF8 / F9toF12\n\treturn (funcmap(p[0], p[1], 5, 8));\n      else\n\treturn (funcmap(p[0], p[1], 9, 12));\n    } \n  }\n  if (params == 3) {\n    if (p[1] == 222) {\n      return (funcmap(p[0], p[2], 13, 20));\n    } else if (p[1] == 223) {\n      return (funcmap(p[0], p[2], 21, 28));\n    } \n  }\n  (void)stream; // NO RESPONSE\n  return false;\n}\n\nbool DCCEXParser::funcmap(int16_t cab, byte value, byte fstart, byte fstop)\n{\n  for (int16_t i = fstart; i <= fstop; i++) {\n    if (! DCC::setFn(cab, i, value & 1)) return false;\n    value >>= 1;\n  }\n  return true;\n}\n\n//===================================\nbool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])\n{\n    switch (params)\n    {\n    case 0: // <T>  list turnout definitions\n        return Turnout::printAll(stream); // will <X> if none found\n\n    case 1: // <T id>  delete turnout\n        if (!Turnout::remove(p[0]))\n            return false;\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return true;\n\n    case 2: // <T id 0|1|T|C> \n        {\n          bool state = false;\n          switch (p[1]) {\n            // Turnout messages use 1=throw, 0=close.\n            case 0:\n            case \"C\"_hk:\n              state = true;\n              break;\n            case 1:\n            case \"T\"_hk:\n              state= false;\n              break;\n            case \"X\"_hk:\n\t    {\n              Turnout *tt = Turnout::get(p[0]);\n              if (tt) {\n                tt->print(stream);\n                return true;\n              }\n              return false;\n\t    }\n            default: // Invalid parameter\n\t      return false;\n          }\n          if (!Turnout::setClosed(p[0], state)) return false;\n          return true;\n        }\n\n    default: // Anything else is some kind of turnout create function.\n      if (params == 6 && p[1] == \"SERVO\"_hk) { // <T id SERVO n n n n>\n        if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))\n          return false;\n      } else \n      if (params == 3 && p[1] == \"VPIN\"_hk) { // <T id VPIN n>\n        if (!VpinTurnout::create(p[0], p[2])) return false;\n      } else \n      if (params >= 3 && p[1] == \"DCC\"_hk) {\n        // <T id DCC addr subadd>   0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>\n        if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>\n          if (!DCCTurnout::create(p[0], p[2], p[3])) return false;\n        } else if (params==3 && p[2]>0 && p[2]<=512*4) { // <T id DCC nn>, 1<=nn<=2048\n          // Linearaddress 1 maps onto decoder address 1/0 (not 0/0!).\n          if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false;\n        } else\n          return false;\n      } else \n      if (params==3) { // legacy <T id addr subadd> for DCC accessory\n        if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) {\n          if (!DCCTurnout::create(p[0], p[1], p[2])) return false;\n        } else\n          return false;\n      } \n      else \n      if (params==4) { // legacy <T id n n n> for Servo\n        if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false;\n      } else\n        return false;\n\n      StringFormatter::send(stream, F(\"<O>\\n\"));\n      return true;\n    }\n}\n\nbool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])\n{\n\n    switch (params)\n    {\n    case 3: // <S id pin pullup>  create sensor. pullUp indicator (0=LOW/1=HIGH)\n        if (!Sensor::create(p[0], p[1], p[2]))\n          return false;\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return true;\n\n    case 1: // S id> remove sensor\n        if (!Sensor::remove(p[0]))\n          return false;\n        StringFormatter::send(stream, F(\"<O>\\n\"));\n        return true;\n\n    case 0: // <S> list sensor definitions\n      Sensor::dumpAll(stream);\n      return true;\n\n    default: // invalid number of arguments\n        break;\n    }\n    return false;\n}\n\nbool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {\n    (void)stream; // arg not used, maybe later?\n    if (params == 0)\n        return false;\n    switch (p[0])\n    {\n#ifndef DISABLE_PROG\n    case \"PROGBOOST\"_hk:\n        TrackManager::progTrackBoosted=true;\n\t    return true;\n#endif\n    case \"RESET\"_hk:\n        DCCTimer::reset();\n        break; // and <X> if we didnt restart\n    case \"SPEED28\"_hk:\n        DCC::setGlobalSpeedsteps(28);\n\tDIAG(F(\"28 Speedsteps\"));\n        return true;\n\n    case \"SPEED128\"_hk:\n        DCC::setGlobalSpeedsteps(128);\n\tDIAG(F(\"128 Speedsteps\"));\n        return true;\n#if defined(HAS_ENOUGH_MEMORY) && !defined(ARDUINO_ARCH_UNO)\n    case \"RAILCOM\"_hk:\n        {   // <C RAILCOM ON|OFF|DEBUG >\n            if (params<2) return false;\n            bool on=false;\n            switch (p[1]) {\n                case \"ON\"_hk:\n                case 1:\n                    on=true;\n                    break;\n                case \"OFF\"_hk:\n                case 0:\n                     break;\n                default:\n                 return false;\n            }              \n        DIAG(F(\"Railcom %S\")\n            ,DCCWaveform::setRailcom(on)?F(\"ON\"):F(\"OFF\"));\n        return true;     \n        }\n#endif\n#ifndef DISABLE_PROG\n    case \"ACK\"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>\n\tif (params >= 3) {\n            long duration;\n\t    if (p[1] == \"LIMIT\"_hk) {\n\t      DCCACK::setAckLimit(p[2]);\n\t      LCD(1, F(\"Ack Limit=%dmA\"), p[2]);       // <D ACK LIMIT 42>\n\t    } else if (p[1] == \"MIN\"_hk) {\n\t      if (params == 4 && p[3] == \"MS\"_hk)\n\t\tduration = p[2] * 1000L;\n\t      else\n\t\tduration = p[2];\n\t      DCCACK::setMinAckPulseDuration(duration);\n\t      LCD(0, F(\"Ack Min=%lus\"), duration);     // <D ACK MIN 1500>\n\t    } else if (p[1] == \"MAX\"_hk) {\n\t      if (params == 4 && p[3] == \"MS\"_hk)      // <D ACK MAX 80 MS>\n\t\tduration = p[2] * 1000L;\n\t      else\n\t\tduration = p[2];\n\t      DCCACK::setMaxAckPulseDuration(duration);\n\t      LCD(0, F(\"Ack Max=%lus\"), duration);     // <D ACK MAX 9000>\n\t    } else if (p[1] == \"RETRY\"_hk) {\n\t      if (p[2] >255) p[2]=3;\n\t      LCD(0, F(\"Ack Retry=%d Sum=%d\"), p[2], DCCACK::setAckRetry(p[2]));  //   <D ACK RETRY 2>\n\t    }\n\t} else {\n      bool onOff = (params > 0) && (p[1] == 1 || p[1] == \"ON\"_hk); // dont care if other stuff or missing... just means off\n    \n\t  DIAG(F(\"Ack diag %S\"), onOff ? F(\"on\") : F(\"off\"));\n\t  Diag::ACK = onOff;\n\t}\n        return true;\n#endif\n    default: // invalid/unknown\n      break;\n    }\n    return false;\n}\n\nbool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])\n{\n    if (params == 0)\n        return false;\n    bool onOff = (params > 0) && (p[1] == 1 || p[1] == \"ON\"_hk); // dont care if other stuff or missing... just means off\n    switch (p[0])\n    {\n    case \"CABS\"_hk: // <D CABS>\n        DCC::displayCabList(stream);\n        return true;\n\n    case \"RAM\"_hk: // <D RAM>\n        DIAG(F(\"Free memory=%d\"), DCCTimer::getMinimumFreeMemory());\n        return true;\n\n    case \"CMD\"_hk: // <D CMD ON/OFF>\n        Diag::CMD = onOff;\n        return true;\n\n#ifdef HAS_ENOUGH_MEMORY\n    case \"RAILCOM\"_hk: // <D RAILCOM ON/OFF>\n        Diag::RAILCOM = onOff;\n        return true;\n\n    case \"WIFI\"_hk: // <D WIFI ON/OFF>\n        Diag::WIFI = onOff;\n        return true;\n\n    case \"ETHERNET\"_hk: // <D ETHERNET ON/OFF>\n        Diag::ETHERNET = onOff;\n        return true;\n\n    case \"WIT\"_hk: // <D WIT ON/OFF>\n        Diag::WITHROTTLE = onOff;\n        return true;\n\n    case \"LCN\"_hk: // <D LCN ON/OFF>\n        Diag::LCN = onOff;\n        return true;\n\n    case \"SNIFFER\"_hk: // <D SNIFFER ON/OFF>\n        Diag::SNIFFER = onOff;\n        return true;\n\n    case \"WEBSOCKET\"_hk: // <D WEBSOCKET ON/OFF>\n        Diag::WEBSOCKET = onOff;\n        return true;\n#endif\n#ifndef DISABLE_EEPROM\n    case \"EEPROM\"_hk: // <D EEPROM NumEntries>\n\tif (params >= 2)\n\t    EEStore::dump(p[1]);\n\treturn true;\n#endif\n    case \"SERVO\"_hk:  // <D SERVO vpin position [profile]>\n\n    case \"ANOUT\"_hk:  // <D ANOUT vpin position [profile]>\n        IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);\n        return true;\n\n    case \"ANIN\"_hk:   // <D ANIN vpin>  Display analogue input value\n        DIAG(F(\"VPIN=%u value=%d\"), p[1], IODevice::readAnalogue(p[1]));\n        return true;\n\n#if !defined(IO_NO_HAL)\n    case \"HAL\"_hk: \n        if (p[1] == \"SHOW\"_hk) {\n          I2CManager.scanForDevices(&USB_SERIAL);\n          IODevice::DumpAll();\n        }\n        else if (p[1] == \"RESET\"_hk)\n          IODevice::reset();\n        return true;\n#endif\n\n    case \"TT\"_hk:     // <D TT vpin steps activity>\n        IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);\n        return true;\n\n    default: // invalid/unknown\n        return parseC(stream, params, p);\n    }\n    return false;\n}\n\n// ==========================\n// Turntable - no support if no HAL\n// <I> - list all\n// <I id> - broadcast type and current position\n// <I id DCC> - create DCC - This is TBA\n// <I id steps> - operate (DCC)\n// <I id steps activity> - operate (EXTT)\n// <I id ADD position value> - add position\n// <I id EXTT i2caddress vpin home> - create EXTT\n#ifndef IO_NO_HAL\nbool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])\n{\n    switch (params)\n    {\n    case 0: // <I> list turntable objects\n        return Turntable::printAll(stream);\n\n    case 1: // <I id> broadcast type and current position\n        {    \n            Turntable *tto = Turntable::get(p[0]);\n            if (tto) {\n                bool type = tto->isEXTT();\n                uint8_t position = tto->getPosition();\n                StringFormatter::send(stream, F(\"<I %d %d>\\n\"), type, position);\n            } else {\n                return false;\n            }\n        }\n        return true;\n    \n    case 2: // <I id position> - rotate a DCC turntable\n        {\n            Turntable *tto = Turntable::get(p[0]);\n            if (tto && !tto->isEXTT()) {\n                if (!tto->setPosition(p[0], p[1])) return false;\n            } else {\n                return false;\n            }\n        }\n        return true;\n\n    case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable\n        {\n            Turntable *tto = Turntable::get(p[0]);\n            if (p[1] == \"DCC\"_hk) {\n                if (tto || p[2] < 0 || p[2] > 3600) return false;\n                if (!DCCTurntable::create(p[0])) return false;\n                Turntable *tto = Turntable::get(p[0]);\n                tto->addPosition(0, 0, p[2]);\n                StringFormatter::send(stream, F(\"<I>\\n\"));\n            } else {\n                if (!tto) return false;\n                if (!tto->isEXTT()) return false;\n                if (!tto->setPosition(p[0], p[1], p[2])) return false;\n            }\n        }\n        return true;\n    \n    case 4: // <I id EXTT vpin home> create an EXTT turntable\n        {\n            Turntable *tto = Turntable::get(p[0]);\n            if (p[1] == \"EXTT\"_hk) {\n                if (tto || p[3] < 0 || p[3] > 3600) return false;\n                if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false;\n                Turntable *tto = Turntable::get(p[0]);\n                tto->addPosition(0, 0, p[3]);\n                StringFormatter::send(stream, F(\"<I>\\n\"));\n            } else {\n                return false;\n            }\n        }\n        return true;\n    \n    case 5: // <I id ADD position value angle> add a position\n        {\n            Turntable *tto = Turntable::get(p[0]);\n            if (p[1] == \"ADD\"_hk) {\n                // tto must exist, no more than 48 positions, angle 0 - 3600\n                if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false;\n                tto->addPosition(p[2], p[3], p[4]);\n                StringFormatter::send(stream, F(\"<I>\\n\"));\n            } else {\n                return false;\n            }\n        }\n        return true;\n    \n    default:    // Anything else is invalid\n        return false;\n    }\n}\n#endif\n\nbool DCCEXParser::parseJM(Print *stream, int16_t params, int16_t p[]) {\n  switch (params) {\n    case 1: // <JM> list all stashed automations\n        Stash::list(stream);\n        return true; \n        \n    case 2: // <JM id> get stash value \n        Stash::list(stream, p[1]);\n        return true;\n\n    case 3: // \n        if (p[1]==\"CLEAR\"_hk) {\n            if (p[2]==\"ALL\"_hk) { // <JM CLEAR ALL>\n                Stash::clearAll();\n                return true;\n            }\n            Stash::clear(p[2]); // <JM CLEAR id>\n            return true;\n        }\n        Stash::set(p[1], p[2]);  // <JM id loco>\n        return true;\n    \n    case 4: // <JM CLEAR ANY id>\n    if (p[1]==\"CLEAR\"_hk && p[2]==\"ANY\"_hk) { \n        // <JM CLEAR ANY id>\n        Stash::clearAny(p[3]);\n        return true;\n    }\n    \n    default: break;\n}\nreturn false;\n}\n\n// CALLBACKS must be static\nbool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)\n{\n    if (stashBusy )\n        return false;\n    stashBusy = true;\n    stashStream = stream;\n    stashRingStream=ringStream;\n    if (ringStream) stashTarget= ringStream->peekTargetMark();\n    memcpy(stashP, p, MAX_COMMAND_PARAMS * sizeof(p[0]));\n    return true;\n}\n\nPrint * DCCEXParser::getAsyncReplyStream() {\n       if (stashRingStream) {\n           stashRingStream->mark(stashTarget);\n           return stashRingStream;\n       }\n       return stashStream;\n}\n\nvoid DCCEXParser::commitAsyncReplyStream() {\n     if (stashRingStream) stashRingStream->commit();\n     stashBusy = false;\n}\n\nvoid DCCEXParser::callback_W(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(),\n          F(\"<r %d %d>\\n\"), stashP[0], result == 1 ? stashP[1] : -1);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_W4(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(),\n\t  F(\"<r%d|%d|%d %d>\\n\"), stashP[2], stashP[3], stashP[0], result == 1 ? stashP[1] : -1);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_B(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(), \n          F(\"<r%d|%d|%d %d %d>\\n\"), stashP[3], stashP[4], stashP[0], stashP[1], result == 1 ? stashP[2] : -1);\n    commitAsyncReplyStream();\n}\nvoid DCCEXParser::callback_Vbit(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(), F(\"<v %d %d %d>\\n\"), stashP[0], stashP[1], result);\n    commitAsyncReplyStream();\n}\nvoid DCCEXParser::callback_Vbyte(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(), F(\"<v %d %d>\\n\"), stashP[0], result);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_R(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(), F(\"<r%d|%d|%d %d>\\n\"), stashP[1], stashP[2], stashP[0], result);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_r(int16_t result)\n{\n    StringFormatter::send(getAsyncReplyStream(), F(\"<r %d %d %d >\\n\"), stashP[0], stashP[1], result);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_Rloco(int16_t result) {\n  // determine type of <R command from stashP[0]\n  const FSH * typetag;\n  if (stashP[0]==\"LOCOID\"_hk) {\n    typetag=F(\"LOCOID \");\n  } else if (stashP[0]==\"CONSIST\"_hk) {\n    typetag=F(\"CONSIST \");\n  } else {\n    typetag=F(\"\");\n  }\n\n  const FSH * detail;\n  if (result<=0) {\n    detail=F(\"<r %S%d>\\n\");\n  } else {\n    bool longAddr=result & LONG_ADDR_MARKER;        //long addr\n    if (longAddr)\n      result = result^LONG_ADDR_MARKER;\n    if (longAddr && result <= HIGHEST_SHORT_ADDR)\n      detail=F(\"<r LONG %S%d UNSUPPORTED>\\n\");\n    else\n      detail=F(\"<r %S%d>\\n\");\n  }\n  StringFormatter::send(getAsyncReplyStream(), detail, typetag,result);\n  commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_Wloco(int16_t result)\n{\n    if (result==1) result=stashP[0]; // pick up original requested id from command\n    StringFormatter::send(getAsyncReplyStream(), F(\"<w %d>\\n\"), result);\n    commitAsyncReplyStream();\n}\n\nvoid DCCEXParser::callback_Wconsist(int16_t result)\n{\n    if (result==-4) DIAG(F(\"Long Consist %d not supported by decoder\"),stashP[1]);\n    if (result==1) result=stashP[1]; // pick up original requested id from command\n    StringFormatter::send(getAsyncReplyStream(), F(\"<w CONSIST %d%S>\\n\"),\n     result, stashP[2]==\"REVERSE\"_hk ? F(\" REVERSE\") : F(\"\"));\n    commitAsyncReplyStream();\n}\n"
  },
  {
    "path": "DCCEXParser.h",
    "content": "/*\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DCCEXParser_h\n#define DCCEXParser_h\n#include <Arduino.h>\n#include \"FSH.h\"\n#include \"RingStream.h\"\n#include \"defines.h\"\n\ntypedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);\ntypedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);\n\nstruct DCCEXParser\n{\n   \n   static void parse(Print * stream,  byte * command,  RingStream * ringStream);\n   static void parse(const FSH * cmd);\n   static void parseOne(Print * stream,  byte * command,  RingStream * ringStream);\n   static void setFilter(FILTER_CALLBACK filter);\n   static void setRMFTFilter(FILTER_CALLBACK filter);\n   static void setCamParserFilter(FILTER_CALLBACK filter);\n   static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);\n   static const int MAX_COMMAND_PARAMS=10;  // Must not exceed this\n   static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);\n \n   private:\n  \n    static const int16_t MAX_BUFFER=50;  // longest command sent in\n    static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], byte * command, bool usehex);\n     \n    static bool parseT(Print * stream, int16_t params, int16_t p[]);\n    static bool parseZ(Print * stream, int16_t params, int16_t p[]);\n    static bool parsey(Print * stream, int16_t params, int16_t p[]);\n    static bool parseS(Print * stream, int16_t params, int16_t p[]);\n    static bool parsef(Print * stream, int16_t params, int16_t p[]);\n    static bool parseC(Print * stream, int16_t params, int16_t p[]);\n    static bool parseD(Print * stream, int16_t params, int16_t p[]);\n    static bool parseJM(Print * stream, int16_t params, int16_t p[]);\n#ifndef IO_NO_HAL\n    static bool parseI(Print * stream, int16_t params, int16_t p[]);\n#endif\n\n    static Print * getAsyncReplyStream();\n    static void commitAsyncReplyStream();\n\n    static bool stashBusy;\n    static byte stashTarget;\n    static Print * stashStream;\n    static RingStream * stashRingStream;\n    \n    static int16_t stashP[MAX_COMMAND_PARAMS];\n    static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);\n    static void callback_W(int16_t result);\n    static void callback_W4(int16_t result);\n    static void callback_B(int16_t result);        \n    static void callback_R(int16_t result); // prog\n    static void callback_r(int16_t result); // main\n    static void callback_Rloco(int16_t result);\n    static void callback_Wloco(int16_t result);\n    static void callback_Wconsist(int16_t result);\n    static void callback_Vbit(int16_t result);\n    static void callback_Vbyte(int16_t result);\n    static FILTER_CALLBACK  filterCallback;\n    static FILTER_CALLBACK  filterRMFTCallback;\n    static FILTER_CALLBACK  filterCamParserCallback;\n    static AT_COMMAND_CALLBACK  atCommandCallback;\n    static void sendFlashList(Print * stream,const int16_t flashList[]);\n\n};\n\n#endif\n"
  },
  {
    "path": "DCCPacket.h",
    "content": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include <Arduino.h>\n#ifndef DCCPacket_h\n#define DCCPacket_h\n#include <strings.h>\n#include \"defines.h\"\n\nclass DCCPacket {\npublic:\n  DCCPacket() {\n    _len = 0;\n    _data = NULL;\n  };\n  DCCPacket(byte *d, byte l) {\n    _len = l;\n    _data = new byte[_len];\n    for (byte n = 0; n<_len; n++)\n      _data[n] = d[n];\n  };\n  DCCPacket(const DCCPacket &old) {\n    _len = old._len;\n    _data = new byte[_len];\n    for (byte n = 0; n<_len; n++)\n      _data[n] = old._data[n];\n  };\n  DCCPacket &operator=(const DCCPacket &rhs) {\n    if (this == &rhs)\n      return *this;\n    delete[]_data;\n    _len = rhs._len;\n    _data = new byte[_len];\n    for (byte n = 0; n<_len; n++)\n      _data[n] = rhs._data[n];\n    return *this;\n  };\n  ~DCCPacket() {\n    if (_len) {\n      delete[]_data;\n      _len = 0;\n      _data = NULL;\n    }\n  };\n  inline bool operator==(const DCCPacket &right) {\n    if (_len != right._len)\n      return false;\n    if (_len == 0)\n      return true;\n    return (bcmp(_data, right._data, _len) == 0);\n  };\n  void print() {\n    static const char hexchars[]=\"0123456789ABCDEF\";\n    USB_SERIAL.print(F(\"<* DCCPACKET \"));\n    for (byte n = 0; n< _len; n++) {\n      USB_SERIAL.print(hexchars[_data[n]>>4]);\n      USB_SERIAL.print(hexchars[_data[n] & 0x0f]);\n      USB_SERIAL.print(' ');\n    }\n    USB_SERIAL.print(F(\"*>\\n\"));\n  };\n  inline byte len() {return _len;};\n  inline byte *data() {return _data;};\nprivate:\n  byte _len = 0;\n  byte *_data = NULL;\n};\n#endif\n"
  },
  {
    "path": "DCCQueue.cpp",
    "content": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* What does this queue manager do:\n  1. It provides a high priority queue and a low priority queue.\n  2. It manages situations where multiple loco speed commands are in the queue.\n  3. It allows an ESTOP to jump the queue and eliminate any outstanding speed commands that would later undo the stop.\n  4. It allows for coil on/off accessory commands to be synchronized to a given time delay.\n  5. It prevents transmission of sequential packets to the same loco id \n  */\n#include \"Arduino.h\"\n#include \"defines.h\"\n#include \"DCCQueue.h\"\n#include \"DCCWaveform.h\"\n#include \"DIAG.h\"\n\n// create statics \nDCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue();\nDCCQueue* DCCQueue::highPriorityQueue=new DCCQueue();\nPendingSlot* DCCQueue::recycleList=nullptr;\nuint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the same loco in a row\n\n\n    DCCQueue::DCCQueue() {\n        head=nullptr;\n        tail=nullptr;\n    }\n    \n    void DCCQueue::addQueue(PendingSlot* p) {\n        if (tail) tail->next=p;\n        else head=p;\n        tail=p;\n        p->next=nullptr;          \n    }\n\n    void DCCQueue::jumpQueue(PendingSlot* p) {\n        p->next=head;\n        head=p;\n        if (!tail) tail=p; \n    }\n\n    \n    void DCCQueue::recycle(PendingSlot* p) {\n        p->next=recycleList;\n        recycleList=p;\n    }\n    \n    void DCCQueue::remove(PendingSlot* premove) {\n        PendingSlot* previous=nullptr;\n        for (auto p=head;p;previous=p,p=p->next) {\n            if (p==premove) {\n                // remove this slot from the queue \n                if (previous) previous->next=p->next;\n                else head=p->next;\n                if (p==tail) tail=previous; // if last packet, update tail\n                return;\n            }\n        }\n        DIAG(F(\"DCCQueue::remove slot not found\"));\n      \n    }\n\n    // Packet joins end of low priority queue.\n    void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {\n        lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));\n    }\n    \n    // Packet replaces existing loco speed packet or joins end of high priority queue. \n    \n    void DCCQueue::scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco) {\n        for (auto p=highPriorityQueue->head;p;p=p->next) {\n            if (p->locoId==loco) {\n                // replace existing packet\n                memcpy(p->packet,packet,length);\n                p->packetLength=length;\n                p->packetRepeat=repeats;\n                return;\n            }\n        }\n        highPriorityQueue->addQueue(getSlot(SPEED_PACKET,packet,length,repeats,loco));\n    }\n    \n    // Packet replaces existing loco function packet or joins end of high priority queue. \n    \n    void DCCQueue::scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group) {\n        PendingType type=DEAD_PACKET;\n        switch(group) {\n            case 1: type=FUNCTION1_PACKET; break;\n            case 2: type=FUNCTION2_PACKET; break;\n            case 3: type=FUNCTION3_PACKET; break;\n            case 4: type=FUNCTION4_PACKET; break;\n            case 5: type=FUNCTION5_PACKET; break;\n            default:\n                DIAG(F(\"DCCQueue::scheduleDCCFunctionPacket invalid group %d\"),group);\n                return; // invalid group\n        }\n\n        for (auto p=lowPriorityQueue->head;p;p=p->next) {\n            if (p->locoId==loco && p->type==type) {\n                // replace existing packet for same loco and function group\n                memcpy(p->packet,packet,length);\n                p->packetLength=length;\n                p->packetRepeat=0;\n                return;\n            }\n        }\n        lowPriorityQueue->addQueue(getSlot(type,packet,length,0,loco));\n    }\n    \n    // ESTOP -  \n    // any outstanding throttle packet for this loco (all if loco=0) discarded\n    // Packet joins start of queue,\n   \n    \n    void DCCQueue::scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco) {\n        \n        // DIAG(F(\"DCC ESTOP loco=%d\"),loco);\n       \n        // kill any existing throttle packets for this loco (or all locos if broadcast)\n        // this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced\n        PendingSlot * pNext=nullptr;\n        for (auto p=highPriorityQueue->head;p;p=pNext) {\n            pNext=p->next; // save next packet in case we recycle this one                \n            if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {\n                // remove this slot from the queue or it will interfere with our ESTOP\n                highPriorityQueue->remove(p);\n                recycle(p);  // recycle this slot\n            }\n        }\n        // add the estop packet to the start of the queue\n        highPriorityQueue->jumpQueue(getSlot(SPEED_PACKET,packet,length,repeats,0));\n    }\n\n    // Accessory coil-On Packet joins end of queue as normal.\n    // When dequeued, packet is retained at start of queue \n    // but modified to coil-off and given the delayed start.\n    // getNext will ignore this packet until the requested start time. \n    void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {\n        auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);\n        p->delayOff=delayms;\n        lowPriorityQueue->addQueue(p);\n    };\n\n    \n    // Schedule the next dcc packet from the queues or an idle packet if none pending.\n    const byte idlePacket[] = {0xFF, 0x00};\n\n    bool DCCQueue::scheduleNext(bool force) {\n        if (highPriorityQueue->scheduleNextInternal()) return true;\n        if (lowPriorityQueue->scheduleNextInternal()) return true;\n        if (force) {\n            // This will arise when there is nothing available to be sent that will not compromise the rules\n            // typically this will only happen when there is only one loco in the reminders as the closely queued \n            // speed and function reminders must be separated by at least one packet not sent to that loco.\n            DCCWaveform::mainTrack.schedulePacket(idlePacket,sizeof(idlePacket),0);\n            lastSentPacketLocoId=0;\n            return true;\n        }\n        return false;\n    }\n\n    bool DCCQueue::scheduleNextInternal() {\n\n        for (auto p=head;p;p=p->next) {\n            // skip over pending ACC_OFF packets which are still delayed\n            if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;\n            if (p->locoId) {\n                // Prevent two consecutive packets to the same loco. \n                // this also means repeats cant be done by waveform \n                if (p->locoId==lastSentPacketLocoId) continue;  // try again later \n                DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,0);\n                lastSentPacketLocoId=p->locoId;\n                if (p->packetRepeat) {\n                    p->packetRepeat--;\n                    return true; // leave this packet in the queue\n                }\n            }\n            else {\n                // Non loco packets can repeat automatically\n                DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);\n                lastSentPacketLocoId=0;\n            }\n\n            // remove this slot from the queue \n            remove(p);\n            \n            // special cases handling \n            if (p->type == ACC_ON_PACKET) {\n                // convert to a delayed off packet and jump the high priority queue\n                p->type= ACC_OFF_PACKET;\n                p->packet[1]  &= ~0x08; // set C to 0 (gate off) \n                p->startTime=millis()+p->delayOff;\n                highPriorityQueue->jumpQueue(p);\n            }\n            else recycle(p); \n            return true;\n        }\n        \n        // No packets found\n        return false;\n    }\n    \n    // obtain and initialise slot for a PendingSlot.\n    PendingSlot*  DCCQueue::getSlot(PendingType type, byte* packet, byte length, byte repeats,uint16_t loco) {\n        PendingSlot * p; \n        if (recycleList) {\n            p=recycleList;\n            recycleList=p->next;\n        }\n        else { \n            static int16_t created=0;\n            int16_t q1=0;\n            int16_t q2=0;\n            for (auto p=highPriorityQueue->head;p;p=p->next) q1++;\n            for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;\n            bool leak=(q1+q2)!=created;\n            DIAG(F(\"New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d\"),\n                   (int16_t)type,length,loco,q1,q2, created);\n            if (leak) {\n                for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F(\"q1 %d %d\"),p->type,p->locoId);\n                for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F(\"q2 %d %d\"),p->type,p->locoId);\n            }       \n            p=new PendingSlot; // need a queue entry\n            created++; \n        }\n        p->next=nullptr;\n        p->type=type;\n        p->packetLength=length;\n        p->packetRepeat=repeats; \n        if (length>sizeof(p->packet)) {\n            DIAG(F(\"DCC bad packet length=%d\"),length);\n            length=sizeof(p->packet); // limit to size of packet\n        }\n        p->startTime=0; // not used for loco packets\n        memcpy((void*)p->packet,packet,length);\n        p->locoId=loco;\n        return p; \n    }\n\n    \n"
  },
  {
    "path": "DCCQueue.h",
    "content": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef DCCQueue_h\n#define DCCQueue_h\n#include \"Arduino.h\"\n#include \"DCCWaveform.h\"\n\nenum PendingType:byte {NORMAL_PACKET,\n  FUNCTION1_PACKET, FUNCTION2_PACKET, FUNCTION3_PACKET, FUNCTION4_PACKET, FUNCTION5_PACKET,\n  SPEED_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};\n  \n  struct PendingSlot {\n      PendingSlot* next; \n      PendingType type;\n      byte packetLength;\n      byte packetRepeat;\n      byte packet[MAX_PACKET_SIZE];\n      \n      union { // use depends on packet type\n        uint16_t locoId;    // SPEED & FUNCTION packets  \n        uint16_t delayOff;  // ACC_ON_PACKET delay to apply between on/off\n        uint32_t startTime; // ACC_OFF_PACKET time (mS) to transmit \n      };\n  };\n  \nclass DCCQueue {\n  public:\n      \n    // Non-speed packets are queued in the main queue\n    static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);\n\n    // Speed packets are queued in the high priority queue\n    static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);\n    \n    // Function group packets are queued in the low priority queue\n    static void scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group);\n\n    // ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco  \n    static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco);\n\n    // Accessory gate-On Packet joins end of main queue as normal. \n    // When dequeued, packet is modified to gate-off and given the delayed start in the high priority queue. \n    // getNext will ignore this packet until the requested start time.\n    static void scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms);\n\n  \n    // Schedules a main track packet from the queues.\n    static bool scheduleNext(bool force); \n\n  private:\n    bool scheduleNextInternal(); \n  // statics to manage high and low priority queues and recycleing of PENDINGs\n    static PendingSlot* recycleList;\n    static DCCQueue* highPriorityQueue;\n    static DCCQueue* lowPriorityQueue;\n    static uint16_t lastSentPacketLocoId; // used to prevent two packets to the same loco in a row\n\n    DCCQueue();\n    \n    PendingSlot*  head;\n    PendingSlot * tail;\n    \n    // obtain and initialise slot for a PendingSlot. \n    static PendingSlot*  getSlot(PendingType type, byte* packet, byte length, byte repeats, uint16_t loco);\n    static void recycle(PendingSlot* p);\n    void addQueue(PendingSlot * p);\n    void jumpQueue(PendingSlot * p);\n    void remove(PendingSlot * p);\n};\n#endif // DCCQueue_h\n"
  },
  {
    "path": "DCCRMT.cpp",
    "content": "/*\n *  © 2021-2024, Harald Barth.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * RMT has \"channels\" which us FIFO RAM where you place what you want to send\n * or receive. Channels can be merged to get more words per channel.\n *\n * WROOM: 8 channels total of 512 words, 64 words per channel. We use currently\n * channel 0+1 for 128 words for DCC MAIN and 2+3 for DCC PROG.\n *\n * S3: 8 channels total of 384 words. 4 channels dedicated for TX and 4 channels\n * dedicated for RX. 48 words per channel. So for TX there are 4 channels and we\n * could use them with 96 words for MAIN and PROG if DCC data does fit in there.\n *\n * C3: 4 channels total of 192 words. As we do not use RX we can use all for TX\n * so the situation is the same as for the -S3\n *\n * C6, H2: 4 channels total of 192 words. 2 channels dedictaed for TX and\n * 2 channels dedicated for RX. Half RMT capacity compared to the C3.\n *\n */\n\n#if defined(ARDUINO_ARCH_ESP32)\n#include \"defines.h\"\n#include \"DIAG.h\"\n#include \"DCCRMT.h\"\n#include \"DCCTimer.h\"\n#include \"DCCWaveform.h\" // for MAX_PACKET_SIZE\n#include \"soc/gpio_sig_map.h\"\n\n// check for right type of ESP32\n#include \"soc/soc_caps.h\"\n#ifndef SOC_RMT_MEM_WORDS_PER_CHANNEL\n#error This symobol should be defined\n#endif\n#if SOC_RMT_MEM_WORDS_PER_CHANNEL < 64\n#warning This is not an ESP32-WROOM but some other unsupported variant\n#warning You are outside of the DCC-EX supported hardware\n#endif\n\nstatic const byte RMT_CHAN_PER_DCC_CHAN = 2;\n\n// Number of bits resulting out of X bytes of DCC payload data\n// Each byte has one bit extra and at the end we have one EOF marker\n#define DATA_LEN(X) ((X)*9+1)\n\n#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,2,0)\n#error wrong IDF version\n#endif\n\nvoid setDCCBit1(rmt_item32_t* item) {\n  item->level0    = 1;\n  item->duration0 = DCC_1_HALFPERIOD;\n  item->level1    = 0;\n  item->duration1 = DCC_1_HALFPERIOD;\n}\n\nvoid setDCCBit0(rmt_item32_t* item) {\n  item->level0    = 1;\n  item->duration0 = DCC_0_HALFPERIOD;\n  item->level1    = 0;\n  item->duration1 = DCC_0_HALFPERIOD;\n}\n\n// special long zero to trigger scope\nvoid setDCCBit0Long(rmt_item32_t* item) {\n  item->level0    = 1;\n  item->duration0 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;\n  item->level1    = 0;\n  item->duration1 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;\n}\n\nvoid setEOT(rmt_item32_t* item) {\n  item->val = 0;\n}\n\n// This is an array that contains the this pointers\n// to all uses channel objects. This is used to determine\n// which of the channels was triggering the ISR as there\n// is only ONE common ISR routine for all channels.\nRMTChannel *channelHandle[8] = { 0 };\n\nvoid IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {\n  RMTChannel *tt = channelHandle[channel];\n  if (tt) tt->RMTinterrupt();\n  if (channel == 0)\n    DCCTimer::updateMinimumFreeMemoryISR(0);\n}\n\nRMTChannel::RMTChannel(pinpair pins, bool isMain) {\n  byte ch;\n  byte plen;\n\n  // Below we check if the DCC packet actually fits into the RMT hardware\n  // Currently MAX_PACKET_SIZE = 5 so with checksum there are\n  // MAX_PACKET_SIZE+1 data packets. Each need DATA_LEN (9) bits.\n  // To that we add the preamble length, the fencepost DCC end bit\n  // and the RMT EOF marker.\n  // SOC_RMT_MEM_WORDS_PER_CHANNEL is either 64 (original WROOM) or\n  // 48 (all other ESP32 like the -C3 or -S2\n  // The formula to get the possible MAX_PACKET_SIZE is\n  //\n  // ALLOCATED = RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL\n  // MAX_PACKET_SIZE = floor((ALLOCATED - PREAMBLE_LEN - 2)/9 - 1)\n  //\n\n  if (isMain) {\n    ch = 0;\n    plen = PREAMBLE_BITS_MAIN;\n    static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_MAIN + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,\n\t\t  \"Number of DCC packet bits greater than ESP32 RMT memory available\");\n  } else {\n    ch = RMT_CHAN_PER_DCC_CHAN; // number == offset\n    plen = PREAMBLE_BITS_PROG;\n    static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_PROG + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,\n\t\t   \"Number of DCC packet bits greater than ESP32 RMT memory available\");\n  }\n    \n  // preamble\n  preambleLen = plen+2; // plen 1 bits, one 0 bit and one EOF marker\n  preamble = (rmt_item32_t*)malloc(preambleLen*sizeof(rmt_item32_t));\n  for (byte n=0; n<plen; n++)\n    setDCCBit1(preamble + n);      // preamble bits\n#ifdef SCOPE\n  setDCCBit0Long(preamble + plen); // start of packet 0 bit long version\n#else\n  setDCCBit0(preamble + plen);     // start of packet 0 bit normal version\n#endif\n  setEOT(preamble + plen + 1);     // EOT marker\n\n  // idle\n  idleLen = 28;\n  idle = (rmt_item32_t*)malloc(idleLen*sizeof(rmt_item32_t));\n  if (isMain) {\n    for (byte n=0; n<8; n++)   // 0 to 7\n      setDCCBit1(idle + n);\n    for (byte n=8; n<18; n++)  // 8, 9 to 16, 17\n      setDCCBit0(idle + n);\n    for (byte n=18; n<26; n++) // 18 to 25\n      setDCCBit1(idle + n);\n  } else {\n    for (byte n=0; n<26; n++)  // all zero\n      setDCCBit0(idle + n);\n  }\n  setDCCBit1(idle + 26);     // end bit\n  setEOT(idle + 27);         // EOT marker\n\n  // data: max packet size today is 5 + checksum\n  maxDataLen = DATA_LEN(MAX_PACKET_SIZE+1);  // plus checksum\n  data = (rmt_item32_t*)malloc(maxDataLen*sizeof(rmt_item32_t));\n\n  rmt_config_t config;\n  // Configure the RMT channel for TX\n  bzero(&config, sizeof(rmt_config_t));\n  config.rmt_mode = RMT_MODE_TX;\n  config.channel = channel = (rmt_channel_t)ch;\n  config.clk_div = RMT_CLOCK_DIVIDER;\n  config.gpio_num = (gpio_num_t)pins.pin;\n  config.mem_block_num = RMT_CHAN_PER_DCC_CHAN;\n  // use config\n  ESP_ERROR_CHECK(rmt_config(&config));\n  addPin(pins.invpin, true);\n  \n  // NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask\n  ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));\n\n  // DIAG(F(\"Register interrupt on core %d\"), xPortGetCoreID());\n\n  ESP_ERROR_CHECK(rmt_set_tx_loop_mode(channel, true));\n  channelHandle[channel] = this; // used by interrupt\n  rmt_register_tx_end_callback(interrupt, 0);\n  rmt_set_tx_intr_en(channel, true);\n\n  DIAG(F(\"Channel %d DCC signal for %s start\"), config.channel, isMain ? \"MAIN\" : \"PROG\");\n\n  // send one bit to kickstart the signal, remaining data will come from the\n  // packet queue. We intentionally do not wait for the RMT TX complete here.\n  //rmt_write_items(channel, preamble, preambleLen, false);\n  RMTprefill();\n  dataReady = false;\n}\n\nvoid RMTChannel::RMTprefill() {\n  rmt_fill_tx_items(channel, preamble, preambleLen, 0);\n  rmt_fill_tx_items(channel, idle, idleLen, preambleLen-1);\n}\n\nconst byte transmitMask[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};\n\nint RMTChannel::RMTfillData(const byte buffer[], byte byteCount, byte repeatCount=0) {\n  //int RMTChannel::RMTfillData(dccPacket packet) {\n  // dataReady: Signals to then interrupt routine. It is set when\n  // we have data in the channel buffer which can be copied out\n  // to the HW. dataRepeat on the other hand signals back to\n  // the caller of this function if the data has been sent enough\n  // times (0 to 3 means 1 to 4 times in total).\n  {\n    volatile byte dr = dataRepeat; // copy to test variable, not to be optimezed away\n    if (dr > 0) // we have still old work to do\n      return dr;\n  }\n  if (dataReady == true) // the packet is not copied out yet\n    return 1000;\n  if (DATA_LEN(byteCount) > maxDataLen) {  // this would overun our allocated memory for data\n    DIAG(F(\"Can not convert DCC bytes # %d to DCC bits %d, buffer too small\"), byteCount, maxDataLen);\n    return -1;                          // something very broken, can not convert packet\n  }\n\n  // convert bytes to RMT stream of \"bits\"\n  byte bitcounter = 0;\n  for(byte n=0; n<byteCount; n++) {\n    for(byte bit=0; bit<8; bit++) {\n      if (buffer[n] & transmitMask[bit])\n\tsetDCCBit1(data + bitcounter++);\n      else\n\tsetDCCBit0(data + bitcounter++);\n    }\n    setDCCBit0(data + bitcounter++); // zero at end of each byte\n  }\n  setDCCBit1(data + bitcounter-1);     // overwrite previous zero bit with one bit\n  setEOT(data + bitcounter++);         // EOT marker\n  dataLen = bitcounter;\n  noInterrupts();                      // keep dataReady and dataRepeat consistnet to each other\n  dataReady = true;\n  dataRepeat = repeatCount+1;         // repeatCount of 0 means send once\n  interrupts();\n  return 0;\n}\n\nvoid IRAM_ATTR RMTChannel::RMTinterrupt() {\n  //no rmt_tx_start(channel,true) as we run in loop mode\n  //preamble is always loaded at beginning of buffer\n  packetCounter++;\n  if (!dataReady && dataRepeat == 0) { // we did run empty\n    rmt_fill_tx_items(channel, idle, idleLen, preambleLen-1);\n    return; // nothing to do about that\n  }\n\n  // take care of incoming data\n  if (dataReady) {            // if we have new data, fill while preamble is running\n    rmt_fill_tx_items(channel, data, dataLen, preambleLen-1);\n    dataReady = false;\n    if (dataRepeat == 0)       // all data should go out at least once\n      DIAG(F(\"Channel %d DCC signal lost data\"), channel);\n  }\n  if (dataRepeat > 0)         // if a repeat count was specified, work on that\n    dataRepeat--;\n}\n\nbool RMTChannel::addPin(byte pin, bool inverted) {\n  if (pin == UNUSED_PIN)\n    return true;\n  gpio_num_t gpioNum = (gpio_num_t)(pin);\n  esp_err_t err;\n  PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);\n  err = gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);\n  if (err != ESP_OK) return false;\n  gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX+channel, inverted, 0);\n  if (err != ESP_OK) return false;\n  return true;\n}\nbool RMTChannel::addPin(pinpair pins) {\n  return addPin(pins.pin) && addPin(pins.invpin, true);\n}\n#endif //ESP32\n"
  },
  {
    "path": "DCCRMT.h",
    "content": "/*\n *  © 2021-2022, Harald Barth.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#if defined(ARDUINO_ARCH_ESP32)\n#pragma once\n#include <Arduino.h>\n#include \"driver/rmt.h\"\n#include \"soc/rmt_reg.h\"\n#include \"soc/rmt_struct.h\"\n#include \"MotorDriver.h\" // for class pinpair\n\n// make calculations easy and set up for microseconds\n#define RMT_CLOCK_DIVIDER 80\n#define DCC_1_HALFPERIOD 58  //4640 // 1 / 80000000 * 4640 = 58us\n#define DCC_0_HALFPERIOD 100 //8000\n\nclass RMTChannel {\n public:\n  RMTChannel(pinpair pins, bool isMain);\n  bool addPin(byte pin, bool inverted=0);\n  bool addPin(pinpair pins);\n  void IRAM_ATTR RMTinterrupt();\n  void RMTprefill();\n  //int RMTfillData(dccPacket packet);\n  int RMTfillData(const byte buffer[], byte byteCount, byte repeatCount);\n  inline bool busy() {\n    if (dataRepeat > 0) // we have still old work to do\n      return true;\n    return dataReady;\n  };\n  inline void waitForDataCopy() {\n    while(1) { // do nothing and wait for interrupt clearing dataReady to happen\n      if (dataReady == false)\n\tbreak;\n    }\n  };\n  inline uint32_t packetCount() { return packetCounter; };\n  \n private:\n    \n  rmt_channel_t channel;\n  // 3 types of data to send, preamble and then idle or data\n  // if this is prog track, idle will contain reset instead\n  rmt_item32_t *idle;\n  byte idleLen;\n  rmt_item32_t *preamble;\n  byte preambleLen;\n  rmt_item32_t *data;\n  byte dataLen;\n  byte maxDataLen;\n  uint32_t packetCounter = 0;\n  // flags \n  volatile bool dataReady = false;    // do we have real data available or send idle\n  volatile byte dataRepeat = 0;\n};\n#endif //ESP32\n"
  },
  {
    "path": "DCCTimer.h",
    "content": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021-2023 Harald Barth\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* There are several different implementations of this class which the compiler will select \n   according to the hardware.\n   */\n\n/* This timer class is used to manage the single timer required to handle the DCC waveform.\n *  All timer access comes through this class so that it can be compiled for \n *  various hardware CPU types. \n *  \n *  DCCEX works on a single timer interrupt at a regular 58uS interval.\n *  The DCCWaveform class generates the signals to the motor shield  \n *  based on this timer. \n *  \n *  If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,\n *  (see isPWMPin() function. )\n *  then this allows us to use a hardware driven pin switching arrangement which is\n *  achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on \n *  the required pin state. (see setPWM())  \n *  This is more accurate than the software interrupt but at the expense of \n *  limiting the choice of available pins. \n *  Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM... \n *  Other shields may be jumpered to PWM pins or run directly using the software interrupt.\n *  \n *  Because the PWM-based waveform is effectively set half a cycle after the software version,\n *  it is not acceptable to drive the two tracks on different methiods or it would cause\n *  problems for <1 JOIN> etc.\n *  \n */\n\n#ifndef DCCTimer_h\n#define DCCTimer_h\n#include \"Arduino.h\"\n\ntypedef void (*INTERRUPT_CALLBACK)();\n\nclass DCCTimer {\n  public:\n  static void begin(INTERRUPT_CALLBACK interrupt);\n  static void getSimulatedMacAddress(byte mac[6]);\n  static bool isPWMPin(byte pin);\n  static void setPWM(byte pin, bool high);\n  static void clearPWM();\n  static void startRailcomTimer(byte brakePin);\n  static void ackRailcomTimer();\n  static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);\n  static void DCCEXanalogWrite(uint8_t pin, int value, bool invert);\n  static void DCCEXledcDetachPin(uint8_t pin);\n  static void DCCEXanalogCopyChannel(int8_t frompin, int8_t topin);\n  static void DCCEXInrushControlOn(uint8_t pin, int duty, bool invert);\n  static void DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted);\n\n// Update low ram level.  Allow for extra bytes to be specified\n// by estimation or inspection, that may be used by other \n// called subroutines.  Must be called with interrupts disabled.\n// \n// Although __brkval may go up and down as heap memory is allocated\n// and freed, this function records only the worst case encountered.\n// So even if all of the heap is freed, the reported minimum free \n// memory will not increase.\n//\n  static void inline updateMinimumFreeMemoryISR(unsigned char extraBytes=0)\n    __attribute__((always_inline)) {\n    int spare = freeMemory()-extraBytes;\n    if (spare < 0) spare = 0;\n    if (spare < minimum_free_memory) minimum_free_memory = spare;\n  };\n\n  static int  getMinimumFreeMemory();\n  static void reset();\n  \nprivate:\n  static void DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency);\n  static int freeMemory();\n  static volatile int minimum_free_memory;\n  static const int DCC_SIGNAL_TIME=58;  // this is the 58uS DCC 1-bit waveform half-cycle \n#if defined(ARDUINO_ARCH_STM32)  // TODO: PMA temporary hack - assumes 100Mhz F_CPU as STM32 can change frequency\n  static const long CLOCK_CYCLES=(100000000L / 1000000 * DCC_SIGNAL_TIME) >>1;\n#else\n  static const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;\n#endif\n\n};\n\n// Class ADCee implements caching of the ADC value for platforms which\n// have a too slow ADC read to wait for. On these platforms the ADC is\n// scanned continiously in the background from an ISR. On such\n// architectures that use the analog read during DCC waveform with\n// specially configured ADC, for example AVR, init must be called\n// PRIOR to the start of the waveform. It returns the current value so\n// that an offset can be initialized.\nclass ADCee {\npublic:\n  // begin is called for any setup that must be done before\n  // **init** can be called. On some architectures this involves ADC\n  // initialisation and clock routing, sampling times etc.\n  static void begin();\n  // init adds the pin to the list of scanned pins (if this\n  // platform's implementation scans pins) and returns the first\n  // read value (which is why it required begin to have been called first!)\n  // It must be called before the regular scan is started.\n  static int init(uint8_t pin);\n  // read does read the pin value from the scanned cache or directly\n  // if this is a platform that does not scan. fromISR is a hint if\n  // it was called from ISR because for some implementations that\n  // makes a difference.\n  static int read(uint8_t pin, bool fromISR=false);\n  // returns possible max value that the ADC can return\n  static int16_t ADCmax();\nprivate:\n  // On platforms that scan, it is called from waveform ISR\n  // only on a regular basis.\n  static void scan();\n  #if defined (ARDUINO_ARCH_STM32)\n  // bit array of used pins (max 32)\n  static uint32_t usedpins;\n  static uint32_t * analogchans;        // Array of channel numbers to be scanned\n  static ADC_TypeDef * * adcchans;      // Array to capture which ADC is each input channel on\n#else\n  // bit array of used pins (max 16)\n  static uint16_t usedpins;\n#endif\n  static uint8_t highestPin;\n  // cached analog values (malloc:ed to actual number of ADC channels)\n  static int *analogvals;\n  // friend so that we can call scan() and begin()\n  friend class DCCWaveform;\n  };\n#endif\n"
  },
  {
    "path": "DCCTimerAVR.cpp",
    "content": "/*\n *  © 2021 Mike S\n *  © 2021-2023 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021-2025 Chris Harlow\n *  © 2021 David Cutting\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// ATTENTION: this file only compiles on a UNO or MEGA\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n#ifdef ARDUINO_ARCH_AVR\n#include <avr/boot.h> \n#include <avr/wdt.h>\n#include \"DCCTimer.h\"\n#include \"DIAG.h\"\n#ifdef DEBUG_ADC\n#include \"TrackManager.h\"\n#endif\nINTERRUPT_CALLBACK interruptHandler=0;\n\n  // Arduino nano, uno, mega etc\n#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n    #define TIMER1_A_PIN   11\n    #define TIMER1_B_PIN   12\n    #define TIMER1_C_PIN   13\n    #define TIMER2_A_PIN   10\n    #define TIMER2_B_PIN   9\n    \n#else\n   #define TIMER1_A_PIN   9\n   #define TIMER1_B_PIN   10\n#endif\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n    interruptHandler=callback;\n    noInterrupts();\n    TCCR1A = 0;\n    ICR1 = CLOCK_CYCLES;\n    TCNT1 = 0;   \n    TCCR1B = _BV(WGM13) | _BV(CS10);     // Mode 8, clock select 1\n    TIMSK1 = _BV(TOIE1); // Enable Software interrupt\n    interrupts();\n//diagnostic    pinMode(4,OUTPUT);\n  }\n\n\nvoid DCCTimer::startRailcomTimer(byte brakePin) {\n  (void) brakePin; // Ignored... works on pin 9 only \n  // diagnostic digitalWrite(4,HIGH);   \n\n  /* The Railcom timer is started in such a way that it \n     - First triggers a calculated time after the previous TIMER1 tick. \n       This provides an accurate offset (in High Accuracy mode)\n       for the start of the Railcom cutout. The delayBeforeCutout value \n       reflects that this call is made at the start of the end-packet bit\n       thus the delay is nominally 58+58+Tcs BUT \n       in HA mode, the dcc pin is flipped at the NEXT \n       interrupt so runs 1 tick behind the code. Thus an extra 58uS is needed. \n     - Timer2 Sets the Railcom pin high at first tick and subsequent ticks \n       until its reset to setting pin 9 low at next tick.\n        \n     - Cycles at cutoutDuration so the second tick is the \n        correct distance from the cutout.\n        \n     - Waveform code is responsible for resetting \n       any time between the first and second tick.\n       (there will be 7 DCC timer1 ticks in which to do this.)\n    \n    */\n  const int Tcs=28;    // NMRA spec is 26..32\n  const int cutoutDuration_uS = 450;    // As chosen by most\n  const uint16_t delayBeforeCutout_uS=58+58+58+Tcs;\n  const uint16_t timer1_ticks_per_uS = 8;\n  const uint16_t timer2_uS_per_tick = 2;\n  \n  // Set Timer2 to CTC mode with set on compare match\n  TCCR2A = (1 << WGM21) | (1 << COM2B0) | (1 << COM2B1);\n  // Prescaler of 32\n  TCCR2B =  (1 << CS21) | (1 << CS20); \n  OCR2A = cutoutDuration_uS/timer2_uS_per_tick; // Compare match value for cutout duration in timer2 ticks (2uSec)\n  // Enable Timer2 output on pin 9 (OC2B)\n  DDRB |= (1 << DDB1);\n\n  // timeSlip is the expired time since the DCC timer interrupt that triggered this call.\n  // This allows us to cope with any delays between the interrupt and this code executing.  \n  \n  \n  noInterrupts();\n  // Time already used since DCC interrupt \n  uint16_t timeSlip_uS=TCNT1/timer1_ticks_per_uS;\n  \n  // Calculate the time left before the cutout is to be triggered\n  uint16_t timeToCutout_uS=cutoutDuration_uS+timeSlip_uS-delayBeforeCutout_uS;\n  // Adjust clock2 to trigger on time\n  TCNT2=timeToCutout_uS/timer2_uS_per_tick;\n  interrupts();\n}\n\nvoid DCCTimer::ackRailcomTimer() {\n  // Change Timer2 to CTC mode with RESET pin 9 on next compare match\n  TCCR2A = (1 << WGM21) | (1 << COM2B1);\n}\n\n\n// ISR called by timer interrupt every 58uS\n  ISR(TIMER1_OVF_vect){ interruptHandler(); }\n\n// Alternative pin manipulation via PWM control.\n  bool DCCTimer::isPWMPin(byte pin) {\n       return pin==TIMER1_A_PIN \n           || pin==TIMER1_B_PIN\n       #ifdef TIMER1_C_PIN \n           || pin==TIMER1_C_PIN\n       #endif       \n       ;\n  }\n\n void DCCTimer::setPWM(byte pin, bool high) {\n    if (pin==TIMER1_A_PIN) {\n      TCCR1A |= _BV(COM1A1);\n      OCR1A= high?1024:0;\n    }\n    else if (pin==TIMER1_B_PIN) { \n      TCCR1A |= _BV(COM1B1);\n      OCR1B= high?1024:0;\n    }\n #ifdef TIMER1_C_PIN \n    else if (pin==TIMER1_C_PIN) { \n      TCCR1A |= _BV(COM1C1);\n      OCR1C= high?1024:0;\n    }\n #endif       \n }\n\nvoid DCCTimer::clearPWM() {\n  TCCR1A= 0;\n}\n\n  void DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n    for (byte i=0; i<6; i++) {\n      // take the fist 3 and last 3 of the serial.\n      // the first 5 of 8 are at 0x0E to 0x013\n      // the last  3 of 8 are at 0x15 to 0x017\n      mac[i]=boot_signature_byte_get(0x0E + i + (i>2? 4 : 0));\n    }\n    mac[0] &= 0xFE;\n    mac[0] |= 0x02;\n  }\n\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = minimum_free_memory;\n  interrupts();\n  return retval;\n}\n\nextern char *__brkval;\nextern char *__malloc_heap_start;\n\nint DCCTimer::freeMemory() {\n  char top;\n  return __brkval ? &top - __brkval : &top - __malloc_heap_start;\n}\n\nvoid DCCTimer::reset() {\n  // 250ms chosen to circumwent bootloader bug which\n  // hangs at too short timepout (like 15ms)\n  wdt_enable( WDTO_250MS); // set Arduino watchdog timer for 250ms \n  delay(500);              // wait for it to happen\n\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n  DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);\n}\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {\n#if defined(ARDUINO_AVR_UNO)\n      (void)fbits;\n      (void) pin;\n      // Not worth doin something here as:\n      // If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.\n      // If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.\n      // We are most likely not on pin 3 or 11 as no known motor shield has that as brake.\n#endif\n#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n  // Speed mapping is done like this:\n  // No functions buttons:   000  0   -> low          131Hz\n  // Only F29 pressed        001  1   -> mid          490Hz\n  // F30 with or w/o F29     01x  2-3 -> high        3400Hz\n  // F31 with or w/o F29/30  1xx  4-7 -> supersonic 62500Hz\n  uint8_t abits;\n  uint8_t bbits;\n  if (pin == 9 || pin == 10) { // timer 2 is different\n\n    if (fbits >= 4)\n      abits = B00000011;\n    else\n      abits = B00000001;\n\n    if (fbits >= 4)\n      bbits = B0001;\n    else if (fbits >= 2)\n      bbits = B0010;\n    else if (fbits == 1)\n      bbits = B0100;\n    else //  fbits == 0\n      bbits = B0110;\n\n    TCCR2A = (TCCR2A & B11111100) | abits; // set WGM0 and WGM1\n    TCCR2B = (TCCR2B & B11110000) | bbits; // set WGM2 and 3 bits of prescaler\n    DIAG(F(\"Timer 2 A=%x B=%x\"), TCCR2A, TCCR2B);\n\n  } else { // not timer 9 or 10\n    abits = B01;\n\n    if (fbits >= 4)\n      bbits = B1001;\n    else if (fbits >= 2)\n      bbits = B0010;\n    else if (fbits == 1)\n      bbits = B0011;\n    else\n      bbits = B0100;\n\n    switch (pin) {\n      // case 9 and 10 taken care of above by if()\n    case 6:\n    case 7:\n    case 8:\n      // Timer4\n      TCCR4A = (TCCR4A & B11111100) | abits; // set WGM0 and WGM1\n      TCCR4B = (TCCR4B & B11100000) | bbits; // set WGM2 and WGM3 and divisor\n      //DIAG(F(\"Timer 4 A=%x B=%x\"), TCCR4A, TCCR4B);\n      break;\n    case 46:\n    case 45:\n    case 44:\n      // Timer5\n      TCCR5A = (TCCR5A & B11111100) | abits; // set WGM0 and WGM1\n      TCCR5B = (TCCR5B & B11100000) | bbits; // set WGM2 and WGM3 and divisor\n      //DIAG(F(\"Timer 5 A=%x B=%x\"), TCCR5A, TCCR5B);\n      break;\n    default:\n      break;\n    }\n  }\n#endif\n}\n\n#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n#define NUM_ADC_INPUTS 16\n#else\n#define NUM_ADC_INPUTS 8\n#endif\nuint16_t ADCee::usedpins = 0;\nuint8_t ADCee::highestPin = 0;\nint * ADCee::analogvals = NULL;\nstatic bool ADCusesHighPort = false;\n\n/*\n * Register a new pin to be scanned\n * Returns current reading of pin and\n * stores that as well\n */\nint ADCee::init(uint8_t pin) {\n  uint8_t id = pin - A0;\n  if (id >= NUM_ADC_INPUTS)\n    return -1023;\n  if (id > 7)\n    ADCusesHighPort = true;\n  pinMode(pin, INPUT);\n  int value = analogRead(pin);\n  if (analogvals == NULL)\n    analogvals = (int *)calloc(NUM_ADC_INPUTS, sizeof(int));\n  analogvals[id] = value;\n  usedpins |= (1<<id);\n  if (id > highestPin) highestPin = id;\n  return value;\n}\nint16_t ADCee::ADCmax() {\n  return 1023;\n}\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  uint8_t id = pin - A0;\n  if ((usedpins & (1<<id) ) == 0)\n    return -1023;\n  // we do not need to check (analogvals == NULL)\n  // because usedpins would still be 0 in that case\n  if (!fromISR) noInterrupts();\n  int a = analogvals[id];\n  if (!fromISR) interrupts();\n  return a;\n}\n/*\n * Scan function that is called from interrupt\n */\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\nvoid ADCee::scan() {\n  static byte id = 0;       // id and mask are the same thing but it is faster to\n  static uint16_t mask = 1; // increment and shift instead to calculate mask from id\n  static bool waiting = false;\n\n  if (waiting) {\n    // look if we have a result\n    byte low, high;\n    if (bit_is_set(ADCSRA, ADSC))\n      return; // no result, continue to wait\n    // found value\n    low = ADCL; //must read low before high\n    high = ADCH;\n    bitSet(ADCSRA, ADIF);\n    analogvals[id] = (high << 8) | low;\n    // advance at least one track\n#ifdef DEBUG_ADC\n    if (id == 1) TrackManager::track[1]->setBrake(0);\n#endif\n    waiting = false;\n    id++;\n    mask = mask << 1;\n    if (id > highestPin) {\n      id = 0;\n      mask = 1;\n    }\n  }\n  if (!waiting) {\n    if (usedpins == 0) // otherwise we would loop forever\n      return;\n    // look for a valid track to sample or until we are around\n    while (true) {\n      if (mask  & usedpins) {\n\t// start new ADC aquire on id\n#if defined(ADCSRB) && defined(MUX5)\n\tif (ADCusesHighPort) { // if we ever have started to use high pins)\n\t  if (id > 7)          // if we use a high ADC pin\n\t    bitSet(ADCSRB, MUX5); // set MUX5 bit\n\t  else\n\t    bitClear(ADCSRB, MUX5);\n\t}\n#endif\n\tADMUX=(1<<REFS0)|(id & 0x07); //select AVCC as reference and set MUX\n\tbitSet(ADCSRA,ADSC); // start conversion\n#ifdef DEBUG_ADC\n\tif (id == 1) TrackManager::track[1]->setBrake(1);\n#endif\n\twaiting = true;\n\treturn;\n      }\n      id++;\n      mask = mask << 1;\n      if (id > highestPin) {\n      \tid = 0;\n\t      mask = 1;\n      }\n    }\n  }\n}\n#pragma GCC pop_options\n\nvoid ADCee::begin() {\n  noInterrupts();\n  // ADCSRA = (ADCSRA & 0b11111000) | 0b00000100;   // speed up analogRead sample time\n  // Set up ADC for free running mode\n  ADMUX=(1<<REFS0); //select AVCC as reference. We set MUX later\n  ADCSRA = (1<<ADEN)|(1 << ADPS2); // ADPS2 means divisor 32 and 16Mhz/32=500kHz.\n  //bitSet(ADCSRA, ADSC); //do not start the ADC yet. Done when we have set the MUX\n  interrupts();\n}\n#endif\n"
  },
  {
    "path": "DCCTimerESP.cpp",
    "content": "/*\n *  © 2020-2022 Harald Barth\n *\n *  This file is part of CommandStation-EX\n *  \n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// ATTENTION: this file only compiles on an ESP8266 and ESP32\n// On ESP32 we do not even use the functions but they are here for completeness sake\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n\n#ifdef ARDUINO_ARCH_ESP8266\n\n#include \"DCCTimer.h\"\nINTERRUPT_CALLBACK interruptHandler=0;\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n  interruptHandler=callback;\n  timer1_disable();\n\n  // There seem to be differnt ways to attach interrupt handler\n  //    ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);\n  //    ETS_FRC_TIMER1_NMI_INTR_ATTACH(interruptHandler);\n  // Let us choose the one from the API\n  timer1_attachInterrupt(interruptHandler);\n\n  // not exactly sure of order:\n  timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);\n  timer1_write(CLOCK_CYCLES);\n}\n// We do not support to use PWM to make the Waveform on ESP\nbool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {\n  return false;\n}\nvoid IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {\n}\nvoid IRAM_ATTR DCCTimer::clearPWM() {\n}\n\n// Fake this as it should not be used\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n  mac[0] = 0xFE;\n  mac[1] = 0xBE;\n  mac[2] = 0xEF;\n  mac[3] = 0xC0;\n  mac[4] = 0xFF;\n  mac[5] = 0xEE;\n}\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = minimum_free_memory;\n  interrupts();\n  return retval;\n}\n\nint DCCTimer::freeMemory() {\n  return ESP.getFreeHeap();\n}\n#endif\n\n////////////////////////////////////////////////////////////////////////\n#ifdef ARDUINO_ARCH_ESP32\n\n#if __has_include(\"esp_idf_version.h\")\n#include \"esp_idf_version.h\"\n#endif\n#if ESP_IDF_VERSION_MAJOR == 4\n// all well correct IDF version\n#else\n#error \"DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDF version 4. Arduino ESP32 library 3.0.0 is too new. Downgrade to one of 2.0.9 to 2.0.17\"\n#endif\n\n// protect all the rest of the code from IDF version 5\n#if ESP_IDF_VERSION_MAJOR == 4\n#include \"DIAG.h\"\n#include <driver/adc.h>\n#include <soc/sens_reg.h>\n#include <soc/sens_struct.h>\n#undef ADC_INPUT_MAX_VALUE\n#define ADC_INPUT_MAX_VALUE 4095 // 12 bit ADC\n#define pinToADC1Channel(X) (adc1_channel_t)(((X) > 35) ? (X)-36 : (X)-28)\n\nint IRAM_ATTR local_adc1_get_raw(int channel) {\n  uint16_t adc_value;\n  SENS.sar_meas_start1.sar1_en_pad = (1 << channel); // only one channel is selected\n  while (SENS.sar_slave_addr1.meas_status != 0);\n  SENS.sar_meas_start1.meas1_start_sar = 0;\n  SENS.sar_meas_start1.meas1_start_sar = 1;\n  while (SENS.sar_meas_start1.meas1_done_sar == 0);\n  adc_value = SENS.sar_meas_start1.meas1_data_sar;\n  return adc_value;\n}\n\n#include \"DCCTimer.h\"\nINTERRUPT_CALLBACK interruptHandler=0;\n\n// https://www.visualmicro.com/page/Timer-Interrupts-Explained.aspx\n\nportMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n  // This should not be called on ESP32 so disable it\n  return;\n  interruptHandler = callback;\n  hw_timer_t *timer = NULL;\n  timer = timerBegin(0, 2, true); // prescaler can be 2 to 65536 so choose 2\n  timerAttachInterrupt(timer, interruptHandler, true);\n  timerAlarmWrite(timer, CLOCK_CYCLES / 6, true); // divide by prescaler*3 (Clockbase is 80Mhz and not F_CPU 240Mhz)\n  timerAlarmEnable(timer);\n}\n\n// We do not support to use PWM to make the Waveform on ESP\nbool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {\n  return false;\n}\nvoid IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {\n}\nvoid IRAM_ATTR DCCTimer::clearPWM() {\n}\n\n// Fake this as it should not be used\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n  mac[0] = 0xFE;\n  mac[1] = 0xBE;\n  mac[2] = 0xEF;\n  mac[3] = 0xC0;\n  mac[4] = 0xFF;\n  mac[5] = 0xEE;\n}\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = minimum_free_memory;\n  interrupts();\n  return retval;\n}\n\nint DCCTimer::freeMemory() {\n  return ESP.getFreeHeap();\n}\n\nvoid DCCTimer::reset() {\n   ESP.restart();\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n  if (f >= 16)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);\n/*\n  else if (f == 7) // not used on ESP32\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);\n*/\n  else if (f >= 4)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);\n  else if (f >= 3)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);\n  else if (f >= 2)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);\n  else if (f == 1)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);\n  else\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);\n}\n\n#include \"esp32-hal.h\"\n#include \"soc/soc_caps.h\"\n\n#ifdef SOC_LEDC_SUPPORT_HS_MODE\n#define LEDC_CHANNELS           (SOC_LEDC_CHANNEL_NUM<<1)\n#else\n#define LEDC_CHANNELS           (SOC_LEDC_CHANNEL_NUM)\n#endif\n\nstatic int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };\nstatic int cnt_channel = LEDC_CHANNELS;\n\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency) {\n  if (pin < SOC_GPIO_PIN_COUNT) {\n    if (pin_to_channel[pin] != 0) {\n      ledcSetup(pin_to_channel[pin], frequency, 8);\n    }\n  }\n}\n\nvoid DCCTimer::DCCEXledcDetachPin(uint8_t pin) {\n#ifdef DIAG_IO\n  DIAG(F(\"Clear pin %d from ledc channel\"), pin);\n#endif\n  pin_to_channel[pin] = 0;\n  pinMatrixOutDetach(pin, false, false);\n}\n\nstatic byte LEDCToMux[] = {\n  LEDC_HS_SIG_OUT0_IDX,\n  LEDC_HS_SIG_OUT1_IDX,\n  LEDC_HS_SIG_OUT2_IDX,\n  LEDC_HS_SIG_OUT3_IDX,\n  LEDC_HS_SIG_OUT4_IDX,\n  LEDC_HS_SIG_OUT5_IDX,\n  LEDC_HS_SIG_OUT6_IDX,\n  LEDC_HS_SIG_OUT7_IDX,\n  LEDC_LS_SIG_OUT0_IDX,\n  LEDC_LS_SIG_OUT1_IDX,\n  LEDC_LS_SIG_OUT2_IDX,\n  LEDC_LS_SIG_OUT3_IDX,\n  LEDC_LS_SIG_OUT4_IDX,\n  LEDC_LS_SIG_OUT5_IDX,\n  LEDC_LS_SIG_OUT6_IDX,\n  LEDC_LS_SIG_OUT7_IDX,\n};\n\nvoid DCCTimer::DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted) {\n  DIAG(F(\"Attaching pin %d to channel %d %c\"), pin, channel, inverted ? 'I' : ' ');\n  ledcAttachPin(pin, channel);\n  if (inverted) // we attach again but with inversion\n    gpio_matrix_out(pin, LEDCToMux[channel], inverted, 0);\n}\n\nvoid DCCTimer::DCCEXanalogCopyChannel(int8_t frompin, int8_t topin) {\n  // arguments are signed depending on inversion of pins\n  DIAG(F(\"Pin %d copied to %d\"), frompin, topin);\n  bool inverted = false;\n  if (frompin<0)\n    frompin = -frompin;\n  if (topin<0) {\n    inverted = true;\n    topin = -topin;\n  }\n  int channel = pin_to_channel[frompin]; // after abs(frompin)\n  pin_to_channel[topin] = channel;\n  DCCTimer::DCCEXledcAttachPin(topin, channel, inverted);\n}\n\nvoid DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {\n  // This allocates channels 15, 13, 11, ....\n  // so each channel gets its own timer.\n  if (pin < SOC_GPIO_PIN_COUNT) {\n    if (pin_to_channel[pin] == 0) {\n      int search_channel;\n      int n;\n      if (!cnt_channel) {\n          log_e(\"No more PWM channels available! All %u already used\", LEDC_CHANNELS);\n          return;\n      }\n      // search for free channels top down\n      for (search_channel=LEDC_CHANNELS-1; search_channel >=cnt_channel; search_channel -= 2) {\n\tbool chanused = false;\n\tfor (n=0; n < SOC_GPIO_PIN_COUNT; n++) {\n\t  if (pin_to_channel[n] == search_channel) { // current search_channel used\n\t    chanused = true;\n\t    break;\n\t  }\n\t}\n\tif (chanused)\n\t  continue;\n\tif (n == SOC_GPIO_PIN_COUNT) // current search_channel unused\n\t  break;\n      }\n      if (search_channel >= cnt_channel) {\n\tpin_to_channel[pin] = search_channel;\n\tDIAG(F(\"Pin %d assigned to search channel %d\"), pin, search_channel);\n      } else {\n\tpin_to_channel[pin] = --cnt_channel; // This sets 15, 13, ...\n\tDIAG(F(\"Pin %d assigned to new channel %d\"), pin, cnt_channel);\n\t--cnt_channel;                       // Now we are at 14, 12, ...\n      }\n      ledcSetup(pin_to_channel[pin], 1000, 8);\n      DCCEXledcAttachPin(pin, pin_to_channel[pin], invert);\n    } else {\n      // This else is only here so we can enable diag\n      // Pin should be already attached to channel\n      // DIAG(F(\"Pin %d assigned to old channel %d\"), pin, pin_to_channel[pin]);\n    }\n    ledcWrite(pin_to_channel[pin], value);\n  }\n}\n\nvoid DCCTimer::DCCEXInrushControlOn(uint8_t pin, int duty, bool inverted) {\n  // this uses hardcoded channel 0\n  ledcSetup(0, 62500, 8);\n  DCCEXledcAttachPin(pin, 0, inverted);\n  ledcWrite(0, duty);\n}\n\nint ADCee::init(uint8_t pin) {\n  pinMode(pin, ANALOG);\n  adc1_config_width(ADC_WIDTH_BIT_12);\n// Espressif deprecated ADC_ATTEN_DB_11 somewhere between 2.0.9 and 2.0.17\n#ifdef ADC_ATTEN_11db\n  adc1_config_channel_atten(pinToADC1Channel(pin),ADC_ATTEN_11db);\n#else\n  adc1_config_channel_atten(pinToADC1Channel(pin),ADC_ATTEN_DB_11);\n#endif\n  return adc1_get_raw(pinToADC1Channel(pin));\n}\nint16_t ADCee::ADCmax() {\n  return 4095;\n}\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  return local_adc1_get_raw(pinToADC1Channel(pin));\n}\n/*\n * Scan function that is called from interrupt\n */\nvoid ADCee::scan() {\n}\n\nvoid ADCee::begin() {\n}\n#endif //IDF v4\n#endif //ESP32\n"
  },
  {
    "path": "DCCTimerMEGAAVR.cpp",
    "content": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n *  © 2021 David Cutting\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n/* This timer class is used to manage the single timer required to handle the DCC waveform.\n *  All timer access comes through this class so that it can be compiled for \n *  various hardware CPU types. \n *  \n *  DCCEX works on a single timer interrupt at a regular 58uS interval.\n *  The DCCWaveform class generates the signals to the motor shield  \n *  based on this timer. \n *  \n *  If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,\n *  (see isPWMPin() function. )\n *  then this allows us to use a hardware driven pin switching arrangement which is\n *  achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on \n *  the required pin state. (see setPWM())  \n *  This is more accurate than the software interrupt but at the expense of \n *  limiting the choice of available pins. \n *  Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM... \n *  Other shields may be jumpered to PWM pins or run directly using the software interrupt.\n *  \n *  Because the PWM-based waveform is effectively set half a cycle after the software version,\n *  it is not acceptable to drive the two tracks on different methiods or it would cause\n *  problems for <1 JOIN> etc.\n *  \n */\n\n// ATTENTION: this file only compiles on a UnoWifiRev3 or NanoEvery\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n#ifdef ARDUINO_ARCH_MEGAAVR\n\n#include \"DCCTimer.h\"\n\nINTERRUPT_CALLBACK interruptHandler=0;\nextern char *__brkval;\nextern char *__malloc_heap_start;\n\n  \n  void DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n    interruptHandler=callback;\n    noInterrupts(); \n    ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011;  // speed up analogRead sample time   \n    TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled\n    TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; //   8 MHz ~ 0.125 us      \n    TCB0.CCMP =  CLOCK_CYCLES -1;  // 1 tick less for timer reset\n    TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag\n    TCB0.INTCTRL = TCB_CAPT_bm;  // Enable the interrupt\n    TCB0.CNT = 0;\n    TCB0.CTRLA |= TCB_ENABLE_bm;  // start\n    interrupts();\n  }\n\n  // ISR called by timer interrupt every 58uS\n  ISR(TCB0_INT_vect){\n    TCB0.INTFLAGS = TCB_CAPT_bm; // Clear interrupt request flag\n    interruptHandler();\n  }\n\nvoid DCCTimer::startRailcomTimer(byte brakePin) {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n  (void) brakePin; \n}\n\nvoid DCCTimer::ackRailcomTimer() {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n}\n\n  bool DCCTimer::isPWMPin(byte pin) {\n       (void) pin; \n       return false;  // TODO what are the relevant pins? \n  }\n\n void DCCTimer::setPWM(byte pin, bool high) {\n    (void) pin;\n    (void) high;\n    // TODO what are the relevant pins?\n }\n\nvoid DCCTimer::clearPWM() {\n    // Do nothing unless we implent HA\n}\n\n  void   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n    memcpy(mac,(void *) &SIGROW.SERNUM0,6);  // serial number\n    mac[0] &= 0xFE;\n    mac[0] |= 0x02;\n  }\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = minimum_free_memory;\n  interrupts();\n  return retval;\n}\n\nextern char *__brkval;\nextern char *__malloc_heap_start;\n\nint DCCTimer::freeMemory() {\n  char top;\n  return __brkval ? &top - __brkval : &top - __malloc_heap_start;\n}\n\nvoid DCCTimer::reset() {\n  CPU_CCP=0xD8;\n  WDT.CTRLA=0x4;\n  while(true){}\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n}\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {\n}\n\nint16_t ADCee::ADCmax() {\n  return 4095;\n}\n\nint ADCee::init(uint8_t pin) {\n  return analogRead(pin);\n}\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  int current;\n  if (!fromISR) noInterrupts();\n  current = analogRead(pin);\n  if (!fromISR) interrupts();\n  return current;\n}\n/*\n * Scan function that is called from interrupt\n */\nvoid ADCee::scan() {\n}\n\nvoid ADCee::begin() {\n  noInterrupts();\n  interrupts();\n}\n#endif\n"
  },
  {
    "path": "DCCTimerSAMD.cpp",
    "content": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021-2022 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n *  © 2021 David Cutting\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// ATTENTION: this file only compiles on a SAMD21 based board\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n#ifdef ARDUINO_ARCH_SAMD\n\n#include \"DCCTimer.h\"\n#include <wiring_private.h>\n\nINTERRUPT_CALLBACK interruptHandler=0;\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n  interruptHandler=callback;\n  noInterrupts();\n  // Timer setup - setup clock sources first\n  REG_GCLK_GENDIV =   GCLK_GENDIV_DIV(1) |            // Divide 48MHz by 1\n                      GCLK_GENDIV_ID(4);              // Apply to GCLK4\n  while (GCLK->STATUS.bit.SYNCBUSY);                  // Wait for synchronization\n                \n  REG_GCLK_GENCTRL =  GCLK_GENCTRL_GENEN |            // Enable GCLK\n                      GCLK_GENCTRL_SRC_DFLL48M |      // Set the 48MHz clock source\n                      GCLK_GENCTRL_ID(4);             // Select GCLK4\n  while (GCLK->STATUS.bit.SYNCBUSY);                  // Wait for synchronization\n            \n  REG_GCLK_CLKCTRL =  GCLK_CLKCTRL_CLKEN |            // Enable generic clock\n                      4 << GCLK_CLKCTRL_GEN_Pos |     // Apply to GCLK4\n                      GCLK_CLKCTRL_ID_TCC0_TCC1;      // Feed GCLK to TCC0/1\n  while (GCLK->STATUS.bit.SYNCBUSY);\n\n  // Assume we're using TCC0... as we're bit-bashing the DCC waveform output pins anyway\n  // for \"normal accuracy\" DCC waveform generation. For high accuracy we're going to need\n  // to a good deal more. The TCC waveform output pins are mux'd on the SAMD, and output\n  // pins for each TCC are only available on certain pins\n  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;     // Select NPWM as waveform\n  while (TCC0->SYNCBUSY.bit.WAVE);            // Wait for sync\n\n  // Set the frequency\n  TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER(TCC_CTRLA_PRESCALER_DIV1_Val);\n  TCC0->PER.reg = CLOCK_CYCLES * 2;\n  while (TCC0->SYNCBUSY.bit.PER);\n\n  // Start the timer\n  TCC0->CTRLA.bit.ENABLE = 1;\n  while (TCC0->SYNCBUSY.bit.ENABLE);\n\n  // Set the interrupt condition, priority and enable it in the NVIC\n  TCC0->INTENSET.reg = TCC_INTENSET_OVF;      // Only interrupt on overflow\n  int USBprio = NVIC_GetPriority((IRQn_Type) USB_IRQn);  // Fetch the USB priority\n  NVIC_SetPriority((IRQn_Type)TCC0_IRQn, USBprio);  // Match the USB priority\n//  NVIC_SetPriority((IRQn_Type)TCC0_IRQn, 0);  // Make this highest priority\n  NVIC_EnableIRQ((IRQn_Type)TCC0_IRQn);       // Enable the interrupt\n  interrupts();\n}\n\nvoid DCCTimer::startRailcomTimer(byte brakePin) {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n  (void) brakePin; \n}\n\nvoid DCCTimer::ackRailcomTimer() {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n}\n\n// Timer IRQ handlers replace the dummy handlers (in cortex_handlers)\n// copied from rf24 branch\nvoid TCC0_Handler() {\n    if(TCC0->INTFLAG.bit.OVF) {\n        TCC0->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag\n        interruptHandler();\n    }\n}\n\nvoid TCC1_Handler() {\n    if(TCC1->INTFLAG.bit.OVF) {\n        TCC1->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag\n        interruptHandler();\n    }\n}\n\nvoid TCC2_Handler() {\n    if(TCC2->INTFLAG.bit.OVF) {\n        TCC2->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag\n        interruptHandler();\n    }\n}\n\n\nbool DCCTimer::isPWMPin(byte pin) {\n  //TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM,\n  //      there's no support yet for High Accuracy, so for now return false\n  //  return digitalPinHasPWM(pin);\n  return false;\n}\n\nvoid DCCTimer::setPWM(byte pin, bool high) {\n    // TODO: High Accuracy mode is not supported as yet, and may never need to be\n    (void) pin;\n    (void) high;\n}\n\nvoid DCCTimer::clearPWM() {\n  return;\n}\n\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n  volatile uint32_t *serno1 = (volatile uint32_t *)0x0080A00C;\n  volatile uint32_t *serno2 = (volatile uint32_t *)0x0080A040;\n//  volatile uint32_t *serno3 = (volatile uint32_t *)0x0080A044;\n//  volatile uint32_t *serno4 = (volatile uint32_t *)0x0080A048;\n\n  volatile uint32_t m1 = *serno1;\n  volatile uint32_t m2 = *serno2;\n  mac[0] = m1 >> 8;\n  mac[1] = m1 >> 0;\n  mac[2] = m2 >> 24;\n  mac[3] = m2 >> 16;\n  mac[4] = m2 >> 8;\n  mac[5] = m2 >> 0;\n}\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = freeMemory();\n  interrupts();\n  return retval;\n}\n\nextern \"C\" char* sbrk(int incr);\n\nint DCCTimer::freeMemory() {\n  char top;\n  return (int)(&top - reinterpret_cast<char *>(sbrk(0)));\n}\n\nvoid DCCTimer::reset() {\n   __disable_irq();\n    NVIC_SystemReset();\n    while(true) {};\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n}\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {\n}\n\n#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS\n\nuint16_t ADCee::usedpins = 0;\nint * ADCee::analogvals = NULL;\n\nint ADCee::init(uint8_t pin) {\n  uint8_t id = pin - A0;\n  int value = 0;\n\n  if (id > NUM_ADC_INPUTS)\n    return -1023;\n\n  // Permanently configure SAMD IO MUX for that pin\n  pinPeripheral(pin, PIO_ANALOG);\n  ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[pin].ulADCChannelNumber; // Selection for the positive ADC input\n\n  // Start conversion\n  ADC->SWTRIG.bit.START = 1;\n\n  // Wait for the conversion to be ready\n  while (ADC->INTFLAG.bit.RESRDY == 0);   // Waiting for conversion to complete\n\n  // Read the value\n  value = ADC->RESULT.reg;\n\n  if (analogvals == NULL)\n    analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));\n  analogvals[id] = value;\n  usedpins |= (1<<id);\n\n  return value;\n}\n\nint16_t ADCee::ADCmax() {\n  return 4095;\n}\n\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  uint8_t id = pin - A0;\n  if ((usedpins & (1<<id) ) == 0)\n    return -1023;\n  // we do not need to check (analogvals == NULL)\n  // because usedpins would still be 0 in that case\n  return analogvals[id];\n}\n/*\n * Scan function that is called from interrupt\n */\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\nvoid ADCee::scan() {\n  static uint8_t id = 0;        // id and mask are the same thing but it is faster to \n  static uint16_t mask = 1;  // increment and shift instead to calculate mask from id\n  static bool waiting = false;\n\n  if (waiting) {\n    // look if we have a result\n    if (ADC->INTFLAG.bit.RESRDY == 0)\n      return; // no result, continue to wait\n    // found value\n    analogvals[id] = ADC->RESULT.reg;\n    // advance at least one track\n    // for scope debug TrackManager::track[1]->setBrake(0);\n    waiting = false;\n    id++;\n    mask = mask << 1;\n    if (id == NUM_ADC_INPUTS+1) {\n      id = 0;\n      mask = 1;\n    }\n  }\n  if (!waiting) {\n    if (usedpins == 0) // otherwise we would loop forever\n      return;\n    // look for a valid track to sample or until we are around\n    while (true) {\n      if (mask  & usedpins) {\n    \t  // start new ADC aquire on id\n        ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[id + A0].ulADCChannelNumber; // Selection for the positive ADC input\n          // Start conversion\n        ADC->SWTRIG.bit.START = 1;\n\t      // for scope debug TrackManager::track[1]->setBrake(1);\n\t      waiting = true;\n\t      return;\n      }\n      id++;\n      mask = mask << 1;\n      if (id == NUM_ADC_INPUTS+1) {\n\t      id = 0;\n\t      mask = 1;\n      }\n    }\n  }\n}\n#pragma GCC pop_options\n\nvoid ADCee::begin() {\n  noInterrupts();\n  // Set up ADC to do faster reads... default for Arduino Zero platform configs is 436uS,\n  // and we need sub-58uS. This code sets it to a read speed of around 5-6uS, and enables\n  // 12-bit mode\n  // Reconfigure ADC\n  ADC->CTRLA.bit.ENABLE = 0;                      // disable ADC\n  while( ADC->STATUS.bit.SYNCBUSY == 1 );         // wait for synchronization\n\n  ADC->CTRLB.reg &= 0b1111100011001111;           // mask PRESCALER and RESSEL bits\n  ADC->CTRLB.reg |= ADC_CTRLB_PRESCALER_DIV64 |   // divide Clock by 16\n                    ADC_CTRLB_RESSEL_12BIT;       // Result 12 bits, 10 bits possible\n  ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 |    // take 1 sample at a time\n                     ADC_AVGCTRL_ADJRES(0x00ul);  // adjusting result by 0\n  ADC->SAMPCTRL.reg = 0x00ul;                     // sampling Time Length = 0\n  ADC->CTRLA.bit.ENABLE = 1;                      // enable ADC\n  while( ADC->STATUS.bit.SYNCBUSY == 1 );         // wait for synchronization\n  interrupts();\n}\n#endif\n"
  },
  {
    "path": "DCCTimerSTM32.cpp",
    "content": "/*\n *  © 2023 Neil McKechnie\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021, 2023 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n *  © 2021 David Cutting\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// ATTENTION: this file only compiles on a STM32 based boards\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n#ifdef ARDUINO_ARCH_STM32\n\n#include \"DCCTimer.h\"\n#ifdef DEBUG_ADC\n#include \"TrackManager.h\"\n#endif\n#include \"DIAG.h\"\n#include <wiring_private.h>\n\n#if defined(ARDUINO_NUCLEO_F401RE)\n// Nucleo-64 boards don't have additional serial ports defined by default\n// Serial1 is available on the F401RE, but not hugely convenient.\n// Rx pin on PB7 is useful, but all the Tx pins map to Arduino digital pins, specifically:\n//   PA9 == D8\n//   PB6 == D10\n// of which D8 is needed by the standard and EX8874 motor shields. D10 would be used if a second\n// EX8874 is stacked. So only disable this if using a second motor shield.\nHardwareSerial Serial1(PB7, PB6);  // Rx=PB7, Tx=PB6 -- CN7 pin 17 and CN10 pin 17\n// Serial2 is defined to use USART2 by default, but is in fact used as the diag console\n// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.\n// Let's define Serial6 as an additional serial port (the only other option for the F401RE)\nHardwareSerial Serial6(PA12, PA11);  // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F401RE\n#elif defined(ARDUINO_NUCLEO_F411RE)\n// Nucleo-64 boards don't have additional serial ports defined by default\nHardwareSerial Serial1(PB7, PA15);  // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F411RE\n// Serial2 is defined to use USART2 by default, but is in fact used as the diag console\n// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.\n// Let's define Serial6 as an additional serial port (the only other option for the Nucleo-64s)\nHardwareSerial Serial6(PA12, PA11);  // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F411RE\n#elif defined(ARDUINO_NUCLEO_F446RE)\n// Nucleo-64 boards don't have additional serial ports defined by default\n// On the F446RE, Serial1 isn't really useable as it's Rx/Tx pair sit on already used D2/D10 pins\n// HardwareSerial Serial1(PA10, PB6);  // Rx=PA10 (D2), Tx=PB6 (D10) -- CN10 pins 17 and 9 - F446RE \n// Serial2 is defined to use USART2 by default, but is in fact used as the diag console\n// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.\n// On the F446RE, Serial3 and Serial5 are easy to use:\nHardwareSerial Serial3(PC11, PC10);  // Rx=PC11, Tx=PC10 -- USART3 - F446RE\nHardwareSerial Serial5(PD2, PC12);  // Rx=PD2, Tx=PC12 -- UART5 - F446RE\n// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins\n#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) || \\\n      defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)\n// Nucleo-144 boards don't have Serial1 defined by default\nHardwareSerial Serial6(PG9, PG14);  // Rx=PG9, Tx=PG14 -- USART6\nHardwareSerial Serial2(PD6, PD5);  // Rx=PD6, Tx=PD5 -- UART2\n#if !defined(ARDUINO_NUCLEO_F412ZG)  // F412ZG does not have UART5\n  HardwareSerial Serial5(PD2, PC12);  // Rx=PD2, Tx=PC12 -- UART5\n#endif  \n// Serial3 is defined to use USART3 by default, but is in fact used as the diag console\n// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.\n#else\n#error STM32 board selected is not yet explicitly supported - so Serial1 peripheral is not defined\n#endif\n\n///////////////////////////////////////////////////////////////////////////////////////////////\n// Experimental code for High Accuracy (HA) DCC Signal mode\n// Warning - use of TIM2 and TIM3 can affect the use of analogWrite() function on certain pins,\n// which is used by the DC motor types.\n///////////////////////////////////////////////////////////////////////////////////////////////\n\n// INTERRUPT_CALLBACK interruptHandler=0;\n// // Let's use STM32's timer #2 which supports hardware pulse generation on pin D13.\n// // Also, timer #3 will do hardware pulses on pin D12. This gives\n// // accurate timing, independent of the latency of interrupt handling.\n// // We only need to interrupt on one of these (TIM2), the other will just generate\n// // pulses.\n// HardwareTimer timer(TIM2);\n// HardwareTimer timerAux(TIM3);\n// static bool tim2ModeHA = false;\n// static bool tim3ModeHA = false;\n\n// // Timer IRQ handler\n// void Timer_Handler() {\n//   interruptHandler();\n// }\n\n// void DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n//   interruptHandler=callback;\n//   noInterrupts();\n\n//   // adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);\n//   timer.pause();\n//   timerAux.pause();\n//   timer.setPrescaleFactor(1);\n//   timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);\n//   timer.attachInterrupt(Timer_Handler);\n//   timer.refresh();\n//   timerAux.setPrescaleFactor(1);\n//   timerAux.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);\n//   timerAux.refresh();\n  \n//   timer.resume();\n//   timerAux.resume();\n\n//   interrupts();\n// }\n\n// bool DCCTimer::isPWMPin(byte pin) {\n//   // Timer 2 Channel 1 controls pin D13, and Timer3 Channel 1 controls D12.\n//   //  Enable the appropriate timer channel.\n//   switch (pin) {\n//     case 12:\n//       return true;\n//     case 13:\n//       return true;\n//     default:\n//       return false;\n//   }\n// }\n\n// void DCCTimer::setPWM(byte pin, bool high) {\n//   // Set the timer so that, at the next counter overflow, the requested\n//   // pin state is activated automatically before the interrupt code runs.\n//   // TIM2 is timer, TIM3 is timerAux.\n//   switch (pin) {\n//     case 12:\n//       if (!tim3ModeHA) {\n//         timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D12);\n//         tim3ModeHA = true;\n//       }\n//       if (high) \n//         TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0;\n//       else\n//         TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1;\n//       break;\n//     case 13:\n//       if (!tim2ModeHA) {\n//         timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D13);\n//         tim2ModeHA = true;\n//       }\n//       if (high) \n//         TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0;\n//       else\n//         TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1;\n//       break;\n//   }   \n// }\n\n// void DCCTimer::clearPWM() {\n//   timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);\n//   tim2ModeHA = false;\n//   timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);  \n//   tim3ModeHA = false;\n// }\n///////////////////////////////////////////////////////////////////////////////////////////////\n\nINTERRUPT_CALLBACK interruptHandler=0;\n\n// On STM32F4xx models that have them, Timers 6 and 7 have no PWM output capability,\n// so are good choices for general timer duties - they are used for tone and servo\n// in stm32duino so we shall usurp those as DCC-EX doesn't use tone or servo libs.\n// NB: the F401, F410 and F411 do **not** have Timer 6 or 7, so we use Timer 11\n#ifndef DCC_EX_TIMER\n#if defined(TIM6)\n#define DCC_EX_TIMER TIM6\n#elif defined(TIM7)\n#define DCC_EX_TIMER TIM7\n#elif defined(TIM11)\n#define DCC_EX_TIMER TIM11\n#else\n#warning This STM32F4XX variant does not have Timers 6,7 or 11!!\n#endif\n#endif // ifndef DCC_EX_TIMER\n\nHardwareTimer dcctimer(DCC_EX_TIMER);\nvoid DCCTimer_Handler() __attribute__((interrupt));\n\n// Timer IRQ handler\nvoid DCCTimer_Handler() {\n  interruptHandler();\n}\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n  interruptHandler=callback;\n  noInterrupts();\n\n  dcctimer.pause();\n  dcctimer.setPrescaleFactor(1);\n//  timer.setOverflow(CLOCK_CYCLES * 2);\n  dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);\n  // dcctimer.attachInterrupt(Timer11_Handler);\n  dcctimer.attachInterrupt(DCCTimer_Handler);\n  dcctimer.setInterruptPriority(1, 0); // Set second highest preemptive priority!\n  dcctimer.refresh();\n  dcctimer.resume();\n\n  interrupts();\n}\n\nvoid DCCTimer::startRailcomTimer(byte brakePin) {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n  (void) brakePin; \n}\n\nvoid DCCTimer::ackRailcomTimer() {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n}\n\nbool DCCTimer::isPWMPin(byte pin) {\n  //TODO: STM32 whilst this call to digitalPinHasPWM will reveal which pins can do PWM,\n  //      there's no support yet for High Accuracy, so for now return false\n  //  return digitalPinHasPWM(pin);\n  (void) pin;\n  return false;\n}\n\nvoid DCCTimer::setPWM(byte pin, bool high) {\n    // TODO: High Accuracy mode is not supported as yet, and may never need to be\n    (void) pin;\n    (void) high;\n}\n\nvoid DCCTimer::clearPWM() {\n  return;\n}\n\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n  volatile uint32_t *serno1 = (volatile uint32_t *)UID_BASE;\n  volatile uint32_t *serno2 = (volatile uint32_t *)UID_BASE+4;\n  // volatile uint32_t *serno3 = (volatile uint32_t *)UID_BASE+8;\n\n  volatile uint32_t m1 = *serno1;\n  volatile uint32_t m2 = *serno2;\n  mac[0] = m1 >> 8;\n  mac[1] = m1 >> 0;\n  mac[2] = m2 >> 24;\n  mac[3] = m2 >> 16;\n  mac[4] = m2 >> 8;\n  mac[5] = m2 >> 0;\n}\n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = freeMemory();\n  interrupts();\n  return retval;\n}\n\nextern \"C\" char* sbrk(int incr);\n\nint DCCTimer::freeMemory() {\n  char top;\n  return (int)(&top - reinterpret_cast<char *>(sbrk(0)));\n}\n\nvoid DCCTimer::reset() {\n   __disable_irq();\n    NVIC_SystemReset();\n    while(true) {};\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n  if (f >= 16)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);\n  else if (f == 7)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);\n  else if (f >= 4)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);\n  else if (f >= 3)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);\n  else if (f >= 2)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);\n  else if (f == 1)\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);\n  else\n    DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);\n}\n\n// TODO: rationalise the size of these... could really use sparse arrays etc.\nstatic HardwareTimer * pin_timer[100] = {0};\nstatic uint32_t channel_frequency[100] = {0};\nstatic uint32_t pin_channel[100] = {0};\n\n// Using the HardwareTimer library API included in stm32duino core to handle PWM duties\n// TODO: in order to use the HA code above which Neil kindly wrote, we may have to do something more\n// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones\n// currently used for HA so they don't interfere with one another. For now we'll just make PWM\n// work well... then work backwards to integrate with HA mode if we can.\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency)\n{\n  if (pin_timer[pin] == NULL) {\n    // Automatically retrieve TIM instance and channel associated to pin\n    // This is used to be compatible with all STM32 series automatically.\n    TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM);\n    if (Instance == NULL) {\n      // We shouldn't get here (famous last words) as it ought to have been caught by brakeCanPWM()!\n      DIAG(F(\"DCCEXanalogWriteFrequency::Pin %d has no PWM function!\"), pin);\n      return;\n    }\n    pin_channel[pin] = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(pin), PinMap_PWM));\n\n    // Instantiate HardwareTimer object. Thanks to 'new' instantiation,\n    // HardwareTimer is not destructed when setup function is finished.\n    pin_timer[pin] = new HardwareTimer(Instance);\n    // Configure and start PWM\n    // MyTim->setPWM(channel, pin, 5, 10, NULL, NULL); // No callback required, we can simplify the function call\n    if (pin_timer[pin] != NULL)\n    {\n      pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle\n      DIAG(F(\"DCCEXanalogWriteFrequency::Pin %d on Timer Channel %d, frequency %d\"), pin, pin_channel[pin], frequency);\n    }\n    else\n      DIAG(F(\"DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!\"));\n  }\n  else\n  {\n    // Frequency change request\n    if (frequency != channel_frequency[pin])\n    {\n      pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!\n      pin_timer[pin]->setOverflow(frequency, HERTZ_FORMAT); // Just change the frequency if it's already running!\n      DIAG(F(\"DCCEXanalogWriteFrequency::setting frequency to %d\"), frequency);\n    }\n  }\n  channel_frequency[pin] = frequency;\n  return;\n}\n\nvoid DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {\n    if (invert)\n      value = 255-value;\n    // Calculate percentage duty cycle from value given\n    uint32_t duty_cycle = (value * 100 / 256) + 1;\n    if (pin_timer[pin] != NULL) {\n      // if (duty_cycle == 100)\n      // {\n      //   pin_timer[pin]->pauseChannel(pin_channel[pin]);\n      //   DIAG(F(\"DCCEXanalogWrite::Pausing timer channel on pin %d\"), pin);\n      // }\n      // else\n      // {\n        pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!\n        // pin_timer[pin]->resumeChannel(pin_channel[pin]);\n        pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle\n        DIAG(F(\"DCCEXanalogWrite::Pin %d, value %d, duty cycle %d\"), pin, value, duty_cycle);\n      // }\n    }\n    else\n      DIAG(F(\"DCCEXanalogWrite::Pin %d is not configured for PWM!\"), pin);\n}\n\n\n// Now we can handle more ADCs, maybe this works!\n#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS\n\nuint32_t ADCee::usedpins = 0;         // Max of 32 ADC input channels!\nuint8_t ADCee::highestPin = 0;        // Highest pin to scan\nint * ADCee::analogvals = NULL;       // Array of analog values last captured\nuint32_t * ADCee::analogchans = NULL;        // Array of channel numbers to be scanned\n// bool adc1configured = false;\nADC_TypeDef * * ADCee::adcchans = NULL;      // Array to capture which ADC is each input channel on\n\nint16_t ADCee::ADCmax()\n{\n    return 4095;\n}\n\nint ADCee::init(uint8_t pin) {\n\n  int value = 0;\n  PinName stmpin = analogInputToPinName(pin);\n  if (stmpin == NC) // do not continue if this is not an analog pin at all\n    return -1024;   // some silly value as error\n\n  uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32)\n  uint32_t adcchan =  STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC input channel\n  ADC_TypeDef *adc = (ADC_TypeDef *)pinmap_find_peripheral(stmpin, PinMap_ADC); // find which ADC this pin is on ADC1/2/3 etc.\n  int adcnum = 1;\n  // All variants have ADC1\n  if (adc == ADC1)\n    DIAG(F(\"ADCee::init(): found pin %d on ADC1\"), pin);\n  // Checking for ADC2 and ADC3 being defined helps cater for more variants\n#if defined(ADC2)\n  else if (adc == ADC2)\n  {\n    DIAG(F(\"ADCee::init(): found pin %d on ADC2\"), pin);\n    adcnum = 2;\n  }\n#endif\n#if defined(ADC3)\n  else if (adc == ADC3)\n  {\n    DIAG(F(\"ADCee::init(): found pin %d on ADC3\"), pin);\n    adcnum = 3;\n  }\n#endif\n  else DIAG(F(\"ADCee::init(): found pin %d on unknown ADC!\"), pin);\n\n    // Port config - find which port we're on and power it up\n    GPIO_TypeDef *gpioBase;\n\n    switch (stmgpio)\n    {\n    case 0x00:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA\n        gpioBase = GPIOA;\n        break;\n    case 0x01:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; //Power up PORTB\n        gpioBase = GPIOB;\n        break;\n    case 0x02:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC\n        gpioBase = GPIOC;\n        break;\n    case 0x03:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; //Power up PORTD\n        gpioBase = GPIOD;\n        break;\n    case 0x04:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; //Power up PORTE\n        gpioBase = GPIOE;\n        break;\n#if defined(GPIOF)\n    case 0x05:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; //Power up PORTF\n        gpioBase = GPIOF;\n        break;\n#endif\n#if defined(GPIOG)\n    case 0x06:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN; //Power up PORTG\n        gpioBase = GPIOG;\n        break;\n#endif\n#if defined(GPIOH)\n    case 0x07:\n        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN; //Power up PORTH\n        gpioBase = GPIOH;\n        break;\n#endif\n    default:\n      return -1023; // some silly value as error\n  }\n\n  // Set pin mux mode to analog input, the 32 bit port mode register has 2 bits per pin\n  gpioBase->MODER |= (0b011 << (STM_PIN(stmpin) << 1)); // Set pin mux to analog mode (binary 11)\n\n  // Set the sampling rate for that analog input\n  // This is F411x specific! Different on for example F334\n  // STM32F11xC/E Reference manual\n  // 11.12.4 ADC sample time register 1 (ADC_SMPR1) (channels 10 to 18)\n  // 11.12.5 ADC sample time register 2 (ADC_SMPR2) (channels 0 to 9)\n  if (adcchan > 18)\n    return -1022; // silly value as error\n  if (adcchan < 10)\n    adc->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles\n  else\n    adc->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles\n\n  // Read the inital ADC value for this analog input\n  adc->SQR3 = adcchan;           // 1st conversion in regular sequence\n  adc->CR2 |= ADC_CR2_SWSTART;   //(1 << 30);                     // Start 1st conversion SWSTART\n  while(!(adc->SR & (1 << 1)));  // Wait until conversion is complete\n  value = adc->DR;               // Read value from register\n\n  uint8_t id = pin - PNUM_ANALOG_BASE;\n  // if (id > 15) { // today we have not enough bits in the mask to support more\n  //   return -1021;\n  // }\n\n  if (analogvals == NULL) {  // allocate analogvals, analogchans and adcchans if this is the first invocation of init\n    analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));\n    analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t));\n    adcchans = (ADC_TypeDef **)calloc(NUM_ADC_INPUTS+1, sizeof(ADC_TypeDef));\n  }\n  analogvals[id] = value;     // Store sampled value\n  analogchans[id] = adcchan;  // Keep track of which ADC channel is used for reading this pin\n  adcchans[id] = adc;         // Keep track of which ADC this channel is on\n  usedpins |= (1 << id);                // This pin is now ready\n  if (id > highestPin) highestPin = id; // Store our highest pin in use\n\n  DIAG(F(\"ADCee::init(): value=%d, ADC%d: channel=%d, id=%d\"), value, adcnum, adcchan, id);\n\n  return value;\n}\n\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  uint8_t id = pin - PNUM_ANALOG_BASE;\n  // Was this pin initialised yet?\n  if ((usedpins & (1<<id) ) == 0)\n    return -1023;\n  // We do not need to check (analogvals == NULL)\n  // because usedpins would still be 0 in that case\n  return analogvals[id];\n}\n\n/*\n * Scan function that is called from interrupt\n */\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\nvoid ADCee::scan() {\n  static uint8_t id = 0;     // id and mask are the same thing but it is faster to\n  static uint16_t mask = 1;  // increment and shift instead to calculate mask from id\n  static bool waiting = false;\n  static ADC_TypeDef *adc;\n\n  adc = adcchans[id];\n  if (waiting)\n  {\n    // look if we have a result\n    if (!(adc->SR & (1 << 1)))\n      return; // no result, continue to wait\n    // found value\n    analogvals[id] = adc->DR;\n    // advance at least one track\n#ifdef DEBUG_ADC\n    if (id == 1) TrackManager::track[1]->setBrake(0);\n#endif\n    waiting = false;\n    id++;\n    mask = mask << 1;\n    if (id > highestPin) { // the 1 has been shifted out\n      id = 0;\n      mask = 1;\n    }\n  }\n  if (!waiting) {\n    if (usedpins == 0) // otherwise we would loop forever\n      return;\n    // look for a valid track to sample or until we are around\n    while (true) {\n      if (mask  & usedpins) {\n        // start new ADC aquire on id\n        adc = adcchans[id];\n        adc->SQR3 = analogchans[id]; // 1st conversion in regular sequence\n        adc->CR2 |= (1 << 30);       // Start 1st conversion SWSTART\n#ifdef DEBUG_ADC\n\tif (id == 1) TrackManager::track[1]->setBrake(1);\n#endif\n\twaiting = true;\n\treturn;\n      }\n      id++;\n      mask = mask << 1;\n      if (id > highestPin) {\n      \tid = 0;\n      \tmask = 1;\n      }\n    }\n  }\n}\n#pragma GCC pop_options\n\nvoid ADCee::begin() {\n  noInterrupts();\n  //ADC1 config sequence\n  RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // Enable ADC1 clock\n  // Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms\n  ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8\n  ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)\n  ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)\n  ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion\n  // Disable the DMA controller for ADC1\n  ADC1->CR2 &= ~ADC_CR2_DMA;\n  ADC1->CR2 &= ~(1 << 1); //Single conversion\n  ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0\n  ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC1->CR2 |= (1 << 0); // Switch on ADC1\n  // Wait for ADC1 to become ready (calibration complete)\n  while (!(ADC1->CR2 & ADC_CR2_ADON)) {\n  }\n#if defined(ADC2)\n  // Enable the ADC2 clock\n  RCC->APB2ENR |= RCC_APB2ENR_ADC2EN;\n\n  // Initialize ADC2\n  ADC2->CR1 = 0; // Disable all channels\n  ADC2->CR2 = 0; // Clear CR2 register\n\n  ADC2->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)\n  ADC2->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)\n  ADC2->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion\n  ADC2->CR2 &= ~ADC_CR2_DMA;   // Disable the DMA controller for ADC3\n  ADC2->CR2 &= ~(1 << 1); //Single conversion\n  ADC2->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0\n  ADC2->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC2->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC2->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n\n  // Enable the ADC\n  ADC2->CR2 |= ADC_CR2_ADON;\n\n  // Wait for ADC2 to become ready (calibration complete)\n  while (!(ADC2->CR2 & ADC_CR2_ADON)) {\n  }\n\n  // Perform ADC3 calibration (optional)\n  // ADC3->CR2 |= ADC_CR2_CAL;\n  // while (ADC3->CR2 & ADC_CR2_CAL) {\n  // }\n#endif\n#if defined(ADC3)\n  // Enable the ADC3 clock\n  RCC->APB2ENR |= RCC_APB2ENR_ADC3EN;\n\n  // Initialize ADC3\n  ADC3->CR1 = 0; // Disable all channels\n  ADC3->CR2 = 0; // Clear CR2 register\n\n  ADC3->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)\n  ADC3->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)\n  ADC3->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion\n  ADC3->CR2 &= ~ADC_CR2_DMA;   // Disable the DMA controller for ADC3\n  ADC3->CR2 &= ~(1 << 1); //Single conversion\n  ADC3->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0\n  ADC3->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC3->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n  ADC3->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register\n\n  // Enable the ADC\n  ADC3->CR2 |= ADC_CR2_ADON;\n\n  // Wait for ADC3 to become ready (calibration complete)\n  while (!(ADC3->CR2 & ADC_CR2_ADON)) {\n  }\n\n  // Perform ADC3 calibration (optional)\n  // ADC3->CR2 |= ADC_CR2_CAL;\n  // while (ADC3->CR2 & ADC_CR2_CAL) {\n  // }\n#endif\n  interrupts();\n}\n#endif\n"
  },
  {
    "path": "DCCTimerTEENSY.cpp",
    "content": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Mike S\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2021 Chris Harlow\n *  © 2021 David Cutting\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// ATTENTION: this file only compiles on a TEENSY\n// Please refer to DCCTimer.h for general comments about how this class works\n// This is to avoid repetition and duplication.\n#ifdef TEENSYDUINO\n\n#include \"DCCTimer.h\"\n\nINTERRUPT_CALLBACK interruptHandler=0;\n\nIntervalTimer myDCCTimer;\n\nvoid DCCTimer::begin(INTERRUPT_CALLBACK callback) {\n  interruptHandler=callback;\n  myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);\n  }\n\nvoid DCCTimer::startRailcomTimer(byte brakePin) {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n  (void) brakePin; \n}\n\nvoid DCCTimer::ackRailcomTimer() {\n  // TODO: for intended operation see DCCTimerAVR.cpp\n}\n\nbool DCCTimer::isPWMPin(byte pin) {\n       //Teensy: digitalPinHasPWM, todo\n      (void) pin;\n       return false;  // TODO what are the relevant pins? \n  }\n\nvoid DCCTimer::setPWM(byte pin, bool high) {\n    // TODO what are the relevant pins?\n    (void) pin;\n    (void) high;\n}\n\nvoid DCCTimer::clearPWM() {\n    // Do nothing unless we implent HA\n}\n\n#if defined(__IMXRT1062__)  //Teensy 4.0 and Teensy 4.1\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n    uint32_t m1 = HW_OCOTP_MAC1;\n    uint32_t m2 = HW_OCOTP_MAC0;\n    mac[0] = m1 >> 8;\n    mac[1] = m1 >> 0;\n    mac[2] = m2 >> 24;\n    mac[3] = m2 >> 16;\n    mac[4] = m2 >> 8;\n    mac[5] = m2 >> 0;\n  }\n\n#else\n\n// http://forum.pjrc.com/threads/91-teensy-3-MAC-address\nvoid teensyRead(uint8_t word, uint8_t *mac, uint8_t offset) {\n  FTFL_FCCOB0 = 0x41;             // Selects the READONCE command\n  FTFL_FCCOB1 = word;             // read the given word of read once area\n\n  // launch command and wait until complete\n  FTFL_FSTAT = FTFL_FSTAT_CCIF;\n  while(!(FTFL_FSTAT & FTFL_FSTAT_CCIF));\n\n  *(mac+offset) =   FTFL_FCCOB5;       // collect only the top three bytes,\n  *(mac+offset+1) = FTFL_FCCOB6;       // in the right orientation (big endian).\n  *(mac+offset+2) = FTFL_FCCOB7;       // Skip FTFL_FCCOB4 as it's always 0.\n}\n\nvoid   DCCTimer::getSimulatedMacAddress(byte mac[6]) {\n    teensyRead(0xe,mac,0);\n    teensyRead(0xf,mac,3);\n  }\n#endif \n\nvolatile int DCCTimer::minimum_free_memory=__INT_MAX__;\n\n// Return low memory value... \nint DCCTimer::getMinimumFreeMemory() {\n  noInterrupts(); // Disable interrupts to get volatile value \n  int retval = freeMemory();\n  interrupts();\n  return retval;\n}\n\nextern \"C\" char* sbrk(int incr);\n\n#if !defined(__IMXRT1062__)\nint DCCTimer::freeMemory() {\n  char top;\n  return &top - reinterpret_cast<char*>(sbrk(0));\n}\n\n#else\n#if defined(ARDUINO_TEENSY40)\n  static const unsigned DTCM_START = 0x20000000UL;\n  static const unsigned OCRAM_START = 0x20200000UL;\n  static const unsigned OCRAM_SIZE = 512;\n  static const unsigned FLASH_SIZE = 1984;\n#elif defined(ARDUINO_TEENSY41)\n  static const unsigned DTCM_START = 0x20000000UL;\n  static const unsigned OCRAM_START = 0x20200000UL;\n  static const unsigned OCRAM_SIZE = 512;\n  static const unsigned FLASH_SIZE = 7936;\n#if TEENSYDUINO>151\n  extern \"C\" uint8_t external_psram_size;\n#endif\n#endif\n\nint DCCTimer::freeMemory() {\n  extern unsigned long _ebss;\n  extern unsigned long _sdata;\n  extern unsigned long _estack;\n  const unsigned DTCM_START = 0x20000000UL;\n  unsigned dtcm = (unsigned)&_estack - DTCM_START;\n  unsigned stackinuse = (unsigned) &_estack -  (unsigned) __builtin_frame_address(0);\n  unsigned varsinuse = (unsigned)&_ebss - (unsigned)&_sdata;\n  unsigned freemem = dtcm - (stackinuse + varsinuse);\n  return freemem;\n}\n\n#endif\nvoid DCCTimer::reset() {\n  // found at https://forum.pjrc.com/threads/59935-Reboot-Teensy-programmatically\n  SCB_AIRCR = 0x05FA0004;\n}\n\nvoid DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {\n}\nvoid DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {\n}\n\nint16_t ADCee::ADCmax() {\n  return 4095;\n}\n\nint ADCee::init(uint8_t pin) {\n  return analogRead(pin);\n}\n/*\n * Read function ADCee::read(pin) to get value instead of analogRead(pin)\n */\nint ADCee::read(uint8_t pin, bool fromISR) {\n  int current;\n  if (!fromISR) noInterrupts();\n  current = analogRead(pin);\n  if (!fromISR) interrupts();\n  return current;\n}\n/*\n * Scan function that is called from interrupt\n */\nvoid ADCee::scan() {\n}\n\nvoid ADCee::begin() {\n  noInterrupts();\n  interrupts();\n}\n#endif\n"
  },
  {
    "path": "DCCWaveform.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef ARDUINO_ARCH_ESP32\n  // This code is replaced entirely on an ESP32\n#include <Arduino.h>\n#include \"DCCWaveform.h\"\n#include \"TrackManager.h\"\n#include \"DCCTimer.h\"\n#include \"DCCACK.h\"\n#include \"DIAG.h\"\n#include \"Railcom.h\"\n\nbool DCCWaveform::cutoutNextTime=false;\nDCCWaveform  DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);\nDCCWaveform  DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);\n\n\n// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.\nconst byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};\n\nconst byte idlePacket[] = {0xFF, 0x00, 0xFF};\nconst byte resetPacket[] = {0x00, 0x00, 0x00};\n\n\n// For each state of the wave  nextState=stateTransform[currentState] \nconst WAVE_STATE stateTransform[]={\n   /* WAVE_START   -> */ WAVE_PENDING,\n   /* WAVE_MID_1   -> */ WAVE_START,\n   /* WAVE_HIGH_0  -> */ WAVE_MID_0,\n   /* WAVE_MID_0   -> */ WAVE_LOW_0,\n   /* WAVE_LOW_0   -> */ WAVE_START,\n   /* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};\n\n// For each state of the wave, signal pin is HIGH or LOW   \nconst bool signalTransform[]={\n   /* WAVE_START   -> */ HIGH,\n   /* WAVE_MID_1   -> */ LOW,\n   /* WAVE_HIGH_0  -> */ HIGH,\n   /* WAVE_MID_0   -> */ LOW,\n   /* WAVE_LOW_0   -> */ LOW,\n   /* WAVE_PENDING (should not happen) -> */ LOW};\n\nvoid DCCWaveform::begin() {\n  DCCTimer::begin(DCCWaveform::interruptHandler);     \n}\n\nvoid DCCWaveform::loop() {\n // empty placemarker in case ESP32 needs something here \n}\n\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\n\nvoid DCCWaveform::interruptHandler() {\n  // call the timer edge sensitive actions for progtrack and maintrack\n  // member functions would be cleaner but have more overhead\n  #if defined(HAS_ENOUGH_MEMORY)\n  if (cutoutNextTime) {\n    cutoutNextTime=false;\n    Railcom::incCutout();\n    DCCTimer::startRailcomTimer(9);\n  }\n  #endif\n  byte sigMain=signalTransform[mainTrack.state];\n  byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];\n  \n  // Set the signal state for both tracks\n  TrackManager::setDCCSignal(sigMain);\n  TrackManager::setPROGSignal(sigProg);\n\n  // Refresh the values in the ADCee object buffering the values of the ADC HW\n  ADCee::scan();\n\n  // Move on in the state engine\n  mainTrack.state=stateTransform[mainTrack.state];    \n  progTrack.state=stateTransform[progTrack.state];    \n\n  // WAVE_PENDING means we dont yet know what the next bit is\n  if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();  \n  if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();\n  else DCCACK::checkAck(progTrack.getResets());\n\n}\n#pragma GCC pop_options\n\n// An instance of this class handles the DCC transmissions for one track. (main or prog)\n// Interrupts are marshalled via the statics.\n// A track has a current transmit buffer, and a pending buffer.\n// When the current buffer is exhausted, either the pending buffer (if there is one waiting) or an idle buffer.\n\n\n\nDCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {\n  isMainTrack = isMain;\n  packetPending = false;\n  reminderWindowOpen = false;\n  memcpy(transmitPacket, idlePacket, sizeof(idlePacket));\n  state = WAVE_START;\n  // The +1 below is to allow the preamble generator to create the stop bit\n  // for the previous packet. \n  requiredPreambles = preambleBits+1;  \n  bytes_sent = 0;\n  bits_sent = 0;\n}\n\nbool DCCWaveform::railcomPossible=false;     // High accuracy only    \nvolatile bool DCCWaveform::railcomActive=false;     // switched on by user\n \nbool DCCWaveform::setRailcom(bool on) {\n  if (on && railcomPossible) {\n    railcomActive=true;\n  }\n  else {\n    railcomActive=false;\n  } \n  return railcomActive;\n}\n\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\nvoid DCCWaveform::interrupt2() {\n  // calculate the next bit to be sent:\n  // set state WAVE_MID_1  for a 1=bit\n  //        or WAVE_HIGH_0 for a 0 bit.\n  if (remainingPreambles > 0 ) {\n    state=WAVE_MID_1;  // switch state to trigger LOW on next interrupt\n    \n    remainingPreambles--;\n  \n    // As we get to the end of the preambles, open the reminder window.\n    // This delays any reminder insertion until the last moment so\n    // that the reminder doesn't block a more urgent packet. \n    reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;\n    if (remainingPreambles==1)\n      promotePendingPacket();\n\n#if defined(HAS_ENOUGH_MEMORY)   \n    else if (isMainTrack && railcomActive) {\n      if (remainingPreambles==(requiredPreambles-1)) {\n        // First look if we need to start a railcom cutout on next interrupt\n        cutoutNextTime= true;\n      } else if (remainingPreambles==(requiredPreambles-12)) {\n        // cutout has ended so its now possible to poll the railcom detectors\n        // requiredPreambles is one higher that preamble length so\n        // if preamble length is 16 then this evaluates to 5\n      } else if (remainingPreambles==(requiredPreambles-3)) {\n        // cutout can be ended when read\n        // see above for requiredPreambles\n        DCCTimer::ackRailcomTimer();\n      }\n    }\n#endif    \n    // Update free memory diagnostic as we don't have anything else to do this time.\n    // Allow for checkAck and its called functions using 22 bytes more.\n    else DCCTimer::updateMinimumFreeMemoryISR(22); \n    return;\n  }\n\n  // Wave has gone HIGH but what happens next depends on the bit to be transmitted\n  // beware OF 9-BIT MASK  generating a zero to start each byte\n  state=(transmitPacket[bytes_sent] & bitMask[bits_sent])? WAVE_MID_1 : WAVE_HIGH_0; \n  bits_sent++;\n\n  // If this is the last bit of a byte, prepare for the next byte\n\n  if (bits_sent == 9) { // zero followed by 8 bits of a byte\n    //end of Byte\n    bits_sent = 0;\n    bytes_sent++;\n    // if this is the last byte, prepere for next packet\n    if (bytes_sent >= transmitLength) {\n      // end of transmission buffer... repeat or switch to next message\n      bytes_sent = 0;\n      // preamble for next packet will start...\n      remainingPreambles = requiredPreambles;\n    }\n  }  \n}\n#pragma GCC pop_options\n\n// Wait until there is no packet pending, then make this pending\nvoid DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {\n  if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum\n  while (packetPending);\n\n  byte checksum = 0;\n  for (byte b = 0; b < byteCount; b++) {\n    checksum ^= buffer[b];\n    pendingPacket[b] = buffer[b];\n  }\n  // buffer is MAX_PACKET_SIZE but pendingPacket is one bigger\n  pendingPacket[byteCount] = checksum;\n  pendingLength = byteCount + 1;\n  pendingRepeats = repeats;\n  packetPending = true;\n  clearResets();\n}\n\nbool DCCWaveform::isReminderWindowOpen() {\n  return reminderWindowOpen && ! packetPending;\n}\n\nvoid DCCWaveform::promotePendingPacket() {\n    // fill the transmission packet from the pending packet\n    \n    // Just keep going if repeating  \n    if (transmitRepeats > 0) {\n        transmitRepeats--;\n        return;\n      }\n    \n    if (packetPending) {\n        // Copy pending packet to transmit packet\n        // a fixed length memcpy is faster than a variable length loop for these small lengths\n        // for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];\n        memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));\n        \n        transmitLength = pendingLength;\n        transmitRepeats = pendingRepeats;\n        packetPending = false;\n        clearResets();\n        return;\n      }\n      \n      // nothing to do, just send idles or resets\n      // Fortunately reset and idle packets are the same length\n      memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));\n      transmitLength = sizeof(idlePacket);\n      transmitRepeats = 0;\n      if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)\n}\n#endif\n"
  },
  {
    "path": "DCCWaveform.h",
    "content": "/*\n *  © 2021 M Steve Todd\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2024 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DCCWaveform_h\n#define DCCWaveform_h\n#ifdef ARDUINO_ARCH_ESP32\n#include \"DCCRMT.h\"\n#endif\n\n\n\n// Number of preamble bits.\nconst byte PREAMBLE_BITS_MAIN = 16;\nconst byte PREAMBLE_BITS_PROG = 22;\nconst byte MAX_PACKET_SIZE = 5;     // NMRA standard extended packets, payload size WITHOUT checksum.\n\n\n// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic\n// to the transform array.\nenum  WAVE_STATE : byte {\n  WAVE_START=0,  // wave going high at start of bit \n  WAVE_MID_1=1,  // middle of 1 bit \n  WAVE_HIGH_0=2, // first part of 0 bit high\n  WAVE_MID_0=3,  // middle of 0 bit\n  WAVE_LOW_0=4,  // first part of 0 bit low\n  WAVE_PENDING=5 // next bit not yet known\n  };\n\n// NOTE: static functions are used for the overall controller, then\n// one instance is created for each track.\n\nclass DCCWaveform {\n  public:\n    DCCWaveform( byte preambleBits, bool isMain);\n    static void begin();\n    static void loop();\n    static DCCWaveform  mainTrack;\n    static DCCWaveform  progTrack;\n    inline void clearRepeats() { transmitRepeats=0; }\n#ifndef ARDUINO_ARCH_ESP32\n    inline void clearResets() { sentResetsSincePacket=0; }\n    inline byte getResets() { return sentResetsSincePacket; }\n#else\n  // extrafudge is added when we know that the resets will first come extrafudge  packets in the future\n    inline void clearResets(byte extrafudge=0) {\n      if ((isMainTrack ? rmtMainChannel : rmtProgChannel) == NULL) return;\n      resetPacketBase = isMainTrack ? rmtMainChannel->packetCount() : rmtProgChannel->packetCount();\n      resetPacketBase += extrafudge;\n    };\n    inline byte getResets() {\n      if ((isMainTrack ? rmtMainChannel : rmtProgChannel) == NULL) return 0;\n      uint32_t packetcount = isMainTrack ?\n\trmtMainChannel->packetCount() : rmtProgChannel->packetCount();\n      uint32_t count = packetcount - resetPacketBase; // Beware of unsigned interger arithmetic.\n      if (count > UINT32_MAX/2)                       // we are in the extrafudge area\n\treturn 0;\n      if (count > 255)                                // cap to 255\n\treturn 255;\n      return count;                                   // all special cases handled above\n    };\n#endif\n    void schedulePacket(const byte buffer[], byte byteCount, byte repeats);\n    bool isReminderWindowOpen();\n    void promotePendingPacket();\n    static bool setRailcom(bool on);\n    inline static bool isRailcom() {\n      return railcomActive;\n    };\n    inline static bool isRailcomPossible() {\n      return railcomPossible;\n    };\n    inline static void setRailcomPossible(bool yes) {\n      railcomPossible=yes;\n      if (!yes) setRailcom(false);\n    };\n    \n\n  private:\n#ifndef ARDUINO_ARCH_ESP32\n    volatile bool packetPending;\n    volatile bool reminderWindowOpen;\n    volatile byte sentResetsSincePacket;\n#else\n    volatile uint32_t resetPacketBase;\n#endif\n    static void interruptHandler();\n    void interrupt2();\n    \n    bool isMainTrack;\n    // Transmission controller\n    byte transmitPacket[MAX_PACKET_SIZE+1]; // +1 for checksum\n    byte transmitLength;\n    byte transmitRepeats;      // remaining repeats of transmission\n    byte remainingPreambles;\n    byte requiredPreambles;\n    byte bits_sent;           // 0-8 (yes 9 bits) sent for current byte\n    byte bytes_sent;          // number of bytes sent from transmitPacket\n    WAVE_STATE state;         // wave generator state machine\n    byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum\n    byte pendingLength;\n    byte pendingRepeats;\n    static bool railcomPossible; // High accuracy mode only\n    static volatile bool railcomActive;     // switched on by user\n    static bool cutoutNextTime;   // railcom\n#ifdef ARDUINO_ARCH_ESP32\n  static RMTChannel *rmtMainChannel;\n  static RMTChannel *rmtProgChannel;\n#endif\n};\n#endif\n"
  },
  {
    "path": "DCCWaveformRMT.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n// This code is ESP32 ONLY.  \n#ifdef ARDUINO_ARCH_ESP32\n#include \"DCCWaveform.h\"\n#include \"DCCACK.h\"\n#include \"TrackManager.h\"\n\nDCCWaveform  DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);\nDCCWaveform  DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);\nRMTChannel *DCCWaveform::rmtMainChannel = NULL;\nRMTChannel *DCCWaveform::rmtProgChannel = NULL;\n\nbool DCCWaveform::railcomPossible=false;     // High accuracy only    \nvolatile bool DCCWaveform::railcomActive=false;     // switched on by user\n\nDCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {\n  isMainTrack = isMain;\n  requiredPreambles = preambleBits;\n}\nvoid DCCWaveform::begin() {\n  for(const auto& md: TrackManager::getMainDrivers()) {\n    pinpair p = md->getSignalPin();\n    if(rmtMainChannel) {\n      //DIAG(F(\"added pins %d %d to MAIN channel\"), p.pin, p.invpin);\n      rmtMainChannel->addPin(p); // add pin to existing main channel\n    } else {\n      //DIAG(F(\"new MAIN channel with pins %d %d\"), p.pin, p.invpin);\n      rmtMainChannel = new RMTChannel(p, true); /* create new main channel */\n    }\n  }\n  MotorDriver *md = TrackManager::getProgDriver();\n  if (md) {\n    pinpair p = md->getSignalPin();\n    if (rmtProgChannel) {\n      //DIAG(F(\"added pins %d %d to PROG channel\"), p.pin, p.invpin);\n      rmtProgChannel->addPin(p); // add pin to existing prog channel\n    } else {\n      //DIAG(F(\"new PROGchannel with pins %d %d\"), p.pin, p.invpin);\n      rmtProgChannel = new RMTChannel(p, false);\n    }\n  }\n}\n\nvoid DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {\n  if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum\n  RMTChannel *rmtchannel = (isMainTrack ? rmtMainChannel : rmtProgChannel);\n  if (rmtchannel == NULL)\n    return; // no idea to prepare packet if we can not send it anyway\n  \n  rmtchannel->waitForDataCopy(); // blocking wait so we can write into buffer\n  byte checksum = 0;\n  for (byte b = 0; b < byteCount; b++) {\n    checksum ^= buffer[b];\n    pendingPacket[b] = buffer[b];\n  }\n  // buffer is MAX_PACKET_SIZE but pendingPacket is one bigger\n  pendingPacket[byteCount] = checksum;\n  pendingLength = byteCount + 1;\n  pendingRepeats = repeats;\n// DIAG repeated commands (accesories)\n//  if (pendingRepeats > 0)\n//    DIAG(F(\"Repeats=%d on %s track\"), pendingRepeats, isMainTrack ? \"MAIN\" : \"PROG\");\n  // The resets will be zero not only now but as well repeats packets into the future\n  clearResets(repeats+1);\n  {\n    int ret = 0;\n    do {\n      ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);\n    } while(ret > 0);\n  }\n}\n\nbool DCCWaveform::isReminderWindowOpen() {\n  if(isMainTrack) {\n    if (rmtMainChannel == NULL)\n      return false;\n    return !rmtMainChannel->busy();\n  } else {\n    if (rmtProgChannel == NULL)\n      return false;\n    return !rmtProgChannel->busy();\n  }\n}\nvoid IRAM_ATTR DCCWaveform::loop() {\n  DCCACK::checkAck(progTrack.getResets());\n}\n\nbool DCCWaveform::setRailcom(bool on) {\n  // TODO... ESP32 railcom waveform\n  return false;\n}\n\n#endif\n"
  },
  {
    "path": "DIAG.h",
    "content": "/*\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DIAG_h\n#define DIAG_h\n\n#include \"StringFormatter.h\"\n#define DIAG  StringFormatter::diag\n#define LCD   StringFormatter::lcd\n#define SCREEN  StringFormatter::lcd2\n#endif\n"
  },
  {
    "path": "Display.cpp",
    "content": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// CAUTION: the device dependent parts of this class are created in the .ini\n// using LCD_Implementation.h\n\n/* The strategy for drawing the screen is as follows.\n *  1) There are up to eight rows of text to be displayed.\n *  2) Blank rows of text are ignored.\n *  3) If there are more non-blank rows than screen lines,\n *     then all of the rows are displayed, with the rest of the\n *     screen being blank.\n *  4) If there are fewer non-blank rows than screen lines,\n *     then a scrolling strategy is adopted so that, on each screen\n *     refresh, a different subset of the rows is presented.\n *  5) On each entry into loop2(), a single operation is sent to the \n *     screen; this may be a position command or a character for\n *     display.  This spreads the onerous work of updating the screen\n *     and ensures that other loop() functions in the application are\n *     not held up significantly.  The exception to this is when \n *     the loop2() function is called with force=true, where \n *     a screen update is executed to completion.  This is normally\n *     only done during start-up.\n *  The scroll mode is selected by defining SCROLLMODE as 0, 1 or 2\n *  in the config.h.\n *  #define SCROLLMODE 0 is scroll continuous (fill screen if poss),\n *  #define SCROLLMODE 1 is by page (alternate between pages),\n *  #define SCROLLMODE 2 is by row (move up 1 row at a time).\n\n */\n\n#include \"Display.h\"\n\n// Constructor - allocates device driver.\nDisplay::Display(DisplayDevice *deviceDriver) {\n  _deviceDriver = deviceDriver;\n  // Get device dimensions in characters (e.g. 16x2).\n  numScreenColumns = _deviceDriver->getNumCols();\n  numScreenRows = _deviceDriver->getNumRows();\n  for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++) \n    rowBuffer[row][0] = '\\0';\n  \n  addDisplay(0);  // Add this display as display number 0\n};\n\nvoid Display::begin() {\n  _deviceDriver->begin();\n  _deviceDriver->clearNative();\n}\n\nvoid Display::_clear() {\n  _deviceDriver->clearNative();\n  for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++) \n    rowBuffer[row][0] = '\\0';\n}\n\nvoid Display::_setRow(uint8_t line) {\n  hotRow = line;\n  hotCol = 0;\n  rowBuffer[hotRow][0] = '\\0';  // Clear existing text\n}\n\nsize_t Display::_write(uint8_t b) {\n  if (hotRow >= MAX_CHARACTER_ROWS || hotCol >= MAX_CHARACTER_COLS) return -1;\n  rowBuffer[hotRow][hotCol] = b;\n  hotCol++;\n  rowBuffer[hotRow][hotCol] = '\\0';\n  return 1;\n}\n\n// Refresh screen completely (will block until complete). Used\n// during start-up.\nvoid Display::_refresh() {\n  loop2(true);\n}\n\n// On normal loop entries, loop will only make one output request on each\n// entry, to avoid blocking while waiting for the I2C.\nvoid Display::_displayLoop() {\n  // If output device is busy, don't do anything on this loop\n  // This avoids blocking while waiting for the device to complete.\n  if (!_deviceDriver->isBusy()) loop2(false);\n}\n\nDisplay *Display::loop2(bool force) {\n  unsigned long currentMillis = millis();\n\n  if (!force) {\n    // See if we're in the time between updates\n    if ((currentMillis - lastScrollTime) < DISPLAY_SCROLL_TIME)\n      return NULL;\n  } else {\n    // force full screen update from the beginning.\n    rowFirst = 0;\n    rowCurrent = 0;\n    bufferPointer = 0;\n    noMoreRowsToDisplay = false;\n    slot = 0;\n  }\n\n  do {\n    if (bufferPointer == 0) {\n      // Search for non-blank row\n      while (!noMoreRowsToDisplay) {\n        if (!isCurrentRowBlank()) break;\n        moveToNextRow();\n        if (rowCurrent == rowFirst) noMoreRowsToDisplay = true;  \n      }\n\n      if (noMoreRowsToDisplay) {\n        // No non-blank lines left, so draw blank line\n        buffer[0] = '\\0';\n      } else {\n        // Non-blank line found, so copy it (including terminator)\n        for (uint8_t i = 0; i <= MAX_CHARACTER_COLS; i++)\n          buffer[i] = rowBuffer[rowCurrent][i];\n      }\n      _deviceDriver->setRowNative(slot);  // Set position for display\n      charIndex = 0;\n      bufferPointer = &buffer[0];\n    } else {\n      // Write next character, or a space to erase current position.\n      char ch = *bufferPointer;\n      if (ch) {\n        _deviceDriver->writeNative(ch);\n        bufferPointer++;\n      } else {\n        _deviceDriver->writeNative(' ');\n      }\n\n      if (++charIndex >= MAX_CHARACTER_COLS) {\n        // Screen slot completed, move to next nonblank row\n        bufferPointer = 0;\n        for (;;) {\n          moveToNextRow();\n          if (rowCurrent == rowFirst) {\n            noMoreRowsToDisplay = true;\n            break;\n          }  \n          if (!isCurrentRowBlank()) break;\n        }\n        // Move to next screen slot, if available\n        slot++;\n        if (slot >= numScreenRows) {\n          // Last slot on screen written, so get ready for next screen update.\n#if SCROLLMODE==0\n          // Scrollmode 0 scrolls continuously.  If the rows fit on the screen,\n          // then restart at row 0, but otherwise continue with the row\n          // after the last one displayed.\n          if (countNonBlankRows() <= numScreenRows)\n            rowCurrent = 0;\n          rowFirst = rowCurrent;\n#elif SCROLLMODE==1\n          // Scrollmode 1 scrolls by page, so if the last page has just completed then\n          // next time restart with row 0.\n          if (noMoreRowsToDisplay) \n            rowFirst = rowCurrent = 0;\n#else\n          // Scrollmode 2 scrolls by row.  If the rows don't fit on the screen,\n          // then start one row further on next time.  If they do fit, then \n          // show them in order and start next page at row 0.\n          if (countNonBlankRows() <= numScreenRows) {\n            rowFirst = rowCurrent = 0;\n          } else {\n            // Find first non-blank row after the previous first row\n            rowCurrent = rowFirst;\n            do {\n              moveToNextRow();\n            } while (isCurrentRowBlank());\n            rowFirst = rowCurrent;\n          }\n#endif\n          noMoreRowsToDisplay = false;\n          slot = 0;\n          _deviceDriver->setRowNative(slot);  // Set position for display\n          lastScrollTime = currentMillis;\n          return NULL;\n        }\n      }\n    }\n  } while (force);\n\n  return NULL;\n}\n\nbool Display::isCurrentRowBlank() {\n  return (rowBuffer[rowCurrent][0] == '\\0');\n}\n\nvoid Display::moveToNextRow() {\n  // Skip blank rows\n  if (++rowCurrent >= MAX_CHARACTER_ROWS) \n      rowCurrent = 0;\n}\n\nuint8_t Display::countNonBlankRows() {\n  uint8_t count = 0;\n  for (uint8_t rowNumber=0; rowNumber<MAX_CHARACTER_ROWS; rowNumber++) {\n    if (rowBuffer[rowNumber][0] != '\\0')\n      count++;\n  }\n  return count;\n}\n  \n"
  },
  {
    "path": "Display.h",
    "content": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef Display_h\n#define Display_h\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"DisplayInterface.h\"\n\n// Allow maximum message length to be overridden from config.h\n#if !defined(MAX_MSG_SIZE)\n#define MAX_MSG_SIZE 20 \n#endif\n\n// Set default scroll mode (overridable in config.h)\n#if !defined(SCROLLMODE) \n#define SCROLLMODE 1\n#endif\n\n// This class is created in Display_Implementation.h\n\nclass Display : public DisplayInterface {\npublic:\n  Display(DisplayDevice *deviceDriver);\n#if !defined (MAX_CHARACTER_ROWS)\n  #if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)\n    static const int MAX_CHARACTER_ROWS = 17;\n  #else\n    static const int MAX_CHARACTER_ROWS = 8;\n  #endif\n#endif\n  static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE;\n  static const long DISPLAY_SCROLL_TIME = 3000;  // 3 seconds\n\nprivate:\n  DisplayDevice *_deviceDriver;\n\n  unsigned long lastScrollTime = 0;\n  uint8_t hotRow = 0;\n  uint8_t hotCol = 0;\n  uint8_t slot = 0;\n  uint8_t rowFirst = 0;\n  uint8_t rowCurrent = 0;\n  uint8_t charIndex = 0;\n  char buffer[MAX_CHARACTER_COLS + 1];\n  char* bufferPointer = 0;\n  bool noMoreRowsToDisplay = false;\n  uint16_t numScreenRows;\n  uint16_t numScreenColumns = MAX_CHARACTER_COLS;\n\n  char rowBuffer[MAX_CHARACTER_ROWS][MAX_CHARACTER_COLS+1];\n\npublic:\n  void begin() override;  \n  void _clear() override;\n  void _setRow(uint8_t line) override;\n  size_t _write(uint8_t b) override;\n  void _refresh() override;\n  void _displayLoop() override;\n  Display *loop2(bool force);\n  bool findNonBlankRow();\n  bool isCurrentRowBlank();\n  void moveToNextRow();\n  uint8_t countNonBlankRows();\n\n};\n\n#endif\n"
  },
  {
    "path": "DisplayInterface.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"DisplayInterface.h\"\n\n// Install null display driver initially - will be replaced if required.\nDisplayInterface *DisplayInterface::_displayHandler = new DisplayInterface();\n\nuint8_t DisplayInterface::_selectedDisplayNo = 255;\n"
  },
  {
    "path": "DisplayInterface.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DisplayInterface_h\n#define DisplayInterface_h\n\n#include <Arduino.h>\n\n// Definition of base class for displays.  The base class does nothing.\nclass DisplayInterface : public Print {\nprotected:\n  static DisplayInterface *_displayHandler;\n  static uint8_t _selectedDisplayNo;  // Nothing selected.\n  DisplayInterface *_nextHandler = NULL;\n  uint8_t _displayNo = 0;\n\npublic:\n  // Add display object to list of displays\n  void addDisplay(uint8_t displayNo) {\n    _nextHandler = _displayHandler;\n    _displayHandler = this;\n    _displayNo = displayNo;\n  }\n  static DisplayInterface *getDisplayHandler() {\n    return _displayHandler;\n  }\n  uint8_t getDisplayNo() {\n    return _displayNo;\n  }\n\n  // The next functions are to provide compatibility with calls to the LCD function\n  // which does not specify a display number.  These always apply to display '0'.\n  static void refresh() { refresh(0); };\n  static void setRow(uint8_t line) { setRow(0, line); };\n  static void clear() { clear(0); };\n\n  // Additional functions to support multiple displays.  These perform a\n  // multicast to all displays that match the selected displayNo.\n  // Display number zero is the default one.\n  static void setRow(uint8_t displayNo, uint8_t line) { \n    _selectedDisplayNo = displayNo;\n    for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler) { \n      if (displayNo == p->_displayNo) p->_setRow(line);\n    }\n  }\n  size_t write (uint8_t c) override {\n    for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler) \n      if (_selectedDisplayNo == p->_displayNo) p->_write(c);\n    return _displayHandler ? 1 : 0;\n  }\n  static void clear(uint8_t displayNo) { \n    for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler) \n      if (displayNo == p->_displayNo) p->_clear();\n  }\n  static void refresh(uint8_t displayNo) {\n    for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler)\n      if (displayNo == p->_displayNo) p->_refresh();\n  }\n  static void loop() {\n    for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler) \n      p->_displayLoop();\n  };\n  // The following are overridden within the specific device class\n  virtual void begin() {};\n  virtual size_t _write(uint8_t c) { (void)c; return 0; };\n  virtual void _setRow(uint8_t line) { (void)line; }\n  virtual void _clear() {}\n  virtual void _refresh() {}\n  virtual void _displayLoop() {}\n};\n\nclass DisplayDevice {\npublic:\n  virtual bool begin() { return true; }\n  virtual void clearNative() = 0;\n  virtual void setRowNative(uint8_t line) = 0;\n  virtual size_t writeNative(uint8_t c) = 0;\n  virtual bool isBusy() = 0;\n  virtual uint16_t getNumRows() = 0;\n  virtual uint16_t getNumCols() = 0;\n};\n#endif\n"
  },
  {
    "path": "Display_Implementation.h",
    "content": "/*\n *  © 2021, Chris Harlow, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n////////////////////////////////////////////////////////////////////////////////////\n// This implementation is designed to be #included ONLY ONCE in the .ino \n//\n// It will create a driver implemntation and a shim class implementation.\n// This means that other classes can reference the shim without knowing\n// which library is involved.\n////////////////////////////////////////////////////////////////////////////////////\n\n#ifndef LCD_Implementation_h\n#define LCD_Implementation_h\n#include \"DisplayInterface.h\"\n#include \"SSD1306Ascii.h\"\n#include \"LiquidCrystal_I2C.h\"\n  \n\n// Implement the Display shim class as a singleton.\n// The DisplayInterface class implements a display handler with no code (null device);\n// The Display class sub-classes DisplayInterface to provide the common display code;\n// Then Display class talks to the specific device type classes:\n//    SSD1306AsciiWire for I2C OLED driver with SSD1306 or SH1106 controllers;\n//    LiquidCrystal_I2C for I2C LCD driver for HD44780 with PCF8574 'backpack'.\n\n#if defined(OLED_DRIVER)\n  #define DISPLAY_START(xxx) { \\\n    DisplayInterface *t = new Display(new SSD1306AsciiWire(OLED_DRIVER)); \\\n    t->begin(); \\\n    xxx; \\\n    t->refresh(); \\\n  } \n  \n#elif defined(LCD_DRIVER)\n  #define DISPLAY_START(xxx) { \\\n    DisplayInterface *t = new Display(new LiquidCrystal_I2C(LCD_DRIVER)); \\\n    t->begin(); \\\n    xxx;  \\\n    t->refresh();}\n#else\n  #define DISPLAY_START(xxx) { \\\n  xxx; \\\n  }\n\n#endif\n#endif // LCD_Implementation_h\n"
  },
  {
    "path": "EEStore.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  © 2013-2016 Gregg E. Berman\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"defines.h\"\n#ifndef DISABLE_EEPROM\n#include \"EEStore.h\"\n\n#include \"DIAG.h\"\n#include \"Outputs.h\"\n#include \"Sensors.h\"\n#include \"Turnouts.h\"\n\n#if defined(ARDUINO_ARCH_SAMC)\nExternalEEPROM EEPROM;\n#endif\n\nvoid EEStore::init() {\n#if defined(ARDUINO_ARCH_SAMC)\n  EEPROM.begin(0x50);  // Address for Microchip 24-series EEPROM with all three\n                       // A pins grounded (0b1010000 = 0x50)\n#endif\n\n  eeStore = (EEStore *)calloc(1, sizeof(EEStore));\n\n  EEPROM.get(0, eeStore->data);  // get eeStore data\n\n  // check to see that eeStore contains valid DCC++ ID\n  if (strncmp(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)) != 0) {  \n    // if not, create blank eeStore structure (no\n    // turnouts, no sensors) and save it back to EEPROM  \n    strncpy(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)+0);  \n    eeStore->data.nTurnouts = 0;\n    eeStore->data.nSensors = 0;\n    eeStore->data.nOutputs = 0;\n    EEPROM.put(0, eeStore->data);\n  }\n\n  reset();          // set memory pointer to first free EEPROM space\n  Turnout::load();  // load turnout definitions\n  Sensor::load();   // load sensor definitions\n  Output::load();   // load output definitions\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nvoid EEStore::clear() {\n  sprintf(eeStore->data.id,\n          EESTORE_ID);  // create blank eeStore structure (no turnouts, no\n                        // sensors) and save it back to EEPROM\n  eeStore->data.nTurnouts = 0;\n  eeStore->data.nSensors = 0;\n  eeStore->data.nOutputs = 0;\n  EEPROM.put(0, eeStore->data);\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nvoid EEStore::store() {\n  reset();\n  Turnout::store();\n  Sensor::store();\n  Output::store();\n  EEPROM.put(0, eeStore->data);\n  DIAG(F(\"EEPROM used: %d/%d bytes\"), EEStore::pointer(), EEPROM.length());\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nvoid EEStore::advance(int n) { eeAddress += n; }\n\n///////////////////////////////////////////////////////////////////////////////\n\nvoid EEStore::reset() { eeAddress = sizeof(EEStore); }\n///////////////////////////////////////////////////////////////////////////////\n\nint EEStore::pointer() { return (eeAddress); }\n///////////////////////////////////////////////////////////////////////////////\n\nvoid EEStore::dump(int num) {\n  byte b = 0;\n  DIAG(F(\"Addr  0x  char\"));\n  for (int n = 0; n < num; n++) {\n    EEPROM.get(n, b);\n    DIAG(F(\"%d     %x    %c\"), n, b, isprint(b) ? b : ' ');\n  }\n}\n///////////////////////////////////////////////////////////////////////////////\n\nEEStore *EEStore::eeStore = NULL;\nint EEStore::eeAddress = 0;\n#endif\n"
  },
  {
    "path": "EEStore.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef DISABLE_EEPROM\n#ifndef EEStore_h\n#define EEStore_h\n\n#include <Arduino.h>\n\n#if defined(ARDUINO_ARCH_SAMC)\n#include <SparkFun_External_EEPROM.h>\nextern ExternalEEPROM EEPROM;\n#else\n#include <EEPROM.h>\n#endif\n\n#define EESTORE_ID \"DCC++1\"\n\nstruct EEStoreData{\n  char id[sizeof(EESTORE_ID)];\n  uint16_t nTurnouts;\n  uint16_t nSensors;\n  uint16_t nOutputs;\n};\n\nstruct EEStore{\n  static EEStore *eeStore;\n  EEStoreData data;\n  static int eeAddress;\n  static void init();\n  static void reset();\n  static int pointer();\n  static void advance(int);\n  static void store();\n  static void clear();\n  static void dump(int);\n};\n\n#endif\n#endif // DISABLE_EEPROM\n"
  },
  {
    "path": "EXRAIL.h",
    "content": "/*\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef EXRAIL_H\n#define EXRAIL_H\n\n#if defined(EXRAIL_ACTIVE)\n #include \"EXRAIL2.h\"\n\n  class RMFT {\n    public:\n      static void inline begin() {RMFT2::begin();}\n      static void inline loop() {RMFT2::loop();}\n  };\n\n  #include \"EXRAILMacros.h\"\n  \n#else \n  // Dummy RMFT \n  class RMFT {\n    public:\n      static void inline begin() {}\n      static void inline loop() {}\n  };\n#endif\n#endif\n"
  },
  {
    "path": "EXRAIL2.cpp",
    "content": "/*\n *  © 2024 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021-2023 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  © 2025 Morten Nielsen\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* EXRAILPlus planned FEATURE additions\n   F1. [DONE] DCC accessory packet opcodes (short and long form)\n   F2. [DONE] ONAccessory catchers \n   F3. [DONE] Turnout descriptions for Withrottle\n   F4. [DONE] Oled announcements (depends on HAL)\n   F5. [DONE] Withrottle roster info\n   F6. Multi-occupancy semaphore\n   F7. [DONE see AUTOSTART] Self starting sequences\n   F8. Park/unpark\n   F9.  [DONE] Analog drive\n   F10. [DONE] Alias anywhere\n   F11. [DONE]EXRAIL/ENDEXRAIL unnecessary\n   F12. [DONE] Allow guarded code (as effect of ALIAS anywhere)\n   F13. [DONE] IFGTE/IFLT function  \n  */\n/* EXRAILPlus planned TRANSPARENT additions\n   T1. [DONE] RAM based fast lookup for sequences ON* event catchers and signals.\n   T2. Extend to >64k\n  */\n\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"EXRAIL2.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"DIAG.h\"\n#include \"WiThrottle.h\"\n#include \"DCCEXParser.h\"\n#include \"Turnouts.h\"\n#include \"CommandDistributor.h\"\n#include \"TrackManager.h\"\n#include \"Turntables.h\"\n#include \"IODevice.h\"\n#include \"EXRAILSensor.h\"\n#include \"Stash.h\"\n#include \"DCCConsist.h\"\n\n\n// One instance of RMFT clas is used for each \"thread\" in the automation.\n// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.\n// The threads exist in a ring, each time through loop() the next thread in the ring is serviced.\n\n// Statics \nconst int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track\nint16_t RMFT2::progtrackLocoId;  // used for callback when detecting a loco on prog track\nbool RMFT2::diag=false;      // <D EXRAIL ON>  \nRMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring.\nRMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.\n // when pausingTask is set, that is the ONLY task that gets any service,\n // and all others will have their locos stopped, then resumed after the pausing task resumes.\nbyte RMFT2::flags[MAX_FLAGS];\nPrint * RMFT2::LCCSerial=0;\nLookList *  RMFT2::routeLookup=NULL;\nLookList *  RMFT2::signalLookup=NULL;\nLookList *  RMFT2::onThrowLookup=NULL;\nLookList *  RMFT2::onCloseLookup=NULL;\nLookList *  RMFT2::onActivateLookup=NULL;\nLookList *  RMFT2::onDeactivateLookup=NULL;\nLookList *  RMFT2::onRedLookup=NULL;\nLookList *  RMFT2::onAmberLookup=NULL;\nLookList *  RMFT2::onGreenLookup=NULL;\nLookList *  RMFT2::onChangeLookup=NULL;\nLookList *  RMFT2::onClockLookup=NULL;\n#ifndef IO_NO_HAL\nLookList *  RMFT2::onRotateLookup=NULL;\n#endif\nLookList *  RMFT2::onOverloadLookup=NULL;\nLookList *  RMFT2::onBlockEnterLookup=NULL;\nLookList *  RMFT2::onBlockExitLookup=NULL;\n#ifdef BOOSTER_INPUT\nLookList *  RMFT2::onRailSyncOnLookup=NULL;\nLookList *  RMFT2::onRailSyncOffLookup=NULL;\n#endif\nbyte * RMFT2::routeStateArray=nullptr; \nconst FSH  * * RMFT2::routeCaptionArray=nullptr; \n\n\n// getOperand instance version, uses progCounter from instance.\nuint16_t RMFT2::getOperand(byte n) {\n  return getOperand(progCounter,n);\n}\n\n// getOperand static version, must be provided prog counter from loop etc.\nuint16_t RMFT2::getOperand(int progCounter,byte n) {\n  int offset=progCounter+1+(n*3);\n  byte lsb=GETHIGHFLASH(RouteCode,offset);\n  byte msb=GETHIGHFLASH(RouteCode,offset+1);\n  return msb<<8|lsb;\n}\n\nLookList::LookList(int16_t size) {\n  m_size=size;\n  m_loaded=0;\n  m_chain=nullptr;\n  if (size) {\n    m_lookupArray=new int16_t[size];\n    m_resultArray=new int16_t[size];\n  }\n}\n\nvoid LookList::add(int16_t lookup, int16_t result) {\n  if (m_loaded==m_size) return; // and forget\n  m_lookupArray[m_loaded]=lookup;\n  m_resultArray[m_loaded]=result;\n  m_loaded++;\n}\n\nint16_t LookList::find(int16_t value) {\n  for (int16_t i=0;i<m_size;i++) {\n    if (m_lookupArray[i]==value) return m_resultArray[i];\n  }\n  return m_chain ?  m_chain->find(value)  :-1;\n}\nvoid LookList::chain(LookList * chain) {\n  m_chain=chain;\n}\nvoid LookList::handleEvent(const FSH* reason,int16_t id, int16_t loco) {\n  // New feature... create multiple ONhandlers\n  for (int i=0;i<m_size;i++) \n    if (m_lookupArray[i]==id)\n       RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i],loco);\n}\n\n\nvoid LookList::stream(Print * _stream) {\n  for (int16_t i=0;i<m_size;i++) {\n    _stream->print(\" \");\n    _stream->print(m_lookupArray[i]);\n  }\n}\n\nint16_t LookList::findPosition(int16_t value) {\n  for (int16_t i=0;i<m_size;i++) {\n    if (m_lookupArray[i]==value) return i;\n  }\n  return -1;\n}\nint16_t LookList::size() {\n   return m_size;\n}\n\nLookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {\n  int progCounter;\n  int16_t count=0;\n  // find size for list\n  for (progCounter=0;; SKIPOP) {\n    byte opcode=GET_OPCODE;\n    if (opcode==OPCODE_ENDEXRAIL) break;\n    if (opcode==op1 || opcode==op2 || opcode==op3) count++;\n  }\n  // create list \n  LookList* list=new LookList(count);\n  if (count==0) return list;\n\n  for (progCounter=0;; SKIPOP) {\n    byte opcode=GET_OPCODE;\n    if (opcode==OPCODE_ENDEXRAIL) break;\n    if (opcode==op1 || opcode==op2 || opcode==op3)  list->add(getOperand(progCounter,0),progCounter);   \n  }\n  return list;\n}\n\n/* static */ void RMFT2::begin() {\n\n  //DIAG(F(\"EXRAIL RoutCode at =%P\"),RouteCode);\n    \n  bool saved_diag=diag;\n  diag=true;\n  DCCEXParser::setRMFTFilter(RMFT2::ComandFilter);\n  for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;\n  \n  // create lookups\n  routeLookup=LookListLoader(OPCODE_ROUTE, OPCODE_AUTOMATION);\n  routeLookup->chain(LookListLoader(OPCODE_SEQUENCE));\n  if (compileFeatures && FEATURE_ROUTESTATE) {\n    routeStateArray=(byte *)calloc(routeLookup->size(),sizeof(byte));\n    routeCaptionArray=(const FSH * *)calloc(routeLookup->size(),sizeof(const FSH *));\n  }\n  onThrowLookup=LookListLoader(OPCODE_ONTHROW);\n  onCloseLookup=LookListLoader(OPCODE_ONCLOSE);\n  onActivateLookup=LookListLoader(OPCODE_ONACTIVATE);\n  onDeactivateLookup=LookListLoader(OPCODE_ONDEACTIVATE);\n  onChangeLookup=LookListLoader(OPCODE_ONCHANGE);\n  onClockLookup=LookListLoader(OPCODE_ONTIME);\n#ifndef IO_NO_HAL\n  onRotateLookup=LookListLoader(OPCODE_ONROTATE);\n#endif\n  onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);\n\n  if (compileFeatures & FEATURE_BLOCK) {\n    onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);\n    onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);\n  }\n#ifdef BOOSTER_INPUT\n  onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);\n  onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);\n#endif\n\n  // onLCCLookup is not the same so not loaded here. \n\n  // Second pass startup, define any turnouts or servos, set signals red\n  // add sequences onRoutines to the lookups\n  if (compileFeatures & FEATURE_SIGNAL) {\n    \n    onRedLookup=LookListLoader(OPCODE_ONRED);\n    onAmberLookup=LookListLoader(OPCODE_ONAMBER);\n    onGreenLookup=LookListLoader(OPCODE_ONGREEN);\n    // Load the signal lookup with slot numbers in the signal table\n    int signalCount=0; \n    for (int16_t slot=0;;slot++) {\n        SIGNAL_DEFINITION signal=getSignalSlot(slot);\n        DIAG(F(\"Signal s=%d id=%d t=%d\"),slot,signal.id,signal.type);\n        if (signal.type==sigtypeNoMoreSignals) break;\n        if (signal.type==sigtypeContinuation) continue;\n        signalCount++;\n    }    \n    signalLookup=new LookList(signalCount);\n    for (int16_t slot=0;;slot++) {\n        SIGNAL_DEFINITION signal=getSignalSlot(slot);\n        if (signal.type==sigtypeNoMoreSignals) break;\n        if (signal.type==sigtypeContinuation) continue;\n        signalLookup->add(signal.id,slot);\n        doSignal(signal.id, SIGNAL_RED);\n    }    \n   }\n\n  int progCounter;\n  for (progCounter=0;; SKIPOP){\n    byte opcode=GET_OPCODE;\n    if (opcode==OPCODE_ENDEXRAIL) break;\n    VPIN operand=getOperand(progCounter,0);\n    \n    switch (opcode) {\n    case OPCODE_AT:\n    case OPCODE_ATTIMEOUT2:\n    case OPCODE_AFTER:\n    case OPCODE_IF:\n    case OPCODE_IFNOT: {\n      int16_t pin = (int16_t)operand;\n      if (pin<0) pin = -pin;\n      DIAG(F(\"EXRAIL input VPIN %u\"),pin);\n      IODevice::configureInput((VPIN)pin,true);\n      break;\n    }\n    \n    case OPCODE_ATGTE:\n    case OPCODE_ATLT:\n    case OPCODE_IFGTE:\n    case OPCODE_IFLT:\n    case OPCODE_IFBITMAP_ALL:\n    case OPCODE_IFBITMAP_ANY:\n    case OPCODE_DRIVE: {\n      DIAG(F(\"EXRAIL analog input VPIN %u\"),(VPIN)operand);\n      IODevice::configureAnalogIn((VPIN)operand);\n      break;\n    }\n\n    case OPCODE_ONSENSOR:\n      if (compileFeatures & FEATURE_SENSOR) \n        new EXRAILSensor(operand,progCounter+3,true );\n      break;\n    case OPCODE_ONBITMAP:\n      if (compileFeatures & FEATURE_SENSOR) \n        new EXRAILSensor(operand,progCounter+3,true, true );\n      break;\n    case OPCODE_ONBUTTON:\n      if (compileFeatures & FEATURE_SENSOR) \n        new EXRAILSensor(operand,progCounter+3,false );\n      break;\n    case OPCODE_TURNOUT: {\n      VPIN id=operand;\n      int addr=getOperand(progCounter,1);\n      byte subAddr=getOperand(progCounter,2);\n      Turnout *t = DCCTurnout::create(id,addr,subAddr);\n      if (t) setTurnoutHiddenState(t);\n      break;\n    }\n\n    case OPCODE_SERVOTURNOUT: {\n      VPIN id=operand;\n      VPIN pin=getOperand(progCounter,1);\n      int activeAngle=getOperand(progCounter,2);\n      int inactiveAngle=getOperand(progCounter,3);\n      int profile=getOperand(progCounter,4);\n      Turnout *t = ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile);\n      if (t) setTurnoutHiddenState(t);\n      break;\n    }\n\n    case OPCODE_PINTURNOUT: {\n      VPIN id=operand;\n      VPIN pin=getOperand(progCounter,1);\n      Turnout *t = VpinTurnout::create(id,pin);\n      if (t) setTurnoutHiddenState(t);\n      break;\n    }\n\n#ifndef IO_NO_HAL\n    case OPCODE_DCCTURNTABLE: {\n      VPIN id=operand;\n      int home=getOperand(progCounter,1);\n      Turntable *tto = DCCTurntable::create(id);\n      if (tto) {\n\tsetTurntableHiddenState(tto);\n\ttto->addPosition(0,0,home);\n      }\n      break;\n    }\n\n    case OPCODE_EXTTTURNTABLE: {\n      VPIN id=operand;\n      VPIN pin=getOperand(progCounter,1);\n      int home=getOperand(progCounter,2);\n      Turntable *tto = EXTTTurntable::create(id,pin);\n      if (tto) {\n         setTurntableHiddenState(tto);\n         tto->addPosition(0,0,home);\n      } else {\n         DIAG(F(\"Create EXTTTURNTABLE %d %d FAILED\"), id, pin);\n      }\n      break;\n    }\n\n    case OPCODE_TTADDPOSITION: {\n      VPIN id=operand;\n      int position=getOperand(progCounter,1);\n      int value=getOperand(progCounter,2);\n      int angle=getOperand(progCounter,3);\n      Turntable *tto=Turntable::get(id);\n      if (tto) tto->addPosition(position,value,angle);\n      break;\n    }\n#endif\n\n    case OPCODE_AUTOSTART:\n      // automatically create a task from here at startup.\n      // Removed if (progCounter>0) check 4.2.31 because \n      // default start it top of file is now removed. .   \n      new RMFT2(progCounter);\n      break;\n      \n    default: // Ignore\n      break;\n    }\n  }\n  SKIPOP; // include ENDROUTES opcode\n  \n  DIAG(F(\"EXRAIL %db, fl=%d\"),progCounter,MAX_FLAGS);\n\n  // Removed for 4.2.31  new RMFT2(0); // add the startup route\n  diag=saved_diag;\n}\n\nvoid RMFT2::setTurnoutHiddenState(Turnout * t) {\n  // turnout descriptions are in low flash F strings\n  const FSH *desc = getTurnoutDescription(t->getId());\n  if (desc) t->setHidden(GETFLASH(desc)==0x01);\n}\n\n#ifndef IO_NO_HAL\nvoid RMFT2::setTurntableHiddenState(Turntable * tto) {\n  const FSH *desc = getTurntableDescription(tto->getId());\n  if (desc) tto->setHidden(GETFLASH(desc)==0x01);\n}\n#endif\n\nchar RMFT2::getRouteType(int16_t id) {\n  int16_t progCounter=routeLookup->find(id);\n  if (progCounter>=0) {\n    byte type=GET_OPCODE; \n    if (type==OPCODE_ROUTE) return 'R';\n    if (type==OPCODE_AUTOMATION) return 'A';\n  }\n  return 'X';\n}\n\n\nRMFT2::RMFT2(int progCtr, int16_t _loco, bool _invert) {\n  progCounter=progCtr;\n\n  // get an unused  task id from the flags table\n  taskId=255; // in case of overflow\n  for (int f=0;f<MAX_FLAGS;f++) {\n    if (!getFlag(f,TASK_FLAG)) {\n      taskId=f;\n      setFlag(f, TASK_FLAG);\n      break;\n    }\n  }\n  delayTime=0;\n  loco=_loco;\n  invert=_invert;\n  blinkState=not_blink_task;\n  stackDepth=0;\n  onEventStartPosition=-1; // Not handling an ONxxx \n\n  // chain into ring of RMFTs\n  if (loopTask==NULL) {\n    loopTask=this;\n    next=this;\n  } else {\n    next=loopTask->next;\n    loopTask->next=this;\n  }\n}\n\n\nRMFT2::~RMFT2() {\n  // estop my loco if this is not an ONevent \n  // (prevents DONE stopping loco at the end of an\n  //   ONBLOCKENTER or ONBLOCKEXIT )\n  if (loco>0 && this->onEventStartPosition==-1) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));\n  setFlag(taskId,0,TASK_FLAG); // we are no longer using this id\n  if (next==this)\n    loopTask=NULL;\n  else\n    for (RMFT2* ring=next;;ring=ring->next)\n      if (ring->next == this) {\n\tring->next=next;\n\tloopTask=next;\n\tbreak;\n      }\n}\n\nvoid RMFT2::createNewTask(int route, uint16_t cab) {\n      int pc=routeLookup->find(route);\n      if (pc<0) return;\n      new RMFT2(pc,cab);\n}\n\n\nbool RMFT2::readSensor(uint16_t sensorId) {\n  // Exrail operands are unsigned but we need the signed version as inserted by the macros.\n  int16_t sId=(int16_t) sensorId;\n\n  VPIN vpin=abs(sId);\n  if (getFlag(vpin,LATCH_FLAG)) return true; // latched on\n  \n  // negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting)\n  bool s= IODevice::read(vpin) ^ (sId<0);\n  if (s && diag) DIAG(F(\"EXRAIL Sensor %d hit\"),sId);\n  return s;\n}\n\n// This skips to the end of an if block, or to the ELSE within it.\nbool RMFT2::skipIfBlock() {\n  // returns false if killed\n  short nest = 1;\n  while (nest > 0) {\n    SKIPOP;\n    byte opcode =  GET_OPCODE;\n    // all other IF type commands increase the nesting level\n    if (opcode>IF_TYPE_OPCODES) nest++;\n    else switch(opcode) {\n      case OPCODE_ENDEXRAIL:\n        kill(F(\"missing ENDIF\"), nest);\n        return false;\n    \n      case OPCODE_ENDIF:\n        nest--;\n        break;\n    \n      case OPCODE_ELSE:\n        // if nest==1 then this is the ELSE for the IF we are skipping\n        if (nest==1) nest=0; // cause loop exit and return after ELSE\n        break;\n    default:\n      break;\n    }\n  }\n  return true;\n}\n\n\n\n/* static */ void RMFT2::readLocoCallback(int16_t cv) {\n  if (cv <= 0) {\n    DIAG(F(\"CV read error\"));\n    progtrackLocoId = -1;\n    return;\n  }\n  if (cv & LONG_ADDR_MARKER) {               // maker bit indicates long addr\n    progtrackLocoId = cv ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr\n    if (progtrackLocoId <= HIGHEST_SHORT_ADDR ) {     // out of range for long addr\n      DIAG(F(\"Long addr %d <= %d unsupported\\n\"), progtrackLocoId, HIGHEST_SHORT_ADDR);\n      progtrackLocoId = -1;\n    }\n  } else {\n    progtrackLocoId=cv;\n  }\n}\n\nvoid RMFT2::pause() {\n  if (loco)\n    pauseSpeed=DCC::getThrottleSpeedByte(loco);\n}\nvoid RMFT2::resume() {\n  if (loco)\n    DCC::setThrottle(loco,pauseSpeed & 0x7f, pauseSpeed & 0x80);\n}\n\nvoid RMFT2::loop() {\n  if (compileFeatures & FEATURE_SENSOR) \n      EXRAILSensor::checkAll();\n\n  // Round Robin call to a RMFT task each time\n  if (loopTask==NULL) return;\n  loopTask=loopTask->next;\n  if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2();\n}\n\nbool RMFT2::skipIf;\n\nvoid RMFT2::loop2() {\n  if (delayTime!=0 && millis()-delayStart < delayTime) return;\n\n  // special stand alone blink task\n  if (compileFeatures & FEATURE_BLINK) { \n    if (blinkState==blink_low) {\n      IODevice::write(blinkPin,HIGH);\n      blinkState=blink_high;\n      delayMe(getOperand(1));\n      return;\n    }\n    if (blinkState==blink_high) {\n      IODevice::write(blinkPin,LOW);\n      blinkState=blink_low;\n      delayMe(getOperand(2));\n      return;\n    }\n  }\n  \n  // Normal progstep following tasks continue here.\n  byte opcode = GET_OPCODE;\n  int16_t operand =  getOperand(0);\n\n  // skipIf will get set to indicate a failing IF condition \n  skipIf=false; \n\n  // if (diag) DIAG(F(\"RMFT2 %d %d\"),opcode,operand);\n  // Attention: Returning from this switch leaves the program counter unchanged.\n  //            This is used for unfinished waits for timers or sensors.\n  //            Breaking from this switch will step to the next step in the route.\n  switch ((OPCODE)opcode) {\n\n  case OPCODE_THROW:\n    Turnout::setClosed(operand, false);\n    break;\n    \n  case OPCODE_CLOSE:\n    Turnout::setClosed(operand, true);\n    break;\n\n  case OPCODE_TOGGLE_TURNOUT:\n    Turnout::setClosed(operand, Turnout::isThrown(operand));\n    break;\n\n#ifndef IO_NO_HAL\n  case OPCODE_ROTATE:\n    uint8_t activity;\n    activity=getOperand(2);\n    Turntable::setPosition(operand,getOperand(1),activity);\n    break;\n#endif\n\n  case OPCODE_REV:\n    if (loco) DCC::setThrottle(loco,operand,invert);\n    break;\n    \n  case OPCODE_FWD:\n    if (loco) DCC::setThrottle(loco,operand,!invert);\n    break;\n      \n  case OPCODE_SPEED:\n    if (loco) DCC::setThrottle(loco,operand,DCC::getThrottleDirection(loco));\n    break;\n  \n  case OPCODE_SAVE_SPEED:\n    if (loco) DCC::saveSpeed(loco);\n    break;\n\n    case OPCODE_RESTORE_SPEED:\n    if (loco) DCC::restoreSpeed(loco);\n    break;\n  \n  case OPCODE_XSAVE_SPEED:\n    DCC::saveSpeed(operand);\n    break;\n\n    case OPCODE_XRESTORE_SPEED:\n    DCC::restoreSpeed(operand);\n    break;\n  \n  case OPCODE_SPEEDUP:\n    if (loco) {\n      int8_t   speed=DCC::getThrottleSpeed(loco);\n\n      // do nothing if speed is 1 (emergency stop) or -1 (loco not found)\n      if ((speed != 1) && (speed != -1))\n      {\n        // handle overflow\n        int16_t newspeed = static_cast<int16_t>(speed) + operand;\n        if (newspeed > 127) {\n            speed = 127;\n        } else if (newspeed == 1) {\n            speed = 2; // skip emergency stop\n        } else {\n            speed = static_cast<int8_t>(newspeed);\n        }\n        DCC::setThrottle(loco,speed,DCC::getThrottleDirection(loco));\n      }\n    }\n    break;\n\n  case OPCODE_SPEED_REL:\n    {\n      if (!loco) break;\n      auto  speed=DCC::getThrottleSpeed(loco);\n      // do nothing if speed is 0 (stop), 1 (emergency stop) or -1 (loco not found)\n      if (speed<2) break; // cant \n      // handle overflow\n      auto newspeed = (speed * operand)/100;\n        if (newspeed > 127) {\n            speed = 127;\n        } else if (newspeed == 1) {\n            speed = 0; // skip emergency stop\n        } else {\n            speed = static_cast<int8_t>(newspeed);\n        }\n        DCC::setThrottle(loco,speed,DCC::getThrottleDirection(loco));\n    }\n    break;\n\n  case OPCODE_SLOWDOWN:\n    if (loco) {\n      int8_t  speed=DCC::getThrottleSpeed(loco);\n\n      // do nothing if speed is 1 (emergency stop) or -1 (loco not found)\n      if ((speed != 1) && (speed != -1))\n      {\n        // handle underflow\n        int16_t newspeed = static_cast<int16_t>(speed) - operand;\n        if (newspeed < 2) {\n            speed = 0; // stop\n        } else {\n            speed = static_cast<int8_t>(newspeed);\n        }\n        DCC::setThrottle(loco,speed,DCC::getThrottleDirection(loco));\n      }\n    }\n    break;\n\n  case OPCODE_MOMENTUM:\n    DCC::setMomentum(loco,operand,getOperand(1));\n    break;\n    \n  case OPCODE_ESTOPALL:\n    if(operand==0) DCC::estopAll(); // all locos stop\n    else DCC::estopLock(operand==1); // stop and lock/unlock\n    break;\n    \n  case OPCODE_FORGET:\n    if (loco!=0) {\n      DCC::forgetLoco(loco);\n      loco=0; \n    } \n    break;\n\n  case OPCODE_CONSIST:\n      if (operand==0) DCCConsist::deleteAnyConsist(loco);\n      else if (!DCCConsist::addLocoToConsist(loco, abs(operand), operand<0)) {\n          DIAG(F(\"EXRAIL Failed to add Loco %d to consist %d\"), abs(operand),loco);\n        }\n    break;\n\n  case OPCODE_INVERT_DIRECTION:\n    invert= !invert;\n    break;\n    \n  case OPCODE_RESERVE:\n    if (getFlag(operand,SECTION_FLAG)) {\n      if (loco) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));\n      delayMe(500);\n      return;\n    }\n    setFlag(operand,SECTION_FLAG);\n    break;\n    \n  case OPCODE_FREE:\n    setFlag(operand,0,SECTION_FLAG);\n    break;\n  \n  case OPCODE_FREEALL:\n    for (int i=0;i<MAX_FLAGS;i++) setFlag(i,0,SECTION_FLAG);\n    break;\n  \n  case OPCODE_AT:\n    blinkState=not_blink_task;\n    if (readSensor(operand)) break;\n    delayMe(50);\n    return;\n    \n  case OPCODE_ATGTE: // wait for analog sensor>= value\n    blinkState=not_blink_task;\n    if (IODevice::readAnalogue(operand) >= (int)(getOperand(1))) break;\n    delayMe(50);\n    return;\n    \n  case OPCODE_ATLT: // wait for analog sensor < value\n    blinkState=not_blink_task;\n    if (IODevice::readAnalogue(operand) < (int)(getOperand(1))) break;\n    delayMe(50);\n    return;\n      \n  case OPCODE_ATTIMEOUT1:   // ATTIMEOUT(vpin,timeout) part 1\n    timeoutStart=millis();\n    blinkState=not_blink_task;\n    break;\n    \n  case OPCODE_ATTIMEOUT2:\n    if (readSensor(operand)) break; // success without timeout\n    if (millis()-timeoutStart > 100*getOperand(1)) {\n      blinkState=at_timeout;\n      break; // and drop through\n    }\n    delayMe(50);\n    return;\n    \n  case OPCODE_IFTIMEOUT: // do next operand if timeout flag set\n    skipIf=blinkState!=at_timeout;\n    break;\n    \n  case OPCODE_AFTER: // waits for sensor to hit and then remain off for x mS. \n    // Note, this must come after an AT operation, which is \n    // automatically inserted by the AFTER macro. \n    if (readSensor(operand)) {\n      // reset timer and keep waiting\n      waitAfter=millis();\n      delayMe(50);\n      return;\n    }\n    if (millis()-waitAfter < getOperand(1) ) return;\n    break;\n\n  case OPCODE_AFTEROVERLOAD: // waits for the power to be turned back on - either by power routine or button\n    if (!TrackManager::isPowerOn(operand)) {\n      // reset timer to half a second and keep waiting\n      waitAfter=millis();\n      delayMe(50);\n      return;\n    }\n    if (millis()-waitAfter < 500 ) return;\n    break;\n\n  case OPCODE_LATCH:\n    setFlag(operand,LATCH_FLAG);\n    break;\n    \n  case OPCODE_UNLATCH:\n    setFlag(operand,0,LATCH_FLAG);\n    break;\n\n  case OPCODE_SET:\n  case OPCODE_RESET:\n    { \n      auto count=getOperand(1);\n      for (uint16_t i=0;i<count;i++) {\n        killBlinkOnVpin(operand+i);\n        IODevice::write(operand+i,opcode==OPCODE_SET);\n      }\n    }\n    break;\n  \n  case OPCODE_BLINK: \n     // Start a new task to blink this vpin\n     killBlinkOnVpin(operand);\n     {\n      auto newtask=new RMFT2(progCounter);\n      newtask->blinkPin=operand;\n      newtask->blinkState=blink_low; // will go high on first call\n     }\n     break; \n\n  case OPCODE_PAUSE:\n    DCC::estopAll();  // pause all locos on the track\n    pausingTask=this;\n    break;\n\n  case OPCODE_POM:\n    if (loco) DCC::writeCVByteMain(loco, operand, getOperand(1));\n    break;\n\n  case OPCODE_XPOM:\n    DCC::writeCVByteMain(operand, getOperand(1), getOperand(2));\n    break;\n\n  case OPCODE_POWEROFF:\n    TrackManager::setPower(POWERMODE::OFF);\n    TrackManager::setJoin(false);\n    break;\n  \n  case OPCODE_SET_POWER:\n      // operand is TRACK_POWER , trackid\n        //byte thistrack=getOperand(1);\n        switch (operand) {\n          case TRACK_POWER_0:\n            TrackManager::setTrackPower(POWERMODE::OFF, getOperand(1));\n          break;\n          case TRACK_POWER_1:\n            TrackManager::setTrackPower(POWERMODE::ON, getOperand(1));\n          break;\n        }\n\n    break;\n\n  case OPCODE_SET_TRACK:\n      // operand is trackmode<<8 | track id\n      // If DC/DCX use  my loco for DC address \n      {\n        TRACK_MODE mode = (TRACK_MODE)(operand>>8);\n        int16_t cab=(mode & TRACK_MODE_DC) ? loco : 0;\n        TrackManager::setTrackMode(operand & 0x0F, mode, cab);\n      }\n      break; \n\n  case OPCODE_SETFREQ:\n      // Frequency is default 0, or 1, 2,3\n      DCC::setDCFreq(loco,operand);\n      break;\n\n  case OPCODE_RESUME:\n    pausingTask=NULL;\n    resume();\n    for (RMFT2 * t=next; t!=this;t=t->next) t->resume();\n    break;\n    \n  case OPCODE_IF: // do next operand if sensor set\n    skipIf=!readSensor(operand);\n    break;\n    \n  case OPCODE_ELSE: // skip to matching ENDIF\n    skipIf=true;\n    break;\n    \n  case OPCODE_IFGTE: // do next operand if sensor>= value\n    skipIf=IODevice::readAnalogue(operand)<(int)(getOperand(1));\n    break;\n    \n  case OPCODE_IFLT: // do next operand if sensor< value\n    skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1));\n    break;\n  \n  case OPCODE_IFBITMAP_ALL: // do next operand if sensor & mask == mask\n    skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) != getOperand(1);\n    break;\n  \n  case OPCODE_IFBITMAP_ANY: // do next operand if sensor & mask !=0\n    skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) == 0;\n    break;\n    \n  case OPCODE_IFNOT: // do next operand if sensor not set\n    skipIf=readSensor(operand);\n    break;\n\n  case OPCODE_IFRE: // do next operand if rotary encoder != position\n    skipIf=IODevice::readAnalogue(operand)!=(int)(getOperand(1));\n    break;\n    \n  case OPCODE_IFRANDOM: // do block on random percentage\n    skipIf=(uint8_t)micros() >= operand * 255/100;\n    break;\n    \n  case OPCODE_IFRESERVE: // do block if we successfully RERSERVE\n    if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG);\n    else skipIf=true;\n    break;\n    \n  case OPCODE_IFRED: // do block if signal as expected\n    skipIf=!isSignal(operand,SIGNAL_RED);\n    break;\n    \n  case OPCODE_WAIT_WHILE_RED: // do block if signal as expected\n    if (isSignal(operand,SIGNAL_RED)) {\n      if (loco && (DCC::getLocoSpeedByte(loco) & 0x7f)>1) \n          DCC::setThrottle(loco,0,DCC::getThrottleDirection(loco));\n      delayMe(500);\n      return;\n    }\n    break;\n    \n  case OPCODE_IFAMBER: // do block if signal as expected\n    skipIf=!isSignal(operand,SIGNAL_AMBER);\n    break;\n    \n  case OPCODE_IFGREEN: // do block if signal as expected\n    skipIf=!isSignal(operand,SIGNAL_GREEN);\n    break;\n    \n  case OPCODE_IFTHROWN:\n    skipIf=Turnout::isClosed(operand);\n    break;\n    \n  case OPCODE_IFCLOSED:\n    skipIf=Turnout::isThrown(operand);\n    break;\n    \n  case OPCODE_IFSTASH:\n    skipIf=Stash::get(operand)==0;\n    break;\n  \n  case OPCODE_IFSTASHED_HERE:\n    skipIf=(Stash::get(operand) & 0x7FFF)!=loco;\n    break;\n\n#ifndef IO_NO_HAL\n  case OPCODE_IFTTPOSITION: // do block if turntable at this position\n    skipIf=Turntable::getPosition(operand)!=(int)getOperand(1);\n    break;\n#endif\n\n  case OPCODE_ENDIF:\n    break;\n    \n  case OPCODE_DELAYMS:\n    delayMe(operand);\n    break;\n    \n  case OPCODE_DELAY:\n    delayMe(operand*100L);\n    break;\n    \n  case OPCODE_DELAYMINS:\n    delayMe(operand*60L*1000L);\n    break;\n    \n  case OPCODE_RANDWAIT:\n    delayMe(operand==0 ? 0 : (micros()%operand) *100L);\n    break;\n    \n  case OPCODE_RED:\n    doSignal(operand,SIGNAL_RED);\n    break;\n    \n  case OPCODE_AMBER:\n    doSignal(operand,SIGNAL_AMBER);\n    break;\n    \n  case OPCODE_GREEN:\n    doSignal(operand,SIGNAL_GREEN);\n    break;\n    \n  case OPCODE_FON:\n    if (loco) DCC::setFn(loco,operand,true);\n    break;\n\n  case OPCODE_FOFF:\n    if (loco) DCC::setFn(loco,operand,false);\n    break;\n  \n  case OPCODE_FTOGGLE:\n    if (loco) DCC::changeFn(loco,operand);\n    break;\n    \n  case OPCODE_DRIVE:\n    {\n      // Non functional but reserved \n      break;\n    }\n    \n  case OPCODE_XFON:\n    DCC::setFn(operand,getOperand(1),true);\n    break;\n    \n  case OPCODE_XFOFF:\n    DCC::setFn(operand,getOperand(1),false);\n    break;\n\n  case OPCODE_XFTOGGLE:\n    DCC::changeFn(operand,getOperand(1));\n    break;\n    \n  case OPCODE_XFWD:\n    DCC::setThrottle(operand,getOperand(1), true);\n    break;\n\n  case OPCODE_XREV:\n    DCC::setThrottle(operand,getOperand(1), false);\n    break;\n\n  case OPCODE_DCCACTIVATE: {\n    // operand is address<<3 | subaddr<<1 | active\n    int16_t addr=operand>>3;\n    int16_t subaddr=(operand>>1) & 0x03;\n    bool active=operand & 0x01;\n    DCC::setAccessory(addr,subaddr,active);\n    break;\n  }\n   case OPCODE_ASPECT: {\n    // operand is address<<5 |  value\n    int16_t address=operand>>5;\n    byte aspect=operand & 0x1f;\n    if (!signalAspectEvent(address,aspect))\n      DCC::setExtendedAccessory(address,aspect);\n    break;\n  }\n    \n  case OPCODE_FOLLOW:\n    progCounter=routeLookup->find(operand);\n    if (progCounter<0) kill(F(\"FOLLOW unknown\"), operand);\n    return;\n    \n  case OPCODE_CALL:\n    if (stackDepth==MAX_STACK_DEPTH) {\n      kill(F(\"CALL stack\"), stackDepth);\n      return;\n    }\n    callStack[stackDepth++]=progCounter+3;\n    progCounter=routeLookup->find(operand);\n    if (progCounter<0) kill(F(\"CALL unknown\"),operand);\n    return;\n   \n  \n  case OPCODE_RANDOM_FOLLOW:\n    // operand is number to choose from\n    { \n      auto newroute=getOperand(1+(millis()%operand));\n      progCounter=routeLookup->find(newroute);\n      if (progCounter<0) kill(F(\"RANDOM_FOLLOW unknown\"), newroute); \n    }\n    return;\n\t\n\t case OPCODE_RANDOM_CALL:\n    // operand is number to choose from\n    if (stackDepth==MAX_STACK_DEPTH) {\n      kill(F(\"CALL stack\"), stackDepth);\n      return;\n    }\n    {\n      auto newroute=getOperand(1+(millis()%operand));\n      \n      // return position is after the RANDOM_CALL + all its operands\n      callStack[stackDepth++]=progCounter+3*(operand+1);\n      progCounter=routeLookup->find(newroute);\n      if (progCounter<0) kill(F(\"CALL unknown\"),newroute);\n    }\n    return;\n  \n  case OPCODE_RETURN:\n    if (stackDepth==0) {\n      kill(F(\"RETURN stack\"));\n      return;\n    }\n    progCounter=callStack[--stackDepth];\n    return;\n    \n  case OPCODE_ENDTASK:\n  case OPCODE_ENDEXRAIL:\n    kill();\n    return;\n\n  case OPCODE_KILLALL:\n    while(loopTask) loopTask->kill(F(\"KILLALL\"));\n    return;\n\n#ifndef DISABLE_PROG\n  case OPCODE_JOIN:\n    TrackManager::setJoin(true);\n    TrackManager::setMainPower(POWERMODE::ON);\n    TrackManager::setProgPower(POWERMODE::ON);\n    break;\n\n  case OPCODE_UNJOIN:\n    TrackManager::setJoin(false);\n    break;\n\n  case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes\n    progtrackLocoId=LOCO_ID_WAITING;  // Nothing found yet\n    DCC::getDriveawayLocoId(readLocoCallback);\n    break;\n    \n  case OPCODE_READ_LOCO2:\n    if (progtrackLocoId==LOCO_ID_WAITING) {\n      delayMe(100);\n      return; // still waiting for callback\n    }\n    \n    // At failed read will result in loco == -1\n    // which is intended so it can be checked\n    // from within EXRAIL\n    loco=progtrackLocoId;\n    invert=false;\n    break;\n#endif\n\n  case OPCODE_POWERON:\n    TrackManager::setMainPower(POWERMODE::ON);\n    TrackManager::setJoin(false);\n    break;\n    \n  case OPCODE_START:\n    {\n      int newPc=routeLookup->find(operand);\n      if (newPc<0) break;\n      new RMFT2(newPc);\n    }\n    break;\n\n  case OPCODE_START_SHARED:\n    {\n      int newPc=routeLookup->find(operand);\n      if (newPc<0) break;\n      new RMFT2(newPc,loco, invert); // create new task and share loco\n    }\n    break;\n\n  case OPCODE_START_SEND:\n    {\n      int newPc=routeLookup->find(operand);\n      if (newPc<0) break;\n      new RMFT2(newPc,loco, invert); // create new task and send loco exclusive\n      loco = 0;\n    }\n    break;\n    \n  case OPCODE_SENDLOCO:  // cab, route\n    {\n      int newPc=routeLookup->find(getOperand(1));\n      if (newPc<0) break;\n      new RMFT2(newPc,operand); // create new task\n    }\n    break;\n    \n  case OPCODE_SETLOCO:\n    {\n      loco=operand;\n      invert=false;\n    }\n    break;\n\n  case OPCODE_LCC:  // short form LCC\n      if ((compileFeatures & FEATURE_LCC) && LCCSerial) \n          StringFormatter::send(LCCSerial,F(\"<L x%h>\"),(uint16_t)operand);\n       break; \n  \n  case OPCODE_ACON:  // MERG adapter \n  case OPCODE_ACOF: \n      if ((compileFeatures & FEATURE_LCC) && LCCSerial) \n          StringFormatter::send(LCCSerial,F(\"<L x%c%h%h>\"),\n          opcode==OPCODE_ACON?'0':'1',\n          (uint16_t)operand,getOperand(progCounter,1));\n       break; \n\n  case OPCODE_LCCX: // long form LCC\n       if ((compileFeatures & FEATURE_LCC) && LCCSerial)\n            StringFormatter::send(LCCSerial,F(\"<L x%h%h%h%h>\\n\"),\n                 getOperand(progCounter,1),\n                 getOperand(progCounter,2),\n                 getOperand(progCounter,3),\n                 getOperand(progCounter,0)\n                 );    \n        break;  \n    \n  case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration)\n    IODevice::writeAnalogue(operand,getOperand(1),getOperand(2),getOperand(3));\n    break;\n    \n  case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin)\n    if (IODevice::isBusy(operand)) {\n      delayMe(100);\n      return;\n    }\n    break;\n    \n#ifndef IO_NO_HAL\n  case OPCODE_NEOPIXEL: \n    // OPCODE_NEOPIXEL,V([-]vpin),OPCODE_PAD,V(colour_RG),OPCODE_PAD,V(colour_B),OPCODE_PAD,V(count)\n    { \n      VPIN vpin=operand>0?operand:-operand;\n      auto count=getOperand(3);\n      killBlinkOnVpin(vpin,count);\n      IODevice::writeAnalogueRange(vpin,getOperand(1),operand>0,getOperand(2),count);\n    }\n    break;\n  \n  case OPCODE_WAITFORTT:  // OPCODE_WAITFOR,V(turntable_id)\n    if (Turntable::ttMoving(operand)) {\n      delayMe(100);\n      return;\n    }\n    break;\n#endif\n\n/* IFLOCO and PRINT use code generated in printMessage\n   but IFLOCO is recognized as an IF when doing\n    IF/ELSE/ENDINF skipping */\n\n  case OPCODE_IFLOCO:  \n  case OPCODE_PRINT:\n    printMessage(operand);\n    break;\n\n  // Route state management  \n  case OPCODE_ROUTE_HIDDEN:\n    manageRouteState(operand,2);\n    break;   \n  case OPCODE_ROUTE_INACTIVE:\n    manageRouteState(operand,0);\n    break;   \n  case OPCODE_ROUTE_ACTIVE:\n    manageRouteState(operand,1);\n    break;   \n  case OPCODE_ROUTE_DISABLED:\n    manageRouteState(operand,4);\n    break;   \n// Route state management  \n  case OPCODE_IF_ROUTE_HIDDEN:\n    skipIf=!ifRouteState(operand,2);\n    break;   \n  case OPCODE_IF_ROUTE_INACTIVE:\n    skipIf=!ifRouteState(operand,0);\n    break;   \n  case OPCODE_IF_ROUTE_ACTIVE:\n    skipIf=!ifRouteState(operand,1);\n    break;   \n  case OPCODE_IF_ROUTE_DISABLED:\n    skipIf=!ifRouteState(operand,4);\n    break;   \n\n  case OPCODE_STASH:\n    Stash::set(operand,invert? -loco : loco);\n    break; \n\n  case OPCODE_CLEAR_STASH:\n    Stash::clear(operand);\n    break; \n       \n  case OPCODE_CLEAR_ALL_STASH:\n    Stash::clearAll();\n    break;\n\n  case OPCODE_CLEAR_ANY_STASH:\n    if (loco) Stash::clearAny(loco);\n    break;\n\n  case OPCODE_PICKUP_STASH:\n      {\n        auto x=Stash::get(operand);\n        if (x>=0) {\n          loco=x;\n          invert=false;\n        }\n        else {\n          loco=-x;\n          invert=true;    \n        }\n    }\n    break;\n  \n  case OPCODE_ROUTE:\n  case OPCODE_AUTOMATION:\n  case OPCODE_SEQUENCE:\n    //if (diag) DIAG(F(\"EXRAIL begin(%d)\"),operand);\n    break;\n  \n  case OPCODE_BITMAP_INC:\n    IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand)+1);\n    break;\n  case OPCODE_BITMAP_DEC:\n     { int newval=IODevice::readAnalogue(operand)-1;\n       if (newval<0) newval=0;\n       IODevice::writeAnalogue(operand,newval);\n     }\n    break;\n  \n  case OPCODE_BITMAP_AND:\n    IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) & getOperand(1));\n    break;\n  case OPCODE_BITMAP_OR:\n    IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) | getOperand(1));\n    break;\n  case OPCODE_BITMAP_XOR:\n    IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) ^ getOperand(1));\n    break;\n  \n  case OPCODE_AUTOSTART: // Handled only during begin process\n  case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte.\n  case OPCODE_TURNOUT: // Turnout definition ignored at runtime\n  case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime\n  case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime\n  case OPCODE_ONCLOSE: // Turnout event catchers ignored here\n  case OPCODE_ONLCC:   // LCC event catchers ignored here \n  case OPCODE_ONACON:   // MERG event catchers ignored here \n  case OPCODE_ONACOF:   // MERG event catchers ignored here \n  case OPCODE_ONTHROW:\n  case OPCODE_ONACTIVATE: // Activate event catchers ignored here\n  case OPCODE_ONDEACTIVATE:\n  case OPCODE_ONRED:\n  case OPCODE_ONAMBER:\n  case OPCODE_ONGREEN:\n  case OPCODE_ONCHANGE:\n  case OPCODE_ONTIME:\n  case OPCODE_ONBUTTON:\n  case OPCODE_ONSENSOR:\n  case OPCODE_ONBITMAP:\n#ifndef IO_NO_HAL\n  case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime\n  case OPCODE_EXTTTURNTABLE:  // Turntable definition ignored at runtime\n  case OPCODE_TTADDPOSITION:  // Turntable position definition ignored at runtime\n  case OPCODE_ONROTATE:\n#endif\n  case OPCODE_ONOVERLOAD:\n  case OPCODE_ONBLOCKENTER:\n  case OPCODE_ONBLOCKEXIT:\n#ifdef BOOSTER_INPUT\n  case OPCODE_ONRAILSYNCON:\n  case OPCODE_ONRAILSYNCOFF:\n#endif\n    break;\n    \n  default:\n    kill(F(\"INVOP\"),operand);\n  }\n  // Falling out of the switch means move on to the next opcode\n  // but if we are skipping a false IF or else\n  if (skipIf)  if (!skipIfBlock()) return;\n  SKIPOP;\n}\n\nvoid RMFT2::delayMe(long delay) {\n  delayTime=delay;\n  delayStart=millis();\n}\n\nbool RMFT2::setFlag(VPIN id,byte onMask, byte offMask) {\n   if (FLAGOVERFLOW(id)) return false; // Outside range limit\n   byte f=flags[id];\n   f &= ~offMask;\n   f |= onMask;\n   flags[id]=f;\n   return true;\n}\n\nbool RMFT2::getFlag(VPIN id,byte mask) {\n  if (FLAGOVERFLOW(id)) return 0; // Outside range limit\n  return flags[id]&mask;\n}\n\nvoid RMFT2::kill(const FSH * reason, int operand) {\n  if (reason) DIAG(F(\"EXRAIL ERROR pc=%d, cab=%d, %S %d\"), progCounter,loco, reason, operand);\n  else if (diag) DIAG(F(\"ENDTASK at pc=%d\"), progCounter);\n  delete this;\n}\n\n\nSIGNAL_DEFINITION RMFT2::getSignalSlot(int16_t slot) {\n  SIGNAL_DEFINITION signal;\n  COPYHIGHFLASH(&signal,SignalDefinitions,slot*sizeof(SIGNAL_DEFINITION),sizeof(SIGNAL_DEFINITION));\n  return signal;\n}\n\n/* static */ void RMFT2::doSignal(int16_t id,char rag) {\n  if (!(compileFeatures & FEATURE_SIGNAL)) return; // dont compile code below\n  //if (diag) DIAG(F(\" doSignal %d %x\"),id,rag);\n  \n  // Schedule any event handler for this signal change.\n  // This will work even without a signal definition. \n  if (rag==SIGNAL_RED) onRedLookup->handleEvent(F(\"RED\"),id);\n  else if (rag==SIGNAL_GREEN) onGreenLookup->handleEvent(F(\"GREEN\"),id);\n  else onAmberLookup->handleEvent(F(\"AMBER\"),id);\n  \n  auto sigslot=signalLookup->find(id);\n  if (sigslot<0) return; \n  \n  // keep track of signal state \n  setFlag(sigslot,rag,SIGNAL_MASK);\n \n  // Correct signal definition found, get the rag values\n  auto signal=getSignalSlot(sigslot);\n  \n  switch (signal.type) {\n  case sigtypeSERVO: \n    { \n    auto servopos = rag==SIGNAL_RED? signal.redpin: (rag==SIGNAL_GREEN? signal.greenpin : signal.amberpin);\n    //if (diag) DIAG(F(\"sigA %d %d\"),id,servopos);\n    if  (servopos!=0) IODevice::writeAnalogue(id,servopos,PCA9685::Bounce);\n    return;  \n   }\n\n  case sigtypeDCC:\n   {\n    // redpin,amberpin are the DCC addr,subaddr \n    DCC::setAccessory(signal.redpin,signal.amberpin, rag!=SIGNAL_RED);\n    return; \n  }\n\n case sigtypeDCCX:\n  {\n    // redpin,amberpin,greenpin are the 3 aspects\n    auto value=signal.redpin;\n    if (rag==SIGNAL_AMBER) value=signal.amberpin;\n    if (rag==SIGNAL_GREEN) value=signal.greenpin; \n    DCC::setExtendedAccessory(id, value);\n    return; \n  }\n\ncase sigtypeNEOPIXEL: \n  {\n    // redpin,amberpin,greenpin are the 3 RG values but with no blue permitted. . (code limitation hack) \n    auto colour_RG=signal.redpin;\n    if (rag==SIGNAL_AMBER) colour_RG=signal.amberpin;\n    if (rag==SIGNAL_GREEN) colour_RG=signal.greenpin; \n\n    // blue channel is in followng signal slot (a continuation)\n    auto signal2=getSignalSlot(sigslot+1);\n    auto colour_B=signal2.redpin;\n    if (rag==SIGNAL_AMBER) colour_B=signal2.amberpin;\n    if (rag==SIGNAL_GREEN) colour_B=signal2.greenpin; \n    IODevice::writeAnalogue(id, colour_RG,true,colour_B);\n    return; \n  }\n  \n  case sigtypeSIGNAL:\n  case sigtypeSIGNALH:\n  {\n  // LED or similar 3 pin signal, (all pins zero would be a virtual signal)\n  // If amberpin is zero, synthesise amber from red+green\n  const byte SIMAMBER=0x00;\n  if (rag==SIGNAL_AMBER && (signal.amberpin==0)) rag=SIMAMBER; // special case this func only\n   \n  // Manage invert (HIGH on) pins\n  bool aHigh=signal.type==sigtypeSIGNALH;\n      \n  // set the three pins \n  if (signal.redpin) {\n    bool redval=(rag==SIGNAL_RED || rag==SIMAMBER);\n    if (!aHigh) redval=!redval;\n    killBlinkOnVpin(signal.redpin);\n    IODevice::write(signal.redpin,redval);\n  }\n  if (signal.amberpin) {\n    bool amberval=(rag==SIGNAL_AMBER);\n    if (!aHigh) amberval=!amberval;\n    killBlinkOnVpin(signal.amberpin);\n    IODevice::write(signal.amberpin,amberval);\n  }\n  if (signal.greenpin) {\n    bool greenval=(rag==SIGNAL_GREEN || rag==SIMAMBER);\n    if (!aHigh) greenval=!greenval;\n    killBlinkOnVpin(signal.greenpin);\n    IODevice::write(signal.greenpin,greenval);\n  }\n}\n  case sigtypeVIRTUAL: break;\n  case sigtypeContinuation: break;\n  case sigtypeNoMoreSignals: break;\n  }\n}\n\n/* static */ bool RMFT2::isSignal(int16_t id,char rag) {\n  if (!(compileFeatures & FEATURE_SIGNAL)) return false; \n  int16_t sigslot=signalLookup->find(id);\n  if (sigslot<0) return false; \n  return (flags[sigslot] & SIGNAL_MASK) == rag;\n}\n\n\n// signalAspectEvent returns true if the aspect is destined\n// for a defined DCCX_SIGNAL which will handle all the RAG flags\n// and ON* handlers.\n// Otherwise false so the parser should send the command directly \nbool RMFT2::signalAspectEvent(int16_t address, byte aspect ) {\n  if (!(compileFeatures & FEATURE_SIGNAL)) return false; \n  auto sigslot=signalLookup->find(address);\n  if (sigslot<0) return false;  // this is not a defined signal \n  auto signal=getSignalSlot(sigslot);\n  if (signal.type!=sigtypeDCCX) return false; // not a DCCX signal\n  // Turn an aspect change into a RED/AMBER/GREEN setting\n  if (aspect==signal.redpin) {\n      doSignal(address,SIGNAL_RED);\n      return true;\n  }\n  \n  if (aspect==signal.amberpin) {\n      doSignal(address,SIGNAL_AMBER);\n      return true;\n  }\n  \n  if (aspect==signal.greenpin) {\n      doSignal(address,SIGNAL_GREEN);\n      return true;\n  }\n\n  return false;  // aspect is not a defined one    \n}\n\nvoid RMFT2::turnoutEvent(int16_t turnoutId, bool closed) {\n  // Hunt for an ONTHROW/ONCLOSE for this turnout\n  if (closed)  onCloseLookup->handleEvent(F(\"CLOSE\"),turnoutId);\n  else onThrowLookup->handleEvent(F(\"THROW\"),turnoutId);\n}\n\n\nvoid RMFT2::activateEvent(int16_t addr, bool activate) {\n  // Hunt for an ONACTIVATE/ONDEACTIVATE for this accessory\n  if (activate)  onActivateLookup->handleEvent(F(\"ACTIVATE\"),addr);\n  else onDeactivateLookup->handleEvent(F(\"DEACTIVATE\"),addr);\n}\n\nvoid RMFT2::blockEvent(int16_t block, int16_t loco, bool entering) {\n  if (compileFeatures & FEATURE_BLOCK) {\n  // Hunt for an ONBLOCKENTER/ONBLOCKEXIT for this accessory\n  if (entering)  onBlockEnterLookup->handleEvent(F(\"BLOCKENTER\"),block,loco);\n  else onBlockExitLookup->handleEvent(F(\"BLOCKEXIT\"),block,loco);\n  }\n}\n\nvoid RMFT2::changeEvent(int16_t vpin, bool change) {\n  // Hunt for an ONCHANGE for this sensor\n  if (change)  onChangeLookup->handleEvent(F(\"CHANGE\"),vpin);\n}\n\n#ifndef IO_NO_HAL\nvoid RMFT2::rotateEvent(int16_t turntableId, bool change) {\n  // Hunt or an ONROTATE for this turntable\n  if (change) onRotateLookup->handleEvent(F(\"ROTATE\"),turntableId);\n}\n#endif\n\nvoid RMFT2::clockEvent(int16_t clocktime, bool change) {\n  // Hunt for an ONTIME for this time\n  if (Diag::CMD)\n   DIAG(F(\"clockEvent at : %d\"), clocktime);\n  if (change) {\n    onClockLookup->handleEvent(F(\"CLOCK\"),clocktime);\n    onClockLookup->handleEvent(F(\"CLOCK\"),25*60+clocktime%60);\n  }\n} \n\nvoid RMFT2::powerEvent(int16_t track, bool overload) {\n  // Hunt for an ONOVERLOAD for this item\n  if (Diag::CMD)\n   DIAG(F(\"powerEvent : %c\"), track + 'A');\n  if (overload) {\n    onOverloadLookup->handleEvent(F(\"POWER\"),track);\n  }\n}\n#ifdef BOOSTER_INPUT\nvoid RMFT2::railsyncEvent(bool on) {\n  if (Diag::CMD)\n   DIAG(F(\"railsyncEvent : %d\"), on);\n  if (on) {\n    if (onRailSyncOnLookup)\n      onRailSyncOnLookup->handleEvent(F(\"RAILSYNCON\"), 0);\n  } else {\n    if (onRailSyncOffLookup)\n      onRailSyncOffLookup->handleEvent(F(\"RAILSYNCOFF\"), 0);\n  }\n}\n#endif\n// This function is used when setting pins so that a SET or RESET\n// will cause any blink task on that pin to terminate.\n// It will be compiled out of existence if no BLINK feature is used.\nvoid RMFT2::killBlinkOnVpin(VPIN pin, uint16_t count) {\n   if (!(compileFeatures & FEATURE_BLINK)) return; \n \n  RMFT2 * stoptask=loopTask; // stop when we get back to here\n  RMFT2 * task=loopTask;\n  VPIN lastPin=pin+count-1;\n  while(task) {\n    auto nextTask=task->next;\n    if (\n      (task->blinkState==blink_high || task->blinkState==blink_low) \n       && task->blinkPin>=pin\n       && task->blinkPin<=lastPin\n       )    {\n        if (diag)  DIAG(F(\"kill blink %d\"),task->blinkPin,lastPin);\n        task->kill();\n       }\n    task=nextTask;\n    if (task==stoptask) return;\n  }\n}\n  \nvoid RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco) {  \n  // Check we dont already have a task running this handler\n  RMFT2 * task=loopTask;\n  while(task) {\n    if (task->onEventStartPosition==pc && task->loco==loco) {\n      DIAG(F(\"Recursive ON%S(%d)\"),reason, id);\n      return;\n    }\n    task=task->next;\n    if (task==loopTask) break;\n  }\n  \n  task=new RMFT2(pc,loco);  // new task starts at this instruction\n  task->onEventStartPosition=pc; // flag for recursion detector\n}\n\nvoid RMFT2::printMessage2(const FSH * msg) {\n  DIAG(F(\"EXRAIL(%d) %S\"),loco,msg);\n}\nstatic StringBuffer * buffer=NULL;\n/* thrungeString is used to stream a HIGHFLASH string to a suitable Serial\nand handle the oddities like LCD, BROADCAST and PARSE */    \nvoid RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {\n   //DIAG(F(\"thrunge addr=%l mode=%d id=%d\"), strfar,mode,id);\n   Print * stream=NULL;\n   // Find out where the string is going \n   switch (mode) {\n    case thrunge_print:\n         StringFormatter::send(&USB_SERIAL,F(\"<* EXRAIL(%d) \"),loco);\n         stream=&USB_SERIAL;\n         break;\n\n    case thrunge_serial: stream=&USB_SERIAL; break;  \n    case thrunge_serial1: \n         #ifdef SERIAL1_COMMANDS\n         stream=&Serial1; \n         #endif\n         break;\n    case thrunge_serial2: \n         #ifdef SERIAL2_COMMANDS\n         stream=&Serial2; \n         #endif\n         break;\n    case thrunge_serial3: \n         #ifdef SERIAL3_COMMANDS\n         stream=&Serial3; \n         #endif\n         break;\n    case thrunge_serial4: \n         #ifdef SERIAL4_COMMANDS\n         stream=&Serial4; \n         #endif\n         break;\n    case thrunge_serial5: \n         #ifdef SERIAL5_COMMANDS\n         stream=&Serial5; \n         #endif\n         break;\n   case thrunge_serial6: \n         #ifdef SERIAL6_COMMANDS\n         stream=&Serial6; \n         #endif\n         break;\n    case thrunge_lcn: \n      #if defined(LCN_SERIAL) \n      stream=&LCN_SERIAL;\n      #endif\n       break;  \n    case thrunge_parse:\n    case thrunge_broadcast:\n    case thrunge_message: \n    case thrunge_lcd:\n    default:    // thrunge_lcd+1, ...\n         if (!buffer) buffer=new StringBuffer();\n         buffer->flush();\n         stream=buffer;\n         break; \n    }\n    if (!stream) return; \n    \n     #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n    // if mega stream it out \n    for (;;strfar++) {\n      char c=pgm_read_byte_far(strfar);\n      if (c=='\\0') break;\n      stream->write(c);\n    }\n    #else\n    // UNO/NANO CPUs dont have high memory\n    // 32 bit cpus dont care anyway\n    stream->print((FSH *)strfar);\n    #endif\n\n  // and decide what to do next\n   switch (mode) {\n    case thrunge_print:\n         StringFormatter::send(&USB_SERIAL,F(\" *>\\n\"));\n         break;\n    // TODO  more serials for SAMx case thrunge_serial4: stream=&Serial4; break;\n    case thrunge_parse: \n      DCCEXParser::parseOne(&USB_SERIAL,(byte*)buffer->getString(),NULL);\n      break;\n    case thrunge_broadcast:\n      CommandDistributor::broadcastRaw(CommandDistributor::COMMAND_TYPE,buffer->getString());\n      break;\n    case thrunge_withrottle:\n      CommandDistributor::broadcastRaw(CommandDistributor::WITHROTTLE_TYPE,buffer->getString());\n      break;\n    case thrunge_message:\n      CommandDistributor::broadcastMessage(buffer->getString());\n      break;\n    case thrunge_lcd:\n         LCD(id,F(\"%s\"),buffer->getString());\n         break;\n    default:  // thrunge_lcd+1, ...\n      if (mode > thrunge_lcd) \n        SCREEN(mode-thrunge_lcd, id, F(\"%s\"),buffer->getString());  // print to other display\n      break;       \n    }\n}\n\nvoid RMFT2::manageRouteState(int16_t id, byte state) {\n  if (compileFeatures && FEATURE_ROUTESTATE) {\n    // Route state must be maintained for when new throttles connect.\n    // locate route id in the Routes lookup\n    int16_t position=routeLookup->findPosition(id);\n    if (position<0) return; \n    // set state beside it \n    if (routeStateArray[position]==state) return; \n    routeStateArray[position]=state;\n    CommandDistributor::broadcastRouteState(id,state);\n  }\n}\nbool RMFT2::ifRouteState(int16_t id, byte state) {\n  if (compileFeatures && FEATURE_ROUTESTATE) {\n    // Route state must be maintained for when new throttles connect.\n    // locate route id in the Routes lookup\n    int16_t position=routeLookup->findPosition(id);\n    return position>=0 &&  routeStateArray[position]==state;\n  }\n  else return false; \n}\n\nvoid RMFT2::manageRouteCaption(int16_t id,const FSH* caption) {\n  if (compileFeatures && FEATURE_ROUTESTATE) {\n    // Route state must be maintained for when new throttles connect.\n    // locate route id in the Routes lookup\n    int16_t position=routeLookup->findPosition(id);\n    if (position<0) return; \n    // set state beside it \n    if (routeCaptionArray[position]==caption) return; \n    routeCaptionArray[position]=caption;\n    CommandDistributor::broadcastRouteCaption(id,caption);\n  }\n}\n\nvoid RMFT2::ifAnyFunc(const int16_t * vpinList, int16_t count) {\n  // Sets skipIf if none found\n  skipIf=false; \n  for (int v=0;v<count;v++) {\n    int16_t vpin=vpinList[v];\n    if (vpin<0 && IODevice::read(-vpin)==0) return;\n    if (IODevice::read(vpin)>0) return;\n  }\n  skipIf=true; // none found \n}\n  \nvoid RMFT2::ifAllFunc(const int16_t * vpinList, int16_t count) {\n  skipIf=true; // return skipping if any are wrong\n  for (int v=0;v<count;v++) {\n    int16_t vpin=vpinList[v];\n    if (vpin<0 && IODevice::read(-vpin)>0) return;\n    if (IODevice::read(vpin)==0) return;\n  }\n  skipIf=false; // no failures found \n}\n"
  },
  {
    "path": "EXRAIL2.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 Morten Nielsen\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef EXRAIL2_H\n#define EXRAIL2_H\n#include \"FSH.h\"\n#include \"IODevice.h\"\n#include \"Turnouts.h\"\n#include \"Turntables.h\"\n   \n// The following are the operation codes (or instructions) for a kind of virtual machine.\n// Each instruction is normally 3 bytes long with an operation code followed by a parameter.\n// In cases where more than one parameter is required, the first parameter is followed by one  \n// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes \n// searching easier as a parameter can never be confused with an opcode. \n// \nenum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,\n             OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,\n             OPCODE_SPEEDUP,OPCODE_SLOWDOWN,OPCODE_SPEED_REL,OPCODE_INVERT_DIRECTION,\n             OPCODE_MOMENTUM,\n             OPCODE_RESERVE,OPCODE_FREE,\n             OPCODE_AT,OPCODE_AFTER,\n             OPCODE_AFTEROVERLOAD,OPCODE_AUTOSTART,\n             OPCODE_ATGTE,OPCODE_ATLT,\n             OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,\n             OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,\n             OPCODE_BLINK,\n             OPCODE_ENDIF,OPCODE_ELSE,\n             OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,\n             OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,\n             OPCODE_FTOGGLE,OPCODE_XFTOGGLE,OPCODE_XFWD,OPCODE_XREV,\n             OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,\n             OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,\n             OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,\n#ifndef DISABLE_PROG\n             OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,\n#endif\n             OPCODE_POM,\n             OPCODE_START,OPCODE_START_SHARED,OPCODE_START_SEND,OPCODE_SETLOCO,OPCODE_SETFREQ,OPCODE_SENDLOCO,OPCODE_FORGET,\n             OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,\n             OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,\n             OPCODE_PRINT,OPCODE_DCCACTIVATE,OPCODE_ASPECT,\n             OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,\n             OPCODE_ROSTER,OPCODE_KILLALL,\n             OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,\n             OPCODE_ENDTASK,OPCODE_ENDEXRAIL,\n             OPCODE_SET_TRACK,OPCODE_SET_POWER,\n             OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,\n             OPCODE_ONCHANGE,\n             OPCODE_ONCLOCKTIME,\n             OPCODE_ONTIME,\n             OPCODE_TTADDPOSITION,OPCODE_DCCTURNTABLE,OPCODE_EXTTTURNTABLE,\n             OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_WAITFORTT,\n             OPCODE_LCC,OPCODE_LCCX,OPCODE_ONLCC,\n             OPCODE_ACON, OPCODE_ACOF, \n             OPCODE_ONACON, OPCODE_ONACOF, \n             OPCODE_ONOVERLOAD,\n\t     OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,\n             OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,\n             OPCODE_ROUTE_DISABLED,\n             OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,\n             OPCODE_CLEAR_ANY_STASH,\n             OPCODE_ONBUTTON,OPCODE_ONSENSOR,OPCODE_ONVP_SENSOR,\n             OPCODE_NEOPIXEL,\n             OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,\n             OPCODE_ESTOPALL,OPCODE_XPOM,\n             OPCODE_BITMAP_AND,OPCODE_BITMAP_OR,OPCODE_BITMAP_XOR,OPCODE_BITMAP_INC,OPCODE_BITMAP_DEC,OPCODE_ONBITMAP,\n             OPCODE_FREEALL,\n             OPCODE_WAIT_WHILE_RED,\n             OPCODE_SAVE_SPEED,OPCODE_RESTORE_SPEED,\n             OPCODE_XSAVE_SPEED,OPCODE_XRESTORE_SPEED,\n             OPCODE_RANDOM_CALL,OPCODE_RANDOM_FOLLOW,\n             OPCODE_CONSIST,\n             \n             // OPcodes below this point are skip-nesting IF operations\n             // placed here so that they may be skipped as a group\n             // see skipIfBlock()\n            IF_TYPE_OPCODES, // do not move this... \n             OPCODE_IFRED,OPCODE_IFAMBER,OPCODE_IFGREEN,\n             OPCODE_IFGTE,OPCODE_IFLT,\n             OPCODE_IFTIMEOUT,\n             OPCODE_IF,OPCODE_IFNOT,\n             OPCODE_IFRANDOM,OPCODE_IFRESERVE,\n             OPCODE_IFCLOSED,OPCODE_IFTHROWN,\n             OPCODE_IFRE,\n             OPCODE_IFLOCO,\n             OPCODE_IFTTPOSITION,\n             OPCODE_IFSTASH,\n             OPCODE_IFSTASHED_HERE,\n             OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,\n             OPCODE_IF_ROUTE_ACTIVE,OPCODE_IF_ROUTE_INACTIVE,\n             OPCODE_IF_ROUTE_HIDDEN,OPCODE_IF_ROUTE_DISABLED,\n             };\n\n// Ensure thrunge_lcd is put last as there may be more than one display, \n// sequentially numbered from thrunge_lcd.\nenum thrunger: byte {\n  thrunge_print, thrunge_broadcast, thrunge_withrottle,\n  thrunge_serial,thrunge_parse,\n  thrunge_serial1, thrunge_serial2, thrunge_serial3,\n  thrunge_serial4, thrunge_serial5, thrunge_serial6,\n  thrunge_lcn,thrunge_message,\n  thrunge_lcd,  // Must be last!!\n  };\n\n\nenum BlinkState: byte {\n    not_blink_task, \n    blink_low, // blink task running with pin LOW\n    blink_high, // blink task running with pin high \n    at_timeout  // ATTIMEOUT timed out flag\n    }; \nenum SignalType {\n       sigtypeVIRTUAL,\n       sigtypeSIGNAL, \n       sigtypeSIGNALH, \n       sigtypeDCC,\n       sigtypeDCCX,\n       sigtypeSERVO,\n       sigtypeNEOPIXEL,\n       sigtypeContinuation,  // neopixels require a second line\n       sigtypeNoMoreSignals\n       }; \n\n  struct SIGNAL_DEFINITION {\n       SignalType type;\n       VPIN id;  \n       VPIN  redpin,amberpin,greenpin; \n  };\n\n  // Flag bits for compile time features.\n  static const byte FEATURE_SIGNAL= 0x80;\n  static const byte FEATURE_LCC   = 0x40;\n  static const byte FEATURE_ROSTER= 0x20;\n  static const byte FEATURE_ROUTESTATE= 0x10;\n  // spare = 0x08;\n  static const byte FEATURE_BLINK = 0x04;\n  static const byte FEATURE_SENSOR = 0x02;\n  static const byte FEATURE_BLOCK = 0x01;\n  \n \n  // Flag bits for status of hardware and TPL\n  static const byte SECTION_FLAG = 0x80;\n  static const byte LATCH_FLAG   = 0x40;\n  static const byte TASK_FLAG    = 0x20;\n  static const byte SPARE_FLAG   = 0x10;\n  static const byte SIGNAL_MASK  = 0x0C;\n  static const byte SIGNAL_RED   = 0x08;\n  static const byte SIGNAL_AMBER = 0x0C;\n  static const byte SIGNAL_GREEN = 0x04;\n\n  static const byte  MAX_STACK_DEPTH=4;\n \n   static const short MAX_FLAGS=256;\n  #define FLAGOVERFLOW(x) x>=MAX_FLAGS\n\nclass LookList {\n  public: \n    LookList(int16_t size);\n    void chain(LookList* chainTo);\n    void add(int16_t lookup, int16_t result);\n    int16_t find(int16_t value); // finds result value\n    int16_t findPosition(int16_t value); // finds index \n    int16_t size();\n    void stream(Print * _stream); \n    void handleEvent(const FSH* reason,int16_t id, int16_t loco=0);\n\n  private:\n     int16_t m_size;\n     int16_t m_loaded;\n     int16_t * m_lookupArray;\n     int16_t * m_resultArray;\n     LookList* m_chain;     \n};\n\n class RMFT2 {\n   public:\n    static void begin();\n    static void loop();\n    RMFT2(int progCounter, int16_t cab=0, bool invert=false);\n    ~RMFT2();\n    static void readLocoCallback(int16_t cv);\n    static void createNewTask(int route, uint16_t cab);\n    static void turnoutEvent(int16_t id, bool closed);  \n    static void activateEvent(int16_t addr, bool active);\n    static void changeEvent(int16_t id, bool change);\n    static void clockEvent(int16_t clocktime, bool change);\n    static void rotateEvent(int16_t id, bool change);\n    static void powerEvent(int16_t track, bool overload);\n#ifdef BOOSTER_INPUT\n    static void railsyncEvent(bool on);\n#endif\n    static void blockEvent(int16_t block, int16_t loco, bool entering);\n    static bool signalAspectEvent(int16_t address, byte aspect );    \n    // Throttle Info Access functions built by exrail macros \n  static const byte rosterNameCount;\n  static const int16_t HIGHFLASH routeIdList[];\n  static const int16_t HIGHFLASH automationIdList[];\n  static const int16_t HIGHFLASH rosterIdList[];\n  static const FSH *  getRouteDescription(int16_t id);\n  static char   getRouteType(int16_t id);\n  static const FSH *  getTurnoutDescription(int16_t id);\n  static const FSH *  getRosterName(int16_t id);\n  static const FSH *  getRosterFunctions(int16_t id);\n  static const FSH *  getTurntableDescription(int16_t id);\n  static const FSH *  getTurntablePositionDescription(int16_t turntableId, uint8_t positionId);\n  static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco=0);\n  static bool readSensor(uint16_t sensorId);\n  static bool isSignal(int16_t id,char rag);\n  static SIGNAL_DEFINITION getSignalSlot(int16_t slotno); \n   \nprivate: \n    static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);\n    static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;\n    static void streamFlags(Print* stream);\n    static bool setFlag(VPIN id,byte onMask, byte OffMask=0);\n    static bool getFlag(VPIN id,byte mask); \n    static int16_t progtrackLocoId;\n    static void doSignal(int16_t id,char rag); \n    static void setTurnoutHiddenState(Turnout * t);\n    #ifndef IO_NO_HAL\n    static void setTurntableHiddenState(Turntable * tto);\n    #endif\n    static LookList* LookListLoader(OPCODE op1,\n                      OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);\n    static uint16_t getOperand(int progCounter,byte n);\n    static void killBlinkOnVpin(VPIN pin,uint16_t count=1);\n    static void ifAllFunc(const int16_t * vpinList, int16_t count); \n    static void ifAnyFunc(const int16_t * vpinList, int16_t count);\n    static RMFT2 * loopTask;\n    static RMFT2 * pausingTask;\n    void delayMe(long millisecs);\n    bool skipIfBlock();\n    bool readLoco();\n    void loop2();\n    void kill(const FSH * reason=NULL,int operand=0);          \n    void printMessage(uint16_t id);  // Built by RMFTMacros.h\n    void printMessage2(const FSH * msg);\n    void thrungeString(uint32_t strfar, thrunger mode, byte id=0);\n    uint16_t getOperand(byte n); \n    void pause();\n    void resume();\n    \n   static bool diag;\n   static bool skipIf;\n   static const  HIGHFLASH3  byte RouteCode[];\n   static const  HIGHFLASH  SIGNAL_DEFINITION SignalDefinitions[];\n   static byte flags[MAX_FLAGS];\n   static Print * LCCSerial;\n   static LookList * routeLookup;\n   static LookList * signalLookup;\n   static LookList * onThrowLookup;\n   static LookList * onCloseLookup;\n   static LookList * onActivateLookup;\n   static LookList * onDeactivateLookup;\n   static LookList * onRedLookup;\n   static LookList * onAmberLookup;\n   static LookList * onGreenLookup;\n   static LookList * onChangeLookup;\n   static LookList * onClockLookup;\n#ifndef IO_NO_HAL\n   static LookList * onRotateLookup;\n#endif\n   static LookList * onOverloadLookup;\n   static LookList * onBlockEnterLookup;\n   static LookList * onBlockExitLookup;\n#ifdef BOOSTER_INPUT\n   static LookList * onRailSyncOnLookup;\n   static LookList * onRailSyncOffLookup;\n#endif\n   \n   \n   static const int countLCCLookup;\n   static int onLCCLookup[];\n   static const byte compileFeatures;\n   static void manageRouteState(int16_t id, byte state);\n   static bool ifRouteState(int16_t id, byte state);\n   static void manageRouteCaption(int16_t id, const FSH* caption);\n   static byte * routeStateArray;\n   static const FSH ** routeCaptionArray;\n   static int16_t * stashArray;\n   static int16_t maxStashId;\n    \n  // Local variables - exist for each instance/task \n    RMFT2 *next;   // loop chain \n    int progCounter;    // Byte offset of next route opcode in ROUTES table\n    unsigned long delayStart; // Used by opcodes that must be recalled before completing\n    unsigned long  delayTime;\n    union {\n      unsigned long waitAfter; // Used by OPCODE_AFTER\n      unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT\n      VPIN blinkPin;  // Used by blink tasks \n    };\n    byte  taskId;\n    BlinkState blinkState; // includes AT_TIMEOUT flag. \n    uint16_t loco;\n    bool invert;\n    byte pauseSpeed;\n    int onEventStartPosition;\n    byte stackDepth;\n    int callStack[MAX_STACK_DEPTH];\n};\n\n#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter)\n#define SKIPOP progCounter+=3\n\n#endif\n"
  },
  {
    "path": "EXRAIL2MacroBase.h",
    "content": "/*\n *  © 2020-2025 Chris Harlow. All rights reserved.\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 Morten Nielsen\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see https://www.gnu.org/licenses/.\n */\n\n/* This file creates dummy entries for all the EXRAIL macros to allow\n * the EXRAIL Macro files to be parsed multiple times during compilation.\n * The actual definitions are created in EXRAILMacros.h according to the\n * current pass.\n *\n * Triple / Comments in this file are intended for the EXRAIL end user and will\n * be extracted to create documentation. Please maintain formatting.\n */\n\n#define ACTIVATE(addr,subaddr)\n///brief Send DCC Accessory Activate packet (gate on then off)\n///param addr DCC short address of accessory\n///param subaddr DCC sub address\n\n\n#define ACTIVATEL(linearaddr)\n///brief Send DCC Accessory Activate packet (gate on then off)\n///param linearaddr DCC linear address of accessory\n\n#define AFTER(vpin,timer...)\n///brief Wait for sensor activated, then deactivated for given time\n///param vpin Virtual Pin number of sensor\n///param timer... optional wait in ms, default 500\n\n#define AFTEROVERLOAD(track_id)\n///brief Wait for overload to be resolved\n///param track_id A..H\n\n#define ALIAS(name,value...)\n///brief defines a named numeric value.\n///param name c++ variable name that can be used throughout the script\n///param value...  if omitted, a large negative value is created automatically\n\n#define AMBER(signal_id)\n///brief Sets a signal to amber state\n///see ONAMBER\n\n#define ANOUT(vpin,value,param1,param2)\n///brief Writes to the HAL analog output interface of a device driver.\n///param vpin Virtual pin number of device\n///param value basic analog value\n///param param1 device dependent\n///param param2 device dependent\n\n#define AT(vpin)\n///brief Wait until a sensor becomes active\n///param vpin Virtual pin of sensor. Use negative value for sensors that are HIGH when activated\n\n#define ASPECT(address,value)\n///brief Sends a DCC aspect value to an accessory address. May also change status of a signal defined using this aspect.\n///param address Linear DCC address of device\n///param value Aspect value (Device dependent)\n\n#define ATGTE(vpin,value)\n///brief Wait for analog sensor to be greater than given value\n///param vpin Analog pin number\n///param value integer value to compare against\n\n#define ATLT(vpin,value)\n///brief Wait for analog sensor value to be less than given value\n///param vpin Analog pin number\n///param value integer value to compare against\n\n#define ATTIMEOUT(vpin,timeout_ms)\n///brief Wait for sensor active, with timeout. Use IFTIMEOUT to determine whether the AT was satisfied.\n///see IFTIMEOUT\n///param vpin Sensor pin number\n///param timeout_ms Milliseconds to wait before timeout\n\n#define AUTOMATION(sequence_id,description)\n///brief Defines starting point of a sequence that will be shown as an Automation by\n///      the throttles. Automations are started by the throttle handing over a loco id to be driven.\n///param sequence_id Unique sequence id value\n///param description (Quoted text) will be shown on throttle button\n\n#define AUTOSTART\n///brief A new task will be created starting from this point at Command Station startup\n\n#define BLINK(vpin,onDuty,offDuty)\n///brief Starts a blinking process for a vpin (typically a LED). Stop blink with SET or RESET.\n///param vpin Pin to blink\n///param onDuty Milliseconds with LED ON\n///param offDuty Milliseconds with LED off\n\n#define BROADCAST(msg)\n///brief Send raw message text to all throttles using the DCC-EX protocol\n///see WITHROTTLE\n///param msg  Quoted message\n\n#define BUILD_CONSIST(loco_id)\n///brief Adds a loco to follow the current loco in a consist\n///param loco_id may be negative to indicate loco facing backwards\n\n#define BREAK_CONSIST\n///brief Breaks up any consist involving the current loco\n\n#define CALL(sequence_id)\n///brief transfer control to another sequence with expectation to return\n///see RETURN\n///param sequence_id SEQUENCE to jump processing to, must terminate or RETURN\n\n#define CLEAR_STASH(stash_id)\n///brief Clears loco value stored in stash\n///param stash_id which stash to clear.\n\n#define CLEAR_ALL_STASH\n///brief Clears all stashed loco values\n\n#define CLEAR_ANY_STASH\n///brief Clears loco value from all stash entries\n\n#define CLOSE(turnout_id)\n///brief Close turnout by id\n///see THROW\n\n#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)\n///brief Set up servo movement parameters for non-turnout\n///param vpin must refer to a servo capable pin\n///param pos1 SET position of servo\n///param pos2 RESET position of servo\n///param profile Movement profile (Instant, Fast, Medium, Slow, Bounce)\n\n#define DCC_SIGNAL(signal_id,addr,subaddr)\n///brief Define a DCC accessory signal with short address\n///param signal_id Id used for all signal manipulation commands\n///param addr DCC address\n///param subaddr DCC subaddress\n\n#define DCCX_SIGNAL(signal_id,redAspect,amberAspect,greenAspect)\n///brief Define advanced DCC accessory signal with aspects\n///param signal_id DCC Linear address AND Id used for all signal manipulation commands\n\n#define DCC_TURNTABLE(turntable_id,home,description...)\n///brief defines a Turntable device\n///param homeAngle the angle of the home position, valid angles are 0 - 3600\n///param description... Quoted text description of turntable\n\n#define DEACTIVATE(addr,subaddr)\n///brief Sends DCC Deactivate packet (gate on, gate off)\n///param addr DCC accessory address\n///param subaddr DCC accessory subaddress\n\n#define DEACTIVATEL(addr)\n///brief Sends DCC Deactivate packet (gate on, gate off)\n///param addr DCC Linear accessory address\n\n#define DELAY(delay_ms)\n///brief Waits for given milliseconds delay (This is not blocking)\n///param delay_mS Delay time in milliseconds\n\n#define DELAYMINS(delay_minutes)\n///brief Waits for given minutes delay (This is not blocking)\n\n#define DELAYRANDOM(mindelay,maxdelay)\n///brief Waits for random delay between min and max milliseconds (This is not blocking)\n///param mindelay minimum delay in ms\n///param maxdelay maximum delay in ms\n\n#define DONE\n///brief Stops task loco (if any) and terminates current task\n\n#define DRIVE(analogpin)\n///brief RESERVED do not use\n\n#define ELSE\n///brief introduces alternate processing path after any kind of IF\n///see IF\n\n#define ENDEXRAIL\n///brief obsolete.. no longer needed. Does nothing.\n\n#define ENDIF\n///brief determines end of IF(any type)  block.\n///      IF something ENDIF, or\n///      IF something ELSE something ENDIF\n///see IF\n\n#define ENDTASK\n///brief same as DONE\n///see DONE\n\n#define ESTOP\n///brief Performs emergency stop on current task loco\n\n#define ESTOPALL\n///brief Performs emergency stop on all locos\n\n#define ESTOP_PAUSE\n///brief Emergency stop, and preventing all movement until resumed\n\n#define ESTOP_RESUME\n///brief Resumes loco speeds after ESTOP_PAUSE\n\n#define EXRAIL\n///brief obsolete.. no longer needed. Does nothing.\n\n#define EXTT_TURNTABLE(id,vpin,home,description...)\n///brief Defines the EX‑Turntable turntable/traverser object only, so you will need a separate HAL() statement for an EX‑Turntable device driver.\n///param homeAngle  the angle of the home position, valid angles are 0 - 3600\n///param quoted description...\n\n#define FADE(vpin,value,ms)\n///brief Modifies analog value slowly taking a given time\n///param vpin Servo virtual pin number\n///param value new target value\n///param ms time to reach value\n\n#define FOFF(func)\n///brief Turns off current loco function\n///see FON\n\n#define FOLLOW(sequence_id)\n///brief Task processing follows given route or sequence (Effectively a GoTo)\n\n#define FON(func)\n///brief Turn on current loco function\n///see FOFF\n\n#define FORGET\n///brief Removes current loco from task and DCC reminders table.\n\n#define FREE(token_id)\n///brief Frees logical token\n///see RESERVE\n///param token_id 0..255\n\n#define FREEALL\n///brief Frees all logical tokens\n///see RESERVE\n\n#define FTOGGLE(func)\n///brief Toggles function for current loco\n\n#define FWD(speed)\n///brief Instructs current loco to set DCC speed\n///param speed 0..127   (1=ESTOP)\n\n#define GREEN(signal_id)\n///brief Sets signal to green state\n\n#define HAL(haltype,params...)\n///brief Defines VPIN mapping for specific hardware drivers\n///param haltype driver name, normally device type\n///param params... depend on driver.\n\n#define HAL_IGNORE_DEFAULTS\n///brief System will ignore default HAL device mappings\n\n#define IF(vpin)\n///brief Checks sensor state, If false jumps to matching nested ELSE or ENDIF\n///param vpin  VPIN of sensor. Negative VPIN will invert sensor state.\n\n#define IF_ALL(vpinList...)\n///brief Checks sensor state, If any are false, it jumps to matching nested ELSE or ENDIF\n///param vpinlist  comma separated list of VPINs of sensors. Negative VPIN will invert sensor state.\n\n#define IF_ANY(vpinList...)\n///brief Checks sensor state, If all are false jumps to matching nested ELSE or ENDIF\n///param vpinlist  comma separated list of VPINs of sensors. Negative VPIN will invert sensor state.\n\n#define IFAMBER(signal_id)\n///brief Checks if signal is in AMBER state.\n///see IF\n\n#define IFCLOSED(turnout_id)\n///brief Checks if given turnout is in close state\n///see IF\n\n#define IFGREEN(signal_id)\n///brief Checks if given signal is in GREEN state\n///see IF\n\n#define IFGTE(vpin,value)\n///brief Checks if analog vpin sensor >= value\n///see IF\n\n#define IFLOCO(loco_id_list...)\n///brief Checks if current task loco is in the list of loco ids. List may be comma separated values\n///see IF\n\n#define IFLT(vpin,value)\n///brief Checks if analog sensor < value\n///see IF\n///param vpin  Analog vpin of sensor\n\n#define IFNOT(vpin)\n///brief Inverse of IF\n///see IF\n\n#define IFRANDOM(percent)\n///brief randomly satisfied IF at given percent probability\n///see IF\n\n#define IFRED(signal_id)\n///brief Checks if given signal is in RED state\n///see IF\n\n#define IFSTASH(stash_id)\n///brief Checks if given stash entry has a non-zero value\n///see IF\n\n#define IFSTASHED_HERE(stash_id)\n///brief Checks if given stash entry has the current loco\n///see IF\n\n#define IFTHROWN(turnout_id)\n///brief Checks if given turnout is in THROWN state\n///see IF\n\n#define IFRESERVE(token_id)\n///brief Attempts to reserve token and if satisfiled the token remains reserved.\n///see IF RESERVE FREE\n\n#define IFTIMEOUT\n///brief Checks TIMEOUT state after an AT/AFTER request with timeout value.\n///see IF AT AFTER\n\n#define IFTTPOSITION(turntable_id,position)\n///brief Checks if Turntable is in given position\n///see IF\n\n#define IFRE(vpin,value)\n///brief Checks external rotary encoder value\n///param vpin of device driver for rotary encoder\n\n#define IFBITMAP_ALL(vpin,mask)\n///brief Checks if (vpin pseudo-analog value & mask) == mask.\n///see IF\n///param mask Binary mask applied to vpin value\n\n#define IFBITMAP_ANY(vpin,mask)\n///brief Checks if vpin pseudo-analog value & mask is non-zero\n///see IF\n///param mask Binary mask applied to vpin value\n\n#define IFROUTE_ACTIVE(sequence_id)\n///brief Checks if route is active\n///see IF\n\n#define IFROUTE_INACTIVE(sequence_id)\n///brief Checks if route is inactive\n///see IF\n\n#define IFROUTE_HIDDEN(sequence_id)\n///brief Checks if route is hidden\n///see IF\n\n#define IFROUTE_DISABLED(sequence_id)\n///brief Checks if route is disabled\n///see IF\n\n#define INVERT_DIRECTION\n///brief Marks current task so that FWD and REV commands are inverted.\n\n#define JMRI_SENSOR(vpin,count...)\n///brief Defines multiple JMRI `<s>` type sensor feedback definitions each with id matching vpin and INPUT_PULLUP\n///param vpin first vpin number\n///param count... Number of consecutive VPINS for which to create JMRI sensor feedbacks. Default 1.\n\n#define JMRI_SENSOR_NOPULLUP(vpin,count...)\n///brief Defines multiple JMRI `<s>` type sensor feedback definitions each with id matching vpin\n///param vpin first vpin number\n///param count... Number of consecutive VPINS for which to create JMRI sensor feedbacks. Default 1.\n\n#define JOIN\n///brief Switches PROG track to receive MAIN track DCC packets. (Drive on PROG track)\n\n#define KILLALL\n///brief Terminates all running EXRAIL tasks\n\n#define LATCH(vpin)\n///brief Make all AT/AFTER/IF see vpin as HIGH without checking hardware\n///param vpin Must only be for VPINS 0..255\n\n#define LCC(eventid)\n///brief Issue event to LCC\n\n#define LCCX(senderid,eventid)\n///brief Issue LCC event while impersonating another sender\n\n#define LCD(row,msg)\n///brief Write message on row of default configured LCD/OLED\n///see SCREEN\n///param msg Quoted text\n\n#define MOMENTUM(accel,decel...)\n///brief sets momentum in ms per DCC 127 step for current loco.\n///param accel Acceleration momentum\n///param decel... Braking momentum. (=Acceleration if not given)\n\n#define SCREEN(display,row,msg)\n///brief Send message to external display handlers\n///param display number, 0=local display, others are handled by external\n///  displays which may have different display numbers on different devices.\n///param msg Quoted text\n\n#define LCN(msg)\n///brief Reserved for LCN communication. Refer to their documentation.\n\n#define MESSAGE(msg)\n///brief Send a human readable message to all throttle users\n///param msg Quoted text\n\n#define MOVETT(turntable_id,steps,activity)\n///brief Move Turntable to specific position\n///see ROTATE\n///param steps position to move to\n///param activity see ROTATE\n\n#define NEOPIXEL(vpin,r,g,b,count...)\n///brief Set a NEOPIXEL vpin to a given red/green/blue colour\n///param vpin VPIN of a pixel\n///param r red component 0-255\n///param g green component 0-255\n///param b blue component 0-255\n///param count... Number of consecutive pixels to set, Default 1.\n\n#define NEOPIXEL_SIGNAL(vpin,redcolour,ambercolour,greencolour)\n///brief Define a signal that uses a single multi colour pixel\n///see NEORGB\n///param vpin unique signal_id\n///param redcolour  RGB colour use NEORGB(red,green,blue) to create values.\n\n#define ACON(eventid)\n///brief Send MERG CBUS ACON to Adapter\n\n#define ACOF(eventid)\n///brief Send MERG CBUS ACOF to Adapter\n\n#define ONACON(eventid)\n///brief Start task here when ACON for event received from MERG CBUS\n\n#define ONACOF(eventid)\n///brief Start task here when ACOF for event received from MERG CBUS\n\n#define ONACTIVATE(addr,subaddr)\n///brief Start task here when DCC Activate sent for short address\n\n#define ONACTIVATEL(linear)\n///brief Start task here when DCC Activate sent for linear address\n\n#define ONAMBER(signal_id)\n///brief Start task here when signal set to AMBER state\n\n#define ONBLOCKENTER(block_id)\n///brief Start task here when a loco enters a railcom block\n///param block_id vpin associated to block by HAL(I2CRailcom..)\n\n#define ONBLOCKEXIT(block_id)\n///brief Start task here when a loco leaves a railcom block\n///param block_id vpin associated to block by HAL(I2CRailcom..)\n\n#define ONTIME(minute_in_day)\n///brief Start task here when fastclock matches\n///param minute_in_day (0..1439)\n\n#define ONCLOCKTIME(hours,mins)\n///brief Start task here when fastclock matches time\n\n#define ONCLOCKMINS(mins)\n///brief Start task here hourly when fastclock minutes matches\n\n#define ONOVERLOAD(track_id)\n///brief Start task here when given track goes into overload\n///param track_id A..H\n\n#define ONRAILSYNCON\n///brief Start task here when the railsync (booster) input port get a valid DCC signal\n\n#define ONRAILSYNCOFF\n///brief Start task here when the railsync (booster) input port does not get a valid DCC signal any more\n\n#define ONDEACTIVATE(addr,subaddr)\n///brief Start task here when DCC deactivate packet sent\n\n#define ONDEACTIVATEL(linear)\n///brief Start task here when DCC deactivate sent to linear address\n\n#define ONCLOSE(turnout_id)\n///brief Start task here when turnout closed\n\n#define ONLCC(sender,event)\n///brief Start task here when LCC event arrives from sender\n\n#define ONGREEN(signal_id)\n///brief Start task here when signal set to GREEN state\n\n#define ONRED(signal_id)\n///brief Start task here when signal set to RED state\n\n#define ONROTATE(turntable_id)\n///brief Start task here when turntable is rotated\n\n#define ONTHROW(turnout_id)\n///brief Start task here when turnout is Thrown\n\n#define ONCHANGE(vpin)\n///brief Rotary encoder change starts task here (This is obscurely different from ONSENSOR which will be merged in a later release.)\n\n#define ONSENSOR(vpin)\n///brief Start task here when sensor changes state (debounced)\n\n#define ONBITMAP(vpin)\n///brief Start task here when bitmap sensor changes state (debounced)\n\n#define ONBUTTON(vpin)\n///brief Start task here when sensor changes HIGH to LOW.\n\n#define PAUSE\n///brief Pauses all EXRAIL tasks except the current one.\n/// Other tasks ESTOP their locos until RESUME issued\n\n#define PIN_TURNOUT(id,vpin,description...)\n///brief Defines a turnout which operates on a single pin\n///param description... Quoted text (shown to throttles) or HIDDEN\n\n#define PRINT(msg)\n///brief prints diagnostic message on USB serial\n///param msg Quoted text\n\n#define PARSE(msg)\n///brief Executes `<>` command as if entered from serial\n///param msg Quoted text, preferably including `<>`\n\n#define PICKUP_STASH(stash_id)\n///see STASH\n///brief Loads stashed value into current task loco\n///param stash_id position in stash where a loco id was previously saved.\n\n#define PLAY_EQ(vpin,eqname)\n///brief Sets EQ setting on DFPlayer module\n///param eqname One of: NORMAL,POP,ROCK,JAZZ,CLASSIC,BASS\n\n#define PLAY_FOLDER(vpin,folder)\n///brief Sets DFPlayer folder to use for tracks \n///param folder number\n\n#define PLAY_PAUSE(vpin)\n///brief Pauses sound play\n///see PLAY_RESUME\n\n#define PLAY_REPEAT(vpin,track,volume...)\n///brief Plays a track on a DFPlayer module on repoeat\n///param track number\n///param volume... optional volume 0-30 or use default set below\n\n#define PLAY_RESET(vpin)\n///brief Resets DFPlayer \n\n#define PLAY_RESUME(vpin)\n///brief Resumes playing sound\n///see PLAY_PAUSE \n\n#define PLAY_STOP(vpin)\n///brief Stops DFPlayer \n\n#define PLAY_TRACK(vpin,track,volume...)\n///brief Plays a track on a DFPlayer module\n///param track number\n///param volume... optional volume 0-30 or use default set below\n\n#define PLAY_VOLUME(vpin,volume)\n///brief Sets default volume on a DFPlayer module\n///param volume... volume 0-30\n\n#define POM(cv,value)\n///brief Write value to cv on current tasks loco (Program on Main)\n\n#define POWEROFF\n///brief Powers off all tracks\n\n#define POWERON\n///brief Powers ON all tracks\n\n#define RANDOM_CALL(sequence_id...)\n///brief CALL to another sequence randomly chosen from list\n///see CALL\n///param sequence_id.. list of SEQUENCE that may be called, each must terminate or RETURN\n\n#define RANDOM_FOLLOW(sequence_id...)\n///brief jump to another sequence randomly chosen from list\n///see FOLLOW\n///param sequence_id.. list of SEQUENCE that may be followed\n\n#define READ_LOCO\n///brief Reads loco Id from prog track and sets currenmt task loco id.\n\n#define RED(signal_id)\n///brief sets signal to RED state\n\n#define RESERVE(token_id)\n///brief Waits for token for block. If not available immediately, current task loco is stopped.\n\n#define RESET(vpin,count...)\n///brief Sets output pin LOW\n///see SET\n///param count... Number of consecutive pins, default 1\n\n#define RESTORE_SPEED\n///brief Resumes locos saved speed\n///see SAVE_SPEED\n\n#define RESUME\n///brief Resumes PAUSEd tasks\n///see PAUSE\n\n#define RETURN\n///brief Returns to CALL\n///see CALL\n\n#define REV(speed)\n///brief Issues DCC speed packet for current loco in reverse.\n///see FWD\n///param speed  (0..127, 1=ESTOP)\n\n#define ROTATE(turntable_id,position,activity)\n///brief Rotates an EX-Turntable to a given position\n///param activity .. One of:\n/// - **Turn**: Rotate turntable, maintain phase\n/// - **Turn_PInvert**: Rotate turntable, invert phase\n/// - **Home**: Initiate homing\n/// - **Calibrate**: Initiate calibration sequence\n/// - **LED_On**: Turn LED on\n/// - **LED_Slow**: Set LED to a slow blink\n/// - **LED_Fast**: Set LED to a fast blink\n/// - **LED_Off**: Turn LED off\n/// - **Acc_On**: Turn accessory pin on\n/// - **Acc_Off**: Turn accessory pin off\n\n#define ROTATE_DCC(turntable_id,position_id)\n///brief Rotates turntable to a given position using DCC commands\n\n#define ROSTER(cab,name,funcmap...)\n///brief Describes a loco roster entry visible to throttles\n///param cab loco DCC address or 0 for default entry\n///param name Quoted text\n///param funcmap... Quoted text, optional list of function names separated by / character with momentary function names prefixed with an *.\n\n#define ROUTE(sequence_id,description)\n///brief Defines starting point of a sequence that will appear as a route on throttle buttons.\n///param description Quoted text, throttle button caption.\n\n#define ROUTE_ACTIVE(sequence_id)\n///brief Tells throttle to display the route button as active\n///param sequence_id of ROUTE/AUTOMATION\n\n#define ROUTE_INACTIVE(sequence_id)\n///brief Tells throttle to display the route button as inactive\n///param sequence_id of ROUTE/AUTOMATION\n\n#define ROUTE_HIDDEN(sequence_id)\n///brief Tells throttle to hide the route button\n///param sequence_id of ROUTE/AUTOMATION\n\n#define ROUTE_DISABLED(sequence_id)\n///brief Tells throttle to display the route button as disabled\n///param sequence_id of ROUTE/AUTOMATION\n\n#define ROUTE_CAPTION(sequence_id,caption)\n///brief Tells throttle to change thr route button caption\n///param sequence_id of ROUTE/AUTOMATION\n\n#define SAVE_SPEED\n///brief Saves loco speed for later restore\n///see RESTORE_SPEED\n\n#define SENDLOCO(cab,sequence_id)\n///brief Start a new task to drive the loco\n///param cab loco to be driven\n///param route sequence_id of route, automation or sequence to drive\n\n#define SEQUENCE(sequence_id)\n///brief Provides a unique label that can be used to call, follow or start.\n///see CALL\n///see FOLLOW\n///see START\n\n#define SERIAL(msg)\n///brief Write direct to Serial output\n///param msg Quoted text\n\n#define SERIAL1(msg)\n///brief Write direct to Serial1 output\n///param msg Quoted text\n\n#define SERIAL2(msg)\n///brief Write direct to Serial2 output\n///param msg Quoted text\n\n#define SERIAL3(msg)\n///brief Write direct to Serial3 output\n///param msg Quoted text\n\n#define SERIAL4(msg)\n///brief Write direct to Serial4 output\n///param msg Quoted text\n\n#define SERIAL5(msg)\n///brief Write direct to Serial5 output\n///param msg Quoted text\n\n#define SERIAL6(msg)\n///brief Write direct to Serial6 output\n///param msg Quoted text\n\n#define SERVO(vpin,position,profile)\n///brief Move servo to given position\n///param vpin of servo\n///param position  servo position (values are hardware dependent)\n///param profile movement profile (Instant, Fast, Medium, Slow, Bounce)\n\n#define SERVO2(vpin,position,duration)\n///brief Move servo to given position taking time\n///param vpin of servo\n///param position  servo position (values are hardware dependent)\n///param duration mS\n\n#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)\n///brief Dedfine a servo based signal with 3 servo positions\n///param vpin of servo, acts as signal_id\n///param redpos servo position (values are hardware dependent)\n///param amberpos servo position (values are hardware dependent)\n///param greenpos servo position (values are hardware dependent)\n\n#define SERVO_TURNOUT(turnout_id,vpin,activeAngle,inactiveAngle,profile,description...)\n///brief Define a servo driven turnout\n///param turnout_id used by THROW/CLOSE\n///param vpin for servo\n///param activeAngle servo position (values are hardware dependent)\n///param inactiveAngle servo position (values are hardware dependent)\n///param profile movement profile (Instant, Fast, Medium, Slow, Bounce)\n///param description... Quoted text shown to throttles or HIDDEN keyword to hide turnout button\n\n#define SET(vpin,count...)\n///brief  Set pin HIGH\n///see RESET\n///param count...  Number of sequential vpins to set. Default 1.\n\n#define SET_TRACK(track,mode)\n///brief Set output track type\n///param track A..H\n///param mode NONE, MAIN, PROG, DC, EXT, BOOST, BOOST_INV, BOOST_AUTO, MAIN_INV, MAIN_AUTO, DC_INV, DCX\n\n#define SET_POWER(track,onoff)\n///brief Set track power mode\n///param track A..H\n///param onoff ON or OFF\n\n#define SETLOCO(loco)\n///brief Sets the loco being handled by the current task\n\n#define SETFREQ(freq)\n///brief Sets the DC track PWM frequency\n///param freq Frequency is default 0, or 1..3\n\n#define SIGNAL(redpin,amberpin,greenpin)\n///brief Define a Signal with LOW=on leds\n///see SIGNALH\n///param redpin vpin for RED state, also acts as signal_id\n\n#define SIGNALH(redpin,amberpin,greenpin)\n///brief define a signal with HIGH=ON leds\n///param redpin vpin for RED state, also acts as signal_id\n\n#define SPEED(speed)\n///brief Changes current tasks loco speed without changing direction\n///param speed 0..127 (1=ESTOP)\n\n#define SPEEDUP(speedstep)\n///brief Increases current tasks loco speed by given amount\n///param speedstep 0..127\n\n#define SLOWDOWN(speedstep)\n///brief Decreases current tasks loco speed by given amount\n///param speedstep 0..127\n\n#define SPEED_REL(percent)\n///brief Sets current tasks loco speed to percentage of current speed\n///param percent 1..500\n\n#define START(sequence_id)\n///brief Starts a new task at the given route/animation/sequence\n\n#define START_SHARED(sequence_id)\n///brief Starts a new task at the given route/animation/sequence and share current loco with it\n\n#define START_SEND(sequence_id)\n///brief Starts a new task at the given route/animation/sequence and send current loco to it. Remove loco from current task.\n\n#define STASH(stash_id)\n///brief saves current task's loco id in the stash array\n///param stash_id  position in stash array to save loco id\n\n#define STEALTH(code...)\n///brief Allows for embedding raw C++ code in context of current task.\n///param code... c++ code to be executed. This requires intimate understanding of the product architecture.\n\n#define STEALTH_GLOBAL(code...)\n///brief Allows for embedding raw c++ code out of context.\n///param code...  c++ code to be defined. This requires intimate understanding of the product architecture.\n\n#define STOP\n///brief Same as SPEED(0)\n\n#define THROW(turnout_id)\n///brief Throws given turnout\n///see CLOSE\n\n#define TOGGLE_TURNOUT(turnout_id)\n///brief Toggles given turnout\n\n#define TT_ADDPOSITION(turntable_id,position_id,value,angle,description...)\n///brief Defines a turntable track position\n///param position_id each position is given an id\n///param address DCC accessory address\n///param angle Used only for throttles that may draw a visual representation of the turntable\n///param description... quoted text or HIDDEN\n\n#define TURNOUT(turnout_id,addr,subaddr,description...)\n///brief Defines a DCC accessory turnout with legacy address\n///param turnout_id to be used in THROW/CLOSE etc\n///param addr DCC accessory address\n///param subaddr DCC accessory subaddress\n///param description... Quoted text or HIDDEN, appears on throttle buttons\n\n#define TURNOUTL(turnout_id,addr,description...)\n///brief Defines a DCC accessory turnout with linear address\n///see TURNOUT\n///param turnout_id to be used in THROW/CLOSE etc\n///param addr DCC accessory linear address\n///param description... Quoted text or HIDDEN, appears on throttle buttons\n\n#define UNJOIN\n///brief Disconnects PROG track from MAIN\n///see JOIN\n\n#define UNLATCH(vpin)\n///brief removes latched on flag\n///see LATCH\n///param vpin (limited to 0..255)\n\n#define VIRTUAL_SIGNAL(signal_id)\n///brief Defines a virtual (no hardware) signal, use ONhandlers to simulate hardware\n///see SIGNAL ONRED ONAMBER ONGREEN\n\n#define VIRTUAL_TURNOUT(id,description...)\n///brief Defines a virtual (no hardware) turnout, use ONhandlers to simulate hardware\n///see TURNOUT ONCLOSE ONTHROW\n///param description... quoted text or HIDDEN\n\n#define BITMAP_AND(vpin1,mask)\n///brief Performs a bitwise AND operation on the given vpin analog value and mask.\n///param mask Binary mask to be ANDed with vpin1 value\n\n#define BITMAP_INC(vpin)\n///brief Increments pseudo analog value by 1\n\n#define BITMAP_DEC(vpin)\n///brief Decrements pseudo analog value by 1  (to zero)\n\n#define BITMAP_OR(vpin1,mask)\n///brief Performs a bitwise OR operation on the given vpin analog value and mask.\n///param mask Binary mask to be ORed with vpin1 value\n\n#define BITMAP_SET(vpin1,value)\n///brief Sets the given vpin analog value\n///param value Value to be set\n\n#define BITMAP_XOR(vpin1,mask)\n///brief Performs a bitwise XOR operation on the given vpin analog value and mask.\n///param mask Binary mask to be XORed with vpin1 value\n\n#define WAITFOR(pin)\n///brief Waits for completion of servo movement\n\n#ifndef IO_NO_HAL\n\n#define WAITFORTT(turntable_id)\n///brief waits for completion of turntable movement\n#endif\n\n#define WAIT_WHILE_RED(signal_id)\n///brief Keeps loco at speed 0 while signal is RED\n\n#define WITHROTTLE(msg)\n///brief Broadcasts a string in Withrottle protocol format to all throttles using this protocol.\n///param msg quoted string\n\n#define XFOFF(cab,func)\n///brief Turns function off for given loco\n///param func function number\n\n#define XFON(cab,func)\n///brief Turns function ON for given loco\n\n#define XFTOGGLE(cab,func)\n///brief Toggles function state for given loco\n\n#define XFWD(cab,speed)\n///brief Sends DCC speed to loco in forward direction\n///param speed (0..127, 1=ESTOP)\n\n#define XREV(cab,speed)\n///brief Sends DCC speed to loco in reverse direction\n///param speed (0..127, 1=ESTOP)\n\n#define XPOM(cab,cv,value)\n///brief updates a cv on a loco using Program on Main.\n///param cab loco id\n///param cv  to be updated\n///param value to be written to cv\n\n#define XRESTORE_SPEED(cab)\n///brief Resumes locos saved speed\n///param cab loco id\n///see XSAVE_SPEED\n\n#define XSAVE_SPEED(cab)\n///brief Saves loco speed for later restore\n///param cab loco id\n///see XRESTORE_SPEED\n\n#define ZTEST(command,testcode...)\n///brief Developer Unit testing.  Do not use. \n\n#define ZTEST2(command,reply)\n///brief Developer Unit testing.  Do not use.\n\n#define ZTEST3(command,reply,testcode...)\n///brief Developer Unit testing.  Do not use. \n"
  },
  {
    "path": "EXRAIL2MacroReset.h",
    "content": "/*\n *  © 2020-2025 Chris Harlow. All rights reserved.\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 Morten Nielsen\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see https://www.gnu.org/licenses/.\n */\n\n// This file cleans and resets the EXRAIL Macros.\n// It is used between passes to reduce complexity in EXRAILMacros.h\n// DO NOT add an include guard to this file.\n\n// Undefine all RMFT macros\n#undef ACTIVATE\n#undef ACTIVATEL\n#undef AFTER\n#undef AFTEROVERLOAD\n#undef ALIAS\n#undef AMBER\n#undef ANOUT\n#undef ASPECT\n#undef AT\n#undef ATGTE\n#undef ATLT\n#undef ATTIMEOUT\n#undef AUTOMATION \n#undef AUTOSTART\n#undef BLINK\n#undef BUILD_CONSIST\n#undef BREAK_CONSIST\n#undef BROADCAST\n#undef CALL \n#undef CLEAR_STASH\n#undef CLEAR_ALL_STASH\n#undef CLEAR_ANY_STASH\n#undef CLOSE \n#undef CONFIGURE_SERVO\n#undef DCC_SIGNAL\n#undef DCCX_SIGNAL\n#undef DCC_TURNTABLE\n#undef DEACTIVATE\n#undef DEACTIVATEL\n#undef DELAY\n#undef DELAYMINS\n#undef DELAYRANDOM \n#undef DONE\n#undef DRIVE\n#undef ELSE\n#undef ENDEXRAIL \n#undef ENDIF  \n#undef ENDTASK\n#undef ESTOP\n#undef ESTOPALL\n#undef ESTOP_PAUSE\n#undef ESTOP_RESUME\n#undef EXRAIL\n#undef EXTT_TURNTABLE\n#undef FADE\n#undef FOFF\n#undef FOLLOW \n#undef FON \n#undef FORGET\n#undef FTOGGLE\n#undef FREE\n#undef FREEALL\n#undef FWD \n#undef GREEN\n#undef HAL\n#undef HAL_IGNORE_DEFAULTS\n#undef IF \n#undef IF_ALL\n#undef IF_ANY\n#undef IFAMBER\n#undef IFCLOSED\n#undef IFGREEN\n#undef IFGTE\n#undef IFLOCO\n#undef IFLT\n#undef IFNOT\n#undef IFRANDOM \n#undef IFRED\n#undef IFRESERVE\n#undef IFSTASH\n#undef IFSTASHED_HERE\n#undef IFTHROWN\n#undef IFTIMEOUT\n#undef IFTTPOSITION\n#undef IFRE\n#undef IFROUTE_ACTIVE\n#undef IFROUTE_INACTIVE\n#undef IFROUTE_HIDDEN\n#undef IFROUTE_DISABLED\n#undef IFBITMAP_ALL\n#undef IFBITMAP_ANY\n#undef INVERT_DIRECTION \n#undef JMRI_SENSOR\n#undef JMRI_SENSOR_NOPULLUP\n#undef JOIN \n#undef KILLALL\n#undef LATCH \n#undef LCD \n#undef SCREEN\n#undef LCC \n#undef LCCX \n#undef LCN \n#undef MOMENTUM\n#undef MOVETT\n#undef NEOPIXEL\n#undef NEOPIXEL_OFF\n#undef NEOPIXEL_SIGNAL\n#undef ACON\n#undef ACOF\n#undef ONACON\n#undef ONACOF\n#undef MESSAGE\n#undef ONACTIVATE\n#undef ONACTIVATEL\n#undef ONAMBER\n#undef ONBLOCKENTER\n#undef ONBLOCKEXIT\n#undef ONDEACTIVATE\n#undef ONDEACTIVATEL \n#undef ONCLOSE\n#undef ONLCC\n#undef ONTIME\n#undef ONCLOCKTIME\n#undef ONCLOCKMINS\n#undef ONOVERLOAD\n#undef ONRAILSYNCON\n#undef ONRAILSYNCOFF\n#undef ONGREEN\n#undef ONRED\n#undef ONROTATE\n#undef ONBUTTON\n#undef ONSENSOR\n#undef ONTHROW \n#undef ONBITMAP\n#undef ONCHANGE\n#undef PARSE\n#undef PAUSE\n#undef PICKUP_STASH\n#undef PIN_TURNOUT\n#undef PLAY_EQ\n#undef PLAY_FOLDER\n#undef PLAY_PAUSE\n#undef PLAY_REPEAT\n#undef PLAY_RESET\n#undef PLAY_RESUME\n#undef PLAY_STOP\n#undef PLAY_TRACK\n#undef PLAY_VOLUME\n#undef PRINT\n#undef POM\n#undef POWEROFF\n#undef POWERON\n#undef RANDOM_CALL\n#undef RANDOM_FOLLOW\n#undef READ_LOCO \n#undef RED \n#undef RESERVE \n#undef RESET \n#undef RESTORE_SPEED\n#undef RESUME \n#undef RETURN \n#undef REV\n#undef ROSTER\n#undef ROTATE\n#undef ROTATE_DCC\n#undef ROUTE\n#undef ROUTE_ACTIVE\n#undef ROUTE_INACTIVE\n#undef ROUTE_HIDDEN\n#undef ROUTE_DISABLED\n#undef ROUTE_CAPTION\n#undef SAVE_SPEED\n#undef SENDLOCO \n#undef SEQUENCE \n#undef SERIAL \n#undef SERIAL1 \n#undef SERIAL2 \n#undef SERIAL3 \n#undef SERIAL4 \n#undef SERIAL5 \n#undef SERIAL6 \n#undef SERVO \n#undef SERVO2 \n#undef SERVO_TURNOUT \n#undef SERVO_SIGNAL\n#undef SET\n#undef SET_TRACK\n#undef SET_POWER\n#undef SETLOCO \n#undef SETFREQ\n#undef SIGNAL \n#undef SIGNALH \n#undef SPEED\n#undef SPEEDUP\n#undef SPEED_REL\n#undef SLOWDOWN\n#undef START\n#undef START_SHARED\n#undef START_SEND\n#undef STASH\n#undef STEALTH\n#undef STEALTH_GLOBAL\n#undef STOP \n#undef THROW\n#undef TOGGLE_TURNOUT\n#undef TT_ADDPOSITION\n#undef TURNOUT \n#undef TURNOUTL\n#undef UNJOIN\n#undef UNLATCH \n#undef VIRTUAL_SIGNAL\n#undef VIRTUAL_TURNOUT\n#undef WAITFOR\n#undef WAIT_WHILE_RED\n#ifndef IO_NO_HAL\n#undef BITMAP_AND\n#undef BITMAP_OR\n#undef BITMAP_SET\n#undef BITMAP_XOR\n#undef BITMAP_INC\n#undef BITMAP_DEC\n#undef WAITFORTT\n#endif\n#undef WITHROTTLE\n#undef XFOFF\n#undef XFON\n#undef XFTOGGLE\n#undef XPOM\n#undef XREV\n#undef XFWD\n#undef XSAVE_SPEED\n#undef XRESTORE_SPEED\n#undef ZTEST\n#undef ZTEST2\n#undef ZTEST3\n\n#ifndef RMFT2_UNDEF_ONLY\n#include \"EXRAIL2MacroBase.h\"\n#endif\n"
  },
  {
    "path": "EXRAIL2Parser.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021-2023 Harald Barth\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// THIS file is an extension of the RMFT2 class \n//  normally found in EXRAIL2.cpp\n\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"EXRAIL2.h\"\n#include \"DCC.h\"\n#include \"KeywordHasher.h\"\n\n// This filter intercepts <> commands to do the following:\n// - Implement RMFT specific commands/diagnostics\n// - Reject/modify JMRI commands that would interfere with RMFT processing\n\nvoid RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {\n  (void)stream; // avoid compiler warning if we don't access this parameter\n  \n  switch(opcode) {\n    \n  case 'D':\n    if (p[0]==\"EXRAIL\"_hk) { // <D EXRAIL ON/OFF>\n      diag = paramCount==2 && (p[1]==\"ON\"_hk || p[1]==1);\n      opcode=0;\n    }\n    break;\n\t\n  case '/':  // New EXRAIL command\n    if (parseSlash(stream,paramCount,p)) opcode=0;\n    break;\n  \n  case 'A': //  <A address aspect>\n    if (paramCount!=2) break; \n    // Ask exrail if this is just changing the aspect on a \n    // predefined DCCX_SIGNAL. Because this will handle all \n    // the IFRED and ONRED type issues at the same time.  \n    if (signalAspectEvent(p[0],p[1])) opcode=0; // all done \n    break;\n\n  case 'L':\n    // This entire code block is compiled out if LLC macros not used \n    if (!(compileFeatures & FEATURE_LCC)) return;\n    static int lccProgCounter=0;\n    static int lccEventIndex=0;\n      \n    if (paramCount==0) {  //<L>  LCC adapter introducing self\n      LCCSerial=stream;   // now we know where to send events we raise\n      opcode=0;  // flag command as intercepted\n\n      // loop through all possible sent/waited events \n      for (int progCounter=lccProgCounter;; SKIPOP) {\n        byte exrailOpcode=GET_OPCODE;\n        switch (exrailOpcode) {\n          case OPCODE_ENDEXRAIL:\n               stream->print(F(\"<LR>\\n\")); // ready to roll\n               lccProgCounter=0; // allow a second pass\n               lccEventIndex=0;\n               return;\n\n          case OPCODE_LCC:  \n               StringFormatter::send(stream,F(\"<LS x%h>\\n\"),getOperand(progCounter,0));\n               SKIPOP;\n               lccProgCounter=progCounter; \n               return;\n\n          case OPCODE_LCCX:  // long form LCC\n               StringFormatter::send(stream,F(\"<LS x%h%h%h%h>\\n\"),\n                 getOperand(progCounter,1),\n                 getOperand(progCounter,2),\n                 getOperand(progCounter,3),\n                 getOperand(progCounter,0)\n                 );\n               SKIPOP;SKIPOP;SKIPOP;SKIPOP;          \n               lccProgCounter=progCounter; \n               return;\n\n          case OPCODE_ACON:  // CBUS ACON \n          case OPCODE_ACOF:  // CBUS ACOF \n                StringFormatter::send(stream,F(\"<LS x%c%h%h>\\n\"),\n                  exrailOpcode==OPCODE_ACOF?'1':'0',\n                  getOperand(progCounter,0),getOperand(progCounter,1)); \n               SKIPOP;SKIPOP;\n               lccProgCounter=progCounter; \n               return;\n      \n      // we stream the hex events we wish to listen to\n      // and at the same time build the event index looku.\n      \n        case OPCODE_ONLCC:\n           StringFormatter::send(stream,F(\"<LL %d x%h%h%h:%h>\\n\"),\n                 lccEventIndex,\n                 getOperand(progCounter,1),\n                 getOperand(progCounter,2),\n                 getOperand(progCounter,3),\n                 getOperand(progCounter,0)\n                 );   \n           SKIPOP;SKIPOP;SKIPOP;SKIPOP;      \n           // start on handler at next      \n           onLCCLookup[lccEventIndex]=progCounter; \n           lccEventIndex++;        \n           lccProgCounter=progCounter; \n           return;\n\n        case OPCODE_ONACON:\n        case OPCODE_ONACOF:\n           StringFormatter::send(stream,F(\"<LL %d x%c%h%h>\\n\"),\n                 lccEventIndex,\n                 exrailOpcode==OPCODE_ONACOF?'1':'0',\n                 getOperand(progCounter,0),getOperand(progCounter,1)\n                 ); \n           SKIPOP;SKIPOP;\n           // start on handler at next      \n           onLCCLookup[lccEventIndex]=progCounter; \n           lccEventIndex++;        \n           lccProgCounter=progCounter; \n           return;\n           \n         default:\n           break;\n        }  \n      }\n    }\n    if (paramCount==1) {  // <L eventid> LCC event arrived from adapter\n        int16_t eventid=p[0];\n        bool reject = eventid<0 || eventid>=countLCCLookup;\n        if (!reject) {\n          startNonRecursiveTask(F(\"LCC\"),eventid,onLCCLookup[eventid]);\n          opcode=0;\n        }\n    }\n    break; \n    \n    case 'J':  // throttle info commands\n        if (paramCount<1) return; \n        switch(p[0]) {\n          case \"A\"_hk: // <JA> returns automations/routes\n            if (paramCount==1) {// <JA>\n              StringFormatter::send(stream, F(\"<jA\"));\n              routeLookup->stream(stream);\n              StringFormatter::send(stream, F(\">\\n\"));\n              opcode=0;\n              return; \n            }\n            if (paramCount==2) {  // <JA id>\n              int16_t id=p[1];\n              StringFormatter::send(stream,F(\"<jA %d %c \\\"%S\\\">\\n\"), \n                id, getRouteType(id), getRouteDescription(id));\n              \n              if (compileFeatures & FEATURE_ROUTESTATE) {\n                // Send any non-default button states or captions\n                int16_t statePos=routeLookup->findPosition(id);\n                if (statePos>=0) {\n                 if (routeStateArray[statePos]) \n                 StringFormatter::send(stream,F(\"<jB %d %d>\\n\"), id, routeStateArray[statePos]);\n                  if (routeCaptionArray[statePos]) \n                  StringFormatter::send(stream,F(\"<jB %d \\\"%S\\\">\\n\"), id,routeCaptionArray[statePos]);\n                }\n              }\n              opcode=0;\n              return;\n            }\n            break;\n        \n\n  case 'K': // <K blockid loco>  Block enter\n  case 'k': // <k blockid loco>  Block exit\n        if (paramCount!=2) break;\n        blockEvent(p[0],p[1],opcode=='K');\n        opcode=0;\n        break; \n  \n  default:  // other commands pass through\n    break;\n  }\n}\n}\n\nbool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {\n\n  if (paramCount==0) { // STATUS\n    StringFormatter::send(stream, F(\"<* EXRAIL STATUS\"));\n    RMFT2 * task=loopTask;\n    while(task) {\n      if ((compileFeatures & FEATURE_BLINK)\n       && (task->blinkState==blink_high || task->blinkState==blink_low)) {\n        StringFormatter::send(stream,F(\"\\nID=%d,PC=%d,BLINK=%d\"),\n\t\t\t    (int)(task->taskId),task->progCounter,task->blinkPin\n\t\t\t    );\n      }\n      else {\n      StringFormatter::send(stream,F(\"\\nID=%d,PC=%d,LOCO=%d %c\"),\n\t\t\t    (int)(task->taskId),task->progCounter,task->loco,\n\t\t\t    task->invert?'I':' '\n\t\t\t    );\n                auto progCounter=task->progCounter; // name to satisfy macros below\n          auto operand=task->getOperand(progCounter,0);\n          switch(GET_OPCODE) {\n              case OPCODE_RESERVE:\n                StringFormatter::send(stream,F(\" WAIT RESERVE %d\"),operand);\n                break;\n              case OPCODE_AT:\n              case OPCODE_ATTIMEOUT2:\n              case OPCODE_AFTER:\n              case OPCODE_ATGTE:\n              case OPCODE_ATLT:\n                StringFormatter::send(stream,F(\" WAIT AT/AFTER %d\"),(int16_t)operand);\n                break;\n              case OPCODE_WAIT_WHILE_RED:\n                StringFormatter::send(stream,F(\" WAIT WHILE RED %d\"),operand);\n                break;  \n              case OPCODE_DELAY:\n              case OPCODE_DELAYMINS:\n              case OPCODE_DELAYMS:\n              case OPCODE_RANDWAIT:\n                StringFormatter::send(stream,F(\" WAIT DELAY\"));\n                break; \n            default: break;\n          }\n      }\n      task=task->next;\n      if (task==loopTask) break;\n    }\n    // Now stream the flags\n    for (int id=0;id<MAX_FLAGS; id++) {\n      byte flag=flags[id];\n      if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above\n\t      StringFormatter::send(stream,F(\"\\nflags[%d] \"),id);\n\t      if (flag & SECTION_FLAG) StringFormatter::send(stream,F(\" RESERVED\"));\n\t      if (flag & LATCH_FLAG) StringFormatter::send(stream,F(\" LATCHED\"));\n      }\n    }\n\n    if (compileFeatures & FEATURE_SIGNAL) {\n      // do the signals\n      // flags[n] represents the state of the nth signal in the table \n      for (int sigslot=0;;sigslot++) {\n        SIGNAL_DEFINITION slot=getSignalSlot(sigslot);\n        if (slot.type==sigtypeNoMoreSignals) break; // end of signal list\n\t      if (slot.type==sigtypeContinuation) continue; // continueation of previous line\n\t      byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this ids\n        StringFormatter::send(stream,F(\"\\n%S[%d]\"), \n\t\t\t      (flag == SIGNAL_RED)? F(\"RED\") : (flag==SIGNAL_GREEN) ? F(\"GREEN\") : F(\"AMBER\"),\n\t\t\t      slot.id);\n      } \n    }\n    StringFormatter::send(stream,F(\" *>\\n\"));\n    return true;\n  }\n  switch (p[0]) {\n  case \"PAUSE\"_hk: // </ PAUSE>\n    if (paramCount!=1) return false;\n    { // pause all tasks \n      RMFT2 * task=loopTask;\n      while(task) {\n\t      task->pause();\n\t      task=task->next;\n\t      if (task==loopTask) break;\n      }\n    }\n    DCC::estopAll();  // pause all locos on the track\n    pausingTask=(RMFT2 *)1; // Impossible task address\n    return true;\n    \n  case \"RESUME\"_hk: // </ RESUME>\n    if (paramCount!=1) return false;\n    pausingTask=NULL;\n    { // resume all tasks\n      RMFT2 * task=loopTask;\n      while(task) {\n\t      task->resume();\n\t      task=task->next;\n\t      if (task==loopTask) break;\n      }\n    }\n    return true;\n      \n  case \"FREEALL\"_hk:  // force free all\n    if (paramCount!=1) return false;\n    for (int i=0;i<MAX_FLAGS;i++) setFlag(i,0,SECTION_FLAG);\n    return true;\n    \n  case \"START\"_hk: // </ START [cab] route >\n    if (paramCount<2 || paramCount>3) return false;\n    {\n      int route=(paramCount==2) ? p[1] : p[2];\n      uint16_t cab=(paramCount==2)? 0 : p[1];\n      int pc=routeLookup->find(route);\n      if (pc<0) return false;\n      new RMFT2(pc,cab);\n    }\n    return true;\n    \n  default:\n    break;\n  }\n\n  // check KILL ALL here, otherwise the next validation confuses ALL with a flag  \n  if (p[0]==\"KILL\"_hk && p[1]==\"ALL\"_hk) {\n    while (loopTask) loopTask->kill(F(\"KILL ALL\")); // destructor changes loopTask\n    return true;   \n  }\n\n  // all other / commands take 1 parameter\n  if (paramCount!=2 ) return false;\n  \n  switch (p[0]) {\n  case \"KILL\"_hk: // Kill taskid|ALL\n    {\n    if ( p[1]<0  || p[1]>=MAX_FLAGS) return false;\n    RMFT2 * task=loopTask;\n      while(task) {\n\t      if (task->taskId==p[1]) {\n\t        task->kill(F(\"KILL\"));\n\t        return  true;\n\t      }\n\t      task=task->next;\n\t      if (task==loopTask) break;\n      }\n    }\n    return false;\n    \n  case \"RESERVE\"_hk:  // force reserve a section\n    return setFlag(p[1],SECTION_FLAG);\n    \n  case \"FREE\"_hk:  // force free a section\n    return setFlag(p[1],0,SECTION_FLAG);\n    \n  case \"LATCH\"_hk:\n    return setFlag(p[1], LATCH_FLAG);\n    \n  case \"UNLATCH\"_hk:\n    return setFlag(p[1], 0, LATCH_FLAG);\n \n  case \"RED\"_hk:\n    doSignal(p[1],SIGNAL_RED);\n    return true;\n \n  case \"AMBER\"_hk:\n    doSignal(p[1],SIGNAL_AMBER);\n    return true;\n \n  case \"GREEN\"_hk:\n    doSignal(p[1],SIGNAL_GREEN);\n    return true;\n    \n  default:\n    return false;\n  }\n}\n"
  },
  {
    "path": "EXRAILAsserts.h",
    "content": "/*\n *  © 2025 Paul M. Antoine\n *  © 2020-2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n// This file checks the myAutomation for errors by generating a list of compile time asserts.\n\n// Assert Pass 1 Collect sequence numbers.\n#include \"EXRAIL2MacroReset.h\"\n#undef AUTOMATION\n#define AUTOMATION(id, description) id,\n#undef ROUTE\n#define ROUTE(id, description) id,\n#undef SEQUENCE\n#define SEQUENCE(id) id,\nconstexpr int16_t compileTimeSequenceList[]={\n   #include \"myAutomation.h\"\n   0\n   };\nconstexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;\n\n\n// Compile time function to check for sequence number duplication\nconstexpr int16_t seqCount(const int16_t value, const int16_t pos=0, const int16_t count=0 ) {\n   return pos>=stuffSize? count :\n             seqCount(value,pos+1,count+((compileTimeSequenceList[pos]==value)?1:0));\n}\n\n\n// Build a compile time blacklist of pin numbers. \n// Includes those defined in defaults.h for the cpu (PIN_BLACKLIST)\n// and cheats in the motor shield pins from config.h (MOTOR_SHIELD_TYPE)\n// for reference the MotorDriver constructor is:\n// MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin, byte current_pin,\n//             float senseFactor, unsigned int tripMilliamps, byte faultPin);\n\n// create capture macros to reinterpret MOTOR_SHIELD_TYPE from configuration\n#define new\n#define MotorDriver(power_pin,signal_pin,signal_pin2, \\\n   brake_pin,current_pin,senseFactor,tripMilliamps,faultPin) \\\n   abs(power_pin),abs(signal_pin),abs(signal_pin2),abs(brake_pin),abs(current_pin),abs(faultPin)\n#ifndef PIN_BLACKLIST \n  #define PIN_BLACKLIST UNUSED_PIN\n#endif \n#define MDFURKLE(stuff) MDFURKLE2(stuff)\n#define MDFURKLE2(description,...) REMOVE_TRAILING_COMMA(__VA_ARGS__)\n#define REMOVE_TRAILING_COMMA(...) __VA_ARGS__\n\n\nconstexpr int16_t compileTimePinBlackList[]={\n   PIN_BLACKLIST, MDFURKLE(MOTOR_SHIELD_TYPE)\n   };\nconstexpr int16_t pbSize=sizeof(compileTimePinBlackList)/sizeof(int16_t);\n\n\n// remove capture macros\n#undef new\n#undef MotorDriver\n\n// Compile time function to check for dangerous pins.\nconstexpr bool unsafePin(const int16_t value, const int16_t pos=0 ) {\n   return pos>=pbSize? false :\n      compileTimePinBlackList[pos]==value \n      || unsafePin(value,pos+1); \n}\n\n\n//pass 2 apply static asserts:\n// check call and follows etc for existing sequence numbers\n// check sequence numbers for duplicates\n// check range on LATCH/UNLATCH\n// check range on RESERVE/FREE\n// check range on SPEED/FWD/REV\n// check range on SET/RESET   (pins that are not safe to use in EXRAIL)\n// \n// This pass generates no runtime data or code \n#include \"EXRAIL2MacroReset.h\"\n#undef ASPECT\n#define ASPECT(address,value) static_assert(address <=2044, \"invalid Address\"); \\\n                              static_assert(address>=-3, \"Invalid value\");\n\n// check references to sequences/routes/automations\n#undef CALL\n#define CALL(id) static_assert(seqCount(id)>0,\"Sequence  not found\");\n#undef FOLLOW\n#define FOLLOW(id)  static_assert(seqCount(id)>0,\"Sequence not found\");\n\n// random call and follow will generate CALL macros here which\n// will check for invalid sequences\n#undef RANDOM_CALL\n#define RANDOM_CALL(...) \\\n  ZCRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__)\n#undef RANDOM_FOLLOW\n#define RANDOM_FOLLOW(...) \\\n  ZCRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__)\n\n#undef START\n#define START(id)  static_assert(seqCount(id)>0,\"Sequence not found\");\n#undef START_SHARED\n#define START_SHARED(id)  static_assert(seqCount(id)>0,\"Sequence not found\");\n#undef START_SEND\n#define START_SEND(id)  static_assert(seqCount(id)>0,\"Sequence not found\");\n#undef SENDLOCO\n#define SENDLOCO(cab,id) static_assert(seqCount(id)>0,\"Sequence not found\");\n#undef ROUTE_ACTIVE\n#define ROUTE_ACTIVE(id)  static_assert(seqCount(id)>0,\"Route not found\");\n#undef ROUTE_INACTIVE\n#define ROUTE_INACTIVE(id)  static_assert(seqCount(id)>0,\"Route not found\");\n#undef ROUTE_HIDDEN\n#define ROUTE_HIDDEN(id)  static_assert(seqCount(id)>0,\"Route not found\");\n#undef ROUTE_DISABLED\n#define ROUTE_DISABLED(id)  static_assert(seqCount(id)>0,\"Route not found\");\n#undef ROUTE_CAPTION \n#define ROUTE_CAPTION(id,caption) static_assert(seqCount(id)>0,\"Route not found\");\n\n\n#undef LATCH\n#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,\"Id out of valid range 0-255\" );\n#undef UNLATCH\n#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,\"Id out of valid range 0-255\" );\n#undef RESERVE\n#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,\"Id out of valid range 0-255\" );\n#undef FREE\n#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,\"Id out of valid range 0-255\" );\n#undef IFRESERVE\n#define IFRESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,\"Id out of valid range 0-255\" );\n\n//check speeds\n#undef SPEED\n#define SPEED(speed) static_assert(speed>=0 && speed<128,\"\\n\\nUSER ERROR: Speed out of valid range 0-127\\n\");\n#undef FWD\n#define FWD(speed) static_assert(speed>=0 && speed<128,\"\\n\\nUSER ERROR: Speed out of valid range 0-127\\n\");\n#undef REV\n#define REV(speed) static_assert(speed>=0 && speed<128,\"\\n\\nUSER ERROR: Speed out of valid range 0-127\\n\");\n#undef SPEEDUP\n#define SPEEDUP(speedstep) static_assert(speedstep>=0 && speedstep<128,\"\\n\\nUSER ERROR: Speed step out of valid range 0-127\\n\");\n#undef SPEED_REL\n#define SPEED_REL(percent) static_assert(percent>0 && percent<=500,\"\\n\\nUSER ERROR: Speed out of valid range 1-500 %\\n\");\n#undef SLOWDOWN\n#define SLOWDOWN(speedstep) static_assert(speedstep>=0 && speedstep<128,\"\\n\\nUSER ERROR: Speed step out of valid range 0-127\\n\");\n\n// check duplicate sequences\n#undef SEQUENCE\n#define SEQUENCE(id) static_assert(seqCount(id)==1,\"\\n\\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(\" #id \")\\n\");\n#undef AUTOMATION\n#define AUTOMATION(id,description) static_assert(seqCount(id)==1,\"\\n\\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(\" #id \")\\n\");\n#undef ROUTE\n#define ROUTE(id,description) static_assert(seqCount(id)==1,\"\\n\\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(\" #id \")\\n\");\n\n// check dangerous pins\n#define _PIN_RESERVED_ \"\\n\\nUSER ERROR: Pin is used by Motor Shield or other critical function.\\n\"\n#undef SET\n#define SET(vpin, ...) static_assert(!unsafePin(vpin),\"SET(\" #vpin \")\" _PIN_RESERVED_);\n#undef RESET\n#define RESET(vpin,...) static_assert(!unsafePin(vpin),\"RESET(\" #vpin \")\" _PIN_RESERVED_);\n#undef BLINK\n#define BLINK(vpin,onDuty,offDuty) static_assert(!unsafePin(vpin),\"BLINK(\" #vpin \")\" _PIN_RESERVED_);\n#undef SIGNAL\n#define SIGNAL(redpin,amberpin,greenpin) \\\n      static_assert(!unsafePin(redpin),\"Red pin \" #redpin _PIN_RESERVED_); \\\n      static_assert(amberpin==0 ||!unsafePin(amberpin),\"Amber pin \" #amberpin _PIN_RESERVED_); \\\n      static_assert(!unsafePin(greenpin),\"Green pin \" #greenpin _PIN_RESERVED_); \n#undef SIGNALH\n#define SIGNALH(redpin,amberpin,greenpin) \\\n      static_assert(!unsafePin(redpin),\"Red pin \" #redpin _PIN_RESERVED_); \\\n      static_assert(amberpin==0 ||!unsafePin(amberpin),\"Amber pin \" #amberpin _PIN_RESERVED_); \\\n      static_assert(!unsafePin(greenpin),\"Green pin \" #greenpin _PIN_RESERVED_); \n\n// and run the assert pass.       \n#include \"myAutomation.h\"\n"
  },
  {
    "path": "EXRAILMacros.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Chris Harlow\n *  © 2022-2023 Colin Murdoch\n *  © 2023 Harald Barth\n *  © 2025 Morten Nielsen\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef EXRAILMacros_H\n#define EXRAILMacros_H\n#include \"IODeviceList.h\"\n\n// remove normal code LCD & SERIAL macros (will be restored later)\n#undef LCD\n#undef SERIAL\n\n\n// This file will include and build the EXRAIL script and associated helper tricks.\n// It does this by including myAutomation.h several times, each with a set of macros to \n// extract the relevant parts.\n\n// The entire automation script is contained within a byte array RMFT2::RouteCode[]\n// made up of opcode and parameter pairs.\n// ech opcode is a 1 byte operation plus 2 byte operand. \n// The array is normally built using the macros below as this makes it easier \n// to manage the cases where:\n// - padding must be applied to ensure the correct alignment of the next instruction\n// - large parameters must be split up\n// - multiple parameters aligned correctly\n// - a single macro requires multiple operations\n\n// Descriptive texts for routes and animations are created in a sepaerate function which\n// can be called to emit a list of routes/automatuions in a form suitable for Withrottle. \n \n// PRINT(msg), LCD(row,msg) and SCREEN(display,row,msg) are implemented in a separate pass to create \n// a getMessageText(id) function. (also now IFLOCO).\n\n// CAUTION: The macros below are multiple passed over myAutomation.h\n\n\n// helper macro for turnout descriptions, creates NULL for missing description\n#define O_DESC(id, desc) case id: return (\"\" desc)[0]?F(\"\" desc):NULL;\n// helper macro for turntable descriptions, creates NULL for missing description\n#define T_DESC(tid,pid,desc) if(turntableId==tid && positionId==pid) return (\"\" desc)[0]?F(\"\" desc):NULL;\n// helper macro for turnout description as HIDDEN \n#define HIDDEN \"\\x01\"\n\n// PLAYSOUND is deprecated and appears here temporarily so it does not get \n// extracted into the documentation. \n#define PLAYSOUND(vpin,v1,v2,code) ANOUT(vpin,v1,v2,DFPLayerBase::code)\n\n// SEG7 is a helper to create ANOUT from a 7-segment request\n#define SEG7(vpin,value,format) \\\n   ANOUT(vpin,(value & 0xFFFF),TM1638::DF_##format,((uint32_t)value)>>16)\n\n// helper macro to strip leading zeros off time inputs\n// (10#mins)%100)\n#define STRIP_ZERO(value) 10##value%100\n\n// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).\n//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;    \n//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;   \n\n// NEOPIXEL RG generator for NEOPIXEL_SIGNAL \n#define NeoRGB(red,green,blue) (((uint32_t)(red & 0xff)<<16) | ((uint32_t)(green & 0xff)<<8) | (uint32_t)(blue & 0xff))  \n\n// Collection of macros to assist variadic exrail macros\n// in particular RANDOMCALL and RANDOMFOLLOW\n// Count the number of arguments\n#define FOR_EACH_NARG(...) FOR_EACH_NARG_HELPER(__VA_ARGS__,8,7, 6,5,4, 3, 2, 1, 0)\n#define FOR_EACH_NARG_HELPER(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N\n// Force proper expansion (extra indirection to resolve `##`)\n#define _CONCAT_(a, b) a##b\n#define _EXPAND_(a) a\n\n#define ZC0()                      \n#define ZC1(_1)                      CALL(_1) \n#define ZC2(_1,_2)                   CALL(_1) CALL(_2)\n#define ZC3(_1,_2,_3)                CALL(_1) CALL(_2) CALL(_3)\n#define ZC4(_1,_2,_3,_4)             CALL(_1) CALL(_2) CALL(_3) CALL(_4)\n#define ZC5(_1,_2,_3,_4,_5)          CALL(_1) CALL(_2) CALL(_3) CALL(_4) CALL(_5)\n#define ZC6(_1,_2,_3,_4,_5,_6)       CALL(_1) CALL(_2) CALL(_3) CALL(_4) CALL(_5) CALL(_6)\n#define ZC7(_1,_2,_3,_4,_5,_6,_7)    CALL(_1) CALL(_2) CALL(_3) CALL(_4) CALL(_5) CALL(_6) CALL(_7)\n#define ZC8(_1,_2,_3,_4,_5,_6,_7,_8) CALL(_1) CALL(_2) CALL(_3) CALL(_4) CALL(_5) CALL(_6) CALL(_7) CALL(_8)\n#define ZCRIP(count) _EXPAND_(_CONCAT_(ZC,count))\n\n\n// Pass 1 Implements aliases \n#include \"EXRAIL2MacroReset.h\"\n#undef ALIAS\n#define ALIAS(name,value...) const int name= #value[0] ? value+0: -__COUNTER__ ; \n#include \"myAutomation.h\"\n\n// Perform compile time asserts to check the script for errors\n#include \"EXRAILAsserts.h\"\n\n// Pass 1g Implants STEALTH_GLOBAL in correct place \n#include \"EXRAIL2MacroReset.h\"\n#undef STEALTH_GLOBAL\n#define STEALTH_GLOBAL(code...) code\n#include \"myAutomation.h\"\n\n// Pass 1h Implements HAL macro by creating exrailHalSetup1 function\n// Also allows creating EXTurntable object\n#include \"EXRAIL2MacroReset.h\"\n#undef HAL\n#define HAL(haltype,params...)  haltype::create(params);\n#undef HAL_IGNORE_DEFAULTS\n#define HAL_IGNORE_DEFAULTS ignore_defaults=true;\nbool exrailHalSetup1() {\n   bool ignore_defaults=false;\n   #include \"myAutomation.h\"\n   return ignore_defaults;\n}\n\n// Pass 1s Implements servos by creating exrailHalSetup2\n// TODO Turnout and turntable creation should be moved to here instead of \n// the first pass from the opcode table. \n#include \"EXRAIL2MacroReset.h\"\n#undef JMRI_SENSOR\n#define JMRI_SENSOR(vpin,count...) \\\n  { \\\n   const int npins=#count[0]? count+0:1; \\\n   static byte state_map[(npins+7)/8]; \\\n   SensorGroup::doSensorGroup(vpin,npins,state_map,action,stream,true); \\\n  }\n#undef JMRI_SENSOR_NOPULLUP\n#define JMRI_SENSOR_NOPULLUP(vpin,count...) \\\n  { \\\n   const int npins=#count[0]? count+0:1; \\\n   static byte state_map[(npins+7)/8]; \\\n   SensorGroup::doSensorGroup(vpin,npins,state_map,action,stream,false); \\\n  }\n\nvoid SensorGroup::doExrailSensorGroup(GroupProcess action, Print * stream) {\n   (void)   action; // suppress unused warnings if no groups\n   (void)   stream;\n   #include \"myAutomation.h\"\n}\n\n// Pass 1s Implements servos by creating exrailHalSetup2\n// TODO Turnout and turntable creation should be moved to here instead of \n// the first pass from the opcode table. \n#include \"EXRAIL2MacroReset.h\"\n#undef  CONFIGURE_SERVO\n#define CONFIGURE_SERVO(vpin,pos1,pos2,profile) IODevice::configureServo(vpin,pos1,pos2,PCA9685::profile);\nvoid exrailHalSetup2() {\n   #include \"myAutomation.h\"\n   // pullup any group sensors\n   SensorGroup::prepareAll();\n}\n\n// Pass 1c detect compile time featurtes\n#include \"EXRAIL2MacroReset.h\"\n#undef SIGNAL\n#define SIGNAL(redpin,amberpin,greenpin) | FEATURE_SIGNAL \n#undef SIGNALH\n#define SIGNALH(redpin,amberpin,greenpin) | FEATURE_SIGNAL \n#undef SERVO_SIGNAL\n#define SERVO_SIGNAL(vpin,redval,amberval,greenval) | FEATURE_SIGNAL \n#undef DCC_SIGNAL\n#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL\n#undef DCCX_SIGNAL\n#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL\n#undef NEOPIXEL_SIGNAL\n#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour) | FEATURE_SIGNAL\n#undef VIRTUAL_SIGNAL\n#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL\n\n#undef LCC\n#define LCC(eventid)  | FEATURE_LCC\n#undef LCCX\n#define LCCX(senderid,eventid) | FEATURE_LCC \n#undef ONLCC\n#define ONLCC(senderid,eventid) | FEATURE_LCC\n#undef ACON\n#define ACON(eventid) | FEATURE_LCC\n#undef ACOF\n#define ACOF(eventid) | FEATURE_LCC\n#undef ONACON\n#define ONACON(eventid) | FEATURE_LCC\n#undef ONACOF\n#define ONACOF(eventid) | FEATURE_LCC\n#undef ROUTE_ACTIVE\n#define ROUTE_ACTIVE(id) | FEATURE_ROUTESTATE\n#undef ROUTE_INACTIVE\n#define ROUTE_INACTIVE(id) | FEATURE_ROUTESTATE\n#undef ROUTE_HIDDEN\n#define ROUTE_HIDDEN(id) | FEATURE_ROUTESTATE\n#undef ROUTE_DISABLED\n#define ROUTE_DISABLED(id) | FEATURE_ROUTESTATE\n#undef ROUTE_CAPTION\n#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE\n#undef IFROUTE_ACTIVE\n#define IFROUTE_ACTIVE(id) | FEATURE_ROUTESTATE\n#undef IFROUTE_INACTIVE\n#define IFROUTE_INACTIVE(id) | FEATURE_ROUTESTATE\n#undef IFROUTE_HIDDEN\n#define IFROUTE_HIDDEN(id) | FEATURE_ROUTESTATE\n#undef IFROUTE_DISABLED\n#define IFROUTE_DISABLED(id) | FEATURE_ROUTESTATE\n#undef BLINK\n#define BLINK(vpin,onDuty,offDuty) | FEATURE_BLINK\n#undef ONBUTTON\n#define ONBUTTON(vpin) | FEATURE_SENSOR\n#undef ONSENSOR\n#define ONSENSOR(vpin) | FEATURE_SENSOR\n#undef ONBITMAP\n#define ONBITMAP(vpin) | FEATURE_SENSOR\n#undef ONBLOCKENTER\n#define ONBLOCKENTER(blockid) | FEATURE_BLOCK\n#undef ONBLOCKEXIT\n#define ONBLOCKEXIT(blockid) | FEATURE_BLOCK\n\nconst byte RMFT2::compileFeatures = 0\n   #include \"myAutomation.h\"\n;\n\n// Pass 2 create throttle route list \n#include \"EXRAIL2MacroReset.h\"\n#undef ROUTE\n#define ROUTE(id, description) id,\nconst int16_t HIGHFLASH  RMFT2::routeIdList[]= {\n    #include \"myAutomation.h\"\n    INT16_MAX};\n// Pass 2a create throttle automation list \n#include \"EXRAIL2MacroReset.h\"\n#undef AUTOMATION\n#define AUTOMATION(id, description) id,\nconst int16_t HIGHFLASH RMFT2::automationIdList[]= {\n    #include \"myAutomation.h\"\n    INT16_MAX};\n\n// Pass 3 Create route descriptions:\n#undef ROUTE\n#define ROUTE(id, description) case id: return F(description);\n#undef AUTOMATION\n#define AUTOMATION(id, description) case id: return F(description);\nconst FSH * RMFT2::getRouteDescription(int16_t id) {\n   switch(id) {\n    #include \"myAutomation.h\"\n    default: break;\n   }\n   return F(\"\");\n}\n\n// Pass 4... Create Text sending functions\n#include \"EXRAIL2MacroReset.h\"\nconst int StringMacroTracker1=__COUNTER__;\n#define THRUNGE(msg,mode) \\\n     case (__COUNTER__ - StringMacroTracker1) : {\\\n         static const char HIGHFLASH thrunge[]=msg;\\\n         strfar=(uint32_t)GETFARPTR(thrunge);\\\n         tmode=mode;\\\n         break;\\\n      } \n#undef BROADCAST\n#define BROADCAST(msg) THRUNGE(msg,thrunge_broadcast)\n#undef PARSE\n#define PARSE(msg) THRUNGE(msg,thrunge_parse)\n#undef PRINT\n#define PRINT(msg) THRUNGE(msg,thrunge_print)\n#undef LCN\n#define LCN(msg)   THRUNGE(msg,thrunge_lcn)\n#undef MESSAGE\n#define MESSAGE(msg) THRUNGE(msg,thrunge_message)\n\n#undef ROUTE_CAPTION\n#define ROUTE_CAPTION(id,caption) \\\ncase (__COUNTER__ - StringMacroTracker1) : {\\\n   manageRouteCaption(id,F(caption));\\\n   return;\\\n   }\n#undef SERIAL\n#define SERIAL(msg)   THRUNGE(msg,thrunge_serial)\n#undef SERIAL1\n#define SERIAL1(msg)  THRUNGE(msg,thrunge_serial1)\n#undef SERIAL2\n#define SERIAL2(msg)  THRUNGE(msg,thrunge_serial2)\n#undef SERIAL3\n#define SERIAL3(msg)  THRUNGE(msg,thrunge_serial3)\n#undef SERIAL4\n#define SERIAL4(msg)  THRUNGE(msg,thrunge_serial4)\n#undef SERIAL5\n#define SERIAL5(msg)  THRUNGE(msg,thrunge_serial5)\n#undef SERIAL6\n#define SERIAL6(msg)  THRUNGE(msg,thrunge_serial6)\n#undef IFLOCO\n#define IFLOCO(locolist...) \\\n  case (__COUNTER__ - StringMacroTracker1) : \\\n  { \\\n   const int16_t temp[]={locolist}; \\\n   skipIf=true; \\\n   for (size_t i=0; i<sizeof(temp)/sizeof(temp[0]); i++) { \\\n      if (loco==(uint16_t)temp[i]) { skipIf=false; break;} \\\n    } \\\n    return;\\\n  }\n\n#undef IF_ALL\n#define IF_ALL(vpinList...) \\\n  case (__COUNTER__ - StringMacroTracker1) : \\\n  { \\\n   const int16_t temp[]={vpinList}; \\\n   ifAllFunc(temp,sizeof(temp)/sizeof(temp[0])); \\\n   return;\\\n  }\n\n#undef IF_ANY  \n#define IF_ANY(vpinList...) \\\n  case (__COUNTER__ - StringMacroTracker1) : \\\n  { \\\n   const int16_t temp[]={vpinList}; \\\n   ifAnyFunc(temp,sizeof(temp)/sizeof(temp[0])); \\\n   return;\\\n  }\n\n#undef LCD\n#define LCD(id,msg)  \\\n     case (__COUNTER__ - StringMacroTracker1) : {\\\n         static const char HIGHFLASH thrunge[]=msg;\\\n         strfar=(uint32_t)GETFARPTR(thrunge);\\\n         tmode=thrunge_lcd; \\\n         lcdid=id;\\\n         break;\\\n      }\n#undef SCREEN\n#define SCREEN(display,id,msg)  \\\n     case (__COUNTER__ - StringMacroTracker1) : {\\\n         static const char HIGHFLASH thrunge[]=msg;\\\n         strfar=(uint32_t)GETFARPTR(thrunge);\\\n         tmode=(thrunger)(thrunge_lcd+display); \\\n         lcdid=id;\\\n         break;\\\n      } \n#undef STEALTH\n#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return; \n#undef WITHROTTLE\n#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)\n#undef ZTEST\n#define ZTEST(command,code...) case (__COUNTER__ - StringMacroTracker1) : \\\n   Ztest::parse(F(command),nullptr,[]() -> bool { return (code);}); \\\n   return;\n#undef ZTEST2\n#define ZTEST2(command,response) case (__COUNTER__ - StringMacroTracker1) : \\\n   Ztest::parse(F(command),F(response),nullptr); \\\n   return;\n#undef ZTEST3\n#define ZTEST3(command,response,code...) case (__COUNTER__ - StringMacroTracker1) : \\\n   Ztest::parse(F(command),F(response),[]() -> bool { return (code);}); \\\n   return;\n\n#include \"Ztest.h\"\n\nvoid  RMFT2::printMessage(uint16_t id) { \n  thrunger tmode;\n  uint32_t strfar=0;\n  byte lcdid=0; \n  switch(id) {\n    #include \"myAutomation.h\"\n    default: break ; \n  }\n  if (strfar) thrungeString(strfar,tmode,lcdid);\n}\n\n\n// Pass 5: Turnout descriptions (optional)\n#include \"EXRAIL2MacroReset.h\"\n#undef TURNOUT\n#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)\n#undef TURNOUTL\n#define TURNOUTL(id,addr,description...) O_DESC(id,description)\n#undef PIN_TURNOUT\n#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)\n#undef SERVO_TURNOUT\n#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) O_DESC(id,description)\n#undef VIRTUAL_TURNOUT\n#define VIRTUAL_TURNOUT(id,description...) O_DESC(id,description)\n\nconst FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {\n     switch (turnoutid) {\n        #include \"myAutomation.h\"\n     default:break;\n     }\n     return NULL;\n}\n\n// Pass to get turntable descriptions (optional)\n#include \"EXRAIL2MacroReset.h\"\n#undef DCC_TURNTABLE\n#define DCC_TURNTABLE(id,home,description...) O_DESC(id,description)\n#undef EXTT_TURNTABLE\n#define EXTT_TURNTABLE(id,vpin,home,description...) O_DESC(id,description)\n\nconst FSH * RMFT2::getTurntableDescription(int16_t turntableId) {\n   switch (turntableId) {\n      #include \"myAutomation.h\"\n   default:break;\n   }\n   return NULL;\n}\n\n// Pass to get turntable position descriptions (optional)\n#include \"EXRAIL2MacroReset.h\"\n#undef TT_ADDPOSITION\n#define TT_ADDPOSITION(turntable_id,position,value,home,description...) T_DESC(turntable_id,position,description)\n\nconst FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, uint8_t positionId) {\n  (void)turntableId;\n  (void)positionId;\n   #include \"myAutomation.h\"\n   return NULL;\n}\n\n// Pass 6: Roster IDs (count)\n#include \"EXRAIL2MacroReset.h\"\n#undef ROSTER\n#define ROSTER(cabid,name,funcmap...) +(cabid <= 0 ? 0 : 1)\nconst byte RMFT2::rosterNameCount=0\n   #include \"myAutomation.h\"\n   ;\n   \n// Pass 6: Roster IDs \n#include \"EXRAIL2MacroReset.h\"\n#undef ROSTER\n#define ROSTER(cabid,name,funcmap...) cabid,\nconst int16_t HIGHFLASH  RMFT2::rosterIdList[]={\n   #include \"myAutomation.h\"\n   INT16_MAX};\n\n// Pass 7: Roster names getter\n#include \"EXRAIL2MacroReset.h\"\n#undef ROSTER\n#define ROSTER(cabid,name,funcmap...) case cabid: return F(name);\nconst FSH * RMFT2::getRosterName(int16_t id) {\n   switch(id) {\n      #include \"myAutomation.h\"\n   default: break;\n   }\n   return F(\"\");   \n} \n\n// Pass to get roster functions \n#undef ROSTER\n#define ROSTER(cabid,name,funcmap...) case cabid: return F(\"\" funcmap);\nconst FSH * RMFT2::getRosterFunctions(int16_t id) {\n   switch(id) {\n      #include \"myAutomation.h\"\n   default: break; \n   }   \n   return NULL;\n} \n\n// Pass 8 Signal definitions\n#include \"EXRAIL2MacroReset.h\"\n#undef SIGNAL\n#define SIGNAL(redpin,amberpin,greenpin) {sigtypeSIGNAL,redpin,redpin,amberpin,greenpin}, \n#undef SIGNALH\n#define SIGNALH(redpin,amberpin,greenpin) {sigtypeSIGNALH,redpin,redpin,amberpin,greenpin}, \n#undef SERVO_SIGNAL\n#define SERVO_SIGNAL(vpin,redval,amberval,greenval) {sigtypeSERVO,vpin,redval,amberval,greenval}, \n#undef DCC_SIGNAL\n#define DCC_SIGNAL(id,addr,subaddr) {sigtypeDCC,id,addr,subaddr,0},\n#undef DCCX_SIGNAL\n#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) {sigtypeDCCX,id,redAspect,amberAspect,greenAspect},\n#undef NEOPIXEL_SIGNAL\n#define NEOPIXEL_SIGNAL(id,redRGB,amberRGB,greenRGB) \\\n        {sigtypeNEOPIXEL,id,((VPIN)((redRGB)>>8)), ((VPIN)((amberRGB)>>8)), ((VPIN)((greenRGB)>>8))},\\\n        {sigtypeContinuation,id,((VPIN)((redRGB) & 0xff)), ((VPIN)((amberRGB) & 0xFF)), ((VPIN)((greenRGB) & 0xFF))},\n#undef VIRTUAL_SIGNAL\n#define VIRTUAL_SIGNAL(id) {sigtypeVIRTUAL,id,0,0,0},\n\nconst  HIGHFLASH  SIGNAL_DEFINITION RMFT2::SignalDefinitions[] = {\n    #include \"myAutomation.h\"\n     {sigtypeNoMoreSignals,0,0,0,0}\n    };\n\n// Pass 9 ONLCC/ ONMERG counter and lookup array\n#include \"EXRAIL2MacroReset.h\"\n#undef ONLCC\n#define ONLCC(sender,event) +1 \n#undef ONACON\n#define ONACON(event) +1 \n#undef ONACOF\n#define ONACOF(event) +1 \n\nconst int RMFT2::countLCCLookup=0\n#include \"myAutomation.h\"\n;\nint RMFT2::onLCCLookup[RMFT2::countLCCLookup];\n\n// Last Pass : create main routes table\n// Only undef the macros, not dummy them.  \n#define  RMFT2_UNDEF_ONLY\n#include \"EXRAIL2MacroReset.h\"\n// Define internal helper macros.\n// Everything we generate here has to be compile-time evaluated to \n// a constant.  \n#define V(val) (byte)(((int16_t)(val))&0x00FF),(byte)(((int16_t)(val)>>8)&0x00FF)\n// Define macros for route code creation \n\n#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),\n#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),\n#define AFTER(sensor_id,timer...) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),OPCODE_PAD,V(#timer[0]?timer+0:500),\n#define AFTEROVERLOAD(track_id) OPCODE_AFTEROVERLOAD,V(TRACK_NUMBER_##track_id),\n#define ALIAS(name,value...) \n#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),\n#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),\n#define ASPECT(address,value) OPCODE_ASPECT,V((address<<5) | (value & 0x1F)),\n#define AT(sensor_id) OPCODE_AT,V(sensor_id),\n#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),  \n#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),  \n#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),\n#define AUTOMATION(id, description)  OPCODE_AUTOMATION, V(id), \n#define AUTOSTART OPCODE_AUTOSTART,0,0,\n#define BLINK(vpin,onDuty,offDuty) OPCODE_BLINK,V(vpin),OPCODE_PAD,V(onDuty),OPCODE_PAD,V(offDuty),\n#define BUILD_CONSIST(addloco) OPCODE_CONSIST,V(addloco),\n#define BREAK_CONSIST OPCODE_CONSIST,V(0),\n#define BROADCAST(msg) PRINT(msg)\n#define CALL(route) OPCODE_CALL,V(route),\n#define CLEAR_STASH(id) OPCODE_CLEAR_STASH,V(id),\n#define CLEAR_ALL_STASH OPCODE_CLEAR_ALL_STASH,V(0),\n#define CLEAR_ANY_STASH OPCODE_CLEAR_ANY_STASH,V(0),\n#define CLOSE(id)  OPCODE_CLOSE,V(id),\n#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)\n#ifndef IO_NO_HAL\n#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home),\n#endif\n#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),\n#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),\n#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),\n#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),\n#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),\n#define DCC_SIGNAL(id,add,subaddr)\n#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)\n#define DONE OPCODE_ENDTASK,0,0,\n#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),\n#define ELSE OPCODE_ELSE,0,0,\n#define ENDEXRAIL \n#define ENDIF  OPCODE_ENDIF,0,0,\n#define ENDTASK OPCODE_ENDTASK,0,0,\n#define ESTOP OPCODE_SPEED,V(1), \n#define ESTOPALL OPCODE_ESTOPALL,V(0),\n#define ESTOP_PAUSE OPCODE_ESTOPALL,V(1),\n#define ESTOP_RESUME OPCODE_ESTOPALL,V(2),\n#define EXRAIL\n#ifndef IO_NO_HAL\n#define EXTT_TURNTABLE(id,vpin,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(home),\n#endif\n#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V((int16_t)PCA9685::ProfileType::UseDuration|(int16_t)PCA9685::ProfileType::NoPowerOff),OPCODE_PAD,V(ms/100L),\n#define FOFF(func) OPCODE_FOFF,V(func),\n#define FOLLOW(route) OPCODE_FOLLOW,V(route),\n#define FON(func) OPCODE_FON,V(func),\n#define FORGET OPCODE_FORGET,0,0,\n#define FREE(blockid) OPCODE_FREE,V(blockid),\n#define FREEALL OPCODE_FREEALL,0,0,\n#define FTOGGLE(func) OPCODE_FTOGGLE,V(func),\n#define FWD(speed) OPCODE_FWD,V(speed),\n#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),\n#define HAL(haltype,params...)\n#define HAL_IGNORE_DEFAULTS\n#define IF(sensor_id) OPCODE_IF,V(sensor_id),\n#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),\n#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),\n#define IFGREEN(signal_id) OPCODE_IFGREEN,V(signal_id),\n#define IFGTE(sensor_id,value) OPCODE_IFGTE,V(sensor_id),OPCODE_PAD,V(value),\n#define IFLOCO(loco_list...) OPCODE_IFLOCO,V(__COUNTER__ - StringMacroTracker2),\n#define IF_ALL(vpinlist...) OPCODE_IFLOCO,V(__COUNTER__ - StringMacroTracker2),\n#define IF_ANY(vpinlist...) OPCODE_IFLOCO,V(__COUNTER__ - StringMacroTracker2),\n#define IFLT(sensor_id,value) OPCODE_IFLT,V(sensor_id),OPCODE_PAD,V(value),\n#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),\n#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),\n#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),\n#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),\n#define IFROUTE_ACTIVE(sequence_id) OPCODE_IF_ROUTE_ACTIVE,V(sequence_id),\n#define IFROUTE_INACTIVE(sequence_id) OPCODE_IF_ROUTE_INACTIVE,V(sequence_id),\n#define IFROUTE_HIDDEN(sequence_id) OPCODE_IF_ROUTE_HIDDEN,V(sequence_id),\n#define IFROUTE_DISABLED(sequence_id) OPCODE_IF_ROUTE_DISABLED,V(sequence_id),\n#define IFSTASH(stash_id) OPCODE_IFSTASH,V(stash_id),\n#define IFSTASHED_HERE(stash_id) OPCODE_IFSTASHED_HERE,V(stash_id),\n#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),\n#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,\n#ifndef IO_NO_HAL\n#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position),\n#endif\n#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),\n#define IFBITMAP_ALL(vpin,mask) OPCODE_IFBITMAP_ALL,V(vpin),OPCODE_PAD,V(mask),\n#define IFBITMAP_ANY(vpin,mask) OPCODE_IFBITMAP_ANY,V(vpin),OPCODE_PAD,V(mask),\n#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,\n#define JMRI_SENSOR(vpin,count...)\n#define JMRI_SENSOR_NOPULLUP(vpin,count...)\n#define JOIN OPCODE_JOIN,0,0,\n#define KILLALL OPCODE_KILLALL,0,0,\n#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),\n#define LCC(eventid) OPCODE_LCC,V(eventid),\n#define LCCX(sender,event) OPCODE_LCCX,V(event),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),  \n#define ACON(eventid) OPCODE_ACON,V(((uint32_t)eventid >>16) & 0xFFFF),OPCODE_PAD,V(eventid & 0xFFFF),\n#define ACOF(eventid) OPCODE_ACOF,V(((uint32_t)eventid >>16) & 0xFFFF),OPCODE_PAD,V(eventid & 0xFFFF),\n#define ONACON(eventid) OPCODE_ONACON,V((uint32_t)(eventid) >>16),OPCODE_PAD,V(eventid & 0xFFFF),\n#define ONACOF(eventid) OPCODE_ONACOF,V((uint32_t)(eventid) >>16),OPCODE_PAD,V(eventid & 0xFFFF),\n#define LCD(id,msg) PRINT(msg)\n#define SCREEN(display,id,msg) PRINT(msg)\n#define STEALTH(code...) PRINT(dummy)\n#define STEALTH_GLOBAL(code...) \n#define LCN(msg) PRINT(msg)\n#define MESSAGE(msg) PRINT(msg)\n#define MOMENTUM(accel,decel...) OPCODE_MOMENTUM,V(accel),OPCODE_PAD,V(#decel[0]?decel+0:accel),\n#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),\n#define NEOPIXEL(id,r,g,b,count...) OPCODE_NEOPIXEL,V(id),\\\n        OPCODE_PAD,V(((r & 0xff)<<8) | (g & 0xff)),\\\n        OPCODE_PAD,V((b & 0xff)),\\\n        OPCODE_PAD,V(#count[0]?(count+0):1),\n         \n#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour)\n#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),\n#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),\n#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),\n#define ONBLOCKENTER(block_id) OPCODE_ONBLOCKENTER,V(block_id),\n#define ONBLOCKEXIT(block_id) OPCODE_ONBLOCKEXIT,V(block_id),\n#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),\n#define ONLCC(sender,event) OPCODE_ONLCC,V(event),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\\\n        OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),        \n#define ONTIME(value) OPCODE_ONTIME,V(value),  \n#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),\n#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)\n#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),\n#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,\n#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,\n#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),\n#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),\n#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),\n#define ONRED(signal_id) OPCODE_ONRED,V(signal_id),\n#ifndef IO_NO_HAL\n#define ONROTATE(id) OPCODE_ONROTATE,V(id),\n#endif\n#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),\n#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),\n#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),\n#define ONBITMAP(sensor_id) OPCODE_ONBITMAP,V(sensor_id),\n#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,V(sensor_id),\n#define PAUSE OPCODE_PAUSE,0,0,\n#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),\n#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),\n#define PLAY_EQ(vpin,eqname)               ANOUT(vpin,0,DFPlayerBase::DF_EQ_##eqname,DFPlayerBase::DF_EQ)\n#define PLAY_FOLDER(vpin,folder)           ANOUT(vpin,0,folder,DFPlayerBase::DF_FOLDER)\n#define PLAY_PAUSE(vpin)                   ANOUT(vpin,0,0,DFPlayerBase::DF_PAUSE)\n#define PLAY_REPEAT(vpin,track,volume...)  ANOUT(vpin,track,volume+0,DFPlayerBase::DF_REPEATPLAY)\n#define PLAY_RESET(vpin)                   ANOUT(vpin,0,0,DFPlayerBase::DF_RESET)\n#define PLAY_RESUME(vpin)                  ANOUT(vpin,0,0,DFPlayerBase::DF_RESUME)\n#define PLAY_STOP(vpin)                    ANOUT(vpin,0,0,DFPlayerBase::DF_STOPPLAY)\n#define PLAY_TRACK(vpin,track,volume...)   ANOUT(vpin,track,volume+0,DFPlayerBase::DF_PLAY) \n#define PLAY_VOLUME(vpin,volume)           ANOUT(vpin,0,volume,DFPlayerBase::DF_VOL)\n#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),\n#define POWEROFF OPCODE_POWEROFF,0,0,\n#define POWERON OPCODE_POWERON,0,0,\n#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),\n#define PARSE(msg) PRINT(msg)\n#define RANDOM_CALL(...) \\\n  OPCODE_RANDOM_CALL,V(FOR_EACH_NARG(__VA_ARGS__)), \\\n  ZCRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__)\n#define RANDOM_FOLLOW(...) \\\n  OPCODE_RANDOM_FOLLOW,V(FOR_EACH_NARG(__VA_ARGS__)), \\\n  ZCRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__)\n#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,\n#define RED(signal_id) OPCODE_RED,V(signal_id),\n#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),\n#define RESET(pin,count...) OPCODE_RESET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),\n#define RESUME OPCODE_RESUME,0,0,\n#define RETURN OPCODE_RETURN,0,0,\n#define REV(speed) OPCODE_REV,V(speed),\n#define ROSTER(cabid,name,funcmap...)\n#ifndef IO_NO_HAL\n#define ROTATE(id,position,activity) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(EXTurntable::activity),\n#define ROTATE_DCC(id,position) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(0),\n#endif\n#define ROUTE(id, description)  OPCODE_ROUTE, V(id), \n#define ROUTE_ACTIVE(id)  OPCODE_ROUTE_ACTIVE,V(id),\n#define ROUTE_INACTIVE(id)  OPCODE_ROUTE_INACTIVE,V(id),\n#define ROUTE_HIDDEN(id)  OPCODE_ROUTE_HIDDEN,V(id),\n#define ROUTE_DISABLED(id)  OPCODE_ROUTE_DISABLED,V(id),\n#define ROUTE_CAPTION(id,caption) PRINT(caption)\n#define SAVE_SPEED OPCODE_SAVE_SPEED,V(0),\n#define RESTORE_SPEED OPCODE_RESTORE_SPEED,V(0),\n#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),\n#define SEQUENCE(id)  OPCODE_SEQUENCE, V(id), \n#define SERIAL(msg) PRINT(msg)\n#define SERIAL1(msg) PRINT(msg)\n#define SERIAL2(msg) PRINT(msg)\n#define SERIAL3(msg) PRINT(msg)\n#define SERIAL4(msg) PRINT(msg)\n#define SERIAL5(msg) PRINT(msg)\n#define SERIAL6(msg) PRINT(msg)\n#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0),\n#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),\n#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)\n#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),\n#define SET(pin,count...) OPCODE_SET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),\n#define SET_TRACK(track,mode)  OPCODE_SET_TRACK,V(TRACK_MODE_##mode  <<8 | TRACK_NUMBER_##track),\n#define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track),\n#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),\n#define SETFREQ(freq) OPCODE_SETFREQ,V(freq),\n#define SIGNAL(redpin,amberpin,greenpin) \n#define SIGNALH(redpin,amberpin,greenpin) \n#define SPEED(speed) OPCODE_SPEED,V(speed),\n#define SPEEDUP(speedstep) OPCODE_SPEEDUP,V(speedstep),\n#define SPEED_REL(percent) OPCODE_SPEED_REL,V(percent),\n#define SLOWDOWN(speedstep) OPCODE_SLOWDOWN,V(speedstep),\n#define START(route) OPCODE_START,V(route), \n#define START_SHARED(route) OPCODE_START_SHARED,V(route),\n#define START_SEND(route) OPCODE_START_SEND,V(route),\n#define STASH(id) OPCODE_STASH,V(id), \n#define STOP OPCODE_SPEED,V(0), \n#define THROW(id)  OPCODE_THROW,V(id),\n#define TOGGLE_TURNOUT(id)  OPCODE_TOGGLE_TURNOUT,V(id),\n#ifndef IO_NO_HAL\n#define TT_ADDPOSITION(id,position,value,angle,description...) OPCODE_TTADDPOSITION,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(value),OPCODE_PAD,V(angle),\n#endif\n#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),\n#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description)\n#define UNJOIN OPCODE_UNJOIN,0,0,\n#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),\n#define VIRTUAL_SIGNAL(id) \n#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0), \n#define BITMAP_AND(vpin,mask) OPCODE_BITMAP_AND,V(vpin),OPCODE_PAD,V(mask),\n#define BITMAP_INC(vpin) OPCODE_BITMAP_INC,V(vpin),\n#define BITMAP_DEC(vpin) OPCODE_BITMAP_DEC,V(vpin),\n#define BITMAP_OR(vpin,mask) OPCODE_BITMAP_OR,V(vpin),OPCODE_PAD,V(mask),\n#define BITMAP_SET(vpin,value) ANOUT(vpin,value,0,0)\n#define BITMAP_XOR(vpin,mask) OPCODE_BITMAP_XOR,V(vpin),OPCODE_PAD,V(mask),\n#define WITHROTTLE(msg) PRINT(msg)\n#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),\n#ifndef IO_NO_HAL\n#define WAITFORTT(turntable_id) OPCODE_WAITFORTT,V(turntable_id),\n#endif\n#define WAIT_WHILE_RED(signal_id) OPCODE_WAIT_WHILE_RED,V(signal_id),\n#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),\n#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),\n#define XFTOGGLE(cab,func) OPCODE_XFTOGGLE,V(cab),OPCODE_PAD,V(func),\n#define XFWD(cab,speed) OPCODE_XFWD,V(cab),OPCODE_PAD,V(speed),\n#define XREV(cab,speed) OPCODE_XREV,V(cab),OPCODE_PAD,V(speed),\n#define XPOM(cab,cv,value) OPCODE_XPOM,V(cab),OPCODE_PAD,V(cv),OPCODE_PAD,V(value),\n#define XSAVE_SPEED(cab) OPCODE_XSAVE_SPEED,V(cab),\n#define XRESTORE_SPEED(cab) OPCODE_XRESTORE_SPEED,V(cab),\n#define ZTEST(command,code...) PRINT(dummy)\n#define ZTEST2(command,response) PRINT(dummy)\n#define ZTEST3(command,response,code...) PRINT(dummy)\n\n// Build RouteCode\nconst int StringMacroTracker2=__COUNTER__;\nconst  HIGHFLASH3  byte RMFT2::RouteCode[] = {\n    #include \"myAutomation.h\"\n    OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };\n\n// Restore normal code LCD & SERIAL  macro\n#undef LCD\n#define LCD   StringFormatter::lcd\n#undef SCREEN\n#define SCREEN  StringFormatter::lcd2\n#undef SERIAL\n#define SERIAL  0x0\n#endif\n"
  },
  {
    "path": "EXRAILSensor.cpp",
    "content": "/*\n *  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**********************************************************************\nEXRAILSensor represents a sensor that should be monitored in order\nto call an exrail ONBUTTON or ONCHANGE handler.\nThese are created at EXRAIL startup and thus need no delete or listing\ncapability.\nThe basic logic is similar to that found in the Sensor class\nexcept that on the relevant change an EXRAIL thread is started.    \n**********************************************************************/\n\n#include \"EXRAILSensor.h\"\n#include \"EXRAIL2.h\"\n\nvoid EXRAILSensor::checkAll() {\n  if (firstSensor == NULL) return;  // No sensors to be scanned\n  if (readingSensor == NULL) { \n    // Not currently scanning sensor list\n    unsigned long thisTime = micros();\n    if (thisTime - lastReadCycle < cycleInterval) return;\n    // Required time has elapsed since last read cycle started,\n    // so initiate new scan through the sensor list\n    readingSensor = firstSensor;\n    lastReadCycle = thisTime;\n  }\n  \n  // Loop until either end of list is encountered or we pause for some reason\n  byte sensorCount = 0;\n\n  while (readingSensor != NULL) {\n    bool pause=readingSensor->check();\n    // Move to next sensor in list.\n    readingSensor = readingSensor->nextSensor;\n    // Currently process max of 16 sensors per entry.\n    // Performance measurements taken during development indicate that, with 128 sensors configured\n    // on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices\n    // within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.\n    if (pause || (++sensorCount)>=16) return; \n  }\n} \n\nbool EXRAILSensor::check() {\n  // check for debounced change in this sensor \n  inputState = useAnalog?IODevice::readAnalogue(pin):RMFT2::readSensor(pin);\n\n  // Check if changed since last time, and process changes.\n  if (inputState == active) {// no change\n    latchDelay = minReadCount; // Reset counter\n    return false;  // no change \n  }\n\n    // Change detected ... has it stayed changed for long enough\n    if (latchDelay > 0) {\n      latchDelay--;\n      return false; \n    } \n    \n    // change validated, act on it.\n    active = inputState;\n    latchDelay = minReadCount;  // Reset debounce counter\n    if (onChange || active) {\n      new RMFT2(progCounter);\n      return true;  // Don't check any more sensors on this entry\n    }\n    return false; \n}\n\nEXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog) {\n\n  nextSensor = firstSensor;\n  firstSensor = this;\n\n  pin=_pin;\n  progCounter=_progCounter;\n  onChange=_onChange;\n  useAnalog=_useAnalog;\n\n  IODevice::configureInput(pin, true);   \n  active = useAnalog?IODevice::readAnalogue(pin): IODevice::read(pin);\n  inputState = active;\n  latchDelay = minReadCount;\n}\n\nEXRAILSensor *EXRAILSensor::firstSensor=NULL;\nEXRAILSensor *EXRAILSensor::readingSensor=NULL;\nunsigned long EXRAILSensor::lastReadCycle=0;\n"
  },
  {
    "path": "EXRAILSensor.h",
    "content": "/*\n *  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef EXRAILSensor_h\n#define EXRAILSensor_h \n#include \"IODevice.h\"\nclass EXRAILSensor {\n  static EXRAILSensor * firstSensor;\n static EXRAILSensor * readingSensor;\n static unsigned long lastReadCycle;\n \n  public:\n  static void checkAll();\n  \n  EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog=false);\n  \n  bool check();\n  \n  private:\n  static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.\n                                                   // should not be less than device scan cycle time.\n  static const byte minReadCount = 4; // number of additional scans before acting on change\n                                        // E.g. 1 means that a change is ignored for one scan and actioned on the next.\n                                        // Max value is 63\n  \n  EXRAILSensor* nextSensor;\n  VPIN pin; \n  int progCounter; \n  uint16_t active; \n  uint16_t inputState;\n  bool onChange;\n  bool useAnalog;\n  byte latchDelay;\n};\n#endif\n"
  },
  {
    "path": "EXRAILTest.h",
    "content": "// This file contains various EXRAIL tests.\n// It will need to be #included in a myAutomation.h to be executed.\n\nROUTE(7000, \"Route state tests\")\n\n  // Test route state management\n  PRINT(\"Test 7001\")\n  ROUTE_ACTIVE(7000)\n  IFROUTE_ACTIVE(7000) ELSE PRINT(\"7001a failed\") ENDIF\n  IFROUTE_INACTIVE(7000) PRINT(\"7001b failed\") ENDIF\n  \n  ROUTE_INACTIVE(7000)\n  IFROUTE_INACTIVE(7000) ELSE PRINT(\"7001c failed\") ENDIF\n  IFROUTE_ACTIVE(7000) PRINT(\"700d failed\") ENDIF\n\n  ROUTE_DISABLED(7000)\n  IFROUTE_DISABLED(7000) ELSE PRINT(\"7001e failed\") ENDIF\n  IFROUTE_ACTIVE(7000) PRINT(\"700df failed\") ENDIF\n  IFROUTE_INACTIVE(7000) PRINT(\"700dg failed\") ENDIF\n\n  ROUTE_HIDDEN(7000)\n  IFROUTE_HIDDEN(7000) ELSE PRINT(\"7001h failed\") ENDIF\n  \n  ROUTE_INACTIVE(7000)\n  DONE\n\n\n  // Test WAIT_WHILE_RED\n  VIRTUAL_SIGNAL(1)\n  ROUTE(7100, \"7100 WAIT_WHILE_RED test\")\n  SETLOCO(3)\n  SPEED(10)\n  RED(1)\n  PRINT(\"7100 Waiting at signal 1\")\n  PRINT(\"7100 Tester please set signal 1 GREEN or AMBER to continue\")\n  WAIT_WHILE_RED(1)\n  SPEED(10)\n  PRINT(\"7100 Resumed at signal 1\")\n  DONE\n\n  // TEST new FREEALL command\n  ROUTE(7200,\"7200 Test FREEALL\")\n  RESERVE(1)\n  RESERVE(255)\n  PARSE(\"</>\")\n  FREEALL\n  PARSE(\"</>\")\n\n  IFRESERVE(1)\n    PRINT(\"7200 FREEALL test worked\")\n    ELSE \n    PRINT(\"7200 FREEALL test fail\")\n    ENDIF\n  DONE\n\n  // Test SEND_LOCO_X and SEND_LOCO_S\n  ROUTE(7300,\"7300 Test SEND_LOCO_ variants\")\n  SETLOCO(5) \n  \n  // Share loco and check we doidnt lose it\n  START_SHARED(7301)\n  IFLOCO(5) ELSE PRINT(\"7300 START_SHARED failed\") ENDIF\n\n  // transfer loco and check we do lose it\n  START_SEND(7302)\n  IFLOCO(0) ELSE PRINT(\"7300 START_SEND failed\") ENDIF\n  DONE\n  \n  SEQUENCE(7301)\n    IFLOCO(5)\n      PRINT(\"7301 START_SHARED received\")\n      ELSE \n      PRINT(\"7301 START_SHARED failed\")\n      ENDIF\n    DONE   \n    SEQUENCE(7302)\n    IFLOCO(5)\n      PRINT(\"7302 START_SEND received\")\n      ELSE \n      PRINT(\"7302 START_SEND failed\")\n      ENDIF\n    DONE  \n\n    SEQUENCE(7400)\n      SETLOCO(3)\n      SPEED(20)\n      SAVE_SPEED\n      SPEED(40)\n      PRINT(\"7401 Speed changed to 40\")\n      RESTORE_SPEED\n      PRINT(\"7401 Speed restored\")\n      XFWD(4,20)\n      XSAVE_SPEED(4)\n      XFWD(4,40)\n      PRINT(\"7401 Speed 4 changed to 40\")\n      XRESTORE_SPEED(4)\n      PRINT(\"7401 Speed 4 restored\")\n      DONE\n      \nSEQUENCE(7500)\n  PRINT(\"7500 Testing RANDOM_CALL and RANDOM_FOLLOW\")\n  RANDOM_CALL(7501, 7502, 7503)\n  PRINT(\"7500 Returned from RANDOM_CALL\")\n  RANDOM_FOLLOW(7511, 7512, 7513)\n  PRINT(\"7500 ERROR Returned from RANDOM_FOLLOW\")\n  DONE\n\n  SEQUENCE(7501) PRINT(\"7501 RANDOM_CALL\") RETURN\n  SEQUENCE(7502) PRINT(\"7502 RANDOM_CALL\") RETURN\n  SEQUENCE(7503) PRINT(\"7503 RANDOM_CALL\") RETURN\n\n  SEQUENCE(7511) PRINT(\"7511 RANDOM_FOLLOW\") DONE\n  SEQUENCE(7512) PRINT(\"7512 RANDOM_FOLLOW\") DONE\n  SEQUENCE(7513) PRINT(\"7513 RANDOM_FOLLOW\") DONE\n  \n  ROUTE(7600, \"7600 Test CONSIST\")\n    PARSE(\"7600 Create 6 77 88 -99\")\n    SETLOCO(6)\n    BUILD_CONSIST(77)\n    BUILD_CONSIST(88)\n    BUILD_CONSIST(-99)\n    PARSE(\"<^>\")  // display consists \n    PRINT(\"break up\") \n    BREAK_CONSIST\n    PARSE(\"<^>\")  // display consists  \n  DONE\n\n  HAL(Bitmap,870,10)\n  ROUTE(7700,\"Set 870 and wait\")\n    SET(870)\n    AT(-870)\n    PRINT(\"870 reset\")\n  DONE\n  ROUTE(7701,\"ResetS 870 and wait\")\n    RESET(870)\n    AT(870)\n    PRINT(\"870 set\")\n  DONE\n\n  ROUTE(7702,\"Bitmap Set 870\")\n    BITMAP_SET(870,1234)\n    IFGTE(870,1234) \n        IFLT(870,1235) \n            PRINT(\"870 set correctly\")\n            DONE\n        ENDIF\n      ENDIF  \n      PRINT(\"870 not set correctly\")      \n  DONE\n\n  ROUTE(7800, \"ZTESTS\")\n  PRINT(\"ZTESTS starting\")\n  ZTEST(\"<t 3 5 1>\",DCC::getLocoSpeedByte(3)==(128+6))\n  ZTEST(\"<t 3 5 0>\",DCC::getLocoSpeedByte(3)==(6))\n  ZTEST(\"<-3>\",DCC::getLocoSpeedByte(3)==(128))\n  \n  // speed up down and relative speed changes.\n  SETLOCO(3)\n  FWD(20)        ZTEST(\"FWD(20)\",DCC::getLocoSpeedByte(3)==(128+20))\n  SPEEDUP(10)    ZTEST(\"SPEEDUP(10)\",DCC::getLocoSpeedByte(3)==(128+30))\n  SPEED_REL(50)  ZTEST(\"SPEED_REL(50)\",DCC::getLocoSpeedByte(3)==(128+15))\n  SPEED_REL(300) ZTEST(\"SPEED_REL(300)\",DCC::getLocoSpeedByte(3)==(128+45))\n  SLOWDOWN(5)    ZTEST(\"SLOWDOWN(5)\",DCC::getLocoSpeedByte(3)==(128+40))\n  DONE\n\nROUTE(7900,\"7900 Test IFSTASHED_HERE\")\n   SETLOCO(4)  // set loco 4\n   STASH(200)  // stash loco 4 in stash 2\n    \n// loco is 4\n  IFSTASHED_HERE(100) // should be false\n      PRINT(\"FAIL Loco 4 in stash 100\")\n  ELSE\n      PRINT(\"OK: loco 4 is not in stash 100\")   \n  ENDIF\n\nSETLOCO(3)\nIFSTASHED_HERE(200) // should be false\n      PRINT(\"FAIL Loco 3 in stash 200\")\nELSE\n      PRINT(\"OK: loco 3 is not in stash 200\")   \nENDIF\n\nIFSTASHED_HERE(100) // should be true\n      PRINT(\"OK: Loco 3 is in stash 100\")\nELSE\n      PRINT(\"FAIL: loco 3 not in stash 100\")\nENDIF\nDONE\n\nROUTE(8000,\"8000 ESTOP_PAUSE test\")\n   SETLOCO(3)  // set loco 3\n   FWD(20)    //\n   ESTOP_PAUSE\n   SETLOCO(0)  // prevent DONE stopping it\n   DONE\nROUTE(8001,\"8001 ESTOP_RESUME test\")\n   ESTOP_RESUME\n   ZTEST(\"<D CABS>\",DCC::getLocoSpeedByte(3)==(128+20))\n   DONE\n\n\n#define MYGROUP 1,2,3,4   \nROUTE(1771,\"1771 Test IFLOCO with multiple loco ids\")\n  SETLOCO(3)\n  IFLOCO(MYGROUP) \n    PRINT(\"IFLOCO 3 test passed\")\n  ELSE\n    PRINT(\"IFLOCO 3 test failed\")\n  ENDIF\n  IFLOCO(1,2,4, -1) \n    PRINT(\"IFLOCO 1,2,4 test failed\")\n  ELSE\n    PRINT(\"IFLOCO 1,2,4 test passed\")\n  ENDIF\n\n  HAL(Bitmap,1700,10)\n  SET(1702) SET(1703) SET(1704)\n  \n  IF_ANY(1700,1701,1702) \n    PRINT(\"IF_ANY 0,1,2 test OK\")\n  ELSE\n    PRINT(\"IF_ANY 0,1,2 test failed\")\n  ENDIF\n  \n  IF_ANY(1700,1701,1706) \n    PRINT(\"IF_ANY 0,1,6 test failed\")\n  ELSE\n    PRINT(\"IF_ANY 0,1,6 test passed\")\n  ENDIF\n  \n  IF_ALL(1702,1703,1704) \n    PRINT(\"IF_ALL 2,3,4 test OK\")\n  ELSE\n    PRINT(\"IF_ALL 2,3,4 test failed\")\n  ENDIF\n  \n  IF_ALL(1702,1703,1706) \n    PRINT(\"IF_ALL 2,3,6 test failed\")\n  ELSE\n    PRINT(\"IF_ALL 2,3,6 test passed\")\n  ENDIF\n\n  IF_ANY(1701,-1706)\n    PRINT(\"IF_ANY 1,-1706 test passed\")\n  ELSE\n    PRINT(\"IF_ANY 1,-1706 test failed\")\n  ENDIF\n\n  IF_ALL(1701,-1706)\n    PRINT(\"IF_ALL 1,-1706 test failed\")\n  ELSE\n    PRINT(\"IF_ALL 1,-1706 test passed\")\n  ENDIF\n\n    DONE\n  \n\n\n// Speedometer example\n// Track is =TS1===TS2===TS3= timing between S2 and S3.\n\nALIAS(TS1,181) ALIAS(TS2,182) ALIAS(TS3,183)\nSTEALTH_GLOBAL(\n  byte testStartSpeed=2;\n  byte testEndSpeed=100;\n  byte testStep=10;\n  byte testSpeed;\n  unsigned long testStartTime;\n)\n\nAUTOMATION(9000,\"Run Speed Test\") // speed test setup\n  STEALTH(testSpeed=testStartSpeed;)\n  PRINT(\"Starting speed test\")\n  REV(20) // reverse loco to start point\n\nSEQUENCE(9001)\n  // make sure loco is at start point\n  AT(TS1) ESTOP\n  // drive loco fwd at testSpeed \n  STEALTH(DCC::setThrottle(loco,testSpeed,true);)\n  // At timing-start sensor(s2) record time\n  AT(TS2) STEALTH(testStartTime=millis();)\n // at timing-end sensor(s3) stop and calculate and print speed\n AT(TS3) ESTOP STEALTH(\n    StringFormatter::send(&USB_SERIAL, \n       F(\"Speed %d Time %l\\n\"), testSpeed, millis()-testStartTime);\n    testSpeed+=testStep;\n    if (testSpeed>testEndSpeed) kill(); // test complete =DONE\n    )\n// Reverse back to start, and test again\nREV(127) FOLLOW(9001)\n"
  },
  {
    "path": "EXmDNS.cpp",
    "content": "/*\n *  © 2024 Harald Barth\n *  © 2024 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <Arduino.h>\n#include \"EthernetInterface.h\"\n#ifdef DO_MDNS\n#include \"EXmDNS.h\"\n\n// fixed values for mDNS\nstatic IPAddress mdnsMulticastIPAddr = IPAddress(224, 0, 0, 251);\n#define MDNS_SERVER_PORT 5353\n\n// dotToLen()\n// converts stings of form \".foo.barbar.x\" to a string with the\n// dots replaced with lenght. So string above  would result in\n// \"\\x03foo\\x06barbar\\x01x\" in C notation. If not NULL, *substr\n// will point to the beginning of the last component, in this\n// example that would be \"\\x01x\".\n//\nstatic void dotToLen(char *str, char **substr) {\n  char *dotplace = NULL;\n  char *s;\n  byte charcount = 0;\n  for (s = str;/*see break*/ ; s++) {\n    if (*s == '.' || *s == '\\0') {\n      // take care of accumulated\n      if (dotplace != NULL && charcount != 0) {\n\t*dotplace = charcount;\n      }\n      if (*s == '\\0')\n\tbreak;\n      if (substr && *s == '.')\n\t*substr = s;\n      // set new values\n      dotplace = s;\n      charcount = 0;\n    } else {\n      charcount++;\n    }\n  }\n}\n\nMDNS::MDNS(EthernetUDP& udp) {\n  _udp = &udp;\n}\nMDNS::~MDNS() {\n  _udp->stop();\n  if (_name) free(_name);\n  if (_serviceName) free(_serviceName);\n  if (_serviceProto) free(_serviceProto);\n}\nint MDNS::begin(const IPAddress& ip, char* name) {\n  // if we were called very soon after the board was booted, we need to give the\n  // EthernetShield (WIZnet) some time to come up. Hence, we delay until millis() is at\n  // least 3000. This is necessary, so that if we need to add a service record directly\n  // after begin, the announce packet does not get lost in the bowels of the WIZnet chip.\n  //while (millis() < 3000) \n  //  delay(100);\n  \n  _ipAddress = ip;\n  _name = (char *)malloc(strlen(name)+2);\n  byte n;\n  for(n = 0; n<strlen(name); n++)\n    _name[n+1] = name[n];\n  _name[n+1] = '\\0';\n  _name[0] = '.';\n  dotToLen(_name, NULL);\n  return _udp->beginMulticast(mdnsMulticastIPAddr, MDNS_SERVER_PORT);\n}\n\nint MDNS::addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto) {\n  // we ignore proto, assume TCP\n  (void)proto;\n  _serviceName = (char *)malloc(strlen(name) + 2);\n  byte n;\n  for(n = 0; n<strlen(name); n++)\n    _serviceName[n+1] = name[n];\n  _serviceName[n+1] = '\\0';\n  _serviceName[0] = '.';\n  _serviceProto = NULL; //to be filled in\n  dotToLen(_serviceName, &_serviceProto);\n  _servicePort = port;\n  return 1;\n}\n\nstatic char dns_rr_services[]   = \"\\x09_services\\x07_dns-sd\\x04_udp\\x05local\";\nstatic char dns_rr_tcplocal[]   = \"\\x04_tcp\\x05local\"; \nstatic char *dns_rr_local       = dns_rr_tcplocal + dns_rr_tcplocal[0] + 1;\n\ntypedef struct _DNSHeader_t \n{\n  uint16_t    xid;\n  uint16_t  flags; // flags condensed\n  uint16_t    queryCount;\n  uint16_t    answerCount;\n  uint16_t    authorityCount;\n  uint16_t    additionalCount;\n} __attribute__((__packed__)) DNSHeader_t;\n\n//\n// MDNS::run()\n// This broadcasts whatever we got evey BROADCASTTIME seconds.\n// Why? Too much brokenness i all mDNS implementations available\n//\nvoid MDNS::run() {\n  static long int lastrun = BROADCASTTIME * 1000UL;\n  unsigned long int now = millis();\n  if (!(now - lastrun > BROADCASTTIME * 1000UL)) {\n    return;\n  }\n  lastrun = now;\n  DNSHeader_t dnsHeader = {0, 0, 0, 0, 0, 0};\n  // DNSHeader_t dnsHeader = { 0 };\n\n  _udp->beginPacket(mdnsMulticastIPAddr, MDNS_SERVER_PORT);\n\n  // dns header\n  dnsHeader.flags = HTONS((uint16_t)0x8400); // Response, authorative\n  dnsHeader.answerCount = HTONS(4 /*5 if TXT but we do not do that */);\n  _udp->write((uint8_t*)&dnsHeader, sizeof(DNSHeader_t));\n\n  // rr #1, the PTR record from generic _services.x.local to service.x.local\n  _udp->write((uint8_t*)dns_rr_services, sizeof(dns_rr_services));\n\n  byte buf[10];\n  buf[0] = 0x00;\n  buf[1] = 0x0c;                           //PTR\n  buf[2] = 0x00;\n  buf[3] = 0x01;                           //IN\n  *((uint32_t*)(buf+4)) = HTONL(120); //TTL in sec\n  *((uint16_t*)(buf+8)) = HTONS( _serviceProto[0] + 1 + strlen(dns_rr_tcplocal) + 1);\n  _udp->write(buf, 10);\n\n  _udp->write(_serviceProto,_serviceProto[0]+1);\n  _udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);\n\n  // rr #2, the PTR record from proto.x to name.proto.x\n  _udp->write(_serviceProto,_serviceProto[0]+1);\n  _udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);\n  *((uint16_t*)(buf+8)) = HTONS(strlen(_serviceName) + strlen(dns_rr_tcplocal) + 1); // recycle most of buf\n  _udp->write(buf, 10);\n\n  _udp->write(_serviceName, strlen(_serviceName));\n  _udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);\n  // rr #3, the SRV record for the service that points to local name\n  _udp->write(_serviceName, strlen(_serviceName));\n  _udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);\n\n  buf[1] = 0x21;                                  // recycle most of buf but here SRV\n  buf[2] = 0x80;                                  // cache flush\n  *((uint16_t*)(buf+8)) = HTONS(strlen(_name) + strlen(dns_rr_local) + 1 + 6);\n  _udp->write(buf, 10);\n\n  byte srv[6];\n  // priority and weight\n  srv[0] = srv[1] = srv[2] = srv[3] = 0;\n  // port\n  *((uint16_t*)(srv+4)) = HTONS(_servicePort);\n  _udp->write(srv, 6);\n  // target\n  _udp->write(_name, _name[0]+1);\n  _udp->write(dns_rr_local, strlen(dns_rr_local)+1);\n\n  // rr #4, the A record for the name.local\n  _udp->write(_name, _name[0]+1);\n  _udp->write(dns_rr_local, strlen(dns_rr_local)+1);\n  \n  buf[1] = 0x01;                                  // recycle most of buf but here A\n  *((uint16_t*)(buf+8)) = HTONS(4);\n  _udp->write(buf, 10);\n  byte ip[4];\n  ip[0] = _ipAddress[0];\n  ip[1] = _ipAddress[1];\n  ip[2] = _ipAddress[2];\n  ip[3] = _ipAddress[3];\n  _udp->write(ip, 4);\n  \n  _udp->endPacket();\n  _udp->flush();\n  // \n}\n#endif //DO_MDNS\n"
  },
  {
    "path": "EXmDNS.h",
    "content": "/*\n *  © 2024 Harald Barth\n *  © 2024 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef DO_MDNS\n#define BROADCASTTIME 15 //seconds\n\n// We do this ourselves because every library is different and/or broken...\n#define HTONS(x) ((uint16_t)(((x) << 8) | (((x) >> 8) & 0xFF)))\n#define HTONL(x) ( ((uint32_t)(x) << 24) | (((uint32_t)(x) << 8) & 0xFF0000) | \\\n                  (((uint32_t)(x) >> 8) & 0xFF00) | ((uint32_t)(x) >> 24) )\n\ntypedef enum _MDNSServiceProtocol_t \n{\n  MDNSServiceTCP,\n  MDNSServiceUDP\n} MDNSServiceProtocol_t;\n\nclass MDNS {\npublic:\n  MDNS(EthernetUDP& udp);\n  ~MDNS();\n  int begin(const IPAddress& ip,  char* name);\n  int addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto);\n  void run();\nprivate:\n  EthernetUDP *_udp;\n  IPAddress _ipAddress;\n  char* _name;\n  char* _serviceName;\n  char* _serviceProto;\n  int _servicePort;\n};\n#endif //DO_MDNS\n"
  },
  {
    "path": "EthernetInterface.cpp",
    "content": "/*\n *  © 2024 Morten \"Doc\" Nielsen\n *  © 2023-2024 Paul M. Antoine\n *  © 2022 Bruno Sanches\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2024 Chris Harlow\n *  © 2020 Gregor Baues\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX/CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n * \n */\n#include \"defines.h\" \n#if ETHERNET_ON == true\n#include \"EthernetInterface.h\"\n#include \"DIAG.h\"\n#include \"CommandDistributor.h\"\n#include \"WiThrottle.h\"\n#include \"DCCTimer.h\"\n\n#ifdef DO_MDNS\n#include \"EXmDNS.h\"\nEthernetUDP udp;\nMDNS mdns(udp);\n#endif\n\n//extern void looptimer(unsigned long timeout, const FSH* message);\n#define looptimer(a,b)\n\nbool EthernetInterface::connected=false;\nEthernetServer * EthernetInterface::server= nullptr;\nEthernetClient EthernetInterface::clients[MAX_SOCK_NUM];                // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield\nbool EthernetInterface::inUse[MAX_SOCK_NUM];                // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield\nuint8_t EthernetInterface::buffer[MAX_ETH_BUFFER+1];                    // buffer used by TCP for the recv\nRingStream * EthernetInterface::outboundRing = nullptr;\n\n/**\n * @brief Setup Ethernet Connection\n * \n */\n\nvoid EthernetInterface::setup() \n{\n  DIAG(F(\"Ethernet starting\"\n  #ifdef DO_MDNS\n    \" (with mDNS)\"\n  #endif \n    \" Please be patient, especially if no cable is connected!\"\n  ));\n  \n  #ifdef STM32_ETHERNET\n    // Set a HOSTNAME for the DHCP request - a nice to have, but hard it seems on LWIP for STM32\n    // The default is \"lwip\", which is **always** set in STM32Ethernet/src/utility/ethernetif.cpp\n    // for some reason. One can edit it to instead read:\n    //      #if LWIP_NETIF_HOSTNAME\n    //      /* Initialize interface hostname */\n    //      if (netif->hostname == NULL)\n    //         netif->hostname = \"lwip\";\n    //      #endif /* LWIP_NETIF_HOSTNAME */\n    // Which seems more useful! We should propose the patch... so the following line actually works!\n    netif_set_hostname(&gnetif, ETHERNET_HOSTNAME);   // Should probably be passed in the contructor...\n  #endif   \n\n    byte mac[6];\n    DCCTimer::getSimulatedMacAddress(mac);\n  \n  #ifdef IP_ADDRESS\n    static IPAddress myIP(IP_ADDRESS);\n    Ethernet.begin(mac,myIP);\n  #else\n    if (Ethernet.begin(mac)==0)\n  {\n    LCD(4,F(\"IP: No DHCP\"));\n    return;\n  }\n  #endif\n\n  auto ip = Ethernet.localIP();    // look what IP was obtained (dynamic or static)\n  if (!ip) {\n    LCD(4,F(\"IP: None\"));\n    return;\n  }\n  server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT\n  server->begin();\n\n  // Arrange display of IP address and port\n  #ifdef LCD_DRIVER\n    const byte lcdData[]={LCD_DRIVER};\n    const bool wideDisplay=lcdData[1]>=24; // data[1] is cols. \n  #else \n    const bool wideDisplay=true;\n  #endif    \n  if (wideDisplay) {\n    // OLEDS or just usb diag is ok on one line. \n    LCD(4,F(\"IP %d.%d.%d.%d:%d\"), ip[0], ip[1], ip[2], ip[3], IP_PORT);    \n  } \n  else { // LCDs generally too narrow, so take 2 lines\n    LCD(4,F(\"IP %d.%d.%d.%d\"), ip[0], ip[1], ip[2], ip[3]);\n    LCD(5,F(\"Port %d\"), IP_PORT);\n  }\n \n  outboundRing=new RingStream(OUTBOUND_RING_SIZE);\n  #ifdef DO_MDNS\n    if (!mdns.begin(Ethernet.localIP(), (char *)ETHERNET_HOSTNAME))\n      DIAG(F(\"mdns.begin fail\")); // hostname\n    mdns.addServiceRecord(ETHERNET_HOSTNAME \"._withrottle\", IP_PORT, MDNSServiceTCP);\n    mdns.run(); // run it right away to get out info ASAP\n  #endif  \n  connected=true;    \n}\n\n#if defined (STM32_ETHERNET)\nvoid EthernetInterface::acceptClient() { // STM32 version\n  auto client=server->available();\n  if (!client) return;\n  // check for existing client\n  for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)\n    if (inUse[socket] && client == clients[socket]) return;\n      \n  // new client\n  for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)\n  {\n    if (!inUse[socket])\n    {\n      clients[socket] = client;\n      inUse[socket]=true;\n      if (Diag::ETHERNET)\n        DIAG(F(\"Ethernet: New client socket %d\"), socket);\n      return;\n    }\n  }\n  // reached here only if more than MAX_SOCK_NUM clients want to connect\n  DIAG(F(\"Ethernet more than %d clients, not accepting new connection\"), MAX_SOCK_NUM);\n  client.stop();\n}\n#else\nvoid EthernetInterface::acceptClient() { // non-STM32 version\n  auto client=server->accept();\n  if (!client) return;\n  auto socket=client.getSocketNumber();\n  clients[socket]=client;\n  inUse[socket]=true;\n  if (Diag::ETHERNET)\n    DIAG(F(\"Ethernet: New client socket %d\"), socket);\n}\n#endif\n\nvoid EthernetInterface::dropClient(byte socket) \n{ \n  clients[socket].stop();\n  inUse[socket]=false;\n  CommandDistributor::forget(socket);\n\tif (Diag::ETHERNET)  DIAG(F(\"Ethernet: Disconnect %d \"), socket);  \n}\n\n/**\n * @brief Main loop for the EthernetInterface\n * \n */\nvoid EthernetInterface::loop()\n{\n    if (!connected) return;\n    looptimer(5000, F(\"E.loop\"));\n\t      \n    static bool warnedAboutLink=false;\n    if (Ethernet.linkStatus() == LinkOFF){\n        if (warnedAboutLink) return;\n        DIAG(F(\"Ethernet link OFF\"));\n        warnedAboutLink=true;\n        return;\n    }\n    looptimer(5000, F(\"E.loop warn\"));\n\t  \n    // link status must be ok here \n    if (warnedAboutLink) {\n      DIAG(F(\"Ethernet link RESTORED\"));\n      warnedAboutLink=false;\n    } \n    \n  #ifdef DO_MDNS\n    // Always do this because we don't want traffic to intefere with being found!\n    mdns.run();\n    looptimer(5000, F(\"E.mdns\"));\n\t  \n  #endif\n\n    //\n    switch (Ethernet.maintain()) {\n    case 1:\n        //renewed fail\n        DIAG(F(\"Ethernet Error: renewed fail\"));\n        connected=false;\n        return;\n    case 3:\n        //rebind fail\n        DIAG(F(\"Ethernet Error: rebind fail\"));\n        connected=false;\n        return;\n    default:\n        //nothing happened\n        //DIAG(F(\"maintained\"));\n        break;\n    }\n    looptimer(5000, F(\"E.maintain\"));\n\t  \n    // get client from the server\n    acceptClient();\n    \n    // handle disconnected sockets because STM32 library doesnt\n    // do the read==0 response.\n    for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)\n    {\n      if (inUse[socket] && !clients[socket].connected()) dropClient(socket);\n    }  \n\n    // check for incoming data from all possible clients\n    for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)\n    {\n      if (!inUse[socket]) continue; // socket is not in use\n\t\n\t    // read any bytes from this client\n\t    auto count = clients[socket].read(buffer, MAX_ETH_BUFFER);\n      \n      if (count<0) continue;  // -1 indicates nothing to read\n\t    \n      if (count > 0) {  // we have incoming data \n\t      buffer[count] = '\\0'; // terminate the string properly\n\t      if (Diag::ETHERNET) DIAG(F(\"Ethernet s=%d, c=%d b=:%e\"), socket, count, buffer);\n\t      // execute with data going directly back\n\t      CommandDistributor::parse(socket,buffer,outboundRing);\n\t      //looptimer(5000, F(\"Ethloop2 parse\"));\n\t      return; // limit the amount of processing that takes place within 1 loop() cycle. \n\t    }\n\t    \n      // count=0 The client has disconnected\n\t    dropClient(socket);\n    }\n\t\n    WiThrottle::loop(outboundRing);\n\n    // handle at most 1 outbound transmission \n    auto socketOut=outboundRing->read();\n    if (socketOut<0) return;  // no outbound pending\n\n    if (socketOut >= MAX_SOCK_NUM) {\n      // This is a catastrophic code failure and unrecoverable.  \n      DIAG(F(\"Ethernet outboundRing s=%d error\"), socketOut);\n      connected=false;\n      return;\n    } \n\n    auto count=outboundRing->count();\n    {\n\t    char tmpbuf[count+1]; // one extra for '\\0'\n\t    for(int i=0;i<count;i++) {\n\t      tmpbuf[i] = outboundRing->read();\n\t    }\n\t    tmpbuf[count]=0;\n      if (inUse[socketOut]) {\n  \t    if (Diag::ETHERNET) DIAG(F(\"Ethernet reply s=%d, c=%d, b:%e\"),\n                              socketOut,count,tmpbuf);\n\t      clients[socketOut].write(tmpbuf,count);\n      }\n    }\n    \n}\n#endif\n"
  },
  {
    "path": "EthernetInterface.h",
    "content": "/*\n *  © 2023-2024 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2024 Harald Barth\n *  © 2020-2024 Chris Harlow\n *  © 2020 Gregor Baues\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX/CommandStation-EX\n *\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n * \n *  Ethernet Interface added by Gregor Baues\n */\n\n#ifndef EthernetInterface_h\n#define EthernetInterface_h\n\n#include \"defines.h\"\n#if ETHERNET_ON == true\n#include \"DCCEXParser.h\"\n#include <Arduino.h>\n//#include <avr/pgmspace.h>\n#if defined (ARDUINO_TEENSY41)\n #include <NativeEthernet.h>         //TEENSY Ethernet Treiber\n #include <NativeEthernetUdp.h>   \n #ifndef MAX_SOCK_NUM\n #define MAX_SOCK_NUM 4\n #endif\n // can't use our MDNS because of a namespace clash with Teensy's NativeEthernet library!\n // #define DO_MDNS\n#elif defined (ARDUINO_NUCLEO_F429ZI) || defined (ARDUINO_NUCLEO_F439ZI) || defined (ARDUINO_NUCLEO_F4X9ZI)\n #include <LwIP.h>\n #include <STM32Ethernet.h>\n #include <lwip/netif.h>\n extern \"C\" struct netif gnetif;\n #define STM32_ETHERNET\n #define MAX_SOCK_NUM MAX_NUM_TCP_CLIENTS\n #define DO_MDNS\n#else\n #include \"Ethernet.h\"\n #define DO_MDNS\n#endif\n\n\n#include \"RingStream.h\"\n\n/**\n * @brief Network Configuration\n * \n */\n\n#define MAX_ETH_BUFFER 128\n#define OUTBOUND_RING_SIZE 2048\n\nclass EthernetInterface {\n\n public:\n     \n     static void setup();       \n     static void loop();\n   \n private:\n    static bool connected;\n    static EthernetServer * server;\n    static EthernetClient clients[MAX_SOCK_NUM];                // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield\n    static bool inUse[MAX_SOCK_NUM];                // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield\n    static uint8_t buffer[MAX_ETH_BUFFER+1];                    // buffer used by TCP for the recv\n    static RingStream * outboundRing;\n    static void acceptClient();\n    static void dropClient(byte socketnum);\n    \n};\n#endif // ETHERNET_ON\n#endif\n"
  },
  {
    "path": "FSH.h",
    "content": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef FSH_h\n#define FSH_h\n\n/* This is an architecture support file to manage the differences \n *  between the nano/uno.mega and the later nanoEvery, unoWifiRev2 etc\n *  \n *  IMPORTANT:\n *  To maintain portability the main code should NOT contain ANY references \n *  to the following: \n *  \n *  __FlashStringHelper     Use FSH instead.\n *  PROGMEM                 use FLASH instead\n *  pgm_read_byte_near      use GETFLASH instead.\n *  pgm_read_word_near      use GETFLASHW instead.\n * \n *  Also:\n *    HIGHFLASH    -  PROGMEM forced to end of link so needs far pointers.\n *    GETHIGHFLASH,GETHIGHFLASHW to access them\n *  \n */\n#include <Arduino.h>\n#ifdef ARDUINO_ARCH_AVR\n// AVR devices have flash memory mapped differently\n// progmem can be accessed by _near functions or _far\ntypedef __FlashStringHelper FSH;\n#define FLASH PROGMEM\n#define GETFLASH(addr) pgm_read_byte_near(addr)\n#define STRCPY_P strcpy_P\n#define STRCMP_P strcmp_P\n#define STRNCPY_P strncpy_P\n#define STRNCMP_P strncmp_P\n#define STRLEN_P strlen_P\n#define STRCHR_P strchr_P \n\n#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n// AVR_MEGA memory deliberately placed at end of link may need _far functions\n#define HIGHFLASH __attribute__((section(\".fini2\")))\n#define HIGHFLASH3 __attribute__((section(\".fini3\")))\n#define GETFARPTR(data) pgm_get_far_address(data)\n#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)\n#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)\n#define COPYHIGHFLASH(target,base,offset,length) \\\n  memcpy_PF(target,GETFARPTR(base) + offset,length)  \n#else\n// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent\n// as there is no progmem above 32kb anyway.\n#define HIGHFLASH PROGMEM\n#define HIGHFLASH3 PROGMEM\n#define GETFARPTR(data) ((uint32_t)(data))\n#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))\n#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))\n#define COPYHIGHFLASH(target,base,offset,length) \\\n  memcpy_P(target,(byte *)base + offset,length)  \n#endif\n\n#else \n// Non-AVR Flat-memory devices have no need of this support so can be remapped to normal memory access\n#ifdef F\n  #undef F\n#endif\n#ifdef FLASH\n  #undef FLASH\n#endif\n#define F(str) (str)\ntypedef char FSH; \n#define FLASH\n#define HIGHFLASH\n#define HIGHFLASH3\n#define GETFARPTR(data) ((uint32_t)(data))\n#define GETFLASH(addr) (*(const byte *)(addr))\n#define GETHIGHFLASH(data,offset)  (*(const byte *)(GETFARPTR(data)+offset))\n#define GETHIGHFLASHW(data,offset) (*(const uint16_t *)(GETFARPTR(data)+offset))\n#define COPYHIGHFLASH(target,base,offset,length) \\\n  memcpy(target,(byte *)&base + offset,length)  \n#define STRCPY_P strcpy\n#define STRCMP_P strcmp\n#define STRNCPY_P strncpy\n#define STRNCMP_P strncmp\n#define STRLEN_P strlen\n#define STRCHR_P strchr\n#endif\n#endif\n"
  },
  {
    "path": "GITHUB_SHA.h",
    "content": "#define GITHUB_SHA \"master-202605011818Z\"\n"
  },
  {
    "path": "I2CManager.cpp",
    "content": "/*\n *  © 2023, Neil McKechnie\n *  © 2022 Paul M Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <stdarg.h>\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\n// Include target-specific portions of I2CManager class\n#if defined(I2C_USE_WIRE) \n#include \"I2CManager_Wire.h\"\n#elif defined(ARDUINO_ARCH_AVR)\n#include \"I2CManager_NonBlocking.h\"\n#include \"I2CManager_AVR.h\"       // Uno/Nano/Mega2560\n#elif defined(ARDUINO_ARCH_MEGAAVR) \n#include \"I2CManager_NonBlocking.h\"\n#include \"I2CManager_Mega4809.h\"  // NanoEvery/UnoWifi\n#elif defined(ARDUINO_ARCH_SAMD)\n#include \"I2CManager_NonBlocking.h\"\n#include \"I2CManager_SAMD.h\"      // SAMD21 for now... SAMD51 as well later\n#elif defined(ARDUINO_ARCH_STM32)\n#include \"I2CManager_NonBlocking.h\"\n#include \"I2CManager_STM32.h\"      // STM32F411RE for now... more later\n#else\n#define I2C_USE_WIRE\n#include \"I2CManager_Wire.h\"      // Other platforms\n#endif\n\n\n// Helper function for listing device types\nstatic const FSH * guessI2CDeviceType(uint8_t address) {\n  if (address >= 0x10 && address <= 0x17)\n    return F(\"EX-SensorCAM\");\n  if (address == 0x1A)\n    // 0x09-0x18 selectable, but for now handle the default\n    return F(\"Piicodev 865/915MHz Transceiver\");\n  if (address == 0x1C)\n    return F(\"QMC6310 Magnetometer\");\n  if (address >= 0x20 && address <= 0x26)\n    return F(\"GPIO Expander\");\n  if (address == 0x27)\n    return F(\"GPIO Expander or LCD Display\");\n  if (address == 0x29)\n    return F(\"Time-of-flight sensor\");\n  if (address == 0x34)\n    return F(\"TCA8418 keypad scanner\");\n  if (address >= 0x3c && address <= 0x3d)\n    // 0x3c can also be an HMC883L magnetometer\n    return F(\"OLED Display or HMC583L Magnetometer\");\n  if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection\n    return F(\"SC16IS75x UART\");\n  if (address >= 0x48 && address <= 0x4f)\n    return F(\"Analogue Inputs or PWM\");\n  if (address >= 0x40 && address <= 0x4f)\n    return F(\"PWM\");\n  if (address >= 0x50 && address <= 0x5f) \n    return F(\"EEPROM\"); \n  if (address >= 0x60 && address < 0x68) \n    return F(\"Adafruit NeoPixel Driver\"); \n  if (address == 0x68) \n    return F(\"Real-time clock\"); \n  if (address >= 0x70 && address <= 0x77)\n    return F(\"I2C Mux\");\n  // Unknown type\n    return F(\"?\");\n}\n\n// If not already initialised, initialise I2C\nvoid I2CManagerClass::begin(void) {\n  if (!_beginCompleted) {\n    _beginCompleted = true;\n\n    // Check for short-circuit or floating lines (no pull-up) on I2C before enabling I2C\n    const FSH *message = F(\"WARNING: Check I2C %S line for short/pullup\");\n    pinMode(SDA, INPUT);\n    if (!digitalRead(SDA))\n      DIAG(message, F(\"SDA\"));\n    pinMode(SCL, INPUT);\n    if (!digitalRead(SCL))\n      DIAG(message, F(\"SCL\"));\n\n    // Now initialise I2C\n    _initialise();\n    scanForDevices(&USB_SERIAL);\n  }\n}\n\n  void I2CManagerClass::scanForDevices(Print* stream) {\n    StringFormatter::send(stream, F(\"<* Scanning I2C bus for devices...\\n\"));\n    #if defined(I2C_USE_WIRE)\n    StringFormatter::send(stream,F(\"   I2CManager: Using Wire library\\n\"));\n    #endif\n\n    // Probe and list devices.  Use standard mode \n    //  (clock speed 100kHz) for best device compatibility.\n    _setClock(100000);\n    uint32_t originalTimeout = _timeout;\n    setTimeout(1000);       // use 1ms timeout for probes\n\n  #if defined(I2C_EXTENDED_ADDRESS)\n    // First count the multiplexers and switch off all subbuses\n    _muxCount = 0;\n    for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {\n      if (I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None})==I2C_STATUS_OK)\n        _muxCount++;\n    }\n  #endif\n\n    // Enumerate devices that are visible\n    bool found = false;\n    for (uint8_t addr=0x08; addr<0x78; addr++) {\n      if (exists(addr)) {\n        found = true; \n        StringFormatter::send(stream,F(\"  Device found at 0x%x, %S?\\n\"), addr, guessI2CDeviceType(addr));\n      }\n    }\n\n#if defined(I2C_EXTENDED_ADDRESS)\n    // Enumerate all I2C devices that are connected via multiplexer, \n    // i.e. that respond when only one multiplexer has one subBus enabled\n    // and the device doesn't respond when the mux subBus is disabled.\n    // If any probes time out, then assume that the subbus is dead and\n    // don't do any more on that subbus.\n    for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {\n      uint8_t muxAddr = I2C_MUX_BASE_ADDRESS + muxNo;\n      if (exists(muxAddr)) {\n        // Select Mux Subbus\n        for (uint8_t subBus=0; subBus<=SubBus_No; subBus++) {\n          muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});\n          for (uint8_t addr=0x08; addr<0x78; addr++) {\n            uint8_t status = checkAddress(addr);\n            if (status == I2C_STATUS_OK) {\n              // De-select subbus\n              muxSelectSubBus({(I2CMux)muxNo, SubBus_None});\n              if (!exists(addr)) {\n                // Device responds when subbus selected but not when\n                // subbus disabled - ergo it must be on subbus!\n                found = true; \n                StringFormatter::send(stream,F(\"  Device found at {I2CMux_%d,SubBus_%d,0x%x}, %S?\\n\"), \n                  muxNo, subBus, addr, guessI2CDeviceType(addr));\n              }\n              // Re-select subbus\n              muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});\n            } else if (status == I2C_STATUS_TIMEOUT) {\n              // Bus stuck, skip to next one.\n              break;\n            }\n          }\n        }\n        // Deselect all subBuses for this mux.  Otherwise its devices will continue to\n        // respond when other muxes are being probed.\n        I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None});  // Deselect Mux\n      } \n    }\n#endif\n    StringFormatter::send(stream,found?F(\"*>\\n\"):F(\"  No I2C Devices found\\n*>\\n\"));\n    _setClock(_clockSpeed);\n    setTimeout(originalTimeout);      // set timeout back to original\n  }\n\n// Set clock speed to the lowest requested one. If none requested,\n//  the Wire default is 100kHz.\nvoid I2CManagerClass::setClock(uint32_t speed) {\n  if (speed < _clockSpeed && !_clockSpeedFixed) {\n    _clockSpeed = speed;\n    DIAG(F(\"I2C clock speed set to %l Hz\"), _clockSpeed);\n  }\n  _setClock(_clockSpeed);\n}\n\n// Force clock speed to that specified.\nvoid I2CManagerClass::forceClock(uint32_t speed) {\n  _clockSpeed = speed;\n  _clockSpeedFixed = true;\n  _setClock(_clockSpeed);\n  DIAG(F(\"I2C clock speed forced to %l Hz\"), _clockSpeed);\n}\n\n// Check if specified I2C address is responding (blocking operation)\n// Returns I2C_STATUS_OK (0) if OK, or error code.\n// Suppress retries.  If it doesn't respond first time it's out of the running.\nuint8_t I2CManagerClass::checkAddress(I2CAddress address) {\n  I2CRB rb;\n  rb.setWriteParams(address, NULL, 0);\n  rb.suppressRetries(true);\n  queueRequest(&rb);\n  return rb.wait();\n}\n\n\n/***************************************************************************\n *  Write a transmission to I2C using a list of data (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::write(I2CAddress address, uint8_t nBytes, ...) {\n  uint8_t buffer[nBytes];\n  va_list args;\n  va_start(args, nBytes);\n  for (uint8_t i=0; i<nBytes; i++)\n    buffer[i] = va_arg(args, int);\n  va_end(args);\n  return write(address, buffer, nBytes);\n}\n\n/***************************************************************************\n *  Initiate a write to an I2C device (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {\n  I2CRB req;\n  uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);\n  return finishRB(&req, status);\n}\n\n/***************************************************************************\n *  Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * data, uint8_t dataLen) {\n  I2CRB req;\n  uint8_t status = write_P(i2cAddress, data, dataLen, &req);\n  return finishRB(&req, status);\n}\n\n/***************************************************************************\n *  Initiate a write (optional) followed by a read from the I2C device (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, \n    const uint8_t *writeBuffer, uint8_t writeLen)\n{\n  I2CRB req;\n  uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);\n  return finishRB(&req, status);\n}\n\n/***************************************************************************\n *  Overload of read() to allow command to be specified as a series of bytes (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, \n                                  uint8_t writeSize, ...) {\n  va_list args;\n  // Copy the series of bytes into an array.\n  va_start(args, writeSize);\n  uint8_t writeBuffer[writeSize];\n  for (uint8_t i=0; i<writeSize; i++)\n    writeBuffer[i] = va_arg(args, int);\n  va_end(args);\n  return read(address, readBuffer, readSize, writeBuffer, writeSize);\n}\n\n/***************************************************************************\n * Finish off request block by posting status, etc. (blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {\n  if ((status == I2C_STATUS_OK) && rb)\n    status = rb->wait();\n  return status;\n}\n\n/***************************************************************************\n * Get a message corresponding to the error status\n ***************************************************************************/\nconst FSH *I2CManagerClass::getErrorMessage(uint8_t status) {\n  switch (status) {\n    case I2C_STATUS_OK: return F(\"OK\");\n    case I2C_STATUS_TRUNCATED: return F(\"Transmission truncated\");\n    case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F(\"No response from device (address NAK)\");\n    case I2C_STATUS_TRANSMIT_ERROR: return F(\"Transmit error (data NAK)\");\n    case I2C_STATUS_OTHER_TWI_ERROR: return F(\"Other Wire/TWI error\");\n    case I2C_STATUS_TIMEOUT: return F(\"I2C bus timeout\");\n    case I2C_STATUS_ARBITRATION_LOST: return F(\"Arbitration lost\");\n    case I2C_STATUS_BUS_ERROR: return F(\"I2C bus error\");\n    case I2C_STATUS_UNEXPECTED_ERROR: return F(\"Unexpected error\");\n    case I2C_STATUS_PENDING: return F(\"Request pending\");\n    default: return F(\"Error code not recognised\");\n  }\n}\n\n/***************************************************************************\n *  Declare singleton class instance.\n ***************************************************************************/\nI2CManagerClass I2CManager = I2CManagerClass();\n\n// Buffer for conversion of I2CAddress to char*.\n/* static */ char I2CAddress::addressBuffer[30];\n\n/////////////////////////////////////////////////////////////////////////////\n// Helper functions associated with I2C Request Block\n/////////////////////////////////////////////////////////////////////////////\n\n/***************************************************************************\n *  Block waiting for request to complete, and return completion status.\n *  Timeout monitoring is performed in the I2CManager.loop() function.\n ***************************************************************************/\nuint8_t I2CRB::wait() {\n  while (status==I2C_STATUS_PENDING) {\n    I2CManager.loop();\n  };\n  return status;\n}\n\n/***************************************************************************\n *  Check whether request is still in progress.\n *  Timeout monitoring is performed in the I2CManager.loop() function.\n ***************************************************************************/\nbool I2CRB::isBusy() {\n  if (status==I2C_STATUS_PENDING) {\n    I2CManager.loop();\n    return true;\n  } else\n    return false;\n}\n\n/***************************************************************************\n *  Helper functions to fill the I2CRequest structure with parameters.\n ***************************************************************************/\nvoid I2CRB::setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen) {\n  this->i2cAddress = i2cAddress;\n  this->writeLen = 0;\n  this->readBuffer = readBuffer;\n  this->readLen = readLen;\n  this->operation = OPERATION_READ;\n  this->status = I2C_STATUS_OK;\n}\n\nvoid I2CRB::setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, \n    const uint8_t *writeBuffer, uint8_t writeLen) {\n  this->i2cAddress = i2cAddress;\n  this->writeBuffer = writeBuffer;\n  this->writeLen = writeLen;\n  this->readBuffer = readBuffer;\n  this->readLen = readLen;\n  this->operation = OPERATION_REQUEST;\n  this->status = I2C_STATUS_OK;\n}\n\nvoid I2CRB::setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {\n  this->i2cAddress = i2cAddress;\n  this->writeBuffer = writeBuffer;\n  this->writeLen = writeLen;\n  this->readLen = 0;\n  this->operation = OPERATION_SEND;\n  this->status = I2C_STATUS_OK;\n}\n\nvoid I2CRB::suppressRetries(bool suppress) {\n  if (suppress)\n    this->operation |= OPERATION_NORETRY;\n  else\n    this->operation &= ~OPERATION_NORETRY;\n}\n\n\n// Helper function for converting a uint8_t to four characters (e.g. 0x23).\nvoid I2CAddress::toHex(const uint8_t value, char *buffer) {\n  char *ptr = buffer;\n  // Just display hex value, two digits.\n  *ptr++ = '0';\n  *ptr++ = 'x';\n  uint8_t bits = (value >> 4) & 0xf;\n  *ptr++ = bits > 9 ? bits-10+'a' : bits+'0';\n  bits = value & 0xf;\n  *ptr++ = bits > 9 ? bits-10+'a' : bits+'0';\n}\n\n#if !defined(I2C_EXTENDED_ADDRESS) \n\n/* static */ bool I2CAddress::_addressWarningDone = false;\n\n#endif\n"
  },
  {
    "path": "I2CManager.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  © 2022 Paul M Antoine\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_H\n#define I2CMANAGER_H\n\n#include <inttypes.h>\n#include \"FSH.h\"\n#include \"defines.h\"\n#include \"DIAG.h\"\n\n/* \n * Manager for I2C communications.  For portability, it allows use \n * of the Wire class, but also has a native implementation for AVR\n * which supports non-blocking queued I/O requests.\n * \n * Helps to avoid calling Wire.begin() multiple times (which is not\n * entirely benign as it reinitialises).\n * \n * Also helps to avoid the Wire clock from being set, by another device\n * driver, to a speed which is higher than a device supports.\n * \n * Thirdly, it provides a convenient way to check whether there is a \n * device on a particular I2C address.\n * \n * Non-blocking requests are issued by creating an I2C Request Block\n * (I2CRB) which is then added to the I2C manager's queue.  The \n * application refers to this block to check for completion of the\n * operation, and for reading completion status.\n * \n * Examples:\n *  I2CRB rb;\n *  uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb);\n *  ...\n *  if (!rb.isBusy()) {\n *    status = rb.status;\n *    // Repeat write\n *    I2CManager.queueRequest(&rb);\n *    ...\n *    status = rb.wait(); // Wait for completion and read status\n *  }\n *  ...\n *  I2CRB rb2;\n *  outbuffer[0] = 12;  // Register number in I2C device to be read\n *  rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1);\n *  status = I2CManager.queueRequest(&rb2);\n *  if (status == I2C_STATUS_OK) { \n *    status = rb2.wait();\n *    if (status == I2C_STATUS_OK) {\n *      registerValue = inBuffer[0];\n *    }\n *  }\n *  ...\n *  \n * Synchronous (blocking) calls are also possible, e.g. \n *  status = I2CManager.write(address, buffer, sizeof(buffer));\n * \n * When using non-blocking requests, neither the I2CRB nor the input or output\n * buffers should be modified until the I2CRB is complete (not busy).\n * \n * Timeout monitoring is possible, but requires that the following call is made\n * reasonably frequently in the program's loop() function:\n *  I2CManager.loop();\n * So that the application doesn't need to do this explicitly, this call is performed\n * from the I2CRB::isBusy() or I2CRB::wait() functions.\n * \n */\n\n/* \n *  I2C Multiplexer (e.g. TCA9547, TCA9548)\n * \n *  A multiplexer offers a way of extending the address range of I2C devices.  For example, GPIO extenders use address range 0x20-0x27\n *  to are limited to 8 on a bus.  By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64.\n *  And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's \n *  as many as 8192 input/output pins!\n *  Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices.\n *  The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus.  For example, with one \n *  multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders). \n *  Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be\n *  put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions.\n * \n * \n *  Non-interrupting I2C:\n * \n *  Non-blocking I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS).  Instead, the I2C state\n *  machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function.\n *  The speed at which I2C operations can be performed then becomes highly dependent on the frequency that \n *  the loop() function is called, and may be adequate under some circumstances.  \n *  The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use)\n *  becomes almost zero.\n * \n */\n\n// Maximum number of retries on an I2C operation.\n// A value of zero will disable retries.\n// Maximum value is 254 (unsigned byte counter)\n// Note that timeout failures are not retried, but any timeout\n// configured applies to each try separately.\n#define MAX_I2C_RETRIES 2\n\n// Add following line to config.h to enable Wire library instead of native I2C drivers\n//#define I2C_USE_WIRE\n\n// Add following line to config.h to disable the use of interrupts by the native I2C drivers.\n//#define I2C_NO_INTERRUPTS\n\n// Default to use interrupts within the native I2C drivers.\n#ifndef I2C_NO_INTERRUPTS\n#define I2C_USE_INTERRUPTS\n#endif\n\n// I2C Extended Address support I2C Multiplexers and allows various properties to be \n// associated with an I2C address such as the MUX and SubBus.  In the future, this\n// may be extended to include multiple buses, and other features. \n// Uncomment to enable extended address.\n//\n\n//#define I2C_EXTENDED_ADDRESS\n\n/////////////////////////////////////////////////////////////////////////////////////\n// Extended I2C Address type to facilitate extended I2C addresses including\n// I2C multiplexer support.\n/////////////////////////////////////////////////////////////////////////////////////\n\n// Currently only one bus supported, and one instance of I2CManager to handle it.\nenum I2CBus : uint8_t {\n    I2CBus_0 = 0,\n};\n\n// Currently I2CAddress supports one I2C bus, with up to eight\n// multipexers (MUX) attached.  Each MUX can have up to eight sub-buses.\nenum I2CMux : uint8_t {\n  I2CMux_0 = 0,\n  I2CMux_1 = 1,\n  I2CMux_2 = 2,\n  I2CMux_3 = 3,\n  I2CMux_4 = 4,\n  I2CMux_5 = 5,\n  I2CMux_6 = 6,\n  I2CMux_7 = 7,\n  I2CMux_None = 255,   // Address doesn't need mux switching\n};\n  \nenum I2CSubBus : uint8_t {\n  SubBus_0 = 0,        // Enable individual sub-buses...\n  SubBus_1 = 1,\n#if !defined(I2CMUX_PCA9542)\n  SubBus_2 = 2,\n  SubBus_3 = 3,\n#if !defined(I2CMUX_PCA9544)\n  SubBus_4 = 4,\n  SubBus_5 = 5,\n  SubBus_6 = 6,\n  SubBus_7 = 7,\n#endif\n#endif\n  SubBus_No,           // Number of subbuses (highest + 1)\n  SubBus_None = 254,   // Disable all sub-buses on selected mux\n  SubBus_All = 255,    // Enable all sub-buses (not supported by some multiplexers)\n};\n\n// Type to hold I2C address\n#if defined(I2C_EXTENDED_ADDRESS)\n\n// First MUX address (they range between 0x70-0x77).\n#define I2C_MUX_BASE_ADDRESS 0x70\n\n// Currently I2C address supports one I2C bus, with up to eight\n// multiplexers (MUX) attached.  Each MUX can have up to eight sub-buses.\n// This structure could be extended in the future (if there is a need) \n// to support 10-bit I2C addresses, different I2C clock speed for each\n// sub-bus, multiple I2C buses, and other features not yet thought of.\nstruct I2CAddress {\nprivate:\n  // Fields\n  I2CBus _busNumber;\n  I2CMux _muxNumber;\n  I2CSubBus _subBus;\n  uint8_t _deviceAddress;\n  static char addressBuffer[];\npublic:\n  // Constructors\n  // For I2CAddress \"{I2CBus_0, Mux_0, SubBus_0, 0x23}\" syntax.\n  I2CAddress(const I2CBus busNumber, const I2CMux muxNumber, const I2CSubBus subBus, const uint8_t deviceAddress) {\n    _busNumber = busNumber;\n    _muxNumber = muxNumber;\n    _subBus = subBus;\n    _deviceAddress = deviceAddress;\n  }\n\n  // Basic constructor\n  I2CAddress() : I2CAddress(I2CMux_None, SubBus_None, 0) {}\n\n  // For I2CAddress \"{Mux_0, SubBus_0, 0x23}\" syntax.\n  I2CAddress(const I2CMux muxNumber, const I2CSubBus subBus, const uint8_t deviceAddress) :\n    I2CAddress(I2CBus_0, muxNumber, subBus, deviceAddress) {}\n\n  // For I2CAddress in form \"{SubBus_0, 0x23}\" - assume Mux0 (0x70)\n  I2CAddress(I2CSubBus subBus, uint8_t deviceAddress) : \n    I2CAddress(I2CMux_0, subBus, deviceAddress) {}\n\n  // Conversion from uint8_t to I2CAddress\n  // For I2CAddress in form \"0x23\"\n  // (device assumed to be on the main I2C bus).\n  I2CAddress(const uint8_t deviceAddress) : \n    I2CAddress(I2CMux_None, SubBus_None, deviceAddress) {}\n    \n  // Conversion from uint8_t to I2CAddress\n  // For I2CAddress in form \"{I2CBus_1, 0x23}\"\n  // (device not connected via multiplexer).\n  I2CAddress(const I2CBus bus, const uint8_t deviceAddress) : \n    I2CAddress(bus, I2CMux_None, SubBus_None, deviceAddress) {}\n\n  // For I2CAddress in form \"{I2CMux_0, SubBus_0}\" (mux selector)\n  I2CAddress(const I2CMux muxNumber, const I2CSubBus subBus) :\n    I2CAddress(muxNumber, subBus, 0x00) {}\n\n  // For I2CAddress in form \"{i2cAddress, deviceAddress}\"\n  // where deviceAddress is to be on the same subbus as i2cAddress.\n  I2CAddress(I2CAddress firstAddress, uint8_t newDeviceAddress) :\n    I2CAddress(firstAddress._muxNumber, firstAddress._subBus, newDeviceAddress) {}\n\n  // Conversion operator from I2CAddress to uint8_t\n  // For \"uint8_t address = i2cAddress;\" syntax\n  // (device assumed to be on the main I2C bus or on a currently selected subbus.\n  operator uint8_t () const { return _deviceAddress; }\n\n  // Conversion from I2CAddress to char* (uses static storage so only \n  // one conversion can be done at a time).  So don't call it twice in a\n  // single DIAG statement for example.\n  const char* toString() { \n    char *ptr = addressBuffer;\n    if (_muxNumber != I2CMux_None) {\n      strcpy_P(ptr, (const char*)F(\"{I2CMux_\"));\n      ptr += 8;\n      *ptr++ = '0' + _muxNumber;\n      strcpy_P(ptr, (const char*)F(\",SubBus_\"));\n      ptr += 8;\n      if (_subBus == SubBus_None) {\n        strcpy_P(ptr, (const char*)F(\"None\"));\n        ptr += 4;\n      } else if (_subBus == SubBus_All) {\n        strcpy_P(ptr, (const char*)F(\"All\"));\n        ptr += 3;\n      } else \n        *ptr++ = '0' + _subBus;\n      *ptr++ = ',';\n    }\n    toHex(_deviceAddress, ptr);\n    ptr += 4;\n    if (_muxNumber != I2CMux_None)\n      *ptr++ = '}';\n    *ptr = 0; // terminate string\n    return addressBuffer;\n  }\n\n  // Comparison operator\n  int operator == (I2CAddress &a) const {\n    if (_deviceAddress != a._deviceAddress) \n      return false; // Different device address so no match\n    if (_muxNumber == I2CMux_None || a._muxNumber == I2CMux_None)\n      return true;  // Same device address, one or other on main bus\n    if (_subBus == SubBus_None || a._subBus == SubBus_None) \n      return true;  // Same device address, one or other on main bus\n    if (_muxNumber != a._muxNumber) \n      return false; // Connected to a subbus on a different mux\n    if (_subBus != a._subBus)\n      return false;  // different subbus\n    return true;  // Same address on same mux and same subbus\n  }\n  // Field accessors\n  I2CMux muxNumber() { return _muxNumber; }\n  I2CSubBus subBus() { return _subBus; }\n  uint8_t deviceAddress() { return _deviceAddress; }\n\nprivate:\n  // Helper function for converting byte to four-character hex string (e.g. 0x23).\n  void toHex(const uint8_t value, char *buffer);\n};\n\n#else\nstruct I2CAddress {\nprivate:\n  uint8_t _deviceAddress;\n  static char addressBuffer[];\npublic:\n  // Constructors\n  I2CAddress(const uint8_t deviceAddress) {\n    _deviceAddress = deviceAddress;\n  }\n  I2CAddress(I2CMux, I2CSubBus, const uint8_t deviceAddress) {\n    addressWarning();\n    _deviceAddress = deviceAddress;\n  }\n  I2CAddress(I2CSubBus, const uint8_t deviceAddress) {\n    addressWarning();\n    _deviceAddress = deviceAddress;\n  }\n\n  // Basic constructor\n  I2CAddress() : I2CAddress(0) {}\n\n  // Conversion operator from I2CAddress to uint8_t\n  // For \"uint8_t address = i2cAddress;\" syntax\n  operator uint8_t () const { return _deviceAddress; }\n\n  // Conversion from I2CAddress to char* (uses static storage so only \n  // one conversion can be done at a time).  So don't call it twice in a\n  // single DIAG statement for example.\n  const char* toString () { \n    char *ptr = addressBuffer;\n    // Just display hex value, two digits.\n    toHex(_deviceAddress, ptr);\n    ptr += 4;\n    *ptr = 0; // terminate string\n    return addressBuffer;\n  }\n\n  // Comparison operator\n  int operator == (I2CAddress &a) const {\n    if (_deviceAddress != a._deviceAddress) \n      return false; // Different device address so no match\n    return true;  // Same address on same mux and same subbus\n  }\nprivate:\n  // Helper function for converting byte to four-character hex string (e.g. 0x23).\n  void toHex(const uint8_t value, char *buffer);\n  void addressWarning() {\n    if (!_addressWarningDone) {\n      DIAG(F(\"WARNIING: Extended I2C address used but not supported in this configuration\"));\n      _addressWarningDone = true;\n    }\n  }    \n  static bool _addressWarningDone;\n};\n#endif // I2C_EXTENDED_ADDRESS\n\n\n// Status codes for I2CRB structures.\nenum : uint8_t {\n  // Codes used by Wire and by native drivers\n  I2C_STATUS_OK=0,\n  I2C_STATUS_TRUNCATED=1,\n  I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2,\n  I2C_STATUS_TRANSMIT_ERROR=3,\n  I2C_STATUS_TIMEOUT=5,\n  // Code used by Wire only\n  I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error\n  // Codes used by native drivers only\n  I2C_STATUS_ARBITRATION_LOST=6,\n  I2C_STATUS_BUS_ERROR=7,\n  I2C_STATUS_UNEXPECTED_ERROR=8,\n  I2C_STATUS_PENDING=253,\n};\n\n// Status codes for the state machine (not returned to caller).\nenum : uint8_t {\n  I2C_STATE_ACTIVE=253,\n  I2C_STATE_FREE=254,\n  I2C_STATE_CLOSING=255,\n  I2C_STATE_COMPLETED=252,\n};\n\ntypedef enum : uint8_t\n{\n  OPERATION_READ = 1,\n  OPERATION_REQUEST = 2,\n  OPERATION_SEND = 3,\n  OPERATION_SEND_P = 4,\n  OPERATION_NORETRY = 0x80,  // OR with operation to suppress retries.\n  OPERATION_MASK = 0x7f,  // mask for extracting the operation code\n} OperationEnum;\n\n\n// Default I2C frequency\n#ifndef I2C_FREQ\n#define I2C_FREQ    400000L\n#endif\n\n// Class defining a request context for an I2C operation.\nclass I2CRB {\npublic:\n  volatile uint8_t status; // Completion status, or pending flag (updated from IRC)\n  volatile uint8_t nBytes; // Number of bytes read (updated from IRC)\n\n  inline I2CRB() { status = I2C_STATUS_OK; };\n  uint8_t wait();\n  bool isBusy();\n\n  void setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen);\n  void setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);\n  void setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);\n  void suppressRetries(bool suppress);\n\n  uint8_t writeLen;\n  uint8_t readLen;\n  uint8_t operation;\n  I2CAddress i2cAddress;\n  uint8_t *readBuffer;\n  const uint8_t *writeBuffer;\n#if !defined(I2C_USE_WIRE)\n  I2CRB *nextRequest;  // Used by non-blocking devices for I2CRB queue management.\n#endif\n};\n\n// I2C Manager\nclass I2CManagerClass {\npublic:\n\n  // If not already initialised, initialise I2C (wire).\n  void begin(void);\n  // Set clock speed to the lowest requested one.\n  void setClock(uint32_t speed);\n  // Force clock speed \n  void forceClock(uint32_t speed);\n  // setTimeout sets the timout value for I2C transactions (milliseconds).\n  void setTimeout(unsigned long);\n  // Check if specified I2C address is responding.\n  uint8_t checkAddress(I2CAddress address);\n  inline bool exists(I2CAddress address) {\n    return checkAddress(address)==I2C_STATUS_OK;\n  }\n  // Select/deselect Mux Sub-Bus (if using legacy addresses, just checks address)\n  // E.g. muxSelectSubBus({I2CMux_0, SubBus_3});\n  uint8_t muxSelectSubBus(I2CAddress address) {\n    return checkAddress(address);\n  }\n  // Write a complete transmission to I2C from an array in RAM\n  uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size);\n  uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb);\n  // Write a complete transmission to I2C from an array in Flash\n  uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size);\n  uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb);\n  // Write a transmission to I2C from a list of bytes.\n  uint8_t write(I2CAddress address, uint8_t nBytes, ...);\n  // Write a command from an array in RAM and read response\n  uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, \n    const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);\n  uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, \n    const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);\n  // Write a command from an arbitrary list of bytes and read response\n  uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, \n    uint8_t writeSize, ...);\n  void queueRequest(I2CRB *req);\n\n  // Function to abort long-running operations.\n  void checkForTimeout();\n\n  // Loop method\n  void loop();\n\n  // Expand error codes into text.  Note that they are in flash so \n  // need to be printed using FSH.\n  static const FSH *getErrorMessage(uint8_t status);\n\n  void scanForDevices(Print * stream);\n\nprivate:\n  bool _beginCompleted = false;\n  bool _clockSpeedFixed = false;\n  uint8_t retryCounter;  // Count of retries\n  // Clock speed must be no higher than 400kHz on AVR. Higher is possible on 4809, SAMD\n  // and STM32 but most popular I2C devices are 400kHz so in practice the higher speeds\n  // will not be useful.  The speed can be overridden by I2CManager::forceClock().\n  uint32_t _clockSpeed = I2C_FREQ;  \n  // Default timeout 100ms on I2C request block completion.\n  // A full 32-byte transmission takes about 8ms at 100kHz,\n  // so this value allows lots of headroom.  \n  // It can be modified by calling I2CManager.setTimeout() function.\n  // When retries are enabled, the timeout applies to each\n  // try, and failure from timeout does not get retried.\n  // A value of 0 means disable timeout monitoring.\n  uint32_t _timeout = 100000UL;\n    \n  // Finish off request block by waiting for completion and posting status.\n  uint8_t finishRB(I2CRB *rb, uint8_t status);\n\n  void _initialise();\n  void _setClock(unsigned long);\n\n#if defined(I2C_EXTENDED_ADDRESS)\n// Count of I2C multiplexers found when initialising.  If there is only one\n// MUX then the subbus does not need de-selecting after use; however, if there\n// are two or more, then the subbus must be deselected to avoid multiple\n// sub-bus legs on different multiplexers being accessible simultaneously.\nprivate:\n  uint8_t _muxCount = 0;\npublic:\n  uint8_t getMuxCount() { return _muxCount; }\n#endif\n\n#if !defined(I2C_USE_WIRE)\n    // I2CRB structs are queued on the following two links.\n    // If there are no requests, both are NULL.\n    // If there is only one request, then queueHead and queueTail both point to it.\n    // Otherwise, queueHead is the pointer to the first request in the queue and\n    // queueTail is the pointer to the last request in the queue.\n    // Within the queue, each request's nextRequest field points to the \n    // next request, or NULL.\n    // Mark volatile as they are updated by IRC and read/written elsewhere.\nprivate:\n    I2CRB * volatile queueHead = NULL;\n    I2CRB * volatile queueTail = NULL;\n\n    // State is set to I2C_STATE_FREE when the interrupt handler has finished\n    // the current request and is ready to complete.\n    uint8_t state = I2C_STATE_FREE;\n\n    // CompletionStatus may be set by the interrupt handler at any time but is\n    // not written to the I2CRB until the state is I2C_STATE_FREE.\n    uint8_t completionStatus = I2C_STATUS_OK;\n    uint8_t overallStatus = I2C_STATUS_OK;\n\n    I2CRB * currentRequest = NULL;\n    uint8_t txCount = 0;\n    uint8_t rxCount = 0;\n    uint8_t bytesToSend = 0;\n    uint8_t bytesToReceive = 0;\n    uint8_t operation = 0;\n    uint32_t startTime = 0;\n    uint8_t muxPhase = 0;\n    uint8_t muxAddress = 0;\n    uint8_t muxData[1];\n    uint8_t deviceAddress;\n    const uint8_t *sendBuffer;\n    uint8_t *receiveBuffer;\n    uint8_t transactionState = 0;\n  \n    volatile uint32_t pendingClockSpeed = 0;\n\n    void startTransaction();\n    \n    // Low-level hardware manipulation functions.\n    void I2C_init();\n    void I2C_setClock(unsigned long i2cClockSpeed);\n    void I2C_handleInterrupt();\n    void I2C_sendStart();\n    void I2C_sendStop();\n    void I2C_close();\n    \n  public:\n    // handleInterrupt needs to be public to be called from the ISR function!\n    void handleInterrupt();\n#endif\n\n\n};\n\n// Pointer to class instance (Note: if there is more than one bus, each will have\n// its own instance of I2CManager, selected by the queueRequest function from\n// the I2CBus field within the request block's I2CAddress).\nextern I2CManagerClass I2CManager;\n\n\n#endif\n"
  },
  {
    "path": "I2CManager_AVR.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_AVR_H\n#define I2CMANAGER_AVR_H\n\n#include <Arduino.h>\n#include \"I2CManager.h\"\n#include \"I2CManager_NonBlocking.h\"   // to satisfy intellisense\n\n#include <avr/io.h>\n#include <avr/interrupt.h>\n\n/****************************************************************************\n  TWI State codes\n****************************************************************************/\n// General TWI Master staus codes                      \n#define TWI_START                  0x08  // START has been transmitted  \n#define TWI_REP_START              0x10  // Repeated START has been transmitted\n#define TWI_ARB_LOST               0x38  // Arbitration lost\n\n// TWI Master Transmitter staus codes                      \n#define TWI_MTX_ADR_ACK            0x18  // SLA+W has been tramsmitted and ACK received\n#define TWI_MTX_ADR_NACK           0x20  // SLA+W has been tramsmitted and NACK received \n#define TWI_MTX_DATA_ACK           0x28  // Data byte has been tramsmitted and ACK received\n#define TWI_MTX_DATA_NACK          0x30  // Data byte has been tramsmitted and NACK received \n\n// TWI Master Receiver staus codes  \n#define TWI_MRX_ADR_ACK            0x40  // SLA+R has been tramsmitted and ACK received\n#define TWI_MRX_ADR_NACK           0x48  // SLA+R has been tramsmitted and NACK received\n#define TWI_MRX_DATA_ACK           0x50  // Data byte has been received and ACK tramsmitted\n#define TWI_MRX_DATA_NACK          0x58  // Data byte has been received and NACK tramsmitted\n\n// TWI Miscellaneous status codes\n#define TWI_NO_STATE               0xF8  // No relevant state information available\n#define TWI_BUS_ERROR              0x00  // Bus error due to an illegal START or STOP condition\n\n#define TWI_TWBR  ((F_CPU / I2C_FREQ) - 16) / 2 // TWI Bit rate Register setting.\n\n#if defined(I2C_USE_INTERRUPTS)\n#define ENABLE_TWI_INTERRUPT (1<<TWIE)\n#else\n#define ENABLE_TWI_INTERRUPT 0\n#endif\n\n/***************************************************************************\n *  Set I2C clock speed register.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {\n  unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2;\n  for (uint8_t preScaler = 0; preScaler<=3; preScaler++) {\n    if (temp <= 255) {\n      TWBR = temp;\n      TWSR = (TWSR & 0xfc) | preScaler;\n      return;\n    } else \n      temp /= 4;\n  }\n  // Set slowest speed ~= 500 bits/sec\n  TWBR = 255;\n  TWSR |= 0x03;\n}\n\n/***************************************************************************\n *  Initialise I2C registers.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_init()\n{\n  TWSR = 0;\n  TWBR = TWI_TWBR;                                  // Set bit rate register (Baudrate). Defined in header file.\n  TWDR = 0xFF;                                      // Default content = SDA released.\n  TWCR = (1<<TWINT);                                // Clear interrupt flag\n \n  pinMode(SDA, INPUT_PULLUP);\n  pinMode(SCL, INPUT_PULLUP);\n}\n\n/***************************************************************************\n *  Initiate a start bit for transmission.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStart() {\n  rxCount = 0;\n  txCount = 0;\n  // We may have already triggered a stop bit in the same run as this.  To avoid\n  // clearing that bit before the stop bit has been sent, we can either wait for\n  // it to complete or we can OR the bit onto the existing bits.\n  TWCR |= (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA);  // Send Start\n\n}\n\n/***************************************************************************\n *  Initiate a stop bit for transmission (does not interrupt)\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStop() {\n  TWDR = 0xff;  // Default condition = SDA released\n  TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO);  // Send Stop\n}\n\n/***************************************************************************\n *  Close I2C down\n ***************************************************************************/\nvoid I2CManagerClass::I2C_close() {\n  // disable TWI\n  TWCR = (1<<TWINT);                 // clear any interrupt and stop twi.\n  delayMicroseconds(10);  // Wait for things to stabilise (hopefully)\n}\n\n/***************************************************************************\n *  Main state machine for I2C, called from interrupt handler or,\n *  if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function\n *  (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).\n ***************************************************************************/\n\nvoid I2CManagerClass::I2C_handleInterrupt() {\n  if (!(TWCR & (1<<TWINT))) return;  // Nothing to do.\n\n  uint8_t twsr = TWSR & 0xF8;\n\n\n  // Main I2C interrupt handler, used for the device communications.\n  // The following variables are used:\n  //    bytesToSend, bytesToReceive (R/W)\n  //    txCount, rxCount (W)\n  //    deviceAddress (R)\n  //    sendBuffer, receiveBuffer (R)\n  //    operation (R)\n  //    state, completionStatus (W)\n  // \n  // Cases are ordered so that the most frequently used ones are tested first.\n  switch (twsr) {\n    case TWI_MTX_DATA_ACK:      // Data byte has been transmitted and ACK received\n    case TWI_MTX_ADR_ACK:       // SLA+W has been transmitted and ACK received\n      if (bytesToSend) {  // Send first.\n        if (operation == OPERATION_SEND_P)\n          TWDR = GETFLASH(sendBuffer + (txCount++));\n        else\n          TWDR = sendBuffer[txCount++];\n        bytesToSend--;\n        TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT);\n      } else if (bytesToReceive) {  // All sent, anything to receive?\n        // Don't need to wait for stop, as the interface won't send the start until\n        // any in-progress stop condition from previous interrupts has been sent.\n        TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWSTA);  // Send Start\n      } else {  \n         // Nothing left to send or receive\n        TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO);  // Send Stop\n        state = I2C_STATE_COMPLETED;\n      }\n      break;\n\n    case TWI_MRX_DATA_ACK:      // Data byte has been received and ACK transmitted\n      if (bytesToReceive > 0) {\n        receiveBuffer[rxCount++] = TWDR;\n        bytesToReceive--;\n      }\n      /* fallthrough */\n\n    case TWI_MRX_ADR_ACK:      // SLA+R has been sent and ACK received\n      if (bytesToReceive <= 1) {\n        TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception\n      } else {\n        // send ack\n        TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);\n      }\n      break;\n\n    case TWI_MRX_DATA_NACK:     // Data byte has been received and NACK transmitted\n      if (bytesToReceive > 0) {\n        receiveBuffer[rxCount++] = TWDR;\n        bytesToReceive--;\n      }\n      TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO);  // Send Stop\n      state = I2C_STATE_COMPLETED;\n      break;\n\n    case TWI_START:             // START has been transmitted  \n    case TWI_REP_START:         // Repeated START has been transmitted\n      // Set up address and R/W\n      if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))\n        TWDR = (deviceAddress << 1) | 1; // SLA+R\n      else\n        TWDR = (deviceAddress << 1) | 0; // SLA+W\n      TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);\n      break;\n\n    case TWI_MTX_ADR_NACK:      // SLA+W has been transmitted and NACK received\n    case TWI_MRX_ADR_NACK:      // SLA+R has been transmitted and NACK received\n    case TWI_MTX_DATA_NACK:     // Data byte has been transmitted and NACK received\n      TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO);  // Send Stop\n      completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;\n      state = I2C_STATE_COMPLETED;\n      break;\n\n    case TWI_ARB_LOST:          // Arbitration lost\n      // Restart transaction from start.\n      I2C_sendStart();\n      break;\n\n    case TWI_BUS_ERROR:         // Bus error due to an illegal START or STOP condition\n    default:\n      TWDR = 0xff;  // Default condition = SDA released\n      TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO);  // Send Stop\n      completionStatus = I2C_STATUS_TRANSMIT_ERROR;\n      state = I2C_STATE_COMPLETED;\n  }\n}\n\n#if defined(I2C_USE_INTERRUPTS)\nISR(TWI_vect) {\n  I2CManager.handleInterrupt();\n}\n#endif\n\n#endif /* I2CMANAGER_AVR_H */\n"
  },
  {
    "path": "I2CManager_Mega4809.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_MEGA4809_H\n#define I2CMANAGER_MEGA4809_H\n\n#include <Arduino.h>\n#include \"I2CManager.h\"\n\n/***************************************************************************\n *  Set I2C clock speed register.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {\n  uint16_t t_rise;\n  if (i2cClockSpeed < 200000)\n    t_rise = 1000;\n  else if (i2cClockSpeed < 800000)\n    t_rise = 300;\n  else\n    t_rise = 120;\n\n  if (t_rise == 120)\n    TWI0.CTRLA |= TWI_FMPEN_bm;\n  else\n    TWI0.CTRLA &= ~TWI_FMPEN_bm;\n  \n  uint32_t baud = (F_CPU_CORRECTED / i2cClockSpeed - F_CPU_CORRECTED / 1000 / 1000\n    * t_rise / 1000 - 10) / 2;\n  if (baud > 255) baud = 255;  // ~30kHz\n  TWI0.MBAUD = (uint8_t)baud;\n}\n\n/***************************************************************************\n *  Initialise I2C registers.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_init()\n{ \n  pinMode(PIN_WIRE_SDA, INPUT_PULLUP);\n  pinMode(PIN_WIRE_SCL, INPUT_PULLUP);\n  PORTMUX.TWISPIROUTEA |= TWI_MUX;\n  I2C_setClock(I2C_FREQ);\n\n#if defined(I2C_USE_INTERRUPTS)\n  TWI0.MCTRLA = TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm;\n#else\n  TWI0.MCTRLA = TWI_ENABLE_bm;\n#endif\n  TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;\n}\n\n/***************************************************************************\n *  Initiate a start bit for transmission, followed by address and R/W\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStart() {\n  txCount = 0;\n  rxCount = 0;\n\n  // If anything to send, initiate write.  Otherwise initiate read.\n  if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))\n    TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;\n  else\n    TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;\n}\n\n/***************************************************************************\n *  Initiate a stop bit for transmission.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStop() {\n  TWI0.MCTRLB = TWI_MCMD_STOP_gc;\n}\n\n/***************************************************************************\n *  Close I2C down\n ***************************************************************************/\nvoid I2CManagerClass::I2C_close() {\n\n  TWI0.MCTRLA &= ~(TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm);        // Switch off I2C\n  TWI0.MSTATUS = TWI_BUSSTATE_UNKNOWN_gc;\n  delayMicroseconds(10);  // Wait for things to stabilise (hopefully)\n}\n\n/***************************************************************************\n *  Main state machine for I2C, called from interrupt handler.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_handleInterrupt() {\n  \n  uint8_t currentStatus = TWI0.MSTATUS;\n\n  if (currentStatus & TWI_ARBLOST_bm) {\n    // Arbitration lost, restart\n    TWI0.MSTATUS = currentStatus; // clear all flags\n    I2C_sendStart();   // Reinitiate request\n  } else if (currentStatus & TWI_BUSERR_bm) {\n    // Bus error\n    completionStatus = I2C_STATUS_BUS_ERROR;\n    state = I2C_STATE_COMPLETED;\n    TWI0.MSTATUS = currentStatus; // clear all flags\n  } else if (currentStatus & TWI_WIF_bm) {\n    // Master write completed\n    if (currentStatus & TWI_RXACK_bm) {\n      // Nacked, send stop.\n      TWI0.MCTRLB = TWI_MCMD_STOP_gc;\n      completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;\n      state = I2C_STATE_COMPLETED;\n\n    } else if (bytesToSend) {\n      // Acked, so send next byte (don't need to use GETFLASH)\n      TWI0.MDATA = sendBuffer[txCount++];\n      bytesToSend--;\n    } else if (bytesToReceive) {\n      // Last sent byte acked and no more to send.  Send repeated start, address and read bit.\n      TWI0.MADDR = (deviceAddress << 1) | 1;\n    } else {\n      // No more data to send/receive. Initiate a STOP condition.\n      TWI0.MCTRLB = TWI_MCMD_STOP_gc;\n      state = I2C_STATE_COMPLETED;\n    }\n  } else if (currentStatus & TWI_RIF_bm) {\n    // Master read completed without errors\n    if (bytesToReceive) {\n      receiveBuffer[rxCount++] = TWI0.MDATA;  // Store received byte\n      bytesToReceive--;\n    } \n    if (bytesToReceive) {\n      // More bytes to receive, issue ack and start another read\n      TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc;\n    } else {\n      // Transaction finished, issue NACK and STOP.\n      TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;\n      state = I2C_STATE_COMPLETED;\n    }\n  }\n}\n\n\n/***************************************************************************\n *  Interrupt handler.\n ***************************************************************************/\nISR(TWI0_TWIM_vect) {\n  I2CManager.handleInterrupt();\n}\n\n#endif\n"
  },
  {
    "path": "I2CManager_NonBlocking.h",
    "content": "/*\n *  © 2023, Neil McKechnie\n *  © 2022 Paul M Antoine\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_NONBLOCKING_H\n#define I2CMANAGER_NONBLOCKING_H\n\n#include <Arduino.h>\n#include \"I2CManager.h\"\n\n// Support for atomic isolation (i.e. a block with interrupts disabled).\n// E.g. \n//       ATOMIC_BLOCK() {\n//         doSomethingWithInterruptsDisabled();\n//       }\n// This has the advantage over simple noInterrupts/Interrupts that the\n// original interrupt state is restored when the block finishes.\n//\n// (This should really be defined in an include file somewhere more global, so \n// it can replace use of noInterrupts/interrupts in other parts of DCC-EX.\n//\nstatic inline uint8_t _deferInterrupts(void) {\n#if defined(ARDUINO_ARCH_STM32)\n  NVIC_DisableIRQ(I2C1_EV_IRQn);\n  NVIC_DisableIRQ(I2C1_ER_IRQn);\n#else\n  noInterrupts();\n#endif\n  return 1;\n}\nstatic inline void _conditionalEnableInterrupts(bool *wasEnabled) {\n#if defined(ARDUINO_ARCH_STM32)\n  (void)wasEnabled;\n  NVIC_EnableIRQ(I2C1_EV_IRQn);\n  NVIC_EnableIRQ(I2C1_ER_IRQn);\n#else\n  if (*wasEnabled) interrupts();\n#endif\n}\n#define ATOMIC_BLOCK(x) \\\nfor (bool _int_saved __attribute__((__cleanup__(_conditionalEnableInterrupts))) \\\n            =_getInterruptState(),_ToDo=_deferInterrupts(); _ToDo; _ToDo=0)\n\n// The  construct of\n// \"variable __attribute__((__cleanup__(func)))\"\n// calls the func with *variable when variable goes out of scope\n\n#if defined(__AVR__) // Nano, Uno, Mega2580, NanoEvery, etc.\n  static inline bool _getInterruptState(void) {\n    return bitRead(SREG, SREG_I);  // true if enabled, false if disabled\n  }\n#elif defined(ARDUINO_ARCH_STM32)\n  static inline bool _getInterruptState( void ) {\n    // as we do ony mess with the I2C interrupts in the STM32 case,\n    // we do not care about their previous state\n    return true;\n  }\n#elif defined(__arm__)  // SAMD, Teensy\n  static inline bool _getInterruptState( void ) {\n    uint32_t reg;\n    __asm__ __volatile__ (\"MRS %0, primask\" : \"=r\" (reg) );\n    return !(reg & 1);  // true if interrupts enabled, false otherwise\n  }\n#else\n  #warning \"ATOMIC_BLOCK() not defined for this target type, I2C interrupts disabled\"\n  #define ATOMIC_BLOCK(x) // expand to nothing.\n  #ifdef I2C_USE_INTERRUPTS\n    #undef I2C_USE_INTERRUPTS\n  #endif\n#endif\n\n\n// This module is only compiled if I2C_USE_WIRE is not defined, so undefine it here\n// to get intellisense to work correctly.\n#if defined(I2C_USE_WIRE)\n#undef I2C_USE_WIRE\n#endif\n\nenum MuxPhase: uint8_t {\n  MuxPhase_OFF = 0,\n  MuxPhase_PROLOG,\n  MuxPhase_PAYLOAD,\n  MuxPhase_EPILOG,\n} ;\n\n\n/***************************************************************************\n * Initialise the I2CManagerAsync class.\n ***************************************************************************/\nvoid I2CManagerClass::_initialise()\n{\n  queueHead = queueTail = NULL;\n  state = I2C_STATE_FREE;\n  I2C_init();\n  _setClock(_clockSpeed);\n}\n\n/***************************************************************************\n *  Set I2C clock speed.  Normally 100000 (Standard) or 400000 (Fast)\n *   on Arduino.  Mega4809 supports 1000000 (Fast+) too.\n *   This function saves the desired clock speed and the startTransaction\n *   function acts on it before a new transaction, to avoid speed changes\n *   during an I2C transaction.\n ***************************************************************************/\nvoid I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {\n  pendingClockSpeed = i2cClockSpeed;\n}\n\n/***************************************************************************\n * Start an I2C transaction, if the I2C interface is free and\n * there is a queued request to be processed.\n * If there's an I2C clock speed change pending, then implement it before \n * starting the operation.\n ***************************************************************************/\nvoid I2CManagerClass::startTransaction() {\n  ATOMIC_BLOCK() {\n    if ((state == I2C_STATE_FREE) && (queueHead != NULL)) {\n      state = I2C_STATE_ACTIVE;\n      completionStatus = I2C_STATUS_OK;\n      // Check for pending clock speed change\n      if (pendingClockSpeed) {\n        // We're about to start a new I2C transaction, so set clock now.\n        I2C_setClock(pendingClockSpeed);\n        pendingClockSpeed = 0;\n      }\n      startTime = micros();\n      currentRequest = queueHead;\n      rxCount = txCount = 0;\n\n      // Start the I2C process going.\n#if defined(I2C_EXTENDED_ADDRESS)\n      I2CMux muxNumber = currentRequest->i2cAddress.muxNumber();\n      if (muxNumber != I2CMux_None) {\n        muxPhase = MuxPhase_PROLOG;\n        uint8_t subBus = currentRequest->i2cAddress.subBus();\n        muxData[0] = (subBus == SubBus_All) ? 0xff :\n                     (subBus == SubBus_None) ? 0x00 :\n#if defined(I2CMUX_PCA9547)\n                      0x08 | subBus;\n#elif defined(I2CMUX_PCA9542) || defined(I2CMUX_PCA9544)\n                      0x04 | subBus;   // NB Only 2 or 4 subbuses respectively\n#else\n                      // Default behaviour for most MUXs is to use a mask\n                      // with a bit set for the subBus to be enabled\n                      1 << subBus;\n#endif\n        deviceAddress = I2C_MUX_BASE_ADDRESS + muxNumber;\n        sendBuffer = &muxData[0];\n        bytesToSend = 1;\n        bytesToReceive = 0;\n        operation = OPERATION_SEND;\n      } else {\n        // Send/receive payload for device only.\n        muxPhase = MuxPhase_OFF;\n        deviceAddress = currentRequest->i2cAddress;\n        sendBuffer = currentRequest->writeBuffer;\n        bytesToSend = currentRequest->writeLen;\n        receiveBuffer = currentRequest->readBuffer;\n        bytesToReceive = currentRequest->readLen;\n        operation = currentRequest->operation & OPERATION_MASK;\n      } \n#else\n      deviceAddress = currentRequest->i2cAddress;\n      sendBuffer = currentRequest->writeBuffer;\n      bytesToSend = currentRequest->writeLen;\n      receiveBuffer = currentRequest->readBuffer;\n      bytesToReceive = currentRequest->readLen;\n      operation = currentRequest->operation & OPERATION_MASK;\n#endif\n      I2C_sendStart();\n    }\n  }\n}\n\n/***************************************************************************\n *  Function to queue a request block and initiate operations.\n ***************************************************************************/\nvoid I2CManagerClass::queueRequest(I2CRB *req) {\n\n  if (((req->operation & OPERATION_MASK) == OPERATION_READ) && req->readLen == 0)\n    return;  // Ignore null read\n\n  req->status = I2C_STATUS_PENDING;\n  req->nextRequest = NULL;\n  ATOMIC_BLOCK() {\n    if (!queueTail) \n      queueHead = queueTail = req;  // Only item on queue\n    else\n      queueTail = queueTail->nextRequest = req; // Add to end\n    startTransaction();\n  }\n\n}\n\n\n/***************************************************************************\n *  Initiate a write to an I2C device (non-blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {\n  // Make sure previous request has completed.\n  req->wait();\n  req->setWriteParams(i2cAddress, writeBuffer, writeLen);\n  queueRequest(req);\n  return I2C_STATUS_OK;\n}\n\n/***************************************************************************\n *  Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {\n  // Make sure previous request has completed.\n  req->wait();\n  req->setWriteParams(i2cAddress, writeBuffer, writeLen);\n  req->operation = OPERATION_SEND_P;\n  queueRequest(req);\n  return I2C_STATUS_OK;\n}\n\n/***************************************************************************\n *  Initiate a read from the I2C device, optionally preceded by a write \n *   (non-blocking operation)\n ***************************************************************************/\nuint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, \n    const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req)\n{\n  // Make sure previous request has completed.\n  req->wait();\n  req->setRequestParams(i2cAddress, readBuffer, readLen, writeBuffer, writeLen);\n  queueRequest(req);\n  return I2C_STATUS_OK;\n}\n\n/***************************************************************************\n *  Set I2C timeout value in microseconds.  The timeout applies to the entire\n *   I2CRB request, e.g. where a write+read is performed, the timer is not\n *   reset before the read.\n ***************************************************************************/\nvoid I2CManagerClass::setTimeout(unsigned long value) { \n  _timeout = value; \n};\n\n/***************************************************************************\n * checkForTimeout() function, called from isBusy() and wait() to cancel\n * requests that are taking too long to complete.  Such faults\n * may be caused by an I2C wire short for example.\n ***************************************************************************/\nvoid I2CManagerClass::checkForTimeout() {\n  ATOMIC_BLOCK() {\n    I2CRB *t = queueHead;\n    if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {\n      // Check for timeout\n      int32_t elapsed = micros() - startTime;\n      if (elapsed > (int32_t)_timeout) { \n#ifdef DIAG_IO\n        //DIAG(F(\"I2CManager Timeout on %s\"), t->i2cAddress.toString());\n#endif\n        // Excessive time. Dequeue request\n        queueHead = t->nextRequest;\n        if (!queueHead) queueTail = NULL;\n        currentRequest = NULL;\n        bytesToReceive = bytesToSend = 0;\n        // Post request as timed out.\n        t->status = I2C_STATUS_TIMEOUT;\n        // Reset TWI interface so it is able to continue\n        // Try close and init, not entirely satisfactory but sort of works...\n        I2C_close();  // Shutdown and restart twi interface\n\n        // If SDA is stuck low, issue up to 9 clock pulses to attempt to free it.\n        pinMode(SCL, INPUT_PULLUP);\n        pinMode(SDA, INPUT_PULLUP);\n        for (int i=0; !digitalRead(SDA) && i<9; i++) {\n          digitalWrite(SCL, 0); \n          pinMode(SCL, OUTPUT);         // Force clock low\n          delayMicroseconds(10);        // ... for 5us\n          pinMode(SCL, INPUT_PULLUP);   // ... then high\n          delayMicroseconds(10);        // ... for 5us (100kHz Clock)\n        }\n        // Whether that's succeeded or not, now try reinitialising.\n        I2C_init();\n        _setClock(_clockSpeed);\n        state = I2C_STATE_FREE;\n        \n        // Initiate next queued request if any.\n        startTransaction();\n      }\n    }\n  }\n}\n\n/***************************************************************************\n *  Loop function, for general background work\n ***************************************************************************/\nvoid I2CManagerClass::loop() {\n#if !defined(I2C_USE_INTERRUPTS)\n  handleInterrupt();\n#endif\n  // Call function to monitor for stuck I2C operations.\n  checkForTimeout();\n}\n\n/***************************************************************************\n * Interupt handler.  Call I2C state machine, and dequeue request\n * if completed.\n ***************************************************************************/\nvoid I2CManagerClass::handleInterrupt() {\n\n  // Update hardware state machine\n  I2C_handleInterrupt();\n\n  // Check if current request has completed.  If there's a current request\n  // and state isn't active then state contains the completion status of the request.\n  if (state == I2C_STATE_COMPLETED && currentRequest != NULL && currentRequest == queueHead) {\n    // Operation has completed.\n    if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES\n      || currentRequest->operation & OPERATION_NORETRY) \n    {\n      // Status is OK, or has failed and retry count exceeded, or failed and retries disabled.\n#if defined(I2C_EXTENDED_ADDRESS)\n      if (muxPhase == MuxPhase_PROLOG ) {\n        overallStatus = completionStatus;\n        uint8_t rbAddress = currentRequest->i2cAddress.deviceAddress();\n        if (completionStatus == I2C_STATUS_OK && rbAddress != 0) {\n          // Mux request OK, start handling application request.\n          muxPhase = MuxPhase_PAYLOAD;\n          deviceAddress = rbAddress;\n          sendBuffer = currentRequest->writeBuffer;\n          bytesToSend = currentRequest->writeLen;\n          receiveBuffer = currentRequest->readBuffer;\n          bytesToReceive = currentRequest->readLen;\n          operation = currentRequest->operation & OPERATION_MASK;\n          state = I2C_STATE_ACTIVE;\n          I2C_sendStart();\n          return;\n        } \n      } else if (muxPhase == MuxPhase_PAYLOAD) {\n        // Application request completed, now send epilogue to mux\n        overallStatus = completionStatus;\n        currentRequest->nBytes = rxCount;  // Save number of bytes read into rb\n        if (_muxCount == 1) {\n          // Only one MUX, don't need to deselect subbus\n          muxPhase = MuxPhase_OFF;\n        } else {\n          muxPhase = MuxPhase_EPILOG;\n          deviceAddress = I2C_MUX_BASE_ADDRESS + currentRequest->i2cAddress.muxNumber();\n          muxData[0] = 0x00;\n          sendBuffer = &muxData[0];\n          bytesToSend = 1;\n          bytesToReceive = 0;\n          operation = OPERATION_SEND;\n          state = I2C_STATE_ACTIVE;\n          I2C_sendStart();\n          return;\n        }\n      } else if (muxPhase == MuxPhase_EPILOG) {\n        // Epilog finished, ignore completionStatus\n        muxPhase = MuxPhase_OFF;\n      } else\n        overallStatus = completionStatus;\n#else\n      overallStatus = completionStatus;\n      currentRequest->nBytes = rxCount;\n#endif\n          \n      // Remove completed request from head of queue\n      I2CRB * t = queueHead;\n      if (t == currentRequest) {\n        queueHead = t->nextRequest;\n        if (!queueHead) queueTail = queueHead;\n        t->status = overallStatus;\n        \n        // I2C state machine is now free for next request\n        currentRequest = NULL;\n        state = I2C_STATE_FREE;\n      }\n      retryCounter = 0;\n    } else {\n      // Status is failed and retry permitted.\n      // Retry previous request.\n      state = I2C_STATE_FREE;  \n    }\n  }\n\n  if (state == I2C_STATE_FREE && queueHead != NULL) {\n    // Allow any pending interrupts before starting the next request.\n    //interrupts();\n    // Start next request\n    I2CManager.startTransaction();\n  }\n}\n\n#endif\n"
  },
  {
    "path": "I2CManager_SAMD.h",
    "content": "/*\n *  © 2022 Paul M Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_SAMD_H\n#define I2CMANAGER_SAMD_H\n\n#include <Arduino.h>\n#include \"I2CManager.h\"\n\n//#include <avr/io.h>\n//#include <avr/interrupt.h>\n#include <wiring_private.h>\n\n/***************************************************************************\n *  Interrupt handler.\n *  IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero\n *  compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.\n *  Later we may wish to allow use of an alternate I2C bus, or more than one I2C\n *  bus on the SAMD architecture \n ***************************************************************************/\n#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_SAMD_ZERO)\nvoid SERCOM3_Handler() {\n  I2CManager.handleInterrupt();\n}\n#endif\n\n// Assume SERCOM3 for now - default I2C bus on Arduino Zero and variants of same\nSercom *s = SERCOM3;\n\n/***************************************************************************\n *  Set I2C clock speed register.  This should only be called outside of\n *  a transmission.  The I2CManagerClass::_setClock() function ensures \n *  that it is only called at the beginning of an I2C transaction.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {\n\n  // Calculate a rise time appropriate to the requested bus speed\n  int t_rise;\n  if (i2cClockSpeed < 200000L) {\n    i2cClockSpeed = 100000L;    // NB: this overrides a \"force clock\" of lower than 100KHz!\n    t_rise = 1000;\n  } else if (i2cClockSpeed < 800000L) {\n    i2cClockSpeed = 400000L;\n    t_rise = 300;\n  } else if (i2cClockSpeed < 1200000L) {\n    i2cClockSpeed = 1000000L;\n    t_rise = 120;\n  } else {\n    i2cClockSpeed = 100000L;\n    t_rise = 1000;\n  }\n\n  // Wait while the bus is busy\n  while (s->I2CM.STATUS.bit.BUSSTATE != 0x1);\n\n  // Disable the I2C master mode and wait for sync\n  s->I2CM.CTRLA.bit.ENABLE = 0 ;\n  while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);\n\n  // Calculate baudrate - using a rise time appropriate for the speed\n  s->I2CM.BAUD.bit.BAUD = SystemCoreClock / (2 * i2cClockSpeed) - 5 - (((SystemCoreClock / 1000000) * t_rise) / (2 * 1000));\n\n  // Enable the I2C master mode and wait for sync\n  s->I2CM.CTRLA.bit.ENABLE = 1 ;\n  while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);\n\n  // Setting bus idle mode and wait for sync\n  s->I2CM.STATUS.bit.BUSSTATE = 1 ;\n  while (s->I2CM.SYNCBUSY.bit.SYSOP != 0);\n}\n\n/***************************************************************************\n *  Initialise I2C registers.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_init()\n{\n  //Setting clock\n  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID(GCM_SERCOM3_CORE) | // Generic Clock 0 (SERCOM3)\n                      GCLK_CLKCTRL_GEN_GCLK0 | // Generic Clock Generator 0 is source\n                      GCLK_CLKCTRL_CLKEN ;\n\n  /* Wait for peripheral clock synchronization */\n  while ( GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY );\n\n  // Software reset the SERCOM\n  s->I2CM.CTRLA.bit.SWRST = 1;\n\n  //Wait both bits Software Reset from CTRLA and SYNCBUSY are equal to 0\n  while(s->I2CM.CTRLA.bit.SWRST || s->I2CM.SYNCBUSY.bit.SWRST);\n\n  // Set master mode and enable SCL Clock Stretch mode (stretch after ACK bit)\n  s->I2CM.CTRLA.reg =  SERCOM_I2CM_CTRLA_MODE( I2C_MASTER_OPERATION )/* |\n                            SERCOM_I2CM_CTRLA_SCLSM*/ ;\n\n  // Enable Smart mode (but not Quick Command)\n  s->I2CM.CTRLB.reg =  SERCOM_I2CM_CTRLB_SMEN;\n\n#if defined(I2C_USE_INTERRUPTS)\n  // Setting NVIC\n  NVIC_EnableIRQ(SERCOM3_IRQn);\n  NVIC_SetPriority (SERCOM3_IRQn, SERCOM_NVIC_PRIORITY);  // Match default SERCOM priorities\n//  NVIC_SetPriority (SERCOM3_IRQn, 0);  // Set highest priority\n\n  // Enable all interrupts\n  s->I2CM.INTENSET.reg = SERCOM_I2CM_INTENSET_MB | SERCOM_I2CM_INTENSET_SB | SERCOM_I2CM_INTENSET_ERROR;\n#endif\n\n  // Calculate baudrate and set default rate for now\n  s->I2CM.BAUD.bit.BAUD = SystemCoreClock / ( 2 * I2C_FREQ) - 7 / (2 * 1000);\n\n  // Enable the I2C master mode and wait for sync\n  s->I2CM.CTRLA.bit.ENABLE = 1 ;\n  while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);\n\n  // Setting bus idle mode and wait for sync\n  s->I2CM.STATUS.bit.BUSSTATE = 1 ;\n  while (s->I2CM.SYNCBUSY.bit.SYSOP != 0);\n\n  // Set SDA/SCL pins as outputs and enable pullups, at present we assume these are\n  // the default ones for SERCOM3 (see assumption above)\n  pinPeripheral(PIN_WIRE_SDA, g_APinDescription[PIN_WIRE_SDA].ulPinType);\n  pinPeripheral(PIN_WIRE_SCL, g_APinDescription[PIN_WIRE_SCL].ulPinType);\n\n  // Enable the SCL and SDA pins on the sercom: includes increased driver strength,\n  // pull-up resistors and pin multiplexer\n\tPORT->Group[g_APinDescription[PIN_WIRE_SCL].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SCL].ulPin].reg =  \n\t\tPORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN;  \n  PORT->Group[g_APinDescription[PIN_WIRE_SDA].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SDA].ulPin].reg = \n\t  PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN;\n}\n\n/***************************************************************************\n *  Initiate a start bit for transmission.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStart() {\n\n  // Set counters here in case this is a retry.\n  txCount = 0;\n  rxCount = 0;\n\n  // On a single-master I2C bus, the start bit won't be sent until the bus \n  // state goes to IDLE so we can request it without waiting.  On a \n  // multi-master bus, the bus may be BUSY under control of another master, \n  // in which case we can avoid some arbitration failures by waiting until\n  // the bus state is IDLE.  We don't do that here.\n\n  // If anything to send, initiate write.  Otherwise initiate read.\n  if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))\n  {\n    // Send start and address with read flag (1) or'd in\n    s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;\n  }\n  else {\n    // Send start and address with write flag (0) or'd in\n    s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1ul) | 0;\n  }\n}\n\n/***************************************************************************\n *  Initiate a stop bit for transmission (does not interrupt)\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStop() {\n  s->I2CM.CTRLB.bit.CMD = 3; // Stop condition\n}\n\n/***************************************************************************\n *  Close I2C down\n ***************************************************************************/\nvoid I2CManagerClass::I2C_close() {\n  I2C_sendStop();\n  // Disable the I2C master mode and wait for sync\n  s->I2CM.CTRLA.bit.ENABLE = 0 ;\n  // Wait for up to 500us only.\n  unsigned long startTime = micros();\n  while (s->I2CM.SYNCBUSY.bit.ENABLE != 0) {\n    if (micros() - startTime >= 500UL) break;\n  }\n}\n\n/***************************************************************************\n *  Main state machine for I2C, called from interrupt handler or,\n *  if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function\n *  (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).\n ***************************************************************************/\nvoid I2CManagerClass::I2C_handleInterrupt() {\n\n  if (s->I2CM.STATUS.bit.ARBLOST) {\n    // Arbitration lost, restart\n    I2C_sendStart();   // Reinitiate request\n  } else if (s->I2CM.STATUS.bit.BUSERR) {\n    // Bus error\n    completionStatus = I2C_STATUS_BUS_ERROR;\n    state = I2C_STATE_COMPLETED;  // Completed with error\n  } else if (s->I2CM.INTFLAG.bit.MB) {\n    // Master write completed\n    if (s->I2CM.STATUS.bit.RXNACK) {\n      // Nacked, send stop.\n      I2C_sendStop();\n      completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;\n      state = I2C_STATE_COMPLETED;  // Completed with error\n    } else if (bytesToSend) {\n      // Acked, so send next byte\n      s->I2CM.DATA.bit.DATA = sendBuffer[txCount++];\n      bytesToSend--;\n    } else if (bytesToReceive) {\n      // Last sent byte acked and no more to send.  Send repeated start, address and read bit.\n      s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;\n    } else {\n      // No more data to send/receive. Initiate a STOP condition\n      I2C_sendStop();\n      state = I2C_STATE_COMPLETED;  // Completed OK\n    }\n  } else if (s->I2CM.INTFLAG.bit.SB) {\n    // Master read completed without errors\n    if (bytesToReceive == 1) {\n      s->I2CM.CTRLB.bit.ACKACT = 1;  // NAK final byte\n      I2C_sendStop();  // send stop\n      receiveBuffer[rxCount++] = s->I2CM.DATA.bit.DATA;  // Store received byte\n      bytesToReceive = 0;\n      state = I2C_STATE_COMPLETED;  // Completed OK\n    } else if (bytesToReceive) {\n      s->I2CM.CTRLB.bit.ACKACT = 0;  // ACK all but final byte\n      receiveBuffer[rxCount++] = s->I2CM.DATA.bit.DATA;  // Store received byte\n      bytesToReceive--;\n    }\n  }\n}\n\n#endif /* I2CMANAGER_SAMD_H */\n"
  },
  {
    "path": "I2CManager_STM32.h",
    "content": "/*\n *  © 2022-24 Paul M Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_STM32_H\n#define I2CMANAGER_STM32_H\n\n#include <Arduino.h>\n#include \"I2CManager.h\"\n#include \"I2CManager_NonBlocking.h\"   // to satisfy intellisense\n\n#include <wiring_private.h>\n#include \"stm32f4xx_hal_rcc.h\"\n\n/*****************************************************************************\n *  STM32F4xx I2C native driver support\n * \n *  Nucleo-64 and Nucleo-144 boards all use I2C1 as the default I2C peripheral\n *  Later we may wish to support other STM32 boards, allow use of an alternate\n *  I2C bus, or more than one I2C bus on the STM32 architecture \n *****************************************************************************/\n#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)\n#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \\\n    || defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) \\\n    || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)\n\n// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely all Nucleo-64\n// and Nucleo-144 variants\nI2C_TypeDef *s = I2C1;\n\n// In init we will ask the STM32 HAL layer for the configured APB1 clock frequency in Hz\nuint32_t APB1clk1; // Peripheral Input Clock speed in Hz.\nuint32_t i2c_MHz;  // Peripheral Input Clock speed in MHz.\n\n// IRQ handler for I2C1, replacing the weak definition in the STM32 HAL \nextern \"C\" void I2C1_EV_IRQHandler(void) {\n  I2CManager.handleInterrupt();\n}\nextern \"C\" void I2C1_ER_IRQHandler(void) {\n  I2CManager.handleInterrupt();\n}\n#else\n#warning STM32 board selected is not yet supported - so I2C1 peripheral is not defined\n#endif\n#endif\n\n// Peripheral Input Clock speed in MHz.\n// For STM32F446RE, the speed is 45MHz.  Ideally, this should be determined\n// at run-time from the APB1 clock, as it can vary from STM32 family to family.\n// #define I2C_PERIPH_CLK 45\n\n// I2C SR1 Status Register #1 bit definitions for convenience\n// #define I2C_SR1_SMBALERT  (1<<15)   // SMBus alert\n// #define I2C_SR1_TIMEOUT   (1<<14)   // Timeout of Tlow error\n// #define I2C_SR1_PECERR    (1<<12)   // PEC error in reception\n// #define I2C_SR1_OVR       (1<<11)   // Overrun/Underrun error\n// #define I2C_SR1_AF        (1<<10)   // Acknowledge failure\n// #define I2C_SR1_ARLO      (1<<9)    // Arbitration lost (master mode)\n// #define I2C_SR1_BERR      (1<<8)    // Bus error (misplaced start or stop condition)\n// #define I2C_SR1_TxE       (1<<7)    // Data register empty on transmit\n// #define I2C_SR1_RxNE      (1<<6)    // Data register not empty on receive\n// #define I2C_SR1_STOPF     (1<<4)    // Stop detection (slave mode)\n// #define I2C_SR1_ADD10     (1<<3)    // 10 bit header sent\n// #define I2C_SR1_BTF       (1<<2)    // Byte transfer finished - data transfer done\n// #define I2C_SR1_ADDR      (1<<1)    // Address sent (master) or matched (slave)\n// #define I2C_SR1_SB        (1<<0)    // Start bit (master mode) 1=start condition generated\n\n// I2C CR1 Control Register #1 bit definitions for convenience\n// #define I2C_CR1_SWRST     (1<<15)   // Software reset - places peripheral under reset\n// #define I2C_CR1_ALERT     (1<<13)   // SMBus alert assertion\n// #define I2C_CR1_PEC       (1<<12)   // Packet Error Checking transfer in progress\n// #define I2C_CR1_POS       (1<<11)   // Acknowledge/PEC Postion (for data reception in PEC mode)\n// #define I2C_CR1_ACK       (1<<10)   // Acknowledge enable - ACK returned after byte is received (address or data)\n// #define I2C_CR1_STOP      (1<<9)    // STOP generated\n// #define I2C_CR1_START     (1<<8)    // START generated\n// #define I2C_CR1_NOSTRETCH (1<<7)    // Clock stretching disable (slave mode)\n// #define I2C_CR1_ENGC      (1<<6)    // General call (broadcast) enable (address 00h is ACKed)\n// #define I2C_CR1_ENPEC     (1<<5)    // PEC Enable\n// #define I2C_CR1_ENARP     (1<<4)    // ARP enable (SMBus)\n// #define I2C_CR1_SMBTYPE   (1<<3)    // SMBus type, 1=host, 0=device\n// #define I2C_CR1_SMBUS     (1<<1)    // SMBus mode, 1=SMBus, 0=I2C\n// #define I2C_CR1_PE        (1<<0)    // I2C Peripheral enable\n\n// States of the STM32 I2C driver state machine\nenum {TS_IDLE,TS_START,TS_W_ADDR,TS_W_DATA,TS_W_STOP,TS_R_ADDR,TS_R_DATA,TS_R_STOP};\n\n\n/***************************************************************************\n *  Set I2C clock speed register.  This should only be called outside of\n *  a transmission.  The I2CManagerClass::_setClock() function ensures \n *  that it is only called at the beginning of an I2C transaction.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {\n  // Calculate a rise time appropriate to the requested bus speed\n  // Use 10x the rise time spec to enable integer divide of 50ns clock period\n  uint16_t t_rise;\n\n  while (s->CR1 & I2C_CR1_STOP);  // Prevents lockup by guarding further\n                                  // writes to CR1 while STOP is being executed!\n\n  // Disable the I2C device, as TRISE can only be programmed whilst disabled\n  s->CR1 &= ~(I2C_CR1_PE);  // Disable I2C\n  s->CR1 |= I2C_CR1_SWRST;  // reset the I2C\n  asm(\"nop\");    // wait a bit... suggestion from online!\n  s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation\n\n  if (i2cClockSpeed > 100000UL)\n  {\n    // if (i2cClockSpeed > 400000L)\n    //   i2cClockSpeed = 400000L;\n\n    t_rise = 300;  // nanoseconds\n  }\n  else\n  {\n    // i2cClockSpeed = 100000L;\n    t_rise = 1000;  // nanoseconds\n  }\n  // Configure the rise time register - max allowed tRISE is 1000ns,\n  // so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.\n  s->TRISE = (t_rise * i2c_MHz / 1000) + 1;\n\n  // Bit 15: I2C Master mode, 0=standard, 1=Fast Mode\n  // Bit 14: Duty, fast mode duty cycle (use 2:1)\n  // Bit 11-0: FREQR\n  // if (i2cClockSpeed > 400000UL) {\n  //   // In fast mode plus, I2C period is 3 * CCR * TPCLK1.\n  //   // s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value\n  //   s->CCR = APB1clk1 / 3 / i2cClockSpeed; // Set I2C clockspeed to start!\n  //   s->CCR |= 0xC000; // We need Fast Mode AND DUTY bits set\n  // } else {\n    // In standard and fast mode, I2C period is 2 * CCR * TPCLK1\n    s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value\n    s->CCR |= (APB1clk1 / 2 / i2cClockSpeed); // Set I2C clockspeed to start!\n    // s->CCR |= (i2c_MHz * 500 / (i2cClockSpeed / 1000)); // Set I2C clockspeed to start!\n    // if (i2cClockSpeed > 100000UL)\n    //     s->CCR |= 0xC000; // We need Fast Mode bits set as well\n  // }\n\n  // DIAG(F(\"I2C_init() peripheral clock is now: %d, full reg is %x\"), (s->CR2 & 0xFF), s->CR2);\n  // DIAG(F(\"I2C_init() peripheral CCR is now: %d\"), s->CCR);\n  // DIAG(F(\"I2C_init() peripheral TRISE is now: %d\"), s->TRISE);\n\n  // Enable the I2C master mode\n  s->CR1 |= I2C_CR1_PE;  // Enable I2C\n}\n\n/***************************************************************************\n *  Initialise I2C registers.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_init()\n{\n  // Query the clockspeed from the STM32 HAL layer\n  APB1clk1 = HAL_RCC_GetPCLK1Freq();\n  i2c_MHz = APB1clk1 / 1000000UL;\n  // DIAG(F(\"I2C_init() peripheral clock speed is: %d\"), i2c_MHz);\n  // Enable clocks\n  RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;//(1 << 21); // Enable I2C CLOCK\n  // Reset the I2C1 peripheral to initial state\n  RCC->APB1RSTR |=  RCC_APB1RSTR_I2C1RST;\n\tRCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST;\n  // Standard I2C pins are SCL on PB8 and SDA on PB9\n  RCC->AHB1ENR |= (1<<1);   // Enable GPIOB CLOCK for PB8/PB9\n  // Bits (17:16)= 1:0 --> Alternate Function for Pin PB8;\n  // Bits (19:18)= 1:0 --> Alternate Function for Pin PB9\n  GPIOB->MODER &= ~((3<<(8*2)) | (3<<(9*2)));    // Clear all MODER bits for PB8 and PB9\n  GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2));    // PB8 and PB9 set to ALT function\n  GPIOB->OTYPER |= (1<<8) | (1<<9);           // PB8 and PB9 set to open drain output capability\n  GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2));  // PB8 and PB9 set to High Speed mode\n  GPIOB->PUPDR &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all PUPDR bits for PB8 and PB9\n  // GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2));    // PB8 and PB9 set to pull-up capability\n  // Alt Function High register routing pins PB8 and PB9 for I2C1:\n  // Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8\n  // Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9\n  GPIOB->AFR[1] &= ~((15<<0) | (15<<4));      // Clear all AFR bits for PB8 on low nibble, PB9 on next nibble up\n  GPIOB->AFR[1] |= (4<<0) | (4<<4);           // PB8 on low nibble, PB9 on next nibble up\n\n  // Software reset the I2C peripheral\n  I2C1->CR1 &= ~I2C_CR1_PE; // Disable I2C1 peripheral\n  s->CR1 |= I2C_CR1_SWRST;  // reset the I2C\n  asm(\"nop\");    // wait a bit... suggestion from online!\n  s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation\n\n  // Clear all bits in I2C CR2 register except reserved bits\n  s->CR2 &= 0xE000;\n\n  // Set I2C peripheral clock frequency\n  // s->CR2 |= I2C_PERIPH_CLK;\n  s->CR2 |= i2c_MHz;\n  // DIAG(F(\"I2C_init() peripheral clock is now: %d\"), s->CR2);\n\n  // set own address to 00 - not used in master mode\n  I2C1->OAR1 = (1 << 14); // bit 14 should be kept at 1 according to the datasheet\n\n#if defined(I2C_USE_INTERRUPTS)\n  // Setting NVIC\n  NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);   // 4 means that we have all bits for preemptive grouping\n  // prio scheme:\n  // systick        : 0\n  // waveform timer : 1\n  // i2c            : 2\n  // one must call NVIC_EncodePriority() to bitshift the priorities\n  // according to the active priority grouping and then use that\n  // value as argument to NVIC_SetPriority().\n  NVIC_SetPriority(I2C1_EV_IRQn,\n\t\t   NVIC_EncodePriority(NVIC_GetPriorityGrouping(),  2, 0));\n  NVIC_EnableIRQ(I2C1_EV_IRQn);\n  NVIC_SetPriority(I2C1_ER_IRQn,\n\t\t   NVIC_EncodePriority(NVIC_GetPriorityGrouping(),  2, 0));\n  NVIC_EnableIRQ(I2C1_ER_IRQn);\n\n  // CR2 Interrupt Settings\n  // Bit 15-13: reserved\n  // Bit 12: LAST - DMA last transfer\n  // Bit 11: DMAEN - DMA enable\n  // Bit 10: ITBUFEN - Buffer interrupt enable\n  // Bit 9: ITEVTEN - Event interrupt enable\n  // Bit 8: ITERREN - Error interrupt enable\n  // Bit 7-6: reserved\n  // Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz)\n  s->CR2 |= (I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | I2C_CR2_ITERREN);   // Enable Buffer, Event and Error interrupts\n#endif\n\n  // DIAG(F(\"I2C_init() setting initial I2C clock to 100KHz\"));\n  // Calculate baudrate and set default rate for now\n  // Configure the Clock Control Register for 100KHz SCL frequency\n  // Bit 15: I2C Master mode, 0=standard, 1=Fast Mode\n  // Bit 14: Duty, fast mode duty cycle\n  // Bit 11-0: so CCR divisor would be clk / 2 / 100000 (where clk is in Hz)\n  // s->CCR = I2C_PERIPH_CLK * 5;\n  s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value\n  s->CCR |= (APB1clk1 / 2 / 100000UL); // Set a default of 100KHz I2C clockspeed to start!\n\n  // Configure the rise time register - max allowed is 1000ns, so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.\n  s->TRISE = (1000 * i2c_MHz / 1000) + 1;\n\n  // DIAG(F(\"I2C_init() peripheral clock is now: %d, full reg is %x\"), (s->CR2 & 0xFF), s->CR2);\n  // DIAG(F(\"I2C_init() peripheral CCR is now: %d\"), s->CCR);\n  // DIAG(F(\"I2C_init() peripheral TRISE is now: %d\"), s->TRISE);\n\n  // Enable the I2C master mode\n  s->CR1 |= I2C_CR1_PE;  // Enable I2C\n}\n\n/***************************************************************************\n *  Initiate a start bit for transmission.\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStart() {\n\n  // Set counters here in case this is a retry.\n  rxCount = txCount = 0;\n\n  // On a single-master I2C bus, the start bit won't be sent until the bus\n  // state goes to IDLE so we can request it without waiting.  On a\n  // multi-master bus, the bus may be BUSY under control of another master,\n  // in which case we can avoid some arbitration failures by waiting until\n  // the bus state is IDLE.  We don't do that here.\n  //while (s->SR2 & I2C_SR2_BUSY) {}\n\n  // Check there's no STOP still in progress.  If we OR the START bit into CR1\n  // and the STOP bit is already set, we could output multiple STOP conditions.\n  while (s->CR1 & I2C_CR1_STOP) {}  // Wait for STOP bit to reset\n\n  s->CR2 |= (I2C_CR2_ITEVTEN | I2C_CR2_ITERREN);  // Enable interrupts\n  s->CR2 &= ~I2C_CR2_ITBUFEN;     // Don't enable buffer interupts yet.\n  s->CR1 &= ~I2C_CR1_POS;   // Clear the POS bit\n  s->CR1 |= (I2C_CR1_ACK | I2C_CR1_START);   // Enable the ACK and generate START\n  transactionState = TS_START;\n}\n\n/***************************************************************************\n *  Initiate a stop bit for transmission (does not interrupt)\n ***************************************************************************/\nvoid I2CManagerClass::I2C_sendStop() {\n  s->CR1 |= I2C_CR1_STOP; // Stop I2C\n}\n\n/***************************************************************************\n *  Close I2C down\n ***************************************************************************/\nvoid I2CManagerClass::I2C_close() {\n  I2C_sendStop();\n  // Disable the I2C master mode and wait for sync\n  s->CR1 &= ~I2C_CR1_PE;  // Disable I2C peripheral\n  // Should never happen, but wait for up to 500us only.\n  unsigned long startTime = micros();\n  while ((s->CR1 & I2C_CR1_PE) != 0) {\n    if ((int32_t)(micros() - startTime) >= 500) break;\n  }\n  NVIC_DisableIRQ(I2C1_EV_IRQn);\n  NVIC_DisableIRQ(I2C1_ER_IRQn);\n}\n\n/***************************************************************************\n *  Main state machine for I2C, called from interrupt handler or,\n *  if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function\n *  (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).\n ***************************************************************************/\nvoid I2CManagerClass::I2C_handleInterrupt() {\n  volatile uint16_t temp_sr1, temp_sr2;\n  (void) temp_sr2; // only used as target for reads\n\n  temp_sr1 = s->SR1;\n\n  // Check for errors first\n  if (temp_sr1 & (I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR)) {\n    // Check which error flag is set\n    if (temp_sr1 & I2C_SR1_AF)\n    {\n      s->SR1 &= ~(I2C_SR1_AF); // Clear AF\n      I2C_sendStop(); // Clear the bus\n      transactionState = TS_IDLE;\n      completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;\n      state = I2C_STATE_COMPLETED;\n    }\n    else if (temp_sr1 & I2C_SR1_ARLO)\n    {\n      // Arbitration lost, restart\n      s->SR1 &= ~(I2C_SR1_ARLO); // Clear ARLO\n      I2C_sendStart(); // Reinitiate request\n      transactionState = TS_START;\n    }\n    else if (temp_sr1 & I2C_SR1_BERR)\n    {\n      // Bus error\n      s->SR1 &= ~(I2C_SR1_BERR); // Clear BERR\n      I2C_sendStop(); // Clear the bus\n      transactionState = TS_IDLE;\n      completionStatus = I2C_STATUS_BUS_ERROR;\n      state = I2C_STATE_COMPLETED;\n    }\n  } \n  else {\n    // No error flags, so process event according to current state.\n    switch (transactionState) {\n      case TS_START:\n        if (temp_sr1 & I2C_SR1_SB) {\n          // Event EV5\n          // Start bit has been sent successfully and we have the bus.\n          // If anything to send, initiate write.  Otherwise initiate read.\n          if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend)) {\n            // Send address with read flag (1) or'd in\n            s->DR = (deviceAddress << 1) | 1;  //  send the address\n            transactionState = TS_R_ADDR;\n          } else {\n            // Send address with write flag (0) or'd in\n            s->DR = (deviceAddress << 1) | 0;  //  send the address\n            transactionState = TS_W_ADDR;\n          }\n        }\n        // SB bit is cleared by writing to DR (already done).\n        break;\n\n      case TS_W_ADDR:\n        if (temp_sr1 & I2C_SR1_ADDR) {\n          temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit\n          // Event EV6\n          // Address sent successfully, device has ack'd in response.\n          if (!bytesToSend) {\n            I2C_sendStop();\n            transactionState = TS_IDLE;\n            completionStatus = I2C_STATUS_OK;\n            state = I2C_STATE_COMPLETED;\n          } else {\n            // Put one byte into DR to load shift register.\n            s->DR = sendBuffer[txCount++];\n            bytesToSend--;\n            if (bytesToSend) {\n              // Put another byte to load DR\n              s->DR = sendBuffer[txCount++];\n              bytesToSend--;\n            }\n            if (!bytesToSend) {\n              // No more bytes to send.\n              // The TXE interrupt occurs when the DR is empty, and the BTF interrupt \n              // occurs when the shift register is also empty (one character later).\n              // To avoid repeated TXE interrupts during this time, we disable TXE interrupt.\n              s->CR2 &= ~I2C_CR2_ITBUFEN;  // Wait for BTF interrupt, disable TXE interrupt\n              transactionState = TS_W_STOP;\n            } else {\n              // More data remaining to send after this interrupt, enable TXE interrupt.\n              s->CR2 |= I2C_CR2_ITBUFEN;\n              transactionState = TS_W_DATA;\n            }\n          }\n        }\n        break;\n\n      case TS_W_DATA:\n        if (temp_sr1 & I2C_SR1_TXE) {\n          // Event EV8_1/EV8\n          // Transmitter empty, write a byte to it.\n          if (bytesToSend) {\n            s->DR = sendBuffer[txCount++];\n            bytesToSend--;\n            if (!bytesToSend) {\n              s->CR2 &= ~I2C_CR2_ITBUFEN;  // Disable TXE interrupt\n              transactionState = TS_W_STOP;\n            }\n          }\n        } \n        break;\n\n      case TS_W_STOP:\n        if (temp_sr1 & I2C_SR1_BTF) {\n          // Event EV8_2\n          // Done, last character sent. Anything to receive?\n          if (bytesToReceive) {\n            I2C_sendStart();\n            // NOTE: Three redundant BTF interrupts take place between the\n            // first BTF interrupt and the START interrupt.  I've tried all sorts\n            // of ways to eliminate them, and the only thing that worked for\n            // me was to loop until the BTF bit becomes reset.  Either way,\n            // it's a waste of processor time.  Anyone got a solution?\n            //while (s->SR1 && I2C_SR1_BTF) {}\n            transactionState = TS_START;\n          } else {\n            I2C_sendStop();\n            transactionState = TS_IDLE;\n            completionStatus = I2C_STATUS_OK;\n            state = I2C_STATE_COMPLETED;\n          }\n          s->SR1 &= I2C_SR1_BTF;  // Clear BTF interrupt\n        }\n        break;\n\n      case TS_R_ADDR:\n        if (temp_sr1 & I2C_SR1_ADDR) {\n          // Event EV6\n          // Address sent for receive.\n          // The next bit is different depending on whether there are \n          // 1 byte, 2 bytes or >2 bytes to be received, in accordance with the\n          // Programmers Reference RM0390.\n          if (bytesToReceive == 1) {\n            // Receive 1 byte\n            s->CR1 &= ~I2C_CR1_ACK;  // Disable ack\n            temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit\n            // Next step will occur after a RXNE interrupt, so enable it\n            s->CR2 |= I2C_CR2_ITBUFEN;\n            transactionState = TS_R_STOP;\n          } else if (bytesToReceive == 2) {\n            // Receive 2 bytes\n            s->CR1 &= ~I2C_CR1_ACK;  // Disable ACK for final byte\n            s->CR1 |= I2C_CR1_POS;  // set POS flag to delay effect of ACK flag\n            // Next step will occur after a BTF interrupt, so disable RXNE interrupt\n            s->CR2 &= ~I2C_CR2_ITBUFEN;\n            temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit\n            transactionState = TS_R_STOP;\n          } else {\n            // >2 bytes, just wait for bytes to come in and ack them for the time being\n            // (ack flag has already been set).\n            // Next step will occur after a BTF interrupt, so disable RXNE interrupt\n            s->CR2 &= ~I2C_CR2_ITBUFEN;\n            temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit\n            transactionState = TS_R_DATA;\n          }\n        }\n        break;\n      \n      case TS_R_DATA:\n        // Event EV7/EV7_1\n        if (temp_sr1 & I2C_SR1_BTF) {\n          // Byte received in receiver - read next byte\n          if (bytesToReceive == 3) {\n            // Getting close to the last byte, so a specific sequence is recommended.\n            s->CR1 &= ~I2C_CR1_ACK;  // Reset ack for next byte received.\n            transactionState = TS_R_STOP;\n          }\n          receiveBuffer[rxCount++] = s->DR;  // Store received byte\n          bytesToReceive--;\n        } \n        break;\n        \n      case TS_R_STOP:\n        if (temp_sr1 & I2C_SR1_BTF) {\n          // Event EV7 (last one)\n          // When we've got here, the receiver has got the last two bytes\n          // (or one byte, if only one byte is being received),\n          // and NAK has already been sent, so we need to read from the receiver.\n          if (bytesToReceive) {\n            if (bytesToReceive > 1) \n              I2C_sendStop();\n            while(bytesToReceive) {\n              receiveBuffer[rxCount++] = s->DR;  // Store received byte(s)\n              bytesToReceive--;\n            }\n            // Finish.\n            transactionState = TS_IDLE;\n            completionStatus = I2C_STATUS_OK;\n            state = I2C_STATE_COMPLETED;\n          }\n        } else if (temp_sr1 & I2C_SR1_RXNE) {\n          if (bytesToReceive == 1) {\n            // One byte on a single-byte transfer.  Ack has already been set.\n            I2C_sendStop();\n            receiveBuffer[rxCount++] = s->DR; // Store received byte\n            bytesToReceive--;\n            // Finish.\n            transactionState = TS_IDLE;\n            completionStatus = I2C_STATUS_OK;\n            state = I2C_STATE_COMPLETED;\n          } else\n            s->SR1 &= I2C_SR1_RXNE;  // Acknowledge interrupt\n        }\n        break;\n    }\n    // If we've received an interrupt at any other time, we're not interested so clear it\n    // to prevent it recurring ad infinitum.\n    s->SR1 = 0;\n  }\n  \n}\n\n#endif /* I2CMANAGER_STM32_H */\n"
  },
  {
    "path": "I2CManager_Wire.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef I2CMANAGER_WIRE_H\n#define I2CMANAGER_WIRE_H\n\n#include <Arduino.h>\n#include <Wire.h>\n#include \"I2CManager.h\"\n\n// This module is only compiled if I2C_USE_WIRE is defined, so define it here\n// to get intellisense to work correctly.\n#if !defined(I2C_USE_WIRE)\n#define I2C_USE_WIRE\n#endif\n\n// Older versions of Wire don't have setWireTimeout function.  AVR does.\n#ifdef ARDUINO_ARCH_AVR\n#define WIRE_HAS_TIMEOUT\n#endif\n\n/***************************************************************************\n *  Initialise I2C interface software\n ***************************************************************************/\nvoid I2CManagerClass::_initialise() {\n  Wire.begin();\n#if defined(WIRE_HAS_TIMEOUT) \n  Wire.setWireTimeout(_timeout, true);\n#endif\n}\n\n/***************************************************************************\n *  Set I2C clock speed.  Normally 100000 (Standard) or 400000 (Fast)\n *   on Arduino.  Mega4809 supports 1000000 (Fast+) too.\n ***************************************************************************/\nvoid I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {\n  Wire.setClock(i2cClockSpeed);\n}\n\n/***************************************************************************\n *  Set I2C timeout value in microseconds.  The timeout applies to each\n *   Wire call separately, i.e. in a write+read, the timer is reset before the\n *   read is started.\n ***************************************************************************/\nvoid I2CManagerClass::setTimeout(unsigned long value) {\n  _timeout = value;\n#if defined(WIRE_HAS_TIMEOUT) \n  Wire.setWireTimeout(value, true);\n#endif\n}\n\n/********************************************************\n * Helper function for I2C Multiplexer operations\n ********************************************************/\n#ifdef I2C_EXTENDED_ADDRESS\nstatic uint8_t muxSelect(I2CAddress address) {\n  // Select MUX sub bus.\n  I2CMux muxNo = address.muxNumber();\n  I2CSubBus subBus = address.subBus();\n  if (muxNo != I2CMux_None) {\n    Wire.beginTransmission(I2C_MUX_BASE_ADDRESS+muxNo); \n    uint8_t data =  (subBus == SubBus_All) ? 0xff :\n                    (subBus == SubBus_None) ? 0x00 :\n#if defined(I2CMUX_PCA9547)\n                    0x08 | subBus;\n#elif defined(I2CMUX_PCA9542) || defined(I2CMUX_PCA9544)\n                    0x04 | subBus;   // NB Only 2 or 4 subbuses respectively\n#else\n                    // Default behaviour for most MUXs is to use a mask\n                    // with a bit set for the subBus to be enabled\n                    1 << subBus;\n#endif\n    Wire.write(&data, 1);\n    return Wire.endTransmission(true);  // have to release I2C bus for it to work\n  }\n  return I2C_STATUS_OK;\n}\n#endif\n\n\n/***************************************************************************\n *  Initiate a write to an I2C device (blocking operation on Wire)\n ***************************************************************************/\nuint8_t I2CManagerClass::write(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {\n  uint8_t status, muxStatus;\n  uint8_t retryCount = 0;\n  // If request fails, retry up to the defined limit, unless the NORETRY flag is set\n  // in the request block.\n  do {\n    status = muxStatus = I2C_STATUS_OK;\n#ifdef I2C_EXTENDED_ADDRESS\n    if (address.muxNumber() != I2CMux_None)\n      muxStatus = muxSelect(address);\n#endif\n    // Only send new transaction if address is non-zero.\n    if (muxStatus == I2C_STATUS_OK && address != 0) {\n      Wire.beginTransmission(address);\n      if (size > 0) Wire.write(buffer, size);\n      status = Wire.endTransmission();\n    }\n#ifdef I2C_EXTENDED_ADDRESS\n    // Deselect MUX if there's more than one MUX present, to avoid having multiple ones selected\n    if (_muxCount > 1 && muxStatus == I2C_STATUS_OK \n          && address.deviceAddress() != 0 && address.muxNumber() != I2CMux_None) {\n      muxSelect({address.muxNumber(), SubBus_None});\n    }\n    if (muxStatus != I2C_STATUS_OK) status = muxStatus;\n#endif\n  } while (!(status == I2C_STATUS_OK\n    || ++retryCount > MAX_I2C_RETRIES || rb->operation & OPERATION_NORETRY));\n  rb->status = status;\n  return I2C_STATUS_OK;\n}\n\n/***************************************************************************\n *  Initiate a write from PROGMEM (flash) to an I2C device (blocking operation on Wire)\n ***************************************************************************/\nuint8_t I2CManagerClass::write_P(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {\n  uint8_t ramBuffer[size];\n  const uint8_t *p1 = buffer;\n  for (uint8_t i=0; i<size; i++)\n    ramBuffer[i] = GETFLASH(p1++);\n  return write(address, ramBuffer, size, rb);\n}\n\n/***************************************************************************\n *  Initiate a write (optional) followed by a read from the I2C device (blocking operation on Wire)\n *  If fewer than the number of requested bytes are received, status is I2C_STATUS_TRUNCATED.\n ***************************************************************************/\nuint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,\n                              const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)\n{\n  uint8_t status, muxStatus;\n  uint8_t nBytes = 0;\n  uint8_t retryCount = 0;\n  // If request fails, retry up to the defined limit, unless the NORETRY flag is set\n  // in the request block.\n  do {\n    status = muxStatus = I2C_STATUS_OK;\n#ifdef I2C_EXTENDED_ADDRESS\n    if (address.muxNumber() != I2CMux_None) {\n      muxStatus = muxSelect(address);\n    }\n#endif\n    // Only start new transaction if address is non-zero.\n    if (muxStatus == I2C_STATUS_OK && address != 0) {\n      if (writeSize > 0) {\n        Wire.beginTransmission(address);\n        Wire.write(writeBuffer, writeSize);\n        status = Wire.endTransmission(false); // Don't free bus yet\n      }\n      if (status == I2C_STATUS_OK) {\n#ifdef WIRE_HAS_TIMEOUT\n        Wire.clearWireTimeoutFlag();\n        Wire.requestFrom(address, (size_t)readSize);\n        if (!Wire.getWireTimeoutFlag()) {\n          while (Wire.available() && nBytes < readSize) \n            readBuffer[nBytes++] = Wire.read();\n          if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;\n        } else {\n          status = I2C_STATUS_TIMEOUT;\n        }\n#else\n        Wire.requestFrom(address, (size_t)readSize);\n          while (Wire.available() && nBytes < readSize) \n            readBuffer[nBytes++] = Wire.read();\n          if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;\n#endif\n      }\n    }\n#ifdef I2C_EXTENDED_ADDRESS\n    // Deselect MUX if there's more than one MUX present, to avoid having multiple ones selected\n    if (_muxCount > 1 && muxStatus == I2C_STATUS_OK && address != 0 && address.muxNumber() != I2CMux_None) {\n      muxSelect({address.muxNumber(), SubBus_None});\n    }\n    if (muxStatus != I2C_STATUS_OK) status = muxStatus;\n#endif\n\n  } while (!((status == I2C_STATUS_OK) \n    || ++retryCount > MAX_I2C_RETRIES || rb->operation & OPERATION_NORETRY));\n\n  rb->nBytes = nBytes;\n  rb->status = status;\n  return I2C_STATUS_OK;\n}\n\n\n/***************************************************************************\n *  Function to queue a request block and initiate operations.\n * \n * For the Wire version, this executes synchronously.\n * The read/write/write_P functions return I2C_STATUS_OK always, and the \n * completion status of the operation is in the request block, as for\n * the non-blocking version.\n ***************************************************************************/\nvoid I2CManagerClass::queueRequest(I2CRB *req) {\n  switch (req->operation & OPERATION_MASK) {\n    case OPERATION_READ:\n      read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);\n      break;\n    case OPERATION_SEND:\n      write(req->i2cAddress, req->writeBuffer, req->writeLen, req);\n      break;\n    case OPERATION_SEND_P:\n      write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);\n      break;\n    case OPERATION_REQUEST:\n      read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);\n      break;\n  }\n}\n\n/***************************************************************************\n *  Loop function, for general background work\n ***************************************************************************/\nvoid I2CManagerClass::loop() {}\n\n#endif\n"
  },
  {
    "path": "IODevice.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n#include <Arduino.h>\n#include \"IODevice.h\"\n#include \"DIAG.h\" \n#include \"FSH.h\"\n#include \"IO_MCP23017.h\"\n#include \"DCCTimer.h\"\n\n#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR)\n#define USE_FAST_IO\n#endif\n\n// Link to halSetup function.  If not defined, the function reference will be NULL.\nextern __attribute__((weak)) void halSetup();\nextern __attribute__((weak)) bool exrailHalSetup1();\nextern __attribute__((weak)) bool exrailHalSetup2();\n\n\n//==================================================================================================================\n// Static methods\n//------------------------------------------------------------------------------------------------------------------\n\n// Static functions\n\n// Static method to initialise the IODevice subsystem.  \n\n#if !defined(IO_NO_HAL)\n\n// Create any standard device instances that may be required, such as the Arduino pins \n// and PCA9685.\nvoid IODevice::begin() {\n  // Initialise the IO subsystem defaults\n  ArduinoPins::create(2, NUM_DIGITAL_PINS-2);  // Reserve pins for direct access\n\n  // Call user's halSetup() function (if defined in the build in myHal.cpp).\n  //  The contents will depend on the user's system hardware configuration.\n  //  The myHal.cpp file is a standard C++ module so has access to all of the DCC++EX APIs.\n  // This is done early so that the subsequent defaults will detect an overlap and not\n  // create something that conflicts with the user's vpin definitions. \n  if (halSetup)\n    halSetup();\n\n  // Include any HAL devices defined in exrail.\n  // The first pass call only creates HAL devices, \n  // the second pass will apply servo settings etc which can only be \n  // done after all devices (including the defaults) are created.\n  // If exrailHalSetup1 is not defined, then it will be NULL and the call\n  // will be ignored.\n  // If it returns true, then the default HAL devices will not be created.\n\n  bool ignoreDefaults=false;\n  if (exrailHalSetup1)\n    ignoreDefaults=exrailHalSetup1();\n  \n  if (!ignoreDefaults) {\n  \n    // Predefine two PCA9685 modules 0x40-0x41 if no conflicts\n    // Allocates 32 pins 100-131\n    const bool silent=true; // no message if these conflict\n    if (checkNoOverlap(100, 16, 0x40, silent)) {\n      PCA9685::create(100, 16, 0x40);\n    } \n\n    if (checkNoOverlap(116, 16, 0x41, silent)) {\n      PCA9685::create(116, 16, 0x41);\n    } \n    \n    // Predefine two MCP23017 module 0x20/0x21 if no conflicts\n    // Allocates 32 pins 164-195\n    if (checkNoOverlap(164, 16, 0x20, silent)) {\n      MCP23017::create(164, 16, 0x20);\n    } \n\n    if (checkNoOverlap(180, 16, 0x21, silent)) {\n      MCP23017::create(180, 16, 0x21);\n    } \n  }\n\n  // apply any second pass HAL setup from EXRAIL.\n  // This will typically set up servo profiles, or create turnouts.\n  if (exrailHalSetup2)\n    exrailHalSetup2();\n}\n\n// reset() function to reinitialise all devices\nvoid IODevice::reset() {\n  unsigned long currentMicros = micros();\n  for (IODevice *dev = _firstDevice; dev != NULL; dev = dev->_nextDevice) {\n    dev->_deviceState = DEVSTATE_DORMANT;\n    // First ensure that _loop isn't delaying \n    dev->delayUntil(currentMicros);\n    // Then invoke _begin to restart driver\n    dev->_begin();\n  }\n}\n\n// Overarching static loop() method for the IODevice subsystem.  Works through the\n// list of installed devices and calls their individual _loop() method.\n// Devices may or may not implement this, but if they do it is useful for things like animations \n// or flashing LEDs.\n// The current value of micros() is passed as a parameter, so the called loop function\n// doesn't need to invoke it.\nvoid IODevice::loop() {\n  unsigned long currentMicros = micros();\n  \n  IODevice *lastLoopDevice = _nextLoopDevice;  // So we know when to stop...\n  // Loop through devices until we find one ready to be serviced.\n  do {\n    if (!_nextLoopDevice) _nextLoopDevice = _firstDevice;\n    if (_nextLoopDevice) {\n      if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED \n            && ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) {\n        // Found one ready to run, so invoke its _loop method.\n        _nextLoopDevice->_nextEntryTime = currentMicros;\n        _nextLoopDevice->_loop(currentMicros);\n        _nextLoopDevice = _nextLoopDevice->_nextDevice;\n        break;\n      }\n      // Not this one, move to next one\n      _nextLoopDevice = _nextLoopDevice->_nextDevice;\n    }\n  } while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all.\n  \n  // Report loop time if diags enabled\n#if defined(DIAG_LOOPTIMES)\n  unsigned long diagMicros = micros();\n  static unsigned long lastMicros = 0;\n  // Measure time since HAL's loop() method started.\n  unsigned long halElapsed = diagMicros - currentMicros;\n  // Measure time between loop() method entries (excluding this diagnostic).\n  unsigned long elapsed = diagMicros - lastMicros;\n  static unsigned long maxElapsed = 0, maxHalElapsed = 0;\n  static unsigned long lastOutputTime = 0;\n  static unsigned long halTotal = 0, total = 0;\n  static unsigned long count = 0;\n  const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec\n\n  // Ignore long loop counts while message is still outputting (~3 milliseconds)\n  if (currentMicros - lastOutputTime > 3000UL) {\n    if (elapsed > maxElapsed) maxElapsed = elapsed;\n    if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed;\n    halTotal += halElapsed;\n    total += elapsed;\n    count++;\n  }\n  if (diagMicros - lastOutputTime > interval) {\n    if (lastOutputTime > 0) \n      DIAG(F(\"Loop Total:%lus (%lus max) HAL:%lus (%lus max)\"), \n        total/count, maxElapsed, halTotal/count, maxHalElapsed);\n    maxElapsed = maxHalElapsed = total = halTotal = count = 0;\n    lastOutputTime = diagMicros;\n  }\n  // Read microsecond count after calculations, so they aren't\n  // included in the overall timings.\n  lastMicros = micros();\n#endif\n}\n\n// Display a list of all the devices on the diagnostic stream.\nvoid IODevice::DumpAll() {\n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    dev->_display();\n  }\n}\n\n// Determine if the specified vpin is allocated to a device.\nbool IODevice::exists(VPIN vpin) {\n  return findDevice(vpin) != NULL;\n}\n\n// Return the status of the device att vpin.\nuint8_t IODevice::getStatus(VPIN vpin) {\n  IODevice *dev = findDevice(vpin);\n  if (!dev) return false;\n  return dev->_deviceState;\n}\n\n// check whether the pin supports notification.  If so, then regular _read calls are not required.\nbool IODevice::hasCallback(VPIN vpin) {\n  IODevice *dev = findDevice(vpin);\n  if (!dev) return false;\n  return dev->_hasCallback;\n}\n\n// Display (to diagnostics) details of the device.\nvoid IODevice::_display() {\n  DIAG(F(\"Unknown device Vpins:%u-%u %S\"), \n    (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F(\"OFFLINE\") : F(\"\"));\n}\n\n// Find device associated with nominated Vpin and pass configuration values on to it.\n//   Return false if not found.\nbool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {\n  IODevice *dev = findDevice(vpin);\n  if (dev) return dev->_configure(vpin, configType, paramCount, params);\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::configure(): VPIN %u not found!\"), (int)vpin);\n#endif\n  return false;\n}\n\n// Read value from virtual pin.\nint IODevice::read(VPIN vpin) {\n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    if (dev->owns(vpin)) \n      return dev->_read(vpin);\n  }\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::read(): VPIN %u not found!\"), (int)vpin);\n#endif\n  return false;\n}\n\n// Read analogue value from virtual pin.\nint IODevice::readAnalogue(VPIN vpin) {\n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    if (dev->owns(vpin)) \n      return dev->_readAnalogue(vpin);\n  }\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::readAnalogue(): VPIN %u not found!\"), (int)vpin);\n#endif\n  return -1023;\n}\nint IODevice::configureAnalogIn(VPIN vpin) {\n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    if (dev->owns(vpin)) \n      return dev->_configureAnalogIn(vpin);\n  }\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::configureAnalogIn(): VPIN %u not found!\"), (int)vpin);\n#endif\n  return -1023;\n}\n\n// Write value to virtual pin(s).  If multiple devices are allocated the same pin\n//  then only the first one found will be used.\nvoid IODevice::write(VPIN vpin, int value) {\n  IODevice *dev = findDevice(vpin);\n  if (dev) {\n    dev->_write(vpin, value);\n    return;\n  }\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::write(): VPIN %u not found!\"), (int)vpin);\n#endif\n}\n\n// Write value to count virtual pin(s).\n// these may be within one driver or separated over several drivers \nvoid IODevice::writeRange(VPIN vpin, int value, int count) {  \n  \n  while(count) {  \n    auto dev = findDevice(vpin);\n    if (dev) {\n      auto vpinBefore=vpin; \n      // write to driver, driver will return next vpin it cant handle\n      vpin=dev->_writeRange(vpin, value,count);\n      count-= vpin-vpinBefore;  // decrement by number of vpins changed\n    }\n    else {\n      // skip a vpin if no device handler\n      vpin++;\n      count--;\n    }\n  }\n}\n\n// Write analogue value to virtual pin(s).  If multiple devices are allocated\n// the same pin then only the first one found will be used.\n//\n// The significance of param1 and param2 may vary from device to device.\n// For servo controllers, param1 is the profile of the transition and param2\n// the duration, i.e. the time that the operation is to be animated over\n// in deciseconds (0-3276 sec)\n//\nvoid IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {\n  IODevice *dev = findDevice(vpin);\n  if (dev) {\n    dev->_writeAnalogue(vpin, value, param1, param2);\n    return;\n  }\n#ifdef DIAG_IO\n  DIAG(F(\"IODevice::writeAnalogue(): VPIN %u not found!\"), (int)vpin);\n#endif\n}\n\n//\nvoid IODevice::writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2,int count) {\n  while(count) {  \n    auto dev = findDevice(vpin);\n    if (dev) {\n      auto vpinBefore=vpin; \n      // write to driver, driver will return next vpin it cant handle\n      vpin=dev->_writeAnalogueRange(vpin, value, param1, param2,count);\n      count-= vpin-vpinBefore;  // decrement by number of vpins changed\n    }\n    else {\n      // skip a vpin if no device handler\n      vpin++;\n      count--;\n    }\n  }\n}\n\n// isBusy, when called for a device pin is always a digital output or analogue output,\n//  returns input feedback state of the pin, i.e. whether the pin is busy performing\n//  an animation or fade over a period of time.\nbool IODevice::isBusy(VPIN vpin) {\n  IODevice *dev = findDevice(vpin);\n  if (dev) \n    return dev->_read(vpin);\n  else\n    return false;\n}\n\nvoid IODevice::setGPIOInterruptPin(int16_t pinNumber) {\n  if (pinNumber >= 0)\n    pinMode(pinNumber, INPUT_PULLUP);\n  _gpioInterruptPin = pinNumber;\n}\n\n// Helper function to add a new device to the device chain.  If \n// slaveDevice is NULL then the device is added to the end of the chain.\n// Otherwise, the chain is searched for slaveDevice and the new device linked\n// in front of it (to support filter devices that share the same VPIN range\n// as the devices they control).  If slaveDevice isn't found, then the\n// device is linked to the end of the chain.\nvoid IODevice::addDevice(IODevice *newDevice, IODevice *slaveDevice /* = NULL */) {\n  if (slaveDevice == _firstDevice) {\n    newDevice->_nextDevice = _firstDevice;\n    _firstDevice = newDevice;\n  } else {\n    for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n      if (dev->_nextDevice == slaveDevice || dev->_nextDevice == NULL) {\n          // Link new device between dev and slaveDevice (or at end of chain)\n        newDevice->_nextDevice = dev->_nextDevice;\n        dev->_nextDevice = newDevice;\n        break;\n      }\n    }\n  }\n  newDevice->_begin();\n}\n\n// Private helper function to locate a device by VPIN.  Returns NULL if not found.\n//  This is performance-critical, so minimises the calculation and function calls necessary.\nIODevice *IODevice::findDevice(VPIN vpin) { \n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    VPIN firstVpin = dev->_firstVpin;\n    if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)\n      return dev;\n  }\n  return NULL;\n}\n\n// Instance helper function for filter devices (layered over others).  Looks for \n//  a device that is further down the chain than the current device.\nIODevice *IODevice::findDeviceFollowing(VPIN vpin) {\n  for (IODevice *dev = _nextDevice; dev != 0; dev = dev->_nextDevice) {\n    VPIN firstVpin = dev->_firstVpin;\n    if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)\n      return dev;\n  }\n  return NULL;\n}\n\n// Private helper function to check for vpin overlap. Run during setup only.\n// returns true if pins DONT overlap with existing device\n// TODO: Move the I2C address reservation and checks into the I2CManager code.\n// That will enable non-HAL devices to reserve I2C addresses too.\n// Silent is used by the default setup so that there is no message if the default \n// device has already been handled by the user setup.\nbool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, \n      I2CAddress i2cAddress, bool silent) {\n#ifdef DIAG_IO\n  DIAG(F(\"Check no overlap %u %u %s\"), firstPin,nPins,i2cAddress.toString());\n#endif\n  VPIN lastPin=firstPin+nPins-1;\n  for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {\n    \n    if (nPins > 0 && dev->_nPins > 0) {\n      // check for pin range overlaps (verbose but compiler will fix that)  \n      VPIN firstDevPin=dev->_firstVpin;\n      VPIN lastDevPin=firstDevPin+dev->_nPins-1;\n      bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;\n      if (!noOverlap) {\n          if (!silent) DIAG(F(\"WARNING HAL Overlap, redefinition of Vpins %u to %u ignored.\"),\n              firstPin, lastPin);\n          return false;\n      } \n    }\n    // Check for overlapping I2C address\n    if (i2cAddress && dev->_I2CAddress==i2cAddress) {\n      if (!silent) DIAG(F(\"WARNING HAL Overlap. i2c Addr %s ignored.\"),i2cAddress.toString());\n      return false;\n    } \n  }\n  return true;  // no overlaps... OK to go on with constructor\n}\n  \n\n//==================================================================================================================\n// Static data\n//------------------------------------------------------------------------------------------------------------------\n\n// Chain of callback blocks (identifying registered callback functions for state changes)\nIONotifyCallback *IONotifyCallback::first = 0;\n\n// Start and end of chain of devices.\nIODevice *IODevice::_firstDevice = 0;\n\n// Reference to next device to be called on _loop() method.\nIODevice *IODevice::_nextLoopDevice = 0;\n\n\n//==================================================================================================================\n// Instance members\n//------------------------------------------------------------------------------------------------------------------\n\n// Method to check whether the id corresponds to this device\nbool IODevice::owns(VPIN id) {\n  return (id >= _firstVpin && id < _firstVpin + _nPins);\n}\n\n\n#else // !defined(IO_NO_HAL)\n\n// Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more.\n\nvoid IODevice::begin() { DIAG(F(\"NO HAL CONFIGURED!\")); }\nbool IODevice::configure(VPIN pin, ConfigTypeEnum configType, int nParams, int p[]) {\n  if (configType!=CONFIGURE_INPUT || nParams!=1 || pin >= NUM_DIGITAL_PINS) return false;\n  #ifdef DIAG_IO\n  DIAG(F(\"Arduino _configurePullup pin:%d Val:%d\"), pin, p[0]);\n  #endif\n  pinMode(pin, p[0] ? INPUT_PULLUP : INPUT);\n  return true;\n}\nvoid IODevice::write(VPIN vpin, int value) {\n  if (vpin >= NUM_DIGITAL_PINS) return;\n  digitalWrite(vpin, value);\n  pinMode(vpin, OUTPUT);\n}\nvoid IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {}\nbool IODevice::isBusy(VPIN) { return false; }\nbool IODevice::hasCallback(VPIN) { return false; }\nint IODevice::read(VPIN vpin) { \n  if (vpin >= NUM_DIGITAL_PINS) return 0;\n  return !digitalRead(vpin);  // Return inverted state (5v=0, 0v=1)\n}\nint IODevice::readAnalogue(VPIN vpin) {\n  return ADCee::read(vpin);\n}\nint IODevice::configureAnalogIn(VPIN vpin) {\n  return ADCee::init(vpin);\n}\nvoid IODevice::loop() {}\nvoid IODevice::DumpAll() {\n  DIAG(F(\"NO HAL CONFIGURED!\"));\n}\nbool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < NUM_DIGITAL_PINS); }\nvoid IODevice::setGPIOInterruptPin(int16_t) {}\n\n// Chain of callback blocks (identifying registered callback functions for state changes)\n// Not used in IO_NO_HAL but must be declared.\nIONotifyCallback *IONotifyCallback::first = 0;\n\n#endif // IO_NO_HAL\n\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n\n// Constructor\nArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) {\n  _firstVpin = firstVpin;\n  _nPins = nPins;\n  int arrayLen = (_nPins+7)/8;\n  _pinPullups = (uint8_t *)calloc(3, arrayLen);\n  _pinModes = (&_pinPullups[0]) + arrayLen;\n  _pinInUse = (&_pinPullups[0]) + 2*arrayLen;\n  for (int i=0; i<arrayLen; i++) {\n    _pinPullups[i] = 0xff;  // default to pullup on, for inputs\n    _pinModes[i] = 0;\n    _pinInUse[i] = 0;\n  }\n}\n\n// Device-specific pin configuration.  Configure should be called infrequently so simplify \n// code by using the standard pinMode function.\nbool ArduinoPins::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {\n  if (configType != CONFIGURE_INPUT) return false;\n  if (paramCount != 1) return false;\n  bool pullup = params[0];\n\n  int pin = vpin;\n  #ifdef DIAG_IO\n  DIAG(F(\"Arduino _configurePullup Pin:%d Val:%d\"), pin, pullup);\n  #endif\n  uint8_t mask = 1 << ((pin-_firstVpin) % 8);\n  uint8_t index = (pin-_firstVpin) / 8;\n  _pinModes[index] &= ~mask;  // set to input mode\n  if (pullup) {\n    _pinPullups[index] |= mask;\n    pinMode(pin, INPUT_PULLUP);\n  } else {\n    _pinPullups[index] &= ~mask;\n    pinMode(pin, INPUT);\n  }\n  _pinInUse[index] |= mask;\n  return true;\n}\n\n// Device-specific write function.\nvoid ArduinoPins::_write(VPIN vpin, int value) {\n  int pin = vpin;\n  #ifdef DIAG_IO\n  DIAG(F(\"Arduino Write Pin:%d Val:%d\"), pin, value);\n  #endif\n  uint8_t mask = 1 << ((pin-_firstVpin) % 8);\n  uint8_t index = (pin-_firstVpin) / 8;\n  // First update the output state, then set into write mode if not already.\n  fastWriteDigital(pin, value);\n  if (!(_pinModes[index] & mask)) {\n    // Currently in read mode, change to write mode\n    _pinModes[index] |= mask;\n    // Since mode changes should be infrequent, use standard pinMode function\n    pinMode(pin, OUTPUT);\n    _pinInUse[index] |= mask;\n  }\n}\n\n// Device-specific read function (digital input).\nint ArduinoPins::_read(VPIN vpin) {\n  int pin = vpin;\n  uint8_t mask = 1 << ((pin-_firstVpin) % 8);\n  uint8_t index = (pin-_firstVpin) / 8;\n  if ((_pinModes[index] | ~_pinInUse[index]) & mask) {\n    // Currently in write mode or not initialised, change to read mode\n    _pinModes[index] &= ~mask;\n    // Since mode changes should be infrequent, use standard pinMode function\n    if (_pinPullups[index] & mask) \n      pinMode(pin, INPUT_PULLUP);\n    else\n      pinMode(pin, INPUT);\n    _pinInUse[index] |= mask;\n  }\n  int value = !fastReadDigital(pin); // Invert (5v=0, 0v=1)\n\n  #ifdef DIAG_IO\n  //DIAG(F(\"Arduino Read Pin:%d Value:%d\"), pin, value);\n  #endif\n  return value;\n}\n\n// Device-specific readAnalogue function (analogue input)\nint ArduinoPins::_readAnalogue(VPIN vpin) {\n  if (vpin > 255) return -1023;\n  uint8_t pin = vpin;\n  int value = ADCee::read(pin);\n\n  #ifdef DIAG_IO\n  DIAG(F(\"Arduino Read Pin:%d Value:%d\"), pin, value);\n  #endif\n  return value;\n}\nint ArduinoPins::_configureAnalogIn(VPIN vpin) {\n  if (vpin > 255) return -1023;\n  uint8_t pin = vpin;\n  uint8_t mask = 1 << ((pin-_firstVpin) % 8);\n  uint8_t index = (pin-_firstVpin) / 8;\n  if (_pinModes[index] & mask) {\n    // Currently in write mode, change to read mode\n    _pinModes[index] &= ~mask;\n    // Since mode changes should be infrequent, use standard pinMode function\n    if (_pinPullups[index] & mask) \n      pinMode(pin, INPUT_PULLUP);\n    else\n      pinMode(pin, INPUT);\n  }\n  int value = ADCee::init(pin);\n  #ifdef DIAG_IO\n  DIAG(F(\"configureAnalogIn Pin:%d Value:%d\"), pin, value);\n  #endif\n  return value;\n}\n\nvoid ArduinoPins::_display() {\n  DIAG(F(\"Arduino Vpins:%u-%u\"), (int)_firstVpin, (int)_firstVpin+_nPins-1);\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n\n\nvoid ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) {\n#if defined(USE_FAST_IO)\n  if (pin >= NUM_DIGITAL_PINS) return;\n  uint8_t mask = digitalPinToBitMask(pin);\n  uint8_t port = digitalPinToPort(pin);\n  volatile uint8_t *outPortAdr = portOutputRegister(port);\n  noInterrupts();\n  if (value) \n    *outPortAdr |= mask;\n  else\n    *outPortAdr &= ~mask;\n  interrupts();\n#else\n  digitalWrite(pin, value);\n#endif\n}\n\nbool ArduinoPins::fastReadDigital(uint8_t pin) {\n#if defined(USE_FAST_IO)\n  if (pin >= NUM_DIGITAL_PINS) return false;\n  uint8_t mask = digitalPinToBitMask(pin);\n  uint8_t port = digitalPinToPort(pin);\n  volatile uint8_t *inPortAdr = portInputRegister(port);\n  // read input\n  bool result = (*inPortAdr & mask) != 0;  \n#else\n  bool result = digitalRead(pin);\n#endif\n  return result;\n}\n"
  },
  {
    "path": "IODevice.h",
    "content": "/*\n *  © 2023, Paul Antoine, Discord user @ADUBOURG\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef iodevice_h\n#define iodevice_h\n\n// Define symbol DIAG_IO to enable diagnostic output\n//#define DIAG_IO Y\n\n// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported\n//#define DIAG_LOOPTIMES\n\n// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an \n// animation has completed.  This switches off the servo motor, preventing \n// the continuous buzz sometimes found on servos, and reducing the \n// power consumption of the servo when inactive.\n// It is recommended to enable this, unless it causes you problems.\n#define IO_SWITCH_OFF_SERVO\n\n#include \"DIAG.h\"\n#include \"FSH.h\"\n#include \"I2CManager.h\"\n#include \"inttypes.h\"\n#include \"TemplateForEnums.h\"\n\ntypedef uint16_t VPIN;\n// Limit VPIN number to max 32767.  Above this number, printing often gives negative values.\n// This should be enough for 99% of users.\n#define VPIN_MAX 32767  \n#define VPIN_NONE 65535\n\n/* \n * Callback support for state change notification from an IODevice subclass to a \n * handler, e.g. Sensor object handling.\n */\n\nclass IONotifyCallback {\npublic: \n  typedef void IONotifyCallbackFunction(VPIN vpin, int value);\n  static void add(IONotifyCallbackFunction *function) {\n    IONotifyCallback *blk = new IONotifyCallback(function);\n    if (first) blk->next = first;\n    first = blk;\n  }\n  static void invokeAll(VPIN vpin, int value) {\n    for (IONotifyCallback *blk = first; blk != NULL; blk = blk->next)\n      blk->invoke(vpin, value);\n  }\n  static bool hasCallback() {\n    return first != NULL;\n  }\nprivate:\n  IONotifyCallback(IONotifyCallbackFunction *function) { invoke = function; };\n  IONotifyCallback *next = 0;\n  IONotifyCallbackFunction *invoke = 0;\n  static IONotifyCallback *first;\n};\n\n/*\n * IODevice class\n * \n * This class is the basis of the Hardware Abstraction Layer (HAL) for\n * the DCC++EX Command Station.  All device classes derive from this.\n * \n */\n\nclass IODevice {\npublic:\n\n  // Parameter values to identify type of call to IODevice::configure.\n  typedef enum : uint8_t {\n    CONFIGURE_INPUT = 1,\n    CONFIGURE_SERVO = 2,\n    CONFIGURE_OUTPUT = 3,\n    CONFIGURE_ANALOGOUTPUT = 4,\n    CONFIGURE_ANALOGINPUT = 5,\n  } ConfigTypeEnum;\n\n  typedef enum : uint8_t {\n    DEVSTATE_DORMANT = 0,\n    DEVSTATE_PROBING = 1,\n    DEVSTATE_INITIALISING = 2,\n    DEVSTATE_NORMAL = 3,\n    DEVSTATE_SCANNING = 4,\n    DEVSTATE_FAILED = 5,\n  } DeviceStateEnum;\n\n  // Static functions to find the device and invoke its member functions\n\n  // begin is invoked to create any standard IODevice subclass instances.\n  // Also, the _begin method of any existing instances is called from here.\n  static void begin();\n\n  // reset function to invoke all driver's _begin() methods again, to\n  // reset the state of the devices and reinitialise.\n  static void reset();\n\n  // configure is used invoke an IODevice instance's _configure method\n  static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]);\n\n  // User-friendly function for configuring an input pin.\n  inline static bool configureInput(VPIN vpin, bool pullupEnable) {\n    int params[] = {pullupEnable};\n    return IODevice::configure(vpin, CONFIGURE_INPUT, 1, params);\n  }\n\n  // User-friendly function for configuring a servo pin.\n  inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile=0, uint16_t duration=0, uint8_t initialState=0) {\n    int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState};\n    return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params);\n  }\n\n  // write invokes the IODevice instance's _write method.\n  static void write(VPIN vpin, int value);\n  static void writeRange(VPIN vpin, int value,int count);\n\n  // write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)\n  static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);\n  static void writeAnalogueRange(VPIN vpin, int value, uint8_t profile, uint16_t duration, int count);\n\n  // isBusy returns true if the device is currently in an animation of some sort, e.g. is changing\n  //  the output over a period of time.\n  static bool isBusy(VPIN vpin);\n\n  // check whether the pin supports notification.  If so, then regular _read calls are not required.\n  static bool hasCallback(VPIN vpin);\n\n  // read invokes the IODevice instance's _read method.\n  static int read(VPIN vpin);\n\n  // read invokes the IODevice instance's _readAnalogue method.\n  static int readAnalogue(VPIN vpin);\n  static int configureAnalogIn(VPIN vpin);\n\n  // loop invokes the IODevice instance's _loop method.\n  static void loop();\n\n  static void DumpAll();\n\n  // exists checks whether there is a device owning the specified vpin\n  static bool exists(VPIN vpin);\n\n  // getStatus returns the state of the device at the specified vpin\n  static uint8_t getStatus(VPIN vpin);\n\n  // Enable shared interrupt on specified pin for GPIO extender modules.  The extender module\n  // should pull down this pin when requesting a scan.  The pin may be shared by multiple modules.\n  // Without the shared interrupt, input states are scanned periodically to detect changes on \n  // GPIO extender pins.  If a shared interrupt pin is configured, then input states are scanned\n  // only when the shared interrupt pin is pulled low.  The external GPIO module releases the pin\n  // once the GPIO port concerned has been read.\n  void setGPIOInterruptPin(int16_t pinNumber);\n\n  // Method to check if pins will overlap before creating new device. \n  static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, \n                  I2CAddress i2cAddress=0, bool silent=false);\n\n  // Method used by IODevice filters to locate slave pins that may be overlayed by their own\n  // pin range.  \n  IODevice *findDeviceFollowing(VPIN vpin);\n\n  // Method to write new state (optionally implemented within device class)\n  virtual void _write(VPIN vpin, int value) {\n    (void)vpin; (void)value;\n  };\n \n // Method to write new state (optionally implemented within device class)\n // This will, by default just write to one vpin and return whet to do next.\n // the real power comes where a single driver can update many vpins in one call.\n  virtual VPIN _writeRange(VPIN vpin, int value, int count) {\n    (void)count;\n    _write(vpin,value); \n    return vpin+1; // try next vpin \n  };\n\n  // Method to write an 'analogue' value (optionally implemented within device class)\n  virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) {\n    (void)vpin; (void)value; (void) param1; (void)param2;\n  };\n  \n  // Method to write an 'analogue' value to a VPIN range (optionally implemented within device class)\n  // This will, by default just write to one vpin and return whet to do next.\n  // the real power comes where a single driver can update many vpins in one call.\n  virtual VPIN _writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2, int count) {\n    (void) count;\n    _writeAnalogue(vpin, value,  param1, param2);\n    return vpin+1; \n  };\n\n  // Method to read digital pin state (optionally implemented within device class)\n  virtual int _read(VPIN vpin) { \n    (void)vpin; \n    return 0;\n  };\n\n  // Method to read analogue pin state (optionally implemented within device class)\n  virtual int _readAnalogue(VPIN vpin) { \n    (void)vpin; \n    return 0;\n  };\n\nprotected:\n  \n  // Constructor\n  IODevice(VPIN firstVpin=0, int nPins=0) {\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    _nextEntryTime = 0;\n    _I2CAddress=0;\n  }\n\n  // Method to perform initialisation of the device (optionally implemented within device class)\n  virtual void _begin() {}\n\n  // Method to check whether the vpin corresponds to this device\n  bool owns(VPIN vpin);\n\n  // Method to configure device (optionally implemented within device class)\n  virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { \n    (void)vpin; (void)configType; (void)paramCount; (void)params; // Suppress compiler warning.\n    return false;\n  };\n\n  virtual int _configureAnalogIn(VPIN vpin) { \n    (void)vpin; \n    return 0;\n  };\n\n  // Method to perform updates on an ongoing basis (optionally implemented within device class)\n  virtual void _loop(unsigned long currentMicros) {\n    delayUntil(currentMicros + 0x7fffffff); // Largest time in the future!  Effectively disable _loop calls.\n  };\n\n  // Method for displaying info on DIAG output (optionally implemented within device class)\n  virtual void _display();\n\n  // Destructor\n  virtual ~IODevice() {};\n\n  // Non-virtual function\n  void delayUntil(unsigned long futureMicrosCount) {\n    _nextEntryTime = futureMicrosCount;\n  }\n  \n  // Common object fields.\n  VPIN _firstVpin;\n  int _nPins;\n  I2CAddress _I2CAddress;\n  // Flag whether the device supports callbacks.\n  bool _hasCallback = false;\n\n  // Pin number of interrupt pin for GPIO extender devices.  The extender module will pull this\n  //  pin low if an input changes state.\n  int16_t _gpioInterruptPin = -1;\n    \n  // Static support function for subclass creation\n  static void addDevice(IODevice *newDevice, IODevice *slaveDevice = NULL);\n\n  // Method to find device handling Vpin\n  static IODevice *findDevice(VPIN vpin);\n\n  // Current state of device\n  DeviceStateEnum _deviceState = DEVSTATE_DORMANT;\n\nprivate:\n  IODevice *_nextDevice = 0;\n  unsigned long _nextEntryTime;\n  static IODevice *_firstDevice;\n\n  static IODevice *_nextLoopDevice;\n};\n\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for PCA9685 16-channel PWM module.\n */\n \nclass PCA9685 : public IODevice {\npublic:\n  static void create(VPIN vpin, int nPins, I2CAddress i2cAddress, uint16_t frequency = 50);\n  enum ProfileType : uint8_t {\n    Instant = 0,  // Moves immediately between positions (if duration not specified)\n    UseDuration = 0, // Use specified duration\n    Fast = 1,     // Takes around 500ms end-to-end\n    Medium = 2,   // 1 second end-to-end\n    Slow = 3,     // 2 seconds end-to-end\n    Bounce = 4,   // For semaphores/turnouts with a bit of bounce!!\n    NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.\n  };\n\nprivate:\n  // Constructor\n  PCA9685(VPIN vpin, int nPins, I2CAddress i2cAddress, uint16_t frequency);\n  // Device-specific initialisation\n  void _begin() override;\n  bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;\n  // Device-specific write functions.\n  void _write(VPIN vpin, int value) override;\n  void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override;\n  int _read(VPIN vpin) override; // returns the digital state or busy status of the device\n  void _loop(unsigned long currentMicros) override;\n  void updatePosition(uint8_t pin);\n  void writeDevice(uint8_t pin, int value);\n  void _display() override;\n  void writeRegister(uint8_t reg, uint8_t value);\n  \n\n  struct ServoData {\n    uint16_t activePosition : 12; // Config parameter\n    uint16_t inactivePosition : 12; // Config parameter\n    uint16_t currentPosition : 12;\n    uint16_t fromPosition : 12;\n    uint16_t toPosition : 12; \n    uint8_t profile;  // Config parameter\n    uint16_t stepNumber; // Index of current step (starting from 0)\n    uint16_t numSteps;  // Number of steps in animation, or 0 if none in progress.\n    uint8_t currentProfile; // profile being used for current animation.\n    uint16_t duration; // time (tenths of a second) for animation to complete.\n  }; // 14 bytes per element, i.e. per pin in use\n  \n  struct ServoData *_servoData [16];\n\n  static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off\n  static const uint8_t FLASH _bounceProfile[30];\n\n  const unsigned int refreshInterval = 50; // refresh every 50ms\n\n  // structures for setting up non-blocking writes to servo controller\n  I2CRB requestBlock;\n  uint8_t outputBuffer[5];\n  uint8_t prescaler; // clock prescaler for setting PWM frequency\n};\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for DCC accessory decoder.\n */\n \nclass DCCAccessoryDecoder: public IODevice {\npublic:\n  static void create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);\n\nprivate:\n  // Constructor\n  DCCAccessoryDecoder(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);\n  // Device-specific write function.\n  void _begin() override;\n  void _write(VPIN vpin, int value) override;\n  void _display() override;\n  int _packedAddress;\n};\n\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/* \n *  IODevice subclass for arduino input/output pins.\n */\n \nclass ArduinoPins: public IODevice {\npublic:\n  static void create(VPIN firstVpin, int nPins) {\n    addDevice(new ArduinoPins(firstVpin, nPins));\n  }\n  \n  static void fastWriteDigital(uint8_t pin, uint8_t value);\n  static bool fastReadDigital(uint8_t pin);\n\nprivate:\n  // Constructor\n  ArduinoPins(VPIN firstVpin, int nPins);\n\n  // Device-specific pin configuration\n  bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;\n  // Device-specific write function.\n  void _write(VPIN vpin, int value) override;\n  // Device-specific read functions.\n  int _read(VPIN vpin) override;\n  int _readAnalogue(VPIN vpin) override;\n  int _configureAnalogIn(VPIN vpin) override;\n  void _display() override;\n\n\n  uint8_t *_pinPullups;\n  uint8_t *_pinModes; // each bit is 1 for output, 0 for input\n  uint8_t *_pinInUse; \n};\n\n#ifndef IO_NO_HAL\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for EX-Turntable.\n */\n \nclass EXTurntable : public IODevice {\npublic:\n  static void create(VPIN firstVpin, int nPins, I2CAddress I2CAddress);\n  // Constructor\n  EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress);\n  enum ActivityNumber : uint8_t {\n    Turn = 0,             // Rotate turntable, maintain phase\n    Turn_PInvert = 1,     // Rotate turntable, invert phase\n    Home = 2,             // Initiate homing\n    Calibrate = 3,        // Initiate calibration sequence\n    LED_On = 4,           // Turn LED on\n    LED_Slow = 5,         // Set LED to a slow blink\n    LED_Fast = 6,         // Set LED to a fast blink\n    LED_Off = 7,          // Turn LED off\n    Acc_On = 8,           // Turn accessory pin on\n    Acc_Off = 9,          // Turn accessory pin off\n  };\n\nprivate:\n  // Device-specific write function.\n  void _begin() override;\n  void _loop(unsigned long currentMicros) override;\n  int _read(VPIN vpin) override;\n  void _broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity);\n  void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override;\n  void _display() override;\n  uint8_t _stepperStatus;\n  uint8_t _previousStatus;\n  uint8_t _currentActivity;\n};\n#endif\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// IODevice framework for invoking user-written functions.\n// To use, define a function that you want to be regularly\n// invoked, and then create an instance of UserAddin.  \n// For example, you can show the status, on screen 3, of the first eight\n// locos in the speed table:\n// \n// void updateLocoScreen() {\n//   for (int i=0; i<8; i++) {\n//     if (DCC::speedTable[i].loco > 0) {\n//       int speed = DCC::speedTable[i].speedCode;\n//       SCREEN(3, i, F(\"Loco:%4d %3d %c\"), DCC::speedTable[i].loco,\n//         speed & 0x7f, speed & 0x80 ? 'R' : 'F');\n//     }\n//   }\n// }\n//\n// void halSetup() {\n//   ...\n//   UserAddin(updateLocoScreen, 1000);  // Update every 1000ms\n//   ...\n// }\n//\nclass UserAddin : public IODevice {\nprivate:\n  void (*_invokeUserFunction)();\n  int _delay; // milliseconds\npublic:\n  UserAddin(void (*func)(), int delay) {\n    _invokeUserFunction = func; \n    _delay = delay; \n    addDevice(this); \n  }\n  // userFunction has no return value, no parameter.  delay is in milliseconds.\n  static void create(void (*userFunction)(), int delay) {\n    new UserAddin(userFunction, delay);\n  }\nprotected:\n  void _begin() { _display(); }\n  void _loop(unsigned long currentMicros) override {\n    _invokeUserFunction();\n    // _loop won't be called again until _delay ms have elapsed.\n    delayUntil(currentMicros + _delay * 1000UL);\n  }\n  void _display() override {\n    DIAG(F(\"UserAddin run every %dms\"), _delay);\n  }\n};\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n// \n// This HAL device driver is intended for communication in automation\n// sequences.  A VPIN can be SET or RESET within a sequence, and its\n// current state checked elsewhere using IF, IFNOT, AT etc. or monitored\n// from JMRI using a Sensor object (DCC-EX <S ...> command).\n// Alternatively, the flag can be set from JMRI and other interfaces\n// using the <Z ...> command, to enable or disable actions within a sequence.\n// \n// Example of configuration in halSetup.h:\n// \n//  FLAGS::create(32000, 128);\n// \n// or in myAutomation.h:\n//  \n//  HAL(FLAGS, 32000, 128);\n// \n// Both create 128 flags numbered with VPINs 32000-32127.\n// \n//\n\nclass FLAGS : IODevice {\nprivate:\n  uint8_t *_states = NULL;\n\npublic:\n  static void create(VPIN firstVpin, unsigned int nPins) {\n    if (checkNoOverlap(firstVpin, nPins))\n        new FLAGS(firstVpin, nPins);\n  }\n\nprotected:\n  // Constructor performs static initialisation of the device object\n  FLAGS (VPIN firstVpin, int nPins) {\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    _states = (uint8_t *)calloc(1, (_nPins+7)/8);\n    if (!_states) {\n      DIAG(F(\"FLAGS: ERROR Memory Allocation Failure\"));\n      return;\n    }\n\n    addDevice(this);\n  }\n\n  int _read(VPIN vpin) override {\n    int pin = vpin - _firstVpin;\n    if (pin >= _nPins || pin < 0) return 0;\n    uint8_t mask = 1 << (pin & 7);\n    return (_states[pin>>3] & mask) ? 1 : 0;\n  }\n\n  void _write(VPIN vpin, int value) override {\n    int pin = vpin - _firstVpin;\n    if (pin >= _nPins || pin < 0) return;\n    uint8_t mask = 1 << (pin & 7);\n    if (value) \n      _states[pin>>3] |= mask;\n    else\n      _states[pin>>3] &= ~mask;\n  }\n\n  void _display() override {\n    DIAG(F(\"FLAGS configured on VPINs %u-%u\"),\n      _firstVpin, _firstVpin+_nPins-1);\n  }\n\n};\n\n//#include \"IODeviceList.h\"\n\n#endif // iodevice_h\n"
  },
  {
    "path": "IODeviceList.h",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n/* \nThis is the list of HAL drivers automatically included by IODevice.h\nIt has been moved here to be easier to maintain than editing IODevice.h\n*/\n#include \"IO_AnalogueInputs.h\"\n#include \"IO_DFPlayer.h\"\n#include \"IO_DS1307.h\"\n#include \"IO_duinoNodes.h\"\n#include \"IO_EncoderThrottle.h\"\n#include \"IO_EXFastclock.h\"\n#include \"IO_EXIOExpander.h\"\n#include \"IO_EXSensorCAM.h\"\n#include \"IO_HALDisplay.h\"\n#include \"IO_HCSR04.h\"\n#include \"IO_I2CRailcom.h\"\n#include \"IO_MCP23008.h\"\n#include \"IO_MCP23017.h\"\n#include \"IO_NeoPixel.h\"\n#include \"IO_PCA9555.h\"\n#include \"IO_PCA9685pwm.h\"\n#include \"IO_PCF8574.h\"\n#include \"IO_PCF8575.h\"\n#include \"IO_RotaryEncoder.h\"\n#include \"IO_Servo.h\"\n#include \"IO_TCA8418.h\"\n#include \"IO_TM1638.h\"\n#include \"IO_TouchKeypad.h\"\n#include \"IO_trainbrains.h\"\n#include \"IO_Bitmap.h\"\n#include \"IO_VL53L0X.h\"\n#include \"IO_XL9535.h\"\n"
  },
  {
    "path": "IO_AnalogueInputs.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_analogueinputs_h\n#define io_analogueinputs_h\n\n// Uncomment following line to slow the scan cycle down to 1second ADC samples, with\n// diagnostic output of scanned values.\n//#define IO_ANALOGUE_SLOW\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"FSH.h\"\n\n/**********************************************************************************************\n * ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115.\n * \n * ADS1113 and ADS1114 are restricted to 1 input.  ADS1115 has a multiplexer which allows \n * any of four input pins to be read by its ADC.\n * \n * The driver polls the device in accordance with the constant 'scanInterval' below.  On first loop\n * entry, the multiplexer is set to pin A0 and the ADC is triggered.  On second and subsequent\n * entries, the analogue value is read from the conversion register and then the multiplexer and\n * ADC are set up to read the next pin.\n * \n * The ADS111x is set up as follows:\n *    Single-shot scan\n *    Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms)\n *    Comparator off\n *    Gain FSR=6.144V\n * The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading \n * of 32767*(5.0/6.144) = 26666.\n * \n * A device is configured by the following:\n *   ADS111x::create(firstVpin, nPins, i2cAddress);\n * for example\n *   ADS111x::create(300, 1, 0x48);  // single-input ADS1113\n *   ADS111x::create(300, 4, 0x48);  // four-input ADS1115\n * \n * Note: The device is simple and does not need initial configuration, so it should recover from\n * temporary loss of communications or power.\n **********************************************************************************************/\nclass ADS111x: public IODevice { \npublic:\n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n    if (checkNoOverlap(firstVpin,nPins,i2cAddress)) new ADS111x(firstVpin, nPins, i2cAddress);\n  }\nprivate:\n  ADS111x(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n    _firstVpin = firstVpin;\n    _nPins = (nPins > 4) ? 4 : nPins;\n    _I2CAddress = i2cAddress;\n    _currentPin = 0;\n    for (int8_t i=0; i<_nPins; i++)\n      _value[i] = -1;\n    addDevice(this);\n  }\n  void _begin() {\n    // Initialise I2C\n    I2CManager.begin();\n    // ADS111x support high-speed I2C (4.3MHz) but that requires special\n    // processing.  So stick to fast mode (400kHz maximum).\n    I2CManager.setClock(400000);\n    // Initialise ADS device\n    if (I2CManager.exists(_I2CAddress)) {\n      _nextState = STATE_STARTSCAN;\n#ifdef DIAG_IO\n      _display();\n#endif\n    } else {\n      DIAG(F(\"ADS111x device not found, I2C:%s\"), _I2CAddress.toString());\n      _deviceState = DEVSTATE_FAILED;\n    }\n  }\n  void _loop(unsigned long currentMicros) override {\n\n    // Check that previous non-blocking write has completed, if not then wait\n    uint8_t status = _i2crb.status;\n    if (status == I2C_STATUS_PENDING) return;  // Busy, so don't do anything.\n    if (status == I2C_STATUS_OK) {\n      switch (_nextState) {\n        case STATE_STARTSCAN:\n          // Configure ADC and multiplexer for next scan.  See ADS111x datasheet for details\n          // of configuration register settings.\n          _outBuffer[0] = 0x01; // Config register address\n          _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n\n          _outBuffer[2] = 0xA3;           // 250 samples/sec, comparator off\n          // Write command, without waiting for completion.\n          I2CManager.write(_I2CAddress, _outBuffer, 3, &_i2crb);\n\n          delayUntil(currentMicros + scanInterval);\n          _nextState = STATE_STARTREAD;\n          break;\n\n        case STATE_STARTREAD:\n          // Reading the pin value\n          _outBuffer[0] = 0x00;  // Conversion register address\n          I2CManager.read(_I2CAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register\n          _nextState = STATE_GETVALUE;\n          break;\n\n        case STATE_GETVALUE:\n          _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1];\n          #ifdef IO_ANALOGUE_SLOW\n          DIAG(F(\"ADS111x VPIN:%u value:%d\"), _currentPin, _value[_currentPin]);\n          #endif\n\n          // Move to next pin\n          if (++_currentPin >= _nPins) _currentPin = 0;\n          _nextState = STATE_STARTSCAN;\n          break;\n        \n        default:\n          break;\n      }\n    } else { // error status\n      DIAG(F(\"ADS111x I2C:%s Error:%d %S\"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));\n      _deviceState = DEVSTATE_FAILED;\n    }\n  }\n\n  int _readAnalogue(VPIN vpin) override {\n    int pin = vpin - _firstVpin;\n    return _value[pin];\n  }\n  \n  void _display() override {\n    DIAG(F(\"ADS111x I2C:%s Configured on Vpins:%u-%u %S\"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,\n      _deviceState == DEVSTATE_FAILED ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n  // ADC conversion rate is 250SPS, or 4ms per conversion.  Set the period between updates to 10ms. \n  // This is enough to allow the conversion to reliably complete in time.\n  #ifndef IO_ANALOGUE_SLOW\n  const unsigned long scanInterval = 10000UL;  // Period between successive ADC scans in microseconds.\n  #else\n  const unsigned long scanInterval = 1000000UL;  // Period between successive ADC scans in microseconds.\n  #endif\n  enum : uint8_t {\n    STATE_STARTSCAN,\n    STATE_STARTREAD, \n    STATE_GETVALUE,\n  };\n  uint16_t _value[4];\n  uint8_t _outBuffer[3];\n  uint8_t _inBuffer[2];\n  uint8_t _currentPin;  // ADC pin currently being scanned\n  I2CRB _i2crb;\n  uint8_t _nextState;\n};\n\n#endif // io_analogueinputs_h\n"
  },
  {
    "path": "IO_Bitmap.h",
    "content": "/*\n *  © 2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef IO_Bitmap_h\n #define IO_Bitmap_h\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"IODevice.h\"\n\n/* \nBitmap provides a set of virtual pins with no hardware.\nBitmap pins are able to be output and input and may be set and tested \nas digital or analogue values.\nWhen writing a digital value, the analogue value is set to 0 or 1. \nWhen reading a digital value, the return is LOW for value 0 or HIGH for any other value\nor analogue.\n\nBitmap pins may be used for any purpose, this is easier to manage than LATCH in EXRAIL\nas they can be explicitely set and tested without interfering with underlying hardware.\nBitmap pins may be set, reset and tested in the same way as any other pin.\nThey are not persistent across reboots, but are retained in the current session.\nBitmap pins may also be monitored by JMRI_SENSOR() and <S> as for any other pin.\n   \n*/\nclass Bitmap : public IODevice {\n\npublic:\n  static void create(VPIN firstVpin, int nPins) {   \n    if (IODevice::checkNoOverlap(firstVpin,nPins))\n         new Bitmap( firstVpin,  nPins);\n  }\n\n  Bitmap(VPIN firstVpin, int nPins) : IODevice(firstVpin, nPins) {\n    _pinValues=(int16_t *) calloc(nPins,sizeof(int16_t));  \n    // Connect to HAL so my _write, _read and _loop will be called as required.\n    IODevice::addDevice(this);  \n  }\n\n// Called by HAL to start handling this device\n  void _begin() override {\n    _deviceState = DEVSTATE_NORMAL;\n    _display();\n  }\n\n  int _read(VPIN vpin) override {\n    int pin=vpin - _firstVpin;\n    return _pinValues[pin]?1:0;\n  }\n\n  void _write(VPIN vpin, int value) override {\n    int pin = vpin - _firstVpin;\n    _pinValues[pin]=value!=0; // this is digital write  \n  }\n\n  int _readAnalogue(VPIN vpin) override {\n    int pin=vpin - _firstVpin;\n    return _pinValues[pin]; // this is analog read  \n  }\n\n  void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {\n    (void)profile; // suppress warning, not used in this function\n    (void)duration; // suppress warning, not used in this function\n    int pin=vpin - _firstVpin;\n    _pinValues[pin]=value; // this is analog write  \n  }\n\n  void _display() override {\n      DIAG(F(\"Bitmap Configured on Vpins:%u-%u\"), \n      (int)_firstVpin, \n      (int)_firstVpin+_nPins-1);\n  }\n\nprivate:\n  int16_t* _pinValues;\n};\n#endif\n"
  },
  {
    "path": "IO_DCCAccessory.cpp",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"DCC.h\"\n#include \"IODevice.h\"\n#include \"DIAG.h\"\n#include \"defines.h\"\n\n#define PACKEDADDRESS(addr, subaddr) (((addr) << 2)  + (subaddr))\n#define ADDRESS(packedaddr) ((packedaddr) >> 2)\n#define SUBADDRESS(packedaddr) ((packedaddr) % 4)\n\nvoid DCCAccessoryDecoder::create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress) {\n  if (checkNoOverlap(firstVpin,nPins)) new DCCAccessoryDecoder(firstVpin, nPins, DCCAddress, DCCSubaddress);\n}\n\n// Constructors\nDCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {\n   _firstVpin = vpin;\n  _nPins = nPins;\n  _packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress);\n  addDevice(this);\n}\n\nvoid DCCAccessoryDecoder::_begin() {\n#if defined(DIAG_IO)\n  _display();\n#endif\n}\n\n// Device-specific write function.  State 1=closed, 0=thrown.  Adjust for RCN-213 compliance\nvoid DCCAccessoryDecoder::_write(VPIN id, int state) {\n  int packedAddress = _packedAddress + id - _firstVpin;\n#if defined(HAL_ACCESSORY_COMMAND_REVERSE)\n  state = !state;\n#ifdef DIAG_IO\n  DIAG(F(\"DCC Write Linear Address:%d State:%d (inverted)\"), packedAddress, state);\n#endif\n#else\n#ifdef DIAG_IO\n  DIAG(F(\"DCC Write Linear Address:%d State:%d\"), packedAddress, state);\n#endif\n#endif\n  DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state);\n}\n\nvoid DCCAccessoryDecoder::_display() {\n  int endAddress = _packedAddress + _nPins - 1;\n  DIAG(F(\"DCCAccessoryDecoder Configured on Vpins:%u-%u Addresses %d/%d-%d/%d)\"), _firstVpin, _firstVpin+_nPins-1,\n      ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));\n}\n"
  },
  {
    "path": "IO_DFPlayer.h",
    "content": "/*\n * © 2026, Nicola Malavasi. All rights reserved.\n * © 2025-26, Chris Harlow. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This file acts as the main wrapper for the DFPlayer driver suite.\n * It provides a unified interface to create Serial or I2C instances.\n * * KEY FEATURES:\n * 1. Unified Interface: Simplified 'create' method for all transport layers, \n * making the driver more user-friendly in myHAL.cpp.\n * 2. Plug-and-Play (I2C): Eliminates the need for manual crystal frequency \n * parameters by leveraging the auto-detection logic in the I2C layer.\n * 3. Platform Abstraction: Automatically handles hardware differences between \n * standard AVR (Mega) and ESP32 (customizable serial pins).\n * 4. Resource Protection: Prevents VPIN and address conflicts by integrating \n * with the DCC-EX IODevice registry.\n */\n\n\n#ifndef IO_DFPlayer_h\n#define IO_DFPlayer_h\n#include \"IO_DFPlayerSerial.h\"\n#include \"IO_DFPlayerI2C.h\"\n\nclass DFPlayer : public IODevice {\npublic:\n  static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress) {\n    if (nPins>2) nPins=2;\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) {\n      new DFPlayerI2C(firstVpin, i2cAddress, 0);\n      if (nPins >= 2) {\n        new DFPlayerI2C(firstVpin + 1, i2cAddress, 1);\n      }\n    }\n  }\n\n  #ifdef ESP32\n  static void create(VPIN f, HardwareSerial &s, int8_t rxPin, int8_t txPin) { \n      create(f, 1, s, rxPin, txPin);\n  }\n  static void create(VPIN firstVpin, int nPins, HardwareSerial &serial, int8_t rxPin, int8_t txPin) {\n    if (checkNoOverlap(firstVpin, nPins)) {\n      serial.begin(9600, SERIAL_8N1, rxPin, txPin);\n      new DFPlayerSerial(firstVpin, nPins, serial);\n    }\n  }\n #else\n  static void create(VPIN f, HardwareSerial &s) { create(f, 1, s); }\n  static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) {\n    if (checkNoOverlap(firstVpin, nPins)) {\n      serial.begin(9600, SERIAL_8N1);\n      new DFPlayerSerial(firstVpin, nPins, serial);\n    }\n  }\n  #endif\n};\n#endif"
  },
  {
    "path": "IO_DFPlayerBase.h",
    "content": "/*\n * © 2026, Nicola Malavasi. All rights reserved.\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This file defines the Abstract Base Class (DFPlayerBase) for DFPlayer devices.\n * It manages the high-level logic, state machine, and command queuing, \n * independent of the physical transport layer.\n * * KEY FEATURES:\n * 1. Command Queue (FIFO): Manages a queue to buffer commands, ensuring \n * a 120ms delay between transmissions to prevent module lock-ups.\n * 2. Protocol Handling: Automatically calculates the 16-bit checksum and \n * constructs the 10-byte serial frames required by the DFPlayer hardware.\n * 3. Power-on Stabilization: Sends a reset command during initialization \n * to ensure the module is ready before receiving functional commands.\n * 4. Bidirectional Feedback: Parses incoming bytes from the module to detect \n * events like \"Track Finished\" (0x3D), enabling RepeatPlay logic and synchronization.\n * 5. EX-RAIL Integration: Maps DCC-EX internal opcodes (DF_PLAY, DF_VOL, etc.) \n * to physical hardware commands via the _writeAnalogue override.\n *\n * SD CARD FILENAME CONVENTION (Mandatory):\n * ---------------------------------------\n * To ensure compatibility with PLAYSOUND(vpin, track) and DF_FOLDER commands, \n * the MicroSD must be formatted in FAT32 and follow this naming convention:\n * * 1. FOLDER NAMING:\n * Folders must be named with a 2-digit prefix (01 to 99).\n * Example: /01, /02, /03...\n * * 2. FILE NAMING:\n * Files inside folders must start with a 3-digit prefix (001 to 255).\n * Example: /01/001.mp3, /01/002_Engine_Start.mp3\n * * 3. SPECIAL \"MP3\" FOLDER (Recommended for simple setups):\n * You can also use a folder named \"mp3\" (case insensitive) with \n * 4-digit filenames: /mp3/0001.mp3, /mp3/0002.mp3.\n * * IMPORTANT: The module often plays files based on their physical copy order.\n * It is recommended to clear the SD card and copy files in alphabetical order.\n */\n\n\n#ifndef IO_DFPlayerBase_h\n#define IO_DFPlayerBase_h\n\n#include \"IODevice.h\"\n#include \"DIAG.h\"\n\nclass DFPlayerBase : public IODevice {\npublic:\n    static const uint8_t DF_PLAY       = 0x0F; \n    static const uint8_t DF_STOPPLAY   = 0x16; \n    static const uint8_t DF_REPEATPLAY = 0x11; \n    static const uint8_t DF_FOLDER     = 0x17;\n    static const uint8_t DF_VOL        = 0x06;\n    static const uint8_t DF_EQ         = 0x07; \n    static const uint8_t DF_RESET      = 0x0C;\n    static const uint8_t DF_PAUSE      = 0x0E;\n    static const uint8_t DF_RESUME     = 0x0D;\n\n    static const uint8_t DF_EQ_NORMAL  = 0;\n    static const uint8_t DF_EQ_POP     = 1;\n    static const uint8_t DF_EQ_ROCK    = 2;\n    static const uint8_t DF_EQ_JAZZ    = 3;\n    static const uint8_t DF_EQ_CLASSIC = 4;\n    static const uint8_t DF_EQ_BASS    = 5;\n\nprotected:\n    volatile bool _playing = false;\n    bool _flagLoop = false;        \n    uint8_t _lastTrack = 1;\n    uint8_t _currentVolume = 20;\n    uint8_t _currentFolder = 1;    \n    uint8_t _inputIndex = 0;\n    uint8_t _recvCMD = 0; \n    unsigned long _lastXmit = 0;\n    unsigned long _initStartTime = 0;\n    unsigned long _unlockTimer = 0; \n    unsigned long _repeatTimer = 0;\n    bool _xtalChecked = false;\n\n    struct CommandEntry { uint8_t cmd; uint8_t a1; uint8_t a2; };\n    static const uint8_t Q_SIZE = 8; \n    CommandEntry _q[Q_SIZE];\n    uint8_t _head = 0; uint8_t _tail = 0;  \n\n    void queuePacket(uint8_t c, uint8_t a1 = 0, uint8_t a2 = 0) {\n        if (_deviceState == DEVSTATE_FAILED) return;\n        uint8_t n = (_head + 1) % Q_SIZE;\n        if (n != _tail) { \n            _q[_head] = {c, a1, a2}; \n            _head = n; \n        }\n    }\n\n    void forceUpdate(int val) {\n        this->IODevice::write(_firstVpin, val);\n    }\n\n    DFPlayerBase(VPIN firstVpin, int nPins=1): IODevice(firstVpin, nPins) {} \n\npublic:\n    void _begin() override { \n        if (_deviceState == DEVSTATE_FAILED) return;\n        _deviceState = DEVSTATE_INITIALISING; \n        _initStartTime = millis();\n        queuePacket(DF_RESET, 0, 0);\n    }\n\n    virtual void transmitCommandBuffer(const uint8_t buffer[], size_t bytes) = 0; \n    virtual bool processIncoming() = 0; \n    virtual void detectXtal() { } \n\n    void _loop(unsigned long currentMicros) override {\n        (void)currentMicros;\n        if (_deviceState == DEVSTATE_FAILED) return;\n\n        processIncoming(); \n        unsigned long now = millis();\n\n        if (_deviceState == DEVSTATE_INITIALISING && (now - _initStartTime > 3000)) {\n            if (!_xtalChecked) {\n                detectXtal();\n                _xtalChecked = true;\n                _deviceState = DEVSTATE_NORMAL;\n                queuePacket(DF_VOL, 0, _currentVolume);\n            }\n        }\n\n        if (_repeatTimer > 0 && now > _repeatTimer) {\n            _repeatTimer = 0;\n            queuePacket(DF_PLAY, _currentFolder, _lastTrack); \n        }\n        if (_unlockTimer > 0 && now > _unlockTimer) {\n            _unlockTimer = 0;\n            _playing = false;\n            forceUpdate(0); \n        }\n\n        if (_head != _tail && (now - _lastXmit > 120)) {\n            uint8_t out[] = {0x7E, 0xFF, 0x06, _q[_tail].cmd, 0x00, _q[_tail].a1, _q[_tail].a2, 0x00, 0x00, 0xEF};\n            int16_t sum = 0; for (int i = 1; i < 7; i++) sum -= out[i];\n            out[7] = (uint8_t)(sum >> 8); out[8] = (uint8_t)(sum & 0xff);\n            transmitCommandBuffer(out, 10);\n            _tail = (_tail + 1) % Q_SIZE;\n            _lastXmit = now;\n        }\n    }\n\n    void processIncomingByte(byte c) {\n        static const byte HDR[] = {0x7E, 0xFF, 0x06};\n        if (_inputIndex < 3) {\n            if (c == HDR[_inputIndex]) _inputIndex++; else _inputIndex = 0;\n            return;\n        }\n        if (_inputIndex == 3) _recvCMD = c;\n        if (_inputIndex == 6 && _recvCMD == 0x3D) { \n            if (_flagLoop) _repeatTimer = millis() + 600; \n            else _unlockTimer = millis() + 150;\n        }\n        _inputIndex++;\n        if (_inputIndex >= 10) _inputIndex = 0; \n    }\n\n    void _write(VPIN vpin, int value) override {\n        (void)vpin;\n        if (_deviceState != DEVSTATE_NORMAL) return; \n        _flagLoop = false; _repeatTimer = 0;\n        if (value) { \n            _playing = true; \n            _lastTrack = (uint8_t)(vpin - _firstVpin + 1);\n            queuePacket(DF_PLAY, _currentFolder, _lastTrack); \n        } else { \n            _unlockTimer = 0; _playing = false; \n            queuePacket(DF_STOPPLAY, 0, 0); \n        }\n    }\n\n    int _read(VPIN vpin) override { (void)vpin; return _playing ? 1 : 0; }\n\nprotected:\n    void _writeAnalogue(VPIN vpin, int v1, uint8_t v2=0, uint16_t cmd=0) override {\n        (void)vpin;\n        if (_deviceState != DEVSTATE_NORMAL) return;\n        switch (cmd){\n            case DF_REPEATPLAY:\n            case DF_PLAY:\n                _flagLoop = (cmd == DF_REPEATPLAY);\n                _repeatTimer = 0; _playing = true; _lastTrack = (uint8_t)v1;\n                if (v2 > 0 && v2 <= 30) { _currentVolume = v2; queuePacket(DF_VOL, 0x00, _currentVolume); }\n                queuePacket(DF_PLAY, _currentFolder, _lastTrack); \n                break;\n            case DF_FOLDER:\n                _flagLoop = false; _playing = true; _currentFolder = (uint8_t)v2; _lastTrack = (uint8_t)v1;\n                queuePacket(DF_FOLDER, _currentFolder, _lastTrack);\n                break;\n            case DF_STOPPLAY:\n                _flagLoop = false; _playing = false; _unlockTimer = 0; _repeatTimer = 0;\n                queuePacket(DF_STOPPLAY, 0, 0); forceUpdate(0);\n                break;\n            case DF_VOL: \n                _currentVolume = (v2 > 0) ? v2 : 15;\n                queuePacket(DF_VOL, 0x00, _currentVolume);\n                break;\n            case DF_EQ:\n                queuePacket(DF_EQ, 0x00, (uint8_t)v2);\n                break;\n            case DF_PAUSE:\n                _playing = false;\n                queuePacket(DF_PAUSE, 0, 0);\n                break;\n            case DF_RESUME:\n                _playing = true;\n                queuePacket(DF_RESUME, 0, 0);\n                break;\n            case DF_RESET:\n                _head = _tail = 0; queuePacket(DF_RESET, 0, 0);\n                _deviceState = DEVSTATE_INITIALISING; _initStartTime = millis(); _xtalChecked = false;\n                break;\n        }\n    }  \n};\n#endif\n"
  },
  {
    "path": "IO_DFPlayerI2C.h",
    "content": "/*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This file implements the I2C transport layer for the DFPlayer driver, \n * using the SC16IS750/752 I2C-to-UART bridge.\n * * KEY FEATURES:\n * 1. Register Management: Directly handles SC16IS75x internal registers (THR, RHR, LCR, etc.) \n * to configure UART parameters over the I2C bus.\n * 2. Dual-Channel Support: Uses the _UART_CH_BITS to switch between Channel A and Channel B \n * on dual-UART bridge chips (like the SC16IS752).\n * 3. Auto-Baud Detection: Automatically probes the bridge crystal frequency \n * (1.8MHz or 14.7MHz) to lock the output at 9600 baud without manual configuration.\n * 4. Transparent Communication: Wraps standard DFPlayer serial packets into \n * I2C messages for seamless integration with the base class logic.\n * 5. I2C Bus Diagnostics: Monitors device presence and reports status (OK/FAILED) \n * via the DIAG console, including the detected crystal frequency.\n */\n\n/*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n /*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n#ifndef IO_DFPlayerI2C_h\n#define IO_DFPlayerI2C_h\n\n#include \"IO_DFPlayerBase.h\"\n\n#define REG_THR    0x00 << 3\n#define REG_RHR    0x00 << 3\n#define REG_FCR    0x02 << 3\n#define REG_LCR    0x03 << 3\n#define REG_DLL    0x00 << 3\n#define REG_DLH    0x01 << 3\n#define REG_RXLVL  0x09 << 3\n\nclass DFPlayerI2C : public DFPlayerBase {\nprivate:\n    uint8_t _UART_CH_BITS;\n    unsigned long _xtal_freq;\n    bool _xtal_detected = false;\n\n    void writeRaw(uint8_t r, uint8_t v) {\n        uint8_t reg = (uint8_t)(r | _UART_CH_BITS);\n        uint8_t data[] = {reg, v}; \n        I2CManager.write(_I2CAddress, data, 2);\n    }\n\n    void setBaud(unsigned long hz) {\n        uint16_t div = (uint16_t)(hz / (9600UL * 16));\n        writeRaw(REG_LCR, 0x83);\n        writeRaw(REG_DLL, (uint8_t)(div & 0xFF));\n        writeRaw(REG_DLH, (uint8_t)(div >> 8));\n        writeRaw(REG_LCR, 0x03);\n        writeRaw(REG_FCR, 0x07);\n    }\n\npublic:\n    DFPlayerI2C(VPIN v, I2CAddress a, uint8_t ch) : DFPlayerBase(v) {\n        _I2CAddress = a;\n        _UART_CH_BITS = (ch << 1);\n        _xtal_freq = 1843200;\n        addDevice(this);\n    }\n\n    void _begin() override {\n        I2CManager.begin();\n        if (!I2CManager.exists(_I2CAddress)) {\n            _deviceState = DEVSTATE_FAILED;\n            DIAG(F(\"DFPlayer I2C Error: Bridge NOT found at %s\"), _I2CAddress.toString());\n            return;\n        }\n        setBaud(_xtal_freq);\n        DFPlayerBase::_begin();\n    }\n\n    void detectXtal() override {\n        if (_deviceState == DEVSTATE_FAILED) return;\n        unsigned long test_xtals[] = {1843200, 14745600};\n        uint8_t lvl_reg = (uint8_t)(REG_RXLVL | _UART_CH_BITS);\n        uint8_t rhr_reg = (uint8_t)(REG_RHR | _UART_CH_BITS);\n\n        for (int i=0; i<2; i++) {\n            setBaud(test_xtals[i]);\n            delay(50); \n            uint8_t avail=0, dummy;\n            I2CManager.read(_I2CAddress, &avail, 1, &lvl_reg, 1);\n            while(avail > 0) {\n                for(int j=0; j<avail; j++) I2CManager.read(_I2CAddress, &dummy, 1, &rhr_reg, 1);\n                I2CManager.read(_I2CAddress, &avail, 1, &lvl_reg, 1);\n            }\n            uint8_t q[] = {0x7E, 0xFF, 0x06, 0x42, 0x00, 0x00, 0x00, 0xFE, 0xB9, 0xEF};\n            transmitCommandBuffer(q, 10);\n            unsigned long start = millis();\n            while (millis() - start < 300) {\n                I2CManager.read(_I2CAddress, &avail, 1, &lvl_reg, 1);\n                if (avail >= 4) {\n                    uint8_t resp[4];\n                    for(int k=0; k<4; k++) I2CManager.read(_I2CAddress, &resp[k], 1, &rhr_reg, 1);\n                    if (resp[0] == 0x7E && resp[3] == 0x42) {\n                        _xtal_freq = test_xtals[i];\n                        _xtal_detected = true;\n                        _display();\n                        return;\n                    }\n                }\n                delay(5);\n            }\n        }\n        _display();\n    }\n\n    void _display() override {\n        DIAG(F(\"DFPlayer I2C (%s) Ch %c: Xtal %S MHz %S VPIN %d %S\"),\n             _I2CAddress.toString(), (_UART_CH_BITS == 0) ? 'A' : 'B',\n             (_xtal_freq > 2000000) ? F(\"14.7\") : F(\"1.8\"),\n             (_xtal_detected) ? F(\"(AUTO)\") : F(\"(TIMEOUT/MANUAL)\"),\n             _firstVpin, (_deviceState == DEVSTATE_FAILED) ? F(\"OFFLINE\"):F(\"\"));\n    }\n\n    void transmitCommandBuffer(const uint8_t b[], size_t s) override {\n        if (_deviceState == DEVSTATE_FAILED) return;\n        uint8_t pkt[s+1]; \n        pkt[0] = (uint8_t)(REG_THR | _UART_CH_BITS);\n        for(size_t i=0; i<s; i++) pkt[i+1] = b[i];\n        I2CManager.write(_I2CAddress, pkt, s + 1);\n    }\n\n    bool processIncoming() override {\n        if (_deviceState == DEVSTATE_FAILED) return false;\n        uint8_t lvl_reg = (uint8_t)(REG_RXLVL | _UART_CH_BITS);\n        uint8_t avail = 0;\n        if (I2CManager.read(_I2CAddress, &avail, 1, &lvl_reg, 1) != I2C_STATUS_OK) {\n            _deviceState = DEVSTATE_FAILED;\n            return false;\n        }\n        if (avail > 0) {\n            uint8_t rhr_reg = (uint8_t)(REG_RHR | _UART_CH_BITS);\n            for (uint8_t i=0; i<avail; i++) {\n                uint8_t b;\n                if (I2CManager.read(_I2CAddress, &b, 1, &rhr_reg, 1) == I2C_STATUS_OK) processIncomingByte(b);\n            }\n            return true;\n        }\n        return false;\n    }\n};\n\n#endif"
  },
  {
    "path": "IO_DFPlayerSerial.h",
    "content": "/*\n * © 2025, Nicola Malavasi. All rights reserved.\n * © 2023, Neil McKechnie. All rights reserved.\n * * This file is part of DCC-EX API\n *\n * This is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * It is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This file implements the Direct Hardware Serial transport layer for \n * the DFPlayer driver.\n * * KEY FEATURES:\n * 1. Hardware Serial Abstraction: Uses the Arduino HardwareSerial interface \n * to send and receive data directly via TX/RX pins.\n * 2. Low-Latency Transmission: Overrides transmitCommandBuffer to write \n * raw packets directly to the serial UART buffer.\n * 3. Asynchronous Receiving: Implements processIncoming using a non-blocking \n * while loop to drain the serial RX buffer and feed the base byte parser.\n * 4. Integration: Relies on the standard baud rate initialization (9600), \n * focusing strictly on high-speed data movement.\n * * TECHNICAL NOTE: \n * On STM32/Nucleo platforms, ensure the use of a 1k Ohm series resistor on \n * the DFPlayer RX line to mitigate signal noise and electrical mismatch.\n */\n\n\n#ifndef IO_DFPlayerSerial_h\n#define IO_DFPlayerSerial_h\n\n#include \"IO_DFPlayerBase.h\"\n\nclass DFPlayerSerial : public DFPlayerBase {\nprotected:\n    HardwareSerial *_serial;\n\npublic:\n    DFPlayerSerial(VPIN firstVpin, int nPins, HardwareSerial &s) : DFPlayerBase(firstVpin, nPins) {\n        _serial = &s;\n        addDevice(this);\n    }\n\n    void _begin() override {\n        // La seriale viene inizializzata nel bridge IO_DFPlayer.h\n        _deviceState = DEVSTATE_NORMAL;\n        _display();\n        DFPlayerBase::_begin();\n    }\n\n    // Implementazione obbligatoria del display per il log\n    void _display() override {\n        DIAG(F(\"DFPlayer Serial: Vpin %u %S\"), \n             (unsigned int)_firstVpin, \n             (_deviceState==DEVSTATE_FAILED) ? F(\"FAILED\") : F(\"OK\"));\n    }\n\n    void transmitCommandBuffer(const uint8_t buffer[], size_t bytes) override {\n        if (_deviceState == DEVSTATE_FAILED) return;\n        _serial->write(buffer, bytes);\n    }\n\n    bool processIncoming() override {\n        if (_deviceState == DEVSTATE_FAILED) return false;\n        \n        bool received = false;\n        while (_serial->available()) {\n            processIncomingByte(_serial->read());\n            received = true;\n        }\n        return received;\n    }\n};\n\n#endif"
  },
  {
    "path": "IO_DS1307.cpp",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_DS1307 device driver is used to interface a standalone realtime clock. \n* The clock will announce every minute (which will trigger EXRAIL ONTIME events).\n* Seconds, and Day/date info is ignored, except that the announced hhmm time\n* will attempt to synchronize with the 0 seconds of the clock. \n* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.\n* The clock can be easily set by an analog write to the vpin using 24 hr clock time\n* with the command <z vpin hh mm ss> \n*/\n\n#include \"IO_DS1307.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"CommandDistributor.h\"\n\nuint8_t d2b(uint8_t d) {\n     return (d >> 4)*10 + (d & 0x0F);  \n}\n\nvoid DS1307::create(VPIN vpin, I2CAddress i2cAddress) {\n    if (checkNoOverlap(vpin, 1, i2cAddress)) new DS1307(vpin, i2cAddress);\n  }\n \n    \n  // Constructor\n    DS1307::DS1307(VPIN vpin,I2CAddress i2cAddress){\n      _firstVpin = vpin;\n      _nPins = 1;\n     _I2CAddress = i2cAddress;\n     addDevice(this);\n    }\n\nuint32_t DS1307::getTime() {\n    // Obtain ss,mm,hh buffers from device\n    uint8_t readBuffer[3];\n    const uint8_t writeBuffer[1]={0};\n\n    // address register 0 for read. \n    I2CManager.write(_I2CAddress, writeBuffer, 1);\n    if (I2CManager.read(_I2CAddress, readBuffer, 3) != I2C_STATUS_OK) {\n       _deviceState=DEVSTATE_FAILED;\n       return 0;\n    } \n    _deviceState=DEVSTATE_NORMAL;\n\n    if (debug) {\n      static const char hexchars[]=\"0123456789ABCDEF\";\n      USB_SERIAL.print(F(\"<*RTC\"));\n        for (int i=2;i>=0;i--) {\n          USB_SERIAL.write(' ');  \n          USB_SERIAL.write(hexchars[readBuffer[i]>>4]);\n          USB_SERIAL.write(hexchars[readBuffer[i]& 0x0F ]);\n        }\n        StringFormatter::send(&USB_SERIAL,F(\" %d *>\\n\"),_deviceState);\n      }\n    \n    if (readBuffer[0] & 0x80) {\n        _deviceState=DEVSTATE_INITIALISING;\n        DIAG(F(\"DS1307 clock in standby\"));\n        return 0; // clock is not running\n    }\n    // convert device format to seconds since midnight\n    uint8_t ss=d2b(readBuffer[0]  & 0x7F);\n    uint8_t mm=d2b(readBuffer[1]);\n    uint8_t hh=d2b(readBuffer[2] & 0x3F);\n    return (hh*60ul +mm)*60ul +ss;     \n}\n\nvoid DS1307::_begin()  {\n  // Initialise  device and sync loop() to zero seconds\n    I2CManager.begin(); \n    auto tstamp=getTime();\n    if (_deviceState==DEVSTATE_NORMAL) {\n        byte seconds=tstamp%60;\n        delayUntil(micros() + ((60-seconds) * 1000000));\n    }\n    _display(); \n}\n\n// Processing loop to obtain clock time.\n// This self-synchronizes to the next minute tickover\nvoid DS1307::_loop(unsigned long currentMicros) {\n    byte ss=0; \n    auto time=getTime();\n    if (_deviceState==DEVSTATE_NORMAL) {\n       ss=time%60;\n       CommandDistributor::setClockTime(time/60, 1);      \n    }\n\n    // delay until next expected minute tickover,\n    // or 1 minute if clock not running\n    delayUntil(currentMicros + ((60-ss) * 1000000));  \n\n}\n\n\n  // Display device driver info.\n  void DS1307::_display()  {\n    auto tstamp=getTime();\n    byte ss=tstamp%60;\n    tstamp/=60;\n    byte mm=tstamp%60;\n    byte hh=tstamp/60;\n    DIAG(F(\"DS1307 on I2C:%s vpin %d %d:%d:%d %S\"), \n      _I2CAddress.toString(), _firstVpin,\n      hh,mm,ss,\n     (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n  // allow user to set the clock \n  void DS1307::_writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss)  {  \n    (void) vpin;\n    uint8_t writeBuffer[3];\n    writeBuffer[0]=1; // write mm,hh first \n    writeBuffer[1]=((mm/10)<<4) + (mm % 10);  \n    writeBuffer[2]=((hh/10)<<4) + (hh % 10);  \n    I2CManager.write(_I2CAddress, writeBuffer, 3);\n    writeBuffer[0]=0; // write ss  \n    writeBuffer[1]=((ss/10)<<4) + (ss % 10);  \n    I2CManager.write(_I2CAddress, writeBuffer, 2);\n    _loop(micros()); // resync with seconds rollover\n  }\n   \n  // Method to read analogue hh*60+mm time \n   int DS1307::_readAnalogue(VPIN vpin) { \n    (void)vpin; \n    return getTime()/60;\n  };\n\n"
  },
  {
    "path": "IO_DS1307.h",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_DS1307 device driver is used to interface a standalone realtime clock. \n* The clock will announce every minute (which will trigger EXRAIL ONTIME events).\n* Seconds, and Day/date info is ignored, except that the announced hhmm time\n* will attempt to synchronize with the 0 seconds of the clock. \n* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.\n* The clock can be easily set by an analog write to the vpin using 24 hr clock time\n* with the command <z vpin hh mm ss> \n*/\n\n#ifndef IO_DS1307_h\n#define IO_DS1307_h\n\n\n#include \"IODevice.h\"\n\nclass DS1307 : public IODevice {\npublic: \n  static const bool debug=false; \n  static void create(VPIN vpin, I2CAddress i2cAddress);\n \n    \nprivate:\n  \n  // Constructor\n    DS1307(VPIN vpin,I2CAddress i2cAddress);\n    uint32_t getTime();\n    void _begin() override;\n    void _display() override;\n    void _loop(unsigned long currentMicros) override;\n    int _readAnalogue(VPIN vpin) override;\n    void _writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss)  override;\n};\n \n#endif\n"
  },
  {
    "path": "IO_EXFastclock.h",
    "content": "/*\n *  © 2022, Colin Murdoch. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_EXFastclock device driver is used to interface the standalone fast clock and receive time data.\n*\n* The EX-fastClock code lives in a separate repo (https://github.com/DCC-EX/EX-Fastclock) and contains the clock logic.\n*\n*\n*/\n\n#ifndef IO_EXFastclock_h\n#define IO_EXFastclock_h\n\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"EXRAIL2.h\"\n#include \"CommandDistributor.h\"\n\nbool FAST_CLOCK_EXISTS = true;\n\nclass EXFastClock : public IODevice {\npublic:\n  // Constructor\n    EXFastClock(I2CAddress i2cAddress){\n    _I2CAddress = i2cAddress;\n    addDevice(this);\n  }\n\nstatic void create(I2CAddress i2cAddress) {\n\n  DIAG(F(\"Checking for Clock\"));\n  // Start by assuming we will find the clock\n  // Check if specified I2C address is responding (blocking operation)\n  // Returns I2C_STATUS_OK (0) if OK, or error code.\n  I2CManager.begin();\n  uint8_t _checkforclock = I2CManager.checkAddress(i2cAddress);\n  DIAG(F(\"Clock check result - %d\"), _checkforclock);\n  // XXXX change thistosave2 bytes\n  if (_checkforclock == 0) {\n      FAST_CLOCK_EXISTS = true;\n      //DIAG(F(\"I2C Fast Clock found at %s\"), i2cAddress.toString());\n      new EXFastClock(i2cAddress); \n    }\n    else {\n      FAST_CLOCK_EXISTS = false;\n      //DIAG(F(\"No Fast Clock found\"));\n      LCD(6,F(\"CLOCK NOT FOUND\"));\n    }\n    \n  }\n    \nprivate:\n  \n\n// Initialisation of Fastclock\nvoid _begin() override {\n  \n  if (FAST_CLOCK_EXISTS == true) {\n    I2CManager.begin();\n    if (I2CManager.exists(_I2CAddress)) {\n      _deviceState = DEVSTATE_NORMAL;\n      #ifdef DIAG_IO\n        _display();\n      #endif\n    } else {\n    _deviceState = DEVSTATE_FAILED;\n    //LCD(6,F(\"CLOCK NOT FOUND\")); \n    DIAG(F(\"Fast Clock Not Found at address %s\"), _I2CAddress.toString());\n    }\n  }\n}\n\n// Processing loop to obtain clock time\n\nvoid _loop(unsigned long currentMicros) override{ \n  \n  if (FAST_CLOCK_EXISTS==true) {\n      uint8_t readBuffer[3];\n      byte a,b;\n      #ifdef EXRAIL_ACTIVE\n        I2CManager.read(_I2CAddress, readBuffer, 3);\n        // XXXX change this to save a few bytes\n        a = readBuffer[0];\n        b = readBuffer[1];\n        //_clocktime = (a << 8) + b;\n        //_clockrate = readBuffer[2];\n\n        CommandDistributor::setClockTime(((a << 8) + b), readBuffer[2]);\n        //setClockTime(int16_t clocktime, int8_t clockrate);\n        \n        // As the minimum clock increment is 2 seconds delay a bit - say 1 sec.\n        // Clock interval is 60/ clockspeed i.e 60/b seconds\n        delayUntil(currentMicros + ((60/b) * 1000000));  \n     \n      #endif\n    \n  }\n}\n\n  // Display EX-FastClock device driver info.\n  void _display() override {\n    DIAG(F(\"FastCLock on I2C:%s - %S\"), _I2CAddress.toString(),  (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n  \n};\n\n#endif\n"
  },
  {
    "path": "IO_EXIOExpander.h",
    "content": "/*\n *  © 2022, Peter Cole. All rights reserved.\n *  © 2024, Harald Barth. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_EXIOExpander.h device driver integrates with one or more EX-IOExpander devices.\n* This device driver will configure the device on startup, along with\n* interacting with the device for all input/output duties.\n*\n* To create EX-IOExpander devices, these are defined in myAutomation.h:\n* (Note the device driver is included by default)\n*\n* HAL(EXIOExpander,800,18,0x65)\n* \n* All pins on an EX-IOExpander device are allocated according to the pin map for the specific\n* device in use. There is no way for the device driver to sanity check pins are used for the\n* correct purpose, however the EX-IOExpander device's pin map will prevent pins being used\n* incorrectly (eg. A6/7 on Nano cannot be used for digital input/output).\n*\n* The total number of pins cannot exceed 256 because of the communications packet format.\n* The number of analogue inputs cannot exceed 16 because of a limit on the maximum\n* I2C packet size of 32 bytes (in the Wire library).\n*/\n\n#ifndef IO_EX_IOEXPANDER_H\n#define IO_EX_IOEXPANDER_H\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for EX-IOExpander.\n */\nclass EXIOExpander : public IODevice {\npublic:\n\n  enum ProfileType : uint8_t {\n    Instant = 0,  // Moves immediately between positions (if duration not specified)\n    UseDuration = 0, // Use specified duration\n    Fast = 1,     // Takes around 500ms end-to-end\n    Medium = 2,   // 1 second end-to-end\n    Slow = 3,     // 2 seconds end-to-end\n    Bounce = 4,   // For semaphores/turnouts with a bit of bounce!!\n    NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.\n  };\n\n  static void create(VPIN vpin, int nPins, I2CAddress i2cAddress) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress);\n  }\n\nprivate:\n  // Constructor\n  EXIOExpander(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n    _firstVpin = firstVpin;\n    // Number of pins cannot exceed 256 (1 byte) because of I2C message structure.\n    if (nPins > 256) nPins = 256;\n    _nPins = nPins;\n    _I2CAddress = i2cAddress;\n    addDevice(this);\n  }\n\n  void _begin() {\n    uint8_t status;\n    // Initialise EX-IOExander device\n    I2CManager.begin();\n    if (I2CManager.exists(_I2CAddress)) {\n      // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline\n      // NB The I2C calls here are done as blocking calls, as they're not time-critical\n      // during initialisation and the reads require waiting for a response anyway.\n      // Hence we can allocate I/O buffers from the stack.\n      uint8_t receiveBuffer[3];\n      uint8_t commandBuffer[4] = {EXIOINIT, (uint8_t)_nPins, (uint8_t)(_firstVpin & 0xFF), (uint8_t)(_firstVpin >> 8)};\n      status = I2CManager.read(_I2CAddress, receiveBuffer, sizeof(receiveBuffer), commandBuffer, sizeof(commandBuffer));\n      if (status == I2C_STATUS_OK) {\n        if (receiveBuffer[0] == EXIOPINS) {\n          _numDigitalPins = receiveBuffer[1];\n          _numAnaloguePins = receiveBuffer[2];\n\n          // See if we already have suitable buffers assigned\n          if (_numDigitalPins>0) {\n            size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;\n            if (_digitalPinBytes < digitalBytesNeeded) {\n              // Not enough space, free any existing buffer and allocate a new one\n              if (_digitalPinBytes > 0) free(_digitalInputStates);\n              if ((_digitalInputStates = (byte*) calloc(digitalBytesNeeded, 1)) != NULL) {\n                _digitalPinBytes = digitalBytesNeeded;\n              } else {\n                DIAG(F(\"EX-IOExpander I2C:%s ERROR alloc %d bytes\"), _I2CAddress.toString(), digitalBytesNeeded);\n                _deviceState = DEVSTATE_FAILED;\n                _digitalPinBytes = 0;\n                return;\n              }\n            }\n          }\n          \n          if (_numAnaloguePins>0) {\n            size_t analogueBytesNeeded = _numAnaloguePins * 2;\n            if (_analoguePinBytes < analogueBytesNeeded) {\n              // Free any existing buffers and allocate new ones.\n              if (_analoguePinBytes > 0) {\n                free(_analogueInputBuffer);\n                free(_analogueInputStates);\n                free(_analoguePinMap);\n              }\n              _analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);\n              _analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);\n              _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);\n\t      if (_analogueInputStates  != NULL &&\n\t\t  _analogueInputBuffer != NULL &&\n\t\t  _analoguePinMap != NULL) {\n\t\t_analoguePinBytes = analogueBytesNeeded;\n\t      } else {\n\t\tDIAG(F(\"EX-IOExpander I2C:%s ERROR alloc analog pin bytes\"), _I2CAddress.toString());\n\t\t_deviceState = DEVSTATE_FAILED;\n\t\t_analoguePinBytes = 0;\n\t\treturn;\n\t      }\n            }\n          }\n        } else {\n          DIAG(F(\"EX-IOExpander I2C:%s ERROR configuring device\"), _I2CAddress.toString());\n          _deviceState = DEVSTATE_FAILED;\n          return;\n        }\n      } \n      // We now need to retrieve the analogue pin map if there are analogue pins\n      if (status == I2C_STATUS_OK && _numAnaloguePins>0) {\n        commandBuffer[0] = EXIOINITA;\n        status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1);\n      }\n      if (status == I2C_STATUS_OK) {\n        // Attempt to get version, if we don't get it, we don't care, don't go offline\n        uint8_t versionBuffer[3];\n        commandBuffer[0] = EXIOVER;\n        if (I2CManager.read(_I2CAddress, versionBuffer, sizeof(versionBuffer), commandBuffer, 1) == I2C_STATUS_OK) {\n          _majorVer = versionBuffer[0];\n          _minorVer = versionBuffer[1];\n          _patchVer = versionBuffer[2];\n        }\n        DIAG(F(\"EX-IOExpander device found, I2C:%s, Version v%d.%d.%d\"),\n            _I2CAddress.toString(), _majorVer, _minorVer, _patchVer);\n\n#ifdef DIAG_IO\n        _display();\n#endif\n      }\n      if (status != I2C_STATUS_OK)\n        reportError(status);\n\n    } else {\n      DIAG(F(\"EX-IOExpander I2C:%s device not found\"), _I2CAddress.toString());\n      _deviceState = DEVSTATE_FAILED;\n    }\n  }\n\n  // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested.\n  // Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can\n  // be allocated from the stack to reduce RAM allocation.\n  bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override {\n    if (paramCount != 1) return false;\n    int pin = vpin - _firstVpin;\n    if (configType == CONFIGURE_INPUT) {\n      uint8_t pullup = params[0];\n      uint8_t outBuffer[] = {EXIODPUP, (uint8_t)pin, pullup};\n      uint8_t responseBuffer[1];\n      uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer),\n                                outBuffer, sizeof(outBuffer));\n      if (status == I2C_STATUS_OK) {\n        if (responseBuffer[0] == EXIORDY) {\n          return true;\n        } else {\n          DIAG(F(\"EXIOVpin %u cannot be used as a digital input pin\"), (int)vpin);\n        }\n      } else\n        reportError(status);\n    } else if (configType == CONFIGURE_ANALOGINPUT) {\n      // TODO:  Consider moving code from _configureAnalogIn() to here and remove _configureAnalogIn\n      // from IODevice class definition.  Not urgent, but each virtual function defined\n      // means increasing the RAM requirement of every HAL device driver, whether it's relevant\n      // to the driver or not.\n      return false;\n    }\n    return false;\n  }\n\n  // Analogue input pin configuration, used to enable an EX-IOExpander device.\n  // Use I2C blocking calls and allocate buffers from stack to save RAM.\n  int _configureAnalogIn(VPIN vpin) override {\n    int pin = vpin - _firstVpin;\n    uint8_t commandBuffer[] = {EXIOENAN, (uint8_t)pin};\n    uint8_t responseBuffer[1];\n    uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer),\n                                  commandBuffer, sizeof(commandBuffer));\n    if (status == I2C_STATUS_OK) {\n      if (responseBuffer[0] == EXIORDY) {\n        return true;\n      } else {\n        DIAG(F(\"EX-IOExpander: Vpin %u cannot be used as an analogue input pin\"), (int)vpin);\n      }\n    } else\n      reportError(status);\n\n    return false;\n  }\n\n  // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads)\n  void _loop(unsigned long currentMicros) override {\n    if (_deviceState == DEVSTATE_FAILED) return;    // If device failed, return\n\n    // Request block is used for analogue and digital reads from the IOExpander, which are performed\n    // on a cyclic basis.  Writes are performed synchronously as and when requested.\n\n    if (_readState != RDS_IDLE) {\n      if (_i2crb.isBusy()) return;                // If I2C operation still in progress, return\n\n      uint8_t status = _i2crb.status;\n      if (status == I2C_STATUS_OK) {             // If device request ok, read input data\n\n        // First check if we need to process received data\n        if (_readState == RDS_ANALOGUE) {\n          // Read of analogue values was in progress, so process received values\n          // Here we need to copy the values from input buffer to the analogue value array.  We need to \n          // do this to avoid tearing of the values (i.e. one byte of a two-byte value being changed\n          // while the value is being read).\n          memcpy(_analogueInputStates, _analogueInputBuffer, _analoguePinBytes); // Copy I2C input buffer to states\n\n        } else if (_readState == RDS_DIGITAL) {\n          // Read of digital states was in progress, so process received values \n          // The received digital states are placed directly into the digital buffer on receipt, \n          // so don't need any further processing at this point (unless we want to check for\n          // changes and notify them to subscribers, to avoid the need for polling - see IO_GPIOBase.h).\n        }\n      } else\n        reportError(status, false);   // report eror but don't go offline.\n\n      _readState = RDS_IDLE;\n    }\n\n    // If we're not doing anything now, check to see if a new input transfer is due.\n    if (_readState == RDS_IDLE) {\n      if (_numDigitalPins>0 && currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh\n        // Issue new read request for digital states.  As the request is non-blocking, the buffer has to\n        // be allocated from heap (object state).\n        _readCommandBuffer[0] = EXIORDD;\n        I2CManager.read(_I2CAddress, _digitalInputStates, (_numDigitalPins+7)/8, _readCommandBuffer, 1, &_i2crb);\n                                                                // non-blocking read\n        _lastDigitalRead = currentMicros;\n        _readState = RDS_DIGITAL;\n      } else if (_numAnaloguePins>0 && currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh\n        // Issue new read for analogue input states\n        _readCommandBuffer[0] = EXIORDAN;\n        I2CManager.read(_I2CAddress, _analogueInputBuffer,\n            _numAnaloguePins * 2, _readCommandBuffer, 1, &_i2crb);\n        _lastAnalogueRead = currentMicros;\n        _readState = RDS_ANALOGUE;\n      }\n    }\n  }\n\n  // Obtain the correct analogue input value, with reference to the analogue\n  // pin map.  \n  // Obtain the correct analogue input value\n  int _readAnalogue(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED) return 0;\n    int pin = vpin - _firstVpin;\n    for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) {\n      if (_analoguePinMap[aPin] == pin) {\n        uint8_t _pinLSBByte = aPin * 2;\n        uint8_t _pinMSBByte = _pinLSBByte + 1;\n        return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte];\n      }\n    }\n    return -1;  // pin not found in table\n  }\n\n  // Obtain the correct digital input value\n  int _read(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED) return 0;\n    int pin = vpin - _firstVpin;\n    uint8_t pinByte = pin / 8;\n    bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8);\n    return value;\n  }\n\n  // Write digital value.  We could have an output buffer of states, that is periodically\n  // written to the device if there are any changes; this would reduce the I2C overhead\n  // if lots of output requests are being made.  We could also cache the last value \n  // sent so that we don't write the same value over and over to the output.  \n  // However, for the time being, we just write the current value (blocking I2C) to the\n  // IOExpander node.  As it is a blocking request, we can use buffers allocated from\n  // the stack to save RAM allocation.\n  void _write(VPIN vpin, int value) override {\n    uint8_t digitalOutBuffer[3];\n    uint8_t responseBuffer[1];\n    if (_deviceState == DEVSTATE_FAILED) return;\n    int pin = vpin - _firstVpin;\n    digitalOutBuffer[0] = EXIOWRD;\n    digitalOutBuffer[1] = pin;\n    digitalOutBuffer[2] = value;\n    uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, digitalOutBuffer, 3);\n    if (status != I2C_STATUS_OK) {\n      reportError(status);\n    } else {\n      if (responseBuffer[0] != EXIORDY) {\n        DIAG(F(\"Vpin %u cannot be used as a digital output pin\"), (int)vpin);\n      }\n    }\n  }\n\n  // Write analogue (integer) value.  Write the parameters (blocking I2C) to the\n  // IOExpander node.  As it is a blocking request, we can use buffers allocated from\n  // the stack to reduce RAM allocation.\n  void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {\n    uint8_t servoBuffer[7];\n    uint8_t responseBuffer[1];\n\n    if (_deviceState == DEVSTATE_FAILED) return;\n    int pin = vpin - _firstVpin;\n#ifdef DIAG_IO\n    DIAG(F(\"Servo: WriteAnalogue Vpin:%u Value:%d Profile:%d Duration:%d %S\"), \n      vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F(\"DEVSTATE_FAILED\"):F(\"\"));\n#endif\n    servoBuffer[0] = EXIOWRAN;\n    servoBuffer[1] = pin;\n    servoBuffer[2] = value & 0xFF;\n    servoBuffer[3] = value >> 8;\n    servoBuffer[4] = profile;\n    servoBuffer[5] = duration & 0xFF;\n    servoBuffer[6] = duration >> 8;\n    uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, servoBuffer, 7);\n    if (status != I2C_STATUS_OK) {\n      DIAG(F(\"EX-IOExpander I2C:%s Error:%d %S\"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));\n      _deviceState = DEVSTATE_FAILED;\n    } else {\n      if (responseBuffer[0] != EXIORDY) {\n        DIAG(F(\"Vpin %u cannot be used as a servo/PWM pin\"), (int)vpin);\n      }\n    }\n  }\n\n  // Display device information and status.\n  void _display() override {\n    DIAG(F(\"EX-IOExpander I2C:%s v%d.%d.%d Vpins %u-%u %S\"),\n              _I2CAddress.toString(), _majorVer, _minorVer, _patchVer,\n              (int)_firstVpin, (int)_firstVpin+_nPins-1,\n              _deviceState == DEVSTATE_FAILED ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n  // Helper function for error handling\n  void reportError(uint8_t status, bool fail=true) {\n    DIAG(F(\"EX-IOExpander I2C:%s Error:%d (%S)\"), _I2CAddress.toString(), \n      status, I2CManager.getErrorMessage(status));\n    if (fail)\n    _deviceState = DEVSTATE_FAILED;\n  }\n\n  uint8_t _numDigitalPins = 0;\n  uint8_t _numAnaloguePins = 0;\n\n  uint8_t _majorVer = 0;\n  uint8_t _minorVer = 0;\n  uint8_t _patchVer = 0;\n\n  uint8_t* _digitalInputStates  = NULL;\n  uint8_t* _analogueInputStates = NULL;\n  uint8_t* _analogueInputBuffer = NULL;  // buffer for I2C input transfers\n  uint8_t _readCommandBuffer[1];\n\n  uint8_t _digitalPinBytes = 0;   // Size of allocated memory buffer (may be longer than needed)\n  uint8_t _analoguePinBytes = 0;  // Size of allocated memory buffer (may be longer than needed)\n  uint8_t* _analoguePinMap = NULL;\n  I2CRB _i2crb;\n\n  enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE};  // Read operation states\n  uint8_t _readState = RDS_IDLE;\n  \n  unsigned long _lastDigitalRead = 0;\n  unsigned long _lastAnalogueRead = 0;\n  const unsigned long _digitalRefresh = 10000UL;    // Delay refreshing digital inputs for 10ms\n  const unsigned long _analogueRefresh = 50000UL;   // Delay refreshing analogue inputs for 50ms\n\n  // EX-IOExpander protocol flags\n  enum {\n    EXIOINIT = 0xE0,    // Flag to initialise setup procedure\n    EXIORDY = 0xE1,     // Flag we have completed setup procedure, also for EX-IO to ACK setup\n    EXIODPUP = 0xE2,    // Flag we're sending digital pin pullup configuration\n    EXIOVER = 0xE3,     // Flag to get version\n    EXIORDAN = 0xE4,    // Flag to read an analogue input\n    EXIOWRD = 0xE5,     // Flag for digital write\n    EXIORDD = 0xE6,     // Flag to read digital input\n    EXIOENAN = 0xE7,    // Flag to enable an analogue pin\n    EXIOINITA = 0xE8,   // Flag we're receiving analogue pin mappings\n    EXIOPINS = 0xE9,    // Flag we're receiving pin counts for buffers\n    EXIOWRAN = 0xEA,   // Flag we're sending an analogue write (PWM)\n    EXIOERR = 0xEF,     // Flag we've received an error\n  };\n};\n\n#endif\n"
  },
  {
    "path": "IO_EXSensorCAM.h",
    "content": "/*  2024/08/14 \n *  © 2024, Barry Daniel ESP32-CAM revision \n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n#define driverVer 309 //devel 5.5.15+ only - specifically for Chris' mod\n// v309 includes changes for Chris' mod addVpin() Specifically for 5.5.15+ method with CAM v320+\n// v308 allows v0 to give version without using '^'. CAM address identifier added to 'm'\n//   \"   added check in Pkt[31] & shared inputBuf[32] \"ACK OK\" & case:'v'. Shows <> formats\n//   \"   added setClock as per 307 devel version  Note: v308 for 5.4.6+ Prod. only with CAM v320+\n// v307 fix to 'p' cmd & CS cmd prompts. includes setClock(100000);\n// v306 Pass vpin to regeister it in CamParser.  Incompatible with 5.4.0+\n//   \"  Move base vpin to camparser. devel 5.5.15+ only\n// v305 less debug & alpha ordered switch\n// v304 static oldb0;  t(##[,%%]); \n// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)\n// v302 SEND=StringFormatter::send, remove Sp(), add 'q', memcpy( .8) -> .7); \n// v301 improved 'f','p'&'q' code and driver version calc. Correct bsNo calc. for 'a'\t\t\t\t\t\t\t \n// v300 stripped & revised without expander functionality. Needs sensorCAM.h v300 AND CamParser.cpp\n// v200 rewrite reduces need for double reads of ESP32 slave CAM. Deleted ESP32CAP. \n//  Inompatible with pre-v170 sensorCAM, unless set S06 to 0 and S07 to 1 (o06 & l07 say)\n/*\n * The IO_EXSensorCAM.h device driver can integrate with the sensorCAM device.\n * It is modelled on the IO_EXIOExpander.h device driver to include specific needs of the ESP32 sensorCAM \n * This device driver will configure the device on startup, along with CamParser.cpp\n * interacting with the sensorCAM device for all input/output duties.\n *\n * includes not required with devel 5.5.15+\t\t\t\t\t\t\t\t\t\t\t\t \n * To create EX-SensorCAM devices, \n *  use HAL(EXSensorCAM, baseVpin, numpins, i2c_address) in myAutomation.h\n * e.g.  \n *   HAL(EXSensorCAM,700, 80, 0x11)\n * \n * or (deprecated) define them in myHal.cpp: with\n * EXSensorCAM::create(baseVpin,num_vpins,i2c_address);\n * \n * I2C packet size of 32 bytes (in the Wire library).\n*/\n#define DIGITALREFRESH 20000UL      // min uSec delay between digital reads of digitalInputStates\n#ifndef IO_EX_EXSENSORCAM_H\n#define IO_EX_EXSENSORCAM_H\n#define SEND StringFormatter::send\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"FSH.h\"\n#include \"CamParser.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for EX-SensorCAM.\n*/\nclass EXSensorCAM : public IODevice {\n  public:\n    static void create(VPIN vpin, int nPins, I2CAddress i2cAddress) {\n      if (checkNoOverlap(vpin, nPins, i2cAddress)) \n      new EXSensorCAM(vpin, nPins, i2cAddress);\n    }\n  \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n    \t\t\t\t\t   \n\n  private:\n   // Constructor\n    EXSensorCAM(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n      _firstVpin = firstVpin;\n     // Number of pins cannot exceed 255 (1 byte) because of I2C message structure.\n      if (nPins > 80) nPins = 80;\n      _nPins = nPins;\n      _I2CAddress = i2cAddress;\n      addDevice(this);\n      CamParser::addVpin(firstVpin); \n    }\n//*************************\nvoid _begin() {\n    uint8_t status;\n    // Initialise EX-SensorCAM device\n    I2CManager.begin();\n    I2CManager.setClock(100000);  // Set speed for I2C operations\n    if (!I2CManager.exists(_I2CAddress)) {\n      DIAG(F(\"EX-SensorCAM I2C:%s device not found\"), _I2CAddress.toString());\n      _deviceState = DEVSTATE_FAILED;\n      return;\n    }else {\n      uint8_t commandBuffer[4]={EXIOINIT,(uint8_t)_nPins,(uint8_t)(_firstVpin & 0xFF),(uint8_t)(_firstVpin>>8)};                                                                                 \n      status = I2CManager.read(_I2CAddress,_inputBuf,sizeof(_inputBuf),commandBuffer,sizeof(commandBuffer));\n        //EXIOINIT needed to trigger and send firstVpin to CAM\n\n      if (status == I2C_STATUS_OK) {\n        // Attempt to get version, non-blocking results in poor placement of response.  Can be blocking here! \n        commandBuffer[0] = '^';    //new version code\n    \n        status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1); \n         // for ESP32 CAM, read again for good immediate response version data\n        status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1);\n        \n        if (status == I2C_STATUS_OK) {\n          _majorVer= _inputBuf[1]/10;\t\n          _minorVer= _inputBuf[1]%10;\n          _patchVer= _inputBuf[2];\t\t\t\t\n          DIAG(F(\"EX-SensorCAM device found, I2C:%s, Version v%d.%d.%d\"),\n                       _I2CAddress.toString(),_majorVer, _minorVer,_patchVer);\n        }  \t\n      }\n      if (status != I2C_STATUS_OK)\n        reportError(status);\n    } \n}\n//*************************\n// Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested.\n// Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can\n// be allocated from the stack to reduce RAM allocation.\nbool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { \n  (void)configType; (void)params; // unused\n  if(_verPrint) DIAG(F(\"_configure() driver IO_EXSensorCAM v0.%d.%d vpin: %d \"), driverVer/100,driverVer%100,vpin);\n  _verPrint=false;           //only give driver versions once\n  if (paramCount != 1) return false;\n  return true; //at least confirm that CAM is (always) configured (no vpin check!)\n}\n//*************************\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t \n// Analogue input pin configuration, used to enable an EX-IOExpander device.\nint _configureAnalogIn(VPIN vpin) override { \n  (void)vpin;  //  DIAG(F(\"_configureAnalogIn() IO_EXSensorCAM vpin %d\"),vpin); \n  return true;     // NOTE: use of EXRAIL IFGTE() etc use \"analog\" reads.\n}\n//*************************\n// Main loop, collect both digital and \"analog\" pin states continuously (faster sensor/input reads)\nvoid _loop(unsigned long currentMicros) override {\n    if (_deviceState == DEVSTATE_FAILED) return;    \n      // Request block is used for \"analogue\" (cmd. data) and digital reads from the sensorCAM, which \n      // are performed on a cyclic basis.  Writes are performed synchronously as and when requested.\n    if (_readState != RDS_IDLE) {                  //expecting a return packet\n      if (_i2crb.isBusy()) return;                 // If I2C operation still in progress, return\n      uint8_t status = _i2crb.status;\n      if (status == I2C_STATUS_OK) {               // If device request ok, read input data\n        //apparently the above checks do not guarantee a good packet! error rate about 1 pkt per 1000\n        //there should be a packet in _inputBuf[32]            \n        if ((_inputBuf[0] & 0x60) >= 0x60) {   //Buff[0] seems to have ascii cmd header (bit6 high) (o06)    \n            int error = processIncomingPkt( _inputBuf, _inputBuf[0]);   // '~' 'i' 'm' 'n' 't' etc\n              if (error>0) DIAG(F(\"CAM packet header(0x%x) not recognised\"),_inputBuf[0]);                   \n        }else{ // Header not valid - typically replaced by bank 0 data!  To avoid any bad responses set S06 to 0  \n               // Versions of sensorCAM.h after v300 should return header for '@' of '`'(0x60) (not 0xE6)  \n               // followed by digitalInputStates sensor state array\n        }\n      }else   reportError(status, false);   // report i2c eror but don't go offline.\n      _readState = RDS_IDLE;\n    }      \n  \n    // If we're not doing anything now, check to see if a new state table transfer, or for 't' repeat, is due.\n    if (_readState == RDS_IDLE) {    //check if time for digitalRefresh   \n      if ( currentMicros - _lastDigitalRead > _digitalRefresh) { \n        // Issue new read request for digital states.  \n             \n        _readCommandBuffer[0] = '@';  //start new read of digitalInputStates Table     // non-blocking read \n        I2CManager.read(_I2CAddress,_inputBuf, sizeof(_inputBuf),_readCommandBuffer, 1, &_i2crb);     \n        _lastDigitalRead = currentMicros;\n        _readState = RDS_DIGITAL;\n        \n      }else{    //slip in a repeat <NT n> if pending\n        if (currentMicros - _lasttStateRead > _tStateRefresh)  // Delay for \"analog\" command repetitions\n         if (_savedCmd[2]>1) {   //repeat a 't' command         \n          for (int i=0;i<7;i++)  _readCommandBuffer[i] =_savedCmd[i];\n          int errors = ioESP32(_I2CAddress, _inputBuf, sizeof(_inputBuf), _readCommandBuffer, 7);    \n          _lasttStateRead = currentMicros;\n          _savedCmd[2] -= 1;     //decrement repeats                  \n          if (errors==0) return;\n          DIAG(F(\"ioESP32 error %d header 0x%x\"),errors,_inputBuf[0]);  \n          _readState = RDS_TSTATE;  //this should stop further cmd requests until packet read (or timeout)\n        }\n      }   //end repeat 't'\n    }\n  }   \n//*************************\n// Obtain the bank of 8 sensors as an \"analog\" value\n// can be used to track the position through a sequential sensor bank\nint _readAnalogue(VPIN vpin) override {\n  if (_deviceState == DEVSTATE_FAILED) return 0;        \n  return _digitalInputStates[(vpin - _firstVpin) / 8];\n}\n//*************************\n// Obtain the correct digital sensor input value\nint _read(VPIN vpin) override {\n  if (_deviceState == DEVSTATE_FAILED) return 0;\n  int pin = vpin - _firstVpin;\n  return bitRead(_digitalInputStates[pin / 8], pin % 8);\n}\n//*************************\n// Write digital value.  \nvoid _write(VPIN vpin, int value) override { \n  DIAG(F(\"**_write() vpin %d = %d\"),vpin,value);\n  return ;\n}\n//*************************\n// i2cAddr of ESP32 CAM\n// rBuf   buffer for return packet \n// inbytes number of bytes to request from CAM\n// outBuff holds outbytes to be sent to CAM  \nint ioESP32(uint8_t i2cAddr,uint8_t *rBuf,int inbytes,uint8_t *outBuff,int outbytes) {\n  uint8_t status = _i2crb.status;\n\n while( _i2crb.status != I2C_STATUS_OK){status = _i2crb.status;}   //wait until bus free\n\n  status = I2CManager.read(i2cAddr, rBuf, inbytes, outBuff, outbytes);\n        \n  if (status != I2C_STATUS_OK){ \n    DIAG(F(\"EX-SensorCAM I2C:%s Error:%d %S\"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));\n    reportError(status); return status;\n  }\n  return 0;  // 0 for no error != 0 for error number.\n}\n//*************************\n//function to interpret packet from sensorCAM.ino\n//i2cAddr to identify CAM# (if # >1)\n//rBuf contains packet of up to 32 bytes usually with (ascii) cmd header in rBuf[0] & check byte in [31]\n//sensorCmd command header byte from CAM (in rBuf[0]?)\nint processIncomingPkt(uint8_t *rBuf,uint8_t sensorCmd) {\n//static uint8_t oldb0;   //for debug only\n  int k; \n  int b;\n  char str[] = \"11111111\";\n\n//\n  if(sensorCmd != (rBuf[31] | 0x20)) {\n#ifdef BADPKTS\n    Serial.print(\"mismatch31: \"); Serial.print((char)sensorCmd);Serial.println(rBuf[31],HEX); \n#endif\n    return 0;\n  }\n\n  switch (sensorCmd){\n    case '`':      //response to request for digitalInputStates[] table  '@'=>'`'  \n      memcpy(_digitalInputStates, rBuf+1, digitalBytesNeeded);\n      break;                                                 \n\n    case EXIORDY:  //some commands give back acknowledgement only\n      SEND(&USB_SERIAL,F(\"<n ACK OK n>\\n\"));\n      break;\n\n    case CAMERR:   //cmd format error code from CAM\n      DIAG(F(\"CAM cmd error 0xFE 0x%x\"),rBuf[1]); \n      break;\n\n    case 'v':\n    case '~':      //information from 'v/^' version request <N v[er]>\n      DIAG(F(\"EX-SensorCAM device found, I2C:%s,CAM Version v%d.%d.%d vpins %u-%u\"),\n              _I2CAddress.toString(), rBuf[1]/10, rBuf[1]%10, rBuf[2],(int) _firstVpin, (int) _firstVpin +_nPins-1);\n      DIAG(F(\"IO_EXSensorCAM driver  v0.%d.%d vpin: %d \"), driverVer/100,driverVer%100,_firstVpin);\n      break;\n \n    case 'f':\n      DIAG(F(\"<Nf %%%%> frame header 'f' for bsNo %d/%d - showing Quarter sample (1 row) only\"), rBuf[1]/8,rBuf[1]%8);  \n      SEND(&USB_SERIAL,F(\"<n  row: %d  Ref bytes: \"),rBuf[2]);\n      for(k=3;k<15;k++)\n        SEND(&USB_SERIAL,F(\"%x%x%s\"), rBuf[k]>>4, rBuf[k]&15, k%3==2 ? \"  \" : \" \"); \n      Serial.print(\" latest grab: \"); \n      for(k=16;k<28;k++)\n        SEND(&USB_SERIAL,F(\"%x%x%s\"), rBuf[k]>>4, rBuf[k]&15, (k%3==0) ? \"  \" : \" \");\n      Serial.print(\" n>\\n\");\n      break; \n\n    case 'i':      //information from i%%\n      k=256*rBuf[5]+rBuf[4];\n      DIAG(F(\"<Ni %%%%[ $$]> Info: Sensor 0%o(%d) enabled:%d status:%d row=%d x=%d Twin=0%o pvtThreshold=%d A~%d\")\n              ,rBuf[1],rBuf[1],rBuf[3],rBuf[2],rBuf[6],k,rBuf[7],rBuf[9],int(rBuf[8])*16);\t \n      break;\n\n    case 'm':\n      DIAG(F(\"%s<Nm $[ ##]> Min/max: $ frames min2flip (trip) %d, maxSensors 0%o, minSensors 0%o, nLED %d, thres\"\n            \"hold %d, TWOIMAGE_MAXBS 0%o\"),_I2CAddress.toString(),rBuf[1],rBuf[3],rBuf[2],rBuf[4],rBuf[5],rBuf[6]);\n      break;\n\n    case 'n':\n      DIAG(F(\"<Nn $[ ##]> Numerate: $ nLED %d, ## minSensors 0%o (maxSensors 0%o threshold %d)\")\n                                       ,rBuf[4],rBuf[2],rBuf[3],rBuf[5]);                                                               \n      break;\n\n    case 'p':\n      b=rBuf[1]-2;  \n      if(b<4) { Serial.print(\"<n <Np %%> Bank empty  n>\\n\"); break; }\n      SEND(&USB_SERIAL,F(\"<n <Np %%> Bank: %d \"),(0x7F&rBuf[2])/8);\n      for (int j=2; j<b; j+=3)  \n        SEND(&USB_SERIAL,F(\" S%d%d: r=%d x=%d\"),(0x7F&rBuf[j])/8,(0x7F&rBuf[j])%8,rBuf[j+1],rBuf[j+2]+2*(rBuf[j]&0x80));\n      Serial.print(\"  n>\\n\");\n      break;\n\n    case 'q':\n      for (int i =0; i<8; i++) str[i] = ((rBuf[2] << i) & 0x80 ? '1' : '0');\n      DIAG(F(\"<Nq $> Query bank %c ENABLED sensors(S%c7-%c0): %s \"), rBuf[1], rBuf[1], rBuf[1], str);\n      break;\n\n    case 't':      //threshold etc. from t##           //bad pkt if 't' FF's\n      if(rBuf[1]==0xFF) {Serial.println(\"<n bad CAM 't' packet: 74 FF  n>\");_savedCmd[2] +=1; return 0;}\n      SEND(&USB_SERIAL,F(\"<n <Nt[ ##[ %%%%]]> Threshold:%d sensor S00:-%d\"),rBuf[1],min(rBuf[2]&0x7F,99));\n      if(rBuf[2]>127) Serial.print(\"##* \"); \n      else{ \n        if(rBuf[2]>rBuf[1]) Serial.print(\"-?* \"); \n        else Serial.print(\"--* \");\n      }\n      for(int i=3;i<31;i+=2){\n        uint8_t valu=rBuf[i];        //get bsn\n        if(valu==80) break;          //80 = end flag\n        else{ \n          SEND(&USB_SERIAL,F(\"%d%d:\"), (valu&0x7F)/8,(valu&0x7F)%8);\n          if(valu>=128) Serial.print(\"?-\"); \n          else {if(rBuf[i+1]>=128) Serial.print(\"oo\");else Serial.print(\"--\");}\n          valu=rBuf[i+1]; \n          SEND(&USB_SERIAL,F(\"%d%s\"),min(valu&0x7F,99),(valu<128) ? \"--* \":\"##* \");\n        }\n      } \n      Serial.print(\" n>\\n\");\n      break;\n\n    default:        //header not a recognised cmd character\n      DIAG(F(\"CAM packet header not valid (0x%x) (0x%x) (0x%x)\"),rBuf[0],rBuf[1],rBuf[2]);\n      return 1;\n  }\t\t\t \n  return 0;  \n}\n//*************************\t\t\t\t\t\t\t\t\t\t\t  \n// Write (analogue) 8bit (command) values.  Write the parameters to the sensorCAM\nvoid _writeAnalogue(VPIN vpin, int param1, uint8_t camop, uint16_t param3) override {\n    uint8_t outputBuffer[7];\n    int errors=0;\n    outputBuffer[0] = camop;  \n    int pin = vpin - _firstVpin;\n\n    if(camop >= 0x80) {   //case \"a\" (4p) also (3p) e.g. <N 713 210 310> \n      camop=param1;        //put row (0-236) in expected place \n      param1=param3;        //put column in expected place\n      outputBuffer[0] = 'A';\n      pin = (pin/8)*10 + pin%8;   //restore bsNo. as integer\n    }   \n    if (_deviceState == DEVSTATE_FAILED) return;\n   \n    outputBuffer[1] = pin;          //vpin => bsn\n    outputBuffer[2] = param1 & 0xFF;\n    outputBuffer[3] = param1 >> 8;\n    outputBuffer[4] = camop;        //command code\n    outputBuffer[5] = param3 & 0xFF;\n    outputBuffer[6] = param3 >> 8;\n\n    int count=param1+1;\n    if(camop=='Q'){\n      if(param3<=10) {count=param3; camop='B';}\n      //if(param1<10) outputBuffer[2] = param1*10;\n    }\n    if(camop=='B'){   //then 'b'(b%) cmd - can totally deal with that here. (but can't do b%,# (brightSF))\n      if(param1>97) return;\n      if(param1>9) param1 = param1/10;  //accept a bsNo\n      for(int bnk=param1;bnk<count;bnk++) {\n        uint8_t b=_digitalInputStates[bnk];\n        char str[] = \"11111111\";\n        for (int i=0;i<8;i++) if(((b<<i)&0x80) == 0) str[i]='0';\n        DIAG(F(\"<Nb $> Bank: %d activated byte: 0x%x%x (sensors S%d7->%d0) %s\"), bnk,b>>4,b&15,bnk,bnk,str ); \n      }\n      return;\n    } \n    if (outputBuffer[4]=='T') {   //then 't' cmd\n      if(param1<31) {   //repeated calls if param < 31\n          //for (int i=0;i<7;i++) _savedCmd[i]=outputBuffer[i];\n        memcpy( _savedCmd, outputBuffer, 7);\n      }else _savedCmd[2] = 0;   //no repeats if ##>30 \n    }else _savedCmd[2] = 0;       //no repeats unless 't'\n  \n    _lasttStateRead = micros();    //don't repeat until _tStateRefresh mSec\n\n    errors = ioESP32(_I2CAddress, _inputBuf, sizeof(_inputBuf) , outputBuffer, 7);  //send to esp32-CAM\n    if (errors==0) return;\n    else {    //       if (_inputBuf[0] != EXIORDY)   //can't be sure what is inBuff[0] !\n      DIAG(F(\"ioESP32 i2c error %d header 0x%x\"),errors,_inputBuf[0]);  \n    }\n}\n//*************************\n // Display device information and status.\n  void _display() override {\n    DIAG(F(\"EX-SensorCAM I2C:%s v%d.%d.%d Vpins %u-%u %S\"),\n              _I2CAddress.toString(), _majorVer, _minorVer, _patchVer,\n              (int)_firstVpin, (int)_firstVpin+_nPins-1,\n              _deviceState == DEVSTATE_FAILED ? F(\"OFFLINE\") : F(\"\"));\n  }\n//*************************\n// Helper function for error handling\nvoid reportError(uint8_t status, bool fail=true) {\n  DIAG(F(\"EX-SensorCAM I2C:%s Error:%d (%S)\"), _I2CAddress.toString(), \n        status, I2CManager.getErrorMessage(status));\n  if (fail)  _deviceState = DEVSTATE_FAILED;\n}\n//************************* \n  uint8_t _numDigitalPins = 80;   \n  size_t  digitalBytesNeeded=10;\n  uint8_t _inputBuf[32];\n  \n  uint8_t _majorVer = 0;\n  uint8_t _minorVer = 0;\n  uint8_t _patchVer = 0;\n\n  uint8_t _digitalInputStates[10];\n  I2CRB _i2crb;\n \n  byte _outputBuffer[8];\n\n  bool    _verPrint=true;\n\n  uint8_t _readCommandBuffer[8];\n  uint8_t _savedCmd[8];           //for repeat 't' command\n  //uint8_t _digitalPinBytes = 10;  // Size of allocated memory buffer (may be longer than needed)\n\n  enum {RDS_IDLE, RDS_DIGITAL, RDS_TSTATE};  // Read operation states\n  uint8_t _readState = RDS_IDLE;\n  //uint8_t cmdBuffer[7]={0,0,0,0,0,0,0};\n  unsigned long _lastDigitalRead = 0;\n  unsigned long _lasttStateRead = 0;\n  unsigned long _digitalRefresh = DIGITALREFRESH;  // Delay refreshing digital inputs for 10ms\n  const unsigned long _tStateRefresh = 120000UL;   // Delay refreshing repeat \"tState\" inputs\n\n  enum {\n    EXIOINIT = 0xE0,    // Flag to initialise setup procedure\n    EXIORDY = 0xE1,     // Flag we have completed setup procedure, also for EX-IO to ACK setup\n    CAMERR = 0xFE\n  };\n};\n#endif\n"
  },
  {
    "path": "IO_EXTurntable.cpp",
    "content": "/*\n *  © 2021, Peter Cole. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C.\n*\n* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/EX-Turntable) and contains the stepper motor logic.\n*\n* This device driver sends a step position to EX-Turntable to indicate the step position to move to using either of these commands:\n* <D TT vpin steps activity> in the serial console\n* MOVETT(vpin, steps, activity) in EX-RAIL\n* Refer to the documentation for further information including the valid activities.\n*/\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"Turntables.h\"\n#include \"CommandDistributor.h\"\n\n#ifndef IO_NO_HAL\n\nvoid EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {\n  new EXTurntable(firstVpin, nPins, I2CAddress);\n}\n\n// Constructor\nEXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {\n  _firstVpin = firstVpin;\n  _nPins = nPins;\n  _I2CAddress = I2CAddress;\n  _stepperStatus = 0;\n  _previousStatus = 0;\n  addDevice(this);\n}\n\n// Initialisation of EXTurntable\nvoid EXTurntable::_begin() {\n  I2CManager.begin();\n  if (I2CManager.exists(_I2CAddress)) {\n    DIAG(F(\"EX-Turntable device found, I2C:%s\"), _I2CAddress.toString());\n#ifdef DIAG_IO\n    _display();\n#endif\n  } else {\n    DIAG(F(\"EX-Turntable I2C:%s device not found\"), _I2CAddress.toString());\n    _deviceState = DEVSTATE_FAILED;\n  }\n}\n\n// Processing loop to obtain status of stepper\n// 0 = finished moving and in correct position\n// 1 = still moving\nvoid EXTurntable::_loop(unsigned long currentMicros) {\n  uint8_t readBuffer[1];\n  I2CManager.read(_I2CAddress, readBuffer, 1);\n  _stepperStatus = readBuffer[0];\n  if (_stepperStatus != _previousStatus && _stepperStatus == 0) { // Broadcast when a rotation finishes\n    if ( _currentActivity < 4) {\n      _broadcastStatus(_firstVpin, _stepperStatus, _currentActivity);\n    }\n    _previousStatus = _stepperStatus;\n  }\n  delayUntil(currentMicros + 100000);  // Wait 100ms before checking again\n}\n\n// Read returns status as obtained in our loop.\n// Return false if our status value is invalid.\nint EXTurntable::_read(VPIN vpin) {\n  (void)vpin; // surpress warning\n  if (_deviceState == DEVSTATE_FAILED) return 0;\n  if (_stepperStatus > 1) {\n    return false;\n  } else {\n    return _stepperStatus;\n  }\n}\n\n// If a status change has occurred for a turntable object, broadcast it\nvoid EXTurntable::_broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity) {\n  Turntable *tto = Turntable::getByVpin(vpin);\n  if (tto) {\n    if (activity < 4) {\n      tto->setMoving(status);\n      CommandDistributor::broadcastTurntable(tto->getId(), tto->getPosition(), status);\n    }\n  }\n}\n\n// writeAnalogue to send the steps and activity to Turntable-EX.\n// Sends 3 bytes containing the MSB and LSB of the step count, and activity.\n// value contains the steps, bit shifted to MSB + LSB.\n// activity contains the activity flag as per this list:\n// \n// Turn = 0,             // Rotate turntable, maintain phase\n// Turn_PInvert = 1,     // Rotate turntable, invert phase\n// Home = 2,             // Initiate homing\n// Calibrate = 3,        // Initiate calibration sequence\n// LED_On = 4,           // Turn LED on\n// LED_Slow = 5,         // Set LED to a slow blink\n// LED_Fast = 6,         // Set LED to a fast blink\n// LED_Off = 7,          // Turn LED off\n// Acc_On = 8,           // Turn accessory pin on\n// Acc_Off = 9           // Turn accessory pin off\nvoid EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) {\n  if (_deviceState == DEVSTATE_FAILED) return;\n  if (value < 0) return;\n  uint8_t stepsMSB = value >> 8;\n  uint8_t stepsLSB = value & 0xFF;\n#ifdef DIAG_IO\n  DIAG(F(\"EX-Turntable WriteAnalogue VPIN:%u Value:%d Activity:%d Duration:%d\"),\n    vpin, value, activity, duration);\n  DIAG(F(\"I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d\"),\n    _I2CAddress.toString(), stepsMSB, stepsLSB, activity);\n#else\n  (void)duration;\n#endif\n  if (activity < 4) _stepperStatus = 1;     // Tell the device driver Turntable-EX is busy\n  _previousStatus = _stepperStatus;\n  _currentActivity = activity;\n  _broadcastStatus(vpin, _stepperStatus, activity); // Broadcast when the rotation starts\n  I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);\n}\n\n// Display Turnetable-EX device driver info.\nvoid EXTurntable::_display() {\n  DIAG(F(\"EX-Turntable I2C:%s Configured on Vpins:%u-%u %S\"), _I2CAddress.toString(), (int)_firstVpin, \n    (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n}\n\n#endif\n"
  },
  {
    "path": "IO_EncoderThrottle.cpp",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_EncoderThrottle device driver uses a rotary encoder connected to vpins\n* to drive a loco.\n*  Loco id is selected by writeAnalog.\n*/\n\n#include \"IODevice.h\"\n#include \"IO_EncoderThrottle.h\"\n#include \"DIAG.h\"\n#include \"DCC.h\"\n\nconst byte _DIR_CW = 0x10;  // Clockwise step\nconst byte _DIR_CCW = 0x20;  // Counter-clockwise step\n\nconst byte transition_table[5][4]= {\n    {0,1,3,0},            // 0: 00\n    {1,1,1,2 | _DIR_CW},  // 1: 00->01\n    {2,2,0,2},            // 2: 00->01->11\n    {3,3,3,4 | _DIR_CCW}, // 3: 00->10\n    {4,0,4,4}             // 4: 00->10->11\n};\n\nconst byte _STATE_MASK = 0x07;\nconst byte _DIR_MASK = 0x30;\n\n\n\n  void EncoderThrottle::create(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch) {\n    if (checkNoOverlap(firstVpin)) new EncoderThrottle(firstVpin, dtPin,clkPin,clickPin,notch);\n  }\n\n\n  // Constructor\n  EncoderThrottle::EncoderThrottle(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch){\n    _firstVpin = firstVpin;\n    _nPins = 1;\n    _I2CAddress = 0;\n    _dtPin=dtPin;\n    _clkPin=clkPin;\n    _clickPin=clickPin;\n    _notch=notch;\n    _locoid=0;\n    _stopState=xrSTOP;\n    _rocoState=0; \n    _prevpinstate=4; // not 01..11 \n    IODevice::configureInput(dtPin,true);\n    IODevice::configureInput(clkPin,true);\n    IODevice::configureInput(clickPin,true);\n    addDevice(this);\n    _display();\n  }\n\n  \n\n  void EncoderThrottle::_loop(unsigned long currentMicros)  {\n    (void) currentMicros; // suppress warning\n\n    if (_locoid==0) return;  // not in use\n    \n    // Clicking down on the roco, stops the loco and sets the direction as unknown.\n  if (IODevice::read(_clickPin)) {\n    if (_stopState==xrSTOP) return; // debounced multiple stops\n    DCC::setThrottle(_locoid,1,DCC::getThrottleDirection(_locoid));\n    _stopState=xrSTOP;\n    DIAG(F(\"DRIVE %d STOP\"),_locoid);\n    return; \n  }\n\n  // read roco pins and detect state change \n  byte pinstate = (IODevice::read(_dtPin) << 1) | IODevice::read(_clkPin);\n  if (pinstate==_prevpinstate) return;\n  _prevpinstate=pinstate;\n\n  _rocoState = transition_table[_rocoState & _STATE_MASK][pinstate];\n  if ((_rocoState & _DIR_MASK) == 0) return; // no value change \n\n  int change=(_rocoState & _DIR_CW)?+1:-1;\n  // handle roco change -1 or +1 (clockwise)\n  \n  if (_stopState==xrSTOP) {\n      // first move after button press sets the direction. (clockwise=fwd)\n      _stopState=change>0?xrFWD:xrREV;\n    }\n\n    // when going fwd, clockwise increases speed. \n    // but when reversing, anticlockwise increases speed.\n    // This is similar to a center-zero pot control but with\n    // the added safety that you cant panic-spin into the other\n    // direction.\n    if (_stopState==xrREV) change=-change; \n    //  manage limits\n    int oldspeed=DCC::getThrottleSpeed(_locoid);\n    if (oldspeed==1)oldspeed=0; // break out of estop\n    int  newspeed=change>0 ?  (min((oldspeed+_notch),126)) : (max(0,(oldspeed-_notch)));\n    if (newspeed==1) newspeed=0; // normal decelereated stop. \n    if (oldspeed!=newspeed) {\n        DIAG(F(\"DRIVE %d notch %S %d %S\"),_locoid,\n             change>0?F(\"UP\"):F(\"DOWN\"),_notch,\n             _stopState==xrFWD?F(\"FWD\"):F(\"REV\"));\n        DCC::setThrottle(_locoid,newspeed,_stopState==xrFWD);\n        }\n}\n\n  // Set locoid as analog value to start drive\n  // use <z vpin locoid [notch]>\n  void EncoderThrottle::_writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2)  {  \n    (void)vpin; // not used, but needed to match IODevice interface\n    (void) param2;\n    _locoid=value;\n    if (param1>0) _notch=param1;\n    _rocoState=0;\n    \n    // If loco is moving, we inherit direction from it.\n    _stopState=xrSTOP;\n    if (_locoid>0) {\n        auto speedbyte=DCC::getThrottleSpeedByte(_locoid);\n        if ((speedbyte & 0x7f) >1) {\n            // loco is moving\n            _stopState= (speedbyte & 0x80)?xrFWD:xrREV;\n        } \n    }\n    _display();\n  }\n\n  \n  void EncoderThrottle::_display() {\n    DIAG(F(\"DRIVE vpin %d loco %d notch %d\"),_firstVpin,_locoid,_notch);\n  }\n"
  },
  {
    "path": "IO_EncoderThrottle.h",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_EncoderThrottle device driver uses a rotary encoder connected to vpins\n* to drive a loco.\n*  Loco id is selected by writeAnalog.\n*/\n\n#ifndef IO_EncoderThrottle_H\n#define IO_EncoderThrottle_H\n#include \"IODevice.h\"\n\nclass EncoderThrottle : public IODevice {\npublic:\n  \n  static void create(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch=10);\n \nprivate:\n  int _dtPin,_clkPin,_clickPin, _locoid, _notch,_prevpinstate; \n  enum {xrSTOP,xrFWD,xrREV} _stopState;\n  byte _rocoState;  \n\n  // Constructor\n  EncoderThrottle(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch);\n  \n  void _loop(unsigned long currentMicros) override ;\n\n  // Selocoid as analog value to start drive\n  // use <z vpin locoid [notch]>\n  void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;\n  \n  void _display() override ;\n\n };\n\n#endif\n"
  },
  {
    "path": "IO_ExampleSerial.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * To declare a device instance, \n *    IO_ExampleSerial myDevice(1000, 10, Serial3, 9600);\n * or to create programmatically,\n *    IO_ExampleSerial::create(1000, 10, Serial3, 9600);\n * \n * (uses VPINs 1000-1009, talke on Serial 3 at 9600 baud.)\n * \n * See IO_ExampleSerial.cpp for the protocol used over the serial line.\n * \n */\n\n#ifndef IO_EXAMPLESERIAL_H\n#define IO_EXAMPLESERIAL_H\n\n#include \"IODevice.h\"\n\nclass IO_ExampleSerial : public IODevice {\nprivate:\n  // Here we define the device-specific variables.  \n  HardwareSerial *_serial;\n  uint8_t _inputState = 0;\n  int _inputIndex = 0;\n  int _inputValue = 0;\n  uint16_t *_pinValues; // Pointer to block of memory containing pin values\n  unsigned long _baud;\n\npublic:\n  //  Static function to handle \"IO_ExampleSerial::create(...)\" calls.\n  static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {\n    if (checkNoOverlap(firstVpin,nPins)) new IO_ExampleSerial(firstVpin, nPins, serial, baud);\n  } \n\nprotected:\n  // Constructor.  This should initialise variables etc. but not call other objects yet\n  // (e.g. Serial, I2CManager, and other parts of the CS functionality).\n  // defer those until the _begin() function.  The 'addDevice' call is required unless\n  // the device is not to be added (e.g. because of incorrect parameters).\n  IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    _pinValues = (uint16_t *)calloc(_nPins, sizeof(uint16_t));\n    _baud = baud;\n    \n    // Save reference to serial port driver\n    _serial = serial;\n\n    addDevice(this);\n  }\n\n  // Device-specific initialisation\n  void _begin() override {\n    _serial->begin(_baud);\n#if defined(DIAG_IO)\n    _display();\n#endif\n\n    // Send a few # characters to the output\n    for (uint8_t i=0; i<3; i++)\n      _serial->write('#');\n  }\n  \n  // Device-specific write function.  Write a string in the form \"#Wm,n#\"\n  //  where m is the vpin number, and n is the value.\n  void _write(VPIN vpin, int value) {\n    int pin = vpin -_firstVpin;\n    #ifdef DIAG_IO\n    DIAG(F(\"IO_ExampleSerial::_write VPIN:%u Value:%d\"), (int)vpin, value);\n    #endif\n    // Send a command string over the serial line\n    _serial->print('#');\n    _serial->print('W');\n    _serial->print(pin);\n    _serial->print(',');\n    _serial->print(value);\n    _serial->println('#');\n    DIAG(F(\"ExampleSerial Sent command, p1=%d, p2=%d\"), vpin, value);\n  }\n\n  // Device-specific read function.\n  int _read(VPIN vpin) {\n\n    // Return a value for the specified vpin.\n    int result = _pinValues[vpin-_firstVpin];\n\n    return result;\n  }\n\n  // Loop function to do background scanning of the input port.  State \n  //  machine parses the incoming command as it is received.  Command\n  //  is in the form \"#Nm,n#\" where m is the index and n is the value.\n  void _loop(unsigned long currentMicros) {\n    (void)currentMicros;  // Suppress compiler warnings\n    if (_serial->available()) {\n      // Input data available to read.  Read a character.\n      char c = _serial->read();\n      switch (_inputState) {\n        case 0: // Waiting for start of command\n          if (c == '#')  // Start of command received.\n            _inputState = 1;\n          break;\n        case 1: // Expecting command character\n          if (c == 'N') { // 'Notify' character received\n            _inputState = 2;\n            _inputValue = _inputIndex = 0;\n          } else\n            _inputState = 0; // Unexpected char, reset\n          break;\n        case 2: // reading first parameter (index)\n          if (isdigit(c))\n            _inputIndex = _inputIndex * 10 + (c-'0');\n          else if (c==',') \n            _inputState = 3;\n          else\n            _inputState = 0; // Unexpected char, reset\n          break;\n        case 3: // reading reading second parameter (value)\n          if (isdigit(c)) \n            _inputValue = _inputValue * 10 - (c-'0');\n          else if (c=='#') { // End of command\n            // Complete command received, do something with it.\n            DIAG(F(\"ExampleSerial Received command, p1=%d, p2=%d\"), _inputIndex, _inputValue);\n            if (_inputIndex >= 0 && _inputIndex < _nPins) { // Store value\n              _pinValues[_inputIndex] = _inputValue;\n            }\n            _inputState = 0; // Done, start again.\n          } else\n            _inputState = 0; // Unexpected char, reset\n          break;\n      }\n    }\n  }\n\n  // Display information about the device, and perhaps its current condition (e.g. active, disabled etc).\n  // Here we display the current values held for the pins.\n  void _display() {\n    DIAG(F(\"IO_ExampleSerial Configured on Vpins:%u-%u\"), (int)_firstVpin, \n      (int)_firstVpin+_nPins-1);\n    for (int i=0; i<_nPins; i++)\n      DIAG(F(\"  VPin %2u: %d\"), _firstVpin+i, _pinValues[i]);\n  }\n\n\n};\n\n#endif // IO_EXAMPLESERIAL_H\n"
  },
  {
    "path": "IO_GPIOBase.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef IO_GPIOBASE_H\n#define IO_GPIOBASE_H\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\n// GPIOBase is defined as a class template.  This allows it to be instantiated by\n// subclasses with different types, according to the number of pins on the GPIO module.\n// For example, GPIOBase<uint8_t> for 8 pins, GPIOBase<uint16_t> for 16 pins etc.\n// A module with up to 64 pins can be handled in this way (uint64_t).\n\ntemplate <class T>\nclass GPIOBase : public IODevice {\n\nprotected:\n  // Constructor\n  GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin);\n  // Device-specific initialisation\n  void _begin() override;\n  // Device-specific pin configuration function.  \n  bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;\n  // Pin write function.\n  void _write(VPIN vpin, int value) override;\n  // Pin read function.\n  int _read(VPIN vpin) override;\n  void _display() override;\n  void _loop(unsigned long currentMicros) override;\n\n  // Data fields\n \n  // Allocate enough space for all input pins\n  T _portInputState; // 1=high (inactive), 0=low (activated)\n  T _portOutputState; // 1 =high, 0=low\n  T _portMode;  // 0=input, 1=output\n  T _portPullup; // 0=nopullup, 1=pullup\n  T _portInUse;  // 0=not in use, 1=in use\n  // Target interval between refreshes of each input port\n  static const int _portTickTime = 4000; // 4ms\n\n  // Virtual functions for interfacing with I2C GPIO Device\n  virtual void _writeGpioPort() = 0;\n  virtual void _readGpioPort(bool immediate=true) = 0;\n  virtual void _writePullups() {};\n  virtual void _writePortModes() {};\n  virtual void _setupDevice() {};\n  virtual void _processCompletion(uint8_t status) {\n    (void)status; // Suppress compiler warning\n  };\n\n  I2CRB requestBlock;\n  FSH *_deviceName;\n};\n\n// Because class GPIOBase is a template, the implementation (below) must be contained within the same\n// file as the class declaration (above).  Otherwise it won't compile!\n\n// Constructor\ntemplate <class T>\nGPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin) :\n  IODevice(firstVpin, nPins)\n{\n  if (_nPins > (int)sizeof(T)*8) _nPins = sizeof(T)*8;  // Ensure nPins is consistent with the number of bits in T\n  _deviceName = deviceName;\n  _I2CAddress = i2cAddress;\n  _gpioInterruptPin = interruptPin;\n  _hasCallback = true;\n  // Add device to list of devices.\n  addDevice(this);\n\n  _portMode = 0;  // default to input mode\n  _portPullup = -1; // default to pullup enabled\n  _portInputState = -1;  // default to all inputs high (inactive)\n  _portInUse = 0;  // No ports in use initially.\n}\n\ntemplate <class T>\nvoid GPIOBase<T>::_begin() {\n  // Configure pin used for GPIO extender notification of change (if allocated)\n  if (_gpioInterruptPin >= 0) \n    pinMode(_gpioInterruptPin, INPUT_PULLUP);\n\n  I2CManager.begin();\n  I2CManager.setClock(400000);\n  if (I2CManager.exists(_I2CAddress)) {\n#if defined(DIAG_IO)\n    _display();\n#endif\n    _setupDevice();\n    _deviceState = DEVSTATE_NORMAL;\n  } else {\n    DIAG(F(\"%S I2C:%s Device not detected\"), _deviceName, _I2CAddress.toString());\n    _deviceState = DEVSTATE_FAILED;\n  }\n}\n\n// Configuration parameters for inputs: \n//  params[0]: enable pullup\ntemplate <class T>\nbool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {\n  if (configType != CONFIGURE_INPUT) return false;\n  if (paramCount == 0 || paramCount > 1) return false;\n  bool pullup = params[0];\n  int pin = vpin - _firstVpin;\n  #ifdef DIAG_IO\n  DIAG(F(\"%S I2C:%s Config Pin:%d Val:%d\"), _deviceName, _I2CAddress.toString(), pin, pullup);\n  #endif\n  uint16_t mask = 1 << pin;\n  if (pullup) \n    _portPullup |= mask;\n  else\n    _portPullup &= ~mask;\n  // Mark that port has been accessed\n  _portInUse |= mask;\n  // Set input mode\n  _portMode &= ~mask;\n\n  // Call subclass's virtual function to write to device\n  _writePortModes();\n  _writePullups();\n  // Port change will be notified on next loop entry.\n\n  return true;\n}\n\n// Periodically read the input port\ntemplate <class T>\nvoid GPIOBase<T>::_loop(unsigned long currentMicros) {\n  T lastPortStates = _portInputState;\n  if (_deviceState == DEVSTATE_SCANNING && !requestBlock.isBusy()) {\n    uint8_t status = requestBlock.status;\n    if (status == I2C_STATUS_OK) {\n      _deviceState = DEVSTATE_NORMAL;\n    } else {\n      _deviceState = DEVSTATE_FAILED;\n      DIAG(F(\"%S I2C:%s Error:%d %S\"), _deviceName, _I2CAddress.toString(), status, \n        I2CManager.getErrorMessage(status));\n    }\n    _processCompletion(status);\n  // Set unused pin and write mode pin value to 1\n    _portInputState |= ~_portInUse | _portMode;\n\n    // Scan for changes in input states and invoke callback (if present)\n    T differences = lastPortStates ^ _portInputState;\n    if (differences && IONotifyCallback::hasCallback()) {\n      // Scan for differences bit by bit\n      T mask = 1;\n      for (int pin=0; pin<_nPins; pin++) {\n        if (differences & mask) {\n          // Change detected.\n          IONotifyCallback::invokeAll(_firstVpin+pin, (_portInputState & mask) == 0);\n        }\n        mask <<= 1;\n      }\n    }\n\n    #ifdef DIAG_IO\n    if (differences)\n      DIAG(F(\"%S I2C:%s PortStates:%x\"), _deviceName, _I2CAddress.toString(), _portInputState);\n    #endif\n  }\n\n  // Check if interrupt configured.  If not, or if it is active (pulled down), then\n  //  initiate a scan.\n  if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) {\n    // TODO: Could suppress reads if there are no pins configured as inputs!\n\n    // Read input\n    if (_deviceState == DEVSTATE_NORMAL) {\n      _readGpioPort(false);  // Initiate non-blocking read\n      _deviceState= DEVSTATE_SCANNING;\n    }\n  }\n  // Delay next entry until tick elapsed.\n  delayUntil(currentMicros + _portTickTime);\n}\n\ntemplate <class T>\nvoid GPIOBase<T>::_display() {\n  DIAG(F(\"%S I2C:%s Configured on Vpins:%u-%u %S\"), _deviceName, _I2CAddress.toString(), \n    _firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n}\n\ntemplate <class T>\nvoid GPIOBase<T>::_write(VPIN vpin, int value) {\n  int pin = vpin - _firstVpin;\n  T mask = 1 << pin;\n  #ifdef DIAG_IO\n  DIAG(F(\"%S I2C:%s Write Pin:%d Val:%d\"), _deviceName, _I2CAddress.toString(), pin, value);\n  #endif\n\n  // Set port mode output if currently not output mode\n  if (!(_portMode & mask)) {\n    _portInUse |= mask;\n    _portMode |= mask;\n    _writePortModes();\n  }\n\n  // Update port output state\n  if (value) \n    _portOutputState |= mask;\n  else\n    _portOutputState &= ~mask;\n\n  // Call subclass's virtual function to write to device.\n  return _writeGpioPort();\n}\n\ntemplate <class T>\nint GPIOBase<T>::_read(VPIN vpin) {\n  int pin = vpin - _firstVpin;\n  T mask = 1 << pin;\n\n  // Set port mode to input if currently output or first use\n  if ((_portMode | ~_portInUse) & mask) {\n    _portMode &= ~mask;\n    _portInUse |= mask;\n    _writePullups();\n    _writePortModes();\n    // Port won't have been read yet, so read it now.\n    _readGpioPort();\n  // Set unused pin and write mode pin value to 1\n    _portInputState |= ~_portInUse | _portMode;\n    #ifdef DIAG_IO\n    DIAG(F(\"%S I2C:%s PortStates:%x\"), _deviceName, _I2CAddress.toString(), _portInputState);\n    #endif\n  }\n  return (_portInputState & mask) ? 0 : 1;  // Invert state (5v=0, 0v=1)\n}\n\n#endif\n"
  },
  {
    "path": "IO_HALDisplay.h",
    "content": "/*\n *  © 2024, Paul Antoine\n *  © 2023, Neil McKechnie\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* \n * This driver provides a more immediate interface into the OLED display\n * than the one installed through the config.h file.  When an LCD(...) call\n * is made, the text is output immediately to the specified display line,\n * without waiting for the next 2.5 second refresh.  However, if the line \n * specified is off the screen then the text in the bottom line will be \n * overwritten.  There is however a special case that if line 255 is specified, \n * the existing text will scroll up and the new line added to the bottom\n * line of the screen.\n * \n * To install, use the following command in myHal.cpp:\n *\n *    HALDisplay<OLED>::create(address, width, height);\n * \n * where address is the I2C address of the OLED display (0x3c or 0x3d),\n * width is the width in pixels, and height is the height in pixels.\n * \n * Valid width and height are 128x32 (SSD1306 controller), \n * 128x64 (SSD1306) and 132x64 (SH1106).  The driver uses\n * a 5x7 character set in a 6x8 pixel cell.\n * \n * OR\n * \n *    HALDisplay<LiquidCrystal>::create(address, width, height);\n * \n * where address is the I2C address of the LCD display (0x27 typically),\n * width is the width in characters (16 or 20 typically),\n * and height is the height in characters (2 or 4 typically).\n */\n\n\n#ifndef IO_HALDisplay_H\n#define IO_HALDisplay_H\n\n#include \"IODevice.h\"\n#include \"DisplayInterface.h\"\n#include \"SSD1306Ascii.h\"\n#include \"LiquidCrystal_I2C.h\"\n#include \"version.h\"\n\ntypedef SSD1306AsciiWire OLED;\ntypedef LiquidCrystal_I2C LiquidCrystal; \n\ntemplate <class T> \nclass HALDisplay : public IODevice, public DisplayInterface {\nprivate:\n  // Here we define the device-specific variables.  \n  uint8_t _height; // in pixels\n  uint8_t _width;  // in pixels\n  T *_displayDriver;\n  uint8_t _rowNo = 0;   // Row number being written by caller\n  uint8_t _colNo = 0;  // Position in line being written by caller\n  uint8_t _numRows;\n  uint8_t _numCols;\n  char *_buffer = NULL;\n  uint8_t *_rowGeneration = NULL;\n  uint8_t *_lastRowGeneration = NULL;\n  uint8_t _rowNoToScreen = 0; \n  uint8_t _charPosToScreen = 0;\n  bool _startAgain = false;\n  DisplayInterface *_nextDisplay = NULL;\n\npublic:\n  //  Static function to handle \"HALDisplay::create(...)\" calls.\n  static void create(I2CAddress i2cAddress, int width, int height) {\n    if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(0, i2cAddress, width, height);\n  } \n  static void create(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {\n    if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(displayNo, i2cAddress, width, height);\n  } \n\nprotected:\n  // Constructor\n  HALDisplay(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {\n    _displayDriver = new T(i2cAddress, width, height);\n    if (!_displayDriver) return;  // Check for memory allocation failure\n    _I2CAddress = i2cAddress;\n    _width = width;\n    _height = height;\n    _numCols = _displayDriver->getNumCols();\n    _numRows = _displayDriver->getNumRows();\n\n    _charPosToScreen = _numCols;\n\n    // Allocate arrays\n    _buffer = (char *)calloc(_numRows*_numCols, sizeof(char));\n    if (!_buffer) return;  // Check for memory allocation failure\n    _rowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));\n    if (!_rowGeneration) return;  // Check for memory allocation failure\n    _lastRowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));\n    if (!_lastRowGeneration) return;  // Check for memory allocation failure\n\n    // Fill buffer with spaces\n    memset(_buffer, ' ', _numCols*_numRows);\n\n    // Add device to list of HAL devices (not necessary but allows\n    // status to be displayed using <D HAL SHOW> and device to be\n    // reinitialised using <D HAL RESET>).\n    IODevice::addDevice(this);\n\n    // Moved after addDevice() to ensure I2CManager.begin() has been called fisrt\n    _displayDriver->clearNative();\n\n    // Also add this display to list of display handlers\n    DisplayInterface::addDisplay(displayNo);\n\n    // Is this the system display (0)?\n    if (displayNo == 0) {\n      // Set first two lines on screen\n      this->setRow(displayNo, 0);\n      print(F(\"DCC-EX v\"));\n      print(F(VERSION));\n      setRow(displayNo, 1);\n      print(F(\"Lic GPLv3\"));\n    }\n  }\n  \n  \n  void screenUpdate() {\n    // Loop through the buffer and if a row has changed\n    // (rowGeneration[row] is changed) then start writing the\n    // characters from the buffer, one character per entry, \n    // to the screen until that row has been refreshed.\n\n    // First check if the OLED driver is still busy from a previous \n    // call.  If so, don't do anything until the next entry.\n    if (!_displayDriver->isBusy()) {\n      // Check if we've just done the end of a row\n      if (_charPosToScreen >= _numCols) {\n        // Move to next line\n        if (++_rowNoToScreen >= _numRows || _startAgain) {\n          _rowNoToScreen = 0; // Wrap to first row\n          _startAgain = false;\n        }\n\n        if (_rowGeneration[_rowNoToScreen] != _lastRowGeneration[_rowNoToScreen]) {\n          // Row content has changed, so start outputting it\n          _lastRowGeneration[_rowNoToScreen] = _rowGeneration[_rowNoToScreen];\n          _displayDriver->setRowNative(_rowNoToScreen);\n          _charPosToScreen = 0;  // Prepare to output first character on next entry\n        } else {\n          // Row not changed, don't bother writing it.\n        }\n      } else {\n        // output character at current position\n        _displayDriver->writeNative(_buffer[_rowNoToScreen*_numCols+_charPosToScreen++]);\n      }  \n    }\n    return;\n  }\n\n  /////////////////////////////////////////////////\n  // IODevice Class Member Overrides\n  /////////////////////////////////////////////////\n\n  // Device-specific initialisation\n  void _begin() override {\n    // Initialise device\n    if (_displayDriver->begin()) {\n\n      _display();\n\n      // Force all rows to be redrawn\n      for (uint8_t row=0; row<_numRows; row++)\n        _rowGeneration[row]++;\n      \n      // Start with top line (looks better).  \n      // The numbers will wrap round on the first loop2 entry.\n      _rowNoToScreen = _numRows;\n      _charPosToScreen = _numCols;\n    }\n  }\n\n  void _loop(unsigned long) override {\n    screenUpdate();\n  }\n  \n  // Display information about the device.\n  void _display() {\n    DIAG(F(\"HALDisplay %d configured on addr %s\"), _displayNo, _I2CAddress.toString());\n  }\n  \n  /////////////////////////////////////////////////\n  // DisplayInterface functions\n  // \n  /////////////////////////////////////////////////\n  \npublic:\n  void _displayLoop() override {\n    screenUpdate();\n  }\n\n  // Position on nominated line number (0 to number of lines -1)\n  // Clear the line in the buffer ready for updating\n  // The displayNo referenced here is remembered and any following\n  // calls to write() will be directed to that display.\n  void _setRow(byte line) override {\n    if (line == 255) {\n      // LCD(255,\"xxx\") or SCREEN(displayNo,255, \"xxx\") - \n      // scroll the contents of the buffer and put the new line\n      // at the bottom of the screen\n      for (int row=1; row<_numRows; row++) {\n        strncpy(&_buffer[(row-1)*_numCols], &_buffer[row*_numCols], _numCols);\n        _rowGeneration[row-1]++;\n      }\n      line = _numRows-1;\n    } else if (line >= _numRows) \n      line = _numRows - 1;  // Overwrite bottom line.\n\n    _rowNo = line;\n    // Fill line with blanks\n    for (_colNo = 0; _colNo < _numCols; _colNo++)\n      _buffer[_rowNo*_numCols+_colNo] = ' ';\n    _colNo = 0;\n    // Mark that the buffer has been touched.  It will start being \n    // sent to the screen on the next loop entry, by which time\n    // the line should have been written to the buffer.\n    _rowGeneration[_rowNo]++;\n    // Indicate that the output loop is to start updating the screen again from\n    // row 0.  Otherwise, on a full screen rewrite the bottom part may be drawn\n    // before the top part!\n    _startAgain = true;\n  }\n\n  // Write one character to the screen referenced in the last setRow() call.\n  virtual size_t _write(uint8_t c) override {\n    // Write character to buffer (if there's space)\n    if (_colNo < _numCols) {\n      _buffer[_rowNo*_numCols+_colNo++] = c;\n    }\n    return 1;\n  }\n\n  // Write blanks to all of the screen buffer\n  void _clear() {\n    // Clear buffer\n    memset(_buffer, ' ', _numCols*_numRows);\n    _colNo = 0;\n    _rowNo = 0;\n  }\n\n};\n\n#endif // IO_HALDisplay_H\n"
  },
  {
    "path": "IO_HCSR04.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver.\n * It is operated through two signal pins.  When the transmit pin is set to 1\n * for 10us, on the falling edge the transmitter sends a short transmission of\n * 8 pulses (like a sonar 'ping').  This is reflected off objects and received\n * by the receiver.  A pulse is sent on the receive pin whose length is equal\n * to the delay between the transmission of the pulse and the detection of\n * its echo.  The distance of the reflecting object is calculated by halving\n * the time (to allow for the out and back distance), then multiplying by the\n * speed of sound (assumed to be constant).\n *\n * This driver polls the HC-SR04 by sending the trigger pulse and then measuring\n * the length of the received pulse.  If the calculated distance is less than\n * the threshold, the output _state returned by a read() call changes to 1.  If\n * the distance is greater than the threshold plus a hysteresis margin, the\n * output changes to 0. The device also supports readAnalogue(), which returns\n * the measured distance in cm, or 32767 if the distance exceeds the\n * offThreshold.\n *\n * It might be thought that the measurement would be more reliable if interrupts\n * were disabled while the pulse is being timed.  However, this would affect\n * other functions in the CS so the measurement is being performed with\n * interrupts enabled.  Also, we could use an interrupt pin in the Arduino for\n * the timing, but the same consideration applies.  In any case, the DCC\n * interrupt occurs once every 58us, so any IRC code is much faster than that.\n * And 58us corresponds to 1cm in the calculation, so the effect of\n * interrupts is negligible.\n *\n * Note: The timing accuracy required for measuring the pulse length means that\n * the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot\n * provide the required accuracy.\n * \n * Example configuration:\n *  HCSR04::create(23000, 32, 33, 80, 85);\n * \n * Where 23000 is the VPIN allocated,\n *       32 is the pin connected to the HCSR04 trigger terminal,\n *       33 is the pin connected to the HCSR04 echo terminal,\n *       80 is the distance in cm below which pin 23000 will be active,\n *   and 85 is the distance in cm above which pin 23000 will be inactive.\n * \n * Alternative configuration, which hogs the processor until the measurement is complete\n * (old behaviour, more accurate but higher impact on other CS tasks):\n *  HCSR04::create(23000, 32, 33, 80, 85, HCSR04::LOOP);\n * \n */\n\n#ifndef IO_HCSR04_H\n#define IO_HCSR04_H\n\n#include \"IODevice.h\"\n\nclass HCSR04 : public IODevice {\n\nprivate:\n  // pins must be arduino GPIO pins, not extender pins or HAL pins.\n  int _trigPin = -1;\n  int _echoPin = -1;\n  // Thresholds for setting active _state in cm.\n  uint8_t _onThreshold;  // cm\n  uint8_t _offThreshold; // cm\n  // Last measured distance in cm.\n  uint16_t _distance;\n  // Active=1/inactive=0 _state \n  uint8_t _value = 0;\n  // Factor for calculating the distance (cm) from echo time (us).\n  //  Based on a speed of sound of 345 metres/second.\n  const uint16_t factor = 58; // us/cm\n  // Limit the time spent looping by dropping out when the expected\n  // worst case threshold value is greater than an arbitrary value.\n  const uint16_t maxPermittedLoopTime = 10 * factor; // max in us\n  unsigned long _startTime = 0;\n  unsigned long _maxTime = 0;\n  enum {DORMANT, MEASURING}; // _state values\n  uint8_t _state = DORMANT;\n  uint8_t _counter = 0;\n  uint16_t _options = 0;\n\npublic:\n  enum Options {\n    LOOP = 1,  // Option HCSR04::LOOP reinstates old behaviour, i.e. complete measurement in one loop entry.\n  };\n \n  // Static create function provides alternative way to create object\n  static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options = 0) {\n    if (checkNoOverlap(vpin))\n        new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold, options);\n  }\n\nprotected:\n  // Constructor performs static initialisation of the device object\n  HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options) {\n    _firstVpin = vpin;\n    _nPins = 1;\n    _trigPin = trigPin;\n    _echoPin = echoPin;\n    _onThreshold = onThreshold;\n    _offThreshold = offThreshold;\n    _options = options;\n    addDevice(this);\n  }\n // _begin function called to perform dynamic initialisation of the device\n  void _begin() override {\n    _state = 0;\n    pinMode(_trigPin, OUTPUT);\n    pinMode(_echoPin, INPUT);\n    ArduinoPins::fastWriteDigital(_trigPin, 0);\n#if defined(DIAG_IO)\n    _display();\n#endif\n  }\n\n  // _read function - just return _value (calculated in _loop).\n  int _read(VPIN vpin) override {\n    (void)vpin;  // avoid compiler warning\n    return _value;\n  }\n\n  int _readAnalogue(VPIN vpin) override {\n    (void)vpin; // avoid compiler warning\n    return _distance;\n  }\n\n  // _loop function - read HC-SR04 once every 100 milliseconds.\n  void _loop(unsigned long currentMicros) override {\n    unsigned long waitTime;\n    switch(_state) {\n      case DORMANT: // Issue pulse\n        // If receive pin is still set on from previous call, do nothing till next entry.\n        if (ArduinoPins::fastReadDigital(_echoPin)) return;\n\n        // Send 10us pulse to trigger transmitter\n        ArduinoPins::fastWriteDigital(_trigPin, 1);\n        delayMicroseconds(10);\n        ArduinoPins::fastWriteDigital(_trigPin, 0);\n\n        // Wait, with timeout, for echo pin to become set.\n        // Measured time delay is just under 500us, so \n        // wait for max of 1000us.\n        _startTime = micros();\n        _maxTime = 1000;\n\n        while (!ArduinoPins::fastReadDigital(_echoPin)) {\n          // Not set yet, see if we've timed out.\n          waitTime = micros() - _startTime;\n          if (waitTime > _maxTime) {\n            // Timeout waiting for pulse start, abort the read and start again\n            _state = DORMANT;\n            return;\n          }\n        }\n\n        // Echo pulse started, so wait for echo pin to reset, and measure length of pulse\n        _startTime = micros();\n        _maxTime = factor * _offThreshold;\n        _state = MEASURING;\n        // If maximum measurement time is high, then skip until next loop entry before \n        // starting to look for pulse end.\n        // This gives better accuracy at shorter distance thresholds but without extending \n        // loop execution time for longer thresholds.  If LOOP option is set on, then\n        // the entire measurement will be done in one loop entry, i.e. the code will fall\n        // through into the measuring phase.\n        if (!(_options & LOOP) && _maxTime > maxPermittedLoopTime) break;\n        /* fallthrough */\n\n      case MEASURING: // Check if echo pulse has finished\n        do {\n          waitTime = micros() - _startTime;\n          if (!ArduinoPins::fastReadDigital(_echoPin)) {\n            // Echo pulse completed; check if pulse length is below threshold and if so set value.\n            if (waitTime <= factor * _onThreshold) {\n              // Measured time is within the onThreshold, so value is one.\n              _value = 1;\n              // If the new distance value is less than the current, use it immediately.\n              // But if the new distance value is longer, then it may be erroneously long\n              // (because of extended loop times delays), so apply a delay to distance increases.\n              uint16_t estimatedDistance = waitTime / factor;\n              if (estimatedDistance < _distance) \n                _distance = estimatedDistance;\n              else\n                _distance += 1;  // Just increase distance slowly.\n              _counter = 0;\n              //DIAG(F(\"HCSR04: Pulse Len=%l Distance=%d\"), waitTime, _distance);\n            }\n            _state = DORMANT;\n          } else {\n            // Echo pulse hasn't finished, so check if maximum time has elapsed\n            // If pulse is too long then set return value to zero,\n            //  and finish without waiting for end of pulse.\n            if (waitTime > _maxTime) {\n              // Pulse length longer than maxTime, value is provisionally zero.\n              // But don't change _value unless provisional value is zero for 10 consecutive measurements\n              if (_value == 1) {\n                if (++_counter >= 10) {\n                  _value = 0;\n                  _distance = 32767;\n                  _counter = 0;\n                }\n              }\n              _state = DORMANT; // start again\n            }\n          }\n          // If there's lots of time remaining before the expected completion time,\n          // then exit and wait for next loop entry.  Otherwise, loop until we finish.\n          // If option LOOP is set, then we loop until finished anyway.\n          uint32_t remainingTime = _maxTime - waitTime;\n          if (!(_options & LOOP) && remainingTime < maxPermittedLoopTime) return;\n        } while (_state == MEASURING) ;\n        break;\n    }\n    // Datasheet recommends a wait of at least 60ms between measurement cycles\n    if (_state == DORMANT)\n      delayUntil(currentMicros+60000UL); // wait 60ms till next measurement\n\n  }\n\n  void _display() override {\n    DIAG(F(\"HCSR04 Configured on VPIN:%u TrigPin:%d EchoPin:%d On:%dcm Off:%dcm\"),\n      _firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold);\n  }\n\n};\n\n#endif //IO_HCSR04_H\n"
  },
  {
    "path": "IO_I2CDFPlayer-OBSOLETE.h",
    "content": "   /*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * DFPlayer is an MP3 player module with an SD card holder.  It also has an integrated\n * amplifier, so it only needs a power supply and a speaker.\n * This driver is a modified version of the IO_DFPlayer.h file\n * *********************************************************************************************\n * \n * Dec 2023, Added NXP SC16IS752 I2C Dual UART to enable the DFPlayer connection over the I2C bus\n * The SC16IS752 has 64 bytes TX & RX FIFO buffer\n * First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be\n * needed as the RX Fifo holds the reply \n * \n * Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work  but the first transmit \n * corrupt data. This need more analysis and experimenatation. \n * Will push this driver to the dev branch with the uart fixed to 0 \n * Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)\n * \n * myHall.cpp configuration syntax:\n * \n * I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);\n * \n * Parameters:\n * 1st vPin     : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)\n * vPins        : Total number of virtual pins allocated (2 vPins are supported, one for each UART)\n *                1st vPin for UART 0, 2nd for UART 1\n * I2C Address  : I2C address of the serial controller, in 0x format\n * xtal         : 0 for 1,8432Mhz, 1 for 14,7456Mhz\n * \n * The vPin is also a pin that can be read, it indicate if the DFPlayer has finished playing a track\n *\n */\n\n#ifndef IO_I2CDFPlayer_h\n#define IO_I2CDFPlayer_h\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\n// Debug and diagnostic defines, enable too many will result in slowing the driver\n//#define DIAG_I2CDFplayer\n//#define DIAG_I2CDFplayer_data\n//#define DIAG_I2CDFplayer_reg\n//#define DIAG_I2CDFplayer_playing\n\nclass I2CDFPlayer : public IODevice {\nprivate: \n  const uint8_t MAXVOLUME=30;\n  uint8_t RETRYCOUNT = 0x03;\n  bool _playing = false;\n  uint8_t _inputIndex = 0;\n  unsigned long _commandSendTime; // Time (us) that last transmit took place.\n  unsigned long _timeoutTime;\n  uint8_t _recvCMD;  // Last received command code byte\n  bool _awaitingResponse = false;  \n  uint8_t _retryCounter = RETRYCOUNT; // Max retries before timing out\n  uint8_t _requestedVolumeLevel = MAXVOLUME;\n  uint8_t _currentVolume = MAXVOLUME;\n  int _requestedSong = -1;  // -1=none, 0=stop, >0=file number\n  bool _repeat = false; // audio file is repeat playing\n  uint8_t _previousCmd = true;\n  // SC16IS752 defines\n  I2CAddress _I2CAddress;\n  I2CRB _rb;\n  uint8_t _UART_CH=0x00;  // Fix uart ch to 0 for now\n  // Communication parameters for the DFPlayer are fixed at 8 bit, No parity, 1 stopbit\n  uint8_t WORD_LEN = 0x03;    // Value LCR bit 0,1\n  uint8_t STOP_BIT = 0x00;    // Value LCR bit 2 \n  uint8_t PARITY_ENA = 0x00;  // Value LCR bit 3\n  uint8_t PARITY_TYPE = 0x00; // Value LCR bit 4\n  uint32_t BAUD_RATE = 9600;\n  uint8_t PRESCALER = 0x01;   // Value MCR bit 7\n  uint8_t TEMP_REG_VAL = 0x00;\n  uint8_t FIFO_RX_LEVEL = 0x00;\n  uint8_t RX_BUFFER = 0x00; // nr of bytes copied into _inbuffer\n  uint8_t FIFO_TX_LEVEL = 0x00;  \n  bool _playCmd = false;\n  bool _volCmd = false;\n  bool _folderCmd = false;\n  uint8_t _requestedFolder = 0x01; // default to folder 01\n  uint8_t _currentFolder = 0x01; // default to folder 01\n  bool _repeatCmd = false;\n  bool _stopplayCmd = false;\n  bool _resetCmd = false;\n  bool _eqCmd = false;\n  uint8_t _requestedEQValue = DF_NORMAL;\n  uint8_t _currentEQvalue = DF_NORMAL; // start equalizer value\n  bool _daconCmd = false;\n  uint8_t _audioMixer = 0x01; // Default to output amplifier 1\n  bool _setamCmd = false; // Set the Audio mixer channel\n  uint8_t _outbuffer [11]; // DFPlayer command is 10 bytes + 1 byte register address & UART channel\n  uint8_t _inbuffer[10]; // expected DFPlayer return 10 bytes\n   \n  unsigned long _sc16is752_xtal_freq;\n  unsigned long SC16IS752_XTAL_FREQ_LOW = 1843200; // To support cheap eBay/AliExpress SC16IS752 boards\n  unsigned long SC16IS752_XTAL_FREQ_HIGH = 14745600; // Support for higher baud rates, standard for modular EX-IO system\n   \npublic:\n  // Constructor\n   I2CDFPlayer(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal){\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    _I2CAddress = i2cAddress;\n    if (xtal == 0){\n      _sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_LOW;\n    } else { // should be 1\n        _sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_HIGH;\n      }\n    addDevice(this);\n   } \n  \npublic:\n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new I2CDFPlayer(firstVpin, nPins, i2cAddress, xtal); \n    }\n\n  void _begin() override {\n    // check if SC16IS752 exist first, initialize and then resume DFPlayer init via SC16IS752\n    I2CManager.begin();\n    I2CManager.setClock(1000000);\n    if (I2CManager.exists(_I2CAddress)){\n      DIAG(F(\"SC16IS752 I2C:%s UART detected\"), _I2CAddress.toString());\n      Init_SC16IS752(); // Initialize UART\n      if (_deviceState == DEVSTATE_FAILED){\n        DIAG(F(\"SC16IS752 I2C:%s UART initialization failed\"), _I2CAddress.toString());\n        }\n      } else {\n         DIAG(F(\"SC16IS752 I2C:%s UART not detected\"), _I2CAddress.toString());\n        }\n      #if defined(DIAG_IO)\n      _display();\n      #endif\n      // Now init DFPlayer\n      // Send a query to the device to see if it responds\n      _deviceState = DEVSTATE_INITIALISING; \n      sendPacket(0x42,0,0);\n      _timeoutTime = micros() + 5000000UL;  // 5 second timeout      \n      _awaitingResponse = true; \n     }\n  \n  \n  void _loop(unsigned long currentMicros) override {\n    // Read responses from device\n    uint8_t status = _rb.status;\n    if (status == I2C_STATUS_PENDING) return;  // Busy, so don't do anything\n    if (status == I2C_STATUS_OK) { \n      processIncoming(currentMicros);\n          // Check if a command sent to device has timed out.  Allow 0.5 second for response\n          // added retry counter, sometimes we do not sent keep alive due to other commands sent to DFPlayer\n      if (_awaitingResponse && (int32_t)(currentMicros - _timeoutTime) > 0) { // timeout triggered\n        if(_retryCounter == 0){ // retry counter out of luck, must take the device to failed state     \n          DIAG(F(\"I2CDFPlayer:%s, DFPlayer not responding on UART channel: 0x%x\"), _I2CAddress.toString(), _UART_CH);\n          _deviceState = DEVSTATE_FAILED;\n          _awaitingResponse = false;\n          _playing = false;\n          _retryCounter = RETRYCOUNT;\n        } else { // timeout and retry protection and recovery of corrupt data frames from DFPlayer\n            #ifdef DIAG_I2CDFplayer_playing\n              DIAG(F(\"I2CDFPlayer: %s, DFPlayer timout, retry counter: %d on UART channel: 0x%x\"), _I2CAddress.toString(), _retryCounter, _UART_CH);\n            #endif\n            _timeoutTime = currentMicros + 5000000UL;  // Timeout if no response within 5 seconds// reset timeout\n            _awaitingResponse = false; // trigger sending a keep alive 0x42 in processOutgoing()\n            _retryCounter --; // decrement retry counter                        \n            resetRX_fifo(); // reset the RX fifo as it has corrupt data            \n          }\n      }      \n    }\n\n    status = _rb.status;\n    if (status == I2C_STATUS_PENDING) return;  // Busy, try next time\n    if (status == I2C_STATUS_OK) {\n     // Send any commands that need to go.\n      processOutgoing(currentMicros);\n     }\n    delayUntil(currentMicros + 10000); // Only enter every 10ms    \n  }\n\n \n  // Check for incoming data, and update busy flag and other state accordingly\n \n  void processIncoming(unsigned long currentMicros) {\n    (void)currentMicros; // suppress warning, not used in this function\n    // Expected message is in the form \"7E FF 06 3D xx xx xx xx xx EF\"\n    RX_fifo_lvl();\n    if (FIFO_RX_LEVEL >= 10) {      \n      #ifdef DIAG_I2CDFplayer\n        DIAG(F(\"I2CDFPlayer: %s Retrieving data from RX Fifo on UART_CH: 0x%x FIFO_RX_LEVEL: %d\"),_I2CAddress.toString(), _UART_CH, FIFO_RX_LEVEL); \n      #endif\n      _outbuffer[0] = REG_RHR << 3 | _UART_CH << 1;\n      // Only copy 10 bytes from RX FIFO, there maybe additional partial return data after a track is finished playing in the RX FIFO\n      I2CManager.read(_I2CAddress, _inbuffer, 10, _outbuffer, 1); // inbuffer[] has the data now\n      //delayUntil(currentMicros + 10000); // Allow time to get the data\n      RX_BUFFER = 10; // We have copied 10 bytes from RX FIFO to _inbuffer\n        #ifdef DIAG_I2CDFplayer_data\n          DIAG(F(\"SC16IS752: At I2C: %s, UART channel: 0x%x, RX FIFO Data\"), _I2CAddress.toString(), _UART_CH);\n          for (int i = 0; i < sizeof _inbuffer; i++){\n            DIAG(F(\"SC16IS752: Data _inbuffer[0x%x]: 0x%x\"), i, _inbuffer[i]);  \n          }\n        #endif       \n    } else {\n        FIFO_RX_LEVEL = 0; //set to 0, we'll read a fresh FIFO_RX_LEVEL next time\n        return; // No data or not enough data in rx fifo, check again next time around\n      }\n\n    \n    bool ok = false;\n    //DIAG(F(\"I2CDFPlayer: RX_BUFFER: %d\"), RX_BUFFER);\n    while (RX_BUFFER != 0) {\n      int c = _inbuffer[_inputIndex]; // Start at 0, increment to FIFO_RX_LEVEL\n      switch (_inputIndex) {\n        case 0:\n          if (c == 0x7E) ok = true;\n          break;\n        case 1:\n          if (c == 0xFF) ok = true;\n          break;\n        case 2:\n          if (c== 0x06) ok = true;\n          break;\n        case 3:\n          _recvCMD = c; // CMD byte\n          ok = true;\n          break;\n        case 6:\n          switch (_recvCMD) {\n            //DIAG(F(\"I2CDFPlayer: %s, _recvCMD: 0x%x _awaitingResponse: 0x0%x\"),_I2CAddress.toString(), _recvCMD, _awaitingResponse);\n            case 0x42:\n              // Response to status query\n              _playing = (c != 0);              \n              // Mark the device online and cancel timeout\n              if (_deviceState==DEVSTATE_INITIALISING) {\n                _deviceState = DEVSTATE_NORMAL;\n                #ifdef DIAG_I2CDFplayer\n                 DIAG(F(\"I2CDFPlayer: %s, UART_CH: 0x0%x, _deviceState: 0x0%x\"),_I2CAddress.toString(), _UART_CH, _deviceState);\n                #endif \n                #ifdef DIAG_IO\n                _display();\n                #endif\n              }\n              _awaitingResponse = false;\n              break;\n            case 0x3d:            \n              // End of play\n              if (_playing) {\n                #ifdef DIAG_IO\n                  DIAG(F(\"I2CDFPlayer: Finished\"));\n                #endif\n                _playing = false;\n              }\n              break;\n            case 0x40:\n              // Error codes; 1: Module Busy\n              DIAG(F(\"I2CDFPlayer: Error %d returned from device\"), c);\n              _playing = false;\n              break;\n          }\n          ok = true;\n          break;\n        case 4: case 5: case 7: case 8: \n          ok = true;  // Skip over these bytes in message.\n          break;\n        case 9:\n          if (c==0xef) {\n            // Message finished\n            _retryCounter = RETRYCOUNT; // reset the retry counter as we have received a valid packet\n          }\n          break;\n        default:\n          break;\n      }\n      if (ok){\n        _inputIndex++;  // character as expected, so increment index\n        RX_BUFFER --; // Decrease FIFO_RX_LEVEL with each character read from _inbuffer[_inputIndex]\n      } else {\n        _inputIndex = 0;  // otherwise reset.\n        RX_BUFFER = 0;\n      }\n    }\n    RX_BUFFER = 0; //Set to 0, we'll read a new RX FIFO level again\n  }\n\n\n  // Send any commands that need to be sent\n  void processOutgoing(unsigned long currentMicros) {\n    // When two commands are sent in quick succession, the device will often fail to \n    // execute one.  Testing has indicated that a delay of 100ms or more is required\n    // between successive commands to get reliable operation.\n    // If 100ms has elapsed since the last thing sent, then check if there's some output to do.\n    if (((int32_t)currentMicros - _commandSendTime) > 100000) {\n      if ( _resetCmd == true){\n          sendPacket(0x0C,0,0);\n          _resetCmd = false;          \n      } else if(_volCmd == true) { // do the volme before palying a track\n         if(_requestedVolumeLevel <= 30) {         \n         _currentVolume = _requestedVolumeLevel; // If _requestedVolumeLevel is out of range, sent _currentV1olume      \n         }\n         sendPacket(0x06, 0x00, _currentVolume);\n        _volCmd = false;\n      } else if (_playCmd == true) {\n        // Change song\n        if (_requestedSong != -1) {\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: _requestedVolumeLevel: %u, _requestedSong: %u, _currentFolder: %u _playCmd: 0x%x\"), _requestedVolumeLevel, _requestedSong, _currentFolder, _playCmd);\n          #endif               \n          sendPacket(0x0F, _currentFolder, _requestedSong);  // audio file in folder          \n          _requestedSong = -1; \n          _playCmd = false;\n        }           \n      } //else if (_requestedSong == 0) {\n        else if (_stopplayCmd == true) {\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: Stop playing: _stopplayCmd: 0x%x\"), _stopplayCmd);\n          #endif\n        sendPacket(0x16, 0x00, 0x00);  // Stop playing        \n        _requestedSong = -1;\n        _repeat = false; // reset repeat        \n        _stopplayCmd = false;\n        } else if (_folderCmd == true) {\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: Folder: _folderCmd: 0x%x, _requestedFolder: %d\"), _stopplayCmd, _requestedFolder);\n          #endif\n          if (_currentFolder != _requestedFolder){\n            _currentFolder = _requestedFolder;\n          }\n          _folderCmd = false;\n      } else if (_repeatCmd == true) {\n        if(_repeat == false) { // No repeat play currently\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: Repeat: _repeatCmd: 0x%x, _requestedSong: %d, _repeat: 0x0%x\"), _repeatCmd, _requestedSong, _repeat);\n          #endif \n          sendPacket(0x08, 0x00, _requestedSong);  // repeat playing audio file in root folder          \n          _requestedSong = -1;\n          _repeat = true; \n        }\n        _repeatCmd= false;      \n      } else if (_daconCmd == true) { // Always turn DAC on\n        #ifdef DIAG_I2CDFplayer_playing\n          DIAG(F(\"I2CDFPlayer: DACON: _daconCmd: 0x%x\"), _daconCmd);\n        #endif \n        sendPacket(0x1A,0,0x00);\n        _daconCmd = false;\n      } else if (_eqCmd == true){ // Set Equalizer, values 0x00 - 0x05        \n        if (_currentEQvalue != _requestedEQValue){\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: EQ: _eqCmd: 0x%x, _currentEQvalue: 0x0%x, _requestedEQValue: 0x0%x\"), _eqCmd, _currentEQvalue, _requestedEQValue);\n          #endif \n          _currentEQvalue = _requestedEQValue;\n          sendPacket(0x07,0x00,_currentEQvalue);\n        }\n        _eqCmd = false;\n      } else if (_setamCmd == true){ // Set Audio mixer channel\n         setGPIO(); // Set the audio mixer channel\n         /*        \n          if (_audioMixer == 1){ // set to audio mixer 1       \n            if (_UART_CH == 0){ \n              TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high\n            } else { // must be UART 1\n              TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high\n              }\n           //_setamCmd = false;\n           //UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);\n          } else { // set to audio mixer 2\n              if (_UART_CH == 0){ \n                TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 0 to Low\n              } else { // must be UART 1\n                TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 1 to Low\n                }\n              //_setamCmd = false;\n              //UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);\n            }*/\n        _setamCmd = false;        \n      } else if ((int32_t)currentMicros - _commandSendTime > 1000000) {\n        // Poll device every second that other commands aren't being sent,\n        // to check if it's still connected and responding.\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: Send keepalive\") );\n          #endif\n        sendPacket(0x42,0,0); \n        if (!_awaitingResponse) {\n          #ifdef DIAG_I2CDFplayer_playing\n           DIAG(F(\"I2CDFPlayer: Send keepalive, _awaitingResponse: 0x0%x\"), _awaitingResponse );\n          #endif\n          _timeoutTime = currentMicros + 5000000UL;  // Timeout if no response within 5 seconds\n          _awaitingResponse = true;\n        }\n      }\n    }  \n  }\n\n\n  // Write to a vPin will do nothing\n  void _write(VPIN vpin, int value) override {\n    (void)vpin; // suppress warning, not used in this function\n    (void)value; // suppress warning, not used in this function\n    \n    if (_deviceState == DEVSTATE_FAILED) return;\n      #ifdef DIAG_IO\n        DIAG(F(\"I2CDFPlayer: Writing to any vPin not supported\"));\n      #endif\n  }\n\n\n  // WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0.\n  // Volume may be specified as second parameter to writeAnalogue.\n  // If value is zero, the player stops playing.  \n  // WriteAnalogue on second pin sets the output volume.\n  //\n  // WriteAnalogue to be done on first vpin\n  //\n  //void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override { \n  void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t cmd=0) override { \n    if (_deviceState == DEVSTATE_FAILED) return;    \n    #ifdef DIAG_IO\n      DIAG(F(\"I2CDFPlayer: VPIN:%u FileNo:%d Volume:%d Command:0x%x\"), vpin, value, volume, cmd);\n    #endif\n    uint8_t pin = vpin - _firstVpin;\n    if (pin == 0) { // Enhanced DFPlayer commands, do nothing if not vPin 0     \n     // Read command and value\n      switch (cmd){\n       //case NONE:\n       // DFPlayerCmd = cmd;\n       // break;\n       case DF_PLAY:\n        _playCmd = true;\n        _volCmd = true;        \n        _requestedSong = value;\n        _requestedVolumeLevel = volume; \n        _playing = true;        \n        break;\n        case DF_VOL:\n          _volCmd = true;          \n          _requestedVolumeLevel = volume;\n        break;\n       case DF_FOLDER:\n        _folderCmd = true;\n        if (volume <= 0 || volume > 99){ // Range checking, valid values 1-99, else default to 1\n          _requestedFolder = 0x01; // if outside range, default to folder 01  \n        } else {\n          _requestedFolder = volume;\n        }        \n        break;\n       case DF_REPEATPLAY: // Need to check if _repeat == true, if so do nothing        \n        if (_repeat == false) {\n           #ifdef DIAG_I2CDFplayer_playing\n              DIAG(F(\"I2CDFPlayer: WriteAnalog Repeat: _repeat: 0x0%x, value: %d _repeatCmd: 0x%x\"), _repeat, value, _repeatCmd);\n           #endif\n          _repeatCmd = true;          \n          _requestedSong = value;\n          _requestedVolumeLevel = volume;\n          _playing = true;         \n        }\n        break;\n       case DF_STOPPLAY:\n        _stopplayCmd = true;        \n        break;\n       case DF_EQ:\n        #ifdef DIAG_I2CDFplayer_playing\n          DIAG(F(\"I2CDFPlayer: WriteAnalog EQ: cmd: 0x%x, EQ value: 0x%x\"), cmd, volume);\n        #endif\n        _eqCmd = true;        \n        if (volume <= 0 || volume > 5) { // If out of range, default to NORMAL\n          _requestedEQValue = DF_NORMAL;            \n        } else { // Valid EQ parameter range\n          _requestedEQValue = volume;     \n          }\n        break;        \n       case DF_RESET:\n        _resetCmd = true;      \n        break; \n       case DF_DACON: // Works, but without the DACOFF command limited value, except when not relying on DFPlayer default to turn the DAC on\n        #ifdef DIAG_I2CDFplayer_playing\n          DIAG(F(\"I2CDFPlayer: WrtieAnalog DACON: cmd: 0x%x\"), cmd);\n        #endif\n        _daconCmd = true;\n        break;\n        case DF_SETAM: // Set the audio mixer channel to 1 or 2\n          _setamCmd = true;\n          #ifdef DIAG_I2CDFplayer_playing\n            DIAG(F(\"I2CDFPlayer: WrtieAnalog SETAM: cmd: 0x%x\"), cmd);\n          #endif\n          if (volume <= 0 || volume > 2) { // If out of range, default to 1\n            _audioMixer = 1;            \n          } else { // Valid SETAM parameter in range\n              _audioMixer = volume; // _audioMixer valid values 1 or 2\n            }          \n        break;\n       default:\n        break;\n      }\n    }    \n  }\n\n  // A read on any pin indicates if the player is still playing.\n  int _read(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED) return false;\n    uint8_t pin = vpin - _firstVpin;\n      if (pin == 0) { // Do nothing if not vPin 0\n        return _playing;\n      }\n      return _playing; // fix for compile error: \"control reaches end of non-void function [-Wreturn-type]\"\n    }\n\n  void _display() override {\n    DIAG(F(\"I2CDFPlayer Configured on Vpins:%u-%u %S\"), _firstVpin, _firstVpin+_nPins-1,\n      (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n  \nprivate: \n  // DFPlayer command frame\n  // 7E FF 06 0F 00 01 01 xx xx EF\n  // 0\t  ->\t7E is start code\n  // 1\t  ->\tFF is version\n  // 2\t  ->\t06 is length\n  // 3\t  ->\t0F is command\n  // 4\t  ->\t00 is no receive\n  // 5~6\t->\t01 01 is argument\n  // 7~8\t->\tchecksum = 0 - ( FF+06+0F+00+01+01 )\n  // 9\t  ->\tEF is end code\n\n  void sendPacket(uint8_t command, uint8_t arg1 = 0, uint8_t arg2 = 0) {\n    FIFO_TX_LEVEL = 0; // Reset FIFO_TX_LEVEL    \n    uint8_t out[] = {\n        0x7E,\n        0xFF,\n        06,\n        command,\n        00,\n        //static_cast<uint8_t>(arg >> 8),\n        //static_cast<uint8_t>(arg & 0x00ff),\n        arg1,\n        arg2,\n        00,\n        00,\n        0xEF };\n\n    setChecksum(out);\n\n      // Prepend the DFPlayer command with REG address and UART Channel in _outbuffer\n      _outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel            \n      for ( uint8_t i = 1; i < sizeof(out)+1 ; i++){\n        _outbuffer[i] = out[i-1];\n      }\n\n      #ifdef DIAG_I2CDFplayer_data\n       DIAG(F(\"SC16IS752: I2C: %s Sent packet function\"), _I2CAddress.toString());\n       for (int i = 0; i < sizeof _outbuffer; i++){\n        DIAG(F(\"SC16IS752: Data _outbuffer[0x%x]: 0x%x\"), i, _outbuffer[i]);  \n       }\n      #endif\n      \n    TX_fifo_lvl();\n    if(FIFO_TX_LEVEL > 0){ //FIFO is empty\n      I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer), &_rb);\n      //I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer));\n      #ifdef DIAG_I2CDFplayer\n       DIAG(F(\"SC16IS752: I2C: %s data transmit complete on UART: 0x%x\"), _I2CAddress.toString(), _UART_CH);\n      #endif\n    } else {\n      DIAG(F(\"I2CDFPlayer at: %s, TX FIFO not empty on UART: 0x%x\"), _I2CAddress.toString(), _UART_CH);\n      _deviceState = DEVSTATE_FAILED; // This should not happen      \n    }\n    _commandSendTime = micros();\n  }\n\n  uint16_t calcChecksum(uint8_t* packet)\n  {\n    uint16_t sum = 0;\n    for (int i = 1; i < 7; i++)\n    {\n      sum += packet[i];\n    }\n    return -sum;\n  }\n\n  void setChecksum(uint8_t* out)\n  {\n    uint16_t sum = calcChecksum(out);\n    out[7] = (sum >> 8);\n    out[8] = (sum & 0xff);\n  }\n\n  // SC16IS752 functions\n  // Initialise SC16IS752 only for this channel\n  // First a software reset\n  // Enable FIFO and clear TX & RX FIFO\n  // Need to set the following registers\n  // IOCONTROL set bit 1 and 2 to 0 indicating that they are GPIO\n  // IODIR set all bit to 1 indicating al are output\n  // IOSTATE set only bit 0 to 1 for UART 0, or only bit 1 for UART 1  // \n  // LCR bit 7=0 divisor latch (clock division registers DLH & DLL, they store 16 bit divisor), \n  //     WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE\n  // MCR bit 7=0 clock divisor devide-by-1 clock input\n  // DLH most significant part of divisor\n  // DLL least significant part of divisor\n  //\n  // BAUD_RATE, WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE have been defined and initialized\n  // \n  void Init_SC16IS752(){ // Return value is in _deviceState\n    #ifdef DIAG_I2CDFplayer\n      DIAG(F(\"SC16IS752: Initialize I2C: %s , UART Ch: 0x%x\"), _I2CAddress.toString(),  _UART_CH);      \n    #endif\n    //uint16_t _divisor = (SC16IS752_XTAL_FREQ / PRESCALER) / (BAUD_RATE * 16);\n    uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16);  // Calculate _divisor for baudrate\n    TEMP_REG_VAL = 0x08; // UART Software reset\n    UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);\n\n    // Extra delay when using low frequency xtal after soft reset\n    // Test when using 1.8432 Mhz xtal\n    if(_sc16is752_xtal_freq == SC16IS752_XTAL_FREQ_LOW){\n      _timeoutTime = micros() + 10000UL;  // 10mS timeout      \n      _awaitingResponse = true;\n    }\n\n    TEMP_REG_VAL = 0x00; // Set pins to GPIO mode\n    UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);\n    TEMP_REG_VAL = 0xFF; //Set all pins as output\n    UART_WriteRegister(REG_IODIR, TEMP_REG_VAL);\n    UART_ReadRegister(REG_IOSTATE); // Read current state as not to overwrite the other GPIO pins\n    TEMP_REG_VAL = _inbuffer[0];\n    setGPIO(); // Set the audio mixer channel\n    /*\n    if (_UART_CH == 0){ // Set Audio mixer channel\n      TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high\n    } else { // must be UART 1\n      TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high\n    }\n    UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);\n    */\n    TEMP_REG_VAL = 0x07; // Reset FIFO, clear RX & TX FIFO\n    UART_WriteRegister(REG_FCR, TEMP_REG_VAL);\n    TEMP_REG_VAL = 0x00; // Set MCR to all 0, includes Clock divisor\n    UART_WriteRegister(REG_MCR, TEMP_REG_VAL);\n    TEMP_REG_VAL = 0x80 | WORD_LEN | STOP_BIT | PARITY_ENA | PARITY_TYPE;\n    UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch enabled\n    UART_WriteRegister(REG_DLL, (uint8_t)_divisor);  // Write DLL\n    UART_WriteRegister(REG_DLH, (uint8_t)(_divisor >> 8)); // Write DLH\n    UART_ReadRegister(REG_LCR);\n    TEMP_REG_VAL = _inbuffer[0] & 0x7F; // Disable Divisor latch enabled bit\n    UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch disabled  \n\n    uint8_t status = _rb.status;\n    if (status != I2C_STATUS_OK) {\n      DIAG(F(\"SC16IS752: I2C: %s failed %S\"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));\n      _deviceState = DEVSTATE_FAILED;\n    } else {\n      #ifdef DIAG_IO\n       DIAG(F(\"SC16IS752: I2C: %s, _deviceState: %S\"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));\n      #endif\n     _deviceState = DEVSTATE_NORMAL; // If I2C state is OK, then proceed to initialize DFPlayer \n    }\n  }\n\n  \n  // Read the Receive FIFO Level register (RXLVL), return a single unsigned integer\n  // of nr of characters in the RX FIFO, bit 6:0, 7 not used, set to zero\n  // value from 0 (0x00) to 64 (0x40) Only display if RX FIFO has data\n  // The RX fifo level is used to check if there are enough bytes to process a frame\n  void RX_fifo_lvl(){\n    UART_ReadRegister(REG_RXLV);\n    FIFO_RX_LEVEL = _inbuffer[0];\n    #ifdef DIAG_I2CDFplayer\n     if (FIFO_RX_LEVEL > 0){\n     //if (FIFO_RX_LEVEL > 0 && FIFO_RX_LEVEL < 10){\n      DIAG(F(\"SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_RX_LEVEL: 0d%d\"), _I2CAddress.toString(), _UART_CH, _inbuffer[0]);\n    }\n    #endif   \n  }\n\n  // When a frame is transmitted from the DFPlayer to the serial port, and at the same time the CS is sending a 42 query\n  // the following two frames from the DFPlayer are corrupt. This result in the receive buffer being out of sync and the \n  // CS will complain and generate a timeout.\n  // The RX fifo has corrupt data and need to be flushed, this function does that\n  // \n  void resetRX_fifo(){\n    #ifdef DIAG_I2CDFplayer\n      DIAG(F(\"SC16IS752: At I2C: %s, UART channel: 0x%x, RX fifo reset\"), _I2CAddress.toString(), _UART_CH);\n    #endif    \n    TEMP_REG_VAL = 0x03; // Reset RX fifo\n    UART_WriteRegister(REG_FCR, TEMP_REG_VAL);\n  }\n\n  // Set or reset GPIO pin 0 and 1 depending on the UART ch\n  // This function may be modified in a future release to enable all 8 pins to be set or reset with EX-Rail\n  // for various auxilary functions\n  void setGPIO(){\n    UART_ReadRegister(REG_IOSTATE); // Get the current GPIO pins state from the IOSTATE register\n    TEMP_REG_VAL = _inbuffer[0];\n    if (_audioMixer == 1){ // set to audio mixer 1\n      if (_UART_CH == 0){ \n        TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high\n      } else { // must be UART 1\n         TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high\n        }\n    } else { // set to audio mixer 2\n        if (_UART_CH == 0){ \n          TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 0 to Low\n        } else { // must be UART 1\n           TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 1 to Low\n          }\n      }    \n    UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);\n    _setamCmd = false;  \n  }\n  \n\n  // Read the Tranmit FIFO Level register (TXLVL), return a single unsigned integer\n  // of nr characters free in the TX FIFO, bit 6:0, 7 not used, set to zero\n  // value from 0 (0x00) to 64 (0x40)\n  //\n  void TX_fifo_lvl(){\n    UART_ReadRegister(REG_TXLV);\n    FIFO_TX_LEVEL = _inbuffer[0];\n    #ifdef DIAG_I2CDFplayer\n    //  DIAG(F(\"SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_TX_LEVEL: 0d%d\"), _I2CAddress.toString(), _UART_CH, FIFO_TX_LEVEL);\n    #endif \n  }\n\n\n  //void UART_WriteRegister(I2CAddress _I2CAddress, uint8_t _UART_CH, uint8_t UART_REG, uint8_t Val, I2CRB &_rb){\n  void UART_WriteRegister(uint8_t UART_REG, uint8_t Val){\n    _outbuffer[0] = UART_REG << 3 | _UART_CH << 1;\n    _outbuffer[1] = Val;\n    #ifdef DIAG_I2CDFplayer_reg\n      DIAG(F(\"SC16IS752: Write register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b\"), _I2CAddress.toString(), _UART_CH, UART_REG, _outbuffer[1]);\n    #endif\n    I2CManager.write(_I2CAddress, _outbuffer, 2);\n  }\n\n \n  void UART_ReadRegister(uint8_t UART_REG){\n     _outbuffer[0] = UART_REG << 3 | _UART_CH << 1; // _outbuffer[0] has now UART_REG and UART_CH\n     I2CManager.read(_I2CAddress, _inbuffer, 1, _outbuffer, 1);    \n    // _inbuffer has the REG data\n    #ifdef DIAG_I2CDFplayer_reg\n      DIAG(F(\"SC16IS752: Read register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b\"), _I2CAddress.toString(), _UART_CH, UART_REG, _inbuffer[0]);\n    #endif\n  }\n\n// SC16IS752 General register set (from the datasheet)\nenum : uint8_t{\n    REG_RHR       = 0x00, // FIFO Read\n    REG_THR       = 0x00, // FIFO Write\n    REG_IER       = 0x01, // Interrupt Enable Register R/W\n    REG_FCR       = 0x02, // FIFO Control Register Write\n    REG_IIR       = 0x02, // Interrupt Identification Register Read\n    REG_LCR       = 0x03, // Line Control Register R/W\n    REG_MCR       = 0x04, // Modem Control Register R/W\n    REG_LSR       = 0x05, // Line Status Register Read\n    REG_MSR       = 0x06, // Modem Status Register Read\n    REG_SPR       = 0x07, // Scratchpad Register R/W\n    REG_TCR       = 0x06, // Transmission Control Register R/W\n    REG_TLR       = 0x07, // Trigger Level Register R/W    \n    REG_TXLV      = 0x08, // Transmitter FIFO Level register Read\n    REG_RXLV      = 0x09, // Receiver FIFO Level register Read\n    REG_IODIR     = 0x0A, // Programmable I/O pins Direction register R/W\n    REG_IOSTATE   = 0x0B, // Programmable I/O pins State register R/W\n    REG_IOINTENA  = 0x0C, // I/O Interrupt Enable register R/W\n    REG_IOCONTROL = 0x0E, // I/O Control register R/W\n    REG_EFCR      = 0x0F, // Extra Features Control Register R/W\n  };\n\n// SC16IS752 Special register set\nenum : uint8_t{\n    REG_DLL       = 0x00, // Division registers R/W\n    REG_DLH       = 0x01, // Division registers R/W\n  };\n\n// SC16IS752 Enhanced regiter set\nenum : uint8_t{\n    REG_EFR       = 0X02, // Enhanced Features Register R/W\n    REG_XON1      = 0x04, // R/W\n    REG_XON2      = 0x05, // R/W\n    REG_XOFF1     = 0x06, // R/W\n    REG_XOFF2     = 0x07, // R/W\n  };\n\n\n// DFPlayer commands and values\n// Declared in this scope\nenum  : uint8_t{\n    DF_PLAY          = 0x0F,\n    DF_VOL           = 0x06,\n    DF_FOLDER        = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is\n    DF_REPEATPLAY    = 0x08,\n    DF_STOPPLAY      = 0x16,\n    DF_EQ            = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS\n    DF_RESET         = 0x0C,\n    DF_DACON         = 0x1A,\n    DF_SETAM         = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer   \n    DF_NORMAL        = 0x00, // Equalizer parameters\n    DF_POP           = 0x01,\n    DF_ROCK          = 0x02,\n    DF_JAZZ          = 0x03,\n    DF_CLASSIC       = 0x04,\n    DF_BASS          = 0x05,    \n  };\n\n};\n\n#endif // IO_I2CDFPlayer_h\n"
  },
  {
    "path": "IO_I2CRailcom.cpp",
    "content": "   /*\n *  © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * \n * Dec 2023, Added NXP SC16IS752 I2C Dual UART\n * The SC16IS752 has 64 bytes TX & RX FIFO buffer\n * First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be\n * needed as the RX Fifo holds the reply \n * \n * Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work  but the first transmit \n * corrupt data. This need more analysis and experimenatation. \n * Will push this driver to the dev branch with the uart fixed to 0 \n * Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)\n * \n * myHall.cpp configuration syntax:\n * \n * I2CRailcom::create(1st vPin, vPins, I2C address);\n * \n * myAutomation configuration\n *  HAL(I2CRailcom, 1st vPin, vPins, I2C address)\n * Parameters:\n * 1st vPin     : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)\n * vPins        : Total number of virtual pins allocated (to prevent overlaps)\n * I2C Address  : I2C address of the serial controller, in 0x format\n */\n\n#include \"IODevice.h\"\n#include \"IO_I2CRailcom.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"Railcom.h\"\n\n\nI2CRailcom::I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress){\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    _I2CAddress = i2cAddress;\n    addDevice(this);\n   } \n  \nvoid I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) \n    new I2CRailcom(firstVpin,nPins,i2cAddress); \n    }\n\n  void I2CRailcom::_begin() {\n    I2CManager.begin();\n    I2CManager.setClock(1000000); // TODO do we need this?\n    auto exists=I2CManager.exists(_I2CAddress);\n    DIAG(F(\"I2CRailcom: %s RailcomCollector %S detected\"), \n           _I2CAddress.toString(), exists?F(\"\"):F(\" NOT\"));\n    if (!exists) return;\n  \n    _deviceState=DEVSTATE_NORMAL;\n    _display();\n    }\n  \n  \n  void I2CRailcom::_loop(unsigned long currentMicros) {\n    (void)currentMicros; // not used, but needed to match IODevice interface\n    // Read responses from device\n    if (_deviceState!=DEVSTATE_NORMAL) return;\n    \n    // have we read this cutout already?\n    // basically we only poll once per packet when railcom cutout is working\n    auto cut=Railcom::getCutout();\n    if (cutoutCounter==cut) return; \n    cutoutCounter=cut; \n    Railcom::loop(); // in case a csv read has timed out \n    \n    // Obtain data length from the collector\n    byte inbuf[1];\n    byte queryLength[]={'?'};\n    auto state=I2CManager.read(_I2CAddress, inbuf, 1,queryLength,sizeof(queryLength)); \n    if (state) {\n      DIAG(F(\"RC ? state=%d\"),state);\n      return;\n    }\n    auto length=inbuf[0];\n    if (length==0) return;  // nothing to report \n\n    // Build a buffer and import the data from the collector\n    byte inbuf2[length];\n    byte queryData[]={'>'};\n    state=I2CManager.read(_I2CAddress, inbuf2, length,queryData,sizeof(queryData)); \n    if (state) {\n      DIAG(F(\"RC > %d state=%d\"),length,state);\n      return;\n    }\n    \n    // process incoming data buffer \n    Railcom::process(_firstVpin,inbuf2,length);\n     \n  }\n          \n \n  void I2CRailcom::_display() {\n    DIAG(F(\"I2CRailcom: %s blocks %d-%d  %S\"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,\n      (_deviceState!=DEVSTATE_NORMAL) ? F(\"OFFLINE\") : F(\"\"));\n  }\n  \n  "
  },
  {
    "path": "IO_I2CRailcom.h",
    "content": "/*\n *  © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This polls the RailcomCollecter device once per dcc packet\n * and obtains an abbreviated list of block occupancy changes which\n * are fortunately very rare compared with Railcom raw data. \n *\n * myAutomation configuration\n *  HAL(I2CRailcom, 1st vPin, vPins, I2C address)\n * Parameters:\n * 1st vPin     : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)\n * vPins        : Total number of virtual pins allocated \n * I2C Address  : I2C address of the Railcom Collector, in 0x format\n */\n\n#ifndef IO_I2CRailcom_h\n#define IO_I2CRailcom_h\n#include \"Arduino.h\"\n#include \"IODevice.h\"\n\nclass I2CRailcom : public IODevice {\nprivate: \n  byte cutoutCounter;\n  public:\n  // Constructor\n  I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress);\n  \n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) ;\n  \n  void _begin() ;\n  void _loop(unsigned long currentMicros) override ;\n  void _display() override ;\n  \nprivate: \n\n\n  \n};\n\n#endif // IO_I2CRailcom_h\n"
  },
  {
    "path": "IO_MCP23008.h",
    "content": "/*\n *  © 2022 Paul M Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef IO_MCP23008_H\n#define IO_MCP23008_H\n\n#include \"IO_GPIOBase.h\"\n\nclass MCP23008 : public GPIOBase<uint8_t> {\npublic:\n  static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new MCP23008(firstVpin, nPins, i2cAddress, interruptPin);\n  }\n\nprivate:\n  // Constructor\n  MCP23008(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)\n    : GPIOBase<uint8_t>((FSH *)F(\"MCP23008\"), firstVpin, nPins, i2cAddress, interruptPin) {\n\n    requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),\n      outputBuffer, sizeof(outputBuffer));\n    outputBuffer[0] = REG_GPIO;\n  }\n  \n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState);\n  }\n  void _writePullups() override {\n    // Set pullups only for in-use pins.  This prevents pullup being set for a pin that\n    //  is intended for use as an output but hasn't been written to yet.\n    I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse);  \n  }\n  void _writePortModes() override {\n    // Write 0 to IODIR for in-use pins that are outputs, 1 for others.\n    uint8_t temp = ~(_portMode & _portInUse);\n    I2CManager.write(_I2CAddress, 2, REG_IODIR, temp);\n    // Enable interrupt-on-change for in-use pins that are inputs (_portMode=0)\n    temp = ~_portMode & _portInUse;\n    I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00);\n    I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp);\n  }\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer;\n      I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);\n      _portInputState = buffer | _portMode;\n    } else {\n      // Queue new request\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = inputBuffer[0] | _portMode;\n    else  \n      _portInputState = 0xff;\n  }\n  void _setupDevice() override {\n    // IOCON is set ODR=1 (open drain shared interrupt pin), INTPOL=0 (active-Low)\n    I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x04);\n    _writePortModes();\n    _writePullups();\n    _writeGpioPort();\n  }\n \n  uint8_t inputBuffer[1];\n  uint8_t outputBuffer[1];\n\n  enum {\n    // Register definitions for MCP23008\n    REG_IODIR=0x00,\n    REG_GPINTEN=0x02,\n    REG_INTCON=0x04,\n    REG_IOCON=0x05,\n    REG_GPPU=0x06,\n    REG_GPIO=0x09,\n  };\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_MCP23017.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_mcp23017_h\n#define io_mcp23017_h\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for MCP23017 16-bit I/O expander.\n */\n \nclass MCP23017 : public GPIOBase<uint16_t> {\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new MCP23017(vpin, nPins, i2cAddress, interruptPin);\n  }\n\nprivate:  \n  // Constructor\n  MCP23017(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) \n    : GPIOBase<uint16_t>((FSH *)F(\"MCP23017\"), vpin, nPins, i2cAddress, interruptPin) \n  {\n    requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),\n      outputBuffer, sizeof(outputBuffer));\n    outputBuffer[0] = REG_GPIOA;\n  }\n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);\n  }\n  void _writePullups() override {\n    // Set pullups only for in-use pins.  This prevents pullup being set for a pin that\n    //  is intended for use as an output but hasn't been written to yet.\n    uint16_t temp = _portPullup & _portInUse;\n    I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8);  \n  }\n  void _writePortModes() override {\n    // Write 0 to IODIR for in-use pins that are outputs, 1 for others.\n    uint16_t temp = ~(_portMode & _portInUse);\n    I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8);\n    // Enable interrupt for in-use pins which are inputs (_portMode=0)\n    temp = ~_portMode & _portInUse;\n    I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);\n    I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8);\n  }\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer[2];\n      I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);\n      _portInputState = ((uint16_t)buffer[1]<<8) | buffer[0] | _portMode;\n    } else {\n      // Queue new request\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;\n    else  \n      _portInputState = 0xffff;\n  }\n\n  void _setupDevice() override {\n    // IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)\n    I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);\n    _writePortModes();\n    _writePullups();\n    _writeGpioPort();\n  }\n \n  uint8_t inputBuffer[2];\n  uint8_t outputBuffer[1];\n \n  enum {\n    REG_IODIRA = 0x00,\n    REG_IODIRB = 0x01,\n    REG_GPINTENA = 0x04,\n    REG_GPINTENB = 0x05,\n    REG_INTCONA = 0x08,\n    REG_INTCONB = 0x09,\n    REG_IOCON = 0x0A,\n    REG_GPPUA = 0x0C,\n    REG_GPPUB = 0x0D,\n    REG_GPIOA = 0x12,\n    REG_GPIOB = 0x13,\n  };\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_NeoPixel.h",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_NEOPIXEL.h device driver integrates with one or more Adafruit neopixel drivers.\n* This device driver will configure the device on startup, along with\n* interacting with the device for all input/output duties.\n*\n* To create NEOPIXEL devices, these are defined in myAutomation.h:\n* (Note the device driver is included by default)\n*\n* HAL(NEOPIXEL,first vpin, number of pixels,mode, i2c address) \n* e.g. HAL(NEOPIXEL,1000,64,NEO_RGB,0x60)\n* This gives each pixel in the chain an individual vpin\n* The number of pixels must match the physical pixels in the chain. \n* \n* This driver maintains a colour (rgb value in 5,5,5 bits only) plus an ON bit.\n* This can be written/read with an analog write/read call. \n* The ON bit can be set on and off with a digital write. This allows for \n* a pixel to be preset a colour and then turned on and off like any other light. \n*/\n\n#ifndef IO_EX_NeoPixel_H\n#define IO_EX_NeoPixel_H\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"FSH.h\"\n\n\n// The following macros to define the Neopixel String type\n// have been copied from the Adafruit Seesaw Library under the \n// terms of the GPL. \n// Credit to: https://github.com/adafruit/Adafruit_Seesaw\n\n// The order of primary colors in the NeoPixel data stream can vary\n// among device types, manufacturers and even different revisions of\n// the same item.  The third parameter to the seesaw_NeoPixel\n// constructor encodes the per-pixel byte offsets of the red, green\n// and blue primaries (plus white, if present) in the data stream --\n// the following #defines provide an easier-to-use named version for\n// each permutation.  e.g. NEO_GRB indicates a NeoPixel-compatible\n// device expecting three bytes per pixel, with the first byte\n// containing the green value, second containing red and third\n// containing blue.  The in-memory representation of a chain of\n// NeoPixels is the same as the data-stream order; no re-ordering of\n// bytes is required when issuing data to the chain.\n\n// Bits 5,4 of this value are the offset (0-3) from the first byte of\n// a pixel to the location of the red color byte.  Bits 3,2 are the\n// green offset and 1,0 are the blue offset.  If it is an RGBW-type\n// device (supporting a white primary in addition to R,G,B), bits 7,6\n// are the offset to the white byte...otherwise, bits 7,6 are set to\n// the same value as 5,4 (red) to indicate an RGB (not RGBW) device.\n// i.e. binary representation:\n// 0bWWRRGGBB for RGBW devices\n// 0bRRRRGGBB for RGB\n\n// RGB NeoPixel permutations; white and red offsets are always same\n// Offset:         W          R          G          B\n#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2))\n#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1))\n#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2))\n#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1))\n#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0))\n#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0))\n\n// RGBW NeoPixel permutations; all 4 offsets are distinct\n// Offset:         W          R          G          B\n#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3))\n#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2))\n#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3))\n#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2))\n#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1))\n#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1))\n\n#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3))\n#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2))\n#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3))\n#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2))\n#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1))\n#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1))\n\n#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3))\n#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2))\n#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3))\n#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2))\n#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1))\n#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1))\n\n#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0))\n#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0))\n#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0))\n#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0))\n#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0))\n#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0))\n\n// If 400 KHz support is enabled, the third parameter to the constructor\n// requires a 16-bit value (in order to select 400 vs 800 KHz speed).\n// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value\n// is sufficient to encode pixel color order, saving some space.\n\n#define NEO_KHZ800 0x0000 // 800 KHz datastream\n#define NEO_KHZ400 0x0100 // 400 KHz datastream\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for NeoPixel.\n */\n\nclass NeoPixel : public IODevice {\npublic:\n  \n  static void create(VPIN vpin, int nPins, uint16_t mode=(NEO_GRB | NEO_KHZ800), I2CAddress i2cAddress=0x60) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new NeoPixel(vpin, nPins, mode, i2cAddress);\n  }\n\nprivate:\n  \n  static const byte SEESAW_NEOPIXEL_BASE=0x0E;\n  static const byte SEESAW_NEOPIXEL_STATUS = 0x00;\n  static const byte SEESAW_NEOPIXEL_PIN = 0x01;\n  static const byte SEESAW_NEOPIXEL_SPEED = 0x02;\n  static const byte SEESAW_NEOPIXEL_BUF_LENGTH = 0x03;\n  static const byte SEESAW_NEOPIXEL_BUF=0x04;\n  static const byte SEESAW_NEOPIXEL_SHOW=0x05;\n\n  // all adafruit examples say this pin. Presumably its hard wired \n  // in the adapter anyway. \n  static const byte SEESAW_PIN15 = 15;\n  \n  // Constructor\n  NeoPixel(VPIN firstVpin, int nPins, uint16_t mode, I2CAddress i2cAddress) {\n    _firstVpin = firstVpin;\n    _nPins=nPins;\n    _I2CAddress = i2cAddress;\n    \n    // calculate the offsets into the seesaw buffer for each colour depending\n    // on the pixel strip type passed in mode.\n\n    _redOffset=4+(mode >> 4 & 0x03);\n    _greenOffset=4+(mode >> 2 & 0x03); \n    _blueOffset=4+(mode & 0x03);\n    _whiteOffset=4+(mode >> 6 & 0x03); // if this is the same as red then we are doing a RGB string and the white byte is not used. If its different then we are doing a RGBW string and the white byte is used. \n    if (_whiteOffset == _redOffset) _bytesPerPixel=3; \n    else _bytesPerPixel=4; // string has a white byte.\n    \n    _kHz800=(mode & NEO_KHZ400)==0;\n    _showPendimg=false;\n    \n    // Each pixel requires 3 bytes RGB memory.\n    // Although the driver device can remember this, it cant do off/on without\n    // forgetting what the on colour was!\n    pixelBuffer=(RGB *) malloc(_nPins*sizeof(RGB)); \n    stateBuffer=(byte *) calloc((_nPins+7)/8,sizeof(byte)); // all pixels off  \n    if (pixelBuffer==nullptr || stateBuffer==nullptr) {\n      DIAG(F(\"NeoPixel I2C:%s not enough RAM\"), _I2CAddress.toString());\n      return;\n    }\n    // preset all pins to white so a digital on/off will do something even if no colour set.\n    memset(pixelBuffer,0xFF,_nPins*sizeof(RGB));\n    addDevice(this);\n  }\n\n  void _begin() {\n    \n    // Initialise Neopixel device\n    I2CManager.begin();\n    if (!I2CManager.exists(_I2CAddress)) {\n      DIAG(F(\"NeoPixel I2C:%s device not found\"), _I2CAddress.toString());\n      _deviceState = DEVSTATE_FAILED;\n      return;\n    }\n    \n    byte speedBuffer[]={SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED,_kHz800};\n    I2CManager.write(_I2CAddress, speedBuffer, sizeof(speedBuffer));\n    \n    // In the driver there are 3 of 4 byts per pixel\n    auto numBytes=_bytesPerPixel * _nPins; \n    byte setbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH,\n                  (byte)(numBytes >> 8), (byte)(numBytes & 0xFF)};\n    I2CManager.write(_I2CAddress, setbuffer, sizeof(setbuffer));\n    \n    const byte pinbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN,SEESAW_PIN15};\n    I2CManager.write(_I2CAddress, pinbuffer, sizeof(pinbuffer));\n    \n    for (auto pin=0;pin<_nPins;pin++) transmit(pin);\n     _display();\n  }\n  \n // loop called by HAL supervisor \n  void _loop(unsigned long currentMicros) override {\n    (void)currentMicros;\n    if (!_showPendimg) return;\n    byte showBuffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_SHOW};\n    I2CManager.write(_I2CAddress,showBuffer,sizeof(showBuffer));\n    _showPendimg=false;\n  }  \n  \n  \n  // read back pixel on/off\n  int _read(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED) return 0;\n    return isPixelOn(vpin-_firstVpin);\n  }\n\n  // Write digital value. Sets pixel on or off\n  void _write(VPIN vpin, int value) override {\n    if (_deviceState == DEVSTATE_FAILED) return;\n    auto pixel=vpin-_firstVpin;\n    if (value) {\n      if (isPixelOn(pixel)) return;\n      setPixelOn(pixel);\n    }\n    else { // set off\n      if (!isPixelOn(pixel)) return;\n      setPixelOff(pixel);\n     }\n     transmit(pixel);\n  }\n   \n  VPIN _writeRange(VPIN vpin,int value, int count) {\n    // using write range cuts out the constant vpin to driver lookup so\n    // we can update multiple pixels much faster.\n    VPIN nextVpin=vpin +  (count>_nPins ? _nPins : count);\n    if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {\n      _write(vpin,value);\n      vpin++;\n    }\n    return nextVpin;  // next pin we cant \n  }  \n  // Write analogue value.\n  // The convoluted parameter mashing here is to allow passing the RGB and on/off\n  // information through the generic HAL _writeAnalog interface which was originally\n  // designed for servos and short integers  \n  void _writeAnalogue(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B) override {\n    if (_deviceState == DEVSTATE_FAILED) return;\n    RGB newColour={(byte)((colour_RG>>8) & 0xFF), (byte)(colour_RG & 0xFF), (byte)(colour_B & 0xFF)};\n    auto pixel=vpin-_firstVpin;\n    if (pixelBuffer[pixel]==newColour && isPixelOn(pixel)==(bool)onoff) return; // no change  \n      \n    if (onoff) setPixelOn(pixel); else setPixelOff(pixel);\n    pixelBuffer[pixel]=newColour;\n    transmit(pixel);\n  }\n VPIN _writeAnalogueRange(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B, int count) override {\n    // using write range cuts out the constant vpin to driver lookup so\n    VPIN nextVpin=vpin +  (count>_nPins ? _nPins : count); \n    if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {\n      _writeAnalogue(vpin,colour_RG, onoff,colour_B);\n      vpin++;\n    }\n    return nextVpin;  // next pin we cant \n }\n \n  // Display device information and status.\n  void _display() override {\n    DIAG(F(\"NeoPixel I2C:%s Vpins %u-%u %S\"),\n              _I2CAddress.toString(), \n              (int)_firstVpin, (int)_firstVpin+_nPins-1,\n              _deviceState == DEVSTATE_FAILED ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n\n  \n  bool isPixelOn(int16_t pixel) {return stateBuffer[pixel/8] & (0x80>>(pixel%8));}\n  void setPixelOn(int16_t pixel) {stateBuffer[pixel/8] |= (0x80>>(pixel%8));}\n  void setPixelOff(int16_t pixel) {stateBuffer[pixel/8] &= ~(0x80>>(pixel%8));}\n  \n  // Helper function for error handling\n  void reportError(uint8_t status, bool fail=true) {\n    DIAG(F(\"NeoPixel I2C:%s Error:%d (%S)\"), _I2CAddress.toString(), \n      status, I2CManager.getErrorMessage(status));\n    if (fail)\n    _deviceState = DEVSTATE_FAILED;\n  }\n\n  \n  void transmit(uint16_t pixel) { \n    byte buffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_BUF,0x00,0x00,0x00,0x00,0x00,0x00};\n    uint16_t offset= pixel * _bytesPerPixel;\n    buffer[2]=(byte)(offset>>8);\n    buffer[3]=(byte)(offset & 0xFF);\n    \n    if (isPixelOn(pixel)) {\n      auto colour=pixelBuffer[pixel];  \n      if (_bytesPerPixel==4 \n           && colour.red==colour.blue \n           && colour.red==colour.green) {\n           // we are doing a white pixel on a RGBW string. \n           // To save electricity we can just send the white byte and leave the RGB bytes as zero.\n           buffer[_whiteOffset]=colour.red;\n      }\n      else {\n        buffer[_redOffset]=colour.red;\n        buffer[_greenOffset]=colour.green;\n        buffer[_blueOffset]=colour.blue;\n      } \n    }\n    // else leave buffer black (in buffer preset to zeros above)\n  \n    // Transmit pixel to driver\n    I2CManager.write(_I2CAddress,buffer,4 +_bytesPerPixel);\n    _showPendimg=true;\n  \n  }\n  struct RGB { \n      byte red; \n      byte green; \n      byte blue; \n      bool operator==(const RGB& other) const {\n        return red == other.red && green == other.green && blue == other.blue;\n      }\n    };\n\n  RGB*   pixelBuffer = nullptr;\n  byte*  stateBuffer = nullptr;  // 1 bit per pixel\n  bool _showPendimg;\n  \n  // mapping of RGB onto pixel buffer for seesaw.\n  byte _bytesPerPixel;\n  byte _redOffset;\n  byte _greenOffset;\n  byte _blueOffset;\n  byte _whiteOffset; // only used if 4 bytes per pixel (RGBW string)\n  bool _kHz800; \n};\n\n#endif\n"
  },
  {
    "path": "IO_PCA9554.h",
    "content": "/*\n *  © 2025, Paul M. Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_pca9554_h\n#define io_pca9554_h\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for PCA9554/TCA9554 8-bit I/O expander (NXP & Texas Instruments).\n */\n \nclass PCA9554 : public GPIOBase<uint8_t> {\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9554(vpin,nPins, i2cAddress, interruptPin);\n  }\n\nprivate:  \n  // Constructor\n  PCA9554(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1) \n    : GPIOBase<uint8_t>((FSH *)F(\"PCA9554\"), vpin, nPins, I2CAddress, interruptPin) \n  {\n    requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),\n      outputBuffer, sizeof(outputBuffer));\n    outputBuffer[0] = REG_INPUT_P0;\n  }\n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 2, REG_OUTPUT_P0, _portOutputState);\n  }\n  void _writePullups() override {\n    // Do nothing, pull-ups are always in place for input ports\n    // This function is here for HAL GPIOBase API compatibilitiy\n      \n  }\n  void _writePortModes() override {\n    // Write 0 to REG_CONF_P0 for in-use pins that are outputs, 1 for others.\n    // PCA9554 & TCA9554, Interrupt is always enabled for raising and falling edge\n    uint8_t temp = ~(_portMode & _portInUse);\n    I2CManager.write(_I2CAddress, 2, REG_CONF_P0, temp);    \n  }\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer[1];\n      I2CManager.read(_I2CAddress, buffer, 1, 1, REG_INPUT_P0);\n      _portInputState = buffer[0];\n    } else {\n      // Queue new request\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = inputBuffer[0];\n    else  \n      _portInputState = 0xff;\n  }\n\n  void _setupDevice() override {\n    // HAL API calls\n    _writePortModes();\n    _writePullups();\n    _writeGpioPort();\n  }\n \n  uint8_t inputBuffer[1];\n  uint8_t outputBuffer[1];\n \n\n  enum {\n    REG_INPUT_P0 = 0x00,\n    REG_OUTPUT_P0 = 0x01,\n    REG_POL_INV_P0 = 0x02,\n    REG_CONF_P0 = 0x03,\n  };\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_PCA9555.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  © 2026, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_pca9555_h\n#define io_pca9555_h\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for PCA9555 16-bit I/O expander (NXP & Texas Instruments).\n */\n\nclass PCA9555 : public GPIOBase<uint16_t> {\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, char PorT='P') {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9555(vpin,nPins, i2cAddress, interruptPin,PorT);\n  }\n\nprivate:  \n  // Constructor\n  PCA9555(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin,char PorT) \n    : GPIOBase<uint16_t>((PorT=='P') ? (FSH *)F(\"PCA9555\") : (FSH *)F(\"TCA9555\"), vpin, nPins, I2CAddress, interruptPin) \n  {\n    requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),\n      outputBuffer, sizeof(outputBuffer));\n    outputBuffer[0] = REG_INPUT_P0;\n  }\n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);\n  }\n  void _writePullups() override {\n    // Do nothing, pull-ups are always in place for input ports\n    // This function is here for HAL GPIOBase API compatibilitiy\n      \n  }\n  void _writePortModes() override {\n    // Write 0 to REG_CONF_P0 & REG_CONF_P1 for in-use pins that are outputs, 1 for others.\n    // PCA9555 & TCA9555, Interrupt is always enabled for raising and falling edge\n    uint16_t temp = ~(_portMode & _portInUse);\n    I2CManager.write(_I2CAddress, 3, REG_CONF_P0, temp, temp>>8);    \n  }\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer[2];\n      I2CManager.read(_I2CAddress, buffer, 2, 1, REG_INPUT_P0);\n      _portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];\n      /* PCA9555 Int bug fix, from PCA9555 datasheet: \"must change command byte to something besides 00h \n       * after a Read operation to the PCA9555 device or before reading from \n       * another device\"\n       * Recommended solution, read from REG_OUTPUT_P0, then do nothing with the received data\n       * Issue not seen during testing, uncomment if needed \n       */\n      //I2CManager.read(_I2CAddress, buffer, 2, 1, REG_OUTPUT_P0);\n    } else {\n      // Queue new request\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = ((uint16_t)inputBuffer[1]<<8) | inputBuffer[0];\n    else  \n      _portInputState = 0xffff;\n  }\n\n  void _setupDevice() override {\n    // HAL API calls\n    _writePortModes();\n    _writePullups();\n    _writeGpioPort();\n  }\n \n  uint8_t inputBuffer[2];\n  uint8_t outputBuffer[1];\n \n\n  enum {\n    REG_INPUT_P0 = 0x00,\n    REG_INPUT_P1 = 0x01,\n    REG_OUTPUT_P0 = 0x02,\n    REG_OUTPUT_P1 = 0x03,\n    REG_POL_INV_P0 = 0x04,\n    REG_POL_INV_P1 = 0x05,\n    REG_CONF_P0 = 0x06,\n    REG_CONF_P1 = 0x07,    \n  };\n\n};\n\nclass TCA9555 {  // ALIAS for PCA9555 as they are identical for our purposes\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    PCA9555::create(vpin, nPins, i2cAddress, interruptPin,'T');\n  }\n};\n\n#endif\n"
  },
  {
    "path": "IO_PCA9685.cpp",
    "content": "/*\n *  © 2026 Paul M. Antoine\n *  © 2026 Filip Šilar\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\n// REGISTER ADDRESSES\nstatic const byte PCA9685_MODE1=0x00;      // Mode Register \nstatic const byte PCA9685_FIRST_SERVO=0x06;  /** low byte first servo register ON*/\nstatic const byte PCA9685_PRESCALE=0xFE;     /** Prescale register for PWM output frequency */\n// MODE1 bits\nstatic const byte MODE1_SLEEP=0x10;   /**< Low power mode. Oscillator off */\nstatic const byte MODE1_AI=0x20;      /**< Auto-Increment enabled */\nstatic const byte MODE1_RESTART=0x80; /**< Restart enabled */\n\nstatic const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes  */\nstatic const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed\n\n// Create device driver instance.\nvoid PCA9685::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {\n  if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new PCA9685(firstVpin, nPins, i2cAddress, frequency);\n}\n\n// Configure a port on the PCA9685.\nbool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {\n  if (configType != CONFIGURE_SERVO) return false;\n  if (paramCount != 5) return false;\n  #ifdef DIAG_IO\n  DIAG(F(\"PCA9685 Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d\"), \n    vpin, params[0], params[1], params[2], params[3], params[4]);\n  #endif\n\n  int8_t pin = vpin - _firstVpin;\n  struct ServoData *s = _servoData[pin];\n  if (s == NULL) { \n    _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));\n    s = _servoData[pin];\n    if (!s) return false; // Check for failed memory allocation\n  }\n\n  s->activePosition = params[0];\n  s->inactivePosition = params[1];\n  s->profile = params[2];\n  s->duration = params[3];\n  int state = params[4];\n\n  if (state != -1) {\n    // Position servo to initial state\n    _writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0);\n  } \n  return true;\n}\n\n// Constructor\nPCA9685::PCA9685(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {\n  _firstVpin = firstVpin;\n    _nPins = (nPins > 16) ? 16 : nPins;\n  _I2CAddress = i2cAddress;\n  // Calculate prescaler value for PWM clock\n  if (frequency > 1526) frequency = 1526;\n  else if (frequency < 24) frequency = 24;\n  prescaler = FREQUENCY_OSCILLATOR / 4096 / frequency;\n  // To save RAM, space for servo configuration is not allocated unless a pin is used.\n  // Initialise the pointers to NULL.\n  for (int i=0; i<_nPins; i++)\n    _servoData[i] = NULL;\n\n  addDevice(this);\n\n  // Initialise structure used for setting pulse rate\n  requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));\n}\n\n// Device-specific initialisation\nvoid PCA9685::_begin() {\n  I2CManager.begin();\n  I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C\n          // In reality, other devices including the Arduino will limit \n          // the clock speed to a lower rate.\n\n  // Initialise I/O module here.\n  if (I2CManager.exists(_I2CAddress)) {\n    writeRegister(PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);    \n    writeRegister(PCA9685_PRESCALE, prescaler);\n    writeRegister(PCA9685_MODE1, MODE1_AI);\n    writeRegister(PCA9685_MODE1, MODE1_RESTART | MODE1_AI);\n    // In theory, we should wait 500us before sending any other commands to each device, to allow\n    // the PWM oscillator to get running.  However, we don't do any specific wait, as there's \n    // plenty of other stuff to do before we will send a command.\n  #if defined(DIAG_IO)\n    _display();\n  #endif\n  } else\n    _deviceState = DEVSTATE_FAILED;\n}\n\n// Device-specific write function, invoked from IODevice::write().  \n// For this function, the configured profile is used.\nvoid PCA9685::_write(VPIN vpin, int value) {\n  #ifdef DIAG_IO\n  DIAG(F(\"PCA9685 Write VPIN:%u Value:%d\"), vpin, value);\n  #endif\n  int pin = vpin - _firstVpin;\n  if (value) value = 1;\n\n  struct ServoData *s = _servoData[pin];\n  if (s != NULL) {\n    // Use configured parameters\n    _writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);\n  }  else {\n     /* simulate digital pin on PWM */\n      _writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0);     \n      }\n}\n\n// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().\n// Profile is as follows:\n//  Bit 7:     0=Set PWM to 0% to power off servo motor when finished\n//             1=Keep PWM pulses on (better when using PWM to drive an LED)\n//  Bits 6-0:  0           Use specified duration (defaults to 0 deciseconds)\n//             1 (Fast)    Move servo in 0.5 seconds\n//             2 (Medium)  Move servo in 1.0 seconds\n//             3 (Slow)    Move servo in 2.0 seconds\n//             4 (Bounce)  Servo 'bounces' at extremes.\n//            \nvoid PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) {\n  #ifdef DIAG_IO\n  DIAG(F(\"PCA9685 WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S\"), \n    vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F(\"DEVSTATE_FAILED\"):F(\"\"));\n  #endif\n  if (_deviceState == DEVSTATE_FAILED) return;\n  int pin = vpin - _firstVpin;\n  if (value > 4095) value = 4095;\n  else if (value < 0) value = 0;\n\n  struct ServoData *s = _servoData[pin];\n  if (s == NULL) {\n    // Servo pin not configured, so configure now using defaults\n    s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);\n    if (s == NULL) return;  // Check for memory allocation failure\n    s->activePosition = 4095;\n    s->inactivePosition = 0;\n    s->currentPosition = value;\n    s->profile = Instant | NoPowerOff;  // Use instant profile (but not this time)\n  }\n\n  // Animated profile.  Initiate the appropriate action.\n  s->currentProfile = profile;\n  uint8_t profileValue = profile & ~NoPowerOff;  // Mask off 'don't-power-off' bit.\n  s->numSteps = profileValue==Fast ? 10 :   // 0.5 seconds\n                profileValue==Medium ? 20 : // 1.0 seconds\n                profileValue==Slow ? 40 :   // 2.0 seconds\n                profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds\n                duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)\n  s->stepNumber = 0;\n  s->toPosition = value;\n  s->fromPosition = s->currentPosition;\n}\n\n// _read returns true if the device is currently in executing an animation, \n//  changing the output over a period of time.\nint PCA9685::_read(VPIN vpin) {\n  if (_deviceState == DEVSTATE_FAILED) return 0;\n  int pin = vpin - _firstVpin;\n  struct ServoData *s = _servoData[pin];\n  if (s == NULL) \n    return false; // No structure means no animation!\n  else\n    return (s->stepNumber < s->numSteps);\n}\n\nvoid PCA9685::_loop(unsigned long currentMicros) {\n  for (int pin=0; pin<_nPins; pin++) {\n    updatePosition(pin);\n  }\n  delayUntil(currentMicros + refreshInterval * 1000UL);\n}\n\n// Private function to reposition servo\n// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.\nvoid PCA9685::updatePosition(uint8_t pin) {\n  struct ServoData *s = _servoData[pin];\n  \n  if (s == NULL) return; // No pin configuration/state data\n\n  if (s->numSteps == 0) return; // No animation in progress\n\n  if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {\n    // Go straight to end of sequence, output final position.\n    s->stepNumber = s->numSteps-1;\n  }\n\n  if (s->stepNumber < s->numSteps) {\n    // Animation in progress, reposition servo\n    s->stepNumber++;\n    if ((s->currentProfile & ~NoPowerOff) == Bounce) {\n      // Retrieve step positions from array in flash\n      byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);\n      s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);\n    } else {\n      // All other profiles - calculate step by linear interpolation between from and to positions.\n      s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);\n    }\n    // Send servo command\n    writeDevice(pin, s->currentPosition);\n  } else if (s->stepNumber < s->numSteps + _catchupSteps) {\n    // We've finished animation, wait a little to allow servo to catch up\n    s->stepNumber++;\n  } else if (s->stepNumber == s->numSteps + _catchupSteps \n            && s->currentPosition != 0) {\n#ifdef IO_SWITCH_OFF_SERVO\n    if ((s->currentProfile & NoPowerOff) == 0) {\n      // Wait has finished, so switch off PWM to prevent annoying servo buzz\n      writeDevice(pin, 0);\n    }\n#endif\n    s->numSteps = 0;  // Done now.\n  }\n}\n\n// writeDevice takes a pin in range 0 to _nPins-1 within the device, and a value\n// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.\nvoid PCA9685::writeDevice(uint8_t pin, int value) {\n  #ifdef DIAG_IO\n  DIAG(F(\"PCA9685 I2C:%s WriteDevice Pin:%d Value:%d\"), _I2CAddress.toString(), pin, value);\n  #endif\n  // Wait for previous request to complete\n  uint8_t status = requestBlock.wait();\n  if (status != I2C_STATUS_OK) {\n    _deviceState = DEVSTATE_FAILED;\n    DIAG(F(\"PCA9685 I2C:%s failed %S\"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));\n  } else {\n    // Set up new request.\n    outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;\n    outputBuffer[1] = 0;\n    outputBuffer[2] = (value == 4095 ? 0x10 : 0);  // 4095=full on\n    outputBuffer[3] = value & 0xff;\n    outputBuffer[4] = value >> 8;\n    I2CManager.queueRequest(&requestBlock);\n  }\n}\n\n// Display details of this device.\nvoid PCA9685::_display() {\n  DIAG(F(\"PCA9685 I2C:%s Configured on Vpins:%u-%u %S\"), _I2CAddress.toString(), (int)_firstVpin, \n    (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n}\n\n// Private member function for this device\nvoid PCA9685::writeRegister(byte reg, byte value) {\n  I2CManager.write(_I2CAddress, 2, reg, value);\n}\n\n// Profile for a bouncing signal or turnout\n// The profile below is in the range 0-100% and should be combined with the desired limits\n// of the servo set by _activePosition and _inactivePosition.  The profile is symmetrical here,\n// i.e. the bounce is the same on the down action as on the up action.  First entry isn't used.\nconst uint8_t FLASH PCA9685::_bounceProfile[30] = \n    {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};\n"
  },
  {
    "path": "IO_PCA9685pwm.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* \n * This driver performs the basic interface between the HAL and an \n * I2C-connected PCA9685 16-channel PWM module.  When requested, it \n * commands the device to set the PWM mark-to-period ratio accordingly.\n * The call to IODevice::writeAnalogue(vpin, value) specifies the\n * desired value in the range 0-4095 (0=0% and 4095=100%).\n * \n * This driver can be used for simple servo control by writing values between\n * about 102 and 450 (extremes of movement for 9g micro servos) or 150 to 250\n * for a more restricted range (corresponding to 1.5ms to 2.5ms pulse length).\n * A value of zero will switch off the servo.  To create the device, use\n * the following syntax:\n * \n * PCA9685_basic::create(vpin, npins, i2caddress);\n * \n * For LED control, a value of 0 is fully off, and 4095 is fully on.  It is\n * recommended, to reduce flicker of LEDs, that the frequency be configured\n * to a value higher than the default of 50Hz.  To do this, create the device\n * as follows, for a frequency of 200Hz.:\n * \n * PCA9685_basic::create(vpin, npins, i2caddress, 200);\n * \n */\n\n#ifndef PCA9685_BASIC_H\n#define PCA9685_BASIC_H\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\n/*\n * IODevice subclass for PCA9685 16-channel PWM module.\n */\n \nclass PCA9685pwm : public IODevice {\npublic:\n  // Create device driver instance.\n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency = 50) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCA9685pwm(firstVpin, nPins, i2cAddress, frequency);\n  }\n\nprivate:\n  \n  // structures for setting up non-blocking writes to PWM controller\n  I2CRB requestBlock;\n  uint8_t outputBuffer[5];\n  uint16_t prescaler;\n\n  // REGISTER ADDRESSES\n  const uint8_t PCA9685_MODE1=0x00;      // Mode Register \n  const uint8_t PCA9685_FIRST_SERVO=0x06;  /** low uint8_t first PWM register ON*/\n  const uint8_t PCA9685_PRESCALE=0xFE;     /** Prescale register for PWM output frequency */\n  // MODE1 bits\n  const uint8_t MODE1_SLEEP=0x10;   /**< Low power mode. Oscillator off */\n  const uint8_t MODE1_AI=0x20;      /**< Auto-Increment enabled */\n  const uint8_t MODE1_RESTART=0x80; /**< Restart enabled */\n\n  const uint32_t FREQUENCY_OSCILLATOR=25000000; /** Accurate enough for our purposes  */\n  const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);\n  const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed\n\n  // Constructor\n  PCA9685pwm(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {\n    _firstVpin = firstVpin;\n    _nPins = (nPins>16) ? 16 : nPins;\n    _I2CAddress = i2cAddress;\n    if (frequency > 1526) frequency = 1526;\n    else if (frequency < 24) frequency = 24;\n    prescaler = FREQUENCY_OSCILLATOR / 4096 / frequency;\n    addDevice(this);\n\n    // Initialise structure used for setting pulse rate\n    requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));\n  }\n\n  // Device-specific initialisation\n  void _begin() override {\n    I2CManager.begin();\n    I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C\n            // In reality, other devices including the Arduino will limit \n            // the clock speed to a lower rate.\n\n    // Initialise I/O module here.\n    if (I2CManager.exists(_I2CAddress)) {\n      writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);\n      writeRegister(_I2CAddress, PCA9685_PRESCALE, prescaler);\n      writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);\n      writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);\n      // In theory, we should wait 500us before sending any other commands to each device, to allow\n      // the PWM oscillator to get running.  However, we don't do any specific wait, as there's \n      // plenty of other stuff to do before we will send a command.\n    #if defined(DIAG_IO)\n      _display();\n    #endif\n    } else\n      _deviceState = DEVSTATE_FAILED;\n  }\n\n  // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().\n  //            \n  void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {\n    (void)param1; (void)param2;  // suppress compiler warning\n    #ifdef DIAG_IO\n    DIAG(F(\"PCA9685pwm WriteAnalogue VPIN:%u Value:%d %S\"), \n      vpin, value, _deviceState == DEVSTATE_FAILED?F(\"DEVSTATE_FAILED\"):F(\"\"));\n    #endif\n    if (_deviceState == DEVSTATE_FAILED) return;\n    int pin = vpin - _firstVpin;\n    if (value > 4095) value = 4095;\n    else if (value < 0) value = 0;\n\n    writeDevice(pin, value);\n  }\n\n  // Display details of this device.\n  void _display() override {\n    DIAG(F(\"PCA9685pwm I2C:%s Configured on Vpins:%u-%u %S\"), _I2CAddress.toString(), (int)_firstVpin, \n      (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n  // writeDevice (helper function) takes a pin in range 0 to _nPins-1 within the device, and a value\n  // between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.\n  void writeDevice(uint8_t pin, int value) {\n    #ifdef DIAG_IO\n    DIAG(F(\"PCA9685pwm I2C:%s WriteDevice Pin:%d Value:%d\"), _I2CAddress.toString(), pin, value);\n    #endif\n    // Wait for previous request to complete\n    uint8_t status = requestBlock.wait();\n    if (status != I2C_STATUS_OK) {\n      _deviceState = DEVSTATE_FAILED;\n      DIAG(F(\"PCA9685pwm I2C:%s failed %S\"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));\n    } else {\n      // Set up new request.\n      outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;\n      outputBuffer[1] = 0;\n      outputBuffer[2] = (value == 4095 ? 0x10 : 0);  // 4095=full on\n      outputBuffer[3] = value & 0xff;\n      outputBuffer[4] = value >> 8;\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n\n  // Internal helper function for this device\n  static void writeRegister(I2CAddress address, uint8_t reg, uint8_t value) {\n    I2CManager.write(address, 2, reg, value);\n  }\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_PCF8574.h",
    "content": "/*\n *  © 2025 Herb Morton\n *  © 2022 Paul M Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* \n * The PCF8574 is a simple device; it only has one register.  The device \n * input/output mode and pullup are configured through this, and the \n * output state is written and the input state read through it too.\n * \n * This is accomplished by having a weak resistor in series with the output,\n * and a read-back of the other end of the resistor.  As an output, the \n * pin state is set to 1 or 0, and the output voltage goes to +5V or 0V\n * (through the weak resistor).\n * \n * In order to use the pin as an input, the output is written as\n * a '1' in order to pull up the resistor.  Therefore the input will be\n * 1 unless the pin is pulled down externally, in which case it will be 0.\n * \n * As a consequence of this approach, it is not possible to use the device for\n * inputs without pullups.\n */\n\n#ifndef IO_PCF8574_H\n#define IO_PCF8574_H\n\n#include \"IO_GPIOBase.h\"\n\nclass PCF8574 : public GPIOBase<uint8_t> {\npublic:\n  static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);\n  }\n\nprivate:\n  PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)\n    : GPIOBase<uint8_t>((FSH *)F(\"PCF8574\"), firstVpin, nPins, i2cAddress, interruptPin) \n  {\n    requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);\n    if (initPortState>=0) {\n      _portMode = 255;         // set all pins to output mode\n      _portInUse = 255;        // 8 ports in use \n      _portOutputState = initPortState;  // initialize pins low-high 0-255\n      I2CManager.write(_I2CAddress, 1, initPortState);\n    }\n  }\n  \n  // The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.\n  // The pin state is driven '1' if the pin is an input, or if it is an output set to 1.\n  // Unused pins are driven '0'.\n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 1, (_portOutputState | ~_portMode) & _portInUse);\n  }\n\n  // The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.\n  // Therefore, writing '1' in _writePortModes is enough to set the module to input mode \n  // and enable pull-up.\n  void _writePullups() override { }\n\n  void _writePortModes() override {\n    _writeGpioPort();\n  }\n\n  // In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.\n  //  When not in immediate mode, it initiates a request using the request block and returns.\n  //  When the request completes, _processCompletion finishes the operation.\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer[1];\n      I2CManager.read(_I2CAddress, buffer, 1);\n      _portInputState = buffer[0] | _portMode;\n    } else {\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = inputBuffer[0] | _portMode;\n    else  \n      _portInputState = 0xff; \n  }\n\n  // Set up device ports\n  void _setupDevice() override { \n    _writePortModes();\n  }\n \n  uint8_t inputBuffer[1];\n};\n\n#endif\n"
  },
  {
    "path": "IO_PCF8575.h",
    "content": "/*\n *  © 2023, Paul Antoine, and Discord user @ADUBOURG\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* \n * The PCF8575 is a simple device; it only has one register.  The device \n * input/output mode and pullup are configured through this, and the \n * output state is written and the input state read through it too.\n * \n * This is accomplished by having a weak resistor in series with the output,\n * and a read-back of the other end of the resistor.  As an output, the \n * pin state is set to 1 or 0, and the output voltage goes to +5V or 0V\n * (through the weak resistor).\n * \n * In order to use the pin as an input, the output is written as\n * a '1' in order to pull up the resistor.  Therefore the input will be\n * 1 unless the pin is pulled down externally, in which case it will be 0.\n * \n * As a consequence of this approach, it is not possible to use the device for\n * inputs without pullups.\n */\n\n#ifndef IO_PCF8575_H\n#define IO_PCF8575_H\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\nclass PCF8575 : public GPIOBase<uint16_t> {\npublic:\n  static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8575(firstVpin, nPins, i2cAddress, interruptPin);\n  }\n\nprivate:\n  PCF8575(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)\n    : GPIOBase<uint16_t>((FSH *)F(\"PCF8575\"), firstVpin, nPins, i2cAddress, interruptPin)\n  {\n    requestBlock.setReadParams(_I2CAddress, inputBuffer, sizeof(inputBuffer));\n  }\n  \n  // The PCF8575 handles inputs by applying a weak pull-up when output is driven to '1'.\n  // The pin state is driven '1' if the pin is an input, or if it is an output set to 1.\n  // Unused pins are driven '0'.\n  void _writeGpioPort() override {\n    uint16_t bits = (_portOutputState | ~_portMode) & _portInUse;\n    I2CManager.write(_I2CAddress, 2, bits, bits>>8);\n  }\n\n  // The PCF8575 handles inputs by applying a weak pull-up when output is driven to '1'.\n  // Therefore, writing '1' in _writePortModes is enough to set the module to input mode \n  // and enable pull-up.\n  void _writePullups() override { }\n\n  // The pin state is '1' if the pin is an input or if it is an output set to 1.  Zero otherwise. \n  void _writePortModes() override {\n    _writeGpioPort();\n  }\n\n  // In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.\n  //  When not in immediate mode, it initiates a request using the request block and returns.\n  //  When the request completes, _processCompletion finishes the operation.\n  void _readGpioPort(bool immediate) override {\n    if (immediate) {\n      uint8_t buffer[2];\n      I2CManager.read(_I2CAddress, buffer, 2);\n      _portInputState = (((uint16_t)buffer[1]<<8) | buffer[0]) | _portMode;\n    } else {\n      requestBlock.wait(); // Wait for preceding operation to complete\n      // Issue new request to read GPIO register\n      I2CManager.queueRequest(&requestBlock);\n    }\n  }\n\n  // This function is invoked when an I/O operation on the requestBlock completes.\n  void _processCompletion(uint8_t status) override {\n    if (status == I2C_STATUS_OK) \n      _portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;\n    else  \n      _portInputState = 0xffff;\n  }\n\n  // Set up device ports\n  void _setupDevice() override { \n    _writePortModes();\n    _writeGpioPort();\n    _writePullups();\n  }\n \n  uint8_t inputBuffer[2];\n};\n\n#endif\n"
  },
  {
    "path": "IO_RotaryEncoder.h",
    "content": "/*\n *  © 2023, Peter Cole. All rights reserved.\n *  © 2022, Peter Cole. All rights reserved.\n *\n *  This file is part of EX-CommandStation\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\n* The IO_RotaryEncoder device driver is used to receive positions from a rotary encoder connected to an Arduino via I2C.\n*\n* There is separate code required for the Arduino the rotary encoder is connected to, which is located here:\n* https://github.com/peteGSX-Projects/dcc-ex-rotary-encoder\n*\n* This device driver receives the rotary encoder position when the rotary encoder button is pushed, and these positions\n* can be tested in EX-RAIL with:\n* ONCHANGE(vpin) - flag when the rotary encoder position has changed from the previous position\n* IFRE(vpin, position) - test to see if specified rotary encoder position has been received\n*\n* Feedback can also be sent to the rotary encoder by using 2 Vpins, and sending a SET()/RESET() to the second Vpin.\n* A SET(vpin) will flag that a turntable (or anything else) is in motion, and a RESET(vpin) that the motion has finished.\n*\n* In addition, defining a third Vpin will allow a position number to be sent so that when an EXRAIL automation or some other\n* activity has moved a turntable, the position can be reflected in the rotary encoder software. This can be accomplished\n* using the EXRAIL SERVO(vpin, position, profile) command, where:\n* - vpin = the third defined Vpin (any other is ignored)\n* - position = the defined position in the DCC-EX Rotary Encoder software, 0 (Home) to 255\n* - profile = Must be defined as per the SERVO() command, but is ignored as it has no relevance\n*\n* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples:\n*\n* #include \"IO_RotaryEncoder.h\"\n* HAL(RotaryEncoder, 700, 1, 0x67)    // Define single Vpin, no feedback or position sent to rotary encoder software\n* HAL(RotaryEncoder, 700, 2, 0x67)    // Define two Vpins, feedback only sent to rotary encoder software\n* HAL(RotaryEncoder, 700, 3, 0x67)    // Define three Vpins, can send feedback and position update to rotary encoder software\n*\n* Refer to the documentation for further information including the valid activities and examples.\n*/\n\n#ifndef IO_ROTARYENCODER_H\n#define IO_ROTARYENCODER_H\n\n#include \"EXRAIL2.h\"\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\nclass RotaryEncoder : public IODevice {\npublic:\n  \n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new RotaryEncoder(firstVpin, nPins, i2cAddress);\n  }\n\nprivate:\n  // Constructor\n  RotaryEncoder(VPIN firstVpin, int nPins, I2CAddress i2cAddress){\n    _firstVpin = firstVpin;\n    _nPins = nPins;\n    if (_nPins > 3) {\n      _nPins = 3;\n      DIAG(F(\"RotaryEncoder WARNING:%d vpins defined, only 3 supported\"), _nPins);\n    }\n    _I2CAddress = i2cAddress;\n    addDevice(this);\n  }\n\n  // Initiate the device\n  void _begin() {\n    uint8_t _status;\n    // Attempt to initilalise device\n    I2CManager.begin();\n    if (I2CManager.exists(_I2CAddress)) {\n      // Send RE_RDY, must receive RE_RDY to be online\n      _sendBuffer[0] = RE_RDY;\n      _status = I2CManager.read(_I2CAddress, _rcvBuffer, 1, _sendBuffer, 1);\n      if (_status == I2C_STATUS_OK) {\n        if (_rcvBuffer[0] == RE_RDY) {\n          _sendBuffer[0] = RE_VER;\n          if (I2CManager.read(_I2CAddress, _versionBuffer, 3, _sendBuffer, 1) == I2C_STATUS_OK) {\n            _majorVer = _versionBuffer[0];\n            _minorVer = _versionBuffer[1];\n            _patchVer = _versionBuffer[2];\n          }\n        } else {\n          DIAG(F(\"RotaryEncoder I2C:%s garbage received: %d\"), _I2CAddress.toString(), _rcvBuffer[0]);\n          _deviceState = DEVSTATE_FAILED;\n          return;\n        }\n      } else {\n        DIAG(F(\"RotaryEncoder I2C:%s ERROR connecting\"), _I2CAddress.toString());\n        _deviceState = DEVSTATE_FAILED;\n        return;\n      }\n#ifdef DIAG_IO\n      _display();\n#endif\n    } else {\n      DIAG(F(\"RotaryEncoder I2C:%s device not found\"), _I2CAddress.toString());\n      _deviceState = DEVSTATE_FAILED;\n    }\n  }\n\n  void _loop(unsigned long currentMicros) override {\n    if (_deviceState == DEVSTATE_FAILED) return;  // Return if device has failed\n    if (_i2crb.isBusy()) return;                  // Return if I2C operation still in progress\n\n    if (currentMicros - _lastPositionRead > _positionRefresh) {\n      _lastPositionRead = currentMicros;\n      _sendBuffer[0] = RE_READ;\n      I2CManager.read(_I2CAddress, _rcvBuffer, 1, _sendBuffer, 1, &_i2crb); // Read position from encoder\n      _position = _rcvBuffer[0];\n      // If EXRAIL is active, we need to trigger the ONCHANGE() event handler if it's in use\n#if defined(EXRAIL_ACTIVE)\n      if (_position != _previousPosition) {\n        _previousPosition = _position;\n        RMFT2::changeEvent(_firstVpin, 1);\n      } else {\n        RMFT2::changeEvent(_firstVpin, 0);\n      }\n#endif\n    }\n  }\n\n  // Return the position sent by the rotary encoder software\n  int _readAnalogue(VPIN vpin) override {\n    (void)vpin; // suppress warning, not used in this function\n    if (_deviceState == DEVSTATE_FAILED) return 0;\n    return _position;\n  }\n\n  // Send the feedback value to the rotary encoder software\n  void _write(VPIN vpin, int value) override {\n    if (vpin == _firstVpin + 1) {\n      if (value != 0) value = 0x01;\n      byte _feedbackBuffer[2] = {RE_OP, (byte)value};\n      I2CManager.write(_I2CAddress, _feedbackBuffer, 2);\n    }\n  }\n\n  // Send a position update to the rotary encoder software\n  // To be valid, must be 0 to 255, and different to the current position\n  // If the current position is the same, it was initiated by the rotary encoder\n  void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t duration) override {\n    (void)profile; // suppress warning, not used in this function\n    (void)duration; // suppress warning, not used in this function\n    if (vpin == _firstVpin + 2) {\n      if (position >= 0 && position <= 255 && position != _position) {\n        byte newPosition = position & 0xFF;\n        byte _positionBuffer[2] = {RE_MOVE, newPosition};\n        I2CManager.write(_I2CAddress, _positionBuffer, 2);\n      }\n    }\n  }\n  \n  void _display() override {\n    DIAG(F(\"Rotary Encoder I2C:%s v%d.%d.%d Configured on VPIN:%u-%d %S\"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer,\n      (int)_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n  int8_t _position;\n  int8_t _previousPosition = 0;\n  uint8_t _versionBuffer[3];\n  uint8_t _sendBuffer[1];\n  uint8_t _rcvBuffer[1];\n  uint8_t _majorVer = 0;\n  uint8_t _minorVer = 0;\n  uint8_t _patchVer = 0;\n  I2CRB _i2crb;\n  unsigned long _lastPositionRead = 0;\n  const unsigned long _positionRefresh = 100000UL;    // Delay refreshing position for 100ms\n\n  enum {\n    RE_RDY = 0xA0,   // Flag to check if encoder is ready for operation\n    RE_VER = 0xA1,   // Flag to retrieve rotary encoder software version\n    RE_READ = 0xA2,  // Flag to read the current position of the encoder\n    RE_OP = 0xA3,    // Flag for operation start/end, sent to when sending feedback on move start/end\n    RE_MOVE = 0xA4,  // Flag for sending a position update from the device driver to the encoder\n  };\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_Servo.cpp",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"IO_Servo.h\"\n#include \"FSH.h\"\n\n// Profile for a bouncing signal or turnout\n// The profile below is in the range 0-100% and should be combined with the desired limits\n// of the servo set by _activePosition and _inactivePosition.  The profile is symmetrical here,\n// i.e. the bounce is the same on the down action as on the up action.  First entry isn't used.\n//\n// Note: This has been put into its own .CPP file to ensure that duplicates aren't created\n// if the IO_Servo.h library is #include'd in multiple source files.\n//\nconst uint8_t FLASH Servo::_bounceProfile[30] = \n    {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};\n"
  },
  {
    "path": "IO_Servo.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * This device is a layered device which is designed to sit on top of another\n * device.  The underlying device class is expected to accept writeAnalogue calls \n * which will normally cause some physical movement of something.  The device may be a servo, \n * a motor or some other kind of positioner, and the something might be a turnout,\n * a semaphore signal or something else.  One user has used this capability for\n * moving a figure along the platform on their layout!\n * \n * Example of use:\n *    In myHal.cpp, \n * \n * #include \"IO_Servo.h\"\n * ...\n * PCA9685::create(100,16,0x40);   // First create the hardware interface device\n * Servo::create(300,16,100);    // Then create the higher level device which \n *                               // references pins 100-115 or a subset of them.\n * \n * Then any reference to pins 300-315 will cause the servo driver to send output\n * PWM commands to the corresponding PCA9685 driver pins 100-115.  The PCA9685 driver may\n * be substituted with any other driver which provides analogue output \n * capability, e.g. EX-IOExpander devices, as long as they are capable of interpreting \n * the writeAnalogue() function calls.\n */\n\n#include \"IODevice.h\"\n\n#ifndef IO_SERVO_H\n#define IO_SERVO_H\n\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n\nclass Servo : IODevice {\n\npublic: \n  enum ProfileType : uint8_t {\n    Instant = 0,  // Moves immediately between positions (if duration not specified)\n    UseDuration = 0, // Use specified duration\n    Fast = 1,     // Takes around 500ms end-to-end\n    Medium = 2,   // 1 second end-to-end\n    Slow = 3,     // 2 seconds end-to-end\n    Bounce = 4,   // For semaphores/turnouts with a bit of bounce!!\n    NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.\n  };\n\n  // Create device driver instance.\n  static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin=VPIN_NONE) {\n    new Servo(firstVpin, nPins, firstSlavePin);\n  }\n\nprivate:\n  VPIN _firstSlavePin;\n  IODevice *_slaveDevice = NULL;\n\n  struct ServoData {\n    uint16_t activePosition : 12; // Config parameter\n    uint16_t inactivePosition : 12; // Config parameter\n    uint16_t currentPosition : 12;\n    uint16_t fromPosition : 12;\n    uint16_t toPosition : 12; \n    uint8_t profile;  // Config parameter\n    uint16_t stepNumber; // Index of current step (starting from 0)\n    uint16_t numSteps;  // Number of steps in animation, or 0 if none in progress.\n    uint8_t currentProfile; // profile being used for current animation.\n    uint16_t duration; // time (tenths of a second) for animation to complete.\n  }; // 14 bytes per element, i.e. per pin in use\n  \n  struct ServoData *_servoData [16];\n\n  static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off\n  static const uint8_t FLASH _bounceProfile[30];\n\n  const unsigned int refreshInterval = 50; // refresh every 50ms\n\n\n  // Configure a port on the Servo.\n  bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {\n    if (_deviceState == DEVSTATE_FAILED) return false;\n    if (configType != CONFIGURE_SERVO) return false;\n    if (paramCount != 5) return false;\n    #ifdef DIAG_IO\n    DIAG(F(\"Servo: Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d\"), \n      vpin, params[0], params[1], params[2], params[3], params[4]);\n    #endif\n\n    int8_t pin = vpin - _firstVpin;\n    struct ServoData *s = _servoData[pin];\n    if (s == NULL) { \n      _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));\n      s = _servoData[pin];\n      if (!s) return false; // Check for failed memory allocation\n    }\n\n    s->activePosition = params[0];\n    s->inactivePosition = params[1];\n    s->profile = params[2];\n    s->duration = params[3];\n    int state = params[4];\n\n    if (state != -1) {\n      // Position servo to initial state\n      writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition);\n    } \n    return true;\n  }\n\n  // Constructor\n  Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin = VPIN_NONE) {\n    _firstVpin = firstVpin;\n    _nPins = (nPins > 16) ? 16 : nPins;\n    if (firstSlavePin == VPIN_NONE)\n      _firstSlavePin = firstVpin;\n    else\n      _firstSlavePin = firstSlavePin;\n\n    // To save RAM, space for servo configuration is not allocated unless a pin is used.\n    // Initialise the pointers to NULL.\n    for (int i=0; i<_nPins; i++)\n      _servoData[i] = NULL;\n\n    // Get reference to slave device.\n    _slaveDevice = findDevice(_firstSlavePin);\n    if (!_slaveDevice) {\n      DIAG(F(\"Servo: Slave device not found on Vpins %u-%u\"), \n        _firstSlavePin, _firstSlavePin+_nPins-1);\n      _deviceState = DEVSTATE_FAILED;\n    }      \n    if (_slaveDevice != findDevice(_firstSlavePin+_nPins-1)) {\n      DIAG(F(\"Servo: Slave device does not cover all Vpins %u-%u\"), \n        _firstSlavePin, _firstSlavePin+_nPins-1);\n      _deviceState = DEVSTATE_FAILED;\n    }\n\n    addDevice(this, _slaveDevice); // Link device ahead of slave device to intercept requests\n  }\n\n  // Device-specific initialisation\n  void _begin() override {\n    #if defined(DIAG_IO)\n    _display();\n    #endif\n  }\n\n  // Device-specific write function, invoked from IODevice::write().  \n  // For this function, the configured profile is used.\n  void _write(VPIN vpin, int value) override {\n    if (_deviceState == DEVSTATE_FAILED) return;\n    #ifdef DIAG_IO\n    DIAG(F(\"Servo Write VPIN:%u Value:%d\"), vpin, value);\n    #endif\n    int pin = vpin - _firstVpin;\n    if (value) value = 1;\n\n    struct ServoData *s = _servoData[pin];\n    if (s != NULL) {\n      // Use configured parameters\n      writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);\n    }  else {\n      /* simulate digital pin on PWM */\n      writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0);     \n    }\n  }\n\n  // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().\n  // Profile is as follows:\n  //  Bit 7:     0=Set output to 0% to power off servo motor when finished\n  //             1=Keep output at final position (better with LEDs, which will stay lit)\n  //  Bits 6-0:  0           Use specified duration (defaults to 0 deciseconds)\n  //             1 (Fast)    Move servo in 0.5 seconds\n  //             2 (Medium)  Move servo in 1.0 seconds\n  //             3 (Slow)    Move servo in 2.0 seconds\n  //             4 (Bounce)  Servo 'bounces' at extremes.\n  // Duration is in deciseconds (tenths of a second) and defaults to 0.  \n  //            \n  void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {\n    #ifdef DIAG_IO\n    DIAG(F(\"Servo: WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S\"), \n      vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F(\"DEVSTATE_FAILED\"):F(\"\"));\n    #endif\n    if (_deviceState == DEVSTATE_FAILED) return;\n    int pin = vpin - _firstVpin;\n    if (value > 4095) value = 4095;\n    else if (value < 0) value = 0;\n\n    struct ServoData *s = _servoData[pin];\n    if (s == NULL) {\n      // Servo pin not configured, so configure now using defaults\n      s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);\n      if (s == NULL) return;  // Check for memory allocation failure\n      s->activePosition = 4095;\n      s->inactivePosition = 0;\n      s->currentPosition = value;\n      s->profile = Instant | NoPowerOff;  // Use instant profile (but not this time)\n    }\n\n    // Animated profile.  Initiate the appropriate action.\n    s->currentProfile = profile;\n    uint8_t profileValue = profile & ~NoPowerOff;  // Mask off 'don't-power-off' bit.\n    s->numSteps = profileValue==Fast ? 10 :   // 0.5 seconds\n                  profileValue==Medium ? 20 : // 1.0 seconds\n                  profileValue==Slow ? 40 :   // 2.0 seconds\n                  profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds\n                  duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)\n    s->stepNumber = 0;\n    s->toPosition = value;\n    s->fromPosition = s->currentPosition;\n  }\n\n  // _read returns true if the device is currently in executing an animation, \n  //  changing the output over a period of time.\n  int _read(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED) return 0;\n    int pin = vpin - _firstVpin;\n    struct ServoData *s = _servoData[pin];\n    if (s == NULL) \n      return false; // No structure means no animation!\n    else\n      return (s->stepNumber < s->numSteps);\n  }\n\n  void _loop(unsigned long currentMicros) override {\n    if (_deviceState == DEVSTATE_FAILED) return;\n    for (int pin=0; pin<_nPins; pin++) {\n      updatePosition(pin);\n    }\n    delayUntil(currentMicros + refreshInterval * 1000UL);\n  }\n\n  // Private function to reposition servo\n  // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.\n  void updatePosition(uint8_t pin) {\n    struct ServoData *s = _servoData[pin];\n    if (s == NULL) return; // No pin configuration/state data\n\n    if (s->numSteps == 0) return; // No animation in progress\n\n    if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {\n      // Go straight to end of sequence, output final position.\n      s->stepNumber = s->numSteps-1;\n    }\n\n    if (s->stepNumber < s->numSteps) {\n      // Animation in progress, reposition servo\n      s->stepNumber++;\n      if ((s->currentProfile & ~NoPowerOff) == Bounce) {\n        // Retrieve step positions from array in flash\n        uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);\n        s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);\n      } else {\n        // All other profiles - calculate step by linear interpolation between from and to positions.\n        s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);\n      }\n      // Send servo command to output driver\n      _slaveDevice->_writeAnalogue(_firstSlavePin+pin, s->currentPosition);\n    } else if (s->stepNumber < s->numSteps + _catchupSteps) {\n      // We've finished animation, wait a little to allow servo to catch up\n      s->stepNumber++;\n    } else if (s->stepNumber == s->numSteps + _catchupSteps \n              && s->currentPosition != 0) {\n  #ifdef IO_SWITCH_OFF_SERVO\n      if ((s->currentProfile & NoPowerOff) == 0) {\n        // Wait has finished, so switch off output driver to avoid servo buzz.\n        _slaveDevice->_writeAnalogue(_firstSlavePin+pin, 0);\n      }\n  #endif\n      s->numSteps = 0;  // Done now.\n    }\n  }\n\n  // Display details of this device.\n  void _display() override {\n    DIAG(F(\"Servo Configured on Vpins:%u-%u, slave pins:%d-%d %S\"),\n      (int)_firstVpin, (int)_firstVpin+_nPins-1,\n      (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1,\n      (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n};\n\n#endif\n"
  },
  {
    "path": "IO_TCA8418.h",
    "content": "/*\n *  © 2023-2024, Paul M. Antoine\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_tca8418_h\n#define io_tca8418_h\n\n#include \"IODevice.h\"\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for TCA8418 80-key keypad encoder, which we'll treat as 80 available VPINs where\n * key down == 1 and key up == 0 by configuring just as an 8x10 keyboard matrix. Users can opt to use\n * up to all 80 of the available VPINs for now, allowing memory to be saved if not all events are required.\n * \n * The datasheet says:\n * \n * The TCA8418 can be configured to support many different configurations of keypad setups.\n * All 18 GPIOs for the rows and columns can be used to support up to 80 keys in an 8x10 key pad\n * array. Another option is that all 18 GPIOs be used for GPIs to read 18 buttons which are\n * not connected in an array. Any combination in between is also acceptable (for example, a\n * 3x4 keypad matrix and using the remaining 11 GPIOs as a combination of inputs and outputs).\n * \n * With an 8x10 key event matrix, the events are numbered as such:\n * \n *     C0  C1  C2  C3  C4  C5  C6  C7  C8  C9\n *    ========================================\n * R0|  0   1   2   3   4   5   6   7   8   9\n * R1| 10  11  12  13  14  15  16  17  18  19\n * R2| 20  21  22  23  24  25  26  27  28  29\n * R3| 30  31  32  33  34  35  36  37  38  39\n * R4| 40  41  42  43  44  45  46  47  48  49\n * R5| 50  51  52  53  54  55  56  57  58  59\n * R6| 60  61  62  63  64  65  66  67  68  69\n * R7| 70  71  72  73  74  75  76  77  78  79\n * \n * So if you start with VPIN 300, R0/C0 will be 300, and R7/C9 will be 379.\n * \n * HAL declaration for myAutomation.h is:\n * HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)\n * \n * Where numPins can be 1-80, and interruptPin can be any spare Arduino pin.\n * \n * Configure using the following on the main I2C bus:\n * HAL(TCA8418, 300, 80, 0x34)\n * \n * Use something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:\n * HAL(TCA8418, 400, 80, {SubBus_1, 0x34})\n * \n * And if needing an Interrupt pin to speed up operations:\n * HAL(TCA8418, 300, 80, 0x34, D21)\n * \n * Note that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100),\n * but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected.\n * Use any available Arduino pin for interrupt monitoring.\n */\n \nclass TCA8418 : public IODevice {\npublic:\n\n  static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (checkNoOverlap(firstVpin, nPins, i2cAddress))\n      new TCA8418(firstVpin, (nPins = (nPins > 80) ? 80 : nPins), i2cAddress, interruptPin);\n  }\n\nprivate:  \n\n  uint8_t* _digitalInputStates = NULL;  // Array of pin states\n  uint8_t _digitalPinBytes = 0;         // Number of bytes in pin state array\n\n  uint8_t _numKeyEvents = 0;            // Number of outsanding key events waiting for us\n\n  unsigned long _lastEventRead = 0;\n  unsigned long _eventRefresh = 10000UL;    // Delay refreshing events for 10ms\n  const unsigned long _eventRefreshSlow = 100000UL;   // Delay refreshing events for 100ms\n  bool _gpioInterruptsEnabled = false;\n\n  uint8_t _inputBuffer[1];\n  uint8_t _commandBuffer[1];\n  I2CRB _i2crb;\n\n  enum {RDS_IDLE, RDS_EVENT, RDS_KEYCODE};  // Read operation states\n  uint8_t _readState = RDS_IDLE;\n\n  // Constructor\n  TCA8418(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {\n    if (nPins > 0)\n    {\n      _firstVpin = firstVpin;\n      _nPins = nPins;\n      _I2CAddress = i2cAddress;\n      _gpioInterruptPin = interruptPin;\n      addDevice(this);\n    }\n  }\n\n  void _begin() {\n\n    I2CManager.begin();\n\n    if (I2CManager.exists(_I2CAddress)) {\n      // Default all GPIO pins to INPUT\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_1, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_2, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_3, 0x00);\n\n      // Remove all GPIO pins from events\n      I2CManager.write(_I2CAddress, 2, REG_GPI_EM_1, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPI_EM_2, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPI_EM_3, 0x00);\n\n      // Set all pins to FALLING interrupts\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_1, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_2, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_3, 0x00);\n\n      // Remove all GPIO pins from interrupts\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_1, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_2, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_3, 0x00);\n\n      // Set up an 8 x 10 matrix by writing 0xFF to all the row and column configs\n      // Row config is maximum of 8, and in REG_KP_GPIO_1\n      I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_1, 0xFF);\n      // Column config is maximum of 10, lower 8 bits in REG_KP_GPIO_2, upper in REG_KP_GPIO_3\n      // Set first 8 columns\n      I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_2, 0xFF);\n      // Turn on cols 9/10\n      I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_3, 0x03);\n\n      // // Set all pins to Enable Debounce\n      I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_1, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_2, 0x00);\n      I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_3, 0x00);\n\n      // Let's assume an 8x10 matrix for now, and configure \n      _digitalPinBytes = (_nPins + 7) / 8;\n      if ((_digitalInputStates = (byte *)calloc(_digitalPinBytes, 1)) == NULL) {\n        DIAG(F(\"TCA8418 I2C: Unable to alloc %d bytes\"), _digitalPinBytes);\n        return;\n      }\n\n    // Configure pin used for GPIO extender notification of change (if allocated)\n    // and configure TCA8418 to produce key event interrupts\n    if (_gpioInterruptPin >= 0) {\n      DIAG(F(\"TCA8418 I2C: interrupt pin configured on %d\"), _gpioInterruptPin);\n      _gpioInterruptsEnabled = true;\n      _eventRefresh = _eventRefreshSlow; // Switch to slower manual refreshes in case the INT pin isn't connected!\n      pinMode(_gpioInterruptPin, INPUT_PULLUP);\n      I2CManager.write(_I2CAddress, 2, REG_CFG, REG_CFG_KE_IEN);\n      // Clear any pending interrupts\n      I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);\n    }\n\n#ifdef DIAG_IO\n      _display();\n#endif\n    }\n  }\n\n  int _read(VPIN vpin) override {\n    if (_deviceState == DEVSTATE_FAILED)\n      return 0;\n    int pin = vpin - _firstVpin;\n    bool result = _digitalInputStates[pin / 8] & (1 << (pin % 8));\n    return result;\n  }\n\n\n  // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads)\n  void _loop(unsigned long currentMicros) override {\n    if (_deviceState == DEVSTATE_FAILED) return;    // If device failed, return\n\n    // Request block is used for key event reads from the TCA8418, which are performed\n    // on a cyclic basis.\n\n    if (_readState != RDS_IDLE) {\n      if (_i2crb.isBusy()) return;                // If I2C operation still in progress, return\n\n      uint8_t status = _i2crb.status;\n      if (status == I2C_STATUS_OK) {             // If device request ok, read input data\n\n        // First check if we have any key events waiting\n        if (_readState == RDS_EVENT) {\n          if ((_numKeyEvents = (_inputBuffer[0] & 0x0F)) != 0) {\n            // We could read each key event waiting in a synchronous loop, which may prove preferable\n            // but for now, schedule an async read of the first key event in the queue\n            _commandBuffer[0] = REG_KEY_EVENT_A;\n            I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb);  // non-blocking read\n            _readState = RDS_KEYCODE; // Shift to reading key events!\n          }\n          else // We found no key events waiting, return to IDLE\n            _readState = RDS_IDLE;\n        }\n         else {\n          // RDS_KEYCODE\n          uint8_t key = _inputBuffer[0] & 0x7F;\n          bool keyDown = _inputBuffer[0] & 0x80;\n          // Check for just keypad events\n          key--; // R0/C0 is key #1, so subtract 1 to create an array offset\n          // We only want to record key events we're configured for, as we have calloc'd an\n          // appropriately sized _digitalInputStates array!\n          if (key < _nPins) {\n            if (keyDown)\n              _digitalInputStates[key / 8] |= (1 << (key % 8));\n            else\n              _digitalInputStates[key / 8] &= ~(1 << (key % 8));\n          }\n          else\n            DIAG(F(\"TCA8418 I2C: key event %d discarded, outside Vpin range\"), key);\n          _numKeyEvents--; // One less key event to get\n          if (_numKeyEvents != 0)\n          {\n            // DIAG(F(\"TCA8418 I2C: more keys in read event queue, # waiting is: %x\"), _numKeyEvents);\n            // We could read each key event waiting in a synchronous loop, which may prove preferable\n            // but for now, schedule an async read of the first key event in the queue\n            _commandBuffer[0] = REG_KEY_EVENT_A;\n            I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read\n          }\n          else {\n            // DIAG(F(\"TCA8418 I2C: no more keys in read event queue\"));\n            // Clear any pending interrupts\n            I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);\n            _readState = RDS_IDLE; // Shift to IDLE\n            return;\n          }\n         }\n      } else\n        reportError(status, false);   // report eror but don't go offline.\n    }\n\n    // If we're not doing anything now, check to see if we have an interrupt pin configured and it is low,\n    // or if our timer has elapsed and we should check anyway in case the interrupt pin is disconnected.\n    if (_readState == RDS_IDLE) {\n      if ((_gpioInterruptsEnabled && !digitalRead(_gpioInterruptPin)) ||\n        ((currentMicros - _lastEventRead) > _eventRefresh))\n      {\n        _commandBuffer[0] = REG_KEY_LCK_EC;\n        I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb);  // non-blocking read\n        _lastEventRead = currentMicros;\n        _readState = RDS_EVENT; // Shift to looking for key events!\n      }\n    }\n  }\n\n  // Display device information and status\n  void _display() override {\n    DIAG(F(\"TCA8418 I2C:%s Vpins %u-%u%S\"),\n              _I2CAddress.toString(),\n              _firstVpin, (_firstVpin+_nPins-1),\n              _deviceState == DEVSTATE_FAILED ? F(\" OFFLINE\") : F(\"\"));\n    if (_gpioInterruptsEnabled)\n      DIAG(F(\"TCA8418 I2C:Interrupt on pin %d\"), _gpioInterruptPin);\n  }\n\n  // Helper function for error handling\n  void reportError(uint8_t status, bool fail=true) {\n    DIAG(F(\"TCA8418 I2C:%s Error:%d (%S)\"), _I2CAddress.toString(), \n      status, I2CManager.getErrorMessage(status));\n    if (fail)\n    _deviceState = DEVSTATE_FAILED;\n  }\n\n  enum tca8418_registers\n  {\n    // REG_RESERVED = 0x00\n    REG_CFG = 0x01,             // Configuration register\n    REG_INT_STAT = 0x02,        // Interrupt status\n    REG_KEY_LCK_EC = 0x03,      // Key lock and event counter\n    REG_KEY_EVENT_A = 0x04,     // Key event register A\n    REG_KEY_EVENT_B = 0x05,     // Key event register B\n    REG_KEY_EVENT_C = 0x06,     // Key event register C\n    REG_KEY_EVENT_D = 0x07,     // Key event register D\n    REG_KEY_EVENT_E = 0x08,     // Key event register E\n    REG_KEY_EVENT_F = 0x09,     // Key event register F\n    REG_KEY_EVENT_G = 0x0A,     // Key event register G\n    REG_KEY_EVENT_H = 0x0B,     // Key event register H\n    REG_KEY_EVENT_I = 0x0C,     // Key event register I\n    REG_KEY_EVENT_J = 0x0D,     // Key event register J\n    REG_KP_LCK_TIMER = 0x0E,    // Keypad lock1 to lock2 timer\n    REG_UNLOCK_1 = 0x0F,        // Unlock register 1\n    REG_UNLOCK_2 = 0x10,        // Unlock register 2\n    REG_GPIO_INT_STAT_1 = 0x11, // GPIO interrupt status 1\n    REG_GPIO_INT_STAT_2 = 0x12, // GPIO interrupt status 2\n    REG_GPIO_INT_STAT_3 = 0x13, // GPIO interrupt status 3\n    REG_GPIO_DAT_STAT_1 = 0x14, // GPIO data status 1\n    REG_GPIO_DAT_STAT_2 = 0x15, // GPIO data status 2\n    REG_GPIO_DAT_STAT_3 = 0x16, // GPIO data status 3\n    REG_GPIO_DAT_OUT_1 = 0x17,  // GPIO data out 1\n    REG_GPIO_DAT_OUT_2 = 0x18,  // GPIO data out 2\n    REG_GPIO_DAT_OUT_3 = 0x19,  // GPIO data out 3\n    REG_GPIO_INT_EN_1 = 0x1A,   // GPIO interrupt enable 1\n    REG_GPIO_INT_EN_2 = 0x1B,   // GPIO interrupt enable 2\n    REG_GPIO_INT_EN_3 = 0x1C,   // GPIO interrupt enable 3\n    REG_KP_GPIO_1 = 0x1D,       // Keypad/GPIO select 1\n    REG_KP_GPIO_2 = 0x1E,       // Keypad/GPIO select 2\n    REG_KP_GPIO_3 = 0x1F,       // Keypad/GPIO select 3\n    REG_GPI_EM_1 = 0x20,        // GPI event mode 1\n    REG_GPI_EM_2 = 0x21,        // GPI event mode 2\n    REG_GPI_EM_3 = 0x22,        // GPI event mode 3\n    REG_GPIO_DIR_1 = 0x23,      // GPIO data direction 1\n    REG_GPIO_DIR_2 = 0x24,      // GPIO data direction 2\n    REG_GPIO_DIR_3 = 0x25,      // GPIO data direction 3\n    REG_GPIO_INT_LVL_1 = 0x26,  // GPIO edge/level detect 1\n    REG_GPIO_INT_LVL_2 = 0x27,  // GPIO edge/level detect 2\n    REG_GPIO_INT_LVL_3 = 0x28,  // GPIO edge/level detect 3\n    REG_DEBOUNCE_DIS_1 = 0x29,  // Debounce disable 1\n    REG_DEBOUNCE_DIS_2 = 0x2A,  // Debounce disable 2\n    REG_DEBOUNCE_DIS_3 = 0x2B,  // Debounce disable 3\n    REG_GPIO_PULL_1 = 0x2C,     // GPIO pull-up disable 1\n    REG_GPIO_PULL_2 = 0x2D,     // GPIO pull-up disable 2\n    REG_GPIO_PULL_3 = 0x2E,     // GPIO pull-up disable 3\n    // REG_RESERVED = 0x2F\n  };\n\n  enum tca8418_config_reg_fields\n  {\n    //  Config Register #1 fields\n    REG_CFG_AI = 0x80,           // Auto-increment for read/write\n    REG_CFG_GPI_E_CGF = 0x40,    // Event mode config\n    REG_CFG_OVR_FLOW_M = 0x20,   // Overflow mode enable\n    REG_CFG_INT_CFG = 0x10,      // Interrupt config\n    REG_CFG_OVR_FLOW_IEN = 0x08, // Overflow interrupt enable\n    REG_CFG_K_LCK_IEN = 0x04,    // Keypad lock interrupt enable\n    REG_CFG_GPI_IEN = 0x02,      // GPI interrupt enable\n    REG_CFG_KE_IEN = 0x01,       // Key events interrupt enable\n  };\n\n  enum tca8418_int_status_fields\n  {\n    //  Interrupt Status Register #2 fields\n    REG_STAT_CAD_INT = 0x10,      // Ctrl-alt-del seq status\n    REG_STAT_OVR_FLOW_INT = 0x08, // Overflow interrupt status\n    REG_STAT_K_LCK_INT = 0x04,    // Key lock interrupt status\n    REG_STAT_GPI_INT = 0x02,      // GPI interrupt status\n    REG_STAT_K_INT = 0x01,        // Key events interrupt status\n  };\n\n  enum tca8418_lock_ec_fields\n  {\n    // Key Lock Event Count Register #3\n    REG_LCK_EC_K_LCK_EN = 0x40, // Key lock enable\n    REG_LCK_EC_LCK_2 = 0x20,    // Keypad lock status 2\n    REG_LCK_EC_LCK_1 = 0x10,    // Keypad lock status 1\n    REG_LCK_EC_KLEC_3 = 0x08,   // Key event count bit 3\n    REG_LCK_EC_KLEC_2 = 0x04,   // Key event count bit 2\n    REG_LCK_EC_KLEC_1 = 0x02,   // Key event count bit 1\n    REG_LCK_EC_KLEC_0 = 0x01,   // Key event count bit 0\n  };\n};\n\n#endif\n"
  },
  {
    "path": "IO_TM1638.cpp",
    "content": "/*\n *  © 2024, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* Credit to https://github.com/dvarrel/TM1638 for the basic formulae.*/\n\n\n#include <Arduino.h>\n#include \"IODevice.h\"\n#include \"IO_TM1638.h\"\n#include \"DIAG.h\"\n\n   \nconst uint8_t HIGHFLASH _digits[16]={\n      0b00111111,0b00000110,0b01011011,0b01001111,\n      0b01100110,0b01101101,0b01111101,0b00000111,\n      0b01111111,0b01101111,0b01110111,0b01111100,\n      0b00111001,0b01011110,0b01111001,0b01110001\n    };\n\n  // Constructor\n   TM1638::TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin){\n    _firstVpin = firstVpin;\n    _nPins = 8;\n    _clk_pin = clk_pin;\n    _stb_pin = stb_pin;\n    _dio_pin = dio_pin;\n    pinMode(clk_pin,OUTPUT);\n    pinMode(stb_pin,OUTPUT);\n    pinMode(dio_pin,OUTPUT);\n    _pulse = PULSE1_16;\n      \n    _buttons=0;\n    _leds=0;\n    _lastLoop=micros();\n    addDevice(this);\n   } \n\n    \n  void TM1638::create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin) {\n    if (checkNoOverlap(firstVpin,8)) \n     new TM1638(firstVpin, clk_pin,dio_pin,stb_pin); \n  }\n\n  void TM1638::_begin()  {\n    displayClear();\n    test();\n    _display();\n  }\n  \n \n  void TM1638::_loop(unsigned long currentMicros)  {\n     if (currentMicros - _lastLoop > (1000000UL/LoopHz)) {\n         _buttons=getButtons();// Read the buttons\n         _lastLoop=currentMicros;   \n     } \n  }\n           \n  void TM1638::_display()  {\n    DIAG(F(\"TM1638 Configured on Vpins:%u-%u\"), _firstVpin, _firstVpin+_nPins-1);\n  }\n\n// digital read gets button state \nint TM1638::_read(VPIN vpin)  {\n  byte pin=vpin - _firstVpin;\n  bool result=bitRead(_buttons,pin);\n  // DIAG(F(\"TM1638 read (%d) buttons %x = %d\"),pin,_buttons,result);  \n  return result;\n}\n\n// digital write sets led state \nvoid TM1638::_write(VPIN vpin, int value)  {\n  // TODO.. skip if no state change  \n  writeLed(vpin - _firstVpin + 1,value!=0);\n  }\n\n// Analog write sets digit displays \n\nvoid TM1638::_writeAnalogue(VPIN vpin, int lowBytes, uint8_t mode, uint16_t highBytes)  {  \n   // mode is in DataFormat defined above.\n   byte formatLength=mode & 0x0F;  // last 4 bits \n   byte formatType=mode & 0xF0;         //           \n   int8_t leftDigit=vpin-_firstVpin; // 0..7 from left\n   int8_t rightDigit=leftDigit+formatLength-1; // 0..7 from left\n   \n   // loading is done right to left startDigit first\n   int8_t startDigit=7-rightDigit; // reverse as 7 on left\n   int8_t lastDigit=7-leftDigit; // reverse as 7 on left\n   uint32_t value=highBytes;\n   value<<=16;\n   value |= (uint16_t)lowBytes;\n   \n   //DIAG(F(\"TM1638 fl=%d ft=%x sd=%d ld=%d v=%l vx=%X\"),\n   // formatLength,formatType,startDigit,lastDigit,value,value);\n    while(startDigit<=lastDigit) {\n        switch (formatType) {\n            case _DF_DECIMAL:// decimal (leading zeros)\n                displayDig(startDigit,GETHIGHFLASH(_digits,(value%10))); \n                value=value/10;\n                break; \n            case _DF_HEX:// HEX (leading zeros)\n                displayDig(startDigit,GETHIGHFLASH(_digits,(value & 0x0F))); \n                value>>=4;\n                break;  \n            case _DF_RAW:// Raw 7-segment pattern \n                displayDig(startDigit,value & 0xFF); \n                value>>=8;\n                break;\n            default:\n                DIAG(F(\"TM1368 invalid mode 0x%x\"),mode);\n                return;\n            }\n    startDigit++;\n    } \n}\n     \nuint8_t TM1638::getButtons(){\n  ArduinoPins::fastWriteDigital(_stb_pin, LOW);\n  writeData(INSTRUCTION_READ_KEY);\n  pinMode(_dio_pin, INPUT);\n  ArduinoPins::fastWriteDigital(_clk_pin, LOW);\n  uint8_t buttons=0;\n  for (uint8_t eachByte=0; eachByte<4;eachByte++) {\n    uint8_t value = 0;\n\t  for (uint8_t eachBit = 0; eachBit < 8; eachBit++) {\n\t\t  ArduinoPins::fastWriteDigital(_clk_pin, HIGH);\n\t\t\tvalue |= ArduinoPins::fastReadDigital(_dio_pin) << eachBit;\n\t\t  ArduinoPins::fastWriteDigital(_clk_pin, LOW);\n\t  }\n    buttons |= value << eachByte; \n    delayMicroseconds(1);\n  }\n  pinMode(_dio_pin, OUTPUT);\n  ArduinoPins::fastWriteDigital(_stb_pin, HIGH);\n  return buttons;\n}\n\n\nvoid TM1638::displayDig(uint8_t digitId, uint8_t pgfedcba){\n  if (digitId>7) return;\n  setDataInstruction(DISPLAY_TURN_ON | _pulse);\n  setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_FIXED);\n  writeDataAt(FIRST_DISPLAY_ADDRESS+14-(digitId*2), pgfedcba);\n}\n\nvoid TM1638::displayClear(){\n  setDataInstruction(DISPLAY_TURN_ON | _pulse);\n  setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);\n  for (uint8_t i=0;i<15;i+=2){\n    writeDataAt(FIRST_DISPLAY_ADDRESS+i,0x00);\n  }\n}\n\nvoid TM1638::writeLed(uint8_t num,bool state){\n  if ((num<1) | (num>8)) return;\n  setDataInstruction(DISPLAY_TURN_ON | _pulse);\n  setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);\n  writeDataAt(FIRST_DISPLAY_ADDRESS + (num*2-1), state);\n}\n\n\nvoid TM1638::writeData(uint8_t data){\n\tfor (uint8_t i = 0; i < 8; i++)  {\n\t\tArduinoPins::fastWriteDigital(_dio_pin, data & 1);\n\t\tdata >>= 1;\n\t\tArduinoPins::fastWriteDigital(_clk_pin, HIGH);\n\t\tArduinoPins::fastWriteDigital(_clk_pin, LOW);\t\t\n\t}\n} \n\nvoid TM1638::writeDataAt(uint8_t displayAddress, uint8_t data){\n    ArduinoPins::fastWriteDigital(_stb_pin, LOW);\n    writeData(displayAddress);\n    writeData(data);\n    ArduinoPins::fastWriteDigital(_stb_pin, HIGH);\n    delayMicroseconds(1);\n}\n\nvoid TM1638::setDataInstruction(uint8_t dataInstruction){\n  ArduinoPins::fastWriteDigital(_stb_pin, LOW);\n  writeData(dataInstruction);\n  ArduinoPins::fastWriteDigital(_stb_pin, HIGH);\n  delayMicroseconds(1);  \n}\n\nvoid TM1638::test(){\n  DIAG(F(\"TM1638 test\"));\n  uint8_t val=0;\n  for(uint8_t i=0;i<5;i++){\n    setDataInstruction(DISPLAY_TURN_ON | _pulse);\n    setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_AUTO);\n    ArduinoPins::fastWriteDigital(_stb_pin, LOW);\n    writeData(FIRST_DISPLAY_ADDRESS);\n    for(uint8_t i=0;i<16;i++)\n      writeData(val);\n    ArduinoPins::fastWriteDigital(_stb_pin, HIGH);\n    delay(1000);\n    val = ~val;\n  }\n\n}\n"
  },
  {
    "path": "IO_TM1638.h",
    "content": "   /*\n *  © 2024, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef IO_TM1638_h\n#define IO_TM1638_h\n#include <Arduino.h>\n#include \"IODevice.h\"\n#include \"DIAG.h\"\n\nclass TM1638 : public IODevice {\nprivate: \n      \n    uint8_t _buttons;\n    uint8_t _leds;\n    unsigned long _lastLoop;\n    static const int LoopHz=20; \n\n    static const byte \n    INSTRUCTION_WRITE_DATA=0x40,\n    INSTRUCTION_READ_KEY=0x42,\n    INSTRUCTION_ADDRESS_AUTO=0x40,\n    INSTRUCTION_ADDRESS_FIXED=0x44,\n    INSTRUCTION_NORMAL_MODE=0x40,\n    INSTRUCTION_TEST_MODE=0x48,\n\n    FIRST_DISPLAY_ADDRESS=0xC0,\n\n    DISPLAY_TURN_OFF=0x80,\n    DISPLAY_TURN_ON=0x88;\n\n        \n    uint8_t _clk_pin;\n    uint8_t _stb_pin;\n    uint8_t _dio_pin;\n    uint8_t _pulse;\n    bool _isOn;\n\n    \n  // Constructor\n   TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);\n\npublic:\n  enum DigitFormat : byte {\n    // last 4 bits are length.\n    // DF_1.. DF_8 decimal \n    DF_1=0x01,DF_2=0x02,DF_3=0x03,DF_4=0x04,\n    DF_5=0x05,DF_6=0x06,DF_7=0x07,DF_8=0x08,\n    // DF_1X.. DF_8X HEX \n    DF_1X=0x11,DF_2X=0x12,DF_3X=0x13,DF_4X=0x14,\n    DF_5X=0x15,DF_6X=0x16,DF_7X=0x17,DF_8X=0x18,\n    // DF_1R .. DF_4R raw 7 segmnent data \n    // only 4 because HAL analogWrite only passes 4 bytes \n    DF_1R=0x21,DF_2R=0x22,DF_3R=0x23,DF_4R=0x24,\n    \n    //  bits of data conversion type  (ored with length) \n    _DF_DECIMAL=0x00,// right adjusted decimal unsigned leading zeros\n    _DF_HEX=0x10,    // right adjusted hex leading zeros \n    _DF_RAW=0x20 // bytes are raw 7-segment pattern (max length 4)\n  };\n\n  static void create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);\n  \n  // Functions overridden in IODevice\n  void _begin();\n  void _loop(unsigned long currentMicros) override ;\n  void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;\n  void _display() override ;\n  int _read(VPIN pin) override;\n  void _write(VPIN pin,int value) override;\n\n  // Device driving functions \n  private:\n   enum pulse_t {\n      PULSE1_16,\n      PULSE2_16,\n      PULSE4_16,\n      PULSE10_16,\n      PULSE11_16,\n      PULSE12_16,\n      PULSE13_16,\n      PULSE14_16\n    };\n\n  /**\n    * @fn getButtons\n    * @return state of 8 buttons\n    */\n    uint8_t getButtons();\n\n    /**\n    * @fn writeLed\n    * @brief put led ON or OFF\n    * @param num num of led(1-8)\n    * @param state (true or false)\n    */\n    void writeLed(uint8_t num, bool state);\n    \n    \n    /**\n    * @fn displayDig\n    * @brief set 7 segment display + dot\n    * @param digitId num of digit(0-7)\n    * @param val value 8 bits\n    */\n    void displayDig(uint8_t digitId, uint8_t pgfedcba);\n\n    /**\n    * @fn displayClear\n    * @brief switch off all leds and segment display\n    */\n    void displayClear();\n    void test();\n    void writeData(uint8_t data);\n    void writeDataAt(uint8_t displayAddress, uint8_t data);\n    void setDisplayMode(uint8_t displayMode);\n    void setDataInstruction(uint8_t dataInstruction);\n};\n#endif\n"
  },
  {
    "path": "IO_TouchKeypad.h",
    "content": "/*\n *  © 2023, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* \n * Driver for capacitative touch-pad based on the TTP229-B chip with serial \n * (not I2C) output.  The touchpad has 16 separate pads in a 4x4 matrix, \n * numbered 1-16.  The communications with the pad are via a clock signal sent\n * from the controller to the device, and a data signal sent back by the device.\n * The pins clockPin and dataPin must be local pins, not external (GPIO Expander)\n * pins.\n * \n * To use, \n *    TouchKeypad::create(firstVpin, 16, clockPin, dataPin);\n * \n * NOTE: Most of these keypads ship with only 8 pads enabled.  To enable all\n * sixteen pads, locate the area of the board labelled P1 (four pairs of \n * holes labelled 1 to 4 from the left); solder a jumper link between the pair \n * labelled 3 (connected to pin TP2 on the chip).  When this link is connected,\n * the pins OUT1 to OUT8 are not used but all sixteen touch pads are operational.\n * \n * TODO: Allow a list of datapins to be provided so that multiple keypads can\n * be read simultaneously by the one device driver and the one shared clock signal.\n * As it stands, we can configure multiple driver instances, one for each keypad, \n * and it will work fine.  The clock will be driven to all devices but only one \n * driver will be reading the responses from its corresponding device at a time.\n */\n\n#ifndef IO_TOUCHKEYPAD_H\n#define IO_TOUCHKEYPAD_H\n\n#include \"IODevice.h\"\n\nclass TouchKeypad : public IODevice {\nprivate:\n  // Here we define the device-specific variables.  \n  uint16_t _inputStates = 0;\n  VPIN _clockPin;\n  VPIN _dataPin;\n\npublic:\n  //  Static function to handle create calls.\n  static void create(VPIN firstVpin, int nPins, VPIN clockPin, VPIN dataPin) {\n    if (checkNoOverlap(firstVpin,nPins)) new TouchKeypad(firstVpin, nPins, clockPin, dataPin);\n  } \n\nprotected:\n  // Constructor. \n  TouchKeypad(VPIN firstVpin, int nPins, VPIN clockPin, VPIN dataPin) {\n    _firstVpin = firstVpin;\n    _nPins = (nPins > 16) ? 16 : nPins;  // Maximum of 16 pads per device\n    _clockPin = clockPin;\n    _dataPin = dataPin;\n\n    addDevice(this);\n  }\n\n  // Device-specific initialisation\n  void _begin() override {\n#if defined(DIAG_IO)\n    _display();\n#endif\n    // Set clock pin as output, initially high, and data pin as input.  \n    // Enable pullup on the input so that the default (not connected) state is \n    // 'keypad not pressed'.\n    ArduinoPins::fastWriteDigital(_clockPin, 1);\n    pinMode(_clockPin, OUTPUT);\n    pinMode(_dataPin, INPUT_PULLUP); // Force defined state when no connection\n  }\n  \n  // Device-specific read function.\n  int _read(VPIN vpin) {\n    if (vpin < _firstVpin || vpin >= _firstVpin + _nPins) return 0;\n\n    // Return a value for the specified vpin.\n    return _inputStates & (1<<(vpin-_firstVpin)) ? 1 : 0;\n  }\n\n  // Loop function to do background scanning of the keyboard.\n  // The TTP229 device requires clock pulses to be sent to it,\n  // and the data bits can be read on the rising edge of the clock.\n  // By default the clock and data are inverted (active-low).\n  // A gap of more  than 2ms is advised between successive read\n  // cycles, we wait for 100ms between reads of the keyboard as this\n  // provide a good enough response time.\n  // Maximum clock frequency is 512kHz, so put a 1us delay\n  // between clock transitions.\n  //\n  void _loop(unsigned long currentMicros) {\n\n    // Clock 16 bits from the device\n    uint16_t data = 0, maskBit = 0x01;\n    for (uint8_t pad=0; pad<16; pad++) {\n      ArduinoPins::fastWriteDigital(_clockPin, 0);\n      delayMicroseconds(1);\n      ArduinoPins::fastWriteDigital(_clockPin, 1);\n      data |= (ArduinoPins::fastReadDigital(_dataPin) ? 0 : maskBit);\n      maskBit <<= 1;\n      delayMicroseconds(1);\n    }\n    _inputStates = data;\n#ifdef DIAG_IO\n    static uint16_t lastData = 0;\n    if (data != lastData) DIAG(F(\"KeyPad: %x\"), data);\n    lastData = data;\n#endif\n    delayUntil(currentMicros + 100000); // read every 100ms\n  }\n\n  // Display information about the device, and perhaps its current condition (e.g. active, disabled etc).\n  void _display() {\n    DIAG(F(\"TouchKeypad Configured on Vpins:%u-%u SCL=%d SDO=%d\"), (int)_firstVpin, \n      (int)_firstVpin+_nPins-1, _clockPin, _dataPin);\n  }\n\n\n};\n\n#endif // IO_TOUCHKEYPAD_H\n"
  },
  {
    "path": "IO_VL53L0X.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting\n * the reflection of the pulse.  The time between the pulse and the receipt of reflections\n * is measured and used to determine the distance to the reflecting object.\n * \n * For economy of memory and processing time, this driver includes only part of the code \n * that ST provide in their API.  Also, the API code isn't very clear and it is not easy\n * to identify what operations are useful and what are not. \n * The operation shown here doesn't include any calibration, so is probably not as accurate\n * as using the full driver, but it's probably accurate enough for the purpose.\n * \n * The device driver allocates up to 3 vpins to the device.  A digital read on the first pin\n * will return a value that indicates whether the object is within the threshold range (1)\n * or not (0).  An analogue read on the first pin returns the last measured distance (in mm), \n * the second pin returns the signal strength, and the third pin returns detected \n * ambient light level.  By default the device takes around 60ms to complete a ranging \n * operation, so we do a 100ms cycle (10 samples per second).\n * \n * The VL53L0X is initially set to respond to I2C address 0x29.  If you only have one module,\n * you can use this address.  However, the address can be modified by software.  If\n * you select another address, that address will be written to the device and used until the device is reset.\n * \n * If you have more than one module, then you will need to specify a digital VPIN (Arduino\n * digital output or I/O extender pin) which you connect to the module's XSHUT pin.  Now,\n * when the device driver starts, the XSHUT pin is set LOW to turn the module off.  Once \n * all VL53L0X modules are turned off, the driver works through each module in turn,\n * setting XSHUT to HIGH to turn that module on, then writing that module's desired I2C address.\n * In this way, many VL53L0X modules can be connected to the one I2C bus, each one \n * using a distinct I2C address.  The process is described in ST Microelectronics application\n * note AN4846.\n * \n * WARNING:  If the device's XSHUT pin is not connected, then it may be prone to noise, \n * and the device may reset spontaneously or when handled and the device will stop responding\n * on its allocated address.  If you're not using XSHUT, then tie it to +5V via a resistor \n * (should be tied to +2.8V strictly). Some manufacturers (Adafruit and Polulu for example)\n * include a pull-up on the module, but others don't.\n * \n * The driver is configured as follows:\n * \n * Single VL53L0X module:\n *      VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold);\n * Where firstVpin is the first vpin reserved for reading the device,\n *       nPins is 1, 2 or 3,\n *       i2cAddress is the address of the device (normally 0x29),\n *       lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),\n *   and highThreshold is the distance at which the digital vpin state is set to 0 (in mm).\n * \n * Multiple VL53L0X modules:\n *       VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin);\n *       ...\n * Where firstVpin is the first vpin reserved for reading the device,\n *       nPins is 1, 2 or 3,\n *       i2cAddress is the address of the device (any valid address except 0x29),\n *       lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),\n *       highThreshold is the distance at which the digital vpin state is set to 0 (in mm),\n *   and xshutPin is the VPIN number corresponding to a digital output that is connected to the\n *       XSHUT terminal on the module.  The digital output may be an Arduino pin or an\n *       I/O extender pin.\n * \n * Example:\n *   In mySetup function within mySetup.cpp:\n *      VL53L0X::create(4000, 3, 0x29, 200, 250);\n *      Sensor::create(4000, 4000, 0);  // Create a sensor\n * \n *   When an object comes within 200mm of the sensor, a message \n *      <Q 4000>\n *   will be sent over the serial USB, and when the object moves more than 250mm from the sensor, \n *   a message\n *      <q 4000>\n *   will be sent. \n * \n */\n\n#ifndef IO_VL53L0X_h\n#define IO_VL53L0X_h\n\n#include \"IODevice.h\"\n\nclass VL53L0X : public IODevice {\nprivate: \n  uint16_t _ambient;\n  uint16_t _distance;\n  uint16_t _signal;\n  uint16_t _onThreshold;\n  uint16_t _offThreshold;\n  VPIN _xshutPin;\n  bool _value;\n  uint8_t _nextState = STATE_INIT;\n  I2CRB _rb;\n  uint8_t _inBuffer[12];\n  uint8_t _outBuffer[2];\n  static bool _addressConfigInProgress;\n\n  // State machine states.\n  enum : uint8_t {\n    STATE_INIT,\n    STATE_RESTARTMODULE,\n    STATE_CONFIGUREADDRESS,\n    STATE_CONFIGUREDEVICE,\n    STATE_INITIATESCAN,\n    STATE_CHECKSTATUS,\n    STATE_GETRESULTS,\n    STATE_DECODERESULTS,\n    STATE_FAILED,\n  };\n\n  // Register addresses\n  enum : uint8_t {\n    VL53L0X_REG_SYSRANGE_START=0x00,\n    VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13,\n    VL53L0X_REG_RESULT_RANGE_STATUS=0x14,\n    VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89,\n    VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A,\n  };\n  const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29;\n\n\n  public:\n  static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {\n     if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin);\n  }\n\nprotected:\n  VL53L0X(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {\n    _firstVpin = firstVpin;\n    _nPins = (nPins > 3) ? 3 : nPins;\n    _I2CAddress = i2cAddress;\n    _onThreshold = onThreshold;\n    _offThreshold = offThreshold;\n    _xshutPin = xshutPin;\n    _value = 0;\n    addDevice(this);\n  }\n  void _begin() override {\n    // If there's only one device, then the XSHUT pin need not be connected.  However, \n    //  the device will not respond on its default address if it has \n    //  already been changed.  Therefore, we skip the address configuration if the \n    //  desired address is already responding on the I2C bus.\n    _nextState = STATE_INIT;\n    _addressConfigInProgress = false;\n  }\n\n  void _loop(unsigned long currentMicros) override {\n    uint8_t status;\n    switch (_nextState) {\n      case STATE_INIT:\n        if (I2CManager.exists(_I2CAddress)) {\n          // Device already present on the nominated address, so skip the address initialisation.\n          _nextState = STATE_CONFIGUREDEVICE;\n        } else {\n          // On first entry to loop, reset this module by pulling XSHUT low.  Each module\n          // will be addressed in turn, until all are in the reset state.\n          // If no XSHUT pin is configured, then only one device is supported.\n          if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);\n          _nextState = STATE_RESTARTMODULE;\n          delayUntil(currentMicros+10000);\n        }\n        break;\n      case STATE_RESTARTMODULE:\n        // On second entry, set XSHUT pin high to allow this module to restart.\n        // I've observed that the device tends to randomly reset if the XSHUT \n        // pin is set high from a 5V arduino, even through a pullup resistor.  \n        // Assume that there will be a pull-up on the XSHUT pin to +2.8V as\n        // recommended in the device datasheet.  Then we only need to\n        // turn our output pin high-impedence (by making it an input) and the\n        // on-board pullup will do its job.\n        // Ensure XSHUT is set for only one module at a time by using a\n        // shared flag accessible to all device instances.\n        if (!_addressConfigInProgress) {\n          _addressConfigInProgress = true;\n          // Configure XSHUT pin (if connected) to bring the module out of sleep mode.\n          if (_xshutPin != VPIN_NONE) IODevice::configureInput(_xshutPin, false);\n          // Allow the module time to restart\n          delayUntil(currentMicros+10000);\n          _nextState = STATE_CONFIGUREADDRESS;\n        }\n        break;\n      case STATE_CONFIGUREADDRESS:\n        // Then write the desired I2C address to the device, while this is the only\n        //  module responding to the default address.\n        {\n          #if defined(I2C_EXTENDED_ADDRESS)\n          // Add subbus reference for desired address to the device default address.\n          I2CAddress defaultAddress = {_I2CAddress, VL53L0X_I2C_DEFAULT_ADDRESS};\n          status = I2CManager.write(defaultAddress, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _I2CAddress.deviceAddress());\n          #else\n          status = I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _I2CAddress);\n          #endif\n          if (status != I2C_STATUS_OK) {\n            reportError(status);\n          }\n        }\n        delayUntil(currentMicros+10000);\n        _nextState = STATE_CONFIGUREDEVICE;\n        break;\n      case STATE_CONFIGUREDEVICE:\n        // Allow next VL53L0X device to be configured\n        _addressConfigInProgress = false;\n        // Now check if device address has been set.\n        if (I2CManager.exists(_I2CAddress)) {\n          #ifdef DIAG_IO\n          _display();\n          #endif\n          // Set 2.8V mode\n          status = write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, \n            read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01);\n          if (status != I2C_STATUS_OK) {\n            reportError(status);\n          } else\n            _nextState = STATE_INITIATESCAN;\n        } else {\n          DIAG(F(\"VL53L0X I2C:%s device not responding\"), _I2CAddress.toString());\n          _deviceState = DEVSTATE_FAILED;\n          _nextState = STATE_FAILED;\n        }\n        break;\n      case STATE_INITIATESCAN:\n        // Not scanning, so initiate a scan\n        _outBuffer[0] = VL53L0X_REG_SYSRANGE_START;\n        _outBuffer[1] = 0x01;\n        I2CManager.write(_I2CAddress, _outBuffer, 2, &_rb);\n        _nextState = STATE_CHECKSTATUS;\n        break;\n      case STATE_CHECKSTATUS:\n        status = _rb.status;\n        if (status == I2C_STATUS_PENDING) return; // try next time\n        if (status != I2C_STATUS_OK) {\n          reportError(status);\n        } else\n          _nextState = STATE_GETRESULTS;\n        delayUntil(currentMicros + 95000); // wait for 95 ms before checking.\n        break;\n      case STATE_GETRESULTS:\n        // Ranging completed.  Request results\n        _outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;\n        I2CManager.read(_I2CAddress, _inBuffer, 12, _outBuffer, 1, &_rb);\n        delayUntil(currentMicros + 5000); // Allow 5ms to get data\n        _nextState = STATE_DECODERESULTS;\n        break;\n      case STATE_DECODERESULTS:\n        // If I2C write still busy, return.\n        status = _rb.status;\n        if (status == I2C_STATUS_PENDING) return; // try again next time\n        if (status == I2C_STATUS_OK) {\n          if (!(_inBuffer[0] & 1)) return; // device still busy\n          uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3);\n          if (deviceRangeStatus == 0x0b) {\n            // Range status OK, so use data\n            _ambient = makeuint16(_inBuffer[7], _inBuffer[6]);\n            _signal = makeuint16(_inBuffer[9], _inBuffer[8]);\n            _distance = makeuint16(_inBuffer[11], _inBuffer[10]);\n            if (_distance <= _onThreshold) \n              _value = true;\n            else if (_distance > _offThreshold) \n              _value = false;\n          }\n          // Completed. Restart scan on next loop entry.\n          _nextState = STATE_INITIATESCAN;\n        } else {\n          reportError(status);\n        }\n        break;\n      case STATE_FAILED:\n        // Do nothing.\n        delayUntil(currentMicros+1000000UL);\n        break;\n      default:\n        break;\n    }\n  }\n\n  // Function to report a failed I2C operation.\n  void reportError(uint8_t status) {\n    DIAG(F(\"VL53L0X I2C:%s Error:%d %S\"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));\n    _deviceState = DEVSTATE_FAILED;\n    _value = false;\n  }\n\n  // For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level.\n  int _readAnalogue(VPIN vpin) override {\n    int pin = vpin - _firstVpin;\n    switch (pin) {\n      case 0:\n        return _distance;\n      case 1:\n        return _signal;\n      case 2:\n        return _ambient;\n      default:\n        return -1;\n    }\n  }\n\n  // For digital read, return zero for all but first pin.\n  int _read(VPIN vpin) override {\n    if (vpin == _firstVpin)\n      return _value;\n    else\n      return 0;\n  }\n\n  void _display() override {\n    DIAG(F(\"VL53L0X I2C:%s Configured on Vpins:%u-%u On:%dmm Off:%dmm %S\"),\n      _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,\n      (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n  \n\nprivate:\n  inline uint16_t makeuint16(byte lsb, byte msb) {\n    return (((uint16_t)msb) << 8) | lsb;\n  }\n  uint8_t write_reg(uint8_t reg, uint8_t data) {\n    // write byte to register\n    uint8_t outBuffer[2];\n    outBuffer[0] = reg;\n    outBuffer[1] = data;\n    return I2CManager.write(_I2CAddress, outBuffer, 2);\n  }\n  uint8_t read_reg(uint8_t reg) {\n    // read byte from register and return value\n    I2CManager.read(_I2CAddress, _inBuffer, 1, &reg, 1);\n    return _inBuffer[0];\n  }\n};\n\nbool VL53L0X::_addressConfigInProgress = false;\n\n#endif // IO_VL53L0X_h\n"
  },
  {
    "path": "IO_XL9535.h",
    "content": "/*\n *  © 2026, Terry Wheatcroft & Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_xl9535_h\n#define io_xl9535_h\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for XL9535 16-bit I/O expander relay board.\n * Since this board is effectively an output-only board, it has no I2C read code.\n */\n \nclass XL9535 : public GPIOBase<uint16_t> {\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) new XL9535(vpin,nPins, i2cAddress);\n  }\n\nprivate:  \n  // Constructor\n  XL9535(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress) \n    : GPIOBase<uint16_t>((FSH *)F(\"XL9535\"), vpin, nPins, I2CAddress,-1) \n  {\n  }\n\n  void _writeGpioPort() override {\n    I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);\n  }\n\n  void _writePullups() override {}\n\n  void _writePortModes() override {\n    // Write 0 to REG_CONF_P0 & REG_CONF_P1 as all pins are output\n    I2CManager.write(_I2CAddress, 3, REG_CONF_P0, 0, 0);    \n  }\n\n  void _readGpioPort(bool immediate) override {\n    (void) immediate; \n    _portInputState = _portOutputState;\n  }\n\n  void _setupDevice() override {\n    // HAL API calls\n    _writeGpioPort();\n  }\n \n  \n  enum {\n    REG_OUTPUT_P0 = 0x02,\n    REG_OUTPUT_P1 = 0x03,\n    REG_POL_INV_P0 = 0x04,\n    REG_POL_INV_P1 = 0x05,\n    REG_CONF_P0 = 0x06,\n    REG_CONF_P1 = 0x07,    \n  };\n\n};\n\n#endif\n"
  },
  {
    "path": "IO_duinoNodes.h",
    "content": "/*\n *  © 2022, Chris Harlow. All rights reserved.\n *  Based on original by: Robin Simonds, Beagle Bay Inc\n *  \n *  This file is part of DCC-EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef IO_duinoNodes_h\n #define IO_duinoNodes_h\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"IODevice.h\"\n\n#define DN_PIN_MASK(bit) (0x80>>(bit%8))\n#define DN_GET_BIT(x) (_pinValues[(x)/8] & DN_PIN_MASK((x)) )\n#define DN_SET_BIT(x) _pinValues[(x)/8] |= DN_PIN_MASK((x))\n#define DN_CLR_BIT(x) _pinValues[(x)/8] &= ~DN_PIN_MASK((x))\n\n\n\nclass IO_duinoNodes : public IODevice {\n\npublic:\n  IO_duinoNodes(VPIN firstVpin, int nPins, \n                byte clockPin, byte latchPin, byte dataPin, \n                const byte* pinmap) :\n    IODevice(firstVpin, nPins) {\n \n   _latchPin=latchPin;\n    _clockPin=clockPin;\n    _dataPin=dataPin;\n    _pinMap=pinmap;\n    _nShiftBytes=(nPins+7)/8; // rounded up to multiples of 8 bits\n    _pinValues=(byte*) calloc(_nShiftBytes,1);  \n    // Connect to HAL so my _write, _read and _loop will be called as required.\n    IODevice::addDevice(this);  \n  }\n\n// Called by HAL to start handling this device\n  void _begin() override {\n     _deviceState = DEVSTATE_NORMAL;\n    pinMode(_latchPin,OUTPUT);\n    pinMode(_clockPin,OUTPUT);\n    pinMode(_dataPin,_pinMap?INPUT_PULLUP:OUTPUT);\n    _display();\n    if (!_pinMap) _loopOutput();\n  }\n\n// loop called by HAL supervisor \nvoid _loop(unsigned long currentMicros) override {\n    if (_pinMap) _loopInput(currentMicros);\n    else if (_xmitPending) _loopOutput();\n}\n\nvoid _loopInput(unsigned long currentMicros)  {\n   \n   if (currentMicros-_prevMicros < POLL_MICROS) return; // Nothing to do\n    _prevMicros=currentMicros;\n   \n    //set latch to HIGH to freeze & store parallel data\n   ArduinoPins::fastWriteDigital(_latchPin, HIGH);\n   delayMicroseconds(1);\n   //set latch to LOW to enable the data to be transmitted serially\n   ArduinoPins::fastWriteDigital(_latchPin, LOW);\n\n  // stream in the bitmap using mapping order provided at constructor   \n  for (int xmitByte=0;xmitByte<_nShiftBytes; xmitByte++) {\n      byte newByte=0;\n      for (int xmitBit=0;xmitBit<8; xmitBit++) {\n        ArduinoPins::fastWriteDigital(_clockPin, LOW);\n        delayMicroseconds(1);\n        bool data = ArduinoPins::fastReadDigital(_dataPin);  \n        byte map=_pinMap[xmitBit];\n        if (data)  newByte |= map;\n            else   newByte &= ~map;\n        ArduinoPins::fastWriteDigital(_clockPin, HIGH); \n        delayMicroseconds(1);   \n      }\n      _pinValues[xmitByte]=newByte;\n      // DIAG(F(\"DIN %x=%x\"),xmitByte, newByte);\n    }\n  }\n\nvoid _loopOutput()  {\n    // stream out the bitmap (highest pin first)\n    _xmitPending=false; \n    ArduinoPins::fastWriteDigital(_latchPin, LOW);\n    for (int xmitBit=_nShiftBytes*8 -1; xmitBit>=0; xmitBit--) {\n        ArduinoPins::fastWriteDigital(_dataPin,DN_GET_BIT(xmitBit));\n        ArduinoPins::fastWriteDigital(_clockPin,HIGH);\n        ArduinoPins::fastWriteDigital(_clockPin,LOW);\n    }  \n    ArduinoPins::fastWriteDigital(_latchPin, HIGH);\n  }\n\n  int _read(VPIN vpin) override {\n    int pin=vpin - _firstVpin;\n    bool b=DN_GET_BIT(pin);\n    return b?1:0;\n  }\n\n  void _write(VPIN vpin, int value) override {\n    int pin = vpin - _firstVpin;\n    bool oldval=DN_GET_BIT(pin);\n    bool newval=value!=0;\n    if (newval==oldval) return; // no change  \n    if (newval) DN_SET_BIT(pin);\n           else DN_CLR_BIT(pin);\n    _xmitPending=true;  // shift register will be sent on next _loop()\n  }\n\n  void _display() override {\n      DIAG(F(\"IO_duinoNodes %SPUT Configured on Vpins:%u-%u shift=%d\"), \n      _pinMap?F(\"IN\"):F(\"OUT\"),\n      (int)_firstVpin, \n      (int)_firstVpin+_nPins-1, _nShiftBytes*8);\n  }\n\nprivate:\n  static const unsigned long POLL_MICROS=100000; // 10 / S\n  unsigned long _prevMicros; \n  int  _nShiftBytes=0; \n  VPIN _latchPin,_clockPin,_dataPin;\n  byte* _pinValues;\n  bool _xmitPending; // Only relevant in output mode\n  const byte* _pinMap;  // NULL in output mode \n};\n\nclass IO_DNIN8  {\npublic:\n  static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin ) \n  {\n      // input arrives as board pin 0,7,6,5,1,2,3,4 \n      static const byte pinmap[8]={0x80,0x01,0x02,0x04,0x40,0x20,0x10,0x08};\n      if (IODevice::checkNoOverlap(firstVpin,nPins))\n        new IO_duinoNodes( firstVpin,  nPins,  clockPin,  latchPin,   dataPin,pinmap);\n  }\n\n};\n\nclass IO_DNIN8K  {\npublic:\n  static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin ) \n  {\n      // input arrives as board pin 0, 1, 2, 3, 4, 5, 6, 7\n      static const byte pinmap[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; \n       if (IODevice::checkNoOverlap(firstVpin,nPins))\n        new IO_duinoNodes( firstVpin,  nPins, clockPin, latchPin, dataPin,pinmap);\n  }\n};\n\nclass IO_DNOU8 {\npublic:\n  static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin ) \n  {\n        if (IODevice::checkNoOverlap(firstVpin,nPins))\n         new IO_duinoNodes( firstVpin,  nPins,  clockPin, latchPin,   dataPin,NULL);\n  }\n\n};\n#endif\n"
  },
  {
    "path": "IO_trainbrains.h",
    "content": "/*\n *  © 2023-2025, Chris Harlow. All rights reserved.\n *  © 2021, Neil McKechnie. All rights reserved.\n *  \n *  This file is part of DCC++EX API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef io_trainbrains_h\n#define io_trainbrains_h\n\n#include \"IO_GPIOBase.h\"\n#include \"FSH.h\"\n\n/////////////////////////////////////////////////////////////////////////////////////////////////////\n/*\n * IODevice subclass for Trainbrains devices.\n * For details see \n * https://trainbrains.eu/wp-content/uploads/trainbrains-railroad-modules-communication-1.2-en.pdf\n */\n \n  // TODO - use non-blocking I2C \n  // TODO - support for all-channels sensor read via new command code\n\n  \n enum TrackUnoccupancy\n{\n    TRACK_UNOCCUPANCY_UNKNOWN = 0,\n    TRACK_OCCUPIED = 1,\n    TRACK_UNOCCUPIED = 2\n};\n\n//   generic class for self-identifying Trainbrains devices\nclass Trainbrains : public IODevice {\npublic:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress,bool debug=false) {\n    if (checkNoOverlap(vpin, nPins, i2cAddress)) \n      new Trainbrains(vpin, nPins, i2cAddress,debug);\n  }\n\nprivate:\n  static const byte DT_Unknown=0;      \n  static const byte DT_Signal=1; \n  static const byte DT_Turnout=2;\n  static const byte DT_Power=3; \n  static const byte DT_Track=4;\n  bool debugme; \n  byte deviceType=0; // 0=none, 1=signal, 2=turnout, 3=power, 4=track\n  uint8_t outputBuffer[10];\n  uint8_t inputBuffer[10];\n  \n  // Constructor\n  Trainbrains(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, bool debug) \n  { \n    debugme=debug;\n    _firstVpin = vpin; \n    _nPins = nPins; \n    _I2CAddress=i2cAddress;\n    outputBuffer[2]=0; // will increment with each command\n    addDevice(this);\n    }\n  \n  void issueCommand(byte code, byte p0, byte dane0=0, byte dane1=0, byte dane2=0, byte dane3=0) {\n     outputBuffer[0] = (uint8_t)_I2CAddress; // strips away the mux part.\n     outputBuffer[1] = code;\n     outputBuffer[2] ++; // increment the command counter\n     outputBuffer[3] =p0;\n     outputBuffer[4] =0;\n     outputBuffer[5] =0;\n     outputBuffer[6] =dane0;\n     outputBuffer[7] =dane1;\n     outputBuffer[8] =dane2;\n     outputBuffer[9] =dane3;\n     memset(inputBuffer, 0, sizeof(inputBuffer)); // clear input buffer\n     auto status=I2CManager.read(_I2CAddress, \n                inputBuffer, sizeof(inputBuffer),\n                outputBuffer, sizeof(outputBuffer) \n               );\n     if (status!=I2C_STATUS_OK) {\n       DIAG(F(\"Trainbrains I2C:%s Error:%S\"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));\n       _deviceState = DEVSTATE_FAILED;\n     } \n     else {\n      if (debugme) _dumpBuffers();\n     }    \n  }\n  \n  void _dumpBuffers() {\n    StringFormatter::send(&USB_SERIAL,F(\"<* TB %s\\n out:\"),_I2CAddress.toString());\n    for (byte i=0;i<sizeof(outputBuffer);i++) {\n      StringFormatter::send(&USB_SERIAL,F(\" %-2x\"),outputBuffer[i]);\n    }\n    StringFormatter::send(&USB_SERIAL,F(\"\\n  in:\"));\n    for (byte i=0;i<sizeof(inputBuffer);i++) {\n      StringFormatter::send(&USB_SERIAL,F(\" %-2x\"),inputBuffer[i]);\n    }\n    StringFormatter::send(&USB_SERIAL,F(\"\\n*>\\n\"));    \n  }\n\n  void _begin() override {\n    issueCommand(20,0); // read device type\n    deviceType=inputBuffer[6]; // 0=notFound 1=signal, 2=turnout, 3=power, 4=track                   \n    if (deviceType == DT_Unknown) { // device not found\n      _deviceState = DEVSTATE_FAILED;\n     }\n    else {\n      issueCommand(20,3); // read channels supported\n      if (_nPins>inputBuffer[6]) _nPins=inputBuffer[6]; // max channels supported\n    }\n    _display();\n  }\n  \n  // Display details of this device.\n  void _display() {\n    DIAG(F(\"Trainbrains type:%d I2C:%s Configured on Vpins:%u-%u %S\"), deviceType, _I2CAddress.toString(), (int)_firstVpin, \n        (int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F(\"OFFLINE\") : F(\"\"));\n  }\n\n\n  // Digital Write will be from a power set or a turnout set\n  void _write(VPIN vpin,int value) override { \n    if (deviceType!=DT_Turnout && deviceType!=DT_Power) return;\n    int16_t pin=vpin-_firstVpin;\n    issueCommand(12,pin+1,value?2:1); // write command    \n  }\n\n  // Read will be from a sensor poll\n  int _read(VPIN vpin) override {\n    if (deviceType!=DT_Track) return 0;\n    byte pin=vpin-_firstVpin;\n    issueCommand(14,pin+1); // read channel status \n    return (inputBuffer[6] == TRACK_OCCUPIED );\n  }\n\n  // analog write is for signal mask and state.\n  // parameters are arranged to be compatible with using\n  // neopixel commands where R,G,B values are mapped to the \n  // aspect, flashing and state values.\n\n  void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) override {\n    (void)param2;\n    if (deviceType!=DT_Signal) return;\n    int16_t pin=vpin-_firstVpin;\n    issueCommand(13,pin+1,value>>8,value & 0xff,param1); // write command    \n  }\n  \n};\n\n// class retained for backward compatibility. Builds the generic class. \nclass Trainbrains02 {\n  public:\n  static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, bool debug=false) {\n    Trainbrains::create(vpin, nPins, i2cAddress,debug);\n  }\n};\n\n#endif\n"
  },
  {
    "path": "KeywordHasher.h",
    "content": "/*\n *  © 2024 Vincent Hamp and Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n/* Reader be aware: \n This function implements the _hk data type so that a string keyword\n is hashed to the same value as the DCCEXParser uses to hash incoming \n keywords. \n Thus  \"MAIN\"_hk  generates exactly the same run time vakue \n as   const int16_t HASH_KEYWORD_MAIN=11339  \n*/\n#ifndef KeywordHasher_h\n#define KeywordHasher_h\n\n#include <Arduino.h>\nconstexpr uint16_t CompiletimeKeywordHasher(const char *  sv, uint16_t running=0) {\n    return (*sv==0) ? running : CompiletimeKeywordHasher(sv+1,\n          (*sv >= '0' && *sv <= '9') \n            ? (10*running+*sv-'0')  // Numeric hash  \n            : ((running << 5) + running) ^ *sv\n            );  // \n}\n\nconstexpr int16_t operator\"\"_hk(const char * keyword, size_t len) \n{\n    return (int16_t) CompiletimeKeywordHasher(keyword,len*0);\n}\n\n/* Some historical values for testing:\nconst int16_t HASH_KEYWORD_MAIN = 11339;\nconst int16_t HASH_KEYWORD_SLOW = -17209;\nconst int16_t HASH_KEYWORD_SPEED28 = -17064;\nconst int16_t HASH_KEYWORD_SPEED128 = 25816;\n*/\n\nstatic_assert(\"MAIN\"_hk == 11339,\"Keyword hasher error\");\nstatic_assert(\"SLOW\"_hk == -17209,\"Keyword hasher error\");\nstatic_assert(\"SPEED28\"_hk == -17064,\"Keyword hasher error\");\nstatic_assert(\"SPEED128\"_hk == 25816,\"Keyword hasher error\");\n\n// Compile time converter from \"abcd\"_s7  to the 7 segment nearest equivalent\n\nconstexpr uint8_t seg7Digits[]={\n      0b00111111,0b00000110,0b01011011,0b01001111, // 0..3\n      0b01100110,0b01101101,0b01111101,0b00000111, // 4..7\n      0b01111111,0b01101111 // 8..9\n    };\n    \nconstexpr uint8_t seg7Letters[]={\n        0b01110111,0b01111100,0b00111001,0b01011110, // ABCD\n        0b01111001,0b01110001,0b00111101,0b01110110, // EFGH \n        0b00000100,0b00011110,0b01110010,0b00111000,  //IJKL\n        0b01010101,0b01010100,0b01011100,0b01110011, // MNOP\n        0b10111111,0b01010000,0b01101101,0b01111000, // QRST\n        0b00111110,0b00011100,0b01101010,0b01001001,  //UVWX\n        0b01100110,0b01011011  //YZ\n    };\nconstexpr uint8_t seg7Space=0b00000000;\nconstexpr uint8_t seg7Minus=0b01000000;\nconstexpr uint8_t seg7Equals=0b01001000;\n    \n\nconstexpr uint32_t CompiletimeSeg7(const char *  sv, uint32_t running, size_t rlen) {\n    return (*sv==0 || rlen==0) ? running << (8*rlen) : CompiletimeSeg7(sv+1,\n          (*sv >= '0' && *sv <= '9') ? (running<<8) | seg7Digits[*sv-'0'] : \n          (*sv >= 'A' && *sv <= 'Z') ? (running<<8) | seg7Letters[*sv-'A'] : \n          (*sv >= 'a' && *sv <= 'z') ? (running<<8) | seg7Letters[*sv-'a'] : \n          (*sv == '-') ? (running<<8) | seg7Minus : \n          (*sv == '=') ? (running<<8) | seg7Equals : \n                         (running<<8) | seg7Space,\n          rlen-1               \n        );  // \n}\n\nconstexpr uint32_t operator\"\"_s7(const char * keyword, size_t len) \n{\n    return  CompiletimeSeg7(keyword,0*len,4);\n}\n#endif\n"
  },
  {
    "path": "LCN.cpp",
    "content": "/*\n *  © 2021, Chris Harlow. All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX \n *  \n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"LCN.h\"\n#include \"DIAG.h\"\n#include \"Turnouts.h\"\n#include \"Sensors.h\"\n\nint  LCN::id = 0;\nStream * LCN::stream=NULL;\nbool LCN::firstLoop=true;\n\nvoid LCN::init(Stream & lcnstream) {\n  stream=&lcnstream; \n  DIAG(F(\"LCN connection setup\")); \n}\n\n\n// Inbound LCN traffic is postfix notation...   nnnX  where nnn is an id, X is the opcode\nvoid LCN::loop() {\n  if (!stream) return;\n  if (firstLoop) {\n    firstLoop=false;\n    stream->println('X');\n    return; \n  }\n  \n  while (stream->available()) {\n    int ch = stream->read();\n    if (ch >= '0' && ch <= '9') {  // accumulate id value\n      id = 10 * id + ch - '0';\n    }\n    else if (ch == 't' || ch == 'T') { // Turnout opcodes\n      if (Diag::LCN) DIAG(F(\"LCN IN %d%c\"),id,(char)ch);\n      if (!Turnout::exists(id)) LCNTurnout::create(id);\n      Turnout::setClosedStateOnly(id,ch=='t');\n      id = 0;\n    }\n    else if (ch == 'y' || ch == 'Y') { // Turnout opcodes\n      if (Diag::LCN) DIAG(F(\"LCN IN %d%c\"),id,(char)ch);\n      Turnout::setClosed(id,ch=='y');\n      id = 0;\n    }\n    else if (ch == 'S' || ch == 's') {\n      if (Diag::LCN) DIAG(F(\"LCN IN %d%c\"),id,(char)ch);\n      Sensor * ss = Sensor::get(id);\n      if (!ss) ss = Sensor::create(id, VPIN_NONE, 0); // impossible pin\n      ss->setState(ch == 'S');\n      id = 0;\n    }\n    else  id = 0; // ignore any other garbage from LCN\n  }\n}\n\nvoid LCN::send(char opcode, int id, bool state) {\n   if (stream) {\n      StringFormatter::send(stream,F(\"%c/%d/%d\"), opcode, id , state);\n      if (Diag::LCN) DIAG(F(\"LCN OUT %c/%d/%d\"), opcode, id , state);\n   }\n}\n"
  },
  {
    "path": "LCN.h",
    "content": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef LCN_h\n#define LCN_h\n#include <Arduino.h>\n\nclass LCN {\n  public: \n    static void init(Stream & lcnstream);\n    static void loop();\n    static void send(char opcode, int id, bool state);\n  private :\n    static bool firstLoop; \n    static Stream * stream; \n    static int id;\n};\n\n#endif\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "LiquidCrystal_I2C.cpp",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  Based on the work by DFRobot, Frank de Brabander and Marco Schwartz.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation-EX.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <Arduino.h>\n#include \"LiquidCrystal_I2C.h\"\n#include \"DIAG.h\"\n\n// When the display powers up, it is configured as follows:\n//\n// 1. Display clear\n// 2. Function set:\n//    DL = 1; 8-bit interface data\n//    N = 0; 1-line display\n//    F = 0; 5x8 dot character font\n// 3. Display on/off control:\n//    D = 0; Display off\n//    C = 0; Cursor off\n//    B = 0; Blinking off\n// 4. Entry mode set:\n//    I/D = 1; Increment by 1\n//    S = 0; No shift\n//\n// Note, however, that resetting the Arduino doesn't reset the LCD, so we\n// can't assume that its in that state when a sketch starts (and the\n// LiquidCrystal constructor is called).\n\nLiquidCrystal_I2C::LiquidCrystal_I2C(I2CAddress lcd_Addr, uint8_t lcd_cols,\n                                     uint8_t lcd_rows) {\n  _Addr = lcd_Addr;\n  lcdRows = lcd_rows;  // Number of character rows (typically 2 or 4).\n  lcdCols = lcd_cols;  // Number of character columns (typically 16 or 20)\n  _backlightval = 0;\n }\n\nbool LiquidCrystal_I2C::begin() {\n\n  I2CManager.begin();\n  I2CManager.setClock(100000L);    // PCF8574 is spec'd to 100kHz.\n\n  if (I2CManager.exists(_Addr)) {\n    DIAG(F(\"%dx%d LCD configured on I2C:%s\"), (int)lcdCols, (int)lcdRows, _Addr.toString());\n    _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;\n    backlight();\n  } else {\n    DIAG(F(\"LCD not found on I2C:%s\"), _Addr.toString());\n    return false;\n  }\n\n  if (lcdRows > 1) {\n    _displayfunction |= LCD_2LINE;\n  }\n\n  // according to datasheet, we need at least 40ms after power rises above 2.7V\n  // before sending commands. Arduino can turn on way before 4.5V so we'll allow\n  // 100 milliseconds after pulling both RS and R/W and backlight pin low\n  expanderWrite(\n      _backlightval);  // reset expander and turn backlight off (Bit 8 =1)\n  delay(100);\n\n  // put the LCD into 4 bit mode\n  // this is according to the hitachi HD44780 datasheet\n  // figure 24, pg 46\n\n  // we start in 8bit mode, try to set 4 bit mode\n  write4bits(0x03);\n  delayMicroseconds(5000);  // wait min 4.1ms\n\n  // second try\n  write4bits(0x03);\n  delayMicroseconds(5000);  // wait min 4.1ms\n\n  // third go!\n  write4bits(0x03);\n  delayMicroseconds(5000);\n\n  // finally, set to 4-bit interface\n  write4bits(0x02);\n\n  // set # lines, font size, etc.\n  command(LCD_FUNCTIONSET | _displayfunction);\n\n  // turn the display on with no cursor or blinking default\n  _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;\n  display();\n\n  // Initialize to default text direction (for roman languages)\n  _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;\n\n  // set the entry mode\n  command(LCD_ENTRYMODESET | _displaymode);\n\n  return true;\n}\n\n/********** high level commands, for the user! */\nvoid LiquidCrystal_I2C::clearNative() {\n  command(LCD_CLEARDISPLAY);  // clear display, set cursor position to zero\n  delayMicroseconds(2000);    // this command takes 1.52ms but allow plenty\n}\n\nvoid LiquidCrystal_I2C::setRowNative(byte row) {\n  uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};\n  if (row >= lcdRows) {\n    row = lcdRows - 1;  // we count rows starting w/0\n  }\n  command(LCD_SETDDRAMADDR | (row_offsets[row]));\n}\n\nvoid LiquidCrystal_I2C::display() {\n  _displaycontrol |= LCD_DISPLAYON;\n  command(LCD_DISPLAYCONTROL | _displaycontrol);\n}\n\n// Turn the (optional) backlight off/on\nvoid LiquidCrystal_I2C::noBacklight(void) {\n  _backlightval &= ~LCD_BACKLIGHT;\n  expanderWrite(0);\n}\n\nvoid LiquidCrystal_I2C::backlight(void) {\n  _backlightval = LCD_BACKLIGHT;\n  expanderWrite(0);\n}\n\nsize_t LiquidCrystal_I2C::writeNative(uint8_t value) {\n  send(value, Rs);\n  return 1;\n}\n\nbool LiquidCrystal_I2C::isBusy() { \n  return rb.isBusy();\n}\n\n/*********** mid level commands, for sending data/cmds */\n\ninline void LiquidCrystal_I2C::command(uint8_t value) { \n  send(value, 0); \n}\n\n/************ low level data pushing commands **********/\n\n/* According to the NXP Datasheet for the PCF8574 section 8.2:\n *   \"The master (microcontroller) sends the START condition and slave address\n *    setting the last bit of the address byte to logic 0 for the write mode.\n *    The PCF8574/74A acknowledges and the master then sends the data byte for\n *    P7 to P0 to the port register. As the clock line goes HIGH, the 8-bit\n *    data is presented on the port lines after it has been acknowledged by the\n *    PCF8574/74A. [...] The master can then send a STOP or ReSTART condition\n *    or continue sending data. The number of data bytes that can be sent \n *    successively is not limited and the previous data is overwritten every\n *    time a data byte has been sent and acknowledged.\"\n * \n * This driver takes advantage of this by sending multiple data bytes in succession\n * within a single I2C transmission.  With a fast clock rate of 400kHz, the time\n * between successive updates of the PCF8574 outputs will be at least 2.5us.  With\n * the default clock rate of 100kHz the time between updates will be at least 10us.\n * \n * The LCD controller HD44780, according to its datasheet, needs nominally 37us\n * (up to 50us) to execute a command (i.e. write to gdram, reposition, etc.). Each\n * command is sent in a separate I2C transmission here.  The time taken to end a \n * transmission and start another one is a stop bit, a start bit, 8 address bits,\n * an ack, 8 data bits and another ack; this is at least 20 bits, i.e. >50us\n * at 400kHz and >200us at 100kHz. Therefore, we don't need additional delay.\n * \n * Similarly, the Enable must be set/reset for at least 450ns.  This is \n * well within the I2C clock cycle time of 2.5us at 400kHz.  Data is clocked in\n * to the HD44780 on the trailing edge of the Enable pin, so we set the Enable\n * as we present the data, then in the next byte we reset Enable without changing\n * the data.\n */\n\n// write either command or data (8 bits) to the HD44780 LCD controller as\n//  a single I2C transmission. \nvoid LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {\n  mode |= _backlightval;\n  uint8_t highnib = (((value >> 4) & 0x0f) << BACKPACK_DATA_BITS) | mode;\n  uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | mode;\n  // Send both nibbles\n  uint8_t len = 0;\n  rb.wait();\n  outputBuffer[len++] = highnib|En;\n  outputBuffer[len++] = highnib;\n  outputBuffer[len++] = lownib|En;\n  outputBuffer[len++] = lownib;\n  I2CManager.write(_Addr, outputBuffer, len, &rb);  // Write command asynchronously\n}\n\n// write 4 data bits to the HD44780 LCD controller.\nvoid LiquidCrystal_I2C::write4bits(uint8_t value) {\n  uint8_t _data = ((value & 0x0f) << BACKPACK_DATA_BITS) | _backlightval;\n  // Enable must be set/reset for at least 450ns.  This is well within the\n  // I2C clock cycle time of 2.5us at 400kHz. Data is clocked in to the\n  // HD44780 on the trailing edge of the Enable pin.\n  uint8_t len = 0;\n  rb.wait();\n  outputBuffer[len++] = _data|En;\n  outputBuffer[len++] = _data;\n  I2CManager.write(_Addr, outputBuffer, len, &rb);  // Write command asynchronously\n}\n\n// write a byte to the PCF8574 I2C interface.  We don't need to set\n// the enable pin for this.\nvoid LiquidCrystal_I2C::expanderWrite(uint8_t value) {\n  rb.wait();\n  outputBuffer[0] = value | _backlightval;\n  I2CManager.write(_Addr, outputBuffer, 1, &rb);  // Write command asynchronously\n}\n"
  },
  {
    "path": "LiquidCrystal_I2C.h",
    "content": "/*\n *  © 2021, Neil McKechnie. All rights reserved.\n *  Based on the work by DFRobot, Frank de Brabander and Marco Schwartz.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef LiquidCrystal_I2C_h\n#define LiquidCrystal_I2C_h\n\n#include <Arduino.h>\n#include \"Display.h\"\n#include \"I2CManager.h\"\n\n// commands\n#define LCD_CLEARDISPLAY 0x01\n#define LCD_ENTRYMODESET 0x04\n#define LCD_DISPLAYCONTROL 0x08\n#define LCD_FUNCTIONSET 0x20\n#define LCD_SETCGRAMADDR 0x40\n#define LCD_SETDDRAMADDR 0x80\n\n// flags for display entry mode\n#define LCD_ENTRYRIGHT 0x00\n#define LCD_ENTRYLEFT 0x02\n#define LCD_ENTRYSHIFTINCREMENT 0x01\n#define LCD_ENTRYSHIFTDECREMENT 0x00\n\n// flags for display on/off control\n#define LCD_DISPLAYON 0x04\n#define LCD_CURSOROFF 0x00\n#define LCD_BLINKOFF 0x00\n\n// flags for function set\n#define LCD_4BITMODE 0x00\n#define LCD_2LINE 0x08\n#define LCD_1LINE 0x00\n#define LCD_5x8DOTS 0x00\n\n// Bit mapping onto PCF8574 port\n#define BACKPACK_Rs_BIT 0\n#define BACKPACK_Rw_BIT 1\n#define BACKPACK_En_BIT 2\n#define BACKPACK_BACKLIGHT_BIT 3\n#define BACKPACK_DATA_BITS 4 // Bits 4-7\n// Equivalent mask bits\n#define LCD_BACKLIGHT (1 << BACKPACK_BACKLIGHT_BIT)  // Backlight enable\n#define En (1 << BACKPACK_En_BIT)  // Enable bit\n#define Rw (1 << BACKPACK_Rw_BIT)  // Read/Write bit\n#define Rs (1 << BACKPACK_Rs_BIT)  // Register select bit\n\nclass LiquidCrystal_I2C : public DisplayDevice {\npublic:\n  LiquidCrystal_I2C(I2CAddress lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);\n  bool begin() override;\n  void clearNative() override;\n  void setRowNative(byte line) override;\n  size_t writeNative(uint8_t c) override;\n  // I/O is synchronous, so if this is called we're not busy!\n  bool isBusy() override; \n  \n  void display();\n  void noBacklight();\n  void backlight();\n  \n  void command(uint8_t);\n  uint16_t getNumCols() { return lcdCols; }\n  uint16_t getNumRows() { return lcdRows; }\n\n\nprivate:\n  void send(uint8_t, uint8_t);\n  void write4bits(uint8_t);\n  void expanderWrite(uint8_t);\n  uint8_t lcdCols=0, lcdRows=0;\n  I2CAddress _Addr;\n  uint8_t _displayfunction;\n  uint8_t _displaycontrol;\n  uint8_t _displaymode;\n  uint8_t _backlightval = 0;\n\n  uint8_t outputBuffer[4];\n  I2CRB rb;\n};\n\n#endif\n"
  },
  {
    "path": "LocoSlot.cpp",
    "content": "/* Copyright (c) 2023 Harald Barth\n * Copyright (c) 2025 Chris Harlow\n * This source is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This source is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this software.  If not, see\n * <http://www.gnu.org/licenses/>.\n */\n#include \"LocoSlot.h\"\n#include \"StringFormatter.h\"\n#include \"DIAG.h\"\nLocoSlot * LocoSlot::firstSlot = nullptr;\nLocoSlot * LocoSlot::recycler = nullptr;\nbool LocoSlot::chainModified = false;\nuint16_t LocoSlot::slotCount = 0;\n\nvoid LocoSlot::prepare(uint16_t locoId) {\n    loco = locoId;\n    speedCode=128; // default direction forward\n    groupFlags=0;\n    functions=0;\n    momentum_base=0;\n    momentumA=MOMENTUM_USE_DEFAULT;\n    momentumD=MOMENTUM_USE_DEFAULT;\n    targetSpeed=128;\n    savedSpeedCode=0; \n\n    snifferSpeedCode=128; // default direction forward\n    snifferFunctions=0;\n\n    consistLead=nullptr;\n    consistNext =nullptr;\n    consistReverse=false;\n\n    // Add to start of list\n    next = firstSlot;\n    firstSlot = this;\n    chainModified=true;\n};\n\n/* static */ LocoSlot *  LocoSlot::getSlot(uint16_t locoId, bool autoCreate) {\n  if (locoId==0) {\n    DIAG(F(\"LocoSlot::getSlot called with locoId 0\"));\n    return nullptr;\n  }\n\n  auto slot=firstSlot;\n  for(;slot;slot=slot->next){\n    if (slot->loco==locoId) return slot;\n  }\n  if (!autoCreate) return nullptr;\n  if (recycler) {\n    slot=recycler;\n    recycler=recycler->next;\n    // slot will be rechained into list in prepare()\n  } else {\n    if (slotCount>=MAX_LOCOS) {\n      DIAG(F(\"MAX_LOCOS %d EXCEEDED\"),MAX_LOCOS);\n      return nullptr; // Too many locos\n    }\n    slot=new LocoSlot();\n    if (!slot) return nullptr; // allocation failure\n    slotCount++;\n  }\n  slot->prepare(locoId);\n  return slot;\n}\n\nvoid LocoSlot::forget() {\n  // remove from list\n  if (firstSlot==this) {\n    firstSlot=next;\n  } else {\n    LocoSlot * prev;\n    for(prev=firstSlot; prev && prev->next!=this; prev=prev->next);\n    if (prev) prev->next=next;\n  }\n  // add to recycler\n  next=recycler;\n  recycler=this;\n  chainModified=true;\n}\n\n/* static */ void LocoSlot::forgetAll() {\n  // remove entire list\n  LocoSlot * killnext=nullptr; \n  for (auto slot=firstSlot;slot;slot=killnext) {\n    killnext=slot->next;\n    delete(slot);\n  }\n  firstSlot=nullptr;\n  // remove recycler \n  killnext=nullptr; \n  for (auto slot=recycler;slot;slot=killnext) {\n    killnext=slot->next;\n    delete(slot);\n  }\n  recycler=nullptr;\n  chainModified=true;\n  slotCount=0;\n}\n\n/* static */ void LocoSlot::dumpTable(Print * output) {\n  StringFormatter::send(output, F(\"\\n<* LocoSlots %d/%d size=%db\"),\n    slotCount,MAX_LOCOS,sizeof(LocoSlot));\n  for (auto slot=firstSlot; slot; slot=slot->next) {\n    StringFormatter::send(output, \n      F(\"\\n Loco=%-5d s=%-3d f=%-11l t=%-3d\"),\n      slot->loco,slot->speedCode,slot->functions,\n      slot->targetSpeed);\n    if (slot->isConsistFollower()) {\n      StringFormatter::send(output, F(\" (Follows %d %s)\"),\n        slot->getConsistLead()->getLoco(),\n        slot->isConsistReverse() ? \"Reversed\":\"Normal\");\n    } \n    else {\n      StringFormatter::send(output, \n        F(\" mA=%-3d mD=%-3d\"),\n        slot->momentumA,slot->momentumD);\n#ifdef ARDUINO_ARCH_ESP32\n    StringFormatter::send(output, F(\" Ss=%-3d Sf=%-11l\"),\n      slot->snifferSpeedCode,slot->snifferFunctions);\n#endif      \n    }\n  }\noutput->print(F(\"\\n*>\\n\"));\n}\n\nvoid LocoSlot::saveSpeed() {\n  savedSpeedCode=targetSpeed;\n}\nbyte LocoSlot::getSavedSpeedCode() {\n  return savedSpeedCode;\n}\n"
  },
  {
    "path": "LocoSlot.h",
    "content": "/* Copyright (c) 2023 Harald Barth\n * Copyright (c) 2025 Chris Harlow\n *\n * This source is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This source is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this software.  If not, see\n * <http://www.gnu.org/licenses/>.\n */\n\n/* this represents a slot in the loco lookup table where\nvarious loco-specific values are stored.\n*/\n#ifndef LocoSlot_h\n#define LocoSlot_h  \n#include <Arduino.h>\nconst byte MOMENTUM_USE_DEFAULT=255;\n\nclass LocoSlot {\nprivate:\n  LocoSlot() {};\n  void prepare(uint16_t locoId);\n  \n  static LocoSlot* firstSlot;\n  static LocoSlot* recycler;\n  static uint16_t slotCount;\n\n\n  // Member veriables here are arranged to reduce padding waste\n  uint32_t functions;        // DCC function map\n  uint32_t snifferFunctions; // sniffer function map\n  uint32_t momentum_base;    // millis() when speed modified under momentum\n  LocoSlot* next;\n  LocoSlot* consistLead;\n  LocoSlot* consistNext;\n  \n  // DCC data for this loco \n  uint16_t loco;          // DCC loco id\n  byte targetSpeed;       // speed set by throttle\n  byte speedCode;         // current DCC speed and direction\n  byte snifferSpeedCode;  // sniffer speed and direction\n  byte savedSpeedCode;    // speed code saved by EXRAIL SAVE_SPEED\n  byte momentumA;         // momentum accelerating\n  byte momentumD;         // momentum decelerating\n  byte groupFlags;        // function groups acivated\n  bool consistReverse;   // true if loco is reversed in consist\n\n  // SNIFFER data for each loco exists to allow sniffer to detect and ignore \n  // sniffed reminders for locos that have been taken over\n  // by DCCEX. These bytes will be dropped on a Mega.\n  \npublic: \n  // set chainModified true when the chain of locos is modified\n  // so that reminders can restart from the beginning.\n  static bool chainModified;\n  \n  static LocoSlot * getFirst(){return firstSlot;}\n  static void forgetAll();\n \n  static LocoSlot * getSlot(uint16_t locoId, bool autoCreate);\n  static void dumpTable(Print *output);\n\n  LocoSlot * getNext() {return next;}\n  uint16_t getLoco() { return loco; }\n  byte getSnifferSpeedCode() { return snifferSpeedCode; }\n  unsigned long getSnifferFunctions() { return snifferFunctions; }\n  void setSnifferSpeedCode(byte v) { snifferSpeedCode=v; }\n  void setSnifferFunctions(unsigned long v) { snifferFunctions=v; }\n\n  \n  byte getMomentumA() { return momentumA; }\n  byte getMomentumD() { return momentumD; }\n  void setMomentumA(byte v) { momentumA=v; }\n  void setMomentumD(byte v) { momentumD=v; }\n  uint32_t getMomentumBase() { return momentum_base; }\n  void setMomentumBase(uint32_t v) { momentum_base=v; } \n  byte getTargetSpeed() { return targetSpeed; }\n  void setTargetSpeed(byte v) { targetSpeed=v; }  \n\n  byte getSpeedCode() { return speedCode; }\n  void setSpeedCode(byte v) { speedCode=v; }\n  byte getGroupFlags() { return groupFlags; }\n  void setGroupFlags(byte v) { groupFlags=v; }\n  uint32_t getFunctions() { return functions; }\n  void setFunctions(uint32_t v) { functions=v; }\n  void forget();\n  void saveSpeed();\n  byte getSavedSpeedCode() ;\n  LocoSlot * getConsistLead() { return consistLead; }\n  LocoSlot * getConsistNext() { return consistNext; } \n  void setConsistLead(LocoSlot * lead) { consistLead=lead; }\n  void setConsistNext(LocoSlot * nextInConsist) { consistNext=nextInConsist; }\n  bool isConsistReverse() { return consistReverse; }\n  void setConsistReverse(bool rev) { consistReverse=rev; }\n  bool isConsistLead() {return (consistNext != nullptr) && consistLead==nullptr; }\n  bool isConsistFollower() {return (consistLead != nullptr); }\n};\n#endif"
  },
  {
    "path": "MotorDriver.cpp",
    "content": "/*\n *  © 2022-2024 Paul M Antoine\n *  © 2024 Herb Morton\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2023 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  © 2023 Colin Murdoch\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include <Arduino.h>\n#include \"MotorDriver.h\"\n#include \"DCCWaveform.h\"\n#include \"DCCTimer.h\"\n#include \"DIAG.h\"\n#include \"EXRAIL2.h\"\n\nunsigned long MotorDriver::globalOverloadStart = 0;\n\nvolatile portreg_t shadowPORTA;\nvolatile portreg_t shadowPORTB;\nvolatile portreg_t shadowPORTC;\n#if defined(ARDUINO_ARCH_STM32)\nvolatile portreg_t shadowPORTD;\nvolatile portreg_t shadowPORTE;\nvolatile portreg_t shadowPORTF;\nvolatile portreg_t shadowPORTG;\nvolatile portreg_t shadowPORTH;\n#endif\n\nMotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin,\n                         byte current_pin, float sense_factor, unsigned int trip_milliamps, int16_t fault_pin) {\n  const FSH * warnString = F(\"** WARNING **\");\n\n  invertPower=power_pin < 0;\n  if (invertPower) {\n    powerPin = 0-power_pin;\n    IODevice::write(powerPin,HIGH);// set to OUTPUT and off\n  } else {\n    powerPin = power_pin;\n    IODevice::write(powerPin,LOW);// set to OUTPUT and off\n  }\n  \n  signalPin=signal_pin;\n  getFastPin(F(\"SIG\"),signalPin,fastSignalPin);\n  pinMode(signalPin, OUTPUT);\n\n  fastSignalPin.shadowinout = NULL;\n  if (HAVE_PORTA(fastSignalPin.inout == &PORTA)) {\n    DIAG(F(\"Found PORTA pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTA;\n  }\n  if (HAVE_PORTB(fastSignalPin.inout == &PORTB)) {\n    DIAG(F(\"Found PORTB pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTB;\n  }\n  if (HAVE_PORTC(fastSignalPin.inout == &PORTC)) {\n    DIAG(F(\"Found PORTC pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTC;\n  }\n  if (HAVE_PORTD(fastSignalPin.inout == &PORTD)) {\n    DIAG(F(\"Found PORTD pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTD;\n  }\n  if (HAVE_PORTE(fastSignalPin.inout == &PORTE)) {\n    DIAG(F(\"Found PORTE pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTE;\n  }\n  if (HAVE_PORTF(fastSignalPin.inout == &PORTF)) {\n    DIAG(F(\"Found PORTF pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTF;\n  }\n  if (HAVE_PORTG(fastSignalPin.inout == &PORTG)) {\n    DIAG(F(\"Found PORTG pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTG;\n  }\n  if (HAVE_PORTH(fastSignalPin.inout == &PORTH)) {\n    DIAG(F(\"Found PORTH pin %d\"),signalPin);\n    fastSignalPin.shadowinout = fastSignalPin.inout;\n    fastSignalPin.inout = &shadowPORTH;\n  }\n\n  signalPin2=signal_pin2;\n  if (signalPin2!=UNUSED_PIN) {\n    dualSignal=true;\n    getFastPin(F(\"SIG2\"),signalPin2,fastSignalPin2);\n    pinMode(signalPin2, OUTPUT);\n\n    fastSignalPin2.shadowinout = NULL;\n    if (HAVE_PORTA(fastSignalPin2.inout == &PORTA)) {\n      DIAG(F(\"Found PORTA pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTA;\n    }\n    if (HAVE_PORTB(fastSignalPin2.inout == &PORTB)) {\n      DIAG(F(\"Found PORTB pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTB;\n    }\n    if (HAVE_PORTC(fastSignalPin2.inout == &PORTC)) {\n      DIAG(F(\"Found PORTC pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTC;\n    }\n    if (HAVE_PORTD(fastSignalPin2.inout == &PORTD)) {\n      DIAG(F(\"Found PORTD pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTD;\n    }\n    if (HAVE_PORTE(fastSignalPin2.inout == &PORTE)) {\n      DIAG(F(\"Found PORTE pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTE;\n    }\n    if (HAVE_PORTF(fastSignalPin2.inout == &PORTF)) {\n      DIAG(F(\"Found PORTF pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTF;\n    }\n    if (HAVE_PORTG(fastSignalPin2.inout == &PORTG)) {\n      DIAG(F(\"Found PORTG pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTG;\n    }\n    if (HAVE_PORTH(fastSignalPin2.inout == &PORTH)) {\n      DIAG(F(\"Found PORTH pin %d\"),signalPin2);\n      fastSignalPin2.shadowinout = fastSignalPin2.inout;\n      fastSignalPin2.inout = &shadowPORTH;\n    }\n  }\n  else dualSignal=false; \n  \n  if (brake_pin!=UNUSED_PIN){\n    invertBrake=brake_pin < 0;\n    if (invertBrake)\n      brake_pin = 0-brake_pin;\n    if (brake_pin > MAX_PIN)\n      DIAG(F(\"%S Brake pin %d > %d\"), warnString, brake_pin, MAX_PIN);\n    brakePin=(byte)brake_pin;\n    getFastPin(F(\"BRAKE\"),brakePin,fastBrakePin);\n    // if brake is used for railcom  cutout we need to do PORTX register trick here as well\n    pinMode(brakePin, OUTPUT);\n    setBrake(true);  // start with brake on in case we hace DC stuff going on\n  } else {\n    brakePin=UNUSED_PIN;\n  }\n  \n  currentPin=current_pin;\n  if (currentPin!=UNUSED_PIN) {\n    int ret = ADCee::init(currentPin);\n    if (ret < -1010) { // XXX give value a name later\n      DIAG(F(\"ADCee::init error %d, disable current pin %d\"), ret, currentPin);\n      currentPin = UNUSED_PIN;\n    }\n  }\n  senseOffset=0; // value can not be obtained until waveform is activated\n\n  if (fault_pin != UNUSED_PIN) {\n    invertFault=fault_pin < 0;\n    if (invertFault)\n      fault_pin =  0-fault_pin;\n    if (fault_pin > MAX_PIN)\n      DIAG(F(\"%S Fault pin %d > %d\"), warnString, fault_pin, MAX_PIN);\n    faultPin=(byte)fault_pin;\n    DIAG(F(\"Fault pin = %d invert %d\"), faultPin, invertFault);\n    getFastPin(F(\"FAULT\"),faultPin, 1 /*input*/, fastFaultPin);\n    pinMode(faultPin, INPUT);\n  } else {\n      faultPin=UNUSED_PIN;\n  }\n\n  // This conversion performed at compile time so the remainder of the code never needs\n  // float calculations or libraray code. \n  senseFactorInternal=sense_factor * senseScale; \n  tripMilliamps=trip_milliamps;\n#ifdef MAX_CURRENT\n  if (MAX_CURRENT > 0 && MAX_CURRENT < tripMilliamps)\n    tripMilliamps = MAX_CURRENT;\n#endif\n  rawCurrentTripValue=mA2raw(tripMilliamps);\n\n  if (rawCurrentTripValue + senseOffset > ADCee::ADCmax()) {\n    // This would mean that the values obtained from the ADC never\n    // can reach the trip value. So independent of the current, the\n    // short circuit protection would never trip. So we adjust the\n    // trip value so that it is tiggered when the ADC reports it's\n    // maximum value instead.\n\n    //    DIAG(F(\"Changing short detection value from %d to %d mA\"),\n    // raw2mA(rawCurrentTripValue), raw2mA(ADCee::ADCmax()-senseOffset));\n    rawCurrentTripValue=ADCee::ADCmax()-senseOffset;\n  }\n\n  if (currentPin==UNUSED_PIN) \n    DIAG(F(\"%S No current or short detection\"), warnString);\n  else  {\n    DIAG(F(\"Pin %d Max %dmA (%d)\"), currentPin, raw2mA(rawCurrentTripValue), rawCurrentTripValue);\n\n    // self testing diagnostic for the non-float converters... may be removed when happy\n    //  DIAG(F(\"senseFactorInternal=%d raw2mA(1000)=%d mA2Raw(1000)=%d\"),\n    //   senseFactorInternal, raw2mA(1000),mA2raw(1000));\n  }\n\n  progTripValue = mA2raw(TRIP_CURRENT_PROG); \n}\n\nbool MotorDriver::isPWMCapable() {\n    return (!dualSignal) && DCCTimer::isPWMPin(signalPin);\n}\n\n\nvoid MotorDriver::setPower(POWERMODE mode) {\n  if (powerMode == mode) return;\n  //DIAG(F(\"Track %c POWERMODE=%d\"), trackLetter, (int)mode);\n  lastPowerChange[(int)mode] = micros();\n  if (mode == POWERMODE::OVERLOAD)\n    globalOverloadStart = lastPowerChange[(int)mode];\n  bool on=(mode==POWERMODE::ON || mode ==POWERMODE::ALERT);\n  if (on) {\n    // when switching a track On, we need to check the crrentOffset with the pin OFF\n    if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) {\n        senseOffset = ADCee::read(currentPin);\n        if (Diag::ACK) DIAG(F(\"Track %c sensOffset=%d\"),trackLetter,senseOffset);\n    }\n\n    IODevice::write(powerPin,invertPower ? LOW : HIGH);\n    if (isProgTrack)\n      DCCWaveform::progTrack.clearResets();\n  }\n  else {\n      IODevice::write(powerPin,invertPower ? HIGH : LOW);\n  }\n  powerMode=mode; \n}\n\n// setBrake applies brake if on == true. So to get\n// voltage from the motor bride one needs to do a\n// setBrake(false).\n// If the brakePin is negative that means the sense\n// of the brake pin on the motor bridge is inverted\n// (HIGH == release brake) and setBrake does\n// compensate for that.\n//\nvoid MotorDriver::setBrake(bool on, bool interruptContext) {\n  if (brakePin == UNUSED_PIN) return;\n  if (!interruptContext) {noInterrupts();}\n  if (on ^ invertBrake)\n    setHIGH(fastBrakePin);\n  else\n    setLOW(fastBrakePin);\n  if (!interruptContext) {interrupts();}\n}\n\nbool MotorDriver::canMeasureCurrent() {\n  return currentPin!=UNUSED_PIN;\n}\n/*\n * Return the current reading as pin reading 0 to max resolution (1024 or 4096).\n * If the fault pin is activated return a negative current to show active fault pin.\n * As there is no -0, cheat a little and return -1 in that case.\n * \n * senseOffset handles the case where a shield returns values above or below \n * a central value depending on direction.\n *\n * Bool fromISR should be adjusted dependent how function is called\n */\nint MotorDriver::getCurrentRaw(bool fromISR) {\n  (void)fromISR;\n  if (currentPin==UNUSED_PIN) return 0; \n  int current;\n  current = ADCee::read(currentPin, fromISR);\n  // here one can diag raw value\n  // if (fromISR == false) DIAG(F(\"%c: %d\"), trackLetter, current);\n  current = current-senseOffset;     // adjust with offset\n  if (current<0) current=0-current;\n  // current >= 0 here, we use negative current as fault pin flag\n  if ((faultPin != UNUSED_PIN) && powerPin) {\n    if (invertFault ? isHIGH(fastFaultPin) : isLOW(fastFaultPin))\n      return (current == 0 ? -1 : -current);\n  }\n  return current;\n}\n\n#ifdef ANALOG_READ_INTERRUPT\n/*\n * This should only be called in interrupt context\n * Copies current value from HW to cached value in\n * Motordriver.\n */\n#pragma GCC push_options\n#pragma GCC optimize (\"-O3\")\nbool MotorDriver::sampleCurrentFromHW() {\n  byte low, high;\n  //if (!bit_is_set(ADCSRA, ADIF))\n  if (bit_is_set(ADCSRA, ADSC))\n    return false;\n  //  if ((ADMUX & mask) != (currentPin - A0))\n  //    return false;\n  low = ADCL; //must read low before high\n  high = ADCH;\n  bitSet(ADCSRA, ADIF);\n  sampleCurrent = (high << 8) | low;\n  sampleCurrentTimestamp = millis();\n  return true;\n}\nvoid MotorDriver::startCurrentFromHW() {\n#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n  const byte mask = 7;\n#else\n  const byte mask = 31;\n#endif\n  ADMUX=(1<<REFS0)|((currentPin-A0) & mask); //select AVCC as reference and set MUX\n  bitSet(ADCSRA,ADSC); // start conversion\n}\n#pragma GCC pop_options\n#endif //ANALOG_READ_INTERRUPT\n\n#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)\n#ifdef VARIABLE_TONES\nuint16_t taurustones[28] = { 165, 175, 196, 220,\n\t\t\t     247, 262, 294, 330,\n\t\t\t     349, 392, 440, 494,\n\t\t\t     523, 587, 659, 698,\n\t\t\t     494, 440, 392, 249,\n\t\t\t     330, 284, 262, 247,\n\t\t\t     220, 196, 175, 165 };\n#endif\n#endif\nvoid MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/) {\n  if (brakePin == UNUSED_PIN)\n    return;\n  // spedcoode is a dcc speed & direction\n  byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127\n  byte tDir=speedcode & 0x80;\n  byte brake;\n\n  if (tSpeed <= 1) brake = 255;\n  else if (tSpeed >= 127) brake = 0;\n  else  brake = 2 * (128-tSpeed);\n\n  { // new block because of variable f\n#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)\n    int f = frequency;\n#ifdef VARIABLE_TONES\n    if (tSpeed > 2) {\n      if (tSpeed <= 58) {\n\tf = taurustones[ (tSpeed-2)/2 ] ;\n      }\n    }\n#endif\n    //DIAG(F(\"Brake pin %d value %d freqency %d\"), brakePin, brake, f);\n    DCCTimer::DCCEXanalogWrite(brakePin, brake, invertBrake);\n    DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency\n#else // all AVR here\n    DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps\n    analogWrite(brakePin, invertBrake ? 255-brake : brake);\n#endif\n  }\n\n  //DIAG(F(\"DCSignal %d\"), speedcode);\n  if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {\n    noInterrupts();\n    HAVE_PORTA(shadowPORTA=PORTA);\n    setSignal(tDir);\n    HAVE_PORTA(PORTA=shadowPORTA);\n    interrupts();\n  } else if (HAVE_PORTB(fastSignalPin.shadowinout == &PORTB)) {\n    noInterrupts();\n    HAVE_PORTB(shadowPORTB=PORTB);\n    setSignal(tDir);\n    HAVE_PORTB(PORTB=shadowPORTB);\n    interrupts();\n  } else if (HAVE_PORTC(fastSignalPin.shadowinout == &PORTC)) {\n    noInterrupts();\n    HAVE_PORTC(shadowPORTC=PORTC);\n    setSignal(tDir);\n    HAVE_PORTC(PORTC=shadowPORTC);\n    interrupts();\n  } else if (HAVE_PORTD(fastSignalPin.shadowinout == &PORTD)) {\n    noInterrupts();\n    HAVE_PORTD(shadowPORTD=PORTD);\n    setSignal(tDir);\n    HAVE_PORTD(PORTD=shadowPORTD);\n    interrupts();\n  } else if (HAVE_PORTE(fastSignalPin.shadowinout == &PORTE)) {\n    noInterrupts();\n    HAVE_PORTE(shadowPORTE=PORTE);\n    setSignal(tDir);\n    HAVE_PORTE(PORTE=shadowPORTE);\n    interrupts();\n  } else if (HAVE_PORTF(fastSignalPin.shadowinout == &PORTF)) {\n    noInterrupts();\n    HAVE_PORTF(shadowPORTF=PORTF);\n    setSignal(tDir);\n    HAVE_PORTF(PORTF=shadowPORTF);\n    interrupts();\n  } else if (HAVE_PORTG(fastSignalPin.shadowinout == &PORTG)) {\n    noInterrupts();\n    HAVE_PORTG(shadowPORTG=PORTG);\n    setSignal(tDir);\n    HAVE_PORTG(PORTG=shadowPORTG);\n    interrupts();\n  } else if (HAVE_PORTH(fastSignalPin.shadowinout == &PORTH)) {\n    noInterrupts();\n    HAVE_PORTH(shadowPORTH=PORTH);\n    setSignal(tDir);\n    HAVE_PORTH(PORTH=shadowPORTH);\n    interrupts();\n  } else {\n    noInterrupts();\n    setSignal(tDir);\n    interrupts();\n  }\n}\nvoid MotorDriver::throttleInrush(bool on) {\n  if (brakePin == UNUSED_PIN)\n    return;\n  if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT | TRACK_MODE_BOOST)))\n    return;\n  byte duty = on ? 207 : 0; // duty of 81% at 62500Hz this gives pauses of 3usec\n#if defined(ARDUINO_ARCH_ESP32)\n  if(on) {\n    DCCTimer::DCCEXInrushControlOn(brakePin, duty, invertBrake);\n  } else {\n    ledcDetachPin(brakePin); // not DCCTimer::DCCEXledcDetachPin() as we have not\n                             // registered the pin in the pin to channel array\n  }\n#elif defined(ARDUINO_ARCH_STM32)\n  if(on) {\n    DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max\n    DCCTimer::DCCEXanalogWrite(brakePin,duty,invertBrake);\n  } else {\n    pinMode(brakePin, OUTPUT);\n  }\n#else // all AVR here\n  if (invertBrake)\n    duty = 255-duty;\n  if(on){\n    DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max\n  }\n  analogWrite(brakePin,duty);\n#endif\n}\nunsigned int MotorDriver::raw2mA( int raw) {\n  //DIAG(F(\"%d = %d * %d / %d\"), (int32_t)raw * senseFactorInternal / senseScale, raw, senseFactorInternal, senseScale);\n  return (int32_t)raw * senseFactorInternal / senseScale;\n}\nunsigned int MotorDriver::mA2raw( unsigned int mA) {\n  //DIAG(F(\"%d = %d * %d / %d\"), (int32_t)mA * senseScale / senseFactorInternal, mA, senseScale, senseFactorInternal);\n  return (int32_t)mA * senseScale / senseFactorInternal;\n}\n\nvoid  MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & result) {\n    // DIAG(F(\"MotorDriver %S Pin=%d,\"),type,pin);\n    (void) type; // avoid compiler warning if diag not used above.\n#if defined(ARDUINO_ARCH_SAMD)\n    PortGroup *port = digitalPinToPort(pin);\n#elif defined(ARDUINO_ARCH_STM32)\n    GPIO_TypeDef *port = digitalPinToPort(pin);\n#else\n    uint8_t port = digitalPinToPort(pin);\n#endif\n    if (input)\n      result.inout = portInputRegister(port);\n    else\n      result.inout = portOutputRegister(port);\n    result.maskHIGH = digitalPinToBitMask(pin);\n    result.maskLOW = ~result.maskHIGH;\n    // DIAG(F(\" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x\"),port, result.inout,input,result.maskHIGH);\n}\n\n///////////////////////////////////////////////////////////////////////////////////////////\n// checkPowerOverload(useProgLimit, trackno)\n// bool useProgLimit: Trackmanager knows if this track is in prog mode or in main mode\n// byte trackno: trackmanager knows it's number (could be skipped?)\n//\n// Short ciruit handling strategy:\n//\n// There are the following power states: ON ALERT OVERLOAD OFF\n// OFF state is only changed to/from manually. Power is on\n// during ON and ALERT. Power is off during OVERLOAD and OFF.\n// The overload mechanism changes between the other states like\n//\n// ON -1-> ALERT -2-> OVERLOAD -3-> ALERT -4-> ON\n// or\n// ON -1-> ALERT -4-> ON\n//\n// Times are in class MotorDriver (MotorDriver.h).\n//\n// 1. ON to ALERT:\n// Transition on fault pin condition or current overload\n//\n// 2. ALERT to OVERLOAD:\n// Transition happens if different timeouts have elapsed.\n// If only the fault pin is active, timeout is\n// POWER_SAMPLE_IGNORE_FAULT_LOW (100ms)\n// If only overcurrent is detected, timeout is\n// POWER_SAMPLE_IGNORE_CURRENT (100ms)\n// If fault pin and overcurrent are active, timeout is\n// POWER_SAMPLE_IGNORE_FAULT_HIGH (5ms)\n// Transition to OVERLOAD turns off power to the affected\n// output (unless fault pins are shared)\n// If the transition conditions are not fullfilled,\n// transition according to 4 is tested.\n//\n// 3. OVERLOAD to ALERT\n// Transiton happens when timeout has elapsed, timeout\n// is named power_sample_overload_wait. It is started\n// at POWER_SAMPLE_OVERLOAD_WAIT (40ms) at first entry\n// to OVERLOAD and then increased by a factor of 2\n// at further entries to the OVERLOAD condition. This\n// happens until POWER_SAMPLE_RETRY_MAX (10sec) is reached.\n// power_sample_overload_wait is reset by a poweroff or\n// a POWER_SAMPLE_ALL_GOOD (5sec) period during ON.\n// After timeout power is turned on again and state\n// goes back to ALERT.\n//\n// 4. ALERT to ON\n// Transition happens by watching the current and fault pin\n// samples during POWER_SAMPLE_ALERT_GOOD (20ms) time. If\n// values have been good during that time, transition is\n// made back to ON. Note that even if state is back to ON,\n// the power_sample_overload_wait time is first reset\n// later (see above).\n//\n// The time keeping is handled by timestamps lastPowerChange[]\n// which are set by each power change and by lastBadSample which\n// keeps track if conditions during ALERT have been good enough\n// to go back to ON. The time differences are calculated by\n// microsSinceLastPowerChange().\n//\n\nvoid MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {\n\n  switch (powerMode) {\n\n  case POWERMODE::OFF: {\n    lastPowerMode = POWERMODE::OFF;\n    power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;\n    break;\n  }\n\n  case POWERMODE::ON: {\n    lastPowerMode = POWERMODE::ON;\n    bool cF = checkFault();\n    bool cC = checkCurrent(useProgLimit);\n    if(cF || cC ) {\n      if (cC) {\n\tunsigned int mA=raw2mA(lastCurrent);\n\tDIAG(F(\"TRACK %c ALERT %s %dmA\"), trackno + 'A',\n\t     cF ? \"FAULT\" : \"\",\n\t     mA);\n      } else {\n\tDIAG(F(\"TRACK %c ALERT FAULT\"), trackno + 'A');\n      }\n      setPower(POWERMODE::ALERT);\n      if ((trackMode & TRACK_MODIFIER_AUTO) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){\n\tDIAG(F(\"TRACK %c INVERT\"), trackno + 'A');\n\tinvertOutput();\n      }\n      break;\n    }\n    // all well\n    if (microsSinceLastPowerChange(POWERMODE::ON) > POWER_SAMPLE_ALL_GOOD) {\n      power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;\n    }\n    break;\n  }\n\n  case POWERMODE::ALERT: {\n    // set local flags that handle how much is output to diag (do not output duplicates)\n    bool notFromOverload = (lastPowerMode != POWERMODE::OVERLOAD);\n    bool powerModeChange = (powerMode != lastPowerMode);\n    unsigned long now = micros();\n    if (powerModeChange)\n      lastBadSample = now;\n    lastPowerMode = POWERMODE::ALERT;\n    // check how long we have been in this state\n    unsigned long mslpc = microsSinceLastPowerChange(POWERMODE::ALERT);\n    if(checkFault()) {\n      throttleInrush(true);\n      lastBadSample = now;\n      unsigned long timeout = checkCurrent(useProgLimit) ? POWER_SAMPLE_IGNORE_FAULT_HIGH : POWER_SAMPLE_IGNORE_FAULT_LOW;\n      if ( mslpc < timeout) {\n\tif (powerModeChange)\n\t  DIAG(F(\"TRACK %c FAULT PIN (%M ignore)\"), trackno + 'A', timeout);\n\tbreak;\n      }\n      DIAG(F(\"TRACK %c FAULT PIN detected after %4M. Pause %4M)\"), trackno + 'A', mslpc, power_sample_overload_wait);\n      throttleInrush(false);\n      setPower(POWERMODE::OVERLOAD);\n      break;\n    }\n    if (checkCurrent(useProgLimit)) {\n      lastBadSample = now;\n      if (mslpc < POWER_SAMPLE_IGNORE_CURRENT) {\n\tif (powerModeChange) {\n\t  unsigned int mA=raw2mA(lastCurrent);\n\t  DIAG(F(\"TRACK %c CURRENT (%M ignore) %dmA\"), trackno + 'A', POWER_SAMPLE_IGNORE_CURRENT, mA);\n\t}\n\tbreak;\n      }\n      unsigned int mA=raw2mA(lastCurrent);\n      unsigned int maxmA=raw2mA(tripValue);\n      DIAG(F(\"TRACK %c POWER OVERLOAD %4dmA (max %4dmA) detected after %4M. Pause %4M\"),\n\t   trackno + 'A', mA, maxmA, mslpc, power_sample_overload_wait);\n      throttleInrush(false);\n      setPower(POWERMODE::OVERLOAD);\n      break;\n    }\n    // all well\n    unsigned long goodtime = micros() - lastBadSample;\n    if (goodtime > POWER_SAMPLE_ALERT_GOOD) {\n      if (true || notFromOverload) { // we did a RESTORE message XXX\n\tunsigned int mA=raw2mA(lastCurrent);\n\tDIAG(F(\"TRACK %c NORMAL (after %M/%M) %dmA\"), trackno + 'A', goodtime, mslpc, mA);\n      }\n      throttleInrush(false);\n      setPower(POWERMODE::ON);\n      break;\n    }\n    if (goodtime > POWER_SAMPLE_ALERT_GOOD/2) {\n      throttleInrush(false);\n    }\n    break;\n  }\n\n  case POWERMODE::OVERLOAD: {\n    lastPowerMode = POWERMODE::OVERLOAD;\n    unsigned long mslpc = (commonFaultPin ? (micros() - globalOverloadStart) : microsSinceLastPowerChange(POWERMODE::OVERLOAD));\n    if (mslpc > power_sample_overload_wait) {\n      // adjust next wait time\n      power_sample_overload_wait *= 2;\n      if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX)\n\t      power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;\n  #ifdef EXRAIL_ACTIVE\n      DIAG(F(\"Calling EXRAIL\"));\n      RMFT2::powerEvent(trackno, true); // Tell EXRAIL we have an overload\n  #endif\n      // power on test\n      DIAG(F(\"TRACK %c POWER RESTORE (after %4M)\"), trackno + 'A', mslpc);\n      setPower(POWERMODE::ALERT);\n    }\n    break;\n  }\n\n  default:\n    break;\n  }\n}\n"
  },
  {
    "path": "MotorDriver.h",
    "content": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  © 2022,2023 Harald Barth\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef MotorDriver_h\n#define MotorDriver_h\n#include \"FSH.h\"\n#include \"IODevice.h\"\n#include \"DCCTimer.h\"\n#include <wiring_private.h>\n\n#include \"TemplateForEnums.h\"\n// use powers of two so we can do logical and/or on the track modes in if clauses.\n// For example TRACK_MODE_DC_INV is (TRACK_MODE_DC|TRACK_MODIFIER_INV)\nenum TRACK_MODE : byte {\n  // main modes\n  TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,\n  TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,\n  // modifiers\n  TRACK_MODIFIER_INV = 64, TRACK_MODIFIER_AUTO = 128,\n#ifdef ARDUINO_ARCH_ESP32\n  TRACK_MODE_BOOST = 32,\n  TRACK_MODE_BOOST_INV = TRACK_MODE_BOOST|TRACK_MODIFIER_INV,\n  TRACK_MODE_BOOST_AUTO = TRACK_MODE_BOOST|TRACK_MODIFIER_AUTO,\n#else\n  TRACK_MODE_BOOST = 0,\n  TRACK_MODE_BOOST_INV = 0,\n  TRACK_MODE_BOOST_AUTO = 0,\n#endif\n  // derived modes; TRACK_ALL is calles that so it does not match TRACK_MODE_*\n  TRACK_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,\n  TRACK_MODE_MAIN_INV  =  TRACK_MODE_MAIN|TRACK_MODIFIER_INV,\n  TRACK_MODE_MAIN_AUTO =  TRACK_MODE_MAIN|TRACK_MODIFIER_AUTO,\n  TRACK_MODE_DC_INV =  TRACK_MODE_DC|TRACK_MODIFIER_INV,\n  TRACK_MODE_DCX = TRACK_MODE_DC_INV // DCX is other name for historical reasons\n};\n\n#define setHIGH(fastpin)  *fastpin.inout |= fastpin.maskHIGH\n#define setLOW(fastpin)   *fastpin.inout &= fastpin.maskLOW\n#define isHIGH(fastpin)   (*fastpin.inout & fastpin.maskHIGH)\n#define isLOW(fastpin)    (!isHIGH(fastpin))\n\n#define TOKENPASTE(x, y) x ## y\n#define TOKENPASTE2(x, y) TOKENPASTE(x, y)\n\n#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)\n#define HAVE_PORTA(X) X\n#define HAVE_PORTB(X) X\n#define HAVE_PORTC(X) X\n#endif\n#if defined(ARDUINO_AVR_UNO)\n#define HAVE_PORTB(X) X\n#endif\n#if defined(ARDUINO_ARCH_SAMD)\n#define PORTA REG_PORT_OUT0\n#define HAVE_PORTA(X) X\n#define PORTB REG_PORT_OUT1\n#define HAVE_PORTB(X) X\n#endif\n#if defined(ARDUINO_ARCH_STM32)\n#define PORTA GPIOA->ODR\n#define HAVE_PORTA(X) X\n#define PORTB GPIOB->ODR\n#define HAVE_PORTB(X) X\n#define PORTC GPIOC->ODR\n#define HAVE_PORTC(X) X\n#define PORTD GPIOD->ODR\n#define HAVE_PORTD(X) X\n#if defined(GPIOE)\n#define PORTE GPIOE->ODR\n#define HAVE_PORTE(X) X\n#endif\n#if defined(GPIOF)\n#define PORTF GPIOF->ODR\n#define HAVE_PORTF(X) X\n#endif\n#if defined(GPIOG)\n#define PORTG GPIOG->ODR\n#define HAVE_PORTG(X) X\n#endif\n#if defined(GPIOH)\n#define PORTH GPIOH->ODR\n#define HAVE_PORTH(X) X\n#endif\n#endif\n\n// if macros not defined as pass-through we define\n// them here as someting that is valid as a\n// statement and evaluates to false.\n#ifndef HAVE_PORTA\n#define HAVE_PORTA(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTB\n#define HAVE_PORTB(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTC\n#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTD\n#define HAVE_PORTD(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTE\n#define HAVE_PORTE(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTF\n#define HAVE_PORTF(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTG\n#define HAVE_PORTG(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n#ifndef HAVE_PORTH\n#define HAVE_PORTH(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0\n#endif\n\n// Virtualised Motor shield 1-track hardware Interface\n\n#ifndef UNUSED_PIN     // sync define with the one in MotorDrivers.h\n#define UNUSED_PIN 255 // inside uint8_t\n#endif\n#define MAX_PIN 254\n\nclass pinpair {\npublic:\n  pinpair(byte p1, byte p2) {\n    pin = p1;\n    invpin = p2;\n  };\n  byte pin = UNUSED_PIN;\n  byte invpin = UNUSED_PIN;\n};\n\n#if defined(__IMXRT1062__) || defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)\ntypedef uint32_t portreg_t;\n#else\ntypedef uint8_t portreg_t;\n#endif\n\nstruct FASTPIN {\n  volatile portreg_t *inout;\n  portreg_t maskHIGH;\n  portreg_t maskLOW;\n  volatile portreg_t *shadowinout;\n};\n// The port registers that are shadowing\n// the real port registers. These are\n// defined in Motordriver.cpp\nextern volatile portreg_t shadowPORTA;\nextern volatile portreg_t shadowPORTB;\nextern volatile portreg_t shadowPORTC;\nextern volatile portreg_t shadowPORTD;\nextern volatile portreg_t shadowPORTE;\nextern volatile portreg_t shadowPORTF;\nextern volatile portreg_t shadowPORTG;\nextern volatile portreg_t shadowPORTH;\n\nenum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT };\n\nclass MotorDriver {\n  public:\n    \n    MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin, \n                byte current_pin, float senseFactor, unsigned int tripMilliamps, int16_t fault_pin);\n    void setPower( POWERMODE mode);\n    POWERMODE getPower() { return powerMode;}\n    // as the port registers can be shadowed to get syncronized DCC signals\n    // we need to take care of that and we have to turn off interrupts if\n    // we setSignal() or setBrake() or setPower() during that time as\n    // otherwise the call from interrupt context can undo whatever we do\n    // from outside interrupt\n    void setBrake( bool on, bool interruptContext=false);\n    __attribute__((always_inline)) inline void setSignal( bool high) {\n#ifndef ARDUINO_ARCH_ESP32\n      if (invertPhase)\n\thigh = !high;\n#endif\n      if (trackPWM) {\n\tDCCTimer::setPWM(signalPin,high);\n      }\n      else {\n\tif (high) {\n\t  setHIGH(fastSignalPin);\n\t  if (dualSignal) setLOW(fastSignalPin2);\n\t}\n\telse {\n\t  setLOW(fastSignalPin);\n\t  if (dualSignal) setHIGH(fastSignalPin2);\n\t}\n      }\n    };\n    inline void enableSignal(bool on) {\n      if (on)\n\tpinMode(signalPin, OUTPUT);\n      else\n\tpinMode(signalPin, INPUT);\n      if (signalPin2 != UNUSED_PIN) {\n\tif (on)\n\t  pinMode(signalPin2, OUTPUT);\n\telse\n\t  pinMode(signalPin2, INPUT);\n      }\n    };\n    inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };\n    inline int8_t getBrakePinSigned() { return invertBrake ? -brakePin : brakePin; };\n    void setDCSignal(byte speedByte, uint8_t frequency=0);\n    void throttleInrush(bool on);\n    inline void detachDCSignal() {\n#if defined(__arm__)\n      pinMode(brakePin, OUTPUT);\n#elif defined(ARDUINO_ARCH_ESP32)\n      DCCTimer::DCCEXledcDetachPin(brakePin);\n#else\n      setDCSignal(128);\n#endif\n    };\n    int  getCurrentRaw(bool fromISR=false);\n    unsigned int raw2mA( int raw);\n    unsigned int mA2raw( unsigned int mA);\n    inline bool brakeCanPWM() {\n#if defined(ARDUINO_ARCH_ESP32)\n      return (brakePin != UNUSED_PIN); // This was just (true) but we probably do need to check for UNUSED_PIN!\n#elif defined(__arm__)\n      // On ARM we can use digitalPinHasPWM\n      return ((brakePin!=UNUSED_PIN) && (digitalPinHasPWM(brakePin)));\n#elif defined(digitalPinToTimer)\n      return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));\n#else\n      return (brakePin<14 && brakePin >1);\n#endif\n    }\n    inline int getRawCurrentTripValue() {\n\t    return rawCurrentTripValue;\n    }\n    bool isPWMCapable();\n    bool canMeasureCurrent();\n    bool trackPWM = false; // this track uses PWM timer to generate the DCC waveform\n    bool commonFaultPin = false; // This is a stupid motor shield which has only a common fault pin for both outputs\n    inline byte setCommonFaultPin() {\n      return commonFaultPin = true;\n    }\n    inline byte getFaultPin() {\n\treturn faultPin;\n    }\n    inline void makeProgTrack(bool on) {  // let this output know it's a prog track.\n      isProgTrack = on;\n    }\n    void checkPowerOverload(bool useProgLimit, byte trackno);\n    inline void setTrackLetter(char c) {\n      trackLetter = c;\n    };\n    // this returns how much time has passed since the last power change. If it\n    // was really long ago (approx > 52min) advance counter approx 35 min so that\n    // we are at 18 minutes again. Times for 32 bit unsigned long.\n    inline unsigned long microsSinceLastPowerChange(POWERMODE mode) {\n      unsigned long now = micros();\n      unsigned long diff = now - lastPowerChange[(int)mode];\n      if (diff > (1UL << (7 *sizeof(unsigned long)))) // 2^(4*7)us = 268.4 seconds\n        lastPowerChange[(int)mode] = now - 30000000UL;           // 30 seconds ago\n      return diff;\n    };\n#ifdef ANALOG_READ_INTERRUPT\n    bool sampleCurrentFromHW();\n    void startCurrentFromHW();\n#endif\n  inline void setMode(TRACK_MODE m) {\n    trackMode = m;\n    invertOutput(trackMode & TRACK_MODIFIER_INV);\n  };\n  inline void invertOutput() {               // toggles output inversion\n    invertPhase = !invertPhase;\n    invertOutput(invertPhase);\n  };\n  inline void invertOutput(bool b) {         // sets output inverted or not\n    if (b)\n      invertPhase = 1;\n    else\n      invertPhase = 0;\n#if defined(ARDUINO_ARCH_ESP32)\n    pinpair p = getSignalPin();\n    uint32_t *outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.pin);\n    if (invertPhase) // set or clear the invert bit in the gpio out register\n      *outreg |=  ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);\n    else\n      *outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);\n    if (p.invpin != UNUSED_PIN) {\n      outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.invpin);\n      if (invertPhase) // clear or set the invert bit in the gpio out register\n\t*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);\n      else\n\t*outreg |=  ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);\n    }\n#endif\n  };\n  inline TRACK_MODE getMode() {\n    return trackMode;\n  };\n  private:\n    char trackLetter = '?';\n    bool isProgTrack = false; // tells us if this is a prog track\n    void  getFastPin(const FSH* type,int pin, bool input, FASTPIN & result);\n    inline void  getFastPin(const FSH* type,int pin, FASTPIN & result) {\n\tgetFastPin(type, pin, 0, result);\n    };\n    // side effect sets lastCurrent and tripValue\n    inline bool checkCurrent(bool useProgLimit) {\n      tripValue= useProgLimit?progTripValue:getRawCurrentTripValue();\n      lastCurrent = getCurrentRaw();\n      if (lastCurrent < 0)\n\tlastCurrent = -lastCurrent;\n      return lastCurrent >= tripValue;\n    };\n    // side effect sets lastCurrent\n    inline bool checkFault() {\n      lastCurrent = getCurrentRaw();\n      return lastCurrent < 0;\n    };\n    VPIN powerPin;\n    byte signalPin, signalPin2, currentPin, faultPin, brakePin;\n    FASTPIN fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin;\n    bool dualSignal;       // true to use signalPin2\n    bool invertBrake;       // brake pin passed as negative means pin is inverted\n    bool invertPower;       // power pin passed as negative means pin is inverted\n    bool invertFault;       // fault pin passed as negative means pin is inverted\n    bool invertPhase = 0;   // phase of out pin is inverted\n    // Raw to milliamp conversion factors avoiding float data types.\n    // Milliamps=rawADCreading * sensefactorInternal / senseScale\n    //\n    // senseScale is chosen as 256 to give enough scale for 2 decimal place \n    // raw->mA conversion with an ultra fast optimised integer multiplication  \n    int senseFactorInternal;  // set to senseFactor * senseScale\n    static const int senseScale=256;\n    int senseOffset;\n    unsigned int tripMilliamps;\n    int rawCurrentTripValue;\n    // current sampling\n    POWERMODE powerMode;\n    POWERMODE lastPowerMode;\n    unsigned long lastPowerChange[4];         // timestamp in microseconds\n    unsigned long lastBadSample;              // timestamp in microseconds\n    // used to sync restore time when common Fault pin detected\n    static unsigned long globalOverloadStart; // timestamp in microseconds\n    int progTripValue;\n    int  lastCurrent; //temp value\n    int  tripValue;   //temp value\n#ifdef ANALOG_READ_INTERRUPT\n    volatile unsigned long sampleCurrentTimestamp;\n    volatile uint16_t sampleCurrent;\n#endif\n    int maxmA;\n    int tripmA;\n\n    // Times for overload management. Unit: microseconds.\n    // Base for wait time until power is turned on again\n    static const unsigned long POWER_SAMPLE_OVERLOAD_WAIT =     40000UL;\n    // Time after we consider all faults old and forgotten\n    static const unsigned long POWER_SAMPLE_ALL_GOOD =        5000000UL;\n    // Time after which we consider a ALERT over \n    static const unsigned long POWER_SAMPLE_ALERT_GOOD =        20000UL;\n    // How long to ignore fault pin if current is under limit\n    static const unsigned long POWER_SAMPLE_IGNORE_FAULT_LOW = 100000UL;\n    // How long to ignore fault pin if current is higher than limit\n    static const unsigned long POWER_SAMPLE_IGNORE_FAULT_HIGH =  5000UL;\n    // How long to wait between overcurrent and turning off\n    static const unsigned long POWER_SAMPLE_IGNORE_CURRENT  =  100000UL;\n    // Upper limit for retry period\n    static const unsigned long POWER_SAMPLE_RETRY_MAX =      10000000UL;\n    \n    // Trip current for programming track, 250mA. Change only if you really\n    // need to be non-NMRA-compliant because of decoders that are not either.\n    static const int TRIP_CURRENT_PROG=250;\n    unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;\n    unsigned int power_good_counter = 0;\n    TRACK_MODE trackMode = TRACK_MODE_NONE; // we assume track not assigned at startup\n\n};\n#endif\n"
  },
  {
    "path": "MotorDrivers.h",
    "content": "/*\n *  © 2022-2023 Paul M. Antoine\n *  © 2021 Fred Decker\n *  © 2020-2024 Harald Barth\n *  (c) 2020 Chris Harlow. All rights reserved.\n *  (c) 2021 Fred Decker.  All rights reserved.\n *  (c) 2020 Harald Barth. All rights reserved.\n *  (c) 2020 Anthony W - Dayton. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef MotorDrivers_h\n#define MotorDrivers_h\n#include <Arduino.h>\n\n// *** PLEASE NOTE *** THIS FILE IS  **NOT**  INTENDED TO BE EDITED WHEN CONFIGURING A SYSTEM.\n// It will be overwritten if the library is updated.\n\n// This file contains configurations for known/supported motor shields.\n// A configuration defined by macro here can be used in your sketch.\n// A custom hardware setup will require your sketch to create MotorDriver instances\n// similar to those defined here, WITHOUT editing this file. You can put your\n// custom defines in config.h.\n\n#ifndef UNUSED_PIN     // sync define with the one in MotorDriver.h\n#define UNUSED_PIN 255 // inside uint8_t\n#endif\n\n// The MotorDriver definition is:\n//\n// MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin, byte current_pin,\n//             float senseFactor, unsigned int tripMilliamps, byte faultPin);\n//\n// power_pin:     Turns the board on/off. Often called ENABLE or PWM on the shield\n// signal_pin:    Where the DCC signal goes in. Often called DIR on the shield\n// signal_pin2:   Inverse of signal_pin. A few shields need this as well, can be replace by hardware inverter\n// brake_pin:     When tuned on, brake is set - output shortened (*)\n// current_pin:   Current sense voltage pin from shield to ADC\n// senseFactor:   Relation between volts on current_pin and actual output current\n// tripMilliamps: Short circuit trip limit in milliampere, max 32767 (32.767A)\n// faultPin:      Some shields have a pin to to report a fault condition to the uCPU. High when fault occurs\n//\n// (*) If the brake_pin is negative that means the sense\n// of the brake pin on the motor bridge is inverted\n// (HIGH == release brake)\n\n// You can have a CS wihout any possibility to do any track signal.\n// That's strange but possible.\n#define NO_SHIELD F(\"No shield at all\")\n\n// Arduino STANDARD Motor Shield, used on different architectures:\n\n#if defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)\n// Standard Motor Shield definition for 3v3 processors (other than the ESP32)\n// Setup for SAMD21 Sparkfun DEV board MUST use Arduino Motor Shield R3 (MUST be R3\n// for 3v3 compatibility!!) senseFactor for 3.3v systems is 1.95 as calculated when using\n// 10-bit A/D samples, and for 12-bit samples it's more like 0.488, but we probably need\n// to tweak both these\n#define STANDARD_MOTOR_SHIELD F(\"STANDARD_MOTOR_SHIELD\"),                                                 \\\n                              new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 0.488, 1500, UNUSED_PIN), \\\n                              new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 0.488, 1500, UNUSED_PIN)\n#define SAMD_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD\n#define STM32_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD\n\n#if defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)\n// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC\n// The Ethernet capable STM32 models cannot use Channel B BRAKE on D8, and must use the ALT pin of D6,\n// AND cannot use Channel B PWN on D11, but must use the ALT pin of D5\n#define EX8874_SHIELD F(\"EX8874\"), \\\n new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \\\n new MotorDriver( 5, 13, UNUSED_PIN, 6, A1, 1.27, 5000, A5)\n#else\n// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC\n#define EX8874_SHIELD F(\"EX8874\"), \\\n new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \\\n new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 1.27, 5000, A5)\n#endif\n\n#elif defined(ARDUINO_ARCH_ESP32)\n// STANDARD shield on an ESPDUINO-32 (ESP32 in Uno form factor). The shield must be eiter the\n// 3.3V compatible R3 version or it has to be modified to not supply more than 3.3V to the\n// analog inputs. Here we use analog inputs A2 and A3 as A0 and A1 are wired in a way so that\n// they are not useable at the same time as WiFi (what a bummer). The numbers below are the\n// actual GPIO numbers. In comments the numbers the pins have on an Uno.\n#define STANDARD_MOTOR_SHIELD F(\"STANDARD_MOTOR_SHIELD\"), \\\n new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 0.70, 1500, UNUSED_PIN), \\\n new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 0.70, 1500, UNUSED_PIN)\n\n// EX 8874 based shield connected to a 3.3V system (like ESP32) and 12bit (4096) ADC\n// numbers are GPIO numbers. comments are UNO form factor shield pin numbers\n#define EX8874_SHIELD F(\"EX8874\"),\\\n new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 1.27, 5000, 36 /*A4*/), \\\n new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 1.27, 5000, 39 /*A5*/)\n\n// EX-CSB1 with integrated motor driver definition\n#define EXCSB1 F(\"EXCSB1\"),\\\n new MotorDriver(25,  0, UNUSED_PIN, -14, 34, 2.23, 5000, 19), \\\n new MotorDriver(27, 15, UNUSED_PIN,  -2, 35, 2.23, 5000, 23)\n\n// EX-CSB1 with EX-8874 stacked on top for 4 outputs\n#define EXCSB1_WITH_EX8874 F(\"EXCSB1_WITH_EX8874\"),\\\n new MotorDriver(25,  0, UNUSED_PIN, -14, 34, 2.23, 5000, 19), \\\n new MotorDriver(27, 15, UNUSED_PIN,  -2, 35, 2.23, 5000, 23), \\\n new MotorDriver(26,  5, UNUSED_PIN,  13, 36, 1.52, 5000, 18), \\\n new MotorDriver(16,  4, UNUSED_PIN,  12, 39, 1.52, 5000, 17)\n\n#else\n// STANDARD shield on any Arduino Uno or Mega compatible with the original specification.\n#define STANDARD_MOTOR_SHIELD F(\"STANDARD_MOTOR_SHIELD\"),                                                 \\\n                              new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 2.99, 1500, UNUSED_PIN), \\\n                              new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 2.99, 1500, UNUSED_PIN)\n#define BRAKE_PWM_SWAPPED_MOTOR_SHIELD F(\"BPS_MOTOR_SHIELD\"),                                       \\\n                              new MotorDriver(-9 , 12, UNUSED_PIN, -3, A0, 2.99, 1500, UNUSED_PIN), \\\n                              new MotorDriver(-8 , 13, UNUSED_PIN,-11, A1, 2.99, 1500, UNUSED_PIN)\n\n// EX 8874 based shield connected to a 5V system (like Arduino) and 10bit (1024) ADC\n#define EX8874_SHIELD F(\"EX8874\"), \\\n new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 5.08, 5000, A4), \\\n new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 5.08, 5000, A5)\n\n#endif\n\n// Pololu Motor Shield\n#define POLOLU_MOTOR_SHIELD F(\"POLOLU_MOTOR_SHIELD\"),                                                 \\\n                            new MotorDriver( 9, 7, UNUSED_PIN,         -4, A0, 18, 3000, 12), \\\n                            new MotorDriver(10, 8, UNUSED_PIN, UNUSED_PIN, A1, 18, 3000, 12)\n//\n// Actually, on the Pololu MC33926 shield the enable lines are tied together on pin 4 and the\n// pins 9 and 10 work as \"inverted brake\" but as we turn on and off the tracks individually\n// via the power pins we above use 9 and 10 as power pins and 4 as \"inverted brake\" which in this\n// version of the code always will be high. That means this config is not usable for generating\n// a railcom cuotout in the future. For that one must wire the second ^D2 to pin 2 and define\n// the motor driver like this:\n//                          new MotorDriver(4, 7, UNUSED_PIN,  -9, A0, 18, 3000, 12)\n//                          new MotorDriver(2, 8, UNUSED_PIN, -10, A1, 18, 3000, 12)\n// See Pololu dial_mc33926_shield_schematic.pdf and truth table on page 17 of the MC33926 data sheet.\n\n// Pololu Dual TB9051FTG Motor Shield\n// This is the shield without modifications. Unfortunately the TB9051FTG driver chip on\n// the shield makes short delays when direction is switched. That means that the chip\n// can NOT provide a standard conformant DCC signal independent how hard we try. If your\n// Decoders tolerate that signal, use it by all mean but it is not recommended. Without\n// modifications it uses the following pins below which means no HA waveform and no\n// RailCom on an Arduino Mega 2560 but the DCC signal is broken anyway.\n#define POLOLU_TB9051FTG F(\"POLOLU_TB9051FTG\"),              \\\n   new MotorDriver(2, 7, UNUSED_PIN,  -9, A0, 10, 2500,  6), \\\n   new MotorDriver(4, 8, UNUSED_PIN, -10, A1, 10, 2500, 12)\n\n// Firebox Mk1\n#define FIREBOX_MK1 F(\"FIREBOX_MK1\"),                                                  \\\n                    new MotorDriver(3, 6, 7, UNUSED_PIN, A5, 9.766, 5500, UNUSED_PIN), \\\n                    new MotorDriver(4, 8, 9, UNUSED_PIN, A1, 5.00, 1000, UNUSED_PIN)\n\n// Firebox Mk1S\n#define FIREBOX_MK1S F(\"FIREBOX_MK1A\"),                                            \\\n                     new MotorDriver(24, 21, 22, 25, 23, 9.766, 5500, UNUSED_PIN), \\\n                     new MotorDriver(30, 27, 28, 31, 29, 5.00, 1000, UNUSED_PIN)\n\n// FunduMoto Motor Shield\n#define FUNDUMOTO_SHIELD F(\"FUNDUMOTO_SHIELD\"),                                              \\\n                         new MotorDriver(10, 12, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN), \\\n                         new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)\n\n// IBT_2 Motor Board for Main and Arduino Motor Shield for Prog\n#define IBT_2_WITH_ARDUINO F(\"IBT_2_WITH_ARDUINO_SHIELD\"),                                              \\\n                         new MotorDriver(4, 5, 6, UNUSED_PIN, A5, 41.54, 5000, UNUSED_PIN), \\\n                         new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)\n// YFROBOT Motor Shield (V3.1)\n#define YFROBOT_MOTOR_SHIELD F(\"YFROBOT_MOTOR_SHIELD\"), \\\n    new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN), \\\n    new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)\n\n// Makeblock ORION UNO like sized board with integrated motor driver\n// This is like an Uno with H-bridge and RJ12 contacts instead of pin rows.\n// No current sense. Barrel connector max 12V, Vmotor max 15V. 1.1A polyfuse as output protection.\n// Main is marked M1 and near RJ12 #5\n// Prog is marked M2 and near RJ12 #4\n// For details see\n// http://docs.makeblock.com/diy-platform/en/electronic-modules/main-control-boards/makeblock-orion.html\n#define ORION_UNO_INTEGRATED_SHIELD F(\"ORION_UNO_INTEGRATED_SHIELD\"),\t\t      \\\n    new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN), \\\n    new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN)\n\n// This is an example how to setup a motor shield definition for a motor shield connected\n// to an NANO EVERY board. You have to make the connectons from the shield to the board\n// as in this example or adjust the values yourself.\n#define NANOEVERY_EXAMPLE F(\"NANOEVERY_EXAMPLE\"), \\\n new MotorDriver(5,  6, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN),\\\n new MotorDriver(9, 10, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)\n\n// This is an example how to stack two standard motor shields. The upper shield\n// needs pins 3 8 9 11 12 13 A0 A1 disconnected from the lower shield and\n// jumpered instead like this:  2-3 6-8 7-9 4-13 5-11 10-12 A0-A4 A1-A5\n// Pin assigment table:\n// 2 Enable C  jumpered\n// 3 Enable A  direct\n// 4 Dir D     jumpered\n// 5 Enable D  jumpered\n// 6 Brake D   jumpered\n// 7 Brake C   jumpered\n// 8 Brake B   direct\n// 9 Brake A   direct\n// 10 Dir C    jumpered\n// 11 Enable B direct\n// 12 Dir A    direct\n// 13 Dir B    direct\n// A0 Sense A  direct\n// A1 Sense B  direct\n// A4 Sense C  jumpered\n// A5 Sense D  jumpered\n//\n#define STACKED_MOTOR_SHIELD F(\"STACKED_MOTOR_SHIELD\"),\\\n  new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 2.99, 1500, UNUSED_PIN), \\\n  new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 2.99, 1500, UNUSED_PIN), \\\n  new MotorDriver( 2, 10, UNUSED_PIN, 7, A4, 2.99, 1500, UNUSED_PIN), \\\n  new MotorDriver( 5,  4, UNUSED_PIN, 6, A5, 2.99, 1500, UNUSED_PIN)\n//\n#endif\n"
  },
  {
    "path": "Outputs.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Harald Barth\n *  © 2020-2021 Fred Decker\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n/**********************************************************************\n\nDCC++ BASE STATION supports optional OUTPUT control of any unused Arduino Pins for custom purposes.\nPins can be activited or de-activated.  The default is to set ACTIVE pins HIGH and INACTIVE pins LOW.\nHowever, this default behavior can be inverted for any pin in which case ACTIVE=LOW and INACTIVE=HIGH.\n\nDefinitions and state (ACTIVE/INACTIVE) for pins are retained in EEPROM and restored on power-up.\nThe default is to set each defined pin to active or inactive according to its restored state.\nHowever, the default behavior can be modified so that any pin can be forced to be either active or inactive\nupon power-up regardless of its previous state before power-down.\n\nTo have this sketch utilize one or more Arduino pins as custom outputs, first define/edit/delete\noutput definitions using the following variation of the \"Z\" command:\n\n  <Z ID PIN IFLAG>:            creates a new output ID, with specified PIN and IFLAG values.\n                               if output ID already exists, it is updated with specificed PIN and IFLAG.\n                               note: output state will be immediately set to ACTIVE/INACTIVE and pin will be set to HIGH/LOW\n                               according to IFLAG value specifcied (see below).\n                               returns: <O> if successful and <X> if unsuccessful (e.g. out of memory)\n\n  <Z ID>:                      deletes definition of output ID\n                               returns: <O> if successful and <X> if unsuccessful (e.g. ID does not exist)\n\n  <Z>:                         lists all defined output pins\n                               returns: <Y ID PIN IFLAG STATE> for each defined output pin or <X> if no output pins defined\n\nwhere\n\n  ID: the numeric ID (0-32767) of the output\n  PIN: the arduino pin number to use for the output\n  STATE: the state of the output (0=INACTIVE / 1=ACTIVE)\n  IFLAG: defines the operational behavior of the output based on bits 0, 1, and 2 as follows:\n\n          IFLAG, bit 0:   0 = forward operation (ACTIVE=HIGH / INACTIVE=LOW)\n                          1 = inverted operation (ACTIVE=LOW / INACTIVE=HIGH)\n\n          IFLAG, bit 1:   0 = state of pin restored on power-up to either ACTIVE or INACTIVE depending\n                              on state before power-down; state of pin set to INACTIVE when first created\n                          1 = state of pin set on power-up, or when first created, to either ACTIVE of INACTIVE\n                              depending on IFLAG, bit 2\n\n          IFLAG, bit 2:   0 = state of pin set to INACTIVE uponm power-up or when first created\n                          1 = state of pin set to ACTIVE uponm power-up or when first created\n\nOnce all outputs have been properly defined, use the <E> command to store their definitions to EEPROM.\nIf you later make edits/additions/deletions to the output definitions, you must invoke the <E> command if you want those\nnew definitions updated in the EEPROM.  You can also clear everything stored in the EEPROM by invoking the <e> command.\n\nTo change the state of outputs that have been defined use:\n\n  <Z ID STATE>:                sets output ID to either ACTIVE or INACTIVE state\n                               returns: <Y ID STATE>, or <X> if turnout ID does not exist\n\nwhere\n\n  ID: the numeric ID (0-32767) of the turnout to control\n  STATE: the state of the output (0=INACTIVE / 1=ACTIVE)\n\nWhen controlled as such, the Arduino updates and stores the direction of each output in EEPROM so\nthat it is retained even without power.  A list of the current states of each output in the form <Y ID STATE> is generated\nby this sketch whenever the <s> status command is invoked.  This provides an efficient way of initializing\nthe state of any outputs being monitored or controlled by a separate interface or GUI program.\n\n**********************************************************************/\n\n#include \"Outputs.h\"\n#ifndef DISABLE_EEPROM\n#include \"EEStore.h\"\n#endif\n#include \"StringFormatter.h\"\n#include \"IODevice.h\"\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to print all output states to stream in the form \"<Y id state>\"\n\nvoid Output::printAll(Print *stream){\n  for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)\n    StringFormatter::send(stream, F(\"<Y %d %d>\\n\"), tt->data.id, tt->data.active);\n} // Output::printAll\n\n///////////////////////////////////////////////////////////////////////////////\n// Object method to activate / deactivate the Output state.\n\nvoid  Output::activate(uint16_t s){\n  s = (s>0);  // Make 0 or 1\n  data.active = s;                     // if s>0, set status to active, else inactive\n  // set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)\n  IODevice::write(data.pin, s ^ data.invert);  \n#ifndef DISABLE_EEPROM\n  // Update EEPROM if output has been stored.    \n  if(EEStore::eeStore->data.nOutputs > 0 && num > 0)\n    EEPROM.put(num, data.oStatus);\n#endif\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to locate Output object specified by ID 'n'.\n//   Return NULL if not found.\n\nOutput* Output::get(uint16_t n){\n  Output *tt;\n  for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);\n  return(tt);\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to delete Output object specified by ID 'n'.\n//   Return false if not found.\n\nbool Output::remove(uint16_t n){\n  Output *tt,*pp=NULL;\n\n  for(tt=firstOutput;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextOutput);\n\n  if(tt==NULL) return false;\n  \n  if(tt==firstOutput)\n    firstOutput=tt->nextOutput;\n  else\n    pp->nextOutput=tt->nextOutput;\n\n  free(tt);\n\n  return true;\n  }\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to load configuration and state of all Outputs from EEPROM\n#ifndef DISABLE_EEPROM\nvoid Output::load(){\n  struct OutputData data;\n  Output *tt;\n\n  for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){\n    EEPROM.get(EEStore::pointer(),data);\n    // Create new object, set current state to default or to saved state from eeprom.\n    tt=create(data.id, data.pin, data.flags);\n    uint8_t state = data.setDefault ? data.defaultValue : data.active;\n    tt->activate(state);\n\n    if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM\n    EEStore::advance(sizeof(tt->data));\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to store configuration and state of all Outputs to EEPROM\n\nvoid Output::store(){\n  Output *tt;\n\n  tt=firstOutput;\n  EEStore::eeStore->data.nOutputs=0;\n\n  while(tt!=NULL){\n    EEPROM.put(EEStore::pointer(),tt->data);\n    tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM\n    EEStore::advance(sizeof(tt->data));\n    tt=tt->nextOutput;\n    EEStore::eeStore->data.nOutputs++;\n  }\n\n}\n#endif\n\n///////////////////////////////////////////////////////////////////////////////\n// Static function to create an Output object\n//   The obscurely named parameter 'v' is 0 if called from the load() function\n//   and 1 if called from the <Z> command processing.\n\nOutput *Output::create(uint16_t id, VPIN pin, int iFlag, int v){\n  Output *tt;\n\n  if (pin > VPIN_MAX) return NULL;\n  \n  if(firstOutput==NULL){\n    firstOutput=(Output *)calloc(1,sizeof(Output));\n    tt=firstOutput;\n  } else if((tt=get(id))==NULL){\n    tt=firstOutput;\n    while(tt->nextOutput!=NULL)\n      tt=tt->nextOutput;\n    tt->nextOutput=(Output *)calloc(1,sizeof(Output));\n    tt=tt->nextOutput;\n  }\n\n  if(tt==NULL) return tt;\n  tt->num = 0; // make sure new object doesn't get written to EEPROM until store() command\n  tt->data.id=id;\n  tt->data.pin=pin;\n  tt->data.flags=iFlag;\n\n  if(v==1){\n    // sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag\n    if (tt->data.setDefault) \n      tt->data.active = tt->data.defaultValue;\n    else\n      tt->data.active = 0;\n  }\n  IODevice::write(tt->data.pin, tt->data.active ^ tt->data.invert);\n\n  return(tt);\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nOutput *Output::firstOutput=NULL;\n"
  },
  {
    "path": "Outputs.h",
    "content": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  © 2020 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef Outputs_h\n#define Outputs_h\n\n#include <Arduino.h>\n#include \"IODevice.h\"\n\nstruct OutputData {\n  union {\n    uint8_t oStatus;      // (Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state, Bit 7=active)\n    struct {\n      unsigned int flags : 7; // Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state\n      unsigned int : 1;\n    };\n    struct {\n      unsigned int invert : 1;\n      unsigned int setDefault : 1;\n      unsigned int defaultValue : 1;\n      unsigned int: 4;\n      unsigned int active : 1;\n    };\n  };\n  uint16_t id;\n  VPIN pin; \n};\n\n\nclass Output{\npublic:\n  void activate(uint16_t s);\n  bool isActive();\n  static Output* get(uint16_t);\n  static bool remove(uint16_t);\n#ifndef DISABLE_EEPROM\n  static void load();\n  static void store();\n#endif\n  static Output *create(uint16_t, VPIN, int, int=0);\n  static Output *firstOutput;\n  struct OutputData data;\n  Output *nextOutput;\n  static void printAll(Print *);\nprivate:\n  uint16_t num;  // EEPROM address of oStatus in OutputData struct, or zero if not stored.\n  \n}; // Output\n  \n#endif\n"
  },
  {
    "path": "README.md",
    "content": "# What is DCC-EX?\nDCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your complete model railroad layout. Our easy to use, do-it-yourself, and free open source products run on off-the-shelf Arduino technology and are supported by numerous third party hardware and apps like JMRI, Engine Driver, wiThrottle, Rocrail and more. \n\nCurrently, our products include the following:\n\n* [EX-CommandStation](https://github.com/DCC-EX/CommandStation-EX/releases)\n* [EX-WebThrottle](https://github.com/DCC-EX/exWebThrottle)\n* [EX-Installer](https://github.com/DCC-EX/EX-Installer)\n* [EX-MotoShield8874](https://dcc-ex.com/reference/hardware/motorboards/ex-motor-shield-8874.html#gsc.tab=0)\n* [EX-DCCInspector](https://github.com/DCC-EX/DCCInspector-EX)\n* [EX-Toolbox](https://github.com/DCC-EX/EX-Toolbox)\n* [EX-Turntable](https://github.com/DCC-EX/EX-Turntable)\n* [EX-IOExpander](https://github.com/DCC-EX/EX-IOExpander)\n* [EX-FastClock](https://github.com/DCC-EX/EX-FastClock)\n* [DCCEXProtocol](https://github.com/DCC-EX/DCCEXProtocol)\n\nDetails of these projects can be found on [our web site](https://dcc-ex.com/).\n\n# What’s in this Repository?\n\nThis repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.\n\nTo utilize this sketch, you can use the following: \n\n1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)\n2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)\n3. (advanced) use git clone on this repository \n\nRefer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.\n\n**We seriously recommend using the EX-Installer**, however if you choose not to use the installer... \n\n* Open the file ``CommandStation-EX.ino`` in the Arduino IDE or Visual Studio Code (VSC). Please do not rename the folder containing the sketch code, nor add any files in that folder. The Arduino IDE relies on the structure and name of the folder to properly display and compile the code. \n* Rename or copy ``config.example.h`` to ``config.h``. \n* You must edit ``config.h`` according to the help texts in ``config.h``.\n\n# More information\nYou can learn more at the [DCC-EX website](https://dcc-ex.com/)\n\n"
  },
  {
    "path": "Railcom.cpp",
    "content": "/*\n *  © 2025 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n /*\n This class acts as a 3 way coordinator between\n  - the DCC waveform cutout generator,\n  - the incoming railcom collector notifications\n  - and the EXRAIL2 block enter/exit system   \n  */\n#include \"Railcom.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"EXRAIL2.h\"\n\nuint16_t Railcom::expectLoco=0;\nuint16_t Railcom::nextLoco=0;\n\nuint16_t Railcom::expectCV=0;\nunsigned long Railcom::expectWait=0;\nACK_CALLBACK Railcom::expectCallback=0;\n\nenum ResponseType: byte { \n        ENTER_BLOCK=0x00,  // loco entering block, block id and loco id follow\n        EXIT_BLOCK=0x80,   // loco exiting block, block id and loco id follow\n        CV_VALUE=0x40,     // cv value read from a POM, cv value follow\n        CV_VALUE_LIST=0xC0, // list of cv values read from a POM, cv id and 4 values follow\n    };\n  \n// anticipate is used when waiting for a CV read from a railcom loco\nvoid Railcom::anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback) { \n    expectLoco=loco;\n    expectCV=cv;\n    expectWait=millis(); // start of timeout \n    expectCallback=callback;\n}\n\n// process is called to handle data buffer sent by collector\nvoid Railcom::process(int16_t firstVpin,byte * buffer, byte length) { \n    //  block,locohi,locolow\n    //  block|0x80,data pom read cv\n    byte i=0; \n    while (i<length) {\n        byte block=buffer[i] & 0x3f;\n        (void)block; // avoid compiler warning if not using EXRAIL\n        byte type=buffer[i] & 0xc0;\n        \n        switch (type) {\n            // a type=0 record has block,locohi,locolow\n            case ENTER_BLOCK:\n            case EXIT_BLOCK:\n             {\n                #ifdef EXRAIL_ACTIVE\n                uint16_t locoid= ((uint16_t)buffer[i+1])<<8 | ((uint16_t)buffer[i+2]);\n                RMFT2::blockEvent(firstVpin+block,locoid,type==ENTER_BLOCK);\n                #endif\n                i+=3;\n            }\n            break;\n       case CV_VALUE: \n            { // loco cv and value from POM read\n              uint16_t locoid= ((uint16_t)buffer[i+1])<<8 | ((uint16_t)buffer[i+2]);\n              uint16_t cv=(buffer[i+3]<<8) | buffer[i+4];\n              byte value=buffer[i+5];\n              DIAG(F(\"POM Read loco=%d cv=%d value=%d\"),locoid,cv,value);\n              if (expectCallback) expectCallback(value);\n              expectCallback=0;\n              i+=6;\n            }\n         break;\n         default:\n          DIAG(F(\"Unknown RC Collector code 0x%x\"),type);\n          return;\n        }\n      }\n    }\n\n\n// loop() is called to detect timeouts waiting for a POM read result\nvoid Railcom::loop() {\n    if (expectCallback && (millis()-expectWait)> POM_READ_TIMEOUT) { // still waiting \n                expectCallback(-1);\n                expectCallback=0;\n    }\n}\n\nbyte Railcom::cutoutCounter=0;\nvoid Railcom::incCutout() {cutoutCounter++;};\nbyte Railcom::getCutout() {return cutoutCounter;};\n \n"
  },
  {
    "path": "Railcom.h",
    "content": "/*\n *  © 202 5Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef Railcom_h\n#define Railcom_h\n#include \"Arduino.h\"\n\ntypedef void (*ACK_CALLBACK)(int16_t result);\n\nclass Railcom {\n  public:\n    static void anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback);\n    static void process(int16_t firstVpin,byte * buffer, byte length );\n    static void loop();\n    static void incCutout();\n    static byte getCutout();\n  private:\n    static const unsigned long POM_READ_TIMEOUT=500; // as per spec\n    static uint16_t expectCV,expectLoco, nextLoco;\n    static unsigned long expectWait;\n    static ACK_CALLBACK expectCallback;\n    static byte cutoutCounter;    // cyclic cutout\n \n    static const byte MAX_WAIT_FOR_GLITCH=20; // number of dead or empty packets before assuming loco=0 \n};\n\n#endif"
  },
  {
    "path": "Release - Architecture Doc/Prod-Release-Notes.md",
    "content": "The DCC-EX Team is pleased to release CommandStation-EX-v3.0.0 as a Production Release.  This release is a major re-write of earlier versions.  We've re-architected the code-base so that it can better handle new features going forward.  \n\n**Known Bugs:**\n - **Consisting through JMRI** - currently does not work in this release.  You may use the <M> command to do this manually.\n - **Wi-Fi** - works, but can be challenging to use if you want to switch between AP mode and STA station mode.\n - **Pololu Motor Shield** - is supported with this release, but the user may have to play around with some timings to enable programming mode due to limitation in its current sensing circuitry \n \n**Summary of the key new features added to CommandStation-EX V3.0.3**\n - **<W addr> command to write loco address and clear consist** \n - **<R> command will allow for consist address**\n - **Startup commands implemented**\n \n**Summary of the key new features added to CommandStation-EX V3.0.2:**\n- **Create new output for current in mA for ``<c>`` command** - New current response outputs current in mA, overlimit current, and maximum board capable current\n- **Simultaneously update JMRI to handle new current meter**\n\n**Summary of the key new features added to CommandStation-EX V3.0.1:**\n - **Add back fix for jitter**\n - **Add Turnouts, Outputs and Sensors to ```<s>``` command output**\n\n **Summary of the key new features added to CommandStation-EX V3.0.0:**\n\n - **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.\n - **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n - **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently. \n - **Add LCD/OLED support** - OLED supported on Mega only\n - **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n - **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n - **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n - **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n - **New, simpler function command** - ```<F>``` command allows setting functions based on their number, not based on a code as in ```<f>```\n - **Function reminders** - Function reminders are sent in addition to speed reminders\n - **Functions to F28** - All NMRA functions are now supported\n - **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)\n - **Diagnostic ```<D>``` commands** - See documentation for a full list of new diagnostic commands\n - **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n - **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n - **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n - **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n - **Fix EEPROM bugs**\n - **Number of locos discovery command** - ```<#>``` command \n - **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.\n - **Automatic slot managment** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. ```<!>``` command added to release locos from memory.\n\n\n**Key Contributors**\n\n**Project Lead**\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n\n**CommandStation-EX Developers**\n- Chris Harlow - Bournemouth, UK (UKBloke)\n- Harald Barth - Stockholm, Sweden (Haba)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- M Steve Todd - - Engine Driver and JMRI Interface\n- Scott Catalanno - Pennsylvania\n- Gregor Baues - Île-de-France, France (grbba)\n\n**exInstaller Software**\n- Anthony W - Dayton, Ohio, USA (Dex, Dex++)\n\n**Website and Documentation**\n- Mani Kumar - Bangalor, India (Mani / Mani Kumar)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- Roger Beschizza - Dorset, UK (Roger Beschizza)\n- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)\n- Kevin Smith - (KCSmith)\n\n**WebThrotle-EX**\n- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)\n- Mani Kumar - Bangalor, India (Mani /Mani Kumar)\n- Matt H - \n\n\n\n**Beta Testing / Release Management / Support**\n- Larry Dribin\t- Release Management\n- Keith Ledbetter\t\n- BradVan der Elst\t\n- Andrew Pye\t\n- Mike Bowers\t\n- Randy McKenzie\n- Roberto Bravin\n- Sim Brigden\n- Alan Lautenslager\n- Martin Bafver\t\n- Mário André Silva\t\n- Anthony Kochevar\t\n- Gajanatha Kobbekaduwe\t\n- Sumner Patterson \n- Paul - Virginia, USA\n"
  },
  {
    "path": "Release - Architecture Doc/Rough Release-Notes.md",
    "content": "# CommandStation-EX Release Notes\n\n## v3.0.0\n\n - **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n - **Withrottle Integrations** - Act as a host for four WiThrottle clients concurrently. \n - **Add LCD/OLED support** - OLED supported on Mega only\n - **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n - **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n - **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n - **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n - **New, simpler function command** - ```<F>``` command allows setting functions based on their number, not based on a code as in ```<f>```\n - **Function reminders** - Function reminders are sent in addition to speed reminders\n - **Functions to F28** - All NMRA functions are now supported\n - **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them\n - **Diagnostic ```<D>``` commands** - See documentation for a full list of new diagnostic commands\n - **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n - **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n - **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n - **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n - **Fix EEPROM bugs**\n - **Support for more decoders** - Support for 20 (Uno) or 50 (Mega) mobile decoders, number automaticlaly recognized by JMRI.\n - **Automatic slot managment** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. ```<!>``` command added to release locos from memory.\n"
  },
  {
    "path": "Release_Notes/CommandRef.md",
    "content": "This file is being used to consolidate the command reference information. \n\nGeneral points:\n  - Commands below have a single character opcode and parameters.\n  Even <JA> is actually read as <J A> \n  - Keyword parameters are shown in upper case but may be entered in mixed case.\n  - value parameters are decimal numeric (unless otherwise noted)\n  - [something]  indicates its optional.\n  - Not all commands have a response, and broadcasts mean that not all responses come from the last commands that you have issued.\n   \nStartup status\n<s> Return status like\n    <iDCC-EX V-4.2.22 / MEGA / STANDARD_MOTOR_SHIELD G-devel-202302281422Z>\n    also returns defined turnout list:\n    <H id 1|0>   1=thrown\n\nTrack power management. After power commands a power state is broadcast to all throttles.\n\n<1>                Power on all\n<1 MAIN|PROG|JOIN> Power on MAIN or PROG track\n<1 JOIN>           Power on MAIN and PROG track but send main track data on both.\n<0>                Power off all tracks \n<0 MAIN|PROG>      Power off main or prog track\n\nBasic manual loco control\n<t locoid speed direction>  Throttle loco. \n    speed in JMRI-form  (-1=ESTOP, 0=STOP, 1..126 = DCC speeds 2..127)\n    direction 1=forward, 0=reverse\n    For response see broadcast <l>\n\n<F locoid function 1|0>     Set loco function 1=ON, 0-OFF\n    For response see broadcast <l>\n\n<!>  emergency stop all locos\n<T id 0|1|T|C>  Control turnout id, 0=C=Closed, 1=T=Thrown\n       response broadcast <H id 0|1>\n\n\nDCC accessory control\n<a address subaddress activate [onoff]>\n<a linearaddress activate> \n\n\nTurnout definition\nNote: Turnouts are best defined in myAutomation.h where a turnout description can also be provided ( refer to EXRAIL documentation) or by using these commands in a mySetup.h file. \n\n<T id SERVO vpin thrown closed profile>\n<T id VPIN vpin>\n<T id DCC addr subaddr>\n<T id DCC linearaddr>\n   Valid commands respond with <O>\n\nDirect pin manipulation (replaces <Z commands, no predefinition required)\n<z vpin>     Set pin HIGH\n<z -vpin>    Set pin LOW\n<z vpin value> Set pin analog value\n<z vpin value profile> Set pin analog with profile\n<z vpin value profile duration> set pin analog with profile and value\n\n\nSensors (Used by JMRI, not required by EXRAIL)\n<S id vpin pullup> define a sensor to be monitored.\n   Responses <Q id> and <q id> as sensor changes  \n\nDecoder programming - main track\n<w cab cv value> POM write value to cv on loco\n<b cab cv bit value> POM write bit to cv on loco\n\nDecoder Programming - prog track\n<W cabid>  Clear consist and write new cab id (includes long/short settings)\n           Responds <W cabid> or <W -1> for error\n<W cv value> Write value to cv\n\n<V cv predictedValue> Read cv value, much faster if prediction is correct. \n<V cv bit predictedValue>      Read CV bit\n\n<R>         Read drive-away loco id. (May be a consist id)\n<D ACK ON|OFF>\n<D ACK LIMIT|MIN|MAX|RETRY value>\n<D PROGBOOST>\n\nAdvanced DCC control\n<M  packet.... >\n<P  packet ...>\n<f map1 map2 [map3]>\n<#>\n<->\n<- cabid>\n<D CABS>\n<D SPEED28>\n<D SPEED128>\n\n\nEEPROM commands\nThese commands exist for\nbackwards JMRI compatibility. \nYou are strongly discouraged from maintaining your configuration settings in EEPROM.    \n<E>\n<e>\n<D EEPROM>\n<T>\n<T id>\n<S>\n<S id>\n<Z>\n<Z id>\n\nDiagnostic commands\n<D CMD ON|OFF>\n<D WIFI ON|OFF>\n<D ETHERNET ON|OFF>\n<D WIT ON|OFF>\n<D LCN ON|OFF>\n<D EXRAIL ON|OFF>\n<D RESET>\n<D SERVO|ANOUT vpin position [profile]>\n<D ANIN vpin>\n<D HAL SHOW>\n<D HAL RESET>\n<+ cmd>\n<+>\n<Q>\n\nUser defined filter commands\n<U ....>\n<u ....>\n\nTrack Management\n<=>\n<= track DCC|PROG|OFF>\n<= track DC|DCX cabid>\n<JG>\n<JI>\n\n\nTurntable interface\n<D TT vpin steps [activity]>\n\nFast clock interface\n<JC>\n<JC mins rate>\n\n\nAdvanced Throttle access to features\n<t cab>\n<JA>\n<JA id>\n<JR>\n<JR id>\n<JT>\n<JT id>\n\n*******************\nEXRAIL Commands\n*******************\n\n</>\n</PAUSE>\n</RESUME>\n</START cab sequence>\n</START sequence>\n</KILL taskid>\n</KILL ALL>\n</RESERVE|FREE blockid>\n</LATCH|UNLATCH latchid>\n</RED|AMBER|GREEN signalid>\n\nObsolete commands/formats\n<c>\n<t ignored cab speed direction> \n<T id vpin thrown closed>\n<T id addr subaddr>\n<B cv bit value obsolete obsolete>\n<R cv obsolete obsolete>\n<W cv value obsolete obsolete>\n<R cv>            V command is much faster if prediction is correct.\n<B cv bit value>  V command is much faster if prediction is correct.\n<Z id vpin active> (use <z)  Define an output pin that JMRI can set by id \n<Z id activate>    (use <z)  Activate an output pin by id\n\n\nBroadcast responses\nNote: broadcasts are sent to all throttles when appropriate (usually because something has changed)\n\n<p0>\n<p1>\n<p1 MAIN|PROG|JOIN>\n\n<l cab slot dccspeed functionmap>\n<H id 1|0>\n<jC mmmm speed>\n\nDiagnostic responses\nThese are not meant to be software readable. They contain diagnostic information for programmers to identify issues.\n<X> \n<*  ... *>\n\n"
  },
  {
    "path": "Release_Notes/Exrail mods.txt",
    "content": "// 5.2.49 \n\nWhich is a more efficient than the AT/AFTER/IF methods \nof handling buttons and switches, especially on MIMIC panels. \n\nONBUTTON(vpin) \n  handles debounce and starts a task if a button is used to \n  short a pin to ground. \n\n  for example:\n     ONBUTTON(30) TOGGLE_TURNOUT(30) DONE\n\nONSENSOR(vpin) \n   handles debounce and starts a task if the pin changes.\n   You may want to check the pin state with an IF ...\n\nNote the ONBUTTON and ONSENSOR are not generally useful \nfor track sensors and running trains, because you dont know which\ntrain triggered the sensor.  \n\n// 5.2.47\n\nBLINK(vpin, onMs,offMs)\n\nwhich will start a vpin blinking until such time as it is SET, RESET or set by a signal operation such as RED, AMBER, GREEN. \n\nBLINK returns immediately, the blinking is autonomous. \n\nThis means a signal that always blinks amber could be done like this:\n\nSIGNAL(30,31,32)\nONAMBER(30) BLINK(31,500,500) DONE\n\nThe RED or GREEN calls will turn off the amber blink automatically.\n\nAlternatively a signal that has normal AMBER and flashing AMBER could be like this:\n\n#define FLASHAMBER(signal) \\\n                AMBER(signal) \\\n                BLINK(signal+1,500,500)\n  \n  (Caution: this assumes that the amber pin is redpin+1)\n\n  ==\n\n  FTOGGLE(function)\n   Toggles the current loco function (see FON and FOFF)\n\n  XFTOGGLE(loco,function)\n     Toggles the function on given loco. (See XFON, XFOFF)\n\n  TOGGLE_TURNOUT(id) \n      Toggles the turnout (see CLOSE, THROW)\n\n  STEALTH_GLOBAL(code) \n      ADVANCED C++ users only.\n      Inserts code such as static variables and functions that\n      may be utilised by multiple STEALTH operations.\n\n\n// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories.\nThis command sends an extended accessory packet to the track, Normally used to set\na signal aspect. Aspect numbers are undefined as sdtandards except for 0 which is\nalways considered a stop.\n\n//        - Exrail ASPECT(address,aspect) for above.\n     The ASPECT command sents an aspect to a DCC accessory using the same logic as \n     <A aspect address>.\n     \n//        - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)\n      This defines a signal (with id same as dcc address) that can be operated\n      by the RED/AMBER/GREEN commands.   In each case the command uses the signal\n      address to refer to the signal and the aspect chosen depends on the use of the RED\n      AMBER or GREEN command sent. Other aspects may be sent but will require the\n      direct use of the ASPECT command.\n      The IFRED/IFAMBER/IFGREEN  and ONRED/ONAMBER/ONGREEN commands contunue to operate\n      as for any other signal type. It is important to be aware that use of the ASPECT\n      or <A> commands will correctly set the IF flags and call the ON handlers if ASPECT\n      is used to set one of the three aspects defined in the DCCX_SIGNAL command. \n      Direct use of other aspects does not affect the signal flags.\n      ASPECT and <A> can be used withput defining any signal if tyhe flag management or\n      ON event handlers are not required.    \n\n// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)\n   This macro offsers a more convenient way of performing the HAL call in halSetup.h\n    In halSetup.h --- IODevice::configureServo(101,300,400,PCA9685::slow);\n    In myAutomation.h --- CONFIGURE_SERVO(101,300,400,slow)\n\n// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)\n    This cutout will only work on a Mega2560 with a single EX8874 motor shield\n    configured in the normal way with the main track brake pin on pin 9.\n    <C RAILCOM ON>    Turns on the cutout mechanism.\n    <C RAILCOM OFF>   Tirns off the cutout. (This is the default)\n    <C RAILCOM DEBUG> ONLY to be used by developers used for waveform diagnostics.\n       (In DEBUG mode the main track idle packets are replaced with reset packets, This\n       makes it far easier to see the preambles and cutouts on a logic analyser or scope.)\n\n// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.\n       This Macro causes the creation of JMRI <S> type sensors in a way that is \n       simpler than repeating lines of <S> commands.\n         JMRI_SENSOR(100)   is equenvelant to <S 100 100 1>\n         JMRI_SENSOR(100,16) will create <S> type sensors for vpins 100-115.\n\n// 5.2.26 - Silently ignore overridden HAL defaults\n//        - include HAL_IGNORE_DEFAULTS macro in EXRAIL\n            The HAL_IGNORE_DEFAULTS command, anywhere in myAutomation.h will \n            prevent the startup code from trying the default I2C sensors/servos.  \n// 5.2.24 - Exrail macro asserts to catch \n//            : duplicate/missing automation/route/sequence/call ids\n//            : latches and reserves out of range\n//            : speeds out of range\n            Causes compiler time messages for EXRAIL issues that would normally\n            only be discovered by things going wrong at run time. \n// 5.2.13 - EXRAIL STEALTH\n            Permits a certain level of C++ code to be embedded as a single step in\n            an exrail sequence. Serious engineers only.\n\n// 5.2.9  - EXRAIL STASH feature \n//        - Added ROUTE_DISABLED macro in EXRAIL\n"
  },
  {
    "path": "Release_Notes/IO_Bitmap.md",
    "content": "Virtual Bitmap device pins.\n\na Bitmap device pin is a software representation of a virtual hardware device that has the ability to store a 16bit value.\n\nThis this is easier to manage than LATCH in EXRAIL as they can be explicitely set and tested without interfering with underlying hardware or breaching the 255 limit.\n\nVirtual pins may be set, reset and tested in the same way as any other pin. Unlike sensors and leds, these device pins are both INPUT and OUTPUT  These can be used in many ways:\n\n  As a simple digital flag to assist in inter-thread communication.\n  A flag or value that can be set from commands and tested in EXRAIL.(e.g. to stop a sequence)\n  As a counter for looping or occupancy counts such as trains passing over a multi track road crossing.\n  As a collection of 16 digital bits that can be set, reset, toggled, masked and tested.\n  \n  Existing <> and exrail commands for vpins work on these pins.  \n\n  Virtual pin creation:\n    HAL(Bitmap,firstpin,npins) \n       creates 1 or more virtual pins in software. (RAM requirement approximately 2 bytes per pin)\n       e.g. HAL(Bitmap,1000,20)  creates pins 1000..1019\n\n  Simple use as flags:\n    This uses the traditional digital pin commands\n       SET(1013) RESET(1013)  sets value 1 or 0\n       SET(1000,20) RESET(1000,20)  sets/resets a range of pins  \n       IF(1000) tests if pin value!=0\n\n       Commands can set 1/0 values using <z 1010> <z -1010> as for any digital output.\n       BLINK can be used to set them on/off on a time pattern. \n\n       In addition, Exrail sensor comands work as if these pins were sensors\n          ONBUTTON(1013) triggers when value changes from 0 to something.\n          ONSENSOR(1013) triggers when value changes to or from 0.\n          <S 1013 1013 1> and JMRI_SENSOR(1013) report <Q/q responses when changing to or from 0.\n\n    Use as analog values:\n          Analog values may be set into the virtual pins and tested using the existing analog value commands and exrail macros.\n          <z vpin value>  <D ANIN vpin> etc.\n\n    Use as counters:\n        For loop counting, counters can be incremented by BITMAP_INC(1013) and decremented by BITMAP_DEC(1013) and tested with IF/IFNOT/IFGTE etc.\n        Counters be used to automate a multi track crossing where each train entering increments the counter and decrements it on clearing the crossing. Crossing gate automation can be started when the value changes from 0, and be stopped when the counter returns to 0.  Detecting the first increment from 0 to 1 can be done with ONBUTTON(1013) and the automation can use IF(1013) or IFNOT(1013) to detect when it needs to reopen the road gates. \n\nUse as binary flag groups: \n    Virtual pins (and others that respond to an analog read in order to provide bitmapped digital data, such as SensorCam) can be set and tested with new special EXRAIL commands\n\n    IFBITMAP_ALL(vpin,mask)   Bitwise ANDs the  the vpin value with the mask value and is true if ALL the 1 bits in the mask are also 1 bits in the value. \n    e.g.    IFBITMAP_ALL(1013,0x0f)  would be true if ALL the last 4 bits of the value are 1s.\n \n    IFBITMAP_ANY(1013,0x0f) would be true if ANY of the last 4 bits are 1s.\n\n\n     Modifying bitmap values:\n      BITMAP_SET(voin,value) sets the value\n      BITMAP_AND(vpin,mask) performs a bitwise AND operation.\n      BITMAP_OR(vpin,mask)  performa a bitwise OR operation\n      BITMAP_XOR(vpin,mask) performs a bitwise EXCLUSIVE OR (which is basically a toggle)   \n\n\n                      "
  },
  {
    "path": "Release_Notes/NeoPixel.md",
    "content": "NeoPixel support\n\nThe IO_NeoPixel.h driver supports the adafruit neopixel seesaw board. It turns each pixel into an individual VPIN which can be given a colour and turned on or off using the new <o> command or the NEOPIXEL Exrail macro. Exrail SIGNALS can also drive a single pixel signal or multiple separate pixels.\n\n\n1.  Defining the hardware driver:\n    Add a driver definition in myAutomation.h for each adafruit I2C driver.\n\n    HAL(neoPixel, firstVpin, numberOfPixels [, mode [, i2caddress])\n      Where mode is selected from the various pixel string types which have varying\n      colour order or refresh frequency. For MOST strings this mode will be NEO_GRB but for others refer to the comments in IO_NeoPixel.h\n      If omitted the node and i2caddress default to NEO_GRB, 0x60.\n\n    HAL(NeoPixel,1000,20)\n       This is a NeoPixel driver defaulting to I2C aqddress 0x60 for a GRB pixel string. Pixels are given vpin numbers from 1000 to 1019. \n    HAL(NeoPixel,1020,20,NEO_GRB,0x61)\n       This is a NeoPixel driver on i2c address 0x61    \n\n2. Setting pixels from the < > commands.\n    By default, each pixel in the string is created as white but switched off.\n    Each pixel has a vpin starting from the first vpin in the HAL definitions.\n\n    <o vpin>   switches pixel on  (same as <z vpin>) e.g. <o 1005>\n    <o -vpin> switches pixel off (same as <z -vpin>) e.g. <o -1003>\n    (the z commands work on pixels the same as other gpio pins.)\n\n    <o [-]vpin count> switches on/off count pixels starting at vpin. e.g <o 1000 5>\n    Note: it IS acceptable to switch across 2 strings of pixels if they are contiguous vpin ranges.  It is also interesting that this command doesnt care if the vpins are NeoPixel or any other type, so it can be used to switch a range of other pin types. \n\n    <o [-]vpin red green blue [count]> sets the colour and on/off status of a pin or pins. Each colour is 0..255 e.g. <o 1005 255 255 0>  sets pin 1005 to bright yellow and ON, <0 -1006 0 0 255 10>  sets pins 1006 to 1015 (10 pins) to bright blue but OFF.     \n    Note: If you set a pin to a colour, you can turn it on and off without having to reset the colour every time. This is something the adafruit seesaw library can't do and is just one of several reasons why we dont use it.  \n\n3. Setting pixels from EXRAIL\n    The new NEOPIXEL macro provides the same functionality as the    <o [-]vpin red green blue [count]> command above. \n    NEOPIXEL([-]vpin, red, green, blue [,count])\n    \n    Setting pixels on or off (without colour change) can be done with SET/RESET [currently there is no set range facility but that may be added as a general exrail thing... watch this space]\n\n    Because the pixels obey set/reset, the BLINK command can also be used to control blinking a pixel.\n\n4. EXRAIL pixel signals.\n    There are two types possible, a mast with separate fixed colour pixels for each aspect, or a mast with one multiple colour pixel for all aspects.\n\n    For separate pixels, the colours should be established at startup and a normal SIGNALH macro used.\n    \n  AUTOSTART \n    SIGNALH(1010,1011,1012)\n    NEOPIXEL(1010,255,0,0)       \n    NEOPIXEL(1011,128,128,0)\n    NEOPIXEL(1012,0,255,0)\n    RED(1010)  // force signal state otherwise all 3 lights will be on\n    DONE \n\n    For signals with 1 pixel, the NEOPIXEL_SIGNAL macro will create a signal \n    NEOPIXEL_SIGNAL(vpin,redfx,amberfx,greenfx)\n\n    ** Changed... ****\n    The fx values above can be created by the NeoRGB macro so a bright red would be NeoRGB(255,0,0)  bright green NeoRGB(0,255,0) and amber something like NeoRGB(255,100,0)\n    NeoRGB creates a single int32_t value so it can be used in several ways as convenient.\n\n// create 1-lamp signal with NeoRGB colours\nNEOPIXEL_SIGNAL(1000,NeoRGB(255,0,0),NeoRGB(255,100,0),NeoRGB(0,255,0))\n\n// Create 1-lamp signal with named colours.\n// This is better if you have multiple signals.\n// (Note: ALIAS is not suitable due to word length defaults) \n#define REDLAMP NeoRGB(255,0,0)\n#define AMBERLAMP NeoRGB(255,100,0)\n#define GREENLAMP NeoRGB(0,255,0)\nNEOPIXEL_SIGNAL(1001,REDLAMP,AMBERLAMP,GREENLAMP)\n\n// Create 1-lamp signal with web type RGB colours \n// (Using blue for the amber  signal , just testing) \nNEOPIXEL_SIGNAL(1002,0xFF0000,0x0000FF,0x00FF00)\n\n      \n  "
  },
  {
    "path": "Release_Notes/Railcom.md",
    "content": "Railcom implementation notes, Chris Harlow Oct 2024\n\nRailcom support is in 3 parts\n1. Generation of the DCC waveform with a Railcom cutout.\n2. Accessing the railcom feedback from a loco using hardware detectors\n3. Utilising the feedback to do something useful.\n\nDCC Waveform Railcom cutout depends on using suitable motor shields (EX8874 primarily) as the standard Arduino shield is not suitable. (Too high resistance during cutout)\nThe choice of track management also depends on wiring all the MAIN tracks to use the same signal and brake pins. This allows separate track power management but prevents switching a single track from MAIN to PROG or DC... \nSome CPUs require very specific choice of brake pins etc to match their internal timer register architecture.\n\n- MEGA.. The default shield setting for an EX8874 is suitable for Railcom on Channel A (MAIN) \n- ESP32 .. not yet supported.\n- Nucleo ... TBA \n\nEnabling the Railcom Cutout requires a `<C RAILCOM ON>` command. This can be added to myAutomation using `PARSE(\"<C RAILCOM ON>\")`\nCode to calculate the cutout position and provide synchronization for the sampling is in `DCCWaveform.cpp` (not ESP32)\nand in general a global search for \"railcom\" will show all code changes that have been made to support this.\n\nCode to actually implement the timing of the cutout is highly cpu dependent and can be found in the various implementations of `DCCTimer.h`. At this time only `DCCTimerAVR.cpp`has implemented this.  \n\n\nReading Railcom data:\n  A new HAL handler (`IO_I2CRailcom.h`) has been added to process input from a 32-block railcom collecter which operates over I2C. The collector and its readers  sit between the CS and the track and collect railcom data from locos during the cutout.\n  The Collector device removes 99.9% of the railcom traffic and returns just a summary of what has changed since the last cutout.\n  After the cutout the HAL driver reads the Collector summary over I2C and passes the raw data to the CS logic (`Railcom.cpp`) for analysis.\n\n  Each 32-block reader is described in myAutomation like `HAL(I2CRailcom,10000,32,0x08)`  which will assign 32 blocks on i2c address 0x08 with vpin numbers 10000 and 10031. If you only use fewer channel in the collector, you can assign fewer pins here.\n  (Implementation notes.. you may have multiple collectors, each one will requite a HAL line to define its i2c address and vpins to represent block numbers.)\n\nMaking use of Railcom data\n\n Exrail has two additional event handlers which can capture locos entering and exiting blocks. These handlers are started with the loco information already set, so for example:\n ```\n ONBLOCKENTER(10000) \n    // a loco has entered block 10000 \n    FON(0)  // turn the light on\n    FON(1)  // make a lot of noise\n    SPEED(20) // slow down\n    DONE \n \n ONBLOCKEXIT(10000) \n    // a loco has left block 10000 \n    FOFF(0)  // turn the light off\n    FOFF(1)  // stop the noise\n    SPEED(50) // speed up again\n    DONE \n    ```\n\n    Note that the Railcom interpretation code is capable of detecting multiple locos in the same block at the same time and will create separate exrail tasks for each one.\n     There is however one minor loophole in the block exit logic...\n      If THREE or more locos are in the same block and ONE of them leaves, then ONBLOCKEXIT will not fire until \n      EITHER - The leaving loco enters another railcom block\n      OR     - only ONE loco remains in the block just left.\n\n      To further support block management in railcom, two additional serial commands are available\n\n      `<K block loco >` to simulate a loco entering a block, and trigger any ONBLOCKENTER\n      `<k block loco >` to simulate a loco leaving a block, and trigger and ONBLOCKEXIT\n\n\n   Reading CV values on MAIN.\n\n      Railcom allows for the facility to read loco cv values while on the main track. This is considerably faster than PROG track access but depends on the loco being in a Railcom monitored block. \n\n      To read from PROG Track we use `<R cv>` response is `<r value>` \n\n      To read from MAIN track use `<r loco cv>`\n        response is `<r loco cv value>`  \n\n     "
  },
  {
    "path": "Release_Notes/Stash.md",
    "content": "# The STASH feature of exrail.\n\nSTASH is used for scenarios where it is helpful to relate a loco id to where it is parked. For example a fiddle yard may have 10 tracks and it's much easier for the operator to select a train to depart by using the track number, or pressing a button relating to that track, rather than by knowing the loco id which may be difficult to see.\n\nAutomated yard parking can use the stash to determine which tracks are empty without the need for block occupancy detectors.\n\nNote that a negative locoid may be stashed to indicate that the loco will operate with inverted direction. For example a loco facing backwards, with the INVERT_DRECTION state may be stashed by exrail and the invert state will be restored along with the loco id when using the PICKUP_STASH.  CLEAR_ANY_STASH will clear all references to the loco regardless of direction. \n\nThe following Stash  commands are available:\n | EXRAIL command | Serial protocol | function |\n | -------------- | --------------- | -------- | \n | STASH(s) | `<JM s locoid>` | Save the current loco id in the stash array element s. |\n | CLEAR_STASH(s) | `<JM s 0>` | Sets stash array element s to zero. |  \n | CLEAR_ALL_STASH | `<JM CLEAR ALL>` | sets all stash entries to zero |\n | CLEAR_ANY_STASH | `<JM CLEAR ANY locoid>` | removes current loco from all stash elements | \n | PICKUP_STASH(s) | N/A | sets current loco to stash element s |\n | IFSTASH(s)  | N/A | True if stash element s is not zero |\n | N/A | `<JM>`  | query all stashes (returns `<jM s loco>` where loco is not zero)\n | N/A | `<JM stash>` | Query loco in stash (returns `<jM s loco>`)  \n\n\n"
  },
  {
    "path": "Release_Notes/TCA8418.md",
    "content": "## TCA8418 ##\n\nThe TCA8418 IC from Texas Instruments is a low cost and very capable GPIO and keyboard scanner. Used as a keyboard scanner, it has 8 rows of 10 columns of IO pins which allow encoding of up to 80 buttons. The IC is available on an Adafruit board with Qwiic I2C interconnect called the \"Adafruit TCA8418 Keypad Matrix and GPIO Expander Breakout\" and available here for the modest sum of $US6 or so: https://www.adafruit.com/product/4918\n\nThe great advantage of this IC is that the keyboard scanning is done continuously, and it has a 10-element event queue, so even if you don't get to the interrupt immediately, keypress and release events will be held for you. Since it's I2C its very easy to use with any DCC-EX command station.\n\nThe TCA8418 driver presently configures the IC in the full 8x10 keyboard scanning mode, and then maps each key down/key up event to the state of a single vpin for extremely easy use from within EX-RAIL and JMRI as each key looks like an individual sensor.\n\nThis is ideal for mimic panels where you may need a lot of buttons, but with this board you can use just 18 wires to handle as many as 80 buttons.\n\nBy adding a simple HAL statement to myAutomation.h it creates between 1 and 80 buttons it will report back.\n\n`HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)`\n\nFor example:\n\n`HAL(TCA8418, 300, 80, 0x34)`\n\nCreates VPINs 300-379 which you can monitor with EX-RAIL, JMRI sensors etc.\n\nWith an 8x10 key event matrix, the events are numbered using the Rn row pins and Cn column pins as such:\n\n     C0  C1  C2  C3  C4  C5  C6  C7  C8  C9\n    ========================================\n R0|  0   1   2   3   4   5   6   7   8   9\n R1| 10  11  12  13  14  15  16  17  18  19\n R2| 20  21  22  23  24  25  26  27  28  29\n R3| 30  31  32  33  34  35  36  37  38  39\n R4| 40  41  42  43  44  45  46  47  48  49\n R5| 50  51  52  53  54  55  56  57  58  59\n R6| 60  61  62  63  64  65  66  67  68  69\n R7| 70  71  72  73  74  75  76  77  78  79\n\nSo if you start with the first pin definition being VPIN 300, R0/C0 will be 300 + 0, and R7/C9 will be 300+79 or 379.\n\nUse something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:\n\n`HAL(TCA8418, 400, 80, {SubBus_1, 0x34})`\n\nAnd if needing an Interrupt pin to speed up operations:\n`HAL(TCA8418, 300, 80, 0x34, 21)`\n\nNote that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100), but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected. Use any available Arduino pin for interrupt monitoring.\n\n"
  },
  {
    "path": "Release_Notes/TM1638.md",
    "content": "## TM1638 ##\n\nThe TM1638 board provides a very cheap way of implementing 8 buttons, 8 leds and an 8 digit 7segment display in a package requiring just 5 Dupont wires (vcc, gnd + 3 GPIO pins) from the command station without soldering.\n\n\nThis is ideal for prototyping and testing, simulating sensors and signals, displaying states etc. For a built layout, this could provide a control for things that are not particularly suited to throttle 'route' buttons, perhaps lineside automations or fiddle yard lane selection.  \n\nBy adding a simple HAL statement to myAutomation.h it creates 8 buttons/sensors and 8 leds.\n\n`HAL(TM1638,500,29,31,33)`\nCreates VPINs 500-507 And desscribes the GPIO pins used to connect the clk,dio,stb pins on the TM1638 board. \n\nSetting each of the VPINs will control the associated LED (using for example SET, RESET or BLINK in Exrail or `<z 500> <z -501>  from a command).\n\nUnlike most pins, you can also read the same pin number and get the button state, using Exrail IF/AT/ONBUTTON etc.\n\nFor example:\n`\nHAL(TM1638,500,29,31,33)\n`\nAll the folowing examples assume you are using VPIN 500 as the first, leftmost,  led/button on the TM1638 board.\n\n \n`ONBUTTON(500) \n  SET(500)     // light the first led\n  BLINK(501,500,500) // blink the second led\n  SETLOCO(3) FWD(50) // set a loco going   \n  AT(501) STOP // press second button to stop\n  RESET(500) RESET(501)  // turn leds off\n  DONE\n`\n\nButtons behave like any other sensor, so using `<S 500 500 1>`  will cause the command station to issue `<Q 500>` and `<q 500>` messages when the first button is pressed or released.\n\nExrail `JMRI_SENSOR(500,8)` will create `<S` commands for all 8 buttons.  \n\n## Using the 7 Segment display ##\n\nThe 8 digit display can be treated as 8 separate digits (left most being the same VPIN as the leftmost button and led) or be written to in sections of any length. Writing uses the existing analogue interface to the common HAL but is awkward to use directly.  To make this easier from Exrail, a SEG7 macro provides a remapping to the ANOUT facility that makes more sense. \n\nSEG7(vpin,value,format)\n\nThe vpin determins which digit to start writing at. \nThe value can be a 32bit unsigned integer but is interpreted differentlky according to the format.\n\nFormat values:\n 1..8  give the length (number of display digits) to fill, and defaults to decimal number with leading zeros.\n\n 1X..8X give the length but display in hex.\n\n 1R..4R treats each byte of the value as raw 7-segment patterns so that it can write letters and symbols using any compination of the 7segments and deciml point.\n\n There is a useful description here:\n https://jetpackacademy.com/wp-content/uploads/2018/06/TM1638_cheat_sheet_download.pdf\n\n\n e.g.  SEG7(500,3,4)\n        writes 0003 to first 4 digits of the display\n       SEG7(504,0xcafe,4X)\n        writes CAFE to the last 4 digits\n       SEG7(500,0xdeadbeef,8X) \n        writes dEAdbEEF to all 8 digits. \n\n    Writing raw segment patters requires knowledge of the bit pattern to segment relationship:\n`      0\n    == 0 ==\n   5|     | 1\n    == 6 ==\n  4 |     | 2\n    == 3 ==  \n            7=decimal point\n\n            Thus Letter A is segments  6 5 4 2 1 0, in bits  that is (0 bit on right)          \n               0b01110111  or 0x77\n    This is not easy to do my hand and thus a new string type suffix has been introduced to make simple text messages. Note that the HAL interface only has width for 32 bits which is only 4 symbols so writing 8 digits requires two calls.\n\n    e.g.  SEG7(500,\"Hell\"_s7,4R) SEG7(504,\"o\"_s7,4R)\n          DELAY(1000)\n          SEG7(500,\"Worl\"_s7,4R) SEG7(504,\"d\"_s7,4R)\n\n    Note that some letters like k,m,v,x do not have particularly readable 7-segment representations. \n                     \n    Credit to https://github.com/dvarrel/TM1638 for the basic formulae.\n\n"
  },
  {
    "path": "Release_Notes/ThrottleAssists.md",
    "content": "Throttle Assist updates for versiuon 4.?\n\nChris Harlow April 2022\n\nThere are a number of additional throttle information commands that have been implemented to assist throttle authors to obtain information from the Command Station in order to implement turnout, route/automation and roster features which are already found in the Withrottle implementations. \nThese commands are new and not overlapped with the existing commands which are probabaly due to be obsoleted as they are over complex and unfit for purpose. \n\nTurnouts:\n\nThe conventional turnout definition commands and the ```<H>``` responses do not contain information about the turnout description which may have been provided in an EXRAIL script. A turnout description is much more user friendly than T123 and having a list helps the throttle UI build a suitable set of buttons.\n\n```<JT>``` command returns a list of turnout ids. The throttle should be uninterested in the turnout technology used but needs to know the ids it can throw/close and monitor the current state. \ne.g.  response ```<jT 1 17 22 19>``` \n\n```<JT 17>`` requests info on turnout 17.\ne.g. response ```<jT 17 T \"Coal yard exit\">``` or ```<jT 17 C \"Coal yard exit\">```\n(T=thrown, C=closed)\nor ```<jT 17 C \"\">``` indicating turnout description not given. \nor ```<jT 17 X>``` indicating turnout unknown (or possibly hidden.) \n\nNote: It is still the throttles responsibility to monitor the status broadcasts.\n (TBD I'm thinking that the existing broadcast is messy and needs cleaning up)\n However, I'm not keen on dynamically created/deleted turnouts so I have no intention of providing a command that indicates the turnout list has been updated since the throttle started. \n Also note that turnouts marked in EXRAIL with the HIDDEN keyword instead of a \"description\" will NOT show up in these commands. \n\n\n Automations/Routes\n\n A throttle need to know which EXRAIL Automations and Routes it can show the user.\n\n ```<JA>``` Returns a list of Automations/Routes\n e.g. ```<jA 13 16 23>```\n Indicates route/automation ids.\n Information on each route needs to be obtained by \n ```<JA 13>``` \n returns e.g. ```<jA 13 R \"description\">``` for a route\n or  ```<jA 13 A \"description\">``` for an automation. \n or ```<jA 13 X>``` for id not found\n\n Whats the difference: \n   A Route is just a call to an EXRAIL ROUTE, traditionally to set some turnouts or signals but can be used to perform any kind of EXRAIL function... but its not expecting to know the loco.  \n   Thus a route can be triggered by sending in for example ```</START 13>```. \n \n   An Automation is a handoff of the last accessed loco id to an EXRAIL AUTOMATION which would typically drive the loco away.\n   Thus an Automation expects a start command with a cab id\n   e.g. ```</START 13 3>```\n\n\n   Roster Information:\n   The ```<JR>``` command requests a list of cab ids from the roster.\n   e.g. responding ```<jR 3 200 6336>```\n   or <jR> for none. \n\n   Each Roster entry had a name and function map obtained by:\n   ```<JR 200>```  reply like ```<jR 200 \"Thomas\" \"whistle/*bell/squeal/panic\">\n   \n   Refer to EXRAIL ROSTER command for function map format.\n\n\n  Obtaining throttle status.\n  ```<t cabid>```  Requests a deliberate update on the cab speed/functions in the same format as the cab broadcast.\n     ```<l cabid slot speedbyte functionMap>```\n      Note that a slot of -1 indicates that the cab is not in the reminders table and this comand will not reserve a slot until such time as the cab is throttled.\n\n\n  COMMANDS TO AVOID\n\n  ```<f cab func1 func2>```     Use ```<F cab function 1/0>```\n  ```<t  slot cab speed dir>``` Just drop the slot number \n  ```<T commands>``` other than ```<T id 0/1>```\n  ```<s>```\n  ```<c>```\n\n\n\n"
  },
  {
    "path": "Release_Notes/TrackManager.md",
    "content": "# 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 track and PROG track, then you DO NOT need to read the rest of this document.**\n\nWhat follows is for advanced users interested in managing power districts and/or running DC locomotives through DCC++EX.\n\n## What is the Track Manager\nTrack Manger (TM from now on) is an integral part of DCC++EX software that is responsible for:\n- Managing track power state.\n- Monitoring track overloads and shorts.\n- Routing the DCC main or prog track waveforms to the correct Motor Driver and thus track.\n- Managing the JOIN feature.\n- Intercepting throttle commands to locos running on DC tracks.\n- Handling user or EXRAIL commands to switch track status.\n\nIn the default scenario of a single DCC track and a PROG track, the TM behaves as for the previous versions of DCC++EX so if thats what you want, you dont need to mess with it.\n\nThe TM is able to handle up to 8 separate track domains. Each domain requires a hardware driver to supply track voltage. A typical motor driver shield supplies two tracks, which is what we have used in the past as main and prog.\n\nUnlike the previous version of DCC++EX, where the shield channel A was always the DCC main and channel B was always the DCC prog track, TM allows :\n- None, any or all the tracks can be DCC Main.\n- None or ONE track may be DCC prog at any given time.\n- Any track may be powered on or off independently of the others.\n- Any track may be disconnected from the DCC signal and used as a DC track with a given loco address. (See DC discussion later)\n\nWith such flexibility comes responsibility... the potential for making mistakes means taking extra care with your configuration!\n\n**NOTE** TM does NOT use \"zero stretching\" to control your DC motor. Instead, it uses true Pulse Width Modulation (PWM) to efficiently run your loco using the same method a decoder uses to control a DCC loco's motor. DC locos can even run better on TM than they can on a normal analog throttle, especially at low speed, since it is always applying the full track voltage, albeit in pulses of varying duration. \n\n## Using the Track Manager (DCC)\nTM names the tracks A to H. In a default setup, you will normally have tracks A and B where A will default to be the DCC main signal and B will be the DCC prog.\n\nThere is a new user command `<=>` which is used to control the TM but the `<0>` and `<1>` commands operate as before.\n\n- `<=>`  lists the current track settings.\nIn a default setup this will normally return \n```\n        <=A DCC>\n        <=B PROG>\n```\n- `<=t DCC>`  sets track t (A..H) to use the DCC main track.   For example `<=C DCC>` sets track C. All tracks that are set to DCC will receive the same DCC signal waveform.\n- `<=t PROG>` Sets track t (A..H) to be the one and only PROG track. Any previous PROG track is turned off.\n- `<=t OFF>` turns off the track t. It will not power on with `<1>` because it will not know what signal to send. \n\nIn an all-DCC environment it is unlikely that you will need to do anything other than setting any additional tracks (C...H) as DCC in your `mySetup.h` file.\n\nBear in mind that a track may actually be only connected to DCC accessories such as signals and turnouts... your layout, your choice. \n\nNote that when setting a track to PROG or OFF, its power is switched off automatically. (The PROG track manages power on an as-needed basis under normal circumstances.\nWhen setting a track to MAIN (or DC, DCX see later) the power is applied according to the most recent `<1>` or `<0>` command as being the most compatible with previous versions.\n\n## using the Track Manager (DC)\n\nTM allows any or all of your tracks to be individually selected as a DC track which responds to throttle commands on any given loco address. So for example if track A is set to DC address 55, then any throttle commands to loco 55 will be transmitted as DC onto track A and thus a DC loco can be driven along that track. almost exactly as if it was DCC.\nYour throttle (JMRI, EX-Webthrottle, Withrottle, Engine Driver etc etc) do not know or care that this is a DC loco so nothing needs to change.\n\nFor a simple Command Station setup to run just two DC tracks instead of DCC, you only need to assign DC addresses to tracks A and B. If you want DCC on track A and DC on track B, you just need to set track B to a suitable DC address.\n\nThe command to set a track to a DC address is as follows \n- `<=t DC a>` Sets track t (A..H) to use loco address a. e.g. <=A DC 3>\n\nA simple 2 separate loop DC track, wired the traditional way in opposite directions, may be set like this to use loco addresses 1 and 2.  \n```\n<=A DC 1>\n<=A DC 2>\n```\n\n### Crossing between DC tracks \n\nThere are some slightly mind-bending issues to be addressed, especially if you want to be able to cross between two separate DC tracks or use your layout in DCC or DC mode. This is because the control of DC loco direction is relative to the TRACK and not the LOCO. (you turn a DC loco round on the track and it continues in the same geographical direction. You turn a DCC loco around and it continues to go forwards or backwards in the opposite geographical direction.)\n\nGenerally DC tracks are wired so that two mainline tracks are in opposite direction which makes operation easy BUT crossovers between tracks will cause shorts unless you have very complex switching arrangements.\nThis is generally incompatible with DCC wiring which expects to be able to cross between tracks with impunity because they are all wired with the same polarity. \n\nTo get over this issue TM allows the polarity of a DC track to be swapped so that tracks wired for DCC may be switched to DC with a polarity chosen at run time according to your operations. So, for example, you may have two loops with a crossing between them. Normally you need them in opposite directions, but when you need to drive over the crossing, you need to switch one or other track so that they are at the same polarity.\n(This is a good case for using EXRAIL to help)\n\nThe command `<=t DCX a>` will set track t (A..H) to be DC but with reversed polarity compared with a track set to DC.\n\nIts perfectly OK to cross between DC tracks by setting them to the same loco address and making sure you get the polarity right!\n\n## Connecting Hardware\nEach track requires hardware to control it\n- Power on/off\n- Polarity (direction, signal etc)\n- Brake (shorts tracks together)\n- Current (analog reading)\n\nThe standard motor shields provide this for two separate tracks and are predictable and easy to use. However STACKING shields is not a viable way of adding more tracks because it prevents the software from gaining access to the individual track pins. Similarly, wiring all the signal pins together for example, will give you a shared DCC signal but it will eliminate any possibility of switching the track purpose at run time. So, you are going to have to understand enough to wire track drivers to various pins if you wish to extend beyond 2 tracks and take advantage of TM.\n\nYou will also need to consider the implications of differing electronic implementations that would cause unexpected issues when a loco moves between tracks. We know this works fine for a typical shield because we use `<1 JOIN>` quite happily but this may be different if you mix hardware types..... (NOT MY PROBLEM !) \n\nThe easiest way to consider the wiring is to treat each track individually (either as a separate driver or as half of a shield).\n\nYou will require,for each track, on the Arduino:\n- A GPIO pin (or a HAL vpin perhaps on an I2C extender, code TBA!!!) to switch power.\n- A GPIO pin to switch the signal direction\n- A GPIO pin with PWM capability to switch the Brake (you may omit this if you dont want any DC capability)\n- Optionally An Analog pin to read the current (unless your hardware cant do that, perhaps its just feeding a booster)\n- Optionally a GPIO fault pin if thats how your hardware works. (NOT recommended as you're going to run out of pins)\n\nIF you have no more than 3 tracks and you can arrange for the signal pins to be one of 11,12,13 on a Mega, THEN there is a slight advantage internally and the waveform will be super-sharp.\n\n**Hardware that has two signal pins still needs some code thought!!!!!!!!**\n\n\n## Configuring the Software\n\nConfiguring the software to provide more tracks is a simple extension of the existing method of customising the #define of MOTOR_SHIELD_TYPE in config.h\nSince there can be no standard setup of your wiring and hardware choices, it will be necessary to create your custom built MOTOR_SHIELD_TYPE in the manner described in MotorDrivers.h and simply continue to add more `new MotorDriver(` definitions to the list, providing all the pin numbers and electronic limits for each track. (or even shorten the list to 1)\n\n## Using EXRAIL to control Track Manager\nEXRAIL has a single additional command that can be used to automate TM.\n\n- `SET_TRACK(t,mode)`\nwhere t is the track letter A..H and mode is one of\n- `OFF`  track is switched off\n- `DCC`  track gets DCC signal\n- `PROG` track gets DCC prog signal\n- `DC`   track is set to DC mode with the cab address of the currently executing EXRAIL sequence. \n- `DCX` as DC but with reversed polarity.\n\nDC/DCX are designed so that you can be automating a DCC loco, drive it onto a separate track and switch to DC without having to know the cab address. (e.g AUTOMATION) \nIf however you are just running a ROUTE you can always do something like this:\n```\n ROUTE(77,\"Set track G to DC 123\")\n   SETLOCO(123)\n   SET_TRACK(G,DC)\n   DONE\n```\n\n## Where and How for the Code.\nThe TM code is primarily in TrackManager.cpp which is responsible for coordinating the track settings and commands. \n\nEach individual track is handled by an instance of MotorDriver created from the MOTOPR_SHIELD_TYPE definition in config.h \n\nMany functions formerly in the DCCWaveform code have been moved to TrackManager or MotorDriver, notably the power control and checking. This makes the code easier to follow.\n"
  },
  {
    "path": "Release_Notes/consists.md",
    "content": "# Consist develeopment spec/checklist\n\n## creating and managing\n\n- ```<^ leadId followerId [id...] >```  creates a CS consist up to  (parser limit) locos  with first id being lead loco. [DONE]\n\n- check each parameter is valid dcc range. [DONE]\n\n- must catch same loco in list twice [DONE]\n\n- negative loco ids in consist mean direction reversed so setting the flag in locoslot. [DONE]\n\n- lead loco could be real or virtual, we couldnt tell but functions would be sent to lead loco address.[DONE]\n\n- <^ leadid> means delete consist led by this loco\n\n- If any follower is already in another consist, error.[DONE]\n\n- all locos in consist are estopped during consist construction.\n\n- <! forget command on any loco will delete a consist chain [DONE]\n\n- `<^ locoid>`  on existing consist lead deletes the consist chain (doesnt create a 1 loco consist :) [DONE]\n\n- ```<^>``` lists all consists [DONE]\n\n- ```<D CABS>``` includes consist chains\n\n- consist deletion will estop all locos in consist. \n\n- Locoslots contain nextConsist, leadConsist, consistInvertedDirection [DONE]\n\n- lead loco has null leadConsist but !null nextConsist[DONE]\n\n- Throttle commands to following locos are routed to the lead. [NO] becaused its impossible to know whether the sent command has allowed for the reversal of the addressed loco or not.\n\n- Instead, throttle commands to the followers are ignored.[DONE]\n\n- Throttle commands to lead loco are reflected directly to the targetSpeed of the followers. [DONE]\n\n- Broadcast of lead loco broadcasts all followers [DONE]\n\n- Multi loco broadcast should be transmission per throttle rather then one per loco per throttle.\n\n- Following locos do NOT issue DCC speed packets.[DONE]\n\n- Lead loco throttle commands / reminders etc queue DCC packets for all locos simultaneoulsy. [DONE]\n\n- reminder loop ignores following loco speed reminders. [DONE]\n(Lead reminder will be sent to all follower addresses, allowing for reversed locos.)\n\n- momentum, speed limits etc will effectively come from the lead loco.[DONE]\n\n- Functions to lead loco are not mapped to followers...[DONE] (Or do we want a way of configuring this)\n"
  },
  {
    "path": "Release_Notes/duinoNodes.md",
    "content": "Using Lew's Duino Gear boards:\n\n1. DNIN8 Input\n   This is a shift-register implementation of a digital input collector. \n   Multiple DNIN8 may be connected in sequence but it is IMPORTANT that the software \n   configuratuion correctly represents the number of boards connected otherwise the results will be meaningless. \n\n   Use in myAnimation.h\n\n   HAL(IO_DNIN8, firstVpin, numPins, clockPin, latchPin, dataPin)\n   e.g. \n   HAL(IO_DNIN8, 400, 16, 40, 42, 44)\n\n   OR Use in myHal.cpp\n     IO_DNIN8::create( firstVpin, numPins, clockPin, latchPin, dataPin)\n\n\n\n   This will create virtaul pins 400-415 using two DNIN8 boards connected in sequence. \n   Vpins 400-407 will be on the first board (closest to the CS) and 408-415 on the second.\n\n   Note: 16 pins uses two boards. You may specify a non-multiple-of-8 pins but this will be rounded up to a multiple of 8 and you must connect ONLY the number of boards that this takes. \n      \n   This example uses Arduino GPIO pins 40,42,44 as these are conveniently side-by-side on a Mega which is easier when you are using a 3 strand cable. \n \n The DNIN8K module works the same but you must use DNIN8K in the HAL setup instead of DNIN8. NO you cant mix 8 and 8k versions in the same string of boards but you can create another string of boards. \n\n\n DNOU8 works the same way, \n   Use in myAnimation.h\n\n   HAL(IO_DNOU8, firstVpin, numPins, clockPin, latchPin, dataPin)\n   e.g. \n   HAL(IO_DNIN8, 450, 16, 45, 47, 49)\n\n   OR Use in myHal.cpp\n     IO_DNIN8::create( firstVpin, numPins, clockPin, latchPin, dataPin)\n\nThis creates a string of input pins 450-465. Note the clock/latch/data pins must be different to any DNIN8/k pins.      \n"
  },
  {
    "path": "Release_Notes/momentum.md",
    "content": "New Momentum feature notes:\n\nThe command station can apply momentum to throttle movements in the same way that a standards compliant DCC decoder can be set to do. This momentum can be defaulted system wide and overridden on individual locos. It does not use or alter the loco CV values and so it also works when driving DC locos.\nThe momentum is applied regardless of the throttle type used (or even EXRAIL). \n\nMomentum is specified in mS / throttle_step.\n\nThere is a new command `<m cabid accelerating [brake]>`\nwhere the brake value defaults to the accelerating value.\n\nFor example: \n`<m 3 0>`   sets loco 3 to no momentum.\n`<m 3 21>`   sets loco 3 to 21 mS/step.\n`<m 3 21 42>`   sets loco 3 to 21 mS/step accelerating and 42 mS/step when decelerating.\n\n`<m 0 21>`  sets the default momentum to 21mS/Step for all current and future locos that have not been specifically set.\n`<m 3 -1>`   sets loco 3 to track the default momentum value. \n\nEXRAIL\n  A new macro `MOMENTUM(accel [, decel])` sets the momentum value of the current tasks loco ot the global default if loco=0. \n\nNote: Setting Momentum 7,14,21 etc is similar in effect to setting a decoder CV03/CV04 to 1,2,3.   \n\nAs an additional option, the momentum calculation is based on the \ndifference in throttle setting and actual speed. For example, the time taken to reach speed 50 from a standing start would be less if the throttle were set to speed 100, thus increasing the acceleration. \n\n`<m LINEAR>` - acceleration is uniform up to selected throttle speed.\n`<m POWER>`  - acceleration depends on difference between loco speed and selected throttle speed.\n "
  },
  {
    "path": "Release_Notes/release_notes_v3.0.0.md",
    "content": "\n\nThe DCC-EX Team is pleased to release CommandStation-EX-v3.0.0 as a Production Release.  This release is a major re-write of earlier versions.  We've re-architected the code-base so that it can better handle new features going forward.  Download the compressed files here:\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/files/5611333/CommandStation-EX.zip)\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/files/5611335/CommandStation-EX.tar.gz)\n\n\n**Known Bugs:**\n - **Consisting through JMRI** - currently does not work in this release.  A number of testers were able to develop a work around.  If interested enter a Support Ticket.\n - **Wi-Fi** - works, but can be challenging to use if you want to switch between AP mode and STA station mode.\n - **Pololu Motor Shield** - is supported with this release, but the user may have to play around with some timings to enable programming mode due to limitation in its current sensing circuitry \n\n **Summary of the key new features added to CommandStation-EX V3.0.0:**\n - **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n - **Withrottle Integrations** - Act as a host for four WiThrottle clients concurrently. \n - **Add LCD/OLED support** - OLED supported on Mega only\n - **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n - **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n - **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n - **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n - **New, simpler function command** - ```<F>``` command allows setting functions based on their number, not based on a code as in ```<f>```\n - **Function reminders** - Function reminders are sent in addition to speed reminders\n - **Functions to F28** - All NMRA functions are now supported\n - **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them\n - **Diagnostic ```<D>``` commands** - See documentation for a full list of new diagnostic commands\n - **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n - **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n - **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n - **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n - **Fix EEPROM bugs**\n - **Number of locos discovery command** - ```<#>``` command \n - **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.\n - **Automatic slot managment** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. ```<!>``` command added to release locos from memory.\n\n\n**Key Contributors**\n\n**Project Lead**\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n\n**CommandStation-EX Developers**\n- Chris Harlow - Bournemouth, UK (UKBloke)\n- Harald Barth - Stockholm, Sweden (Haba)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- M Steve Todd - - Engine Driver and JMRI Interface\n- Scott Catalanno - Pennsylvania\n- Gregor Baues - Île-de-France, France (grbba)\n\n**exInstaller Software**\n- Anthony W - Dayton, Ohio, USA (Dex, Dex++)\n\n**Website and Documentation**\n- Mani Kumar - Bangalor, India (Mani / Mani Kumar)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- Roger Beschizza - Dorset, UK (Roger Beschizza)\n- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)\n-Kevin Smith - (KCSmith)\n\n**Beta Testing / Release Management / Support**\n- Larry Dribin\t- Release Management\n- Keith Ledbetter\t\n- BradVan der Elst\t\n- Andrew Pye\t\n- Mike Bowers\t\n- Randy McKenzie\n- Roberto Bravin\n- Sim Brigden\n- Alan Lautenslager\n- Martin Bafver\t\n- Mário André Silva\t\n- Anthony Kochevar\t\n- Gajanatha Kobbekaduwe\t\n- Sumner Patterson \n- Paul - Virginia, USA\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/files/5611333/CommandStation-EX.zip)\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/files/5611335/CommandStation-EX.tar.gz)\n\n"
  },
  {
    "path": "Release_Notes/release_notes_v3.1.0.md",
    "content": "The DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release.  Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors.  This release includes all of the Point Releases from v3.0.1 to v3.0.16.\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)\n\n**Known Issues**\n\n- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup\n- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitation in its current sensing circuitry\n\n#### Summary of key features and/or bug fixes by Point Release\n\n**Summary of the key new features added to CommandStation-EX V3.0.16**\n\n- Ignore CV1 bit 7 read if rejected by a non NMRA compliant decoder when identifying loco id\n\n**Summary of the key new features added to CommandStation-EX V3.0.15**\n\n- Send function commands just once instead of repeating them 4 times\n\n**Summary of the key new features added to CommandStation-EX V3.0.14**\n\n- Add feature to tolerate decoders that incorrectly have gaps in their ACK pulse\n- Provide proper track power management when joining and unjoining tracks with <1 JOIN>\n\n**Summary of the key new features added to CommandStation-EX V3.0.13**\n\n- Fix for CAB Functions greater than 127\n\n**Summary of the key new features added to CommandStation-EX V3.0.12**\n\n- Fixed clear screen issue for nanoEvery and nanoWifi\n\n**Summary of the key new features added to CommandStation-EX V3.0.11**\n\n- Reorganized files for support of 128 speed steps\n\n**Summary of the key new features added to CommandStation-EX V3.0.10**\n\n- Added Support for the Teensy 3.2, 3.5, 3.6, 4.0 and 4.1 MCUs\n- No functional change just changes to avoid complier warnings for Teensy/nanoEvery\n\n**Summary of the key new features added to CommandStation-EX V3.0.9**\n\n- Rearranges serial newlines for the benefit of JMRI\n- Major update for efficiencies in displays (LCD, OLED)\n- Add I2C Support functions\n\n**Summary of the key new features added to CommandStation-EX V3.0.8**\n\n- Wraps <* *> around DIAGS for the benefit of JMRI\n\n**Summary of the key new features added to CommandStation-EX V3.0.7**\n\n- Implemented support for older 28 apeed step decoders - Option to turn on 28 step speed decoders in addition to 128. If set, all locos will use 28 steps.\n- Improved overload messages with raw values (relative to offset)\n\n**Summary of the key new features added to CommandStation-EX V3.0.6**\n\n- Prevent compiler warning about deprecated B constants\n- Fix Bug that did not let us transmit 5 byte sized packets - 5 Byte commands like PoM (programming on main) were not being sent correctly\n- Support for Huge function numbers (DCC BinaryStateControl) - Support Functions beyond F28\n- <!> ESTOP all - New command to emergency stop all locos on the main track\n- <- [cab]> estop and forget cab/all cabs - Stop and remove loco from the CS. Stops the repeating throttle messages\n- `<D RESET>` command to reboot Arduino\n- Automatic sensor offset detect\n- Improved startup msgs from Motor Drivers (accuracy and auto sense factors)\n- Drop post-write verify - No need to double check CV writes. Writes are now even faster.\n- Allow current sense pin set to UNUSED_PIN - No need to ground an unused analog current pin. Produce startup warning and callback -2 for prog track cmds.\n\n**Summary of the key new features added to CommandStation-EX V3.0.5**\n\n- Fix Fn Key startup with loco ID and fix state change for F16-28\n- Removed ethernet mac config and made it automatic\n- Show wifi ip and port on lcd\n- Auto load config.example.h with warning\n- Dropped example .ino files\n- Corrected .ino comments\n- Add Pololu fault pin handling\n- Waveform speed/simplicity improvements\n- Improved pin speed in waveform\n- Portability to nanoEvery and UnoWifiRev2 CPUs\n- Analog read speed improvements\n- Drop need for DIO2 library\n- Improved current check code\n- Linear command\n- Removed need for ArduinoTimers files\n- Removed option to choose different timer\n- Added EX-RAIL hooks for automation in future version\n- Fixed Turnout list\n- Allow command keywords in mixed case\n- Dropped unused memstream\n- PWM pin accuracy if requirements met\n\n**Summary of the key new features added to CommandStation-EX V3.0.4**\n\n- \"Drive-Away\" Feature - added so that throttles like Engine Driver can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track\n- WiFi Startup Fixes\n\n**Summary of the key new features added to CommandStation-EX V3.0.3**\n\n- Command to write loco address and clear consist\n- Command will allow for consist address\n- Startup commands implemented\n\n**Summary of the key new features added to CommandStation-EX V3.0.2:**\n\n- Create new output for current in mA for `<c>` command - New current response outputs current in mA, overlimit current, and maximum board capable current\n- Simultaneously update JMRI to handle new current meter\n\n**Summary of the key new features added to CommandStation-EX V3.0.1:**\n\n- Add back fix for jitter\n- Add Turnouts, Outputs and Sensors to `<s>` command output\n\n**CommandStation-EX V3.0.0:**\n\n**Release v3.0.0 was a major rewrite if earlier versions of DCC++.  The code base was re-architeced and core changes were made to the Waveform generator to reduce overhead and make better use of Arduino.** **Summary of the key new features added in Release v3.0.0 include:**\n\n- **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.\n- **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n- **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently.\n- **Add LCD/OLED support** - OLED supported on Mega only\n- **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n- **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n- **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n- **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n- **New, simpler function command** - `<F>` command allows setting functions based on their number, not based on a code as in `<f>`\n- **Function reminders** - Function reminders are sent in addition to speed reminders\n- **Functions to F28** - All NMRA functions are now supported\n- **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)\n- **Diagnostic `<D>` commands** - See documentation for a full list of new diagnostic commands\n- **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n- **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n- **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n- **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n- **Fix EEPROM bugs**\n- **Number of locos discovery command** - `<#>` command\n- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.\n- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.\n\n**Key Contributors**\n\n**Project Lead**\n\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n\n**CommandStation-EX Developers**\n\n- Chris Harlow - Bournemouth, UK (UKBloke)\n- Harald Barth - Stockholm, Sweden (Haba)\n- Neil McKechnie - Worcestershire, UK (NeilMck)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- M Steve Todd -  \n- Scott Catalano - Pennsylvania\n- Gregor Baues - Île-de-France, France (grbba)\n\n**Engine Driver and JMRI Interface**\n\n- M Steve Todd\n\n**exInstaller Software**\n\n- Anthony W - Dayton, Ohio, USA (Dex, Dex++)\n\n**Website and Documentation**\n\n- Mani Kumar - Bangalor, India (Mani / Mani Kumar)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- Roger Beschizza - Dorset, UK (Roger Beschizza)\n- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n\n**WebThrotle-EX**\n\n- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)\n- Mani Kumar - Bangalor, India (Mani /Mani Kumar)\n- Matt H - Somewhere in Europe\n\n**Beta Testing / Release Management / Support**\n\n- Larry Dribin - Release Management\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n- Keith Ledbetter\n- BradVan der Elst\n- Andrew Pye\n- Mike Bowers\n- Randy McKenzie\n- Roberto Bravin\n- Sim Brigden\n- Alan Lautenslager\n- Martin Bafver\n- Mário André Silva\n- Anthony Kochevar\n- Gajanatha Kobbekaduwe\n- Sumner Patterson\n- Paul - Virginia, USA\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)\n"
  },
  {
    "path": "Release_Notes/release_notes_v4.0.0.md",
    "content": "Version 4.0 Release Notes\n*************************\n\nThe DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production Release.  Release v4.0.0 is a Major release that adds significant new product design, plus Automation features and bug fixes. The team continues improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors.  This release includes all of the Point Releases from v3.2.0 to v3.2.0 rc13.\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)\n\n\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v0.0.0-Prod/CommandStation-EX.tar.gz)\n\n**Known Issues**\n\n- **Wi-Fi** - Requires sending `<AT>` commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup\n- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitations in its current sensing circuitry\n\n**All New Major DCC++EX 4.0.0 features**\n\n- **New HAL Hardware Abstraction Layer API** that automatically detects and greatly simplifies interfacing to many predefined accessory boards for servos, signals & sensors and added I/O (digital and analog inputs and outputs, servos etc). \n- HAL Support for;\n  - MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules. \n  - PCA9685 PWM (servo & signal) control modules. \n  - Analogue inputs on Arduino pins and on ADS111x I2C modules. \n  - MP3 sound playback via DFPlayer module. \n  - HC-SR04 Ultrasonic range sensor module. \n  - VL53L0X Laser range sensor module (Time-Of-Flight). \n  - A new `<D HAL SHOW>` command to list the HAL devices attached to the command station\n \n**New Command Station Broadcast throttle logic**\n\n- Synchronizes multiple WiThrottles and PC based JMRI Throttles for direction, speed and F-key updates\n\n**New ‘Discovered Servers’ on WiFi Throttles**\n\n- Our New multicast Dynamic Network Server (mDNS) enhancement allows us to display the available WiFi server connections to a DCC++EX Command Station. Selecting it allows your WiThrottle App to connect to and load Server Rosters and function keys to your throttle from the new DCC++EX Command Station Server Roster.\n\n**New DCC++EX 4.0.0 with EX-RAIL Extended Railroad Automation Instruction Language**\n\n- Use to control your entire layout or as a separate accessory/animation controller\n- Awesome, cleverly powerful yet simple user friendly scripting language for user built Automation & Routing scripts. \n- You can control Engines, Sensors, Turnouts, Signals, Outputs and Accessories that are entered into your new myAutomation.h file, then uploaded into the DCC++EX Command Station.\n- EX-RAIL scripts are automatically displayed as Automation {Handoff} and Route {Set} buttons on supported WiFi Throttle Apps.\n\n**New EX-RAIL ‘Roster’ Feature**\n\n- List and store user defined engine roster & function keys inside the command station, and automatically load them in WiFi Throttle Apps.\n- When choosing “DCC++EX” from discovered servers an Engine Driver or WiThrottle is directly connected to the Command Station. \n- The EX-RAIL ’ROSTER’ command allows all the engine numbers, names and function keys you’ve listed in your myAutomation.h file to automatically upload the Command Station's ‘Server Roster’ into your Engine Driver and WiThrottle Apps.  \n\n**New JMRI 4.99.2 and above specific DCC++EX 4.0 features**\n\n- Enhanced JMRI DCC++ Configure Base Station pane for building and maintaining Sensor, Turnout and Output devices, or these can automatically be populated from the DCC++EX Command Station's mySetup.h file into JMRI.\n\n- JMRI now supports multiple serial connected DCC++EX Command Stations, to display and track separate \"Send DCC++ Command\" and \"DCC++ Traffic\" Monitors for each Command Station at the same time.\n  For example: Use an Uno DCC++EX DecoderPro Programming Station {DCC++Prg} on a desktop programming track and a second Mega DCC++EX EX-RAIL Command Station for Operations {DCC++Ops} on the layout with an additional `<JOINED>` programming spur or siding track for acquiring an engine and ‘Drive Away’ onto the mainline (see the DriveAway feature for more information).\n\n**DCC++EX 4.0.0 additional product enhancements**\n\n- Additional Motor Shields and Motor Board {boosters) supported\n- Additional Accessory boards supported for GPIO expansion, Sensors, Servos & Signals\n- Additional diagnostic commands like ‘D ACK RETRY’ and ‘D EXRAIL ON’ events, ‘D HAL SHOW’ devices and ‘D SERVO’ positions, and ‘D RESET’ the command station while maintaining the serial connection with JMRI\n- Automatic retry on failed ACK detection to give decoders another chance\n- New EX-RAIL ’/’ slash command allows JMRI to directly communicate with many EX-RAIL scripts\n- Turnout class revised to expand turnout capabilities and allow turnout names/descriptors to display in WiThrottle Apps.\n- Build turnouts through either or both mySetup.h and myAutomation.h files, and have them automatically passed to, and populate, JMRI Turnout Tables\n- Turnout user names display in Engine Driver & WiThrottles\n- Output class now allows ID > 255. \n- Configuration options to globally flip polarity of DCC Accessory states when driven from `<a>` command and `<T>` command.\n- Increased use of display for showing loco decoder programming information. \n- Can disable EEPROM memory code to allow room for DCC++EX 4.0 to fit on a Uno Command Station\n- Can define border between long and short addresses \n- Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms). \n- EEPROM layout change - deletes EEPROM contents on first start following upgrade. \n\n**4.0.0 Bug Fixes**\n\n- Compiles on Nano Every\n- Diagnostic display of ack pulses >32ms\n- Current read from wrong ADC during interrupt\n- AT(+) Command Pass Through \n- CiDAP WiFi Drop out and the WiThrottle F-key looping error corrected\n- One-off error in CIPSEND drop\n- Common Fault Pin Error\n- Uno Memory Utilization optimized\n\n#### Summary of Release 3.1.0 key features and/or bug fixes by Point Release\n\n**Summary of the key new features added to CommandStation-EX V3.0.16**\n\n- Ignore CV1 bit 7 read if rejected by a non NMRA compliant decoder when identifying loco id\n\n**Summary of the key new features added to CommandStation-EX V3.0.15**\n\n- Send function commands just once instead of repeating them 4 times\n\n**Summary of the key new features added to CommandStation-EX V3.0.14**\n\n- Add feature to tolerate decoders that incorrectly have gaps in their ACK pulse\n- Provide proper track power management when joining and unjoining tracks with <1 JOIN>\n\n**Summary of the key new features added to CommandStation-EX V3.0.13**\n\n- Fix for CAB Functions greater than 127\n\n**Summary of the key new features added to CommandStation-EX V3.0.12**\n\n- Fixed clear screen issue for nanoEvery and nanoWifi\n\n**Summary of the key new features added to CommandStation-EX V3.0.11**\n\n- Reorganized files for support of 128 speed steps\n\n**Summary of the key new features added to CommandStation-EX V3.0.10**\n\n- Added Support for the Teensy 3.2, 3.5, 3.6, 4.0 and 4.1 MCUs\n- No functional change just changes to avoid complier warnings for Teensy/nanoEvery\n\n**Summary of the key new features added to CommandStation-EX V3.0.9**\n\n- Rearranges serial newlines for the benefit of JMRI\n- Major update for efficiencies in displays (LCD, OLED)\n- Add I2C Support functions\n\n**Summary of the key new features added to CommandStation-EX V3.0.8**\n\n- Wraps <* *> around DIAGS for the benefit of JMRI\n\n**Summary of the key new features added to CommandStation-EX V3.0.7**\n\n- Implemented support for older 28 apeed step decoders - Option to turn on 28 step speed decoders in addition to 128. If set, all locos will use 28 steps.\n- Improved overload messages with raw values (relative to offset)\n\n**Summary of the key new features added to CommandStation-EX V3.0.6**\n\n- Prevent compiler warning about deprecated B constants\n- Fix Bug that did not let us transmit 5 byte sized packets - 5 Byte commands like PoM (programming on main) were not being sent correctly\n- Support for Huge function numbers (DCC BinaryStateControl) - Support Functions beyond F28\n- <!> ESTOP all - New command to emergency stop all locos on the main track\n- <- [cab]> estop and forget cab/all cabs - Stop and remove loco from the CS. Stops the repeating throttle messages\n- `<D RESET>` command to reboot Arduino\n- Automatic sensor offset detect\n- Improved startup msgs from Motor Drivers (accuracy and auto sense factors)\n- Drop post-write verify - No need to double check CV writes. Writes are now even faster.\n- Allow current sense pin set to UNUSED_PIN - No need to ground an unused analog current pin. Produce startup warning and callback -2 for prog track cmds.\n\n**Summary of the key new features added to CommandStation-EX V3.0.5**\n\n- Fix Fn Key startup with loco ID and fix state change for F16-28\n- Removed ethernet mac config and made it automatic\n- Show wifi ip and port on lcd\n- Auto load config.example.h with warning\n- Dropped example .ino files\n- Corrected .ino comments\n- Add Pololu fault pin handling\n- Waveform speed/simplicity improvements\n- Improved pin speed in waveform\n- Portability to nanoEvery and UnoWifiRev2 CPUs\n- Analog read speed improvements\n- Drop need for DIO2 library\n- Improved current check code\n- Linear command\n- Removed need for ArduinoTimers files\n- Removed option to choose different timer\n- Added EX-RAIL hooks for automation in future version\n- Fixed Turnout list\n- Allow command keywords in mixed case\n- Dropped unused memstream\n- PWM pin accuracy if requirements met\n\n**Summary of the key new features added to CommandStation-EX V3.0.4**\n\n- \"Drive-Away\" Feature - added so that throttles like Engine Driver can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track\n- WiFi Startup Fixes\n\n**Summary of the key new features added to CommandStation-EX V3.0.3**\n\n- Command to write loco address and clear consist\n- Command will allow for consist address\n- Startup commands implemented\n\n**Summary of the key new features added to CommandStation-EX V3.0.2:**\n\n- Create new output for current in mA for `<c>` command - New current response outputs current in mA, overlimit current, and maximum board capable current\n- Simultaneously update JMRI to handle new current meter\n\n**Summary of the key new features added to CommandStation-EX V3.0.1:**\n\n- Add back fix for jitter\n- Add Turnouts, Outputs and Sensors to `<s>` command output\n\n**CommandStation-EX V3.0.0:**\n\n**Release v3.0.0 was a major rewrite if earlier versions of DCC++.  The code base was re-architeced and core changes were made to the Waveform generator to reduce overhead and make better use of Arduino.** **Summary of the key new features added in Release v3.0.0 include:**\n\n- **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.\n- **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n- **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently.\n- **Add LCD/OLED support** - OLED supported on Mega only\n- **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n- **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n- **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n- **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n- **New, simpler function command** - `<F>` command allows setting functions based on their number, not based on a code as in `<f>`\n- **Function reminders** - Function reminders are sent in addition to speed reminders\n- **Functions to F28** - All NMRA functions are now supported\n- **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)\n- **Diagnostic `<D>` commands** - See documentation for a full list of new diagnostic commands\n- **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n- **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n- **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n- **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n- **Fix EEPROM bugs**\n- **Number of locos discovery command** - `<#>` command\n- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.\n- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.\n\n**Key Contributors**\n\n**Project Lead**\n\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n\n**CommandStation-EX Developers**\n\n- Chris Harlow - Bournemouth, UK (UKBloke)\n- Harald Barth - Stockholm, Sweden (Haba)\n- Neil McKechnie - Worcestershire, UK (NeilMck)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- M Steve Todd - Oregon, USA (MSteveTodd) \n- Scott Catalano - Pennsylvania\n- Gregor Baues - Île-de-France, France (grbba)\n\n**Engine Driver and JMRI Interface**\n\n- M Steve Todd\n\n**exInstaller Software**\n\n- Anthony W - Dayton, Ohio, USA (Dex, Dex++)\n\n**Website and Documentation**\n\n- Mani Kumar - Bangalor, India (Mani / Mani Kumar)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- Roger Beschizza - Dorset, UK (Roger Beschizza)\n- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n- Colin Grabham - Central NSW, Australia (Kebbin)\n\n**WebThrotle-EX**\n\n- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)\n- Mani Kumar - Bangalor, India (Mani /Mani Kumar)\n- Matt H - Somewhere in Europe\n\n**Beta Testing / Release Management / Support**\n\n- Larry Dribin - Release Management\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n- Herb Morton - Kingwood Texas, USA (Ash++)\n- Keith Ledbetter\n- Brad Van der Elst\n- Andrew Pye\n- Mike Bowers\n- Randy McKenzie\n- Roberto Bravin\n- Sam Brigden\n- Alan Lautenslager\n- Martin Bafver\n- Mário André Silva\n- Anthony Kochevar\n- Gajanatha Kobbekaduwe\n- Sumner Patterson\n- Paul - Virginia, USA\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)\n\n\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.tar.gz)\n"
  },
  {
    "path": "Release_Notes/websocketTester.html",
    "content": "<html>\n  <!-- Minimalist test page for the DCCEX websocket API.-->\n<head>\n  <script>\n  let socket = new WebSocket(\"ws://192.168.1.242:2560\",\"DCCEX\");\n  \n  // send message from the form\n  var sender = function() {\n    var msg=document.getElementById('message').value;\n    socket.send(msg);\n  }\n  // message received - show the message in div#messages\n  socket.onmessage = function(event) {\n    let message = event.data;\n  \n    let messageElem = document.createElement('div');\n    messageElem.textContent = message;\n    document.getElementById('messages').prepend(messageElem);\n  }\n  socket.onerror = function(event) {\n    let message = event.data;\n    let messageElem = document.createElement('div');\n    messageElem.textContent = message;\n    document.getElementById('messages').prepend(messageElem);\n  }\n  </script>\n</head>\n<body>\n  This is a minimalist test page for the DCCEX websocket API.\n   It demonstrates the Websocket connection and how to send\n   or receive websocket traffic.\n   The connection string must be edited to address your command station \n   correctly.<p>\n<!-- message form -->\n\n  <input type=\"text\" id=\"message\">\n  <input type=\"button\" value=\"Send\" onclick=\"sender();\">\n<!-- div with messages -->\n<div id=\"messages\"></div>\n</body>\n</html>"
  },
  {
    "path": "RingStream.cpp",
    "content": "/*\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n// NOTE: The use of a marker byte without an escape algorithm means\n// RingStream is unsuitable for binary data. Should binary data need to be \n// streamed it will be necessary to implementr an escape strategy to handle the \n// marker char when embedded in data. \n\n#include \"RingStream.h\"\n#include \"DIAG.h\"\n\nconst byte FLASH_INSERT_MARKER=0xff;\n\nRingStream::RingStream( const uint16_t len)\n{\n  _len=len;\n  _buffer=new byte[len];\n  _pos_write=0;\n  _pos_read=0;\n  _buffer[0]=0;\n  _overflow=false;\n  _mark=0;\n  _count=0; \n  _flashInsert=0;\n}\n\nsize_t RingStream::write(uint8_t b) {\n  if (_overflow) return 0;\n  _buffer[_pos_write] = b;\n  ++_pos_write;\n  if (_pos_write==_len) _pos_write=0;\n  if (_pos_write==_pos_read) {\n    _overflow=true; \n    return 0;\n  }\n  _count++;\n  return 1;\n}\n\n// Ideally, I would prefer to override the Print:print(_FlashStringHelper) function\n// but the library authors omitted to make this virtual.\n// Therefore we obveride the only other simple function that has no side effects\n// in order that StringFormatter can recognise a RingStream and call its \n// printFlash() directly.  \nint RingStream::availableForWrite()  { \n  return  THIS_IS_A_RINGSTREAM; \n  }\n\nsize_t RingStream::printFlash(const FSH * flashBuffer) {\n  // This function does not work on a 32 bit processor where the runtime\n  // sometimes misrepresents the pointer size in uintptr_t.\n  // In any case its not really necessary in a 32 bit processor because\n  // we have adequate ram. \n  if (sizeof(void*)>2) return print(flashBuffer);\n\n\n// We are about to add a PROGMEM string to the buffer. \n// To save RAM we can insert a marker and the\n// progmem address into the buffer instead.\n// The buffer reading code must recognise this marker and \n// silently extract the progmem bytes.\n// In addition, we must make the count correct as if the \n// string had been embedded so that things like the wifi code\n// can read the expected count before reading the buffer. \n\n// Establish the actual length of the progmem string.\nchar * flash=(char *)flashBuffer;\nint16_t plength=STRLEN_P(flash);\nif (plength==0) return 0; // just ignore empty string\n\n// Retain the buffer count as it will be modified by the marker+address insert\nint prevCount=_count;\nwrite(FLASH_INSERT_MARKER); // write the marker\nuintptr_t iFlash=reinterpret_cast<uintptr_t>(flash); // expect size match with pointer\n\n// write address bytes LSB first (size depends on CPU) \nfor (byte f=0;f<sizeof(iFlash); f++) {\n    write((byte) (iFlash & 0xFF));\n    iFlash>>=8;\n}\n\n// correct the buffer count to reflect the flash length, not the marker/addr.\n_count=prevCount+plength;  \nreturn plength;\n}\n\nint RingStream::read() {\n  if (_flashInsert) {\n    // we are reading out of a flash string \n    byte fb=GETFLASH(_flashInsert);\n    _flashInsert++;\n    if (fb) return fb; // we have a byte from the flash\n    // flash insert complete, clear and drop through to next buffer byte\n    _flashInsert=NULL; \n  }\n  if ((_pos_read==_pos_write) && !_overflow) return -1;  // empty  \n  byte b=readRawByte();\n  if (b!=FLASH_INSERT_MARKER) return b; \n  // Detected a flash insert \n  if (sizeof(void*)>2) {\n    DIAG(F(\"Detected invalid flash insert marker at pos %d\"),_pos_read);\n    return '?';\n  }\n  // read address bytes LSB first (size depends on CPU) \n  uintptr_t iFlash=0; \n  for (byte f=0; f<sizeof(iFlash); f++) {\n    uintptr_t bf=readRawByte();\n    bf&=0x00ff;\n    bf<<= (8*f); // shift byte to correct position in iFlash\n    iFlash |= bf;  \n  }\n  _flashInsert=reinterpret_cast<char * >( iFlash);\n  // and try again... so will read the first byte of the insert. \n  return read();\n}\n\nbyte RingStream::readRawByte() {\n  byte b=_buffer[_pos_read];\n  _pos_read++;\n  if (_pos_read==_len) _pos_read=0;\n  _overflow=false;\n  return b;\n}\n\nint RingStream::count() {\n  return (readRawByte()<<8) | readRawByte(); \n  }\n\nint RingStream::freeSpace() {\n  // allow space for client flag and length bytes\n  if (_pos_read>_pos_write) return _pos_read-_pos_write-3;\n  else return _len - _pos_write + _pos_read-3;  \n}\n\n\n// mark start of message with client id (0...9)\nvoid RingStream::mark(uint8_t b) {\n    //DIAG(F(\"RS mark client %d at %d core %d\"), b, _pos_write, xPortGetCoreID());\n    _ringClient = b;\n    _mark=_pos_write;\n    write(b); // client id\n    write((uint8_t)0);  // count MSB placemarker\n    write((uint8_t)0);  // count LSB placemarker\n    _count=0;\n}\n\n// peekTargetMark is used by the parser stash routines to know which client\n// to send a callback response to some time later. \nuint8_t RingStream::peekTargetMark() {\n  return _ringClient;\n}\n\nvoid RingStream::info() {\n  DIAG(F(\"Info len=%d count=%d pr=%d pw=%d m=%d\"),_len, _count,_pos_read,_pos_write,_mark);\n}\n\nbool RingStream::commit() {\n  _flashInsert=NULL; // prepared for first read\n  if (_overflow) {\n        //DIAG(F(\"RingStream(%d) commit(%d) OVERFLOW\"),_len, _count);\n        // just throw it away \n        _pos_write=_mark;\n        _overflow=false;\n        return false; // commit failed\n  }\n  if (_count==0) {\n    //DIAG(F(\"RS commit count=0 rewind back to %d core %d\"), _mark, xPortGetCoreID());\n    // ignore empty response\n    _pos_write=_mark;\n    _ringClient = NO_CLIENT;         //XXX make else clause later\n    return true; // true=commit ok\n  }\n  // Go back to the _mark and inject the count 1 byte later\n  _mark++;\n  if (_mark==_len) _mark=0;\n  _buffer[_mark]=highByte(_count);\n  _mark++;\n  if (_mark==_len) _mark=0;\n  _buffer[_mark]=lowByte(_count);\n  _ringClient = NO_CLIENT;\n  return true; // commit worked\n}\nvoid RingStream::flush() {\n  _pos_write=0;\n  _pos_read=0;\n  _buffer[0]=0;\n  _flashInsert=NULL; // prepared for first read\n  _ringClient = NO_CLIENT;\n}\n  \n"
  },
  {
    "path": "RingStream.h",
    "content": "#ifndef RingStream_h\n#define RingStream_h\n/*\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <Arduino.h>\n#include \"FSH.h\"\n  \nclass RingStream : public Print {\n\n  public:\n    RingStream( const uint16_t len);\n    static const int THIS_IS_A_RINGSTREAM=777;\n    virtual size_t write(uint8_t b);\n\n    // This availableForWrite function is subverted from its original intention so that a caller \n    // can destinguish between a normal stream and a RingStream. \n    // The Arduino compiler does not support runtime dynamic cast to perform\n    // an instranceOf check. \n    // This is necessary since the Print functions are mostly not virtual so \n    // we cant override the print(__FlashStringHelper *) function.\n   virtual int availableForWrite() override;\n    using Print::write;\n    size_t printFlash(const FSH * flashBuffer);\n    int read();\n    int count();\n    int freeSpace();\n    void mark(uint8_t b);\n    bool commit();\n    uint8_t peekTargetMark();\n    void flush();\n    void info();\n    byte readRawByte();\n    inline int peek() {\n      if ((_pos_read==_pos_write) && !_overflow) return -1;  // empty\n      return _buffer[_pos_read];\n    };\n    static const byte NO_CLIENT=255;\n private:\n   int _len;\n   int _pos_write;\n   int _pos_read;\n   bool _overflow;\n   int _mark;\n   int _count;\n   byte * _buffer;\n   char * _flashInsert;\n   byte _ringClient = NO_CLIENT;\n};\n\n#endif\n"
  },
  {
    "path": "SSD1306Ascii.cpp",
    "content": "/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman\n * Modifications (C) 2021 Neil McKechnie\n *\n *  This file is part of CommandStation-EX\n *\n * This Library is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This Library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with the Arduino SSD1306Ascii Library.  If not, see\n * <http://www.gnu.org/licenses/>.\n */\n#include \"SSD1306Ascii.h\"\n#include \"I2CManager.h\"\n#include \"FSH.h\"\n\n//==============================================================================\n// SSD1306/SSD1106 I2C command bytes\n//------------------------------------------------------------------------------\n/** Set Lower Column Start Address for Page Addressing Mode. */\nstatic const uint8_t SSD1306_SETLOWCOLUMN = 0x00;\n/** Set Higher Column Start Address for Page Addressing Mode. */\nstatic const uint8_t SSD1306_SETHIGHCOLUMN = 0x10;\n/** Set Memory Addressing Mode. */\nstatic const uint8_t SSD1306_MEMORYMODE = 0x20;\n/** Set display RAM display start line register from 0 - 63. */\nstatic const uint8_t SSD1306_SETSTARTLINE = 0x40;\n/** Set Display Contrast to one of 256 steps. */\nstatic const uint8_t SSD1306_SETCONTRAST = 0x81;\n/** Enable or disable charge pump.  Follow with 0X14 enable, 0X10 disable. */\nstatic const uint8_t SSD1306_CHARGEPUMP = 0x8D;\n/** Set Segment Re-map between data column and the segment driver. */\nstatic const uint8_t SSD1306_SEGREMAP = 0xA0;\n/** Resume display from GRAM content. */\nstatic const uint8_t SSD1306_DISPLAYALLON_RESUME = 0xA4;\n/** Force display on regardless of GRAM content. */\nstatic const uint8_t SSD1306_DISPLAYALLON = 0xA5;\n/** Set Normal Display. */\nstatic const uint8_t SSD1306_NORMALDISPLAY = 0xA6;\n/** Set Inverse Display. */\nstatic const uint8_t SSD1306_INVERTDISPLAY = 0xA7;\n/** Set Multiplex Ratio from 16 to 63. */\nstatic const uint8_t SSD1306_SETMULTIPLEX = 0xA8;\n/** Set Display off. */\nstatic const uint8_t SSD1306_DISPLAYOFF = 0xAE;\n/** Set Display on. */\nstatic const uint8_t SSD1306_DISPLAYON = 0xAF;\n/**Set GDDRAM Page Start Address. */\nstatic const uint8_t SSD1306_SETSTARTPAGE = 0xB0;\n/** Set COM output scan direction normal. */\nstatic const uint8_t SSD1306_COMSCANINC = 0xC0;\n/** Set COM output scan direction reversed. */\nstatic const uint8_t SSD1306_COMSCANDEC = 0xC8;\n/** Set Display Offset. */\nstatic const uint8_t SSD1306_SETDISPLAYOFFSET = 0xD3;\n/** Sets COM signals pin configuration to match the OLED panel layout. */\nstatic const uint8_t SSD1306_SETCOMPINS = 0xDA;\n/** This command adjusts the VCOMH regulator output. */\nstatic const uint8_t SSD1306_SETVCOMDETECT = 0xDB;\n/** Set Display Clock Divide Ratio/ Oscillator Frequency. */\nstatic const uint8_t SSD1306_SETDISPLAYCLOCKDIV = 0xD5;\n/** Set Pre-charge Period */\nstatic const uint8_t SSD1306_SETPRECHARGE = 0xD9;\n/** Deactivate scroll */\nstatic const uint8_t SSD1306_DEACTIVATE_SCROLL = 0x2E;\n/** No Operation Command. */\nstatic const uint8_t SSD1306_NOP = 0xE3;\n//------------------------------------------------------------------------------\n/** Set Pump voltage value: (30H~33H) 6.4, 7.4, 8.0 (POR), 9.0. */\nstatic const uint8_t SH1106_SET_PUMP_VOLTAGE = 0x30;\n/** First byte of set charge pump mode */\nstatic const uint8_t SH1106_SET_PUMP_MODE = 0xAD;\n/** Second byte charge pump on. */\nstatic const uint8_t SH1106_PUMP_ON = 0x8B;\n/** Second byte charge pump off. */\nstatic const uint8_t SH1106_PUMP_OFF = 0x8A;\n//------------------------------------------------------------------------------\n\n// Sequence of blank pixels, to optimise clearing screen.\n// Send a maximum of 30 pixels per transmission.\nconst uint8_t FLASH SSD1306AsciiWire::blankPixels[30] = \n  {0x40,        // First byte specifies data mode\n  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};  \n\n\n//==============================================================================\n// this section is based on https://github.com/adafruit/Adafruit_SSD1306\n\n/** Initialization commands for a 128x32 or 128x64 SSD1306 oled display. */\nconst uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {\n    // Init sequence for Adafruit 128x32/64 OLED module\n    0x00,                              // Set to command mode\n    SSD1306_DISPLAYOFF,\n    SSD1306_SETDISPLAYCLOCKDIV, 0x80,  // the suggested ratio 0x80 \n    SSD1306_SETMULTIPLEX, 0x3F,        // ratio 64 (initially)\n    SSD1306_SETDISPLAYOFFSET, 0x0,     // no offset\n    SSD1306_SETSTARTLINE | 0x0,        // line #0\n    SSD1306_CHARGEPUMP, 0x14,          // internal vcc\n    SSD1306_MEMORYMODE, 0x02,          // page mode\n    SSD1306_SEGREMAP | 0x1,            // column 127 mapped to SEG0\n    SSD1306_COMSCANDEC,                // column scan direction reversed\n    SSD1306_SETCOMPINS, 0X12,          // set COM pins\n    SSD1306_SETCONTRAST, 0x7F,         // contrast level 127\n    SSD1306_SETPRECHARGE, 0xF1,        // pre-charge period (1, 15)\n    SSD1306_SETVCOMDETECT, 0x40,       // vcomh regulator level\n    SSD1306_DISPLAYALLON_RESUME,\n    SSD1306_NORMALDISPLAY,\n    SSD1306_DISPLAYON\n};\n\n//------------------------------------------------------------------------------\n// This section is based on https://github.com/stanleyhuangyc/MultiLCD\n\n/** Initialization commands for a 128x64 SH1106 oled display. */\nconst uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {\n  0x00,                                  // Set to command mode\n  SSD1306_DISPLAYOFF,\n  SSD1306_SETDISPLAYCLOCKDIV, 0X80,      // set osc division\n  SSD1306_SETMULTIPLEX, 0x3F,            // ratio 64\n  SSD1306_SETDISPLAYOFFSET, 0X00,        // set display offset\n  SSD1306_SETSTARTPAGE | 0X0,            // set page address\n  SSD1306_SETSTARTLINE | 0x0,            // set start line\n  SH1106_SET_PUMP_MODE, SH1106_PUMP_ON,  // set charge pump enable\n  SSD1306_SEGREMAP | 0X1,                // set segment remap\n  SSD1306_COMSCANDEC,                    // Com scan direction\n  SSD1306_SETCOMPINS, 0X12,              // set COM pins\n  SSD1306_SETCONTRAST, 0x80,             // 128\n  SSD1306_SETPRECHARGE, 0X1F,            // set pre-charge period\n  SSD1306_SETVCOMDETECT,  0x40,          // set vcomh\n  SH1106_SET_PUMP_VOLTAGE | 0X2,         // 8.0 volts\n  SSD1306_NORMALDISPLAY,                 // normal / reverse\n  SSD1306_DISPLAYON\n};\n\n//==============================================================================\n// SSD1306AsciiWire Method Definitions\n//------------------------------------------------------------------------------\n \n// Auto-detect address\nSSD1306AsciiWire::SSD1306AsciiWire(int width, int height) \n  : SSD1306AsciiWire(0, width, height) { }\n\n// Constructor with explicit address\nSSD1306AsciiWire::SSD1306AsciiWire(I2CAddress address, int width, int height) {\n  m_i2cAddr = address;\n  m_displayWidth = width;\n  m_displayHeight = height;\n  // Set size in characters\n  m_charsPerColumn = m_displayHeight / fontHeight;\n  m_charsPerRow = (m_displayWidth+fontWidth-1) / fontWidth; // Round up\n}\n\nbool SSD1306AsciiWire::begin() {\n  I2CManager.begin();\n  I2CManager.setClock(400000L);  // Set max supported I2C speede\n\n  if (m_i2cAddr == 0) {\n    // Probe for I2C device on 0x3c and 0x3d.\n    for (uint8_t address = 0x3c; address <= 0x3d; address++) {\n      if (I2CManager.exists(address)) {\n        m_i2cAddr = address;\n        break;\n      }\n    }\n    if (m_i2cAddr == 0)\n      DIAG(F(\"OLED display not found\"));\n  }\n\n  m_col = 0;\n  m_row = 0;\n  m_colOffset = 0;\n\n  if (m_displayWidth==132 && m_displayHeight==64) {\n    // SH1106 display.  This uses 128x64 centered within a 132x64 OLED.\n    m_colOffset = 2;\n    I2CManager.write_P(m_i2cAddr, SH1106_132x64init, sizeof(SH1106_132x64init));\n  } else if (m_displayWidth==128 && (m_displayHeight==64 || m_displayHeight==32)) {\n    // SSD1306 or SSD1309 128x64 or 128x32\n    I2CManager.write_P(m_i2cAddr, Adafruit128xXXinit, sizeof(Adafruit128xXXinit));\n    if (m_displayHeight == 32) \n      I2CManager.write(m_i2cAddr, 5, 0, // Set command mode\n        SSD1306_SETMULTIPLEX, 0x1F,     // ratio 32\n        SSD1306_SETCOMPINS, 0x02);      // sequential COM pins, disable remap\n  } else {\n    DIAG(F(\"OLED configuration option not recognised\"));\n    return false;\n  }\n  // Device found\n  DIAG(F(\"%dx%d OLED display configured on I2C:%s\"), m_displayWidth, m_displayHeight, m_i2cAddr.toString());\n  return true;\n}\n\n/* Clear screen by writing blank pixels. */\nvoid SSD1306AsciiWire::clearNative() {\n  const int maxBytes = sizeof(blankPixels) - 1;  // max number of pixel columns (bytes) per transmission\n  for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) {\n    setRowNative(r);   // Position at start of row to be erased\n    for (uint8_t c = 0; c < m_displayWidth; c += maxBytes) {\n      uint8_t len = m_displayWidth-c;  // Number of pixel columns remaining\n      if (len > maxBytes) len = maxBytes;\n      I2CManager.write_P(m_i2cAddr, blankPixels, len+1);  // Write command + 'len' blank columns\n    }\n  }\n}\n\n//------------------------------------------------------------------------------\n\n// Set cursor position (by text line)\nvoid SSD1306AsciiWire::setRowNative(uint8_t line) {\n  // Calculate pixel position from line number\n  uint8_t row = line*8;\n  if (row < m_displayHeight) {\n    m_row = row;\n    m_col = m_colOffset;\n    // Before using buffer, wait for last request to complete\n    requestBlock.wait();\n    // Build output buffer for I2C\n    uint8_t len = 0;\n    outputBuffer[len++] = 0x00;  // Set to command mode\n    outputBuffer[len++] = SSD1306_SETLOWCOLUMN | (m_col & 0XF);\n    outputBuffer[len++] = SSD1306_SETHIGHCOLUMN | (m_col >> 4);\n    outputBuffer[len++] = SSD1306_SETSTARTPAGE | (m_row/8);\n    I2CManager.write(m_i2cAddr, outputBuffer, len, &requestBlock);\n  }\n}\n//------------------------------------------------------------------------------\n\n// Write a character to the OLED\nsize_t SSD1306AsciiWire::writeNative(uint8_t ch) {\n  const uint8_t* base = m_font;\n\n#if defined(NOLOWERCASE)\n  // Adjust if lowercase is missing\n  if (ch >= 'a') {\n    if (ch <= 'z')\n      ch = ch - 'a' + 'A';  // Capitalise\n    else\n      ch -= 26; // Allow for missing lowercase letters\n  }\n#endif\n  if (ch < m_fontFirstChar || ch >= (m_fontFirstChar + m_fontCharCount))\n    return 0;\n  // Check if character would be partly or wholly off the display\n  if (m_col + fontWidth > m_displayWidth)\n    return 0;\n\n  ch -= m_fontFirstChar;\n  base += fontWidth * ch;\n  // Before using buffer, wait for last request to complete\n  requestBlock.wait();\n  // Build output buffer for I2C\n  outputBuffer[0] = 0x40;     // set SSD1306 controller to data mode\n  uint8_t bufferPos = 1;\n  // Copy character pixel columns\n  for (uint8_t i = 0; i < fontWidth; i++) {\n    if (m_col++ < m_displayWidth) \n      outputBuffer[bufferPos++] = GETFLASH(base++);\n  }\n\n  // Write the data to I2C display\n  I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);\n  return 1;\n}\n\n\n//------------------------------------------------------------------------------\n\n// Font characters, 6x8 pixels, starting at 0x20.\n// Lower case characters optionally omitted.\nconst uint8_t FLASH SSD1306AsciiWire::System6x8[] = {\n\n    // Fixed width; char width table not used !!!!\n\n    // font data\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // (space) (20)\n    0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,  // ! (21)\n    0x00, 0x07, 0x00, 0x07, 0x00, 0x00,  // \"\n    0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00,  // #\n    0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00,  // $\n    0x23, 0x13, 0x08, 0x64, 0x62, 0x00,  // %\n    0x36, 0x49, 0x55, 0x22, 0x50, 0x00,  // &\n    0x00, 0x05, 0x03, 0x00, 0x00, 0x00,  // '\n    0x00, 0x1C, 0x22, 0x41, 0x00, 0x00,  // (\n    0x00, 0x41, 0x22, 0x1C, 0x00, 0x00,  // )\n    0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00,  // *\n    0x08, 0x08, 0x3E, 0x08, 0x08, 0x00,  // +\n    0x00, 0x50, 0x30, 0x00, 0x00, 0x00,  // ,\n    0x08, 0x08, 0x08, 0x08, 0x08, 0x00,  // -\n    0x00, 0x60, 0x60, 0x00, 0x00, 0x00,  // .\n    0x20, 0x10, 0x08, 0x04, 0x02, 0x00,  // / (47)\n    0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00,  // 0 (48)\n    0x00, 0x42, 0x7F, 0x40, 0x00, 0x00,  // 1\n    0x42, 0x61, 0x51, 0x49, 0x46, 0x00,  // 2\n    0x21, 0x41, 0x45, 0x4B, 0x31, 0x00,  // 3\n    0x18, 0x14, 0x12, 0x7F, 0x10, 0x00,  // 4\n    0x27, 0x45, 0x45, 0x45, 0x39, 0x00,  // 5\n    0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00,  // 6\n    0x01, 0x71, 0x09, 0x05, 0x03, 0x00,  // 7\n    0x36, 0x49, 0x49, 0x49, 0x36, 0x00,  // 8\n    0x06, 0x49, 0x49, 0x29, 0x1E, 0x00,  // 9 (57)\n    0x00, 0x36, 0x36, 0x00, 0x00, 0x00,  // :\n    0x00, 0x56, 0x36, 0x00, 0x00, 0x00,  // ;\n    0x00, 0x08, 0x14, 0x22, 0x41, 0x00,  // <\n    0x14, 0x14, 0x14, 0x14, 0x14, 0x00,  // =\n    0x41, 0x22, 0x14, 0x08, 0x00, 0x00,  // >\n    0x02, 0x01, 0x51, 0x09, 0x06, 0x00,  // ?\n    0x32, 0x49, 0x79, 0x41, 0x3E, 0x00,  // @ (64)\n    0x7E, 0x11, 0x11, 0x11, 0x7E, 0x00,  // A (65)\n    0x7F, 0x49, 0x49, 0x49, 0x36, 0x00,  // B\n    0x3E, 0x41, 0x41, 0x41, 0x22, 0x00,  // C\n    0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00,  // D\n    0x7F, 0x49, 0x49, 0x49, 0x41, 0x00,  // E\n    0x7F, 0x09, 0x09, 0x01, 0x01, 0x00,  // F\n    0x3E, 0x41, 0x41, 0x51, 0x32, 0x00,  // G\n    0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00,  // H\n    0x00, 0x41, 0x7F, 0x41, 0x00, 0x00,  // I\n    0x20, 0x40, 0x41, 0x3F, 0x01, 0x00,  // J\n    0x7F, 0x08, 0x14, 0x22, 0x41, 0x00,  // K\n    0x7F, 0x40, 0x40, 0x40, 0x40, 0x00,  // L\n    0x7F, 0x02, 0x04, 0x02, 0x7F, 0x00,  // M\n    0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00,  // N\n    0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00,  // O\n    0x7F, 0x09, 0x09, 0x09, 0x06, 0x00,  // P\n    0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00,  // Q\n    0x7F, 0x09, 0x19, 0x29, 0x46, 0x00,  // R\n    0x46, 0x49, 0x49, 0x49, 0x31, 0x00,  // S\n    0x01, 0x01, 0x7F, 0x01, 0x01, 0x00,  // T\n    0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00,  // U\n    0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00,  // V\n    0x7F, 0x20, 0x18, 0x20, 0x7F, 0x00,  // W\n    0x63, 0x14, 0x08, 0x14, 0x63, 0x00,  // X\n    0x03, 0x04, 0x78, 0x04, 0x03, 0x00,  // Y\n    0x61, 0x51, 0x49, 0x45, 0x43, 0x00,  // Z (90)\n    0x00, 0x00, 0x7F, 0x41, 0x41, 0x00,  // [\n    0x02, 0x04, 0x08, 0x10, 0x20, 0x00,  // \"\\\"\n    0x41, 0x41, 0x7F, 0x00, 0x00, 0x00,  // ]\n    0x04, 0x02, 0x01, 0x02, 0x04, 0x00,  // ^\n    0x40, 0x40, 0x40, 0x40, 0x40, 0x00,  // _\n    0x00, 0x01, 0x02, 0x04, 0x00, 0x00,  // ' (96)\n#ifndef NOLOWERCASE\n    0x20, 0x54, 0x54, 0x54, 0x78, 0x00,  // a (97)\n    0x7F, 0x48, 0x44, 0x44, 0x38, 0x00,  // b\n    0x38, 0x44, 0x44, 0x44, 0x20, 0x00,  // c\n    0x38, 0x44, 0x44, 0x48, 0x7F, 0x00,  // d\n    0x38, 0x54, 0x54, 0x54, 0x18, 0x00,  // e\n    0x08, 0x7E, 0x09, 0x01, 0x02, 0x00,  // f\n    0x08, 0x14, 0x54, 0x54, 0x3C, 0x00,  // g\n    0x7F, 0x08, 0x04, 0x04, 0x78, 0x00,  // h\n    0x00, 0x44, 0x7D, 0x40, 0x00, 0x00,  // i\n    0x20, 0x40, 0x44, 0x3D, 0x00, 0x00,  // j\n    0x00, 0x7F, 0x10, 0x28, 0x44, 0x00,  // k\n    0x00, 0x41, 0x7F, 0x40, 0x00, 0x00,  // l\n    0x7C, 0x04, 0x18, 0x04, 0x78, 0x00,  // m\n    0x7C, 0x08, 0x04, 0x04, 0x78, 0x00,  // n\n    0x38, 0x44, 0x44, 0x44, 0x38, 0x00,  // o\n    0x7C, 0x14, 0x14, 0x14, 0x08, 0x00,  // p\n    0x08, 0x14, 0x14, 0x18, 0x7C, 0x00,  // q\n    0x7C, 0x08, 0x04, 0x04, 0x08, 0x00,  // r\n    0x48, 0x54, 0x54, 0x54, 0x20, 0x00,  // s\n    0x04, 0x3F, 0x44, 0x40, 0x20, 0x00,  // t\n    0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00,  // u\n    0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00,  // v\n    0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00,  // w\n    0x44, 0x28, 0x10, 0x28, 0x44, 0x00,  // x\n    0x0C, 0x50, 0x50, 0x50, 0x3C, 0x00,  // y\n    0x44, 0x64, 0x54, 0x4C, 0x44, 0x00,  // z (122)\n#endif\n    0x00, 0x08, 0x36, 0x41, 0x00, 0x00,  // { (123)\n    0x00, 0x00, 0x7F, 0x00, 0x00, 0x00,  // |\n    0x00, 0x41, 0x36, 0x08, 0x00, 0x00,  // }\n    0x08, 0x08, 0x2A, 0x1C, 0x08, 0x00,  // ->\n    0x08, 0x1C, 0x2A, 0x08, 0x08, 0x00,  // <- (127)\n#ifndef NO_EXTENDED_CHARACTERS\n// Extended characters - based on \"DOS Western Europe\" characters\n//   International characters not yet implemented.\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented 0x80\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented 0x90\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x38, 0x44, 0xc6, 0x44, 0x20, 0x00,  // cent 0x9b\n    0x44, 0x6e, 0x59, 0x49, 0x62, 0x00,  // £ 0x9c\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented 0xa0\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x10, 0x28, 0x54, 0x28, 0x44, 0x00,  // <<\n    0x44, 0x28, 0x54, 0x28, 0x10, 0x00,  // >>\n    // Extended characters 176-180\n    0x92, 0x00, 0x49, 0x00, 0x24, 0x00,  // Light grey 0xb0\n    0xaa, 0x44, 0xaa, 0x11, 0xaa, 0x55,  // Mid grey 0xb1\n    0x6d, 0xff, 0xb6, 0xff, 0xdb, 0xff,  // Dark grey 0xb2\n    0x00, 0x00, 0x00, 0xff, 0x00, 0x00,  // Vertical line 0xb3\n    0x08, 0x08, 0x08, 0xff, 0x00, 0x00,  // Vertical line with left spur 0xb4\n\n    0x14, 0x14, 0x14, 0xff, 0x00, 0x00,  // Vertical line with double left spur 0xb5\n    0x08, 0x08, 0xff, 0x00, 0xff, 0x00,  // Double vertical line with single left spur\n    0x08, 0x08, 0xf8, 0x08, 0xf8, 0x00,  // Top right corner, single horiz, double vert\n    0x14, 0x14, 0x14, 0xfc, 0x00, 0x00,  // Top right corner, double horiz, single vert\n\n    // Extended characters 185-190\n    0x14, 0x14, 0xf7, 0x00, 0xff, 0x00,  // Double vertical line with double left spur 0xb9\n    0x00, 0x00, 0xff, 0x00, 0xff, 0x00,  // Double vertical line 0xba\n    0x14, 0x14, 0xf4, 0x04, 0xfc, 0x00,  // Double top right corner 0xbb\n    0x14, 0x14, 0x17, 0x10, 0x1f, 0x00,  // Double bottom right corner 0xbc\n    0x08, 0x08, 0x0f, 0x08, 0x0f, 0x00,  // Bottom right corner, single horiz, double vert 0xbd\n    0x14, 0x14, 0x14, 0x1f, 0x00, 0x00,  // Bottom right corner, double horiz, single vert 0xbe\n\n    // Extended characters 191-199\n    0x08, 0x08, 0x08, 0xf8, 0x00, 0x00,  // Top right corner 0xbf\n    0x00, 0x00, 0x00, 0x0f, 0x08, 0x08,  // Bottom left corner 0xc0\n    0x08, 0x08, 0x08, 0x0f, 0x08, 0x08,  // Horizontal line with upward spur 0xc1\n    0x08, 0x08, 0x08, 0xf8, 0x08, 0x08,  // Horizontal line with downward spur 0xc2\n    0x00, 0x00, 0x00, 0xff, 0x08, 0x08,  // Vertical line with right spur 0xc3\n    0x08, 0x08, 0x08, 0x08, 0x08, 0x08,  // Horizontal line 0xc4\n    0x08, 0x08, 0x08, 0xff, 0x08, 0x08,  // Cross 0xc5\n    0x00, 0x00, 0x00, 0xff, 0x14, 0x14,  // Vertical line double right spur 0xc6\n    0x00, 0x00, 0xff, 0x00, 0xff, 0x08,  // Double vertical line single right spur 0xc7\n\n    // Extended characters 200-206\n    0x00, 0x00, 0x1f, 0x10, 0x17, 0x14,  // Double bottom left corner 0xc8\n    0x00, 0x00, 0xfc, 0x04, 0xf4, 0x14,  // Double top left corner 0xc9\n    0x14, 0x14, 0x17, 0x10, 0x17, 0x14,  // Double horizontal with double upward spur 0xca\n    0x14, 0x14, 0xf4, 0x04, 0xf4, 0x14,  // Double horizontal with double downward spur 0xcb\n    0x00, 0x00, 0xff, 0x00, 0xf7, 0x14,  // Double vertical line with double right spur 0xcc\n    0x14, 0x14, 0x14, 0x14, 0x14, 0x14,  // Double horizontal line 0xcd\n    0x14, 0x14, 0xf7, 0x00, 0xf7, 0x14,  // Double cross 0xce\n\n    0x14, 0x14, 0x14, 0x17, 0x14, 0x14,  // Double horizontal line single upward spur 0xcf\n    0x08, 0x08, 0x0f, 0x08, 0x0f, 0x08,  // Horiz single line with double upward spur 0xd0\n    0x14, 0x14, 0x14, 0xf4, 0x14, 0x14,  // Horiz double line with single downward spur 0xd1\n    0x08, 0x08, 0xf8, 0x08, 0xf8, 0x08,  // Horiz single line with double downward spur 0xd2\n    0x00, 0x00, 0x0f, 0x08, 0x0f, 0x08,  // Bottom left corner, double vert single horiz 0xd3\n    0x00, 0x00, 0x00, 0x1f, 0x14, 0x14,  // Bottom left corner, single vert double horiz 0xd4\n    0x00, 0x00, 0x00, 0xfc, 0x14, 0x14,  // Top left corner, single vert double horiz 0xd5\n    0x00, 0x00, 0xf8, 0x08, 0xf8, 0x08,  // Top left corner, double vert single horiz 0xd6\n    0x08, 0x08, 0xff, 0x00, 0xff, 0x08,  // Cross, double vert single horiz 0xd7\n    0x14, 0x14, 0x14, 0xf7, 0x14, 0x14,  // Cross, single vert double horiz 0xd8\n\n    // Extended characters 217-223\n    0x08, 0x08, 0x08, 0x0f, 0x00, 0x00,  // Bottom right corner 0xd9\n    0x00, 0x00, 0x00, 0xf8, 0x08, 0x08,  // Top left corner 0xda\n    0xff, 0xff, 0xff, 0xff, 0xff, 0xff,  // Solid block 0xdb\n    0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,  // Bottom half block 0xdc\n    0xff, 0xff, 0xff, 0x00, 0x00, 0x00,  // Left half block 0xdd\n    0x00, 0x00, 0x00, 0xff, 0xff, 0xff,  // Right half block 0xde\n    0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,  // Top half block 0xdf\n\n    0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00,  // Bottom Left block 0xe0\n    0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0,  // Bottom Right block\n    0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00,  // Top left block \n    0x00, 0x00, 0x00, 0x0f, 0x0f, 0x0f,  // Top right block 0xe3\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented 0xf0\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Not implemented\n    // Extended character 248\n    0x00, 0x06, 0x09, 0x09, 0x06, 0x00   // degree symbol 0xf8\n#endif\n};\n\nconst uint8_t SSD1306AsciiWire::m_fontCharCount = sizeof(System6x8) / 6;\n"
  },
  {
    "path": "SSD1306Ascii.h",
    "content": "/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman\n * Modifications (C) 2021 Neil McKechnie\n *\n *  This file is part of CommandStation-EX\n *\n * This Library is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This Library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this software.  If not, see\n * <http://www.gnu.org/licenses/>.\n */\n\n#ifndef SSD1306Ascii_h\n#define SSD1306Ascii_h\n\n#include \"Arduino.h\"\n#include \"FSH.h\"\n#include \"Display.h\"\n\n#include \"I2CManager.h\"\n#include \"DIAG.h\"\n#include \"DisplayInterface.h\"\n\n// Uncomment to remove lower-case letters to save 108 bytes of flash\n//#define NOLOWERCASE\n\n\n\n//------------------------------------------------------------------------------\n// Constructor\nclass SSD1306AsciiWire : public DisplayDevice {\n public:\n\n  // Constructors\n  SSD1306AsciiWire(int width, int height); // Auto-detects I2C address\n  SSD1306AsciiWire(I2CAddress address, int width, int height);  \n\n  // Initialize the display controller.\n  bool begin();\n\n  // Clear the display and set the cursor to (0, 0).\n  void clearNative() override;\n\n  // Set cursor to start of specified text line\n  void setRowNative(byte line) override;\n  \n  // Write one character to OLED\n  size_t writeNative(uint8_t c) override;\n\n  bool isBusy() override { return requestBlock.isBusy(); }\n  uint16_t getNumCols() { return m_charsPerRow; }\n  uint16_t getNumRows() { return m_charsPerColumn; }\n\n private:\n  // Cursor column.\n  uint8_t m_col;\n  // Cursor RAM row.\n  uint8_t m_row;\n  // Display width.\n  uint8_t m_displayWidth;\n  // Display height.\n  uint8_t m_displayHeight;\n  // Display width in characters\n  uint8_t m_charsPerRow;\n  // Display height in characters\n  uint8_t m_charsPerColumn;\n  // Column offset RAM to SEG.\n  uint8_t m_colOffset = 0;\n  // Current font.\n  const uint8_t* const m_font = System6x8;\n  // Flag to prevent calling begin() twice\n  uint8_t m_initialised = false;\n\n  // Only fixed size 6x8 fonts in a 6x8 cell are supported.\n  static const uint8_t fontWidth = 6;\n  static const uint8_t fontHeight = 8;\n  static const uint8_t m_fontFirstChar = 0x20;\n  static const uint8_t m_fontCharCount;\n\n  I2CAddress m_i2cAddr = 0;\n\n  I2CRB requestBlock;\n  uint8_t outputBuffer[fontWidth+1];\n\n  static const uint8_t blankPixels[];\n\n  static const uint8_t System6x8[];\n  static const uint8_t FLASH Adafruit128xXXinit[];\n  static const uint8_t FLASH SH1106_132x64init[];\n};\n\n#endif  // SSD1306Ascii_h\n"
  },
  {
    "path": "STM32lwipopts.h.copyme",
    "content": "/*\n *  © 2024 Harald Barth\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n//\n// Rewrite of the STM32lwipopts.h file from STM\n// To be copied into where lwipopts_default.h resides\n// typically into STM32Ethernet/src/STM32lwipopts.h\n// or STM32Ethernet\\src\\STM32lwipopts.h\n// search for `lwipopts_default.h` and copy this file into the\n// same directory but name it STM32lwipopts.h\n//\n#ifndef __STM32LWIPOPTS_H__\n#define __STM32LWIPOPTS_H__\n\n// include this here and then override things we do differnet\n#include \"lwipopts_default.h\"\n\n// we can not include our \"defines.h\" here\n// so we need to duplicate that define\n#define MAX_NUM_TCP_CLIENTS_HERE 9\n\n#ifdef MAX_NUM_TCP_CLIENTS\n #if MAX_NUM_TCP_CLIENTS != MAX_NUM_TCP_CLIENTS_HERE\n  #error MAX_NUM_TCP_CLIENTS and MAX_NUM_TCP_CLIENTS_HERE must be same\n #endif\n#else\n #define MAX_NUM_TCP_CLIENTS MAX_NUM_TCP_CLIENTS_HERE\n#endif\n\n// increase ARP cache\n#undef  MEMP_NUM_APR_QUEUE\n#define MEMP_NUM_ARP_QUEUE MAX_NUM_TCP_CLIENTS+3 // one for each client (all on different HW) and a few extra\n\n// Example for debug\n//#define LWIP_DEBUG                      1\n//#define TCP_DEBUG                       LWIP_DBG_ON\n\n// NOT STRICT NECESSARY ANY MORE BUT CAN BE USED TO SAVE RAM\n#undef  MEM_LIBC_MALLOC\n#define MEM_LIBC_MALLOC         1       // use the same malloc as for everything else\n#undef  MEMP_MEM_MALLOC\n#define MEMP_MEM_MALLOC         1       // uses malloc which means no pools which means slower but not mean 32KB up front\n\n#undef  MEMP_NUM_TCP_PCB\n#define MEMP_NUM_TCP_PCB        MAX_NUM_TCP_CLIENTS+1 // one extra so we can reject number N+1 from our code\n#define MEMP_NUM_TCP_PCB_LISTEN 6\n\n#undef  MEMP_NUM_TCP_SEG\n#define MEMP_NUM_TCP_SEG        MAX_NUM_TCP_CLIENTS\n\n#undef  MEMP_NUM_SYS_TIMEOUT\n#define MEMP_NUM_SYS_TIMEOUT    MAX_NUM_TCP_CLIENTS+2\n\n#undef  PBUF_POOL_SIZE\n#define PBUF_POOL_SIZE          MAX_NUM_TCP_CLIENTS\n\n#undef  LWIO_ICMP\n#define LWIP_ICMP                       1\n#undef  LWIP_RAW\n#define LWIP_RAW                        1 /* PING changed to 1 */\n#undef  DEFAULT_RAW_RECVMBOX_SIZE\n#define DEFAULT_RAW_RECVMBOX_SIZE       3 /* for ICMP PING */\n\n#undef  LWIP_DHCP\n#define LWIP_DHCP               1\n#undef  LWIP_UDP\n#define LWIP_UDP                1\n\n/*\nThe STM32F4x7 allows computing and verifying the IP, UDP, TCP and ICMP checksums by hardware:\n - To use this feature let the following define uncommented.\n - To disable it and process by CPU comment the  the checksum.\n*/\n\n#if CHECKSUM_GEN_TCP == 1\n#error On STM32 TCP checksum should be in HW\n#endif\n\n#undef  LWIP_IGMP\n#define LWIP_IGMP       1\n\n//#define SO_REUSE 1\n//#define SO_REUSE_RXTOALL 1\n\n#endif /* __STM32LWIPOPTS_H__ */\n"
  },
  {
    "path": "SensorGroup.cpp",
    "content": "#include \"SensorGroup.h\"\n#include \"CommandDistributor.h\"\n\n#ifdef EXRAIL_ACTIVE\n\n// called in loop to check sensors\nvoid SensorGroup::checkAll() {\n    doExrailSensorGroup(GroupProcess::check, & USB_SERIAL);\n}\n\n// called by command to get sensor list   \nvoid SensorGroup::printAll(Print * serial) {\n    (void)serial; // suppress unused warning\n    doExrailSensorGroup(GroupProcess::print,serial);\n}\n\nvoid SensorGroup::prepareAll() {\n    doExrailSensorGroup(GroupProcess::prepare, & USB_SERIAL);\n}\n\nvoid SensorGroup::dumpAll(Print * stream) {\n    doExrailSensorGroup(GroupProcess::dump, stream);\n}\n\n#else\n// if EXRAIL is not active, these functions are empty\nvoid SensorGroup::checkAll() {}\nvoid SensorGroup::printAll(Print * serial) {(void)serial;}\nvoid SensorGroup::prepareAll() {}\nvoid SensorGroup::dumpAll(Print * stream) {(void)stream;}\n#endif \n\n// called by EXRAIL constructed doExrailSensorGroup for each group \nvoid SensorGroup::doSensorGroup(VPIN firstVpin, int nPins, byte* statebits,\n  GroupProcess action, Print * serial, bool pullup) {\n  \n  // Loop through the pins in the group  \n  for (auto i=0;i<nPins;i++) {   \n    // locate position of state bit\n    byte stateByte=i/8;\n    byte stateMask=1<<(i%8);\n    VPIN vpin= firstVpin+i;\n    switch(action) {\n      case GroupProcess::prepare:\n          IODevice::configureInput(vpin, pullup);\n          if (IODevice::read(vpin))  statebits[stateByte]|=stateMask;\n          break; \n    \n      case GroupProcess::check:\n         // check for state unchanged\n         if ((bool)(statebits[stateByte]&stateMask) == IODevice::read(vpin)) break; // no change  \n         // flip state bit\n         statebits[stateByte]^=stateMask;\n         CommandDistributor::broadcastSensor(vpin,statebits[stateByte]&stateMask);\n         break;\n      \n      case GroupProcess::print:\n        StringFormatter::send(serial, F(\"<%c %d>\\n\"), \n         (statebits[stateByte]&stateMask)?'Q':'q', vpin);\n         break;\n\n      case GroupProcess::dump:\n        StringFormatter::send(serial, F(\"<Q %d %d %c>\\n\"), \n         vpin, vpin, pullup?'1':'0');\n         break;\n    } \n  }\n}\n    "
  },
  {
    "path": "SensorGroup.h",
    "content": "#ifndef SensorGroup_h\n#define SensorGroup_h\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"IODevice.h\"\n#include \"StringFormatter.h\"\n\n\n// reference to the optional exrail built function which contains the \n// calls to SensorGroup::doSensorGroup \n\nenum GroupProcess:byte {prepare,print,check,dump};\n\nclass SensorGroup {\n    public:\n     static void checkAll();\n     static void printAll(Print * serial);\n     static void prepareAll();\n     static void dumpAll(Print* serial);\n\n     // doSensorGroup is called from the automatically \n     // built doExrailSensorGroup, once for each user defined group.\n     static void doSensorGroup(VPIN vpin, int nPins, byte* statebits,\n        GroupProcess action, Print * serial, bool pullup=false);\n     private: \n       static void doExrailSensorGroup(GroupProcess action, Print * stream);  \n};\n#endif // SensorGroup_h\n"
  },
  {
    "path": "Sensors.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n/**********************************************************************\n\nDCC++ BASE STATION supports Sensor inputs that can be connected to any Arduino Pin\nnot in use by this program.  Sensors can be of any type (infrared, magentic, mechanical...).\nThe only requirement is that when \"activated\" the Sensor must force the specified Arduino\nPin LOW (i.e. to ground), and when not activated, this Pin should remain HIGH (e.g. 5V),\nor be allowed to float HIGH if use of the Arduino Pin's internal pull-up resistor is specified.\n\nTo ensure proper voltage levels, some part of the Sensor circuitry\nMUST be tied back to the same ground as used by the Arduino.\n\nThe Sensor code below utilises \"de-bounce\" logic to remove spikes generated by\nmechanical switches and transistors.  This avoids the need to create smoothing circuitry\nfor each sensor.  You may need to change the parameters through trial and error for your specific sensors.\n\nTo have this sketch monitor one or more Arduino pins for sensor triggers, first define/edit/delete\nsensor definitions using the following variation of the \"S\" command:\n\n  <S ID PIN PULLUP>:           creates a new sensor ID, with specified PIN and PULLUP\n                               if sensor ID already exists, it is updated with specificed PIN and PULLUP\n                               returns: <O> if successful and <X> if unsuccessful (e.g. out of memory)\n\n  <S ID>:                      deletes definition of sensor ID\n                               returns: <O> if successful and <X> if unsuccessful (e.g. ID does not exist)\n\n  <S>:                         lists all defined sensors\n                               returns: <Q ID PIN PULLUP> for each defined sensor or <X> if no sensors defined\n\nwhere\n\n  ID: the numeric ID (0-32767) of the sensor\n  PIN: the arduino pin number the sensor is connected to\n  PULLUP: 1=use internal pull-up resistor for PIN, 0=don't use internal pull-up resistor for PIN\n\nOnce all sensors have been properly defined, use the <E> command to store their definitions to EEPROM.\nIf you later make edits/additions/deletions to the sensor definitions, you must invoke the <E> command if you want those\nnew definitions updated in the EEPROM.  You can also clear everything stored in the EEPROM by invoking the <e> command.\n\nAll sensors defined as per above are repeatedly and sequentially checked within the main loop of this sketch.\nIf a Sensor Pin is found to have transitioned from one state to another, one of the following serial messages are generated:\n\n  <Q ID>     - for transition of Sensor ID from HIGH state to LOW state (i.e. the sensor is triggered)\n  <q ID>     - for transition of Sensor ID from LOW state to HIGH state (i.e. the sensor is no longer triggered)\n\nDepending on whether the physical sensor is acting as an \"event-trigger\" or a \"detection-sensor,\" you may\ndecide to ignore the <q ID> return and only react to <Q ID> triggers.\n\n**********************************************************************/\n\n#include \"StringFormatter.h\"\n#include \"CommandDistributor.h\"\n#include \"Sensors.h\"\n#ifndef DISABLE_EEPROM\n#include \"EEStore.h\"\n#endif\n#include \"IODevice.h\"\n\n\n///////////////////////////////////////////////////////////////////////////////\n// checks a number of defined sensors per entry and prints _changed_ sensor state\n// to stream unless stream is NULL in which case only internal\n// state is updated. Then advances to next sensor which will\n// be checked at next invocation.  Each cycle of reading all sensors will\n// be initiated no more frequently than the time set by 'cycleInterval' microseconds.\n//\n// The list of sensors is divided such that the first part of the list\n// contains sensors that support change notification via callback, and the second\n// part of the list contains sensors that require cyclic polling.  The start of the\n// second part of the list is determined from by the 'firstPollSensor' pointer.\n///////////////////////////////////////////////////////////////////////////////\n\nvoid Sensor::checkAll(){\n\n  SensorGroup::checkAll();\n  \n  uint16_t sensorCount = 0;\n\n#ifdef USE_NOTIFY\n  // Register the event handler ONCE!\n  if (!inputChangeCallbackRegistered)\n    IONotifyCallback::add(inputChangeCallback);\n  inputChangeCallbackRegistered = true;\n#endif\n\n  if (firstSensor == NULL) return;  // No sensors to be scanned\n  if (readingSensor == NULL) { \n    // Not currently scanning sensor list\n    unsigned long thisTime = micros();\n    if (thisTime - lastReadCycle >= cycleInterval) {\n      // Required time elapsed since last read cycle started,\n      // so initiate new scan through the sensor list\n      readingSensor = firstSensor;\n      lastReadCycle = thisTime;\n    }\n  }\n\n  // Loop until either end of list is encountered or we pause for some reason\n  bool pause = false;\n  while (readingSensor != NULL && !pause) {\n\n    // Where the sensor is attached to a pin, read pin status.  For sources such as LCN,\n    // which don't have an input pin to read, the LCN class calls setState() to update inputState when\n    // a message is received.  The IODevice::read() call returns 1 for active pins (0v) and 0 for inactive (5v).\n    // Also, on HAL drivers that support change notifications, the driver calls the notification callback\n    // routine when an input signal change is detected, and this updates the inputState directly,\n    // so these inputs don't need to be polled here.\n    VPIN pin = readingSensor->data.pin;\n    if (readingSensor->pollingRequired && pin != VPIN_NONE)\n      readingSensor->inputState = IODevice::read(pin);\n\n    // Check if changed since last time, and process changes.\n    if (readingSensor->inputState == readingSensor->active) {\n      // no change\n      readingSensor->latchDelay = minReadCount; // Reset counter\n    } else if (readingSensor->latchDelay > 0) {\n      // change detected, but first decrement delay\n      readingSensor->latchDelay--;\n    } else { \n      // change validated, act on it.\n      readingSensor->active = readingSensor->inputState;\n      readingSensor->latchDelay = minReadCount;  // Reset counter\n      \n      CommandDistributor::broadcastSensor(readingSensor->data.snum,readingSensor->active);\n      pause = true;  // Don't check any more sensors on this entry\n    }\n\n    // Move to next sensor in list.\n    readingSensor = readingSensor->nextSensor;\n\n    // Currently process max of 16 sensors per entry.\n    // Performance measurements taken during development indicate that, with 128 sensors configured\n    // on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices\n    // within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.\n    sensorCount++;\n    if (sensorCount >= 16) pause = true;\n  }\n\n} // Sensor::checkAll\n\n\n#ifdef USE_NOTIFY\n// Callback from HAL (IODevice class) when a digital input change is recognised.\n// Updates the inputState field, which is subsequently scanned for changes in the checkAll \n// method.  Ideally the <Q>/<q> message should be sent from here, instead of waiting for\n// the checkAll method, but the output stream is not available at this point.\nvoid Sensor::inputChangeCallback(VPIN vpin, int state) {\n  Sensor *tt;\n  // This bit is not ideal since it has, potentially, to look through the entire list of\n  // sensors to find the one that has changed.  Ideally this should be improved somehow.\n  for (tt=firstSensor; tt!=NULL ; tt=tt->nextSensor) {\n    if (tt->data.pin == vpin) break;\n  }\n  if (tt != NULL) { // Sensor found\n    tt->inputState = (state != 0); \n  }\n}\n#endif\n\n///////////////////////////////////////////////////////////////////////////////\n//\n// prints all sensor states to stream\n//\n///////////////////////////////////////////////////////////////////////////////\n\nvoid Sensor::printAll(Print *stream){\n  \n  if (stream == NULL) return; // Nothing to do\n    \n  SensorGroup::printAll(stream);\n  for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){\n    StringFormatter::send(stream, F(\"<%c %d>\\n\"), tt->active ? 'Q' : 'q', tt->data.snum);\n  }\n} \n\nvoid Sensor::dumpAll(Print *stream){\n  \n  if (stream == NULL) return; // Nothing to do\n    \n  SensorGroup::dumpAll(stream);\n  for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){\n    StringFormatter::send(stream, F(\"<Q %d %d %d>\\n\"),\n      tt->data.snum, tt->data.pin,tt->data.pullUp);\n  }\n} // Sensor::dumpAll\n\n///////////////////////////////////////////////////////////////////////////////\n// Static Function to create/find Sensor object.\n\nSensor *Sensor::create(int snum, VPIN pin, int pullUp){\n  Sensor *tt;\n\n  if (pin > VPIN_MAX && pin != VPIN_NONE) return NULL;\n\n  remove(snum);  // Unlink and free any existing sensor with the same id, before creating the new one.\n\n  tt = (Sensor *)calloc(1,sizeof(Sensor));\n  if (!tt) return tt;     // memory allocation failure\n\n  if (pin == VPIN_NONE) \n    tt->pollingRequired = false;\n  #ifdef USE_NOTIFY\n  else if (IODevice::hasCallback(pin)) \n    tt->pollingRequired = false;\n  #endif\n  else \n    tt->pollingRequired = true;\n\n  // Add to the start of the list\n  tt->nextSensor = firstSensor;\n  firstSensor = tt;\n\n  tt->data.snum = snum;\n  tt->data.pin = pin;\n  tt->data.pullUp = pullUp;\n  tt->active = 0;\n  tt->inputState = 0;\n  tt->latchDelay = minReadCount;\n\n  if (pin != VPIN_NONE) \n    IODevice::configureInput(pin, pullUp);   \n    // Generally, internal pull-up resistors are not, on their own, sufficient \n    // for external infrared sensors --- each sensor must have its own 1K external pull-up resistor\n\n  return tt;\n}\n\n// Creet multiple eponymous sensors based on vpin alone. \nvoid Sensor::createMultiple(VPIN firstPin, byte count) {\n  for (byte i=0;i<count;i++) {\n    create(firstPin+i,firstPin+i,1); \n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Object method to directly change the input state, for sensors such as LCN which are updated\n//  by means other than by polling an input.\n\nvoid Sensor::setState(int value) {\n  // Trigger sensor change to be reported on next checkAll loop.\n  inputState = (value != 0);\n  latchDelay = 0; // Don't wait for anti-jitter logic\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nSensor* Sensor::get(int n){\n  Sensor *tt;\n  for(tt=firstSensor;tt!=NULL && tt->data.snum!=n;tt=tt->nextSensor);\n  return tt ;\n}\n///////////////////////////////////////////////////////////////////////////////\n\nbool Sensor::remove(int n){\n  Sensor *tt,*pp=NULL;\n\n  for(tt=firstSensor;tt!=NULL && tt->data.snum!=n;pp=tt,tt=tt->nextSensor);\n\n  if (tt==NULL)  return false;\n\n  // Unlink the sensor from the list\n  if(tt==firstSensor) \n    firstSensor=tt->nextSensor;\n  else \n    pp->nextSensor=tt->nextSensor;\n#ifdef USE_NOTIFY\n  if (tt==lastSensor)\n    lastSensor = pp;\n  if (tt==firstPollSensor)\n    firstPollSensor = tt->nextSensor;\n#endif\n\n  // Check if the sensor being deleted is the next one to be read.  If so, \n  // make the following one the next one to be read.\n  if (readingSensor==tt) readingSensor=tt->nextSensor;\n\n  free(tt);\n\n  return true;\n}\n\n///////////////////////////////////////////////////////////////////////////////\n#ifndef DISABLE_EEPROM\nvoid Sensor::load(){\n  struct SensorData data;\n  Sensor *tt;\n\n  uint16_t i=EEStore::eeStore->data.nSensors;\n  while(i--){\n    EEPROM.get(EEStore::pointer(),data);\n    tt=create(data.snum, data.pin, data.pullUp);\n    EEStore::advance(sizeof(tt->data));\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nvoid Sensor::store(){\n  Sensor *tt;\n\n  tt=firstSensor;\n  EEStore::eeStore->data.nSensors=0;\n\n  while(tt!=NULL){\n    EEPROM.put(EEStore::pointer(),tt->data);\n    EEStore::advance(sizeof(tt->data));\n    tt=tt->nextSensor;\n    EEStore::eeStore->data.nSensors++;\n  }\n}\n#endif\n///////////////////////////////////////////////////////////////////////////////\n\nSensor *Sensor::firstSensor=NULL;\nSensor *Sensor::readingSensor=NULL;\nunsigned long Sensor::lastReadCycle=0;\n\n#ifdef USE_NOTIFY\nSensor *Sensor::firstPollSensor = NULL;\nSensor *Sensor::lastSensor = NULL;\nbool Sensor::inputChangeCallbackRegistered = false;\n#endif\n"
  },
  {
    "path": "Sensors.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef Sensor_h\n#define Sensor_h\n\n#include \"Arduino.h\"\n#include \"IODevice.h\"\n#include \"SensorGroup.h\"\n\n// Uncomment the following #define statement to use callback notification\n//  where the driver supports it.\n//  The principle of callback notification is to avoid the Sensor class\n//  having to poll the device driver cyclically for input values, and then scan \n//  for changes.  Instead, when the driver scans the inputs, if it detects\n//  a change it invokes a callback function in the Sensor class.  In the current\n//  implementation, the advantages are limited because (a) the Sensor class \n//  performs debounce checks, and (b) the Sensor class does not have a \n//  static reference to the output stream for sending <Q>/<q> messages\n//  when a change is detected.  These restrictions mean that the checkAll() \n//  method still has to iterate through all of the Sensor objects looking \n//  for changes.\n#define USE_NOTIFY\n\nstruct SensorData {\n  int snum;\n  VPIN pin;\n  uint8_t pullUp;\n};\n\nclass Sensor{\n  // The sensor list is a linked list where each sensor's 'nextSensor' field points to the next.\n  //   The pointer is null in the last on the list.\n\npublic:\n  SensorData data;\n  struct {\n    uint8_t active:1;\n    uint8_t inputState:1;\n    uint8_t latchDelay:6;\n  };   // bit 7=active; bit 6=input state; bits 5-0=latchDelay\n\n  static Sensor *firstSensor;\n#ifdef USE_NOTIFY\n  static Sensor *firstPollSensor;\n  static Sensor *lastSensor;\n#endif\n  // readingSensor points to the next sensor to be polled, or null if the poll cycle is completed for\n  // the period.\n  static Sensor *readingSensor;\n\n  // Constructor\n  Sensor(); \n  Sensor *nextSensor;\n\n  void setState(int state);\n#ifndef DISABLE_EEPROM\n  static void load();\n  static void store();\n#endif\n  static Sensor *create(int id, VPIN vpin, int pullUp);\n  static void createMultiple(VPIN firstPin, byte count=1);\n  static Sensor* get(int id);  \n  static bool remove(int id);  \n  static void checkAll();\n  static void printAll(Print *stream);\n  static void dumpAll(Print* stream);\n  \n  static unsigned long lastReadCycle; // value of micros at start of last read cycle\n  static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.\n                                                   // should not be less than device scan cycle time.\n  static const unsigned int minReadCount = 1; // number of additional scans before acting on change\n                                        // E.g. 1 means that a change is ignored for one scan and actioned on the next.\n                                        // Max value is 63\n  bool pollingRequired = true;\n\n#ifdef USE_NOTIFY\n  static void inputChangeCallback(VPIN vpin, int state);\n  static bool inputChangeCallbackRegistered;\n#endif\n  \n}; // Sensor\n\n#endif\n"
  },
  {
    "path": "SerialManager.cpp",
    "content": " /*\n *  © 2022 Paul M. Antoine\n *  © 2021 Chris Harlow\n *  © 2022 2024 Harald Barth\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"SerialManager.h\"\n#include \"DCCEXParser.h\"\n#include \"StringFormatter.h\"\n#include \"DIAG.h\"\n\n#ifdef ARDUINO_ARCH_ESP32\n#ifdef SERIAL_BT_COMMANDS\n#include <BluetoothSerial.h>\n//#include <BleSerial.h>\n#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)\n#error No Bluetooth library available\n#endif //ENABLED\nBluetoothSerial SerialBT;\n//BleSerial SerialBT;\n#endif //COMMANDS\n#endif //ESP32\n\nstatic const byte PAYLOAD_FALSE = 0;\nstatic const byte PAYLOAD_NORMAL = 1;\nstatic const byte PAYLOAD_STRING = 2;\n\nSerialManager * SerialManager::first=NULL;\n\nSerialManager::SerialManager(Stream * myserial) {\n  serial=myserial;\n  next=first;\n  first=this;\n  bufferLength=0;\n  inCommandPayload=PAYLOAD_FALSE; \n} \n\nvoid SerialManager::init() {\n  USB_SERIAL.begin(115200);\n  while (!USB_SERIAL && millis() < 5000); // wait max 5s for Serial to start\n  new SerialManager(&USB_SERIAL);\n  \n#ifdef SERIAL6_COMMANDS\n  Serial6.begin(115200);\n  new SerialManager(&Serial6);\n#endif\n#ifdef SERIAL5_COMMANDS\n  Serial5.begin(115200);\n  new SerialManager(&Serial5);\n#endif\n#ifdef SERIAL4_COMMANDS\n  Serial4.begin(115200);\n  new SerialManager(&Serial4);\n#endif\n#ifdef SERIAL3_COMMANDS\n  Serial3.begin(115200);\n  new SerialManager(&Serial3);\n#endif\n#ifdef SERIAL2_COMMANDS\n#ifdef ARDUINO_ARCH_ESP32\n  Serial2.begin(115200, SERIAL_8N1, 16, 17); // GPIO 16 RXD2; GPIO 17 TXD2 on ESP32\n#else  // not ESP32\n  Serial2.begin(115200);\n#endif // ESP32\n  new SerialManager(&Serial2);\n#endif\n#ifdef SERIAL1_COMMANDS\n  Serial1.begin(115200);\n  new SerialManager(&Serial1);\n#endif\n#ifdef SERIAL_BT_COMMANDS\n  {\n    //SerialBT.setPin(\"6666\"); // choose other pin\n    uint64_t chipid = ESP.getEfuseMac();\n    char idstr[16] = {0};\n    snprintf(idstr, 15, \"DCCEX-%08X\",\n\t     __builtin_bswap32((uint32_t)(chipid>>16)));\n    SerialBT.begin(idstr);\n    new SerialManager(&SerialBT);\n    delay(1000);\n  }\n#endif\n#ifdef SABERTOOTH\n#ifdef ARDUINO_ARCH_ESP32\n  Serial2.begin(9600, SERIAL_8N1, 16, 17); // GPIO 16 RXD2; GPIO 17 TXD2 on ESP32\n#else\n  Serial2.begin(9600);\n#endif\n#endif\n}\n\nvoid SerialManager::broadcast(char * stringBuffer) {\n    for (SerialManager * s=first;s;s=s->next) s->broadcast2(stringBuffer);\n}\nvoid SerialManager::broadcast2(char * stringBuffer) {\n    serial->print(stringBuffer);\n}\n\nvoid SerialManager::loop() {\n    for (SerialManager * s=first;s;s=s->next) s->loop2();\n}\n\nvoid SerialManager::loop2() {\n  while (serial->available()) {\n    char ch = serial->read();\n    if (!inCommandPayload) {\n      if (ch == '<') {\n        inCommandPayload = PAYLOAD_NORMAL;\n        bufferLength = 0;\n        buffer[0] = '\\0';\n      }\n    } else { // if (inCommandPayload)\n      if (bufferLength <  (COMMAND_BUFFER_SIZE-1)) {\n        buffer[bufferLength++] = ch;          // advance bufferLength\n\tif (inCommandPayload > PAYLOAD_NORMAL) {\n\t  if (inCommandPayload > COMMAND_BUFFER_SIZE/2) { // String way too long, see SerialManager.h\n\t    ch = '>';                                     // we end this nonsense\n\t    inCommandPayload = PAYLOAD_NORMAL;\n\t    DIAG(F(\"Parse error: Unbalanced string\"));\n\t    // fall through to ending parsing below\n\t  } else if (ch == '\"') {               // String end\n\t    inCommandPayload = PAYLOAD_NORMAL;\n\t    continue; // do not fall through\n\t  } else\n\t    inCommandPayload++;\n\t}\n\tif (inCommandPayload == PAYLOAD_NORMAL) {\n\t  if (ch == '>') {\n\t    buffer[bufferLength] = '\\0';               // This \\0 is after the '>'\n\t    DCCEXParser::parse(serial, buffer, NULL);  // buffer parsed with trailing '>'\n\t    inCommandPayload = PAYLOAD_FALSE;\n\t    break;\n\t  } else if (ch == '\"') {\n\t    inCommandPayload = PAYLOAD_STRING;\n\t  }\n\t}\n      } else {\n\tDIAG(F(\"Parse error: input buffer overflow\"));\n\tinCommandPayload = PAYLOAD_FALSE;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "SerialManager.h",
    "content": " /*\n *  © 2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef SerialManager_h\n#define SerialManager_h\n\n#include \"Arduino.h\"\n#include \"defines.h\"\n\n\n// We size this to keep max two commands of maximum lenght\n// in the COMMAND_BUFFER. Most commands are shorter, the\n// <C WIFI SSID PASSWORD> can be 106 bytes. We do not\n// support that on AVR\n// AVR:   32bytes+32bytes\n//        <foo  ><bar   >\n// Other: 128bytes+128bytes\n//        <fooo   ><baar  >\n#ifndef COMMAND_BUFFER_SIZE\n#ifdef ARDUINO_ARCH_AVR\n #define COMMAND_BUFFER_SIZE 64\n#else\n #define COMMAND_BUFFER_SIZE 256\n#endif\n#endif\n\nclass SerialManager {\npublic:\n  static void init();\n  static void loop();\n  static void broadcast(char * stringBuffer);\n  \nprivate:  \n  static SerialManager * first;\n  SerialManager(Stream * myserial);\n  void loop2();\n  void broadcast2(char * stringBuffer);\n  Stream * serial;\n  SerialManager * next;\n  byte bufferLength;\n  byte buffer[COMMAND_BUFFER_SIZE]; \n  byte inCommandPayload;\n};\n#endif\n"
  },
  {
    "path": "SerialUsbLog.cpp",
    "content": "/*\n *  © 2026 Chris Harlow\n *  © 2026 Paul M. Antoine\n *  All rights reserved.\n *\n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/*\n * SerialUsbLog.cpp\n *\n * This code acts as a serial output log collector.\n * This is pointed to by defines.h as the USB_SERIAL object so that the remainder\n * of the code does not have to be aware of the difference.\n *\n * All output to the serial log is collected in a rolling buffer that can be:\n *  - dumped as a one-shot stream (streamOut)\n *  - or streamed incrementally via a lightweight HTTP endpoint\n *\n * ESP32 Web UI improvements:\n *  - Small HTML “console” page at \"/\" (no giant page reloads)\n *  - Incremental log feed at \"/log?from=<seq>&chunk=<n>\n *  - Nicer UX: Follow (tail) behaviour only when user is at bottom, Pause/Resume,\n *    Wrap toggle, Clear, Copy, simple Filter.\n *\n * Notes:\n *  - For incremental output, the buffer includes < > & chars as plain text\n */\n\n#include \"defines.h\"\n#ifdef ENABLE_SERIAL_LOG\n// This entire file is ignored if ENABLE_SERIAL_LOG is not defined in defines.h.\n#include \"Arduino.h\"\n#include \"DIAG.h\"\n#include \"SerialUsbLog.h\"\n#include \"StringBuffer.h\"\n#include \"DCCEXParser.h\"\n\n#if WIFI_ON\n  #include <WiFi.h>\n  WiFiServer server(80);\n#else\n  #include <STM32Ethernet.h>\n  EthernetServer server(80);\n#endif\n\n#include \"SerialUsbLog.h\"\n\n  // Log buffer size. You you have RAM to spare on thyese devices, so feel free to bump this.\n  // Keep it sensible; very large buffers make /dump and filter operations heavier.\n  #ifndef LOG_BUFFER\n    #define LOG_BUFFER 8192\n  #endif\n\n  // Maximum bytes returned per /log request (upper safety bound)\n  #ifndef LOG_CHUNK_MAX\n    #define LOG_CHUNK_MAX 1000\n  #endif\n\n// Global instance\nSerialUsbLog SerialLog(LOG_BUFFER, &Serial);\nStringBuffer dummyClient(2048); // buffering client for response construction\n\n// --------------------------- Small helpers (ESP32 only) ---------------------------\n\nstatic inline bool startsWithNoCase(const String& s, const char* prefix) {\n  int n = (int)strlen(prefix);\n  if ((int)s.length() < n) return false;\n  for (int i = 0; i < n; i++) {\n    char a = s[i];\n    char b = prefix[i];\n    if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');\n    if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');\n    if (a != b) return false;\n  }\n  return true;\n}\n\nstatic String urlPathOnly(const String& uri) {\n  int q = uri.indexOf('?');\n  return (q >= 0) ? uri.substring(0, q) : uri;\n}\n\nstatic String queryParamRaw(const String& uri, const char* key, String defaultValue) {\n  int q = uri.indexOf('?');\n  if (q < 0) return defaultValue;\n\n  String qs = uri.substring(q + 1);\n  String k = String(key) + \"=\";\n\n  int p = qs.indexOf(k);\n  if (p < 0) return defaultValue;\n\n  int vStart = p + k.length();\n  int amp = qs.indexOf('&', vStart);\n  String v = (amp >= 0) ? qs.substring(vStart, amp) : qs.substring(vStart);\n\n  v.trim();\n  if (!v.length()) return defaultValue;\n  return v;\n}\nstatic int queryParamInt(const String& uri, const char* key, int defaultValue) {\n  String v = queryParamRaw(uri, key, \"\");\n  if (v.length() == 0) return defaultValue;\n  return v.toInt();\n}\n\nstatic String uriDecode(const String& str) {\n  String result;\n  for (int i = 0; i < str.length(); i++) {\n    if (str[i] == '%' && i + 2 < str.length()) {\n      // hex decode: %20 → space, %2B → +\n      char hex[3] = { str[i+1], str[i+2], 0 };\n      char c = (char)strtol(hex, NULL, 16);\n      result += c;\n      i += 2;\n    } else if (str[i] == '+') {\n      result += ' ';\n    } else {\n      result += str[i];\n    }\n  }\n  return result;\n}\n\nstatic String queryParamString(const String& uri, const char* key, String defaultValue) {\n  String v = queryParamRaw(uri, key, \"\");\n  if (v.length() == 0) return defaultValue;\n  return uriDecode(v);\n}\n\nstatic void drainHttpHeaders(Client& client) {\n  // Read until blank line. Keep it short to avoid blocking too long.\n  uint32_t start = millis();\n  while (client.connected() && (millis() - start) < 50) {\n    String line = client.readStringUntil('\\n');\n    if (line.length() == 0 || line == \"\\r\") break;\n  }\n}\n\n/**\n * Constructor\n * @param len Maximum length of the log buffer\n * @param serialPort underlying serial port (eg. &Serial)\n */\nSerialUsbLog::SerialUsbLog(const uint16_t len, HardwareSerial* serialPort) {\n  _bufferSize = len;\n  _buffer = new byte[len];\n  _pos_write = 0;\n  _overflow = false;\n  _serialPort = serialPort;\n\n  // Monotonic write sequence counter (increments per byte stored into the ring).\n  _seq_write = 0;\n}\n\n/**\n * Write a single byte to the log buffer and to the underlying serial port.\n */\nsize_t SerialUsbLog::write(uint8_t b) {\n  _serialPort->write(b);\n\n  // Store directly (no translation)\n  shoveToBuffer(b);\n\n  return 1;\n}\n\n/**\n * Internal ring-buffer write (single byte).\n * On ESP32 we guard ring/seq updates with a critical section.\n */\nvoid SerialUsbLog::shoveToBuffer(uint8_t b) {\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portENTER_CRITICAL(&_mux);\n// #endif\n\n  if (_pos_write >= _bufferSize) {\n    _overflow = true;\n    _pos_write = 0;\n  }\n  _buffer[_pos_write++] = b;\n  _seq_write++;\n\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portEXIT_CRITICAL(&_mux);\n// #endif\n}\n\n/**\n * Stream the entire log buffer contents to a target Print stream.\n * This is the legacy one-shot dump of the current ring buffer snapshot.\n */\nvoid SerialUsbLog::streamOut(Print* targetStream) {\n  if (targetStream == nullptr) return;\n\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portENTER_CRITICAL(&_mux);\n// #endif\n\n  if (_overflow) {\n    // output from current position to end, then start to current position\n    targetStream->write(_buffer + _pos_write, _bufferSize - _pos_write);\n    targetStream->write(_buffer, _pos_write);\n  } else {\n    targetStream->write(_buffer, _pos_write);\n  }\n\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portEXIT_CRITICAL(&_mux);\n// #endif\n}\n\n/**\n * Return current write sequence number.\n * (Monotonic counter of bytes written into the ring.)\n */\nuint32_t SerialUsbLog::getWriteSeq() const {\n  return _seq_write;\n}\n\n/**\n * Stream from a given sequence number up to maxBytes (incremental streaming).\n *\n * fromSeq:\n *   - Client's last seen sequence number.\n *   - If too old, we fast-forward to earliest available.\n *\n * nextSeq (out):\n *   - Updated sequence number after streaming.\n *\n * Returns:\n *   - Number of bytes written to targetStream.\n */\nsize_t SerialUsbLog::streamOutFrom(Print* targetStream, uint32_t fromSeq, size_t maxBytes, uint32_t& nextSeq) {\n  if (!targetStream) { nextSeq = fromSeq; return 0; }\n\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portENTER_CRITICAL(&_mux);\n// #endif\n\n  const uint32_t writeSeq = _seq_write;\n  const uint32_t earliest = (writeSeq > (uint32_t)_bufferSize) ? (writeSeq - (uint32_t)_bufferSize) : 0;\n\n  uint32_t start = fromSeq;\n  if (start < earliest) start = earliest;\n  if (start > writeSeq) start = writeSeq;\n\n  uint32_t available = writeSeq - start;\n  if (available > (uint32_t)maxBytes) available = (uint32_t)maxBytes;\n\n  for (uint32_t s = 0; s < available; s++) {\n    const uint32_t seq = start + s;\n    const uint32_t pos = seq % (uint32_t)_bufferSize;\n    targetStream->write(_buffer[pos]);\n  }\n\n  nextSeq = start + available;\n\n// #if defined(ARDUINO_ARCH_ESP32)\n//   portEXIT_CRITICAL(&_mux);\n// #endif\n\n  return (size_t)available;\n}\n\n// begin() shim\nvoid SerialUsbLog::begin(unsigned long baud) {\n  _serialPort->begin(baud);\n}\n\n// while(!Serial) shim\nbool SerialUsbLog::operator!() const {\n  return _serialPort == nullptr;\n}\n\n// available() shim\nint SerialUsbLog::available() {\n  return _serialPort->available();\n}\n\n// read() shim\nint SerialUsbLog::read() {\n  return _serialPort->read();\n}\n\n// peek() shim\nint SerialUsbLog::peek() {\n  return _serialPort->peek();\n}\n\n/**\n * Lightweight HTTP server loop \n *\n * Endpoints:\n *  - GET /                : HTML log console (polling)\n *  - GET /log?from=N&chunk=M : returns text/plain chunk and X-Next-Seq header\n */\nvoid SerialUsbLog::loop() {\n\n  static bool started = false;\n  if (!started) {\n    server.begin();\n    started = true;\n    return;\n  }\n\n  auto client = server.available();\n  if (!client) return;\n  \n  // Read request line: \"GET /path?... HTTP/1.1\"\n  String reqLine = client.readStringUntil('\\r');\n  if (reqLine.length() == 0) { client.stop(); return; }\n  if (Diag::WIFI || Diag::ETHERNET) {\n    StringFormatter::send(_serialPort,F(\"<* http: %s *>\\n\"), reqLine.c_str());\n  }\n  int sp1 = reqLine.indexOf(' ');\n  int sp2 = reqLine.indexOf(' ', sp1 + 1);\n  if (sp1 < 0 || sp2 < 0) { client.stop(); return; }\n\n  String method = reqLine.substring(0, sp1);\n  String uri    = reqLine.substring(sp1 + 1, sp2);\n  String path   = urlPathOnly(uri);\n\n  // Drain the rest of the headers to keep the TCP state clean-ish.\n  drainHttpHeaders(client);\n  dummyClient.flush();\n\n  if (method != \"GET\") {\n    dummyClient.println(\n      \"HTTP/1.1 405 Method Not Allowed\\r\\n\"\n      \"Connection: close\\r\\n\\r\\n\"\n    );\n  }\n\n  // ----------------------------- /log incremental feed -----------------------------\n  else if (path == \"/log\") {\n    String cmd= queryParamString(uri, \"cmd\", \"\");\n    if (cmd.length()>0) {\n      DCCEXParser::parse(cmd.c_str());\n    }\n\n    uint32_t from = (uint32_t)queryParamInt(uri, \"from\", 0);\n\n    int chunk = queryParamInt(uri, \"chunk\", 1024);\n    if (chunk < 64) chunk = 64;\n    if (chunk > LOG_CHUNK_MAX) chunk = LOG_CHUNK_MAX;\n    \n    // Compute next seq (for header) using the same logic as streamOutFrom.\n    uint32_t writeSeq = SerialLog.getWriteSeq();\n    uint32_t earliest = (writeSeq > (uint32_t)_bufferSize) ? (writeSeq - (uint32_t)_bufferSize) : 0;\n    uint32_t start = from;\n    if (start < earliest) start = earliest;\n    if (start > writeSeq) start = writeSeq;\n    uint32_t avail = writeSeq - start;\n    if (avail > (uint32_t)chunk) avail = (uint32_t)chunk;\n    uint32_t next = start + avail;\n\n    dummyClient.print(\n      \"HTTP/1.1 200 OK\\r\\n\"\n      \"Content-Type: text/plain; charset=utf-8\\r\\n\"\n      \"Cache-Control: no-store\\r\\n\"\n      \"Connection: close\\r\\n\"\n      \"X-Next-Seq: \"\n    );\n    dummyClient.print(next);\n    dummyClient.print(\"\\r\\n\\r\\n\");\n\n    uint32_t nextSeqOut = from;\n    SerialLog.streamOutFrom(&dummyClient, from, (size_t)chunk, nextSeqOut);\n  }\n\n  // --------------------------------- / HTML shell ---------------------------------\nelse  if (path == \"/\" ) {\n    dummyClient.print(\n      \"HTTP/1.1 200 OK\\r\\n\"\n      \"Content-Type: text/html; charset=utf-8\\r\\n\"\n      \"Cache-Control: no-store\\r\\n\"\n      \"Connection: close\\r\\n\\r\\n\"\n      #include \"SerialUsbLog.html.h\"\n    );\n\n  }\n\n  else if (path == \"/style.css\" ) {\n    dummyClient.print(\n      \"HTTP/1.1 200 OK\\r\\n\"\n      \"Content-Type: text/css; charset=utf-8\\r\\n\"\n      \"Cache-Control: no-store\\r\\n\"\n      \"Connection: close\\r\\n\\r\\n\"\n      #include \"SerialUsbLog.style.css.h\"\n    );\n  }\n\n else if (path == \"/script1.js\" ) {\n    dummyClient.print(\n      \"HTTP/1.1 200 OK\\r\\n\"\n      \"Content-Type: text/javascript; charset=utf-8\\r\\n\"\n      \"Cache-Control: no-store\\r\\n\"\n      \"Connection: close\\r\\n\\r\\n\"\n      #include \"SerialUsbLog.script1.js.h\"\n    );\n\n  }\n  \n  else if (path == \"/script2.js\" ) {\n    dummyClient.print(\n      \"HTTP/1.1 200 OK\\r\\n\"\n      \"Content-Type: text/javascript; charset=utf-8\\r\\n\"\n      \"Cache-Control: no-store\\r\\n\"\n      \"Connection: close\\r\\n\\r\\n\"\n      #include \"SerialUsbLog.script2.js.h\"\n    );\n  }\nelse {\n  // --------------------------------- 404 ---------------------------------\n  dummyClient.print(\n    \"HTTP/1.1 404 Not Found\\r\\n\"\n    \"Connection: close\\r\\n\\r\\n\"\n  );\n}\nif (Diag::WIFI || Diag::ETHERNET) {\n    StringFormatter::send(_serialPort,F(\"<* http:replyLength %d *>\\n\"),\n    dummyClient.getLength());\n  }\n  \n  client.print(dummyClient.getString());\n  client.stop();\n}\n// --------------------------- End of SerialUsbLog.cpp ---------------------------\n#endif // ENABLE_SERIAL_LOG\n"
  },
  {
    "path": "SerialUsbLog.h",
    "content": "/*\n *  © 2026 Chris Harlow\n *  © 2026 Paul M. Antoine\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ENABLE_SERIAL_LOG\n#ifndef SerialUsbLog_h\n#define SerialUsbLog_h\n\n#include <Arduino.h>\n#include \"FSH.h\"\n  \nclass SerialUsbLog : public Stream {\n\n  public:\n    SerialUsbLog( const uint16_t len, HardwareSerial* serialPort  );\n    void begin( unsigned long baud );\n    virtual size_t write(uint8_t b);\n    using Print::write;\n    void streamOut(Print * targetStream);\n    // NEW: incremental streaming support\n    uint32_t getWriteSeq() const;\n    size_t streamOutFrom(Print* targetStream, uint32_t fromSeq, size_t maxBytes, uint32_t& nextSeq);\n    bool operator!() const;\n    void shoveToBuffer(uint8_t b);\n    virtual int available();\n    virtual int read();\n    virtual int peek();\n    void loop();\n\n private:\n   HardwareSerial * _serialPort;\n   int _pos_write;\n   bool _overflow;\n   byte * _buffer;\n   int _bufferSize;\n   // NEW\n   volatile uint32_t _seq_write;\n\n#if defined(ARDUINO_ARCH_ESP32)\n   // protect buffer/seq from concurrent access\n   portMUX_TYPE _mux = portMUX_INITIALIZER_UNLOCKED;\n#endif\n};\nextern SerialUsbLog SerialLog;\n\n#endif\n#endif // ENABLE_SERIAL_LOG\n"
  },
  {
    "path": "SerialUsbLog.html.h",
    "content": "R\"???(\n<!doctype html><html><head>\n      <meta name=viewport content='width=device-width,initial-scale=1'>\n      <title>DCC-EX Server Log</title>\n      </head>\n      <link rel=stylesheet href='/style.css'/>\n      <body>\n      <header>\n        <h1>DCC-EX Server Log</h1>\n        <button id=pause>Pause</button>\n        <button id=clear>Clear</button>\n        <label id=followLbl><input id=follow type=checkbox checked>Follow</label>\n        <label id=wrapLbl><input id=wrap type=checkbox>Wrap</label>\n        <input id=filter placeholder='Filter…' size=10>\n        Command:\n        <input id=cmd name=\"cmd\" placeholder='Command input' size=40>\n        <button id=cmdButton>Send</button>\n        <span id=stat class=dim></span>\n      </header>\n      <textarea id=log readOnly></textarea>\n      <script src=\"/script1.js\" defer></script>\n      <script src=\"/script2.js\" defer></script> \n</body></html>\n)???\""
  },
  {
    "path": "SerialUsbLog.script1.js.h",
    "content": "R\"???(\nfunction dog(tag) {return document.getElementById(tag);}\nconst logEl=dog('log');\nconst pauseBtn=dog('pause');\nconst clearBtn=dog('clear');\nconst wrapCb=dog('wrap');\nconst followCb=dog('follow');\nconst filterIn=dog('filter');\nconst stat=dog('stat');\nconst cmdInput = dog('cmd');\nconst cmdButton = dog('cmdButton');\nlet paused=false, seq=0, buf='',cmdWaiting=false;\nlet userScrolled=false;\nfunction atBottom(){return (logEl.scrollHeight-logEl.scrollTop-logEl.clientHeight)<8;}\nlogEl.addEventListener('scroll',()=>{userScrolled=!atBottom();});\nwrapCb.addEventListener('change',()=>{logEl.style.whiteSpace=wrapCb.checked?'pre-wrap':'pre';});\npauseBtn.onclick=()=>{paused=!paused; pauseBtn.textContent=paused?'Resume':'Pause';};\nclearBtn.onclick=()=>{buf=''; logEl.value=''; seq=0;};\ncmdButton.onclick=()=>{cmdWaiting=true;};\nfunction applyFilter(text){\nconst f=filterIn.value.trim();\nif(!f) return text;\nreturn text.split('\\n').filter(l=>l.includes(f)).join('\\n');\n}\nfilterIn.addEventListener('input',()=>{logEl.value=applyFilter(buf); if(followCb.checked&&!userScrolled){logEl.scrollTop=logEl.scrollHeight;}});\ncmdInput?.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); cmdButton.click(); }});\n)???\""
  },
  {
    "path": "SerialUsbLog.script2.js.h",
    "content": "R\"???(\nasync function tick(){\ntry{\nif(!paused && logEl.selectionStart===logEl.selectionEnd){\n  const chunk=2048;\n  let cmd=cmdWaiting ? cmdInput.value.trim() : '';\n  cmdWaiting=false;\n  if (cmd.length>0 && cmd[0]!='<') cmd='<' + cmd + '>';\n  const cmdpart = cmd.length>0 ? (\"&cmd=\"+encodeURIComponent(cmd)) : \"\";\n  const uri='/log?from='+seq+'&chunk='+chunk+cmdpart;\n  console.log(\"Fetching URI:\", uri);\n  const r=await fetch(uri,{cache:'no-store'});\n  if(r.ok){\n    const t=await r.text();\n    const nxt=r.headers.get('X-Next-Seq');\n    if(nxt) seq=parseInt(nxt,10)||seq;\n    if(t && t.length){\n      buf+=t;\n      logEl.textContent=applyFilter(buf);\n      if(followCb.checked&&!userScrolled) logEl.scrollTop=logEl.scrollHeight;\n    }\n    stat.textContent=(paused?'paused':'live')+' • seq '+seq+' • '+buf.length+' chars';\n  } else {\n    stat.textContent='http '+r.status;\n  }\n} else {\n  stat.textContent='paused • seq '+seq+' • '+buf.length+' chars';\n}\n}catch(e){\nstat.textContent='offline';\n}\nsetTimeout(tick, 400);\n}\ntick();\n\n)???\""
  },
  {
    "path": "SerialUsbLog.style.css.h",
    "content": "R\"???(\nhtml,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial}\nbody{background:#0f1115;color:#d7dae0;display:flex;flex-direction:column}\nheader{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;padding:.6rem .75rem;border-bottom:1px solid #2a2d34}\nheader h1{font-size:1rem;margin:0;flex:1;min-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\nbutton,input,textarea,label,a.btn{background:#181a1f;color:#d7dae0;border:1px solid #2a2d34;border-radius:10px;padding:.35rem .6rem;text-decoration:none}\nbutton:hover,input:hover,label:hover,a.btn:hover{border-color:#3a3f4a}\n#wrapLbl{display:flex;align-items:center;gap:.35rem;opacity:.9}\n#followLbl{display:flex;align-items:center;gap:.35rem;opacity:.9}\n#log{flex:1;overflow:auto;padding:.75rem;font:12px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre;tab-size:2}\n#stat{opacity:.75;font-size:.85rem;min-width:160px;text-align:right}\n.dim{opacity:.75}\n)???\""
  },
  {
    "path": "Sniffer.cpp",
    "content": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ARDUINO_ARCH_ESP32\n#include \"Sniffer.h\"\n#include \"DIAG.h\"\n//extern Sniffer *DCCSniffer;\n\nstatic void packeterror() {\n#ifndef WIFI_LED\n#ifdef SNIFFER_LED\n  digitalWrite(SNIFFER_LED,HIGH);\n#endif\n#endif\n}\n\nstatic void clear_packeterror() {\n#ifndef WIFI_LED\n#ifdef SNIFFER_LED\n  digitalWrite(SNIFFER_LED,LOW);\n#endif\n#endif\n}\n\nstatic bool halfbits2byte(uint16_t b, byte *dccbyte) {\n/*\n  if (b!=0 && b!=0xFFFF) {\n    Serial.print(\"[ \");\n    for(int n=0; n<16; n++) {\n      Serial.print(b&(1<<n)?\"1\":\"0\");\n    }\n    Serial.println(\" ]\");\n  }\n*/\n  for(byte n=0; n<8; n++) {\n    switch (b & 0x03) {\n    case 0x01:\n    case 0x02:\n      // broken bits\n      packeterror();\n      return false;\n      break;\n    case 0x00:\n      bitClear(*dccbyte, n);\n      break;\n    case 0x03:\n      bitSet(*dccbyte, n);\n      break;\n    }\n    b = b>>2;\n  }\n  return true;\n}\n\nstatic void IRAM_ATTR blink_diag(int limit) {\n#ifndef WIFI_LED\n#ifdef SNIFFER_LED\n  delay(500);\n  for (int n=0 ; n<limit; n++) {\n    digitalWrite(SNIFFER_LED,HIGH);\n    delay(200);\n    digitalWrite(SNIFFER_LED,LOW);\n    delay(200);\n  }\n#endif\n#endif\n}\n\nstatic bool IRAM_ATTR cap_ISR_cb(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,void *user_data) {\n  if (edata->cap_edge == MCPWM_BOTH_EDGE) {\n    // should not happen at all\n    // delays here might crash sketch\n    blink_diag(2);\n    return 0;\n  }\n  if (user_data) ((Sniffer *)user_data)->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);\n//if (DCCSniffer)            DCCSniffer->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);\n  \n  return 0;\n}\n\nSniffer::Sniffer(byte snifferpin) {\n  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, snifferpin);\n  // set capture edge, BIT(0) - negative edge, BIT(1) - positive edge\n  // MCPWM_POS_EDGE|MCPWM_NEG_EDGE should be 3.\n  //mcpwm_capture_enable(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, MCPWM_POS_EDGE|MCPWM_NEG_EDGE, 0);\n  //mcpwm_isr_register(MCPWM_UNIT_0, sniffer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);\n  //MCPWM0.int_ena.cap0_int_ena = 1;                      // Enable interrupt on CAP0 signal\n\n  mcpwm_capture_config_t MCPWM_cap_config = { //Capture channel configuration\n    .cap_edge = MCPWM_BOTH_EDGE,              // according to mcpwm.h\n    .cap_prescale = 1,                        // 1 to 256 (see .h file)\n    .capture_cb = cap_ISR_cb,                 // user defined ISR/callback\n    .user_data = (void *)this                 // user defined argument to callback\n  };\n#ifndef WIFI_LED\n#ifdef SNIFFER_LED\n  pinMode(SNIFFER_LED ,OUTPUT);\n#endif\n#endif\n  blink_diag(3); // so that we know we have SNIFFER_LED\n  DIAG(F(\"Init sniffer on pin %d\"), snifferpin);\n  ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &MCPWM_cap_config));\n}\n\n#define SNIFFER_TIMEOUT 100L // 100 Milliseconds\nbool Sniffer::inputActive(){\n  unsigned long now = millis();\n  return ((now - lastendofpacket) < SNIFFER_TIMEOUT);\n}\n\n#define DCC_TOO_SHORT 4000L // 4000 ticks are 50usec\n#define DCC_ONE_LIMIT 6400L // 6400 ticks are 80usec\n\nvoid IRAM_ATTR Sniffer::processInterrupt(int32_t capticks, bool posedge) {\n  byte bit = 0;\n  diffticks = capticks - lastticks;\n  if (lastedge != posedge) {\n    if (diffticks < DCC_TOO_SHORT) {\n      return;\n    }\n    if (diffticks < DCC_ONE_LIMIT) {\n      bit = 1;\n    } else {\n      bit = 0;\n    }\n    // update state variables for next round\n    lastticks = capticks;\n    lastedge = posedge;\n    bitfield = bitfield << (uint64_t)1;\n    bitfield = bitfield + (uint64_t)bit;\n\n    // now the halfbit is in the bitfield. Analyze...\n    \n    if ((bitfield & 0xFFFFFF) == 0xFFFFFC){\n      // This looks at the 24 last halfbits\n      // and detects a preamble if\n      // 22 are ONE and 2 are ZERO which is a\n      // preabmle of 11 ONES and one ZERO\n      if (inpacket) {\n\t// if we are already inpacket here we\n\t// got a preamble in the middle of a\n\t// packet\n\tpacketerror();\n      } else {\n\tclear_packeterror(); // everything fine again at end of preable after good packet\n      }\n      currentbyte = 0;\n      dcclen = 0;\n      inpacket = true;\n      halfbitcounter = 18; // count 18 steps from 17 to 0 and then look at the byte\n      return;\n    }\n    if (inpacket) {\n      halfbitcounter--;\n      if (halfbitcounter) {\n\treturn; // wait until we have full byte\n      } else {\n\t// have reached end of byte\n\t//if (currentbyte == 2) debugfield = bitfield;\n\tbyte twohalfbits = bitfield & 0x03;\n\tswitch (twohalfbits) {\n\tcase 0x01:\n\tcase 0x02:\n\t  // broken bits\n\t  inpacket = false;\n\t  packeterror();\n\t  return;\n\t  break;\n\tcase 0x00:\n\tcase 0x03:\n\t  // byte end\n\t  uint16_t b = (bitfield & 0x3FFFF)>>2; // take 18 halfbits and use 16 of them\n\t  if (!halfbits2byte(b, dccbytes + currentbyte)) {\n\t    // broken halfbits\n\t    inpacket = false;\n\t    packeterror();\n\t    return;\n\t  }\n\t  if (twohalfbits == 0x03) { // end of packet marker\n\t    inpacket = false;\n\t    dcclen = currentbyte+1;\n\t    debugfield = bitfield;\n\t    // We have something we want to give to the outpacket queue\n\t    // Check length of outpacket queue\n\t    if (outpacket.size() > 3) {\n\t      // not good, these should have been fetched\n\t      // the arbitraty number to check is THREE (see the holy grail)\n              // blink_diag(1); DO NOT DO THIS HERE -> will crash\n\t      packeterror(); // or what to do better?\n\t      // take emergency action:\n\t      while (!outpacket.empty()) {\n\t\toutpacket.pop_front();\n\t      }\n\t    }\n\t    lastendofpacket = millis();\n\t    DCCPacket temppacket(dccbytes, dcclen);\n\t    if (!(temppacket == prevpacket)) {\n\t      // we have something new to offer to the fetch routine\n\t      // put it into the outpacket queue\n\t      outpacket.push_back(temppacket);\n\t      prevpacket = temppacket;\n\t    }\n\t    return;\n\t  }\n\t  break;\n\t}\n\thalfbitcounter = 18;\n\tcurrentbyte++; // everything done for this end of byte\n\tif (currentbyte >= MAXDCCPACKETLEN) {\n\t  inpacket = false; // this is an error because we should have retured above\n\t  packeterror();    // when endof packet marker was active\n\t}\n      }\n    }\n  } else { // lastedge == posedge\n    // this should not happen, check later\n  }\n}\n\n/*\nstatic void IRAM_ATTR sniffer_isr_handler(void *) {\n  DCCSniffer.processInterrupt();\n}\n*/\n#endif // ESP32\n"
  },
  {
    "path": "Sniffer.h",
    "content": "/*\n *  © 2025 Harald Barth\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ARDUINO_ARCH_ESP32\n#include <Arduino.h>\n#include <list>\n#include \"driver/mcpwm.h\"\n#include \"soc/mcpwm_struct.h\"\n#include \"soc/mcpwm_reg.h\"\n\n#define MAXDCCPACKETLEN 8\n#include \"DCCPacket.h\"\n\nclass Sniffer {\npublic:\n  Sniffer(byte snifferpin);\n  void IRAM_ATTR processInterrupt(int32_t capticks, bool posedge);\n  inline int32_t getTicks() {\n    noInterrupts();\n    int32_t i = diffticks;\n    interrupts();\n    return i;\n  };\n  inline int64_t getDebug() {\n    noInterrupts();\n    int64_t i = debugfield;\n    interrupts();\n    return i;\n  };\n  inline DCCPacket fetchPacket() {\n    // if there is no new data, this will create a\n    // packet with length 0 (which is no packet)\n    DCCPacket p;\n    noInterrupts();\n    if (!outpacket.empty()) {\n      p = outpacket.front();\n      outpacket.pop_front();\n    }\n    interrupts();\n    return p;\n  };\n  bool inputActive();\nprivate:\n  // keep these vars in processInterrupt only\n  uint64_t bitfield = 0;\n  uint64_t debugfield = 0;\n  int32_t diffticks;\n  int32_t lastticks;\n  bool lastedge;\n  byte currentbyte = 0;\n  byte dccbytes[MAXDCCPACKETLEN];\n  byte dcclen = 0;\n  bool inpacket = false;\n  // these vars are used as interface to other parts of sniffer\n  byte halfbitcounter = 0;\n  std::list<DCCPacket> outpacket;\n  DCCPacket prevpacket;\n  volatile unsigned long lastendofpacket = 0; // timestamp millis\n\n};\n#endif\n"
  },
  {
    "path": "Stash.cpp",
    "content": "/* \n*  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>. \n*/\n#include \"Stash.h\"\n#include \"StringFormatter.h\"\n\nStash::Stash(int16_t stash_id, int16_t loco_id) {\n    this->stashId = stash_id;\n    this->locoId = loco_id;\n    this->next = first;\n    first = this;\n}\n\nvoid Stash::clearAll() {\n    for (auto s=first;s;s=s->next) {\n        s->locoId = 0;\n        s->stashId =0;\n    }\n}\n\nvoid Stash::clearAny(int16_t loco_id) {\n    auto lid=abs(loco_id);\n    for (auto s=first;s;s=s->next)\n        if (abs(s->locoId) == lid) {\n            s->locoId = 0;\n            s->stashId =0;\n        }\n}\n\nvoid Stash::clear(int16_t stash_id) {\n    set(stash_id,0);\n}\n\nint16_t Stash::get(int16_t stash_id) {\n    for (auto s=first;s;s=s->next)\n        if (s->stashId == stash_id) return s->locoId;\n    return 0;\n}\n\nvoid Stash::set(int16_t stash_id, int16_t loco_id) {\n    // replace any existing stash\n    for (auto s=first;s;s=s->next)\n        if (s->stashId == stash_id) {\n            s->locoId=loco_id;\n            if (loco_id==0) s->stashId=0; // recycle\n            return;\n        }\n    if (loco_id==0) return; // no need to create a zero entry.\n\n    // replace any empty stash \n    for (auto s=first;s;s=s->next)\n        if (s->locoId == 0) {\n            s->locoId=loco_id;\n            s->stashId=stash_id;\n            return;\n        }\n    // create a new stash\n    new Stash(stash_id, loco_id);\n}\n \nvoid Stash::list(Print * stream, int16_t stash_id) {\n    bool sent=false;\n    for (auto s=first;s;s=s->next)\n      if ((s->locoId) && (stash_id==0 || s->stashId==stash_id)) {\n          StringFormatter::send(stream,F(\"<jM %d %d>\\n\"),\n            s->stashId,s->locoId);\n          sent=true;\n      }\n    if (!sent) StringFormatter::send(stream,F(\"<jM %d 0>\\n\"),\n          stash_id);\n}\n\nStash* Stash::first=nullptr;\n"
  },
  {
    "path": "Stash.h",
    "content": "/* \n*  © 2024 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>. \n*/\n#ifndef Stash_h\n#define Stash_h\n#include <Arduino.h>\n\nclass Stash {\n  public:\n    static void clear(int16_t stash_id);\n    static void clearAll();\n    static void clearAny(int16_t loco_id);\n    static int16_t get(int16_t stash_id);\n    static void set(int16_t stash_id, int16_t loco_id);\n    static void list(Print * stream, int16_t stash_id=0); // id0 = LIST ALL\n  private:\n    Stash(int16_t stash_id, int16_t loco_id);\n    static Stash* first;\n    Stash* next;\n    int16_t stashId;   \n    int16_t locoId;\n};\n#endif\n"
  },
  {
    "path": "StringBuffer.cpp",
    "content": "/*\n *  © 2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC-EX CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"StringBuffer.h\"\n#include \"DIAG.h\"\n\nStringBuffer::StringBuffer(int16_t size) {\n    _buffer_max=size-2; // max write offset, allowing for null terminator\n    _buffer=new char[size];\n    flush();\n};\n\nStringBuffer::~StringBuffer() {\n    delete[] _buffer;\n}\n\nchar * StringBuffer::getString() { \n   return _buffer;\n}\n\nint16_t StringBuffer::getLength() { \n   return _pos_write;\n}\n\nvoid StringBuffer::flush() {\n    _pos_write=0;\n    _buffer[0]='\\0';\n}\n\nsize_t StringBuffer::write(uint8_t b) {\n  if (_pos_write>=_buffer_max) return 0; // max allows for 2 chars.\n  _buffer[_pos_write] = b;\n  ++_pos_write;\n  _buffer[_pos_write]='\\0';\n  return 1;\n}\n"
  },
  {
    "path": "StringBuffer.h",
    "content": " /*\n *  © 2022 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef StringBuffer_h\n#define StringBuffer_h\n#include <Arduino.h>\n\nclass StringBuffer : public Print {\n  public:\n    StringBuffer(int16_t size=64); \n    ~StringBuffer();\n    // Override Print default\n    virtual size_t write(uint8_t b);\n    void flush();\n    char * getString();\n    int16_t getLength();\n  private:\n    int16_t _buffer_max;\n    int16_t _pos_write;\n    char * _buffer;\n};\n\n#endif\n"
  },
  {
    "path": "StringFormatter.cpp",
    "content": "/*\n *  © 2020=2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include \"StringFormatter.h\"\n#include <stdarg.h>\n#include \"DisplayInterface.h\"\n#include \"CommandDistributor.h\"\n\nbool Diag::ACK=false;\nbool Diag::CMD=false;\nbool Diag::WIFI=false;\nbool Diag::WITHROTTLE=false;\nbool Diag::ETHERNET=false;\nbool Diag::LCN=false;\nbool Diag::RAILCOM=false;\nbool Diag::WEBSOCKET=false; \nbool Diag::SNIFFER=false;\n\n\n \nvoid StringFormatter::diag( const FSH* input...) {\n USB_SERIAL.print(F(\"<* \"));   \n  va_list args;\n  va_start(args, input);\n  send2(&USB_SERIAL,input,args);\n  USB_SERIAL.print(F(\" *>\\n\"));\n}\n\nvoid StringFormatter::lcd(byte row, const FSH* input...) {\n  va_list args;\n#ifndef DISABLE_VDPY\n  Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row);\n#else\n  Print * virtualLCD=NULL;\n#endif\n  // Issue the LCD as a diag first\n  // Unless the same serial is asking for the virtual @ respomnse\n  if (virtualLCD!=&USB_SERIAL) {\n    send(&USB_SERIAL,F(\"<* LCD%d:\"),row);\n    va_start(args, input);\n    send2(&USB_SERIAL,input,args);\n    send(&USB_SERIAL,F(\" *>\\n\"));\n  }\n  \n#ifndef DISABLE_VDPY\n  // send to virtual LCD collector (if any) \n  if (virtualLCD) {\n    va_start(args, input);\n    send2(virtualLCD,input,args);\n    CommandDistributor::commitVirtualLCDSerial();\n  }\n#endif\n  DisplayInterface::setRow(row);    \n  va_start(args, input);\n  send2(DisplayInterface::getDisplayHandler(),input,args);\n}\n\nvoid StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {\n  va_list args;\n  \n   // send to virtual LCD collector (if any) \n#ifndef DISABLE_VDPY\n  Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row);\n  if (virtualLCD) {\n    va_start(args, input);\n    send2(virtualLCD,input,args);\n    CommandDistributor::commitVirtualLCDSerial();\n  }\n#endif\n\n  DisplayInterface::setRow(display, row);    \n  va_start(args, input);\n  send2(DisplayInterface::getDisplayHandler(),input,args);\n}\n\nvoid StringFormatter::send(Print * stream, const FSH* input...) {\n  va_list args;\n  va_start(args, input);\n  send2(stream,input,args);\n}\n\nvoid StringFormatter::send(Print & stream, const FSH* input...) {\n  va_list args;\n  va_start(args, input);\n  send2(&stream,input,args);\n}\n\nvoid StringFormatter::send2(Print * stream,const FSH* format, va_list args) {\n    \n  // thanks to Jan Turoň  https://arduino.stackexchange.com/questions/56517/formatting-strings-in-arduino-for-output\n\n  char* flash=(char*)format;\n  for(int i=0; ; ++i) {\n    char c=GETFLASH(flash+i);\n    if (c=='\\0') break; // to va_end()\n    if(c!='%') { stream->print(c); continue; }\n\n    bool formatContinues=false;\n    byte formatWidth=0;\n    bool formatLeft=false; \n  do {\n    \n    formatContinues=false;\n    i++;\n    c=GETFLASH(flash+i);\n    switch(c) {\n      case '%': stream->print('%'); break;\n      case 'c': stream->print((char) va_arg(args, int)); break;\n      case 's': stream->print(va_arg(args, char*)); break;\n      case 'e': printEscapes(stream,va_arg(args, char*)); break;\n      case 'E': printEscapes(stream,(const FSH*)va_arg(args, char*)); break;\n      case 'S':\n      { \n        const FSH*  flash= (const FSH*)va_arg(args, char*);\n\n#if WIFI_ON | ETHERNET_ON\n        // RingStream has special logic to handle flash strings\n        // but is not implemented unless wifi or ethernet are enabled.\n        // The define prevents RingStream code being added unnecessariliy.        \n        if (stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM)\n              ((RingStream *)stream)->printFlash(flash);\n              else \n#endif\n        stream->print(flash);\n        break;\n             }\n      case 'P': stream->print((uint32_t)va_arg(args, void*), HEX); break;\n      case 'd': printPadded(stream,va_arg(args, int), formatWidth, formatLeft); break;\n      case 'u': printPadded(stream,va_arg(args, unsigned int), formatWidth, formatLeft); break;\n      case 'l': printPadded(stream,va_arg(args, long), formatWidth, formatLeft); break;\n      case 'L': stream->print(va_arg(args, unsigned long), DEC); break;\n      case 'b': stream->print(va_arg(args, int), BIN); break;\n      case 'o': stream->print(va_arg(args, int), OCT); break;\n      case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;\n      case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break;\n      case 'h': printHex(stream,(unsigned int)va_arg(args, unsigned int)); break;\n      case 'M':\n      { // this prints a unsigned long microseconds time in readable format\n\tunsigned long time = va_arg(args, long);\n\tif (time >= 2000) {\n\t  time = time / 1000;\n\t  if (time >= 2000) {\n\t    printPadded(stream, time/1000, formatWidth, formatLeft);\n\t    stream->print(F(\"sec\"));\n\t  } else {\n\t    printPadded(stream,time, formatWidth, formatLeft);\n\t    stream->print(F(\"msec\"));\n\t  }\n\t} else {\n\t  printPadded(stream,time, formatWidth, formatLeft);\n\t  stream->print(F(\"usec\"));\n\t}\n      }\n      break;\n      //case 'f': stream->print(va_arg(args, double), 2); break;\n      //format width prefix\n      case '-': \n            formatLeft=true;\n            formatContinues=true;\n            break; \n      case '0': \n      case '1': \n      case '2': \n      case '3': \n      case '4': \n      case '5': \n      case '6': \n      case '7': \n      case '8': \n      case '9': \n            formatWidth=formatWidth * 10 + (c-'0');\n            formatContinues=true;\n            break;\n    }\n  } while(formatContinues);\n  }\n  va_end(args);\n}\n\nvoid StringFormatter::printEscapes(Print * stream,char * input) {\n if (!stream) return;\n for(int i=0; ; ++i) {\n  char c=input[i];\n  printEscape(stream,c);\n  if (c=='\\0') return;\n }\n}\n\nvoid StringFormatter::printEscapes(Print * stream, const FSH * input) {\n \n if (!stream) return;\n char* flash=(char*)input;\n for(int i=0; ; ++i) {\n  char c=GETFLASH(flash+i);\n  printEscape(stream,c);\n  if (c=='\\0') return;\n }\n}\n\nvoid StringFormatter::printEscape( char c) {\n  printEscape(&USB_SERIAL,c);\n}\n\nvoid StringFormatter::printEscape(Print * stream, char c) {\n  if (!stream) return;\n  switch(c) {\n     case '\\n': stream->print(F(\"\\\\n\")); break; \n     case '\\r': stream->print(F(\"\\\\r\")); break; \n     case '\\0': stream->print(F(\"\\\\0\")); return; \n     case '\\t': stream->print(F(\"\\\\t\")); break;\n     case '\\\\': stream->print(F(\"\\\\\\\\\")); break;\n     default: stream->write(c);\n  }\n }\n\n \nvoid StringFormatter::printPadded(Print* stream, long value, byte width, bool formatLeft) {\n  if (width==0) {\n    stream->print(value, DEC);\n    return;\n  }\n  \n    int digits=(value <= 0)? 1: 0;  // zero and negative need extra digot\n    long v=value;\n    while (v) {\n        v /= 10;\n        digits++;\n    }\n    \n    if (formatLeft) stream->print(value, DEC);\n    while(digits<width) {\n      stream->print(' ');\n      digits++;\n    }\n    if (!formatLeft) stream->print(value, DEC);    \n  }\n\n// printHex prints the full 2 byte hex with leading zeros, unlike print(value,HEX)\nconst char FLASH hexchars[]=\"0123456789ABCDEF\";\nvoid StringFormatter::printHex(Print * stream,uint16_t value) {\n    char result[5];\n    for (int i=3;i>=0;i--) {\n      result[i]=GETFLASH(hexchars+(value & 0x0F));\n      value>>=4;\n    }\n    result[4]='\\0';\n     stream->print(result);\n}\n"
  },
  {
    "path": "StringFormatter.h",
    "content": "/*\n *  © 2020-2025, Chris Harlow. All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef StringFormatter_h\n#define StringFormatter_h\n#include <Arduino.h>\n#include \"FSH.h\"\n#include \"RingStream.h\"\n#include \"Display.h\"\nclass Diag {\n  public:\n  static bool ACK;\n  static bool CMD;\n  static bool WIFI;\n  static bool WITHROTTLE;\n  static bool ETHERNET;\n  static bool LCN;\n  static bool RAILCOM;\n  static bool WEBSOCKET;\n  static bool SNIFFER;\n};\n\nclass StringFormatter\n{\n  public:\n    static void send(Print * serial, const FSH* input...);\n    static void send(Print & serial, const FSH* input...);\n    \n    static void printEscapes(Print * serial,char * input);\n    static void printEscapes(Print * serial,const FSH* input);\n    static void printEscape(Print * serial, char c);\n\n    // DIAG support\n    static void diag( const FSH* input...);\n    static void lcd(byte row, const FSH* input...);\n    static void lcd2(uint8_t display, byte row, const FSH* input...);\n    static void printEscapes(char * input);\n    static void printEscape( char c);\n    static void printHex(Print * stream,uint16_t value);\n\n    private: \n    static void send2(Print * serial, const FSH* input,va_list args);\n    static void printPadded(Print* stream, long value, byte width, bool formatLeft);\n};\n#endif\n"
  },
  {
    "path": "TemplateForEnums.h",
    "content": "/*\n *  © 2024, Harald Barth. All rights reserved.\n *  \n *  This file is part of DCC-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef TemplateForEnums\n#define TemplateForEnums\ntemplate<class T> inline T operator~ (T a) { return (T)~(int)a; }\ntemplate<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }\ntemplate<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }\ntemplate<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }\n#endif\n\n"
  },
  {
    "path": "TrackManager.cpp",
    "content": "/*\n *  © 2022-2025 Chris Harlow\n *  © 2022-2024 Harald Barth\n *  © 2023-2024 Paul M. Antoine\n *  © 2024-2025 Herb Morton\n *  © 2023 Colin Murdoch\n *  All rights reserved.\n *  \n *  This file is part of DCC++EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#include \"defines.h\"\n#include \"TrackManager.h\"\n#include \"FSH.h\"\n#include \"DCCWaveform.h\"\n#include \"DCC.h\"\n#include \"MotorDriver.h\"\n#include \"DCCTimer.h\"\n#include \"DIAG.h\"\n#include \"CommandDistributor.h\"\n#include \"DCCEXParser.h\"\n#include \"KeywordHasher.h\"\n// Virtualised Motor shield multi-track hardware Interface\n#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)\n    \n#define APPLY_BY_MODE(findmode,function) \\\n        FOR_EACH_TRACK(t) \\\n\t    if (track[t]->getMode() & findmode)\t\\\n                track[t]->function;\n\nMotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };\nint16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };\nint16_t TrackManager::trackPwrMA[MAX_TRACKS] = { 0 };\n\nint8_t TrackManager::lastTrack=-1;\nbool TrackManager::progTrackSyncMain=false; \nbool TrackManager::progTrackBoosted=false; \nint16_t TrackManager::joinRelay=UNUSED_PIN;\n#ifdef ARDUINO_ARCH_ESP32\nbyte TrackManager::tempProgTrack=MAX_TRACKS+1; // MAX_TRACKS+1 is the unused flag\n#endif\n\n#ifdef ANALOG_READ_INTERRUPT\n/*\n * sampleCurrent() runs from Interrupt\n */\nvoid TrackManager::sampleCurrent() {\n  static byte tr = 0;\n  byte trAtStart = tr;\n  static bool waiting = false;\n\n  if (waiting) {\n    if (! track[tr]->sampleCurrentFromHW()) {\n      return; // no result, continue to wait\n    }\n    // found value, advance at least one track\n    // for scope debug track[1]->setBrake(0);\n    waiting = false;\n    tr++;\n    if (tr > lastTrack) tr = 0;\n    if (lastTrack < 2 || track[tr]->getMode() & TRACK_MODE_PROG) {\n      return; // We could continue but for prog track we\n              // rather do it in next interrupt beacuse\n              // that gives us well defined sampling point.\n              // For other tracks we care less unless we\n              // have only few (max 2) tracks.\n    }\n  }\n  if (!waiting) {\n    // look for a valid track to sample or until we are around\n    while (true) {\n      if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT )) {\n\ttrack[tr]->startCurrentFromHW();\n\t// for scope debug track[1]->setBrake(1);\n\twaiting = true;\n\tbreak;\n      }\n      tr++;\n      if (tr > lastTrack) tr = 0;\n      if (tr == trAtStart) // we are through and nothing found to do\n\treturn;\n    }\n  }\n}\n#endif\n\n// The setup call is done this way so that the tracks can be in a list \n// from the config... the tracks default to NULL in the declaration                 \nvoid TrackManager::Setup(const FSH * shieldname,\n        MotorDriver * track0, MotorDriver * track1, MotorDriver * track2,\n        MotorDriver * track3, MotorDriver * track4,  MotorDriver * track5,\n        MotorDriver * track6, MotorDriver * track7 ) {       \n    addTrack(0,track0);\n    addTrack(1,track1);\n    addTrack(2,track2);\n    addTrack(3,track3);\n    addTrack(4,track4);\n    addTrack(5,track5);\n    addTrack(6,track6);\n    addTrack(7,track7);\n    \n    // Default the first 2 tracks (which may be null) and perform HA waveform check.\n    setTrackMode(0,TRACK_MODE_MAIN);\n#ifndef DISABLE_PROG\n    setTrackMode(1,TRACK_MODE_PROG);\n#else\n    setTrackMode(1,TRACK_MODE_MAIN);\n#endif\n  \n  // Fault pin config for odd motor boards (example pololu)\n  FOR_EACH_TRACK(t) {\n    for (byte s=t+1;s<=lastTrack;s++) {\n      if (track[t]->getFaultPin() != UNUSED_PIN &&\n\t  track[t]->getFaultPin() == track[s]->getFaultPin()) {\n\ttrack[t]->setCommonFaultPin();\n\ttrack[s]->setCommonFaultPin();\n\tDIAG(F(\"Common Fault pin tracks %c and %c\"), t+'A', s+'A');\n      }\n    }\n  }\n  DCC::setShieldName(shieldname);\n}\n\nvoid TrackManager::addTrack(byte t, MotorDriver* driver) {\n     track[t]=driver;\n     if (driver) {\n         track[t]->setPower(POWERMODE::OFF);\n         track[t]->setMode(TRACK_MODE_NONE);\n\t track[t]->setTrackLetter('A'+t);\n         lastTrack=t;\n     } \n}\n\n// setDCCSignal(), called from interrupt context\n// does assume ports are shadowed if they can be\nvoid TrackManager::setDCCSignal( bool on) {\n  HAVE_PORTA(shadowPORTA=PORTA);\n  HAVE_PORTB(shadowPORTB=PORTB);\n  HAVE_PORTC(shadowPORTC=PORTC);\n  HAVE_PORTD(shadowPORTD=PORTD);\n  HAVE_PORTE(shadowPORTE=PORTE);\n  HAVE_PORTF(shadowPORTF=PORTF);\n  HAVE_PORTG(shadowPORTG=PORTG);\n  HAVE_PORTH(shadowPORTH=PORTH);\n  APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on));\n  HAVE_PORTA(PORTA=shadowPORTA);\n  HAVE_PORTB(PORTB=shadowPORTB);\n  HAVE_PORTC(PORTC=shadowPORTC);\n  HAVE_PORTD(PORTD=shadowPORTD);\n  HAVE_PORTE(PORTE=shadowPORTE);\n  HAVE_PORTF(PORTF=shadowPORTF);\n  HAVE_PORTG(PORTG=shadowPORTG);\n  HAVE_PORTH(PORTH=shadowPORTH);\n}\n\n// setPROGSignal(), called from interrupt context\n// does assume ports are shadowed if they can be\nvoid TrackManager::setPROGSignal( bool on) {\n  HAVE_PORTA(shadowPORTA=PORTA);\n  HAVE_PORTB(shadowPORTB=PORTB);\n  HAVE_PORTC(shadowPORTC=PORTC);\n  HAVE_PORTD(shadowPORTD=PORTD);\n  HAVE_PORTE(shadowPORTE=PORTE);\n  HAVE_PORTF(shadowPORTF=PORTF);\n  HAVE_PORTG(shadowPORTG=PORTG);\n  HAVE_PORTH(shadowPORTH=PORTH);\n  APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on));\n  HAVE_PORTA(PORTA=shadowPORTA);\n  HAVE_PORTB(PORTB=shadowPORTB);\n  HAVE_PORTC(PORTC=shadowPORTC);\n  HAVE_PORTD(PORTD=shadowPORTD);\n  HAVE_PORTE(PORTE=shadowPORTE);\n  HAVE_PORTF(PORTF=shadowPORTF);\n  HAVE_PORTG(PORTG=shadowPORTG);\n  HAVE_PORTH(PORTH=shadowPORTH);\n}\n\n// setDCSignal(), called from normal context\n// MotorDriver::setDCSignal handles shadowed IO port changes.\n// with interrupts turned off around the critical section\nvoid TrackManager::setDCSignal(int16_t cab, byte speedbyte) {\n  FOR_EACH_TRACK(t) {\n    if (trackDCAddr[t]!=cab && cab != 0) continue;\n    if (track[t]->getMode() & TRACK_MODE_DC)\n      track[t]->setDCSignal(speedbyte, DCC::getThrottleFrequency(trackDCAddr[t]));\n  }\n}    \n\nbool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr, bool offAtChange) {\n    if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;\n\n    // Remember track mode we came from for later\n    TRACK_MODE oldmode = track[trackToSet]->getMode();\n\n    //DIAG(F(\"Track=%c Mode=%d\"),trackToSet+'A', mode);\n    // DC tracks require a motorDriver that can set brake!\n    if (mode & TRACK_MODE_DC) {\n#if defined(ARDUINO_AVR_UNO)\n      DIAG(F(\"Uno has no PWM timers available for DC\"));\n      return false;\n#endif\n      if (!track[trackToSet]->brakeCanPWM()) {\n\tDIAG(F(\"Brake pin can't PWM: No DC\"));\n\treturn false;\n      }\n    }\n\n#ifdef ARDUINO_ARCH_ESP32\n    // remove pin from MUX matrix and turn it off\n    pinpair p = track[trackToSet]->getSignalPin();\n    //DIAG(F(\"Track=%c remove  pin %d\"),trackToSet+'A', p.pin);\n    gpio_reset_pin((gpio_num_t)p.pin);\n    if (p.invpin != UNUSED_PIN) {\n      //DIAG(F(\"Track=%c remove ^pin %d\"),trackToSet+'A', p.invpin);\n      gpio_reset_pin((gpio_num_t)p.invpin);\n    }\n#ifdef BOOSTER_INPUT\n    if (mode & TRACK_MODE_BOOST) {\n      //DIAG(F(\"Track=%c mode boost pin %d\"),trackToSet+'A', p.pin);\n      pinMode(BOOSTER_INPUT, INPUT);\n      gpio_matrix_in(BOOSTER_INPUT, SIG_IN_FUNC228_IDX, false); //pads 224 to 228 available as loopback\n      gpio_matrix_out(p.pin, SIG_IN_FUNC228_IDX, false, false);\n      if (p.invpin != UNUSED_PIN) {\n\tgpio_matrix_out(p.invpin, SIG_IN_FUNC228_IDX, true /*inverted*/, false);\n      }\n    } else // elseif clause continues\n#endif\n    if (mode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_DC)) {\n      // gpio_reset_pin may reset to input\n      pinMode(p.pin, OUTPUT);\n      if (p.invpin != UNUSED_PIN)\n\tpinMode(p.invpin, OUTPUT);\n    }\n\n#endif\n#ifndef DISABLE_PROG\n    if (mode & TRACK_MODE_PROG) {\n      // only allow 1 track to be prog\n      FOR_EACH_TRACK(t)\n\tif ( (track[t]->getMode() & TRACK_MODE_PROG) && t != trackToSet) {\n\t  track[t]->setPower(POWERMODE::OFF);\n\t  track[t]->setMode(TRACK_MODE_NONE);\n\t  track[t]->makeProgTrack(false);     // revoke prog track special handling\n\t  streamTrackState(NULL,t);\n\t}\n      track[trackToSet]->makeProgTrack(true); // set for prog track special handling\n    } else {\n      track[trackToSet]->makeProgTrack(false); // only the prog track knows it's type\n    }\n#endif\n\n    // When a track is switched, we must clear any side effects of its previous \n    // state, otherwise trains run away or just dont move.\n\n    // This can be done BEFORE the PWM-Timer evaluation (methinks)\n    if (mode & TRACK_MODE_DC) {\n      if (trackDCAddr[trackToSet] != dcAddr) {\n\t// new or changed DC Addr, run the new setup\n\tif (trackDCAddr[trackToSet] != 0) {\n\t  // if we change dcAddr and not only\n\t  // change from another mode,\n\t  // first detach old DC signal\n\t  track[trackToSet]->detachDCSignal();\n\t}\n#ifdef ARDUINO_ARCH_ESP32\n\tint trackfound = -1;\n\tFOR_EACH_TRACK(t) {\n\t  //DIAG(F(\"Checking track %c mode %x dcAddr %d\"), 'A'+t, track[t]->getMode(), trackDCAddr[t]);\n\t  if (t != trackToSet                          // not our track\n\t      && (track[t]->getMode() & TRACK_MODE_DC) // right mode\n\t      && trackDCAddr[t] == dcAddr) {           // right addr\n\t    //DIAG(F(\"Found track %c\"), 'A'+t);\n\t    trackfound = t;\n\t    break;\n\t  }\n\t}\n\tif (trackfound > -1) {\n\t  DCCTimer::DCCEXanalogCopyChannel(track[trackfound]->getBrakePinSigned(),\n\t\t\t\t\t   track[trackToSet]->getBrakePinSigned());\n\t}\n#endif\n      }\n      // set future DC Addr;\n      trackDCAddr[trackToSet]=dcAddr;\n    } else {\n      // DCC tracks need to have set the PWM to zero or they will not work.\n      track[trackToSet]->detachDCSignal();\n      track[trackToSet]->setBrake(false);\n      trackDCAddr[trackToSet]=0; // clear that an addr is set for DC as this is not a DC track\n    }\n    track[trackToSet]->setMode(mode);\n\n    // BOOST:\n    //  Leave it as is\n    // otherwise:\n    //  EXT is a special case where the signal pin is\n    //  turned off. So unless that is set, the signal\n    //  pin should be turned on\n    if (!(mode & TRACK_MODE_BOOST))\n      track[trackToSet]->enableSignal(!(mode & TRACK_MODE_EXT));\n\n#ifndef ARDUINO_ARCH_ESP32\n    // re-evaluate HighAccuracy mode\n    // We can only do this is all main and prog tracks agree\n    bool canDo=true;\n    FOR_EACH_TRACK(t) {\n      // DC tracks must not have the DCC PWM switched on\n      // so we globally turn it off if one of the PWM\n      // capable tracks is now DC or DCX.\n      if (track[t]->getMode() & TRACK_MODE_DC) {\n\tif (track[t]->isPWMCapable()) {\n\t  canDo=false;    // this track is capable but can not run PWM\n\t  break;          // in this mode, so abort and prevent globally below\n\t} else {\n\t  track[t]->trackPWM=false; // this track sure can not run with PWM\n\t  //DIAG(F(\"Track %c trackPWM 0 (not capable)\"), t+'A');\n\t}\n      } else if (track[t]->getMode() & (TRACK_MODE_MAIN |TRACK_MODE_PROG)) {\n\ttrack[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here\n\t//DIAG(F(\"Track %c trackPWM %d\"), t+'A', track[t]->trackPWM);\n\tcanDo &= track[t]->trackPWM;\n      }\n    }\n    if (canDo) DIAG(F(\"HA mode\")); \n    else {\n      // if we discover that HA mode was globally impossible\n      // we must adjust the trackPWM capabilities\n      FOR_EACH_TRACK(t) {\n\ttrack[t]->trackPWM=false;\n\t//DIAG(F(\"Track %c trackPWM 0 (global override)\"), t+'A');\n      }\n      DCCTimer::clearPWM(); // has to be AFTER trackPWM changes because if trackPWM==true this is undone for  that track\n    }\n    DCCWaveform::setRailcomPossible(canDo);\n#else\n    // For ESP32 we just reinitialize the DCC Waveform\n    DCCWaveform::begin();\n    // setMode() again AFTER Waveform::begin() of ESP32 fixes INVERTED signal\n    track[trackToSet]->setMode(mode);\n#endif\n\n    // This block must be AFTER the PWM-Timer modifications\n    if (mode & TRACK_MODE_DC) {\n        // DC tracks need to be given speed of the throttle for that cab address\n        // otherwise will not match other tracks on same cab.\n        // This also needs to allow for inverted DCX\n        applyDCSpeed(trackToSet);\n    }\n\n#ifdef ARDUINO_ARCH_ESP32\n#ifndef DISABLE_PROG\n    if (tempProgTrack == trackToSet && oldmode & TRACK_MODE_MAIN && !(mode & TRACK_MODE_PROG)) {\n      // If we just take away the prog track, the join should not\n      // be active either. So do in effect an unjoin\n      //DIAG(F(\"Unsync\"));\n      tempProgTrack = MAX_TRACKS+1;\n      progTrackSyncMain=false;\n      if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,LOW);\n    }\n#endif\n#endif\n    // Turn off power if we changed the mode of this track\n    if (mode != oldmode && offAtChange) {\n      track[trackToSet]->setPower(POWERMODE::OFF);\n    }\n    streamTrackState(NULL,trackToSet);\n    //DIAG(F(\"TrackMode=%d\"),mode);\n    return true; \n}\n\nvoid TrackManager::applyDCSpeed(byte t) {\n  track[t]->setDCSignal(DCC::getLocoSpeedByte(trackDCAddr[t]),\n\t\t\tDCC::getThrottleFrequency(trackDCAddr[t]));\n}\n\nbool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])\n{\n    \n    if (params==0) { // <=>  List track assignments\n        FOR_EACH_TRACK(t)\n             streamTrackState(stream,t);\n        return true;\n        \n    }\n    \n    p[0]-=\"A\"_hk;  // convert A... to 0.... \n\n    if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS)) \n        return false;\n    \n    if (params==2  && p[1]==\"MAIN\"_hk)                     // <= id MAIN>\n        return setTrackMode(p[0],TRACK_MODE_MAIN);\n    if (params==2  && p[1]==\"MAIN_INV\"_hk)                 // <= id MAIN_INV>\n        return setTrackMode(p[0],TRACK_MODE_MAIN_INV);\n    if (params==2  && p[1]==\"MAIN_AUTO\"_hk)                // <= id MAIN_AUTO>\n        return setTrackMode(p[0],TRACK_MODE_MAIN_AUTO);\n    \n#ifndef DISABLE_PROG\n    if (params==2  && p[1]==\"PROG\"_hk)                     // <= id PROG>\n        return setTrackMode(p[0],TRACK_MODE_PROG);\n#endif\n    \n    if (params==2  && (p[1]==\"OFF\"_hk || p[1]==\"NONE\"_hk)) // <= id OFF> <= id NONE>\n        return setTrackMode(p[0],TRACK_MODE_NONE);\n\n    if (params==2  && p[1]==\"EXT\"_hk) // <= id EXT>\n        return setTrackMode(p[0],TRACK_MODE_EXT);\n#ifdef BOOSTER_INPUT\n    if (TRACK_MODE_BOOST != 0 &&        // compile time optimization\n\tparams==2  && p[1]==\"BOOST\"_hk)                    // <= id BOOST>\n        return setTrackMode(p[0],TRACK_MODE_BOOST);\n    if (TRACK_MODE_BOOST_INV != 0 &&        // compile time optimization\n\tparams==2  && p[1]==\"BOOST_INV\"_hk)                // <= id BOOST_INV>\n        return setTrackMode(p[0],TRACK_MODE_BOOST_INV);\n    if (TRACK_MODE_BOOST_AUTO != 0 &&        // compile time optimization\n\tparams==2  && p[1]==\"BOOST_AUTO\"_hk)               // <= id BOOST_AUTO>\n        return setTrackMode(p[0],TRACK_MODE_BOOST_AUTO);\n#endif\n    if (params==2  && p[1]==\"AUTO\"_hk)                     // <= id AUTO>\n      return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_AUTO);\n\n    if (params==2  && p[1]==\"INV\"_hk)                      // <= id INV>\n      return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_INV);\n\n    if (params==3  && p[1]==\"DC\"_hk && p[2]>0)             // <= id DC cab>\n        return setTrackMode(p[0],TRACK_MODE_DC,p[2]);\n    \n    if (params==3  && (p[1]==\"DC_INV\"_hk ||                // <= id DC_INV cab>\n\t\t       p[1]==\"DCX\"_hk) && p[2]>0)          // <= id DCX cab>\n        return setTrackMode(p[0],TRACK_MODE_DC_INV,p[2]);\n\n    return false;\n}\n\nconst FSH* TrackManager::getModeName(TRACK_MODE tm) {\n  const FSH *modename=F(\"---\");\n  \n  if (tm & TRACK_MODE_MAIN) {\n    if(tm & TRACK_MODIFIER_AUTO)\n      modename=F(\"MAIN A\");\n    else if (tm & TRACK_MODIFIER_INV)\n      modename=F(\"MAIN I\");\n    else\n      modename=F(\"MAIN\");\n  }\n#ifndef DISABLE_PROG\n  else if (tm & TRACK_MODE_PROG)\n    modename=F(\"PROG\");\n#endif\n  else if (tm & TRACK_MODE_NONE)\n    modename=F(\"NONE\");\n  else if(tm & TRACK_MODE_EXT)\n    modename=F(\"EXT\");\n  else if(tm & TRACK_MODE_BOOST) {\n        if(tm & TRACK_MODIFIER_AUTO)\n      modename=F(\"BOOST A\");\n    else if (tm & TRACK_MODIFIER_INV)\n      modename=F(\"BOOST I\");\n    else\n      modename=F(\"BOOST\");\n  }\n  else if (tm & TRACK_MODE_DC) {\n    if (tm & TRACK_MODIFIER_INV)\n      modename=F(\"DCX\");\n    else\n      modename=F(\"DC\");\n  }\n  return modename;\n}\n\n// null stream means send to commandDistributor for broadcast\nvoid TrackManager::streamTrackState(Print* stream, byte t) {\n  const FSH *format;\n  \n  if (track[t]==NULL) return;\n  TRACK_MODE tm = track[t]->getMode();\n  if (tm & TRACK_MODE_DC)\n    format=F(\"<= %c %S %d>\\n\");\n  else\n    format=F(\"<= %c %S>\\n\");\n\n  const FSH *modename=getModeName(tm);\n  if (stream) {  // null stream means send to commandDistributor for broadcast\n    StringFormatter::send(stream,format,'A'+t, modename, trackDCAddr[t]);\n  } else {\n    CommandDistributor::broadcastTrackState(format,'A'+t, modename, trackDCAddr[t]);\n    CommandDistributor::broadcastPower();\n  }\n  \n}\n\nbyte TrackManager::nextCycleTrack=MAX_TRACKS;\n\nvoid TrackManager::loop() {\n    DCCWaveform::loop();\n#ifndef DISABLE_PROG\n    DCCACK::loop();\n#endif\n    bool dontLimitProg=DCCACK::isActive() || progTrackSyncMain || progTrackBoosted;\n    nextCycleTrack++;\n    if (nextCycleTrack>lastTrack) nextCycleTrack=0;\n    if (track[nextCycleTrack]==NULL) return;\n    MotorDriver * motorDriver=track[nextCycleTrack];\n    bool useProgLimit=dontLimitProg ? false : (bool)(track[nextCycleTrack]->getMode() & TRACK_MODE_PROG);\n    motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);   \n}\n\nMotorDriver * TrackManager::getProgDriver() {\n    FOR_EACH_TRACK(t)\n      if (track[t]->getMode() & TRACK_MODE_PROG) return track[t];\n    return NULL;\n} \n\n#ifdef ARDUINO_ARCH_ESP32\nstd::vector<MotorDriver *>TrackManager::getMainDrivers() {\n  std::vector<MotorDriver *>  v;\n  FOR_EACH_TRACK(t)\n    if (track[t]->getMode() & TRACK_MODE_MAIN) v.push_back(track[t]);\n  return v;\n}\n#endif\n\n// Set track power for all tracks with this mode\nvoid TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) {\n  bool didChange=false;\n  FOR_EACH_TRACK(t) {\n    MotorDriver *driver=track[t];\n    TRACK_MODE trackmodeOfTrack = driver->getMode();\n    if (trackmodeToMatch & trackmodeOfTrack) {\n      if (powermode != driver->getPower())\n\tdidChange=true;\n      if (powermode == POWERMODE::ON) {\n\tif (trackmodeOfTrack & TRACK_MODE_DC) {\n\t  driver->setBrake(true);   // DC starts with brake on\n\t  applyDCSpeed(t);          // speed match DCC throttles\n\t} else {\n\t  // toggle brake before turning power on - resets overcurrent error\n\t  // on the Pololu board if brake is wired to ^D2.\n\t  driver->setBrake(true);\n\t  driver->setBrake(false); // DCC runs with brake off\n\t}\n      }\n      driver->setPower(powermode);\n    }\n  }\n  if (didChange)\n    CommandDistributor::broadcastPower();\n}\n\n// Set track power for this track, inependent of mode\nvoid TrackManager::setTrackPower(POWERMODE powermode, byte t) {\n  MotorDriver *driver=track[t];\n  if (driver == NULL) { // track is not defined at all\n    DIAG(F(\"Error: Track %c does not exist\"), t+'A');\n    return;\n  }\n  TRACK_MODE trackmode = driver->getMode();\n  POWERMODE oldpower = driver->getPower();\n  if (trackmode & TRACK_MODE_NONE) {\n    driver->setBrake(true);     // Track is unused. Brake is good to have.\n    powermode = POWERMODE::OFF; // Track is unused. Force it to OFF\n  } else if (trackmode & TRACK_MODE_DC) { // includes inverted DC (called DCX)\n    if (powermode == POWERMODE::ON) {\n      driver->setBrake(true);   // DC starts with brake on\n      applyDCSpeed(t);          // speed match DCC throttles\n    }\n  } else /* MAIN PROG EXT BOOST */ {\n    if (powermode == POWERMODE::ON) {\n      // toggle brake before turning power on - resets overcurrent error\n      // on the Pololu board if brake is wired to ^D2.\n      driver->setBrake(true);\n      driver->setBrake(false); // DCC runs with brake off\n    }\n  }\n  driver->setPower(powermode);\n  if (oldpower != driver->getPower())\n    CommandDistributor::broadcastPower();\n}\n\n// returns state of the one and only prog track\nPOWERMODE TrackManager::getProgPower() {\n  FOR_EACH_TRACK(t)\n    if (track[t]->getMode() & TRACK_MODE_PROG)\n      return track[t]->getPower(); // optimize: there is max one prog track\n  return POWERMODE::OFF;\n}\n\n// returns on if all are on. returns off otherwise\nPOWERMODE TrackManager::getMainPower() {\n  POWERMODE result = POWERMODE::OFF;\n  FOR_EACH_TRACK(t) {\n    if (track[t]->getMode() & TRACK_MODE_MAIN) {\n      POWERMODE p = track[t]->getPower();\n      if (p == POWERMODE::OFF)\n\treturn POWERMODE::OFF; // done and out\n      if (p == POWERMODE::ON)\n\tresult = POWERMODE::ON;\n    }\n  }\n  return result;\n}\n\nbool TrackManager::getPower(byte t, char s[]) {\n  if (t > lastTrack)\n    return false;\n  if (track[t]) {\n    s[0] = track[t]->getPower() == POWERMODE::ON ? '1' : '0';\n    s[2] = t + 'A';\n    return true;\n  }\n  return false;\n}\n\nvoid TrackManager::reportObsoleteCurrent(Print* stream) {\n  // This function is for backward JMRI compatibility only\n  // It reports the first track only, as main, regardless of track settings.\n  //  <c MeterName value C/V unit min max res warn>\n#ifdef HAS_ENOUGH_MEMORY\n  int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());\n  StringFormatter::send(stream, F(\"<c CurrentMAIN %d C Milli 0 %d 1 %d>\\n\"), \n            track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);\n#else\n  (void)stream;\n#endif\n}\n\nvoid TrackManager::reportCurrent(Print* stream) {\n    StringFormatter::send(stream,F(\"<jI\"));\n    FOR_EACH_TRACK(t) {\n         StringFormatter::send(stream, F(\" %d\"),\n         (track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :\n            track[t]->raw2mA(track[t]->getCurrentRaw(false)));\n         }\n    StringFormatter::send(stream,F(\">\\n\"));    \n}\n\nvoid TrackManager::reportCurrentLCD(uint8_t display, byte row) {\n  int16_t trackPwrTotalMA = 0;\n  FOR_EACH_TRACK(t) {\n    bool pstate = TrackManager::isPowerOn(t);  // checks if power is on or off\n    TRACK_MODE tMode=(TrackManager::getMode(t)); // gets to current power mode\n    int16_t DCAddr=(TrackManager::returnDCAddr(t));\n\n      if (pstate) {                                                 // if power is on do this section\n        trackPwrMA[t]=(3*trackPwrMA[t]>>2) + ((track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :\n                        track[t]->raw2mA(track[t]->getCurrentRaw(false)));\n        trackPwrTotalMA += trackPwrMA[t];\n        if (tMode & TRACK_MODE_DC) {    // Test if track is in DC or DCX mode\n          SCREEN(display, row+t, F(\"%c: %S %d  %dmA\"), t+'A', (TrackManager::getModeName(tMode)),DCAddr, trackPwrMA[t]>>2);\n        }\n        else {                                                      // formats without DCAddress\n          SCREEN(display, row+t, F(\"%c: %S  %dmA\"), t+'A', (TrackManager::getModeName(tMode)), trackPwrMA[t]>>2);\n        }\n      } \n      else {                                                        // if power is off do this section\n        trackPwrMA[t] = 0;\n        if (tMode & TRACK_MODE_DC) {   // DC / DCX\n          SCREEN(display, row+t, F(\"%c: %S %d\"), t+'A', (TrackManager::getModeName(tMode)),DCAddr);\n        }\n        else {                                                      // Not DC or DCX\n          SCREEN(display, row+t, F(\"%c: %S\"), t+'A', (TrackManager::getModeName(tMode)));\n        }\n      }\n  }\n  SCREEN(display, row+lastTrack+1, F(\"%d Districts  %dmA\"), lastTrack+1,  trackPwrTotalMA>>2);\n} \n\nvoid TrackManager::reportGauges(Print* stream) {\n    StringFormatter::send(stream,F(\"<jG\"));\n    FOR_EACH_TRACK(t) {\n         StringFormatter::send(stream, F(\" %d\"),\n            track[t]->raw2mA(track[t]->getRawCurrentTripValue()));\n         }\n    StringFormatter::send(stream,F(\">\\n\"));    \n}\n\nvoid TrackManager::setJoinRelayPin(byte joinRelayPin) {\n  joinRelay=joinRelayPin;\n  if (joinRelay!=UNUSED_PIN) {\n    pinMode(joinRelay,OUTPUT);\n    digitalWrite(joinRelay,LOW);  // LOW is relay disengaged\n  }\n}\n\nvoid TrackManager::setJoin(bool joined) {\n#ifdef ARDUINO_ARCH_ESP32\n  if (joined) {                                          // if we go into joined mode (PROG acts as MAIN)\n    FOR_EACH_TRACK(t) {\n      if (track[t]->getMode() & TRACK_MODE_PROG) {       // find PROG track\n\ttempProgTrack = t;                               // remember PROG track\n\tsetTrackMode(t, TRACK_MODE_MAIN, 0, false);      // 0 = no DC loco; false = do not turn off pwr\n\t// then in some cases setPower() is called\n\t// seperately after the setJoin() as well\n\tgoto success;                                    // there is only one prog track, done\n      }\n    }\n    return;                                              // no prog track found, can not do more\n  success: /* continue here when prog tack found */;\n  } else {\n    if (tempProgTrack != MAX_TRACKS+1) {\n      // setTrackMode defaults to power off, so we\n      // need to preserve that state.\n      setTrackMode(tempProgTrack, TRACK_MODE_PROG, 0, false); // 0 = no DC loco; false = do not turn off pwr\n      tempProgTrack = MAX_TRACKS+1;\n    }\n  }\n#endif\n  progTrackSyncMain=joined;\n  if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,joined?HIGH:LOW);\n}\n\nbool TrackManager::isPowerOn(byte t) {\n      if (track[t]->getPower()!=POWERMODE::ON) \n\t        return false;\n    return true;   \n  }\n\nbool TrackManager::isProg(byte t) {\n    if (track[t]->getMode() & TRACK_MODE_PROG)\n        return true;\n    return false;\n}\n\nTRACK_MODE TrackManager::getMode(byte t) {\n    return (track[t]->getMode());\n}\n\nint16_t TrackManager::returnDCAddr(byte t) {\n    return (trackDCAddr[t]);\n}\n"
  },
  {
    "path": "TrackManager.h",
    "content": "/*\n *  © 2022 Chris Harlow\n *  © 2022-2024 Harald Barth\n *  © 2023 Colin Murdoch\n *  © 2025 Herb Morton\n * \n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifdef ARDUINO_ARCH_ESP32\n#include <vector>\n#endif\n#ifndef TrackManager_h\n#define TrackManager_h\n#include \"FSH.h\"\n#include \"MotorDriver.h\"\n// Virtualised Motor shield multi-track hardware Interface\n\n// These constants help EXRAIL macros say SET_TRACK(2,mode) OR SET_TRACK(C,mode) etc.\nconst byte TRACK_NUMBER_0=0, TRACK_NUMBER_A=0;    \nconst byte TRACK_NUMBER_1=1, TRACK_NUMBER_B=1;    \nconst byte TRACK_NUMBER_2=2, TRACK_NUMBER_C=2;    \nconst byte TRACK_NUMBER_3=3, TRACK_NUMBER_D=3;    \nconst byte TRACK_NUMBER_4=4, TRACK_NUMBER_E=4;    \nconst byte TRACK_NUMBER_5=5, TRACK_NUMBER_F=5;    \nconst byte TRACK_NUMBER_6=6, TRACK_NUMBER_G=6;    \nconst byte TRACK_NUMBER_7=7, TRACK_NUMBER_H=7;    \n\n// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).\nconst byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;    \nconst byte TRACK_POWER_1=1, TRACK_POWER_ON=1;   \n\nclass TrackManager {\n  public:\n    static void Setup(const FSH * shieldName,\n                 MotorDriver * track0=NULL,\n                 MotorDriver * track1=NULL,\n                 MotorDriver * track2=NULL,\n                 MotorDriver * track3=NULL,\n                 MotorDriver * track4=NULL,\n                 MotorDriver * track5=NULL,\n                 MotorDriver * track6=NULL,\n                 MotorDriver * track7=NULL\n                 );\n    \n    static void setDCCSignal( bool on);\n    static void setPROGSignal( bool on);\n    static void setDCSignal(int16_t cab, byte speedbyte);\n    static MotorDriver * getProgDriver();\n#ifdef ARDUINO_ARCH_ESP32\n    static std::vector<MotorDriver *>getMainDrivers();\n#endif\n  \n    static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);}\n    static void setTrackPower(POWERMODE mode, byte t);\n    static void setTrackPower(TRACK_MODE trackmode, POWERMODE powermode);\n    static void setMainPower(POWERMODE mode) {setTrackPower(TRACK_MODE_MAIN, mode);}\n    static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);}\n\n    static const int16_t MAX_TRACKS=8;\n    static inline int8_t numTracks() { return lastTrack + 1; }\n    static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0, bool offAtChange=true);\n    static bool parseEqualSign(Print * stream,  int16_t params, int16_t p[]);\n    static void loop();\n    static POWERMODE getMainPower();\n    static POWERMODE getProgPower();\n    static inline POWERMODE getPower(byte t) { return track[t]->getPower(); }\n    static bool getPower(byte t, char s[]);\n    static void setJoin(bool join);\n    static bool isJoined() { return progTrackSyncMain;}\n    static inline bool isActive (byte tr) {\n      if (tr > lastTrack) return false;\n      return track[tr]->getMode() & (TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT);}\n    static void setJoinRelayPin(byte joinRelayPin);\n    static void sampleCurrent();\n    static void reportGauges(Print* stream);\n    static void reportCurrent(Print* stream);\n    static void reportCurrentLCD(uint8_t display, byte row);\n    static void reportObsoleteCurrent(Print* stream); \n    static void streamTrackState(Print* stream, byte t);\n    static bool isPowerOn(byte t);\n    static bool isProg(byte t);\n    static TRACK_MODE getMode(byte t);\n    static int16_t returnDCAddr(byte t);\n    static const FSH* getModeName(TRACK_MODE Mode);\n\n    static int16_t joinRelay;\n    static bool progTrackSyncMain;  // true when prog track is a siding switched to main\n    static bool progTrackBoosted;   // true when prog track is not current limited\n\n#ifdef DEBUG_ADC\n  public:\n#else\n  private:\n#endif\n    static MotorDriver* track[MAX_TRACKS];\n\n  private:\n    static void addTrack(byte t, MotorDriver* driver);\n    static int8_t lastTrack;\n    static byte nextCycleTrack;\n    static void applyDCSpeed(byte t);\n\n    static int16_t trackDCAddr[MAX_TRACKS];  // dc address if TRACK_MODE_DC\n    static int16_t trackPwrMA[MAX_TRACKS];      // for <JL ..> command\n#ifdef ARDUINO_ARCH_ESP32\n    static byte tempProgTrack; // holds the prog track number during join\n#endif\n    };\n\n#endif\n"
  },
  {
    "path": "Turnouts.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 M Steve Todd\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2021 Chris Harlow\n *  © 2013-2016 Gregg E. Berman\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n#include \"defines.h\"  // includes config.h\n#ifndef DISABLE_EEPROM\n#include \"EEStore.h\"\n#endif\n#include \"StringFormatter.h\"\n#include \"CommandDistributor.h\"\n#include \"EXRAIL2.h\"\n#include \"Turnouts.h\"\n#include \"DCC.h\"\n#include \"LCN.h\"\n#ifdef EESTOREDEBUG\n#include \"DIAG.h\"\n#endif\n\n  /* \n   * Protected static data\n   */ \n\n  /* static */ Turnout *Turnout::_firstTurnout = 0;\n\n  /* \n   * Public static data\n   */\n  /* static */ int Turnout::turnoutlistHash = 0;\n \n  /*\n   * Protected static functions\n   */\n\n  /* static */ Turnout *Turnout::get(uint16_t id) {\n    // Find turnout object from list.\n    for (Turnout *tt = _firstTurnout; tt != NULL; tt = tt->_nextTurnout)\n      if (tt->_turnoutData.id == id) return tt;\n    return NULL;\n  }\n\n  // Add new turnout to end of chain\n  /* static */ void Turnout::add(Turnout *tt) {\n    if (!_firstTurnout) \n      _firstTurnout = tt;\n    else {\n      // Find last object on chain\n      Turnout *ptr = _firstTurnout;\n      for ( ; ptr->_nextTurnout!=0; ptr=ptr->_nextTurnout) {}\n      // Line new object to last object.\n      ptr->_nextTurnout = tt;\n    }\n    turnoutlistHash++;\n  }\n  \n  \n\n  // Remove nominated turnout from turnout linked list and delete the object.\n  /* static */ bool Turnout::remove(uint16_t id) {\n    Turnout *tt,*pp=NULL;\n\n    for(tt=_firstTurnout; tt!=NULL && tt->_turnoutData.id!=id; pp=tt, tt=tt->_nextTurnout) {}\n    if (tt == NULL) return false;\n    \n    if (tt == _firstTurnout)\n      _firstTurnout = tt->_nextTurnout;\n    else\n      pp->_nextTurnout = tt->_nextTurnout;\n\n    delete (ServoTurnout *)tt;\n\n    turnoutlistHash++;\n    return true; \n  } \n\n\n  /*\n   * Public static functions\n   */\n\n  /* static */ bool Turnout::isClosed(uint16_t id) {\n    Turnout *tt = get(id);\n    if (tt) \n      return tt->isClosed();\n    else\n      return false;\n  }\n\n  /* static */ bool Turnout::setClosedStateOnly(uint16_t id, bool closeFlag) {\n    Turnout *tt = get(id);\n    if (!tt) return false;\n    // I know it says setClosedStateOnly, but we need to tell others\n    // that the state has changed too. But we only broadcast if there\n    // really has been a change.\n    if (tt->_turnoutData.closed != closeFlag) {\n      tt->_turnoutData.closed = closeFlag;\n      CommandDistributor::broadcastTurnout(id, closeFlag);\n    }\n#if defined(EXRAIL_ACTIVE)\n    RMFT2::turnoutEvent(id, closeFlag);\n#endif\n    return true;\n  }\n\n  // Static setClosed function is invoked from close(), throw() etc. to perform the \n  //  common parts of the turnout operation.  Code which is specific to a turnout\n  //  type should be placed in the virtual function setClosedInternal(bool) which is\n  //  called from here.\n  /* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) { \n#if defined(DIAG_IO)\n    DIAG(F(\"Turnout(%d,%c)\"), id, closeFlag ? 'c':'t');\n#endif\n    Turnout *tt = Turnout::get(id);\n    if (!tt) return false;\n    bool ok = tt->setClosedInternal(closeFlag);\n\n    if (ok) {\n      tt->setClosedStateOnly(id, closeFlag);\n#ifndef DISABLE_EEPROM\n      // Write byte containing new closed/thrown state to EEPROM if required.  Note that eepromAddress\n      // is always zero for LCN turnouts.\n      if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0) \n        EEPROM.put(tt->_eepromAddress, tt->_turnoutData.flags);\n#endif\n    }\n    return ok;\n  }\n\n#ifndef DISABLE_EEPROM\n  // Load all turnout objects\n  /* static */ void Turnout::load() {\n    for (uint16_t i=0; i<EEStore::eeStore->data.nTurnouts; i++) {\n      Turnout::loadTurnout();\n    }\n  }\n\n  // Save all turnout objects\n  /* static */ void Turnout::store() {\n    EEStore::eeStore->data.nTurnouts=0;\n    for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) {\n      tt->save();\n      EEStore::eeStore->data.nTurnouts++;\n    }\n  }\n\n  // Load one turnout from EEPROM\n  /* static */ Turnout *Turnout::loadTurnout () {\n    Turnout *tt = 0;\n    // Read turnout type from EEPROM\n    struct TurnoutData turnoutData;\n    int eepromAddress = EEStore::pointer() + offsetof(struct TurnoutData, flags); // Address of byte containing the closed flag.\n    EEPROM.get(EEStore::pointer(), turnoutData);\n    EEStore::advance(sizeof(turnoutData));\n\n    switch (turnoutData.turnoutType) {\n      case TURNOUT_SERVO:\n        // Servo turnout\n        tt = ServoTurnout::load(&turnoutData);\n        break;\n      case TURNOUT_DCC:\n        // DCC Accessory turnout\n        tt = DCCTurnout::load(&turnoutData);\n        break;\n      case TURNOUT_VPIN:\n        // VPIN turnout\n        tt = VpinTurnout::load(&turnoutData);\n        break;\n      default:\n        // If we find anything else, then we don't know what it is or how long it is, \n        // so we can't go any further through the EEPROM!\n        return NULL;\n    }\n    if (tt) {\n      // Save EEPROM address in object.  Note that LCN turnouts always have eepromAddress of zero.\n      tt->_eepromAddress = eepromAddress + offsetof(struct TurnoutData, flags);\n    }\n\n#ifdef EESTOREDEBUG\n    printAll(&USB_SERIAL);\n#endif\n    return tt;\n  }\n#endif\n\n/*************************************************************************************\n * ServoTurnout - Turnout controlled by servo device.\n * \n *************************************************************************************/\n\n  // Private Constructor\n  ServoTurnout::ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) :\n    Turnout(id, TURNOUT_SERVO, closed) \n  {\n    _servoTurnoutData.vpin = vpin;\n    _servoTurnoutData.thrownPosition = thrownPosition; \n    _servoTurnoutData.closedPosition = closedPosition;\n    _servoTurnoutData.profile = profile;\n  }\n\n  // Create function\n  /* static */ Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) {\n#ifndef IO_NO_HAL\n    Turnout *tt = get(id);\n    if (tt) { \n      // Object already exists, check if it is usable\n      if (tt->isType(TURNOUT_SERVO)) {\n        // Yes, so set parameters\n        ServoTurnout *st = (ServoTurnout *)tt;\n        st->_servoTurnoutData.vpin = vpin;\n        st->_servoTurnoutData.thrownPosition = thrownPosition;\n        st->_servoTurnoutData.closedPosition = closedPosition;\n        st->_servoTurnoutData.profile = profile;\n        // Don't touch the _closed parameter, retain the original value.\n\n        // We don't really need to do the following, since a call to IODevice::_writeAnalogue \n        //  will provide all the data that is required!  However, if someone has configured \n        //  a Turnout, we should ensure that the SET() RESET() and other commands that use write() \n        //  behave consistently with the turnout commands.\n        IODevice::configureServo(vpin, thrownPosition, closedPosition, profile, 0, closed);\n\n        // Set position directly to specified position - we don't know where it is moving from.\n        IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);\n\n        return tt;\n      } else {\n        // Incompatible object, delete and recreate\n        remove(id);\n      }\n    }\n    tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed);\n    DIAG(F(\"Turnout 0x%x size %d size %d\"), tt, sizeof(Turnout),sizeof(struct TurnoutData));\n    IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);\n    return tt;\n#else\n    (void)id; (void)vpin; (void)thrownPosition; (void)closedPosition;\n    (void)profile; (void)closed;          // avoid compiler warnings.\n    return NULL;\n#endif\n  }\n\n  // Load a Servo turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  Turnout *ServoTurnout::load(struct TurnoutData *turnoutData) {\n#ifndef DISABLE_EEPROM\n    ServoTurnoutData servoTurnoutData;\n    // Read class-specific data from EEPROM\n    EEPROM.get(EEStore::pointer(), servoTurnoutData);\n    EEStore::advance(sizeof(servoTurnoutData));\n    \n    // Create new object\n    Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition,\n      servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed);\n    return tt;\n#else\n    (void)turnoutData;\n    return NULL;\n#endif\n  }\n\n  // For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed\n  void ServoTurnout::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<H %d SERVO %d %d %d %d %d>\\n\"), _turnoutData.id, _servoTurnoutData.vpin, \n      _servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile, \n      !_turnoutData.closed);\n  }\n\n  // ServoTurnout-specific code for throwing or closing a servo turnout.\n  bool ServoTurnout::setClosedInternal(bool close) {\n#ifndef IO_NO_HAL\n    IODevice::writeAnalogue(_servoTurnoutData.vpin, \n      close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile);\n#else\n    (void)close;  // avoid compiler warnings\n#endif\n    return true;\n  }\n\n  void ServoTurnout::save() {\n#ifndef DISABLE_EEPROM\n    // Write turnout definition and current position to EEPROM\n    // First write common servo data, then\n    // write the servo-specific data\n    EEPROM.put(EEStore::pointer(), _turnoutData);\n    EEStore::advance(sizeof(_turnoutData));\n    EEPROM.put(EEStore::pointer(), _servoTurnoutData);\n    EEStore::advance(sizeof(_servoTurnoutData));\n#endif\n  }\n\n/*************************************************************************************\n * DCCTurnout - Turnout controlled by DCC Accessory Controller.\n * \n *************************************************************************************/\n\n  // DCCTurnoutData contains data specific to this subclass that is \n  // written to EEPROM when the turnout is saved.\n  struct DCCTurnoutData {\n    // DCC address (Address in bits 15-2, subaddress in bits 1-0\n    uint16_t address; // CS currently supports linear address 1-2048\n      // That's DCC accessory address 1-512 and subaddress 0-3.\n  } _dccTurnoutData; // 2 bytes\n\n  // Constructor\n  DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) :\n    Turnout(id, TURNOUT_DCC, false)\n  {\n    _dccTurnoutData.address = address;\n    _dccTurnoutData.subAddress = subAdd;\n  }\n\n  // Create function\n  /* static */ Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) {\n    Turnout *tt = get(id);\n    if (tt) { \n      // Object already exists, check if it is usable\n      if (tt->isType(TURNOUT_DCC)) {\n        // Yes, so set parameters<T>\n        DCCTurnout *dt = (DCCTurnout *)tt;\n        dt->_dccTurnoutData.address = add;\n        dt->_dccTurnoutData.subAddress = subAdd;\n        // Don't touch the _closed parameter, retain the original value.\n        return tt;\n      } else {\n        // Incompatible object, delete and recreate\n        remove(id);\n      }\n    }\n    tt = (Turnout *)new DCCTurnout(id, add, subAdd);\n    return tt;\n  }\n\n  // Load a DCC turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  /* static */ Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) {\n#ifndef DISABLE_EEPROM\n    DCCTurnoutData dccTurnoutData;\n    // Read class-specific data from EEPROM\n    EEPROM.get(EEStore::pointer(), dccTurnoutData);\n    EEStore::advance(sizeof(dccTurnoutData));\n    \n    // Create new object\n    DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress);\n\n    return tt;\n#else\n    (void)turnoutData;\n    return NULL;\n#endif\n  }\n\n  void DCCTurnout::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<H %d DCC %d %d %d>\\n\"), _turnoutData.id, \n      _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); \n    // Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this.\n    StringFormatter::send(stream, F(\"<H %d %d %d %d>\\n\"), _turnoutData.id, \n      _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); \n  }\n\n  bool DCCTurnout::setClosedInternal(bool close) {\n    // DCC++ Classic behaviour is that Throw writes a 1 in the packet,\n    // and Close writes a 0.  \n    // RCN-213 specifies that Throw is 0 and Close is 1.\n#ifndef DCC_TURNOUTS_RCN_213\n    close = !close;\n#endif\n    DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);\n    return true;\n  }\n\n  void DCCTurnout::save() {\n#ifndef DISABLE_EEPROM\n    // Write turnout definition and current position to EEPROM\n    // First write common servo data, then\n    // write the servo-specific data\n    EEPROM.put(EEStore::pointer(), _turnoutData);\n    EEStore::advance(sizeof(_turnoutData));\n    EEPROM.put(EEStore::pointer(), _dccTurnoutData);\n    EEStore::advance(sizeof(_dccTurnoutData));\n#endif\n  }\n\n\n\n/*************************************************************************************\n * VpinTurnout - Turnout controlled through a HAL vpin.\n * \n *************************************************************************************/\n\n  // Constructor\n  VpinTurnout::VpinTurnout(uint16_t id, VPIN vpin, bool closed) :\n    Turnout(id, TURNOUT_VPIN, closed)\n  {\n    _vpinTurnoutData.vpin = vpin;\n  }\n\n  // Create function\n  /* static */ Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) {\n    Turnout *tt = get(id);\n    if (tt) { \n      // Object already exists, check if it is usable\n      if (tt->isType(TURNOUT_VPIN)) {\n        // Yes, so set parameters\n        VpinTurnout *vt = (VpinTurnout *)tt;\n        vt->_vpinTurnoutData.vpin = vpin;\n        // Don't touch the _closed parameter, retain the original value.\n        return tt;\n      } else {\n        // Incompatible object, delete and recreate\n        remove(id);\n      }\n    }\n    tt = (Turnout *)new VpinTurnout(id, vpin, closed);\n    return tt;\n  }\n\n  // Load a VPIN turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  /* static */ Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) {\n#ifndef DISABLE_EEPROM\n    VpinTurnoutData vpinTurnoutData;\n    // Read class-specific data from EEPROM\n    EEPROM.get(EEStore::pointer(), vpinTurnoutData);\n    EEStore::advance(sizeof(vpinTurnoutData));\n    \n    // Create new object\n    VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed);\n\n    return tt;\n#else\n    (void)turnoutData;\n    return NULL;\n#endif\n  }\n\n  // Report 1 for thrown, 0 for closed.\n  void VpinTurnout::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<H %d VPIN %d %d>\\n\"), _turnoutData.id, _vpinTurnoutData.vpin, \n      !_turnoutData.closed); \n  }\n\n  bool VpinTurnout::setClosedInternal(bool close) {\n    IODevice::write(_vpinTurnoutData.vpin, close);\n    return true;\n  }\n\n  void VpinTurnout::save() {\n#ifndef DISABLE_EEPROM\n    // Write turnout definition and current position to EEPROM\n    // First write common servo data, then\n    // write the servo-specific data\n    EEPROM.put(EEStore::pointer(), _turnoutData);\n    EEStore::advance(sizeof(_turnoutData));\n    EEPROM.put(EEStore::pointer(), _vpinTurnoutData);\n    EEStore::advance(sizeof(_vpinTurnoutData));\n#endif\n  }\n\n\n/*************************************************************************************\n * LCNTurnout - Turnout controlled by Loconet\n * \n *************************************************************************************/\n\n  // LCNTurnout has no specific data, and in any case is not written to EEPROM!\n  // struct LCNTurnoutData {\n  // } _lcnTurnoutData; // 0 bytes\n\n  // Constructor\n  LCNTurnout::LCNTurnout(uint16_t id, bool closed) :\n    Turnout(id, TURNOUT_LCN, closed)\n  { }\n\n  // Create function\n  /* static */ Turnout *LCNTurnout::create(uint16_t id, bool closed) {\n    Turnout *tt = get(id);\n    if (tt) { \n      // Object already exists, check if it is usable\n      if (tt->isType(TURNOUT_LCN)) {\n        // Yes, so return this object\n        return tt;\n      } else {\n        // Incompatible object, delete and recreate\n        remove(id);\n      }\n    }\n    tt = (Turnout *)new LCNTurnout(id, closed);\n    return tt;\n  }\n\n  bool LCNTurnout::setClosedInternal(bool close) {\n    // Assume that the LCN command still uses 1 for throw and 0 for close...\n    LCN::send('T', _turnoutData.id, !close);\n    // The _turnoutData.closed flag should be updated by a message from the LCN master.\n    // but in this implementation it is updated in setClosedStateOnly() instead.\n    // If the LCN master updates this, setClosedStateOnly() and all setClosedInternal()\n    // have to be updated accordingly so that the closed flag is only set once.\n    return true;\n  }\n\n  // LCN turnouts not saved to EEPROM.\n  //void save() override {  }\n  //static Turnout *load(struct TurnoutData *turnoutData) {\n\n  // Report 1 for thrown, 0 for closed.\n  void LCNTurnout::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<H %d LCN %d>\\n\"), _turnoutData.id, \n    !_turnoutData.closed); \n  }\n"
  },
  {
    "path": "Turnouts.h",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 M Steve Todd\n *  © 2021 Fred Decker\n *  © 2020-2021 Harald Barth\n *  © 2020-2022 Chris Harlow\n *  © 2013-2016 Gregg E. Berman\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef TURNOUTS_H\n#define TURNOUTS_H\n\n//#define EESTOREDEBUG \n#include \"Arduino.h\"\n#include \"IODevice.h\"\n#include \"StringFormatter.h\"\n\n// Turnout type definitions\nenum {\n  TURNOUT_DCC = 1,\n  TURNOUT_SERVO = 2,\n  TURNOUT_VPIN = 3,\n  TURNOUT_LCN = 4,\n};\n\n/*************************************************************************************\n * Turnout - Base class for turnouts.\n * \n *************************************************************************************/\n\nclass Turnout {\nprotected:\n  /* \n   * Object data\n   */\n\n  // The TurnoutData struct contains data common to all turnout types, that \n  // is written to EEPROM when the turnout is saved.\n  // The first byte of this struct contains the 'closed' flag which is\n  // updated whenever the turnout changes from thrown to closed and\n  // vice versa.  If the turnout has been saved, then this byte is rewritten\n  // when changed in RAM.  The 'closed' flag must be located in the first byte.\n  struct TurnoutData {\n    union {\n      struct {\n        bool closed : 1;\n        bool hidden : 1;\n        bool _rfu : 1;\n        uint8_t turnoutType : 5;\n      };\n      uint8_t flags;\n    };\n    uint16_t id;\n  } _turnoutData;  // 3 bytes\n\n#ifndef DISABLE_EEPROM\n  // Address in eeprom of first byte of the _turnoutData struct (containing the closed flag).\n  // Set to zero if the object has not been saved in EEPROM, e.g. for newly created Turnouts, and \n  // for all LCN turnouts.\n  uint16_t _eepromAddress = 0;\n#endif\n\n  // Pointer to next turnout on linked list.\n  Turnout *_nextTurnout = 0;\n\n  /*\n   * Constructor\n   */\n  Turnout(uint16_t id, uint8_t turnoutType, bool closed) {\n    _turnoutData.id = id;\n    _turnoutData.turnoutType = turnoutType;\n    _turnoutData.closed = closed;\n    _turnoutData.hidden=false;\n    add(this);\n  }\n\n  /* \n   * Static data\n   */ \n\n  static Turnout *_firstTurnout;\n  static int _turnoutlistHash;\n\n  /* \n   * Virtual functions\n   */\n\n  virtual bool setClosedInternal(bool close) = 0;  // Mandatory in subclass\n  virtual void save() {}\n  \n  /*\n   * Static functions\n   */\n\n\n  static void add(Turnout *tt);\n  \npublic:\n  static Turnout *get(uint16_t id);\n  /* \n   * Static data\n   */\n  static int turnoutlistHash;\n  static const bool useClassicTurnoutCommands;\n  \n  /*\n   * Public base class functions\n   */\n  inline bool isClosed() { return _turnoutData.closed; };\n  inline bool isThrown() { return !_turnoutData.closed; }\n  inline bool isHidden() { return _turnoutData.hidden; }\n  inline void setHidden(bool h) { _turnoutData.hidden=h; }\n  inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; }\n  inline uint16_t getId() { return _turnoutData.id; }\n  inline Turnout *next() { return _nextTurnout; }\n  void printState(Print *stream);\n  /* \n   * Virtual functions\n   */\n  virtual void print(Print *stream) {\n    (void)stream;  // avoid compiler warnings.\n  }\n  virtual ~Turnout() {}   // Destructor\n\n  /*\n   * Public static functions\n   */\n  inline static bool exists(uint16_t id) { return get(id) != 0; }\n\n  static bool remove(uint16_t id);\n\n  static bool isClosed(uint16_t id);\n\n  inline static bool isThrown(uint16_t id) {\n    return !isClosed(id);\n  }\n\n  static bool setClosed(uint16_t id, bool closeFlag);\n\n  inline static bool setClosed(uint16_t id) {\n    return setClosed(id, true);\n  }\n\n  inline static bool setThrown(uint16_t id) {\n    return setClosed(id, false);\n  }\n\n  static bool setClosedStateOnly(uint16_t id, bool close);\n\n  inline static Turnout *first() { return _firstTurnout; }\n\n#ifndef DISABLE_EEPROM\n  // Load all turnout definitions.\n  static void load();\n  // Load one turnout definition\n  static Turnout *loadTurnout();\n  // Save all turnout definitions\n  static void store();\n#endif\n  static bool printAll(Print *stream) {\n    bool gotOne=false;\n    for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout)\n      if (!tt->isHidden()) {\n\tgotOne=true;\n\tStringFormatter::send(stream, F(\"<H %d %d>\\n\"),tt->getId(), tt->isThrown());\n      }\n    return gotOne;\n  }\n\n\n};\n\n\n/*************************************************************************************\n * ServoTurnout - Turnout controlled by servo device.\n * \n *************************************************************************************/\nclass ServoTurnout : public Turnout {\nprivate:\n  // ServoTurnoutData contains data specific to this subclass that is \n  // written to EEPROM when the turnout is saved.\n  struct ServoTurnoutData {\n    VPIN vpin;\n    uint16_t closedPosition : 12;\n    uint16_t thrownPosition : 12;\n    uint8_t profile;\n  } _servoTurnoutData; // 6 bytes\n\n  // Constructor\n  ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed);\n\npublic:\n  // Create function\n  static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed=true);\n\n  // Load a Servo turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  static Turnout *load(struct TurnoutData *turnoutData);\n  void print(Print *stream) override;\n\nprotected:\n  // ServoTurnout-specific code for throwing or closing a servo turnout.\n  bool setClosedInternal(bool close) override;\n  void save() override;\n\n};\n\n/*************************************************************************************\n * DCCTurnout - Turnout controlled by DCC Accessory Controller.\n * \n *************************************************************************************/\nclass DCCTurnout : public Turnout {\nprivate:\n  // DCCTurnoutData contains data specific to this subclass that is \n  // written to EEPROM when the turnout is saved.\n  struct DCCTurnoutData {\n    // DCC address (Address in bits 15-2, subaddress in bits 1-0)\n    struct {\n      uint16_t address : 14;\n      uint8_t subAddress : 2;\n    };\n  } _dccTurnoutData; // 2 bytes\n\n  // Constructor\n  DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd);\n\npublic:\n  // Create function\n  static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd);\n  // Load a VPIN turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  static Turnout *load(struct TurnoutData *turnoutData);\n  void print(Print *stream) override;\n\nprotected:\n  bool setClosedInternal(bool close) override;\n  void save() override;\n\n};\n\n\n/*************************************************************************************\n * VpinTurnout - Turnout controlled through a HAL vpin.\n * \n *************************************************************************************/\nclass VpinTurnout : public Turnout {\nprivate:\n  // VpinTurnoutData contains data specific to this subclass that is \n  // written to EEPROM when the turnout is saved.\n  struct VpinTurnoutData {\n    VPIN vpin;\n  } _vpinTurnoutData; // 2 bytes\n\n  // Constructor\n VpinTurnout(uint16_t id, VPIN vpin, bool closed);\n\npublic:\n  // Create function\n  static Turnout *create(uint16_t id, VPIN vpin, bool closed=true);\n\n  // Load a VPIN turnout definition from EEPROM.  The common Turnout data has already been read at this point.\n  static Turnout *load(struct TurnoutData *turnoutData);\n  void print(Print *stream) override;\n\nprotected:\n  bool setClosedInternal(bool close) override;\n  void save() override;\n\n};\n\n\n/*************************************************************************************\n * LCNTurnout - Turnout controlled by Loconet\n * \n *************************************************************************************/\nclass LCNTurnout : public Turnout {\nprivate:\n  // LCNTurnout has no specific data, and in any case is not written to EEPROM!\n  // struct LCNTurnoutData {\n  // } _lcnTurnoutData; // 0 bytes\n\n  // Constructor \n  LCNTurnout(uint16_t id, bool closed);\n\npublic:\n  // Create function\n  static Turnout *create(uint16_t id, bool closed=true);\n\n\n  bool setClosedInternal(bool close) override;\n\n  // LCN turnouts not saved to EEPROM.\n  //void save() override {  }\n  //static Turnout *load(struct TurnoutData *turnoutData) {\n\n  void print(Print *stream) override;\n\n};\n\n#endif\n"
  },
  {
    "path": "Turntables.cpp",
    "content": "/*\n *  © 2023 Peter Cole\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include \"defines.h\"\n#include <Arduino.h>\n#include \"Turntables.h\"\n#include \"StringFormatter.h\"\n#include \"CommandDistributor.h\"\n#include \"EXRAIL2.h\"\n#include \"DCC.h\"\n\n// No turntable support without HAL\n#ifndef IO_NO_HAL\n\n/*\n * Protected static data\n */\nTurntable *Turntable::_firstTurntable = 0;\n\n\n/*\n * Public static data\n */\nint Turntable::turntablelistHash = 0;\n\n\n/*\n * Protected static functions\n */\n// Add new turntable to end of list\n\nvoid Turntable::add(Turntable *tto) {\n  if (!_firstTurntable) {\n    _firstTurntable = tto;\n  } else {\n    Turntable *ptr = _firstTurntable;\n    for ( ; ptr->_nextTurntable!=0; ptr=ptr->_nextTurntable) {}\n    ptr->_nextTurntable = tto;\n  }\n  turntablelistHash++;\n}\n\n// Add a position\nvoid Turntable::addPosition(uint8_t idx, uint16_t value, uint16_t angle) {\n  _turntablePositions.insert(idx, value, angle);\n}\n\n// Get value for position\nuint16_t Turntable::getPositionValue(uint8_t position) {\n  TurntablePosition* currentPosition = _turntablePositions.getHead();\n  while (currentPosition) {\n    if (currentPosition->index == position) {\n      return currentPosition->data;\n    }\n    currentPosition = currentPosition->next;\n  }\n  return false;\n}\n\n// Get value for position\nuint16_t Turntable::getPositionAngle(uint8_t position) {\n  TurntablePosition* currentPosition = _turntablePositions.getHead();\n  while (currentPosition) {\n    if (currentPosition->index == position) {\n      return currentPosition->angle;\n    }\n    currentPosition = currentPosition->next;\n  }\n  return false;\n}\n\n// Get the count of positions associated with the turntable\nuint8_t Turntable::getPositionCount()  {\n  TurntablePosition* currentPosition = _turntablePositions.getHead();\n  uint8_t count = 0;\n  while (currentPosition) {\n    count++;\n    currentPosition = currentPosition->next;\n  }\n  return count;\n}\n\n/*\n * Public static functions\n */\n// Find turntable from list\nTurntable *Turntable::get(uint16_t id) {\n  for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable)\n    if (tto->_turntableData.id == id) return tto;\n  return NULL;\n}\n\n// Find turntable via Vpin\nTurntable *Turntable::getByVpin(VPIN vpin) {\n  for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable) {\n    if (tto->isEXTT()) {\n      EXTTTurntable *exttTto = static_cast<EXTTTurntable*>(tto);\n      if (exttTto->getVpin() == vpin) {\n        return tto;\n      }\n    }\n  }\n  return nullptr;\n}\n\n// Get the current position for turntable with the specified ID\nuint8_t Turntable::getPosition(uint16_t id) {\n  Turntable *tto = get(id);\n  if (!tto) return false;\n  return tto->getPosition();\n}\n\n// Got the moving state of the specified turntable\nbool Turntable::ttMoving(uint16_t id) {\n  Turntable *tto = get(id);\n  if (!tto) return false;\n  return tto->isMoving();\n}\n\n// Initiate a turntable move\nbool Turntable::setPosition(uint16_t id, uint8_t position, uint8_t activity) {\n#if defined(DIAG_IO)\n  DIAG(F(\"Rotate turntable %d to position %d, activity %d)\"), id, position, activity);\n#endif\n  Turntable *tto = Turntable::get(id);\n  if (!tto) return false;\n  if (tto->isMoving()) return false;\n  bool ok = tto->setPositionInternal(position, activity);\n\n  if (ok) {\n    // We only deal with broadcasts for DCC turntables here, EXTT in the device driver\n    if (!tto->isEXTT()) {\n      CommandDistributor::broadcastTurntable(id, position, false);\n    }\n    // Trigger EXRAIL rotateEvent for both types here if changed\n#if defined(EXRAIL_ACTIVE)\n    bool rotated = false;\n    if (position != tto->_previousPosition) rotated = true;\n    RMFT2::rotateEvent(id, rotated);\n#endif\n  }\n  return ok;\n}\n\n/*************************************************************************************\n * EXTTTurntable - EX-Turntable device.\n * \n *************************************************************************************/\n// Private constructor\nEXTTTurntable::EXTTTurntable(uint16_t id, VPIN vpin) :\n  Turntable(id, TURNTABLE_EXTT)\n{\n  _exttTurntableData.vpin = vpin;\n}\n\nusing DevState = IODevice::DeviceStateEnum;\n\n// Create function\n  Turntable *EXTTTurntable::create(uint16_t id, VPIN vpin) {\n#ifndef IO_NO_HAL\n    Turntable *tto = get(id);\n    if (tto) {\n      if (tto->isType(TURNTABLE_EXTT)) {\n        EXTTTurntable *extt = (EXTTTurntable *)tto;\n        extt->_exttTurntableData.vpin = vpin;\n        return tto;\n      }\n    }\n    if (!IODevice::exists(vpin)) return nullptr;\n    if (IODevice::getStatus(vpin) == DevState::DEVSTATE_FAILED) return nullptr;\n    if (Turntable::getByVpin(vpin)) return nullptr;\n    tto = (Turntable *)new EXTTTurntable(id, vpin);\n    DIAG(F(\"Turntable 0x%x size %d size %d\"), tto, sizeof(Turntable), sizeof(struct TurntableData));\n    return tto;\n#else\n  (void)id;\n  (void)vpin;\n  return NULL;\n#endif\n  }\n\n  void EXTTTurntable::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<i %d EXTURNTABLE %d>\\n\"), _turntableData.id, _exttTurntableData.vpin);\n  }\n\n  // EX-Turntable specific code for moving to the specified position\n  bool EXTTTurntable::setPositionInternal(uint8_t position, uint8_t activity) {\n#ifndef IO_NO_HAL\n    int16_t value;\n    if (position == 0) {\n      value = 0;  // Position 0 is just to send activities\n    } else {\n      if (activity > 1) return false; // If sending a position update, only phase changes valid (0|1)\n      value = getPositionValue(position); // Get position value from position list\n    }\n    if (position > 0 && !value) return false; // Return false if it's not a valid position\n    // Set position via device driver\n    _previousPosition = _turntableData.position;\n    _turntableData.position = position;\n    EXTurntable::writeAnalogue(_exttTurntableData.vpin, value, activity);\n#else\n    (void)position;\n#endif\n    return true;\n  }\n\n/*************************************************************************************\n * DCCTurntable - DCC Turntable device.\n * \n *************************************************************************************/\n// Private constructor\nDCCTurntable::DCCTurntable(uint16_t id) : Turntable(id, TURNTABLE_DCC) {}\n\n// Create function\n  Turntable *DCCTurntable::create(uint16_t id) {\n#ifndef IO_NO_HAL\n    Turntable *tto = get(id);\n    if (!tto) {\n      tto = (Turntable *)new DCCTurntable(id);\n      DIAG(F(\"Turntable 0x%x size %d size %d\"), tto, sizeof(Turntable), sizeof(struct TurntableData));\n    }\n    return tto;\n#else\n  (void)id;\n  return NULL;\n#endif\n  }\n\n  void DCCTurntable::print(Print *stream) {\n    StringFormatter::send(stream, F(\"<i %d DCCTURNTABLE>\\n\"), _turntableData.id);\n  }\n\n// EX-Turntable specific code for moving to the specified position\nbool DCCTurntable::setPositionInternal(uint8_t position, uint8_t activity) {\n  (void) activity;\n#ifndef IO_NO_HAL\n  int16_t value = getPositionValue(position);\n  if (position == 0 || !value) return false; // Return false if it's not a valid position\n  // Set position via device driver\n  int16_t addr=value>>3;\n  int16_t subaddr=(value>>1) & 0x03;\n  bool active=value & 0x01;\n  _previousPosition = _turntableData.position;\n  _turntableData.position = position;\n  DCC::setAccessory(addr, subaddr, active);\n#else\n  (void)position;\n#endif\n  return true;\n}\n\n#endif\n"
  },
  {
    "path": "Turntables.h",
    "content": "/*\n *  © 2023 Peter Cole\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef TURNTABLES_H\n#define TURNTABLES_H\n\n#include <Arduino.h>\n#include \"IODevice.h\"\n#include \"StringFormatter.h\"\n\n// No turntable support without HAL\n#ifndef IO_NO_HAL\n\n// Turntable type definitions\n// EXTT = EX-Turntable\n// DCC = DCC accessory turntables - to be added later\nenum {\n  TURNTABLE_EXTT = 0,\n  TURNTABLE_DCC = 1,\n};\n\n/*************************************************************************************\n * Turntable positions.\n * \n *************************************************************************************/\nstruct TurntablePosition {\n  uint8_t index;\n  uint16_t data;\n  uint16_t angle;\n  TurntablePosition* next;\n  \n  TurntablePosition(uint8_t idx, uint16_t value, uint16_t angle) : index(idx), data(value), angle(angle), next(nullptr) {}\n};\n\nclass TurntablePositionList {\npublic:\n  TurntablePositionList() : head(nullptr) {}\n\n  void insert(uint8_t idx, uint16_t value, uint16_t angle) {\n    TurntablePosition* newPosition = new TurntablePosition(idx, value, angle);\n    if(!head) {\n      head = newPosition;\n    } else {\n      newPosition->next = head;\n      head = newPosition;\n    }\n  }\n\n  TurntablePosition* getHead() {\n    return head;\n  }\n\nprivate:\n  TurntablePosition* head;\n\n};\n\n\n/*************************************************************************************\n * Turntable - Base class for turntables.\n * \n *************************************************************************************/\n\nclass Turntable {\nprotected:\n  /*\n   * Object data\n   */\n\n  //  Data common to all turntable types\n  struct TurntableData {\n    union {\n      struct {\n        bool hidden : 1;\n        bool turntableType : 1;\n        uint8_t position : 6;       // Allows up to 63 positions including 0/home\n      };\n      uint8_t flags;\n    };\n    uint16_t id;\n  } _turntableData;\n\n  // Pointer to next turntable object\n  Turntable *_nextTurntable = 0;\n\n  // Linked list for positions\n  TurntablePositionList _turntablePositions;\n  \n  // Store the previous position to allow checking for changes\n  uint8_t _previousPosition = 0;\n\n  // Store the current state of the turntable\n  bool _isMoving = false;\n\n  /*\n   * Constructor\n   */\n  Turntable(uint16_t id, uint8_t turntableType) {\n    _turntableData.id = id;\n    _turntableData.turntableType = turntableType;\n    _turntableData.hidden = false;\n    _turntableData.position = 0;\n    add(this);\n  }\n\n  /*\n   * Static data\n   */\n  static Turntable *_firstTurntable;\n  static int _turntablelistHash;\n\n  /*\n   * Virtual functions\n   */\n  virtual bool setPositionInternal(uint8_t position, uint8_t activity) = 0;\n\n  /*\n   * Static functions\n   */\n  static void add(Turntable *tto);\n\npublic:\n  static Turntable *get(uint16_t id);\n  static Turntable *getByVpin(VPIN vpin);\n\n  /*\n   * Static data\n   */\n  static int turntablelistHash;\n\n  /*\n   * Public base class functions\n   */\n  inline uint8_t getPosition() { return _turntableData.position; }\n  inline bool isHidden() { return _turntableData.hidden; }\n  inline void setHidden(bool h) {_turntableData.hidden=h; }\n  inline bool isType(uint8_t type) { return _turntableData.turntableType == type; }\n  inline bool isEXTT() const { return _turntableData.turntableType == TURNTABLE_EXTT; }\n  inline uint16_t getId() { return _turntableData.id; }\n  inline Turntable *next() { return _nextTurntable; }\n  void printState(Print *stream);\n  void addPosition(uint8_t idx, uint16_t value, uint16_t angle);\n  uint16_t getPositionValue(uint8_t position);\n  uint16_t getPositionAngle(uint8_t position);\n  uint8_t getPositionCount();\n  bool isMoving() { return _isMoving; }\n  void setMoving(bool moving) { _isMoving=moving; }\n\n  /*\n   * Virtual functions\n   */\n  virtual void print(Print *stream) {\n    (void)stream; // suppress compiler warnings\n  }\n  virtual ~Turntable() {} // Destructor\n  \n\n  /*\n   * Public static functions\n   */\n  inline static bool exists(uint16_t id) { return get(id) != 0; }\n  static bool setPosition(uint16_t id, uint8_t position, uint8_t activity=0);\n  static uint8_t getPosition(uint16_t id);\n  static bool ttMoving(uint16_t id);\n  inline static Turntable *first() { return _firstTurntable; }\n  static bool printAll(Print *stream) {\n    bool gotOne = false;\n    for (Turntable *tto = _firstTurntable; tto != 0; tto = tto->_nextTurntable)\n      if (!tto->isHidden()) {\n        gotOne = true;\n        StringFormatter::send(stream, F(\"<I %d %d>\\n\"), tto->getId(), tto->getPosition());\n      }\n    return gotOne;\n  }\n\n};\n\n/*************************************************************************************\n * EXTTTurntable - EX-Turntable device.\n * \n *************************************************************************************/\nclass EXTTTurntable : public Turntable {\nprivate:\n  // EXTTTurntableData contains device specific data\n  struct EXTTTurntableData {\n    VPIN vpin;\n  } _exttTurntableData;\n\n  // Constructor\n  EXTTTurntable(uint16_t id, VPIN vpin);\n\npublic:\n  // Create function\n  static Turntable *create(uint16_t id, VPIN vpin);\n  void print(Print *stream) override;\n  VPIN getVpin() const { return _exttTurntableData.vpin; }\n\nprotected:\n  // EX-Turntable specific code for setting position\n  bool setPositionInternal(uint8_t position, uint8_t activity) override;\n\n};\n\n/*************************************************************************************\n * DCCTurntable - DCC accessory Turntable device.\n * \n *************************************************************************************/\nclass DCCTurntable : public Turntable {\nprivate:\n  // Constructor\n  DCCTurntable(uint16_t id);\n\npublic:\n  // Create function\n  static Turntable *create(uint16_t id);\n  void print(Print *stream) override;\n\nprotected:\n  // DCC specific code for setting position\n  bool setPositionInternal(uint8_t position, uint8_t activity=0) override;\n\n};\n\n#endif\n\n#endif\n"
  },
  {
    "path": "Websockets.cpp",
    "content": "/*\n *  © 2023-2026 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n\n/**************************************************\n  HOW IT WORKS\n\n  1) Refer to https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers\n  \n  2) When a new client sends in a socket stream, the\n     CommandDistributor pass it to this code \n     checkConnectionString() to check for an HTTP\n     protocol GET requesting a change to websocket protocol.\n     [Note that the WifiInboundHandler has a shortcut to detecting this so that\n      it does not need to use up 500+ bytes of RAM just to get at the one parameter that\n      actually means something.]\n     If that is found, the relevant answer is generated and queued and\n     the CommandDistributor marks this client as a websocket client awaiting connection.\n     Once the outbound handshake has completed, the CommandDistributor promotes the client\n     from awaiting connection to connected websocket so that all\n     future traffic for this client is handled with websocket protocol.\n\n  3) When an input is received from a client marked as websocket,\n     CommandDistributor calls  unmask() to strip off the websocket header and\n     un-mask the input bytes. The command distributor will flag the\n     clientid in the ringstream so that anyone transmitting this\n     output will know to handle it differently.   \n\n   4) when the  Wifi/Ethernet handler needs to transmit the result from the\n      output ring, it recognises the websockets flag and adds the websocket \n      header to the output dynamically. \n\n *************************************************************/\n#include <Arduino.h>\n#include \"FSH.h\"\n#include \"RingStream.h\"\n#include \"libsha1.h\"\n#include \"Websockets.h\"\n#include \"DIAG.h\"\n#ifdef ARDUINO_ARCH_ESP32\n  // ESP32 runtime or definitions has strlcat_P missing \n  #define strlcat_P strlcat\n#endif  \nstatic const char b64_table[] = {\n  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',\n  'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',\n  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',\n  'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',\n  'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',\n  'o', 'p', 'q', 'r', 's', 't', 'u', 'v',\n  'w', 'x', 'y', 'z', '0', '1', '2', '3',\n  '4', '5', '6', '7', '8', '9', '+', '/'\n};\n\nbool Websockets::checkConnectionString(byte clientId,byte * cmd, RingStream * outbound ) {\n    // returns true if this input is a websocket connect    \n    if (Diag::WEBSOCKET) DIAG(F(\"Websock check connection\"));   \n    /* Heuristic suppose this is a websocket GET\n    typically looking like this:\n  \n    GET / HTTP/1.1\n    Host: 192.168.1.242:2560\n    Connection: Upgrade\n    Pragma: no-cache\n    Cache-Control: no-cache\n    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0\n    Upgrade: websocket\n    Origin: null\n    Sec-WebSocket-Version: 13\n    Accept-Encoding: gzip, deflate\n    Accept-Language: en-US,en;q=0.9\n    Sec-WebSocket-Key: SpRkQKPPNZcO62pYf1X6Yg==\n    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\n    */\n   \n   // check contents to find Sec-WebSocket-Key: and get key up to \\n \n   auto keyPos=strstr_P((char*)cmd,(char*)F(\"Sec-WebSocket-Key: \"));\n   if (!keyPos) return false;\n   keyPos+=19; // length of Sec-Websocket-Key: \n   auto endkeypos=strstr(keyPos,\"\\r\");\n   if (!endkeypos) return false; \n   *endkeypos=0;\n   \n   if (Diag::WEBSOCKET) DIAG(F(\"Websock key=\\\"%s\\\"\"),keyPos);\n// generate the reply key \n    uint8_t sha1HashBin[21] = { 0 }; // 21 to make it base64 div 3\n    char replyKey[100];\n    strlcpy(replyKey,keyPos, sizeof(replyKey));\n    strlcat_P(replyKey,(char*)F(\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"), sizeof(replyKey));\n     \n    if (Diag::WEBSOCKET) DIAG(F(\"Websock replykey=%s\"),replyKey);\n\n    SHA1_CTX ctx;\n    SHA1Init(&ctx);\n    SHA1Update(&ctx, (unsigned char *)replyKey, strlen(replyKey));\n    SHA1Final(sha1HashBin, &ctx);\n      \n    // generate the response and embed the base64 encode \n    // of the key\n    outbound->mark(clientId);\n    outbound->print(F(\"HTTP/1.1 101 Switching Protocols\\r\\n\"\n                    \"Server: DCCEX-WebSocketsServer\\r\\n\"\n                    \"Upgrade: websocket\\r\\n\"\n                    \"Connection: Upgrade\\r\\n\"\n                    \"Origin: null\\r\\n\"\n                    \"Sec-WebSocket-Version: 13\\r\\n\"\n                    \"Sec-WebSocket-Protocol: DCCEX\\r\\n\"\n                    \"Sec-WebSocket-Accept: \"));\n// encode and emit the reply key as base 64\n    auto * tmp=sha1HashBin;\n    for (int i=0;i<7;i++) { \n      outbound->print(b64_table[(tmp[0] & 0xfc) >> 2]);\n      outbound->print(b64_table[((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4)]);\n      outbound->print(b64_table[((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6)]);\n      if (i<6) outbound->print(b64_table[tmp[2] & 0x3f]);\n      tmp+=3;\n    }\n    outbound->print(F(\"=\\r\\n\\r\\n\"));  // because we have padded 1 byte\n    outbound->commit();\n    return true;          \n}\n\nbyte * Websockets::unmask(byte clientId,RingStream *ring, byte * buffer) {\n // buffer should have a websocket header\n //byte opcode=buffer[0] & 0x0f;\n if (Diag::WEBSOCKET) DIAG(F(\"Websock in: %x %x %x %x %x %x %x\"),\n      buffer[0],buffer[1],buffer[2],buffer[3],\n       buffer[4],buffer[5],buffer[6]);\n\n byte opcode=buffer[0];\n bool maskbit=buffer[1]&0x80;\n int16_t payloadLength=buffer[1]&0x7f;\n \n byte * mask;\n if (payloadLength<126) {\n     mask=buffer+2;\n     }\n else {\n     // bytes 2 and 3 are the big-endian payload length\n     payloadLength=(buffer[2]<<8)|(buffer[3]);\n     mask=buffer+4;\n }\n if (Diag::WEBSOCKET) DIAG(F(\"Websock op=%x mb=%b pl=%d m=%x %x %x %x\"), opcode, maskbit, payloadLength, \n      mask[0],mask[1],mask[2], mask[3]);\n\n if (opcode==0x89) { // ping\n     DIAG(F(\"Websock ping\"));\n     buffer[0]=0x8a;  // pong.. and send it back\n     ring->mark(clientId &0x7f); // dont readjust\n     ring->print((char *)buffer);\n     ring->commit();\n     return nullptr; \n     }\n  \n  if (opcode==0x88) { // close\n    DIAG(F(\"Websock close\"));\n    // Echo back an unmasked close frame (server-to-client frames are never masked)\n    buffer[0]=0x88;\n    buffer[1]=0x00;\n    ring->mark(clientId & 0x7f); // dont readjust\n    ring->write((uint8_t *)buffer, 2);\n    ring->commit();\n    return nullptr;  // no payload to process\n }\n\n if (opcode!=0x81) {\n  DIAG(F(\"Websock unknown opcode 0x%x\"),opcode);\n  return nullptr;\n }\n \n byte * payload=mask+4;\n for (int i=0;i<payloadLength;i++) {\n     payload[i]^=mask[i%4];\n }\n \n if (Diag::WEBSOCKET) DIAG(F(\"Websoc payload=%s\"),payload);\n   \n return payload; // payload will be parsed as normal\n     \n }\n\n int16_t Websockets::getOutboundHeaderSize(uint16_t dataLength) {\n   return (dataLength>=126)? 4:2;\n }\n\nint Websockets::fillOutboundHeader(uint16_t dataLength, byte * buffer) {\n    // text opcode, flag(126= use 2 length bytes, no mask bit) , length\n    buffer[0]=0x81;\n    if (dataLength<126) {\n         buffer[1]=(byte)dataLength;\n         return 2; \n    }\n    buffer[1]=126;\n    buffer[2]=(byte)(dataLength >> 8);\n    buffer[3]=(byte)(dataLength & 0xFF);\n    return 4;  \n}\n    \n void Websockets::writeOutboundHeader(Print * stream,uint16_t dataLength) {\n    byte prefix[4];\n    int headerlen=fillOutboundHeader(dataLength,prefix);\n    stream->write(prefix,headerlen);\n  }\n \n\n\n"
  },
  {
    "path": "Websockets.h",
    "content": "/*\n *  © 2023 Chris Harlow\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef Websockets_h\n#define Websockets_h\n#include <Arduino.h>\n#include \"RingStream.h\"\nclass Websockets {\n    public:\n    static bool checkConnectionString(byte clientId,byte * cmd, RingStream * outbound );\n    static byte * unmask(byte clientId,RingStream *ring, byte * buffer);\n    static int16_t getOutboundHeaderSize(uint16_t dataLength);\n    static int fillOutboundHeader(uint16_t dataLength, byte * buffer);\n    static void writeOutboundHeader(Print * stream,uint16_t dataLength);\n    static const byte WEBSOCK_CLIENT_MARKER=0x80;\n};\n\n#endif"
  },
  {
    "path": "WiThrottle.cpp",
    "content": "/*\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2020-2022 Harald Barth\n *  © 2020-2021 M Steve Todd\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n/*\n * Truncated JMRI WiThrottle server implementation for DCC-EX command station\n * Credit is due to  Valerie Valley RR https://sites.google.com/site/valerievalleyrr/\n *  for showing how it could be done, but this code is very different to the original\n *  implementation as it is designed to run on the Arduino and not the ESP and is\n *  also calling directly into the DCCEX Api rather than  simulating JMRI text commands.\n * Refer JMRI WiFi Throttle Communications Protocol https://www.jmri.org/help/en/package/jmri/jmrit/withrottle/Protocol.shtml\n * \n * \n * PROTOTYPE NOTES:\n *  There will be one WiThrottle instance created for each WiThrottle client detected by the WifiInterface.\n *  Some shortcuts have been taken and there are some things that are yet to be included:\n *  e.g. Full response to adding a loco.\n *  What to do about unknown turnouts.\n *  Broadcasting to other WiThrottles when things change.  \n *    -  Bear in mind that changes may have taken place due to  \n *      other WiThrottles, OR JMRI commands received OR TPL automation. \n *    - I suggest that at the end of parse(), then anything that has changed and is of interest could  \n *       be notified then.  (e.g loco speeds, directions or functions, turnout states.\n *       \n *  WiThrottle.h sets the max locos per client at 10, this is ok to increase but requires just an extra 3 bytes per loco per client.      \n*/\n#include <Arduino.h>\n#include \"defines.h\"\n#include \"WiThrottle.h\"\n#include \"DCC.h\"\n#include \"DCCWaveform.h\"\n#include \"StringFormatter.h\"\n#include \"Turnouts.h\"\n#include \"DIAG.h\"\n#include \"GITHUB_SHA.h\"\n#include \"version.h\"\n#include \"EXRAIL2.h\"\n#include \"CommandDistributor.h\"\n#include \"TrackManager.h\"\n#include \"DCCTimer.h\"\n\n#define LOOPLOCOS(THROTTLECHAR, CAB)  for (int loco=0;loco<MAX_MY_LOCO;loco++) \\\n      if ((myLocos[loco].throttle==THROTTLECHAR || '*'==THROTTLECHAR) && (CAB<0 || myLocos[loco].cab==CAB))\n\nWiThrottle * WiThrottle::firstThrottle=NULL;\n\nWiThrottle* WiThrottle::getThrottle( int wifiClient) {\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)  \n     if (wt->clientid==wifiClient) return wt; \n  return new WiThrottle( wifiClient);\n}\n\nvoid WiThrottle::forget( byte clientId) {\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)  \n     if (wt->clientid==clientId) {\n      DIAG(F(\"Withrottle client %d dropped\"),clientId);\n      delete wt;\n      break; \n     }\n}\n\nbool WiThrottle::isThrottleInUse(int cab) {\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)  \n     if (wt->areYouUsingThrottle(cab)) return true;\n  return false;\n}\n\nbool WiThrottle::areYouUsingThrottle(int cab) {\n  LOOPLOCOS('*', cab) { // see if I have this cab in use\n      return true;\n  }\n  return false;\n}\n // One instance of WiThrottle per connected client, so we know what the locos are \n \nWiThrottle::WiThrottle( int wificlientid) {\n   if (Diag::WITHROTTLE) DIAG(F(\"%l Creating new WiThrottle for client %d\"),millis(),wificlientid); \n   nextThrottle=firstThrottle;\n   firstThrottle= this;\n   clientid=wificlientid;\n   heartBeatEnable=false; // until client turns it on\n   mostRecentCab=0;                \n   for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\\0';\n}\n\nWiThrottle::~WiThrottle() {\n  if (Diag::WITHROTTLE) DIAG(F(\"Deleting WiThrottle client %d\"),this->clientid);\n  if (firstThrottle== this) {\n    firstThrottle=this->nextThrottle;\n    return;\n  }\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle) {\n    if (wt->nextThrottle==this) {\n      wt->nextThrottle=this->nextThrottle;\n      return;  \n     }\n  }\n}\n\nvoid WiThrottle::parse(RingStream * stream, byte * cmdx) {\n  \n  byte * cmd=cmdx;\n  \n  heartBeat=millis();\n  if (Diag::WITHROTTLE) DIAG(F(\"%l WiThrottle(%d)<-[%e]\"),millis(),clientid,cmd);\n  \n  // On first few commands, send turnout, roster and routes \n  if (introSent) {  \n    if (!turnoutsSent) sendTurnouts(stream);\n    else if(!rosterSent) sendRoster(stream);\n    else if (!routesSent) sendRoutes(stream);\n    else if (!heartrateSent) {\n         heartrateSent=true;\n        // allow heartbeat to slow down once all metadata sent     \n        StringFormatter::send(stream,F(\"*%d\\nHMConnected\\n\"),HEARTBEAT_SECONDS);\n\n    }\n  }\n  \n  while (cmd[0]) {\n    switch (cmd[0]) {\n    case '*':  // heartbeat control\n      if (cmd[1]=='+') heartBeatEnable=true;\n      else if (cmd[1]=='-') heartBeatEnable=false;\n      break;\n    case 'P':  \n      if (cmd[1]=='P' && cmd[2]=='A' )  {  //PPA power mode \n\tTrackManager::setMainPower(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);\n/* TODO \n\tif (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling\n\t  DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);\n*/\n\n      }\n#if defined(EXRAIL_ACTIVE)\n      else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate\n\t// exrail routes are RA2Rn , Animations are RA2An \n\tint route=getInt(cmd+5);\n\tuint16_t cab=cmd[4]=='A' ? mostRecentCab : 0; \n\tRMFT2::createNewTask(route, cab);\n      }\n#endif    \n      else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle \n\tint id=getInt(cmd+4); \n\tif (!Turnout::exists(id)) {\n\t  // If turnout does not exist, create it\n\t  int addr = ((id - 1) / 4) + 1;\n\t  int subaddr = (id - 1) % 4;\n\t  DCCTurnout::create(id,addr,subaddr);\n\t  StringFormatter::send(stream, F(\"HmTurnout %d created\\n\"),id);\n\t}\n\tswitch (cmd[3]) {\n\t  // T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging.\n\tcase 'T': \n\t  Turnout::setClosed(id,false);\n\t  break;\n\tcase 'C': \n\t  Turnout::setClosed(id,true);\n\t  break;\n\tcase '2': \n\t  Turnout::setClosed(id,!Turnout::isClosed(id));\n\t  break;\n\tdefault :\n\t  Turnout::setClosed(id,true);\n\t  break;\n\t}\n\tStringFormatter::send(stream, F(\"PTA%c%d\\n\"),Turnout::isClosed(id)?'2':'4',id );\n      }\n      break;\n    case 'N':  // Heartbeat (2), only send if connection completed by 'HU' message\n      sendIntro(stream);\n      StringFormatter::send(stream, F(\"*%d\\n\"), heartrateSent ? HEARTBEAT_SECONDS : HEARTBEAT_PRELOAD); // return timeout value\n      break;\n    case 'M': // multithrottle\n      multithrottle(stream, cmd); \n      break;\n    case 'H': // send initial connection info after receiving \"HU\" message\n      if (cmd[1] == 'U') {    \n\tsendIntro(stream);\n      }\n      break;           \n    case 'Q': // \n      LOOPLOCOS('*', -1) { // tell client to drop any locos still assigned to this WiThrottle\n\tif (myLocos[loco].throttle!='\\0') {\n\t  StringFormatter::send(stream, F(\"M%c-%c%d<;>\\n\"), myLocos[loco].throttle, LorS(myLocos[loco].cab), myLocos[loco].cab);\n\t}\n      }\n      if (Diag::WITHROTTLE) DIAG(F(\"WiThrottle(%d) Quit\"),clientid);\n      delete this; \n      break;           \n    }\n    // skip over cmd until 0 or past \\r or \\n\n    while(*cmd !='\\0' && *cmd != '\\r' && *cmd !='\\n') cmd++;\n    if (*cmd!='\\0') cmd++; // skip \\r or \\n  \n  }           \n}\n\nint WiThrottle::getInt(byte * cmd) {\n  int i=0;\n  bool negate=cmd[0]=='-';\n  if (negate) cmd++;\n  while (cmd[0]>='0' && cmd[0]<='9') {\n    i=i*10 + (cmd[0]-'0');\n    cmd++;\n  }\n  if (negate) i=0-i;\n  return i ;    \n}\n\nint WiThrottle::getLocoId(byte * cmd) {\n    if (cmd[0]=='*') return -1;  // match all locos \n    if (cmd[0]!='L' && cmd[0]!='S') return 0; // should not match any locos\n    return getInt(cmd+1); \n}\n\nvoid WiThrottle::multithrottle(RingStream * stream, byte * cmd){ \n  char throttleChar=cmd[1];\n  int locoid=getLocoId(cmd+3); // -1 for *\n  if (locoid > 10239 || locoid < -1) {\n    StringFormatter::send(stream, F(\"No valid DCC loco %d\\n\"), locoid);\n    return;\n  }\n  byte * aval=cmd;\n  while(*aval !=';' && *aval !='\\0') aval++;\n  if (*aval) aval+=2;  // skip ;>\n  \n  //       DIAG(F(\"Multithrottle aval=%c cab=%d\"), aval[0],locoid);    \n  switch(cmd[2]) {\n  case '+':  // add loco request\n    if (cmd[3]=='*') { \n      // M+* means get loco from prog track, then join tracks ready to drive away\n      // Stash the things the callback will need later\n      stashStream= stream;\n      stashClient=stream->peekTargetMark();\n      stashThrottleChar=throttleChar;\n      stashInstance=this;\n      // ask DCC to call us back when the loco id has been read\n      DCC::getDriveawayLocoId(getLocoCallback); // will remove any previous join                    \n      return; // return nothing in stream as response is sent later in the callback \n    }\n    //return error if address zero requested\n    if (locoid==0) { \n      StringFormatter::send(stream, F(\"HMAddress '0' not supported!\\n\"), cmd[3] ,locoid);                    \n      return;\n    }\n    //return error if L or S from request doesn't match DCC++ assumptions\n    if (cmd[3] != LorS(locoid)) { \n      StringFormatter::send(stream, F(\"HMLength '%c' not valid for %d!\\n\"), cmd[3] ,locoid);                    \n      return;\n    }\n    //return error if loco slots or ram full\n    if (!LocoSlot::getSlot(locoid,true)) { \n      StringFormatter::send(stream, F(\"HMUnable to create loco %d slot\\n\"), locoid);                    \n      return;\n    }\n    //use first empty \"slot\" on this client's list, will be added to DCC registration list\n    for (int loco=0;loco<MAX_MY_LOCO;loco++) {\n      if (myLocos[loco].throttle=='\\0') {\n\t      myLocos[loco].throttle=throttleChar;\n\t      myLocos[loco].cab=locoid; \n\t      myLocos[loco].functionMap=DCC::getFunctionMap(locoid); \n\t      myLocos[loco].broadcastPending=true; // means speed/dir will be sent later\n\t      mostRecentCab=locoid;\n\t      StringFormatter::send(stream, F(\"M%c+%c%d<;>\\n\"), throttleChar, cmd[3] ,locoid); //tell client to add loco\n\t      sendFunctions(stream,loco);\n\t      //speed and direction will be published at next broadcast cycle\n\t      StringFormatter::send(stream, F(\"M%cA%c%d<;>s1\\n\"), throttleChar, cmd[3], locoid); //default speed step 128\n\t      return;\n      }\n    }\n    StringFormatter::send(stream, F(\"HMMax locos (%d) exceeded, %d not added!\\n\"), MAX_MY_LOCO ,locoid);                    \n    break;\n  case '-': // remove loco(s) from this client (leave in DCC registration)\n    LOOPLOCOS(throttleChar, locoid) {\n      myLocos[loco].throttle='\\0';\n      StringFormatter::send(stream, F(\"M%c-%c%d<;>\\n\"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab);\n    }\n    \n    break;\n  case 'A':\n    locoAction(stream,aval, throttleChar, locoid);\n  }\n}\n\nvoid WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar, int cab){\n  // Note cab=-1 for all cabs in the consist called throttleChar.  \n  //    DIAG(F(\"Loco Action aval=%c%c throttleChar=%c, cab=%d\"), aval[0],aval[1],throttleChar, cab);\n  (void) stream;\n  switch (aval[0]) {\n  case 'V':  // Vspeed\n    { \n      int witSpeed=getInt(aval+1);\n      LOOPLOCOS(throttleChar, cab) {\n\tmostRecentCab=myLocos[loco].cab;\n\tDCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));\n\t// SetThrottle will cause speed change broadcast\n      }\n    } \n    break;\n  case 'F': // Function key pressed/released\n    {  \n      bool pressed=aval[1]=='1';\n      int fKey = getInt(aval+2);\n      LOOPLOCOS(throttleChar, cab) {\n\tbool unsetOnRelease = myLocos[loco].functionToggles & (1L<<fKey);\n\tif (unsetOnRelease) DCC::setFn(myLocos[loco].cab,fKey, pressed);\n\telse if (pressed)  DCC::changeFn(myLocos[loco].cab, fKey);\n      }\n      break;  \n    }\n  case 'f': // Function key set, force function variant\n    {\n      bool pressed=aval[1]=='1';\n      int fKey = getInt(aval+2);\n      LOOPLOCOS(throttleChar, cab) {\n\tDCC::setFn(myLocos[loco].cab,fKey, pressed);\n      }\n      break;\n    }\n  case 'q':\n    if (aval[1]=='V' || aval[1]=='R' ) {   //qV or qR\n      // just flag the loco for broadcast and it will happen.\n      bool foundone = false;\n      LOOPLOCOS(throttleChar, cab) {\n\tfoundone = true;\n\tmyLocos[loco].broadcastPending=true;\n      }\n      if (!foundone)\n\tStringFormatter::send(stream,F(\"HMCS loco list empty\\n\"));\n    }     \n    break;    \n  case 'R':\n    { \n      bool forward=aval[1]!='0';\n      LOOPLOCOS(throttleChar, cab) {\n\tmostRecentCab=myLocos[loco].cab;\n\tint8_t speed = DCC::getThrottleSpeed(myLocos[loco].cab);\n\tif (speed < 0) //can not find any speed for this cab\n\t  speed = 0;\n\tDCC::setThrottle(myLocos[loco].cab, speed, forward);\n\t// setThrottle will cause a broadcast so notification will be sent\n      }\n    }        \n    break;      \n  case 'X':\n    //Emergency Stop  (speed code 1)\n    LOOPLOCOS(throttleChar, cab) {\n      DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));\n      // setThrottle will cause a broadcast so notification will be sent\n    }\n    break;\n  case 'I': // Idle, set speed to 0\n  case 'Q': // Quit, set speed to 0\n    LOOPLOCOS(throttleChar, cab) {\n      mostRecentCab=myLocos[loco].cab;\n      DCC::setThrottle(myLocos[loco].cab, 0, DCC::getThrottleDirection(myLocos[loco].cab));\n      // setThrottle will cause a broadcast so notification will be sent\n    }\n    break;\n  }               \n}\n\n// convert between DCC++ speed values and WiThrottle speed values\nint WiThrottle::DCCToWiTSpeed(int DCCSpeed) {\n  if (DCCSpeed == 0) return 0; //stop is stop\n  if (DCCSpeed == 1) return -1; //eStop value\n  return DCCSpeed - 1; //offset others by 1\n}\n\n// convert between WiThrottle speed values and DCC++ speed values\nint WiThrottle::WiTToDCCSpeed(int WiTSpeed) {\n  if (WiTSpeed == 0) return 0;  //stop is stop\n  if (WiTSpeed == -1) return 1; //eStop value\n  return WiTSpeed + 1; //offset others by 1\n}\n\nvoid WiThrottle::loop(RingStream * stream) {\n  // for each WiThrottle, check the heartbeat and broadcast needed\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle) \n    wt->checkHeartbeat(stream);\n}\n\nvoid WiThrottle::checkHeartbeat(RingStream * stream) {\n  // if eStop time passed... eStop any locos still assigned to this client and then drop the connection\n  if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {\n    if (Diag::WITHROTTLE)  DIAG(F(\"%l WiThrottle(%d) eStop(%ds) timeout, drop connection\"), millis(), clientid, ESTOP_SECONDS);\n    LOOPLOCOS('*', -1) { \n      if (myLocos[loco].throttle!='\\0') {\n        if (Diag::WITHROTTLE) DIAG(F(\"%l  eStopping cab %d\"),millis(),myLocos[loco].cab);\n        DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab)); // speed 1 is eStop\n\theartBeat=millis(); // We have just stopped everyting, we don't need to do that again at next loop.\n      }\n    }\n    // if it does come back, the throttle should re-acquire \n    delete this;\n    return;\n  }\n   \n   // send any outstanding speed/direction/function changes for this clients locos\n   // Changes may have been caused by this client, or another non-Withrottle or Exrail\n  bool streamHasBeenMarked=false; \n  LOOPLOCOS('*', -1) { \n    if (myLocos[loco].throttle!='\\0' && myLocos[loco].broadcastPending) {\n      if (!streamHasBeenMarked) {\n\tstream->mark(clientid);\n\tstreamHasBeenMarked=true;\n      }\n      myLocos[loco].broadcastPending=false;\n      int cab=myLocos[loco].cab;\n      char lors=LorS(cab);\n      char throttle=myLocos[loco].throttle;\n      StringFormatter::send(stream,F(\"M%cA%c%d<;>V%d\\n\"),\n\t\t\t    throttle, lors , cab, DCCToWiTSpeed(DCC::getThrottleSpeed(cab)));\n      StringFormatter::send(stream,F(\"M%cA%c%d<;>R%d\\n\"), \n\t\t\t    throttle, lors , cab, DCC::getThrottleDirection(cab));\n      \n      // compare the DCC functionmap with the local copy and send changes  \n      uint32_t dccFunctionMap=DCC::getFunctionMap(cab);\n      uint32_t myFunctionMap=myLocos[loco].functionMap;\n      myLocos[loco].functionMap=dccFunctionMap;\n      \n      // loop the maps sending any bit changed\n      // Loop is terminated as soon as no changes are left\n      for (byte fn=0;dccFunctionMap!=myFunctionMap;fn++) {\n\tif ((dccFunctionMap&1) != (myFunctionMap&1)) {\n\t  StringFormatter::send(stream,F(\"M%cA%c%d<;>F%c%d\\n\"),\n\t\t\t\tthrottle, lors , cab, (dccFunctionMap&1)?'1':'0',fn);\n\t} \n\t// shift just checked bit off end of both maps\n\tdccFunctionMap>>=1;\n\tmyFunctionMap>>=1;\n      } \n    }\n    }\n  if (streamHasBeenMarked)   stream->commit();     \n}\n\nvoid WiThrottle::markForBroadcast(int cab) {\n  for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle) \n      wt->markForBroadcast2(cab);\n}\nvoid WiThrottle::markForBroadcast2(int cab) {\n  LOOPLOCOS('*', cab) { \n    myLocos[loco].broadcastPending=true;\n  }\n}\n\n\nchar WiThrottle::LorS(int cab) {\n  return (cab<=HIGHEST_SHORT_ADDR)?'S':'L';\n}\n\n// Drive Away feature. Callback handling\n\nRingStream * WiThrottle::stashStream;\nWiThrottle * WiThrottle::stashInstance;\nbyte         WiThrottle::stashClient;\nchar         WiThrottle::stashThrottleChar;\n\nvoid WiThrottle::getLocoCallback(int16_t locoid) {\n  //DIAG(F(\"LocoCallback mark client %d\"), stashClient);\n  stashStream->mark(stashClient);\n  \n  if (locoid<=0) {\n    StringFormatter::send(stashStream,F(\"HMNo loco found on prog track\\n\"));\n    //DIAG(F(\"LocoCallback commit (noloco)\"));\n    stashStream->commit();                  // done here, commit and return\n    return;\n  }\n\n  // short or long\n  char addrchar;\n  if (locoid & LONG_ADDR_MARKER) {          // maker bit indicates long addr\n    locoid = locoid ^ LONG_ADDR_MARKER;     // remove marker bit to get real long addr\n    if (locoid <= HIGHEST_SHORT_ADDR ) {    // out of range for long addr\n      StringFormatter::send(stashStream,F(\"HMLong addr %d <= %d unsupported\\n\"), locoid, HIGHEST_SHORT_ADDR);\n      //DIAG(F(\"LocoCallback commit (error)\"));\n      stashStream->commit();                // done here, commit and return\n      return;\n    }\n    addrchar = 'L';\n  } else {\n    addrchar = 'S';\n  }\n  \n  char addcmd[20]={'M',stashThrottleChar,'+', addrchar};\n  itoa(locoid,addcmd+4,10);\n  stashInstance->multithrottle(stashStream, (byte *)addcmd);\n  TrackManager::setJoin(true);          // <1 JOIN> so we can drive loco away\n  TrackManager::setMainPower(POWERMODE::ON);\n  TrackManager::setProgPower(POWERMODE::ON);\n  DIAG(F(\"LocoCallback commit success\"));\n  stashStream->commit();\n}\n\nvoid WiThrottle::sendIntro(Print* stream) {\n  if (introSent) // sendIntro only once\n    return;\n  introSent=true; \n  StringFormatter::send(stream,F(\"VN2.0\\nHTDCC-EX\\nRL0\\n\"));\n  StringFormatter::send(stream,F(\"HtDCC-EX v%S, %S, %S, %S\\n\"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));\n  StringFormatter::send(stream,F(\"PTT]\\\\[Turnouts}|{Turnout]\\\\[THROW}|{2]\\\\[CLOSE}|{4\\n\"));\n  StringFormatter::send(stream,F(\"PPA%x\\n\"),TrackManager::getMainPower()==POWERMODE::ON);\n  // set heartbeat to 2 seconds because we need to sync the metadata (1 second is too short!)\n  StringFormatter::send(stream,F(\"*%d\\nHMConnecting..\\n\"), HEARTBEAT_PRELOAD);\n}\n\nvoid WiThrottle::sendTurnouts(Print* stream) {\n     turnoutsSent=true;\n      StringFormatter::send(stream,F(\"PTL\"));\n      for(Turnout *tt=Turnout::first();tt!=NULL;tt=tt->next()){\n          if (tt->isHidden()) continue;\n          int id=tt->getId();\n          const FSH * tdesc=NULL;\n          #ifdef EXRAIL_ACTIVE\n          tdesc=RMFT2::getTurnoutDescription(id);\n          #endif\n          char tchar=Turnout::isClosed(id)?'2':'4';\n          if (tdesc==NULL) // turnout with no description\n              StringFormatter::send(stream,F(\"]\\\\[%d}|{T%d}|{T%c\"), id,id,tchar);\n\t        else \n              StringFormatter::send(stream,F(\"]\\\\[%d}|{%S}|{%c\"), id,tdesc,tchar);\n      }\n      StringFormatter::send(stream,F(\"\\n\"));\n}\nvoid WiThrottle::sendRoster(Print* stream) {\n  rosterSent=true;\n#ifdef EXRAIL_ACTIVE\n  StringFormatter::send(stream,F(\"RL%d\"), RMFT2::rosterNameCount);\n  for (int16_t r=0;;r++) {\n      int16_t cabid=GETHIGHFLASHW(RMFT2::rosterIdList,r*2);\n      if (cabid == INT16_MAX)\n\tbreak;\n      if (cabid > 0)\n\tStringFormatter::send(stream,F(\"]\\\\[%S}|{%d}|{%c\"),\n\t\t\t      RMFT2::getRosterName(cabid),cabid,cabid<128?'S':'L');\n  }\n  StringFormatter::send(stream,F(\"\\n\"));       \n#else\n   (void)stream; // remove warning\n#endif\n}\nvoid WiThrottle::sendRoutes(Print* stream) {\n  routesSent=true; \n#ifdef EXRAIL_ACTIVE\n   StringFormatter::send(stream,F(\"PRT]\\\\[Routes}|{Route]\\\\[Set}|{2]\\\\[Handoff}|{4\\nPRL\"));\n    // first pass automations\n    for (int ix=0;;ix+=2) {\n        int16_t id =GETHIGHFLASHW(RMFT2::automationIdList,ix);\n        if (id==INT16_MAX) break;\n        const FSH * desc=RMFT2::getRouteDescription(id);\n        StringFormatter::send(stream,F(\"]\\\\[A%d}|{%S}|{4\"),id,desc);\n    }\n    // second pass routes.\n    for (int ix=0;;ix+=2) {\n        int16_t id=GETHIGHFLASHW(RMFT2::routeIdList,ix);\n        if (id==INT16_MAX) break;\n        const FSH * desc=RMFT2::getRouteDescription(id);\n        StringFormatter::send(stream,F(\"]\\\\[R%d}|{%S}|{2\"),id,desc);\n    }\n   StringFormatter::send(stream,F(\"\\n\"));\n#else\n   (void)stream; // remove warning\n#endif\n}\n\nvoid WiThrottle::sendFunctions(Print* stream, byte loco) {\n  int16_t locoid=myLocos[loco].cab;\n  int fkeys=32; // upper limit (send functions 0 to 31)\n\tmyLocos[loco].functionToggles=1<<2; // F2 (HORN)  is a non-toggle\n        \n#ifdef EXRAIL_ACTIVE\n\tconst FSH * functionNames= RMFT2::getRosterFunctions(locoid);\n\tif (functionNames == NULL) {\n\t  // no roster entry for locoid, try to find default entry\n\t  functionNames= RMFT2::getRosterFunctions(0);\n\t}\n\tif (functionNames == NULL) {\n\t  // no default roster entry either, use non-exrail presets as above \n\t}\n\telse if (GETFLASH(functionNames)=='\\0') {\n\t  // \"\" = Roster but no functions given\n\t  fkeys=0;\n\t}  \n\telse {\n\t  // we have function names... \n\t  // scan names list emitting names, counting functions and \n\t  // flagging non-toggling things like horn.\n\t  myLocos[loco].functionToggles =0;\n\t  StringFormatter::send(stream, F(\"M%cL%c%d<;>]\\\\[\"), myLocos[loco].throttle,LorS(locoid),locoid);   \n\t  fkeys=0;\n\t  bool firstchar=true;\n\t  for (int fx=0;;fx++) {\n\t    char c=GETFLASH((char *)functionNames+fx);\n\t    if (c=='\\0') {\n\t      fkeys++;\n\t      break;\n\t    }\n\t    if (c=='/') {\n\t      fkeys++;\n\t      StringFormatter::send(stream,F(\"]\\\\[\"));\n\t      firstchar=true;\n\t    }\n\t    else if (firstchar && c=='*') {\n\t      myLocos[loco].functionToggles |= 1UL<<fkeys;\n\t      firstchar=false;\n\t    } \n\t    else {\n\t      firstchar=false;\n\t      stream->write(c);\n\t    }\n\t  }\n\t  StringFormatter::send(stream,F(\"\\n\"));\n\t}\n        \n#endif\n\t\n\tfor(int fKey=0; fKey<fkeys; fKey++) { \n      int8_t fstate=DCC::getFn(locoid,fKey);\n      if (fstate>=0) StringFormatter::send(stream,F(\"M%cA%c%d<;>F%d%d\\n\"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey);                     \n\t}\n}\n"
  },
  {
    "path": "WiThrottle.h",
    "content": "/*\n *  © 2021 Mike S\n *  © 2020-2021 Chris Harlow\n *  All rights reserved.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef WiThrottle_h\n#define WiThrottle_h\n\n#include \"RingStream.h\"\n\nstruct MYLOCO {\n    char throttle; //indicates which throttle letter on client, often '0','1' or '2'\n    int cab; //address of this loco\n    bool broadcastPending;\n    uint32_t functionMap;\n    uint32_t functionToggles;\n};\n\nclass WiThrottle {\n  public:  \n    static void loop(RingStream * stream);\n    void parse(RingStream * stream, byte * cmd);\n    static WiThrottle* getThrottle( int wifiClient); \n    static void markForBroadcast(int cab);\n    static void forget(byte clientId);\n    static void findUniqThrottle(int id, char *u);\n\n  private: \n    WiThrottle( int wifiClientId);\n    ~WiThrottle();\n   \n      static const int MAX_MY_LOCO=10;      // maximum number of locos assigned to a single client\n      static const int HEARTBEAT_SECONDS=10; // heartbeat at 10 secs to provide messaging transport\n      static const int HEARTBEAT_PRELOAD=2; // request fast callback when connecting multiple messages\n      static const int ESTOP_SECONDS=20;     // eStop if no incoming messages for more than 8secs\n      static WiThrottle* firstThrottle;\n      static int getInt(byte * cmd);\n      static int getLocoId(byte * cmd);\n      static char LorS(int cab); \n      static bool isThrottleInUse(int cab);\n      static void setSendTurnoutList();\n      bool areYouUsingThrottle(int cab);\n      WiThrottle* nextThrottle;\n      int clientid;\n      char uniq[17] = \"\";\n       \n      MYLOCO myLocos[MAX_MY_LOCO];   \n      bool heartBeatEnable;\n      unsigned long heartBeat;\n      bool introSent=false; \n      bool turnoutsSent=false; \n      bool rosterSent=false; \n      bool routesSent=false; \n      bool heartrateSent=false;\n      uint16_t mostRecentCab;\n      bool lastPowerState;  // last power state sent to this client\n\n      int DCCToWiTSpeed(int DCCSpeed);\n      int WiTToDCCSpeed(int WiTSpeed);\n      void multithrottle(RingStream * stream, byte * cmd);\n      void locoAction(RingStream * stream, byte* aval, char throttleChar, int cab);\n      void accessory(RingStream *, byte* cmd);\n      void checkHeartbeat(RingStream * stream); \n      void markForBroadcast2(int cab);\n      void sendIntro(Print * stream);\n      void sendTurnouts(Print * stream);\n      void sendRoster(Print * stream);\n      void sendRoutes(Print * stream);\n      void sendFunctions(Print* stream, byte loco);\n       // callback stuff to support prog track acquire\n       static RingStream * stashStream;\n       static WiThrottle * stashInstance;\n       static byte         stashClient;\n       static char         stashThrottleChar;\n       static void         getLocoCallback(int16_t locoid);\n\n};\n#endif\n"
  },
  {
    "path": "WifiESP32.cpp",
    "content": "/*\n    © 2023 Paul M. Antoine\n    © 2021 Harald Barth\n    © 2023 Nathan Kellenicki\n    © 2025 Chris Harlow\n    \n\n    This file is part of CommandStation-EX\n\n    This is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    It is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n#if defined(ARDUINO_ARCH_ESP32)\n#include <vector>\n#include \"defines.h\"\n#include \"ESPmDNS.h\"\n#include \"esp_wifi.h\"\n#include \"WifiESP32.h\"\n#include \"DIAG.h\"\n#include \"RingStream.h\"\n#include \"CommandDistributor.h\"\n#include \"WiThrottle.h\"\n#include \"DCC.h\"\n#include \"Websockets.h\"\n/*\n#include \"soc/rtc_wdt.h\"\n#include \"esp_task_wdt.h\"\n*/\n\n#include \"soc/timer_group_struct.h\"\n#include \"soc/timer_group_reg.h\"\nvoid feedTheDog0(){\n  // feed dog 0\n  TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable\n  TIMERG0.wdt_feed=1;                       // feed dog\n  TIMERG0.wdt_wprotect=0;                   // write protect\n  // feed dog 1\n  //TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable\n  //TIMERG1.wdt_feed=1;                       // feed dog\n  //TIMERG1.wdt_wprotect=0;                   // write protect\n}\n\n/*\nvoid enableCoreWDT(byte core){\n  TaskHandle_t idle = xTaskGetIdleTaskHandleForCPU(core);\n  if(idle == NULL){\n    DIAG(F(\"Get idle rask on core %d failed\"),core);\n  } else {\n    if(esp_task_wdt_add(idle) != ESP_OK){\n      DIAG(F(\"Failed to add Core %d IDLE task to WDT\"),core);\n    } else {\n      DIAG(F(\"Added Core %d IDLE task to WDT\"),core);\n    }\n  }\n}\n\nvoid disableCoreWDT(byte core){\n    TaskHandle_t idle = xTaskGetIdleTaskHandleForCPU(core);\n    if(idle == NULL || esp_task_wdt_delete(idle) != ESP_OK){\n      DIAG(F(\"Failed to remove Core %d IDLE task from WDT\"),core);\n    }\n}\n*/\n\nclass NetworkClient {\npublic:\n  NetworkClient(WiFiClient c) {\n    wifi = c;\n    inUse = true;\n  };\n  bool active(byte clientId) {\n    if (!inUse)\n      return false;\n    if(!wifi.connected()) {\n      DIAG(F(\"Remove client %d\"), clientId);\n      CommandDistributor::forget(clientId);\n      wifi.stop();\n      inUse = false;\n      return false;\n    }\n    return true;\n  }\n  bool recycle(WiFiClient c) {\n    if (wifi == c) {\n      if (inUse == true)\n\tDIAG(F(\"WARNING: Duplicate\"));\n      else\n\tDIAG(F(\"Returning\"));\n      inUse = true;\n      return true;\n    }\n    if (inUse == false) {\n      wifi = c;\n      inUse = true;\n      return true;\n    }\n    return false;\n  };\n  WiFiClient wifi;\nprivate:\n  bool inUse;\n};\n\n// file scope variables\nstatic std::vector<NetworkClient> clients; // a list to hold all clients\nstatic RingStream *outboundRing = new RingStream(10240);\nstatic bool APmode = false;\n// init of static class scope variables\nbool WifiESP::wifiUp = false;\nWiFiServer *WifiESP::server = NULL;\n\n#ifdef WIFI_TASK_ON_CORE0\nvoid wifiLoop(void *){\n  for(;;){\n    WifiESP::loop();\n  }\n}\n#endif\n\nchar asciitolower(char in) {\n  if (in <= 'Z' && in >= 'A')\n    return in - ('Z' - 'z');\n  return in;\n}\n\nvoid WifiESP::teardown() {\n  // stop all locos\n  DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.\n  // terminate all clients connections\n  while (!clients.empty()) {\n    // pop_back() should invoke destructor which does stop()\n    // on the underlying TCP connction\n    clients.pop_back();\n  }\n  // stop server\n  if (server != NULL) {\n    server->stop();\n    server->close();\n    server->end();\n    DIAG(F(\"server stop, close, end\"));\n  }\n  // terminate MDNS anouncement\n  mdns_service_remove_all();\n  mdns_free();\n  // stop WiFi\n  WiFi.disconnect(true);\n  wifiUp = false;\n}\n\nbool WifiESP::setup(const char *SSid,\n                    const char *password,\n                    const char *hostname,\n                    int port,\n                    const byte channel,\n                    const bool forceAP) {\n  bool havePassword = true;\n  bool haveSSID = true;\n//  bool wifiUp = false;\n  uint8_t tries = 40;\n  if (wifiUp)\n    teardown();\n  if (strcmp(\"OFF\", SSid) == 0) {\n    WiFi.disconnect(true);\n    WiFi.mode(WIFI_OFF);\n    return false; // debatable if that is true (success) or false (no network)\n  }\n  //#ifdef SERIAL_BT_COMMANDS\n  //return false;\n  //#endif\n\n  // tests\n  //  enableCoreWDT(1);\n  //  disableCoreWDT(0);\n\n#ifdef WIFI_LED\n  // Turn off Wifi LED\n  pinMode(WIFI_LED, OUTPUT);\n  digitalWrite(WIFI_LED, 0);\n#endif\n\n  // clean start\n  WiFi.mode(WIFI_STA);\n  WiFi.disconnect(true);\n  // differnet settings that did not improve for haba\n  // WiFi.useStaticBuffers(true);\n  // WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);\n  // WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SECURITY);\n\n  const char *yourNetwork = \"Your network \";\n  if (strncmp(yourNetwork, SSid, 13) == 0 || strncmp(\"\", SSid, 13) == 0)\n    haveSSID = false;\n  if (strncmp(yourNetwork, password, 13) == 0 || strncmp(\"\", password, 13) == 0)\n    havePassword = false;\n\n  if (haveSSID && havePassword && !forceAP) {\n    WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE!\n    WiFi.mode(WIFI_STA);\n    WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // Scan all channels so we find strongest\n                                               // (default in Wifi library is first match)\n#ifdef SERIAL_BT_COMMANDS\n    WiFi.setSleep(true);\n#else\n    WiFi.setSleep(false);\n#endif\n    WiFi.setAutoReconnect(true);\n    WiFi.begin(SSid, password);\n    while (WiFi.status() != WL_CONNECTED && tries) {\n      Serial.print('.');\n      tries--;\n      delay(500);\n    }\n    if (WiFi.status() == WL_CONNECTED) {\n      // DIAG(F(\"Wifi STA IP %s\"),WiFi.localIP().toString().c_str());\n      DIAG(F(\"Wifi in STA mode\"));\n      LCD(7, F(\"IP: %s\"), WiFi.localIP().toString().c_str());\n      wifiUp = true;\n    } else {\n      DIAG(F(\"Could not connect to Wifi SSID %s\"),SSid);\n      DIAG(F(\"Forcing one more Wifi restart\"));\n      esp_wifi_start();\n      esp_wifi_connect();\n      tries=40;\n      while (WiFi.status() != WL_CONNECTED && tries) {\n\tSerial.print('.');\n\ttries--;\n\tdelay(500);\n      }\n      if (WiFi.status() == WL_CONNECTED) {\n\tDIAG(F(\"Wifi STA IP 2nd try %s\"),WiFi.localIP().toString().c_str());\n\twifiUp = true;\n      } else {\n\tDIAG(F(\"Wifi STA mode FAIL. Will revert to AP mode\"));\n\twifiUp=false;\n      }\n    }\n  }\n  if (!wifiUp || forceAP) {\n    // prepare all strings\n    String strMac;\n    if (!haveSSID || !havePassword) {\n      strMac = WiFi.macAddress();\n      strMac.remove(0,9);\n      strMac.replace(\":\",\"\");\n      strMac.replace(\":\",\"\");\n      // convert mac addr hex chars to lower case to be compatible with AT software\n      std::transform(strMac.begin(), strMac.end(), strMac.begin(), asciitolower);\n    }\n    String strSSID;\n    if (!haveSSID) {\n      strSSID.concat(\"DCCEX_\");\n      strSSID.concat(strMac);\n    } else {\n      strSSID.concat(SSid);\n    }\n    String strPass;\n    if (!havePassword) {\n      strPass.concat(\"PASS_\");\n      strPass.concat(strMac);\n    } else {\n      strPass.concat(password);\n    }\n\n    WiFi.mode(WIFI_AP);\n#ifdef SERIAL_BT_COMMANDS\n    WiFi.setSleep(true);\n#else\n    WiFi.setSleep(false);\n#endif\n\n#ifdef WIFI_HIDE_SSID\n const bool hiddenAP = true;\n#else\n const bool hiddenAP = false;\n#endif\n\n    if (WiFi.softAP(strSSID.c_str(),\n\t\t    havePassword ? password : strPass.c_str(),\n\t\t    channel, hiddenAP, 8)) {\n      // DIAG(F(\"Wifi AP SSID %s PASS %s\"),strSSID.c_str(),havePassword ? password : strPass.c_str());\n      DIAG(F(\"Wifi in AP mode\"));\n      LCD(5, F(\"Wifi: %s\"), strSSID.c_str());\n      if (!havePassword)\n\tLCD(6, F(\"PASS: %s\"),strPass.c_str());\n      // DIAG(F(\"Wifi AP IP %s\"),WiFi.softAPIP().toString().c_str());\n      LCD(7, F(\"IP: %s\"),WiFi.softAPIP().toString().c_str());\n      wifiUp = true;\n      APmode = true;\n    } else {\n      DIAG(F(\"Could not set up AP with Wifi SSID %s\"),strSSID.c_str());\n    }\n  }\n\n\n  if (!wifiUp) {\n    DIAG(F(\"Wifi setup all fail (STA and AP mode)\"));\n    // no idea to go on\n    return false;\n  }\n#ifdef WIFI_LED\n  else{\n    // Turn on Wifi connected LED\n    digitalWrite(WIFI_LED, 1);\n  }\n#endif\n\n\n  // Now Wifi is up, register the mDNS service\n  if(!MDNS.begin(hostname)) {\n    DIAG(F(\"Wifi setup failed to start mDNS\"));\n  }\n  if(!MDNS.addService(\"withrottle\", \"tcp\", port)) {\n    DIAG(F(\"Wifi setup failed to add withrottle service to mDNS\"));\n  }\n\n  server = new WiFiServer(port); // start listening on tcp port\n  server->begin();\n  // server started here\n\n#ifdef WIFI_TASK_ON_CORE0\n  //start loop task\n  if (pdPASS != xTaskCreatePinnedToCore(\n\twifiLoop, /* Task function. */\n\t\"wifiLoop\",/* name of task.  */\n\t10000,     /* Stack size of task */\n\tNULL,      /* parameter of the task */\n\t1,         /* priority of the task */\n\tNULL,      /* Task handle to keep track of created task */\n\t0)) {      /* pin task to core 0 */\n    DIAG(F(\"Could not create wifiLoop task\"));\n    return false;\n  }\n\n  // report server started after wifiLoop creation\n  // when everything looks good\n  DIAG(F(\"Server starting (core 0) port %d\"),port);\n#else\n  DIAG(F(\"Server will be started on port %d\"),port);\n#endif\n  return true;\n}\n\nconst char *wlerror[] = {\n\t\t\t \"WL_IDLE_STATUS\",\n\t\t\t \"WL_NO_SSID_AVAIL\",\n\t\t\t \"WL_SCAN_COMPLETED\",\n\t\t\t \"WL_CONNECTED\",\n\t\t\t \"WL_CONNECT_FAILED\",\n\t\t\t \"WL_CONNECTION_LOST\",\n\t\t\t \"WL_DISCONNECTED\"\n};\n\nvoid WifiESP::loop() {\n  int clientId; //tmp loop var\n\n  // really no good way to check for LISTEN especially in AP mode?\n  wl_status_t wlStatus;\n  if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) {\n    if (server->hasClient()) {\n      WiFiClient client;\n      while (client = server->available()) {\n\tfor (clientId=0; clientId<clients.size(); clientId++){\n\t  if (clients[clientId].recycle(client)) {\n\t    DIAG(F(\"Recycle client %d %s:%d\"), clientId, client.remoteIP().toString().c_str(),client.remotePort());\n\t    break;\n\t  }\n\t}\n\tif (clientId>=clients.size()) {\n\t  NetworkClient nc(client);\n\t  clients.push_back(nc);\n\t  DIAG(F(\"New client %d, %s:%d\"), clientId, client.remoteIP().toString().c_str(),client.remotePort());\n\t}\n      }\n    }\n    // loop over all connected clients\n    // this removes as a side effect inactive clients when checking ::active()\n    for (clientId=0; clientId<clients.size(); clientId++){\n      if(clients[clientId].active(clientId)) {\n\tint len;\n\tif ((len = clients[clientId].wifi.available()) > 0) {\n\t  // read data from client\n\t  byte cmd[len+1];\n\t  for(int i=0; i<len; i++) {\n\t    cmd[i]=clients[clientId].wifi.read();\n\t  }\n\t  cmd[len]=0;\n\t  CommandDistributor::parse(clientId,cmd,outboundRing);\n\t}\n      }\n    } // all clients\n\n    WiThrottle::loop(outboundRing);\n\n    // something to write out?\n    clientId=outboundRing->read();\n    bool useWebsocket=clientId & Websockets::WEBSOCK_CLIENT_MARKER;\n    clientId &= ~ Websockets::WEBSOCK_CLIENT_MARKER;\n    if (clientId >= 0) {\n      // We have data to send in outboundRing\n      // and we have a valid clientId.\n      // First read it out to buffer\n      // and then look if it can be sent because\n      // we can not leave it in the ring for ever\n      int count=outboundRing->count();\n      auto wsHeaderLen=useWebsocket? Websockets::getOutboundHeaderSize(count) : 0;\n      {\n        byte buffer[wsHeaderLen + count + 1];  // one extra for '\\0'\n        if (useWebsocket) Websockets::fillOutboundHeader(count, buffer);\n        for (int i = 0; i < count; i++) {\n          int c = outboundRing->read();\n          if (!c) {\n            DIAG(F(\"Ringread fail at %d\"), i);\n            break;\n          }\n          // websocket implementations at browser end can barf at \\n\n          if (useWebsocket && (c == '\\n')) c = '\\r';\n          buffer[i + wsHeaderLen] = (char)c;\n        }\n        // buffer filled, end with '\\0' so we can use it as C string\n\tbuffer[wsHeaderLen+count]='\\0';\n\tif((unsigned int)clientId <= clients.size()) {\n\t  if (clients[clientId].active(clientId)) {\n\t    if (Diag::WIFI)\n\t      DIAG(F(\"SEND%S %d:%s\"), useWebsocket?F(\"ws\"):F(\"\"),clientId, buffer+wsHeaderLen);\n\t    clients[clientId].wifi.write(buffer,count+wsHeaderLen);\n\t  } else {\n\t    // existed but not active\n\t    DIAG(F(\"Unsent(%d): %s\"), clientId, buffer+wsHeaderLen);\n\t  }\n\t} else {\n\t  DIAG(F(\"Non existent client %d has message: %s\"), clientId, buffer+wsHeaderLen);\n\t}\n      }\n    }\n  } else if (!APmode) { // in STA mode but not connected any more\n    // kick it again\n    if (wlStatus <= 6) {\n      DIAG(F(\"Wifi aborted with error %s. Kicking Wifi!\"), wlerror[wlStatus]);\n      esp_wifi_start();\n      esp_wifi_connect();\n      uint8_t tries=40;\n      while (WiFi.status() != WL_CONNECTED && tries) {\n\tSerial.print('.');\n\ttries--;\n\tdelay(500);\n      }\n    } else {\n      // all well, probably\n      //DIAG(F(\"Running BT\"));\n    }\n  }\n\n  // when loop() is running on core0 we must\n  // feed the core0 wdt ourselves as yield()\n  // is not necessarily yielding to a low\n  // prio task. On core1 this is not a problem\n  // as there the wdt is disabled by the\n  // arduio IDE startup routines.\n  if (xPortGetCoreID() == 0) {\n    feedTheDog0();\n    yield();\n  }\n}\n#endif //ESP32\n"
  },
  {
    "path": "WifiESP32.h",
    "content": "/*\n *  © 2021 Harald Barth\n *  © 2023 Nathan Kellenicki\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n#if defined(ARDUINO_ARCH_ESP32)\n#ifndef WifiESP32_h\n#define WifiESP32_h\n\n#include <WiFi.h>\n#include \"FSH.h\"\n\nclass WifiESP\n{\n\npublic:\n  static bool setup(const char *wifiESSID,\n\t\t    const char *wifiPassword,\n\t\t    const char *hostname,\n\t\t    const int port,\n\t\t    const byte channel,\n\t\t\tconst bool forceAP);\n  static void loop();\nprivate:\n  static void teardown();\n  static bool wifiUp;\n  static WiFiServer *server;\n};\n#endif //WifiESP8266_h\n#endif //ESP8266\n"
  },
  {
    "path": "WifiInboundHandler.cpp",
    "content": "/*\n *  © 2021 Fred Decker\n *  © 2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  © 2020, Chris Harlow. All rights reserved.\n *  © 2020, Harald Barth.\n *  \n *  This file is part of Asbelos DCC API\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef ARDUINO_AVR_UNO_WIFI_REV2\n#include <Arduino.h>\n#include \"WifiInboundHandler.h\"\n#include \"RingStream.h\"\n#include \"CommandDistributor.h\"\n#include \"DIAG.h\"\n#include \"Websockets.h\"\n\nWifiInboundHandler * WifiInboundHandler::singleton;\n\nvoid WifiInboundHandler::setup(Stream * ESStream) {\n  singleton=new WifiInboundHandler(ESStream);\n}\n\nvoid WifiInboundHandler::loop() {\n  singleton->loop1();\n}\n\n\nWifiInboundHandler::WifiInboundHandler(Stream * ESStream) {\n  wifiStream=ESStream;\n  clientPendingCIPSEND=-1;\n  inboundRing=new RingStream(INBOUND_RING);\n  outboundRing=new RingStream(OUTBOUND_RING);\n  pendingCipsend=false;\n} \n\n\n// Handle any inbound transmission\n// +IPD,x,lll:data is stored in streamer[x]\n// Other input returns  \nvoid WifiInboundHandler::loop1() {\n   // First handle all inbound traffic events because they will block the sending \n   if (loop2()!=INBOUND_IDLE) return;\n\n   WiThrottle::loop(outboundRing);\n   \n    // if nothing is already CIPSEND pending, we can CIPSEND one reply\n    if (clientPendingCIPSEND<0) {\n       clientPendingCIPSEND=outboundRing->read();\n       if (clientPendingCIPSEND>=0) {\n         currentReplySize=outboundRing->count();\n         pendingCipsend=true;\n       }\n     }\n    \n\n    if (pendingCipsend && millis()-lastCIPSEND > CIPSENDgap) {\n         // add allowances for websockets\n         bool websocket=clientPendingCIPSEND & Websockets::WEBSOCK_CLIENT_MARKER;\n         byte realClient=clientPendingCIPSEND & ~Websockets::WEBSOCK_CLIENT_MARKER;\n         int16_t realSize=currentReplySize;\n         if (websocket) realSize+=Websockets::getOutboundHeaderSize(currentReplySize);\n         if (Diag::WIFI) DIAG( F(\"WiFi: [[CIPSEND=%d,%d]]\"), realClient, realSize);\n         StringFormatter::send(wifiStream, F(\"AT+CIPSEND=%d,%d\\r\\n\"),  realClient,realSize);\n         pendingCipsend=false;\n         return;\n      }\n    \n    \n    // if something waiting to execute, we can call it \n      int clientId=inboundRing->read();\n      if (clientId>=0) {\n         int count=inboundRing->count();\n         if (Diag::WIFI) DIAG(F(\"Wifi EXEC: %d %d:\"),clientId,count); \n         byte cmd[count+1];\n         // Copy raw bytes to avoid websocket masked data being\n         // confused with a ram-saving flash insert marker.\n         for (int i=0;i<count;i++) cmd[i]=inboundRing->readRawByte();   \n         cmd[count]=0;\n         if (Diag::WIFI) DIAG(F(\"%e\"),cmd); \n         \n         CommandDistributor::parse(clientId,cmd,outboundRing);\n         return;\n      }\n   }\n\n\n\n// This is a Finite State Automation (FSA) handling the inbound bytes from an ES AT command processor    \n\nWifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {\n  const char WebSocketKeyName[]=\"Sec-WebSocket-Key: \";\n  static byte prescanPoint=0;\n\n  while (wifiStream->available()) {\n    int ch = wifiStream->read();\n\n    // echo the char to the diagnostic stream in escaped format\n    if (Diag::WIFI) {\n      // DIAG(F(\" %d/\"), loopState);\n      StringFormatter::printEscape(ch); // DIAG in disguise\n    }\n\n    switch (loopState) {\n      case ANYTHING:  // looking for +IPD, > , busy ,  n,CONNECTED, n,CLOSED, ERROR, SEND OK \n        \n        if (ch == '+') {\n          loopState = IPD;\n          break; \n        }\n        \n        if (ch=='>') { \n           bool websocket=clientPendingCIPSEND & Websockets::WEBSOCK_CLIENT_MARKER; \n           if (Diag::WIFI) DIAG(F(\"[XMIT %d ws=%b]\"),currentReplySize,websocket);\n           if (websocket) Websockets::writeOutboundHeader(wifiStream,currentReplySize); \n           for (int i=0;i<currentReplySize;i++) {\n             int cout=outboundRing->read();\n             if (websocket && (cout=='\\n')) cout='\\r';\n             wifiStream->write(cout);\n             if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise\n           }\n           clientPendingCIPSEND=-1;\n           pendingCipsend=false;\n           loopState=SKIPTOEND;\n           break;\n        }\n        \n        if (ch=='R') { // Received ... bytes \n          loopState=SKIPTOEND;\n          break;\n        }\n       \n        if (ch=='S') { // SEND OK probably \n          loopState=SKIPTOEND;\n          lastCIPSEND=0; // no need to wait next time \n          break;\n        }\n        \n        if (ch=='b') {   // This is a busy indicator... probabaly must restart a CIPSEND  \n           pendingCipsend=(clientPendingCIPSEND>=0);\n           if (pendingCipsend) lastCIPSEND=millis(); // forces a gap to next CIPSEND\n           loopState=SKIPTOEND; \n           break; \n        }\n        \n        if (ch>='0' && ch<='9') { \n              runningClientId=ch-'0';\n              loopState=GOT_CLIENT_ID;\n              break;\n        }\n\n        if (ch=='E' || ch=='l') { // ERROR or \"link is not valid\"\n          if (clientPendingCIPSEND>=0) {\n            // A CIPSEND was errored... just toss it away\n            purgeCurrentCIPSEND(); \n          }\n          loopState=SKIPTOEND; \n          break; \n        }\n        \n        break;\n        \n      case IPD:  // Looking for I   in +IPD\n        loopState = (ch == 'I') ? IPD1 : SKIPTOEND;\n        break;\n        \n      case IPD1:  // Looking for P   in +IPD\n        loopState = (ch == 'P') ? IPD2 : SKIPTOEND;\n        break;\n        \n      case IPD2:  // Looking for D   in +IPD\n        loopState = (ch == 'D') ?  IPD3 : SKIPTOEND;\n        break;\n        \n      case IPD3:  // Looking for ,   After +IPD\n        loopState = (ch == ',') ? IPD4_CLIENT : SKIPTOEND;\n        break;\n        \n      case IPD4_CLIENT:  // reading connection id\n        if (ch >= '0' || ch <='9'){\n           runningClientId=ch-'0';\n           loopState=IPD5;\n        }\n        else loopState=SKIPTOEND;\n        break;\n        \n      case IPD5:  // Looking for ,   After +IPD,client\n        loopState = (ch == ',') ? IPD6_LENGTH : SKIPTOEND;\n        dataLength=0;  // ready to start collecting the length\n        break;\n        \n      case IPD6_LENGTH: // reading for length\n        if (ch == ':') {\n          if (dataLength==0) {\n            loopState=ANYTHING;\n            break;\n          }\n          if (Diag::WIFI) DIAG(F(\"Wifi inbound data(%d:%d):\"),runningClientId,dataLength); \n          \n          // we normally dont read >100 bytes \n          // so assume its an HTTP GET or similar\n         \n          if (dataLength<100 && inboundRing->freeSpace()<=(dataLength+1)) {\n            // This input would overflow the inbound ring, ignore it  \n            loopState=IPD_IGNORE_DATA;\n            if (Diag::WIFI) DIAG(F(\"Wifi OVERFLOW IGNORING:\"));    \n            break;\n          }\n          inboundRing->mark(runningClientId);\n          prescanPoint=0;\n          loopState=(dataLength>100)? IPD_PRESCAN: IPD_DATA;\n          break; \n        }\n        dataLength = dataLength * 10 + (ch - '0');\n        break;\n        \n      case IPD_DATA: // reading data\n        inboundRing->write(ch);    \n        dataLength--;\n        if (dataLength == 0) {\n          inboundRing->commit();    \n          loopState = ANYTHING;\n        }\n        break;\n\n      case IPD_PRESCAN: // prescan reading data\n        dataLength--;\n        if (dataLength == 0) {\n          // Nothing found, this input is lost \n          DIAG(F(\"Wifi prescan for websock not found\"));\n          inboundRing->commit();    \n          loopState = ANYTHING;\n        }\n        if (ch!=WebSocketKeyName[prescanPoint]) {\n            prescanPoint=0;\n            break;\n        }\n        // matched the next char of the key\n        prescanPoint++;\n        if (WebSocketKeyName[prescanPoint]==0) {\n            if (Diag::WEBSOCKET) DIAG(F(\"Wifi prescan found\"));\n           // prescan has detected full key\n           inboundRing->print(WebSocketKeyName);\n           loopState=IPD_POSTSCAN; // continmue as normal\n        }\n        break;\n\n      case IPD_POSTSCAN: // reading data\n        inboundRing->write(ch);    \n        dataLength--;\n        if (ch=='\\n') {\n          inboundRing->commit();    \n          loopState = IPD_IGNORE_DATA;\n        }\n        break;\n  \n\n      case IPD_IGNORE_DATA: // ignoring data that would not fit in inbound ring\n        dataLength--;\n        if (dataLength == 0) loopState = ANYTHING;\n        break;\n\n      case GOT_CLIENT_ID:  // got x before CLOSE or CONNECTED\n        loopState=(ch==',') ? GOT_CLIENT_ID2: SKIPTOEND;\n        break;\n        \n      case GOT_CLIENT_ID2:  // got \"x,\"  \n        if (ch=='C') {\n         // got \"x C\" before CLOSE or CONNECTED, or CONNECT FAILED\n         if (runningClientId==clientPendingCIPSEND) purgeCurrentCIPSEND();\n         else CommandDistributor::forget(runningClientId);\n        }\n        loopState=SKIPTOEND;   \n        break;\n         \n      case SKIPTOEND: // skipping for /n\n        if (ch=='\\n') loopState=ANYTHING;\n        break;\n    }  // switch\n  } // available\n  return (loopState==ANYTHING) ? INBOUND_IDLE: INBOUND_BUSY;\n}\n\nvoid WifiInboundHandler::purgeCurrentCIPSEND() {\n         // A CIPSEND was sent but errored... or the client closed just toss it away\n         CommandDistributor::forget(clientPendingCIPSEND); \n         DIAG(F(\"Wifi: DROPPING CIPSEND=%d,%d\"),clientPendingCIPSEND,currentReplySize);\n         for (int i=0;i<currentReplySize;i++) outboundRing->read();\n         pendingCipsend=false;  \n         clientPendingCIPSEND=-1;\n}\n\n#endif\n"
  },
  {
    "path": "WifiInboundHandler.h",
    "content": "/*\n *  © 2021 Harald Barth\n *  © 2021 Fred Decker\n *  (c) 2021 Fred Decker.  All rights reserved.\n *  (c) 2020-2025 Chris Harlow. All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef WifiInboundHandler_h\n#define WifiInboundHandler_h\n\n#include \"RingStream.h\"\n#include \"WiThrottle.h\"\n#include \"DIAG.h\"\n\nclass WifiInboundHandler {\n public:  \n   static void setup(Stream * ESStream);\n   static void loop();\n   \n   private:\n\n   static WifiInboundHandler * singleton;\n  \n   \n   enum INBOUND_STATE : byte {\n        INBOUND_BUSY,     // keep calling in loop() \n        INBOUND_IDLE     // Nothing happening, outbound may xcall CIPSEND\n   };      \n\n        enum LOOP_STATE : byte {\n          ANYTHING,    // ready for +IPD, n CLOSED, n CONNECTED, busy etc...\n          SKIPTOEND,   // skip to newline\n          \n          // +IPD,client,length:data\n          IPD,         // got +\n          IPD1,        // got +I\n          IPD2,        // got +IP\n          IPD3,        // got +IPD\n          IPD4_CLIENT,  // got +IPD,  reading cient id\n          IPD5,        // got +IPD,c \n          IPD6_LENGTH, // got +IPD,c, reading length \n          IPD_DATA,    // got +IPD,c,ll,: collecting data\n          IPD_IGNORE_DATA, // got +IPD,c,ll,: ignoring the data that won't fit inblound Ring\n          IPD_PRESCAN,    // prescanning data for websocket keys\n          IPD_POSTSCAN,    // copyimg data for websocket keys\n          GOT_CLIENT_ID,  // clientid prefix to CONNECTED / CLOSED\n          GOT_CLIENT_ID2  // clientid prefix to CONNECTED / CLOSED\n  };\n\n  \n   WifiInboundHandler(Stream * ESStream);\n   void loop1();\n   INBOUND_STATE loop2();\n   void purgeCurrentCIPSEND();\n   Stream * wifiStream;\n   \n   static const int INBOUND_RING = 128;\n   static const int OUTBOUND_RING = sizeof(void*)==2?2048:8192;\n \n   static const int CIPSENDgap=100; // millis() between retries of cipsend. \n \n   RingStream * inboundRing;\n   RingStream * outboundRing;\n     \n  LOOP_STATE loopState=ANYTHING;\n  int runningClientId;   // latest client inbound processing data or CLOSE\n  int dataLength; // dataLength of +IPD\n  int clientPendingCIPSEND=-1;\n  int currentReplySize;\n  bool pendingCipsend;\n  uint32_t lastCIPSEND=0; // millis() of previous cipsend\n  \n};\n#endif\n"
  },
  {
    "path": "WifiInterface.cpp",
    "content": "/*\n *  © 2022-2024 Paul M. Antoine\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2022 Chris Harlow\n *  © 2023 Nathan Kellenicki\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef ARDUINO_AVR_UNO_WIFI_REV2\n// This code is NOT compiled on a unoWifiRev2 processor which uses a different architecture \n#include \"WifiInterface.h\"        /* config.h included there */\n//#include <avr/pgmspace.h>\n#include \"DIAG.h\"\n#include \"StringFormatter.h\"\n\n#include \"WifiInboundHandler.h\"\n\n\n\n\nconst unsigned long LOOP_TIMEOUT = 2000;\nbool WifiInterface::connected = false;\nStream * WifiInterface::wifiStream;\n\n#ifndef WIFI_CONNECT_TIMEOUT\n// Tested how long it takes to FAIL an unknown SSID on firmware 1.7.4.\n// The ES should fail a connect in 15 seconds, we don't want to fail BEFORE that\n// or ot will cause issues with the following commands. \n#define WIFI_CONNECT_TIMEOUT 16000\n#endif\n\n////////////////////////////////////////////////////////////////////////////////\n//\n// Figure out number of serial ports depending on hardware\n//\n#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO)\n#define NUM_SERIAL 0\n#endif\n \n#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560))\n#define NUM_SERIAL 3\n#define SERIAL1 Serial1\n#define SERIAL3 Serial3\n#endif\n\n#if defined(ARDUINO_ARCH_STM32)\n// Handle serial ports availability on STM32 for variants!\n// #undef NUM_SERIAL\n#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE)\n#define NUM_SERIAL 3\n#define SERIAL1 Serial1\n#define SERIAL3 Serial6\n#elif defined(ARDUINO_NUCLEO_F446RE)\n#define NUM_SERIAL 3\n#define SERIAL1 Serial3\n#define SERIAL3 Serial5\n#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) \\\n    || defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) \\\n    || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)\n#define NUM_SERIAL 3\n#define SERIAL1 Serial6\n#define SERIAL3 Serial2\n#else\n#warning This variant of Nucleo not yet explicitly supported\n#endif\n#endif\n\n#ifndef NUM_SERIAL\n#define NUM_SERIAL 1\n#define SERIAL1 Serial1\n#endif\n\nbool WifiInterface::setup(long serial_link_speed, \n                          const FSH *wifiESSID,\n                          const FSH *wifiPassword,\n                          const FSH *hostname,\n                          const int port,\n                          const byte channel,\n                          const bool forceAP) {\n\n  wifiSerialState wifiUp = WIFI_NOAT;\n\n#if NUM_SERIAL == 0\n  // no warning about unused parameters. \n  (void) serial_link_speed;\n  (void) wifiESSID;\n  (void) wifiPassword;\n  (void) hostname;\n  (void) port;\n  (void) channel;\n  (void) forceAP;\n#endif  \n\n// See if the WiFi is attached to the first serial port\n#if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS)\n  SERIAL1.begin(serial_link_speed);\n  wifiUp = setup(SERIAL1, wifiESSID, wifiPassword, hostname, port, channel, forceAP);\n#endif\n\n// Other serials are tried, depending on hardware.\n// Currently only the Arduino Mega 2560 has usable Serial2 (Nucleo-64 boards use Serial 2 for console!)\n#if defined(ARDUINO_AVR_MEGA2560)\n#if NUM_SERIAL > 1 && !defined(SERIAL2_COMMANDS)\n  if (wifiUp == WIFI_NOAT)\n  {\n    Serial2.begin(serial_link_speed);\n    wifiUp = setup(Serial2, wifiESSID, wifiPassword, hostname, port, channel, forceAP);\n  }\n#endif\n#endif\n\n// We guess here that in all architctures that have a Serial3\n// we can use it for our purpose.\n#if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS)\n  if (wifiUp == WIFI_NOAT)\n  {\n    SERIAL3.begin(serial_link_speed);\n    wifiUp = setup(SERIAL3, wifiESSID, wifiPassword, hostname, port, channel, forceAP);\n  }\n#endif\n\n  if (wifiUp == WIFI_NOAT) // here and still not AT commands found\n      return false;\n\n  DCCEXParser::setAtCommandCallback(ATCommand);\n  // CAUTION... ONLY CALL THIS ONCE \n  WifiInboundHandler::setup(wifiStream);\n  if (wifiUp == WIFI_CONNECTED)\n      connected = true;\n  else\n      connected = false;\n  return connected; \n}\n\nwifiSerialState WifiInterface::setup(Stream & setupStream,  const FSH* SSid, const FSH* password,\n\t\t\t\t     const FSH* hostname,  int port, byte channel, bool forceAP) {\n  wifiSerialState wifiState;\n  static uint8_t ntry = 0;\n  ntry++;\n\n  wifiStream = &setupStream;\n\n  DIAG(F(\"++ Wifi Setup Try %d ++\"), ntry);\n\n  wifiState = setup2( SSid, password, hostname,  port, channel, forceAP);\n\n  if (wifiState == WIFI_NOAT) {\n    LCD(4, F(\"WiFi no AT chip\"));\n    return wifiState;\n  }\n \n  if (wifiState == WIFI_CONNECTED) {\n    StringFormatter::send(wifiStream, F(\"ATE0\\r\\n\")); // turn off the echo \n    checkForOK(200, true);\n    DIAG(F(\"WiFi CONNECTED\"));\n    // LCD already shows IP\n  } else {\n    LCD(4,F(\"WiFi DISCON.\"));\n  }\n  return wifiState;\n}\n\n#ifdef DONT_TOUCH_WIFI_CONF\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wunused-variable\"\n#pragma GCC diagnostic ignored \"-Wunused-parameter\"\n#endif\nwifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,\n\t\t\t\t      const FSH* hostname, int port, byte channel, bool forceAP) {\n  bool ipOK = false;\n  bool oldCmd = false;\n  bool apMode = false;\n\n  char macAddress[17];  //  mac address extraction   \n \n  // First check... Restarting the Arduino does not restart the ES. \n  //  There may alrerady be a connection with data in the pipeline.\n  // If there is, just shortcut the setup and continue to read the data as normal.\n  if (checkForOK(200,F(\"+IPD\"), true)) {\n    DIAG(F(\"Preconfigured Wifi already running with data waiting\"));\n    return WIFI_CONNECTED; \n  }\n\n  StringFormatter::send(wifiStream, F(\"AT\\r\\n\"));   // Is something here that understands AT?\n  if(!checkForOK(200, true))\n    return WIFI_NOAT;                               // No AT compatible WiFi module here\n\n  StringFormatter::send(wifiStream, F(\"ATE1\\r\\n\")); // Turn on the echo, se we can see what's happening\n  checkForOK(2000, true);                // Makes this visible on the console\n\n  // Display the AT version information\n  StringFormatter::send(wifiStream, F(\"AT+GMR\\r\\n\")); \n  if (checkForOK(2000, F(\"AT version:\"), true, false)) {\n    char version[] = \"0.0.0.0-xxx\";\n    for (int i=0; i<11;i++) {\n      while(!wifiStream->available());\n      version[i]=wifiStream->read();\n      StringFormatter::printEscape(version[i]);\n    }\n    if ((version[0] == '0') ||\n\t(version[0] == '2' && version[2] == '0') ||\n\t(version[0] == '2' && version[2] == '2' && version[4] == '0' && version[6] == '0'\n\t && version[7] == '-' && version[8] == 'd' && version[9] == 'e' && version[10] == 'v')) {\n      DIAG(F(\"You need to up/downgrade the ESP firmware\"));\n      SSid = F(\"UPDATE_ESP_FIRMWARE\");\n      forceAP = true;\n    }\n  }\n  checkForOK(2000, true, false);\n\n#ifdef DONT_TOUCH_WIFI_CONF\n  DIAG(F(\"DONT_TOUCH_WIFI_CONF was set: Using existing config\"));\n#else\n  // Older ES versions have AT+CWJAP, newer ones have AT+CWJAP_CUR and AT+CWHOSTNAME\n  StringFormatter::send(wifiStream, F(\"AT+CWJAP_CUR?\\r\\n\"));\n  if (!(checkForOK(2000, true))) {\n      oldCmd=true;\n      while (wifiStream->available()) StringFormatter::printEscape( wifiStream->read()); /// THIS IS A DIAG IN DISGUISE\n  }\n\n  StringFormatter::send(wifiStream, F(\"AT+CWMODE%s=1\\r\\n\"), oldCmd ? \"\" : \"_CUR\"); // configure as \"station\" = WiFi client\n  checkForOK(1000, true);                       // Not always OK, sometimes \"no change\"\n\n#ifdef DHCPTEST\n  // sometimes the esp8266 will get stuck with DHCP off, so reset DHCP to on\n  // mode=1 which means DHCP for STA mode\n  StringFormatter::send(wifiStream, F(\"AT+CWDHCP%s=1,1\\r\\n\"), oldCmd ? \"\" : \"_CUR\");\n  checkForOK(5000, true);\n#else\n  StringFormatter::send(wifiStream, F(\"AT+CWDHCP%s?\\r\\n\"), oldCmd ? \"\" : \"_CUR\");\n  if (checkForOK(5000, F(\"+CWDHCP\"), true,false)) {\n    if (!checkForOK(5000, F(\"3\"), true,false))\n      DIAG(F(\"Warning: DHCP may be off\"));\n  } else {\n    DIAG(F(\"Warning: Can not determine DHCP state\"));\n  }\n  checkForOK(1000, true); // consume the OK\n#endif\n\n  const char *yourNetwork = \"Your network \";\n  if (STRNCMP_P(yourNetwork, (const char*)SSid, 13) == 0 || STRNCMP_P(\"\", (const char*)SSid, 13) == 0) {\n    if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) {\n      // If the source code looks unconfigured, check if the\n      // ESP8266 is preconfigured in station mode.\n      // We check the first 13 chars of the SSid and the password\n\n      // give a preconfigured ES8266 a chance to connect to a router\n      // typical connect time approx 7 seconds\n      delay(8000);\n      StringFormatter::send(wifiStream, F(\"AT+CIFSR\\r\\n\"));\n      if (checkForOK(5000, F(\"+CIFSR:STAIP\"), true,false))\n\t  if (!checkForOK(1000, F(\"0.0.0.0\"), true,false))\n\t      ipOK = true;\n    }\n  } else if (!forceAP) {\n      // SSID was configured, so we assume station (client) mode.\n      if (oldCmd) {\n\t      // AT command early version supports CWJAP/CWSAP\n\t      StringFormatter::send(wifiStream, F(\"AT+CWJAP=\\\"%S\\\",\\\"%S\\\"\\r\\n\"), SSid, password);\n\t      ipOK = checkForOK(WIFI_CONNECT_TIMEOUT, true);\n      } else {\n\t// later version supports CWJAP_CUR\n        StringFormatter::send(wifiStream, F(\"AT+CWHOSTNAME=\\\"%S\\\"\\r\\n\"), hostname); // Set Host name for Wifi Client\n      \tcheckForOK(5001, true); // dont care if not supported\n\t\n        StringFormatter::send(wifiStream, F(\"AT+CWJAP_CUR=\\\"%S\\\",\\\"%S\\\"\\r\\n\"), SSid, password);\n\tipOK = checkForOK(WIFI_CONNECT_TIMEOUT, true);\n      }\n\n      if (ipOK) {\n\t// But we really only have the ESSID and password correct\n        // Let's check for IP (via DHCP)\n        ipOK = false;\n        StringFormatter::send(wifiStream, F(\"AT+CIFSR\\r\\n\"));\n        if (checkForOK(5004, F(\"+CIFSR:STAIP\"), true,false))\n\t  if (!checkForOK(1000, F(\"0.0.0.0\"), true,false))\n\t    ipOK = true;\n      }\n  }\n\n  if (!ipOK) {\n    // If we have not managed to get this going in station mode, go for AP mode\n\n    //    StringFormatter::send(wifiStream, F(\"AT+RST\\r\\n\"));\n    //    checkForOK(1000, true); // Not always OK, sometimes \"no change\"\n\n    int i=0;\n    do {\n      // configure as AccessPoint. Try really hard as this is the\n      // last way out to get any Wifi connectivity. \n      StringFormatter::send(wifiStream, F(\"AT+CWMODE%s=2\\r\\n\"), oldCmd ? \"\" : \"_CUR\"); \n    } while (!checkForOK(1000+i*500, true) && i++<10);\n    apMode = true;\n\n    while (wifiStream->available()) StringFormatter::printEscape( wifiStream->read()); /// THIS IS A DIAG IN DISGUISE\n\n#ifdef DHCPTEST\n    // sometimes the esp8266 will get stuck with DHCP off, so reset DHCP to on\n    // mode=0 which means DHCP for softAP \n    StringFormatter::send(wifiStream, F(\"AT+CWDHCP%s=0,1\\r\\n\"), oldCmd ? \"\" : \"_CUR\");\n    checkForOK(5000, true);\n#endif\n\n    // Figure out MAC addr\n    StringFormatter::send(wifiStream, F(\"AT+CIFSR\\r\\n\")); // not TOMATO\n    // looking fpr mac addr eg +CIFSR:APMAC,\"be:dd:c2:5c:6b:b7\"\n    if (checkForOK(5000, F(\"+CIFSR:APMAC,\\\"\"), true,false)) {\n      // Copy 17 byte mac address\n      for (int i=0; i<17;i++) {\n        while(!wifiStream->available());\n\tmacAddress[i]=wifiStream->read();\n\tif (macAddress[i] < '0' || macAddress[i] > 'f' || (macAddress[i] > ':' && macAddress[i] < 'a')) {\n\t  // this does not look like a MAC addr format, try to save the day\n\t  // with some printable character\n\t  macAddress[i] = 'x';\n\t}\n\tStringFormatter::printEscape(macAddress[i]);\n      }\n    } else {\n\tmemset(macAddress,'f',sizeof(macAddress));\n    }\n    char macTail[]={macAddress[9],macAddress[10],macAddress[12],macAddress[13],macAddress[15],macAddress[16],'\\0'};\n\n    checkForOK(1000, true, false);  // suck up remainder of AT+CIFSR\n  \n    i=0;\n#ifdef WIFI_HIDE_SSID\n const byte hiddenAP = true;\n#else\n const bool hiddenAP = false;\n#endif\n\n    do {\n      if (!forceAP) {\n        if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) {\n    // unconfigured\n          StringFormatter::send(wifiStream, F(\"AT+CWSAP%s=\\\"DCCEX_%s\\\",\\\"PASS_%s\\\",%d,4,4,%b\\r\\n\"),\n                                            oldCmd ? \"\" : \"_CUR\", macTail, macTail, channel,hiddenAP);\n        } else {\n          // password configured by user\n          StringFormatter::send(wifiStream, F(\"AT+CWSAP%s=\\\"DCCEX_%s\\\",\\\"%S\\\",%d,4,4,%b\\r\\n\"), oldCmd ? \"\" : \"_CUR\",\n                                          macTail, password, channel,hiddenAP);\n        }\n      } else {\n        StringFormatter::send(wifiStream, F(\"AT+CWSAP%s=\\\"%S\\\",\\\"%S\\\",%d,4,4,%b\\r\\n\"),\n                                        oldCmd ? \"\" : \"_CUR\", SSid, password, channel,hiddenAP);\n      }\n    } while (!checkForOK(WIFI_CONNECT_TIMEOUT, true) && i++<2); // do twice if necessary but ignore failure as AP mode may still be ok\n    if (i >= 2)\n\tDIAG(F(\"Warning: Setting AP SSID and password failed\"));       // but issue warning\n\n    if (!oldCmd) {\n      StringFormatter::send(wifiStream, F(\"AT+CIPRECVMODE=0\\r\\n\"), port); // make sure transfer mode is correct\n      checkForOK(2000, true);\n    }\n  }\n#endif //DONT_TOUCH_WIFI_CONF\n\n  StringFormatter::send(wifiStream, F(\"AT+CIPSERVER=0\\r\\n\")); // turn off tcp server (to clean connections before CIPMUX=1)\n  checkForOK(1000, true); // ignore result in case it already was off\n\n  StringFormatter::send(wifiStream, F(\"AT+CIPMUX=1\\r\\n\")); // configure for multiple connections\n  if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;\n\n  if(!oldCmd && !apMode) {  // no idea to test this on old firmware and it works only in STA mode anyway\n    StringFormatter::send(wifiStream, F(\"AT+MDNS=1,\\\"%S\\\",\\\"withrottle\\\",%d\\r\\n\"),\n\t\t\t  hostname, port);                                         // mDNS responder\n    checkForOK(1000, true);                                                        // dont care if not supported\n  }\n\n  StringFormatter::send(wifiStream, F(\"AT+CIPSERVER=1,%d\\r\\n\"), port); // turn on server on port\n  if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;\n \n  StringFormatter::send(wifiStream, F(\"AT+CIFSR\\r\\n\")); // Display  ip addresses to the DIAG \n  if (!checkForOK(1000, F(\"IP,\\\"\") , true, false)) return WIFI_DISCONNECTED;\n  // Copy the IP address\n  {\n    const byte MAX_IP_LENGTH=15;\n    char ipString[MAX_IP_LENGTH+1];\n    ipString[MAX_IP_LENGTH]='\\0'; // protection against missing \" character on end. \n    for(byte ipLen=0;ipLen<MAX_IP_LENGTH;ipLen++) {\n      while(!wifiStream->available());\n      int ipChar=wifiStream->read();\n      StringFormatter::printEscape(ipChar);\n      if (ipChar=='\"') {\n        ipString[ipLen]='\\0';\n        break;\n      }\n      ipString[ipLen]=ipChar;\n    }\n    LCD(4,F(\"%s\"),ipString);  // There is not enough room on some LCDs to put a title to this      \n  }\n  // suck up anything after the IP. \n  if (!checkForOK(1000, true, false)) return WIFI_DISCONNECTED;\n  LCD(5,F(\"PORT=%d\"),port);\n   \n  return WIFI_CONNECTED;\n}\n#ifdef DONT_TOUCH_WIFI_CONF\n#pragma GCC diagnostic pop\n#endif\n\n\n// This function is used to allow users to enter <+ commands> through the DCCEXParser\n// <+command>  sends AT+command to the ES and returns to the caller.\n// Once the user has made whatever changes to the AT commands, a <+X> command can be used\n// to force on the connectd flag so that the loop will start picking up wifi traffic.\n// If the settings are corrupted <+RST> will clear this and then you must restart the arduino.\n\n// Using the <+> command with no command string causes the code to enter an echo loop so that all\n// input is directed to the ES and all ES output written to the USB Serial.\n// The sequence \"!!!\" returns the Arduino to the normal loop mode\n\n \nvoid WifiInterface::ATCommand(HardwareSerial * stream,const byte * command) {\n  command++;\n  if (*command=='\\0') { // User gave <+> command  \n    stream->print(F(\"\\nES AT command passthrough mode, use ! to exit\\n\"));\n    while(stream->available()) stream->read(); // Drain serial input first \n    bool startOfLine=true;\n    while(true) {\n      while (wifiStream->available()) stream->write(wifiStream->read());\n      if (stream->available()) {\n        int cx=stream->read();\n        // A newline followed by ! is an exit\n        if (cx=='\\n' || cx=='\\r') startOfLine=true; \n        else if (startOfLine && cx=='!')  break;\n        else startOfLine=false; \n        wifiStream->write(cx);  \n      }\n    }\n    stream->print(F(\"Passthrough Ended\"));\n    return; \n  }\n  \n  if (*command=='X') {\n    connected = true;\n    DIAG(F(\"++++++ Wifi Connction forced on ++++++++\"));\n  }\n  else {\n    StringFormatter::  send(wifiStream, F(\"AT+%s\\r\\n\"), command);\n    checkForOK(10000,  true);\n  }\n}\n\n\n\nbool WifiInterface::checkForOK( const unsigned int timeout,  bool echo, bool escapeEcho) {\n  return checkForOK(timeout,F(\"\\r\\nOK\\r\\n\"),echo,escapeEcho);\n}\n\nbool WifiInterface::checkForOK( const unsigned int timeout, const FSH * waitfor, bool echo, bool escapeEcho) {\n  unsigned long  startTime = millis();\n  char *locator = (char *)waitfor;\n  DIAG(F(\"Wifi Check: [%E]\"), waitfor);\n  while ( millis() - startTime < timeout) {\n    int nextchar;\n    while (wifiStream->available() && (nextchar = wifiStream->read()) > -1) {\n      char ch = (char)nextchar;\n      if (echo) {\n        if (escapeEcho) StringFormatter::printEscape( ch); /// THIS IS A DIAG IN DISGUISE\n        else USB_SERIAL.print(ch);\n      }\n      if (ch != GETFLASH(locator)) locator = (char *)waitfor;\n      if (ch == GETFLASH(locator)) {\n        locator++;\n        if (!GETFLASH(locator)) {\n          DIAG(F(\"Found in %dms of %d\"), (int)(millis() - startTime), timeout);\n          return true;\n        }\n      }\n    }\n  }\n  DIAG(F(\"TIMEOUT after %dms\"), timeout);\n  return false;\n}\n\n\nvoid WifiInterface::loop() {\n  if (connected) {\n    WifiInboundHandler::loop(); \n  }\n}\n\n#endif\n"
  },
  {
    "path": "WifiInterface.h",
    "content": "/*\n *  © 2020-2021 Chris Harlow\n *  © 2020, Harald Barth.\n *  © 2023 Nathan Kellenicki\n *  All rights reserved.\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n#ifndef WifiInterface_h\n#define WifiInterface_h\n#include \"FSH.h\"\n#include \"DCCEXParser.h\"\n#include <Arduino.h>\n//#include <avr/pgmspace.h>\n\nenum wifiSerialState { WIFI_NOAT, WIFI_DISCONNECTED, WIFI_CONNECTED };\n\nclass WifiInterface\n{\n\npublic:\n  static bool setup(long serial_link_speed, \n                          const FSH *wifiESSID,\n                          const FSH *wifiPassword,\n                          const FSH *hostname,\n                          const int port,\n                          const byte channel,\n                          const bool forceAP);\n  static void loop();\n  static void ATCommand(HardwareSerial * stream,const byte *command);\n  \nprivate:\n  static wifiSerialState setup(Stream &setupStream, const FSH *SSSid, const FSH *password,\n                    const FSH *hostname, int port, byte channel, bool forceAP);\n  static Stream *wifiStream;\n  static DCCEXParser parser;\n  static wifiSerialState setup2(const FSH *SSSid, const FSH *password,\n                     const FSH *hostname, int port, byte channel, bool forceAP);\n  static bool checkForOK(const unsigned int timeout, bool echo, bool escapeEcho = true);\n  static bool checkForOK(const unsigned int timeout, const FSH *waitfor, bool echo, bool escapeEcho = true);\n  static bool connected;\n};\n#endif\n"
  },
  {
    "path": "Ztest.cpp",
    "content": "#include <Arduino.h>\n#include \"DIAG.h\"\n#include \"Ztest.h\"\n#include \"DCCEXParser.h\"\n#include \"StringFormatter.h\"\n \nRingStream * Ztest::ring = new RingStream(128);\n   \nvoid Ztest::parse(const FSH * command, const FSH * expect, bool (*test)() ) {\n  \n  DIAG(F(\"ZTEST %S\"), command);\n\n  // create copy of command in RAM\n  auto commandLength= STRLEN_P((char *)command) + 1;\n  char commandBuffer[commandLength+1];\n  STRNCPY_P(commandBuffer, (PGM_P)command,commandLength+1);\n    \n  if (expect) {\n    // create response buffer to collect comparison\n    ring->flush();\n    ring->mark(0); // mark the start of the response  \n    DCCEXParser::parseOne(ring, (byte *)commandBuffer, ring);\n    ring->commit();\n  }\n  else {\n    // run command (but not if its just a title) without output test\n    if (commandBuffer[0]=='<') \n      DCCEXParser::parseOne(& USB_SERIAL, (byte *)commandBuffer,nullptr);\n  }\n\n  \n  // test the assert of side effects\n  if (test) {\n    auto result=test();\n    DIAG(F(\"ZTEST assert %S\"), result ? F(\"OK\") : F(\"FAILED\"));\n  }\n\n  if (expect) {\n    // Copy output to serial and check with expected\n    ring->read(); // read redundant client id\n    auto responseLength=ring->count();\n    if (responseLength < 1) {\n      DIAG(F(\"!!....(no resp) ZTEST\"));\n      return; // no response\n    }\n\n    // show output while comparing\n    char output[responseLength+1];\n    for (int16_t i=0;i<responseLength;i++) output[i]=ring->read();   \n    output[responseLength]=0;\n    DIAG(F(\"ZTEST response:%s\"), output);\n    DIAG(F(\"ZTEST expect  :%S\"), (char *)expect);\n    if (STRCMP_P(output, (char *)expect) != 0)   \n      DIAG(F(\"ZTEST expect failed\\n\"));\n\n  }          \n  \n}\n"
  },
  {
    "path": "Ztest.h",
    "content": "#ifndef ztest_h\n#define ztest_h\n#include \"RingStream.h\"\n\nclass Ztest {\n  public:\n    static void parse(const FSH * command, const FSH * expect, bool (*test)() );\n    \n    private:\n     static RingStream * ring;\n};\n\n#endif\n"
  },
  {
    "path": "config.example.h",
    "content": "/*\n *  © 2022 Paul M. Antoine\n *  © 2021 Neil McKechnie\n *  © 2020-2025 Harald Barth\n *  © 2020-2021 Fred Decker\n *  © 2020-2025 Chris Harlow\n *  © 2023 Nathan Kellenicki\n *  \n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**********************************************************************\n\nThe configuration file for DCC-EX Command Station\n\n**********************************************************************/\n\n/////////////////////////////////////////////////////////////////////////////////////\n// If you want to add your own motor driver definition(s), add them here\n//   For example MY_SHIELD with display name \"MINE\":\n//   (remove comment start and end marker if you want to edit and use that)\n/* \n#define MY_SHIELD F(\"MINE\"), \\\n new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 5.08, 3000, A4), \\\n new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 5.08, 1500, A5)\n*/\n\n/////////////////////////////////////////////////////////////////////////////////////\n//  NOTE: Before connecting these boards and selecting one in this software\n//        check the quick install guides!!! Some of these boards require a voltage\n//        generating resistor on the current sense pin of the device. Failure to select\n//        the correct resistor could damage the sense pin on your Arduino or destroy\n//        the device.\n//\n// DEFINE MOTOR_SHIELD_TYPE BELOW. THESE ARE EXAMPLES. Full list in MotorDrivers.h\n//\n//  STANDARD_MOTOR_SHIELD : Arduino Motor shield Rev3 based on the L298 with 18V 2A per channel\n//  POLOLU_MOTOR_SHIELD   : Pololu MC33926 Motor Driver (not recommended for prog track)\n//  EX8874_SHIELD         : DCC-EX TI DRV8874 based motor shield\n//  EXCSB1                : DCC-EX CSB-1 hardware\n//  EXCSB1_WITH_EX8874    : DCC-EX CSB-1 hardware with DCC-EX TI DRV8874 shield\n//  NO_SHIELD             : CS without any motor shield (as an accessory only CS)\n//   |\n//   +-----------------------v\n//\n#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD\n//\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// If you want to restrict the maximum current LOWER than what your\n// motor shield can provide, you can do that here. For example if you\n// have a motor shield that can provide 5A and your power supply can\n// only provide 2.5A then you should restict the maximum current to\n// 2.25A (90% of 2.5A) so that DCC-EX does shut off the track before\n// your PS does shut DCC-EX. MAX_CURRENT is in mA so for this example\n// it would be 2250, adjust the number according to your PS. If your\n// PS has a higher rating than your motor shield you do not need this.\n// You can use this as well if you are cautious and your trains do not\n// need full current.\n// #define MAX_CURRENT 2250\n//\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// The IP port to talk to a WIFI or Ethernet shield.\n//\n#define IP_PORT 2560\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// NOTE: Not supported on Arduino Uno or Nano\n// Set to false if you not even want it on the Arduino Mega\n//\n#define ENABLE_WIFI true\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// DEFINE WiFi Parameters (only in effect if WIFI is on)\n//\n// If DONT_TOUCH_WIFI_CONF is set, all WIFI config will be done with\n// the <+> commands and this sketch will not change anything over\n// AT commands and the other WIFI_* defines below do not have any effect.\n//#define DONT_TOUCH_WIFI_CONF\n//\n// WIFI_SSID is the network name IF you want to use your existing home network.\n// Do NOT change this if you want to use the WiFi in Access Point (AP) mode. \n//\n// If you do NOT set the WIFI_SSID and do NOT set the WIFI_PASSWORD,\n// then the WiFi chip will first try to connect to the previously\n// configured network and if that fails fall back to Access Point mode.\n// The SSID of the AP will be automatically set to DCCEX_*.\n// If you DO set the WIFI_SSID then the WiFi chip will try to connect\n// to that (home) network in station (client) mode. If a WIFI_PASSWORD\n// is set (recommended), that password will be used for AP mode.\n// The AP mode password must be at least 8 characters long.\n//\n// Your SSID may not contain ``\"'' (double quote, ASCII 0x22).\n#define WIFI_SSID \"Your network name\"\n//\n// WIFI_PASSWORD is the network password for your home network or if\n// you want to change the password from default AP mode password\n// to the AP password you want. \n// Your password may not contain ``\"'' (double quote, ASCII 0x22).\n#define WIFI_PASSWORD \"Your network passwd\"\n//\n// WIFI_HOSTNAME: You can change this if you have more than one\n// CS to make them show up with different names on the network.\n// Otherwise do not touch.\n#define WIFI_HOSTNAME \"dccex\"\n//\n// WIFI_CHANNEL: The default channel is set to \"1\". If you need to use an\n// alternate channel (we recommend using only 1,6, or 11) you may change it here.\n#define WIFI_CHANNEL 1\n//\n// WIFI_FORCE_AP: If you'd like to specify your own WIFI_SSID in AP mode, set this\n// true. Otherwise it is assumed that you'd like to connect to an existing network\n// with that SSID.\n#define WIFI_FORCE_AP false\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired) based\n// on the W5100/W5500 ethernet chip or an STM32 CS with builin ethernet like the F429ZI.\n// This is not for Wifi. You will then need the Arduino Ethernet library as well.\n//\n//#define ENABLE_ETHERNET true\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// MAX_NUM_TCP_CLIENTS: If you on STM32 Ethernet (and only there) want more than\n// 9 (*) TCP clients, change this number to for example 20 here **AND** in\n// STM32lwiopts.h and follow the instructions in STM32lwiopts.h\n//\n// (*) It would be 10 if there would not be a bug in LwIP by STM32duino.\n//\n//#define MAX_NUM_TCP_CLIENTS 20\n\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// DEFINE STATIC IP ADDRESS *OR* COMMENT OUT TO USE DHCP\n//\n//#define IP_ADDRESS { 192, 168, 1, 200 }\n\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// DEFINE LCD SCREEN USAGE BY THE BASE STATION\n//\n// Note: This feature requires an I2C enabled LCD screen using a Hitachi HD44780\n//       controller and a commonly available PCF8574 based I2C 'backpack'.\n// To enable, uncomment one of the #define lines below\n\n// define LCD_DRIVER for I2C address 0x27, 16 cols, 2 rows\n// #define LCD_DRIVER  0x27,16,2\n\n//OR define OLED_DRIVER width,height[,address] in pixels (address auto detected if not supplied)\n// 128x32 or 128x64 I2C SSD1306-based devices are supported.\n// Use 132,64 for a SH1106-based I2C device with a 128x64 display.\n// #define OLED_DRIVER 0x3c,128,32\n\n// Define scroll mode as 0, 1 or 2\n//  *  #define SCROLLMODE 0 is scroll continuous (fill screen if poss),\n//  *  #define SCROLLMODE 1 is by page (alternate between pages),\n//  *  #define SCROLLMODE 2 is by row (move up 1 row at a time).\n#define SCROLLMODE 1\n\n// In order to avoid wasting memory the current scroll buffer is limited\n// to 8 lines.  Some users wishing to display additional information\n// such as TrackManager power states have requested additional rows aware\n// of the warning that this will take extra RAM.  if you wish to include additional rows\n// uncomment the following #define and set the number of lines you need.\n//#define MAX_CHARACTER_ROWS 12\n\n\n/////////////////////////////////////////////////////////////////////////////////////\n// DISABLE EEPROM\n//\n// If you do not need the EEPROM at all, you can disable all the code that saves\n// data in the EEPROM. You might want to do that if you are in a Arduino UNO\n// and want to use the EXRAIL automation. Otherwise you do not have enough RAM\n// to do that. Of course, then none of the EEPROM related commands work.\n//\n// EEPROM does not work on ESP32. So on ESP32, EEPROM will always be disabled,\n// at least until it works.\n//\n// #define DISABLE_EEPROM\n\n/////////////////////////////////////////////////////////////////////////////////////\n// DISABLE PROG\n//\n// If you do not need programming capability, you can disable all programming related\n// commands. You might want to do that if you are using an Arduino UNO and still want\n// to use EXRAIL automation, as the Uno is lacking in RAM and Flash to run both.\n// \n// Note this disables all programming functionality, including EXRAIL.\n//\n// #define DISABLE_PROG\n\n/////////////////////////////////////////////////////////////////////////////////////\n// DISABLE / ENABLE VDPY\n//\n// The Virtual display \"VDPY\" feature is by default enabled everywhere\n// but on Uno and Nano. If you think you can fit it (for example\n// having disabled some of the features above) you can enable it with\n// ENABLE_VDPY. You can even disable it on all other CPUs with\n// DISABLE_VDPY\n//\n// #define DISABLE_VDPY\n// #define ENABLE_VDPY\n\n/////////////////////////////////////////////////////////////////////////////////////\n// DISABLE / ENABLE DIAG\n//\n// To diagose different errors, you can turn on differnet messages. This costs\n// program memory which we do not have enough on the Uno and Nano, so it is\n// by default DISABLED on those. If you think you can fit it (for example\n// having disabled some of the features above) you can enable it with\n// ENABLE_DIAG. You can even disable it on all other CPUs with\n// DISABLE_DIAG\n//\n// #define DISABLE_DIAG\n// #define ENABLE_DIAG\n\n/////////////////////////////////////////////////////////////////////////////////////\n// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address\n// is 127 and the first long address is 128. There are manufacturers which have\n// another view. Lenz CS for example have considered addresses long from 100. If\n// you want to change to that mode, do \n//#define HIGHEST_SHORT_ADDR 99\n// If you want to run all your locos addressed long format, you could even do a \n//#define HIGHEST_SHORT_ADDR 0\n// We do not support to use the same address, for example 100(long) and 100(short)\n// at the same time, there must be a border.\n\n/////////////////////////////////////////////////////////////////////////////////////\n// REDEFINE locomotive state table size.\n// This is the maximum number of locos that can be controlled at the same time.\n// This defaults to 50 (8 on a UNO/NANO).\n// If you have enough free memory you can increase this to a maximum of 255.\n// If you are short of memory (typically a Mega with WiFi and lots of accessories)\n// you can decrease it (minimum 2)   \n//#define MAX_LOCOS 100\n\n/////////////////////////////////////////////////////////////////////////////////////\n// Some newer 32bit microcontrollers boot very quickly, so powering on I2C and other\n// peripheral devices at the same time may result in the CommandStation booting too\n// quickly to detect them.\n// To work around this, uncomment the STARTUP_DELAY line below and set a value in\n// milliseconds that works for your environment, default is 3000 (3 seconds).\n// #define STARTUP_DELAY 3000\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213\n//\n// According to norm RCN-213 a DCC packet with a 1 is closed/straight\n// and one with a 0 is thrown/diverging.  In DCC++ Classic, and in previous\n// versions of DCC++EX, a turnout throw command was implemented in the packet as \n// '1' and a close command as '0'. The #define below makes the states\n// match with the norm.  But we don't want to cause havoc on existent layouts,\n// so we define this only for new installations. If you don't want this,\n// don't add it to your config.h.\n//#define DCC_TURNOUTS_RCN_213\n\n// By default, the driver which defines a DCC accessory decoder\n// does send out the same state change on the DCC packet as it\n// receives. This means a VPIN state=1 sends D=1 (close turnout\n// or signal green) in the DCC packet. This can be reversed if\n// necessary.\n//#define HAL_ACCESSORY_COMMAND_REVERSE\n\n// If you have issues with that the direction of the accessory commands is\n// reversed (for example when converting from another CS to DCC-EX) then\n// you can use this to reverse the sense of all accessory commmands sent\n// over DCC++. This #define likewise inverts the behaviour of the <a> command\n// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a\n// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0 \n// (throw turnout).\n//#define DCC_ACCESSORY_COMMAND_REVERSE\n\n\n// HANDLING MULTIPLE SERIAL THROTTLES\n// The command station always operates with the default Serial port.\n// Diagnostics are only emitted on the default serial port and not broadcast.\n// Other serial throttles may be added to the Serial1, Serial2, Serial3, Serial4,\n// Serial5, and Serial6 ports which may or may not exist on your CPU. (Mega has 3,\n// SAMD/SAMC and STM32 have up to 6.)\n// To monitor a throttle on one or more serial ports, uncomment the defines below.\n// NOTE: do not define here the WiFi shield serial port or your wifi will not work.\n//\n//#define SERIAL1_COMMANDS\n//#define SERIAL2_COMMANDS\n//#define SERIAL3_COMMANDS\n//#define SERIAL4_COMMANDS\n//#define SERIAL5_COMMANDS\n//#define SERIAL6_COMMANDS\n//\n// BLUETOOTH SERIAL ON ESP32\n// On ESP32 you have the possibility to use the builtin BT serial to connect to\n// the CS.\n//\n// The CS shows up as a pairable BT Clasic device. Name is \"DCCEX-hexnumber\".\n// BT is as an additional serial port, debug messages are still sent over USB,\n// not BT serial.\n//\n// If you enable this there are some implications:\n// 1. WiFi will sleep more (as WiFi and BT share the radio. So WiFi performance\n//    may suffer\n// 2. The app will be bigger that 1.2MB, so the default partition scheme will not\n//    work any more. You need to choose a partition scheme with 2MB (or bigger).\n//    For example \"NO OTA (2MB APP, 2MB SPIFFS)\" in the Arduino IDE.\n// 3. There is no securuity (PIN) implemented. Everyone in radio range can pair\n//    with your CS.\n//\n//#define SERIAL_BT_COMMANDS\n\n// BOOSTER PIN INPUT ON ESP32 CS\n// On ESP32 you have the possibility to define a pin as booster input\n//\n// Arduino pin D2 is GPIO 26 is Booster Input on ESPDuino32\n//#define BOOSTER_INPUT 26\n//\n// GPIO 32 is Booster Input on EX-CSB1\n//#define BOOSTER_INPUT 32\n\n// ESP32 LED Wifi Indicator\n// GPIO 2 on ESPduino32\n//#define WIFI_LED 2\n//\n// GPIO 33 on EX-CSB1\n//#define WIFI_LED 33\n\n// SABERTOOTH\n//\n// This is a very special option and only useful if you happen to have a\n// sabertooth motor controller from dimension engineering configured to\n// take commands from and ESP32 via serial at 9600 baud from GPIO17 (TX)\n// and GPIO16 (RX, currently unused).\n// The number defined is the DCC address for which speed controls are sent\n// to the sabertooth controller _as_well_. Default: Undefined.\n//\n//#define SABERTOOTH 1\n\n/////////////////////////////////////////////////////////////////////////////////////\n//\n// SENSORCAM\n// ESP32-CAM based video sensors require #define to use appropriate base vpin number.\n//#define SENSORCAM_VPIN 700\n// To bypass vPin number, define CAM for ex-rail use e.g. AT(CAM 012) for S12 etc.\n//#define CAM SENSORCAM_VPIN+\n//\n//#define SENSORCAM2_VPIN 600   //define other CAM's if installed.\n//#define CAM2 SENSORCAM2_VPIN+ //for EX-RAIL commands e.g. IFLT(CAM2 020,1)\n//\n// For smoother power-up, when using the CAM, you may need a STARTUP_DELAY.\n// That is described further above.\n//\n/////////////////////////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "defines.h",
    "content": "/*\n *  © 2022 Paul M Antoine\n *  © 2021 Neil McKechnie\n *  © 2021 Mike S\n *  © 2021 Fred Decker\n *  © 2020-2022 Harald Barth\n *  © 2020-2025 Chris Harlow\n *\n *  This file is part of CommandStation-EX\n *\n *  This is free software: you can redistribute it and/or modify\n *  it under the terms of the GNU General Public License as published by\n *  the Free Software Foundation, either version 3 of the License, or\n *  (at your option) any later version.\n *\n *  It is distributed in the hope that it will be useful,\n *  but WITHOUT ANY WARRANTY; without even the implied warranty of\n *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n *  GNU General Public License for more details.\n *\n *  You should have received a copy of the GNU General Public License\n *  along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n *\n */\n\n#ifndef DEFINES_H\n#define DEFINES_H\n// defines.h relies on macros defined in config.h\n// but it may have already been included (for cosmetic convenience) by the .ino\n\n#if __has_include ( \"config.h\")\n    #include \"config.h\"\n#endif\n\n#ifndef MOTOR_SHIELD_TYPE\n  #define MOTOR_SHIELD_TYPE NO_SHIELD\n#endif\n\n\n////////////////////////////////////////////////////////////////////////////////\n// Create a cpu type we can share and \n// figure out if we have enough memory for advanced features\n// so define HAS_ENOUGH_MEMORY until proved otherwise.\n#define HAS_ENOUGH_MEMORY\n#undef USB_SERIAL     // Teensy has this defined by default...\n#define USB_SERIAL Serial\n#define USB_SERIAL_WEB \n// Include extended addresses unless specifically excluded\n#define I2C_EXTENDED_ADDRESS\n\n#if defined(ARDUINO_AVR_UNO)\n  #define ARDUINO_TYPE \"UNO\"\n  #undef HAS_ENOUGH_MEMORY\n  #define NO_EXTENDED_CHARACTERS\n  #undef I2C_EXTENDED_ADDRESS\n  #define DEFAULT_MAX_LOCOS 8\n\n#elif defined(ARDUINO_AVR_NANO)\n  #define ARDUINO_TYPE \"NANO\"\n  #undef HAS_ENOUGH_MEMORY\n  #define NO_EXTENDED_CHARACTERS\n  #undef I2C_EXTENDED_ADDRESS\n  #define DEFAULT_MAX_LOCOS 8\n#elif defined(ARDUINO_AVR_MEGA)\n  #define ARDUINO_TYPE \"MEGA\"\n  #define DEFAULT_MAX_LOCOS 50\n#elif defined(ARDUINO_AVR_MEGA2560)\n  #define ARDUINO_TYPE \"MEGA\"\n  #define DEFAULT_MAX_LOCOS 50\n#elif defined(ARDUINO_ARCH_MEGAAVR)\n  #define ARDUINO_TYPE \"MEGAAVR\"\n  #undef HAS_ENOUGH_MEMORY\n  #define NO_EXTENDED_CHARACTERS\n  #undef I2C_EXTENDED_ADDRESS\n  #define DEFAULT_MAX_LOCOS 8\n#elif defined(ARDUINO_TEENSY31)\n  #define ARDUINO_TYPE \"TEENSY3132\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  // Teensy support for native I2C is awaiting development \n  #ifndef I2C_USE_WIRE\n  #define I2C_USE_WIRE\n  #endif\n#elif defined(ARDUINO_TEENSY35)\n  #define ARDUINO_TYPE \"TEENSY35\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  // Teensy support for I2C is awaiting development \n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  // Teensy support for native I2C is awaiting development \n  #ifndef I2C_USE_WIRE\n  #define I2C_USE_WIRE\n  #endif\n#elif defined(ARDUINO_TEENSY36)\n  #define ARDUINO_TYPE \"TEENSY36\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  // Teensy support for native I2C is awaiting development \n  #ifndef I2C_USE_WIRE\n  #define I2C_USE_WIRE\n  #endif\n#elif defined(ARDUINO_TEENSY40)\n  #define ARDUINO_TYPE \"TEENSY40\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  // Teensy support for native I2C is awaiting development \n  #ifndef I2C_USE_WIRE\n  #define I2C_USE_WIRE\n  #endif\n#elif defined(ARDUINO_TEENSY41)\n  #define ARDUINO_TYPE \"TEENSY41\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  // Teensy support for native I2C is awaiting development \n  #ifndef I2C_USE_WIRE\n    #define I2C_USE_WIRE\n  #endif\n#elif defined(ARDUINO_ARCH_ESP8266)\n  #define ARDUINO_TYPE \"ESP8266\"\n  #warning \"ESP8266 platform untested, you are on your own\"\n#elif defined(ARDUINO_ARCH_ESP32)\n  #define ARDUINO_TYPE \"ESP32\"\n  #ifndef DISABLE_EEPROM\n  #define DISABLE_EEPROM\n  #endif\n  #if ENABLE_WIFI\n   #define ENABLE_SERIAL_LOG\n   #endif\n\n#elif defined(ARDUINO_ARCH_SAMD)\n  #define ARDUINO_TYPE \"SAMD21\"\n  #undef USB_SERIAL\n  #define USB_SERIAL SerialUSB\n  // SAMD no EEPROM by default \n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n#elif defined(ARDUINO_ARCH_STM32)\n  #define ARDUINO_TYPE \"STM32\"\n  // STM32 no EEPROM by default \n  #ifndef DISABLE_EEPROM\n    #define DISABLE_EEPROM\n  #endif\n  #if ENABLE_ETHERNET\n    // WAITING FOR STM32 ETHERNET SUPPORT FIX\n    // #define ENABLE_SERIAL_LOG\n  #endif\n\n  // STM32 support for native I2C is awaiting development \n  // #ifndef I2C_USE_WIRE\n  // #define I2C_USE_WIRE\n  // #endif\n\n/* TODO when ready \n#elif defined(ARDUINO_ARCH_RP2040)\n  #define ARDUINO_TYPE \"RP2040\"\n*/\n\n#else\n  #define CPU_TYPE_ERROR\n#endif\n\n// replace board type if provided by compiler\n#ifdef BOARD_NAME\n  #undef ARDUINO_TYPE\n  #define ARDUINO_TYPE BOARD_NAME\n#endif\n\n////////////////////////////////////////////////////////////////////////////////\n//\n// WIFI_ON: All prereqs for running with WIFI are met\n// Note: WIFI_CHANNEL may not exist in early config.h files so is added here if needed.\n\n#if ENABLE_WIFI\n  #if defined(HAS_ENOUGH_MEMORY)\n    #define WIFI_ON true\n    #ifndef WIFI_CHANNEL\n      #define WIFI_CHANNEL 1\n    #endif\n  #else\n    #define WIFI_WARNING\n    #define WIFI_ON false\n  #endif\n#else\n  #define WIFI_ON false\n#endif\n\n#ifndef WIFI_FORCE_AP\n  #define WIFI_FORCE_AP false\n#else\n  #if WIFI_FORCE_AP==true || WIFI_FORCE_AP==false\n  #else\n    #error WIFI_FORCE_AP needs to be true or false\n  #endif\n#endif\n\n#if ENABLE_ETHERNET\n  #if defined(HAS_ENOUGH_MEMORY)\n    #define ETHERNET_ON true\n  #else\n    #define ETHERNET_WARNING\n    #define ETHERNET_ON false\n  #endif\n#else\n  #define ETHERNET_ON false\n#endif\n\n#if WIFI_ON && ETHERNET_ON\n #error Command Station does not support WIFI and ETHERNET at the same time.\n#endif\n  \n////////////////////////////////////////////////////////////////////////////////\n//\n// This defines the speed at which the Arduino will communicate with the ESP8266 module.\n// Currently only devices which can communicate at 115200 are supported.\n//\n#define WIFI_SERIAL_LINK_SPEED 115200\n\n// configure serial log browser feature if possible\n#ifdef ENABLE_SERIAL_LOG\n    // Replace USB_SERIAL with SerialLog so we can browse it!\n    #undef USB_SERIAL\n    #include \"SerialUsbLog.h\"\n    #define USB_SERIAL SerialLog\n  #endif\n\n////////////////////////////////////////////////////////////////////////////////\n//\n// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required\n// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.\n// \n#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)\n#define IO_NO_HAL // HAL too big whatever you disable otherwise\n\n#ifndef ENABLE_VDPY\n#define DISABLE_VDPY\n#endif\n\n#ifndef ENABLE_DIAG\n#define DISABLE_DIAG\n#endif\n\n#endif\n\n#if __has_include ( \"myAutomation.h\")\n  #if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) || defined(DISABLE_PROG)\n    #define EXRAIL_ACTIVE\n  #else\n    #define EXRAIL_WARNING\n  #endif\n#endif\n\n#if defined(ARDUINO_ARCH_STM32)\n// The LwIP library for the STM32 wired ethernet has by default 10 TCP\n// clients defined but because of a bug in the library #11 is not\n// rejected but kicks out any old connection. By restricting our limit\n// to 9 the #10 will be rejected by our code so that the number can\n// never get to 11 which would kick an existing connection.\n// If you want to change this value, do that in\n// config.h AND in STM32lwipopts.h.\n #ifndef MAX_NUM_TCP_CLIENTS\n  #define MAX_NUM_TCP_CLIENTS 9\n #endif\n#else\n #if defined(ARDUINO_ARCH_ESP32)\n// Espressif LWIP stack\n  #define MAX_NUM_TCP_CLIENTS 10\n #else\n// Wifi shields etc\n  #define MAX_NUM_TCP_CLIENTS 8\n #endif\n#endif\n\n\n// Default MAX_LOCOS if not found in config.h\n#ifndef DEFAULT_MAX_LOCOS \n   #define DEFAULT_MAX_LOCOS 120\n#endif   \n#ifndef MAX_LOCOS \n   #define MAX_LOCOS DEFAULT_MAX_LOCOS\n#endif   \n\n// Default IP_PORT if not found in config.h\n#ifndef IP_PORT\n    #define IP_PORT 2560\n#endif\n\n// Default WIFI_SSID if not found in config.h\n#ifndef WIFI_SSID\n    #define WIFI_SSID \"\"\n \n#endif\n\n// Default WIFI_PASSWORD if not found in config.h\n#ifndef WIFI_PASSWORD\n    #define WIFI_PASSWORD \"Your network passwd\"\n#endif\n\n// Default WIFI_HOSTNAME if not found in config.h\n#ifndef WIFI_HOSTNAME\n    #define WIFI_HOSTNAME \"DCC-EX\"\n#endif\n// Default ETHERNET_HOSTNAME to WIFI_HOSTNAME if not found in config.h (for old EXinstaller compatibility)\n#ifndef ETHERNET_HOSTNAME\n    #define ETHERNET_HOSTNAME WIFI_HOSTNAME\n#endif\n\n\n#endif //DEFINES_H\n"
  },
  {
    "path": "docs/DoxyfileEXRAIL",
    "content": "# Doxyfile 1.9.8\n\n# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) for a project.\n#\n# All text after a double hash (##) is considered a comment and is placed in\n# front of the TAG it is preceding.\n#\n# All text after a single hash (#) is considered a comment and will be ignored.\n# The format is:\n# TAG = value [value, ...]\n# For lists, items can also be appended using:\n# TAG += value [value, ...]\n# Values that contain spaces should be placed between quotes (\\\" \\\").\n#\n# Note:\n#\n# Use doxygen to compare the used configuration file with the template\n# configuration file:\n# doxygen -x [configFile]\n# Use doxygen to compare the used configuration file with the template\n# configuration file without replacing the environment variables or CMake type\n# replacement variables:\n# doxygen -x_noenv [configFile]\n\n#---------------------------------------------------------------------------\n# Project related configuration options\n#---------------------------------------------------------------------------\n\n# This tag specifies the encoding used for all characters in the configuration\n# file that follow. The default is UTF-8 which is also the encoding used for all\n# text before the first occurrence of this tag. Doxygen uses libiconv (or the\n# iconv built into libc) for the transcoding. See\n# https://www.gnu.org/software/libiconv/ for the list of possible encodings.\n# The default value is: UTF-8.\n\nDOXYFILE_ENCODING      = UTF-8\n\n# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by\n# double-quotes, unless you are using Doxywizard) that should identify the\n# project for which the documentation is generated. This name is used in the\n# title of most generated pages and in a few other places.\n# The default value is: My Project.\n\nPROJECT_NAME           = \"EXRAIL Language\"\n\n# The PROJECT_NUMBER tag can be used to enter a project or revision number. This\n# could be handy for archiving the generated documentation or if some version\n# control system is used.\n\nPROJECT_NUMBER         =\n\n# Using the PROJECT_BRIEF tag one can provide an optional one line description\n# for a project that appears at the top of each page and should give viewer a\n# quick idea about the purpose of the project. Keep the description short.\n\nPROJECT_BRIEF          = \"EXRAIL Language\"\n\n# With the PROJECT_LOGO tag one can specify a logo or an icon that is included\n# in the documentation. The maximum height of the logo should not exceed 55\n# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy\n# the logo to the output directory.\n\nPROJECT_LOGO           =\n\n# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path\n# into which the generated documentation will be written. If a relative path is\n# entered, it will be relative to the location where doxygen was started. If\n# left blank the current directory will be used.\n\nOUTPUT_DIRECTORY       = \"_build\"\n\n# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096\n# sub-directories (in 2 levels) under the output directory of each output format\n# and will distribute the generated files over these directories. Enabling this\n# option can be useful when feeding doxygen a huge amount of source files, where\n# putting all generated files in the same directory would otherwise causes\n# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to\n# control the number of sub-directories.\n# The default value is: NO.\n\nCREATE_SUBDIRS         = NO\n\n# Controls the number of sub-directories that will be created when\n# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every\n# level increment doubles the number of directories, resulting in 4096\n# directories at level 8 which is the default and also the maximum value. The\n# sub-directories are organized in 2 levels, the first level always has a fixed\n# number of 16 directories.\n# Minimum value: 0, maximum value: 8, default value: 8.\n# This tag requires that the tag CREATE_SUBDIRS is set to YES.\n\nCREATE_SUBDIRS_LEVEL   = 8\n\n# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII\n# characters to appear in the names of generated files. If set to NO, non-ASCII\n# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode\n# U+3044.\n# The default value is: NO.\n\nALLOW_UNICODE_NAMES    = NO\n\n# The OUTPUT_LANGUAGE tag is used to specify the language in which all\n# documentation generated by doxygen is written. Doxygen will use this\n# information to generate all constant output in the proper language.\n# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian,\n# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English\n# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek,\n# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with\n# English messages), Korean, Korean-en (Korean with English messages), Latvian,\n# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese,\n# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish,\n# Swedish, Turkish, Ukrainian and Vietnamese.\n# The default value is: English.\n\nOUTPUT_LANGUAGE        = English\n\n# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member\n# descriptions after the members that are listed in the file and class\n# documentation (similar to Javadoc). Set to NO to disable this.\n# The default value is: YES.\n\nBRIEF_MEMBER_DESC      = YES\n\n# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief\n# description of a member or function before the detailed description\n#\n# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the\n# brief descriptions will be completely suppressed.\n# The default value is: YES.\n\nREPEAT_BRIEF           = YES\n\n# This tag implements a quasi-intelligent brief description abbreviator that is\n# used to form the text in various listings. Each string in this list, if found\n# as the leading text of the brief description, will be stripped from the text\n# and the result, after processing the whole list, is used as the annotated\n# text. Otherwise, the brief description is used as-is. If left blank, the\n# following values are used ($name is automatically replaced with the name of\n# the entity):The $name class, The $name widget, The $name file, is, provides,\n# specifies, contains, represents, a, an and the.\n\nABBREVIATE_BRIEF       = \"The $name class\" \\\n                         \"The $name widget\" \\\n                         \"The $name file\" \\\n                         is \\\n                         provides \\\n                         specifies \\\n                         contains \\\n                         represents \\\n                         a \\\n                         an \\\n                         the\n\n# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then\n# doxygen will generate a detailed section even if there is only a brief\n# description.\n# The default value is: NO.\n\nALWAYS_DETAILED_SEC    = NO\n\n# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all\n# inherited members of a class in the documentation of that class as if those\n# members were ordinary class members. Constructors, destructors and assignment\n# operators of the base classes will not be shown.\n# The default value is: NO.\n\nINLINE_INHERITED_MEMB  = NO\n\n# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path\n# before files name in the file list and in the header files. If set to NO the\n# shortest path that makes the file name unique will be used\n# The default value is: YES.\n\nFULL_PATH_NAMES        = YES\n\n# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.\n# Stripping is only done if one of the specified strings matches the left-hand\n# part of the path. The tag can be used to show relative paths in the file list.\n# If left blank the directory from which doxygen is run is used as the path to\n# strip.\n#\n# Note that you can specify absolute paths here, but also relative paths, which\n# will be relative from the directory where doxygen is started.\n# This tag requires that the tag FULL_PATH_NAMES is set to YES.\n\nSTRIP_FROM_PATH        =\n\n# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the\n# path mentioned in the documentation of a class, which tells the reader which\n# header file to include in order to use a class. If left blank only the name of\n# the header file containing the class definition is used. Otherwise one should\n# specify the list of include paths that are normally passed to the compiler\n# using the -I flag.\n\nSTRIP_FROM_INC_PATH    =\n\n# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but\n# less readable) file names. This can be useful is your file systems doesn't\n# support long names like on DOS, Mac, or CD-ROM.\n# The default value is: NO.\n\nSHORT_NAMES            = NO\n\n# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the\n# first line (until the first dot) of a Javadoc-style comment as the brief\n# description. If set to NO, the Javadoc-style will behave just like regular Qt-\n# style comments (thus requiring an explicit @brief command for a brief\n# description.)\n# The default value is: NO.\n\nJAVADOC_AUTOBRIEF      = NO\n\n# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line\n# such as\n# /***************\n# as being the beginning of a Javadoc-style comment \"banner\". If set to NO, the\n# Javadoc-style will behave just like regular comments and it will not be\n# interpreted by doxygen.\n# The default value is: NO.\n\nJAVADOC_BANNER         = NO\n\n# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first\n# line (until the first dot) of a Qt-style comment as the brief description. If\n# set to NO, the Qt-style will behave just like regular Qt-style comments (thus\n# requiring an explicit \\brief command for a brief description.)\n# The default value is: NO.\n\nQT_AUTOBRIEF           = NO\n\n# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a\n# multi-line C++ special comment block (i.e. a block of //! or /// comments) as\n# a brief description. This used to be the default behavior. The new default is\n# to treat a multi-line C++ comment block as a detailed description. Set this\n# tag to YES if you prefer the old behavior instead.\n#\n# Note that setting this tag to YES also means that rational rose comments are\n# not recognized any more.\n# The default value is: NO.\n\nMULTILINE_CPP_IS_BRIEF = NO\n\n# By default Python docstrings are displayed as preformatted text and doxygen's\n# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the\n# doxygen's special commands can be used and the contents of the docstring\n# documentation blocks is shown as doxygen documentation.\n# The default value is: YES.\n\nPYTHON_DOCSTRING       = YES\n\n# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the\n# documentation from any documented member that it re-implements.\n# The default value is: YES.\n\nINHERIT_DOCS           = YES\n\n# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new\n# page for each member. If set to NO, the documentation of a member will be part\n# of the file/class/namespace that contains it.\n# The default value is: NO.\n\nSEPARATE_MEMBER_PAGES  = NO\n\n# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen\n# uses this value to replace tabs by spaces in code fragments.\n# Minimum value: 1, maximum value: 16, default value: 4.\n\nTAB_SIZE               = 4\n\n# This tag can be used to specify a number of aliases that act as commands in\n# the documentation. An alias has the form:\n# name=value\n# For example adding\n# \"sideeffect=@par Side Effects:^^\"\n# will allow you to put the command \\sideeffect (or @sideeffect) in the\n# documentation, which will result in a user-defined paragraph with heading\n# \"Side Effects:\". Note that you cannot put \\n's in the value part of an alias\n# to insert newlines (in the resulting output). You can put ^^ in the value part\n# of an alias to insert a newline as if a physical newline was in the original\n# file. When you need a literal { or } or , in the value part of an alias you\n# have to escape them by means of a backslash (\\), this can lead to conflicts\n# with the commands \\{ and \\} for these it is advised to use the version @{ and\n# @} or use a double escape (\\\\{ and \\\\})\n\nALIASES                =\n\n# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources\n# only. Doxygen will then generate output that is more tailored for C. For\n# instance, some of the names that are used will be different. The list of all\n# members will be omitted, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_FOR_C  = NO\n\n# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or\n# Python sources only. Doxygen will then generate output that is more tailored\n# for that language. For instance, namespaces will be presented as packages,\n# qualified scopes will look different, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_JAVA   = NO\n\n# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran\n# sources. Doxygen will then generate output that is tailored for Fortran.\n# The default value is: NO.\n\nOPTIMIZE_FOR_FORTRAN   = NO\n\n# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL\n# sources. Doxygen will then generate output that is tailored for VHDL.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_VHDL   = NO\n\n# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice\n# sources only. Doxygen will then generate output that is more tailored for that\n# language. For instance, namespaces will be presented as modules, types will be\n# separated into more groups, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_SLICE  = NO\n\n# Doxygen selects the parser to use depending on the extension of the files it\n# parses. With this tag you can assign which parser to use for a given\n# extension. Doxygen has a built-in mapping, but you can override or extend it\n# using this tag. The format is ext=language, where ext is a file extension, and\n# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,\n# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,\n# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:\n# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser\n# tries to guess whether the code is fixed or free formatted code, this is the\n# default for Fortran type files). For instance to make doxygen treat .inc files\n# as Fortran files (default is PHP), and .f files as C (default is Fortran),\n# use: inc=Fortran f=C.\n#\n# Note: For files without extension you can use no_extension as a placeholder.\n#\n# Note that for custom extensions you also need to set FILE_PATTERNS otherwise\n# the files are not read by doxygen. When specifying no_extension you should add\n# * to the FILE_PATTERNS.\n#\n# Note see also the list of default file extension mappings.\n\nEXTENSION_MAPPING      =\n\n# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments\n# according to the Markdown format, which allows for more readable\n# documentation. See https://daringfireball.net/projects/markdown/ for details.\n# The output of markdown processing is further processed by doxygen, so you can\n# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in\n# case of backward compatibilities issues.\n# The default value is: YES.\n\nMARKDOWN_SUPPORT       = YES\n\n# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up\n# to that level are automatically included in the table of contents, even if\n# they do not have an id attribute.\n# Note: This feature currently applies only to Markdown headings.\n# Minimum value: 0, maximum value: 99, default value: 5.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nTOC_INCLUDE_HEADINGS   = 5\n\n# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to\n# generate identifiers for the Markdown headings. Note: Every identifier is\n# unique.\n# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a\n# sequence number starting at 0 and GITHUB use the lower case version of title\n# with any whitespace replaced by '-' and punctuation characters removed.\n# The default value is: DOXYGEN.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nMARKDOWN_ID_STYLE      = DOXYGEN\n\n# When enabled doxygen tries to link words that correspond to documented\n# classes, or namespaces to their corresponding documentation. Such a link can\n# be prevented in individual cases by putting a % sign in front of the word or\n# globally by setting AUTOLINK_SUPPORT to NO.\n# The default value is: YES.\n\nAUTOLINK_SUPPORT       = YES\n\n# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want\n# to include (a tag file for) the STL sources as input, then you should set this\n# tag to YES in order to let doxygen match functions declarations and\n# definitions whose arguments contain STL classes (e.g. func(std::string);\n# versus func(std::string) {}). This also make the inheritance and collaboration\n# diagrams that involve STL classes more complete and accurate.\n# The default value is: NO.\n\nBUILTIN_STL_SUPPORT    = NO\n\n# If you use Microsoft's C++/CLI language, you should set this option to YES to\n# enable parsing support.\n# The default value is: NO.\n\nCPP_CLI_SUPPORT        = NO\n\n# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:\n# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen\n# will parse them like normal C++ but will assume all classes use public instead\n# of private inheritance when no explicit protection keyword is present.\n# The default value is: NO.\n\nSIP_SUPPORT            = NO\n\n# For Microsoft's IDL there are propget and propput attributes to indicate\n# getter and setter methods for a property. Setting this option to YES will make\n# doxygen to replace the get and set methods by a property in the documentation.\n# This will only work if the methods are indeed getting or setting a simple\n# type. If this is not the case, or you want to show the methods anyway, you\n# should set this option to NO.\n# The default value is: YES.\n\nIDL_PROPERTY_SUPPORT   = YES\n\n# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC\n# tag is set to YES then doxygen will reuse the documentation of the first\n# member in the group (if any) for the other members of the group. By default\n# all members of a group must be documented explicitly.\n# The default value is: NO.\n\nDISTRIBUTE_GROUP_DOC   = NO\n\n# If one adds a struct or class to a group and this option is enabled, then also\n# any nested class or struct is added to the same group. By default this option\n# is disabled and one has to add nested compounds explicitly via \\ingroup.\n# The default value is: NO.\n\nGROUP_NESTED_COMPOUNDS = NO\n\n# Set the SUBGROUPING tag to YES to allow class member groups of the same type\n# (for instance a group of public functions) to be put as a subgroup of that\n# type (e.g. under the Public Functions section). Set it to NO to prevent\n# subgrouping. Alternatively, this can be done per class using the\n# \\nosubgrouping command.\n# The default value is: YES.\n\nSUBGROUPING            = YES\n\n# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions\n# are shown inside the group in which they are included (e.g. using \\ingroup)\n# instead of on a separate page (for HTML and Man pages) or section (for LaTeX\n# and RTF).\n#\n# Note that this feature does not work in combination with\n# SEPARATE_MEMBER_PAGES.\n# The default value is: NO.\n\nINLINE_GROUPED_CLASSES = NO\n\n# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions\n# with only public data fields or simple typedef fields will be shown inline in\n# the documentation of the scope in which they are defined (i.e. file,\n# namespace, or group documentation), provided this scope is documented. If set\n# to NO, structs, classes, and unions are shown on a separate page (for HTML and\n# Man pages) or section (for LaTeX and RTF).\n# The default value is: NO.\n\nINLINE_SIMPLE_STRUCTS  = NO\n\n# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or\n# enum is documented as struct, union, or enum with the name of the typedef. So\n# typedef struct TypeS {} TypeT, will appear in the documentation as a struct\n# with name TypeT. When disabled the typedef will appear as a member of a file,\n# namespace, or class. And the struct will be named TypeS. This can typically be\n# useful for C code in case the coding convention dictates that all compound\n# types are typedef'ed and only the typedef is referenced, never the tag name.\n# The default value is: NO.\n\nTYPEDEF_HIDES_STRUCT   = NO\n\n# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This\n# cache is used to resolve symbols given their name and scope. Since this can be\n# an expensive process and often the same symbol appears multiple times in the\n# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small\n# doxygen will become slower. If the cache is too large, memory is wasted. The\n# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range\n# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536\n# symbols. At the end of a run doxygen will report the cache usage and suggest\n# the optimal cache size from a speed point of view.\n# Minimum value: 0, maximum value: 9, default value: 0.\n\nLOOKUP_CACHE_SIZE      = 0\n\n# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use\n# during processing. When set to 0 doxygen will based this on the number of\n# cores available in the system. You can set it explicitly to a value larger\n# than 0 to get more control over the balance between CPU load and processing\n# speed. At this moment only the input processing can be done using multiple\n# threads. Since this is still an experimental feature the default is set to 1,\n# which effectively disables parallel processing. Please report any issues you\n# encounter. Generating dot graphs in parallel is controlled by the\n# DOT_NUM_THREADS setting.\n# Minimum value: 0, maximum value: 32, default value: 1.\n\nNUM_PROC_THREADS       = 1\n\n# If the TIMESTAMP tag is set different from NO then each generated page will\n# contain the date or date and time when the page was generated. Setting this to\n# NO can help when comparing the output of multiple runs.\n# Possible values are: YES, NO, DATETIME and DATE.\n# The default value is: NO.\n\nTIMESTAMP              = NO\n\n#---------------------------------------------------------------------------\n# Build related configuration options\n#---------------------------------------------------------------------------\n\n# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in\n# documentation are documented, even if no documentation was available. Private\n# class members and static file members will be hidden unless the\n# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.\n# Note: This will also disable the warnings about undocumented members that are\n# normally produced when WARNINGS is set to YES.\n# The default value is: NO.\n\nEXTRACT_ALL            = YES\n\n# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will\n# be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIVATE        = YES\n\n# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual\n# methods of a class will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIV_VIRTUAL   = NO\n\n# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal\n# scope will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PACKAGE        = NO\n\n# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be\n# included in the documentation.\n# The default value is: NO.\n\nEXTRACT_STATIC         = YES\n\n# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined\n# locally in source files will be included in the documentation. If set to NO,\n# only classes defined in header files are included. Does not have any effect\n# for Java sources.\n# The default value is: YES.\n\nEXTRACT_LOCAL_CLASSES  = YES\n\n# This flag is only useful for Objective-C code. If set to YES, local methods,\n# which are defined in the implementation section but not in the interface are\n# included in the documentation. If set to NO, only methods in the interface are\n# included.\n# The default value is: NO.\n\nEXTRACT_LOCAL_METHODS  = YES\n\n# If this flag is set to YES, the members of anonymous namespaces will be\n# extracted and appear in the documentation as a namespace called\n# 'anonymous_namespace{file}', where file will be replaced with the base name of\n# the file that contains the anonymous namespace. By default anonymous namespace\n# are hidden.\n# The default value is: NO.\n\nEXTRACT_ANON_NSPACES   = NO\n\n# If this flag is set to YES, the name of an unnamed parameter in a declaration\n# will be determined by the corresponding definition. By default unnamed\n# parameters remain unnamed in the output.\n# The default value is: YES.\n\nRESOLVE_UNNAMED_PARAMS = YES\n\n# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all\n# undocumented members inside documented classes or files. If set to NO these\n# members will be included in the various overviews, but no documentation\n# section is generated. This option has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_MEMBERS     = NO\n\n# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all\n# undocumented classes that are normally visible in the class hierarchy. If set\n# to NO, these classes will be included in the various overviews. This option\n# will also hide undocumented C++ concepts if enabled. This option has no effect\n# if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_CLASSES     = NO\n\n# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend\n# declarations. If set to NO, these declarations will be included in the\n# documentation.\n# The default value is: NO.\n\nHIDE_FRIEND_COMPOUNDS  = NO\n\n# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any\n# documentation blocks found inside the body of a function. If set to NO, these\n# blocks will be appended to the function's detailed documentation block.\n# The default value is: NO.\n\nHIDE_IN_BODY_DOCS      = NO\n\n# The INTERNAL_DOCS tag determines if documentation that is typed after a\n# \\internal command is included. If the tag is set to NO then the documentation\n# will be excluded. Set it to YES to include the internal documentation.\n# The default value is: NO.\n\nINTERNAL_DOCS          = NO\n\n# With the correct setting of option CASE_SENSE_NAMES doxygen will better be\n# able to match the capabilities of the underlying filesystem. In case the\n# filesystem is case sensitive (i.e. it supports files in the same directory\n# whose names only differ in casing), the option must be set to YES to properly\n# deal with such files in case they appear in the input. For filesystems that\n# are not case sensitive the option should be set to NO to properly deal with\n# output files written for symbols that only differ in casing, such as for two\n# classes, one named CLASS and the other named Class, and to also support\n# references to files without having to specify the exact matching casing. On\n# Windows (including Cygwin) and MacOS, users should typically set this option\n# to NO, whereas on Linux or other Unix flavors it should typically be set to\n# YES.\n# Possible values are: SYSTEM, NO and YES.\n# The default value is: SYSTEM.\n\nCASE_SENSE_NAMES       = SYSTEM\n\n# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with\n# their full class and namespace scopes in the documentation. If set to YES, the\n# scope will be hidden.\n# The default value is: NO.\n\nHIDE_SCOPE_NAMES       = NO\n\n# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will\n# append additional text to a page's title, such as Class Reference. If set to\n# YES the compound reference will be hidden.\n# The default value is: NO.\n\nHIDE_COMPOUND_REFERENCE= NO\n\n# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class\n# will show which file needs to be included to use the class.\n# The default value is: YES.\n\nSHOW_HEADERFILE        = YES\n\n# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of\n# the files that are included by a file in the documentation of that file.\n# The default value is: YES.\n\nSHOW_INCLUDE_FILES     = YES\n\n# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each\n# grouped member an include statement to the documentation, telling the reader\n# which file to include in order to use the member.\n# The default value is: NO.\n\nSHOW_GROUPED_MEMB_INC  = NO\n\n# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include\n# files with double quotes in the documentation rather than with sharp brackets.\n# The default value is: NO.\n\nFORCE_LOCAL_INCLUDES   = NO\n\n# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the\n# documentation for inline members.\n# The default value is: YES.\n\nINLINE_INFO            = YES\n\n# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the\n# (detailed) documentation of file and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order.\n# The default value is: YES.\n\nSORT_MEMBER_DOCS       = YES\n\n# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief\n# descriptions of file, namespace and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order. Note that\n# this will also influence the order of the classes in the class list.\n# The default value is: NO.\n\nSORT_BRIEF_DOCS        = NO\n\n# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the\n# (brief and detailed) documentation of class members so that constructors and\n# destructors are listed first. If set to NO the constructors will appear in the\n# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.\n# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief\n# member documentation.\n# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting\n# detailed member documentation.\n# The default value is: NO.\n\nSORT_MEMBERS_CTORS_1ST = NO\n\n# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy\n# of group names into alphabetical order. If set to NO the group names will\n# appear in their defined order.\n# The default value is: NO.\n\nSORT_GROUP_NAMES       = NO\n\n# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by\n# fully-qualified names, including namespaces. If set to NO, the class list will\n# be sorted only by class name, not including the namespace part.\n# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.\n# Note: This option applies only to the class list, not to the alphabetical\n# list.\n# The default value is: NO.\n\nSORT_BY_SCOPE_NAME     = NO\n\n# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper\n# type resolution of all parameters of a function it will reject a match between\n# the prototype and the implementation of a member function even if there is\n# only one candidate or it is obvious which candidate to choose by doing a\n# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still\n# accept a match between prototype and implementation in such cases.\n# The default value is: NO.\n\nSTRICT_PROTO_MATCHING  = NO\n\n# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo\n# list. This list is created by putting \\todo commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TODOLIST      = YES\n\n# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test\n# list. This list is created by putting \\test commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TESTLIST      = YES\n\n# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug\n# list. This list is created by putting \\bug commands in the documentation.\n# The default value is: YES.\n\nGENERATE_BUGLIST       = YES\n\n# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)\n# the deprecated list. This list is created by putting \\deprecated commands in\n# the documentation.\n# The default value is: YES.\n\nGENERATE_DEPRECATEDLIST= YES\n\n# The ENABLED_SECTIONS tag can be used to enable conditional documentation\n# sections, marked by \\if <section_label> ... \\endif and \\cond <section_label>\n# ... \\endcond blocks.\n\nENABLED_SECTIONS       =\n\n# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the\n# initial value of a variable or macro / define can have for it to appear in the\n# documentation. If the initializer consists of more lines than specified here\n# it will be hidden. Use a value of 0 to hide initializers completely. The\n# appearance of the value of individual variables and macros / defines can be\n# controlled using \\showinitializer or \\hideinitializer command in the\n# documentation regardless of this setting.\n# Minimum value: 0, maximum value: 10000, default value: 30.\n\nMAX_INITIALIZER_LINES  = 30\n\n# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at\n# the bottom of the documentation of classes and structs. If set to YES, the\n# list will mention the files that were used to generate the documentation.\n# The default value is: YES.\n\nSHOW_USED_FILES        = YES\n\n# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This\n# will remove the Files entry from the Quick Index and from the Folder Tree View\n# (if specified).\n# The default value is: YES.\n\nSHOW_FILES             = YES\n\n# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces\n# page. This will remove the Namespaces entry from the Quick Index and from the\n# Folder Tree View (if specified).\n# The default value is: YES.\n\nSHOW_NAMESPACES        = YES\n\n# The FILE_VERSION_FILTER tag can be used to specify a program or script that\n# doxygen should invoke to get the current version for each file (typically from\n# the version control system). Doxygen will invoke the program by executing (via\n# popen()) the command command input-file, where command is the value of the\n# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided\n# by doxygen. Whatever the program writes to standard output is used as the file\n# version. For an example see the documentation.\n\nFILE_VERSION_FILTER    =\n\n# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed\n# by doxygen. The layout file controls the global structure of the generated\n# output files in an output format independent way. To create the layout file\n# that represents doxygen's defaults, run doxygen with the -l option. You can\n# optionally specify a file name after the option, if omitted DoxygenLayout.xml\n# will be used as the name of the layout file. See also section \"Changing the\n# layout of pages\" for information.\n#\n# Note that if you run doxygen from a directory containing a file called\n# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE\n# tag is left empty.\n\nLAYOUT_FILE            =\n\n# The CITE_BIB_FILES tag can be used to specify one or more bib files containing\n# the reference definitions. This must be a list of .bib files. The .bib\n# extension is automatically appended if omitted. This requires the bibtex tool\n# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.\n# For LaTeX the style of the bibliography can be controlled using\n# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the\n# search path. See also \\cite for info how to create references.\n\nCITE_BIB_FILES         =\n\n#---------------------------------------------------------------------------\n# Configuration options related to warning and progress messages\n#---------------------------------------------------------------------------\n\n# The QUIET tag can be used to turn on/off the messages that are generated to\n# standard output by doxygen. If QUIET is set to YES this implies that the\n# messages are off.\n# The default value is: NO.\n\nQUIET                  = NO\n\n# The WARNINGS tag can be used to turn on/off the warning messages that are\n# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES\n# this implies that the warnings are on.\n#\n# Tip: Turn warnings on while writing the documentation.\n# The default value is: YES.\n\nWARNINGS               = YES\n\n# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate\n# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: YES.\n\nWARN_IF_UNDOCUMENTED   = YES\n\n# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for\n# potential errors in the documentation, such as documenting some parameters in\n# a documented function twice, or documenting parameters that don't exist or\n# using markup commands wrongly.\n# The default value is: YES.\n\nWARN_IF_DOC_ERROR      = YES\n\n# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete\n# function parameter documentation. If set to NO, doxygen will accept that some\n# parameters have no documentation without warning.\n# The default value is: YES.\n\nWARN_IF_INCOMPLETE_DOC = YES\n\n# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that\n# are documented, but have no documentation for their parameters or return\n# value. If set to NO, doxygen will only warn about wrong parameter\n# documentation, but not about the absence of documentation. If EXTRACT_ALL is\n# set to YES then this flag will automatically be disabled. See also\n# WARN_IF_INCOMPLETE_DOC\n# The default value is: NO.\n\nWARN_NO_PARAMDOC       = NO\n\n# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about\n# undocumented enumeration values. If set to NO, doxygen will accept\n# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: NO.\n\nWARN_IF_UNDOC_ENUM_VAL = NO\n\n# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when\n# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS\n# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but\n# at the end of the doxygen process doxygen will return with a non-zero status.\n# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves\n# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not\n# write the warning messages in between other messages but write them at the end\n# of a run, in case a WARN_LOGFILE is defined the warning messages will be\n# besides being in the defined file also be shown at the end of a run, unless\n# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case\n# the behavior will remain as with the setting FAIL_ON_WARNINGS.\n# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.\n# The default value is: NO.\n\nWARN_AS_ERROR          = NO\n\n# The WARN_FORMAT tag determines the format of the warning messages that doxygen\n# can produce. The string should contain the $file, $line, and $text tags, which\n# will be replaced by the file and line number from which the warning originated\n# and the warning text. Optionally the format may contain $version, which will\n# be replaced by the version of the file (if it could be obtained via\n# FILE_VERSION_FILTER)\n# See also: WARN_LINE_FORMAT\n# The default value is: $file:$line: $text.\n\nWARN_FORMAT            = \"$file:$line: $text\"\n\n# In the $text part of the WARN_FORMAT command it is possible that a reference\n# to a more specific place is given. To make it easier to jump to this place\n# (outside of doxygen) the user can define a custom \"cut\" / \"paste\" string.\n# Example:\n# WARN_LINE_FORMAT = \"'vi $file +$line'\"\n# See also: WARN_FORMAT\n# The default value is: at line $line of file $file.\n\nWARN_LINE_FORMAT       = \"at line $line of file $file\"\n\n# The WARN_LOGFILE tag can be used to specify a file to which warning and error\n# messages should be written. If left blank the output is written to standard\n# error (stderr). In case the file specified cannot be opened for writing the\n# warning and error messages are written to standard error. When as file - is\n# specified the warning and error messages are written to standard output\n# (stdout).\n\nWARN_LOGFILE           =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the input files\n#---------------------------------------------------------------------------\n\n# The INPUT tag is used to specify the files and/or directories that contain\n# documented source files. You may enter file names like myfile.cpp or\n# directories like /usr/src/myproject. Separate the files or directories with\n# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING\n# Note: If this tag is empty the current directory is searched.\n\nINPUT                  = ../\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses\n# libiconv (or the iconv built into libc) for the transcoding. See the libiconv\n# documentation (see:\n# https://www.gnu.org/software/libiconv/) for the list of possible encodings.\n# See also: INPUT_FILE_ENCODING\n# The default value is: UTF-8.\n\nINPUT_ENCODING         = UTF-8\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify\n# character encoding on a per file pattern basis. Doxygen will compare the file\n# name with each pattern and apply the encoding instead of the default\n# INPUT_ENCODING) if there is a match. The character encodings are a list of the\n# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding\n# \"INPUT_ENCODING\" for further information on supported encodings.\n\nINPUT_FILE_ENCODING    =\n\n# If the value of the INPUT tag contains directories, you can use the\n# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and\n# *.h) to filter out the source-files in the directories.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# read by doxygen.\n#\n# Note the list of default checked file patterns might differ from the list of\n# default file extension mappings.\n#\n# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,\n# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,\n# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,\n# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be\n# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,\n# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.\n\nFILE_PATTERNS          = EXRAIL2MacroReset.h\n\n# The RECURSIVE tag can be used to specify whether or not subdirectories should\n# be searched for input files as well.\n# The default value is: NO.\n\nRECURSIVE              = NO\n\n# The EXCLUDE tag can be used to specify files and/or directories that should be\n# excluded from the INPUT source files. This way you can easily exclude a\n# subdirectory from a directory tree whose root is specified with the INPUT tag.\n#\n# Note that relative paths are relative to the directory from which doxygen is\n# run.\n\nEXCLUDE                = _build\n\n# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or\n# directories that are symbolic links (a Unix file system feature) are excluded\n# from the input.\n# The default value is: NO.\n\nEXCLUDE_SYMLINKS       = NO\n\n# If the value of the INPUT tag contains directories, you can use the\n# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude\n# certain files from those directories.\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories for example use the pattern */test/*\n\nEXCLUDE_PATTERNS       =\n\n# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names\n# (namespaces, classes, functions, etc.) that should be excluded from the\n# output. The symbol name can be a fully qualified name, a word, or if the\n# wildcard * is used, a substring. Examples: ANamespace, AClass,\n# ANamespace::AClass, ANamespace::*Test\n\nEXCLUDE_SYMBOLS        =\n\n# The EXAMPLE_PATH tag can be used to specify one or more files or directories\n# that contain example code fragments that are included (see the \\include\n# command).\n\nEXAMPLE_PATH           =\n\n# If the value of the EXAMPLE_PATH tag contains directories, you can use the\n# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and\n# *.h) to filter out the source-files in the directories. If left blank all\n# files are included.\n\nEXAMPLE_PATTERNS       = *\n\n# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be\n# searched for input files to be used with the \\include or \\dontinclude commands\n# irrespective of the value of the RECURSIVE tag.\n# The default value is: NO.\n\nEXAMPLE_RECURSIVE      = NO\n\n# The IMAGE_PATH tag can be used to specify one or more files or directories\n# that contain images that are to be included in the documentation (see the\n# \\image command).\n\nIMAGE_PATH             =\n\n# The INPUT_FILTER tag can be used to specify a program that doxygen should\n# invoke to filter for each input file. Doxygen will invoke the filter program\n# by executing (via popen()) the command:\n#\n# <filter> <input-file>\n#\n# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the\n# name of an input file. Doxygen will then use the output that the filter\n# program writes to standard output. If FILTER_PATTERNS is specified, this tag\n# will be ignored.\n#\n# Note that the filter must not add or remove lines; it is applied before the\n# code is scanned, but not when the output code is generated. If lines are added\n# or removed, the anchors will not be placed correctly.\n#\n# Note that doxygen will use the data processed and written to standard output\n# for further processing, therefore nothing else, like debug statements or used\n# commands (so in case of a Windows batch file always use @echo OFF), should be\n# written to standard output.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nINPUT_FILTER           =\n\n# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern\n# basis. Doxygen will compare the file name with each pattern and apply the\n# filter if there is a match. The filters are a list of the form: pattern=filter\n# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how\n# filters are used. If the FILTER_PATTERNS tag is empty or if none of the\n# patterns match the file name, INPUT_FILTER is applied.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nFILTER_PATTERNS        =\n\n# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using\n# INPUT_FILTER) will also be used to filter the input files that are used for\n# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).\n# The default value is: NO.\n\nFILTER_SOURCE_FILES    = NO\n\n# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file\n# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and\n# it is also possible to disable source filtering for a specific pattern using\n# *.ext= (so without naming a filter).\n# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.\n\nFILTER_SOURCE_PATTERNS =\n\n# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that\n# is part of the input, its contents will be placed on the main page\n# (index.html). This can be useful if you have a project on for instance GitHub\n# and want to reuse the introduction page also for the doxygen output.\n\nUSE_MDFILE_AS_MAINPAGE =\n\n# The Fortran standard specifies that for fixed formatted Fortran code all\n# characters from position 72 are to be considered as comment. A common\n# extension is to allow longer lines before the automatic comment starts. The\n# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can\n# be processed before the automatic comment starts.\n# Minimum value: 7, maximum value: 10000, default value: 72.\n\nFORTRAN_COMMENT_AFTER  = 72\n\n#---------------------------------------------------------------------------\n# Configuration options related to source browsing\n#---------------------------------------------------------------------------\n\n# If the SOURCE_BROWSER tag is set to YES then a list of source files will be\n# generated. Documented entities will be cross-referenced with these sources.\n#\n# Note: To get rid of all source code in the generated output, make sure that\n# also VERBATIM_HEADERS is set to NO.\n# The default value is: NO.\n\nSOURCE_BROWSER         = NO\n\n# Setting the INLINE_SOURCES tag to YES will include the body of functions,\n# classes and enums directly into the documentation.\n# The default value is: NO.\n\nINLINE_SOURCES         = NO\n\n# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any\n# special comment blocks from generated source code fragments. Normal C, C++ and\n# Fortran comments will always remain visible.\n# The default value is: YES.\n\nSTRIP_CODE_COMMENTS    = YES\n\n# If the REFERENCED_BY_RELATION tag is set to YES then for each documented\n# entity all documented functions referencing it will be listed.\n# The default value is: NO.\n\nREFERENCED_BY_RELATION = NO\n\n# If the REFERENCES_RELATION tag is set to YES then for each documented function\n# all documented entities called/used by that function will be listed.\n# The default value is: NO.\n\nREFERENCES_RELATION    = NO\n\n# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set\n# to YES then the hyperlinks from functions in REFERENCES_RELATION and\n# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will\n# link to the documentation.\n# The default value is: YES.\n\nREFERENCES_LINK_SOURCE = YES\n\n# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the\n# source code will show a tooltip with additional information such as prototype,\n# brief description and links to the definition and documentation. Since this\n# will make the HTML file larger and loading of large files a bit slower, you\n# can opt to disable this feature.\n# The default value is: YES.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nSOURCE_TOOLTIPS        = YES\n\n# If the USE_HTAGS tag is set to YES then the references to source code will\n# point to the HTML generated by the htags(1) tool instead of doxygen built-in\n# source browser. The htags tool is part of GNU's global source tagging system\n# (see https://www.gnu.org/software/global/global.html). You will need version\n# 4.8.6 or higher.\n#\n# To use it do the following:\n# - Install the latest version of global\n# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file\n# - Make sure the INPUT points to the root of the source tree\n# - Run doxygen as normal\n#\n# Doxygen will invoke htags (and that will in turn invoke gtags), so these\n# tools must be available from the command line (i.e. in the search path).\n#\n# The result: instead of the source browser generated by doxygen, the links to\n# source code will now point to the output of htags.\n# The default value is: NO.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nUSE_HTAGS              = NO\n\n# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a\n# verbatim copy of the header file for each class for which an include is\n# specified. Set to NO to disable this.\n# See also: Section \\class.\n# The default value is: YES.\n\nVERBATIM_HEADERS       = YES\n\n# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the\n# clang parser (see:\n# http://clang.llvm.org/) for more accurate parsing at the cost of reduced\n# performance. This can be particularly helpful with template rich C++ code for\n# which doxygen's built-in parser lacks the necessary type information.\n# Note: The availability of this option depends on whether or not doxygen was\n# generated with the -Duse_libclang=ON option for CMake.\n# The default value is: NO.\n\nCLANG_ASSISTED_PARSING = NO\n\n# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS\n# tag is set to YES then doxygen will add the directory of each input to the\n# include path.\n# The default value is: YES.\n# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.\n\nCLANG_ADD_INC_PATHS    = YES\n\n# If clang assisted parsing is enabled you can provide the compiler with command\n# line options that you would normally use when invoking the compiler. Note that\n# the include paths will already be set by doxygen for the files and directories\n# specified with INPUT and INCLUDE_PATH.\n# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.\n\nCLANG_OPTIONS          =\n\n# If clang assisted parsing is enabled you can provide the clang parser with the\n# path to the directory containing a file called compile_commands.json. This\n# file is the compilation database (see:\n# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the\n# options used when the source files were built. This is equivalent to\n# specifying the -p option to a clang tool, such as clang-check. These options\n# will then be passed to the parser. Any options specified with CLANG_OPTIONS\n# will be added as well.\n# Note: The availability of this option depends on whether or not doxygen was\n# generated with the -Duse_libclang=ON option for CMake.\n\nCLANG_DATABASE_PATH    =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the alphabetical class index\n#---------------------------------------------------------------------------\n\n# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all\n# compounds will be generated. Enable this if the project contains a lot of\n# classes, structs, unions or interfaces.\n# The default value is: YES.\n\nALPHABETICAL_INDEX     = YES\n\n# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)\n# that should be ignored while generating the index headers. The IGNORE_PREFIX\n# tag works for classes, function and member names. The entity will be placed in\n# the alphabetical list under the first letter of the entity name that remains\n# after removing the prefix.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nIGNORE_PREFIX          =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the HTML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output\n# The default value is: YES.\n\nGENERATE_HTML          = NO\n\n# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_OUTPUT            = html\n\n# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each\n# generated HTML page (for example: .htm, .php, .asp).\n# The default value is: .html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FILE_EXTENSION    = .html\n\n# The HTML_HEADER tag can be used to specify a user-defined HTML header file for\n# each generated HTML page. If the tag is left blank doxygen will generate a\n# standard header.\n#\n# To get valid HTML the header file that includes any scripts and style sheets\n# that doxygen needs, which is dependent on the configuration options used (e.g.\n# the setting GENERATE_TREEVIEW). It is highly recommended to start with a\n# default header using\n# doxygen -w html new_header.html new_footer.html new_stylesheet.css\n# YourConfigFile\n# and then modify the file new_header.html. See also section \"Doxygen usage\"\n# for information on how to generate the default header that doxygen normally\n# uses.\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. For a description\n# of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_HEADER            =\n\n# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each\n# generated HTML page. If the tag is left blank doxygen will generate a standard\n# footer. See HTML_HEADER for more information on how to generate a default\n# footer and what special commands can be used inside the footer. See also\n# section \"Doxygen usage\" for information on how to generate the default footer\n# that doxygen normally uses.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FOOTER            =\n\n# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style\n# sheet that is used by each HTML page. It can be used to fine-tune the look of\n# the HTML output. If left blank doxygen will generate a default style sheet.\n# See also section \"Doxygen usage\" for information on how to generate the style\n# sheet that doxygen normally uses.\n# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as\n# it is more robust and this tag (HTML_STYLESHEET) will in the future become\n# obsolete.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_STYLESHEET        =\n\n# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# cascading style sheets that are included after the standard style sheets\n# created by doxygen. Using this option one can overrule certain style aspects.\n# This is preferred over using HTML_STYLESHEET since it does not replace the\n# standard style sheet and is therefore more robust against future updates.\n# Doxygen will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# Note: Since the styling of scrollbars can currently not be overruled in\n# Webkit/Chromium, the styling will be left out of the default doxygen.css if\n# one or more extra stylesheets have been specified. So if scrollbar\n# customization is desired it has to be added explicitly. For an example see the\n# documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_STYLESHEET  =\n\n# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the HTML output directory. Note\n# that these files will be copied to the base HTML output directory. Use the\n# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these\n# files. In the HTML_STYLESHEET file, use the file name only. Also note that the\n# files will be copied as-is; there are no commands or markers available.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_FILES       =\n\n# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output\n# should be rendered with a dark or light theme.\n# Possible values are: LIGHT always generate light mode output, DARK always\n# generate dark mode output, AUTO_LIGHT automatically set the mode according to\n# the user preference, use light mode if no preference is set (the default),\n# AUTO_DARK automatically set the mode according to the user preference, use\n# dark mode if no preference is set and TOGGLE allow to user to switch between\n# light and dark mode via a button.\n# The default value is: AUTO_LIGHT.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE        = AUTO_LIGHT\n\n# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen\n# will adjust the colors in the style sheet and background images according to\n# this color. Hue is specified as an angle on a color-wheel, see\n# https://en.wikipedia.org/wiki/Hue for more information. For instance the value\n# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300\n# purple, and 360 is red again.\n# Minimum value: 0, maximum value: 359, default value: 220.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_HUE    = 220\n\n# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors\n# in the HTML output. For a value of 0 the output will use gray-scales only. A\n# value of 255 will produce the most vivid colors.\n# Minimum value: 0, maximum value: 255, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_SAT    = 100\n\n# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the\n# luminance component of the colors in the HTML output. Values below 100\n# gradually make the output lighter, whereas values above 100 make the output\n# darker. The value divided by 100 is the actual gamma applied, so 80 represents\n# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not\n# change the gamma.\n# Minimum value: 40, maximum value: 240, default value: 80.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_GAMMA  = 80\n\n# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML\n# documentation will contain a main index with vertical navigation menus that\n# are dynamically created via JavaScript. If disabled, the navigation index will\n# consists of multiple levels of tabs that are statically embedded in every HTML\n# page. Disable this option to support browsers that do not have JavaScript,\n# like the Qt help browser.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_MENUS     = YES\n\n# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML\n# documentation will contain sections that can be hidden and shown after the\n# page has loaded.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_SECTIONS  = NO\n\n# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be\n# dynamically folded and expanded in the generated HTML source code.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_CODE_FOLDING      = YES\n\n# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries\n# shown in the various tree structured indices initially; the user can expand\n# and collapse entries dynamically later on. Doxygen will expand the tree to\n# such a level that at most the specified number of entries are visible (unless\n# a fully collapsed tree already exceeds this amount). So setting the number of\n# entries 1 will produce a full collapsed tree by default. 0 is a special value\n# representing an infinite number of entries and will result in a full expanded\n# tree by default.\n# Minimum value: 0, maximum value: 9999, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_INDEX_NUM_ENTRIES = 100\n\n# If the GENERATE_DOCSET tag is set to YES, additional index files will be\n# generated that can be used as input for Apple's Xcode 3 integrated development\n# environment (see:\n# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To\n# create a documentation set, doxygen will generate a Makefile in the HTML\n# output directory. Running make will produce the docset in that directory and\n# running make install will install the docset in\n# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at\n# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy\n# genXcode/_index.html for more information.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_DOCSET        = NO\n\n# This tag determines the name of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# The default value is: Doxygen generated docs.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDNAME        = \"Doxygen generated docs\"\n\n# This tag determines the URL of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDURL         =\n\n# This tag specifies a string that should uniquely identify the documentation\n# set bundle. This should be a reverse domain-name style string, e.g.\n# com.mycompany.MyDocSet. Doxygen will append .docset to the name.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_BUNDLE_ID       = org.doxygen.Project\n\n# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify\n# the documentation publisher. This should be a reverse domain-name style\n# string, e.g. com.mycompany.MyDocSet.documentation.\n# The default value is: org.doxygen.Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_ID    = org.doxygen.Publisher\n\n# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.\n# The default value is: Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_NAME  = Publisher\n\n# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three\n# additional HTML index files: index.hhp, index.hhc, and index.hhk. The\n# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop\n# on Windows. In the beginning of 2021 Microsoft took the original page, with\n# a.o. the download links, offline the HTML help workshop was already many years\n# in maintenance mode). You can download the HTML help workshop from the web\n# archives at Installation executable (see:\n# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo\n# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).\n#\n# The HTML Help Workshop contains a compiler that can convert all HTML output\n# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML\n# files are now used as the Windows 98 help format, and will replace the old\n# Windows help format (.hlp) on all Windows platforms in the future. Compressed\n# HTML files also contain an index, a table of contents, and you can search for\n# words in the documentation. The HTML workshop also contains a viewer for\n# compressed HTML files.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_HTMLHELP      = NO\n\n# The CHM_FILE tag can be used to specify the file name of the resulting .chm\n# file. You can add a path in front of the file if the result should not be\n# written to the html output directory.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_FILE               =\n\n# The HHC_LOCATION tag can be used to specify the location (absolute path\n# including file name) of the HTML help compiler (hhc.exe). If non-empty,\n# doxygen will try to run the HTML help compiler on the generated index.hhp.\n# The file has to be specified with full path.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nHHC_LOCATION           =\n\n# The GENERATE_CHI flag controls if a separate .chi index file is generated\n# (YES) or that it should be included in the main .chm file (NO).\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nGENERATE_CHI           = NO\n\n# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)\n# and project file content.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_INDEX_ENCODING     =\n\n# The BINARY_TOC flag controls whether a binary table of contents is generated\n# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it\n# enables the Previous and Next buttons.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nBINARY_TOC             = NO\n\n# The TOC_EXPAND flag can be set to YES to add extra items for group members to\n# the table of contents of the HTML help documentation and to the tree view.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nTOC_EXPAND             = NO\n\n# The SITEMAP_URL tag is used to specify the full URL of the place where the\n# generated documentation will be placed on the server by the user during the\n# deployment of the documentation. The generated sitemap is called sitemap.xml\n# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL\n# is specified no sitemap is generated. For information about the sitemap\n# protocol see https://www.sitemaps.org\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSITEMAP_URL            =\n\n# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and\n# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that\n# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help\n# (.qch) of the generated HTML documentation.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_QHP           = NO\n\n# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify\n# the file name of the resulting .qch file. The path specified is relative to\n# the HTML output folder.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQCH_FILE               =\n\n# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help\n# Project output. For more information please see Qt Help Project / Namespace\n# (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_NAMESPACE          = org.doxygen.Project\n\n# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt\n# Help Project output. For more information please see Qt Help Project / Virtual\n# Folders (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).\n# The default value is: doc.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_VIRTUAL_FOLDER     = doc\n\n# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom\n# filter to add. For more information please see Qt Help Project / Custom\n# Filters (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_NAME   =\n\n# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the\n# custom filter to add. For more information please see Qt Help Project / Custom\n# Filters (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_ATTRS  =\n\n# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this\n# project's filter section matches. Qt Help Project / Filter Attributes (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_SECT_FILTER_ATTRS  =\n\n# The QHG_LOCATION tag can be used to specify the location (absolute path\n# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to\n# run qhelpgenerator on the generated .qhp file.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHG_LOCATION           =\n\n# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be\n# generated, together with the HTML files, they form an Eclipse help plugin. To\n# install this plugin and make it available under the help contents menu in\n# Eclipse, the contents of the directory containing the HTML and XML files needs\n# to be copied into the plugins directory of eclipse. The name of the directory\n# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.\n# After copying Eclipse needs to be restarted before the help appears.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_ECLIPSEHELP   = NO\n\n# A unique identifier for the Eclipse help plugin. When installing the plugin\n# the directory name containing the HTML and XML files should also have this\n# name. Each documentation set should have its own identifier.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.\n\nECLIPSE_DOC_ID         = org.doxygen.Project\n\n# If you want full control over the layout of the generated HTML pages it might\n# be necessary to disable the index and replace it with your own. The\n# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top\n# of each HTML page. A value of NO enables the index and the value YES disables\n# it. Since the tabs in the index contain the same information as the navigation\n# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nDISABLE_INDEX          = NO\n\n# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index\n# structure should be generated to display hierarchical information. If the tag\n# value is set to YES, a side panel will be generated containing a tree-like\n# index structure (just like the one that is generated for HTML Help). For this\n# to work a browser that supports JavaScript, DHTML, CSS and frames is required\n# (i.e. any modern browser). Windows users are probably better off using the\n# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can\n# further fine tune the look of the index (see \"Fine-tuning the output\"). As an\n# example, the default style sheet generated by doxygen has an example that\n# shows how to put an image at the root of the tree instead of the PROJECT_NAME.\n# Since the tree basically has the same information as the tab index, you could\n# consider setting DISABLE_INDEX to YES when enabling this option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_TREEVIEW      = NO\n\n# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the\n# FULL_SIDEBAR option determines if the side bar is limited to only the treeview\n# area (value NO) or if it should extend to the full height of the window (value\n# YES). Setting this to YES gives a layout similar to\n# https://docs.readthedocs.io with more room for contents, but less room for the\n# project logo, title, and description. If either GENERATE_TREEVIEW or\n# DISABLE_INDEX is set to NO, this option has no effect.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFULL_SIDEBAR           = NO\n\n# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that\n# doxygen will group on one line in the generated HTML documentation.\n#\n# Note that a value of 0 will completely suppress the enum values from appearing\n# in the overview section.\n# Minimum value: 0, maximum value: 20, default value: 4.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nENUM_VALUES_PER_LINE   = 4\n\n# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used\n# to set the initial width (in pixels) of the frame in which the tree is shown.\n# Minimum value: 0, maximum value: 1500, default value: 250.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nTREEVIEW_WIDTH         = 250\n\n# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to\n# external symbols imported via tag files in a separate window.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nEXT_LINKS_IN_WINDOW    = NO\n\n# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email\n# addresses.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nOBFUSCATE_EMAILS       = YES\n\n# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg\n# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see\n# https://inkscape.org) to generate formulas as SVG images instead of PNGs for\n# the HTML output. These images will generally look nicer at scaled resolutions.\n# Possible values are: png (the default) and svg (looks nicer but requires the\n# pdf2svg or inkscape tool).\n# The default value is: png.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FORMULA_FORMAT    = png\n\n# Use this tag to change the font size of LaTeX formulas included as images in\n# the HTML documentation. When you change the font size after a successful\n# doxygen run you need to manually remove any form_*.png images from the HTML\n# output directory to force them to be regenerated.\n# Minimum value: 8, maximum value: 50, default value: 10.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_FONTSIZE       = 10\n\n# The FORMULA_MACROFILE can contain LaTeX \\newcommand and \\renewcommand commands\n# to create new LaTeX commands to be used in formulas as building blocks. See\n# the section \"Including formulas\" for details.\n\nFORMULA_MACROFILE      =\n\n# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see\n# https://www.mathjax.org) which uses client side JavaScript for the rendering\n# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX\n# installed or if you want to formulas look prettier in the HTML output. When\n# enabled you may also need to install MathJax separately and configure the path\n# to it using the MATHJAX_RELPATH option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nUSE_MATHJAX            = NO\n\n# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.\n# Note that the different versions of MathJax have different requirements with\n# regards to the different settings, so it is possible that also other MathJax\n# settings have to be changed when switching between the different MathJax\n# versions.\n# Possible values are: MathJax_2 and MathJax_3.\n# The default value is: MathJax_2.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_VERSION        = MathJax_2\n\n# When MathJax is enabled you can set the default output format to be used for\n# the MathJax output. For more details about the output format see MathJax\n# version 2 (see:\n# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3\n# (see:\n# http://docs.mathjax.org/en/latest/web/components/output.html).\n# Possible values are: HTML-CSS (which is slower, but has the best\n# compatibility. This is the name for Mathjax version 2, for MathJax version 3\n# this will be translated into chtml), NativeMML (i.e. MathML. Only supported\n# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This\n# is the name for Mathjax version 3, for MathJax version 2 this will be\n# translated into HTML-CSS) and SVG.\n# The default value is: HTML-CSS.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_FORMAT         = HTML-CSS\n\n# When MathJax is enabled you need to specify the location relative to the HTML\n# output directory using the MATHJAX_RELPATH option. The destination directory\n# should contain the MathJax.js script. For instance, if the mathjax directory\n# is located at the same level as the HTML output directory, then\n# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax\n# Content Delivery Network so you can quickly see the result without installing\n# MathJax. However, it is strongly recommended to install a local copy of\n# MathJax from https://www.mathjax.org before deployment. The default value is:\n# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2\n# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_RELPATH        =\n\n# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax\n# extension names that should be enabled during MathJax rendering. For example\n# for MathJax version 2 (see\n# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):\n# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols\n# For example for MathJax version 3 (see\n# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):\n# MATHJAX_EXTENSIONS = ams\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_EXTENSIONS     =\n\n# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces\n# of code that will be used on startup of the MathJax code. See the MathJax site\n# (see:\n# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an\n# example see the documentation.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_CODEFILE       =\n\n# When the SEARCHENGINE tag is enabled doxygen will generate a search box for\n# the HTML output. The underlying search engine uses javascript and DHTML and\n# should work on any modern browser. Note that when using HTML help\n# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)\n# there is already a search function so this one should typically be disabled.\n# For large projects the javascript based search engine can be slow, then\n# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to\n# search using the keyboard; to jump to the search box use <access key> + S\n# (what the <access key> is depends on the OS and browser, but it is typically\n# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down\n# key> to jump into the search results window, the results can be navigated\n# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel\n# the search. The filter options can be selected when the cursor is inside the\n# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>\n# to select a filter and <Enter> or <escape> to activate or cancel the filter\n# option.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSEARCHENGINE           = YES\n\n# When the SERVER_BASED_SEARCH tag is enabled the search engine will be\n# implemented using a web server instead of a web client using JavaScript. There\n# are two flavors of web server based searching depending on the EXTERNAL_SEARCH\n# setting. When disabled, doxygen will generate a PHP script for searching and\n# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing\n# and searching needs to be provided by external tools. See the section\n# \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSERVER_BASED_SEARCH    = NO\n\n# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP\n# script for searching. Instead the search results are written to an XML file\n# which needs to be processed by an external indexer. Doxygen will invoke an\n# external search engine pointed to by the SEARCHENGINE_URL option to obtain the\n# search results.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see:\n# https://xapian.org/).\n#\n# See the section \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH        = NO\n\n# The SEARCHENGINE_URL should point to a search engine hosted by a web server\n# which will return the search results when EXTERNAL_SEARCH is enabled.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see:\n# https://xapian.org/). See the section \"External Indexing and Searching\" for\n# details.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHENGINE_URL       =\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed\n# search data is written to a file for indexing by an external tool. With the\n# SEARCHDATA_FILE tag the name of this file can be specified.\n# The default file is: searchdata.xml.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHDATA_FILE        = searchdata.xml\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the\n# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is\n# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple\n# projects and redirect the results back to the right project.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH_ID     =\n\n# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen\n# projects other than the one defined by this configuration file, but that are\n# all added to the same external search index. Each project needs to have a\n# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of\n# to a relative location where the documentation can be found. The format is:\n# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTRA_SEARCH_MAPPINGS  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the LaTeX output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.\n# The default value is: YES.\n\nGENERATE_LATEX         = YES\n\n# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: latex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_OUTPUT           = latex\n\n# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be\n# invoked.\n#\n# Note that when not enabling USE_PDFLATEX the default is latex when enabling\n# USE_PDFLATEX the default is pdflatex and when in the later case latex is\n# chosen this is overwritten by pdflatex. For specific output languages the\n# default can have been set differently, this depends on the implementation of\n# the output language.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_CMD_NAME         =\n\n# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate\n# index for LaTeX.\n# Note: This tag is used in the Makefile / make.bat.\n# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file\n# (.tex).\n# The default file is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nMAKEINDEX_CMD_NAME     = makeindex\n\n# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to\n# generate index for LaTeX. In case there is no backslash (\\) as first character\n# it will be automatically added in the LaTeX code.\n# Note: This tag is used in the generated output file (.tex).\n# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.\n# The default value is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_MAKEINDEX_CMD    = makeindex\n\n# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nCOMPACT_LATEX          = NO\n\n# The PAPER_TYPE tag can be used to set the paper type that is used by the\n# printer.\n# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x\n# 14 inches) and executive (7.25 x 10.5 inches).\n# The default value is: a4.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPAPER_TYPE             = a4\n\n# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names\n# that should be included in the LaTeX output. The package can be specified just\n# by its name or with the correct syntax as to be used with the LaTeX\n# \\usepackage command. To get the times font for instance you can specify :\n# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}\n# To use the option intlimits with the amsmath package you can specify:\n# EXTRA_PACKAGES=[intlimits]{amsmath}\n# If left blank no extra packages will be included.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nEXTRA_PACKAGES         =\n\n# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for\n# the generated LaTeX document. The header should contain everything until the\n# first chapter. If it is left blank doxygen will generate a standard header. It\n# is highly recommended to start with a default header using\n# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty\n# and then modify the file new_header.tex. See also section \"Doxygen usage\" for\n# information on how to generate the default header that doxygen normally uses.\n#\n# Note: Only use a user-defined header if you know what you are doing!\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. The following\n# commands have a special meaning inside the header (and footer): For a\n# description of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HEADER           =\n\n# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for\n# the generated LaTeX document. The footer should contain everything after the\n# last chapter. If it is left blank doxygen will generate a standard footer. See\n# LATEX_HEADER for more information on how to generate a default footer and what\n# special commands can be used inside the footer. See also section \"Doxygen\n# usage\" for information on how to generate the default footer that doxygen\n# normally uses. Note: Only use a user-defined footer if you know what you are\n# doing!\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_FOOTER           =\n\n# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# LaTeX style sheets that are included after the standard style sheets created\n# by doxygen. Using this option one can overrule certain style aspects. Doxygen\n# will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_STYLESHEET =\n\n# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the LATEX_OUTPUT output\n# directory. Note that the files will be copied as-is; there are no commands or\n# markers available.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_FILES      =\n\n# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is\n# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will\n# contain links (just like the HTML output) instead of page references. This\n# makes the output suitable for online browsing using a PDF viewer.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPDF_HYPERLINKS         = NO\n\n# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as\n# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX\n# files. Set this option to YES, to get a higher quality PDF documentation.\n#\n# See also section LATEX_CMD_NAME for selecting the engine.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nUSE_PDFLATEX           = NO\n\n# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.\n# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch\n# mode nothing is printed on the terminal, errors are scrolled as if <return> is\n# hit at every error; missing files that TeX tries to input or request from\n# keyboard input (\\read on a not open input stream) cause the job to abort,\n# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,\n# but there is no possibility of user interaction just like in batch mode,\n# SCROLL In scroll mode, TeX will stop only for missing files to input or if\n# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at\n# each error, asking for user intervention.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BATCHMODE        = NO\n\n# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the\n# index chapters (such as File Index, Compound Index, etc.) in the output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HIDE_INDICES     = NO\n\n# The LATEX_BIB_STYLE tag can be used to specify the style to use for the\n# bibliography, e.g. plainnat, or ieeetr. See\n# https://en.wikipedia.org/wiki/BibTeX and \\cite for more info.\n# The default value is: plain.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BIB_STYLE        = plain\n\n# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)\n# path from which the emoji images will be read. If a relative path is entered,\n# it will be relative to the LATEX_OUTPUT directory. If left blank the\n# LATEX_OUTPUT directory will be used.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EMOJI_DIRECTORY  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the RTF output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The\n# RTF output is optimized for Word 97 and may not look too pretty with other RTF\n# readers/editors.\n# The default value is: NO.\n\nGENERATE_RTF           = NO\n\n# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: rtf.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_OUTPUT             = rtf\n\n# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nCOMPACT_RTF            = NO\n\n# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will\n# contain hyperlink fields. The RTF file will contain links (just like the HTML\n# output) instead of page references. This makes the output suitable for online\n# browsing using Word or some other Word compatible readers that support those\n# fields.\n#\n# Note: WordPad (write) and others do not support links.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_HYPERLINKS         = NO\n\n# Load stylesheet definitions from file. Syntax is similar to doxygen's\n# configuration file, i.e. a series of assignments. You only have to provide\n# replacements, missing definitions are set to their default value.\n#\n# See also section \"Doxygen usage\" for information on how to generate the\n# default style sheet that doxygen normally uses.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_STYLESHEET_FILE    =\n\n# Set optional variables used in the generation of an RTF document. Syntax is\n# similar to doxygen's configuration file. A template extensions file can be\n# generated using doxygen -e rtf extensionFile.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_EXTENSIONS_FILE    =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the man page output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for\n# classes and files.\n# The default value is: NO.\n\nGENERATE_MAN           = NO\n\n# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it. A directory man3 will be created inside the directory specified by\n# MAN_OUTPUT.\n# The default directory is: man.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_OUTPUT             = man\n\n# The MAN_EXTENSION tag determines the extension that is added to the generated\n# man pages. In case the manual section does not start with a number, the number\n# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is\n# optional.\n# The default value is: .3.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_EXTENSION          = .3\n\n# The MAN_SUBDIR tag determines the name of the directory created within\n# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by\n# MAN_EXTENSION with the initial . removed.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_SUBDIR             =\n\n# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it\n# will generate one additional man file for each entity documented in the real\n# man page(s). These additional files only source the real man page, but without\n# them the man command would be unable to find the correct page.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_LINKS              = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the XML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that\n# captures the structure of the code including all documentation.\n# The default value is: NO.\n\nGENERATE_XML           = YES\n\n# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: xml.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_OUTPUT             = xml\n\n# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program\n# listings (including syntax highlighting and cross-referencing information) to\n# the XML output. Note that enabling this will significantly increase the size\n# of the XML output.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_PROGRAMLISTING     = YES\n\n# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include\n# namespace members in file scope as well, matching the HTML output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_NS_MEMB_FILE_SCOPE = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the DOCBOOK output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files\n# that can be used to generate PDF.\n# The default value is: NO.\n\nGENERATE_DOCBOOK       = NO\n\n# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.\n# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in\n# front of it.\n# The default directory is: docbook.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_OUTPUT         = docbook\n\n#---------------------------------------------------------------------------\n# Configuration options for the AutoGen Definitions output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an\n# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures\n# the structure of the code including all documentation. Note that this feature\n# is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_AUTOGEN_DEF   = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to Sqlite3 output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3\n# database with symbols found by doxygen stored in tables.\n# The default value is: NO.\n\nGENERATE_SQLITE3       = NO\n\n# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be\n# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put\n# in front of it.\n# The default directory is: sqlite3.\n# This tag requires that the tag GENERATE_SQLITE3 is set to YES.\n\nSQLITE3_OUTPUT         = sqlite3\n\n# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db\n# database file will be recreated with each doxygen run. If set to NO, doxygen\n# will warn if an a database file is already found and not modify it.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_SQLITE3 is set to YES.\n\nSQLITE3_RECREATE_DB    = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the Perl module output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module\n# file that captures the structure of the code including all documentation.\n#\n# Note that this feature is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_PERLMOD       = NO\n\n# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary\n# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI\n# output from the Perl module output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_LATEX          = NO\n\n# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely\n# formatted so it can be parsed by a human reader. This is useful if you want to\n# understand what is going on. On the other hand, if this tag is set to NO, the\n# size of the Perl module output will be much smaller and Perl will parse it\n# just the same.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_PRETTY         = YES\n\n# The names of the make variables in the generated doxyrules.make file are\n# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful\n# so different doxyrules.make files included by the same Makefile don't\n# overwrite each other's variables.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_MAKEVAR_PREFIX =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the preprocessor\n#---------------------------------------------------------------------------\n\n# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all\n# C-preprocessor directives found in the sources and include files.\n# The default value is: YES.\n\nENABLE_PREPROCESSING   = YES\n\n# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names\n# in the source code. If set to NO, only conditional compilation will be\n# performed. Macro expansion can be done in a controlled way by setting\n# EXPAND_ONLY_PREDEF to YES.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nMACRO_EXPANSION        = NO\n\n# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then\n# the macro expansion is limited to the macros specified with the PREDEFINED and\n# EXPAND_AS_DEFINED tags.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_ONLY_PREDEF     = NO\n\n# If the SEARCH_INCLUDES tag is set to YES, the include files in the\n# INCLUDE_PATH will be searched if a #include is found.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSEARCH_INCLUDES        = YES\n\n# The INCLUDE_PATH tag can be used to specify one or more directories that\n# contain include files that are not input files but should be processed by the\n# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of\n# RECURSIVE has no effect here.\n# This tag requires that the tag SEARCH_INCLUDES is set to YES.\n\nINCLUDE_PATH           =\n\n# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard\n# patterns (like *.h and *.hpp) to filter out the header-files in the\n# directories. If left blank, the patterns specified with FILE_PATTERNS will be\n# used.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nINCLUDE_FILE_PATTERNS  =\n\n# The PREDEFINED tag can be used to specify one or more macro names that are\n# defined before the preprocessor is started (similar to the -D option of e.g.\n# gcc). The argument of the tag is a list of macros of the form: name or\n# name=definition (no spaces). If the definition and the \"=\" are omitted, \"=1\"\n# is assumed. To prevent a macro definition from being undefined via #undef or\n# recursively expanded use the := operator instead of the = operator.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nPREDEFINED             =\n\n# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this\n# tag can be used to specify a list of macro names that should be expanded. The\n# macro definition that is found in the sources will be used. Use the PREDEFINED\n# tag if you want to use a different macro definition that overrules the\n# definition found in the source code.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_AS_DEFINED      =\n\n# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will\n# remove all references to function-like macros that are alone on a line, have\n# an all uppercase name, and do not end with a semicolon. Such function macros\n# are typically used for boiler-plate code, and will confuse the parser if not\n# removed.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSKIP_FUNCTION_MACROS   = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to external references\n#---------------------------------------------------------------------------\n\n# The TAGFILES tag can be used to specify one or more tag files. For each tag\n# file the location of the external documentation should be added. The format of\n# a tag file without this location is as follows:\n# TAGFILES = file1 file2 ...\n# Adding location for the tag files is done as follows:\n# TAGFILES = file1=loc1 \"file2 = loc2\" ...\n# where loc1 and loc2 can be relative or absolute paths or URLs. See the\n# section \"Linking to external documentation\" for more information about the use\n# of tag files.\n# Note: Each tag file must have a unique name (where the name does NOT include\n# the path). If a tag file is not located in the directory in which doxygen is\n# run, you must also specify the path to the tagfile here.\n\nTAGFILES               =\n\n# When a file name is specified after GENERATE_TAGFILE, doxygen will create a\n# tag file that is based on the input files it reads. See section \"Linking to\n# external documentation\" for more information about the usage of tag files.\n\nGENERATE_TAGFILE       =\n\n# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces\n# will be listed in the class and namespace index. If set to NO, only the\n# inherited external classes will be listed.\n# The default value is: NO.\n\nALLEXTERNALS           = NO\n\n# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed\n# in the topic index. If set to NO, only the current project's groups will be\n# listed.\n# The default value is: YES.\n\nEXTERNAL_GROUPS        = YES\n\n# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in\n# the related pages index. If set to NO, only the current project's pages will\n# be listed.\n# The default value is: YES.\n\nEXTERNAL_PAGES         = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to diagram generator tools\n#---------------------------------------------------------------------------\n\n# If set to YES the inheritance and collaboration graphs will hide inheritance\n# and usage relations if the target is undocumented or is not a class.\n# The default value is: YES.\n\nHIDE_UNDOC_RELATIONS   = YES\n\n# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is\n# available from the path. This tool is part of Graphviz (see:\n# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent\n# Bell Labs. The other options in this section have no effect if this option is\n# set to NO\n# The default value is: NO.\n\nHAVE_DOT               = NO\n\n# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed\n# to run in parallel. When set to 0 doxygen will base this on the number of\n# processors available in the system. You can set it explicitly to a value\n# larger than 0 to get control over the balance between CPU load and processing\n# speed.\n# Minimum value: 0, maximum value: 32, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NUM_THREADS        = 0\n\n# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of\n# subgraphs. When you want a differently looking font in the dot files that\n# doxygen generates you can specify fontname, fontcolor and fontsize attributes.\n# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,\n# Edge and Graph Attributes specification</a> You need to make sure dot is able\n# to find the font, which can be done by putting it in a standard location or by\n# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the\n# directory containing the font. Default graphviz fontsize is 14.\n# The default value is: fontname=Helvetica,fontsize=10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_COMMON_ATTR        = \"fontname=Helvetica,fontsize=10\"\n\n# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can\n# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a\n# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about\n# arrows shapes.</a>\n# The default value is: labelfontname=Helvetica,labelfontsize=10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_EDGE_ATTR          = \"labelfontname=Helvetica,labelfontsize=10\"\n\n# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes\n# around nodes set 'shape=plain' or 'shape=plaintext' <a\n# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>\n# The default value is: shape=box,height=0.2,width=0.4.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NODE_ATTR          = \"shape=box,height=0.2,width=0.4\"\n\n# You can set the path where dot can find font specified with fontname in\n# DOT_COMMON_ATTR and others dot attributes.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTPATH           =\n\n# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will\n# generate a graph for each documented class showing the direct and indirect\n# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and\n# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case\n# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the\n# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.\n# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance\n# relations will be shown as texts / links.\n# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.\n# The default value is: YES.\n\nCLASS_GRAPH            = YES\n\n# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a\n# graph for each documented class showing the direct and indirect implementation\n# dependencies (inheritance, containment, and class references variables) of the\n# class with other documented classes. Explicit enabling a collaboration graph,\n# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the\n# command \\collaborationgraph. Disabling a collaboration graph can be\n# accomplished by means of the command \\hidecollaborationgraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCOLLABORATION_GRAPH    = YES\n\n# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for\n# groups, showing the direct groups dependencies. Explicit enabling a group\n# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means\n# of the command \\groupgraph. Disabling a directory graph can be accomplished by\n# means of the command \\hidegroupgraph. See also the chapter Grouping in the\n# manual.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGROUP_GRAPHS           = YES\n\n# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and\n# collaboration diagrams in a style similar to the OMG's Unified Modeling\n# Language.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LOOK               = NO\n\n# If the UML_LOOK tag is enabled, the fields and methods are shown inside the\n# class node. If there are many fields or methods and many nodes the graph may\n# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the\n# number of items for each type to make the size more manageable. Set this to 0\n# for no limit. Note that the threshold may be exceeded by 50% before the limit\n# is enforced. So when you set the threshold to 10, up to 15 fields may appear,\n# but if the number exceeds 15, the total amount of fields shown is limited to\n# 10.\n# Minimum value: 0, maximum value: 100, default value: 10.\n# This tag requires that the tag UML_LOOK is set to YES.\n\nUML_LIMIT_NUM_FIELDS   = 10\n\n# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and\n# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS\n# tag is set to YES, doxygen will add type and arguments for attributes and\n# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen\n# will not generate fields with class member information in the UML graphs. The\n# class diagrams will look similar to the default class diagrams but using UML\n# notation for the relationships.\n# Possible values are: NO, YES and NONE.\n# The default value is: NO.\n# This tag requires that the tag UML_LOOK is set to YES.\n\nDOT_UML_DETAILS        = NO\n\n# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters\n# to display on a single line. If the actual line length exceeds this threshold\n# significantly it will wrapped across multiple lines. Some heuristics are apply\n# to avoid ugly line breaks.\n# Minimum value: 0, maximum value: 1000, default value: 17.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_WRAP_THRESHOLD     = 17\n\n# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and\n# collaboration graphs will show the relations between templates and their\n# instances.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nTEMPLATE_RELATIONS     = NO\n\n# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to\n# YES then doxygen will generate a graph for each documented file showing the\n# direct and indirect include dependencies of the file with other documented\n# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,\n# can be accomplished by means of the command \\includegraph. Disabling an\n# include graph can be accomplished by means of the command \\hideincludegraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDE_GRAPH          = YES\n\n# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are\n# set to YES then doxygen will generate a graph for each documented file showing\n# the direct and indirect include dependencies of the file with other documented\n# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set\n# to NO, can be accomplished by means of the command \\includedbygraph. Disabling\n# an included by graph can be accomplished by means of the command\n# \\hideincludedbygraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDED_BY_GRAPH      = YES\n\n# If the CALL_GRAPH tag is set to YES then doxygen will generate a call\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable call graphs for selected\n# functions only using the \\callgraph command. Disabling a call graph can be\n# accomplished by means of the command \\hidecallgraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALL_GRAPH             = NO\n\n# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable caller graphs for selected\n# functions only using the \\callergraph command. Disabling a caller graph can be\n# accomplished by means of the command \\hidecallergraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALLER_GRAPH           = NO\n\n# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical\n# hierarchy of all classes instead of a textual one.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGRAPHICAL_HIERARCHY    = YES\n\n# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the\n# dependencies a directory has on other directories in a graphical way. The\n# dependency relations are determined by the #include relations between the\n# files in the directories. Explicit enabling a directory graph, when\n# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command\n# \\directorygraph. Disabling a directory graph can be accomplished by means of\n# the command \\hidedirectorygraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDIRECTORY_GRAPH        = YES\n\n# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels\n# of child directories generated in directory dependency graphs by dot.\n# Minimum value: 1, maximum value: 25, default value: 1.\n# This tag requires that the tag DIRECTORY_GRAPH is set to YES.\n\nDIR_GRAPH_MAX_DEPTH    = 1\n\n# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images\n# generated by dot. For an explanation of the image formats see the section\n# output formats in the documentation of the dot tool (Graphviz (see:\n# https://www.graphviz.org/)).\n# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order\n# to make the SVG files visible in IE 9+ (other browsers do not have this\n# requirement).\n# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,\n# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and\n# png:gdiplus:gdiplus.\n# The default value is: png.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_IMAGE_FORMAT       = png\n\n# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to\n# enable generation of interactive SVG images that allow zooming and panning.\n#\n# Note that this requires a modern browser other than Internet Explorer. Tested\n# and working are Firefox, Chrome, Safari, and Opera.\n# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make\n# the SVG files visible. Older versions of IE do not have SVG support.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINTERACTIVE_SVG        = NO\n\n# The DOT_PATH tag can be used to specify the path where the dot tool can be\n# found. If left blank, it is assumed the dot tool can be found in the path.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_PATH               =\n\n# The DOTFILE_DIRS tag can be used to specify one or more directories that\n# contain dot files that are included in the documentation (see the \\dotfile\n# command).\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOTFILE_DIRS           =\n\n# You can include diagrams made with dia in doxygen documentation. Doxygen will\n# then run dia to produce the diagram and insert it in the documentation. The\n# DIA_PATH tag allows you to specify the directory where the dia binary resides.\n# If left empty dia is assumed to be found in the default search path.\n\nDIA_PATH               =\n\n# The DIAFILE_DIRS tag can be used to specify one or more directories that\n# contain dia files that are included in the documentation (see the \\diafile\n# command).\n\nDIAFILE_DIRS           =\n\n# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the\n# path where java can find the plantuml.jar file or to the filename of jar file\n# to be used. If left blank, it is assumed PlantUML is not used or called during\n# a preprocessing step. Doxygen will generate a warning when it encounters a\n# \\startuml command in this case and will not generate output for the diagram.\n\nPLANTUML_JAR_PATH      =\n\n# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a\n# configuration file for plantuml.\n\nPLANTUML_CFG_FILE      =\n\n# When using plantuml, the specified paths are searched for files specified by\n# the !include statement in a plantuml block.\n\nPLANTUML_INCLUDE_PATH  =\n\n# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes\n# that will be shown in the graph. If the number of nodes in a graph becomes\n# larger than this value, doxygen will truncate the graph, which is visualized\n# by representing a node as a red box. Note that doxygen if the number of direct\n# children of the root node in a graph is already larger than\n# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that\n# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.\n# Minimum value: 0, maximum value: 10000, default value: 50.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_GRAPH_MAX_NODES    = 50\n\n# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs\n# generated by dot. A depth value of 3 means that only nodes reachable from the\n# root by following a path via at most 3 edges will be shown. Nodes that lay\n# further from the root node will be omitted. Note that setting this option to 1\n# or 2 may greatly reduce the computation time needed for large code bases. Also\n# note that the size of a graph can be further restricted by\n# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.\n# Minimum value: 0, maximum value: 1000, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nMAX_DOT_GRAPH_DEPTH    = 0\n\n# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output\n# files in one run (i.e. multiple -o and -T options on the command line). This\n# makes dot run faster, but since only newer versions of dot (>1.8.10) support\n# this, this feature is disabled by default.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_MULTI_TARGETS      = NO\n\n# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page\n# explaining the meaning of the various boxes and arrows in the dot generated\n# graphs.\n# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal\n# graphical representation for inheritance and collaboration diagrams is used.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGENERATE_LEGEND        = YES\n\n# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate\n# files that are used to generate the various graphs.\n#\n# Note: This setting is not only used for dot files but also for msc temporary\n# files.\n# The default value is: YES.\n\nDOT_CLEANUP            = YES\n\n# You can define message sequence charts within doxygen comments using the \\msc\n# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will\n# use a built-in version of mscgen tool to produce the charts. Alternatively,\n# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,\n# specifying prog as the value, doxygen will call the tool as prog -T\n# <outfile_format> -o <outputfile> <inputfile>. The external tool should support\n# output file formats \"png\", \"eps\", \"svg\", and \"ismap\".\n\nMSCGEN_TOOL            =\n\n# The MSCFILE_DIRS tag can be used to specify one or more directories that\n# contain msc files that are included in the documentation (see the \\mscfile\n# command).\n\nMSCFILE_DIRS           =\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/_static/css/dccex_theme.css",
    "content": "@import url(https://fonts.googleapis.com/css?family=Audiowide);\n@import url(https://fonts.googleapis.com/css?family=Roboto);\n\nh1, .h1 {\n  font-family: Audiowide,Helvetica,Arial,sans-serif !important;\n  font-weight: 500 !important;\n  color: #00353d !important;\n  /* font-size: 200% !important; */\n  font-size: 180% !important;\n  text-shadow: 1px 1px #ffffff78;\n}\nhtml[data-theme='dark'] h1, .h1 {\n  color: #ffffff !important;\n  text-shadow: 1px 1px #00353d;\n}\n\nh2, .h2 {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  color: #00353d !important;\n  /* font-size: 190% !important; */\n  font-size: 160% !important;\n  text-shadow: 1px 1px #ffffff78;\n}\nhtml[data-theme='dark'] h2, .h2 {\n  color: #ffffff !important;\n  text-shadow: 1px 1px #00353d;\n}\nhtml[data-theme='dark'] h2 a, \nhtml[data-theme='dark'] h2 a:visited {\n  color: #00a3b9ff !important;\n}\n\nh3, .h3 {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  color: #00353d !important;\n  /* font-size: 160% !important; */\n  font-size: 140% !important;\n  font-style: italic !important;\n  text-shadow: 1px 1px #ffffff78;\n}\nhtml[data-theme='dark'] h3, .h3 {\n  color: #ffffff !important;\n  text-shadow: 1px 1px #00353d;\n}\nhtml[data-theme='dark'] h3 a, \nhtml[data-theme='dark'] h3 a:visited {\n  color: #00a3b9ff !important;\n}\nh4, .h4 {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  color: #00353d !important;\n  /* font-size: 130% !important; */\n  font-size: 120% !important;\n  text-shadow: 1px 1px #ffffff78;\n}\nhtml[data-theme='dark'] h4, .h4 {\n  color: #00a3b9ff !important;\n  text-shadow: 1px 1px #00353d;\n}\nhtml[data-theme='dark'] h4 a, \nhtml[data-theme='dark'] h4 a:visited {\n  color: #00a3b9ff !important;\n  text-shadow: 1px 1px #00353d;\n}\nh5, .h5 {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  color: #00a3b9ff !important;\n  /* font-size: 110% !important; */\n  font-size: 100% !important;\n}\n\nh6, .h6 {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  color: #00a3b9ff !important;\n  font-size: 90% !important;\n  font-style: italic !important;\n}\n\n.clearer {\n  clear: both;\n}\n\n.wy-nav-side {\n  background: #031c20 !important;\n  /* background: #031214 !important; */\n}\n\n.caption-text {\n  color: #00a3b9ff !important;\n}\n\n.wy-nav-top {\n  background:#00a3b9ff !important;\n  font-size: 80% !important; \n}\n\n.wy-nav-top a {\n  font-family: Audiowide,Helvetica,Arial,sans-serif !important;\n  font-weight: 100 !important;\n}\n\n.wy-nav-content {\n  max-width: 1024px;\n}\n\n.wy-breadcrumbs {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n}\n\n.wy-side-nav-search>a img.logo {\n    width: 100%;\n}\n\n.rst-content table.docutils th {\n  background-color: #F3F6F6;\n}\n\n.rst-content table.docutils td {\n  background-color: #F3F6F6;\n}\n\n.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {\n  background-color: #E0E0E0;\n}\nhtml[data-theme='dark'] .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {\n  background-color: #ffffff08 !important;\n}\n\n.caption-number {\n  font-size: small !important;\n}\n\n.caption-text {\n  font-size: small !important;\n}\n\ntable.intro-table {\n  max-width: 600px;\n}\n\n.intro-table img {\n  width: 70%;\n  height: auto;\n  margin: 5% 15%;\n}\n\nhtml[data-theme='dark'] .btn-neutral {\n  color: #c1c1c1 !important;\n}\n\n#ex-rail-command-summary .wy-table-responsive {\n  overflow: visible;\n}\n\n/* product titles */\n.ex-prefix {\n  font-weight: bold; \n  color: #00a3b9; \n  font-size: 110%;\n}\n.ex-suffix {\n  font-weight: bold; \n  color: #00353d;\n  font-size: 110%;\n}\nhtml[data-theme='dark'] .ex-suffix {\n  font-weight: bold; \n  color: #006979;\n  font-size: 110%;\n}\n\n/* main dcc-ex text only */\n.dccex-prefix {\n  font-family: Audiowide,Helvetica,Arial,sans-serif;\n  font-weight: 600; \n  color: #00353d;\n  font-size: 110%;\n}\nhtml[data-theme='dark'] .dccex-prefix {\n  font-family: Audiowide,Helvetica,Arial,sans-serif;\n  font-weight: 600; \n  color: #006979;\n  font-size: 110%;\n}\n.dccex-suffix {\n  font-family: Audiowide,Helvetica,Arial,sans-serif;\n  font-weight: 600; \n  color: #00a3b9; \n  font-size: 110%;\n}\n\n/***************************/\n\n.command-table thead th {\n  text-align: center;\n}\n\n.command-table tbody td {\n  white-space: normal;\n  margin: 10px;\n  padding: 8px 8px 8px 8px !important;\n}\n\n.command-table tbody tr:first-child td p code {\n  white-space: nowrap !important;\n}\n\n.command-table tbody tr td p code {\n  font-size: 110% !important;\n}\n\n.command-table tbody tr td p {\n  font-size: 90% !important;\n}\n\n.command-table tbody tr td ol li p {\n  font-size: 90% !important;\n}\n\n.command-table tbody tr td ol {\n  margin-bottom: 0px !important;\n}\n\n.command-table .category {\n  display: block;\n  text-align: center;\n}\n\n.command-table tr:nth-child(odd) {\n  background-color: #f1f1f1 !important;  \n}\n\n.command-table tr:nth-child(even) {\n  background-color: #f8f8f8 !important;  \n}\nhtml[data-theme='dark']  .command-table tr:nth-child(even) {\n  background-color: #ffffff08 !important;\n}\n\n.command-table td {\n  background-color: #ffffff00  !important;  \n}\n\n/* html[data-theme='dark'] .rst-content table.docutils tr:nth-child(odd) {\n  background-color: #ffffff08 !important;\n} */\nhtml[data-theme='dark'] .rst-content table.docutils td, .wy-table-bordered-all td {\n  background-color: #fff40000 !important;\n}\n/* html[data-theme='dark']  .rst-content table.docutils .row-odd {\n  background-color: #36ff0000 !important;\n} */\n\nhtml[data-theme='dark'] .rst-content table.docutils th {\n  background-color: #36ff0000 !important;\n  color: white !important;\n  font-style: italic !important;;\n  font-weight: 700 !important;;\n}\n\n/* *************************************** */\n\nhtml[data-theme='dark'] .sd-card {\n  background-color: #0000008a;\n  box-shadow: 0 0.5rem 1rem rgb(32 88 91 / 25%) !important;\n}\n\n\n/* *************************************** */\n\n.dcclink a {\n  background-color: #00a3b9ff;\n  box-shadow: 0 2px 0 #00353dff;\n  color: white !important;\n  padding: 0.5em 0.5em;\n  position: relative;\n  text-decoration: none;\n  text-transform: none;\n  border-radius: 5px;\n}\n\n.dcclink-right a {\n  background-color: #00a3b9ff;\n  box-shadow: 0 2px 0 #00353dff;\n  color: white !important;\n  padding: 0.5em 0.5em;\n  position: relative;\n  text-decoration: none;\n  text-transform: none;\n  border-radius: 10px;\n  float:right;\n  margin: 0px 0px 0px 10px;\n}\n\n.dcclink a:visited {\n  color: whitesmoke !important;\n}\n\n.dcclink a:hover {\n  background-color: darkslategrey;\n  cursor: pointer;\n}\n\n.dcclink a:active {\n  box-shadow: none;\n  top: 5px;\n}\n\nhtml[data-theme='dark'] .rst-content .guilabel {\n  color: black;\n}\n\n.hr-dashed {\n  margin: -10px 0px -10px 0px; \n  border-top: 1px dashed #d2dfe3;\n}\n\n.hr-heavy {\n  margin: -10px 0px -10px 0px; \n  border-top: 5px solid #d2dfe3;\n}\n\nhtml[data-theme='dark'] .hr-dashed {\n  border-top: 1px dashed #114759;\n}\n\n/* *************************************** */\n\na.githublink, .githublink a {\n  background-color: #f7b656;\n  box-shadow: 0 2px 0 #00353dff;\n  color: white;\n  padding: 3px 5px 3px 5px;\n  position: relative;\n  font-size: 90% !important;\n  text-decoration: none;\n  text-transform: none;\n  border-radius: 5px;\n}\n\n.githublink-right a {\n  background-color: #f7b656;\n  box-shadow: 0 2px 0 #00353dff;\n  color: white;\n  padding: 3px 5px 3px 5px;\n  position: relative;\n  font-size: 90% !important;\n  text-decoration: none;\n  text-transform: none;\n  border-radius: 10px;\n  float:right;\n  margin: 0px 0px 0px 0px;\n}\n\n.githublink a:visited {\n  color: whitesmoke\n}\n\n.githublink a:hover {\n  background-color: rgb(172, 95, 7);\n  cursor: pointer;\n}\n\n.githublink a:active {\n  box-shadow: none;\n  top: 5px;\n}\n\n/* *************************************** */\n\nsvg {\n  max-width: 100%;\n  height: auto;\n}\n\n.responsive-image {\n  max-width: 100%;\n  height: auto;\n}\n\n/* *************************************** */\n\n.warning-float-right {\n  float: right;\n  width: 40%;\n}\n\n.warning-float-right-narrow {\n  float: right;\n  width: 20%;\n}\n\n.warning-float-right-wide {\n  float: right;\n  width: 60%;\n}\n\n.note-float-right {\n  float: right;\n  width: 40%;\n}\n\n.note-float-right-narrow {\n  float: right;\n  width: 20%;\n}\n\n.code-block-float-right {\n  float: right;\n  width: 40%;\n  margin: 0px 0px 0px 24px;\n}\n\n.note {\n  background: #f7fcff !important;\n  clear: none !important;\n}\nhtml[data-theme='dark'] .note {\n  background: #ffffff24 !important;\n}\n\n.note p.admonition-title {\n  background: #cbe1ef !important;\n}\nhtml[data-theme='dark'] .note p.admonition-title {\n  background: #256a97 !important;\n}\n\n.tip {\n  background: #eef5f4 !important;\n  clear: none !important;\n}\nhtml[data-theme='dark'] .tip {\n  background: #ffffff24 !important;\n  clear: none !important;\n}\n\n.tip p.admonition-title {\n  background: #9cd7cb !important;\n}\nhtml[data-theme='dark'] .tip p.admonition-title {\n  background: #256a97 !important;\n}\n\n.admonition-todo {\n  background: #f9f0e0 !important;\n  clear: none !important;\n}\nhtml[data-theme='dark'] .admonition-todo {\n  background: #ffffff24 !important;\n  clear: none !important;\n}\n\n.admonition-todo p.admonition-title {\n  background: #f7d1b0 !important;\n}\nhtml[data-theme='dark'] .admonition-todo p.admonition-title {\n  background: #6d3403 !important;\n}\n\n/* *************************************** */\n\n.menuselection {\n  font-style: italic;\n  font-weight: 700;\n}\n\n/* *************************************** */\n\n.wy-table-responsive {\n    margin-bottom: 12px !important;\n}\n\n/* override table width restrictions */ \n.table-wrap-text p, .table-grid-homepage p, .table-list-homepage p { \n  white-space: normal !important; \n  font-size: 110% !important;\n  line-height: 140% !important;\n}\n\n.table-wrap-text tr:nth-child(odd), .table-grid-homepage tr:nth-child(odd), .table-list-homepage tr:nth-child(odd) {\n  background-color: white !important;  \n  border-style: none !important;\n  border-width:0px !important;\n}\nhtml[data-theme='dark'] tr:nth-child(odd), .table-grid-homepage tr:nth-child(odd), .table-list-homepage tr:nth-child(odd) {\n  background-color: #ffffff08 !important;\n}\n\n.table-wrap-text tr:nth-child(even), .table-grid-homepage tr:nth-child(even), .table-list-homepage tr:nth-child(even) {\n  background-color: #ffffff00 !important;  \n  border-style: none !important;\n  border-width:0px !important;\n}\n\n.table-wrap-text td {\n  background-color: white !important;  \n  border-style: none !important;\n  border-width:0px !important;\n}\nhtml[data-theme='dark'] .table-wrap-text td {\n  background-color: ffffff08 !important;  \n}\n\n.table-grid-homepage td, .table-list-homepage td {\n  font-size: 80% !important;\n  color: #666666 !important;\n  vertical-align:top !important;\n  background-color: #ffffff00 !important;  \n  border-style: none !important;\n  border-width: 0px !important;\n}\n\n.table-wrap-text, .table-grid-homepage, .table-list-homepage { \n  margin-bottom: 24px; \n  max-width: 100% !important; \n  overflow: visible !important; \n  border-style: none !important;\n  border-width: 0px !important;\n} \n\n@media screen and (max-width: 900px) {\n  .table-grid-homepage {\n    display: none;\n  }\n  .table-list-homepage {\n    display: block;\n  }\n}\n\n@media not screen and (max-width: 900px) {\n  .table-grid-homepage {\n    display: block;\n  }\n  .table-list-homepage {\n    display: none;\n  }\n}\n\n\n.table-wrap-text th p, table-wrap-text-align-top th p { \n    margin-bottom: unset; \n} \n\n/* *************************************** */\n\n.image-min-width-144 {\n  min-width: 144px;\n  height: auto !important;\n}\n\n.image-min-width-72 {\n  min-width: 72px;\n  height: auto !important;\n}\n\n.image-float-right img {\n\tfloat:right;\n}\n\n.image-product-logo-float-right img {\n\tfloat:right;\n}\n\n@media screen and (max-width: 1000px) {\n  .image-product-logo-float-right img {\n    display: none;\n  }\n}\n\n/* *************************************** */\n/* Google search */\n\n.gsc-input-box {\n  border: 0px !important;\n}\n\n.gsib_a input {\n  padding: 5px !important;\n  background-color: #141414 !important;\n  color:white !important;\n}\n\n.gsc-search-button .gsc-search-button-v2 {\n  width: 40px !important;\n  height: 21px !important;\n  padding: 4px 4px !important;\n  background-color: #00a3b9ff !important;\n  border-color: #00a3b9ff !important;\n  border-radius: 5px;\n}\n\n/* .gsc-search-button .gsc-search-button-v2 {\n  width: 0px !important;\n  padding: 7px 7px !important;\n  border-color: #009300 !important;\n  background-color: #009300 !important;\n} */\n\n/* *************************************** */\n\n/* sidebar level 3 bullet points */\nnav#on-this-page ul.simple li ul li p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* sidebar level 3 bullet points */\nnav#on-this-page ul.simple li ul li {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* sidebar level 2 bullet points */\nnav#on-this-page ul.simple li p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* sidebar level 2 bullet points */\nnav#on-this-page ul.simple li {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\nnav#on-this-page ul.simple {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  margin-bottom: 0px !important;\n}\n\nnav#on-this-page p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  margin-top: 0px !important;\n  margin-bottom: 6px !important;\n}\n\nnav#on-this-page {\n  margin-bottom: 10px !important;\n}\n\n\n/* in-this-section level 3 bullet points */\nnav.in-this-section ul.simple li ul li p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* in-this-section level 3 bullet points */\nnav.in-this-section ul.simple li ul li {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* in-this-section level 2 bullet points */\nnav.in-this-section ul.simple li p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\n/* in-this-section level 2 bullet points */\nnav.in-this-section ul.simple li {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  line-height: 120% !important;\n  margin-bottom: 0px !important;\n}\n\nnav.in-this-section ul.simple {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  margin-bottom: 0px !important;\n}\n\nnav.in-this-section p {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-style: italic;\n  font-size: 90%;\n  margin-top: 0px !important;\n  margin-bottom: 6px !important;\n  margin-left: -30px;\n}\n\nnav.in-this-section {\n  margin-bottom: 20px !important;\n  margin-left: 30px;\n}\n\n\n/* sidebars */\n.rst-content .sidebar {\n  padding: 12px 24px 12px 24px !important;\n  border-radius: 10px;\n}\n\nhtml[data-theme='dark'] .rst-content .sidebar {\n  background: #000000ff !important;\n  border:#000000ff !important;\n}\n\n.sidebar-title {\n  border-radius: 10px;\n}\n\nhtml[data-theme='dark'] .sidebar-title {\n  background: #002735 !important;\n}\n\n\n\n\n/* news */\nsection#dcc-ex-model-railroading aside p.sidebar-title {\t\n  font-size: 110% !important;\n  font-family: Audiowide,Helvetica,Arial,sans-serif !important;\n  font-weight: 500 !important;  \n  color: #00a3b9ff;\n  text-shadow: 1px 1px 0 #00353dff;\n  margin: -24px -24px 12px !important;\n}\n\n/* news */\np.ablog-post-title {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 90% !important;\n  line-height: 130% !important;\n  margin-bottom: 0px !important;\n  font-weight: bold !important;\n}\n\np.ablog-post-excerpt {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 90% !important;\n  line-height: 130% !important;\n  margin-bottom: 0px !important;\n  margin-top: 6px !important;\n}\n\np.ablog-post-expand {\n  font-family: Roboto,Helvetica,Arial,sans-serif !important;\n  font-size: 80% !important;\n  line-height: 130% !important;\n  margin-bottom: 10px !important;\n  margin-top: 0px !important;\n  margin-left: 20px;\n}\n\nli.ablog-post {\n  list-style-type: none !important;\n  margin: 0px !important;\n}\n\nimg.sd-card-img-top {\n  max-width: 30% !important;\n  display: block !important;\n  margin-left: auto !important;\n  margin-right: auto !important;\n  margin-top: 10px;\n  margin-bottom: -5px !important;\n}\n\n.sd-card-header {\n  margin-bottom: -10px !important;\n  margin-top: 10px !important;\n  padding-top: 0px !important;\n  padding-bottom: 0px !important;\n}\n\n.sd-card-header p {\n  line-height: 18px !important;\n}\n\nhtml[data-theme='dark'] .sd-card-header {\n  border-bottom: 1px solid rgb(255 253 253 / 13%);\n}\n\n.sd-card-body ul li p {\n  margin-bottom: 5px !important;\n}\n\n.sd-card-text {\n  margin: 0 0 12px !important;\n}\n\n/* code */\n.rst-content code {\n  font-size: 100% !important;\n}\n.rst-content code.literal, .rst-content tt.literal {\n  color: #ba2121 !important;\n  font-size: 100% important;\n}\nhtml[data-theme='dark'] .rst-content code.literal, .rst-content tt.literal {\n  color: #ff6000 !important;\n}\n\n/* general purpose */\n\n.dcc-ex-red {\n  color:red;\n}\n\n.dcc-ex-red-bold {\n  color:red;\n  font-weight: bold !important;\n}\n\n.dcc-ex-red-bold-italic {\n  color:red;\n  font-weight: bold !important;\n  font-style: italic !important;\n}\n\n.dcc-ex-code {\n  color:#ba2121;\n  font-weight: bold !important;\n}\n\n.dcc-ex-text-size-200pct {\n  font-size: 200% !important;\n  line-height: 110% !important;\n}\n\n.dcc-ex-text-size-80pct {\n  font-size: 80% !important;\n}\n\n.dcc-ex-text-size-60pct {\n  font-size: 80% !important;\n}\n\n.new-in-v5 {\n  font-family: Audiowide,Helvetica,Arial,sans-serif;  \n  font-weight: bold; \n  font-style: italic; \n  color: #00a3b9; \n  font-size: 110%; \n}\n\nhtml[data-theme='dark'] .new-in-v5 {\n  font-weight: normal; \n  color: #ffffff; \n  text-shadow: 0px 0px 10px #00a3b9;\n}\n\n/* *************************************** */\n\n@media not screen and (max-width: 900px) {\n  div.rst-footer-buttons {\n    position: fixed;\n    bottom:5px;\n    width:350px;\n    background: #c9c9c999;\n    padding: 10px; \n    border-radius: 10px;\n    border-color: white !important;\n    border: 4px solid;\n    transform: translateX(50%);\n  }\n  html[data-theme='dark'] div.rst-footer-buttons {\n    border-color: #141414 !important;\n    background: #c9c9c92e;\n  }\n  footer {\n    padding-bottom: 40px;\n    font-size: 80% !important;\n  }\n}\n\n@media screen and (max-width: 900px) {\n  div.rst-footer-buttons {\n    display:block;\n    font-size: 80% !important;\n  }\n}\n\nhtml[data-theme='dark'] .rst-content span.descname {\n  color: #dbdd7c !important;\n}\n"
  },
  {
    "path": "docs/_static/css/sphinx_design_overrides.css",
    "content": "/* Override for the sphinx-design extension classes */\n.sd-card-header {\t\n  font-size: 110% !important;\n  font-family: Audiowide,Helvetica,Arial,sans-serif !important;\n  font-weight: 500 !important;  \n  color: #00a3b9ff;\n  text-shadow: 1px 1px 0 #00353dff;\n  margin-bottom: .5rem !important;\n}"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\nimport os\nimport subprocess\n\n# Doxygen\nsubprocess.call('doxygen DoxyfileEXRAIL', shell=True)\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = 'EXRAIL Language'\ncopyright = '2025 - Peter Cole'\nauthor = 'Peter Cole'\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n  'sphinx_sitemap',\n  'sphinxcontrib.spelling',\n  'sphinx_rtd_dark_mode',\n  'breathe'\n]\n\nautosectionlabel_prefix_document = True\n\n# Don't make dark mode the user default\ndefault_dark_mode = False\n\nspelling_lang = 'en_UK'\ntokenizer_lang = 'en_UK'\nspelling_word_list_filename = ['spelling_wordlist.txt']\n\ntemplates_path = ['_templates']\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\nhighlight_language = 'c++'\n\nnumfig = True\n\nnumfig_format = {'figure': 'Figure %s'}\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\nhtml_theme = 'sphinx_rtd_theme'\nhtml_static_path = ['_static']\n\nhtml_logo = \"./_static/images/product-logo-ex-rail.png\"\n\nhtml_favicon = \"./_static/images/favicon.ico\"\n\nhtml_theme_options = {\n    'style_nav_header_background': 'white',\n    'logo_only': True,\n    # Toc options\n    'includehidden': True,\n    'titles_only': False,\n    # 'titles_only': True,\n    'collapse_navigation': False,\n    # 'navigation_depth': 3,\n    'navigation_depth': 1,\n    'analytics_id': 'G-L5X0KNBF0W',\n}\n\nhtml_context = {\n    'display_github': True,\n    'github_user': 'DCC-EX',\n    'github_repo': 'CommandStation-EX',\n    'github_version': 'sphinx/docs/',\n}\n\nhtml_css_files = [\n    'css/dccex_theme.css',\n    'css/sphinx_design_overrides.css',\n]\n\nhtml_baseurl = 'https://dcc-ex.com/CommandStation-EX/'\n\n# Sphinx sitemap\nhtml_extra_path = [\n  'robots.txt',\n]\n\n# -- Breathe configuration -------------------------------------------------\n\nbreathe_projects = {\n  \"EXRAIL Language\": \"_build/xml/\"\n}\nbreathe_default_project = \"EXRAIL Language\"\nbreathe_default_members = ()\n"
  },
  {
    "path": "docs/index.rst",
    "content": "EXRAIL Language documentation\n=============================\n\nIntroduction\n------------\n\nEXRAIL - Extended Railroad Automation Instruction Language\n\nThis page is a reference to all EXRAIL commands available with EX-CommandStation.\n\nMacros\n------\n\n.. doxygenfile:: EXRAIL2MacroReset.h\n   :project: EXRAIL Language\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=.\nset BUILDDIR=_build\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.https://www.sphinx-doc.org/\n\texit /b 1\n)\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "alabaster==1.0.0\nattrs==25.1.0\nbabel==2.17.0\nbreathe==4.35.0\ncattrs==24.1.2\ncertifi==2025.1.31\ncharset-normalizer==3.4.1\ncolorama==0.4.6\ndocutils==0.21.2\nesbonio==0.16.5\nexceptiongroup==1.2.2\nidna==3.10\nimagesize==1.4.1\nJinja2==3.1.5\nlsprotocol==2023.0.1\nMarkupSafe==3.0.2\npackaging==24.2\nplatformdirs==4.3.6\npyenchant==3.2.2\npygls==1.3.1\nPygments==2.19.1\npyspellchecker==0.8.2\nrequests==2.32.3\nsnowballstemmer==2.2.0\nSphinx==8.1.3\nsphinx-rtd-dark-mode==1.3.0\nsphinx-rtd-theme==3.0.2\nsphinx-sitemap==2.6.0\nsphinxcontrib-applehelp==2.0.0\nsphinxcontrib-devhelp==2.0.0\nsphinxcontrib-htmlhelp==2.1.0\nsphinxcontrib-jquery==4.1\nsphinxcontrib-jsmath==1.0.1\nsphinxcontrib-qthelp==2.0.0\nsphinxcontrib-serializinghtml==2.0.0\nsphinxcontrib-spelling==8.0.1\ntomli==2.2.1\ntyping_extensions==4.12.2\nurllib3==2.3.0\n"
  },
  {
    "path": "docs/robots.txt",
    "content": "User-agent: *\n\nSitemap: https://dcc-ex.com/CommandStation-EX/sitemap.xml\n"
  },
  {
    "path": "install_via_powershell.cmd",
    "content": "@ECHO OFF\n\nFOR /f \"tokens=*\" %%a IN ('powershell Get-ExecutionPolicy -Scope CurrentUser') DO SET PS_POLICY=%%a\n\nIF NOT %PS_POLICY==\"Bypass\" (\n  powershell Set-ExecutionPolicy -Scope CurrentUser Bypass\n)\n\npowershell %~dp0%installer.ps1\n\nIF NOT %PS_POLICY==\"Bypass\" (\n  powershell Set-ExecutionPolicy -Scope CurrentUser %PS_POLICY%\n)\n"
  },
  {
    "path": "installer.json",
    "content": "[\n    {\n        \"Name\": \"BaseStationClassic\",\n        \"Git\": \"DCC-EX/BaseStation-Classic\",\n        \"Libraries\": [\n            {\n                \"Name\": \"Ethernet\",\n                \"Repo\": \"arduino-libraries/Ethernet\",\n                \"Location\": \"libraries/Ethernet\",\n                \"LibraryDownloadAvailable\": true\n            }\n        ],\n        \"SupportedBoards\": [\n            {\n                \"Name\": \"Uno\",\n                \"FQBN\": \"arduino:avr:uno\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Mega\",\n                \"FQBN\": \"arduino:avr:mega\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            }\n        ],\n        \"DisplayName\": \"Base Station Classic\",\n        \"InputFileLocation\": \"DCCpp\",\n        \"AllowAdvanced\": false,\n        \"ConfigFile\": \"DCCpp/Config.h\"\n    },\n    {\n        \"Name\": \"CommandStation-EX\",\n        \"Git\": \"DCC-EX/CommandStation-EX\",\n        \"Libraries\": [\n            {\n                \"Name\": \"Ethernet\",\n                \"Repo\": \"arduino-libraries/Ethernet\",\n                \"Location\": \"libraries/Ethernet\",\n                \"LibraryDownloadAvailable\": true\n            },\n            {\n                \"Name\": \"DIO2\",\n                \"Repo\": \"\",\n                \"Location\": \"libraries/DIO2\",\n                \"LibraryDownloadAvailable\": true\n            }\n        ],\n        \"SupportedBoards\": [\n            {\n                \"Name\": \"Uno\",\n                \"FQBN\": \"arduino:avr:uno\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Mega\",\n                \"FQBN\": \"arduino:avr:mega\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Mega Wifi\",\n                \"FQBN\": \"arduino:avr:mega\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Nano\",\n                \"FQBN\": \"arduino:avr:nano\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Nano Every\",\n                \"FQBN\": \"arduino:megaavr:nanoevery\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"megaavr\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Teensy 3.x\",\n                \"FQBN\": \"teensy:avr:teensy31\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"teensy\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"Teensy 4.x\",\n                \"FQBN\": \"teensy:avr:teensy41\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"teensy\",\n                        \"Package\": \"arduino\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            },\n            {\n                \"Name\": \"SAMD21\",\n                \"FQBN\": \"SparkFun:samd\",\n                \"Platforms\": [\n                    {\n                        \"Architecture\": \"avr\",\n                        \"Package\": \"arduino\"\n                    },\n                    {\n                        \"Architecture\": \"samd\",\n                        \"Package\": \"arduino\"\n                    },\n                    {\n                        \"Architecture\": \"samd\",\n                        \"Package\": \"SparkFun\"\n                    }\n                ],\n                \"SupportedMotoShields\": [\n                    {\n                        \"Name\": \"Arduino Motor Shield\",\n                        \"Declaration\": \"STANDARD_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"Pololu MC33926 Motor Shield\",\n                        \"Declaration\": \"POLOLU_MOTOR_SHIELD\"\n                    },\n                    {\n                        \"Name\": \"FireBox MK1\",\n                        \"Declaration\": \"FIREBOX_MK1\"\n                    },\n                    {\n                        \"Name\": \"FireBox MK1S\",\n                        \"Declaration\": \"FIREBOX_MK1S\"\n                    }\n                ],\n                \"ExtraLibraries\": []\n            }\n        ],\n        \"DisplayName\": \"CommandStation EX\",\n        \"InputFileLocation\": \"\",\n        \"AllowAdvanced\": true,\n        \"ConfigFile\": \"config.h\"\n    }\n]\n"
  },
  {
    "path": "installer.ps1",
    "content": "<#\n# © 2023 Peter Cole\n# \n# This file is part of EX-CommandStation\n#\n# This is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# It is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n#>\n\n<############################################\nFor script errors set ExecutionPolicy:\nSet-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Bypass\n############################################>\n\n<############################################\nOptional command line parameters:\n  $buildDirectory - specify an existing directory rather than generating a new unique one\n  $configDirectory - specify a directory containing existing files as per $configFiles\n############################################>\nParam(\n  [Parameter()]\n  [String]$buildDirectory,\n  [Parameter()]\n  [String]$configDirectory\n)\n\n<############################################\nDefine global parameters here such as known URLs etc.\n############################################>\n$installerVersion = \"v0.0.8\"\n$configFiles = @(\"config.h\", \"myAutomation.h\", \"myHal.cpp\", \"mySetup.h\")\n$wifiBoards = @(\"arduino:avr:mega\", \"esp32:esp32:esp32\")\n$userDirectory = $env:USERPROFILE + \"\\\"\n$gitHubAPITags = \"https://api.github.com/repos/DCC-EX/CommandStation-EX/git/refs/tags\"\n$gitHubURLPrefix = \"https://github.com/DCC-EX/CommandStation-EX/archive/\"\nif ((Get-WmiObject win32_operatingsystem | Select-Object osarchitecture).osarchitecture -eq \"64-bit\") {\n  $arduinoCLIURL = \"https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Windows_64bit.zip\"\n  $arduinoCLIZip = $userDirectory + \"Downloads\\\" + \"arduino-cli_latest_Windows_64bit.zip\"\n} else {\n  $arduinoCLIURL = \"https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Windows_32bit.zip\"\n  $arduinoCLIZip = $userDirectory + \"Downloads\\\" + \"arduino-cli_latest_Windows_32bit.zip\"\n}\n$arduinoCLIDirectory = $userDirectory + \"arduino-cli\"\n$arduinoCLI = $arduinoCLIDirectory + \"\\arduino-cli.exe\"\n\n<############################################\nList of supported devices with FQBN in case clones used that aren't detected\n############################################>\n$supportedDevices = @(\n  @{\n    name = \"Arduino Mega or Mega 2560\"\n    fqbn = \"arduino:avr:mega\"\n  },\n  @{\n    name = \"Arduino Nano\"\n    fqbn = \"arduino:avr:nano\"\n  },\n  @{\n    name = \"Arduino Uno\"\n    fqbn = \"arduino:avr:uno\"\n  },\n  @{\n    name = \"ESP32 Dev Module\"\n    fqbn = \"esp32:esp32:esp32\"\n  }\n)\n\n<############################################\nList of supported displays\n############################################>\n$displayList = @(\n  @{\n    option = \"LCD 16 columns x 2 rows\"\n    configLine = \"#define LCD_DRIVER  0x27,16,2\"\n  },\n  @{\n    option = \"LCD 16 columns x 4 rows\"\n    configLine = \"#define LCD_DRIVER  0x27,16,4\"\n  },\n  @{\n    option = \"OLED 128 x 32\"\n    configLine = \"#define OLED_DRIVER 128,32\"\n  },\n  @{\n    option = \"OLED 128 x 64\"\n    configLine = \"#define OLED_DRIVER 128,64\"\n  }\n)\n\n<############################################\nBasics of config.h\n############################################>\n$configLines = @(\n  \"/*\",\n  \"This config.h file was generated by the DCC-EX PowerShell installer $installerVersion\",\n  \"*/\",\n  \"\",\n  \"// Define standard motor shield\",\n  \"#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD\",\n  \"\"\n)\n\n<############################################\nSet default action for progress indicators, warnings, and errors\n############################################>\n$global:ProgressPreference = \"SilentlyContinue\"\n$global:WarningPreference = \"SilentlyContinue\"\n$global:ErrorActionPreference = \"SilentlyContinue\"\n\n<############################################\nIf $buildDirectory not provided, generate a new time/date stamp based directory to use\n############################################>\nif (!$PSBoundParameters.ContainsKey('buildDirectory')) {\n  $buildDate = Get-Date -Format 'yyyyMMdd-HHmmss'\n  $buildDirectory = $userDirectory + \"EX-CommandStation-Installer\\\" + $buildDate\n}\n$commandStationDirectory = $buildDirectory + \"\\CommandStation-EX\"\n\n<############################################\nWrite out intro message and prompt to continue\n############################################>\n@\"\nWelcome to the DCC-EX PowerShell installer for EX-CommandStation ($installerVersion)\n\nCurrent installer options:\n\n- EX-CommandStation will be built in $commandStationDirectory\n- Arduino CLI will downloaded and extracted to $arduinoCLIDirectory\n\nBefore continuing, please ensure:\n\n- Your computer is connected to the internet\n- The device you wish to install EX-CommandStation on is connected to a USB port\n\nThis installer will obtain the Arduino CLI (if not already present), and then download and install your chosen version of EX-CommandStation\n\n\"@\n\n<############################################\nPrompt user to confirm all is ready to proceed\n############################################>\n$confirmation = Read-Host \"Enter 'Y' or 'y' then press <Enter> to confirm you are ready to proceed, any other key to exit\"\nif ($confirmation -ne \"Y\" -and $confirmation -ne \"y\") {\n  Exit\n}\n\n<############################################\nSee if we have the Arduino CLI already, otherwise download and extract it\n############################################>\nif (!(Test-Path -PathType Leaf -Path $arduinoCLI)) {\n  if (!(Test-Path -PathType Container -Path $arduinoCLIDirectory)) {\n    try {\n      New-Item -ItemType Directory -Path $arduinoCLIDirectory | Out-Null\n    }\n    catch {\n      Write-Output \"Arduino CLI does not exist and cannot create directory $arduinoCLIDirectory\"\n      Exit\n    }\n  }\n  Write-Output \"`r`nDownloading and extracting Arduino CLI\"\n  try {\n    Invoke-WebRequest -Uri $arduinoCLIURL -OutFile $arduinoCLIZip\n  }\n  catch {\n    Write-Output \"Failed to download Arduino CLI\"\n    Exit\n  }\n  try {\n    Expand-Archive -Path $arduinoCLIZip -DestinationPath $arduinoCLIDirectory -Force\n  }\n  catch {\n    Write-Output \"Failed to extract Arduino CLI\"\n  }\n} else {\n  Write-Output \"`r`nArduino CLI already downloaded, ensuring it is up to date and you have a board connected\"\n}\n\n<############################################\nMake sure Arduino CLI core index updated and list of boards populated\n############################################>\n# Need to do an initial board list to download everything first\ntry {\n  & $arduinoCLI core update-index | Out-Null\n}\ncatch {\n  Write-Output \"Failed to update Arduino CLI core index\"\n  Exit\n}\n# Need to do an initial board list to download everything first\ntry {\n  & $arduinoCLI board list | Out-Null\n}\ncatch {\n  Write-Output \"Failed to update Arduino CLI board list\"\n  Exit\n}\n\n<############################################\nIdentify available board(s)\n############################################>\ntry {\n  $boardList = & $arduinoCLI board list --format jsonmini | ConvertFrom-Json\n}\ncatch {\n  Write-Output \"Failed to obtain list of boards\"\n  Exit\n}\n\n<############################################\nGet user to select board\n############################################>\nif ($boardList.count -eq 0) {\n  Write-Output \"Could not find any attached devices, please ensure your device is plugged in to a USB port and Windows recognises it\"\n  Exit\n} else {\n@\"\n\nDevices attached to COM ports:\n------------------------------\n\"@\n\n  $boardSelect = 1\n  foreach ($board in $boardList) {\n    if ($board.matching_boards.name) {\n      $boardName = $board.matching_boards.name\n    } else {\n      $boardName = \"Unknown device\"\n    }\n    $port = $board.port.address\n    Write-Output \"$boardSelect - $boardName on port $port\"\n    $boardSelect++\n  }\n  Write-Output \"$boardSelect - Exit\"\n  $userSelection = 0\n  do {\n    [int]$userSelection = Read-Host \"`r`nSelect the device to use from the list above\"\n  } until (\n    (($userSelection -ge 1) -and ($userSelection -le ($boardList.count + 1)))\n  )\n  if ($userSelection -eq ($boardList.count + 1)) {\n    Write-Output \"Exiting installer\"\n    Exit\n  } else {\n    $selectedBoard = $userSelection - 1\n  }\n}\n\n<############################################\nIf the board is unknown, need to choose which one\n############################################>\nif ($null -eq $boardList[$selectedBoard].matching_boards.name) {\n  Write-Output \"The device selected is unknown, these boards are supported:`r`n\"\n  $deviceSelect = 1\n  foreach ($device in $supportedDevices) {\n    Write-Output \"$deviceSelect - $($supportedDevices[$deviceSelect - 1].name)\"\n    $deviceSelect++\n  }\n  Write-Output \"$deviceSelect - Exit\"\n  $userSelection = 0\n  do {\n    [int]$userSelection = Read-Host \"Select the board type from the list above\"\n  } until (\n    (($userSelection -ge 1) -and ($userSelection -le ($supportedDevices.count + 1)))\n  )\n  if ($userSelection -eq ($supportedDevices.count + 1)) {\n    Write-Output \"Exiting installer\"\n    Exit\n  } else {\n    $deviceName = $supportedDevices[$userSelection - 1].name\n    $deviceFQBN = $supportedDevices[$userSelection - 1].fqbn\n    $devicePort = $boardList[$selectedBoard].port.address\n  }\n} else {\n  $deviceName = $boardList[$selectedBoard].matching_boards.name\n  $deviceFQBN = $boardList[$selectedBoard].matching_boards.fqbn\n  $devicePort = $boardList[$selectedBoard].port.address\n}\n\n<############################################\nGet the list of tags\n############################################>\ntry {\n  $gitHubTags = Invoke-RestMethod -Uri $gitHubAPITags\n}\ncatch {\n  Write-Output \"Failed to obtain list of available EX-CommandStation versions\"\n  Exit\n}\n\n<############################################\nGet our GitHub tag list in a hash so we can sort by version numbers and extract just the ones we want\n############################################>\n$versionMatch = \".*?v(\\d+)\\.(\\d+).(\\d+)-(.*)\"\n$tagList = @{}\nforeach ($tag in $gitHubTags) {\n  $tagHash = @{}\n  $tagHash[\"Ref\"] = $tag.ref\n  $version = $tag.ref.split(\"/\")[2]\n  $null = $version -match $versionMatch\n  $tagHash[\"Major\"] = [int]$Matches[1]\n  $tagHash[\"Minor\"] = [int]$Matches[2]\n  $tagHash[\"Patch\"] = [int]$Matches[3]\n  $tagHash[\"Type\"] = $Matches[4]\n  $tagList.Add($version, $tagHash)\n}\n\n<############################################\nGet latest two Prod and Devel for user to select\n############################################>\n$userList = @{}\n$prodCount = 1\n$devCount = 1\n$select = 1\nforeach ($tag in $tagList.Keys | Sort-Object {$tagList[$_][\"Major\"]},{$tagList[$_][\"Minor\"]},{$tagList[$_][\"Patch\"]} -Descending) {\n  if (($tagList[$tag][\"Type\"] -eq \"Prod\") -and $prodCount -le 2) {\n    $userList[$select] = $tag\n    $select++\n    $prodCount++\n  } elseif (($tagList[$tag][\"Type\"] -eq \"Devel\") -and $devCount -le 2) {\n    $userList[$select] = $tag\n    $select++\n    $devCount++\n  }\n}\n\n<############################################\nDisplay options for user to select and get the selection\n############################################>\n@\"\n\nAvailable EX-CommandStation versions:\n-------------------------------------\n\"@\nforeach ($selection in $userList.Keys | Sort-Object $selection) {\n  Write-Output \"$selection - $($userList[$selection])\"\n}\nWrite-Output \"5 - Exit\"\n$userSelection = 0\ndo {\n  [int]$userSelection = Read-Host \"`r`nSelect the version to install from the list above (1 - 5)\"\n} until (\n  (($userSelection -ge 1) -and ($userSelection -le 5))\n)\nif ($userSelection -eq 5) {\n  Write-Output \"Exiting installer\"\n  Exit\n} else {\n  $downloadURL = $gitHubURLPrefix + $tagList[$userList[$userSelection]][\"Ref\"] + \".zip\"\n}\n\n<############################################\nCreate build directory if it doesn't exist, or fail\n############################################>\nif (!(Test-Path -PathType Container -Path $buildDirectory)) {\n  try {\n    New-Item -ItemType Directory -Path $buildDirectory | Out-Null\n  }\n  catch {\n    Write-Output \"Could not create build directory $buildDirectory\"\n    Exit\n  }\n}\n\n<############################################\nDownload the chosen version to the build directory\n############################################>\n$downladFile = $buildDirectory + \"\\CommandStation-EX.zip\"\nWrite-Output \"Downloading and extracting $($userList[$userSelection])\"\ntry {\n  Invoke-WebRequest -Uri $downloadURL -OutFile $downladFile\n}\ncatch {\n  Write-Output \"Error downloading EX-CommandStation zip file\"\n  Exit\n}\n\n<############################################\nIf folder exists, bail out and tell user\n############################################>\nif (Test-Path -PathType Container -Path \"$buildDirectory\\CommandStation-EX\") {\n  Write-Output \"EX-CommandStation directory already exists, please ensure you have copied any user files then delete manually: $buildDirectory\\CommandStation-EX\"\n  Exit\n}\n\n<############################################\nExtract and rename to CommandStation-EX to allow building\n############################################>\ntry {\n  Expand-Archive -Path $downladFile -DestinationPath $buildDirectory -Force\n}\ncatch {\n  Write-Output \"Failed to extract EX-CommandStation zip file\"\n  Exit\n}\n\n$folderName = $buildDirectory + \"\\CommandStation-EX-\" + ($userList[$userSelection] -replace \"^v\", \"\")\ntry {\n  Rename-Item -Path $folderName -NewName $commandStationDirectory\n}\ncatch {\n  Write-Output \"Could not rename folder\"\n  Exit\n}\n\n<############################################\nIf config directory provided, copy files here\n############################################>\nif ($PSBoundParameters.ContainsKey('configDirectory')) {\n  if (Test-Path -PathType Container -Path $configDirectory) {\n    foreach ($file in $configFiles) {\n      if (Test-Path -PathType Leaf -Path \"$configDirectory\\$file\") {\n        Copy-Item -Path \"$configDirectory\\$file\" -Destination \"$commandStationDirectory\\$file\"\n      }\n    }\n  } else {\n    Write-Output \"User provided configuration directory $configDirectory does not exist, skipping\"\n  }\n} else {\n\n<############################################\nIf no config directory provided, prompt for display option\n############################################>\n  Write-Output \"`r`nIf you have an LCD or OLED display connected, you can configure it here`r`n\"\n  Write-Output \"1 - I have no display, skip this step\"\n  $displaySelect = 2\n  foreach ($display in $displayList) {\n    Write-Output \"$displaySelect - $($displayList[$displaySelect - 2].option)\"\n    $displaySelect++\n  }\n  Write-Output \"$($displayList.Count + 2) - Exit\"\n  do {\n    [int]$displayChoice = Read-Host \"`r`nSelect a display option\"\n  } until (\n    ($displayChoice -ge 1 -and $displayChoice -le ($displayList.Count + 2))\n  )\n  if ($displayChoice -eq ($displayList.Count + 2)) {\n    Exit\n  } elseif ($displayChoice -ge 2) {\n    $configLines+= \"// Display configuration\"\n    $configLines+= \"$($displayList[$displayChoice - 2].configLine)\"\n    $configLines+= \"#define SCROLLMODE 1 // Alternate between pages\"\n  }\n<############################################\nIf device supports WiFi, prompt to configure\n############################################>\n  if ($wifiBoards.Contains($deviceFQBN)) {\n    Write-Output \"`r`nYour chosen board supports WiFi`r`n\"\n    Write-Output \"1 - I don't want WiFi, skip this step\n2 - Configure my device as an access point I will connect to directly\n3 - Configure my device to connect to my home WiFi network\n4 - Exit\"\n    do {\n      [int]$wifiChoice = Read-Host \"`r`nSelect a WiFi option\"\n    } until (\n      ($wifiChoice -ge 1 -and $wifiChoice -le 4)\n    )\n    if ($wifiChoice -eq 4) {\n      Exit\n    } elseif ($wifiChoice -ne 1) {\n      $configLines+= \"\"\n      $configLines+= \"// WiFi configuration\"\n      $configLines+= \"#define ENABLE_WIFI true\"\n      $configLines+= \"#define IP_PORT 2560\"\n      $configLines+= \"#define WIFI_HOSTNAME \"\"dccex\"\"\"\n      $configLines+= \"#define WIFI_CHANNEL 1\"\n      if ($wifiChoice -eq 2) {\n        $configLines+= \"#define WIFI_SSID \"\"Your network name\"\"\"\n        $configLines+= \"#define WIFI_PASSWORD \"\"Your network passwd\"\"\"\n      }\n      if ($wifiChoice -eq 3) {\n        $wifiSSID = Read-Host \"Please enter the SSID of your home network here\"\n        $wifiPassword = Read-Host \"Please enter your home network WiFi password here\"\n        $configLines+= \"#define WIFI_SSID \"\"$($wifiSSID)\"\"\"\n        $configLines+= \"#define WIFI_PASSWORD \"\"$($wifiPassword)\"\"\"\n      }\n    }\n  }\n\n<############################################\nWrite out config.h to a file here only if config directory not provided\n############################################>\n  $configH = $commandStationDirectory + \"\\config.h\"\n  try {\n    $configLines | Out-File -FilePath $configH -Encoding ascii\n  }\n  catch {\n    Write-Output \"Error writing config file to $configH\"\n    Exit\n  }\n}\n\n<############################################\nInstall core libraries for the platform\n############################################>\n$platformArray = $deviceFQBN.split(\":\")\n$platform = $platformArray[0] + \":\" + $platformArray[1]\ntry {\n  & $arduinoCLI core install $platform\n}\ncatch {\n  Write-Output \"Error install core libraries\"\n  Exit\n}\n\n<############################################\nUpload the sketch to the selected board\n############################################>\n#$arduinoCLI upload -b fqbn -p port $commandStationDirectory\nWrite-Output \"Compiling and uploading to $deviceName on $devicePort\"\ntry {\n  $output = & $arduinoCLI compile -b $deviceFQBN -u -t -p $devicePort $commandStationDirectory --format jsonmini | ConvertFrom-Json\n}\ncatch {\n  Write-Output \"Failed to compile\"\n  Exit\n}\nif ($output.success -eq \"True\") {\n  Write-Output \"`r`nCongratulations! DCC-EX EX-CommandStation $($userList[$userSelection]) has been installed on your $deviceName`r`n\"\n} else {\n  Write-Output \"`r`nThere was an error installing $($userList[$userSelection]) on your $($deviceName), please take note of the errors provided:`r`n\"\n  if ($null -ne $output.compiler_err) {\n    Write-Output \"Compiler error: $($output.compiler_err)`r`n\"\n  }\n  if ($null -ne $output.builder_result) {\n    Write-Output \"Builder result: $($output.builder_result)`r`n\"\n  }\n}\n\nWrite-Output \"`r`nPress any key to exit the installer\"\n[void][System.Console]::ReadKey($true)\n"
  },
  {
    "path": "installer.sh",
    "content": "#!/bin/bash\n\n#\n# © 2022,2023 Harald Barth\n# \n# This file is part of CommandStation-EX\n#\n# This is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# It is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with CommandStation.  If not, see <https://www.gnu.org/licenses/>.\n#\n#\n# Usage: mkdir DIRNAME ; cd DIRNAME ; ../installer.sh\n# or from install directory ./installer.sh\n#\n\nDCCEXGITURL=\"https://github.com/DCC-EX/CommandStation-EX\"\nACLIINSTALL=\"https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh\"\nACLI=\"./bin/arduino-cli\"\n\nfunction need () {\n    type -p $1 > /dev/null && return\n    dpkg -l $1 2>&1 | egrep ^ii >/dev/null && return\n    sudo apt-get install $1\n    type -p $1 > /dev/null && return\n    echo \"Could not install $1, abort\"\n    exit 255\n}\n\nneed git\n\nif cat /etc/issue | egrep '^Raspbian' 2>&1 >/dev/null ; then\n    # we are on a raspi where we do not support graphical\n    unset DISPLAY\nfi\n\nif [ x$DISPLAY != x ] ; then\n    # we have DISPLAY, do the graphic thing\n    need python3-tk\n    need python3.8-venv\n    mkdir -p ~/ex-installer/venv\n    python3 -m venv ~/ex-installer/venv\n    cd ~/ex-installer/venv || exit 255\n    source ./bin/activate\n    git clone https://github.com/DCC-EX/EX-Installer\n    cd EX-Installer || exit 255\n    pip3 install -r requirements.txt\n    exec python3 -m ex_installer\nfi\nif test -d `basename \"$DCCEXGITURL\"` ; then\n    : assume we are almost there\n    cd `basename \"$DCCEXGITURL\"` || exit 255\nfi\nif test -d .git ; then\n    : assume we are right here\n    git pull\nelse\n    git clone \"$DCCEXGITURL\"\n    cd `basename \"$DCCEXGITURL\"` || exit 255\nfi\n\n# prepare versions\nVERSIONS=/tmp/versions.$$\ngit tag --sort=v:refname | grep Prod  | tail -1  >  $VERSIONS\necho master                                      >> $VERSIONS\ngit tag --sort=v:refname | grep Devel | tail -1  >> $VERSIONS\necho devel                                       >> $VERSIONS\n\n# ask user what version to use\necho \"What version to use? (give line number) If in doubt, use 1\"\ncat -n $VERSIONS\necho -n \"> \"\nLINE=`awk 'BEGIN {getline A < \"/dev/tty\"} ; A == NR {print}' $VERSIONS`\ngit checkout $LINE\n\nif test -f config.h ; then\n    : all well\nelse\n    # need to do this config better\n    cp -p config.example.h config.h\nfi\nif test -x \"$ACLI\" ; then\n    : all well\nelse\n    need curl\n    curl \"$ACLIINSTALL\" > acliinstall.sh\n    chmod +x acliinstall.sh\n    ./acliinstall.sh\nfi\n\n$ACLI core update-index || exit 255\n\n# Board discovery\nBOARDS=/tmp/boards.$$\n$ACLI board list > /dev/null               # download missing components\n$ACLI board list | grep serial > $BOARDS   # real run\nif test -s $BOARDS ; then\n    : all well\nelse\n    echo \"$ACLI: No boards found\"\n    exit 255\nfi\nif test x`< $BOARDS wc -l` = 'x1' ; then\n    LINE=`cat $BOARDS`\nelse\n    # ask user\n    echo \"What board to use? (give line number)\"\n    cat -n $BOARDS\n    echo -n \"> \"\n    LINE=`awk 'BEGIN {getline A < \"/dev/tty\"} ; A == NR {print}' $BOARDS`\nfi\nrm $BOARDS\nPORT=`echo $LINE | cut -d\" \" -f1`\necho Will use port: $PORT\n\n# FQBN discovery\nFQBN=`echo $LINE | egrep 'arduino:avr:[a-z][a-z]*' | sed 's/.*\\(arduino:avr:[a-z][a-z]*\\) .*/\\1/1'`\nif test x$FQBN = x ; then\n    # ask user\n    cat > /tmp/fqbn.$$ <<EOF\narduino:avr:uno\narduino:avr:mega\nesp32:esp32:esp32\nEOF\n    echo \"What board type? (give line number)\"\n    cat -n /tmp/fqbn.$$\n    echo -n \"> \"\n    FQBN=`awk 'BEGIN {getline A < \"/dev/tty\"} ; A == NR {print}' /tmp/fqbn.$$`\nfi\nrm /tmp/fqbn.$$\necho FQBN is $FQBN\n\n# Install phase\n$ACLI core install `echo $FQBN | sed 's,:[^:]*$,,1'` # remove last component to get package\n$ACLI board attach -p $PORT --fqbn $FQBN \"$PWD\"\n$ACLI compile --fqbn $FQBN \"$PWD\"\n$ACLI upload -v -t -p $PORT \"$PWD\"\n"
  },
  {
    "path": "libsha1.cpp",
    "content": "// For DCC-EX: This file downloaded from:\n//  https://github.com/Links2004/arduinoWebSockets\n// All due credit to Steve Reid\n\n/* from valgrind tests */\n\n/* ================ sha1.c ================ */\n/*\nSHA-1 in C\nBy Steve Reid <steve@edmweb.com>\n100% Public Domain\n\nTest Vectors (from FIPS PUB 180-1)\n\"abc\"\n  A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D\n\"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq\"\n  84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1\nA million repetitions of \"a\"\n  34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F\n*/\n\n/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */\n/* #define SHA1HANDSOFF * Copies data before messing with it. */\n\n// DCC-EX removed  #if !defined(ESP8266) && !defined(ESP32)\n\n#define SHA1HANDSOFF\n\n#include <stdio.h>\n#include <string.h>\n#include <stdint.h>\n\n#include \"libsha1.h\"\n\n\n#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))\n\n/* blk0() and blk() perform the initial expand. */\n/* I got the idea of expanding during the round function from SSLeay */\n#if BYTE_ORDER == LITTLE_ENDIAN\n#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \\\n    |(rol(block->l[i],8)&0x00FF00FF))\n#elif BYTE_ORDER == BIG_ENDIAN\n#define blk0(i) block->l[i]\n#else\n#error \"Endianness not defined!\"\n#endif\n#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \\\n    ^block->l[(i+2)&15]^block->l[i&15],1))\n\n/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */\n#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);\n#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);\n#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);\n#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);\n#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);\n\n\n/* Hash a single 512-bit block. This is the core of the algorithm. */\n\nvoid SHA1Transform(uint32_t state[5], const unsigned char buffer[64])\n{\n    uint32_t a, b, c, d, e;\n    typedef union {\n        unsigned char c[64];\n        uint32_t l[16];\n    } CHAR64LONG16;\n#ifdef SHA1HANDSOFF\n    CHAR64LONG16 block[1];  /* use array to appear as a pointer */\n    memcpy(block, buffer, 64);\n#else\n    /* The following had better never be used because it causes the\n     * pointer-to-const buffer to be cast into a pointer to non-const.\n     * And the result is written through.  I threw a \"const\" in, hoping\n     * this will cause a diagnostic.\n     */\n    CHAR64LONG16* block = (const CHAR64LONG16*)buffer;\n#endif\n    /* Copy context->state[] to working vars */\n    a = state[0];\n    b = state[1];\n    c = state[2];\n    d = state[3];\n    e = state[4];\n    /* 4 rounds of 20 operations each. Loop unrolled. */\n    R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3);\n    R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7);\n    R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11);\n    R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15);\n    R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19);\n    R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23);\n    R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27);\n    R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31);\n    R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35);\n    R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39);\n    R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43);\n    R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47);\n    R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51);\n    R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55);\n    R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59);\n    R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63);\n    R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67);\n    R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71);\n    R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75);\n    R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79);\n    /* Add the working vars back into context.state[] */\n    state[0] += a;\n    state[1] += b;\n    state[2] += c;\n    state[3] += d;\n    state[4] += e;\n    /* Wipe variables */\n    a = b = c = d = e = 0;\n#ifdef SHA1HANDSOFF\n    memset(block, '\\0', sizeof(block));\n#endif\n}\n\n\n/* SHA1Init - Initialize new context */\n\nvoid SHA1Init(SHA1_CTX* context)\n{\n    /* SHA1 initialization constants */\n    context->state[0] = 0x67452301;\n    context->state[1] = 0xEFCDAB89;\n    context->state[2] = 0x98BADCFE;\n    context->state[3] = 0x10325476;\n    context->state[4] = 0xC3D2E1F0;\n    context->count[0] = context->count[1] = 0;\n}\n\n\n/* Run your data through this. */\n\nvoid SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len)\n{\n    uint32_t i, j;\n\n    j = context->count[0];\n    if ((context->count[0] += len << 3) < j)\n        context->count[1]++;\n    context->count[1] += (len>>29);\n    j = (j >> 3) & 63;\n    if ((j + len) > 63) {\n        memcpy(&context->buffer[j], data, (i = 64-j));\n        SHA1Transform(context->state, context->buffer);\n        for ( ; i + 63 < len; i += 64) {\n            SHA1Transform(context->state, &data[i]);\n        }\n        j = 0;\n    }\n    else i = 0;\n    memcpy(&context->buffer[j], &data[i], len - i);\n}\n\n\n/* Add padding and return the message digest. */\n\nvoid SHA1Final(unsigned char digest[20], SHA1_CTX* context)\n{\n    unsigned i;\n    unsigned char finalcount[8];\n    unsigned char c;\n\n#if 0\t/* untested \"improvement\" by DHR */\n    /* Convert context->count to a sequence of bytes\n     * in finalcount.  Second element first, but\n     * big-endian order within element.\n     * But we do it all backwards.\n     */\n    unsigned char *fcp = &finalcount[8];\n\n    for (i = 0; i < 2; i++)\n       {\n        uint32_t t = context->count[i];\n        int j;\n\n        for (j = 0; j < 4; t >>= 8, j++)\n\t          *--fcp = (unsigned char) t;\n    }\n#else\n    for (i = 0; i < 8; i++) {\n        finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)]\n         >> ((3-(i & 3)) * 8) ) & 255);  /* Endian independent */\n    }\n#endif\n    c = 0200;\n    SHA1Update(context, &c, 1);\n    while ((context->count[0] & 504) != 448) {\n\tc = 0000;\n        SHA1Update(context, &c, 1);\n    }\n    SHA1Update(context, finalcount, 8);  /* Should cause a SHA1Transform() */\n    for (i = 0; i < 20; i++) {\n        digest[i] = (unsigned char)\n         ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255);\n    }\n    /* Wipe variables */\n    memset(context, '\\0', sizeof(*context));\n    memset(&finalcount, '\\0', sizeof(finalcount));\n}\n/* ================ end of sha1.c ================ */\n\n\n// DCC-EX Removed: #endif\n"
  },
  {
    "path": "libsha1.h",
    "content": "// For DCC-EX: This file downloaded from:\n//  https://github.com/Links2004/arduinoWebSockets\n// All due credit to Steve Reid\n\n/* ================ sha1.h ================ */\n/*\nSHA-1 in C\nBy Steve Reid <steve@edmweb.com>\n100% Public Domain\n*/\n\n// DCC-EX REMOVED #if !defined(ESP8266) && !defined(ESP32)\n#ifndef libsha1_h\n#define libsha1_h\ntypedef struct {\n    uint32_t state[5];\n    uint32_t count[2];\n    unsigned char buffer[64];\n} SHA1_CTX;\n\nvoid SHA1Transform(uint32_t state[5], const unsigned char buffer[64]);\nvoid SHA1Init(SHA1_CTX* context);\nvoid SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len);\nvoid SHA1Final(unsigned char digest[20], SHA1_CTX* context);\n\n#endif"
  },
  {
    "path": "myAutomation.example.h",
    "content": "/* This is an automation example file.\n *  The presence of a file called \"myAutomation.h\" brings EX-RAIL code into\n *  the command station.\n *  The automation may have multiple concurrent tasks.\n *  A task may \n *  - Act as a ROUTE setup macro for a user to drive over \n *  - drive a loco through an AUTOMATION \n *  - automate some cosmetic part of the layout without any loco.\n *  \n *  At startup, a single task is created to execute the startup sequence.\n *  This task may simply follow a route, or may START  \n *  further tasks (that is.. send a loco out along a route).\n *  \n *  Where the loco id is not known at compile time, a new task \n *  can be created with the command:\n *  </ START [cab] route> \n *  \n *  A ROUTE, AUTOMATION or SEQUENCE are internally identical in ExRail terms  \n *  but are just represented differently to a Withrottle user:\n *  ROUTE(n,\"name\") - as Route_n .. to setup a route through a layout\n *  AUTOMATION(n,\"name\") as Auto_n .. to send the current loco off along an automated journey\n *  SEQUENCE(n) is not visible to Withrottle.\n *  \n */\n\n// This is the startup sequence, \nAUTOSTART\nPOWERON        // turn on track power\nSENDLOCO(3,1) // send loco 3 off along route 1\nSENDLOCO(10,2) // send loco 10 off along route 2\nDONE     // This just ends the startup thread, leaving 2 others running.\n\n/* SEQUENCE(1) is a simple shuttle between 2 sensors      \n *  S20 and S21 are sensors on arduino pins 20 and 21 \n *  S20                    S21                   \n *  === START->================\n */\n   SEQUENCE(1) \n     DELAY(10000)   // wait 10 seconds\n     FON(3)       // Set Loco Function 3, Horn on\n     DELAY(1000)    // wait 1 second\n     FOFF(3)      // Horn off\n     FWD(80)      // Move forward at speed 80\n     AT(21)       // until we hit sensor id 21\n     STOP         // then stop\n     DELAY(5000)    // Wait 5 seconds\n     FON(2)       // ring bell\n     REV(60)      // reverse at speed 60\n     AT(20)       // until we get to S20\n     STOP         // then stop\n     FOFF(2)      // Bell off \n     FOLLOW(1)    // and follow sequence 1 again\n   \n/* SEQUENCE(2) is an automation example for a single loco Y shaped journey\n *  S31,S32,S33 are sensors, T4 is a turnout\n *  \n *  S33                      T4                            S31\n *  ===-START->=============================================\n *                          //\n *  S32                    //\n *  ======================//\n *  \n *  Train runs from START to S31, back to S32, again to S31, Back to start.\n */\n  SEQUENCE(2)\n   FWD(60)     // go forward at DCC speed 60 \n   AT(31) STOP  // when we get to sensor 31 \n   DELAY(10000)  // wait 10 seconds \n   THROW(4)    // throw turnout for route to S32\n   REV(45)     // go backwards at speed 45\n   AT(32) STOP  // until we arrive at sensor 32\n   DELAY(5000)   // wait 5 seconds\n   FWD(50)     // go forwards at speed 50\n   AT(31) STOP  // and stop at sensor 31\n   DELAY(5000)   // wait 5 seconds \n   CLOSE(4)    // set turnout closed\n   REV(50)     // reverse back to S3\n   AT(33) STOP\n   DELAY(20000)  // wait 20 seconds \n   FOLLOW(2)   // follow sequence 2... ie repeat the process\n"
  },
  {
    "path": "myEX-Turntable.example.h",
    "content": "/**************************************************************************************************\n * This is an example automation file to control EX-Turntable using recommended techniques.\n ************************************************************************************************** \n * INSTRUCTIONS\n ************************************************************************************************** \n * To use this example file as the starting point for your layout, there are two options:\n * \n * 1. If you don't have an existing \"myAutomation.h\" file, simply rename \"myEX-Turntable.example.h\" to\n *    \"myAutomation.h\".\n * 2. If you have an existing \"myAutomation.h\" file, rename \"myEX-Turntable.example.h\" to \"myEX-Turntable.h\",\n *    and then include it by adding the line below at the end of your existing \"myAutomation.h\", on a\n *    line of its own:\n * \n *    #include \"myEX-Turntable.h\"\n * \n * Note that there are further instructions in the documentation at https://dcc-ex.com/.\n *************************************************************************************************/\n\n/**************************************************************************************************\n * The MOVETT() command below will automatically move your turntable to the defined step position on\n * start up.\n * \n * If you do not wish this to occur, simply comment the line out.\n * \n * NOTE: If you are including this file at the end of an existing \"myAutomation.h\" file, you will likely\n * need to move this line to the beginning of your existing \"myAutomation.h\" file in order for it to\n * be effective.\n *************************************************************************************************/\nMOVETT(600, 114, Turn)\nDONE\n\n// For Conductor level users who wish to just use EX-Turntable, you don't need to understand this\n// and can move to defining the turntable positions below. You must, however, ensure this remains\n// before any position definitions or you will get compile errors when uploading.\n//\n// Definition of the EX_TURNTABLE macro to correctly create the ROUTEs required for each position.\n// This includes RESERVE()/FREE() to protect any automation activities.\n//\n#define EX_TURNTABLE(route_id, reserve_id, vpin, steps, activity, desc) \\\n  ROUTE(route_id, desc) \\\n    RESERVE(reserve_id) \\\n    MOVETT(vpin, steps, activity) \\\n    WAITFOR(vpin) \\\n    FREE(reserve_id) \\\n    DONE\n\n/**************************************************************************************************\n * TURNTABLE POSITION DEFINITIONS\n *************************************************************************************************/\n// EX_TURNTABLE(route_id, reserve_id, vpin, steps, activity, desc)\n//\n// route_id = A unique number for each defined route, the route is what appears in throttles\n// reserve_id = A unique reservation number (0 - 255) to ensure nothing interferes with automation\n// vpin = The Vpin defined for the Turntable-EX device driver, default is 600\n// steps = The target step position\n// activity = The activity performed for this ROUTE (Note do not enclose in quotes \"\")\n// desc = Description that will appear in throttles (Must use quotes \"\")\n//\nEX_TURNTABLE(TTRoute1, Turntable, 600, 114, Turn, \"Position 1\")\nEX_TURNTABLE(TTRoute2, Turntable, 600, 227, Turn, \"Position 2\")\nEX_TURNTABLE(TTRoute3, Turntable, 600, 341, Turn, \"Position 3\")\nEX_TURNTABLE(TTRoute4, Turntable, 600, 2159, Turn, \"Position 4\")\nEX_TURNTABLE(TTRoute5, Turntable, 600, 2273, Turn, \"Position 5\")\nEX_TURNTABLE(TTRoute6, Turntable, 600, 2386, Turn, \"Position 6\")\nEX_TURNTABLE(TTRoute7, Turntable, 600, 0, Home, \"Home turntable\")\n\n// Pre-defined aliases to ensure unique IDs are used.\n// Turntable reserve ID, valid is 0 - 255\nALIAS(Turntable, 255)\n\n// Turntable ROUTE ID reservations, using <? TTRouteX> for uniqueness:\nALIAS(TTRoute1)\nALIAS(TTRoute2)\nALIAS(TTRoute3)\nALIAS(TTRoute4)\nALIAS(TTRoute5)\nALIAS(TTRoute6)\nALIAS(TTRoute7)\nALIAS(TTRoute8)\nALIAS(TTRoute9)\nALIAS(TTRoute10)\nALIAS(TTRoute11)\nALIAS(TTRoute12)\nALIAS(TTRoute13)\nALIAS(TTRoute14)\nALIAS(TTRoute15)\nALIAS(TTRoute16)\nALIAS(TTRoute17)\nALIAS(TTRoute18)\nALIAS(TTRoute19)\nALIAS(TTRoute20)\nALIAS(TTRoute21)\nALIAS(TTRoute22)\nALIAS(TTRoute23)\nALIAS(TTRoute24)\nALIAS(TTRoute25)\nALIAS(TTRoute26)\nALIAS(TTRoute27)\nALIAS(TTRoute28)\nALIAS(TTRoute29)\nALIAS(TTRoute30)\n"
  },
  {
    "path": "myHal.cpp.txt",
    "content": "#include \"defines.h\"\n#include \"IODevice.h\"\n\n#ifndef IO_NO_HAL\n\n#include \"IO_VL53L0X.h\"\n#include \"IO_HCSR04.h\"\n#include \"Sensors.h\"\n#include \"Turnouts.h\"\n#include \"IO_DFPlayer.h\"\n//#include \"IO_Wire.h\"\n#include \"IO_AnalogueInputs.h\"\n#if __has_include(\"IO_Servo.h\")\n#include \"IO_Servo.h\"\n#include \"IO_PCA9685pwm.h\"\n#endif\n\n#include \"IO_HALDisplay.h\"\n#include \"LiquidCrystal_I2C.h\"\n\n#if __has_include(\"IO_CMRI.h\")\n#include \"IO_CMRI.h\"\n#endif\n\n//#include \"IO_ExampleSerial.h\"\n\n//#include \"IO_EXFastclock.h\"\n//#include \"IO_EXTurntable.h\"\n\n#if __has_include(\"IO_ExternalEEPROM.h\")\n#include \"IO_ExternalEEPROM.h\"\n#endif\n\n#if __has_include(\"IO_Network.h\")\n#include \"IO_Network.h\"\n#include \"Net_RF24.h\"\n#include \"Net_ENC28J60.h\"\n#include \"Net_Ethernet.h\"\n#define NETWORK_PRESENT\n#endif\n\n#include \"IO_TouchKeypad.h\"\n\n#define WIRE_TEST 0\n#define TESTHARNESS 1\n#define I2C_STRESS_TEST 0\n#define I2C_SETCLOCK 0\n\n#include \"DCC.h\"\n\n\n#if 0 // Long Strings\n#define s10 \"0123456789\"\n#define s100 s10 s10 s10 s10 s10 s10 s10 s10 s10 s10\n#define s1k s100 s100 s100 s100 s100 s100 s100 s100 s100 s100\n#define s10k s1k s1k s1k s1k s1k s1k s1k s1k s1k s1k\n#define s32k s10k s10k s10k s1k s1k\nvolatile const char PROGMEM ss1[] = s32k;\n#endif\n\n\n#if TESTHARNESS\n\n// Function to be invoked by test harness\nvoid myTest() {\n  // DIAG(F(\"VL53L0X #1 Test: dist=%d signal=%d ambient=%d value=%d\"), \n  //   IODevice::readAnalogue(5000),\n  //   IODevice::readAnalogue(5001),\n  //   IODevice::readAnalogue(5002),\n  //   IODevice::read(5000));\n  // DIAG(F(\"VL53L0X #2 Test: dist=%d signal=%d ambient=%d value=%d\"), \n  //   IODevice::readAnalogue(5003),\n  //   IODevice::readAnalogue(5004),\n  //   IODevice::readAnalogue(5005),\n  //   IODevice::read(5003));\n  // DIAG(F(\"HCSR04 Test: dist=%d value=%d\"),\n  //   IODevice::readAnalogue(2000),\n  //   IODevice::read(2000));\n  // DIAG(F(\"ADS111x Test: %d %d %d %d %d\"), \n  //   IODevice::readAnalogue(4500), \n  //   IODevice::readAnalogue(4501),\n  //   IODevice::readAnalogue(4502),\n  //   IODevice::readAnalogue(4503),\n  //   IODevice::readAnalogue(A5)\n  // );\n  // DIAG(F(\"RF24 Test: 4000:%d 4002:%d\"), \n  //   IODevice::read(4000), \n  //   IODevice::read(4002)\n  // );\n  DIAG(F(\"EXPANDER: 2212:%d 2213:%d 2214:%d\"),\n    IODevice::readAnalogue(2212), \n    IODevice::readAnalogue(2213),\n    IODevice::readAnalogue(2214));\n}\n#endif\n\n#if I2C_STRESS_TEST\nstatic bool initialised = false;\nstatic uint8_t lastStatus = 0;\nstatic const int nRBs = 3; // request blocks concurrently\nstatic const int I2cTestPeriod = 1; // milliseconds\nstatic I2CAddress testDevice = {SubBus_6, 0x27};\nstatic I2CRB rb[nRBs];\nstatic uint8_t readBuffer[nRBs*32]; // nRB x 32-byte input buffer\nstatic uint8_t writeBuffer[nRBs];  // nRB x 1-byte output buffer\nstatic unsigned long count = 0;\nstatic unsigned long errors = 0;\nstatic unsigned long lastOutput = millis();\n\nvoid I2CTest() {\n  if (!initialised) {\n    // I2C Loading for stress test.\n    // Write value then read back 32 times\n    for (int i=0; i<nRBs; i++) {\n      writeBuffer[i] = (0xc5 ^ i ^ i<<3 ^ i<<6) & ~0x08; // bit corresponding to 08 is hard-wired low\n      rb[i].setRequestParams(testDevice, &readBuffer[i*32], 32, \n        &writeBuffer[i], 1);\n      I2CManager.queueRequest(&rb[i]);\n    }\n    initialised = true;\n  }\n\n  for (int i=0; i<nRBs; i++) {\n    if (!rb[i].isBusy()) {\n      count++;\n      uint8_t status = rb[i].status;\n      if (status != lastStatus) {\n        DIAG(F(\"I2CTest: status=%d (%S)\"), \n          (int)status, I2CManager.getErrorMessage(status));\n        lastStatus = status; \n      }\n      if (status == I2C_STATUS_OK) {\n        bool diff = false;\n        // Check contents of response\n        for (uint8_t j=0; j<32; j++) {\n          if (readBuffer[i*32+j] != writeBuffer[i]) {\n            DIAG(F(\"I2CTest: Received message mismatch, sent %2x rcvd %2x\"), \n              writeBuffer[i], readBuffer[i*32+j]);\n            diff = true;\n          }\n        }\n        if (diff) errors++;\n      } else\n        errors++;\n      I2CManager.queueRequest(&rb[i]);\n    }\n  }\n  if (millis() - lastOutput > 60000) { // 1 minute\n    DIAG(F(\"I2CTest: Count=%l Errors=%l\"), count, errors);\n    count = errors = 0;\n    lastOutput = millis();\n  }\n}\n#endif\n\nvoid updateLocoScreen() {\n  for (int i=0; i<8; i++) {\n    if (DCC::speedTable[i].loco > 0) {\n      int speed = DCC::speedTable[i].speedCode;\n      char direction = (speed & 0x80) ? 'R' : 'F';\n      speed = speed & 0x7f;\n      if (speed > 0) speed = speed - 1;\n      SCREEN(3, i, F(\"Loco:%4d %3d %c\"), DCC::speedTable[i].loco,\n        speed, direction);\n    }\n  }\n}\n\nvoid updateTime() {\n  uint8_t buffer[20];\n  I2CAddress rtc = {SubBus_1, 0x68};  // Real-time clock I2C address\n  buffer[0] = 0;\n\n  // Set time - only needs to be done once if battery is ok.\n  static bool timeSet = false;\n  if (!timeSet) {\n    // I2CManager.read(rtc, buffer+1, sizeof(buffer)-1);\n    // uint8_t year = 23;    // 2023\n    // uint8_t day = 2;      // tuesday\n    // uint8_t date = 21;    // 21st\n    // uint8_t month = 2;    // feb\n    // uint8_t hours = 23;   // xx:\n    // uint8_t minutes = 25; // :xx\n    // buffer[1] = 0;   // seconds\n    // buffer[2] = ((minutes / 10) << 4) | (minutes % 10);\n    // buffer[3] = ((hours / 10) << 4) | (hours % 10);\n    // buffer[4] = day;\n    // buffer[5] = ((date/10) << 4) + date%10; // 24th\n    // buffer[6] = ((month/10) << 4) + month%10; // feb\n    // buffer[7] = ((year/10) << 4) + year%10; // xx23\n    // for (uint8_t i=8; i<sizeof(buffer); i++) buffer[i] = 0;\n    // I2CManager.write(rtc, buffer, sizeof(buffer));\n    timeSet = true;\n  }\n\n  uint8_t status = I2CManager.read(rtc, buffer+1, sizeof(buffer)-1, 1, 0);\n  if (status == I2C_STATUS_OK) {\n    uint8_t seconds10 = buffer[1] >> 4;\n    uint8_t seconds1 = buffer[1] & 0xf;\n    uint8_t minutes10 = buffer[2] >> 4;\n    uint8_t minutes1 = buffer[2] & 0xf;\n    uint8_t hours10 = buffer[3] >> 4;\n    uint8_t hours1 = buffer[3] & 0xf;\n    SCREEN(10, 0, F(\"Departures  %d%d:%d%d:%d%d\"), \n      hours10, hours1, minutes10, minutes1, seconds10, seconds1);\n  }\n}\n\nvoid showCharacterSet() {\n  if (millis() < 3000) return;\n  const uint8_t lineLen = 20;\n  char buffer[lineLen+1];\n  static uint8_t nextChar = 0x20;\n  for (uint8_t row=0; row<8; row+=1) {\n    for (uint8_t col=0; col<lineLen; col++) {\n      buffer[col] = nextChar++;\n      buffer[++col] = ' ';\n      if (nextChar == 0) nextChar = 0x20; // check for wrap-around\n    }\n    buffer[lineLen] = '\\0';\n    SCREEN(3, row, F(\"%s\"), buffer);\n  }\n}\n\n#if defined(ARDUINO_NUCLEO_F446RE)\nHardwareSerial Serial3(PC11, PC10);\n#endif\n\n\n// HAL device initialisation\nvoid halSetup() {\n\n  I2CManager.setTimeout(500); // microseconds\n  I2CManager.forceClock(400000);\n\n  HALDisplay<OLED>::create(10, {SubBus_5, 0x3c}, 132, 64); // SH1106\n  // UserAddin::create(updateLocoScreen, 1000);\n  // UserAddin::create(showCharacterSet, 5000);\n  // UserAddin::create(updateTime, 1000);\n  \n  HALDisplay<OLED>::create(10, {SubBus_4, 0x3c}, 128, 32);\n  HALDisplay<OLED>::create(10, {SubBus_7, 0x3c}, 128, 32);\n\n  //HALDisplay<LiquidCrystal_I2C>::create(10, {SubBus_4, 0x27}, 20, 4);\n\n    // Draw double boxes with X O O X inside.\n  // SCREEN(3, 2, F(\"\\xc9\\xcd\\xcd\\xcd\\xcb\\xcd\\xcd\\xcd\\xcb\\xcd\\xcd\\xcd\\xcb\\xcd\\xcd\\xcd\\xcb\\xcd\\xcd\\xcd\\xbb\"));\n  // SCREEN(3, 3, F(\"\\xba X \\xba O \\xba O \\xba O \\xba X \\xba\"));\n  // SCREEN(3, 4, F(\"\\xcc\\xcd\\xcd\\xcd\\xce\\xcd\\xcd\\xcd\\xce\\xcd\\xcd\\xcd\\xce\\xcd\\xcd\\xcd\\xce\\xcd\\xcd\\xcd\\xb9\"));\n  // SCREEN(3, 5, F(\"\\xba X \\xba O \\xba O \\xba O \\xba X \\xba\"));\n  // SCREEN(3, 6, F(\"\\xc8\\xcd\\xcd\\xcd\\xca\\xcd\\xcd\\xcd\\xca\\xcd\\xcd\\xcd\\xca\\xcd\\xcd\\xcd\\xca\\xcd\\xcd\\xcd\\xbc\"));\n\n  // Draw single boxes with X O O X inside.\n  // SCREEN(3, 0, F(\"Summary Data:\"));\n  // SCREEN(3, 1, F(\"\\xda\\xc4\\xc4\\xc4\\xc2\\xc4\\xc4\\xc4\\xc2\\xc4\\xc4\\xc4\\xc2\\xc4\\xc4\\xc4\\xc2\\xc4\\xc4\\xc4\\xbf\"));\n  // SCREEN(3, 2, F(\"\\xb3 X \\xb3 O \\xb3 O \\xb3 O \\xb3 X \\xb3\"));\n  // SCREEN(3, 3, F(\"\\xc3\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xb4\"));\n  // SCREEN(3, 4, F(\"\\xb3 X \\xb3 O \\xb3 O \\xb3 O \\xb3 X \\xb3\"));\n  // SCREEN(3, 5, F(\"\\xc3\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xc5\\xc4\\xc4\\xc4\\xb4\"));\n  // SCREEN(3, 6, F(\"\\xb3 X \\xb3 O \\xb3 O \\xb3 O \\xb3 X \\xb3\"));\n  // SCREEN(3, 7, F(\"\\xc0\\xc4\\xc4\\xc4\\xc1\\xc4\\xc4\\xc4\\xc1\\xc4\\xc4\\xc4\\xc1\\xc4\\xc4\\xc4\\xc1\\xc4\\xc4\\xc4\\xd9\"));\n\n  // Blocks of different greyness\n  // SCREEN(3, 0, F(\"\\xb0\\xb0\\xb0\\xb0\\xb1\\xb1\\xb1\\xb1\\xb2\\xb2\\xb2\\xb2\\xdb\\xdb\\xdb\\xdb\"));\n  // SCREEN(3, 1, F(\"\\xb0\\xb0\\xb0\\xb0\\xb1\\xb1\\xb1\\xb1\\xb2\\xb2\\xb2\\xb2\\xdb\\xdb\\xdb\\xdb\"));\n  // SCREEN(3, 2, F(\"\\xb0\\xb0\\xb0\\xb0\\xb1\\xb1\\xb1\\xb1\\xb2\\xb2\\xb2\\xb2\\xdb\\xdb\\xdb\\xdb\"));\n\n  // DCCEX logo\n  // SCREEN(3, 1, F(\"\\xb0\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\x20\\xb0\\xb0\\xb0\\x20\\xb0\\x20\\xb0\"));\n  // SCREEN(3, 2, F(\"\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\xb0\"));\n  // SCREEN(3, 3, F(\"\\xb0\\x20\\xb0\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\xb0\\xb0\\x20\\x20\\x20\\xb0\\x20\"));\n  // SCREEN(3, 4, F(\"\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\xb0\"));\n  // SCREEN(3, 5, F(\"\\xb0\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\xb0\\x20\\x20\\x20\\x20\\xb0\\xb0\\xb0\\x20\\xb0\\x20\\xb0\"));\n  // SCREEN(3, 7, F(\"\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\\xb1\"));\n  \n#if 0\n  // List versions of devices that respond to the version request\n  for (uint8_t address = 8; address<0x78; address++) {\n    uint8_t buffer[3];\n    uint8_t status = I2CManager.read(0x7c, buffer, sizeof(buffer), 1, address);\n    if (status == I2C_STATUS_OK) {\n      uint16_t manufacturer = ((uint16_t)buffer[0] << 4 ) | (buffer[1] >> 4);\n      uint16_t deviceID = ((uint16_t)(buffer[1] & 0x0f) << 5) | (buffer[2] >> 3);\n      uint16_t dieRevision = buffer[2] & 0x1f;\n      DIAG(F(\"Addr %s version: %x %x %x\"), address.toString(), manufacturer, deviceID, dieRevision);\n    } \n  }\n#endif\n\n#if I2C_STRESS_TEST\n  UserAddin::create(I2CTest, I2cTestPeriod);\n#endif\n\n#if WIRE_TEST\n  // Test of Wire-I2CManager interface\n  Wire.begin();\n  Wire.setClock(400000);\n  Wire.beginTransmission(0x23);\n  Wire.print(\"Hello\");\n  uint8_t status = Wire.endTransmission();\n  if (status==0) DIAG(F(\"Wire: device Found on 0x23\"));\n\n  Wire.beginTransmission(0x23);\n  Wire.write(0xde);\n  Wire.endTransmission(false); // don't send stop\n  Wire.requestFrom(0x23, 1);\n  if (Wire.available()) {\n    DIAG(F(\"Wire: value=x%x\"), Wire.read());\n  }\n  uint8_t st = I2CManager.write(0x33, 0, 0);\n  DIAG(F(\"I2CManager 0x33 st=%d \\\"%S\\\"\"), st, \n    I2CManager.getErrorMessage(st));\n#endif\n\n#if I2C_SETCLOCK\n  // Test I2C clock changes\n  // Set up two I2C request blocks\n  I2CRB rb1, rb2;\n  uint8_t readBuff[32];\n  rb1.setRequestParams(0x23, readBuff, sizeof(readBuff), readBuff, sizeof(readBuff));\n  rb2.setRequestParams(0x23, readBuff, sizeof(readBuff), readBuff, sizeof(readBuff));\n  // First set clock to 400kHz and then issue requests\n  I2CManager.forceClock(400000);\n  I2CManager.queueRequest(&rb1);\n  I2CManager.queueRequest(&rb2);\n  // Wait a little to allow the first transaction to start\n  delayMicroseconds(2);\n  // ... then request a clock speed change\n  I2CManager.forceClock(100000);\n  DIAG(F(\"I2CClock: rb1 status=%d\"), rb1.wait());\n  DIAG(F(\"I2CClock: rb2 status=%d\"), rb2.wait());\n  // Reset clock speed\n  I2CManager.forceClock(400000);\n#endif\n  \n  EXIOExpander::create(2200, 18, {SubBus_0, 0x65});\n  //UserAddin::create(myTest, 1000);\n  // ServoTurnout::create(2200, 2200, 400, 200, 0);\n  // ServoTurnout::create(2200, 2200, 400, 200, 0);\n\n  TouchKeypad::create(2300, 16, 25, 24);\n\n  // GPIO\n  PCF8574::create(800, 8, {SubBus_1, 0x23});\n  //PCF8574::create(808, 8, {SubBus_2, 0x27});\n  PCF8574::create(65000, 8, 0x27);\n\n  MCP23017::create(164,16,{SubBus_3, 0x20});\n  //MCP23017::create(180,16,{SubBus_0, 0x27});\n  Sensor::create(170, 170, 1); // Hall effect, enable pullup.\n  Sensor::create(171, 171, 1);\n\n  // PWM (LEDs and Servos)\n  // For servos, use default 50Hz pulses.\n  PCA9685::create(100, 16, {SubBus_1, 0x41});\n  // For LEDs, use 1kHz pulses.\n  PCA9685::create(116, 16, {SubBus_1, 0x40}, 1000);\n\n  // 4-pin Analogue Input Module\n  //ADS111x::create(4500, 4, 0x48);\n\n  // Laser Time-Of-Flight Sensors\n  VL53L0X::create(5000, 3, {SubBus_0, 0x60}, 300, 310, 46);\n  //VL53L0X::create(5003, 3, {SubBus_6, 0x61}, 300, 310, 47);\n  Sensor::create(5000, 5000, 0);\n  Sensor::create(5003, 5003, 0);\n  // Monitor reset digital on first TOF\n  //Sensor::create(46,46,0);\n\n  // // External 24C256 EEPROM (256kBytes) on I2C address 0x50.\n  // ExternalEEPROM::create({SubBus_0, 0x50}, 256);\n\n  // Play up to 10 sounds on pins 10000-10009. Player is connected to Serial1 or Serial2.\n  #if defined(HAVE_HWSERIAL1) && !defined(ARDUINO_ARCH_STM32)\n  DFPlayer::create(10000, 14, Serial1);\n  #elif defined(ARDUINO_ARCH_STM32)\n  DFPlayer::create(10000, 10, Serial3);  // Pins PC11 (RX) and PC10 (TX)\n  #endif\n\n  // Ultrasound echo device\n  HCSR04::create(2000, 32, 33, 80, 85 /*, HCSR04::LOOP */);\n  Sensor::create(2000, 2000, 0);\n\n#if __has_include(\"IO_CMRI.h\")\n  CMRIbus::create(0, Serial2, 115200, 50, 40); // 50ms cycle, pin 40 for DE/!RE pins\n  CMRInode::create(25000, 72, 0, 0, 'M'); // SMINI address 0\n  for (int pin=0; pin<24; pin++) {\n    Sensor::create(25000+pin, 25000+pin, 0);\n  }\n#endif\n\n  //CMRInode::create(25072, 72, 0, 13, 'M');  // SMINI address 13\n  //CMRInode::create(25144, 288, 0, 14, 'C', 144, 144); // CPNODE address 14\n\n#ifdef NETWORK_PRESENT\n  // Define remote pins to be used.  The range of remote pins is like a common data area shared\n  // between all nodes.\n  // For outputs, a write to a remote VPIN causes a message to be sent to another node, which then performs\n  // the write operation on the device VPIN that is local to that node.\n  // For inputs, the state of remote input VPIN is read on the node where it is connected, and then \n  // sent to other nodes in the system where the state is saved and processed.  Updates are sent on change, and\n  // also periodically if no changes.\n  //\n  // Each definition is a triple of remote node, remote pin, indexed by relative pin.  Up to 224 rpins can\n  // be configured (per node).  This is to fit into a 32-byte packet.\n  REMOTEPINS rpins[] = {\n      {30,164,RPIN_IN} ,       //4000  Node 30, first MCP23017 pin, input\n      {30,165,RPIN_IN},        //4001  Node 30, second MCP23017 pin, input\n      {30,166,RPIN_OUT},       //4002  Node 30, third MCP23017 pin, output\n      {30,166,RPIN_OUT},       //4003  Node 30, fourth MCP23017 pin, output\n      {30,100,RPIN_INOUT},     //4004  Node 30, first PCA9685 servo pin\n      {30,101,RPIN_INOUT},     //4005  Node 30, second PCA9685 servo pin\n      {30,102,RPIN_INOUT},     //4006  Node 30, third PCA9685 servo pin\n      {30,103,RPIN_INOUT},     //4007  Node 30, fourth PCA9685 servo pin\n      {30,24,RPIN_IN},         //4008  Node 30, Arduino pin D24\n      {30,25,RPIN_IN},         //4009  Node 30, Arduino pin D25\n      {30,26,RPIN_IN},         //4010  Node 30, Arduino pin D26\n      {30,27,RPIN_IN},         //4011  Node 30, Arduino pin D27\n      {30,1000,RPIN_OUT},      //4012  Node 30, DFPlayer playing flag (when read) / Song selector (when written)\n      {30,5000,RPIN_IN},       //4013  Node 30, VL53L0X detect pin\n      {30,VPIN_NONE,0},        //4014  Node 30, spare\n      {30,VPIN_NONE,0},        //4015  Node 30, spare\n    \n      {31,164,RPIN_IN} ,       //4016  Node 31, first MCP23017 pin, input\n      {31,165,RPIN_IN},        //4017  Node 31, second MCP23017 pin, input\n      {31,166,RPIN_OUT},       //4018  Node 31, third MCP23017 pin, output\n      {31,166,RPIN_OUT},       //4019  Node 31, fourth MCP23017 pin, output\n      {31,100,RPIN_INOUT},     //4020  Node 31, first PCA9685 servo pin\n      {31,101,RPIN_INOUT},     //4021  Node 31, second PCA9685 servo pin\n      {31,102,RPIN_INOUT},     //4022  Node 31, third PCA9685 servo pin\n      {31,103,RPIN_INOUT},     //4023  Node 31, fourth PCA9685 servo pin\n      {31,24,RPIN_IN},         //4024  Node 31, Arduino pin D24\n      {31,25,RPIN_IN},         //4025  Node 31, Arduino pin D25\n      {31,26,RPIN_IN},         //4026  Node 31, Arduino pin D26\n      {31,27,RPIN_IN},         //4027  Node 31, Arduino pin D27\n      {31,3,RPIN_IN},          //4028  Node 31, Arduino pin D3\n      {31,VPIN_NONE,0},        //4029  Node 31, spare\n      {31,VPIN_NONE,0},        //4030  Node 31, spare\n      {31,VPIN_NONE,0}         //4031  Node 31, spare\n    };\n  // FirstVPIN, nPins, thisNode, pinDefs, CEPin, CSNPin\n  // Net_RF24 *rf24Driver = new Net_RF24(48, 49);\n  // Network<Net_RF24>::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, rf24Driver);\n  #if NODE==30\n  //Net_ENC28J60 *encDriver = new Net_ENC28J60(49);\n  //Network<Net_ENC28J60>::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, encDriver);\n  #elif NODE==31\n  Net_ENC28J60 *encDriver = new Net_ENC28J60(53);\n  Network<Net_ENC28J60>::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, encDriver);\n  #else\n  Net_Ethernet *etherDriver = new Net_Ethernet();\n  Network<Net_Ethernet>::create(4000, NUMREMOTEPINS(rpins), NODE, rpins, etherDriver);\n  #endif\n  for (int i=0; i<=32; i++) \n    Sensor::create(4000+i, 4000+i, 0);\n#endif\n\n#ifdef ARDUINO_ARCH_STM32\n//PCF8574::create(1900, 8, 0x27);\nSensor::create(1900,100,1);\nSensor::create(1901,101,1);\n#endif\n\n}\n#endif // IO_NO_HAL\n"
  },
  {
    "path": "myHal.cpp_example.txt",
    "content": "//  Sample myHal.cpp file.\n//\n// To use this file, copy it to myHal.cpp and uncomment the directives and/or\n// edit them to satisfy your requirements.  If you only want to use up to \n// two MCP23017 GPIO Expander modules and/or up to two PCA9685 Servo modules,\n// then you don't need this file as DCC++EX configures these for free!\n\n// Note that if the file has a .cpp extension it WILL be compiled into the build\n// and the halSetup() function WILL be invoked.\n//\n// To prevent this, temporarily rename the file to myHal.txt or similar.\n//\n\n// The #if directive prevent compile errors for Uno and Nano by excluding the \n//  HAL directives from the build.\n#if !defined(IO_NO_HAL)\n\n// Include devices you need.\n#include \"IODevice.h\"\n//#include \"IO_HALDisplay.h\"  // Auxiliary display devices (LCD/OLED)\n//#include \"IO_HCSR04.h\"    // Ultrasonic range sensor\n//#include \"IO_VL53L0X.h\"   // Laser time-of-flight sensor\n//#include \"IO_DFPlayer.h\"  // MP3 sound player\n//#include \"IO_TouchKeypad.h  // Touch keypad with 16 keys\n//#include \"IO_EXTurntable.h\"   // Turntable-EX turntable controller\n//#include \"IO_EXFastClock.h\"  // FastClock driver\n//#include \"IO_PCA9555.h\"     // 16-bit I/O expander (NXP & Texas Instruments).\n//#include \"IO_I2CDFPlayer.h\" // DFPlayer over I2C\n\n//==========================================================================\n// The function halSetup() is invoked from CS if it exists within the build.\n// The setup calls are included between the open and close braces \"{ ... }\".\n// Comments (lines preceded by \"//\") are optional.\n//==========================================================================\n\nvoid halSetup() {\n\n  //=======================================================================\n  // The following directives define auxiliary display devices.\n  // These can be defined in addition to the system display (display\n  // number 0) that is defined in config.h.\n  // A write to a line which is beyond the length of the screen will overwrite\n  // the bottom line, unless the line number is 255 in which case the \n  // screen contents will scroll up before the text is written to the\n  // bottom line.\n  //=======================================================================\n  // \n  // Create a 128x32 OLED display device as display number 1 \n  // (line 0 is written by EX-RAIL 'SCREEN(1, 0, \"text\")').\n\n  //HALDisplay<OLED>::create(1, 0x3d, 128, 32);\n\n  // Create a 20x4 LCD display device as display number 2 \n  // (line 0 is written by EX-RAIL 'SCREEN(2, 0, \"text\")').\n\n  // HALDisplay<LiquidCrystal>::create(2, 0x27, 20, 4);\n\n\n  //=======================================================================\n  // User Add-ins\n  //=======================================================================\n  // User add-ins can be created when you want to do something that \n  // can't be done in EX-RAIL but does not merit a HAL driver.  The \n  // user add-in is a C++ function that is executed periodically by the\n  // HAL subsystem.\n\n  // Example: The function will be executed once per second and will display, \n  // on screen #3, the first eight entries (assuming an 8-line display)\n  // from the loco speed table.\n  \n  // Put the following block of code in myHal.cpp OUTSIDE of the \n  //   halSetup() function:\n  //\n  // void updateLocoScreen() {\n  //   for (int i=0; i<8; i++) {\n  //     if (DCC::speedTable[i].loco > 0) {\n  //       int speed = DCC::speedTable[i].speedCode;\n  //       char direction = (speed & 0x80) ? 'R' : 'F';\n  //       speed = speed & 0x7f;\n  //       if (speed > 0) speed = speed - 1;\n  //       SCREEN(3, i, F(\"Loco:%4d %3d %c\"), DCC::speedTable[i].loco,\n  //         speed, direction);\n  //     }\n  //   }\n  // }\n  //\n  // Put the following line INSIDE the halSetup() function:\n  //\n  // UserAddin::create(updateLocoScreen, 1000);\n  //\n\n\n  //=======================================================================\n  // The following directive defines a PCA9685 PWM Servo driver module.\n  //=======================================================================\n  // The parameters are: \n  //   First Vpin=100\n  //   Number of VPINs=16 (numbered 100-115)\n  //   I2C address of module=0x40\n\n  //PCA9685::create(100, 16, 0x40);\n\n\n  //=======================================================================\n  // The following directive defines an MCP23017 16-port I2C GPIO Extender module.\n  //=======================================================================\n  // The parameters are: \n  //   First Vpin=196\n  //   Number of VPINs=16 (numbered 196-211)\n  //   I2C address of module=0x22\n\n  //MCP23017::create(196, 16, 0x22);\n\n\n  // Alternative form, which allows the INT pin of the module to request a scan\n  // by pulling Arduino pin 40 to ground.  Means that the I2C isn't being polled\n  // all the time, only when a change takes place. Multiple modules' INT pins\n  // may be connected to the same Arduino pin.\n\n  //MCP23017::create(196, 16, 0x22, 40);\n\n\n  //=======================================================================\n  // The following directive defines an MCP23008 8-port I2C GPIO Extender module.\n  //=======================================================================\n  // The parameters are: \n  //   First Vpin=300\n  //   Number of VPINs=8 (numbered 300-307)\n  //   I2C address of module=0x22\n\n  //MCP23008::create(300, 8, 0x22);\n\n\n  //=======================================================================\n  // The following directive defines a PCF8574 8-port I2C GPIO Extender module.\n  //=======================================================================\n  // The parameters are: \n  //   First Vpin=200\n  //   Number of VPINs=8 (numbered 200-207)\n  //   I2C address of module=0x23\n\n  //PCF8574::create(200, 8, 0x23);\n\n\n  // Alternative form using INT pin (see above)\n\n  //PCF8574::create(200, 8, 0x23, 40);\n\n  // Alternative form to initialize 8 pins as output\n  //  INT pin -1, when INT is not used\n\n  //PCF8574::create(200, 8, 0x23, -1, 255);  8 pins High\n  //PCF8574::create(200, 8, 0x23, -1, 0);    8 pins Low\n  //PCF8574::create(200, 8, 0x23, -1, 0b11000010);  \n  //                       pins listed sequentially from 7 to 0\n  \n  //=======================================================================\n  // The following directive defines a PCF8575 16-port I2C GPIO Extender module.\n  //=======================================================================\n  // The parameters are: \n  //   First Vpin=200\n  //   Number of VPINs=16 (numbered 200-215)\n  //   I2C address of module=0x23\n\n  //PCF8575::create(200, 16, 0x23);\n\n\n  // Alternative form using INT pin (see above)\n\n  //PCF8575::create(200, 16, 0x23, 40);\n\n  //=======================================================================\n  // The following directive defines an HCSR04 ultrasonic ranging module.\n  //=======================================================================\n  // The parameters are: \n  //   Vpin=2000 (only one VPIN per directive)\n  //   Number of VPINs=1\n  //   Arduino pin connected to TRIG=30\n  //   Arduino pin connected to ECHO=31\n  //   Minimum trigger range=20cm (VPIN goes to 1 when <20cm)\n  //   Maximum trigger range=25cm (VPIN goes to 0 when >25cm)\n  // Note: Multiple devices can be configured by using a different ECHO pin\n  // for each one.  The TRIG pin can be shared between multiple devices.\n  // Be aware that the 'ping' of one device may be received by another\n  // device and position them accordingly!\n\n  //HCSR04::create(2000, 30, 31, 20, 25);\n  //HCSR04::create(2001, 30, 32, 20, 25);\n\n\n  //=======================================================================\n  // The following directive defines a single VL53L0X Time-of-Flight range sensor.\n  //=======================================================================\n  // The parameters are:\n  //   VPIN=5000\n  //   Number of VPINs=1\n  //   I2C address=0x29 (default for this chip)\n  //   Minimum trigger range=200mm (VPIN goes to 1 when <20cm)\n  //   Maximum trigger range=250mm (VPIN goes to 0 when >25cm)\n\n  //VL53L0X::create(5000, 1, 0x29, 200, 250); \n\n  // For multiple VL53L0X modules, add another parameter which is a VPIN connected to the\n  // module's XSHUT pin.  This allows the modules to be configured, at start,\n  // with distinct I2C addresses.  In this case, the address 0x29 is only used during\n  // initialisation to configure each device in turn with the desired unique I2C address.\n  // The examples below have the modules' XSHUT pins connected to the first two pins of \n  // the first MCP23017 module (164 and 165), but Arduino pins may be used instead.\n  // The first module here is given I2C address 0x30 and the second is 0x31.\n\n  //VL53L0X::create(5000, 1, 0x30, 200, 250, 164); \n  //VL53L0X::create(5001, 1, 0x31, 200, 250, 165); \n\n\n  //=======================================================================\n  // Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module.\n  //=======================================================================\n  // Parameters: \n  //   10000 = first VPIN allocated.\n  //   10 = number of VPINs allocated.\n  //   Serial1 = name of serial port (usually Serial1 or Serial2).\n  // With these parameters, up to 10 files may be played on pins 10000-10009.\n  // Play is started from EX-RAIL with SET(10000) for first mp3 file, SET(10001)\n  // for second file, etc.  Play may also be initiated by writing an analogue\n  // value to the first pin, e.g. ANOUT(10000,23,0,0) will play the 23rd mp3 file.\n  // ANOUT(10000,23,30,0) will do the same thing, as well as setting the volume to \n  // 30 (maximum value).\n  // Play is stopped by RESET(10000) (or any other allocated VPIN).\n  // Volume may also be set by writing an analogue value to the second pin for the player, \n  // e.g. ANOUT(10001,30,0,0) sets volume to maximum (30).\n  // The EX-RAIL script may check for completion of play by calling WAITFOR(pin), which will only proceed to the\n  // following line when the player is no longer busy.\n  // E.g.\n  //    SEQUENCE(1)\n  //      AT(164)           // Wait for sensor attached to pin 164 to activate\n  //      SET(10003)        // Play fourth MP3 file\n  //      LCD(4, \"Playing\") // Display message on LCD/OLED\n  //      WAITFOR(10003)    // Wait for playing to finish\n  //      LCD(4, \"\")       // Clear LCD/OLED line \n  //      FOLLOW(1)         // Go back to start\n\n  // DFPlayer::create(10000, 10, Serial1);\n\n\n  //=======================================================================\n  // Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module on a SC16IS750/SC16IS752 I2C UART\n  //=======================================================================\n  // DFPlayer via NXP SC16IS752 I2C Dual UART.\n  // I2C address range 0x48 - 0x57\n  // \n  // Generic format: \n  // I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);\n  // Parameters:\n  // 1st vPin     : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)\n  // vPins        : Total number of virtual pins allocated (1 vPin is supported currently)\n  //                1st vPin for UART 0\n  // I2C Address  : I2C address of the serial controller, in 0x format\n  // xtal         : 0 for 1.8432Mhz, 1 for 14.7456Mhz\n  //\n  // The vPin is also a pin that can be read with the WAITFOR(vPin) command indicating if the DFPlayer has finished playing a track\n  //\n  \n  // I2CDFPlayer::create(10000,  1, 0x48, 1);\n  //\n  // Configuration example on a multiplexer\n  // I2CDFPlayer::create(10000, 1, {I2CMux_0, SubBus_0, 0x48}, 1);\n\n\n\n  //=======================================================================\n  // 16-pad capacitative touch key pad based on TP229 IC.\n  //=======================================================================\n  // Parameters below:\n  //   11000 = first VPIN allocated\n  //   16 = number of VPINs allocated\n  //   25 = local GPIO pin number for clock signal\n  //   24 = local GPIO pin number for data signal\n  //\n  // Pressing the key pads numbered 1-16 cause each of the nominated digital VPINs \n  // (11000-11015 in this case) to be activated.\n\n  // TouchKeypad::create(11000, 16, 25, 24);\n\n\n  //=======================================================================\n  // The following directive defines an EX-Turntable turntable instance.\n  //=======================================================================\n  // EXTurntable::create(VPIN, Number of VPINs, I2C Address)\n  //\n  // The parameters are:\n  //   VPIN=600\n  //   Number of VPINs=1 (Note there is no reason to change this)\n  //   I2C address=0x60\n  //\n  // Note that the I2C address is defined in the EX-Turntable code, and 0x60 is the default.\n\n  //EXTurntable::create(600, 1, 0x60);\n\n\n  //=======================================================================\n  // The following directive defines an EX-IOExpander instance.\n  //=======================================================================\n  // EXIOExpander::create(VPIN, Number of VPINs, I2C Address)\n  //\n  // The parameters are:\n  //   VPIN=an available Vpin\n  //   Number of VPINs=pin count (must match device in use as per documentation)\n  //   I2C address=an available I2C address (default 0x65)\n  //\n  // Note that the I2C address is defined in the EX-IOExpander code, and 0x65 is the default.\n  // The example is for an Arduino Nano.\n\n  //EXIOExpander::create(800, 18, 0x65);\n\n\n  //=======================================================================\n  // The following directive defines a rotary encoder instance.\n  //=======================================================================\n  // The parameters are: \n  //   firstVpin = First available Vpin to allocate\n  //   numPins= Number of Vpins to allocate, can be either 1 to 3\n  //   i2cAddress = Available I2C address (default 0x67)\n\n  //RotaryEncoder::create(firstVpin, numPins, i2cAddress);\n  //RotaryEncoder::create(700, 1, 0x67);\n  //RotaryEncoder::create(700, 2, 0x67);\n  //RotaryEncoder::create(700, 3, 0x67);\n\n //=======================================================================\n  // The following directive defines an EX-FastClock instance.\n  //=======================================================================\n  // EXFastCLock::create(I2C Address)\n  //\n  // The parameters are:\n  //   \n  //   I2C address=0x55 (decimal 85)\n  //\n  // Note that the I2C address is defined in the EX-FastClock code, and 0x55 is the default.\n\n \n  //   EXFastClock::create(0x55);\n\n}\n\n#endif\n"
  },
  {
    "path": "myTrackStatus.example.h",
    "content": "// ************* OLED JL Display Track mA Amperage  **************** //\n// by Herb Morton    [Ash]                          March 23, 2025 \n//    Colin Murdoch  [ColinM] \n\n//  Inside your config.h file First edit OLED max char rows set to 17\n// #define MAX_CHARACTER_ROWS 17\n\n// myAutomation.h\n// Reporting power status and mA for each track on the LCD\nHAL(Bitmap,8236,1) // create flag 8236\nAUTOSTART DELAY(5000) \n ROUTE(\"TRACKSTATUS\"_hk, \"Resume/Pause JL Display\")\n  IF(8236) \n    RESET(8236)\n     ROUTE_CAPTION(\"TRACKSTATUS\"_hk, \"Paused\") ROUTE_INACTIVE(\"TRACKSTATUS\"_hk)\n      SCREEN(0, 8, \"Track status paused\")\n      SCREEN(0, 9, \"\")\n      SCREEN(0,10, \"\")   // several blank lines as needed\n      SCREEN(0,11, \"\")\n      SCREEN(0,12, \"\")\n      SCREEN(0,13, \"\")\n      SCREEN(0,14, \"\")\n      SCREEN(0,15, \"\")\n      SCREEN(0,16, \"\")\n      PRINT(\"to pause/resume: </START TRACKSTATUS> \\n\")\n    DONE ENDIF\n  SET(8236) \n   ROUTE_CAPTION(\"TRACKSTATUS\"_hk, \"Running\") ROUTE_ACTIVE(\"TRACKSTATUS\"_hk)\n    PRINT(\"Resume JL Display\")\n   FOLLOW(\"PAUSETRACKSTATUS\"_hk)\n  SEQUENCE(\"PAUSETRACKSTATUS\"_hk)\n   PARSE(\"<JL 0 8>\")  // screen 0  start on line 8\n    PRINT(\"to pause/resume: </START TRACKSTATUS> \\n\")\n    DELAY(3000)\n  IF(8236) FOLLOW(\"PAUSETRACKSTATUS\"_hk) ENDIF\n  DONE\n// ************ End OLED JL Display Track mA Amperage ************** //\n\n// Display motor shield after 10 seconds\nAUTOSTART\n  DELAY(10000)\n  STEALTH(StringFormatter::lcd(1, F(\"MS: %S\"), DCC::getMotorShieldName());)\nDONE\n"
  },
  {
    "path": "objdump.bat",
    "content": "ECHO ON\nFOR /F \"delims=\" %%i IN ('dir %TMP%\\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i\nSET DUMP=%TEMP%\\OBJDUMP.txt\necho Most recent subfolder: %ELF% >%DUMP%\n\nset PATH=\"C:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr\\bin\\\";%PATH%\navr-objdump --private=mem-usage  %ELF%  >>%DUMP%\nECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%\navr-objdump -x -C %ELF% | find \".text\" | sort /+25 /R >>%DUMP%\nECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%\navr-objdump -x -C %ELF% | find \".data\" | sort /+25 /R >>%DUMP%\nECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%\navr-objdump -x -C %ELF% | find \".bss\" | sort /+25 /R >>%DUMP%\nECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%\navr-objdump -D -S %ELF%  >>%DUMP%\n%DUMP%\nEXIT\n"
  },
  {
    "path": "objdump.sh",
    "content": "#!/bin/bash\n\n# Find avr-objdump that matches the installed arduino binary\nARDUINOBIN=$(ls -l $(type -p arduino)| awk '{print $NF ; exit 0}')\nPATH=$(dirname \"$ARDUINOBIN\")/hardware/tools/avr/bin:$PATH\n\nLASTBUILD=$(ls -tr /tmp/arduino_build_*/*.ino.elf | tail -1)\navr-objdump  --private=mem-usage  \"$LASTBUILD\"\n\nfor segment in .text .data .bss ; do\n    echo '++++++++++++++++++++++++++++++++++'\n    avr-objdump  -x -C \"$LASTBUILD\" | awk '$2 == \"'$segment'\" && $3 != 0 {print $3,$2} ; $4 == \"'$segment'\" && $5 != 0 { print $5,$6}' | sort -r\ndone\n"
  },
  {
    "path": "platformio.ini",
    "content": "; PlatformIO Project Configuration File\n;\n;   Build options: build flags, source filter\n;   Upload options: custom upload port, speed and extra flags\n;   Library options: dependencies, extra library storages\n;   Advanced options: extra scripting\n;\n; Please visit documentation for the other options and examples\n; https://docs.platformio.org/page/projectconf.html\n\n[platformio]\ndefault_envs = \n\tmega2560\n;\tuno\n;\tnano\n\tESP32\n\tNucleo-F411RE\n\tNucleo-F446RE\n\tNucleo-F429ZI\nsrc_dir = .\ninclude_dir = .\n\n[env]\nbuild_flags = -Wall -Wextra\n; monitor_filters = time\n\n[env:samd21-dev-usb]\nplatform = atmelsam\nboard = sparkfun_samd21_dev_usb\nframework = arduino\nupload_protocol = sam-ba\nlib_deps = ${env.lib_deps}\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -std=c++17 ${env.build_flags}\n\n[env:samd21-zero-usb]\nplatform = atmelsam\nboard = zeroUSB\nframework = arduino\nupload_protocol = sam-ba\nlib_deps = ${env.lib_deps}\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -std=c++17 ${env.build_flags}\n\n[env:Arduino-M0]\nplatform = atmelsam\nboard = mzeroUSB\nframework = arduino\nlib_deps = ${env.lib_deps}\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -std=c++17 ${env.build_flags}\n\n[env:mega2560-debug]\nplatform = atmelavr\nboard = megaatmega2560\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES ${env.build_flags}\n\n[env:mega2560-no-HAL]\nplatform = atmelavr\nboard = megaatmega2560\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -DIO_NO_HAL ${env.build_flags}\n\n[env:mega2560-I2C-wire]\nplatform = atmelavr\nboard = megaatmega2560\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -DI2C_USE_WIRE ${env.build_flags}\n\n[env:mega2560]\nplatform = atmelavr\nboard = megaatmega2560\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\n\nlib_ignore = WiFi101\n\t\t\tWiFi101_Generic\n\t\t\tWiFiEspAT\n\t\t\tWiFiMulti_Generic\n\t\t\tWiFiNINA_Generic\n\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = ${env.build_flags}\n\n[env:mega2560-eth]\nplatform = atmelavr\nboard = megaatmega2560\nframework = arduino\nlib_deps = \n    ${env.lib_deps}\n    arduino-libraries/Ethernet\n    SPI\nlib_ignore = WiFi101\n            WiFi101_Generic\n            WiFiEspAT\n            WiFiMulti_Generic\n            WiFiNINA_Generic\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = ${env.build_flags}\n\n[env:mega328]\nplatform = atmelavr\nboard = uno\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags= ${env.build_flags}\n\n[env:unowifiR2]\nplatform = atmelmegaavr\nboard = uno_wifi_rev2\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = ${env.build_flags} \"-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200\"\n\n[env:nanoevery]\nplatform = atmelmegaavr\nboard = nano_every\nframework = arduino\nlib_deps = \n\t${env.lib_deps}\n\tarduino-libraries/Ethernet\n\tSPI\nmonitor_speed = 115200\nmonitor_echo = yes\nupload_speed = 19200\nbuild_flags = ${env.build_flags}\n\n[env:uno]\nplatform = atmelavr\nboard = uno\nframework = arduino\nlib_deps = ${env.lib_deps}\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -mcall-prologues ${env.build_flags}\n\n[env:nano]\nplatform = atmelavr\nboard = nanoatmega328new\nboard_upload.maximum_size = 32256\nframework = arduino\nlib_deps = ${env.lib_deps}\nmonitor_speed = 115200\nmonitor_echo = yes\nbuild_flags = -mcall-prologues ${env.build_flags}\n\n[env:ESP32]\n; Lock version to 6.7.0 as that is\n; Arduino v2.0.16 (based on IDF v4.4.7)\n; which is the latest version based\n; on IDF v4. We can not use IDF v5.\nplatform = espressif32 @ 6.7.0\nboard = esp32dev\nframework = arduino\nlib_deps = ${env.lib_deps}\nbuild_flags = -std=c++17 ${env.build_flags}\nmonitor_speed = 115200\nmonitor_echo = yes\n\n[env:Nucleo-F411RE]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f411re\nframework = arduino\nlib_deps = ${env.lib_deps}\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nmonitor_speed = 115200\nmonitor_echo = yes\n\n[env:Nucleo-F446RE]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f446re\nframework = arduino\nlib_deps = ${env.lib_deps}\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nmonitor_speed = 115200\nmonitor_echo = yes\n\n; Experimental - no reason this should not work, but not\n; tested as yet\n;\n[env:Nucleo-F401RE]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f401re\nframework = arduino\nlib_deps = ${env.lib_deps}\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nmonitor_speed = 115200\nmonitor_echo = yes\n\n; Commented out by default as the F13ZH has variant files\n; but NOT the nucleo_f413zh.json file which needs to be\n; installed before you can let PlatformIO see this\n;\n; [env:Nucleo-F413ZH]\n; platform = ststm32 @ 19.0.0\n; board = nucleo_f413zh\n; framework = arduino\n; lib_deps = ${env.lib_deps}\n; build_flags = -std=c++17  -Os -g2 -Wunused-variable\n; monitor_speed = 115200\n; monitor_echo = yes\n\n; Commented out by default as the F446ZE needs variant files\n; installed before you can let PlatformIO see this\n;\n[env:Nucleo-F446ZE]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f446ze\nframework = arduino\nlib_deps = ${env.lib_deps}\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nmonitor_speed = 115200\nmonitor_echo = yes\n\n; Commented out by default as the F412ZG needs variant files\n; installed before you can let PlatformIO see this\n;\n; [env:Nucleo-F412ZG]\n; platform = ststm32 @ 19.0.0\n; board = nucleo_f412zg\n; framework = arduino\n; lib_deps = ${env.lib_deps}\n; build_flags = -std=c++17 -Os -g2 -Wunused-variable\n; monitor_speed = 115200\n; monitor_echo = yes\n; upload_protocol = stlink\n\n; Experimental - Ethernet beta test\n;\n[env:Nucleo-F429ZI]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f429zi\nframework = arduino\nlib_deps = ${env.lib_deps}\n\t\t\tstm32duino/STM32Ethernet @ ^1.4.0\n; for test only         https://github.com/habazut/lwip#stm32\n\t\t\tstm32duino/STM32duino LwIP @ ^2.1.3\nlib_ignore = WiFi101\n\t\t\tWiFi101_Generic\n\t\t\tWiFiEspAT\n\t\t\tWiFiMulti_Generic\n\t\t\tWiFiNINA_Generic\nbuild_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS\nmonitor_speed = 115200\nmonitor_echo = yes\nupload_protocol = stlink\n\n; Experimental - Ethernet beta test\n;\n[env:Nucleo-F439ZI]\nplatform = ststm32 @ 19.0.0\nboard = nucleo_f439zi\nframework = arduino\nlib_deps = ${env.lib_deps}\n\t\t\tstm32duino/STM32Ethernet @ ^1.4.0\n\t\t\tstm32duino/STM32duino LwIP @ ^2.1.3\nlib_ignore = WiFi101\n\t\t\tWiFi101_Generic\n\t\t\tWiFiEspAT\n\t\t\tWiFiMulti_Generic\n\t\t\tWiFiNINA_Generic\nbuild_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS\nmonitor_speed = 115200\nmonitor_echo = yes\nupload_protocol = stlink\n\n[env:Teensy3_2]\nplatform = teensy\nboard = teensy31\nframework = arduino\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nlib_deps = ${env.lib_deps}\nlib_ignore = NativeEthernet\n\n[env:Teensy3_5]\nplatform = teensy\nboard = teensy35\nframework = arduino\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nlib_deps = ${env.lib_deps}\nlib_ignore = NativeEthernet\n\n[env:Teensy3_6]\nplatform = teensy\nboard = teensy36\nframework = arduino\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nlib_deps = ${env.lib_deps}\nlib_ignore = NativeEthernet\n\n[env:Teensy4_0]\nplatform = teensy\nboard = teensy40\nframework = arduino\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nlib_deps = ${env.lib_deps}\nlib_ignore = NativeEthernet\n\n[env:Teensy4_1]\nplatform = teensy\nboard = teensy41\nframework = arduino\nbuild_flags = -std=c++17  -Os -g2 ${env.build_flags}\nlib_deps = ${env.lib_deps}\nlib_ignore =\n"
  },
  {
    "path": "release_notes.md",
    "content": "The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production Release.  Release v4.0.0 is a Major release that adds significant new product design, plus Automation features and bug fixes. The team continues improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors.  This release includes all of the Point Releases from v3.2.0 to v3.2.0 rc13.\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)\n\n\n\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v0.0.0-Prod/CommandStation-EX.tar.gz)\n\n**Known Issues**\n\n- **Wi-Fi** - Requires sending `<AT>` commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup\n- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitations in its current sensing circuitry\n\n**All New Major DCC++EX 4.0.0 features**\n\n- **New HAL Hardware Abstraction Layer API** that automatically detects and greatly simplifies interfacing to many predefined accessory boards for servos, signals & sensors and added I/O (digital and analog inputs and outputs, servos etc). \n- HAL Support for;\n  - MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules. \n  - PCA9685 PWM (servo & signal) control modules. \n  - Analogue inputs on Arduino pins and on ADS111x I2C modules. \n  - MP3 sound playback via DFPlayer module. \n  - HC-SR04 Ultrasonic range sensor module. \n  - VL53L0X Laser range sensor module (Time-Of-Flight). \n  - A new `<D HAL SHOW>` command to list the HAL devices attached to the command station\n \n**New Command Station Broadcast throttle logic**\n\n- Synchronizes multiple WiThrottles and PC based JMRI Throttles for direction, speed and F-key updates\n\n**New ‘Discovered Servers’ on WiFi Throttles**\n\n- Our New multicast Dynamic Network Server (mDNS) enhancement allows us to display the available WiFi server connections to a DCC++EX Command Station. Selecting it allows your WiThrottle App to connect to and load Server Rosters and function keys to your throttle from the new DCC++EX Command Station Server Roster.\n\n**New DCC++EX 4.0.0 with EX-RAIL Extended Railroad Automation Instruction Language**\n\n- Use to control your entire layout or as a separate accessory/animation controller\n- Awesome, cleverly powerful yet simple user friendly scripting language for user built Automation & Routing scripts. \n- You can control Engines, Sensors, Turnouts, Signals, Outputs and Accessories that are entered into your new myAutomation.h file, then uploaded into the DCC++EX Command Station.\n- EX-RAIL scripts are automatically displayed as Automation {Handoff} and Route {Set} buttons on supported WiFi Throttle Apps.\n\n**New EX-RAIL ‘Roster’ Feature**\n\n- List and store user defined engine roster & function keys inside the command station, and automatically load them in WiFi Throttle Apps.\n- When choosing “DCC++EX” from discovered servers an Engine Driver or WiThrottle is directly connected to the Command Station. \n- The EX-RAIL ’ROSTER’ command allows all the engine numbers, names and function keys you’ve listed in your myAutomation.h file to automatically upload the Command Station's ‘Server Roster’ into your Engine Driver and WiThrottle Apps.  \n\n**New JMRI 4.99.2 and above specific DCC++EX 4.0 features**\n\n- Enhanced JMRI DCC++ Configure Base Station pane for building and maintaining Sensor, Turnout and Output devices, or these can automatically be populated from the DCC++EX Command Station's mySetup.h file into JMRI.\n\n- JMRI now supports multiple serial connected DCC++EX Command Stations, to display and track separate \"Send DCC++ Command\" and \"DCC++ Traffic\" Monitors for each Command Station at the same time.\n  For example: Use an Uno DCC++EX DecoderPro Programming Station {DCC++Prg} on a desktop programming track and a second Mega DCC++EX EX-RAIL Command Station for Operations {DCC++Ops} on the layout with an additional `<JOINED>` programming spur or siding track for acquiring an engine and ‘Drive Away’ onto the mainline (see the DriveAway feature for more information).\n\n**DCC++EX 4.0.0 additional product enhancements**\n\n- Additional Motor Shields and Motor Board {boosters) supported\n- Additional Accessory boards supported for GPIO expansion, Sensors, Servos & Signals\n- Additional diagnostic commands like ‘D ACK RETRY’ and ‘D EXRAIL ON’ events, ‘D HAL SHOW’ devices and ‘D SERVO’ positions, and ‘D RESET’ the command station while maintaining the serial connection with JMRI\n- Automatic retry on failed ACK detection to give decoders another chance\n- New EX-RAIL ’/’ slash command allows JMRI to directly communicate with many EX-RAIL scripts\n- Turnout class revised to expand turnout capabilities and allow turnout names/descriptors to display in WiThrottle Apps.\n- Build turnouts through either or both mySetup.h and myAutomation.h files, and have them automatically passed to, and populate, JMRI Turnout Tables\n- Turnout user names display in Engine Driver & WiThrottles\n- Output class now allows ID > 255. \n- Configuration options to globally flip polarity of DCC Accessory states when driven from `<a>` command and `<T>` command.\n- Increased use of display for showing loco decoder programming information. \n- Can disable EEPROM memory code to allow room for DCC++EX 4.0 to fit on a Uno Command Station\n- Can define border between long and short addresses \n- Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms). \n- EEPROM layout change - deletes EEPROM contents on first start following upgrade. \n\n**4.0.0 Bug Fixes**\n\n- Compiles on Nano Every\n- Diagnostic display of ack pulses >32ms\n- Current read from wrong ADC during interrupt\n- AT(+) Command Pass Through \n- CiDAP WiFi Drop out and the WiThrottle F-key looping error corrected\n- One-off error in CIPSEND drop\n- Common Fault Pin Error\n- Uno Memory Utilization optimized\n\n#### Summary of Release 3.1.0 key features and/or bug fixes by Point Release\n\n**Summary of the key new features added to CommandStation-EX V3.0.16**\n\n- Ignore CV1 bit 7 read if rejected by a non NMRA compliant decoder when identifying loco id\n\n**Summary of the key new features added to CommandStation-EX V3.0.15**\n\n- Send function commands just once instead of repeating them 4 times\n\n**Summary of the key new features added to CommandStation-EX V3.0.14**\n\n- Add feature to tolerate decoders that incorrectly have gaps in their ACK pulse\n- Provide proper track power management when joining and unjoining tracks with <1 JOIN>\n\n**Summary of the key new features added to CommandStation-EX V3.0.13**\n\n- Fix for CAB Functions greater than 127\n\n**Summary of the key new features added to CommandStation-EX V3.0.12**\n\n- Fixed clear screen issue for nanoEvery and nanoWifi\n\n**Summary of the key new features added to CommandStation-EX V3.0.11**\n\n- Reorganized files for support of 128 speed steps\n\n**Summary of the key new features added to CommandStation-EX V3.0.10**\n\n- Added Support for the Teensy 3.2, 3.5, 3.6, 4.0 and 4.1 MCUs\n- No functional change just changes to avoid complier warnings for Teensy/nanoEvery\n\n**Summary of the key new features added to CommandStation-EX V3.0.9**\n\n- Rearranges serial newlines for the benefit of JMRI\n- Major update for efficiencies in displays (LCD, OLED)\n- Add I2C Support functions\n\n**Summary of the key new features added to CommandStation-EX V3.0.8**\n\n- Wraps <* *> around DIAGS for the benefit of JMRI\n\n**Summary of the key new features added to CommandStation-EX V3.0.7**\n\n- Implemented support for older 28 apeed step decoders - Option to turn on 28 step speed decoders in addition to 128. If set, all locos will use 28 steps.\n- Improved overload messages with raw values (relative to offset)\n\n**Summary of the key new features added to CommandStation-EX V3.0.6**\n\n- Prevent compiler warning about deprecated B constants\n- Fix Bug that did not let us transmit 5 byte sized packets - 5 Byte commands like PoM (programming on main) were not being sent correctly\n- Support for Huge function numbers (DCC BinaryStateControl) - Support Functions beyond F28\n- <!> ESTOP all - New command to emergency stop all locos on the main track\n- <- [cab]> estop and forget cab/all cabs - Stop and remove loco from the CS. Stops the repeating throttle messages\n- `<D RESET>` command to reboot Arduino\n- Automatic sensor offset detect\n- Improved startup msgs from Motor Drivers (accuracy and auto sense factors)\n- Drop post-write verify - No need to double check CV writes. Writes are now even faster.\n- Allow current sense pin set to UNUSED_PIN - No need to ground an unused analog current pin. Produce startup warning and callback -2 for prog track cmds.\n\n**Summary of the key new features added to CommandStation-EX V3.0.5**\n\n- Fix Fn Key startup with loco ID and fix state change for F16-28\n- Removed ethernet mac config and made it automatic\n- Show wifi ip and port on lcd\n- Auto load config.example.h with warning\n- Dropped example .ino files\n- Corrected .ino comments\n- Add Pololu fault pin handling\n- Waveform speed/simplicity improvements\n- Improved pin speed in waveform\n- Portability to nanoEvery and UnoWifiRev2 CPUs\n- Analog read speed improvements\n- Drop need for DIO2 library\n- Improved current check code\n- Linear command\n- Removed need for ArduinoTimers files\n- Removed option to choose different timer\n- Added EX-RAIL hooks for automation in future version\n- Fixed Turnout list\n- Allow command keywords in mixed case\n- Dropped unused memstream\n- PWM pin accuracy if requirements met\n\n**Summary of the key new features added to CommandStation-EX V3.0.4**\n\n- \"Drive-Away\" Feature - added so that throttles like Engine Driver can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track\n- WiFi Startup Fixes\n\n**Summary of the key new features added to CommandStation-EX V3.0.3**\n\n- Command to write loco address and clear consist\n- Command will allow for consist address\n- Startup commands implemented\n\n**Summary of the key new features added to CommandStation-EX V3.0.2:**\n\n- Create new output for current in mA for `<c>` command - New current response outputs current in mA, overlimit current, and maximum board capable current\n- Simultaneously update JMRI to handle new current meter\n\n**Summary of the key new features added to CommandStation-EX V3.0.1:**\n\n- Add back fix for jitter\n- Add Turnouts, Outputs and Sensors to `<s>` command output\n\n**CommandStation-EX V3.0.0:**\n\n**Release v3.0.0 was a major rewrite if earlier versions of DCC++.  The code base was re-architeced and core changes were made to the Waveform generator to reduce overhead and make better use of Arduino.** **Summary of the key new features added in Release v3.0.0 include:**\n\n- **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.\n- **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.\n- **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently.\n- **Add LCD/OLED support** - OLED supported on Mega only\n- **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.\n- **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers\n- **Individual track power control** - Ability to toggle power on either or both tracks, and to \"JOIN\" the tracks and make them output the same waveform for multiple power districts.\n- **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs\n- **New, simpler function command** - `<F>` command allows setting functions based on their number, not based on a code as in `<f>`\n- **Function reminders** - Function reminders are sent in addition to speed reminders\n- **Functions to F28** - All NMRA functions are now supported\n- **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)\n- **Diagnostic `<D>` commands** - See documentation for a full list of new diagnostic commands\n- **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM\n- **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers\n- **Rewritten packet generator** - Simplify and make smaller, remove idea of \"registers\" from original code\n- **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM\n- **Fix EEPROM bugs**\n- **Number of locos discovery command** - `<#>` command\n- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.\n- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.\n\n**Key Contributors**\n\n**Project Lead**\n\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n\n**CommandStation-EX Developers**\n\n- Chris Harlow - Bournemouth, UK (UKBloke)\n- Harald Barth - Stockholm, Sweden (Haba)\n- Neil McKechnie - Worcestershire, UK (NeilMck)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- M Steve Todd - Oregon, USA (MSteveTodd) \n- Scott Catalano - Pennsylvania\n- Gregor Baues - Île-de-France, France (grbba)\n\n**Engine Driver and JMRI Interface**\n\n- M Steve Todd\n\n**exInstaller Software**\n\n- Anthony W - Dayton, Ohio, USA (Dex, Dex++)\n\n**Website and Documentation**\n\n- Mani Kumar - Bangalor, India (Mani / Mani Kumar)\n- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)\n- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)\n- Roger Beschizza - Dorset, UK (Roger Beschizza)\n- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n- Colin Grabham - Central NSW, Australia (Kebbin)\n\n**WebThrotle-EX**\n\n- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)\n- Mani Kumar - Bangalor, India (Mani /Mani Kumar)\n- Matt H - Somewhere in Europe\n\n**Beta Testing / Release Management / Support**\n\n- Larry Dribin - Release Management\n- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)\n- Herb Morton - Kingwood Texas, USA (Ash++)\n- Keith Ledbetter\n- Brad Van der Elst\n- Andrew Pye\n- Mike Bowers\n- Randy McKenzie\n- Roberto Bravin\n- Sam Brigden\n- Alan Lautenslager\n- Martin Bafver\n- Mário André Silva\n- Anthony Kochevar\n- Gajanatha Kobbekaduwe\n- Sumner Patterson\n- Paul - Virginia, USA\n\n**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**\n\n[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)\n\n\n[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.tar.gz)\n"
  },
  {
    "path": "version.h",
    "content": "#ifndef version_h\n#define version_h\n\n#include \"StringFormatter.h\"\n\n#define VERSION \"5.6.0\"\n// 5.6.0  - Minor adaptions to make production version\n// 5.5.68 - EXRAIL: BITMAP_SET\n//        - EXRAIL: Comments corrections for doc build\n//        - RailCom: Simplification for sniffer-based detector implementation      \n// 5.5.67 - RailCom, AVR: Correct cutout timer calculation\n//        - RailCom: Improved block handling\n//        - EXRAIL: new IF_ALL,IF_ANY\n// 5.5.66 - Bugfix: NeoPixel buffer size for RGBW devices\n//        - <D WIFI/ETHERNET ON> for SerialUsbLog diagnostics\n// 5.5.65 - Enable ETHERNET_HOST_NAME (defaults to WIFI_HOST_NAME)\n//        - SerialUsbLog restructure \n// 5.5.64 - I2C scan included in <D HAL SHOW>\n// 5.5.63 - Bugfix: EXRAIL IFLOCO did not handle -1 (loco not found)\n// 5.5.62 - EXRAIL IFLOCO(list)\n//        - NEOPIXEL automatic WHITE(if available)  when red=green=blue\n// 5.5.61 - Bugfix: PCA9685 driver did not handle mux I2C addresses for writing registers\n// 5.5.60 - EXRAIL SPEEDUP(by) SLOWDOWN(by) SPEED_REL(percent)\n//        - EXRAIL ZTEST diff between cmds and test captions \n//        - Tuning DFPlayer base timing.  \n// 5.5.59 - Bugfix buffer size for consist feedback\n//        - <!Q> command to query estop lock status\n// 5.5.58 - Freemem in 4k blocks on 32bit cpus\n//        - newline fix for Engine Driver\n// 5.5.57 - XL9535 HAL driver\n//        - ESTOP LOCK feedback to throttles\n// 5.5.56 - ESTOP_PAUSE and ESTOP_RESUME functions/commands\n//        - DFPlayerI2C auto detect UART crystal frequency\n//        - DFPlayer PAUSE and RESUME commands, and EQ names\n//        - TCA9555 alias of PCA9555\n// 5.5.55 - Bugfix: Real time clock i2c address recognition\n//        - DFPlayer redesign to amalgamate Serial and i2c drivers\n//        - DFPlayer <y> commands and EXRAIL PLAY_ macros\n//        - EXRAIL Documentation spelling corrections  \n// 5.5.54 - Bugfix: Allow longer serial payload length in parser on 32 bit arch, save RAM on AVR\n// 5.5.53 - Bugfix: EXRAIL Asserts pbSize calculation error for pin blacklist\n// 5.5.52 - Command station consists <^> and EXRAIL BUILD_CONSIST/BREAK_COMNSIST\n// 5.5.51 - Bugfix: EXRAIL failed TURNTABLE create commands (I2C off) can crash CS\n//        - Bugfix: EXRAIL be extra careful not to deref nullptr\n// 5.5.50 - Replace the SC power status with something better\n//        - EXRAIL RANDOM_CALL, RANDOM_FOLLOW\n// 5.5.49 - EXRAIL </> displays wait_while_red state\n//        - EXRAIL SAVE_SPEED,RESTORE_SPEED,XSAVE_SPEED,XRESTORE_SPEED macros\n// 5.4.48 - EXRAIL improvements:\n//        - IFROUTE_HIDDEN, IFROUTE_DISABLED, IFROUTE_ACTIVE, IFROUTE_INACTIVE macros\n//        - FREEALL and </FREEALL> to free all reserves\n//        - WAIT_WHILE_RED macro to wait at red signals\n//        - START_SHARED nd START_SEND macros to share or transfer locos to new tasks\n// 5.5.47 - Make it possible to turn off max ack duration check\n//        - Wifi: Make it possible to hide the SSID\n//        - New trainbrains debug option\n//        - ESP32: SSID named \"OFF\" will turn all WIFI off\n//        - DCCEX protocol change: <#> returns <# N> where N is the number of presumably empty slots for mobile decoders\n//        - Bugfix: Race condition with RMTChannel::dataRepeat causes packets to be lost #460\n//        - Handle loco table full\n// 5.5.46 - Major rewrite of loco table \n// 5.5.45 - Incoming <@ row col \"text\"> command\n//        - Bugfix: <t cab> for unknown loco\n//        - new <R LOCOID> and <R CONSIST> commands\n//        - new EXRAIL IFSTASHED_HERE(stashId) function  \n// 5.5.44 - Config simplification (default obvious values)\n//        - Config will no longer include config.example.h\n//        - Bugfix: DS1307 serial when clock not set\n//        - Bugfix: I2C speed set in some drivers\n// 5.5.43 - Upgraded Trainbrains HAL driver \n// 5.5.42 - Bugfix: ESP32 WIFI AP mode was not always selected when wanted\n// 5.5.41 - Bugfix: Toolbox not showing sensors\n//        - MAX_LOCOS can be defined in config.h\n// 5.5.40 - Bugfix: EXSensorCAM.h use check byte for i2c error detection\n// 5.5.39 - Remove of unused EX-CommandStation-installer.exe\n//        - Bugfix: Sniffer and reminder table overflow\n// 5.5.38 - Bugfix: restore EXRAIL/ENDEXRAIL obsolete macros as no-operations\n//        - RAM Saver feature: JMRI_SENSOR and JMRI_SENSOR_NOPULLUP\n// 5.5.37 - Bugfix: Keep power status of track when doing join/unjoin, new keep power option for setTrackMode()\n// 5.5.36 - make more diag conditional\n//        - Bugfix: SSD1309 OLED controllers\n// 5.5.35 - BUGFIX DCC Packet Queue timing leak with single loco\n// 5.5.34 - STM32: Remove I2C interrupt blocking waveform interrupt\n//        - Bugfix: Negative route Ids\n// 5.5.33 - Fix CONFIG_SERVO when default PCA9685 definition used.\n// 5.5.32 - Feature: Enable sniffer on CSB-1\n// 5.5.31 - <JL screen startRow> track status command\n//        - myTrackStatus.example.h added\n//        - Provide for WiFi false on ESP32\n// 5.5.30 - EXRAIL </> shows why tasks are waiting \n// 5.5.29 - Resolved compiler warnings \n// 5.5.28 - DCC Queue memory leak fix\n// 5.5.27 - PCF8574 output pin initialization parameter\n// 5.5.26 - PCA9554 and TCA9554/9534 I2C 8-bit GPIO expander drivers\n// 5.2.25 - IO_Bitmap and assicated Exrail macros\n// 5.5.24 - SensorCAM in I2C scan and automatically setClock\n// 5.5.23 - Reminder loop Idle packet optimization\n// 5.5.22 - (5.4.9) Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.\n//        - DCC 5mS gap to same loco DCC packet restriction\n//        - Catch up MASTER for ESP32 IDF version check\n//        - Catch up MASTER for Serial input length check\n//        - Catch up MASTER for JOIN/POWER order change\n// 5.5.21 - Backed out the broken merge with frequency change and \n// 5.5.20 - EXRAIL SET/RESET assert fix  \n// 5.5.19 - Railcom change to use RailcomCollector device  \n// 5.5.18 - New STASH internals \n//        - EXRAIL IFSTASH/CLEAR_ANY_STASH\n//        - <JM CLEAR ANY id> to clear any stash with loco id\n//        - See Release_Notes/Stash.md  \n// 5.5.17 - Extensive new compile time checking in exrail scripts (duplicate sequences etc), no function change \n// 5.5.16 - DOXYGEN comments in EXRAIL2MacroReset.h\n// 5.5.15 - Support for F429ZI/F329ZI\n//        - Own mDNS support for (wired) Ethernet\n// 5.5.14 - DCC Non-blocking packet queue with priority\n// 5.5.13 - Update STM32duino core to v19.0.0. for updated PeripheralPins.c in preparation for F429/439ZI Ethernet support \n// 5.5.12 - Websocket support (wifi only) \n// 5.5.11 - (5.4.2) accessory command reverse\n// 5.5.10 - CamParser fix\n// 5.5.9  - (5.4.3) fix changeFn for functions 29..31\n// 5.5.8  - EXSensorCam clean up to match other filters and\n//        - avoid need for config.h settings\n//        - Test: IO_I2CDFPlayer.h inserted 10mS deleay in Init_SC16IS752() just after soft-reset for board with 1.8432 Mhz xtal\n//        - IO_I2CDFPlayer.h: fixed 2 compiler errors as the compilers are getting stricter\n// 5.5.7  - ESP32 bugfix packet buffer race (as 5.4.1)\n// 5.5.6  - Fix ESP32 build bug caused by include reference loop\n// 5.5.5  - Railcom implementation with IO_I2CRailcom driver\n//        - response analysis and block management.\n//        - <r locoid cv>  POM read using Railcom.\n//        - See Release_notes/Railcom.md\n// 5.5.4  - Split ESP32 from DCCWaveform to DCCWaveformRMT\n//        - Railcom Cutout control (DCCTimerAVR Mega only so far) \n// 5.5.3  - EXRAIL ESTOPALL,XPOM, \n//        - Bugfix RESERVE to cause ESTOP.(was STOP)\n//        - Correct direction sync after manual throttle change. \n//        - plus ONBLOCKENTER/EXIT in preparation for Railcom\n// 5.5.2  - DS1307 Real Time clock \n// 5.5.1  - Momentum \n// 5.5.0  - New version on devel\n// 5.4.3  - bugfix changeFn for functions 29..31\n// 5.4.2  - Reversed turnout bugfix\n// 5.4.1  - ESP32 bugfix packet buffer race\n// 5.4.0  - New version on master\n// 5.2.96 - EXRAIL additions XFWD() and XREV()\n// 5.2.95 - Release candidate for 5.4\n// 5.2.94 - Bugfix: Less confusion and simpler code around the RCN213 defines\n// 5.2.93 - Bugfix ESP32: clear progTrackSyncMain (join flag) when prog track is removed\n// 5.2.92 - Bugfix: FADE power off fix, EXRAIL power diagnostic fix.\n// 5.2.91 - Bugfix: Neopixel I2C overlap check\n// 5.2.90 - Bugfix: EXRAIL EXTT_TURNTABLE() now has description as optional in line with ocumentation (also fixed DCC_TURNTABLE) \n// 5.2.89 - EXRAIL SET(vpin[,npins]) RESET(vpin,[,npins]) pin range manipulation \n// 5.2.88 - Fix bug where EX-Turntable objects return incorrect angle for home with <JP x>\n// 5.2.87 - CamParser and IO_EXSensorCam driver\n// 5.2.86 - IO_TCA8418 driver for keypad matrix input now fully functioning, including being able to use an interrupt pin\n// 5.2.85 - IO_TM1638 driver, SEG7 Exrail macro and _s7 segment pattern generator.\n// 5.2.84 - Fix TrackManager setDCCSignal and setPROGSignal for STM32 shadowing of PORTG/PORTH - this time it really is correct!\n// 5.2.83 - Various STM32 related fixes for serial ports, I2C pullups now turned off, and shadowing of PORTG/PORTH for TrackManager now correct\n// 5.2.82 - TrackManager and EXRAIL: Introduce more consistent names for <= ...> and SET_TRACK\n// 5.2.81 - STM32 Ethernet boards support, also now have specific EX8874 motor driver definition\n// 5.2.80 - EthernetInterface upgrade, including STM32 Ethernet support\n// 5.2.79 - serial manager loop that handles quoted strings\n//        - WiFiESP32 reconfig\n// 5.2.78 - NeoPixel support.\n//        - <o command\n//        - HAL driver \n//        - EXRAIL NEOPIXEL and NEOPIXEL_SIGNAL   \n// 5.2.77 - Withrottle: Implement \"force function\" subcommand \"f\"\n// 5.2.76 - Bugfix: EXRAIL: Catch CV read errors in the callback\n// 5.2.75 - Bugfix: Serial lines 4 to 6 OK\n// 5.2.74 - Bugfix: ESP32 turn on the joined prog (as main) again after a prog operation\n// 5.2.73 - Bugfix: STM32 further fixes to shadowPORT entries in TrackManager.cpp for PORTG and PORTH\n// 5.2.72 - Bugfix: added shadowPORT entries in TrackManager.cpp for PORTG and PORTH on STM32, fixed typo in MotorDriver.cpp\n// 5.2.71 -  Broadcasts of loco forgets. \n// 5.2.70 -  IO_RocoDriver renamed to IO_EncoderThrottle.\n//        -  and included in IODEvice.h (circular dependency removed) \n// 5.2.69 -  IO_RocoDriver. Direct drive train with rotary encoder hw.\n// 5.2.68 -  Revert function map to signed (from 5.2.66) to avoid\n//           incompatibilities with ED etc for F31 frequency flag.\n// 5.2.67 -  EXRAIL AFTER optional debounce time variable (default 500mS)\n//        -  AFTER(42) == AFTER(42,500) sets time sensor must \n//        -  be continuously off.\n// 5.2.66 - <F cab DCFREQ 0..3>\n//        - EXRAIL SETFREQ drop loco param (breaking since 5.2.28)\n// 5.2.65 - Speedup Exrail SETFREQ \n// 5.2.64 - Bugfix: <0 PROG> updated to undo JOIN \n// 5.2.63 - Implement WIFI_LED for ESP32, ESPduino32 and EX-CSB1, that is turned on when STA mode connects or AP mode is up\n//        - Add BOOSTER_INPUT definitions for ESPduino32 and EX-CSB1 to config.example.h\n//        - Add WIFI_LED definitions for ESPduino32 and EX-CSB1 to config.example.h\n// 5.2.62 - Allow acks way longer than standard\n// 5.2.61 - Merg CBUS  ACON/ACOF/ONACON/ONACOF Adapter interface.\n//        - LCC Adapter interface throttled startup,\n//          (Breaking change with Adapter base code)\n// 5.2.60 - Bugfix: Opcode AFTEROVERLOAD does not have an argument that is a pin and needs to be initialized\n//        - Remove inrush throttle after half good time so that we go to mode overload if problem persists\n// 5.2.59 - STM32 bugfix correct Serial1 definition for Nucleo-F401RE\n//        - STM32 add support for ARDUINO_NUCLEO_F4X9ZI type to span F429/F439 in upcoming STM32duino release v2.8 as a result of our PR\n// 5.2.58 - EXRAIL ALIAS allows named pins\n// 5.2.57 - Bugfix autoreverse: Apply mode by binart bit match and not by equality\n// 5.2.56 - Bugfix and refactor for EXRAIL getSignalSlot\n// 5.2.55 - Move EXRAIL isSignal() to public to allow use in STEALTH call\n// 5.2.54 - Bugfix for EXRAIL signal handling for active high \n// 5.2.53 - Bugfix for EX-Fastclock, call I2CManager.begin() before checking I2C address \n// 5.2.52 - Bugfix for ADCee() to handle ADC2 and ADC3 channel inputs on F446ZE and others\n//        - Add support for ports G and H on STM32 for ADCee() and MotorDriver pins/shadow regs \n// 5.2.51 - Bugfix for SIGNAL: Distinguish between sighandle and sigid\n// 5.2.50 - EXRAIL ONBUTTON/ONSENSOR observe LATCH\n// 5.2.49 - EXRAIL additions:\n//          ONBUTTON, ONSENSOR\n// 5.2.48 - Bugfix: HALDisplay was generating I2C traffic prior to I2C being initialised\n// 5.2.47 - EXRAIL additions:\n//          STEALTH_GLOBAL\n//          BLINK\n//          TOGGLE_TURNOUT\n//          FTOGGLE, XFTOGGLE\n//          Reduced code-developmenmt DIAG noise\n// 5.2.46 - Support for extended consist CV20 in <R> and <W id>\n//        - New cmd <W CONSIST id [REVERSE]> to handle long/short consist ids\n// 5.2.45 - ESP32 Trackmanager reset cab number to 0 when track is not DC\n//          ESP32 fix PWM LEDC inverted pin mode\n//          ESP32 rewrite PWM LEDC to use pin mux\n// 5.2.42 - ESP32 Bugfix: Uninitialized stack variable\n// 5.2.41 - Update rotary encoder default address to 0x67\n// 5.2.40 - Allow no shield\n// 5.2.39 - Functions for DC frequency: Use func up to F31\n// 5.2.38 - Exrail MESSAGE(\"text\") to send a user message to all \n//          connected throttles (uses <m \"text\"> and  withrottle Hmtext.\n// 5.2.37 - Bugfix ESP32: Use BOOSTER_INPUT define\n// 5.2.36 - Variable frequency for DC mode\n// 5.2.35 - Bugfix: Make DCC Extended Accessories follow RCN-213\n// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories\n//        - Exrail ASPECT(address,aspect) for above.\n//        - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)\n//        - Exrail intercept <A ...> for DCC Signals. \n// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)\n// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)\n// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.  \n// 5.2.30 - Bugfix: WiThrottle sendIntro after initial N message as well\n// 5.2.29 - Added IO_I2CDFPlayer.h to support DFPLayer over I2C connected to NXP SC16IS750/SC16IS752 (currently only single UART for SC16IS752)\n//        - Added enhanced IO_I2CDFPLayer enum commands to EXRAIL2.h\n//        - Added PLAYSOUND alias of ANOUT to EXRAILMacros.h\n//        - Added UART detection to I2CManager.cpp\n// 5.2.28 - ESP32: Can all Wifi channels.\n//        - ESP32: Only write Wifi password to display if it is a well known one\n// 5.2.27 - Bugfix: IOExpander memory allocation\n// 5.2.26 - Silently ignore overridden HAL defaults\n//        - include HAL_IGNORE_DEFAULTS macro in EXRAIL \n// 5.2.25 - Fix bug causing <X> after working <D commands \n// 5.2.24 - Exrail macro asserts to catch \n//            : duplicate/missing automation/route/sequence/call ids\n//            : latches and reserves out of range\n//            : speeds out of range\n// 5.2.23 - KeywordHasher _hk (no functional change) \n// 5.2.22 - Bugfixes: Empty turnout descriptions ok; negative route numbers valid.\n// 5.2.21 - Add STARTUP_DELAY config option to delay CS bootup\n// 5.2.20 - Check return of Ethernet.begin()\n// 5.2.19 - ESP32: Determine if the RMT hardware can handle DCC\n// 5.2.18 - Display network IP fix\n// 5.2.17 - ESP32 simplify network logic\n// 5.2.16 - Bugfix to allow for devices using the EX-IOExpander protocol to have no analogue or no digital pins\n// 5.2.15 - move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions\n//        - add repeats to function packets that are not reminded in accordance with accessory packets\n// 5.2.14 - Reminder window DCC packet optimization\n//        - Optional #define DISABLE_FUNCTION_REMINDERS \n// 5.2.13 - EXRAIL STEALTH \n// 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for\n//        - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers\n//        - STM32 extra UART/USARTs for larger Nucleo models\n// 5.2.11 - Change from TrackManager::returnMode to TrackManager::getMode\n// 5.2.10 - Include trainbrains.eu block unoccupancy driver\n//        - include IO_PCA9555  \n// 5.2.9  - Bugfix LCD startup with no LCD, uses <@\n// 5.2.9  - EXRAIL STASH feature \n// 5.2.8  - Bugfix: Do not turn off all tracks on change\n//          give better power messages\n// 5.2.7  - Bugfix: EXRAIL ling segment\n//        - Bugfix: Back out wrongly added const\n//        - Bugfix ESP32: Do not inverse DCX direction signal twice\n// 5.2.6  - Trackmanager broadcast power state on track mode change\n// 5.2.5  - Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC\n// 5.2.4  - LCD macro will not do diag if that duplicates @ to same target.\n//        - Added ROUTE_DISABLED macro in EXRAIL\n// 5.2.3  - Bugfix: Catch stange input to parser\n// 5.2.2  - Added option to allow MAX_CHARACTER_ROWS to be defined in config.h\n// 5.2.1  - Trackmanager rework for simpler structure\n// 5.2.0  - ESP32: Autoreverse and booster mode support\n// 5.1.21 - EXRAIL invoke multiple ON handlers for same event\n// 5.1.20 - EXRAIL Tidy and ROUTE_STATE, ROUTE_CAPTION \n// 5.1.19 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0\n// 5.1.18 - TURNOUTL bugfix\n// 5.1.17 - Divide out C for config and D for diag commands\n// 5.1.16 - Remove I2C address from EXTT_TURNTABLE macro to work with MUX, requires separate HAL macro to create\n// 5.1.15 - LCC/Adapter support and Exrail feature-compile-out.\n// 5.1.14 - Fixed IFTTPOSITION\n// 5.1.13 - Changed turntable broadcast from i to I due to server string conflict\n// 5.1.12 - Added Power commands <0 A> & <1 A> etc. and update to <=>\n//          Added EXRAIL SET_POWER(track, ON/OFF)\n//          Fixed a problem whereby <1 MAIN> also powered on PROG track\n//          Added functions to TrackManager.cpp to allow UserAddin code for power display on OLED/LCD\n//          Added - returnMode(byte t), returnDCAddr(byte t) & getModeName(byte Mode)\n// 5.1.11 - STM32F4xx revised I2C clock setup, no correctly sets clock and has fully variable frequency selection\n// 5.1.10 - STM32F4xx DCCEXanalogWrite to handle PWM generation for TrackManager DC/DCX\n//        - STM32F4xx DCC 58uS timer now using non-PWM output timers where possible\n//        - ESP32 brakeCanPWM check now detects UNUSED_PIN\n//        - ARM architecture brakeCanPWM now uses digitalPinHasPWM()\n//        - STM32F4xx shadowpin extensions to handle pins on ports D, E and F\n// 5.1.9  - Fixed IO_PCA9555'h to work with PCA9548 mux, tested OK\n// 5.1.8  - STM32Fxx ADCee extension to support ADCs #2 and #3\n// 5.1.7  - Fix turntable broadcasts for non-movement activities and <JP> result\n// 5.1.6  - STM32F4xx native I2C driver added\n// 5.1.5  - Added turntable object and EXRAIL commands\n//        - <I ...>, <JO ...>, <JP ...> - turntable commands\n//        - DCC_TURNTABLE, EXTT_TURNTABLE, IFTTPOSITION, ONROTATE, ROTATE, ROTATE_DCC, TT_ADDPOSITION, WAITFORTT EXRAIL\n// 5.1.4  - Added ONOVERLOAD & AFTEROVERLOAD to EXRAIL\n// 5.1.3  - Make parser more fool proof\n// 5.1.2  - Bugfix: ESP32 30ms off time\n// 5.1.1  - Check bad AT firmware version\n//        - Update IO_PCA9555.h reflecting IO_MCP23017.h changes to support PCA9548 mux\n// 5.0.1  - Bugfix: execute 30ms off time before rejoin\n// 5.0.0  - Make 4.2.69 the 5.0.0 release\n// 4.2.69 - Bugfix: Make <!> work in DC mode\n// 4.2.68 - Rename track mode OFF to NONE\n// 4.2.67 - AVR: Pin specific timer register seting\n//        - Protect Uno user from choosing DC(X)\n//        - More Nucleo variant defines\n//        - GPIO PCA9555 / TCA9555 support\n// 4.2.66 - Throttle inrush current by applying PWM to brake pin when\n//          fault pin goes active\n// 4.2.65 - new config WIFI_FORCE_AP option\n// 4.2.63 - completely new overcurrent detection\n//        - ESP32 protect from race in RMT code\n// 4.2.62 - Update IO_RotaryEncoder.h to ignore sending current position\n//        - Update IO_EXTurntable.h to remove forced I2C clock speed\n//        - Show device offline if EX-Turntable not connected\n// 4.2.61 - MAX_CURRENT restriction (caps motor shield value)\n// 4.2.60 - Add mDNS capability to ESP32 for autodiscovery\n// 4.2.59 - Fix: AP SSID was DCC_ instead of DCCEX_\n// 4.2.58 - Start motordriver as soon as possible but without waveform\n// 4.2.57 - New overload handling (faster and handles commonFaultPin again)\n//        - Optimize analog read STM32\n// 4.2.56 - Update IO_RotaryEncoder.h:\n//        - Improved I2C communication, non-blocking reads\n//        - Enable sending positions to the encoder from EXRAIL via SERVO()\n// 4.2.55 - Optimize analog read for AVR\n// 4.2.54 - EX8874 shield in config.example.h\n//        - Fix: Better warnings for pin number errors\n//        - Fix: Default roster list possible in Withrottle and <jR>\n//        - Fix: Pin handling supports pins up to 254\n// 4.2.53 - Fix: Fault pin handling made more straight forward\n// 4.2.52 - Experimental support for sabertooth motor controller on ESP32\n// 4.2.51 - Add DISABLE_PROG to disable programming to save RAM/Flash\n// 4.2.50 - Fixes: estop all, turnout eeprom, cab ID check\n// 4.2.49 - Exrail SPEED take notice of external direction change \n// 4.2.48 - BROADCAST/WITHROTTLE Exrail macros \n// 4.2.47 - Correct response to <JA 0>\n// 4.2.46 - Support boards with inverted fault pin\n// 4.2.45 - Add ONCLOCKMINS to FastClock to allow hourly repeat events\n// 4.2.44 - Add PowerShell installer EX-CommandStation-installer.exe\n// 4.2.43 - Fix STM32 set right port mode bits for analog\n// 4.2.42 - Added EXRAIL TURNOUTL Macro definition\n// 4.2.41 - Move HAl startup to ASAP in setup()\n//        - Fix DNOU8 output pin setup to all LOW  \n// 4.2.40 - Automatically detect conflicting default I2C devices and disable\n// 4.2.39 - DFplayer driver now polls device to detect failures and errors.\n// 4.2.38 - Clean up compiler warning when IO_RotaryEncoder.h included\n// 4.2.37 - Add new FLAGS HAL device for communications to/from EX-RAIL;\n//        - Fix diag display of high VPINs within IODevice class.\n// 4.2.36 - do not broadcast a turnout state that has not changed\n//        - Use A2/A3 for current sensing on ESP32 + Motor Shield\n// 4.2.35 - add <z> direct pin manipulation command \n// 4.2.34 - Completely fix EX-IOExpander analogue inputs\n// 4.2.33 - Fix EX-IOExpander non-working analogue inputs\n// 4.2.32 - Fix LCD/Display bugfixes from 4.2.29\n// 4.2.31 - Removes EXRAIL statup from top of file. (BREAKING CHANGE !!)\n//          Just add AUTOSTART to the top of your myAutomation.h to restore this function.\n// 4.2.30 - Fixes/enhancements to EX-IOExpander device driver.\n// 4.2.29 - Bugfix Scroll LCD without empty lines and consistent\n// 4.2.28 - Reinstate use of timer11 in STM32 - remove HA mode.\n//        - Update IO_DFPlayer to work with MP3-TF-16P rev3.\n// 4.2.27 - Bugfix LCD showed random characters in SCROLLMODE 2\n// 4.2.26 - EX-IOExpander device driver enhancements\n//        - Enhance I2C error checking\n//        - Introduce delays to _loop to allow room for other I2C device comms\n//        - Improve analogue read reliability\n// 4.2.25 - Bugfix SAMD21 Exrail odd byte boundary\n// 4.2.24 - Bugfix Ethernet shield: Static IP now possible\n// 4.2.23 - Bugfix signalpin2 was not set up in shadow port\n// 4.2.22 - Implement broadcast of Track Manager changes\n// 4.2.21 - Implement non-blocking I2C for EX-IOExpander device driver\n// 4.2.20 - <JG> & <JI> commands for multi-track gauges\n//        - Reinstate <c> but remember its a bit useless when TM involved.   \n// 4.2.19 - Bugfix for analog reading of track current sensor offset.\n// 4.2.18 - I2C Multiplexer support through Extended Addresses,\n//          added for Wire, 4209 and AVR I2C drivers.\n//        - I2C retries when an operation fails.\n//        - I2C timeout handling and recovery completed.\n//        - I2C SAMD Driver Read code completed.\n//        - PCF8575 I2C GPIO driver added.\n//        - EX-RAIL ANOUT function for triggering analogue\n//          HAL drivers (e.g. analogue outputs, DFPlayer, PWM).\n//        - EX-RAIL SCREEN function for writing to screens other \n//          than the primary one.\n//        - Installable HALDisplay Driver, with support\n//          for multiple displays.\n//        - Layered HAL Drivers PCA9685pwm and Servo added for \n//          native PWM on PCA9685 module and\n//          for animations of servo movement via PCA9685pwm.\n//          This is intended to support EXIOExpander and also\n//          replace the existing PCA9685 driver.\n//        - Add <D HAL RESET> to reinitialise failed drivers.\n//        - Add UserAddin facility to allow a user-written C++ function to be \n//          declared in myHal.cpp, to be called at a user-specified frequency.\n//        - Add ability to configure clock speed of PCA9685 drivers \n//          (to allow flicker-free LED control).\n//        - Improve stability of VL53L0X driver when XSHUT pin connected.\n//        - Enable DCC high accuracy mode for STM32 on standard motor shield (pins D12/D13).\n//        - Incorporate improvements to ADC scanning performance (courtesy of HABA).\n// 4.2.17 LCN bugfix\n// 4.2.16 Move EX-IOExpander servo support to the EX-IOExpander software\n// 4.2.15 Add basic experimental PWM support to EX-IOExpander\n//        EX-IOExpander 0.0.14 minimum required\n// 4.2.14 STM32F4xx fast ADC read implementation\n// 4.2.13 Broadcast power for <s> again\n// 4.2.12 Bugfix for issue #299 TurnoutDescription NULL\n// 4.2.11 Exrail IFLOCO feature added\n// 4.2.10 SIGNAL/SIGNALH bug fix as they were inverted\n//        IO_EXIOExpander.h input speed optimisation\n//        ONCLOCK and ONCLOCKTIME command added to EXRAIL for EX-FastCLock\n//        <JC> Serial command added for EX-FastClock\n//        <jC> Broadcast added for EX-FastClock\n//        IO_EXFastClock.h added for I2C FastClock connection\n// 4.2.9 duinoNodes support\n// 4.2.8 HIGHMEM (EXRAIL support beyond 64kb)\n//       Withrottle connect/disconnect improvements\n//       Report BOARD_TYPE if provided by compiler\n// 4.2.7 FIX: Static IP addr\n//       FIX: Reuse WiThrottle list entries\n// 4.2.6 FIX: Remove RAM thief\n//       FIX: ADC port 8-15 fix\n// 4.2.5 Make GETFLASHW code more universal\n//       FIX: Withrottle roster index\n//       Ethernet start improvement and link detection\n// 4.2.4 ESP32 experimental BT support\n//       More DC configurations possible and lower frequency\n//       Handle decoders that do not ack at write better\n// 4.2.3 Bugfix direction when togging between MAIN and DC\n//       Bugfix return fail when F/f argument out of range\n//       More error checking for out of bounds motor driver current trip limit\n// 4.2.2 ESP32 beta\n//       JOIN/UMJOIN on ESP32\n// 4.2.1 ESP32 alpha\n//       Ready for alpha test on ESP32. Track switching with <=> untested\n//       Send DCC signal on MAIN\n//       Detects ACK on PROG\n// 4.2.0 Track Manager additions:\n//       Broadcast improvements to separate <> and Withrottle responses\n//       Float eliminated saving >1.5kb PROGMEM and speed. \n//       SET_TRACK(track,mode) Functions (A-H, MAIN|PROG|DC|DCX|OFF)\n//       New DC track function and DCX reverse polarity function\n//       TrackManager DCC & DC up to 8 Districts Architecture \n//       Automatic ALIAS(name) \n//       Command Parser now accepts Underscore in Alias Names\n// 4.1.1 Bugfix: preserve turnout format\n//       Bugfix: parse multiple commands in one buffer string correct\n//       Bugfix: </> command signal status in Exrail\n// 4.1.0 ...\n//\n// 4.0.2 EXRAIL additions:\n//       ACK defaults set to 50mA LIMIT, 2000uS MIN, 20000uS MAX\n//       myFilter automatic detection (no need to call setFilter)\n//       FIX negative route ids in WIthrottle problem. \n//       IFRED(signal_id), IFAMBER(signal_id), IFGREEN(signal_id)\n//       </RED signal_id> </AMBER signal_id> </GREEN signal_id> commands\n//       <t cab> command to obtain current throttle settings \n//       JA, JR, JT commands to obtain route, roster and turnout descriptions\n//       HIDDEN turnouts\n//       PARSE <> commands in EXRAIL\n//       VIRTUAL_TURNOUT\n//       </KILL ALL> and KILLALL command to stop all tasks. \n//       FORGET forgets the current loco in DCC reminder tables.\n//       Servo signals (SERVO_SIGNAL) \n//       High-On signal pins (SIGNALH)\n//       Wait for analog value (ATGTE, ATLT)  \n// 4.0.1 Small EXRAIL updates\n//       EXRAIL BROADCAST(\"msg\") \n//       EXRAIL POWERON\n// 4.0.0 Major functional and non-functional changes.\n//       Engine Driver \"DriveAway\" feature enhancement\n//       'Discovered Server' multicast Dynamic Network Server (mDNS) displays available WiFi connections to a DCC++EX Command Station\n//       New EX-RAIL \"Extended Railroad Automation Instruction Language\" automation capability.\n//         EX-Rail Function commands for creating Automation, Route & Sequence Scripts\n//         EX-RAIL “ROSTER” Engines Id & Function key layout on Engine Driver or WiThrottle\n//         EX-RAIL DCC++EX Commands to Control EX-RAIL via JMRI Send pane and IDE Serial monitors\n//       New JMRI feature enhancements; \n//         Reads DCC++EX EEPROM & automatically uploades any Signals, DCC Turnouts, Servo Turnouts, Vpin Turnouts , & Output pane\n//         Turnout class revised to expand turnout capabilities, new commands added.\n//         Provides for multiple additional DCC++EX WiFi connections as accessory controllers or CS for a programming track when Motor Shields are added\n//         Supports Multiple Command Station connections and individual tracking of Send DCC++ Command panes and DCC++ Traffic Monitor panes\n//       New HAL added for I/O (digital and analogue inputs and outputs, servos etc)\n//         Automatically detects & connects to supported devices included in your config.h file\n//         Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.\n//         Support for PCA9685 PWM (servo) control modules.\n//         Support for analogue inputs on Arduino pins and on ADS111x I2C modules.\n//         Support for MP3 sound playback via DFPlayer module.\n//         Support for HC-SR04 Ultrasonic range sensor module.\n//         Support for VL53L0X Laser range sensor module (Time-Of-Flight).\n//         Added <D HAL SHOW> diagnostic command to show configured devices\n//       New Processor Support added\n//         Compiles on Nano Every and Teensy\n//       Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms).\n//       Can disable EEPROM code\n//       EEPROM layout change - deletes EEPROM contents on first start following upgrade.\n//       Output class now allows ID > 255.\n//       Configuration options to globally flip polarity of DCC Accessory states when driven from <a> command and <T> command.\n//       Increased use of display for showing loco decoder programming information.\n//       Can define border between long and short addresses\n//       Turnout and accessory states (thrown/closed = 0/1 or 1/0) can be set to match RCN-213\n//       Bugfix: one-off error in CIPSEND drop\n//       Bugfix: disgnostic display of ack pulses >32kus\n//       Bugfix: Current read from wrong ADC during interrupt\n// 3.2.0 Development Release Includes all of 3.1.1 thru 3.1.7 enhancements\n// 3.1.7 Bugfix: Unknown locos should have speed forward \n// 3.1.6 Make output ID two bytes and guess format/size of registered outputs found in EEPROM\n// 3.1.5 Fix LCD corruption on power-up\n// 3.1.4 Refactor OLED and LCD drivers and remove unused code\n// 3.1.3 Add a loop delay to give more time for sensing an Ethernet cable connection\n// 3.1.2 Eliminate wait after write when prog is joined or prog power is off\n// 3.1.1 SH1106 OLED Display Offset Fix\n// 3.0.16 Ignore CV1 bit 7 read rejected by decoder when identifying loco id.  \n// 3.0.15 only send function commands once, not 4 times\n// 3.0.14 gap in ack tolerant fix,  prog track power management over join fix. \n// 3.0.13 Functions>127 fix\n// 3.0.12 Fix HOSTNAME function for STA mode for WiFi\n// 3.0.11 28 speedstep support\n// 3.0.10 Teensy Support\n// 3.0.9 rearranges serial newlines for the benefit of JMRI.\n// 3.0.8 Includes <* *> wraps around DIAGs for the benefit of JMRI.\n// 3.0.7 Includes merge from assortedBits (many changes) and ACK manager change for lazy decoders\n// 3.0.6 Includes:\n// Fix Bug that did not let us transmit 5 byte sized packets like PoM\n// 3.0.5 Includes:\n// Fix Fn Key startup with loco ID and fix state change for F16-28\n// 3.0.4 Includes:\n// Wifi startup bugfixes\n// 3.0.3 Includes:\n//  <W addr> command to write loco address and clear consist \n//  <R> command will allow for consist address\n//  Startup commands implemented\n\n#endif\n"
  }
]