mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-29 18:33:44 +02:00
Compare commits
30 Commits
devel-Ash-
...
v5.5.37-De
Author | SHA1 | Date | |
---|---|---|---|
|
8b7d87d6bd | ||
|
baf6c3036f | ||
|
1f74e3ca6f | ||
|
8558ac6c61 | ||
|
844dac8e3c | ||
|
143c62d6a6 | ||
|
4d8ee2322a | ||
|
3eac6a57b0 | ||
|
d87c3bc87e | ||
|
9adfff8e0a | ||
|
2e0a596afe | ||
|
ad92cc9323 | ||
|
51c4b4d34a | ||
|
e45febc785 | ||
|
e6a829d8ef | ||
|
6a361872e2 | ||
|
e6c6f78bb6 | ||
|
36c773df7a | ||
|
dccb7abac7 | ||
|
1122bf5326 | ||
|
a3455225f4 | ||
|
378452479b | ||
|
94b0a7e364 | ||
|
08ef723a86 | ||
|
ffc0f74312 | ||
|
bdbfc95284 | ||
|
f163e171e4 | ||
|
4cb09e11a5 | ||
|
4f3b7068ca | ||
|
93a3ea49a2 |
@@ -355,11 +355,11 @@ void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter
|
||||
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
|
||||
void CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
|
||||
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
|
||||
void CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
|
||||
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
|
||||
}
|
||||
|
||||
|
@@ -60,8 +60,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(uint16_t routeId,byte state);
|
||||
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
|
||||
static void broadcastRouteState(int16_t routeId,byte state);
|
||||
static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
|
||||
static void broadcastMessage(char * message);
|
||||
|
||||
// Handling code for virtual LCD receiver.
|
||||
|
@@ -31,6 +31,7 @@
|
||||
* © 2020-2021 Chris Harlow, Harald Barth, David Cutting,
|
||||
* Fred Decker, Gregor Baues, Anthony W - Dayton
|
||||
* © 2023 Nathan Kellenicki
|
||||
* © 2025 Herb Morton
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -51,6 +52,12 @@
|
||||
|
||||
#include "DCCEX.h"
|
||||
#include "Display_Implementation.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "Sniffer.h"
|
||||
#include "DCCDecoder.h"
|
||||
Sniffer *dccSniffer = NULL;
|
||||
bool DCCDecoder::active = false;
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
#ifdef CPU_TYPE_ERROR
|
||||
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
|
||||
@@ -109,9 +116,10 @@ void setup()
|
||||
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||
#endif // WIFI_ON
|
||||
#else
|
||||
// ESP32 needs wifi on always
|
||||
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||
#if WIFI_ON
|
||||
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||
#endif // WIFI_ON
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
#if ETHERNET_ON
|
||||
@@ -124,6 +132,11 @@ void setup()
|
||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||
RMFT::begin();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
dccSniffer = new Sniffer(BOOSTER_INPUT);
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
|
||||
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
|
||||
@@ -141,25 +154,28 @@ void setup()
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
|
||||
/**************** for future reference
|
||||
void looptimer(unsigned long timeout, const FSH* message)
|
||||
{
|
||||
static unsigned long lasttimestamp = 0;
|
||||
unsigned long now = micros();
|
||||
if (timeout != 0) {
|
||||
unsigned long diff = now - lasttimestamp;
|
||||
if (diff > timeout) {
|
||||
DIAG(message);
|
||||
DIAG(F("DeltaT=%L"), diff);
|
||||
lasttimestamp = micros();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lasttimestamp = now;
|
||||
}
|
||||
*********************************************/
|
||||
void loop()
|
||||
{
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
static bool oldactive = false;
|
||||
if (dccSniffer) {
|
||||
bool newactive = dccSniffer->inputActive();
|
||||
if (oldactive != newactive) {
|
||||
RMFT2::railsyncEvent(newactive);
|
||||
oldactive = newactive;
|
||||
}
|
||||
DCCPacket p = dccSniffer->fetchPacket();
|
||||
if (p.len() != 0) {
|
||||
if (DCCDecoder::parse(p)) {
|
||||
if (Diag::SNIFFER)
|
||||
p.print();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// The main sketch has responsibilities during loop()
|
||||
|
||||
// Responsibility 1: Handle DCC background processes
|
||||
@@ -176,9 +192,11 @@ void loop()
|
||||
|
||||
#endif //WIFI_ON
|
||||
#else //ARDUINO_ARCH_ESP32
|
||||
#if WIFI_ON
|
||||
#ifndef WIFI_TASK_ON_CORE0
|
||||
WifiESP::loop();
|
||||
#endif
|
||||
#endif //WIFI_ON
|
||||
#endif //ARDUINO_ARCH_ESP32
|
||||
#if ETHERNET_ON
|
||||
EthernetInterface::loop();
|
||||
|
24
DCC.cpp
24
DCC.cpp
@@ -162,7 +162,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
|
||||
}
|
||||
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
void DCC::setFunctionInternal(int cab, byte group, byte byte1, byte byte2) {
|
||||
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
|
||||
byte b[4];
|
||||
byte nB = 0;
|
||||
@@ -172,8 +172,8 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
b[nB++] = lowByte(cab);
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
b[nB++] = byte2;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 0, cab);
|
||||
|
||||
DCCQueue::scheduleDCCFunctionPacket(b, nB, cab,group);
|
||||
}
|
||||
|
||||
// returns speed steps 0 to 127 (1 == emergency stop)
|
||||
@@ -327,7 +327,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
// the initial decoders were orgnized and that influenced how the DCC
|
||||
// standard was made.
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
|
||||
#endif
|
||||
// use masks to detect wrong values and do nothing
|
||||
if(address != (address & 511))
|
||||
@@ -949,31 +949,31 @@ bool DCC::issueReminder(LOCO * slot) {
|
||||
return true; // reminder sent
|
||||
case 1: // remind function group 1 (F0-F4)
|
||||
if (flags & FN_GROUP_1) {
|
||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
setFunctionInternal(loco,1,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 3: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2) {
|
||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
setFunctionInternal(loco,2,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 5: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3) {
|
||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
setFunctionInternal(loco,3,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 7: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4) {
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
||||
setFunctionInternal(loco,4,222, ((functions>>13)& 0xFF));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 9: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5) {
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
||||
setFunctionInternal(loco,5,223, ((functions>>21)& 0xFF));
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
@@ -1079,6 +1079,9 @@ void DCC::displayCabList(Print * stream) {
|
||||
}
|
||||
|
||||
void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
|
||||
// avoid unused warnings when EXRAIL not active
|
||||
(void)loco; (void)blockid; (void)exclusive;
|
||||
|
||||
// update block loco is in, tell exrail leaving old block, and entering new.
|
||||
|
||||
// NOTE: The loco table scanning is really inefficient and needs rewriting
|
||||
@@ -1107,7 +1110,8 @@ void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
|
||||
}
|
||||
|
||||
void DCC::clearBlock(uint16_t blockid) {
|
||||
// Railcom reports block empty... tell Exrail about all leavers
|
||||
(void)blockid; // avoid unused warning when EXRAIL not active
|
||||
// clear block occupied by loco, tell exrail about all leavers
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
|
2
DCC.h
2
DCC.h
@@ -130,7 +130,7 @@ private:
|
||||
static byte defaultMomentumA; // Accelerating
|
||||
static byte defaultMomentumD; // Accelerating
|
||||
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
||||
static void setFunctionInternal(int cab, byte fByte, byte eByte);
|
||||
static void setFunctionInternal(int cab, byte group, byte fByte, byte eByte);
|
||||
static bool issueReminder(LOCO * slot);
|
||||
static LOCO* nextLocoReminder;
|
||||
static FSH *shieldName;
|
||||
|
178
DCCDecoder.cpp
Normal file
178
DCCDecoder.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* © 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
|
30
DCCDecoder.h
Normal file
30
DCCDecoder.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* © 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
|
@@ -2,8 +2,8 @@
|
||||
* © 2022 Paul M Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021-2024 Herb Morton
|
||||
* © 2020-2023 Harald Barth
|
||||
* © 2021-2025 Herb Morton
|
||||
* © 2020-2025 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2025 Chris Harlow
|
||||
@@ -122,6 +122,7 @@ Once a new OPCODE is decided upon, update this list.
|
||||
#include "Stash.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "WifiESP32.h"
|
||||
#include "DCCDecoder.h"
|
||||
#endif
|
||||
|
||||
// This macro can't be created easily as a portable function because the
|
||||
@@ -712,6 +713,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
case 'C': // CONFIG <C [params]>
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
// currently this only works on ESP32
|
||||
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
|
||||
bool on = false;
|
||||
if (params>1 && p[1] == "ON"_hk) {
|
||||
on = true;
|
||||
}
|
||||
DCCDecoder::onoff(on);
|
||||
return;
|
||||
}
|
||||
#if defined(HAS_ENOUGH_MEMORY)
|
||||
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
|
||||
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
|
||||
@@ -796,6 +805,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
TrackManager::reportCurrent(stream); // <g limit...limit>
|
||||
return;
|
||||
|
||||
case "L"_hk: // <JL display row> track state and mA value on display
|
||||
if (params<3) break;
|
||||
TrackManager::reportCurrentLCD(p[1], p[2]); // Track power status
|
||||
return;
|
||||
|
||||
case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
|
||||
if (params!=1) break; // <JA>
|
||||
StringFormatter::send(stream, F("<jA>\n"));
|
||||
@@ -1263,6 +1277,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
Diag::LCN = onOff;
|
||||
return true;
|
||||
|
||||
case "SNIFFER"_hk: // <D SNIFFER ON/OFF>
|
||||
Diag::SNIFFER = onOff;
|
||||
return true;
|
||||
|
||||
case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
|
||||
Diag::WEBSOCKET = onOff;
|
||||
return true;
|
||||
|
@@ -40,6 +40,7 @@ struct DCCEXParser
|
||||
static void setCamParserFilter(FILTER_CALLBACK filter);
|
||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
||||
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||
|
||||
private:
|
||||
|
||||
@@ -81,7 +82,6 @@ struct DCCEXParser
|
||||
static FILTER_CALLBACK filterRMFTCallback;
|
||||
static FILTER_CALLBACK filterCamParserCallback;
|
||||
static AT_COMMAND_CALLBACK atCommandCallback;
|
||||
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||
static void sendFlashList(Print * stream,const int16_t flashList[]);
|
||||
|
||||
};
|
||||
|
83
DCCPacket.h
Normal file
83
DCCPacket.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* © 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
|
83
DCCQueue.cpp
83
DCCQueue.cpp
@@ -62,6 +62,21 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
recycleList=p;
|
||||
}
|
||||
|
||||
void DCCQueue::remove(PendingSlot* premove) {
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=head;p;previous=p,p=p->next) {
|
||||
if (p==premove) {
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else head=p->next;
|
||||
if (p==tail) tail=previous; // if last packet, update tail
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("DCCQueue::remove slot not found"));
|
||||
|
||||
}
|
||||
|
||||
// Packet joins end of low priority queue.
|
||||
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
|
||||
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
@@ -73,19 +88,41 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) {
|
||||
if (p->locoId==loco) {
|
||||
// replace existing packet
|
||||
if (length>sizeof(p->packet)) {
|
||||
DIAG(F("DCC bad packet length=%d"),length);
|
||||
length=sizeof(p->packet); // limit to size of packet
|
||||
}
|
||||
memcpy(p->packet,packet,length);
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=repeats;
|
||||
return;
|
||||
}
|
||||
}
|
||||
highPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
highPriorityQueue->addQueue(getSlot(SPEED_PACKET,packet,length,repeats,loco));
|
||||
}
|
||||
|
||||
// Packet replaces existing loco function packet or joins end of high priority queue.
|
||||
|
||||
void DCCQueue::scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group) {
|
||||
PendingType type=DEAD_PACKET;
|
||||
switch(group) {
|
||||
case 1: type=FUNCTION1_PACKET; break;
|
||||
case 2: type=FUNCTION2_PACKET; break;
|
||||
case 3: type=FUNCTION3_PACKET; break;
|
||||
case 4: type=FUNCTION4_PACKET; break;
|
||||
case 5: type=FUNCTION5_PACKET; break;
|
||||
default:
|
||||
DIAG(F("DCCQueue::scheduleDCCFunctionPacket invalid group %d"),group);
|
||||
return; // invalid group
|
||||
}
|
||||
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) {
|
||||
if (p->locoId==loco && p->type==type) {
|
||||
// replace existing packet for same loco and function group
|
||||
memcpy(p->packet,packet,length);
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
lowPriorityQueue->addQueue(getSlot(type,packet,length,0,loco));
|
||||
}
|
||||
|
||||
// ESTOP -
|
||||
// any outstanding throttle packet for this loco (all if loco=0) discarded
|
||||
@@ -98,26 +135,17 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
|
||||
// kill any existing throttle packets for this loco (or all locos if broadcast)
|
||||
// this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
|
||||
PendingSlot * previous=nullptr;
|
||||
auto p=highPriorityQueue->head;
|
||||
while(p) {
|
||||
PendingSlot * pNext=nullptr;
|
||||
for (auto p=highPriorityQueue->head;p;p=pNext) {
|
||||
pNext=p->next; // save next packet in case we recycle this one
|
||||
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
|
||||
// drop this packet from the highPriority queue
|
||||
if (previous) previous->next=p->next;
|
||||
else highPriorityQueue->head=p->next;
|
||||
|
||||
// remove this slot from the queue or it will interfere with our ESTOP
|
||||
highPriorityQueue->remove(p);
|
||||
recycle(p); // recycle this slot
|
||||
|
||||
// address next packet
|
||||
p=previous?previous->next : highPriorityQueue->head;
|
||||
}
|
||||
else {
|
||||
previous=p;
|
||||
p=p->next;
|
||||
}
|
||||
}
|
||||
// add the estop packet to the start of the queue
|
||||
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
|
||||
highPriorityQueue->jumpQueue(getSlot(SPEED_PACKET,packet,length,repeats,0));
|
||||
}
|
||||
|
||||
// Accessory coil-On Packet joins end of queue as normal.
|
||||
@@ -149,8 +177,8 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
}
|
||||
|
||||
bool DCCQueue::scheduleNextInternal() {
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=head;p;previous=p,p=p->next) {
|
||||
|
||||
for (auto p=head;p;p=p->next) {
|
||||
// skip over pending ACC_OFF packets which are still delayed
|
||||
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
|
||||
if (p->locoId) {
|
||||
@@ -171,9 +199,7 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
}
|
||||
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else head=p->next;
|
||||
if (!head) tail=nullptr;
|
||||
remove(p);
|
||||
|
||||
// special cases handling
|
||||
if (p->type == ACC_ON_PACKET) {
|
||||
@@ -182,11 +208,8 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
p->packet[1] &= ~0x08; // set C to 0 (gate off)
|
||||
p->startTime=millis()+p->delayOff;
|
||||
highPriorityQueue->jumpQueue(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recycle packet just consumed
|
||||
recycle(p);
|
||||
else recycle(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -209,7 +232,7 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
|
||||
bool leak=(q1+q2)!=created;
|
||||
DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
|
||||
type,length,loco,q1,q2, created);
|
||||
(int16_t)type,length,loco,q1,q2, created);
|
||||
if (leak) {
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
|
||||
|
15
DCCQueue.h
15
DCCQueue.h
@@ -23,7 +23,10 @@
|
||||
#include "Arduino.h"
|
||||
#include "DCCWaveform.h"
|
||||
|
||||
enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
|
||||
enum PendingType:byte {NORMAL_PACKET,
|
||||
FUNCTION1_PACKET, FUNCTION2_PACKET, FUNCTION3_PACKET, FUNCTION4_PACKET, FUNCTION5_PACKET,
|
||||
SPEED_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
|
||||
|
||||
struct PendingSlot {
|
||||
PendingSlot* next;
|
||||
PendingType type;
|
||||
@@ -40,14 +43,16 @@ enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,
|
||||
|
||||
class DCCQueue {
|
||||
public:
|
||||
|
||||
|
||||
|
||||
// Non-speed packets are queued in the main queue
|
||||
static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);
|
||||
|
||||
// Speed packets are queued in the high priority queue
|
||||
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
|
||||
|
||||
// Function group packets are queued in the low priority queue
|
||||
static void scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group);
|
||||
|
||||
// ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco
|
||||
static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco);
|
||||
|
||||
@@ -78,6 +83,6 @@ class DCCQueue {
|
||||
static void recycle(PendingSlot* p);
|
||||
void addQueue(PendingSlot * p);
|
||||
void jumpQueue(PendingSlot * p);
|
||||
|
||||
void remove(PendingSlot * p);
|
||||
};
|
||||
#endif
|
||||
#endif // DCCQueue_h
|
||||
|
@@ -203,7 +203,9 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
|
||||
}
|
||||
|
||||
void DCCTimer::DCCEXledcDetachPin(uint8_t pin) {
|
||||
DIAG(F("Clear pin %d channel"), pin);
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Clear pin %d from ledc channel"), pin);
|
||||
#endif
|
||||
pin_to_channel[pin] = 0;
|
||||
pinMatrixOutDetach(pin, false, false);
|
||||
}
|
||||
|
@@ -208,7 +208,7 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
|
||||
// dcctimer.attachInterrupt(Timer11_Handler);
|
||||
dcctimer.attachInterrupt(DCCTimer_Handler);
|
||||
dcctimer.setInterruptPriority(0, 0); // Set highest preemptive priority!
|
||||
dcctimer.setInterruptPriority(1, 0); // Set second highest preemptive priority!
|
||||
dcctimer.refresh();
|
||||
dcctimer.resume();
|
||||
|
||||
|
@@ -188,6 +188,7 @@ Display *Display::loop2(bool force) {
|
||||
#endif
|
||||
noMoreRowsToDisplay = false;
|
||||
slot = 0;
|
||||
_deviceDriver->setRowNative(slot); // Set position for display
|
||||
lastScrollTime = currentMillis;
|
||||
return NULL;
|
||||
}
|
||||
|
30
EXRAIL2.cpp
30
EXRAIL2.cpp
@@ -91,6 +91,10 @@ LookList * RMFT2::onRotateLookup=NULL;
|
||||
LookList * RMFT2::onOverloadLookup=NULL;
|
||||
LookList * RMFT2::onBlockEnterLookup=NULL;
|
||||
LookList * RMFT2::onBlockExitLookup=NULL;
|
||||
#ifdef BOOSTER_INPUT
|
||||
LookList * RMFT2::onRailSyncOnLookup=NULL;
|
||||
LookList * RMFT2::onRailSyncOffLookup=NULL;
|
||||
#endif
|
||||
byte * RMFT2::routeStateArray=nullptr;
|
||||
const FSH * * RMFT2::routeCaptionArray=nullptr;
|
||||
|
||||
@@ -211,6 +215,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);
|
||||
onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);
|
||||
}
|
||||
#ifdef BOOSTER_INPUT
|
||||
onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
|
||||
onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
|
||||
#endif
|
||||
|
||||
// onLCCLookup is not the same so not loaded here.
|
||||
|
||||
@@ -1158,6 +1166,10 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONOVERLOAD:
|
||||
case OPCODE_ONBLOCKENTER:
|
||||
case OPCODE_ONBLOCKEXIT:
|
||||
#ifdef BOOSTER_INPUT
|
||||
case OPCODE_ONRAILSYNCON:
|
||||
case OPCODE_ONRAILSYNCOFF:
|
||||
#endif
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1387,7 +1399,19 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
|
||||
onOverloadLookup->handleEvent(F("POWER"),track);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef BOOSTER_INPUT
|
||||
void RMFT2::railsyncEvent(bool on) {
|
||||
if (Diag::CMD)
|
||||
DIAG(F("railsyncEvent : %d"), on);
|
||||
if (on) {
|
||||
if (onRailSyncOnLookup)
|
||||
onRailSyncOnLookup->handleEvent(F("RAILSYNCON"), 0);
|
||||
} else {
|
||||
if (onRailSyncOffLookup)
|
||||
onRailSyncOffLookup->handleEvent(F("RAILSYNCOFF"), 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// This function is used when setting pins so that a SET or RESET
|
||||
// will cause any blink task on that pin to terminate.
|
||||
// It will be compiled out of existence if no BLINK feature is used.
|
||||
@@ -1533,7 +1557,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::manageRouteState(uint16_t id, byte state) {
|
||||
void RMFT2::manageRouteState(int16_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
|
||||
@@ -1545,7 +1569,7 @@ void RMFT2::manageRouteState(uint16_t id, byte state) {
|
||||
CommandDistributor::broadcastRouteState(id,state);
|
||||
}
|
||||
}
|
||||
void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) {
|
||||
void RMFT2::manageRouteCaption(int16_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
|
||||
|
12
EXRAIL2.h
12
EXRAIL2.h
@@ -74,6 +74,7 @@ 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,
|
||||
@@ -195,6 +196,9 @@ class LookList {
|
||||
static void clockEvent(int16_t clocktime, bool change);
|
||||
static void rotateEvent(int16_t id, bool change);
|
||||
static void powerEvent(int16_t track, bool overload);
|
||||
#ifdef BOOSTER_INPUT
|
||||
static void railsyncEvent(bool on);
|
||||
#endif
|
||||
static void blockEvent(int16_t block, int16_t loco, bool entering);
|
||||
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||
// Throttle Info Access functions built by exrail macros
|
||||
@@ -266,13 +270,17 @@ private:
|
||||
static LookList * onOverloadLookup;
|
||||
static LookList * onBlockEnterLookup;
|
||||
static LookList * onBlockExitLookup;
|
||||
#ifdef BOOSTER_INPUT
|
||||
static LookList * onRailSyncOnLookup;
|
||||
static LookList * onRailSyncOffLookup;
|
||||
#endif
|
||||
|
||||
|
||||
static const int countLCCLookup;
|
||||
static int onLCCLookup[];
|
||||
static const byte compileFeatures;
|
||||
static void manageRouteState(uint16_t id, byte state);
|
||||
static void manageRouteCaption(uint16_t id, const FSH* caption);
|
||||
static void manageRouteState(int16_t id, byte state);
|
||||
static void manageRouteCaption(int16_t id, const FSH* caption);
|
||||
static byte * routeStateArray;
|
||||
static const FSH ** routeCaptionArray;
|
||||
static int16_t * stashArray;
|
||||
|
@@ -129,6 +129,8 @@
|
||||
#undef ONCLOCKTIME
|
||||
#undef ONCLOCKMINS
|
||||
#undef ONOVERLOAD
|
||||
#undef ONRAILSYNCON
|
||||
#undef ONRAILSYNCOFF
|
||||
#undef ONGREEN
|
||||
#undef ONRED
|
||||
#undef ONROTATE
|
||||
@@ -861,6 +863,16 @@
|
||||
* @param track_id A..H
|
||||
*/
|
||||
#define ONOVERLOAD(track_id)
|
||||
/**
|
||||
* @def ONRAILSYNCON
|
||||
* @brief Start task here when the railsync (booster) input port get a valid DCC signal
|
||||
*/
|
||||
#define ONRAILSYNCON
|
||||
/**
|
||||
* @def ONRAILSYNCOFF
|
||||
* @brief Start task here when the railsync (booster) input port does not get a valid DCC signal any more
|
||||
*/
|
||||
#define ONRAILSYNCOFF
|
||||
/**
|
||||
* @def ONDEACTIVATE(addr,subaddr)
|
||||
* @brief Start task here when DCC deactivate packet sent
|
||||
|
@@ -213,6 +213,27 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' '
|
||||
);
|
||||
auto progCounter=task->progCounter; // name to satisfy macros below
|
||||
auto operand=task->getOperand(progCounter,0);
|
||||
switch(GET_OPCODE) {
|
||||
case OPCODE_RESERVE:
|
||||
StringFormatter::send(stream,F(" WAIT RESERVE %d"),operand);
|
||||
break;
|
||||
case OPCODE_AT:
|
||||
case OPCODE_ATTIMEOUT2:
|
||||
case OPCODE_AFTER:
|
||||
case OPCODE_ATGTE:
|
||||
case OPCODE_ATLT:
|
||||
StringFormatter::send(stream,F(" WAIT AT/AFTER %d"),operand);
|
||||
break;
|
||||
case OPCODE_DELAY:
|
||||
case OPCODE_DELAYMINS:
|
||||
case OPCODE_DELAYMS:
|
||||
case OPCODE_RANDWAIT:
|
||||
StringFormatter::send(stream,F(" WAIT DELAY"));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
|
@@ -95,21 +95,29 @@
|
||||
#define STEALTH_GLOBAL(code...) code
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Pass 1h Implements HAL macro by creating exrailHalSetup function
|
||||
// Pass 1h Implements HAL macro by creating exrailHalSetup1 function
|
||||
// Also allows creating EXTurntable object
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef HAL
|
||||
#define HAL(haltype,params...) haltype::create(params);
|
||||
#undef HAL_IGNORE_DEFAULTS
|
||||
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
|
||||
bool exrailHalSetup1() {
|
||||
bool ignore_defaults=false;
|
||||
#include "myAutomation.h"
|
||||
return ignore_defaults;
|
||||
}
|
||||
|
||||
// Pass 1s Implements servos by creating exrailHalSetup2
|
||||
// TODO Turnout and turntable creation should be moved to here instead of
|
||||
// the first pass from the opcode table.
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef JMRI_SENSOR
|
||||
#define JMRI_SENSOR(vpin,count...) Sensor::createMultiple(vpin,##count);
|
||||
#undef CONFIGURE_SERVO
|
||||
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile) IODevice::configureServo(vpin,pos1,pos2,PCA9685::profile);
|
||||
bool exrailHalSetup() {
|
||||
bool ignore_defaults=false;
|
||||
void exrailHalSetup2() {
|
||||
#include "myAutomation.h"
|
||||
return ignore_defaults;
|
||||
}
|
||||
|
||||
// Pass 1c detect compile time featurtes
|
||||
@@ -527,6 +535,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
||||
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
||||
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
|
||||
#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
|
||||
#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
|
||||
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
||||
|
@@ -1 +1 @@
|
||||
#define GITHUB_SHA "devel-202503250850Z"
|
||||
#define GITHUB_SHA "devel-202507181124Z"
|
||||
|
@@ -37,21 +37,42 @@
|
||||
// it can replace use of noInterrupts/interrupts in other parts of DCC-EX.
|
||||
//
|
||||
static inline uint8_t _deferInterrupts(void) {
|
||||
#if defined(ARDUINO_ARCH_STM32)
|
||||
NVIC_DisableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_DisableIRQ(I2C1_ER_IRQn);
|
||||
#else
|
||||
noInterrupts();
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
static inline void _conditionalEnableInterrupts(bool *wasEnabled) {
|
||||
#if defined(ARDUINO_ARCH_STM32)
|
||||
(void)wasEnabled;
|
||||
NVIC_EnableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_EnableIRQ(I2C1_ER_IRQn);
|
||||
#else
|
||||
if (*wasEnabled) interrupts();
|
||||
#endif
|
||||
}
|
||||
#define ATOMIC_BLOCK(x) \
|
||||
for (bool _int_saved __attribute__((__cleanup__(_conditionalEnableInterrupts))) \
|
||||
=_getInterruptState(),_ToDo=_deferInterrupts(); _ToDo; _ToDo=0)
|
||||
|
||||
// The construct of
|
||||
// "variable __attribute__((__cleanup__(func)))"
|
||||
// calls the func with *variable when variable goes out of scope
|
||||
|
||||
#if defined(__AVR__) // Nano, Uno, Mega2580, NanoEvery, etc.
|
||||
static inline bool _getInterruptState(void) {
|
||||
return bitRead(SREG, SREG_I); // true if enabled, false if disabled
|
||||
}
|
||||
#elif defined(__arm__) // STM32, SAMD, Teensy
|
||||
#elif defined(ARDUINO_ARCH_STM32)
|
||||
static inline bool _getInterruptState( void ) {
|
||||
// as we do ony mess with the I2C interrupts in the STM32 case,
|
||||
// we do not care about their previous state
|
||||
return true;
|
||||
}
|
||||
#elif defined(__arm__) // SAMD, Teensy
|
||||
static inline bool _getInterruptState( void ) {
|
||||
uint32_t reg;
|
||||
__asm__ __volatile__ ("MRS %0, primask" : "=r" (reg) );
|
||||
|
@@ -211,9 +211,19 @@ void I2CManagerClass::I2C_init()
|
||||
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
// Setting NVIC
|
||||
NVIC_SetPriority(I2C1_EV_IRQn, 1); // Match default priorities
|
||||
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 4 means that we have all bits for preemptive grouping
|
||||
// prio scheme:
|
||||
// systick : 0
|
||||
// waveform timer : 1
|
||||
// i2c : 2
|
||||
// one must call NVIC_EncodePriority() to bitshift the priorities
|
||||
// according to the active priority grouping and then use that
|
||||
// value as argument to NVIC_SetPriority().
|
||||
NVIC_SetPriority(I2C1_EV_IRQn,
|
||||
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
|
||||
NVIC_EnableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_SetPriority(I2C1_ER_IRQn, 1); // Match default priorities
|
||||
NVIC_SetPriority(I2C1_ER_IRQn,
|
||||
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
|
||||
NVIC_EnableIRQ(I2C1_ER_IRQn);
|
||||
|
||||
// CR2 Interrupt Settings
|
||||
@@ -305,6 +315,7 @@ void I2CManagerClass::I2C_close() {
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_handleInterrupt() {
|
||||
volatile uint16_t temp_sr1, temp_sr2;
|
||||
(void) temp_sr2; // only used as target for reads
|
||||
|
||||
temp_sr1 = s->SR1;
|
||||
|
||||
|
62
IODevice.cpp
62
IODevice.cpp
@@ -33,7 +33,9 @@
|
||||
|
||||
// Link to halSetup function. If not defined, the function reference will be NULL.
|
||||
extern __attribute__((weak)) void halSetup();
|
||||
extern __attribute__((weak)) bool exrailHalSetup();
|
||||
extern __attribute__((weak)) bool exrailHalSetup1();
|
||||
extern __attribute__((weak)) bool exrailHalSetup2();
|
||||
|
||||
|
||||
//==================================================================================================================
|
||||
// Static methods
|
||||
@@ -59,32 +61,46 @@ void IODevice::begin() {
|
||||
if (halSetup)
|
||||
halSetup();
|
||||
|
||||
// include any HAL devices defined in exrail.
|
||||
// Include any HAL devices defined in exrail.
|
||||
// The first pass call only creates HAL devices,
|
||||
// the second pass will apply servo settings etc which can only be
|
||||
// done after all devices (including the defaults) are created.
|
||||
// If exrailHalSetup1 is not defined, then it will be NULL and the call
|
||||
// will be ignored.
|
||||
// If it returns true, then the default HAL devices will not be created.
|
||||
|
||||
bool ignoreDefaults=false;
|
||||
if (exrailHalSetup)
|
||||
ignoreDefaults=exrailHalSetup();
|
||||
if (ignoreDefaults) return;
|
||||
if (exrailHalSetup1)
|
||||
ignoreDefaults=exrailHalSetup1();
|
||||
|
||||
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
|
||||
// Allocates 32 pins 100-131
|
||||
const bool silent=true; // no message if these conflict
|
||||
if (checkNoOverlap(100, 16, 0x40, silent)) {
|
||||
PCA9685::create(100, 16, 0x40);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(116, 16, 0x41, silent)) {
|
||||
PCA9685::create(116, 16, 0x41);
|
||||
}
|
||||
if (!ignoreDefaults) {
|
||||
|
||||
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
||||
// Allocates 32 pins 164-195
|
||||
if (checkNoOverlap(164, 16, 0x20, silent)) {
|
||||
MCP23017::create(164, 16, 0x20);
|
||||
}
|
||||
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
|
||||
// Allocates 32 pins 100-131
|
||||
const bool silent=true; // no message if these conflict
|
||||
if (checkNoOverlap(100, 16, 0x40, silent)) {
|
||||
PCA9685::create(100, 16, 0x40);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(180, 16, 0x21, silent)) {
|
||||
MCP23017::create(180, 16, 0x21);
|
||||
}
|
||||
if (checkNoOverlap(116, 16, 0x41, silent)) {
|
||||
PCA9685::create(116, 16, 0x41);
|
||||
}
|
||||
|
||||
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
||||
// Allocates 32 pins 164-195
|
||||
if (checkNoOverlap(164, 16, 0x20, silent)) {
|
||||
MCP23017::create(164, 16, 0x20);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(180, 16, 0x21, silent)) {
|
||||
MCP23017::create(180, 16, 0x21);
|
||||
}
|
||||
}
|
||||
|
||||
// apply any second pass HAL setup from EXRAIL.
|
||||
// This will typically set up servo profiles, or create turnouts.
|
||||
if (exrailHalSetup2)
|
||||
exrailHalSetup2();
|
||||
}
|
||||
|
||||
// reset() function to reinitialise all devices
|
||||
|
@@ -73,6 +73,8 @@ public:
|
||||
}
|
||||
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
|
||||
(void)profile; // suppress warning, not used in this function
|
||||
(void)duration; // suppress warning, not used in this function
|
||||
int pin=vpin - _firstVpin;
|
||||
_pinValues[pin]=value; // this is analog write
|
||||
}
|
||||
|
@@ -72,6 +72,8 @@ const byte _DIR_MASK = 0x30;
|
||||
|
||||
|
||||
void EncoderThrottle::_loop(unsigned long currentMicros) {
|
||||
(void) currentMicros; // suppress warning
|
||||
|
||||
if (_locoid==0) return; // not in use
|
||||
|
||||
// Clicking down on the roco, stops the loco and sets the direction as unknown.
|
||||
@@ -118,9 +120,10 @@ const byte _DIR_MASK = 0x30;
|
||||
}
|
||||
}
|
||||
|
||||
// Selocoid as analog value to start drive
|
||||
// Set locoid as analog value to start drive
|
||||
// use <z vpin locoid [notch]>
|
||||
void EncoderThrottle::_writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
|
||||
(void)vpin; // not used, but needed to match IODevice interface
|
||||
(void) param2;
|
||||
_locoid=value;
|
||||
if (param1>0) _notch=param1;
|
||||
|
@@ -197,6 +197,7 @@ public:
|
||||
// Check for incoming data, and update busy flag and other state accordingly
|
||||
|
||||
void processIncoming(unsigned long currentMicros) {
|
||||
(void)currentMicros; // suppress warning, not used in this function
|
||||
// Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF"
|
||||
RX_fifo_lvl();
|
||||
if (FIFO_RX_LEVEL >= 10) {
|
||||
@@ -308,7 +309,7 @@ public:
|
||||
sendPacket(0x0C,0,0);
|
||||
_resetCmd = false;
|
||||
} else if(_volCmd == true) { // do the volme before palying a track
|
||||
if(_requestedVolumeLevel >= 0 && _requestedVolumeLevel <= 30){
|
||||
if(_requestedVolumeLevel <= 30) {
|
||||
_currentVolume = _requestedVolumeLevel; // If _requestedVolumeLevel is out of range, sent _currentV1olume
|
||||
}
|
||||
sendPacket(0x06, 0x00, _currentVolume);
|
||||
@@ -407,6 +408,9 @@ public:
|
||||
|
||||
// Write to a vPin will do nothing
|
||||
void _write(VPIN vpin, int value) override {
|
||||
(void)vpin; // suppress warning, not used in this function
|
||||
(void)value; // suppress warning, not used in this function
|
||||
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("I2CDFPlayer: Writing to any vPin not supported"));
|
||||
|
@@ -77,6 +77,7 @@ void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
|
||||
|
||||
|
||||
void I2CRailcom::_loop(unsigned long currentMicros) {
|
||||
(void)currentMicros; // not used, but needed to match IODevice interface
|
||||
// Read responses from device
|
||||
if (_deviceState!=DEVSTATE_NORMAL) return;
|
||||
|
||||
|
101
IO_PCA9554.h
Normal file
101
IO_PCA9554.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* © 2025, Paul M. Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef io_pca9554_h
|
||||
#define io_pca9554_h
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for PCA9554/TCA9554 8-bit I/O expander (NXP & Texas Instruments).
|
||||
*/
|
||||
|
||||
class PCA9554 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9554(vpin,nPins, i2cAddress, interruptPin);
|
||||
}
|
||||
|
||||
private:
|
||||
// Constructor
|
||||
PCA9554(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCA9554"), vpin, nPins, I2CAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||
outputBuffer, sizeof(outputBuffer));
|
||||
outputBuffer[0] = REG_INPUT_P0;
|
||||
}
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 2, REG_OUTPUT_P0, _portOutputState);
|
||||
}
|
||||
void _writePullups() override {
|
||||
// Do nothing, pull-ups are always in place for input ports
|
||||
// This function is here for HAL GPIOBase API compatibilitiy
|
||||
|
||||
}
|
||||
void _writePortModes() override {
|
||||
// Write 0 to REG_CONF_P0 for in-use pins that are outputs, 1 for others.
|
||||
// PCA9554 & TCA9554, Interrupt is always enabled for raising and falling edge
|
||||
uint8_t temp = ~(_portMode & _portInUse);
|
||||
I2CManager.write(_I2CAddress, 2, REG_CONF_P0, temp);
|
||||
}
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer[1];
|
||||
I2CManager.read(_I2CAddress, buffer, 1, 1, REG_INPUT_P0);
|
||||
_portInputState = buffer[0];
|
||||
} else {
|
||||
// Queue new request
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xff;
|
||||
}
|
||||
|
||||
void _setupDevice() override {
|
||||
// HAL API calls
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
_writeGpioPort();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[1];
|
||||
uint8_t outputBuffer[1];
|
||||
|
||||
|
||||
enum {
|
||||
REG_INPUT_P0 = 0x00,
|
||||
REG_OUTPUT_P0 = 0x01,
|
||||
REG_POL_INV_P0 = 0x02,
|
||||
REG_CONF_P0 = 0x03,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif
|
13
IO_PCF8574.h
13
IO_PCF8574.h
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
* © 2025 Herb Morton
|
||||
* © 2022 Paul M Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
@@ -43,15 +44,21 @@
|
||||
|
||||
class PCF8574 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);
|
||||
}
|
||||
|
||||
private:
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
|
||||
if (initPortState>=0) {
|
||||
_portMode = 255; // set all pins to output mode
|
||||
_portInUse = 255; // 8 ports in use
|
||||
_portOutputState = initPortState; // initialize pins low-high 0-255
|
||||
I2CManager.write(_I2CAddress, 1, initPortState);
|
||||
}
|
||||
}
|
||||
|
||||
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
|
||||
|
@@ -136,6 +136,7 @@ private:
|
||||
|
||||
// Return the position sent by the rotary encoder software
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
(void)vpin; // suppress warning, not used in this function
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
return _position;
|
||||
}
|
||||
@@ -153,6 +154,8 @@ private:
|
||||
// To be valid, must be 0 to 255, and different to the current position
|
||||
// If the current position is the same, it was initiated by the rotary encoder
|
||||
void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t duration) override {
|
||||
(void)profile; // suppress warning, not used in this function
|
||||
(void)duration; // suppress warning, not used in this function
|
||||
if (vpin == _firstVpin + 2) {
|
||||
if (position >= 0 && position <= 255 && position != _position) {
|
||||
byte newPosition = position & 0xFF;
|
||||
|
130
LocoTable.cpp
Normal file
130
LocoTable.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This source is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "LocoTable.h"
|
||||
|
||||
LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
|
||||
int LocoTable::highestUsedReg = 0;
|
||||
|
||||
int LocoTable::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
// determine speed reg for this loco
|
||||
int firstEmpty = MAX_LOCOS;
|
||||
int reg;
|
||||
for (reg = 0; reg < MAX_LOCOS; reg++) {
|
||||
if (speedTable[reg].loco == locoId) break;
|
||||
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
|
||||
}
|
||||
|
||||
// return -1 if not found and not auto creating
|
||||
if (reg == MAX_LOCOS && !autoCreate) return -1;
|
||||
if (reg == MAX_LOCOS) reg = firstEmpty;
|
||||
if (reg >= MAX_LOCOS) {
|
||||
//DIAG(F("Too many locos"));
|
||||
return -1;
|
||||
}
|
||||
if (reg==firstEmpty){
|
||||
speedTable[reg].loco = locoId;
|
||||
speedTable[reg].speedCode=128; // default direction forward
|
||||
speedTable[reg].groupFlags=0;
|
||||
speedTable[reg].functions=0;
|
||||
}
|
||||
if (reg > highestUsedReg) highestUsedReg = reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
// returns false only if loco existed but nothing was changed
|
||||
bool LocoTable::updateLoco(int loco, byte speedCode) {
|
||||
if (loco==0) {
|
||||
/*
|
||||
// broadcast stop/estop but dont change direction
|
||||
for (int reg = 0; reg < highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco==0) continue;
|
||||
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].speedCode != newspeed) {
|
||||
speedTable[reg].speedCode = newspeed;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco, false);
|
||||
if (reg>=0) {
|
||||
speedTable[reg].speedcounter++;
|
||||
if (speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// new
|
||||
reg=lookupSpeedTable(loco, true);
|
||||
if(reg >=0) speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool LocoTable::updateFunc(int loco, byte func, int shift) {
|
||||
unsigned long previous;
|
||||
unsigned long newfunc;
|
||||
bool retval = false; // nothing was touched
|
||||
int reg = lookupSpeedTable(loco, false);
|
||||
if (reg < 0) { // not found
|
||||
retval = true;
|
||||
reg = lookupSpeedTable(loco, true);
|
||||
newfunc = previous = 0;
|
||||
} else {
|
||||
newfunc = previous = speedTable[reg].functions;
|
||||
}
|
||||
|
||||
speedTable[reg].funccounter++;
|
||||
|
||||
if(shift == 1) { // special case for light
|
||||
newfunc &= ~1UL;
|
||||
newfunc |= ((func & 0B10000) >> 4);
|
||||
}
|
||||
newfunc &= ~(0B1111UL << shift);
|
||||
newfunc |= ((func & 0B1111) << shift);
|
||||
|
||||
if (newfunc != previous) {
|
||||
speedTable[reg].functions = newfunc;
|
||||
retval = true;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
void LocoTable::dumpTable(Stream *output) {
|
||||
output->print("\n-----------Table---------\n");
|
||||
for (byte reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco != 0) {
|
||||
output->print(speedTable[reg].loco);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].speedCode);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].functions);
|
||||
output->print(" #funcpacks:");
|
||||
output->print(speedTable[reg].funccounter);
|
||||
output->print(" #speedpacks:");
|
||||
output->print(speedTable[reg].speedcounter);
|
||||
speedTable[reg].funccounter = 0;
|
||||
speedTable[reg].speedcounter = 0;
|
||||
output->print('\n');
|
||||
}
|
||||
}
|
||||
}
|
44
LocoTable.h
Normal file
44
LocoTable.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This source is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "DCC.h" // fetch MAX_LOCOS from there
|
||||
|
||||
class LocoTable {
|
||||
public:
|
||||
void forgetLoco(int cab) {
|
||||
int reg=lookupSpeedTable(cab, false);
|
||||
if (reg>=0) speedTable[reg].loco=0;
|
||||
}
|
||||
static int lookupSpeedTable(int locoId, bool autoCreate);
|
||||
static bool updateLoco(int loco, byte speedCode);
|
||||
static bool updateFunc(int loco, byte func, int shift);
|
||||
static void dumpTable(Stream *output);
|
||||
|
||||
private:
|
||||
struct LOCO
|
||||
{
|
||||
int loco;
|
||||
byte speedCode;
|
||||
byte groupFlags;
|
||||
unsigned long functions;
|
||||
unsigned int funccounter;
|
||||
unsigned int speedcounter;
|
||||
};
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
static int highestUsedReg;
|
||||
};
|
@@ -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);
|
||||
DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
|
||||
if (Diag::ACK) DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
|
||||
}
|
||||
|
||||
IODevice::write(powerPin,invertPower ? LOW : HIGH);
|
||||
|
241
Sniffer.cpp
Normal file
241
Sniffer.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* © 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
|
76
Sniffer.h
Normal file
76
Sniffer.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* © 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
|
@@ -29,6 +29,7 @@ bool Diag::ETHERNET=false;
|
||||
bool Diag::LCN=false;
|
||||
bool Diag::RAILCOM=false;
|
||||
bool Diag::WEBSOCKET=false;
|
||||
bool Diag::SNIFFER=false;
|
||||
|
||||
|
||||
|
||||
|
@@ -32,7 +32,7 @@ class Diag {
|
||||
static bool LCN;
|
||||
static bool RAILCOM;
|
||||
static bool WEBSOCKET;
|
||||
|
||||
static bool SNIFFER;
|
||||
};
|
||||
|
||||
class StringFormatter
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* © 2022-2025 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2024 Herb Morton
|
||||
* © 2024-2025 Herb Morton
|
||||
* © 2023 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
MotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };
|
||||
int16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };
|
||||
int16_t TrackManager::trackPwrMA[MAX_TRACKS] = { 0 };
|
||||
|
||||
int8_t TrackManager::lastTrack=-1;
|
||||
bool TrackManager::progTrackSyncMain=false;
|
||||
@@ -197,7 +198,7 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
|
||||
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr, bool offAtChange) {
|
||||
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
|
||||
|
||||
// Remember track mode we came from for later
|
||||
@@ -371,10 +372,9 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
#endif
|
||||
#endif
|
||||
// Turn off power if we changed the mode of this track
|
||||
if (mode != oldmode) {
|
||||
if (mode != oldmode && offAtChange) {
|
||||
track[trackToSet]->setPower(POWERMODE::OFF);
|
||||
}
|
||||
|
||||
streamTrackState(NULL,trackToSet);
|
||||
//DIAG(F("TrackMode=%d"),mode);
|
||||
return true;
|
||||
@@ -451,7 +451,7 @@ const FSH* TrackManager::getModeName(TRACK_MODE tm) {
|
||||
if(tm & TRACK_MODIFIER_AUTO)
|
||||
modename=F("MAIN A");
|
||||
else if (tm & TRACK_MODIFIER_INV)
|
||||
modename=F("MAIN I>\n");
|
||||
modename=F("MAIN I");
|
||||
else
|
||||
modename=F("MAIN");
|
||||
}
|
||||
@@ -646,6 +646,37 @@ void TrackManager::reportCurrent(Print* stream) {
|
||||
StringFormatter::send(stream,F(">\n"));
|
||||
}
|
||||
|
||||
void TrackManager::reportCurrentLCD(uint8_t display, byte row) {
|
||||
int16_t trackPwrTotalMA = 0;
|
||||
FOR_EACH_TRACK(t) {
|
||||
bool pstate = TrackManager::isPowerOn(t); // checks if power is on or off
|
||||
TRACK_MODE tMode=(TrackManager::getMode(t)); // gets to current power mode
|
||||
int16_t DCAddr=(TrackManager::returnDCAddr(t));
|
||||
|
||||
if (pstate) { // if power is on do this section
|
||||
trackPwrMA[t]=(3*trackPwrMA[t]>>2) + ((track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :
|
||||
track[t]->raw2mA(track[t]->getCurrentRaw(false)));
|
||||
trackPwrTotalMA += trackPwrMA[t];
|
||||
if (tMode & TRACK_MODE_DC) { // Test if track is in DC or DCX mode
|
||||
SCREEN(display, row+t, F("%c: %S %d %dmA"), t+'A', (TrackManager::getModeName(tMode)),DCAddr, trackPwrMA[t]>>2);
|
||||
}
|
||||
else { // formats without DCAddress
|
||||
SCREEN(display, row+t, F("%c: %S %dmA"), t+'A', (TrackManager::getModeName(tMode)), trackPwrMA[t]>>2);
|
||||
}
|
||||
}
|
||||
else { // if power is off do this section
|
||||
trackPwrMA[t] = 0;
|
||||
if (tMode & TRACK_MODE_DC) { // DC / DCX
|
||||
SCREEN(display, row+t, F("%c: %S %d"), t+'A', (TrackManager::getModeName(tMode)),DCAddr);
|
||||
}
|
||||
else { // Not DC or DCX
|
||||
SCREEN(display, row+t, F("%c: %S"), t+'A', (TrackManager::getModeName(tMode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
SCREEN(display, row+lastTrack+1, F("%d Districts %dmA"), lastTrack+1, trackPwrTotalMA>>2);
|
||||
}
|
||||
|
||||
void TrackManager::reportGauges(Print* stream) {
|
||||
StringFormatter::send(stream,F("<jG"));
|
||||
FOR_EACH_TRACK(t) {
|
||||
@@ -669,9 +700,9 @@ 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);
|
||||
// setPower() of the track called after
|
||||
// seperately after setJoin() instead
|
||||
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
|
||||
break; // there is only one prog track, done
|
||||
}
|
||||
}
|
||||
@@ -679,9 +710,7 @@ void TrackManager::setJoin(bool joined) {
|
||||
if (tempProgTrack != MAX_TRACKS+1) {
|
||||
// setTrackMode defaults to power off, so we
|
||||
// need to preserve that state.
|
||||
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
|
||||
setTrackMode(tempProgTrack, TRACK_MODE_PROG, 0, false); // 0 = no DC loco; false = do not turn off pwr
|
||||
tempProgTrack = MAX_TRACKS+1;
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
* © 2022 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023 Colin Murdoch
|
||||
* © 2025 Herb Morton
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -71,7 +72,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);
|
||||
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0, bool offAtChange=true);
|
||||
static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
|
||||
static void loop();
|
||||
static POWERMODE getMainPower();
|
||||
@@ -87,6 +88,7 @@ class TrackManager {
|
||||
static void sampleCurrent();
|
||||
static void reportGauges(Print* stream);
|
||||
static void reportCurrent(Print* stream);
|
||||
static void reportCurrentLCD(uint8_t display, byte row);
|
||||
static void reportObsoleteCurrent(Print* stream);
|
||||
static void streamTrackState(Print* stream, byte t);
|
||||
static bool isPowerOn(byte t);
|
||||
@@ -113,6 +115,7 @@ class TrackManager {
|
||||
static void applyDCSpeed(byte t);
|
||||
|
||||
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC
|
||||
static int16_t trackPwrMA[MAX_TRACKS]; // for <JL ..> command
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
static byte tempProgTrack; // holds the prog track number during join
|
||||
#endif
|
||||
|
@@ -146,7 +146,14 @@ void halSetup() {
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, 40);
|
||||
|
||||
// Alternative form to initialize 8 pins as output
|
||||
// INT pin -1, when INT is not used
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, -1, 255); 8 pins High
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0); 8 pins Low
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0b11000010);
|
||||
// pins listed sequentially from 7 to 0
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a PCF8575 16-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
|
37
myTrackStatus.example.h
Normal file
37
myTrackStatus.example.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// ************* OLED JL Display Track mA Amperage **************** //
|
||||
// by Herb Morton [Ash] March 23, 2025
|
||||
// Colin Murdoch [ColinM]
|
||||
|
||||
// Inside your config.h file First edit OLED max char rows set to 17
|
||||
// #define MAX_CHARACTER_ROWS 17
|
||||
|
||||
// myAutomation.h
|
||||
// Reporting power status and mA for each track on the LCD
|
||||
HAL(Bitmap,8236,1) // create flag 8236
|
||||
AUTOSTART DELAY(5000)
|
||||
ROUTE("TRACKSTATUS"_hk, "Resume/Pause JL Display")
|
||||
IF(8236)
|
||||
RESET(8236)
|
||||
ROUTE_CAPTION("TRACKSTATUS"_hk, "Paused") ROUTE_INACTIVE("TRACKSTATUS"_hk)
|
||||
PRINT("Pause JL Display")
|
||||
SCREEN(0, 8, "Track status paused")
|
||||
SCREEN(0, 9, "")
|
||||
SCREEN(0,10, "") // several blank lines as needed
|
||||
SCREEN(0,11, "")
|
||||
SCREEN(0,12, "")
|
||||
SCREEN(0,13, "")
|
||||
SCREEN(0,14, "")
|
||||
SCREEN(0,15, "")
|
||||
SCREEN(0,16, "")
|
||||
DONE ENDIF
|
||||
SET(8236)
|
||||
ROUTE_CAPTION("TRACKSTATUS"_hk, "Running") ROUTE_ACTIVE("TRACKSTATUS"_hk)
|
||||
PRINT("Resume JL Display")
|
||||
FOLLOW("PAUSETRACKSTATUS"_hk)
|
||||
SEQUENCE("PAUSETRACKSTATUS"_hk)
|
||||
PARSE("<JL 0 8>") // screen 0 start on line 8
|
||||
PRINT("\n")
|
||||
DELAY(3000)
|
||||
IF(8236) FOLLOW("PAUSETRACKSTATUS"_hk) ENDIF
|
||||
DONE
|
||||
// ************ End OLED JL Display Track mA Amperage ************** //
|
@@ -1,5 +1,5 @@
|
||||
ECHO ON
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\arduino\sketches\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||
SET DUMP=%TEMP%\OBJDUMP.txt
|
||||
echo Most recent subfolder: %ELF% >%DUMP%
|
||||
|
||||
|
@@ -32,7 +32,7 @@ upload_protocol = sam-ba
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:samd21-zero-usb]
|
||||
platform = atmelsam
|
||||
@@ -42,7 +42,7 @@ upload_protocol = sam-ba
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:Arduino-M0]
|
||||
platform = atmelsam
|
||||
@@ -51,7 +51,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:mega2560-debug]
|
||||
platform = atmelavr
|
||||
@@ -63,7 +63,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES
|
||||
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES ${env.build_flags}
|
||||
|
||||
[env:mega2560-no-HAL]
|
||||
platform = atmelavr
|
||||
@@ -75,7 +75,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DIO_NO_HAL
|
||||
build_flags = -DIO_NO_HAL ${env.build_flags}
|
||||
|
||||
[env:mega2560-I2C-wire]
|
||||
platform = atmelavr
|
||||
@@ -87,7 +87,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DI2C_USE_WIRE
|
||||
build_flags = -DI2C_USE_WIRE ${env.build_flags}
|
||||
|
||||
[env:mega2560]
|
||||
platform = atmelavr
|
||||
@@ -106,7 +106,7 @@ lib_ignore = WiFi101
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags =
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:mega2560-eth]
|
||||
platform = atmelavr
|
||||
@@ -123,6 +123,7 @@ lib_ignore = WiFi101
|
||||
WiFiNINA_Generic
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:mega328]
|
||||
platform = atmelavr
|
||||
@@ -134,6 +135,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags= ${env.build_flags}
|
||||
|
||||
[env:unowifiR2]
|
||||
platform = atmelmegaavr
|
||||
@@ -145,7 +147,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
|
||||
build_flags = ${env.build_flags} "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
|
||||
|
||||
[env:nanoevery]
|
||||
platform = atmelmegaavr
|
||||
@@ -158,7 +160,7 @@ lib_deps =
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_speed = 19200
|
||||
build_flags =
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:uno]
|
||||
platform = atmelavr
|
||||
@@ -167,7 +169,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -mcall-prologues
|
||||
build_flags = -mcall-prologues ${env.build_flags}
|
||||
|
||||
[env:nano]
|
||||
platform = atmelavr
|
||||
@@ -177,7 +179,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -mcall-prologues
|
||||
build_flags = -mcall-prologues ${env.build_flags}
|
||||
|
||||
[env:ESP32]
|
||||
; Lock version to 6.7.0 as that is
|
||||
@@ -188,7 +190,7 @@ platform = espressif32 @ 6.7.0
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -197,7 +199,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f411re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -206,7 +208,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -218,7 +220,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f401re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -243,7 +245,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446ze
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -274,7 +276,7 @@ lib_ignore = WiFi101
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
@@ -293,7 +295,7 @@ lib_ignore = WiFi101
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
@@ -302,7 +304,7 @@ upload_protocol = stlink
|
||||
platform = teensy
|
||||
board = teensy31
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -310,7 +312,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy35
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -318,7 +320,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy36
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -326,7 +328,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy40
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -334,6 +336,6 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy41
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore =
|
||||
|
18
version.h
18
version.h
@@ -3,7 +3,23 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
#define VERSION "5.5.25"
|
||||
#define VERSION "5.5.37"
|
||||
// 5.5.37 - Bugfix: Keep power status of track when doing join/unjoin, new keep power option for setTrackMode()
|
||||
// 5.5.36 - make more diag conditional
|
||||
// - Bugfix: SSD1309 OLED controllers
|
||||
// 5.5.35 - BUGFIX DCC Packet Queue timing leak with single loco
|
||||
// 5.5.34 - STM32: Remove I2C interrupt blocking waveform interrupt
|
||||
// - Bugfix: Negative route Ids
|
||||
// 5.5.33 - Fix CONFIG_SERVO when default PCA9685 definition used.
|
||||
// 5.5.32 - Feature: Enable sniffer on CSB-1
|
||||
// 5.5.31 - <JL screen startRow> track status command
|
||||
// - myTrackStatus.example.h added
|
||||
// - Provide for WiFi false on ESP32
|
||||
// 5.5.30 - EXRAIL </> shows why tasks are waiting
|
||||
// 5.5.29 - Resolved compiler warnings
|
||||
// 5.5.28 - DCC Queue memory leak fix
|
||||
// 5.5.27 - PCF8574 output pin initialization parameter
|
||||
// 5.5.26 - PCA9554 and TCA9554/9534 I2C 8-bit GPIO expander drivers
|
||||
// 5.2.25 - IO_Bitmap and assicated Exrail macros
|
||||
// 5.5.24 - SensorCAM in I2C scan and automatically setClock
|
||||
// 5.5.23 - Reminder loop Idle packet optimization
|
||||
|
Reference in New Issue
Block a user