mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-04-21 12:31:19 +02:00
Compare commits
20 Commits
89a5e31cba
...
0ae7749556
Author | SHA1 | Date | |
---|---|---|---|
|
0ae7749556 | ||
|
f3b87877ef | ||
|
07691e3985 | ||
|
d14aa46d51 | ||
|
2b50e31e50 | ||
|
ee8f6eea1f | ||
|
fb6070784e | ||
|
2f1d5b993c | ||
|
9054d8d9f5 | ||
|
865f75dda4 | ||
|
b40fa779a6 | ||
|
2115ada2a1 | ||
|
58b180603a | ||
|
95d90aa337 | ||
|
137008ceb3 | ||
|
4ec9a62ab6 | ||
|
830de850a9 | ||
|
c28965c58d | ||
|
0476b9c1d8 | ||
|
ba9ca1ccad |
100
CamParser.cpp
100
CamParser.cpp
@ -1,31 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* © 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.03 Sep 2024
|
//sensorCAM parser.cpp version 3.06 Jan 2025
|
||||||
|
#include "DCCEXParser.h"
|
||||||
#include "CamParser.h"
|
#include "CamParser.h"
|
||||||
#include "FSH.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 ver=30177;
|
||||||
const int16_t ve =2899;
|
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[]) {
|
bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
||||||
(void)stream; // probably unused parameter
|
(void)stream; // probably unused parameter
|
||||||
VPIN vpin=EXSensorCAM::CAMBaseVpin; //use current CAM selection
|
if (CAMBaseVpin==0) CAMBaseVpin=CAMVPINS[0]; // default to CAM 1.
|
||||||
|
VPIN vpin=CAMBaseVpin; //use current CAM selection
|
||||||
|
|
||||||
if (paramCount==0) {
|
if (paramCount==0) {
|
||||||
DIAG(F("vpin:%d EXSensorCAMs defined at Vpins #1@ %d #2@ %d #3@ %d"),vpin,CAMVPINS[1],CAMVPINS[2],CAMVPINS[3]);
|
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]);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
uint8_t camop=p[0]; // cam oprerator
|
uint8_t camop=p[0]; // cam oprerator
|
||||||
@ -33,44 +59,45 @@ bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
|||||||
int16_t param3=9999; // =0 could invoke parameter changes. & -1 gives later errors
|
int16_t param3=9999; // =0 could invoke parameter changes. & -1 gives later errors
|
||||||
|
|
||||||
if(camop=='C'){
|
if(camop=='C'){
|
||||||
if(p[1]>=100) EXSensorCAM::CAMBaseVpin=p[1];
|
if(p[1]>=100) CAMBaseVpin=p[1];
|
||||||
if(p[1]<4) EXSensorCAM::CAMBaseVpin=CAMVPINS[p[1]];
|
if(p[1]<=vpcount && p[1]>0) CAMBaseVpin=CAMVPINS[p[1]-1];
|
||||||
DIAG(F("CAM base Vpin: %c %d "),p[0],EXSensorCAM::CAMBaseVpin);
|
DIAG(F("CAM base Vpin: %c %d "),p[0],CAMBaseVpin);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (camop<100) { //switch CAM# if p[1] dictates
|
if (camop<100) { //switch CAM# if p[1] dictates
|
||||||
if(p[1]>=100 && p[1]<400) { //limits to CAM# 1 to 3 for now
|
if(p[1]>=100 && p[1]<=(vpcount*100+99)) { //limits to CAM# 1 to 4 for now
|
||||||
vpin=CAMVPINS[p[1]/100];
|
vpin=CAMVPINS[p[1]/100-1];
|
||||||
EXSensorCAM::CAMBaseVpin=vpin;
|
CAMBaseVpin=vpin;
|
||||||
DIAG(F("switching to CAM %d baseVpin:%d"),p[1]/100,vpin);
|
DIAG(F("switching to CAM %d baseVpin:%d"),p[1]/100,vpin);
|
||||||
p[1]=p[1]%100; //strip off CAM #
|
p[1]=p[1]%100; //strip off CAM #
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (EXSensorCAM::CAMBaseVpin==0) return false; // no cam defined
|
if (CAMBaseVpin==0) {DIAG(F("<n Error: Invalid CAM selected, default to CAM1>"));
|
||||||
|
return false; // cam not defined
|
||||||
|
}
|
||||||
|
|
||||||
// send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser
|
// send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser
|
||||||
switch(paramCount) {
|
switch(paramCount) {
|
||||||
case 1: //<N ver> produces '^'
|
case 1: //<N ver> produces '^'
|
||||||
if((p[0] == ve) || (p[0] == ver) || (p[0] == 'V')) camop='^';
|
if((camop == 'V') || (p[0] == ve) || (p[0] == ver) ) camop='^';
|
||||||
if (STRCHR_P((const char *)F("EFGMQRVW^"),camop) == nullptr) return false;
|
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=='Q') param3=10; //<NQ> for activation state of all 10 banks of sensors
|
||||||
if (camop=='F') camop=']'; //<NF> for Reset/Finish webCAM.
|
if (camop=='F') camop=']'; //<NF> for Reset/Finish webCAM.
|
||||||
break; // F Coded as ']' else conflicts with <Nf %%>
|
break; // F Coded as ']' else conflicts with <Nf %%>
|
||||||
|
|
||||||
case 2: //<N camop p1>
|
case 2: //<N camop p1>
|
||||||
if (STRCHR_P((const char *)F("ABFILMNOPQRSTUV"),camop)==nullptr) return false;
|
if (STRCHR_P((const char *)F("ABFHILMNOPQRSTUV"),camop)==nullptr) return false;
|
||||||
param1=p[1];
|
param1=p[1];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3: //<N vpin rowY colx > or <N cmd p1 p2>
|
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[0]>=100) { //vpin - i.e. NOT 'A' through 'Z'
|
||||||
if (p[1]>236 || p[1]<0) return false; //row
|
if (p[1]>236 || p[1]<0) return false; //row
|
||||||
if (p[2]>316 || p[2]<0) return false; //column
|
if (p[2]>316 || p[2]<0) return false; //column
|
||||||
camop=0x80; // special 'a' case for IO_SensorCAM
|
camop=0x80; // special 'a' case for IO_SensorCAM
|
||||||
vpin = p[0];
|
vpin = p[0];
|
||||||
}else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false;
|
}else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false;
|
||||||
|
camop=p[0];
|
||||||
param1 = p[1];
|
param1 = p[1];
|
||||||
param3 = p[2];
|
param3 = p[2];
|
||||||
break;
|
break;
|
||||||
@ -93,3 +120,22 @@ bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
|
|||||||
IODevice::writeAnalogue(vpin,param1,camop,param3);
|
IODevice::writeAnalogue(vpin,param1,camop,param3);
|
||||||
return true;
|
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);
|
||||||
|
}
|
28
CamParser.h
28
CamParser.h
@ -1,3 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* © 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
|
#ifndef CamParser_H
|
||||||
#define CamParser_H
|
#define CamParser_H
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
@ -5,7 +27,13 @@
|
|||||||
|
|
||||||
class CamParser {
|
class CamParser {
|
||||||
public:
|
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 bool parseN(Print * stream, byte paramCount, int16_t p[]);
|
||||||
|
static VPIN CAMBaseVpin;
|
||||||
|
static VPIN CAMVPINS[];
|
||||||
|
static int vpcount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
#include "DCC.h"
|
#include "DCC.h"
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#include "StringFormatter.h"
|
#include "StringFormatter.h"
|
||||||
|
#include "Websockets.h"
|
||||||
|
|
||||||
// variables to hold clock time
|
// variables to hold clock time
|
||||||
int16_t lastclocktime;
|
int16_t lastclocktime;
|
||||||
@ -44,6 +45,7 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
|
|||||||
broadcastBufferWriter->flush();
|
broadcastBufferWriter->flush();
|
||||||
StringFormatter::send(broadcastBufferWriter, msg...);
|
StringFormatter::send(broadcastBufferWriter, msg...);
|
||||||
broadcastToClients(type);
|
broadcastToClients(type);
|
||||||
|
if (type==COMMAND_TYPE) broadcastToClients(WEBSOCKET_TYPE);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// on a single USB connection config, write direct to Serial and ignore flush/shove
|
// on a single USB connection config, write direct to Serial and ignore flush/shove
|
||||||
@ -56,14 +58,22 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
|
|||||||
#ifdef CD_HANDLE_RING
|
#ifdef CD_HANDLE_RING
|
||||||
// wifi or ethernet ring streams with multiple client types
|
// wifi or ethernet ring streams with multiple client types
|
||||||
RingStream * CommandDistributor::ring=0;
|
RingStream * CommandDistributor::ring=0;
|
||||||
CommandDistributor::clientType CommandDistributor::clients[8]={
|
CommandDistributor::clientType CommandDistributor::clients[20]={
|
||||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
|
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||||
|
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||||
|
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||||
|
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||||
|
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
|
||||||
|
|
||||||
// Parse is called by Withrottle or Ethernet interface to determine which
|
// Parse is called by Wifi or Ethernet interface to determine which
|
||||||
// protocol the client is using and call the appropriate part of dcc++Ex
|
// protocol the client is using and call the appropriate part of dcc++Ex
|
||||||
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
|
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
|
||||||
if (Diag::WIFI && Diag::CMD)
|
if (clientId>=sizeof (clients)) {
|
||||||
DIAG(F("Parse C=%d T=%d B=%s"),clientId, clients[clientId], buffer);
|
// Caution, diag dump of buffer could corrupt ringstream
|
||||||
|
// if headed by websocket bytes.
|
||||||
|
DIAG(F("::parse invalid client=%d"),clientId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ring=stream;
|
ring=stream;
|
||||||
|
|
||||||
// First check if the client is not known
|
// First check if the client is not known
|
||||||
@ -72,22 +82,40 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
|
|||||||
// client is using the DCC++ protocol where all commands start
|
// client is using the DCC++ protocol where all commands start
|
||||||
// with '<'
|
// with '<'
|
||||||
if (clients[clientId] == NONE_TYPE) {
|
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] == '<')
|
if (buffer[0] == '<')
|
||||||
clients[clientId]=COMMAND_TYPE;
|
clients[clientId]=COMMAND_TYPE;
|
||||||
else
|
else
|
||||||
clients[clientId]=WITHROTTLE_TYPE;
|
clients[clientId]=WITHROTTLE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark buffer that is sent to parser
|
// after first inbound transmission the websocket is connected
|
||||||
ring->mark(clientId);
|
if (clients[clientId]==WEBSOCK_CONNECTING_TYPE)
|
||||||
|
clients[clientId]=WEBSOCKET_TYPE;
|
||||||
|
|
||||||
|
|
||||||
|
// mark buffer that is sent to parser
|
||||||
// When type is known, send the string
|
// When type is known, send the string
|
||||||
// to the right parser
|
// to the right parser
|
||||||
if (clients[clientId] == COMMAND_TYPE) {
|
if (clients[clientId] == COMMAND_TYPE) {
|
||||||
|
ring->mark(clientId);
|
||||||
DCCEXParser::parse(stream, buffer, ring);
|
DCCEXParser::parse(stream, buffer, ring);
|
||||||
} else if (clients[clientId] == WITHROTTLE_TYPE) {
|
} else if (clients[clientId] == WITHROTTLE_TYPE) {
|
||||||
|
ring->mark(clientId);
|
||||||
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
|
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) {
|
if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {
|
||||||
// The commit call will either write the length bytes
|
// The commit call will either write the length bytes
|
||||||
@ -131,7 +159,7 @@ void CommandDistributor::broadcastToClients(clientType type) {
|
|||||||
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
|
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
|
||||||
if (clients[clientId]==type) {
|
if (clients[clientId]==type) {
|
||||||
//DIAG(F("CD mark client %d"), clientId);
|
//DIAG(F("CD mark client %d"), clientId);
|
||||||
ring->mark(clientId);
|
ring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));
|
||||||
ring->print(broadcastBufferWriter->getString());
|
ring->print(broadcastBufferWriter->getString());
|
||||||
//DIAG(F("CD commit client %d"), clientId);
|
//DIAG(F("CD commit client %d"), clientId);
|
||||||
ring->commit();
|
ring->commit();
|
||||||
@ -191,7 +219,9 @@ void CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate, byte
|
|||||||
// CAH. DIAG removed because LCD does it anyway.
|
// CAH. DIAG removed because LCD does it anyway.
|
||||||
LCD(6,F("Clk Time:%d Sp %d"), clocktime, clockrate);
|
LCD(6,F("Clk Time:%d Sp %d"), clocktime, clockrate);
|
||||||
// look for an event for this time
|
// look for an event for this time
|
||||||
|
#ifdef EXRAIL_ACTIVE
|
||||||
RMFT2::clockEvent(clocktime,1);
|
RMFT2::clockEvent(clocktime,1);
|
||||||
|
#endif
|
||||||
// Now tell everyone else what the time is.
|
// Now tell everyone else what the time is.
|
||||||
CommandDistributor::broadcastClockTime(clocktime, clockrate);
|
CommandDistributor::broadcastClockTime(clocktime, clockrate);
|
||||||
lastclocktime = clocktime;
|
lastclocktime = clocktime;
|
||||||
|
@ -37,13 +37,13 @@
|
|||||||
|
|
||||||
class CommandDistributor {
|
class CommandDistributor {
|
||||||
public:
|
public:
|
||||||
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE};
|
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE};
|
||||||
private:
|
private:
|
||||||
static void broadcastToClients(clientType type);
|
static void broadcastToClients(clientType type);
|
||||||
static StringBuffer * broadcastBufferWriter;
|
static StringBuffer * broadcastBufferWriter;
|
||||||
#ifdef CD_HANDLE_RING
|
#ifdef CD_HANDLE_RING
|
||||||
static RingStream * ring;
|
static RingStream * ring;
|
||||||
static clientType clients[8];
|
static clientType clients[20];
|
||||||
#endif
|
#endif
|
||||||
public :
|
public :
|
||||||
static void parse(byte clientId,byte* buffer, RingStream * ring);
|
static void parse(byte clientId,byte* buffer, RingStream * ring);
|
||||||
|
11
DCC.cpp
11
DCC.cpp
@ -268,14 +268,9 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
|||||||
|
|
||||||
// Flip function state (used from withrottle protocol)
|
// Flip function state (used from withrottle protocol)
|
||||||
void DCC::changeFn( int cab, int16_t functionNumber) {
|
void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||||
if (cab<=0 || functionNumber>31) return;
|
auto currentValue=getFn(cab,functionNumber);
|
||||||
auto slot=lookupSpeedTable(cab);
|
if (currentValue<0) return; // function not valid for change
|
||||||
unsigned long funcmask = (1UL<<functionNumber);
|
setFn(cab,functionNumber, currentValue?false:true);
|
||||||
slot->functions ^= funcmask;
|
|
||||||
if (functionNumber <= 28) {
|
|
||||||
updateGroupflags(slot->groupFlags, functionNumber);
|
|
||||||
}
|
|
||||||
CommandDistributor::broadcastLoco(slot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report function state (used from withrottle protocol)
|
// Report function state (used from withrottle protocol)
|
||||||
|
@ -238,6 +238,7 @@ 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[]);
|
extern __attribute__((weak)) void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||||
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
|
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
|
||||||
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
|
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
|
||||||
|
FILTER_CALLBACK DCCEXParser::filterCamParserCallback = 0;
|
||||||
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
|
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
|
||||||
|
|
||||||
// deprecated
|
// deprecated
|
||||||
@ -249,6 +250,10 @@ void DCCEXParser::setRMFTFilter(FILTER_CALLBACK filter)
|
|||||||
{
|
{
|
||||||
filterRMFTCallback = filter;
|
filterRMFTCallback = filter;
|
||||||
}
|
}
|
||||||
|
void DCCEXParser::setCamParserFilter(FILTER_CALLBACK filter)
|
||||||
|
{
|
||||||
|
filterCamParserCallback = filter;
|
||||||
|
}
|
||||||
void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
|
void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
|
||||||
{
|
{
|
||||||
atCommandCallback = callback;
|
atCommandCallback = callback;
|
||||||
@ -304,6 +309,8 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
|||||||
filterCallback(stream, opcode, params, p);
|
filterCallback(stream, opcode, params, p);
|
||||||
if (filterRMFTCallback && opcode!='\0')
|
if (filterRMFTCallback && opcode!='\0')
|
||||||
filterRMFTCallback(stream, opcode, params, p);
|
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
|
// Functions return from this switch if complete, break from switch implies error <X> to send
|
||||||
switch (opcode)
|
switch (opcode)
|
||||||
@ -401,7 +408,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
|||||||
) break;
|
) break;
|
||||||
// Honour the configuration option (config.h) which allows the <a> command to be reversed
|
// Honour the configuration option (config.h) which allows the <a> command to be reversed
|
||||||
// Because of earlier confusion we need to do the same thing under both defines
|
// Because of earlier confusion we need to do the same thing under both defines
|
||||||
#if defined(DCC_ACCESSORY_COMMAND_REVERSE) || defined(DCC_ACCESSORY_RCN_213)
|
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
|
||||||
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
|
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
|
||||||
#else
|
#else
|
||||||
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
|
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
|
||||||
@ -898,15 +905,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
|||||||
if (parseI(stream, params, p))
|
if (parseI(stream, params, p))
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
#endif
|
|
||||||
#ifndef IO_NO_HAL
|
|
||||||
case 'N': // <N commands for SensorCam
|
|
||||||
if (CamParser::parseN(stream,params,p)) return;
|
|
||||||
break;
|
|
||||||
#endif
|
#endif
|
||||||
case '/': // implemented in EXRAIL parser
|
case '/': // implemented in EXRAIL parser
|
||||||
case 'L': // LCC interface implemented in EXRAIL parser
|
case 'L': // LCC interface implemented in EXRAIL parser
|
||||||
break; // Will <X> if not intercepted by EXRAIL
|
case 'N': // interface implemented in CamParser
|
||||||
|
break; // Will <X> if not intercepted by filters
|
||||||
|
|
||||||
#ifndef DISABLE_VDPY
|
#ifndef DISABLE_VDPY
|
||||||
case '@': // JMRI saying "give me virtual LCD msgs"
|
case '@': // JMRI saying "give me virtual LCD msgs"
|
||||||
@ -1252,6 +1255,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
|||||||
case "LCN"_hk: // <D LCN ON/OFF>
|
case "LCN"_hk: // <D LCN ON/OFF>
|
||||||
Diag::LCN = onOff;
|
Diag::LCN = onOff;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
|
||||||
|
Diag::WEBSOCKET = onOff;
|
||||||
|
return true;
|
||||||
#endif
|
#endif
|
||||||
#ifndef DISABLE_EEPROM
|
#ifndef DISABLE_EEPROM
|
||||||
case "EEPROM"_hk: // <D EEPROM NumEntries>
|
case "EEPROM"_hk: // <D EEPROM NumEntries>
|
||||||
|
@ -37,6 +37,7 @@ struct DCCEXParser
|
|||||||
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
|
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
|
||||||
static void setFilter(FILTER_CALLBACK filter);
|
static void setFilter(FILTER_CALLBACK filter);
|
||||||
static void setRMFTFilter(FILTER_CALLBACK filter);
|
static void setRMFTFilter(FILTER_CALLBACK filter);
|
||||||
|
static void setCamParserFilter(FILTER_CALLBACK filter);
|
||||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||||
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
||||||
|
|
||||||
@ -77,6 +78,7 @@ struct DCCEXParser
|
|||||||
static void callback_Vbyte(int16_t result);
|
static void callback_Vbyte(int16_t result);
|
||||||
static FILTER_CALLBACK filterCallback;
|
static FILTER_CALLBACK filterCallback;
|
||||||
static FILTER_CALLBACK filterRMFTCallback;
|
static FILTER_CALLBACK filterRMFTCallback;
|
||||||
|
static FILTER_CALLBACK filterCamParserCallback;
|
||||||
static AT_COMMAND_CALLBACK atCommandCallback;
|
static AT_COMMAND_CALLBACK atCommandCallback;
|
||||||
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||||
static void sendFlashList(Print * stream,const int16_t flashList[]);
|
static void sendFlashList(Print * stream,const int16_t flashList[]);
|
||||||
|
6
DCCRMT.h
6
DCCRMT.h
@ -44,6 +44,12 @@ class RMTChannel {
|
|||||||
return true;
|
return true;
|
||||||
return dataReady;
|
return dataReady;
|
||||||
};
|
};
|
||||||
|
inline void waitForDataCopy() {
|
||||||
|
while(1) { // do nothing and wait for interrupt clearing dataReady to happen
|
||||||
|
if (dataReady == false)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
inline uint32_t packetCount() { return packetCounter; };
|
inline uint32_t packetCount() { return packetCounter; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -70,7 +70,11 @@ void DCCWaveform::begin() {
|
|||||||
|
|
||||||
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
||||||
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
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;
|
byte checksum = 0;
|
||||||
for (byte b = 0; b < byteCount; b++) {
|
for (byte b = 0; b < byteCount; b++) {
|
||||||
checksum ^= buffer[b];
|
checksum ^= buffer[b];
|
||||||
@ -88,13 +92,7 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
|
|||||||
{
|
{
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
do {
|
do {
|
||||||
if(isMainTrack) {
|
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
||||||
if (rmtMainChannel != NULL)
|
|
||||||
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
|
||||||
} else {
|
|
||||||
if (rmtProgChannel != NULL)
|
|
||||||
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
|
||||||
}
|
|
||||||
} while(ret > 0);
|
} while(ret > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
#define GITHUB_SHA "devel-202501092043Z"
|
#define GITHUB_SHA "devel-202501171827Z"
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#define driverVer 305
|
#define driverVer 306
|
||||||
|
// v306 Pass vpin to regeister it in CamParser.
|
||||||
|
// Move base vpin to camparser.
|
||||||
|
// No more need for config.h settings.
|
||||||
// v305 less debug & alpha ordered switch
|
// v305 less debug & alpha ordered switch
|
||||||
// v304 static oldb0; t(##[,%%];
|
// v304 static oldb0; t(##[,%%];
|
||||||
// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)
|
// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)
|
||||||
@ -35,23 +38,18 @@
|
|||||||
* This device driver will configure the device on startup, along with CamParser.cpp
|
* This device driver will configure the device on startup, along with CamParser.cpp
|
||||||
* interacting with the sensorCAM device for all input/output duties.
|
* interacting with the sensorCAM device for all input/output duties.
|
||||||
*
|
*
|
||||||
* #include "CamParser.h" in DCCEXParser.cpp
|
* To create EX-SensorCAM devices,
|
||||||
* #include "IO_EXSensorCAM.h" in IODevice.h
|
* use HAL(EXSensorCAM, baseVpin, numpins, i2c_address) in myAutomation.h
|
||||||
* To create EX-SensorCAM devices, define them in myHal.cpp: with
|
* e.g.
|
||||||
* EXSensorCAM::create(baseVpin,num_vpins,i2c_address) or
|
* HAL(EXSensorCAM,700, 80, 0x11)
|
||||||
* alternatively use HAL(EXSensorCAM baseVpin numpins i2c_address) in myAutomation.h
|
|
||||||
* also #define SENSORCAM_VPIN baseVpin in config.h
|
|
||||||
*
|
*
|
||||||
* void halSetup() {
|
* or (deprecated) define them in myHal.cpp: with
|
||||||
* // EXSensorCAM::create(vpin, num_vpins, i2c_address);
|
* EXSensorCAM::create(baseVpin,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
|
#ifndef IO_EX_EXSENSORCAM_H
|
||||||
#define IO_EX_EXSENSORCAM_H
|
#define IO_EX_EXSENSORCAM_H
|
||||||
|
#define DIGITALREFRESH 20000UL // min uSec delay between digital reads of digitalInputStates
|
||||||
#define SEND StringFormatter::send
|
#define SEND StringFormatter::send
|
||||||
#include "IODevice.h"
|
#include "IODevice.h"
|
||||||
#include "I2CManager.h"
|
#include "I2CManager.h"
|
||||||
@ -70,7 +68,7 @@ class EXSensorCAM : public IODevice {
|
|||||||
new EXSensorCAM(vpin, nPins, i2cAddress);
|
new EXSensorCAM(vpin, nPins, i2cAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
static VPIN CAMBaseVpin;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Constructor
|
// Constructor
|
||||||
@ -81,6 +79,7 @@ class EXSensorCAM : public IODevice {
|
|||||||
_nPins = nPins;
|
_nPins = nPins;
|
||||||
_I2CAddress = i2cAddress;
|
_I2CAddress = i2cAddress;
|
||||||
addDevice(this);
|
addDevice(this);
|
||||||
|
CamParser::addVpin(firstVpin);
|
||||||
}
|
}
|
||||||
//*************************
|
//*************************
|
||||||
void _begin() {
|
void _begin() {
|
||||||
|
@ -511,6 +511,7 @@ public:
|
|||||||
if (pin == 0) { // Do nothing if not vPin 0
|
if (pin == 0) { // Do nothing if not vPin 0
|
||||||
return _playing;
|
return _playing;
|
||||||
}
|
}
|
||||||
|
return _playing; // fix for compile error: "control reaches end of non-void function [-Wreturn-type]"
|
||||||
}
|
}
|
||||||
|
|
||||||
void _display() override {
|
void _display() override {
|
||||||
@ -550,7 +551,7 @@ private:
|
|||||||
|
|
||||||
// Prepend the DFPlayer command with REG address and UART Channel in _outbuffer
|
// 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
|
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
|
||||||
for ( int i = 1; i < sizeof(out)+1 ; i++){
|
for ( uint8_t i = 1; i < sizeof(out)+1 ; i++){
|
||||||
_outbuffer[i] = out[i-1];
|
_outbuffer[i] = out[i-1];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,6 +617,14 @@ private:
|
|||||||
uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16); // Calculate _divisor for baudrate
|
uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16); // Calculate _divisor for baudrate
|
||||||
TEMP_REG_VAL = 0x08; // UART Software reset
|
TEMP_REG_VAL = 0x08; // UART Software reset
|
||||||
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
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
|
TEMP_REG_VAL = 0x00; // Set pins to GPIO mode
|
||||||
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
||||||
TEMP_REG_VAL = 0xFF; //Set all pins as output
|
TEMP_REG_VAL = 0xFF; //Set all pins as output
|
||||||
|
41
Release_Notes/websocketTester.html
Normal file
41
Release_Notes/websocketTester.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
@ -28,6 +28,7 @@ bool Diag::WITHROTTLE=false;
|
|||||||
bool Diag::ETHERNET=false;
|
bool Diag::ETHERNET=false;
|
||||||
bool Diag::LCN=false;
|
bool Diag::LCN=false;
|
||||||
bool Diag::RAILCOM=false;
|
bool Diag::RAILCOM=false;
|
||||||
|
bool Diag::WEBSOCKET=false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ class Diag {
|
|||||||
static bool ETHERNET;
|
static bool ETHERNET;
|
||||||
static bool LCN;
|
static bool LCN;
|
||||||
static bool RAILCOM;
|
static bool RAILCOM;
|
||||||
|
static bool WEBSOCKET;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -379,7 +379,7 @@
|
|||||||
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
|
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
|
||||||
// and Close writes a 0.
|
// and Close writes a 0.
|
||||||
// RCN-213 specifies that Throw is 0 and Close is 1.
|
// RCN-213 specifies that Throw is 0 and Close is 1.
|
||||||
#if defined(DCC_TURNOUTS_RCN_213)
|
#ifndef DCC_TURNOUTS_RCN_213
|
||||||
close = !close;
|
close = !close;
|
||||||
#endif
|
#endif
|
||||||
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);
|
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);
|
||||||
|
211
Websockets.cpp
Normal file
211
Websockets.cpp
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* © 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
34
Websockets.h
Normal file
34
Websockets.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* © 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/>.
|
||||||
|
*/
|
||||||
|
#ifndef Websockets_h
|
||||||
|
#define Websockets_h
|
||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -2,6 +2,8 @@
|
|||||||
© 2023 Paul M. Antoine
|
© 2023 Paul M. Antoine
|
||||||
© 2021 Harald Barth
|
© 2021 Harald Barth
|
||||||
© 2023 Nathan Kellenicki
|
© 2023 Nathan Kellenicki
|
||||||
|
© 2025 Chris Harlow
|
||||||
|
|
||||||
|
|
||||||
This file is part of CommandStation-EX
|
This file is part of CommandStation-EX
|
||||||
|
|
||||||
@ -30,6 +32,7 @@
|
|||||||
#include "CommandDistributor.h"
|
#include "CommandDistributor.h"
|
||||||
#include "WiThrottle.h"
|
#include "WiThrottle.h"
|
||||||
#include "DCC.h"
|
#include "DCC.h"
|
||||||
|
#include "Websockets.h"
|
||||||
/*
|
/*
|
||||||
#include "soc/rtc_wdt.h"
|
#include "soc/rtc_wdt.h"
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
@ -378,6 +381,8 @@ void WifiESP::loop() {
|
|||||||
|
|
||||||
// something to write out?
|
// something to write out?
|
||||||
clientId=outboundRing->read();
|
clientId=outboundRing->read();
|
||||||
|
bool useWebsocket=clientId & Websockets::WEBSOCK_CLIENT_MARKER;
|
||||||
|
clientId &= ~ Websockets::WEBSOCK_CLIENT_MARKER;
|
||||||
if (clientId >= 0) {
|
if (clientId >= 0) {
|
||||||
// We have data to send in outboundRing
|
// We have data to send in outboundRing
|
||||||
// and we have a valid clientId.
|
// and we have a valid clientId.
|
||||||
@ -385,25 +390,28 @@ void WifiESP::loop() {
|
|||||||
// and then look if it can be sent because
|
// and then look if it can be sent because
|
||||||
// we can not leave it in the ring for ever
|
// we can not leave it in the ring for ever
|
||||||
int count=outboundRing->count();
|
int count=outboundRing->count();
|
||||||
|
auto wsHeaderLen=useWebsocket? Websockets::getOutboundHeaderSize(count) : 0;
|
||||||
{
|
{
|
||||||
char buffer[count+1]; // one extra for '\0'
|
byte buffer[wsHeaderLen + count + 1]; // one extra for '\0'
|
||||||
for(int i=0;i<count;i++) {
|
if (useWebsocket) Websockets::fillOutboundHeader(count, buffer);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
int c = outboundRing->read();
|
int c = outboundRing->read();
|
||||||
if (c >= 0) // Panic check, should never be false
|
if (!c) {
|
||||||
buffer[i] = (char)c;
|
DIAG(F("Ringread fail at %d"), i);
|
||||||
else {
|
|
||||||
DIAG(F("Ringread fail at %d"),i);
|
|
||||||
break;
|
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 filled, end with '\0' so we can use it as C string
|
||||||
buffer[count]='\0';
|
buffer[wsHeaderLen+count]='\0';
|
||||||
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
|
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
|
||||||
if (Diag::CMD || Diag::WITHROTTLE)
|
if (Diag::WIFI)
|
||||||
DIAG(F("SEND %d:%s"), clientId, buffer);
|
DIAG(F("SEND%S %d:%s"), useWebsocket?F("ws"):F(""),clientId, buffer+wsHeaderLen);
|
||||||
clients[clientId].wifi.write(buffer,count);
|
clients[clientId].wifi.write(buffer,count+wsHeaderLen);
|
||||||
} else {
|
} else {
|
||||||
DIAG(F("Unsent(%d): %s"), clientId, buffer);
|
DIAG(F("Unsent(%d): %s"), clientId, buffer+wsHeaderLen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020-2021 Chris Harlow
|
* © 2020-2025 Chris Harlow
|
||||||
* © 2020, Chris Harlow. All rights reserved.
|
* © 2020, Chris Harlow. All rights reserved.
|
||||||
* © 2020, Harald Barth.
|
* © 2020, Harald Barth.
|
||||||
*
|
*
|
||||||
@ -26,6 +26,7 @@
|
|||||||
#include "RingStream.h"
|
#include "RingStream.h"
|
||||||
#include "CommandDistributor.h"
|
#include "CommandDistributor.h"
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
|
#include "Websockets.h"
|
||||||
|
|
||||||
WifiInboundHandler * WifiInboundHandler::singleton;
|
WifiInboundHandler * WifiInboundHandler::singleton;
|
||||||
|
|
||||||
@ -67,8 +68,13 @@ void WifiInboundHandler::loop1() {
|
|||||||
|
|
||||||
|
|
||||||
if (pendingCipsend && millis()-lastCIPSEND > CIPSENDgap) {
|
if (pendingCipsend && millis()-lastCIPSEND > CIPSENDgap) {
|
||||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), clientPendingCIPSEND, currentReplySize);
|
// add allowances for websockets
|
||||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), clientPendingCIPSEND, currentReplySize);
|
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);
|
||||||
pendingCipsend=false;
|
pendingCipsend=false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -80,7 +86,9 @@ void WifiInboundHandler::loop1() {
|
|||||||
int count=inboundRing->count();
|
int count=inboundRing->count();
|
||||||
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"),clientId,count);
|
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"),clientId,count);
|
||||||
byte cmd[count+1];
|
byte cmd[count+1];
|
||||||
for (int i=0;i<count;i++) cmd[i]=inboundRing->read();
|
// 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();
|
||||||
cmd[count]=0;
|
cmd[count]=0;
|
||||||
if (Diag::WIFI) DIAG(F("%e"),cmd);
|
if (Diag::WIFI) DIAG(F("%e"),cmd);
|
||||||
|
|
||||||
@ -94,6 +102,9 @@ void WifiInboundHandler::loop1() {
|
|||||||
// This is a Finite State Automation (FSA) handling the inbound bytes from an ES AT command processor
|
// This is a Finite State Automation (FSA) handling the inbound bytes from an ES AT command processor
|
||||||
|
|
||||||
WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||||
|
const char WebSocketKeyName[]="Sec-WebSocket-Key: ";
|
||||||
|
static byte prescanPoint=0;
|
||||||
|
|
||||||
while (wifiStream->available()) {
|
while (wifiStream->available()) {
|
||||||
int ch = wifiStream->read();
|
int ch = wifiStream->read();
|
||||||
|
|
||||||
@ -112,9 +123,12 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ch=='>') {
|
if (ch=='>') {
|
||||||
if (Diag::WIFI) DIAG(F("[XMIT %d]"),currentReplySize);
|
bool websocket=clientPendingCIPSEND & Websockets::WEBSOCK_CLIENT_MARKER;
|
||||||
|
if (Diag::WIFI) DIAG(F("[XMIT %d ws=%b]"),currentReplySize,websocket);
|
||||||
|
if (websocket) Websockets::writeOutboundHeader(wifiStream,currentReplySize);
|
||||||
for (int i=0;i<currentReplySize;i++) {
|
for (int i=0;i<currentReplySize;i++) {
|
||||||
int cout=outboundRing->read();
|
int cout=outboundRing->read();
|
||||||
|
if (websocket && (cout=='\n')) cout='\r';
|
||||||
wifiStream->write(cout);
|
wifiStream->write(cout);
|
||||||
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
||||||
}
|
}
|
||||||
@ -195,14 +209,19 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (Diag::WIFI) DIAG(F("Wifi inbound data(%d:%d):"),runningClientId,dataLength);
|
if (Diag::WIFI) DIAG(F("Wifi inbound data(%d:%d):"),runningClientId,dataLength);
|
||||||
if (inboundRing->freeSpace()<=(dataLength+1)) {
|
|
||||||
|
// we normally dont read >100 bytes
|
||||||
|
// so assume its an HTTP GET or similar
|
||||||
|
|
||||||
|
if (dataLength<100 && inboundRing->freeSpace()<=(dataLength+1)) {
|
||||||
// This input would overflow the inbound ring, ignore it
|
// This input would overflow the inbound ring, ignore it
|
||||||
loopState=IPD_IGNORE_DATA;
|
loopState=IPD_IGNORE_DATA;
|
||||||
if (Diag::WIFI) DIAG(F("Wifi OVERFLOW IGNORING:"));
|
if (Diag::WIFI) DIAG(F("Wifi OVERFLOW IGNORING:"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
inboundRing->mark(runningClientId);
|
inboundRing->mark(runningClientId);
|
||||||
loopState=IPD_DATA;
|
prescanPoint=0;
|
||||||
|
loopState=(dataLength>100)? IPD_PRESCAN: IPD_DATA;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dataLength = dataLength * 10 + (ch - '0');
|
dataLength = dataLength * 10 + (ch - '0');
|
||||||
@ -217,6 +236,38 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
|||||||
}
|
}
|
||||||
break;
|
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
|
case IPD_IGNORE_DATA: // ignoring data that would not fit in inbound ring
|
||||||
dataLength--;
|
dataLength--;
|
||||||
if (dataLength == 0) loopState = ANYTHING;
|
if (dataLength == 0) loopState = ANYTHING;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* © 2021 Harald Barth
|
* © 2021 Harald Barth
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* (c) 2021 Fred Decker. All rights reserved.
|
* (c) 2021 Fred Decker. All rights reserved.
|
||||||
* (c) 2020 Chris Harlow. All rights reserved.
|
* (c) 2020-2025 Chris Harlow. All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of CommandStation-EX
|
* This file is part of CommandStation-EX
|
||||||
*
|
*
|
||||||
@ -55,7 +55,8 @@ class WifiInboundHandler {
|
|||||||
IPD6_LENGTH, // got +IPD,c, reading length
|
IPD6_LENGTH, // got +IPD,c, reading length
|
||||||
IPD_DATA, // got +IPD,c,ll,: collecting data
|
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_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_ID, // clientid prefix to CONNECTED / CLOSED
|
||||||
GOT_CLIENT_ID2 // clientid prefix to CONNECTED / CLOSED
|
GOT_CLIENT_ID2 // clientid prefix to CONNECTED / CLOSED
|
||||||
};
|
};
|
||||||
@ -67,7 +68,7 @@ class WifiInboundHandler {
|
|||||||
void purgeCurrentCIPSEND();
|
void purgeCurrentCIPSEND();
|
||||||
Stream * wifiStream;
|
Stream * wifiStream;
|
||||||
|
|
||||||
static const int INBOUND_RING = 512;
|
static const int INBOUND_RING = 128;
|
||||||
static const int OUTBOUND_RING = sizeof(void*)==2?2048:8192;
|
static const int OUTBOUND_RING = sizeof(void*)==2?2048:8192;
|
||||||
|
|
||||||
static const int CIPSENDgap=100; // millis() between retries of cipsend.
|
static const int CIPSENDgap=100; // millis() between retries of cipsend.
|
||||||
|
@ -273,8 +273,8 @@ The configuration file for DCC-EX Command Station
|
|||||||
// over DCC++. This #define likewise inverts the behaviour of the <a> command
|
// over DCC++. This #define likewise inverts the behaviour of the <a> command
|
||||||
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
|
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
|
||||||
// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0
|
// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0
|
||||||
// (throw turnout). This is the same as DCC_ACCESSORY_COMMAND_REVERSE
|
// (throw turnout).
|
||||||
//#define DCC_ACCESSORY_RCN_213
|
//#define DCC_ACCESSORY_COMMAND_REVERSE
|
||||||
|
|
||||||
|
|
||||||
// HANDLING MULTIPLE SERIAL THROTTLES
|
// HANDLING MULTIPLE SERIAL THROTTLES
|
||||||
|
206
libsha1.cpp
Normal file
206
libsha1.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// 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
Normal file
26
libsha1.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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
|
27
objdump.bat
27
objdump.bat
@ -1,16 +1,17 @@
|
|||||||
ECHO ON
|
ECHO ON
|
||||||
FOR /F "delims=" %%i IN ('dir %TMP%\arduino_build_* /b /ad-h /t:c /od') DO SET a=%%i
|
FOR /F "delims=" %%i IN ('dir %TMP%\arduino\sketches\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||||
echo Most recent subfolder: %a% >%TMP%\OBJDUMP_%a%.txt
|
SET DUMP=%TEMP%\OBJDUMP.txt
|
||||||
SET ELF=%TMP%\%a%\CommandStation-EX.ino.elf
|
echo Most recent subfolder: %ELF% >%DUMP%
|
||||||
|
|
||||||
set PATH="C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\";%PATH%
|
set PATH="C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\";%PATH%
|
||||||
avr-objdump --private=mem-usage %ELF% >>%TMP%\OBJDUMP_%a%.txt
|
avr-objdump --private=mem-usage %ELF% >>%DUMP%
|
||||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||||
avr-objdump -x -C %ELF% | find ".text" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
avr-objdump -x -C %ELF% | find ".text" | sort /+25 /R >>%DUMP%
|
||||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||||
avr-objdump -x -C %ELF% | find ".data" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
avr-objdump -x -C %ELF% | find ".data" | sort /+25 /R >>%DUMP%
|
||||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||||
avr-objdump -x -C %ELF% | find ".bss" | sort /+25 /R >>%TMP%\OBJDUMP_%a%.txt
|
avr-objdump -x -C %ELF% | find ".bss" | sort /+25 /R >>%DUMP%
|
||||||
ECHO ++++++++++++++++++++++++++++++++++ >>%TMP%\OBJDUMP_%a%.txt
|
ECHO ++++++++++++++++++++++++++++++++++ >>%DUMP%
|
||||||
avr-objdump -D -S %ELF% >>%TMP%\OBJDUMP_%a%.txt
|
avr-objdump -D -S %ELF% >>%DUMP%
|
||||||
%TMP%\OBJDUMP_%a%.txt
|
%DUMP%
|
||||||
EXIT
|
EXIT
|
||||||
|
@ -11,11 +11,11 @@
|
|||||||
[platformio]
|
[platformio]
|
||||||
default_envs =
|
default_envs =
|
||||||
mega2560
|
mega2560
|
||||||
uno
|
; uno
|
||||||
nano
|
; nano
|
||||||
ESP32
|
; ESP32
|
||||||
Nucleo-F411RE
|
; Nucleo-F411RE
|
||||||
Nucleo-F446RE
|
; Nucleo-F446RE
|
||||||
src_dir = .
|
src_dir = .
|
||||||
include_dir = .
|
include_dir = .
|
||||||
|
|
||||||
|
14
version.h
14
version.h
@ -3,7 +3,16 @@
|
|||||||
|
|
||||||
#include "StringFormatter.h"
|
#include "StringFormatter.h"
|
||||||
|
|
||||||
#define VERSION "5.5.6"
|
#define VERSION "5.5.12"
|
||||||
|
// 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.6 - Fix ESP32 build bug caused by include reference loop
|
||||||
// 5.5.5 - Railcom implementation with IO_I2CRailcom driver
|
// 5.5.5 - Railcom implementation with IO_I2CRailcom driver
|
||||||
// - response analysis and block management.
|
// - response analysis and block management.
|
||||||
@ -18,6 +27,9 @@
|
|||||||
// 5.5.2 - DS1307 Real Time clock
|
// 5.5.2 - DS1307 Real Time clock
|
||||||
// 5.5.1 - Momentum
|
// 5.5.1 - Momentum
|
||||||
// 5.5.0 - New version on devel
|
// 5.5.0 - New version on devel
|
||||||
|
// 5.4.3 - bugfix changeFn for functions 29..31
|
||||||
|
// 5.4.2 - Reversed turnout bugfix
|
||||||
|
// 5.4.1 - ESP32 bugfix packet buffer race
|
||||||
// 5.4.0 - New version on master
|
// 5.4.0 - New version on master
|
||||||
// 5.2.96 - EXRAIL additions XFWD() and XREV()
|
// 5.2.96 - EXRAIL additions XFWD() and XREV()
|
||||||
// 5.2.95 - Release candidate for 5.4
|
// 5.2.95 - Release candidate for 5.4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user