1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-13 17:08:53 +02:00

Compare commits

..

No commits in common. "master" and "v5.2.93-Devel" have entirely different histories.

37 changed files with 211 additions and 1094 deletions

View File

@ -280,9 +280,6 @@ void CommandDistributor::broadcastPower() {
state = '1';
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
@ -291,7 +288,7 @@ void CommandDistributor::broadcastPower() {
const FSH * reason=F("");
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
@ -302,6 +299,9 @@ void CommandDistributor::broadcastPower() {
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
@ -323,11 +323,11 @@ void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
}
void CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
}
void CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
}

View File

@ -59,8 +59,8 @@ public :
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId);
static void broadcastRouteState(int16_t routeId,byte state);
static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
static void broadcastRouteState(uint16_t routeId,byte state);
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
static void broadcastMessage(char * message);
// Handling code for virtual LCD receiver.

View File

@ -51,12 +51,6 @@
#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
@ -130,11 +124,6 @@ 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.
@ -152,28 +141,25 @@ void setup()
CommandDistributor::broadcastPower();
}
void loop()
/**************** for future reference
void looptimer(unsigned long timeout, const FSH* message)
{
#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
static bool oldactive = false;
if (dccSniffer) {
bool newactive = dccSniffer->inputActive();
if (oldactive != newactive) {
RMFT2::railsyncEvent(newactive);
oldactive = newactive;
}
DCCPacket p = dccSniffer->fetchPacket();
if (p.len() != 0) {
if (DCCDecoder::parse(p)) {
if (Diag::SNIFFER)
p.print();
}
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;
}
}
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32
lasttimestamp = now;
}
*********************************************/
void loop()
{
// The main sketch has responsibilities during loop()
// Responsibility 1: Handle DCC background processes

50
DCC.cpp
View File

@ -229,9 +229,15 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
auto currentValue=getFn(cab,functionNumber);
if (currentValue<0) return; // function not valid for change
setFn(cab,functionNumber, currentValue?false:true);
if (cab<=0 || functionNumber>31) return;
int reg = lookupSpeedTable(cab);
if (reg<0) return;
unsigned long funcmask = (1UL<<functionNumber);
speedTable[reg].functions ^= funcmask;
if (functionNumber <= 28) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
}
CommandDistributor::broadcastLoco(reg);
}
// Report function state (used from withrottle protocol)
@ -289,7 +295,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,%d)"), address, port, gate, onoff);
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
#endif
// use masks to detect wrong values and do nothing
if(address != (address & 511))
@ -523,7 +529,6 @@ const ackOp FLASH LOCO_ID_PROG[] = {
V0, WACK, MERGE,
V0, WACK, MERGE,
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
BAD20SKIP, // detect invalid cv20 value and ignore
STASHLOCOID, // keep cv 20 until we have cv19 as well.
SETCV, (ackOp)19,
STARTMERGE, // Setup to read cv 19
@ -629,9 +634,7 @@ const ackOp FLASH CONSIST_ID_PROG[] = {
BASELINE,
SETCV,(ackOp)20,
SETBYTEH, // high byte to CV 20
WB,WACK,ITSKIP,
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET,
WB,WACK, // ignore dedcoder without cv20 support
SETCV,(ackOp)19,
SETBYTEL, // low byte of word
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
@ -762,15 +765,7 @@ void DCC::issueReminders() {
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
// Move to next loco slot. If occupied, send a reminder.
int reg = lastLocoReminder+1;
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 (reg > highestUsedReg) reg = 0; // Go to start of table
if (speedTable[reg].loco > 0) {
// have found loco to remind
if (issueReminder(reg))
@ -791,23 +786,40 @@ bool DCC::issueReminder(int reg) {
break;
case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1)
#ifndef DISABLE_FUNCTION_REMINDERS
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;
case 2: // remind function group 2 F5-F8
if (flags & FN_GROUP_2)
#ifndef DISABLE_FUNCTION_REMINDERS
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;
case 3: // remind function group 3 F9-F12
if (flags & FN_GROUP_3)
#ifndef DISABLE_FUNCTION_REMINDERS
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;
case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),0);
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again
break;
case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),0);
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again
break;
}
loopStatus++;

View File

@ -347,20 +347,6 @@ void DCCACK::loop() {
opcode=GETFLASH(ackManagerProg);
}
break;
case BAD20SKIP:
if (ackManagerByte > 120) {
// skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
if (Diag::ACK) DIAG(F("XX cv20=%d "),ackManagerByte);
while (opcode!=SKIPTARGET) {
ackManagerProg++;
opcode=GETFLASH(ackManagerProg);
}
}
break;
case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it
if (ackManagerByte==0) break;
callback(-4);
return;
case SKIPTARGET:
break;
default:

View File

@ -58,8 +58,6 @@ enum ackOp : byte
ITSKIP, // skip to SKIPTARGET if ack true
NAKSKIP, // skip to SKIPTARGET if ack false
COMBINE1920, // combine cvs 19 and 20 and callback
BAD20SKIP, // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET = 0xFF // jump to target
};

View File

@ -1,178 +0,0 @@
/*
* © 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!
if (Diag::SNIFFER) {
DIAG(F("Checksum error"));
p.print();
}
return false;
}
/*
Serial.print("< ");
for(int n=0; n<8; n++) {
Serial.print(d[0]&(1<<n)?"1":"0");
}
Serial.println(" >");
*/
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
decoderType = DECODER_MOBILE;
instr = d+1;
addr = d[0];
} else {
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
decoderType = DECODER_MOBILE;
instr = d+2;
addr = 256 * (d[0] & 0B00111111) + d[1];
} else { // bit7 == 1 and bit 6 == 0
decoderType = DECODER_ACCESSORY;
instr = d+1;
addr = d[0] & 0B00111111;
}
}
if (decoderType == DECODER_MOBILE) {
switch (instr[0] & 0xE0) {
case 0x20: // 001x-xxxx Extended commands
if (instr[0] == 0B00111111) { // 128 speed steps
if ((locoInfoChanged = 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

View File

@ -1,30 +0,0 @@
/*
* © 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

View File

@ -3,7 +3,7 @@
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2024 Herb Morton
* © 2020-2025 Harald Barth
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
@ -120,7 +120,6 @@ Once a new OPCODE is decided upon, update this list.
#include "CamParser.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
@ -168,10 +167,8 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd,
break;
if (hot == '\0')
return -1;
if (hot == '>') {
*remainingCmd = '\0'; // terminate the cmd string with 0 instead of '>'
if (hot == '>')
return parameterCount;
}
state = 2;
continue;
@ -268,22 +265,17 @@ void DCCEXParser::parse(const FSH * cmd) {
// See documentation on DCC class for info on this section
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
// This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
// found is true first after the leading "<" has been passed which results
// in parseOne() getting c="C OMM AND>"
byte *cForLater = NULL;
// 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
bool found = (com[0] != '<');
for (byte *c=com; c[0] != '\0'; c++) {
if (found) {
cForLater = c;
parseOne(stream, c, ringStream);
found=false;
}
if (c[0] == '<') {
if (cForLater) parseOne(stream, cForLater, ringStream);
if (c[0] == '<')
found = true;
}
}
if (cForLater) parseOne(stream, cForLater, ringStream);
}
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
@ -410,8 +402,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|| (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1
) break;
// Honour the configuration option (config.h) which allows the <a> command to be reversed
// Because of earlier confusion we need to do the same thing under both defines
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
#else
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
@ -684,14 +675,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'C': // CONFIG <C [params]>
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
bool on = false;
if (params>1 && p[1] == "ON"_hk) {
on = true;
}
DCCDecoder::onoff(on);
return;
}
#if defined(HAS_ENOUGH_MEMORY)
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
@ -1243,9 +1226,6 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
case "LCN"_hk: // <D LCN ON/OFF>
Diag::LCN = onOff;
return true;
case "SNIFFER"_hk: // <D SNIFFER ON/OFF>
Diag::SNIFFER = onOff;
return true;
#endif
#ifndef DISABLE_EEPROM
case "EEPROM"_hk: // <D EEPROM NumEntries>
@ -1464,7 +1444,6 @@ void DCCEXParser::callback_Wloco(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
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));

View File

@ -39,7 +39,6 @@ struct DCCEXParser
static void setRMFTFilter(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:
@ -78,6 +77,7 @@ struct DCCEXParser
static FILTER_CALLBACK filterCallback;
static FILTER_CALLBACK filterRMFTCallback;
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[]);
};

View File

@ -1,83 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifndef DCCPacket_h
#define DCCPacket_h
#include <strings.h>
#include "defines.h"
class DCCPacket {
public:
DCCPacket() {
_len = 0;
_data = NULL;
};
DCCPacket(byte *d, byte l) {
_len = l;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = d[n];
};
DCCPacket(const DCCPacket &old) {
_len = old._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = old._data[n];
};
DCCPacket &operator=(const DCCPacket &rhs) {
if (this == &rhs)
return *this;
delete[]_data;
_len = rhs._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = rhs._data[n];
return *this;
};
~DCCPacket() {
if (_len) {
delete[]_data;
_len = 0;
_data = NULL;
}
};
inline bool operator==(const DCCPacket &right) {
if (_len != right._len)
return false;
if (_len == 0)
return true;
return (bcmp(_data, right._data, _len) == 0);
};
void print() {
static const char hexchars[]="0123456789ABCDEF";
USB_SERIAL.print(F("<* DCCPACKET "));
for (byte n = 0; n< _len; n++) {
USB_SERIAL.print(hexchars[_data[n]>>4]);
USB_SERIAL.print(hexchars[_data[n] & 0x0f]);
USB_SERIAL.print(' ');
}
USB_SERIAL.print(F("*>\n"));
};
inline byte len() {return _len;};
inline byte *data() {return _data;};
private:
byte _len = 0;
byte *_data = NULL;
};
#endif

View File

@ -44,12 +44,6 @@ class RMTChannel {
return true;
return dataReady;
};
inline void waitForDataCopy() {
while(1) { // do nothing and wait for interrupt clearing dataReady to happen
if (dataReady == false)
break;
}
};
inline uint32_t packetCount() { return packetCounter; };
private:

View File

@ -61,7 +61,6 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
void DCCTimer::startRailcomTimer(byte brakePin) {
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
/* The Railcom timer is started in such a way that it
- First triggers 28uS after the last TIMER1 tick.
This provides an accurate offset (in High Accuracy mode)
@ -114,13 +113,10 @@ void DCCTimer::startRailcomTimer(byte brakePin) {
// it does seems to get a good answer.
TCNT2=193 + (ICR1 - TCNT1)/8;
#endif
}
void DCCTimer::ackRailcomTimer() {
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
#endif
}

View File

@ -78,17 +78,11 @@ int DCCTimer::freeMemory() {
////////////////////////////////////////////////////////////////////////
#ifdef ARDUINO_ARCH_ESP32
#if __has_include("esp_idf_version.h")
#include "esp_idf_version.h"
#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"
#if ESP_IDF_VERSION_MAJOR > 4
#error "DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDE version 4. Arduino ESP32 library 3.0.0 is too new. Downgrade to one of 2.0.9 to 2.0.17"
#endif
// protect all the rest of the code from IDF version 5
#if ESP_IDF_VERSION_MAJOR == 4
#include "DIAG.h"
#include <driver/adc.h>
#include <soc/sens_reg.h>
@ -328,5 +322,5 @@ void ADCee::scan() {
void ADCee::begin() {
}
#endif //IDF v4
#endif //ESP32

View File

@ -145,7 +145,7 @@ void DCCWaveform::interrupt2() {
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
if (remainingPreambles==1) promotePendingPacket();
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
// Update free memory diagnostic as we don't have anything else to do this time.
@ -278,11 +278,7 @@ void DCCWaveform::begin() {
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
RMTChannel *rmtchannel = (isMainTrack ? rmtMainChannel : rmtProgChannel);
if (rmtchannel == NULL)
return; // no idea to prepare packet if we can not send it anyway
rmtchannel->waitForDataCopy(); // blocking wait so we can write into buffer
byte checksum = 0;
for (byte b = 0; b < byteCount; b++) {
checksum ^= buffer[b];
@ -300,7 +296,13 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
{
int ret = 0;
do {
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
if(isMainTrack) {
if (rmtMainChannel != NULL)
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} else {
if (rmtProgChannel != NULL)
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
}
} while(ret > 0);
}
}

View File

@ -1,23 +1,3 @@
/*
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EXRAIL_H
#define EXRAIL_H

View File

@ -4,7 +4,6 @@
* © 2021-2023 Harald Barth
* © 2020-2023 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -88,10 +87,6 @@ LookList * RMFT2::onClockLookup=NULL;
LookList * RMFT2::onRotateLookup=NULL;
#endif
LookList * RMFT2::onOverloadLookup=NULL;
#ifdef BOOSTER_INPUT
LookList * RMFT2::onRailSyncOnLookup=NULL;
LookList * RMFT2::onRailSyncOffLookup=NULL;
#endif
byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr;
int16_t * RMFT2::stashArray=nullptr;
@ -208,10 +203,6 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
#endif
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.
// Second pass startup, define any turnouts or servos, set signals red
@ -880,14 +871,6 @@ void RMFT2::loop2() {
DCC::changeFn(operand,getOperand(1));
break;
case OPCODE_XFWD:
DCC::setThrottle(operand,getOperand(1), true);
break;
case OPCODE_XREV:
DCC::setThrottle(operand,getOperand(1), false);
break;
case OPCODE_DCCACTIVATE: {
// operand is address<<3 | subaddr<<1 | active
int16_t addr=operand>>3;
@ -939,9 +922,8 @@ void RMFT2::loop2() {
#ifndef DISABLE_PROG
case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true);
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
break;
case OPCODE_UNJOIN:
@ -1129,11 +1111,7 @@ void RMFT2::loop2() {
case OPCODE_ONROTATE:
#endif
case OPCODE_ONOVERLOAD:
#ifdef BOOSTER_INPUT
case OPCODE_ONRAILSYNCON:
case OPCODE_ONRAILSYNCOFF:
#endif
break;
default:
@ -1355,19 +1333,7 @@ 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.
@ -1513,7 +1479,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
}
}
void RMFT2::manageRouteState(int16_t id, byte state) {
void RMFT2::manageRouteState(uint16_t id, byte state) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup
@ -1525,7 +1491,7 @@ void RMFT2::manageRouteState(int16_t id, byte state) {
CommandDistributor::broadcastRouteState(id,state);
}
}
void RMFT2::manageRouteCaption(int16_t id,const FSH* caption) {
void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup

View File

@ -3,7 +3,6 @@
* © 2020-2022 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -46,7 +45,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_ENDIF,OPCODE_ELSE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,OPCODE_XFWD,OPCODE_XREV,
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
@ -73,7 +72,6 @@ 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,
@ -189,9 +187,6 @@ 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 bool signalAspectEvent(int16_t address, byte aspect );
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
@ -259,15 +254,12 @@ private:
static LookList * onRotateLookup;
#endif
static LookList * onOverloadLookup;
#ifdef BOOSTER_INPUT
static LookList * onRailSyncOnLookup;
static LookList * onRailSyncOffLookup;
#endif
static const int countLCCLookup;
static int onLCCLookup[];
static const byte compileFeatures;
static void manageRouteState(int16_t id, byte state);
static void manageRouteCaption(int16_t id, const FSH* caption);
static void manageRouteState(uint16_t id, byte state);
static void manageRouteCaption(uint16_t id, const FSH* caption);
static byte * routeStateArray;
static const FSH ** routeCaptionArray;
static int16_t * stashArray;

View File

@ -2,7 +2,6 @@
* © 2020-2022 Chris Harlow. All rights reserved.
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
*
* This file is part of CommandStation-EX
*
@ -119,8 +118,6 @@
#undef ONCLOCKTIME
#undef ONCLOCKMINS
#undef ONOVERLOAD
#undef ONRAILSYNCON
#undef ONRAILSYNCOFF
#undef ONGREEN
#undef ONRED
#undef ONROTATE
@ -197,8 +194,6 @@
#undef XFOFF
#undef XFON
#undef XFTOGGLE
#undef XREV
#undef XFWD
#ifndef RMFT2_UNDEF_ONLY
#define ACTIVATE(addr,subaddr)
@ -219,7 +214,7 @@
#define BROADCAST(msg)
#define CALL(route)
#define CLEAR_STASH(id)
#define CLEAR_ALL_STASH
#define CLEAR_ALL_STASH(id)
#define CLOSE(id)
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
#define DCC_SIGNAL(id,add,subaddr)
@ -290,8 +285,6 @@
#define ONCLOCKTIME(hours,mins)
#define ONCLOCKMINS(mins)
#define ONOVERLOAD(track_id)
#define ONRAILSYNCON
#define ONRAILSYNCOFF
#define ONDEACTIVATE(addr,subaddr)
#define ONDEACTIVATEL(linear)
#define ONCLOSE(turnout_id)
@ -372,7 +365,5 @@
#define XFOFF(cab,func)
#define XFON(cab,func)
#define XFTOGGLE(cab,func)
#define XFWD(cab,speed)
#define XREV(cab,speed)
#endif

View File

@ -3,7 +3,6 @@
* © 2020-2022 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -584,8 +583,6 @@ 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),
@ -668,8 +665,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
#define XFTOGGLE(cab,func) OPCODE_XFTOGGLE,V(cab),OPCODE_PAD,V(func),
#define XFWD(cab,speed) OPCODE_XFWD,V(cab),OPCODE_PAD,V(speed),
#define XREV(cab,speed) OPCODE_XREV,V(cab),OPCODE_PAD,V(speed),
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;

View File

@ -1 +1 @@
#define GITHUB_SHA "devel-202504182148Z"
#define GITHUB_SHA "devel-202412222121Z"

View File

@ -1,130 +0,0 @@
/* 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');
}
}
}

View File

@ -1,44 +0,0 @@
/* 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;
};

View File

@ -1,39 +1,77 @@
# What is DCC-EX?
DCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your complete model railroad layout. Our easy to use, do-it-yourself, and free open source products run on off-the-shelf Arduino technology and are supported by numerous third party hardware and apps like JMRI, Engine Driver, wiThrottle, Rocrail and more.
# What is DCC++ EX?
DCC++ EX is the organization maintaining several codebases that together represent a fully open source DCC system. Currently, this includes the following:
Currently, our products include the following:
* [CommandStation-EX](https://github.com/DCC-EX/CommandStation-EX/releases) - the latest take on the DCC++ command station for controlling your trains. Runs on an Arduino board, and includes advanced features such as a WiThrottle server implementation, turnout operation, general purpose inputs and outputs (I/O), and JMRI integration.
* [exWebThrottle](https://github.com/DCC-EX/exWebThrottle) - a simple web based controller for your DCC++ command station.
* [BaseStation-installer](https://github.com/DCC-EX/BaseStation-Installer) - an installer executable that takes care of downloading and installing DCC++ firmware onto your hardware setup.
* [BaseStation-Classic](https://github.com/DCC-EX/BaseStation-Classic) - the original DCC++ software, packaged in a stable release. No active development, bug fixes only.
* [EX-CommandStation](https://github.com/DCC-EX/CommandStation-EX/releases)
* [EX-WebThrottle](https://github.com/DCC-EX/exWebThrottle)
* [EX-Installer](https://github.com/DCC-EX/EX-Installer)
* [EX-MotoShield8874](https://dcc-ex.com/reference/hardware/motorboards/ex-motor-shield-8874.html#gsc.tab=0)
* [EX-DCCInspector](https://github.com/DCC-EX/DCCInspector-EX)
* [EX-Toolbox](https://github.com/DCC-EX/EX-Toolbox)
* [EX-Turntable](https://github.com/DCC-EX/EX-Turntable)
* [EX-IOExpander](https://github.com/DCC-EX/EX-IOExpander)
* [EX-FastClock](https://github.com/DCC-EX/EX-FastClock)
* [DCCEXProtocol](https://github.com/DCC-EX/DCCEXProtocol)
A basic DCC++ EX hardware setup can use easy to find, widely avalable Arduino boards that you can assemble yourself.
Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital Command Control (DCC) [standards](http://www.nmra.org/dcc-working-group "NMRA DCC Working Group"), including:
* simultaneous control of multiple locomotives
* 2-byte and 4-byte locomotive addressing
* 28 or 128-step speed throttling
* Activate/de-activate all accessory function addresses 0-2048
* Control of all cab functions F0-F28 and F29-F68
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
Details of these projects can be found on [our web site](https://dcc-ex.com/).
# Whats in this Repository?
This repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
To utilize this sketch, you can use the following:
1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)
1. (beginner) our [automated installer](https://github.com/DCC-EX/BaseStation-Installer)
2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)
3. (advanced) use git clone on this repository
Refer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.
Not using the installer? Open the file "CommandStation-EX.ino" in the
Arduino IDE. Please do not rename the folder containing the sketch
code, nor add any files in that folder. The Arduino IDE relies on the
structure and name of the folder to properly display and compile the
code. Rename or copy config.example.h to config.h. If you do not have
the standard setup, you must edit config.h according to the help texts
in config.h.
**We seriously recommend using the EX-Installer**, however if you choose not to use the installer...
## What's new in CommandStation-EX?
* Open the file ``CommandStation-EX.ino`` in the Arduino IDE or Visual Studio Code (VSC). Please do not rename the folder containing the sketch code, nor add any files in that folder. The Arduino IDE relies on the structure and name of the folder to properly display and compile the code.
* Rename or copy ``config.example.h`` to ``config.h``.
* You must edit ``config.h`` according to the help texts in ``config.h``.
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station (or through JMRI as before)
* WiFi and Ethernet shield support
* No more jumpers or soldering!
* Direct support for all the most popular motor control boards including single pin (Arduino) or dual pin (IBT_2) type PWM inputs without the need for an adapter circuit
* I2C Display support (LCD and OLED)
* Improved short circuit detection and automatic reset from an overload
* Current reading, sensing and ACK detection settings in milliAmps instead of just pin readings
* Improved adherence to the NMRA DCC specification
* Complete support for all the old commands and front ends like JMRI
* Railcom cutout (beta)
* Simpler, modular, faster code with an API Library for developers for easy expansion
* New features and functions in JMRI
* Ability to join MAIN and PROG tracks into one MAIN track to run your locos
* "Drive-Away" feature - Throttles with support, like Engine Driver, can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
* Diagnostic commands to test decoders that aren't reading or writing correctly
* Support for Uno, Nano, Mega, Nano Every and Teensy microcontrollers
* User Functions: Filter regular commands (like a turnout or output command) and pass it to your own function or accessory
* Support for LCN (layout control nodes)
* mySetup.h file that acts like an Autoexec.Bat command to send startup commands to the CS
* High Accuracty Waveform option for rock steady DCC signals
* New current response outputs current in mA, overlimit current, and maximum board capable current. Support for new current meter in JMRI
* USB Browser based EX-WebThrottle
* New, simpler, function control command
* Number of locos discovery command `<#>`
* Emergency stop command <!>
* Release cabs from memory command <-> all cabs, <- CAB> for just one loco address
* Automatic slot (register) management
* Automation (coming soon)
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
# More information
You can learn more at the [DCC-EX website](https://dcc-ex.com/)
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)
- November 14, 2020

View File

@ -126,33 +126,29 @@ void SerialManager::loop2() {
buffer[0] = '\0';
}
} else { // if (inCommandPayload)
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) {
buffer[bufferLength++] = ch; // advance bufferLength
if (inCommandPayload > PAYLOAD_NORMAL) {
if (inCommandPayload > 32 + 2) { // String way too long
ch = '>'; // we end this nonsense
inCommandPayload = PAYLOAD_NORMAL;
DIAG(F("Parse error: Unbalanced string"));
// fall through to ending parsing below
} else if (ch == '"') { // String end
inCommandPayload = PAYLOAD_NORMAL;
continue; // do not fall through
} else
inCommandPayload++;
}
if (inCommandPayload == PAYLOAD_NORMAL) {
if (ch == '>') {
buffer[bufferLength] = '\0'; // This \0 is after the '>'
DCCEXParser::parse(serial, buffer, NULL); // buffer parsed with trailing '>'
inCommandPayload = PAYLOAD_FALSE;
break;
} else if (ch == '"') {
inCommandPayload = PAYLOAD_STRING;
}
}
} else {
DIAG(F("Parse error: input buffer overflow"));
inCommandPayload = PAYLOAD_FALSE;
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
buffer[bufferLength++] = ch;
if (inCommandPayload > PAYLOAD_NORMAL) {
if (inCommandPayload > 32 + 2) { // String way too long
ch = '>'; // we end this nonsense
inCommandPayload = PAYLOAD_NORMAL;
DIAG(F("Parse error: Unbalanced string"));
// fall through to ending parsing below
} else if (ch == '"') { // String end
inCommandPayload = PAYLOAD_NORMAL;
continue; // do not fall through
} else
inCommandPayload++;
}
if (inCommandPayload == PAYLOAD_NORMAL) {
if (ch == '>') {
buffer[bufferLength] = '\0';
DCCEXParser::parse(serial, buffer, NULL);
inCommandPayload = PAYLOAD_FALSE;
break;
} else if (ch == '"') {
inCommandPayload = PAYLOAD_STRING;
}
}
}
}

View File

@ -1,241 +0,0 @@
/*
* © 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 "Sniffer.h"
#include "DIAG.h"
//extern Sniffer *DCCSniffer;
static void packeterror() {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
digitalWrite(SNIFFER_LED,HIGH);
#endif
#endif
}
static void clear_packeterror() {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
digitalWrite(SNIFFER_LED,LOW);
#endif
#endif
}
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) {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
delay(500);
for (int n=0 ; n<limit; n++) {
digitalWrite(SNIFFER_LED,HIGH);
delay(200);
digitalWrite(SNIFFER_LED,LOW);
delay(200);
}
#endif
#endif
}
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
};
#ifndef WIFI_LED
#ifdef SNIFFER_LED
pinMode(SNIFFER_LED ,OUTPUT);
#endif
#endif
blink_diag(3); // so that we know we have SNIFFER_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;
// We have something we want to give to the outpacket queue
// Check length of outpacket queue
if (outpacket.size() > 3) {
// not good, these should have been fetched
// the arbitraty number to check is THREE (see the holy grail)
// blink_diag(1); DO NOT DO THIS HERE -> will crash
packeterror(); // or what to do better?
// take emergency action:
while (!outpacket.empty()) {
outpacket.pop_front();
}
}
lastendofpacket = millis();
DCCPacket temppacket(dccbytes, dcclen);
if (!(temppacket == prevpacket)) {
// we have something new to offer to the fetch routine
// put it into the outpacket queue
outpacket.push_back(temppacket);
prevpacket = temppacket;
}
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

View File

@ -1,76 +0,0 @@
/*
* © 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();
}
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;
std::list<DCCPacket> outpacket;
DCCPacket prevpacket;
volatile unsigned long lastendofpacket = 0; // timestamp millis
};
#endif

View File

@ -27,7 +27,6 @@ bool Diag::WIFI=false;
bool Diag::WITHROTTLE=false;
bool Diag::ETHERNET=false;
bool Diag::LCN=false;
bool Diag::SNIFFER=false;
void StringFormatter::diag( const FSH* input...) {

View File

@ -30,7 +30,7 @@ class Diag {
static bool WITHROTTLE;
static bool ETHERNET;
static bool LCN;
static bool SNIFFER;
};
class StringFormatter

View File

@ -668,8 +668,7 @@ void TrackManager::setJoin(bool joined) {
if (track[t]->getMode() & TRACK_MODE_PROG) { // find PROG track
tempProgTrack = t; // remember PROG track
setTrackMode(t, TRACK_MODE_MAIN);
// setPower() of the track called after
// seperately after setJoin() instead
track[t]->setPower(POWERMODE::ON); // if joined, always on
break; // there is only one prog track, done
}
}
@ -681,6 +680,8 @@ void TrackManager::setJoin(bool joined) {
setTrackMode(tempProgTrack, TRACK_MODE_PROG); // set track mode back to prog
track[tempProgTrack]->setPower(tPTmode); // set power status as it was before
tempProgTrack = MAX_TRACKS+1;
} else {
DIAG(F("Unjoin but no remembered prog track"));
}
}
#endif

View File

@ -312,6 +312,12 @@
*
*************************************************************************************/
#if defined(DCC_TURNOUTS_RCN_213)
const bool DCCTurnout::rcn213Compliant = true;
#else
const bool DCCTurnout::rcn213Compliant = false;
#endif
// DCCTurnoutData contains data specific to this subclass that is
// written to EEPROM when the turnout is saved.
struct DCCTurnoutData {
@ -379,10 +385,7 @@
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
// and Close writes a 0.
// RCN-213 specifies that Throw is 0 and Close is 1.
#ifndef DCC_TURNOUTS_RCN_213
close = !close;
#endif
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant);
return true;
}

View File

@ -245,6 +245,8 @@ public:
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
static Turnout *load(struct TurnoutData *turnoutData);
void print(Print *stream) override;
// Flag whether DCC Accessory packets are to contain 1=close/0=throw(RCN-213) or 1=throw/0-close (DCC++ Classic)
static const bool rcn213Compliant;
protected:
bool setClosedInternal(bool close) override;

View File

@ -500,9 +500,9 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
itoa(locoid,addcmd+4,10);
stashInstance->multithrottle(stashStream, (byte *)addcmd);
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
DIAG(F("LocoCallback commit success"));
stashStream->commit();
}

View File

@ -1,7 +1,7 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Neil McKechnie
* © 2020-2025 Harald Barth
* © 2020-2023 Harald Barth
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2023 Nathan Kellenicki
@ -45,14 +45,15 @@ The configuration file for DCC-EX Command Station
// the correct resistor could damage the sense pin on your Arduino or destroy
// the device.
//
// DEFINE MOTOR_SHIELD_TYPE BELOW. THESE ARE EXAMPLES. Full list in MotorDrivers.h
// DEFINE MOTOR_SHIELD_TYPE BELOW. THESE ARE EXAMPLES. FULL LIST IN MotorDrivers.h
//
// STANDARD_MOTOR_SHIELD : Arduino Motor shield Rev3 based on the L298 with 18V 2A per channel
// POLOLU_MOTOR_SHIELD : Pololu MC33926 Motor Driver (not recommended for prog track)
// FUNDUMOTO_SHIELD : Fundumoto Shield, no current sensing (not recommended, no short protection)
// FIREBOX_MK1 : The Firebox MK1
// FIREBOX_MK1S : The Firebox MK1S
// IBT_2_WITH_ARDUINO : Arduino Motor Shield for PROG and IBT-2 for MAIN
// EX8874_SHIELD : DCC-EX TI DRV8874 based motor shield
// EXCSB1 : DCC-EX CSB-1 hardware
// EXCSB1_WITH_EX8874 : DCC-EX CSB-1 hardware with DCC-EX TI DRV8874 shield
// NO_SHIELD : CS without any motor shield (as an accessory only CS)
// |
// +-----------------------v
//
@ -80,7 +81,7 @@ The configuration file for DCC-EX Command Station
/////////////////////////////////////////////////////////////////////////////////////
//
// NOTE: Not supported on Arduino Uno or Nano
// NOTE: Only supported on Arduino Mega
// Set to false if you not even want it on the Arduino Mega
//
#define ENABLE_WIFI true
@ -115,13 +116,13 @@ The configuration file for DCC-EX Command Station
// Your password may not contain ``"'' (double quote, ASCII 0x22).
#define WIFI_PASSWORD "Your network passwd"
//
// WIFI_HOSTNAME: You can change this if you have more than one
// CS to make them show up with different names on the network.
// Otherwise do not touch.
// WIFI_HOSTNAME: You probably don't need to change this
#define WIFI_HOSTNAME "dccex"
//
// WIFI_CHANNEL: The default channel is set to "1". If you need to use an
// alternate channel (we recommend using only 1,6, or 11) you may change it here.
// WIFI_CHANNEL: If the line "#define ENABLE_WIFI true" is uncommented,
// WiFi will be enabled (Mega only). The default channel is set to "1" whether
// this line exists or not. If you need to use an alternate channel (we recommend
// using only 1,6, or 11) you may change it here.
#define WIFI_CHANNEL 1
//
// WIFI_FORCE_AP: If you'd like to specify your own WIFI_SSID in AP mode, set this
@ -131,9 +132,8 @@ The configuration file for DCC-EX Command Station
/////////////////////////////////////////////////////////////////////////////////////
//
// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired) based
// on the W5100/W5500 ethernet chip or an STM32 CS with builin ethernet like the F429ZI.
// This is not for Wifi. You will then need the Arduino Ethernet library as well.
// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired). This
// is not for Wifi. You will then need the Arduino Ethernet library as well
//
//#define ENABLE_ETHERNET true
@ -270,9 +270,8 @@ The configuration file for DCC-EX Command Station
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0
// (throw turnout).
//#define DCC_ACCESSORY_COMMAND_REVERSE
//#define DCC_ACCESSORY_RCN_213
//
// HANDLING MULTIPLE SERIAL THROTTLES
// The command station always operates with the default Serial port.
// Diagnostics are only emitted on the default serial port and not broadcast.
@ -334,7 +333,7 @@ The configuration file for DCC-EX Command Station
// to the sabertooth controller _as_well_. Default: Undefined.
//
//#define SABERTOOTH 1
//
/////////////////////////////////////////////////////////////////////////////////////
//
// SENSORCAM
@ -346,7 +345,7 @@ The configuration file for DCC-EX Command Station
//#define SENSORCAM2_VPIN 600 //define other CAM's if installed.
//#define CAM2 SENSORCAM2_VPIN+ //for EX-RAIL commands e.g. IFLT(CAM2 020,1)
//
// For smoother power-up, when using the CAM, you may need a STARTUP_DELAY.
// That is described further above.
// For smoother power-up, define a STARTUP_DELAY to allow CAM to initialise ref images
//#define STARTUP_DELAY 5000 // up to 20sec. CS delay
//
/////////////////////////////////////////////////////////////////////////////////////

View File

@ -12,10 +12,18 @@
default_envs =
mega2560
uno
unowifiR2
nano
samd21-dev-usb
samd21-zero-usb
ESP32
Nucleo-F411RE
Nucleo-F446RE
Teensy3_2
Teensy3_5
Teensy3_6
Teensy4_0
Teensy4_1
src_dir = .
include_dir = .

View File

@ -3,25 +3,7 @@
#include "StringFormatter.h"
#define VERSION "5.4.12"
// 5.4.12 - Bugfix: Negative route Ids
// 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.2 - Reversed turnout bugfix
// 5.4.1 - ESP32 bugfix packet buffer race
// 5.4.0 - New version on master
// 5.2.96 - EXRAIL additions XFWD() and XREV()
// 5.2.95 - Release candidate for 5.4
// 5.2.94 - Bugfix: Less confusion and simpler code around the RCN213 defines
#define VERSION "5.2.93"
// 5.2.93 - Bugfix ESP32: clear progTrackSyncMain (join flag) when prog track is removed
// 5.2.92 - Bugfix: FADE power off fix, EXRAIL power diagnostic fix.
// 5.2.91 - Bugfix: Neopixel I2C overlap check