1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-30 10:53:44 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Ash-4
79e1596dd1 EX8874 F413ZH F446ZE brake pin Track B 2025-03-15 00:10:29 -05:00
Ash-4
73e52f9671 Add EX8874 define for F413ZH/F446ZE version 5.5.17 2025-03-07 00:07:56 -06:00
Ash-4
a0f6af6a1b Update to 5.1.16 from DCC-EX/devel
Updates to 5.1.16
2025-03-06 23:07:27 -06:00
58 changed files with 1074 additions and 5055 deletions

View File

@@ -1,36 +0,0 @@
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,6 +15,3 @@ my*.h
compile_commands.json
newcode.txt.old
UserAddin.txt
_build
venv
.DS_Store

View File

@@ -312,9 +312,6 @@ void CommandDistributor::broadcastPower() {
state = '1';
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
@@ -323,7 +320,7 @@ void CommandDistributor::broadcastPower() {
const FSH * reason=F("");
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
@@ -334,6 +331,9 @@ void CommandDistributor::broadcastPower() {
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);

130
DCC.cpp
View File

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

2
DCC.h
View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
* © 2022 Paul M Antoine
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2024 Herb Morton
* © 2021-2025 Herb Morton
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
@@ -119,7 +119,6 @@ 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
@@ -169,10 +168,8 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd,
break;
if (hot == '\0')
return -1;
if (hot == '>') {
*remainingCmd = '\0'; // terminate the cmd string with 0 instead of '>'
if (hot == '>')
return parameterCount;
}
state = 2;
continue;
@@ -274,22 +271,17 @@ void DCCEXParser::parse(const FSH * cmd) {
// See documentation on DCC class for info on this section
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
// This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
// found is true first after the leading "<" has been passed which results
// in parseOne() getting c="C OMM AND>"
byte *cForLater = NULL;
// This function can get stings of the form "<C OMM AND>" or "C OMM AND"
// found is true first after the leading "<" has been passed
bool found = (com[0] != '<');
for (byte *c=com; c[0] != '\0'; c++) {
if (found) {
cForLater = c;
parseOne(stream, c, ringStream);
found=false;
}
if (c[0] == '<') {
if (cForLater) parseOne(stream, cForLater, ringStream);
if (c[0] == '<')
found = true;
}
}
if (cForLater) parseOne(stream, cForLater, ringStream);
}
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
@@ -639,7 +631,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;
}
@@ -773,7 +777,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'J' : // throttle info access
{
if (params<1) break; // <J>
if ((params<1) | (params>3)) break; // <J>
//if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0;
switch(p[0]) {
@@ -796,15 +800,21 @@ 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> Stash management
if (parseJM(stream, params, p))
return;
break;
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 "R"_hk: // <JR> returns rosters
StringFormatter::send(stream, F("<jR"));
@@ -1395,40 +1405,6 @@ 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)
{
@@ -1524,7 +1500,6 @@ void DCCEXParser::callback_Wloco(int16_t result)
void DCCEXParser::callback_Wconsist(int16_t result)
{
if (result==-4) DIAG(F("Long Consist %d not supported by decoder"),stashP[1]);
if (result==1) result=stashP[1]; // pick up original requested id from command
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));

View File

@@ -52,7 +52,6 @@ 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

View File

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

View File

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

View File

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

View File

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

@@ -159,7 +159,7 @@ void DCCWaveform::interrupt2() {
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<10 && remainingPreambles>1;
if (remainingPreambles==1)
promotePendingPacket();

View File

@@ -57,7 +57,6 @@
#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.
@@ -93,7 +92,8 @@ 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,17 @@ 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:
case OPCODE_IFLT:
case OPCODE_IFBITMAP_ALL:
case OPCODE_IFBITMAP_ANY:
case OPCODE_DRIVE: {
DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand);
IODevice::configureAnalogIn((VPIN)operand);
@@ -275,10 +279,6 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,true );
break;
case OPCODE_ONBITMAP:
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,true, true );
break;
case OPCODE_ONBUTTON:
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,false );
@@ -352,7 +352,13 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
}
SKIPOP; // include ENDROUTES opcode
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS);
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);
// Removed for 4.2.31 new RMFT2(0); // add the startup route
diag=saved_diag;
@@ -768,14 +774,6 @@ void RMFT2::loop2() {
case OPCODE_IFLT: // do next operand if sensor< value
skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1));
break;
case OPCODE_IFBITMAP_ALL: // do next operand if sensor & mask == mask
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) != getOperand(1);
break;
case OPCODE_IFBITMAP_ANY: // do next operand if sensor & mask !=0
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) == 0;
break;
case OPCODE_IFLOCO: // do if the loco is the active one
skipIf=loco!=(uint16_t)operand; // bad luck if someone enters negative loco numbers into EXRAIL
@@ -817,10 +815,6 @@ 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
@@ -948,9 +942,8 @@ void RMFT2::loop2() {
#ifndef DISABLE_PROG
case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true);
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
break;
case OPCODE_UNJOIN:
@@ -1074,32 +1067,30 @@ void RMFT2::loop2() {
break;
case OPCODE_STASH:
Stash::set(operand,invert? -loco : loco);
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = invert? -loco : loco;
break;
case OPCODE_CLEAR_STASH:
Stash::clear(operand);
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = 0;
break;
case OPCODE_CLEAR_ALL_STASH:
Stash::clearAll();
break;
case OPCODE_CLEAR_ANY_STASH:
if (loco) Stash::clearAny(loco);
if (compileFeatures & FEATURE_STASH)
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
break;
case OPCODE_PICKUP_STASH:
{
auto x=Stash::get(operand);
if (x>=0) {
loco=x;
invert=false;
}
else {
loco=-x;
invert=true;
}
if (compileFeatures & FEATURE_STASH) {
int16_t x=stashArray[operand];
if (x>=0) {
loco=x;
invert=false;
break;
}
loco=-x;
invert=true;
}
break;
@@ -1108,27 +1099,7 @@ void RMFT2::loop2() {
case OPCODE_SEQUENCE:
//if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
break;
case OPCODE_BITMAP_INC:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand)+1);
break;
case OPCODE_BITMAP_DEC:
{ int newval=IODevice::readAnalogue(operand)-1;
if (newval<0) newval=0;
IODevice::writeAnalogue(operand,newval);
}
break;
case OPCODE_BITMAP_AND:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) & getOperand(1));
break;
case OPCODE_BITMAP_OR:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) | getOperand(1));
break;
case OPCODE_BITMAP_XOR:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) ^ getOperand(1));
break;
case OPCODE_AUTOSTART: // Handled only during begin process
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte.
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
@@ -1148,7 +1119,6 @@ void RMFT2::loop2() {
case OPCODE_ONTIME:
case OPCODE_ONBUTTON:
case OPCODE_ONSENSOR:
case OPCODE_ONBITMAP:
#ifndef IO_NO_HAL
case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime
case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime

View File

@@ -77,13 +77,11 @@ 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_ONVP_SENSOR,
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
OPCODE_NEOPIXEL,
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
OPCODE_ESTOPALL,OPCODE_XPOM,
OPCODE_BITMAP_AND,OPCODE_BITMAP_OR,OPCODE_BITMAP_XOR,OPCODE_BITMAP_INC,OPCODE_BITMAP_DEC,OPCODE_ONBITMAP,
// OPcodes below this point are skip-nesting IF operations
// OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group
// see skipIfBlock()
IF_TYPE_OPCODES, // do not move this...
@@ -95,9 +93,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO,
OPCODE_IFTTPOSITION,
OPCODE_IFSTASH,
OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,
OPCODE_IFTTPOSITION
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@@ -141,7 +137,7 @@ enum SignalType {
static const byte FEATURE_LCC = 0x40;
static const byte FEATURE_ROSTER= 0x20;
static const byte FEATURE_ROUTESTATE= 0x10;
// spare = 0x08;
static const byte FEATURE_STASH = 0x08;
static const byte FEATURE_BLINK = 0x04;
static const byte FEATURE_SENSOR = 0x02;
static const byte FEATURE_BLOCK = 0x01;

View File

@@ -46,7 +46,6 @@
#undef CALL
#undef CLEAR_STASH
#undef CLEAR_ALL_STASH
#undef CLEAR_ANY_STASH
#undef CLOSE
#undef CONFIGURE_SERVO
#undef DCC_SIGNAL
@@ -89,13 +88,10 @@
#undef IFRANDOM
#undef IFRED
#undef IFRESERVE
#undef IFSTASH
#undef IFTHROWN
#undef IFTIMEOUT
#undef IFTTPOSITION
#undef IFRE
#undef IFBITMAP_ALL
#undef IFBITMAP_ANY
#undef INVERT_DIRECTION
#undef JMRI_SENSOR
#undef JOIN
@@ -135,7 +131,6 @@
#undef ONBUTTON
#undef ONSENSOR
#undef ONTHROW
#undef ONBITMAP
#undef ONCHANGE
#undef PARSE
#undef PAUSE
@@ -198,11 +193,6 @@
#undef VIRTUAL_TURNOUT
#undef WAITFOR
#ifndef IO_NO_HAL
#undef BITMAP_AND
#undef BITMAP_OR
#undef BITMAP_XOR
#undef BITMAP_INC
#undef BITMAP_DEC
#undef WAITFORTT
#endif
#undef WITHROTTLE
@@ -348,11 +338,6 @@
* @brief Clears all stashed loco values
*/
#define CLEAR_ALL_STASH
/**
* @def CLEAR_ANY_STASH
* @brief Clears loco value from all stash entries
*/
#define CLEAR_ANY_STASH
/**
* @def CLOSE(turnout_id)
* @brief Close turnout by id
@@ -617,13 +602,6 @@
* @param signal_id
*/
#define IFRED(signal_id)
/**
* @def IFSTASH(stash_id)
* @brief Checks if given stash entry has a non zero value
* @see IF
* @param stash_id
*/
#define IFSTASH(stash_id)
/**
* @def IFTHROWN(turnout_id)
* @brief Checks if given turnout is in THROWN state
@@ -659,22 +637,6 @@
* @param value
*/
#define IFRE(vpin,value)
/**
* @def IFBITMAP_ALL(vpin,mask)
* @briaf Checks if (vpin pseudo-analog value & mask) == mask.
* @see IF
* @param vpin
* @param mask Binary mask applied to vpin value
*/
#define IFBITMAP_ALL(vpin,mask)
/**
* @def IFBITMAP_ANY(vpin,mask)
* @briaf Checks if vpin pseudo-analog value & mask is non zero
* @see IF
* @param vpin
* @param mask Binary mask applied to vpin value
*/
#define IFBITMAP_ANY(vpin,mask)
/**
* @def INVERT_DIRECTION
* @brief Marks current task so that FWD and REV commands are inverted.
@@ -913,7 +875,7 @@
#define ONTHROW(turnout_id)
/**
* @def ONCHANGE(vpin)
* @brief Rotary encoder change starts task here (This is obscurely different from ONSENSOR which will be merged in a later release.)
* @brief Toratry encoder change starts task here (This is obscurely different from ONSENSOR which will be merged in a later release.)
* @param vpin
*/
#define ONCHANGE(vpin)
@@ -923,12 +885,6 @@
* @param vpin
*/
#define ONSENSOR(vpin)
/**
* @def ONBITMAP(vpin)
* @brief Start task here when bitmap sensor changes state (debounced)
* @param vpin
*/
#define ONBITMAP(vpin)
/**
* @def ONBUTTON(vpin)
* @brief Start task here when sensor changes HIGH to LOW.
@@ -1352,42 +1308,9 @@
* @param description... quoted text or HIDDEN
*/
#define VIRTUAL_TURNOUT(id,description...)
/**
* @def BITMAP_AND(vpin1,mask)
* @brief Performs a bitwise AND operation on the given vpin analog value and mask.
* @param vpin1
* @param mask Binary mask to be ANDed with vpin1 value
*/
#define BITMAP_AND(vpin1,mask)
/**
* @def BITMAP_INC(vpin)
* @brief Increments poesudo analog value by 1
* @param vpin
*/
#define BITMAP_INC(vpin)
/**
* @def BITMAP_DEC(vpin)
* @brief Decrements poesudo analog value by 1 (to zero)
* @param vpin
*/
#define BITMAP_DEC(vpin)
/**
* @def BITMAP_OR(vpin1,mask)
* @brief Performs a bitwise OR operation on the given vpin analog value and mask.
* @param vpin1
* @param mask Binary mask to be ORed with vpin1 value
*/
#define BITMAP_OR(vpin1,mask)
/**
* @def BITMAP_XOR(vpin1,mask)
* @brief Performs a bitwise XOR operation on the given vpin analog value and mask.
* @param vpin1
* @param mask Binary mask to be XORed with vpin1 value
*/
#define BITMAP_XOR(vpin1,mask)
/**
* @def WAITFOR(vpin)
* @brief Waits for completion of servo movement
* @brief WAits for completion of servo movement
* @param vpin
*/
#define WAITFOR(pin)

View File

@@ -181,7 +181,36 @@ 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
@@ -194,7 +223,6 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
break;
}
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
@@ -240,6 +268,15 @@ 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;
}

View File

@@ -1,165 +0,0 @@
/*
* © 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,8 +86,72 @@
#define ALIAS(name,value...) const int name= #value[0] ? value+0: -__COUNTER__ ;
#include "myAutomation.h"
// Perform compile time asserts to check the script for errors
#include "EXRAILAsserts.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"
// Pass 1g Implants STEALTH_GLOBAL in correct place
#include "EXRAIL2MacroReset.h"
@@ -154,14 +218,20 @@ 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
#define ONBUTTON(vpin) | FEATURE_SENSOR
#undef ONSENSOR
#define ONSENSOR(vpin) | FEATURE_SENSOR
#undef ONBITMAP
#define ONBITMAP(vpin) | FEATURE_SENSOR
#undef ONBLOCKENTER
#define ONBLOCKENTER(blockid) | FEATURE_BLOCK
#undef ONBLOCKEXIT
@@ -429,7 +499,6 @@ 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
@@ -476,15 +545,12 @@ 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
#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position),
#endif
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
#define IFBITMAP_ALL(vpin,mask) OPCODE_IFBITMAP_ALL,V(vpin),OPCODE_PAD,V(mask),
#define IFBITMAP_ANY(vpin,mask) OPCODE_IFBITMAP_ANY,V(vpin),OPCODE_PAD,V(mask),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
#define JMRI_SENSOR(vpin,count...)
#define JOIN OPCODE_JOIN,0,0,
@@ -537,12 +603,13 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),
#define ONBITMAP(sensor_id) OPCODE_ONBITMAP,V(sensor_id),
#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,V(sensor_id),
#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),
@@ -600,11 +667,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
#define VIRTUAL_SIGNAL(id)
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
#define BITMAP_AND(vpin,mask) OPCODE_BITMAP_AND,V(vpin),OPCODE_PAD,V(mask),
#define BITMAP_INC(vpin) OPCODE_BITMAP_INC,V(vpin),
#define BITMAP_DEC(vpin) OPCODE_BITMAP_DEC,V(vpin),
#define BITMAP_OR(vpin,mask) OPCODE_BITMAP_OR,V(vpin),OPCODE_PAD,V(mask),
#define BITMAP_XOR(vpin,mask) OPCODE_BITMAP_XOR,V(vpin),OPCODE_PAD,V(mask),
#define WITHROTTLE(msg) PRINT(msg)
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
#ifndef IO_NO_HAL

View File

@@ -59,7 +59,7 @@ void EXRAILSensor::checkAll() {
bool EXRAILSensor::check() {
// check for debounced change in this sensor
inputState = useAnalog?IODevice::readAnalogue(pin):RMFT2::readSensor(pin);
inputState = RMFT2::readSensor(pin);
// Check if changed since last time, and process changes.
if (inputState == active) {// no change
@@ -83,18 +83,18 @@ bool EXRAILSensor::check() {
return false;
}
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog) {
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange) {
// Add to the start of the list
//DIAG(F("ONthing vpin=%d at %d"), _pin, _progCounter);
nextSensor = firstSensor;
firstSensor = this;
pin=_pin;
progCounter=_progCounter;
onChange=_onChange;
useAnalog=_useAnalog;
IODevice::configureInput(pin, true);
active = useAnalog?IODevice::readAnalogue(pin): IODevice::read(pin);
active = IODevice::read(pin);
inputState = active;
latchDelay = minReadCount;
}

View File

@@ -29,8 +29,7 @@ class EXRAILSensor {
public:
static void checkAll();
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog=false);
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange);
bool check();
private:
@@ -43,10 +42,9 @@ class EXRAILSensor {
EXRAILSensor* nextSensor;
VPIN pin;
int progCounter;
uint16_t active;
uint16_t inputState;
bool active;
bool inputState;
bool onChange;
bool useAnalog;
byte latchDelay;
};
#endif

View File

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

View File

@@ -46,8 +46,6 @@
// Helper function for listing device types
static const FSH * guessI2CDeviceType(uint8_t address) {
if (address >= 0x10 && address <= 0x17)
return F("EX-SensorCAM");
if (address == 0x1A)
// 0x09-0x18 selectable, but for now handle the default
return F("Piicodev 865/915MHz Transceiver");

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
*
@@ -20,31 +22,19 @@
This is the list of HAL drivers automatically included by IODevice.h
It has been moved here to be easier to maintain than editing IODevice.h
*/
#include "IO_AnalogueInputs.h"
#include "IO_DFPlayer.h"
#include "IO_DS1307.h"
#include "IO_duinoNodes.h"
#include "IO_EncoderThrottle.h"
#include "IO_EXFastclock.h"
#include "IO_EXIOExpander.h"
#include "IO_EXSensorCAM.h"
#include "IO_HALDisplay.h"
#include "IO_HCSR04.h"
#include "IO_I2CDFPlayer.h"
#include "IO_I2CRailcom.h"
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_NeoPixel.h"
#include "IO_PCA9555.h"
#include "IO_PCA9685pwm.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_RotaryEncoder.h"
#include "IO_Servo.h"
#include "IO_TCA8418.h"
#include "IO_TM1638.h"
#include "IO_TouchKeypad.h"
#include "IO_PCA9555.h"
#include "IO_duinoNodes.h"
#include "IO_EXIOExpander.h"
#include "IO_trainbrains.h"
#include "IO_Bitmap.h"
#include "IO_VL53L0X.h"
#include "IO_EncoderThrottle.h"
#include "IO_TCA8418.h"
#include "IO_NeoPixel.h"
#include "IO_TM1638.h"
#include "IO_EXSensorCAM.h"
#include "IO_DS1307.h"
#include "IO_I2CRailcom.h"
#include "IO_HALDisplay.h"

View File

@@ -1,89 +0,0 @@
/*
* © 2025, Chris Harlow. All rights reserved.
*
* This file is part of DCC-EX API
*
* 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 IO_Bitmap_h
#define IO_Bitmap_h
#include <Arduino.h>
#include "defines.h"
#include "IODevice.h"
/*
Bitmap provides a set of virtual pins with no hardware.
Bitmap pins are able to be output and input and may be set and tested
as digital or analogue values.
When writing a digital value, the analogue value is set to 0 or 1.
When reading a digital value, the return is LOW for value 0 or HIGH for any other value
or analogue.
Bitmap pins may be used for any purpose, this is easier to manage than LATCH in EXRAIL
as they can be explicitely set and tested without interfering with underlying hardware.
Bitmap pins may be set, reset and tested in the same way as any other pin.
They are not persistent across reboots, but are retained in the current session.
Bitmap pins may also be monitored by JMRI_SENSOR() and <S> as for any other pin.
*/
class Bitmap : public IODevice {
public:
static void create(VPIN firstVpin, int nPins) {
if (IODevice::checkNoOverlap(firstVpin,nPins))
new Bitmap( firstVpin, nPins);
}
Bitmap(VPIN firstVpin, int nPins) : IODevice(firstVpin, nPins) {
_pinValues=(int16_t *) calloc(nPins,sizeof(int16_t));
// Connect to HAL so my _write, _read and _loop will be called as required.
IODevice::addDevice(this);
}
// Called by HAL to start handling this device
void _begin() override {
_deviceState = DEVSTATE_NORMAL;
_display();
}
int _read(VPIN vpin) override {
int pin=vpin - _firstVpin;
return _pinValues[pin]?1:0;
}
void _write(VPIN vpin, int value) override {
int pin = vpin - _firstVpin;
_pinValues[pin]=value!=0; // this is digital write
}
int _readAnalogue(VPIN vpin) override {
int pin=vpin - _firstVpin;
return _pinValues[pin]; // this is analog read
}
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
int pin=vpin - _firstVpin;
_pinValues[pin]=value; // this is analog write
}
void _display() override {
DIAG(F("Bitmap Configured on Vpins:%u-%u"),
(int)_firstVpin,
(int)_firstVpin+_nPins-1);
}
private:
int16_t* _pinValues;
};
#endif

View File

@@ -85,7 +85,6 @@ class EXSensorCAM : public IODevice {
void _begin() {
uint8_t status;
// Initialise EX-SensorCAM device
I2CManager.setClock(100000); // Set speed for I2C operations
I2CManager.begin();
if (!I2CManager.exists(_I2CAddress)) {
DIAG(F("EX-SensorCAM I2C:%s device not found"), _I2CAddress.toString());

View File

@@ -38,7 +38,8 @@
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
* Parameters:
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
* vPins : Total number of virtual pins allocated (to prevent overlaps)
* vPins : Total number of virtual pins allocated (2 vPins are supported, one for each UART)
* 1st vPin for UART 0, 2nd for UART 1
* I2C Address : I2C address of the serial controller, in 0x format
*/
@@ -46,32 +47,43 @@
#include "IO_I2CRailcom.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "Railcom.h"
// Debug and diagnostic defines, enable too many will result in slowing the driver
#define DIAG_I2CRailcom
I2CRailcom::I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress){
I2CRailcom::I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress){
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = i2cAddress;
_channelMonitors[0]=new Railcom(firstVpin);
if (nPins>1) _channelMonitors[1]=new Railcom(firstVpin+1);
addDevice(this);
}
void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
if (nPins>2) nPins=2;
if (checkNoOverlap(firstVpin, nPins, i2cAddress))
new I2CRailcom(firstVpin,nPins,i2cAddress);
new I2CRailcom(firstVpin, nPins, i2cAddress);
}
void I2CRailcom::_begin() {
I2CManager.setClock(1000000); // TODO do we need this?
I2CManager.begin();
auto exists=I2CManager.exists(_I2CAddress);
DIAG(F("I2CRailcom: %s RailcomCollector %S detected"),
DIAG(F("I2CRailcom: %s UART%S detected"),
_I2CAddress.toString(), exists?F(""):F(" NOT"));
if (!exists) return;
_deviceState=DEVSTATE_NORMAL;
_UART_CH=0;
Init_SC16IS752(); // Initialize UART0
if (_nPins>1) {
_UART_CH=1;
Init_SC16IS752(); // Initialize UART1
}
if (_deviceState==DEVSTATE_INITIALISING) _deviceState=DEVSTATE_NORMAL;
_display();
}
@@ -79,43 +91,213 @@ void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
void I2CRailcom::_loop(unsigned long currentMicros) {
// Read responses from device
if (_deviceState!=DEVSTATE_NORMAL) return;
// have we read this cutout already?
// basically we only poll once per packet when railcom cutout is working
auto cut=DCCWaveform::getRailcomCutoutCounter();
if (cutoutCounter==cut) return;
cutoutCounter=cut;
Railcom::loop(); // in case a csv read has timed out
// Obtain data length from the collector
byte inbuf[1];
byte queryLength[]={'?'};
auto state=I2CManager.read(_I2CAddress, inbuf, 1,queryLength,sizeof(queryLength));
if (state) {
DIAG(F("RC ? state=%d"),state);
return;
}
auto length=inbuf[0];
if (length==0) return; // nothing to report
// Build a buffer and import the data from the collector
byte inbuf2[length];
byte queryData[]={'>'};
state=I2CManager.read(_I2CAddress, inbuf2, length,queryData,sizeof(queryData));
if (state) {
DIAG(F("RC > %d state=%d"),length,state);
return;
}
// return if in cutout or cutout very soon.
if (!DCCWaveform::isRailcomSampleWindow()) return;
// process incoming data buffer
Railcom::process(_firstVpin,inbuf2,length);
// IF we have 2 channels, flip channels each loop
if (_nPins>1) _UART_CH=_UART_CH?0:1;
// have we read this cutout already?
auto cut=DCCWaveform::getRailcomCutoutCounter();
if (cutoutCounter[_UART_CH]==cut) return;
cutoutCounter[_UART_CH]=cut;
// Read incoming raw Railcom data, and process accordingly
auto inlength = UART_ReadRegister(REG_RXLV);
if (inlength> sizeof(_inbuf)) inlength=sizeof(_inbuf);
_inbuf[0]=0;
if (inlength>0) {
// Read data buffer from UART
_outbuf[0]=(byte)(REG_RHR << 3 | _UART_CH << 1);
I2CManager.read(_I2CAddress, _inbuf, inlength, _outbuf, 1);
}
// HK: Reset FIFO at end of read cycle
UART_WriteRegister(REG_FCR, 0x07,false);
// Ask Railcom to interpret the raw data
_channelMonitors[_UART_CH]->process(_inbuf,inlength);
}
void I2CRailcom::_display() {
DIAG(F("I2CRailcom: %s blocks %d-%d %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,
DIAG(F("I2CRailcom: Configured on Vpins:%u-%u %S"), _firstVpin, _firstVpin+_nPins-1,
(_deviceState!=DEVSTATE_NORMAL) ? F("OFFLINE") : F(""));
}
// SC16IS752 functions
// Initialise SC16IS752 only for this channel
// First a software reset
// Enable FIFO and clear TX & RX FIFO
// Need to set the following registers
// IOCONTROL set bit 1 and 2 to 0 indicating that they are GPIO
// IODIR set all bit to 1 indicating al are output
// IOSTATE set only bit 0 to 1 for UART 0, or only bit 1 for UART 1 //
// LCR bit 7=0 divisor latch (clock division registers DLH & DLL, they store 16 bit divisor),
// WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE
// MCR bit 7=0 clock divisor devide-by-1 clock input
// DLH most significant part of divisor
// DLL least significant part of divisor
//
// BAUD_RATE, WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE have been defined and initialized
//
// Communication parameters 8 bit, No parity, 1 stopbit
static const uint8_t WORD_LEN = 0x03; // Value LCR bit 0,1
static const uint8_t STOP_BIT = 0x00; // Value LCR bit 2
static const uint8_t PARITY_ENA = 0x00; // Value LCR bit 3
static const uint8_t PARITY_TYPE = 0x00; // Value LCR bit 4
static const uint32_t BAUD_RATE = 250000;
static const uint8_t PRESCALER = 0x01; // Value MCR bit 7
static const unsigned long SC16IS752_XTAL_FREQ_RAILCOM = 16000000; // Baud rate for Railcom signal
static const uint16_t _divisor = (SC16IS752_XTAL_FREQ_RAILCOM / PRESCALER) / (BAUD_RATE * 16);
void I2CRailcom::Init_SC16IS752(){
if (_UART_CH==0) { // HK: Currently fixed on ch 0
// only reset on channel 0}
UART_WriteRegister(REG_IOCONTROL, 0x08,false); // UART Software reset
//_deviceState=DEVSTATE_INITIALISING; // ignores error during reset which seems normal. // HK: this line is moved to below
auto iocontrol_readback = UART_ReadRegister(REG_IOCONTROL);
if (iocontrol_readback == 0x00){
_deviceState=DEVSTATE_INITIALISING;
DIAG(F("I2CRailcom: %s SRESET readback: 0x%x"),_I2CAddress.toString(), iocontrol_readback);
} else {
DIAG(F("I2CRailcom: %s SRESET: 0x%x"),_I2CAddress.toString(), iocontrol_readback);
}
}
// HK:
// You write 0x08 to the IOCONTROL register, setting bit 3 (SRESET), as per datasheet 8.18:
// "Software Reset. A write to this bit will reset the device. Once the
// device is reset this bit is automatically set to logic 0"
// So you can not readback the val you have written as this has changed.
// I've added an extra UART_ReadRegister(REG_IOCONTROL) and check if the return value is 0x00
// then set _deviceState=DEVSTATE_INITIALISING;
// HK: only do clear FIFO at end of Init_SC16IS752
//UART_WriteRegister(REG_FCR, 0x07,false); // Reset FIFO, clear RX & TX FIFO (write only)
UART_WriteRegister(REG_MCR, 0x00); // Set MCR to all 0, includes Clock divisor
//UART_WriteRegister(REG_LCR, 0x80); // Divisor latch enabled
UART_WriteRegister(REG_LCR, 0x80 | WORD_LEN | STOP_BIT | PARITY_ENA | PARITY_TYPE); // Divisor latch enabled and comm parameters set
UART_WriteRegister(REG_DLL, (uint8_t)_divisor); // Write DLL
UART_WriteRegister(REG_DLH, (uint8_t)(_divisor >> 8)); // Write DLH
auto lcr_readback = UART_ReadRegister(REG_LCR);
lcr_readback = lcr_readback & 0x7F;
UART_WriteRegister(REG_LCR, lcr_readback); // Divisor latch disabled
//UART_WriteRegister(REG_LCR, WORD_LEN | STOP_BIT | PARITY_ENA | PARITY_TYPE); // Divisor latch disabled
UART_WriteRegister(REG_FCR, 0x07,false); // Reset FIFO, clear RX & TX FIFO (write only)
#ifdef DIAG_I2CRailcom
// HK: Test to see if internal loopback works and if REG_RXLV increment to at least 0x01
// Set REG_MCR bit 4 to 1, Enable Loopback
UART_WriteRegister(REG_MCR, 0x10);
UART_WriteRegister(REG_THR, 0x88, false); // Send 0x88
auto inlen = UART_ReadRegister(REG_RXLV);
if (inlen == 0){
DIAG(F("I2CRailcom: Loopback test: %s/%d failed"),_I2CAddress.toString(), _UART_CH);
} else {
DIAG(F("Railcom: Loopback test: %s/%d RX Fifo lvl: 0x%x"),_I2CAddress.toString(), _UART_CH, inlen);
_outbuf[0]=(byte)(REG_RHR << 3 | _UART_CH << 1);
I2CManager.read(_I2CAddress, _inbuf, inlen, _outbuf, 1);
#ifdef DIAG_I2CRailcom_data
DIAG(F("Railcom: Loopback test: %s/%d RX FIFO Data"), _I2CAddress.toString(), _UART_CH);
for (int i = 0; i < inlen; i++){
DIAG(F("Railcom: Loopback data [0x%x]: 0x%x"), i, _inbuf[i]);
//DIAG(F("[0x%x]: 0x%x"), i, _inbuf[i]);
}
#endif
}
UART_WriteRegister(REG_MCR, 0x00); // Set REG_MCR back to 0x00
#endif
#ifdef DIAG_I2CRailcom
// Sent some data to check if UART baudrate is set correctly, check with logic analyzer on TX pin
UART_WriteRegister(REG_THR, 9, false);
DIAG(F("I2CRailcom: UART %s/%d Test TX = 0x09"),_I2CAddress.toString(), _UART_CH);
#endif
if (_deviceState==DEVSTATE_INITIALISING) {
DIAG(F("I2CRailcom: UART %d init complete"),_UART_CH);
}
// HK: final FIFO reset
UART_WriteRegister(REG_FCR, 0x07,false); // Reset FIFO, clear RX & TX FIFO (write only)
}
void I2CRailcom::UART_WriteRegister(uint8_t reg, uint8_t val, bool readback){
_outbuf[0] = (byte)( reg << 3 | _UART_CH << 1);
_outbuf[1]=val;
auto status=I2CManager.write(_I2CAddress, _outbuf, (uint8_t)2);
if(status!=I2C_STATUS_OK) {
DIAG(F("I2CRailcom: %s/%d write reg=0x%x,data=0x%x,I2Cstate=%d"),
_I2CAddress.toString(), _UART_CH, reg, val, status);
_deviceState=DEVSTATE_FAILED;
}
if (readback) { // Read it back to cross check
auto readback=UART_ReadRegister(reg);
if (readback!=val) {
DIAG(F("I2CRailcom readback: %s/%d reg:0x%x write=0x%x read=0x%x"),_I2CAddress.toString(),_UART_CH,reg,val,readback);
}
}
}
uint8_t I2CRailcom::UART_ReadRegister(uint8_t reg){
_outbuf[0] = (byte)(reg << 3 | _UART_CH << 1); // _outbuffer[0] has now UART_REG and UART_CH
_inbuf[0]=0;
auto status=I2CManager.read(_I2CAddress, _inbuf, 1, _outbuf, 1);
if (status!=I2C_STATUS_OK) {
DIAG(F("I2CRailcom read: %s/%d read reg=0x%x,I2Cstate=%d"),
_I2CAddress.toString(), _UART_CH, reg, status);
_deviceState=DEVSTATE_FAILED;
}
return _inbuf[0];
}
// SC16IS752 General register set (from the datasheet)
enum : uint8_t {
REG_RHR = 0x00, // FIFO Read
REG_THR = 0x00, // FIFO Write
REG_IER = 0x01, // Interrupt Enable Register R/W
REG_FCR = 0x02, // FIFO Control Register Write
REG_IIR = 0x02, // Interrupt Identification Register Read
REG_LCR = 0x03, // Line Control Register R/W
REG_MCR = 0x04, // Modem Control Register R/W
REG_LSR = 0x05, // Line Status Register Read
REG_MSR = 0x06, // Modem Status Register Read
REG_SPR = 0x07, // Scratchpad Register R/W
REG_TCR = 0x06, // Transmission Control Register R/W
REG_TLR = 0x07, // Trigger Level Register R/W
REG_TXLV = 0x08, // Transmitter FIFO Level register Read
REG_RXLV = 0x09, // Receiver FIFO Level register Read
REG_IODIR = 0x0A, // Programmable I/O pins Direction register R/W
REG_IOSTATE = 0x0B, // Programmable I/O pins State register R/W
REG_IOINTENA = 0x0C, // I/O Interrupt Enable register R/W
REG_IOCONTROL = 0x0E, // I/O Control register R/W
REG_EFCR = 0x0F, // Extra Features Control Register R/W
};
// SC16IS752 Special register set
enum : uint8_t{
REG_DLL = 0x00, // Division registers R/W
REG_DLH = 0x01, // Division registers R/W
};
// SC16IS752 Enhanced regiter set
enum : uint8_t{
REG_EFR = 0X02, // Enhanced Features Register R/W
REG_XON1 = 0x04, // R/W
REG_XON2 = 0x05, // R/W
REG_XOFF1 = 0x06, // R/W
REG_XOFF2 = 0x07, // R/W
};

View File

@@ -1,4 +1,4 @@
/*
/*
* © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.
* © 2023, Neil McKechnie. All rights reserved.
*
@@ -19,27 +19,47 @@
*/
/*
* This polls the RailcomCollecter device once per dcc packet
* and obtains an abbreviated list of block occupancy changes which
* are fortunately very rare compared with Railcom raw data.
*
*
* Dec 2023, Added NXP SC16IS752 I2C Dual UART
* The SC16IS752 has 64 bytes TX & RX FIFO buffer
* First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be
* needed as the RX Fifo holds the reply
*
* Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work but the first transmit
* corrupt data. This need more analysis and experimenatation.
* Will push this driver to the dev branch with the uart fixed to 0
* Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)
*
* myHall.cpp configuration syntax:
*
* I2CRailcom::create(1st vPin, vPins, I2C address);
*
* myAutomation configuration
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
* Parameters:
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
* vPins : Total number of virtual pins allocated
* I2C Address : I2C address of the Railcom Collector, in 0x format
* vPins : Total number of virtual pins allocated (2 vPins are supported, one for each UART)
* 1st vPin for UART 0, 2nd for UART 1
* I2C Address : I2C address of the serial controller, in 0x format
*/
#ifndef IO_I2CRailcom_h
#define IO_I2CRailcom_h
#include "Arduino.h"
#include "IODevice.h"
#include "Railcom.h"
// Debug and diagnostic defines, enable too many will result in slowing the driver
#define DIAG_I2CRailcom
class I2CRailcom : public IODevice {
private:
byte cutoutCounter;
public:
// SC16IS752 defines
uint8_t _UART_CH=0x00; // channel 0 or 1 flips each loop if npins>1
byte _inbuf[12];
byte _outbuf[2];
byte cutoutCounter[2];
Railcom * _channelMonitors[2];
public:
// Constructor
I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress);
@@ -52,7 +72,74 @@ private:
private:
// SC16IS752 functions
// Initialise SC16IS752 only for this channel
// First a software reset
// Enable FIFO and clear TX & RX FIFO
// Need to set the following registers
// IOCONTROL set bit 1 and 2 to 0 indicating that they are GPIO
// IODIR set all bit to 1 indicating al are output
// IOSTATE set only bit 0 to 1 for UART 0, or only bit 1 for UART 1 //
// LCR bit 7=0 divisor latch (clock division registers DLH & DLL, they store 16 bit divisor),
// WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE
// MCR bit 7=0 clock divisor devide-by-1 clock input
// DLH most significant part of divisor
// DLL least significant part of divisor
//
// BAUD_RATE, WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE have been defined and initialized
//
// Communication parameters 8 bit, No parity, 1 stopbit
static const uint8_t WORD_LEN = 0x03; // Value LCR bit 0,1
static const uint8_t STOP_BIT = 0x00; // Value LCR bit 2
static const uint8_t PARITY_ENA = 0x00; // Value LCR bit 3
static const uint8_t PARITY_TYPE = 0x00; // Value LCR bit 4
static const uint32_t BAUD_RATE = 250000;
static const uint8_t PRESCALER = 0x01; // Value MCR bit 7
static const unsigned long SC16IS752_XTAL_FREQ_RAILCOM = 16000000; // Baud rate for Railcom signal
static const uint16_t _divisor = (SC16IS752_XTAL_FREQ_RAILCOM / PRESCALER) / (BAUD_RATE * 16);
void Init_SC16IS752();
void UART_WriteRegister(uint8_t reg, uint8_t val, bool readback=true);
uint8_t UART_ReadRegister(uint8_t reg);
// SC16IS752 General register set (from the datasheet)
enum : uint8_t {
REG_RHR = 0x00, // FIFO Read
REG_THR = 0x00, // FIFO Write
REG_IER = 0x01, // Interrupt Enable Register R/W
REG_FCR = 0x02, // FIFO Control Register Write
REG_IIR = 0x02, // Interrupt Identification Register Read
REG_LCR = 0x03, // Line Control Register R/W
REG_MCR = 0x04, // Modem Control Register R/W
REG_LSR = 0x05, // Line Status Register Read
REG_MSR = 0x06, // Modem Status Register Read
REG_SPR = 0x07, // Scratchpad Register R/W
REG_TCR = 0x06, // Transmission Control Register R/W
REG_TLR = 0x07, // Trigger Level Register R/W
REG_TXLV = 0x08, // Transmitter FIFO Level register Read
REG_RXLV = 0x09, // Receiver FIFO Level register Read
REG_IODIR = 0x0A, // Programmable I/O pins Direction register R/W
REG_IOSTATE = 0x0B, // Programmable I/O pins State register R/W
REG_IOINTENA = 0x0C, // I/O Interrupt Enable register R/W
REG_IOCONTROL = 0x0E, // I/O Control register R/W
REG_EFCR = 0x0F, // Extra Features Control Register R/W
};
// SC16IS752 Special register set
enum : uint8_t{
REG_DLL = 0x00, // Division registers R/W
REG_DLH = 0x01, // Division registers R/W
};
// SC16IS752 Enhanced regiter set
enum : uint8_t{
REG_EFR = 0X02, // Enhanced Features Register R/W
REG_XON1 = 0x04, // R/W
REG_XON2 = 0x05, // R/W
REG_XOFF1 = 0x06, // R/W
REG_XOFF2 = 0x07, // R/W
};
};
#endif // IO_I2CRailcom_h

View File

@@ -1,5 +1,4 @@
/*
* © 2025 Herb Morton
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
@@ -44,21 +43,15 @@
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
}
private:
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
if (initPortState>=0) {
_portMode = 255; // set all pins to output mode
_portInUse = 255; // 8 ports in use
_portOutputState = initPortState; // initialize pins low-high 0-255
I2CManager.write(_I2CAddress, 1, initPortState);
}
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.

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);

View File

@@ -82,6 +82,10 @@
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
new MotorDriver( 5, 13, UNUSED_PIN, 6, A1, 1.27, 5000, A5)
#elif defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F413ZH)
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
new MotorDriver( 11, 13, UNUSED_PIN, 6, A1, 1.27, 5000, A5)
#else
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
#define EX8874_SHIELD F("EX8874"), \

View File

@@ -1,5 +1,6 @@
/*
* © 2025 Chris Harlow
* SEE ADDITIONAL COPYRIGHT ATTRIBUTION BELOW
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
@@ -18,65 +19,258 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/** Sections of this code (the decode table constants)
* are taken from openmrn
* https://github.com/bakerstu/openmrn/blob/master/src/dcc/RailCom.cxx
* under the following copyright.
*
* Copyright (c) 2014, Balazs Racz
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
**/
#include "Railcom.h"
#include "defines.h"
#include "FSH.h"
#include "DCC.h"
#include "DIAG.h"
#include "DCCWaveform.h"
/** Table for 8-to-6 decoding of railcom data. This table can be indexed by the
* 8-bit value read from the railcom channel, and the return value will be
* either a 6-bit number, or one of the defined Railcom constrantrs. If the
* value is invalid, the INV constant is returned. */
// These values appear in the railcom_decode table to mean special symbols.
static constexpr uint8_t
// highest valid 6-bit value
MAX_VALID = 0x3F,
/// invalid value (not conforming to the 4bit weighting requirement)
INV = 0xff,
/// Railcom ACK; the decoder received the message ok. NOTE: There are
/// two codepoints that map to this.
ACK = 0xfe,
/// The decoder rejected the packet.
NACK = 0xfd,
/// The decoder is busy; send the packet again. This is typically
/// returned when a POM CV write is still pending; the caller must
/// re-try sending the packet later.
RCBUSY = 0xfc,
/// Reserved for future expansion.
RESVD1 = 0xfb,
/// Reserved for future expansion.
RESVD2 = 0xfa;
const uint8_t HIGHFLASH decode[256] =
// 0|8 1|9 2|a 3|b 4|c 5|d 6|e 7|f
{ INV, INV, INV, INV, INV, INV, INV, INV, // 0
INV, INV, INV, INV, INV, INV, INV, ACK, // 0
INV, INV, INV, INV, INV, INV, INV, 0x33, // 1
INV, INV, INV, 0x34, INV, 0x35, 0x36, INV, // 1
INV, INV, INV, INV, INV, INV, INV, 0x3A, // 2
INV, INV, INV, 0x3B, INV, 0x3C, 0x37, INV, // 2
INV, INV, INV, 0x3F, INV, 0x3D, 0x38, INV, // 3
INV, 0x3E, 0x39, INV, NACK, INV, INV, INV, // 3
INV, INV, INV, INV, INV, INV, INV, 0x24, // 4
INV, INV, INV, 0x23, INV, 0x22, 0x21, INV, // 4
INV, INV, INV, 0x1F, INV, 0x1E, 0x20, INV, // 5
INV, 0x1D, 0x1C, INV, 0x1B, INV, INV, INV, // 5
INV, INV, INV, 0x19, INV, 0x18, 0x1A, INV, // 6
INV, 0x17, 0x16, INV, 0x15, INV, INV, INV, // 6
INV, 0x25, 0x14, INV, 0x13, INV, INV, INV, // 7
0x32, INV, INV, INV, INV, INV, INV, INV, // 7
INV, INV, INV, INV, INV, INV, INV, RESVD2, // 8
INV, INV, INV, 0x0E, INV, 0x0D, 0x0C, INV, // 8
INV, INV, INV, 0x0A, INV, 0x09, 0x0B, INV, // 9
INV, 0x08, 0x07, INV, 0x06, INV, INV, INV, // 9
INV, INV, INV, 0x04, INV, 0x03, 0x05, INV, // a
INV, 0x02, 0x01, INV, 0x00, INV, INV, INV, // a
INV, 0x0F, 0x10, INV, 0x11, INV, INV, INV, // b
0x12, INV, INV, INV, INV, INV, INV, INV, // b
INV, INV, INV, RESVD1, INV, 0x2B, 0x30, INV, // c
INV, 0x2A, 0x2F, INV, 0x31, INV, INV, INV, // c
INV, 0x29, 0x2E, INV, 0x2D, INV, INV, INV, // d
0x2C, INV, INV, INV, INV, INV, INV, INV, // d
INV, RCBUSY, 0x28, INV, 0x27, INV, INV, INV, // e
0x26, INV, INV, INV, INV, INV, INV, INV, // e
ACK, INV, INV, INV, INV, INV, INV, INV, // f
INV, INV, INV, INV, INV, INV, INV, INV, // f
};
/// Packet identifiers from Mobile Decoders.
enum RailcomMobilePacketId
{
RMOB_POM = 0,
RMOB_ADRHIGH = 1,
RMOB_ADRLOW = 2,
RMOB_EXT = 3,
RMOB_DYN = 7,
RMOB_XPOM0 = 8,
RMOB_XPOM1 = 9,
RMOB_XPOM2 = 10,
RMOB_XPOM3 = 11,
RMOB_SUBID = 12,
RMOB_LOGON_ASSIGN_FEEDBACK = 13,
RMOB_LOGON_ENABLE_FEEDBACK = 15,
};
// each railcom block is represented by an instance of this class.
// The blockvpin is the vpin associated with this block for the purposes of
// a HAL driver for the railcom detection and the EXRAIL ONBLOCKENTER/ONBLOCKEXIT
// need to know if there is any detector Railcom detector
// otherwise <r cab cv> would block the async reply feature.
bool Railcom::hasActiveDetectors=false;
Railcom::Railcom(uint16_t blockvpin) {
DIAG(F("Create Railcom block %d"),blockvpin);
haveHigh=false;
haveLow=false;
packetsWithNoData=0;
lastChannel1Loco=0;
vpin=blockvpin;
}
uint16_t Railcom::expectLoco=0;
uint16_t Railcom::expectCV=0;
unsigned long Railcom::expectWait=0;
ACK_CALLBACK Railcom::expectCallback=0;
// anticipate is used when waiting for a CV read from a railcom loco
void Railcom::anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback) {
expectLoco=loco;
expectCV=cv;
expectWait=millis(); // start of timeout
expectCallback=callback;
}
// process is called to handle data buffer sent by collector
void Railcom::process(int16_t firstVpin,byte * buffer, byte length) {
// block,locohi,locolow
// block|0x80,data pom read cv
byte i=0;
while (i<length) {
byte block=buffer[i] & 0x3f;
byte type=buffer[i]>>6;
// Process is called by a raw data collector.
void Railcom::process(uint8_t * inbound, uint8_t length) {
hasActiveDetectors=true;
if (length<2 || (inbound[0]==0 && inbound[1]==0)) {
noData();
return;
}
switch (type) {
// a type=0 record has block,locohi,locolow
case 0: {
uint16_t locoid= ((uint16_t)buffer[i+1])<<8 | ((uint16_t)buffer[i+2]);
DIAG(F("RC3 b=%d l=%d"),block,locoid);
if (locoid==0) DCC::clearBlock(firstVpin+block);
else DCC::setLocoInBlock(locoid,firstVpin+block,true);
i+=3;
}
break;
case 2: { // csv value from POM read
byte value=buffer[i+1];
if (expectCV && DCCWaveform::getRailcomLastLocoAddress()==expectLoco) {
DCC::setLocoInBlock(expectLoco,firstVpin+block,false);
if (expectCallback) expectCallback(value);
expectCV=0;
}
i+=2;
}
break;
default:
DIAG(F("Unknown RC Collector code %d"),type);
return;
if (Diag::RAILCOM) {
static const char hexchars[]="0123456789ABCDEF";
if (length>2) {
USB_SERIAL.print(F("<*R "));
for (byte i=0;i<length;i++) {
if (i==2) Serial.write(' ');
USB_SERIAL.write(hexchars[inbound[i]>>4]);
USB_SERIAL.write(hexchars[inbound[i]& 0x0F ]);
}
USB_SERIAL.print(F(" *>\n"));
}
}
if (expectCV && DCCWaveform::getRailcomLastLocoAddress()==expectLoco) {
if (length>=4) {
auto v2=GETHIGHFLASH(decode,inbound[2]);
auto v3=GETHIGHFLASH(decode,inbound[3]);
uint16_t packet=(v2<<6) | (v3 & 0x3f);
// packet is 12 bits TTTTDDDDDDDD
byte type=(packet>>8) & 0x0F;
byte data= packet & 0xFF;
if (type==RMOB_POM) {
// DIAG(F("POM READ loco=%d cv(%d)=%d/0x%x"), expectLoco, expectCV,data,data);
expectCallback(data);
expectCV=0;
}
}
}
// loop() is called to detect timeouts waiting for a POM read result
void Railcom::loop() {
if (expectCV && (millis()-expectWait)> POM_READ_TIMEOUT) { // still waiting
expectCallback(-1);
expectCV=0;
}
auto v1=GETHIGHFLASH(decode,inbound[0]);
auto v2=(length>1) ? GETHIGHFLASH(decode,inbound[1]):INV;
uint16_t packet=(v1<<6) | (v2 & 0x3f);
// packet is 12 bits TTTTDDDDDDDD
byte type=(packet>>8) & 0x0F;
byte data= packet & 0xFF;
if (type==RMOB_ADRHIGH) {
holdoverHigh=data;
haveHigh=true;
packetsWithNoData=0;
}
else if (type==RMOB_ADRLOW) {
holdoverLow=data;
haveLow=true;
packetsWithNoData=0;
}
else {
// channel1 is unreadable or not loco address so maybe multiple locos in block
if (length>2 && GETHIGHFLASH(decode,inbound[0])!=INV) {
// it looks like we have channel2 data
auto thisLoco=DCCWaveform::getRailcomLastLocoAddress();
if (Diag::RAILCOM) DIAG(F("c2=%d"),thisLoco);
if (thisLoco==lastChannel1Loco) return;
if (thisLoco) DCC::setLocoInBlock(thisLoco,vpin,false); // this loco is in block, but not exclusive
return;
}
// channel1 no good and no channel2
noData();
return;
}
if (haveHigh && haveLow) {
uint16_t thisLoco=((holdoverHigh<<8)| holdoverLow) & 0x7FFF; // drop top bit
if (thisLoco!=lastChannel1Loco) {
// the exclusive DCC call is quite expensive, we dont want to call it every packet
if (Diag::RAILCOM) DIAG(F("h=%x l=%xc1=%d"),holdoverHigh, holdoverLow,thisLoco);
DCC::setLocoInBlock(thisLoco,vpin,true); // only this loco is in block
lastChannel1Loco=thisLoco;
}
}
}
void Railcom::noData() {
if (packetsWithNoData>MAX_WAIT_FOR_GLITCH) return;
if (packetsWithNoData==MAX_WAIT_FOR_GLITCH) {
// treat as no loco
haveHigh=false;
haveLow=false;
lastChannel1Loco=0;
// Previous locos (if any) is exiting block
DCC::clearBlock(vpin);
}
packetsWithNoData++;
}
// anticipate is used when waiting for a CV read from a railcom loco
void Railcom::anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback) {
if (!hasActiveDetectors) {
// if there are no active railcom detectors, this will
// not be timed out in process()... so deny it now.
callback(-2);
return;
}
expectLoco=loco;
expectCV=cv;
expectWait=millis(); // start of timeout
expectCallback=callback;
};

View File

@@ -1,5 +1,5 @@
/*
* © 202 5Chris Harlow
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
@@ -26,15 +26,28 @@ typedef void (*ACK_CALLBACK)(int16_t result);
class Railcom {
public:
static void anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback);
static void process(int16_t firstVpin,byte * buffer, byte length );
static void loop();
Railcom(uint16_t vpin);
/* Process returns -1: Call again next packet
0: No loco on track
>0: loco id
*/
void process(uint8_t * inbound,uint8_t length);
static void anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback);
private:
static const unsigned long POM_READ_TIMEOUT=500; // as per spec
static uint16_t expectCV,expectLoco;
static unsigned long expectWait;
static ACK_CALLBACK expectCallback;
static const byte MAX_WAIT_FOR_GLITCH=20; // number of dead or empty packets before assuming loco=0
static bool hasActiveDetectors;
static const unsigned long POM_READ_TIMEOUT=500; // as per spec
static uint16_t expectCV,expectLoco;
static unsigned long expectWait;
static ACK_CALLBACK expectCallback;
void noData();
uint16_t vpin;
uint8_t holdoverHigh,holdoverLow;
bool haveHigh,haveLow;
uint8_t packetsWithNoData;
uint16_t lastChannel1Loco;
static const byte MAX_WAIT_FOR_GLITCH=20; // number of dead or empty packets before assuming loco=0
};
#endif

View File

@@ -1,58 +0,0 @@
Virtual Bitmap device pins.
a Bitmap device pin is a software representation of a virtual hardware device that has the ability to store a 16bit value.
This this is easier to manage than LATCH in EXRAIL as they can be explicitely set and tested without interfering with underlying hardware or breaching the 255 limit.
Virtual pins may be set, reset and tested in the same way as any other pin. Unlike sensors and leds, these device pins are both INPUT and OUTPUT These can be used in many ways:
As a simple digital flag to assist in inter-thread communication.
A flag or value that can be set from commands and tested in EXRAIL.(e.g. to stop a sequence)
As a counter for looping or occupancy counts such as trains passing over a multi track road crossing.
As a collection of 16 digital bits that can be set, reset, toggled, masked and tested.
Existing <> and exrail commands for vpins work on these pins.
Virtual pin creation:
HAL(Bitmap,firstpin,npins)
creates 1 or more virtual pins in software. (RAM requirement approximately 2 bytes per pin)
e.g. HAL(Bitmap,1000,20) creates pins 1000..1019
Simple use as flags:
This uses the traditional digital pin commands
SET(1013) RESET(1013) sets value 1 or 0
SET(1000,20) RESET(1000,20) sets/resets a range of pins
IF(1000) tests if pin value!=0
Commands can set 1/0 values using <z 1010> <z -1010> as for any digital output.
BLINK can be used to set them on/off on a time pattern.
In addition, Exrail sensor comands work as if these pins were sensors
ONBUTTON(1013) triggers when value changes from 0 to something.
ONSENSOR(1013) triggers when value changes to or from 0.
<S 1013 1013 1> and JMRI_SENSOR(1013) report <Q/q responses when changing to or from 0.
Use as analog values:
Analog values may be set into the virtual pins and tested using the existing analog value commands and exrail macros.
<z vpin value> <D ANIN vpin> etc.
Use as counters:
For loop counting, counters can be incremented by BITMAP_INC(1013) and decremented by BITMAP_DEC(1013) and tested with IF/IFNOT/IFGTE etc.
Counters be used to automate a multi track crossing where each train entering increments the counter and decrements it on clearing the crossing. Crossing gate automation can be started when the value changes from 0, and be stopped when the counter returns to 0. Detecting the first increment from 0 to 1 can be done with ONBUTTON(1013) and the automation can use IF(1013) or IFNOT(1013) to detect when it needs to reopen the road gates.
Use as binary flag groups:
Virtual pins (and others that respond to an analog read in order to provide bitmapped digital data, such as SensorCam) can be set and tested with new special EXRAIL commands
IFBITMAP_ALL(vpin,mask) Bitwise ANDs the the vpin value with the mask value and is true if ALL the 1 bits in the mask are also 1 bits in the value.
e.g. IFBITMAP_ALL(1013,0x0f) would be true if ALL the last 4 bits of the value are 1s.
IFBITMAP_ANY(1013,0x0f) would be true if ANY of the last 4 bits are 1s.
Modifying bitmap values:
BITMAP_AND(vpin,mask) performs a bitwise AND operation.
BITMAP_OR(vpin,mask) performa a bitwise OR operation
BITMAP_XOR(vpin,mask) performs a bitwise EXCLUSIVE OR (which is basically a toggle)

View File

@@ -17,16 +17,15 @@ Enabling the Railcom Cutout requires a `<C RAILCOM ON>` command. This can be add
Code to calculate the cutout position and provide synchronization for the sampling is in `DCCWaveform.cpp` (not ESP32)
and in general a global search for "railcom" will show all code changes that have been made to support this.
Code to actually implement the timing of the cutout is highly cpu dependent and can be found in the various implementations of `DCCTimer.h`. At this time only `DCCTimerAVR.cpp`has implemented this.
Code to actually implement the timing of the cutout is hihjly cpu dependent and can be found in gthe various implementations of `DCCTimer.h`. At this time only `DCCTimerAVR.cpp`has implemented this.
Reading Railcom data:
A new HAL handler (`IO_I2CRailcom.h`) has been added to process input from a 32-block railcom collecter which operates over I2C. The collector and its readers sit between the CS and the track and collect railcom data from locos during the cutout.
The Collector device removes 99.9% of the railcom traffic and returns just a summary of what has changed since the last cutout.
After the cutout the HAL driver reads the Collector summary over I2C and passes the raw data to the CS logic (`Railcom.cpp`) for analysis.
A new HAL handler (`IO_I2CRailcom.h`)has been added to process input from a 2-block railcom reader (Refer Henk) which operates as a 2 channel UART accessible over I2C. The reader(s) sit between the CS and the track and collect railcom data from locos during the cutout.
After the cutout the HAL driver reads the UARTs over I2C and passes the raw data to the CS logic (`Railcom.cpp`)for analysis.
Each 32-block reader is described in myAutomation like `HAL(I2CRailcom,10000,32,0x08)` which will assign 32 blocks on i2c address 0x08 with vpin numbers 10000 and 10031. If you only use fewer channel in the collector, you can assign fewer pins here.
(Implementation notes.. you may have multiple collectors, each one will requite a HAL line to define its i2c address and vpins to represent block numbers.)
Each 2-block reader is described in myAutomation like `HAL(I2CRailcom,10000,2,0x48)` which will assign 2 channels on i2c address 0x48 with vpin numbers 10000 and 10001. If you only use the first channel in the reader, just asign one pin instead of two.
(Implementation notes.. potentially other readers are possible with suitable HAL drivers. There are however several touch-points with the code DCC Waveform code which helps the HAL driver to understand when the data is safe to sample, and how to interpret responses when the sender is unknown. )
Making use of Railcom data
@@ -63,9 +62,14 @@ Making use of Railcom data
Railcom allows for the facility to read loco cv values while on the main track. This is considerably faster than PROG track access but depends on the loco being in a Railcom monitored block.
To read from PROG Track we use `<R cv>` response is `<r value>`
To read from prog Track we use `<R cv>` response is `<r value>`
To read from MAIN track use `<r loco cv>`
To read from main track use `<r loco cv>`
response is `<r loco cv value>`
Additional EXRAIL features in Railcom Branch:
- ESTAOPALL stops all locos immediately
- XPOM(cab,cv,value) POM write cv to sepcific loco
(POM(cv,value) already writes cv to current loco)

View File

@@ -1,21 +0,0 @@
# 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>`)

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
# 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)

View File

@@ -1,888 +0,0 @@
@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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,94 +0,0 @@
# 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 = ()

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,35 +0,0 @@
@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

View File

@@ -1,39 +0,0 @@
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

View File

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

View File

@@ -146,13 +146,6 @@ void halSetup() {
//PCF8574::create(200, 8, 0x23, 40);
// Alternative form to initialize 8 pins as output
// INT pin -1, when INT pin is not used
//PCF8574::create(200, 8, 0x23, -1, 255); // 8 pins High
//PCF8574::create(200, 8, 0x23, -1, 0); // 8 pins Low
//PCF8574::create(200, 8, 0x23, -1, 0b11000010);
// pins listed sequentially from 7 to 0
//=======================================================================
// The following directive defines a PCF8575 16-port I2C GPIO Extender module.

View File

@@ -13,10 +13,9 @@ default_envs =
mega2560
; uno
; nano
ESP32
Nucleo-F411RE
Nucleo-F446RE
Nucleo-F429ZI
; ESP32
; Nucleo-F411RE
; Nucleo-F446RE
src_dir = .
include_dir = .

View File

@@ -3,24 +3,10 @@
#include "StringFormatter.h"
#define VERSION "5.5.25"
// - PCF8574 output pin initialization parameter
// 5.2.25 - IO_Bitmap and assicated Exrail macros
// 5.5.24 - SensorCAM in I2C scan and automatically setClock
// 5.5.23 - Reminder loop Idle packet optimization
// 5.5.22 - (5.4.9) Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
// - DCC 5mS gap to same loco DCC packet restriction
// - Catch up MASTER for ESP32 IDF version check
// - Catch up MASTER for Serial input length check
// - Catch up MASTER for JOIN/POWER order change
// 5.5.21 - Backed out the broken merge with frequency change and
// 5.5.20 - EXRAIL SET/RESET assert fix
// 5.5.19 - Railcom change to use RailcomCollector device
// 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
#define VERSION "5.5.17"
// 5.5.17 - Add EX8874 shield for F413ZH/F446RE
// - Nucleo-F4 timer sync for DC mode
// - <JL> command - power state and current by track
// 5.5.16 - DOXYGEN comments in EXRAIL2MacroReset.h
// 5.5.15 - Support for F429ZI/F329ZI
// - Own mDNS support for (wired) Ethernet