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

Compare commits

..

2 Commits

Author SHA1 Message Date
Asbelos
ec4c6b9c02 Cleanup to new structure 2025-03-05 00:30:13 +00:00
Asbelos
620ad6275b Railcom3 2025-02-28 01:16:03 +00:00
48 changed files with 1658 additions and 7136 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

@@ -1,111 +0,0 @@
/*
* © 2023-2025, Barry Daniel
* © 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/>.
*/
//sensorCAM parser.cpp version 3.06 Jan 2025
#include "DCCEXParser.h"
#include "CamParser.h"
#include "FSH.h"
void camsend(byte camop,int16_t param1,int16_t param3) {
DIAG(F("CamParser: %d %c %d %d"),CAMBaseVpin,camop,param1,param3);
IODevice::writeAnalogue(CAMBaseVpin,param1,camop,param3);
}
// The CAMVPINS array will be filled by IO_EXSensorCam HAL drivers calling
// the CamParser::addVpin() function.
// The CAMBaseVpin is the one to be used when commands are given without a vpin.
VPIN CamParser::CAMBaseVpin = 0; // no vpins yet known
VPIN CamParser::CAMVPINS[] = {0,0,0,0}; // determines max # CAM's
int CamParser::vpcount=sizeof(CAMVPINS)/sizeof(CAMVPINS[0]);
ZZ(N) // lists current base vpin and others available
DIAG(F("Cam base vpin:%d"),CAMBaseVpin);
for (auto i=0;i<vpcount;i++){
if (CAMVPINS[i]==0) break;
DIAG(F("EXSensorCam #%d vpin %d"),i+1,CAMVPINS[i]);
}
ZZ(N,V) // show version
camsend('^',0,999);
ZZ(N,F) //
camsend(']',0,999);
ZZ(N,Q) //
camsend('Q',0,10);
ZZ(N,camop) //
CHECK(STRCHR_P((const char *)F("EGMRW"),camop))
camsend(camop,0,999);
ZZ(N,C,pin) // change CAM base vpin or cam number from list
CHECK(pin>=100 || (pin<vpcount && pin>=0))
CAMBaseVpin=(pin>=100? pin:CAMVPINS[pin];
DIAG(F("CAM base Vpin:%d "),pin,CAMBaseVpin);
ZZ(N,camop,p1) //send camop p1
CHECK(STRCHR_P((const char *)F("ABFHILMNOPQRSTUV"),camop))
camsend(camop,p1,999);
ZZ(N,I,p1,p2) //send camop p1 p2
camsend('I',p1,p2);
ZZ(N,J,p1,p2) //send camop p1 p2
camsend('J',p1,p2);
ZZ(N,M,p1,p2) //send camop p1 p2
camsend('M',p1,p2);
ZZ(N,N,p1,p2) //send camop p1 p2
camsend('N',p1,p2);
ZZ(N,T,p1,p2) //send camop p1 p2
camsend('T',p1,p2);
ZZ(N,vpin,rowY,colX) //send 0x80 row col
auto hold=CAMBaseVpin;
CAMBaseVpin=vpin;
camsend(0x80,rowY,colX);
CAMBaseVpin=hold;
ZZ(N,A,id,row,col)
CHECK(col<=316 && col>=0)
CHECK(row<=236 && row>=0)
CHECK(id<=97 && id >=0)
auto hold=CAMBaseVpin;
CAMBaseVpin=CAMBaseVpin + (id/10)*8 + id%10; //translate from pseudo octal
camsend(0x80,row,col)
CAMBaseVpin=hold;
void CamParser::addVpin(VPIN pin) {
// called by IO_EXSensorCam starting up a camera on a vpin
byte slot=255;
for (auto i=0;i<vpcount && slot==255;i++) {
if (CAMVPINS[i]==0) {
slot=i;
CAMVPINS[slot]=pin;
}
}
if (slot==255) {
DIAG(F("No more than %d cameras supported"),vpcount);
return;
}
if (slot==0) CAMBaseVpin=pin;
DIAG(F("CamParser Registered cam #%dvpin %d"),slot+1,pin);
// tell the DCCEXParser that we wish to filter commands
DCCEXParser::setCamParserFilter(&parse);
}

View File

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

View File

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

View File

@@ -1,634 +0,0 @@
/*
* © 2022 Paul M Antoine
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2025 Herb Morton
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2025 Chris Harlow
* © 2022 Colin Murdoch
* 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/>.
*/
/*
List of single character OPCODEs in use for reference.
When determining a new OPCODE for a new feature, refer to this list as the source of truth.
Once a new OPCODE is decided upon, update this list.
Character, Usage
/, |EX-R| interactive commands
-, Remove from reminder table
=, |TM| configuration
!, Emergency stop
@, Reserved for future use - LCD messages to JMRI
#, Request number of supported cabs/locos; heartbeat
+, WiFi AT commands
?, Reserved for future use
0, Track power off
1, Track power on
a, DCC accessory control
A, DCC extended accessory control
b, Write CV bit on main
B, Write CV bit
c, Request current command
C, configure the CS
d,
D, Diagnostic commands
e, Erase EEPROM
E, Store configuration in EEPROM
f, Loco decoder function control (deprecated)
F, Loco decoder function control
g,
G,
h,
H, Turnout state broadcast
i, Server details string
I, Turntable object command, control, and broadcast
j, Throttle responses
J, Throttle queries
k, Block exit (Railcom)
K, Block enter (Railcom)
l, Loco speedbyte/function map broadcast
L, Reserved for LCC interface (implemented in EXRAIL)
m, message to throttles (broadcast output)
m, set momentum
M, Write DCC packet
n, Reserved for SensorCam
N, Reserved for Sensorcam
o, Neopixel driver (see also IO_NeoPixel.h)
O, Output broadcast
p, Broadcast power state
P, Write DCC packet
q, Sensor deactivated
Q, Sensor activated
r, Broadcast address read on programming track
R, Read CVs
s, Display status
S, Sensor configuration
t, Cab/loco update command
T, Turnout configuration/control
u, Reserved for user commands
U, Reserved for user commands
v,
V, Verify CVs
w, Write CV on main
W, Write CV
x,
X, Invalid command response
y,
Y, Output broadcast
z, Direct output
Z, Output configuration/control
*/
/*
Each ZZ macro matches a command opcode and its parameters.
Paramters in UPPER case are matched as keywords, parameters in lower case are values provided by the user.
Its important to recognise that if the same opcode has more than one match with the same length, you must match the
keywprds before picking up user values.
e.g.
ZZ(X,value1,value2)
ZZ(X,SET,value1) This will never be matched.
Use of the CHECK() macro validates a condition to be true.
If the condition is false an error is genarated, resulting in an <X> reply.
Commonly known parameters such as loco, cv bitvalue etc are range checked automatically.
The REPLY( format, ...) macro sends a formatted string to the stream.
These macros are included into the DCCEXParser::execute function so
stream, ringStream and other DCCEXParser variables are available in context. */
ZZBEGIN
ZZ(#) // Request number of simultaneously supported locos
REPLY( "<# %d>\n", MAX_LOCOS)
ZZ(!) // Emergency stop all locos
DCC::estopAll();
ZZ(t,loco) // Request loco status
CommandDistributor::broadcastLoco(DCC::lookupSpeedTable(loco,false));
ZZ(t,loco,tspeed,direction) // Set throttle speed(0..127) and direction (0=reverse, 1=fwd)
CHECK(setThrottle(loco,tspeed,direction))
ZZ(t,ignore,loco,tspeed,direction) // (Deprecated) Set throttle speed and direction
CHECK(setThrottle(loco,tspeed,direction))
ZZ(f,loco,byte1) // (Deprecated use F) Set loco function group
switch ( byte1 & 0b11110000) { // 1111 0000
case 0b11100000: // 111x xxxx Function group 1 F0..F4
case 0b11110000:
// Shuffle bits from order F0 F4 F3 F2 F1 to F4 F3 F2 F1 F0
return (funcmap(loco, (byte1 << 1 & 0x1e) | (byte1 >> 4 & 0x01), 0, 4));
case 0b10110000: // 1011 xxxx Function group 2 F5..F8
return (funcmap(loco, byte1, 5, 8));
case 0b10100000: // 1010 xxxx Function group 3 F9..F12
return (funcmap(loco, byte1, 9, 12));
default:
CHECK(false,Invalid function group)
}
ZZ(f,loco,group,byte2) // (Deprecated use F) Set loco function group
if (group == 222) return (funcmap(loco, byte2, 13, 20));
if (group == 223) return (funcmap(loco, byte2, 21, 28));
CHECK(false,Invalid function group)
ZZ(T) // List all turnouts
Turnout::printAll(stream); // will <X> if none found
ZZ(T,id) // Delete turnout
CHECK(Turnout::remove(id))
ZZ(T,id,X) // List turnout details
auto tt=Turnout::get(id); CHECK(tt) tt->print(stream);
ZZ(T,id,T) // Throw Turnout
Turnout::setClosed(id, false);
ZZ(T,id,C) // Close turnout#
Turnout::setClosed(id, true);
ZZ(T,id,value) // Close (value=0) ot Throw turnout
Turnout::setClosed(id, value==0);
ZZ(T,id,SERVO,vpin,closedValue,thrownValue) // Create Servo turnout
CHECK(ServoTurnout::create(id, (VPIN)vpin, (uint16_t)closedValue, (uint16_t)thrownValue, 1))
ZZ(T,id,VPIN,vpin) // Create pin turnout
CHECK(VpinTurnout::create(id, vpin))
ZZ(T,id,DCC,addr,subadd) // Create DCC turnout
CHECK(DCCTurnout::create(id, addr, subadd))
ZZ(T,id,DCC,linearAddr) // Create DCC turnout
CHECK(DCCTurnout::create(id, (linearAddr-1)/4+1, (linearAddr-1)%4))
ZZ(T,id,addr,subadd) // Create DCC turnout
CHECK(DCCTurnout::create(id, addr, subadd))
ZZ(T,id,vpin,closedValue,thrownValue) // Create SERVO turnout
CHECK(ServoTurnout::create(id, (VPIN)vpin, (uint16_t)closedValue, (uint16_t)thrownValue, 1))
ZZ(S,id,vpin,pullup) // Create Sensor
CHECK(Sensor::create(id,vpin,pullup))
ZZ(S,id) // Delete sensor
CHECK(Sensor::remove(id))
ZZ(S) // List sensors
for (auto *tt = Sensor::firstSensor; tt; tt = tt->nextSensor) {
REPLY("<Q %d %d %d>\n", tt->data.snum, tt->data.pin, tt->data.pullUp)
}
ZZ(J,M) // List stash values
Stash::list(stream);
ZZ(J,M,stash_id) // get stash value
Stash::list(stream, stash_id);
ZZ(J,M,CLEAR,ALL) // Clear all stash values
Stash::clearAll();
ZZ(J,M,CLEAR,stash_id) // Clear given stash
Stash::clear(stash_id);
ZZ(J,M,stashId,locoId) // Set stash value
Stash::set(stashId,locoId);
ZZ(J,M,CLEAR,ANY,locoId) // Clear all stash entries that contain locoId
Stash::clearAny(locoId);
ZZ(J,C) // get fastclock time
REPLY("<jC %d>\n", CommandDistributor::retClockTime())
ZZ(J,C,mmmm,nn) // Set fastclock time
CommandDistributor::setClockTime(mmmm, nn, 1);
ZZ(J,G) // FReport gauge limits
TrackManager::reportGauges(stream);
ZZ(J,I) // Report currents
TrackManager::reportCurrent(stream);
// TODO... Ask @Ash zz(J,L,display,row) // Direct current displays to LCS/OLED
// TrackManager::reportCurrentLCD(display,row); // Track power status
ZZ(J,A) // List Routes
REPLY( "<jA>\n")
ZZ(J,R) // List Roster
REPLY("<jR")
#ifdef EXRAIL_ACTIVE
SENDFLASHLIST(stream,RMFT2::rosterIdList)
#endif
REPLY(">\n");
#ifdef EXRAIL_ACTIVE
ZZ(J,R,id) // Get roster for loco
auto rosterName= RMFT2::getRosterName(id);
if (!rosterName) rosterName=F("");
auto functionNames= RMFT2::getRosterFunctions(id);
if (!functionNames) functionNames=RMFT2::getRosterFunctions(0);
if (!functionNames) functionNames=F("");
REPLY("<jR %d \"%S\" \"%S\">\n",id, rosterName, functionNames)
#endif
ZZ(J,T) // Get turnout list
REPLY("<jT")
for ( auto t=Turnout::first(); t; t=t->next()) if (!t->isHidden()) REPLY(" %d",t->getId())
REPLY(">\n");
ZZ(J,T,id) // Get turnout state and description
auto t=Turnout::get(id);
if (!t || t->isHidden()) { REPLY("<jT %d X>\n",id) return true; }
const FSH *tdesc=nullptr;
#ifdef EXRAIL_ACTIVE
tdesc = RMFT2::getTurnoutDescription(id);
#endif
if (!tdesc) tdesc = F("");
REPLY("<jT %d %c \"%S\">\n",id,t->isThrown()?'T':'C',tdesc)
ZZ(z,vpin) // Set pin. HIGH iv vpin positive, LOW if vpin negative
IODevice::write(vpin,(vpin>0)?HIGH:LOW);
ZZ(z,vpin,analog,profile,duration) // Change analog value over duration (Fade or servo move)
IODevice::writeAnalogue(vpin,analog,profile,duration);
ZZ(z,vpin,analog,profile) // Write analog device using profile number (Fade or servo movement)
IODevice::writeAnalogue(vpin,analog,profile,0);
ZZ(z,vpin,analog) // Write analog device value
IODevice::writeAnalogue(vpin,analog,0,0);
// ==========================
// Turntable - no support if no HAL
// <I> - list all
// <I id> - broadcast type and current position
// <I id DCC> - create DCC - This is TBA
// <I id steps> - operate (DCC)
// <I id steps activity> - operate (EXTT)
// <I id ADD position value> - add position
// <I id EXTT i2caddress vpin home> - create EXTT
ZZ(I) // List all turntables
return Turntable::printAll(stream);
ZZ(I,id) // Broadcast turntable type and current position
auto tto = Turntable::get(id);
CHECK(tto,Turntable not found)
REPLY("<I %d %d>\n", tto->isEXTT(), tto->getPosition())
ZZ(I,id,position) // Rotate a DCC turntable
auto tto = Turntable::get(id);
CHECK(tto,Turntable not found)
CHECK(!tto->isEXTT(),Turntable type incorrect)
CHECK(tto->setPosition(id,position))
ZZ(I,id,DCC,home) // Create DCC turntable
CHECK(home >=0 && home <= 3600)
auto tto = Turntable::get(id);
CHECK(!tto,Turntable already exists)
CHECK(DCCTurntable::create(id))
tto = Turntable::get(id);
CHECK(tto)
tto->addPosition(0, 0, home);
REPLY("<I>\n")
ZZ(I,id,position,activity) // Rotate an EXTT turntable
auto tto = Turntable::get(id);
CHECK(tto,Turntable not found)
CHECK(tto->isEXTT(), Turntable wrong type)
CHECK(tto->setPosition(id, position,activity))
ZZ(I,id,EXTT,vpin,home) // Create an EXTT turntable
auto tto = Turntable::get(id);
CHECK(!tto,Turntable already exists)
CHECK(home >= 0 && home <= 3600)
CHECK(EXTTTurntable::create(id, (VPIN)vpin))
tto = Turntable::get(id);
tto->addPosition(0, 0, home);
REPLY("<I>\n")
ZZ(I,id,ADD,position,value,angle) // Add turntable position
auto tto = Turntable::get(id);
CHECK(tto,Turntable not found)
CHECK(position <= 48 && angle >=0 && angle <= 3600)
tto->addPosition(id,value,angle);
REPLY("<I>\n")
ZZ(Q) // List all sensors
Sensor::printAll(stream);
ZZ(s) // Command station status
REPLY("<iDCC-EX V-" VERSION " / " ARDUINO_TYPE " / %S G-" GITHUB_SHA ">\n", DCC::getMotorShieldName())
CommandDistributor::broadcastPower(); // <s> is the only "get power status" command we have
Turnout::printAll(stream); //send all Turnout states
Sensor::printAll(stream); //send all Sensor states
#ifndef DISABLE_EEPROM
ZZ(E) // STORE EPROM
EEStore::store();
REPLY("<e %d %d %d>\n", EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs)
ZZ(e) // CLEAR EPROM
EEStore::clear();
REPLY("<O>\n")
#endif
ZZ(Z) // List Output definitions
bool gotone = false;
for (auto *tt = Output::firstOutput; tt ; tt = tt->nextOutput) {
gotone = true;
REPLY("<Y %d %d %d %d>\n",tt->data.id, tt->data.pin, tt->data.flags, tt->data.active)
}
CHECK(gotone,No Outputs found)
ZZ(Z,id,pin,iflag) // Create Output
CHECK(id > 0 && iflag >= 0 && iflag <= 7 )
CHECK(Output::create(id,pin,iflag, 1))
REPLY("<O>\n")
ZZ(Z,id,active) // Set output
auto o = Output::get(id);
CHECK(o,Output not found)
o->activate(active);
REPLY("<Y %d %d>\n", id,active)
ZZ(Z,id) // Delete output
CHECK(Output::remove(id))
REPLY("<O>\n")
ZZ(D,ACK,ON) // Enable PROG track diagnostics
Diag::ACK = true;
ZZ(D,ACK,OFF) // Disable PROG track diagnostics
Diag::ACK = false;
ZZ(D,CABS) // Diagnostic display loco state table
DCC::displayCabList(stream);
ZZ(D,RAM) // Diagnostic display free RAM
DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
ZZ(D,CMD,ON) // Enable command input diagnostics
Diag::CMD = true;
ZZ(D,CMD,OFF) // Disable command input diagnostics
Diag::CMD = false;
ZZ(D,RAILCOM,ON) // Enable Railcom diagnostics
Diag::RAILCOM = true;
ZZ(D,RAILCOM,OFF) // DIsable Railcom diagnostics
Diag::RAILCOM = false;
ZZ(D,WIFI,ON) // Enable Wifi diagnostics
Diag::WIFI = true;
ZZ(D,WIFI,OFF) // Disable Wifi diagnostics
Diag::WIFI = false;
ZZ(D,ETHERNET,ON) // Enable Ethernet diagnostics
Diag::ETHERNET = true;
ZZ(D,ETHERNET,OFF) // Disabel Ethernet diagnostics
Diag::ETHERNET = false;
ZZ(D,WIT,ON) // Enable Withrottle diagnostics
Diag::WITHROTTLE = true;
ZZ(D,WIT,OFF) // Disable Withrottle diagnostics
Diag::WITHROTTLE = false;
ZZ(D,LCN,ON) // Enable LCN Diagnostics
Diag::LCN = true;
ZZ(D,LCN,OFF) // Disabel LCN diagnostics
Diag::LCN = false;
ZZ(D,WEBSOCKET,ON) // Enable Websocket diagnostics
Diag::WEBSOCKET = true;
ZZ(D,WEBSOCKET,OFF) // Disable wensocket diagnostics
Diag::WEBSOCKET = false;
#ifndef DISABLE_EEPROM
ZZ(D,EEPROM,numentries) // Dump EEPROM contents
EEStore::dump(numentries);
#endif
ZZ(D,ANOUT,vpin,position) // see <z vpin position>
IODevice::writeAnalogue(vpin,position,0);
ZZ(D,ANOUT,vpin,position,profile) // see <z vpin position profile>
IODevice::writeAnalogue(vpin,position,profile);
ZZ(D,SERVO,vpin,position) // Test servo
IODevice::writeAnalogue(vpin,position,0);
ZZ(D,SERVO,vpin,position,profile) // Test servo
IODevice::writeAnalogue(vpin,position,profile);
ZZ(D,ANIN,vpin) // Display analogue input value
DIAG(F("VPIN=%u value=%d"), vpin, IODevice::readAnalogue(vpin));
ZZ(D,HAL,SHOW) // Show HAL devices table
IODevice::DumpAll();
ZZ(D,HAL,RESET) // Reset all HAL devices
IODevice::reset();
ZZ(D,TT,vpin,steps) // Test turntable
IODevice::writeAnalogue(vpin,steps,0);
ZZ(D,TT,vpin,steps,activity) // Test turntable
IODevice::writeAnalogue(vpin,steps,activity);
ZZ(C,PROGBOOST) // Configute PROG track boost
TrackManager::progTrackBoosted=true;
ZZ(C,RESET) // Reset and restart command station
DCCTimer::reset();
ZZ(C,SPEED28) // Set all DCC speed commands as 28 step to old decoders
DCC::setGlobalSpeedsteps(28); DIAG(F("28 Speedsteps"));
ZZ(C,SPEED128) // Set all DCC speed commands to 128 step (default)
DCC::setGlobalSpeedsteps(128); DIAG(F("128 Speedsteps"));
ZZ(C,RAILCOM,ON) // Enable Railcom cutout
DIAG(F("Railcom %S"),DCCWaveform::setRailcom(true,false)?F("ON"):F("OFF"));
ZZ(C,RAILCOM,OFF) // Disable Railcom cutout
DIAG(F("Railcom OFF")); DCCWaveform::setRailcom(false,false);
ZZ(C,RAILCOM,DEBUG) // Enable Railcom cutout for easy scope reading test
DIAG(F("Railcom %S"), DCCWaveform::setRailcom(true,true)?F("ON"):F("OFF"));
#ifndef DISABLE_PROG
ZZ(D,ACK,LIMIT,value) // Set ACK detection limit mA
DCCACK::setAckLimit(value); LCD(1, F("Ack Limit=%dmA"), value);
ZZ(D,ACK,MIN,value,MS) // Set ACK minimum duration mS
DCCACK::setMinAckPulseDuration(value*1000L); LCD(1, F("Ack Min=%dmS"), value);
ZZ(D,ACK,MIN,value) // Set ACK minimum duration uS
DCCACK::setMinAckPulseDuration(value); LCD(1, F("Ack Min=%duS"), value);
ZZ(D,ACK,MAX,value,MS) // Set ACK maximum duration mS
DCCACK::setMaxAckPulseDuration(value*1000L); LCD(1, F("Ack Max=%dmS"), value);
ZZ(D,ACK,MAX,value) // Set ACK maximum duration uS
DCCACK::setMaxAckPulseDuration(value); LCD(1, F("Ack Max=%duS"), value);
ZZ(D,ACK,RETRY,value) // Set ACK retry count
DCCACK::setAckRetry(value); LCD(1, F("Ack Retry=%d"), value);
#endif
#if defined(ARDUINO_ARCH_ESP32)
// Dirty definition tricks because the executed check needs quote separation markers
// that should be invisible to the doc extractor.
// The equivalent documentation will be extracted from the commented line below
// and the matchedFormat is hand modified to the correct format which includes quotes.
// (documented version) ZZ(C,WIFI,"ssid","password") // reconfigure stored wifi credentials
ZZ_nodoc(C,WIFI,ssid,password)
CHECK(false, ssid and password must be in "quotes")
ZZ_nodoc(C,WIFI,marker1,ssid,marker2,password)
DCCEXParser::matchedCommandFormat=F("C,WIFI,\"ssid\",\"password\""); // for error reporting
CHECK(marker1==0x7777 && marker2==0x7777, ssid and password must be in "quotes")
WifiESP::setup((const char*)(com + ssid), (const char*)(com + password), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif
ZZ(o,vpin) // Set neopixel on(vpin>0) or off(vpin<0)
IODevice::write(abs(vpin),vpin>0);
ZZ(o,vpin,count) // Set multiple neopixels on(vpin>0) or off(vpin<0)
IODevice::writeRange(abs(vpin),vpin>0,count);
ZZ(o,vpin,r,g,b) // Set neopixel colour
CHECK(r>=0 && r<=0xff && g>=0 && g<=0xff && b>=0 && b<=0xff, r,g,b values range 0..255)
IODevice::writeAnalogueRange(abs(vpin),vpin>0,r<<8 | g,b,1);
ZZ(o,vpin,r,g,b,count) // Set multiple neopixels colour
CHECK(r>=0 && r<=0xff && g>=0 && g<=0xff && b>=0 && b<=0xff, r,g,b values range 0..255)
IODevice::writeAnalogueRange(abs(vpin),vpin>0,r<<8 | g,b,count);
ZZ(1) // Power ON all tracks
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);
ZZ(1,MAIN) // Power on MAIN track
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
#ifndef DISABLE_PROG
ZZ(1,PROG) // Power on PROG track
TrackManager::setJoin(false); TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
ZZ(1,JOIN) // JOIN prog track to MAIN and power
TrackManager::setJoin(true); TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
#endif
ZZ(1,track) // Power on given track
TrackManager::setTrackPower(POWERMODE::ON, (byte)track-'A');
ZZ(0) // Power off all tracks
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);
ZZ(0,MAIN) // Power off MAIN track
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
ZZ(0,PROG) // Power off PROG track
TrackManager::setJoin(false);
TrackManager::progTrackBoosted=false;
// todo move to TrackManager Prog track boost mode will not outlive prog track off
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);
ZZ(0,track) // Power off given track
TrackManager::setJoin(false);
TrackManager::setTrackPower(POWERMODE::OFF, (byte)track-'a');
ZZ(c) // Report main track currect (Deprecated)
TrackManager::reportObsoleteCurrent(stream);
ZZ(a,address,subaddress,activate) // Send DCC accessory command
CHECK(activate==0 || activate ==1, invalid activate 0..1 )
DCC::setAccessory(address, subaddress,activate ^ accessoryCommandReverse);
ZZ(a,address,subaddress,activate,onoff) // Send DCC accessory command with onoff control (TODO.. numbers)
CHECK(activate==0 || activate ==1, invalid activate 0..1 )
CHECK(onoff>=0 && onoff<=2,invalid onoff 0..2 )
DCC::setAccessory(address, subaddress,activate ^ accessoryCommandReverse ,onoff);
ZZ(a,linearaddress,activate) // send dcc accessory command
CHECK(activate==0 || activate ==1, invalid activate 0..1 )
DCC::setAccessory((linearaddress - 1) / 4 + 1,(linearaddress - 1) % 4 ,activate ^ accessoryCommandReverse);
ZZ(A,address,value) // Send DCC extended accessory (Aspect) command
DCC::setExtendedAccessory(address,value);
ZZ(w,loco,cv,value) // POM write cv on main track
DCC::writeCVByteMain(loco,cv,value);
ZZ(r,loco,cv) // POM read cv on main track
CHECK(DCCWaveform::isRailcom(),Railcom not active)
EXPECT_CALLBACK
DCC::readCVByteMain(loco,cv,callback_r);
ZZ(b,loco,cv,bit,bitvalue) // POM write cv bit on main track
DCC::writeCVBitMain(loco,cv,bit,bitvalue);
ZZ(m,LINEAR) // Set Momentum algorithm to linear acceleration
DCC::linearAcceleration=true;
ZZ(m,POWER) // Set momentum algortithm to very based on difference between current speed and throttle seting
DCC::linearAcceleration=false;
ZZ(m,loco,momentum) // set momentum for loco (accel and braking)
CHECK(DCC::setMomentum(loco,momentum,momentum))
ZZ(m,loco,accelerating,braking) // set momentum for loco
CHECK(DCC::setMomentum(loco,accelerating,braking))
// todo reorder for more sensible doco.
ZZ(W,cv,value,ignore1,ignore2) // (Deprecated) Write cv value on PROG track
EXPECT_CALLBACK DCC::writeCVByte(cv,value, callback_W);
ZZ(W,loco) // Write loco address on PROG track
EXPECT_CALLBACK DCC::setLocoId(loco,callback_Wloco);
ZZ(W,CONSIST,loco,REVERSE) // Write consist address and reverse flag on PROG track
EXPECT_CALLBACK DCC::setConsistId(loco,true,callback_Wconsist);
ZZ(W,CONSIST,loco) // write consist address on PROG track
EXPECT_CALLBACK DCC::setConsistId(loco,false,callback_Wconsist);
ZZ(W,cv,value) // Write cv value on PROG track
EXPECT_CALLBACK DCC::writeCVByte(cv,value, callback_W);
ZZ(W,cv,bitvalue,bit) // Write cv bit on prog track
EXPECT_CALLBACK DCC::writeCVBit(cv,bitvalue,bit,callback_W);
ZZ(V,cv,value) // Fast read cv with expected value
EXPECT_CALLBACK DCC::verifyCVByte(cv,value, callback_Vbyte);
ZZ(V,cv,bit,bitvalue) // Fast read bit with expected value
EXPECT_CALLBACK DCC::verifyCVBit(cv,bit,bitvalue,callback_Vbit);
ZZ(B,cv,bit,bitvalue) // Write cv bit
EXPECT_CALLBACK DCC::writeCVBit(cv,bit,bitvalue,callback_B);
ZZ(R,cv,ignore1,ignore2) // (Deprecated) read cv value on PROG track
EXPECT_CALLBACK DCC::readCV(cv,callback_R);
ZZ(R,cv) // Read cv
EXPECT_CALLBACK DCC::verifyCVByte(cv, 0, callback_Vbyte);
ZZ(R) // Read driveable loco id (may be long, short or consist)
EXPECT_CALLBACK DCC::getLocoId(callback_Rloco);
#ifndef DISABLE_VDPY
ZZ_nodoc(@) CommandDistributor::setVirtualLCDSerial(stream);
REPLY( "<@ 0 0 \"DCC-EX v" VERSION "\">\n<@ 0 1 \"Lic GPLv3\">\n")
#endif
ZZ(-) // Clear loco state and reminder table
DCC::forgetAllLocos();
ZZ(-,loco) // remove loco state amnd reminders
DCC::forgetLoco(loco);
ZZ(F,loco,DCCFREQ,freqvalue) // Set DC frequencey for loco
CHECK(freqvalue>=0 && freqvalue<=3) DCC::setDCFreq(loco,freqvalue);
ZZ(F,loco,function,onoff) // Set loco function ON/OFF
CHECK(onoff==0 || onoff==1) DCC::setFn(loco,function,onoff);
// ZZ(M,ignore,d0,d1,d2,d3,d4,d5) // Send up to 5 byte DCC packet on MAIN track (all d values in hex)
ZZ_nodoc(M,ignore,d0,d1,d2,d3,d4,d5) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3,(byte)d4,(byte)d5}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(M,ignore,d0,d1,d2,d3,d4) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3,(byte)d4}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(M,ignore,d0,d1,d2,d3) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(M,ignore,d0,d1,d2) byte packet[]={(byte)d0,(byte)d1,(byte)d2}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(M,ignore,d0,d1) byte packet[]={(byte)d0,(byte)d1}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
// ZZ(P,ignore,d0,d1,d2,d3,d4,d5) // Send up to 5 byte DCC packet on PROG track (all d values in hex)
ZZ_nodoc(P,ignore,d0,d1,d2,d3,d4,d5) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3,(byte)d4,(byte)d5}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(P,ignore,d0,d1,d2,d3,d4) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3,(byte)d4}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(P,ignore,d0,d1,d2,d3) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(P,ignore,d0,d1,d2) byte packet[]={(byte)d0,(byte)d1,(byte)d2}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ_nodoc(P,ignore,d0,d1) byte packet[]={(byte)d0,(byte)d1}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(J,O) // List turntable IDs
REPLY("<jO")
for (auto tto=Turntable::first(); tto; tto=tto->next()) if (!tto->isHidden()) REPLY(" %d",tto->getId())
REPLY(">\n")
ZZ(J,O,id) // List turntable state
auto tto=Turntable::get(id);
if (!tto || tto->isHidden()) {REPLY("<jO %d X>\n", id) return true;}
const FSH *todesc = nullptr;
#ifdef EXRAIL_ACTIVE
todesc = RMFT2::getTurntableDescription(id);
#endif
if (todesc == nullptr) todesc = F("");
REPLY("<jO %d %d %d %d \"%S\">\n", id, tto->isEXTT(), tto->getPosition(), tto->getPositionCount(), todesc)
ZZ(J,P,id) // list turntable positions
auto tto=Turntable::get(id);
if (!tto || tto->isHidden()) {REPLY("<jP %d X>\n", id) return true;}
auto posCount = tto->getPositionCount();
if (posCount==0) {REPLY("<jP X>\n") return true;}
for (auto p = 0; p < posCount; p++) {
const FSH *tpdesc = nullptr;
#ifdef EXRAIL_ACTIVE
tpdesc = RMFT2::getTurntablePositionDescription(id, p);
#endif
if (tpdesc == NULL) tpdesc = F("");
REPLY("<jP %d %d %d \"%S\">\n", id, p, tto->getPositionAngle(p), tpdesc)
}
// Track manager
ZZ(=) // list track manager states
TrackManager::list(stream);
ZZ(=,track,MAIN) // Set track to MAIN
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN))
ZZ(=,track,MAIN_INV) // Set track to MAIN inverted polatity
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN_INV))
ZZ(=,track,MAIN_AUTO) // Set track to MAIN with auto reversing
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN_AUTO))
ZZ(=,track,PROG) // Set track to PROG
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_PROG))
ZZ(=,track,OFF) // Set track power OFF
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_NONE))
ZZ(=,track,NONE) // Set track no output
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_NONE))
ZZ(=,track,EXT) // Set track to use external sync
CHECK(TrackManager::setTrackMode(track,TRACK_MODE_EXT))
#ifdef BOOSTER_INPUT
ZZ_nodoc(=,track,BOOST) CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST))
ZZ_nodoc(=,track,BOOST_INV) CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST_INV))
ZZ_nodoc(=,track,BOOST_AUTO) CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST_AUTO))
#endif
ZZ(=,track,AUTO) // Update track to auto reverse
CHECK(TrackManager::orTrackMode(track, TRACK_MODIFIER_AUTO))
ZZ(=,track,INV) // Update track to inverse polarity
CHECK(TrackManager::orTrackMode(track, TRACK_MODIFIER_INV))
ZZ(=,track,DC,loco) // Set track to DC
CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC, loco))
ZZ(=,track,DC_INV,loco) // Set track to DC with inverted polarity
CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC_INV, loco))
ZZ(=,track,DCX,loco) // Set track to DC with inverted polarity
CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC_INV, loco))
ZZEND

File diff suppressed because it is too large Load Diff

View File

@@ -40,18 +40,21 @@ struct DCCEXParser
static void setCamParserFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
static const FSH * matchedCommandFormat;
static const FSH * checkFailedFormat;
private:
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
static const bool accessoryCommandReverse = true;
#else
static const bool accessoryCommandReverse = false;
#endif
static const int16_t MAX_BUFFER=50; // longest command sent in
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], byte * command, bool usehex);
static bool execute(byte * command, Print * stream, byte opcode, byte params, int16_t p[], RingStream * ringStream);
static bool parseT(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
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[]);
#ifndef IO_NO_HAL
static bool parseI(Print * stream, int16_t params, int16_t p[]);
#endif
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
@@ -79,7 +82,6 @@ struct DCCEXParser
static AT_COMMAND_CALLBACK atCommandCallback;
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
static void sendFlashList(Print * stream,const int16_t flashList[]);
static bool setThrottle(int16_t cab,int16_t tspeed,int16_t direction);
};

View File

@@ -1,90 +0,0 @@
/*
* © 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/>.
*/
// Count the number of arguments
#define FOR_EACH_NARG(...) FOR_EACH_NARG_HELPER(__VA_ARGS__,8,7, 6,5,4, 3, 2, 1, 0)
#define FOR_EACH_NARG_HELPER(_1, _2, _3, _4, _5, _6, _7, _8, N, ...) N
// Step 2: Force proper expansion (extra indirection to resolve `##`)
#define EXPAND(x) x
#define CONCAT(a, b) a##b
#define ZZZ(_i,_arg) \
if ( #_arg[0]<='Z' && p[_i]!=CONCAT(#_arg,_hk)) break; \
auto _arg=p[_i]; (void) _arg;
// Each ZZ terminates the previous one
#define ZPREP(op,count) return true; } if (opcode==#op[0] && params==count) for (;;) {
#define Z1(op) ZPREP(op,0)
#define Z2(op,_1) ZPREP(op,1) ZZZ(0,_1)
#define Z3(op,_1,_2) ZPREP(op,2) ZZZ(0,_1) ZZZ(1,_2)
#define Z4(op,_1,_2,_3) ZPREP(op,3) ZZZ(0,_1) ZZZ(1,_2) ZZZ(2,_3)
#define Z5(op,_1,_2,_3,_4) ZPREP(op,4) ZZZ(0,_1) ZZZ(1,_2) ZZZ(2,_3) ZZZ(3,_4)
#define Z6(op,_1,_2,_3,_4,_5) ZPREP(op,5) ZZZ(0,_1) ZZZ(1,_2) ZZZ(2,_3) ZZZ(3,_4) ZZZ(4,_5)
#define Z7(op,_1,_2,_3,_4,_5,_6) ZPREP(op,6) ZZZ(0,_1) ZZZ(1,_2) ZZZ(2,_3) ZZZ(3,_4) ZZZ(4,_5) ZZZ(5,_6)
#define Z8(op,_1,_2,_3,_4,_5,_6,_7) ZPREP(op,7) ZZZ(0,_1) ZZZ(1,_2) ZZZ(2,_3) ZZZ(3,_4) ZZZ(4,_5) ZZZ(5,_6) ZZZ(6,_7)
#define ZRIP(count) CONCAT(Z,count)
#define ZC1(op)
#define ZC2(op,_1) ZZCHK(0,_1)
#define ZC3(op,_1,_2) ZZCHK(0,_1) ZZCHK(1,_2)
#define ZC4(op,_1,_2,_3) ZZCHK(0,_1) ZZCHK(1,_2) ZZCHK(2,_3)
#define ZC5(op,_1,_2,_3,_4) ZZCHK(0,_1) ZZCHK(1,_2) ZZCHK(2,_3) ZZCHK(3,_4)
#define ZC6(op,_1,_2,_3,_4,_5) ZZCHK(0,_1) ZZCHK(1,_2) ZZCHK(2,_3) ZZCHK(3,_4) ZZCHK(4,_5)
#define ZC7(op,_1,_2,_3,_4,_5,_6) ZZCHK(0,_1) ZZCHK(1,_2) ZZCHK(2,_3) ZZCHK(3,_4) ZZCHK(4,_5) ZZCHK(5,_6)
#define ZC8(op,_1,_2,_3,_4,_5,_6,_7) ZZCHK(0,_1) ZZCHK(1,_2) ZZCHK(2,_3) ZZCHK(3,_4) ZZCHK(4,_5) ZZCHK(5,_6) ZZCHK(6,_7)
#define ZCRIP(count) CONCAT(ZC,count)
#define ZZ(...) \
ZRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__) \
DCCEXParser::matchedCommandFormat = F( #__VA_ARGS__); \
ZCRIP(FOR_EACH_NARG(__VA_ARGS__))(__VA_ARGS__)
#define ZZBEGIN if (false) {
#define ZZEND return true; } return false;
#define CHECK(x,...) if (!(x)) { DCCEXParser::checkFailedFormat=#__VA_ARGS__[0]?F(#__VA_ARGS__):F(#x); return false;}
#define REPLY(format,...) StringFormatter::send(stream,F(format), ##__VA_ARGS__);
#define EXPECT_CALLBACK CHECK(stashCallback(stream, p, ringStream))
// helper macro to hide command from documentation extractor
#define ZZ_nodoc ZZ
#define ZCHECK(_checkname,_index,_pname,_min,_max) \
if (CONCAT(#_pname,_hk) == CONCAT(#_checkname,_hk) \
&& (p[_index]<_min || p[_index]>_max)) CHECK(false,_checkname _min .. _max)
// Automatic range checks based on name of inserted parameter
#define ZZCHK(_index,_pname)\
ZCHECK(loco,_index,_pname,0,10239) \
ZCHECK(track,_index,_pname,'A','H') \
ZCHECK(cv,_index,_pname,1,255) \
ZCHECK(value,_index,_pname,0,255) \
ZCHECK(bit,_index,_pname,0,7) \
ZCHECK(bitvalue,_index,_pname,0,1)

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,7 +258,13 @@ 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:
@@ -346,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;
@@ -803,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
@@ -1059,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;

View File

@@ -77,7 +77,6 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
OPCODE_CLEAR_ANY_STASH,
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
OPCODE_NEOPIXEL,
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
@@ -94,8 +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_IFTTPOSITION
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@@ -139,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;
@@ -214,13 +212,8 @@ class LookList {
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static bool parseCommands(Print * stream, byte opcode, byte params, int16_t p[]);
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
static void streamFlags(Print* stream);
static void streamStatus(Print * stream);
static bool streamLCC(Print * stream);
static bool setFlag(VPIN id,byte onMask, byte OffMask=0);
static bool getFlag(VPIN id,byte mask);
static int16_t progtrackLocoId;

File diff suppressed because it is too large Load Diff

View File

@@ -29,27 +29,218 @@
#include "EXRAIL2.h"
#include "DCC.h"
#include "KeywordHasher.h"
#include "DCCEXParser.h"
#include "DCCEXParserMacros.h"
// This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
if (parseCommands(stream,opcode,paramCount,p)) opcode='\0'; // command was handled by parseCommands()
(void)stream; // avoid compiler warning if we don't access this parameter
switch(opcode) {
case 'D':
if (p[0]=="EXRAIL"_hk) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]=="ON"_hk || p[1]==1);
opcode=0;
}
break;
case '/': // New EXRAIL command
if (parseSlash(stream,paramCount,p)) opcode=0;
break;
case 'A': // <A address aspect>
if (paramCount!=2) break;
// Ask exrail if this is just changing the aspect on a
// predefined DCCX_SIGNAL. Because this will handle all
// the IFRED and ONRED type issues at the same time.
if (signalAspectEvent(p[0],p[1])) opcode=0; // all done
break;
case 'L':
// This entire code block is compiled out if LLC macros not used
if (!(compileFeatures & FEATURE_LCC)) return;
static int lccProgCounter=0;
static int lccEventIndex=0;
if (paramCount==0) { //<L> LCC adapter introducing self
LCCSerial=stream; // now we know where to send events we raise
opcode=0; // flag command as intercepted
// loop through all possible sent/waited events
for (int progCounter=lccProgCounter;; SKIPOP) {
byte exrailOpcode=GET_OPCODE;
switch (exrailOpcode) {
case OPCODE_ENDEXRAIL:
stream->print(F("<LR>\n")); // ready to roll
lccProgCounter=0; // allow a second pass
lccEventIndex=0;
return;
case OPCODE_LCC:
StringFormatter::send(stream,F("<LS x%h>\n"),getOperand(progCounter,0));
SKIPOP;
lccProgCounter=progCounter;
return;
case OPCODE_LCCX: // long form LCC
StringFormatter::send(stream,F("<LS x%h%h%h%h>\n"),
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return;
case OPCODE_ACON: // CBUS ACON
case OPCODE_ACOF: // CBUS ACOF
StringFormatter::send(stream,F("<LS x%c%h%h>\n"),
exrailOpcode==OPCODE_ACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1));
SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return;
// we stream the hex events we wish to listen to
// and at the same time build the event index looku.
case OPCODE_ONLCC:
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
lccEventIndex,
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return;
case OPCODE_ONACON:
case OPCODE_ONACOF:
StringFormatter::send(stream,F("<LL %d x%c%h%h>\n"),
lccEventIndex,
exrailOpcode==OPCODE_ONACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1)
);
SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return;
default:
break;
}
}
}
if (paramCount==1) { // <L eventid> LCC event arrived from adapter
int16_t eventid=p[0];
bool reject = eventid<0 || eventid>=countLCCLookup;
if (!reject) {
startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
opcode=0;
}
}
break;
case 'J': // throttle info commands
if (paramCount<1) return;
switch(p[0]) {
case "A"_hk: // <JA> returns automations/routes
if (paramCount==1) {// <JA>
StringFormatter::send(stream, F("<jA"));
routeLookup->stream(stream);
StringFormatter::send(stream, F(">\n"));
opcode=0;
return;
}
if (paramCount==2) { // <JA id>
int16_t id=p[1];
StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"),
id, getRouteType(id), getRouteDescription(id));
if (compileFeatures & FEATURE_ROUTESTATE) {
// Send any non-default button states or captions
int16_t statePos=routeLookup->findPosition(id);
if (statePos>=0) {
if (routeStateArray[statePos])
StringFormatter::send(stream,F("<jB %d %d>\n"), id, routeStateArray[statePos]);
if (routeCaptionArray[statePos])
StringFormatter::send(stream,F("<jB %d \"%S\">\n"), id,routeCaptionArray[statePos]);
}
}
opcode=0;
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
if (paramCount!=2) break;
blockEvent(p[0],p[1],opcode=='K');
opcode=0;
break;
default: // other commands pass through
break;
}
}
void RMFT2::streamStatus(Print * stream) {
REPLY("<* EXRAIL STATUS")
auto task=loopTask;
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
if ((compileFeatures & FEATURE_BLINK)
&& (task->blinkState==blink_high || task->blinkState==blink_low)) {
REPLY("\nID=%d,PC=%d,BLINK=%d",(int)(task->taskId),task->progCounter,task->blinkPin)
StringFormatter::send(stream,F("\nID=%d,PC=%d,BLINK=%d"),
(int)(task->taskId),task->progCounter,task->blinkPin
);
}
else {
REPLY("\nID=%d,PC=%d,LOCO=%d %c",(int)(task->taskId),task->progCounter,task->loco,task->invert?'I':' ')
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d %c"),
(int)(task->taskId),task->progCounter,task->loco,
task->invert?'I':' '
);
}
task=task->next;
if (task==loopTask) break;
@@ -58,9 +249,9 @@ void RMFT2::streamStatus(Print * stream) {
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above
REPLY("\nflags[%d] ",id);
if (flag & SECTION_FLAG) REPLY(" RESERVED");
if (flag & LATCH_FLAG) REPLY(" LATCHED");
StringFormatter::send(stream,F("\nflags[%d] "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
@@ -72,189 +263,117 @@ void RMFT2::streamStatus(Print * stream) {
if (slot.type==sigtypeNoMoreSignals) break; // end of signal list
if (slot.type==sigtypeContinuation) continue; // continueation of previous line
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this ids
REPLY("\n%S[%d]",
StringFormatter::send(stream,F("\n%S[%d]"),
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
slot.id)
slot.id);
}
}
REPLY(" *>\n")
}
//
bool RMFT2::streamLCC(Print * stream) {
if (!(compileFeatures & FEATURE_LCC)) return false;
// This function is called to stream the LCC commands to the LCC adapter.
LCCSerial=stream; // now we know where to send events we raise
static int lccProgCounter=0;
static int lccEventIndex=0;
for (int progCounter=lccProgCounter;; SKIPOP) {
byte exrailOpcode=GET_OPCODE;
switch (exrailOpcode) {
case OPCODE_ENDEXRAIL:
stream->print(F("<LR>\n")); // ready to roll
lccProgCounter=0; // allow a second pass
lccEventIndex=0;
return true;
case OPCODE_LCC:
StringFormatter::send(stream,F("<LS x%h>\n"),getOperand(progCounter,0));
SKIPOP;
lccProgCounter=progCounter;
return true;
case OPCODE_LCCX: // long form LCC
StringFormatter::send(stream,F("<LS x%h%h%h%h>\n"),
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return true;
case OPCODE_ACON: // CBUS ACON
case OPCODE_ACOF: // CBUS ACOF
StringFormatter::send(stream,F("<LS x%c%h%h>\n"),
exrailOpcode==OPCODE_ACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1));
SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return true;
// we stream the hex events we wish to listen to
// and at the same time build the event index looku.
case OPCODE_ONLCC:
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
lccEventIndex,
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return true;
case OPCODE_ONACON:
case OPCODE_ONACOF:
StringFormatter::send(stream,F("<LL %d x%c%h%h>\n"),
lccEventIndex,
exrailOpcode==OPCODE_ONACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1)
);
SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return true;
default:
break;
}
}
return true;
}
bool RMFT2::parseCommands(Print * stream, byte opcode, byte params, int16_t p[]) {
ZZBEGIN
ZZ(D,EXRAIL,ON) // EXRAIL diagnostics on
diag=1;
ZZ(D,EXRAIL,OFF) // EXRAIL doagnostics off
diag=0; // <D EXRAIL OFF> - turn off diagnostics
// This is not documented here because its an override of the one in DCCEXParserCommands.h
ZZ_nodoc(A,address,aspect) // Send DCC extended accessory (aspect) and syncronize any signal on this address
return signalAspectEvent(address,aspect);
ZZ(L) // LCC/CBUS adapter introducing self
CHECK(streamLCC(stream),no LCC/CBUS events)
ZZ(L,eventid) // LCC incoming event
CHECK(eventid>=0 && eventid<countLCCLookup)
startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
ZZ(J,A) // List automation ids
REPLY("<jA") routeLookup->stream(stream); REPLY(">\n")
ZZ(J,A,id) // list automation details
REPLY("<jA %d %c \"%S\">\n",id, getRouteType(id), getRouteDescription(id));
if (compileFeatures & FEATURE_ROUTESTATE) {
// Send any non-default button states or captions
int16_t statePos=routeLookup->findPosition(id);
if (statePos>=0) {
if (routeStateArray[statePos]) REPLY("<jB %d %d>\n", id, routeStateArray[statePos]);
if (routeCaptionArray[statePos]) REPLY("<jB %d \"%S\">\n", id,routeCaptionArray[statePos]);
}
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]);
}
}
ZZ(K,blockid,loco) // Loco entering Block
blockEvent(blockid,loco,true);
ZZ(k,blockid,loco) // Loco exiting block
blockEvent(blockid,loco,false);
ZZ(/) // Stream EXRAIL status
streamStatus(stream);
ZZ(/,PAUSE) // pause all tasks
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case "PAUSE"_hk: // </ PAUSE>
if (paramCount!=1) return false;
{ // pause all tasks
RMFT2 * task=loopTask;
while(task) {
task->pause();
task=task->next;
if (task==loopTask) break;
}
DCC::estopAll(); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
ZZ(/,RESUME) // Resume all tasks
pausingTask=NULL;
}
DCC::estopAll(); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
return true;
case "RESUME"_hk: // </ RESUME>
if (paramCount!=1) return false;
pausingTask=NULL;
{ // resume all tasks
RMFT2 * task=loopTask;
while(task) {
task->resume();
task=task->next;
if (task==loopTask) break;
}
}
return true;
case "START"_hk: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=routeLookup->find(route);
if (pc<0) return false;
new RMFT2(pc,cab);
}
return true;
default:
break;
}
ZZ(/,START,route) // Start a route or sequence
auto pc=routeLookup->find(route);
CHECK(pc>=0,route not found)
new RMFT2(pc,0); // no cab for route start
// check KILL ALL here, otherwise the next validation confuses ALL with a flag
if (p[0]=="KILL"_hk && p[1]=="ALL"_hk) {
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
return true;
}
ZZ(/,START,loco,route) // Start an AUTOMATION or sequence with a loco
auto pc=routeLookup->find(route);
CHECK(pc>=0, route not found)
new RMFT2(pc,loco);
ZZ(/,KILL,ALL) // Kill all exrail tasks
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
ZZ(/,KILL,taskid) // Kill specific exrail tasks
CHECK(taskid>=0 && taskid<MAX_FLAGS)
auto task=loopTask;
bool found=false;
while(task) {
if (task->taskId==taskid) {
found=true;
// all other / commands take 1 parameter
if (paramCount!=2 ) return false;
switch (p[0]) {
case "KILL"_hk: // Kill taskid|ALL
{
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
RMFT2 * task=loopTask;
while(task) {
if (task->taskId==p[1]) {
task->kill(F("KILL"));
break;
return true;
}
task=task->next;
if (task==loopTask) break;
}
CHECK(found, task not found)
ZZ(/,RESERVE,section) // Flag section as reserved
CHECK(setFlag(section,SECTION_FLAG),invalid section)
ZZ(/,FREE,section) // Free reserve on section
CHECK(setFlag(section,0,SECTION_FLAG),invalid section)
ZZ(/,LATCH,latch) // Set pin latch
CHECK(setFlag(latch,LATCH_FLAG),invalid section)
ZZ(/,UNLATCH,latch) // Removeve pin latch
CHECK(setFlag(latch,0,LATCH_FLAG),invalid section)
ZZ(/,RED,signal) // Set signal to Red
doSignal(signal,SIGNAL_RED);
ZZ(/,AMBER,signal) // set Signal to Amber
doSignal(signal,SIGNAL_AMBER);
ZZ(/,GREEN,signal) // Set signal to Green
doSignal(signal,SIGNAL_GREEN);
ZZEND
}
return false;
case "RESERVE"_hk: // force reserve a section
return setFlag(p[1],SECTION_FLAG);
case "FREE"_hk: // force free a section
return setFlag(p[1],0,SECTION_FLAG);
case "LATCH"_hk:
return setFlag(p[1], LATCH_FLAG);
case "UNLATCH"_hk:
return setFlag(p[1], 0, LATCH_FLAG);
case "RED"_hk:
doSignal(p[1],SIGNAL_RED);
return true;
case "AMBER"_hk:
doSignal(p[1],SIGNAL_AMBER);
return true;
case "GREEN"_hk:
doSignal(p[1],SIGNAL_GREEN);
return true;
default:
return false;
}
}

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,6 +218,14 @@ 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
@@ -427,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
@@ -474,7 +545,6 @@ 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
@@ -537,7 +607,9 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define PAUSE OPCODE_PAUSE,0,0,
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#ifndef DISABLE_PROG
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#endif
#define POWEROFF OPCODE_POWEROFF,0,0,
#define POWERON OPCODE_POWERON,0,0,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),

View File

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

View File

@@ -1,50 +0,0 @@
/*
* © 2024 Harald Barth
* © 2024 Paul M. Antoine
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef DO_MDNS
#define BROADCASTTIME 15 //seconds
// We do this ourselves because every library is different and/or broken...
#define HTONS(x) ((uint16_t)(((x) << 8) | (((x) >> 8) & 0xFF)))
#define HTONL(x) ( ((uint32_t)(x) << 24) | (((uint32_t)(x) << 8) & 0xFF0000) | \
(((uint32_t)(x) >> 8) & 0xFF00) | ((uint32_t)(x) >> 24) )
typedef enum _MDNSServiceProtocol_t
{
MDNSServiceTCP,
MDNSServiceUDP
} MDNSServiceProtocol_t;
class MDNS {
public:
MDNS(EthernetUDP& udp);
~MDNS();
int begin(const IPAddress& ip, char* name);
int addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto);
void run();
private:
EthernetUDP *_udp;
IPAddress _ipAddress;
char* _name;
char* _serviceName;
char* _serviceProto;
int _servicePort;
};
#endif //DO_MDNS

View File

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

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
<html>
<head>
<script lang="javascript">
function ZZ(header, body) {
document.write( `<div class="html-block">
<h2 style="font-family: monospace;">&lt;${header}&gt;</h3>
<p>${body}</p>
</div>`);
}
</script>
<script src="AutoRefManual.js" type="text/javascript"></script>
</head>
<body></body>
</html>

View File

@@ -1,177 +0,0 @@
ZZ('#','Request number of simultaneously supported locos');
ZZ('!','Emergency stop all locos');
ZZ('t loco','Request loco status');
ZZ('t loco tspeed direction','Set throttle speed(0..127) and direction (0=reverse, 1=fwd) ');
ZZ('t ignore loco tspeed direction','(Deprecated) Set throttle speed and direction');
ZZ('T','List all turnouts');
ZZ('T id','Delete turnout');
ZZ('T id X','List turnout details');
ZZ('T id T','Throw Turnout');
ZZ('T id C','Close turnout#');
ZZ('T id value','Close (value=0) ot Throw turnout');
ZZ('T id SERVO vpin closedValue thrownValue','Create Servo turnout ');
ZZ('T id VPIN vpin','Create pin turnout');
ZZ('T id DCC addr subadd','Create DCC turnout ');
ZZ('T id DCC linearAddr','Create DCC turnout');
ZZ('T id addr subadd','Create DCC turnout');
ZZ('T id vpin closedValue thrownValue','Create SERVO turnout');
ZZ('S id vpin pullup','Create Sensor');
ZZ('S id','Delete sensor');
ZZ('S','List sensors');
ZZ('J M','List stash values');
ZZ('J M stash_id','get stash value');
ZZ('J M CLEAR ALL','Clear all stash values');
ZZ('J M CLEAR stash_id','Clear given stash');
ZZ('J M stashId locoId','Set stash value');
ZZ('J M CLEAR ANY locoId','Clear all stash entries that contain locoId');
ZZ('J C','get fastclock time');
ZZ('J C mmmm nn','Set fastclock time');
ZZ('J G','FReport gauge limits ');
ZZ('J I','Report currents ');
ZZ('J L display row','Direct current displays to LCS/OLED');
ZZ('J A','List Routes');
ZZ('J R','List Roster');
ZZ('J R id','Get roster for loco');
ZZ('J T','Get turnout list ');
ZZ('J T id','Get turnout state and description');
ZZ('z vpin','Set pin. HIGH iv vpin positive, LOW if vpin negative ');
ZZ('z vpin analog profile duration','Change analog value over duration (Fade or servo move)');
ZZ('z vpin analog profile','Write analog device using profile number (Fade or servo movement)');
ZZ('z vpin analog','Write analog device value');
ZZ('I','List all turntables');
ZZ('I id','Broadcast turntable type and current position ');
ZZ('I id position','Rotate a DCC turntable');
ZZ('I id DCC home','Create DCC turntable');
ZZ('I id position activity','Rotate an EXTT turntable');
ZZ('I id EXTT vpin home','Create an EXTT turntable');
ZZ('I id ADD position value angle','Add turntable position');
ZZ('Q','List all sensors ');
ZZ('s','Command station status');
ZZ('E','STORE EPROM');
ZZ('e','CLEAR EPROM');
ZZ('Z','List Output definitions ');
ZZ('Z id pin iflag','Create Output');
ZZ('Z id active','Set output ');
ZZ('Z id','Delete output');
ZZ('D ACK ON','Enable PROG track diagnostics');
ZZ('D ACK OFF','Disable PROG track diagnostics');
ZZ('D CABS','Diagnostic display loco state table');
ZZ('D RAM','Diagnostic display free RAM');
ZZ('D CMD ON','Enable command input diagnostics');
ZZ('D CMD OFF','Disable command input diagnostics');
ZZ('D RAILCOM ON','Enable Railcom diagnostics');
ZZ('D RAILCOM OFF','DIsable Railcom diagnostics');
ZZ('D WIFI ON','Enable Wifi diagnostics');
ZZ('D WIFI OFF','Disable Wifi diagnostics');
ZZ('D ETHERNET ON','Enable Ethernet diagnostics');
ZZ('D ETHERNET OFF','Disabel Ethernet diagnostics ');
ZZ('D WIT ON','Enable Withrottle diagnostics');
ZZ('D WIT OFF','Disable Withrottle diagnostics ');
ZZ('D LCN ON','Enable LCN Diagnostics');
ZZ('D LCN OFF','Disabel LCN diagnostics');
ZZ('D WEBSOCKET ON','Enable Websocket diagnostics ');
ZZ('D WEBSOCKET OFF','Disable wensocket diagnostics ');
ZZ('D EEPROM numentries','Dump EEPROM contents');
ZZ('D ANOUT vpin position','see <z vpin position>');
ZZ('D ANOUT vpin position profile','see <z vpin position profile>');
ZZ('D SERVO vpin position','Test servo');
ZZ('D SERVO vpin position profile','Test servo');
ZZ('D ANIN vpin','Display analogue input value');
ZZ('D HAL SHOW','Show HAL devices table');
ZZ('D HAL RESET','Reset all HAL devices');
ZZ('D TT vpin steps','Test turntable');
ZZ('D TT vpin steps activity','Test turntable');
ZZ('C PROGBOOST','Configute PROG track boost');
ZZ('C RESET','Reset and restart command station');
ZZ('C SPEED28','Set all DCC speed commands as 28 step to old decoders');
ZZ('C SPEED128','Set all DCC speed commands to 128 step (default)');
ZZ('C RAILCOM ON','Enable Railcom cutout ');
ZZ('C RAILCOM OFF','Disable Railcom cutout');
ZZ('C RAILCOM DEBUG','Enable Railcom cutout for easy scope reading test');
ZZ('D ACK LIMIT value','Set ACK detection limit mA');
ZZ('D ACK MIN value MS','Set ACK minimum duration mS');
ZZ('D ACK MIN value','Set ACK minimum duration uS');
ZZ('D ACK MAX value MS','Set ACK maximum duration mS');
ZZ('D ACK MAX value','Set ACK maximum duration uS');
ZZ('D ACK RETRY value','Set ACK retry count');
ZZ('C WIFI "ssid" "password"','reconfigure stored wifi credentials ');
ZZ('o vpin','Set neopixel on(vpin>0) or off(vpin<0)');
ZZ('o vpin count','Set multiple neopixels on(vpin>0) or off(vpin<0)');
ZZ('o vpin r g b','Set neopixel colour');
ZZ('o vpin r g b count','Set multiple neopixels colour ');
ZZ('1','Power ON all tracks');
ZZ('1 MAIN','Power on MAIN track');
ZZ('1 PROG','Power on PROG track');
ZZ('1 JOIN','JOIN prog track to MAIN and power');
ZZ('1 track','Power on given track');
ZZ('0','Power off all tracks');
ZZ('0 MAIN','Power off MAIN track');
ZZ('0 PROG','Power off PROG track');
ZZ('0 track','Power off given track');
ZZ('c','Report main track currect (Deprecated)');
ZZ('a address subaddress activate','Send DCC accessory command');
ZZ('a address subaddress activate onoff','Send DCC accessory command with onoff control (TODO.. numbers) ');
ZZ('a linearaddress activate','send dcc accessory command ');
ZZ('A address value','Send DCC extended accessory (Aspect) command');
ZZ('w loco cv value','POM write cv on main track');
ZZ('r loco cv','POM read cv on main track');
ZZ('b loco cv bit value','POM write cv bit on main track');
ZZ('m LINEAR','Set Momentum algorithm to linear acceleration');
ZZ('m POWER','Set momentum algortithm to very based on difference between current speed and throttle seting');
ZZ('m loco momentum','set momentum for loco (accel and braking)');
ZZ('m loco accelerating braking','set momentum for loco');
ZZ('W cv value ignore1 ignore2','(Deprecated) Write cv value on PROG track');
ZZ('W cab','Write loco address on PROG track');
ZZ('W CONSIST cab REVERSE','Write consist address and reverse flag on PROG track ');
ZZ('W CONSIST cab','write consist address on PROG track ');
ZZ('W cv value','Write cv value on PROG track');
ZZ('W cv value bit','Write cv bit on prog track');
ZZ('V cv value','Fast read cv with expected value');
ZZ('V cv bit value','Fast read bit with expected value');
ZZ('B cv bit value','Write cv bit');
ZZ('R cv ignore1 ignore2','(Deprecated) read cv');
ZZ('R cv','Read cv');
ZZ('R','Read driveable loco id (may be long, short or consist)');
ZZ('-','Clear loco state and reminder table');
ZZ('- loco','remove loco state amnd reminders');
ZZ('F loco DCCFREQ value','Set DC frequencey for loco ');
ZZ('F loco function value','Set loco function ON/OFF');
ZZ('M ignore d0 d1 d2 d3 d4 d5','Send up to 5 byte DCC packet on MAIN track (all d values in hex)');
ZZ('P ignore d0 d1 d2 d3 d4 d5','Send up to 5 byte DCC packet on PROG track (all d values in hex)');
ZZ('J O','List turntable IDs');
ZZ('J O id','List turntable state');
ZZ('J P id','list turntable positions');
ZZ('=','list track manager states');
ZZ('= track MAIN','Set track to MAIN');
ZZ('= track MAIN_INV','Set track to MAIN inverted polatity');
ZZ('= track MAIN_AUTO','Set track to MAIN with auto reversing');
ZZ('= track PROG','Set track to PROG');
ZZ('= track OFF','Set track power OFF');
ZZ('= track NONE','Set track no output');
ZZ('= track EXT','Set track to use external sync');
ZZ('= track AUTO','Update track to auto reverse');
ZZ('= track INV','Update track to inverse polarity');
ZZ('= track DC locoid','Set track to DC');
ZZ('= track DC_INV locoid','Set track to DC with inverted polarity');
ZZ('= track DCX locoid','Set track to DC with inverted polarity');ZZ('D EXRAIL ON','EXRAIL diagnostics on');
ZZ('D EXRAIL OFF','EXRAIL doagnostics off');
ZZ('L','LCC/CBUS adapter introducing self');
ZZ('L eventid','LCC incoming event ');
ZZ('J A','List automation ids');
ZZ('J A id','list automation details');
ZZ('K blockid loco','Loco entering Block');
ZZ('k blockid loco','Loco exiting block');
ZZ('/','Stream EXRAIL status');
ZZ('/ PAUSE','pause all tasks ');
ZZ('/ RESUME','Resume all tasks');
ZZ('/ START route','Start a route or sequence');
ZZ('/ START loco route','Start an AUTOMATION or sequence with a loco ');
ZZ('/ KILL ALL','Kill all exrail tasks');
ZZ('/ KILL taskid','Kill specific exrail tasks ');
ZZ('/ RESERVE section','Flag section as reserved');
ZZ('/ FREE section','Free reserve on section');
ZZ('/ LATCH latch','Set pin latch');
ZZ('/ UNLATCH latch','Removeve pin latch');
ZZ('/ RED signal','Set signal to Red ');
ZZ('/ AMBER signal','set Signal to Amber');
ZZ('/ GREEN signal','Set signal to Green ');

View File

@@ -1,15 +0,0 @@
#!/usr/bin/awk -f
{
# Match the pattern ZZ(something) // comments
if ($0 ~ /ZZ\([^)]*\)\s*\/\/.*/) {
# Extract "something" and "comments"
match($0, /ZZ\(([^)]*)\)\s*\/\/\s*(.*)/, arr);
something = arr[1];
comments = arr[2];
# Replace commas in "something" with spaces
gsub(/,/, " ", something);
# Print in the new format as a JS call
printf "ZZ('%s','%s');\n", something, comments;
}
}

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

@@ -1,101 +0,0 @@
/*
* © 2024 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
//
// Rewrite of the STM32lwipopts.h file from STM
// To be copied into where lwipopts_default.h resides
// typically into STM32Ethernet/src/STM32lwipopts.h
// or STM32Ethernet\src\STM32lwipopts.h
// search for `lwipopts_default.h` and copy this file into the
// same directory but name it STM32lwipopts.h
//
#ifndef __STM32LWIPOPTS_H__
#define __STM32LWIPOPTS_H__
// include this here and then override things we do differnet
#include "lwipopts_default.h"
// we can not include our "defines.h" here
// so we need to duplicate that define
#define MAX_NUM_TCP_CLIENTS_HERE 9
#ifdef MAX_NUM_TCP_CLIENTS
#if MAX_NUM_TCP_CLIENTS != MAX_NUM_TCP_CLIENTS_HERE
#error MAX_NUM_TCP_CLIENTS and MAX_NUM_TCP_CLIENTS_HERE must be same
#endif
#else
#define MAX_NUM_TCP_CLIENTS MAX_NUM_TCP_CLIENTS_HERE
#endif
// increase ARP cache
#undef MEMP_NUM_APR_QUEUE
#define MEMP_NUM_ARP_QUEUE MAX_NUM_TCP_CLIENTS+3 // one for each client (all on different HW) and a few extra
// Example for debug
//#define LWIP_DEBUG 1
//#define TCP_DEBUG LWIP_DBG_ON
// NOT STRICT NECESSARY ANY MORE BUT CAN BE USED TO SAVE RAM
#undef MEM_LIBC_MALLOC
#define MEM_LIBC_MALLOC 1 // use the same malloc as for everything else
#undef MEMP_MEM_MALLOC
#define MEMP_MEM_MALLOC 1 // uses malloc which means no pools which means slower but not mean 32KB up front
#undef MEMP_NUM_TCP_PCB
#define MEMP_NUM_TCP_PCB MAX_NUM_TCP_CLIENTS+1 // one extra so we can reject number N+1 from our code
#define MEMP_NUM_TCP_PCB_LISTEN 6
#undef MEMP_NUM_TCP_SEG
#define MEMP_NUM_TCP_SEG MAX_NUM_TCP_CLIENTS
#undef MEMP_NUM_SYS_TIMEOUT
#define MEMP_NUM_SYS_TIMEOUT MAX_NUM_TCP_CLIENTS+2
#undef PBUF_POOL_SIZE
#define PBUF_POOL_SIZE MAX_NUM_TCP_CLIENTS
#undef LWIO_ICMP
#define LWIP_ICMP 1
#undef LWIP_RAW
#define LWIP_RAW 1 /* PING changed to 1 */
#undef DEFAULT_RAW_RECVMBOX_SIZE
#define DEFAULT_RAW_RECVMBOX_SIZE 3 /* for ICMP PING */
#undef LWIP_DHCP
#define LWIP_DHCP 1
#undef LWIP_UDP
#define LWIP_UDP 1
/*
The STM32F4x7 allows computing and verifying the IP, UDP, TCP and ICMP checksums by hardware:
- To use this feature let the following define uncommented.
- To disable it and process by CPU comment the the checksum.
*/
#if CHECKSUM_GEN_TCP == 1
#error On STM32 TCP checksum should be in HW
#endif
#undef LWIP_IGMP
#define LWIP_IGMP 1
//#define SO_REUSE 1
//#define SO_REUSE_RXTOALL 1
#endif /* __STM32LWIPOPTS_H__ */

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

@@ -123,7 +123,6 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
case 's': stream->print(va_arg(args, char*)); break;
case 'e': printEscapes(stream,va_arg(args, char*)); break;
case 'E': printEscapes(stream,(const FSH*)va_arg(args, char*)); break;
case '<': printCmdFormat(stream,(const FSH*)va_arg(args, char*)); break;
case 'S':
{
const FSH* flash= (const FSH*)va_arg(args, char*);
@@ -203,27 +202,15 @@ void StringFormatter::printEscapes(Print * stream,char * input) {
void StringFormatter::printEscapes(Print * stream, const FSH * input) {
if (!stream) return;
char* flash=(char*)input;
for(int i=0; ; ++i) {
char c=GETFLASH(flash+i);
printEscape(stream,c);
if (c=='\0') return;
}
if (!stream) return;
char* flash=(char*)input;
for(int i=0; ; ++i) {
char c=GETFLASH(flash+i);
printEscape(stream,c);
if (c=='\0') return;
}
}
void StringFormatter::printCmdFormat(Print * stream, const FSH * input) {
if (!stream) return;
char* flash=(char*)input;
for(int i=0; ; ++i) {
char c=GETFLASH(flash+i);
if (c=='\0') return;
if (c==',') c=' ';
stream->write(c);
}
}
void StringFormatter::printEscape( char c) {
printEscape(&USB_SERIAL,c);
}

View File

@@ -43,7 +43,6 @@ class StringFormatter
static void printEscapes(Print * serial,char * input);
static void printEscapes(Print * serial,const FSH* input);
static void printCmdFormat(Print * serial,const FSH* input);
static void printEscape(Print * serial, char c);
// DIAG support

View File

@@ -197,13 +197,7 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
}
}
bool TrackManager::orTrackMode(byte trackToSet, TRACK_MODE mode) {
if (trackToSet>='A' && trackToSet<='H') trackToSet-='A';
return setTrackMode(trackToSet, track[trackToSet]->getMode() | mode);
}
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>='A' && trackToSet<='H') trackToSet-='A';
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
// Remember track mode we came from for later
@@ -391,12 +385,63 @@ void TrackManager::applyDCSpeed(byte t) {
DCC::getThrottleFrequency(trackDCAddr[t]));
}
bool TrackManager::list(Print *stream)
bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
{
if (params==0) { // <=> List track assignments
FOR_EACH_TRACK(t)
streamTrackState(stream,t);
return true;
return true;
}
p[0]-="A"_hk; // convert A... to 0....
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
return false;
if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
return setTrackMode(p[0],TRACK_MODE_MAIN);
if (params==2 && p[1]=="MAIN_INV"_hk) // <= id MAIN_INV>
return setTrackMode(p[0],TRACK_MODE_MAIN_INV);
if (params==2 && p[1]=="MAIN_AUTO"_hk) // <= id MAIN_AUTO>
return setTrackMode(p[0],TRACK_MODE_MAIN_AUTO);
#ifndef DISABLE_PROG
if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
return setTrackMode(p[0],TRACK_MODE_PROG);
#endif
if (params==2 && (p[1]=="OFF"_hk || p[1]=="NONE"_hk)) // <= id OFF> <= id NONE>
return setTrackMode(p[0],TRACK_MODE_NONE);
if (params==2 && p[1]=="EXT"_hk) // <= id EXT>
return setTrackMode(p[0],TRACK_MODE_EXT);
#ifdef BOOSTER_INPUT
if (TRACK_MODE_BOOST != 0 && // compile time optimization
params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
return setTrackMode(p[0],TRACK_MODE_BOOST);
if (TRACK_MODE_BOOST_INV != 0 && // compile time optimization
params==2 && p[1]=="BOOST_INV"_hk) // <= id BOOST_INV>
return setTrackMode(p[0],TRACK_MODE_BOOST_INV);
if (TRACK_MODE_BOOST_AUTO != 0 && // compile time optimization
params==2 && p[1]=="BOOST_AUTO"_hk) // <= id BOOST_AUTO>
return setTrackMode(p[0],TRACK_MODE_BOOST_AUTO);
#endif
if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_AUTO);
if (params==2 && p[1]=="INV"_hk) // <= id INV>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_INV);
if (params==3 && p[1]=="DC"_hk && p[2]>0) // <= id DC cab>
return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
if (params==3 && (p[1]=="DC_INV"_hk || // <= id DC_INV cab>
p[1]=="DCX"_hk) && p[2]>0) // <= id DCX cab>
return setTrackMode(p[0],TRACK_MODE_DC_INV,p[2]);
return false;
}
const FSH* TrackManager::getModeName(TRACK_MODE tm) {

View File

@@ -72,8 +72,7 @@ class TrackManager {
static const int16_t MAX_TRACKS=8;
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
static bool orTrackMode(byte track, TRACK_MODE mode);
static bool list(Print * stream);
static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
static void loop();
static POWERMODE getMainPower();
static POWERMODE getProgPower();

View File

@@ -137,16 +137,6 @@ The configuration file for DCC-EX Command Station
//
//#define ENABLE_ETHERNET true
/////////////////////////////////////////////////////////////////////////////////////
//
// MAX_NUM_TCP_CLIENTS: If you on STM32 Ethernet (and only there) want more than
// 9 (*) TCP clients, change this number to for example 20 here **AND** in
// STM32lwiopts.h and follow the instructions in STM32lwiopts.h
//
// (*) It would be 10 if there would not be a bug in LwIP by STM32duino.
//
//#define MAX_NUM_TCP_CLIENTS 20
/////////////////////////////////////////////////////////////////////////////////////
//

View File

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

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

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

View File

@@ -3,18 +3,7 @@
#include "StringFormatter.h"
#define VERSION "5.5.21"
// 5.5.21 - Backed out the broken merge with frequency change and
// 5.5.20 - EXRAIL SET/RESET assert fix
// 5.5.19 - Railcom change to use RailcomCollector device
// 5.5.18 - New STASH internals
// - EXRAIL IFSTASH/CLEAR_ANY_STASH
// - <JM CLEAR ANY id> to clear any stash with loco id
// - See Release_Notes/Stash.md
// 5.5.17 - Extensive new compile time checking in exrail scripts (duplicate sequences etc), no function change
// 5.5.16 - DOXYGEN comments in EXRAIL2MacroReset.h
// 5.5.15 - Support for F429ZI/F329ZI
// - Own mDNS support for (wired) Ethernet
#define VERSION "5.5.14"
// 5.5.14 - DCC Non-blocking packet queue with priority
// 5.5.13 - Update STM32duino core to v19.0.0. for updated PeripheralPins.c in preparation for F429/439ZI Ethernet support
// 5.5.12 - Websocket support (wifi only)