diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino
index f689d21..ba00b25 100644
--- a/CommandStation-EX.ino
+++ b/CommandStation-EX.ino
@@ -52,6 +52,12 @@
#include "DCCEX.h"
#include "Display_Implementation.h"
+#ifdef ARDUINO_ARCH_ESP32
+#include "Sniffer.h"
+#include "DCCDecoder.h"
+Sniffer *dccSniffer = NULL;
+bool DCCDecoder::active = false;
+#endif // ARDUINO_ARCH_ESP32
#ifdef CPU_TYPE_ERROR
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
@@ -126,6 +132,11 @@ void setup()
// Start RMFT aka EX-RAIL (ignored if no automnation)
RMFT::begin();
+#ifdef ARDUINO_ARCH_ESP32
+#ifdef BOOSTER_INPUT
+ dccSniffer = new Sniffer(BOOSTER_INPUT);
+#endif // BOOSTER_INPUT
+#endif // ARDUINO_ARCH_ESP32
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
@@ -143,25 +154,27 @@ void setup()
CommandDistributor::broadcastPower();
}
-/**************** for future reference
-void looptimer(unsigned long timeout, const FSH* message)
-{
- static unsigned long lasttimestamp = 0;
- unsigned long now = micros();
- if (timeout != 0) {
- unsigned long diff = now - lasttimestamp;
- if (diff > timeout) {
- DIAG(message);
- DIAG(F("DeltaT=%L"), diff);
- lasttimestamp = micros();
- return;
- }
- }
- lasttimestamp = now;
-}
-*********************************************/
void loop()
{
+#ifdef ARDUINO_ARCH_ESP32
+#ifdef BOOSTER_INPUT
+ static bool oldactive = false;
+ if (dccSniffer) {
+ bool newactive = dccSniffer->inputActive();
+ if (oldactive != newactive) {
+ RMFT2::railsyncEvent(newactive);
+ oldactive = newactive;
+ }
+ DCCPacket p = dccSniffer->fetchPacket();
+ if (p.len() != 0) {
+ if (DCCDecoder::parse(p)) {
+ p.print(Serial);
+ }
+ }
+ }
+#endif // BOOSTER_INPUT
+#endif // ARDUINO_ARCH_ESP32
+
// The main sketch has responsibilities during loop()
// Responsibility 1: Handle DCC background processes
diff --git a/DCC.cpp b/DCC.cpp
index e92db91..9704639 100644
--- a/DCC.cpp
+++ b/DCC.cpp
@@ -327,7 +327,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
// the initial decoders were orgnized and that influenced how the DCC
// standard was made.
#ifdef DIAG_IO
- DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
+ DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
#endif
// use masks to detect wrong values and do nothing
if(address != (address & 511))
diff --git a/DCCDecoder.cpp b/DCCDecoder.cpp
new file mode 100644
index 0000000..fb61bda
--- /dev/null
+++ b/DCCDecoder.cpp
@@ -0,0 +1,176 @@
+/*
+ * © 2025 Harald Barth
+ *
+ * This file is part of CommandStation-EX
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+ */
+#ifdef ARDUINO_ARCH_ESP32
+#include "DCCDecoder.h"
+#include "LocoTable.h"
+#include "DCCEXParser.h"
+#include "DIAG.h"
+#include "DCC.h"
+
+bool DCCDecoder::parse(DCCPacket &p) {
+ if (!active)
+ return false;
+ const byte DECODER_MOBILE = 1;
+ const byte DECODER_ACCESSORY = 2;
+ byte decoderType = 0; // use 0 as none
+ byte *d = p.data();
+ byte *instr = 0; // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
+ uint16_t addr; // will be set to decoder addr (long/shor mobile or accessory)
+ bool locoInfoChanged = false;
+
+ if (d[0] == 0B11111111) { // Idle packet
+ return false;
+ }
+ // CRC verification here
+ byte checksum = 0;
+ for (byte n = 0; n < p.len(); n++)
+ checksum ^= d[n];
+ if (checksum) { // Result should be zero, if not it's an error!
+ digitalWrite(2,HIGH);
+ DIAG(F("Checksum error"));
+ return false;
+ }
+
+/*
+ Serial.print("< ");
+ for(int n=0; n<8; n++) {
+ Serial.print(d[0]&(1<");
+*/
+ if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
+ decoderType = DECODER_MOBILE;
+ instr = d+1;
+ addr = d[0];
+ } else {
+ if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
+ decoderType = DECODER_MOBILE;
+ instr = d+2;
+ addr = 256 * (d[0] & 0B00111111) + d[1];
+ } else { // bit7 == 1 and bit 6 == 0
+ decoderType = DECODER_ACCESSORY;
+ instr = d+1;
+ addr = d[0] & 0B00111111;
+ }
+ }
+ if (decoderType == DECODER_MOBILE) {
+ switch (instr[0] & 0xE0) {
+ case 0x20: // 001x-xxxx Extended commands
+ if (instr[0] == 0B00111111) { // 128 speed steps
+ if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[1])) == true) {
+ byte speed = instr[1] & 0B01111111;
+ byte direction = instr[1] & 0B10000000;
+ DCC::setThrottle(addr, speed, direction);
+ //DIAG(F("UPDATE"));
+ // send speed change to DCCEX here
+ }
+ }
+ break;
+ case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
+ case 0x60: // 011x-xxxx
+ if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[0] & 0B00111111)) == true) {
+ byte speed = instr[0] & 0B00001111; // first only look at 4 bits
+ if (speed > 1) { // neither stop nor emergency stop, recalculate speed
+ speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
+ speed = (speed - 3) * 9/2;
+ }
+ byte direction = instr[0] & 0B00100000;
+ DCC::setThrottle(addr, speed, direction);
+ }
+ break;
+ case 0x80: // 100x-xxxx Function group 1
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 1)) == true) {
+ byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
+ DCCEXParser::funcmap(addr, normalized, 0, 4);
+ }
+ break;
+ case 0xA0: // 101x-xxxx Function group 3 and 2
+ {
+ byte low, high;
+ if (bitRead(instr[0], 4)) {
+ low = 5;
+ high = 8;
+ } else {
+ low = 9;
+ high = 12;
+ }
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], low)) == true) {
+ DCCEXParser::funcmap(addr, instr[0], low, high);
+ }
+ }
+ break;
+ case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
+ switch (instr[0] & 0B00011111) {
+ case 0B00011110: // F13-F20 Function Control
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 13)) == true) {
+ DCCEXParser::funcmap(addr, instr[1], 13, 20);
+ }
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 17)) == true) {
+ DCCEXParser::funcmap(addr, instr[1], 13, 20);
+ }
+ break;
+ case 0B00011111: // F21-F28 Function Control
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1], 21)) == true) {
+ DCCEXParser::funcmap(addr, instr[1], 21, 28);
+ } // updateFunc handles only the 4 low bits as that is the most common case
+ if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1]>>4, 25)) == true) {
+ DCCEXParser::funcmap(addr, instr[1], 21, 28);
+ }
+ break;
+ /* do that later
+ case 0B00011000: // F29-F36 Function Control
+ break;
+ case 0B00011001: // F37-F44 Function Control
+ break;
+ case 0B00011010: // F45-F52 Function Control
+ break;
+ case 0B00011011: // F53-F60 Function Control
+ break;
+ case 0B00011100: // F61-F68 Function Control
+ break;
+ */
+ }
+ break;
+ case 0xE0: // 111x-xxxx Config vars
+ break;
+ }
+ return locoInfoChanged;
+ }
+ if (decoderType == DECODER_ACCESSORY) {
+ if (instr[0] & 0B10000000) { // Basic Accessory
+ addr = (((~instr[0]) & 0B01110000) << 2) + addr;
+ byte port = (instr[0] & 0B00000110) >> 1;
+ byte activate = (instr[0] & 0B00001000) >> 3;
+ byte coil = (instr[0] & 0B00000001);
+ locoInfoChanged = true;
+ //(void)addr; (void)port; (void)coil; (void)activate;
+ //DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
+ DCC::setAccessory(addr, port, coil, activate);
+ } else { // Accessory Extended NMRA spec, do we need to decode this?
+ /*
+ addr = (addr << 5) +
+ ((instr[0] & 0B01110000) >> 2) +
+ ((instr[0] & 0B00000110) >> 1);
+ */
+ }
+ return locoInfoChanged;
+ }
+ return false;
+}
+#endif // ARDUINO_ARCH_ESP32
diff --git a/DCCDecoder.h b/DCCDecoder.h
new file mode 100644
index 0000000..cb2a0ee
--- /dev/null
+++ b/DCCDecoder.h
@@ -0,0 +1,30 @@
+/*
+ * © 2025 Harald Barth
+ *
+ * This file is part of CommandStation-EX
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+ */
+#ifdef ARDUINO_ARCH_ESP32
+#include
+#include "DCCPacket.h"
+
+class DCCDecoder {
+public:
+ static bool parse(DCCPacket &p);
+ static inline void onoff(bool on) {active = on;};
+private:
+ static bool active;
+};
+#endif // ARDUINO_ARCH_ESP32
diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp
index 4849fb2..37f67cd 100644
--- a/DCCEXParser.cpp
+++ b/DCCEXParser.cpp
@@ -3,7 +3,7 @@
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2025 Herb Morton
- * © 2020-2023 Harald Barth
+ * © 2020-2025 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2025 Chris Harlow
@@ -122,6 +122,7 @@ Once a new OPCODE is decided upon, update this list.
#include "Stash.h"
#ifdef ARDUINO_ARCH_ESP32
#include "WifiESP32.h"
+#include "DCCDecoder.h"
#endif
// This macro can't be created easily as a portable function because the
@@ -712,6 +713,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'C': // CONFIG
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
+ if (p[0] == "SNIFFER"_hk) { //
+ bool on = false;
+ if (params>1 && p[1] == "ON"_hk) {
+ on = true;
+ }
+ DCCDecoder::onoff(on);
+ return;
+ }
#if defined(HAS_ENOUGH_MEMORY)
if (p[0] == "WIFI"_hk) { //
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
diff --git a/DCCEXParser.h b/DCCEXParser.h
index 8896816..5be855a 100644
--- a/DCCEXParser.h
+++ b/DCCEXParser.h
@@ -40,6 +40,7 @@ struct DCCEXParser
static void setCamParserFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
+ static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
private:
@@ -81,7 +82,6 @@ struct DCCEXParser
static FILTER_CALLBACK filterRMFTCallback;
static FILTER_CALLBACK filterCamParserCallback;
static AT_COMMAND_CALLBACK atCommandCallback;
- static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
static void sendFlashList(Print * stream,const int16_t flashList[]);
};
diff --git a/DCCPacket.h b/DCCPacket.h
new file mode 100644
index 0000000..e754142
--- /dev/null
+++ b/DCCPacket.h
@@ -0,0 +1,80 @@
+/*
+ * © 2025 Harald Barth
+ *
+ * This file is part of CommandStation-EX
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+ */
+#include
+#ifndef DCCPacket_h
+#define DCCPacket_h
+#include
+
+class DCCPacket {
+public:
+ DCCPacket() {
+ _len = 0;
+ _data = NULL;
+ };
+ DCCPacket(byte *d, byte l) {
+ _len = l;
+ _data = new byte[_len];
+ for (byte n = 0; n<_len; n++)
+ _data[n] = d[n];
+ };
+ DCCPacket(const DCCPacket &old) {
+ _len = old._len;
+ _data = new byte[_len];
+ for (byte n = 0; n<_len; n++)
+ _data[n] = old._data[n];
+ };
+ DCCPacket &operator=(const DCCPacket &rhs) {
+ if (this == &rhs)
+ return *this;
+ delete[]_data;
+ _len = rhs._len;
+ _data = new byte[_len];
+ for (byte n = 0; n<_len; n++)
+ _data[n] = rhs._data[n];
+ return *this;
+ };
+ ~DCCPacket() {
+ if (_len) {
+ delete[]_data;
+ _len = 0;
+ _data = NULL;
+ }
+ };
+ inline bool operator==(const DCCPacket &right) {
+ if (_len != right._len)
+ return false;
+ if (_len == 0)
+ return true;
+ return (bcmp(_data, right._data, _len) == 0);
+ };
+ void print(HardwareSerial &s) {
+ s.print("<* DCCPACKET ");
+ for (byte n = 0; n< _len; n++) {
+ s.print(_data[n], HEX);
+ s.print(" ");
+ }
+ s.print("*>\n");
+ };
+ inline byte len() {return _len;};
+ inline byte *data() {return _data;};
+private:
+ byte _len = 0;
+ byte *_data = NULL;
+};
+#endif
diff --git a/EXRAIL2.cpp b/EXRAIL2.cpp
index f73ba35..8b41688 100644
--- a/EXRAIL2.cpp
+++ b/EXRAIL2.cpp
@@ -91,6 +91,10 @@ LookList * RMFT2::onRotateLookup=NULL;
LookList * RMFT2::onOverloadLookup=NULL;
LookList * RMFT2::onBlockEnterLookup=NULL;
LookList * RMFT2::onBlockExitLookup=NULL;
+#ifdef BOOSTER_INPUT
+LookList * RMFT2::onRailSyncOnLookup=NULL;
+LookList * RMFT2::onRailSyncOffLookup=NULL;
+#endif
byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr;
@@ -211,6 +215,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);
onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);
}
+#ifdef BOOSTER_INPUT
+ onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
+ onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
+#endif
// onLCCLookup is not the same so not loaded here.
@@ -1158,6 +1166,10 @@ void RMFT2::loop2() {
case OPCODE_ONOVERLOAD:
case OPCODE_ONBLOCKENTER:
case OPCODE_ONBLOCKEXIT:
+#ifdef BOOSTER_INPUT
+ case OPCODE_ONRAILSYNCON:
+ case OPCODE_ONRAILSYNCOFF:
+#endif
break;
default:
@@ -1387,7 +1399,19 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
onOverloadLookup->handleEvent(F("POWER"),track);
}
}
-
+#ifdef BOOSTER_INPUT
+void RMFT2::railsyncEvent(bool on) {
+ if (Diag::CMD)
+ DIAG(F("railsyncEvent : %d"), on);
+ if (on) {
+ if (onRailSyncOnLookup)
+ onRailSyncOnLookup->handleEvent(F("RAILSYNCON"), 0);
+ } else {
+ if (onRailSyncOffLookup)
+ onRailSyncOffLookup->handleEvent(F("RAILSYNCOFF"), 0);
+ }
+}
+#endif
// This function is used when setting pins so that a SET or RESET
// will cause any blink task on that pin to terminate.
// It will be compiled out of existence if no BLINK feature is used.
diff --git a/EXRAIL2.h b/EXRAIL2.h
index ddef0c9..82b9839 100644
--- a/EXRAIL2.h
+++ b/EXRAIL2.h
@@ -74,6 +74,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_ACON, OPCODE_ACOF,
OPCODE_ONACON, OPCODE_ONACOF,
OPCODE_ONOVERLOAD,
+ OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
@@ -195,6 +196,9 @@ class LookList {
static void clockEvent(int16_t clocktime, bool change);
static void rotateEvent(int16_t id, bool change);
static void powerEvent(int16_t track, bool overload);
+#ifdef BOOSTER_INPUT
+ static void railsyncEvent(bool on);
+#endif
static void blockEvent(int16_t block, int16_t loco, bool entering);
static bool signalAspectEvent(int16_t address, byte aspect );
// Throttle Info Access functions built by exrail macros
@@ -266,6 +270,10 @@ private:
static LookList * onOverloadLookup;
static LookList * onBlockEnterLookup;
static LookList * onBlockExitLookup;
+#ifdef BOOSTER_INPUT
+ static LookList * onRailSyncOnLookup;
+ static LookList * onRailSyncOffLookup;
+#endif
static const int countLCCLookup;
diff --git a/EXRAIL2MacroReset.h b/EXRAIL2MacroReset.h
index b6658dd..60cdd07 100644
--- a/EXRAIL2MacroReset.h
+++ b/EXRAIL2MacroReset.h
@@ -129,6 +129,8 @@
#undef ONCLOCKTIME
#undef ONCLOCKMINS
#undef ONOVERLOAD
+#undef ONRAILSYNCON
+#undef ONRAILSYNCOFF
#undef ONGREEN
#undef ONRED
#undef ONROTATE
@@ -861,6 +863,16 @@
* @param track_id A..H
*/
#define ONOVERLOAD(track_id)
+/**
+ * @def ONRAILSYNCON
+ * @brief Start task here when the railsync (booster) input port get a valid DCC signal
+ */
+#define ONRAILSYNCON
+/**
+ * @def ONRAILSYNCOFF
+ * @brief Start task here when the railsync (booster) input port does not get a valid DCC signal any more
+ */
+#define ONRAILSYNCOFF
/**
* @def ONDEACTIVATE(addr,subaddr)
* @brief Start task here when DCC deactivate packet sent
diff --git a/EXRAILMacros.h b/EXRAILMacros.h
index d926eff..70c31eb 100644
--- a/EXRAILMacros.h
+++ b/EXRAILMacros.h
@@ -527,6 +527,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
+#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
+#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h
index 4b53d37..275c7eb 100644
--- a/GITHUB_SHA.h
+++ b/GITHUB_SHA.h
@@ -1 +1 @@
-#define GITHUB_SHA "devel-202506082312Z"
+#define GITHUB_SHA "devel-202506232044Z"
diff --git a/LocoTable.cpp b/LocoTable.cpp
new file mode 100644
index 0000000..47fd9ea
--- /dev/null
+++ b/LocoTable.cpp
@@ -0,0 +1,130 @@
+/* Copyright (c) 2023 Harald Barth
+ *
+ * This source is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This source is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software. If not, see
+ * .
+ */
+#include "LocoTable.h"
+
+LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
+int LocoTable::highestUsedReg = 0;
+
+int LocoTable::lookupSpeedTable(int locoId, bool autoCreate) {
+ // determine speed reg for this loco
+ int firstEmpty = MAX_LOCOS;
+ int reg;
+ for (reg = 0; reg < MAX_LOCOS; reg++) {
+ if (speedTable[reg].loco == locoId) break;
+ if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
+ }
+
+ // return -1 if not found and not auto creating
+ if (reg == MAX_LOCOS && !autoCreate) return -1;
+ if (reg == MAX_LOCOS) reg = firstEmpty;
+ if (reg >= MAX_LOCOS) {
+ //DIAG(F("Too many locos"));
+ return -1;
+ }
+ if (reg==firstEmpty){
+ speedTable[reg].loco = locoId;
+ speedTable[reg].speedCode=128; // default direction forward
+ speedTable[reg].groupFlags=0;
+ speedTable[reg].functions=0;
+ }
+ if (reg > highestUsedReg) highestUsedReg = reg;
+ return reg;
+}
+
+// returns false only if loco existed but nothing was changed
+bool LocoTable::updateLoco(int loco, byte speedCode) {
+ if (loco==0) {
+ /*
+ // broadcast stop/estop but dont change direction
+ for (int reg = 0; reg < highestUsedReg; reg++) {
+ if (speedTable[reg].loco==0) continue;
+ byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
+ if (speedTable[reg].speedCode != newspeed) {
+ speedTable[reg].speedCode = newspeed;
+ CommandDistributor::broadcastLoco(reg);
+ }
+ }
+ */
+ return true;
+ }
+
+ // determine speed reg for this loco
+ int reg=lookupSpeedTable(loco, false);
+ if (reg>=0) {
+ speedTable[reg].speedcounter++;
+ if (speedTable[reg].speedCode!=speedCode) {
+ speedTable[reg].speedCode = speedCode;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ // new
+ reg=lookupSpeedTable(loco, true);
+ if(reg >=0) speedTable[reg].speedCode = speedCode;
+ return true;
+ }
+}
+
+bool LocoTable::updateFunc(int loco, byte func, int shift) {
+ unsigned long previous;
+ unsigned long newfunc;
+ bool retval = false; // nothing was touched
+ int reg = lookupSpeedTable(loco, false);
+ if (reg < 0) { // not found
+ retval = true;
+ reg = lookupSpeedTable(loco, true);
+ newfunc = previous = 0;
+ } else {
+ newfunc = previous = speedTable[reg].functions;
+ }
+
+ speedTable[reg].funccounter++;
+
+ if(shift == 1) { // special case for light
+ newfunc &= ~1UL;
+ newfunc |= ((func & 0B10000) >> 4);
+ }
+ newfunc &= ~(0B1111UL << shift);
+ newfunc |= ((func & 0B1111) << shift);
+
+ if (newfunc != previous) {
+ speedTable[reg].functions = newfunc;
+ retval = true;
+ }
+ return retval;
+}
+
+void LocoTable::dumpTable(Stream *output) {
+ output->print("\n-----------Table---------\n");
+ for (byte reg = 0; reg <= highestUsedReg; reg++) {
+ if (speedTable[reg].loco != 0) {
+ output->print(speedTable[reg].loco);
+ output->print(' ');
+ output->print(speedTable[reg].speedCode);
+ output->print(' ');
+ output->print(speedTable[reg].functions);
+ output->print(" #funcpacks:");
+ output->print(speedTable[reg].funccounter);
+ output->print(" #speedpacks:");
+ output->print(speedTable[reg].speedcounter);
+ speedTable[reg].funccounter = 0;
+ speedTable[reg].speedcounter = 0;
+ output->print('\n');
+ }
+ }
+}
diff --git a/LocoTable.h b/LocoTable.h
new file mode 100644
index 0000000..977656f
--- /dev/null
+++ b/LocoTable.h
@@ -0,0 +1,44 @@
+/* Copyright (c) 2023 Harald Barth
+ *
+ * This source is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This source is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software. If not, see
+ * .
+ */
+#include
+
+#include "DCC.h" // fetch MAX_LOCOS from there
+
+class LocoTable {
+public:
+ void forgetLoco(int cab) {
+ int reg=lookupSpeedTable(cab, false);
+ if (reg>=0) speedTable[reg].loco=0;
+ }
+ static int lookupSpeedTable(int locoId, bool autoCreate);
+ static bool updateLoco(int loco, byte speedCode);
+ static bool updateFunc(int loco, byte func, int shift);
+ static void dumpTable(Stream *output);
+
+private:
+ struct LOCO
+ {
+ int loco;
+ byte speedCode;
+ byte groupFlags;
+ unsigned long functions;
+ unsigned int funccounter;
+ unsigned int speedcounter;
+ };
+ static LOCO speedTable[MAX_LOCOS];
+ static int highestUsedReg;
+};
diff --git a/Sniffer.cpp b/Sniffer.cpp
new file mode 100644
index 0000000..8f5692b
--- /dev/null
+++ b/Sniffer.cpp
@@ -0,0 +1,220 @@
+/*
+ * © 2025 Harald Barth
+ *
+ * This file is part of CommandStation-EX
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+ */
+#ifdef ARDUINO_ARCH_ESP32
+#define DIAG_LED 33
+#include "Sniffer.h"
+#include "DIAG.h"
+//extern Sniffer *DCCSniffer;
+
+static void packeterror() {
+ digitalWrite(DIAG_LED,HIGH);
+}
+
+static void clear_packeterror() {
+ digitalWrite(DIAG_LED,LOW);
+}
+
+static bool halfbits2byte(uint16_t b, byte *dccbyte) {
+/*
+ if (b!=0 && b!=0xFFFF) {
+ Serial.print("[ ");
+ for(int n=0; n<16; n++) {
+ Serial.print(b&(1<>2;
+ }
+ return true;
+}
+
+static void IRAM_ATTR blink_diag(int limit) {
+ delay(500);
+ for (int n=0 ; ncap_edge == MCPWM_BOTH_EDGE) {
+ // should not happen at all
+ // delays here might crash sketch
+ blink_diag(2);
+ return 0;
+ }
+ if (user_data) ((Sniffer *)user_data)->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
+//if (DCCSniffer) DCCSniffer->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
+
+ return 0;
+}
+
+Sniffer::Sniffer(byte snifferpin) {
+ mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, snifferpin);
+ // set capture edge, BIT(0) - negative edge, BIT(1) - positive edge
+ // MCPWM_POS_EDGE|MCPWM_NEG_EDGE should be 3.
+ //mcpwm_capture_enable(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, MCPWM_POS_EDGE|MCPWM_NEG_EDGE, 0);
+ //mcpwm_isr_register(MCPWM_UNIT_0, sniffer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
+ //MCPWM0.int_ena.cap0_int_ena = 1; // Enable interrupt on CAP0 signal
+
+ mcpwm_capture_config_t MCPWM_cap_config = { //Capture channel configuration
+ .cap_edge = MCPWM_BOTH_EDGE, // according to mcpwm.h
+ .cap_prescale = 1, // 1 to 256 (see .h file)
+ .capture_cb = cap_ISR_cb, // user defined ISR/callback
+ .user_data = (void *)this // user defined argument to callback
+ };
+ pinMode(DIAG_LED ,OUTPUT);
+ blink_diag(3); // so that we know we have DIAG_LED
+ DIAG(F("Init sniffer on pin %d"), snifferpin);
+ ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &MCPWM_cap_config));
+}
+
+#define SNIFFER_TIMEOUT 100L // 100 Milliseconds
+bool Sniffer::inputActive(){
+ unsigned long now = millis();
+ return ((now - lastendofpacket) < SNIFFER_TIMEOUT);
+}
+
+#define DCC_TOO_SHORT 4000L // 4000 ticks are 50usec
+#define DCC_ONE_LIMIT 6400L // 6400 ticks are 80usec
+
+void IRAM_ATTR Sniffer::processInterrupt(int32_t capticks, bool posedge) {
+ byte bit = 0;
+ diffticks = capticks - lastticks;
+ if (lastedge != posedge) {
+ if (diffticks < DCC_TOO_SHORT) {
+ return;
+ }
+ if (diffticks < DCC_ONE_LIMIT) {
+ bit = 1;
+ } else {
+ bit = 0;
+ }
+ // update state variables for next round
+ lastticks = capticks;
+ lastedge = posedge;
+ bitfield = bitfield << (uint64_t)1;
+ bitfield = bitfield + (uint64_t)bit;
+
+ // now the halfbit is in the bitfield. Analyze...
+
+ if ((bitfield & 0xFFFFFF) == 0xFFFFFC){
+ // This looks at the 24 last halfbits
+ // and detects a preamble if
+ // 22 are ONE and 2 are ZERO which is a
+ // preabmle of 11 ONES and one ZERO
+ if (inpacket) {
+ // if we are already inpacket here we
+ // got a preamble in the middle of a
+ // packet
+ packeterror();
+ } else {
+ clear_packeterror(); // everything fine again at end of preable after good packet
+ }
+ currentbyte = 0;
+ dcclen = 0;
+ inpacket = true;
+ halfbitcounter = 18; // count 18 steps from 17 to 0 and then look at the byte
+ return;
+ }
+ if (inpacket) {
+ halfbitcounter--;
+ if (halfbitcounter) {
+ return; // wait until we have full byte
+ } else {
+ // have reached end of byte
+ //if (currentbyte == 2) debugfield = bitfield;
+ byte twohalfbits = bitfield & 0x03;
+ switch (twohalfbits) {
+ case 0x01:
+ case 0x02:
+ // broken bits
+ inpacket = false;
+ packeterror();
+ return;
+ break;
+ case 0x00:
+ case 0x03:
+ // byte end
+ uint16_t b = (bitfield & 0x3FFFF)>>2; // take 18 halfbits and use 16 of them
+ if (!halfbits2byte(b, dccbytes + currentbyte)) {
+ // broken halfbits
+ inpacket = false;
+ packeterror();
+ return;
+ }
+ if (twohalfbits == 0x03) { // end of packet marker
+ inpacket = false;
+ dcclen = currentbyte+1;
+ debugfield = bitfield;
+ // put it into the out packet
+ if (fetchflag) {
+ // not good, should have been fetched
+ // blink_diag(1);
+ packeterror(); // or better?
+ }
+ lastendofpacket = millis();
+ DCCPacket temppacket(dccbytes, dcclen);
+ if (!(temppacket == prevpacket)) {
+ // we have something new to offer to the fetch routine
+ outpacket.push_back(temppacket);
+ prevpacket = temppacket;
+ fetchflag = true;
+ }
+ return;
+ }
+ break;
+ }
+ halfbitcounter = 18;
+ currentbyte++; // everything done for this end of byte
+ if (currentbyte >= MAXDCCPACKETLEN) {
+ inpacket = false; // this is an error because we should have retured above
+ packeterror(); // when endof packet marker was active
+ }
+ }
+ }
+ } else { // lastedge == posedge
+ // this should not happen, check later
+ }
+}
+
+/*
+static void IRAM_ATTR sniffer_isr_handler(void *) {
+ DCCSniffer.processInterrupt();
+}
+*/
+#endif // ESP32
diff --git a/Sniffer.h b/Sniffer.h
new file mode 100644
index 0000000..5994c79
--- /dev/null
+++ b/Sniffer.h
@@ -0,0 +1,80 @@
+/*
+ * © 2025 Harald Barth
+ *
+ * This file is part of CommandStation-EX
+ *
+ * This is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * It is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with CommandStation. If not, see .
+ */
+#ifdef ARDUINO_ARCH_ESP32
+#include
+#include
+#include "driver/mcpwm.h"
+#include "soc/mcpwm_struct.h"
+#include "soc/mcpwm_reg.h"
+
+#define MAXDCCPACKETLEN 8
+#include "DCCPacket.h"
+
+class Sniffer {
+public:
+ Sniffer(byte snifferpin);
+ void IRAM_ATTR processInterrupt(int32_t capticks, bool posedge);
+ inline int32_t getTicks() {
+ noInterrupts();
+ int32_t i = diffticks;
+ interrupts();
+ return i;
+ };
+ inline int64_t getDebug() {
+ noInterrupts();
+ int64_t i = debugfield;
+ interrupts();
+ return i;
+ };
+ inline DCCPacket fetchPacket() {
+ // if there is no new data, this will create a
+ // packet with length 0 (which is no packet)
+ DCCPacket p;
+ noInterrupts();
+ if (!outpacket.empty()) {
+ p = outpacket.front();
+ outpacket.pop_front();
+ }
+ if (fetchflag) {
+ fetchflag = false; // (data has been fetched)
+ }
+ interrupts();
+ return p;
+ };
+ bool inputActive();
+private:
+ // keep these vars in processInterrupt only
+ uint64_t bitfield = 0;
+ uint64_t debugfield = 0;
+ int32_t diffticks;
+ int32_t lastticks;
+ bool lastedge;
+ byte currentbyte = 0;
+ byte dccbytes[MAXDCCPACKETLEN];
+ byte dcclen = 0;
+ bool inpacket = false;
+ // these vars are used as interface to other parts of sniffer
+ byte halfbitcounter = 0;
+ bool fetchflag = false;
+ std::list outpacket;
+ DCCPacket prevpacket;
+ volatile unsigned long lastendofpacket = 0; // timestamp millis
+
+};
+#endif