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

Compare commits

...

92 Commits

Author SHA1 Message Date
Harald Barth
fcbd5f96ab versioncomment 2024-01-14 22:47:03 +01:00
Harald Barth
4b486900ad remove preamble doubling and try to hit right preamble bit for cutout 2024-01-14 22:45:43 +01:00
habazut
8bd6403cd1 Merge pull request #391 from Arq/arq-RailCom
RailCom cutout for MAIN and PROG track
2024-01-14 19:43:22 +01:00
Arkadiusz Hahn
65b9079337 RailCom cutout for MAIN and PROG track 2024-01-14 16:14:19 +01:00
Harald Barth
818e05b425 version 5.0.8 2024-01-10 08:37:54 +01:00
Harald Barth
c5168f030f Do not crash on turnouts without description 2024-01-10 08:25:34 +01:00
Harald Barth
387ea019bd version 5.0.7 2023-11-06 22:11:56 +01:00
Harald Barth
a981f83bb9 Only flag 2.2.0.0-dev as broken, not 2.2.0.0 2023-11-06 22:11:31 +01:00
Asbelos
749a859db5 Bugfix TURNOUTL 2023-11-01 20:13:05 +00:00
Harald Barth
659c58b307 version 5.0.5 2023-10-28 19:20:33 +02:00
Harald Barth
0b9ec7460b Bugfix version detection logic and better message 2023-10-28 19:18:59 +02:00
Asbelos
8b8e9e4919 clean result from invalid <JR n> 2023-10-12 11:07:05 +01:00
Asbelos
bef4b2ec35 fix <JR> default roster 2023-10-09 18:09:48 +01:00
Harald Barth
9333beda49 correct return when requesting D RAM 2023-09-24 20:54:17 +02:00
Harald Barth
46289fa78c Check bad AT firmware version 2023-09-14 09:05:23 +02:00
Harald Barth
b3cafd126e sample file corrections 2023-08-30 23:26:20 +02:00
Harald Barth
c55fa9f9d2 version number update 2023-08-25 19:08:58 +02:00
Harald Barth
210d96a3e3 Bugfix: ESP32 30ms off time 2023-08-25 19:07:57 +02:00
Harald Barth
42f3c7c128 version number update 2023-08-24 10:05:31 +02:00
Harald Barth
6cd7002e91 Bugfix: execute 30ms off time before rejoin 2023-08-24 10:03:29 +02:00
peteGSX
085762e800 Add OPCODE list to DCCEXParser.cpp 2023-08-18 18:52:34 +10:00
Harald Barth
2db2b0ecc6 Committing a SHA 2023-08-07 20:27:22 +02:00
Harald Barth
fd58a749ef Committing a SHA 2023-08-07 20:25:14 +02:00
Harald Barth
3bddf4dfd1 Make 4.2.69 the 5.0.0 release 2023-08-07 19:45:45 +02:00
Harald Barth
e0e965f81c Merge branch 'master' into devel 2023-08-07 19:41:00 +02:00
Harald Barth
f2be3aeac3 Make <!> work in DC mode 2023-08-04 14:45:05 +02:00
Harald Barth
a74d85e895 Rename track mode OFF to NONE 2023-08-02 10:00:43 +02:00
Harald Barth
df2e651217 version, compile warning 2023-08-02 01:12:32 +02:00
Harald Barth
36d139268d AVR: Pin specific timer register seting for speed and mode when inrush throttling and for DC PWM 2023-08-02 01:05:31 +02:00
Harald Barth
e3ac3a8ddf Protect Uno user from choosing DC(X) 2023-08-02 01:02:46 +02:00
pmantoine
415e756020 More Nucleo variant defines 2023-07-31 16:51:25 +08:00
kempe63
f754fe2fbf GPIO PCA9555 / TCA9555 support
My 1st commit, be gentle
2023-07-29 20:34:39 +01:00
Harald Barth
399030d8ae make variable frequency a compile option 2023-07-25 12:51:23 +02:00
Harald Barth
4c7e11ddc1 version 2023-07-25 11:30:08 +02:00
Harald Barth
495bbf66bf better variable name 2023-07-25 11:23:36 +02:00
Harald Barth
2950ef010a diag 2023-07-18 01:25:38 +02:00
Harald Barth
c2eb5f23b4 restrict to relevant TRACK_MODE(s) 2023-07-17 09:42:39 +02:00
Harald Barth
94648ead28 versiontag 2023-07-17 02:31:00 +02:00
Harald Barth
ec0499e9da throttleInrush() (tested on ESP32) 2023-07-17 02:30:11 +02:00
Harald Barth
9b75026eef change from trackMode[t] to track[t]->{get,set}Mode 2023-07-17 02:26:29 +02:00
Harald Barth
6036ff9b15 ESP32: ledcSetup before ledcAttach 2023-07-17 02:22:35 +02:00
Harald Barth
6476a7aac2 version 2023-07-14 23:11:22 +02:00
Harald Barth
0edf34bfe2 inrush test ESP32 only 2023-07-14 23:10:50 +02:00
Fred
2eb0f48994 Update .gitignore
Updated .gitignore from the devel branch and used "my@.cpp" instead of listing the individual files so we can ignore anything that starts with "my"
2023-06-29 14:24:51 -04:00
Harald Barth
99521f8a3f Support DCC-EX shield 2023-05-20 17:35:09 +02:00
mstevetodd
fcf05206b4 Merge pull request #333 from mstevetodd/master
Fix: turnout state should be 2/4, not T2/T4
2023-04-25 16:06:10 -04:00
stevet
cc3aba1feb Update WiThrottle.cpp
Fix: turnout state should be 2/4, not T2/T4
2023-04-25 16:02:42 -04:00
Fred
91d36ae909 Update ThrottleAssists.md 2023-03-03 21:59:18 -05:00
Fred
98af5c45ed Update ThrottleAssists.md 2023-03-03 21:46:07 -05:00
Fred
d3eceb6d6c Update ThrottleAssists.md 2023-03-03 21:41:22 -05:00
Fred
79eaaa85fa Update ThrottleAssists.md
Fixing formatting
2023-03-03 21:35:13 -05:00
Fred
0f5b8adb6b Update ThrottleAssists.md 2023-03-03 21:26:46 -05:00
Fred
da8faa808b Update ThrottleAssists.md 2023-03-03 21:07:58 -05:00
Harald Barth
7311f2ce64 LCN bugfix 2023-02-12 20:38:03 +01:00
Harald Barth
7e4f9eb0e1 jT answer should contain empty string 2023-01-29 11:33:28 +01:00
Harald Barth
1f5eafbcca Bugfix for issue #299 TurnoutDescription NULL 2023-01-29 11:32:54 +01:00
peteGSX
7e16ec7088 Fix support request issue template 2022-11-05 05:17:03 +10:00
Harald Barth
912646f8ff Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX into HEAD 2022-11-04 15:41:05 +01:00
Harald Barth
dd309a3705 Ethernet init order 2022-11-04 15:39:35 +01:00
peteGSX
5376c9f410 Update project workflow for forks 2022-11-04 06:54:49 +10:00
peteGSX
b1d110ecbf Fix project workflow 2022-11-03 14:06:43 +10:00
Fred
5b7801ca6c Update version.h 2022-10-28 14:05:35 -04:00
Fred
aca9c9c941 Update release_notes.md 2022-10-28 10:52:12 -04:00
Fred
6f94cd71ab Update release_notes_v4.1.2.md 2022-10-28 10:50:35 -04:00
Fred
1827a11f83 Update release_notes_v4.1.1.md 2022-10-28 10:49:32 -04:00
Fred
0023ce3356 Create release_notes_v4.1.2.md 2022-10-28 10:48:40 -04:00
Fred
7b9f3ae08d Update release_notes.md 2022-10-28 10:39:13 -04:00
Fred
5e50731a78 Update version.h
Fix version number in notes from 4.2.1 to 4.1.2
2022-10-28 10:28:54 -04:00
Harald Barth
df6c511d1d Fix for W5100 ethernet shield which does not report as the W5200 or W5500 2022-10-28 13:24:12 +02:00
peteGSX
4bfd4b1a12 Add templates and project workflow (#258)
* Add templates and project workflow

* Fixed template typos
2022-10-26 19:34:13 -04:00
Fred
4a3f3d0f34 Update release_notes_v4.1.1.md 2022-10-23 08:22:00 -04:00
Fred
f0d1909d9f Update release_notes.md 2022-10-23 08:21:36 -04:00
Fred
daf6799ac1 Update release_notes.md 2022-10-22 18:10:42 -04:00
Kcsmith0708
b7a010f904 Verion.h 4.1.1 (#263)
Edited & Reformatted
 verify then ready for release
2022-10-22 18:01:37 -04:00
Kcsmith0708
d1518b8af0 Update Release_notes_v4.1.1.md (#264)
* Update release_notes_v4.1.1.md

Edited </RED > etc., commands and added KILLALL function to EXRAIL list

* Update release_notes_v4.1.1.md

added <t cab> back in

* Update release_notes_v4.1.1.md

fixed < t cab>  so it would display
2022-10-22 18:00:21 -04:00
Fred
39a85903ce Update release_notes.md 2022-10-21 20:04:05 -04:00
Fred
d72474cd8f Update release_notes_v4.1.1.md 2022-10-21 20:03:00 -04:00
Kcsmith0708
941e74beaf Realese Document Edit & Enhancements (#262)
* Realese Document Edit & Enhancements

Edited Intro and Rearranged EXRAIL Enhancements

* Update release_notes_v4.1.1.md

edited indents

* Update release_notes_v4.1.1.md

Fomating
2022-10-21 13:45:54 -04:00
Fred
e618b91900 Update version.h 2022-10-21 11:57:55 -04:00
Fred
dcab5a0e72 Create release_notes_v4.1.1.md 2022-10-20 16:25:10 -04:00
Fred
1901d9547e Update release_notes.md 2022-10-20 16:23:13 -04:00
Fred
7388d14bab Update release_notes.md 2022-10-20 16:08:40 -04:00
Harald Barth
2da28ad2db version 2022-09-18 22:23:18 +02:00
Harald Barth
06bd80438e new version 2022-09-13 22:46:43 +02:00
Harald Barth
cd15eed005 EX-RAIL bugfix: Could not read long loco addrs 2022-09-13 22:43:31 +02:00
Harald Barth
23d0158804 simplify EthernetInterface::setup, make code shorter and format according to our overall style 2022-09-05 22:19:18 +02:00
habazut
2e9e614ad5 Merge pull request #256 from bcsanches/master
Keep Ethernet singleton "alive" until connection is established.
2022-09-05 20:34:11 +02:00
Bruno Crivelari Sanches
64b1de08be Detects when ethernet cable is connected and is disconnected, also correctly handles EthernetServer tead down on such situations 2022-09-05 14:23:54 -03:00
Bruno Crivelari Sanches
34c3d10767 Keep Ethernet singleton "alive" until connection is established. 2022-09-03 17:16:33 -03:00
Harald Barth
f2eb64fd21 make service start to be outside the DONT_TOUCH_WIFI_CONF area 2022-07-31 23:07:19 +02:00
Harald Barth
a80b16acba HH not supported 2022-06-21 19:46:59 +02:00
Harald Barth
b1f5e9f48c Initial version 2022-06-21 15:04:45 +02:00
17 changed files with 641 additions and 155 deletions

View File

@@ -351,7 +351,7 @@ void DCCACK::callback(int value) {
switch (callbackState) {
case AFTER_READ:
if (ackManagerRejoin && autoPowerOff) {
if (ackManagerRejoin && !autoPowerOff) {
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;

View File

@@ -25,6 +25,79 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
List of single character OPCODEs in use for reference.
When determining a new OPCODE for a new feature, refer to this list as the source of truth.
Once a new OPCODE is decided upon, update this list.
Character, Usage
/, |EX-R| interactive commands
-, Remove from reminder table
=, |TM| configuration
!, Emergency stop
@, Reserved for future use - LCD messages to JMRI
#, Request number of supported cabs/locos; heartbeat
+, WiFi AT commands
?, Reserved for future use
0, Track power off
1, Track power on
a, DCC accessory control
A,
b, Write CV bit on main
B, Write CV bit
c, Request current command
C,
d,
D, Diagnostic commands
e, Erase EEPROM
E, Store configuration in EEPROM
f, Loco decoder function control (deprecated)
F, Loco decoder function control
g,
G,
h,
H, Turnout state broadcast
i, Reserved for future use - Turntable object broadcast
I, Reserved for future use - Turntable object command and control
j, Throttle responses
J, Throttle queries
k, Reserved for future use - Potentially Railcom
K, Reserved for future use - Potentially Railcom
l, Loco speedbyte/function map broadcast
L,
m,
M, Write DCC packet
n,
N,
o,
O, Output broadcast
p, Broadcast power state
P, Write DCC packet
q, Sensor deactivated
Q, Sensor activated
r, Broadcast address read on programming track
R, Read CVs
s, Display status
S, Sensor configuration
t, Cab/loco update command
T, Turnout configuration/control
u, Reserved for user commands
U, Reserved for user commands
v,
V, Verify CVs
w, Write CV on main
W, Write CV
x,
X, Invalid command
y,
Y, Output broadcast
z,
Z, Output configuration/control
*/
#include "StringFormatter.h"
#include "DCCEXParser.h"
#include "DCC.h"
@@ -48,7 +121,7 @@
for (int16_t i=0;;i+=sizeof(flashList[0])) { \
int16_t value=GETHIGHFLASHW(flashList,i); \
if (value==INT16_MAX) break; \
if (value != 0) StringFormatter::send(stream,F(" %d"),value); \
StringFormatter::send(stream,F(" %d"),value); \
}
@@ -219,6 +292,9 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
{
#ifdef DISABLE_PROG
(void)ringStream;
#endif
#ifndef DISABLE_EEPROM
(void)EEPROM; // tell compiler not to warn this is unused
#endif
@@ -653,11 +729,15 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
SENDFLASHLIST(stream,RMFT2::rosterIdList)
}
else {
const FSH * functionNames= RMFT2::getRosterFunctions(id);
StringFormatter::send(stream,F(" %d \"%S\" \"%S\""),
id, RMFT2::getRosterName(id),
functionNames == NULL ? RMFT2::getRosterFunctions(0) : functionNames);
}
auto rosterName= RMFT2::getRosterName(id);
if (!rosterName) rosterName=F("");
auto functionNames= RMFT2::getRosterFunctions(id);
if (!functionNames) functionNames=RMFT2::getRosterFunctions(0);
if (!functionNames) functionNames=F("");
StringFormatter::send(stream,F(" %d \"%S\" \"%S\""),
id, rosterName, functionNames);
}
#endif
StringFormatter::send(stream, F(">\n"));
return;
@@ -909,7 +989,7 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
case HASH_KEYWORD_RAM: // <D RAM>
StringFormatter::send(stream, F("Free memory=%d\n"), DCCTimer::getMinimumFreeMemory());
break;
return true;
#ifndef DISABLE_PROG
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>

View File

@@ -180,8 +180,8 @@ void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) {
return;
}
pin_to_channel[pin] = --cnt_channel;
ledcAttachPin(pin, cnt_channel);
ledcSetup(cnt_channel, 1000, 8);
ledcAttachPin(pin, cnt_channel);
} else {
ledcAttachPin(pin, pin_to_channel[pin]);
}

View File

@@ -35,7 +35,7 @@
#endif
#include "DIAG.h"
#if defined(ARDUINO_NUCLEO_F411RE)
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE)
// Nucleo-64 boards don't have additional serial ports defined by default
HardwareSerial Serial1(PB7, PA15); // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F411RE
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
@@ -52,7 +52,7 @@ HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14
HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE
HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE
// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)|| defined(ARDUINO_NUCLEO_F412ZG)
// Nucleo-144 boards don't have Serial1 defined by default
HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
// Serial3 is defined to use USART3 by default, but is in fact used as the diag console

View File

@@ -1,4 +1,5 @@
/*
* @ 2024 Arkadiusz Hahn
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
@@ -35,6 +36,8 @@
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
bool DCCWaveform::supportsRailcom=false;
bool DCCWaveform::useRailcom=false;
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
@@ -62,6 +65,20 @@ const bool signalTransform[]={
/* WAVE_PENDING (should not happen) -> */ LOW};
void DCCWaveform::begin() {
// supportsRailcom depends on hardware capability
supportsRailcom = TrackManager::isRailcomCapable();
// useRailcom is user switchable at run time.
useRailcom=supportsRailcom;
if (useRailcom) {
DIAG(F("Railcom is enabled"));
} else {
DIAG(F("Railcom is disabled"));
}
TrackManager::setCutout(false,false);
TrackManager::setPROGCutout(false,false);
DCCTimer::begin(DCCWaveform::interruptHandler);
}
@@ -69,13 +86,28 @@ void DCCWaveform::loop() {
// empty placemarker in case ESP32 needs something here
}
bool DCCWaveform::setUseRailcom(bool on) {
if (!supportsRailcom) return false;
useRailcom=on;
if (!on) {
// turn off any existing cutout
TrackManager::setCutout(false);
TrackManager::setPROGCutout(false);
}
return true;
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::interruptHandler() {
// call the timer edge sensitive actions for progtrack and maintrack
// member functions would be cleaner but have more overhead
byte sigMain=signalTransform[mainTrack.state];
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
byte sigMain= signalTransform[mainTrack.state];
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
// Set the signal state for both tracks
TrackManager::setDCCSignal(sigMain);
@@ -84,15 +116,26 @@ void DCCWaveform::interruptHandler() {
// Refresh the values in the ADCee object buffering the values of the ADC HW
ADCee::scan();
// WAVE_START is at start of bit where we need to find
// out if this is an railcom start or stop time
if (useRailcom) {
if (mainTrack.state==WAVE_MID_1) mainTrack.railcom2(true);
if (mainTrack.state==WAVE_START) mainTrack.railcom2(false);
if (progTrack.state==WAVE_MID_1) progTrack.railcom2(true);
if (progTrack.state==WAVE_START) progTrack.railcom2(false);
}
// Move on in the state engine
mainTrack.state=stateTransform[mainTrack.state];
progTrack.state=stateTransform[progTrack.state];
// WAVE_PENDING means we dont yet know what the next bit is
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();
else DCCACK::checkAck(progTrack.getResets());
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
if (progTrack.state==WAVE_PENDING) {
progTrack.interrupt2();
} else {
DCCACK::checkAck(progTrack.getResets());
}
}
#pragma GCC pop_options
@@ -110,11 +153,39 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
state = WAVE_START;
// The +1 below is to allow the preamble generator to create the stop bit
// for the previous packet.
requiredPreambles = preambleBits+1;
requiredPreambles = preambleBits+1;
//requiredPreambles <<=1; // double the number of preamble wave halves
remainingPreambles=0;
bytes_sent = 0;
bits_sent = 0;
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::railcom2(bool starting) {
bool cutout;
if (starting && remainingPreambles==(requiredPreambles-2)) {
cutout=true;
} else if (!starting && remainingPreambles==(requiredPreambles-5)) {
cutout=false;
} else {
return; // neither start or end of cutout, do nothing
}
if (isMainTrack) {
if (TrackManager::progTrackSyncMain) {// we are main track and synced so we take care of prog track as well
TrackManager::setPROGCutout(cutout,true);
}
TrackManager::setCutout(cutout,true);
} else {
if (!TrackManager::progTrackSyncMain) {// we are prog track and not synced so we take care of ourselves
TrackManager::setPROGCutout(cutout,true);
}
}
}
#pragma GCC pop_options
#pragma GCC push_options
@@ -127,19 +198,20 @@ void DCCWaveform::interrupt2() {
if (remainingPreambles > 0 ) {
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
remainingPreambles--;
// Update free memory diagnostic as we don't have anything else to do this time.
// Allow for checkAck and its called functions using 22 bytes more.
DCCTimer::updateMinimumFreeMemoryISR(22);
return;
}
// Wave has gone HIGH but what happens next depends on the bit to be transmitted
// beware OF 9-BIT MASK generating a zero to start each byte
state=(transmitPacket[bytes_sent] & bitMask[bits_sent])? WAVE_MID_1 : WAVE_HIGH_0;
bits_sent++;
// If this is the last bit of a byte, prepare for the next byte
if (bits_sent == 9) { // zero followed by 8 bits of a byte
//end of Byte
bits_sent = 0;
@@ -149,30 +221,30 @@ void DCCWaveform::interrupt2() {
// end of transmission buffer... repeat or switch to next message
bytes_sent = 0;
remainingPreambles = requiredPreambles;
if (transmitRepeats > 0) {
transmitRepeats--;
transmitRepeats--;
}
else if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
}
else {
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
}
}
}
}
#pragma GCC pop_options

View File

@@ -33,9 +33,9 @@
// Number of preamble bits.
const int PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
const int PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
@@ -52,6 +52,11 @@ class DCCWaveform {
static void loop();
static DCCWaveform mainTrack;
static DCCWaveform progTrack;
static bool supportsRailcom;
static bool useRailcom;
static bool setUseRailcom(bool on);
inline void clearRepeats() { transmitRepeats=0; }
#ifndef ARDUINO_ARCH_ESP32
inline void clearResets() { sentResetsSincePacket=0; }
@@ -87,6 +92,7 @@ class DCCWaveform {
#endif
static void interruptHandler();
void interrupt2();
void railcom2(bool starting);
bool isMainTrack;
// Transmission controller

View File

@@ -259,8 +259,9 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
}
void RMFT2::setTurnoutHiddenState(Turnout * t) {
// turnout descriptions are in low flash F strings
t->setHidden(GETFLASH(getTurnoutDescription(t->getId()))==0x01);
// turnout descriptions are in low flash F strings
const FSH *desc = getTurnoutDescription(t->getId());
if (desc) t->setHidden(GETFLASH(desc)==0x01);
}
char RMFT2::getRouteType(int16_t id) {

View File

@@ -172,6 +172,8 @@ void RMFT2::printMessage(uint16_t id) {
#include "EXRAIL2MacroReset.h"
#undef TURNOUT
#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)
#undef TURNOUTL
#define TURNOUTL(id,addr,description...) O_DESC(id,description)
#undef PIN_TURNOUT
#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)
#undef SERVO_TURNOUT

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202307091003Z"
#define GITHUB_SHA "railcomtests-202401142146Z"

112
IO_PCA9555.h Normal file
View File

@@ -0,0 +1,112 @@
/*
* © 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_pca9555_h
#define io_pca9555_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for PCA9555 16-bit I/O expander (NXP & Texas Instruments).
*/
class PCA9555 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
new PCA9555(vpin, min(nPins,16), I2CAddress, interruptPin);
}
// Constructor
PCA9555(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("PCA9555"), vpin, nPins, I2CAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_INPUT_P0;
}
private:
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);
}
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 & REG_CONF_P1 for in-use pins that are outputs, 1 for others.
// PCA9555 & TCA9555, Interrupt is always enabled for raising and falling edge
uint16_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 3, REG_CONF_P0, temp, temp>>8);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_INPUT_P0);
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];
/* PCA9555 Int bug fix, from PCA9555 datasheet: "must change command byte to something besides 00h
* after a Read operation to the PCA9555 device or before reading from
* another device"
* Recommended solution, read from REG_OUTPUT_P0, then do nothing with the received data
* Issue not seen during testing, uncomment if needed
*/
//I2CManager.read(_I2CAddress, buffer, 2, 1, REG_OUTPUT_P0);
} 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];
else
_portInputState = 0xffff;
}
void _setupDevice() override {
// HAL API calls
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[2];
uint8_t outputBuffer[1];
enum {
REG_INPUT_P0 = 0x00,
REG_INPUT_P1 = 0x01,
REG_OUTPUT_P0 = 0x02,
REG_OUTPUT_P1 = 0x03,
REG_POL_INV_P0 = 0x04,
REG_POL_INV_P1 = 0x05,
REG_CONF_P0 = 0x06,
REG_CONF_P1 = 0x07,
};
};
#endif

View File

@@ -1,4 +1,4 @@
/*
/* @ 2024 Arkadiusz Hahn
* © 2022-2023 Paul M Antoine
* © 2021 Mike S
* © 2021 Fred Decker
@@ -32,6 +32,7 @@ unsigned long MotorDriver::globalOverloadStart = 0;
volatile portreg_t shadowPORTA;
volatile portreg_t shadowPORTB;
volatile portreg_t shadowPORTC;
volatile portreg_t shadowPORTH;
MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin,
byte current_pin, float sense_factor, unsigned int trip_milliamps, int16_t fault_pin) {
@@ -52,17 +53,17 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin.shadowinout = NULL;
if (HAVE_PORTA(fastSignalPin.inout == &PORTA)) {
DIAG(F("Found PORTA pin %d"),signalPin);
DIAG(F("Found SignalPin PORTA pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastSignalPin.inout == &PORTB)) {
DIAG(F("Found PORTB pin %d"),signalPin);
DIAG(F("Found SignalPin PORTB pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastSignalPin.inout == &PORTC)) {
DIAG(F("Found PORTC pin %d"),signalPin);
DIAG(F("Found SignalPin PORTC pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTC;
}
@@ -75,24 +76,24 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin2.shadowinout = NULL;
if (HAVE_PORTA(fastSignalPin2.inout == &PORTA)) {
DIAG(F("Found PORTA pin %d"),signalPin2);
DIAG(F("Found SignalPin2 PORTA pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastSignalPin2.inout == &PORTB)) {
DIAG(F("Found PORTB pin %d"),signalPin2);
DIAG(F("Found SignalPin2 PORTB pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastSignalPin2.inout == &PORTC)) {
DIAG(F("Found PORTC pin %d"),signalPin2);
DIAG(F("Found SignalPin2 PORTC pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTC;
}
}
else dualSignal=false;
if (brake_pin!=UNUSED_PIN){
if (brake_pin!=UNUSED_PIN) {
invertBrake=brake_pin < 0;
if (invertBrake)
brake_pin = 0-brake_pin;
@@ -102,6 +103,31 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
getFastPin(F("BRAKE"),brakePin,fastBrakePin);
// if brake is used for railcom cutout we need to do PORTX register trick here as well
pinMode(brakePin, OUTPUT);
fastBrakePin.shadowinout = NULL;
//DIAG(F("Found BrakePin %d "), brake_pin);
if (HAVE_PORTA(fastBrakePin.inout == &PORTA)) {
DIAG(F("Found BrakePin PORTA pin %d"),brakePin);
fastBrakePin.shadowinout = fastBrakePin.inout;
fastBrakePin.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastBrakePin.inout == &PORTB)) {
DIAG(F("Found BrakePin PORTB pin %d"),brakePin);
fastBrakePin.shadowinout = fastBrakePin.inout;
fastBrakePin.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastBrakePin.inout == &PORTC)) {
DIAG(F("Found BrakePin PORTC pin %d"),brakePin);
fastBrakePin.shadowinout = fastBrakePin.inout;
fastBrakePin.inout = &shadowPORTC;
}
if (HAVE_PORTH(fastBrakePin.inout == &PORTH)) {
DIAG(F("Found BrakePin PORTH pin %d"),brakePin);
fastBrakePin.shadowinout = fastBrakePin.inout;
fastBrakePin.inout = &shadowPORTH;
}
setBrake(true); // start with brake on in case we hace DC stuff going on
} else {
brakePin=UNUSED_PIN;
@@ -170,6 +196,9 @@ bool MotorDriver::isPWMCapable() {
return (!dualSignal) && DCCTimer::isPWMPin(signalPin);
}
bool MotorDriver::isRailcomCapable() {
return (!dualSignal) && (brakePin!=UNUSED_PIN);
}
void MotorDriver::setPower(POWERMODE mode) {
if (powerMode == mode) return;
@@ -195,23 +224,6 @@ void MotorDriver::setPower(POWERMODE mode) {
powerMode=mode;
}
// setBrake applies brake if on == true. So to get
// voltage from the motor bride one needs to do a
// setBrake(false).
// If the brakePin is negative that means the sense
// of the brake pin on the motor bridge is inverted
// (HIGH == release brake) and setBrake does
// compensate for that.
//
void MotorDriver::setBrake(bool on, bool interruptContext) {
if (brakePin == UNUSED_PIN) return;
if (!interruptContext) {noInterrupts();}
if (on ^ invertBrake)
setHIGH(fastBrakePin);
else
setLOW(fastBrakePin);
if (!interruptContext) {interrupts();}
}
bool MotorDriver::canMeasureCurrent() {
return currentPin!=UNUSED_PIN;
@@ -278,6 +290,7 @@ void MotorDriver::startCurrentFromHW() {
#endif //ANALOG_READ_INTERRUPT
#if defined(ARDUINO_ARCH_ESP32)
#ifdef VARIABLE_TONES
uint16_t taurustones[28] = { 165, 175, 196, 220,
247, 262, 294, 330,
349, 392, 440, 494,
@@ -286,17 +299,43 @@ uint16_t taurustones[28] = { 165, 175, 196, 220,
330, 284, 262, 247,
220, 196, 175, 165 };
#endif
#endif
void MotorDriver::setDCSignal(byte speedcode) {
if (brakePin == UNUSED_PIN)
return;
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
TCCR2B = (TCCR2B & B11111000) | B00000110; // set divisor on timer 2 to result in (approx) 122.55Hz
// 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.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
TCCR2B = (TCCR2B & B11111000) | B00000110; // set divisor on timer 2 to result in (approx) 122.55Hz
TCCR4B = (TCCR4B & B11111000) | B00000100; // same for timer 4 but maxcount and thus divisor differs
TCCR5B = (TCCR5B & B11111000) | B00000100; // same for timer 5 which is like timer 4
case 9:
case 10:
// Timer2 (is differnet)
TCCR2A = (TCCR2A & B11111100) | B00000001; // set WGM1=0 and WGM0=1 phase correct PWM
TCCR2B = (TCCR2B & B11110000) | B00000110; // set WGM2=0 ; set divisor on timer 2 to 1/256 for 122.55Hz
//DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
#endif
default:
break;
}
// spedcoode is a dcc speed & direction
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
byte tDir=speedcode & 0x80;
@@ -304,11 +343,13 @@ void MotorDriver::setDCSignal(byte speedcode) {
#if defined(ARDUINO_ARCH_ESP32)
{
int f = 131;
#ifdef VARIABLE_TONES
if (tSpeed > 2) {
if (tSpeed <= 58) {
f = taurustones[ (tSpeed-2)/2 ] ;
}
}
#endif
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup
}
#endif
@@ -347,7 +388,60 @@ void MotorDriver::setDCSignal(byte speedcode) {
interrupts();
}
}
void MotorDriver::throttleInrush(bool on) {
if (brakePin == UNUSED_PIN)
return;
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT)))
return;
byte duty = on ? 208 : 0;
if (invertBrake)
duty = 255-duty;
#if defined(ARDUINO_ARCH_ESP32)
if(on) {
DCCTimer::DCCEXanalogWrite(brakePin,duty);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500);
} else {
ledcDetachPin(brakePin);
}
#else
if(on){
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// 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.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is different)
TCCR2A = (TCCR2A & B11111100) | B00000011; // set WGM0=1 and WGM1=1 for fast PWM
TCCR2B = (TCCR2B & B11110000) | B00000001; // set WGM2=0 and prescaler div=1 (max)
DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
#endif
default:
break;
}
}
analogWrite(brakePin,duty);
#endif
}
unsigned int MotorDriver::raw2mA( int raw) {
//DIAG(F("%d = %d * %d / %d"), (int32_t)raw * senseFactorInternal / senseScale, raw, senseFactorInternal, senseScale);
return (int32_t)raw * senseFactorInternal / senseScale;
@@ -373,7 +467,7 @@ void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & res
result.inout = portOutputRegister(port);
result.maskHIGH = digitalPinToBitMask(pin);
result.maskLOW = ~result.maskHIGH;
// DIAG(F(" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH);
//DIAG(F("MotorDriver::getFastPin port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH);
}
///////////////////////////////////////////////////////////////////////////////////////////
@@ -473,29 +567,31 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
case POWERMODE::ALERT: {
// set local flags that handle how much is output to diag (do not output duplicates)
bool notFromOverload = (lastPowerMode != POWERMODE::OVERLOAD);
bool newPowerMode = (powerMode != lastPowerMode);
bool powerModeChange = (powerMode != lastPowerMode);
unsigned long now = micros();
if (newPowerMode)
if (powerModeChange)
lastBadSample = now;
lastPowerMode = POWERMODE::ALERT;
// check how long we have been in this state
unsigned long mslpc = microsSinceLastPowerChange(POWERMODE::ALERT);
if(checkFault()) {
throttleInrush(true);
lastBadSample = now;
unsigned long timeout = checkCurrent(useProgLimit) ? POWER_SAMPLE_IGNORE_FAULT_HIGH : POWER_SAMPLE_IGNORE_FAULT_LOW;
if ( mslpc < timeout) {
if (newPowerMode)
if (powerModeChange)
DIAG(F("TRACK %c FAULT PIN (%M ignore)"), trackno + 'A', timeout);
break;
}
DIAG(F("TRACK %c FAULT PIN detected after %4M. Pause %4M)"), trackno + 'A', mslpc, power_sample_overload_wait);
throttleInrush(false);
setPower(POWERMODE::OVERLOAD);
break;
}
if (checkCurrent(useProgLimit)) {
lastBadSample = now;
if (mslpc < POWER_SAMPLE_IGNORE_CURRENT) {
if (newPowerMode) {
if (powerModeChange) {
unsigned int mA=raw2mA(lastCurrent);
DIAG(F("TRACK %c CURRENT (%M ignore) %dmA"), trackno + 'A', POWER_SAMPLE_IGNORE_CURRENT, mA);
}
@@ -505,6 +601,7 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
unsigned int maxmA=raw2mA(tripValue);
DIAG(F("TRACK %c POWER OVERLOAD %4dmA (max %4dmA) detected after %4M. Pause %4M"),
trackno + 'A', mA, maxmA, mslpc, power_sample_overload_wait);
throttleInrush(false);
setPower(POWERMODE::OVERLOAD);
break;
}
@@ -515,6 +612,7 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
unsigned int mA=raw2mA(lastCurrent);
DIAG(F("TRACK %c NORMAL (after %M/%M) %dmA"), trackno + 'A', goodtime, mslpc, mA);
}
throttleInrush(false);
setPower(POWERMODE::ON);
}
break;

View File

@@ -27,6 +27,10 @@
#include "IODevice.h"
#include "DCCTimer.h"
// use powers of two so we can do logical and/or on the track modes in if clauses.
enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
TRACK_MODE_DC = 8, TRACK_MODE_DCX = 16, TRACK_MODE_EXT = 32};
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
@@ -39,6 +43,7 @@
#define HAVE_PORTA(X) X
#define HAVE_PORTB(X) X
#define HAVE_PORTC(X) X
#define HAVE_PORTH(X) X
#endif
#if defined(ARDUINO_AVR_UNO)
#define HAVE_PORTB(X) X
@@ -70,7 +75,9 @@
#ifndef HAVE_PORTC
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTH
#define HAVE_PORTH(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
// Virtualised Motor shield 1-track hardware Interface
#ifndef UNUSED_PIN // sync define with the one in MotorDrivers.h
@@ -106,6 +113,7 @@ struct FASTPIN {
extern volatile portreg_t shadowPORTA;
extern volatile portreg_t shadowPORTB;
extern volatile portreg_t shadowPORTC;
extern volatile portreg_t shadowPORTH;
enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT };
@@ -114,37 +122,62 @@ class MotorDriver {
MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin,
byte current_pin, float senseFactor, unsigned int tripMilliamps, int16_t fault_pin);
void setPower( POWERMODE mode);
POWERMODE getPower() { return powerMode;}
// as the port registers can be shadowed to get syncronized DCC signals
// we need to take care of that and we have to turn off interrupts if
// we setSignal() or setBrake() or setPower() during that time as
// otherwise the call from interrupt context can undo whatever we do
// from outside interrupt
void setBrake( bool on, bool interruptContext=false);
__attribute__((always_inline)) inline void setSignal( bool high) {
// setBrake applies brake if on == true. So to get
// voltage from the motor bride one needs to do a
// setBrake(false).
// If the brakePin is negative that means the sense
// of the brake pin on the motor bridge is inverted
// (HIGH == release brake) and setBrake does
// compensate for that.
__attribute__((always_inline)) inline void setBrake(bool on, bool interruptContext=false) {
if (brakePin == UNUSED_PIN) return;
if (!interruptContext) {noInterrupts();}
if (on ^ invertBrake) {
setHIGH(fastBrakePin);
} else {
setLOW(fastBrakePin);
}
if (!interruptContext) {interrupts();}
};
__attribute__((always_inline)) inline void setSignal( bool high) {
if (trackPWM) {
DCCTimer::setPWM(signalPin,high);
}
else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
}
else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
DCCTimer::setPWM(signalPin,high);
} else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
} else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
}
};
inline void enableSignal(bool on) {
if (on)
pinMode(signalPin, OUTPUT);
else
pinMode(signalPin, INPUT);
if (on) {
pinMode(signalPin, OUTPUT);
} else {
pinMode(signalPin, INPUT);
}
};
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
void setDCSignal(byte speedByte);
void throttleInrush(bool on);
inline void detachDCSignal() {
#if defined(__arm__)
pinMode(brakePin, OUTPUT);
@@ -173,6 +206,7 @@ class MotorDriver {
return rawCurrentTripValue;
}
bool isPWMCapable();
bool isRailcomCapable();
bool canMeasureCurrent();
bool trackPWM = false; // this track uses PWM timer to generate the DCC waveform
bool commonFaultPin = false; // This is a stupid motor shield which has only a common fault pin for both outputs
@@ -203,12 +237,18 @@ class MotorDriver {
bool sampleCurrentFromHW();
void startCurrentFromHW();
#endif
inline void setMode(TRACK_MODE m) {
trackMode = m;
};
inline TRACK_MODE getMode() {
return trackMode;
};
private:
char trackLetter = '?';
bool isProgTrack = false; // tells us if this is a prog track
void getFastPin(const FSH* type,int pin, bool input, FASTPIN & result);
inline void getFastPin(const FSH* type,int pin, FASTPIN & result) {
getFastPin(type, pin, 0, result);
getFastPin(type, pin, 0, result);
};
// side effect sets lastCurrent and tripValue
inline bool checkCurrent(bool useProgLimit) {
@@ -279,6 +319,7 @@ class MotorDriver {
static const int TRIP_CURRENT_PROG=250;
unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;
unsigned int power_good_counter = 0;
TRACK_MODE trackMode = TRACK_MODE_NONE; // we assume track not assigned at startup
};
#endif

View File

@@ -1,4 +1,4 @@
/*
/* @ 2024 Arkadiusz Hahn
* © 2022 Chris Harlow
* © 2022 Harald Barth
* All rights reserved.
@@ -31,20 +31,20 @@
#define APPLY_BY_MODE(findmode,function) \
FOR_EACH_TRACK(t) \
if (trackMode[t]==findmode) \
if (track[t]->getMode()==findmode) \
track[t]->function;
#ifndef DISABLE_PROG
const int16_t HASH_KEYWORD_PROG = -29718;
#endif
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_OFF = 22479;
const int16_t HASH_KEYWORD_NONE = -26550;
const int16_t HASH_KEYWORD_DC = 2183;
const int16_t HASH_KEYWORD_DCX = 6463; // DC reversed polarity
const int16_t HASH_KEYWORD_EXT = 8201; // External DCC signal
const int16_t HASH_KEYWORD_A = 65; // parser makes single chars the ascii.
MotorDriver * TrackManager::track[MAX_TRACKS];
TRACK_MODE TrackManager::trackMode[MAX_TRACKS];
int16_t TrackManager::trackDCAddr[MAX_TRACKS];
POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF;
@@ -53,7 +53,7 @@ bool TrackManager::progTrackSyncMain=false;
bool TrackManager::progTrackBoosted=false;
int16_t TrackManager::joinRelay=UNUSED_PIN;
#ifdef ARDUINO_ARCH_ESP32
byte TrackManager::tempProgTrack=MAX_TRACKS+1;
byte TrackManager::tempProgTrack=MAX_TRACKS+1; // MAX_TRACKS+1 is the unused flag
#endif
#ifdef ANALOG_READ_INTERRUPT
@@ -74,7 +74,7 @@ void TrackManager::sampleCurrent() {
waiting = false;
tr++;
if (tr > lastTrack) tr = 0;
if (lastTrack < 2 || trackMode[tr] & TRACK_MODE_PROG) {
if (lastTrack < 2 || track[tr]->getMode() & TRACK_MODE_PROG) {
return; // We could continue but for prog track we
// rather do it in next interrupt beacuse
// that gives us well defined sampling point.
@@ -85,7 +85,7 @@ void TrackManager::sampleCurrent() {
if (!waiting) {
// look for a valid track to sample or until we are around
while (true) {
if (trackMode[tr] & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_DCX|TRACK_MODE_EXT )) {
if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_DCX|TRACK_MODE_EXT )) {
track[tr]->startCurrentFromHW();
// for scope debug track[1]->setBrake(1);
waiting = true;
@@ -127,10 +127,10 @@ void TrackManager::Setup(const FSH * shieldname,
FOR_EACH_TRACK(t) {
for (byte s=t+1;s<=lastTrack;s++) {
if (track[t]->getFaultPin() != UNUSED_PIN &&
track[t]->getFaultPin() == track[s]->getFaultPin()) {
track[t]->setCommonFaultPin();
track[s]->setCommonFaultPin();
DIAG(F("Common Fault pin tracks %c and %c"), t+'A', s+'A');
track[t]->getFaultPin() == track[s]->getFaultPin()) {
track[t]->setCommonFaultPin();
track[s]->setCommonFaultPin();
DIAG(F("Common Fault pin tracks %c and %c"), t+'A', s+'A');
}
}
}
@@ -138,12 +138,12 @@ void TrackManager::Setup(const FSH * shieldname,
}
void TrackManager::addTrack(byte t, MotorDriver* driver) {
trackMode[t]=TRACK_MODE_OFF;
track[t]=driver;
if (driver) {
track[t]->setPower(POWERMODE::OFF);
track[t]->setTrackLetter('A'+t);
lastTrack=t;
track[t]->setPower(POWERMODE::OFF);
track[t]->setMode(TRACK_MODE_NONE);
track[t]->setTrackLetter('A'+t);
lastTrack=t;
}
}
@@ -159,10 +159,41 @@ void TrackManager::setDCCSignal( bool on) {
HAVE_PORTC(PORTC=shadowPORTC);
}
void TrackManager::setCutout( bool on) {
(void) on;
// TODO Cutout needs fake ports as well
// TODO APPLY_BY_MODE(TRACK_MODE_MAIN,setCutout(on));
// setCutout() for MAIN track
void TrackManager::setCutout( bool on,bool interruptContext) {
//(void) on; // avoid compiler warning -Wunused
// Cutout needs fake ports as well
HAVE_PORTA(shadowPORTA=PORTA);
HAVE_PORTB(shadowPORTB=PORTB);
HAVE_PORTC(shadowPORTC=PORTC);
HAVE_PORTH(shadowPORTH=PORTH);
APPLY_BY_MODE(TRACK_MODE_MAIN,setBrake(on,interruptContext));
HAVE_PORTA(PORTA=shadowPORTA);
HAVE_PORTB(PORTB=shadowPORTB);
HAVE_PORTC(PORTC=shadowPORTC);
HAVE_PORTH(PORTH=shadowPORTH);
}
void TrackManager::setPROGCutout( bool on,bool interruptContext) {
HAVE_PORTA(shadowPORTA=PORTA);
HAVE_PORTB(shadowPORTB=PORTB);
HAVE_PORTC(shadowPORTC=PORTC);
HAVE_PORTH(shadowPORTH=PORTH);
APPLY_BY_MODE(TRACK_MODE_PROG,setBrake(on,interruptContext));
HAVE_PORTA(PORTA=shadowPORTA);
HAVE_PORTB(PORTB=shadowPORTB);
HAVE_PORTC(PORTC=shadowPORTC);
HAVE_PORTH(PORTH=shadowPORTH);
}
// true when there is any railcom capable MAIN track
bool TrackManager::isRailcomCapable() {
FOR_EACH_TRACK(t) {
if((track[t]->getMode()==TRACK_MODE_MAIN) && (track[t]->isRailcomCapable())){
return true;
}
}
return false;
}
// setPROGSignal(), called from interrupt context
@@ -182,22 +213,27 @@ void TrackManager::setPROGSignal( bool on) {
// with interrupts turned off around the critical section
void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
FOR_EACH_TRACK(t) {
if (trackDCAddr[t]!=cab) continue;
if (trackMode[t]==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte);
else if (trackMode[t]==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128);
if (trackDCAddr[t]!=cab && cab != 0) continue;
if (track[t]->getMode()==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte);
else if (track[t]->getMode()==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128);
}
}
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
//DIAG(F("Track=%c"),trackToSet+'A');
//DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode);
// DC tracks require a motorDriver that can set brake!
if ((mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX)
&& !track[trackToSet]->brakeCanPWM()) {
DIAG(F("Brake pin can't PWM: No DC"));
return false;
}
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
#if defined(ARDUINO_AVR_UNO)
DIAG(F("Uno has no PWM timers available for DC"));
return false;
#endif
if (!track[trackToSet]->brakeCanPWM()) {
DIAG(F("Brake pin can't PWM: No DC"));
return false;
}
}
#ifdef ARDUINO_ARCH_ESP32
// remove pin from MUX matrix and turn it off
@@ -218,9 +254,9 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
#endif
// only allow 1 track to be prog
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG && t != trackToSet) {
if (track[t]->getMode()==TRACK_MODE_PROG && t != trackToSet) {
track[t]->setPower(POWERMODE::OFF);
trackMode[t]=TRACK_MODE_OFF;
track[t]->setMode(TRACK_MODE_NONE);
track[t]->makeProgTrack(false); // revoke prog track special handling
streamTrackState(NULL,t);
}
@@ -228,7 +264,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
} else {
track[trackToSet]->makeProgTrack(false); // only the prog track knows it's type
}
trackMode[trackToSet]=mode;
track[trackToSet]->setMode(mode);
trackDCAddr[trackToSet]=dcAddr;
streamTrackState(NULL,trackToSet);
@@ -255,7 +291,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
// DC tracks must not have the DCC PWM switched on
// so we globally turn it off if one of the PWM
// capable tracks is now DC or DCX.
if (trackMode[t]==TRACK_MODE_DC || trackMode[t]==TRACK_MODE_DCX) {
if (track[t]->getMode()==TRACK_MODE_DC || track[t]->getMode()==TRACK_MODE_DCX) {
if (track[t]->isPWMCapable()) {
canDo=false; // this track is capable but can not run PWM
break; // in this mode, so abort and prevent globally below
@@ -263,7 +299,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
track[t]->trackPWM=false; // this track sure can not run with PWM
//DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A');
}
} else if (trackMode[t]==TRACK_MODE_MAIN || trackMode[t]==TRACK_MODE_PROG) {
} else if (track[t]->getMode()==TRACK_MODE_MAIN || track[t]->getMode()==TRACK_MODE_PROG) {
track[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here
//DIAG(F("Track %c trackPWM %d"), t+'A', track[t]->trackPWM);
canDo &= track[t]->trackPWM;
@@ -301,7 +337,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
void TrackManager::applyDCSpeed(byte t) {
uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]);
if (trackMode[t]==TRACK_MODE_DCX)
if (track[t]->getMode()==TRACK_MODE_DCX)
speedByte = speedByte ^ 128; // reverse direction bit
track[t]->setDCSignal(speedByte);
}
@@ -328,8 +364,8 @@ bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[])
return setTrackMode(p[0],TRACK_MODE_PROG);
#endif
if (params==2 && p[1]==HASH_KEYWORD_OFF) // <= id OFF>
return setTrackMode(p[0],TRACK_MODE_OFF);
if (params==2 && (p[1]==HASH_KEYWORD_OFF || p[1]==HASH_KEYWORD_NONE)) // <= id OFF> <= id NONE>
return setTrackMode(p[0],TRACK_MODE_NONE);
if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT>
return setTrackMode(p[0],TRACK_MODE_EXT);
@@ -347,7 +383,7 @@ void TrackManager::streamTrackState(Print* stream, byte t) {
// null stream means send to commandDistributor for broadcast
if (track[t]==NULL) return;
auto format=F("");
switch(trackMode[t]) {
switch(track[t]->getMode()) {
case TRACK_MODE_MAIN:
format=F("<= %c MAIN>\n");
break;
@@ -356,8 +392,8 @@ void TrackManager::streamTrackState(Print* stream, byte t) {
format=F("<= %c PROG>\n");
break;
#endif
case TRACK_MODE_OFF:
format=F("<= %c OFF>\n");
case TRACK_MODE_NONE:
format=F("<= %c NONE>\n");
break;
case TRACK_MODE_EXT:
format=F("<= %c EXT>\n");
@@ -387,13 +423,13 @@ void TrackManager::loop() {
if (nextCycleTrack>lastTrack) nextCycleTrack=0;
if (track[nextCycleTrack]==NULL) return;
MotorDriver * motorDriver=track[nextCycleTrack];
bool useProgLimit=dontLimitProg? false: trackMode[nextCycleTrack]==TRACK_MODE_PROG;
bool useProgLimit=dontLimitProg? false: track[nextCycleTrack]->getMode()==TRACK_MODE_PROG;
motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);
}
MotorDriver * TrackManager::getProgDriver() {
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG) return track[t];
if (track[t]->getMode()==TRACK_MODE_PROG) return track[t];
return NULL;
}
@@ -401,7 +437,7 @@ MotorDriver * TrackManager::getProgDriver() {
std::vector<MotorDriver *>TrackManager::getMainDrivers() {
std::vector<MotorDriver *> v;
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_MAIN) v.push_back(track[t]);
if (track[t]->getMode()==TRACK_MODE_MAIN) v.push_back(track[t]);
return v;
}
#endif
@@ -411,7 +447,7 @@ void TrackManager::setPower2(bool setProg,POWERMODE mode) {
FOR_EACH_TRACK(t) {
MotorDriver * driver=track[t];
if (!driver) continue;
switch (trackMode[t]) {
switch (track[t]->getMode()) {
case TRACK_MODE_MAIN:
if (setProg) break;
// toggle brake before turning power on - resets overcurrent error
@@ -439,7 +475,7 @@ void TrackManager::setPower2(bool setProg,POWERMODE mode) {
driver->setBrake(false);
driver->setPower(mode);
break;
case TRACK_MODE_OFF:
case TRACK_MODE_NONE:
break;
}
}
@@ -447,8 +483,8 @@ void TrackManager::setPower2(bool setProg,POWERMODE mode) {
POWERMODE TrackManager::getProgPower() {
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG)
return track[t]->getPower();
if (track[t]->getMode()==TRACK_MODE_PROG)
return track[t]->getPower();
return POWERMODE::OFF;
}
@@ -492,7 +528,7 @@ void TrackManager::setJoin(bool joined) {
#ifdef ARDUINO_ARCH_ESP32
if (joined) {
FOR_EACH_TRACK(t) {
if (trackMode[t]==TRACK_MODE_PROG) {
if (track[t]->getMode()==TRACK_MODE_PROG) {
tempProgTrack = t;
setTrackMode(t, TRACK_MODE_MAIN);
break;
@@ -500,7 +536,12 @@ void TrackManager::setJoin(bool joined) {
}
} else {
if (tempProgTrack != MAX_TRACKS+1) {
// as setTrackMode with TRACK_MODE_PROG defaults to
// power off, we will take the current power state
// of our track and then preserve that state.
POWERMODE tPTmode = track[tempProgTrack]->getPower(); //get current power status of this track
setTrackMode(tempProgTrack, TRACK_MODE_PROG);
track[tempProgTrack]->setPower(tPTmode); //set track status as it was before
tempProgTrack = MAX_TRACKS+1;
}
}

View File

@@ -27,10 +27,6 @@
#include "MotorDriver.h"
// Virtualised Motor shield multi-track hardware Interface
// use powers of two so we can do logical and/or on the track modes in if clauses.
enum TRACK_MODE : byte {TRACK_MODE_OFF = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
TRACK_MODE_DC = 8, TRACK_MODE_DCX = 16, TRACK_MODE_EXT = 32};
// These constants help EXRAIL macros say SET_TRACK(2,mode) OR SET_TRACK(C,mode) etc.
const byte TRACK_NUMBER_0=0, TRACK_NUMBER_A=0;
const byte TRACK_NUMBER_1=1, TRACK_NUMBER_B=1;
@@ -55,9 +51,11 @@ class TrackManager {
);
static void setDCCSignal( bool on);
static void setCutout( bool on);
static void setPROGSignal( bool on);
static void setDCSignal(int16_t cab, byte speedbyte);
static void setCutout( bool on,bool interruptContext=false);
static void setPROGCutout( bool on,bool interruptContext=false);
static bool isRailcomCapable();
static MotorDriver * getProgDriver();
#ifdef ARDUINO_ARCH_ESP32
static std::vector<MotorDriver *>getMainDrivers();
@@ -100,7 +98,6 @@ class TrackManager {
static POWERMODE mainPowerGuess;
static void applyDCSpeed(byte t);
static TRACK_MODE trackMode[MAX_TRACKS];
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX
#ifdef ARDUINO_ARCH_ESP32
static byte tempProgTrack; // holds the prog track number during join

View File

@@ -60,7 +60,7 @@ Stream * WifiInterface::wifiStream;
#if defined(ARDUINO_ARCH_STM32)
// Handle serial ports availability on STM32 for variants!
// #undef NUM_SERIAL
#if defined(ARDUINO_NUCLEO_F411RE)
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE)
#define NUM_SERIAL 3
#define SERIAL1 Serial1
#define SERIAL3 Serial6
@@ -68,9 +68,11 @@ Stream * WifiInterface::wifiStream;
#define NUM_SERIAL 3
#define SERIAL1 Serial3
#define SERIAL3 Serial5
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG)
#define NUM_SERIAL 2
#define SERIAL1 Serial6
#else
#warning This variant of Nucleo not yet explicitly supported
#endif
#endif
@@ -198,7 +200,23 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
// Display the AT version information
StringFormatter::send(wifiStream, F("AT+GMR\r\n"));
checkForOK(2000, true, false); // Makes this visible on the console
if (checkForOK(2000, F("AT version:"), true, false)) {
char version[] = "0.0.0.0-xxx";
for (int i=0; i<11;i++) {
while(!wifiStream->available());
version[i]=wifiStream->read();
StringFormatter::printEscape(version[i]);
}
if ((version[0] == '0') ||
(version[0] == '2' && version[2] == '0') ||
(version[0] == '2' && version[2] == '2' && version[4] == '0' && version[6] == '0'
&& version[7] == '-' && version[8] == 'd' && version[9] == 'e' && version[10] == 'v')) {
DIAG(F("You need to up/downgrade the ESP firmware"));
SSid = F("UPDATE_ESP_FIRMWARE");
forceAP = true;
}
}
checkForOK(2000, true, false);
#ifdef DONT_TOUCH_WIFI_CONF
DIAG(F("DONT_TOUCH_WIFI_CONF was set: Using existing config"));

View File

@@ -24,6 +24,7 @@
//#include "IO_TouchKeypad.h // Touch keypad with 16 keys
//#include "IO_EXTurntable.h" // Turntable-EX turntable controller
//#include "IO_EXFastClock.h" // FastClock driver
//#include "IO_PCA9555.h" // 16-bit I/O expander (NXP & Texas Instruments).
//==========================================================================
// The function halSetup() is invoked from CS if it exists within the build.
@@ -51,7 +52,7 @@ void halSetup() {
// Create a 20x4 LCD display device as display number 2
// (line 0 is written by EX-RAIL 'SCREEN(2, 0, "text")').
// HALDisplay<LiquidCrystal>(2, 0x27, 20, 4);
// HALDisplay<LiquidCrystal>::create(2, 0x27, 20, 4);
//=======================================================================

View File

@@ -3,7 +3,24 @@
#include "StringFormatter.h"
#define VERSION "4.2.65"
#define VERSION "5.0.8"
// 5.0.8 - Bugfix: Do not crash on turnouts without description
// 5.0.7 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0
// 5.0.6 - Bugfix lost TURNOUTL description
// 5.0.5 - Bugfix version detection logic and better message
// 5.0.4 - Bugfix: <JR> misses default roster.
// 5.0.3 - Check bad AT firmware version
// 5.0.2 - Bugfix: ESP32 30ms off time
// 5.0.1 - Bugfix: execute 30ms off time before rejoin
// 5.0.0 - Make 4.2.69 the 5.0.0 release
// 4.2.69 - Bugfix: Make <!> work in DC mode
// 4.2.68 - Rename track mode OFF to NONE
// 4.2.67 - AVR: Pin specific timer register seting
// - Protect Uno user from choosing DC(X)
// - More Nucleo variant defines
// - GPIO PCA9555 / TCA9555 support
// 4.2.66 - Throttle inrush current by applying PWM to brake pin when
// fault pin goes active
// 4.2.65 - new config WIFI_FORCE_AP option
// 4.2.63 - completely new overcurrent detection
// - ESP32 protect from race in RMT code