mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-04 13:05:24 +02:00
Compare commits
37 Commits
v5.5.28-De
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
48908c99a8 | ||
|
b8e1583b71 | ||
|
3b15491608 | ||
|
23073231ee | ||
|
3fc3c2329a | ||
|
c79e01056e | ||
|
946a784661 | ||
|
90ee8ea7d8 | ||
|
b63703a365 | ||
|
97f50910f6 | ||
|
08b8e43cd0 | ||
|
8fcc2b0083 | ||
|
978671a688 | ||
|
0effaaba87 | ||
|
8ac61b88d4 | ||
|
8aabede3ee | ||
|
839ea582a4 | ||
|
795d0edad9 | ||
|
f48f755608 | ||
|
40c30822f1 | ||
|
c7c9159a99 | ||
|
4df7df7be5 | ||
|
64f470a130 | ||
|
4125e73318 | ||
|
911bbd63be | ||
|
393b0bbd16 | ||
|
d9bd1e75f2 | ||
|
d1daf41f12 | ||
|
6bfa7028c4 | ||
|
a5d1d04882 | ||
|
bd6e426499 | ||
|
09bae44cc0 | ||
|
9f3354c687 | ||
|
fb495985f4 | ||
|
f868604ca9 | ||
|
41168a9dd8 | ||
|
0154e7fd78 |
@ -280,6 +280,9 @@ void CommandDistributor::broadcastPower() {
|
|||||||
state = '1';
|
state = '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state != '2')
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
|
||||||
|
|
||||||
// additional info about MAIN, PROG and JOIN
|
// additional info about MAIN, PROG and JOIN
|
||||||
bool main=TrackManager::getMainPower()==POWERMODE::ON;
|
bool main=TrackManager::getMainPower()==POWERMODE::ON;
|
||||||
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
|
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
|
||||||
@ -288,7 +291,7 @@ void CommandDistributor::broadcastPower() {
|
|||||||
const FSH * reason=F("");
|
const FSH * reason=F("");
|
||||||
if (join) {
|
if (join) {
|
||||||
reason = F(" JOIN"); // with space at start so we can append without space
|
reason = F(" JOIN"); // with space at start so we can append without space
|
||||||
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
|
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
|
||||||
} else {
|
} else {
|
||||||
if (main) {
|
if (main) {
|
||||||
//reason = F("MAIN");
|
//reason = F("MAIN");
|
||||||
@ -299,9 +302,6 @@ void CommandDistributor::broadcastPower() {
|
|||||||
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
|
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != '2')
|
|
||||||
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
|
|
||||||
#ifdef CD_HANDLE_RING
|
#ifdef CD_HANDLE_RING
|
||||||
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
|
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
|
||||||
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
|
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
|
||||||
|
@ -51,6 +51,12 @@
|
|||||||
|
|
||||||
#include "DCCEX.h"
|
#include "DCCEX.h"
|
||||||
#include "Display_Implementation.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
|
#ifdef CPU_TYPE_ERROR
|
||||||
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
|
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
|
||||||
@ -124,6 +130,11 @@ void setup()
|
|||||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||||
RMFT::begin();
|
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.
|
// 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.
|
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
|
||||||
@ -141,25 +152,27 @@ void setup()
|
|||||||
CommandDistributor::broadcastPower();
|
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()
|
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()
|
// The main sketch has responsibilities during loop()
|
||||||
|
|
||||||
// Responsibility 1: Handle DCC background processes
|
// Responsibility 1: Handle DCC background processes
|
||||||
|
38
DCC.cpp
38
DCC.cpp
@ -289,7 +289,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
|||||||
// the initial decoders were orgnized and that influenced how the DCC
|
// the initial decoders were orgnized and that influenced how the DCC
|
||||||
// standard was made.
|
// standard was made.
|
||||||
#ifdef DIAG_IO
|
#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
|
#endif
|
||||||
// use masks to detect wrong values and do nothing
|
// use masks to detect wrong values and do nothing
|
||||||
if(address != (address & 511))
|
if(address != (address & 511))
|
||||||
@ -523,6 +523,7 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
|||||||
V0, WACK, MERGE,
|
V0, WACK, MERGE,
|
||||||
V0, WACK, MERGE,
|
V0, WACK, MERGE,
|
||||||
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
|
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
|
||||||
|
BAD20SKIP, // detect invalid cv20 value and ignore
|
||||||
STASHLOCOID, // keep cv 20 until we have cv19 as well.
|
STASHLOCOID, // keep cv 20 until we have cv19 as well.
|
||||||
SETCV, (ackOp)19,
|
SETCV, (ackOp)19,
|
||||||
STARTMERGE, // Setup to read cv 19
|
STARTMERGE, // Setup to read cv 19
|
||||||
@ -628,7 +629,9 @@ const ackOp FLASH CONSIST_ID_PROG[] = {
|
|||||||
BASELINE,
|
BASELINE,
|
||||||
SETCV,(ackOp)20,
|
SETCV,(ackOp)20,
|
||||||
SETBYTEH, // high byte to CV 20
|
SETBYTEH, // high byte to CV 20
|
||||||
WB,WACK, // ignore dedcoder without cv20 support
|
WB,WACK,ITSKIP,
|
||||||
|
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
|
||||||
|
SKIPTARGET,
|
||||||
SETCV,(ackOp)19,
|
SETCV,(ackOp)19,
|
||||||
SETBYTEL, // low byte of word
|
SETBYTEL, // low byte of word
|
||||||
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
|
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
|
||||||
@ -759,7 +762,15 @@ void DCC::issueReminders() {
|
|||||||
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
|
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
|
||||||
// Move to next loco slot. If occupied, send a reminder.
|
// Move to next loco slot. If occupied, send a reminder.
|
||||||
int reg = lastLocoReminder+1;
|
int reg = lastLocoReminder+1;
|
||||||
if (reg > highestUsedReg) reg = 0; // Go to start of table
|
if (reg > highestUsedReg) {
|
||||||
|
if (loopStatus == 0 /*only needed if numLocos == 1 but we do not have a counter*/) {
|
||||||
|
// insert idle packet in the speed packet loop to fullfill the *censored*
|
||||||
|
// >5ms between packets to same decoder rule
|
||||||
|
const byte idlepacket[] = {0xFF, 0x00};
|
||||||
|
DCCWaveform::mainTrack.schedulePacket(idlepacket, 2, 0);
|
||||||
|
}
|
||||||
|
reg = 0; // Go to start of table
|
||||||
|
}
|
||||||
if (speedTable[reg].loco > 0) {
|
if (speedTable[reg].loco > 0) {
|
||||||
// have found loco to remind
|
// have found loco to remind
|
||||||
if (issueReminder(reg))
|
if (issueReminder(reg))
|
||||||
@ -780,40 +791,23 @@ bool DCC::issueReminder(int reg) {
|
|||||||
break;
|
break;
|
||||||
case 1: // remind function group 1 (F0-F4)
|
case 1: // remind function group 1 (F0-F4)
|
||||||
if (flags & FN_GROUP_1)
|
if (flags & FN_GROUP_1)
|
||||||
#ifndef DISABLE_FUNCTION_REMINDERS
|
|
||||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
|
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
|
||||||
#else
|
|
||||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),2);
|
|
||||||
flags&= ~FN_GROUP_1; // dont send them again
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
case 2: // remind function group 2 F5-F8
|
case 2: // remind function group 2 F5-F8
|
||||||
if (flags & FN_GROUP_2)
|
if (flags & FN_GROUP_2)
|
||||||
#ifndef DISABLE_FUNCTION_REMINDERS
|
|
||||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
|
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
|
||||||
#else
|
|
||||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),2);
|
|
||||||
flags&= ~FN_GROUP_2; // dont send them again
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
case 3: // remind function group 3 F9-F12
|
case 3: // remind function group 3 F9-F12
|
||||||
if (flags & FN_GROUP_3)
|
if (flags & FN_GROUP_3)
|
||||||
#ifndef DISABLE_FUNCTION_REMINDERS
|
|
||||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
|
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
|
||||||
#else
|
|
||||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),2);
|
|
||||||
flags&= ~FN_GROUP_3; // dont send them again
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
case 4: // remind function group 4 F13-F20
|
case 4: // remind function group 4 F13-F20
|
||||||
if (flags & FN_GROUP_4)
|
if (flags & FN_GROUP_4)
|
||||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
|
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),0);
|
||||||
flags&= ~FN_GROUP_4; // dont send them again
|
|
||||||
break;
|
break;
|
||||||
case 5: // remind function group 5 F21-F28
|
case 5: // remind function group 5 F21-F28
|
||||||
if (flags & FN_GROUP_5)
|
if (flags & FN_GROUP_5)
|
||||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
|
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),0);
|
||||||
flags&= ~FN_GROUP_5; // dont send them again
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
loopStatus++;
|
loopStatus++;
|
||||||
|
14
DCCACK.cpp
14
DCCACK.cpp
@ -347,6 +347,20 @@ void DCCACK::loop() {
|
|||||||
opcode=GETFLASH(ackManagerProg);
|
opcode=GETFLASH(ackManagerProg);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case BAD20SKIP:
|
||||||
|
if (ackManagerByte > 120) {
|
||||||
|
// skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
|
||||||
|
if (Diag::ACK) DIAG(F("XX cv20=%d "),ackManagerByte);
|
||||||
|
while (opcode!=SKIPTARGET) {
|
||||||
|
ackManagerProg++;
|
||||||
|
opcode=GETFLASH(ackManagerProg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it
|
||||||
|
if (ackManagerByte==0) break;
|
||||||
|
callback(-4);
|
||||||
|
return;
|
||||||
case SKIPTARGET:
|
case SKIPTARGET:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
2
DCCACK.h
2
DCCACK.h
@ -58,6 +58,8 @@ enum ackOp : byte
|
|||||||
ITSKIP, // skip to SKIPTARGET if ack true
|
ITSKIP, // skip to SKIPTARGET if ack true
|
||||||
NAKSKIP, // skip to SKIPTARGET if ack false
|
NAKSKIP, // skip to SKIPTARGET if ack false
|
||||||
COMBINE1920, // combine cvs 19 and 20 and callback
|
COMBINE1920, // combine cvs 19 and 20 and callback
|
||||||
|
BAD20SKIP, // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
|
||||||
|
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
|
||||||
SKIPTARGET = 0xFF // jump to target
|
SKIPTARGET = 0xFF // jump to target
|
||||||
};
|
};
|
||||||
|
|
||||||
|
175
DCCDecoder.cpp
Normal file
175
DCCDecoder.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* © 2025 Harald Barth
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
#include "DCCDecoder.h"
|
||||||
|
#include "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!
|
||||||
|
DIAG(F("Checksum error"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Serial.print("< ");
|
||||||
|
for(int n=0; n<8; n++) {
|
||||||
|
Serial.print(d[0]&(1<<n)?"1":"0");
|
||||||
|
}
|
||||||
|
Serial.println(" >");
|
||||||
|
*/
|
||||||
|
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
|
||||||
|
decoderType = DECODER_MOBILE;
|
||||||
|
instr = d+1;
|
||||||
|
addr = d[0];
|
||||||
|
} else {
|
||||||
|
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
|
||||||
|
decoderType = DECODER_MOBILE;
|
||||||
|
instr = d+2;
|
||||||
|
addr = 256 * (d[0] & 0B00111111) + d[1];
|
||||||
|
} else { // bit7 == 1 and bit 6 == 0
|
||||||
|
decoderType = DECODER_ACCESSORY;
|
||||||
|
instr = d+1;
|
||||||
|
addr = d[0] & 0B00111111;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decoderType == DECODER_MOBILE) {
|
||||||
|
switch (instr[0] & 0xE0) {
|
||||||
|
case 0x20: // 001x-xxxx Extended commands
|
||||||
|
if (instr[0] == 0B00111111) { // 128 speed steps
|
||||||
|
if ((locoInfoChanged = 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
|
30
DCCDecoder.h
Normal file
30
DCCDecoder.h
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "DCCPacket.h"
|
||||||
|
|
||||||
|
class DCCDecoder {
|
||||||
|
public:
|
||||||
|
static bool parse(DCCPacket &p);
|
||||||
|
static inline void onoff(bool on) {active = on;};
|
||||||
|
private:
|
||||||
|
static bool active;
|
||||||
|
};
|
||||||
|
#endif // ARDUINO_ARCH_ESP32
|
@ -3,7 +3,7 @@
|
|||||||
* © 2021 Neil McKechnie
|
* © 2021 Neil McKechnie
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021-2024 Herb Morton
|
* © 2021-2024 Herb Morton
|
||||||
* © 2020-2023 Harald Barth
|
* © 2020-2025 Harald Barth
|
||||||
* © 2020-2021 M Steve Todd
|
* © 2020-2021 M Steve Todd
|
||||||
* © 2020-2021 Fred Decker
|
* © 2020-2021 Fred Decker
|
||||||
* © 2020-2021 Chris Harlow
|
* © 2020-2021 Chris Harlow
|
||||||
@ -120,6 +120,7 @@ Once a new OPCODE is decided upon, update this list.
|
|||||||
#include "CamParser.h"
|
#include "CamParser.h"
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
#include "WifiESP32.h"
|
#include "WifiESP32.h"
|
||||||
|
#include "DCCDecoder.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// This macro can't be created easily as a portable function because the
|
// This macro can't be created easily as a portable function because the
|
||||||
@ -167,8 +168,10 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd,
|
|||||||
break;
|
break;
|
||||||
if (hot == '\0')
|
if (hot == '\0')
|
||||||
return -1;
|
return -1;
|
||||||
if (hot == '>')
|
if (hot == '>') {
|
||||||
|
*remainingCmd = '\0'; // terminate the cmd string with 0 instead of '>'
|
||||||
return parameterCount;
|
return parameterCount;
|
||||||
|
}
|
||||||
state = 2;
|
state = 2;
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -265,17 +268,22 @@ void DCCEXParser::parse(const FSH * cmd) {
|
|||||||
// See documentation on DCC class for info on this section
|
// See documentation on DCC class for info on this section
|
||||||
|
|
||||||
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
|
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
|
||||||
// This function can get stings of the form "<C OMM AND>" or "C OMM AND"
|
// This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
|
||||||
// found is true first after the leading "<" has been passed
|
// found is true first after the leading "<" has been passed which results
|
||||||
|
// in parseOne() getting c="C OMM AND>"
|
||||||
|
byte *cForLater = NULL;
|
||||||
bool found = (com[0] != '<');
|
bool found = (com[0] != '<');
|
||||||
for (byte *c=com; c[0] != '\0'; c++) {
|
for (byte *c=com; c[0] != '\0'; c++) {
|
||||||
if (found) {
|
if (found) {
|
||||||
parseOne(stream, c, ringStream);
|
cForLater = c;
|
||||||
found=false;
|
found=false;
|
||||||
}
|
}
|
||||||
if (c[0] == '<')
|
if (c[0] == '<') {
|
||||||
|
if (cForLater) parseOne(stream, cForLater, ringStream);
|
||||||
found = true;
|
found = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (cForLater) parseOne(stream, cForLater, ringStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
@ -676,6 +684,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
|||||||
case 'C': // CONFIG <C [params]>
|
case 'C': // CONFIG <C [params]>
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
#if defined(ARDUINO_ARCH_ESP32)
|
||||||
// currently this only works on ESP32
|
// currently this only works on ESP32
|
||||||
|
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
|
||||||
|
bool on = false;
|
||||||
|
if (params>1 && p[1] == "ON"_hk) {
|
||||||
|
on = true;
|
||||||
|
}
|
||||||
|
DCCDecoder::onoff(on);
|
||||||
|
return;
|
||||||
|
}
|
||||||
#if defined(HAS_ENOUGH_MEMORY)
|
#if defined(HAS_ENOUGH_MEMORY)
|
||||||
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
|
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
|
||||||
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
|
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
|
||||||
@ -1445,6 +1461,7 @@ void DCCEXParser::callback_Wloco(int16_t result)
|
|||||||
|
|
||||||
void DCCEXParser::callback_Wconsist(int16_t result)
|
void DCCEXParser::callback_Wconsist(int16_t result)
|
||||||
{
|
{
|
||||||
|
if (result==-4) DIAG(F("Long Consist %d not supported by decoder"),stashP[1]);
|
||||||
if (result==1) result=stashP[1]; // pick up original requested id from command
|
if (result==1) result=stashP[1]; // pick up original requested id from command
|
||||||
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
|
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
|
||||||
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));
|
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));
|
||||||
|
@ -39,6 +39,7 @@ struct DCCEXParser
|
|||||||
static void setRMFTFilter(FILTER_CALLBACK filter);
|
static void setRMFTFilter(FILTER_CALLBACK filter);
|
||||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||||
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
||||||
|
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
@ -77,7 +78,6 @@ struct DCCEXParser
|
|||||||
static FILTER_CALLBACK filterCallback;
|
static FILTER_CALLBACK filterCallback;
|
||||||
static FILTER_CALLBACK filterRMFTCallback;
|
static FILTER_CALLBACK filterRMFTCallback;
|
||||||
static AT_COMMAND_CALLBACK atCommandCallback;
|
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[]);
|
static void sendFlashList(Print * stream,const int16_t flashList[]);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
80
DCCPacket.h
Normal file
80
DCCPacket.h
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifndef DCCPacket_h
|
||||||
|
#define DCCPacket_h
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
class DCCPacket {
|
||||||
|
public:
|
||||||
|
DCCPacket() {
|
||||||
|
_len = 0;
|
||||||
|
_data = NULL;
|
||||||
|
};
|
||||||
|
DCCPacket(byte *d, byte l) {
|
||||||
|
_len = l;
|
||||||
|
_data = new byte[_len];
|
||||||
|
for (byte n = 0; n<_len; n++)
|
||||||
|
_data[n] = d[n];
|
||||||
|
};
|
||||||
|
DCCPacket(const DCCPacket &old) {
|
||||||
|
_len = old._len;
|
||||||
|
_data = new byte[_len];
|
||||||
|
for (byte n = 0; n<_len; n++)
|
||||||
|
_data[n] = old._data[n];
|
||||||
|
};
|
||||||
|
DCCPacket &operator=(const DCCPacket &rhs) {
|
||||||
|
if (this == &rhs)
|
||||||
|
return *this;
|
||||||
|
delete[]_data;
|
||||||
|
_len = rhs._len;
|
||||||
|
_data = new byte[_len];
|
||||||
|
for (byte n = 0; n<_len; n++)
|
||||||
|
_data[n] = rhs._data[n];
|
||||||
|
return *this;
|
||||||
|
};
|
||||||
|
~DCCPacket() {
|
||||||
|
if (_len) {
|
||||||
|
delete[]_data;
|
||||||
|
_len = 0;
|
||||||
|
_data = NULL;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inline bool operator==(const DCCPacket &right) {
|
||||||
|
if (_len != right._len)
|
||||||
|
return false;
|
||||||
|
if (_len == 0)
|
||||||
|
return true;
|
||||||
|
return (bcmp(_data, right._data, _len) == 0);
|
||||||
|
};
|
||||||
|
void print(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
|
@ -78,11 +78,17 @@ int DCCTimer::freeMemory() {
|
|||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
#if __has_include("esp_idf_version.h")
|
||||||
#include "esp_idf_version.h"
|
#include "esp_idf_version.h"
|
||||||
#if ESP_IDF_VERSION_MAJOR > 4
|
#endif
|
||||||
|
#if ESP_IDF_VERSION_MAJOR == 4
|
||||||
|
// all well correct IDF version
|
||||||
|
#else
|
||||||
#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"
|
#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"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// protect all the rest of the code from IDF version 5
|
||||||
|
#if ESP_IDF_VERSION_MAJOR == 4
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
#include <driver/adc.h>
|
#include <driver/adc.h>
|
||||||
#include <soc/sens_reg.h>
|
#include <soc/sens_reg.h>
|
||||||
@ -322,5 +328,5 @@ void ADCee::scan() {
|
|||||||
|
|
||||||
void ADCee::begin() {
|
void ADCee::begin() {
|
||||||
}
|
}
|
||||||
|
#endif //IDF v4
|
||||||
#endif //ESP32
|
#endif //ESP32
|
||||||
|
@ -145,7 +145,7 @@ void DCCWaveform::interrupt2() {
|
|||||||
// As we get to the end of the preambles, open the reminder window.
|
// As we get to the end of the preambles, open the reminder window.
|
||||||
// This delays any reminder insertion until the last moment so
|
// This delays any reminder insertion until the last moment so
|
||||||
// that the reminder doesn't block a more urgent packet.
|
// that the reminder doesn't block a more urgent packet.
|
||||||
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
|
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
|
||||||
if (remainingPreambles==1) promotePendingPacket();
|
if (remainingPreambles==1) promotePendingPacket();
|
||||||
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
|
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
|
||||||
// Update free memory diagnostic as we don't have anything else to do this time.
|
// Update free memory diagnostic as we don't have anything else to do this time.
|
||||||
|
29
EXRAIL2.cpp
29
EXRAIL2.cpp
@ -88,6 +88,10 @@ LookList * RMFT2::onClockLookup=NULL;
|
|||||||
LookList * RMFT2::onRotateLookup=NULL;
|
LookList * RMFT2::onRotateLookup=NULL;
|
||||||
#endif
|
#endif
|
||||||
LookList * RMFT2::onOverloadLookup=NULL;
|
LookList * RMFT2::onOverloadLookup=NULL;
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
LookList * RMFT2::onRailSyncOnLookup=NULL;
|
||||||
|
LookList * RMFT2::onRailSyncOffLookup=NULL;
|
||||||
|
#endif
|
||||||
byte * RMFT2::routeStateArray=nullptr;
|
byte * RMFT2::routeStateArray=nullptr;
|
||||||
const FSH * * RMFT2::routeCaptionArray=nullptr;
|
const FSH * * RMFT2::routeCaptionArray=nullptr;
|
||||||
int16_t * RMFT2::stashArray=nullptr;
|
int16_t * RMFT2::stashArray=nullptr;
|
||||||
@ -204,6 +208,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
|||||||
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
|
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
|
||||||
#endif
|
#endif
|
||||||
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
|
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
|
||||||
|
onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
|
||||||
|
#endif
|
||||||
// onLCCLookup is not the same so not loaded here.
|
// onLCCLookup is not the same so not loaded here.
|
||||||
|
|
||||||
// Second pass startup, define any turnouts or servos, set signals red
|
// Second pass startup, define any turnouts or servos, set signals red
|
||||||
@ -931,8 +939,9 @@ void RMFT2::loop2() {
|
|||||||
|
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
case OPCODE_JOIN:
|
case OPCODE_JOIN:
|
||||||
TrackManager::setPower(POWERMODE::ON);
|
|
||||||
TrackManager::setJoin(true);
|
TrackManager::setJoin(true);
|
||||||
|
TrackManager::setMainPower(POWERMODE::ON);
|
||||||
|
TrackManager::setProgPower(POWERMODE::ON);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OPCODE_UNJOIN:
|
case OPCODE_UNJOIN:
|
||||||
@ -1120,6 +1129,10 @@ void RMFT2::loop2() {
|
|||||||
case OPCODE_ONROTATE:
|
case OPCODE_ONROTATE:
|
||||||
#endif
|
#endif
|
||||||
case OPCODE_ONOVERLOAD:
|
case OPCODE_ONOVERLOAD:
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
case OPCODE_ONRAILSYNCON:
|
||||||
|
case OPCODE_ONRAILSYNCOFF:
|
||||||
|
#endif
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -1342,7 +1355,19 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
|
|||||||
onOverloadLookup->handleEvent(F("POWER"),track);
|
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
|
// This function is used when setting pins so that a SET or RESET
|
||||||
// will cause any blink task on that pin to terminate.
|
// will cause any blink task on that pin to terminate.
|
||||||
// It will be compiled out of existence if no BLINK feature is used.
|
// It will be compiled out of existence if no BLINK feature is used.
|
||||||
|
@ -73,6 +73,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
|||||||
OPCODE_ACON, OPCODE_ACOF,
|
OPCODE_ACON, OPCODE_ACOF,
|
||||||
OPCODE_ONACON, OPCODE_ONACOF,
|
OPCODE_ONACON, OPCODE_ONACOF,
|
||||||
OPCODE_ONOVERLOAD,
|
OPCODE_ONOVERLOAD,
|
||||||
|
OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
|
||||||
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
|
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
|
||||||
OPCODE_ROUTE_DISABLED,
|
OPCODE_ROUTE_DISABLED,
|
||||||
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
||||||
@ -188,6 +189,9 @@ class LookList {
|
|||||||
static void clockEvent(int16_t clocktime, bool change);
|
static void clockEvent(int16_t clocktime, bool change);
|
||||||
static void rotateEvent(int16_t id, bool change);
|
static void rotateEvent(int16_t id, bool change);
|
||||||
static void powerEvent(int16_t track, bool overload);
|
static void powerEvent(int16_t track, bool overload);
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
static void railsyncEvent(bool on);
|
||||||
|
#endif
|
||||||
static bool signalAspectEvent(int16_t address, byte aspect );
|
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||||
// Throttle Info Access functions built by exrail macros
|
// Throttle Info Access functions built by exrail macros
|
||||||
static const byte rosterNameCount;
|
static const byte rosterNameCount;
|
||||||
@ -255,7 +259,10 @@ private:
|
|||||||
static LookList * onRotateLookup;
|
static LookList * onRotateLookup;
|
||||||
#endif
|
#endif
|
||||||
static LookList * onOverloadLookup;
|
static LookList * onOverloadLookup;
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
static LookList * onRailSyncOnLookup;
|
||||||
|
static LookList * onRailSyncOffLookup;
|
||||||
|
#endif
|
||||||
static const int countLCCLookup;
|
static const int countLCCLookup;
|
||||||
static int onLCCLookup[];
|
static int onLCCLookup[];
|
||||||
static const byte compileFeatures;
|
static const byte compileFeatures;
|
||||||
|
@ -119,6 +119,8 @@
|
|||||||
#undef ONCLOCKTIME
|
#undef ONCLOCKTIME
|
||||||
#undef ONCLOCKMINS
|
#undef ONCLOCKMINS
|
||||||
#undef ONOVERLOAD
|
#undef ONOVERLOAD
|
||||||
|
#undef ONRAILSYNCON
|
||||||
|
#undef ONRAILSYNCOFF
|
||||||
#undef ONGREEN
|
#undef ONGREEN
|
||||||
#undef ONRED
|
#undef ONRED
|
||||||
#undef ONROTATE
|
#undef ONROTATE
|
||||||
@ -217,7 +219,7 @@
|
|||||||
#define BROADCAST(msg)
|
#define BROADCAST(msg)
|
||||||
#define CALL(route)
|
#define CALL(route)
|
||||||
#define CLEAR_STASH(id)
|
#define CLEAR_STASH(id)
|
||||||
#define CLEAR_ALL_STASH(id)
|
#define CLEAR_ALL_STASH
|
||||||
#define CLOSE(id)
|
#define CLOSE(id)
|
||||||
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||||
#define DCC_SIGNAL(id,add,subaddr)
|
#define DCC_SIGNAL(id,add,subaddr)
|
||||||
@ -288,6 +290,8 @@
|
|||||||
#define ONCLOCKTIME(hours,mins)
|
#define ONCLOCKTIME(hours,mins)
|
||||||
#define ONCLOCKMINS(mins)
|
#define ONCLOCKMINS(mins)
|
||||||
#define ONOVERLOAD(track_id)
|
#define ONOVERLOAD(track_id)
|
||||||
|
#define ONRAILSYNCON
|
||||||
|
#define ONRAILSYNCOFF
|
||||||
#define ONDEACTIVATE(addr,subaddr)
|
#define ONDEACTIVATE(addr,subaddr)
|
||||||
#define ONDEACTIVATEL(linear)
|
#define ONDEACTIVATEL(linear)
|
||||||
#define ONCLOSE(turnout_id)
|
#define ONCLOSE(turnout_id)
|
||||||
|
@ -584,6 +584,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
|||||||
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
||||||
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
||||||
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
|
#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 ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||||
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
||||||
|
@ -1 +1 @@
|
|||||||
#define GITHUB_SHA "c389fe9"
|
#define GITHUB_SHA "devel-202504182148Z"
|
||||||
|
130
LocoTable.cpp
Normal file
130
LocoTable.cpp
Normal file
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
LocoTable.h
Normal file
44
LocoTable.h
Normal file
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
};
|
BIN
Release_Notes/DCC-EX v5.4 Release Notes.xlsx
Normal file
BIN
Release_Notes/DCC-EX v5.4 Release Notes.xlsx
Normal file
Binary file not shown.
@ -126,29 +126,33 @@ void SerialManager::loop2() {
|
|||||||
buffer[0] = '\0';
|
buffer[0] = '\0';
|
||||||
}
|
}
|
||||||
} else { // if (inCommandPayload)
|
} else { // if (inCommandPayload)
|
||||||
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
|
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) {
|
||||||
buffer[bufferLength++] = ch;
|
buffer[bufferLength++] = ch; // advance bufferLength
|
||||||
if (inCommandPayload > PAYLOAD_NORMAL) {
|
if (inCommandPayload > PAYLOAD_NORMAL) {
|
||||||
if (inCommandPayload > 32 + 2) { // String way too long
|
if (inCommandPayload > 32 + 2) { // String way too long
|
||||||
ch = '>'; // we end this nonsense
|
ch = '>'; // we end this nonsense
|
||||||
inCommandPayload = PAYLOAD_NORMAL;
|
inCommandPayload = PAYLOAD_NORMAL;
|
||||||
DIAG(F("Parse error: Unbalanced string"));
|
DIAG(F("Parse error: Unbalanced string"));
|
||||||
// fall through to ending parsing below
|
// fall through to ending parsing below
|
||||||
} else if (ch == '"') { // String end
|
} else if (ch == '"') { // String end
|
||||||
inCommandPayload = PAYLOAD_NORMAL;
|
inCommandPayload = PAYLOAD_NORMAL;
|
||||||
continue; // do not fall through
|
continue; // do not fall through
|
||||||
} else
|
} else
|
||||||
inCommandPayload++;
|
inCommandPayload++;
|
||||||
}
|
}
|
||||||
if (inCommandPayload == PAYLOAD_NORMAL) {
|
if (inCommandPayload == PAYLOAD_NORMAL) {
|
||||||
if (ch == '>') {
|
if (ch == '>') {
|
||||||
buffer[bufferLength] = '\0';
|
buffer[bufferLength] = '\0'; // This \0 is after the '>'
|
||||||
DCCEXParser::parse(serial, buffer, NULL);
|
DCCEXParser::parse(serial, buffer, NULL); // buffer parsed with trailing '>'
|
||||||
inCommandPayload = PAYLOAD_FALSE;
|
inCommandPayload = PAYLOAD_FALSE;
|
||||||
break;
|
break;
|
||||||
} else if (ch == '"') {
|
} else if (ch == '"') {
|
||||||
inCommandPayload = PAYLOAD_STRING;
|
inCommandPayload = PAYLOAD_STRING;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DIAG(F("Parse error: input buffer overflow"));
|
||||||
|
inCommandPayload = PAYLOAD_FALSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
220
Sniffer.cpp
Normal file
220
Sniffer.cpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#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<<n)?"1":"0");
|
||||||
|
}
|
||||||
|
Serial.println(" ]");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
for(byte n=0; n<8; n++) {
|
||||||
|
switch (b & 0x03) {
|
||||||
|
case 0x01:
|
||||||
|
case 0x02:
|
||||||
|
// broken bits
|
||||||
|
packeterror();
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
case 0x00:
|
||||||
|
bitClear(*dccbyte, n);
|
||||||
|
break;
|
||||||
|
case 0x03:
|
||||||
|
bitSet(*dccbyte, n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
b = b>>2;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void IRAM_ATTR blink_diag(int limit) {
|
||||||
|
delay(500);
|
||||||
|
for (int n=0 ; n<limit; n++) {
|
||||||
|
digitalWrite(DIAG_LED,HIGH);
|
||||||
|
delay(200);
|
||||||
|
digitalWrite(DIAG_LED,LOW);
|
||||||
|
delay(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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) {
|
||||||
|
if (edata->cap_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
|
80
Sniffer.h
Normal file
80
Sniffer.h
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <list>
|
||||||
|
#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<DCCPacket> outpacket;
|
||||||
|
DCCPacket prevpacket;
|
||||||
|
volatile unsigned long lastendofpacket = 0; // timestamp millis
|
||||||
|
|
||||||
|
};
|
||||||
|
#endif
|
@ -668,7 +668,8 @@ void TrackManager::setJoin(bool joined) {
|
|||||||
if (track[t]->getMode() & TRACK_MODE_PROG) { // find PROG track
|
if (track[t]->getMode() & TRACK_MODE_PROG) { // find PROG track
|
||||||
tempProgTrack = t; // remember PROG track
|
tempProgTrack = t; // remember PROG track
|
||||||
setTrackMode(t, TRACK_MODE_MAIN);
|
setTrackMode(t, TRACK_MODE_MAIN);
|
||||||
track[t]->setPower(POWERMODE::ON); // if joined, always on
|
// setPower() of the track called after
|
||||||
|
// seperately after setJoin() instead
|
||||||
break; // there is only one prog track, done
|
break; // there is only one prog track, done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -680,8 +681,6 @@ void TrackManager::setJoin(bool joined) {
|
|||||||
setTrackMode(tempProgTrack, TRACK_MODE_PROG); // set track mode back to prog
|
setTrackMode(tempProgTrack, TRACK_MODE_PROG); // set track mode back to prog
|
||||||
track[tempProgTrack]->setPower(tPTmode); // set power status as it was before
|
track[tempProgTrack]->setPower(tPTmode); // set power status as it was before
|
||||||
tempProgTrack = MAX_TRACKS+1;
|
tempProgTrack = MAX_TRACKS+1;
|
||||||
} else {
|
|
||||||
DIAG(F("Unjoin but no remembered prog track"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -500,9 +500,9 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
|
|||||||
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
|
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
|
||||||
itoa(locoid,addcmd+4,10);
|
itoa(locoid,addcmd+4,10);
|
||||||
stashInstance->multithrottle(stashStream, (byte *)addcmd);
|
stashInstance->multithrottle(stashStream, (byte *)addcmd);
|
||||||
|
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
|
||||||
TrackManager::setMainPower(POWERMODE::ON);
|
TrackManager::setMainPower(POWERMODE::ON);
|
||||||
TrackManager::setProgPower(POWERMODE::ON);
|
TrackManager::setProgPower(POWERMODE::ON);
|
||||||
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
|
|
||||||
DIAG(F("LocoCallback commit success"));
|
DIAG(F("LocoCallback commit success"));
|
||||||
stashStream->commit();
|
stashStream->commit();
|
||||||
}
|
}
|
||||||
|
12
version.h
12
version.h
@ -3,7 +3,17 @@
|
|||||||
|
|
||||||
#include "StringFormatter.h"
|
#include "StringFormatter.h"
|
||||||
|
|
||||||
#define VERSION "5.4.3"
|
#define VERSION "5.4.11"
|
||||||
|
// 5.4.11 - Feature: Enable sniffer on CSB-1
|
||||||
|
// 5.4.10 - Bugfix: MEGA DCC waveform starvation (sends too many idles)
|
||||||
|
// 5.4.9 - Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
|
||||||
|
// - <W CONSIST longaddr> handles non-compliant decoders which NAK cv 20 writes.
|
||||||
|
// 5.4.8 - Bugfix: Insert idle packet at end of speed reminder loop; treat all function groups equal
|
||||||
|
// 5.4.7 - Bugfix: EXRAIL fix CLEAR_ALL_STASH
|
||||||
|
// 5.4.6 - Bugfix: Do not drop further commands in same packet
|
||||||
|
// 5.4.5 - ESP32: Better detection of correct IDF version
|
||||||
|
// - track power is always turned on after setJoin() not by setJoin()
|
||||||
|
// 5.4.4 - bugfix in parser, input buffer overrun and trailing > that did break <+>
|
||||||
// 5.4.3 - bugfix changeFn for functions 29..31
|
// 5.4.3 - bugfix changeFn for functions 29..31
|
||||||
// 5.4.2 - Reversed turnout bugfix
|
// 5.4.2 - Reversed turnout bugfix
|
||||||
// 5.4.1 - ESP32 bugfix packet buffer race
|
// 5.4.1 - ESP32 bugfix packet buffer race
|
||||||
|
Loading…
x
Reference in New Issue
Block a user