diff --git a/DCC.cpp b/DCC.cpp index aec1662..b4b6fc1 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -38,6 +38,7 @@ #include "TrackManager.h" #include "DCCTimer.h" #include "Railcom.h" +#include "DCCQueue.h" // This module is responsible for converting API calls into // messages to be sent to the waveform generator. @@ -157,8 +158,8 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) { b[nB++] = speedCode; // for encoding see setThrottle } - - DCCWaveform::mainTrack.schedulePacket(b, nB, 0); + if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority + else DCCQueue::scheduleDCCSpeedPacket( b, nB, 4, cab); } void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) { @@ -172,7 +173,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) { if (byte1!=0) b[nB++] = byte1; b[nB++] = byte2; - DCCWaveform::mainTrack.schedulePacket(b, nB, count); + DCCQueue::scheduleDCCPacket(b, nB, count); } // returns speed steps 0 to 127 (1 == emergency stop) @@ -238,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 } - DCCWaveform::mainTrack.schedulePacket(b, nB, 4); + 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 @@ -339,16 +340,17 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) { // second byte is of the form 1AAACPPG, where C is 1 for on, PP the ports 0 to 3 and G the gate (coil). b[0] = address % 64 + 128; b[1] = ((((address / 64) % 8) << 4) + (port % 4 << 1) + gate % 2) ^ 0xF8; - if (onoff != 0) { - DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat on packet three times -#if defined(EXRAIL_ACTIVE) - RMFT2::activateEvent(address<<2|port,gate); -#endif - } - if (onoff != 1) { + if (onoff==0) { // off packet only b[1] &= ~0x08; // set C to 0 - DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat off packet three times - } + DCCQueue::scheduleDCCPacket(b, 2, 3); + } else if (onoff==1) { // on packet only + DCCQueue::scheduleDCCPacket(b, 2, 3); + } else { // auto timed on then off + DCCQueue::scheduleAccOnOffPacket(b, 2, 3, 100); // On then off after 100mS + } +#if defined(EXRAIL_ACTIVE) + if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate); +#endif } bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) { @@ -398,7 +400,7 @@ whole range of the 11 bits sent to track. | (((~(address>>8)) & 0x07)<<4) // shift out 8, invert, mask 3 bits, shift up 4 | ((address & 0x03)<<1); // mask 2 bits, shift up 1 b[2]=value; - DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), repeats); + DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats); return true; } @@ -417,7 +419,7 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) { b[nB++] = cv2(cv); b[nB++] = bValue; - DCCWaveform::mainTrack.schedulePacket(b, nB, 4); + DCCQueue::scheduleDCCPacket(b, nB, 4); } // @@ -435,7 +437,7 @@ void DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback) { b[nB++] = cv2(cv); b[nB++] = 0; - DCCWaveform::mainTrack.schedulePacket(b, nB, 4); + DCCQueue::scheduleDCCPacket(b, nB, 4); Railcom::anticipate(cab,cv,callback); } @@ -457,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; - DCCWaveform::mainTrack.schedulePacket(b, nB, 4); + DCCQueue::scheduleDCCPacket(b, nB, 4); } bool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) { @@ -494,7 +496,7 @@ b[1]=0b11000001; // 1100-0001 (model time) b[2]=minutes % 60 ; // MM b[3]= 0b11100000 | (minutes/60); // 111H-HHHH weekday not supported b[4]= (suddenChange ? 0b10000000 : 0) | speed; -DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), 2); +DCCQueue::scheduleDCCPacket(b, sizeof(b), 2); return true; } @@ -844,12 +846,17 @@ byte DCC::loopStatus=0; void DCC::loop() { TrackManager::loop(); // power overload checks - issueReminders(); + 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()) { + issueReminders(); + DCCQueue::scheduleNext(); // push through any just created reminder + } + } } void DCC::issueReminders() { - // if the main track transmitter still has a pending packet, skip this time around. - if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return; // Move to next loco slot. If occupied, send a reminder. auto slot = nextLocoReminder; if (slot >= &speedTable[MAX_LOCOS]) slot=&speedTable[0]; // Go to start of table diff --git a/DCCQueue.cpp b/DCCQueue.cpp new file mode 100644 index 0000000..8330e74 --- /dev/null +++ b/DCCQueue.cpp @@ -0,0 +1,185 @@ +/* + * © 2025 Chris Harlow + * 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 . + */ + + +#include "Arduino.h" +#include "defines.h" +#include "DCCQueue.h" +#include "DCCWaveform.h" +#include "DIAG.h" + +// create statics +DCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue(); +DCCQueue* DCCQueue::highPriorityQueue=new DCCQueue(); +PendingSlot* DCCQueue::recycleList=nullptr; + + DCCQueue::DCCQueue() { + head=nullptr; + tail=nullptr; + } + + void DCCQueue::addQueue(PendingSlot* p) { + if (tail) tail->next=p; + else head=p; + tail=p; + p->next=nullptr; + } + + void DCCQueue::jumpQueue(PendingSlot* p) { + p->next=head; + head=p; + if (!tail) tail=p; + } + + + void DCCQueue::recycle(PendingSlot* p) { + p->next=recycleList; + recycleList=p; + } + + // Packet joins end of low priority queue. + 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. + + void DCCQueue::scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco) { + for (auto p=highPriorityQueue->head;p;p=p->next) { + if (p->locoId==loco) { + // replace existing packet + memcpy(p->packet,packet,length); + p->packetLength=length; + p->packetRepeat=repeats; + return; + } + } + highPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco)); + } + + + // ESTOP - + // any outstanding throttle packet for this loco (all if loco=0) discarded + // Packet joins start of queue, + + + void DCCQueue::scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco) { + + // DIAG(F("DCC ESTOP loco=%d"),loco); + + // kill any existing throttle packets for this loco + PendingSlot * previous=nullptr; + auto p=highPriorityQueue->head; + while(p) { + if (loco==0 || p->locoId==loco) { + // drop this packet from the highPriority queue + if (previous) previous->next=p->next; + else highPriorityQueue->head=p->next; + + recycle(p); // recycle this slot + + // address next packet + p=previous?previous->next : highPriorityQueue->head; + } + else { + previous=p; + p=p->next; + } + } + // add the estop packet to the start of the queue + highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0)); + } + + // Accessory gate-On Packet joins end of queue as normal. + // When dequeued, packet is retained at start of queue + // 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); + p->delayOff=delayms; + lowPriorityQueue->addQueue(p); + }; + + + // Obtain packet (fills packet, length and repeats) + // returns 0 length if nothing in queue. + + bool DCCQueue::scheduleNext() { + // check high priority queue first + if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return false; + PendingSlot* previous=nullptr; + 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()startTime) continue; + // 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 highPriorityQueue->head=p->next; + if (!highPriorityQueue->head) highPriorityQueue->tail=nullptr; + + // 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); + } + else recycle(p); // recycle this slot + return true; + } + + // obtain and initialise slot for a PendingSlot. + PendingSlot* DCCQueue::getSlot(PendingType type, byte* packet, byte length, byte repeats,uint16_t loco) { + PendingSlot * p; + if (recycleList) { + p=recycleList; + recycleList=p->next; + } + else { + 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; + memcpy((void*)p->packet,packet,length); + p->locoId=loco; + return p; + } + + diff --git a/DCCQueue.h b/DCCQueue.h new file mode 100644 index 0000000..4c1eacb --- /dev/null +++ b/DCCQueue.h @@ -0,0 +1,84 @@ +/* + * © 2025 Chris Harlow + * 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 . + */ + +#ifndef DCCQueue_h +#define DCCQueue_h +#include "Arduino.h" +#include "DCCWaveform.h" + +enum PendingType:byte {NORMAL_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET}; + struct PendingSlot { + PendingSlot* next; + PendingType type; + byte packetLength; + byte packetRepeat; + byte packet[MAX_PACKET_SIZE]; + + union { // use depends on packet type + 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 + }; + }; + +class DCCQueue { + public: + + + // Non-speed packets are queued in the main queue + 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); + + // ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco + static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco); + + // Accessory gate-On Packet joins end of main queue as normal. + // When dequeued, packet is modified to gate-off and given the delayed start in the high priority queue. + // getNext will ignore this packet until the requested start time. + static void scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms); + + + // Schedules a main track packet from the queues if none pending. + // returns true if a packet was scheduled. + static bool scheduleNext(); + + private: + + // statics to manage high and low priority queues and recycleing of PENDINGs + static PendingSlot* recycleList; + static DCCQueue* highPriorityQueue; + static DCCQueue* lowPriorityQueue; + + DCCQueue(); + + PendingSlot* head; + PendingSlot * tail; + + // obtain and initialise slot for a PendingSlot. + static PendingSlot* getSlot(PendingType type, byte* packet, byte length, byte repeats, uint16_t loco); + static void recycle(PendingSlot* p); + void addQueue(PendingSlot * p); + void jumpQueue(PendingSlot * p); + +}; +#endif \ No newline at end of file diff --git a/version.h b/version.h index 372491e..6334f14 100644 --- a/version.h +++ b/version.h @@ -3,7 +3,8 @@ #include "StringFormatter.h" -#define VERSION "5.5.13" +#define VERSION "5.5.14" +// 5.5.14 - DCC Non-blocking packet queue with priority // 5.5.13 - Update STM32duino core to v19.0.0. for updated PeripheralPins.c in preparation for F429/439ZI Ethernet support // 5.5.12 - Websocket support (wifi only) // 5.5.11 - (5.4.2) accessory command reverse