From 1122bf53267e56ea3028110eaefe9f587b37af0e Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Mon, 23 Jun 2025 22:56:33 +0200 Subject: [PATCH] Sniffer feature Sniffer feature 2 --- CommandStation-EX.ino | 47 +++++---- DCC.cpp | 2 +- DCCDecoder.cpp | 176 +++++++++++++++++++++++++++++++++ DCCDecoder.h | 30 ++++++ DCCEXParser.cpp | 11 ++- DCCEXParser.h | 2 +- DCCPacket.h | 80 +++++++++++++++ EXRAIL2.cpp | 26 ++++- EXRAIL2.h | 8 ++ EXRAIL2MacroReset.h | 12 +++ EXRAILMacros.h | 2 + GITHUB_SHA.h | 2 +- LocoTable.cpp | 130 +++++++++++++++++++++++++ LocoTable.h | 44 +++++++++ Sniffer.cpp | 220 ++++++++++++++++++++++++++++++++++++++++++ Sniffer.h | 80 +++++++++++++++ 16 files changed, 850 insertions(+), 22 deletions(-) create mode 100644 DCCDecoder.cpp create mode 100644 DCCDecoder.h create mode 100644 DCCPacket.h create mode 100644 LocoTable.cpp create mode 100644 LocoTable.h create mode 100644 Sniffer.cpp create mode 100644 Sniffer.h 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