1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-29 10:23:45 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Harald Barth
88caef89f1 version 2024-10-20 01:26:26 +02:00
Harald Barth
f7496b7853 add HEARTBEAT_CRITICAL to enforce heartbeat check 2024-10-20 01:23:17 +02:00
75 changed files with 464 additions and 2140 deletions

View File

@@ -1,95 +0,0 @@
//sensorCAM parser.cpp version 3.03 Sep 2024
#include "CamParser.h"
#include "FSH.h"
#include "IO_EXSensorCAM.h"
#ifndef SENSORCAM_VPIN //define CAM vpin (700?) in config.h
#define SENSORCAM_VPIN 0
#endif
#define CAM_VPIN SENSORCAM_VPIN
#ifndef SENSORCAM2_VPIN
#define SENSORCAM2_VPIN CAM_VPIN
#endif
#ifndef SENSORCAM3_VPIN
#define SENSORCAM3_VPIN 0
#endif
const int CAMVPINS[] = {CAM_VPIN,SENSORCAM_VPIN,SENSORCAM2_VPIN,SENSORCAM3_VPIN};
const int16_t ver=30177;
const int16_t ve =2899;
VPIN EXSensorCAM::CAMBaseVpin = CAM_VPIN;
bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
(void)stream; // probably unused parameter
VPIN vpin=EXSensorCAM::CAMBaseVpin; //use current CAM selection
if (paramCount==0) {
DIAG(F("vpin:%d EXSensorCAMs defined at Vpins #1@ %d #2@ %d #3@ %d"),vpin,CAMVPINS[1],CAMVPINS[2],CAMVPINS[3]);
return true;
}
uint8_t camop=p[0]; // cam oprerator
int param1=0;
int16_t param3=9999; // =0 could invoke parameter changes. & -1 gives later errors
if(camop=='C'){
if(p[1]>=100) EXSensorCAM::CAMBaseVpin=p[1];
if(p[1]<4) EXSensorCAM::CAMBaseVpin=CAMVPINS[p[1]];
DIAG(F("CAM base Vpin: %c %d "),p[0],EXSensorCAM::CAMBaseVpin);
return true;
}
if (camop<100) { //switch CAM# if p[1] dictates
if(p[1]>=100 && p[1]<400) { //limits to CAM# 1 to 3 for now
vpin=CAMVPINS[p[1]/100];
EXSensorCAM::CAMBaseVpin=vpin;
DIAG(F("switching to CAM %d baseVpin:%d"),p[1]/100,vpin);
p[1]=p[1]%100; //strip off CAM #
}
}
if (EXSensorCAM::CAMBaseVpin==0) return false; // no cam defined
// send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser
switch(paramCount) {
case 1: //<N ver> produces '^'
if((p[0] == ve) || (p[0] == ver) || (p[0] == 'V')) camop='^';
if (STRCHR_P((const char *)F("EFGMQRVW^"),camop) == nullptr) return false;
if (camop=='Q') param3=10; //<NQ> for activation state of all 10 banks of sensors
if (camop=='F') camop=']'; //<NF> for Reset/Finish webCAM.
break; // F Coded as ']' else conflicts with <Nf %%>
case 2: //<N camop p1>
if (STRCHR_P((const char *)F("ABFILMNOPQRSTUV"),camop)==nullptr) return false;
param1=p[1];
break;
case 3: //<N vpin rowY colx > or <N cmd p1 p2>
camop=p[0];
if (p[0]>=100) { //vpin - i.e. NOT 'A' through 'Z'
if (p[1]>236 || p[1]<0) return false; //row
if (p[2]>316 || p[2]<0) return false; //column
camop=0x80; // special 'a' case for IO_SensorCAM
vpin = p[0];
}else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false;
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;
}

View File

@@ -1,12 +0,0 @@
#ifndef CamParser_H
#define CamParser_H
#include <Arduino.h>
#include "IODevice.h"
class CamParser {
public:
static bool parseN(Print * stream, byte paramCount, int16_t p[]);
};
#endif

View File

@@ -280,9 +280,6 @@ void CommandDistributor::broadcastPower() {
state = '1';
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
@@ -291,7 +288,7 @@ void CommandDistributor::broadcastPower() {
const FSH * reason=F("");
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
@@ -302,6 +299,9 @@ void CommandDistributor::broadcastPower() {
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
@@ -323,11 +323,11 @@ void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
}
void CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
}
void CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
}
@@ -377,3 +377,4 @@ void CommandDistributor::setVirtualLCDSerial(Print * stream) {
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
byte CommandDistributor::virtualLCDClient=0xFF;
byte CommandDistributor::rememberVLCDClient=0;

View File

@@ -59,8 +59,8 @@ public :
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId);
static void broadcastRouteState(int16_t routeId,byte state);
static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
static void broadcastRouteState(uint16_t routeId,byte state);
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
static void broadcastMessage(char * message);
// Handling code for virtual LCD receiver.

View File

@@ -51,12 +51,6 @@
#include "DCCEX.h"
#include "Display_Implementation.h"
#ifdef ARDUINO_ARCH_ESP32
#include "Sniffer.h"
#include "DCCDecoder.h"
Sniffer *dccSniffer = NULL;
bool DCCDecoder::active = false;
#endif // ARDUINO_ARCH_ESP32
#ifdef CPU_TYPE_ERROR
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
@@ -130,11 +124,6 @@ void setup()
// Start RMFT aka EX-RAIL (ignored if no automnation)
RMFT::begin();
#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
dccSniffer = new Sniffer(BOOSTER_INPUT);
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
@@ -152,28 +141,25 @@ void setup()
CommandDistributor::broadcastPower();
}
void loop()
/**************** for future reference
void looptimer(unsigned long timeout, const FSH* message)
{
#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
static bool oldactive = false;
if (dccSniffer) {
bool newactive = dccSniffer->inputActive();
if (oldactive != newactive) {
RMFT2::railsyncEvent(newactive);
oldactive = newactive;
}
DCCPacket p = dccSniffer->fetchPacket();
if (p.len() != 0) {
if (DCCDecoder::parse(p)) {
if (Diag::SNIFFER)
p.print();
}
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;
}
}
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32
lasttimestamp = now;
}
*********************************************/
void loop()
{
// The main sketch has responsibilities during loop()
// Responsibility 1: Handle DCC background processes

50
DCC.cpp
View File

@@ -229,9 +229,15 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
auto currentValue=getFn(cab,functionNumber);
if (currentValue<0) return; // function not valid for change
setFn(cab,functionNumber, currentValue?false:true);
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);
}
// Report function state (used from withrottle protocol)
@@ -289,7 +295,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
// the initial decoders were orgnized and that influenced how the DCC
// standard was made.
#ifdef DIAG_IO
DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
#endif
// use masks to detect wrong values and do nothing
if(address != (address & 511))
@@ -523,7 +529,6 @@ const ackOp FLASH LOCO_ID_PROG[] = {
V0, WACK, MERGE,
V0, WACK, MERGE,
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
BAD20SKIP, // detect invalid cv20 value and ignore
STASHLOCOID, // keep cv 20 until we have cv19 as well.
SETCV, (ackOp)19,
STARTMERGE, // Setup to read cv 19
@@ -629,9 +634,7 @@ const ackOp FLASH CONSIST_ID_PROG[] = {
BASELINE,
SETCV,(ackOp)20,
SETBYTEH, // high byte to CV 20
WB,WACK,ITSKIP,
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET,
WB,WACK, // ignore dedcoder without cv20 support
SETCV,(ackOp)19,
SETBYTEL, // low byte of word
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
@@ -762,15 +765,7 @@ void DCC::issueReminders() {
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
// Move to next loco slot. If occupied, send a reminder.
int reg = lastLocoReminder+1;
if (reg > highestUsedReg) {
if (loopStatus == 0 /*only needed if numLocos == 1 but we do not have a counter*/) {
// insert idle packet in the speed packet loop to fullfill the *censored*
// >5ms between packets to same decoder rule
const byte idlepacket[] = {0xFF, 0x00};
DCCWaveform::mainTrack.schedulePacket(idlepacket, 2, 0);
}
reg = 0; // Go to start of table
}
if (reg > highestUsedReg) reg = 0; // Go to start of table
if (speedTable[reg].loco > 0) {
// have found loco to remind
if (issueReminder(reg))
@@ -791,23 +786,40 @@ bool DCC::issueReminder(int reg) {
break;
case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1)
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
#else
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),2);
flags&= ~FN_GROUP_1; // dont send them again
#endif
break;
case 2: // remind function group 2 F5-F8
if (flags & FN_GROUP_2)
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
#else
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),2);
flags&= ~FN_GROUP_2; // dont send them again
#endif
break;
case 3: // remind function group 3 F9-F12
if (flags & FN_GROUP_3)
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
#else
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),2);
flags&= ~FN_GROUP_3; // dont send them again
#endif
break;
case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),0);
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again
break;
case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),0);
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again
break;
}
loopStatus++;

View File

@@ -67,24 +67,16 @@ 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) {
if (ackManagerRejoin) {
DIAG(F("Joined but no Prog track"));
TrackManager::setJoin(false);
}
TrackManager::setJoin(ackManagerRejoin);
callback(-3); // we dont have a prog track!
return;
}
@@ -347,20 +339,6 @@ void DCCACK::loop() {
opcode=GETFLASH(ackManagerProg);
}
break;
case BAD20SKIP:
if (ackManagerByte > 120) {
// skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
if (Diag::ACK) DIAG(F("XX cv20=%d "),ackManagerByte);
while (opcode!=SKIPTARGET) {
ackManagerProg++;
opcode=GETFLASH(ackManagerProg);
}
}
break;
case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it
if (ackManagerByte==0) break;
callback(-4);
return;
case SKIPTARGET:
break;
default:
@@ -505,3 +483,4 @@ 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
}

View File

@@ -58,8 +58,6 @@ enum ackOp : byte
ITSKIP, // skip to SKIPTARGET if ack true
NAKSKIP, // skip to SKIPTARGET if ack false
COMBINE1920, // combine cvs 19 and 20 and callback
BAD20SKIP, // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET = 0xFF // jump to target
};

View File

@@ -1,178 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include "DCCDecoder.h"
#include "LocoTable.h"
#include "DCCEXParser.h"
#include "DIAG.h"
#include "DCC.h"
bool DCCDecoder::parse(DCCPacket &p) {
if (!active)
return false;
const byte DECODER_MOBILE = 1;
const byte DECODER_ACCESSORY = 2;
byte decoderType = 0; // use 0 as none
byte *d = p.data();
byte *instr = 0; // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
uint16_t addr; // will be set to decoder addr (long/shor mobile or accessory)
bool locoInfoChanged = false;
if (d[0] == 0B11111111) { // Idle packet
return false;
}
// CRC verification here
byte checksum = 0;
for (byte n = 0; n < p.len(); n++)
checksum ^= d[n];
if (checksum) { // Result should be zero, if not it's an error!
if (Diag::SNIFFER) {
DIAG(F("Checksum error"));
p.print();
}
return false;
}
/*
Serial.print("< ");
for(int n=0; n<8; n++) {
Serial.print(d[0]&(1<<n)?"1":"0");
}
Serial.println(" >");
*/
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
decoderType = DECODER_MOBILE;
instr = d+1;
addr = d[0];
} else {
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
decoderType = DECODER_MOBILE;
instr = d+2;
addr = 256 * (d[0] & 0B00111111) + d[1];
} else { // bit7 == 1 and bit 6 == 0
decoderType = DECODER_ACCESSORY;
instr = d+1;
addr = d[0] & 0B00111111;
}
}
if (decoderType == DECODER_MOBILE) {
switch (instr[0] & 0xE0) {
case 0x20: // 001x-xxxx Extended commands
if (instr[0] == 0B00111111) { // 128 speed steps
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[1])) == true) {
byte speed = instr[1] & 0B01111111;
byte direction = instr[1] & 0B10000000;
DCC::setThrottle(addr, speed, direction);
//DIAG(F("UPDATE"));
// send speed change to DCCEX here
}
}
break;
case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
case 0x60: // 011x-xxxx
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[0] & 0B00111111)) == true) {
byte speed = instr[0] & 0B00001111; // first only look at 4 bits
if (speed > 1) { // neither stop nor emergency stop, recalculate speed
speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
speed = (speed - 3) * 9/2;
}
byte direction = instr[0] & 0B00100000;
DCC::setThrottle(addr, speed, direction);
}
break;
case 0x80: // 100x-xxxx Function group 1
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 1)) == true) {
byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
DCCEXParser::funcmap(addr, normalized, 0, 4);
}
break;
case 0xA0: // 101x-xxxx Function group 3 and 2
{
byte low, high;
if (bitRead(instr[0], 4)) {
low = 5;
high = 8;
} else {
low = 9;
high = 12;
}
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], low)) == true) {
DCCEXParser::funcmap(addr, instr[0], low, high);
}
}
break;
case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
switch (instr[0] & 0B00011111) {
case 0B00011110: // F13-F20 Function Control
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 13)) == true) {
DCCEXParser::funcmap(addr, instr[1], 13, 20);
}
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 17)) == true) {
DCCEXParser::funcmap(addr, instr[1], 13, 20);
}
break;
case 0B00011111: // F21-F28 Function Control
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1], 21)) == true) {
DCCEXParser::funcmap(addr, instr[1], 21, 28);
} // updateFunc handles only the 4 low bits as that is the most common case
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1]>>4, 25)) == true) {
DCCEXParser::funcmap(addr, instr[1], 21, 28);
}
break;
/* do that later
case 0B00011000: // F29-F36 Function Control
break;
case 0B00011001: // F37-F44 Function Control
break;
case 0B00011010: // F45-F52 Function Control
break;
case 0B00011011: // F53-F60 Function Control
break;
case 0B00011100: // F61-F68 Function Control
break;
*/
}
break;
case 0xE0: // 111x-xxxx Config vars
break;
}
return locoInfoChanged;
}
if (decoderType == DECODER_ACCESSORY) {
if (instr[0] & 0B10000000) { // Basic Accessory
addr = (((~instr[0]) & 0B01110000) << 2) + addr;
byte port = (instr[0] & 0B00000110) >> 1;
byte activate = (instr[0] & 0B00001000) >> 3;
byte coil = (instr[0] & 0B00000001);
locoInfoChanged = true;
//(void)addr; (void)port; (void)coil; (void)activate;
//DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
DCC::setAccessory(addr, port, coil, activate);
} else { // Accessory Extended NMRA spec, do we need to decode this?
/*
addr = (addr << 5) +
((instr[0] & 0B01110000) >> 2) +
((instr[0] & 0B00000110) >> 1);
*/
}
return locoInfoChanged;
}
return false;
}
#endif // ARDUINO_ARCH_ESP32

View File

@@ -1,30 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include <Arduino.h>
#include "DCCPacket.h"
class DCCDecoder {
public:
static bool parse(DCCPacket &p);
static inline void onoff(bool on) {active = on;};
private:
static bool active;
};
#endif // ARDUINO_ARCH_ESP32

View File

@@ -3,7 +3,7 @@
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021-2024 Herb Morton
* © 2020-2025 Harald Barth
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
@@ -117,10 +117,8 @@ 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"
#include "DCCDecoder.h"
#endif
// This macro can't be created easily as a portable function because the
@@ -168,10 +166,8 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd,
break;
if (hot == '\0')
return -1;
if (hot == '>') {
*remainingCmd = '\0'; // terminate the cmd string with 0 instead of '>'
if (hot == '>')
return parameterCount;
}
state = 2;
continue;
@@ -268,22 +264,17 @@ void DCCEXParser::parse(const FSH * cmd) {
// See documentation on DCC class for info on this section
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
// This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
// found is true first after the leading "<" has been passed which results
// in parseOne() getting c="C OMM AND>"
byte *cForLater = NULL;
// This function can get stings of the form "<C OMM AND>" or "C OMM AND"
// found is true first after the leading "<" has been passed
bool found = (com[0] != '<');
for (byte *c=com; c[0] != '\0'; c++) {
if (found) {
cForLater = c;
parseOne(stream, c, ringStream);
found=false;
}
if (c[0] == '<') {
if (cForLater) parseOne(stream, cForLater, ringStream);
if (c[0] == '<')
found = true;
}
}
if (cForLater) parseOne(stream, cForLater, ringStream);
}
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
@@ -410,8 +401,7 @@ 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
// Because of earlier confusion we need to do the same thing under both defines
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
#else
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
@@ -467,7 +457,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
else IODevice::write(-p[0],LOW);
return;
}
if (params>=2 && params<=4) { // <z vpin analog profile duration>
if (params>=2 && params<=4) { // <z vpin ana;og profile duration>
// unused params default to 0
IODevice::writeAnalogue(p[0],p[1],p[2],p[3]);
return;
@@ -684,14 +674,6 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'C': // CONFIG <C [params]>
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
bool on = false;
if (params>1 && p[1] == "ON"_hk) {
on = true;
}
DCCDecoder::onoff(on);
return;
}
#if defined(HAS_ENOUGH_MEMORY)
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
@@ -894,11 +876,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
return;
break;
#endif
#ifndef IO_NO_HAL
case 'N': // <N commands for SensorCam
if (CamParser::parseN(stream,params,p)) return;
break;
#endif
case '/': // implemented in EXRAIL parser
case 'L': // LCC interface implemented in EXRAIL parser
break; // Will <X> if not intercepted by EXRAIL
@@ -1243,9 +1221,6 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
case "LCN"_hk: // <D LCN ON/OFF>
Diag::LCN = onOff;
return true;
case "SNIFFER"_hk: // <D SNIFFER ON/OFF>
Diag::SNIFFER = onOff;
return true;
#endif
#ifndef DISABLE_EEPROM
case "EEPROM"_hk: // <D EEPROM NumEntries>
@@ -1464,7 +1439,6 @@ void DCCEXParser::callback_Wloco(int16_t result)
void DCCEXParser::callback_Wconsist(int16_t result)
{
if (result==-4) DIAG(F("Long Consist %d not supported by decoder"),stashP[1]);
if (result==1) result=stashP[1]; // pick up original requested id from command
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));

View File

@@ -39,7 +39,6 @@ struct DCCEXParser
static void setRMFTFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
private:
@@ -78,6 +77,7 @@ struct DCCEXParser
static FILTER_CALLBACK filterCallback;
static FILTER_CALLBACK filterRMFTCallback;
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[]);
};

View File

@@ -1,83 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifndef DCCPacket_h
#define DCCPacket_h
#include <strings.h>
#include "defines.h"
class DCCPacket {
public:
DCCPacket() {
_len = 0;
_data = NULL;
};
DCCPacket(byte *d, byte l) {
_len = l;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = d[n];
};
DCCPacket(const DCCPacket &old) {
_len = old._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = old._data[n];
};
DCCPacket &operator=(const DCCPacket &rhs) {
if (this == &rhs)
return *this;
delete[]_data;
_len = rhs._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = rhs._data[n];
return *this;
};
~DCCPacket() {
if (_len) {
delete[]_data;
_len = 0;
_data = NULL;
}
};
inline bool operator==(const DCCPacket &right) {
if (_len != right._len)
return false;
if (_len == 0)
return true;
return (bcmp(_data, right._data, _len) == 0);
};
void print() {
static const char hexchars[]="0123456789ABCDEF";
USB_SERIAL.print(F("<* DCCPACKET "));
for (byte n = 0; n< _len; n++) {
USB_SERIAL.print(hexchars[_data[n]>>4]);
USB_SERIAL.print(hexchars[_data[n] & 0x0f]);
USB_SERIAL.print(' ');
}
USB_SERIAL.print(F("*>\n"));
};
inline byte len() {return _len;};
inline byte *data() {return _data;};
private:
byte _len = 0;
byte *_data = NULL;
};
#endif

View File

@@ -44,12 +44,6 @@ 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:

View File

@@ -61,7 +61,6 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
void DCCTimer::startRailcomTimer(byte brakePin) {
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
/* The Railcom timer is started in such a way that it
- First triggers 28uS after the last TIMER1 tick.
This provides an accurate offset (in High Accuracy mode)
@@ -114,13 +113,10 @@ void DCCTimer::startRailcomTimer(byte brakePin) {
// it does seems to get a good answer.
TCNT2=193 + (ICR1 - TCNT1)/8;
#endif
}
void DCCTimer::ackRailcomTimer() {
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
#endif
}
@@ -201,8 +197,6 @@ 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.

View File

@@ -78,17 +78,11 @@ int DCCTimer::freeMemory() {
////////////////////////////////////////////////////////////////////////
#ifdef ARDUINO_ARCH_ESP32
#if __has_include("esp_idf_version.h")
#include "esp_idf_version.h"
#endif
#if ESP_IDF_VERSION_MAJOR == 4
// all well correct IDF version
#else
#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"
#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"
#endif
// protect all the rest of the code from IDF version 5
#if ESP_IDF_VERSION_MAJOR == 4
#include "DIAG.h"
#include <driver/adc.h>
#include <soc/sens_reg.h>
@@ -203,9 +197,7 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
}
void DCCTimer::DCCEXledcDetachPin(uint8_t pin) {
#ifdef DIAG_IO
DIAG(F("Clear pin %d from ledc channel"), pin);
#endif
DIAG(F("Clear pin %d channel"), pin);
pin_to_channel[pin] = 0;
pinMatrixOutDetach(pin, false, false);
}
@@ -330,5 +322,6 @@ void ADCee::scan() {
void ADCee::begin() {
}
#endif //IDF v4
#endif //ESP32

View File

@@ -145,7 +145,7 @@ void DCCWaveform::interrupt2() {
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
if (remainingPreambles==1) promotePendingPacket();
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
// Update free memory diagnostic as we don't have anything else to do this time.
@@ -278,11 +278,7 @@ void DCCWaveform::begin() {
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];
@@ -300,7 +296,13 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
{
int ret = 0;
do {
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
if(isMainTrack) {
if (rmtMainChannel != NULL)
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} else {
if (rmtProgChannel != NULL)
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
}
} while(ret > 0);
}
}

View File

@@ -188,7 +188,6 @@ Display *Display::loop2(bool force) {
#endif
noMoreRowsToDisplay = false;
slot = 0;
_deviceDriver->setRowNative(slot); // Set position for display
lastScrollTime = currentMillis;
return NULL;
}

View File

@@ -1,23 +1,3 @@
/*
* © 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

View File

@@ -4,7 +4,6 @@
* © 2021-2023 Harald Barth
* © 2020-2023 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -88,10 +87,6 @@ LookList * RMFT2::onClockLookup=NULL;
LookList * RMFT2::onRotateLookup=NULL;
#endif
LookList * RMFT2::onOverloadLookup=NULL;
#ifdef BOOSTER_INPUT
LookList * RMFT2::onRailSyncOnLookup=NULL;
LookList * RMFT2::onRailSyncOffLookup=NULL;
#endif
byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr;
int16_t * RMFT2::stashArray=nullptr;
@@ -208,10 +203,6 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
#endif
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
#ifdef BOOSTER_INPUT
onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
#endif
// onLCCLookup is not the same so not loaded here.
// Second pass startup, define any turnouts or servos, set signals red
@@ -321,7 +312,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,2);
int home=getOperand(progCounter,3);
setTurntableHiddenState(EXTTTurntable::create(id,pin));
Turntable *tto=Turntable::get(id);
tto->addPosition(0,0,home);
@@ -686,14 +677,13 @@ void RMFT2::loop2() {
break;
case OPCODE_SET:
killBlinkOnVpin(operand);
IODevice::write(operand,true);
break;
case OPCODE_RESET:
{
auto count=getOperand(1);
for (uint16_t i=0;i<count;i++) {
killBlinkOnVpin(operand+i);
IODevice::write(operand+i,opcode==OPCODE_SET);
}
}
killBlinkOnVpin(operand);
IODevice::write(operand,false);
break;
case OPCODE_BLINK:
@@ -880,14 +870,6 @@ 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;
@@ -939,9 +921,8 @@ void RMFT2::loop2() {
#ifndef DISABLE_PROG
case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true);
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
break;
case OPCODE_UNJOIN:
@@ -1129,11 +1110,7 @@ void RMFT2::loop2() {
case OPCODE_ONROTATE:
#endif
case OPCODE_ONOVERLOAD:
#ifdef BOOSTER_INPUT
case OPCODE_ONRAILSYNCON:
case OPCODE_ONRAILSYNCOFF:
#endif
break;
default:
@@ -1350,24 +1327,12 @@ 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 + 'A');
DIAG(F("powerEvent : %c"), track);
if (overload) {
onOverloadLookup->handleEvent(F("POWER"),track);
}
}
#ifdef BOOSTER_INPUT
void RMFT2::railsyncEvent(bool on) {
if (Diag::CMD)
DIAG(F("railsyncEvent : %d"), on);
if (on) {
if (onRailSyncOnLookup)
onRailSyncOnLookup->handleEvent(F("RAILSYNCON"), 0);
} else {
if (onRailSyncOffLookup)
onRailSyncOffLookup->handleEvent(F("RAILSYNCOFF"), 0);
}
}
#endif
// This function is used when setting pins so that a SET or RESET
// will cause any blink task on that pin to terminate.
// It will be compiled out of existence if no BLINK feature is used.
@@ -1513,7 +1478,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
}
}
void RMFT2::manageRouteState(int16_t id, byte state) {
void RMFT2::manageRouteState(uint16_t id, byte state) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup
@@ -1525,7 +1490,7 @@ void RMFT2::manageRouteState(int16_t id, byte state) {
CommandDistributor::broadcastRouteState(id,state);
}
}
void RMFT2::manageRouteCaption(int16_t id,const FSH* caption) {
void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup

View File

@@ -3,7 +3,6 @@
* © 2020-2022 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -46,7 +45,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_XFWD,OPCODE_XREV,
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
@@ -73,7 +72,6 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_ACON, OPCODE_ACOF,
OPCODE_ONACON, OPCODE_ONACOF,
OPCODE_ONOVERLOAD,
OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
@@ -189,9 +187,6 @@ 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);
#ifdef BOOSTER_INPUT
static void railsyncEvent(bool on);
#endif
static bool signalAspectEvent(int16_t address, byte aspect );
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
@@ -259,15 +254,12 @@ private:
static LookList * onRotateLookup;
#endif
static LookList * onOverloadLookup;
#ifdef BOOSTER_INPUT
static LookList * onRailSyncOnLookup;
static LookList * onRailSyncOffLookup;
#endif
static const int countLCCLookup;
static int onLCCLookup[];
static const byte compileFeatures;
static void manageRouteState(int16_t id, byte state);
static void manageRouteCaption(int16_t id, const FSH* caption);
static void manageRouteState(uint16_t id, byte state);
static void manageRouteCaption(uint16_t id, const FSH* caption);
static byte * routeStateArray;
static const FSH ** routeCaptionArray;
static int16_t * stashArray;

View File

@@ -2,7 +2,6 @@
* © 2020-2022 Chris Harlow. All rights reserved.
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
*
* This file is part of CommandStation-EX
*
@@ -119,8 +118,6 @@
#undef ONCLOCKTIME
#undef ONCLOCKMINS
#undef ONOVERLOAD
#undef ONRAILSYNCON
#undef ONRAILSYNCOFF
#undef ONGREEN
#undef ONRED
#undef ONROTATE
@@ -197,8 +194,6 @@
#undef XFOFF
#undef XFON
#undef XFTOGGLE
#undef XREV
#undef XFWD
#ifndef RMFT2_UNDEF_ONLY
#define ACTIVATE(addr,subaddr)
@@ -219,12 +214,12 @@
#define BROADCAST(msg)
#define CALL(route)
#define CLEAR_STASH(id)
#define CLEAR_ALL_STASH
#define CLEAR_ALL_STASH(id)
#define CLOSE(id)
#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)
@@ -238,7 +233,7 @@
#define ENDTASK
#define ESTOP
#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)
@@ -290,8 +285,6 @@
#define ONCLOCKTIME(hours,mins)
#define ONCLOCKMINS(mins)
#define ONOVERLOAD(track_id)
#define ONRAILSYNCON
#define ONRAILSYNCOFF
#define ONDEACTIVATE(addr,subaddr)
#define ONDEACTIVATEL(linear)
#define ONCLOSE(turnout_id)
@@ -316,7 +309,7 @@
#define READ_LOCO
#define RED(signal_id)
#define RESERVE(blockid)
#define RESET(pin,count...)
#define RESET(pin)
#define RESUME
#define RETURN
#define REV(speed)
@@ -342,7 +335,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,count...)
#define SET(pin)
#define SET_TRACK(track,mode)
#define SET_POWER(track,onoff)
#define SETLOCO(loco)
@@ -372,7 +365,5 @@
#define XFOFF(cab,func)
#define XFON(cab,func)
#define XFTOGGLE(cab,func)
#define XFWD(cab,speed)
#define XREV(cab,speed)
#endif

View File

@@ -363,3 +363,4 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
return false;
}
}

View File

@@ -3,7 +3,6 @@
* © 2020-2022 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -517,7 +516,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#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((int16_t)PCA9685::ProfileType::UseDuration|(int16_t)PCA9685::ProfileType::NoPowerOff),OPCODE_PAD,V(ms/100L),
#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 FOFF(func) OPCODE_FOFF,V(func),
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
#define FON(func) OPCODE_FON,V(func),
@@ -584,8 +583,6 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
@@ -610,7 +607,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,count...) OPCODE_RESET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
#define RESET(pin) OPCODE_RESET,V(pin),
#define RESUME OPCODE_RESUME,0,0,
#define RETURN OPCODE_RETURN,0,0,
#define REV(speed) OPCODE_REV,V(speed),
@@ -638,7 +635,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,count...) OPCODE_SET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
#define SET(pin) OPCODE_SET,V(pin),
#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),
@@ -668,8 +665,6 @@ 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),
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;

View File

@@ -47,4 +47,4 @@ class EXRAILSensor {
bool onChange;
byte latchDelay;
};
#endif
#endif

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202504182148Z"
#define GITHUB_SHA "devel-heartbeat202410192325ZZ"

View File

@@ -384,4 +384,4 @@ void I2CManagerClass::handleInterrupt() {
}
}
#endif
#endif

View File

@@ -231,4 +231,4 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
***************************************************************************/
void I2CManagerClass::loop() {}
#endif
#endif

View File

@@ -627,3 +627,4 @@ bool ArduinoPins::fastReadDigital(uint8_t pin) {
#endif
return result;
}

View File

@@ -38,7 +38,6 @@
#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.
@@ -572,6 +571,5 @@ protected:
#include "IO_TCA8418.h"
#include "IO_NeoPixel.h"
#include "IO_TM1638.h"
#include "IO_EXSensorCAM.h"
#endif // iodevice_h

View File

@@ -166,4 +166,4 @@ private:
uint8_t _nextState;
};
#endif // io_analogueinputs_h
#endif // io_analogueinputs_h

View File

@@ -65,3 +65,4 @@ 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));
}

View File

@@ -1,425 +0,0 @@
/* 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 305
// 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.
*
* #include "CamParser.h" in DCCEXParser.cpp
* #include "IO_EXSensorCAM.h" in IODevice.h
* To create EX-SensorCAM devices, define them in myHal.cpp: with
* EXSensorCAM::create(baseVpin,num_vpins,i2c_address) or
* alternatively use HAL(EXSensorCAM baseVpin numpins i2c_address) in myAutomation.h
* also #define SENSORCAM_VPIN baseVpin in config.h
*
* void halSetup() {
* // EXSensorCAM::create(vpin, num_vpins, i2c_address);
* EXSensorCAM::create(700, 80, 0x11);
* }
*
* I2C packet size of 32 bytes (in the Wire library).
*/
# define DIGITALREFRESH 20000UL // min uSec delay between digital reads of digitalInputStates
#ifndef IO_EX_EXSENSORCAM_H
#define IO_EX_EXSENSORCAM_H
#define 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);
}
static VPIN CAMBaseVpin;
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);
}
//*************************
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

View File

@@ -141,3 +141,4 @@ const byte _DIR_MASK = 0x30;
void EncoderThrottle::_display() {
DIAG(F("DRIVE vpin %d loco %d notch %d"),_firstVpin,_locoid,_notch);
}

View File

@@ -162,4 +162,4 @@ protected:
};
#endif // IO_EXAMPLESERIAL_H
#endif // IO_EXAMPLESERIAL_H

View File

@@ -262,4 +262,4 @@ public:
};
#endif // IO_HALDisplay_H
#endif // IO_HALDisplay_H

View File

@@ -98,4 +98,4 @@ private:
};
#endif
#endif

View File

@@ -108,4 +108,4 @@ private:
};
#endif
#endif

View File

@@ -129,7 +129,7 @@ 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);
if (checkNoOverlap(vpin, nPins, mode, i2cAddress)) new NeoPixel(vpin, nPins, mode, i2cAddress);
}
private:
@@ -206,7 +206,6 @@ private:
// 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));
@@ -292,7 +291,7 @@ private:
}
void transmit(uint16_t pixel) {
void transmit(uint16_t pixel, bool show=true) {
byte buffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_BUF,0x00,0x00,0x00,0x00,0x00};
uint16_t offset= pixel * _bytesPerPixel;
buffer[2]=(byte)(offset>>8);

View File

@@ -167,4 +167,4 @@ private:
};
#endif
#endif

View File

@@ -101,4 +101,4 @@ private:
uint8_t inputBuffer[1];
};
#endif
#endif

View File

@@ -106,4 +106,4 @@ private:
uint8_t inputBuffer[2];
};
#endif
#endif

View File

@@ -30,3 +30,4 @@
//
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};

View File

@@ -295,4 +295,4 @@ private:
}
};
#endif
#endif

View File

@@ -1,5 +1,5 @@
/*
* © 2023-2024, Paul M. Antoine
* © 2023, Paul M. Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC-EX API
@@ -21,351 +21,164 @@
#ifndef io_tca8418_h
#define io_tca8418_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "IO_GPIOBase.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:
* IODevice subclass for TCA8418 80-key keypad encoder, which we'll treat as 64 of the possible
* 80 inputs for now, in an 8x8 matrix only, although 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 {
class TCA8418 : public GPIOBase<uint64_t> {
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);
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(vpin, nPins, i2cAddress))
// temporarily use the simple 18-pin GPIO mode - we'll switch to 8x8 matrix once this works
new TCA8418(vpin, (nPins = (nPins > 18) ? 18 : 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
TCA8418(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint64_t>((FSH *)F("TCA8418"), vpin, nPins, i2cAddress, interruptPin)
{
// 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
};
uint8_t receiveBuffer[1];
uint8_t commandBuffer[1];
uint8_t status;
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
};
commandBuffer[0] = REG_INT_STAT; // Check interrupt status
status = I2CManager.read(_I2CAddress, receiveBuffer, sizeof(receiveBuffer), commandBuffer, sizeof(commandBuffer));
if (status == I2C_STATUS_OK) {
DIAG(F("TCA8418 Interrupt status was: %x"), receiveBuffer[0]);
}
else
DIAG(F("TCA8418 Interrupt status failed to read!"));
// requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
// outputBuffer, sizeof(outputBuffer));
// outputBuffer[0] = REG_GPIOA;
}
void _writeGpioPort() override {
// I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);
}
void _writePullups() override {
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
// is intended for use as an output but hasn't been written to yet.
uint32_t temp = _portPullup & _portInUse;
(void)temp; // Chris did this so he could see warnings that mattered
// I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8);
}
void _writePortModes() override {
// Write 0 to each GPIO_DIRn for in-use pins that are inputs, 1 for outputs
uint64_t temp = _portMode & _portInUse;
DIAG(F("TCA8418 writing Port Mode: %x, to GPIO_DIRs"), temp);
DIAG(F("TCA8418 writing Port Mode: %x, to GPIO_DIR1"), (temp&0xFF));
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR1, (temp&0xFF));
DIAG(F("TCA8418 writing Port Mode: %x, to GPIO_DIR2"), ((temp&0xFF00)>>8));
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR2, ((temp&0xFF00)>>8));
DIAG(F("TCA8418 writing Port Mode: %x, to GPIO_DIR3"), (temp&0x30000)>>16);
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR3, ((temp&0x30000)>>16));
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
};
// Enable interrupt for in-use pins which are inputs (_portMode=0)
// TCA8418 has interrupt enables per pin, but must be configured for low->high
// or high->low... unlike the MCP23017
temp = ~_portMode & _portInUse;
DIAG(F("TCA8418 writing interrupt Port Mode: %x, to GPIO_INT_ENs"), temp);
DIAG(F("TCA8418 writing interrupt Port Mode: %x, to GPIO_INT_EN1"), (temp&0xFF));
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN1, (temp&0xFF));
DIAG(F("TCA8418 writing interrupt Port Mode: %x, to GPIO_INT_EN2"), ((temp&0xFF00)>>8));
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN2, ((temp&0xFF00)>>8));
DIAG(F("TCA8418 writing interrupt Port Mode: %x, to GPIO_INT_EN3"), (temp&0x30000)>>16);
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN3, ((temp&0x30000)>>16));
// I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);
// I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8);
}
void _readGpioPort(bool immediate) override {
// if (immediate) {
// uint8_t buffer[2];
// I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
// _portInputState = ((uint16_t)buffer[1]<<8) | buffer[0] | _portMode;
// } else {
// // Queue new request
// requestBlock.wait(); // Wait for preceding operation to complete
// // Issue new request to read GPIO register
// I2CManager.queueRequest(&requestBlock);
// }
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
// if (status == I2C_STATUS_OK)
// _portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;
// else
// _portInputState = 0xffff;
}
enum tca8418_lock_ec_fields
void _setupDevice() override {
DIAG(F("TCA8418 setupDevice() called"));
// IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)
// I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);
_writePortModes();
_writePullups();
_writeGpioPort();
}
enum
{
// 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
REG_FIRST_RESERVED = 0x00,
REG_CFG = 0x01,
REG_INT_STAT = 0x02,
REG_KEY_LCK_EC = 0x03,
REG_KEY_EVENT_A = 0x04,
REG_KEY_EVENT_B = 0x05,
REG_KEY_EVENT_C = 0x06,
REG_KEY_EVENT_D = 0x07,
REG_KEY_EVENT_E = 0x08,
REG_KEY_EVENT_F = 0x09,
REG_KEY_EVENT_G = 0x0A,
REG_KEY_EVENT_H = 0x0B,
REG_KEY_EVENT_I = 0x0C,
REG_KEY_EVENT_J = 0x0D,
REG_KP_LCK_TIMER = 0x0E,
REG_UNLOCK1 = 0x0F,
REG_UNLOCK2 = 0x10,
REG_GPIO_INT_STAT1 = 0x11,
REG_GPIO_INT_STAT2 = 0x12,
REG_GPIO_INT_STAT3 = 0x13,
REG_GPIO_DAT_STAT1 = 0x14,
REG_GPIO_DAT_STAT2 = 0x15,
REG_GPIO_DAT_STAT3 = 0x16,
REG_GPIO_DAT_OUT1 = 0x17,
REG_GPIO_DAT_OUT2 = 0x18,
REG_GPIO_DAT_OUT3 = 0x19,
REG_GPIO_INT_EN1 = 0x1A,
REG_GPIO_INT_EN2 = 0x1B,
REG_GPIO_INT_EN3 = 0x1C,
REG_KP_GPIO1 = 0x1D,
REG_KP_GPIO2 = 0x1E,
REG_KP_GPIO3 = 0x1F,
REG_GPI_EM1 = 0x20,
REG_GPI_EM2 = 0x21,
REG_GPI_EM3 = 0x22,
REG_GPIO_DIR1 = 0x23,
REG_GPIO_DIR2 = 0x24,
REG_GPIO_DIR3 = 0x25,
REG_GPIO_INT_LVL1 = 0x26,
REG_GPIO_INT_LVL2 = 0x27,
REG_GPIO_INT_LVL3 = 0x28,
REG_DEBOUNCE_DIS1 = 0x29,
REG_DEBOUNCE_DIS2 = 0x2A,
REG_DEBOUNCE_DIS3 = 0x2B,
REG_GPIO_PULL1 = 0x2C,
REG_GPIO_PULL2 = 0x2D,
REG_GPIO_PULL3 = 0x2E,
REG_LAST_RESERVED = 0x2F,
};
};
#endif
#endif

View File

@@ -213,3 +213,5 @@ void TM1638::test(){
}
}

View File

@@ -131,4 +131,4 @@ protected:
};
#endif // IO_TOUCHKEYPAD_H
#endif // IO_TOUCHKEYPAD_H

View File

@@ -170,4 +170,4 @@ public:
}
};
#endif
#endif

View File

@@ -95,4 +95,4 @@ private:
};
#endif
#endif

View File

@@ -93,4 +93,4 @@ constexpr uint32_t operator""_s7(const char * keyword, size_t len)
{
return CompiletimeSeg7(keyword,0*len,4);
}
#endif
#endif

View File

@@ -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
}
}

View File

@@ -1,130 +0,0 @@
/* Copyright (c) 2023 Harald Barth
*
* This source is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include "LocoTable.h"
LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
int LocoTable::highestUsedReg = 0;
int LocoTable::lookupSpeedTable(int locoId, bool autoCreate) {
// determine speed reg for this loco
int firstEmpty = MAX_LOCOS;
int reg;
for (reg = 0; reg < MAX_LOCOS; reg++) {
if (speedTable[reg].loco == locoId) break;
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
}
// return -1 if not found and not auto creating
if (reg == MAX_LOCOS && !autoCreate) return -1;
if (reg == MAX_LOCOS) reg = firstEmpty;
if (reg >= MAX_LOCOS) {
//DIAG(F("Too many locos"));
return -1;
}
if (reg==firstEmpty){
speedTable[reg].loco = locoId;
speedTable[reg].speedCode=128; // default direction forward
speedTable[reg].groupFlags=0;
speedTable[reg].functions=0;
}
if (reg > highestUsedReg) highestUsedReg = reg;
return reg;
}
// returns false only if loco existed but nothing was changed
bool LocoTable::updateLoco(int loco, byte speedCode) {
if (loco==0) {
/*
// broadcast stop/estop but dont change direction
for (int reg = 0; reg < highestUsedReg; reg++) {
if (speedTable[reg].loco==0) continue;
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
if (speedTable[reg].speedCode != newspeed) {
speedTable[reg].speedCode = newspeed;
CommandDistributor::broadcastLoco(reg);
}
}
*/
return true;
}
// determine speed reg for this loco
int reg=lookupSpeedTable(loco, false);
if (reg>=0) {
speedTable[reg].speedcounter++;
if (speedTable[reg].speedCode!=speedCode) {
speedTable[reg].speedCode = speedCode;
return true;
} else {
return false;
}
} else {
// new
reg=lookupSpeedTable(loco, true);
if(reg >=0) speedTable[reg].speedCode = speedCode;
return true;
}
}
bool LocoTable::updateFunc(int loco, byte func, int shift) {
unsigned long previous;
unsigned long newfunc;
bool retval = false; // nothing was touched
int reg = lookupSpeedTable(loco, false);
if (reg < 0) { // not found
retval = true;
reg = lookupSpeedTable(loco, true);
newfunc = previous = 0;
} else {
newfunc = previous = speedTable[reg].functions;
}
speedTable[reg].funccounter++;
if(shift == 1) { // special case for light
newfunc &= ~1UL;
newfunc |= ((func & 0B10000) >> 4);
}
newfunc &= ~(0B1111UL << shift);
newfunc |= ((func & 0B1111) << shift);
if (newfunc != previous) {
speedTable[reg].functions = newfunc;
retval = true;
}
return retval;
}
void LocoTable::dumpTable(Stream *output) {
output->print("\n-----------Table---------\n");
for (byte reg = 0; reg <= highestUsedReg; reg++) {
if (speedTable[reg].loco != 0) {
output->print(speedTable[reg].loco);
output->print(' ');
output->print(speedTable[reg].speedCode);
output->print(' ');
output->print(speedTable[reg].functions);
output->print(" #funcpacks:");
output->print(speedTable[reg].funccounter);
output->print(" #speedpacks:");
output->print(speedTable[reg].speedcounter);
speedTable[reg].funccounter = 0;
speedTable[reg].speedcounter = 0;
output->print('\n');
}
}
}

View File

@@ -1,44 +0,0 @@
/* Copyright (c) 2023 Harald Barth
*
* This source is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "DCC.h" // fetch MAX_LOCOS from there
class LocoTable {
public:
void forgetLoco(int cab) {
int reg=lookupSpeedTable(cab, false);
if (reg>=0) speedTable[reg].loco=0;
}
static int lookupSpeedTable(int locoId, bool autoCreate);
static bool updateLoco(int loco, byte speedCode);
static bool updateFunc(int loco, byte func, int shift);
static void dumpTable(Stream *output);
private:
struct LOCO
{
int loco;
byte speedCode;
byte groupFlags;
unsigned long functions;
unsigned int funccounter;
unsigned int speedcounter;
};
static LOCO speedTable[MAX_LOCOS];
static int highestUsedReg;
};

View File

@@ -242,7 +242,7 @@ void MotorDriver::setPower(POWERMODE mode) {
// when switching a track On, we need to check the crrentOffset with the pin OFF
if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) {
senseOffset = ADCee::read(currentPin);
if (Diag::ACK) DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
}
IODevice::write(powerPin,invertPower ? LOW : HIGH);

View File

@@ -28,9 +28,12 @@
#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.
// For example TRACK_MODE_DC_INV is (TRACK_MODE_DC|TRACK_MODIFIER_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 {
// main modes
TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,

View File

@@ -1,39 +1,77 @@
# 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.
# 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:
Currently, our products include the following:
* [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.
* [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)
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)
Details of these projects can be found on [our web site](https://dcc-ex.com/).
# Whats in this Repository?
This repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
To utilize this sketch, you can use the following:
1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)
1. (beginner) our [automated installer](https://github.com/DCC-EX/BaseStation-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
Refer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.
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.
**We seriously recommend using the EX-Installer**, however if you choose not to use the installer...
## What's new in CommandStation-EX?
* 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``.
* 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).
# 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

View File

@@ -1,44 +0,0 @@
## 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.

View File

@@ -126,33 +126,29 @@ void SerialManager::loop2() {
buffer[0] = '\0';
}
} else { // if (inCommandPayload)
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) {
buffer[bufferLength++] = ch; // advance bufferLength
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'; // This \0 is after the '>'
DCCEXParser::parse(serial, buffer, NULL); // buffer parsed with trailing '>'
inCommandPayload = PAYLOAD_FALSE;
break;
} else if (ch == '"') {
inCommandPayload = PAYLOAD_STRING;
}
}
} else {
DIAG(F("Parse error: input buffer overflow"));
inCommandPayload = PAYLOAD_FALSE;
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;
}
}
}
}

View File

@@ -1,241 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include "Sniffer.h"
#include "DIAG.h"
//extern Sniffer *DCCSniffer;
static void packeterror() {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
digitalWrite(SNIFFER_LED,HIGH);
#endif
#endif
}
static void clear_packeterror() {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
digitalWrite(SNIFFER_LED,LOW);
#endif
#endif
}
static bool halfbits2byte(uint16_t b, byte *dccbyte) {
/*
if (b!=0 && b!=0xFFFF) {
Serial.print("[ ");
for(int n=0; n<16; n++) {
Serial.print(b&(1<<n)?"1":"0");
}
Serial.println(" ]");
}
*/
for(byte n=0; n<8; n++) {
switch (b & 0x03) {
case 0x01:
case 0x02:
// broken bits
packeterror();
return false;
break;
case 0x00:
bitClear(*dccbyte, n);
break;
case 0x03:
bitSet(*dccbyte, n);
break;
}
b = b>>2;
}
return true;
}
static void IRAM_ATTR blink_diag(int limit) {
#ifndef WIFI_LED
#ifdef SNIFFER_LED
delay(500);
for (int n=0 ; n<limit; n++) {
digitalWrite(SNIFFER_LED,HIGH);
delay(200);
digitalWrite(SNIFFER_LED,LOW);
delay(200);
}
#endif
#endif
}
static bool IRAM_ATTR cap_ISR_cb(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,void *user_data) {
if (edata->cap_edge == MCPWM_BOTH_EDGE) {
// should not happen at all
// delays here might crash sketch
blink_diag(2);
return 0;
}
if (user_data) ((Sniffer *)user_data)->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
//if (DCCSniffer) DCCSniffer->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
return 0;
}
Sniffer::Sniffer(byte snifferpin) {
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, snifferpin);
// set capture edge, BIT(0) - negative edge, BIT(1) - positive edge
// MCPWM_POS_EDGE|MCPWM_NEG_EDGE should be 3.
//mcpwm_capture_enable(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, MCPWM_POS_EDGE|MCPWM_NEG_EDGE, 0);
//mcpwm_isr_register(MCPWM_UNIT_0, sniffer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
//MCPWM0.int_ena.cap0_int_ena = 1; // Enable interrupt on CAP0 signal
mcpwm_capture_config_t MCPWM_cap_config = { //Capture channel configuration
.cap_edge = MCPWM_BOTH_EDGE, // according to mcpwm.h
.cap_prescale = 1, // 1 to 256 (see .h file)
.capture_cb = cap_ISR_cb, // user defined ISR/callback
.user_data = (void *)this // user defined argument to callback
};
#ifndef WIFI_LED
#ifdef SNIFFER_LED
pinMode(SNIFFER_LED ,OUTPUT);
#endif
#endif
blink_diag(3); // so that we know we have SNIFFER_LED
DIAG(F("Init sniffer on pin %d"), snifferpin);
ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &MCPWM_cap_config));
}
#define SNIFFER_TIMEOUT 100L // 100 Milliseconds
bool Sniffer::inputActive(){
unsigned long now = millis();
return ((now - lastendofpacket) < SNIFFER_TIMEOUT);
}
#define DCC_TOO_SHORT 4000L // 4000 ticks are 50usec
#define DCC_ONE_LIMIT 6400L // 6400 ticks are 80usec
void IRAM_ATTR Sniffer::processInterrupt(int32_t capticks, bool posedge) {
byte bit = 0;
diffticks = capticks - lastticks;
if (lastedge != posedge) {
if (diffticks < DCC_TOO_SHORT) {
return;
}
if (diffticks < DCC_ONE_LIMIT) {
bit = 1;
} else {
bit = 0;
}
// update state variables for next round
lastticks = capticks;
lastedge = posedge;
bitfield = bitfield << (uint64_t)1;
bitfield = bitfield + (uint64_t)bit;
// now the halfbit is in the bitfield. Analyze...
if ((bitfield & 0xFFFFFF) == 0xFFFFFC){
// This looks at the 24 last halfbits
// and detects a preamble if
// 22 are ONE and 2 are ZERO which is a
// preabmle of 11 ONES and one ZERO
if (inpacket) {
// if we are already inpacket here we
// got a preamble in the middle of a
// packet
packeterror();
} else {
clear_packeterror(); // everything fine again at end of preable after good packet
}
currentbyte = 0;
dcclen = 0;
inpacket = true;
halfbitcounter = 18; // count 18 steps from 17 to 0 and then look at the byte
return;
}
if (inpacket) {
halfbitcounter--;
if (halfbitcounter) {
return; // wait until we have full byte
} else {
// have reached end of byte
//if (currentbyte == 2) debugfield = bitfield;
byte twohalfbits = bitfield & 0x03;
switch (twohalfbits) {
case 0x01:
case 0x02:
// broken bits
inpacket = false;
packeterror();
return;
break;
case 0x00:
case 0x03:
// byte end
uint16_t b = (bitfield & 0x3FFFF)>>2; // take 18 halfbits and use 16 of them
if (!halfbits2byte(b, dccbytes + currentbyte)) {
// broken halfbits
inpacket = false;
packeterror();
return;
}
if (twohalfbits == 0x03) { // end of packet marker
inpacket = false;
dcclen = currentbyte+1;
debugfield = bitfield;
// We have something we want to give to the outpacket queue
// Check length of outpacket queue
if (outpacket.size() > 3) {
// not good, these should have been fetched
// the arbitraty number to check is THREE (see the holy grail)
// blink_diag(1); DO NOT DO THIS HERE -> will crash
packeterror(); // or what to do better?
// take emergency action:
while (!outpacket.empty()) {
outpacket.pop_front();
}
}
lastendofpacket = millis();
DCCPacket temppacket(dccbytes, dcclen);
if (!(temppacket == prevpacket)) {
// we have something new to offer to the fetch routine
// put it into the outpacket queue
outpacket.push_back(temppacket);
prevpacket = temppacket;
}
return;
}
break;
}
halfbitcounter = 18;
currentbyte++; // everything done for this end of byte
if (currentbyte >= MAXDCCPACKETLEN) {
inpacket = false; // this is an error because we should have retured above
packeterror(); // when endof packet marker was active
}
}
}
} else { // lastedge == posedge
// this should not happen, check later
}
}
/*
static void IRAM_ATTR sniffer_isr_handler(void *) {
DCCSniffer.processInterrupt();
}
*/
#endif // ESP32

View File

@@ -1,76 +0,0 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include <Arduino.h>
#include <list>
#include "driver/mcpwm.h"
#include "soc/mcpwm_struct.h"
#include "soc/mcpwm_reg.h"
#define MAXDCCPACKETLEN 8
#include "DCCPacket.h"
class Sniffer {
public:
Sniffer(byte snifferpin);
void IRAM_ATTR processInterrupt(int32_t capticks, bool posedge);
inline int32_t getTicks() {
noInterrupts();
int32_t i = diffticks;
interrupts();
return i;
};
inline int64_t getDebug() {
noInterrupts();
int64_t i = debugfield;
interrupts();
return i;
};
inline DCCPacket fetchPacket() {
// if there is no new data, this will create a
// packet with length 0 (which is no packet)
DCCPacket p;
noInterrupts();
if (!outpacket.empty()) {
p = outpacket.front();
outpacket.pop_front();
}
interrupts();
return p;
};
bool inputActive();
private:
// keep these vars in processInterrupt only
uint64_t bitfield = 0;
uint64_t debugfield = 0;
int32_t diffticks;
int32_t lastticks;
bool lastedge;
byte currentbyte = 0;
byte dccbytes[MAXDCCPACKETLEN];
byte dcclen = 0;
bool inpacket = false;
// these vars are used as interface to other parts of sniffer
byte halfbitcounter = 0;
std::list<DCCPacket> outpacket;
DCCPacket prevpacket;
volatile unsigned long lastendofpacket = 0; // timestamp millis
};
#endif

View File

@@ -41,3 +41,5 @@ size_t StringBuffer::write(uint8_t b) {
_buffer[_pos_write]='\0';
return 1;
}

View File

@@ -35,4 +35,4 @@ class StringBuffer : public Print {
char _buffer[buffer_max+2];
};
#endif
#endif

View File

@@ -27,7 +27,6 @@ bool Diag::WIFI=false;
bool Diag::WITHROTTLE=false;
bool Diag::ETHERNET=false;
bool Diag::LCN=false;
bool Diag::SNIFFER=false;
void StringFormatter::diag( const FSH* input...) {

View File

@@ -30,7 +30,7 @@ class Diag {
static bool WITHROTTLE;
static bool ETHERNET;
static bool LCN;
static bool SNIFFER;
};
class StringFormatter

View File

@@ -1,26 +0,0 @@
/*
* © 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

View File

@@ -197,7 +197,7 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
}
}
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr, bool offAtChange) {
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
// Remember track mode we came from for later
@@ -246,6 +246,9 @@ 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) {
@@ -258,7 +261,6 @@ 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.
@@ -356,23 +358,11 @@ 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 && offAtChange) {
if (mode != oldmode)
track[trackToSet]->setPower(POWERMODE::OFF);
}
streamTrackState(NULL,trackToSet);
//DIAG(F("TrackMode=%d"),mode);
return true;
}
@@ -666,9 +656,8 @@ void TrackManager::setJoin(bool joined) {
FOR_EACH_TRACK(t) {
if (track[t]->getMode() & TRACK_MODE_PROG) { // find PROG track
tempProgTrack = t; // remember PROG track
setTrackMode(t, TRACK_MODE_MAIN, 0, false); // 0 = no DC loco; false = do not turn off pwr
// then in some cases setPower() is called
// seperately after the setJoin() as well
setTrackMode(t, TRACK_MODE_MAIN);
track[t]->setPower(POWERMODE::ON); // if joined, always on
break; // there is only one prog track, done
}
}
@@ -676,8 +665,12 @@ void TrackManager::setJoin(bool joined) {
if (tempProgTrack != MAX_TRACKS+1) {
// setTrackMode defaults to power off, so we
// need to preserve that state.
setTrackMode(tempProgTrack, TRACK_MODE_PROG, 0, false); // 0 = no DC loco; false = do not turn off pwr
POWERMODE tPTmode = track[tempProgTrack]->getPower(); // get current power status of this track
setTrackMode(tempProgTrack, TRACK_MODE_PROG); // set track mode back to prog
track[tempProgTrack]->setPower(tPTmode); // set power status as it was before
tempProgTrack = MAX_TRACKS+1;
} else {
DIAG(F("Unjoin but no remembered prog track"));
}
}
#endif
@@ -704,3 +697,4 @@ TRACK_MODE TrackManager::getMode(byte t) {
int16_t TrackManager::returnDCAddr(byte t) {
return (trackDCAddr[t]);
}

View File

@@ -71,7 +71,7 @@ class TrackManager {
static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);}
static const int16_t MAX_TRACKS=8;
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0, bool offAtChange=true);
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
static void loop();
static POWERMODE getMainPower();

View File

@@ -312,6 +312,12 @@
*
*************************************************************************************/
#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 {
@@ -379,10 +385,7 @@
// 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.
#ifndef DCC_TURNOUTS_RCN_213
close = !close;
#endif
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close);
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant);
return true;
}
@@ -524,3 +527,4 @@
StringFormatter::send(stream, F("<H %d LCN %d>\n"), _turnoutData.id,
!_turnoutData.closed);
}

View File

@@ -245,6 +245,8 @@ 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;

View File

@@ -1,7 +1,7 @@
/*
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2020-2022 Harald Barth
* © 2020-2024 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Chris Harlow
* All rights reserved.
@@ -97,12 +97,18 @@ WiThrottle::WiThrottle( int wificlientid) {
nextThrottle=firstThrottle;
firstThrottle= this;
clientid=wificlientid;
#ifdef HEARTBEAT_CRITICAL
heartBeatEnable=true; // do not run without heartbeat
#else
heartBeatEnable=false; // until client turns it on
#endif
mostRecentCab=0;
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
}
WiThrottle::~WiThrottle() {
// emergency stop any loco that was controlled from this throttle
eStop();
if (Diag::WITHROTTLE) DIAG(F("Deleting WiThrottle client %d"),this->clientid);
if (firstThrottle== this) {
firstThrottle=this->nextThrottle;
@@ -140,7 +146,12 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
switch (cmd[0]) {
case '*': // heartbeat control
if (cmd[1]=='+') heartBeatEnable=true;
else if (cmd[1]=='-') heartBeatEnable=false;
else if (cmd[1]=='-') {
#ifdef HEARTBEAT_CRITICAL
eStop();
#endif
heartBeatEnable=false;
}
break;
case 'P':
if (cmd[1]=='P' && cmd[2]=='A' ) { //PPA power mode
@@ -303,10 +314,14 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
switch (aval[0]) {
case 'V': // Vspeed
{
int witSpeed=getInt(aval+1);
uint8_t dccSpeed = WiTToDCCSpeed(getInt(aval+1));
#ifdef HEARTBEAT_CRITICAL
if (heartBeatEnable == false) // if there is no heartBeat, keep throttle at
dccSpeed = 1; // emegency stop (dccSpeed 1)
#endif
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));
DCC::setThrottle(myLocos[loco].cab, dccSpeed, DCC::getThrottleDirection(myLocos[loco].cab));
// SetThrottle will cause speed change broadcast
}
}
@@ -394,19 +409,28 @@ void WiThrottle::loop(RingStream * stream) {
wt->checkHeartbeat(stream);
}
void WiThrottle::eStop() {
LOOPLOCOS('*', -1) {
if (myLocos[loco].throttle!='\0') {
if (Diag::WITHROTTLE) DIAG(F("%l eStopping cab %d"),millis(),myLocos[loco].cab);
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab)); // speed 1 is eStop
}
}
}
void WiThrottle::checkHeartbeat(RingStream * stream) {
// if eStop time passed... eStop any locos still assigned to this client and then drop the connection
if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
LOOPLOCOS('*', -1) {
if (myLocos[loco].throttle!='\0') {
if (Diag::WITHROTTLE) DIAG(F("%l eStopping cab %d"),millis(),myLocos[loco].cab);
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab)); // speed 1 is eStop
heartBeat=millis(); // We have just stopped everyting, we don't need to do that again at next loop.
}
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout"), millis(), clientid, ESTOP_SECONDS);
if (missedHeartbeatCounter++ > 10) {
if (Diag::WITHROTTLE) DIAG(F("Too many missed heartbeats, drop connection"));
delete this;
return;
}
// if it does come back, the throttle should re-acquire
delete this;
eStop();
heartBeat=millis(); // We have just stopped everyting, we don't need to do that again at next loop.
// Destructor will take care of estop. If it does come back, the throttle should re-acquire
// delete this;
return;
}
@@ -500,9 +524,9 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
itoa(locoid,addcmd+4,10);
stashInstance->multithrottle(stashStream, (byte *)addcmd);
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
DIAG(F("LocoCallback commit success"));
stashStream->commit();
}

View File

@@ -1,6 +1,7 @@
/*
* © 2021 Mike S
* © 2020-2021 Chris Harlow
* © 2024 Harald Barth
* All rights reserved.
*
* This file is part of Asbelos DCC API
@@ -20,7 +21,7 @@
*/
#ifndef WiThrottle_h
#define WiThrottle_h
#include "defines.h"
#include "RingStream.h"
struct MYLOCO {
@@ -45,9 +46,15 @@ class WiThrottle {
~WiThrottle();
static const int MAX_MY_LOCO=10; // maximum number of locos assigned to a single client
#ifdef HEARTBEAT_CRITICAL
static const int HEARTBEAT_SECONDS=1;
static const int HEARTBEAT_PRELOAD=2;
static const int ESTOP_SECONDS=3;
#else
static const int HEARTBEAT_SECONDS=10; // heartbeat at 10 secs to provide messaging transport
static const int HEARTBEAT_PRELOAD=2; // request fast callback when connecting multiple messages
static const int ESTOP_SECONDS=20; // eStop if no incoming messages for more than 8secs
static const int ESTOP_SECONDS=20; // eStop if no incoming messages for more than 20secs
#endif
static WiThrottle* firstThrottle;
static int getInt(byte * cmd);
static int getLocoId(byte * cmd);
@@ -62,6 +69,7 @@ class WiThrottle {
MYLOCO myLocos[MAX_MY_LOCO];
bool heartBeatEnable;
unsigned long heartBeat;
uint8_t missedHeartbeatCounter = 0;
bool introSent=false;
bool turnoutsSent=false;
bool rosterSent=false;
@@ -75,6 +83,7 @@ class WiThrottle {
void multithrottle(RingStream * stream, byte * cmd);
void locoAction(RingStream * stream, byte* aval, char throttleChar, int cab);
void accessory(RingStream *, byte* cmd);
void eStop();
void checkHeartbeat(RingStream * stream);
void markForBroadcast2(int cab);
void sendIntro(Print * stream);

View File

@@ -1,7 +1,7 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Neil McKechnie
* © 2020-2025 Harald Barth
* © 2020-2023 Harald Barth
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2023 Nathan Kellenicki
@@ -45,14 +45,15 @@ 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
//
@@ -80,7 +81,7 @@ The configuration file for DCC-EX Command Station
/////////////////////////////////////////////////////////////////////////////////////
//
// NOTE: Not supported on Arduino Uno or Nano
// NOTE: Only supported on Arduino Mega
// Set to false if you not even want it on the Arduino Mega
//
#define ENABLE_WIFI true
@@ -115,13 +116,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 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.
// WIFI_HOSTNAME: You probably don't need to change this
#define WIFI_HOSTNAME "dccex"
//
// 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.
// 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.
#define WIFI_CHANNEL 1
//
// WIFI_FORCE_AP: If you'd like to specify your own WIFI_SSID in AP mode, set this
@@ -131,9 +132,8 @@ The configuration file for DCC-EX Command Station
/////////////////////////////////////////////////////////////////////////////////////
//
// 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.
// 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
//
//#define ENABLE_ETHERNET true
@@ -270,9 +270,8 @@ 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_COMMAND_REVERSE
//#define DCC_ACCESSORY_RCN_213
//
// 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.
@@ -336,17 +335,3 @@ 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.
//
/////////////////////////////////////////////////////////////////////////////////////

View File

@@ -12,10 +12,18 @@
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
src_dir = .
include_dir = .

View File

@@ -3,36 +3,8 @@
#include "StringFormatter.h"
#define VERSION "5.4.14"
// 5.4.14 - Bugfix: Keep power status of track when doing join/unjoin, new keep power option for setTrackMode()
// 5.4.13 - Bugfix: SSD1309 OLED controllers artefatcs
// - Less verbose diag by default
// 5.4.12 - Bugfix: Negative route Ids
// 5.4.11 - Feature: Enable sniffer on CSB-1
// 5.4.10 - Bugfix: MEGA DCC waveform starvation (sends too many idles)
// 5.4.9 - Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
// - <W CONSIST longaddr> handles non-compliant decoders which NAK cv 20 writes.
// 5.4.8 - Bugfix: Insert idle packet at end of speed reminder loop; treat all function groups equal
// 5.4.7 - Bugfix: EXRAIL fix CLEAR_ALL_STASH
// 5.4.6 - Bugfix: Do not drop further commands in same packet
// 5.4.5 - ESP32: Better detection of correct IDF version
// - track power is always turned on after setJoin() not by setJoin()
// 5.4.4 - bugfix in parser, input buffer overrun and trailing > that did break <+>
// 5.4.3 - bugfix changeFn for functions 29..31
// 5.4.2 - Reversed turnout bugfix
// 5.4.1 - ESP32 bugfix packet buffer race
// 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
#define VERSION "5.2.86"
// 5.2.86 - add HEARTBEAT_CRITICAL to enforce heartbeat check
// 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