1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-04-16 18:30:12 +02:00

Compare commits

...

44 Commits

Author SHA1 Message Date
Oskar Senft
3cfd75f2dd
Merge b36fb352b6 into 6d8ca67a2b 2025-03-14 20:58:40 +00:00
Asbelos
6d8ca67a2b STASH upgrade 2025-03-12 11:14:49 +00:00
Asbelos
5d18c910fa More EXRAIL asserts 2025-03-10 10:34:21 +00:00
Asbelos
a4c71889c6 Squashed commit of the following:
commit 5995f62456c7a614bb4607928e78bfc9f907af0d
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Mar 8 17:21:11 2025 +0000

    5.5.17 exrail asserts

commit d6fed968a220a0b4328e60390489f38a544831bc
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Mar 8 17:09:26 2025 +0000

    Additional asserts in EXRAIL
2025-03-08 17:21:49 +00:00
peteGSX
cb24a4dec7 Fix gitignore for docs 2025-03-08 09:14:12 +10:00
peteGSX
cda3b3ca1c Fix workflow 2025-03-08 08:50:05 +10:00
peteGSX
ed21284930 Add requirements.txt 2025-03-08 08:47:38 +10:00
peteGSX
6d9951c871 Fix workflow branch 2025-03-08 08:45:30 +10:00
peteGSX
56add464ac Add EXRAIL docs and workflow 2025-03-08 08:44:34 +10:00
Ash-4
3d794c59d8
Merge branch 'devel-Ash-F439sync' into devel 2025-03-06 23:05:32 -06:00
Asbelos
84918cbf36 DOXYGEN in Exrail 2025-03-05 01:32:57 +00:00
pmantoine
0294409214 STM32 update platformio.ini for F439ZI 2025-03-03 11:18:31 +11:00
Harald Barth
2a007f99dd version 5.5.15 2025-03-02 22:44:01 +01:00
Harald Barth
3095de4672 Merge branch 'devel-more-ether' into devel 2025-03-02 22:19:09 +01:00
Asbelos
dcfb3f061d Squashed commit of the following:
commit 87d0a94443a830510923829fe87e89442c7f3bbe
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Feb 16 14:40:09 2025 +0000

    Update version.h

commit 8c051710c6
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Feb 16 10:57:54 2025 +0000

    Now working

commit 6095bf2b3b
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Feb 15 01:06:55 2025 +0000

    Untested dcc q implementation

commit 52ecab10a7
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Feb 15 00:22:50 2025 +0000

    Initial dcc queue manager
2025-02-16 14:40:57 +00:00
Ash-4
7f488de06e Nucleo-F4 Timer sync F439 build flag 2025-02-11 01:09:11 -06:00
pmantoine
c99eac6ada STM32 platformio.ini PeripheralPins update 2025-02-11 11:47:58 +11:00
Ash-4
56a339a598 Nucleo-F4 Timer Sync - speed fix 2025-02-01 17:28:50 -06:00
Ash-4
a3bd5ac86f DC mode timer sync STM32 2025-01-29 00:54:16 -06:00
Ash-4
da66469faa DC mode timer sync STM32 2025-01-29 00:34:45 -06:00
Ash-4
5d1b3a7a03 DC mode timer sync STM32 and JL command 2025-01-28 16:06:08 -06:00
pmantoine
c93dd75323 Upgrade ststm32 to v18.0.0 for PeripheralPins issue 2024-12-12 15:18:37 +08:00
Harald Barth
519cabffb6 version tag 2024-12-05 22:10:55 +01:00
Harald Barth
aa4306123d more explaining 2024-12-05 22:10:29 +01:00
Harald Barth
ccbff56355 rename to STM32lwipopts.h.copyme because of #include hell 2024-12-05 22:04:44 +01:00
Harald Barth
e0aa16ff2c adjust max default TCP clients to 9 because of bug in LwIP 2024-12-05 22:03:55 +01:00
pmantoine
6b0dc272ea Fixes for Mega, HTONS/L macro 2024-11-27 19:59:15 +08:00
Harald Barth
9602c32ea7 mDNS malloc error fix 2024-11-16 20:50:25 +01:00
Harald Barth
1235c288dc make it compile on all default platforms 2024-11-10 21:11:40 +01:00
Harald Barth
001c4664c1 as MDNS_Generic does not work and we now have our own, remove it 2024-11-10 20:32:06 +01:00
Harald Barth
9786ea9b3a fix compiler warnings 2024-11-10 20:25:20 +01:00
Harald Barth
6a35daab6b remove dead code 2024-11-10 20:20:25 +01:00
Harald Barth
afff10df28 add copyright notices 2024-11-10 20:15:45 +01:00
Harald Barth
b9ce166028 remove everything that are not our changes 2024-11-10 20:13:40 +01:00
Harald Barth
0a96320fd0 tidy up values into name, serviceName, serviceProto and packet generation 2024-11-10 19:48:52 +01:00
Harald Barth
9a6e1707e7 add own mDNS files 2024-11-10 11:31:49 +01:00
Harald Barth
64a34b3a32 proto works 2024-11-10 11:31:14 +01:00
Harald Barth
19f4869401 multicast receive tests 2024-11-09 12:57:49 +01:00
Harald Barth
bcdf9cb1c5 Add STM32lwipopts.h 2024-11-04 17:35:34 +01:00
Harald Barth
701f4e852f Variable number of TCP clients 2024-11-04 17:28:51 +01:00
Harald Barth
657fb7009c tag 2024-11-04 17:27:34 +01:00
Harald Barth
e131a9cce8 Ethernet: Reject additonal connection instead of looping on OVERFLOW 2024-11-02 21:25:21 +01:00
Oskar Senft
b36fb352b6 Reference LiquidCrystal library only if driver is enabled 2024-07-26 10:53:27 -04:00
Oskar Senft
cc759e3d55 Add support for Arduino LiquidCrystal LED displays (Hitachi HD44780) 2024-07-25 18:03:36 -04:00
48 changed files with 6433 additions and 289 deletions

36
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Docs
on:
push:
branches:
- devel
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Install Requirements
run: |
cd docs
python -m pip install --upgrade pip
pip3 install -r requirements.txt
sudo apt-get install doxygen
- name: Build Prod docs
run: |
cd docs
make html
touch _build/html/.nojekyll
- name: Deploy
uses: JamesIves/github-pages-deploy-action@ba1486788b0490a235422264426c45848eac35c6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages # The branch the action should deploy to.
folder: docs/_build/html # The folder the action should deploy.

3
.gitignore vendored
View File

@ -15,3 +15,6 @@ my*.h
compile_commands.json
newcode.txt.old
UserAddin.txt
_build
venv
.DS_Store

View File

@ -58,14 +58,9 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
#ifdef CD_HANDLE_RING
// wifi or ethernet ring streams with multiple client types
RingStream * CommandDistributor::ring=0;
CommandDistributor::clientType CommandDistributor::clients[20]={
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
CommandDistributor::clientType CommandDistributor::clients[MAX_NUM_TCP_CLIENTS]={ NONE_TYPE }; // 0 is and must be NONE_TYPE
// Parse is called by Wifi or Ethernet interface to determine which
// Parse is called by Withrottle or Ethernet interface to determine which
// protocol the client is using and call the appropriate part of dcc++Ex
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
if (clientId>=sizeof (clients)) {

View File

@ -37,13 +37,13 @@
class CommandDistributor {
public:
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE};
enum clientType: byte {NONE_TYPE = 0,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE}; // independent of other types, NONE_TYPE must be 0
private:
static void broadcastToClients(clientType type);
static StringBuffer * broadcastBufferWriter;
#ifdef CD_HANDLE_RING
static RingStream * ring;
static clientType clients[20];
static clientType clients[MAX_NUM_TCP_CLIENTS];
#endif
public :
static void parse(byte clientId,byte* buffer, RingStream * ring);

49
DCC.cpp
View File

@ -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

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
@ -119,6 +119,7 @@ Once a new OPCODE is decided upon, update this list.
#include "version.h"
#include "KeywordHasher.h"
#include "CamParser.h"
#include "Stash.h"
#ifdef ARDUINO_ARCH_ESP32
#include "WifiESP32.h"
#endif
@ -631,7 +632,19 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
else break; // will reply <X>
}
//TrackManager::streamTrackState(NULL,t);
// reinitialize DC mode timer settings following powerON
#ifdef ARDUINO_ARCH_STM32
for (uint8_t i = 0; i < 8; i++) {
TrackManager::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 (uint8_t i = 0; i < 7; i++) {
TrackManager::setTrackPowerF439ZI(i);
}
#endif
return;
}
@ -765,7 +778,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'J' : // throttle info access
{
if ((params<1) | (params>3)) break; // <J>
if (params<1) break; // <J>
//if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0;
switch(p[0]) {
@ -788,16 +801,20 @@ 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"));
return;
case "M"_hk: // <JM> intercepted by EXRAIL
if (params>1) break; // invalid cant do
// <JM> requests stash size so say none.
StringFormatter::send(stream,F("<jM 0>\n"));
return;
case "M"_hk: // <JM> Stash management
if (parseJM(stream, params, p))
return;
break;
case "R"_hk: // <JR> returns rosters
StringFormatter::send(stream, F("<jR"));
@ -1388,6 +1405,40 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
}
#endif
bool DCCEXParser::parseJM(Print *stream, int16_t params, int16_t p[]) {
switch (params) {
case 1: // <JM> list all stashed automations
Stash::list(stream);
return true;
case 2: // <JM id> get stash value
Stash::list(stream, p[1]);
return true;
case 3: //
if (p[1]=="CLEAR"_hk) {
if (p[2]=="ALL"_hk) { // <JM CLEAR ALL>
Stash::clearAll();
return true;
}
Stash::clear(p[2]); // <JM CLEAR id>
return true;
}
Stash::set(p[1], p[2]); // <JM id loco>
return true;
case 4: // <JM CLEAR ANY id>
if (p[1]=="CLEAR"_hk && p[2]=="ANY"_hk) {
// <JM CLEAR ANY id>
Stash::clearAny(p[3]);
return true;
}
default: break;
}
return false;
}
// CALLBACKS must be static
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
{

View File

@ -52,6 +52,7 @@ struct DCCEXParser
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseC(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
static bool parseJM(Print * stream, int16_t params, int16_t p[]);
#ifndef IO_NO_HAL
static bool parseI(Print * stream, int16_t params, int16_t p[]);
#endif

185
DCCQueue.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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()<p->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;
}

84
DCCQueue.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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

View File

@ -1,6 +1,7 @@
/*
* © 2023 Neil McKechnie
* © 2022-2024 Paul M. Antoine
* © 2025 Herb Morton
* © 2021 Mike S
* © 2021, 2023 Harald Barth
* © 2021 Fred Decker
@ -36,6 +37,21 @@
#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.
@ -290,7 +306,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, 3400);
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3600);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
@ -328,7 +344,8 @@ 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 Channel %d, frequency %d"), pin, pin_channel[pin], frequency);
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d Channel %d, frequency %d"), pin, pin_timer[pin], pin_channel[pin], frequency);
resetCounterDCmodeTimers();
}
else
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
@ -336,11 +353,13 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
else
{
// Frequency change request
//DIAG(F("DCCEXanalogWriteFrequency_356::pin %d frequency %d"), pin, frequency);
if (frequency != channel_frequency[pin])
{
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;
@ -365,6 +384,9 @@ 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);
@ -659,4 +681,35 @@ 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, 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

@ -30,6 +30,7 @@
#include "DisplayInterface.h"
#include "SSD1306Ascii.h"
#include "LiquidCrystal_I2C.h"
#include "LiquidCrystal_Parallel.h"
// Implement the Display shim class as a singleton.
@ -38,6 +39,7 @@
// Then Display class talks to the specific device type classes:
// SSD1306AsciiWire for I2C OLED driver with SSD1306 or SH1106 controllers;
// LiquidCrystal_I2C for I2C LCD driver for HD44780 with PCF8574 'backpack'.
// LiquidCrystal_Parallel for HD44780 in parallel mode.
#if defined(OLED_DRIVER)
#define DISPLAY_START(xxx) { \
@ -53,6 +55,14 @@
t->begin(); \
xxx; \
t->refresh();}
#elif defined(PARALLEL_LCD_DRIVER)
#define DISPLAY_START(xxx) { \
DisplayInterface *t = new Display( \
new LiquidCrystal_Parallel(PARALLEL_LCD_DRIVER)); \
t->begin(); \
xxx; \
t->refresh();}
#else
#define DISPLAY_START(xxx) { \
xxx; \

View File

@ -57,6 +57,7 @@
#include "Turntables.h"
#include "IODevice.h"
#include "EXRAILSensor.h"
#include "Stash.h"
// One instance of RMFT clas is used for each "thread" in the automation.
@ -92,8 +93,7 @@ LookList * RMFT2::onBlockEnterLookup=NULL;
LookList * RMFT2::onBlockExitLookup=NULL;
byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr;
int16_t * RMFT2::stashArray=nullptr;
int16_t RMFT2::maxStashId=0;
// getOperand instance version, uses progCounter from instance.
uint16_t RMFT2::getOperand(byte n) {
@ -258,13 +258,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
IODevice::configureInput((VPIN)pin,true);
break;
}
case OPCODE_STASH:
case OPCODE_CLEAR_STASH:
case OPCODE_PICKUP_STASH: {
maxStashId=max(maxStashId,((int16_t)operand));
break;
}
case OPCODE_ATGTE:
case OPCODE_ATLT:
case OPCODE_IFGTE:
@ -352,13 +346,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
}
SKIPOP; // include ENDROUTES opcode
if (compileFeatures & FEATURE_STASH) {
// create the stash array from the highest id found
if (maxStashId>0) stashArray=(int16_t*)calloc(maxStashId+1, sizeof(int16_t));
//TODO check EEPROM and fetch stashArray
}
DIAG(F("EXRAIL %db, fl=%d, stash=%d"),progCounter,MAX_FLAGS, maxStashId);
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS);
// Removed for 4.2.31 new RMFT2(0); // add the startup route
diag=saved_diag;
@ -815,6 +803,10 @@ void RMFT2::loop2() {
case OPCODE_IFCLOSED:
skipIf=Turnout::isThrown(operand);
break;
case OPCODE_IFSTASH:
skipIf=Stash::get(operand)==0;
break;
#ifndef IO_NO_HAL
case OPCODE_IFTTPOSITION: // do block if turntable at this position
@ -1067,30 +1059,32 @@ void RMFT2::loop2() {
break;
case OPCODE_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = invert? -loco : loco;
Stash::set(operand,invert? -loco : loco);
break;
case OPCODE_CLEAR_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = 0;
Stash::clear(operand);
break;
case OPCODE_CLEAR_ALL_STASH:
if (compileFeatures & FEATURE_STASH)
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
Stash::clearAll();
break;
case OPCODE_CLEAR_ANY_STASH:
if (loco) Stash::clearAny(loco);
break;
case OPCODE_PICKUP_STASH:
if (compileFeatures & FEATURE_STASH) {
int16_t x=stashArray[operand];
if (x>=0) {
loco=x;
invert=false;
break;
}
loco=-x;
invert=true;
{
auto x=Stash::get(operand);
if (x>=0) {
loco=x;
invert=false;
}
else {
loco=-x;
invert=true;
}
}
break;

View File

@ -77,6 +77,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
OPCODE_CLEAR_ANY_STASH,
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
OPCODE_NEOPIXEL,
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
@ -93,7 +94,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO,
OPCODE_IFTTPOSITION
OPCODE_IFTTPOSITION,
OPCODE_IFSTASH,
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@ -137,7 +139,7 @@ enum SignalType {
static const byte FEATURE_LCC = 0x40;
static const byte FEATURE_ROSTER= 0x20;
static const byte FEATURE_ROUTESTATE= 0x10;
static const byte FEATURE_STASH = 0x08;
// spare = 0x08;
static const byte FEATURE_BLINK = 0x04;
static const byte FEATURE_SENSOR = 0x02;
static const byte FEATURE_BLOCK = 0x01;

File diff suppressed because it is too large Load Diff

View File

@ -181,36 +181,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
return;
}
break;
case "M"_hk:
// NOTE: we only need to handle valid calls here because
// DCCEXParser has to have code to handle the <J<> cases where
// exrail isnt involved anyway.
// This entire code block is compiled out if STASH macros not used
if (!(compileFeatures & FEATURE_STASH)) return;
if (paramCount==1) { // <JM>
StringFormatter::send(stream,F("<jM %d>\n"),maxStashId);
opcode=0;
break;
}
if (paramCount==2) { // <JM id>
if (p[1]<=0 || p[1]>maxStashId) break;
StringFormatter::send(stream,F("<jM %d %d>\n"),
p[1],stashArray[p[1]]);
opcode=0;
break;
}
if (paramCount==3) { // <JM id cab>
if (p[1]<=0 || p[1]>maxStashId) break;
stashArray[p[1]]=p[2];
opcode=0;
break;
}
break;
default:
break;
}
break;
case 'K': // <K blockid loco> Block enter
case 'k': // <k blockid loco> Block exit
@ -223,6 +194,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
break;
}
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
@ -268,15 +240,6 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
slot.id);
}
}
if (compileFeatures & FEATURE_STASH) {
for (int i=1;i<=maxStashId;i++) {
if (stashArray[i])
StringFormatter::send(stream,F("\nSTASH[%d] Loco=%d"),
i, stashArray[i]);
}
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}

165
EXRAILAsserts.h Normal file
View File

@ -0,0 +1,165 @@
/*
* © 2020-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 <https://www.gnu.org/licenses/>.
*/
// This file checks the myAutomation for errors by generating a list of compile time asserts.
// Assert Pass 1 Collect sequence numbers.
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
#undef ROUTE
#define ROUTE(id, description) id,
#undef SEQUENCE
#define SEQUENCE(id) id,
constexpr int16_t compileTimeSequenceList[]={
#include "myAutomation.h"
0
};
constexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;
// Compile time function to check for sequence number duplication
constexpr int16_t seqCount(const int16_t value, const int16_t pos=0, const int16_t count=0 ) {
return pos>=stuffSize? count :
seqCount(value,pos+1,count+((compileTimeSequenceList[pos]==value)?1:0));
}
// Build a compile time blacklist of pin numbers.
// Includes those defined in defaults.h for the cpu (PIN_BLACKLIST)
// and cheats in the motor shield pins from config.h (MOTOR_SHIELD_TYPE)
// for reference the MotorDriver constructor is:
// MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin, byte current_pin,
// float senseFactor, unsigned int tripMilliamps, byte faultPin);
// create capture macros to reinterpret MOTOR_SHIELD_TYPE from configuration
#define new
#define MotorDriver(power_pin,signal_pin,signal_pin2, \
brake_pin,current_pin,senseFactor,tripMilliamps,faultPin) \
abs(power_pin),abs(signal_pin),abs(signal_pin2),abs(brake_pin),abs(current_pin),abs(faultPin)
#ifndef PIN_BLACKLIST
#define PIN_BLACKLIST UNUSED_PIN
#endif
#define MDFURKLE(stuff) MDFURKLE2(stuff)
#define MDFURKLE2(description,...) REMOVE_TRAILING_COMMA(__VA_ARGS__)
#define REMOVE_TRAILING_COMMA(...) __VA_ARGS__
constexpr int16_t compileTimePinBlackList[]={
PIN_BLACKLIST, MDFURKLE(MOTOR_SHIELD_TYPE)
};
constexpr int16_t pbSize=sizeof(compileTimePinBlackList)/sizeof(int16_t) - 1;
// remove capture macros
#undef new
#undef MotorDriver
// Compile time function to check for dangerous pins.
constexpr bool unsafePin(const int16_t value, const uint16_t pos=0 ) {
return pos>=pbSize? false :
compileTimePinBlackList[pos]==value
|| unsafePin(value,pos+1);
}
//pass 2 apply static asserts:
// check call and follows etc for existing sequence numbers
// check sequence numbers for duplicates
// check range on LATCH/UNLATCH
// check range on RESERVE/FREE
// check range on SPEED/FWD/REV
// check range on SET/RESET (pins that are not safe to use in EXRAIL)
//
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef ASPECT
#define ASPECT(address,value) static_assert(address <=2044, "invalid Address"); \
static_assert(address>=-3, "Invalid value");
// check references to sequences/routes/automations
#undef CALL
#define CALL(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef FOLLOW
#define FOLLOW(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef START
#define START(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef SENDLOCO
#define SENDLOCO(cab,id) static_assert(seqCount(id)>0,"Sequence not found");
#undef ROUTE_ACTIVE
#define ROUTE_ACTIVE(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_INACTIVE
#define ROUTE_INACTIVE(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_HIDDEN
#define ROUTE_HIDDEN(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_DISABLED
#define ROUTE_DISABLED(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) static_assert(seqCount(id)>0,"Route not found");
#undef LATCH
#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef UNLATCH
#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef RESERVE
#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef FREE
#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef IFRESERVE
#define IFRESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
//check speeds
#undef SPEED
#define SPEED(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
#undef FWD
#define FWD(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
#undef REV
#define REV(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
// check duplicate sequences
#undef SEQUENCE
#define SEQUENCE(id) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
#undef AUTOMATION
#define AUTOMATION(id,description) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
#undef ROUTE
#define ROUTE(id,description) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
// check dangerous pins
#define _PIN_RESERVED_ "\n\nUSER ERROR: Pin is used by Motor Shield or other critical function.\n"
#undef SET
#define SET(vpin) static_assert(!unsafePin(vpin),"SET(" #vpin ")" _PIN_RESERVED_);
#undef RESET
#define RESET(vpin) static_assert(!unsafePin(vpin),"RESET(" #vpin ")" _PIN_RESERVED_);
#undef BLINK
#define BLINK(vpin,onDuty,offDuty) static_assert(!unsafePin(vpin),"BLINK(" #vpin ")" _PIN_RESERVED_);
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) \
static_assert(!unsafePin(redpin),"Red pin " #redpin _PIN_RESERVED_); \
static_assert(amberpin==0 ||!unsafePin(amberpin),"Amber pin " #amberpin _PIN_RESERVED_); \
static_assert(!unsafePin(greenpin),"Green pin " #greenpin _PIN_RESERVED_);
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) \
static_assert(!unsafePin(redpin),"Red pin " #redpin _PIN_RESERVED_); \
static_assert(amberpin==0 ||!unsafePin(amberpin),"Amber pin " #amberpin _PIN_RESERVED_); \
static_assert(!unsafePin(greenpin),"Green pin " #greenpin _PIN_RESERVED_);
// and run the assert pass.
#include "myAutomation.h"

View File

@ -86,72 +86,8 @@
#define ALIAS(name,value...) const int name= #value[0] ? value+0: -__COUNTER__ ;
#include "myAutomation.h"
// Pass 1d Detect sequence duplicates.
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
#undef ROUTE
#define ROUTE(id, description) id,
#undef SEQUENCE
#define SEQUENCE(id) id,
constexpr int16_t compileTimeSequenceList[]={
#include "myAutomation.h"
0
};
constexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;
// Compile time function to check for sequence nos.
constexpr bool hasseq(const int16_t value, const int16_t pos=0 ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1);
}
// Compile time function to check for duplicate sequence nos.
constexpr bool hasdup(const int16_t value, const int16_t pos ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1)
|| hasdup(compileTimeSequenceList[pos],pos+1);
}
static_assert(!hasdup(compileTimeSequenceList[0],1),"Duplicate SEQUENCE/ROUTE/AUTOMATION detected");
//pass 1s static asserts to
// - check call and follows etc for existing sequence numbers
// - check range on LATCH/UNLATCH
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef ASPECT
#define ASPECT(address,value) static_assert(address <=2044, "invalid Address"); \
static_assert(address>=-3, "Invalid value");
#undef CALL
#define CALL(id) static_assert(hasseq(id),"Sequence not found");
#undef FOLLOW
#define FOLLOW(id) static_assert(hasseq(id),"Sequence not found");
#undef START
#define START(id) static_assert(hasseq(id),"Sequence not found");
#undef SENDLOCO
#define SENDLOCO(cab,id) static_assert(hasseq(id),"Sequence not found");
#undef LATCH
#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef UNLATCH
#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef RESERVE
#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef FREE
#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef SPEED
#define SPEED(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef FWD
#define FWD(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef REV
#define REV(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#include "myAutomation.h"
// Perform compile time asserts to check the script for errors
#include "EXRAILAsserts.h"
// Pass 1g Implants STEALTH_GLOBAL in correct place
#include "EXRAIL2MacroReset.h"
@ -218,14 +154,6 @@ bool exrailHalSetup() {
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
#undef CLEAR_STASH
#define CLEAR_STASH(id) | FEATURE_STASH
#undef CLEAR_ALL_STASH
#define CLEAR_ALL_STASH | FEATURE_STASH
#undef PICKUP_STASH
#define PICKUP_STASH(id) | FEATURE_STASH
#undef STASH
#define STASH(id) | FEATURE_STASH
#undef BLINK
#define BLINK(vpin,onDuty,offDuty) | FEATURE_BLINK
#undef ONBUTTON
@ -499,6 +427,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define CALL(route) OPCODE_CALL,V(route),
#define CLEAR_STASH(id) OPCODE_CLEAR_STASH,V(id),
#define CLEAR_ALL_STASH OPCODE_CLEAR_ALL_STASH,V(0),
#define CLEAR_ANY_STASH OPCODE_CLEAR_ANY_STASH,V(0),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
#ifndef IO_NO_HAL
@ -545,6 +474,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define IFSTASH(stash_id) OPCODE_IFSTASH,V(stash_id),
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
#ifndef IO_NO_HAL
@ -607,9 +537,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define PAUSE OPCODE_PAUSE,0,0,
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#ifndef DISABLE_PROG
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#endif
#define POWEROFF OPCODE_POWEROFF,0,0,
#define POWERON OPCODE_POWERON,0,0,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),

200
EXmDNS.cpp Normal file
View File

@ -0,0 +1,200 @@
/*
* © 2024 Harald Barth
* © 2024 Paul M. Antoine
* 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/>.
*/
#include <Arduino.h>
#include "EthernetInterface.h"
#ifdef DO_MDNS
#include "EXmDNS.h"
// fixed values for mDNS
static IPAddress mdnsMulticastIPAddr = IPAddress(224, 0, 0, 251);
#define MDNS_SERVER_PORT 5353
// dotToLen()
// converts stings of form ".foo.barbar.x" to a string with the
// dots replaced with lenght. So string above would result in
// "\x03foo\x06barbar\x01x" in C notation. If not NULL, *substr
// will point to the beginning of the last component, in this
// example that would be "\x01x".
//
static void dotToLen(char *str, char **substr) {
char *dotplace = NULL;
char *s;
byte charcount = 0;
for (s = str;/*see break*/ ; s++) {
if (*s == '.' || *s == '\0') {
// take care of accumulated
if (dotplace != NULL && charcount != 0) {
*dotplace = charcount;
}
if (*s == '\0')
break;
if (substr && *s == '.')
*substr = s;
// set new values
dotplace = s;
charcount = 0;
} else {
charcount++;
}
}
}
MDNS::MDNS(EthernetUDP& udp) {
_udp = &udp;
}
MDNS::~MDNS() {
_udp->stop();
if (_name) free(_name);
if (_serviceName) free(_serviceName);
if (_serviceProto) free(_serviceProto);
}
int MDNS::begin(const IPAddress& ip, char* name) {
// if we were called very soon after the board was booted, we need to give the
// EthernetShield (WIZnet) some time to come up. Hence, we delay until millis() is at
// least 3000. This is necessary, so that if we need to add a service record directly
// after begin, the announce packet does not get lost in the bowels of the WIZnet chip.
//while (millis() < 3000)
// delay(100);
_ipAddress = ip;
_name = (char *)malloc(strlen(name)+2);
byte n;
for(n = 0; n<strlen(name); n++)
_name[n+1] = name[n];
_name[n+1] = '\0';
_name[0] = '.';
dotToLen(_name, NULL);
return _udp->beginMulticast(mdnsMulticastIPAddr, MDNS_SERVER_PORT);
}
int MDNS::addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto) {
// we ignore proto, assume TCP
(void)proto;
_serviceName = (char *)malloc(strlen(name) + 2);
byte n;
for(n = 0; n<strlen(name); n++)
_serviceName[n+1] = name[n];
_serviceName[n+1] = '\0';
_serviceName[0] = '.';
_serviceProto = NULL; //to be filled in
dotToLen(_serviceName, &_serviceProto);
_servicePort = port;
return 1;
}
static char dns_rr_services[] = "\x09_services\x07_dns-sd\x04_udp\x05local";
static char dns_rr_tcplocal[] = "\x04_tcp\x05local";
static char *dns_rr_local = dns_rr_tcplocal + dns_rr_tcplocal[0] + 1;
typedef struct _DNSHeader_t
{
uint16_t xid;
uint16_t flags; // flags condensed
uint16_t queryCount;
uint16_t answerCount;
uint16_t authorityCount;
uint16_t additionalCount;
} __attribute__((__packed__)) DNSHeader_t;
//
// MDNS::run()
// This broadcasts whatever we got evey BROADCASTTIME seconds.
// Why? Too much brokenness i all mDNS implementations available
//
void MDNS::run() {
static long int lastrun = BROADCASTTIME * 1000UL;
unsigned long int now = millis();
if (!(now - lastrun > BROADCASTTIME * 1000UL)) {
return;
}
lastrun = now;
DNSHeader_t dnsHeader = {0, 0, 0, 0, 0, 0};
// DNSHeader_t dnsHeader = { 0 };
_udp->beginPacket(mdnsMulticastIPAddr, MDNS_SERVER_PORT);
// dns header
dnsHeader.flags = HTONS((uint16_t)0x8400); // Response, authorative
dnsHeader.answerCount = HTONS(4 /*5 if TXT but we do not do that */);
_udp->write((uint8_t*)&dnsHeader, sizeof(DNSHeader_t));
// rr #1, the PTR record from generic _services.x.local to service.x.local
_udp->write((uint8_t*)dns_rr_services, sizeof(dns_rr_services));
byte buf[10];
buf[0] = 0x00;
buf[1] = 0x0c; //PTR
buf[2] = 0x00;
buf[3] = 0x01; //IN
*((uint32_t*)(buf+4)) = HTONL(120); //TTL in sec
*((uint16_t*)(buf+8)) = HTONS( _serviceProto[0] + 1 + strlen(dns_rr_tcplocal) + 1);
_udp->write(buf, 10);
_udp->write(_serviceProto,_serviceProto[0]+1);
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
// rr #2, the PTR record from proto.x to name.proto.x
_udp->write(_serviceProto,_serviceProto[0]+1);
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
*((uint16_t*)(buf+8)) = HTONS(strlen(_serviceName) + strlen(dns_rr_tcplocal) + 1); // recycle most of buf
_udp->write(buf, 10);
_udp->write(_serviceName, strlen(_serviceName));
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
// rr #3, the SRV record for the service that points to local name
_udp->write(_serviceName, strlen(_serviceName));
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
buf[1] = 0x21; // recycle most of buf but here SRV
buf[2] = 0x80; // cache flush
*((uint16_t*)(buf+8)) = HTONS(strlen(_name) + strlen(dns_rr_local) + 1 + 6);
_udp->write(buf, 10);
byte srv[6];
// priority and weight
srv[0] = srv[1] = srv[2] = srv[3] = 0;
// port
*((uint16_t*)(srv+4)) = HTONS(_servicePort);
_udp->write(srv, 6);
// target
_udp->write(_name, _name[0]+1);
_udp->write(dns_rr_local, strlen(dns_rr_local)+1);
// rr #4, the A record for the name.local
_udp->write(_name, _name[0]+1);
_udp->write(dns_rr_local, strlen(dns_rr_local)+1);
buf[1] = 0x01; // recycle most of buf but here A
*((uint16_t*)(buf+8)) = HTONS(4);
_udp->write(buf, 10);
byte ip[4];
ip[0] = _ipAddress[0];
ip[1] = _ipAddress[1];
ip[2] = _ipAddress[2];
ip[3] = _ipAddress[3];
_udp->write(ip, 4);
_udp->endPacket();
_udp->flush();
//
}
#endif //DO_MDNS

50
EXmDNS.h Normal file
View File

@ -0,0 +1,50 @@
/*
* © 2024 Harald Barth
* © 2024 Paul M. Antoine
* 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/>.
*/
#ifdef DO_MDNS
#define BROADCASTTIME 15 //seconds
// We do this ourselves because every library is different and/or broken...
#define HTONS(x) ((uint16_t)(((x) << 8) | (((x) >> 8) & 0xFF)))
#define HTONL(x) ( ((uint32_t)(x) << 24) | (((uint32_t)(x) << 8) & 0xFF0000) | \
(((uint32_t)(x) >> 8) & 0xFF00) | ((uint32_t)(x) >> 24) )
typedef enum _MDNSServiceProtocol_t
{
MDNSServiceTCP,
MDNSServiceUDP
} MDNSServiceProtocol_t;
class MDNS {
public:
MDNS(EthernetUDP& udp);
~MDNS();
int begin(const IPAddress& ip, char* name);
int addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto);
void run();
private:
EthernetUDP *_udp;
IPAddress _ipAddress;
char* _name;
char* _serviceName;
char* _serviceProto;
int _servicePort;
};
#endif //DO_MDNS

View File

@ -31,13 +31,12 @@
#include "CommandDistributor.h"
#include "WiThrottle.h"
#include "DCCTimer.h"
#if __has_include ( "MDNS_Generic.h")
#include "MDNS_Generic.h"
#define DO_MDNS
EthernetUDP udp;
MDNS mdns(udp);
#endif
#ifdef DO_MDNS
#include "EXmDNS.h"
EthernetUDP udp;
MDNS mdns(udp);
#endif
//extern void looptimer(unsigned long timeout, const FSH* message);
#define looptimer(a,b)
@ -116,10 +115,10 @@ void EthernetInterface::setup()
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
#ifdef DO_MDNS
mdns.begin(Ethernet.localIP(), WIFI_HOSTNAME); // hostname
if (!mdns.begin(Ethernet.localIP(), (char *)WIFI_HOSTNAME))
DIAG(F("mdns.begin fail")); // hostname
mdns.addServiceRecord(WIFI_HOSTNAME "._withrottle", IP_PORT, MDNSServiceTCP);
// Not sure if we need to run it once, but just in case!
mdns.run();
mdns.run(); // run it right away to get out info ASAP
#endif
connected=true;
}
@ -144,7 +143,9 @@ void EthernetInterface::acceptClient() { // STM32 version
return;
}
}
DIAG(F("Ethernet OVERFLOW"));
// reached here only if more than MAX_SOCK_NUM clients want to connect
DIAG(F("Ethernet more than %d clients, not accepting new connection"), MAX_SOCK_NUM);
client.stop();
}
#else
void EthernetInterface::acceptClient() { // non-STM32 version

View File

@ -3,7 +3,7 @@
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2024 Harald Barth
* © 2020-2024 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
@ -31,24 +31,32 @@
#define EthernetInterface_h
#include "defines.h"
#if ETHERNET_ON == true
#include "DCCEXParser.h"
#include <Arduino.h>
//#include <avr/pgmspace.h>
#if defined (ARDUINO_TEENSY41)
#include <NativeEthernet.h> //TEENSY Ethernet Treiber
#include <NativeEthernetUdp.h>
#ifndef MAX_SOCK_NUM
#define MAX_SOCK_NUM 4
#endif
// can't use our MDNS because of a namespace clash with Teensy's NativeEthernet library!
// #define DO_MDNS
#elif defined (ARDUINO_NUCLEO_F429ZI) || defined (ARDUINO_NUCLEO_F439ZI) || defined (ARDUINO_NUCLEO_F4X9ZI)
#include <LwIP.h>
// #include "STM32lwipopts.h"
#include <STM32Ethernet.h>
#include <lwip/netif.h>
extern "C" struct netif gnetif;
#define STM32_ETHERNET
#define MAX_SOCK_NUM 8
#define MAX_SOCK_NUM MAX_NUM_TCP_CLIENTS
#define DO_MDNS
#else
#include "Ethernet.h"
#define DO_MDNS
#endif
#include "RingStream.h"
/**
@ -77,5 +85,5 @@ class EthernetInterface {
static void dropClient(byte socketnum);
};
#endif // ETHERNET_ON
#endif

View File

@ -1 +1 @@
#define GITHUB_SHA "devel-202501171827Z"
#define GITHUB_SHA "devel-202503022043Z"

View File

@ -1,5 +1,7 @@
/*
* © 2024, Chris Harlow. All rights reserved.
* © 2024, Chris Harlow.
* © 2025 Herb Morton
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@ -35,4 +37,4 @@ It has been moved here to be easier to maintain than editing IODevice.h
#include "IO_EXSensorCAM.h"
#include "IO_DS1307.h"
#include "IO_I2CRailcom.h"
#include "IO_HALDisplay.h"

View File

@ -0,0 +1,61 @@
/*
* © 2024, Oskar Senft. 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-EX. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "LiquidCrystal_Parallel.h"
#include "DIAG.h"
LiquidCrystal_Parallel::LiquidCrystal_Parallel(
uint16_t cols, uint16_t rows,
uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7)
: _cols(cols), _rows(rows), lcd(rs, rw, enable, d4, d5, d6, d7)
{
}
bool LiquidCrystal_Parallel::begin()
{
lcd.begin(getNumCols(), getNumRows());
lcd.noCursor();
return true;
}
void LiquidCrystal_Parallel::clearNative()
{
lcd.clear();
}
void LiquidCrystal_Parallel::setRowNative(byte row)
{
if (row >= getNumRows())
{
row = getNumRows() - 1; // we count rows starting w/0
}
lcd.setCursor(0, row);
}
size_t LiquidCrystal_Parallel::writeNative(uint8_t value)
{
return lcd.write(value);
}
bool LiquidCrystal_Parallel::isBusy()
{
return false;
}

69
LiquidCrystal_Parallel.h Normal file
View File

@ -0,0 +1,69 @@
/*
* © 2024, Oskar Senft. 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 LiquidCrystal_Parallel_h
#define LiquidCrystal_Parallel_h
#include <Arduino.h>
#include "Display.h"
#ifdef PARALLEL_LCD_DRIVER
// Only use the Arduino library if the driver is actually enabled.
#include <LiquidCrystal.h>
#else
// If the driver is not enabled, use a dummy version instead.
class LiquidCrystal
{
public:
LiquidCrystal(uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) {};
void begin(uint16_t cols, uint16_t rows) {};
void noCursor() {};
void setCursor(uint16_t col, uint16_t row) {};
void clear() {};
size_t write(uint8_t val) { return 0; };
};
#endif
// Support for an LCD based on the Hitachi HD44780 (or a compatible) chipset
// as supported by Arduino's LiquidCrystal library.
class LiquidCrystal_Parallel : public DisplayDevice
{
public:
// Specify the display's number of columns and rows as well
// as Arduino pins numbers for the display's pins (4-bit mode)
LiquidCrystal_Parallel(uint16_t cols, uint16_t rows,
uint8_t rs, uint8_t rw, uint8_t enable,
uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7);
bool begin() override;
void clearNative() override;
void setRowNative(byte line) override;
size_t writeNative(uint8_t c) override;
bool isBusy() override;
uint16_t getNumCols() override { return _cols; }
uint16_t getNumRows() override { return _rows; }
private:
LiquidCrystal lcd;
const uint16_t _cols;
const uint16_t _rows;
};
#endif

View File

@ -1,6 +1,6 @@
/*
* © 2022-2024 Paul M Antoine
* © 2024 Herb Morton
* © 2024-2025 Herb Morton
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
@ -371,8 +371,10 @@ 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);
//DIAG(F("MotorDriver_cpp_374_DCCEXanalogWriteFequency::Pin %d, frequency %d, tSpeed %d"), brakePin, f, tSpeed);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
//DIAG(F("MotorDriver_cpp_375_DCCEXanalogWrite::brakePin %d, frequency %d, invertBrake"), brakePin, brake, invertBrake);
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);

21
Release_Notes/Stash.md Normal file
View File

@ -0,0 +1,21 @@
# The STASH feature of exrail.
STASH is used for scenarios where it is helpful to relate a loco id to where it is parked. For example a fiddle yard may have 10 tracks and it's much easier for the operator to select a train to depart by using the track number, or pressing a button relating to that track, rather than by knowing the loco id which may be difficult to see.
Automated yard parking can use the stash to determine which tracks are empty without the need for block occupancy detectors.
Note that a negative locoid may be stashed to indicate that the loco will operate with inverted direction. For example a loco facing backwards, with the INVERT_DRECTION state may be stashed by exrail and the invert state will be restored along with the loco id when using the PICKUP_STASH. CLEAR_ANY_STASH will clear all references to the loco regardless of direction.
The following Stash commands are available:
| EXRAIL command | Serial protocol | function |
| -------------- | --------------- | -------- |
| STASH(s) | `<JM s locoid>` | Save the current loco id in the stash array element s. |
| CLEAR_STASH(s) | `<JM s 0>` | Sets stash array element s to zero. |
| CLEAR_ALL_STASH | `<JM CLEAR ALL>` | sets all stash entries to zero |
| CLEAR_ANY_STASH | `<JM CLEAR ANY locoid>` | removes current loco from all stash elements |
| PICKUP_STASH(s) | N/A | sets current loco to stash element s |
| IFSTASH(s) | N/A | True if stash element s is not zero |
| N/A | `<JM>` | query all stashes (returns `<jM s loco>` where loco is not zero)
| N/A | `<JM stash>` | Query loco in stash (returns `<jM s loco>`)

101
STM32lwipopts.h.copyme Normal file
View File

@ -0,0 +1,101 @@
/*
* © 2024 Harald Barth
* 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/>.
*/
//
// Rewrite of the STM32lwipopts.h file from STM
// To be copied into where lwipopts_default.h resides
// typically into STM32Ethernet/src/STM32lwipopts.h
// or STM32Ethernet\src\STM32lwipopts.h
// search for `lwipopts_default.h` and copy this file into the
// same directory but name it STM32lwipopts.h
//
#ifndef __STM32LWIPOPTS_H__
#define __STM32LWIPOPTS_H__
// include this here and then override things we do differnet
#include "lwipopts_default.h"
// we can not include our "defines.h" here
// so we need to duplicate that define
#define MAX_NUM_TCP_CLIENTS_HERE 9
#ifdef MAX_NUM_TCP_CLIENTS
#if MAX_NUM_TCP_CLIENTS != MAX_NUM_TCP_CLIENTS_HERE
#error MAX_NUM_TCP_CLIENTS and MAX_NUM_TCP_CLIENTS_HERE must be same
#endif
#else
#define MAX_NUM_TCP_CLIENTS MAX_NUM_TCP_CLIENTS_HERE
#endif
// increase ARP cache
#undef MEMP_NUM_APR_QUEUE
#define MEMP_NUM_ARP_QUEUE MAX_NUM_TCP_CLIENTS+3 // one for each client (all on different HW) and a few extra
// Example for debug
//#define LWIP_DEBUG 1
//#define TCP_DEBUG LWIP_DBG_ON
// NOT STRICT NECESSARY ANY MORE BUT CAN BE USED TO SAVE RAM
#undef MEM_LIBC_MALLOC
#define MEM_LIBC_MALLOC 1 // use the same malloc as for everything else
#undef MEMP_MEM_MALLOC
#define MEMP_MEM_MALLOC 1 // uses malloc which means no pools which means slower but not mean 32KB up front
#undef MEMP_NUM_TCP_PCB
#define MEMP_NUM_TCP_PCB MAX_NUM_TCP_CLIENTS+1 // one extra so we can reject number N+1 from our code
#define MEMP_NUM_TCP_PCB_LISTEN 6
#undef MEMP_NUM_TCP_SEG
#define MEMP_NUM_TCP_SEG MAX_NUM_TCP_CLIENTS
#undef MEMP_NUM_SYS_TIMEOUT
#define MEMP_NUM_SYS_TIMEOUT MAX_NUM_TCP_CLIENTS+2
#undef PBUF_POOL_SIZE
#define PBUF_POOL_SIZE MAX_NUM_TCP_CLIENTS
#undef LWIO_ICMP
#define LWIP_ICMP 1
#undef LWIP_RAW
#define LWIP_RAW 1 /* PING changed to 1 */
#undef DEFAULT_RAW_RECVMBOX_SIZE
#define DEFAULT_RAW_RECVMBOX_SIZE 3 /* for ICMP PING */
#undef LWIP_DHCP
#define LWIP_DHCP 1
#undef LWIP_UDP
#define LWIP_UDP 1
/*
The STM32F4x7 allows computing and verifying the IP, UDP, TCP and ICMP checksums by hardware:
- To use this feature let the following define uncommented.
- To disable it and process by CPU comment the the checksum.
*/
#if CHECKSUM_GEN_TCP == 1
#error On STM32 TCP checksum should be in HW
#endif
#undef LWIP_IGMP
#define LWIP_IGMP 1
//#define SO_REUSE 1
//#define SO_REUSE_RXTOALL 1
#endif /* __STM32LWIPOPTS_H__ */

89
Stash.cpp Normal file
View File

@ -0,0 +1,89 @@
/*
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-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 "Stash.h"
#include "StringFormatter.h"
Stash::Stash(int16_t stash_id, int16_t loco_id) {
this->stashId = stash_id;
this->locoId = loco_id;
this->next = first;
first = this;
}
void Stash::clearAll() {
for (auto s=first;s;s=s->next) {
s->locoId = 0;
s->stashId =0;
}
}
void Stash::clearAny(int16_t loco_id) {
auto lid=abs(loco_id);
for (auto s=first;s;s=s->next)
if (abs(s->locoId) == lid) {
s->locoId = 0;
s->stashId =0;
}
}
void Stash::clear(int16_t stash_id) {
set(stash_id,0);
}
int16_t Stash::get(int16_t stash_id) {
for (auto s=first;s;s=s->next)
if (s->stashId == stash_id) return s->locoId;
return 0;
}
void Stash::set(int16_t stash_id, int16_t loco_id) {
// replace any existing stash
for (auto s=first;s;s=s->next)
if (s->stashId == stash_id) {
s->locoId=loco_id;
if (loco_id==0) s->stashId=0; // recycle
return;
}
if (loco_id==0) return; // no need to create a zero entry.
// replace any empty stash
for (auto s=first;s;s=s->next)
if (s->locoId == 0) {
s->locoId=loco_id;
s->stashId=stash_id;
return;
}
// create a new stash
new Stash(stash_id, loco_id);
}
void Stash::list(Print * stream, int16_t stash_id) {
bool sent=false;
for (auto s=first;s;s=s->next)
if ((s->locoId) && (stash_id==0 || s->stashId==stash_id)) {
StringFormatter::send(stream,F("<jM %d %d>\n"),
s->stashId,s->locoId);
sent=true;
}
if (!sent) StringFormatter::send(stream,F("<jM %d 0>\n"),
stash_id);
}
Stash* Stash::first=nullptr;

39
Stash.h Normal file
View File

@ -0,0 +1,39 @@
/*
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-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 Stash_h
#define Stash_h
#include <Arduino.h>
class Stash {
public:
static void clear(int16_t stash_id);
static void clearAll();
static void clearAny(int16_t loco_id);
static int16_t get(int16_t stash_id);
static void set(int16_t stash_id, int16_t loco_id);
static void list(Print * stream, int16_t stash_id=0); // id0 = LIST ALL
private:
Stash(int16_t stash_id, int16_t loco_id);
static Stash* first;
Stash* next;
int16_t stashId;
int16_t locoId;
};
#endif

View File

@ -2,7 +2,7 @@
* © 2022-2025 Chris Harlow
* © 2022-2024 Harald Barth
* © 2023-2024 Paul M. Antoine
* © 2024 Herb Morton
* © 2024-2025 Herb Morton
* © 2023 Colin Murdoch
* All rights reserved.
*
@ -42,6 +42,7 @@
MotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };
int16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };
int16_t TrackManager::tPwr_mA[8]={0,0,0,0,0,0,0,0};
int8_t TrackManager::lastTrack=-1;
bool TrackManager::progTrackSyncMain=false;
@ -646,6 +647,33 @@ void TrackManager::reportCurrent(Print* stream) {
StringFormatter::send(stream,F(">\n"));
}
void TrackManager::reportCurrentLCD(uint8_t display, byte row) {
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
tPwr_mA[t]=(3*tPwr_mA[t]>>2) + ((track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :
track[t]->raw2mA(track[t]->getCurrentRaw(false)));
if (tMode & TRACK_MODE_DC) { // Test if track is in DC or DCX mode
SCREEN(display, row+t, F("%c: %S %d ON %dmA"), t+'A', (TrackManager::getModeName(tMode)),DCAddr, tPwr_mA[t]>>2);
}
else { // formats without DCAddress
SCREEN(display, row+t, F("%c: %S ON %dmA"), t+'A', (TrackManager::getModeName(tMode)), tPwr_mA[t]>>2);
}
}
else { // if power is off do this section
if (tMode & TRACK_MODE_DC) { // DC / DCX
SCREEN(display, row+t, F("Track %c: %S %d OFF"), t+'A', (TrackManager::getModeName(tMode)),DCAddr);
}
else { // Not DC or DCX
SCREEN(display, row+t, F("Track %c: %S OFF"), t+'A', (TrackManager::getModeName(tMode)));
}
}
}
}
void TrackManager::reportGauges(Print* stream) {
StringFormatter::send(stream,F("<jG"));
FOR_EACH_TRACK(t) {
@ -710,3 +738,38 @@ 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,6 +1,7 @@
/*
* © 2022 Chris Harlow
* © 2022-2024 Harald Barth
* © 2025 Herb Morton
* © 2023 Colin Murdoch
*
* All rights reserved.
@ -66,6 +67,7 @@ class TrackManager {
static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);}
static void setTrackPower(POWERMODE mode, byte t);
static void setTrackPowerF439ZI(byte t);
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);}
@ -87,6 +89,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);
@ -105,6 +108,7 @@ class TrackManager {
private:
#endif
static MotorDriver* track[MAX_TRACKS];
static int16_t tPwr_mA[8]; // for <JL ..> command
private:
static void addTrack(byte t, MotorDriver* driver);

View File

@ -137,6 +137,16 @@ The configuration file for DCC-EX Command Station
//
//#define ENABLE_ETHERNET true
/////////////////////////////////////////////////////////////////////////////////////
//
// MAX_NUM_TCP_CLIENTS: If you on STM32 Ethernet (and only there) want more than
// 9 (*) TCP clients, change this number to for example 20 here **AND** in
// STM32lwiopts.h and follow the instructions in STM32lwiopts.h
//
// (*) It would be 10 if there would not be a bug in LwIP by STM32duino.
//
//#define MAX_NUM_TCP_CLIENTS 20
/////////////////////////////////////////////////////////////////////////////////////
//
@ -161,6 +171,10 @@ The configuration file for DCC-EX Command Station
// Use 132,64 for a SH1106-based I2C device with a 128x64 display.
// #define OLED_DRIVER 0x3c,128,32
//OR define PARALLEL_LCD_DRIVER COLS,ROWS,RS,RW,ENABLE,D4,D5,D6,D7
// using Arduino pin numbers for RS,RW,ENABLE,D4,D5,D6,D7
// #define PARALLEL_LCD_DRIVER 20, 4, 26, 27, 28, 22, 23, 24, 25
// Define scroll mode as 0, 1 or 2
// * #define SCROLLMODE 0 is scroll continuous (fill screen if poss),
// * #define SCROLLMODE 1 is by page (alternate between pages),

View File

@ -239,4 +239,25 @@
#endif
#endif
#if defined(ARDUINO_ARCH_STM32)
// The LwIP library for the STM32 wired ethernet has by default 10 TCP
// clients defined but because of a bug in the library #11 is not
// rejected but kicks out any old connection. By restricting our limit
// to 9 the #10 will be rejected by our code so that the number can
// never get to 11 which would kick an existing connection.
// If you want to change this value, do that in
// config.h AND in STM32lwipopts.h.
#ifndef MAX_NUM_TCP_CLIENTS
#define MAX_NUM_TCP_CLIENTS 9
#endif
#else
#if defined(ARDUINO_ARCH_ESP32)
// Espressif LWIP stack
#define MAX_NUM_TCP_CLIENTS 10
#else
// Wifi shields etc
#define MAX_NUM_TCP_CLIENTS 8
#endif
#endif
#endif //DEFINES_H

2813
docs/DoxyfileEXRAIL Normal file

File diff suppressed because it is too large Load Diff

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

888
docs/_static/css/dccex_theme.css vendored Normal file
View File

@ -0,0 +1,888 @@
@import url(https://fonts.googleapis.com/css?family=Audiowide);
@import url(https://fonts.googleapis.com/css?family=Roboto);
h1, .h1 {
font-family: Audiowide,Helvetica,Arial,sans-serif !important;
font-weight: 500 !important;
color: #00353d !important;
/* font-size: 200% !important; */
font-size: 180% !important;
text-shadow: 1px 1px #ffffff78;
}
html[data-theme='dark'] h1, .h1 {
color: #ffffff !important;
text-shadow: 1px 1px #00353d;
}
h2, .h2 {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
color: #00353d !important;
/* font-size: 190% !important; */
font-size: 160% !important;
text-shadow: 1px 1px #ffffff78;
}
html[data-theme='dark'] h2, .h2 {
color: #ffffff !important;
text-shadow: 1px 1px #00353d;
}
html[data-theme='dark'] h2 a,
html[data-theme='dark'] h2 a:visited {
color: #00a3b9ff !important;
}
h3, .h3 {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
color: #00353d !important;
/* font-size: 160% !important; */
font-size: 140% !important;
font-style: italic !important;
text-shadow: 1px 1px #ffffff78;
}
html[data-theme='dark'] h3, .h3 {
color: #ffffff !important;
text-shadow: 1px 1px #00353d;
}
html[data-theme='dark'] h3 a,
html[data-theme='dark'] h3 a:visited {
color: #00a3b9ff !important;
}
h4, .h4 {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
color: #00353d !important;
/* font-size: 130% !important; */
font-size: 120% !important;
text-shadow: 1px 1px #ffffff78;
}
html[data-theme='dark'] h4, .h4 {
color: #00a3b9ff !important;
text-shadow: 1px 1px #00353d;
}
html[data-theme='dark'] h4 a,
html[data-theme='dark'] h4 a:visited {
color: #00a3b9ff !important;
text-shadow: 1px 1px #00353d;
}
h5, .h5 {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
color: #00a3b9ff !important;
/* font-size: 110% !important; */
font-size: 100% !important;
}
h6, .h6 {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
color: #00a3b9ff !important;
font-size: 90% !important;
font-style: italic !important;
}
.clearer {
clear: both;
}
.wy-nav-side {
background: #031c20 !important;
/* background: #031214 !important; */
}
.caption-text {
color: #00a3b9ff !important;
}
.wy-nav-top {
background:#00a3b9ff !important;
font-size: 80% !important;
}
.wy-nav-top a {
font-family: Audiowide,Helvetica,Arial,sans-serif !important;
font-weight: 100 !important;
}
.wy-nav-content {
max-width: 1024px;
}
.wy-breadcrumbs {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
}
.wy-side-nav-search>a img.logo {
width: 100%;
}
.rst-content table.docutils th {
background-color: #F3F6F6;
}
.rst-content table.docutils td {
background-color: #F3F6F6;
}
.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {
background-color: #E0E0E0;
}
html[data-theme='dark'] .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {
background-color: #ffffff08 !important;
}
.caption-number {
font-size: small !important;
}
.caption-text {
font-size: small !important;
}
table.intro-table {
max-width: 600px;
}
.intro-table img {
width: 70%;
height: auto;
margin: 5% 15%;
}
html[data-theme='dark'] .btn-neutral {
color: #c1c1c1 !important;
}
#ex-rail-command-summary .wy-table-responsive {
overflow: visible;
}
/* product titles */
.ex-prefix {
font-weight: bold;
color: #00a3b9;
font-size: 110%;
}
.ex-suffix {
font-weight: bold;
color: #00353d;
font-size: 110%;
}
html[data-theme='dark'] .ex-suffix {
font-weight: bold;
color: #006979;
font-size: 110%;
}
/* main dcc-ex text only */
.dccex-prefix {
font-family: Audiowide,Helvetica,Arial,sans-serif;
font-weight: 600;
color: #00353d;
font-size: 110%;
}
html[data-theme='dark'] .dccex-prefix {
font-family: Audiowide,Helvetica,Arial,sans-serif;
font-weight: 600;
color: #006979;
font-size: 110%;
}
.dccex-suffix {
font-family: Audiowide,Helvetica,Arial,sans-serif;
font-weight: 600;
color: #00a3b9;
font-size: 110%;
}
/***************************/
.command-table thead th {
text-align: center;
}
.command-table tbody td {
white-space: normal;
margin: 10px;
padding: 8px 8px 8px 8px !important;
}
.command-table tbody tr:first-child td p code {
white-space: nowrap !important;
}
.command-table tbody tr td p code {
font-size: 110% !important;
}
.command-table tbody tr td p {
font-size: 90% !important;
}
.command-table tbody tr td ol li p {
font-size: 90% !important;
}
.command-table tbody tr td ol {
margin-bottom: 0px !important;
}
.command-table .category {
display: block;
text-align: center;
}
.command-table tr:nth-child(odd) {
background-color: #f1f1f1 !important;
}
.command-table tr:nth-child(even) {
background-color: #f8f8f8 !important;
}
html[data-theme='dark'] .command-table tr:nth-child(even) {
background-color: #ffffff08 !important;
}
.command-table td {
background-color: #ffffff00 !important;
}
/* html[data-theme='dark'] .rst-content table.docutils tr:nth-child(odd) {
background-color: #ffffff08 !important;
} */
html[data-theme='dark'] .rst-content table.docutils td, .wy-table-bordered-all td {
background-color: #fff40000 !important;
}
/* html[data-theme='dark'] .rst-content table.docutils .row-odd {
background-color: #36ff0000 !important;
} */
html[data-theme='dark'] .rst-content table.docutils th {
background-color: #36ff0000 !important;
color: white !important;
font-style: italic !important;;
font-weight: 700 !important;;
}
/* *************************************** */
html[data-theme='dark'] .sd-card {
background-color: #0000008a;
box-shadow: 0 0.5rem 1rem rgb(32 88 91 / 25%) !important;
}
/* *************************************** */
.dcclink a {
background-color: #00a3b9ff;
box-shadow: 0 2px 0 #00353dff;
color: white !important;
padding: 0.5em 0.5em;
position: relative;
text-decoration: none;
text-transform: none;
border-radius: 5px;
}
.dcclink-right a {
background-color: #00a3b9ff;
box-shadow: 0 2px 0 #00353dff;
color: white !important;
padding: 0.5em 0.5em;
position: relative;
text-decoration: none;
text-transform: none;
border-radius: 10px;
float:right;
margin: 0px 0px 0px 10px;
}
.dcclink a:visited {
color: whitesmoke !important;
}
.dcclink a:hover {
background-color: darkslategrey;
cursor: pointer;
}
.dcclink a:active {
box-shadow: none;
top: 5px;
}
html[data-theme='dark'] .rst-content .guilabel {
color: black;
}
.hr-dashed {
margin: -10px 0px -10px 0px;
border-top: 1px dashed #d2dfe3;
}
.hr-heavy {
margin: -10px 0px -10px 0px;
border-top: 5px solid #d2dfe3;
}
html[data-theme='dark'] .hr-dashed {
border-top: 1px dashed #114759;
}
/* *************************************** */
a.githublink, .githublink a {
background-color: #f7b656;
box-shadow: 0 2px 0 #00353dff;
color: white;
padding: 3px 5px 3px 5px;
position: relative;
font-size: 90% !important;
text-decoration: none;
text-transform: none;
border-radius: 5px;
}
.githublink-right a {
background-color: #f7b656;
box-shadow: 0 2px 0 #00353dff;
color: white;
padding: 3px 5px 3px 5px;
position: relative;
font-size: 90% !important;
text-decoration: none;
text-transform: none;
border-radius: 10px;
float:right;
margin: 0px 0px 0px 0px;
}
.githublink a:visited {
color: whitesmoke
}
.githublink a:hover {
background-color: rgb(172, 95, 7);
cursor: pointer;
}
.githublink a:active {
box-shadow: none;
top: 5px;
}
/* *************************************** */
svg {
max-width: 100%;
height: auto;
}
.responsive-image {
max-width: 100%;
height: auto;
}
/* *************************************** */
.warning-float-right {
float: right;
width: 40%;
}
.warning-float-right-narrow {
float: right;
width: 20%;
}
.warning-float-right-wide {
float: right;
width: 60%;
}
.note-float-right {
float: right;
width: 40%;
}
.note-float-right-narrow {
float: right;
width: 20%;
}
.code-block-float-right {
float: right;
width: 40%;
margin: 0px 0px 0px 24px;
}
.note {
background: #f7fcff !important;
clear: none !important;
}
html[data-theme='dark'] .note {
background: #ffffff24 !important;
}
.note p.admonition-title {
background: #cbe1ef !important;
}
html[data-theme='dark'] .note p.admonition-title {
background: #256a97 !important;
}
.tip {
background: #eef5f4 !important;
clear: none !important;
}
html[data-theme='dark'] .tip {
background: #ffffff24 !important;
clear: none !important;
}
.tip p.admonition-title {
background: #9cd7cb !important;
}
html[data-theme='dark'] .tip p.admonition-title {
background: #256a97 !important;
}
.admonition-todo {
background: #f9f0e0 !important;
clear: none !important;
}
html[data-theme='dark'] .admonition-todo {
background: #ffffff24 !important;
clear: none !important;
}
.admonition-todo p.admonition-title {
background: #f7d1b0 !important;
}
html[data-theme='dark'] .admonition-todo p.admonition-title {
background: #6d3403 !important;
}
/* *************************************** */
.menuselection {
font-style: italic;
font-weight: 700;
}
/* *************************************** */
.wy-table-responsive {
margin-bottom: 12px !important;
}
/* override table width restrictions */
.table-wrap-text p, .table-grid-homepage p, .table-list-homepage p {
white-space: normal !important;
font-size: 110% !important;
line-height: 140% !important;
}
.table-wrap-text tr:nth-child(odd), .table-grid-homepage tr:nth-child(odd), .table-list-homepage tr:nth-child(odd) {
background-color: white !important;
border-style: none !important;
border-width:0px !important;
}
html[data-theme='dark'] tr:nth-child(odd), .table-grid-homepage tr:nth-child(odd), .table-list-homepage tr:nth-child(odd) {
background-color: #ffffff08 !important;
}
.table-wrap-text tr:nth-child(even), .table-grid-homepage tr:nth-child(even), .table-list-homepage tr:nth-child(even) {
background-color: #ffffff00 !important;
border-style: none !important;
border-width:0px !important;
}
.table-wrap-text td {
background-color: white !important;
border-style: none !important;
border-width:0px !important;
}
html[data-theme='dark'] .table-wrap-text td {
background-color: ffffff08 !important;
}
.table-grid-homepage td, .table-list-homepage td {
font-size: 80% !important;
color: #666666 !important;
vertical-align:top !important;
background-color: #ffffff00 !important;
border-style: none !important;
border-width: 0px !important;
}
.table-wrap-text, .table-grid-homepage, .table-list-homepage {
margin-bottom: 24px;
max-width: 100% !important;
overflow: visible !important;
border-style: none !important;
border-width: 0px !important;
}
@media screen and (max-width: 900px) {
.table-grid-homepage {
display: none;
}
.table-list-homepage {
display: block;
}
}
@media not screen and (max-width: 900px) {
.table-grid-homepage {
display: block;
}
.table-list-homepage {
display: none;
}
}
.table-wrap-text th p, table-wrap-text-align-top th p {
margin-bottom: unset;
}
/* *************************************** */
.image-min-width-144 {
min-width: 144px;
height: auto !important;
}
.image-min-width-72 {
min-width: 72px;
height: auto !important;
}
.image-float-right img {
float:right;
}
.image-product-logo-float-right img {
float:right;
}
@media screen and (max-width: 1000px) {
.image-product-logo-float-right img {
display: none;
}
}
/* *************************************** */
/* Google search */
.gsc-input-box {
border: 0px !important;
}
.gsib_a input {
padding: 5px !important;
background-color: #141414 !important;
color:white !important;
}
.gsc-search-button .gsc-search-button-v2 {
width: 40px !important;
height: 21px !important;
padding: 4px 4px !important;
background-color: #00a3b9ff !important;
border-color: #00a3b9ff !important;
border-radius: 5px;
}
/* .gsc-search-button .gsc-search-button-v2 {
width: 0px !important;
padding: 7px 7px !important;
border-color: #009300 !important;
background-color: #009300 !important;
} */
/* *************************************** */
/* sidebar level 3 bullet points */
nav#on-this-page ul.simple li ul li p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* sidebar level 3 bullet points */
nav#on-this-page ul.simple li ul li {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* sidebar level 2 bullet points */
nav#on-this-page ul.simple li p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* sidebar level 2 bullet points */
nav#on-this-page ul.simple li {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
nav#on-this-page ul.simple {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
margin-bottom: 0px !important;
}
nav#on-this-page p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
margin-top: 0px !important;
margin-bottom: 6px !important;
}
nav#on-this-page {
margin-bottom: 10px !important;
}
/* in-this-section level 3 bullet points */
nav.in-this-section ul.simple li ul li p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* in-this-section level 3 bullet points */
nav.in-this-section ul.simple li ul li {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* in-this-section level 2 bullet points */
nav.in-this-section ul.simple li p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
/* in-this-section level 2 bullet points */
nav.in-this-section ul.simple li {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
line-height: 120% !important;
margin-bottom: 0px !important;
}
nav.in-this-section ul.simple {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
margin-bottom: 0px !important;
}
nav.in-this-section p {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-style: italic;
font-size: 90%;
margin-top: 0px !important;
margin-bottom: 6px !important;
margin-left: -30px;
}
nav.in-this-section {
margin-bottom: 20px !important;
margin-left: 30px;
}
/* sidebars */
.rst-content .sidebar {
padding: 12px 24px 12px 24px !important;
border-radius: 10px;
}
html[data-theme='dark'] .rst-content .sidebar {
background: #000000ff !important;
border:#000000ff !important;
}
.sidebar-title {
border-radius: 10px;
}
html[data-theme='dark'] .sidebar-title {
background: #002735 !important;
}
/* news */
section#dcc-ex-model-railroading aside p.sidebar-title {
font-size: 110% !important;
font-family: Audiowide,Helvetica,Arial,sans-serif !important;
font-weight: 500 !important;
color: #00a3b9ff;
text-shadow: 1px 1px 0 #00353dff;
margin: -24px -24px 12px !important;
}
/* news */
p.ablog-post-title {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 90% !important;
line-height: 130% !important;
margin-bottom: 0px !important;
font-weight: bold !important;
}
p.ablog-post-excerpt {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 90% !important;
line-height: 130% !important;
margin-bottom: 0px !important;
margin-top: 6px !important;
}
p.ablog-post-expand {
font-family: Roboto,Helvetica,Arial,sans-serif !important;
font-size: 80% !important;
line-height: 130% !important;
margin-bottom: 10px !important;
margin-top: 0px !important;
margin-left: 20px;
}
li.ablog-post {
list-style-type: none !important;
margin: 0px !important;
}
img.sd-card-img-top {
max-width: 30% !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
margin-top: 10px;
margin-bottom: -5px !important;
}
.sd-card-header {
margin-bottom: -10px !important;
margin-top: 10px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
}
.sd-card-header p {
line-height: 18px !important;
}
html[data-theme='dark'] .sd-card-header {
border-bottom: 1px solid rgb(255 253 253 / 13%);
}
.sd-card-body ul li p {
margin-bottom: 5px !important;
}
.sd-card-text {
margin: 0 0 12px !important;
}
/* code */
.rst-content code {
font-size: 100% !important;
}
.rst-content code.literal, .rst-content tt.literal {
color: #ba2121 !important;
font-size: 100% important;
}
html[data-theme='dark'] .rst-content code.literal, .rst-content tt.literal {
color: #ff6000 !important;
}
/* general purpose */
.dcc-ex-red {
color:red;
}
.dcc-ex-red-bold {
color:red;
font-weight: bold !important;
}
.dcc-ex-red-bold-italic {
color:red;
font-weight: bold !important;
font-style: italic !important;
}
.dcc-ex-code {
color:#ba2121;
font-weight: bold !important;
}
.dcc-ex-text-size-200pct {
font-size: 200% !important;
line-height: 110% !important;
}
.dcc-ex-text-size-80pct {
font-size: 80% !important;
}
.dcc-ex-text-size-60pct {
font-size: 80% !important;
}
.new-in-v5 {
font-family: Audiowide,Helvetica,Arial,sans-serif;
font-weight: bold;
font-style: italic;
color: #00a3b9;
font-size: 110%;
}
html[data-theme='dark'] .new-in-v5 {
font-weight: normal;
color: #ffffff;
text-shadow: 0px 0px 10px #00a3b9;
}
/* *************************************** */
@media not screen and (max-width: 900px) {
div.rst-footer-buttons {
position: fixed;
bottom:5px;
width:350px;
background: #c9c9c999;
padding: 10px;
border-radius: 10px;
border-color: white !important;
border: 4px solid;
transform: translateX(50%);
}
html[data-theme='dark'] div.rst-footer-buttons {
border-color: #141414 !important;
background: #c9c9c92e;
}
footer {
padding-bottom: 40px;
font-size: 80% !important;
}
}
@media screen and (max-width: 900px) {
div.rst-footer-buttons {
display:block;
font-size: 80% !important;
}
}
html[data-theme='dark'] .rst-content span.descname {
color: #dbdd7c !important;
}

View File

@ -0,0 +1,9 @@
/* Override for the sphinx-design extension classes */
.sd-card-header {
font-size: 110% !important;
font-family: Audiowide,Helvetica,Arial,sans-serif !important;
font-weight: 500 !important;
color: #00a3b9ff;
text-shadow: 1px 1px 0 #00353dff;
margin-bottom: .5rem !important;
}

BIN
docs/_static/images/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

BIN
docs/_static/images/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

94
docs/conf.py Normal file
View File

@ -0,0 +1,94 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import subprocess
# Doxygen
subprocess.call('doxygen DoxyfileEXRAIL', shell=True)
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'EXRAIL Language'
copyright = '2025 - Peter Cole'
author = 'Peter Cole'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx_sitemap',
'sphinxcontrib.spelling',
'sphinx_rtd_dark_mode',
'breathe'
]
autosectionlabel_prefix_document = True
# Don't make dark mode the user default
default_dark_mode = False
spelling_lang = 'en_UK'
tokenizer_lang = 'en_UK'
spelling_word_list_filename = ['spelling_wordlist.txt']
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
highlight_language = 'c++'
numfig = True
numfig_format = {'figure': 'Figure %s'}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_logo = "./_static/images/product-logo-ex-rail.png"
html_favicon = "./_static/images/favicon.ico"
html_theme_options = {
'style_nav_header_background': 'white',
'logo_only': True,
# Toc options
'includehidden': True,
'titles_only': False,
# 'titles_only': True,
'collapse_navigation': False,
# 'navigation_depth': 3,
'navigation_depth': 1,
'analytics_id': 'G-L5X0KNBF0W',
}
html_context = {
'display_github': True,
'github_user': 'DCC-EX',
'github_repo': 'CommandStation-EX',
'github_version': 'sphinx/docs/',
}
html_css_files = [
'css/dccex_theme.css',
'css/sphinx_design_overrides.css',
]
html_baseurl = 'https://dcc-ex.com/CommandStation-EX/'
# Sphinx sitemap
html_extra_path = [
'robots.txt',
]
# -- Breathe configuration -------------------------------------------------
breathe_projects = {
"EXRAIL Language": "_build/xml/"
}
breathe_default_project = "EXRAIL Language"
breathe_default_members = ()

15
docs/index.rst Normal file
View File

@ -0,0 +1,15 @@
EXRAIL Language documentation
=============================
Introduction
------------
EXRAIL - Extended Railroad Automation Instruction Language
This page is a reference to all EXRAIL commands available with EX-CommandStation.
Macros
------
.. doxygenfile:: EXRAIL2MacroReset.h
:project: EXRAIL Language

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

39
docs/requirements.txt Normal file
View File

@ -0,0 +1,39 @@
alabaster==1.0.0
attrs==25.1.0
babel==2.17.0
breathe==4.35.0
cattrs==24.1.2
certifi==2025.1.31
charset-normalizer==3.4.1
colorama==0.4.6
docutils==0.21.2
esbonio==0.16.5
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
Jinja2==3.1.5
lsprotocol==2023.0.1
MarkupSafe==3.0.2
packaging==24.2
platformdirs==4.3.6
pyenchant==3.2.2
pygls==1.3.1
Pygments==2.19.1
pyspellchecker==0.8.2
requests==2.32.3
snowballstemmer==2.2.0
Sphinx==8.1.3
sphinx-rtd-dark-mode==1.3.0
sphinx-rtd-theme==3.0.2
sphinx-sitemap==2.6.0
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
sphinxcontrib-spelling==8.0.1
tomli==2.2.1
typing_extensions==4.12.2
urllib3==2.3.0

3
docs/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Sitemap: https://dcc-ex.com/CommandStation-EX/sitemap.xml

View File

@ -96,7 +96,6 @@ lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
MDNS_Generic
lib_ignore = WiFi101
WiFi101_Generic
@ -115,7 +114,6 @@ framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
MDNS_Generic
SPI
lib_ignore = WiFi101
WiFi101_Generic
@ -261,7 +259,7 @@ monitor_echo = yes
; monitor_echo = yes
; upload_protocol = stlink
; Experimental - Ethernet work still in progress
; Experimental - Ethernet beta test
;
[env:Nucleo-F429ZI]
platform = ststm32 @ 19.0.0
@ -270,7 +268,6 @@ framework = arduino
lib_deps = ${env.lib_deps}
stm32duino/STM32Ethernet @ ^1.4.0
stm32duino/STM32duino LwIP @ ^2.1.3
MDNS_Generic
lib_ignore = WiFi101
WiFi101_Generic
WiFiEspAT
@ -281,20 +278,15 @@ monitor_speed = 115200
monitor_echo = yes
upload_protocol = stlink
; Experimental - Ethernet work still in progress
; Experimental - Ethernet beta test
;
[env:Nucleo-F439ZI]
platform = ststm32 @ 19.0.0
; board = nucleo_f439zi
; Temporarily treat it as an F429ZI (they are code compatible) until
; the PR to PlatformIO to update the F439ZI JSON file is available
; PMA - 28-Sep-2024
board = nucleo_f429zi
board = nucleo_f439zi
framework = arduino
lib_deps = ${env.lib_deps}
stm32duino/STM32Ethernet @ ^1.4.0
stm32duino/STM32duino LwIP @ ^2.1.3
MDNS_Generic
lib_ignore = WiFi101
WiFi101_Generic
WiFiEspAT

View File

@ -3,7 +3,16 @@
#include "StringFormatter.h"
#define VERSION "5.5.13"
#define VERSION "5.5.18"
// 5.5.18 - New STASH internals
// - EXRAIL IFSTASH/CLEAR_ANY_STASH
// - <JM CLEAR ANY id> to clear any stash with loco id
// - See Release_Notes/Stash.md
// 5.5.17 - Extensive new compile time checking in exrail scripts (duplicate sequences etc), no function change
// 5.5.16 - DOXYGEN comments in EXRAIL2MacroReset.h
// 5.5.15 - Support for F429ZI/F329ZI
// - Own mDNS support for (wired) Ethernet
// 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