mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-04 13:05:24 +02:00
Compare commits
37 Commits
v5.5.28-De
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
48908c99a8 | ||
|
b8e1583b71 | ||
|
3b15491608 | ||
|
23073231ee | ||
|
3fc3c2329a | ||
|
c79e01056e | ||
|
946a784661 | ||
|
90ee8ea7d8 | ||
|
b63703a365 | ||
|
97f50910f6 | ||
|
08b8e43cd0 | ||
|
8fcc2b0083 | ||
|
978671a688 | ||
|
0effaaba87 | ||
|
8ac61b88d4 | ||
|
8aabede3ee | ||
|
839ea582a4 | ||
|
795d0edad9 | ||
|
f48f755608 | ||
|
40c30822f1 | ||
|
c7c9159a99 | ||
|
4df7df7be5 | ||
|
64f470a130 | ||
|
4125e73318 | ||
|
911bbd63be | ||
|
393b0bbd16 | ||
|
d9bd1e75f2 | ||
|
d1daf41f12 | ||
|
6bfa7028c4 | ||
|
a5d1d04882 | ||
|
bd6e426499 | ||
|
09bae44cc0 | ||
|
9f3354c687 | ||
|
fb495985f4 | ||
|
f868604ca9 | ||
|
41168a9dd8 | ||
|
0154e7fd78 |
36
.github/workflows/docs.yml
vendored
36
.github/workflows/docs.yml
vendored
@ -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
3
.gitignore
vendored
@ -15,6 +15,3 @@ my*.h
|
||||
compile_commands.json
|
||||
newcode.txt.old
|
||||
UserAddin.txt
|
||||
_build
|
||||
venv
|
||||
.DS_Store
|
||||
|
104
CamParser.cpp
104
CamParser.cpp
@ -1,57 +1,31 @@
|
||||
/*
|
||||
* © 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"
|
||||
//sensorCAM parser.cpp version 3.03 Sep 2024
|
||||
#include "CamParser.h"
|
||||
#include "FSH.h"
|
||||
#include "IO_EXSensorCAM.h"
|
||||
|
||||
#ifndef SENSORCAM_VPIN //define CAM vpin (700?) in config.h
|
||||
#define SENSORCAM_VPIN 0
|
||||
#endif
|
||||
#define CAM_VPIN SENSORCAM_VPIN
|
||||
#ifndef SENSORCAM2_VPIN
|
||||
#define SENSORCAM2_VPIN CAM_VPIN
|
||||
#endif
|
||||
#ifndef SENSORCAM3_VPIN
|
||||
#define SENSORCAM3_VPIN 0
|
||||
#endif
|
||||
const int CAMVPINS[] = {CAM_VPIN,SENSORCAM_VPIN,SENSORCAM2_VPIN,SENSORCAM3_VPIN};
|
||||
const int16_t ver=30177;
|
||||
const int16_t ve =2899;
|
||||
|
||||
VPIN EXSensorCAM::CAMBaseVpin = CAM_VPIN;
|
||||
|
||||
// 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]);
|
||||
|
||||
void CamParser::parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
|
||||
if (opcode!='N') return; // this is not for us.
|
||||
if (parseN(stream,paramCount,p)) opcode=0; // we have consumed this
|
||||
// If we fail, the caller will <X> the <N command.
|
||||
}
|
||||
|
||||
bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
||||
(void)stream; // probably unused parameter
|
||||
if (CAMBaseVpin==0) CAMBaseVpin=CAMVPINS[0]; // default to CAM 1.
|
||||
VPIN vpin=CAMBaseVpin; //use current CAM selection
|
||||
(void)stream; // probably unused parameter
|
||||
VPIN vpin=EXSensorCAM::CAMBaseVpin; //use current CAM selection
|
||||
|
||||
if (paramCount==0) {
|
||||
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]);
|
||||
}
|
||||
DIAG(F("vpin:%d EXSensorCAMs defined at Vpins #1@ %d #2@ %d #3@ %d"),vpin,CAMVPINS[1],CAMVPINS[2],CAMVPINS[3]);
|
||||
return true;
|
||||
}
|
||||
uint8_t camop=p[0]; // cam oprerator
|
||||
@ -59,45 +33,44 @@ bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
||||
int16_t param3=9999; // =0 could invoke parameter changes. & -1 gives later errors
|
||||
|
||||
if(camop=='C'){
|
||||
if(p[1]>=100) CAMBaseVpin=p[1];
|
||||
if(p[1]<=vpcount && p[1]>0) CAMBaseVpin=CAMVPINS[p[1]-1];
|
||||
DIAG(F("CAM base Vpin: %c %d "),p[0],CAMBaseVpin);
|
||||
if(p[1]>=100) EXSensorCAM::CAMBaseVpin=p[1];
|
||||
if(p[1]<4) EXSensorCAM::CAMBaseVpin=CAMVPINS[p[1]];
|
||||
DIAG(F("CAM base Vpin: %c %d "),p[0],EXSensorCAM::CAMBaseVpin);
|
||||
return true;
|
||||
}
|
||||
if (camop<100) { //switch CAM# if p[1] dictates
|
||||
if(p[1]>=100 && p[1]<=(vpcount*100+99)) { //limits to CAM# 1 to 4 for now
|
||||
vpin=CAMVPINS[p[1]/100-1];
|
||||
CAMBaseVpin=vpin;
|
||||
if(p[1]>=100 && p[1]<400) { //limits to CAM# 1 to 3 for now
|
||||
vpin=CAMVPINS[p[1]/100];
|
||||
EXSensorCAM::CAMBaseVpin=vpin;
|
||||
DIAG(F("switching to CAM %d baseVpin:%d"),p[1]/100,vpin);
|
||||
p[1]=p[1]%100; //strip off CAM #
|
||||
}
|
||||
}
|
||||
if (CAMBaseVpin==0) {DIAG(F("<n Error: Invalid CAM selected, default to CAM1>"));
|
||||
return false; // cam not defined
|
||||
}
|
||||
|
||||
if (EXSensorCAM::CAMBaseVpin==0) return false; // no cam defined
|
||||
|
||||
|
||||
// send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser
|
||||
switch(paramCount) {
|
||||
case 1: //<N ver> produces '^'
|
||||
if((camop == 'V') || (p[0] == ve) || (p[0] == ver) ) camop='^';
|
||||
if((p[0] == ve) || (p[0] == ver) || (p[0] == 'V')) camop='^';
|
||||
if (STRCHR_P((const char *)F("EFGMQRVW^"),camop) == nullptr) return false;
|
||||
if (camop=='Q') param3=10; //<NQ> for activation state of all 10 banks of sensors
|
||||
if (camop=='F') camop=']'; //<NF> for Reset/Finish webCAM.
|
||||
break; // F Coded as ']' else conflicts with <Nf %%>
|
||||
|
||||
case 2: //<N camop p1>
|
||||
if (STRCHR_P((const char *)F("ABFHILMNOPQRSTUV"),camop)==nullptr) return false;
|
||||
if (STRCHR_P((const char *)F("ABFILMNOPQRSTUV"),camop)==nullptr) return false;
|
||||
param1=p[1];
|
||||
break;
|
||||
|
||||
case 3: //<N vpin rowY colx > or <N cmd p1 p2>
|
||||
camop=p[0];
|
||||
if (p[0]>=100) { //vpin - i.e. NOT 'A' through 'Z'
|
||||
if (p[1]>236 || p[1]<0) return false; //row
|
||||
if (p[2]>316 || p[2]<0) return false; //column
|
||||
camop=0x80; // special 'a' case for IO_SensorCAM
|
||||
vpin = p[0];
|
||||
}else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false;
|
||||
camop=p[0];
|
||||
param1 = p[1];
|
||||
param3 = p[2];
|
||||
break;
|
||||
@ -119,23 +92,4 @@ bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
||||
DIAG(F("CamParser: %d %c %d %d"),vpin,camop,param1,param3);
|
||||
IODevice::writeAnalogue(vpin,param1,camop,param3);
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
30
CamParser.h
30
CamParser.h
@ -1,25 +1,3 @@
|
||||
/*
|
||||
* © 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/>.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef CamParser_H
|
||||
#define CamParser_H
|
||||
#include <Arduino.h>
|
||||
@ -27,13 +5,7 @@
|
||||
|
||||
class CamParser {
|
||||
public:
|
||||
static void parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
static void addVpin(VPIN pin);
|
||||
private:
|
||||
static bool parseN(Print * stream, byte paramCount, int16_t p[]);
|
||||
static VPIN CAMBaseVpin;
|
||||
static VPIN CAMVPINS[];
|
||||
static int vpcount;
|
||||
static bool parseN(Print * stream, byte paramCount, int16_t p[]);
|
||||
};
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* © 2022 Colin Murdoch
|
||||
* All rights reserved.
|
||||
@ -31,7 +31,6 @@
|
||||
#include "DCC.h"
|
||||
#include "TrackManager.h"
|
||||
#include "StringFormatter.h"
|
||||
#include "Websockets.h"
|
||||
|
||||
// variables to hold clock time
|
||||
int16_t lastclocktime;
|
||||
@ -45,7 +44,6 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
|
||||
broadcastBufferWriter->flush();
|
||||
StringFormatter::send(broadcastBufferWriter, msg...);
|
||||
broadcastToClients(type);
|
||||
if (type==COMMAND_TYPE) broadcastToClients(WEBSOCKET_TYPE);
|
||||
}
|
||||
#else
|
||||
// on a single USB connection config, write direct to Serial and ignore flush/shove
|
||||
@ -58,17 +56,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[8]={
|
||||
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
|
||||
// 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)) {
|
||||
// Caution, diag dump of buffer could corrupt ringstream
|
||||
// if headed by websocket bytes.
|
||||
DIAG(F("::parse invalid client=%d"),clientId);
|
||||
return;
|
||||
}
|
||||
if (Diag::WIFI && Diag::CMD)
|
||||
DIAG(F("Parse C=%d T=%d B=%s"),clientId, clients[clientId], buffer);
|
||||
ring=stream;
|
||||
|
||||
// First check if the client is not known
|
||||
@ -77,40 +72,22 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
|
||||
// client is using the DCC++ protocol where all commands start
|
||||
// with '<'
|
||||
if (clients[clientId] == NONE_TYPE) {
|
||||
auto websock=Websockets::checkConnectionString(clientId,buffer,stream);
|
||||
if (websock) {
|
||||
clients[clientId]=WEBSOCK_CONNECTING_TYPE;
|
||||
// websockets will have replied already
|
||||
return;
|
||||
}
|
||||
if (buffer[0] == '<')
|
||||
clients[clientId]=COMMAND_TYPE;
|
||||
else
|
||||
clients[clientId]=WITHROTTLE_TYPE;
|
||||
}
|
||||
|
||||
// after first inbound transmission the websocket is connected
|
||||
if (clients[clientId]==WEBSOCK_CONNECTING_TYPE)
|
||||
clients[clientId]=WEBSOCKET_TYPE;
|
||||
|
||||
|
||||
// mark buffer that is sent to parser
|
||||
ring->mark(clientId);
|
||||
|
||||
// When type is known, send the string
|
||||
// to the right parser
|
||||
if (clients[clientId] == COMMAND_TYPE) {
|
||||
ring->mark(clientId);
|
||||
DCCEXParser::parse(stream, buffer, ring);
|
||||
} else if (clients[clientId] == WITHROTTLE_TYPE) {
|
||||
ring->mark(clientId);
|
||||
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
|
||||
}
|
||||
else if (clients[clientId] == WEBSOCKET_TYPE) {
|
||||
buffer=Websockets::unmask(clientId,ring, buffer);
|
||||
if (!buffer) return; // unmask may have handled it alrerday (ping/pong)
|
||||
// mark ring with client flagged as websocket for transmission later
|
||||
ring->mark(clientId | Websockets::WEBSOCK_CLIENT_MARKER);
|
||||
DCCEXParser::parse(stream, buffer, ring);
|
||||
}
|
||||
|
||||
if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {
|
||||
// The commit call will either write the length bytes
|
||||
@ -154,7 +131,7 @@ void CommandDistributor::broadcastToClients(clientType type) {
|
||||
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
|
||||
if (clients[clientId]==type) {
|
||||
//DIAG(F("CD mark client %d"), clientId);
|
||||
ring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));
|
||||
ring->mark(clientId);
|
||||
ring->print(broadcastBufferWriter->getString());
|
||||
//DIAG(F("CD commit client %d"), clientId);
|
||||
ring->commit();
|
||||
@ -208,15 +185,10 @@ void CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate, byte
|
||||
{
|
||||
case 1:
|
||||
if (clocktime != lastclocktime){
|
||||
auto difference = clocktime - lastclocktime;
|
||||
if (difference<0) difference+=1440;
|
||||
DCC::setTime(clocktime,clockrate,difference>2);
|
||||
// CAH. DIAG removed because LCD does it anyway.
|
||||
LCD(6,F("Clk Time:%d Sp %d"), clocktime, clockrate);
|
||||
// look for an event for this time
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
RMFT2::clockEvent(clocktime,1);
|
||||
#endif
|
||||
// Now tell everyone else what the time is.
|
||||
CommandDistributor::broadcastClockTime(clocktime, clockrate);
|
||||
lastclocktime = clocktime;
|
||||
@ -235,13 +207,9 @@ int16_t CommandDistributor::retClockTime() {
|
||||
return lastclocktime;
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastLoco(DCC::LOCO* sp) {
|
||||
if (!sp) {
|
||||
broadcastReply(COMMAND_TYPE,F("<l 0 -1 128 0>\n"));
|
||||
return;
|
||||
}
|
||||
broadcastReply(COMMAND_TYPE, F("<l %d 0 %d %l>\n"),
|
||||
sp->loco,sp->targetSpeed,sp->functions);
|
||||
void CommandDistributor::broadcastLoco(byte slot) {
|
||||
DCC::LOCO * sp=&DCC::speedTable[slot];
|
||||
broadcastReply(COMMAND_TYPE, F("<l %d %d %d %l>\n"), sp->loco,slot,sp->speedCode,sp->functions);
|
||||
#ifdef SABERTOOTH
|
||||
if (Serial2 && sp->loco == SABERTOOTH) {
|
||||
static uint8_t rampingmode = 0;
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* © 2022 Colin Murdoch
|
||||
*
|
||||
@ -28,7 +28,6 @@
|
||||
#include "StringBuffer.h"
|
||||
#include "defines.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "DCC.h"
|
||||
|
||||
#if WIFI_ON | ETHERNET_ON
|
||||
// Command Distributor must handle a RingStream of clients
|
||||
@ -37,17 +36,17 @@
|
||||
|
||||
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};
|
||||
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[8];
|
||||
#endif
|
||||
public :
|
||||
static void parse(byte clientId,byte* buffer, RingStream * ring);
|
||||
static void broadcastLoco(DCC::LOCO * slot);
|
||||
static void broadcastLoco(byte slot);
|
||||
static void broadcastForgetLoco(int16_t loco);
|
||||
static void broadcastSensor(int16_t id, bool value);
|
||||
static void broadcastTurnout(int16_t id, bool isClosed);
|
||||
|
@ -51,6 +51,12 @@
|
||||
|
||||
#include "DCCEX.h"
|
||||
#include "Display_Implementation.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "Sniffer.h"
|
||||
#include "DCCDecoder.h"
|
||||
Sniffer *dccSniffer = NULL;
|
||||
bool DCCDecoder::active = false;
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
#ifdef CPU_TYPE_ERROR
|
||||
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
|
||||
@ -124,6 +130,11 @@ void setup()
|
||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||
RMFT::begin();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
dccSniffer = new Sniffer(BOOSTER_INPUT);
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
|
||||
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
|
||||
@ -141,25 +152,27 @@ void setup()
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
|
||||
/**************** for future reference
|
||||
void looptimer(unsigned long timeout, const FSH* message)
|
||||
{
|
||||
static unsigned long lasttimestamp = 0;
|
||||
unsigned long now = micros();
|
||||
if (timeout != 0) {
|
||||
unsigned long diff = now - lasttimestamp;
|
||||
if (diff > timeout) {
|
||||
DIAG(message);
|
||||
DIAG(F("DeltaT=%L"), diff);
|
||||
lasttimestamp = micros();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lasttimestamp = now;
|
||||
}
|
||||
*********************************************/
|
||||
void loop()
|
||||
{
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
static bool oldactive = false;
|
||||
if (dccSniffer) {
|
||||
bool newactive = dccSniffer->inputActive();
|
||||
if (oldactive != newactive) {
|
||||
RMFT2::railsyncEvent(newactive);
|
||||
oldactive = newactive;
|
||||
}
|
||||
DCCPacket p = dccSniffer->fetchPacket();
|
||||
if (p.len() != 0) {
|
||||
if (DCCDecoder::parse(p)) {
|
||||
p.print(Serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// The main sketch has responsibilities during loop()
|
||||
|
||||
// Responsibility 1: Handle DCC background processes
|
||||
|
531
DCC.cpp
531
DCC.cpp
@ -5,7 +5,7 @@
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX
|
||||
@ -37,8 +37,6 @@
|
||||
#include "CommandDistributor.h"
|
||||
#include "TrackManager.h"
|
||||
#include "DCCTimer.h"
|
||||
#include "Railcom.h"
|
||||
#include "DCCQueue.h"
|
||||
|
||||
// This module is responsible for converting API calls into
|
||||
// messages to be sent to the waveform generator.
|
||||
@ -62,8 +60,6 @@ const byte FN_GROUP_5=0x10;
|
||||
FSH* DCC::shieldName=NULL;
|
||||
byte DCC::globalSpeedsteps=128;
|
||||
|
||||
#define SLOTLOOP for (auto slot=&speedTable[0];slot!=&speedTable[MAX_LOCOS];slot++)
|
||||
|
||||
void DCC::begin() {
|
||||
StringFormatter::send(&USB_SERIAL,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
|
||||
#ifndef DISABLE_EEPROM
|
||||
@ -76,49 +72,13 @@ void DCC::begin() {
|
||||
#endif
|
||||
}
|
||||
|
||||
byte DCC::defaultMomentumA=0;
|
||||
byte DCC::defaultMomentumD=0;
|
||||
bool DCC::linearAcceleration=false;
|
||||
|
||||
byte DCC::getMomentum(LOCO * slot) {
|
||||
auto target=slot->targetSpeed & 0x7f;
|
||||
auto current=slot->speedCode & 0x7f;
|
||||
if (target > current) {
|
||||
// accelerating
|
||||
auto momentum=slot->momentumA==MOMENTUM_USE_DEFAULT ? defaultMomentumA : slot->momentumA;
|
||||
// if nonlinear acceleration, momentum is reduced according to
|
||||
// gap between throttle and speed.
|
||||
// ie. Loco takes accelerates faster if high throttle
|
||||
if (momentum==0 || linearAcceleration) return momentum;
|
||||
auto powerDifference= (target-current)/8;
|
||||
if (momentum-powerDifference <0) return 0;
|
||||
return momentum-powerDifference;
|
||||
}
|
||||
return slot->momentumD==MOMENTUM_USE_DEFAULT ? defaultMomentumD : slot->momentumD;
|
||||
}
|
||||
|
||||
void DCC::setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection) {
|
||||
if (tSpeed==1) {
|
||||
if (cab==0) {
|
||||
estopAll(); // ESTOP broadcast fix
|
||||
return;
|
||||
}
|
||||
}
|
||||
byte speedCode = (tSpeed & 0x7F) + tDirection * 128;
|
||||
LOCO * slot=lookupSpeedTable(cab);
|
||||
if (slot->targetSpeed==speedCode) return;
|
||||
slot->targetSpeed=speedCode;
|
||||
byte momentum=getMomentum(slot);
|
||||
if (momentum && tSpeed!=1) { // not ESTOP
|
||||
// we dont throttle speed, we just let the reminders take it to target
|
||||
slot->momentum_base=millis();
|
||||
}
|
||||
else { // Momentum not involved, throttle now.
|
||||
slot->speedCode = speedCode;
|
||||
setThrottle2(cab, speedCode);
|
||||
TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr
|
||||
}
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
setThrottle2(cab, speedCode);
|
||||
TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr
|
||||
// retain speed for loco reminders
|
||||
updateLocoReminder(cab, speedCode );
|
||||
}
|
||||
|
||||
void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
@ -158,11 +118,11 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
b[nB++] = speedCode; // for encoding see setThrottle
|
||||
|
||||
}
|
||||
if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority
|
||||
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
||||
}
|
||||
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
|
||||
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
|
||||
byte b[4];
|
||||
byte nB = 0;
|
||||
@ -173,28 +133,24 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
b[nB++] = byte2;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 0, cab);
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, count);
|
||||
}
|
||||
|
||||
// returns speed steps 0 to 127 (1 == emergency stop)
|
||||
// or -1 on "loco not found"
|
||||
int8_t DCC::getThrottleSpeed(int cab) {
|
||||
return getThrottleSpeedByte(cab) & 0x7F;
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0) return -1;
|
||||
return speedTable[reg].speedCode & 0x7F;
|
||||
}
|
||||
|
||||
// returns speed code byte
|
||||
// or 128 (speed 0, dir forward) on "loco not found".
|
||||
// This is the throttle set speed
|
||||
uint8_t DCC::getThrottleSpeedByte(int cab) {
|
||||
LOCO * slot=lookupSpeedTable(cab,false);
|
||||
return slot?slot->targetSpeed:128;
|
||||
}
|
||||
// returns speed code byte for loco.
|
||||
// This is the most recently send DCC speed packet byte
|
||||
// or 128 (speed 0, dir forward) on "loco not found".
|
||||
uint8_t DCC::getLocoSpeedByte(int cab) {
|
||||
LOCO* slot=lookupSpeedTable(cab,false);
|
||||
return slot?slot->speedCode:128;
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return 128;
|
||||
return speedTable[reg].speedCode;
|
||||
}
|
||||
|
||||
// returns 0 to 7 for frequency
|
||||
@ -203,11 +159,12 @@ uint8_t DCC::getThrottleFrequency(int cab) {
|
||||
(void)cab;
|
||||
return 0;
|
||||
#else
|
||||
LOCO* slot=lookupSpeedTable(cab);
|
||||
if (!slot) return 0; // use default frequency
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return 0; // use default frequency
|
||||
// shift out first 29 bits so we have the 3 "frequency bits" left
|
||||
uint8_t res = (uint8_t)(slot->functions >>29);
|
||||
//DIAG(F("Speed table %d functions %l shifted %d"), reg, slot->functions, res);
|
||||
uint8_t res = (uint8_t)(speedTable[reg].functions >>29);
|
||||
//DIAG(F("Speed table %d functions %l shifted %d"), reg, speedTable[reg].functions, res);
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
@ -215,7 +172,9 @@ uint8_t DCC::getThrottleFrequency(int cab) {
|
||||
// returns direction on loco
|
||||
// or true/forward on "loco not found"
|
||||
bool DCC::getThrottleDirection(int cab) {
|
||||
return getThrottleSpeedByte(cab) & 0x80;
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0) return true;
|
||||
return (speedTable[reg].speedCode & 0x80) !=0;
|
||||
}
|
||||
|
||||
// Set function to value on or off
|
||||
@ -239,7 +198,7 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||
b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0); // low order bits and state flag
|
||||
b[nB++] = functionNumber >>7 ; // high order bits
|
||||
}
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
}
|
||||
// We use the reminder table up to 28 for normal functions.
|
||||
// We use 29 to 31 for DC frequency as well so up to 28
|
||||
@ -248,21 +207,22 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||
if (functionNumber > 31)
|
||||
return true;
|
||||
|
||||
LOCO * slot = lookupSpeedTable(cab);
|
||||
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return false;
|
||||
|
||||
// Take care of functions:
|
||||
// Set state of function
|
||||
uint32_t previous=slot->functions;
|
||||
uint32_t previous=speedTable[reg].functions;
|
||||
uint32_t funcmask = (1UL<<functionNumber);
|
||||
if (on) {
|
||||
slot->functions |= funcmask;
|
||||
speedTable[reg].functions |= funcmask;
|
||||
} else {
|
||||
slot->functions &= ~funcmask;
|
||||
speedTable[reg].functions &= ~funcmask;
|
||||
}
|
||||
if (slot->functions != previous) {
|
||||
if (speedTable[reg].functions != previous) {
|
||||
if (functionNumber <= 28)
|
||||
updateGroupflags(slot->groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -279,10 +239,12 @@ void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||
int8_t DCC::getFn( int cab, int16_t functionNumber) {
|
||||
if (cab<=0 || functionNumber>31)
|
||||
return -1; // unknown
|
||||
auto slot = lookupSpeedTable(cab);
|
||||
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return -1;
|
||||
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
return (slot->functions & funcmask)? 1 : 0;
|
||||
return (speedTable[reg].functions & funcmask)? 1 : 0;
|
||||
}
|
||||
|
||||
// Set the group flag to say we have touched the particular group.
|
||||
@ -299,22 +261,22 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
|
||||
|
||||
uint32_t DCC::getFunctionMap(int cab) {
|
||||
if (cab<=0) return 0; // unknown pretend all functions off
|
||||
auto slot = lookupSpeedTable(cab,false);
|
||||
return slot?slot->functions:0;
|
||||
int reg = lookupSpeedTable(cab);
|
||||
return (reg<0)?0:speedTable[reg].functions;
|
||||
}
|
||||
|
||||
// saves DC frequency (0..3) in spare functions 29,30,31
|
||||
void DCC::setDCFreq(int cab,byte freq) {
|
||||
if (cab==0 || freq>3) return;
|
||||
auto slot=lookupSpeedTable(cab,true);
|
||||
auto reg=lookupSpeedTable(cab,true);
|
||||
// drop and replace F29,30,31 (top 3 bits)
|
||||
auto newFunctions=slot->functions & 0x1FFFFFFFUL;
|
||||
auto newFunctions=speedTable[reg].functions & 0x1FFFFFFFUL;
|
||||
if (freq==1) newFunctions |= (1UL<<29); // F29
|
||||
else if (freq==2) newFunctions |= (1UL<<30); // F30
|
||||
else if (freq==3) newFunctions |= (1UL<<31); // F31
|
||||
if (newFunctions==slot->functions) return; // no change
|
||||
slot->functions=newFunctions;
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
if (newFunctions==speedTable[reg].functions) return; // no change
|
||||
speedTable[reg].functions=newFunctions;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
|
||||
void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
@ -327,7 +289,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
// the initial decoders were orgnized and that influenced how the DCC
|
||||
// standard was made.
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
|
||||
#endif
|
||||
// use masks to detect wrong values and do nothing
|
||||
if(address != (address & 511))
|
||||
@ -340,17 +302,16 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
// second byte is of the form 1AAACPPG, where C is 1 for on, PP the ports 0 to 3 and G the gate (coil).
|
||||
b[0] = address % 64 + 128;
|
||||
b[1] = ((((address / 64) % 8) << 4) + (port % 4 << 1) + gate % 2) ^ 0xF8;
|
||||
if (onoff==0) { // off packet only
|
||||
b[1] &= ~0x08; // set C to 0
|
||||
DCCQueue::scheduleDCCPacket(b, 2, 3);
|
||||
} else if (onoff==1) { // on packet only
|
||||
DCCQueue::scheduleDCCPacket(b, 2, 3);
|
||||
} else { // auto timed on then off
|
||||
DCCQueue::scheduleAccOnOffPacket(b, 2, 3, 100); // On then off after 100mS
|
||||
}
|
||||
if (onoff != 0) {
|
||||
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat on packet three times
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate);
|
||||
RMFT2::activateEvent(address<<2|port,gate);
|
||||
#endif
|
||||
}
|
||||
if (onoff != 1) {
|
||||
b[1] &= ~0x08; // set C to 0
|
||||
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat off packet three times
|
||||
}
|
||||
}
|
||||
|
||||
bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {
|
||||
@ -400,7 +361,7 @@ whole range of the 11 bits sent to track.
|
||||
| (((~(address>>8)) & 0x07)<<4) // shift out 8, invert, mask 3 bits, shift up 4
|
||||
| ((address & 0x03)<<1); // mask 2 bits, shift up 1
|
||||
b[2]=value;
|
||||
DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);
|
||||
DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), repeats);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -419,26 +380,7 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = bValue;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
|
||||
}
|
||||
|
||||
//
|
||||
// readCVByteMain: Read a byte with PoM on main.
|
||||
// This requires Railcom active
|
||||
//
|
||||
void DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback) {
|
||||
byte b[5];
|
||||
byte nB = 0;
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
|
||||
b[nB++] = lowByte(cab);
|
||||
b[nB++] = cv1(READ_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = 0;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
|
||||
Railcom::anticipate(cab,cv,callback);
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
}
|
||||
|
||||
//
|
||||
@ -459,45 +401,7 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
|
||||
}
|
||||
|
||||
bool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) {
|
||||
/* see rcn-122
|
||||
5 Global commands
|
||||
These commands are sent and begin exclusively with a broadcast address 0
|
||||
always with {synchronous bits} 0 0000-0000 … and end with the checksum
|
||||
... PPPPPPPP 1. Therefore, only the bytes of the commands and not that of
|
||||
shown below whole package shown. The commands can be used by vehicle and
|
||||
accessory decoders alike.
|
||||
|
||||
5.1 Time command
|
||||
This command is four bytes long and has the format:
|
||||
1100-0001 CCxx-xxxx xxxx-xxxxx xxxx-xxxx
|
||||
CC indicates what data is transmitted in the packet:
|
||||
CC = 00 Model Time
|
||||
1100-0001 00MM-MMMM WWWH-HHHH U0BB-BBBB with:
|
||||
MMMMMM = Minutes, Value range: 0..59
|
||||
WWW = Day of the Week, Value range: 0 = Monday, 1 = Tuesday, 2 = Wednesday,
|
||||
3 = Thursday, 4 = Friday, 5 = Saturday, 6 = Sunday, 7 = Weekday
|
||||
is not supported.
|
||||
HHHHH = Hours, value range: 0..23
|
||||
U =
|
||||
Update, i.e. the time has changed suddenly, e.g. by a new one timetable to start.
|
||||
Up to 4 can occur per sudden change commands can be marked like this.
|
||||
BBBBBB = Acceleration factor, value range 0..63. An acceleration factor of 0 means the
|
||||
model clock has been stopped, a factor of 1 corresponds to real time, at 2 the
|
||||
clock runs twice as fast, at three times as fast as real time, etc.
|
||||
*/
|
||||
if (minutes>=1440 || speed>63 ) return false;
|
||||
byte b[5];
|
||||
b[0]=0; // broadcast address
|
||||
b[1]=0b11000001; // 1100-0001 (model time)
|
||||
b[2]=minutes % 60 ; // MM
|
||||
b[3]= 0b11100000 | (minutes/60); // 111H-HHHH weekday not supported
|
||||
b[4]= (suddenChange ? 0b10000000 : 0) | speed;
|
||||
DCCQueue::scheduleDCCPacket(b, sizeof(b), 2);
|
||||
return true;
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
}
|
||||
|
||||
FSH* DCC::getMotorShieldName() {
|
||||
@ -831,9 +735,10 @@ void DCC::setConsistId(int id,bool reverse,ACK_CALLBACK callback) {
|
||||
|
||||
void DCC::forgetLoco(int cab) { // removes any speed reminders for this loco
|
||||
setThrottle2(cab,1); // ESTOP this loco if still on track
|
||||
auto slot=lookupSpeedTable(cab, false);
|
||||
if (slot) {
|
||||
slot->loco=-1; // no longer used but not end of world
|
||||
int reg=lookupSpeedTable(cab, false);
|
||||
if (reg>=0) {
|
||||
speedTable[reg].loco=0;
|
||||
setThrottle2(cab,1); // ESTOP if this loco still on track
|
||||
CommandDistributor::broadcastForgetLoco(cab);
|
||||
}
|
||||
}
|
||||
@ -841,7 +746,7 @@ void DCC::forgetAllLocos() { // removes all speed reminders
|
||||
setThrottle2(0,1); // ESTOP all locos still on track
|
||||
for (int i=0;i<MAX_LOCOS;i++) {
|
||||
if (speedTable[i].loco) CommandDistributor::broadcastForgetLoco(speedTable[i].loco);
|
||||
speedTable[i].loco=0; // no longer used and looks like end
|
||||
speedTable[i].loco=0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -849,136 +754,68 @@ byte DCC::loopStatus=0;
|
||||
|
||||
void DCC::loop() {
|
||||
TrackManager::loop(); // power overload checks
|
||||
if (DCCWaveform::mainTrack.isReminderWindowOpen()) {
|
||||
// Now is a good time to choose a packet to be sent
|
||||
// Either highest priority from the queues or a reminder
|
||||
if (!DCCQueue::scheduleNext(false)) {
|
||||
// none pending,
|
||||
issueReminders();
|
||||
DCCQueue::scheduleNext(true); // send any pending and force an idle if none
|
||||
}
|
||||
|
||||
}
|
||||
issueReminders();
|
||||
}
|
||||
|
||||
void DCC::issueReminders() {
|
||||
while(true) {
|
||||
// if the main track transmitter still has a pending packet, skip this time around.
|
||||
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
|
||||
// Move to next loco slot. If occupied, send a reminder.
|
||||
// slot.loco is -1 for deleted locos, 0 for end of list.
|
||||
for (auto slot=nextLocoReminder;slot->loco;slot++) {
|
||||
if (slot->loco<0) continue; // deleted loco, skip it
|
||||
if (issueReminder(slot)) {
|
||||
nextLocoReminder=slot+1; // remember next one to check
|
||||
return; // reminder sent, exit
|
||||
}
|
||||
int reg = lastLocoReminder+1;
|
||||
if (reg > highestUsedReg) {
|
||||
if (loopStatus == 0 /*only needed if numLocos == 1 but we do not have a counter*/) {
|
||||
// insert idle packet in the speed packet loop to fullfill the *censored*
|
||||
// >5ms between packets to same decoder rule
|
||||
const byte idlepacket[] = {0xFF, 0x00};
|
||||
DCCWaveform::mainTrack.schedulePacket(idlepacket, 2, 0);
|
||||
}
|
||||
// we have reached the end of the table, so we can move on to
|
||||
// the next loop state and start from the top.
|
||||
// There are 0-9 loop states.. speed,f1,speed,f2,speed,f3,speed,f4,speed,f5
|
||||
loopStatus++;
|
||||
if (loopStatus>9) loopStatus=0; // reset to 0
|
||||
|
||||
// try looking from the start of the table down to where we started last time
|
||||
|
||||
for (auto slot=&speedTable[0];slot<nextLocoReminder;slot++) {
|
||||
if (slot->loco<0) continue; // deleted loco, skip it
|
||||
if (issueReminder(slot)) {
|
||||
nextLocoReminder=slot+1; // remember next one to check
|
||||
return; // reminder sent, exit
|
||||
}
|
||||
}
|
||||
// if we get here then we can update the loop status and start again
|
||||
if (loopStatus==0) return; // nothing found at all
|
||||
reg = 0; // Go to start of table
|
||||
}
|
||||
if (speedTable[reg].loco > 0) {
|
||||
// have found loco to remind
|
||||
if (issueReminder(reg))
|
||||
lastLocoReminder = reg;
|
||||
} else
|
||||
lastLocoReminder = reg;
|
||||
}
|
||||
|
||||
int16_t normalize(byte speed) {
|
||||
if (speed & 0x80) return speed & 0x7F;
|
||||
return 0-1-speed;
|
||||
}
|
||||
byte dccalize(int16_t speed) {
|
||||
if (speed>127) return 0xFF; // 127 forward
|
||||
if (speed<-127) return 0x7F; // 127 reverse
|
||||
if (speed >=0) return speed | 0x80;
|
||||
// negative speeds... -1==dcc 0, -2==dcc 1
|
||||
return (int16_t)-1 - speed;
|
||||
}
|
||||
|
||||
bool DCC::issueReminder(LOCO * slot) {
|
||||
unsigned long functions=slot->functions;
|
||||
int loco=slot->loco;
|
||||
byte flags=slot->groupFlags;
|
||||
bool DCC::issueReminder(int reg) {
|
||||
unsigned long functions=speedTable[reg].functions;
|
||||
int loco=speedTable[reg].loco;
|
||||
byte flags=speedTable[reg].groupFlags;
|
||||
|
||||
switch (loopStatus) {
|
||||
case 0:
|
||||
case 2:
|
||||
case 4:
|
||||
case 6:
|
||||
case 8: {
|
||||
// calculate any momentum change going on
|
||||
auto sc=slot->speedCode;
|
||||
if (slot->targetSpeed!=sc) {
|
||||
// calculate new speed code
|
||||
auto now=millis();
|
||||
int16_t delay=now-slot->momentum_base;
|
||||
auto millisPerNotch=MOMENTUM_FACTOR * (int16_t)getMomentum(slot);
|
||||
// allow for momentum change to 0 while accelerating/slowing
|
||||
auto ticks=(millisPerNotch>0)?(delay/millisPerNotch):500;
|
||||
if (ticks>0) {
|
||||
auto current=normalize(sc); // -128..+127
|
||||
auto target=normalize(slot->targetSpeed);
|
||||
// DIAG(F("Momentum l=%d ti=%d sc=%d c=%d t=%d"),loco,ticks,sc,current,target);
|
||||
if (current<target) { // accelerate
|
||||
current+=ticks;
|
||||
if (current>target) current=target;
|
||||
}
|
||||
else { // slow
|
||||
current-=ticks;
|
||||
if (current<target) current=target;
|
||||
}
|
||||
sc=dccalize(current);
|
||||
//DIAG(F("c=%d newsc=%d"),current,sc);
|
||||
slot->speedCode=sc;
|
||||
TrackManager::setDCSignal(loco,sc); // in case this is a dcc track on this addr
|
||||
slot->momentum_base=now;
|
||||
}
|
||||
}
|
||||
// DIAG(F("Reminder %d speed %d"),loco,slot->speedCode);
|
||||
setThrottle2(loco, sc);
|
||||
}
|
||||
return true; // reminder sent
|
||||
// DIAG(F("Reminder %d speed %d"),loco,speedTable[reg].speedCode);
|
||||
setThrottle2(loco, speedTable[reg].speedCode);
|
||||
break;
|
||||
case 1: // remind function group 1 (F0-F4)
|
||||
if (flags & FN_GROUP_1) {
|
||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
if (flags & FN_GROUP_1)
|
||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
|
||||
break;
|
||||
case 3: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2) {
|
||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
case 2: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2)
|
||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
|
||||
break;
|
||||
case 5: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3) {
|
||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
case 3: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3)
|
||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
|
||||
break;
|
||||
case 7: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4) {
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
||||
return true;
|
||||
}
|
||||
case 4: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4)
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),0);
|
||||
break;
|
||||
case 9: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5) {
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
||||
return true; // reminder sent
|
||||
}
|
||||
case 5: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5)
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),0);
|
||||
break;
|
||||
}
|
||||
return false; // no reminder sent
|
||||
loopStatus++;
|
||||
// if we reach status 6 then this loco is done so
|
||||
// reset status to 0 for next loco and return true so caller
|
||||
// moves on to next loco.
|
||||
if (loopStatus>5) loopStatus=0;
|
||||
return loopStatus==0;
|
||||
}
|
||||
|
||||
|
||||
@ -995,128 +832,70 @@ byte DCC::cv2(int cv) {
|
||||
return lowByte(cv);
|
||||
}
|
||||
|
||||
DCC::LOCO * DCC::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
int DCC::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
// determine speed reg for this loco
|
||||
LOCO * firstEmpty=nullptr;
|
||||
SLOTLOOP {
|
||||
if (firstEmpty==nullptr && slot->loco<=0) firstEmpty=slot;
|
||||
if (slot->loco == locoId) return slot;
|
||||
if (slot->loco==0) break;
|
||||
int firstEmpty = MAX_LOCOS;
|
||||
int reg;
|
||||
for (reg = 0; reg < MAX_LOCOS; reg++) {
|
||||
if (speedTable[reg].loco == locoId) break;
|
||||
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
|
||||
}
|
||||
if (!autoCreate) return nullptr;
|
||||
if (firstEmpty==nullptr) {
|
||||
// return last slot if full
|
||||
DIAG(F("Too many locos, reusing last slot"));
|
||||
firstEmpty=&speedTable[MAX_LOCOS-1];
|
||||
|
||||
// return -1 if not found and not auto creating
|
||||
if (reg== MAX_LOCOS && !autoCreate) return -1;
|
||||
if (reg == MAX_LOCOS) reg = firstEmpty;
|
||||
if (reg >= MAX_LOCOS) {
|
||||
DIAG(F("Too many locos"));
|
||||
return -1;
|
||||
}
|
||||
// fill first empty slot with new entry
|
||||
firstEmpty->loco = locoId;
|
||||
firstEmpty->speedCode=128; // default direction forward
|
||||
firstEmpty->targetSpeed=128; // default direction forward
|
||||
firstEmpty->groupFlags=0;
|
||||
firstEmpty->functions=0;
|
||||
firstEmpty->momentumA=MOMENTUM_USE_DEFAULT;
|
||||
firstEmpty->momentumD=MOMENTUM_USE_DEFAULT;
|
||||
return firstEmpty;
|
||||
if (reg==firstEmpty){
|
||||
speedTable[reg].loco = locoId;
|
||||
speedTable[reg].speedCode=128; // default direction forward
|
||||
speedTable[reg].groupFlags=0;
|
||||
speedTable[reg].functions=0;
|
||||
}
|
||||
if (reg > highestUsedReg) highestUsedReg = reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
bool DCC::setMomentum(int locoId,int16_t accelerating, int16_t decelerating) {
|
||||
if (locoId<0) return false;
|
||||
if (locoId==0) {
|
||||
if (accelerating<0 || decelerating<0) return false;
|
||||
defaultMomentumA=accelerating/MOMENTUM_FACTOR;
|
||||
defaultMomentumD=decelerating/MOMENTUM_FACTOR;
|
||||
return true;
|
||||
void DCC::updateLocoReminder(int loco, byte speedCode) {
|
||||
|
||||
if (loco==0) {
|
||||
// broadcast stop/estop but dont change direction
|
||||
for (int reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco==0) continue;
|
||||
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].speedCode != newspeed) {
|
||||
speedTable[reg].speedCode = newspeed;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// -1 is ok and means this loco should use the default.
|
||||
if (accelerating<-1 || decelerating<-1) return false;
|
||||
if (accelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT ||
|
||||
decelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT) return false;
|
||||
|
||||
// Values stored are 255=MOMENTUM_USE_DEFAULT, or millis/MOMENTUM_FACTOR.
|
||||
// This is to keep the values in a byte rather than int16
|
||||
// thus saving 2 bytes RAM per loco slot.
|
||||
LOCO* slot=lookupSpeedTable(locoId,true);
|
||||
slot->momentumA=(accelerating<0)? MOMENTUM_USE_DEFAULT: (accelerating/MOMENTUM_FACTOR);
|
||||
slot->momentumD=(decelerating<0)? MOMENTUM_USE_DEFAULT: (decelerating/MOMENTUM_FACTOR);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void DCC::estopAll() {
|
||||
setThrottle2(0,1); // estop all locos
|
||||
TrackManager::setDCSignal(0,1);
|
||||
|
||||
// remind stop/estop but dont change direction
|
||||
SLOTLOOP {
|
||||
if (slot->loco<=0) continue;
|
||||
byte newspeed=(slot->targetSpeed & 0x80) | 0x01;
|
||||
slot->speedCode = newspeed;
|
||||
slot->targetSpeed = newspeed;
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0 && speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DCC::LOCO DCC::speedTable[MAX_LOCOS];
|
||||
DCC::LOCO * DCC::nextLocoReminder = &DCC::speedTable[0];
|
||||
int DCC::lastLocoReminder = 0;
|
||||
int DCC::highestUsedReg = 0;
|
||||
|
||||
|
||||
void DCC::displayCabList(Print * stream) {
|
||||
StringFormatter::send(stream,F("<*\n"));
|
||||
|
||||
int used=0;
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
if (slot->loco>0) {
|
||||
for (int reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco>0) {
|
||||
used ++;
|
||||
StringFormatter::send(stream,F("cab=%d, speed=%d, target=%d, momentum=%d/%d, block=%d\n"),
|
||||
slot->loco, slot->speedCode, slot->targetSpeed,
|
||||
slot->momentumA, slot->momentumD, slot->blockOccupied);
|
||||
StringFormatter::send(stream,F("cab=%d, speed=%d, dir=%c \n"),
|
||||
speedTable[reg].loco, speedTable[reg].speedCode & 0x7f,(speedTable[reg].speedCode & 0x80) ? 'F':'R');
|
||||
}
|
||||
}
|
||||
StringFormatter::send(stream,F("Used=%d, max=%d, momentum=%d/%d *>\n"),
|
||||
used,MAX_LOCOS, DCC::defaultMomentumA,DCC::defaultMomentumD);
|
||||
}
|
||||
|
||||
void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
|
||||
// update block loco is in, tell exrail leaving old block, and entering new.
|
||||
|
||||
// NOTE: The loco table scanning is really inefficient and needs rewriting
|
||||
// This was done once in the momentum poc.
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
auto slot=lookupSpeedTable(loco,true);
|
||||
if (!slot) return;
|
||||
auto oldBlock=slot->blockOccupied;
|
||||
if (oldBlock==blockid) return;
|
||||
if (oldBlock) RMFT2::blockEvent(oldBlock,loco,false);
|
||||
slot->blockOccupied=blockid;
|
||||
if (blockid) RMFT2::blockEvent(blockid,loco,true);
|
||||
|
||||
if (exclusive) {
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
if (slot->loco>0) {
|
||||
if (slot->loco!=loco && slot->blockOccupied==blockid) {
|
||||
RMFT2::blockEvent(blockid,slot->loco,false);
|
||||
slot->blockOccupied=0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DCC::clearBlock(uint16_t blockid) {
|
||||
// Railcom reports block empty... tell Exrail about all leavers
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
if (slot->loco>0) {
|
||||
if (slot->blockOccupied==blockid) {
|
||||
RMFT2::blockEvent(blockid,slot->loco,false);
|
||||
slot->blockOccupied=0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
StringFormatter::send(stream,F("Used=%d, max=%d\n"),used,MAX_LOCOS);
|
||||
|
||||
}
|
||||
|
34
DCC.h
34
DCC.h
@ -3,7 +3,7 @@
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
@ -59,15 +59,11 @@ public:
|
||||
|
||||
// Public DCC API functions
|
||||
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
|
||||
static void estopAll();
|
||||
static int8_t getThrottleSpeed(int cab);
|
||||
static uint8_t getThrottleSpeedByte(int cab);
|
||||
static uint8_t getLocoSpeedByte(int cab); // may lag throttle
|
||||
static uint8_t getThrottleFrequency(int cab);
|
||||
static bool getThrottleDirection(int cab);
|
||||
static void writeCVByteMain(int cab, int cv, byte bValue);
|
||||
static void readCVByteMain(int cab, int cv, ACK_CALLBACK callback);
|
||||
|
||||
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
|
||||
static void setFunction(int cab, byte fByte, byte eByte);
|
||||
static bool setFn(int cab, int16_t functionNumber, bool on);
|
||||
@ -87,9 +83,7 @@ public:
|
||||
static void writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
|
||||
static void verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);
|
||||
static void verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
|
||||
static bool setTime(uint16_t minutes,uint8_t speed, bool suddenChange);
|
||||
static void setLocoInBlock(int loco, uint16_t blockid, bool exclusive);
|
||||
static void clearBlock(uint16_t blockid);
|
||||
|
||||
static void getLocoId(ACK_CALLBACK callback);
|
||||
static void setLocoId(int id,ACK_CALLBACK callback);
|
||||
static void setConsistId(int id,bool reverse,ACK_CALLBACK callback);
|
||||
@ -108,31 +102,20 @@ public:
|
||||
byte speedCode;
|
||||
byte groupFlags;
|
||||
uint32_t functions;
|
||||
// Momentum management variables
|
||||
uint32_t momentum_base; // millis() when speed modified under momentum
|
||||
byte momentumA, momentumD;
|
||||
byte targetSpeed; // speed set by throttle
|
||||
uint16_t blockOccupied; // railcom detected block
|
||||
};
|
||||
static const int16_t MOMENTUM_FACTOR=7;
|
||||
static const byte MOMENTUM_USE_DEFAULT=255;
|
||||
static bool linearAcceleration;
|
||||
static byte getMomentum(LOCO * slot);
|
||||
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
static LOCO * lookupSpeedTable(int locoId, bool autoCreate=true);
|
||||
static int lookupSpeedTable(int locoId, bool autoCreate=true);
|
||||
static byte cv1(byte opcode, int cv);
|
||||
static byte cv2(int cv);
|
||||
static bool setMomentum(int locoId,int16_t accelerating, int16_t decelerating);
|
||||
|
||||
private:
|
||||
static byte loopStatus;
|
||||
static byte defaultMomentumA; // Accelerating
|
||||
static byte defaultMomentumD; // Accelerating
|
||||
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
||||
static void setFunctionInternal(int cab, byte fByte, byte eByte);
|
||||
static bool issueReminder(LOCO * slot);
|
||||
static LOCO* nextLocoReminder;
|
||||
static void updateLocoReminder(int loco, byte speedCode);
|
||||
static void setFunctionInternal(int cab, byte fByte, byte eByte, byte count);
|
||||
static bool issueReminder(int reg);
|
||||
static int lastLocoReminder;
|
||||
static int highestUsedReg;
|
||||
static FSH *shieldName;
|
||||
static byte globalSpeedsteps;
|
||||
|
||||
@ -143,7 +126,6 @@ private:
|
||||
// NMRA codes #
|
||||
static const byte SET_SPEED = 0x3f;
|
||||
static const byte WRITE_BYTE_MAIN = 0xEC;
|
||||
static const byte READ_BYTE_MAIN = 0xE4;
|
||||
static const byte WRITE_BIT_MAIN = 0xE8;
|
||||
static const byte WRITE_BYTE = 0x7C;
|
||||
static const byte VERIFY_BYTE = 0x74;
|
||||
|
175
DCCDecoder.cpp
Normal file
175
DCCDecoder.cpp
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* 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 ARDUINO_ARCH_ESP32
|
||||
#include "DCCDecoder.h"
|
||||
#include "LocoTable.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
|
||||
bool DCCDecoder::parse(DCCPacket &p) {
|
||||
if (!active)
|
||||
return false;
|
||||
const byte DECODER_MOBILE = 1;
|
||||
const byte DECODER_ACCESSORY = 2;
|
||||
byte decoderType = 0; // use 0 as none
|
||||
byte *d = p.data();
|
||||
byte *instr = 0; // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
|
||||
uint16_t addr; // will be set to decoder addr (long/shor mobile or accessory)
|
||||
bool locoInfoChanged = false;
|
||||
|
||||
if (d[0] == 0B11111111) { // Idle packet
|
||||
return false;
|
||||
}
|
||||
// CRC verification here
|
||||
byte checksum = 0;
|
||||
for (byte n = 0; n < p.len(); n++)
|
||||
checksum ^= d[n];
|
||||
if (checksum) { // Result should be zero, if not it's an error!
|
||||
DIAG(F("Checksum error"));
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Serial.print("< ");
|
||||
for(int n=0; n<8; n++) {
|
||||
Serial.print(d[0]&(1<<n)?"1":"0");
|
||||
}
|
||||
Serial.println(" >");
|
||||
*/
|
||||
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
|
||||
decoderType = DECODER_MOBILE;
|
||||
instr = d+1;
|
||||
addr = d[0];
|
||||
} else {
|
||||
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
|
||||
decoderType = DECODER_MOBILE;
|
||||
instr = d+2;
|
||||
addr = 256 * (d[0] & 0B00111111) + d[1];
|
||||
} else { // bit7 == 1 and bit 6 == 0
|
||||
decoderType = DECODER_ACCESSORY;
|
||||
instr = d+1;
|
||||
addr = d[0] & 0B00111111;
|
||||
}
|
||||
}
|
||||
if (decoderType == DECODER_MOBILE) {
|
||||
switch (instr[0] & 0xE0) {
|
||||
case 0x20: // 001x-xxxx Extended commands
|
||||
if (instr[0] == 0B00111111) { // 128 speed steps
|
||||
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[1])) == true) {
|
||||
byte speed = instr[1] & 0B01111111;
|
||||
byte direction = instr[1] & 0B10000000;
|
||||
DCC::setThrottle(addr, speed, direction);
|
||||
//DIAG(F("UPDATE"));
|
||||
// send speed change to DCCEX here
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
|
||||
case 0x60: // 011x-xxxx
|
||||
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[0] & 0B00111111)) == true) {
|
||||
byte speed = instr[0] & 0B00001111; // first only look at 4 bits
|
||||
if (speed > 1) { // neither stop nor emergency stop, recalculate speed
|
||||
speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
|
||||
speed = (speed - 3) * 9/2;
|
||||
}
|
||||
byte direction = instr[0] & 0B00100000;
|
||||
DCC::setThrottle(addr, speed, direction);
|
||||
}
|
||||
break;
|
||||
case 0x80: // 100x-xxxx Function group 1
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 1)) == true) {
|
||||
byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
|
||||
DCCEXParser::funcmap(addr, normalized, 0, 4);
|
||||
}
|
||||
break;
|
||||
case 0xA0: // 101x-xxxx Function group 3 and 2
|
||||
{
|
||||
byte low, high;
|
||||
if (bitRead(instr[0], 4)) {
|
||||
low = 5;
|
||||
high = 8;
|
||||
} else {
|
||||
low = 9;
|
||||
high = 12;
|
||||
}
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], low)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[0], low, high);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
|
||||
switch (instr[0] & 0B00011111) {
|
||||
case 0B00011110: // F13-F20 Function Control
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 13)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 13, 20);
|
||||
}
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 17)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 13, 20);
|
||||
}
|
||||
break;
|
||||
case 0B00011111: // F21-F28 Function Control
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1], 21)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 21, 28);
|
||||
} // updateFunc handles only the 4 low bits as that is the most common case
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1]>>4, 25)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 21, 28);
|
||||
}
|
||||
break;
|
||||
/* do that later
|
||||
case 0B00011000: // F29-F36 Function Control
|
||||
break;
|
||||
case 0B00011001: // F37-F44 Function Control
|
||||
break;
|
||||
case 0B00011010: // F45-F52 Function Control
|
||||
break;
|
||||
case 0B00011011: // F53-F60 Function Control
|
||||
break;
|
||||
case 0B00011100: // F61-F68 Function Control
|
||||
break;
|
||||
*/
|
||||
}
|
||||
break;
|
||||
case 0xE0: // 111x-xxxx Config vars
|
||||
break;
|
||||
}
|
||||
return locoInfoChanged;
|
||||
}
|
||||
if (decoderType == DECODER_ACCESSORY) {
|
||||
if (instr[0] & 0B10000000) { // Basic Accessory
|
||||
addr = (((~instr[0]) & 0B01110000) << 2) + addr;
|
||||
byte port = (instr[0] & 0B00000110) >> 1;
|
||||
byte activate = (instr[0] & 0B00001000) >> 3;
|
||||
byte coil = (instr[0] & 0B00000001);
|
||||
locoInfoChanged = true;
|
||||
//(void)addr; (void)port; (void)coil; (void)activate;
|
||||
//DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
|
||||
DCC::setAccessory(addr, port, coil, activate);
|
||||
} else { // Accessory Extended NMRA spec, do we need to decode this?
|
||||
/*
|
||||
addr = (addr << 5) +
|
||||
((instr[0] & 0B01110000) >> 2) +
|
||||
((instr[0] & 0B00000110) >> 1);
|
||||
*/
|
||||
}
|
||||
return locoInfoChanged;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // ARDUINO_ARCH_ESP32
|
@ -1,7 +1,6 @@
|
||||
/*
|
||||
* © 2023 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@ -17,18 +16,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef Websockets_h
|
||||
#define Websockets_h
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <Arduino.h>
|
||||
#include "RingStream.h"
|
||||
class Websockets {
|
||||
public:
|
||||
static bool checkConnectionString(byte clientId,byte * cmd, RingStream * outbound );
|
||||
static byte * unmask(byte clientId,RingStream *ring, byte * buffer);
|
||||
static int16_t getOutboundHeaderSize(uint16_t dataLength);
|
||||
static int fillOutboundHeader(uint16_t dataLength, byte * buffer);
|
||||
static void writeOutboundHeader(Print * stream,uint16_t dataLength);
|
||||
static const byte WEBSOCK_CLIENT_MARKER=0x80;
|
||||
};
|
||||
#include "DCCPacket.h"
|
||||
|
||||
#endif
|
||||
class DCCDecoder {
|
||||
public:
|
||||
static bool parse(DCCPacket &p);
|
||||
static inline void onoff(bool on) {active = on;};
|
||||
private:
|
||||
static bool active;
|
||||
};
|
||||
#endif // ARDUINO_ARCH_ESP32
|
129
DCCEXParser.cpp
129
DCCEXParser.cpp
@ -3,10 +3,10 @@
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021-2024 Herb Morton
|
||||
* © 2020-2023 Harald Barth
|
||||
* © 2020-2025 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2022 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@ -64,12 +64,11 @@ Once a new OPCODE is decided upon, update this list.
|
||||
I, Turntable object command, control, and broadcast
|
||||
j, Throttle responses
|
||||
J, Throttle queries
|
||||
k, Block exit (Railcom)
|
||||
K, Block enter (Railcom)
|
||||
k, Reserved for future use - Potentially Railcom
|
||||
K, Reserved for future use - Potentially 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, message to throttles broadcast
|
||||
M, Write DCC packet
|
||||
n, Reserved for SensorCam
|
||||
N, Reserved for Sensorcam
|
||||
@ -119,9 +118,9 @@ Once a new OPCODE is decided upon, update this list.
|
||||
#include "version.h"
|
||||
#include "KeywordHasher.h"
|
||||
#include "CamParser.h"
|
||||
#include "Stash.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "WifiESP32.h"
|
||||
#include "DCCDecoder.h"
|
||||
#endif
|
||||
|
||||
// This macro can't be created easily as a portable function because the
|
||||
@ -241,7 +240,6 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd,
|
||||
extern __attribute__((weak)) void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
|
||||
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
|
||||
FILTER_CALLBACK DCCEXParser::filterCamParserCallback = 0;
|
||||
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
|
||||
|
||||
// deprecated
|
||||
@ -253,10 +251,6 @@ void DCCEXParser::setRMFTFilter(FILTER_CALLBACK filter)
|
||||
{
|
||||
filterRMFTCallback = filter;
|
||||
}
|
||||
void DCCEXParser::setCamParserFilter(FILTER_CALLBACK filter)
|
||||
{
|
||||
filterCamParserCallback = filter;
|
||||
}
|
||||
void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
|
||||
{
|
||||
atCommandCallback = callback;
|
||||
@ -317,8 +311,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
filterCallback(stream, opcode, params, p);
|
||||
if (filterRMFTCallback && opcode!='\0')
|
||||
filterRMFTCallback(stream, opcode, params, p);
|
||||
if (filterCamParserCallback && opcode!='\0')
|
||||
filterCamParserCallback(stream, opcode, params, p);
|
||||
|
||||
// Functions return from this switch if complete, break from switch implies error <X> to send
|
||||
switch (opcode)
|
||||
@ -332,9 +324,12 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
int16_t direction;
|
||||
|
||||
if (params==1) { // <t cab> display state
|
||||
if (p[0]<=0) break;
|
||||
CommandDistributor::broadcastLoco(DCC::lookupSpeedTable(p[0],false));
|
||||
return;
|
||||
int16_t slot=DCC::lookupSpeedTable(p[0],false);
|
||||
if (slot>=0)
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
else // send dummy state speed 0 fwd no functions.
|
||||
StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params == 4)
|
||||
@ -496,35 +491,12 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
DCC::writeCVByteMain(p[0], p[1], p[2]);
|
||||
return;
|
||||
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
case 'r': // READ CV on MAIN <r CAB CV> Requires Railcom
|
||||
if (params != 2)
|
||||
break;
|
||||
if (!DCCWaveform::isRailcom()) break;
|
||||
if (!stashCallback(stream, p, ringStream)) break;
|
||||
DCC::readCVByteMain(p[0], p[1],callback_r);
|
||||
return;
|
||||
#endif
|
||||
|
||||
case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>
|
||||
if (params != 4)
|
||||
break;
|
||||
DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);
|
||||
return;
|
||||
#endif
|
||||
|
||||
case 'm': // <m cabid momentum [braking]>
|
||||
// <m LINEAR|POWER>
|
||||
if (params==1) {
|
||||
if (p[0]=="LINEAR"_hk) DCC::linearAcceleration=true;
|
||||
else if (p[0]=="POWER"_hk) DCC::linearAcceleration=false;
|
||||
else break;
|
||||
return;
|
||||
}
|
||||
if (params<2 || params>3) break;
|
||||
if (params==2) p[2]=p[1];
|
||||
if (DCC::setMomentum(p[0],p[1],p[2])) return;
|
||||
break;
|
||||
|
||||
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
|
||||
#ifndef DISABLE_PROG
|
||||
@ -674,7 +646,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
}
|
||||
|
||||
case '!': // ESTOP ALL <!>
|
||||
DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
return;
|
||||
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
@ -712,6 +684,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
case 'C': // CONFIG <C [params]>
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
// currently this only works on ESP32
|
||||
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
|
||||
bool on = false;
|
||||
if (params>1 && p[1] == "ON"_hk) {
|
||||
on = true;
|
||||
}
|
||||
DCCDecoder::onoff(on);
|
||||
return;
|
||||
}
|
||||
#if defined(HAS_ENOUGH_MEMORY)
|
||||
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
|
||||
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
|
||||
@ -773,7 +753,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
|
||||
case 'J' : // throttle info access
|
||||
{
|
||||
if (params<1) break; // <J>
|
||||
if ((params<1) | (params>3)) break; // <J>
|
||||
//if ((params<1) | (params>2)) break; // <J>
|
||||
int16_t id=(params==2)?p[1]:0;
|
||||
switch(p[0]) {
|
||||
@ -801,10 +781,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
StringFormatter::send(stream, F("<jA>\n"));
|
||||
return;
|
||||
|
||||
case "M"_hk: // <JM> Stash management
|
||||
if (parseJM(stream, params, p))
|
||||
return;
|
||||
break;
|
||||
case "M"_hk: // <JM> intercepted by EXRAIL
|
||||
if (params>1) break; // invalid cant do
|
||||
// <JM> requests stash size so say none.
|
||||
StringFormatter::send(stream,F("<jM 0>\n"));
|
||||
return;
|
||||
|
||||
case "R"_hk: // <JR> returns rosters
|
||||
StringFormatter::send(stream, F("<jR"));
|
||||
@ -912,11 +893,15 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
if (parseI(stream, params, p))
|
||||
return;
|
||||
break;
|
||||
#endif
|
||||
#ifndef IO_NO_HAL
|
||||
case 'N': // <N commands for SensorCam
|
||||
if (CamParser::parseN(stream,params,p)) return;
|
||||
break;
|
||||
#endif
|
||||
case '/': // implemented in EXRAIL parser
|
||||
case 'L': // LCC interface implemented in EXRAIL parser
|
||||
case 'N': // interface implemented in CamParser
|
||||
break; // Will <X> if not intercepted by filters
|
||||
break; // Will <X> if not intercepted by EXRAIL
|
||||
|
||||
#ifndef DISABLE_VDPY
|
||||
case '@': // JMRI saying "give me virtual LCD msgs"
|
||||
@ -1243,10 +1228,6 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
return true;
|
||||
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
case "RAILCOM"_hk: // <D RAILCOM ON/OFF>
|
||||
Diag::RAILCOM = onOff;
|
||||
return true;
|
||||
|
||||
case "WIFI"_hk: // <D WIFI ON/OFF>
|
||||
Diag::WIFI = onOff;
|
||||
return true;
|
||||
@ -1262,10 +1243,6 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
case "LCN"_hk: // <D LCN ON/OFF>
|
||||
Diag::LCN = onOff;
|
||||
return true;
|
||||
|
||||
case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
|
||||
Diag::WEBSOCKET = onOff;
|
||||
return true;
|
||||
#endif
|
||||
#ifndef DISABLE_EEPROM
|
||||
case "EEPROM"_hk: // <D EEPROM NumEntries>
|
||||
@ -1395,40 +1372,6 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
|
||||
}
|
||||
#endif
|
||||
|
||||
bool DCCEXParser::parseJM(Print *stream, int16_t params, int16_t p[]) {
|
||||
switch (params) {
|
||||
case 1: // <JM> list all stashed automations
|
||||
Stash::list(stream);
|
||||
return true;
|
||||
|
||||
case 2: // <JM id> get stash value
|
||||
Stash::list(stream, p[1]);
|
||||
return true;
|
||||
|
||||
case 3: //
|
||||
if (p[1]=="CLEAR"_hk) {
|
||||
if (p[2]=="ALL"_hk) { // <JM CLEAR ALL>
|
||||
Stash::clearAll();
|
||||
return true;
|
||||
}
|
||||
Stash::clear(p[2]); // <JM CLEAR id>
|
||||
return true;
|
||||
}
|
||||
Stash::set(p[1], p[2]); // <JM id loco>
|
||||
return true;
|
||||
|
||||
case 4: // <JM CLEAR ANY id>
|
||||
if (p[1]=="CLEAR"_hk && p[2]=="ANY"_hk) {
|
||||
// <JM CLEAR ANY id>
|
||||
Stash::clearAny(p[3]);
|
||||
return true;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CALLBACKS must be static
|
||||
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
|
||||
{
|
||||
@ -1492,12 +1435,6 @@ void DCCEXParser::callback_R(int16_t result)
|
||||
commitAsyncReplyStream();
|
||||
}
|
||||
|
||||
void DCCEXParser::callback_r(int16_t result)
|
||||
{
|
||||
StringFormatter::send(getAsyncReplyStream(), F("<r %d %d %d >\n"), stashP[0], stashP[1], result);
|
||||
commitAsyncReplyStream();
|
||||
}
|
||||
|
||||
void DCCEXParser::callback_Rloco(int16_t result) {
|
||||
const FSH * detail;
|
||||
if (result<=0) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
@ -37,9 +37,9 @@ struct DCCEXParser
|
||||
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
|
||||
static void setFilter(FILTER_CALLBACK filter);
|
||||
static void setRMFTFilter(FILTER_CALLBACK filter);
|
||||
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 bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||
|
||||
private:
|
||||
|
||||
@ -52,7 +52,6 @@ struct DCCEXParser
|
||||
static bool parsef(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseC(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseD(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseJM(Print * stream, int16_t params, int16_t p[]);
|
||||
#ifndef IO_NO_HAL
|
||||
static bool parseI(Print * stream, int16_t params, int16_t p[]);
|
||||
#endif
|
||||
@ -70,8 +69,7 @@ struct DCCEXParser
|
||||
static void callback_W(int16_t result);
|
||||
static void callback_W4(int16_t result);
|
||||
static void callback_B(int16_t result);
|
||||
static void callback_R(int16_t result); // prog
|
||||
static void callback_r(int16_t result); // main
|
||||
static void callback_R(int16_t result);
|
||||
static void callback_Rloco(int16_t result);
|
||||
static void callback_Wloco(int16_t result);
|
||||
static void callback_Wconsist(int16_t result);
|
||||
@ -79,9 +77,7 @@ struct DCCEXParser
|
||||
static void callback_Vbyte(int16_t result);
|
||||
static FILTER_CALLBACK filterCallback;
|
||||
static FILTER_CALLBACK filterRMFTCallback;
|
||||
static FILTER_CALLBACK filterCamParserCallback;
|
||||
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[]);
|
||||
|
||||
};
|
||||
|
80
DCCPacket.h
Normal file
80
DCCPacket.h
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* 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>
|
||||
#ifndef DCCPacket_h
|
||||
#define DCCPacket_h
|
||||
#include <strings.h>
|
||||
|
||||
class DCCPacket {
|
||||
public:
|
||||
DCCPacket() {
|
||||
_len = 0;
|
||||
_data = NULL;
|
||||
};
|
||||
DCCPacket(byte *d, byte l) {
|
||||
_len = l;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = d[n];
|
||||
};
|
||||
DCCPacket(const DCCPacket &old) {
|
||||
_len = old._len;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = old._data[n];
|
||||
};
|
||||
DCCPacket &operator=(const DCCPacket &rhs) {
|
||||
if (this == &rhs)
|
||||
return *this;
|
||||
delete[]_data;
|
||||
_len = rhs._len;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = rhs._data[n];
|
||||
return *this;
|
||||
};
|
||||
~DCCPacket() {
|
||||
if (_len) {
|
||||
delete[]_data;
|
||||
_len = 0;
|
||||
_data = NULL;
|
||||
}
|
||||
};
|
||||
inline bool operator==(const DCCPacket &right) {
|
||||
if (_len != right._len)
|
||||
return false;
|
||||
if (_len == 0)
|
||||
return true;
|
||||
return (bcmp(_data, right._data, _len) == 0);
|
||||
};
|
||||
void print(HardwareSerial &s) {
|
||||
s.print("<* DCCPACKET ");
|
||||
for (byte n = 0; n< _len; n++) {
|
||||
s.print(_data[n], HEX);
|
||||
s.print(" ");
|
||||
}
|
||||
s.print("*>\n");
|
||||
};
|
||||
inline byte len() {return _len;};
|
||||
inline byte *data() {return _data;};
|
||||
private:
|
||||
byte _len = 0;
|
||||
byte *_data = NULL;
|
||||
};
|
||||
#endif
|
231
DCCQueue.cpp
231
DCCQueue.cpp
@ -1,231 +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/>.
|
||||
*/
|
||||
|
||||
/* What does this queue manager do:
|
||||
1. It provides a high priority queue and a low priority queue.
|
||||
2. It manages situations where multiple loco speed commands are in the queue.
|
||||
3. It allows an ESTOP to jump the queue and eliminate any outstanding speed commands that would later undo the stop.
|
||||
4. It allows for coil on/off accessory commands to be synchronized to a given time delay.
|
||||
5. It prevents transmission of sequential packets to the same loco id
|
||||
*/
|
||||
#include "Arduino.h"
|
||||
#include "defines.h"
|
||||
#include "DCCQueue.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
// create statics
|
||||
DCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue();
|
||||
DCCQueue* DCCQueue::highPriorityQueue=new DCCQueue();
|
||||
PendingSlot* DCCQueue::recycleList=nullptr;
|
||||
uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the same loco in a row
|
||||
|
||||
|
||||
DCCQueue::DCCQueue() {
|
||||
head=nullptr;
|
||||
tail=nullptr;
|
||||
}
|
||||
|
||||
void DCCQueue::addQueue(PendingSlot* p) {
|
||||
if (tail) tail->next=p;
|
||||
else head=p;
|
||||
tail=p;
|
||||
p->next=nullptr;
|
||||
}
|
||||
|
||||
void DCCQueue::jumpQueue(PendingSlot* p) {
|
||||
p->next=head;
|
||||
head=p;
|
||||
if (!tail) tail=p;
|
||||
}
|
||||
|
||||
|
||||
void DCCQueue::recycle(PendingSlot* p) {
|
||||
p->next=recycleList;
|
||||
recycleList=p;
|
||||
}
|
||||
|
||||
void DCCQueue::remove(PendingSlot* premove) {
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=head;p;previous=p,p=p->next) {
|
||||
if (p==premove) {
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else head=p->next;
|
||||
if (p==tail) tail=previous; // if last packet, update tail
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("DCCQueue::remove slot not found"));
|
||||
|
||||
}
|
||||
|
||||
// Packet joins end of low priority queue.
|
||||
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
|
||||
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
}
|
||||
|
||||
// Packet replaces existing loco speed packet or joins end of high priority queue.
|
||||
|
||||
void DCCQueue::scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) {
|
||||
if (p->locoId==loco) {
|
||||
// replace existing packet
|
||||
memcpy(p->packet,packet,length);
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=repeats;
|
||||
return;
|
||||
}
|
||||
}
|
||||
highPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
}
|
||||
|
||||
|
||||
// ESTOP -
|
||||
// any outstanding throttle packet for this loco (all if loco=0) discarded
|
||||
// Packet joins start of queue,
|
||||
|
||||
|
||||
void DCCQueue::scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco) {
|
||||
|
||||
// DIAG(F("DCC ESTOP loco=%d"),loco);
|
||||
|
||||
// kill any existing throttle packets for this loco (or all locos if broadcast)
|
||||
// this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
|
||||
PendingSlot * pNext;
|
||||
for (auto p=highPriorityQueue->head;p;p=pNext) {
|
||||
auto pNext=p->next; // save next packet in case we recycle this one
|
||||
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
|
||||
// remove this slot from the queue or it will interfere with our ESTOP
|
||||
highPriorityQueue->remove(p);
|
||||
recycle(p); // recycle this slot
|
||||
}
|
||||
}
|
||||
// add the estop packet to the start of the queue
|
||||
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
|
||||
}
|
||||
|
||||
// Accessory coil-On Packet joins end of queue as normal.
|
||||
// When dequeued, packet is retained at start of queue
|
||||
// but modified to coil-off and given the delayed start.
|
||||
// getNext will ignore this packet until the requested start time.
|
||||
void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {
|
||||
auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);
|
||||
p->delayOff=delayms;
|
||||
lowPriorityQueue->addQueue(p);
|
||||
};
|
||||
|
||||
|
||||
// Schedule the next dcc packet from the queues or an idle packet if none pending.
|
||||
const byte idlePacket[] = {0xFF, 0x00};
|
||||
|
||||
bool DCCQueue::scheduleNext(bool force) {
|
||||
if (highPriorityQueue->scheduleNextInternal()) return true;
|
||||
if (lowPriorityQueue->scheduleNextInternal()) return true;
|
||||
if (force) {
|
||||
// This will arise when there is nothing available to be sent that will not compromise the rules
|
||||
// typically this will only happen when there is only one loco in the reminders as the closely queued
|
||||
// speed and function reminders must be separated by at least one packet not sent to that loco.
|
||||
DCCWaveform::mainTrack.schedulePacket(idlePacket,sizeof(idlePacket),0);
|
||||
lastSentPacketLocoId=0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DCCQueue::scheduleNextInternal() {
|
||||
|
||||
for (auto p=head;p;p=p->next) {
|
||||
// skip over pending ACC_OFF packets which are still delayed
|
||||
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
|
||||
if (p->locoId) {
|
||||
// Prevent two consecutive packets to the same loco.
|
||||
// this also means repeats cant be done by waveform
|
||||
if (p->locoId==lastSentPacketLocoId) continue; // try again later
|
||||
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,0);
|
||||
lastSentPacketLocoId=p->locoId;
|
||||
if (p->packetRepeat) {
|
||||
p->packetRepeat--;
|
||||
return true; // leave this packet in the queue
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Non loco packets can repeat automatically
|
||||
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
|
||||
lastSentPacketLocoId=0;
|
||||
}
|
||||
|
||||
// remove this slot from the queue
|
||||
remove(p);
|
||||
|
||||
// special cases handling
|
||||
if (p->type == ACC_ON_PACKET) {
|
||||
// convert to a delayed off packet and jump the high priority queue
|
||||
p->type= ACC_OFF_PACKET;
|
||||
p->packet[1] &= ~0x08; // set C to 0 (gate off)
|
||||
p->startTime=millis()+p->delayOff;
|
||||
highPriorityQueue->jumpQueue(p);
|
||||
}
|
||||
else recycle(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No packets found
|
||||
return false;
|
||||
}
|
||||
|
||||
// obtain and initialise slot for a PendingSlot.
|
||||
PendingSlot* DCCQueue::getSlot(PendingType type, byte* packet, byte length, byte repeats,uint16_t loco) {
|
||||
PendingSlot * p;
|
||||
if (recycleList) {
|
||||
p=recycleList;
|
||||
recycleList=p->next;
|
||||
}
|
||||
else {
|
||||
static int16_t created=0;
|
||||
int16_t q1=0;
|
||||
int16_t q2=0;
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) q1++;
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
|
||||
bool leak=(q1+q2)!=created;
|
||||
DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
|
||||
(int16_t)type,length,loco,q1,q2, created);
|
||||
if (leak) {
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
|
||||
}
|
||||
p=new PendingSlot; // need a queue entry
|
||||
created++;
|
||||
}
|
||||
p->next=nullptr;
|
||||
p->type=type;
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=repeats;
|
||||
if (length>sizeof(p->packet)) {
|
||||
DIAG(F("DCC bad packet length=%d"),length);
|
||||
length=sizeof(p->packet); // limit to size of packet
|
||||
}
|
||||
p->startTime=0; // not used for loco packets
|
||||
memcpy((void*)p->packet,packet,length);
|
||||
p->locoId=loco;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
83
DCCQueue.h
83
DCCQueue.h
@ -1,83 +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/>.
|
||||
*/
|
||||
|
||||
#ifndef DCCQueue_h
|
||||
#define DCCQueue_h
|
||||
#include "Arduino.h"
|
||||
#include "DCCWaveform.h"
|
||||
|
||||
enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
|
||||
struct PendingSlot {
|
||||
PendingSlot* next;
|
||||
PendingType type;
|
||||
byte packetLength;
|
||||
byte packetRepeat;
|
||||
byte packet[MAX_PACKET_SIZE];
|
||||
|
||||
union { // use depends on packet type
|
||||
uint16_t locoId; // SPEED & FUNCTION packets
|
||||
uint16_t delayOff; // ACC_ON_PACKET delay to apply between on/off
|
||||
uint32_t startTime; // ACC_OFF_PACKET time (mS) to transmit
|
||||
};
|
||||
};
|
||||
|
||||
class DCCQueue {
|
||||
public:
|
||||
|
||||
|
||||
// Non-speed packets are queued in the main queue
|
||||
static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);
|
||||
|
||||
// Speed packets are queued in the high priority queue
|
||||
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
|
||||
|
||||
// ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco
|
||||
static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco);
|
||||
|
||||
// Accessory gate-On Packet joins end of main queue as normal.
|
||||
// When dequeued, packet is modified to gate-off and given the delayed start in the high priority queue.
|
||||
// getNext will ignore this packet until the requested start time.
|
||||
static void scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms);
|
||||
|
||||
|
||||
// Schedules a main track packet from the queues.
|
||||
static bool scheduleNext(bool force);
|
||||
|
||||
private:
|
||||
bool scheduleNextInternal();
|
||||
// statics to manage high and low priority queues and recycleing of PENDINGs
|
||||
static PendingSlot* recycleList;
|
||||
static DCCQueue* highPriorityQueue;
|
||||
static DCCQueue* lowPriorityQueue;
|
||||
static uint16_t lastSentPacketLocoId; // used to prevent two packets to the same loco in a row
|
||||
|
||||
DCCQueue();
|
||||
|
||||
PendingSlot* head;
|
||||
PendingSlot * tail;
|
||||
|
||||
// obtain and initialise slot for a PendingSlot.
|
||||
static PendingSlot* getSlot(PendingType type, byte* packet, byte length, byte repeats, uint16_t loco);
|
||||
static void recycle(PendingSlot* p);
|
||||
void addQueue(PendingSlot * p);
|
||||
void jumpQueue(PendingSlot * p);
|
||||
void remove(PendingSlot * p);
|
||||
};
|
||||
#endif // DCCQueue_h
|
@ -2,7 +2,7 @@
|
||||
* © 2021 Mike S
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2021-2025 Chris Harlow
|
||||
* © 2021 Chris Harlow
|
||||
* © 2021 David Cutting
|
||||
* All rights reserved.
|
||||
*
|
||||
@ -57,59 +57,66 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
|
||||
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
|
||||
interrupts();
|
||||
//diagnostic pinMode(4,OUTPUT);
|
||||
}
|
||||
|
||||
|
||||
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||
(void) brakePin; // Ignored... works on pin 9 only
|
||||
// diagnostic digitalWrite(4,HIGH);
|
||||
|
||||
/* The Railcom timer is started in such a way that it
|
||||
- First triggers 58+29 uS after the previous TIMER1 tick.
|
||||
- First triggers 28uS after the last TIMER1 tick.
|
||||
This provides an accurate offset (in High Accuracy mode)
|
||||
for the start of the Railcom cutout.
|
||||
- Sets the Railcom pin high at first tick and subsequent ticks
|
||||
until its reset to setting pin 9 low at next tick.
|
||||
- Sets the Railcom pin high at first tick,
|
||||
because its been setup with 100% PWM duty cycle.
|
||||
|
||||
- Cycles at 436uS so the second tick is the
|
||||
correct distance from the cutout.
|
||||
|
||||
- Waveform code is responsible for resetting
|
||||
any time between the first and second tick.
|
||||
- Waveform code is responsible for altering the PWM
|
||||
duty cycle to 0% any time between the first and last tick.
|
||||
(there will be 7 DCC timer1 ticks in which to do this.)
|
||||
|
||||
*/
|
||||
(void) brakePin; // Ignored... works on pin 9 only
|
||||
const int cutoutDuration = 430; // Desired interval in microseconds
|
||||
const int cycle=cutoutDuration/2;
|
||||
|
||||
const byte RailcomFudge0=58+58+29;
|
||||
|
||||
// Set Timer2 to CTC mode with set on compare match
|
||||
TCCR2A = (1 << WGM21) | (1 << COM2B0) | (1 << COM2B1);
|
||||
// Prescaler of 32
|
||||
TCCR2B = (1 << CS21) | (1 << CS20);
|
||||
OCR2A = cycle-1; // Compare match value for 430 uS
|
||||
// Set up Timer2 for CTC mode (Clear Timer on Compare Match)
|
||||
TCCR2A = 0; // Clear Timer2 control register A
|
||||
TCCR2B = 0; // Clear Timer2 control register B
|
||||
TCNT2 = 0; // Initialize Timer2 counter value to 0
|
||||
// Configure Phase and Frequency Correct PWM mode
|
||||
TCCR2A = (1 << COM2B1); // enable pwm on pin 9
|
||||
TCCR2A |= (1 << WGM20);
|
||||
|
||||
|
||||
// Set Timer 2 prescaler to 32
|
||||
TCCR2B = (1 << CS21) | (1 << CS20); // 32 prescaler
|
||||
|
||||
// Set the compare match value for desired interval
|
||||
OCR2A = (F_CPU / 1000000) * cutoutDuration / 64 - 1;
|
||||
|
||||
// Calculate the compare match value for desired duty cycle
|
||||
OCR2B = OCR2A+1; // set duty cycle to 100%= OCR2A)
|
||||
|
||||
// Enable Timer2 output on pin 9 (OC2B)
|
||||
DDRB |= (1 << DDB1);
|
||||
// TODO Fudge TCNT2 to sync with last tcnt1 tick + 28uS
|
||||
|
||||
// RailcomFudge2 is the expected time from idealised
|
||||
// setup call (at previous DCC timer interrupt) to the cutout.
|
||||
// This value should be reduced to reflect the Timer1 value
|
||||
// measuring the time since the previous hardware interrupt
|
||||
byte tcfudge=TCNT1/16;
|
||||
TCNT2=cycle-RailcomFudge0/2+tcfudge/2;
|
||||
|
||||
|
||||
// Previous TIMER1 Tick was at rising end-of-packet bit
|
||||
// Cutout starts half way through first preamble
|
||||
// that is 2.5 * 58uS later.
|
||||
}
|
||||
// TCNT1 ticks 8 times / microsecond
|
||||
// auto microsendsToFirstRailcomTick=(58+58+29)-(TCNT1/8);
|
||||
// set the railcom timer counter allowing for phase-correct
|
||||
|
||||
// CHris's NOTE:
|
||||
// I dont kniow quite how this calculation works out but
|
||||
// it does seems to get a good answer.
|
||||
|
||||
TCNT2=193 + (ICR1 - TCNT1)/8;
|
||||
}
|
||||
|
||||
void DCCTimer::ackRailcomTimer() {
|
||||
// Change Timer2 to CTC mode with RESET pin 9 on next compare match
|
||||
TCCR2A = (1 << WGM21) | (1 << COM2B1);
|
||||
// diagnostic digitalWrite(4,LOW);
|
||||
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
|
||||
}
|
||||
|
||||
|
||||
|
150
DCCWaveform.cpp
150
DCCWaveform.cpp
@ -24,13 +24,14 @@
|
||||
#ifndef ARDUINO_ARCH_ESP32
|
||||
// This code is replaced entirely on an ESP32
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "DCCWaveform.h"
|
||||
#include "TrackManager.h"
|
||||
#include "DCCTimer.h"
|
||||
#include "DCCACK.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
bool DCCWaveform::cutoutNextTime=false;
|
||||
|
||||
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
||||
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
||||
|
||||
@ -70,18 +71,9 @@ void DCCWaveform::loop() {
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
|
||||
void DCCWaveform::interruptHandler() {
|
||||
// call the timer edge sensitive actions for progtrack and maintrack
|
||||
// member functions would be cleaner but have more overhead
|
||||
#if defined(HAS_ENOUGH_MEMORY)
|
||||
if (cutoutNextTime) {
|
||||
cutoutNextTime=false;
|
||||
railcomSampleWindow=false; // about to cutout, stop reading railcom data.
|
||||
railcomCutoutCounter++;
|
||||
DCCTimer::startRailcomTimer(9);
|
||||
}
|
||||
#endif
|
||||
byte sigMain=signalTransform[mainTrack.state];
|
||||
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
|
||||
|
||||
@ -123,24 +115,19 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
||||
bytes_sent = 0;
|
||||
bits_sent = 0;
|
||||
}
|
||||
|
||||
bool DCCWaveform::railcomPossible=false; // High accuracy only
|
||||
|
||||
volatile bool DCCWaveform::railcomActive=false; // switched on by user
|
||||
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
|
||||
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
|
||||
volatile byte DCCWaveform::railcomCutoutCounter=0; // cyclic cutout
|
||||
volatile byte DCCWaveform::railcomLastAddressHigh=0;
|
||||
volatile byte DCCWaveform::railcomLastAddressLow=0;
|
||||
|
||||
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
||||
if (on && railcomPossible) {
|
||||
if (on) {
|
||||
// TODO check possible
|
||||
railcomActive=true;
|
||||
railcomDebug=debug;
|
||||
}
|
||||
else {
|
||||
railcomActive=false;
|
||||
railcomDebug=false;
|
||||
railcomSampleWindow=false;
|
||||
}
|
||||
return railcomActive;
|
||||
}
|
||||
@ -153,37 +140,14 @@ void DCCWaveform::interrupt2() {
|
||||
// or WAVE_HIGH_0 for a 0 bit.
|
||||
if (remainingPreambles > 0 ) {
|
||||
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
|
||||
|
||||
remainingPreambles--;
|
||||
|
||||
// As we get to the end of the preambles, open the reminder window.
|
||||
// This delays any reminder insertion until the last moment so
|
||||
// that the reminder doesn't block a more urgent packet.
|
||||
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
|
||||
if (remainingPreambles==1)
|
||||
promotePendingPacket();
|
||||
|
||||
#if defined(HAS_ENOUGH_MEMORY)
|
||||
else if (isMainTrack && railcomActive) {
|
||||
if (remainingPreambles==(requiredPreambles-1)) {
|
||||
// First look if we need to start a railcom cutout on next interrupt
|
||||
cutoutNextTime= true;
|
||||
} else if (remainingPreambles==(requiredPreambles-12)) {
|
||||
// cutout has ended so its now possible to poll the railcom detectors
|
||||
// requiredPreambles is one higher that preamble length so
|
||||
// if preamble length is 16 then this evaluates to 5
|
||||
// Remember address bytes of last sent packet so that Railcom can
|
||||
// work out where the channel2 data came from.
|
||||
railcomLastAddressHigh=transmitPacket[0];
|
||||
railcomLastAddressLow =transmitPacket[1];
|
||||
railcomSampleWindow=true;
|
||||
} else if (remainingPreambles==(requiredPreambles-3)) {
|
||||
// cutout can be ended when read
|
||||
// see above for requiredPreambles
|
||||
DCCTimer::ackRailcomTimer();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (remainingPreambles==1) promotePendingPacket();
|
||||
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
|
||||
// Update free memory diagnostic as we don't have anything else to do this time.
|
||||
// Allow for checkAck and its called functions using 22 bytes more.
|
||||
else DCCTimer::updateMinimumFreeMemoryISR(22);
|
||||
@ -207,7 +171,13 @@ void DCCWaveform::interrupt2() {
|
||||
bytes_sent = 0;
|
||||
// preamble for next packet will start...
|
||||
remainingPreambles = requiredPreambles;
|
||||
}
|
||||
|
||||
// set the railcom coundown to trigger half way
|
||||
// through the first preamble bit.
|
||||
// Note.. we are still sending the last packet bit
|
||||
// and we then have to allow for the packet end bit
|
||||
if (isMainTrack && railcomActive) DCCTimer::startRailcomTimer(9);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma GCC pop_options
|
||||
@ -242,7 +212,7 @@ void DCCWaveform::promotePendingPacket() {
|
||||
transmitRepeats--;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (packetPending) {
|
||||
// Copy pending packet to transmit packet
|
||||
// a fixed length memcpy is faster than a variable length loop for these small lengths
|
||||
@ -260,7 +230,7 @@ void DCCWaveform::promotePendingPacket() {
|
||||
// Fortunately reset and idle packets are the same length
|
||||
// Note: If railcomDebug is on, then we send resets to the main
|
||||
// track instead of idles. This means that all data will be zeros
|
||||
// and only the presets will be ones, making it much
|
||||
// and only the porersets will be ones, making it much
|
||||
// easier to read on a logic analyser.
|
||||
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
|
||||
transmitLength = sizeof(idlePacket);
|
||||
@ -268,3 +238,91 @@ void DCCWaveform::promotePendingPacket() {
|
||||
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "DCCWaveform.h"
|
||||
#include "DCCACK.h"
|
||||
|
||||
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
||||
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
||||
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
|
||||
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
|
||||
|
||||
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
|
||||
isMainTrack = isMain;
|
||||
requiredPreambles = preambleBits;
|
||||
}
|
||||
void DCCWaveform::begin() {
|
||||
for(const auto& md: TrackManager::getMainDrivers()) {
|
||||
pinpair p = md->getSignalPin();
|
||||
if(rmtMainChannel) {
|
||||
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
|
||||
rmtMainChannel->addPin(p); // add pin to existing main channel
|
||||
} else {
|
||||
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
|
||||
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
|
||||
}
|
||||
}
|
||||
MotorDriver *md = TrackManager::getProgDriver();
|
||||
if (md) {
|
||||
pinpair p = md->getSignalPin();
|
||||
if (rmtProgChannel) {
|
||||
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
|
||||
rmtProgChannel->addPin(p); // add pin to existing prog channel
|
||||
} else {
|
||||
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
|
||||
rmtProgChannel = new RMTChannel(p, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
||||
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
||||
RMTChannel *rmtchannel = (isMainTrack ? rmtMainChannel : rmtProgChannel);
|
||||
if (rmtchannel == NULL)
|
||||
return; // no idea to prepare packet if we can not send it anyway
|
||||
|
||||
rmtchannel->waitForDataCopy(); // blocking wait so we can write into buffer
|
||||
byte checksum = 0;
|
||||
for (byte b = 0; b < byteCount; b++) {
|
||||
checksum ^= buffer[b];
|
||||
pendingPacket[b] = buffer[b];
|
||||
}
|
||||
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
|
||||
pendingPacket[byteCount] = checksum;
|
||||
pendingLength = byteCount + 1;
|
||||
pendingRepeats = repeats;
|
||||
// DIAG repeated commands (accesories)
|
||||
// if (pendingRepeats > 0)
|
||||
// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG");
|
||||
// The resets will be zero not only now but as well repeats packets into the future
|
||||
clearResets(repeats+1);
|
||||
{
|
||||
int ret = 0;
|
||||
do {
|
||||
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
||||
} while(ret > 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool DCCWaveform::isReminderWindowOpen() {
|
||||
if(isMainTrack) {
|
||||
if (rmtMainChannel == NULL)
|
||||
return false;
|
||||
return !rmtMainChannel->busy();
|
||||
} else {
|
||||
if (rmtProgChannel == NULL)
|
||||
return false;
|
||||
return !rmtProgChannel->busy();
|
||||
}
|
||||
}
|
||||
void IRAM_ATTR DCCWaveform::loop() {
|
||||
DCCACK::checkAck(progTrack.getResets());
|
||||
}
|
||||
|
||||
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
||||
// TODO... ESP32 railcom waveform
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -3,7 +3,7 @@
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2024 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@ -23,8 +23,11 @@
|
||||
*/
|
||||
#ifndef DCCWaveform_h
|
||||
#define DCCWaveform_h
|
||||
|
||||
#include "MotorDriver.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "DCCRMT.h"
|
||||
#include "TrackManager.h"
|
||||
#endif
|
||||
|
||||
|
||||
@ -83,30 +86,8 @@ class DCCWaveform {
|
||||
bool isReminderWindowOpen();
|
||||
void promotePendingPacket();
|
||||
static bool setRailcom(bool on, bool debug);
|
||||
inline static bool isRailcom() {
|
||||
return railcomActive;
|
||||
};
|
||||
inline static byte getRailcomCutoutCounter() {
|
||||
return railcomCutoutCounter;
|
||||
};
|
||||
inline static bool isRailcomSampleWindow() {
|
||||
return railcomSampleWindow;
|
||||
};
|
||||
inline static bool isRailcomPossible() {
|
||||
return railcomPossible;
|
||||
};
|
||||
inline static void setRailcomPossible(bool yes) {
|
||||
railcomPossible=yes;
|
||||
if (!yes) setRailcom(false,false);
|
||||
};
|
||||
inline static uint16_t getRailcomLastLocoAddress() {
|
||||
// first 2 bits 00=short loco, 11=long loco , 01/10 = accessory
|
||||
byte addressType=railcomLastAddressHigh & 0xC0;
|
||||
if (addressType==0xC0) return ((railcomLastAddressHigh & 0x3f)<<8) | railcomLastAddressLow;
|
||||
if (addressType==0x00) return railcomLastAddressHigh & 0x3F;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool isRailcom() {return railcomActive;}
|
||||
|
||||
private:
|
||||
#ifndef ARDUINO_ARCH_ESP32
|
||||
volatile bool packetPending;
|
||||
@ -131,13 +112,9 @@ class DCCWaveform {
|
||||
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
|
||||
byte pendingLength;
|
||||
byte pendingRepeats;
|
||||
static bool railcomPossible; // High accuracy mode only
|
||||
static volatile bool railcomActive; // switched on by user
|
||||
static volatile bool railcomDebug; // switched on by user
|
||||
static volatile bool railcomSampleWindow; // when safe to sample
|
||||
static volatile byte railcomCutoutCounter; // incremented for each cutout
|
||||
static volatile byte railcomLastAddressHigh,railcomLastAddressLow;
|
||||
static bool cutoutNextTime; // railcom
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
static RMTChannel *rmtMainChannel;
|
||||
static RMTChannel *rmtProgChannel;
|
||||
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 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 code is ESP32 ONLY.
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "DCCWaveform.h"
|
||||
#include "DCCACK.h"
|
||||
#include "TrackManager.h"
|
||||
|
||||
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
||||
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
||||
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
|
||||
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
|
||||
|
||||
bool DCCWaveform::railcomPossible=false; // High accuracy only
|
||||
volatile bool DCCWaveform::railcomActive=false; // switched on by user
|
||||
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
|
||||
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
|
||||
volatile byte DCCWaveform::railcomCutoutCounter=0; // cyclic cutout
|
||||
volatile byte DCCWaveform::railcomLastAddressHigh=0;
|
||||
volatile byte DCCWaveform::railcomLastAddressLow=0;
|
||||
|
||||
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
|
||||
isMainTrack = isMain;
|
||||
requiredPreambles = preambleBits;
|
||||
}
|
||||
void DCCWaveform::begin() {
|
||||
for(const auto& md: TrackManager::getMainDrivers()) {
|
||||
pinpair p = md->getSignalPin();
|
||||
if(rmtMainChannel) {
|
||||
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
|
||||
rmtMainChannel->addPin(p); // add pin to existing main channel
|
||||
} else {
|
||||
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
|
||||
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
|
||||
}
|
||||
}
|
||||
MotorDriver *md = TrackManager::getProgDriver();
|
||||
if (md) {
|
||||
pinpair p = md->getSignalPin();
|
||||
if (rmtProgChannel) {
|
||||
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
|
||||
rmtProgChannel->addPin(p); // add pin to existing prog channel
|
||||
} else {
|
||||
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
|
||||
rmtProgChannel = new RMTChannel(p, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
||||
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
||||
RMTChannel *rmtchannel = (isMainTrack ? rmtMainChannel : rmtProgChannel);
|
||||
if (rmtchannel == NULL)
|
||||
return; // no idea to prepare packet if we can not send it anyway
|
||||
|
||||
rmtchannel->waitForDataCopy(); // blocking wait so we can write into buffer
|
||||
byte checksum = 0;
|
||||
for (byte b = 0; b < byteCount; b++) {
|
||||
checksum ^= buffer[b];
|
||||
pendingPacket[b] = buffer[b];
|
||||
}
|
||||
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
|
||||
pendingPacket[byteCount] = checksum;
|
||||
pendingLength = byteCount + 1;
|
||||
pendingRepeats = repeats;
|
||||
// DIAG repeated commands (accesories)
|
||||
// if (pendingRepeats > 0)
|
||||
// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG");
|
||||
// The resets will be zero not only now but as well repeats packets into the future
|
||||
clearResets(repeats+1);
|
||||
{
|
||||
int ret = 0;
|
||||
do {
|
||||
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
||||
} while(ret > 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool DCCWaveform::isReminderWindowOpen() {
|
||||
if(isMainTrack) {
|
||||
if (rmtMainChannel == NULL)
|
||||
return false;
|
||||
return !rmtMainChannel->busy();
|
||||
} else {
|
||||
if (rmtProgChannel == NULL)
|
||||
return false;
|
||||
return !rmtProgChannel->busy();
|
||||
}
|
||||
}
|
||||
void IRAM_ATTR DCCWaveform::loop() {
|
||||
DCCACK::checkAck(progTrack.getResets());
|
||||
}
|
||||
|
||||
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
||||
// TODO... ESP32 railcom waveform
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
230
EXRAIL2.cpp
230
EXRAIL2.cpp
@ -2,7 +2,7 @@
|
||||
* © 2024 Paul M. Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2023 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2025 Morten Nielsen
|
||||
* All rights reserved.
|
||||
@ -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.
|
||||
@ -89,11 +88,14 @@ LookList * RMFT2::onClockLookup=NULL;
|
||||
LookList * RMFT2::onRotateLookup=NULL;
|
||||
#endif
|
||||
LookList * RMFT2::onOverloadLookup=NULL;
|
||||
LookList * RMFT2::onBlockEnterLookup=NULL;
|
||||
LookList * RMFT2::onBlockExitLookup=NULL;
|
||||
#ifdef BOOSTER_INPUT
|
||||
LookList * RMFT2::onRailSyncOnLookup=NULL;
|
||||
LookList * RMFT2::onRailSyncOffLookup=NULL;
|
||||
#endif
|
||||
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) {
|
||||
@ -134,11 +136,11 @@ int16_t LookList::find(int16_t value) {
|
||||
void LookList::chain(LookList * chain) {
|
||||
m_chain=chain;
|
||||
}
|
||||
void LookList::handleEvent(const FSH* reason,int16_t id, int16_t loco) {
|
||||
void LookList::handleEvent(const FSH* reason,int16_t id) {
|
||||
// New feature... create multiple ONhandlers
|
||||
for (int i=0;i<m_size;i++)
|
||||
if (m_lookupArray[i]==id)
|
||||
RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i],loco);
|
||||
RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i]);
|
||||
}
|
||||
|
||||
|
||||
@ -206,12 +208,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
|
||||
#endif
|
||||
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
|
||||
|
||||
if (compileFeatures & FEATURE_BLOCK) {
|
||||
onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);
|
||||
onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);
|
||||
}
|
||||
|
||||
#ifdef BOOSTER_INPUT
|
||||
onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
|
||||
onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
|
||||
#endif
|
||||
// onLCCLookup is not the same so not loaded here.
|
||||
|
||||
// Second pass startup, define any turnouts or servos, set signals red
|
||||
@ -258,13 +258,17 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
IODevice::configureInput((VPIN)pin,true);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_STASH:
|
||||
case OPCODE_CLEAR_STASH:
|
||||
case OPCODE_PICKUP_STASH: {
|
||||
maxStashId=max(maxStashId,((int16_t)operand));
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_ATGTE:
|
||||
case OPCODE_ATLT:
|
||||
case OPCODE_IFGTE:
|
||||
case OPCODE_IFLT:
|
||||
case OPCODE_IFBITMAP_ALL:
|
||||
case OPCODE_IFBITMAP_ANY:
|
||||
case OPCODE_DRIVE: {
|
||||
DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand);
|
||||
IODevice::configureAnalogIn((VPIN)operand);
|
||||
@ -275,10 +279,6 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,true );
|
||||
break;
|
||||
case OPCODE_ONBITMAP:
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,true, true );
|
||||
break;
|
||||
case OPCODE_ONBUTTON:
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,false );
|
||||
@ -352,7 +352,13 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
}
|
||||
SKIPOP; // include ENDROUTES opcode
|
||||
|
||||
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS);
|
||||
if (compileFeatures & FEATURE_STASH) {
|
||||
// create the stash array from the highest id found
|
||||
if (maxStashId>0) stashArray=(int16_t*)calloc(maxStashId+1, sizeof(int16_t));
|
||||
//TODO check EEPROM and fetch stashArray
|
||||
}
|
||||
|
||||
DIAG(F("EXRAIL %db, fl=%d, stash=%d"),progCounter,MAX_FLAGS, maxStashId);
|
||||
|
||||
// Removed for 4.2.31 new RMFT2(0); // add the startup route
|
||||
diag=saved_diag;
|
||||
@ -382,7 +388,7 @@ char RMFT2::getRouteType(int16_t id) {
|
||||
}
|
||||
|
||||
|
||||
RMFT2::RMFT2(int progCtr, int16_t _loco) {
|
||||
RMFT2::RMFT2(int progCtr) {
|
||||
progCounter=progCtr;
|
||||
|
||||
// get an unused task id from the flags table
|
||||
@ -395,7 +401,9 @@ RMFT2::RMFT2(int progCtr, int16_t _loco) {
|
||||
}
|
||||
}
|
||||
delayTime=0;
|
||||
loco=_loco;
|
||||
loco=0;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
blinkState=not_blink_task;
|
||||
stackDepth=0;
|
||||
@ -413,10 +421,7 @@ RMFT2::RMFT2(int progCtr, int16_t _loco) {
|
||||
|
||||
|
||||
RMFT2::~RMFT2() {
|
||||
// estop my loco if this is not an ONevent
|
||||
// (prevents DONE stopping loco at the end of an
|
||||
// ONBLOCKENTER or ONBLOCKEXIT )
|
||||
if (loco>0 && this->onEventStartPosition==-1) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));
|
||||
driveLoco(1); // ESTOP my loco if any
|
||||
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
|
||||
if (next==this)
|
||||
loopTask=NULL;
|
||||
@ -432,9 +437,23 @@ RMFT2::~RMFT2() {
|
||||
void RMFT2::createNewTask(int route, uint16_t cab) {
|
||||
int pc=routeLookup->find(route);
|
||||
if (pc<0) return;
|
||||
new RMFT2(pc,cab);
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
|
||||
void RMFT2::driveLoco(byte speed) {
|
||||
if (loco<=0) return; // Prevent broadcast!
|
||||
//if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
|
||||
/* TODO.....
|
||||
power on appropriate track if DC or main if dcc
|
||||
if (TrackManager::getMainPowerMode()==POWERMODE::OFF) {
|
||||
TrackManager::setMainPower(POWERMODE::ON);
|
||||
}
|
||||
**********/
|
||||
|
||||
DCC::setThrottle(loco,speed, forward^invert);
|
||||
speedo=speed;
|
||||
}
|
||||
|
||||
bool RMFT2::readSensor(uint16_t sensorId) {
|
||||
// Exrail operands are unsigned but we need the signed version as inserted by the macros.
|
||||
@ -489,7 +508,7 @@ bool RMFT2::skipIfBlock() {
|
||||
if (cv & LONG_ADDR_MARKER) { // maker bit indicates long addr
|
||||
progtrackLocoId = cv ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr
|
||||
if (progtrackLocoId <= HIGHEST_SHORT_ADDR ) { // out of range for long addr
|
||||
DIAG(F("Long addr %d <= %d unsupported\n"), progtrackLocoId, HIGHEST_SHORT_ADDR);
|
||||
DIAG(F("Long addr %d <= %d unsupported"), progtrackLocoId, HIGHEST_SHORT_ADDR);
|
||||
progtrackLocoId = -1;
|
||||
}
|
||||
} else {
|
||||
@ -497,15 +516,6 @@ bool RMFT2::skipIfBlock() {
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::pause() {
|
||||
if (loco)
|
||||
pauseSpeed=DCC::getThrottleSpeedByte(loco);
|
||||
}
|
||||
void RMFT2::resume() {
|
||||
if (loco)
|
||||
DCC::setThrottle(loco,pauseSpeed & 0x7f, pauseSpeed & 0x80);
|
||||
}
|
||||
|
||||
void RMFT2::loop() {
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
EXRAILSensor::checkAll();
|
||||
@ -570,23 +580,18 @@ void RMFT2::loop2() {
|
||||
#endif
|
||||
|
||||
case OPCODE_REV:
|
||||
if (loco) DCC::setThrottle(loco,operand,invert);
|
||||
forward = false;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_FWD:
|
||||
if (loco) DCC::setThrottle(loco,operand,!invert);
|
||||
break;
|
||||
forward = true;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_SPEED:
|
||||
if (loco) DCC::setThrottle(loco,operand,DCC::getThrottleDirection(loco));
|
||||
break;
|
||||
|
||||
case OPCODE_MOMENTUM:
|
||||
DCC::setMomentum(loco,operand,getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_ESTOPALL:
|
||||
DCC::setThrottle(0,1,1); // all locos speed=1
|
||||
forward=DCC::getThrottleDirection(loco)^invert;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_FORGET:
|
||||
@ -598,11 +603,12 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_INVERT_DIRECTION:
|
||||
invert= !invert;
|
||||
driveLoco(speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_RESERVE:
|
||||
if (getFlag(operand,SECTION_FLAG)) {
|
||||
if (loco) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));
|
||||
driveLoco(0);
|
||||
delayMe(500);
|
||||
return;
|
||||
}
|
||||
@ -701,7 +707,7 @@ void RMFT2::loop2() {
|
||||
break;
|
||||
|
||||
case OPCODE_PAUSE:
|
||||
DCC::estopAll(); // pause all locos on the track
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
pausingTask=this;
|
||||
break;
|
||||
|
||||
@ -709,10 +715,6 @@ void RMFT2::loop2() {
|
||||
if (loco) DCC::writeCVByteMain(loco, operand, getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_XPOM:
|
||||
DCC::writeCVByteMain(operand, getOperand(1), getOperand(2));
|
||||
break;
|
||||
|
||||
case OPCODE_POWEROFF:
|
||||
TrackManager::setPower(POWERMODE::OFF);
|
||||
TrackManager::setJoin(false);
|
||||
@ -749,8 +751,8 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_RESUME:
|
||||
pausingTask=NULL;
|
||||
resume();
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) t->resume();
|
||||
driveLoco(speedo);
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_IF: // do next operand if sensor set
|
||||
@ -768,14 +770,6 @@ void RMFT2::loop2() {
|
||||
case OPCODE_IFLT: // do next operand if sensor< value
|
||||
skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_IFBITMAP_ALL: // do next operand if sensor & mask == mask
|
||||
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) != getOperand(1);
|
||||
break;
|
||||
|
||||
case OPCODE_IFBITMAP_ANY: // do next operand if sensor & mask !=0
|
||||
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) == 0;
|
||||
break;
|
||||
|
||||
case OPCODE_IFLOCO: // do if the loco is the active one
|
||||
skipIf=loco!=(uint16_t)operand; // bad luck if someone enters negative loco numbers into EXRAIL
|
||||
@ -817,10 +811,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
|
||||
@ -873,7 +863,8 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_DRIVE:
|
||||
{
|
||||
// Non functional but reserved
|
||||
byte analogSpeed=IODevice::readAnalogue(operand) *127 / 1024;
|
||||
if (speedo!=analogSpeed) driveLoco(analogSpeed);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -972,6 +963,8 @@ void RMFT2::loop2() {
|
||||
// which is intended so it can be checked
|
||||
// from within EXRAIL
|
||||
loco=progtrackLocoId;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
break;
|
||||
#endif
|
||||
@ -993,13 +986,16 @@ void RMFT2::loop2() {
|
||||
{
|
||||
int newPc=routeLookup->find(getOperand(1));
|
||||
if (newPc<0) break;
|
||||
new RMFT2(newPc,operand); // create new task
|
||||
RMFT2* newtask=new RMFT2(newPc); // create new task
|
||||
newtask->loco=operand;
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SETLOCO:
|
||||
{
|
||||
loco=operand;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
}
|
||||
break;
|
||||
@ -1074,32 +1070,30 @@ void RMFT2::loop2() {
|
||||
break;
|
||||
|
||||
case OPCODE_STASH:
|
||||
Stash::set(operand,invert? -loco : loco);
|
||||
if (compileFeatures & FEATURE_STASH)
|
||||
stashArray[operand] = invert? -loco : loco;
|
||||
break;
|
||||
|
||||
case OPCODE_CLEAR_STASH:
|
||||
Stash::clear(operand);
|
||||
if (compileFeatures & FEATURE_STASH)
|
||||
stashArray[operand] = 0;
|
||||
break;
|
||||
|
||||
case OPCODE_CLEAR_ALL_STASH:
|
||||
Stash::clearAll();
|
||||
break;
|
||||
|
||||
case OPCODE_CLEAR_ANY_STASH:
|
||||
if (loco) Stash::clearAny(loco);
|
||||
if (compileFeatures & FEATURE_STASH)
|
||||
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
|
||||
break;
|
||||
|
||||
case OPCODE_PICKUP_STASH:
|
||||
{
|
||||
auto x=Stash::get(operand);
|
||||
if (x>=0) {
|
||||
loco=x;
|
||||
invert=false;
|
||||
}
|
||||
else {
|
||||
loco=-x;
|
||||
invert=true;
|
||||
}
|
||||
if (compileFeatures & FEATURE_STASH) {
|
||||
int16_t x=stashArray[operand];
|
||||
if (x>=0) {
|
||||
loco=x;
|
||||
invert=false;
|
||||
break;
|
||||
}
|
||||
loco=-x;
|
||||
invert=true;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -1108,27 +1102,7 @@ void RMFT2::loop2() {
|
||||
case OPCODE_SEQUENCE:
|
||||
//if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
|
||||
break;
|
||||
|
||||
case OPCODE_BITMAP_INC:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand)+1);
|
||||
break;
|
||||
case OPCODE_BITMAP_DEC:
|
||||
{ int newval=IODevice::readAnalogue(operand)-1;
|
||||
if (newval<0) newval=0;
|
||||
IODevice::writeAnalogue(operand,newval);
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_BITMAP_AND:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) & getOperand(1));
|
||||
break;
|
||||
case OPCODE_BITMAP_OR:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) | getOperand(1));
|
||||
break;
|
||||
case OPCODE_BITMAP_XOR:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) ^ getOperand(1));
|
||||
break;
|
||||
|
||||
|
||||
case OPCODE_AUTOSTART: // Handled only during begin process
|
||||
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte.
|
||||
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
|
||||
@ -1148,7 +1122,6 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONTIME:
|
||||
case OPCODE_ONBUTTON:
|
||||
case OPCODE_ONSENSOR:
|
||||
case OPCODE_ONBITMAP:
|
||||
#ifndef IO_NO_HAL
|
||||
case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime
|
||||
case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime
|
||||
@ -1156,8 +1129,11 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONROTATE:
|
||||
#endif
|
||||
case OPCODE_ONOVERLOAD:
|
||||
case OPCODE_ONBLOCKENTER:
|
||||
case OPCODE_ONBLOCKEXIT:
|
||||
#ifdef BOOSTER_INPUT
|
||||
case OPCODE_ONRAILSYNCON:
|
||||
case OPCODE_ONRAILSYNCOFF:
|
||||
#endif
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -1349,14 +1325,6 @@ void RMFT2::activateEvent(int16_t addr, bool activate) {
|
||||
else onDeactivateLookup->handleEvent(F("DEACTIVATE"),addr);
|
||||
}
|
||||
|
||||
void RMFT2::blockEvent(int16_t block, int16_t loco, bool entering) {
|
||||
if (compileFeatures & FEATURE_BLOCK) {
|
||||
// Hunt for an ONBLOCKENTER/ONBLOCKEXIT for this accessory
|
||||
if (entering) onBlockEnterLookup->handleEvent(F("BLOCKENTER"),block,loco);
|
||||
else onBlockExitLookup->handleEvent(F("BLOCKEXIT"),block,loco);
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::changeEvent(int16_t vpin, bool change) {
|
||||
// Hunt for an ONCHANGE for this sensor
|
||||
if (change) onChangeLookup->handleEvent(F("CHANGE"),vpin);
|
||||
@ -1387,7 +1355,19 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
|
||||
onOverloadLookup->handleEvent(F("POWER"),track);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef BOOSTER_INPUT
|
||||
void RMFT2::railsyncEvent(bool on) {
|
||||
if (Diag::CMD)
|
||||
DIAG(F("railsyncEvent : %d"), on);
|
||||
if (on) {
|
||||
if (onRailSyncOnLookup)
|
||||
onRailSyncOnLookup->handleEvent(F("RAILSYNCON"), 0);
|
||||
} else {
|
||||
if (onRailSyncOffLookup)
|
||||
onRailSyncOffLookup->handleEvent(F("RAILSYNCOFF"), 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// This function is used when setting pins so that a SET or RESET
|
||||
// will cause any blink task on that pin to terminate.
|
||||
// It will be compiled out of existence if no BLINK feature is used.
|
||||
@ -1412,11 +1392,11 @@ void RMFT2::killBlinkOnVpin(VPIN pin, uint16_t count) {
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco) {
|
||||
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
|
||||
// Check we dont already have a task running this handler
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->onEventStartPosition==pc && task->loco==loco) {
|
||||
if (task->onEventStartPosition==pc) {
|
||||
DIAG(F("Recursive ON%S(%d)"),reason, id);
|
||||
return;
|
||||
}
|
||||
@ -1424,7 +1404,7 @@ void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
|
||||
task=new RMFT2(pc,loco); // new task starts at this instruction
|
||||
task=new RMFT2(pc); // new task starts at this instruction
|
||||
task->onEventStartPosition=pc; // flag for recursion detector
|
||||
}
|
||||
|
||||
|
44
EXRAIL2.h
44
EXRAIL2.h
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2022 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2023 Harald Barth
|
||||
* © 2025 Morten Nielsen
|
||||
@ -36,7 +36,6 @@
|
||||
//
|
||||
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
|
||||
OPCODE_MOMENTUM,
|
||||
OPCODE_RESERVE,OPCODE_FREE,
|
||||
OPCODE_AT,OPCODE_AFTER,
|
||||
OPCODE_AFTEROVERLOAD,OPCODE_AUTOSTART,
|
||||
@ -74,16 +73,13 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_ACON, OPCODE_ACOF,
|
||||
OPCODE_ONACON, OPCODE_ONACOF,
|
||||
OPCODE_ONOVERLOAD,
|
||||
OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
|
||||
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
|
||||
OPCODE_ROUTE_DISABLED,
|
||||
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
||||
OPCODE_CLEAR_ANY_STASH,
|
||||
OPCODE_ONBUTTON,OPCODE_ONSENSOR,OPCODE_ONVP_SENSOR,
|
||||
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
|
||||
OPCODE_NEOPIXEL,
|
||||
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
|
||||
OPCODE_ESTOPALL,OPCODE_XPOM,
|
||||
OPCODE_BITMAP_AND,OPCODE_BITMAP_OR,OPCODE_BITMAP_XOR,OPCODE_BITMAP_INC,OPCODE_BITMAP_DEC,OPCODE_ONBITMAP,
|
||||
// OPcodes below this point are skip-nesting IF operations
|
||||
// OPcodes below this point are skip-nesting IF operations
|
||||
// placed here so that they may be skipped as a group
|
||||
// see skipIfBlock()
|
||||
IF_TYPE_OPCODES, // do not move this...
|
||||
@ -95,9 +91,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
|
||||
OPCODE_IFRE,
|
||||
OPCODE_IFLOCO,
|
||||
OPCODE_IFTTPOSITION,
|
||||
OPCODE_IFSTASH,
|
||||
OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,
|
||||
OPCODE_IFTTPOSITION
|
||||
};
|
||||
|
||||
// Ensure thrunge_lcd is put last as there may be more than one display,
|
||||
@ -141,10 +135,9 @@ 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;
|
||||
|
||||
|
||||
// Flag bits for status of hardware and TPL
|
||||
@ -171,7 +164,7 @@ class LookList {
|
||||
int16_t findPosition(int16_t value); // finds index
|
||||
int16_t size();
|
||||
void stream(Print * _stream);
|
||||
void handleEvent(const FSH* reason,int16_t id, int16_t loco=0);
|
||||
void handleEvent(const FSH* reason,int16_t id);
|
||||
|
||||
private:
|
||||
int16_t m_size;
|
||||
@ -185,7 +178,8 @@ class LookList {
|
||||
public:
|
||||
static void begin();
|
||||
static void loop();
|
||||
RMFT2(int progCounter, int16_t cab=0);
|
||||
RMFT2(int progCounter);
|
||||
RMFT2(int route, uint16_t cab);
|
||||
~RMFT2();
|
||||
static void readLocoCallback(int16_t cv);
|
||||
static void createNewTask(int route, uint16_t cab);
|
||||
@ -195,7 +189,9 @@ class LookList {
|
||||
static void clockEvent(int16_t clocktime, bool change);
|
||||
static void rotateEvent(int16_t id, bool change);
|
||||
static void powerEvent(int16_t track, bool overload);
|
||||
static void blockEvent(int16_t block, int16_t loco, bool entering);
|
||||
#ifdef BOOSTER_INPUT
|
||||
static void railsyncEvent(bool on);
|
||||
#endif
|
||||
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||
// Throttle Info Access functions built by exrail macros
|
||||
static const byte rosterNameCount;
|
||||
@ -209,7 +205,7 @@ class LookList {
|
||||
static const FSH * getRosterFunctions(int16_t id);
|
||||
static const FSH * getTurntableDescription(int16_t id);
|
||||
static const FSH * getTurntablePositionDescription(int16_t turntableId, uint8_t positionId);
|
||||
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco=0);
|
||||
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc);
|
||||
static bool readSensor(uint16_t sensorId);
|
||||
static bool isSignal(int16_t id,char rag);
|
||||
static SIGNAL_DEFINITION getSignalSlot(int16_t slotno);
|
||||
@ -233,6 +229,7 @@ private:
|
||||
static RMFT2 * loopTask;
|
||||
static RMFT2 * pausingTask;
|
||||
void delayMe(long millisecs);
|
||||
void driveLoco(byte speedo);
|
||||
bool skipIfBlock();
|
||||
bool readLoco();
|
||||
void loop2();
|
||||
@ -241,8 +238,6 @@ private:
|
||||
void printMessage2(const FSH * msg);
|
||||
void thrungeString(uint32_t strfar, thrunger mode, byte id=0);
|
||||
uint16_t getOperand(byte n);
|
||||
void pause();
|
||||
void resume();
|
||||
|
||||
static bool diag;
|
||||
static const HIGHFLASH3 byte RouteCode[];
|
||||
@ -264,10 +259,10 @@ private:
|
||||
static LookList * onRotateLookup;
|
||||
#endif
|
||||
static LookList * onOverloadLookup;
|
||||
static LookList * onBlockEnterLookup;
|
||||
static LookList * onBlockExitLookup;
|
||||
|
||||
|
||||
#ifdef BOOSTER_INPUT
|
||||
static LookList * onRailSyncOnLookup;
|
||||
static LookList * onRailSyncOffLookup;
|
||||
#endif
|
||||
static const int countLCCLookup;
|
||||
static int onLCCLookup[];
|
||||
static const byte compileFeatures;
|
||||
@ -291,8 +286,9 @@ private:
|
||||
byte taskId;
|
||||
BlinkState blinkState; // includes AT_TIMEOUT flag.
|
||||
uint16_t loco;
|
||||
bool forward;
|
||||
bool invert;
|
||||
byte pauseSpeed;
|
||||
byte speedo;
|
||||
int onEventStartPosition;
|
||||
byte stackDepth;
|
||||
int callStack[MAX_STACK_DEPTH];
|
||||
|
1217
EXRAIL2MacroReset.h
1217
EXRAIL2MacroReset.h
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2023 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@ -181,20 +181,39 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case "M"_hk:
|
||||
// NOTE: we only need to handle valid calls here because
|
||||
// DCCEXParser has to have code to handle the <J<> cases where
|
||||
// exrail isnt involved anyway.
|
||||
// This entire code block is compiled out if STASH macros not used
|
||||
if (!(compileFeatures & FEATURE_STASH)) return;
|
||||
if (paramCount==1) { // <JM>
|
||||
StringFormatter::send(stream,F("<jM %d>\n"),maxStashId);
|
||||
opcode=0;
|
||||
break;
|
||||
}
|
||||
if (paramCount==2) { // <JM id>
|
||||
if (p[1]<=0 || p[1]>maxStashId) break;
|
||||
StringFormatter::send(stream,F("<jM %d %d>\n"),
|
||||
p[1],stashArray[p[1]]);
|
||||
opcode=0;
|
||||
break;
|
||||
}
|
||||
if (paramCount==3) { // <JM id cab>
|
||||
if (p[1]<=0 || p[1]>maxStashId) break;
|
||||
stashArray[p[1]]=p[2];
|
||||
opcode=0;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
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:
|
||||
break;
|
||||
}
|
||||
default: // other commands pass through
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
|
||||
@ -209,9 +228,11 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
);
|
||||
}
|
||||
else {
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d %c"),
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' '
|
||||
task->invert?'I':' ',
|
||||
task->speedo,
|
||||
task->forward?'F':'R'
|
||||
);
|
||||
}
|
||||
task=task->next;
|
||||
@ -240,33 +261,34 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
slot.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (compileFeatures & FEATURE_STASH) {
|
||||
for (int i=1;i<=maxStashId;i++) {
|
||||
if (stashArray[i])
|
||||
StringFormatter::send(stream,F("\nSTASH[%d] Loco=%d"),
|
||||
i, stashArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
StringFormatter::send(stream,F(" *>\n"));
|
||||
return true;
|
||||
}
|
||||
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
|
||||
DCC::setThrottle(0,1,true); // 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;
|
||||
if (task->loco) task->driveLoco(task->speedo);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -279,7 +301,8 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
uint16_t cab=(paramCount==2)? 0 : p[1];
|
||||
int pc=routeLookup->find(route);
|
||||
if (pc<0) return false;
|
||||
new RMFT2(pc,cab);
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
return true;
|
||||
|
||||
|
165
EXRAILAsserts.h
165
EXRAILAsserts.h
@ -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"
|
104
EXRAILMacros.h
104
EXRAILMacros.h
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2022 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2023 Harald Barth
|
||||
* © 2025 Morten Nielsen
|
||||
@ -24,7 +24,6 @@
|
||||
|
||||
#ifndef EXRAILMacros_H
|
||||
#define EXRAILMacros_H
|
||||
#include "IODeviceList.h"
|
||||
|
||||
// remove normal code LCD & SERIAL macros (will be restored later)
|
||||
#undef LCD
|
||||
@ -86,8 +85,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,18 +217,20 @@ bool exrailHalSetup() {
|
||||
#undef ROUTE_CAPTION
|
||||
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
|
||||
|
||||
#undef CLEAR_STASH
|
||||
#define CLEAR_STASH(id) | FEATURE_STASH
|
||||
#undef CLEAR_ALL_STASH
|
||||
#define CLEAR_ALL_STASH | FEATURE_STASH
|
||||
#undef PICKUP_STASH
|
||||
#define PICKUP_STASH(id) | FEATURE_STASH
|
||||
#undef STASH
|
||||
#define STASH(id) | FEATURE_STASH
|
||||
#undef BLINK
|
||||
#define BLINK(vpin,onDuty,offDuty) | FEATURE_BLINK
|
||||
#undef ONBUTTON
|
||||
#define ONBUTTON(vpin) | FEATURE_SENSOR
|
||||
#undef ONSENSOR
|
||||
#define ONSENSOR(vpin) | FEATURE_SENSOR
|
||||
#undef ONBITMAP
|
||||
#define ONBITMAP(vpin) | FEATURE_SENSOR
|
||||
#undef ONBLOCKENTER
|
||||
#define ONBLOCKENTER(blockid) | FEATURE_BLOCK
|
||||
#undef ONBLOCKEXIT
|
||||
#define ONBLOCKEXIT(blockid) | FEATURE_BLOCK
|
||||
|
||||
const byte RMFT2::compileFeatures = 0
|
||||
#include "myAutomation.h"
|
||||
@ -429,7 +494,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
|
||||
@ -449,7 +513,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ENDIF OPCODE_ENDIF,0,0,
|
||||
#define ENDTASK OPCODE_ENDTASK,0,0,
|
||||
#define ESTOP OPCODE_SPEED,V(1),
|
||||
#define ESTOPALL OPCODE_ESTOPALL,0,0,
|
||||
#define EXRAIL
|
||||
#ifndef IO_NO_HAL
|
||||
#define EXTT_TURNTABLE(id,vpin,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(home),
|
||||
@ -476,15 +539,12 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
|
||||
#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),
|
||||
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
|
||||
#define IFSTASH(stash_id) OPCODE_IFSTASH,V(stash_id),
|
||||
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
|
||||
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
|
||||
#ifndef IO_NO_HAL
|
||||
#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position),
|
||||
#endif
|
||||
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
|
||||
#define IFBITMAP_ALL(vpin,mask) OPCODE_IFBITMAP_ALL,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define IFBITMAP_ANY(vpin,mask) OPCODE_IFBITMAP_ANY,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
|
||||
#define JMRI_SENSOR(vpin,count...)
|
||||
#define JOIN OPCODE_JOIN,0,0,
|
||||
@ -505,7 +565,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define STEALTH_GLOBAL(code...)
|
||||
#define LCN(msg) PRINT(msg)
|
||||
#define MESSAGE(msg) PRINT(msg)
|
||||
#define MOMENTUM(accel,decel...) OPCODE_MOMENTUM,V(accel),OPCODE_PAD,V(#decel[0]?decel+0:accel),
|
||||
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
|
||||
#define NEOPIXEL(id,r,g,b,count...) OPCODE_NEOPIXEL,V(id),\
|
||||
OPCODE_PAD,V(((r & 0xff)<<8) | (g & 0xff)),\
|
||||
@ -516,8 +575,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
|
||||
#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),
|
||||
#define ONBLOCKENTER(block_id) OPCODE_ONBLOCKENTER,V(block_id),
|
||||
#define ONBLOCKEXIT(block_id) OPCODE_ONBLOCKEXIT,V(block_id),
|
||||
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
|
||||
#define ONLCC(sender,event) OPCODE_ONLCC,V(event),\
|
||||
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
|
||||
@ -527,6 +584,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
||||
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
||||
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
|
||||
#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
|
||||
#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
|
||||
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
||||
@ -537,12 +596,13 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
|
||||
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
|
||||
#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),
|
||||
#define ONBITMAP(sensor_id) OPCODE_ONBITMAP,V(sensor_id),
|
||||
#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,V(sensor_id),
|
||||
#define PAUSE OPCODE_PAUSE,0,0,
|
||||
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
|
||||
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
|
||||
#ifndef DISABLE_PROG
|
||||
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
|
||||
#endif
|
||||
#define POWEROFF OPCODE_POWEROFF,0,0,
|
||||
#define POWERON OPCODE_POWERON,0,0,
|
||||
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
|
||||
@ -600,11 +660,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
|
||||
#define VIRTUAL_SIGNAL(id)
|
||||
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
|
||||
#define BITMAP_AND(vpin,mask) OPCODE_BITMAP_AND,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define BITMAP_INC(vpin) OPCODE_BITMAP_INC,V(vpin),
|
||||
#define BITMAP_DEC(vpin) OPCODE_BITMAP_DEC,V(vpin),
|
||||
#define BITMAP_OR(vpin,mask) OPCODE_BITMAP_OR,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define BITMAP_XOR(vpin,mask) OPCODE_BITMAP_XOR,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define WITHROTTLE(msg) PRINT(msg)
|
||||
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
|
||||
#ifndef IO_NO_HAL
|
||||
@ -615,7 +670,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define XFTOGGLE(cab,func) OPCODE_XFTOGGLE,V(cab),OPCODE_PAD,V(func),
|
||||
#define XFWD(cab,speed) OPCODE_XFWD,V(cab),OPCODE_PAD,V(speed),
|
||||
#define XREV(cab,speed) OPCODE_XREV,V(cab),OPCODE_PAD,V(speed),
|
||||
#define XPOM(cab,cv,value) OPCODE_XPOM,V(cab),OPCODE_PAD,V(cv),OPCODE_PAD,V(value),
|
||||
|
||||
// Build RouteCode
|
||||
const int StringMacroTracker2=__COUNTER__;
|
||||
|
@ -59,7 +59,7 @@ void EXRAILSensor::checkAll() {
|
||||
|
||||
bool EXRAILSensor::check() {
|
||||
// check for debounced change in this sensor
|
||||
inputState = useAnalog?IODevice::readAnalogue(pin):RMFT2::readSensor(pin);
|
||||
inputState = RMFT2::readSensor(pin);
|
||||
|
||||
// Check if changed since last time, and process changes.
|
||||
if (inputState == active) {// no change
|
||||
@ -83,18 +83,18 @@ bool EXRAILSensor::check() {
|
||||
return false;
|
||||
}
|
||||
|
||||
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog) {
|
||||
|
||||
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange) {
|
||||
// Add to the start of the list
|
||||
//DIAG(F("ONthing vpin=%d at %d"), _pin, _progCounter);
|
||||
nextSensor = firstSensor;
|
||||
firstSensor = this;
|
||||
|
||||
pin=_pin;
|
||||
progCounter=_progCounter;
|
||||
onChange=_onChange;
|
||||
useAnalog=_useAnalog;
|
||||
|
||||
IODevice::configureInput(pin, true);
|
||||
active = useAnalog?IODevice::readAnalogue(pin): IODevice::read(pin);
|
||||
active = IODevice::read(pin);
|
||||
inputState = active;
|
||||
latchDelay = minReadCount;
|
||||
}
|
||||
|
@ -29,8 +29,7 @@ class EXRAILSensor {
|
||||
public:
|
||||
static void checkAll();
|
||||
|
||||
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog=false);
|
||||
|
||||
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange);
|
||||
bool check();
|
||||
|
||||
private:
|
||||
@ -43,10 +42,9 @@ class EXRAILSensor {
|
||||
EXRAILSensor* nextSensor;
|
||||
VPIN pin;
|
||||
int progCounter;
|
||||
uint16_t active;
|
||||
uint16_t inputState;
|
||||
bool active;
|
||||
bool inputState;
|
||||
bool onChange;
|
||||
bool useAnalog;
|
||||
byte latchDelay;
|
||||
};
|
||||
#endif
|
||||
|
200
EXmDNS.cpp
200
EXmDNS.cpp
@ -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
|
50
EXmDNS.h
50
EXmDNS.h
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
#define GITHUB_SHA "devel-202503250850Z"
|
||||
#define GITHUB_SHA "devel-202504182148Z"
|
||||
|
@ -46,8 +46,6 @@
|
||||
|
||||
// Helper function for listing device types
|
||||
static const FSH * guessI2CDeviceType(uint8_t address) {
|
||||
if (address >= 0x10 && address <= 0x17)
|
||||
return F("EX-SensorCAM");
|
||||
if (address == 0x1A)
|
||||
// 0x09-0x18 selectable, but for now handle the default
|
||||
return F("Piicodev 865/915MHz Transceiver");
|
||||
|
14
IODevice.h
14
IODevice.h
@ -560,6 +560,18 @@ protected:
|
||||
|
||||
};
|
||||
|
||||
//#include "IODeviceList.h"
|
||||
#include "IO_MCP23008.h"
|
||||
#include "IO_MCP23017.h"
|
||||
#include "IO_PCF8574.h"
|
||||
#include "IO_PCF8575.h"
|
||||
#include "IO_PCA9555.h"
|
||||
#include "IO_duinoNodes.h"
|
||||
#include "IO_EXIOExpander.h"
|
||||
#include "IO_trainbrains.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "IO_TCA8418.h"
|
||||
#include "IO_NeoPixel.h"
|
||||
#include "IO_TM1638.h"
|
||||
#include "IO_EXSensorCAM.h"
|
||||
|
||||
#endif // iodevice_h
|
||||
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* © 2024, 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 is the list of HAL drivers automatically included by IODevice.h
|
||||
It has been moved here to be easier to maintain than editing IODevice.h
|
||||
*/
|
||||
#include "IO_AnalogueInputs.h"
|
||||
#include "IO_DFPlayer.h"
|
||||
#include "IO_DS1307.h"
|
||||
#include "IO_duinoNodes.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "IO_EXFastclock.h"
|
||||
#include "IO_EXIOExpander.h"
|
||||
#include "IO_EXSensorCAM.h"
|
||||
#include "IO_HALDisplay.h"
|
||||
#include "IO_HCSR04.h"
|
||||
#include "IO_I2CDFPlayer.h"
|
||||
#include "IO_I2CRailcom.h"
|
||||
#include "IO_MCP23008.h"
|
||||
#include "IO_MCP23017.h"
|
||||
#include "IO_NeoPixel.h"
|
||||
#include "IO_PCA9555.h"
|
||||
#include "IO_PCA9685pwm.h"
|
||||
#include "IO_PCF8574.h"
|
||||
#include "IO_PCF8575.h"
|
||||
#include "IO_RotaryEncoder.h"
|
||||
#include "IO_Servo.h"
|
||||
#include "IO_TCA8418.h"
|
||||
#include "IO_TM1638.h"
|
||||
#include "IO_TouchKeypad.h"
|
||||
#include "IO_trainbrains.h"
|
||||
#include "IO_Bitmap.h"
|
||||
#include "IO_VL53L0X.h"
|
||||
|
89
IO_Bitmap.h
89
IO_Bitmap.h
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* © 2025, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef IO_Bitmap_h
|
||||
#define IO_Bitmap_h
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
/*
|
||||
Bitmap provides a set of virtual pins with no hardware.
|
||||
Bitmap pins are able to be output and input and may be set and tested
|
||||
as digital or analogue values.
|
||||
When writing a digital value, the analogue value is set to 0 or 1.
|
||||
When reading a digital value, the return is LOW for value 0 or HIGH for any other value
|
||||
or analogue.
|
||||
|
||||
Bitmap pins may be used for any purpose, this is easier to manage than LATCH in EXRAIL
|
||||
as they can be explicitely set and tested without interfering with underlying hardware.
|
||||
Bitmap pins may be set, reset and tested in the same way as any other pin.
|
||||
They are not persistent across reboots, but are retained in the current session.
|
||||
Bitmap pins may also be monitored by JMRI_SENSOR() and <S> as for any other pin.
|
||||
|
||||
*/
|
||||
class Bitmap : public IODevice {
|
||||
|
||||
public:
|
||||
static void create(VPIN firstVpin, int nPins) {
|
||||
if (IODevice::checkNoOverlap(firstVpin,nPins))
|
||||
new Bitmap( firstVpin, nPins);
|
||||
}
|
||||
|
||||
Bitmap(VPIN firstVpin, int nPins) : IODevice(firstVpin, nPins) {
|
||||
_pinValues=(int16_t *) calloc(nPins,sizeof(int16_t));
|
||||
// Connect to HAL so my _write, _read and _loop will be called as required.
|
||||
IODevice::addDevice(this);
|
||||
}
|
||||
|
||||
// Called by HAL to start handling this device
|
||||
void _begin() override {
|
||||
_deviceState = DEVSTATE_NORMAL;
|
||||
_display();
|
||||
}
|
||||
|
||||
int _read(VPIN vpin) override {
|
||||
int pin=vpin - _firstVpin;
|
||||
return _pinValues[pin]?1:0;
|
||||
}
|
||||
|
||||
void _write(VPIN vpin, int value) override {
|
||||
int pin = vpin - _firstVpin;
|
||||
_pinValues[pin]=value!=0; // this is digital write
|
||||
}
|
||||
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
int pin=vpin - _firstVpin;
|
||||
return _pinValues[pin]; // this is analog read
|
||||
}
|
||||
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
|
||||
int pin=vpin - _firstVpin;
|
||||
_pinValues[pin]=value; // this is analog write
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("Bitmap Configured on Vpins:%u-%u"),
|
||||
(int)_firstVpin,
|
||||
(int)_firstVpin+_nPins-1);
|
||||
}
|
||||
|
||||
private:
|
||||
int16_t* _pinValues;
|
||||
};
|
||||
#endif
|
143
IO_DS1307.cpp
143
IO_DS1307.cpp
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* © 2024, 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The IO_DS1307 device driver is used to interface a standalone realtime clock.
|
||||
* The clock will announce every minute (which will trigger EXRAIL ONTIME events).
|
||||
* Seconds, and Day/date info is ignored, except that the announced hhmm time
|
||||
* will attempt to synchronize with the 0 seconds of the clock.
|
||||
* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.
|
||||
* The clock can be easily set by an analog write to the vpin using 24 hr clock time
|
||||
* with the command <z vpin hh mm ss>
|
||||
*/
|
||||
|
||||
#include "IO_DS1307.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
uint8_t d2b(uint8_t d) {
|
||||
return (d >> 4)*10 + (d & 0x0F);
|
||||
}
|
||||
|
||||
void DS1307::create(VPIN vpin, I2CAddress i2cAddress) {
|
||||
if (checkNoOverlap(vpin, 1, i2cAddress)) new DS1307(vpin, i2cAddress);
|
||||
}
|
||||
|
||||
|
||||
// Constructor
|
||||
DS1307::DS1307(VPIN vpin,I2CAddress i2cAddress){
|
||||
_firstVpin = vpin;
|
||||
_nPins = 1;
|
||||
_I2CAddress = i2cAddress;
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
uint32_t DS1307::getTime() {
|
||||
// Obtain ss,mm,hh buffers from device
|
||||
uint8_t readBuffer[3];
|
||||
const uint8_t writeBuffer[1]={0};
|
||||
|
||||
// address register 0 for read.
|
||||
I2CManager.write(_I2CAddress, writeBuffer, 1);
|
||||
if (I2CManager.read(_I2CAddress, readBuffer, 3) != I2C_STATUS_OK) {
|
||||
_deviceState=DEVSTATE_FAILED;
|
||||
return 0;
|
||||
}
|
||||
_deviceState=DEVSTATE_NORMAL;
|
||||
|
||||
if (debug) {
|
||||
static const char hexchars[]="0123456789ABCDEF";
|
||||
USB_SERIAL.print(F("<*RTC"));
|
||||
for (int i=2;i>=0;i--) {
|
||||
USB_SERIAL.write(' ');
|
||||
USB_SERIAL.write(hexchars[readBuffer[i]>>4]);
|
||||
USB_SERIAL.write(hexchars[readBuffer[i]& 0x0F ]);
|
||||
}
|
||||
StringFormatter::send(&USB_SERIAL,F(" %d *>\n"),_deviceState);
|
||||
}
|
||||
|
||||
if (readBuffer[0] & 0x80) {
|
||||
_deviceState=DEVSTATE_INITIALISING;
|
||||
DIAG(F("DS1307 clock in standby"));
|
||||
return 0; // clock is not running
|
||||
}
|
||||
// convert device format to seconds since midnight
|
||||
uint8_t ss=d2b(readBuffer[0] & 0x7F);
|
||||
uint8_t mm=d2b(readBuffer[1]);
|
||||
uint8_t hh=d2b(readBuffer[2] & 0x3F);
|
||||
return (hh*60ul +mm)*60ul +ss;
|
||||
}
|
||||
|
||||
void DS1307::_begin() {
|
||||
// Initialise device and sync loop() to zero seconds
|
||||
I2CManager.begin();
|
||||
auto tstamp=getTime();
|
||||
if (_deviceState==DEVSTATE_NORMAL) {
|
||||
byte seconds=tstamp%60;
|
||||
delayUntil(micros() + ((60-seconds) * 1000000));
|
||||
}
|
||||
_display();
|
||||
}
|
||||
|
||||
// Processing loop to obtain clock time.
|
||||
// This self-synchronizes to the next minute tickover
|
||||
void DS1307::_loop(unsigned long currentMicros) {
|
||||
auto time=getTime();
|
||||
if (_deviceState==DEVSTATE_NORMAL) {
|
||||
byte ss=time%60;
|
||||
CommandDistributor::setClockTime(time/60, 1, 1);
|
||||
delayUntil(currentMicros + ((60-ss) * 1000000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Display device driver info.
|
||||
void DS1307::_display() {
|
||||
auto tstamp=getTime();
|
||||
byte ss=tstamp%60;
|
||||
tstamp/=60;
|
||||
byte mm=tstamp%60;
|
||||
byte hh=tstamp/60;
|
||||
DIAG(F("DS1307 on I2C:%s vpin %d %d:%d:%d %S"),
|
||||
_I2CAddress.toString(), _firstVpin,
|
||||
hh,mm,ss,
|
||||
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
// allow user to set the clock
|
||||
void DS1307::_writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss) {
|
||||
(void) vpin;
|
||||
uint8_t writeBuffer[3];
|
||||
writeBuffer[0]=1; // write mm,hh first
|
||||
writeBuffer[1]=((mm/10)<<4) + (mm % 10);
|
||||
writeBuffer[2]=((hh/10)<<4) + (hh % 10);
|
||||
I2CManager.write(_I2CAddress, writeBuffer, 3);
|
||||
writeBuffer[0]=0; // write ss
|
||||
writeBuffer[1]=((ss/10)<<4) + (ss % 10);
|
||||
I2CManager.write(_I2CAddress, writeBuffer, 2);
|
||||
_loop(micros()); // resync with seconds rollover
|
||||
}
|
||||
|
||||
// Method to read analogue hh*60+mm time
|
||||
int DS1307::_readAnalogue(VPIN vpin) {
|
||||
(void)vpin;
|
||||
return getTime()/60;
|
||||
};
|
||||
|
54
IO_DS1307.h
54
IO_DS1307.h
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* © 2024, 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The IO_DS1307 device driver is used to interface a standalone realtime clock.
|
||||
* The clock will announce every minute (which will trigger EXRAIL ONTIME events).
|
||||
* Seconds, and Day/date info is ignored, except that the announced hhmm time
|
||||
* will attempt to synchronize with the 0 seconds of the clock.
|
||||
* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.
|
||||
* The clock can be easily set by an analog write to the vpin using 24 hr clock time
|
||||
* with the command <z vpin hh mm ss>
|
||||
*/
|
||||
|
||||
#ifndef IO_DS1307_h
|
||||
#define IO_DS1307_h
|
||||
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
class DS1307 : public IODevice {
|
||||
public:
|
||||
static const bool debug=false;
|
||||
static void create(VPIN vpin, I2CAddress i2cAddress);
|
||||
|
||||
|
||||
private:
|
||||
|
||||
// Constructor
|
||||
DS1307(VPIN vpin,I2CAddress i2cAddress);
|
||||
uint32_t getTime();
|
||||
void _begin() override;
|
||||
void _display() override;
|
||||
void _loop(unsigned long currentMicros) override;
|
||||
int _readAnalogue(VPIN vpin) override;
|
||||
void _writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss) override;
|
||||
};
|
||||
|
||||
#endif
|
@ -16,10 +16,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#define driverVer 306
|
||||
// v306 Pass vpin to regeister it in CamParser.
|
||||
// Move base vpin to camparser.
|
||||
// No more need for config.h settings.
|
||||
#define driverVer 305
|
||||
// v305 less debug & alpha ordered switch
|
||||
// v304 static oldb0; t(##[,%%];
|
||||
// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)
|
||||
@ -38,18 +35,23 @@
|
||||
* This device driver will configure the device on startup, along with CamParser.cpp
|
||||
* interacting with the sensorCAM device for all input/output duties.
|
||||
*
|
||||
* To create EX-SensorCAM devices,
|
||||
* use HAL(EXSensorCAM, baseVpin, numpins, i2c_address) in myAutomation.h
|
||||
* e.g.
|
||||
* HAL(EXSensorCAM,700, 80, 0x11)
|
||||
*
|
||||
* or (deprecated) define them in myHal.cpp: with
|
||||
* EXSensorCAM::create(baseVpin,num_vpins,i2c_address);
|
||||
* #include "CamParser.h" in DCCEXParser.cpp
|
||||
* #include "IO_EXSensorCAM.h" in IODevice.h
|
||||
* To create EX-SensorCAM devices, define them in myHal.cpp: with
|
||||
* EXSensorCAM::create(baseVpin,num_vpins,i2c_address) or
|
||||
* alternatively use HAL(EXSensorCAM baseVpin numpins i2c_address) in myAutomation.h
|
||||
* also #define SENSORCAM_VPIN baseVpin in config.h
|
||||
*
|
||||
* void halSetup() {
|
||||
* // EXSensorCAM::create(vpin, num_vpins, i2c_address);
|
||||
* EXSensorCAM::create(700, 80, 0x11);
|
||||
* }
|
||||
*
|
||||
* I2C packet size of 32 bytes (in the Wire library).
|
||||
*/
|
||||
# define DIGITALREFRESH 20000UL // min uSec delay between digital reads of digitalInputStates
|
||||
#ifndef IO_EX_EXSENSORCAM_H
|
||||
#define IO_EX_EXSENSORCAM_H
|
||||
#define DIGITALREFRESH 20000UL // min uSec delay between digital reads of digitalInputStates
|
||||
#define SEND StringFormatter::send
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
@ -68,7 +70,7 @@ class EXSensorCAM : public IODevice {
|
||||
new EXSensorCAM(vpin, nPins, i2cAddress);
|
||||
}
|
||||
|
||||
|
||||
static VPIN CAMBaseVpin;
|
||||
|
||||
private:
|
||||
// Constructor
|
||||
@ -79,13 +81,11 @@ class EXSensorCAM : public IODevice {
|
||||
_nPins = nPins;
|
||||
_I2CAddress = i2cAddress;
|
||||
addDevice(this);
|
||||
CamParser::addVpin(firstVpin);
|
||||
}
|
||||
//*************************
|
||||
void _begin() {
|
||||
uint8_t status;
|
||||
// Initialise EX-SensorCAM device
|
||||
I2CManager.setClock(100000); // Set speed for I2C operations
|
||||
I2CManager.begin();
|
||||
if (!I2CManager.exists(_I2CAddress)) {
|
||||
DIAG(F("EX-SensorCAM I2C:%s device not found"), _I2CAddress.toString());
|
||||
|
@ -24,7 +24,6 @@
|
||||
*/
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
|
||||
|
@ -511,7 +511,6 @@ public:
|
||||
if (pin == 0) { // Do nothing if not vPin 0
|
||||
return _playing;
|
||||
}
|
||||
return _playing; // fix for compile error: "control reaches end of non-void function [-Wreturn-type]"
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
@ -550,8 +549,8 @@ private:
|
||||
setChecksum(out);
|
||||
|
||||
// Prepend the DFPlayer command with REG address and UART Channel in _outbuffer
|
||||
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
|
||||
for ( uint8_t i = 1; i < sizeof(out)+1 ; i++){
|
||||
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
|
||||
for ( int i = 1; i < sizeof(out)+1 ; i++){
|
||||
_outbuffer[i] = out[i-1];
|
||||
}
|
||||
|
||||
@ -617,14 +616,6 @@ private:
|
||||
uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16); // Calculate _divisor for baudrate
|
||||
TEMP_REG_VAL = 0x08; // UART Software reset
|
||||
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
||||
|
||||
// Extra delay when using low frequency xtal after soft reset
|
||||
// Test when using 1.8432 Mhz xtal
|
||||
if(_sc16is752_xtal_freq == SC16IS752_XTAL_FREQ_LOW){
|
||||
_timeoutTime = micros() + 10000UL; // 10mS timeout
|
||||
_awaitingResponse = true;
|
||||
}
|
||||
|
||||
TEMP_REG_VAL = 0x00; // Set pins to GPIO mode
|
||||
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
||||
TEMP_REG_VAL = 0xFF; //Set all pins as output
|
||||
|
@ -1,121 +0,0 @@
|
||||
/*
|
||||
* © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.
|
||||
* © 2023, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Dec 2023, Added NXP SC16IS752 I2C Dual UART
|
||||
* The SC16IS752 has 64 bytes TX & RX FIFO buffer
|
||||
* First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be
|
||||
* needed as the RX Fifo holds the reply
|
||||
*
|
||||
* Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work but the first transmit
|
||||
* corrupt data. This need more analysis and experimenatation.
|
||||
* Will push this driver to the dev branch with the uart fixed to 0
|
||||
* Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)
|
||||
*
|
||||
* myHall.cpp configuration syntax:
|
||||
*
|
||||
* I2CRailcom::create(1st vPin, vPins, I2C address);
|
||||
*
|
||||
* myAutomation configuration
|
||||
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
|
||||
* Parameters:
|
||||
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
|
||||
* vPins : Total number of virtual pins allocated (to prevent overlaps)
|
||||
* I2C Address : I2C address of the serial controller, in 0x format
|
||||
*/
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "IO_I2CRailcom.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "Railcom.h"
|
||||
|
||||
|
||||
I2CRailcom::I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress){
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
_I2CAddress = i2cAddress;
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress))
|
||||
new I2CRailcom(firstVpin,nPins,i2cAddress);
|
||||
}
|
||||
|
||||
void I2CRailcom::_begin() {
|
||||
I2CManager.setClock(1000000); // TODO do we need this?
|
||||
I2CManager.begin();
|
||||
auto exists=I2CManager.exists(_I2CAddress);
|
||||
DIAG(F("I2CRailcom: %s RailcomCollector %S detected"),
|
||||
_I2CAddress.toString(), exists?F(""):F(" NOT"));
|
||||
if (!exists) return;
|
||||
|
||||
_deviceState=DEVSTATE_NORMAL;
|
||||
_display();
|
||||
}
|
||||
|
||||
|
||||
void I2CRailcom::_loop(unsigned long currentMicros) {
|
||||
// Read responses from device
|
||||
if (_deviceState!=DEVSTATE_NORMAL) return;
|
||||
|
||||
// have we read this cutout already?
|
||||
// basically we only poll once per packet when railcom cutout is working
|
||||
auto cut=DCCWaveform::getRailcomCutoutCounter();
|
||||
if (cutoutCounter==cut) return;
|
||||
cutoutCounter=cut;
|
||||
Railcom::loop(); // in case a csv read has timed out
|
||||
|
||||
// Obtain data length from the collector
|
||||
byte inbuf[1];
|
||||
byte queryLength[]={'?'};
|
||||
auto state=I2CManager.read(_I2CAddress, inbuf, 1,queryLength,sizeof(queryLength));
|
||||
if (state) {
|
||||
DIAG(F("RC ? state=%d"),state);
|
||||
return;
|
||||
}
|
||||
auto length=inbuf[0];
|
||||
if (length==0) return; // nothing to report
|
||||
|
||||
// Build a buffer and import the data from the collector
|
||||
byte inbuf2[length];
|
||||
byte queryData[]={'>'};
|
||||
state=I2CManager.read(_I2CAddress, inbuf2, length,queryData,sizeof(queryData));
|
||||
if (state) {
|
||||
DIAG(F("RC > %d state=%d"),length,state);
|
||||
return;
|
||||
}
|
||||
|
||||
// process incoming data buffer
|
||||
Railcom::process(_firstVpin,inbuf2,length);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void I2CRailcom::_display() {
|
||||
DIAG(F("I2CRailcom: %s blocks %d-%d %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,
|
||||
(_deviceState!=DEVSTATE_NORMAL) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.
|
||||
* © 2023, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This polls the RailcomCollecter device once per dcc packet
|
||||
* and obtains an abbreviated list of block occupancy changes which
|
||||
* are fortunately very rare compared with Railcom raw data.
|
||||
*
|
||||
* myAutomation configuration
|
||||
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
|
||||
* Parameters:
|
||||
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
|
||||
* vPins : Total number of virtual pins allocated
|
||||
* I2C Address : I2C address of the Railcom Collector, in 0x format
|
||||
*/
|
||||
|
||||
#ifndef IO_I2CRailcom_h
|
||||
#define IO_I2CRailcom_h
|
||||
#include "Arduino.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
class I2CRailcom : public IODevice {
|
||||
private:
|
||||
byte cutoutCounter;
|
||||
public:
|
||||
// Constructor
|
||||
I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress);
|
||||
|
||||
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) ;
|
||||
|
||||
void _begin() ;
|
||||
void _loop(unsigned long currentMicros) override ;
|
||||
void _display() override ;
|
||||
|
||||
private:
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif // IO_I2CRailcom_h
|
101
IO_PCA9554.h
101
IO_PCA9554.h
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* © 2025, Paul M. Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef io_pca9554_h
|
||||
#define io_pca9554_h
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for PCA9554/TCA9554 8-bit I/O expander (NXP & Texas Instruments).
|
||||
*/
|
||||
|
||||
class PCA9554 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9554(vpin,nPins, i2cAddress, interruptPin);
|
||||
}
|
||||
|
||||
private:
|
||||
// Constructor
|
||||
PCA9554(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCA9554"), vpin, nPins, I2CAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||
outputBuffer, sizeof(outputBuffer));
|
||||
outputBuffer[0] = REG_INPUT_P0;
|
||||
}
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 2, REG_OUTPUT_P0, _portOutputState);
|
||||
}
|
||||
void _writePullups() override {
|
||||
// Do nothing, pull-ups are always in place for input ports
|
||||
// This function is here for HAL GPIOBase API compatibilitiy
|
||||
|
||||
}
|
||||
void _writePortModes() override {
|
||||
// Write 0 to REG_CONF_P0 for in-use pins that are outputs, 1 for others.
|
||||
// PCA9554 & TCA9554, Interrupt is always enabled for raising and falling edge
|
||||
uint8_t temp = ~(_portMode & _portInUse);
|
||||
I2CManager.write(_I2CAddress, 2, REG_CONF_P0, temp);
|
||||
}
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer[1];
|
||||
I2CManager.read(_I2CAddress, buffer, 1, 1, REG_INPUT_P0);
|
||||
_portInputState = buffer[0];
|
||||
} else {
|
||||
// Queue new request
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xff;
|
||||
}
|
||||
|
||||
void _setupDevice() override {
|
||||
// HAL API calls
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
_writeGpioPort();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[1];
|
||||
uint8_t outputBuffer[1];
|
||||
|
||||
|
||||
enum {
|
||||
REG_INPUT_P0 = 0x00,
|
||||
REG_OUTPUT_P0 = 0x01,
|
||||
REG_POL_INV_P0 = 0x02,
|
||||
REG_CONF_P0 = 0x03,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif
|
13
IO_PCF8574.h
13
IO_PCF8574.h
@ -1,5 +1,4 @@
|
||||
/*
|
||||
* © 2025 Herb Morton
|
||||
* © 2022 Paul M Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
@ -44,21 +43,15 @@
|
||||
|
||||
class PCF8574 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
|
||||
}
|
||||
|
||||
private:
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
|
||||
if (initPortState>=0) {
|
||||
_portMode = 255; // set all pins to output mode
|
||||
_portInUse = 255; // 8 ports in use
|
||||
_portOutputState = initPortState; // initialize pins low-high 0-255
|
||||
I2CManager.write(_I2CAddress, 1, initPortState);
|
||||
}
|
||||
}
|
||||
|
||||
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
|
||||
|
@ -22,7 +22,6 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "IODevice.h"
|
||||
#include "IO_TM1638.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
|
||||
|
130
LocoTable.cpp
Normal file
130
LocoTable.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source 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.
|
||||
*
|
||||
* This source 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 this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "LocoTable.h"
|
||||
|
||||
LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
|
||||
int LocoTable::highestUsedReg = 0;
|
||||
|
||||
int LocoTable::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
// determine speed reg for this loco
|
||||
int firstEmpty = MAX_LOCOS;
|
||||
int reg;
|
||||
for (reg = 0; reg < MAX_LOCOS; reg++) {
|
||||
if (speedTable[reg].loco == locoId) break;
|
||||
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
|
||||
}
|
||||
|
||||
// return -1 if not found and not auto creating
|
||||
if (reg == MAX_LOCOS && !autoCreate) return -1;
|
||||
if (reg == MAX_LOCOS) reg = firstEmpty;
|
||||
if (reg >= MAX_LOCOS) {
|
||||
//DIAG(F("Too many locos"));
|
||||
return -1;
|
||||
}
|
||||
if (reg==firstEmpty){
|
||||
speedTable[reg].loco = locoId;
|
||||
speedTable[reg].speedCode=128; // default direction forward
|
||||
speedTable[reg].groupFlags=0;
|
||||
speedTable[reg].functions=0;
|
||||
}
|
||||
if (reg > highestUsedReg) highestUsedReg = reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
// returns false only if loco existed but nothing was changed
|
||||
bool LocoTable::updateLoco(int loco, byte speedCode) {
|
||||
if (loco==0) {
|
||||
/*
|
||||
// broadcast stop/estop but dont change direction
|
||||
for (int reg = 0; reg < highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco==0) continue;
|
||||
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].speedCode != newspeed) {
|
||||
speedTable[reg].speedCode = newspeed;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco, false);
|
||||
if (reg>=0) {
|
||||
speedTable[reg].speedcounter++;
|
||||
if (speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// new
|
||||
reg=lookupSpeedTable(loco, true);
|
||||
if(reg >=0) speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool LocoTable::updateFunc(int loco, byte func, int shift) {
|
||||
unsigned long previous;
|
||||
unsigned long newfunc;
|
||||
bool retval = false; // nothing was touched
|
||||
int reg = lookupSpeedTable(loco, false);
|
||||
if (reg < 0) { // not found
|
||||
retval = true;
|
||||
reg = lookupSpeedTable(loco, true);
|
||||
newfunc = previous = 0;
|
||||
} else {
|
||||
newfunc = previous = speedTable[reg].functions;
|
||||
}
|
||||
|
||||
speedTable[reg].funccounter++;
|
||||
|
||||
if(shift == 1) { // special case for light
|
||||
newfunc &= ~1UL;
|
||||
newfunc |= ((func & 0B10000) >> 4);
|
||||
}
|
||||
newfunc &= ~(0B1111UL << shift);
|
||||
newfunc |= ((func & 0B1111) << shift);
|
||||
|
||||
if (newfunc != previous) {
|
||||
speedTable[reg].functions = newfunc;
|
||||
retval = true;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
void LocoTable::dumpTable(Stream *output) {
|
||||
output->print("\n-----------Table---------\n");
|
||||
for (byte reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco != 0) {
|
||||
output->print(speedTable[reg].loco);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].speedCode);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].functions);
|
||||
output->print(" #funcpacks:");
|
||||
output->print(speedTable[reg].funccounter);
|
||||
output->print(" #speedpacks:");
|
||||
output->print(speedTable[reg].speedcounter);
|
||||
speedTable[reg].funccounter = 0;
|
||||
speedTable[reg].speedcounter = 0;
|
||||
output->print('\n');
|
||||
}
|
||||
}
|
||||
}
|
44
LocoTable.h
Normal file
44
LocoTable.h
Normal file
@ -0,0 +1,44 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source 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.
|
||||
*
|
||||
* This source 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 this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "DCC.h" // fetch MAX_LOCOS from there
|
||||
|
||||
class LocoTable {
|
||||
public:
|
||||
void forgetLoco(int cab) {
|
||||
int reg=lookupSpeedTable(cab, false);
|
||||
if (reg>=0) speedTable[reg].loco=0;
|
||||
}
|
||||
static int lookupSpeedTable(int locoId, bool autoCreate);
|
||||
static bool updateLoco(int loco, byte speedCode);
|
||||
static bool updateFunc(int loco, byte func, int shift);
|
||||
static void dumpTable(Stream *output);
|
||||
|
||||
private:
|
||||
struct LOCO
|
||||
{
|
||||
int loco;
|
||||
byte speedCode;
|
||||
byte groupFlags;
|
||||
unsigned long functions;
|
||||
unsigned int funccounter;
|
||||
unsigned int speedcounter;
|
||||
};
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
static int highestUsedReg;
|
||||
};
|
82
Railcom.cpp
82
Railcom.cpp
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* © 2025 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 "Railcom.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
|
||||
uint16_t Railcom::expectLoco=0;
|
||||
uint16_t Railcom::expectCV=0;
|
||||
unsigned long Railcom::expectWait=0;
|
||||
ACK_CALLBACK Railcom::expectCallback=0;
|
||||
|
||||
// anticipate is used when waiting for a CV read from a railcom loco
|
||||
void Railcom::anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback) {
|
||||
expectLoco=loco;
|
||||
expectCV=cv;
|
||||
expectWait=millis(); // start of timeout
|
||||
expectCallback=callback;
|
||||
}
|
||||
|
||||
// process is called to handle data buffer sent by collector
|
||||
void Railcom::process(int16_t firstVpin,byte * buffer, byte length) {
|
||||
// block,locohi,locolow
|
||||
// block|0x80,data pom read cv
|
||||
byte i=0;
|
||||
while (i<length) {
|
||||
byte block=buffer[i] & 0x3f;
|
||||
byte type=buffer[i]>>6;
|
||||
|
||||
switch (type) {
|
||||
// a type=0 record has block,locohi,locolow
|
||||
case 0: {
|
||||
uint16_t locoid= ((uint16_t)buffer[i+1])<<8 | ((uint16_t)buffer[i+2]);
|
||||
DIAG(F("RC3 b=%d l=%d"),block,locoid);
|
||||
|
||||
if (locoid==0) DCC::clearBlock(firstVpin+block);
|
||||
else DCC::setLocoInBlock(locoid,firstVpin+block,true);
|
||||
i+=3;
|
||||
}
|
||||
break;
|
||||
case 2: { // csv value from POM read
|
||||
byte value=buffer[i+1];
|
||||
if (expectCV && DCCWaveform::getRailcomLastLocoAddress()==expectLoco) {
|
||||
DCC::setLocoInBlock(expectLoco,firstVpin+block,false);
|
||||
if (expectCallback) expectCallback(value);
|
||||
expectCV=0;
|
||||
}
|
||||
i+=2;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DIAG(F("Unknown RC Collector code %d"),type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// loop() is called to detect timeouts waiting for a POM read result
|
||||
void Railcom::loop() {
|
||||
if (expectCV && (millis()-expectWait)> POM_READ_TIMEOUT) { // still waiting
|
||||
expectCallback(-1);
|
||||
expectCV=0;
|
||||
}
|
||||
}
|
40
Railcom.h
40
Railcom.h
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* © 202 5Chris 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 Railcom_h
|
||||
#define Railcom_h
|
||||
#include "Arduino.h"
|
||||
|
||||
typedef void (*ACK_CALLBACK)(int16_t result);
|
||||
|
||||
class Railcom {
|
||||
public:
|
||||
static void anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback);
|
||||
static void process(int16_t firstVpin,byte * buffer, byte length );
|
||||
static void loop();
|
||||
private:
|
||||
static const unsigned long POM_READ_TIMEOUT=500; // as per spec
|
||||
static uint16_t expectCV,expectLoco;
|
||||
static unsigned long expectWait;
|
||||
static ACK_CALLBACK expectCallback;
|
||||
static const byte MAX_WAIT_FOR_GLITCH=20; // number of dead or empty packets before assuming loco=0
|
||||
};
|
||||
|
||||
#endif
|
BIN
Release_Notes/DCC-EX v5.4 Release Notes.xlsx
Normal file
BIN
Release_Notes/DCC-EX v5.4 Release Notes.xlsx
Normal file
Binary file not shown.
@ -1,58 +0,0 @@
|
||||
Virtual Bitmap device pins.
|
||||
|
||||
a Bitmap device pin is a software representation of a virtual hardware device that has the ability to store a 16bit value.
|
||||
|
||||
This this is easier to manage than LATCH in EXRAIL as they can be explicitely set and tested without interfering with underlying hardware or breaching the 255 limit.
|
||||
|
||||
Virtual pins may be set, reset and tested in the same way as any other pin. Unlike sensors and leds, these device pins are both INPUT and OUTPUT These can be used in many ways:
|
||||
|
||||
As a simple digital flag to assist in inter-thread communication.
|
||||
A flag or value that can be set from commands and tested in EXRAIL.(e.g. to stop a sequence)
|
||||
As a counter for looping or occupancy counts such as trains passing over a multi track road crossing.
|
||||
As a collection of 16 digital bits that can be set, reset, toggled, masked and tested.
|
||||
|
||||
Existing <> and exrail commands for vpins work on these pins.
|
||||
|
||||
Virtual pin creation:
|
||||
HAL(Bitmap,firstpin,npins)
|
||||
creates 1 or more virtual pins in software. (RAM requirement approximately 2 bytes per pin)
|
||||
e.g. HAL(Bitmap,1000,20) creates pins 1000..1019
|
||||
|
||||
Simple use as flags:
|
||||
This uses the traditional digital pin commands
|
||||
SET(1013) RESET(1013) sets value 1 or 0
|
||||
SET(1000,20) RESET(1000,20) sets/resets a range of pins
|
||||
IF(1000) tests if pin value!=0
|
||||
|
||||
Commands can set 1/0 values using <z 1010> <z -1010> as for any digital output.
|
||||
BLINK can be used to set them on/off on a time pattern.
|
||||
|
||||
In addition, Exrail sensor comands work as if these pins were sensors
|
||||
ONBUTTON(1013) triggers when value changes from 0 to something.
|
||||
ONSENSOR(1013) triggers when value changes to or from 0.
|
||||
<S 1013 1013 1> and JMRI_SENSOR(1013) report <Q/q responses when changing to or from 0.
|
||||
|
||||
Use as analog values:
|
||||
Analog values may be set into the virtual pins and tested using the existing analog value commands and exrail macros.
|
||||
<z vpin value> <D ANIN vpin> etc.
|
||||
|
||||
Use as counters:
|
||||
For loop counting, counters can be incremented by BITMAP_INC(1013) and decremented by BITMAP_DEC(1013) and tested with IF/IFNOT/IFGTE etc.
|
||||
Counters be used to automate a multi track crossing where each train entering increments the counter and decrements it on clearing the crossing. Crossing gate automation can be started when the value changes from 0, and be stopped when the counter returns to 0. Detecting the first increment from 0 to 1 can be done with ONBUTTON(1013) and the automation can use IF(1013) or IFNOT(1013) to detect when it needs to reopen the road gates.
|
||||
|
||||
Use as binary flag groups:
|
||||
Virtual pins (and others that respond to an analog read in order to provide bitmapped digital data, such as SensorCam) can be set and tested with new special EXRAIL commands
|
||||
|
||||
IFBITMAP_ALL(vpin,mask) Bitwise ANDs the the vpin value with the mask value and is true if ALL the 1 bits in the mask are also 1 bits in the value.
|
||||
e.g. IFBITMAP_ALL(1013,0x0f) would be true if ALL the last 4 bits of the value are 1s.
|
||||
|
||||
IFBITMAP_ANY(1013,0x0f) would be true if ANY of the last 4 bits are 1s.
|
||||
|
||||
|
||||
Modifying bitmap values:
|
||||
BITMAP_AND(vpin,mask) performs a bitwise AND operation.
|
||||
BITMAP_OR(vpin,mask) performa a bitwise OR operation
|
||||
BITMAP_XOR(vpin,mask) performs a bitwise EXCLUSIVE OR (which is basically a toggle)
|
||||
|
||||
|
||||
|
@ -1,71 +0,0 @@
|
||||
Railcom implementation notes, Chris Harlow Oct 2024
|
||||
|
||||
Railcom support is in 3 parts
|
||||
1. Generation of the DCC waveform with a Railcom cutout.
|
||||
2. Accessing the railcom feedback from a loco using hardware detectors
|
||||
3. Utilising the feedback to do something useful.
|
||||
|
||||
DCC Waveform Railcom cutout depends on using suitable motor shields (EX8874 primarily) as the standard Arduino shield is not suitable. (Too high resistance during cutout)
|
||||
The choice of track management also depends on wiring all the MAIN tracks to use the same signal and brake pins. This allows separate track power management but prevents switching a single track from MAIN to PROG or DC...
|
||||
Some CPUs require very specific choice of brake pins etc to match their internal timer register architecture.
|
||||
|
||||
- MEGA.. The default shield setting for an EX8874 is suitable for Railcom on Channel A (MAIN)
|
||||
- ESP32 .. not yet supported.
|
||||
- Nucleo ... TBA
|
||||
|
||||
Enabling the Railcom Cutout requires a `<C RAILCOM ON>` command. This can be added to myAutomation using `PARSE("<C RAILCOM ON>")`
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.)
|
||||
|
||||
Making use of Railcom data
|
||||
|
||||
Exrail has two additional event handlers which can capture locos entering and exiting blocks. These handlers are started with the loco information already set, so for example:
|
||||
```
|
||||
ONBLOCKENTER(10000)
|
||||
// a loco has entered block 10000
|
||||
FON(0) // turn the light on
|
||||
FON(1) // make a lot of noise
|
||||
SPEED(20) // slow down
|
||||
DONE
|
||||
|
||||
ONBLOCKEXIT(10000)
|
||||
// a loco has left block 10000
|
||||
FOFF(0) // turn the light off
|
||||
FOFF(1) // stop the noise
|
||||
SPEED(50) // speed up again
|
||||
DONE
|
||||
```
|
||||
|
||||
Note that the Railcom interpretation code is capable of detecting multiple locos in the same block at the same time and will create separate exrail tasks for each one.
|
||||
There is however one minor loophole in the block exit logic...
|
||||
If THREE or more locos are in the same block and ONE of them leaves, then ONBLOCKEXIT will not fire until
|
||||
EITHER - The leaving loco enters another railcom block
|
||||
OR - only ONE loco remains in the block just left.
|
||||
|
||||
To further support block management in railcom, two additional serial commands are available
|
||||
|
||||
`<K block loco >` to simulate a loco entering a block, and trigger any ONBLOCKENTER
|
||||
`<k block loco >` to simulate a loco leaving a block, and trigger and ONBLOCKEXIT
|
||||
|
||||
|
||||
Reading CV values on MAIN.
|
||||
|
||||
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 MAIN track use `<r loco cv>`
|
||||
response is `<r loco cv value>`
|
||||
|
||||
|
@ -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>`)
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
New Momentum feature notes:
|
||||
|
||||
The command station can apply momentum to throttle movements in the same way that a standards compliant DCC decoder can be set to do. This momentum can be defaulted system wide and overridden on individual locos. It does not use or alter the loco CV values and so it also works when driving DC locos.
|
||||
The momentum is applied regardless of the throttle type used (or even EXRAIL).
|
||||
|
||||
Momentum is specified in mS / throttle_step.
|
||||
|
||||
There is a new command `<m cabid accelerating [brake]>`
|
||||
where the brake value defaults to the accelerating value.
|
||||
|
||||
For example:
|
||||
`<m 3 0>` sets loco 3 to no momentum.
|
||||
`<m 3 21>` sets loco 3 to 21 mS/step.
|
||||
`<m 3 21 42>` sets loco 3 to 21 mS/step accelerating and 42 mS/step when decelerating.
|
||||
|
||||
`<m 0 21>` sets the default momentum to 21mS/Step for all current and future locos that have not been specifically set.
|
||||
`<m 3 -1>` sets loco 3 to track the default momentum value.
|
||||
|
||||
EXRAIL
|
||||
A new macro `MOMENTUM(accel [, decel])` sets the momentum value of the current tasks loco ot the global default if loco=0.
|
||||
|
||||
Note: Setting Momentum 7,14,21 etc is similar in effect to setting a decoder CV03/CV04 to 1,2,3.
|
||||
|
||||
As an additional option, the momentum calculation is based on the
|
||||
difference in throttle setting and actual speed. For example, the time taken to reach speed 50 from a standing start would be less if the throttle were set to speed 100, thus increasing the acceleration.
|
||||
|
||||
`<m LINEAR>` - acceleration is uniform up to selected throttle speed.
|
||||
`<m POWER>` - acceleration depends on difference between loco speed and selected throttle speed.
|
||||
|
@ -1,41 +0,0 @@
|
||||
<html>
|
||||
<!-- Minimalist test page for the DCCEX websocket API.-->
|
||||
<head>
|
||||
<script>
|
||||
let socket = new WebSocket("ws://192.168.1.242:2560","DCCEX");
|
||||
|
||||
// send message from the form
|
||||
var sender = function() {
|
||||
var msg=document.getElementById('message').value;
|
||||
socket.send(msg);
|
||||
}
|
||||
// message received - show the message in div#messages
|
||||
socket.onmessage = function(event) {
|
||||
let message = event.data;
|
||||
|
||||
let messageElem = document.createElement('div');
|
||||
messageElem.textContent = message;
|
||||
document.getElementById('messages').prepend(messageElem);
|
||||
}
|
||||
socket.onerror = function(event) {
|
||||
let message = event.data;
|
||||
let messageElem = document.createElement('div');
|
||||
messageElem.textContent = message;
|
||||
document.getElementById('messages').prepend(messageElem);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
This is a minimalist test page for the DCCEX websocket API.
|
||||
It demonstrates the Websocket connection and how to send
|
||||
or receive websocket traffic.
|
||||
The connection string must be edited to address your command station
|
||||
correctly.<p>
|
||||
<!-- message form -->
|
||||
|
||||
<input type="text" id="message">
|
||||
<input type="button" value="Send" onclick="sender();">
|
||||
<!-- div with messages -->
|
||||
<div id="messages"></div>
|
||||
</body>
|
||||
</html>
|
@ -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__ */
|
220
Sniffer.cpp
Normal file
220
Sniffer.cpp
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* 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 ARDUINO_ARCH_ESP32
|
||||
#define DIAG_LED 33
|
||||
#include "Sniffer.h"
|
||||
#include "DIAG.h"
|
||||
//extern Sniffer *DCCSniffer;
|
||||
|
||||
static void packeterror() {
|
||||
digitalWrite(DIAG_LED,HIGH);
|
||||
}
|
||||
|
||||
static void clear_packeterror() {
|
||||
digitalWrite(DIAG_LED,LOW);
|
||||
}
|
||||
|
||||
static bool halfbits2byte(uint16_t b, byte *dccbyte) {
|
||||
/*
|
||||
if (b!=0 && b!=0xFFFF) {
|
||||
Serial.print("[ ");
|
||||
for(int n=0; n<16; n++) {
|
||||
Serial.print(b&(1<<n)?"1":"0");
|
||||
}
|
||||
Serial.println(" ]");
|
||||
}
|
||||
*/
|
||||
for(byte n=0; n<8; n++) {
|
||||
switch (b & 0x03) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
// broken bits
|
||||
packeterror();
|
||||
return false;
|
||||
break;
|
||||
case 0x00:
|
||||
bitClear(*dccbyte, n);
|
||||
break;
|
||||
case 0x03:
|
||||
bitSet(*dccbyte, n);
|
||||
break;
|
||||
}
|
||||
b = b>>2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR blink_diag(int limit) {
|
||||
delay(500);
|
||||
for (int n=0 ; n<limit; n++) {
|
||||
digitalWrite(DIAG_LED,HIGH);
|
||||
delay(200);
|
||||
digitalWrite(DIAG_LED,LOW);
|
||||
delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
static bool IRAM_ATTR cap_ISR_cb(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,void *user_data) {
|
||||
if (edata->cap_edge == MCPWM_BOTH_EDGE) {
|
||||
// should not happen at all
|
||||
// delays here might crash sketch
|
||||
blink_diag(2);
|
||||
return 0;
|
||||
}
|
||||
if (user_data) ((Sniffer *)user_data)->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
|
||||
//if (DCCSniffer) DCCSniffer->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Sniffer::Sniffer(byte snifferpin) {
|
||||
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, snifferpin);
|
||||
// set capture edge, BIT(0) - negative edge, BIT(1) - positive edge
|
||||
// MCPWM_POS_EDGE|MCPWM_NEG_EDGE should be 3.
|
||||
//mcpwm_capture_enable(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, MCPWM_POS_EDGE|MCPWM_NEG_EDGE, 0);
|
||||
//mcpwm_isr_register(MCPWM_UNIT_0, sniffer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
|
||||
//MCPWM0.int_ena.cap0_int_ena = 1; // Enable interrupt on CAP0 signal
|
||||
|
||||
mcpwm_capture_config_t MCPWM_cap_config = { //Capture channel configuration
|
||||
.cap_edge = MCPWM_BOTH_EDGE, // according to mcpwm.h
|
||||
.cap_prescale = 1, // 1 to 256 (see .h file)
|
||||
.capture_cb = cap_ISR_cb, // user defined ISR/callback
|
||||
.user_data = (void *)this // user defined argument to callback
|
||||
};
|
||||
pinMode(DIAG_LED ,OUTPUT);
|
||||
blink_diag(3); // so that we know we have DIAG_LED
|
||||
DIAG(F("Init sniffer on pin %d"), snifferpin);
|
||||
ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &MCPWM_cap_config));
|
||||
}
|
||||
|
||||
#define SNIFFER_TIMEOUT 100L // 100 Milliseconds
|
||||
bool Sniffer::inputActive(){
|
||||
unsigned long now = millis();
|
||||
return ((now - lastendofpacket) < SNIFFER_TIMEOUT);
|
||||
}
|
||||
|
||||
#define DCC_TOO_SHORT 4000L // 4000 ticks are 50usec
|
||||
#define DCC_ONE_LIMIT 6400L // 6400 ticks are 80usec
|
||||
|
||||
void IRAM_ATTR Sniffer::processInterrupt(int32_t capticks, bool posedge) {
|
||||
byte bit = 0;
|
||||
diffticks = capticks - lastticks;
|
||||
if (lastedge != posedge) {
|
||||
if (diffticks < DCC_TOO_SHORT) {
|
||||
return;
|
||||
}
|
||||
if (diffticks < DCC_ONE_LIMIT) {
|
||||
bit = 1;
|
||||
} else {
|
||||
bit = 0;
|
||||
}
|
||||
// update state variables for next round
|
||||
lastticks = capticks;
|
||||
lastedge = posedge;
|
||||
bitfield = bitfield << (uint64_t)1;
|
||||
bitfield = bitfield + (uint64_t)bit;
|
||||
|
||||
// now the halfbit is in the bitfield. Analyze...
|
||||
|
||||
if ((bitfield & 0xFFFFFF) == 0xFFFFFC){
|
||||
// This looks at the 24 last halfbits
|
||||
// and detects a preamble if
|
||||
// 22 are ONE and 2 are ZERO which is a
|
||||
// preabmle of 11 ONES and one ZERO
|
||||
if (inpacket) {
|
||||
// if we are already inpacket here we
|
||||
// got a preamble in the middle of a
|
||||
// packet
|
||||
packeterror();
|
||||
} else {
|
||||
clear_packeterror(); // everything fine again at end of preable after good packet
|
||||
}
|
||||
currentbyte = 0;
|
||||
dcclen = 0;
|
||||
inpacket = true;
|
||||
halfbitcounter = 18; // count 18 steps from 17 to 0 and then look at the byte
|
||||
return;
|
||||
}
|
||||
if (inpacket) {
|
||||
halfbitcounter--;
|
||||
if (halfbitcounter) {
|
||||
return; // wait until we have full byte
|
||||
} else {
|
||||
// have reached end of byte
|
||||
//if (currentbyte == 2) debugfield = bitfield;
|
||||
byte twohalfbits = bitfield & 0x03;
|
||||
switch (twohalfbits) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
// broken bits
|
||||
inpacket = false;
|
||||
packeterror();
|
||||
return;
|
||||
break;
|
||||
case 0x00:
|
||||
case 0x03:
|
||||
// byte end
|
||||
uint16_t b = (bitfield & 0x3FFFF)>>2; // take 18 halfbits and use 16 of them
|
||||
if (!halfbits2byte(b, dccbytes + currentbyte)) {
|
||||
// broken halfbits
|
||||
inpacket = false;
|
||||
packeterror();
|
||||
return;
|
||||
}
|
||||
if (twohalfbits == 0x03) { // end of packet marker
|
||||
inpacket = false;
|
||||
dcclen = currentbyte+1;
|
||||
debugfield = bitfield;
|
||||
// put it into the out packet
|
||||
if (fetchflag) {
|
||||
// not good, should have been fetched
|
||||
// blink_diag(1);
|
||||
packeterror(); // or better?
|
||||
}
|
||||
lastendofpacket = millis();
|
||||
DCCPacket temppacket(dccbytes, dcclen);
|
||||
if (!(temppacket == prevpacket)) {
|
||||
// we have something new to offer to the fetch routine
|
||||
outpacket.push_back(temppacket);
|
||||
prevpacket = temppacket;
|
||||
fetchflag = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
halfbitcounter = 18;
|
||||
currentbyte++; // everything done for this end of byte
|
||||
if (currentbyte >= MAXDCCPACKETLEN) {
|
||||
inpacket = false; // this is an error because we should have retured above
|
||||
packeterror(); // when endof packet marker was active
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // lastedge == posedge
|
||||
// this should not happen, check later
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
static void IRAM_ATTR sniffer_isr_handler(void *) {
|
||||
DCCSniffer.processInterrupt();
|
||||
}
|
||||
*/
|
||||
#endif // ESP32
|
80
Sniffer.h
Normal file
80
Sniffer.h
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* 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 ARDUINO_ARCH_ESP32
|
||||
#include <Arduino.h>
|
||||
#include <list>
|
||||
#include "driver/mcpwm.h"
|
||||
#include "soc/mcpwm_struct.h"
|
||||
#include "soc/mcpwm_reg.h"
|
||||
|
||||
#define MAXDCCPACKETLEN 8
|
||||
#include "DCCPacket.h"
|
||||
|
||||
class Sniffer {
|
||||
public:
|
||||
Sniffer(byte snifferpin);
|
||||
void IRAM_ATTR processInterrupt(int32_t capticks, bool posedge);
|
||||
inline int32_t getTicks() {
|
||||
noInterrupts();
|
||||
int32_t i = diffticks;
|
||||
interrupts();
|
||||
return i;
|
||||
};
|
||||
inline int64_t getDebug() {
|
||||
noInterrupts();
|
||||
int64_t i = debugfield;
|
||||
interrupts();
|
||||
return i;
|
||||
};
|
||||
inline DCCPacket fetchPacket() {
|
||||
// if there is no new data, this will create a
|
||||
// packet with length 0 (which is no packet)
|
||||
DCCPacket p;
|
||||
noInterrupts();
|
||||
if (!outpacket.empty()) {
|
||||
p = outpacket.front();
|
||||
outpacket.pop_front();
|
||||
}
|
||||
if (fetchflag) {
|
||||
fetchflag = false; // (data has been fetched)
|
||||
}
|
||||
interrupts();
|
||||
return p;
|
||||
};
|
||||
bool inputActive();
|
||||
private:
|
||||
// keep these vars in processInterrupt only
|
||||
uint64_t bitfield = 0;
|
||||
uint64_t debugfield = 0;
|
||||
int32_t diffticks;
|
||||
int32_t lastticks;
|
||||
bool lastedge;
|
||||
byte currentbyte = 0;
|
||||
byte dccbytes[MAXDCCPACKETLEN];
|
||||
byte dcclen = 0;
|
||||
bool inpacket = false;
|
||||
// these vars are used as interface to other parts of sniffer
|
||||
byte halfbitcounter = 0;
|
||||
bool fetchflag = false;
|
||||
std::list<DCCPacket> outpacket;
|
||||
DCCPacket prevpacket;
|
||||
volatile unsigned long lastendofpacket = 0; // timestamp millis
|
||||
|
||||
};
|
||||
#endif
|
89
Stash.cpp
89
Stash.cpp
@ -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
39
Stash.h
@ -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
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2020=2025, Chris Harlow. All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -27,9 +27,6 @@ bool Diag::WIFI=false;
|
||||
bool Diag::WITHROTTLE=false;
|
||||
bool Diag::ETHERNET=false;
|
||||
bool Diag::LCN=false;
|
||||
bool Diag::RAILCOM=false;
|
||||
bool Diag::WEBSOCKET=false;
|
||||
|
||||
|
||||
|
||||
void StringFormatter::diag( const FSH* input...) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2020-2025, Chris Harlow. All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -30,8 +30,6 @@ class Diag {
|
||||
static bool WITHROTTLE;
|
||||
static bool ETHERNET;
|
||||
static bool LCN;
|
||||
static bool RAILCOM;
|
||||
static bool WEBSOCKET;
|
||||
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2022-2025 Chris Harlow
|
||||
* © 2022 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2024 Herb Morton
|
||||
@ -332,8 +332,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
canDo &= track[t]->trackPWM;
|
||||
}
|
||||
}
|
||||
if (canDo) DIAG(F("HA mode"));
|
||||
else {
|
||||
if (!canDo) {
|
||||
// if we discover that HA mode was globally impossible
|
||||
// we must adjust the trackPWM capabilities
|
||||
FOR_EACH_TRACK(t) {
|
||||
@ -342,7 +341,6 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
}
|
||||
DCCTimer::clearPWM(); // has to be AFTER trackPWM changes because if trackPWM==true this is undone for that track
|
||||
}
|
||||
DCCWaveform::setRailcomPossible(canDo);
|
||||
#else
|
||||
// For ESP32 we just reinitialize the DCC Waveform
|
||||
DCCWaveform::begin();
|
||||
@ -381,7 +379,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
}
|
||||
|
||||
void TrackManager::applyDCSpeed(byte t) {
|
||||
track[t]->setDCSignal(DCC::getLocoSpeedByte(trackDCAddr[t]),
|
||||
track[t]->setDCSignal(DCC::getThrottleSpeedByte(trackDCAddr[t]),
|
||||
DCC::getThrottleFrequency(trackDCAddr[t]));
|
||||
}
|
||||
|
||||
|
211
Websockets.cpp
211
Websockets.cpp
@ -1,211 +0,0 @@
|
||||
/*
|
||||
* © 2023 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/>.
|
||||
*/
|
||||
|
||||
|
||||
/**************************************************
|
||||
HOW IT WORKS
|
||||
|
||||
1) Refer to https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
||||
|
||||
2) When a new client sends in a socket stream, the
|
||||
CommandDistributor pass it to this code
|
||||
checkConnectionString() to check for an HTTP
|
||||
protocol GET requesting a change to websocket protocol.
|
||||
[Note that the WifiInboundHandler has a shortcut to detecting this so that
|
||||
it does not need to use up 500+ bytes of RAM just to get at the one parameter that
|
||||
actually means something.]
|
||||
If that is found, the relevant answer is generated and queued and
|
||||
the CommandDistributor marks this client as a websocket client awaiting connection.
|
||||
Once the outbound handshake has completed, the CommandDistributor promotes the client
|
||||
from awaiting connection to connected websocket so that all
|
||||
future traffic for this client is handled with websocket protocol.
|
||||
|
||||
3) When an input is received from a client marked as websocket,
|
||||
CommandDistributor calls unmask() to strip off the websocket header and
|
||||
un-mask the input bytes. The command distributor will flag the
|
||||
clientid in the ringstream so that anyone transmitting this
|
||||
output will know to handle it differently.
|
||||
|
||||
4) when the Wifi/Ethernet handler needs to transmit the result from the
|
||||
output ring, it recognises the websockets flag and adds the websocket
|
||||
header to the output dynamically.
|
||||
|
||||
*************************************************************/
|
||||
#include <Arduino.h>
|
||||
#include "FSH.h"
|
||||
#include "RingStream.h"
|
||||
#include "libsha1.h"
|
||||
#include "Websockets.h"
|
||||
#include "DIAG.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
// ESP32 runtime or definitions has strlcat_P missing
|
||||
#define strlcat_P strlcat
|
||||
#endif
|
||||
static const char b64_table[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9', '+', '/'
|
||||
};
|
||||
|
||||
bool Websockets::checkConnectionString(byte clientId,byte * cmd, RingStream * outbound ) {
|
||||
// returns true if this input is a websocket connect
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websock check connection"));
|
||||
/* Heuristic suppose this is a websocket GET
|
||||
typically looking like this:
|
||||
|
||||
GET / HTTP/1.1
|
||||
Host: 192.168.1.242:2560
|
||||
Connection: Upgrade
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
|
||||
Upgrade: websocket
|
||||
Origin: null
|
||||
Sec-WebSocket-Version: 13
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Sec-WebSocket-Key: SpRkQKPPNZcO62pYf1X6Yg==
|
||||
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
|
||||
*/
|
||||
|
||||
// check contents to find Sec-WebSocket-Key: and get key up to \n
|
||||
auto keyPos=strstr_P((char*)cmd,(char*)F("Sec-WebSocket-Key: "));
|
||||
if (!keyPos) return false;
|
||||
keyPos+=19; // length of Sec-Websocket-Key:
|
||||
auto endkeypos=strstr(keyPos,"\r");
|
||||
if (!endkeypos) return false;
|
||||
*endkeypos=0;
|
||||
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websock key=\"%s\""),keyPos);
|
||||
// generate the reply key
|
||||
uint8_t sha1HashBin[21] = { 0 }; // 21 to make it base64 div 3
|
||||
char replyKey[100];
|
||||
strlcpy(replyKey,keyPos, sizeof(replyKey));
|
||||
strlcat_P(replyKey,(char*)F("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"), sizeof(replyKey));
|
||||
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websock replykey=%s"),replyKey);
|
||||
|
||||
SHA1_CTX ctx;
|
||||
SHA1Init(&ctx);
|
||||
SHA1Update(&ctx, (unsigned char *)replyKey, strlen(replyKey));
|
||||
SHA1Final(sha1HashBin, &ctx);
|
||||
|
||||
// generate the response and embed the base64 encode
|
||||
// of the key
|
||||
outbound->mark(clientId);
|
||||
outbound->print(F("HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Server: DCCEX-WebSocketsServer\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Origin: null\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"Sec-WebSocket-Protocol: DCCEX\r\n"
|
||||
"Sec-WebSocket-Accept: "));
|
||||
// encode and emit the reply key as base 64
|
||||
auto * tmp=sha1HashBin;
|
||||
for (int i=0;i<7;i++) {
|
||||
outbound->print(b64_table[(tmp[0] & 0xfc) >> 2]);
|
||||
outbound->print(b64_table[((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4)]);
|
||||
outbound->print(b64_table[((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6)]);
|
||||
if (i<6) outbound->print(b64_table[tmp[2] & 0x3f]);
|
||||
tmp+=3;
|
||||
}
|
||||
outbound->print(F("=\r\n\r\n")); // because we have padded 1 byte
|
||||
outbound->commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
byte * Websockets::unmask(byte clientId,RingStream *ring, byte * buffer) {
|
||||
// buffer should have a websocket header
|
||||
//byte opcode=buffer[0] & 0x0f;
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websock in: %x %x %x %x %x %x %x"),
|
||||
buffer[0],buffer[1],buffer[2],buffer[3],
|
||||
buffer[4],buffer[5],buffer[6]);
|
||||
|
||||
byte opcode=buffer[0];
|
||||
bool maskbit=buffer[1]&0x80;
|
||||
int16_t payloadLength=buffer[1]&0x7f;
|
||||
|
||||
byte * mask;
|
||||
if (payloadLength<126) {
|
||||
mask=buffer+2;
|
||||
}
|
||||
else {
|
||||
payloadLength=(buffer[3]<<8)|(buffer[2]);
|
||||
mask=buffer+4;
|
||||
}
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websock op=%x mb=%b pl=%d m=%x %x %x %x"), opcode, maskbit, payloadLength,
|
||||
mask[0],mask[1],mask[2], mask[3]);
|
||||
|
||||
if (opcode==0x89) { // ping
|
||||
DIAG(F("Websock ping"));
|
||||
buffer[0]=0x8a; // pong.. and send it back
|
||||
ring->mark(clientId &0x7f); // dont readjust
|
||||
ring->print((char *)buffer);
|
||||
ring->commit();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (opcode!=0x81) {
|
||||
DIAG(F("Websock unknown opcode 0x%x"),opcode);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
byte * payload=mask+4;
|
||||
for (int i=0;i<payloadLength;i++) {
|
||||
payload[i]^=mask[i%4];
|
||||
}
|
||||
|
||||
if (Diag::WEBSOCKET) DIAG(F("Websoc payload=%s"),payload);
|
||||
|
||||
return payload; // payload will be parsed as normal
|
||||
|
||||
}
|
||||
|
||||
int16_t Websockets::getOutboundHeaderSize(uint16_t dataLength) {
|
||||
return (dataLength>=126)? 4:2;
|
||||
}
|
||||
|
||||
int Websockets::fillOutboundHeader(uint16_t dataLength, byte * buffer) {
|
||||
// text opcode, flag(126= use 2 length bytes, no mask bit) , length
|
||||
buffer[0]=0x81;
|
||||
if (dataLength<126) {
|
||||
buffer[1]=(byte)dataLength;
|
||||
return 2;
|
||||
}
|
||||
buffer[1]=126;
|
||||
buffer[2]=(byte)(dataLength & 0xFF);
|
||||
buffer[3]= (byte)(dataLength>>8);
|
||||
return 4;
|
||||
}
|
||||
|
||||
void Websockets::writeOutboundHeader(Print * stream,uint16_t dataLength) {
|
||||
byte prefix[4];
|
||||
int headerlen=fillOutboundHeader(dataLength,prefix);
|
||||
stream->write(prefix,sizeof(headerlen));
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,8 +2,6 @@
|
||||
© 2023 Paul M. Antoine
|
||||
© 2021 Harald Barth
|
||||
© 2023 Nathan Kellenicki
|
||||
© 2025 Chris Harlow
|
||||
|
||||
|
||||
This file is part of CommandStation-EX
|
||||
|
||||
@ -32,7 +30,6 @@
|
||||
#include "CommandDistributor.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCC.h"
|
||||
#include "Websockets.h"
|
||||
/*
|
||||
#include "soc/rtc_wdt.h"
|
||||
#include "esp_task_wdt.h"
|
||||
@ -381,8 +378,6 @@ void WifiESP::loop() {
|
||||
|
||||
// something to write out?
|
||||
clientId=outboundRing->read();
|
||||
bool useWebsocket=clientId & Websockets::WEBSOCK_CLIENT_MARKER;
|
||||
clientId &= ~ Websockets::WEBSOCK_CLIENT_MARKER;
|
||||
if (clientId >= 0) {
|
||||
// We have data to send in outboundRing
|
||||
// and we have a valid clientId.
|
||||
@ -390,28 +385,25 @@ void WifiESP::loop() {
|
||||
// and then look if it can be sent because
|
||||
// we can not leave it in the ring for ever
|
||||
int count=outboundRing->count();
|
||||
auto wsHeaderLen=useWebsocket? Websockets::getOutboundHeaderSize(count) : 0;
|
||||
{
|
||||
byte buffer[wsHeaderLen + count + 1]; // one extra for '\0'
|
||||
if (useWebsocket) Websockets::fillOutboundHeader(count, buffer);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int c = outboundRing->read();
|
||||
if (!c) {
|
||||
DIAG(F("Ringread fail at %d"), i);
|
||||
break;
|
||||
}
|
||||
// websocket implementations at browser end can barf at \n
|
||||
if (useWebsocket && (c == '\n')) c = '\r';
|
||||
buffer[i + wsHeaderLen] = (char)c;
|
||||
}
|
||||
// buffer filled, end with '\0' so we can use it as C string
|
||||
buffer[wsHeaderLen+count]='\0';
|
||||
char buffer[count+1]; // one extra for '\0'
|
||||
for(int i=0;i<count;i++) {
|
||||
int c = outboundRing->read();
|
||||
if (c >= 0) // Panic check, should never be false
|
||||
buffer[i] = (char)c;
|
||||
else {
|
||||
DIAG(F("Ringread fail at %d"),i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// buffer filled, end with '\0' so we can use it as C string
|
||||
buffer[count]='\0';
|
||||
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
|
||||
if (Diag::WIFI)
|
||||
DIAG(F("SEND%S %d:%s"), useWebsocket?F("ws"):F(""),clientId, buffer+wsHeaderLen);
|
||||
clients[clientId].wifi.write(buffer,count+wsHeaderLen);
|
||||
if (Diag::CMD || Diag::WITHROTTLE)
|
||||
DIAG(F("SEND %d:%s"), clientId, buffer);
|
||||
clients[clientId].wifi.write(buffer,count);
|
||||
} else {
|
||||
DIAG(F("Unsent(%d): %s"), clientId, buffer+wsHeaderLen);
|
||||
DIAG(F("Unsent(%d): %s"), clientId, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
@ -26,7 +26,6 @@
|
||||
#include "RingStream.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "DIAG.h"
|
||||
#include "Websockets.h"
|
||||
|
||||
WifiInboundHandler * WifiInboundHandler::singleton;
|
||||
|
||||
@ -68,13 +67,8 @@ void WifiInboundHandler::loop1() {
|
||||
|
||||
|
||||
if (pendingCipsend && millis()-lastCIPSEND > CIPSENDgap) {
|
||||
// add allowances for websockets
|
||||
bool websocket=clientPendingCIPSEND & Websockets::WEBSOCK_CLIENT_MARKER;
|
||||
byte realClient=clientPendingCIPSEND & ~Websockets::WEBSOCK_CLIENT_MARKER;
|
||||
int16_t realSize=currentReplySize;
|
||||
if (websocket) realSize+=Websockets::getOutboundHeaderSize(currentReplySize);
|
||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), realClient, realSize);
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), realClient,realSize);
|
||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), clientPendingCIPSEND, currentReplySize);
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), clientPendingCIPSEND, currentReplySize);
|
||||
pendingCipsend=false;
|
||||
return;
|
||||
}
|
||||
@ -86,9 +80,7 @@ void WifiInboundHandler::loop1() {
|
||||
int count=inboundRing->count();
|
||||
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"),clientId,count);
|
||||
byte cmd[count+1];
|
||||
// Copy raw bytes to avoid websocket masked data being
|
||||
// confused with a ram-saving flash insert marker.
|
||||
for (int i=0;i<count;i++) cmd[i]=inboundRing->readRawByte();
|
||||
for (int i=0;i<count;i++) cmd[i]=inboundRing->read();
|
||||
cmd[count]=0;
|
||||
if (Diag::WIFI) DIAG(F("%e"),cmd);
|
||||
|
||||
@ -102,9 +94,6 @@ void WifiInboundHandler::loop1() {
|
||||
// This is a Finite State Automation (FSA) handling the inbound bytes from an ES AT command processor
|
||||
|
||||
WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
const char WebSocketKeyName[]="Sec-WebSocket-Key: ";
|
||||
static byte prescanPoint=0;
|
||||
|
||||
while (wifiStream->available()) {
|
||||
int ch = wifiStream->read();
|
||||
|
||||
@ -123,12 +112,9 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
}
|
||||
|
||||
if (ch=='>') {
|
||||
bool websocket=clientPendingCIPSEND & Websockets::WEBSOCK_CLIENT_MARKER;
|
||||
if (Diag::WIFI) DIAG(F("[XMIT %d ws=%b]"),currentReplySize,websocket);
|
||||
if (websocket) Websockets::writeOutboundHeader(wifiStream,currentReplySize);
|
||||
if (Diag::WIFI) DIAG(F("[XMIT %d]"),currentReplySize);
|
||||
for (int i=0;i<currentReplySize;i++) {
|
||||
int cout=outboundRing->read();
|
||||
if (websocket && (cout=='\n')) cout='\r';
|
||||
wifiStream->write(cout);
|
||||
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
||||
}
|
||||
@ -209,19 +195,14 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
break;
|
||||
}
|
||||
if (Diag::WIFI) DIAG(F("Wifi inbound data(%d:%d):"),runningClientId,dataLength);
|
||||
|
||||
// we normally dont read >100 bytes
|
||||
// so assume its an HTTP GET or similar
|
||||
|
||||
if (dataLength<100 && inboundRing->freeSpace()<=(dataLength+1)) {
|
||||
if (inboundRing->freeSpace()<=(dataLength+1)) {
|
||||
// This input would overflow the inbound ring, ignore it
|
||||
loopState=IPD_IGNORE_DATA;
|
||||
if (Diag::WIFI) DIAG(F("Wifi OVERFLOW IGNORING:"));
|
||||
break;
|
||||
}
|
||||
inboundRing->mark(runningClientId);
|
||||
prescanPoint=0;
|
||||
loopState=(dataLength>100)? IPD_PRESCAN: IPD_DATA;
|
||||
loopState=IPD_DATA;
|
||||
break;
|
||||
}
|
||||
dataLength = dataLength * 10 + (ch - '0');
|
||||
@ -236,38 +217,6 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
}
|
||||
break;
|
||||
|
||||
case IPD_PRESCAN: // prescan reading data
|
||||
dataLength--;
|
||||
if (dataLength == 0) {
|
||||
// Nothing found, this input is lost
|
||||
DIAG(F("Wifi prescan for websock not found"));
|
||||
inboundRing->commit();
|
||||
loopState = ANYTHING;
|
||||
}
|
||||
if (ch!=WebSocketKeyName[prescanPoint]) {
|
||||
prescanPoint=0;
|
||||
break;
|
||||
}
|
||||
// matched the next char of the key
|
||||
prescanPoint++;
|
||||
if (WebSocketKeyName[prescanPoint]==0) {
|
||||
if (Diag::WEBSOCKET) DIAG(F("Wifi prescan found"));
|
||||
// prescan has detected full key
|
||||
inboundRing->print(WebSocketKeyName);
|
||||
loopState=IPD_POSTSCAN; // continmue as normal
|
||||
}
|
||||
break;
|
||||
|
||||
case IPD_POSTSCAN: // reading data
|
||||
inboundRing->write(ch);
|
||||
dataLength--;
|
||||
if (ch=='\n') {
|
||||
inboundRing->commit();
|
||||
loopState = IPD_IGNORE_DATA;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case IPD_IGNORE_DATA: // ignoring data that would not fit in inbound ring
|
||||
dataLength--;
|
||||
if (dataLength == 0) loopState = ANYTHING;
|
||||
|
@ -2,7 +2,7 @@
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020-2025 Chris Harlow. All rights reserved.
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -55,8 +55,7 @@ class WifiInboundHandler {
|
||||
IPD6_LENGTH, // got +IPD,c, reading length
|
||||
IPD_DATA, // got +IPD,c,ll,: collecting data
|
||||
IPD_IGNORE_DATA, // got +IPD,c,ll,: ignoring the data that won't fit inblound Ring
|
||||
IPD_PRESCAN, // prescanning data for websocket keys
|
||||
IPD_POSTSCAN, // copyimg data for websocket keys
|
||||
|
||||
GOT_CLIENT_ID, // clientid prefix to CONNECTED / CLOSED
|
||||
GOT_CLIENT_ID2 // clientid prefix to CONNECTED / CLOSED
|
||||
};
|
||||
@ -68,7 +67,7 @@ class WifiInboundHandler {
|
||||
void purgeCurrentCIPSEND();
|
||||
Stream * wifiStream;
|
||||
|
||||
static const int INBOUND_RING = 128;
|
||||
static const int INBOUND_RING = 512;
|
||||
static const int OUTBOUND_RING = sizeof(void*)==2?2048:8192;
|
||||
|
||||
static const int CIPSENDgap=100; // millis() between retries of cipsend.
|
||||
|
@ -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
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
|
21
defines.h
21
defines.h
@ -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
|
||||
|
2813
docs/DoxyfileEXRAIL
2813
docs/DoxyfileEXRAIL
File diff suppressed because it is too large
Load Diff
@ -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)
|
888
docs/_static/css/dccex_theme.css
vendored
888
docs/_static/css/dccex_theme.css
vendored
@ -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;
|
||||
}
|
9
docs/_static/css/sphinx_design_overrides.css
vendored
9
docs/_static/css/sphinx_design_overrides.css
vendored
@ -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;
|
||||
}
|
BIN
docs/_static/images/favicon.ico
vendored
BIN
docs/_static/images/favicon.ico
vendored
Binary file not shown.
Before Width: | Height: | Size: 627 KiB |
BIN
docs/_static/images/logo.png
vendored
BIN
docs/_static/images/logo.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
BIN
docs/_static/images/product-logo-ex-rail.png
vendored
BIN
docs/_static/images/product-logo-ex-rail.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
94
docs/conf.py
94
docs/conf.py
@ -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 = ()
|
@ -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
|
@ -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
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
User-agent: *
|
||||
|
||||
Sitemap: https://dcc-ex.com/CommandStation-EX/sitemap.xml
|
206
libsha1.cpp
206
libsha1.cpp
@ -1,206 +0,0 @@
|
||||
// For DCC-EX: This file downloaded from:
|
||||
// https://github.com/Links2004/arduinoWebSockets
|
||||
// All due credit to Steve Reid
|
||||
|
||||
/* from valgrind tests */
|
||||
|
||||
/* ================ sha1.c ================ */
|
||||
/*
|
||||
SHA-1 in C
|
||||
By Steve Reid <steve@edmweb.com>
|
||||
100% Public Domain
|
||||
|
||||
Test Vectors (from FIPS PUB 180-1)
|
||||
"abc"
|
||||
A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
|
||||
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
|
||||
84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
|
||||
A million repetitions of "a"
|
||||
34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
|
||||
*/
|
||||
|
||||
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
|
||||
/* #define SHA1HANDSOFF * Copies data before messing with it. */
|
||||
|
||||
// DCC-EX removed #if !defined(ESP8266) && !defined(ESP32)
|
||||
|
||||
#define SHA1HANDSOFF
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "libsha1.h"
|
||||
|
||||
|
||||
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
|
||||
|
||||
/* blk0() and blk() perform the initial expand. */
|
||||
/* I got the idea of expanding during the round function from SSLeay */
|
||||
#if BYTE_ORDER == LITTLE_ENDIAN
|
||||
#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \
|
||||
|(rol(block->l[i],8)&0x00FF00FF))
|
||||
#elif BYTE_ORDER == BIG_ENDIAN
|
||||
#define blk0(i) block->l[i]
|
||||
#else
|
||||
#error "Endianness not defined!"
|
||||
#endif
|
||||
#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
|
||||
^block->l[(i+2)&15]^block->l[i&15],1))
|
||||
|
||||
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
|
||||
#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
|
||||
#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
|
||||
#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
|
||||
#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
|
||||
#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);
|
||||
|
||||
|
||||
/* Hash a single 512-bit block. This is the core of the algorithm. */
|
||||
|
||||
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64])
|
||||
{
|
||||
uint32_t a, b, c, d, e;
|
||||
typedef union {
|
||||
unsigned char c[64];
|
||||
uint32_t l[16];
|
||||
} CHAR64LONG16;
|
||||
#ifdef SHA1HANDSOFF
|
||||
CHAR64LONG16 block[1]; /* use array to appear as a pointer */
|
||||
memcpy(block, buffer, 64);
|
||||
#else
|
||||
/* The following had better never be used because it causes the
|
||||
* pointer-to-const buffer to be cast into a pointer to non-const.
|
||||
* And the result is written through. I threw a "const" in, hoping
|
||||
* this will cause a diagnostic.
|
||||
*/
|
||||
CHAR64LONG16* block = (const CHAR64LONG16*)buffer;
|
||||
#endif
|
||||
/* Copy context->state[] to working vars */
|
||||
a = state[0];
|
||||
b = state[1];
|
||||
c = state[2];
|
||||
d = state[3];
|
||||
e = state[4];
|
||||
/* 4 rounds of 20 operations each. Loop unrolled. */
|
||||
R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3);
|
||||
R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7);
|
||||
R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11);
|
||||
R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15);
|
||||
R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19);
|
||||
R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23);
|
||||
R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27);
|
||||
R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31);
|
||||
R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35);
|
||||
R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39);
|
||||
R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43);
|
||||
R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47);
|
||||
R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51);
|
||||
R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55);
|
||||
R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59);
|
||||
R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63);
|
||||
R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67);
|
||||
R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71);
|
||||
R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75);
|
||||
R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79);
|
||||
/* Add the working vars back into context.state[] */
|
||||
state[0] += a;
|
||||
state[1] += b;
|
||||
state[2] += c;
|
||||
state[3] += d;
|
||||
state[4] += e;
|
||||
/* Wipe variables */
|
||||
a = b = c = d = e = 0;
|
||||
#ifdef SHA1HANDSOFF
|
||||
memset(block, '\0', sizeof(block));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/* SHA1Init - Initialize new context */
|
||||
|
||||
void SHA1Init(SHA1_CTX* context)
|
||||
{
|
||||
/* SHA1 initialization constants */
|
||||
context->state[0] = 0x67452301;
|
||||
context->state[1] = 0xEFCDAB89;
|
||||
context->state[2] = 0x98BADCFE;
|
||||
context->state[3] = 0x10325476;
|
||||
context->state[4] = 0xC3D2E1F0;
|
||||
context->count[0] = context->count[1] = 0;
|
||||
}
|
||||
|
||||
|
||||
/* Run your data through this. */
|
||||
|
||||
void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len)
|
||||
{
|
||||
uint32_t i, j;
|
||||
|
||||
j = context->count[0];
|
||||
if ((context->count[0] += len << 3) < j)
|
||||
context->count[1]++;
|
||||
context->count[1] += (len>>29);
|
||||
j = (j >> 3) & 63;
|
||||
if ((j + len) > 63) {
|
||||
memcpy(&context->buffer[j], data, (i = 64-j));
|
||||
SHA1Transform(context->state, context->buffer);
|
||||
for ( ; i + 63 < len; i += 64) {
|
||||
SHA1Transform(context->state, &data[i]);
|
||||
}
|
||||
j = 0;
|
||||
}
|
||||
else i = 0;
|
||||
memcpy(&context->buffer[j], &data[i], len - i);
|
||||
}
|
||||
|
||||
|
||||
/* Add padding and return the message digest. */
|
||||
|
||||
void SHA1Final(unsigned char digest[20], SHA1_CTX* context)
|
||||
{
|
||||
unsigned i;
|
||||
unsigned char finalcount[8];
|
||||
unsigned char c;
|
||||
|
||||
#if 0 /* untested "improvement" by DHR */
|
||||
/* Convert context->count to a sequence of bytes
|
||||
* in finalcount. Second element first, but
|
||||
* big-endian order within element.
|
||||
* But we do it all backwards.
|
||||
*/
|
||||
unsigned char *fcp = &finalcount[8];
|
||||
|
||||
for (i = 0; i < 2; i++)
|
||||
{
|
||||
uint32_t t = context->count[i];
|
||||
int j;
|
||||
|
||||
for (j = 0; j < 4; t >>= 8, j++)
|
||||
*--fcp = (unsigned char) t;
|
||||
}
|
||||
#else
|
||||
for (i = 0; i < 8; i++) {
|
||||
finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)]
|
||||
>> ((3-(i & 3)) * 8) ) & 255); /* Endian independent */
|
||||
}
|
||||
#endif
|
||||
c = 0200;
|
||||
SHA1Update(context, &c, 1);
|
||||
while ((context->count[0] & 504) != 448) {
|
||||
c = 0000;
|
||||
SHA1Update(context, &c, 1);
|
||||
}
|
||||
SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
|
||||
for (i = 0; i < 20; i++) {
|
||||
digest[i] = (unsigned char)
|
||||
((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255);
|
||||
}
|
||||
/* Wipe variables */
|
||||
memset(context, '\0', sizeof(*context));
|
||||
memset(&finalcount, '\0', sizeof(finalcount));
|
||||
}
|
||||
/* ================ end of sha1.c ================ */
|
||||
|
||||
|
||||
// DCC-EX Removed: #endif
|
26
libsha1.h
26
libsha1.h
@ -1,26 +0,0 @@
|
||||
// For DCC-EX: This file downloaded from:
|
||||
// https://github.com/Links2004/arduinoWebSockets
|
||||
// All due credit to Steve Reid
|
||||
|
||||
/* ================ sha1.h ================ */
|
||||
/*
|
||||
SHA-1 in C
|
||||
By Steve Reid <steve@edmweb.com>
|
||||
100% Public Domain
|
||||
*/
|
||||
|
||||
// DCC-EX REMOVED #if !defined(ESP8266) && !defined(ESP32)
|
||||
#ifndef libsha1_h
|
||||
#define libsha1_h
|
||||
typedef struct {
|
||||
uint32_t state[5];
|
||||
uint32_t count[2];
|
||||
unsigned char buffer[64];
|
||||
} SHA1_CTX;
|
||||
|
||||
void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]);
|
||||
void SHA1Init(SHA1_CTX* context);
|
||||
void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len);
|
||||
void SHA1Final(unsigned char digest[20], SHA1_CTX* context);
|
||||
|
||||
#endif
|
@ -146,14 +146,7 @@ void halSetup() {
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, 40);
|
||||
|
||||
// Alternative form to initialize 8 pins as output
|
||||
// INT pin -1, when INT is not used
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, -1, 255); 8 pins High
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0); 8 pins Low
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0b11000010);
|
||||
// pins listed sequentially from 7 to 0
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a PCF8575 16-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
|
27
objdump.bat
27
objdump.bat
@ -1,17 +1,16 @@
|
||||
ECHO ON
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\arduino\sketches\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||
SET DUMP=%TEMP%\OBJDUMP.txt
|
||||
echo Most recent subfolder: %ELF% >%DUMP%
|
||||
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\arduino_build_* /b /ad-h /t:c /od') DO SET a=%%i
|
||||
echo Most recent subfolder: %a% >%TMP%\OBJDUMP_%a%.txt
|
||||
SET ELF=%TMP%\%a%\CommandStation-EX.ino.elf
|
||||
set PATH="C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\";%PATH%
|
||||
avr-objdump --private=mem-usage %ELF% >>%DUMP%
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||
avr-objdump -x -C %ELF% | find ".text" | sort /+25 /R >>%DUMP%
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||
avr-objdump -x -C %ELF% | find ".data" | sort /+25 /R >>%DUMP%
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||
avr-objdump -x -C %ELF% | find ".bss" | sort /+25 /R >>%DUMP%
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||
avr-objdump -D -S %ELF% >>%DUMP%
|
||||
%DUMP%
|
||||
avr-objdump --private=mem-usage %ELF% >>%TMP%\OBJDUMP_%a%.txt
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
||||
avr-objdump -x -C %ELF% | find ".text" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
||||
avr-objdump -x -C %ELF% | find ".data" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
||||
avr-objdump -x -C %ELF% | find ".bss" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
||||
avr-objdump -D -S %ELF% >>%TMP%\OBJDUMP_%a%.txt
|
||||
%TMP%\OBJDUMP_%a%.txt
|
||||
EXIT
|
||||
|
@ -11,12 +11,11 @@
|
||||
[platformio]
|
||||
default_envs =
|
||||
mega2560
|
||||
; uno
|
||||
; nano
|
||||
uno
|
||||
nano
|
||||
ESP32
|
||||
Nucleo-F411RE
|
||||
Nucleo-F446RE
|
||||
Nucleo-F429ZI
|
||||
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
|
||||
@ -193,7 +194,7 @@ monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
[env:Nucleo-F411RE]
|
||||
platform = ststm32 @ 19.0.0
|
||||
platform = ststm32 @ 17.6.0
|
||||
board = nucleo_f411re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@ -202,7 +203,7 @@ monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
[env:Nucleo-F446RE]
|
||||
platform = ststm32 @ 19.0.0
|
||||
platform = ststm32 @ 17.6.0
|
||||
board = nucleo_f446re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@ -214,7 +215,7 @@ monitor_echo = yes
|
||||
; tested as yet
|
||||
;
|
||||
[env:Nucleo-F401RE]
|
||||
platform = ststm32 @ 19.0.0
|
||||
platform = ststm32 @ 17.6.0
|
||||
board = nucleo_f401re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@ -227,7 +228,7 @@ monitor_echo = yes
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
; [env:Nucleo-F413ZH]
|
||||
; platform = ststm32 @ 19.0.0
|
||||
; platform = ststm32 @ 17.6.0
|
||||
; board = nucleo_f413zh
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
@ -239,7 +240,7 @@ monitor_echo = yes
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
[env:Nucleo-F446ZE]
|
||||
platform = ststm32 @ 19.0.0
|
||||
platform = ststm32 @ 17.6.0
|
||||
board = nucleo_f446ze
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@ -251,7 +252,7 @@ monitor_echo = yes
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
; [env:Nucleo-F412ZG]
|
||||
; platform = ststm32 @ 19.0.0
|
||||
; platform = ststm32 @ 17.6.0
|
||||
; board = nucleo_f412zg
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
@ -260,40 +261,46 @@ 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
|
||||
platform = ststm32 @ 17.6.0
|
||||
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
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
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
|
||||
platform = ststm32 @ 17.6.0
|
||||
; board = nucleo_f439zi
|
||||
; Temporarily treat it as an F429ZI (they are code compatible) until
|
||||
; the PR to PlatformIO to update the F439ZI JSON file is available
|
||||
; PMA - 28-Sep-2024
|
||||
board = nucleo_f429zi
|
||||
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
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
|
59
version.h
59
version.h
@ -3,54 +3,17 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
#define VERSION "5.5.28"
|
||||
// 5.5.28 - DCC Queue memory leak fix
|
||||
// 5.5.27 - PCF8574 output pin initialization parameter
|
||||
// 5.5.26 - PCA9554 and TCA9554/9534 I2C 8-bit GPIO expander drivers
|
||||
// 5.2.25 - IO_Bitmap and assicated Exrail macros
|
||||
// 5.5.24 - SensorCAM in I2C scan and automatically setClock
|
||||
// 5.5.23 - Reminder loop Idle packet optimization
|
||||
// 5.5.22 - (5.4.9) Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
|
||||
// - DCC 5mS gap to same loco DCC packet restriction
|
||||
// - Catch up MASTER for ESP32 IDF version check
|
||||
// - Catch up MASTER for Serial input length check
|
||||
// - Catch up MASTER for JOIN/POWER order change
|
||||
// 5.5.21 - Backed out the broken merge with frequency change and
|
||||
// 5.5.20 - EXRAIL SET/RESET assert fix
|
||||
// 5.5.19 - Railcom change to use RailcomCollector device
|
||||
// 5.5.18 - New STASH internals
|
||||
// - EXRAIL IFSTASH/CLEAR_ANY_STASH
|
||||
// - <JM CLEAR ANY id> to clear any stash with loco id
|
||||
// - See Release_Notes/Stash.md
|
||||
// 5.5.17 - Extensive new compile time checking in exrail scripts (duplicate sequences etc), no function change
|
||||
// 5.5.16 - DOXYGEN comments in EXRAIL2MacroReset.h
|
||||
// 5.5.15 - Support for F429ZI/F329ZI
|
||||
// - Own mDNS support for (wired) Ethernet
|
||||
// 5.5.14 - DCC Non-blocking packet queue with priority
|
||||
// 5.5.13 - Update STM32duino core to v19.0.0. for updated PeripheralPins.c in preparation for F429/439ZI Ethernet support
|
||||
// 5.5.12 - Websocket support (wifi only)
|
||||
// 5.5.11 - (5.4.2) accessory command reverse
|
||||
// 5.5.10 - CamParser fix
|
||||
// 5.5.9 - (5.4.3) fix changeFn for functions 29..31
|
||||
// 5.5.8 - EXSensorCam clean up to match other filters and
|
||||
// - avoid need for config.h settings
|
||||
// - Test: IO_I2CDFPlayer.h inserted 10mS deleay in Init_SC16IS752() just after soft-reset for board with 1.8432 Mhz xtal
|
||||
// - IO_I2CDFPlayer.h: fixed 2 compiler errors as the compilers are getting stricter
|
||||
// 5.5.7 - ESP32 bugfix packet buffer race (as 5.4.1)
|
||||
// 5.5.6 - Fix ESP32 build bug caused by include reference loop
|
||||
// 5.5.5 - Railcom implementation with IO_I2CRailcom driver
|
||||
// - response analysis and block management.
|
||||
// - <r locoid cv> POM read using Railcom.
|
||||
// - See Release_notes/Railcom.md
|
||||
// 5.5.4 - Split ESP32 from DCCWaveform to DCCWaveformRMT
|
||||
// - Railcom Cutout control (DCCTimerAVR Mega only so far)
|
||||
// 5.5.3 - EXRAIL ESTOPALL,XPOM,
|
||||
// - Bugfix RESERVE to cause ESTOP.(was STOP)
|
||||
// - Correct direction sync after manual throttle change.
|
||||
// - plus ONBLOCKENTER/EXIT in preparation for Railcom
|
||||
// 5.5.2 - DS1307 Real Time clock
|
||||
// 5.5.1 - Momentum
|
||||
// 5.5.0 - New version on devel
|
||||
#define VERSION "5.4.11"
|
||||
// 5.4.11 - Feature: Enable sniffer on CSB-1
|
||||
// 5.4.10 - Bugfix: MEGA DCC waveform starvation (sends too many idles)
|
||||
// 5.4.9 - Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
|
||||
// - <W CONSIST longaddr> handles non-compliant decoders which NAK cv 20 writes.
|
||||
// 5.4.8 - Bugfix: Insert idle packet at end of speed reminder loop; treat all function groups equal
|
||||
// 5.4.7 - Bugfix: EXRAIL fix CLEAR_ALL_STASH
|
||||
// 5.4.6 - Bugfix: Do not drop further commands in same packet
|
||||
// 5.4.5 - ESP32: Better detection of correct IDF version
|
||||
// - track power is always turned on after setJoin() not by setJoin()
|
||||
// 5.4.4 - bugfix in parser, input buffer overrun and trailing > that did break <+>
|
||||
// 5.4.3 - bugfix changeFn for functions 29..31
|
||||
// 5.4.2 - Reversed turnout bugfix
|
||||
// 5.4.1 - ESP32 bugfix packet buffer race
|
||||
|
Loading…
x
Reference in New Issue
Block a user