mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-30 19:03:44 +02:00
Compare commits
121 Commits
v5.2.77-De
...
devel_rail
Author | SHA1 | Date | |
---|---|---|---|
|
ec4c6b9c02 | ||
|
620ad6275b | ||
|
dcfb3f061d | ||
|
ed69a51e97 | ||
|
f2a7577313 | ||
|
f3b87877ef | ||
|
07691e3985 | ||
|
d14aa46d51 | ||
|
2b50e31e50 | ||
|
ee8f6eea1f | ||
|
fb6070784e | ||
|
2f1d5b993c | ||
|
9054d8d9f5 | ||
|
865f75dda4 | ||
|
b40fa779a6 | ||
|
2115ada2a1 | ||
|
58b180603a | ||
|
95d90aa337 | ||
|
137008ceb3 | ||
|
4ec9a62ab6 | ||
|
830de850a9 | ||
|
c28965c58d | ||
|
08076443cb | ||
|
748ddcde8c | ||
|
dc84f560e6 | ||
|
680f765775 | ||
|
7e8841611d | ||
|
2503158e32 | ||
|
f031add7a0 | ||
|
0476b9c1d8 | ||
|
ba9ca1ccad | ||
|
32066a3bfa | ||
|
e20935848f | ||
|
6f0ff49945 | ||
|
c389fe9d3b | ||
|
79c30ec516 | ||
|
147fe15e04 | ||
|
b5491f9b52 | ||
|
6f1c7a9e98 | ||
|
42986c3b2d | ||
|
c1046ddcc0 | ||
|
818240b349 | ||
|
3c725afab4 | ||
|
13488e1e93 | ||
|
6cc3b4c6bf | ||
|
43fe772661 | ||
|
cafd53a0e5 | ||
|
d4a99b5db5 | ||
|
3ead534c81 | ||
|
84bc098157 | ||
|
8329fd83ce | ||
|
4f16091670 | ||
|
377f10e1c5 | ||
|
016a20259a | ||
|
14724aeb2a | ||
|
8f48e2ed94 | ||
|
6710c47f03 | ||
|
420d14567d | ||
|
953b8054f5 | ||
|
8081bfdf1e | ||
|
03bd1e897a | ||
|
d8f6d91408 | ||
|
dbb15c6aaa | ||
|
614802c756 | ||
|
5efe385f2e | ||
|
c50f3e016c | ||
|
535dcabcec | ||
|
1d18d5dea5 | ||
|
21c01ab69a | ||
|
fa00e9e11b | ||
|
ece2ac3ccf | ||
|
ea2e5ab8e9 | ||
|
480eb1bfde | ||
|
21dca05257 | ||
|
f5014f5595 | ||
|
33c8ed19a9 | ||
|
0e99ad143b | ||
|
01533e2cd2 | ||
|
07ab7286ba | ||
|
dc481a2f0c | ||
|
692f97e480 | ||
|
7fb7751f19 | ||
|
546ddd8139 | ||
|
4aa353edbc | ||
|
c1d6ee2804 | ||
|
14360b4198 | ||
|
dd898d3c16 | ||
|
277431e84c | ||
|
ec42c09e06 | ||
|
4174c2a4ab | ||
|
30236f9b36 | ||
|
8245208b2b | ||
|
4ed2ee9adc | ||
|
06a353cfa0 | ||
|
dfe9e6b69f | ||
|
4d84eccac3 | ||
|
edb02a00ce | ||
|
5db19a0fb8 | ||
|
b62661c337 | ||
|
048ba3fd1e | ||
|
c8c3697fa0 | ||
|
8c3c5dfe33 | ||
|
92288603bf | ||
|
80c8b3ef62 | ||
|
127f3acce5 | ||
|
690c629e6d | ||
|
e328ea5c5d | ||
|
923b031d06 | ||
|
7e29011d63 | ||
|
4e491a1e56 | ||
|
430161ef60 | ||
|
28d60d4984 | ||
|
3b162996ad | ||
|
fb414a7a50 | ||
|
818e05b425 | ||
|
c5168f030f | ||
|
387ea019bd | ||
|
a981f83bb9 | ||
|
749a859db5 | ||
|
659c58b307 | ||
|
0b9ec7460b |
141
CamParser.cpp
Normal file
141
CamParser.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* © 2023-2025, Barry Daniel
|
||||
* © 2025 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//sensorCAM parser.cpp version 3.06 Jan 2025
|
||||
#include "DCCEXParser.h"
|
||||
#include "CamParser.h"
|
||||
#include "FSH.h"
|
||||
|
||||
const int16_t ver=30177;
|
||||
const int16_t ve =2899;
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
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]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t camop=p[0]; // cam oprerator
|
||||
int param1=0;
|
||||
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);
|
||||
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;
|
||||
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
|
||||
}
|
||||
|
||||
// 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 (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;
|
||||
param1=p[1];
|
||||
break;
|
||||
|
||||
case 3: //<N vpin rowY colx > or <N cmd p1 p2>
|
||||
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;
|
||||
|
||||
case 4: //<N a id row col>
|
||||
if (camop!='A') return false; //must start with 'a'
|
||||
if (p[3]>316 || p[3]<0) return false;
|
||||
if (p[2]>236 || p[2]<0) return false;
|
||||
if (p[1]>97 || p[1]<0) return false; //treat as bsNo.
|
||||
vpin = vpin + (p[1]/10)*8 + p[1]%10; //translate p[1]
|
||||
camop=0x80; // special 'a' case for IO_SensorCAM
|
||||
param1=p[2]; // row
|
||||
param3=p[3]; // col
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
}
|
40
CamParser.h
Normal file
40
CamParser.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* © 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>
|
||||
#include "IODevice.h"
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* © 2022 Colin Murdoch
|
||||
* All rights reserved.
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "DCC.h"
|
||||
#include "TrackManager.h"
|
||||
#include "StringFormatter.h"
|
||||
#include "Websockets.h"
|
||||
|
||||
// variables to hold clock time
|
||||
int16_t lastclocktime;
|
||||
@@ -44,6 +45,7 @@ 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
|
||||
@@ -56,14 +58,22 @@ 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[8]={
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
|
||||
CommandDistributor::clientType CommandDistributor::clients[20]={
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,
|
||||
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
|
||||
|
||||
// Parse is called by Withrottle or Ethernet interface to determine which
|
||||
// Parse is called by Wifi or Ethernet interface to determine which
|
||||
// protocol the client is using and call the appropriate part of dcc++Ex
|
||||
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
|
||||
if (Diag::WIFI && Diag::CMD)
|
||||
DIAG(F("Parse C=%d T=%d B=%s"),clientId, clients[clientId], buffer);
|
||||
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;
|
||||
}
|
||||
ring=stream;
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -131,7 +159,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);
|
||||
ring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));
|
||||
ring->print(broadcastBufferWriter->getString());
|
||||
//DIAG(F("CD commit client %d"), clientId);
|
||||
ring->commit();
|
||||
@@ -185,10 +213,15 @@ 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;
|
||||
@@ -207,9 +240,13 @@ int16_t CommandDistributor::retClockTime() {
|
||||
return lastclocktime;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
#ifdef SABERTOOTH
|
||||
if (Serial2 && sp->loco == SABERTOOTH) {
|
||||
static uint8_t rampingmode = 0;
|
||||
@@ -377,4 +414,3 @@ void CommandDistributor::setVirtualLCDSerial(Print * stream) {
|
||||
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
|
||||
byte CommandDistributor::virtualLCDClient=0xFF;
|
||||
byte CommandDistributor::rememberVLCDClient=0;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* © 2022 Colin Murdoch
|
||||
*
|
||||
@@ -28,6 +28,7 @@
|
||||
#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
|
||||
@@ -36,17 +37,17 @@
|
||||
|
||||
class CommandDistributor {
|
||||
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:
|
||||
static void broadcastToClients(clientType type);
|
||||
static StringBuffer * broadcastBufferWriter;
|
||||
#ifdef CD_HANDLE_RING
|
||||
static RingStream * ring;
|
||||
static clientType clients[8];
|
||||
static clientType clients[20];
|
||||
#endif
|
||||
public :
|
||||
static void parse(byte clientId,byte* buffer, RingStream * ring);
|
||||
static void broadcastLoco(byte slot);
|
||||
static void broadcastLoco(DCC::LOCO * slot);
|
||||
static void broadcastForgetLoco(int16_t loco);
|
||||
static void broadcastSensor(int16_t id, bool value);
|
||||
static void broadcastTurnout(int16_t id, bool isClosed);
|
||||
|
@@ -141,6 +141,23 @@ 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()
|
||||
{
|
||||
// The main sketch has responsibilities during loop()
|
||||
@@ -148,14 +165,15 @@ void loop()
|
||||
// Responsibility 1: Handle DCC background processes
|
||||
// (loco reminders and power checks)
|
||||
DCC::loop();
|
||||
|
||||
|
||||
// Responsibility 2: handle any incoming commands on USB connection
|
||||
SerialManager::loop();
|
||||
|
||||
|
||||
// Responsibility 3: Optionally handle any incoming WiFi traffic
|
||||
#ifndef ARDUINO_ARCH_ESP32
|
||||
#if WIFI_ON
|
||||
WifiInterface::loop();
|
||||
|
||||
#endif //WIFI_ON
|
||||
#else //ARDUINO_ARCH_ESP32
|
||||
#ifndef WIFI_TASK_ON_CORE0
|
||||
|
469
DCC.cpp
469
DCC.cpp
@@ -5,7 +5,7 @@
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX
|
||||
@@ -37,6 +37,8 @@
|
||||
#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.
|
||||
@@ -60,6 +62,8 @@ 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
|
||||
@@ -72,13 +76,49 @@ 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;
|
||||
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 );
|
||||
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);
|
||||
}
|
||||
|
||||
void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
@@ -118,8 +158,8 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
b[nB++] = speedCode; // for encoding see setThrottle
|
||||
|
||||
}
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
||||
if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority
|
||||
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 4, cab);
|
||||
}
|
||||
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
|
||||
@@ -133,24 +173,28 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
b[nB++] = byte2;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, count);
|
||||
DCCQueue::scheduleDCCPacket(b, nB, count);
|
||||
}
|
||||
|
||||
// returns speed steps 0 to 127 (1 == emergency stop)
|
||||
// or -1 on "loco not found"
|
||||
int8_t DCC::getThrottleSpeed(int cab) {
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0) return -1;
|
||||
return speedTable[reg].speedCode & 0x7F;
|
||||
return getThrottleSpeedByte(cab) & 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) {
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return 128;
|
||||
return speedTable[reg].speedCode;
|
||||
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;
|
||||
}
|
||||
|
||||
// returns 0 to 7 for frequency
|
||||
@@ -159,12 +203,11 @@ uint8_t DCC::getThrottleFrequency(int cab) {
|
||||
(void)cab;
|
||||
return 0;
|
||||
#else
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return 0; // use default frequency
|
||||
LOCO* slot=lookupSpeedTable(cab);
|
||||
if (!slot) return 0; // use default frequency
|
||||
// shift out first 29 bits so we have the 3 "frequency bits" left
|
||||
uint8_t res = (uint8_t)(speedTable[reg].functions >>29);
|
||||
//DIAG(F("Speed table %d functions %l shifted %d"), reg, speedTable[reg].functions, res);
|
||||
uint8_t res = (uint8_t)(slot->functions >>29);
|
||||
//DIAG(F("Speed table %d functions %l shifted %d"), reg, slot->functions, res);
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
@@ -172,9 +215,7 @@ uint8_t DCC::getThrottleFrequency(int cab) {
|
||||
// returns direction on loco
|
||||
// or true/forward on "loco not found"
|
||||
bool DCC::getThrottleDirection(int cab) {
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0) return true;
|
||||
return (speedTable[reg].speedCode & 0x80) !=0;
|
||||
return getThrottleSpeedByte(cab) & 0x80;
|
||||
}
|
||||
|
||||
// Set function to value on or off
|
||||
@@ -198,7 +239,7 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||
b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0); // low order bits and state flag
|
||||
b[nB++] = functionNumber >>7 ; // high order bits
|
||||
}
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4);
|
||||
}
|
||||
// We use the reminder table up to 28 for normal functions.
|
||||
// We use 29 to 31 for DC frequency as well so up to 28
|
||||
@@ -207,37 +248,30 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||
if (functionNumber > 31)
|
||||
return true;
|
||||
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return false;
|
||||
|
||||
LOCO * slot = lookupSpeedTable(cab);
|
||||
|
||||
// Take care of functions:
|
||||
// Set state of function
|
||||
uint32_t previous=speedTable[reg].functions;
|
||||
uint32_t previous=slot->functions;
|
||||
uint32_t funcmask = (1UL<<functionNumber);
|
||||
if (on) {
|
||||
speedTable[reg].functions |= funcmask;
|
||||
slot->functions |= funcmask;
|
||||
} else {
|
||||
speedTable[reg].functions &= ~funcmask;
|
||||
slot->functions &= ~funcmask;
|
||||
}
|
||||
if (speedTable[reg].functions != previous) {
|
||||
if (slot->functions != previous) {
|
||||
if (functionNumber <= 28)
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
updateGroupflags(slot->groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flip function state (used from withrottle protocol)
|
||||
void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||
if (cab<=0 || functionNumber>31) return;
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return;
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
speedTable[reg].functions ^= funcmask;
|
||||
if (functionNumber <= 28) {
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
}
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
auto currentValue=getFn(cab,functionNumber);
|
||||
if (currentValue<0) return; // function not valid for change
|
||||
setFn(cab,functionNumber, currentValue?false:true);
|
||||
}
|
||||
|
||||
// Report function state (used from withrottle protocol)
|
||||
@@ -245,12 +279,10 @@ 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
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0)
|
||||
return -1;
|
||||
|
||||
auto slot = lookupSpeedTable(cab);
|
||||
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
return (speedTable[reg].functions & funcmask)? 1 : 0;
|
||||
return (slot->functions & funcmask)? 1 : 0;
|
||||
}
|
||||
|
||||
// Set the group flag to say we have touched the particular group.
|
||||
@@ -267,22 +299,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
|
||||
int reg = lookupSpeedTable(cab);
|
||||
return (reg<0)?0:speedTable[reg].functions;
|
||||
auto slot = lookupSpeedTable(cab,false);
|
||||
return slot?slot->functions:0;
|
||||
}
|
||||
|
||||
// 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 reg=lookupSpeedTable(cab,true);
|
||||
auto slot=lookupSpeedTable(cab,true);
|
||||
// drop and replace F29,30,31 (top 3 bits)
|
||||
auto newFunctions=speedTable[reg].functions & 0x1FFFFFFFUL;
|
||||
auto newFunctions=slot->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==speedTable[reg].functions) return; // no change
|
||||
speedTable[reg].functions=newFunctions;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
if (newFunctions==slot->functions) return; // no change
|
||||
slot->functions=newFunctions;
|
||||
CommandDistributor::broadcastLoco(slot);
|
||||
}
|
||||
|
||||
void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
@@ -308,16 +340,17 @@ 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) {
|
||||
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat on packet three times
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
RMFT2::activateEvent(address<<2|port,gate);
|
||||
#endif
|
||||
}
|
||||
if (onoff != 1) {
|
||||
if (onoff==0) { // off packet only
|
||||
b[1] &= ~0x08; // set C to 0
|
||||
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat off packet three times
|
||||
}
|
||||
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 defined(EXRAIL_ACTIVE)
|
||||
if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {
|
||||
@@ -367,7 +400,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;
|
||||
DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), repeats);
|
||||
DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -386,7 +419,26 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = bValue;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4);
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
Railcom::anticipate(cab,cv,callback);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -407,7 +459,45 @@ 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;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 4);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
FSH* DCC::getMotorShieldName() {
|
||||
@@ -738,10 +828,9 @@ 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
|
||||
int reg=lookupSpeedTable(cab, false);
|
||||
if (reg>=0) {
|
||||
speedTable[reg].loco=0;
|
||||
setThrottle2(cab,1); // ESTOP if this loco still on track
|
||||
auto slot=lookupSpeedTable(cab, false);
|
||||
if (slot) {
|
||||
slot->loco=-1; // no longer used but not end of world
|
||||
CommandDistributor::broadcastForgetLoco(cab);
|
||||
}
|
||||
}
|
||||
@@ -749,7 +838,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;
|
||||
speedTable[i].loco=0; // no longer used and looks like end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,33 +846,79 @@ byte DCC::loopStatus=0;
|
||||
|
||||
void DCC::loop() {
|
||||
TrackManager::loop(); // power overload checks
|
||||
issueReminders();
|
||||
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()) {
|
||||
issueReminders();
|
||||
DCCQueue::scheduleNext(); // push through any just created reminder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DCC::issueReminders() {
|
||||
// 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.
|
||||
int reg = lastLocoReminder+1;
|
||||
if (reg > highestUsedReg) 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;
|
||||
auto slot = nextLocoReminder;
|
||||
if (slot >= &speedTable[MAX_LOCOS]) slot=&speedTable[0]; // Go to start of table
|
||||
if (slot->loco > 0)
|
||||
if (!issueReminder(slot))
|
||||
return;
|
||||
// a loco=0 is at the end of the list, a loco <0 is deleted
|
||||
if (slot->loco==0) nextLocoReminder = &speedTable[0];
|
||||
else nextLocoReminder=slot+1;
|
||||
}
|
||||
|
||||
bool DCC::issueReminder(int reg) {
|
||||
unsigned long functions=speedTable[reg].functions;
|
||||
int loco=speedTable[reg].loco;
|
||||
byte flags=speedTable[reg].groupFlags;
|
||||
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;
|
||||
|
||||
switch (loopStatus) {
|
||||
case 0:
|
||||
// DIAG(F("Reminder %d speed %d"),loco,speedTable[reg].speedCode);
|
||||
setThrottle2(loco, speedTable[reg].speedCode);
|
||||
break;
|
||||
case 0: {
|
||||
// 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);
|
||||
}
|
||||
break;
|
||||
case 1: // remind function group 1 (F0-F4)
|
||||
if (flags & FN_GROUP_1)
|
||||
#ifndef DISABLE_FUNCTION_REMINDERS
|
||||
@@ -844,70 +979,128 @@ byte DCC::cv2(int cv) {
|
||||
return lowByte(cv);
|
||||
}
|
||||
|
||||
int DCC::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
DCC::LOCO * DCC::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;
|
||||
LOCO * firstEmpty=nullptr;
|
||||
SLOTLOOP {
|
||||
if (firstEmpty==nullptr && slot->loco<=0) firstEmpty=slot;
|
||||
if (slot->loco == locoId) return slot;
|
||||
if (slot->loco==0) break;
|
||||
}
|
||||
|
||||
// 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 (!autoCreate) return nullptr;
|
||||
if (firstEmpty==nullptr) {
|
||||
// return last slot if full
|
||||
DIAG(F("Too many locos, reusing last slot"));
|
||||
firstEmpty=&speedTable[MAX_LOCOS-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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
// -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;
|
||||
}
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0 && speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DCC::LOCO DCC::speedTable[MAX_LOCOS];
|
||||
int DCC::lastLocoReminder = 0;
|
||||
int DCC::highestUsedReg = 0;
|
||||
DCC::LOCO * DCC::nextLocoReminder = &DCC::speedTable[0];
|
||||
|
||||
|
||||
void DCC::displayCabList(Print * stream) {
|
||||
|
||||
StringFormatter::send(stream,F("<*\n"));
|
||||
int used=0;
|
||||
for (int reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco>0) {
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
if (slot->loco>0) {
|
||||
used ++;
|
||||
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("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("Used=%d, max=%d\n"),used,MAX_LOCOS);
|
||||
|
||||
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
|
||||
}
|
||||
|
32
DCC.h
32
DCC.h
@@ -3,7 +3,7 @@
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
@@ -59,11 +59,15 @@ 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);
|
||||
@@ -83,7 +87,9 @@ 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);
|
||||
@@ -102,20 +108,31 @@ 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 int lookupSpeedTable(int locoId, bool autoCreate=true);
|
||||
static LOCO * 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 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 bool issueReminder(LOCO * slot);
|
||||
static LOCO* nextLocoReminder;
|
||||
static FSH *shieldName;
|
||||
static byte globalSpeedsteps;
|
||||
|
||||
@@ -126,6 +143,7 @@ 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;
|
||||
|
13
DCCACK.cpp
13
DCCACK.cpp
@@ -67,16 +67,24 @@ CALLBACK_STATE DCCACK::callbackState=READY;
|
||||
ACK_CALLBACK DCCACK::ackManagerCallback;
|
||||
|
||||
void DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
|
||||
// On ESP32 the joined track is hidden from sight (it has type MAIN)
|
||||
// and because of that we need first check if track was joined and
|
||||
// then unjoin if necessary. This requires that the joined flag is
|
||||
// cleared when the prog track is removed.
|
||||
ackManagerRejoin=TrackManager::isJoined();
|
||||
//DIAG(F("Joined is %d"), ackManagerRejoin);
|
||||
if (ackManagerRejoin) {
|
||||
// Change from JOIN must zero resets packet.
|
||||
TrackManager::setJoin(false);
|
||||
DCCWaveform::progTrack.clearResets();
|
||||
}
|
||||
|
||||
progDriver=TrackManager::getProgDriver();
|
||||
//DIAG(F("Progdriver is %d"), progDriver);
|
||||
if (progDriver==NULL) {
|
||||
TrackManager::setJoin(ackManagerRejoin);
|
||||
if (ackManagerRejoin) {
|
||||
DIAG(F("Joined but no Prog track"));
|
||||
TrackManager::setJoin(false);
|
||||
}
|
||||
callback(-3); // we dont have a prog track!
|
||||
return;
|
||||
}
|
||||
@@ -483,4 +491,3 @@ void DCCACK::checkAck(byte sentResetsSincePacket) {
|
||||
}
|
||||
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
|
||||
}
|
||||
|
||||
|
168
DCCEXParser.cpp
168
DCCEXParser.cpp
@@ -6,7 +6,7 @@
|
||||
* © 2020-2023 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2022 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -64,15 +64,16 @@ Once a new OPCODE is decided upon, update this list.
|
||||
I, Turntable object command, control, and broadcast
|
||||
j, Throttle responses
|
||||
J, Throttle queries
|
||||
k, Reserved for future use - Potentially Railcom
|
||||
K, Reserved for future use - Potentially Railcom
|
||||
k, Block exit (Railcom)
|
||||
K, Block enter (Railcom)
|
||||
l, Loco speedbyte/function map broadcast
|
||||
L, Reserved for LCC interface (implemented in EXRAIL)
|
||||
m, message to throttles broadcast
|
||||
m, message to throttles (broadcast output)
|
||||
m, set momentum
|
||||
M, Write DCC packet
|
||||
n, Reserved for SensorCam
|
||||
N, Reserved for Sensorcam
|
||||
o,
|
||||
o, Neopixel driver (see also IO_NeoPixel.h)
|
||||
O, Output broadcast
|
||||
p, Broadcast power state
|
||||
P, Write DCC packet
|
||||
@@ -117,6 +118,10 @@ Once a new OPCODE is decided upon, update this list.
|
||||
#include "Turntables.h"
|
||||
#include "version.h"
|
||||
#include "KeywordHasher.h"
|
||||
#include "CamParser.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "WifiESP32.h"
|
||||
#endif
|
||||
|
||||
// This macro can't be created easily as a portable function because the
|
||||
// flashlist requires a far pointer for high flash access.
|
||||
@@ -140,12 +145,12 @@ byte DCCEXParser::stashTarget=0;
|
||||
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.
|
||||
|
||||
|
||||
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd, bool usehex)
|
||||
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd, bool usehex)
|
||||
{
|
||||
byte state = 1;
|
||||
byte parameterCount = 0;
|
||||
int16_t runningValue = 0;
|
||||
const byte *remainingCmd = cmd + 1; // skips the opcode
|
||||
byte *remainingCmd = cmd + 1; // skips the opcode
|
||||
bool signNegative = false;
|
||||
|
||||
// clear all parameters in case not enough found
|
||||
@@ -155,7 +160,6 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
while (parameterCount < MAX_COMMAND_PARAMS)
|
||||
{
|
||||
byte hot = *remainingCmd;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
|
||||
@@ -169,7 +173,22 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
state = 2;
|
||||
continue;
|
||||
|
||||
case 2: // checking sign
|
||||
case 2: // checking sign or quoted string
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
if (hot == '"') {
|
||||
// this inserts an extra parameter 0x7777 in front
|
||||
// of each string parameter as a marker that can
|
||||
// be checked that a string parameter follows
|
||||
// This clashes of course with the real value
|
||||
// 0x7777 which we hope is used seldom
|
||||
result[parameterCount] = (int16_t)0x7777;
|
||||
parameterCount++;
|
||||
result[parameterCount] = (int16_t)(remainingCmd - cmd + 1);
|
||||
parameterCount++;
|
||||
state = 4;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
signNegative = false;
|
||||
runningValue = 0;
|
||||
state = 3;
|
||||
@@ -200,6 +219,16 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
parameterCount++;
|
||||
state = 1;
|
||||
continue;
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
case 4: // skipover text
|
||||
if (hot == '\0') // We did run to end of buffer without finding the "
|
||||
return -1;
|
||||
if (hot == '"') {
|
||||
*remainingCmd = '\0'; // overwrite " in command buffer with the end-of-string
|
||||
state = 1;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
remainingCmd++;
|
||||
}
|
||||
@@ -209,6 +238,7 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
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
|
||||
@@ -220,6 +250,10 @@ 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;
|
||||
@@ -275,6 +309,8 @@ 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)
|
||||
@@ -288,12 +324,9 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
int16_t direction;
|
||||
|
||||
if (params==1) { // <t cab> display state
|
||||
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 (p[0]<=0) break;
|
||||
CommandDistributor::broadcastLoco(DCC::lookupSpeedTable(p[0],false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (params == 4)
|
||||
@@ -374,7 +407,8 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
|| (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1
|
||||
) break;
|
||||
// Honour the configuration option (config.h) which allows the <a> command to be reversed
|
||||
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
|
||||
// Because of earlier confusion we need to do the same thing under both defines
|
||||
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
|
||||
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
|
||||
#else
|
||||
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
|
||||
@@ -394,14 +428,43 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
return;
|
||||
break;
|
||||
|
||||
case 'z': // direct pin manipulation
|
||||
#ifndef IO_NO_HAL
|
||||
case 'o': // Neopixel pin manipulation
|
||||
if (p[0]==0) break;
|
||||
{
|
||||
VPIN vpin=p[0]>0 ? p[0]:-p[0];
|
||||
bool setON=p[0]>0;
|
||||
if (params==1) { // <o [-]vpin>
|
||||
IODevice::write(vpin,setON);
|
||||
return;
|
||||
}
|
||||
if (params==2) { // <o [-]vpin count>
|
||||
IODevice::writeRange(vpin,setON,p[1]);
|
||||
return;
|
||||
}
|
||||
if (params==4 || params==5) { // <z [-]vpin r g b [count]>
|
||||
auto count=p[4]?p[4]:1;
|
||||
if (p[1]<0 || p[1]>0xFF) break;
|
||||
if (p[2]<0 || p[2]>0xFF) break;
|
||||
if (p[3]<0 || p[3]>0xFF) break;
|
||||
// strange parameter mangling... see IO_NeoPixel.h NeoPixel::_writeAnalogue
|
||||
int colour_RG=(p[1]<<8) | p[2];
|
||||
uint16_t colour_B=p[3];
|
||||
IODevice::writeAnalogueRange(vpin,colour_RG,setON,colour_B,count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 'z': // direct pin manipulation
|
||||
if (p[0]==0) break;
|
||||
if (params==1) { // <z vpin | -vpin>
|
||||
if (p[0]>0) IODevice::write(p[0],HIGH);
|
||||
else IODevice::write(-p[0],LOW);
|
||||
return;
|
||||
}
|
||||
if (params>=2 && params<=4) { // <z vpin ana;og profile duration>
|
||||
if (params>=2 && params<=4) { // <z vpin analog profile duration>
|
||||
// unused params default to 0
|
||||
IODevice::writeAnalogue(p[0],p[1],p[2],p[3]);
|
||||
return;
|
||||
@@ -425,12 +488,35 @@ 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
|
||||
@@ -521,7 +607,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
{
|
||||
if (params > 1) break;
|
||||
if (params==0) { // All
|
||||
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON);
|
||||
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);
|
||||
}
|
||||
if (params==1) {
|
||||
if (p[0]=="MAIN"_hk) { // <1 MAIN>
|
||||
@@ -554,7 +640,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
if (params > 1) break;
|
||||
if (params==0) { // All
|
||||
TrackManager::setJoin(false);
|
||||
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF);
|
||||
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);
|
||||
}
|
||||
if (params==1) {
|
||||
if (p[0]=="MAIN"_hk) { // <0 MAIN>
|
||||
@@ -580,7 +666,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
}
|
||||
|
||||
case '!': // ESTOP ALL <!>
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
return;
|
||||
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
@@ -616,9 +702,22 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
StringFormatter::send(stream, F("\n"));
|
||||
return;
|
||||
case 'C': // CONFIG <C [params]>
|
||||
if (parseC(stream, params, p))
|
||||
return;
|
||||
break;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
// currently this only works on ESP32
|
||||
#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
|
||||
break;
|
||||
if (p[1] == 0x7777 && p[3] == 0x7777) {
|
||||
WifiESP::setup((const char*)(com + p[2]), (const char*)(com + p[4]), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif //ESP32
|
||||
if (parseC(stream, params, p))
|
||||
return;
|
||||
break;
|
||||
#ifndef DISABLE_DIAG
|
||||
case 'D': // DIAG <D [params]>
|
||||
if (parseD(stream, params, p))
|
||||
@@ -807,10 +906,10 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
return;
|
||||
break;
|
||||
#endif
|
||||
|
||||
case '/': // 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
|
||||
case '@': // JMRI saying "give me virtual LCD msgs"
|
||||
@@ -1111,8 +1210,7 @@ bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
|
||||
default: // invalid/unknown
|
||||
default: // invalid/unknown
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
@@ -1138,6 +1236,10 @@ 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;
|
||||
@@ -1153,6 +1255,10 @@ 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>
|
||||
@@ -1345,6 +1451,12 @@ 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-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
@@ -37,13 +37,14 @@ 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
|
||||
|
||||
private:
|
||||
|
||||
static const int16_t MAX_BUFFER=50; // longest command sent in
|
||||
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
|
||||
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], byte * command, bool usehex);
|
||||
|
||||
static bool parseT(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
|
||||
@@ -68,7 +69,8 @@ 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);
|
||||
static void callback_R(int16_t result); // prog
|
||||
static void callback_r(int16_t result); // main
|
||||
static void callback_Rloco(int16_t result);
|
||||
static void callback_Wloco(int16_t result);
|
||||
static void callback_Wconsist(int16_t result);
|
||||
@@ -76,6 +78,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[]);
|
||||
|
185
DCCQueue.cpp
Normal file
185
DCCQueue.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* © 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/>.
|
||||
*/
|
||||
|
||||
|
||||
#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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Packet joins end of low priority queue.
|
||||
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats) {
|
||||
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
|
||||
}
|
||||
|
||||
// Packet replaces existing loco speed packet or joins end of high priority queue.
|
||||
|
||||
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
|
||||
PendingSlot * previous=nullptr;
|
||||
auto p=highPriorityQueue->head;
|
||||
while(p) {
|
||||
if (loco==0 || p->locoId==loco) {
|
||||
// drop this packet from the highPriority queue
|
||||
if (previous) previous->next=p->next;
|
||||
else highPriorityQueue->head=p->next;
|
||||
|
||||
recycle(p); // recycle this slot
|
||||
|
||||
// address next packet
|
||||
p=previous?previous->next : highPriorityQueue->head;
|
||||
}
|
||||
else {
|
||||
previous=p;
|
||||
p=p->next;
|
||||
}
|
||||
}
|
||||
// add the estop packet to the start of the queue
|
||||
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
|
||||
}
|
||||
|
||||
// Accessory gate-On Packet joins end of queue as normal.
|
||||
// When dequeued, packet is retained at start of queue
|
||||
// but modified to gate-off and given the delayed start.
|
||||
// getNext will ignore this packet until the requested start time.
|
||||
void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {
|
||||
auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);
|
||||
p->delayOff=delayms;
|
||||
lowPriorityQueue->addQueue(p);
|
||||
};
|
||||
|
||||
|
||||
// Obtain packet (fills packet, length and repeats)
|
||||
// returns 0 length if nothing in queue.
|
||||
|
||||
bool DCCQueue::scheduleNext() {
|
||||
// check high priority queue first
|
||||
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return false;
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) {
|
||||
// skip over pending ACC_OFF packets which are still delayed
|
||||
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
|
||||
// use this slot
|
||||
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else highPriorityQueue->head=p->next;
|
||||
if (!highPriorityQueue->head) highPriorityQueue->tail=nullptr;
|
||||
|
||||
// and recycle it.
|
||||
recycle(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No high priopity packets found, check low priority queue
|
||||
auto p=lowPriorityQueue->head;
|
||||
if (!p) return false; // nothing in queues
|
||||
|
||||
// schedule first packet in queue
|
||||
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
|
||||
|
||||
// remove from queue
|
||||
lowPriorityQueue->head=p->next;
|
||||
if (!lowPriorityQueue->head) lowPriorityQueue->tail=nullptr;
|
||||
|
||||
if (p->type == ACC_ON_PACKET) {
|
||||
// convert to a delayed off packet and jump the high priority queue
|
||||
p->type= ACC_OFF_PACKET;
|
||||
p->packet[1] &= ~0x08; // set C to 0 (gate off)
|
||||
p->startTime=millis()+p->delayOff;
|
||||
highPriorityQueue->jumpQueue(p);
|
||||
}
|
||||
else recycle(p); // recycle this slot
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
DIAG(F("New DCC queue slot"));
|
||||
p=new PendingSlot; // need a queue entry
|
||||
}
|
||||
p->next=nullptr;
|
||||
p->type=type;
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=repeats;
|
||||
memcpy((void*)p->packet,packet,length);
|
||||
p->locoId=loco;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
84
DCCQueue.h
Normal file
84
DCCQueue.h
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* © 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,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; // NORMAL_PACKET .. only set >0 for speed change packets
|
||||
// so they can be easily discarded if an estop jumps the queue.
|
||||
uint16_t delayOff; // ACC_ON_PACKET delay to apply between on/off
|
||||
uint32_t startTime; // ACC_OFF_PACKET time (mS) to transmit
|
||||
};
|
||||
};
|
||||
|
||||
class DCCQueue {
|
||||
public:
|
||||
|
||||
|
||||
// Non-speed packets are queued in the main queue
|
||||
static void scheduleDCCPacket(byte* packet, byte length, byte repeats);
|
||||
|
||||
// Speed packets are queued in the high priority queue
|
||||
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
|
||||
|
||||
// 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 if none pending.
|
||||
// returns true if a packet was scheduled.
|
||||
static bool scheduleNext();
|
||||
|
||||
private:
|
||||
|
||||
// statics to manage high and low priority queues and recycleing of PENDINGs
|
||||
static PendingSlot* recycleList;
|
||||
static DCCQueue* highPriorityQueue;
|
||||
static DCCQueue* lowPriorityQueue;
|
||||
|
||||
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);
|
||||
|
||||
};
|
||||
#endif
|
19
DCCRMT.cpp
19
DCCRMT.cpp
@@ -17,6 +17,25 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* RMT has "channels" which us FIFO RAM where you place what you want to send
|
||||
* or receive. Channels can be merged to get more words per channel.
|
||||
*
|
||||
* WROOM: 8 channels total of 512 words, 64 words per channel. We use currently
|
||||
* channel 0+1 for 128 words for DCC MAIN and 2+3 for DCC PROG.
|
||||
*
|
||||
* S3: 8 channels total of 384 words. 4 channels dedicated for TX and 4 channels
|
||||
* dedicated for RX. 48 words per channel. So for TX there are 4 channels and we
|
||||
* could use them with 96 words for MAIN and PROG if DCC data does fit in there.
|
||||
*
|
||||
* C3: 4 channels total of 192 words. As we do not use RX we can use all for TX
|
||||
* so the situation is the same as for the -S3
|
||||
*
|
||||
* C6, H2: 4 channels total of 192 words. 2 channels dedictaed for TX and
|
||||
* 2 channels dedicated for RX. Half RMT capacity compared to the C3.
|
||||
*
|
||||
*/
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include "defines.h"
|
||||
#include "DIAG.h"
|
||||
|
6
DCCRMT.h
6
DCCRMT.h
@@ -44,6 +44,12 @@ class RMTChannel {
|
||||
return true;
|
||||
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; };
|
||||
|
||||
private:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* © 2021 Mike S
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Chris Harlow
|
||||
* © 2021-2025 Chris Harlow
|
||||
* © 2021 David Cutting
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -57,66 +57,59 @@ 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 28uS after the last TIMER1 tick.
|
||||
- First triggers 58+29 uS after the previous 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,
|
||||
because its been setup with 100% PWM duty cycle.
|
||||
- Sets the Railcom pin high at first tick and subsequent ticks
|
||||
until its reset to setting pin 9 low at next tick.
|
||||
|
||||
- Cycles at 436uS so the second tick is the
|
||||
correct distance from the cutout.
|
||||
|
||||
- Waveform code is responsible for altering the PWM
|
||||
duty cycle to 0% any time between the first and last tick.
|
||||
- Waveform code is responsible for resetting
|
||||
any time between the first and second 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
|
||||
|
||||
// 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);
|
||||
|
||||
const int cycle=cutoutDuration/2;
|
||||
|
||||
// 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)
|
||||
|
||||
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
|
||||
// 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() {
|
||||
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
|
||||
// Change Timer2 to CTC mode with RESET pin 9 on next compare match
|
||||
TCCR2A = (1 << WGM21) | (1 << COM2B1);
|
||||
// diagnostic digitalWrite(4,LOW);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +190,8 @@ void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||
}
|
||||
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
|
||||
#if defined(ARDUINO_AVR_UNO)
|
||||
(void)fbits;
|
||||
(void) pin;
|
||||
// Not worth doin something here as:
|
||||
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
|
||||
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
|
||||
|
@@ -80,7 +80,7 @@ int DCCTimer::freeMemory() {
|
||||
|
||||
#include "esp_idf_version.h"
|
||||
#if ESP_IDF_VERSION_MAJOR > 4
|
||||
#error "DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDE version 4. Arduino ESP32 library 3.0.0 is too new. Downgrade to one of 2.0.9 to 2.0.17"
|
||||
#error "DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDF version 4. Arduino ESP32 library 3.0.0 is too new. Downgrade to one of 2.0.9 to 2.0.17"
|
||||
#endif
|
||||
|
||||
#include "DIAG.h"
|
||||
@@ -324,4 +324,3 @@ void ADCee::begin() {
|
||||
}
|
||||
|
||||
#endif //ESP32
|
||||
|
||||
|
@@ -70,9 +70,9 @@ HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 - F446RE
|
||||
defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||
// Nucleo-144 boards don't have Serial1 defined by default
|
||||
HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
|
||||
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5
|
||||
#if !defined(ARDUINO_NUCLEO_F412ZG)
|
||||
HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART5
|
||||
HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART2
|
||||
#if !defined(ARDUINO_NUCLEO_F412ZG) // F412ZG does not have UART5
|
||||
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5
|
||||
#endif
|
||||
// Serial3 is defined to use USART3 by default, but is in fact used as the diag console
|
||||
// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
||||
@@ -328,7 +328,7 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
|
||||
if (pin_timer[pin] != NULL)
|
||||
{
|
||||
pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle
|
||||
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d, frequency %d"), pin, pin_channel[pin], frequency);
|
||||
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer Channel %d, frequency %d"), pin, pin_channel[pin], frequency);
|
||||
}
|
||||
else
|
||||
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
|
||||
|
154
DCCWaveform.cpp
154
DCCWaveform.cpp
@@ -24,14 +24,13 @@
|
||||
#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);
|
||||
|
||||
@@ -71,9 +70,18 @@ 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];
|
||||
|
||||
@@ -115,19 +123,24 @@ 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) {
|
||||
// TODO check possible
|
||||
if (on && railcomPossible) {
|
||||
railcomActive=true;
|
||||
railcomDebug=debug;
|
||||
}
|
||||
else {
|
||||
railcomActive=false;
|
||||
railcomDebug=false;
|
||||
railcomSampleWindow=false;
|
||||
}
|
||||
return railcomActive;
|
||||
}
|
||||
@@ -140,14 +153,37 @@ 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<4 && remainingPreambles>1;
|
||||
if (remainingPreambles==1) promotePendingPacket();
|
||||
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
|
||||
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<10 && 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
|
||||
// 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);
|
||||
@@ -171,13 +207,7 @@ 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
|
||||
@@ -212,7 +242,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
|
||||
@@ -230,7 +260,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 porersets will be ones, making it much
|
||||
// and only the presets will be ones, making it much
|
||||
// easier to read on a logic analyser.
|
||||
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
|
||||
transmitLength = sizeof(idlePacket);
|
||||
@@ -238,93 +268,3 @@ 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
|
||||
|
||||
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 {
|
||||
if(isMainTrack) {
|
||||
if (rmtMainChannel != NULL)
|
||||
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
||||
} else {
|
||||
if (rmtProgChannel != NULL)
|
||||
ret = rmtProgChannel->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-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -23,11 +23,8 @@
|
||||
*/
|
||||
#ifndef DCCWaveform_h
|
||||
#define DCCWaveform_h
|
||||
|
||||
#include "MotorDriver.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "DCCRMT.h"
|
||||
#include "TrackManager.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -86,8 +83,30 @@ class DCCWaveform {
|
||||
bool isReminderWindowOpen();
|
||||
void promotePendingPacket();
|
||||
static bool setRailcom(bool on, bool debug);
|
||||
static bool isRailcom() {return railcomActive;}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef ARDUINO_ARCH_ESP32
|
||||
volatile bool packetPending;
|
||||
@@ -112,9 +131,13 @@ 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;
|
||||
|
120
DCCWaveformRMT.cpp
Normal file
120
DCCWaveformRMT.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* © 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
|
20
EXRAIL.h
20
EXRAIL.h
@@ -1,3 +1,23 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* 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 EXRAIL_H
|
||||
#define EXRAIL_H
|
||||
|
||||
|
325
EXRAIL2.cpp
325
EXRAIL2.cpp
@@ -2,8 +2,9 @@
|
||||
* © 2024 Paul M. Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2020-2023 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2025 Morten Nielsen
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -73,6 +74,7 @@ RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
|
||||
byte RMFT2::flags[MAX_FLAGS];
|
||||
Print * RMFT2::LCCSerial=0;
|
||||
LookList * RMFT2::routeLookup=NULL;
|
||||
LookList * RMFT2::signalLookup=NULL;
|
||||
LookList * RMFT2::onThrowLookup=NULL;
|
||||
LookList * RMFT2::onCloseLookup=NULL;
|
||||
LookList * RMFT2::onActivateLookup=NULL;
|
||||
@@ -86,6 +88,8 @@ LookList * RMFT2::onClockLookup=NULL;
|
||||
LookList * RMFT2::onRotateLookup=NULL;
|
||||
#endif
|
||||
LookList * RMFT2::onOverloadLookup=NULL;
|
||||
LookList * RMFT2::onBlockEnterLookup=NULL;
|
||||
LookList * RMFT2::onBlockExitLookup=NULL;
|
||||
byte * RMFT2::routeStateArray=nullptr;
|
||||
const FSH * * RMFT2::routeCaptionArray=nullptr;
|
||||
int16_t * RMFT2::stashArray=nullptr;
|
||||
@@ -130,11 +134,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) {
|
||||
void LookList::handleEvent(const FSH* reason,int16_t id, int16_t loco) {
|
||||
// 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]);
|
||||
RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i],loco);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,21 +206,39 @@ 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);
|
||||
}
|
||||
|
||||
// onLCCLookup is not the same so not loaded here.
|
||||
|
||||
// Second pass startup, define any turnouts or servos, set signals red
|
||||
// add sequences onRoutines to the lookups
|
||||
if (compileFeatures & FEATURE_SIGNAL) {
|
||||
|
||||
onRedLookup=LookListLoader(OPCODE_ONRED);
|
||||
onAmberLookup=LookListLoader(OPCODE_ONAMBER);
|
||||
onGreenLookup=LookListLoader(OPCODE_ONGREEN);
|
||||
for (int sigslot=0;;sigslot++) {
|
||||
int16_t sighandle=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
|
||||
if (sighandle==0) break; // end of signal list
|
||||
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||
doSignal(sigid, SIGNAL_RED);
|
||||
}
|
||||
}
|
||||
// Load the signal lookup with slot numbers in the signal table
|
||||
int signalCount=0;
|
||||
for (int16_t slot=0;;slot++) {
|
||||
SIGNAL_DEFINITION signal=getSignalSlot(slot);
|
||||
DIAG(F("Signal s=%d id=%d t=%d"),slot,signal.id,signal.type);
|
||||
if (signal.type==sigtypeNoMoreSignals) break;
|
||||
if (signal.type==sigtypeContinuation) continue;
|
||||
signalCount++;
|
||||
}
|
||||
signalLookup=new LookList(signalCount);
|
||||
for (int16_t slot=0;;slot++) {
|
||||
SIGNAL_DEFINITION signal=getSignalSlot(slot);
|
||||
if (signal.type==sigtypeNoMoreSignals) break;
|
||||
if (signal.type==sigtypeContinuation) continue;
|
||||
signalLookup->add(signal.id,slot);
|
||||
doSignal(signal.id, SIGNAL_RED);
|
||||
}
|
||||
}
|
||||
|
||||
int progCounter;
|
||||
for (progCounter=0;; SKIPOP){
|
||||
@@ -299,7 +321,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
case OPCODE_EXTTTURNTABLE: {
|
||||
VPIN id=operand;
|
||||
VPIN pin=getOperand(progCounter,1);
|
||||
int home=getOperand(progCounter,3);
|
||||
int home=getOperand(progCounter,2);
|
||||
setTurntableHiddenState(EXTTTurntable::create(id,pin));
|
||||
Turntable *tto=Turntable::get(id);
|
||||
tto->addPosition(0,0,home);
|
||||
@@ -366,7 +388,7 @@ char RMFT2::getRouteType(int16_t id) {
|
||||
}
|
||||
|
||||
|
||||
RMFT2::RMFT2(int progCtr) {
|
||||
RMFT2::RMFT2(int progCtr, int16_t _loco) {
|
||||
progCounter=progCtr;
|
||||
|
||||
// get an unused task id from the flags table
|
||||
@@ -379,9 +401,7 @@ RMFT2::RMFT2(int progCtr) {
|
||||
}
|
||||
}
|
||||
delayTime=0;
|
||||
loco=0;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
loco=_loco;
|
||||
invert=false;
|
||||
blinkState=not_blink_task;
|
||||
stackDepth=0;
|
||||
@@ -399,7 +419,10 @@ RMFT2::RMFT2(int progCtr) {
|
||||
|
||||
|
||||
RMFT2::~RMFT2() {
|
||||
driveLoco(1); // ESTOP my loco if any
|
||||
// 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));
|
||||
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
|
||||
if (next==this)
|
||||
loopTask=NULL;
|
||||
@@ -415,23 +438,9 @@ RMFT2::~RMFT2() {
|
||||
void RMFT2::createNewTask(int route, uint16_t cab) {
|
||||
int pc=routeLookup->find(route);
|
||||
if (pc<0) return;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
new RMFT2(pc,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.
|
||||
@@ -486,7 +495,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"), progtrackLocoId, HIGHEST_SHORT_ADDR);
|
||||
DIAG(F("Long addr %d <= %d unsupported\n"), progtrackLocoId, HIGHEST_SHORT_ADDR);
|
||||
progtrackLocoId = -1;
|
||||
}
|
||||
} else {
|
||||
@@ -494,6 +503,15 @@ 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();
|
||||
@@ -558,18 +576,23 @@ void RMFT2::loop2() {
|
||||
#endif
|
||||
|
||||
case OPCODE_REV:
|
||||
forward = false;
|
||||
driveLoco(operand);
|
||||
if (loco) DCC::setThrottle(loco,operand,invert);
|
||||
break;
|
||||
|
||||
case OPCODE_FWD:
|
||||
forward = true;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
if (loco) DCC::setThrottle(loco,operand,!invert);
|
||||
break;
|
||||
|
||||
case OPCODE_SPEED:
|
||||
forward=DCC::getThrottleDirection(loco)^invert;
|
||||
driveLoco(operand);
|
||||
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
|
||||
break;
|
||||
|
||||
case OPCODE_FORGET:
|
||||
@@ -581,12 +604,11 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_INVERT_DIRECTION:
|
||||
invert= !invert;
|
||||
driveLoco(speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_RESERVE:
|
||||
if (getFlag(operand,SECTION_FLAG)) {
|
||||
driveLoco(0);
|
||||
if (loco) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));
|
||||
delayMe(500);
|
||||
return;
|
||||
}
|
||||
@@ -664,13 +686,14 @@ void RMFT2::loop2() {
|
||||
break;
|
||||
|
||||
case OPCODE_SET:
|
||||
killBlinkOnVpin(operand);
|
||||
IODevice::write(operand,true);
|
||||
break;
|
||||
|
||||
case OPCODE_RESET:
|
||||
killBlinkOnVpin(operand);
|
||||
IODevice::write(operand,false);
|
||||
{
|
||||
auto count=getOperand(1);
|
||||
for (uint16_t i=0;i<count;i++) {
|
||||
killBlinkOnVpin(operand+i);
|
||||
IODevice::write(operand+i,opcode==OPCODE_SET);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_BLINK:
|
||||
@@ -684,7 +707,7 @@ void RMFT2::loop2() {
|
||||
break;
|
||||
|
||||
case OPCODE_PAUSE:
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
DCC::estopAll(); // pause all locos on the track
|
||||
pausingTask=this;
|
||||
break;
|
||||
|
||||
@@ -692,6 +715,10 @@ 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);
|
||||
@@ -728,8 +755,8 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_RESUME:
|
||||
pausingTask=NULL;
|
||||
driveLoco(speedo);
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
|
||||
resume();
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) t->resume();
|
||||
break;
|
||||
|
||||
case OPCODE_IF: // do next operand if sensor set
|
||||
@@ -840,8 +867,7 @@ void RMFT2::loop2() {
|
||||
|
||||
case OPCODE_DRIVE:
|
||||
{
|
||||
byte analogSpeed=IODevice::readAnalogue(operand) *127 / 1024;
|
||||
if (speedo!=analogSpeed) driveLoco(analogSpeed);
|
||||
// Non functional but reserved
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -857,6 +883,14 @@ void RMFT2::loop2() {
|
||||
DCC::changeFn(operand,getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_XFWD:
|
||||
DCC::setThrottle(operand,getOperand(1), true);
|
||||
break;
|
||||
|
||||
case OPCODE_XREV:
|
||||
DCC::setThrottle(operand,getOperand(1), false);
|
||||
break;
|
||||
|
||||
case OPCODE_DCCACTIVATE: {
|
||||
// operand is address<<3 | subaddr<<1 | active
|
||||
int16_t addr=operand>>3;
|
||||
@@ -931,8 +965,6 @@ void RMFT2::loop2() {
|
||||
// which is intended so it can be checked
|
||||
// from within EXRAIL
|
||||
loco=progtrackLocoId;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
break;
|
||||
#endif
|
||||
@@ -954,16 +986,13 @@ void RMFT2::loop2() {
|
||||
{
|
||||
int newPc=routeLookup->find(getOperand(1));
|
||||
if (newPc<0) break;
|
||||
RMFT2* newtask=new RMFT2(newPc); // create new task
|
||||
newtask->loco=operand;
|
||||
new RMFT2(newPc,operand); // create new task
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SETLOCO:
|
||||
{
|
||||
loco=operand;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
}
|
||||
break;
|
||||
@@ -1001,8 +1030,18 @@ void RMFT2::loop2() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
#ifndef IO_NO_HAL
|
||||
case OPCODE_NEOPIXEL:
|
||||
// OPCODE_NEOPIXEL,V([-]vpin),OPCODE_PAD,V(colour_RG),OPCODE_PAD,V(colour_B),OPCODE_PAD,V(count)
|
||||
{
|
||||
VPIN vpin=operand>0?operand:-operand;
|
||||
auto count=getOperand(3);
|
||||
killBlinkOnVpin(vpin,count);
|
||||
IODevice::writeAnalogueRange(vpin,getOperand(1),operand>0,getOperand(2),count);
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_WAITFORTT: // OPCODE_WAITFOR,V(turntable_id)
|
||||
if (Turntable::ttMoving(operand)) {
|
||||
delayMe(100);
|
||||
@@ -1087,7 +1126,8 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONROTATE:
|
||||
#endif
|
||||
case OPCODE_ONOVERLOAD:
|
||||
|
||||
case OPCODE_ONBLOCKENTER:
|
||||
case OPCODE_ONBLOCKEXIT:
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1124,26 +1164,11 @@ void RMFT2::kill(const FSH * reason, int operand) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
int16_t RMFT2::getSignalSlot(int16_t id) {
|
||||
|
||||
if (id > 0) {
|
||||
int sigslot = 0;
|
||||
int16_t sighandle = 0;
|
||||
// Trundle down the signal list until we reach the end
|
||||
while ((sighandle = GETHIGHFLASHW(RMFT2::SignalDefinitions, sigslot * 8)) != 0)
|
||||
{
|
||||
// sigid is the signal id used in RED/AMBER/GREEN macro
|
||||
// for a LED signal it will be same as redpin
|
||||
// but for a servo signal it will also have SERVO_SIGNAL_FLAG set.
|
||||
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||
if (sigid == (VPIN)id) // cast to keep compiler happy but id is positive
|
||||
return sigslot; // found it
|
||||
sigslot++; // keep looking
|
||||
};
|
||||
}
|
||||
// If we got here, we did not find the signal
|
||||
DIAG(F("EXRAIL Signal %d not defined"), id);
|
||||
return -1;
|
||||
SIGNAL_DEFINITION RMFT2::getSignalSlot(int16_t slot) {
|
||||
SIGNAL_DEFINITION signal;
|
||||
COPYHIGHFLASH(&signal,SignalDefinitions,slot*sizeof(SIGNAL_DEFINITION),sizeof(SIGNAL_DEFINITION));
|
||||
return signal;
|
||||
}
|
||||
|
||||
/* static */ void RMFT2::doSignal(int16_t id,char rag) {
|
||||
@@ -1156,81 +1181,97 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
|
||||
else if (rag==SIGNAL_GREEN) onGreenLookup->handleEvent(F("GREEN"),id);
|
||||
else onAmberLookup->handleEvent(F("AMBER"),id);
|
||||
|
||||
int16_t sigslot=getSignalSlot(id);
|
||||
auto sigslot=signalLookup->find(id);
|
||||
if (sigslot<0) return;
|
||||
|
||||
// keep track of signal state
|
||||
setFlag(sigslot,rag,SIGNAL_MASK);
|
||||
|
||||
// Correct signal definition found, get the rag values
|
||||
int16_t sigpos=sigslot*8;
|
||||
int16_t sighandle=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos);
|
||||
VPIN redpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+2);
|
||||
VPIN amberpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+4);
|
||||
VPIN greenpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+6);
|
||||
//if (diag) DIAG(F("signal %d %d %d %d %d"),sigid,id,redpin,amberpin,greenpin);
|
||||
|
||||
VPIN sigtype=sighandle & ~SIGNAL_ID_MASK;
|
||||
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||
|
||||
if (sigtype == SERVO_SIGNAL_FLAG) {
|
||||
// A servo signal, the pin numbers are actually servo positions
|
||||
// Note, setting a signal to a zero position has no effect.
|
||||
int16_t servopos= rag==SIGNAL_RED? redpin: (rag==SIGNAL_GREEN? greenpin : amberpin);
|
||||
auto signal=getSignalSlot(sigslot);
|
||||
|
||||
switch (signal.type) {
|
||||
case sigtypeSERVO:
|
||||
{
|
||||
auto servopos = rag==SIGNAL_RED? signal.redpin: (rag==SIGNAL_GREEN? signal.greenpin : signal.amberpin);
|
||||
//if (diag) DIAG(F("sigA %d %d"),id,servopos);
|
||||
if (servopos!=0) IODevice::writeAnalogue(id,servopos,PCA9685::Bounce);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (sigtype== DCC_SIGNAL_FLAG) {
|
||||
case sigtypeDCC:
|
||||
{
|
||||
// redpin,amberpin are the DCC addr,subaddr
|
||||
DCC::setAccessory(redpin,amberpin, rag!=SIGNAL_RED);
|
||||
DCC::setAccessory(signal.redpin,signal.amberpin, rag!=SIGNAL_RED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sigtype== DCCX_SIGNAL_FLAG) {
|
||||
case sigtypeDCCX:
|
||||
{
|
||||
// redpin,amberpin,greenpin are the 3 aspects
|
||||
byte value=redpin;
|
||||
if (rag==SIGNAL_AMBER) value=amberpin;
|
||||
if (rag==SIGNAL_GREEN) value=greenpin;
|
||||
DCC::setExtendedAccessory(sigid, value);
|
||||
auto value=signal.redpin;
|
||||
if (rag==SIGNAL_AMBER) value=signal.amberpin;
|
||||
if (rag==SIGNAL_GREEN) value=signal.greenpin;
|
||||
DCC::setExtendedAccessory(id, value);
|
||||
return;
|
||||
}
|
||||
|
||||
case sigtypeNEOPIXEL:
|
||||
{
|
||||
// redpin,amberpin,greenpin are the 3 RG values but with no blue permitted. . (code limitation hack)
|
||||
auto colour_RG=signal.redpin;
|
||||
if (rag==SIGNAL_AMBER) colour_RG=signal.amberpin;
|
||||
if (rag==SIGNAL_GREEN) colour_RG=signal.greenpin;
|
||||
|
||||
// blue channel is in followng signal slot (a continuation)
|
||||
auto signal2=getSignalSlot(sigslot+1);
|
||||
auto colour_B=signal2.redpin;
|
||||
if (rag==SIGNAL_AMBER) colour_B=signal2.amberpin;
|
||||
if (rag==SIGNAL_GREEN) colour_B=signal2.greenpin;
|
||||
IODevice::writeAnalogue(id, colour_RG,true,colour_B);
|
||||
return;
|
||||
}
|
||||
|
||||
case sigtypeSIGNAL:
|
||||
case sigtypeSIGNALH:
|
||||
{
|
||||
// LED or similar 3 pin signal, (all pins zero would be a virtual signal)
|
||||
// If amberpin is zero, synthesise amber from red+green
|
||||
const byte SIMAMBER=0x00;
|
||||
if (rag==SIGNAL_AMBER && (amberpin==0)) rag=SIMAMBER; // special case this func only
|
||||
if (rag==SIGNAL_AMBER && (signal.amberpin==0)) rag=SIMAMBER; // special case this func only
|
||||
|
||||
// Manage invert (HIGH on) pins
|
||||
bool aHigh=sighandle & ACTIVE_HIGH_SIGNAL_FLAG;
|
||||
bool aHigh=signal.type==sigtypeSIGNALH;
|
||||
|
||||
// set the three pins
|
||||
if (redpin) {
|
||||
if (signal.redpin) {
|
||||
bool redval=(rag==SIGNAL_RED || rag==SIMAMBER);
|
||||
if (!aHigh) redval=!redval;
|
||||
killBlinkOnVpin(redpin);
|
||||
IODevice::write(redpin,redval);
|
||||
killBlinkOnVpin(signal.redpin);
|
||||
IODevice::write(signal.redpin,redval);
|
||||
}
|
||||
if (amberpin) {
|
||||
if (signal.amberpin) {
|
||||
bool amberval=(rag==SIGNAL_AMBER);
|
||||
if (!aHigh) amberval=!amberval;
|
||||
killBlinkOnVpin(amberpin);
|
||||
IODevice::write(amberpin,amberval);
|
||||
killBlinkOnVpin(signal.amberpin);
|
||||
IODevice::write(signal.amberpin,amberval);
|
||||
}
|
||||
if (greenpin) {
|
||||
if (signal.greenpin) {
|
||||
bool greenval=(rag==SIGNAL_GREEN || rag==SIMAMBER);
|
||||
if (!aHigh) greenval=!greenval;
|
||||
killBlinkOnVpin(greenpin);
|
||||
IODevice::write(greenpin,greenval);
|
||||
killBlinkOnVpin(signal.greenpin);
|
||||
IODevice::write(signal.greenpin,greenval);
|
||||
}
|
||||
}
|
||||
case sigtypeVIRTUAL: break;
|
||||
case sigtypeContinuation: break;
|
||||
case sigtypeNoMoreSignals: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* static */ bool RMFT2::isSignal(int16_t id,char rag) {
|
||||
if (!(compileFeatures & FEATURE_SIGNAL)) return false;
|
||||
int16_t sigslot=getSignalSlot(id);
|
||||
int16_t sigslot=signalLookup->find(id);
|
||||
if (sigslot<0) return false;
|
||||
return (flags[sigslot] & SIGNAL_MASK) == rag;
|
||||
}
|
||||
@@ -1242,26 +1283,23 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
|
||||
// Otherwise false so the parser should send the command directly
|
||||
bool RMFT2::signalAspectEvent(int16_t address, byte aspect ) {
|
||||
if (!(compileFeatures & FEATURE_SIGNAL)) return false;
|
||||
int16_t sigslot=getSignalSlot(address);
|
||||
auto sigslot=signalLookup->find(address);
|
||||
if (sigslot<0) return false; // this is not a defined signal
|
||||
int16_t sigpos=sigslot*8;
|
||||
int16_t sighandle=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos);
|
||||
VPIN sigtype=sighandle & ~SIGNAL_ID_MASK;
|
||||
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||
if (sigtype!=DCCX_SIGNAL_FLAG) return false; // not a DCCX signal
|
||||
auto signal=getSignalSlot(sigslot);
|
||||
if (signal.type!=sigtypeDCCX) return false; // not a DCCX signal
|
||||
// Turn an aspect change into a RED/AMBER/GREEN setting
|
||||
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+2)) {
|
||||
doSignal(sigid,SIGNAL_RED);
|
||||
if (aspect==signal.redpin) {
|
||||
doSignal(address,SIGNAL_RED);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+4)) {
|
||||
doSignal(sigid,SIGNAL_AMBER);
|
||||
if (aspect==signal.amberpin) {
|
||||
doSignal(address,SIGNAL_AMBER);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+6)) {
|
||||
doSignal(sigid,SIGNAL_GREEN);
|
||||
if (aspect==signal.greenpin) {
|
||||
doSignal(address,SIGNAL_GREEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1281,6 +1319,14 @@ 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);
|
||||
@@ -1306,7 +1352,7 @@ void RMFT2::clockEvent(int16_t clocktime, bool change) {
|
||||
void RMFT2::powerEvent(int16_t track, bool overload) {
|
||||
// Hunt for an ONOVERLOAD for this item
|
||||
if (Diag::CMD)
|
||||
DIAG(F("powerEvent : %c"), track);
|
||||
DIAG(F("powerEvent : %c"), track + 'A');
|
||||
if (overload) {
|
||||
onOverloadLookup->handleEvent(F("POWER"),track);
|
||||
}
|
||||
@@ -1315,27 +1361,32 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
|
||||
// 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.
|
||||
void RMFT2::killBlinkOnVpin(VPIN pin) {
|
||||
void RMFT2::killBlinkOnVpin(VPIN pin, uint16_t count) {
|
||||
if (!(compileFeatures & FEATURE_BLINK)) return;
|
||||
|
||||
RMFT2 * stoptask=loopTask; // stop when we get back to here
|
||||
RMFT2 * task=loopTask;
|
||||
VPIN lastPin=pin+count-1;
|
||||
while(task) {
|
||||
auto nextTask=task->next;
|
||||
if (
|
||||
(task->blinkState==blink_high || task->blinkState==blink_low)
|
||||
&& task->blinkPin==pin) {
|
||||
&& task->blinkPin>=pin
|
||||
&& task->blinkPin<=lastPin
|
||||
) {
|
||||
if (diag) DIAG(F("kill blink %d"),task->blinkPin,lastPin);
|
||||
task->kill();
|
||||
return;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) return;
|
||||
}
|
||||
task=nextTask;
|
||||
if (task==stoptask) return;
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
|
||||
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco) {
|
||||
// Check we dont already have a task running this handler
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->onEventStartPosition==pc) {
|
||||
if (task->onEventStartPosition==pc && task->loco==loco) {
|
||||
DIAG(F("Recursive ON%S(%d)"),reason, id);
|
||||
return;
|
||||
}
|
||||
@@ -1343,7 +1394,7 @@ void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
|
||||
task=new RMFT2(pc); // new task starts at this instruction
|
||||
task=new RMFT2(pc,loco); // new task starts at this instruction
|
||||
task->onEventStartPosition=pc; // flag for recursion detector
|
||||
}
|
||||
|
||||
|
60
EXRAIL2.h
60
EXRAIL2.h
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2022 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2023 Harald Barth
|
||||
* © 2025 Morten Nielsen
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -35,6 +36,7 @@
|
||||
//
|
||||
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,
|
||||
@@ -45,7 +47,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_ENDIF,OPCODE_ELSE,
|
||||
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
|
||||
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
|
||||
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,
|
||||
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,OPCODE_XFWD,OPCODE_XREV,
|
||||
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
|
||||
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
|
||||
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
|
||||
@@ -76,6 +78,9 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_ROUTE_DISABLED,
|
||||
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
||||
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
|
||||
OPCODE_NEOPIXEL,
|
||||
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
|
||||
OPCODE_ESTOPALL,OPCODE_XPOM,
|
||||
// OPcodes below this point are skip-nesting IF operations
|
||||
// placed here so that they may be skipped as a group
|
||||
// see skipIfBlock()
|
||||
@@ -109,6 +114,23 @@ enum BlinkState: byte {
|
||||
blink_high, // blink task running with pin high
|
||||
at_timeout // ATTIMEOUT timed out flag
|
||||
};
|
||||
enum SignalType {
|
||||
sigtypeVIRTUAL,
|
||||
sigtypeSIGNAL,
|
||||
sigtypeSIGNALH,
|
||||
sigtypeDCC,
|
||||
sigtypeDCCX,
|
||||
sigtypeSERVO,
|
||||
sigtypeNEOPIXEL,
|
||||
sigtypeContinuation, // neopixels require a second line
|
||||
sigtypeNoMoreSignals
|
||||
};
|
||||
|
||||
struct SIGNAL_DEFINITION {
|
||||
SignalType type;
|
||||
VPIN id;
|
||||
VPIN redpin,amberpin,greenpin;
|
||||
};
|
||||
|
||||
// Flag bits for compile time features.
|
||||
static const byte FEATURE_SIGNAL= 0x80;
|
||||
@@ -118,6 +140,7 @@ enum BlinkState: byte {
|
||||
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
|
||||
@@ -144,7 +167,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);
|
||||
void handleEvent(const FSH* reason,int16_t id, int16_t loco=0);
|
||||
|
||||
private:
|
||||
int16_t m_size;
|
||||
@@ -158,8 +181,7 @@ class LookList {
|
||||
public:
|
||||
static void begin();
|
||||
static void loop();
|
||||
RMFT2(int progCounter);
|
||||
RMFT2(int route, uint16_t cab);
|
||||
RMFT2(int progCounter, int16_t cab=0);
|
||||
~RMFT2();
|
||||
static void readLocoCallback(int16_t cv);
|
||||
static void createNewTask(int route, uint16_t cab);
|
||||
@@ -169,13 +191,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);
|
||||
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
|
||||
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
|
||||
static const int16_t DCC_SIGNAL_FLAG=0x1000;
|
||||
static const int16_t DCCX_SIGNAL_FLAG=0x3000;
|
||||
static const int16_t SIGNAL_ID_MASK=0x0FFF;
|
||||
// Throttle Info Access functions built by exrail macros
|
||||
// Throttle Info Access functions built by exrail macros
|
||||
static const byte rosterNameCount;
|
||||
static const int16_t HIGHFLASH routeIdList[];
|
||||
static const int16_t HIGHFLASH automationIdList[];
|
||||
@@ -187,9 +205,10 @@ 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);
|
||||
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco=0);
|
||||
static bool readSensor(uint16_t sensorId);
|
||||
static bool isSignal(int16_t id,char rag);
|
||||
static bool isSignal(int16_t id,char rag);
|
||||
static SIGNAL_DEFINITION getSignalSlot(int16_t slotno);
|
||||
|
||||
private:
|
||||
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
@@ -199,7 +218,6 @@ private:
|
||||
static bool getFlag(VPIN id,byte mask);
|
||||
static int16_t progtrackLocoId;
|
||||
static void doSignal(int16_t id,char rag);
|
||||
static int16_t getSignalSlot(int16_t id);
|
||||
static void setTurnoutHiddenState(Turnout * t);
|
||||
#ifndef IO_NO_HAL
|
||||
static void setTurntableHiddenState(Turntable * tto);
|
||||
@@ -207,11 +225,10 @@ private:
|
||||
static LookList* LookListLoader(OPCODE op1,
|
||||
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
|
||||
static uint16_t getOperand(int progCounter,byte n);
|
||||
static void killBlinkOnVpin(VPIN pin);
|
||||
static void killBlinkOnVpin(VPIN pin,uint16_t count=1);
|
||||
static RMFT2 * loopTask;
|
||||
static RMFT2 * pausingTask;
|
||||
void delayMe(long millisecs);
|
||||
void driveLoco(byte speedo);
|
||||
bool skipIfBlock();
|
||||
bool readLoco();
|
||||
void loop2();
|
||||
@@ -220,13 +237,16 @@ 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[];
|
||||
static const HIGHFLASH int16_t SignalDefinitions[];
|
||||
static const HIGHFLASH SIGNAL_DEFINITION SignalDefinitions[];
|
||||
static byte flags[MAX_FLAGS];
|
||||
static Print * LCCSerial;
|
||||
static LookList * routeLookup;
|
||||
static LookList * signalLookup;
|
||||
static LookList * onThrowLookup;
|
||||
static LookList * onCloseLookup;
|
||||
static LookList * onActivateLookup;
|
||||
@@ -240,6 +260,9 @@ private:
|
||||
static LookList * onRotateLookup;
|
||||
#endif
|
||||
static LookList * onOverloadLookup;
|
||||
static LookList * onBlockEnterLookup;
|
||||
static LookList * onBlockExitLookup;
|
||||
|
||||
|
||||
static const int countLCCLookup;
|
||||
static int onLCCLookup[];
|
||||
@@ -264,9 +287,8 @@ private:
|
||||
byte taskId;
|
||||
BlinkState blinkState; // includes AT_TIMEOUT flag.
|
||||
uint16_t loco;
|
||||
bool forward;
|
||||
bool invert;
|
||||
byte speedo;
|
||||
byte pauseSpeed;
|
||||
int onEventStartPosition;
|
||||
byte stackDepth;
|
||||
int callStack[MAX_STACK_DEPTH];
|
||||
|
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* © 2020-2022 Chris Harlow. All rights reserved.
|
||||
* © 2020-2025 Chris Harlow. All rights reserved.
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2023 Harald Barth
|
||||
* © 2025 Morten Nielsen
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -60,6 +61,7 @@
|
||||
#undef ENDIF
|
||||
#undef ENDTASK
|
||||
#undef ESTOP
|
||||
#undef ESTOPALL
|
||||
#undef EXRAIL
|
||||
#undef EXTT_TURNTABLE
|
||||
#undef FADE
|
||||
@@ -98,7 +100,11 @@
|
||||
#undef LCC
|
||||
#undef LCCX
|
||||
#undef LCN
|
||||
#undef MOMENTUM
|
||||
#undef MOVETT
|
||||
#undef NEOPIXEL
|
||||
#undef NEOPIXEL_OFF
|
||||
#undef NEOPIXEL_SIGNAL
|
||||
#undef ACON
|
||||
#undef ACOF
|
||||
#undef ONACON
|
||||
@@ -107,6 +113,8 @@
|
||||
#undef ONACTIVATE
|
||||
#undef ONACTIVATEL
|
||||
#undef ONAMBER
|
||||
#undef ONBLOCKENTER
|
||||
#undef ONBLOCKEXIT
|
||||
#undef ONDEACTIVATE
|
||||
#undef ONDEACTIVATEL
|
||||
#undef ONCLOSE
|
||||
@@ -191,6 +199,9 @@
|
||||
#undef XFOFF
|
||||
#undef XFON
|
||||
#undef XFTOGGLE
|
||||
#undef XPOM
|
||||
#undef XREV
|
||||
#undef XFWD
|
||||
|
||||
#ifndef RMFT2_UNDEF_ONLY
|
||||
#define ACTIVATE(addr,subaddr)
|
||||
@@ -216,7 +227,7 @@
|
||||
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||
#define DCC_SIGNAL(id,add,subaddr)
|
||||
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
|
||||
#define DCC_TURNTABLE(id,home,description)
|
||||
#define DCC_TURNTABLE(id,home,description...)
|
||||
#define DEACTIVATE(addr,subaddr)
|
||||
#define DEACTIVATEL(addr)
|
||||
#define DELAY(mindelay)
|
||||
@@ -229,8 +240,9 @@
|
||||
#define ENDIF
|
||||
#define ENDTASK
|
||||
#define ESTOP
|
||||
#define ESTOPALL
|
||||
#define EXRAIL
|
||||
#define EXTT_TURNTABLE(id,vpin,home,description)
|
||||
#define EXTT_TURNTABLE(id,vpin,home,description...)
|
||||
#define FADE(pin,value,ms)
|
||||
#define FOFF(func)
|
||||
#define FOLLOW(route)
|
||||
@@ -265,10 +277,13 @@
|
||||
#define LCC(eventid)
|
||||
#define LCCX(senderid,eventid)
|
||||
#define LCD(row,msg)
|
||||
#define MOMENTUM(accel,decel...)
|
||||
#define SCREEN(display,row,msg)
|
||||
#define LCN(msg)
|
||||
#define MESSAGE(msg)
|
||||
#define MOVETT(id,steps,activity)
|
||||
#define NEOPIXEL(id,r,g,b,count...)
|
||||
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour)
|
||||
#define ACON(eventid)
|
||||
#define ACOF(eventid)
|
||||
#define ONACON(eventid)
|
||||
@@ -276,6 +291,8 @@
|
||||
#define ONACTIVATE(addr,subaddr)
|
||||
#define ONACTIVATEL(linear)
|
||||
#define ONAMBER(signal_id)
|
||||
#define ONBLOCKENTER(blockid)
|
||||
#define ONBLOCKEXIT(blockid)
|
||||
#define ONTIME(value)
|
||||
#define ONCLOCKTIME(hours,mins)
|
||||
#define ONCLOCKMINS(mins)
|
||||
@@ -304,7 +321,7 @@
|
||||
#define READ_LOCO
|
||||
#define RED(signal_id)
|
||||
#define RESERVE(blockid)
|
||||
#define RESET(pin)
|
||||
#define RESET(pin,count...)
|
||||
#define RESUME
|
||||
#define RETURN
|
||||
#define REV(speed)
|
||||
@@ -330,7 +347,7 @@
|
||||
#define SERVO2(id,position,duration)
|
||||
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
|
||||
#define SET(pin)
|
||||
#define SET(pin,count...)
|
||||
#define SET_TRACK(track,mode)
|
||||
#define SET_POWER(track,onoff)
|
||||
#define SETLOCO(loco)
|
||||
@@ -360,5 +377,7 @@
|
||||
#define XFOFF(cab,func)
|
||||
#define XFON(cab,func)
|
||||
#define XFTOGGLE(cab,func)
|
||||
|
||||
#define XFWD(cab,speed)
|
||||
#define XREV(cab,speed)
|
||||
#define XPOM(cab,cv,value)
|
||||
#endif
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021-2023 Harald Barth
|
||||
* © 2020-2023 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -210,6 +210,15 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'K': // <K blockid loco> Block enter
|
||||
case 'k': // <k blockid loco> Block exit
|
||||
if (paramCount!=2) break;
|
||||
blockEvent(p[0],p[1],opcode=='K');
|
||||
opcode=0;
|
||||
break;
|
||||
|
||||
default: // other commands pass through
|
||||
break;
|
||||
}
|
||||
@@ -228,11 +237,9 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
);
|
||||
}
|
||||
else {
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d %c"),
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' ',
|
||||
task->speedo,
|
||||
task->forward?'F':'R'
|
||||
task->invert?'I':' '
|
||||
);
|
||||
}
|
||||
task=task->next;
|
||||
@@ -252,13 +259,13 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
// do the signals
|
||||
// flags[n] represents the state of the nth signal in the table
|
||||
for (int sigslot=0;;sigslot++) {
|
||||
int16_t sighandle=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
|
||||
if (sighandle==0) break; // end of signal list
|
||||
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this id
|
||||
SIGNAL_DEFINITION slot=getSignalSlot(sigslot);
|
||||
if (slot.type==sigtypeNoMoreSignals) break; // end of signal list
|
||||
if (slot.type==sigtypeContinuation) continue; // continueation of previous line
|
||||
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this ids
|
||||
StringFormatter::send(stream,F("\n%S[%d]"),
|
||||
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
|
||||
sigid);
|
||||
slot.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,19 +283,27 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
switch (p[0]) {
|
||||
case "PAUSE"_hk: // </ PAUSE>
|
||||
if (paramCount!=1) return false;
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
{ // pause all tasks
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
task->pause();
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
DCC::estopAll(); // pause all locos on the track
|
||||
pausingTask=(RMFT2 *)1; // Impossible task address
|
||||
return true;
|
||||
|
||||
case "RESUME"_hk: // </ RESUME>
|
||||
if (paramCount!=1) return false;
|
||||
pausingTask=NULL;
|
||||
{
|
||||
{ // resume all tasks
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->loco) task->driveLoco(task->speedo);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
task->resume();
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -301,8 +316,7 @@ 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;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
new RMFT2(pc,cab);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -363,4 +377,3 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2022 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2022-2023 Colin Murdoch
|
||||
* © 2023 Harald Barth
|
||||
* © 2025 Morten Nielsen
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
#ifndef EXRAILMacros_H
|
||||
#define EXRAILMacros_H
|
||||
#include "IODeviceList.h"
|
||||
|
||||
// remove normal code LCD & SERIAL macros (will be restored later)
|
||||
#undef LCD
|
||||
@@ -63,6 +65,10 @@
|
||||
// playing sounds with IO_I2CDFPlayer
|
||||
#define PLAYSOUND ANOUT
|
||||
|
||||
// SEG7 is a helper to create ANOUT from a 7-segment request
|
||||
#define SEG7(vpin,value,format) \
|
||||
ANOUT(vpin,(value & 0xFFFF),TM1638::DF_##format,((uint32_t)value)>>16)
|
||||
|
||||
// helper macro to strip leading zeros off time inputs
|
||||
// (10#mins)%100)
|
||||
#define STRIP_ZERO(value) 10##value%100
|
||||
@@ -71,6 +77,8 @@
|
||||
//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
|
||||
//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
|
||||
|
||||
// NEOPIXEL RG generator for NEOPIXEL_SIGNAL
|
||||
#define NeoRGB(red,green,blue) (((uint32_t)(red & 0xff)<<16) | ((uint32_t)(green & 0xff)<<8) | (uint32_t)(blue & 0xff))
|
||||
|
||||
// Pass 1 Implements aliases
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
@@ -180,6 +188,8 @@ bool exrailHalSetup() {
|
||||
#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL
|
||||
#undef DCCX_SIGNAL
|
||||
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL
|
||||
#undef NEOPIXEL_SIGNAL
|
||||
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour) | FEATURE_SIGNAL
|
||||
#undef VIRTUAL_SIGNAL
|
||||
#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL
|
||||
|
||||
@@ -222,6 +232,10 @@ bool exrailHalSetup() {
|
||||
#define ONBUTTON(vpin) | FEATURE_SENSOR
|
||||
#undef ONSENSOR
|
||||
#define ONSENSOR(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"
|
||||
@@ -421,21 +435,26 @@ const FSH * RMFT2::getRosterFunctions(int16_t id) {
|
||||
// Pass 8 Signal definitions
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef SIGNAL
|
||||
#define SIGNAL(redpin,amberpin,greenpin) redpin,redpin,amberpin,greenpin,
|
||||
#define SIGNAL(redpin,amberpin,greenpin) {sigtypeSIGNAL,redpin,redpin,amberpin,greenpin},
|
||||
#undef SIGNALH
|
||||
#define SIGNALH(redpin,amberpin,greenpin) redpin | RMFT2::ACTIVE_HIGH_SIGNAL_FLAG,redpin,amberpin,greenpin,
|
||||
#define SIGNALH(redpin,amberpin,greenpin) {sigtypeSIGNALH,redpin,redpin,amberpin,greenpin},
|
||||
#undef SERVO_SIGNAL
|
||||
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
|
||||
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) {sigtypeSERVO,vpin,redval,amberval,greenval},
|
||||
#undef DCC_SIGNAL
|
||||
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
|
||||
#define DCC_SIGNAL(id,addr,subaddr) {sigtypeDCC,id,addr,subaddr,0},
|
||||
#undef DCCX_SIGNAL
|
||||
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) id | RMFT2::DCCX_SIGNAL_FLAG,redAspect,amberAspect,greenAspect,
|
||||
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) {sigtypeDCCX,id,redAspect,amberAspect,greenAspect},
|
||||
#undef NEOPIXEL_SIGNAL
|
||||
#define NEOPIXEL_SIGNAL(id,redRGB,amberRGB,greenRGB) \
|
||||
{sigtypeNEOPIXEL,id,((VPIN)((redRGB)>>8)), ((VPIN)((amberRGB)>>8)), ((VPIN)((greenRGB)>>8))},\
|
||||
{sigtypeContinuation,id,((VPIN)((redRGB) & 0xff)), ((VPIN)((amberRGB) & 0xFF)), ((VPIN)((greenRGB) & 0xFF))},
|
||||
#undef VIRTUAL_SIGNAL
|
||||
#define VIRTUAL_SIGNAL(id) id,0,0,0,
|
||||
#define VIRTUAL_SIGNAL(id) {sigtypeVIRTUAL,id,0,0,0},
|
||||
|
||||
const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||
const HIGHFLASH SIGNAL_DEFINITION RMFT2::SignalDefinitions[] = {
|
||||
#include "myAutomation.h"
|
||||
0,0,0,0 };
|
||||
{sigtypeNoMoreSignals,0,0,0,0}
|
||||
};
|
||||
|
||||
// Pass 9 ONLCC/ ONMERG counter and lookup array
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
@@ -499,11 +518,12 @@ 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),
|
||||
#endif
|
||||
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
|
||||
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V((int16_t)PCA9685::ProfileType::UseDuration|(int16_t)PCA9685::ProfileType::NoPowerOff),OPCODE_PAD,V(ms/100L),
|
||||
#define FOFF(func) OPCODE_FOFF,V(func),
|
||||
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
|
||||
#define FON(func) OPCODE_FON,V(func),
|
||||
@@ -551,10 +571,19 @@ 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)),\
|
||||
OPCODE_PAD,V((b & 0xff)),\
|
||||
OPCODE_PAD,V(#count[0]?(count+0):1),
|
||||
|
||||
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour)
|
||||
#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),\
|
||||
@@ -588,7 +617,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
|
||||
#define RED(signal_id) OPCODE_RED,V(signal_id),
|
||||
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
|
||||
#define RESET(pin) OPCODE_RESET,V(pin),
|
||||
#define RESET(pin,count...) OPCODE_RESET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
|
||||
#define RESUME OPCODE_RESUME,0,0,
|
||||
#define RETURN OPCODE_RETURN,0,0,
|
||||
#define REV(speed) OPCODE_REV,V(speed),
|
||||
@@ -616,7 +645,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),
|
||||
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
|
||||
#define SET(pin) OPCODE_SET,V(pin),
|
||||
#define SET(pin,count...) OPCODE_SET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
|
||||
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
|
||||
#define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track),
|
||||
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
|
||||
@@ -646,6 +675,9 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
|
||||
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
|
||||
#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__;
|
||||
|
@@ -47,4 +47,4 @@ class EXRAILSensor {
|
||||
bool onChange;
|
||||
byte latchDelay;
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* © 2024 Morten "Doc" Nielsen
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2022 Bruno Sanches
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2024 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -29,76 +31,139 @@
|
||||
#include "CommandDistributor.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCCTimer.h"
|
||||
#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)
|
||||
|
||||
bool EthernetInterface::connected=false;
|
||||
EthernetServer * EthernetInterface::server= nullptr;
|
||||
EthernetClient EthernetInterface::clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
|
||||
bool EthernetInterface::inUse[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
|
||||
uint8_t EthernetInterface::buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
|
||||
RingStream * EthernetInterface::outboundRing = nullptr;
|
||||
|
||||
EthernetInterface * EthernetInterface::singleton=NULL;
|
||||
/**
|
||||
* @brief Setup Ethernet Connection
|
||||
*
|
||||
*/
|
||||
void EthernetInterface::setup()
|
||||
|
||||
void EthernetInterface::setup()
|
||||
{
|
||||
if (singleton!=NULL) {
|
||||
DIAG(F("Prog Error!"));
|
||||
return;
|
||||
}
|
||||
if ((singleton=new EthernetInterface()))
|
||||
return;
|
||||
DIAG(F("Ethernet not initialized"));
|
||||
};
|
||||
DIAG(F("Ethernet starting"
|
||||
#ifdef DO_MDNS
|
||||
" (with mDNS)"
|
||||
#endif
|
||||
" Please be patient, especially if no cable is connected!"
|
||||
));
|
||||
|
||||
#ifdef STM32_ETHERNET
|
||||
// Set a HOSTNAME for the DHCP request - a nice to have, but hard it seems on LWIP for STM32
|
||||
// The default is "lwip", which is **always** set in STM32Ethernet/src/utility/ethernetif.cpp
|
||||
// for some reason. One can edit it to instead read:
|
||||
// #if LWIP_NETIF_HOSTNAME
|
||||
// /* Initialize interface hostname */
|
||||
// if (netif->hostname == NULL)
|
||||
// netif->hostname = "lwip";
|
||||
// #endif /* LWIP_NETIF_HOSTNAME */
|
||||
// Which seems more useful! We should propose the patch... so the following line actually works!
|
||||
netif_set_hostname(&gnetif, WIFI_HOSTNAME); // Should probably be passed in the contructor...
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef IP_ADDRESS
|
||||
static IPAddress myIP(IP_ADDRESS);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Aquire IP Address from DHCP and start server
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
EthernetInterface::EthernetInterface()
|
||||
{
|
||||
byte mac[6];
|
||||
DCCTimer::getSimulatedMacAddress(mac);
|
||||
connected=false;
|
||||
|
||||
#ifdef IP_ADDRESS
|
||||
Ethernet.begin(mac, myIP);
|
||||
#else
|
||||
if (Ethernet.begin(mac) == 0)
|
||||
{
|
||||
DIAG(F("Ethernet.begin FAILED"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
|
||||
DIAG(F("Ethernet shield not found or W5100"));
|
||||
}
|
||||
|
||||
unsigned long startmilli = millis();
|
||||
while ((millis() - startmilli) < 5500) { // Loop to give time to check for cable connection
|
||||
if (Ethernet.linkStatus() == LinkON)
|
||||
break;
|
||||
DIAG(F("Ethernet waiting for link (1sec) "));
|
||||
delay(1000);
|
||||
}
|
||||
// now we either do have link of we have a W5100
|
||||
// where we do not know if we have link. That's
|
||||
// the reason to now run checkLink.
|
||||
// CheckLinks sets up outboundRing if it does
|
||||
// not exist yet as well.
|
||||
checkLink();
|
||||
#ifdef IP_ADDRESS
|
||||
static IPAddress myIP(IP_ADDRESS);
|
||||
Ethernet.begin(mac,myIP);
|
||||
#else
|
||||
if (Ethernet.begin(mac)==0)
|
||||
{
|
||||
LCD(4,F("IP: No DHCP"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
|
||||
if (!ip) {
|
||||
LCD(4,F("IP: None"));
|
||||
return;
|
||||
}
|
||||
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
|
||||
server->begin();
|
||||
|
||||
// Arrange display of IP address and port
|
||||
#ifdef LCD_DRIVER
|
||||
const byte lcdData[]={LCD_DRIVER};
|
||||
const bool wideDisplay=lcdData[1]>=24; // data[1] is cols.
|
||||
#else
|
||||
const bool wideDisplay=true;
|
||||
#endif
|
||||
if (wideDisplay) {
|
||||
// OLEDS or just usb diag is ok on one line.
|
||||
LCD(4,F("IP %d.%d.%d.%d:%d"), ip[0], ip[1], ip[2], ip[3], IP_PORT);
|
||||
}
|
||||
else { // LCDs generally too narrow, so take 2 lines
|
||||
LCD(4,F("IP %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
|
||||
LCD(5,F("Port %d"), IP_PORT);
|
||||
}
|
||||
|
||||
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
|
||||
#ifdef DO_MDNS
|
||||
mdns.begin(Ethernet.localIP(), WIFI_HOSTNAME); // hostname
|
||||
mdns.addServiceRecord(WIFI_HOSTNAME "._withrottle", IP_PORT, MDNSServiceTCP);
|
||||
// Not sure if we need to run it once, but just in case!
|
||||
mdns.run();
|
||||
#endif
|
||||
connected=true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cleanup any resources
|
||||
*
|
||||
* @return none
|
||||
*/
|
||||
EthernetInterface::~EthernetInterface() {
|
||||
delete server;
|
||||
delete outboundRing;
|
||||
#if defined (STM32_ETHERNET)
|
||||
void EthernetInterface::acceptClient() { // STM32 version
|
||||
auto client=server->available();
|
||||
if (!client) return;
|
||||
// check for existing client
|
||||
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
|
||||
if (inUse[socket] && client == clients[socket]) return;
|
||||
|
||||
// new client
|
||||
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
|
||||
{
|
||||
if (!inUse[socket])
|
||||
{
|
||||
clients[socket] = client;
|
||||
inUse[socket]=true;
|
||||
if (Diag::ETHERNET)
|
||||
DIAG(F("Ethernet: New client socket %d"), socket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("Ethernet OVERFLOW"));
|
||||
}
|
||||
#else
|
||||
void EthernetInterface::acceptClient() { // non-STM32 version
|
||||
auto client=server->accept();
|
||||
if (!client) return;
|
||||
auto socket=client.getSocketNumber();
|
||||
clients[socket]=client;
|
||||
inUse[socket]=true;
|
||||
if (Diag::ETHERNET)
|
||||
DIAG(F("Ethernet: New client socket %d"), socket);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EthernetInterface::dropClient(byte socket)
|
||||
{
|
||||
clients[socket].stop();
|
||||
inUse[socket]=false;
|
||||
CommandDistributor::forget(socket);
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet: Disconnect %d "), socket);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,134 +172,109 @@ EthernetInterface::~EthernetInterface() {
|
||||
*/
|
||||
void EthernetInterface::loop()
|
||||
{
|
||||
if (!singleton || (!singleton->checkLink()))
|
||||
return;
|
||||
if (!connected) return;
|
||||
looptimer(5000, F("E.loop"));
|
||||
|
||||
static bool warnedAboutLink=false;
|
||||
if (Ethernet.linkStatus() == LinkOFF){
|
||||
if (warnedAboutLink) return;
|
||||
DIAG(F("Ethernet link OFF"));
|
||||
warnedAboutLink=true;
|
||||
return;
|
||||
}
|
||||
looptimer(5000, F("E.loop warn"));
|
||||
|
||||
// link status must be ok here
|
||||
if (warnedAboutLink) {
|
||||
DIAG(F("Ethernet link RESTORED"));
|
||||
warnedAboutLink=false;
|
||||
}
|
||||
|
||||
#ifdef DO_MDNS
|
||||
// Always do this because we don't want traffic to intefere with being found!
|
||||
mdns.run();
|
||||
looptimer(5000, F("E.mdns"));
|
||||
|
||||
#endif
|
||||
|
||||
//
|
||||
switch (Ethernet.maintain()) {
|
||||
case 1:
|
||||
//renewed fail
|
||||
DIAG(F("Ethernet Error: renewed fail"));
|
||||
singleton=NULL;
|
||||
connected=false;
|
||||
return;
|
||||
case 3:
|
||||
//rebind fail
|
||||
DIAG(F("Ethernet Error: rebind fail"));
|
||||
singleton=NULL;
|
||||
connected=false;
|
||||
return;
|
||||
default:
|
||||
//nothing happened
|
||||
//DIAG(F("maintained"));
|
||||
break;
|
||||
}
|
||||
singleton->loop2();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks ethernet link cable status and detects when it connects / disconnects
|
||||
*
|
||||
* @return true when cable is connected, false otherwise
|
||||
*/
|
||||
bool EthernetInterface::checkLink() {
|
||||
if (Ethernet.linkStatus() != LinkOFF) { // check for not linkOFF instead of linkON as the W5100 does return LinkUnknown
|
||||
//if we are not connected yet, setup a new server
|
||||
if(!connected) {
|
||||
DIAG(F("Ethernet cable connected"));
|
||||
connected=true;
|
||||
#ifdef IP_ADDRESS
|
||||
Ethernet.setLocalIP(myIP); // for static IP, set it again
|
||||
#endif
|
||||
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
|
||||
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
|
||||
server->begin();
|
||||
LCD(4,F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
|
||||
LCD(5,F("Port:%d"), IP_PORT);
|
||||
// only create a outboundRing it none exists, this may happen if the cable
|
||||
// gets disconnected and connected again
|
||||
if(!outboundRing)
|
||||
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
|
||||
}
|
||||
return true;
|
||||
} else { // connected
|
||||
DIAG(F("Ethernet cable disconnected"));
|
||||
connected=false;
|
||||
//clean up any client
|
||||
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++) {
|
||||
if(clients[socket].connected())
|
||||
clients[socket].stop();
|
||||
}
|
||||
// tear down server
|
||||
delete server;
|
||||
server = nullptr;
|
||||
LCD(4,F("IP: None"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EthernetInterface::loop2() {
|
||||
if (!outboundRing) { // no idea to call loop2() if we can't handle outgoing data in it
|
||||
if (Diag::ETHERNET) DIAG(F("No outboundRing"));
|
||||
return;
|
||||
}
|
||||
looptimer(5000, F("E.maintain"));
|
||||
|
||||
// get client from the server
|
||||
EthernetClient client = server->accept();
|
||||
|
||||
// check for new client
|
||||
if (client)
|
||||
acceptClient();
|
||||
|
||||
// handle disconnected sockets because STM32 library doesnt
|
||||
// do the read==0 response.
|
||||
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
|
||||
{
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet: New client "));
|
||||
byte socket;
|
||||
for (socket = 0; socket < MAX_SOCK_NUM; socket++)
|
||||
{
|
||||
if (!clients[socket])
|
||||
{
|
||||
// On accept() the EthernetServer doesn't track the client anymore
|
||||
// so we store it in our client array
|
||||
if (Diag::ETHERNET) DIAG(F("Socket %d"),socket);
|
||||
clients[socket] = client;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (socket==MAX_SOCK_NUM) DIAG(F("new Ethernet OVERFLOW"));
|
||||
}
|
||||
if (inUse[socket] && !clients[socket].connected()) dropClient(socket);
|
||||
}
|
||||
|
||||
// check for incoming data from all possible clients
|
||||
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
|
||||
{
|
||||
if (clients[socket]) {
|
||||
|
||||
int available=clients[socket].available();
|
||||
if (available > 0) {
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet: available socket=%d,avail=%d"), socket, available);
|
||||
// read bytes from a client
|
||||
int count = clients[socket].read(buffer, MAX_ETH_BUFFER);
|
||||
buffer[count] = '\0'; // terminate the string properly
|
||||
if (Diag::ETHERNET) DIAG(F(",count=%d:%e"), socket,buffer);
|
||||
// execute with data going directly back
|
||||
CommandDistributor::parse(socket,buffer,outboundRing);
|
||||
return; // limit the amount of processing that takes place within 1 loop() cycle.
|
||||
}
|
||||
}
|
||||
if (!inUse[socket]) continue; // socket is not in use
|
||||
|
||||
// read any bytes from this client
|
||||
auto count = clients[socket].read(buffer, MAX_ETH_BUFFER);
|
||||
|
||||
if (count<0) continue; // -1 indicates nothing to read
|
||||
|
||||
if (count > 0) { // we have incoming data
|
||||
buffer[count] = '\0'; // terminate the string properly
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet s=%d, c=%d b=:%e"), socket, count, buffer);
|
||||
// execute with data going directly back
|
||||
CommandDistributor::parse(socket,buffer,outboundRing);
|
||||
//looptimer(5000, F("Ethloop2 parse"));
|
||||
return; // limit the amount of processing that takes place within 1 loop() cycle.
|
||||
}
|
||||
|
||||
// count=0 The client has disconnected
|
||||
dropClient(socket);
|
||||
}
|
||||
|
||||
// stop any clients which disconnect
|
||||
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
|
||||
if (clients[socket] && !clients[socket].connected()) {
|
||||
clients[socket].stop();
|
||||
CommandDistributor::forget(socket);
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WiThrottle::loop(outboundRing);
|
||||
|
||||
|
||||
// handle at most 1 outbound transmission
|
||||
int socketOut=outboundRing->read();
|
||||
auto socketOut=outboundRing->read();
|
||||
if (socketOut<0) return; // no outbound pending
|
||||
|
||||
if (socketOut >= MAX_SOCK_NUM) {
|
||||
DIAG(F("Ethernet outboundRing socket=%d error"), socketOut);
|
||||
} else if (socketOut >= 0) {
|
||||
int count=outboundRing->count();
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet reply socket=%d, count=:%d"), socketOut,count);
|
||||
for(;count>0;count--) clients[socketOut].write(outboundRing->read());
|
||||
clients[socketOut].flush(); //maybe
|
||||
// This is a catastrophic code failure and unrecoverable.
|
||||
DIAG(F("Ethernet outboundRing s=%d error"), socketOut);
|
||||
connected=false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto count=outboundRing->count();
|
||||
{
|
||||
char tmpbuf[count+1]; // one extra for '\0'
|
||||
for(int i=0;i<count;i++) {
|
||||
tmpbuf[i] = outboundRing->read();
|
||||
}
|
||||
tmpbuf[count]=0;
|
||||
if (inUse[socketOut]) {
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet reply s=%d, c=%d, b:%e"),
|
||||
socketOut,count,tmpbuf);
|
||||
clients[socketOut].write(tmpbuf,count);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2024 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -35,6 +37,15 @@
|
||||
#if defined (ARDUINO_TEENSY41)
|
||||
#include <NativeEthernet.h> //TEENSY Ethernet Treiber
|
||||
#include <NativeEthernetUdp.h>
|
||||
#define MAX_SOCK_NUM 4
|
||||
#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 8
|
||||
#else
|
||||
#include "Ethernet.h"
|
||||
#endif
|
||||
@@ -45,7 +56,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#define MAX_ETH_BUFFER 512
|
||||
#define MAX_ETH_BUFFER 128
|
||||
#define OUTBOUND_RING_SIZE 2048
|
||||
|
||||
class EthernetInterface {
|
||||
@@ -56,16 +67,15 @@ class EthernetInterface {
|
||||
static void loop();
|
||||
|
||||
private:
|
||||
static EthernetInterface * singleton;
|
||||
bool connected;
|
||||
EthernetInterface();
|
||||
~EthernetInterface();
|
||||
void loop2();
|
||||
bool checkLink();
|
||||
EthernetServer * server = NULL;
|
||||
EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
|
||||
uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
|
||||
RingStream * outboundRing = NULL;
|
||||
static bool connected;
|
||||
static EthernetServer * server;
|
||||
static EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
|
||||
static bool inUse[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
|
||||
static uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
|
||||
static RingStream * outboundRing;
|
||||
static void acceptClient();
|
||||
static void dropClient(byte socketnum);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
8
FSH.h
8
FSH.h
@@ -52,6 +52,7 @@ typedef __FlashStringHelper FSH;
|
||||
#define STRNCPY_P strncpy_P
|
||||
#define STRNCMP_P strncmp_P
|
||||
#define STRLEN_P strlen_P
|
||||
#define STRCHR_P strchr_P
|
||||
|
||||
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
|
||||
// AVR_MEGA memory deliberately placed at end of link may need _far functions
|
||||
@@ -60,6 +61,8 @@ typedef __FlashStringHelper FSH;
|
||||
#define GETFARPTR(data) pgm_get_far_address(data)
|
||||
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
|
||||
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
|
||||
#define COPYHIGHFLASH(target,base,offset,length) \
|
||||
memcpy_PF(target,GETFARPTR(base) + offset,length)
|
||||
#else
|
||||
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
|
||||
// as there is no progmem above 32kb anyway.
|
||||
@@ -68,6 +71,8 @@ typedef __FlashStringHelper FSH;
|
||||
#define GETFARPTR(data) ((uint32_t)(data))
|
||||
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
|
||||
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
|
||||
#define COPYHIGHFLASH(target,base,offset,length) \
|
||||
memcpy_P(target,(byte *)base + offset,length)
|
||||
#endif
|
||||
|
||||
#else
|
||||
@@ -87,10 +92,13 @@ typedef char FSH;
|
||||
#define GETFLASH(addr) (*(const byte *)(addr))
|
||||
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
|
||||
#define GETHIGHFLASHW(data,offset) (*(const uint16_t *)(GETFARPTR(data)+offset))
|
||||
#define COPYHIGHFLASH(target,base,offset,length) \
|
||||
memcpy(target,(byte *)&base + offset,length)
|
||||
#define STRCPY_P strcpy
|
||||
#define STRCMP_P strcmp
|
||||
#define STRNCPY_P strncpy
|
||||
#define STRNCMP_P strncmp
|
||||
#define STRLEN_P strlen
|
||||
#define STRCHR_P strchr
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -1 +1 @@
|
||||
#define GITHUB_SHA "devel-202409121220Z"
|
||||
#define GITHUB_SHA "devel-202501171827Z"
|
||||
|
@@ -46,27 +46,37 @@
|
||||
|
||||
// Helper function for listing device types
|
||||
static const FSH * guessI2CDeviceType(uint8_t address) {
|
||||
if (address == 0x1A)
|
||||
// 0x09-0x18 selectable, but for now handle the default
|
||||
return F("Piicodev 865/915MHz Transceiver");
|
||||
if (address == 0x1C)
|
||||
return F("QMC6310 Magnetometer");
|
||||
if (address >= 0x20 && address <= 0x26)
|
||||
return F("GPIO Expander");
|
||||
else if (address == 0x27)
|
||||
if (address == 0x27)
|
||||
return F("GPIO Expander or LCD Display");
|
||||
else if (address == 0x29)
|
||||
if (address == 0x29)
|
||||
return F("Time-of-flight sensor");
|
||||
else if (address >= 0x3c && address <= 0x3d)
|
||||
return F("OLED Display");
|
||||
else if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection
|
||||
if (address == 0x34)
|
||||
return F("TCA8418 keypad scanner");
|
||||
if (address >= 0x3c && address <= 0x3d)
|
||||
// 0x3c can also be an HMC883L magnetometer
|
||||
return F("OLED Display or HMC583L Magnetometer");
|
||||
if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection
|
||||
return F("SC16IS75x UART");
|
||||
else if (address >= 0x48 && address <= 0x4f)
|
||||
if (address >= 0x48 && address <= 0x4f)
|
||||
return F("Analogue Inputs or PWM");
|
||||
else if (address >= 0x40 && address <= 0x4f)
|
||||
if (address >= 0x40 && address <= 0x4f)
|
||||
return F("PWM");
|
||||
else if (address >= 0x50 && address <= 0x5f)
|
||||
if (address >= 0x50 && address <= 0x5f)
|
||||
return F("EEPROM");
|
||||
else if (address == 0x68)
|
||||
if (address >= 0x60 && address <= 0x68)
|
||||
return F("Adafruit NeoPixel Driver");
|
||||
if (address == 0x68)
|
||||
return F("Real-time clock");
|
||||
else if (address >= 0x70 && address <= 0x77)
|
||||
if (address >= 0x70 && address <= 0x77)
|
||||
return F("I2C Mux");
|
||||
else
|
||||
// Unknown type
|
||||
return F("?");
|
||||
}
|
||||
|
||||
|
@@ -384,4 +384,4 @@ void I2CManagerClass::handleInterrupt() {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2022-23 Paul M Antoine
|
||||
* © 2022-24 Paul M Antoine
|
||||
* © 2023, Neil McKechnie
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -38,8 +38,9 @@
|
||||
*****************************************************************************/
|
||||
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
|
||||
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \
|
||||
|| defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) \
|
||||
|| defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
|
||||
|| defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) \
|
||||
|| defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||
|
||||
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely all Nucleo-64
|
||||
// and Nucleo-144 variants
|
||||
I2C_TypeDef *s = I2C1;
|
||||
@@ -184,7 +185,7 @@ void I2CManagerClass::I2C_init()
|
||||
GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability
|
||||
GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode
|
||||
GPIOB->PUPDR &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all PUPDR bits for PB8 and PB9
|
||||
GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
|
||||
// GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
|
||||
// Alt Function High register routing pins PB8 and PB9 for I2C1:
|
||||
// Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8
|
||||
// Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9
|
||||
|
@@ -231,4 +231,4 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::loop() {}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
39
IODevice.cpp
39
IODevice.cpp
@@ -251,6 +251,26 @@ void IODevice::write(VPIN vpin, int value) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Write value to count virtual pin(s).
|
||||
// these may be within one driver or separated over several drivers
|
||||
void IODevice::writeRange(VPIN vpin, int value, int count) {
|
||||
|
||||
while(count) {
|
||||
auto dev = findDevice(vpin);
|
||||
if (dev) {
|
||||
auto vpinBefore=vpin;
|
||||
// write to driver, driver will return next vpin it cant handle
|
||||
vpin=dev->_writeRange(vpin, value,count);
|
||||
count-= vpin-vpinBefore; // decrement by number of vpins changed
|
||||
}
|
||||
else {
|
||||
// skip a vpin if no device handler
|
||||
vpin++;
|
||||
count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write analogue value to virtual pin(s). If multiple devices are allocated
|
||||
// the same pin then only the first one found will be used.
|
||||
//
|
||||
@@ -270,6 +290,24 @@ void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t para
|
||||
#endif
|
||||
}
|
||||
|
||||
//
|
||||
void IODevice::writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2,int count) {
|
||||
while(count) {
|
||||
auto dev = findDevice(vpin);
|
||||
if (dev) {
|
||||
auto vpinBefore=vpin;
|
||||
// write to driver, driver will return next vpin it cant handle
|
||||
vpin=dev->_writeAnalogueRange(vpin, value, param1, param2,count);
|
||||
count-= vpin-vpinBefore; // decrement by number of vpins changed
|
||||
}
|
||||
else {
|
||||
// skip a vpin if no device handler
|
||||
vpin++;
|
||||
count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isBusy, when called for a device pin is always a digital output or analogue output,
|
||||
// returns input feedback state of the pin, i.e. whether the pin is busy performing
|
||||
// an animation or fade over a period of time.
|
||||
@@ -589,4 +627,3 @@ bool ArduinoPins::fastReadDigital(uint8_t pin) {
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
|
31
IODevice.h
31
IODevice.h
@@ -38,6 +38,7 @@
|
||||
#include "FSH.h"
|
||||
#include "I2CManager.h"
|
||||
#include "inttypes.h"
|
||||
#include "TemplateForEnums.h"
|
||||
|
||||
typedef uint16_t VPIN;
|
||||
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
|
||||
@@ -128,9 +129,11 @@ public:
|
||||
|
||||
// write invokes the IODevice instance's _write method.
|
||||
static void write(VPIN vpin, int value);
|
||||
static void writeRange(VPIN vpin, int value,int count);
|
||||
|
||||
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
|
||||
static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);
|
||||
static void writeAnalogueRange(VPIN vpin, int value, uint8_t profile, uint16_t duration, int count);
|
||||
|
||||
// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing
|
||||
// the output over a period of time.
|
||||
@@ -177,11 +180,29 @@ public:
|
||||
virtual void _write(VPIN vpin, int value) {
|
||||
(void)vpin; (void)value;
|
||||
};
|
||||
|
||||
// Method to write new state (optionally implemented within device class)
|
||||
// This will, by default just write to one vpin and return whet to do next.
|
||||
// the real power comes where a single driver can update many vpins in one call.
|
||||
virtual VPIN _writeRange(VPIN vpin, int value, int count) {
|
||||
(void)count;
|
||||
_write(vpin,value);
|
||||
return vpin+1; // try next vpin
|
||||
};
|
||||
|
||||
// Method to write an 'analogue' value (optionally implemented within device class)
|
||||
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) {
|
||||
(void)vpin; (void)value; (void) param1; (void)param2;
|
||||
};
|
||||
|
||||
// Method to write an 'analogue' value to a VPIN range (optionally implemented within device class)
|
||||
// This will, by default just write to one vpin and return whet to do next.
|
||||
// the real power comes where a single driver can update many vpins in one call.
|
||||
virtual VPIN _writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2, int count) {
|
||||
(void) count;
|
||||
_writeAnalogue(vpin, value, param1, param2);
|
||||
return vpin+1;
|
||||
};
|
||||
|
||||
// Method to read digital pin state (optionally implemented within device class)
|
||||
virtual int _read(VPIN vpin) {
|
||||
@@ -539,14 +560,6 @@ protected:
|
||||
|
||||
};
|
||||
|
||||
#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 "IODeviceList.h"
|
||||
|
||||
#endif // iodevice_h
|
||||
|
38
IODeviceList.h
Normal file
38
IODeviceList.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* © 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_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"
|
||||
#include "IO_DS1307.h"
|
||||
#include "IO_I2CRailcom.h"
|
||||
|
@@ -166,4 +166,4 @@ private:
|
||||
uint8_t _nextState;
|
||||
};
|
||||
|
||||
#endif // io_analogueinputs_h
|
||||
#endif // io_analogueinputs_h
|
||||
|
@@ -65,4 +65,3 @@ void DCCAccessoryDecoder::_display() {
|
||||
DIAG(F("DCCAccessoryDecoder Configured on Vpins:%u-%u Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
|
||||
ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));
|
||||
}
|
||||
|
||||
|
143
IO_DS1307.cpp
Normal file
143
IO_DS1307.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* © 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
Normal file
54
IO_DS1307.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* © 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
|
424
IO_EXSensorCAM.h
Normal file
424
IO_EXSensorCAM.h
Normal file
@@ -0,0 +1,424 @@
|
||||
/* 2024/08/14
|
||||
* © 2024, Barry Daniel ESP32-CAM revision
|
||||
*
|
||||
* This file is part of EX-CommandStation
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
#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
|
||||
// v304 static oldb0; t(##[,%%];
|
||||
// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)
|
||||
// v302 SEND=StringFormatter::send, remove Sp(), add 'q', memcpy( .8) -> .7);
|
||||
// v301 improved 'f','p'&'q' code and driver version calc. Correct bsNo calc. for 'a'
|
||||
// v300 stripped & revised without expander functionality. Needs sensorCAM.h v300 AND CamParser.cpp
|
||||
// v222 uses '@'for EXIORDD read. handles <NB $> and <NN $ ##>
|
||||
// v216 includes 'j' command and uses CamParser rather than myFilter.h Incompatible with v203 senorCAM
|
||||
// v203 added pvtThreshold to 'i' output
|
||||
// v201 deleted code for compatibility with CAM pre v171. Needs CAM ver201 with o06 only
|
||||
// v200 rewrite reduces need for double reads of ESP32 slave CAM. Deleted ESP32CAP.
|
||||
// Inompatible with pre-v170 sensorCAM, unless set S06 to 0 and S07 to 1 (o06 & l07 say)
|
||||
/*
|
||||
* The IO_EXSensorCAM.h device driver can integrate with the sensorCAM device.
|
||||
* It is modelled on the IO_EXIOExpander.h device driver to include specific needs of the ESP32 sensorCAM
|
||||
* 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);
|
||||
*
|
||||
*/
|
||||
#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"
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
#include "CamParser.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for EX-SensorCAM.
|
||||
*/
|
||||
class EXSensorCAM : public IODevice {
|
||||
public:
|
||||
static void create(VPIN vpin, int nPins, I2CAddress i2cAddress) {
|
||||
if (checkNoOverlap(vpin, nPins, i2cAddress))
|
||||
new EXSensorCAM(vpin, nPins, i2cAddress);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private:
|
||||
// Constructor
|
||||
EXSensorCAM(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
|
||||
_firstVpin = firstVpin;
|
||||
// Number of pins cannot exceed 255 (1 byte) because of I2C message structure.
|
||||
if (nPins > 80) nPins = 80;
|
||||
_nPins = nPins;
|
||||
_I2CAddress = i2cAddress;
|
||||
addDevice(this);
|
||||
CamParser::addVpin(firstVpin);
|
||||
}
|
||||
//*************************
|
||||
void _begin() {
|
||||
uint8_t status;
|
||||
// Initialise EX-SensorCAM device
|
||||
I2CManager.begin();
|
||||
if (!I2CManager.exists(_I2CAddress)) {
|
||||
DIAG(F("EX-SensorCAM I2C:%s device not found"), _I2CAddress.toString());
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
return;
|
||||
}else {
|
||||
uint8_t commandBuffer[4]={EXIOINIT,(uint8_t)_nPins,(uint8_t)(_firstVpin & 0xFF),(uint8_t)(_firstVpin>>8)};
|
||||
status = I2CManager.read(_I2CAddress,_inputBuf,sizeof(_inputBuf),commandBuffer,sizeof(commandBuffer));
|
||||
//EXIOINIT needed to trigger and send firstVpin to CAM
|
||||
|
||||
if (status == I2C_STATUS_OK) {
|
||||
// Attempt to get version, non-blocking results in poor placement of response. Can be blocking here!
|
||||
commandBuffer[0] = '^'; //new version code
|
||||
|
||||
status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1);
|
||||
// for ESP32 CAM, read again for good immediate response version data
|
||||
status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1);
|
||||
|
||||
if (status == I2C_STATUS_OK) {
|
||||
_majorVer= _inputBuf[1]/10;
|
||||
_minorVer= _inputBuf[1]%10;
|
||||
_patchVer= _inputBuf[2];
|
||||
DIAG(F("EX-SensorCAM device found, I2C:%s, Version v%d.%d.%d"),
|
||||
_I2CAddress.toString(),_majorVer, _minorVer,_patchVer);
|
||||
}
|
||||
}
|
||||
if (status != I2C_STATUS_OK)
|
||||
reportError(status);
|
||||
}
|
||||
}
|
||||
//*************************
|
||||
// Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested.
|
||||
// Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can
|
||||
// be allocated from the stack to reduce RAM allocation.
|
||||
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override {
|
||||
(void)configType; (void)params; // unused
|
||||
if(_verPrint) DIAG(F("_configure() driver IO_EXSensorCAM v0.%d.%d vpin: %d "), driverVer/100,driverVer%100,vpin);
|
||||
_verPrint=false; //only give driver versions once
|
||||
if (paramCount != 1) return false;
|
||||
return true; //at least confirm that CAM is (always) configured (no vpin check!)
|
||||
}
|
||||
//*************************
|
||||
// Analogue input pin configuration, used to enable an EX-IOExpander device.
|
||||
int _configureAnalogIn(VPIN vpin) override {
|
||||
DIAG(F("_configureAnalogIn() IO_EXSensorCAM vpin %d"),vpin);
|
||||
return true; // NOTE: use of EXRAIL IFGTE() etc use "analog" reads.
|
||||
}
|
||||
//*************************
|
||||
// Main loop, collect both digital and "analog" pin states continuously (faster sensor/input reads)
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
// Request block is used for "analogue" (cmd. data) and digital reads from the sensorCAM, which
|
||||
// are performed on a cyclic basis. Writes are performed synchronously as and when requested.
|
||||
if (_readState != RDS_IDLE) { //expecting a return packet
|
||||
if (_i2crb.isBusy()) return; // If I2C operation still in progress, return
|
||||
uint8_t status = _i2crb.status;
|
||||
if (status == I2C_STATUS_OK) { // If device request ok, read input data
|
||||
//apparently the above checks do not guarantee a good packet! error rate about 1 pkt per 1000
|
||||
//there should be a packet in _CAMresponseBuff[32]
|
||||
if ((_CAMresponseBuff[0] & 0x60) >= 0x60) { //Buff[0] seems to have ascii cmd header (bit6 high) (o06)
|
||||
int error = processIncomingPkt( _CAMresponseBuff, _CAMresponseBuff[0]); // '~' 'i' 'm' 'n' 't' etc
|
||||
if (error>0) DIAG(F("CAM packet header(0x%x) not recognised"),_CAMresponseBuff[0]);
|
||||
}else{ // Header not valid - typically replaced by bank 0 data! To avoid any bad responses set S06 to 0
|
||||
// Versions of sensorCAM.h after v300 should return header for '@' of '`'(0x60) (not 0xE6)
|
||||
// followed by digitalInputStates sensor state array
|
||||
}
|
||||
}else reportError(status, false); // report i2c eror but don't go offline.
|
||||
_readState = RDS_IDLE;
|
||||
}
|
||||
|
||||
// If we're not doing anything now, check to see if a new state table transfer, or for 't' repeat, is due.
|
||||
if (_readState == RDS_IDLE) { //check if time for digitalRefresh
|
||||
if ( currentMicros - _lastDigitalRead > _digitalRefresh) {
|
||||
// Issue new read request for digital states.
|
||||
|
||||
_readCommandBuffer[0] = '@'; //start new read of digitalInputStates Table // non-blocking read
|
||||
I2CManager.read(_I2CAddress,_CAMresponseBuff, 32,_readCommandBuffer, 1, &_i2crb);
|
||||
_lastDigitalRead = currentMicros;
|
||||
_readState = RDS_DIGITAL;
|
||||
|
||||
}else{ //slip in a repeat <NT n> if pending
|
||||
if (currentMicros - _lasttStateRead > _tStateRefresh) // Delay for "analog" command repetitions
|
||||
if (_savedCmd[2]>1) { //repeat a 't' command
|
||||
for (int i=0;i<7;i++) _readCommandBuffer[i] =_savedCmd[i];
|
||||
int errors = ioESP32(_I2CAddress, _CAMresponseBuff, 32, _readCommandBuffer, 7);
|
||||
_lasttStateRead = currentMicros;
|
||||
_savedCmd[2] -= 1; //decrement repeats
|
||||
if (errors==0) return;
|
||||
DIAG(F("ioESP32 error %d header 0x%x"),errors,_CAMresponseBuff[0]);
|
||||
_readState = RDS_TSTATE; //this should stop further cmd requests until packet read (or timeout)
|
||||
}
|
||||
} //end repeat 't'
|
||||
}
|
||||
}
|
||||
//*************************
|
||||
// Obtain the bank of 8 sensors as an "analog" value
|
||||
// can be used to track the position through a sequential sensor bank
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
return _digitalInputStates[(vpin - _firstVpin) / 8];
|
||||
}
|
||||
//*************************
|
||||
// Obtain the correct digital sensor input value
|
||||
int _read(VPIN vpin) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
int pin = vpin - _firstVpin;
|
||||
return bitRead(_digitalInputStates[pin / 8], pin % 8);
|
||||
}
|
||||
//*************************
|
||||
// Write digital value.
|
||||
void _write(VPIN vpin, int value) override {
|
||||
DIAG(F("**_write() vpin %d = %d"),vpin,value);
|
||||
return ;
|
||||
}
|
||||
//*************************
|
||||
// i2cAddr of ESP32 CAM
|
||||
// rBuf buffer for return packet
|
||||
// inbytes number of bytes to request from CAM
|
||||
// outBuff holds outbytes to be sent to CAM
|
||||
int ioESP32(uint8_t i2cAddr,uint8_t *rBuf,int inbytes,uint8_t *outBuff,int outbytes) {
|
||||
uint8_t status = _i2crb.status;
|
||||
|
||||
while( _i2crb.status != I2C_STATUS_OK){status = _i2crb.status;} //wait until bus free
|
||||
|
||||
status = I2CManager.read(i2cAddr, rBuf, inbytes, outBuff, outbytes);
|
||||
|
||||
if (status != I2C_STATUS_OK){
|
||||
DIAG(F("EX-SensorCAM I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
|
||||
reportError(status); return status;
|
||||
}
|
||||
return 0; // 0 for no error != 0 for error number.
|
||||
}
|
||||
//*************************
|
||||
//function to interpret packet from sensorCAM.ino
|
||||
//i2cAddr to identify CAM# (if # >1)
|
||||
//rBuf contains packet of up to 32 bytes usually with (ascii) cmd header in rBuf[0]
|
||||
//sensorCmd command header byte from CAM (in rBuf[0]?)
|
||||
int processIncomingPkt(uint8_t *rBuf,uint8_t sensorCmd) {
|
||||
//static uint8_t oldb0; //for debug only
|
||||
int k;
|
||||
int b;
|
||||
char str[] = "11111111";
|
||||
// if (sensorCmd <= '~') DIAG(F("processIncomingPkt %c %d %d %d"),rBuf[0],rBuf[1],rBuf[2],rBuf[3]);
|
||||
switch (sensorCmd){
|
||||
case '`': //response to request for digitalInputStates[] table '@'=>'`'
|
||||
memcpy(_digitalInputStates, rBuf+1, digitalBytesNeeded);
|
||||
// if ( _digitalInputStates[0]!=oldb0) { oldb0=_digitalInputStates[0]; //debug
|
||||
// for (k=0;k<5;k++) {Serial.print(" ");Serial.print(_digitalInputStates[k],HEX);}
|
||||
// }
|
||||
break;
|
||||
|
||||
case EXIORDY: //some commands give back acknowledgement only
|
||||
break;
|
||||
|
||||
case CAMERR: //cmd format error code from CAM
|
||||
DIAG(F("CAM cmd error 0xFE 0x%x"),rBuf[1]);
|
||||
break;
|
||||
|
||||
case '~': //information from '^' version request <N v[er]>
|
||||
DIAG(F("EX-SensorCAM device found, I2C:%s,CAM Version v%d.%d.%d vpins %u-%u"),
|
||||
_I2CAddress.toString(), rBuf[1]/10, rBuf[1]%10, rBuf[2],(int) _firstVpin, (int) _firstVpin +_nPins-1);
|
||||
DIAG(F("IO_EXSensorCAM driver v0.%d.%d vpin: %d "), driverVer/100,driverVer%100,_firstVpin);
|
||||
break;
|
||||
|
||||
case 'f':
|
||||
DIAG(F("(f %%%%) frame header 'f' for bsNo %d/%d - showing Quarter sample (1 row) only"), rBuf[1]/8,rBuf[1]%8);
|
||||
SEND(&USB_SERIAL,F("<n row: %d Ref bytes: "),rBuf[2]);
|
||||
for(k=3;k<15;k++)
|
||||
SEND(&USB_SERIAL,F("%x%x%s"), rBuf[k]>>4, rBuf[k]&15, k%3==2 ? " " : " ");
|
||||
Serial.print(" latest grab: ");
|
||||
for(k=16;k<28;k++)
|
||||
SEND(&USB_SERIAL,F("%x%x%s"), rBuf[k]>>4, rBuf[k]&15, (k%3==0) ? " " : " ");
|
||||
Serial.print(" n>\n");
|
||||
break;
|
||||
|
||||
case 'i': //information from i%%
|
||||
k=256*rBuf[5]+rBuf[4];
|
||||
DIAG(F("(i%%%%[,$$]) Info: Sensor 0%o(%d) enabled:%d status:%d row=%d x=%d Twin=0%o pvtThreshold=%d A~%d")
|
||||
,rBuf[1],rBuf[1],rBuf[3],rBuf[2],rBuf[6],k,rBuf[7],rBuf[9],int(rBuf[8])*16);
|
||||
break;
|
||||
|
||||
case 'm':
|
||||
DIAG(F("(m$[,##]) Min/max: $ frames min2flip (trip) %d, maxSensors 0%o, minSensors 0%o, nLED %d,"
|
||||
" threshold %d, TWOIMAGE_MAXBS 0%o"),rBuf[1],rBuf[3],rBuf[2],rBuf[4],rBuf[5],rBuf[6]);
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
DIAG(F("(n$[,##]) Nominate: $ nLED %d, ## minSensors 0%o (maxSensors 0%o threshold %d)")
|
||||
,rBuf[4],rBuf[2],rBuf[3],rBuf[5]);
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
b=rBuf[1]-2;
|
||||
if(b<4) { Serial.print("<n (p%%) Bank empty n>\n"); break; }
|
||||
SEND(&USB_SERIAL,F("<n (p%%) Bank: %d "),(0x7F&rBuf[2])/8);
|
||||
for (int j=2; j<b; j+=3)
|
||||
SEND(&USB_SERIAL,F(" S[%d%d]: r=%d x=%d"),0x7F&rBuf[j]/8,0x7F&rBuf[j]%8,rBuf[j+1],rBuf[j+2]+2*(rBuf[j]&0x80));
|
||||
Serial.print(" n>\n");
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
for (int i =0; i<8; i++) str[i] = ((rBuf[2] << i) & 0x80 ? '1' : '0');
|
||||
DIAG(F("(q $) Query bank %c ENABLED sensors(S%c7-%c0): %s "), rBuf[1], rBuf[1], rBuf[1], str);
|
||||
break;
|
||||
|
||||
case 't': //threshold etc. from t## //bad pkt if 't' FF's
|
||||
if(rBuf[1]==0xFF) {Serial.println("<n bad CAM 't' packet: 74 FF n>");_savedCmd[2] +=1; return 0;}
|
||||
SEND(&USB_SERIAL,F("<n (t[##[,%%%%]]) Threshold:%d sensor S00:-%d"),rBuf[1],min(rBuf[2]&0x7F,99));
|
||||
if(rBuf[2]>127) Serial.print("##* ");
|
||||
else{
|
||||
if(rBuf[2]>rBuf[1]) Serial.print("-?* ");
|
||||
else Serial.print("--* ");
|
||||
}
|
||||
for(int i=3;i<31;i+=2){
|
||||
uint8_t valu=rBuf[i]; //get bsn
|
||||
if(valu==80) break; //80 = end flag
|
||||
else{
|
||||
SEND(&USB_SERIAL,F("%d%d:"), (valu&0x7F)/8,(valu&0x7F)%8);
|
||||
if(valu>=128) Serial.print("?-");
|
||||
else {if(rBuf[i+1]>=128) Serial.print("oo");else Serial.print("--");}
|
||||
valu=rBuf[i+1];
|
||||
SEND(&USB_SERIAL,F("%d%s"),min(valu&0x7F,99),(valu<128) ? "--* ":"##* ");
|
||||
}
|
||||
}
|
||||
Serial.print(" >\n");
|
||||
break;
|
||||
|
||||
default: //header not a recognised cmd character
|
||||
DIAG(F("CAM packet header not valid (0x%x) (0x%x) (0x%x)"),rBuf[0],rBuf[1],rBuf[2]);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
//*************************
|
||||
// Write (analogue) 8bit (command) values. Write the parameters to the sensorCAM
|
||||
void _writeAnalogue(VPIN vpin, int param1, uint8_t camop, uint16_t param3) override {
|
||||
uint8_t outputBuffer[7];
|
||||
int errors=0;
|
||||
outputBuffer[0] = camop;
|
||||
int pin = vpin - _firstVpin;
|
||||
|
||||
if(camop >= 0x80) { //case "a" (4p) also (3p) e.g. <N 713 210 310>
|
||||
camop=param1; //put row (0-236) in expected place
|
||||
param1=param3; //put column in expected place
|
||||
outputBuffer[0] = 'A';
|
||||
pin = (pin/8)*10 + pin%8; //restore bsNo. as integer
|
||||
}
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
|
||||
outputBuffer[1] = pin; //vpin => bsn
|
||||
outputBuffer[2] = param1 & 0xFF;
|
||||
outputBuffer[3] = param1 >> 8;
|
||||
outputBuffer[4] = camop; //command code
|
||||
outputBuffer[5] = param3 & 0xFF;
|
||||
outputBuffer[6] = param3 >> 8;
|
||||
|
||||
int count=param1+1;
|
||||
if(camop=='Q'){
|
||||
if(param3<=10) {count=param3; camop='B';}
|
||||
//if(param1<10) outputBuffer[2] = param1*10;
|
||||
}
|
||||
if(camop=='B'){ //then 'b'(b%) cmd - can totally deal with that here. (but can't do b%,# (brightSF))
|
||||
if(param1>97) return;
|
||||
if(param1>9) param1 = param1/10; //accept a bsNo
|
||||
for(int bnk=param1;bnk<count;bnk++) {
|
||||
uint8_t b=_digitalInputStates[bnk];
|
||||
char str[] = "11111111";
|
||||
for (int i=0;i<8;i++) if(((b<<i)&0x80) == 0) str[i]='0';
|
||||
DIAG(F("(b $) Bank: %d activated byte: 0x%x%x (sensors S%d7->%d0) %s"), bnk,b>>4,b&15,bnk,bnk,str );
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (outputBuffer[4]=='T') { //then 't' cmd
|
||||
if(param1<31) { //repeated calls if param < 31
|
||||
//for (int i=0;i<7;i++) _savedCmd[i]=outputBuffer[i];
|
||||
memcpy( _savedCmd, outputBuffer, 7);
|
||||
}else _savedCmd[2] = 0; //no repeats if ##>30
|
||||
}else _savedCmd[2] = 0; //no repeats unless 't'
|
||||
|
||||
_lasttStateRead = micros(); //don't repeat until _tStateRefresh mSec
|
||||
|
||||
errors = ioESP32(_I2CAddress, _CAMresponseBuff, 32 , outputBuffer, 7); //send to esp32-CAM
|
||||
if (errors==0) return;
|
||||
else { // if (_CAMresponseBuff[0] != EXIORDY) //can't be sure what is inBuff[0] !
|
||||
DIAG(F("ioESP32 i2c error %d header 0x%x"),errors,_CAMresponseBuff[0]);
|
||||
}
|
||||
}
|
||||
//*************************
|
||||
// Display device information and status.
|
||||
void _display() override {
|
||||
DIAG(F("EX-SensorCAM I2C:%s v%d.%d.%d Vpins %u-%u %S"),
|
||||
_I2CAddress.toString(), _majorVer, _minorVer, _patchVer,
|
||||
(int)_firstVpin, (int)_firstVpin+_nPins-1,
|
||||
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
|
||||
}
|
||||
//*************************
|
||||
// Helper function for error handling
|
||||
void reportError(uint8_t status, bool fail=true) {
|
||||
DIAG(F("EX-SensorCAM I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
|
||||
status, I2CManager.getErrorMessage(status));
|
||||
if (fail) _deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
//*************************
|
||||
uint8_t _numDigitalPins = 80;
|
||||
size_t digitalBytesNeeded=10;
|
||||
uint8_t _CAMresponseBuff[34];
|
||||
|
||||
uint8_t _majorVer = 0;
|
||||
uint8_t _minorVer = 0;
|
||||
uint8_t _patchVer = 0;
|
||||
|
||||
uint8_t _digitalInputStates[10];
|
||||
I2CRB _i2crb;
|
||||
uint8_t _inputBuf[12];
|
||||
byte _outputBuffer[8];
|
||||
|
||||
bool _verPrint=true;
|
||||
|
||||
uint8_t _readCommandBuffer[8];
|
||||
uint8_t _savedCmd[8]; //for repeat 't' command
|
||||
//uint8_t _digitalPinBytes = 10; // Size of allocated memory buffer (may be longer than needed)
|
||||
|
||||
enum {RDS_IDLE, RDS_DIGITAL, RDS_TSTATE}; // Read operation states
|
||||
uint8_t _readState = RDS_IDLE;
|
||||
//uint8_t cmdBuffer[7]={0,0,0,0,0,0,0};
|
||||
unsigned long _lastDigitalRead = 0;
|
||||
unsigned long _lasttStateRead = 0;
|
||||
unsigned long _digitalRefresh = DIGITALREFRESH; // Delay refreshing digital inputs for 10ms
|
||||
const unsigned long _tStateRefresh = 120000UL; // Delay refreshing repeat "tState" inputs
|
||||
|
||||
enum {
|
||||
EXIOINIT = 0xE0, // Flag to initialise setup procedure
|
||||
EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup
|
||||
CAMERR = 0xFE
|
||||
};
|
||||
};
|
||||
#endif
|
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
|
||||
@@ -141,4 +142,3 @@ const byte _DIR_MASK = 0x30;
|
||||
void EncoderThrottle::_display() {
|
||||
DIAG(F("DRIVE vpin %d loco %d notch %d"),_firstVpin,_locoid,_notch);
|
||||
}
|
||||
|
||||
|
@@ -162,4 +162,4 @@ protected:
|
||||
|
||||
};
|
||||
|
||||
#endif // IO_EXAMPLESERIAL_H
|
||||
#endif // IO_EXAMPLESERIAL_H
|
||||
|
@@ -262,4 +262,4 @@ public:
|
||||
|
||||
};
|
||||
|
||||
#endif // IO_HALDisplay_H
|
||||
#endif // IO_HALDisplay_H
|
||||
|
@@ -511,6 +511,7 @@ 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 {
|
||||
@@ -549,8 +550,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 ( int i = 1; i < sizeof(out)+1 ; i++){
|
||||
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
|
||||
for ( uint8_t i = 1; i < sizeof(out)+1 ; i++){
|
||||
_outbuffer[i] = out[i-1];
|
||||
}
|
||||
|
||||
@@ -616,6 +617,14 @@ 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
|
||||
|
121
IO_I2CRailcom.cpp
Normal file
121
IO_I2CRailcom.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* © 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(""));
|
||||
}
|
||||
|
||||
|
58
IO_I2CRailcom.h
Normal file
58
IO_I2CRailcom.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* © 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
|
@@ -98,4 +98,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -108,4 +108,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
334
IO_NeoPixel.h
Normal file
334
IO_NeoPixel.h
Normal file
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* © 2024, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of EX-CommandStation
|
||||
*
|
||||
* 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_NEOPIXEL.h device driver integrates with one or more Adafruit neopixel drivers.
|
||||
* This device driver will configure the device on startup, along with
|
||||
* interacting with the device for all input/output duties.
|
||||
*
|
||||
* To create NEOPIXEL devices, these are defined in myAutomation.h:
|
||||
* (Note the device driver is included by default)
|
||||
*
|
||||
* HAL(NEOPIXEL,first vpin, number of pixels,mode, i2c address)
|
||||
* e.g. HAL(NEOPIXEL,1000,64,NEO_RGB,0x60)
|
||||
* This gives each pixel in the chain an individual vpin
|
||||
* The number of pixels must match the physical pixels in the chain.
|
||||
*
|
||||
* This driver maintains a colour (rgb value in 5,5,5 bits only) plus an ON bit.
|
||||
* This can be written/read with an analog write/read call.
|
||||
* The ON bit can be set on and off with a digital write. This allows for
|
||||
* a pixel to be preset a colour and then turned on and off like any other light.
|
||||
*/
|
||||
|
||||
#ifndef IO_EX_NeoPixel_H
|
||||
#define IO_EX_NeoPixel_H
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
|
||||
|
||||
// The following macros to define the Neopixel String type
|
||||
// have been copied from the Adafruit Seesaw Library under the
|
||||
// terms of the GPL.
|
||||
// Credit to: https://github.com/adafruit/Adafruit_Seesaw
|
||||
|
||||
// The order of primary colors in the NeoPixel data stream can vary
|
||||
// among device types, manufacturers and even different revisions of
|
||||
// the same item. The third parameter to the seesaw_NeoPixel
|
||||
// constructor encodes the per-pixel byte offsets of the red, green
|
||||
// and blue primaries (plus white, if present) in the data stream --
|
||||
// the following #defines provide an easier-to-use named version for
|
||||
// each permutation. e.g. NEO_GRB indicates a NeoPixel-compatible
|
||||
// device expecting three bytes per pixel, with the first byte
|
||||
// containing the green value, second containing red and third
|
||||
// containing blue. The in-memory representation of a chain of
|
||||
// NeoPixels is the same as the data-stream order; no re-ordering of
|
||||
// bytes is required when issuing data to the chain.
|
||||
|
||||
// Bits 5,4 of this value are the offset (0-3) from the first byte of
|
||||
// a pixel to the location of the red color byte. Bits 3,2 are the
|
||||
// green offset and 1,0 are the blue offset. If it is an RGBW-type
|
||||
// device (supporting a white primary in addition to R,G,B), bits 7,6
|
||||
// are the offset to the white byte...otherwise, bits 7,6 are set to
|
||||
// the same value as 5,4 (red) to indicate an RGB (not RGBW) device.
|
||||
// i.e. binary representation:
|
||||
// 0bWWRRGGBB for RGBW devices
|
||||
// 0bRRRRGGBB for RGB
|
||||
|
||||
// RGB NeoPixel permutations; white and red offsets are always same
|
||||
// Offset: W R G B
|
||||
#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2))
|
||||
#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1))
|
||||
#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2))
|
||||
#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1))
|
||||
#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0))
|
||||
#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0))
|
||||
|
||||
// RGBW NeoPixel permutations; all 4 offsets are distinct
|
||||
// Offset: W R G B
|
||||
#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3))
|
||||
#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2))
|
||||
#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3))
|
||||
#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2))
|
||||
#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1))
|
||||
#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1))
|
||||
|
||||
#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3))
|
||||
#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2))
|
||||
#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3))
|
||||
#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2))
|
||||
#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1))
|
||||
#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1))
|
||||
|
||||
#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3))
|
||||
#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2))
|
||||
#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3))
|
||||
#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2))
|
||||
#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1))
|
||||
#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1))
|
||||
|
||||
#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0))
|
||||
#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0))
|
||||
#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0))
|
||||
#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0))
|
||||
#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0))
|
||||
#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0))
|
||||
|
||||
// If 400 KHz support is enabled, the third parameter to the constructor
|
||||
// requires a 16-bit value (in order to select 400 vs 800 KHz speed).
|
||||
// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value
|
||||
// is sufficient to encode pixel color order, saving some space.
|
||||
|
||||
#define NEO_KHZ800 0x0000 // 800 KHz datastream
|
||||
#define NEO_KHZ400 0x0100 // 400 KHz datastream
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for NeoPixel.
|
||||
*/
|
||||
|
||||
class NeoPixel : public IODevice {
|
||||
public:
|
||||
|
||||
static void create(VPIN vpin, int nPins, uint16_t mode=(NEO_GRB | NEO_KHZ800), I2CAddress i2cAddress=0x60) {
|
||||
if (checkNoOverlap(vpin, nPins, i2cAddress)) new NeoPixel(vpin, nPins, mode, i2cAddress);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static const byte SEESAW_NEOPIXEL_BASE=0x0E;
|
||||
static const byte SEESAW_NEOPIXEL_STATUS = 0x00;
|
||||
static const byte SEESAW_NEOPIXEL_PIN = 0x01;
|
||||
static const byte SEESAW_NEOPIXEL_SPEED = 0x02;
|
||||
static const byte SEESAW_NEOPIXEL_BUF_LENGTH = 0x03;
|
||||
static const byte SEESAW_NEOPIXEL_BUF=0x04;
|
||||
static const byte SEESAW_NEOPIXEL_SHOW=0x05;
|
||||
|
||||
// all adafruit examples say this pin. Presumably its hard wired
|
||||
// in the adapter anyway.
|
||||
static const byte SEESAW_PIN15 = 15;
|
||||
|
||||
// Constructor
|
||||
NeoPixel(VPIN firstVpin, int nPins, uint16_t mode, I2CAddress i2cAddress) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins=nPins;
|
||||
_I2CAddress = i2cAddress;
|
||||
|
||||
// calculate the offsets into the seesaw buffer for each colour depending
|
||||
// on the pixel strip type passed in mode.
|
||||
|
||||
_redOffset=4+(mode >> 4 & 0x03);
|
||||
_greenOffset=4+(mode >> 2 & 0x03);
|
||||
_blueOffset=4+(mode & 0x03);
|
||||
if (4+(mode >>6 & 0x03) == _redOffset) _bytesPerPixel=3;
|
||||
else _bytesPerPixel=4; // string has a white byte.
|
||||
|
||||
_kHz800=(mode & NEO_KHZ400)==0;
|
||||
_showPendimg=false;
|
||||
|
||||
// Each pixel requires 3 bytes RGB memory.
|
||||
// Although the driver device can remember this, it cant do off/on without
|
||||
// forgetting what the on colour was!
|
||||
pixelBuffer=(RGB *) malloc(_nPins*sizeof(RGB));
|
||||
stateBuffer=(byte *) calloc((_nPins+7)/8,sizeof(byte)); // all pixels off
|
||||
if (pixelBuffer==nullptr || stateBuffer==nullptr) {
|
||||
DIAG(F("NeoPixel I2C:%s not enough RAM"), _I2CAddress.toString());
|
||||
return;
|
||||
}
|
||||
// preset all pins to white so a digital on/off will do something even if no colour set.
|
||||
memset(pixelBuffer,0xFF,_nPins*sizeof(RGB));
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
void _begin() {
|
||||
|
||||
// Initialise Neopixel device
|
||||
I2CManager.begin();
|
||||
if (!I2CManager.exists(_I2CAddress)) {
|
||||
DIAG(F("NeoPixel I2C:%s device not found"), _I2CAddress.toString());
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
byte speedBuffer[]={SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED,_kHz800};
|
||||
I2CManager.write(_I2CAddress, speedBuffer, sizeof(speedBuffer));
|
||||
|
||||
// In the driver there are 3 of 4 byts per pixel
|
||||
auto numBytes=_bytesPerPixel * _nPins;
|
||||
byte setbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH,
|
||||
(byte)(numBytes >> 8), (byte)(numBytes & 0xFF)};
|
||||
I2CManager.write(_I2CAddress, setbuffer, sizeof(setbuffer));
|
||||
|
||||
const byte pinbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN,SEESAW_PIN15};
|
||||
I2CManager.write(_I2CAddress, pinbuffer, sizeof(pinbuffer));
|
||||
|
||||
for (auto pin=0;pin<_nPins;pin++) transmit(pin);
|
||||
_display();
|
||||
}
|
||||
|
||||
// loop called by HAL supervisor
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
(void)currentMicros;
|
||||
if (!_showPendimg) return;
|
||||
byte showBuffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_SHOW};
|
||||
I2CManager.write(_I2CAddress,showBuffer,sizeof(showBuffer));
|
||||
_showPendimg=false;
|
||||
}
|
||||
|
||||
|
||||
// read back pixel on/off
|
||||
int _read(VPIN vpin) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
return isPixelOn(vpin-_firstVpin);
|
||||
}
|
||||
|
||||
// Write digital value. Sets pixel on or off
|
||||
void _write(VPIN vpin, int value) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
auto pixel=vpin-_firstVpin;
|
||||
if (value) {
|
||||
if (isPixelOn(pixel)) return;
|
||||
setPixelOn(pixel);
|
||||
}
|
||||
else { // set off
|
||||
if (!isPixelOn(pixel)) return;
|
||||
setPixelOff(pixel);
|
||||
}
|
||||
transmit(pixel);
|
||||
}
|
||||
|
||||
VPIN _writeRange(VPIN vpin,int value, int count) {
|
||||
// using write range cuts out the constant vpin to driver lookup so
|
||||
// we can update multiple pixels much faster.
|
||||
VPIN nextVpin=vpin + (count>_nPins ? _nPins : count);
|
||||
if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {
|
||||
_write(vpin,value);
|
||||
vpin++;
|
||||
}
|
||||
return nextVpin; // next pin we cant
|
||||
}
|
||||
// Write analogue value.
|
||||
// The convoluted parameter mashing here is to allow passing the RGB and on/off
|
||||
// information through the generic HAL _writeAnalog interface which was originally
|
||||
// designed for servos and short integers
|
||||
void _writeAnalogue(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
RGB newColour={(byte)((colour_RG>>8) & 0xFF), (byte)(colour_RG & 0xFF), (byte)(colour_B & 0xFF)};
|
||||
auto pixel=vpin-_firstVpin;
|
||||
if (pixelBuffer[pixel]==newColour && isPixelOn(pixel)==(bool)onoff) return; // no change
|
||||
|
||||
if (onoff) setPixelOn(pixel); else setPixelOff(pixel);
|
||||
pixelBuffer[pixel]=newColour;
|
||||
transmit(pixel);
|
||||
}
|
||||
VPIN _writeAnalogueRange(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B, int count) override {
|
||||
// using write range cuts out the constant vpin to driver lookup so
|
||||
VPIN nextVpin=vpin + (count>_nPins ? _nPins : count);
|
||||
if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {
|
||||
_writeAnalogue(vpin,colour_RG, onoff,colour_B);
|
||||
vpin++;
|
||||
}
|
||||
return nextVpin; // next pin we cant
|
||||
}
|
||||
|
||||
// Display device information and status.
|
||||
void _display() override {
|
||||
DIAG(F("NeoPixel I2C:%s Vpins %u-%u %S"),
|
||||
_I2CAddress.toString(),
|
||||
(int)_firstVpin, (int)_firstVpin+_nPins-1,
|
||||
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool isPixelOn(int16_t pixel) {return stateBuffer[pixel/8] & (0x80>>(pixel%8));}
|
||||
void setPixelOn(int16_t pixel) {stateBuffer[pixel/8] |= (0x80>>(pixel%8));}
|
||||
void setPixelOff(int16_t pixel) {stateBuffer[pixel/8] &= ~(0x80>>(pixel%8));}
|
||||
|
||||
// Helper function for error handling
|
||||
void reportError(uint8_t status, bool fail=true) {
|
||||
DIAG(F("NeoPixel I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
|
||||
status, I2CManager.getErrorMessage(status));
|
||||
if (fail)
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
|
||||
|
||||
void transmit(uint16_t pixel) {
|
||||
byte buffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_BUF,0x00,0x00,0x00,0x00,0x00};
|
||||
uint16_t offset= pixel * _bytesPerPixel;
|
||||
buffer[2]=(byte)(offset>>8);
|
||||
buffer[3]=(byte)(offset & 0xFF);
|
||||
|
||||
if (isPixelOn(pixel)) {
|
||||
auto colour=pixelBuffer[pixel];
|
||||
buffer[_redOffset]=colour.red;
|
||||
buffer[_greenOffset]=colour.green;
|
||||
buffer[_blueOffset]=colour.blue;
|
||||
} // else leave buffer black (in buffer preset to zeros above)
|
||||
|
||||
// Transmit pixel to driver
|
||||
I2CManager.write(_I2CAddress,buffer,4 +_bytesPerPixel);
|
||||
_showPendimg=true;
|
||||
|
||||
}
|
||||
struct RGB {
|
||||
byte red;
|
||||
byte green;
|
||||
byte blue;
|
||||
bool operator==(const RGB& other) const {
|
||||
return red == other.red && green == other.green && blue == other.blue;
|
||||
}
|
||||
};
|
||||
|
||||
RGB* pixelBuffer = nullptr;
|
||||
byte* stateBuffer = nullptr; // 1 bit per pixel
|
||||
bool _showPendimg;
|
||||
|
||||
// mapping of RGB onto pixel buffer for seesaw.
|
||||
byte _bytesPerPixel;
|
||||
byte _redOffset;
|
||||
byte _greenOffset;
|
||||
byte _blueOffset;
|
||||
bool _kHz800;
|
||||
};
|
||||
|
||||
#endif
|
@@ -167,4 +167,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -101,4 +101,4 @@ private:
|
||||
uint8_t inputBuffer[1];
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -106,4 +106,4 @@ private:
|
||||
uint8_t inputBuffer[2];
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -30,4 +30,3 @@
|
||||
//
|
||||
const uint8_t FLASH Servo::_bounceProfile[30] =
|
||||
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};
|
||||
|
||||
|
@@ -295,4 +295,4 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
371
IO_TCA8418.h
Normal file
371
IO_TCA8418.h
Normal file
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* © 2023-2024, 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_tca8418_h
|
||||
#define io_tca8418_h
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for TCA8418 80-key keypad encoder, which we'll treat as 80 available VPINs where
|
||||
* key down == 1 and key up == 0 by configuring just as an 8x10 keyboard matrix. Users can opt to use
|
||||
* up to all 80 of the available VPINs for now, allowing memory to be saved if not all events are required.
|
||||
*
|
||||
* The datasheet says:
|
||||
*
|
||||
* The TCA8418 can be configured to support many different configurations of keypad setups.
|
||||
* All 18 GPIOs for the rows and columns can be used to support up to 80 keys in an 8x10 key pad
|
||||
* array. Another option is that all 18 GPIOs be used for GPIs to read 18 buttons which are
|
||||
* not connected in an array. Any combination in between is also acceptable (for example, a
|
||||
* 3x4 keypad matrix and using the remaining 11 GPIOs as a combination of inputs and outputs).
|
||||
*
|
||||
* With an 8x10 key event matrix, the events are numbered as such:
|
||||
*
|
||||
* C0 C1 C2 C3 C4 C5 C6 C7 C8 C9
|
||||
* ========================================
|
||||
* R0| 0 1 2 3 4 5 6 7 8 9
|
||||
* R1| 10 11 12 13 14 15 16 17 18 19
|
||||
* R2| 20 21 22 23 24 25 26 27 28 29
|
||||
* R3| 30 31 32 33 34 35 36 37 38 39
|
||||
* R4| 40 41 42 43 44 45 46 47 48 49
|
||||
* R5| 50 51 52 53 54 55 56 57 58 59
|
||||
* R6| 60 61 62 63 64 65 66 67 68 69
|
||||
* R7| 70 71 72 73 74 75 76 77 78 79
|
||||
*
|
||||
* So if you start with VPIN 300, R0/C0 will be 300, and R7/C9 will be 379.
|
||||
*
|
||||
* HAL declaration for myAutomation.h is:
|
||||
* HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)
|
||||
*
|
||||
* Where numPins can be 1-80, and interruptPin can be any spare Arduino pin.
|
||||
*
|
||||
* Configure using the following on the main I2C bus:
|
||||
* HAL(TCA8418, 300, 80, 0x34)
|
||||
*
|
||||
* Use something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:
|
||||
* HAL(TCA8418, 400, 80, {SubBus_1, 0x34})
|
||||
*
|
||||
* And if needing an Interrupt pin to speed up operations:
|
||||
* HAL(TCA8418, 300, 80, 0x34, D21)
|
||||
*
|
||||
* Note that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100),
|
||||
* but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected.
|
||||
* Use any available Arduino pin for interrupt monitoring.
|
||||
*/
|
||||
|
||||
class TCA8418 : public IODevice {
|
||||
public:
|
||||
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress))
|
||||
new TCA8418(firstVpin, (nPins = (nPins > 80) ? 80 : nPins), i2cAddress, interruptPin);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
uint8_t* _digitalInputStates = NULL; // Array of pin states
|
||||
uint8_t _digitalPinBytes = 0; // Number of bytes in pin state array
|
||||
|
||||
uint8_t _numKeyEvents = 0; // Number of outsanding key events waiting for us
|
||||
|
||||
unsigned long _lastEventRead = 0;
|
||||
unsigned long _eventRefresh = 10000UL; // Delay refreshing events for 10ms
|
||||
const unsigned long _eventRefreshSlow = 100000UL; // Delay refreshing events for 100ms
|
||||
bool _gpioInterruptsEnabled = false;
|
||||
|
||||
uint8_t _inputBuffer[1];
|
||||
uint8_t _commandBuffer[1];
|
||||
I2CRB _i2crb;
|
||||
|
||||
enum {RDS_IDLE, RDS_EVENT, RDS_KEYCODE}; // Read operation states
|
||||
uint8_t _readState = RDS_IDLE;
|
||||
|
||||
// Constructor
|
||||
TCA8418(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (nPins > 0)
|
||||
{
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
_I2CAddress = i2cAddress;
|
||||
_gpioInterruptPin = interruptPin;
|
||||
addDevice(this);
|
||||
}
|
||||
}
|
||||
|
||||
void _begin() {
|
||||
|
||||
I2CManager.begin();
|
||||
|
||||
if (I2CManager.exists(_I2CAddress)) {
|
||||
// Default all GPIO pins to INPUT
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_1, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_2, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_3, 0x00);
|
||||
|
||||
// Remove all GPIO pins from events
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_1, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_2, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_3, 0x00);
|
||||
|
||||
// Set all pins to FALLING interrupts
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_1, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_2, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_3, 0x00);
|
||||
|
||||
// Remove all GPIO pins from interrupts
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_1, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_2, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_3, 0x00);
|
||||
|
||||
// Set up an 8 x 10 matrix by writing 0xFF to all the row and column configs
|
||||
// Row config is maximum of 8, and in REG_KP_GPIO_1
|
||||
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_1, 0xFF);
|
||||
// Column config is maximum of 10, lower 8 bits in REG_KP_GPIO_2, upper in REG_KP_GPIO_3
|
||||
// Set first 8 columns
|
||||
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_2, 0xFF);
|
||||
// Turn on cols 9/10
|
||||
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_3, 0x03);
|
||||
|
||||
// // Set all pins to Enable Debounce
|
||||
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_1, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_2, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_3, 0x00);
|
||||
|
||||
// Let's assume an 8x10 matrix for now, and configure
|
||||
_digitalPinBytes = (_nPins + 7) / 8;
|
||||
if ((_digitalInputStates = (byte *)calloc(_digitalPinBytes, 1)) == NULL) {
|
||||
DIAG(F("TCA8418 I2C: Unable to alloc %d bytes"), _digitalPinBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure pin used for GPIO extender notification of change (if allocated)
|
||||
// and configure TCA8418 to produce key event interrupts
|
||||
if (_gpioInterruptPin >= 0) {
|
||||
DIAG(F("TCA8418 I2C: interrupt pin configured on %d"), _gpioInterruptPin);
|
||||
_gpioInterruptsEnabled = true;
|
||||
_eventRefresh = _eventRefreshSlow; // Switch to slower manual refreshes in case the INT pin isn't connected!
|
||||
pinMode(_gpioInterruptPin, INPUT_PULLUP);
|
||||
I2CManager.write(_I2CAddress, 2, REG_CFG, REG_CFG_KE_IEN);
|
||||
// Clear any pending interrupts
|
||||
I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);
|
||||
}
|
||||
|
||||
#ifdef DIAG_IO
|
||||
_display();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
int _read(VPIN vpin) override {
|
||||
if (_deviceState == DEVSTATE_FAILED)
|
||||
return 0;
|
||||
int pin = vpin - _firstVpin;
|
||||
bool result = _digitalInputStates[pin / 8] & (1 << (pin % 8));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads)
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
if (_deviceState == DEVSTATE_FAILED) return; // If device failed, return
|
||||
|
||||
// Request block is used for key event reads from the TCA8418, which are performed
|
||||
// on a cyclic basis.
|
||||
|
||||
if (_readState != RDS_IDLE) {
|
||||
if (_i2crb.isBusy()) return; // If I2C operation still in progress, return
|
||||
|
||||
uint8_t status = _i2crb.status;
|
||||
if (status == I2C_STATUS_OK) { // If device request ok, read input data
|
||||
|
||||
// First check if we have any key events waiting
|
||||
if (_readState == RDS_EVENT) {
|
||||
if ((_numKeyEvents = (_inputBuffer[0] & 0x0F)) != 0) {
|
||||
// We could read each key event waiting in a synchronous loop, which may prove preferable
|
||||
// but for now, schedule an async read of the first key event in the queue
|
||||
_commandBuffer[0] = REG_KEY_EVENT_A;
|
||||
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
|
||||
_readState = RDS_KEYCODE; // Shift to reading key events!
|
||||
}
|
||||
else // We found no key events waiting, return to IDLE
|
||||
_readState = RDS_IDLE;
|
||||
}
|
||||
else {
|
||||
// RDS_KEYCODE
|
||||
uint8_t key = _inputBuffer[0] & 0x7F;
|
||||
bool keyDown = _inputBuffer[0] & 0x80;
|
||||
// Check for just keypad events
|
||||
key--; // R0/C0 is key #1, so subtract 1 to create an array offset
|
||||
// We only want to record key events we're configured for, as we have calloc'd an
|
||||
// appropriately sized _digitalInputStates array!
|
||||
if (key < _nPins) {
|
||||
if (keyDown)
|
||||
_digitalInputStates[key / 8] |= (1 << (key % 8));
|
||||
else
|
||||
_digitalInputStates[key / 8] &= ~(1 << (key % 8));
|
||||
}
|
||||
else
|
||||
DIAG(F("TCA8418 I2C: key event %d discarded, outside Vpin range"), key);
|
||||
_numKeyEvents--; // One less key event to get
|
||||
if (_numKeyEvents != 0)
|
||||
{
|
||||
// DIAG(F("TCA8418 I2C: more keys in read event queue, # waiting is: %x"), _numKeyEvents);
|
||||
// We could read each key event waiting in a synchronous loop, which may prove preferable
|
||||
// but for now, schedule an async read of the first key event in the queue
|
||||
_commandBuffer[0] = REG_KEY_EVENT_A;
|
||||
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
|
||||
}
|
||||
else {
|
||||
// DIAG(F("TCA8418 I2C: no more keys in read event queue"));
|
||||
// Clear any pending interrupts
|
||||
I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);
|
||||
_readState = RDS_IDLE; // Shift to IDLE
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else
|
||||
reportError(status, false); // report eror but don't go offline.
|
||||
}
|
||||
|
||||
// If we're not doing anything now, check to see if we have an interrupt pin configured and it is low,
|
||||
// or if our timer has elapsed and we should check anyway in case the interrupt pin is disconnected.
|
||||
if (_readState == RDS_IDLE) {
|
||||
if ((_gpioInterruptsEnabled && !digitalRead(_gpioInterruptPin)) ||
|
||||
((currentMicros - _lastEventRead) > _eventRefresh))
|
||||
{
|
||||
_commandBuffer[0] = REG_KEY_LCK_EC;
|
||||
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
|
||||
_lastEventRead = currentMicros;
|
||||
_readState = RDS_EVENT; // Shift to looking for key events!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display device information and status
|
||||
void _display() override {
|
||||
DIAG(F("TCA8418 I2C:%s Vpins %u-%u%S"),
|
||||
_I2CAddress.toString(),
|
||||
_firstVpin, (_firstVpin+_nPins-1),
|
||||
_deviceState == DEVSTATE_FAILED ? F(" OFFLINE") : F(""));
|
||||
if (_gpioInterruptsEnabled)
|
||||
DIAG(F("TCA8418 I2C:Interrupt on pin %d"), _gpioInterruptPin);
|
||||
}
|
||||
|
||||
// Helper function for error handling
|
||||
void reportError(uint8_t status, bool fail=true) {
|
||||
DIAG(F("TCA8418 I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
|
||||
status, I2CManager.getErrorMessage(status));
|
||||
if (fail)
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
|
||||
enum tca8418_registers
|
||||
{
|
||||
// REG_RESERVED = 0x00
|
||||
REG_CFG = 0x01, // Configuration register
|
||||
REG_INT_STAT = 0x02, // Interrupt status
|
||||
REG_KEY_LCK_EC = 0x03, // Key lock and event counter
|
||||
REG_KEY_EVENT_A = 0x04, // Key event register A
|
||||
REG_KEY_EVENT_B = 0x05, // Key event register B
|
||||
REG_KEY_EVENT_C = 0x06, // Key event register C
|
||||
REG_KEY_EVENT_D = 0x07, // Key event register D
|
||||
REG_KEY_EVENT_E = 0x08, // Key event register E
|
||||
REG_KEY_EVENT_F = 0x09, // Key event register F
|
||||
REG_KEY_EVENT_G = 0x0A, // Key event register G
|
||||
REG_KEY_EVENT_H = 0x0B, // Key event register H
|
||||
REG_KEY_EVENT_I = 0x0C, // Key event register I
|
||||
REG_KEY_EVENT_J = 0x0D, // Key event register J
|
||||
REG_KP_LCK_TIMER = 0x0E, // Keypad lock1 to lock2 timer
|
||||
REG_UNLOCK_1 = 0x0F, // Unlock register 1
|
||||
REG_UNLOCK_2 = 0x10, // Unlock register 2
|
||||
REG_GPIO_INT_STAT_1 = 0x11, // GPIO interrupt status 1
|
||||
REG_GPIO_INT_STAT_2 = 0x12, // GPIO interrupt status 2
|
||||
REG_GPIO_INT_STAT_3 = 0x13, // GPIO interrupt status 3
|
||||
REG_GPIO_DAT_STAT_1 = 0x14, // GPIO data status 1
|
||||
REG_GPIO_DAT_STAT_2 = 0x15, // GPIO data status 2
|
||||
REG_GPIO_DAT_STAT_3 = 0x16, // GPIO data status 3
|
||||
REG_GPIO_DAT_OUT_1 = 0x17, // GPIO data out 1
|
||||
REG_GPIO_DAT_OUT_2 = 0x18, // GPIO data out 2
|
||||
REG_GPIO_DAT_OUT_3 = 0x19, // GPIO data out 3
|
||||
REG_GPIO_INT_EN_1 = 0x1A, // GPIO interrupt enable 1
|
||||
REG_GPIO_INT_EN_2 = 0x1B, // GPIO interrupt enable 2
|
||||
REG_GPIO_INT_EN_3 = 0x1C, // GPIO interrupt enable 3
|
||||
REG_KP_GPIO_1 = 0x1D, // Keypad/GPIO select 1
|
||||
REG_KP_GPIO_2 = 0x1E, // Keypad/GPIO select 2
|
||||
REG_KP_GPIO_3 = 0x1F, // Keypad/GPIO select 3
|
||||
REG_GPI_EM_1 = 0x20, // GPI event mode 1
|
||||
REG_GPI_EM_2 = 0x21, // GPI event mode 2
|
||||
REG_GPI_EM_3 = 0x22, // GPI event mode 3
|
||||
REG_GPIO_DIR_1 = 0x23, // GPIO data direction 1
|
||||
REG_GPIO_DIR_2 = 0x24, // GPIO data direction 2
|
||||
REG_GPIO_DIR_3 = 0x25, // GPIO data direction 3
|
||||
REG_GPIO_INT_LVL_1 = 0x26, // GPIO edge/level detect 1
|
||||
REG_GPIO_INT_LVL_2 = 0x27, // GPIO edge/level detect 2
|
||||
REG_GPIO_INT_LVL_3 = 0x28, // GPIO edge/level detect 3
|
||||
REG_DEBOUNCE_DIS_1 = 0x29, // Debounce disable 1
|
||||
REG_DEBOUNCE_DIS_2 = 0x2A, // Debounce disable 2
|
||||
REG_DEBOUNCE_DIS_3 = 0x2B, // Debounce disable 3
|
||||
REG_GPIO_PULL_1 = 0x2C, // GPIO pull-up disable 1
|
||||
REG_GPIO_PULL_2 = 0x2D, // GPIO pull-up disable 2
|
||||
REG_GPIO_PULL_3 = 0x2E, // GPIO pull-up disable 3
|
||||
// REG_RESERVED = 0x2F
|
||||
};
|
||||
|
||||
enum tca8418_config_reg_fields
|
||||
{
|
||||
// Config Register #1 fields
|
||||
REG_CFG_AI = 0x80, // Auto-increment for read/write
|
||||
REG_CFG_GPI_E_CGF = 0x40, // Event mode config
|
||||
REG_CFG_OVR_FLOW_M = 0x20, // Overflow mode enable
|
||||
REG_CFG_INT_CFG = 0x10, // Interrupt config
|
||||
REG_CFG_OVR_FLOW_IEN = 0x08, // Overflow interrupt enable
|
||||
REG_CFG_K_LCK_IEN = 0x04, // Keypad lock interrupt enable
|
||||
REG_CFG_GPI_IEN = 0x02, // GPI interrupt enable
|
||||
REG_CFG_KE_IEN = 0x01, // Key events interrupt enable
|
||||
};
|
||||
|
||||
enum tca8418_int_status_fields
|
||||
{
|
||||
// Interrupt Status Register #2 fields
|
||||
REG_STAT_CAD_INT = 0x10, // Ctrl-alt-del seq status
|
||||
REG_STAT_OVR_FLOW_INT = 0x08, // Overflow interrupt status
|
||||
REG_STAT_K_LCK_INT = 0x04, // Key lock interrupt status
|
||||
REG_STAT_GPI_INT = 0x02, // GPI interrupt status
|
||||
REG_STAT_K_INT = 0x01, // Key events interrupt status
|
||||
};
|
||||
|
||||
enum tca8418_lock_ec_fields
|
||||
{
|
||||
// Key Lock Event Count Register #3
|
||||
REG_LCK_EC_K_LCK_EN = 0x40, // Key lock enable
|
||||
REG_LCK_EC_LCK_2 = 0x20, // Keypad lock status 2
|
||||
REG_LCK_EC_LCK_1 = 0x10, // Keypad lock status 1
|
||||
REG_LCK_EC_KLEC_3 = 0x08, // Key event count bit 3
|
||||
REG_LCK_EC_KLEC_2 = 0x04, // Key event count bit 2
|
||||
REG_LCK_EC_KLEC_1 = 0x02, // Key event count bit 1
|
||||
REG_LCK_EC_KLEC_0 = 0x01, // Key event count bit 0
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
216
IO_TM1638.cpp
Normal file
216
IO_TM1638.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* © 2024, 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/>.
|
||||
*/
|
||||
|
||||
/* Credit to https://github.com/dvarrel/TM1638 for the basic formulae.*/
|
||||
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "IODevice.h"
|
||||
#include "IO_TM1638.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
|
||||
const uint8_t HIGHFLASH _digits[16]={
|
||||
0b00111111,0b00000110,0b01011011,0b01001111,
|
||||
0b01100110,0b01101101,0b01111101,0b00000111,
|
||||
0b01111111,0b01101111,0b01110111,0b01111100,
|
||||
0b00111001,0b01011110,0b01111001,0b01110001
|
||||
};
|
||||
|
||||
// Constructor
|
||||
TM1638::TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin){
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = 8;
|
||||
_clk_pin = clk_pin;
|
||||
_stb_pin = stb_pin;
|
||||
_dio_pin = dio_pin;
|
||||
pinMode(clk_pin,OUTPUT);
|
||||
pinMode(stb_pin,OUTPUT);
|
||||
pinMode(dio_pin,OUTPUT);
|
||||
_pulse = PULSE1_16;
|
||||
|
||||
_buttons=0;
|
||||
_leds=0;
|
||||
_lastLoop=micros();
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
|
||||
void TM1638::create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin) {
|
||||
if (checkNoOverlap(firstVpin,8))
|
||||
new TM1638(firstVpin, clk_pin,dio_pin,stb_pin);
|
||||
}
|
||||
|
||||
void TM1638::_begin() {
|
||||
displayClear();
|
||||
test();
|
||||
_display();
|
||||
}
|
||||
|
||||
|
||||
void TM1638::_loop(unsigned long currentMicros) {
|
||||
if (currentMicros - _lastLoop > (1000000UL/LoopHz)) {
|
||||
_buttons=getButtons();// Read the buttons
|
||||
_lastLoop=currentMicros;
|
||||
}
|
||||
}
|
||||
|
||||
void TM1638::_display() {
|
||||
DIAG(F("TM1638 Configured on Vpins:%u-%u"), _firstVpin, _firstVpin+_nPins-1);
|
||||
}
|
||||
|
||||
// digital read gets button state
|
||||
int TM1638::_read(VPIN vpin) {
|
||||
byte pin=vpin - _firstVpin;
|
||||
bool result=bitRead(_buttons,pin);
|
||||
// DIAG(F("TM1638 read (%d) buttons %x = %d"),pin,_buttons,result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// digital write sets led state
|
||||
void TM1638::_write(VPIN vpin, int value) {
|
||||
// TODO.. skip if no state change
|
||||
writeLed(vpin - _firstVpin + 1,value!=0);
|
||||
}
|
||||
|
||||
// Analog write sets digit displays
|
||||
|
||||
void TM1638::_writeAnalogue(VPIN vpin, int lowBytes, uint8_t mode, uint16_t highBytes) {
|
||||
// mode is in DataFormat defined above.
|
||||
byte formatLength=mode & 0x0F; // last 4 bits
|
||||
byte formatType=mode & 0xF0; //
|
||||
int8_t leftDigit=vpin-_firstVpin; // 0..7 from left
|
||||
int8_t rightDigit=leftDigit+formatLength-1; // 0..7 from left
|
||||
|
||||
// loading is done right to left startDigit first
|
||||
int8_t startDigit=7-rightDigit; // reverse as 7 on left
|
||||
int8_t lastDigit=7-leftDigit; // reverse as 7 on left
|
||||
uint32_t value=highBytes;
|
||||
value<<=16;
|
||||
value |= (uint16_t)lowBytes;
|
||||
|
||||
//DIAG(F("TM1638 fl=%d ft=%x sd=%d ld=%d v=%l vx=%X"),
|
||||
// formatLength,formatType,startDigit,lastDigit,value,value);
|
||||
while(startDigit<=lastDigit) {
|
||||
switch (formatType) {
|
||||
case _DF_DECIMAL:// decimal (leading zeros)
|
||||
displayDig(startDigit,GETHIGHFLASH(_digits,(value%10)));
|
||||
value=value/10;
|
||||
break;
|
||||
case _DF_HEX:// HEX (leading zeros)
|
||||
displayDig(startDigit,GETHIGHFLASH(_digits,(value & 0x0F)));
|
||||
value>>=4;
|
||||
break;
|
||||
case _DF_RAW:// Raw 7-segment pattern
|
||||
displayDig(startDigit,value & 0xFF);
|
||||
value>>=8;
|
||||
break;
|
||||
default:
|
||||
DIAG(F("TM1368 invalid mode 0x%x"),mode);
|
||||
return;
|
||||
}
|
||||
startDigit++;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t TM1638::getButtons(){
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
|
||||
writeData(INSTRUCTION_READ_KEY);
|
||||
pinMode(_dio_pin, INPUT);
|
||||
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
|
||||
uint8_t buttons=0;
|
||||
for (uint8_t eachByte=0; eachByte<4;eachByte++) {
|
||||
uint8_t value = 0;
|
||||
for (uint8_t eachBit = 0; eachBit < 8; eachBit++) {
|
||||
ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
|
||||
value |= ArduinoPins::fastReadDigital(_dio_pin) << eachBit;
|
||||
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
|
||||
}
|
||||
buttons |= value << eachByte;
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
pinMode(_dio_pin, OUTPUT);
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
|
||||
void TM1638::displayDig(uint8_t digitId, uint8_t pgfedcba){
|
||||
if (digitId>7) return;
|
||||
setDataInstruction(DISPLAY_TURN_ON | _pulse);
|
||||
setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_FIXED);
|
||||
writeDataAt(FIRST_DISPLAY_ADDRESS+14-(digitId*2), pgfedcba);
|
||||
}
|
||||
|
||||
void TM1638::displayClear(){
|
||||
setDataInstruction(DISPLAY_TURN_ON | _pulse);
|
||||
setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
|
||||
for (uint8_t i=0;i<15;i+=2){
|
||||
writeDataAt(FIRST_DISPLAY_ADDRESS+i,0x00);
|
||||
}
|
||||
}
|
||||
|
||||
void TM1638::writeLed(uint8_t num,bool state){
|
||||
if ((num<1) | (num>8)) return;
|
||||
setDataInstruction(DISPLAY_TURN_ON | _pulse);
|
||||
setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
|
||||
writeDataAt(FIRST_DISPLAY_ADDRESS + (num*2-1), state);
|
||||
}
|
||||
|
||||
|
||||
void TM1638::writeData(uint8_t data){
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
ArduinoPins::fastWriteDigital(_dio_pin, data & 1);
|
||||
data >>= 1;
|
||||
ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
|
||||
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
|
||||
}
|
||||
}
|
||||
|
||||
void TM1638::writeDataAt(uint8_t displayAddress, uint8_t data){
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
|
||||
writeData(displayAddress);
|
||||
writeData(data);
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
void TM1638::setDataInstruction(uint8_t dataInstruction){
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
|
||||
writeData(dataInstruction);
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
void TM1638::test(){
|
||||
DIAG(F("TM1638 test"));
|
||||
uint8_t val=0;
|
||||
for(uint8_t i=0;i<5;i++){
|
||||
setDataInstruction(DISPLAY_TURN_ON | _pulse);
|
||||
setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_AUTO);
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
|
||||
writeData(FIRST_DISPLAY_ADDRESS);
|
||||
for(uint8_t i=0;i<16;i++)
|
||||
writeData(val);
|
||||
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
|
||||
delay(1000);
|
||||
val = ~val;
|
||||
}
|
||||
|
||||
}
|
134
IO_TM1638.h
Normal file
134
IO_TM1638.h
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* © 2024, 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_TM1638_h
|
||||
#define IO_TM1638_h
|
||||
#include <Arduino.h>
|
||||
#include "IODevice.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
class TM1638 : public IODevice {
|
||||
private:
|
||||
|
||||
uint8_t _buttons;
|
||||
uint8_t _leds;
|
||||
unsigned long _lastLoop;
|
||||
static const int LoopHz=20;
|
||||
|
||||
static const byte
|
||||
INSTRUCTION_WRITE_DATA=0x40,
|
||||
INSTRUCTION_READ_KEY=0x42,
|
||||
INSTRUCTION_ADDRESS_AUTO=0x40,
|
||||
INSTRUCTION_ADDRESS_FIXED=0x44,
|
||||
INSTRUCTION_NORMAL_MODE=0x40,
|
||||
INSTRUCTION_TEST_MODE=0x48,
|
||||
|
||||
FIRST_DISPLAY_ADDRESS=0xC0,
|
||||
|
||||
DISPLAY_TURN_OFF=0x80,
|
||||
DISPLAY_TURN_ON=0x88;
|
||||
|
||||
|
||||
uint8_t _clk_pin;
|
||||
uint8_t _stb_pin;
|
||||
uint8_t _dio_pin;
|
||||
uint8_t _pulse;
|
||||
bool _isOn;
|
||||
|
||||
|
||||
// Constructor
|
||||
TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
|
||||
|
||||
public:
|
||||
enum DigitFormat : byte {
|
||||
// last 4 bits are length.
|
||||
// DF_1.. DF_8 decimal
|
||||
DF_1=0x01,DF_2=0x02,DF_3=0x03,DF_4=0x04,
|
||||
DF_5=0x05,DF_6=0x06,DF_7=0x07,DF_8=0x08,
|
||||
// DF_1X.. DF_8X HEX
|
||||
DF_1X=0x11,DF_2X=0x12,DF_3X=0x13,DF_4X=0x14,
|
||||
DF_5X=0x15,DF_6X=0x16,DF_7X=0x17,DF_8X=0x18,
|
||||
// DF_1R .. DF_4R raw 7 segmnent data
|
||||
// only 4 because HAL analogWrite only passes 4 bytes
|
||||
DF_1R=0x21,DF_2R=0x22,DF_3R=0x23,DF_4R=0x24,
|
||||
|
||||
// bits of data conversion type (ored with length)
|
||||
_DF_DECIMAL=0x00,// right adjusted decimal unsigned leading zeros
|
||||
_DF_HEX=0x10, // right adjusted hex leading zeros
|
||||
_DF_RAW=0x20 // bytes are raw 7-segment pattern (max length 4)
|
||||
};
|
||||
|
||||
static void create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
|
||||
|
||||
// Functions overridden in IODevice
|
||||
void _begin();
|
||||
void _loop(unsigned long currentMicros) override ;
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;
|
||||
void _display() override ;
|
||||
int _read(VPIN pin) override;
|
||||
void _write(VPIN pin,int value) override;
|
||||
|
||||
// Device driving functions
|
||||
private:
|
||||
enum pulse_t {
|
||||
PULSE1_16,
|
||||
PULSE2_16,
|
||||
PULSE4_16,
|
||||
PULSE10_16,
|
||||
PULSE11_16,
|
||||
PULSE12_16,
|
||||
PULSE13_16,
|
||||
PULSE14_16
|
||||
};
|
||||
|
||||
/**
|
||||
* @fn getButtons
|
||||
* @return state of 8 buttons
|
||||
*/
|
||||
uint8_t getButtons();
|
||||
|
||||
/**
|
||||
* @fn writeLed
|
||||
* @brief put led ON or OFF
|
||||
* @param num num of led(1-8)
|
||||
* @param state (true or false)
|
||||
*/
|
||||
void writeLed(uint8_t num, bool state);
|
||||
|
||||
|
||||
/**
|
||||
* @fn displayDig
|
||||
* @brief set 7 segment display + dot
|
||||
* @param digitId num of digit(0-7)
|
||||
* @param val value 8 bits
|
||||
*/
|
||||
void displayDig(uint8_t digitId, uint8_t pgfedcba);
|
||||
|
||||
/**
|
||||
* @fn displayClear
|
||||
* @brief switch off all leds and segment display
|
||||
*/
|
||||
void displayClear();
|
||||
void test();
|
||||
void writeData(uint8_t data);
|
||||
void writeDataAt(uint8_t displayAddress, uint8_t data);
|
||||
void setDisplayMode(uint8_t displayMode);
|
||||
void setDataInstruction(uint8_t dataInstruction);
|
||||
};
|
||||
#endif
|
@@ -131,4 +131,4 @@ protected:
|
||||
|
||||
};
|
||||
|
||||
#endif // IO_TOUCHKEYPAD_H
|
||||
#endif // IO_TOUCHKEYPAD_H
|
||||
|
@@ -170,4 +170,4 @@ public:
|
||||
}
|
||||
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -95,4 +95,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -54,4 +54,43 @@ static_assert("MAIN"_hk == 11339,"Keyword hasher error");
|
||||
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
|
||||
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
|
||||
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
|
||||
#endif
|
||||
|
||||
// Compile time converter from "abcd"_s7 to the 7 segment nearest equivalent
|
||||
|
||||
constexpr uint8_t seg7Digits[]={
|
||||
0b00111111,0b00000110,0b01011011,0b01001111, // 0..3
|
||||
0b01100110,0b01101101,0b01111101,0b00000111, // 4..7
|
||||
0b01111111,0b01101111 // 8..9
|
||||
};
|
||||
|
||||
constexpr uint8_t seg7Letters[]={
|
||||
0b01110111,0b01111100,0b00111001,0b01011110, // ABCD
|
||||
0b01111001,0b01110001,0b00111101,0b01110110, // EFGH
|
||||
0b00000100,0b00011110,0b01110010,0b00111000, //IJKL
|
||||
0b01010101,0b01010100,0b01011100,0b01110011, // MNOP
|
||||
0b10111111,0b01010000,0b01101101,0b01111000, // QRST
|
||||
0b00111110,0b00011100,0b01101010,0b01001001, //UVWX
|
||||
0b01100110,0b01011011 //YZ
|
||||
};
|
||||
constexpr uint8_t seg7Space=0b00000000;
|
||||
constexpr uint8_t seg7Minus=0b01000000;
|
||||
constexpr uint8_t seg7Equals=0b01001000;
|
||||
|
||||
|
||||
constexpr uint32_t CompiletimeSeg7(const char * sv, uint32_t running, size_t rlen) {
|
||||
return (*sv==0 || rlen==0) ? running << (8*rlen) : CompiletimeSeg7(sv+1,
|
||||
(*sv >= '0' && *sv <= '9') ? (running<<8) | seg7Digits[*sv-'0'] :
|
||||
(*sv >= 'A' && *sv <= 'Z') ? (running<<8) | seg7Letters[*sv-'A'] :
|
||||
(*sv >= 'a' && *sv <= 'z') ? (running<<8) | seg7Letters[*sv-'a'] :
|
||||
(*sv == '-') ? (running<<8) | seg7Minus :
|
||||
(*sv == '=') ? (running<<8) | seg7Equals :
|
||||
(running<<8) | seg7Space,
|
||||
rlen-1
|
||||
); //
|
||||
}
|
||||
|
||||
constexpr uint32_t operator""_s7(const char * keyword, size_t len)
|
||||
{
|
||||
return CompiletimeSeg7(keyword,0*len,4);
|
||||
}
|
||||
#endif
|
||||
|
@@ -221,4 +221,4 @@ void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
|
||||
rb.wait();
|
||||
outputBuffer[0] = value | _backlightval;
|
||||
I2CManager.write(_Addr, outputBuffer, 1, &rb); // Write command asynchronously
|
||||
}
|
||||
}
|
||||
|
@@ -576,7 +576,7 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
|
||||
DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A');
|
||||
}
|
||||
setPower(POWERMODE::ALERT);
|
||||
if ((trackMode & TRACK_MODE_AUTOINV) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
|
||||
if ((trackMode & TRACK_MODIFIER_AUTO) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
|
||||
DIAG(F("TRACK %c INVERT"), trackno + 'A');
|
||||
invertOutput();
|
||||
}
|
||||
|
@@ -28,22 +28,31 @@
|
||||
#include "DCCTimer.h"
|
||||
#include <wiring_private.h>
|
||||
|
||||
#include "TemplateForEnums.h"
|
||||
// use powers of two so we can do logical and/or on the track modes in if clauses.
|
||||
// RACK_MODE_DCX is (TRACK_MODE_DC|TRACK_MODE_INV)
|
||||
template<class T> inline T operator~ (T a) { return (T)~(int)a; }
|
||||
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
|
||||
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
|
||||
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
|
||||
enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
|
||||
TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,
|
||||
// For example TRACK_MODE_DC_INV is (TRACK_MODE_DC|TRACK_MODIFIER_INV)
|
||||
enum TRACK_MODE : byte {
|
||||
// main modes
|
||||
TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
|
||||
TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,
|
||||
// modifiers
|
||||
TRACK_MODIFIER_INV = 64, TRACK_MODIFIER_AUTO = 128,
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
TRACK_MODE_BOOST = 32,
|
||||
TRACK_MODE_BOOST = 32,
|
||||
TRACK_MODE_BOOST_INV = TRACK_MODE_BOOST|TRACK_MODIFIER_INV,
|
||||
TRACK_MODE_BOOST_AUTO = TRACK_MODE_BOOST|TRACK_MODIFIER_AUTO,
|
||||
#else
|
||||
TRACK_MODE_BOOST = 0,
|
||||
TRACK_MODE_BOOST = 0,
|
||||
TRACK_MODE_BOOST_INV = 0,
|
||||
TRACK_MODE_BOOST_AUTO = 0,
|
||||
#endif
|
||||
TRACK_MODE_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,
|
||||
TRACK_MODE_INV = 64,
|
||||
TRACK_MODE_DCX = TRACK_MODE_DC|TRACK_MODE_INV, TRACK_MODE_AUTOINV = 128};
|
||||
// derived modes; TRACK_ALL is calles that so it does not match TRACK_MODE_*
|
||||
TRACK_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,
|
||||
TRACK_MODE_MAIN_INV = TRACK_MODE_MAIN|TRACK_MODIFIER_INV,
|
||||
TRACK_MODE_MAIN_AUTO = TRACK_MODE_MAIN|TRACK_MODIFIER_AUTO,
|
||||
TRACK_MODE_DC_INV = TRACK_MODE_DC|TRACK_MODIFIER_INV,
|
||||
TRACK_MODE_DCX = TRACK_MODE_DC_INV // DCX is other name for historical reasons
|
||||
};
|
||||
|
||||
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
|
||||
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
|
||||
@@ -273,7 +282,7 @@ class MotorDriver {
|
||||
#endif
|
||||
inline void setMode(TRACK_MODE m) {
|
||||
trackMode = m;
|
||||
invertOutput(trackMode & TRACK_MODE_INV);
|
||||
invertOutput(trackMode & TRACK_MODIFIER_INV);
|
||||
};
|
||||
inline void invertOutput() { // toggles output inversion
|
||||
invertPhase = !invertPhase;
|
||||
|
@@ -75,11 +75,19 @@
|
||||
#define SAMD_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
|
||||
#define STM32_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
|
||||
|
||||
#if defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
|
||||
// The Ethernet capable STM32 models cannot use Channel B BRAKE on D8, and must use the ALT pin of D6,
|
||||
// AND cannot use Channel B PWN on D11, but must use the ALT pin of D5
|
||||
#define EX8874_SHIELD F("EX8874"), \
|
||||
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
|
||||
new MotorDriver( 5, 13, UNUSED_PIN, 6, A1, 1.27, 5000, A5)
|
||||
#else
|
||||
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
|
||||
#define EX8874_SHIELD F("EX8874"), \
|
||||
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
|
||||
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 1.27, 5000, A5)
|
||||
|
||||
#endif
|
||||
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
// STANDARD shield on an ESPDUINO-32 (ESP32 in Uno form factor). The shield must be eiter the
|
||||
|
82
README.md
82
README.md
@@ -1,77 +1,39 @@
|
||||
# What is DCC++ EX?
|
||||
DCC++ EX is the organization maintaining several codebases that together represent a fully open source DCC system. Currently, this includes the following:
|
||||
# What is DCC-EX?
|
||||
DCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your complete model railroad layout. Our easy to use, do-it-yourself, and free open source products run on off-the-shelf Arduino technology and are supported by numerous third party hardware and apps like JMRI, Engine Driver, wiThrottle, Rocrail and more.
|
||||
|
||||
* [CommandStation-EX](https://github.com/DCC-EX/CommandStation-EX/releases) - the latest take on the DCC++ command station for controlling your trains. Runs on an Arduino board, and includes advanced features such as a WiThrottle server implementation, turnout operation, general purpose inputs and outputs (I/O), and JMRI integration.
|
||||
* [exWebThrottle](https://github.com/DCC-EX/exWebThrottle) - a simple web based controller for your DCC++ command station.
|
||||
* [BaseStation-installer](https://github.com/DCC-EX/BaseStation-Installer) - an installer executable that takes care of downloading and installing DCC++ firmware onto your hardware setup.
|
||||
* [BaseStation-Classic](https://github.com/DCC-EX/BaseStation-Classic) - the original DCC++ software, packaged in a stable release. No active development, bug fixes only.
|
||||
Currently, our products include the following:
|
||||
|
||||
A basic DCC++ EX hardware setup can use easy to find, widely avalable Arduino boards that you can assemble yourself.
|
||||
|
||||
Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital Command Control (DCC) [standards](http://www.nmra.org/dcc-working-group "NMRA DCC Working Group"), including:
|
||||
|
||||
* simultaneous control of multiple locomotives
|
||||
* 2-byte and 4-byte locomotive addressing
|
||||
* 28 or 128-step speed throttling
|
||||
* Activate/de-activate all accessory function addresses 0-2048
|
||||
* Control of all cab functions F0-F28 and F29-F68
|
||||
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
|
||||
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
|
||||
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
||||
* [EX-CommandStation](https://github.com/DCC-EX/CommandStation-EX/releases)
|
||||
* [EX-WebThrottle](https://github.com/DCC-EX/exWebThrottle)
|
||||
* [EX-Installer](https://github.com/DCC-EX/EX-Installer)
|
||||
* [EX-MotoShield8874](https://dcc-ex.com/reference/hardware/motorboards/ex-motor-shield-8874.html#gsc.tab=0)
|
||||
* [EX-DCCInspector](https://github.com/DCC-EX/DCCInspector-EX)
|
||||
* [EX-Toolbox](https://github.com/DCC-EX/EX-Toolbox)
|
||||
* [EX-Turntable](https://github.com/DCC-EX/EX-Turntable)
|
||||
* [EX-IOExpander](https://github.com/DCC-EX/EX-IOExpander)
|
||||
* [EX-FastClock](https://github.com/DCC-EX/EX-FastClock)
|
||||
* [DCCEXProtocol](https://github.com/DCC-EX/DCCEXProtocol)
|
||||
|
||||
Details of these projects can be found on [our web site](https://dcc-ex.com/).
|
||||
|
||||
# What’s in this Repository?
|
||||
|
||||
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
|
||||
This repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
|
||||
|
||||
To utilize this sketch, you can use the following:
|
||||
|
||||
1. (beginner) our [automated installer](https://github.com/DCC-EX/BaseStation-Installer)
|
||||
1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)
|
||||
2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)
|
||||
3. (advanced) use git clone on this repository
|
||||
|
||||
Not using the installer? Open the file "CommandStation-EX.ino" in the
|
||||
Arduino IDE. Please do not rename the folder containing the sketch
|
||||
code, nor add any files in that folder. The Arduino IDE relies on the
|
||||
structure and name of the folder to properly display and compile the
|
||||
code. Rename or copy config.example.h to config.h. If you do not have
|
||||
the standard setup, you must edit config.h according to the help texts
|
||||
in config.h.
|
||||
Refer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.
|
||||
|
||||
## What's new in CommandStation-EX?
|
||||
**We seriously recommend using the EX-Installer**, however if you choose not to use the installer...
|
||||
|
||||
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station (or through JMRI as before)
|
||||
* WiFi and Ethernet shield support
|
||||
* No more jumpers or soldering!
|
||||
* Direct support for all the most popular motor control boards including single pin (Arduino) or dual pin (IBT_2) type PWM inputs without the need for an adapter circuit
|
||||
* I2C Display support (LCD and OLED)
|
||||
* Improved short circuit detection and automatic reset from an overload
|
||||
* Current reading, sensing and ACK detection settings in milliAmps instead of just pin readings
|
||||
* Improved adherence to the NMRA DCC specification
|
||||
* Complete support for all the old commands and front ends like JMRI
|
||||
* Railcom cutout (beta)
|
||||
* Simpler, modular, faster code with an API Library for developers for easy expansion
|
||||
* New features and functions in JMRI
|
||||
* Ability to join MAIN and PROG tracks into one MAIN track to run your locos
|
||||
* "Drive-Away" feature - Throttles with support, like Engine Driver, can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
|
||||
* Diagnostic commands to test decoders that aren't reading or writing correctly
|
||||
* Support for Uno, Nano, Mega, Nano Every and Teensy microcontrollers
|
||||
* User Functions: Filter regular commands (like a turnout or output command) and pass it to your own function or accessory
|
||||
* Support for LCN (layout control nodes)
|
||||
* mySetup.h file that acts like an Autoexec.Bat command to send startup commands to the CS
|
||||
* High Accuracty Waveform option for rock steady DCC signals
|
||||
* New current response outputs current in mA, overlimit current, and maximum board capable current. Support for new current meter in JMRI
|
||||
* USB Browser based EX-WebThrottle
|
||||
* New, simpler, function control command
|
||||
* Number of locos discovery command `<#>`
|
||||
* Emergency stop command <!>
|
||||
* Release cabs from memory command <-> all cabs, <- CAB> for just one loco address
|
||||
* Automatic slot (register) management
|
||||
* Automation (coming soon)
|
||||
|
||||
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
|
||||
* Open the file ``CommandStation-EX.ino`` in the Arduino IDE or Visual Studio Code (VSC). Please do not rename the folder containing the sketch code, nor add any files in that folder. The Arduino IDE relies on the structure and name of the folder to properly display and compile the code.
|
||||
* Rename or copy ``config.example.h`` to ``config.h``.
|
||||
* You must edit ``config.h`` according to the help texts in ``config.h``.
|
||||
|
||||
# More information
|
||||
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)
|
||||
You can learn more at the [DCC-EX website](https://dcc-ex.com/)
|
||||
|
||||
- November 14, 2020
|
||||
|
82
Railcom.cpp
Normal file
82
Railcom.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* © 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
Normal file
40
Railcom.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* © 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
|
77
Release_Notes/NeoPixel.md
Normal file
77
Release_Notes/NeoPixel.md
Normal file
@@ -0,0 +1,77 @@
|
||||
NeoPixel support
|
||||
|
||||
The IO_NeoPixel.h driver supports the adafruit neopixel seesaw board. It turns each pixel into an individual VPIN which can be given a colour and turned on or off using the new <o> command or the NEOPIXEL Exrail macro. Exrail SIGNALS can also drive a single pixel signal or multiple separate pixels.
|
||||
|
||||
|
||||
1. Defining the hardware driver:
|
||||
Add a driver definition in myAutomation.h for each adafruit I2C driver.
|
||||
|
||||
HAL(neoPixel, firstVpin, numberOfPixels [, mode [, i2caddress])
|
||||
Where mode is selected from the various pixel string types which have varying
|
||||
colour order or refresh frequency. For MOST strings this mode will be NEO_GRB but for others refer to the comments in IO_NeoPixel.h
|
||||
If omitted the node and i2caddress default to NEO_GRB, 0x60.
|
||||
|
||||
HAL(NeoPixel,1000,20)
|
||||
This is a NeoPixel driver defaulting to I2C aqddress 0x60 for a GRB pixel string. Pixels are given vpin numbers from 1000 to 1019.
|
||||
HAL(NeoPixel,1020,20,NEO_GRB,0x61)
|
||||
This is a NeoPixel driver on i2c address 0x61
|
||||
|
||||
2. Setting pixels from the < > commands.
|
||||
By default, each pixel in the string is created as white but switched off.
|
||||
Each pixel has a vpin starting from the first vpin in the HAL definitions.
|
||||
|
||||
<o vpin> switches pixel on (same as <z vpin>) e.g. <o 1005>
|
||||
<o -vpin> switches pixel off (same as <z -vpin>) e.g. <o -1003>
|
||||
(the z commands work on pixels the same as other gpio pins.)
|
||||
|
||||
<o [-]vpin count> switches on/off count pixels starting at vpin. e.g <o 1000 5>
|
||||
Note: it IS acceptable to switch across 2 strings of pixels if they are contiguous vpin ranges. It is also interesting that this command doesnt care if the vpins are NeoPixel or any other type, so it can be used to switch a range of other pin types.
|
||||
|
||||
<o [-]vpin red green blue [count]> sets the colour and on/off status of a pin or pins. Each colour is 0..255 e.g. <o 1005 255 255 0> sets pin 1005 to bright yellow and ON, <0 -1006 0 0 255 10> sets pins 1006 to 1015 (10 pins) to bright blue but OFF.
|
||||
Note: If you set a pin to a colour, you can turn it on and off without having to reset the colour every time. This is something the adafruit seesaw library can't do and is just one of several reasons why we dont use it.
|
||||
|
||||
3. Setting pixels from EXRAIL
|
||||
The new NEOPIXEL macro provides the same functionality as the <o [-]vpin red green blue [count]> command above.
|
||||
NEOPIXEL([-]vpin, red, green, blue [,count])
|
||||
|
||||
Setting pixels on or off (without colour change) can be done with SET/RESET [currently there is no set range facility but that may be added as a general exrail thing... watch this space]
|
||||
|
||||
Because the pixels obey set/reset, the BLINK command can also be used to control blinking a pixel.
|
||||
|
||||
4. EXRAIL pixel signals.
|
||||
There are two types possible, a mast with separate fixed colour pixels for each aspect, or a mast with one multiple colour pixel for all aspects.
|
||||
|
||||
For separate pixels, the colours should be established at startup and a normal SIGNALH macro used.
|
||||
|
||||
AUTOSTART
|
||||
SIGNALH(1010,1011,1012)
|
||||
NEOPIXEL(1010,255,0,0)
|
||||
NEOPIXEL(1011,128,128,0)
|
||||
NEOPIXEL(1012,0,255,0)
|
||||
RED(1010) // force signal state otherwise all 3 lights will be on
|
||||
DONE
|
||||
|
||||
For signals with 1 pixel, the NEOPIXEL_SIGNAL macro will create a signal
|
||||
NEOPIXEL_SIGNAL(vpin,redfx,amberfx,greenfx)
|
||||
|
||||
** Changed... ****
|
||||
The fx values above can be created by the NeoRGB macro so a bright red would be NeoRGB(255,0,0) bright green NeoRGB(0,255,0) and amber something like NeoRGB(255,100,0)
|
||||
NeoRGB creates a single int32_t value so it can be used in several ways as convenient.
|
||||
|
||||
// create 1-lamp signal with NeoRGB colours
|
||||
NEOPIXEL_SIGNAL(1000,NeoRGB(255,0,0),NeoRGB(255,100,0),NeoRGB(0,255,0))
|
||||
|
||||
// Create 1-lamp signal with named colours.
|
||||
// This is better if you have multiple signals.
|
||||
// (Note: ALIAS is not suitable due to word length defaults)
|
||||
#define REDLAMP NeoRGB(255,0,0)
|
||||
#define AMBERLAMP NeoRGB(255,100,0)
|
||||
#define GREENLAMP NeoRGB(0,255,0)
|
||||
NEOPIXEL_SIGNAL(1001,REDLAMP,AMBERLAMP,GREENLAMP)
|
||||
|
||||
// Create 1-lamp signal with web type RGB colours
|
||||
// (Using blue for the amber signal , just testing)
|
||||
NEOPIXEL_SIGNAL(1002,0xFF0000,0x0000FF,0x00FF00)
|
||||
|
||||
|
||||
|
75
Release_Notes/Railcom.md
Normal file
75
Release_Notes/Railcom.md
Normal file
@@ -0,0 +1,75 @@
|
||||
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 hihjly cpu dependent and can be found in gthe various implementations of `DCCTimer.h`. At this time only `DCCTimerAVR.cpp`has implemented this.
|
||||
|
||||
|
||||
Reading Railcom data:
|
||||
A new HAL handler (`IO_I2CRailcom.h`)has been added to process input from a 2-block railcom reader (Refer Henk) which operates as a 2 channel UART accessible over I2C. The reader(s) sit between the CS and the track and collect railcom data from locos during the cutout.
|
||||
After the cutout the HAL driver reads the UARTs over I2C and passes the raw data to the CS logic (`Railcom.cpp`)for analysis.
|
||||
|
||||
Each 2-block reader is described in myAutomation like `HAL(I2CRailcom,10000,2,0x48)` which will assign 2 channels on i2c address 0x48 with vpin numbers 10000 and 10001. If you only use the first channel in the reader, just asign one pin instead of two.
|
||||
(Implementation notes.. potentially other readers are possible with suitable HAL drivers. There are however several touch-points with the code DCC Waveform code which helps the HAL driver to understand when the data is safe to sample, and how to interpret responses when the sender is unknown. )
|
||||
|
||||
Making use of Railcom data
|
||||
|
||||
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>`
|
||||
|
||||
|
||||
Additional EXRAIL features in Railcom Branch:
|
||||
- ESTAOPALL stops all locos immediately
|
||||
- XPOM(cab,cv,value) POM write cv to sepcific loco
|
||||
(POM(cv,value) already writes cv to current loco)
|
||||
|
44
Release_Notes/TCA8418.md
Normal file
44
Release_Notes/TCA8418.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## TCA8418 ##
|
||||
|
||||
The TCA8418 IC from Texas Instruments is a low cost and very capable GPIO and keyboard scanner. Used as a keyboard scanner, it has 8 rows of 10 columns of IO pins which allow encoding of up to 80 buttons. The IC is available on an Adafruit board with Qwiic I2C interconnect called the "Adafruit TCA8418 Keypad Matrix and GPIO Expander Breakout" and available here for the modest sum of $US6 or so: https://www.adafruit.com/product/4918
|
||||
|
||||
The great advantage of this IC is that the keyboard scanning is done continuously, and it has a 10-element event queue, so even if you don't get to the interrupt immediately, keypress and release events will be held for you. Since it's I2C its very easy to use with any DCC-EX command station.
|
||||
|
||||
The TCA8418 driver presently configures the IC in the full 8x10 keyboard scanning mode, and then maps each key down/key up event to the state of a single vpin for extremely easy use from within EX-RAIL and JMRI as each key looks like an individual sensor.
|
||||
|
||||
This is ideal for mimic panels where you may need a lot of buttons, but with this board you can use just 18 wires to handle as many as 80 buttons.
|
||||
|
||||
By adding a simple HAL statement to myAutomation.h it creates between 1 and 80 buttons it will report back.
|
||||
|
||||
`HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)`
|
||||
|
||||
For example:
|
||||
|
||||
`HAL(TCA8418, 300, 80, 0x34)`
|
||||
|
||||
Creates VPINs 300-379 which you can monitor with EX-RAIL, JMRI sensors etc.
|
||||
|
||||
With an 8x10 key event matrix, the events are numbered using the Rn row pins and Cn column pins as such:
|
||||
|
||||
C0 C1 C2 C3 C4 C5 C6 C7 C8 C9
|
||||
========================================
|
||||
R0| 0 1 2 3 4 5 6 7 8 9
|
||||
R1| 10 11 12 13 14 15 16 17 18 19
|
||||
R2| 20 21 22 23 24 25 26 27 28 29
|
||||
R3| 30 31 32 33 34 35 36 37 38 39
|
||||
R4| 40 41 42 43 44 45 46 47 48 49
|
||||
R5| 50 51 52 53 54 55 56 57 58 59
|
||||
R6| 60 61 62 63 64 65 66 67 68 69
|
||||
R7| 70 71 72 73 74 75 76 77 78 79
|
||||
|
||||
So if you start with the first pin definition being VPIN 300, R0/C0 will be 300 + 0, and R7/C9 will be 300+79 or 379.
|
||||
|
||||
Use something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:
|
||||
|
||||
`HAL(TCA8418, 400, 80, {SubBus_1, 0x34})`
|
||||
|
||||
And if needing an Interrupt pin to speed up operations:
|
||||
`HAL(TCA8418, 300, 80, 0x34, 21)`
|
||||
|
||||
Note that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100), but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected. Use any available Arduino pin for interrupt monitoring.
|
||||
|
84
Release_Notes/TM1638.md
Normal file
84
Release_Notes/TM1638.md
Normal file
@@ -0,0 +1,84 @@
|
||||
## TM1638 ##
|
||||
|
||||
The TM1638 board provides a very cheap way of implementing 8 buttons, 8 leds and an 8 digit 7segment display in a package requiring just 5 Dupont wires (vcc, gnd + 3 GPIO pins) from the command station without soldering.
|
||||
|
||||
|
||||
This is ideal for prototyping and testing, simulating sensors and signals, displaying states etc. For a built layout, this could provide a control for things that are not particularly suited to throttle 'route' buttons, perhaps lineside automations or fiddle yard lane selection.
|
||||
|
||||
By adding a simple HAL statement to myAutomation.h it creates 8 buttons/sensors and 8 leds.
|
||||
|
||||
`HAL(TM1638,500,29,31,33)`
|
||||
Creates VPINs 500-507 And desscribes the GPIO pins used to connect the clk,dio,stb pins on the TM1638 board.
|
||||
|
||||
Setting each of the VPINs will control the associated LED (using for example SET, RESET or BLINK in Exrail or `<z 500> <z -501> from a command).
|
||||
|
||||
Unlike most pins, you can also read the same pin number and get the button state, using Exrail IF/AT/ONBUTTON etc.
|
||||
|
||||
For example:
|
||||
`
|
||||
HAL(TM1638,500,29,31,33)
|
||||
`
|
||||
All the folowing examples assume you are using VPIN 500 as the first, leftmost, led/button on the TM1638 board.
|
||||
|
||||
|
||||
`ONBUTTON(500)
|
||||
SET(500) // light the first led
|
||||
BLINK(501,500,500) // blink the second led
|
||||
SETLOCO(3) FWD(50) // set a loco going
|
||||
AT(501) STOP // press second button to stop
|
||||
RESET(500) RESET(501) // turn leds off
|
||||
DONE
|
||||
`
|
||||
|
||||
Buttons behave like any other sensor, so using `<S 500 500 1>` will cause the command station to issue `<Q 500>` and `<q 500>` messages when the first button is pressed or released.
|
||||
|
||||
Exrail `JMRI_SENSOR(500,8)` will create `<S` commands for all 8 buttons.
|
||||
|
||||
## Using the 7 Segment display ##
|
||||
|
||||
The 8 digit display can be treated as 8 separate digits (left most being the same VPIN as the leftmost button and led) or be written to in sections of any length. Writing uses the existing analogue interface to the common HAL but is awkward to use directly. To make this easier from Exrail, a SEG7 macro provides a remapping to the ANOUT facility that makes more sense.
|
||||
|
||||
SEG7(vpin,value,format)
|
||||
|
||||
The vpin determins which digit to start writing at.
|
||||
The value can be a 32bit unsigned integer but is interpreted differentlky according to the format.
|
||||
|
||||
Format values:
|
||||
1..8 give the length (number of display digits) to fill, and defaults to decimal number with leading zeros.
|
||||
|
||||
1X..8X give the length but display in hex.
|
||||
|
||||
1R..4R treats each byte of the value as raw 7-segment patterns so that it can write letters and symbols using any compination of the 7segments and deciml point.
|
||||
|
||||
There is a useful description here:
|
||||
https://jetpackacademy.com/wp-content/uploads/2018/06/TM1638_cheat_sheet_download.pdf
|
||||
|
||||
|
||||
e.g. SEG7(500,3,4)
|
||||
writes 0003 to first 4 digits of the display
|
||||
SEG7(504,0xcafe,4X)
|
||||
writes CAFE to the last 4 digits
|
||||
SEG7(500,0xdeadbeef,8X)
|
||||
writes dEAdbEEF to all 8 digits.
|
||||
|
||||
Writing raw segment patters requires knowledge of the bit pattern to segment relationship:
|
||||
` 0
|
||||
== 0 ==
|
||||
5| | 1
|
||||
== 6 ==
|
||||
4 | | 2
|
||||
== 3 ==
|
||||
7=decimal point
|
||||
|
||||
Thus Letter A is segments 6 5 4 2 1 0, in bits that is (0 bit on right)
|
||||
0b01110111 or 0x77
|
||||
This is not easy to do my hand and thus a new string type suffix has been introduced to make simple text messages. Note that the HAL interface only has width for 32 bits which is only 4 symbols so writing 8 digits requires two calls.
|
||||
|
||||
e.g. SEG7(500,"Hell"_s7,4R) SEG7(504,"o"_s7,4R)
|
||||
DELAY(1000)
|
||||
SEG7(500,"Worl"_s7,4R) SEG7(504,"d"_s7,4R)
|
||||
|
||||
Note that some letters like k,m,v,x do not have particularly readable 7-segment representations.
|
||||
|
||||
Credit to https://github.com/dvarrel/TM1638 for the basic formulae.
|
||||
|
29
Release_Notes/momentum.md
Normal file
29
Release_Notes/momentum.md
Normal file
@@ -0,0 +1,29 @@
|
||||
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.
|
||||
|
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>
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2022 Paul M. Antoine
|
||||
* © 2021 Chris Harlow
|
||||
* © 2022 Harald Barth
|
||||
* © 2022 2024 Harald Barth
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "SerialManager.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "StringFormatter.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef SERIAL_BT_COMMANDS
|
||||
@@ -36,6 +37,10 @@ BluetoothSerial SerialBT;
|
||||
#endif //COMMANDS
|
||||
#endif //ESP32
|
||||
|
||||
static const byte PAYLOAD_FALSE = 0;
|
||||
static const byte PAYLOAD_NORMAL = 1;
|
||||
static const byte PAYLOAD_STRING = 2;
|
||||
|
||||
SerialManager * SerialManager::first=NULL;
|
||||
|
||||
SerialManager::SerialManager(Stream * myserial) {
|
||||
@@ -43,7 +48,7 @@ SerialManager::SerialManager(Stream * myserial) {
|
||||
next=first;
|
||||
first=this;
|
||||
bufferLength=0;
|
||||
inCommandPayload=false;
|
||||
inCommandPayload=PAYLOAD_FALSE;
|
||||
}
|
||||
|
||||
void SerialManager::init() {
|
||||
@@ -112,23 +117,39 @@ void SerialManager::loop() {
|
||||
}
|
||||
|
||||
void SerialManager::loop2() {
|
||||
while (serial->available()) {
|
||||
char ch = serial->read();
|
||||
if (ch == '<') {
|
||||
inCommandPayload = true;
|
||||
bufferLength = 0;
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
else if (inCommandPayload) {
|
||||
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
|
||||
buffer[bufferLength++] = ch;
|
||||
if (ch == '>') {
|
||||
buffer[bufferLength] = '\0';
|
||||
DCCEXParser::parse(serial, buffer, NULL);
|
||||
inCommandPayload = false;
|
||||
break;
|
||||
}
|
||||
while (serial->available()) {
|
||||
char ch = serial->read();
|
||||
if (!inCommandPayload) {
|
||||
if (ch == '<') {
|
||||
inCommandPayload = PAYLOAD_NORMAL;
|
||||
bufferLength = 0;
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
} else { // if (inCommandPayload)
|
||||
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
|
||||
buffer[bufferLength++] = ch;
|
||||
if (inCommandPayload > PAYLOAD_NORMAL) {
|
||||
if (inCommandPayload > 32 + 2) { // String way too long
|
||||
ch = '>'; // we end this nonsense
|
||||
inCommandPayload = PAYLOAD_NORMAL;
|
||||
DIAG(F("Parse error: Unbalanced string"));
|
||||
// fall through to ending parsing below
|
||||
} else if (ch == '"') { // String end
|
||||
inCommandPayload = PAYLOAD_NORMAL;
|
||||
continue; // do not fall through
|
||||
} else
|
||||
inCommandPayload++;
|
||||
}
|
||||
if (inCommandPayload == PAYLOAD_NORMAL) {
|
||||
if (ch == '>') {
|
||||
buffer[bufferLength] = '\0';
|
||||
DCCEXParser::parse(serial, buffer, NULL);
|
||||
inCommandPayload = PAYLOAD_FALSE;
|
||||
break;
|
||||
} else if (ch == '"') {
|
||||
inCommandPayload = PAYLOAD_STRING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,6 @@ private:
|
||||
SerialManager * next;
|
||||
byte bufferLength;
|
||||
byte buffer[COMMAND_BUFFER_SIZE];
|
||||
bool inCommandPayload;
|
||||
byte inCommandPayload;
|
||||
};
|
||||
#endif
|
||||
|
@@ -41,5 +41,3 @@ size_t StringBuffer::write(uint8_t b) {
|
||||
_buffer[_pos_write]='\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -35,4 +35,4 @@ class StringBuffer : public Print {
|
||||
char _buffer[buffer_max+2];
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020=2025, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -27,6 +27,9 @@ 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, Chris Harlow. All rights reserved.
|
||||
* © 2020-2025, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -30,6 +30,8 @@ class Diag {
|
||||
static bool WITHROTTLE;
|
||||
static bool ETHERNET;
|
||||
static bool LCN;
|
||||
static bool RAILCOM;
|
||||
static bool WEBSOCKET;
|
||||
|
||||
};
|
||||
|
||||
|
26
TemplateForEnums.h
Normal file
26
TemplateForEnums.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* © 2024, Harald Barth. 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 TemplateForEnums
|
||||
#define TemplateForEnums
|
||||
template<class T> inline T operator~ (T a) { return (T)~(int)a; }
|
||||
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
|
||||
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
|
||||
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
|
||||
#endif
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* © 2022 Chris Harlow
|
||||
* © 2022-2025 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2024 Herb Morton
|
||||
@@ -151,8 +151,8 @@ void TrackManager::setDCCSignal( bool on) {
|
||||
HAVE_PORTD(shadowPORTD=PORTD);
|
||||
HAVE_PORTE(shadowPORTE=PORTE);
|
||||
HAVE_PORTF(shadowPORTF=PORTF);
|
||||
HAVE_PORTG(shadowPORTF=PORTG);
|
||||
HAVE_PORTH(shadowPORTF=PORTH);
|
||||
HAVE_PORTG(shadowPORTG=PORTG);
|
||||
HAVE_PORTH(shadowPORTH=PORTH);
|
||||
APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on));
|
||||
HAVE_PORTA(PORTA=shadowPORTA);
|
||||
HAVE_PORTB(PORTB=shadowPORTB);
|
||||
@@ -160,8 +160,8 @@ void TrackManager::setDCCSignal( bool on) {
|
||||
HAVE_PORTD(PORTD=shadowPORTD);
|
||||
HAVE_PORTE(PORTE=shadowPORTE);
|
||||
HAVE_PORTF(PORTF=shadowPORTF);
|
||||
HAVE_PORTG(shadowPORTF=PORTG);
|
||||
HAVE_PORTH(shadowPORTF=PORTH);
|
||||
HAVE_PORTG(PORTG=shadowPORTG);
|
||||
HAVE_PORTH(PORTH=shadowPORTH);
|
||||
}
|
||||
|
||||
// setPROGSignal(), called from interrupt context
|
||||
@@ -173,8 +173,8 @@ void TrackManager::setPROGSignal( bool on) {
|
||||
HAVE_PORTD(shadowPORTD=PORTD);
|
||||
HAVE_PORTE(shadowPORTE=PORTE);
|
||||
HAVE_PORTF(shadowPORTF=PORTF);
|
||||
HAVE_PORTG(shadowPORTF=PORTG);
|
||||
HAVE_PORTH(shadowPORTF=PORTH);
|
||||
HAVE_PORTG(shadowPORTG=PORTG);
|
||||
HAVE_PORTH(shadowPORTH=PORTH);
|
||||
APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on));
|
||||
HAVE_PORTA(PORTA=shadowPORTA);
|
||||
HAVE_PORTB(PORTB=shadowPORTB);
|
||||
@@ -182,8 +182,8 @@ void TrackManager::setPROGSignal( bool on) {
|
||||
HAVE_PORTD(PORTD=shadowPORTD);
|
||||
HAVE_PORTE(PORTE=shadowPORTE);
|
||||
HAVE_PORTF(PORTF=shadowPORTF);
|
||||
HAVE_PORTG(shadowPORTF=PORTG);
|
||||
HAVE_PORTH(shadowPORTF=PORTH);
|
||||
HAVE_PORTG(PORTG=shadowPORTG);
|
||||
HAVE_PORTH(PORTH=shadowPORTH);
|
||||
}
|
||||
|
||||
// setDCSignal(), called from normal context
|
||||
@@ -246,9 +246,6 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
#endif
|
||||
#ifndef DISABLE_PROG
|
||||
if (mode & TRACK_MODE_PROG) {
|
||||
#else
|
||||
if (false) {
|
||||
#endif
|
||||
// only allow 1 track to be prog
|
||||
FOR_EACH_TRACK(t)
|
||||
if ( (track[t]->getMode() & TRACK_MODE_PROG) && t != trackToSet) {
|
||||
@@ -261,6 +258,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
} else {
|
||||
track[trackToSet]->makeProgTrack(false); // only the prog track knows it's type
|
||||
}
|
||||
#endif
|
||||
|
||||
// When a track is switched, we must clear any side effects of its previous
|
||||
// state, otherwise trains run away or just dont move.
|
||||
@@ -334,7 +332,8 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
canDo &= track[t]->trackPWM;
|
||||
}
|
||||
}
|
||||
if (!canDo) {
|
||||
if (canDo) DIAG(F("HA mode"));
|
||||
else {
|
||||
// if we discover that HA mode was globally impossible
|
||||
// we must adjust the trackPWM capabilities
|
||||
FOR_EACH_TRACK(t) {
|
||||
@@ -343,6 +342,7 @@ 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();
|
||||
@@ -358,17 +358,30 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
applyDCSpeed(trackToSet);
|
||||
}
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifndef DISABLE_PROG
|
||||
if (tempProgTrack == trackToSet && oldmode & TRACK_MODE_MAIN && !(mode & TRACK_MODE_PROG)) {
|
||||
// If we just take away the prog track, the join should not
|
||||
// be active either. So do in effect an unjoin
|
||||
//DIAG(F("Unsync"));
|
||||
tempProgTrack = MAX_TRACKS+1;
|
||||
progTrackSyncMain=false;
|
||||
if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,LOW);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
// Turn off power if we changed the mode of this track
|
||||
if (mode != oldmode)
|
||||
if (mode != oldmode) {
|
||||
track[trackToSet]->setPower(POWERMODE::OFF);
|
||||
streamTrackState(NULL,trackToSet);
|
||||
}
|
||||
|
||||
streamTrackState(NULL,trackToSet);
|
||||
//DIAG(F("TrackMode=%d"),mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TrackManager::applyDCSpeed(byte t) {
|
||||
track[t]->setDCSignal(DCC::getThrottleSpeedByte(trackDCAddr[t]),
|
||||
track[t]->setDCSignal(DCC::getLocoSpeedByte(trackDCAddr[t]),
|
||||
DCC::getThrottleFrequency(trackDCAddr[t]));
|
||||
}
|
||||
|
||||
@@ -387,11 +400,15 @@ bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
|
||||
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
|
||||
return false;
|
||||
|
||||
if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
|
||||
if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
|
||||
return setTrackMode(p[0],TRACK_MODE_MAIN);
|
||||
if (params==2 && p[1]=="MAIN_INV"_hk) // <= id MAIN_INV>
|
||||
return setTrackMode(p[0],TRACK_MODE_MAIN_INV);
|
||||
if (params==2 && p[1]=="MAIN_AUTO"_hk) // <= id MAIN_AUTO>
|
||||
return setTrackMode(p[0],TRACK_MODE_MAIN_AUTO);
|
||||
|
||||
#ifndef DISABLE_PROG
|
||||
if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
|
||||
if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
|
||||
return setTrackMode(p[0],TRACK_MODE_PROG);
|
||||
#endif
|
||||
|
||||
@@ -402,20 +419,27 @@ bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
|
||||
return setTrackMode(p[0],TRACK_MODE_EXT);
|
||||
#ifdef BOOSTER_INPUT
|
||||
if (TRACK_MODE_BOOST != 0 && // compile time optimization
|
||||
params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
|
||||
params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
|
||||
return setTrackMode(p[0],TRACK_MODE_BOOST);
|
||||
if (TRACK_MODE_BOOST_INV != 0 && // compile time optimization
|
||||
params==2 && p[1]=="BOOST_INV"_hk) // <= id BOOST_INV>
|
||||
return setTrackMode(p[0],TRACK_MODE_BOOST_INV);
|
||||
if (TRACK_MODE_BOOST_AUTO != 0 && // compile time optimization
|
||||
params==2 && p[1]=="BOOST_AUTO"_hk) // <= id BOOST_AUTO>
|
||||
return setTrackMode(p[0],TRACK_MODE_BOOST_AUTO);
|
||||
#endif
|
||||
if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
|
||||
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV);
|
||||
if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
|
||||
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_AUTO);
|
||||
|
||||
if (params==2 && p[1]=="INV"_hk) // <= id INV>
|
||||
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_INV);
|
||||
if (params==2 && p[1]=="INV"_hk) // <= id INV>
|
||||
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODIFIER_INV);
|
||||
|
||||
if (params==3 && p[1]=="DC"_hk && p[2]>0) // <= id DC cab>
|
||||
if (params==3 && p[1]=="DC"_hk && p[2]>0) // <= id DC cab>
|
||||
return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
|
||||
|
||||
if (params==3 && p[1]=="DCX"_hk && p[2]>0) // <= id DCX cab>
|
||||
return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]);
|
||||
if (params==3 && (p[1]=="DC_INV"_hk || // <= id DC_INV cab>
|
||||
p[1]=="DCX"_hk) && p[2]>0) // <= id DCX cab>
|
||||
return setTrackMode(p[0],TRACK_MODE_DC_INV,p[2]);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -424,9 +448,9 @@ const FSH* TrackManager::getModeName(TRACK_MODE tm) {
|
||||
const FSH *modename=F("---");
|
||||
|
||||
if (tm & TRACK_MODE_MAIN) {
|
||||
if(tm & TRACK_MODE_AUTOINV)
|
||||
if(tm & TRACK_MODIFIER_AUTO)
|
||||
modename=F("MAIN A");
|
||||
else if (tm & TRACK_MODE_INV)
|
||||
else if (tm & TRACK_MODIFIER_INV)
|
||||
modename=F("MAIN I>\n");
|
||||
else
|
||||
modename=F("MAIN");
|
||||
@@ -440,15 +464,15 @@ const FSH* TrackManager::getModeName(TRACK_MODE tm) {
|
||||
else if(tm & TRACK_MODE_EXT)
|
||||
modename=F("EXT");
|
||||
else if(tm & TRACK_MODE_BOOST) {
|
||||
if(tm & TRACK_MODE_AUTOINV)
|
||||
if(tm & TRACK_MODIFIER_AUTO)
|
||||
modename=F("BOOST A");
|
||||
else if (tm & TRACK_MODE_INV)
|
||||
else if (tm & TRACK_MODIFIER_INV)
|
||||
modename=F("BOOST I");
|
||||
else
|
||||
modename=F("BOOST");
|
||||
}
|
||||
else if (tm & TRACK_MODE_DC) {
|
||||
if (tm & TRACK_MODE_INV)
|
||||
if (tm & TRACK_MODIFIER_INV)
|
||||
modename=F("DCX");
|
||||
else
|
||||
modename=F("DC");
|
||||
@@ -686,4 +710,3 @@ TRACK_MODE TrackManager::getMode(byte t) {
|
||||
int16_t TrackManager::returnDCAddr(byte t) {
|
||||
return (trackDCAddr[t]);
|
||||
}
|
||||
|
||||
|
12
Turnouts.cpp
12
Turnouts.cpp
@@ -312,12 +312,6 @@
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
#if defined(DCC_TURNOUTS_RCN_213)
|
||||
const bool DCCTurnout::rcn213Compliant = true;
|
||||
#else
|
||||
const bool DCCTurnout::rcn213Compliant = false;
|
||||
#endif
|
||||
|
||||
// DCCTurnoutData contains data specific to this subclass that is
|
||||
// written to EEPROM when the turnout is saved.
|
||||
struct DCCTurnoutData {
|
||||
@@ -385,7 +379,10 @@
|
||||
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
|
||||
// and Close writes a 0.
|
||||
// RCN-213 specifies that Throw is 0 and Close is 1.
|
||||
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant);
|
||||
#ifndef DCC_TURNOUTS_RCN_213
|
||||
close = !close;
|
||||
#endif
|
||||
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -527,4 +524,3 @@
|
||||
StringFormatter::send(stream, F("<H %d LCN %d>\n"), _turnoutData.id,
|
||||
!_turnoutData.closed);
|
||||
}
|
||||
|
||||
|
@@ -245,8 +245,6 @@ public:
|
||||
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
static Turnout *load(struct TurnoutData *turnoutData);
|
||||
void print(Print *stream) override;
|
||||
// Flag whether DCC Accessory packets are to contain 1=close/0=throw(RCN-213) or 1=throw/0-close (DCC++ Classic)
|
||||
static const bool rcn213Compliant;
|
||||
|
||||
protected:
|
||||
bool setClosedInternal(bool close) override;
|
||||
|
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
|
||||
© 2021 Harald Barth
|
||||
© 2023 Nathan Kellenicki
|
||||
© 2025 Chris Harlow
|
||||
|
||||
|
||||
This file is part of CommandStation-EX
|
||||
|
||||
@@ -23,13 +25,14 @@
|
||||
#include <vector>
|
||||
#include "defines.h"
|
||||
#include "ESPmDNS.h"
|
||||
#include <WiFi.h>
|
||||
#include "esp_wifi.h"
|
||||
#include "WifiESP32.h"
|
||||
#include "DIAG.h"
|
||||
#include "RingStream.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCC.h"
|
||||
#include "Websockets.h"
|
||||
/*
|
||||
#include "soc/rtc_wdt.h"
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -109,10 +112,13 @@ private:
|
||||
bool inUse;
|
||||
};
|
||||
|
||||
// file scope variables
|
||||
static std::vector<NetworkClient> clients; // a list to hold all clients
|
||||
static WiFiServer *server = NULL;
|
||||
static RingStream *outboundRing = new RingStream(10240);
|
||||
static bool APmode = false;
|
||||
// init of static class scope variables
|
||||
bool WifiESP::wifiUp = false;
|
||||
WiFiServer *WifiESP::server = NULL;
|
||||
|
||||
#ifdef WIFI_TASK_ON_CORE0
|
||||
void wifiLoop(void *){
|
||||
@@ -128,6 +134,30 @@ char asciitolower(char in) {
|
||||
return in;
|
||||
}
|
||||
|
||||
void WifiESP::teardown() {
|
||||
// stop all locos
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
// terminate all clients connections
|
||||
while (!clients.empty()) {
|
||||
// pop_back() should invoke destructor which does stop()
|
||||
// on the underlying TCP connction
|
||||
clients.pop_back();
|
||||
}
|
||||
// stop server
|
||||
if (server != NULL) {
|
||||
server->stop();
|
||||
server->close();
|
||||
server->end();
|
||||
DIAG(F("server stop, close, end"));
|
||||
}
|
||||
// terminate MDNS anouncement
|
||||
mdns_service_remove_all();
|
||||
mdns_free();
|
||||
// stop WiFi
|
||||
WiFi.disconnect(true);
|
||||
wifiUp = false;
|
||||
}
|
||||
|
||||
bool WifiESP::setup(const char *SSid,
|
||||
const char *password,
|
||||
const char *hostname,
|
||||
@@ -136,8 +166,10 @@ bool WifiESP::setup(const char *SSid,
|
||||
const bool forceAP) {
|
||||
bool havePassword = true;
|
||||
bool haveSSID = true;
|
||||
bool wifiUp = false;
|
||||
// bool wifiUp = false;
|
||||
uint8_t tries = 40;
|
||||
if (wifiUp)
|
||||
teardown();
|
||||
|
||||
//#ifdef SERIAL_BT_COMMANDS
|
||||
//return false;
|
||||
@@ -349,6 +381,8 @@ 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.
|
||||
@@ -356,25 +390,28 @@ 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;
|
||||
{
|
||||
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';
|
||||
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';
|
||||
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
|
||||
if (Diag::CMD || Diag::WITHROTTLE)
|
||||
DIAG(F("SEND %d:%s"), clientId, buffer);
|
||||
clients[clientId].wifi.write(buffer,count);
|
||||
if (Diag::WIFI)
|
||||
DIAG(F("SEND%S %d:%s"), useWebsocket?F("ws"):F(""),clientId, buffer+wsHeaderLen);
|
||||
clients[clientId].wifi.write(buffer,count+wsHeaderLen);
|
||||
} else {
|
||||
DIAG(F("Unsent(%d): %s"), clientId, buffer);
|
||||
DIAG(F("Unsent(%d): %s"), clientId, buffer+wsHeaderLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@
|
||||
#ifndef WifiESP32_h
|
||||
#define WifiESP32_h
|
||||
|
||||
#include <WiFi.h>
|
||||
#include "FSH.h"
|
||||
|
||||
class WifiESP
|
||||
@@ -36,6 +37,9 @@ public:
|
||||
const bool forceAP);
|
||||
static void loop();
|
||||
private:
|
||||
static void teardown();
|
||||
static bool wifiUp;
|
||||
static WiFiServer *server;
|
||||
};
|
||||
#endif //WifiESP8266_h
|
||||
#endif //ESP8266
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020-2025 Chris Harlow
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "RingStream.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "DIAG.h"
|
||||
#include "Websockets.h"
|
||||
|
||||
WifiInboundHandler * WifiInboundHandler::singleton;
|
||||
|
||||
@@ -67,8 +68,13 @@ void WifiInboundHandler::loop1() {
|
||||
|
||||
|
||||
if (pendingCipsend && millis()-lastCIPSEND > CIPSENDgap) {
|
||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), clientPendingCIPSEND, currentReplySize);
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), clientPendingCIPSEND, currentReplySize);
|
||||
// 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);
|
||||
pendingCipsend=false;
|
||||
return;
|
||||
}
|
||||
@@ -80,7 +86,9 @@ void WifiInboundHandler::loop1() {
|
||||
int count=inboundRing->count();
|
||||
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"),clientId,count);
|
||||
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;
|
||||
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
|
||||
|
||||
WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
const char WebSocketKeyName[]="Sec-WebSocket-Key: ";
|
||||
static byte prescanPoint=0;
|
||||
|
||||
while (wifiStream->available()) {
|
||||
int ch = wifiStream->read();
|
||||
|
||||
@@ -112,9 +123,12 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
}
|
||||
|
||||
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++) {
|
||||
int cout=outboundRing->read();
|
||||
if (websocket && (cout=='\n')) cout='\r';
|
||||
wifiStream->write(cout);
|
||||
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
||||
}
|
||||
@@ -195,14 +209,19 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
break;
|
||||
}
|
||||
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
|
||||
loopState=IPD_IGNORE_DATA;
|
||||
if (Diag::WIFI) DIAG(F("Wifi OVERFLOW IGNORING:"));
|
||||
break;
|
||||
}
|
||||
inboundRing->mark(runningClientId);
|
||||
loopState=IPD_DATA;
|
||||
prescanPoint=0;
|
||||
loopState=(dataLength>100)? IPD_PRESCAN: IPD_DATA;
|
||||
break;
|
||||
}
|
||||
dataLength = dataLength * 10 + (ch - '0');
|
||||
@@ -217,6 +236,38 @@ 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 Chris Harlow. All rights reserved.
|
||||
* (c) 2020-2025 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -55,7 +55,8 @@ 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
|
||||
};
|
||||
@@ -67,7 +68,7 @@ class WifiInboundHandler {
|
||||
void purgeCurrentCIPSEND();
|
||||
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 CIPSENDgap=100; // millis() between retries of cipsend.
|
||||
|
@@ -72,8 +72,9 @@ Stream * WifiInterface::wifiStream;
|
||||
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) \
|
||||
|| defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) \
|
||||
|| defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||
#define NUM_SERIAL 2
|
||||
#define NUM_SERIAL 3
|
||||
#define SERIAL1 Serial6
|
||||
#define SERIAL3 Serial2
|
||||
#else
|
||||
#warning This variant of Nucleo not yet explicitly supported
|
||||
#endif
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* © 2022 Paul M. Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2023 Harald Barth
|
||||
* © 2020-2025 Harald Barth
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2023 Nathan Kellenicki
|
||||
@@ -45,15 +45,14 @@ The configuration file for DCC-EX Command Station
|
||||
// the correct resistor could damage the sense pin on your Arduino or destroy
|
||||
// the device.
|
||||
//
|
||||
// DEFINE MOTOR_SHIELD_TYPE BELOW. THESE ARE EXAMPLES. FULL LIST IN MotorDrivers.h
|
||||
// DEFINE MOTOR_SHIELD_TYPE BELOW. THESE ARE EXAMPLES. Full list in MotorDrivers.h
|
||||
//
|
||||
// STANDARD_MOTOR_SHIELD : Arduino Motor shield Rev3 based on the L298 with 18V 2A per channel
|
||||
// POLOLU_MOTOR_SHIELD : Pololu MC33926 Motor Driver (not recommended for prog track)
|
||||
// FUNDUMOTO_SHIELD : Fundumoto Shield, no current sensing (not recommended, no short protection)
|
||||
// FIREBOX_MK1 : The Firebox MK1
|
||||
// FIREBOX_MK1S : The Firebox MK1S
|
||||
// IBT_2_WITH_ARDUINO : Arduino Motor Shield for PROG and IBT-2 for MAIN
|
||||
// EX8874_SHIELD : DCC-EX TI DRV8874 based motor shield
|
||||
// EXCSB1 : DCC-EX CSB-1 hardware
|
||||
// EXCSB1_WITH_EX8874 : DCC-EX CSB-1 hardware with DCC-EX TI DRV8874 shield
|
||||
// NO_SHIELD : CS without any motor shield (as an accessory only CS)
|
||||
// |
|
||||
// +-----------------------v
|
||||
//
|
||||
@@ -81,7 +80,7 @@ The configuration file for DCC-EX Command Station
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// NOTE: Only supported on Arduino Mega
|
||||
// NOTE: Not supported on Arduino Uno or Nano
|
||||
// Set to false if you not even want it on the Arduino Mega
|
||||
//
|
||||
#define ENABLE_WIFI true
|
||||
@@ -116,13 +115,13 @@ The configuration file for DCC-EX Command Station
|
||||
// Your password may not contain ``"'' (double quote, ASCII 0x22).
|
||||
#define WIFI_PASSWORD "Your network passwd"
|
||||
//
|
||||
// WIFI_HOSTNAME: You probably don't need to change this
|
||||
// WIFI_HOSTNAME: You can change this if you have more than one
|
||||
// CS to make them show up with different names on the network.
|
||||
// Otherwise do not touch.
|
||||
#define WIFI_HOSTNAME "dccex"
|
||||
//
|
||||
// WIFI_CHANNEL: If the line "#define ENABLE_WIFI true" is uncommented,
|
||||
// WiFi will be enabled (Mega only). The default channel is set to "1" whether
|
||||
// this line exists or not. If you need to use an alternate channel (we recommend
|
||||
// using only 1,6, or 11) you may change it here.
|
||||
// WIFI_CHANNEL: The default channel is set to "1". If you need to use an
|
||||
// alternate channel (we recommend using only 1,6, or 11) you may change it here.
|
||||
#define WIFI_CHANNEL 1
|
||||
//
|
||||
// WIFI_FORCE_AP: If you'd like to specify your own WIFI_SSID in AP mode, set this
|
||||
@@ -132,8 +131,9 @@ The configuration file for DCC-EX Command Station
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired). This
|
||||
// is not for Wifi. You will then need the Arduino Ethernet library as well
|
||||
// ENABLE_ETHERNET: Set to true if you have an Arduino Ethernet card (wired) based
|
||||
// on the W5100/W5500 ethernet chip or an STM32 CS with builin ethernet like the F429ZI.
|
||||
// This is not for Wifi. You will then need the Arduino Ethernet library as well.
|
||||
//
|
||||
//#define ENABLE_ETHERNET true
|
||||
|
||||
@@ -270,8 +270,9 @@ The configuration file for DCC-EX Command Station
|
||||
// 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
|
||||
// (throw turnout).
|
||||
//#define DCC_ACCESSORY_RCN_213
|
||||
//
|
||||
//#define DCC_ACCESSORY_COMMAND_REVERSE
|
||||
|
||||
|
||||
// HANDLING MULTIPLE SERIAL THROTTLES
|
||||
// The command station always operates with the default Serial port.
|
||||
// Diagnostics are only emitted on the default serial port and not broadcast.
|
||||
@@ -335,3 +336,17 @@ The configuration file for DCC-EX Command Station
|
||||
//#define SABERTOOTH 1
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SENSORCAM
|
||||
// ESP32-CAM based video sensors require #define to use appropriate base vpin number.
|
||||
//#define SENSORCAM_VPIN 700
|
||||
// To bypass vPin number, define CAM for ex-rail use e.g. AT(CAM 012) for S12 etc.
|
||||
//#define CAM SENSORCAM_VPIN+
|
||||
//
|
||||
//#define SENSORCAM2_VPIN 600 //define other CAM's if installed.
|
||||
//#define CAM2 SENSORCAM2_VPIN+ //for EX-RAIL commands e.g. IFLT(CAM2 020,1)
|
||||
//
|
||||
// For smoother power-up, when using the CAM, you may need a STARTUP_DELAY.
|
||||
// That is described further above.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
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
|
||||
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
|
||||
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%
|
||||
|
||||
set PATH="C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\";%PATH%
|
||||
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
|
||||
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%
|
||||
EXIT
|
||||
|
124
platformio.ini
124
platformio.ini
@@ -11,19 +11,11 @@
|
||||
[platformio]
|
||||
default_envs =
|
||||
mega2560
|
||||
uno
|
||||
unowifiR2
|
||||
nano
|
||||
samd21-dev-usb
|
||||
samd21-zero-usb
|
||||
ESP32
|
||||
Nucleo-F411RE
|
||||
Nucleo-F446RE
|
||||
Teensy3_2
|
||||
Teensy3_5
|
||||
Teensy3_6
|
||||
Teensy4_0
|
||||
Teensy4_1
|
||||
; uno
|
||||
; nano
|
||||
; ESP32
|
||||
; Nucleo-F411RE
|
||||
; Nucleo-F446RE
|
||||
src_dir = .
|
||||
include_dir = .
|
||||
|
||||
@@ -104,10 +96,35 @@ lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
SPI
|
||||
MDNS_Generic
|
||||
|
||||
lib_ignore = WiFi101
|
||||
WiFi101_Generic
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags =
|
||||
|
||||
[env:mega2560-eth]
|
||||
platform = atmelavr
|
||||
board = megaatmega2560
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
MDNS_Generic
|
||||
SPI
|
||||
lib_ignore = WiFi101
|
||||
WiFi101_Generic
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
[env:mega328]
|
||||
platform = atmelavr
|
||||
board = uno
|
||||
@@ -177,7 +194,7 @@ monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
[env:Nucleo-F411RE]
|
||||
platform = ststm32
|
||||
platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f411re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@@ -186,7 +203,7 @@ monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
[env:Nucleo-F446RE]
|
||||
platform = ststm32
|
||||
platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@@ -198,7 +215,7 @@ monitor_echo = yes
|
||||
; tested as yet
|
||||
;
|
||||
[env:Nucleo-F401RE]
|
||||
platform = ststm32
|
||||
platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f401re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
@@ -211,7 +228,7 @@ monitor_echo = yes
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
; [env:Nucleo-F413ZH]
|
||||
; platform = ststm32
|
||||
; platform = ststm32 @ 19.0.0
|
||||
; board = nucleo_f413zh
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
@@ -222,21 +239,21 @@ monitor_echo = yes
|
||||
; Commented out by default as the F446ZE needs variant files
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
; [env:Nucleo-F446ZE]
|
||||
; platform = ststm32
|
||||
; board = nucleo_f446ze
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
; monitor_speed = 115200
|
||||
; monitor_echo = yes
|
||||
[env:Nucleo-F446ZE]
|
||||
platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446ze
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
; Commented out by default as the F412ZG needs variant files
|
||||
; installed before you can let PlatformIO see this
|
||||
;
|
||||
; [env:Nucleo-F412ZG]
|
||||
; platform = ststm32
|
||||
; board = blah_f412zg
|
||||
; platform = ststm32 @ 19.0.0
|
||||
; board = nucleo_f412zg
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
@@ -246,18 +263,47 @@ monitor_echo = yes
|
||||
|
||||
; Experimental - Ethernet work still in progress
|
||||
;
|
||||
; [env:Nucleo-F429ZI]
|
||||
; platform = ststm32
|
||||
; board = nucleo_f429zi
|
||||
; framework = arduino
|
||||
; lib_deps = ${env.lib_deps}
|
||||
; arduino-libraries/Ethernet @ ^2.0.1
|
||||
; stm32duino/STM32Ethernet @ ^1.3.0
|
||||
; stm32duino/STM32duino LwIP @ ^2.1.2
|
||||
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
; monitor_speed = 115200
|
||||
; monitor_echo = yes
|
||||
; upload_protocol = stlink
|
||||
[env:Nucleo-F429ZI]
|
||||
platform = ststm32 @ 19.0.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
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
|
||||
; Experimental - Ethernet work still in progress
|
||||
;
|
||||
[env:Nucleo-F439ZI]
|
||||
platform = ststm32 @ 19.0.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
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
|
||||
[env:Teensy3_2]
|
||||
platform = teensy
|
||||
|
54
version.h
54
version.h
@@ -3,7 +3,59 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
#define VERSION "5.2.77"
|
||||
#define VERSION "5.5.14"
|
||||
// 5.5.14 - DCC Non-blocking packet queue with priority
|
||||
// 5.5.13 - Update STM32duino core to v19.0.0. for updated PeripheralPins.c in preparation for F429/439ZI Ethernet support
|
||||
// 5.5.12 - Websocket support (wifi only)
|
||||
// 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
|
||||
// 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.2.96 - EXRAIL additions XFWD() and XREV()
|
||||
// 5.2.95 - Release candidate for 5.4
|
||||
// 5.2.94 - Bugfix: Less confusion and simpler code around the RCN213 defines
|
||||
// 5.2.93 - Bugfix ESP32: clear progTrackSyncMain (join flag) when prog track is removed
|
||||
// 5.2.92 - Bugfix: FADE power off fix, EXRAIL power diagnostic fix.
|
||||
// 5.2.91 - Bugfix: Neopixel I2C overlap check
|
||||
// 5.2.90 - Bugfix: EXRAIL EXTT_TURNTABLE() now has description as optional in line with ocumentation (also fixed DCC_TURNTABLE)
|
||||
// 5.2.89 - EXRAIL SET(vpin[,npins]) RESET(vpin,[,npins]) pin range manipulation
|
||||
// 5.2.88 - Fix bug where EX-Turntable objects return incorrect angle for home with <JP x>
|
||||
// 5.2.87 - CamParser and IO_EXSensorCam driver
|
||||
// 5.2.86 - IO_TCA8418 driver for keypad matrix input now fully functioning, including being able to use an interrupt pin
|
||||
// 5.2.85 - IO_TM1638 driver, SEG7 Exrail macro and _s7 segment pattern generator.
|
||||
// 5.2.84 - Fix TrackManager setDCCSignal and setPROGSignal for STM32 shadowing of PORTG/PORTH - this time it really is correct!
|
||||
// 5.2.83 - Various STM32 related fixes for serial ports, I2C pullups now turned off, and shadowing of PORTG/PORTH for TrackManager now correct
|
||||
// 5.2.82 - TrackManager and EXRAIL: Introduce more consistent names for <= ...> and SET_TRACK
|
||||
// 5.2.81 - STM32 Ethernet boards support, also now have specific EX8874 motor driver definition
|
||||
// 5.2.80 - EthernetInterface upgrade, including STM32 Ethernet support
|
||||
// 5.2.79 - serial manager loop that handles quoted strings
|
||||
// - WiFiESP32 reconfig
|
||||
// 5.2.78 - NeoPixel support.
|
||||
// - <o command
|
||||
// - HAL driver
|
||||
// - EXRAIL NEOPIXEL and NEOPIXEL_SIGNAL
|
||||
// 5.2.77 - Withrottle: Implement "force function" subcommand "f"
|
||||
// 5.2.76 - Bugfix: EXRAIL: Catch CV read errors in the callback
|
||||
// 5.2.75 - Bugfix: Serial lines 4 to 6 OK
|
||||
|
Reference in New Issue
Block a user