mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2024-11-23 16:16:13 +01:00
Compare commits
344 Commits
e236c150f2
...
66c83ee3a6
Author | SHA1 | Date | |
---|---|---|---|
|
66c83ee3a6 | ||
|
df358c0bc8 | ||
|
15855d1e9c | ||
|
5f65fd5944 | ||
|
a26610bc7f | ||
|
4e491a1e56 | ||
|
430161ef60 | ||
|
264a53dacf | ||
|
0c96d4ffc2 | ||
|
843fa42692 | ||
|
b17dc5a0dd | ||
|
449a5f1670 | ||
|
06b8995861 | ||
|
2172d2e175 | ||
|
86291cbec4 | ||
|
66791b19f5 | ||
|
6689a1d35f | ||
|
91818ed80c | ||
|
86310aea4f | ||
|
a610e83f6e | ||
|
1449dc7bac | ||
|
bd11cfbf8b | ||
|
16214fad66 | ||
|
76ad3ee48d | ||
|
742b100f65 | ||
|
83d4930124 | ||
|
b4e7982099 | ||
|
3af2f67792 | ||
|
c382bd33bc | ||
|
ebe8f62cf0 | ||
|
7dafe0383d | ||
|
4aa97e1731 | ||
|
91e60b3716 | ||
|
8a5a832b1d | ||
|
5ea6feb11a | ||
|
263c3d01e3 | ||
|
182479c07b | ||
|
3317b4666e | ||
|
f41f61dd5f | ||
|
6b713bf57c | ||
|
38a9585a41 | ||
|
1a307eea3d | ||
|
e4a3aa9f1e | ||
|
f581d56bdc | ||
|
7b77d4ce1e | ||
|
d367f5dc81 | ||
|
dc5f5e05b9 | ||
|
cff4075937 | ||
|
84b90ae757 | ||
|
6d7d2325da | ||
|
fdc956576b | ||
|
02bf50b909 | ||
|
c8f18e4d67 | ||
|
87073b0d36 | ||
|
0587e6fc09 | ||
|
3cda869c6e | ||
|
59d855549e | ||
|
e3081a7e56 | ||
|
8eec85edcf | ||
|
d753eb43e3 | ||
|
9aac34b403 | ||
|
be218d3032 | ||
|
4b04a80e6f | ||
|
b752666899 | ||
|
3d6f41398d | ||
|
7503421eb6 | ||
|
274affce45 | ||
|
b29a01f436 | ||
|
1101cfd637 | ||
|
3fa2edb0da | ||
|
423d1932ae | ||
|
dec39a2ae1 | ||
|
821115caad | ||
|
fe9b1da8a3 | ||
|
fbbedc7577 | ||
|
dcd332603c | ||
|
7e4093f03f | ||
|
7ee4188d88 | ||
|
5742b71ec6 | ||
|
8705c8c33f | ||
|
28d60d4984 | ||
|
e4904e4080 | ||
|
59b0e8383d | ||
|
784088b0df | ||
|
c780b96856 | ||
|
4b97d63cf3 | ||
|
6f1df6ce8e | ||
|
eacf48380b | ||
|
8293749ac7 | ||
|
25cb878060 | ||
|
7a9e225602 | ||
|
1443ea8df9 | ||
|
cd47782052 | ||
|
4931c5ed75 | ||
|
53fec9bc3a | ||
|
4780ea63cf | ||
|
5f6e18e1e7 | ||
|
be40a7e274 | ||
|
e7f82bdf92 | ||
|
63702ae64e | ||
|
7cbf4de1b9 | ||
|
3c4e4bb14d | ||
|
6d0740eab4 | ||
|
0a52a26d50 | ||
|
daa2ffc459 | ||
|
9728d19b19 | ||
|
99a09c713f | ||
|
811bce4b2a | ||
|
cf1e1c92b3 | ||
|
a5b73c823a | ||
|
657c08c653 | ||
|
bc37a2d2cf | ||
|
3c0704dbd1 | ||
|
95bf5aae38 | ||
|
8216579f62 | ||
|
a54a262f68 | ||
|
a508ee7055 | ||
|
20ae915eaf | ||
|
35a0bde115 | ||
|
d24d09c37a | ||
|
9ab6b3d4ea | ||
|
d8c282434c | ||
|
43648fd9f4 | ||
|
b5ddade2b3 | ||
|
2e4995cab3 | ||
|
796d5c4774 | ||
|
27bd444884 | ||
|
ca380d11dc | ||
|
c336ab0bb4 | ||
|
5ac26ce505 | ||
|
b51a8fe126 | ||
|
718e78fca6 | ||
|
70a1b9538c | ||
|
39d0cbb791 | ||
|
4a3d3228a9 | ||
|
8036ba1c48 | ||
|
74f7af1675 | ||
|
6f076720f7 | ||
|
d899da5898 | ||
|
3ce9d2ec88 | ||
|
9ebb1c5fb1 | ||
|
19efa749b8 | ||
|
36cc46e88d | ||
|
bba74a08f6 | ||
|
ab58c38e7b | ||
|
d4f0a7c8f3 | ||
|
ba0a41b6f2 | ||
|
bf17f2018b | ||
|
67387d2dc3 | ||
|
adb8b56c92 | ||
|
bd44184f57 | ||
|
e7d3d92c23 | ||
|
e3bab887a2 | ||
|
041a6534da | ||
|
198d762a21 | ||
|
1398cf1999 | ||
|
797028b223 | ||
|
1881d4c9ad | ||
|
18116a391c | ||
|
a1accec79a | ||
|
08f0a2b37d | ||
|
6637ea6fe7 | ||
|
a69017f8bb | ||
|
763c9d8ae6 | ||
|
753567427e | ||
|
3f4099520a | ||
|
07fd4bc309 | ||
|
1f05ef42d2 | ||
|
96fdbfdc89 | ||
|
ebaf1b984e | ||
|
697f228a05 | ||
|
c8e307db7a | ||
|
a5ccb2e29e | ||
|
42e2e69f5f | ||
|
2075bc50e8 | ||
|
a16214790e | ||
|
784934024e | ||
|
b478056a9f | ||
|
ef47257d67 | ||
|
03db06f2ee | ||
|
4308739c2b | ||
|
0cfea3e1a5 | ||
|
d0df9f3c33 | ||
|
ac4af407aa | ||
|
a236a205fe | ||
|
478e9661bb | ||
|
2c1b3e0a8f | ||
|
e7c4af5d4a | ||
|
263ed18b25 | ||
|
4e1fad4832 | ||
|
29ea746062 | ||
|
e6f33cfdee | ||
|
a7096e782c | ||
|
f935756538 | ||
|
74d11ccb1e | ||
|
2ba5adc8b4 | ||
|
102d6078a7 | ||
|
8943f2da18 | ||
|
7bd2ba9b41 | ||
|
b472230b47 | ||
|
6da3153dd5 | ||
|
b5d9798144 | ||
|
566ce1b7f8 | ||
|
1af5132e6a | ||
|
763ef8be34 | ||
|
fd6e8705c8 | ||
|
503378f1bb | ||
|
582ff890f4 | ||
|
86ed8ff8a6 | ||
|
148d4d30f8 | ||
|
b3ba647b09 | ||
|
4c89b26c79 | ||
|
e8b9f80c8c | ||
|
befcfebec7 | ||
|
9ce95c07aa | ||
|
d8cc0c632a | ||
|
d877fc315e | ||
|
6c18226cb5 | ||
|
1c5f299b0e | ||
|
fb14fbd81b | ||
|
2f3d489f18 | ||
|
d2d7a5cd16 | ||
|
337af77a03 | ||
|
670645db4b | ||
|
a4eabf235e | ||
|
2cbcecf9e6 | ||
|
26cf28dff7 | ||
|
44351b83ae | ||
|
4e08177b7b | ||
|
f2ff1ba22a | ||
|
043e6fdb26 | ||
|
24e0f189e1 | ||
|
33b2820095 | ||
|
7b3b16b211 | ||
|
27a5f76a8d | ||
|
754bd99381 | ||
|
650e411a4f | ||
|
0978bb0c11 | ||
|
6eb7051fd6 | ||
|
5726844c83 | ||
|
0214a55b23 | ||
|
7db4a9575a | ||
|
ce84974967 | ||
|
034c441c34 | ||
|
d5978b1578 | ||
|
ea4f90d5fc | ||
|
1181fd855d | ||
|
a092e06a6f | ||
|
68fd56e7fc | ||
|
370dae0ab8 | ||
|
fe618d0b85 | ||
|
2ff1619ad1 | ||
|
7afd4443d6 | ||
|
52cfc18754 | ||
|
ed0cfee091 | ||
|
25bbfa4c68 | ||
|
2a46b96083 | ||
|
17c004aecf | ||
|
9e3ae21bb8 | ||
|
624656ebc9 | ||
|
5a7f278b1e | ||
|
aacb980dc8 | ||
|
11b9fd4ef5 | ||
|
d07718be8c | ||
|
d57b5ba537 | ||
|
dab02ec659 | ||
|
6ad5326f1d | ||
|
39e1363ce0 | ||
|
c9d4f5e94d | ||
|
8052090e0f | ||
|
dfe3e9d42c | ||
|
5d810a620b | ||
|
550ad58c4d | ||
|
7a305e179c | ||
|
8437b0e7aa | ||
|
ebbeea5fbb | ||
|
a8321fff42 | ||
|
a16790f585 | ||
|
da6a3c442f | ||
|
4fcd81a118 | ||
|
eb450dbd89 | ||
|
a0562fdf5c | ||
|
7ee2c29a52 | ||
|
dba5d35aa2 | ||
|
be10be5a1a | ||
|
dca023ffd7 | ||
|
4eef9581fe | ||
|
bd02d1c15b | ||
|
004d7b6631 | ||
|
21ce87eb3e | ||
|
ab393047c1 | ||
|
1f5f7754c1 | ||
|
6adff43f4b | ||
|
1ac104704e | ||
|
2f8e915b1e | ||
|
152f9850bb | ||
|
3094c52bf8 | ||
|
86f4567556 | ||
|
dd890e65bf | ||
|
1e48c59cd8 | ||
|
004d10ee58 | ||
|
e734661d1b | ||
|
bcb250bacf | ||
|
798241927f | ||
|
df2f09f4d2 | ||
|
f40d57d8bd | ||
|
9fa213e198 | ||
|
44d8154223 | ||
|
01919b33df | ||
|
a0c1ad182c | ||
|
dbf053858b | ||
|
232ac993ec | ||
|
26ddd27ecf | ||
|
6cad794411 | ||
|
b0d8510127 | ||
|
3bfdd16288 | ||
|
df4a501e8a | ||
|
2202cb0c5e | ||
|
1425da20b5 | ||
|
b823a647ac | ||
|
2c64f10da8 | ||
|
25426d076d | ||
|
3453da0671 | ||
|
fb226311e5 | ||
|
6392c74ead | ||
|
25f8852af6 | ||
|
9842ea8a42 | ||
|
fa0aa27d46 | ||
|
57d4655d54 | ||
|
ff9c558b61 | ||
|
b277d204f0 | ||
|
c4febd1d0f | ||
|
98f8022268 | ||
|
1491da4813 | ||
|
4b2c0702a4 | ||
|
e27cceeb74 | ||
|
247763ac00 | ||
|
e327e0ae8d | ||
|
9f38dae8ba | ||
|
e51f8e9c0a | ||
|
4f43a413b5 | ||
|
4f56837d28 | ||
|
cc2846d932 | ||
|
83325ebf78 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,3 +13,6 @@ myFilter.cpp
|
||||||
my*.h
|
my*.h
|
||||||
!my*.example.h
|
!my*.example.h
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
newcode.txt.old
|
||||||
|
UserAddin.txt
|
||||||
|
.readme.txt
|
||||||
|
|
|
@ -105,6 +105,7 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
|
||||||
void CommandDistributor::forget(byte clientId) {
|
void CommandDistributor::forget(byte clientId) {
|
||||||
if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);
|
if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);
|
||||||
clients[clientId]=NONE_TYPE;
|
clients[clientId]=NONE_TYPE;
|
||||||
|
if (virtualLCDClient==clientId) virtualLCDClient=RingStream::NO_CLIENT;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -161,6 +162,10 @@ void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) {
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<I %d %d %d>\n"), id, position, moving);
|
||||||
|
}
|
||||||
|
|
||||||
void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
|
void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
|
||||||
// The JMRI clock command is of the form : PFT65871<;>4
|
// The JMRI clock command is of the form : PFT65871<;>4
|
||||||
// The CS broadcast is of the form "<jC mmmm nn" where mmmm is time minutes and dd speed
|
// The CS broadcast is of the form "<jC mmmm nn" where mmmm is time minutes and dd speed
|
||||||
|
@ -257,27 +262,128 @@ void CommandDistributor::broadcastLoco(byte slot) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandDistributor::broadcastPower() {
|
void CommandDistributor::broadcastPower() {
|
||||||
|
char pstr[] = "? x";
|
||||||
|
for(byte t=0; t<TrackManager::MAX_TRACKS; t++)
|
||||||
|
if (TrackManager::getPower(t, pstr))
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr);
|
||||||
|
|
||||||
|
byte trackcount=0;
|
||||||
|
byte oncount=0;
|
||||||
|
byte offcount=0;
|
||||||
|
for(byte t=0; t<TrackManager::MAX_TRACKS; t++) {
|
||||||
|
if (TrackManager::isActive(t)) {
|
||||||
|
trackcount++;
|
||||||
|
// do not call getPower(t) unless isActive(t)!
|
||||||
|
if (TrackManager::getPower(t) == POWERMODE::ON)
|
||||||
|
oncount++;
|
||||||
|
else
|
||||||
|
offcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//DIAG(F("t=%d on=%d off=%d"), trackcount, oncount, offcount);
|
||||||
|
|
||||||
|
char state='2';
|
||||||
|
if (oncount==0 || offcount == trackcount)
|
||||||
|
state = '0';
|
||||||
|
else if (oncount == trackcount) {
|
||||||
|
state = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional info about MAIN, PROG and JOIN
|
||||||
bool main=TrackManager::getMainPower()==POWERMODE::ON;
|
bool main=TrackManager::getMainPower()==POWERMODE::ON;
|
||||||
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
|
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
|
||||||
bool join=TrackManager::isJoined();
|
bool join=TrackManager::isJoined();
|
||||||
|
//DIAG(F("m=%d p=%d j=%d"), main, prog, join);
|
||||||
const FSH * reason=F("");
|
const FSH * reason=F("");
|
||||||
char state='1';
|
if (join) {
|
||||||
if (main && prog && join) reason=F(" JOIN");
|
reason = F(" JOIN"); // with space at start so we can append without space
|
||||||
else if (main && prog);
|
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
|
||||||
else if (main) reason=F(" MAIN");
|
} else {
|
||||||
else if (prog) reason=F(" PROG");
|
if (main) {
|
||||||
else state='0';
|
//reason = F("MAIN");
|
||||||
broadcastReply(COMMAND_TYPE, F("<p%c%S>\n"),state,reason);
|
broadcastReply(COMMAND_TYPE, F("<p1 MAIN>\n"));
|
||||||
|
}
|
||||||
|
if (prog) {
|
||||||
|
//reason = F("PROG");
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state != '2')
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
|
||||||
#ifdef CD_HANDLE_RING
|
#ifdef CD_HANDLE_RING
|
||||||
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1':'0');
|
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
|
||||||
|
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
|
||||||
#endif
|
#endif
|
||||||
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
|
|
||||||
|
LCD(2,F("Power %S%S"),state=='1'?F("On"): ( state=='0'? F("Off") : F("SC") ),reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandDistributor::broadcastRaw(clientType type, char * msg) {
|
void CommandDistributor::broadcastRaw(clientType type, char * msg) {
|
||||||
broadcastReply(type, F("%s"),msg);
|
broadcastReply(type, F("%s"),msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) {
|
void CommandDistributor::broadcastMessage(char * message) {
|
||||||
broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr);
|
broadcastReply(COMMAND_TYPE, F("<m \"%s\">\n"),message);
|
||||||
|
broadcastReply(WITHROTTLE_TYPE, F("Hm%s\n"),message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {
|
||||||
|
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
|
||||||
|
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
Print * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) {
|
||||||
|
Print * stream=virtualLCDSerial;
|
||||||
|
#ifdef CD_HANDLE_RING
|
||||||
|
rememberVLCDClient=RingStream::NO_CLIENT;
|
||||||
|
if (!stream && virtualLCDClient!=RingStream::NO_CLIENT) {
|
||||||
|
// If we are broadcasting from a wifi/eth process we need to complete its output
|
||||||
|
// before merging broadcasts in the ring, then reinstate it in case
|
||||||
|
// the process continues to output to its client.
|
||||||
|
if ((rememberVLCDClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {
|
||||||
|
ring->commit();
|
||||||
|
}
|
||||||
|
ring->mark(virtualLCDClient);
|
||||||
|
stream=ring;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (stream) StringFormatter::send(stream,F("<@ %d %d \""), screen,row);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::commitVirtualLCDSerial() {
|
||||||
|
#ifdef CD_HANDLE_RING
|
||||||
|
if (virtualLCDClient!=RingStream::NO_CLIENT) {
|
||||||
|
StringFormatter::send(ring,F("\">\n"));
|
||||||
|
ring->commit();
|
||||||
|
if (rememberVLCDClient!=RingStream::NO_CLIENT) ring->mark(rememberVLCDClient);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
StringFormatter::send(virtualLCDSerial,F("\">\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandDistributor::setVirtualLCDSerial(Print * stream) {
|
||||||
|
#ifdef CD_HANDLE_RING
|
||||||
|
virtualLCDClient=RingStream::NO_CLIENT;
|
||||||
|
if (stream && stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM) {
|
||||||
|
virtualLCDClient=((RingStream *) stream)->peekTargetMark();
|
||||||
|
virtualLCDSerial=nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
virtualLCDSerial=stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
|
||||||
|
byte CommandDistributor::virtualLCDClient=0xFF;
|
||||||
|
byte CommandDistributor::rememberVLCDClient=0;
|
||||||
|
|
||||||
|
|
|
@ -49,15 +49,27 @@ public :
|
||||||
static void broadcastLoco(byte slot);
|
static void broadcastLoco(byte slot);
|
||||||
static void broadcastSensor(int16_t id, bool value);
|
static void broadcastSensor(int16_t id, bool value);
|
||||||
static void broadcastTurnout(int16_t id, bool isClosed);
|
static void broadcastTurnout(int16_t id, bool isClosed);
|
||||||
|
static void broadcastTurntable(int16_t id, uint8_t position, bool moving);
|
||||||
static void broadcastClockTime(int16_t time, int8_t rate);
|
static void broadcastClockTime(int16_t time, int8_t rate);
|
||||||
static void setClockTime(int16_t time, int8_t rate, byte opt);
|
static void setClockTime(int16_t time, int8_t rate, byte opt);
|
||||||
static int16_t retClockTime();
|
static int16_t retClockTime();
|
||||||
static void broadcastPower();
|
static void broadcastPower();
|
||||||
static void broadcastRaw(clientType type,char * msg);
|
static void broadcastRaw(clientType type,char * msg);
|
||||||
static void broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr);
|
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
|
||||||
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
|
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
|
||||||
static void forget(byte clientId);
|
static void forget(byte clientId);
|
||||||
|
static void broadcastRouteState(uint16_t routeId,byte state);
|
||||||
|
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
|
||||||
|
static void broadcastMessage(char * message);
|
||||||
|
|
||||||
|
// Handling code for virtual LCD receiver.
|
||||||
|
static Print * getVirtualLCDSerial(byte screen, byte row);
|
||||||
|
static void commitVirtualLCDSerial();
|
||||||
|
static void setVirtualLCDSerial(Print * stream);
|
||||||
|
private:
|
||||||
|
static Print * virtualLCDSerial;
|
||||||
|
static byte virtualLCDClient;
|
||||||
|
static byte rememberVLCDClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -65,6 +65,9 @@
|
||||||
#ifdef EXRAIL_WARNING
|
#ifdef EXRAIL_WARNING
|
||||||
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
|
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
|
||||||
#endif
|
#endif
|
||||||
|
// compile time check, passwords 1 to 7 chars do not work, so do not try to compile with them at all
|
||||||
|
// remember trailing '\0', sizeof("") == 1.
|
||||||
|
#define PASSWDCHECK(S) static_assert(sizeof(S) == 1 || sizeof(S) > 8, "Password shorter than 8 chars")
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
|
@ -76,6 +79,12 @@ void setup()
|
||||||
|
|
||||||
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
|
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
|
||||||
|
|
||||||
|
// If user has defined a startup delay, delay here before starting IO
|
||||||
|
#if defined(STARTUP_DELAY)
|
||||||
|
DIAG(F("Delaying startup for %dms"), STARTUP_DELAY);
|
||||||
|
delay(STARTUP_DELAY);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
|
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
|
||||||
IODevice::begin();
|
IODevice::begin();
|
||||||
|
|
||||||
|
@ -87,7 +96,7 @@ void setup()
|
||||||
|
|
||||||
DISPLAY_START (
|
DISPLAY_START (
|
||||||
// This block is still executed for DIAGS if display not in use
|
// This block is still executed for DIAGS if display not in use
|
||||||
LCD(0,F("DCC-EX v%S"),F(VERSION));
|
LCD(0,F("DCC-EX v" VERSION));
|
||||||
LCD(1,F("Lic GPLv3"));
|
LCD(1,F("Lic GPLv3"));
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -96,10 +105,12 @@ void setup()
|
||||||
// Start Ethernet if it exists
|
// Start Ethernet if it exists
|
||||||
#ifndef ARDUINO_ARCH_ESP32
|
#ifndef ARDUINO_ARCH_ESP32
|
||||||
#if WIFI_ON
|
#if WIFI_ON
|
||||||
|
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||||
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
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
|
#endif // WIFI_ON
|
||||||
#else
|
#else
|
||||||
// ESP32 needs wifi on always
|
// ESP32 needs wifi on always
|
||||||
|
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||||
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||||
#endif // ARDUINO_ARCH_ESP32
|
#endif // ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
|
202
DCC.cpp
202
DCC.cpp
|
@ -122,7 +122,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
|
||||||
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
|
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
|
||||||
byte b[4];
|
byte b[4];
|
||||||
byte nB = 0;
|
byte nB = 0;
|
||||||
|
@ -133,7 +133,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||||
if (byte1!=0) b[nB++] = byte1;
|
if (byte1!=0) b[nB++] = byte1;
|
||||||
b[nB++] = byte2;
|
b[nB++] = byte2;
|
||||||
|
|
||||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
DCCWaveform::mainTrack.schedulePacket(b, nB, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns speed steps 0 to 127 (1 == emergency stop)
|
// returns speed steps 0 to 127 (1 == emergency stop)
|
||||||
|
@ -153,6 +153,22 @@ uint8_t DCC::getThrottleSpeedByte(int cab) {
|
||||||
return speedTable[reg].speedCode;
|
return speedTable[reg].speedCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns 0 to 7 for frequency
|
||||||
|
uint8_t DCC::getThrottleFrequency(int cab) {
|
||||||
|
#if defined(ARDUINO_AVR_UNO)
|
||||||
|
(void)cab;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
int reg=lookupSpeedTable(cab);
|
||||||
|
if (reg<0)
|
||||||
|
return 0; // use default frequency
|
||||||
|
// shift out first 29 bits so we have the 3 "frequency bits" left
|
||||||
|
uint8_t res = (uint8_t)(speedTable[reg].functions >>29);
|
||||||
|
//DIAG(F("Speed table %d functions %l shifted %d"), reg, speedTable[reg].functions, res);
|
||||||
|
return res;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// returns direction on loco
|
// returns direction on loco
|
||||||
// or true/forward on "loco not found"
|
// or true/forward on "loco not found"
|
||||||
bool DCC::getThrottleDirection(int cab) {
|
bool DCC::getThrottleDirection(int cab) {
|
||||||
|
@ -183,43 +199,55 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||||
b[nB++] = functionNumber >>7 ; // high order bits
|
b[nB++] = functionNumber >>7 ; // high order bits
|
||||||
}
|
}
|
||||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
// We use the reminder table up to 28 for normal functions.
|
||||||
|
// We use 29 to 31 for DC frequency as well so up to 28
|
||||||
|
// are "real" functions and 29 to 31 are frequency bits
|
||||||
|
// controlled by function buttons
|
||||||
|
if (functionNumber > 31)
|
||||||
|
return true;
|
||||||
|
|
||||||
int reg = lookupSpeedTable(cab);
|
int reg = lookupSpeedTable(cab);
|
||||||
if (reg<0) return false;
|
if (reg<0) return false;
|
||||||
|
|
||||||
// Take care of functions:
|
// Take care of functions:
|
||||||
// Set state of function
|
// Set state of function
|
||||||
unsigned long previous=speedTable[reg].functions;
|
uint32_t previous=speedTable[reg].functions;
|
||||||
unsigned long funcmask = (1UL<<functionNumber);
|
uint32_t funcmask = (1UL<<functionNumber);
|
||||||
if (on) {
|
if (on) {
|
||||||
speedTable[reg].functions |= funcmask;
|
speedTable[reg].functions |= funcmask;
|
||||||
} else {
|
} else {
|
||||||
speedTable[reg].functions &= ~funcmask;
|
speedTable[reg].functions &= ~funcmask;
|
||||||
}
|
}
|
||||||
if (speedTable[reg].functions != previous) {
|
if (speedTable[reg].functions != previous) {
|
||||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
if (functionNumber <= 28)
|
||||||
|
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||||
CommandDistributor::broadcastLoco(reg);
|
CommandDistributor::broadcastLoco(reg);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip function state
|
// Flip function state (used from withrottle protocol)
|
||||||
void DCC::changeFn( int cab, int16_t functionNumber) {
|
void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||||
if (cab<=0 || functionNumber>28) return;
|
if (cab<=0 || functionNumber>31) return;
|
||||||
int reg = lookupSpeedTable(cab);
|
int reg = lookupSpeedTable(cab);
|
||||||
if (reg<0) return;
|
if (reg<0) return;
|
||||||
unsigned long funcmask = (1UL<<functionNumber);
|
unsigned long funcmask = (1UL<<functionNumber);
|
||||||
speedTable[reg].functions ^= funcmask;
|
speedTable[reg].functions ^= funcmask;
|
||||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
if (functionNumber <= 28) {
|
||||||
|
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||||
|
}
|
||||||
CommandDistributor::broadcastLoco(reg);
|
CommandDistributor::broadcastLoco(reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
int DCC::getFn( int cab, int16_t functionNumber) {
|
// Report function state (used from withrottle protocol)
|
||||||
if (cab<=0 || functionNumber>28) return -1; // unknown
|
// returns 0 false, 1 true or -1 for do not know
|
||||||
|
int8_t DCC::getFn( int cab, int16_t functionNumber) {
|
||||||
|
if (cab<=0 || functionNumber>31)
|
||||||
|
return -1; // unknown
|
||||||
int reg = lookupSpeedTable(cab);
|
int reg = lookupSpeedTable(cab);
|
||||||
if (reg<0) return -1;
|
if (reg<0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
unsigned long funcmask = (1UL<<functionNumber);
|
unsigned long funcmask = (1UL<<functionNumber);
|
||||||
return (speedTable[reg].functions & funcmask)? 1 : 0;
|
return (speedTable[reg].functions & funcmask)? 1 : 0;
|
||||||
|
@ -278,6 +306,57 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {
|
||||||
|
|
||||||
|
/* From https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf
|
||||||
|
|
||||||
|
The Extended Accessory Decoder Control Packet is included for the purpose of transmitting aspect control to signal
|
||||||
|
decoders or data bytes to more complex accessory decoders. Each signal head can display one aspect at a time.
|
||||||
|
{preamble} 0 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1
|
||||||
|
|
||||||
|
XXXXX is for a single head. A value of 00000 for XXXXX indicates the absolute stop aspect. All other aspects
|
||||||
|
represented by the values for XXXXX are determined by the signaling system used and the prototype being
|
||||||
|
modeled.
|
||||||
|
|
||||||
|
From https://normen.railcommunity.de/RCN-213.pdf:
|
||||||
|
|
||||||
|
More information is in RCN-213 about how the address bits are organized.
|
||||||
|
preamble -0- 1 0 A7 A6 A5 A4 A3 A2 -0- 0 ^A10 ^A9 ^A8 0 A1 A0 1 -0- ....
|
||||||
|
|
||||||
|
Thus in byte packet form the format is 10AAAAAA, 0AAA0AA1, 000XXXXX
|
||||||
|
|
||||||
|
Die Adresse f<EFBFBD>r den ersten erweiterten Zubeh<EFBFBD>rdecoder ist wie bei den einfachen
|
||||||
|
Zubeh<EFBFBD>rdecodern die Adresse 4 = 1000-0001 0111-0001 . Diese Adresse wird in
|
||||||
|
Anwenderdialogen als Adresse 1 dargestellt.
|
||||||
|
|
||||||
|
This means that the first address shown to the user as "1" is mapped
|
||||||
|
to internal address 4.
|
||||||
|
|
||||||
|
Note that the Basic accessory format mentions "By convention these
|
||||||
|
bits (bits 4-6 of the second data byte) are in ones complement" but
|
||||||
|
this note is absent from the advanced packet description. The
|
||||||
|
english translation does not mention that the address format for
|
||||||
|
the advanced packet follows the one for the basic packet but
|
||||||
|
according to the RCN-213 this is the case.
|
||||||
|
|
||||||
|
We allow for addresses from -3 to 2047-3 as that allows to address the
|
||||||
|
whole range of the 11 bits sent to track.
|
||||||
|
*/
|
||||||
|
if ((address > 2044) || (address < -3)) return false; // 2047-3, 11 bits but offset 3
|
||||||
|
if (value != (value & 0x1F)) return false; // 5 bits
|
||||||
|
|
||||||
|
address+=3; // +3 offset according to RCN-213
|
||||||
|
byte b[3];
|
||||||
|
b[0]= 0x80 // bits always on
|
||||||
|
| ((address>>2) & 0x3F); // shift out 2, mask out used bits
|
||||||
|
b[1]= 0x01 // bits always on
|
||||||
|
| (((~(address>>8)) & 0x07)<<4) // shift out 8, invert, mask 3 bits, shift up 4
|
||||||
|
| ((address & 0x03)<<1); // mask 2 bits, shift up 1
|
||||||
|
b[2]=value;
|
||||||
|
DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), repeats);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// writeCVByteMain: Write a byte with PoM on main. This writes
|
// writeCVByteMain: Write a byte with PoM on main. This writes
|
||||||
// the 5 byte sized packet to implement this DCC function
|
// the 5 byte sized packet to implement this DCC function
|
||||||
|
@ -421,6 +500,36 @@ const ackOp FLASH READ_CV_PROG[] = {
|
||||||
|
|
||||||
const ackOp FLASH LOCO_ID_PROG[] = {
|
const ackOp FLASH LOCO_ID_PROG[] = {
|
||||||
BASELINE,
|
BASELINE,
|
||||||
|
// first check cv20 for extended addressing
|
||||||
|
SETCV, (ackOp)20, // CV 19 is extended
|
||||||
|
SETBYTE, (ackOp)0,
|
||||||
|
VB, WACK, ITSKIP, // skip past extended section if cv20 is zero
|
||||||
|
// read cv20 and 19 and merge
|
||||||
|
STARTMERGE, // Setup to read cv 20
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
|
||||||
|
STASHLOCOID, // keep cv 20 until we have cv19 as well.
|
||||||
|
SETCV, (ackOp)19,
|
||||||
|
STARTMERGE, // Setup to read cv 19
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
V0, WACK, MERGE,
|
||||||
|
VB, WACK, NAKFAIL, // cant recover if cv 19 unreadable
|
||||||
|
COMBINE1920, // Combile byte with stash and callback
|
||||||
|
// end of advanced 20,19 check
|
||||||
|
SKIPTARGET,
|
||||||
SETCV, (ackOp)19, // CV 19 is consist setting
|
SETCV, (ackOp)19, // CV 19 is consist setting
|
||||||
SETBYTE, (ackOp)0,
|
SETBYTE, (ackOp)0,
|
||||||
VB, WACK, ITSKIP, // ignore consist if cv19 is zero (no consist)
|
VB, WACK, ITSKIP, // ignore consist if cv19 is zero (no consist)
|
||||||
|
@ -487,6 +596,10 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
||||||
|
|
||||||
const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
|
const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
|
||||||
BASELINE,
|
BASELINE,
|
||||||
|
// Clear consist CV 19,20
|
||||||
|
SETCV,(ackOp)20,
|
||||||
|
SETBYTE, (ackOp)0,
|
||||||
|
WB,WACK, // ignore dedcoder without cv20 support
|
||||||
SETCV,(ackOp)19,
|
SETCV,(ackOp)19,
|
||||||
SETBYTE, (ackOp)0,
|
SETBYTE, (ackOp)0,
|
||||||
WB,WACK, // ignore dedcoder without cv19 support
|
WB,WACK, // ignore dedcoder without cv19 support
|
||||||
|
@ -502,9 +615,25 @@ const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
|
||||||
CALLFAIL
|
CALLFAIL
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// for CONSIST_ID_PROG the 20,19 values are already calculated
|
||||||
|
const ackOp FLASH CONSIST_ID_PROG[] = {
|
||||||
|
BASELINE,
|
||||||
|
SETCV,(ackOp)20,
|
||||||
|
SETBYTEH, // high byte to CV 20
|
||||||
|
WB,WACK, // ignore dedcoder without cv20 support
|
||||||
|
SETCV,(ackOp)19,
|
||||||
|
SETBYTEL, // low byte of word
|
||||||
|
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
|
||||||
|
VB,WACK,ITC1, // Some decoders do not ack and need verify
|
||||||
|
CALLFAIL
|
||||||
|
};
|
||||||
|
|
||||||
const ackOp FLASH LONG_LOCO_ID_PROG[] = {
|
const ackOp FLASH LONG_LOCO_ID_PROG[] = {
|
||||||
BASELINE,
|
BASELINE,
|
||||||
// Clear consist CV 19
|
// Clear consist CV 19,20
|
||||||
|
SETCV,(ackOp)20,
|
||||||
|
SETBYTE, (ackOp)0,
|
||||||
|
WB,WACK, // ignore dedcoder without cv20 support
|
||||||
SETCV,(ackOp)19,
|
SETCV,(ackOp)19,
|
||||||
SETBYTE, (ackOp)0,
|
SETBYTE, (ackOp)0,
|
||||||
WB,WACK, // ignore decoder without cv19 support
|
WB,WACK, // ignore decoder without cv19 support
|
||||||
|
@ -573,6 +702,26 @@ void DCC::setLocoId(int id,ACK_CALLBACK callback) {
|
||||||
DCCACK::Setup(id | 0xc000,LONG_LOCO_ID_PROG, callback);
|
DCCACK::Setup(id | 0xc000,LONG_LOCO_ID_PROG, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCC::setConsistId(int id,bool reverse,ACK_CALLBACK callback) {
|
||||||
|
if (id<0 || id>10239) { //0x27FF according to standard
|
||||||
|
callback(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte cv20;
|
||||||
|
byte cv19;
|
||||||
|
|
||||||
|
if (id<=HIGHEST_SHORT_ADDR) {
|
||||||
|
cv19=id;
|
||||||
|
cv20=0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cv20=id/100;
|
||||||
|
cv19=id%100;
|
||||||
|
}
|
||||||
|
if (reverse) cv19|=0x80;
|
||||||
|
DCCACK::Setup((cv20<<8)|cv19, CONSIST_ID_PROG, callback);
|
||||||
|
}
|
||||||
|
|
||||||
void DCC::forgetLoco(int cab) { // removes any speed reminders for this loco
|
void DCC::forgetLoco(int cab) { // removes any speed reminders for this loco
|
||||||
setThrottle2(cab,1); // ESTOP this loco if still on track
|
setThrottle2(cab,1); // ESTOP this loco if still on track
|
||||||
int reg=lookupSpeedTable(cab, false);
|
int reg=lookupSpeedTable(cab, false);
|
||||||
|
@ -595,7 +744,7 @@ void DCC::loop() {
|
||||||
|
|
||||||
void DCC::issueReminders() {
|
void DCC::issueReminders() {
|
||||||
// if the main track transmitter still has a pending packet, skip this time around.
|
// if the main track transmitter still has a pending packet, skip this time around.
|
||||||
if ( DCCWaveform::mainTrack.getPacketPending()) return;
|
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
|
||||||
// Move to next loco slot. If occupied, send a reminder.
|
// Move to next loco slot. If occupied, send a reminder.
|
||||||
int reg = lastLocoReminder+1;
|
int reg = lastLocoReminder+1;
|
||||||
if (reg > highestUsedReg) reg = 0; // Go to start of table
|
if (reg > highestUsedReg) reg = 0; // Go to start of table
|
||||||
|
@ -619,24 +768,39 @@ bool DCC::issueReminder(int reg) {
|
||||||
break;
|
break;
|
||||||
case 1: // remind function group 1 (F0-F4)
|
case 1: // remind function group 1 (F0-F4)
|
||||||
if (flags & FN_GROUP_1)
|
if (flags & FN_GROUP_1)
|
||||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
#ifndef DISABLE_FUNCTION_REMINDERS
|
||||||
|
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
|
||||||
|
#else
|
||||||
|
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),2);
|
||||||
|
flags&= ~FN_GROUP_1; // dont send them again
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case 2: // remind function group 2 F5-F8
|
case 2: // remind function group 2 F5-F8
|
||||||
if (flags & FN_GROUP_2)
|
if (flags & FN_GROUP_2)
|
||||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
#ifndef DISABLE_FUNCTION_REMINDERS
|
||||||
|
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
|
||||||
|
#else
|
||||||
|
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),2);
|
||||||
|
flags&= ~FN_GROUP_2; // dont send them again
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case 3: // remind function group 3 F9-F12
|
case 3: // remind function group 3 F9-F12
|
||||||
if (flags & FN_GROUP_3)
|
if (flags & FN_GROUP_3)
|
||||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
#ifndef DISABLE_FUNCTION_REMINDERS
|
||||||
|
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
|
||||||
|
#else
|
||||||
|
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),2);
|
||||||
|
flags&= ~FN_GROUP_3; // dont send them again
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case 4: // remind function group 4 F13-F20
|
case 4: // remind function group 4 F13-F20
|
||||||
if (flags & FN_GROUP_4)
|
if (flags & FN_GROUP_4)
|
||||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
|
||||||
flags&= ~FN_GROUP_4; // dont send them again
|
flags&= ~FN_GROUP_4; // dont send them again
|
||||||
break;
|
break;
|
||||||
case 5: // remind function group 5 F21-F28
|
case 5: // remind function group 5 F21-F28
|
||||||
if (flags & FN_GROUP_5)
|
if (flags & FN_GROUP_5)
|
||||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
|
||||||
flags&= ~FN_GROUP_5; // dont send them again
|
flags&= ~FN_GROUP_5; // dont send them again
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
10
DCC.h
10
DCC.h
|
@ -61,16 +61,18 @@ public:
|
||||||
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
|
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
|
||||||
static int8_t getThrottleSpeed(int cab);
|
static int8_t getThrottleSpeed(int cab);
|
||||||
static uint8_t getThrottleSpeedByte(int cab);
|
static uint8_t getThrottleSpeedByte(int cab);
|
||||||
|
static uint8_t getThrottleFrequency(int cab);
|
||||||
static bool getThrottleDirection(int cab);
|
static bool getThrottleDirection(int cab);
|
||||||
static void writeCVByteMain(int cab, int cv, byte bValue);
|
static void writeCVByteMain(int cab, int cv, byte bValue);
|
||||||
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
|
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
|
||||||
static void setFunction(int cab, byte fByte, byte eByte);
|
static void setFunction(int cab, byte fByte, byte eByte);
|
||||||
static bool setFn(int cab, int16_t functionNumber, bool on);
|
static bool setFn(int cab, int16_t functionNumber, bool on);
|
||||||
static void changeFn(int cab, int16_t functionNumber);
|
static void changeFn(int cab, int16_t functionNumber);
|
||||||
static int getFn(int cab, int16_t functionNumber);
|
static int8_t getFn(int cab, int16_t functionNumber);
|
||||||
static uint32_t getFunctionMap(int cab);
|
static uint32_t getFunctionMap(int cab);
|
||||||
static void updateGroupflags(byte &flags, int16_t functionNumber);
|
static void updateGroupflags(byte &flags, int16_t functionNumber);
|
||||||
static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
|
static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
|
||||||
|
static bool setExtendedAccessory(int16_t address, int16_t value, byte repeats=3);
|
||||||
static bool writeTextPacket(byte *b, int nBytes);
|
static bool writeTextPacket(byte *b, int nBytes);
|
||||||
|
|
||||||
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
|
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
|
||||||
|
@ -83,7 +85,7 @@ public:
|
||||||
|
|
||||||
static void getLocoId(ACK_CALLBACK callback);
|
static void getLocoId(ACK_CALLBACK callback);
|
||||||
static void setLocoId(int id,ACK_CALLBACK callback);
|
static void setLocoId(int id,ACK_CALLBACK callback);
|
||||||
|
static void setConsistId(int id,bool reverse,ACK_CALLBACK callback);
|
||||||
// Enhanced API functions
|
// Enhanced API functions
|
||||||
static void forgetLoco(int cab); // removes any speed reminders for this loco
|
static void forgetLoco(int cab); // removes any speed reminders for this loco
|
||||||
static void forgetAllLocos(); // removes all speed reminders
|
static void forgetAllLocos(); // removes all speed reminders
|
||||||
|
@ -98,7 +100,7 @@ public:
|
||||||
int loco;
|
int loco;
|
||||||
byte speedCode;
|
byte speedCode;
|
||||||
byte groupFlags;
|
byte groupFlags;
|
||||||
unsigned long functions;
|
uint32_t functions;
|
||||||
};
|
};
|
||||||
static LOCO speedTable[MAX_LOCOS];
|
static LOCO speedTable[MAX_LOCOS];
|
||||||
static int lookupSpeedTable(int locoId, bool autoCreate=true);
|
static int lookupSpeedTable(int locoId, bool autoCreate=true);
|
||||||
|
@ -109,7 +111,7 @@ private:
|
||||||
static byte loopStatus;
|
static byte loopStatus;
|
||||||
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
||||||
static void updateLocoReminder(int loco, byte speedCode);
|
static void updateLocoReminder(int loco, byte speedCode);
|
||||||
static void setFunctionInternal(int cab, byte fByte, byte eByte);
|
static void setFunctionInternal(int cab, byte fByte, byte eByte, byte count);
|
||||||
static bool issueReminder(int reg);
|
static bool issueReminder(int reg);
|
||||||
static int lastLocoReminder;
|
static int lastLocoReminder;
|
||||||
static int highestUsedReg;
|
static int highestUsedReg;
|
||||||
|
|
17
DCCACK.cpp
17
DCCACK.cpp
|
@ -314,6 +314,14 @@ void DCCACK::loop() {
|
||||||
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
|
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case COMBINE1920:
|
||||||
|
// ackManagerStash is cv20, ackManagerByte is CV 19
|
||||||
|
// This will not be called if cv20==0
|
||||||
|
ackManagerByte &= 0x7F; // ignore direction marker
|
||||||
|
ackManagerByte %=100; // take last 2 decimal digits
|
||||||
|
callback( ackManagerStash*100+ackManagerByte);
|
||||||
|
return;
|
||||||
|
|
||||||
case ITSKIP:
|
case ITSKIP:
|
||||||
if (!ackReceived) break;
|
if (!ackReceived) break;
|
||||||
// SKIP opcodes until SKIPTARGET found
|
// SKIP opcodes until SKIPTARGET found
|
||||||
|
@ -322,6 +330,15 @@ void DCCACK::loop() {
|
||||||
opcode=GETFLASH(ackManagerProg);
|
opcode=GETFLASH(ackManagerProg);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case NAKSKIP:
|
||||||
|
if (ackReceived) break;
|
||||||
|
// SKIP opcodes until SKIPTARGET found
|
||||||
|
while (opcode!=SKIPTARGET) {
|
||||||
|
ackManagerProg++;
|
||||||
|
opcode=GETFLASH(ackManagerProg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case SKIPTARGET:
|
case SKIPTARGET:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
2
DCCACK.h
2
DCCACK.h
|
@ -56,6 +56,8 @@ enum ackOp : byte
|
||||||
STASHLOCOID, // keeps current byte value for later
|
STASHLOCOID, // keeps current byte value for later
|
||||||
COMBINELOCOID, // combines current value with stashed value and returns it
|
COMBINELOCOID, // combines current value with stashed value and returns it
|
||||||
ITSKIP, // skip to SKIPTARGET if ack true
|
ITSKIP, // skip to SKIPTARGET if ack true
|
||||||
|
NAKSKIP, // skip to SKIPTARGET if ack false
|
||||||
|
COMBINE1920, // combine cvs 19 and 20 and callback
|
||||||
SKIPTARGET = 0xFF // jump to target
|
SKIPTARGET = 0xFF // jump to target
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
1
DCCEX.h
1
DCCEX.h
|
@ -49,6 +49,7 @@
|
||||||
#include "CommandDistributor.h"
|
#include "CommandDistributor.h"
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
|
#include "KeywordHasher.h"
|
||||||
#include "EXRAIL.h"
|
#include "EXRAIL.h"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
639
DCCEXParser.cpp
639
DCCEXParser.cpp
|
@ -45,11 +45,11 @@ Once a new OPCODE is decided upon, update this list.
|
||||||
0, Track power off
|
0, Track power off
|
||||||
1, Track power on
|
1, Track power on
|
||||||
a, DCC accessory control
|
a, DCC accessory control
|
||||||
A,
|
A, DCC extended accessory control
|
||||||
b, Write CV bit on main
|
b, Write CV bit on main
|
||||||
B, Write CV bit
|
B, Write CV bit
|
||||||
c, Request current command
|
c, Request current command
|
||||||
C,
|
C, configure the CS
|
||||||
d,
|
d,
|
||||||
D, Diagnostic commands
|
D, Diagnostic commands
|
||||||
e, Erase EEPROM
|
e, Erase EEPROM
|
||||||
|
@ -60,18 +60,18 @@ Once a new OPCODE is decided upon, update this list.
|
||||||
G,
|
G,
|
||||||
h,
|
h,
|
||||||
H, Turnout state broadcast
|
H, Turnout state broadcast
|
||||||
i, Reserved for future use - Turntable object broadcast
|
i, Server details string
|
||||||
I, Reserved for future use - Turntable object command and control
|
I, Turntable object command, control, and broadcast
|
||||||
j, Throttle responses
|
j, Throttle responses
|
||||||
J, Throttle queries
|
J, Throttle queries
|
||||||
k, Reserved for future use - Potentially Railcom
|
k, Reserved for future use - Potentially Railcom
|
||||||
K, Reserved for future use - Potentially Railcom
|
K, Reserved for future use - Potentially Railcom
|
||||||
l, Loco speedbyte/function map broadcast
|
l, Loco speedbyte/function map broadcast
|
||||||
L,
|
L, Reserved for LCC interface (implemented in EXRAIL)
|
||||||
m,
|
m, message to throttles broadcast
|
||||||
M, Write DCC packet
|
M, Write DCC packet
|
||||||
n,
|
n, Reserved for SensorCam
|
||||||
N,
|
N, Reserved for Sensorcam
|
||||||
o,
|
o,
|
||||||
O, Output broadcast
|
O, Output broadcast
|
||||||
p, Broadcast power state
|
p, Broadcast power state
|
||||||
|
@ -91,10 +91,10 @@ Once a new OPCODE is decided upon, update this list.
|
||||||
w, Write CV on main
|
w, Write CV on main
|
||||||
W, Write CV
|
W, Write CV
|
||||||
x,
|
x,
|
||||||
X, Invalid command
|
X, Invalid command response
|
||||||
y,
|
y,
|
||||||
Y, Output broadcast
|
Y, Output broadcast
|
||||||
z,
|
z, Direct output
|
||||||
Z, Output configuration/control
|
Z, Output configuration/control
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -114,6 +114,9 @@ Once a new OPCODE is decided upon, update this list.
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
#include "EXRAIL2.h"
|
#include "EXRAIL2.h"
|
||||||
|
#include "Turntables.h"
|
||||||
|
#include "version.h"
|
||||||
|
#include "KeywordHasher.h"
|
||||||
|
|
||||||
// This macro can't be created easily as a portable function because the
|
// This macro can't be created easily as a portable function because the
|
||||||
// flashlist requires a far pointer for high flash access.
|
// flashlist requires a far pointer for high flash access.
|
||||||
|
@ -124,51 +127,6 @@ Once a new OPCODE is decided upon, update this list.
|
||||||
StringFormatter::send(stream,F(" %d"),value); \
|
StringFormatter::send(stream,F(" %d"),value); \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter.
|
|
||||||
// To discover new keyword numbers , use the <$ YOURKEYWORD> command
|
|
||||||
const int16_t HASH_KEYWORD_MAIN = 11339;
|
|
||||||
const int16_t HASH_KEYWORD_CABS = -11981;
|
|
||||||
const int16_t HASH_KEYWORD_RAM = 25982;
|
|
||||||
const int16_t HASH_KEYWORD_CMD = 9962;
|
|
||||||
const int16_t HASH_KEYWORD_ACK = 3113;
|
|
||||||
const int16_t HASH_KEYWORD_ON = 2657;
|
|
||||||
const int16_t HASH_KEYWORD_DCC = 6436;
|
|
||||||
const int16_t HASH_KEYWORD_SLOW = -17209;
|
|
||||||
#ifndef DISABLE_PROG
|
|
||||||
const int16_t HASH_KEYWORD_JOIN = -30750;
|
|
||||||
const int16_t HASH_KEYWORD_PROG = -29718;
|
|
||||||
const int16_t HASH_KEYWORD_PROGBOOST = -6353;
|
|
||||||
#endif
|
|
||||||
#ifndef DISABLE_EEPROM
|
|
||||||
const int16_t HASH_KEYWORD_EEPROM = -7168;
|
|
||||||
#endif
|
|
||||||
const int16_t HASH_KEYWORD_LIMIT = 27413;
|
|
||||||
const int16_t HASH_KEYWORD_MAX = 16244;
|
|
||||||
const int16_t HASH_KEYWORD_MIN = 15978;
|
|
||||||
const int16_t HASH_KEYWORD_RESET = 26133;
|
|
||||||
const int16_t HASH_KEYWORD_RETRY = 25704;
|
|
||||||
const int16_t HASH_KEYWORD_SPEED28 = -17064;
|
|
||||||
const int16_t HASH_KEYWORD_SPEED128 = 25816;
|
|
||||||
const int16_t HASH_KEYWORD_SERVO=27709;
|
|
||||||
const int16_t HASH_KEYWORD_TT=2688;
|
|
||||||
const int16_t HASH_KEYWORD_VPIN=-415;
|
|
||||||
const int16_t HASH_KEYWORD_A='A';
|
|
||||||
const int16_t HASH_KEYWORD_C='C';
|
|
||||||
const int16_t HASH_KEYWORD_G='G';
|
|
||||||
const int16_t HASH_KEYWORD_I='I';
|
|
||||||
const int16_t HASH_KEYWORD_R='R';
|
|
||||||
const int16_t HASH_KEYWORD_T='T';
|
|
||||||
const int16_t HASH_KEYWORD_X='X';
|
|
||||||
const int16_t HASH_KEYWORD_LCN = 15137;
|
|
||||||
const int16_t HASH_KEYWORD_HAL = 10853;
|
|
||||||
const int16_t HASH_KEYWORD_SHOW = -21309;
|
|
||||||
const int16_t HASH_KEYWORD_ANIN = -10424;
|
|
||||||
const int16_t HASH_KEYWORD_ANOUT = -26399;
|
|
||||||
const int16_t HASH_KEYWORD_WIFI = -5583;
|
|
||||||
const int16_t HASH_KEYWORD_ETHERNET = -30767;
|
|
||||||
const int16_t HASH_KEYWORD_WIT = 31594;
|
|
||||||
|
|
||||||
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
|
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
|
||||||
bool DCCEXParser::stashBusy;
|
bool DCCEXParser::stashBusy;
|
||||||
Print *DCCEXParser::stashStream = NULL;
|
Print *DCCEXParser::stashStream = NULL;
|
||||||
|
@ -204,8 +162,10 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||||
case 1: // skipping spaces before a param
|
case 1: // skipping spaces before a param
|
||||||
if (hot == ' ')
|
if (hot == ' ')
|
||||||
break;
|
break;
|
||||||
if (hot == '\0' || hot == '>')
|
if (hot == '\0')
|
||||||
return parameterCount;
|
return -1;
|
||||||
|
if (hot == '>')
|
||||||
|
return parameterCount;
|
||||||
state = 2;
|
state = 2;
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -298,13 +258,18 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
#ifndef DISABLE_EEPROM
|
#ifndef DISABLE_EEPROM
|
||||||
(void)EEPROM; // tell compiler not to warn this is unused
|
(void)EEPROM; // tell compiler not to warn this is unused
|
||||||
#endif
|
#endif
|
||||||
|
byte params = 0;
|
||||||
if (Diag::CMD)
|
if (Diag::CMD)
|
||||||
DIAG(F("PARSING:%s"), com);
|
DIAG(F("PARSING:%s"), com);
|
||||||
int16_t p[MAX_COMMAND_PARAMS];
|
int16_t p[MAX_COMMAND_PARAMS];
|
||||||
while (com[0] == '<' || com[0] == ' ')
|
while (com[0] == '<' || com[0] == ' ')
|
||||||
com++; // strip off any number of < or spaces
|
com++; // strip off any number of < or spaces
|
||||||
byte opcode = com[0];
|
byte opcode = com[0];
|
||||||
byte params = splitValues(p, com, opcode=='M' || opcode=='P');
|
int16_t splitnum = splitValues(p, com, opcode=='M' || opcode=='P');
|
||||||
|
if (splitnum < 0 || splitnum >= MAX_COMMAND_PARAMS) // if arguments are broken, leave but via printing <X>
|
||||||
|
goto out;
|
||||||
|
// Because of check above we are now inside byte size
|
||||||
|
params = splitnum;
|
||||||
|
|
||||||
if (filterCallback)
|
if (filterCallback)
|
||||||
filterCallback(stream, opcode, params, p);
|
filterCallback(stream, opcode, params, p);
|
||||||
|
@ -318,25 +283,22 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
return; // filterCallback asked us to ignore
|
return; // filterCallback asked us to ignore
|
||||||
case 't': // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>
|
case 't': // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>
|
||||||
{
|
{
|
||||||
if (params==1) { // <t cab> display state
|
|
||||||
|
|
||||||
int16_t slot=DCC::lookupSpeedTable(p[0],false);
|
|
||||||
if (slot>=0) {
|
|
||||||
DCC::LOCO * sp=&DCC::speedTable[slot];
|
|
||||||
StringFormatter::send(stream,F("<l %d %d %d %l>\n"),
|
|
||||||
sp->loco,slot,sp->speedCode,sp->functions);
|
|
||||||
}
|
|
||||||
else // send dummy state speed 0 fwd no functions.
|
|
||||||
StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t cab;
|
int16_t cab;
|
||||||
int16_t tspeed;
|
int16_t tspeed;
|
||||||
int16_t direction;
|
int16_t direction;
|
||||||
|
|
||||||
|
if (params==1) { // <t cab> display state
|
||||||
|
int16_t slot=DCC::lookupSpeedTable(p[0],false);
|
||||||
|
if (slot>=0)
|
||||||
|
CommandDistributor::broadcastLoco(slot);
|
||||||
|
else // send dummy state speed 0 fwd no functions.
|
||||||
|
StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (params == 4)
|
if (params == 4)
|
||||||
{ // <t REGISTER CAB SPEED DIRECTION>
|
{ // <t REGISTER CAB SPEED DIRECTION>
|
||||||
|
// ignore register p[0]
|
||||||
cab = p[1];
|
cab = p[1];
|
||||||
tspeed = p[2];
|
tspeed = p[2];
|
||||||
direction = p[3];
|
direction = p[3];
|
||||||
|
@ -420,6 +382,13 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'A': // EXTENDED ACCESSORY <A address value>
|
||||||
|
// Note: if this happens to match a defined EXRAIL
|
||||||
|
// DCCX_SIGNAL, then EXRAIL will have intercepted
|
||||||
|
// this command alrerady.
|
||||||
|
if (params==2 && DCC::setExtendedAccessory(p[0],p[1])) return;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'T': // TURNOUT <T ...>
|
case 'T': // TURNOUT <T ...>
|
||||||
if (parseT(stream, params, p))
|
if (parseT(stream, params, p))
|
||||||
return;
|
return;
|
||||||
|
@ -451,12 +420,16 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
|
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
case 'w': // WRITE CV on MAIN <w CAB CV VALUE>
|
case 'w': // WRITE CV on MAIN <w CAB CV VALUE>
|
||||||
DCC::writeCVByteMain(p[0], p[1], p[2]);
|
if (params != 3)
|
||||||
return;
|
break;
|
||||||
|
DCC::writeCVByteMain(p[0], p[1], p[2]);
|
||||||
|
return;
|
||||||
|
|
||||||
case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>
|
case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>
|
||||||
DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);
|
if (params != 4)
|
||||||
return;
|
break;
|
||||||
|
DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);
|
||||||
|
return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
|
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
|
||||||
|
@ -479,14 +452,19 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
|
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
case 'W': // WRITE CV ON PROG <W CV VALUE CALLBACKNUM CALLBACKSUB>
|
case 'W': // WRITE CV ON PROG <W CV VALUE CALLBACKNUM CALLBACKSUB>
|
||||||
if (!stashCallback(stream, p, ringStream))
|
if (!stashCallback(stream, p, ringStream))
|
||||||
break;
|
break;
|
||||||
if (params == 1) // <W id> Write new loco id (clearing consist and managing short/long)
|
if (params == 1) // <W id> Write new loco id (clearing consist and managing short/long)
|
||||||
DCC::setLocoId(p[0],callback_Wloco);
|
DCC::setLocoId(p[0],callback_Wloco);
|
||||||
else if (params == 4) // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
|
else if (params == 4) // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
|
||||||
DCC::writeCVByte(p[0], p[1], callback_W4);
|
DCC::writeCVByte(p[0], p[1], callback_W4);
|
||||||
else // WRITE CV ON PROG <W CV VALUE>
|
else if ((params==2 || params==3 ) && p[0]=="CONSIST"_hk ) {
|
||||||
|
DCC::setConsistId(p[1],p[2]=="REVERSE"_hk,callback_Wconsist);
|
||||||
|
}
|
||||||
|
else if (params == 2) // WRITE CV ON PROG <W CV VALUE>
|
||||||
DCC::writeCVByte(p[0], p[1], callback_W);
|
DCC::writeCVByte(p[0], p[1], callback_W);
|
||||||
|
else
|
||||||
|
break;
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 'V': // VERIFY CV ON PROG <V CV VALUE> <V CV BIT 0|1>
|
case 'V': // VERIFY CV ON PROG <V CV VALUE> <V CV BIT 0|1>
|
||||||
|
@ -506,9 +484,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'B': // WRITE CV BIT ON PROG <B CV BIT VALUE CALLBACKNUM CALLBACKSUB>
|
case 'B': // WRITE CV BIT ON PROG <B CV BIT VALUE CALLBACKNUM CALLBACKSUB> or <B CV BIT VALUE>
|
||||||
|
if (params != 3 && params != 5)
|
||||||
|
break;
|
||||||
if (!stashCallback(stream, p, ringStream))
|
if (!stashCallback(stream, p, ringStream))
|
||||||
break;
|
break;
|
||||||
DCC::writeCVBit(p[0], p[1], p[2], callback_B);
|
DCC::writeCVBit(p[0], p[1], p[2], callback_B);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -539,85 +519,81 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
|
|
||||||
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
|
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
|
||||||
{
|
{
|
||||||
bool main=false;
|
if (params > 1) break;
|
||||||
bool prog=false;
|
if (params==0) { // All
|
||||||
bool join=false;
|
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON);
|
||||||
if (params > 1) break;
|
|
||||||
if (params==0) { // All
|
|
||||||
main=true;
|
|
||||||
prog=true;
|
|
||||||
}
|
|
||||||
if (params==1) {
|
|
||||||
if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN>
|
|
||||||
main=true;
|
|
||||||
}
|
}
|
||||||
|
if (params==1) {
|
||||||
|
if (p[0]=="MAIN"_hk) { // <1 MAIN>
|
||||||
|
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
|
||||||
|
}
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN>
|
else if (p[0] == "JOIN"_hk) { // <1 JOIN>
|
||||||
main=true;
|
TrackManager::setJoin(true);
|
||||||
prog=true;
|
TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
|
||||||
join=true;
|
}
|
||||||
}
|
else if (p[0]=="PROG"_hk) { // <1 PROG>
|
||||||
else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG>
|
TrackManager::setJoin(false);
|
||||||
prog=true;
|
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
else break; // will reply <X>
|
else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
|
||||||
}
|
byte t = (p[0] - 'A');
|
||||||
TrackManager::setJoin(join);
|
TrackManager::setTrackPower(POWERMODE::ON, t);
|
||||||
if (main) TrackManager::setMainPower(POWERMODE::ON);
|
//StringFormatter::send(stream, F("<p1 %c>\n"), t+'A');
|
||||||
if (prog) TrackManager::setProgPower(POWERMODE::ON);
|
}
|
||||||
|
else break; // will reply <X>
|
||||||
|
}
|
||||||
|
//TrackManager::streamTrackState(NULL,t);
|
||||||
|
|
||||||
CommandDistributor::broadcastPower();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case '0': // POWEROFF <0 [MAIN | PROG] >
|
case '0': // POWEROFF <0 [MAIN | PROG] >
|
||||||
{
|
{
|
||||||
bool main=false;
|
if (params > 1) break;
|
||||||
bool prog=false;
|
if (params==0) { // All
|
||||||
if (params > 1) break;
|
TrackManager::setJoin(false);
|
||||||
if (params==0) { // All
|
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF);
|
||||||
main=true;
|
|
||||||
prog=true;
|
|
||||||
}
|
|
||||||
if (params==1) {
|
|
||||||
if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN>
|
|
||||||
main=true;
|
|
||||||
}
|
}
|
||||||
|
if (params==1) {
|
||||||
|
if (p[0]=="MAIN"_hk) { // <0 MAIN>
|
||||||
|
TrackManager::setJoin(false);
|
||||||
|
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
|
||||||
|
}
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG>
|
else if (p[0]=="PROG"_hk) { // <0 PROG>
|
||||||
prog=true;
|
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
|
||||||
}
|
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
else break; // will reply <X>
|
else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
|
||||||
|
byte t = (p[0] - 'A');
|
||||||
|
TrackManager::setJoin(false);
|
||||||
|
TrackManager::setTrackPower(POWERMODE::OFF, t);
|
||||||
|
//StringFormatter::send(stream, F("<p0 %c>\n"), t+'A');
|
||||||
|
}
|
||||||
|
else break; // will reply <X>
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackManager::setJoin(false);
|
|
||||||
if (main) TrackManager::setMainPower(POWERMODE::OFF);
|
|
||||||
if (prog) {
|
|
||||||
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
|
|
||||||
TrackManager::setProgPower(POWERMODE::OFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandDistributor::broadcastPower();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case '!': // ESTOP ALL <!>
|
case '!': // ESTOP ALL <!>
|
||||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
#ifdef HAS_ENOUGH_MEMORY
|
||||||
case 'c': // SEND METER RESPONSES <c>
|
case 'c': // SEND METER RESPONSES <c>
|
||||||
// No longer useful because of multiple tracks See <JG> and <JI>
|
// No longer useful because of multiple tracks See <JG> and <JI>
|
||||||
if (params>0) break;
|
if (params>0) break;
|
||||||
TrackManager::reportObsoleteCurrent(stream);
|
TrackManager::reportObsoleteCurrent(stream);
|
||||||
return;
|
return;
|
||||||
|
#endif
|
||||||
case 'Q': // SENSORS <Q>
|
case 'Q': // SENSORS <Q>
|
||||||
Sensor::printAll(stream);
|
Sensor::printAll(stream);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 's': // <s>
|
case 's': // STATUS <s>
|
||||||
StringFormatter::send(stream, F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
|
StringFormatter::send(stream, F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
|
||||||
CommandDistributor::broadcastPower(); // <s> is the only "get power status" command we have
|
CommandDistributor::broadcastPower(); // <s> is the only "get power status" command we have
|
||||||
Turnout::printAll(stream); //send all Turnout states
|
Turnout::printAll(stream); //send all Turnout states
|
||||||
|
@ -638,14 +614,18 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
case ' ': // < >
|
case ' ': // < >
|
||||||
StringFormatter::send(stream, F("\n"));
|
StringFormatter::send(stream, F("\n"));
|
||||||
return;
|
return;
|
||||||
|
case 'C': // CONFIG <C [params]>
|
||||||
case 'D': // < >
|
if (parseC(stream, params, p))
|
||||||
|
return;
|
||||||
|
break;
|
||||||
|
#ifndef DISABLE_DIAG
|
||||||
|
case 'D': // DIAG <D [params]>
|
||||||
if (parseD(stream, params, p))
|
if (parseD(stream, params, p))
|
||||||
return;
|
return;
|
||||||
return;
|
break;
|
||||||
|
#endif
|
||||||
case '=': // <= Track manager control >
|
case '=': // TRACK MANAGER CONTROL <= [params]>
|
||||||
if (TrackManager::parseJ(stream, params, p))
|
if (TrackManager::parseEqualSign(stream, params, p))
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -682,7 +662,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
//if ((params<1) | (params>2)) break; // <J>
|
//if ((params<1) | (params>2)) break; // <J>
|
||||||
int16_t id=(params==2)?p[1]:0;
|
int16_t id=(params==2)?p[1]:0;
|
||||||
switch(p[0]) {
|
switch(p[0]) {
|
||||||
case HASH_KEYWORD_C: // <JC mmmm nn> sets time and speed
|
case "C"_hk: // <JC mmmm nn> sets time and speed
|
||||||
if (params==1) { // <JC> returns latest time
|
if (params==1) { // <JC> returns latest time
|
||||||
int16_t x = CommandDistributor::retClockTime();
|
int16_t x = CommandDistributor::retClockTime();
|
||||||
StringFormatter::send(stream, F("<jC %d>\n"), x);
|
StringFormatter::send(stream, F("<jC %d>\n"), x);
|
||||||
|
@ -691,38 +671,28 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
CommandDistributor::setClockTime(p[1], p[2], 1);
|
CommandDistributor::setClockTime(p[1], p[2], 1);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case HASH_KEYWORD_G: // <JG> current gauge limits
|
case "G"_hk: // <JG> current gauge limits
|
||||||
if (params>1) break;
|
if (params>1) break;
|
||||||
TrackManager::reportGauges(stream); // <g limit...limit>
|
TrackManager::reportGauges(stream); // <g limit...limit>
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case HASH_KEYWORD_I: // <JI> current values
|
case "I"_hk: // <JI> current values
|
||||||
if (params>1) break;
|
if (params>1) break;
|
||||||
TrackManager::reportCurrent(stream); // <g limit...limit>
|
TrackManager::reportCurrent(stream); // <g limit...limit>
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case HASH_KEYWORD_A: // <JA> returns automations/routes
|
case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
|
||||||
StringFormatter::send(stream, F("<jA"));
|
if (params!=1) break; // <JA>
|
||||||
if (params==1) {// <JA>
|
StringFormatter::send(stream, F("<jA>\n"));
|
||||||
#ifdef EXRAIL_ACTIVE
|
|
||||||
SENDFLASHLIST(stream,RMFT2::routeIdList)
|
|
||||||
SENDFLASHLIST(stream,RMFT2::automationIdList)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else { // <JA id>
|
|
||||||
StringFormatter::send(stream,F(" %d %c \"%S\""),
|
|
||||||
id,
|
|
||||||
#ifdef EXRAIL_ACTIVE
|
|
||||||
RMFT2::getRouteType(id), // A/R
|
|
||||||
RMFT2::getRouteDescription(id)
|
|
||||||
#else
|
|
||||||
'X',F("")
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
}
|
|
||||||
StringFormatter::send(stream, F(">\n"));
|
|
||||||
return;
|
return;
|
||||||
case HASH_KEYWORD_R: // <JR> returns rosters
|
|
||||||
|
case "M"_hk: // <JM> intercepted by EXRAIL
|
||||||
|
if (params>1) break; // invalid cant do
|
||||||
|
// <JM> requests stash size so say none.
|
||||||
|
StringFormatter::send(stream,F("<jM 0>\n"));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "R"_hk: // <JR> returns rosters
|
||||||
StringFormatter::send(stream, F("<jR"));
|
StringFormatter::send(stream, F("<jR"));
|
||||||
#ifdef EXRAIL_ACTIVE
|
#ifdef EXRAIL_ACTIVE
|
||||||
if (params==1) {
|
if (params==1) {
|
||||||
|
@ -741,7 +711,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
#endif
|
#endif
|
||||||
StringFormatter::send(stream, F(">\n"));
|
StringFormatter::send(stream, F(">\n"));
|
||||||
return;
|
return;
|
||||||
case HASH_KEYWORD_T: // <JT> returns turnout list
|
case "T"_hk: // <JT> returns turnout list
|
||||||
StringFormatter::send(stream, F("<jT"));
|
StringFormatter::send(stream, F("<jT"));
|
||||||
if (params==1) { // <JT>
|
if (params==1) { // <JT>
|
||||||
for ( Turnout * t=Turnout::first(); t; t=t->next()) {
|
for ( Turnout * t=Turnout::first(); t; t=t->next()) {
|
||||||
|
@ -766,20 +736,95 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||||
}
|
}
|
||||||
StringFormatter::send(stream, F(">\n"));
|
StringFormatter::send(stream, F(">\n"));
|
||||||
return;
|
return;
|
||||||
|
// No turntables without HAL support
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
case "O"_hk: // <JO returns turntable list
|
||||||
|
StringFormatter::send(stream, F("<jO"));
|
||||||
|
if (params==1) { // <JO>
|
||||||
|
for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) {
|
||||||
|
if (tto->isHidden()) continue;
|
||||||
|
StringFormatter::send(stream, F(" %d"),tto->getId());
|
||||||
|
}
|
||||||
|
StringFormatter::send(stream, F(">\n"));
|
||||||
|
} else { // <JO id>
|
||||||
|
Turntable *tto=Turntable::get(id);
|
||||||
|
if (!tto || tto->isHidden()) {
|
||||||
|
StringFormatter::send(stream, F(" %d X>\n"), id);
|
||||||
|
} else {
|
||||||
|
uint8_t pos = tto->getPosition();
|
||||||
|
uint8_t type = tto->isEXTT();
|
||||||
|
uint8_t posCount = tto->getPositionCount();
|
||||||
|
const FSH *todesc = NULL;
|
||||||
|
#ifdef EXRAIL_ACTIVE
|
||||||
|
todesc = RMFT2::getTurntableDescription(id);
|
||||||
|
#endif
|
||||||
|
if (todesc == NULL) todesc = F("");
|
||||||
|
StringFormatter::send(stream, F(" %d %d %d %d \"%S\">\n"), id, type, pos, posCount, todesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "P"_hk: // <JP id> returns turntable position list for the turntable id
|
||||||
|
if (params==2) { // <JP id>
|
||||||
|
Turntable *tto=Turntable::get(id);
|
||||||
|
if (!tto || tto->isHidden()) {
|
||||||
|
StringFormatter::send(stream, F(" %d X>\n"), id);
|
||||||
|
} else {
|
||||||
|
uint8_t posCount = tto->getPositionCount();
|
||||||
|
const FSH *tpdesc = NULL;
|
||||||
|
for (uint8_t p = 0; p < posCount; p++) {
|
||||||
|
StringFormatter::send(stream, F("<jP"));
|
||||||
|
int16_t angle = tto->getPositionAngle(p);
|
||||||
|
#ifdef EXRAIL_ACTIVE
|
||||||
|
tpdesc = RMFT2::getTurntablePositionDescription(id, p);
|
||||||
|
#endif
|
||||||
|
if (tpdesc == NULL) tpdesc = F("");
|
||||||
|
StringFormatter::send(stream, F(" %d %d %d \"%S\""), id, p, angle, tpdesc);
|
||||||
|
StringFormatter::send(stream, F(">\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StringFormatter::send(stream, F("<jP X>\n"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
default: break;
|
default: break;
|
||||||
} // switch(p[1])
|
} // switch(p[1])
|
||||||
break; // case J
|
break; // case J
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No turntables without HAL support
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
case 'I': // TURNTABLE <I ...>
|
||||||
|
if (parseI(stream, params, p))
|
||||||
|
return;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
case '/': // implemented in EXRAIL parser
|
||||||
|
case 'L': // LCC interface implemented in EXRAIL parser
|
||||||
|
break; // Will <X> if not intercepted by EXRAIL
|
||||||
|
|
||||||
|
#ifndef DISABLE_VDPY
|
||||||
|
case '@': // JMRI saying "give me virtual LCD msgs"
|
||||||
|
CommandDistributor::setVirtualLCDSerial(stream);
|
||||||
|
StringFormatter::send(stream,
|
||||||
|
F("<@ 0 0 \"DCC-EX v" VERSION "\">\n"
|
||||||
|
"<@ 0 1 \"Lic GPLv3\">\n"));
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
default: //anything else will diagnose and drop out to <X>
|
default: //anything else will diagnose and drop out to <X>
|
||||||
|
if (opcode >= ' ' && opcode <= '~') {
|
||||||
DIAG(F("Opcode=%c params=%d"), opcode, params);
|
DIAG(F("Opcode=%c params=%d"), opcode, params);
|
||||||
for (int i = 0; i < params; i++)
|
for (int i = 0; i < params; i++)
|
||||||
DIAG(F("p[%d]=%d (0x%x)"), i, p[i], p[i]);
|
DIAG(F("p[%d]=%d (0x%x)"), i, p[i], p[i]);
|
||||||
break;
|
} else {
|
||||||
|
DIAG(F("Unprintable %x"), opcode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
} // end of opcode switch
|
} // end of opcode switch
|
||||||
|
|
||||||
// Any fallout here sends an <X>
|
out:// Any fallout here sends an <X>
|
||||||
StringFormatter::send(stream, F("<X>\n"));
|
StringFormatter::send(stream, F("<X>\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,14 +931,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
|
||||||
switch (p[1]) {
|
switch (p[1]) {
|
||||||
// Turnout messages use 1=throw, 0=close.
|
// Turnout messages use 1=throw, 0=close.
|
||||||
case 0:
|
case 0:
|
||||||
case HASH_KEYWORD_C:
|
case "C"_hk:
|
||||||
state = true;
|
state = true;
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
case HASH_KEYWORD_T:
|
case "T"_hk:
|
||||||
state= false;
|
state= false;
|
||||||
break;
|
break;
|
||||||
case HASH_KEYWORD_X:
|
case "X"_hk:
|
||||||
{
|
{
|
||||||
Turnout *tt = Turnout::get(p[0]);
|
Turnout *tt = Turnout::get(p[0]);
|
||||||
if (tt) {
|
if (tt) {
|
||||||
|
@ -910,14 +955,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
|
||||||
}
|
}
|
||||||
|
|
||||||
default: // Anything else is some kind of turnout create function.
|
default: // Anything else is some kind of turnout create function.
|
||||||
if (params == 6 && p[1] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n>
|
if (params == 6 && p[1] == "SERVO"_hk) { // <T id SERVO n n n n>
|
||||||
if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
|
if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
|
||||||
return false;
|
return false;
|
||||||
} else
|
} else
|
||||||
if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
|
if (params == 3 && p[1] == "VPIN"_hk) { // <T id VPIN n>
|
||||||
if (!VpinTurnout::create(p[0], p[2])) return false;
|
if (!VpinTurnout::create(p[0], p[2])) return false;
|
||||||
} else
|
} else
|
||||||
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) {
|
if (params >= 3 && p[1] == "DCC"_hk) {
|
||||||
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
|
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
|
||||||
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
|
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
|
||||||
if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
|
if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
|
||||||
|
@ -976,120 +1021,250 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
|
||||||
{
|
(void)stream; // arg not used, maybe later?
|
||||||
if (params == 0)
|
if (params == 0)
|
||||||
return false;
|
return false;
|
||||||
bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off
|
|
||||||
switch (p[0])
|
switch (p[0])
|
||||||
{
|
{
|
||||||
case HASH_KEYWORD_CABS: // <D CABS>
|
|
||||||
DCC::displayCabList(stream);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case HASH_KEYWORD_RAM: // <D RAM>
|
|
||||||
StringFormatter::send(stream, F("Free memory=%d\n"), DCCTimer::getMinimumFreeMemory());
|
|
||||||
return true;
|
|
||||||
|
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
|
case "PROGBOOST"_hk:
|
||||||
|
TrackManager::progTrackBoosted=true;
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
case "RESET"_hk:
|
||||||
|
DCCTimer::reset();
|
||||||
|
break; // and <X> if we didnt restart
|
||||||
|
case "SPEED28"_hk:
|
||||||
|
DCC::setGlobalSpeedsteps(28);
|
||||||
|
DIAG(F("28 Speedsteps"));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "SPEED128"_hk:
|
||||||
|
DCC::setGlobalSpeedsteps(128);
|
||||||
|
DIAG(F("128 Speedsteps"));
|
||||||
|
return true;
|
||||||
|
#if defined(HAS_ENOUGH_MEMORY) && !defined(ARDUINO_ARCH_UNO)
|
||||||
|
case "RAILCOM"_hk:
|
||||||
|
{ // <C RAILCOM ON|OFF|DEBUG >
|
||||||
|
if (params<2) return false;
|
||||||
|
bool on=false;
|
||||||
|
bool debug=false;
|
||||||
|
switch (p[1]) {
|
||||||
|
case "ON"_hk:
|
||||||
|
case 1:
|
||||||
|
on=true;
|
||||||
|
break;
|
||||||
|
case "DEBUG"_hk:
|
||||||
|
on=true;
|
||||||
|
debug=true;
|
||||||
|
break;
|
||||||
|
case "OFF"_hk:
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DIAG(F("Railcom %S")
|
||||||
|
,DCCWaveform::setRailcom(on,debug)?F("ON"):F("OFF"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifndef DISABLE_PROG
|
||||||
|
case "ACK"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
|
||||||
if (params >= 3) {
|
if (params >= 3) {
|
||||||
if (p[1] == HASH_KEYWORD_LIMIT) {
|
if (p[1] == "LIMIT"_hk) {
|
||||||
DCCACK::setAckLimit(p[2]);
|
DCCACK::setAckLimit(p[2]);
|
||||||
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
|
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
|
||||||
} else if (p[1] == HASH_KEYWORD_MIN) {
|
} else if (p[1] == "MIN"_hk) {
|
||||||
DCCACK::setMinAckPulseDuration(p[2]);
|
DCCACK::setMinAckPulseDuration(p[2]);
|
||||||
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
|
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
|
||||||
} else if (p[1] == HASH_KEYWORD_MAX) {
|
} else if (p[1] == "MAX"_hk) {
|
||||||
DCCACK::setMaxAckPulseDuration(p[2]);
|
DCCACK::setMaxAckPulseDuration(p[2]);
|
||||||
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
|
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
|
||||||
} else if (p[1] == HASH_KEYWORD_RETRY) {
|
} else if (p[1] == "RETRY"_hk) {
|
||||||
if (p[2] >255) p[2]=3;
|
if (p[2] >255) p[2]=3;
|
||||||
LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // <D ACK RETRY 2>
|
LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCCACK::setAckRetry(p[2])); // <D ACK RETRY 2>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off"));
|
bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
|
||||||
|
|
||||||
|
DIAG(F("Ack diag %S"), onOff ? F("on") : F("off"));
|
||||||
Diag::ACK = onOff;
|
Diag::ACK = onOff;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case HASH_KEYWORD_CMD: // <D CMD ON/OFF>
|
default: // invalid/unknown
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||||
|
{
|
||||||
|
if (params == 0)
|
||||||
|
return false;
|
||||||
|
bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
|
||||||
|
switch (p[0])
|
||||||
|
{
|
||||||
|
case "CABS"_hk: // <D CABS>
|
||||||
|
DCC::displayCabList(stream);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "RAM"_hk: // <D RAM>
|
||||||
|
DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "CMD"_hk: // <D CMD ON/OFF>
|
||||||
Diag::CMD = onOff;
|
Diag::CMD = onOff;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
#ifdef HAS_ENOUGH_MEMORY
|
#ifdef HAS_ENOUGH_MEMORY
|
||||||
case HASH_KEYWORD_WIFI: // <D WIFI ON/OFF>
|
case "WIFI"_hk: // <D WIFI ON/OFF>
|
||||||
Diag::WIFI = onOff;
|
Diag::WIFI = onOff;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
|
case "ETHERNET"_hk: // <D ETHERNET ON/OFF>
|
||||||
Diag::ETHERNET = onOff;
|
Diag::ETHERNET = onOff;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case HASH_KEYWORD_WIT: // <D WIT ON/OFF>
|
case "WIT"_hk: // <D WIT ON/OFF>
|
||||||
Diag::WITHROTTLE = onOff;
|
Diag::WITHROTTLE = onOff;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case HASH_KEYWORD_LCN: // <D LCN ON/OFF>
|
case "LCN"_hk: // <D LCN ON/OFF>
|
||||||
Diag::LCN = onOff;
|
Diag::LCN = onOff;
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
#ifndef DISABLE_PROG
|
|
||||||
case HASH_KEYWORD_PROGBOOST:
|
|
||||||
TrackManager::progTrackBoosted=true;
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
case HASH_KEYWORD_RESET:
|
|
||||||
DCCTimer::reset();
|
|
||||||
break; // and <X> if we didnt restart
|
|
||||||
|
|
||||||
|
|
||||||
#ifndef DISABLE_EEPROM
|
#ifndef DISABLE_EEPROM
|
||||||
case HASH_KEYWORD_EEPROM: // <D EEPROM NumEntries>
|
case "EEPROM"_hk: // <D EEPROM NumEntries>
|
||||||
if (params >= 2)
|
if (params >= 2)
|
||||||
EEStore::dump(p[1]);
|
EEStore::dump(p[1]);
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
case "SERVO"_hk: // <D SERVO vpin position [profile]>
|
||||||
|
|
||||||
case HASH_KEYWORD_SPEED28:
|
case "ANOUT"_hk: // <D ANOUT vpin position [profile]>
|
||||||
DCC::setGlobalSpeedsteps(28);
|
|
||||||
StringFormatter::send(stream, F("28 Speedsteps"));
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case HASH_KEYWORD_SPEED128:
|
|
||||||
DCC::setGlobalSpeedsteps(128);
|
|
||||||
StringFormatter::send(stream, F("128 Speedsteps"));
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case HASH_KEYWORD_SERVO: // <D SERVO vpin position [profile]>
|
|
||||||
case HASH_KEYWORD_ANOUT: // <D ANOUT vpin position [profile]>
|
|
||||||
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
|
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
case HASH_KEYWORD_ANIN: // <D ANIN vpin> Display analogue input value
|
case "ANIN"_hk: // <D ANIN vpin> Display analogue input value
|
||||||
DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1]));
|
DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1]));
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
#if !defined(IO_NO_HAL)
|
#if !defined(IO_NO_HAL)
|
||||||
case HASH_KEYWORD_HAL:
|
case "HAL"_hk:
|
||||||
if (p[1] == HASH_KEYWORD_SHOW)
|
if (p[1] == "SHOW"_hk)
|
||||||
IODevice::DumpAll();
|
IODevice::DumpAll();
|
||||||
else if (p[1] == HASH_KEYWORD_RESET)
|
else if (p[1] == "RESET"_hk)
|
||||||
IODevice::reset();
|
IODevice::reset();
|
||||||
break;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case HASH_KEYWORD_TT: // <D TT vpin steps activity>
|
case "TT"_hk: // <D TT vpin steps activity>
|
||||||
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
|
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
default: // invalid/unknown
|
default: // invalid/unknown
|
||||||
break;
|
return parseC(stream, params, p);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Turntable - no support if no HAL
|
||||||
|
// <I> - list all
|
||||||
|
// <I id> - broadcast type and current position
|
||||||
|
// <I id DCC> - create DCC - This is TBA
|
||||||
|
// <I id steps> - operate (DCC)
|
||||||
|
// <I id steps activity> - operate (EXTT)
|
||||||
|
// <I id ADD position value> - add position
|
||||||
|
// <I id EXTT i2caddress vpin home> - create EXTT
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
|
||||||
|
{
|
||||||
|
switch (params)
|
||||||
|
{
|
||||||
|
case 0: // <I> list turntable objects
|
||||||
|
return Turntable::printAll(stream);
|
||||||
|
|
||||||
|
case 1: // <I id> broadcast type and current position
|
||||||
|
{
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
if (tto) {
|
||||||
|
bool type = tto->isEXTT();
|
||||||
|
uint8_t position = tto->getPosition();
|
||||||
|
StringFormatter::send(stream, F("<I %d %d>\n"), type, position);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 2: // <I id position> - rotate a DCC turntable
|
||||||
|
{
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
if (tto && !tto->isEXTT()) {
|
||||||
|
if (!tto->setPosition(p[0], p[1])) return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable
|
||||||
|
{
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
if (p[1] == "DCC"_hk) {
|
||||||
|
if (tto || p[2] < 0 || p[2] > 3600) return false;
|
||||||
|
if (!DCCTurntable::create(p[0])) return false;
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
tto->addPosition(0, 0, p[2]);
|
||||||
|
StringFormatter::send(stream, F("<I>\n"));
|
||||||
|
} else {
|
||||||
|
if (!tto) return false;
|
||||||
|
if (!tto->isEXTT()) return false;
|
||||||
|
if (!tto->setPosition(p[0], p[1], p[2])) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 4: // <I id EXTT vpin home> create an EXTT turntable
|
||||||
|
{
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
if (p[1] == "EXTT"_hk) {
|
||||||
|
if (tto || p[3] < 0 || p[3] > 3600) return false;
|
||||||
|
if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false;
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
tto->addPosition(0, 0, p[3]);
|
||||||
|
StringFormatter::send(stream, F("<I>\n"));
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 5: // <I id ADD position value angle> add a position
|
||||||
|
{
|
||||||
|
Turntable *tto = Turntable::get(p[0]);
|
||||||
|
if (p[1] == "ADD"_hk) {
|
||||||
|
// tto must exist, no more than 48 positions, angle 0 - 3600
|
||||||
|
if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false;
|
||||||
|
tto->addPosition(p[2], p[3], p[4]);
|
||||||
|
StringFormatter::send(stream, F("<I>\n"));
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default: // Anything else is invalid
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// CALLBACKS must be static
|
// CALLBACKS must be static
|
||||||
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
|
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
|
||||||
{
|
{
|
||||||
|
@ -1176,3 +1351,11 @@ void DCCEXParser::callback_Wloco(int16_t result)
|
||||||
StringFormatter::send(getAsyncReplyStream(), F("<w %d>\n"), result);
|
StringFormatter::send(getAsyncReplyStream(), F("<w %d>\n"), result);
|
||||||
commitAsyncReplyStream();
|
commitAsyncReplyStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCEXParser::callback_Wconsist(int16_t result)
|
||||||
|
{
|
||||||
|
if (result==1) result=stashP[1]; // pick up original requested id from command
|
||||||
|
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
|
||||||
|
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));
|
||||||
|
commitAsyncReplyStream();
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "FSH.h"
|
#include "FSH.h"
|
||||||
#include "RingStream.h"
|
#include "RingStream.h"
|
||||||
|
#include "defines.h"
|
||||||
|
|
||||||
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||||
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
|
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
|
||||||
|
@ -45,13 +46,17 @@ struct DCCEXParser
|
||||||
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
|
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
|
||||||
|
|
||||||
static bool parseT(Print * stream, int16_t params, int16_t p[]);
|
static bool parseT(Print * stream, int16_t params, int16_t p[]);
|
||||||
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
|
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
|
||||||
static bool parseS(Print * stream, int16_t params, int16_t p[]);
|
static bool parseS(Print * stream, int16_t params, int16_t p[]);
|
||||||
static bool parsef(Print * stream, int16_t params, int16_t p[]);
|
static bool parsef(Print * stream, int16_t params, int16_t p[]);
|
||||||
static bool parseD(Print * stream, int16_t params, int16_t p[]);
|
static bool parseC(Print * stream, int16_t params, int16_t p[]);
|
||||||
|
static bool parseD(Print * stream, int16_t params, int16_t p[]);
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
static bool parseI(Print * stream, int16_t params, int16_t p[]);
|
||||||
|
#endif
|
||||||
|
|
||||||
static Print * getAsyncReplyStream();
|
static Print * getAsyncReplyStream();
|
||||||
static void commitAsyncReplyStream();
|
static void commitAsyncReplyStream();
|
||||||
|
|
||||||
static bool stashBusy;
|
static bool stashBusy;
|
||||||
static byte stashTarget;
|
static byte stashTarget;
|
||||||
|
@ -66,6 +71,7 @@ struct DCCEXParser
|
||||||
static void callback_R(int16_t result);
|
static void callback_R(int16_t result);
|
||||||
static void callback_Rloco(int16_t result);
|
static void callback_Rloco(int16_t result);
|
||||||
static void callback_Wloco(int16_t result);
|
static void callback_Wloco(int16_t result);
|
||||||
|
static void callback_Wconsist(int16_t result);
|
||||||
static void callback_Vbit(int16_t result);
|
static void callback_Vbit(int16_t result);
|
||||||
static void callback_Vbyte(int16_t result);
|
static void callback_Vbyte(int16_t result);
|
||||||
static FILTER_CALLBACK filterCallback;
|
static FILTER_CALLBACK filterCallback;
|
||||||
|
|
48
DCCRMT.cpp
48
DCCRMT.cpp
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* © 2021-2022, Harald Barth.
|
* © 2021-2024, Harald Barth.
|
||||||
*
|
*
|
||||||
* This file is part of DCC-EX
|
* This file is part of DCC-EX
|
||||||
*
|
*
|
||||||
|
@ -25,6 +25,18 @@
|
||||||
#include "DCCWaveform.h" // for MAX_PACKET_SIZE
|
#include "DCCWaveform.h" // for MAX_PACKET_SIZE
|
||||||
#include "soc/gpio_sig_map.h"
|
#include "soc/gpio_sig_map.h"
|
||||||
|
|
||||||
|
// check for right type of ESP32
|
||||||
|
#include "soc/soc_caps.h"
|
||||||
|
#ifndef SOC_RMT_MEM_WORDS_PER_CHANNEL
|
||||||
|
#error This symobol should be defined
|
||||||
|
#endif
|
||||||
|
#if SOC_RMT_MEM_WORDS_PER_CHANNEL < 64
|
||||||
|
#warning This is not an ESP32-WROOM but some other unsupported variant
|
||||||
|
#warning You are outside of the DCC-EX supported hardware
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static const byte RMT_CHAN_PER_DCC_CHAN = 2;
|
||||||
|
|
||||||
// Number of bits resulting out of X bytes of DCC payload data
|
// Number of bits resulting out of X bytes of DCC payload data
|
||||||
// Each byte has one bit extra and at the end we have one EOF marker
|
// Each byte has one bit extra and at the end we have one EOF marker
|
||||||
#define DATA_LEN(X) ((X)*9+1)
|
#define DATA_LEN(X) ((X)*9+1)
|
||||||
|
@ -75,12 +87,30 @@ void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {
|
||||||
RMTChannel::RMTChannel(pinpair pins, bool isMain) {
|
RMTChannel::RMTChannel(pinpair pins, bool isMain) {
|
||||||
byte ch;
|
byte ch;
|
||||||
byte plen;
|
byte plen;
|
||||||
|
|
||||||
|
// Below we check if the DCC packet actually fits into the RMT hardware
|
||||||
|
// Currently MAX_PACKET_SIZE = 5 so with checksum there are
|
||||||
|
// MAX_PACKET_SIZE+1 data packets. Each need DATA_LEN (9) bits.
|
||||||
|
// To that we add the preamble length, the fencepost DCC end bit
|
||||||
|
// and the RMT EOF marker.
|
||||||
|
// SOC_RMT_MEM_WORDS_PER_CHANNEL is either 64 (original WROOM) or
|
||||||
|
// 48 (all other ESP32 like the -C3 or -S2
|
||||||
|
// The formula to get the possible MAX_PACKET_SIZE is
|
||||||
|
//
|
||||||
|
// ALLOCATED = RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL
|
||||||
|
// MAX_PACKET_SIZE = floor((ALLOCATED - PREAMBLE_LEN - 2)/9 - 1)
|
||||||
|
//
|
||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
ch = 0;
|
ch = 0;
|
||||||
plen = PREAMBLE_BITS_MAIN;
|
plen = PREAMBLE_BITS_MAIN;
|
||||||
|
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_MAIN + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
|
||||||
|
"Number of DCC packet bits greater than ESP32 RMT memory available");
|
||||||
} else {
|
} else {
|
||||||
ch = 2;
|
ch = RMT_CHAN_PER_DCC_CHAN; // number == offset
|
||||||
plen = PREAMBLE_BITS_PROG;
|
plen = PREAMBLE_BITS_PROG;
|
||||||
|
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_PROG + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
|
||||||
|
"Number of DCC packet bits greater than ESP32 RMT memory available");
|
||||||
}
|
}
|
||||||
|
|
||||||
// preamble
|
// preamble
|
||||||
|
@ -123,20 +153,10 @@ RMTChannel::RMTChannel(pinpair pins, bool isMain) {
|
||||||
config.channel = channel = (rmt_channel_t)ch;
|
config.channel = channel = (rmt_channel_t)ch;
|
||||||
config.clk_div = RMT_CLOCK_DIVIDER;
|
config.clk_div = RMT_CLOCK_DIVIDER;
|
||||||
config.gpio_num = (gpio_num_t)pins.pin;
|
config.gpio_num = (gpio_num_t)pins.pin;
|
||||||
config.mem_block_num = 2; // With longest DCC packet 11 inc checksum (future expansion)
|
config.mem_block_num = RMT_CHAN_PER_DCC_CHAN;
|
||||||
// number of bits needed is 22preamble + start +
|
// use config
|
||||||
// 11*9 + extrazero + EOT = 124
|
|
||||||
// 2 mem block of 64 RMT items should be enough
|
|
||||||
|
|
||||||
ESP_ERROR_CHECK(rmt_config(&config));
|
ESP_ERROR_CHECK(rmt_config(&config));
|
||||||
addPin(pins.invpin, true);
|
addPin(pins.invpin, true);
|
||||||
/*
|
|
||||||
// test: config another gpio pin
|
|
||||||
gpio_num_t gpioNum = (gpio_num_t)(pin-1);
|
|
||||||
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
|
|
||||||
gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
|
|
||||||
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX, 0, 0);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
|
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
|
||||||
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));
|
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));
|
||||||
|
|
18
DCCTimer.h
18
DCCTimer.h
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* © 2022-2023 Paul M. Antoine
|
* © 2022-2024 Paul M. Antoine
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021-2023 Harald Barth
|
* © 2021-2023 Harald Barth
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
|
@ -62,8 +62,14 @@ class DCCTimer {
|
||||||
static bool isPWMPin(byte pin);
|
static bool isPWMPin(byte pin);
|
||||||
static void setPWM(byte pin, bool high);
|
static void setPWM(byte pin, bool high);
|
||||||
static void clearPWM();
|
static void clearPWM();
|
||||||
|
static void startRailcomTimer(byte brakePin);
|
||||||
|
static void ackRailcomTimer();
|
||||||
static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);
|
static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);
|
||||||
static void DCCEXanalogWrite(uint8_t pin, int value);
|
static void DCCEXanalogWrite(uint8_t pin, int value, bool invert);
|
||||||
|
static void DCCEXledcDetachPin(uint8_t pin);
|
||||||
|
static void DCCEXanalogCopyChannel(int8_t frompin, int8_t topin);
|
||||||
|
static void DCCEXInrushControlOn(uint8_t pin, int duty, bool invert);
|
||||||
|
static void DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted);
|
||||||
|
|
||||||
// Update low ram level. Allow for extra bytes to be specified
|
// Update low ram level. Allow for extra bytes to be specified
|
||||||
// by estimation or inspection, that may be used by other
|
// by estimation or inspection, that may be used by other
|
||||||
|
@ -85,6 +91,7 @@ class DCCTimer {
|
||||||
static void reset();
|
static void reset();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static void DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency);
|
||||||
static int freeMemory();
|
static int freeMemory();
|
||||||
static volatile int minimum_free_memory;
|
static volatile int minimum_free_memory;
|
||||||
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
|
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
|
||||||
|
@ -125,8 +132,15 @@ private:
|
||||||
// On platforms that scan, it is called from waveform ISR
|
// On platforms that scan, it is called from waveform ISR
|
||||||
// only on a regular basis.
|
// only on a regular basis.
|
||||||
static void scan();
|
static void scan();
|
||||||
|
#if defined (ARDUINO_ARCH_STM32)
|
||||||
|
// bit array of used pins (max 32)
|
||||||
|
static uint32_t usedpins;
|
||||||
|
static uint32_t * analogchans; // Array of channel numbers to be scanned
|
||||||
|
static ADC_TypeDef * * adcchans; // Array to capture which ADC is each input channel on
|
||||||
|
#else
|
||||||
// bit array of used pins (max 16)
|
// bit array of used pins (max 16)
|
||||||
static uint16_t usedpins;
|
static uint16_t usedpins;
|
||||||
|
#endif
|
||||||
static uint8_t highestPin;
|
static uint8_t highestPin;
|
||||||
// cached analog values (malloc:ed to actual number of ADC channels)
|
// cached analog values (malloc:ed to actual number of ADC channels)
|
||||||
static int *analogvals;
|
static int *analogvals;
|
||||||
|
|
146
DCCTimerAVR.cpp
146
DCCTimerAVR.cpp
|
@ -29,6 +29,7 @@
|
||||||
#include <avr/boot.h>
|
#include <avr/boot.h>
|
||||||
#include <avr/wdt.h>
|
#include <avr/wdt.h>
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
|
#include "DIAG.h"
|
||||||
#ifdef DEBUG_ADC
|
#ifdef DEBUG_ADC
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#endif
|
#endif
|
||||||
|
@ -39,6 +40,9 @@ INTERRUPT_CALLBACK interruptHandler=0;
|
||||||
#define TIMER1_A_PIN 11
|
#define TIMER1_A_PIN 11
|
||||||
#define TIMER1_B_PIN 12
|
#define TIMER1_B_PIN 12
|
||||||
#define TIMER1_C_PIN 13
|
#define TIMER1_C_PIN 13
|
||||||
|
#define TIMER2_A_PIN 10
|
||||||
|
#define TIMER2_B_PIN 9
|
||||||
|
|
||||||
#else
|
#else
|
||||||
#define TIMER1_A_PIN 9
|
#define TIMER1_A_PIN 9
|
||||||
#define TIMER1_B_PIN 10
|
#define TIMER1_B_PIN 10
|
||||||
|
@ -55,6 +59,67 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||||
interrupts();
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||||
|
/* The Railcom timer is started in such a way that it
|
||||||
|
- First triggers 28uS after the last TIMER1 tick.
|
||||||
|
This provides an accurate offset (in High Accuracy mode)
|
||||||
|
for the start of the Railcom cutout.
|
||||||
|
- Sets the Railcom pin high at first tick,
|
||||||
|
because its been setup with 100% PWM duty cycle.
|
||||||
|
|
||||||
|
- Cycles at 436uS so the second tick is the
|
||||||
|
correct distance from the cutout.
|
||||||
|
|
||||||
|
- Waveform code is responsible for altering the PWM
|
||||||
|
duty cycle to 0% any time between the first and last tick.
|
||||||
|
(there will be 7 DCC timer1 ticks in which to do this.)
|
||||||
|
|
||||||
|
*/
|
||||||
|
(void) brakePin; // Ignored... works on pin 9 only
|
||||||
|
const int cutoutDuration = 430; // Desired interval in microseconds
|
||||||
|
|
||||||
|
// Set up Timer2 for CTC mode (Clear Timer on Compare Match)
|
||||||
|
TCCR2A = 0; // Clear Timer2 control register A
|
||||||
|
TCCR2B = 0; // Clear Timer2 control register B
|
||||||
|
TCNT2 = 0; // Initialize Timer2 counter value to 0
|
||||||
|
// Configure Phase and Frequency Correct PWM mode
|
||||||
|
TCCR2A = (1 << COM2B1); // enable pwm on pin 9
|
||||||
|
TCCR2A |= (1 << WGM20);
|
||||||
|
|
||||||
|
|
||||||
|
// Set Timer 2 prescaler to 32
|
||||||
|
TCCR2B = (1 << CS21) | (1 << CS20); // 32 prescaler
|
||||||
|
|
||||||
|
// Set the compare match value for desired interval
|
||||||
|
OCR2A = (F_CPU / 1000000) * cutoutDuration / 64 - 1;
|
||||||
|
|
||||||
|
// Calculate the compare match value for desired duty cycle
|
||||||
|
OCR2B = OCR2A+1; // set duty cycle to 100%= OCR2A)
|
||||||
|
|
||||||
|
// Enable Timer2 output on pin 9 (OC2B)
|
||||||
|
DDRB |= (1 << DDB1);
|
||||||
|
// TODO Fudge TCNT2 to sync with last tcnt1 tick + 28uS
|
||||||
|
|
||||||
|
// Previous TIMER1 Tick was at rising end-of-packet bit
|
||||||
|
// Cutout starts half way through first preamble
|
||||||
|
// that is 2.5 * 58uS later.
|
||||||
|
// TCNT1 ticks 8 times / microsecond
|
||||||
|
// auto microsendsToFirstRailcomTick=(58+58+29)-(TCNT1/8);
|
||||||
|
// set the railcom timer counter allowing for phase-correct
|
||||||
|
|
||||||
|
// CHris's NOTE:
|
||||||
|
// I dont kniow quite how this calculation works out but
|
||||||
|
// it does seems to get a good answer.
|
||||||
|
|
||||||
|
TCNT2=193 + (ICR1 - TCNT1)/8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::ackRailcomTimer() {
|
||||||
|
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ISR called by timer interrupt every 58uS
|
// ISR called by timer interrupt every 58uS
|
||||||
ISR(TIMER1_OVF_vect){ interruptHandler(); }
|
ISR(TIMER1_OVF_vect){ interruptHandler(); }
|
||||||
|
|
||||||
|
@ -120,11 +185,88 @@ int DCCTimer::freeMemory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void DCCTimer::reset() {
|
void DCCTimer::reset() {
|
||||||
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
|
// 250ms chosen to circumwent bootloader bug which
|
||||||
delay(50); // wait for the prescaller time to expire
|
// hangs at too short timepout (like 15ms)
|
||||||
|
wdt_enable( WDTO_250MS); // set Arduino watchdog timer for 250ms
|
||||||
|
delay(500); // wait for it to happen
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
|
||||||
|
}
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
|
||||||
|
#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)
|
||||||
|
// Speed mapping is done like this:
|
||||||
|
// No functions buttons: 000 0 -> low 131Hz
|
||||||
|
// Only F29 pressed 001 1 -> mid 490Hz
|
||||||
|
// F30 with or w/o F29 01x 2-3 -> high 3400Hz
|
||||||
|
// F31 with or w/o F29/30 1xx 4-7 -> supersonic 62500Hz
|
||||||
|
uint8_t abits;
|
||||||
|
uint8_t bbits;
|
||||||
|
if (pin == 9 || pin == 10) { // timer 2 is different
|
||||||
|
|
||||||
|
if (fbits >= 4)
|
||||||
|
abits = B00000011;
|
||||||
|
else
|
||||||
|
abits = B00000001;
|
||||||
|
|
||||||
|
if (fbits >= 4)
|
||||||
|
bbits = B0001;
|
||||||
|
else if (fbits >= 2)
|
||||||
|
bbits = B0010;
|
||||||
|
else if (fbits == 1)
|
||||||
|
bbits = B0100;
|
||||||
|
else // fbits == 0
|
||||||
|
bbits = B0110;
|
||||||
|
|
||||||
|
TCCR2A = (TCCR2A & B11111100) | abits; // set WGM0 and WGM1
|
||||||
|
TCCR2B = (TCCR2B & B11110000) | bbits; // set WGM2 and 3 bits of prescaler
|
||||||
|
DIAG(F("Timer 2 A=%x B=%x"), TCCR2A, TCCR2B);
|
||||||
|
|
||||||
|
} else { // not timer 9 or 10
|
||||||
|
abits = B01;
|
||||||
|
|
||||||
|
if (fbits >= 4)
|
||||||
|
bbits = B1001;
|
||||||
|
else if (fbits >= 2)
|
||||||
|
bbits = B0010;
|
||||||
|
else if (fbits == 1)
|
||||||
|
bbits = B0011;
|
||||||
|
else
|
||||||
|
bbits = B0100;
|
||||||
|
|
||||||
|
switch (pin) {
|
||||||
|
// case 9 and 10 taken care of above by if()
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
case 8:
|
||||||
|
// Timer4
|
||||||
|
TCCR4A = (TCCR4A & B11111100) | abits; // set WGM0 and WGM1
|
||||||
|
TCCR4B = (TCCR4B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
|
||||||
|
//DIAG(F("Timer 4 A=%x B=%x"), TCCR4A, TCCR4B);
|
||||||
|
break;
|
||||||
|
case 46:
|
||||||
|
case 45:
|
||||||
|
case 44:
|
||||||
|
// Timer5
|
||||||
|
TCCR5A = (TCCR5A & B11111100) | abits; // set WGM0 and WGM1
|
||||||
|
TCCR5B = (TCCR5B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
|
||||||
|
//DIAG(F("Timer 5 A=%x B=%x"), TCCR5A, TCCR5B);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
|
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
|
||||||
#define NUM_ADC_INPUTS 16
|
#define NUM_ADC_INPUTS 16
|
||||||
#else
|
#else
|
||||||
|
|
120
DCCTimerESP.cpp
120
DCCTimerESP.cpp
|
@ -76,8 +76,13 @@ int DCCTimer::freeMemory() {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
#include "esp_idf_version.h"
|
||||||
|
#if ESP_IDF_VERSION_MAJOR > 4
|
||||||
|
#error "DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDF version 4. Arduino ESP32 library 3.0.0 is too new. Use 2.0.9 to 2.0.17"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <driver/adc.h>
|
#include <driver/adc.h>
|
||||||
#include <soc/sens_reg.h>
|
#include <soc/sens_reg.h>
|
||||||
#include <soc/sens_struct.h>
|
#include <soc/sens_struct.h>
|
||||||
|
@ -151,10 +156,28 @@ void DCCTimer::reset() {
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
|
if (f >= 16)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
|
||||||
|
/*
|
||||||
|
else if (f == 7) // not used on ESP32
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
|
||||||
|
*/
|
||||||
|
else if (f >= 4)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
|
||||||
|
else if (f >= 3)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
|
||||||
|
else if (f >= 2)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
|
||||||
|
else if (f == 1)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
|
||||||
|
else
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
|
||||||
|
}
|
||||||
|
|
||||||
#include "esp32-hal.h"
|
#include "esp32-hal.h"
|
||||||
#include "soc/soc_caps.h"
|
#include "soc/soc_caps.h"
|
||||||
|
|
||||||
|
|
||||||
#ifdef SOC_LEDC_SUPPORT_HS_MODE
|
#ifdef SOC_LEDC_SUPPORT_HS_MODE
|
||||||
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
|
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
|
||||||
#else
|
#else
|
||||||
|
@ -164,7 +187,7 @@ void DCCTimer::reset() {
|
||||||
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
|
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
|
||||||
static int cnt_channel = LEDC_CHANNELS;
|
static int cnt_channel = LEDC_CHANNELS;
|
||||||
|
|
||||||
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency) {
|
||||||
if (pin < SOC_GPIO_PIN_COUNT) {
|
if (pin < SOC_GPIO_PIN_COUNT) {
|
||||||
if (pin_to_channel[pin] != 0) {
|
if (pin_to_channel[pin] != 0) {
|
||||||
ledcSetup(pin_to_channel[pin], frequency, 8);
|
ledcSetup(pin_to_channel[pin], frequency, 8);
|
||||||
|
@ -172,23 +195,104 @@ void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) {
|
void DCCTimer::DCCEXledcDetachPin(uint8_t pin) {
|
||||||
|
DIAG(F("Clear pin %d channel"), pin);
|
||||||
|
pin_to_channel[pin] = 0;
|
||||||
|
pinMatrixOutDetach(pin, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte LEDCToMux[] = {
|
||||||
|
LEDC_HS_SIG_OUT0_IDX,
|
||||||
|
LEDC_HS_SIG_OUT1_IDX,
|
||||||
|
LEDC_HS_SIG_OUT2_IDX,
|
||||||
|
LEDC_HS_SIG_OUT3_IDX,
|
||||||
|
LEDC_HS_SIG_OUT4_IDX,
|
||||||
|
LEDC_HS_SIG_OUT5_IDX,
|
||||||
|
LEDC_HS_SIG_OUT6_IDX,
|
||||||
|
LEDC_HS_SIG_OUT7_IDX,
|
||||||
|
LEDC_LS_SIG_OUT0_IDX,
|
||||||
|
LEDC_LS_SIG_OUT1_IDX,
|
||||||
|
LEDC_LS_SIG_OUT2_IDX,
|
||||||
|
LEDC_LS_SIG_OUT3_IDX,
|
||||||
|
LEDC_LS_SIG_OUT4_IDX,
|
||||||
|
LEDC_LS_SIG_OUT5_IDX,
|
||||||
|
LEDC_LS_SIG_OUT6_IDX,
|
||||||
|
LEDC_LS_SIG_OUT7_IDX,
|
||||||
|
};
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted) {
|
||||||
|
DIAG(F("Attaching pin %d to channel %d %c"), pin, channel, inverted ? 'I' : ' ');
|
||||||
|
ledcAttachPin(pin, channel);
|
||||||
|
if (inverted) // we attach again but with inversion
|
||||||
|
gpio_matrix_out(pin, LEDCToMux[channel], inverted, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogCopyChannel(int8_t frompin, int8_t topin) {
|
||||||
|
// arguments are signed depending on inversion of pins
|
||||||
|
DIAG(F("Pin %d copied to %d"), frompin, topin);
|
||||||
|
bool inverted = false;
|
||||||
|
if (frompin<0)
|
||||||
|
frompin = -frompin;
|
||||||
|
if (topin<0) {
|
||||||
|
inverted = true;
|
||||||
|
topin = -topin;
|
||||||
|
}
|
||||||
|
int channel = pin_to_channel[frompin]; // after abs(frompin)
|
||||||
|
pin_to_channel[topin] = channel;
|
||||||
|
DCCTimer::DCCEXledcAttachPin(topin, channel, inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
|
||||||
|
// This allocates channels 15, 13, 11, ....
|
||||||
|
// so each channel gets its own timer.
|
||||||
if (pin < SOC_GPIO_PIN_COUNT) {
|
if (pin < SOC_GPIO_PIN_COUNT) {
|
||||||
if (pin_to_channel[pin] == 0) {
|
if (pin_to_channel[pin] == 0) {
|
||||||
|
int search_channel;
|
||||||
|
int n;
|
||||||
if (!cnt_channel) {
|
if (!cnt_channel) {
|
||||||
log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS);
|
log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pin_to_channel[pin] = --cnt_channel;
|
// search for free channels top down
|
||||||
ledcSetup(cnt_channel, 1000, 8);
|
for (search_channel=LEDC_CHANNELS-1; search_channel >=cnt_channel; search_channel -= 2) {
|
||||||
ledcAttachPin(pin, cnt_channel);
|
bool chanused = false;
|
||||||
|
for (n=0; n < SOC_GPIO_PIN_COUNT; n++) {
|
||||||
|
if (pin_to_channel[n] == search_channel) { // current search_channel used
|
||||||
|
chanused = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chanused)
|
||||||
|
continue;
|
||||||
|
if (n == SOC_GPIO_PIN_COUNT) // current search_channel unused
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (search_channel >= cnt_channel) {
|
||||||
|
pin_to_channel[pin] = search_channel;
|
||||||
|
DIAG(F("Pin %d assigned to search channel %d"), pin, search_channel);
|
||||||
|
} else {
|
||||||
|
pin_to_channel[pin] = --cnt_channel; // This sets 15, 13, ...
|
||||||
|
DIAG(F("Pin %d assigned to new channel %d"), pin, cnt_channel);
|
||||||
|
--cnt_channel; // Now we are at 14, 12, ...
|
||||||
|
}
|
||||||
|
ledcSetup(pin_to_channel[pin], 1000, 8);
|
||||||
|
DCCEXledcAttachPin(pin, pin_to_channel[pin], invert);
|
||||||
} else {
|
} else {
|
||||||
ledcAttachPin(pin, pin_to_channel[pin]);
|
// This else is only here so we can enable diag
|
||||||
|
// Pin should be already attached to channel
|
||||||
|
// DIAG(F("Pin %d assigned to old channel %d"), pin, pin_to_channel[pin]);
|
||||||
}
|
}
|
||||||
ledcWrite(pin_to_channel[pin], value);
|
ledcWrite(pin_to_channel[pin], value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXInrushControlOn(uint8_t pin, int duty, bool inverted) {
|
||||||
|
// this uses hardcoded channel 0
|
||||||
|
ledcSetup(0, 62500, 8);
|
||||||
|
DCCEXledcAttachPin(pin, 0, inverted);
|
||||||
|
ledcWrite(0, duty);
|
||||||
|
}
|
||||||
|
|
||||||
int ADCee::init(uint8_t pin) {
|
int ADCee::init(uint8_t pin) {
|
||||||
pinMode(pin, ANALOG);
|
pinMode(pin, ANALOG);
|
||||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
adc1_config_width(ADC_WIDTH_BIT_12);
|
||||||
|
|
|
@ -80,6 +80,15 @@ extern char *__malloc_heap_start;
|
||||||
interruptHandler();
|
interruptHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
(void) brakePin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::ackRailcomTimer() {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
}
|
||||||
|
|
||||||
bool DCCTimer::isPWMPin(byte pin) {
|
bool DCCTimer::isPWMPin(byte pin) {
|
||||||
(void) pin;
|
(void) pin;
|
||||||
return false; // TODO what are the relevant pins?
|
return false; // TODO what are the relevant pins?
|
||||||
|
@ -125,6 +134,11 @@ void DCCTimer::reset() {
|
||||||
while(true){}
|
while(true){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
|
}
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
|
||||||
|
}
|
||||||
|
|
||||||
int16_t ADCee::ADCmax() {
|
int16_t ADCee::ADCmax() {
|
||||||
return 4095;
|
return 4095;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,15 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||||
interrupts();
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
(void) brakePin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::ackRailcomTimer() {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
}
|
||||||
|
|
||||||
// Timer IRQ handlers replace the dummy handlers (in cortex_handlers)
|
// Timer IRQ handlers replace the dummy handlers (in cortex_handlers)
|
||||||
// copied from rf24 branch
|
// copied from rf24 branch
|
||||||
void TCC0_Handler() {
|
void TCC0_Handler() {
|
||||||
|
@ -156,6 +165,11 @@ void DCCTimer::reset() {
|
||||||
while(true) {};
|
while(true) {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
|
}
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
|
||||||
|
}
|
||||||
|
|
||||||
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
|
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
|
||||||
|
|
||||||
uint16_t ADCee::usedpins = 0;
|
uint16_t ADCee::usedpins = 0;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* © 2023 Neil McKechnie
|
* © 2023 Neil McKechnie
|
||||||
* © 2022-23 Paul M. Antoine
|
* © 2022-2024 Paul M. Antoine
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021, 2023 Harald Barth
|
* © 2021, 2023 Harald Barth
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
|
@ -34,8 +34,22 @@
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#endif
|
#endif
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
|
#include <wiring_private.h>
|
||||||
|
|
||||||
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE)
|
#if defined(ARDUINO_NUCLEO_F401RE)
|
||||||
|
// Nucleo-64 boards don't have additional serial ports defined by default
|
||||||
|
// Serial1 is available on the F401RE, but not hugely convenient.
|
||||||
|
// Rx pin on PB7 is useful, but all the Tx pins map to Arduino digital pins, specifically:
|
||||||
|
// PA9 == D8
|
||||||
|
// PB6 == D10
|
||||||
|
// of which D8 is needed by the standard and EX8874 motor shields. D10 would be used if a second
|
||||||
|
// EX8874 is stacked. So only disable this if using a second motor shield.
|
||||||
|
HardwareSerial Serial1(PB7, PB6); // Rx=PB7, Tx=PB6 -- CN7 pin 17 and CN10 pin 17
|
||||||
|
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
|
||||||
|
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
||||||
|
// Let's define Serial6 as an additional serial port (the only other option for the F401RE)
|
||||||
|
HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F401RE
|
||||||
|
#elif defined(ARDUINO_NUCLEO_F411RE)
|
||||||
// Nucleo-64 boards don't have additional serial ports defined by default
|
// 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
|
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
|
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
|
||||||
|
@ -50,11 +64,16 @@ HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14
|
||||||
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
||||||
// On the F446RE, Serial3 and Serial5 are easy to use:
|
// On the F446RE, Serial3 and Serial5 are easy to use:
|
||||||
HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE
|
HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE
|
||||||
HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE
|
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 - F446RE
|
||||||
// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins
|
// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins
|
||||||
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)|| defined(ARDUINO_NUCLEO_F412ZG)
|
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) || \
|
||||||
|
defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||||
// Nucleo-144 boards don't have Serial1 defined by default
|
// Nucleo-144 boards don't have Serial1 defined by default
|
||||||
HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
|
HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
|
||||||
|
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5
|
||||||
|
#if !defined(ARDUINO_NUCLEO_F412ZG)
|
||||||
|
HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART5
|
||||||
|
#endif
|
||||||
// Serial3 is defined to use USART3 by default, but is in fact used as the diag console
|
// Serial3 is defined to use USART3 by default, but is in fact used as the diag console
|
||||||
// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
|
||||||
#else
|
#else
|
||||||
|
@ -154,13 +173,28 @@ HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
INTERRUPT_CALLBACK interruptHandler=0;
|
INTERRUPT_CALLBACK interruptHandler=0;
|
||||||
// Let's use STM32's timer #11 until disabused of this notion
|
|
||||||
// Timer #11 is used for "servo" library, but as DCC-EX is not using
|
// On STM32F4xx models that have them, Timers 6 and 7 have no PWM output capability,
|
||||||
// this libary, we should be free and clear.
|
// so are good choices for general timer duties - they are used for tone and servo
|
||||||
HardwareTimer timer(TIM11);
|
// in stm32duino so we shall usurp those as DCC-EX doesn't use tone or servo libs.
|
||||||
|
// NB: the F401, F410 and F411 do **not** have Timer 6 or 7, so we use Timer 11
|
||||||
|
#ifndef DCC_EX_TIMER
|
||||||
|
#if defined(TIM6)
|
||||||
|
#define DCC_EX_TIMER TIM6
|
||||||
|
#elif defined(TIM7)
|
||||||
|
#define DCC_EX_TIMER TIM7
|
||||||
|
#elif defined(TIM11)
|
||||||
|
#define DCC_EX_TIMER TIM11
|
||||||
|
#else
|
||||||
|
#warning This STM32F4XX variant does not have Timers 6,7 or 11!!
|
||||||
|
#endif
|
||||||
|
#endif // ifndef DCC_EX_TIMER
|
||||||
|
|
||||||
|
HardwareTimer dcctimer(DCC_EX_TIMER);
|
||||||
|
void DCCTimer_Handler() __attribute__((interrupt));
|
||||||
|
|
||||||
// Timer IRQ handler
|
// Timer IRQ handler
|
||||||
void Timer11_Handler() {
|
void DCCTimer_Handler() {
|
||||||
interruptHandler();
|
interruptHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,22 +202,33 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||||
interruptHandler=callback;
|
interruptHandler=callback;
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
|
|
||||||
// adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);
|
dcctimer.pause();
|
||||||
timer.pause();
|
dcctimer.setPrescaleFactor(1);
|
||||||
timer.setPrescaleFactor(1);
|
|
||||||
// timer.setOverflow(CLOCK_CYCLES * 2);
|
// timer.setOverflow(CLOCK_CYCLES * 2);
|
||||||
timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
|
dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
|
||||||
timer.attachInterrupt(Timer11_Handler);
|
// dcctimer.attachInterrupt(Timer11_Handler);
|
||||||
timer.refresh();
|
dcctimer.attachInterrupt(DCCTimer_Handler);
|
||||||
timer.resume();
|
dcctimer.setInterruptPriority(0, 0); // Set highest preemptive priority!
|
||||||
|
dcctimer.refresh();
|
||||||
|
dcctimer.resume();
|
||||||
|
|
||||||
interrupts();
|
interrupts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
(void) brakePin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::ackRailcomTimer() {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
}
|
||||||
|
|
||||||
bool DCCTimer::isPWMPin(byte pin) {
|
bool DCCTimer::isPWMPin(byte pin) {
|
||||||
//TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
|
//TODO: STM32 whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
|
||||||
// there's no support yet for High Accuracy, so for now return false
|
// there's no support yet for High Accuracy, so for now return false
|
||||||
// return digitalPinHasPWM(pin);
|
// return digitalPinHasPWM(pin);
|
||||||
|
(void) pin;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +243,9 @@ void DCCTimer::clearPWM() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
|
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
|
||||||
volatile uint32_t *serno1 = (volatile uint32_t *)0x1FFF7A10;
|
volatile uint32_t *serno1 = (volatile uint32_t *)UID_BASE;
|
||||||
volatile uint32_t *serno2 = (volatile uint32_t *)0x1FFF7A14;
|
volatile uint32_t *serno2 = (volatile uint32_t *)UID_BASE+4;
|
||||||
// volatile uint32_t *serno3 = (volatile uint32_t *)0x1FFF7A18;
|
// volatile uint32_t *serno3 = (volatile uint32_t *)UID_BASE+8;
|
||||||
|
|
||||||
volatile uint32_t m1 = *serno1;
|
volatile uint32_t m1 = *serno1;
|
||||||
volatile uint32_t m2 = *serno2;
|
volatile uint32_t m2 = *serno2;
|
||||||
|
@ -235,22 +280,110 @@ void DCCTimer::reset() {
|
||||||
while(true) {};
|
while(true) {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: may need to use uint32_t on STMF4xx variants with > 16 analog inputs!
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
#if defined(ARDUINO_NUCLEO_F446RE) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
|
if (f >= 16)
|
||||||
#warning STM32 board selected not fully supported - only use ADC1 inputs 0-15 for current sensing!
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
|
||||||
#endif
|
else if (f == 7)
|
||||||
// For now, define the max of 16 ports - some variants have more, but this not **yet** supported
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
|
||||||
#define NUM_ADC_INPUTS 16
|
else if (f >= 4)
|
||||||
// #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
|
||||||
|
else if (f >= 3)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
|
||||||
|
else if (f >= 2)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
|
||||||
|
else if (f == 1)
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
|
||||||
|
else
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t ADCee::usedpins = 0;
|
// TODO: rationalise the size of these... could really use sparse arrays etc.
|
||||||
uint8_t ADCee::highestPin = 0;
|
static HardwareTimer * pin_timer[100] = {0};
|
||||||
int * ADCee::analogvals = NULL;
|
static uint32_t channel_frequency[100] = {0};
|
||||||
uint32_t * analogchans = NULL;
|
static uint32_t pin_channel[100] = {0};
|
||||||
bool adc1configured = false;
|
|
||||||
|
|
||||||
int16_t ADCee::ADCmax() {
|
// Using the HardwareTimer library API included in stm32duino core to handle PWM duties
|
||||||
return 4095;
|
// TODO: in order to use the HA code above which Neil kindly wrote, we may have to do something more
|
||||||
|
// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones
|
||||||
|
// currently used for HA so they don't interfere with one another. For now we'll just make PWM
|
||||||
|
// work well... then work backwards to integrate with HA mode if we can.
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency)
|
||||||
|
{
|
||||||
|
if (pin_timer[pin] == NULL) {
|
||||||
|
// Automatically retrieve TIM instance and channel associated to pin
|
||||||
|
// This is used to be compatible with all STM32 series automatically.
|
||||||
|
TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM);
|
||||||
|
if (Instance == NULL) {
|
||||||
|
// We shouldn't get here (famous last words) as it ought to have been caught by brakeCanPWM()!
|
||||||
|
DIAG(F("DCCEXanalogWriteFrequency::Pin %d has no PWM function!"), pin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pin_channel[pin] = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(pin), PinMap_PWM));
|
||||||
|
|
||||||
|
// Instantiate HardwareTimer object. Thanks to 'new' instantiation,
|
||||||
|
// HardwareTimer is not destructed when setup function is finished.
|
||||||
|
pin_timer[pin] = new HardwareTimer(Instance);
|
||||||
|
// Configure and start PWM
|
||||||
|
// MyTim->setPWM(channel, pin, 5, 10, NULL, NULL); // No callback required, we can simplify the function call
|
||||||
|
if (pin_timer[pin] != NULL)
|
||||||
|
{
|
||||||
|
pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle
|
||||||
|
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d, frequency %d"), pin, pin_channel[pin], frequency);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Frequency change request
|
||||||
|
if (frequency != channel_frequency[pin])
|
||||||
|
{
|
||||||
|
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
|
||||||
|
pin_timer[pin]->setOverflow(frequency, HERTZ_FORMAT); // Just change the frequency if it's already running!
|
||||||
|
DIAG(F("DCCEXanalogWriteFrequency::setting frequency to %d"), frequency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel_frequency[pin] = frequency;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
|
||||||
|
if (invert)
|
||||||
|
value = 255-value;
|
||||||
|
// Calculate percentage duty cycle from value given
|
||||||
|
uint32_t duty_cycle = (value * 100 / 256) + 1;
|
||||||
|
if (pin_timer[pin] != NULL) {
|
||||||
|
// if (duty_cycle == 100)
|
||||||
|
// {
|
||||||
|
// pin_timer[pin]->pauseChannel(pin_channel[pin]);
|
||||||
|
// DIAG(F("DCCEXanalogWrite::Pausing timer channel on pin %d"), pin);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
|
||||||
|
// pin_timer[pin]->resumeChannel(pin_channel[pin]);
|
||||||
|
pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle
|
||||||
|
DIAG(F("DCCEXanalogWrite::Pin %d, value %d, duty cycle %d"), pin, value, duty_cycle);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DIAG(F("DCCEXanalogWrite::Pin %d is not configured for PWM!"), pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Now we can handle more ADCs, maybe this works!
|
||||||
|
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
|
||||||
|
|
||||||
|
uint32_t ADCee::usedpins = 0; // Max of 32 ADC input channels!
|
||||||
|
uint8_t ADCee::highestPin = 0; // Highest pin to scan
|
||||||
|
int * ADCee::analogvals = NULL; // Array of analog values last captured
|
||||||
|
uint32_t * ADCee::analogchans = NULL; // Array of channel numbers to be scanned
|
||||||
|
// bool adc1configured = false;
|
||||||
|
ADC_TypeDef * * ADCee::adcchans = NULL; // Array to capture which ADC is each input channel on
|
||||||
|
|
||||||
|
int16_t ADCee::ADCmax()
|
||||||
|
{
|
||||||
|
return 4095;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ADCee::init(uint8_t pin) {
|
int ADCee::init(uint8_t pin) {
|
||||||
|
@ -261,11 +394,34 @@ int ADCee::init(uint8_t pin) {
|
||||||
return -1024; // some silly value as error
|
return -1024; // some silly value as error
|
||||||
|
|
||||||
uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32)
|
uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32)
|
||||||
uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC channel (only valid for ADC1!)
|
uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC input channel
|
||||||
GPIO_TypeDef * gpioBase;
|
ADC_TypeDef *adc = (ADC_TypeDef *)pinmap_find_peripheral(stmpin, PinMap_ADC); // find which ADC this pin is on ADC1/2/3 etc.
|
||||||
|
int adcnum = 1;
|
||||||
|
// All variants have ADC1
|
||||||
|
if (adc == ADC1)
|
||||||
|
DIAG(F("ADCee::init(): found pin %d on ADC1"), pin);
|
||||||
|
// Checking for ADC2 and ADC3 being defined helps cater for more variants
|
||||||
|
#if defined(ADC2)
|
||||||
|
else if (adc == ADC2)
|
||||||
|
{
|
||||||
|
DIAG(F("ADCee::init(): found pin %d on ADC2"), pin);
|
||||||
|
adcnum = 2;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if defined(ADC3)
|
||||||
|
else if (adc == ADC3)
|
||||||
|
{
|
||||||
|
DIAG(F("ADCee::init(): found pin %d on ADC3"), pin);
|
||||||
|
adcnum = 3;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else DIAG(F("ADCee::init(): found pin %d on unknown ADC!"), pin);
|
||||||
|
|
||||||
// Port config - find which port we're on and power it up
|
// Port config - find which port we're on and power it up
|
||||||
switch(stmgpio) {
|
GPIO_TypeDef *gpioBase;
|
||||||
|
|
||||||
|
switch (stmgpio)
|
||||||
|
{
|
||||||
case 0x00:
|
case 0x00:
|
||||||
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA
|
||||||
gpioBase = GPIOA;
|
gpioBase = GPIOA;
|
||||||
|
@ -278,6 +434,32 @@ int ADCee::init(uint8_t pin) {
|
||||||
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC
|
||||||
gpioBase = GPIOC;
|
gpioBase = GPIOC;
|
||||||
break;
|
break;
|
||||||
|
case 0x03:
|
||||||
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; //Power up PORTD
|
||||||
|
gpioBase = GPIOD;
|
||||||
|
break;
|
||||||
|
case 0x04:
|
||||||
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; //Power up PORTE
|
||||||
|
gpioBase = GPIOE;
|
||||||
|
break;
|
||||||
|
#if defined(GPIOF)
|
||||||
|
case 0x05:
|
||||||
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; //Power up PORTF
|
||||||
|
gpioBase = GPIOF;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#if defined(GPIOG)
|
||||||
|
case 0x06:
|
||||||
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN; //Power up PORTG
|
||||||
|
gpioBase = GPIOG;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#if defined(GPIOH)
|
||||||
|
case 0x07:
|
||||||
|
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN; //Power up PORTH
|
||||||
|
gpioBase = GPIOH;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
return -1023; // some silly value as error
|
return -1023; // some silly value as error
|
||||||
}
|
}
|
||||||
|
@ -293,31 +475,33 @@ int ADCee::init(uint8_t pin) {
|
||||||
if (adcchan > 18)
|
if (adcchan > 18)
|
||||||
return -1022; // silly value as error
|
return -1022; // silly value as error
|
||||||
if (adcchan < 10)
|
if (adcchan < 10)
|
||||||
ADC1->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles
|
adc->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles
|
||||||
else
|
else
|
||||||
ADC1->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles
|
adc->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles
|
||||||
|
|
||||||
// Read the inital ADC value for this analog input
|
// Read the inital ADC value for this analog input
|
||||||
ADC1->SQR3 = adcchan; // 1st conversion in regular sequence
|
adc->SQR3 = adcchan; // 1st conversion in regular sequence
|
||||||
ADC1->CR2 |= (1 << 30); // Start 1st conversion SWSTART
|
adc->CR2 |= ADC_CR2_SWSTART; //(1 << 30); // Start 1st conversion SWSTART
|
||||||
while(!(ADC1->SR & (1 << 1))); // Wait until conversion is complete
|
while(!(adc->SR & (1 << 1))); // Wait until conversion is complete
|
||||||
value = ADC1->DR; // Read value from register
|
value = adc->DR; // Read value from register
|
||||||
|
|
||||||
uint8_t id = pin - PNUM_ANALOG_BASE;
|
uint8_t id = pin - PNUM_ANALOG_BASE;
|
||||||
if (id > 15) { // today we have not enough bits in the mask to support more
|
// if (id > 15) { // today we have not enough bits in the mask to support more
|
||||||
return -1021;
|
// return -1021;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (analogvals == NULL) { // allocate analogvals and analogchans if this is the first invocation of init.
|
if (analogvals == NULL) { // allocate analogvals, analogchans and adcchans if this is the first invocation of init
|
||||||
analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));
|
analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));
|
||||||
analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t));
|
analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t));
|
||||||
|
adcchans = (ADC_TypeDef **)calloc(NUM_ADC_INPUTS+1, sizeof(ADC_TypeDef));
|
||||||
}
|
}
|
||||||
analogvals[id] = value; // Store sampled value
|
analogvals[id] = value; // Store sampled value
|
||||||
analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin
|
analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin
|
||||||
usedpins |= (1 << id); // This pin is now ready
|
adcchans[id] = adc; // Keep track of which ADC this channel is on
|
||||||
|
usedpins |= (1 << id); // This pin is now ready
|
||||||
if (id > highestPin) highestPin = id; // Store our highest pin in use
|
if (id > highestPin) highestPin = id; // Store our highest pin in use
|
||||||
|
|
||||||
DIAG(F("ADCee::init(): value=%d, channel=%d, id=%d"), value, adcchan, id);
|
DIAG(F("ADCee::init(): value=%d, ADC%d: channel=%d, id=%d"), value, adcnum, adcchan, id);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -344,13 +528,16 @@ void ADCee::scan() {
|
||||||
static uint8_t id = 0; // id and mask are the same thing but it is faster to
|
static uint8_t id = 0; // id and mask are the same thing but it is faster to
|
||||||
static uint16_t mask = 1; // increment and shift instead to calculate mask from id
|
static uint16_t mask = 1; // increment and shift instead to calculate mask from id
|
||||||
static bool waiting = false;
|
static bool waiting = false;
|
||||||
|
static ADC_TypeDef *adc;
|
||||||
|
|
||||||
if (waiting) {
|
adc = adcchans[id];
|
||||||
|
if (waiting)
|
||||||
|
{
|
||||||
// look if we have a result
|
// look if we have a result
|
||||||
if (!(ADC1->SR & (1 << 1)))
|
if (!(adc->SR & (1 << 1)))
|
||||||
return; // no result, continue to wait
|
return; // no result, continue to wait
|
||||||
// found value
|
// found value
|
||||||
analogvals[id] = ADC1->DR;
|
analogvals[id] = adc->DR;
|
||||||
// advance at least one track
|
// advance at least one track
|
||||||
#ifdef DEBUG_ADC
|
#ifdef DEBUG_ADC
|
||||||
if (id == 1) TrackManager::track[1]->setBrake(0);
|
if (id == 1) TrackManager::track[1]->setBrake(0);
|
||||||
|
@ -369,9 +556,10 @@ void ADCee::scan() {
|
||||||
// look for a valid track to sample or until we are around
|
// look for a valid track to sample or until we are around
|
||||||
while (true) {
|
while (true) {
|
||||||
if (mask & usedpins) {
|
if (mask & usedpins) {
|
||||||
// start new ADC aquire on id
|
// start new ADC aquire on id
|
||||||
ADC1->SQR3 = analogchans[id]; //1st conversion in regular sequence
|
adc = adcchans[id];
|
||||||
ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART
|
adc->SQR3 = analogchans[id]; // 1st conversion in regular sequence
|
||||||
|
adc->CR2 |= (1 << 30); // Start 1st conversion SWSTART
|
||||||
#ifdef DEBUG_ADC
|
#ifdef DEBUG_ADC
|
||||||
if (id == 1) TrackManager::track[1]->setBrake(1);
|
if (id == 1) TrackManager::track[1]->setBrake(1);
|
||||||
#endif
|
#endif
|
||||||
|
@ -392,19 +580,83 @@ void ADCee::scan() {
|
||||||
void ADCee::begin() {
|
void ADCee::begin() {
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
//ADC1 config sequence
|
//ADC1 config sequence
|
||||||
// TODO: currently defaults to ADC1, may need more to handle other members of STM32F4xx family
|
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // Enable ADC1 clock
|
||||||
RCC->APB2ENR |= (1 << 8); //Enable ADC1 clock (Bit8)
|
|
||||||
// Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms
|
// Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms
|
||||||
ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8
|
ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8
|
||||||
ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
|
ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
|
||||||
ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
|
ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
|
||||||
ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
|
ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
|
||||||
|
// Disable the DMA controller for ADC1
|
||||||
|
ADC1->CR2 &= ~ADC_CR2_DMA;
|
||||||
ADC1->CR2 &= ~(1 << 1); //Single conversion
|
ADC1->CR2 &= ~(1 << 1); //Single conversion
|
||||||
ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
|
ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
|
||||||
ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
ADC1->CR2 |= (1 << 0); // Switch on ADC1
|
ADC1->CR2 |= (1 << 0); // Switch on ADC1
|
||||||
|
// Wait for ADC1 to become ready (calibration complete)
|
||||||
|
while (!(ADC1->CR2 & ADC_CR2_ADON)) {
|
||||||
|
}
|
||||||
|
#if defined(ADC2)
|
||||||
|
// Enable the ADC2 clock
|
||||||
|
RCC->APB2ENR |= RCC_APB2ENR_ADC2EN;
|
||||||
|
|
||||||
|
// Initialize ADC2
|
||||||
|
ADC2->CR1 = 0; // Disable all channels
|
||||||
|
ADC2->CR2 = 0; // Clear CR2 register
|
||||||
|
|
||||||
|
ADC2->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
|
||||||
|
ADC2->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
|
||||||
|
ADC2->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
|
||||||
|
ADC2->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3
|
||||||
|
ADC2->CR2 &= ~(1 << 1); //Single conversion
|
||||||
|
ADC2->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
|
||||||
|
ADC2->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
ADC2->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
ADC2->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
|
||||||
|
// Enable the ADC
|
||||||
|
ADC2->CR2 |= ADC_CR2_ADON;
|
||||||
|
|
||||||
|
// Wait for ADC2 to become ready (calibration complete)
|
||||||
|
while (!(ADC2->CR2 & ADC_CR2_ADON)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform ADC3 calibration (optional)
|
||||||
|
// ADC3->CR2 |= ADC_CR2_CAL;
|
||||||
|
// while (ADC3->CR2 & ADC_CR2_CAL) {
|
||||||
|
// }
|
||||||
|
#endif
|
||||||
|
#if defined(ADC3)
|
||||||
|
// Enable the ADC3 clock
|
||||||
|
RCC->APB2ENR |= RCC_APB2ENR_ADC3EN;
|
||||||
|
|
||||||
|
// Initialize ADC3
|
||||||
|
ADC3->CR1 = 0; // Disable all channels
|
||||||
|
ADC3->CR2 = 0; // Clear CR2 register
|
||||||
|
|
||||||
|
ADC3->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
|
||||||
|
ADC3->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
|
||||||
|
ADC3->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
|
||||||
|
ADC3->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3
|
||||||
|
ADC3->CR2 &= ~(1 << 1); //Single conversion
|
||||||
|
ADC3->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
|
||||||
|
ADC3->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
ADC3->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
ADC3->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
|
||||||
|
|
||||||
|
// Enable the ADC
|
||||||
|
ADC3->CR2 |= ADC_CR2_ADON;
|
||||||
|
|
||||||
|
// Wait for ADC3 to become ready (calibration complete)
|
||||||
|
while (!(ADC3->CR2 & ADC_CR2_ADON)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform ADC3 calibration (optional)
|
||||||
|
// ADC3->CR2 |= ADC_CR2_CAL;
|
||||||
|
// while (ADC3->CR2 & ADC_CR2_CAL) {
|
||||||
|
// }
|
||||||
|
#endif
|
||||||
interrupts();
|
interrupts();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -39,6 +39,15 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||||
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
|
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::startRailcomTimer(byte brakePin) {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
(void) brakePin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTimer::ackRailcomTimer() {
|
||||||
|
// TODO: for intended operation see DCCTimerAVR.cpp
|
||||||
|
}
|
||||||
|
|
||||||
bool DCCTimer::isPWMPin(byte pin) {
|
bool DCCTimer::isPWMPin(byte pin) {
|
||||||
//Teensy: digitalPinHasPWM, todo
|
//Teensy: digitalPinHasPWM, todo
|
||||||
(void) pin;
|
(void) pin;
|
||||||
|
@ -141,6 +150,11 @@ void DCCTimer::reset() {
|
||||||
SCB_AIRCR = 0x05FA0004;
|
SCB_AIRCR = 0x05FA0004;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
|
||||||
|
}
|
||||||
|
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
|
||||||
|
}
|
||||||
|
|
||||||
int16_t ADCee::ADCmax() {
|
int16_t ADCee::ADCmax() {
|
||||||
return 4095;
|
return 4095;
|
||||||
}
|
}
|
||||||
|
|
109
DCCWaveform.cpp
109
DCCWaveform.cpp
|
@ -106,6 +106,7 @@ void DCCWaveform::interruptHandler() {
|
||||||
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
||||||
isMainTrack = isMain;
|
isMainTrack = isMain;
|
||||||
packetPending = false;
|
packetPending = false;
|
||||||
|
reminderWindowOpen = false;
|
||||||
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
|
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
|
||||||
state = WAVE_START;
|
state = WAVE_START;
|
||||||
// The +1 below is to allow the preamble generator to create the stop bit
|
// The +1 below is to allow the preamble generator to create the stop bit
|
||||||
|
@ -115,7 +116,21 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
||||||
bits_sent = 0;
|
bits_sent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volatile bool DCCWaveform::railcomActive=false; // switched on by user
|
||||||
|
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
|
||||||
|
|
||||||
|
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
||||||
|
if (on) {
|
||||||
|
// TODO check possible
|
||||||
|
railcomActive=true;
|
||||||
|
railcomDebug=debug;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
railcomActive=false;
|
||||||
|
railcomDebug=false;
|
||||||
|
}
|
||||||
|
return railcomActive;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma GCC push_options
|
#pragma GCC push_options
|
||||||
#pragma GCC optimize ("-O3")
|
#pragma GCC optimize ("-O3")
|
||||||
|
@ -123,13 +138,19 @@ void DCCWaveform::interrupt2() {
|
||||||
// calculate the next bit to be sent:
|
// calculate the next bit to be sent:
|
||||||
// set state WAVE_MID_1 for a 1=bit
|
// set state WAVE_MID_1 for a 1=bit
|
||||||
// or WAVE_HIGH_0 for a 0 bit.
|
// or WAVE_HIGH_0 for a 0 bit.
|
||||||
|
|
||||||
if (remainingPreambles > 0 ) {
|
if (remainingPreambles > 0 ) {
|
||||||
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
|
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
|
||||||
remainingPreambles--;
|
remainingPreambles--;
|
||||||
|
|
||||||
|
// As we get to the end of the preambles, open the reminder window.
|
||||||
|
// This delays any reminder insertion until the last moment so
|
||||||
|
// that the reminder doesn't block a more urgent packet.
|
||||||
|
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
|
||||||
|
if (remainingPreambles==1) promotePendingPacket();
|
||||||
|
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
|
||||||
// Update free memory diagnostic as we don't have anything else to do this time.
|
// 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.
|
// Allow for checkAck and its called functions using 22 bytes more.
|
||||||
DCCTimer::updateMinimumFreeMemoryISR(22);
|
else DCCTimer::updateMinimumFreeMemoryISR(22);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,30 +169,15 @@ void DCCWaveform::interrupt2() {
|
||||||
if (bytes_sent >= transmitLength) {
|
if (bytes_sent >= transmitLength) {
|
||||||
// end of transmission buffer... repeat or switch to next message
|
// end of transmission buffer... repeat or switch to next message
|
||||||
bytes_sent = 0;
|
bytes_sent = 0;
|
||||||
|
// preamble for next packet will start...
|
||||||
remainingPreambles = requiredPreambles;
|
remainingPreambles = requiredPreambles;
|
||||||
|
|
||||||
if (transmitRepeats > 0) {
|
// set the railcom coundown to trigger half way
|
||||||
transmitRepeats--;
|
// through the first preamble bit.
|
||||||
|
// Note.. we are still sending the last packet bit
|
||||||
|
// and we then have to allow for the packet end bit
|
||||||
|
if (isMainTrack && railcomActive) DCCTimer::startRailcomTimer(9);
|
||||||
}
|
}
|
||||||
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));
|
|
||||||
|
|
||||||
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!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma GCC pop_options
|
#pragma GCC pop_options
|
||||||
|
@ -193,8 +199,43 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
|
||||||
packetPending = true;
|
packetPending = true;
|
||||||
clearResets();
|
clearResets();
|
||||||
}
|
}
|
||||||
bool DCCWaveform::getPacketPending() {
|
|
||||||
return packetPending;
|
bool DCCWaveform::isReminderWindowOpen() {
|
||||||
|
return reminderWindowOpen && ! packetPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCWaveform::promotePendingPacket() {
|
||||||
|
// fill the transmission packet from the pending packet
|
||||||
|
|
||||||
|
// Just keep going if repeating
|
||||||
|
if (transmitRepeats > 0) {
|
||||||
|
transmitRepeats--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packetPending) {
|
||||||
|
// Copy pending packet to transmit packet
|
||||||
|
// a fixed length memcpy is faster than a variable length loop for these small lengths
|
||||||
|
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
|
||||||
|
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
|
||||||
|
|
||||||
|
transmitLength = pendingLength;
|
||||||
|
transmitRepeats = pendingRepeats;
|
||||||
|
packetPending = false;
|
||||||
|
clearResets();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to do, just send idles or resets
|
||||||
|
// Fortunately reset and idle packets are the same length
|
||||||
|
// Note: If railcomDebug is on, then we send resets to the main
|
||||||
|
// track instead of idles. This means that all data will be zeros
|
||||||
|
// and only the porersets will be ones, making it much
|
||||||
|
// easier to read on a logic analyser.
|
||||||
|
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
|
||||||
|
transmitLength = sizeof(idlePacket);
|
||||||
|
transmitRepeats = 0;
|
||||||
|
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -253,7 +294,7 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
|
||||||
// The resets will be zero not only now but as well repeats packets into the future
|
// The resets will be zero not only now but as well repeats packets into the future
|
||||||
clearResets(repeats+1);
|
clearResets(repeats+1);
|
||||||
{
|
{
|
||||||
int ret;
|
int ret = 0;
|
||||||
do {
|
do {
|
||||||
if(isMainTrack) {
|
if(isMainTrack) {
|
||||||
if (rmtMainChannel != NULL)
|
if (rmtMainChannel != NULL)
|
||||||
|
@ -266,18 +307,24 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DCCWaveform::getPacketPending() {
|
bool DCCWaveform::isReminderWindowOpen() {
|
||||||
if(isMainTrack) {
|
if(isMainTrack) {
|
||||||
if (rmtMainChannel == NULL)
|
if (rmtMainChannel == NULL)
|
||||||
return true;
|
return false;
|
||||||
return rmtMainChannel->busy();
|
return !rmtMainChannel->busy();
|
||||||
} else {
|
} else {
|
||||||
if (rmtProgChannel == NULL)
|
if (rmtProgChannel == NULL)
|
||||||
return true;
|
return false;
|
||||||
return rmtProgChannel->busy();
|
return !rmtProgChannel->busy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void IRAM_ATTR DCCWaveform::loop() {
|
void IRAM_ATTR DCCWaveform::loop() {
|
||||||
DCCACK::checkAck(progTrack.getResets());
|
DCCACK::checkAck(progTrack.getResets());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
||||||
|
// TODO... ESP32 railcom waveform
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* © 2021 M Steve Todd
|
* © 2021 M Steve Todd
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020-2021 Harald Barth
|
* © 2020-2024 Harald Barth
|
||||||
* © 2020-2021 Chris Harlow
|
* © 2020-2021 Chris Harlow
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
|
@ -33,14 +33,21 @@
|
||||||
|
|
||||||
|
|
||||||
// Number of preamble bits.
|
// Number of preamble bits.
|
||||||
const int PREAMBLE_BITS_MAIN = 16;
|
const byte PREAMBLE_BITS_MAIN = 16;
|
||||||
const int PREAMBLE_BITS_PROG = 22;
|
const byte PREAMBLE_BITS_PROG = 22;
|
||||||
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
|
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
|
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
|
||||||
// to the transform array.
|
// to the transform array.
|
||||||
enum WAVE_STATE : byte {WAVE_START=0,WAVE_MID_1=1,WAVE_HIGH_0=2,WAVE_MID_0=3,WAVE_LOW_0=4,WAVE_PENDING=5};
|
enum WAVE_STATE : byte {
|
||||||
|
WAVE_START=0, // wave going high at start of bit
|
||||||
|
WAVE_MID_1=1, // middle of 1 bit
|
||||||
|
WAVE_HIGH_0=2, // first part of 0 bit high
|
||||||
|
WAVE_MID_0=3, // middle of 0 bit
|
||||||
|
WAVE_LOW_0=4, // first part of 0 bit low
|
||||||
|
WAVE_PENDING=5 // next bit not yet known
|
||||||
|
};
|
||||||
|
|
||||||
// NOTE: static functions are used for the overall controller, then
|
// NOTE: static functions are used for the overall controller, then
|
||||||
// one instance is created for each track.
|
// one instance is created for each track.
|
||||||
|
@ -76,11 +83,15 @@ class DCCWaveform {
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
|
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
|
||||||
bool getPacketPending();
|
bool isReminderWindowOpen();
|
||||||
|
void promotePendingPacket();
|
||||||
|
static bool setRailcom(bool on, bool debug);
|
||||||
|
static bool isRailcom() {return railcomActive;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#ifndef ARDUINO_ARCH_ESP32
|
#ifndef ARDUINO_ARCH_ESP32
|
||||||
volatile bool packetPending;
|
volatile bool packetPending;
|
||||||
|
volatile bool reminderWindowOpen;
|
||||||
volatile byte sentResetsSincePacket;
|
volatile byte sentResetsSincePacket;
|
||||||
#else
|
#else
|
||||||
volatile uint32_t resetPacketBase;
|
volatile uint32_t resetPacketBase;
|
||||||
|
@ -101,6 +112,9 @@ class DCCWaveform {
|
||||||
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
|
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
|
||||||
byte pendingLength;
|
byte pendingLength;
|
||||||
byte pendingRepeats;
|
byte pendingRepeats;
|
||||||
|
static volatile bool railcomActive; // switched on by user
|
||||||
|
static volatile bool railcomDebug; // switched on by user
|
||||||
|
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
static RMTChannel *rmtMainChannel;
|
static RMTChannel *rmtMainChannel;
|
||||||
static RMTChannel *rmtProgChannel;
|
static RMTChannel *rmtProgChannel;
|
||||||
|
|
|
@ -37,7 +37,9 @@
|
||||||
class Display : public DisplayInterface {
|
class Display : public DisplayInterface {
|
||||||
public:
|
public:
|
||||||
Display(DisplayDevice *deviceDriver);
|
Display(DisplayDevice *deviceDriver);
|
||||||
|
#if !defined (MAX_CHARACTER_ROWS)
|
||||||
static const int MAX_CHARACTER_ROWS = 8;
|
static const int MAX_CHARACTER_ROWS = 8;
|
||||||
|
#endif
|
||||||
static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE;
|
static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE;
|
||||||
static const long DISPLAY_SCROLL_TIME = 3000; // 3 seconds
|
static const long DISPLAY_SCROLL_TIME = 3000; // 3 seconds
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,9 @@
|
||||||
xxx; \
|
xxx; \
|
||||||
t->refresh();}
|
t->refresh();}
|
||||||
#else
|
#else
|
||||||
#define DISPLAY_START(xxx) {}
|
#define DISPLAY_START(xxx) { \
|
||||||
|
xxx; \
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#endif // LCD_Implementation_h
|
#endif // LCD_Implementation_h
|
||||||
|
|
749
EXRAIL2.cpp
749
EXRAIL2.cpp
File diff suppressed because it is too large
Load Diff
117
EXRAIL2.h
117
EXRAIL2.h
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* © 2021 Neil McKechnie
|
* © 2021 Neil McKechnie
|
||||||
* © 2020-2022 Chris Harlow
|
* © 2020-2022 Chris Harlow
|
||||||
* © 2022 Colin Murdoch
|
* © 2022-2023 Colin Murdoch
|
||||||
* © 2023 Harald Barth
|
* © 2023 Harald Barth
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
#include "FSH.h"
|
#include "FSH.h"
|
||||||
#include "IODevice.h"
|
#include "IODevice.h"
|
||||||
#include "Turnouts.h"
|
#include "Turnouts.h"
|
||||||
|
#include "Turntables.h"
|
||||||
|
|
||||||
// The following are the operation codes (or instructions) for a kind of virtual machine.
|
// The following are the operation codes (or instructions) for a kind of virtual machine.
|
||||||
// Each instruction is normally 3 bytes long with an operation code followed by a parameter.
|
// Each instruction is normally 3 bytes long with an operation code followed by a parameter.
|
||||||
|
@ -32,16 +33,19 @@
|
||||||
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
|
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
|
||||||
// searching easier as a parameter can never be confused with an opcode.
|
// searching easier as a parameter can never be confused with an opcode.
|
||||||
//
|
//
|
||||||
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
|
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||||
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
|
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
|
||||||
OPCODE_RESERVE,OPCODE_FREE,
|
OPCODE_RESERVE,OPCODE_FREE,
|
||||||
OPCODE_AT,OPCODE_AFTER,OPCODE_AUTOSTART,
|
OPCODE_AT,OPCODE_AFTER,
|
||||||
|
OPCODE_AFTEROVERLOAD,OPCODE_AUTOSTART,
|
||||||
OPCODE_ATGTE,OPCODE_ATLT,
|
OPCODE_ATGTE,OPCODE_ATLT,
|
||||||
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,
|
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,
|
||||||
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
|
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
|
||||||
|
OPCODE_BLINK,
|
||||||
OPCODE_ENDIF,OPCODE_ELSE,
|
OPCODE_ENDIF,OPCODE_ELSE,
|
||||||
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
|
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
|
||||||
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
|
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
|
||||||
|
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,
|
||||||
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
|
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
|
||||||
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
|
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
|
||||||
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
|
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
|
||||||
|
@ -49,20 +53,27 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
|
||||||
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,
|
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,
|
||||||
#endif
|
#endif
|
||||||
OPCODE_POM,
|
OPCODE_POM,
|
||||||
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET,
|
OPCODE_START,OPCODE_SETLOCO,OPCODE_SETFREQ,OPCODE_SENDLOCO,OPCODE_FORGET,
|
||||||
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
|
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
|
||||||
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
|
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
|
||||||
OPCODE_PRINT,OPCODE_DCCACTIVATE,
|
OPCODE_PRINT,OPCODE_DCCACTIVATE,OPCODE_ASPECT,
|
||||||
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
|
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
|
||||||
OPCODE_ROSTER,OPCODE_KILLALL,
|
OPCODE_ROSTER,OPCODE_KILLALL,
|
||||||
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
|
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
|
||||||
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
|
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
|
||||||
OPCODE_SET_TRACK,
|
OPCODE_SET_TRACK,OPCODE_SET_POWER,
|
||||||
OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,
|
OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,
|
||||||
OPCODE_ONCHANGE,
|
OPCODE_ONCHANGE,
|
||||||
OPCODE_ONCLOCKTIME,
|
OPCODE_ONCLOCKTIME,
|
||||||
OPCODE_ONTIME,
|
OPCODE_ONTIME,
|
||||||
|
OPCODE_TTADDPOSITION,OPCODE_DCCTURNTABLE,OPCODE_EXTTTURNTABLE,
|
||||||
|
OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_WAITFORTT,
|
||||||
|
OPCODE_LCC,OPCODE_LCCX,OPCODE_ONLCC,
|
||||||
|
OPCODE_ONOVERLOAD,
|
||||||
|
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
|
||||||
|
OPCODE_ROUTE_DISABLED,
|
||||||
|
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
||||||
|
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
|
||||||
// OPcodes below this point are skip-nesting IF operations
|
// OPcodes below this point are skip-nesting IF operations
|
||||||
// placed here so that they may be skipped as a group
|
// placed here so that they may be skipped as a group
|
||||||
// see skipIfBlock()
|
// see skipIfBlock()
|
||||||
|
@ -74,7 +85,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
|
||||||
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
|
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
|
||||||
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
|
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
|
||||||
OPCODE_IFRE,
|
OPCODE_IFRE,
|
||||||
OPCODE_IFLOCO
|
OPCODE_IFLOCO,
|
||||||
|
OPCODE_IFTTPOSITION
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure thrunge_lcd is put last as there may be more than one display,
|
// Ensure thrunge_lcd is put last as there may be more than one display,
|
||||||
|
@ -84,11 +96,27 @@ enum thrunger: byte {
|
||||||
thrunge_serial,thrunge_parse,
|
thrunge_serial,thrunge_parse,
|
||||||
thrunge_serial1, thrunge_serial2, thrunge_serial3,
|
thrunge_serial1, thrunge_serial2, thrunge_serial3,
|
||||||
thrunge_serial4, thrunge_serial5, thrunge_serial6,
|
thrunge_serial4, thrunge_serial5, thrunge_serial6,
|
||||||
thrunge_lcn,
|
thrunge_lcn,thrunge_message,
|
||||||
thrunge_lcd, // Must be last!!
|
thrunge_lcd, // Must be last!!
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum BlinkState: byte {
|
||||||
|
not_blink_task,
|
||||||
|
blink_low, // blink task running with pin LOW
|
||||||
|
blink_high, // blink task running with pin high
|
||||||
|
at_timeout // ATTIMEOUT timed out flag
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flag bits for compile time features.
|
||||||
|
static const byte FEATURE_SIGNAL= 0x80;
|
||||||
|
static const byte FEATURE_LCC = 0x40;
|
||||||
|
static const byte FEATURE_ROSTER= 0x20;
|
||||||
|
static const byte FEATURE_ROUTESTATE= 0x10;
|
||||||
|
static const byte FEATURE_STASH = 0x08;
|
||||||
|
static const byte FEATURE_BLINK = 0x04;
|
||||||
|
static const byte FEATURE_SENSOR = 0x02;
|
||||||
|
|
||||||
|
|
||||||
// Flag bits for status of hardware and TPL
|
// Flag bits for status of hardware and TPL
|
||||||
static const byte SECTION_FLAG = 0x80;
|
static const byte SECTION_FLAG = 0x80;
|
||||||
|
@ -108,13 +136,20 @@ enum thrunger: byte {
|
||||||
class LookList {
|
class LookList {
|
||||||
public:
|
public:
|
||||||
LookList(int16_t size);
|
LookList(int16_t size);
|
||||||
|
void chain(LookList* chainTo);
|
||||||
void add(int16_t lookup, int16_t result);
|
void add(int16_t lookup, int16_t result);
|
||||||
int16_t find(int16_t value);
|
int16_t find(int16_t value); // finds result value
|
||||||
|
int16_t findPosition(int16_t value); // finds index
|
||||||
|
int16_t size();
|
||||||
|
void stream(Print * _stream);
|
||||||
|
void handleEvent(const FSH* reason,int16_t id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int16_t m_size;
|
int16_t m_size;
|
||||||
int16_t m_loaded;
|
int16_t m_loaded;
|
||||||
int16_t * m_lookupArray;
|
int16_t * m_lookupArray;
|
||||||
int16_t * m_resultArray;
|
int16_t * m_resultArray;
|
||||||
|
LookList* m_chain;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RMFT2 {
|
class RMFT2 {
|
||||||
|
@ -130,9 +165,13 @@ class LookList {
|
||||||
static void activateEvent(int16_t addr, bool active);
|
static void activateEvent(int16_t addr, bool active);
|
||||||
static void changeEvent(int16_t id, bool change);
|
static void changeEvent(int16_t id, bool change);
|
||||||
static void clockEvent(int16_t clocktime, bool change);
|
static void clockEvent(int16_t clocktime, bool change);
|
||||||
|
static void rotateEvent(int16_t id, bool change);
|
||||||
|
static void powerEvent(int16_t track, bool overload);
|
||||||
|
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||||
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
|
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
|
||||||
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
|
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
|
||||||
static const int16_t DCC_SIGNAL_FLAG=0x1000;
|
static const int16_t DCC_SIGNAL_FLAG=0x1000;
|
||||||
|
static const int16_t DCCX_SIGNAL_FLAG=0x3000;
|
||||||
static const int16_t SIGNAL_ID_MASK=0x0FFF;
|
static const int16_t SIGNAL_ID_MASK=0x0FFF;
|
||||||
// Throttle Info Access functions built by exrail macros
|
// Throttle Info Access functions built by exrail macros
|
||||||
static const byte rosterNameCount;
|
static const byte rosterNameCount;
|
||||||
|
@ -144,6 +183,11 @@ class LookList {
|
||||||
static const FSH * getTurnoutDescription(int16_t id);
|
static const FSH * getTurnoutDescription(int16_t id);
|
||||||
static const FSH * getRosterName(int16_t id);
|
static const FSH * getRosterName(int16_t id);
|
||||||
static const FSH * getRosterFunctions(int16_t id);
|
static const FSH * getRosterFunctions(int16_t id);
|
||||||
|
static const FSH * getTurntableDescription(int16_t id);
|
||||||
|
static const FSH * getTurntablePositionDescription(int16_t turntableId, uint8_t positionId);
|
||||||
|
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc);
|
||||||
|
static bool readSensor(uint16_t sensorId);
|
||||||
|
static bool isSignal(int16_t id,char rag);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||||
|
@ -153,18 +197,19 @@ private:
|
||||||
static bool getFlag(VPIN id,byte mask);
|
static bool getFlag(VPIN id,byte mask);
|
||||||
static int16_t progtrackLocoId;
|
static int16_t progtrackLocoId;
|
||||||
static void doSignal(int16_t id,char rag);
|
static void doSignal(int16_t id,char rag);
|
||||||
static bool isSignal(int16_t id,char rag);
|
|
||||||
static int16_t getSignalSlot(int16_t id);
|
static int16_t getSignalSlot(int16_t id);
|
||||||
static void setTurnoutHiddenState(Turnout * t);
|
static void setTurnoutHiddenState(Turnout * t);
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
static void setTurntableHiddenState(Turntable * tto);
|
||||||
|
#endif
|
||||||
static LookList* LookListLoader(OPCODE op1,
|
static LookList* LookListLoader(OPCODE op1,
|
||||||
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
|
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
|
||||||
static void handleEvent(const FSH* reason,LookList* handlers, int16_t id);
|
|
||||||
static uint16_t getOperand(int progCounter,byte n);
|
static uint16_t getOperand(int progCounter,byte n);
|
||||||
|
static void killBlinkOnVpin(VPIN pin);
|
||||||
static RMFT2 * loopTask;
|
static RMFT2 * loopTask;
|
||||||
static RMFT2 * pausingTask;
|
static RMFT2 * pausingTask;
|
||||||
void delayMe(long millisecs);
|
void delayMe(long millisecs);
|
||||||
void driveLoco(byte speedo);
|
void driveLoco(byte speedo);
|
||||||
bool readSensor(uint16_t sensorId);
|
|
||||||
bool skipIfBlock();
|
bool skipIfBlock();
|
||||||
bool readLoco();
|
bool readLoco();
|
||||||
void loop2();
|
void loop2();
|
||||||
|
@ -175,10 +220,11 @@ private:
|
||||||
uint16_t getOperand(byte n);
|
uint16_t getOperand(byte n);
|
||||||
|
|
||||||
static bool diag;
|
static bool diag;
|
||||||
static const HIGHFLASH byte RouteCode[];
|
static const HIGHFLASH3 byte RouteCode[];
|
||||||
static const HIGHFLASH int16_t SignalDefinitions[];
|
static const HIGHFLASH int16_t SignalDefinitions[];
|
||||||
static byte flags[MAX_FLAGS];
|
static byte flags[MAX_FLAGS];
|
||||||
static LookList * sequenceLookup;
|
static Print * LCCSerial;
|
||||||
|
static LookList * routeLookup;
|
||||||
static LookList * onThrowLookup;
|
static LookList * onThrowLookup;
|
||||||
static LookList * onCloseLookup;
|
static LookList * onCloseLookup;
|
||||||
static LookList * onActivateLookup;
|
static LookList * onActivateLookup;
|
||||||
|
@ -188,6 +234,20 @@ private:
|
||||||
static LookList * onGreenLookup;
|
static LookList * onGreenLookup;
|
||||||
static LookList * onChangeLookup;
|
static LookList * onChangeLookup;
|
||||||
static LookList * onClockLookup;
|
static LookList * onClockLookup;
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
static LookList * onRotateLookup;
|
||||||
|
#endif
|
||||||
|
static LookList * onOverloadLookup;
|
||||||
|
|
||||||
|
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 byte * routeStateArray;
|
||||||
|
static const FSH ** routeCaptionArray;
|
||||||
|
static int16_t * stashArray;
|
||||||
|
static int16_t maxStashId;
|
||||||
|
|
||||||
// Local variables - exist for each instance/task
|
// Local variables - exist for each instance/task
|
||||||
RMFT2 *next; // loop chain
|
RMFT2 *next; // loop chain
|
||||||
|
@ -197,10 +257,10 @@ private:
|
||||||
union {
|
union {
|
||||||
unsigned long waitAfter; // Used by OPCODE_AFTER
|
unsigned long waitAfter; // Used by OPCODE_AFTER
|
||||||
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
|
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
|
||||||
|
VPIN blinkPin; // Used by blink tasks
|
||||||
};
|
};
|
||||||
bool timeoutFlag;
|
|
||||||
byte taskId;
|
byte taskId;
|
||||||
|
BlinkState blinkState; // includes AT_TIMEOUT flag.
|
||||||
uint16_t loco;
|
uint16_t loco;
|
||||||
bool forward;
|
bool forward;
|
||||||
bool invert;
|
bool invert;
|
||||||
|
@ -209,4 +269,27 @@ private:
|
||||||
byte stackDepth;
|
byte stackDepth;
|
||||||
int callStack[MAX_STACK_DEPTH];
|
int callStack[MAX_STACK_DEPTH];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter)
|
||||||
|
#define SKIPOP progCounter+=3
|
||||||
|
|
||||||
|
// IO_I2CDFPlayer commands and values
|
||||||
|
enum : uint8_t{
|
||||||
|
DF_PLAY = 0x0F,
|
||||||
|
DF_VOL = 0x06,
|
||||||
|
DF_FOLDER = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is
|
||||||
|
DF_REPEATPLAY = 0x08,
|
||||||
|
DF_STOPPLAY = 0x16,
|
||||||
|
DF_EQ = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS
|
||||||
|
DF_RESET = 0x0C,
|
||||||
|
DF_DACON = 0x1A,
|
||||||
|
DF_SETAM = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer
|
||||||
|
DF_NORMAL = 0x00, // Equalizer parameters
|
||||||
|
DF_POP = 0x01,
|
||||||
|
DF_ROCK = 0x02,
|
||||||
|
DF_JAZZ = 0x03,
|
||||||
|
DF_CLASSIC = 0x04,
|
||||||
|
DF_BASS = 0x05,
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* © 2020-2022 Chris Harlow. All rights reserved.
|
* © 2020-2022 Chris Harlow. All rights reserved.
|
||||||
* © 2022 Colin Murdoch
|
* © 2022-2023 Colin Murdoch
|
||||||
* © 2023 Harald Barth
|
* © 2023 Harald Barth
|
||||||
*
|
*
|
||||||
* This file is part of CommandStation-EX
|
* This file is part of CommandStation-EX
|
||||||
|
@ -27,19 +27,27 @@
|
||||||
#undef ACTIVATE
|
#undef ACTIVATE
|
||||||
#undef ACTIVATEL
|
#undef ACTIVATEL
|
||||||
#undef AFTER
|
#undef AFTER
|
||||||
|
#undef AFTEROVERLOAD
|
||||||
#undef ALIAS
|
#undef ALIAS
|
||||||
#undef AMBER
|
#undef AMBER
|
||||||
#undef ANOUT
|
#undef ANOUT
|
||||||
|
#undef ASPECT
|
||||||
#undef AT
|
#undef AT
|
||||||
#undef ATGTE
|
#undef ATGTE
|
||||||
#undef ATLT
|
#undef ATLT
|
||||||
#undef ATTIMEOUT
|
#undef ATTIMEOUT
|
||||||
#undef AUTOMATION
|
#undef AUTOMATION
|
||||||
#undef AUTOSTART
|
#undef AUTOSTART
|
||||||
|
#undef BLINK
|
||||||
#undef BROADCAST
|
#undef BROADCAST
|
||||||
#undef CALL
|
#undef CALL
|
||||||
|
#undef CLEAR_STASH
|
||||||
|
#undef CLEAR_ALL_STASH
|
||||||
#undef CLOSE
|
#undef CLOSE
|
||||||
|
#undef CONFIGURE_SERVO
|
||||||
#undef DCC_SIGNAL
|
#undef DCC_SIGNAL
|
||||||
|
#undef DCCX_SIGNAL
|
||||||
|
#undef DCC_TURNTABLE
|
||||||
#undef DEACTIVATE
|
#undef DEACTIVATE
|
||||||
#undef DEACTIVATEL
|
#undef DEACTIVATEL
|
||||||
#undef DELAY
|
#undef DELAY
|
||||||
|
@ -53,15 +61,18 @@
|
||||||
#undef ENDTASK
|
#undef ENDTASK
|
||||||
#undef ESTOP
|
#undef ESTOP
|
||||||
#undef EXRAIL
|
#undef EXRAIL
|
||||||
|
#undef EXTT_TURNTABLE
|
||||||
#undef FADE
|
#undef FADE
|
||||||
#undef FOFF
|
#undef FOFF
|
||||||
#undef FOLLOW
|
#undef FOLLOW
|
||||||
#undef FON
|
#undef FON
|
||||||
#undef FORGET
|
#undef FORGET
|
||||||
|
#undef FTOGGLE
|
||||||
#undef FREE
|
#undef FREE
|
||||||
#undef FWD
|
#undef FWD
|
||||||
#undef GREEN
|
#undef GREEN
|
||||||
#undef HAL
|
#undef HAL
|
||||||
|
#undef HAL_IGNORE_DEFAULTS
|
||||||
#undef IF
|
#undef IF
|
||||||
#undef IFAMBER
|
#undef IFAMBER
|
||||||
#undef IFCLOSED
|
#undef IFCLOSED
|
||||||
|
@ -75,30 +86,41 @@
|
||||||
#undef IFRESERVE
|
#undef IFRESERVE
|
||||||
#undef IFTHROWN
|
#undef IFTHROWN
|
||||||
#undef IFTIMEOUT
|
#undef IFTIMEOUT
|
||||||
|
#undef IFTTPOSITION
|
||||||
#undef IFRE
|
#undef IFRE
|
||||||
#undef INVERT_DIRECTION
|
#undef INVERT_DIRECTION
|
||||||
|
#undef JMRI_SENSOR
|
||||||
#undef JOIN
|
#undef JOIN
|
||||||
#undef KILLALL
|
#undef KILLALL
|
||||||
#undef LATCH
|
#undef LATCH
|
||||||
#undef LCD
|
#undef LCD
|
||||||
#undef SCREEN
|
#undef SCREEN
|
||||||
|
#undef LCC
|
||||||
|
#undef LCCX
|
||||||
#undef LCN
|
#undef LCN
|
||||||
#undef MOVETT
|
#undef MOVETT
|
||||||
|
#undef MESSAGE
|
||||||
#undef ONACTIVATE
|
#undef ONACTIVATE
|
||||||
#undef ONACTIVATEL
|
#undef ONACTIVATEL
|
||||||
#undef ONAMBER
|
#undef ONAMBER
|
||||||
#undef ONDEACTIVATE
|
#undef ONDEACTIVATE
|
||||||
#undef ONDEACTIVATEL
|
#undef ONDEACTIVATEL
|
||||||
#undef ONCLOSE
|
#undef ONCLOSE
|
||||||
|
#undef ONLCC
|
||||||
#undef ONTIME
|
#undef ONTIME
|
||||||
#undef ONCLOCKTIME
|
#undef ONCLOCKTIME
|
||||||
#undef ONCLOCKMINS
|
#undef ONCLOCKMINS
|
||||||
|
#undef ONOVERLOAD
|
||||||
#undef ONGREEN
|
#undef ONGREEN
|
||||||
#undef ONRED
|
#undef ONRED
|
||||||
|
#undef ONROTATE
|
||||||
|
#undef ONBUTTON
|
||||||
|
#undef ONSENSOR
|
||||||
#undef ONTHROW
|
#undef ONTHROW
|
||||||
#undef ONCHANGE
|
#undef ONCHANGE
|
||||||
#undef PARSE
|
#undef PARSE
|
||||||
#undef PAUSE
|
#undef PAUSE
|
||||||
|
#undef PICKUP_STASH
|
||||||
#undef PIN_TURNOUT
|
#undef PIN_TURNOUT
|
||||||
#undef PRINT
|
#undef PRINT
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
|
@ -114,7 +136,14 @@
|
||||||
#undef RETURN
|
#undef RETURN
|
||||||
#undef REV
|
#undef REV
|
||||||
#undef ROSTER
|
#undef ROSTER
|
||||||
|
#undef ROTATE
|
||||||
|
#undef ROTATE_DCC
|
||||||
#undef ROUTE
|
#undef ROUTE
|
||||||
|
#undef ROUTE_ACTIVE
|
||||||
|
#undef ROUTE_INACTIVE
|
||||||
|
#undef ROUTE_HIDDEN
|
||||||
|
#undef ROUTE_DISABLED
|
||||||
|
#undef ROUTE_CAPTION
|
||||||
#undef SENDLOCO
|
#undef SENDLOCO
|
||||||
#undef SEQUENCE
|
#undef SEQUENCE
|
||||||
#undef SERIAL
|
#undef SERIAL
|
||||||
|
@ -130,13 +159,20 @@
|
||||||
#undef SERVO_SIGNAL
|
#undef SERVO_SIGNAL
|
||||||
#undef SET
|
#undef SET
|
||||||
#undef SET_TRACK
|
#undef SET_TRACK
|
||||||
|
#undef SET_POWER
|
||||||
#undef SETLOCO
|
#undef SETLOCO
|
||||||
|
#undef SETFREQ
|
||||||
#undef SIGNAL
|
#undef SIGNAL
|
||||||
#undef SIGNALH
|
#undef SIGNALH
|
||||||
#undef SPEED
|
#undef SPEED
|
||||||
#undef START
|
#undef START
|
||||||
|
#undef STASH
|
||||||
|
#undef STEALTH
|
||||||
|
#undef STEALTH_GLOBAL
|
||||||
#undef STOP
|
#undef STOP
|
||||||
#undef THROW
|
#undef THROW
|
||||||
|
#undef TOGGLE_TURNOUT
|
||||||
|
#undef TT_ADDPOSITION
|
||||||
#undef TURNOUT
|
#undef TURNOUT
|
||||||
#undef TURNOUTL
|
#undef TURNOUTL
|
||||||
#undef UNJOIN
|
#undef UNJOIN
|
||||||
|
@ -144,27 +180,39 @@
|
||||||
#undef VIRTUAL_SIGNAL
|
#undef VIRTUAL_SIGNAL
|
||||||
#undef VIRTUAL_TURNOUT
|
#undef VIRTUAL_TURNOUT
|
||||||
#undef WAITFOR
|
#undef WAITFOR
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#undef WAITFORTT
|
||||||
|
#endif
|
||||||
#undef WITHROTTLE
|
#undef WITHROTTLE
|
||||||
#undef XFOFF
|
#undef XFOFF
|
||||||
#undef XFON
|
#undef XFON
|
||||||
|
#undef XFTOGGLE
|
||||||
|
|
||||||
#ifndef RMFT2_UNDEF_ONLY
|
#ifndef RMFT2_UNDEF_ONLY
|
||||||
#define ACTIVATE(addr,subaddr)
|
#define ACTIVATE(addr,subaddr)
|
||||||
#define ACTIVATEL(addr)
|
#define ACTIVATEL(addr)
|
||||||
#define AFTER(sensor_id)
|
#define AFTER(sensor_id)
|
||||||
|
#define AFTEROVERLOAD(track_id)
|
||||||
#define ALIAS(name,value...)
|
#define ALIAS(name,value...)
|
||||||
#define AMBER(signal_id)
|
#define AMBER(signal_id)
|
||||||
#define ANOUT(vpin,value,param1,param2)
|
#define ANOUT(vpin,value,param1,param2)
|
||||||
#define AT(sensor_id)
|
#define AT(sensor_id)
|
||||||
|
#define ASPECT(address,value)
|
||||||
#define ATGTE(sensor_id,value)
|
#define ATGTE(sensor_id,value)
|
||||||
#define ATLT(sensor_id,value)
|
#define ATLT(sensor_id,value)
|
||||||
#define ATTIMEOUT(sensor_id,timeout_ms)
|
#define ATTIMEOUT(sensor_id,timeout_ms)
|
||||||
#define AUTOMATION(id,description)
|
#define AUTOMATION(id,description)
|
||||||
#define AUTOSTART
|
#define AUTOSTART
|
||||||
|
#define BLINK(vpin,onDuty,offDuty)
|
||||||
#define BROADCAST(msg)
|
#define BROADCAST(msg)
|
||||||
#define CALL(route)
|
#define CALL(route)
|
||||||
|
#define CLEAR_STASH(id)
|
||||||
|
#define CLEAR_ALL_STASH(id)
|
||||||
#define CLOSE(id)
|
#define CLOSE(id)
|
||||||
|
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||||
#define DCC_SIGNAL(id,add,subaddr)
|
#define DCC_SIGNAL(id,add,subaddr)
|
||||||
|
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
|
||||||
|
#define DCC_TURNTABLE(id,home,description)
|
||||||
#define DEACTIVATE(addr,subaddr)
|
#define DEACTIVATE(addr,subaddr)
|
||||||
#define DEACTIVATEL(addr)
|
#define DEACTIVATEL(addr)
|
||||||
#define DELAY(mindelay)
|
#define DELAY(mindelay)
|
||||||
|
@ -178,15 +226,18 @@
|
||||||
#define ENDTASK
|
#define ENDTASK
|
||||||
#define ESTOP
|
#define ESTOP
|
||||||
#define EXRAIL
|
#define EXRAIL
|
||||||
|
#define EXTT_TURNTABLE(id,vpin,home,description)
|
||||||
#define FADE(pin,value,ms)
|
#define FADE(pin,value,ms)
|
||||||
#define FOFF(func)
|
#define FOFF(func)
|
||||||
#define FOLLOW(route)
|
#define FOLLOW(route)
|
||||||
#define FON(func)
|
#define FON(func)
|
||||||
#define FORGET
|
#define FORGET
|
||||||
#define FREE(blockid)
|
#define FREE(blockid)
|
||||||
|
#define FTOGGLE(func)
|
||||||
#define FWD(speed)
|
#define FWD(speed)
|
||||||
#define GREEN(signal_id)
|
#define GREEN(signal_id)
|
||||||
#define HAL(haltype,params...)
|
#define HAL(haltype,params...)
|
||||||
|
#define HAL_IGNORE_DEFAULTS
|
||||||
#define IF(sensor_id)
|
#define IF(sensor_id)
|
||||||
#define IFAMBER(signal_id)
|
#define IFAMBER(signal_id)
|
||||||
#define IFCLOSED(turnout_id)
|
#define IFCLOSED(turnout_id)
|
||||||
|
@ -200,14 +251,19 @@
|
||||||
#define IFTHROWN(turnout_id)
|
#define IFTHROWN(turnout_id)
|
||||||
#define IFRESERVE(block)
|
#define IFRESERVE(block)
|
||||||
#define IFTIMEOUT
|
#define IFTIMEOUT
|
||||||
|
#define IFTTPOSITION(turntable_id,position)
|
||||||
#define IFRE(sensor_id,value)
|
#define IFRE(sensor_id,value)
|
||||||
#define INVERT_DIRECTION
|
#define INVERT_DIRECTION
|
||||||
|
#define JMRI_SENSOR(vpin,count...)
|
||||||
#define JOIN
|
#define JOIN
|
||||||
#define KILLALL
|
#define KILLALL
|
||||||
#define LATCH(sensor_id)
|
#define LATCH(sensor_id)
|
||||||
|
#define LCC(eventid)
|
||||||
|
#define LCCX(senderid,eventid)
|
||||||
#define LCD(row,msg)
|
#define LCD(row,msg)
|
||||||
#define SCREEN(display,row,msg)
|
#define SCREEN(display,row,msg)
|
||||||
#define LCN(msg)
|
#define LCN(msg)
|
||||||
|
#define MESSAGE(msg)
|
||||||
#define MOVETT(id,steps,activity)
|
#define MOVETT(id,steps,activity)
|
||||||
#define ONACTIVATE(addr,subaddr)
|
#define ONACTIVATE(addr,subaddr)
|
||||||
#define ONACTIVATEL(linear)
|
#define ONACTIVATEL(linear)
|
||||||
|
@ -215,17 +271,23 @@
|
||||||
#define ONTIME(value)
|
#define ONTIME(value)
|
||||||
#define ONCLOCKTIME(hours,mins)
|
#define ONCLOCKTIME(hours,mins)
|
||||||
#define ONCLOCKMINS(mins)
|
#define ONCLOCKMINS(mins)
|
||||||
|
#define ONOVERLOAD(track_id)
|
||||||
#define ONDEACTIVATE(addr,subaddr)
|
#define ONDEACTIVATE(addr,subaddr)
|
||||||
#define ONDEACTIVATEL(linear)
|
#define ONDEACTIVATEL(linear)
|
||||||
#define ONCLOSE(turnout_id)
|
#define ONCLOSE(turnout_id)
|
||||||
|
#define ONLCC(sender,event)
|
||||||
#define ONGREEN(signal_id)
|
#define ONGREEN(signal_id)
|
||||||
#define ONRED(signal_id)
|
#define ONRED(signal_id)
|
||||||
|
#define ONROTATE(turntable_id)
|
||||||
#define ONTHROW(turnout_id)
|
#define ONTHROW(turnout_id)
|
||||||
#define ONCHANGE(sensor_id)
|
#define ONCHANGE(sensor_id)
|
||||||
|
#define ONSENSOR(sensor_id)
|
||||||
|
#define ONBUTTON(sensor_id)
|
||||||
#define PAUSE
|
#define PAUSE
|
||||||
#define PIN_TURNOUT(id,pin,description...)
|
#define PIN_TURNOUT(id,pin,description...)
|
||||||
#define PRINT(msg)
|
#define PRINT(msg)
|
||||||
#define PARSE(msg)
|
#define PARSE(msg)
|
||||||
|
#define PICKUP_STASH(id)
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
#define POM(cv,value)
|
#define POM(cv,value)
|
||||||
#endif
|
#endif
|
||||||
|
@ -238,8 +300,15 @@
|
||||||
#define RESUME
|
#define RESUME
|
||||||
#define RETURN
|
#define RETURN
|
||||||
#define REV(speed)
|
#define REV(speed)
|
||||||
#define ROUTE(id,description)
|
#define ROTATE(turntable_id,position,activity)
|
||||||
|
#define ROTATE_DCC(turntable_id,position)
|
||||||
#define ROSTER(cab,name,funcmap...)
|
#define ROSTER(cab,name,funcmap...)
|
||||||
|
#define ROUTE(id,description)
|
||||||
|
#define ROUTE_ACTIVE(id)
|
||||||
|
#define ROUTE_INACTIVE(id)
|
||||||
|
#define ROUTE_HIDDEN(id)
|
||||||
|
#define ROUTE_DISABLED(id)
|
||||||
|
#define ROUTE_CAPTION(id,caption)
|
||||||
#define SENDLOCO(cab,route)
|
#define SENDLOCO(cab,route)
|
||||||
#define SEQUENCE(id)
|
#define SEQUENCE(id)
|
||||||
#define SERIAL(msg)
|
#define SERIAL(msg)
|
||||||
|
@ -255,13 +324,20 @@
|
||||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
|
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
|
||||||
#define SET(pin)
|
#define SET(pin)
|
||||||
#define SET_TRACK(track,mode)
|
#define SET_TRACK(track,mode)
|
||||||
|
#define SET_POWER(track,onoff)
|
||||||
#define SETLOCO(loco)
|
#define SETLOCO(loco)
|
||||||
|
#define SETFREQ(loco,freq)
|
||||||
#define SIGNAL(redpin,amberpin,greenpin)
|
#define SIGNAL(redpin,amberpin,greenpin)
|
||||||
#define SIGNALH(redpin,amberpin,greenpin)
|
#define SIGNALH(redpin,amberpin,greenpin)
|
||||||
#define SPEED(speed)
|
#define SPEED(speed)
|
||||||
#define START(route)
|
#define START(route)
|
||||||
|
#define STASH(id)
|
||||||
|
#define STEALTH(code...)
|
||||||
|
#define STEALTH_GLOBAL(code...)
|
||||||
#define STOP
|
#define STOP
|
||||||
#define THROW(id)
|
#define THROW(id)
|
||||||
|
#define TOGGLE_TURNOUT(id)
|
||||||
|
#define TT_ADDPOSITION(turntable_id,position,value,angle,description...)
|
||||||
#define TURNOUT(id,addr,subaddr,description...)
|
#define TURNOUT(id,addr,subaddr,description...)
|
||||||
#define TURNOUTL(id,addr,description...)
|
#define TURNOUTL(id,addr,description...)
|
||||||
#define UNJOIN
|
#define UNJOIN
|
||||||
|
@ -269,7 +345,12 @@
|
||||||
#define VIRTUAL_SIGNAL(id)
|
#define VIRTUAL_SIGNAL(id)
|
||||||
#define VIRTUAL_TURNOUT(id,description...)
|
#define VIRTUAL_TURNOUT(id,description...)
|
||||||
#define WAITFOR(pin)
|
#define WAITFOR(pin)
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define WAITFORTT(turntable_id)
|
||||||
|
#endif
|
||||||
#define WITHROTTLE(msg)
|
#define WITHROTTLE(msg)
|
||||||
#define XFOFF(cab,func)
|
#define XFOFF(cab,func)
|
||||||
#define XFON(cab,func)
|
#define XFON(cab,func)
|
||||||
|
#define XFTOGGLE(cab,func)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
328
EXRAIL2Parser.cpp
Normal file
328
EXRAIL2Parser.cpp
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
/*
|
||||||
|
* © 2021 Neil McKechnie
|
||||||
|
* © 2021-2023 Harald Barth
|
||||||
|
* © 2020-2023 Chris Harlow
|
||||||
|
* © 2022-2023 Colin Murdoch
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// THIS file is an extension of the RMFT2 class
|
||||||
|
// normally found in EXRAIL2.cpp
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "defines.h"
|
||||||
|
#include "EXRAIL2.h"
|
||||||
|
#include "DCC.h"
|
||||||
|
#include "KeywordHasher.h"
|
||||||
|
|
||||||
|
// This filter intercepts <> commands to do the following:
|
||||||
|
// - Implement RMFT specific commands/diagnostics
|
||||||
|
// - Reject/modify JMRI commands that would interfere with RMFT processing
|
||||||
|
|
||||||
|
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
|
||||||
|
(void)stream; // avoid compiler warning if we don't access this parameter
|
||||||
|
|
||||||
|
switch(opcode) {
|
||||||
|
|
||||||
|
case 'D':
|
||||||
|
if (p[0]=="EXRAIL"_hk) { // <D EXRAIL ON/OFF>
|
||||||
|
diag = paramCount==2 && (p[1]=="ON"_hk || p[1]==1);
|
||||||
|
opcode=0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '/': // New EXRAIL command
|
||||||
|
if (parseSlash(stream,paramCount,p)) opcode=0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'A': // <A address aspect>
|
||||||
|
if (paramCount!=2) break;
|
||||||
|
// Ask exrail if this is just changing the aspect on a
|
||||||
|
// predefined DCCX_SIGNAL. Because this will handle all
|
||||||
|
// the IFRED and ONRED type issues at the same time.
|
||||||
|
if (signalAspectEvent(p[0],p[1])) opcode=0; // all done
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'L':
|
||||||
|
// This entire code block is compiled out if LLC macros not used
|
||||||
|
if (!(compileFeatures & FEATURE_LCC)) return;
|
||||||
|
|
||||||
|
if (paramCount==0) { //<L> LCC adapter introducing self
|
||||||
|
LCCSerial=stream; // now we know where to send events we raise
|
||||||
|
|
||||||
|
// loop through all possible sent events
|
||||||
|
for (int progCounter=0;; SKIPOP) {
|
||||||
|
byte opcode=GET_OPCODE;
|
||||||
|
if (opcode==OPCODE_ENDEXRAIL) break;
|
||||||
|
if (opcode==OPCODE_LCC) StringFormatter::send(stream,F("<LS x%h>\n"),getOperand(progCounter,0));
|
||||||
|
if (opcode==OPCODE_LCCX) { // long form LCC
|
||||||
|
StringFormatter::send(stream,F("<LS x%h%h%h%h>\n"),
|
||||||
|
getOperand(progCounter,1),
|
||||||
|
getOperand(progCounter,2),
|
||||||
|
getOperand(progCounter,3),
|
||||||
|
getOperand(progCounter,0)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// we stream the hex events we wish to listen to
|
||||||
|
// and at the same time build the event index looku.
|
||||||
|
|
||||||
|
|
||||||
|
int eventIndex=0;
|
||||||
|
for (int progCounter=0;; SKIPOP) {
|
||||||
|
byte opcode=GET_OPCODE;
|
||||||
|
if (opcode==OPCODE_ENDEXRAIL) break;
|
||||||
|
if (opcode==OPCODE_ONLCC) {
|
||||||
|
onLCCLookup[eventIndex]=progCounter; // TODO skip...
|
||||||
|
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
|
||||||
|
eventIndex,
|
||||||
|
getOperand(progCounter,1),
|
||||||
|
getOperand(progCounter,2),
|
||||||
|
getOperand(progCounter,3),
|
||||||
|
getOperand(progCounter,0)
|
||||||
|
);
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringFormatter::send(stream,F("<LR>\n")); // Ready to rumble
|
||||||
|
opcode=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (paramCount==1) { // <L eventid> LCC event arrived from adapter
|
||||||
|
int16_t eventid=p[0];
|
||||||
|
bool reject = eventid<0 || eventid>=countLCCLookup;
|
||||||
|
if (!reject) {
|
||||||
|
startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
|
||||||
|
opcode=0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'J': // throttle info commands
|
||||||
|
if (paramCount<1) return;
|
||||||
|
switch(p[0]) {
|
||||||
|
case "A"_hk: // <JA> returns automations/routes
|
||||||
|
if (paramCount==1) {// <JA>
|
||||||
|
StringFormatter::send(stream, F("<jA"));
|
||||||
|
routeLookup->stream(stream);
|
||||||
|
StringFormatter::send(stream, F(">\n"));
|
||||||
|
opcode=0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (paramCount==2) { // <JA id>
|
||||||
|
int16_t id=p[1];
|
||||||
|
StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"),
|
||||||
|
id, getRouteType(id), getRouteDescription(id));
|
||||||
|
|
||||||
|
if (compileFeatures & FEATURE_ROUTESTATE) {
|
||||||
|
// Send any non-default button states or captions
|
||||||
|
int16_t statePos=routeLookup->findPosition(id);
|
||||||
|
if (statePos>=0) {
|
||||||
|
if (routeStateArray[statePos])
|
||||||
|
StringFormatter::send(stream,F("<jB %d %d>\n"), id, routeStateArray[statePos]);
|
||||||
|
if (routeCaptionArray[statePos])
|
||||||
|
StringFormatter::send(stream,F("<jB %d \"%S\">\n"), id,routeCaptionArray[statePos]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opcode=0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "M"_hk:
|
||||||
|
// NOTE: we only need to handle valid calls here because
|
||||||
|
// DCCEXParser has to have code to handle the <J<> cases where
|
||||||
|
// exrail isnt involved anyway.
|
||||||
|
// This entire code block is compiled out if STASH macros not used
|
||||||
|
if (!(compileFeatures & FEATURE_STASH)) return;
|
||||||
|
if (paramCount==1) { // <JM>
|
||||||
|
StringFormatter::send(stream,F("<jM %d>\n"),maxStashId);
|
||||||
|
opcode=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (paramCount==2) { // <JM id>
|
||||||
|
if (p[1]<=0 || p[1]>maxStashId) break;
|
||||||
|
StringFormatter::send(stream,F("<jM %d %d>\n"),
|
||||||
|
p[1],stashArray[p[1]]);
|
||||||
|
opcode=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (paramCount==3) { // <JM id cab>
|
||||||
|
if (p[1]<=0 || p[1]>maxStashId) break;
|
||||||
|
stashArray[p[1]]=p[2];
|
||||||
|
opcode=0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: // other commands pass through
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||||
|
|
||||||
|
if (paramCount==0) { // STATUS
|
||||||
|
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
|
||||||
|
RMFT2 * task=loopTask;
|
||||||
|
while(task) {
|
||||||
|
if ((compileFeatures & FEATURE_BLINK)
|
||||||
|
&& (task->blinkState==blink_high || task->blinkState==blink_low)) {
|
||||||
|
StringFormatter::send(stream,F("\nID=%d,PC=%d,BLINK=%d"),
|
||||||
|
(int)(task->taskId),task->progCounter,task->blinkPin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
|
||||||
|
(int)(task->taskId),task->progCounter,task->loco,
|
||||||
|
task->invert?'I':' ',
|
||||||
|
task->speedo,
|
||||||
|
task->forward?'F':'R'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
task=task->next;
|
||||||
|
if (task==loopTask) break;
|
||||||
|
}
|
||||||
|
// Now stream the flags
|
||||||
|
for (int id=0;id<MAX_FLAGS; id++) {
|
||||||
|
byte flag=flags[id];
|
||||||
|
if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above
|
||||||
|
StringFormatter::send(stream,F("\nflags[%d] "),id);
|
||||||
|
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
|
||||||
|
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compileFeatures & FEATURE_SIGNAL) {
|
||||||
|
// do the signals
|
||||||
|
// flags[n] represents the state of the nth signal in the table
|
||||||
|
for (int sigslot=0;;sigslot++) {
|
||||||
|
int16_t sighandle=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
|
||||||
|
if (sighandle==0) break; // end of signal list
|
||||||
|
VPIN sigid = sighandle & SIGNAL_ID_MASK;
|
||||||
|
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this id
|
||||||
|
StringFormatter::send(stream,F("\n%S[%d]"),
|
||||||
|
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
|
||||||
|
sigid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compileFeatures & FEATURE_STASH) {
|
||||||
|
for (int i=1;i<=maxStashId;i++) {
|
||||||
|
if (stashArray[i])
|
||||||
|
StringFormatter::send(stream,F("\nSTASH[%d] Loco=%d"),
|
||||||
|
i, stashArray[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringFormatter::send(stream,F(" *>\n"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (p[0]) {
|
||||||
|
case "PAUSE"_hk: // </ PAUSE>
|
||||||
|
if (paramCount!=1) return false;
|
||||||
|
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||||
|
pausingTask=(RMFT2 *)1; // Impossible task address
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "RESUME"_hk: // </ RESUME>
|
||||||
|
if (paramCount!=1) return false;
|
||||||
|
pausingTask=NULL;
|
||||||
|
{
|
||||||
|
RMFT2 * task=loopTask;
|
||||||
|
while(task) {
|
||||||
|
if (task->loco) task->driveLoco(task->speedo);
|
||||||
|
task=task->next;
|
||||||
|
if (task==loopTask) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
|
||||||
|
case "START"_hk: // </ START [cab] route >
|
||||||
|
if (paramCount<2 || paramCount>3) return false;
|
||||||
|
{
|
||||||
|
int route=(paramCount==2) ? p[1] : p[2];
|
||||||
|
uint16_t cab=(paramCount==2)? 0 : p[1];
|
||||||
|
int pc=routeLookup->find(route);
|
||||||
|
if (pc<0) return false;
|
||||||
|
RMFT2* task=new RMFT2(pc);
|
||||||
|
task->loco=cab;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check KILL ALL here, otherwise the next validation confuses ALL with a flag
|
||||||
|
if (p[0]=="KILL"_hk && p[1]=="ALL"_hk) {
|
||||||
|
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// all other / commands take 1 parameter
|
||||||
|
if (paramCount!=2 ) return false;
|
||||||
|
|
||||||
|
switch (p[0]) {
|
||||||
|
case "KILL"_hk: // Kill taskid|ALL
|
||||||
|
{
|
||||||
|
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
|
||||||
|
RMFT2 * task=loopTask;
|
||||||
|
while(task) {
|
||||||
|
if (task->taskId==p[1]) {
|
||||||
|
task->kill(F("KILL"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
task=task->next;
|
||||||
|
if (task==loopTask) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case "RESERVE"_hk: // force reserve a section
|
||||||
|
return setFlag(p[1],SECTION_FLAG);
|
||||||
|
|
||||||
|
case "FREE"_hk: // force free a section
|
||||||
|
return setFlag(p[1],0,SECTION_FLAG);
|
||||||
|
|
||||||
|
case "LATCH"_hk:
|
||||||
|
return setFlag(p[1], LATCH_FLAG);
|
||||||
|
|
||||||
|
case "UNLATCH"_hk:
|
||||||
|
return setFlag(p[1], 0, LATCH_FLAG);
|
||||||
|
|
||||||
|
case "RED"_hk:
|
||||||
|
doSignal(p[1],SIGNAL_RED);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "AMBER"_hk:
|
||||||
|
doSignal(p[1],SIGNAL_AMBER);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "GREEN"_hk:
|
||||||
|
doSignal(p[1],SIGNAL_GREEN);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
262
EXRAILMacros.h
262
EXRAILMacros.h
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* © 2021 Neil McKechnie
|
* © 2021 Neil McKechnie
|
||||||
* © 2020-2022 Chris Harlow
|
* © 2020-2022 Chris Harlow
|
||||||
* © 2022 Colin Murdoch
|
* © 2022-2023 Colin Murdoch
|
||||||
* © 2023 Harald Barth
|
* © 2023 Harald Barth
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
|
@ -54,32 +54,176 @@
|
||||||
|
|
||||||
// helper macro for turnout descriptions, creates NULL for missing description
|
// helper macro for turnout descriptions, creates NULL for missing description
|
||||||
#define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL;
|
#define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL;
|
||||||
|
// helper macro for turntable descriptions, creates NULL for missing description
|
||||||
|
#define T_DESC(tid,pid,desc) if(turntableId==tid && positionId==pid) return ("" desc)[0]?F("" desc):NULL;
|
||||||
// helper macro for turnout description as HIDDEN
|
// helper macro for turnout description as HIDDEN
|
||||||
#define HIDDEN "\x01"
|
#define HIDDEN "\x01"
|
||||||
|
|
||||||
|
// PLAYSOUND is alias of ANOUT to make the user experience of a Conductor beter for
|
||||||
|
// playing sounds with IO_I2CDFPlayer
|
||||||
|
#define PLAYSOUND ANOUT
|
||||||
|
|
||||||
// helper macro to strip leading zeros off time inputs
|
// helper macro to strip leading zeros off time inputs
|
||||||
// (10#mins)%100)
|
// (10#mins)%100)
|
||||||
#define STRIP_ZERO(value) 10##value%100
|
#define STRIP_ZERO(value) 10##value%100
|
||||||
|
|
||||||
|
// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).
|
||||||
|
//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
|
||||||
|
//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
|
||||||
|
|
||||||
|
|
||||||
// Pass 1 Implements aliases
|
// Pass 1 Implements aliases
|
||||||
#include "EXRAIL2MacroReset.h"
|
#include "EXRAIL2MacroReset.h"
|
||||||
#undef ALIAS
|
#undef ALIAS
|
||||||
#define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10;
|
#define ALIAS(name,value...) const int name= #value[0] ? value+0: -__COUNTER__ ;
|
||||||
|
#include "myAutomation.h"
|
||||||
|
|
||||||
|
// Pass 1d Detect sequence duplicates.
|
||||||
|
// This pass generates no runtime data or code
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef AUTOMATION
|
||||||
|
#define AUTOMATION(id, description) id,
|
||||||
|
#undef ROUTE
|
||||||
|
#define ROUTE(id, description) id,
|
||||||
|
#undef SEQUENCE
|
||||||
|
#define SEQUENCE(id) id,
|
||||||
|
constexpr int16_t compileTimeSequenceList[]={
|
||||||
|
#include "myAutomation.h"
|
||||||
|
0
|
||||||
|
};
|
||||||
|
constexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;
|
||||||
|
|
||||||
|
|
||||||
|
// Compile time function to check for sequence nos.
|
||||||
|
constexpr bool hasseq(const int16_t value, const int16_t pos=0 ) {
|
||||||
|
return pos>=stuffSize? false :
|
||||||
|
compileTimeSequenceList[pos]==value
|
||||||
|
|| hasseq(value,pos+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile time function to check for duplicate sequence nos.
|
||||||
|
constexpr bool hasdup(const int16_t value, const int16_t pos ) {
|
||||||
|
return pos>=stuffSize? false :
|
||||||
|
compileTimeSequenceList[pos]==value
|
||||||
|
|| hasseq(value,pos+1)
|
||||||
|
|| hasdup(compileTimeSequenceList[pos],pos+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static_assert(!hasdup(compileTimeSequenceList[0],1),"Duplicate SEQUENCE/ROUTE/AUTOMATION detected");
|
||||||
|
|
||||||
|
//pass 1s static asserts to
|
||||||
|
// - check call and follows etc for existing sequence numbers
|
||||||
|
// - check range on LATCH/UNLATCH
|
||||||
|
// This pass generates no runtime data or code
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef ASPECT
|
||||||
|
#define ASPECT(address,value) static_assert(address <=2044, "invalid Address"); \
|
||||||
|
static_assert(address>=-3, "Invalid value");
|
||||||
|
#undef CALL
|
||||||
|
#define CALL(id) static_assert(hasseq(id),"Sequence not found");
|
||||||
|
#undef FOLLOW
|
||||||
|
#define FOLLOW(id) static_assert(hasseq(id),"Sequence not found");
|
||||||
|
#undef START
|
||||||
|
#define START(id) static_assert(hasseq(id),"Sequence not found");
|
||||||
|
#undef SENDLOCO
|
||||||
|
#define SENDLOCO(cab,id) static_assert(hasseq(id),"Sequence not found");
|
||||||
|
#undef LATCH
|
||||||
|
#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
|
||||||
|
#undef UNLATCH
|
||||||
|
#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
|
||||||
|
#undef RESERVE
|
||||||
|
#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
|
||||||
|
#undef FREE
|
||||||
|
#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
|
||||||
|
#undef SPEED
|
||||||
|
#define SPEED(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
|
||||||
|
#undef FWD
|
||||||
|
#define FWD(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
|
||||||
|
#undef REV
|
||||||
|
#define REV(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
|
||||||
|
|
||||||
|
#include "myAutomation.h"
|
||||||
|
|
||||||
|
// Pass 1g Implants STEALTH_GLOBAL in correct place
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef STEALTH_GLOBAL
|
||||||
|
#define STEALTH_GLOBAL(code...) code
|
||||||
#include "myAutomation.h"
|
#include "myAutomation.h"
|
||||||
|
|
||||||
// Pass 1h Implements HAL macro by creating exrailHalSetup function
|
// Pass 1h Implements HAL macro by creating exrailHalSetup function
|
||||||
|
// Also allows creating EXTurntable object
|
||||||
#include "EXRAIL2MacroReset.h"
|
#include "EXRAIL2MacroReset.h"
|
||||||
#undef HAL
|
#undef HAL
|
||||||
#define HAL(haltype,params...) haltype::create(params);
|
#define HAL(haltype,params...) haltype::create(params);
|
||||||
void exrailHalSetup() {
|
#undef HAL_IGNORE_DEFAULTS
|
||||||
|
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
|
||||||
|
#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;
|
||||||
#include "myAutomation.h"
|
#include "myAutomation.h"
|
||||||
|
return ignore_defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 1c detect compile time featurtes
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef SIGNAL
|
||||||
|
#define SIGNAL(redpin,amberpin,greenpin) | FEATURE_SIGNAL
|
||||||
|
#undef SIGNALH
|
||||||
|
#define SIGNALH(redpin,amberpin,greenpin) | FEATURE_SIGNAL
|
||||||
|
#undef SERVO_SIGNAL
|
||||||
|
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) | FEATURE_SIGNAL
|
||||||
|
#undef DCC_SIGNAL
|
||||||
|
#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL
|
||||||
|
#undef DCCX_SIGNAL
|
||||||
|
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL
|
||||||
|
#undef VIRTUAL_SIGNAL
|
||||||
|
#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL
|
||||||
|
|
||||||
|
#undef LCC
|
||||||
|
#define LCC(eventid) | FEATURE_LCC
|
||||||
|
#undef LCCX
|
||||||
|
#define LCCX(senderid,eventid) | FEATURE_LCC
|
||||||
|
#undef ONLCC
|
||||||
|
#define ONLCC(senderid,eventid) | FEATURE_LCC
|
||||||
|
#undef ROUTE_ACTIVE
|
||||||
|
#define ROUTE_ACTIVE(id) | FEATURE_ROUTESTATE
|
||||||
|
#undef ROUTE_INACTIVE
|
||||||
|
#define ROUTE_INACTIVE(id) | FEATURE_ROUTESTATE
|
||||||
|
#undef ROUTE_HIDDEN
|
||||||
|
#define ROUTE_HIDDEN(id) | FEATURE_ROUTESTATE
|
||||||
|
#undef ROUTE_DISABLED
|
||||||
|
#define ROUTE_DISABLED(id) | FEATURE_ROUTESTATE
|
||||||
|
#undef ROUTE_CAPTION
|
||||||
|
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
|
||||||
|
|
||||||
|
#undef CLEAR_STASH
|
||||||
|
#define CLEAR_STASH(id) | FEATURE_STASH
|
||||||
|
#undef CLEAR_ALL_STASH
|
||||||
|
#define CLEAR_ALL_STASH | FEATURE_STASH
|
||||||
|
#undef PICKUP_STASH
|
||||||
|
#define PICKUP_STASH(id) | FEATURE_STASH
|
||||||
|
#undef STASH
|
||||||
|
#define STASH(id) | FEATURE_STASH
|
||||||
|
#undef BLINK
|
||||||
|
#define BLINK(vpin,onDuty,offDuty) | FEATURE_BLINK
|
||||||
|
#undef ONBUTTON
|
||||||
|
#define ONBUTTON(vpin) | FEATURE_SENSOR
|
||||||
|
#undef ONSENSOR
|
||||||
|
#define ONSENSOR(vpin) | FEATURE_SENSOR
|
||||||
|
|
||||||
|
const byte RMFT2::compileFeatures = 0
|
||||||
|
#include "myAutomation.h"
|
||||||
|
;
|
||||||
|
|
||||||
// Pass 2 create throttle route list
|
// Pass 2 create throttle route list
|
||||||
#include "EXRAIL2MacroReset.h"
|
#include "EXRAIL2MacroReset.h"
|
||||||
#undef ROUTE
|
#undef ROUTE
|
||||||
#define ROUTE(id, description) id,
|
#define ROUTE(id, description) id,
|
||||||
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
|
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
|
||||||
#include "myAutomation.h"
|
#include "myAutomation.h"
|
||||||
INT16_MAX};
|
INT16_MAX};
|
||||||
// Pass 2a create throttle automation list
|
// Pass 2a create throttle automation list
|
||||||
|
@ -121,6 +265,15 @@ const int StringMacroTracker1=__COUNTER__;
|
||||||
#define PRINT(msg) THRUNGE(msg,thrunge_print)
|
#define PRINT(msg) THRUNGE(msg,thrunge_print)
|
||||||
#undef LCN
|
#undef LCN
|
||||||
#define LCN(msg) THRUNGE(msg,thrunge_lcn)
|
#define LCN(msg) THRUNGE(msg,thrunge_lcn)
|
||||||
|
#undef MESSAGE
|
||||||
|
#define MESSAGE(msg) THRUNGE(msg,thrunge_message)
|
||||||
|
|
||||||
|
#undef ROUTE_CAPTION
|
||||||
|
#define ROUTE_CAPTION(id,caption) \
|
||||||
|
case (__COUNTER__ - StringMacroTracker1) : {\
|
||||||
|
manageRouteCaption(id,F(caption));\
|
||||||
|
return;\
|
||||||
|
}
|
||||||
#undef SERIAL
|
#undef SERIAL
|
||||||
#define SERIAL(msg) THRUNGE(msg,thrunge_serial)
|
#define SERIAL(msg) THRUNGE(msg,thrunge_serial)
|
||||||
#undef SERIAL1
|
#undef SERIAL1
|
||||||
|
@ -153,6 +306,8 @@ const int StringMacroTracker1=__COUNTER__;
|
||||||
lcdid=id;\
|
lcdid=id;\
|
||||||
break;\
|
break;\
|
||||||
}
|
}
|
||||||
|
#undef STEALTH
|
||||||
|
#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return;
|
||||||
#undef WITHROTTLE
|
#undef WITHROTTLE
|
||||||
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
|
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
|
||||||
|
|
||||||
|
@ -189,6 +344,33 @@ const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass to get turntable descriptions (optional)
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef DCC_TURNTABLE
|
||||||
|
#define DCC_TURNTABLE(id,home,description...) O_DESC(id,description)
|
||||||
|
#undef EXTT_TURNTABLE
|
||||||
|
#define EXTT_TURNTABLE(id,vpin,home,description...) O_DESC(id,description)
|
||||||
|
|
||||||
|
const FSH * RMFT2::getTurntableDescription(int16_t turntableId) {
|
||||||
|
switch (turntableId) {
|
||||||
|
#include "myAutomation.h"
|
||||||
|
default:break;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass to get turntable position descriptions (optional)
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef TT_ADDPOSITION
|
||||||
|
#define TT_ADDPOSITION(turntable_id,position,value,home,description...) T_DESC(turntable_id,position,description)
|
||||||
|
|
||||||
|
const FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, uint8_t positionId) {
|
||||||
|
(void)turntableId;
|
||||||
|
(void)positionId;
|
||||||
|
#include "myAutomation.h"
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
// Pass 6: Roster IDs (count)
|
// Pass 6: Roster IDs (count)
|
||||||
#include "EXRAIL2MacroReset.h"
|
#include "EXRAIL2MacroReset.h"
|
||||||
#undef ROSTER
|
#undef ROSTER
|
||||||
|
@ -238,6 +420,8 @@ const FSH * RMFT2::getRosterFunctions(int16_t id) {
|
||||||
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
|
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
|
||||||
#undef DCC_SIGNAL
|
#undef DCC_SIGNAL
|
||||||
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
|
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
|
||||||
|
#undef DCCX_SIGNAL
|
||||||
|
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) id | RMFT2::DCCX_SIGNAL_FLAG,redAspect,amberAspect,greenAspect,
|
||||||
#undef VIRTUAL_SIGNAL
|
#undef VIRTUAL_SIGNAL
|
||||||
#define VIRTUAL_SIGNAL(id) id,0,0,0,
|
#define VIRTUAL_SIGNAL(id) id,0,0,0,
|
||||||
|
|
||||||
|
@ -245,6 +429,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#include "myAutomation.h"
|
#include "myAutomation.h"
|
||||||
0,0,0,0 };
|
0,0,0,0 };
|
||||||
|
|
||||||
|
// Pass 9 ONLCC counter and lookup array
|
||||||
|
#include "EXRAIL2MacroReset.h"
|
||||||
|
#undef ONLCC
|
||||||
|
#define ONLCC(sender,event) +1
|
||||||
|
|
||||||
|
const int RMFT2::countLCCLookup=0
|
||||||
|
#include "myAutomation.h"
|
||||||
|
;
|
||||||
|
int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||||
|
|
||||||
// Last Pass : create main routes table
|
// Last Pass : create main routes table
|
||||||
// Only undef the macros, not dummy them.
|
// Only undef the macros, not dummy them.
|
||||||
#define RMFT2_UNDEF_ONLY
|
#define RMFT2_UNDEF_ONLY
|
||||||
|
@ -258,24 +452,34 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
|
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
|
||||||
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
|
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
|
||||||
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
|
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
|
||||||
|
#define AFTEROVERLOAD(track_id) OPCODE_AFTEROVERLOAD,V(TRACK_NUMBER_##track_id),
|
||||||
#define ALIAS(name,value...)
|
#define ALIAS(name,value...)
|
||||||
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
|
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
|
||||||
#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),
|
#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),
|
||||||
|
#define ASPECT(address,value) OPCODE_ASPECT,V((address<<5) | (value & 0x1F)),
|
||||||
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
|
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
|
||||||
#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),
|
#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),
|
||||||
#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),
|
#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),
|
||||||
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
|
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
|
||||||
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
|
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
|
||||||
#define AUTOSTART OPCODE_AUTOSTART,0,0,
|
#define AUTOSTART OPCODE_AUTOSTART,0,0,
|
||||||
|
#define BLINK(vpin,onDuty,offDuty) OPCODE_BLINK,V(vpin),OPCODE_PAD,V(onDuty),OPCODE_PAD,V(offDuty),
|
||||||
#define BROADCAST(msg) PRINT(msg)
|
#define BROADCAST(msg) PRINT(msg)
|
||||||
#define CALL(route) OPCODE_CALL,V(route),
|
#define CALL(route) OPCODE_CALL,V(route),
|
||||||
|
#define CLEAR_STASH(id) OPCODE_CLEAR_STASH,V(id),
|
||||||
|
#define CLEAR_ALL_STASH OPCODE_CLEAR_ALL_STASH,V(0),
|
||||||
#define CLOSE(id) OPCODE_CLOSE,V(id),
|
#define CLOSE(id) OPCODE_CLOSE,V(id),
|
||||||
|
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home),
|
||||||
|
#endif
|
||||||
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
|
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
|
||||||
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
|
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
|
||||||
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
|
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
|
||||||
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
|
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
|
||||||
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
|
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
|
||||||
#define DCC_SIGNAL(id,add,subaddr)
|
#define DCC_SIGNAL(id,add,subaddr)
|
||||||
|
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
|
||||||
#define DONE OPCODE_ENDTASK,0,0,
|
#define DONE OPCODE_ENDTASK,0,0,
|
||||||
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
|
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
|
||||||
#define ELSE OPCODE_ELSE,0,0,
|
#define ELSE OPCODE_ELSE,0,0,
|
||||||
|
@ -284,15 +488,20 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define ENDTASK OPCODE_ENDTASK,0,0,
|
#define ENDTASK OPCODE_ENDTASK,0,0,
|
||||||
#define ESTOP OPCODE_SPEED,V(1),
|
#define ESTOP OPCODE_SPEED,V(1),
|
||||||
#define EXRAIL
|
#define EXRAIL
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define EXTT_TURNTABLE(id,vpin,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(home),
|
||||||
|
#endif
|
||||||
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
|
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
|
||||||
#define FOFF(func) OPCODE_FOFF,V(func),
|
#define FOFF(func) OPCODE_FOFF,V(func),
|
||||||
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
|
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
|
||||||
#define FON(func) OPCODE_FON,V(func),
|
#define FON(func) OPCODE_FON,V(func),
|
||||||
#define FORGET OPCODE_FORGET,0,0,
|
#define FORGET OPCODE_FORGET,0,0,
|
||||||
#define FREE(blockid) OPCODE_FREE,V(blockid),
|
#define FREE(blockid) OPCODE_FREE,V(blockid),
|
||||||
|
#define FTOGGLE(func) OPCODE_FTOGGLE,V(func),
|
||||||
#define FWD(speed) OPCODE_FWD,V(speed),
|
#define FWD(speed) OPCODE_FWD,V(speed),
|
||||||
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
|
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
|
||||||
#define HAL(haltype,params...)
|
#define HAL(haltype,params...)
|
||||||
|
#define HAL_IGNORE_DEFAULTS
|
||||||
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
|
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
|
||||||
#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
|
#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
|
||||||
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
|
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
|
||||||
|
@ -306,29 +515,52 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
|
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
|
||||||
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
|
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
|
||||||
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
|
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position),
|
||||||
|
#endif
|
||||||
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
|
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
|
||||||
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
|
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
|
||||||
|
#define JMRI_SENSOR(vpin,count...)
|
||||||
#define JOIN OPCODE_JOIN,0,0,
|
#define JOIN OPCODE_JOIN,0,0,
|
||||||
#define KILLALL OPCODE_KILLALL,0,0,
|
#define KILLALL OPCODE_KILLALL,0,0,
|
||||||
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
|
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
|
||||||
|
#define LCC(eventid) OPCODE_LCC,V(eventid),
|
||||||
|
#define LCCX(sender,event) OPCODE_LCCX,V(event),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
|
||||||
#define LCD(id,msg) PRINT(msg)
|
#define LCD(id,msg) PRINT(msg)
|
||||||
#define SCREEN(display,id,msg) PRINT(msg)
|
#define SCREEN(display,id,msg) PRINT(msg)
|
||||||
|
#define STEALTH(code...) PRINT(dummy)
|
||||||
|
#define STEALTH_GLOBAL(code...)
|
||||||
#define LCN(msg) PRINT(msg)
|
#define LCN(msg) PRINT(msg)
|
||||||
|
#define MESSAGE(msg) PRINT(msg)
|
||||||
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
|
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
|
||||||
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
|
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
|
||||||
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
|
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
|
||||||
#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),
|
#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),
|
||||||
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
|
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
|
||||||
|
#define ONLCC(sender,event) OPCODE_ONLCC,V(event),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
|
||||||
|
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
|
||||||
#define ONTIME(value) OPCODE_ONTIME,V(value),
|
#define ONTIME(value) OPCODE_ONTIME,V(value),
|
||||||
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
||||||
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
||||||
|
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
|
||||||
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||||
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
||||||
#define ONRED(signal_id) OPCODE_ONRED,V(signal_id),
|
#define ONRED(signal_id) OPCODE_ONRED,V(signal_id),
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define ONROTATE(id) OPCODE_ONROTATE,V(id),
|
||||||
|
#endif
|
||||||
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
|
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
|
||||||
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
|
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
|
||||||
|
#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),
|
||||||
|
#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,V(sensor_id),
|
||||||
#define PAUSE OPCODE_PAUSE,0,0,
|
#define PAUSE OPCODE_PAUSE,0,0,
|
||||||
|
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
|
||||||
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
|
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
|
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
|
||||||
|
@ -345,7 +577,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define RETURN OPCODE_RETURN,0,0,
|
#define RETURN OPCODE_RETURN,0,0,
|
||||||
#define REV(speed) OPCODE_REV,V(speed),
|
#define REV(speed) OPCODE_REV,V(speed),
|
||||||
#define ROSTER(cabid,name,funcmap...)
|
#define ROSTER(cabid,name,funcmap...)
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define ROTATE(id,position,activity) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(EXTurntable::activity),
|
||||||
|
#define ROTATE_DCC(id,position) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(0),
|
||||||
|
#endif
|
||||||
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
|
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
|
||||||
|
#define ROUTE_ACTIVE(id) OPCODE_ROUTE_ACTIVE,V(id),
|
||||||
|
#define ROUTE_INACTIVE(id) OPCODE_ROUTE_INACTIVE,V(id),
|
||||||
|
#define ROUTE_HIDDEN(id) OPCODE_ROUTE_HIDDEN,V(id),
|
||||||
|
#define ROUTE_DISABLED(id) OPCODE_ROUTE_DISABLED,V(id),
|
||||||
|
#define ROUTE_CAPTION(id,caption) PRINT(caption)
|
||||||
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
|
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
|
||||||
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
|
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
|
||||||
#define SERIAL(msg) PRINT(msg)
|
#define SERIAL(msg) PRINT(msg)
|
||||||
|
@ -361,13 +602,20 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
|
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
|
||||||
#define SET(pin) OPCODE_SET,V(pin),
|
#define SET(pin) OPCODE_SET,V(pin),
|
||||||
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
|
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
|
||||||
|
#define SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track),
|
||||||
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
|
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
|
||||||
|
#define SETFREQ(loco,freq) OPCODE_SETLOCO,V(loco), OPCODE_SETFREQ,V(freq),
|
||||||
#define SIGNAL(redpin,amberpin,greenpin)
|
#define SIGNAL(redpin,amberpin,greenpin)
|
||||||
#define SIGNALH(redpin,amberpin,greenpin)
|
#define SIGNALH(redpin,amberpin,greenpin)
|
||||||
#define SPEED(speed) OPCODE_SPEED,V(speed),
|
#define SPEED(speed) OPCODE_SPEED,V(speed),
|
||||||
#define START(route) OPCODE_START,V(route),
|
#define START(route) OPCODE_START,V(route),
|
||||||
|
#define STASH(id) OPCODE_STASH,V(id),
|
||||||
#define STOP OPCODE_SPEED,V(0),
|
#define STOP OPCODE_SPEED,V(0),
|
||||||
#define THROW(id) OPCODE_THROW,V(id),
|
#define THROW(id) OPCODE_THROW,V(id),
|
||||||
|
#define TOGGLE_TURNOUT(id) OPCODE_TOGGLE_TURNOUT,V(id),
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define TT_ADDPOSITION(id,position,value,angle,description...) OPCODE_TTADDPOSITION,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(value),OPCODE_PAD,V(angle),
|
||||||
|
#endif
|
||||||
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
|
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
|
||||||
#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description)
|
#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description)
|
||||||
#define UNJOIN OPCODE_UNJOIN,0,0,
|
#define UNJOIN OPCODE_UNJOIN,0,0,
|
||||||
|
@ -376,12 +624,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||||
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
|
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
|
||||||
#define WITHROTTLE(msg) PRINT(msg)
|
#define WITHROTTLE(msg) PRINT(msg)
|
||||||
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
|
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
#define WAITFORTT(turntable_id) OPCODE_WAITFORTT,V(turntable_id),
|
||||||
|
#endif
|
||||||
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
|
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
|
||||||
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
|
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
|
||||||
|
#define XFTOGGLE(cab,func) OPCODE_XFTOGGLE,V(cab),OPCODE_PAD,V(func),
|
||||||
|
|
||||||
// Build RouteCode
|
// Build RouteCode
|
||||||
const int StringMacroTracker2=__COUNTER__;
|
const int StringMacroTracker2=__COUNTER__;
|
||||||
const HIGHFLASH byte RMFT2::RouteCode[] = {
|
const HIGHFLASH3 byte RMFT2::RouteCode[] = {
|
||||||
#include "myAutomation.h"
|
#include "myAutomation.h"
|
||||||
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
|
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
|
||||||
|
|
||||||
|
|
104
EXRAILSensor.cpp
Normal file
104
EXRAILSensor.cpp
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* © 2024 Chris Harlow
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
EXRAILSensor represents a sensor that should be monitored in order
|
||||||
|
to call an exrail ONBUTTON or ONCHANGE handler.
|
||||||
|
These are created at EXRAIL startup and thus need no delete or listing
|
||||||
|
capability.
|
||||||
|
The basic logic is similar to that found in the Sensor class
|
||||||
|
except that on the relevant change an EXRAIL thread is started.
|
||||||
|
**********************************************************************/
|
||||||
|
|
||||||
|
#include "EXRAILSensor.h"
|
||||||
|
#include "EXRAIL2.h"
|
||||||
|
|
||||||
|
void EXRAILSensor::checkAll() {
|
||||||
|
if (firstSensor == NULL) return; // No sensors to be scanned
|
||||||
|
if (readingSensor == NULL) {
|
||||||
|
// Not currently scanning sensor list
|
||||||
|
unsigned long thisTime = micros();
|
||||||
|
if (thisTime - lastReadCycle < cycleInterval) return;
|
||||||
|
// Required time has elapsed since last read cycle started,
|
||||||
|
// so initiate new scan through the sensor list
|
||||||
|
readingSensor = firstSensor;
|
||||||
|
lastReadCycle = thisTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop until either end of list is encountered or we pause for some reason
|
||||||
|
byte sensorCount = 0;
|
||||||
|
|
||||||
|
while (readingSensor != NULL) {
|
||||||
|
bool pause=readingSensor->check();
|
||||||
|
// Move to next sensor in list.
|
||||||
|
readingSensor = readingSensor->nextSensor;
|
||||||
|
// Currently process max of 16 sensors per entry.
|
||||||
|
// Performance measurements taken during development indicate that, with 128 sensors configured
|
||||||
|
// on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices
|
||||||
|
// within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.
|
||||||
|
if (pause || (++sensorCount)>=16) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EXRAILSensor::check() {
|
||||||
|
// check for debounced change in this sensor
|
||||||
|
inputState = RMFT2::readSensor(pin);
|
||||||
|
|
||||||
|
// Check if changed since last time, and process changes.
|
||||||
|
if (inputState == active) {// no change
|
||||||
|
latchDelay = minReadCount; // Reset counter
|
||||||
|
return false; // no change
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change detected ... has it stayed changed for long enough
|
||||||
|
if (latchDelay > 0) {
|
||||||
|
latchDelay--;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// change validated, act on it.
|
||||||
|
active = inputState;
|
||||||
|
latchDelay = minReadCount; // Reset debounce counter
|
||||||
|
if (onChange || active) {
|
||||||
|
new RMFT2(progCounter);
|
||||||
|
return true; // Don't check any more sensors on this entry
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange) {
|
||||||
|
// Add to the start of the list
|
||||||
|
//DIAG(F("ONthing vpin=%d at %d"), _pin, _progCounter);
|
||||||
|
nextSensor = firstSensor;
|
||||||
|
firstSensor = this;
|
||||||
|
|
||||||
|
pin=_pin;
|
||||||
|
progCounter=_progCounter;
|
||||||
|
onChange=_onChange;
|
||||||
|
|
||||||
|
IODevice::configureInput(pin, true);
|
||||||
|
active = IODevice::read(pin);
|
||||||
|
inputState = active;
|
||||||
|
latchDelay = minReadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXRAILSensor *EXRAILSensor::firstSensor=NULL;
|
||||||
|
EXRAILSensor *EXRAILSensor::readingSensor=NULL;
|
||||||
|
unsigned long EXRAILSensor::lastReadCycle=0;
|
50
EXRAILSensor.h
Normal file
50
EXRAILSensor.h
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* © 2024 Chris Harlow
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef EXRAILSensor_h
|
||||||
|
#define EXRAILSensor_h
|
||||||
|
#include "IODevice.h"
|
||||||
|
class EXRAILSensor {
|
||||||
|
static EXRAILSensor * firstSensor;
|
||||||
|
static EXRAILSensor * readingSensor;
|
||||||
|
static unsigned long lastReadCycle;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void checkAll();
|
||||||
|
|
||||||
|
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange);
|
||||||
|
bool check();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.
|
||||||
|
// should not be less than device scan cycle time.
|
||||||
|
static const byte minReadCount = 4; // number of additional scans before acting on change
|
||||||
|
// E.g. 1 means that a change is ignored for one scan and actioned on the next.
|
||||||
|
// Max value is 63
|
||||||
|
|
||||||
|
EXRAILSensor* nextSensor;
|
||||||
|
VPIN pin;
|
||||||
|
int progCounter;
|
||||||
|
bool active;
|
||||||
|
bool inputState;
|
||||||
|
bool onChange;
|
||||||
|
byte latchDelay;
|
||||||
|
};
|
||||||
|
#endif
|
|
@ -47,6 +47,10 @@ void EthernetInterface::setup()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef IP_ADDRESS
|
||||||
|
static IPAddress myIP(IP_ADDRESS);
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Aquire IP Address from DHCP and start server
|
* @brief Aquire IP Address from DHCP and start server
|
||||||
*
|
*
|
||||||
|
@ -59,15 +63,15 @@ EthernetInterface::EthernetInterface()
|
||||||
DCCTimer::getSimulatedMacAddress(mac);
|
DCCTimer::getSimulatedMacAddress(mac);
|
||||||
connected=false;
|
connected=false;
|
||||||
|
|
||||||
#ifdef IP_ADDRESS
|
#ifdef IP_ADDRESS
|
||||||
Ethernet.begin(mac, IP_ADDRESS);
|
Ethernet.begin(mac, myIP);
|
||||||
#else
|
#else
|
||||||
if (Ethernet.begin(mac) == 0)
|
if (Ethernet.begin(mac) == 0)
|
||||||
{
|
{
|
||||||
DIAG(F("Ethernet.begin FAILED"));
|
DIAG(F("Ethernet.begin FAILED"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
|
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
|
||||||
DIAG(F("Ethernet shield not found or W5100"));
|
DIAG(F("Ethernet shield not found or W5100"));
|
||||||
}
|
}
|
||||||
|
@ -136,7 +140,7 @@ bool EthernetInterface::checkLink() {
|
||||||
DIAG(F("Ethernet cable connected"));
|
DIAG(F("Ethernet cable connected"));
|
||||||
connected=true;
|
connected=true;
|
||||||
#ifdef IP_ADDRESS
|
#ifdef IP_ADDRESS
|
||||||
Ethernet.setLocalIP(IP_ADDRESS); // for static IP, set it again
|
Ethernet.setLocalIP(myIP); // for static IP, set it again
|
||||||
#endif
|
#endif
|
||||||
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
|
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
|
||||||
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
|
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
|
||||||
|
|
3
FSH.h
3
FSH.h
|
@ -56,6 +56,7 @@ typedef __FlashStringHelper FSH;
|
||||||
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
|
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
|
||||||
// AVR_MEGA memory deliberately placed at end of link may need _far functions
|
// AVR_MEGA memory deliberately placed at end of link may need _far functions
|
||||||
#define HIGHFLASH __attribute__((section(".fini2")))
|
#define HIGHFLASH __attribute__((section(".fini2")))
|
||||||
|
#define HIGHFLASH3 __attribute__((section(".fini3")))
|
||||||
#define GETFARPTR(data) pgm_get_far_address(data)
|
#define GETFARPTR(data) pgm_get_far_address(data)
|
||||||
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
|
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
|
||||||
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
|
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
|
||||||
|
@ -63,6 +64,7 @@ typedef __FlashStringHelper FSH;
|
||||||
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
|
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
|
||||||
// as there is no progmem above 32kb anyway.
|
// as there is no progmem above 32kb anyway.
|
||||||
#define HIGHFLASH PROGMEM
|
#define HIGHFLASH PROGMEM
|
||||||
|
#define HIGHFLASH3 PROGMEM
|
||||||
#define GETFARPTR(data) ((uint32_t)(data))
|
#define GETFARPTR(data) ((uint32_t)(data))
|
||||||
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
|
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
|
||||||
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
|
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
|
||||||
|
@ -80,6 +82,7 @@ typedef __FlashStringHelper FSH;
|
||||||
typedef char FSH;
|
typedef char FSH;
|
||||||
#define FLASH
|
#define FLASH
|
||||||
#define HIGHFLASH
|
#define HIGHFLASH
|
||||||
|
#define HIGHFLASH3
|
||||||
#define GETFARPTR(data) ((uint32_t)(data))
|
#define GETFARPTR(data) ((uint32_t)(data))
|
||||||
#define GETFLASH(addr) (*(const byte *)(addr))
|
#define GETFLASH(addr) (*(const byte *)(addr))
|
||||||
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
|
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
#define GITHUB_SHA "3bddf4d"
|
#define GITHUB_SHA "devel-202406021945Z"
|
||||||
|
|
|
@ -58,6 +58,8 @@ static const FSH * guessI2CDeviceType(uint8_t address) {
|
||||||
return F("Time-of-flight sensor");
|
return F("Time-of-flight sensor");
|
||||||
else if (address >= 0x3c && address <= 0x3d)
|
else if (address >= 0x3c && address <= 0x3d)
|
||||||
return F("OLED Display");
|
return F("OLED Display");
|
||||||
|
else if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection
|
||||||
|
return F("SC16IS75x UART");
|
||||||
else if (address >= 0x48 && address <= 0x4f)
|
else if (address >= 0x48 && address <= 0x4f)
|
||||||
return F("Analogue Inputs or PWM");
|
return F("Analogue Inputs or PWM");
|
||||||
else if (address >= 0x40 && address <= 0x4f)
|
else if (address >= 0x40 && address <= 0x4f)
|
||||||
|
@ -96,7 +98,7 @@ void I2CManagerClass::begin(void) {
|
||||||
// Probe and list devices. Use standard mode
|
// Probe and list devices. Use standard mode
|
||||||
// (clock speed 100kHz) for best device compatibility.
|
// (clock speed 100kHz) for best device compatibility.
|
||||||
_setClock(100000);
|
_setClock(100000);
|
||||||
unsigned long originalTimeout = _timeout;
|
uint32_t originalTimeout = _timeout;
|
||||||
setTimeout(1000); // use 1ms timeout for probes
|
setTimeout(1000); // use 1ms timeout for probes
|
||||||
|
|
||||||
#if defined(I2C_EXTENDED_ADDRESS)
|
#if defined(I2C_EXTENDED_ADDRESS)
|
||||||
|
|
|
@ -485,7 +485,7 @@ private:
|
||||||
// When retries are enabled, the timeout applies to each
|
// When retries are enabled, the timeout applies to each
|
||||||
// try, and failure from timeout does not get retried.
|
// try, and failure from timeout does not get retried.
|
||||||
// A value of 0 means disable timeout monitoring.
|
// A value of 0 means disable timeout monitoring.
|
||||||
unsigned long _timeout = 100000UL;
|
uint32_t _timeout = 100000UL;
|
||||||
|
|
||||||
// Finish off request block by waiting for completion and posting status.
|
// Finish off request block by waiting for completion and posting status.
|
||||||
uint8_t finishRB(I2CRB *rb, uint8_t status);
|
uint8_t finishRB(I2CRB *rb, uint8_t status);
|
||||||
|
@ -532,13 +532,14 @@ private:
|
||||||
uint8_t bytesToSend = 0;
|
uint8_t bytesToSend = 0;
|
||||||
uint8_t bytesToReceive = 0;
|
uint8_t bytesToReceive = 0;
|
||||||
uint8_t operation = 0;
|
uint8_t operation = 0;
|
||||||
unsigned long startTime = 0;
|
uint32_t startTime = 0;
|
||||||
uint8_t muxPhase = 0;
|
uint8_t muxPhase = 0;
|
||||||
uint8_t muxAddress = 0;
|
uint8_t muxAddress = 0;
|
||||||
uint8_t muxData[1];
|
uint8_t muxData[1];
|
||||||
uint8_t deviceAddress;
|
uint8_t deviceAddress;
|
||||||
const uint8_t *sendBuffer;
|
const uint8_t *sendBuffer;
|
||||||
uint8_t *receiveBuffer;
|
uint8_t *receiveBuffer;
|
||||||
|
uint8_t transactionState = 0;
|
||||||
|
|
||||||
volatile uint32_t pendingClockSpeed = 0;
|
volatile uint32_t pendingClockSpeed = 0;
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,10 @@ void I2CManagerClass::startTransaction() {
|
||||||
* Function to queue a request block and initiate operations.
|
* Function to queue a request block and initiate operations.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
void I2CManagerClass::queueRequest(I2CRB *req) {
|
void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||||
|
|
||||||
|
if (((req->operation & OPERATION_MASK) == OPERATION_READ) && req->readLen == 0)
|
||||||
|
return; // Ignore null read
|
||||||
|
|
||||||
req->status = I2C_STATUS_PENDING;
|
req->status = I2C_STATUS_PENDING;
|
||||||
req->nextRequest = NULL;
|
req->nextRequest = NULL;
|
||||||
ATOMIC_BLOCK() {
|
ATOMIC_BLOCK() {
|
||||||
|
@ -184,6 +188,7 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Initiate a write to an I2C device (non-blocking operation)
|
* Initiate a write to an I2C device (non-blocking operation)
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
@ -240,8 +245,8 @@ void I2CManagerClass::checkForTimeout() {
|
||||||
I2CRB *t = queueHead;
|
I2CRB *t = queueHead;
|
||||||
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {
|
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {
|
||||||
// Check for timeout
|
// Check for timeout
|
||||||
unsigned long elapsed = micros() - startTime;
|
int32_t elapsed = micros() - startTime;
|
||||||
if (elapsed > _timeout) {
|
if (elapsed > (int32_t)_timeout) {
|
||||||
#ifdef DIAG_IO
|
#ifdef DIAG_IO
|
||||||
//DIAG(F("I2CManager Timeout on %s"), t->i2cAddress.toString());
|
//DIAG(F("I2CManager Timeout on %s"), t->i2cAddress.toString());
|
||||||
#endif
|
#endif
|
||||||
|
@ -300,12 +305,12 @@ void I2CManagerClass::handleInterrupt() {
|
||||||
|
|
||||||
// Check if current request has completed. If there's a current request
|
// Check if current request has completed. If there's a current request
|
||||||
// and state isn't active then state contains the completion status of the request.
|
// and state isn't active then state contains the completion status of the request.
|
||||||
if (state == I2C_STATE_COMPLETED && currentRequest != NULL) {
|
if (state == I2C_STATE_COMPLETED && currentRequest != NULL && currentRequest == queueHead) {
|
||||||
// Operation has completed.
|
// Operation has completed.
|
||||||
if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES
|
if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES
|
||||||
|| currentRequest->operation & OPERATION_NORETRY)
|
|| currentRequest->operation & OPERATION_NORETRY)
|
||||||
{
|
{
|
||||||
// Status is OK, or has failed and retry count exceeded, or retries disabled.
|
// Status is OK, or has failed and retry count exceeded, or failed and retries disabled.
|
||||||
#if defined(I2C_EXTENDED_ADDRESS)
|
#if defined(I2C_EXTENDED_ADDRESS)
|
||||||
if (muxPhase == MuxPhase_PROLOG ) {
|
if (muxPhase == MuxPhase_PROLOG ) {
|
||||||
overallStatus = completionStatus;
|
overallStatus = completionStatus;
|
||||||
|
|
|
@ -26,27 +26,44 @@
|
||||||
#include "I2CManager.h"
|
#include "I2CManager.h"
|
||||||
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
|
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
|
||||||
|
|
||||||
//#include <avr/io.h>
|
|
||||||
//#include <avr/interrupt.h>
|
|
||||||
#include <wiring_private.h>
|
#include <wiring_private.h>
|
||||||
|
#include "stm32f4xx_hal_rcc.h"
|
||||||
|
|
||||||
/***************************************************************************
|
/*****************************************************************************
|
||||||
* Interrupt handler.
|
* STM32F4xx I2C native driver support
|
||||||
* IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero
|
*
|
||||||
* compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.
|
* Nucleo-64 and Nucleo-144 boards all use I2C1 as the default I2C peripheral
|
||||||
* Later we may wish to allow use of an alternate I2C bus, or more than one I2C
|
* Later we may wish to support other STM32 boards, allow use of an alternate
|
||||||
* bus on the SAMD architecture
|
* I2C bus, or more than one I2C bus on the STM32 architecture
|
||||||
***************************************************************************/
|
*****************************************************************************/
|
||||||
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
|
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
|
||||||
void I2C1_IRQHandler() {
|
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \
|
||||||
|
|| defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) \
|
||||||
|
|| defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
|
||||||
|
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely all Nucleo-64
|
||||||
|
// and Nucleo-144 variants
|
||||||
|
I2C_TypeDef *s = I2C1;
|
||||||
|
|
||||||
|
// In init we will ask the STM32 HAL layer for the configured APB1 clock frequency in Hz
|
||||||
|
uint32_t APB1clk1; // Peripheral Input Clock speed in Hz.
|
||||||
|
uint32_t i2c_MHz; // Peripheral Input Clock speed in MHz.
|
||||||
|
|
||||||
|
// IRQ handler for I2C1, replacing the weak definition in the STM32 HAL
|
||||||
|
extern "C" void I2C1_EV_IRQHandler(void) {
|
||||||
I2CManager.handleInterrupt();
|
I2CManager.handleInterrupt();
|
||||||
}
|
}
|
||||||
|
extern "C" void I2C1_ER_IRQHandler(void) {
|
||||||
|
I2CManager.handleInterrupt();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#warning STM32 board selected is not yet supported - so I2C1 peripheral is not defined
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely Nucleo-64 variants
|
// Peripheral Input Clock speed in MHz.
|
||||||
I2C_TypeDef *s = I2C1;
|
// For STM32F446RE, the speed is 45MHz. Ideally, this should be determined
|
||||||
#define I2C_IRQn I2C1_EV_IRQn
|
// at run-time from the APB1 clock, as it can vary from STM32 family to family.
|
||||||
#define I2C_BUSFREQ 16
|
// #define I2C_PERIPH_CLK 45
|
||||||
|
|
||||||
// I2C SR1 Status Register #1 bit definitions for convenience
|
// I2C SR1 Status Register #1 bit definitions for convenience
|
||||||
// #define I2C_SR1_SMBALERT (1<<15) // SMBus alert
|
// #define I2C_SR1_SMBALERT (1<<15) // SMBus alert
|
||||||
|
@ -80,52 +97,65 @@ I2C_TypeDef *s = I2C1;
|
||||||
// #define I2C_CR1_SMBUS (1<<1) // SMBus mode, 1=SMBus, 0=I2C
|
// #define I2C_CR1_SMBUS (1<<1) // SMBus mode, 1=SMBus, 0=I2C
|
||||||
// #define I2C_CR1_PE (1<<0) // I2C Peripheral enable
|
// #define I2C_CR1_PE (1<<0) // I2C Peripheral enable
|
||||||
|
|
||||||
|
// States of the STM32 I2C driver state machine
|
||||||
|
enum {TS_IDLE,TS_START,TS_W_ADDR,TS_W_DATA,TS_W_STOP,TS_R_ADDR,TS_R_DATA,TS_R_STOP};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Set I2C clock speed register. This should only be called outside of
|
* Set I2C clock speed register. This should only be called outside of
|
||||||
* a transmission. The I2CManagerClass::_setClock() function ensures
|
* a transmission. The I2CManagerClass::_setClock() function ensures
|
||||||
* that it is only called at the beginning of an I2C transaction.
|
* that it is only called at the beginning of an I2C transaction.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
|
void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
|
||||||
|
|
||||||
// Calculate a rise time appropriate to the requested bus speed
|
// Calculate a rise time appropriate to the requested bus speed
|
||||||
// Use 10x the rise time spec to enable integer divide of 62.5ns clock period
|
// Use 10x the rise time spec to enable integer divide of 50ns clock period
|
||||||
uint16_t t_rise;
|
uint16_t t_rise;
|
||||||
uint32_t ccr_freq;
|
|
||||||
if (i2cClockSpeed < 200000L) {
|
while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further
|
||||||
// i2cClockSpeed = 100000L;
|
// writes to CR1 while STOP is being executed!
|
||||||
t_rise = 0x11; // (1000ns /62.5ns) + 1;
|
|
||||||
}
|
// Disable the I2C device, as TRISE can only be programmed whilst disabled
|
||||||
else if (i2cClockSpeed < 800000L)
|
s->CR1 &= ~(I2C_CR1_PE); // Disable I2C
|
||||||
|
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
|
||||||
|
asm("nop"); // wait a bit... suggestion from online!
|
||||||
|
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
|
||||||
|
|
||||||
|
if (i2cClockSpeed > 100000UL)
|
||||||
{
|
{
|
||||||
i2cClockSpeed = 400000L;
|
// if (i2cClockSpeed > 400000L)
|
||||||
t_rise = 0x06; // (300ns / 62.5ns) + 1;
|
// i2cClockSpeed = 400000L;
|
||||||
// } else if (i2cClockSpeed < 1200000L) {
|
|
||||||
// i2cClockSpeed = 1000000L;
|
t_rise = 300; // nanoseconds
|
||||||
// t_rise = 120;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
i2cClockSpeed = 100000L;
|
// i2cClockSpeed = 100000L;
|
||||||
t_rise = 0x11; // (1000ns /62.5ns) + 1;
|
t_rise = 1000; // nanoseconds
|
||||||
}
|
}
|
||||||
|
// Configure the rise time register - max allowed tRISE is 1000ns,
|
||||||
// Enable the I2C master mode
|
// so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
|
||||||
s->CR1 &= ~(I2C_CR1_PE); // Enable I2C
|
s->TRISE = (t_rise * i2c_MHz / 1000) + 1;
|
||||||
// Software reset the I2C peripheral
|
|
||||||
// s->CR1 |= I2C_CR1_SWRST; // reset the I2C
|
|
||||||
// Release reset
|
|
||||||
// s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
|
|
||||||
|
|
||||||
// Calculate baudrate - using a rise time appropriate for the speed
|
|
||||||
ccr_freq = I2C_BUSFREQ * 1000000 / i2cClockSpeed / 2;
|
|
||||||
|
|
||||||
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
|
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
|
||||||
// Bit 14: Duty, fast mode duty cycle
|
// Bit 14: Duty, fast mode duty cycle (use 2:1)
|
||||||
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
|
// Bit 11-0: FREQR
|
||||||
s->CCR = (uint16_t)ccr_freq;
|
// if (i2cClockSpeed > 400000UL) {
|
||||||
|
// // In fast mode plus, I2C period is 3 * CCR * TPCLK1.
|
||||||
|
// // s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
|
||||||
|
// s->CCR = APB1clk1 / 3 / i2cClockSpeed; // Set I2C clockspeed to start!
|
||||||
|
// s->CCR |= 0xC000; // We need Fast Mode AND DUTY bits set
|
||||||
|
// } else {
|
||||||
|
// In standard and fast mode, I2C period is 2 * CCR * TPCLK1
|
||||||
|
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
|
||||||
|
s->CCR |= (APB1clk1 / 2 / i2cClockSpeed); // Set I2C clockspeed to start!
|
||||||
|
// s->CCR |= (i2c_MHz * 500 / (i2cClockSpeed / 1000)); // Set I2C clockspeed to start!
|
||||||
|
// if (i2cClockSpeed > 100000UL)
|
||||||
|
// s->CCR |= 0xC000; // We need Fast Mode bits set as well
|
||||||
|
// }
|
||||||
|
|
||||||
// Configure the rise time register
|
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
|
||||||
s->TRISE = t_rise; // 1000 ns / 62.5 ns = 16 + 1
|
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
|
||||||
|
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
|
||||||
|
|
||||||
// Enable the I2C master mode
|
// Enable the I2C master mode
|
||||||
s->CR1 |= I2C_CR1_PE; // Enable I2C
|
s->CR1 |= I2C_CR1_PE; // Enable I2C
|
||||||
|
@ -136,32 +166,54 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
void I2CManagerClass::I2C_init()
|
void I2CManagerClass::I2C_init()
|
||||||
{
|
{
|
||||||
//Setting up the clocks
|
// Query the clockspeed from the STM32 HAL layer
|
||||||
RCC->APB1ENR |= (1<<21); // Enable I2C CLOCK
|
APB1clk1 = HAL_RCC_GetPCLK1Freq();
|
||||||
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9
|
i2c_MHz = APB1clk1 / 1000000UL;
|
||||||
|
// DIAG(F("I2C_init() peripheral clock speed is: %d"), i2c_MHz);
|
||||||
|
// Enable clocks
|
||||||
|
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;//(1 << 21); // Enable I2C CLOCK
|
||||||
|
// Reset the I2C1 peripheral to initial state
|
||||||
|
RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST;
|
||||||
|
RCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST;
|
||||||
// Standard I2C pins are SCL on PB8 and SDA on PB9
|
// Standard I2C pins are SCL on PB8 and SDA on PB9
|
||||||
|
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9
|
||||||
// Bits (17:16)= 1:0 --> Alternate Function for Pin PB8;
|
// Bits (17:16)= 1:0 --> Alternate Function for Pin PB8;
|
||||||
// Bits (19:18)= 1:0 --> Alternate Function for Pin PB9
|
// Bits (19:18)= 1:0 --> Alternate Function for Pin PB9
|
||||||
|
GPIOB->MODER &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all MODER bits for PB8 and PB9
|
||||||
GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2)); // PB8 and PB9 set to ALT function
|
GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2)); // PB8 and PB9 set to ALT function
|
||||||
GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability
|
GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability
|
||||||
GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode
|
GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode
|
||||||
|
GPIOB->PUPDR &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all PUPDR bits for PB8 and PB9
|
||||||
GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
|
GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
|
||||||
// Alt Function High register routing pins PB8 and PB9 for I2C1:
|
// Alt Function High register routing pins PB8 and PB9 for I2C1:
|
||||||
// Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8
|
// Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8
|
||||||
// Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9
|
// Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9
|
||||||
|
GPIOB->AFR[1] &= ~((15<<0) | (15<<4)); // Clear all AFR bits for PB8 on low nibble, PB9 on next nibble up
|
||||||
GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up
|
GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up
|
||||||
|
|
||||||
// Software reset the I2C peripheral
|
// Software reset the I2C peripheral
|
||||||
|
I2C1->CR1 &= ~I2C_CR1_PE; // Disable I2C1 peripheral
|
||||||
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
|
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
|
||||||
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
|
asm("nop"); // wait a bit... suggestion from online!
|
||||||
|
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
|
||||||
|
|
||||||
// Program the peripheral input clock in CR2 Register in order to generate correct timings
|
// Clear all bits in I2C CR2 register except reserved bits
|
||||||
s->CR2 |= I2C_BUSFREQ; // PCLK1 FREQUENCY in MHz
|
s->CR2 &= 0xE000;
|
||||||
|
|
||||||
|
// Set I2C peripheral clock frequency
|
||||||
|
// s->CR2 |= I2C_PERIPH_CLK;
|
||||||
|
s->CR2 |= i2c_MHz;
|
||||||
|
// DIAG(F("I2C_init() peripheral clock is now: %d"), s->CR2);
|
||||||
|
|
||||||
|
// set own address to 00 - not used in master mode
|
||||||
|
I2C1->OAR1 = (1 << 14); // bit 14 should be kept at 1 according to the datasheet
|
||||||
|
|
||||||
#if defined(I2C_USE_INTERRUPTS)
|
#if defined(I2C_USE_INTERRUPTS)
|
||||||
// Setting NVIC
|
// Setting NVIC
|
||||||
NVIC_SetPriority(I2C_IRQn, 1); // Match default priorities
|
NVIC_SetPriority(I2C1_EV_IRQn, 1); // Match default priorities
|
||||||
NVIC_EnableIRQ(I2C_IRQn);
|
NVIC_EnableIRQ(I2C1_EV_IRQn);
|
||||||
|
NVIC_SetPriority(I2C1_ER_IRQn, 1); // Match default priorities
|
||||||
|
NVIC_EnableIRQ(I2C1_ER_IRQn);
|
||||||
|
|
||||||
// CR2 Interrupt Settings
|
// CR2 Interrupt Settings
|
||||||
// Bit 15-13: reserved
|
// Bit 15-13: reserved
|
||||||
|
@ -172,23 +224,28 @@ void I2CManagerClass::I2C_init()
|
||||||
// Bit 8: ITERREN - Error interrupt enable
|
// Bit 8: ITERREN - Error interrupt enable
|
||||||
// Bit 7-6: reserved
|
// Bit 7-6: reserved
|
||||||
// Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz)
|
// Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz)
|
||||||
// s->CR2 |= 0x0700; // Enable Buffer, Event and Error interrupts
|
s->CR2 |= (I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable Buffer, Event and Error interrupts
|
||||||
s->CR2 |= 0x0300; // Enable Event and Error interrupts
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// DIAG(F("I2C_init() setting initial I2C clock to 100KHz"));
|
||||||
// Calculate baudrate and set default rate for now
|
// Calculate baudrate and set default rate for now
|
||||||
// Configure the Clock Control Register for 100KHz SCL frequency
|
// Configure the Clock Control Register for 100KHz SCL frequency
|
||||||
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
|
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
|
||||||
// Bit 14: Duty, fast mode duty cycle
|
// Bit 14: Duty, fast mode duty cycle
|
||||||
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
|
// Bit 11-0: so CCR divisor would be clk / 2 / 100000 (where clk is in Hz)
|
||||||
s->CCR = 0x0050;
|
// s->CCR = I2C_PERIPH_CLK * 5;
|
||||||
|
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
|
||||||
|
s->CCR |= (APB1clk1 / 2 / 100000UL); // Set a default of 100KHz I2C clockspeed to start!
|
||||||
|
|
||||||
// Configure the rise time register - max allowed in 1000ns
|
// Configure the rise time register - max allowed is 1000ns, so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
|
||||||
s->TRISE = 0x0011; // 1000 ns / 62.5 ns = 16 + 1
|
s->TRISE = (1000 * i2c_MHz / 1000) + 1;
|
||||||
|
|
||||||
|
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
|
||||||
|
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
|
||||||
|
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
|
||||||
|
|
||||||
// Enable the I2C master mode
|
// Enable the I2C master mode
|
||||||
s->CR1 |= I2C_CR1_PE; // Enable I2C
|
s->CR1 |= I2C_CR1_PE; // Enable I2C
|
||||||
// Setting bus idle mode and wait for sync
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
@ -198,49 +255,30 @@ void I2CManagerClass::I2C_sendStart() {
|
||||||
|
|
||||||
// Set counters here in case this is a retry.
|
// Set counters here in case this is a retry.
|
||||||
rxCount = txCount = 0;
|
rxCount = txCount = 0;
|
||||||
uint8_t temp;
|
|
||||||
|
|
||||||
// On a single-master I2C bus, the start bit won't be sent until the bus
|
// On a single-master I2C bus, the start bit won't be sent until the bus
|
||||||
// state goes to IDLE so we can request it without waiting. On a
|
// state goes to IDLE so we can request it without waiting. On a
|
||||||
// multi-master bus, the bus may be BUSY under control of another master,
|
// multi-master bus, the bus may be BUSY under control of another master,
|
||||||
// in which case we can avoid some arbitration failures by waiting until
|
// in which case we can avoid some arbitration failures by waiting until
|
||||||
// the bus state is IDLE. We don't do that here.
|
// the bus state is IDLE. We don't do that here.
|
||||||
|
//while (s->SR2 & I2C_SR2_BUSY) {}
|
||||||
|
|
||||||
// If anything to send, initiate write. Otherwise initiate read.
|
// Check there's no STOP still in progress. If we OR the START bit into CR1
|
||||||
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
|
// and the STOP bit is already set, we could output multiple STOP conditions.
|
||||||
{
|
while (s->CR1 & I2C_CR1_STOP) {} // Wait for STOP bit to reset
|
||||||
// Send start for read operation
|
|
||||||
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
|
s->CR2 |= (I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable interrupts
|
||||||
s->CR1 |= I2C_CR1_START; // Generate START
|
s->CR2 &= ~I2C_CR2_ITBUFEN; // Don't enable buffer interupts yet.
|
||||||
// Send address with read flag (1) or'd in
|
s->CR1 &= ~I2C_CR1_POS; // Clear the POS bit
|
||||||
s->DR = (deviceAddress << 1) | 1; // send the address
|
s->CR1 |= (I2C_CR1_ACK | I2C_CR1_START); // Enable the ACK and generate START
|
||||||
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
|
transactionState = TS_START;
|
||||||
// Special case for 1 byte reads!
|
|
||||||
if (bytesToReceive == 1)
|
|
||||||
{
|
|
||||||
s->CR1 &= ~I2C_CR1_ACK; // clear the ACK bit
|
|
||||||
temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition
|
|
||||||
s->CR1 |= I2C_CR1_STOP; // Stop I2C
|
|
||||||
}
|
|
||||||
else
|
|
||||||
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Send start for write operation
|
|
||||||
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
|
|
||||||
s->CR1 |= I2C_CR1_START; // Generate START
|
|
||||||
// Send address with write flag (0) or'd in
|
|
||||||
s->DR = (deviceAddress << 1) | 0; // send the address
|
|
||||||
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
|
|
||||||
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Initiate a stop bit for transmission (does not interrupt)
|
* Initiate a stop bit for transmission (does not interrupt)
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
void I2CManagerClass::I2C_sendStop() {
|
void I2CManagerClass::I2C_sendStop() {
|
||||||
s->CR1 |= I2C_CR1_STOP; // Stop I2C
|
s->CR1 |= I2C_CR1_STOP; // Stop I2C
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
@ -252,9 +290,11 @@ void I2CManagerClass::I2C_close() {
|
||||||
s->CR1 &= ~I2C_CR1_PE; // Disable I2C peripheral
|
s->CR1 &= ~I2C_CR1_PE; // Disable I2C peripheral
|
||||||
// Should never happen, but wait for up to 500us only.
|
// Should never happen, but wait for up to 500us only.
|
||||||
unsigned long startTime = micros();
|
unsigned long startTime = micros();
|
||||||
while ((s->CR1 && I2C_CR1_PE) != 0) {
|
while ((s->CR1 & I2C_CR1_PE) != 0) {
|
||||||
if (micros() - startTime >= 500UL) break;
|
if ((int32_t)(micros() - startTime) >= 500) break;
|
||||||
}
|
}
|
||||||
|
NVIC_DisableIRQ(I2C1_EV_IRQn);
|
||||||
|
NVIC_DisableIRQ(I2C1_ER_IRQn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
@ -263,50 +303,217 @@ void I2CManagerClass::I2C_close() {
|
||||||
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
|
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
void I2CManagerClass::I2C_handleInterrupt() {
|
void I2CManagerClass::I2C_handleInterrupt() {
|
||||||
|
volatile uint16_t temp_sr1, temp_sr2;
|
||||||
|
|
||||||
if (s->SR1 && I2C_SR1_ARLO) {
|
temp_sr1 = s->SR1;
|
||||||
// Arbitration lost, restart
|
|
||||||
I2C_sendStart(); // Reinitiate request
|
// Check for errors first
|
||||||
} else if (s->SR1 && I2C_SR1_BERR) {
|
if (temp_sr1 & (I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR)) {
|
||||||
// Bus error
|
// Check which error flag is set
|
||||||
completionStatus = I2C_STATUS_BUS_ERROR;
|
if (temp_sr1 & I2C_SR1_AF)
|
||||||
state = I2C_STATE_COMPLETED;
|
{
|
||||||
} else if (s->SR1 && I2C_SR1_TXE) {
|
s->SR1 &= ~(I2C_SR1_AF); // Clear AF
|
||||||
// Master write completed
|
I2C_sendStop(); // Clear the bus
|
||||||
if (s->SR1 && (1<<10)) {
|
transactionState = TS_IDLE;
|
||||||
// Nacked, send stop.
|
|
||||||
I2C_sendStop();
|
|
||||||
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
|
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
|
||||||
state = I2C_STATE_COMPLETED;
|
state = I2C_STATE_COMPLETED;
|
||||||
} else if (bytesToSend) {
|
|
||||||
// Acked, so send next byte
|
|
||||||
s->DR = sendBuffer[txCount++];
|
|
||||||
bytesToSend--;
|
|
||||||
} else if (bytesToReceive) {
|
|
||||||
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
|
|
||||||
// s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
|
|
||||||
} else {
|
|
||||||
// Check both TxE/BTF == 1 before generating stop
|
|
||||||
while (!(s->SR1 && I2C_SR1_TXE)); // Check TxE
|
|
||||||
while (!(s->SR1 && I2C_SR1_BTF)); // Check BTF
|
|
||||||
// No more data to send/receive. Initiate a STOP condition and finish
|
|
||||||
I2C_sendStop();
|
|
||||||
state = I2C_STATE_COMPLETED;
|
|
||||||
}
|
}
|
||||||
} else if (s->SR1 && I2C_SR1_RXNE) {
|
else if (temp_sr1 & I2C_SR1_ARLO)
|
||||||
// Master read completed without errors
|
{
|
||||||
if (bytesToReceive == 1) {
|
// Arbitration lost, restart
|
||||||
// s->I2CM.CTRLB.bit.ACKACT = 1; // NAK final byte
|
s->SR1 &= ~(I2C_SR1_ARLO); // Clear ARLO
|
||||||
I2C_sendStop(); // send stop
|
I2C_sendStart(); // Reinitiate request
|
||||||
receiveBuffer[rxCount++] = s->DR; // Store received byte
|
transactionState = TS_START;
|
||||||
bytesToReceive = 0;
|
}
|
||||||
|
else if (temp_sr1 & I2C_SR1_BERR)
|
||||||
|
{
|
||||||
|
// Bus error
|
||||||
|
s->SR1 &= ~(I2C_SR1_BERR); // Clear BERR
|
||||||
|
I2C_sendStop(); // Clear the bus
|
||||||
|
transactionState = TS_IDLE;
|
||||||
|
completionStatus = I2C_STATUS_BUS_ERROR;
|
||||||
state = I2C_STATE_COMPLETED;
|
state = I2C_STATE_COMPLETED;
|
||||||
} else if (bytesToReceive) {
|
|
||||||
// s->I2CM.CTRLB.bit.ACKACT = 0; // ACK all but final byte
|
|
||||||
receiveBuffer[rxCount++] = s->DR; // Store received byte
|
|
||||||
bytesToReceive--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// No error flags, so process event according to current state.
|
||||||
|
switch (transactionState) {
|
||||||
|
case TS_START:
|
||||||
|
if (temp_sr1 & I2C_SR1_SB) {
|
||||||
|
// Event EV5
|
||||||
|
// Start bit has been sent successfully and we have the bus.
|
||||||
|
// If anything to send, initiate write. Otherwise initiate read.
|
||||||
|
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend)) {
|
||||||
|
// Send address with read flag (1) or'd in
|
||||||
|
s->DR = (deviceAddress << 1) | 1; // send the address
|
||||||
|
transactionState = TS_R_ADDR;
|
||||||
|
} else {
|
||||||
|
// Send address with write flag (0) or'd in
|
||||||
|
s->DR = (deviceAddress << 1) | 0; // send the address
|
||||||
|
transactionState = TS_W_ADDR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SB bit is cleared by writing to DR (already done).
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_W_ADDR:
|
||||||
|
if (temp_sr1 & I2C_SR1_ADDR) {
|
||||||
|
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
|
||||||
|
// Event EV6
|
||||||
|
// Address sent successfully, device has ack'd in response.
|
||||||
|
if (!bytesToSend) {
|
||||||
|
I2C_sendStop();
|
||||||
|
transactionState = TS_IDLE;
|
||||||
|
completionStatus = I2C_STATUS_OK;
|
||||||
|
state = I2C_STATE_COMPLETED;
|
||||||
|
} else {
|
||||||
|
// Put one byte into DR to load shift register.
|
||||||
|
s->DR = sendBuffer[txCount++];
|
||||||
|
bytesToSend--;
|
||||||
|
if (bytesToSend) {
|
||||||
|
// Put another byte to load DR
|
||||||
|
s->DR = sendBuffer[txCount++];
|
||||||
|
bytesToSend--;
|
||||||
|
}
|
||||||
|
if (!bytesToSend) {
|
||||||
|
// No more bytes to send.
|
||||||
|
// The TXE interrupt occurs when the DR is empty, and the BTF interrupt
|
||||||
|
// occurs when the shift register is also empty (one character later).
|
||||||
|
// To avoid repeated TXE interrupts during this time, we disable TXE interrupt.
|
||||||
|
s->CR2 &= ~I2C_CR2_ITBUFEN; // Wait for BTF interrupt, disable TXE interrupt
|
||||||
|
transactionState = TS_W_STOP;
|
||||||
|
} else {
|
||||||
|
// More data remaining to send after this interrupt, enable TXE interrupt.
|
||||||
|
s->CR2 |= I2C_CR2_ITBUFEN;
|
||||||
|
transactionState = TS_W_DATA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_W_DATA:
|
||||||
|
if (temp_sr1 & I2C_SR1_TXE) {
|
||||||
|
// Event EV8_1/EV8
|
||||||
|
// Transmitter empty, write a byte to it.
|
||||||
|
if (bytesToSend) {
|
||||||
|
s->DR = sendBuffer[txCount++];
|
||||||
|
bytesToSend--;
|
||||||
|
if (!bytesToSend) {
|
||||||
|
s->CR2 &= ~I2C_CR2_ITBUFEN; // Disable TXE interrupt
|
||||||
|
transactionState = TS_W_STOP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_W_STOP:
|
||||||
|
if (temp_sr1 & I2C_SR1_BTF) {
|
||||||
|
// Event EV8_2
|
||||||
|
// Done, last character sent. Anything to receive?
|
||||||
|
if (bytesToReceive) {
|
||||||
|
I2C_sendStart();
|
||||||
|
// NOTE: Three redundant BTF interrupts take place between the
|
||||||
|
// first BTF interrupt and the START interrupt. I've tried all sorts
|
||||||
|
// of ways to eliminate them, and the only thing that worked for
|
||||||
|
// me was to loop until the BTF bit becomes reset. Either way,
|
||||||
|
// it's a waste of processor time. Anyone got a solution?
|
||||||
|
//while (s->SR1 && I2C_SR1_BTF) {}
|
||||||
|
transactionState = TS_START;
|
||||||
|
} else {
|
||||||
|
I2C_sendStop();
|
||||||
|
transactionState = TS_IDLE;
|
||||||
|
completionStatus = I2C_STATUS_OK;
|
||||||
|
state = I2C_STATE_COMPLETED;
|
||||||
|
}
|
||||||
|
s->SR1 &= I2C_SR1_BTF; // Clear BTF interrupt
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_R_ADDR:
|
||||||
|
if (temp_sr1 & I2C_SR1_ADDR) {
|
||||||
|
// Event EV6
|
||||||
|
// Address sent for receive.
|
||||||
|
// The next bit is different depending on whether there are
|
||||||
|
// 1 byte, 2 bytes or >2 bytes to be received, in accordance with the
|
||||||
|
// Programmers Reference RM0390.
|
||||||
|
if (bytesToReceive == 1) {
|
||||||
|
// Receive 1 byte
|
||||||
|
s->CR1 &= ~I2C_CR1_ACK; // Disable ack
|
||||||
|
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
|
||||||
|
// Next step will occur after a RXNE interrupt, so enable it
|
||||||
|
s->CR2 |= I2C_CR2_ITBUFEN;
|
||||||
|
transactionState = TS_R_STOP;
|
||||||
|
} else if (bytesToReceive == 2) {
|
||||||
|
// Receive 2 bytes
|
||||||
|
s->CR1 &= ~I2C_CR1_ACK; // Disable ACK for final byte
|
||||||
|
s->CR1 |= I2C_CR1_POS; // set POS flag to delay effect of ACK flag
|
||||||
|
// Next step will occur after a BTF interrupt, so disable RXNE interrupt
|
||||||
|
s->CR2 &= ~I2C_CR2_ITBUFEN;
|
||||||
|
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
|
||||||
|
transactionState = TS_R_STOP;
|
||||||
|
} else {
|
||||||
|
// >2 bytes, just wait for bytes to come in and ack them for the time being
|
||||||
|
// (ack flag has already been set).
|
||||||
|
// Next step will occur after a BTF interrupt, so disable RXNE interrupt
|
||||||
|
s->CR2 &= ~I2C_CR2_ITBUFEN;
|
||||||
|
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
|
||||||
|
transactionState = TS_R_DATA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_R_DATA:
|
||||||
|
// Event EV7/EV7_1
|
||||||
|
if (temp_sr1 & I2C_SR1_BTF) {
|
||||||
|
// Byte received in receiver - read next byte
|
||||||
|
if (bytesToReceive == 3) {
|
||||||
|
// Getting close to the last byte, so a specific sequence is recommended.
|
||||||
|
s->CR1 &= ~I2C_CR1_ACK; // Reset ack for next byte received.
|
||||||
|
transactionState = TS_R_STOP;
|
||||||
|
}
|
||||||
|
receiveBuffer[rxCount++] = s->DR; // Store received byte
|
||||||
|
bytesToReceive--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TS_R_STOP:
|
||||||
|
if (temp_sr1 & I2C_SR1_BTF) {
|
||||||
|
// Event EV7 (last one)
|
||||||
|
// When we've got here, the receiver has got the last two bytes
|
||||||
|
// (or one byte, if only one byte is being received),
|
||||||
|
// and NAK has already been sent, so we need to read from the receiver.
|
||||||
|
if (bytesToReceive) {
|
||||||
|
if (bytesToReceive > 1)
|
||||||
|
I2C_sendStop();
|
||||||
|
while(bytesToReceive) {
|
||||||
|
receiveBuffer[rxCount++] = s->DR; // Store received byte(s)
|
||||||
|
bytesToReceive--;
|
||||||
|
}
|
||||||
|
// Finish.
|
||||||
|
transactionState = TS_IDLE;
|
||||||
|
completionStatus = I2C_STATUS_OK;
|
||||||
|
state = I2C_STATE_COMPLETED;
|
||||||
|
}
|
||||||
|
} else if (temp_sr1 & I2C_SR1_RXNE) {
|
||||||
|
if (bytesToReceive == 1) {
|
||||||
|
// One byte on a single-byte transfer. Ack has already been set.
|
||||||
|
I2C_sendStop();
|
||||||
|
receiveBuffer[rxCount++] = s->DR; // Store received byte
|
||||||
|
bytesToReceive--;
|
||||||
|
// Finish.
|
||||||
|
transactionState = TS_IDLE;
|
||||||
|
completionStatus = I2C_STATUS_OK;
|
||||||
|
state = I2C_STATE_COMPLETED;
|
||||||
|
} else
|
||||||
|
s->SR1 &= I2C_SR1_RXNE; // Acknowledge interrupt
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If we've received an interrupt at any other time, we're not interested so clear it
|
||||||
|
// to prevent it recurring ad infinitum.
|
||||||
|
s->SR1 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* I2CMANAGER_STM32_H */
|
#endif /* I2CMANAGER_STM32_H */
|
||||||
|
|
57
IODevice.cpp
57
IODevice.cpp
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
// Link to halSetup function. If not defined, the function reference will be NULL.
|
// Link to halSetup function. If not defined, the function reference will be NULL.
|
||||||
extern __attribute__((weak)) void halSetup();
|
extern __attribute__((weak)) void halSetup();
|
||||||
extern __attribute__((weak)) void exrailHalSetup();
|
extern __attribute__((weak)) bool exrailHalSetup();
|
||||||
|
|
||||||
//==================================================================================================================
|
//==================================================================================================================
|
||||||
// Static methods
|
// Static methods
|
||||||
|
@ -66,38 +66,35 @@ void IODevice::begin() {
|
||||||
halSetup();
|
halSetup();
|
||||||
|
|
||||||
// include any HAL devices defined in exrail.
|
// include any HAL devices defined in exrail.
|
||||||
|
bool ignoreDefaults=false;
|
||||||
if (exrailHalSetup)
|
if (exrailHalSetup)
|
||||||
exrailHalSetup();
|
ignoreDefaults=exrailHalSetup();
|
||||||
|
if (ignoreDefaults) return;
|
||||||
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
|
|
||||||
// Allocates 32 pins 100-131
|
|
||||||
if (checkNoOverlap(100, 16, 0x40)) {
|
|
||||||
PCA9685::create(100, 16, 0x40);
|
|
||||||
} else {
|
|
||||||
DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device"));
|
|
||||||
}
|
|
||||||
if (checkNoOverlap(116, 16, 0x41)) {
|
|
||||||
PCA9685::create(116, 16, 0x41);
|
|
||||||
} else {
|
|
||||||
DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef FAST_CLOCK_I2C
|
#ifdef FAST_CLOCK_I2C
|
||||||
DIAG(F("EXFastClock::create"));
|
// DIAG(F("EXFastClock::create"));
|
||||||
EXFastClock::create(FAST_CLOCK_I2C);
|
EXFastClock::create(FAST_CLOCK_I2C);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
||||||
// Allocates 32 pins 164-195
|
// Allocates 32 pins 164-195
|
||||||
if (checkNoOverlap(164, 16, 0x20)) {
|
if (checkNoOverlap(164, 16, 0x20, silent)) {
|
||||||
MCP23017::create(164, 16, 0x20);
|
MCP23017::create(164, 16, 0x20);
|
||||||
} else {
|
|
||||||
DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device"));
|
|
||||||
}
|
}
|
||||||
if (checkNoOverlap(180, 16, 0x21)) {
|
|
||||||
|
if (checkNoOverlap(180, 16, 0x21, silent)) {
|
||||||
MCP23017::create(180, 16, 0x21);
|
MCP23017::create(180, 16, 0x21);
|
||||||
} else {
|
|
||||||
DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +184,13 @@ bool IODevice::exists(VPIN vpin) {
|
||||||
return findDevice(vpin) != NULL;
|
return findDevice(vpin) != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the status of the device att vpin.
|
||||||
|
uint8_t IODevice::getStatus(VPIN vpin) {
|
||||||
|
IODevice *dev = findDevice(vpin);
|
||||||
|
if (!dev) return false;
|
||||||
|
return dev->_deviceState;
|
||||||
|
}
|
||||||
|
|
||||||
// check whether the pin supports notification. If so, then regular _read calls are not required.
|
// check whether the pin supports notification. If so, then regular _read calls are not required.
|
||||||
bool IODevice::hasCallback(VPIN vpin) {
|
bool IODevice::hasCallback(VPIN vpin) {
|
||||||
IODevice *dev = findDevice(vpin);
|
IODevice *dev = findDevice(vpin);
|
||||||
|
@ -343,7 +347,10 @@ IODevice *IODevice::findDeviceFollowing(VPIN vpin) {
|
||||||
// returns true if pins DONT overlap with existing device
|
// returns true if pins DONT overlap with existing device
|
||||||
// TODO: Move the I2C address reservation and checks into the I2CManager code.
|
// TODO: Move the I2C address reservation and checks into the I2CManager code.
|
||||||
// That will enable non-HAL devices to reserve I2C addresses too.
|
// That will enable non-HAL devices to reserve I2C addresses too.
|
||||||
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) {
|
// Silent is used by the default setup so that there is no message if the default
|
||||||
|
// device has already been handled by the user setup.
|
||||||
|
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins,
|
||||||
|
I2CAddress i2cAddress, bool silent) {
|
||||||
#ifdef DIAG_IO
|
#ifdef DIAG_IO
|
||||||
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
|
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
|
||||||
#endif
|
#endif
|
||||||
|
@ -356,14 +363,14 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddres
|
||||||
VPIN lastDevPin=firstDevPin+dev->_nPins-1;
|
VPIN lastDevPin=firstDevPin+dev->_nPins-1;
|
||||||
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
|
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
|
||||||
if (!noOverlap) {
|
if (!noOverlap) {
|
||||||
DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
|
if (!silent) DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
|
||||||
firstPin, lastPin);
|
firstPin, lastPin);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check for overlapping I2C address
|
// Check for overlapping I2C address
|
||||||
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
|
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
|
||||||
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
|
if (!silent) DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
IODevice.h
19
IODevice.h
|
@ -27,12 +27,6 @@
|
||||||
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
|
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
|
||||||
//#define DIAG_LOOPTIMES
|
//#define DIAG_LOOPTIMES
|
||||||
|
|
||||||
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
|
|
||||||
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
|
|
||||||
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
|
|
||||||
#define IO_NO_HAL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
|
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
|
||||||
// animation has completed. This switches off the servo motor, preventing
|
// animation has completed. This switches off the servo motor, preventing
|
||||||
// the continuous buzz sometimes found on servos, and reducing the
|
// the continuous buzz sometimes found on servos, and reducing the
|
||||||
|
@ -160,6 +154,9 @@ public:
|
||||||
// exists checks whether there is a device owning the specified vpin
|
// exists checks whether there is a device owning the specified vpin
|
||||||
static bool exists(VPIN vpin);
|
static bool exists(VPIN vpin);
|
||||||
|
|
||||||
|
// getStatus returns the state of the device at the specified vpin
|
||||||
|
static uint8_t getStatus(VPIN vpin);
|
||||||
|
|
||||||
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
|
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
|
||||||
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
|
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
|
||||||
// Without the shared interrupt, input states are scanned periodically to detect changes on
|
// Without the shared interrupt, input states are scanned periodically to detect changes on
|
||||||
|
@ -169,7 +166,8 @@ public:
|
||||||
void setGPIOInterruptPin(int16_t pinNumber);
|
void setGPIOInterruptPin(int16_t pinNumber);
|
||||||
|
|
||||||
// Method to check if pins will overlap before creating new device.
|
// Method to check if pins will overlap before creating new device.
|
||||||
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=0);
|
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1,
|
||||||
|
I2CAddress i2cAddress=0, bool silent=false);
|
||||||
|
|
||||||
// Method used by IODevice filters to locate slave pins that may be overlayed by their own
|
// Method used by IODevice filters to locate slave pins that may be overlayed by their own
|
||||||
// pin range.
|
// pin range.
|
||||||
|
@ -383,6 +381,7 @@ private:
|
||||||
uint8_t *_pinInUse;
|
uint8_t *_pinInUse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/*
|
/*
|
||||||
* IODevice subclass for EX-Turntable.
|
* IODevice subclass for EX-Turntable.
|
||||||
|
@ -411,10 +410,14 @@ private:
|
||||||
void _begin() override;
|
void _begin() override;
|
||||||
void _loop(unsigned long currentMicros) override;
|
void _loop(unsigned long currentMicros) override;
|
||||||
int _read(VPIN vpin) override;
|
int _read(VPIN vpin) override;
|
||||||
|
void _broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity);
|
||||||
void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override;
|
void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override;
|
||||||
void _display() override;
|
void _display() override;
|
||||||
uint8_t _stepperStatus;
|
uint8_t _stepperStatus;
|
||||||
|
uint8_t _previousStatus;
|
||||||
|
uint8_t _currentActivity;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -540,8 +543,10 @@ protected:
|
||||||
#include "IO_MCP23017.h"
|
#include "IO_MCP23017.h"
|
||||||
#include "IO_PCF8574.h"
|
#include "IO_PCF8574.h"
|
||||||
#include "IO_PCF8575.h"
|
#include "IO_PCF8575.h"
|
||||||
|
#include "IO_PCA9555.h"
|
||||||
#include "IO_duinoNodes.h"
|
#include "IO_duinoNodes.h"
|
||||||
#include "IO_EXIOExpander.h"
|
#include "IO_EXIOExpander.h"
|
||||||
|
#include "IO_trainbrains.h"
|
||||||
|
|
||||||
|
|
||||||
#endif // iodevice_h
|
#endif // iodevice_h
|
||||||
|
|
|
@ -51,6 +51,7 @@ static void create(I2CAddress i2cAddress) {
|
||||||
// Start by assuming we will find the clock
|
// Start by assuming we will find the clock
|
||||||
// Check if specified I2C address is responding (blocking operation)
|
// Check if specified I2C address is responding (blocking operation)
|
||||||
// Returns I2C_STATUS_OK (0) if OK, or error code.
|
// Returns I2C_STATUS_OK (0) if OK, or error code.
|
||||||
|
I2CManager.begin();
|
||||||
uint8_t _checkforclock = I2CManager.checkAddress(i2cAddress);
|
uint8_t _checkforclock = I2CManager.checkAddress(i2cAddress);
|
||||||
DIAG(F("Clock check result - %d"), _checkforclock);
|
DIAG(F("Clock check result - %d"), _checkforclock);
|
||||||
// XXXX change thistosave2 bytes
|
// XXXX change thistosave2 bytes
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* © 2022, Peter Cole. All rights reserved.
|
* © 2022, Peter Cole. All rights reserved.
|
||||||
* © 2024, Harald Barth. All rights reserved.
|
* © 2024, Harald Barth. All rights reserved.
|
||||||
|
* © 2024, Harald Barth. All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of EX-CommandStation
|
* This file is part of EX-CommandStation
|
||||||
*
|
*
|
||||||
|
@ -23,13 +24,10 @@
|
||||||
* This device driver will configure the device on startup, along with
|
* This device driver will configure the device on startup, along with
|
||||||
* interacting with the device for all input/output duties.
|
* interacting with the device for all input/output duties.
|
||||||
*
|
*
|
||||||
* To create EX-IOExpander devices, these are defined in myHal.cpp:
|
* To create EX-IOExpander devices, these are defined in myAutomation.h:
|
||||||
* (Note the device driver is included by default)
|
* (Note the device driver is included by default)
|
||||||
*
|
*
|
||||||
* void halSetup() {
|
* HAL(EXIOExpander,800,18,0x65)
|
||||||
* // EXIOExpander::create(vpin, num_vpins, i2c_address);
|
|
||||||
* EXIOExpander::create(800, 18, 0x65);
|
|
||||||
* }
|
|
||||||
*
|
*
|
||||||
* All pins on an EX-IOExpander device are allocated according to the pin map for the specific
|
* All pins on an EX-IOExpander device are allocated according to the pin map for the specific
|
||||||
* device in use. There is no way for the device driver to sanity check pins are used for the
|
* device in use. There is no way for the device driver to sanity check pins are used for the
|
||||||
|
|
|
@ -20,20 +20,21 @@
|
||||||
/*
|
/*
|
||||||
* The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C.
|
* The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C.
|
||||||
*
|
*
|
||||||
* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/Turntable-EX) and contains the stepper motor logic.
|
* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/EX-Turntable) and contains the stepper motor logic.
|
||||||
*
|
*
|
||||||
* This device driver sends a step position to Turntable-EX to indicate the step position to move to using either of these commands:
|
* This device driver sends a step position to EX-Turntable to indicate the step position to move to using either of these commands:
|
||||||
* <D TT vpin steps activity> in the serial console
|
* <D TT vpin steps activity> in the serial console
|
||||||
* MOVETT(vpin, steps, activity) in EX-RAIL
|
* MOVETT(vpin, steps, activity) in EX-RAIL
|
||||||
* Refer to the documentation for further information including the valid activities.
|
* Refer to the documentation for further information including the valid activities.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IO_EXTurntable_h
|
|
||||||
#define IO_EXTurntable_h
|
|
||||||
|
|
||||||
#include "IODevice.h"
|
#include "IODevice.h"
|
||||||
#include "I2CManager.h"
|
#include "I2CManager.h"
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
|
#include "Turntables.h"
|
||||||
|
#include "CommandDistributor.h"
|
||||||
|
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
|
||||||
void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
|
void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
|
||||||
new EXTurntable(firstVpin, nPins, I2CAddress);
|
new EXTurntable(firstVpin, nPins, I2CAddress);
|
||||||
|
@ -44,6 +45,8 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
|
||||||
_firstVpin = firstVpin;
|
_firstVpin = firstVpin;
|
||||||
_nPins = nPins;
|
_nPins = nPins;
|
||||||
_I2CAddress = I2CAddress;
|
_I2CAddress = I2CAddress;
|
||||||
|
_stepperStatus = 0;
|
||||||
|
_previousStatus = 0;
|
||||||
addDevice(this);
|
addDevice(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +54,7 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
|
||||||
void EXTurntable::_begin() {
|
void EXTurntable::_begin() {
|
||||||
I2CManager.begin();
|
I2CManager.begin();
|
||||||
if (I2CManager.exists(_I2CAddress)) {
|
if (I2CManager.exists(_I2CAddress)) {
|
||||||
|
DIAG(F("EX-Turntable device found, I2C:%s"), _I2CAddress.toString());
|
||||||
#ifdef DIAG_IO
|
#ifdef DIAG_IO
|
||||||
_display();
|
_display();
|
||||||
#endif
|
#endif
|
||||||
|
@ -67,15 +71,20 @@ void EXTurntable::_loop(unsigned long currentMicros) {
|
||||||
uint8_t readBuffer[1];
|
uint8_t readBuffer[1];
|
||||||
I2CManager.read(_I2CAddress, readBuffer, 1);
|
I2CManager.read(_I2CAddress, readBuffer, 1);
|
||||||
_stepperStatus = readBuffer[0];
|
_stepperStatus = readBuffer[0];
|
||||||
// DIAG(F("Turntable-EX returned status: %d"), _stepperStatus);
|
if (_stepperStatus != _previousStatus && _stepperStatus == 0) { // Broadcast when a rotation finishes
|
||||||
delayUntil(currentMicros + 500000); // Wait 500ms before checking again, turntables turn slowly
|
if ( _currentActivity < 4) {
|
||||||
|
_broadcastStatus(_firstVpin, _stepperStatus, _currentActivity);
|
||||||
|
}
|
||||||
|
_previousStatus = _stepperStatus;
|
||||||
|
}
|
||||||
|
delayUntil(currentMicros + 100000); // Wait 100ms before checking again
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read returns status as obtained in our loop.
|
// Read returns status as obtained in our loop.
|
||||||
// Return false if our status value is invalid.
|
// Return false if our status value is invalid.
|
||||||
int EXTurntable::_read(VPIN vpin) {
|
int EXTurntable::_read(VPIN vpin) {
|
||||||
|
(void)vpin; // surpress warning
|
||||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||||
// DIAG(F("_read status: %d"), _stepperStatus);
|
|
||||||
if (_stepperStatus > 1) {
|
if (_stepperStatus > 1) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -83,6 +92,17 @@ int EXTurntable::_read(VPIN vpin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a status change has occurred for a turntable object, broadcast it
|
||||||
|
void EXTurntable::_broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity) {
|
||||||
|
Turntable *tto = Turntable::getByVpin(vpin);
|
||||||
|
if (tto) {
|
||||||
|
if (activity < 4) {
|
||||||
|
tto->setMoving(status);
|
||||||
|
CommandDistributor::broadcastTurntable(tto->getId(), tto->getPosition(), status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// writeAnalogue to send the steps and activity to Turntable-EX.
|
// writeAnalogue to send the steps and activity to Turntable-EX.
|
||||||
// Sends 3 bytes containing the MSB and LSB of the step count, and activity.
|
// Sends 3 bytes containing the MSB and LSB of the step count, and activity.
|
||||||
// value contains the steps, bit shifted to MSB + LSB.
|
// value contains the steps, bit shifted to MSB + LSB.
|
||||||
|
@ -100,6 +120,7 @@ int EXTurntable::_read(VPIN vpin) {
|
||||||
// Acc_Off = 9 // Turn accessory pin off
|
// Acc_Off = 9 // Turn accessory pin off
|
||||||
void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) {
|
void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) {
|
||||||
if (_deviceState == DEVSTATE_FAILED) return;
|
if (_deviceState == DEVSTATE_FAILED) return;
|
||||||
|
if (value < 0) return;
|
||||||
uint8_t stepsMSB = value >> 8;
|
uint8_t stepsMSB = value >> 8;
|
||||||
uint8_t stepsLSB = value & 0xFF;
|
uint8_t stepsLSB = value & 0xFF;
|
||||||
#ifdef DIAG_IO
|
#ifdef DIAG_IO
|
||||||
|
@ -107,8 +128,13 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_
|
||||||
vpin, value, activity, duration);
|
vpin, value, activity, duration);
|
||||||
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
|
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
|
||||||
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
|
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
|
||||||
|
#else
|
||||||
|
(void)duration;
|
||||||
#endif
|
#endif
|
||||||
_stepperStatus = 1; // Tell the device driver Turntable-EX is busy
|
if (activity < 4) _stepperStatus = 1; // Tell the device driver Turntable-EX is busy
|
||||||
|
_previousStatus = _stepperStatus;
|
||||||
|
_currentActivity = activity;
|
||||||
|
_broadcastStatus(vpin, _stepperStatus, activity); // Broadcast when the rotation starts
|
||||||
I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);
|
I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* © 2023, Neil McKechnie. All rights reserved.
|
* © 2024, Paul Antoine
|
||||||
|
* © 2023, Neil McKechnie
|
||||||
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of DCC++EX API
|
* This file is part of DCC-EX API
|
||||||
*
|
*
|
||||||
* This is free software: you can redistribute it and/or modify
|
* This is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -112,13 +114,14 @@ protected:
|
||||||
// Fill buffer with spaces
|
// Fill buffer with spaces
|
||||||
memset(_buffer, ' ', _numCols*_numRows);
|
memset(_buffer, ' ', _numCols*_numRows);
|
||||||
|
|
||||||
_displayDriver->clearNative();
|
|
||||||
|
|
||||||
// Add device to list of HAL devices (not necessary but allows
|
// Add device to list of HAL devices (not necessary but allows
|
||||||
// status to be displayed using <D HAL SHOW> and device to be
|
// status to be displayed using <D HAL SHOW> and device to be
|
||||||
// reinitialised using <D HAL RESET>).
|
// reinitialised using <D HAL RESET>).
|
||||||
IODevice::addDevice(this);
|
IODevice::addDevice(this);
|
||||||
|
|
||||||
|
// Moved after addDevice() to ensure I2CManager.begin() has been called fisrt
|
||||||
|
_displayDriver->clearNative();
|
||||||
|
|
||||||
// Also add this display to list of display handlers
|
// Also add this display to list of display handlers
|
||||||
DisplayInterface::addDisplay(displayNo);
|
DisplayInterface::addDisplay(displayNo);
|
||||||
|
|
||||||
|
|
805
IO_I2CDFPlayer.h
Normal file
805
IO_I2CDFPlayer.h
Normal file
|
@ -0,0 +1,805 @@
|
||||||
|
/*
|
||||||
|
* © 2023, Neil McKechnie. All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of DCC++EX API
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DFPlayer is an MP3 player module with an SD card holder. It also has an integrated
|
||||||
|
* amplifier, so it only needs a power supply and a speaker.
|
||||||
|
* This driver is a modified version of the IO_DFPlayer.h file
|
||||||
|
* *********************************************************************************************
|
||||||
|
*
|
||||||
|
* Dec 2023, Added NXP SC16IS752 I2C Dual UART to enable the DFPlayer connection over the I2C bus
|
||||||
|
* The SC16IS752 has 64 bytes TX & RX FIFO buffer
|
||||||
|
* First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be
|
||||||
|
* needed as the RX Fifo holds the reply
|
||||||
|
*
|
||||||
|
* Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work but the first transmit
|
||||||
|
* corrupt data. This need more analysis and experimenatation.
|
||||||
|
* Will push this driver to the dev branch with the uart fixed to 0
|
||||||
|
* Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)
|
||||||
|
*
|
||||||
|
* myHall.cpp configuration syntax:
|
||||||
|
*
|
||||||
|
* I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
|
||||||
|
* vPins : Total number of virtual pins allocated (2 vPins are supported, one for each UART)
|
||||||
|
* 1st vPin for UART 0, 2nd for UART 1
|
||||||
|
* I2C Address : I2C address of the serial controller, in 0x format
|
||||||
|
* xtal : 0 for 1,8432Mhz, 1 for 14,7456Mhz
|
||||||
|
*
|
||||||
|
* The vPin is also a pin that can be read, it indicate if the DFPlayer has finished playing a track
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef IO_I2CDFPlayer_h
|
||||||
|
#define IO_I2CDFPlayer_h
|
||||||
|
|
||||||
|
#include "IODevice.h"
|
||||||
|
#include "I2CManager.h"
|
||||||
|
#include "DIAG.h"
|
||||||
|
|
||||||
|
// Debug and diagnostic defines, enable too many will result in slowing the driver
|
||||||
|
//#define DIAG_I2CDFplayer
|
||||||
|
//#define DIAG_I2CDFplayer_data
|
||||||
|
//#define DIAG_I2CDFplayer_reg
|
||||||
|
//#define DIAG_I2CDFplayer_playing
|
||||||
|
|
||||||
|
class I2CDFPlayer : public IODevice {
|
||||||
|
private:
|
||||||
|
const uint8_t MAXVOLUME=30;
|
||||||
|
uint8_t RETRYCOUNT = 0x03;
|
||||||
|
bool _playing = false;
|
||||||
|
uint8_t _inputIndex = 0;
|
||||||
|
unsigned long _commandSendTime; // Time (us) that last transmit took place.
|
||||||
|
unsigned long _timeoutTime;
|
||||||
|
uint8_t _recvCMD; // Last received command code byte
|
||||||
|
bool _awaitingResponse = false;
|
||||||
|
uint8_t _retryCounter = RETRYCOUNT; // Max retries before timing out
|
||||||
|
uint8_t _requestedVolumeLevel = MAXVOLUME;
|
||||||
|
uint8_t _currentVolume = MAXVOLUME;
|
||||||
|
int _requestedSong = -1; // -1=none, 0=stop, >0=file number
|
||||||
|
bool _repeat = false; // audio file is repeat playing
|
||||||
|
uint8_t _previousCmd = true;
|
||||||
|
// SC16IS752 defines
|
||||||
|
I2CAddress _I2CAddress;
|
||||||
|
I2CRB _rb;
|
||||||
|
uint8_t _UART_CH=0x00; // Fix uart ch to 0 for now
|
||||||
|
// Communication parameters for the DFPlayer are fixed at 8 bit, No parity, 1 stopbit
|
||||||
|
uint8_t WORD_LEN = 0x03; // Value LCR bit 0,1
|
||||||
|
uint8_t STOP_BIT = 0x00; // Value LCR bit 2
|
||||||
|
uint8_t PARITY_ENA = 0x00; // Value LCR bit 3
|
||||||
|
uint8_t PARITY_TYPE = 0x00; // Value LCR bit 4
|
||||||
|
uint32_t BAUD_RATE = 9600;
|
||||||
|
uint8_t PRESCALER = 0x01; // Value MCR bit 7
|
||||||
|
uint8_t TEMP_REG_VAL = 0x00;
|
||||||
|
uint8_t FIFO_RX_LEVEL = 0x00;
|
||||||
|
uint8_t RX_BUFFER = 0x00; // nr of bytes copied into _inbuffer
|
||||||
|
uint8_t FIFO_TX_LEVEL = 0x00;
|
||||||
|
bool _playCmd = false;
|
||||||
|
bool _volCmd = false;
|
||||||
|
bool _folderCmd = false;
|
||||||
|
uint8_t _requestedFolder = 0x01; // default to folder 01
|
||||||
|
uint8_t _currentFolder = 0x01; // default to folder 01
|
||||||
|
bool _repeatCmd = false;
|
||||||
|
bool _stopplayCmd = false;
|
||||||
|
bool _resetCmd = false;
|
||||||
|
bool _eqCmd = false;
|
||||||
|
uint8_t _requestedEQValue = DF_NORMAL;
|
||||||
|
uint8_t _currentEQvalue = DF_NORMAL; // start equalizer value
|
||||||
|
bool _daconCmd = false;
|
||||||
|
uint8_t _audioMixer = 0x01; // Default to output amplifier 1
|
||||||
|
bool _setamCmd = false; // Set the Audio mixer channel
|
||||||
|
uint8_t _outbuffer [11]; // DFPlayer command is 10 bytes + 1 byte register address & UART channel
|
||||||
|
uint8_t _inbuffer[10]; // expected DFPlayer return 10 bytes
|
||||||
|
|
||||||
|
unsigned long _sc16is752_xtal_freq;
|
||||||
|
unsigned long SC16IS752_XTAL_FREQ_LOW = 1843200; // To support cheap eBay/AliExpress SC16IS752 boards
|
||||||
|
unsigned long SC16IS752_XTAL_FREQ_HIGH = 14745600; // Support for higher baud rates, standard for modular EX-IO system
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
I2CDFPlayer(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal){
|
||||||
|
_firstVpin = firstVpin;
|
||||||
|
_nPins = nPins;
|
||||||
|
_I2CAddress = i2cAddress;
|
||||||
|
if (xtal == 0){
|
||||||
|
_sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_LOW;
|
||||||
|
} else { // should be 1
|
||||||
|
_sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_HIGH;
|
||||||
|
}
|
||||||
|
addDevice(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal) {
|
||||||
|
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new I2CDFPlayer(firstVpin, nPins, i2cAddress, xtal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _begin() override {
|
||||||
|
// check if SC16IS752 exist first, initialize and then resume DFPlayer init via SC16IS752
|
||||||
|
I2CManager.begin();
|
||||||
|
I2CManager.setClock(1000000);
|
||||||
|
if (I2CManager.exists(_I2CAddress)){
|
||||||
|
DIAG(F("SC16IS752 I2C:%s UART detected"), _I2CAddress.toString());
|
||||||
|
Init_SC16IS752(); // Initialize UART
|
||||||
|
if (_deviceState == DEVSTATE_FAILED){
|
||||||
|
DIAG(F("SC16IS752 I2C:%s UART initialization failed"), _I2CAddress.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DIAG(F("SC16IS752 I2C:%s UART not detected"), _I2CAddress.toString());
|
||||||
|
}
|
||||||
|
#if defined(DIAG_IO)
|
||||||
|
_display();
|
||||||
|
#endif
|
||||||
|
// Now init DFPlayer
|
||||||
|
// Send a query to the device to see if it responds
|
||||||
|
_deviceState = DEVSTATE_INITIALISING;
|
||||||
|
sendPacket(0x42,0,0);
|
||||||
|
_timeoutTime = micros() + 5000000UL; // 5 second timeout
|
||||||
|
_awaitingResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _loop(unsigned long currentMicros) override {
|
||||||
|
// Read responses from device
|
||||||
|
uint8_t status = _rb.status;
|
||||||
|
if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything
|
||||||
|
if (status == I2C_STATUS_OK) {
|
||||||
|
processIncoming(currentMicros);
|
||||||
|
// Check if a command sent to device has timed out. Allow 0.5 second for response
|
||||||
|
// added retry counter, sometimes we do not sent keep alive due to other commands sent to DFPlayer
|
||||||
|
if (_awaitingResponse && (int32_t)(currentMicros - _timeoutTime) > 0) { // timeout triggered
|
||||||
|
if(_retryCounter == 0){ // retry counter out of luck, must take the device to failed state
|
||||||
|
DIAG(F("I2CDFPlayer:%s, DFPlayer not responding on UART channel: 0x%x"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
_deviceState = DEVSTATE_FAILED;
|
||||||
|
_awaitingResponse = false;
|
||||||
|
_playing = false;
|
||||||
|
_retryCounter = RETRYCOUNT;
|
||||||
|
} else { // timeout and retry protection and recovery of corrupt data frames from DFPlayer
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: %s, DFPlayer timout, retry counter: %d on UART channel: 0x%x"), _I2CAddress.toString(), _retryCounter, _UART_CH);
|
||||||
|
#endif
|
||||||
|
_timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds// reset timeout
|
||||||
|
_awaitingResponse = false; // trigger sending a keep alive 0x42 in processOutgoing()
|
||||||
|
_retryCounter --; // decrement retry counter
|
||||||
|
resetRX_fifo(); // reset the RX fifo as it has corrupt data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status = _rb.status;
|
||||||
|
if (status == I2C_STATUS_PENDING) return; // Busy, try next time
|
||||||
|
if (status == I2C_STATUS_OK) {
|
||||||
|
// Send any commands that need to go.
|
||||||
|
processOutgoing(currentMicros);
|
||||||
|
}
|
||||||
|
delayUntil(currentMicros + 10000); // Only enter every 10ms
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check for incoming data, and update busy flag and other state accordingly
|
||||||
|
|
||||||
|
void processIncoming(unsigned long currentMicros) {
|
||||||
|
// Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF"
|
||||||
|
RX_fifo_lvl();
|
||||||
|
if (FIFO_RX_LEVEL >= 10) {
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
DIAG(F("I2CDFPlayer: %s Retrieving data from RX Fifo on UART_CH: 0x%x FIFO_RX_LEVEL: %d"),_I2CAddress.toString(), _UART_CH, FIFO_RX_LEVEL);
|
||||||
|
#endif
|
||||||
|
_outbuffer[0] = REG_RHR << 3 | _UART_CH << 1;
|
||||||
|
// Only copy 10 bytes from RX FIFO, there maybe additional partial return data after a track is finished playing in the RX FIFO
|
||||||
|
I2CManager.read(_I2CAddress, _inbuffer, 10, _outbuffer, 1); // inbuffer[] has the data now
|
||||||
|
//delayUntil(currentMicros + 10000); // Allow time to get the data
|
||||||
|
RX_BUFFER = 10; // We have copied 10 bytes from RX FIFO to _inbuffer
|
||||||
|
#ifdef DIAG_I2CDFplayer_data
|
||||||
|
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, RX FIFO Data"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
for (int i = 0; i < sizeof _inbuffer; i++){
|
||||||
|
DIAG(F("SC16IS752: Data _inbuffer[0x%x]: 0x%x"), i, _inbuffer[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
FIFO_RX_LEVEL = 0; //set to 0, we'll read a fresh FIFO_RX_LEVEL next time
|
||||||
|
return; // No data or not enough data in rx fifo, check again next time around
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
//DIAG(F("I2CDFPlayer: RX_BUFFER: %d"), RX_BUFFER);
|
||||||
|
while (RX_BUFFER != 0) {
|
||||||
|
int c = _inbuffer[_inputIndex]; // Start at 0, increment to FIFO_RX_LEVEL
|
||||||
|
switch (_inputIndex) {
|
||||||
|
case 0:
|
||||||
|
if (c == 0x7E) ok = true;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (c == 0xFF) ok = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (c== 0x06) ok = true;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
_recvCMD = c; // CMD byte
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
switch (_recvCMD) {
|
||||||
|
//DIAG(F("I2CDFPlayer: %s, _recvCMD: 0x%x _awaitingResponse: 0x0%x"),_I2CAddress.toString(), _recvCMD, _awaitingResponse);
|
||||||
|
case 0x42:
|
||||||
|
// Response to status query
|
||||||
|
_playing = (c != 0);
|
||||||
|
// Mark the device online and cancel timeout
|
||||||
|
if (_deviceState==DEVSTATE_INITIALISING) {
|
||||||
|
_deviceState = DEVSTATE_NORMAL;
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
DIAG(F("I2CDFPlayer: %s, UART_CH: 0x0%x, _deviceState: 0x0%x"),_I2CAddress.toString(), _UART_CH, _deviceState);
|
||||||
|
#endif
|
||||||
|
#ifdef DIAG_IO
|
||||||
|
_display();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
_awaitingResponse = false;
|
||||||
|
break;
|
||||||
|
case 0x3d:
|
||||||
|
// End of play
|
||||||
|
if (_playing) {
|
||||||
|
#ifdef DIAG_IO
|
||||||
|
DIAG(F("I2CDFPlayer: Finished"));
|
||||||
|
#endif
|
||||||
|
_playing = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x40:
|
||||||
|
// Error codes; 1: Module Busy
|
||||||
|
DIAG(F("I2CDFPlayer: Error %d returned from device"), c);
|
||||||
|
_playing = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
case 4: case 5: case 7: case 8:
|
||||||
|
ok = true; // Skip over these bytes in message.
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
if (c==0xef) {
|
||||||
|
// Message finished
|
||||||
|
_retryCounter = RETRYCOUNT; // reset the retry counter as we have received a valid packet
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ok){
|
||||||
|
_inputIndex++; // character as expected, so increment index
|
||||||
|
RX_BUFFER --; // Decrease FIFO_RX_LEVEL with each character read from _inbuffer[_inputIndex]
|
||||||
|
} else {
|
||||||
|
_inputIndex = 0; // otherwise reset.
|
||||||
|
RX_BUFFER = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RX_BUFFER = 0; //Set to 0, we'll read a new RX FIFO level again
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Send any commands that need to be sent
|
||||||
|
void processOutgoing(unsigned long currentMicros) {
|
||||||
|
// When two commands are sent in quick succession, the device will often fail to
|
||||||
|
// execute one. Testing has indicated that a delay of 100ms or more is required
|
||||||
|
// between successive commands to get reliable operation.
|
||||||
|
// If 100ms has elapsed since the last thing sent, then check if there's some output to do.
|
||||||
|
if (((int32_t)currentMicros - _commandSendTime) > 100000) {
|
||||||
|
if ( _resetCmd == true){
|
||||||
|
sendPacket(0x0C,0,0);
|
||||||
|
_resetCmd = false;
|
||||||
|
} else if(_volCmd == true) { // do the volme before palying a track
|
||||||
|
if(_requestedVolumeLevel >= 0 && _requestedVolumeLevel <= 30){
|
||||||
|
_currentVolume = _requestedVolumeLevel; // If _requestedVolumeLevel is out of range, sent _currentV1olume
|
||||||
|
}
|
||||||
|
sendPacket(0x06, 0x00, _currentVolume);
|
||||||
|
_volCmd = false;
|
||||||
|
} else if (_playCmd == true) {
|
||||||
|
// Change song
|
||||||
|
if (_requestedSong != -1) {
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: _requestedVolumeLevel: %u, _requestedSong: %u, _currentFolder: %u _playCmd: 0x%x"), _requestedVolumeLevel, _requestedSong, _currentFolder, _playCmd);
|
||||||
|
#endif
|
||||||
|
sendPacket(0x0F, _currentFolder, _requestedSong); // audio file in folder
|
||||||
|
_requestedSong = -1;
|
||||||
|
_playCmd = false;
|
||||||
|
}
|
||||||
|
} //else if (_requestedSong == 0) {
|
||||||
|
else if (_stopplayCmd == true) {
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: Stop playing: _stopplayCmd: 0x%x"), _stopplayCmd);
|
||||||
|
#endif
|
||||||
|
sendPacket(0x16, 0x00, 0x00); // Stop playing
|
||||||
|
_requestedSong = -1;
|
||||||
|
_repeat = false; // reset repeat
|
||||||
|
_stopplayCmd = false;
|
||||||
|
} else if (_folderCmd == true) {
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: Folder: _folderCmd: 0x%x, _requestedFolder: %d"), _stopplayCmd, _requestedFolder);
|
||||||
|
#endif
|
||||||
|
if (_currentFolder != _requestedFolder){
|
||||||
|
_currentFolder = _requestedFolder;
|
||||||
|
}
|
||||||
|
_folderCmd = false;
|
||||||
|
} else if (_repeatCmd == true) {
|
||||||
|
if(_repeat == false) { // No repeat play currently
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: Repeat: _repeatCmd: 0x%x, _requestedSong: %d, _repeat: 0x0%x"), _repeatCmd, _requestedSong, _repeat);
|
||||||
|
#endif
|
||||||
|
sendPacket(0x08, 0x00, _requestedSong); // repeat playing audio file in root folder
|
||||||
|
_requestedSong = -1;
|
||||||
|
_repeat = true;
|
||||||
|
}
|
||||||
|
_repeatCmd= false;
|
||||||
|
} else if (_daconCmd == true) { // Always turn DAC on
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: DACON: _daconCmd: 0x%x"), _daconCmd);
|
||||||
|
#endif
|
||||||
|
sendPacket(0x1A,0,0x00);
|
||||||
|
_daconCmd = false;
|
||||||
|
} else if (_eqCmd == true){ // Set Equalizer, values 0x00 - 0x05
|
||||||
|
if (_currentEQvalue != _requestedEQValue){
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: EQ: _eqCmd: 0x%x, _currentEQvalue: 0x0%x, _requestedEQValue: 0x0%x"), _eqCmd, _currentEQvalue, _requestedEQValue);
|
||||||
|
#endif
|
||||||
|
_currentEQvalue = _requestedEQValue;
|
||||||
|
sendPacket(0x07,0x00,_currentEQvalue);
|
||||||
|
}
|
||||||
|
_eqCmd = false;
|
||||||
|
} else if (_setamCmd == true){ // Set Audio mixer channel
|
||||||
|
setGPIO(); // Set the audio mixer channel
|
||||||
|
/*
|
||||||
|
if (_audioMixer == 1){ // set to audio mixer 1
|
||||||
|
if (_UART_CH == 0){
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
|
||||||
|
} else { // must be UART 1
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
|
||||||
|
}
|
||||||
|
//_setamCmd = false;
|
||||||
|
//UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
|
||||||
|
} else { // set to audio mixer 2
|
||||||
|
if (_UART_CH == 0){
|
||||||
|
TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 0 to Low
|
||||||
|
} else { // must be UART 1
|
||||||
|
TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 1 to Low
|
||||||
|
}
|
||||||
|
//_setamCmd = false;
|
||||||
|
//UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
|
||||||
|
}*/
|
||||||
|
_setamCmd = false;
|
||||||
|
} else if ((int32_t)currentMicros - _commandSendTime > 1000000) {
|
||||||
|
// Poll device every second that other commands aren't being sent,
|
||||||
|
// to check if it's still connected and responding.
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: Send keepalive") );
|
||||||
|
#endif
|
||||||
|
sendPacket(0x42,0,0);
|
||||||
|
if (!_awaitingResponse) {
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: Send keepalive, _awaitingResponse: 0x0%x"), _awaitingResponse );
|
||||||
|
#endif
|
||||||
|
_timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds
|
||||||
|
_awaitingResponse = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Write to a vPin will do nothing
|
||||||
|
void _write(VPIN vpin, int value) override {
|
||||||
|
if (_deviceState == DEVSTATE_FAILED) return;
|
||||||
|
#ifdef DIAG_IO
|
||||||
|
DIAG(F("I2CDFPlayer: Writing to any vPin not supported"));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0.
|
||||||
|
// Volume may be specified as second parameter to writeAnalogue.
|
||||||
|
// If value is zero, the player stops playing.
|
||||||
|
// WriteAnalogue on second pin sets the output volume.
|
||||||
|
//
|
||||||
|
// WriteAnalogue to be done on first vpin
|
||||||
|
//
|
||||||
|
//void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
|
||||||
|
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t cmd=0) override {
|
||||||
|
if (_deviceState == DEVSTATE_FAILED) return;
|
||||||
|
#ifdef DIAG_IO
|
||||||
|
DIAG(F("I2CDFPlayer: VPIN:%u FileNo:%d Volume:%d Command:0x%x"), vpin, value, volume, cmd);
|
||||||
|
#endif
|
||||||
|
uint8_t pin = vpin - _firstVpin;
|
||||||
|
if (pin == 0) { // Enhanced DFPlayer commands, do nothing if not vPin 0
|
||||||
|
// Read command and value
|
||||||
|
switch (cmd){
|
||||||
|
//case NONE:
|
||||||
|
// DFPlayerCmd = cmd;
|
||||||
|
// break;
|
||||||
|
case DF_PLAY:
|
||||||
|
_playCmd = true;
|
||||||
|
_volCmd = true;
|
||||||
|
_requestedSong = value;
|
||||||
|
_requestedVolumeLevel = volume;
|
||||||
|
_playing = true;
|
||||||
|
break;
|
||||||
|
case DF_VOL:
|
||||||
|
_volCmd = true;
|
||||||
|
_requestedVolumeLevel = volume;
|
||||||
|
break;
|
||||||
|
case DF_FOLDER:
|
||||||
|
_folderCmd = true;
|
||||||
|
if (volume <= 0 || volume > 99){ // Range checking, valid values 1-99, else default to 1
|
||||||
|
_requestedFolder = 0x01; // if outside range, default to folder 01
|
||||||
|
} else {
|
||||||
|
_requestedFolder = volume;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DF_REPEATPLAY: // Need to check if _repeat == true, if so do nothing
|
||||||
|
if (_repeat == false) {
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: WriteAnalog Repeat: _repeat: 0x0%x, value: %d _repeatCmd: 0x%x"), _repeat, value, _repeatCmd);
|
||||||
|
#endif
|
||||||
|
_repeatCmd = true;
|
||||||
|
_requestedSong = value;
|
||||||
|
_requestedVolumeLevel = volume;
|
||||||
|
_playing = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DF_STOPPLAY:
|
||||||
|
_stopplayCmd = true;
|
||||||
|
break;
|
||||||
|
case DF_EQ:
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: WriteAnalog EQ: cmd: 0x%x, EQ value: 0x%x"), cmd, volume);
|
||||||
|
#endif
|
||||||
|
_eqCmd = true;
|
||||||
|
if (volume <= 0 || volume > 5) { // If out of range, default to NORMAL
|
||||||
|
_requestedEQValue = DF_NORMAL;
|
||||||
|
} else { // Valid EQ parameter range
|
||||||
|
_requestedEQValue = volume;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DF_RESET:
|
||||||
|
_resetCmd = true;
|
||||||
|
break;
|
||||||
|
case DF_DACON: // Works, but without the DACOFF command limited value, except when not relying on DFPlayer default to turn the DAC on
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: WrtieAnalog DACON: cmd: 0x%x"), cmd);
|
||||||
|
#endif
|
||||||
|
_daconCmd = true;
|
||||||
|
break;
|
||||||
|
case DF_SETAM: // Set the audio mixer channel to 1 or 2
|
||||||
|
_setamCmd = true;
|
||||||
|
#ifdef DIAG_I2CDFplayer_playing
|
||||||
|
DIAG(F("I2CDFPlayer: WrtieAnalog SETAM: cmd: 0x%x"), cmd);
|
||||||
|
#endif
|
||||||
|
if (volume <= 0 || volume > 2) { // If out of range, default to 1
|
||||||
|
_audioMixer = 1;
|
||||||
|
} else { // Valid SETAM parameter in range
|
||||||
|
_audioMixer = volume; // _audioMixer valid values 1 or 2
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A read on any pin indicates if the player is still playing.
|
||||||
|
int _read(VPIN vpin) override {
|
||||||
|
if (_deviceState == DEVSTATE_FAILED) return false;
|
||||||
|
uint8_t pin = vpin - _firstVpin;
|
||||||
|
if (pin == 0) { // Do nothing if not vPin 0
|
||||||
|
return _playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _display() override {
|
||||||
|
DIAG(F("I2CDFPlayer Configured on Vpins:%u-%u %S"), _firstVpin, _firstVpin+_nPins-1,
|
||||||
|
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// DFPlayer command frame
|
||||||
|
// 7E FF 06 0F 00 01 01 xx xx EF
|
||||||
|
// 0 -> 7E is start code
|
||||||
|
// 1 -> FF is version
|
||||||
|
// 2 -> 06 is length
|
||||||
|
// 3 -> 0F is command
|
||||||
|
// 4 -> 00 is no receive
|
||||||
|
// 5~6 -> 01 01 is argument
|
||||||
|
// 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 )
|
||||||
|
// 9 -> EF is end code
|
||||||
|
|
||||||
|
void sendPacket(uint8_t command, uint8_t arg1 = 0, uint8_t arg2 = 0) {
|
||||||
|
FIFO_TX_LEVEL = 0; // Reset FIFO_TX_LEVEL
|
||||||
|
uint8_t out[] = {
|
||||||
|
0x7E,
|
||||||
|
0xFF,
|
||||||
|
06,
|
||||||
|
command,
|
||||||
|
00,
|
||||||
|
//static_cast<uint8_t>(arg >> 8),
|
||||||
|
//static_cast<uint8_t>(arg & 0x00ff),
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
00,
|
||||||
|
00,
|
||||||
|
0xEF };
|
||||||
|
|
||||||
|
setChecksum(out);
|
||||||
|
|
||||||
|
// Prepend the DFPlayer command with REG address and UART Channel in _outbuffer
|
||||||
|
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
|
||||||
|
for ( int i = 1; i < sizeof(out)+1 ; i++){
|
||||||
|
_outbuffer[i] = out[i-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DIAG_I2CDFplayer_data
|
||||||
|
DIAG(F("SC16IS752: I2C: %s Sent packet function"), _I2CAddress.toString());
|
||||||
|
for (int i = 0; i < sizeof _outbuffer; i++){
|
||||||
|
DIAG(F("SC16IS752: Data _outbuffer[0x%x]: 0x%x"), i, _outbuffer[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TX_fifo_lvl();
|
||||||
|
if(FIFO_TX_LEVEL > 0){ //FIFO is empty
|
||||||
|
I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer), &_rb);
|
||||||
|
//I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer));
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
DIAG(F("SC16IS752: I2C: %s data transmit complete on UART: 0x%x"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
DIAG(F("I2CDFPlayer at: %s, TX FIFO not empty on UART: 0x%x"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
_deviceState = DEVSTATE_FAILED; // This should not happen
|
||||||
|
}
|
||||||
|
_commandSendTime = micros();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t calcChecksum(uint8_t* packet)
|
||||||
|
{
|
||||||
|
uint16_t sum = 0;
|
||||||
|
for (int i = 1; i < 7; i++)
|
||||||
|
{
|
||||||
|
sum += packet[i];
|
||||||
|
}
|
||||||
|
return -sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChecksum(uint8_t* out)
|
||||||
|
{
|
||||||
|
uint16_t sum = calcChecksum(out);
|
||||||
|
out[7] = (sum >> 8);
|
||||||
|
out[8] = (sum & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SC16IS752 functions
|
||||||
|
// Initialise SC16IS752 only for this channel
|
||||||
|
// First a software reset
|
||||||
|
// Enable FIFO and clear TX & RX FIFO
|
||||||
|
// Need to set the following registers
|
||||||
|
// IOCONTROL set bit 1 and 2 to 0 indicating that they are GPIO
|
||||||
|
// IODIR set all bit to 1 indicating al are output
|
||||||
|
// IOSTATE set only bit 0 to 1 for UART 0, or only bit 1 for UART 1 //
|
||||||
|
// LCR bit 7=0 divisor latch (clock division registers DLH & DLL, they store 16 bit divisor),
|
||||||
|
// WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE
|
||||||
|
// MCR bit 7=0 clock divisor devide-by-1 clock input
|
||||||
|
// DLH most significant part of divisor
|
||||||
|
// DLL least significant part of divisor
|
||||||
|
//
|
||||||
|
// BAUD_RATE, WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE have been defined and initialized
|
||||||
|
//
|
||||||
|
void Init_SC16IS752(){ // Return value is in _deviceState
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
DIAG(F("SC16IS752: Initialize I2C: %s , UART Ch: 0x%x"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
#endif
|
||||||
|
//uint16_t _divisor = (SC16IS752_XTAL_FREQ / PRESCALER) / (BAUD_RATE * 16);
|
||||||
|
uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16); // Calculate _divisor for baudrate
|
||||||
|
TEMP_REG_VAL = 0x08; // UART Software reset
|
||||||
|
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
||||||
|
TEMP_REG_VAL = 0x00; // Set pins to GPIO mode
|
||||||
|
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
|
||||||
|
TEMP_REG_VAL = 0xFF; //Set all pins as output
|
||||||
|
UART_WriteRegister(REG_IODIR, TEMP_REG_VAL);
|
||||||
|
UART_ReadRegister(REG_IOSTATE); // Read current state as not to overwrite the other GPIO pins
|
||||||
|
TEMP_REG_VAL = _inbuffer[0];
|
||||||
|
setGPIO(); // Set the audio mixer channel
|
||||||
|
/*
|
||||||
|
if (_UART_CH == 0){ // Set Audio mixer channel
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
|
||||||
|
} else { // must be UART 1
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
|
||||||
|
}
|
||||||
|
UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
|
||||||
|
*/
|
||||||
|
TEMP_REG_VAL = 0x07; // Reset FIFO, clear RX & TX FIFO
|
||||||
|
UART_WriteRegister(REG_FCR, TEMP_REG_VAL);
|
||||||
|
TEMP_REG_VAL = 0x00; // Set MCR to all 0, includes Clock divisor
|
||||||
|
UART_WriteRegister(REG_MCR, TEMP_REG_VAL);
|
||||||
|
TEMP_REG_VAL = 0x80 | WORD_LEN | STOP_BIT | PARITY_ENA | PARITY_TYPE;
|
||||||
|
UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch enabled
|
||||||
|
UART_WriteRegister(REG_DLL, (uint8_t)_divisor); // Write DLL
|
||||||
|
UART_WriteRegister(REG_DLH, (uint8_t)(_divisor >> 8)); // Write DLH
|
||||||
|
UART_ReadRegister(REG_LCR);
|
||||||
|
TEMP_REG_VAL = _inbuffer[0] & 0x7F; // Disable Divisor latch enabled bit
|
||||||
|
UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch disabled
|
||||||
|
|
||||||
|
uint8_t status = _rb.status;
|
||||||
|
if (status != I2C_STATUS_OK) {
|
||||||
|
DIAG(F("SC16IS752: I2C: %s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
|
||||||
|
_deviceState = DEVSTATE_FAILED;
|
||||||
|
} else {
|
||||||
|
#ifdef DIAG_IO
|
||||||
|
DIAG(F("SC16IS752: I2C: %s, _deviceState: %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
|
||||||
|
#endif
|
||||||
|
_deviceState = DEVSTATE_NORMAL; // If I2C state is OK, then proceed to initialize DFPlayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read the Receive FIFO Level register (RXLVL), return a single unsigned integer
|
||||||
|
// of nr of characters in the RX FIFO, bit 6:0, 7 not used, set to zero
|
||||||
|
// value from 0 (0x00) to 64 (0x40) Only display if RX FIFO has data
|
||||||
|
// The RX fifo level is used to check if there are enough bytes to process a frame
|
||||||
|
void RX_fifo_lvl(){
|
||||||
|
UART_ReadRegister(REG_RXLV);
|
||||||
|
FIFO_RX_LEVEL = _inbuffer[0];
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
if (FIFO_RX_LEVEL > 0){
|
||||||
|
//if (FIFO_RX_LEVEL > 0 && FIFO_RX_LEVEL < 10){
|
||||||
|
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_RX_LEVEL: 0d%d"), _I2CAddress.toString(), _UART_CH, _inbuffer[0]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a frame is transmitted from the DFPlayer to the serial port, and at the same time the CS is sending a 42 query
|
||||||
|
// the following two frames from the DFPlayer are corrupt. This result in the receive buffer being out of sync and the
|
||||||
|
// CS will complain and generate a timeout.
|
||||||
|
// The RX fifo has corrupt data and need to be flushed, this function does that
|
||||||
|
//
|
||||||
|
void resetRX_fifo(){
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, RX fifo reset"), _I2CAddress.toString(), _UART_CH);
|
||||||
|
#endif
|
||||||
|
TEMP_REG_VAL = 0x03; // Reset RX fifo
|
||||||
|
UART_WriteRegister(REG_FCR, TEMP_REG_VAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set or reset GPIO pin 0 and 1 depending on the UART ch
|
||||||
|
// This function may be modified in a future release to enable all 8 pins to be set or reset with EX-Rail
|
||||||
|
// for various auxilary functions
|
||||||
|
void setGPIO(){
|
||||||
|
UART_ReadRegister(REG_IOSTATE); // Get the current GPIO pins state from the IOSTATE register
|
||||||
|
TEMP_REG_VAL = _inbuffer[0];
|
||||||
|
if (_audioMixer == 1){ // set to audio mixer 1
|
||||||
|
if (_UART_CH == 0){
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
|
||||||
|
} else { // must be UART 1
|
||||||
|
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
|
||||||
|
}
|
||||||
|
} else { // set to audio mixer 2
|
||||||
|
if (_UART_CH == 0){
|
||||||
|
TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 0 to Low
|
||||||
|
} else { // must be UART 1
|
||||||
|
TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 1 to Low
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
|
||||||
|
_setamCmd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read the Tranmit FIFO Level register (TXLVL), return a single unsigned integer
|
||||||
|
// of nr characters free in the TX FIFO, bit 6:0, 7 not used, set to zero
|
||||||
|
// value from 0 (0x00) to 64 (0x40)
|
||||||
|
//
|
||||||
|
void TX_fifo_lvl(){
|
||||||
|
UART_ReadRegister(REG_TXLV);
|
||||||
|
FIFO_TX_LEVEL = _inbuffer[0];
|
||||||
|
#ifdef DIAG_I2CDFplayer
|
||||||
|
// DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_TX_LEVEL: 0d%d"), _I2CAddress.toString(), _UART_CH, FIFO_TX_LEVEL);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//void UART_WriteRegister(I2CAddress _I2CAddress, uint8_t _UART_CH, uint8_t UART_REG, uint8_t Val, I2CRB &_rb){
|
||||||
|
void UART_WriteRegister(uint8_t UART_REG, uint8_t Val){
|
||||||
|
_outbuffer[0] = UART_REG << 3 | _UART_CH << 1;
|
||||||
|
_outbuffer[1] = Val;
|
||||||
|
#ifdef DIAG_I2CDFplayer_reg
|
||||||
|
DIAG(F("SC16IS752: Write register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b"), _I2CAddress.toString(), _UART_CH, UART_REG, _outbuffer[1]);
|
||||||
|
#endif
|
||||||
|
I2CManager.write(_I2CAddress, _outbuffer, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void UART_ReadRegister(uint8_t UART_REG){
|
||||||
|
_outbuffer[0] = UART_REG << 3 | _UART_CH << 1; // _outbuffer[0] has now UART_REG and UART_CH
|
||||||
|
I2CManager.read(_I2CAddress, _inbuffer, 1, _outbuffer, 1);
|
||||||
|
// _inbuffer has the REG data
|
||||||
|
#ifdef DIAG_I2CDFplayer_reg
|
||||||
|
DIAG(F("SC16IS752: Read register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b"), _I2CAddress.toString(), _UART_CH, UART_REG, _inbuffer[0]);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// SC16IS752 General register set (from the datasheet)
|
||||||
|
enum : uint8_t{
|
||||||
|
REG_RHR = 0x00, // FIFO Read
|
||||||
|
REG_THR = 0x00, // FIFO Write
|
||||||
|
REG_IER = 0x01, // Interrupt Enable Register R/W
|
||||||
|
REG_FCR = 0x02, // FIFO Control Register Write
|
||||||
|
REG_IIR = 0x02, // Interrupt Identification Register Read
|
||||||
|
REG_LCR = 0x03, // Line Control Register R/W
|
||||||
|
REG_MCR = 0x04, // Modem Control Register R/W
|
||||||
|
REG_LSR = 0x05, // Line Status Register Read
|
||||||
|
REG_MSR = 0x06, // Modem Status Register Read
|
||||||
|
REG_SPR = 0x07, // Scratchpad Register R/W
|
||||||
|
REG_TCR = 0x06, // Transmission Control Register R/W
|
||||||
|
REG_TLR = 0x07, // Trigger Level Register R/W
|
||||||
|
REG_TXLV = 0x08, // Transmitter FIFO Level register Read
|
||||||
|
REG_RXLV = 0x09, // Receiver FIFO Level register Read
|
||||||
|
REG_IODIR = 0x0A, // Programmable I/O pins Direction register R/W
|
||||||
|
REG_IOSTATE = 0x0B, // Programmable I/O pins State register R/W
|
||||||
|
REG_IOINTENA = 0x0C, // I/O Interrupt Enable register R/W
|
||||||
|
REG_IOCONTROL = 0x0E, // I/O Control register R/W
|
||||||
|
REG_EFCR = 0x0F, // Extra Features Control Register R/W
|
||||||
|
};
|
||||||
|
|
||||||
|
// SC16IS752 Special register set
|
||||||
|
enum : uint8_t{
|
||||||
|
REG_DLL = 0x00, // Division registers R/W
|
||||||
|
REG_DLH = 0x01, // Division registers R/W
|
||||||
|
};
|
||||||
|
|
||||||
|
// SC16IS752 Enhanced regiter set
|
||||||
|
enum : uint8_t{
|
||||||
|
REG_EFR = 0X02, // Enhanced Features Register R/W
|
||||||
|
REG_XON1 = 0x04, // R/W
|
||||||
|
REG_XON2 = 0x05, // R/W
|
||||||
|
REG_XOFF1 = 0x06, // R/W
|
||||||
|
REG_XOFF2 = 0x07, // R/W
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// DFPlayer commands and values
|
||||||
|
// Declared in this scope
|
||||||
|
enum : uint8_t{
|
||||||
|
DF_PLAY = 0x0F,
|
||||||
|
DF_VOL = 0x06,
|
||||||
|
DF_FOLDER = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is
|
||||||
|
DF_REPEATPLAY = 0x08,
|
||||||
|
DF_STOPPLAY = 0x16,
|
||||||
|
DF_EQ = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS
|
||||||
|
DF_RESET = 0x0C,
|
||||||
|
DF_DACON = 0x1A,
|
||||||
|
DF_SETAM = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer
|
||||||
|
DF_NORMAL = 0x00, // Equalizer parameters
|
||||||
|
DF_POP = 0x01,
|
||||||
|
DF_ROCK = 0x02,
|
||||||
|
DF_JAZZ = 0x03,
|
||||||
|
DF_CLASSIC = 0x04,
|
||||||
|
DF_BASS = 0x05,
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // IO_I2CDFPlayer_h
|
|
@ -30,20 +30,19 @@
|
||||||
|
|
||||||
class PCA9555 : public GPIOBase<uint16_t> {
|
class PCA9555 : public GPIOBase<uint16_t> {
|
||||||
public:
|
public:
|
||||||
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
|
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||||
new PCA9555(vpin, min(nPins,16), I2CAddress, interruptPin);
|
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9555(vpin,nPins, i2cAddress, interruptPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
// Constructor
|
// Constructor
|
||||||
PCA9555(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
|
PCA9555(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
|
||||||
: GPIOBase<uint16_t>((FSH *)F("PCA9555"), vpin, nPins, I2CAddress, interruptPin)
|
: GPIOBase<uint16_t>((FSH *)F("PCA9555"), vpin, nPins, I2CAddress, interruptPin)
|
||||||
{
|
{
|
||||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||||
outputBuffer, sizeof(outputBuffer));
|
outputBuffer, sizeof(outputBuffer));
|
||||||
outputBuffer[0] = REG_INPUT_P0;
|
outputBuffer[0] = REG_INPUT_P0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
void _writeGpioPort() override {
|
void _writeGpioPort() override {
|
||||||
I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);
|
I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,9 @@
|
||||||
* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples:
|
* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples:
|
||||||
*
|
*
|
||||||
* #include "IO_RotaryEncoder.h"
|
* #include "IO_RotaryEncoder.h"
|
||||||
* HAL(RotaryEncoder, 700, 1, 0x70) // Define single Vpin, no feedback or position sent to rotary encoder software
|
* HAL(RotaryEncoder, 700, 1, 0x67) // Define single Vpin, no feedback or position sent to rotary encoder software
|
||||||
* HAL(RotaryEncoder, 700, 2, 0x70) // Define two Vpins, feedback only sent to rotary encoder software
|
* HAL(RotaryEncoder, 700, 2, 0x67) // Define two Vpins, feedback only sent to rotary encoder software
|
||||||
* HAL(RotaryEncoder, 700, 3, 0x70) // Define three Vpins, can send feedback and position update to rotary encoder software
|
* HAL(RotaryEncoder, 700, 3, 0x67) // Define three Vpins, can send feedback and position update to rotary encoder software
|
||||||
*
|
*
|
||||||
* Refer to the documentation for further information including the valid activities and examples.
|
* Refer to the documentation for further information including the valid activities and examples.
|
||||||
*/
|
*/
|
||||||
|
|
98
IO_trainbrains.h
Normal file
98
IO_trainbrains.h
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* © 2023, Chris Harlow. All rights reserved.
|
||||||
|
* © 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_trainbrains_h
|
||||||
|
#define io_trainbrains_h
|
||||||
|
|
||||||
|
#include "IO_GPIOBase.h"
|
||||||
|
#include "FSH.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/*
|
||||||
|
* IODevice subclass for trainbrains 3-block occupancy detector.
|
||||||
|
* For details see http://trainbrains.eu
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum TrackUnoccupancy
|
||||||
|
{
|
||||||
|
TRACK_UNOCCUPANCY_UNKNOWN = 0,
|
||||||
|
TRACK_OCCUPIED = 1,
|
||||||
|
TRACK_UNOCCUPIED = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
class Trainbrains02 : public GPIOBase<uint16_t> {
|
||||||
|
public:
|
||||||
|
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress) {
|
||||||
|
if (checkNoOverlap(vpin, nPins, i2cAddress)) new Trainbrains02(vpin, nPins, i2cAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Constructor
|
||||||
|
Trainbrains02(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
|
||||||
|
: GPIOBase<uint16_t>((FSH *)F("Trainbrains02"), vpin, nPins, i2cAddress, interruptPin)
|
||||||
|
{
|
||||||
|
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||||
|
outputBuffer, sizeof(outputBuffer));
|
||||||
|
|
||||||
|
outputBuffer[0] = (uint8_t)_I2CAddress; // strips away the mux part.
|
||||||
|
outputBuffer[1] =14;
|
||||||
|
outputBuffer[2] =1;
|
||||||
|
outputBuffer[3] =0; // This is the channel updated at each poling call
|
||||||
|
outputBuffer[4] =0;
|
||||||
|
outputBuffer[5] =0;
|
||||||
|
outputBuffer[6] =0;
|
||||||
|
outputBuffer[7] =0;
|
||||||
|
outputBuffer[8] =0;
|
||||||
|
outputBuffer[9] =0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeGpioPort() override {}
|
||||||
|
|
||||||
|
void _readGpioPort(bool immediate) override {
|
||||||
|
// cycle channel on device each time
|
||||||
|
outputBuffer[3]=channelInProgress+1; // 1-origin
|
||||||
|
channelInProgress++;
|
||||||
|
if(channelInProgress>=_nPins) channelInProgress=0;
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
_processCompletion(I2CManager.read(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||||
|
outputBuffer, sizeof(outputBuffer)));
|
||||||
|
} 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) inputBuffer[6]=TRACK_UNOCCUPANCY_UNKNOWN;
|
||||||
|
if (inputBuffer[6] == TRACK_UNOCCUPIED ) _portInputState |= 0x01 <<channelInProgress;
|
||||||
|
else _portInputState &= ~(0x01 <<channelInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t channelInProgress=0;
|
||||||
|
uint8_t outputBuffer[10];
|
||||||
|
uint8_t inputBuffer[10];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
57
KeywordHasher.h
Normal file
57
KeywordHasher.h
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* © 2024 Vincent Hamp and Chris Harlow
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* Reader be aware:
|
||||||
|
This function implements the _hk data type so that a string keyword
|
||||||
|
is hashed to the same value as the DCCEXParser uses to hash incoming
|
||||||
|
keywords.
|
||||||
|
Thus "MAIN"_hk generates exactly the same run time vakue
|
||||||
|
as const int16_t HASH_KEYWORD_MAIN=11339
|
||||||
|
*/
|
||||||
|
#ifndef KeywordHAsher_h
|
||||||
|
#define KeywordHasher_h
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
constexpr uint16_t CompiletimeKeywordHasher(const char * sv, uint16_t running=0) {
|
||||||
|
return (*sv==0) ? running : CompiletimeKeywordHasher(sv+1,
|
||||||
|
(*sv >= '0' && *sv <= '9')
|
||||||
|
? (10*running+*sv-'0') // Numeric hash
|
||||||
|
: ((running << 5) + running) ^ *sv
|
||||||
|
); //
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int16_t operator""_hk(const char * keyword, size_t len)
|
||||||
|
{
|
||||||
|
return (int16_t) CompiletimeKeywordHasher(keyword,len*0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Some historical values for testing:
|
||||||
|
const int16_t HASH_KEYWORD_MAIN = 11339;
|
||||||
|
const int16_t HASH_KEYWORD_SLOW = -17209;
|
||||||
|
const int16_t HASH_KEYWORD_SPEED28 = -17064;
|
||||||
|
const int16_t HASH_KEYWORD_SPEED128 = 25816;
|
||||||
|
*/
|
||||||
|
|
||||||
|
static_assert("MAIN"_hk == 11339,"Keyword hasher error");
|
||||||
|
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
|
||||||
|
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
|
||||||
|
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
|
||||||
|
#endif
|
230
MotorDriver.cpp
230
MotorDriver.cpp
|
@ -1,9 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* © 2022-2023 Paul M Antoine
|
* © 2022-2024 Paul M Antoine
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020-2023 Harald Barth
|
* © 2020-2023 Harald Barth
|
||||||
* © 2020-2021 Chris Harlow
|
* © 2020-2021 Chris Harlow
|
||||||
|
* © 2023 Colin Murdoch
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of CommandStation-EX
|
* This file is part of CommandStation-EX
|
||||||
|
@ -26,12 +27,20 @@
|
||||||
#include "DCCWaveform.h"
|
#include "DCCWaveform.h"
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
|
#include "EXRAIL2.h"
|
||||||
|
|
||||||
unsigned long MotorDriver::globalOverloadStart = 0;
|
unsigned long MotorDriver::globalOverloadStart = 0;
|
||||||
|
|
||||||
volatile portreg_t shadowPORTA;
|
volatile portreg_t shadowPORTA;
|
||||||
volatile portreg_t shadowPORTB;
|
volatile portreg_t shadowPORTB;
|
||||||
volatile portreg_t shadowPORTC;
|
volatile portreg_t shadowPORTC;
|
||||||
|
#if defined(ARDUINO_ARCH_STM32)
|
||||||
|
volatile portreg_t shadowPORTD;
|
||||||
|
volatile portreg_t shadowPORTE;
|
||||||
|
volatile portreg_t shadowPORTF;
|
||||||
|
volatile portreg_t shadowPORTG;
|
||||||
|
volatile portreg_t shadowPORTH;
|
||||||
|
#endif
|
||||||
|
|
||||||
MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin,
|
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) {
|
byte current_pin, float sense_factor, unsigned int trip_milliamps, int16_t fault_pin) {
|
||||||
|
@ -66,6 +75,31 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
|
||||||
fastSignalPin.shadowinout = fastSignalPin.inout;
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
fastSignalPin.inout = &shadowPORTC;
|
fastSignalPin.inout = &shadowPORTC;
|
||||||
}
|
}
|
||||||
|
if (HAVE_PORTD(fastSignalPin.inout == &PORTD)) {
|
||||||
|
DIAG(F("Found PORTD pin %d"),signalPin);
|
||||||
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
|
fastSignalPin.inout = &shadowPORTD;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTE(fastSignalPin.inout == &PORTE)) {
|
||||||
|
DIAG(F("Found PORTE pin %d"),signalPin);
|
||||||
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
|
fastSignalPin.inout = &shadowPORTE;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTF(fastSignalPin.inout == &PORTF)) {
|
||||||
|
DIAG(F("Found PORTF pin %d"),signalPin);
|
||||||
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
|
fastSignalPin.inout = &shadowPORTF;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTG(fastSignalPin.inout == &PORTG)) {
|
||||||
|
DIAG(F("Found PORTG pin %d"),signalPin);
|
||||||
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
|
fastSignalPin.inout = &shadowPORTG;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTH(fastSignalPin.inout == &PORTH)) {
|
||||||
|
DIAG(F("Found PORTH pin %d"),signalPin);
|
||||||
|
fastSignalPin.shadowinout = fastSignalPin.inout;
|
||||||
|
fastSignalPin.inout = &shadowPORTF;
|
||||||
|
}
|
||||||
|
|
||||||
signalPin2=signal_pin2;
|
signalPin2=signal_pin2;
|
||||||
if (signalPin2!=UNUSED_PIN) {
|
if (signalPin2!=UNUSED_PIN) {
|
||||||
|
@ -89,6 +123,31 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
|
||||||
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
fastSignalPin2.inout = &shadowPORTC;
|
fastSignalPin2.inout = &shadowPORTC;
|
||||||
}
|
}
|
||||||
|
if (HAVE_PORTD(fastSignalPin2.inout == &PORTD)) {
|
||||||
|
DIAG(F("Found PORTD pin %d"),signalPin2);
|
||||||
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
|
fastSignalPin2.inout = &shadowPORTD;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTE(fastSignalPin2.inout == &PORTE)) {
|
||||||
|
DIAG(F("Found PORTE pin %d"),signalPin2);
|
||||||
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
|
fastSignalPin2.inout = &shadowPORTE;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTF(fastSignalPin2.inout == &PORTF)) {
|
||||||
|
DIAG(F("Found PORTF pin %d"),signalPin2);
|
||||||
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
|
fastSignalPin2.inout = &shadowPORTF;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTG(fastSignalPin2.inout == &PORTG)) {
|
||||||
|
DIAG(F("Found PORTG pin %d"),signalPin2);
|
||||||
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
|
fastSignalPin2.inout = &shadowPORTG;
|
||||||
|
}
|
||||||
|
if (HAVE_PORTH(fastSignalPin2.inout == &PORTH)) {
|
||||||
|
DIAG(F("Found PORTH pin %d"),signalPin2);
|
||||||
|
fastSignalPin2.shadowinout = fastSignalPin2.inout;
|
||||||
|
fastSignalPin2.inout = &shadowPORTH;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else dualSignal=false;
|
else dualSignal=false;
|
||||||
|
|
||||||
|
@ -277,7 +336,7 @@ void MotorDriver::startCurrentFromHW() {
|
||||||
#pragma GCC pop_options
|
#pragma GCC pop_options
|
||||||
#endif //ANALOG_READ_INTERRUPT
|
#endif //ANALOG_READ_INTERRUPT
|
||||||
|
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
|
||||||
#ifdef VARIABLE_TONES
|
#ifdef VARIABLE_TONES
|
||||||
uint16_t taurustones[28] = { 165, 175, 196, 220,
|
uint16_t taurustones[28] = { 165, 175, 196, 220,
|
||||||
247, 262, 294, 330,
|
247, 262, 294, 330,
|
||||||
|
@ -288,49 +347,21 @@ uint16_t taurustones[28] = { 165, 175, 196, 220,
|
||||||
220, 196, 175, 165 };
|
220, 196, 175, 165 };
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
void MotorDriver::setDCSignal(byte speedcode) {
|
void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/) {
|
||||||
if (brakePin == UNUSED_PIN)
|
if (brakePin == UNUSED_PIN)
|
||||||
return;
|
return;
|
||||||
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 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
|
// spedcoode is a dcc speed & direction
|
||||||
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
|
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
|
||||||
byte tDir=speedcode & 0x80;
|
byte tDir=speedcode & 0x80;
|
||||||
byte brake;
|
byte brake;
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
|
||||||
{
|
if (tSpeed <= 1) brake = 255;
|
||||||
int f = 131;
|
else if (tSpeed >= 127) brake = 0;
|
||||||
|
else brake = 2 * (128-tSpeed);
|
||||||
|
|
||||||
|
{ // new block because of variable f
|
||||||
|
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
|
||||||
|
int f = frequency;
|
||||||
#ifdef VARIABLE_TONES
|
#ifdef VARIABLE_TONES
|
||||||
if (tSpeed > 2) {
|
if (tSpeed > 2) {
|
||||||
if (tSpeed <= 58) {
|
if (tSpeed <= 58) {
|
||||||
|
@ -338,19 +369,15 @@ void MotorDriver::setDCSignal(byte speedcode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup
|
//DIAG(F("Brake pin %d value %d freqency %d"), brakePin, brake, f);
|
||||||
|
DCCTimer::DCCEXanalogWrite(brakePin, brake, invertBrake);
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
|
||||||
|
#else // all AVR here
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps
|
||||||
|
analogWrite(brakePin, invertBrake ? 255-brake : brake);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (tSpeed <= 1) brake = 255;
|
|
||||||
else if (tSpeed >= 127) brake = 0;
|
|
||||||
else brake = 2 * (128-tSpeed);
|
|
||||||
if (invertBrake)
|
|
||||||
brake=255-brake;
|
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
|
||||||
DCCTimer::DCCEXanalogWrite(brakePin,brake);
|
|
||||||
#else
|
|
||||||
analogWrite(brakePin,brake);
|
|
||||||
#endif
|
|
||||||
//DIAG(F("DCSignal %d"), speedcode);
|
//DIAG(F("DCSignal %d"), speedcode);
|
||||||
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
|
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
|
@ -370,6 +397,36 @@ void MotorDriver::setDCSignal(byte speedcode) {
|
||||||
setSignal(tDir);
|
setSignal(tDir);
|
||||||
HAVE_PORTC(PORTC=shadowPORTC);
|
HAVE_PORTC(PORTC=shadowPORTC);
|
||||||
interrupts();
|
interrupts();
|
||||||
|
} else if (HAVE_PORTD(fastSignalPin.shadowinout == &PORTD)) {
|
||||||
|
noInterrupts();
|
||||||
|
HAVE_PORTD(shadowPORTD=PORTD);
|
||||||
|
setSignal(tDir);
|
||||||
|
HAVE_PORTD(PORTD=shadowPORTD);
|
||||||
|
interrupts();
|
||||||
|
} else if (HAVE_PORTE(fastSignalPin.shadowinout == &PORTE)) {
|
||||||
|
noInterrupts();
|
||||||
|
HAVE_PORTE(shadowPORTE=PORTE);
|
||||||
|
setSignal(tDir);
|
||||||
|
HAVE_PORTE(PORTE=shadowPORTE);
|
||||||
|
interrupts();
|
||||||
|
} else if (HAVE_PORTF(fastSignalPin.shadowinout == &PORTF)) {
|
||||||
|
noInterrupts();
|
||||||
|
HAVE_PORTF(shadowPORTF=PORTF);
|
||||||
|
setSignal(tDir);
|
||||||
|
HAVE_PORTF(PORTF=shadowPORTF);
|
||||||
|
interrupts();
|
||||||
|
} else if (HAVE_PORTG(fastSignalPin.shadowinout == &PORTG)) {
|
||||||
|
noInterrupts();
|
||||||
|
HAVE_PORTG(shadowPORTG=PORTG);
|
||||||
|
setSignal(tDir);
|
||||||
|
HAVE_PORTG(PORTG=shadowPORTG);
|
||||||
|
interrupts();
|
||||||
|
} else if (HAVE_PORTH(fastSignalPin.shadowinout == &PORTH)) {
|
||||||
|
noInterrupts();
|
||||||
|
HAVE_PORTH(shadowPORTH=PORTH);
|
||||||
|
setSignal(tDir);
|
||||||
|
HAVE_PORTH(PORTH=shadowPORTH);
|
||||||
|
interrupts();
|
||||||
} else {
|
} else {
|
||||||
noInterrupts();
|
noInterrupts();
|
||||||
setSignal(tDir);
|
setSignal(tDir);
|
||||||
|
@ -379,53 +436,28 @@ void MotorDriver::setDCSignal(byte speedcode) {
|
||||||
void MotorDriver::throttleInrush(bool on) {
|
void MotorDriver::throttleInrush(bool on) {
|
||||||
if (brakePin == UNUSED_PIN)
|
if (brakePin == UNUSED_PIN)
|
||||||
return;
|
return;
|
||||||
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT)))
|
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT | TRACK_MODE_BOOST)))
|
||||||
return;
|
return;
|
||||||
byte duty = on ? 208 : 0;
|
byte duty = on ? 207 : 0; // duty of 81% at 62500Hz this gives pauses of 3usec
|
||||||
if (invertBrake)
|
|
||||||
duty = 255-duty;
|
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
#if defined(ARDUINO_ARCH_ESP32)
|
||||||
if(on) {
|
if(on) {
|
||||||
DCCTimer::DCCEXanalogWrite(brakePin,duty);
|
DCCTimer::DCCEXInrushControlOn(brakePin, duty, invertBrake);
|
||||||
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500);
|
|
||||||
} else {
|
} else {
|
||||||
ledcDetachPin(brakePin);
|
ledcDetachPin(brakePin); // not DCCTimer::DCCEXledcDetachPin() as we have not
|
||||||
|
// registered the pin in the pin to channel array
|
||||||
}
|
}
|
||||||
#else
|
#elif defined(ARDUINO_ARCH_STM32)
|
||||||
|
if(on) {
|
||||||
|
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
|
||||||
|
DCCTimer::DCCEXanalogWrite(brakePin,duty,invertBrake);
|
||||||
|
} else {
|
||||||
|
pinMode(brakePin, OUTPUT);
|
||||||
|
}
|
||||||
|
#else // all AVR here
|
||||||
|
if (invertBrake)
|
||||||
|
duty = 255-duty;
|
||||||
if(on){
|
if(on){
|
||||||
switch(brakePin) {
|
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
|
||||||
#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);
|
analogWrite(brakePin,duty);
|
||||||
#endif
|
#endif
|
||||||
|
@ -543,6 +575,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
|
||||||
DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A');
|
DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A');
|
||||||
}
|
}
|
||||||
setPower(POWERMODE::ALERT);
|
setPower(POWERMODE::ALERT);
|
||||||
|
if ((trackMode & TRACK_MODE_AUTOINV) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
|
||||||
|
DIAG(F("TRACK %c INVERT"), trackno + 'A');
|
||||||
|
invertOutput();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// all well
|
// all well
|
||||||
|
@ -602,6 +638,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
|
||||||
}
|
}
|
||||||
throttleInrush(false);
|
throttleInrush(false);
|
||||||
setPower(POWERMODE::ON);
|
setPower(POWERMODE::ON);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (goodtime > POWER_SAMPLE_ALERT_GOOD/2) {
|
||||||
|
throttleInrush(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -613,7 +653,11 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
|
||||||
// adjust next wait time
|
// adjust next wait time
|
||||||
power_sample_overload_wait *= 2;
|
power_sample_overload_wait *= 2;
|
||||||
if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX)
|
if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX)
|
||||||
power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;
|
power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;
|
||||||
|
#ifdef EXRAIL_ACTIVE
|
||||||
|
DIAG(F("Calling EXRAIL"));
|
||||||
|
RMFT2::powerEvent(trackno, true); // Tell EXRAIL we have an overload
|
||||||
|
#endif
|
||||||
// power on test
|
// power on test
|
||||||
DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc);
|
DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc);
|
||||||
setPower(POWERMODE::ALERT);
|
setPower(POWERMODE::ALERT);
|
||||||
|
|
117
MotorDriver.h
117
MotorDriver.h
|
@ -1,9 +1,9 @@
|
||||||
/*
|
/*
|
||||||
* © 2022 Paul M Antoine
|
* © 2022-2024 Paul M. Antoine
|
||||||
* © 2021 Mike S
|
* © 2021 Mike S
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020 Chris Harlow
|
* © 2020 Chris Harlow
|
||||||
* © 2022 Harald Barth
|
* © 2022,2023 Harald Barth
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of CommandStation-EX
|
* This file is part of CommandStation-EX
|
||||||
|
@ -26,10 +26,24 @@
|
||||||
#include "FSH.h"
|
#include "FSH.h"
|
||||||
#include "IODevice.h"
|
#include "IODevice.h"
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
|
#include <wiring_private.h>
|
||||||
|
|
||||||
// use powers of two so we can do logical and/or on the track modes in if clauses.
|
// use powers of two so we can do logical and/or on the track modes in if clauses.
|
||||||
|
// RACK_MODE_DCX is (TRACK_MODE_DC|TRACK_MODE_INV)
|
||||||
|
template<class T> inline T operator~ (T a) { return (T)~(int)a; }
|
||||||
|
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
|
||||||
|
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
|
||||||
|
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
|
||||||
enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
|
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};
|
TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
TRACK_MODE_BOOST = 32,
|
||||||
|
#else
|
||||||
|
TRACK_MODE_BOOST = 0,
|
||||||
|
#endif
|
||||||
|
TRACK_MODE_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,
|
||||||
|
TRACK_MODE_INV = 64,
|
||||||
|
TRACK_MODE_DCX = TRACK_MODE_DC|TRACK_MODE_INV, TRACK_MODE_AUTOINV = 128};
|
||||||
|
|
||||||
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
|
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
|
||||||
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
|
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
|
||||||
|
@ -60,6 +74,24 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
|
||||||
#define HAVE_PORTB(X) X
|
#define HAVE_PORTB(X) X
|
||||||
#define PORTC GPIOC->ODR
|
#define PORTC GPIOC->ODR
|
||||||
#define HAVE_PORTC(X) X
|
#define HAVE_PORTC(X) X
|
||||||
|
#define PORTD GPIOD->ODR
|
||||||
|
#define HAVE_PORTD(X) X
|
||||||
|
#if defined(GPIOE)
|
||||||
|
#define PORTE GPIOE->ODR
|
||||||
|
#define HAVE_PORTE(X) X
|
||||||
|
#endif
|
||||||
|
#if defined(GPIOF)
|
||||||
|
#define PORTF GPIOF->ODR
|
||||||
|
#define HAVE_PORTF(X) X
|
||||||
|
#endif
|
||||||
|
#if defined(GPIOG)
|
||||||
|
#define PORTG GPIOG->ODR
|
||||||
|
#define HAVE_PORTG(X) X
|
||||||
|
#endif
|
||||||
|
#if defined(GPIOH)
|
||||||
|
#define PORTH GPIOH->ODR
|
||||||
|
#define HAVE_PORTH(X) X
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// if macros not defined as pass-through we define
|
// if macros not defined as pass-through we define
|
||||||
|
@ -74,6 +106,21 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
|
||||||
#ifndef HAVE_PORTC
|
#ifndef HAVE_PORTC
|
||||||
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
|
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef HAVE_PORTD
|
||||||
|
#define HAVE_PORTD(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
|
||||||
|
#endif
|
||||||
|
#ifndef HAVE_PORTE
|
||||||
|
#define HAVE_PORTE(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
|
||||||
|
#endif
|
||||||
|
#ifndef HAVE_PORTF
|
||||||
|
#define HAVE_PORTF(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
|
||||||
|
#endif
|
||||||
|
#ifndef HAVE_PORTG
|
||||||
|
#define HAVE_PORTG(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
|
// Virtualised Motor shield 1-track hardware Interface
|
||||||
|
|
||||||
|
@ -110,6 +157,11 @@ struct FASTPIN {
|
||||||
extern volatile portreg_t shadowPORTA;
|
extern volatile portreg_t shadowPORTA;
|
||||||
extern volatile portreg_t shadowPORTB;
|
extern volatile portreg_t shadowPORTB;
|
||||||
extern volatile portreg_t shadowPORTC;
|
extern volatile portreg_t shadowPORTC;
|
||||||
|
extern volatile portreg_t shadowPORTD;
|
||||||
|
extern volatile portreg_t shadowPORTE;
|
||||||
|
extern volatile portreg_t shadowPORTF;
|
||||||
|
extern volatile portreg_t shadowPORTG;
|
||||||
|
extern volatile portreg_t shadowPORTH;
|
||||||
|
|
||||||
enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT };
|
enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT };
|
||||||
|
|
||||||
|
@ -126,7 +178,11 @@ class MotorDriver {
|
||||||
// otherwise the call from interrupt context can undo whatever we do
|
// otherwise the call from interrupt context can undo whatever we do
|
||||||
// from outside interrupt
|
// from outside interrupt
|
||||||
void setBrake( bool on, bool interruptContext=false);
|
void setBrake( bool on, bool interruptContext=false);
|
||||||
__attribute__((always_inline)) inline void setSignal( bool high) {
|
__attribute__((always_inline)) inline void setSignal( bool high) {
|
||||||
|
#ifndef ARDUINO_ARCH_ESP32
|
||||||
|
if (invertPhase)
|
||||||
|
high = !high;
|
||||||
|
#endif
|
||||||
if (trackPWM) {
|
if (trackPWM) {
|
||||||
DCCTimer::setPWM(signalPin,high);
|
DCCTimer::setPWM(signalPin,high);
|
||||||
}
|
}
|
||||||
|
@ -146,15 +202,22 @@ class MotorDriver {
|
||||||
pinMode(signalPin, OUTPUT);
|
pinMode(signalPin, OUTPUT);
|
||||||
else
|
else
|
||||||
pinMode(signalPin, INPUT);
|
pinMode(signalPin, INPUT);
|
||||||
|
if (signalPin2 != UNUSED_PIN) {
|
||||||
|
if (on)
|
||||||
|
pinMode(signalPin2, OUTPUT);
|
||||||
|
else
|
||||||
|
pinMode(signalPin2, INPUT);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
|
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
|
||||||
void setDCSignal(byte speedByte);
|
inline int8_t getBrakePinSigned() { return invertBrake ? -brakePin : brakePin; };
|
||||||
|
void setDCSignal(byte speedByte, uint8_t frequency=0);
|
||||||
void throttleInrush(bool on);
|
void throttleInrush(bool on);
|
||||||
inline void detachDCSignal() {
|
inline void detachDCSignal() {
|
||||||
#if defined(__arm__)
|
#if defined(__arm__)
|
||||||
pinMode(brakePin, OUTPUT);
|
pinMode(brakePin, OUTPUT);
|
||||||
#elif defined(ARDUINO_ARCH_ESP32)
|
#elif defined(ARDUINO_ARCH_ESP32)
|
||||||
ledcDetachPin(brakePin);
|
DCCTimer::DCCEXledcDetachPin(brakePin);
|
||||||
#else
|
#else
|
||||||
setDCSignal(128);
|
setDCSignal(128);
|
||||||
#endif
|
#endif
|
||||||
|
@ -163,16 +226,16 @@ class MotorDriver {
|
||||||
unsigned int raw2mA( int raw);
|
unsigned int raw2mA( int raw);
|
||||||
unsigned int mA2raw( unsigned int mA);
|
unsigned int mA2raw( unsigned int mA);
|
||||||
inline bool brakeCanPWM() {
|
inline bool brakeCanPWM() {
|
||||||
#if defined(ARDUINO_ARCH_ESP32) || defined(__arm__)
|
#if defined(ARDUINO_ARCH_ESP32)
|
||||||
// TODO: on ARM we can use digitalPinHasPWM, and may wish/need to
|
return (brakePin != UNUSED_PIN); // This was just (true) but we probably do need to check for UNUSED_PIN!
|
||||||
return true;
|
#elif defined(__arm__)
|
||||||
#else
|
// On ARM we can use digitalPinHasPWM
|
||||||
#ifdef digitalPinToTimer
|
return ((brakePin!=UNUSED_PIN) && (digitalPinHasPWM(brakePin)));
|
||||||
|
#elif defined(digitalPinToTimer)
|
||||||
return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));
|
return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));
|
||||||
#else
|
#else
|
||||||
return (brakePin<14 && brakePin >1);
|
return (brakePin<14 && brakePin >1);
|
||||||
#endif //digitalPinToTimer
|
#endif
|
||||||
#endif //ESP32/ARM
|
|
||||||
}
|
}
|
||||||
inline int getRawCurrentTripValue() {
|
inline int getRawCurrentTripValue() {
|
||||||
return rawCurrentTripValue;
|
return rawCurrentTripValue;
|
||||||
|
@ -210,6 +273,32 @@ class MotorDriver {
|
||||||
#endif
|
#endif
|
||||||
inline void setMode(TRACK_MODE m) {
|
inline void setMode(TRACK_MODE m) {
|
||||||
trackMode = m;
|
trackMode = m;
|
||||||
|
invertOutput(trackMode & TRACK_MODE_INV);
|
||||||
|
};
|
||||||
|
inline void invertOutput() { // toggles output inversion
|
||||||
|
invertPhase = !invertPhase;
|
||||||
|
invertOutput(invertPhase);
|
||||||
|
};
|
||||||
|
inline void invertOutput(bool b) { // sets output inverted or not
|
||||||
|
if (b)
|
||||||
|
invertPhase = 1;
|
||||||
|
else
|
||||||
|
invertPhase = 0;
|
||||||
|
#if defined(ARDUINO_ARCH_ESP32)
|
||||||
|
pinpair p = getSignalPin();
|
||||||
|
uint32_t *outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.pin);
|
||||||
|
if (invertPhase) // set or clear the invert bit in the gpio out register
|
||||||
|
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
|
||||||
|
else
|
||||||
|
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
|
||||||
|
if (p.invpin != UNUSED_PIN) {
|
||||||
|
outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.invpin);
|
||||||
|
if (invertPhase) // clear or set the invert bit in the gpio out register
|
||||||
|
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
|
||||||
|
else
|
||||||
|
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
inline TRACK_MODE getMode() {
|
inline TRACK_MODE getMode() {
|
||||||
return trackMode;
|
return trackMode;
|
||||||
|
@ -241,7 +330,7 @@ class MotorDriver {
|
||||||
bool invertBrake; // brake pin passed as negative means pin is inverted
|
bool invertBrake; // brake pin passed as negative means pin is inverted
|
||||||
bool invertPower; // power pin passed as negative means pin is inverted
|
bool invertPower; // power pin passed as negative means pin is inverted
|
||||||
bool invertFault; // fault pin passed as negative means pin is inverted
|
bool invertFault; // fault pin passed as negative means pin is inverted
|
||||||
|
bool invertPhase = 0; // phase of out pin is inverted
|
||||||
// Raw to milliamp conversion factors avoiding float data types.
|
// Raw to milliamp conversion factors avoiding float data types.
|
||||||
// Milliamps=rawADCreading * sensefactorInternal / senseScale
|
// Milliamps=rawADCreading * sensefactorInternal / senseScale
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* © 2022-2023 Paul M. Antoine
|
* © 2022-2023 Paul M. Antoine
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020-2023 Harald Barth
|
* © 2020-2024 Harald Barth
|
||||||
* (c) 2020 Chris Harlow. All rights reserved.
|
* (c) 2020 Chris Harlow. All rights reserved.
|
||||||
* (c) 2021 Fred Decker. All rights reserved.
|
* (c) 2021 Fred Decker. All rights reserved.
|
||||||
* (c) 2020 Harald Barth. All rights reserved.
|
* (c) 2020 Harald Barth. All rights reserved.
|
||||||
|
@ -57,6 +57,10 @@
|
||||||
// of the brake pin on the motor bridge is inverted
|
// of the brake pin on the motor bridge is inverted
|
||||||
// (HIGH == release brake)
|
// (HIGH == release brake)
|
||||||
|
|
||||||
|
// You can have a CS wihout any possibility to do any track signal.
|
||||||
|
// That's strange but possible.
|
||||||
|
#define NO_SHIELD F("No shield at all")
|
||||||
|
|
||||||
// Arduino STANDARD Motor Shield, used on different architectures:
|
// Arduino STANDARD Motor Shield, used on different architectures:
|
||||||
|
|
||||||
#if defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)
|
#if defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)
|
||||||
|
|
82
README.md
82
README.md
|
@ -2,80 +2,42 @@
|
||||||
<img src="/images/IMG_5870_1.jpg" height="400px" title="DCC++ Ex Hardware">
|
<img src="/images/IMG_5870_1.jpg" height="400px" title="DCC++ Ex Hardware">
|
||||||
|
|
||||||
|
|
||||||
# What is DCC++ EX?
|
# What is DCC-EX?
|
||||||
DCC++ EX is the organization maintaining several codebases that together represent a fully open source DCC system. Currently, this includes the following:
|
DCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your complete model railroad layout. Our easy to use, do-it-yourself, and free open source products run on off-the-shelf Arduino technology and are supported by numerous third party hardware and apps like JMRI, Engine Driver, wiThrottle, Rocrail and more.
|
||||||
|
|
||||||
* [CommandStation-EX](https://github.com/DCC-EX/CommandStation-EX/releases) - the latest take on the DCC++ command station for controlling your trains. Runs on an Arduino board, and includes advanced features such as a WiThrottle server implementation, turnout operation, general purpose inputs and outputs (I/O), and JMRI integration.
|
Currently, our products include the following:
|
||||||
* [exWebThrottle](https://github.com/DCC-EX/exWebThrottle) - a simple web based controller for your DCC++ command station.
|
|
||||||
* [BaseStation-installer](https://github.com/DCC-EX/BaseStation-Installer) - an installer executable that takes care of downloading and installing DCC++ firmware onto your hardware setup.
|
|
||||||
* [BaseStation-Classic](https://github.com/DCC-EX/BaseStation-Classic) - the original DCC++ software, packaged in a stable release. No active development, bug fixes only.
|
|
||||||
|
|
||||||
A basic DCC++ EX hardware setup can use easy to find, widely avalable Arduino boards that you can assemble yourself.
|
* [EX-CommandStation](https://github.com/DCC-EX/CommandStation-EX/releases)
|
||||||
|
* [EX-WebThrottle](https://github.com/DCC-EX/exWebThrottle)
|
||||||
Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital Command Control (DCC) [standards](http://www.nmra.org/dcc-working-group "NMRA DCC Working Group"), including:
|
* [EX-Installer](https://github.com/DCC-EX/EX-Installer)
|
||||||
|
* [EX-MotoShield8874](https://dcc-ex.com/reference/hardware/motorboards/ex-motor-shield-8874.html#gsc.tab=0)
|
||||||
* simultaneous control of multiple locomotives
|
* [EX-DCCInspector](https://github.com/DCC-EX/DCCInspector-EX)
|
||||||
* 2-byte and 4-byte locomotive addressing
|
* [EX-Toolbox](https://github.com/DCC-EX/EX-Toolbox)
|
||||||
* 28 or 128-step speed throttling
|
* [EX-Turntable](https://github.com/DCC-EX/EX-Turntable)
|
||||||
* Activate/de-activate all accessory function addresses 0-2048
|
* [EX-IOExpander](https://github.com/DCC-EX/EX-IOExpander)
|
||||||
* Control of all cab functions F0-F28 and F29-F68
|
* [EX-FastClock](https://github.com/DCC-EX/EX-FastClock)
|
||||||
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
|
* [DCCEXProtocol](https://github.com/DCC-EX/DCCEXProtocol)
|
||||||
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
|
|
||||||
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
|
||||||
|
|
||||||
|
Details of these projects can be found on [our web site](https://dcc-ex.com/).
|
||||||
|
|
||||||
# What’s in this Repository?
|
# What’s in this Repository?
|
||||||
|
|
||||||
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
|
This repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
|
||||||
|
|
||||||
To utilize this sketch, you can use the following:
|
To utilize this sketch, you can use the following:
|
||||||
|
|
||||||
1. (beginner) our [automated installer](https://github.com/DCC-EX/BaseStation-Installer)
|
1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)
|
||||||
2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)
|
2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)
|
||||||
3. (advanced) use git clone on this repository
|
3. (advanced) use git clone on this repository
|
||||||
|
|
||||||
Not using the installer? Open the file "CommandStation-EX.ino" in the
|
Refer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.
|
||||||
Arduino IDE. Please do not rename the folder containing the sketch
|
|
||||||
code, nor add any files in that folder. The Arduino IDE relies on the
|
|
||||||
structure and name of the folder to properly display and compile the
|
|
||||||
code. Rename or copy config.example.h to config.h. If you do not have
|
|
||||||
the standard setup, you must edit config.h according to the help texts
|
|
||||||
in config.h.
|
|
||||||
|
|
||||||
## What's new in CommandStation-EX?
|
**We seriously recommend using the EX-Installer**, however if you choose not to use the installer...
|
||||||
|
|
||||||
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station (or through JMRI as before)
|
* Open the file ``CommandStation-EX.ino`` in the Arduino IDE or Visual Studio Code (VSC). Please do not rename the folder containing the sketch code, nor add any files in that folder. The Arduino IDE relies on the structure and name of the folder to properly display and compile the code.
|
||||||
* WiFi and Ethernet shield support
|
* Rename or copy ``config.example.h`` to ``config.h``.
|
||||||
* No more jumpers or soldering!
|
* You must edit ``config.h`` according to the help texts in ``config.h``.
|
||||||
* Direct support for all the most popular motor control boards including single pin (Arduino) or dual pin (IBT_2) type PWM inputs without the need for an adapter circuit
|
|
||||||
* I2C Display support (LCD and OLED)
|
|
||||||
* Improved short circuit detection and automatic reset from an overload
|
|
||||||
* Current reading, sensing and ACK detection settings in milliAmps instead of just pin readings
|
|
||||||
* Improved adherence to the NMRA DCC specification
|
|
||||||
* Complete support for all the old commands and front ends like JMRI
|
|
||||||
* Railcom cutout (beta)
|
|
||||||
* Simpler, modular, faster code with an API Library for developers for easy expansion
|
|
||||||
* New features and functions in JMRI
|
|
||||||
* Ability to join MAIN and PROG tracks into one MAIN track to run your locos
|
|
||||||
* "Drive-Away" feature - Throttles with support, like Engine Driver, can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
|
|
||||||
* Diagnostic commands to test decoders that aren't reading or writing correctly
|
|
||||||
* Support for Uno, Nano, Mega, Nano Every and Teensy microcontrollers
|
|
||||||
* User Functions: Filter regular commands (like a turnout or output command) and pass it to your own function or accessory
|
|
||||||
* Support for LCN (layout control nodes)
|
|
||||||
* mySetup.h file that acts like an Autoexec.Bat command to send startup commands to the CS
|
|
||||||
* High Accuracty Waveform option for rock steady DCC signals
|
|
||||||
* New current response outputs current in mA, overlimit current, and maximum board capable current. Support for new current meter in JMRI
|
|
||||||
* USB Browser based EX-WebThrottle
|
|
||||||
* New, simpler, function control command
|
|
||||||
* Number of locos discovery command `<#>`
|
|
||||||
* Emergency stop command <!>
|
|
||||||
* Release cabs from memory command <-> all cabs, <- CAB> for just one loco address
|
|
||||||
* Automatic slot (register) management
|
|
||||||
* Automation (coming soon)
|
|
||||||
|
|
||||||
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
|
|
||||||
|
|
||||||
# More information
|
# More information
|
||||||
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)
|
You can learn more at the [DCC-EX website](https://dcc-ex.com/)
|
||||||
|
|
||||||
- November 14, 2020
|
|
||||||
|
|
BIN
README.pdf
Normal file
BIN
README.pdf
Normal file
Binary file not shown.
119
Release_Notes/Exrail mods.txt
Normal file
119
Release_Notes/Exrail mods.txt
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// 5.2.49
|
||||||
|
|
||||||
|
Which is a more efficient than the AT/AFTER/IF methods
|
||||||
|
of handling buttons and switches, especially on MIMIC panels.
|
||||||
|
|
||||||
|
ONBUTTON(vpin)
|
||||||
|
handles debounce and starts a task if a button is used to
|
||||||
|
short a pin to ground.
|
||||||
|
|
||||||
|
for example:
|
||||||
|
ONBUTTON(30) TOGGLE_TURNOUT(30) DONE
|
||||||
|
|
||||||
|
ONSENSOR(vpin)
|
||||||
|
handles debounce and starts a task if the pin changes.
|
||||||
|
You may want to check the pin state with an IF ...
|
||||||
|
|
||||||
|
Note the ONBUTTON and ONSENSOR are not generally useful
|
||||||
|
for track sensors and running trains, because you dont know which
|
||||||
|
train triggered the sensor.
|
||||||
|
|
||||||
|
// 5.2.47
|
||||||
|
|
||||||
|
BLINK(vpin, onMs,offMs)
|
||||||
|
|
||||||
|
which will start a vpin blinking until such time as it is SET, RESET or set by a signal operation such as RED, AMBER, GREEN.
|
||||||
|
|
||||||
|
BLINK returns immediately, the blinking is autonomous.
|
||||||
|
|
||||||
|
This means a signal that always blinks amber could be done like this:
|
||||||
|
|
||||||
|
SIGNAL(30,31,32)
|
||||||
|
ONAMBER(30) BLINK(31,500,500) DONE
|
||||||
|
|
||||||
|
The RED or GREEN calls will turn off the amber blink automatically.
|
||||||
|
|
||||||
|
Alternatively a signal that has normal AMBER and flashing AMBER could be like this:
|
||||||
|
|
||||||
|
#define FLASHAMBER(signal) \
|
||||||
|
AMBER(signal) \
|
||||||
|
BLINK(signal+1,500,500)
|
||||||
|
|
||||||
|
(Caution: this assumes that the amber pin is redpin+1)
|
||||||
|
|
||||||
|
==
|
||||||
|
|
||||||
|
FTOGGLE(function)
|
||||||
|
Toggles the current loco function (see FON and FOFF)
|
||||||
|
|
||||||
|
XFTOGGLE(loco,function)
|
||||||
|
Toggles the function on given loco. (See XFON, XFOFF)
|
||||||
|
|
||||||
|
TOGGLE_TURNOUT(id)
|
||||||
|
Toggles the turnout (see CLOSE, THROW)
|
||||||
|
|
||||||
|
STEALTH_GLOBAL(code)
|
||||||
|
ADVANCED C++ users only.
|
||||||
|
Inserts code such as static variables and functions that
|
||||||
|
may be utilised by multiple STEALTH operations.
|
||||||
|
|
||||||
|
|
||||||
|
// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories.
|
||||||
|
This command sends an extended accessory packet to the track, Normally used to set
|
||||||
|
a signal aspect. Aspect numbers are undefined as sdtandards except for 0 which is
|
||||||
|
always considered a stop.
|
||||||
|
|
||||||
|
// - Exrail ASPECT(address,aspect) for above.
|
||||||
|
The ASPECT command sents an aspect to a DCC accessory using the same logic as
|
||||||
|
<A aspect address>.
|
||||||
|
|
||||||
|
// - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)
|
||||||
|
This defines a signal (with id same as dcc address) that can be operated
|
||||||
|
by the RED/AMBER/GREEN commands. In each case the command uses the signal
|
||||||
|
address to refer to the signal and the aspect chosen depends on the use of the RED
|
||||||
|
AMBER or GREEN command sent. Other aspects may be sent but will require the
|
||||||
|
direct use of the ASPECT command.
|
||||||
|
The IFRED/IFAMBER/IFGREEN and ONRED/ONAMBER/ONGREEN commands contunue to operate
|
||||||
|
as for any other signal type. It is important to be aware that use of the ASPECT
|
||||||
|
or <A> commands will correctly set the IF flags and call the ON handlers if ASPECT
|
||||||
|
is used to set one of the three aspects defined in the DCCX_SIGNAL command.
|
||||||
|
Direct use of other aspects does not affect the signal flags.
|
||||||
|
ASPECT and <A> can be used withput defining any signal if tyhe flag management or
|
||||||
|
ON event handlers are not required.
|
||||||
|
|
||||||
|
// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||||
|
This macro offsers a more convenient way of performing the HAL call in halSetup.h
|
||||||
|
In halSetup.h --- IODevice::configureServo(101,300,400,PCA9685::slow);
|
||||||
|
In myAutomation.h --- CONFIGURE_SERVO(101,300,400,slow)
|
||||||
|
|
||||||
|
// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)
|
||||||
|
This cutout will only work on a Mega2560 with a single EX8874 motor shield
|
||||||
|
configured in the normal way with the main track brake pin on pin 9.
|
||||||
|
<C RAILCOM ON> Turns on the cutout mechanism.
|
||||||
|
<C RAILCOM OFF> Tirns off the cutout. (This is the default)
|
||||||
|
<C RAILCOM DEBUG> ONLY to be used by developers used for waveform diagnostics.
|
||||||
|
(In DEBUG mode the main track idle packets are replaced with reset packets, This
|
||||||
|
makes it far easier to see the preambles and cutouts on a logic analyser or scope.)
|
||||||
|
|
||||||
|
// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.
|
||||||
|
This Macro causes the creation of JMRI <S> type sensors in a way that is
|
||||||
|
simpler than repeating lines of <S> commands.
|
||||||
|
JMRI_SENSOR(100) is equenvelant to <S 100 100 1>
|
||||||
|
JMRI_SENSOR(100,16) will create <S> type sensors for vpins 100-115.
|
||||||
|
|
||||||
|
// 5.2.26 - Silently ignore overridden HAL defaults
|
||||||
|
// - include HAL_IGNORE_DEFAULTS macro in EXRAIL
|
||||||
|
The HAL_IGNORE_DEFAULTS command, anywhere in myAutomation.h will
|
||||||
|
prevent the startup code from trying the default I2C sensors/servos.
|
||||||
|
// 5.2.24 - Exrail macro asserts to catch
|
||||||
|
// : duplicate/missing automation/route/sequence/call ids
|
||||||
|
// : latches and reserves out of range
|
||||||
|
// : speeds out of range
|
||||||
|
Causes compiler time messages for EXRAIL issues that would normally
|
||||||
|
only be discovered by things going wrong at run time.
|
||||||
|
// 5.2.13 - EXRAIL STEALTH
|
||||||
|
Permits a certain level of C++ code to be embedded as a single step in
|
||||||
|
an exrail sequence. Serious engineers only.
|
||||||
|
|
||||||
|
// 5.2.9 - EXRAIL STASH feature
|
||||||
|
// - Added ROUTE_DISABLED macro in EXRAIL
|
|
@ -230,6 +230,13 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
|
||||||
return tt;
|
return tt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creet multiple eponymous sensors based on vpin alone.
|
||||||
|
void Sensor::createMultiple(VPIN firstPin, byte count) {
|
||||||
|
for (byte i=0;i<count;i++) {
|
||||||
|
create(firstPin+i,firstPin+i,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Object method to directly change the input state, for sensors such as LCN which are updated
|
// Object method to directly change the input state, for sensors such as LCN which are updated
|
||||||
// by means other than by polling an input.
|
// by means other than by polling an input.
|
||||||
|
|
|
@ -76,6 +76,7 @@ public:
|
||||||
static void store();
|
static void store();
|
||||||
#endif
|
#endif
|
||||||
static Sensor *create(int id, VPIN vpin, int pullUp);
|
static Sensor *create(int id, VPIN vpin, int pullUp);
|
||||||
|
static void createMultiple(VPIN firstPin, byte count=1);
|
||||||
static Sensor* get(int id);
|
static Sensor* get(int id);
|
||||||
static bool remove(int id);
|
static bool remove(int id);
|
||||||
static void checkAll();
|
static void checkAll();
|
||||||
|
|
|
@ -111,14 +111,15 @@ void SerialManager::loop2() {
|
||||||
bufferLength = 0;
|
bufferLength = 0;
|
||||||
buffer[0] = '\0';
|
buffer[0] = '\0';
|
||||||
}
|
}
|
||||||
else if (ch == '>') {
|
else if (inCommandPayload) {
|
||||||
buffer[bufferLength] = '\0';
|
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
|
||||||
DCCEXParser::parse(serial, buffer, NULL);
|
buffer[bufferLength++] = ch;
|
||||||
inCommandPayload = false;
|
if (ch == '>') {
|
||||||
break;
|
buffer[bufferLength] = '\0';
|
||||||
}
|
DCCEXParser::parse(serial, buffer, NULL);
|
||||||
else if (inCommandPayload) {
|
inCommandPayload = false;
|
||||||
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) buffer[bufferLength++] = ch;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#include "StringFormatter.h"
|
#include "StringFormatter.h"
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include "DisplayInterface.h"
|
#include "DisplayInterface.h"
|
||||||
|
#include "CommandDistributor.h"
|
||||||
|
|
||||||
bool Diag::ACK=false;
|
bool Diag::ACK=false;
|
||||||
bool Diag::CMD=false;
|
bool Diag::CMD=false;
|
||||||
|
@ -38,13 +39,28 @@ void StringFormatter::diag( const FSH* input...) {
|
||||||
|
|
||||||
void StringFormatter::lcd(byte row, const FSH* input...) {
|
void StringFormatter::lcd(byte row, const FSH* input...) {
|
||||||
va_list args;
|
va_list args;
|
||||||
|
#ifndef DISABLE_VDPY
|
||||||
|
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row);
|
||||||
|
#else
|
||||||
|
Print * virtualLCD=NULL;
|
||||||
|
#endif
|
||||||
// Issue the LCD as a diag first
|
// Issue the LCD as a diag first
|
||||||
send(&USB_SERIAL,F("<* LCD%d:"),row);
|
// Unless the same serial is asking for the virtual @ respomnse
|
||||||
va_start(args, input);
|
if (virtualLCD!=&USB_SERIAL) {
|
||||||
send2(&USB_SERIAL,input,args);
|
send(&USB_SERIAL,F("<* LCD%d:"),row);
|
||||||
send(&USB_SERIAL,F(" *>\n"));
|
va_start(args, input);
|
||||||
|
send2(&USB_SERIAL,input,args);
|
||||||
|
send(&USB_SERIAL,F(" *>\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef DISABLE_VDPY
|
||||||
|
// send to virtual LCD collector (if any)
|
||||||
|
if (virtualLCD) {
|
||||||
|
va_start(args, input);
|
||||||
|
send2(virtualLCD,input,args);
|
||||||
|
CommandDistributor::commitVirtualLCDSerial();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
DisplayInterface::setRow(row);
|
DisplayInterface::setRow(row);
|
||||||
va_start(args, input);
|
va_start(args, input);
|
||||||
send2(DisplayInterface::getDisplayHandler(),input,args);
|
send2(DisplayInterface::getDisplayHandler(),input,args);
|
||||||
|
@ -53,6 +69,16 @@ void StringFormatter::lcd(byte row, const FSH* input...) {
|
||||||
void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {
|
void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {
|
||||||
va_list args;
|
va_list args;
|
||||||
|
|
||||||
|
// send to virtual LCD collector (if any)
|
||||||
|
#ifndef DISABLE_VDPY
|
||||||
|
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row);
|
||||||
|
if (virtualLCD) {
|
||||||
|
va_start(args, input);
|
||||||
|
send2(virtualLCD,input,args);
|
||||||
|
CommandDistributor::commitVirtualLCDSerial();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
DisplayInterface::setRow(display, row);
|
DisplayInterface::setRow(display, row);
|
||||||
va_start(args, input);
|
va_start(args, input);
|
||||||
send2(DisplayInterface::getDisplayHandler(),input,args);
|
send2(DisplayInterface::getDisplayHandler(),input,args);
|
||||||
|
@ -117,6 +143,7 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
|
||||||
case 'o': stream->print(va_arg(args, int), OCT); break;
|
case 'o': stream->print(va_arg(args, int), OCT); break;
|
||||||
case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;
|
case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;
|
||||||
case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break;
|
case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break;
|
||||||
|
case 'h': printHex(stream,(unsigned int)va_arg(args, unsigned int)); break;
|
||||||
case 'M':
|
case 'M':
|
||||||
{ // this prints a unsigned long microseconds time in readable format
|
{ // this prints a unsigned long microseconds time in readable format
|
||||||
unsigned long time = va_arg(args, long);
|
unsigned long time = va_arg(args, long);
|
||||||
|
@ -218,4 +245,14 @@ void StringFormatter::printPadded(Print* stream, long value, byte width, bool fo
|
||||||
if (!formatLeft) stream->print(value, DEC);
|
if (!formatLeft) stream->print(value, DEC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printHex prints the full 2 byte hex with leading zeros, unlike print(value,HEX)
|
||||||
|
const char FLASH hexchars[]="0123456789ABCDEF";
|
||||||
|
void StringFormatter::printHex(Print * stream,uint16_t value) {
|
||||||
|
char result[5];
|
||||||
|
for (int i=3;i>=0;i--) {
|
||||||
|
result[i]=GETFLASH(hexchars+(value & 0x0F));
|
||||||
|
value>>=4;
|
||||||
|
}
|
||||||
|
result[4]='\0';
|
||||||
|
stream->print(result);
|
||||||
|
}
|
||||||
|
|
|
@ -49,10 +49,10 @@ class StringFormatter
|
||||||
static void lcd2(uint8_t display, byte row, const FSH* input...);
|
static void lcd2(uint8_t display, byte row, const FSH* input...);
|
||||||
static void printEscapes(char * input);
|
static void printEscapes(char * input);
|
||||||
static void printEscape( char c);
|
static void printEscape( char c);
|
||||||
|
static void printHex(Print * stream,uint16_t value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void send2(Print * serial, const FSH* input,va_list args);
|
static void send2(Print * serial, const FSH* input,va_list args);
|
||||||
static void printPadded(Print* stream, long value, byte width, bool formatLeft);
|
static void printPadded(Print* stream, long value, byte width, bool formatLeft);
|
||||||
|
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
425
TrackManager.cpp
425
TrackManager.cpp
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* © 2022 Chris Harlow
|
* © 2022 Chris Harlow
|
||||||
* © 2022 Harald Barth
|
* © 2022-2024 Harald Barth
|
||||||
|
* © 2023 Colin Murdoch
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of DCC++EX
|
* This file is part of DCC++EX
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
#include "defines.h"
|
||||||
#include "TrackManager.h"
|
#include "TrackManager.h"
|
||||||
#include "FSH.h"
|
#include "FSH.h"
|
||||||
#include "DCCWaveform.h"
|
#include "DCCWaveform.h"
|
||||||
|
@ -25,30 +27,21 @@
|
||||||
#include "MotorDriver.h"
|
#include "MotorDriver.h"
|
||||||
#include "DCCTimer.h"
|
#include "DCCTimer.h"
|
||||||
#include "DIAG.h"
|
#include "DIAG.h"
|
||||||
#include"CommandDistributor.h"
|
#include "CommandDistributor.h"
|
||||||
|
#include "DCCEXParser.h"
|
||||||
|
#include "KeywordHasher.h"
|
||||||
// Virtualised Motor shield multi-track hardware Interface
|
// Virtualised Motor shield multi-track hardware Interface
|
||||||
#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)
|
#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)
|
||||||
|
|
||||||
#define APPLY_BY_MODE(findmode,function) \
|
#define APPLY_BY_MODE(findmode,function) \
|
||||||
FOR_EACH_TRACK(t) \
|
FOR_EACH_TRACK(t) \
|
||||||
if (track[t]->getMode()==findmode) \
|
if (track[t]->getMode() & findmode) \
|
||||||
track[t]->function;
|
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];
|
MotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };
|
||||||
int16_t TrackManager::trackDCAddr[MAX_TRACKS];
|
int16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };
|
||||||
|
|
||||||
POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF;
|
int8_t TrackManager::lastTrack=-1;
|
||||||
byte TrackManager::lastTrack=0;
|
|
||||||
bool TrackManager::progTrackSyncMain=false;
|
bool TrackManager::progTrackSyncMain=false;
|
||||||
bool TrackManager::progTrackBoosted=false;
|
bool TrackManager::progTrackBoosted=false;
|
||||||
int16_t TrackManager::joinRelay=UNUSED_PIN;
|
int16_t TrackManager::joinRelay=UNUSED_PIN;
|
||||||
|
@ -85,7 +78,7 @@ void TrackManager::sampleCurrent() {
|
||||||
if (!waiting) {
|
if (!waiting) {
|
||||||
// look for a valid track to sample or until we are around
|
// look for a valid track to sample or until we are around
|
||||||
while (true) {
|
while (true) {
|
||||||
if (track[tr]->getMode() & ( 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_BOOST|TRACK_MODE_EXT )) {
|
||||||
track[tr]->startCurrentFromHW();
|
track[tr]->startCurrentFromHW();
|
||||||
// for scope debug track[1]->setBrake(1);
|
// for scope debug track[1]->setBrake(1);
|
||||||
waiting = true;
|
waiting = true;
|
||||||
|
@ -153,16 +146,16 @@ void TrackManager::setDCCSignal( bool on) {
|
||||||
HAVE_PORTA(shadowPORTA=PORTA);
|
HAVE_PORTA(shadowPORTA=PORTA);
|
||||||
HAVE_PORTB(shadowPORTB=PORTB);
|
HAVE_PORTB(shadowPORTB=PORTB);
|
||||||
HAVE_PORTC(shadowPORTC=PORTC);
|
HAVE_PORTC(shadowPORTC=PORTC);
|
||||||
|
HAVE_PORTD(shadowPORTD=PORTD);
|
||||||
|
HAVE_PORTE(shadowPORTE=PORTE);
|
||||||
|
HAVE_PORTF(shadowPORTF=PORTF);
|
||||||
APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on));
|
APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on));
|
||||||
HAVE_PORTA(PORTA=shadowPORTA);
|
HAVE_PORTA(PORTA=shadowPORTA);
|
||||||
HAVE_PORTB(PORTB=shadowPORTB);
|
HAVE_PORTB(PORTB=shadowPORTB);
|
||||||
HAVE_PORTC(PORTC=shadowPORTC);
|
HAVE_PORTC(PORTC=shadowPORTC);
|
||||||
}
|
HAVE_PORTD(PORTD=shadowPORTD);
|
||||||
|
HAVE_PORTE(PORTE=shadowPORTE);
|
||||||
void TrackManager::setCutout( bool on) {
|
HAVE_PORTF(PORTF=shadowPORTF);
|
||||||
(void) on;
|
|
||||||
// TODO Cutout needs fake ports as well
|
|
||||||
// TODO APPLY_BY_MODE(TRACK_MODE_MAIN,setCutout(on));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPROGSignal(), called from interrupt context
|
// setPROGSignal(), called from interrupt context
|
||||||
|
@ -171,10 +164,16 @@ void TrackManager::setPROGSignal( bool on) {
|
||||||
HAVE_PORTA(shadowPORTA=PORTA);
|
HAVE_PORTA(shadowPORTA=PORTA);
|
||||||
HAVE_PORTB(shadowPORTB=PORTB);
|
HAVE_PORTB(shadowPORTB=PORTB);
|
||||||
HAVE_PORTC(shadowPORTC=PORTC);
|
HAVE_PORTC(shadowPORTC=PORTC);
|
||||||
|
HAVE_PORTD(shadowPORTD=PORTD);
|
||||||
|
HAVE_PORTE(shadowPORTE=PORTE);
|
||||||
|
HAVE_PORTF(shadowPORTF=PORTF);
|
||||||
APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on));
|
APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on));
|
||||||
HAVE_PORTA(PORTA=shadowPORTA);
|
HAVE_PORTA(PORTA=shadowPORTA);
|
||||||
HAVE_PORTB(PORTB=shadowPORTB);
|
HAVE_PORTB(PORTB=shadowPORTB);
|
||||||
HAVE_PORTC(PORTC=shadowPORTC);
|
HAVE_PORTC(PORTC=shadowPORTC);
|
||||||
|
HAVE_PORTD(PORTD=shadowPORTD);
|
||||||
|
HAVE_PORTE(PORTE=shadowPORTE);
|
||||||
|
HAVE_PORTF(PORTF=shadowPORTF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// setDCSignal(), called from normal context
|
// setDCSignal(), called from normal context
|
||||||
|
@ -183,17 +182,20 @@ void TrackManager::setPROGSignal( bool on) {
|
||||||
void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
|
void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
|
||||||
FOR_EACH_TRACK(t) {
|
FOR_EACH_TRACK(t) {
|
||||||
if (trackDCAddr[t]!=cab && cab != 0) continue;
|
if (trackDCAddr[t]!=cab && cab != 0) continue;
|
||||||
if (track[t]->getMode()==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte);
|
if (track[t]->getMode() & TRACK_MODE_DC)
|
||||||
else if (track[t]->getMode()==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128);
|
track[t]->setDCSignal(speedbyte, DCC::getThrottleFrequency(trackDCAddr[t]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
|
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
|
||||||
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
|
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
|
||||||
|
|
||||||
|
// Remember track mode we came from for later
|
||||||
|
TRACK_MODE oldmode = track[trackToSet]->getMode();
|
||||||
|
|
||||||
//DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode);
|
//DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode);
|
||||||
// DC tracks require a motorDriver that can set brake!
|
// DC tracks require a motorDriver that can set brake!
|
||||||
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
|
if (mode & TRACK_MODE_DC) {
|
||||||
#if defined(ARDUINO_AVR_UNO)
|
#if defined(ARDUINO_AVR_UNO)
|
||||||
DIAG(F("Uno has no PWM timers available for DC"));
|
DIAG(F("Uno has no PWM timers available for DC"));
|
||||||
return false;
|
return false;
|
||||||
|
@ -209,48 +211,96 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||||
pinpair p = track[trackToSet]->getSignalPin();
|
pinpair p = track[trackToSet]->getSignalPin();
|
||||||
//DIAG(F("Track=%c remove pin %d"),trackToSet+'A', p.pin);
|
//DIAG(F("Track=%c remove pin %d"),trackToSet+'A', p.pin);
|
||||||
gpio_reset_pin((gpio_num_t)p.pin);
|
gpio_reset_pin((gpio_num_t)p.pin);
|
||||||
pinMode(p.pin, OUTPUT); // gpio_reset_pin may reset to input
|
|
||||||
if (p.invpin != UNUSED_PIN) {
|
if (p.invpin != UNUSED_PIN) {
|
||||||
//DIAG(F("Track=%c remove ^pin %d"),trackToSet+'A', p.invpin);
|
//DIAG(F("Track=%c remove ^pin %d"),trackToSet+'A', p.invpin);
|
||||||
gpio_reset_pin((gpio_num_t)p.invpin);
|
gpio_reset_pin((gpio_num_t)p.invpin);
|
||||||
pinMode(p.invpin, OUTPUT); // gpio_reset_pin may reset to input
|
|
||||||
}
|
}
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
if (mode & TRACK_MODE_BOOST) {
|
||||||
|
//DIAG(F("Track=%c mode boost pin %d"),trackToSet+'A', p.pin);
|
||||||
|
pinMode(BOOSTER_INPUT, INPUT);
|
||||||
|
gpio_matrix_in(BOOSTER_INPUT, SIG_IN_FUNC228_IDX, false); //pads 224 to 228 available as loopback
|
||||||
|
gpio_matrix_out(p.pin, SIG_IN_FUNC228_IDX, false, false);
|
||||||
|
if (p.invpin != UNUSED_PIN) {
|
||||||
|
gpio_matrix_out(p.invpin, SIG_IN_FUNC228_IDX, true /*inverted*/, false);
|
||||||
|
}
|
||||||
|
} else // elseif clause continues
|
||||||
|
#endif
|
||||||
|
if (mode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_DC)) {
|
||||||
|
// gpio_reset_pin may reset to input
|
||||||
|
pinMode(p.pin, OUTPUT);
|
||||||
|
if (p.invpin != UNUSED_PIN)
|
||||||
|
pinMode(p.invpin, OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
if (mode==TRACK_MODE_PROG) {
|
if (mode & TRACK_MODE_PROG) {
|
||||||
#else
|
#else
|
||||||
if (false) {
|
if (false) {
|
||||||
#endif
|
#endif
|
||||||
// only allow 1 track to be prog
|
// only allow 1 track to be prog
|
||||||
FOR_EACH_TRACK(t)
|
FOR_EACH_TRACK(t)
|
||||||
if (track[t]->getMode()==TRACK_MODE_PROG && t != trackToSet) {
|
if ( (track[t]->getMode() & TRACK_MODE_PROG) && t != trackToSet) {
|
||||||
track[t]->setPower(POWERMODE::OFF);
|
track[t]->setPower(POWERMODE::OFF);
|
||||||
track[t]->setMode(TRACK_MODE_NONE);
|
track[t]->setMode(TRACK_MODE_NONE);
|
||||||
track[t]->makeProgTrack(false); // revoke prog track special handling
|
track[t]->makeProgTrack(false); // revoke prog track special handling
|
||||||
streamTrackState(NULL,t);
|
streamTrackState(NULL,t);
|
||||||
}
|
}
|
||||||
track[trackToSet]->makeProgTrack(true); // set for prog track special handling
|
track[trackToSet]->makeProgTrack(true); // set for prog track special handling
|
||||||
} else {
|
} else {
|
||||||
track[trackToSet]->makeProgTrack(false); // only the prog track knows it's type
|
track[trackToSet]->makeProgTrack(false); // only the prog track knows it's type
|
||||||
}
|
}
|
||||||
track[trackToSet]->setMode(mode);
|
|
||||||
trackDCAddr[trackToSet]=dcAddr;
|
|
||||||
streamTrackState(NULL,trackToSet);
|
|
||||||
|
|
||||||
// When a track is switched, we must clear any side effects of its previous
|
// When a track is switched, we must clear any side effects of its previous
|
||||||
// state, otherwise trains run away or just dont move.
|
// state, otherwise trains run away or just dont move.
|
||||||
|
|
||||||
// This can be done BEFORE the PWM-Timer evaluation (methinks)
|
// This can be done BEFORE the PWM-Timer evaluation (methinks)
|
||||||
if (!(mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX)) {
|
if (mode & TRACK_MODE_DC) {
|
||||||
|
if (trackDCAddr[trackToSet] != dcAddr) {
|
||||||
|
// new or changed DC Addr, run the new setup
|
||||||
|
if (trackDCAddr[trackToSet] != 0) {
|
||||||
|
// if we change dcAddr and not only
|
||||||
|
// change from another mode,
|
||||||
|
// first detach old DC signal
|
||||||
|
track[trackToSet]->detachDCSignal();
|
||||||
|
}
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
int trackfound = -1;
|
||||||
|
FOR_EACH_TRACK(t) {
|
||||||
|
//DIAG(F("Checking track %c mode %x dcAddr %d"), 'A'+t, track[t]->getMode(), trackDCAddr[t]);
|
||||||
|
if (t != trackToSet // not our track
|
||||||
|
&& (track[t]->getMode() & TRACK_MODE_DC) // right mode
|
||||||
|
&& trackDCAddr[t] == dcAddr) { // right addr
|
||||||
|
//DIAG(F("Found track %c"), 'A'+t);
|
||||||
|
trackfound = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trackfound > -1) {
|
||||||
|
DCCTimer::DCCEXanalogCopyChannel(track[trackfound]->getBrakePinSigned(),
|
||||||
|
track[trackToSet]->getBrakePinSigned());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
// set future DC Addr;
|
||||||
|
trackDCAddr[trackToSet]=dcAddr;
|
||||||
|
} else {
|
||||||
// DCC tracks need to have set the PWM to zero or they will not work.
|
// DCC tracks need to have set the PWM to zero or they will not work.
|
||||||
track[trackToSet]->detachDCSignal();
|
track[trackToSet]->detachDCSignal();
|
||||||
track[trackToSet]->setBrake(false);
|
track[trackToSet]->setBrake(false);
|
||||||
|
trackDCAddr[trackToSet]=0; // clear that an addr is set for DC as this is not a DC track
|
||||||
}
|
}
|
||||||
|
track[trackToSet]->setMode(mode);
|
||||||
|
|
||||||
// EXT is a special case where the signal pin is
|
// BOOST:
|
||||||
// turned off. So unless that is set, the signal
|
// Leave it as is
|
||||||
// pin should be turned on
|
// otherwise:
|
||||||
track[trackToSet]->enableSignal(mode != TRACK_MODE_EXT);
|
// EXT is a special case where the signal pin is
|
||||||
|
// turned off. So unless that is set, the signal
|
||||||
|
// pin should be turned on
|
||||||
|
if (!(mode & TRACK_MODE_BOOST))
|
||||||
|
track[trackToSet]->enableSignal(!(mode & TRACK_MODE_EXT));
|
||||||
|
|
||||||
#ifndef ARDUINO_ARCH_ESP32
|
#ifndef ARDUINO_ARCH_ESP32
|
||||||
// re-evaluate HighAccuracy mode
|
// re-evaluate HighAccuracy mode
|
||||||
|
@ -260,7 +310,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||||
// DC tracks must not have the DCC PWM switched on
|
// DC tracks must not have the DCC PWM switched on
|
||||||
// so we globally turn it off if one of the PWM
|
// so we globally turn it off if one of the PWM
|
||||||
// capable tracks is now DC or DCX.
|
// capable tracks is now DC or DCX.
|
||||||
if (track[t]->getMode()==TRACK_MODE_DC || track[t]->getMode()==TRACK_MODE_DCX) {
|
if (track[t]->getMode() & TRACK_MODE_DC) {
|
||||||
if (track[t]->isPWMCapable()) {
|
if (track[t]->isPWMCapable()) {
|
||||||
canDo=false; // this track is capable but can not run PWM
|
canDo=false; // this track is capable but can not run PWM
|
||||||
break; // in this mode, so abort and prevent globally below
|
break; // in this mode, so abort and prevent globally below
|
||||||
|
@ -268,7 +318,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||||
track[t]->trackPWM=false; // this track sure can not run with PWM
|
track[t]->trackPWM=false; // this track sure can not run with PWM
|
||||||
//DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A');
|
//DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A');
|
||||||
}
|
}
|
||||||
} else if (track[t]->getMode()==TRACK_MODE_MAIN || track[t]->getMode()==TRACK_MODE_PROG) {
|
} else if (track[t]->getMode() & (TRACK_MODE_MAIN |TRACK_MODE_PROG)) {
|
||||||
track[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here
|
track[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here
|
||||||
//DIAG(F("Track %c trackPWM %d"), t+'A', track[t]->trackPWM);
|
//DIAG(F("Track %c trackPWM %d"), t+'A', track[t]->trackPWM);
|
||||||
canDo &= track[t]->trackPWM;
|
canDo &= track[t]->trackPWM;
|
||||||
|
@ -286,98 +336,135 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||||
#else
|
#else
|
||||||
// For ESP32 we just reinitialize the DCC Waveform
|
// For ESP32 we just reinitialize the DCC Waveform
|
||||||
DCCWaveform::begin();
|
DCCWaveform::begin();
|
||||||
|
// setMode() again AFTER Waveform::begin() of ESP32 fixes INVERTED signal
|
||||||
|
track[trackToSet]->setMode(mode);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// This block must be AFTER the PWM-Timer modifications
|
// This block must be AFTER the PWM-Timer modifications
|
||||||
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
|
if (mode & TRACK_MODE_DC) {
|
||||||
// DC tracks need to be given speed of the throttle for that cab address
|
// DC tracks need to be given speed of the throttle for that cab address
|
||||||
// otherwise will not match other tracks on same cab.
|
// otherwise will not match other tracks on same cab.
|
||||||
// This also needs to allow for inverted DCX
|
// This also needs to allow for inverted DCX
|
||||||
applyDCSpeed(trackToSet);
|
applyDCSpeed(trackToSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal running tracks are set to the global power state
|
// Turn off power if we changed the mode of this track
|
||||||
track[trackToSet]->setPower(
|
if (mode != oldmode)
|
||||||
(mode==TRACK_MODE_MAIN || mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX || mode==TRACK_MODE_EXT) ?
|
track[trackToSet]->setPower(POWERMODE::OFF);
|
||||||
mainPowerGuess : POWERMODE::OFF);
|
streamTrackState(NULL,trackToSet);
|
||||||
|
|
||||||
//DIAG(F("TrackMode=%d"),mode);
|
//DIAG(F("TrackMode=%d"),mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackManager::applyDCSpeed(byte t) {
|
void TrackManager::applyDCSpeed(byte t) {
|
||||||
uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]);
|
track[t]->setDCSignal(DCC::getThrottleSpeedByte(trackDCAddr[t]),
|
||||||
if (track[t]->getMode()==TRACK_MODE_DCX)
|
DCC::getThrottleFrequency(trackDCAddr[t]));
|
||||||
speedByte = speedByte ^ 128; // reverse direction bit
|
|
||||||
track[t]->setDCSignal(speedByte);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[])
|
bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
|
||||||
{
|
{
|
||||||
|
|
||||||
if (params==0) { // <=> List track assignments
|
if (params==0) { // <=> List track assignments
|
||||||
FOR_EACH_TRACK(t)
|
FOR_EACH_TRACK(t)
|
||||||
streamTrackState(stream,t);
|
streamTrackState(stream,t);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p[0]-=HASH_KEYWORD_A; // convert A... to 0....
|
p[0]-="A"_hk; // convert A... to 0....
|
||||||
|
|
||||||
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
|
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (params==2 && p[1]==HASH_KEYWORD_MAIN) // <= id MAIN>
|
if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
|
||||||
return setTrackMode(p[0],TRACK_MODE_MAIN);
|
return setTrackMode(p[0],TRACK_MODE_MAIN);
|
||||||
|
|
||||||
#ifndef DISABLE_PROG
|
#ifndef DISABLE_PROG
|
||||||
if (params==2 && p[1]==HASH_KEYWORD_PROG) // <= id PROG>
|
if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
|
||||||
return setTrackMode(p[0],TRACK_MODE_PROG);
|
return setTrackMode(p[0],TRACK_MODE_PROG);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (params==2 && (p[1]==HASH_KEYWORD_OFF || p[1]==HASH_KEYWORD_NONE)) // <= id OFF> <= id NONE>
|
if (params==2 && (p[1]=="OFF"_hk || p[1]=="NONE"_hk)) // <= id OFF> <= id NONE>
|
||||||
return setTrackMode(p[0],TRACK_MODE_NONE);
|
return setTrackMode(p[0],TRACK_MODE_NONE);
|
||||||
|
|
||||||
if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT>
|
if (params==2 && p[1]=="EXT"_hk) // <= id EXT>
|
||||||
return setTrackMode(p[0],TRACK_MODE_EXT);
|
return setTrackMode(p[0],TRACK_MODE_EXT);
|
||||||
|
#ifdef BOOSTER_INPUT
|
||||||
|
if (TRACK_MODE_BOOST != 0 && // compile time optimization
|
||||||
|
params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
|
||||||
|
return setTrackMode(p[0],TRACK_MODE_BOOST);
|
||||||
|
#endif
|
||||||
|
if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
|
||||||
|
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV);
|
||||||
|
|
||||||
if (params==3 && p[1]==HASH_KEYWORD_DC && p[2]>0) // <= id DC cab>
|
if (params==2 && p[1]=="INV"_hk) // <= id INV>
|
||||||
|
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_INV);
|
||||||
|
|
||||||
|
if (params==3 && p[1]=="DC"_hk && p[2]>0) // <= id DC cab>
|
||||||
return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
|
return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
|
||||||
|
|
||||||
if (params==3 && p[1]==HASH_KEYWORD_DCX && p[2]>0) // <= id DCX cab>
|
if (params==3 && p[1]=="DCX"_hk && p[2]>0) // <= id DCX cab>
|
||||||
return setTrackMode(p[0],TRACK_MODE_DCX,p[2]);
|
return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackManager::streamTrackState(Print* stream, byte t) {
|
const FSH* TrackManager::getModeName(TRACK_MODE tm) {
|
||||||
// null stream means send to commandDistributor for broadcast
|
const FSH *modename=F("---");
|
||||||
if (track[t]==NULL) return;
|
|
||||||
auto format=F("");
|
if (tm & TRACK_MODE_MAIN) {
|
||||||
switch(track[t]->getMode()) {
|
if(tm & TRACK_MODE_AUTOINV)
|
||||||
case TRACK_MODE_MAIN:
|
modename=F("MAIN A");
|
||||||
format=F("<= %c MAIN>\n");
|
else if (tm & TRACK_MODE_INV)
|
||||||
break;
|
modename=F("MAIN I>\n");
|
||||||
#ifndef DISABLE_PROG
|
else
|
||||||
case TRACK_MODE_PROG:
|
modename=F("MAIN");
|
||||||
format=F("<= %c PROG>\n");
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
case TRACK_MODE_NONE:
|
|
||||||
format=F("<= %c NONE>\n");
|
|
||||||
break;
|
|
||||||
case TRACK_MODE_EXT:
|
|
||||||
format=F("<= %c EXT>\n");
|
|
||||||
break;
|
|
||||||
case TRACK_MODE_DC:
|
|
||||||
format=F("<= %c DC %d>\n");
|
|
||||||
break;
|
|
||||||
case TRACK_MODE_DCX:
|
|
||||||
format=F("<= %c DCX %d>\n");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break; // unknown, dont care
|
|
||||||
}
|
}
|
||||||
if (stream) StringFormatter::send(stream,format,'A'+t,trackDCAddr[t]);
|
#ifndef DISABLE_PROG
|
||||||
else CommandDistributor::broadcastTrackState(format,'A'+t,trackDCAddr[t]);
|
else if (tm & TRACK_MODE_PROG)
|
||||||
|
modename=F("PROG");
|
||||||
|
#endif
|
||||||
|
else if (tm & TRACK_MODE_NONE)
|
||||||
|
modename=F("NONE");
|
||||||
|
else if(tm & TRACK_MODE_EXT)
|
||||||
|
modename=F("EXT");
|
||||||
|
else if(tm & TRACK_MODE_BOOST) {
|
||||||
|
if(tm & TRACK_MODE_AUTOINV)
|
||||||
|
modename=F("BOOST A");
|
||||||
|
else if (tm & TRACK_MODE_INV)
|
||||||
|
modename=F("BOOST I");
|
||||||
|
else
|
||||||
|
modename=F("BOOST");
|
||||||
|
}
|
||||||
|
else if (tm & TRACK_MODE_DC) {
|
||||||
|
if (tm & TRACK_MODE_INV)
|
||||||
|
modename=F("DCX");
|
||||||
|
else
|
||||||
|
modename=F("DC");
|
||||||
|
}
|
||||||
|
return modename;
|
||||||
|
}
|
||||||
|
|
||||||
|
// null stream means send to commandDistributor for broadcast
|
||||||
|
void TrackManager::streamTrackState(Print* stream, byte t) {
|
||||||
|
const FSH *format;
|
||||||
|
|
||||||
|
if (track[t]==NULL) return;
|
||||||
|
TRACK_MODE tm = track[t]->getMode();
|
||||||
|
if (tm & TRACK_MODE_DC)
|
||||||
|
format=F("<= %c %S %d>\n");
|
||||||
|
else
|
||||||
|
format=F("<= %c %S>\n");
|
||||||
|
|
||||||
|
const FSH *modename=getModeName(tm);
|
||||||
|
if (stream) { // null stream means send to commandDistributor for broadcast
|
||||||
|
StringFormatter::send(stream,format,'A'+t, modename, trackDCAddr[t]);
|
||||||
|
} else {
|
||||||
|
CommandDistributor::broadcastTrackState(format,'A'+t, modename, trackDCAddr[t]);
|
||||||
|
CommandDistributor::broadcastPower();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte TrackManager::nextCycleTrack=MAX_TRACKS;
|
byte TrackManager::nextCycleTrack=MAX_TRACKS;
|
||||||
|
@ -392,13 +479,13 @@ void TrackManager::loop() {
|
||||||
if (nextCycleTrack>lastTrack) nextCycleTrack=0;
|
if (nextCycleTrack>lastTrack) nextCycleTrack=0;
|
||||||
if (track[nextCycleTrack]==NULL) return;
|
if (track[nextCycleTrack]==NULL) return;
|
||||||
MotorDriver * motorDriver=track[nextCycleTrack];
|
MotorDriver * motorDriver=track[nextCycleTrack];
|
||||||
bool useProgLimit=dontLimitProg? false: track[nextCycleTrack]->getMode()==TRACK_MODE_PROG;
|
bool useProgLimit=dontLimitProg ? false : (bool)(track[nextCycleTrack]->getMode() & TRACK_MODE_PROG);
|
||||||
motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);
|
motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
MotorDriver * TrackManager::getProgDriver() {
|
MotorDriver * TrackManager::getProgDriver() {
|
||||||
FOR_EACH_TRACK(t)
|
FOR_EACH_TRACK(t)
|
||||||
if (track[t]->getMode()==TRACK_MODE_PROG) return track[t];
|
if (track[t]->getMode() & TRACK_MODE_PROG) return track[t];
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,64 +493,113 @@ MotorDriver * TrackManager::getProgDriver() {
|
||||||
std::vector<MotorDriver *>TrackManager::getMainDrivers() {
|
std::vector<MotorDriver *>TrackManager::getMainDrivers() {
|
||||||
std::vector<MotorDriver *> v;
|
std::vector<MotorDriver *> v;
|
||||||
FOR_EACH_TRACK(t)
|
FOR_EACH_TRACK(t)
|
||||||
if (track[t]->getMode()==TRACK_MODE_MAIN) v.push_back(track[t]);
|
if (track[t]->getMode() & TRACK_MODE_MAIN) v.push_back(track[t]);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void TrackManager::setPower2(bool setProg,POWERMODE mode) {
|
// Set track power for all tracks with this mode
|
||||||
if (!setProg) mainPowerGuess=mode;
|
void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) {
|
||||||
FOR_EACH_TRACK(t) {
|
bool didChange=false;
|
||||||
MotorDriver * driver=track[t];
|
FOR_EACH_TRACK(t) {
|
||||||
if (!driver) continue;
|
MotorDriver *driver=track[t];
|
||||||
switch (track[t]->getMode()) {
|
TRACK_MODE trackmodeOfTrack = driver->getMode();
|
||||||
case TRACK_MODE_MAIN:
|
if (trackmodeToMatch & trackmodeOfTrack) {
|
||||||
if (setProg) break;
|
if (powermode != driver->getPower())
|
||||||
// toggle brake before turning power on - resets overcurrent error
|
didChange=true;
|
||||||
// on the Pololu board if brake is wired to ^D2.
|
if (powermode == POWERMODE::ON) {
|
||||||
// XXX see if we can make this conditional
|
if (trackmodeOfTrack & TRACK_MODE_DC) {
|
||||||
driver->setBrake(true);
|
driver->setBrake(true); // DC starts with brake on
|
||||||
driver->setBrake(false); // DCC runs with brake off
|
applyDCSpeed(t); // speed match DCC throttles
|
||||||
driver->setPower(mode);
|
} else {
|
||||||
break;
|
// toggle brake before turning power on - resets overcurrent error
|
||||||
case TRACK_MODE_DC:
|
// on the Pololu board if brake is wired to ^D2.
|
||||||
case TRACK_MODE_DCX:
|
driver->setBrake(true);
|
||||||
if (setProg) break;
|
driver->setBrake(false); // DCC runs with brake off
|
||||||
driver->setBrake(true); // DC starts with brake on
|
}
|
||||||
applyDCSpeed(t); // speed match DCC throttles
|
}
|
||||||
driver->setPower(mode);
|
driver->setPower(powermode);
|
||||||
break;
|
|
||||||
case TRACK_MODE_PROG:
|
|
||||||
if (!setProg) break;
|
|
||||||
driver->setBrake(true);
|
|
||||||
driver->setBrake(false);
|
|
||||||
driver->setPower(mode);
|
|
||||||
break;
|
|
||||||
case TRACK_MODE_EXT:
|
|
||||||
driver->setBrake(true);
|
|
||||||
driver->setBrake(false);
|
|
||||||
driver->setPower(mode);
|
|
||||||
break;
|
|
||||||
case TRACK_MODE_NONE:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (didChange)
|
||||||
|
CommandDistributor::broadcastPower();
|
||||||
}
|
}
|
||||||
|
|
||||||
POWERMODE TrackManager::getProgPower() {
|
// Set track power for this track, inependent of mode
|
||||||
FOR_EACH_TRACK(t)
|
void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
|
||||||
if (track[t]->getMode()==TRACK_MODE_PROG)
|
MotorDriver *driver=track[t];
|
||||||
return track[t]->getPower();
|
if (driver == NULL) { // track is not defined at all
|
||||||
return POWERMODE::OFF;
|
DIAG(F("Error: Track %c does not exist"), t+'A');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
TRACK_MODE trackmode = driver->getMode();
|
||||||
|
POWERMODE oldpower = driver->getPower();
|
||||||
|
if (trackmode & TRACK_MODE_NONE) {
|
||||||
|
driver->setBrake(true); // Track is unused. Brake is good to have.
|
||||||
|
powermode = POWERMODE::OFF; // Track is unused. Force it to OFF
|
||||||
|
} else if (trackmode & TRACK_MODE_DC) { // includes inverted DC (called DCX)
|
||||||
|
if (powermode == POWERMODE::ON) {
|
||||||
|
driver->setBrake(true); // DC starts with brake on
|
||||||
|
applyDCSpeed(t); // speed match DCC throttles
|
||||||
|
}
|
||||||
|
} else /* MAIN PROG EXT BOOST */ {
|
||||||
|
if (powermode == POWERMODE::ON) {
|
||||||
|
// toggle brake before turning power on - resets overcurrent error
|
||||||
|
// on the Pololu board if brake is wired to ^D2.
|
||||||
|
driver->setBrake(true);
|
||||||
|
driver->setBrake(false); // DCC runs with brake off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
driver->setPower(powermode);
|
||||||
|
if (oldpower != driver->getPower())
|
||||||
|
CommandDistributor::broadcastPower();
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns state of the one and only prog track
|
||||||
|
POWERMODE TrackManager::getProgPower() {
|
||||||
|
FOR_EACH_TRACK(t)
|
||||||
|
if (track[t]->getMode() & TRACK_MODE_PROG)
|
||||||
|
return track[t]->getPower(); // optimize: there is max one prog track
|
||||||
|
return POWERMODE::OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns on if all are on. returns off otherwise
|
||||||
|
POWERMODE TrackManager::getMainPower() {
|
||||||
|
POWERMODE result = POWERMODE::OFF;
|
||||||
|
FOR_EACH_TRACK(t) {
|
||||||
|
if (track[t]->getMode() & TRACK_MODE_MAIN) {
|
||||||
|
POWERMODE p = track[t]->getPower();
|
||||||
|
if (p == POWERMODE::OFF)
|
||||||
|
return POWERMODE::OFF; // done and out
|
||||||
|
if (p == POWERMODE::ON)
|
||||||
|
result = POWERMODE::ON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrackManager::getPower(byte t, char s[]) {
|
||||||
|
if (t > lastTrack)
|
||||||
|
return false;
|
||||||
|
if (track[t]) {
|
||||||
|
s[0] = track[t]->getPower() == POWERMODE::ON ? '1' : '0';
|
||||||
|
s[2] = t + 'A';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void TrackManager::reportObsoleteCurrent(Print* stream) {
|
void TrackManager::reportObsoleteCurrent(Print* stream) {
|
||||||
// This function is for backward JMRI compatibility only
|
// This function is for backward JMRI compatibility only
|
||||||
// It reports the first track only, as main, regardless of track settings.
|
// It reports the first track only, as main, regardless of track settings.
|
||||||
// <c MeterName value C/V unit min max res warn>
|
// <c MeterName value C/V unit min max res warn>
|
||||||
|
#ifdef HAS_ENOUGH_MEMORY
|
||||||
int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());
|
int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());
|
||||||
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"),
|
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"),
|
||||||
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
|
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
|
||||||
|
#else
|
||||||
|
(void)stream;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackManager::reportCurrent(Print* stream) {
|
void TrackManager::reportCurrent(Print* stream) {
|
||||||
|
@ -497,7 +633,7 @@ void TrackManager::setJoin(bool joined) {
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
if (joined) {
|
if (joined) {
|
||||||
FOR_EACH_TRACK(t) {
|
FOR_EACH_TRACK(t) {
|
||||||
if (track[t]->getMode()==TRACK_MODE_PROG) {
|
if (track[t]->getMode() & TRACK_MODE_PROG) {
|
||||||
tempProgTrack = t;
|
tempProgTrack = t;
|
||||||
setTrackMode(t, TRACK_MODE_MAIN);
|
setTrackMode(t, TRACK_MODE_MAIN);
|
||||||
break;
|
break;
|
||||||
|
@ -518,3 +654,24 @@ void TrackManager::setJoin(bool joined) {
|
||||||
progTrackSyncMain=joined;
|
progTrackSyncMain=joined;
|
||||||
if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,joined?HIGH:LOW);
|
if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,joined?HIGH:LOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TrackManager::isPowerOn(byte t) {
|
||||||
|
if (track[t]->getPower()!=POWERMODE::ON)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrackManager::isProg(byte t) {
|
||||||
|
if (track[t]->getMode() & TRACK_MODE_PROG)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TRACK_MODE TrackManager::getMode(byte t) {
|
||||||
|
return (track[t]->getMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t TrackManager::returnDCAddr(byte t) {
|
||||||
|
return (trackDCAddr[t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* © 2022 Chris Harlow
|
* © 2022 Chris Harlow
|
||||||
* © 2022 Harald Barth
|
* © 2022-2024 Harald Barth
|
||||||
|
* © 2023 Colin Murdoch
|
||||||
|
*
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* This file is part of CommandStation-EX
|
* This file is part of CommandStation-EX
|
||||||
|
@ -37,10 +39,14 @@ const byte TRACK_NUMBER_5=5, TRACK_NUMBER_F=5;
|
||||||
const byte TRACK_NUMBER_6=6, TRACK_NUMBER_G=6;
|
const byte TRACK_NUMBER_6=6, TRACK_NUMBER_G=6;
|
||||||
const byte TRACK_NUMBER_7=7, TRACK_NUMBER_H=7;
|
const byte TRACK_NUMBER_7=7, TRACK_NUMBER_H=7;
|
||||||
|
|
||||||
|
// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).
|
||||||
|
const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
|
||||||
|
const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
|
||||||
|
|
||||||
class TrackManager {
|
class TrackManager {
|
||||||
public:
|
public:
|
||||||
static void Setup(const FSH * shieldName,
|
static void Setup(const FSH * shieldName,
|
||||||
MotorDriver * track0,
|
MotorDriver * track0=NULL,
|
||||||
MotorDriver * track1=NULL,
|
MotorDriver * track1=NULL,
|
||||||
MotorDriver * track2=NULL,
|
MotorDriver * track2=NULL,
|
||||||
MotorDriver * track3=NULL,
|
MotorDriver * track3=NULL,
|
||||||
|
@ -51,32 +57,43 @@ class TrackManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
static void setDCCSignal( bool on);
|
static void setDCCSignal( bool on);
|
||||||
static void setCutout( bool on);
|
|
||||||
static void setPROGSignal( bool on);
|
static void setPROGSignal( bool on);
|
||||||
static void setDCSignal(int16_t cab, byte speedbyte);
|
static void setDCSignal(int16_t cab, byte speedbyte);
|
||||||
static MotorDriver * getProgDriver();
|
static MotorDriver * getProgDriver();
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
static std::vector<MotorDriver *>getMainDrivers();
|
static std::vector<MotorDriver *>getMainDrivers();
|
||||||
#endif
|
#endif
|
||||||
static void setPower2(bool progTrack,POWERMODE mode);
|
|
||||||
static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);}
|
static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);}
|
||||||
static void setMainPower(POWERMODE mode) {setPower2(false,mode);}
|
static void setTrackPower(POWERMODE mode, byte t);
|
||||||
static void setProgPower(POWERMODE mode) {setPower2(true,mode);}
|
static void setTrackPower(TRACK_MODE trackmode, POWERMODE powermode);
|
||||||
|
static void setMainPower(POWERMODE mode) {setTrackPower(TRACK_MODE_MAIN, mode);}
|
||||||
|
static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);}
|
||||||
|
|
||||||
static const int16_t MAX_TRACKS=8;
|
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);
|
||||||
static bool parseJ(Print * stream, int16_t params, int16_t p[]);
|
static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
|
||||||
static void loop();
|
static void loop();
|
||||||
static POWERMODE getMainPower() {return mainPowerGuess;}
|
static POWERMODE getMainPower();
|
||||||
static POWERMODE getProgPower();
|
static POWERMODE getProgPower();
|
||||||
|
static inline POWERMODE getPower(byte t) { return track[t]->getPower(); }
|
||||||
|
static bool getPower(byte t, char s[]);
|
||||||
static void setJoin(bool join);
|
static void setJoin(bool join);
|
||||||
static bool isJoined() { return progTrackSyncMain;}
|
static bool isJoined() { return progTrackSyncMain;}
|
||||||
|
static inline bool isActive (byte tr) {
|
||||||
|
if (tr > lastTrack) return false;
|
||||||
|
return track[tr]->getMode() & (TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT);}
|
||||||
static void setJoinRelayPin(byte joinRelayPin);
|
static void setJoinRelayPin(byte joinRelayPin);
|
||||||
static void sampleCurrent();
|
static void sampleCurrent();
|
||||||
static void reportGauges(Print* stream);
|
static void reportGauges(Print* stream);
|
||||||
static void reportCurrent(Print* stream);
|
static void reportCurrent(Print* stream);
|
||||||
static void reportObsoleteCurrent(Print* stream);
|
static void reportObsoleteCurrent(Print* stream);
|
||||||
static void streamTrackState(Print* stream, byte t);
|
static void streamTrackState(Print* stream, byte t);
|
||||||
|
static bool isPowerOn(byte t);
|
||||||
|
static bool isProg(byte t);
|
||||||
|
static TRACK_MODE getMode(byte t);
|
||||||
|
static int16_t returnDCAddr(byte t);
|
||||||
|
static const FSH* getModeName(TRACK_MODE Mode);
|
||||||
|
|
||||||
static int16_t joinRelay;
|
static int16_t joinRelay;
|
||||||
static bool progTrackSyncMain; // true when prog track is a siding switched to main
|
static bool progTrackSyncMain; // true when prog track is a siding switched to main
|
||||||
|
@ -91,12 +108,11 @@ class TrackManager {
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void addTrack(byte t, MotorDriver* driver);
|
static void addTrack(byte t, MotorDriver* driver);
|
||||||
static byte lastTrack;
|
static int8_t lastTrack;
|
||||||
static byte nextCycleTrack;
|
static byte nextCycleTrack;
|
||||||
static POWERMODE mainPowerGuess;
|
|
||||||
static void applyDCSpeed(byte t);
|
static void applyDCSpeed(byte t);
|
||||||
|
|
||||||
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX
|
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
static byte tempProgTrack; // holds the prog track number during join
|
static byte tempProgTrack; // holds the prog track number during join
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -123,7 +123,6 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DIAG_IO
|
|
||||||
// Static setClosed function is invoked from close(), throw() etc. to perform the
|
// Static setClosed function is invoked from close(), throw() etc. to perform the
|
||||||
// common parts of the turnout operation. Code which is specific to a turnout
|
// common parts of the turnout operation. Code which is specific to a turnout
|
||||||
// type should be placed in the virtual function setClosedInternal(bool) which is
|
// type should be placed in the virtual function setClosedInternal(bool) which is
|
||||||
|
|
269
Turntables.cpp
Normal file
269
Turntables.cpp
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
/*
|
||||||
|
* © 2023 Peter Cole
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "defines.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "Turntables.h"
|
||||||
|
#include "StringFormatter.h"
|
||||||
|
#include "CommandDistributor.h"
|
||||||
|
#include "EXRAIL2.h"
|
||||||
|
#include "DCC.h"
|
||||||
|
|
||||||
|
// No turntable support without HAL
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Protected static data
|
||||||
|
*/
|
||||||
|
Turntable *Turntable::_firstTurntable = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Public static data
|
||||||
|
*/
|
||||||
|
int Turntable::turntablelistHash = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Protected static functions
|
||||||
|
*/
|
||||||
|
// Add new turntable to end of list
|
||||||
|
|
||||||
|
void Turntable::add(Turntable *tto) {
|
||||||
|
if (!_firstTurntable) {
|
||||||
|
_firstTurntable = tto;
|
||||||
|
} else {
|
||||||
|
Turntable *ptr = _firstTurntable;
|
||||||
|
for ( ; ptr->_nextTurntable!=0; ptr=ptr->_nextTurntable) {}
|
||||||
|
ptr->_nextTurntable = tto;
|
||||||
|
}
|
||||||
|
turntablelistHash++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a position
|
||||||
|
void Turntable::addPosition(uint8_t idx, uint16_t value, uint16_t angle) {
|
||||||
|
_turntablePositions.insert(idx, value, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get value for position
|
||||||
|
uint16_t Turntable::getPositionValue(uint8_t position) {
|
||||||
|
TurntablePosition* currentPosition = _turntablePositions.getHead();
|
||||||
|
while (currentPosition) {
|
||||||
|
if (currentPosition->index == position) {
|
||||||
|
return currentPosition->data;
|
||||||
|
}
|
||||||
|
currentPosition = currentPosition->next;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get value for position
|
||||||
|
uint16_t Turntable::getPositionAngle(uint8_t position) {
|
||||||
|
TurntablePosition* currentPosition = _turntablePositions.getHead();
|
||||||
|
while (currentPosition) {
|
||||||
|
if (currentPosition->index == position) {
|
||||||
|
return currentPosition->angle;
|
||||||
|
}
|
||||||
|
currentPosition = currentPosition->next;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the count of positions associated with the turntable
|
||||||
|
uint8_t Turntable::getPositionCount() {
|
||||||
|
TurntablePosition* currentPosition = _turntablePositions.getHead();
|
||||||
|
uint8_t count = 0;
|
||||||
|
while (currentPosition) {
|
||||||
|
count++;
|
||||||
|
currentPosition = currentPosition->next;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Public static functions
|
||||||
|
*/
|
||||||
|
// Find turntable from list
|
||||||
|
Turntable *Turntable::get(uint16_t id) {
|
||||||
|
for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable)
|
||||||
|
if (tto->_turntableData.id == id) return tto;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find turntable via Vpin
|
||||||
|
Turntable *Turntable::getByVpin(VPIN vpin) {
|
||||||
|
for (Turntable *tto = _firstTurntable; tto != nullptr; tto = tto->_nextTurntable) {
|
||||||
|
if (tto->isEXTT()) {
|
||||||
|
EXTTTurntable *exttTto = static_cast<EXTTTurntable*>(tto);
|
||||||
|
if (exttTto->getVpin() == vpin) {
|
||||||
|
return tto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current position for turntable with the specified ID
|
||||||
|
uint8_t Turntable::getPosition(uint16_t id) {
|
||||||
|
Turntable *tto = get(id);
|
||||||
|
if (!tto) return false;
|
||||||
|
return tto->getPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got the moving state of the specified turntable
|
||||||
|
bool Turntable::ttMoving(uint16_t id) {
|
||||||
|
Turntable *tto = get(id);
|
||||||
|
if (!tto) return false;
|
||||||
|
return tto->isMoving();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate a turntable move
|
||||||
|
bool Turntable::setPosition(uint16_t id, uint8_t position, uint8_t activity) {
|
||||||
|
#if defined(DIAG_IO)
|
||||||
|
DIAG(F("Rotate turntable %d to position %d, activity %d)"), id, position, activity);
|
||||||
|
#endif
|
||||||
|
Turntable *tto = Turntable::get(id);
|
||||||
|
if (!tto) return false;
|
||||||
|
if (tto->isMoving()) return false;
|
||||||
|
bool ok = tto->setPositionInternal(position, activity);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
// We only deal with broadcasts for DCC turntables here, EXTT in the device driver
|
||||||
|
if (!tto->isEXTT()) {
|
||||||
|
CommandDistributor::broadcastTurntable(id, position, false);
|
||||||
|
}
|
||||||
|
// Trigger EXRAIL rotateEvent for both types here if changed
|
||||||
|
#if defined(EXRAIL_ACTIVE)
|
||||||
|
bool rotated = false;
|
||||||
|
if (position != tto->_previousPosition) rotated = true;
|
||||||
|
RMFT2::rotateEvent(id, rotated);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* EXTTTurntable - EX-Turntable device.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
// Private constructor
|
||||||
|
EXTTTurntable::EXTTTurntable(uint16_t id, VPIN vpin) :
|
||||||
|
Turntable(id, TURNTABLE_EXTT)
|
||||||
|
{
|
||||||
|
_exttTurntableData.vpin = vpin;
|
||||||
|
}
|
||||||
|
|
||||||
|
using DevState = IODevice::DeviceStateEnum;
|
||||||
|
|
||||||
|
// Create function
|
||||||
|
Turntable *EXTTTurntable::create(uint16_t id, VPIN vpin) {
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
Turntable *tto = get(id);
|
||||||
|
if (tto) {
|
||||||
|
if (tto->isType(TURNTABLE_EXTT)) {
|
||||||
|
EXTTTurntable *extt = (EXTTTurntable *)tto;
|
||||||
|
extt->_exttTurntableData.vpin = vpin;
|
||||||
|
return tto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!IODevice::exists(vpin)) return nullptr;
|
||||||
|
if (IODevice::getStatus(vpin) == DevState::DEVSTATE_FAILED) return nullptr;
|
||||||
|
if (Turntable::getByVpin(vpin)) return nullptr;
|
||||||
|
tto = (Turntable *)new EXTTTurntable(id, vpin);
|
||||||
|
DIAG(F("Turntable 0x%x size %d size %d"), tto, sizeof(Turntable), sizeof(struct TurntableData));
|
||||||
|
return tto;
|
||||||
|
#else
|
||||||
|
(void)id;
|
||||||
|
(void)vpin;
|
||||||
|
return NULL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void EXTTTurntable::print(Print *stream) {
|
||||||
|
StringFormatter::send(stream, F("<i %d EXTURNTABLE %d>\n"), _turntableData.id, _exttTurntableData.vpin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EX-Turntable specific code for moving to the specified position
|
||||||
|
bool EXTTTurntable::setPositionInternal(uint8_t position, uint8_t activity) {
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
int16_t value;
|
||||||
|
if (position == 0) {
|
||||||
|
value = 0; // Position 0 is just to send activities
|
||||||
|
} else {
|
||||||
|
if (activity > 1) return false; // If sending a position update, only phase changes valid (0|1)
|
||||||
|
value = getPositionValue(position); // Get position value from position list
|
||||||
|
}
|
||||||
|
if (position > 0 && !value) return false; // Return false if it's not a valid position
|
||||||
|
// Set position via device driver
|
||||||
|
_previousPosition = _turntableData.position;
|
||||||
|
_turntableData.position = position;
|
||||||
|
EXTurntable::writeAnalogue(_exttTurntableData.vpin, value, activity);
|
||||||
|
#else
|
||||||
|
(void)position;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* DCCTurntable - DCC Turntable device.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
// Private constructor
|
||||||
|
DCCTurntable::DCCTurntable(uint16_t id) : Turntable(id, TURNTABLE_DCC) {}
|
||||||
|
|
||||||
|
// Create function
|
||||||
|
Turntable *DCCTurntable::create(uint16_t id) {
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
Turntable *tto = get(id);
|
||||||
|
if (!tto) {
|
||||||
|
tto = (Turntable *)new DCCTurntable(id);
|
||||||
|
DIAG(F("Turntable 0x%x size %d size %d"), tto, sizeof(Turntable), sizeof(struct TurntableData));
|
||||||
|
}
|
||||||
|
return tto;
|
||||||
|
#else
|
||||||
|
(void)id;
|
||||||
|
return NULL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void DCCTurntable::print(Print *stream) {
|
||||||
|
StringFormatter::send(stream, F("<i %d DCCTURNTABLE>\n"), _turntableData.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EX-Turntable specific code for moving to the specified position
|
||||||
|
bool DCCTurntable::setPositionInternal(uint8_t position, uint8_t activity) {
|
||||||
|
(void) activity;
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
int16_t value = getPositionValue(position);
|
||||||
|
if (position == 0 || !value) return false; // Return false if it's not a valid position
|
||||||
|
// Set position via device driver
|
||||||
|
int16_t addr=value>>3;
|
||||||
|
int16_t subaddr=(value>>1) & 0x03;
|
||||||
|
bool active=value & 0x01;
|
||||||
|
_previousPosition = _turntableData.position;
|
||||||
|
_turntableData.position = position;
|
||||||
|
DCC::setAccessory(addr, subaddr, active);
|
||||||
|
#else
|
||||||
|
(void)position;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
243
Turntables.h
Normal file
243
Turntables.h
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
/*
|
||||||
|
* © 2023 Peter Cole
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is part of CommandStation-EX
|
||||||
|
*
|
||||||
|
* This is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* It is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef TURNTABLES_H
|
||||||
|
#define TURNTABLES_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "IODevice.h"
|
||||||
|
#include "StringFormatter.h"
|
||||||
|
|
||||||
|
// No turntable support without HAL
|
||||||
|
#ifndef IO_NO_HAL
|
||||||
|
|
||||||
|
// Turntable type definitions
|
||||||
|
// EXTT = EX-Turntable
|
||||||
|
// DCC = DCC accessory turntables - to be added later
|
||||||
|
enum {
|
||||||
|
TURNTABLE_EXTT = 0,
|
||||||
|
TURNTABLE_DCC = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* Turntable positions.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
struct TurntablePosition {
|
||||||
|
uint8_t index;
|
||||||
|
uint16_t data;
|
||||||
|
uint16_t angle;
|
||||||
|
TurntablePosition* next;
|
||||||
|
|
||||||
|
TurntablePosition(uint8_t idx, uint16_t value, uint16_t angle) : index(idx), data(value), angle(angle), next(nullptr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TurntablePositionList {
|
||||||
|
public:
|
||||||
|
TurntablePositionList() : head(nullptr) {}
|
||||||
|
|
||||||
|
void insert(uint8_t idx, uint16_t value, uint16_t angle) {
|
||||||
|
TurntablePosition* newPosition = new TurntablePosition(idx, value, angle);
|
||||||
|
if(!head) {
|
||||||
|
head = newPosition;
|
||||||
|
} else {
|
||||||
|
newPosition->next = head;
|
||||||
|
head = newPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TurntablePosition* getHead() {
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TurntablePosition* head;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* Turntable - Base class for turntables.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
|
||||||
|
class Turntable {
|
||||||
|
protected:
|
||||||
|
/*
|
||||||
|
* Object data
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Data common to all turntable types
|
||||||
|
struct TurntableData {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
bool hidden : 1;
|
||||||
|
bool turntableType : 1;
|
||||||
|
uint8_t position : 6; // Allows up to 63 positions including 0/home
|
||||||
|
};
|
||||||
|
uint8_t flags;
|
||||||
|
};
|
||||||
|
uint16_t id;
|
||||||
|
} _turntableData;
|
||||||
|
|
||||||
|
// Pointer to next turntable object
|
||||||
|
Turntable *_nextTurntable = 0;
|
||||||
|
|
||||||
|
// Linked list for positions
|
||||||
|
TurntablePositionList _turntablePositions;
|
||||||
|
|
||||||
|
// Store the previous position to allow checking for changes
|
||||||
|
uint8_t _previousPosition = 0;
|
||||||
|
|
||||||
|
// Store the current state of the turntable
|
||||||
|
bool _isMoving = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
Turntable(uint16_t id, uint8_t turntableType) {
|
||||||
|
_turntableData.id = id;
|
||||||
|
_turntableData.turntableType = turntableType;
|
||||||
|
_turntableData.hidden = false;
|
||||||
|
_turntableData.position = 0;
|
||||||
|
add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Static data
|
||||||
|
*/
|
||||||
|
static Turntable *_firstTurntable;
|
||||||
|
static int _turntablelistHash;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Virtual functions
|
||||||
|
*/
|
||||||
|
virtual bool setPositionInternal(uint8_t position, uint8_t activity) = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Static functions
|
||||||
|
*/
|
||||||
|
static void add(Turntable *tto);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static Turntable *get(uint16_t id);
|
||||||
|
static Turntable *getByVpin(VPIN vpin);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Static data
|
||||||
|
*/
|
||||||
|
static int turntablelistHash;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Public base class functions
|
||||||
|
*/
|
||||||
|
inline uint8_t getPosition() { return _turntableData.position; }
|
||||||
|
inline bool isHidden() { return _turntableData.hidden; }
|
||||||
|
inline void setHidden(bool h) {_turntableData.hidden=h; }
|
||||||
|
inline bool isType(uint8_t type) { return _turntableData.turntableType == type; }
|
||||||
|
inline bool isEXTT() const { return _turntableData.turntableType == TURNTABLE_EXTT; }
|
||||||
|
inline uint16_t getId() { return _turntableData.id; }
|
||||||
|
inline Turntable *next() { return _nextTurntable; }
|
||||||
|
void printState(Print *stream);
|
||||||
|
void addPosition(uint8_t idx, uint16_t value, uint16_t angle);
|
||||||
|
uint16_t getPositionValue(uint8_t position);
|
||||||
|
uint16_t getPositionAngle(uint8_t position);
|
||||||
|
uint8_t getPositionCount();
|
||||||
|
bool isMoving() { return _isMoving; }
|
||||||
|
void setMoving(bool moving) { _isMoving=moving; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Virtual functions
|
||||||
|
*/
|
||||||
|
virtual void print(Print *stream) {
|
||||||
|
(void)stream; // suppress compiler warnings
|
||||||
|
}
|
||||||
|
virtual ~Turntable() {} // Destructor
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Public static functions
|
||||||
|
*/
|
||||||
|
inline static bool exists(uint16_t id) { return get(id) != 0; }
|
||||||
|
static bool setPosition(uint16_t id, uint8_t position, uint8_t activity=0);
|
||||||
|
static uint8_t getPosition(uint16_t id);
|
||||||
|
static bool ttMoving(uint16_t id);
|
||||||
|
inline static Turntable *first() { return _firstTurntable; }
|
||||||
|
static bool printAll(Print *stream) {
|
||||||
|
bool gotOne = false;
|
||||||
|
for (Turntable *tto = _firstTurntable; tto != 0; tto = tto->_nextTurntable)
|
||||||
|
if (!tto->isHidden()) {
|
||||||
|
gotOne = true;
|
||||||
|
StringFormatter::send(stream, F("<I %d %d>\n"), tto->getId(), tto->getPosition());
|
||||||
|
}
|
||||||
|
return gotOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* EXTTTurntable - EX-Turntable device.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
class EXTTTurntable : public Turntable {
|
||||||
|
private:
|
||||||
|
// EXTTTurntableData contains device specific data
|
||||||
|
struct EXTTTurntableData {
|
||||||
|
VPIN vpin;
|
||||||
|
} _exttTurntableData;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
EXTTTurntable(uint16_t id, VPIN vpin);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Create function
|
||||||
|
static Turntable *create(uint16_t id, VPIN vpin);
|
||||||
|
void print(Print *stream) override;
|
||||||
|
VPIN getVpin() const { return _exttTurntableData.vpin; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// EX-Turntable specific code for setting position
|
||||||
|
bool setPositionInternal(uint8_t position, uint8_t activity) override;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************************************************************************************
|
||||||
|
* DCCTurntable - DCC accessory Turntable device.
|
||||||
|
*
|
||||||
|
*************************************************************************************/
|
||||||
|
class DCCTurntable : public Turntable {
|
||||||
|
private:
|
||||||
|
// Constructor
|
||||||
|
DCCTurntable(uint16_t id);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Create function
|
||||||
|
static Turntable *create(uint16_t id);
|
||||||
|
void print(Print *stream) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// DCC specific code for setting position
|
||||||
|
bool setPositionInternal(uint8_t position, uint8_t activity=0) override;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
|
@ -150,7 +150,6 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
|
||||||
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
CommandDistributor::broadcastPower();
|
|
||||||
}
|
}
|
||||||
#if defined(EXRAIL_ACTIVE)
|
#if defined(EXRAIL_ACTIVE)
|
||||||
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
|
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
|
||||||
|
@ -188,6 +187,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
|
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
|
||||||
|
sendIntro(stream);
|
||||||
StringFormatter::send(stream, F("*%d\n"), heartrateSent ? HEARTBEAT_SECONDS : HEARTBEAT_PRELOAD); // return timeout value
|
StringFormatter::send(stream, F("*%d\n"), heartrateSent ? HEARTBEAT_SECONDS : HEARTBEAT_PRELOAD); // return timeout value
|
||||||
break;
|
break;
|
||||||
case 'M': // multithrottle
|
case 'M': // multithrottle
|
||||||
|
@ -195,7 +195,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
|
||||||
break;
|
break;
|
||||||
case 'H': // send initial connection info after receiving "HU" message
|
case 'H': // send initial connection info after receiving "HU" message
|
||||||
if (cmd[1] == 'U') {
|
if (cmd[1] == 'U') {
|
||||||
sendIntro(stream);
|
sendIntro(stream);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Q': //
|
case 'Q': //
|
||||||
|
@ -496,16 +496,17 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
|
||||||
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
|
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
|
||||||
DIAG(F("LocoCallback commit success"));
|
DIAG(F("LocoCallback commit success"));
|
||||||
stashStream->commit();
|
stashStream->commit();
|
||||||
CommandDistributor::broadcastPower();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiThrottle::sendIntro(Print* stream) {
|
void WiThrottle::sendIntro(Print* stream) {
|
||||||
|
if (introSent) // sendIntro only once
|
||||||
|
return;
|
||||||
introSent=true;
|
introSent=true;
|
||||||
StringFormatter::send(stream,F("VN2.0\nHTDCC-EX\nRL0\n"));
|
StringFormatter::send(stream,F("VN2.0\nHTDCC-EX\nRL0\n"));
|
||||||
StringFormatter::send(stream,F("HtDCC-EX v%S, %S, %S, %S\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
|
StringFormatter::send(stream,F("HtDCC-EX v%S, %S, %S, %S\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
|
||||||
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{4\n"));
|
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{4\n"));
|
||||||
StringFormatter::send(stream,F("PPA%x\n"),TrackManager::getMainPower()==POWERMODE::ON);
|
StringFormatter::send(stream,F("PPA%x\n"),TrackManager::getMainPower()==POWERMODE::ON);
|
||||||
// set heartbeat to 2 seconds because we need to sync the metadata (1 second is too short!)
|
// set heartbeat to 2 seconds because we need to sync the metadata (1 second is too short!)
|
||||||
StringFormatter::send(stream,F("*%d\nHMConnecting..\n"), HEARTBEAT_PRELOAD);
|
StringFormatter::send(stream,F("*%d\nHMConnecting..\n"), HEARTBEAT_PRELOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -570,7 +571,7 @@ void WiThrottle::sendRoutes(Print* stream) {
|
||||||
|
|
||||||
void WiThrottle::sendFunctions(Print* stream, byte loco) {
|
void WiThrottle::sendFunctions(Print* stream, byte loco) {
|
||||||
int16_t locoid=myLocos[loco].cab;
|
int16_t locoid=myLocos[loco].cab;
|
||||||
int fkeys=29;
|
int fkeys=32; // upper limit (send functions 0 to 31)
|
||||||
myLocos[loco].functionToggles=1<<2; // F2 (HORN) is a non-toggle
|
myLocos[loco].functionToggles=1<<2; // F2 (HORN) is a non-toggle
|
||||||
|
|
||||||
#ifdef EXRAIL_ACTIVE
|
#ifdef EXRAIL_ACTIVE
|
||||||
|
@ -620,7 +621,7 @@ void WiThrottle::sendFunctions(Print* stream, byte loco) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
for(int fKey=0; fKey<fkeys; fKey++) {
|
for(int fKey=0; fKey<fkeys; fKey++) {
|
||||||
int fstate=DCC::getFn(locoid,fKey);
|
int8_t fstate=DCC::getFn(locoid,fKey);
|
||||||
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey);
|
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,25 +74,39 @@ class NetworkClient {
|
||||||
public:
|
public:
|
||||||
NetworkClient(WiFiClient c) {
|
NetworkClient(WiFiClient c) {
|
||||||
wifi = c;
|
wifi = c;
|
||||||
};
|
|
||||||
bool ok() {
|
|
||||||
return (inUse && wifi.connected());
|
|
||||||
};
|
|
||||||
bool recycle(WiFiClient c) {
|
|
||||||
|
|
||||||
if (inUse == true) return false;
|
|
||||||
|
|
||||||
// return false here until we have
|
|
||||||
// implemented a LRU timer
|
|
||||||
// if (LRU too recent) return false;
|
|
||||||
return false;
|
|
||||||
|
|
||||||
wifi = c;
|
|
||||||
inUse = true;
|
inUse = true;
|
||||||
|
};
|
||||||
|
bool active(byte clientId) {
|
||||||
|
if (!inUse)
|
||||||
|
return false;
|
||||||
|
if(!wifi.connected()) {
|
||||||
|
DIAG(F("Remove client %d"), clientId);
|
||||||
|
CommandDistributor::forget(clientId);
|
||||||
|
wifi.stop();
|
||||||
|
inUse = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
bool recycle(WiFiClient c) {
|
||||||
|
if (wifi == c) {
|
||||||
|
if (inUse == true)
|
||||||
|
DIAG(F("WARNING: Duplicate"));
|
||||||
|
else
|
||||||
|
DIAG(F("Returning"));
|
||||||
|
inUse = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (inUse == false) {
|
||||||
|
wifi = c;
|
||||||
|
inUse = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
WiFiClient wifi;
|
WiFiClient wifi;
|
||||||
bool inUse = true;
|
private:
|
||||||
|
bool inUse;
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<NetworkClient> clients; // a list to hold all clients
|
static std::vector<NetworkClient> clients; // a list to hold all clients
|
||||||
|
@ -150,6 +164,8 @@ bool WifiESP::setup(const char *SSid,
|
||||||
if (haveSSID && havePassword && !forceAP) {
|
if (haveSSID && havePassword && !forceAP) {
|
||||||
WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE!
|
WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE!
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // Scan all channels so we find strongest
|
||||||
|
// (default in Wifi library is first match)
|
||||||
#ifdef SERIAL_BT_COMMANDS
|
#ifdef SERIAL_BT_COMMANDS
|
||||||
WiFi.setSleep(true);
|
WiFi.setSleep(true);
|
||||||
#else
|
#else
|
||||||
|
@ -163,7 +179,9 @@ bool WifiESP::setup(const char *SSid,
|
||||||
delay(500);
|
delay(500);
|
||||||
}
|
}
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
|
// DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
|
||||||
|
DIAG(F("Wifi in STA mode"));
|
||||||
|
LCD(7, F("IP: %s"), WiFi.localIP().toString().c_str());
|
||||||
wifiUp = true;
|
wifiUp = true;
|
||||||
} else {
|
} else {
|
||||||
DIAG(F("Could not connect to Wifi SSID %s"),SSid);
|
DIAG(F("Could not connect to Wifi SSID %s"),SSid);
|
||||||
|
@ -188,7 +206,7 @@ bool WifiESP::setup(const char *SSid,
|
||||||
if (!haveSSID || forceAP) {
|
if (!haveSSID || forceAP) {
|
||||||
// prepare all strings
|
// prepare all strings
|
||||||
String strSSID(forceAP ? SSid : "DCCEX_");
|
String strSSID(forceAP ? SSid : "DCCEX_");
|
||||||
String strPass(forceAP ? password : "PASS_");
|
String strPass( (forceAP && havePassword) ? password : "PASS_");
|
||||||
if (!forceAP) {
|
if (!forceAP) {
|
||||||
String strMac = WiFi.macAddress();
|
String strMac = WiFi.macAddress();
|
||||||
strMac.remove(0,9);
|
strMac.remove(0,9);
|
||||||
|
@ -209,8 +227,13 @@ bool WifiESP::setup(const char *SSid,
|
||||||
if (WiFi.softAP(strSSID.c_str(),
|
if (WiFi.softAP(strSSID.c_str(),
|
||||||
havePassword ? password : strPass.c_str(),
|
havePassword ? password : strPass.c_str(),
|
||||||
channel, false, 8)) {
|
channel, false, 8)) {
|
||||||
DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
|
// DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
|
||||||
DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
|
DIAG(F("Wifi in AP mode"));
|
||||||
|
LCD(5, F("Wifi: %s"), strSSID.c_str());
|
||||||
|
if (!havePassword)
|
||||||
|
LCD(6, F("PASS: %s"),strPass.c_str());
|
||||||
|
// DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
|
||||||
|
LCD(7, F("IP: %s"),WiFi.softAPIP().toString().c_str());
|
||||||
wifiUp = true;
|
wifiUp = true;
|
||||||
APmode = true;
|
APmode = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -276,37 +299,26 @@ void WifiESP::loop() {
|
||||||
// really no good way to check for LISTEN especially in AP mode?
|
// really no good way to check for LISTEN especially in AP mode?
|
||||||
wl_status_t wlStatus;
|
wl_status_t wlStatus;
|
||||||
if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) {
|
if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) {
|
||||||
// loop over all clients and remove inactive
|
|
||||||
for (clientId=0; clientId<clients.size(); clientId++){
|
|
||||||
// check if client is there and alive
|
|
||||||
if(clients[clientId].inUse && !clients[clientId].wifi.connected()) {
|
|
||||||
DIAG(F("Remove client %d"), clientId);
|
|
||||||
CommandDistributor::forget(clientId);
|
|
||||||
clients[clientId].wifi.stop();
|
|
||||||
clients[clientId].inUse = false;
|
|
||||||
//Do NOT clients.erase(clients.begin()+clientId) as
|
|
||||||
//that would mix up clientIds for later.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (server->hasClient()) {
|
if (server->hasClient()) {
|
||||||
WiFiClient client;
|
WiFiClient client;
|
||||||
while (client = server->available()) {
|
while (client = server->available()) {
|
||||||
for (clientId=0; clientId<clients.size(); clientId++){
|
for (clientId=0; clientId<clients.size(); clientId++){
|
||||||
if (clients[clientId].recycle(client)) {
|
if (clients[clientId].recycle(client)) {
|
||||||
DIAG(F("Recycle client %d %s"), clientId, client.remoteIP().toString().c_str());
|
DIAG(F("Recycle client %d %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clientId>=clients.size()) {
|
if (clientId>=clients.size()) {
|
||||||
NetworkClient nc(client);
|
NetworkClient nc(client);
|
||||||
clients.push_back(nc);
|
clients.push_back(nc);
|
||||||
DIAG(F("New client %d, %s"), clientId, client.remoteIP().toString().c_str());
|
DIAG(F("New client %d, %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// loop over all connected clients
|
// loop over all connected clients
|
||||||
|
// this removes as a side effect inactive clients when checking ::active()
|
||||||
for (clientId=0; clientId<clients.size(); clientId++){
|
for (clientId=0; clientId<clients.size(); clientId++){
|
||||||
if(clients[clientId].ok()) {
|
if(clients[clientId].active(clientId)) {
|
||||||
int len;
|
int len;
|
||||||
if ((len = clients[clientId].wifi.available()) > 0) {
|
if ((len = clients[clientId].wifi.available()) > 0) {
|
||||||
// read data from client
|
// read data from client
|
||||||
|
@ -344,7 +356,7 @@ void WifiESP::loop() {
|
||||||
}
|
}
|
||||||
// buffer filled, end with '\0' so we can use it as C string
|
// buffer filled, end with '\0' so we can use it as C string
|
||||||
buffer[count]='\0';
|
buffer[count]='\0';
|
||||||
if((unsigned int)clientId <= clients.size() && clients[clientId].ok()) {
|
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
|
||||||
if (Diag::CMD || Diag::WITHROTTLE)
|
if (Diag::CMD || Diag::WITHROTTLE)
|
||||||
DIAG(F("SEND %d:%s"), clientId, buffer);
|
DIAG(F("SEND %d:%s"), clientId, buffer);
|
||||||
clients[clientId].wifi.write(buffer,count);
|
clients[clientId].wifi.write(buffer,count);
|
||||||
|
@ -377,8 +389,9 @@ void WifiESP::loop() {
|
||||||
// prio task. On core1 this is not a problem
|
// prio task. On core1 this is not a problem
|
||||||
// as there the wdt is disabled by the
|
// as there the wdt is disabled by the
|
||||||
// arduio IDE startup routines.
|
// arduio IDE startup routines.
|
||||||
if (xPortGetCoreID() == 0)
|
if (xPortGetCoreID() == 0) {
|
||||||
feedTheDog0();
|
feedTheDog0();
|
||||||
yield();
|
yield();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif //ESP32
|
#endif //ESP32
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/*
|
/*
|
||||||
|
* © 2022-2024 Paul M. Antoine
|
||||||
* © 2021 Fred Decker
|
* © 2021 Fred Decker
|
||||||
* © 2020-2022 Harald Barth
|
* © 2020-2022 Harald Barth
|
||||||
* © 2020-2022 Chris Harlow
|
* © 2020-2022 Chris Harlow
|
||||||
|
@ -68,7 +69,9 @@ Stream * WifiInterface::wifiStream;
|
||||||
#define NUM_SERIAL 3
|
#define NUM_SERIAL 3
|
||||||
#define SERIAL1 Serial3
|
#define SERIAL1 Serial3
|
||||||
#define SERIAL3 Serial5
|
#define SERIAL3 Serial5
|
||||||
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG)
|
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) \
|
||||||
|
|| defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) \
|
||||||
|
|| defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
|
||||||
#define NUM_SERIAL 2
|
#define NUM_SERIAL 2
|
||||||
#define SERIAL1 Serial6
|
#define SERIAL1 Serial6
|
||||||
#else
|
#else
|
||||||
|
|
|
@ -194,6 +194,31 @@ The configuration file for DCC-EX Command Station
|
||||||
//
|
//
|
||||||
// #define DISABLE_PROG
|
// #define DISABLE_PROG
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DISABLE / ENABLE VDPY
|
||||||
|
//
|
||||||
|
// The Virtual display "VDPY" feature is by default enabled everywhere
|
||||||
|
// but on Uno and Nano. If you think you can fit it (for example
|
||||||
|
// having disabled some of the features above) you can enable it with
|
||||||
|
// ENABLE_VDPY. You can even disable it on all other CPUs with
|
||||||
|
// DISABLE_VDPY
|
||||||
|
//
|
||||||
|
// #define DISABLE_VDPY
|
||||||
|
// #define ENABLE_VDPY
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DISABLE / ENABLE DIAG
|
||||||
|
//
|
||||||
|
// To diagose different errors, you can turn on differnet messages. This costs
|
||||||
|
// program memory which we do not have enough on the Uno and Nano, so it is
|
||||||
|
// by default DISABLED on those. If you think you can fit it (for example
|
||||||
|
// having disabled some of the features above) you can enable it with
|
||||||
|
// ENABLE_DIAG. You can even disable it on all other CPUs with
|
||||||
|
// DISABLE_DIAG
|
||||||
|
//
|
||||||
|
// #define DISABLE_DIAG
|
||||||
|
// #define ENABLE_DIAG
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address
|
// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address
|
||||||
// is 127 and the first long address is 128. There are manufacturers which have
|
// is 127 and the first long address is 128. There are manufacturers which have
|
||||||
|
@ -205,6 +230,14 @@ The configuration file for DCC-EX Command Station
|
||||||
// We do not support to use the same address, for example 100(long) and 100(short)
|
// We do not support to use the same address, for example 100(long) and 100(short)
|
||||||
// at the same time, there must be a border.
|
// at the same time, there must be a border.
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Some newer 32bit microcontrollers boot very quickly, so powering on I2C and other
|
||||||
|
// peripheral devices at the same time may result in the CommandStation booting too
|
||||||
|
// quickly to detect them.
|
||||||
|
// To work around this, uncomment the STARTUP_DELAY line below and set a value in
|
||||||
|
// milliseconds that works for your environment, default is 3000 (3 seconds).
|
||||||
|
// #define STARTUP_DELAY 3000
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213
|
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213
|
||||||
|
@ -283,6 +316,12 @@ The configuration file for DCC-EX Command Station
|
||||||
//
|
//
|
||||||
//#define SERIAL_BT_COMMANDS
|
//#define SERIAL_BT_COMMANDS
|
||||||
|
|
||||||
|
// BOOSTER PIN INPUT ON ESP32
|
||||||
|
// On ESP32 you have the possibility to define a pin as booster input
|
||||||
|
// Arduio pin D2 is GPIO 26 on ESPDuino32
|
||||||
|
//
|
||||||
|
//#define BOOSTER_INPUT 26
|
||||||
|
|
||||||
// SABERTOOTH
|
// SABERTOOTH
|
||||||
//
|
//
|
||||||
// This is a very special option and only useful if you happen to have a
|
// This is a very special option and only useful if you happen to have a
|
||||||
|
|
24
defines.h
24
defines.h
|
@ -144,9 +144,9 @@
|
||||||
#define DISABLE_EEPROM
|
#define DISABLE_EEPROM
|
||||||
#endif
|
#endif
|
||||||
// STM32 support for native I2C is awaiting development
|
// STM32 support for native I2C is awaiting development
|
||||||
#ifndef I2C_USE_WIRE
|
// #ifndef I2C_USE_WIRE
|
||||||
#define I2C_USE_WIRE
|
// #define I2C_USE_WIRE
|
||||||
#endif
|
// #endif
|
||||||
|
|
||||||
/* TODO when ready
|
/* TODO when ready
|
||||||
#elif defined(ARDUINO_ARCH_RP2040)
|
#elif defined(ARDUINO_ARCH_RP2040)
|
||||||
|
@ -213,6 +213,24 @@
|
||||||
//
|
//
|
||||||
#define WIFI_SERIAL_LINK_SPEED 115200
|
#define WIFI_SERIAL_LINK_SPEED 115200
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
|
||||||
|
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
|
||||||
|
//
|
||||||
|
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
|
||||||
|
#define IO_NO_HAL // HAL too big whatever you disable otherwise
|
||||||
|
|
||||||
|
#ifndef ENABLE_VDPY
|
||||||
|
#define DISABLE_VDPY
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ENABLE_DIAG
|
||||||
|
#define DISABLE_DIAG
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include ( "myAutomation.h")
|
#if __has_include ( "myAutomation.h")
|
||||||
#if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) || defined(DISABLE_PROG)
|
#if defined(HAS_ENOUGH_MEMORY) || defined(DISABLE_EEPROM) || defined(DISABLE_PROG)
|
||||||
#define EXRAIL_ACTIVE
|
#define EXRAIL_ACTIVE
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
//#include "IO_EXTurntable.h" // Turntable-EX turntable controller
|
//#include "IO_EXTurntable.h" // Turntable-EX turntable controller
|
||||||
//#include "IO_EXFastClock.h" // FastClock driver
|
//#include "IO_EXFastClock.h" // FastClock driver
|
||||||
//#include "IO_PCA9555.h" // 16-bit I/O expander (NXP & Texas Instruments).
|
//#include "IO_PCA9555.h" // 16-bit I/O expander (NXP & Texas Instruments).
|
||||||
|
//#include "IO_I2CDFPlayer.h" // DFPlayer over I2C
|
||||||
|
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
// The function halSetup() is invoked from CS if it exists within the build.
|
// The function halSetup() is invoked from CS if it exists within the build.
|
||||||
|
@ -234,6 +235,31 @@ void halSetup() {
|
||||||
// DFPlayer::create(10000, 10, Serial1);
|
// DFPlayer::create(10000, 10, Serial1);
|
||||||
|
|
||||||
|
|
||||||
|
//=======================================================================
|
||||||
|
// Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module on a SC16IS750/SC16IS752 I2C UART
|
||||||
|
//=======================================================================
|
||||||
|
// DFPlayer via NXP SC16IS752 I2C Dual UART.
|
||||||
|
// I2C address range 0x48 - 0x57
|
||||||
|
//
|
||||||
|
// Generic format:
|
||||||
|
// I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);
|
||||||
|
// Parameters:
|
||||||
|
// 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
|
||||||
|
// vPins : Total number of virtual pins allocated (1 vPin is supported currently)
|
||||||
|
// 1st vPin for UART 0
|
||||||
|
// I2C Address : I2C address of the serial controller, in 0x format
|
||||||
|
// xtal : 0 for 1.8432Mhz, 1 for 14.7456Mhz
|
||||||
|
//
|
||||||
|
// The vPin is also a pin that can be read with the WAITFOR(vPin) command indicating if the DFPlayer has finished playing a track
|
||||||
|
//
|
||||||
|
|
||||||
|
// I2CDFPlayer::create(10000, 1, 0x48, 1);
|
||||||
|
//
|
||||||
|
// Configuration example on a multiplexer
|
||||||
|
// I2CDFPlayer::create(10000, 1, {I2CMux_0, SubBus_0, 0x48}, 1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//=======================================================================
|
//=======================================================================
|
||||||
// 16-pad capacitative touch key pad based on TP229 IC.
|
// 16-pad capacitative touch key pad based on TP229 IC.
|
||||||
//=======================================================================
|
//=======================================================================
|
||||||
|
@ -285,12 +311,13 @@ void halSetup() {
|
||||||
//=======================================================================
|
//=======================================================================
|
||||||
// The parameters are:
|
// The parameters are:
|
||||||
// firstVpin = First available Vpin to allocate
|
// firstVpin = First available Vpin to allocate
|
||||||
// numPins= Number of Vpins to allocate, can be either 1 or 2
|
// numPins= Number of Vpins to allocate, can be either 1 to 3
|
||||||
// i2cAddress = Available I2C address (default 0x70)
|
// i2cAddress = Available I2C address (default 0x67)
|
||||||
|
|
||||||
//RotaryEncoder::create(firstVpin, numPins, i2cAddress);
|
//RotaryEncoder::create(firstVpin, numPins, i2cAddress);
|
||||||
//RotaryEncoder::create(700, 1, 0x70);
|
//RotaryEncoder::create(700, 1, 0x67);
|
||||||
//RotaryEncoder::create(701, 2, 0x71);
|
//RotaryEncoder::create(700, 2, 0x67);
|
||||||
|
//RotaryEncoder::create(700, 3, 0x67);
|
||||||
|
|
||||||
//=======================================================================
|
//=======================================================================
|
||||||
// The following directive defines an EX-FastClock instance.
|
// The following directive defines an EX-FastClock instance.
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
default_envs =
|
default_envs =
|
||||||
mega2560
|
mega2560
|
||||||
uno
|
uno
|
||||||
mega328
|
|
||||||
unowifiR2
|
unowifiR2
|
||||||
nano
|
nano
|
||||||
samd21-dev-usb
|
samd21-dev-usb
|
||||||
|
@ -30,8 +29,7 @@ include_dir = .
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
build_flags = -Wall -Wextra
|
build_flags = -Wall -Wextra
|
||||||
monitor_filters = time
|
; monitor_filters = time
|
||||||
; lib_deps = adafruit/Adafruit ST7735 and ST7789 Library @ ^1.10.0
|
|
||||||
|
|
||||||
[env:samd21-dev-usb]
|
[env:samd21-dev-usb]
|
||||||
platform = atmelsam
|
platform = atmelsam
|
||||||
|
@ -60,7 +58,7 @@ framework = arduino
|
||||||
lib_deps = ${env.lib_deps}
|
lib_deps = ${env.lib_deps}
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
build_flags = -std=c++17 ; -DI2C_USE_WIRE -DDIAG_LOOPTIMES -DDIAG_IO
|
build_flags = -std=c++17
|
||||||
|
|
||||||
[env:mega2560-debug]
|
[env:mega2560-debug]
|
||||||
platform = atmelavr
|
platform = atmelavr
|
||||||
|
@ -72,7 +70,7 @@ lib_deps =
|
||||||
SPI
|
SPI
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES
|
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES
|
||||||
|
|
||||||
[env:mega2560-no-HAL]
|
[env:mega2560-no-HAL]
|
||||||
platform = atmelavr
|
platform = atmelavr
|
||||||
|
@ -108,7 +106,7 @@ lib_deps =
|
||||||
SPI
|
SPI
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
build_flags = ; -DDIAG_LOOPTIMES
|
build_flags =
|
||||||
|
|
||||||
[env:mega328]
|
[env:mega328]
|
||||||
platform = atmelavr
|
platform = atmelavr
|
||||||
|
@ -150,10 +148,7 @@ build_flags =
|
||||||
platform = atmelavr
|
platform = atmelavr
|
||||||
board = uno
|
board = uno
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps =
|
lib_deps = ${env.lib_deps}
|
||||||
${env.lib_deps}
|
|
||||||
arduino-libraries/Ethernet
|
|
||||||
SPI
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
build_flags = -mcall-prologues
|
build_flags = -mcall-prologues
|
||||||
|
@ -166,9 +161,14 @@ framework = arduino
|
||||||
lib_deps = ${env.lib_deps}
|
lib_deps = ${env.lib_deps}
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
|
build_flags = -mcall-prologues
|
||||||
|
|
||||||
[env:ESP32]
|
[env:ESP32]
|
||||||
platform = espressif32
|
; Lock version to 6.7.0 as that is
|
||||||
|
; Arduino v2.0.16 (based on IDF v4.4.7)
|
||||||
|
; which is the latest version based
|
||||||
|
; on IDF v4. We can not use IDF v5.
|
||||||
|
platform = espressif32 @ 6.7.0
|
||||||
board = esp32dev
|
board = esp32dev
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps = ${env.lib_deps}
|
lib_deps = ${env.lib_deps}
|
||||||
|
@ -190,10 +190,75 @@ platform = ststm32
|
||||||
board = nucleo_f446re
|
board = nucleo_f446re
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps = ${env.lib_deps}
|
lib_deps = ${env.lib_deps}
|
||||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable ; -DDIAG_LOOPTIMES ; -DDIAG_IO
|
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_echo = yes
|
monitor_echo = yes
|
||||||
|
|
||||||
|
; Experimental - no reason this should not work, but not
|
||||||
|
; tested as yet
|
||||||
|
;
|
||||||
|
[env:Nucleo-F401RE]
|
||||||
|
platform = ststm32
|
||||||
|
board = nucleo_f401re
|
||||||
|
framework = arduino
|
||||||
|
lib_deps = ${env.lib_deps}
|
||||||
|
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_echo = yes
|
||||||
|
|
||||||
|
; Commented out by default as the F13ZH has variant files
|
||||||
|
; but NOT the nucleo_f413zh.json file which needs to be
|
||||||
|
; installed before you can let PlatformIO see this
|
||||||
|
;
|
||||||
|
; [env:Nucleo-F413ZH]
|
||||||
|
; platform = ststm32
|
||||||
|
; board = nucleo_f413zh
|
||||||
|
; framework = arduino
|
||||||
|
; lib_deps = ${env.lib_deps}
|
||||||
|
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
|
; monitor_speed = 115200
|
||||||
|
; monitor_echo = yes
|
||||||
|
|
||||||
|
; Commented out by default as the F446ZE needs variant files
|
||||||
|
; installed before you can let PlatformIO see this
|
||||||
|
;
|
||||||
|
; [env:Nucleo-F446ZE]
|
||||||
|
; platform = ststm32
|
||||||
|
; board = nucleo_f446ze
|
||||||
|
; framework = arduino
|
||||||
|
; lib_deps = ${env.lib_deps}
|
||||||
|
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
|
; monitor_speed = 115200
|
||||||
|
; monitor_echo = yes
|
||||||
|
|
||||||
|
; Commented out by default as the F412ZG needs variant files
|
||||||
|
; installed before you can let PlatformIO see this
|
||||||
|
;
|
||||||
|
; [env:Nucleo-F412ZG]
|
||||||
|
; platform = ststm32
|
||||||
|
; board = blah_f412zg
|
||||||
|
; framework = arduino
|
||||||
|
; lib_deps = ${env.lib_deps}
|
||||||
|
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
|
; monitor_speed = 115200
|
||||||
|
; monitor_echo = yes
|
||||||
|
; upload_protocol = stlink
|
||||||
|
|
||||||
|
; Experimental - Ethernet work still in progress
|
||||||
|
;
|
||||||
|
; [env:Nucleo-F429ZI]
|
||||||
|
; platform = ststm32
|
||||||
|
; board = nucleo_f429zi
|
||||||
|
; framework = arduino
|
||||||
|
; lib_deps = ${env.lib_deps}
|
||||||
|
; arduino-libraries/Ethernet @ ^2.0.1
|
||||||
|
; stm32duino/STM32Ethernet @ ^1.3.0
|
||||||
|
; stm32duino/STM32duino LwIP @ ^2.1.2
|
||||||
|
; build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||||
|
; monitor_speed = 115200
|
||||||
|
; monitor_echo = yes
|
||||||
|
; upload_protocol = stlink
|
||||||
|
|
||||||
[env:Teensy3_2]
|
[env:Teensy3_2]
|
||||||
platform = teensy
|
platform = teensy
|
||||||
board = teensy31
|
board = teensy31
|
||||||
|
@ -233,4 +298,3 @@ framework = arduino
|
||||||
build_flags = -std=c++17 -Os -g2
|
build_flags = -std=c++17 -Os -g2
|
||||||
lib_deps = ${env.lib_deps}
|
lib_deps = ${env.lib_deps}
|
||||||
lib_ignore =
|
lib_ignore =
|
||||||
|
|
||||||
|
|
136
version.h
136
version.h
|
@ -3,16 +3,132 @@
|
||||||
|
|
||||||
#include "StringFormatter.h"
|
#include "StringFormatter.h"
|
||||||
|
|
||||||
#define VERSION "5.0.9"
|
#define VERSION "5.2.60"
|
||||||
// 5.0.9 - EX-IOExpander bug fix for memory allocation
|
// 5.2.60 - Bugfix: Opcode AFTEROVERLOAD does not have an argument that is a pin and needs to be initialized
|
||||||
// - EX-IOExpander bug fix to allow for devices with no analogue or no digital pins
|
// - Remove inrush throttle after half good time so that we go to mode overload if problem persists
|
||||||
// 5.0.8 - Bugfix: Do not crash on turnouts without description
|
// 5.2.59 - STM32 bugfix correct Serial1 definition for Nucleo-F401RE
|
||||||
// 5.0.7 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0
|
// - STM32 add support for ARDUINO_NUCLEO_F4X9ZI type to span F429/F439 in upcoming STM32duino release v2.8 as a result of our PR
|
||||||
// 5.0.6 - Bugfix lost TURNOUTL description
|
// 5.2.58 - EXRAIL ALIAS allows named pins
|
||||||
// 5.0.5 - Bugfix version detection logic and better message
|
// 5.2.57 - Bugfix autoreverse: Apply mode by binart bit match and not by equality
|
||||||
// 5.0.4 - Bugfix: <JR> misses default roster.
|
// 5.2.56 - Bugfix and refactor for EXRAIL getSignalSlot
|
||||||
// 5.0.3 - Check bad AT firmware version
|
// 5.2.55 - Move EXRAIL isSignal() to public to allow use in STEALTH call
|
||||||
// 5.0.2 - Bugfix: ESP32 30ms off time
|
// 5.2.54 - Bugfix for EXRAIL signal handling for active high
|
||||||
|
// 5.2.53 - Bugfix for EX-Fastclock, call I2CManager.begin() before checking I2C address
|
||||||
|
// 5.2.52 - Bugfix for ADCee() to handle ADC2 and ADC3 channel inputs on F446ZE and others
|
||||||
|
// - Add support for ports G and H on STM32 for ADCee() and MotorDriver pins/shadow regs
|
||||||
|
// 5.2.51 - Bugfix for SIGNAL: Distinguish between sighandle and sigid
|
||||||
|
// 5.2.50 - EXRAIL ONBUTTON/ONSENSOR observe LATCH
|
||||||
|
// 5.2.49 - EXRAIL additions:
|
||||||
|
// ONBUTTON, ONSENSOR
|
||||||
|
// 5.2.48 - Bugfix: HALDisplay was generating I2C traffic prior to I2C being initialised
|
||||||
|
// 5.2.47 - EXRAIL additions:
|
||||||
|
// STEALTH_GLOBAL
|
||||||
|
// BLINK
|
||||||
|
// TOGGLE_TURNOUT
|
||||||
|
// FTOGGLE, XFTOGGLE
|
||||||
|
// Reduced code-developmenmt DIAG noise
|
||||||
|
// 5.2.46 - Support for extended consist CV20 in <R> and <W id>
|
||||||
|
// - New cmd <W CONSIST id [REVERSE]> to handle long/short consist ids
|
||||||
|
// 5.2.45 - ESP32 Trackmanager reset cab number to 0 when track is not DC
|
||||||
|
// ESP32 fix PWM LEDC inverted pin mode
|
||||||
|
// ESP32 rewrite PWM LEDC to use pin mux
|
||||||
|
// 5.2.42 - ESP32 Bugfix: Uninitialized stack variable
|
||||||
|
// 5.2.41 - Update rotary encoder default address to 0x67
|
||||||
|
// 5.2.40 - Allow no shield
|
||||||
|
// 5.2.39 - Functions for DC frequency: Use func up to F31
|
||||||
|
// 5.2.38 - Exrail MESSAGE("text") to send a user message to all
|
||||||
|
// connected throttles (uses <m "text"> and withrottle Hmtext.
|
||||||
|
// 5.2.37 - Bugfix ESP32: Use BOOSTER_INPUT define
|
||||||
|
// 5.2.36 - Variable frequency for DC mode
|
||||||
|
// 5.2.35 - Bugfix: Make DCC Extended Accessories follow RCN-213
|
||||||
|
// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories
|
||||||
|
// - Exrail ASPECT(address,aspect) for above.
|
||||||
|
// - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)
|
||||||
|
// - Exrail intercept <A ...> for DCC Signals.
|
||||||
|
// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)
|
||||||
|
// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)
|
||||||
|
// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.
|
||||||
|
// 5.2.30 - Bugfix: WiThrottle sendIntro after initial N message as well
|
||||||
|
// 5.2.29 - Added IO_I2CDFPlayer.h to support DFPLayer over I2C connected to NXP SC16IS750/SC16IS752 (currently only single UART for SC16IS752)
|
||||||
|
// - Added enhanced IO_I2CDFPLayer enum commands to EXRAIL2.h
|
||||||
|
// - Added PLAYSOUND alias of ANOUT to EXRAILMacros.h
|
||||||
|
// - Added UART detection to I2CManager.cpp
|
||||||
|
// 5.2.28 - ESP32: Can all Wifi channels.
|
||||||
|
// - ESP32: Only write Wifi password to display if it is a well known one
|
||||||
|
// 5.2.27 - Bugfix: IOExpander memory allocation
|
||||||
|
// 5.2.26 - Silently ignore overridden HAL defaults
|
||||||
|
// - include HAL_IGNORE_DEFAULTS macro in EXRAIL
|
||||||
|
// 5.2.25 - Fix bug causing <X> after working <D commands
|
||||||
|
// 5.2.24 - Exrail macro asserts to catch
|
||||||
|
// : duplicate/missing automation/route/sequence/call ids
|
||||||
|
// : latches and reserves out of range
|
||||||
|
// : speeds out of range
|
||||||
|
// 5.2.23 - KeywordHasher _hk (no functional change)
|
||||||
|
// 5.2.22 - Bugfixes: Empty turnout descriptions ok; negative route numbers valid.
|
||||||
|
// 5.2.21 - Add STARTUP_DELAY config option to delay CS bootup
|
||||||
|
// 5.2.20 - Check return of Ethernet.begin()
|
||||||
|
// 5.2.19 - ESP32: Determine if the RMT hardware can handle DCC
|
||||||
|
// 5.2.18 - Display network IP fix
|
||||||
|
// 5.2.17 - ESP32 simplify network logic
|
||||||
|
// 5.2.16 - Bugfix to allow for devices using the EX-IOExpander protocol to have no analogue or no digital pins
|
||||||
|
// 5.2.15 - move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions
|
||||||
|
// - add repeats to function packets that are not reminded in accordance with accessory packets
|
||||||
|
// 5.2.14 - Reminder window DCC packet optimization
|
||||||
|
// - Optional #define DISABLE_FUNCTION_REMINDERS
|
||||||
|
// 5.2.13 - EXRAIL STEALTH
|
||||||
|
// 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for
|
||||||
|
// - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers
|
||||||
|
// - STM32 extra UART/USARTs for larger Nucleo models
|
||||||
|
// 5.2.11 - Change from TrackManager::returnMode to TrackManager::getMode
|
||||||
|
// 5.2.10 - Include trainbrains.eu block unoccupancy driver
|
||||||
|
// - include IO_PCA9555
|
||||||
|
// 5.2.9 - Bugfix LCD startup with no LCD, uses <@
|
||||||
|
// 5.2.9 - EXRAIL STASH feature
|
||||||
|
// 5.2.8 - Bugfix: Do not turn off all tracks on change
|
||||||
|
// give better power messages
|
||||||
|
// 5.2.7 - Bugfix: EXRAIL ling segment
|
||||||
|
// - Bugfix: Back out wrongly added const
|
||||||
|
// - Bugfix ESP32: Do not inverse DCX direction signal twice
|
||||||
|
// 5.2.6 - Trackmanager broadcast power state on track mode change
|
||||||
|
// 5.2.5 - Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC
|
||||||
|
// 5.2.4 - LCD macro will not do diag if that duplicates @ to same target.
|
||||||
|
// - Added ROUTE_DISABLED macro in EXRAIL
|
||||||
|
// 5.2.3 - Bugfix: Catch stange input to parser
|
||||||
|
// 5.2.2 - Added option to allow MAX_CHARACTER_ROWS to be defined in config.h
|
||||||
|
// 5.2.1 - Trackmanager rework for simpler structure
|
||||||
|
// 5.2.0 - ESP32: Autoreverse and booster mode support
|
||||||
|
// 5.1.21 - EXRAIL invoke multiple ON handlers for same event
|
||||||
|
// 5.1.20 - EXRAIL Tidy and ROUTE_STATE, ROUTE_CAPTION
|
||||||
|
// 5.1.19 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0
|
||||||
|
// 5.1.18 - TURNOUTL bugfix
|
||||||
|
// 5.1.17 - Divide out C for config and D for diag commands
|
||||||
|
// 5.1.16 - Remove I2C address from EXTT_TURNTABLE macro to work with MUX, requires separate HAL macro to create
|
||||||
|
// 5.1.15 - LCC/Adapter support and Exrail feature-compile-out.
|
||||||
|
// 5.1.14 - Fixed IFTTPOSITION
|
||||||
|
// 5.1.13 - Changed turntable broadcast from i to I due to server string conflict
|
||||||
|
// 5.1.12 - Added Power commands <0 A> & <1 A> etc. and update to <=>
|
||||||
|
// Added EXRAIL SET_POWER(track, ON/OFF)
|
||||||
|
// Fixed a problem whereby <1 MAIN> also powered on PROG track
|
||||||
|
// Added functions to TrackManager.cpp to allow UserAddin code for power display on OLED/LCD
|
||||||
|
// Added - returnMode(byte t), returnDCAddr(byte t) & getModeName(byte Mode)
|
||||||
|
// 5.1.11 - STM32F4xx revised I2C clock setup, no correctly sets clock and has fully variable frequency selection
|
||||||
|
// 5.1.10 - STM32F4xx DCCEXanalogWrite to handle PWM generation for TrackManager DC/DCX
|
||||||
|
// - STM32F4xx DCC 58uS timer now using non-PWM output timers where possible
|
||||||
|
// - ESP32 brakeCanPWM check now detects UNUSED_PIN
|
||||||
|
// - ARM architecture brakeCanPWM now uses digitalPinHasPWM()
|
||||||
|
// - STM32F4xx shadowpin extensions to handle pins on ports D, E and F
|
||||||
|
// 5.1.9 - Fixed IO_PCA9555'h to work with PCA9548 mux, tested OK
|
||||||
|
// 5.1.8 - STM32Fxx ADCee extension to support ADCs #2 and #3
|
||||||
|
// 5.1.7 - Fix turntable broadcasts for non-movement activities and <JP> result
|
||||||
|
// 5.1.6 - STM32F4xx native I2C driver added
|
||||||
|
// 5.1.5 - Added turntable object and EXRAIL commands
|
||||||
|
// - <I ...>, <JO ...>, <JP ...> - turntable commands
|
||||||
|
// - DCC_TURNTABLE, EXTT_TURNTABLE, IFTTPOSITION, ONROTATE, ROTATE, ROTATE_DCC, TT_ADDPOSITION, WAITFORTT EXRAIL
|
||||||
|
// 5.1.4 - Added ONOVERLOAD & AFTEROVERLOAD to EXRAIL
|
||||||
|
// 5.1.3 - Make parser more fool proof
|
||||||
|
// 5.1.2 - Bugfix: ESP32 30ms off time
|
||||||
|
// 5.1.1 - Check bad AT firmware version
|
||||||
|
// - Update IO_PCA9555.h reflecting IO_MCP23017.h changes to support PCA9548 mux
|
||||||
// 5.0.1 - Bugfix: execute 30ms off time before rejoin
|
// 5.0.1 - Bugfix: execute 30ms off time before rejoin
|
||||||
// 5.0.0 - Make 4.2.69 the 5.0.0 release
|
// 5.0.0 - Make 4.2.69 the 5.0.0 release
|
||||||
// 4.2.69 - Bugfix: Make <!> work in DC mode
|
// 4.2.69 - Bugfix: Make <!> work in DC mode
|
||||||
|
|
Loading…
Reference in New Issue
Block a user