1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-31 11:23:44 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Ash-4
5749a0d551 JL LCD track status 2025-05-04 10:47:27 -05:00
18 changed files with 192 additions and 373 deletions

View File

@@ -312,9 +312,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;
@@ -323,7 +320,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");
@@ -334,6 +331,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);

130
DCC.cpp
View File

@@ -159,10 +159,10 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
}
if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 4, cab);
}
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
byte b[4];
byte nB = 0;
@@ -173,7 +173,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
if (byte1!=0) b[nB++] = byte1;
b[nB++] = byte2;
DCCQueue::scheduleDCCPacket(b, nB, 0, cab);
DCCQueue::scheduleDCCPacket(b, nB, count);
}
// returns speed steps 0 to 127 (1 == emergency stop)
@@ -239,7 +239,7 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0); // low order bits and state flag
b[nB++] = functionNumber >>7 ; // high order bits
}
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
DCCQueue::scheduleDCCPacket(b, nB, 4);
}
// We use the reminder table up to 28 for normal functions.
// We use 29 to 31 for DC frequency as well so up to 28
@@ -419,7 +419,7 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
b[nB++] = cv2(cv);
b[nB++] = bValue;
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
DCCQueue::scheduleDCCPacket(b, nB, 4);
}
//
@@ -437,7 +437,7 @@ void DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback) {
b[nB++] = cv2(cv);
b[nB++] = 0;
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
DCCQueue::scheduleDCCPacket(b, nB, 4);
Railcom::anticipate(cab,cv,callback);
}
@@ -459,7 +459,7 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
b[nB++] = cv2(cv);
b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
DCCQueue::scheduleDCCPacket(b, nB, 4);
}
bool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) {
@@ -619,7 +619,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
@@ -725,9 +724,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
@@ -852,44 +849,23 @@ void DCC::loop() {
if (DCCWaveform::mainTrack.isReminderWindowOpen()) {
// Now is a good time to choose a packet to be sent
// Either highest priority from the queues or a reminder
if (!DCCQueue::scheduleNext(false)) {
// none pending,
if (!DCCQueue::scheduleNext()) {
issueReminders();
DCCQueue::scheduleNext(true); // send any pending and force an idle if none
DCCQueue::scheduleNext(); // push through any just created reminder
}
}
}
void DCC::issueReminders() {
while(true) {
// Move to next loco slot. If occupied, send a reminder.
// slot.loco is -1 for deleted locos, 0 for end of list.
for (auto slot=nextLocoReminder;slot->loco;slot++) {
if (slot->loco<0) continue; // deleted loco, skip it
if (issueReminder(slot)) {
nextLocoReminder=slot+1; // remember next one to check
return; // reminder sent, exit
}
}
// we have reached the end of the table, so we can move on to
// the next loop state and start from the top.
// There are 0-9 loop states.. speed,f1,speed,f2,speed,f3,speed,f4,speed,f5
loopStatus++;
if (loopStatus>9) loopStatus=0; // reset to 0
// try looking from the start of the table down to where we started last time
for (auto slot=&speedTable[0];slot<nextLocoReminder;slot++) {
if (slot->loco<0) continue; // deleted loco, skip it
if (issueReminder(slot)) {
nextLocoReminder=slot+1; // remember next one to check
return; // reminder sent, exit
}
}
// if we get here then we can update the loop status and start again
if (loopStatus==0) return; // nothing found at all
}
auto slot = nextLocoReminder;
if (slot >= &speedTable[MAX_LOCOS]) slot=&speedTable[0]; // Go to start of table
if (slot->loco > 0)
if (!issueReminder(slot))
return;
// a loco=0 is at the end of the list, a loco <0 is deleted
if (slot->loco==0) nextLocoReminder = &speedTable[0];
else nextLocoReminder=slot+1;
}
int16_t normalize(byte speed) {
@@ -910,11 +886,7 @@ bool DCC::issueReminder(LOCO * slot) {
byte flags=slot->groupFlags;
switch (loopStatus) {
case 0:
case 2:
case 4:
case 6:
case 8: {
case 0: {
// calculate any momentum change going on
auto sc=slot->speedCode;
if (slot->targetSpeed!=sc) {
@@ -946,39 +918,51 @@ bool DCC::issueReminder(LOCO * slot) {
// DIAG(F("Reminder %d speed %d"),loco,slot->speedCode);
setThrottle2(loco, sc);
}
return true; // reminder sent
break;
case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1) {
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
return true; // reminder sent
}
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 3: // remind function group 2 F5-F8
if (flags & FN_GROUP_2) {
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
return true; // reminder sent
}
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 5: // remind function group 3 F9-F12
if (flags & FN_GROUP_3) {
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
return true; // reminder sent
}
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 7: // remind function group 4 F13-F20
if (flags & FN_GROUP_4) {
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
return true;
}
case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again
break;
case 9: // remind function group 5 F21-F28
if (flags & FN_GROUP_5) {
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
return true; // reminder sent
}
case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again
break;
}
return false; // no reminder sent
loopStatus++;
// if we reach status 6 then this loco is done so
// reset status to 0 for next loco and return true so caller
// moves on to next loco.
if (loopStatus>5) loopStatus=0;
return loopStatus==0;
}

2
DCC.h
View File

@@ -130,7 +130,7 @@ private:
static byte defaultMomentumA; // Accelerating
static byte defaultMomentumD; // Accelerating
static void setThrottle2(uint16_t cab, uint8_t speedCode);
static void setFunctionInternal(int cab, byte fByte, byte eByte);
static void setFunctionInternal(int cab, byte fByte, byte eByte, byte count);
static bool issueReminder(LOCO * slot);
static LOCO* nextLocoReminder;
static FSH *shieldName;

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

@@ -2,7 +2,7 @@
* © 2022 Paul M Antoine
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2024 Herb Morton
* © 2021-2025 Herb Morton
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
@@ -169,10 +169,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;
@@ -274,22 +272,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)
@@ -796,6 +789,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
TrackManager::reportCurrent(stream); // <g limit...limit>
return;
case "L"_hk: // <JL display row> track state and mA value on display
if (params<3) break;
TrackManager::reportCurrentLCD(p[1], p[2]); // Track power status
return;
case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
if (params!=1) break; // <JA>
StringFormatter::send(stream, F("<jA>\n"));
@@ -1524,7 +1522,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

@@ -18,13 +18,7 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* What does this queue manager do:
1. It provides a high priority queue and a low priority queue.
2. It manages situations where multiple loco speed commands are in the queue.
3. It allows an ESTOP to jump the queue and eliminate any outstanding speed commands that would later undo the stop.
4. It allows for coil on/off accessory commands to be synchronized to a given time delay.
5. It prevents transmission of sequential packets to the same loco id
*/
#include "Arduino.h"
#include "defines.h"
#include "DCCQueue.h"
@@ -35,8 +29,6 @@
DCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue();
DCCQueue* DCCQueue::highPriorityQueue=new DCCQueue();
PendingSlot* DCCQueue::recycleList=nullptr;
uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the same loco in a row
DCCQueue::DCCQueue() {
head=nullptr;
@@ -63,8 +55,8 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
}
// Packet joins end of low priority queue.
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats) {
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
}
// Packet replaces existing loco speed packet or joins end of high priority queue.
@@ -73,10 +65,6 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
for (auto p=highPriorityQueue->head;p;p=p->next) {
if (p->locoId==loco) {
// replace existing packet
if (length>sizeof(p->packet)) {
DIAG(F("DCC bad packet length=%d"),length);
length=sizeof(p->packet); // limit to size of packet
}
memcpy(p->packet,packet,length);
p->packetLength=length;
p->packetRepeat=repeats;
@@ -96,12 +84,11 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
// DIAG(F("DCC ESTOP loco=%d"),loco);
// kill any existing throttle packets for this loco (or all locos if broadcast)
// this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
// kill any existing throttle packets for this loco
PendingSlot * previous=nullptr;
auto p=highPriorityQueue->head;
while(p) {
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
if (loco==0 || p->locoId==loco) {
// drop this packet from the highPriority queue
if (previous) previous->next=p->next;
else highPriorityQueue->head=p->next;
@@ -120,9 +107,9 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
}
// Accessory coil-On Packet joins end of queue as normal.
// Accessory gate-On Packet joins end of queue as normal.
// When dequeued, packet is retained at start of queue
// but modified to coil-off and given the delayed start.
// but modified to gate-off and given the delayed start.
// getNext will ignore this packet until the requested start time.
void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {
auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);
@@ -131,67 +118,48 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
};
// Schedule the next dcc packet from the queues or an idle packet if none pending.
const byte idlePacket[] = {0xFF, 0x00};
// Obtain packet (fills packet, length and repeats)
// returns 0 length if nothing in queue.
bool DCCQueue::scheduleNext(bool force) {
if (highPriorityQueue->scheduleNextInternal()) return true;
if (lowPriorityQueue->scheduleNextInternal()) return true;
if (force) {
// This will arise when there is nothing available to be sent that will not compromise the rules
// typically this will only happen when there is only one loco in the reminders as the closely queued
// speed and function reminders must be separated by at least one packet not sent to that loco.
DCCWaveform::mainTrack.schedulePacket(idlePacket,sizeof(idlePacket),0);
lastSentPacketLocoId=0;
return true;
}
return false;
}
bool DCCQueue::scheduleNextInternal() {
bool DCCQueue::scheduleNext() {
// check high priority queue first
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return false;
PendingSlot* previous=nullptr;
for (auto p=head;p;previous=p,p=p->next) {
for (auto p=highPriorityQueue->head;p;p=p->next) {
// skip over pending ACC_OFF packets which are still delayed
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
if (p->locoId) {
// Prevent two consecutive packets to the same loco.
// this also means repeats cant be done by waveform
if (p->locoId==lastSentPacketLocoId) continue; // try again later
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,0);
lastSentPacketLocoId=p->locoId;
if (p->packetRepeat) {
p->packetRepeat--;
return true; // leave this packet in the queue
}
}
else {
// Non loco packets can repeat automatically
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
lastSentPacketLocoId=0;
}
// use this slot
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
// remove this slot from the queue
if (previous) previous->next=p->next;
else head=p->next;
if (!head) tail=nullptr;
else highPriorityQueue->head=p->next;
if (!highPriorityQueue->head) highPriorityQueue->tail=nullptr;
// special cases handling
if (p->type == ACC_ON_PACKET) {
// and recycle it.
recycle(p);
return true;
}
// No high priopity packets found, check low priority queue
auto p=lowPriorityQueue->head;
if (!p) return false; // nothing in queues
// schedule first packet in queue
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
// remove from queue
lowPriorityQueue->head=p->next;
if (!lowPriorityQueue->head) lowPriorityQueue->tail=nullptr;
if (p->type == ACC_ON_PACKET) {
// convert to a delayed off packet and jump the high priority queue
p->type= ACC_OFF_PACKET;
p->packet[1] &= ~0x08; // set C to 0 (gate off)
p->startTime=millis()+p->delayOff;
highPriorityQueue->jumpQueue(p);
return true;
}
// Recycle packet just consumed
recycle(p);
return true;
}
// No packets found
return false;
}
else recycle(p); // recycle this slot
return true;
}
// obtain and initialise slot for a PendingSlot.
@@ -202,30 +170,13 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
recycleList=p->next;
}
else {
static int16_t created=0;
int16_t q1=0;
int16_t q2=0;
for (auto p=highPriorityQueue->head;p;p=p->next) q1++;
for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
bool leak=(q1+q2)!=created;
DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
type,length,loco,q1,q2, created);
if (leak) {
for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
}
p=new PendingSlot; // need a queue entry
created++;
DIAG(F("New DCC queue slot"));
p=new PendingSlot; // need a queue entry
}
p->next=nullptr;
p->type=type;
p->packetLength=length;
p->packetRepeat=repeats;
if (length>sizeof(p->packet)) {
DIAG(F("DCC bad packet length=%d"),length);
length=sizeof(p->packet); // limit to size of packet
}
p->startTime=0; // not used for loco packets
memcpy((void*)p->packet,packet,length);
p->locoId=loco;
return p;

View File

@@ -23,7 +23,7 @@
#include "Arduino.h"
#include "DCCWaveform.h"
enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
enum PendingType:byte {NORMAL_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
struct PendingSlot {
PendingSlot* next;
PendingType type;
@@ -32,7 +32,8 @@ enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,
byte packet[MAX_PACKET_SIZE];
union { // use depends on packet type
uint16_t locoId; // SPEED & FUNCTION packets
uint16_t locoId; // NORMAL_PACKET .. only set >0 for speed change packets
// so they can be easily discarded if an estop jumps the queue.
uint16_t delayOff; // ACC_ON_PACKET delay to apply between on/off
uint32_t startTime; // ACC_OFF_PACKET time (mS) to transmit
};
@@ -43,7 +44,7 @@ class DCCQueue {
// Non-speed packets are queued in the main queue
static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);
static void scheduleDCCPacket(byte* packet, byte length, byte repeats);
// Speed packets are queued in the high priority queue
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
@@ -57,16 +58,16 @@ class DCCQueue {
static void scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms);
// Schedules a main track packet from the queues.
static bool scheduleNext(bool force);
// Schedules a main track packet from the queues if none pending.
// returns true if a packet was scheduled.
static bool scheduleNext();
private:
bool scheduleNextInternal();
// statics to manage high and low priority queues and recycleing of PENDINGs
static PendingSlot* recycleList;
static DCCQueue* highPriorityQueue;
static DCCQueue* lowPriorityQueue;
static uint16_t lastSentPacketLocoId; // used to prevent two packets to the same loco in a row
DCCQueue();

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
#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 IDF 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

@@ -1,7 +1,6 @@
/*
* © 2023 Neil McKechnie
* © 2022-2024 Paul M. Antoine
* © 2025 Herb Morton
* © 2021 Mike S
* © 2021, 2023 Harald Barth
* © 2021 Fred Decker
@@ -37,21 +36,6 @@
#include "DIAG.h"
#include <wiring_private.h>
// DC mode timers enable the PWM signal on select pins.
// Code added to sync timers which have the same frequency.
// Function prototypes
void refreshDCmodeTimers();
void resetCounterDCmodeTimers();
HardwareTimer *Timer1 = new HardwareTimer(TIM1);
HardwareTimer *Timer2 = new HardwareTimer(TIM2);
HardwareTimer *Timer3 = new HardwareTimer(TIM3);
HardwareTimer *Timer4 = new HardwareTimer(TIM4);
HardwareTimer *Timer9 = new HardwareTimer(TIM9);
#if defined(TIM13)
HardwareTimer *Timer13 = new HardwareTimer(TIM13);
#endif
#if defined(ARDUINO_NUCLEO_F401RE)
// Nucleo-64 boards don't have additional serial ports defined by default
// Serial1 is available on the F401RE, but not hugely convenient.
@@ -306,7 +290,7 @@ void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3600);
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
@@ -344,8 +328,7 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
if (pin_timer[pin] != NULL)
{
pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d Channel %d, frequency %d"), pin, pin_timer[pin], pin_channel[pin], frequency);
resetCounterDCmodeTimers();
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer Channel %d, frequency %d"), pin, pin_channel[pin], frequency);
}
else
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
@@ -358,7 +341,6 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
pin_timer[pin]->setOverflow(frequency, HERTZ_FORMAT); // Just change the frequency if it's already running!
DIAG(F("DCCEXanalogWriteFrequency::setting frequency to %d"), frequency);
resetCounterDCmodeTimers();
}
}
channel_frequency[pin] = frequency;
@@ -383,8 +365,6 @@ void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle
DIAG(F("DCCEXanalogWrite::Pin %d, value %d, duty cycle %d"), pin, value, duty_cycle);
// }
refreshDCmodeTimers();
resetCounterDCmodeTimers();
}
else
DIAG(F("DCCEXanalogWrite::Pin %d is not configured for PWM!"), pin);
@@ -679,35 +659,4 @@ void ADCee::begin() {
#endif
interrupts();
}
// NOTE: additional testing is needed to check the DCC signal
// where the DCC signal pin is a pwm pin on timers 1, 2, 3, 4, 9, 13
// or the brake pin is defined on a different timer.
// -- example: F411RE/F446RE - pin 10 on stacked EX8874
// lines added to sync timers --
// not exact sync, but timers with the same frequency should be in sync
void refreshDCmodeTimers() {
Timer1->refresh();
Timer2->refresh();
Timer3->refresh();
Timer4->refresh();
Timer9->refresh();
#if defined(TIM13)
Timer13->refresh();
#endif
}
// Function to synchronize timers - called every time there is powerON commmand for any DC track
void resetCounterDCmodeTimers() {
// Reset the counter for all DC mode timers
TIM1->CNT = 0;
TIM2->CNT = 0;
TIM3->CNT = 0;
TIM4->CNT = 0;
TIM9->CNT = 0;
#if defined(TIM13)
TIM13->CNT = 0;
#endif
}
#endif

View File

@@ -159,7 +159,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<10 && remainingPreambles>1;
if (remainingPreambles==1)
promotePendingPacket();

View File

@@ -934,9 +934,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:

View File

@@ -1,6 +1,6 @@
/*
* © 2022-2024 Paul M Antoine
* © 2024-2025 Herb Morton
* © 2024 Herb Morton
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
@@ -371,8 +371,8 @@ void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/)
}
#endif
//DIAG(F("Brake pin %d value %d freqency %d"), brakePin, brake, f);
DCCTimer::DCCEXanalogWrite(brakePin, brake, invertBrake);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
DCCTimer::DCCEXanalogWrite(brakePin, brake, invertBrake); // line swapped to set frequency first
#else // all AVR here
DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps
analogWrite(brakePin, invertBrake ? 255-brake : brake);

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

@@ -42,6 +42,7 @@
MotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };
int16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };
int16_t TrackManager::trackPwrMA[MAX_TRACKS] = { 0 };
int8_t TrackManager::lastTrack=-1;
bool TrackManager::progTrackSyncMain=false;
@@ -451,7 +452,7 @@ const FSH* TrackManager::getModeName(TRACK_MODE tm) {
if(tm & TRACK_MODIFIER_AUTO)
modename=F("MAIN A");
else if (tm & TRACK_MODIFIER_INV)
modename=F("MAIN I>\n");
modename=F("MAIN I");
else
modename=F("MAIN");
}
@@ -557,20 +558,6 @@ void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermod
}
if (didChange)
CommandDistributor::broadcastPower();
// re-initialize DC mode timer settings following powerON
if (powermode == POWERMODE::ON) {
#ifdef ARDUINO_ARCH_STM32
// for (byte i=0;i<=lastTrack;i++) {
// setTrackPowerF439ZI(i);
// }
// repeated in case the <F29..31 was set on a later track than power
// Note: this retains power but prevents speed doubling
for (byte i=0;i<lastTrack;i++) {
setTrackPowerF439ZI(i);
}
#endif
}
}
// Set track power for this track, inependent of mode
@@ -601,20 +588,6 @@ void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
driver->setPower(powermode);
if (oldpower != driver->getPower())
CommandDistributor::broadcastPower();
// re-initialize DC mode timer settings following powerON
if (powermode == POWERMODE::ON) {
#ifdef ARDUINO_ARCH_STM32
for (byte i=0;i<=lastTrack;i++) {
setTrackPowerF439ZI(i);
}
// repeated in case the <F29..31 was set on a later track than power
// Note: this retains power but prevents speed doubling
for (byte i=0;i<lastTrack;i++) {
setTrackPowerF439ZI(i);
}
#endif
}
}
// returns state of the one and only prog track
@@ -674,6 +647,37 @@ void TrackManager::reportCurrent(Print* stream) {
StringFormatter::send(stream,F(">\n"));
}
void TrackManager::reportCurrentLCD(uint8_t display, byte row) {
int16_t trackPwrTotalMA = 0;
FOR_EACH_TRACK(t) {
bool pstate = TrackManager::isPowerOn(t); // checks if power is on or off
TRACK_MODE tMode=(TrackManager::getMode(t)); // gets to current power mode
int16_t DCAddr=(TrackManager::returnDCAddr(t));
if (pstate) { // if power is on do this section
trackPwrMA[t]=(3*trackPwrMA[t]>>2) + ((track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :
track[t]->raw2mA(track[t]->getCurrentRaw(false)));
trackPwrTotalMA += trackPwrMA[t];
if (tMode & TRACK_MODE_DC) { // Test if track is in DC or DCX mode
SCREEN(display, row+t, F("%c: %S %d %dmA"), t+'A', (TrackManager::getModeName(tMode)),DCAddr, trackPwrMA[t]>>2);
}
else { // formats without DCAddress
SCREEN(display, row+t, F("%c: %S %dmA"), t+'A', (TrackManager::getModeName(tMode)), trackPwrMA[t]>>2);
}
}
else { // if power is off do this section
trackPwrMA[t] = 0;
if (tMode & TRACK_MODE_DC) { // DC / DCX
SCREEN(display, row+t, F("%c: %S %d"), t+'A', (TrackManager::getModeName(tMode)),DCAddr);
}
else { // Not DC or DCX
SCREEN(display, row+t, F("%c: %S"), t+'A', (TrackManager::getModeName(tMode)));
}
}
}
SCREEN(display, row+lastTrack+1, F("%d Districts %dmA"), lastTrack+1, trackPwrTotalMA>>2);
}
void TrackManager::reportGauges(Print* stream) {
StringFormatter::send(stream,F("<jG"));
FOR_EACH_TRACK(t) {
@@ -698,8 +702,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
}
}
@@ -711,6 +714,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
@@ -737,38 +742,3 @@ TRACK_MODE TrackManager::getMode(byte t) {
int16_t TrackManager::returnDCAddr(byte t) {
return (trackDCAddr[t]);
}
// Set track power for EACH track, independent of mode
// This updates the settings so that speed is correct
// following a frequency change - DC mode
void TrackManager::setTrackPowerF439ZI(byte t) {
MotorDriver *driver=track[t];
if (driver == NULL) { // track is not defined at all
// DIAG(F("Error: Track %c does not exist"), t+'A');
return;
}
TRACK_MODE trackmode = driver->getMode();
POWERMODE powermode = driver->getPower(); // line added to enable processing for DC mode tracks
POWERMODE oldpower = driver->getPower();
//if (trackmode & TRACK_MODE_NONE) {
// driver->setBrake(true); // Track is unused. Brake is good to have.
// powermode = POWERMODE::OFF; // Track is unused. Force it to OFF
//} else
if (trackmode & TRACK_MODE_DC) { // includes inverted DC (called DCX)
if (powermode == POWERMODE::ON) {
driver->setBrake(true); // DC starts with brake on
applyDCSpeed(t); // speed match DCC throttles
}
}
//else /* MAIN PROG EXT BOOST */ {
// if (powermode == POWERMODE::ON) {
// // toggle brake before turning power on - resets overcurrent error
// // on the Pololu board if brake is wired to ^D2.
// driver->setBrake(true);
// driver->setBrake(false); // DCC runs with brake off
// }
//}
driver->setPower(powermode);
if (oldpower != driver->getPower())
CommandDistributor::broadcastPower();
}

View File

@@ -1,8 +1,8 @@
/*
* © 2022 Chris Harlow
* © 2022-2024 Harald Barth
* © 2025 Herb Morton
* © 2023 Colin Murdoch
* © 2025 Herb Morton
*
* All rights reserved.
*
@@ -70,7 +70,6 @@ class TrackManager {
static void setTrackPower(TRACK_MODE trackmode, POWERMODE powermode);
static void setMainPower(POWERMODE mode) {setTrackPower(TRACK_MODE_MAIN, mode);}
static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);}
static void setTrackPowerF439ZI(byte t);
static const int16_t MAX_TRACKS=8;
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
@@ -89,6 +88,7 @@ class TrackManager {
static void sampleCurrent();
static void reportGauges(Print* stream);
static void reportCurrent(Print* stream);
static void reportCurrentLCD(uint8_t display, byte row);
static void reportObsoleteCurrent(Print* stream);
static void streamTrackState(Print* stream, byte t);
static bool isPowerOn(byte t);
@@ -115,6 +115,7 @@ class TrackManager {
static void applyDCSpeed(byte t);
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC
static int16_t trackPwrMA[MAX_TRACKS]; // for <JL ..> command
#ifdef ARDUINO_ARCH_ESP32
static byte tempProgTrack; // holds the prog track number during join
#endif

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

@@ -3,14 +3,7 @@
#include "StringFormatter.h"
#define VERSION "5.5.23"
// 5.5.24 - Sync DC mode tracks - Nucleo-F4
// 5.5.23 - Reminder loop Idle packet optimization
// 5.5.22 - (5.4.9) Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
// - DCC 5mS gap to same loco DCC packet restriction
// - Catch up MASTER for ESP32 IDF version check
// - Catch up MASTER for Serial input length check
// - Catch up MASTER for JOIN/POWER order change
#define VERSION "5.5.21"
// 5.5.21 - Backed out the broken merge with frequency change and
// 5.5.20 - EXRAIL SET/RESET assert fix
// 5.5.19 - Railcom change to use RailcomCollector device