1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-04-02 19:50:13 +02:00
CommandStation-EX/DCCEXCommands.h
2025-03-17 02:42:00 +00:00

477 lines
22 KiB
C

/*
* © 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.
The REPLY( format, ...) macro sends a formatted string to the stream.
These macros are included into the DCCEXParser::execute function so
strea, ringStream and other DCCEXParser variables are available in context. */
ZZBEGIN
ZZ(#) REPLY( "<# %d>\n", MAX_LOCOS)
ZZ(t,cab) CHECK(cab>0)
CommandDistributor::broadcastLoco(DCC::lookupSpeedTable(cab,false));
ZZ(t,cab,tspeed,direction) CHECK(setThrottle(cab,tspeed,direction))
ZZ(t,ignore,cab,tspeed,direction) CHECK(setThrottle(cab,tspeed,direction))
// todo ZZ(f,cab,byte1) CHECK(handleFunctionGroup(cab,byte1))
// todo ZZ(f,cab,byte1,byte2) CHECK(handleFunctionGroup(cab,byte1,byte2))
ZZ(T) Turnout::printAll(stream); // will <X> if none found
ZZ(T,id) CHECK(Turnout::remove(id))
ZZ(T,id,X) auto tt=Turnout::get(id); CHECK(tt) tt->print(stream);
ZZ(T,id,T) Turnout::setClosed(id, false);
ZZ(T,id,C) Turnout::setClosed(id, true);
ZZ(T,id,value) Turnout::setClosed(id, value==0);
ZZ(T,id,SERVO,pin,low,high) CHECK(ServoTurnout::create(id, (VPIN)pin, (uint16_t)low, (uint16_t)high, 1))
ZZ(T,id,VPIN,pin) CHECK(VpinTurnout::create(id, pin))
ZZ(T,id,DCC,addr,subadd) CHECK(DCCTurnout::create(id, addr, subadd))
ZZ(T,id,DCC,nn) CHECK(DCCTurnout::create(id, (nn-1)/4+1, (nn-1)%4))
ZZ(T,id,addr,subadd) CHECK(DCCTurnout::create(id, addr, subadd))
ZZ(T,id,pin,low,high) CHECK(ServoTurnout::create(id, (VPIN)pin,low,high,1))
ZZ(S,id,pin,pullup) CHECK(Sensor::create(id,pin,pullup))
ZZ(S,id) CHECK(Sensor::remove(id))
ZZ(S) 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) Stash::list(stream);
ZZ(J,M,stash_id) Stash::list(stream, stash_id);
ZZ(J,M,CLEAR,ALL) Stash::clearAll();
ZZ(J,M,CLEAR,stash_id) Stash::clear(stash_id);
ZZ(J,M,stashId,locoId) Stash::set(stashId,locoId);
ZZ(J,M,CLEAR,ANY,locoId) Stash::clearAny(locoId);
ZZ(J,C) REPLY("<jC %d>\n", CommandDistributor::retClockTime())
ZZ(J,C,mmmm,nn) CommandDistributor::setClockTime(mmmm, nn, 1);
ZZ(J,G) TrackManager::reportGauges(stream); // <g limit...limit>
ZZ(J,I) TrackManager::reportCurrent(stream); // <g limit...limit>
ZZ(J,L,display,row) TrackManager::reportCurrentLCD(display,row); // Track power status
ZZ(J,A) REPLY( "<jA>\n") // <JA> intercepted by EXRAIL// <JA> returns automations/routes
#ifdef EXRAIL_ACTIVE
ZZ(J,R) REPLY("<jR")
SENDFLASHLIST(stream,RMFT2::rosterIdList)
REPLY(">\n");
ZZ(J,R,id) 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)
#else
ZZ(J,R) REPLY("<jR>\n") // <JR> returns empty roster list
#endif
ZZ(J,T) // <JT> returns 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) auto t=Turnout::get(id);
if (!t || t->isHidden()) REPLY("<jT %d X>\n",id)
else {
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) // <z vpin | -vpin>
if (vpin>0) IODevice::write(vpin,HIGH);
else IODevice::write(-vpin,LOW);
ZZ(z,vpin,analog,profile,duration) IODevice::writeAnalogue(vpin,analog,profile,duration);
ZZ(z,vpin,analog,profile) IODevice::writeAnalogue(vpin,analog,profile,0);
ZZ(z,vpin,analog) 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) return Turntable::printAll(stream);
ZZ(I,id) // <I id> broadcast type and current position
auto tto = Turntable::get(id);
CHECK(tto)
REPLY("<I %d %d>\n", tto->isEXTT(), tto->getPosition())
ZZ(I,id,position) // <I id position> - rotate a DCC turntable
auto tto = Turntable::get(id);
CHECK(tto)
CHECK(!tto->isEXTT())
CHECK(tto->setPosition(id,position))
ZZ(I,id,DCC,home)
auto tto = Turntable::get(id);
CHECK(tto)
CHECK(home >=0 && home <= 3600)
CHECK(DCCTurntable::create(id))
tto = Turntable::get(id);
CHECK(tto)
tto->addPosition(0, 0, home);
REPLY("<I>\n")
ZZ(I,id,position,activity)
auto tto = Turntable::get(id);
CHECK(tto)
CHECK(tto->isEXTT())
CHECK(tto->setPosition(id, position,activity))
ZZ(I,id,EXTT,vpin,home) // <I id EXTT vpin home> create an EXTT turntable
auto tto = Turntable::get(id);
CHECK(!tto && 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) // <I id ADD position value angle> add a position
auto tto = Turntable::get(id);
// tto must exist, no more than 48 positions, angle 0 - 3600
CHECK(tto && position <= 48 && angle >=0 && angle <= 3600)
tto->addPosition(id,value,angle);
REPLY("<I>\n")
ZZ(Q) Sensor::printAll(stream);
ZZ(s) // STATUS <s>
REPLY("<iDCC-EX V-%S / %S / %S G-%S>\n", F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA))
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 <E>
EEStore::store();
REPLY("<e %d %d %d>\n", EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs)
ZZ(e) // CLEAR EPROM <e>
EEStore::clear();
REPLY("<O>\n")
#endif
ZZ(Z,id,active) auto o = Output::get(id);
CHECK(o)
o->activate(active);
REPLY("<Y %d %d>\n", id,active)
ZZ(Z,id,pin,iflag) // <Z ID PIN IFLAG>
CHECK(id > 0 && iflag >= 0 && iflag <= 7 )
CHECK(Output::create(id,pin,iflag, 1))
REPLY("<O>\n")
ZZ(Z,id) CHECK(Output::remove(id))
REPLY("<O>\n")
ZZ(Z) // <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)
ZZ(D,ACK,ON) Diag::ACK = true;
ZZ(D,ACK,OFF) Diag::ACK = false;
ZZ(D,CABS) DCC::displayCabList(stream);
ZZ(D,RAM) DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
ZZ(D,CMD,ON) Diag::CMD = true;
ZZ(D,CMD,OFF) Diag::CMD = false;
ZZ(D,RAILCOM,ON) Diag::RAILCOM = true;
ZZ(D,RAILCOM,OFF) Diag::RAILCOM = false;
ZZ(D,WIFI,ON) Diag::WIFI = true;
ZZ(D,WIFI,OFF) Diag::WIFI = false;
ZZ(D,ETHERNET,ON) Diag::ETHERNET = true;
ZZ(D,ETHERNET,OFF) Diag::ETHERNET = false;
ZZ(D,WIT,ON) Diag::WITHROTTLE = true;
ZZ(D,WIT,OFF) Diag::WITHROTTLE = false;
ZZ(D,LCN,ON) Diag::LCN = true;
ZZ(D,LCN,OFF) Diag::LCN = false;
ZZ(D,WEBSOCKET,ON) Diag::WEBSOCKET = true;
ZZ(D,WEBSOCKET,OFF) Diag::WEBSOCKET = false;
#ifndef DISABLE_EEPROM
ZZ(D,EEPROM,numentries) EEStore::dump(numentries);
#endif
ZZ(D,ANOUT,vpin,position) IODevice::writeAnalogue(vpin,position,0);
ZZ(D,ANOUT,vpin,position,profile) IODevice::writeAnalogue(vpin,position,profile);
ZZ(D,SERVO,vpin,position) IODevice::writeAnalogue(vpin,position,0);
ZZ(D,SERVO,vpin,position,profile) IODevice::writeAnalogue(vpin,position,profile);
ZZ(D,ANIN,vpin)// <D ANIN vpin> Display analogue input value
DIAG(F("VPIN=%u value=%d"), vpin, IODevice::readAnalogue(vpin));
ZZ(D,HAL,SHOW) IODevice::DumpAll();
ZZ(D,HAL,RESET) IODevice::reset();
ZZ(D,TT,vpin,steps) IODevice::writeAnalogue(vpin,steps,0);
ZZ(D,TT,vpin,steps,activity) IODevice::writeAnalogue(vpin,steps,activity);
ZZ(C,PROGBOOST) TrackManager::progTrackBoosted=true;
ZZ(C,RESET) DCCTimer::reset();
ZZ(C,SPEED28) DCC::setGlobalSpeedsteps(28); DIAG(F("28 Speedsteps"));
ZZ(C,SPEED128) DCC::setGlobalSpeedsteps(128); DIAG(F("128 Speedsteps"));
ZZ(C,RAILCOM,ON) DIAG(F("Railcom %S"),DCCWaveform::setRailcom(true,false)?F("ON"):F("OFF"));
ZZ(C,RAILCOM,OFF) DIAG(F("Railcom OFF")); DCCWaveform::setRailcom(false,false);
ZZ(C,RAILCOM,DEBUG) DIAG(F("Railcom %S"), DCCWaveform::setRailcom(true,true)?F("ON"):F("OFF"));
#ifndef DISABLE_PROG
ZZ(D,ACK,LIMIT,value) DCCACK::setAckLimit(value); LCD(1, F("Ack Limit=%dmA"), value);
ZZ(D,ACK,MIN,value,MS) DCCACK::setMinAckPulseDuration(value*1000L); LCD(1, F("Ack Min=%dmS"), value);
ZZ(D,ACK,MIN,value) DCCACK::setMinAckPulseDuration(value); LCD(1, F("Ack Min=%duS"), value);
ZZ(D,ACK,MAX,value,MS) DCCACK::setMaxAckPulseDuration(value*1000L); LCD(1, F("Ack Max=%dmS"), value);
ZZ(D,ACK,MAX,value) DCCACK::setMaxAckPulseDuration(value); LCD(1, F("Ack Max=%duS"), value);
ZZ(D,ACK,RETRY,value) DCCACK::setAckRetry(value); LCD(1, F("Ack Retry=%d"), value);
#endif
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
ZZ(C,WIFI,marker1,ssid,marker2,password)
// <C WIFI SSID PASSWORD>
CHECK(marker1==0x7777 && marker2==0x7777)
WifiESP::setup((const char*)(com + ssid), (const char*)(com + password), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif
ZZ(o,vpin) IODevice::write(abs(vpin),vpin>0);
ZZ(o,vpin,count) IODevice::writeRange(abs(vpin),vpin>0,count);
ZZ(o,vpin,r,g,b) CHECK(r>-0 && r<=0xff) CHECK(g>-0 && g<=0xff) CHECK(b>-0 && b<=0xff)
IODevice::writeAnalogueRange(abs(vpin),vpin>0,r<<8 | g,b,1);
ZZ(o,vpin,r,g,b,count) CHECK(r>-0 && r<=0xff) CHECK(g>-0 && g<=0xff) CHECK(b>-0 && b<=0xff)
IODevice::writeAnalogueRange(abs(vpin),vpin>0,r<<8 | g,b,count);
ZZ(1) TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);
ZZ(1,MAIN) TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
#ifndef DISABLE_PROG
ZZ(1,PROG) TrackManager::setJoin(false); TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
ZZ(1,JOIN) TrackManager::setJoin(true); TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
#endif
ZZ(1,letter) CHECK(letter>='A' && letter<='H') TrackManager::setTrackPower(POWERMODE::ON, (byte)letter-'A');
ZZ(0) TrackManager::setJoin(false); TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);
ZZ(0,MAIN)TrackManager::setJoin(false); TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
ZZ(0,PROG) 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,letter) CHECK(letter>='A' && letter <='H')
TrackManager::setJoin(false);
TrackManager::setTrackPower(POWERMODE::OFF, (byte)letter-'a');
ZZ(!) DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
ZZ(c) TrackManager::reportObsoleteCurrent(stream);
ZZ(a,address,subaddress,activate) DCC::setAccessory(address, subaddress,activate ^ accessoryCommandReverse);
ZZ(a,address,subaddress,activate,onoff) CHECK(onoff>=0 && onoff<-2)
DCC::setAccessory(address, subaddress,activate ^ accessoryCommandReverse ,onoff);
ZZ(a,linearaddress,activate)
DCC::setAccessory((linearaddress - 1) / 4 + 1,(linearaddress - 1) % 4 ,activate ^ accessoryCommandReverse);
ZZ(A,address,value) DCC::setExtendedAccessory(address,value);
ZZ(w,cab,cv,value) DCC::writeCVByteMain(cab,cv,value);
ZZ(r,cab,cv)
CHECK(DCCWaveform::isRailcom())
EXPECT_CALLBACK
DCC::readCVByteMain(cab,cv,callback_r);
ZZ(b,cab,cv,bit,value) DCC::writeCVBitMain(cab,cv,bit,value);
ZZ(m,LINEAR) DCC::linearAcceleration=true;
ZZ(m,POWER) DCC::linearAcceleration=false;
ZZ(m,cab,momentum) CHECK(DCC::setMomentum(cab,momentum,momentum))
ZZ(m,cab,momentum,braking) CHECK(DCC::setMomentum(cab,momentum,braking))
ZZ(W,cv,value,ignore1,ignore2) EXPECT_CALLBACK DCC::writeCVByte(cv,value, callback_W4);
ZZ(W,cab) EXPECT_CALLBACK DCC::setLocoId(cab,callback_Wloco);
ZZ(W,CONSIST,cab,REVERSE) EXPECT_CALLBACK DCC::setConsistId(cab,true,callback_Wconsist);
ZZ(W,CONSIST,cab) EXPECT_CALLBACK DCC::setConsistId(cab,false,callback_Wconsist);
ZZ(W,cv,value) EXPECT_CALLBACK DCC::writeCVByte(cv,value, callback_W);
ZZ(W,cv,value,bit) EXPECT_CALLBACK DCC::writeCVBit(cv,value,bit,callback_W);
ZZ(V,cv,value) EXPECT_CALLBACK DCC::verifyCVByte(cv,value, callback_Vbyte);
ZZ(V,cv,bit,value) EXPECT_CALLBACK DCC::verifyCVBit(cv,bit,value,callback_Vbit);
ZZ(B,cv,bit,value) EXPECT_CALLBACK DCC::writeCVBit(cv,bit,value,callback_B);
ZZ(R,cv,ignore1,ignore2) EXPECT_CALLBACK DCC::readCV(cv,callback_R);
ZZ(R,cv) EXPECT_CALLBACK DCC::verifyCVByte(cv, 0, callback_Vbyte);
ZZ(R) EXPECT_CALLBACK DCC::getLocoId(callback_Rloco);
#ifndef DISABLE_VDPY
ZZ(@) CommandDistributor::setVirtualLCDSerial(stream);
REPLY( "<@ 0 0 \"DCC-EX v" VERSION "\">\n<@ 0 1 \"Lic GPLv3\">\n")
#endif
ZZ(-) DCC::forgetAllLocos();
ZZ(-,cab) DCC::forgetLoco(cab);
ZZ(F,cab,DCCFREQ,value) CHECK(value>=0 && value<=3) DCC::setDCFreq(cab,value);
ZZ(F,cab,function,value) CHECK(value==0 || value==1) DCC::setFn(cab,function,value);
ZZ(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(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(M,ignore,d0,d1,d2,d3) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(M,ignore,d0,d1,d2) byte packet[]={(byte)d0,(byte)d1,(byte)d2}; DCCWaveform::mainTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(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) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3,(byte)d4,(byte)d5}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(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(P,ignore,d0,d1,d2,d3) byte packet[]={(byte)d0,(byte)d1,(byte)d2,(byte)d3}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(P,ignore,d0,d1,d2) byte packet[]={(byte)d0,(byte)d1,(byte)d2}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(P,ignore,d0,d1) byte packet[]={(byte)d0,(byte)d1}; DCCWaveform::progTrack.schedulePacket(packet,sizeof(packet),3);
ZZ(J,O) REPLY("<jO")
for (auto tto=Turntable::first(); tto; tto=tto->next()) if (!tto->isHidden()) REPLY(" %d",tto->getId())
REPLY(">\n")
ZZ(J,O,id) 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) 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(=) TrackManager::list(stream);
ZZ(=,track,MAIN) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN))
ZZ(=,track,MAIN_INV) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN_INV))
ZZ(=,track,MAIN_AUTO) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_MAIN_AUTO))
ZZ(=,track,PROG) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_PROG))
ZZ(=,track,OFF) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_NONE))
ZZ(=,track,NONE) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_NONE))
ZZ(=,track,EXT) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_EXT))
#ifdef BOOSTER_INPUT
ZZ(=,track,BOOST) CHECK(TRACK_MODE_BOOST) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST))
ZZ(=,track,BOOST_INV) CHECK(TRACK_MODE_BOOST_INV) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST_INV))
ZZ(=,track,BOOST_AUTO) CHECK(TRACK_MODE_BOOST_AUTO) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track,TRACK_MODE_BOOST_AUTO))
#endif
ZZ(=,track,AUTO) CHECK(track>='A' && track<='H') CHECK(TrackManager::orTrackMode(track, TRACK_MODIFIER_AUTO))
ZZ(=,track,INV) CHECK(track>='A' && track<='H') CHECK(TrackManager::orTrackMode(track, TRACK_MODIFIER_INV))
ZZ(=,track,DC,locoid) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC, locoid))
ZZ(=,track,DC_INV,locoid) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC_INV, locoid))
ZZ(=,track,DCX,locoid) CHECK(track>='A' && track<='H') CHECK(TrackManager::setTrackMode(track, TRACK_MODE_DC_INV, locoid))
ZZEND