mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-30 02:43:45 +02:00
Compare commits
38 Commits
devel-Ash-
...
sensorRam
Author | SHA1 | Date | |
---|---|---|---|
|
c699eb3492 | ||
|
d84e354142 | ||
|
823ceef338 | ||
|
940d7d70e2 | ||
|
8b7d87d6bd | ||
|
baf6c3036f | ||
|
1f74e3ca6f | ||
|
8558ac6c61 | ||
|
844dac8e3c | ||
|
143c62d6a6 | ||
|
4d8ee2322a | ||
|
3eac6a57b0 | ||
|
d87c3bc87e | ||
|
9adfff8e0a | ||
|
2e0a596afe | ||
|
ad92cc9323 | ||
|
51c4b4d34a | ||
|
e45febc785 | ||
|
e6a829d8ef | ||
|
6a361872e2 | ||
|
e6c6f78bb6 | ||
|
36c773df7a | ||
|
dccb7abac7 | ||
|
1122bf5326 | ||
|
a3455225f4 | ||
|
378452479b | ||
|
94b0a7e364 | ||
|
08ef723a86 | ||
|
ffc0f74312 | ||
|
bdbfc95284 | ||
|
f163e171e4 | ||
|
4cb09e11a5 | ||
|
4f3b7068ca | ||
|
93a3ea49a2 | ||
|
54f8aaf116 | ||
|
fefc4c6035 | ||
|
6e5668c258 | ||
|
a623a79c6a |
@@ -355,11 +355,11 @@ void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter
|
||||
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
|
||||
void CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
|
||||
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
|
||||
void CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
|
||||
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
|
||||
}
|
||||
|
||||
|
@@ -60,8 +60,8 @@ public :
|
||||
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
|
||||
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
|
||||
static void forget(byte clientId);
|
||||
static void broadcastRouteState(uint16_t routeId,byte state);
|
||||
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
|
||||
static void broadcastRouteState(int16_t routeId,byte state);
|
||||
static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
|
||||
static void broadcastMessage(char * message);
|
||||
|
||||
// Handling code for virtual LCD receiver.
|
||||
|
@@ -31,6 +31,7 @@
|
||||
* © 2020-2021 Chris Harlow, Harald Barth, David Cutting,
|
||||
* Fred Decker, Gregor Baues, Anthony W - Dayton
|
||||
* © 2023 Nathan Kellenicki
|
||||
* © 2025 Herb Morton
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
@@ -51,6 +52,12 @@
|
||||
|
||||
#include "DCCEX.h"
|
||||
#include "Display_Implementation.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "Sniffer.h"
|
||||
#include "DCCDecoder.h"
|
||||
Sniffer *dccSniffer = NULL;
|
||||
bool DCCDecoder::active = false;
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
#ifdef CPU_TYPE_ERROR
|
||||
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
|
||||
@@ -109,9 +116,10 @@ void setup()
|
||||
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||
#endif // WIFI_ON
|
||||
#else
|
||||
// ESP32 needs wifi on always
|
||||
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||
#if WIFI_ON
|
||||
PASSWDCHECK(WIFI_PASSWORD); // compile time check
|
||||
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
|
||||
#endif // WIFI_ON
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
#if ETHERNET_ON
|
||||
@@ -124,6 +132,11 @@ void setup()
|
||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||
RMFT::begin();
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
dccSniffer = new Sniffer(BOOSTER_INPUT);
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
|
||||
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
|
||||
@@ -141,25 +154,28 @@ void setup()
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
|
||||
/**************** for future reference
|
||||
void looptimer(unsigned long timeout, const FSH* message)
|
||||
{
|
||||
static unsigned long lasttimestamp = 0;
|
||||
unsigned long now = micros();
|
||||
if (timeout != 0) {
|
||||
unsigned long diff = now - lasttimestamp;
|
||||
if (diff > timeout) {
|
||||
DIAG(message);
|
||||
DIAG(F("DeltaT=%L"), diff);
|
||||
lasttimestamp = micros();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lasttimestamp = now;
|
||||
}
|
||||
*********************************************/
|
||||
void loop()
|
||||
{
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifdef BOOSTER_INPUT
|
||||
static bool oldactive = false;
|
||||
if (dccSniffer) {
|
||||
bool newactive = dccSniffer->inputActive();
|
||||
if (oldactive != newactive) {
|
||||
RMFT2::railsyncEvent(newactive);
|
||||
oldactive = newactive;
|
||||
}
|
||||
DCCPacket p = dccSniffer->fetchPacket();
|
||||
if (p.len() != 0) {
|
||||
if (DCCDecoder::parse(p)) {
|
||||
if (Diag::SNIFFER)
|
||||
p.print();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // BOOSTER_INPUT
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
|
||||
// The main sketch has responsibilities during loop()
|
||||
|
||||
// Responsibility 1: Handle DCC background processes
|
||||
@@ -176,9 +192,11 @@ void loop()
|
||||
|
||||
#endif //WIFI_ON
|
||||
#else //ARDUINO_ARCH_ESP32
|
||||
#if WIFI_ON
|
||||
#ifndef WIFI_TASK_ON_CORE0
|
||||
WifiESP::loop();
|
||||
#endif
|
||||
#endif //WIFI_ON
|
||||
#endif //ARDUINO_ARCH_ESP32
|
||||
#if ETHERNET_ON
|
||||
EthernetInterface::loop();
|
||||
|
102
DCC.cpp
102
DCC.cpp
@@ -162,7 +162,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
|
||||
}
|
||||
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
void DCC::setFunctionInternal(int cab, byte group, byte byte1, byte byte2) {
|
||||
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
|
||||
byte b[4];
|
||||
byte nB = 0;
|
||||
@@ -172,8 +172,8 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
b[nB++] = lowByte(cab);
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
b[nB++] = byte2;
|
||||
|
||||
DCCQueue::scheduleDCCPacket(b, nB, 0, cab);
|
||||
|
||||
DCCQueue::scheduleDCCFunctionPacket(b, nB, cab,group);
|
||||
}
|
||||
|
||||
// returns speed steps 0 to 127 (1 == emergency stop)
|
||||
@@ -327,7 +327,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
|
||||
// the initial decoders were orgnized and that influenced how the DCC
|
||||
// standard was made.
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
|
||||
#endif
|
||||
// use masks to detect wrong values and do nothing
|
||||
if(address != (address & 511))
|
||||
@@ -862,15 +862,34 @@ void DCC::loop() {
|
||||
}
|
||||
|
||||
void DCC::issueReminders() {
|
||||
while(true) {
|
||||
// Move to next loco slot. If occupied, send a reminder.
|
||||
auto slot = nextLocoReminder;
|
||||
if (slot >= &speedTable[MAX_LOCOS]) slot=&speedTable[0]; // Go to start of table
|
||||
if (slot->loco > 0)
|
||||
if (!issueReminder(slot))
|
||||
return;
|
||||
// a loco=0 is at the end of the list, a loco <0 is deleted
|
||||
if (slot->loco==0) nextLocoReminder = &speedTable[0];
|
||||
else nextLocoReminder=slot+1;
|
||||
// slot.loco is -1 for deleted locos, 0 for end of list.
|
||||
for (auto slot=nextLocoReminder;slot->loco;slot++) {
|
||||
if (slot->loco<0) continue; // deleted loco, skip it
|
||||
if (issueReminder(slot)) {
|
||||
nextLocoReminder=slot+1; // remember next one to check
|
||||
return; // reminder sent, exit
|
||||
}
|
||||
}
|
||||
// we have reached the end of the table, so we can move on to
|
||||
// the next loop state and start from the top.
|
||||
// There are 0-9 loop states.. speed,f1,speed,f2,speed,f3,speed,f4,speed,f5
|
||||
loopStatus++;
|
||||
if (loopStatus>9) loopStatus=0; // reset to 0
|
||||
|
||||
// try looking from the start of the table down to where we started last time
|
||||
|
||||
for (auto slot=&speedTable[0];slot<nextLocoReminder;slot++) {
|
||||
if (slot->loco<0) continue; // deleted loco, skip it
|
||||
if (issueReminder(slot)) {
|
||||
nextLocoReminder=slot+1; // remember next one to check
|
||||
return; // reminder sent, exit
|
||||
}
|
||||
}
|
||||
// if we get here then we can update the loop status and start again
|
||||
if (loopStatus==0) return; // nothing found at all
|
||||
}
|
||||
}
|
||||
|
||||
int16_t normalize(byte speed) {
|
||||
@@ -891,7 +910,11 @@ bool DCC::issueReminder(LOCO * slot) {
|
||||
byte flags=slot->groupFlags;
|
||||
|
||||
switch (loopStatus) {
|
||||
case 0: {
|
||||
case 0:
|
||||
case 2:
|
||||
case 4:
|
||||
case 6:
|
||||
case 8: {
|
||||
// calculate any momentum change going on
|
||||
auto sc=slot->speedCode;
|
||||
if (slot->targetSpeed!=sc) {
|
||||
@@ -923,34 +946,39 @@ bool DCC::issueReminder(LOCO * slot) {
|
||||
// DIAG(F("Reminder %d speed %d"),loco,slot->speedCode);
|
||||
setThrottle2(loco, sc);
|
||||
}
|
||||
break;
|
||||
return true; // reminder sent
|
||||
case 1: // remind function group 1 (F0-F4)
|
||||
if (flags & FN_GROUP_1)
|
||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
if (flags & FN_GROUP_1) {
|
||||
setFunctionInternal(loco,1,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 2: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2)
|
||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
case 3: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2) {
|
||||
setFunctionInternal(loco,2,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 3: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3)
|
||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
case 5: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3) {
|
||||
setFunctionInternal(loco,3,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
case 4: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4)
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
||||
case 7: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4) {
|
||||
setFunctionInternal(loco,4,222, ((functions>>13)& 0xFF));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 5: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5)
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
||||
case 9: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5) {
|
||||
setFunctionInternal(loco,5,223, ((functions>>21)& 0xFF));
|
||||
return true; // reminder sent
|
||||
}
|
||||
break;
|
||||
}
|
||||
loopStatus++;
|
||||
// if we reach status 6 then this loco is done so
|
||||
// reset status to 0 for next loco and return true so caller
|
||||
// moves on to next loco.
|
||||
if (loopStatus>5) loopStatus=0;
|
||||
return loopStatus==0;
|
||||
return false; // no reminder sent
|
||||
}
|
||||
|
||||
|
||||
@@ -1051,6 +1079,9 @@ void DCC::displayCabList(Print * stream) {
|
||||
}
|
||||
|
||||
void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
|
||||
// avoid unused warnings when EXRAIL not active
|
||||
(void)loco; (void)blockid; (void)exclusive;
|
||||
|
||||
// update block loco is in, tell exrail leaving old block, and entering new.
|
||||
|
||||
// NOTE: The loco table scanning is really inefficient and needs rewriting
|
||||
@@ -1079,7 +1110,8 @@ void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
|
||||
}
|
||||
|
||||
void DCC::clearBlock(uint16_t blockid) {
|
||||
// Railcom reports block empty... tell Exrail about all leavers
|
||||
(void)blockid; // avoid unused warning when EXRAIL not active
|
||||
// clear block occupied by loco, tell exrail about all leavers
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
SLOTLOOP {
|
||||
if (slot->loco==0) break; // no more locos
|
||||
|
2
DCC.h
2
DCC.h
@@ -130,7 +130,7 @@ private:
|
||||
static byte defaultMomentumA; // Accelerating
|
||||
static byte defaultMomentumD; // Accelerating
|
||||
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
||||
static void setFunctionInternal(int cab, byte fByte, byte eByte);
|
||||
static void setFunctionInternal(int cab, byte group, byte fByte, byte eByte);
|
||||
static bool issueReminder(LOCO * slot);
|
||||
static LOCO* nextLocoReminder;
|
||||
static FSH *shieldName;
|
||||
|
178
DCCDecoder.cpp
Normal file
178
DCCDecoder.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "DCCDecoder.h"
|
||||
#include "LocoTable.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
|
||||
bool DCCDecoder::parse(DCCPacket &p) {
|
||||
if (!active)
|
||||
return false;
|
||||
const byte DECODER_MOBILE = 1;
|
||||
const byte DECODER_ACCESSORY = 2;
|
||||
byte decoderType = 0; // use 0 as none
|
||||
byte *d = p.data();
|
||||
byte *instr = 0; // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
|
||||
uint16_t addr; // will be set to decoder addr (long/shor mobile or accessory)
|
||||
bool locoInfoChanged = false;
|
||||
|
||||
if (d[0] == 0B11111111) { // Idle packet
|
||||
return false;
|
||||
}
|
||||
// CRC verification here
|
||||
byte checksum = 0;
|
||||
for (byte n = 0; n < p.len(); n++)
|
||||
checksum ^= d[n];
|
||||
if (checksum) { // Result should be zero, if not it's an error!
|
||||
if (Diag::SNIFFER) {
|
||||
DIAG(F("Checksum error:"));
|
||||
p.print();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Serial.print("< ");
|
||||
for(int n=0; n<8; n++) {
|
||||
Serial.print(d[0]&(1<<n)?"1":"0");
|
||||
}
|
||||
Serial.println(" >");
|
||||
*/
|
||||
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
|
||||
decoderType = DECODER_MOBILE;
|
||||
instr = d+1;
|
||||
addr = d[0];
|
||||
} else {
|
||||
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
|
||||
decoderType = DECODER_MOBILE;
|
||||
instr = d+2;
|
||||
addr = 256 * (d[0] & 0B00111111) + d[1];
|
||||
} else { // bit7 == 1 and bit 6 == 0
|
||||
decoderType = DECODER_ACCESSORY;
|
||||
instr = d+1;
|
||||
addr = d[0] & 0B00111111;
|
||||
}
|
||||
}
|
||||
if (decoderType == DECODER_MOBILE) {
|
||||
switch (instr[0] & 0xE0) {
|
||||
case 0x20: // 001x-xxxx Extended commands
|
||||
if (instr[0] == 0B00111111) { // 128 speed steps
|
||||
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[1])) == true) {
|
||||
byte speed = instr[1] & 0B01111111;
|
||||
byte direction = instr[1] & 0B10000000;
|
||||
DCC::setThrottle(addr, speed, direction);
|
||||
//DIAG(F("UPDATE"));
|
||||
// send speed change to DCCEX here
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
|
||||
case 0x60: // 011x-xxxx
|
||||
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[0] & 0B00111111)) == true) {
|
||||
byte speed = instr[0] & 0B00001111; // first only look at 4 bits
|
||||
if (speed > 1) { // neither stop nor emergency stop, recalculate speed
|
||||
speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
|
||||
speed = (speed - 3) * 9/2;
|
||||
}
|
||||
byte direction = instr[0] & 0B00100000;
|
||||
DCC::setThrottle(addr, speed, direction);
|
||||
}
|
||||
break;
|
||||
case 0x80: // 100x-xxxx Function group 1
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 1)) == true) {
|
||||
byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
|
||||
DCCEXParser::funcmap(addr, normalized, 0, 4);
|
||||
}
|
||||
break;
|
||||
case 0xA0: // 101x-xxxx Function group 3 and 2
|
||||
{
|
||||
byte low, high;
|
||||
if (bitRead(instr[0], 4)) {
|
||||
low = 5;
|
||||
high = 8;
|
||||
} else {
|
||||
low = 9;
|
||||
high = 12;
|
||||
}
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], low)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[0], low, high);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
|
||||
switch (instr[0] & 0B00011111) {
|
||||
case 0B00011110: // F13-F20 Function Control
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 13)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 13, 20);
|
||||
}
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 17)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 13, 20);
|
||||
}
|
||||
break;
|
||||
case 0B00011111: // F21-F28 Function Control
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1], 21)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 21, 28);
|
||||
} // updateFunc handles only the 4 low bits as that is the most common case
|
||||
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1]>>4, 25)) == true) {
|
||||
DCCEXParser::funcmap(addr, instr[1], 21, 28);
|
||||
}
|
||||
break;
|
||||
/* do that later
|
||||
case 0B00011000: // F29-F36 Function Control
|
||||
break;
|
||||
case 0B00011001: // F37-F44 Function Control
|
||||
break;
|
||||
case 0B00011010: // F45-F52 Function Control
|
||||
break;
|
||||
case 0B00011011: // F53-F60 Function Control
|
||||
break;
|
||||
case 0B00011100: // F61-F68 Function Control
|
||||
break;
|
||||
*/
|
||||
}
|
||||
break;
|
||||
case 0xE0: // 111x-xxxx Config vars
|
||||
break;
|
||||
}
|
||||
return locoInfoChanged;
|
||||
}
|
||||
if (decoderType == DECODER_ACCESSORY) {
|
||||
if (instr[0] & 0B10000000) { // Basic Accessory
|
||||
addr = (((~instr[0]) & 0B01110000) << 2) + addr;
|
||||
byte port = (instr[0] & 0B00000110) >> 1;
|
||||
byte activate = (instr[0] & 0B00001000) >> 3;
|
||||
byte coil = (instr[0] & 0B00000001);
|
||||
locoInfoChanged = true;
|
||||
//(void)addr; (void)port; (void)coil; (void)activate;
|
||||
//DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
|
||||
DCC::setAccessory(addr, port, coil, activate);
|
||||
} else { // Accessory Extended NMRA spec, do we need to decode this?
|
||||
/*
|
||||
addr = (addr << 5) +
|
||||
((instr[0] & 0B01110000) >> 2) +
|
||||
((instr[0] & 0B00000110) >> 1);
|
||||
*/
|
||||
}
|
||||
return locoInfoChanged;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // ARDUINO_ARCH_ESP32
|
30
DCCDecoder.h
Normal file
30
DCCDecoder.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <Arduino.h>
|
||||
#include "DCCPacket.h"
|
||||
|
||||
class DCCDecoder {
|
||||
public:
|
||||
static bool parse(DCCPacket &p);
|
||||
static inline void onoff(bool on) {active = on;};
|
||||
private:
|
||||
static bool active;
|
||||
};
|
||||
#endif // ARDUINO_ARCH_ESP32
|
@@ -2,8 +2,8 @@
|
||||
* © 2022 Paul M Antoine
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021-2024 Herb Morton
|
||||
* © 2020-2023 Harald Barth
|
||||
* © 2021-2025 Herb Morton
|
||||
* © 2020-2025 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2025 Chris Harlow
|
||||
@@ -122,6 +122,7 @@ Once a new OPCODE is decided upon, update this list.
|
||||
#include "Stash.h"
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "WifiESP32.h"
|
||||
#include "DCCDecoder.h"
|
||||
#endif
|
||||
|
||||
// This macro can't be created easily as a portable function because the
|
||||
@@ -712,6 +713,14 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
case 'C': // CONFIG <C [params]>
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
// currently this only works on ESP32
|
||||
if (p[0] == "SNIFFER"_hk) { // <C SNIFFER ON|OFF>
|
||||
bool on = false;
|
||||
if (params>1 && p[1] == "ON"_hk) {
|
||||
on = true;
|
||||
}
|
||||
DCCDecoder::onoff(on);
|
||||
return;
|
||||
}
|
||||
#if defined(HAS_ENOUGH_MEMORY)
|
||||
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
|
||||
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
|
||||
@@ -796,6 +805,11 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|
||||
TrackManager::reportCurrent(stream); // <g limit...limit>
|
||||
return;
|
||||
|
||||
case "L"_hk: // <JL display row> track state and mA value on display
|
||||
if (params<3) break;
|
||||
TrackManager::reportCurrentLCD(p[1], p[2]); // Track power status
|
||||
return;
|
||||
|
||||
case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
|
||||
if (params!=1) break; // <JA>
|
||||
StringFormatter::send(stream, F("<jA>\n"));
|
||||
@@ -1121,12 +1135,7 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
|
||||
return true;
|
||||
|
||||
case 0: // <S> list sensor definitions
|
||||
if (Sensor::firstSensor == NULL)
|
||||
return false;
|
||||
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
|
||||
{
|
||||
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
|
||||
}
|
||||
Sensor::dumpAll(stream);
|
||||
return true;
|
||||
|
||||
default: // invalid number of arguments
|
||||
@@ -1263,6 +1272,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
Diag::LCN = onOff;
|
||||
return true;
|
||||
|
||||
case "SNIFFER"_hk: // <D SNIFFER ON/OFF>
|
||||
Diag::SNIFFER = onOff;
|
||||
return true;
|
||||
|
||||
case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
|
||||
Diag::WEBSOCKET = onOff;
|
||||
return true;
|
||||
|
@@ -40,6 +40,7 @@ struct DCCEXParser
|
||||
static void setCamParserFilter(FILTER_CALLBACK filter);
|
||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
|
||||
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||
|
||||
private:
|
||||
|
||||
@@ -81,7 +82,6 @@ struct DCCEXParser
|
||||
static FILTER_CALLBACK filterRMFTCallback;
|
||||
static FILTER_CALLBACK filterCamParserCallback;
|
||||
static AT_COMMAND_CALLBACK atCommandCallback;
|
||||
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
|
||||
static void sendFlashList(Print * stream,const int16_t flashList[]);
|
||||
|
||||
};
|
||||
|
83
DCCPacket.h
Normal file
83
DCCPacket.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#ifndef DCCPacket_h
|
||||
#define DCCPacket_h
|
||||
#include <strings.h>
|
||||
#include "defines.h"
|
||||
|
||||
class DCCPacket {
|
||||
public:
|
||||
DCCPacket() {
|
||||
_len = 0;
|
||||
_data = NULL;
|
||||
};
|
||||
DCCPacket(byte *d, byte l) {
|
||||
_len = l;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = d[n];
|
||||
};
|
||||
DCCPacket(const DCCPacket &old) {
|
||||
_len = old._len;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = old._data[n];
|
||||
};
|
||||
DCCPacket &operator=(const DCCPacket &rhs) {
|
||||
if (this == &rhs)
|
||||
return *this;
|
||||
delete[]_data;
|
||||
_len = rhs._len;
|
||||
_data = new byte[_len];
|
||||
for (byte n = 0; n<_len; n++)
|
||||
_data[n] = rhs._data[n];
|
||||
return *this;
|
||||
};
|
||||
~DCCPacket() {
|
||||
if (_len) {
|
||||
delete[]_data;
|
||||
_len = 0;
|
||||
_data = NULL;
|
||||
}
|
||||
};
|
||||
inline bool operator==(const DCCPacket &right) {
|
||||
if (_len != right._len)
|
||||
return false;
|
||||
if (_len == 0)
|
||||
return true;
|
||||
return (bcmp(_data, right._data, _len) == 0);
|
||||
};
|
||||
void print() {
|
||||
static const char hexchars[]="0123456789ABCDEF";
|
||||
USB_SERIAL.print(F("<* DCCPACKET "));
|
||||
for (byte n = 0; n< _len; n++) {
|
||||
USB_SERIAL.print(hexchars[_data[n]>>4]);
|
||||
USB_SERIAL.print(hexchars[_data[n] & 0x0f]);
|
||||
USB_SERIAL.print(' ');
|
||||
}
|
||||
USB_SERIAL.print(F("*>\n"));
|
||||
};
|
||||
inline byte len() {return _len;};
|
||||
inline byte *data() {return _data;};
|
||||
private:
|
||||
byte _len = 0;
|
||||
byte *_data = NULL;
|
||||
};
|
||||
#endif
|
83
DCCQueue.cpp
83
DCCQueue.cpp
@@ -62,6 +62,21 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
recycleList=p;
|
||||
}
|
||||
|
||||
void DCCQueue::remove(PendingSlot* premove) {
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=head;p;previous=p,p=p->next) {
|
||||
if (p==premove) {
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else head=p->next;
|
||||
if (p==tail) tail=previous; // if last packet, update tail
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("DCCQueue::remove slot not found"));
|
||||
|
||||
}
|
||||
|
||||
// Packet joins end of low priority queue.
|
||||
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
|
||||
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
@@ -73,19 +88,41 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) {
|
||||
if (p->locoId==loco) {
|
||||
// replace existing packet
|
||||
if (length>sizeof(p->packet)) {
|
||||
DIAG(F("DCC bad packet length=%d"),length);
|
||||
length=sizeof(p->packet); // limit to size of packet
|
||||
}
|
||||
memcpy(p->packet,packet,length);
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=repeats;
|
||||
return;
|
||||
}
|
||||
}
|
||||
highPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
|
||||
highPriorityQueue->addQueue(getSlot(SPEED_PACKET,packet,length,repeats,loco));
|
||||
}
|
||||
|
||||
// Packet replaces existing loco function packet or joins end of high priority queue.
|
||||
|
||||
void DCCQueue::scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group) {
|
||||
PendingType type=DEAD_PACKET;
|
||||
switch(group) {
|
||||
case 1: type=FUNCTION1_PACKET; break;
|
||||
case 2: type=FUNCTION2_PACKET; break;
|
||||
case 3: type=FUNCTION3_PACKET; break;
|
||||
case 4: type=FUNCTION4_PACKET; break;
|
||||
case 5: type=FUNCTION5_PACKET; break;
|
||||
default:
|
||||
DIAG(F("DCCQueue::scheduleDCCFunctionPacket invalid group %d"),group);
|
||||
return; // invalid group
|
||||
}
|
||||
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) {
|
||||
if (p->locoId==loco && p->type==type) {
|
||||
// replace existing packet for same loco and function group
|
||||
memcpy(p->packet,packet,length);
|
||||
p->packetLength=length;
|
||||
p->packetRepeat=0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
lowPriorityQueue->addQueue(getSlot(type,packet,length,0,loco));
|
||||
}
|
||||
|
||||
// ESTOP -
|
||||
// any outstanding throttle packet for this loco (all if loco=0) discarded
|
||||
@@ -98,26 +135,17 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
|
||||
// kill any existing throttle packets for this loco (or all locos if broadcast)
|
||||
// this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
|
||||
PendingSlot * previous=nullptr;
|
||||
auto p=highPriorityQueue->head;
|
||||
while(p) {
|
||||
PendingSlot * pNext=nullptr;
|
||||
for (auto p=highPriorityQueue->head;p;p=pNext) {
|
||||
pNext=p->next; // save next packet in case we recycle this one
|
||||
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
|
||||
// drop this packet from the highPriority queue
|
||||
if (previous) previous->next=p->next;
|
||||
else highPriorityQueue->head=p->next;
|
||||
|
||||
// remove this slot from the queue or it will interfere with our ESTOP
|
||||
highPriorityQueue->remove(p);
|
||||
recycle(p); // recycle this slot
|
||||
|
||||
// address next packet
|
||||
p=previous?previous->next : highPriorityQueue->head;
|
||||
}
|
||||
else {
|
||||
previous=p;
|
||||
p=p->next;
|
||||
}
|
||||
}
|
||||
// add the estop packet to the start of the queue
|
||||
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
|
||||
highPriorityQueue->jumpQueue(getSlot(SPEED_PACKET,packet,length,repeats,0));
|
||||
}
|
||||
|
||||
// Accessory coil-On Packet joins end of queue as normal.
|
||||
@@ -149,8 +177,8 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
}
|
||||
|
||||
bool DCCQueue::scheduleNextInternal() {
|
||||
PendingSlot* previous=nullptr;
|
||||
for (auto p=head;p;previous=p,p=p->next) {
|
||||
|
||||
for (auto p=head;p;p=p->next) {
|
||||
// skip over pending ACC_OFF packets which are still delayed
|
||||
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
|
||||
if (p->locoId) {
|
||||
@@ -171,9 +199,7 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
}
|
||||
|
||||
// remove this slot from the queue
|
||||
if (previous) previous->next=p->next;
|
||||
else head=p->next;
|
||||
if (!head) tail=nullptr;
|
||||
remove(p);
|
||||
|
||||
// special cases handling
|
||||
if (p->type == ACC_ON_PACKET) {
|
||||
@@ -182,11 +208,8 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
p->packet[1] &= ~0x08; // set C to 0 (gate off)
|
||||
p->startTime=millis()+p->delayOff;
|
||||
highPriorityQueue->jumpQueue(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recycle packet just consumed
|
||||
recycle(p);
|
||||
else recycle(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -209,7 +232,7 @@ uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
|
||||
bool leak=(q1+q2)!=created;
|
||||
DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
|
||||
type,length,loco,q1,q2, created);
|
||||
(int16_t)type,length,loco,q1,q2, created);
|
||||
if (leak) {
|
||||
for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
|
||||
for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
|
||||
|
15
DCCQueue.h
15
DCCQueue.h
@@ -23,7 +23,10 @@
|
||||
#include "Arduino.h"
|
||||
#include "DCCWaveform.h"
|
||||
|
||||
enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
|
||||
enum PendingType:byte {NORMAL_PACKET,
|
||||
FUNCTION1_PACKET, FUNCTION2_PACKET, FUNCTION3_PACKET, FUNCTION4_PACKET, FUNCTION5_PACKET,
|
||||
SPEED_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
|
||||
|
||||
struct PendingSlot {
|
||||
PendingSlot* next;
|
||||
PendingType type;
|
||||
@@ -40,14 +43,16 @@ enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,
|
||||
|
||||
class DCCQueue {
|
||||
public:
|
||||
|
||||
|
||||
|
||||
// Non-speed packets are queued in the main queue
|
||||
static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);
|
||||
|
||||
// Speed packets are queued in the high priority queue
|
||||
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
|
||||
|
||||
// Function group packets are queued in the low priority queue
|
||||
static void scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group);
|
||||
|
||||
// ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco
|
||||
static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco);
|
||||
|
||||
@@ -78,6 +83,6 @@ class DCCQueue {
|
||||
static void recycle(PendingSlot* p);
|
||||
void addQueue(PendingSlot * p);
|
||||
void jumpQueue(PendingSlot * p);
|
||||
|
||||
void remove(PendingSlot * p);
|
||||
};
|
||||
#endif
|
||||
#endif // DCCQueue_h
|
||||
|
@@ -203,7 +203,9 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
|
||||
}
|
||||
|
||||
void DCCTimer::DCCEXledcDetachPin(uint8_t pin) {
|
||||
DIAG(F("Clear pin %d channel"), pin);
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Clear pin %d from ledc channel"), pin);
|
||||
#endif
|
||||
pin_to_channel[pin] = 0;
|
||||
pinMatrixOutDetach(pin, false, false);
|
||||
}
|
||||
|
@@ -208,7 +208,7 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
|
||||
// dcctimer.attachInterrupt(Timer11_Handler);
|
||||
dcctimer.attachInterrupt(DCCTimer_Handler);
|
||||
dcctimer.setInterruptPriority(0, 0); // Set highest preemptive priority!
|
||||
dcctimer.setInterruptPriority(1, 0); // Set second highest preemptive priority!
|
||||
dcctimer.refresh();
|
||||
dcctimer.resume();
|
||||
|
||||
|
@@ -159,7 +159,7 @@ void DCCWaveform::interrupt2() {
|
||||
// As we get to the end of the preambles, open the reminder window.
|
||||
// This delays any reminder insertion until the last moment so
|
||||
// that the reminder doesn't block a more urgent packet.
|
||||
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<10 && remainingPreambles>1;
|
||||
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
|
||||
if (remainingPreambles==1)
|
||||
promotePendingPacket();
|
||||
|
||||
|
@@ -188,6 +188,7 @@ Display *Display::loop2(bool force) {
|
||||
#endif
|
||||
noMoreRowsToDisplay = false;
|
||||
slot = 0;
|
||||
_deviceDriver->setRowNative(slot); // Set position for display
|
||||
lastScrollTime = currentMillis;
|
||||
return NULL;
|
||||
}
|
||||
|
67
EXRAIL2.cpp
67
EXRAIL2.cpp
@@ -91,6 +91,10 @@ LookList * RMFT2::onRotateLookup=NULL;
|
||||
LookList * RMFT2::onOverloadLookup=NULL;
|
||||
LookList * RMFT2::onBlockEnterLookup=NULL;
|
||||
LookList * RMFT2::onBlockExitLookup=NULL;
|
||||
#ifdef BOOSTER_INPUT
|
||||
LookList * RMFT2::onRailSyncOnLookup=NULL;
|
||||
LookList * RMFT2::onRailSyncOffLookup=NULL;
|
||||
#endif
|
||||
byte * RMFT2::routeStateArray=nullptr;
|
||||
const FSH * * RMFT2::routeCaptionArray=nullptr;
|
||||
|
||||
@@ -211,6 +215,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);
|
||||
onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);
|
||||
}
|
||||
#ifdef BOOSTER_INPUT
|
||||
onRailSyncOnLookup=LookListLoader(OPCODE_ONRAILSYNCON);
|
||||
onRailSyncOffLookup=LookListLoader(OPCODE_ONRAILSYNCOFF);
|
||||
#endif
|
||||
|
||||
// onLCCLookup is not the same so not loaded here.
|
||||
|
||||
@@ -263,6 +271,8 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
case OPCODE_ATLT:
|
||||
case OPCODE_IFGTE:
|
||||
case OPCODE_IFLT:
|
||||
case OPCODE_IFBITMAP_ALL:
|
||||
case OPCODE_IFBITMAP_ANY:
|
||||
case OPCODE_DRIVE: {
|
||||
DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand);
|
||||
IODevice::configureAnalogIn((VPIN)operand);
|
||||
@@ -273,6 +283,10 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,true );
|
||||
break;
|
||||
case OPCODE_ONBITMAP:
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,true, true );
|
||||
break;
|
||||
case OPCODE_ONBUTTON:
|
||||
if (compileFeatures & FEATURE_SENSOR)
|
||||
new EXRAILSensor(operand,progCounter+3,false );
|
||||
@@ -762,6 +776,14 @@ void RMFT2::loop2() {
|
||||
case OPCODE_IFLT: // do next operand if sensor< value
|
||||
skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_IFBITMAP_ALL: // do next operand if sensor & mask == mask
|
||||
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) != getOperand(1);
|
||||
break;
|
||||
|
||||
case OPCODE_IFBITMAP_ANY: // do next operand if sensor & mask !=0
|
||||
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) == 0;
|
||||
break;
|
||||
|
||||
case OPCODE_IFLOCO: // do if the loco is the active one
|
||||
skipIf=loco!=(uint16_t)operand; // bad luck if someone enters negative loco numbers into EXRAIL
|
||||
@@ -1094,7 +1116,27 @@ void RMFT2::loop2() {
|
||||
case OPCODE_SEQUENCE:
|
||||
//if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
|
||||
break;
|
||||
|
||||
|
||||
case OPCODE_BITMAP_INC:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand)+1);
|
||||
break;
|
||||
case OPCODE_BITMAP_DEC:
|
||||
{ int newval=IODevice::readAnalogue(operand)-1;
|
||||
if (newval<0) newval=0;
|
||||
IODevice::writeAnalogue(operand,newval);
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_BITMAP_AND:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) & getOperand(1));
|
||||
break;
|
||||
case OPCODE_BITMAP_OR:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) | getOperand(1));
|
||||
break;
|
||||
case OPCODE_BITMAP_XOR:
|
||||
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) ^ getOperand(1));
|
||||
break;
|
||||
|
||||
case OPCODE_AUTOSTART: // Handled only during begin process
|
||||
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte.
|
||||
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
|
||||
@@ -1114,6 +1156,7 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONTIME:
|
||||
case OPCODE_ONBUTTON:
|
||||
case OPCODE_ONSENSOR:
|
||||
case OPCODE_ONBITMAP:
|
||||
#ifndef IO_NO_HAL
|
||||
case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime
|
||||
case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime
|
||||
@@ -1123,6 +1166,10 @@ void RMFT2::loop2() {
|
||||
case OPCODE_ONOVERLOAD:
|
||||
case OPCODE_ONBLOCKENTER:
|
||||
case OPCODE_ONBLOCKEXIT:
|
||||
#ifdef BOOSTER_INPUT
|
||||
case OPCODE_ONRAILSYNCON:
|
||||
case OPCODE_ONRAILSYNCOFF:
|
||||
#endif
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1352,7 +1399,19 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
|
||||
onOverloadLookup->handleEvent(F("POWER"),track);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef BOOSTER_INPUT
|
||||
void RMFT2::railsyncEvent(bool on) {
|
||||
if (Diag::CMD)
|
||||
DIAG(F("railsyncEvent : %d"), on);
|
||||
if (on) {
|
||||
if (onRailSyncOnLookup)
|
||||
onRailSyncOnLookup->handleEvent(F("RAILSYNCON"), 0);
|
||||
} else {
|
||||
if (onRailSyncOffLookup)
|
||||
onRailSyncOffLookup->handleEvent(F("RAILSYNCOFF"), 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// This function is used when setting pins so that a SET or RESET
|
||||
// will cause any blink task on that pin to terminate.
|
||||
// It will be compiled out of existence if no BLINK feature is used.
|
||||
@@ -1498,7 +1557,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::manageRouteState(uint16_t id, byte state) {
|
||||
void RMFT2::manageRouteState(int16_t id, byte state) {
|
||||
if (compileFeatures && FEATURE_ROUTESTATE) {
|
||||
// Route state must be maintained for when new throttles connect.
|
||||
// locate route id in the Routes lookup
|
||||
@@ -1510,7 +1569,7 @@ void RMFT2::manageRouteState(uint16_t id, byte state) {
|
||||
CommandDistributor::broadcastRouteState(id,state);
|
||||
}
|
||||
}
|
||||
void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) {
|
||||
void RMFT2::manageRouteCaption(int16_t id,const FSH* caption) {
|
||||
if (compileFeatures && FEATURE_ROUTESTATE) {
|
||||
// Route state must be maintained for when new throttles connect.
|
||||
// locate route id in the Routes lookup
|
||||
|
18
EXRAIL2.h
18
EXRAIL2.h
@@ -74,15 +74,17 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_ACON, OPCODE_ACOF,
|
||||
OPCODE_ONACON, OPCODE_ONACOF,
|
||||
OPCODE_ONOVERLOAD,
|
||||
OPCODE_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
|
||||
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
|
||||
OPCODE_ROUTE_DISABLED,
|
||||
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
|
||||
OPCODE_CLEAR_ANY_STASH,
|
||||
OPCODE_ONBUTTON,OPCODE_ONSENSOR,
|
||||
OPCODE_ONBUTTON,OPCODE_ONSENSOR,OPCODE_ONVP_SENSOR,
|
||||
OPCODE_NEOPIXEL,
|
||||
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
|
||||
OPCODE_ESTOPALL,OPCODE_XPOM,
|
||||
// OPcodes below this point are skip-nesting IF operations
|
||||
OPCODE_BITMAP_AND,OPCODE_BITMAP_OR,OPCODE_BITMAP_XOR,OPCODE_BITMAP_INC,OPCODE_BITMAP_DEC,OPCODE_ONBITMAP,
|
||||
// OPcodes below this point are skip-nesting IF operations
|
||||
// placed here so that they may be skipped as a group
|
||||
// see skipIfBlock()
|
||||
IF_TYPE_OPCODES, // do not move this...
|
||||
@@ -96,6 +98,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
|
||||
OPCODE_IFLOCO,
|
||||
OPCODE_IFTTPOSITION,
|
||||
OPCODE_IFSTASH,
|
||||
OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,
|
||||
};
|
||||
|
||||
// Ensure thrunge_lcd is put last as there may be more than one display,
|
||||
@@ -193,6 +196,9 @@ class LookList {
|
||||
static void clockEvent(int16_t clocktime, bool change);
|
||||
static void rotateEvent(int16_t id, bool change);
|
||||
static void powerEvent(int16_t track, bool overload);
|
||||
#ifdef BOOSTER_INPUT
|
||||
static void railsyncEvent(bool on);
|
||||
#endif
|
||||
static void blockEvent(int16_t block, int16_t loco, bool entering);
|
||||
static bool signalAspectEvent(int16_t address, byte aspect );
|
||||
// Throttle Info Access functions built by exrail macros
|
||||
@@ -264,13 +270,17 @@ private:
|
||||
static LookList * onOverloadLookup;
|
||||
static LookList * onBlockEnterLookup;
|
||||
static LookList * onBlockExitLookup;
|
||||
#ifdef BOOSTER_INPUT
|
||||
static LookList * onRailSyncOnLookup;
|
||||
static LookList * onRailSyncOffLookup;
|
||||
#endif
|
||||
|
||||
|
||||
static const int countLCCLookup;
|
||||
static int onLCCLookup[];
|
||||
static const byte compileFeatures;
|
||||
static void manageRouteState(uint16_t id, byte state);
|
||||
static void manageRouteCaption(uint16_t id, const FSH* caption);
|
||||
static void manageRouteState(int16_t id, byte state);
|
||||
static void manageRouteCaption(int16_t id, const FSH* caption);
|
||||
static byte * routeStateArray;
|
||||
static const FSH ** routeCaptionArray;
|
||||
static int16_t * stashArray;
|
||||
|
@@ -94,8 +94,11 @@
|
||||
#undef IFTIMEOUT
|
||||
#undef IFTTPOSITION
|
||||
#undef IFRE
|
||||
#undef IFBITMAP_ALL
|
||||
#undef IFBITMAP_ANY
|
||||
#undef INVERT_DIRECTION
|
||||
#undef JMRI_SENSOR
|
||||
#undef JMRI_SENSOR_NOPULLUP
|
||||
#undef JOIN
|
||||
#undef KILLALL
|
||||
#undef LATCH
|
||||
@@ -127,12 +130,15 @@
|
||||
#undef ONCLOCKTIME
|
||||
#undef ONCLOCKMINS
|
||||
#undef ONOVERLOAD
|
||||
#undef ONRAILSYNCON
|
||||
#undef ONRAILSYNCOFF
|
||||
#undef ONGREEN
|
||||
#undef ONRED
|
||||
#undef ONROTATE
|
||||
#undef ONBUTTON
|
||||
#undef ONSENSOR
|
||||
#undef ONTHROW
|
||||
#undef ONBITMAP
|
||||
#undef ONCHANGE
|
||||
#undef PARSE
|
||||
#undef PAUSE
|
||||
@@ -195,6 +201,11 @@
|
||||
#undef VIRTUAL_TURNOUT
|
||||
#undef WAITFOR
|
||||
#ifndef IO_NO_HAL
|
||||
#undef BITMAP_AND
|
||||
#undef BITMAP_OR
|
||||
#undef BITMAP_XOR
|
||||
#undef BITMAP_INC
|
||||
#undef BITMAP_DEC
|
||||
#undef WAITFORTT
|
||||
#endif
|
||||
#undef WITHROTTLE
|
||||
@@ -651,6 +662,22 @@
|
||||
* @param value
|
||||
*/
|
||||
#define IFRE(vpin,value)
|
||||
/**
|
||||
* @def IFBITMAP_ALL(vpin,mask)
|
||||
* @briaf Checks if (vpin pseudo-analog value & mask) == mask.
|
||||
* @see IF
|
||||
* @param vpin
|
||||
* @param mask Binary mask applied to vpin value
|
||||
*/
|
||||
#define IFBITMAP_ALL(vpin,mask)
|
||||
/**
|
||||
* @def IFBITMAP_ANY(vpin,mask)
|
||||
* @briaf Checks if vpin pseudo-analog value & mask is non zero
|
||||
* @see IF
|
||||
* @param vpin
|
||||
* @param mask Binary mask applied to vpin value
|
||||
*/
|
||||
#define IFBITMAP_ANY(vpin,mask)
|
||||
/**
|
||||
* @def INVERT_DIRECTION
|
||||
* @brief Marks current task so that FWD and REV commands are inverted.
|
||||
@@ -658,11 +685,18 @@
|
||||
#define INVERT_DIRECTION
|
||||
/**
|
||||
* @def JMRI_SENSOR(vpin,count...)
|
||||
* @brief Defines multiple JMRI <s> type sensor feedback definitions each with id matching vpin
|
||||
* @brief Defines multiple JMRI <s> type sensor feedback definitions each with id matching vpin and INPUT_PULLUP
|
||||
* @param vpin first vpin number
|
||||
* @param count... Number of consecutine VPINS for which to create JMRI sensor feedbacks. Default 1.
|
||||
*/
|
||||
#define JMRI_SENSOR(vpin,count...)
|
||||
/**
|
||||
* @def JMRI_SENSOR_NOPULLUP(vpin,count...)
|
||||
* @brief Defines multiple JMRI <s> type sensor feedback definitions each with id matching vpin
|
||||
* @param vpin first vpin number
|
||||
* @param count... Number of consecutine VPINS for which to create JMRI sensor feedbacks. Default 1.
|
||||
*/
|
||||
#define JMRI_SENSOR_NOPULLUP(vpin,count...)
|
||||
/**
|
||||
* @def JOIN
|
||||
* @brief Switches PROG track to receive MAIN track DCC packets. (Drive on PROG track)
|
||||
@@ -837,6 +871,16 @@
|
||||
* @param track_id A..H
|
||||
*/
|
||||
#define ONOVERLOAD(track_id)
|
||||
/**
|
||||
* @def ONRAILSYNCON
|
||||
* @brief Start task here when the railsync (booster) input port get a valid DCC signal
|
||||
*/
|
||||
#define ONRAILSYNCON
|
||||
/**
|
||||
* @def ONRAILSYNCOFF
|
||||
* @brief Start task here when the railsync (booster) input port does not get a valid DCC signal any more
|
||||
*/
|
||||
#define ONRAILSYNCOFF
|
||||
/**
|
||||
* @def ONDEACTIVATE(addr,subaddr)
|
||||
* @brief Start task here when DCC deactivate packet sent
|
||||
@@ -899,6 +943,12 @@
|
||||
* @param vpin
|
||||
*/
|
||||
#define ONSENSOR(vpin)
|
||||
/**
|
||||
* @def ONBITMAP(vpin)
|
||||
* @brief Start task here when bitmap sensor changes state (debounced)
|
||||
* @param vpin
|
||||
*/
|
||||
#define ONBITMAP(vpin)
|
||||
/**
|
||||
* @def ONBUTTON(vpin)
|
||||
* @brief Start task here when sensor changes HIGH to LOW.
|
||||
@@ -1322,9 +1372,42 @@
|
||||
* @param description... quoted text or HIDDEN
|
||||
*/
|
||||
#define VIRTUAL_TURNOUT(id,description...)
|
||||
/**
|
||||
* @def BITMAP_AND(vpin1,mask)
|
||||
* @brief Performs a bitwise AND operation on the given vpin analog value and mask.
|
||||
* @param vpin1
|
||||
* @param mask Binary mask to be ANDed with vpin1 value
|
||||
*/
|
||||
#define BITMAP_AND(vpin1,mask)
|
||||
/**
|
||||
* @def BITMAP_INC(vpin)
|
||||
* @brief Increments poesudo analog value by 1
|
||||
* @param vpin
|
||||
*/
|
||||
#define BITMAP_INC(vpin)
|
||||
/**
|
||||
* @def BITMAP_DEC(vpin)
|
||||
* @brief Decrements poesudo analog value by 1 (to zero)
|
||||
* @param vpin
|
||||
*/
|
||||
#define BITMAP_DEC(vpin)
|
||||
/**
|
||||
* @def BITMAP_OR(vpin1,mask)
|
||||
* @brief Performs a bitwise OR operation on the given vpin analog value and mask.
|
||||
* @param vpin1
|
||||
* @param mask Binary mask to be ORed with vpin1 value
|
||||
*/
|
||||
#define BITMAP_OR(vpin1,mask)
|
||||
/**
|
||||
* @def BITMAP_XOR(vpin1,mask)
|
||||
* @brief Performs a bitwise XOR operation on the given vpin analog value and mask.
|
||||
* @param vpin1
|
||||
* @param mask Binary mask to be XORed with vpin1 value
|
||||
*/
|
||||
#define BITMAP_XOR(vpin1,mask)
|
||||
/**
|
||||
* @def WAITFOR(vpin)
|
||||
* @brief WAits for completion of servo movement
|
||||
* @brief Waits for completion of servo movement
|
||||
* @param vpin
|
||||
*/
|
||||
#define WAITFOR(pin)
|
||||
|
@@ -213,6 +213,27 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' '
|
||||
);
|
||||
auto progCounter=task->progCounter; // name to satisfy macros below
|
||||
auto operand=task->getOperand(progCounter,0);
|
||||
switch(GET_OPCODE) {
|
||||
case OPCODE_RESERVE:
|
||||
StringFormatter::send(stream,F(" WAIT RESERVE %d"),operand);
|
||||
break;
|
||||
case OPCODE_AT:
|
||||
case OPCODE_ATTIMEOUT2:
|
||||
case OPCODE_AFTER:
|
||||
case OPCODE_ATGTE:
|
||||
case OPCODE_ATLT:
|
||||
StringFormatter::send(stream,F(" WAIT AT/AFTER %d"),operand);
|
||||
break;
|
||||
case OPCODE_DELAY:
|
||||
case OPCODE_DELAYMINS:
|
||||
case OPCODE_DELAYMS:
|
||||
case OPCODE_RANDWAIT:
|
||||
StringFormatter::send(stream,F(" WAIT DELAY"));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
|
@@ -95,23 +95,56 @@
|
||||
#define STEALTH_GLOBAL(code...) code
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Pass 1h Implements HAL macro by creating exrailHalSetup function
|
||||
// Pass 1h Implements HAL macro by creating exrailHalSetup1 function
|
||||
// Also allows creating EXTurntable object
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef HAL
|
||||
#define HAL(haltype,params...) haltype::create(params);
|
||||
#undef HAL_IGNORE_DEFAULTS
|
||||
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
|
||||
#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 exrailHalSetup1() {
|
||||
bool ignore_defaults=false;
|
||||
#include "myAutomation.h"
|
||||
return ignore_defaults;
|
||||
}
|
||||
|
||||
// Pass 1s Implements servos by creating exrailHalSetup2
|
||||
// TODO Turnout and turntable creation should be moved to here instead of
|
||||
// the first pass from the opcode table.
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef JMRI_SENSOR
|
||||
#define JMRI_SENSOR(vpin,count...) \
|
||||
{ \
|
||||
const int npins=#count[0]? count+0:1; \
|
||||
static byte state_map[(npins+7)/8]; \
|
||||
SensorGroup::doSensorGroup(vpin,npins,state_map,action,&USB_SERIAL,true); \
|
||||
}
|
||||
#undef JMRI_SENSOR_NOPULLUP
|
||||
#define JMRI_SENSOR_NOPULLUP(vpin,count...) \
|
||||
{ \
|
||||
const int npins=#count[0]? count+0:1; \
|
||||
static byte state_map[(npins+7)/8]; \
|
||||
SensorGroup::doSensorGroup(vpin,npins,state_map,action,&USB_SERIAL,false); \
|
||||
}
|
||||
|
||||
void SensorGroup::doExrailSensorGroup(GroupProcess action, Print * stream) {
|
||||
(void) action; // suppress unused warnings if no groups
|
||||
(void) stream;
|
||||
#include "myAutomation.h"
|
||||
}
|
||||
|
||||
// Pass 1s Implements servos by creating exrailHalSetup2
|
||||
// TODO Turnout and turntable creation should be moved to here instead of
|
||||
// the first pass from the opcode table.
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef CONFIGURE_SERVO
|
||||
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile) IODevice::configureServo(vpin,pos1,pos2,PCA9685::profile);
|
||||
void exrailHalSetup2() {
|
||||
#include "myAutomation.h"
|
||||
// pullup any group sensors
|
||||
SensorGroup::prepareAll();
|
||||
}
|
||||
|
||||
// Pass 1c detect compile time featurtes
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef SIGNAL
|
||||
@@ -160,6 +193,8 @@ bool exrailHalSetup() {
|
||||
#define ONBUTTON(vpin) | FEATURE_SENSOR
|
||||
#undef ONSENSOR
|
||||
#define ONSENSOR(vpin) | FEATURE_SENSOR
|
||||
#undef ONBITMAP
|
||||
#define ONBITMAP(vpin) | FEATURE_SENSOR
|
||||
#undef ONBLOCKENTER
|
||||
#define ONBLOCKENTER(blockid) | FEATURE_BLOCK
|
||||
#undef ONBLOCKEXIT
|
||||
@@ -481,8 +516,11 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#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 IFBITMAP_ALL(vpin,mask) OPCODE_IFBITMAP_ALL,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define IFBITMAP_ANY(vpin,mask) OPCODE_IFBITMAP_ANY,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
|
||||
#define JMRI_SENSOR(vpin,count...)
|
||||
#define JMRI_SENSOR_NOPULLUP(vpin,count...)
|
||||
#define JOIN OPCODE_JOIN,0,0,
|
||||
#define KILLALL OPCODE_KILLALL,0,0,
|
||||
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
|
||||
@@ -523,6 +561,8 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
|
||||
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
|
||||
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
|
||||
#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
|
||||
#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
|
||||
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
|
||||
@@ -533,6 +573,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
|
||||
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
|
||||
#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),
|
||||
#define ONBITMAP(sensor_id) OPCODE_ONBITMAP,V(sensor_id),
|
||||
#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,V(sensor_id),
|
||||
#define PAUSE OPCODE_PAUSE,0,0,
|
||||
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
|
||||
@@ -595,6 +636,11 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
|
||||
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
|
||||
#define VIRTUAL_SIGNAL(id)
|
||||
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
|
||||
#define BITMAP_AND(vpin,mask) OPCODE_BITMAP_AND,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define BITMAP_INC(vpin) OPCODE_BITMAP_INC,V(vpin),
|
||||
#define BITMAP_DEC(vpin) OPCODE_BITMAP_DEC,V(vpin),
|
||||
#define BITMAP_OR(vpin,mask) OPCODE_BITMAP_OR,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define BITMAP_XOR(vpin,mask) OPCODE_BITMAP_XOR,V(vpin),OPCODE_PAD,V(mask),
|
||||
#define WITHROTTLE(msg) PRINT(msg)
|
||||
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
|
||||
#ifndef IO_NO_HAL
|
||||
|
@@ -59,7 +59,7 @@ void EXRAILSensor::checkAll() {
|
||||
|
||||
bool EXRAILSensor::check() {
|
||||
// check for debounced change in this sensor
|
||||
inputState = RMFT2::readSensor(pin);
|
||||
inputState = useAnalog?IODevice::readAnalogue(pin):RMFT2::readSensor(pin);
|
||||
|
||||
// Check if changed since last time, and process changes.
|
||||
if (inputState == active) {// no change
|
||||
@@ -83,18 +83,18 @@ bool EXRAILSensor::check() {
|
||||
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);
|
||||
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog) {
|
||||
|
||||
nextSensor = firstSensor;
|
||||
firstSensor = this;
|
||||
|
||||
pin=_pin;
|
||||
progCounter=_progCounter;
|
||||
onChange=_onChange;
|
||||
useAnalog=_useAnalog;
|
||||
|
||||
IODevice::configureInput(pin, true);
|
||||
active = IODevice::read(pin);
|
||||
active = useAnalog?IODevice::readAnalogue(pin): IODevice::read(pin);
|
||||
inputState = active;
|
||||
latchDelay = minReadCount;
|
||||
}
|
||||
|
@@ -29,7 +29,8 @@ class EXRAILSensor {
|
||||
public:
|
||||
static void checkAll();
|
||||
|
||||
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange);
|
||||
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog=false);
|
||||
|
||||
bool check();
|
||||
|
||||
private:
|
||||
@@ -42,9 +43,10 @@ class EXRAILSensor {
|
||||
EXRAILSensor* nextSensor;
|
||||
VPIN pin;
|
||||
int progCounter;
|
||||
bool active;
|
||||
bool inputState;
|
||||
uint16_t active;
|
||||
uint16_t inputState;
|
||||
bool onChange;
|
||||
bool useAnalog;
|
||||
byte latchDelay;
|
||||
};
|
||||
#endif
|
||||
|
@@ -1 +1 @@
|
||||
#define GITHUB_SHA "devel-202503250850Z"
|
||||
#define GITHUB_SHA "devel-202507181124Z"
|
||||
|
@@ -46,6 +46,8 @@
|
||||
|
||||
// Helper function for listing device types
|
||||
static const FSH * guessI2CDeviceType(uint8_t address) {
|
||||
if (address >= 0x10 && address <= 0x17)
|
||||
return F("EX-SensorCAM");
|
||||
if (address == 0x1A)
|
||||
// 0x09-0x18 selectable, but for now handle the default
|
||||
return F("Piicodev 865/915MHz Transceiver");
|
||||
|
@@ -37,21 +37,42 @@
|
||||
// it can replace use of noInterrupts/interrupts in other parts of DCC-EX.
|
||||
//
|
||||
static inline uint8_t _deferInterrupts(void) {
|
||||
#if defined(ARDUINO_ARCH_STM32)
|
||||
NVIC_DisableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_DisableIRQ(I2C1_ER_IRQn);
|
||||
#else
|
||||
noInterrupts();
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
static inline void _conditionalEnableInterrupts(bool *wasEnabled) {
|
||||
#if defined(ARDUINO_ARCH_STM32)
|
||||
(void)wasEnabled;
|
||||
NVIC_EnableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_EnableIRQ(I2C1_ER_IRQn);
|
||||
#else
|
||||
if (*wasEnabled) interrupts();
|
||||
#endif
|
||||
}
|
||||
#define ATOMIC_BLOCK(x) \
|
||||
for (bool _int_saved __attribute__((__cleanup__(_conditionalEnableInterrupts))) \
|
||||
=_getInterruptState(),_ToDo=_deferInterrupts(); _ToDo; _ToDo=0)
|
||||
|
||||
// The construct of
|
||||
// "variable __attribute__((__cleanup__(func)))"
|
||||
// calls the func with *variable when variable goes out of scope
|
||||
|
||||
#if defined(__AVR__) // Nano, Uno, Mega2580, NanoEvery, etc.
|
||||
static inline bool _getInterruptState(void) {
|
||||
return bitRead(SREG, SREG_I); // true if enabled, false if disabled
|
||||
}
|
||||
#elif defined(__arm__) // STM32, SAMD, Teensy
|
||||
#elif defined(ARDUINO_ARCH_STM32)
|
||||
static inline bool _getInterruptState( void ) {
|
||||
// as we do ony mess with the I2C interrupts in the STM32 case,
|
||||
// we do not care about their previous state
|
||||
return true;
|
||||
}
|
||||
#elif defined(__arm__) // SAMD, Teensy
|
||||
static inline bool _getInterruptState( void ) {
|
||||
uint32_t reg;
|
||||
__asm__ __volatile__ ("MRS %0, primask" : "=r" (reg) );
|
||||
|
@@ -211,9 +211,19 @@ void I2CManagerClass::I2C_init()
|
||||
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
// Setting NVIC
|
||||
NVIC_SetPriority(I2C1_EV_IRQn, 1); // Match default priorities
|
||||
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 4 means that we have all bits for preemptive grouping
|
||||
// prio scheme:
|
||||
// systick : 0
|
||||
// waveform timer : 1
|
||||
// i2c : 2
|
||||
// one must call NVIC_EncodePriority() to bitshift the priorities
|
||||
// according to the active priority grouping and then use that
|
||||
// value as argument to NVIC_SetPriority().
|
||||
NVIC_SetPriority(I2C1_EV_IRQn,
|
||||
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
|
||||
NVIC_EnableIRQ(I2C1_EV_IRQn);
|
||||
NVIC_SetPriority(I2C1_ER_IRQn, 1); // Match default priorities
|
||||
NVIC_SetPriority(I2C1_ER_IRQn,
|
||||
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
|
||||
NVIC_EnableIRQ(I2C1_ER_IRQn);
|
||||
|
||||
// CR2 Interrupt Settings
|
||||
@@ -305,6 +315,7 @@ void I2CManagerClass::I2C_close() {
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_handleInterrupt() {
|
||||
volatile uint16_t temp_sr1, temp_sr2;
|
||||
(void) temp_sr2; // only used as target for reads
|
||||
|
||||
temp_sr1 = s->SR1;
|
||||
|
||||
|
62
IODevice.cpp
62
IODevice.cpp
@@ -33,7 +33,9 @@
|
||||
|
||||
// Link to halSetup function. If not defined, the function reference will be NULL.
|
||||
extern __attribute__((weak)) void halSetup();
|
||||
extern __attribute__((weak)) bool exrailHalSetup();
|
||||
extern __attribute__((weak)) bool exrailHalSetup1();
|
||||
extern __attribute__((weak)) bool exrailHalSetup2();
|
||||
|
||||
|
||||
//==================================================================================================================
|
||||
// Static methods
|
||||
@@ -59,32 +61,46 @@ void IODevice::begin() {
|
||||
if (halSetup)
|
||||
halSetup();
|
||||
|
||||
// include any HAL devices defined in exrail.
|
||||
// Include any HAL devices defined in exrail.
|
||||
// The first pass call only creates HAL devices,
|
||||
// the second pass will apply servo settings etc which can only be
|
||||
// done after all devices (including the defaults) are created.
|
||||
// If exrailHalSetup1 is not defined, then it will be NULL and the call
|
||||
// will be ignored.
|
||||
// If it returns true, then the default HAL devices will not be created.
|
||||
|
||||
bool ignoreDefaults=false;
|
||||
if (exrailHalSetup)
|
||||
ignoreDefaults=exrailHalSetup();
|
||||
if (ignoreDefaults) return;
|
||||
if (exrailHalSetup1)
|
||||
ignoreDefaults=exrailHalSetup1();
|
||||
|
||||
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
|
||||
// Allocates 32 pins 100-131
|
||||
const bool silent=true; // no message if these conflict
|
||||
if (checkNoOverlap(100, 16, 0x40, silent)) {
|
||||
PCA9685::create(100, 16, 0x40);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(116, 16, 0x41, silent)) {
|
||||
PCA9685::create(116, 16, 0x41);
|
||||
}
|
||||
if (!ignoreDefaults) {
|
||||
|
||||
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
||||
// Allocates 32 pins 164-195
|
||||
if (checkNoOverlap(164, 16, 0x20, silent)) {
|
||||
MCP23017::create(164, 16, 0x20);
|
||||
}
|
||||
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
|
||||
// Allocates 32 pins 100-131
|
||||
const bool silent=true; // no message if these conflict
|
||||
if (checkNoOverlap(100, 16, 0x40, silent)) {
|
||||
PCA9685::create(100, 16, 0x40);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(180, 16, 0x21, silent)) {
|
||||
MCP23017::create(180, 16, 0x21);
|
||||
}
|
||||
if (checkNoOverlap(116, 16, 0x41, silent)) {
|
||||
PCA9685::create(116, 16, 0x41);
|
||||
}
|
||||
|
||||
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
|
||||
// Allocates 32 pins 164-195
|
||||
if (checkNoOverlap(164, 16, 0x20, silent)) {
|
||||
MCP23017::create(164, 16, 0x20);
|
||||
}
|
||||
|
||||
if (checkNoOverlap(180, 16, 0x21, silent)) {
|
||||
MCP23017::create(180, 16, 0x21);
|
||||
}
|
||||
}
|
||||
|
||||
// apply any second pass HAL setup from EXRAIL.
|
||||
// This will typically set up servo profiles, or create turnouts.
|
||||
if (exrailHalSetup2)
|
||||
exrailHalSetup2();
|
||||
}
|
||||
|
||||
// reset() function to reinitialise all devices
|
||||
|
@@ -20,19 +20,31 @@
|
||||
This is the list of HAL drivers automatically included by IODevice.h
|
||||
It has been moved here to be easier to maintain than editing IODevice.h
|
||||
*/
|
||||
#include "IO_AnalogueInputs.h"
|
||||
#include "IO_DFPlayer.h"
|
||||
#include "IO_DS1307.h"
|
||||
#include "IO_duinoNodes.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "IO_EXFastclock.h"
|
||||
#include "IO_EXIOExpander.h"
|
||||
#include "IO_EXSensorCAM.h"
|
||||
#include "IO_HALDisplay.h"
|
||||
#include "IO_HCSR04.h"
|
||||
#include "IO_I2CDFPlayer.h"
|
||||
#include "IO_I2CRailcom.h"
|
||||
#include "IO_MCP23008.h"
|
||||
#include "IO_MCP23017.h"
|
||||
#include "IO_NeoPixel.h"
|
||||
#include "IO_PCA9555.h"
|
||||
#include "IO_PCA9685pwm.h"
|
||||
#include "IO_PCF8574.h"
|
||||
#include "IO_PCF8575.h"
|
||||
#include "IO_PCA9555.h"
|
||||
#include "IO_duinoNodes.h"
|
||||
#include "IO_EXIOExpander.h"
|
||||
#include "IO_trainbrains.h"
|
||||
#include "IO_EncoderThrottle.h"
|
||||
#include "IO_RotaryEncoder.h"
|
||||
#include "IO_Servo.h"
|
||||
#include "IO_TCA8418.h"
|
||||
#include "IO_NeoPixel.h"
|
||||
#include "IO_TM1638.h"
|
||||
#include "IO_EXSensorCAM.h"
|
||||
#include "IO_DS1307.h"
|
||||
#include "IO_I2CRailcom.h"
|
||||
#include "IO_TouchKeypad.h"
|
||||
#include "IO_trainbrains.h"
|
||||
#include "IO_Bitmap.h"
|
||||
#include "IO_VL53L0X.h"
|
||||
|
||||
|
91
IO_Bitmap.h
Normal file
91
IO_Bitmap.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* © 2025, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef IO_Bitmap_h
|
||||
#define IO_Bitmap_h
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
/*
|
||||
Bitmap provides a set of virtual pins with no hardware.
|
||||
Bitmap pins are able to be output and input and may be set and tested
|
||||
as digital or analogue values.
|
||||
When writing a digital value, the analogue value is set to 0 or 1.
|
||||
When reading a digital value, the return is LOW for value 0 or HIGH for any other value
|
||||
or analogue.
|
||||
|
||||
Bitmap pins may be used for any purpose, this is easier to manage than LATCH in EXRAIL
|
||||
as they can be explicitely set and tested without interfering with underlying hardware.
|
||||
Bitmap pins may be set, reset and tested in the same way as any other pin.
|
||||
They are not persistent across reboots, but are retained in the current session.
|
||||
Bitmap pins may also be monitored by JMRI_SENSOR() and <S> as for any other pin.
|
||||
|
||||
*/
|
||||
class Bitmap : public IODevice {
|
||||
|
||||
public:
|
||||
static void create(VPIN firstVpin, int nPins) {
|
||||
if (IODevice::checkNoOverlap(firstVpin,nPins))
|
||||
new Bitmap( firstVpin, nPins);
|
||||
}
|
||||
|
||||
Bitmap(VPIN firstVpin, int nPins) : IODevice(firstVpin, nPins) {
|
||||
_pinValues=(int16_t *) calloc(nPins,sizeof(int16_t));
|
||||
// Connect to HAL so my _write, _read and _loop will be called as required.
|
||||
IODevice::addDevice(this);
|
||||
}
|
||||
|
||||
// Called by HAL to start handling this device
|
||||
void _begin() override {
|
||||
_deviceState = DEVSTATE_NORMAL;
|
||||
_display();
|
||||
}
|
||||
|
||||
int _read(VPIN vpin) override {
|
||||
int pin=vpin - _firstVpin;
|
||||
return _pinValues[pin]?1:0;
|
||||
}
|
||||
|
||||
void _write(VPIN vpin, int value) override {
|
||||
int pin = vpin - _firstVpin;
|
||||
_pinValues[pin]=value!=0; // this is digital write
|
||||
}
|
||||
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
int pin=vpin - _firstVpin;
|
||||
return _pinValues[pin]; // this is analog read
|
||||
}
|
||||
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
|
||||
(void)profile; // suppress warning, not used in this function
|
||||
(void)duration; // suppress warning, not used in this function
|
||||
int pin=vpin - _firstVpin;
|
||||
_pinValues[pin]=value; // this is analog write
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("Bitmap Configured on Vpins:%u-%u"),
|
||||
(int)_firstVpin,
|
||||
(int)_firstVpin+_nPins-1);
|
||||
}
|
||||
|
||||
private:
|
||||
int16_t* _pinValues;
|
||||
};
|
||||
#endif
|
316
IO_CMRI.cpp
316
IO_CMRI.cpp
@@ -1,316 +0,0 @@
|
||||
/*
|
||||
* © 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/>.
|
||||
*/
|
||||
|
||||
#include "IO_CMRI.h"
|
||||
#include "defines.h"
|
||||
|
||||
/************************************************************
|
||||
* CMRIbus implementation
|
||||
************************************************************/
|
||||
|
||||
// Constructor for CMRIbus
|
||||
CMRIbus::CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin) {
|
||||
_busNo = busNo;
|
||||
_serial = &serial;
|
||||
_baud = baud;
|
||||
_cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds.
|
||||
_transmitEnablePin = transmitEnablePin;
|
||||
if (_transmitEnablePin != VPIN_NONE) {
|
||||
pinMode(_transmitEnablePin, OUTPUT);
|
||||
ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); // transmitter initially off
|
||||
}
|
||||
|
||||
// Max message length is 256+6=262 bytes.
|
||||
// Each byte is one start bit, 8 data bits and 1 or 2 stop bits, assume 11 bits per byte.
|
||||
// Calculate timeout based on treble this time.
|
||||
_timeoutPeriod = 3 * 11 * 262 * 1000UL / (_baud / 1000UL);
|
||||
#if defined(ARDUINOCMRI_COMPATIBLE)
|
||||
// NOTE: The ArduinoCMRI library, unless modified, contains a 'delay(50)' between
|
||||
// receiving the end of the prompt message and starting to send the response. This
|
||||
// is allowed for below.
|
||||
_timeoutPeriod += 50000UL;
|
||||
#endif
|
||||
|
||||
// Calculate the time in microseconds to transmit one byte (11 bits max).
|
||||
_byteTransmitTime = 1000000UL * 11 / _baud;
|
||||
// Postdelay is only required if we need to allow for data still being sent when
|
||||
// we want to switch off the transmitter. The flush() method of HardwareSerial
|
||||
// ensures that the data has completed being sent over the line.
|
||||
_postDelay = 0;
|
||||
|
||||
// Add device to HAL device chain
|
||||
IODevice::addDevice(this);
|
||||
|
||||
// Add bus to CMRIbus chain.
|
||||
_nextBus = _busList;
|
||||
_busList = this;
|
||||
}
|
||||
|
||||
|
||||
// Main loop function for CMRIbus.
|
||||
// Work through list of nodes. For each node, in separate loop entries
|
||||
// send initialisation message (once only); then send
|
||||
// output message; then send prompt for input data, and
|
||||
// process any response data received.
|
||||
// When the slot time has finished, move on to the next device.
|
||||
void CMRIbus::_loop(unsigned long currentMicros) {
|
||||
|
||||
_currentMicros = currentMicros;
|
||||
|
||||
while (_serial->available())
|
||||
processIncoming();
|
||||
|
||||
// Send any data that needs sending.
|
||||
processOutgoing();
|
||||
|
||||
}
|
||||
|
||||
// Send output data to the bus for nominated CMRInode
|
||||
uint16_t CMRIbus::sendData(CMRInode *node) {
|
||||
uint16_t numDataBytes = (node->getNumOutputs()+7)/8;
|
||||
_serial->write(SYN);
|
||||
_serial->write(SYN);
|
||||
_serial->write(STX);
|
||||
_serial->write(node->getAddress() + 65);
|
||||
_serial->write('T'); // T for Transmit data message
|
||||
uint16_t charsSent = 6; // include header and trailer
|
||||
for (uint8_t index=0; index<numDataBytes; index++) {
|
||||
uint8_t value = node->getOutputStates(index);
|
||||
if (value == DLE || value == STX || value == ETX) {
|
||||
_serial->write(DLE);
|
||||
charsSent++;
|
||||
}
|
||||
_serial->write(value);
|
||||
charsSent++;
|
||||
}
|
||||
_serial->write(ETX);
|
||||
return charsSent; // number of characters sent
|
||||
}
|
||||
|
||||
// Send request for input data to nominated CMRInode.
|
||||
uint16_t CMRIbus::requestData(CMRInode *node) {
|
||||
_serial->write(SYN);
|
||||
_serial->write(SYN);
|
||||
_serial->write(STX);
|
||||
_serial->write(node->getAddress() + 65);
|
||||
_serial->write('P'); // P for Poll message
|
||||
_serial->write(ETX);
|
||||
return 6; // number of characters sent
|
||||
}
|
||||
|
||||
// Send initialisation message
|
||||
uint16_t CMRIbus::sendInitialisation(CMRInode *node) {
|
||||
_serial->write(SYN);
|
||||
_serial->write(SYN);
|
||||
_serial->write(STX);
|
||||
_serial->write(node->getAddress() + 65);
|
||||
_serial->write('I'); // I for initialise message
|
||||
_serial->write(node->getType()); // NDP
|
||||
_serial->write((uint8_t)0); // dH
|
||||
_serial->write((uint8_t)0); // dL
|
||||
_serial->write((uint8_t)0); // NS
|
||||
_serial->write(ETX);
|
||||
return 10; // number of characters sent
|
||||
}
|
||||
|
||||
void CMRIbus::processOutgoing() {
|
||||
uint16_t charsSent = 0;
|
||||
if (_currentNode == NULL) {
|
||||
// If we're between read/write cycles then don't do anything else.
|
||||
if (_currentMicros - _cycleStartTime < _cycleTime) return;
|
||||
// ... otherwise start processing the first node in the list
|
||||
_currentNode = _nodeListStart;
|
||||
_transmitState = TD_INIT;
|
||||
_cycleStartTime = _currentMicros;
|
||||
}
|
||||
if (_currentNode == NULL) return;
|
||||
switch (_transmitState) {
|
||||
case TD_IDLE:
|
||||
case TD_INIT:
|
||||
enableTransmitter();
|
||||
if (!_currentNode->isInitialised()) {
|
||||
charsSent = sendInitialisation(_currentNode);
|
||||
_currentNode->setInitialised();
|
||||
_transmitState = TD_TRANSMIT;
|
||||
delayUntil(_currentMicros+_byteTransmitTime*charsSent);
|
||||
break;
|
||||
}
|
||||
/* fallthrough */
|
||||
case TD_TRANSMIT:
|
||||
charsSent = sendData(_currentNode);
|
||||
_transmitState = TD_PROMPT;
|
||||
// Defer next entry for as long as it takes to transmit the characters,
|
||||
// to allow output queue to empty. Allow 2 bytes extra.
|
||||
delayUntil(_currentMicros+_byteTransmitTime*(charsSent+2));
|
||||
break;
|
||||
case TD_PROMPT:
|
||||
charsSent = requestData(_currentNode);
|
||||
disableTransmitter();
|
||||
_transmitState = TD_RECEIVE;
|
||||
_timeoutStart = _currentMicros; // Start timeout on response
|
||||
break;
|
||||
case TD_RECEIVE: // Waiting for response / timeout
|
||||
if (_currentMicros - _timeoutStart > _timeoutPeriod) {
|
||||
// End of time slot allocated for responses.
|
||||
_transmitState = TD_IDLE;
|
||||
// Reset state of receiver
|
||||
_receiveState = RD_SYN1;
|
||||
// Move to next node
|
||||
_currentNode = _currentNode->getNext();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process any data bytes received from a CMRInode.
|
||||
void CMRIbus::processIncoming() {
|
||||
int data = _serial->read();
|
||||
if (data < 0) return; // No characters to read
|
||||
|
||||
if (_transmitState != TD_RECEIVE || !_currentNode) return; // Not waiting for input, so ignore.
|
||||
|
||||
uint8_t nextState = RD_SYN1; // default to resetting state machine
|
||||
switch(_receiveState) {
|
||||
case RD_SYN1:
|
||||
if (data == SYN) nextState = RD_SYN2;
|
||||
break;
|
||||
case RD_SYN2:
|
||||
if (data == SYN) nextState = RD_STX; else nextState = RD_SYN2;
|
||||
break;
|
||||
case RD_STX:
|
||||
if (data == STX) nextState = RD_ADDR;
|
||||
break;
|
||||
case RD_ADDR:
|
||||
// If address doesn't match, then ignore everything until next SYN-SYN-STX.
|
||||
if (data == _currentNode->getAddress() + 65) nextState = RD_TYPE;
|
||||
break;
|
||||
case RD_TYPE:
|
||||
_receiveDataIndex = 0; // Initialise data pointer
|
||||
if (data == 'R') nextState = RD_DATA;
|
||||
break;
|
||||
case RD_DATA: // data body
|
||||
if (data == DLE) // escape next character
|
||||
nextState = RD_ESCDATA;
|
||||
else if (data == ETX) { // end of data
|
||||
// End of data message. Protocol has all data in one
|
||||
// message, so we don't need to wait any more. Allow
|
||||
// transmitter to proceed with next node in list.
|
||||
_currentNode = _currentNode->getNext();
|
||||
_transmitState = TD_IDLE;
|
||||
} else {
|
||||
// Not end yet, so save data byte
|
||||
_currentNode->saveIncomingData(_receiveDataIndex++, data);
|
||||
nextState = RD_DATA; // wait for more data
|
||||
}
|
||||
break;
|
||||
case RD_ESCDATA: // escaped data byte
|
||||
_currentNode->saveIncomingData(_receiveDataIndex++, data);
|
||||
nextState = RD_DATA;
|
||||
break;
|
||||
}
|
||||
_receiveState = nextState;
|
||||
}
|
||||
|
||||
// If configured for half duplex RS485, switch RS485 interface
|
||||
// into transmit mode.
|
||||
void CMRIbus::enableTransmitter() {
|
||||
if (_transmitEnablePin != VPIN_NONE)
|
||||
ArduinoPins::fastWriteDigital(_transmitEnablePin, 1);
|
||||
// If we need a delay before we start the packet header,
|
||||
// we can send a character or two to synchronise the
|
||||
// transmitter and receiver.
|
||||
// SYN characters should be used, but a bug in the
|
||||
// ArduinoCMRI library causes it to ignore the packet if
|
||||
// it's preceded by an odd number of SYN characters.
|
||||
// So send a SYN followed by a NUL in that case.
|
||||
_serial->write(SYN);
|
||||
#if defined(ARDUINOCMRI_COMPATIBLE)
|
||||
_serial->write(NUL); // Reset the ArduinoCMRI library's parser
|
||||
#endif
|
||||
}
|
||||
|
||||
// If configured for half duplex RS485, switch RS485 interface
|
||||
// into receive mode.
|
||||
void CMRIbus::disableTransmitter() {
|
||||
// Wait until all data has been transmitted. On the standard
|
||||
// AVR driver, this waits until the FIFO is empty and all
|
||||
// data has been sent over the link.
|
||||
_serial->flush();
|
||||
// If we don't trust the 'flush' function and think the
|
||||
// data's still in transit, then wait a bit longer.
|
||||
if (_postDelay > 0)
|
||||
delayMicroseconds(_postDelay);
|
||||
// Hopefully, we can now safely switch off the transmitter.
|
||||
if (_transmitEnablePin != VPIN_NONE)
|
||||
ArduinoPins::fastWriteDigital(_transmitEnablePin, 0);
|
||||
}
|
||||
|
||||
// Link to chain of CMRI bus instances
|
||||
CMRIbus *CMRIbus::_busList = NULL;
|
||||
|
||||
|
||||
/************************************************************
|
||||
* CMRInode implementation
|
||||
************************************************************/
|
||||
|
||||
// Constructor for CMRInode object
|
||||
CMRInode::CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs, uint16_t outputs) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
_busNo = busNo;
|
||||
_address = address;
|
||||
_type = type;
|
||||
|
||||
switch (_type) {
|
||||
case 'M': // SMINI, fixed 24 inputs and 48 outputs
|
||||
_numInputs = 24;
|
||||
_numOutputs = 48;
|
||||
break;
|
||||
case 'C': // CPNODE with 16 to 144 inputs/outputs using 8-bit cards
|
||||
_numInputs = inputs;
|
||||
_numOutputs = outputs;
|
||||
break;
|
||||
case 'N': // Classic USIC and SUSIC using 24 bit i/o cards
|
||||
case 'X': // SUSIC using 32 bit i/o cards
|
||||
default:
|
||||
DIAG(F("CMRInode: bus:%d address:%d ERROR unsupported type %c"), _busNo, _address, _type);
|
||||
return; // Don't register device.
|
||||
}
|
||||
if ((unsigned int)_nPins < _numInputs + _numOutputs)
|
||||
DIAG(F("CMRInode: bus:%d address:%d WARNING number of Vpins does not cover all inputs and outputs"), _busNo, _address);
|
||||
|
||||
// Allocate memory for states
|
||||
_inputStates = (uint8_t *)calloc((_numInputs+7)/8, 1);
|
||||
_outputStates = (uint8_t *)calloc((_numOutputs+7)/8, 1);
|
||||
if (!_inputStates || !_outputStates) {
|
||||
DIAG(F("CMRInode: ERROR insufficient memory"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add this device to HAL device list
|
||||
IODevice::addDevice(this);
|
||||
|
||||
// Add CMRInode to CMRIbus object.
|
||||
CMRIbus *bus = CMRIbus::findBus(_busNo);
|
||||
if (bus != NULL) {
|
||||
bus->addNode(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
293
IO_CMRI.h
293
IO_CMRI.h
@@ -1,293 +0,0 @@
|
||||
/*
|
||||
* © 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* CMRIbus
|
||||
* =======
|
||||
* To define a CMRI bus, example syntax:
|
||||
* CMRIbus::create(bus, serial, baud[, cycletime[, pin]]);
|
||||
*
|
||||
* bus = 0-255
|
||||
* serial = serial port to be used (e.g. Serial3)
|
||||
* baud = baud rate (9600, 19200, 28800, 57600 or 115200)
|
||||
* cycletime = minimum time between successive updates/reads of a node in millisecs (default 500ms)
|
||||
* pin = pin number connected to RS485 module's DE and !RE terminals for half-duplex operation (default VPIN_NONE)
|
||||
*
|
||||
* Each bus must use a different serial port.
|
||||
*
|
||||
* IMPORTANT: If you are using ArduinoCMRI library code by Michael Adams, at the time of writing this library
|
||||
* is not compliant with the LCS-9.10.1 specification for CMRInet protocol.
|
||||
* Various work-arounds may be enabled within the driver by adding the following line to your config.h file,
|
||||
* to allow nodes running the ArduinoCMRI library to communicate:
|
||||
*
|
||||
* #define ARDUINOCMRI_COMPATIBLE
|
||||
*
|
||||
* CMRINode
|
||||
* ========
|
||||
* To define a CMRI node and associate it with a CMRI bus,
|
||||
* CMRInode::create(firstVPIN, numVPINs, bus, address, type [, inputs, outputs]);
|
||||
*
|
||||
* firstVPIN = first vpin in block allocated to this device
|
||||
* numVPINs = number of vpins (e.g. 72 for an SMINI node)
|
||||
* bus = 0-255
|
||||
* address = 0-127
|
||||
* type = 'M' for SMINI (fixed 24 inputs and 48 outputs)
|
||||
* 'C' for CPNODE (16 to 144 inputs/outputs in groups of 8)
|
||||
* (other types are not supported at this time).
|
||||
* inputs = number of inputs (CPNODE only)
|
||||
* outputs = number of outputs (CPNODE only)
|
||||
*
|
||||
* Reference: "LCS-9.10.1
|
||||
* Layout Control Specification: CMRInet Protocol
|
||||
* Version 1.1 December 2014."
|
||||
*/
|
||||
|
||||
#ifndef IO_CMRI_H
|
||||
#define IO_CMRI_H
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
/**********************************************************************
|
||||
* CMRInode class
|
||||
*
|
||||
* This encapsulates the state associated with a single CMRI node,
|
||||
* which includes the address type, number of inputs and outputs, and
|
||||
* the states of the inputs and outputs.
|
||||
**********************************************************************/
|
||||
class CMRInode : public IODevice {
|
||||
private:
|
||||
uint8_t _busNo;
|
||||
uint8_t _address;
|
||||
char _type;
|
||||
CMRInode *_next = NULL;
|
||||
uint8_t *_inputStates = NULL;
|
||||
uint8_t *_outputStates = NULL;
|
||||
uint16_t _numInputs = 0;
|
||||
uint16_t _numOutputs = 0;
|
||||
bool _initialised = false;
|
||||
|
||||
public:
|
||||
static void create(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs=0, uint16_t outputs=0) {
|
||||
if (checkNoOverlap(firstVpin, nPins)) new CMRInode(firstVpin, nPins, busNo, address, type, inputs, outputs);
|
||||
}
|
||||
CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs=0, uint16_t outputs=0);
|
||||
|
||||
uint8_t getAddress() {
|
||||
return _address;
|
||||
}
|
||||
CMRInode *getNext() {
|
||||
return _next;
|
||||
}
|
||||
void setNext(CMRInode *node) {
|
||||
_next = node;
|
||||
}
|
||||
bool isInitialised() {
|
||||
return _initialised;
|
||||
}
|
||||
void setInitialised() {
|
||||
_initialised = true;
|
||||
}
|
||||
|
||||
void _begin() {
|
||||
_initialised = false;
|
||||
}
|
||||
|
||||
int _read(VPIN vpin) {
|
||||
// Return current state from this device
|
||||
uint16_t pin = vpin - _firstVpin;
|
||||
if (pin < _numInputs) {
|
||||
uint8_t mask = 1 << (pin & 0x7);
|
||||
int index = pin / 8;
|
||||
return (_inputStates[index] & mask) != 0;
|
||||
} else
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _write(VPIN vpin, int value) {
|
||||
// Update current state for this device, in preparation the bus transmission
|
||||
uint16_t pin = vpin - _firstVpin - _numInputs;
|
||||
if (pin < _numOutputs) {
|
||||
uint8_t mask = 1 << (pin & 0x7);
|
||||
int index = pin / 8;
|
||||
if (value)
|
||||
_outputStates[index] |= mask;
|
||||
else
|
||||
_outputStates[index] &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
void saveIncomingData(uint8_t index, uint8_t data) {
|
||||
if (index < (_numInputs+7)/8)
|
||||
_inputStates[index] = data;
|
||||
}
|
||||
|
||||
uint8_t getOutputStates(uint8_t index) {
|
||||
if (index < (_numOutputs+7)/8)
|
||||
return _outputStates[index];
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t getNumInputs() {
|
||||
return _numInputs;
|
||||
}
|
||||
|
||||
uint16_t getNumOutputs() {
|
||||
return _numOutputs;
|
||||
}
|
||||
|
||||
char getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
uint8_t getBusNumber() {
|
||||
return _busNo;
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("CMRInode type:'%c' configured on bus:%d address:%d VPINs:%u-%u (in) %u-%u (out)"),
|
||||
_type, _busNo, _address, _firstVpin, _firstVpin+_numInputs-1,
|
||||
_firstVpin+_numInputs, _firstVpin+_numInputs+_numOutputs-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**********************************************************************
|
||||
* CMRIbus class
|
||||
*
|
||||
* This encapsulates the properties state of the bus and the
|
||||
* transmission and reception of data across that bus. Each CMRIbus
|
||||
* object owns a set of CMRInode objects which represent the nodes
|
||||
* attached to that bus.
|
||||
**********************************************************************/
|
||||
class CMRIbus : public IODevice {
|
||||
private:
|
||||
// Here we define the device-specific variables.
|
||||
uint8_t _busNo;
|
||||
HardwareSerial *_serial;
|
||||
unsigned long _baud;
|
||||
VPIN _transmitEnablePin = VPIN_NONE;
|
||||
CMRInode *_nodeListStart = NULL, *_nodeListEnd = NULL;
|
||||
CMRInode *_currentNode = NULL;
|
||||
|
||||
// Transmitter state machine states
|
||||
enum {TD_IDLE, TD_PRETRANSMIT, TD_INIT, TD_TRANSMIT, TD_PROMPT, TD_RECEIVE};
|
||||
uint8_t _transmitState = TD_IDLE;
|
||||
// Receiver state machine states.
|
||||
enum {RD_SYN1, RD_SYN2, RD_STX, RD_ADDR, RD_TYPE,
|
||||
RD_DATA, RD_ESCDATA, RD_SKIPDATA, RD_SKIPESCDATA, RD_ETX};
|
||||
uint8_t _receiveState = RD_SYN1;
|
||||
uint16_t _receiveDataIndex = 0; // Index of next data byte to be received.
|
||||
CMRIbus *_nextBus = NULL; // Pointer to next bus instance in list.
|
||||
unsigned long _cycleStartTime = 0;
|
||||
unsigned long _timeoutStart = 0;
|
||||
unsigned long _cycleTime; // target time between successive read/write cycles, microseconds
|
||||
unsigned long _timeoutPeriod; // timeout on read responses, in microseconds.
|
||||
unsigned long _currentMicros; // last value of micros() from _loop function.
|
||||
unsigned long _postDelay; // delay time after transmission before switching off transmitter (in us)
|
||||
unsigned long _byteTransmitTime; // time in us for transmission of one byte
|
||||
|
||||
static CMRIbus *_busList; // linked list of defined bus instances
|
||||
|
||||
// Definition of special characters in CMRInet protocol
|
||||
enum : uint8_t {
|
||||
NUL = 0x00,
|
||||
STX = 0x02,
|
||||
ETX = 0x03,
|
||||
DLE = 0x10,
|
||||
SYN = 0xff,
|
||||
};
|
||||
|
||||
public:
|
||||
static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS=500, VPIN transmitEnablePin=VPIN_NONE) {
|
||||
new CMRIbus(busNo, serial, baud, cycleTimeMS, transmitEnablePin);
|
||||
}
|
||||
|
||||
// Device-specific initialisation
|
||||
void _begin() override {
|
||||
// CMRInet spec states one stop bit, JMRI and ArduinoCMRI use two stop bits
|
||||
#if defined(ARDUINOCMRI_COMPATIBLE)
|
||||
_serial->begin(_baud, SERIAL_8N2);
|
||||
#else
|
||||
_serial->begin(_baud, SERIAL_8N1);
|
||||
#endif
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Loop function (overriding IODevice::_loop(unsigned long))
|
||||
void _loop(unsigned long currentMicros) override;
|
||||
|
||||
// Display information about the device
|
||||
void _display() override {
|
||||
DIAG(F("CMRIbus %d configured, speed=%d baud, cycle=%d ms"), _busNo, _baud, _cycleTime/1000);
|
||||
}
|
||||
|
||||
// Locate CMRInode object with specified address.
|
||||
CMRInode *findNode(uint8_t address) {
|
||||
for (CMRInode *node = _nodeListStart; node != NULL; node = node->getNext()) {
|
||||
if (node->getAddress() == address)
|
||||
return node;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add new CMRInode to the list of nodes for this bus.
|
||||
void addNode(CMRInode *newNode) {
|
||||
if (!_nodeListStart)
|
||||
_nodeListStart = newNode;
|
||||
if (!_nodeListEnd)
|
||||
_nodeListEnd = newNode;
|
||||
else {
|
||||
_nodeListEnd->setNext(newNode);
|
||||
_nodeListEnd = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin);
|
||||
uint16_t sendData(CMRInode *node);
|
||||
uint16_t requestData(CMRInode *node);
|
||||
uint16_t sendInitialisation(CMRInode *node);
|
||||
|
||||
// Process any data bytes received from a CMRInode.
|
||||
void processIncoming();
|
||||
// Process any outgoing traffic that is due.
|
||||
void processOutgoing();
|
||||
// Enable transmitter
|
||||
void enableTransmitter();
|
||||
// Disable transmitter and enable receiver
|
||||
void disableTransmitter();
|
||||
|
||||
|
||||
public:
|
||||
uint8_t getBusNumber() {
|
||||
return _busNo;
|
||||
}
|
||||
|
||||
static CMRIbus *findBus(uint8_t busNo) {
|
||||
for (CMRIbus *bus=_busList; bus!=NULL; bus=bus->_nextBus) {
|
||||
if (bus->_busNo == busNo) return bus;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // IO_CMRI_H
|
@@ -85,6 +85,7 @@ class EXSensorCAM : public IODevice {
|
||||
void _begin() {
|
||||
uint8_t status;
|
||||
// Initialise EX-SensorCAM device
|
||||
I2CManager.setClock(100000); // Set speed for I2C operations
|
||||
I2CManager.begin();
|
||||
if (!I2CManager.exists(_I2CAddress)) {
|
||||
DIAG(F("EX-SensorCAM I2C:%s device not found"), _I2CAddress.toString());
|
||||
|
@@ -72,6 +72,8 @@ const byte _DIR_MASK = 0x30;
|
||||
|
||||
|
||||
void EncoderThrottle::_loop(unsigned long currentMicros) {
|
||||
(void) currentMicros; // suppress warning
|
||||
|
||||
if (_locoid==0) return; // not in use
|
||||
|
||||
// Clicking down on the roco, stops the loco and sets the direction as unknown.
|
||||
@@ -118,9 +120,10 @@ const byte _DIR_MASK = 0x30;
|
||||
}
|
||||
}
|
||||
|
||||
// Selocoid as analog value to start drive
|
||||
// Set locoid as analog value to start drive
|
||||
// use <z vpin locoid [notch]>
|
||||
void EncoderThrottle::_writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
|
||||
(void)vpin; // not used, but needed to match IODevice interface
|
||||
(void) param2;
|
||||
_locoid=value;
|
||||
if (param1>0) _notch=param1;
|
||||
|
@@ -197,6 +197,7 @@ public:
|
||||
// Check for incoming data, and update busy flag and other state accordingly
|
||||
|
||||
void processIncoming(unsigned long currentMicros) {
|
||||
(void)currentMicros; // suppress warning, not used in this function
|
||||
// Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF"
|
||||
RX_fifo_lvl();
|
||||
if (FIFO_RX_LEVEL >= 10) {
|
||||
@@ -308,7 +309,7 @@ public:
|
||||
sendPacket(0x0C,0,0);
|
||||
_resetCmd = false;
|
||||
} else if(_volCmd == true) { // do the volme before palying a track
|
||||
if(_requestedVolumeLevel >= 0 && _requestedVolumeLevel <= 30){
|
||||
if(_requestedVolumeLevel <= 30) {
|
||||
_currentVolume = _requestedVolumeLevel; // If _requestedVolumeLevel is out of range, sent _currentV1olume
|
||||
}
|
||||
sendPacket(0x06, 0x00, _currentVolume);
|
||||
@@ -407,6 +408,9 @@ public:
|
||||
|
||||
// Write to a vPin will do nothing
|
||||
void _write(VPIN vpin, int value) override {
|
||||
(void)vpin; // suppress warning, not used in this function
|
||||
(void)value; // suppress warning, not used in this function
|
||||
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("I2CDFPlayer: Writing to any vPin not supported"));
|
||||
|
@@ -77,6 +77,7 @@ void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
|
||||
|
||||
|
||||
void I2CRailcom::_loop(unsigned long currentMicros) {
|
||||
(void)currentMicros; // not used, but needed to match IODevice interface
|
||||
// Read responses from device
|
||||
if (_deviceState!=DEVSTATE_NORMAL) return;
|
||||
|
||||
|
101
IO_PCA9554.h
Normal file
101
IO_PCA9554.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* © 2025, Paul M. Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef io_pca9554_h
|
||||
#define io_pca9554_h
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for PCA9554/TCA9554 8-bit I/O expander (NXP & Texas Instruments).
|
||||
*/
|
||||
|
||||
class PCA9554 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9554(vpin,nPins, i2cAddress, interruptPin);
|
||||
}
|
||||
|
||||
private:
|
||||
// Constructor
|
||||
PCA9554(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCA9554"), vpin, nPins, I2CAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||
outputBuffer, sizeof(outputBuffer));
|
||||
outputBuffer[0] = REG_INPUT_P0;
|
||||
}
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 2, REG_OUTPUT_P0, _portOutputState);
|
||||
}
|
||||
void _writePullups() override {
|
||||
// Do nothing, pull-ups are always in place for input ports
|
||||
// This function is here for HAL GPIOBase API compatibilitiy
|
||||
|
||||
}
|
||||
void _writePortModes() override {
|
||||
// Write 0 to REG_CONF_P0 for in-use pins that are outputs, 1 for others.
|
||||
// PCA9554 & TCA9554, Interrupt is always enabled for raising and falling edge
|
||||
uint8_t temp = ~(_portMode & _portInUse);
|
||||
I2CManager.write(_I2CAddress, 2, REG_CONF_P0, temp);
|
||||
}
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer[1];
|
||||
I2CManager.read(_I2CAddress, buffer, 1, 1, REG_INPUT_P0);
|
||||
_portInputState = buffer[0];
|
||||
} else {
|
||||
// Queue new request
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xff;
|
||||
}
|
||||
|
||||
void _setupDevice() override {
|
||||
// HAL API calls
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
_writeGpioPort();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[1];
|
||||
uint8_t outputBuffer[1];
|
||||
|
||||
|
||||
enum {
|
||||
REG_INPUT_P0 = 0x00,
|
||||
REG_OUTPUT_P0 = 0x01,
|
||||
REG_POL_INV_P0 = 0x02,
|
||||
REG_CONF_P0 = 0x03,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif
|
13
IO_PCF8574.h
13
IO_PCF8574.h
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
* © 2025 Herb Morton
|
||||
* © 2022 Paul M Antoine
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
@@ -43,15 +44,21 @@
|
||||
|
||||
class PCF8574 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
|
||||
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {
|
||||
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);
|
||||
}
|
||||
|
||||
private:
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
|
||||
if (initPortState>=0) {
|
||||
_portMode = 255; // set all pins to output mode
|
||||
_portInUse = 255; // 8 ports in use
|
||||
_portOutputState = initPortState; // initialize pins low-high 0-255
|
||||
I2CManager.write(_I2CAddress, 1, initPortState);
|
||||
}
|
||||
}
|
||||
|
||||
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
|
||||
|
@@ -136,6 +136,7 @@ private:
|
||||
|
||||
// Return the position sent by the rotary encoder software
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
(void)vpin; // suppress warning, not used in this function
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
return _position;
|
||||
}
|
||||
@@ -153,6 +154,8 @@ private:
|
||||
// To be valid, must be 0 to 255, and different to the current position
|
||||
// If the current position is the same, it was initiated by the rotary encoder
|
||||
void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t duration) override {
|
||||
(void)profile; // suppress warning, not used in this function
|
||||
(void)duration; // suppress warning, not used in this function
|
||||
if (vpin == _firstVpin + 2) {
|
||||
if (position >= 0 && position <= 255 && position != _position) {
|
||||
byte newPosition = position & 0xFF;
|
||||
|
130
LocoTable.cpp
Normal file
130
LocoTable.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This source is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "LocoTable.h"
|
||||
|
||||
LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
|
||||
int LocoTable::highestUsedReg = 0;
|
||||
|
||||
int LocoTable::lookupSpeedTable(int locoId, bool autoCreate) {
|
||||
// determine speed reg for this loco
|
||||
int firstEmpty = MAX_LOCOS;
|
||||
int reg;
|
||||
for (reg = 0; reg < MAX_LOCOS; reg++) {
|
||||
if (speedTable[reg].loco == locoId) break;
|
||||
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
|
||||
}
|
||||
|
||||
// return -1 if not found and not auto creating
|
||||
if (reg == MAX_LOCOS && !autoCreate) return -1;
|
||||
if (reg == MAX_LOCOS) reg = firstEmpty;
|
||||
if (reg >= MAX_LOCOS) {
|
||||
//DIAG(F("Too many locos"));
|
||||
return -1;
|
||||
}
|
||||
if (reg==firstEmpty){
|
||||
speedTable[reg].loco = locoId;
|
||||
speedTable[reg].speedCode=128; // default direction forward
|
||||
speedTable[reg].groupFlags=0;
|
||||
speedTable[reg].functions=0;
|
||||
}
|
||||
if (reg > highestUsedReg) highestUsedReg = reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
// returns false only if loco existed but nothing was changed
|
||||
bool LocoTable::updateLoco(int loco, byte speedCode) {
|
||||
if (loco==0) {
|
||||
/*
|
||||
// broadcast stop/estop but dont change direction
|
||||
for (int reg = 0; reg < highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco==0) continue;
|
||||
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].speedCode != newspeed) {
|
||||
speedTable[reg].speedCode = newspeed;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco, false);
|
||||
if (reg>=0) {
|
||||
speedTable[reg].speedcounter++;
|
||||
if (speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// new
|
||||
reg=lookupSpeedTable(loco, true);
|
||||
if(reg >=0) speedTable[reg].speedCode = speedCode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool LocoTable::updateFunc(int loco, byte func, int shift) {
|
||||
unsigned long previous;
|
||||
unsigned long newfunc;
|
||||
bool retval = false; // nothing was touched
|
||||
int reg = lookupSpeedTable(loco, false);
|
||||
if (reg < 0) { // not found
|
||||
retval = true;
|
||||
reg = lookupSpeedTable(loco, true);
|
||||
newfunc = previous = 0;
|
||||
} else {
|
||||
newfunc = previous = speedTable[reg].functions;
|
||||
}
|
||||
|
||||
speedTable[reg].funccounter++;
|
||||
|
||||
if(shift == 1) { // special case for light
|
||||
newfunc &= ~1UL;
|
||||
newfunc |= ((func & 0B10000) >> 4);
|
||||
}
|
||||
newfunc &= ~(0B1111UL << shift);
|
||||
newfunc |= ((func & 0B1111) << shift);
|
||||
|
||||
if (newfunc != previous) {
|
||||
speedTable[reg].functions = newfunc;
|
||||
retval = true;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
void LocoTable::dumpTable(Stream *output) {
|
||||
output->print("\n-----------Table---------\n");
|
||||
for (byte reg = 0; reg <= highestUsedReg; reg++) {
|
||||
if (speedTable[reg].loco != 0) {
|
||||
output->print(speedTable[reg].loco);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].speedCode);
|
||||
output->print(' ');
|
||||
output->print(speedTable[reg].functions);
|
||||
output->print(" #funcpacks:");
|
||||
output->print(speedTable[reg].funccounter);
|
||||
output->print(" #speedpacks:");
|
||||
output->print(speedTable[reg].speedcounter);
|
||||
speedTable[reg].funccounter = 0;
|
||||
speedTable[reg].speedcounter = 0;
|
||||
output->print('\n');
|
||||
}
|
||||
}
|
||||
}
|
44
LocoTable.h
Normal file
44
LocoTable.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Copyright (c) 2023 Harald Barth
|
||||
*
|
||||
* This source is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This source is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this software. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "DCC.h" // fetch MAX_LOCOS from there
|
||||
|
||||
class LocoTable {
|
||||
public:
|
||||
void forgetLoco(int cab) {
|
||||
int reg=lookupSpeedTable(cab, false);
|
||||
if (reg>=0) speedTable[reg].loco=0;
|
||||
}
|
||||
static int lookupSpeedTable(int locoId, bool autoCreate);
|
||||
static bool updateLoco(int loco, byte speedCode);
|
||||
static bool updateFunc(int loco, byte func, int shift);
|
||||
static void dumpTable(Stream *output);
|
||||
|
||||
private:
|
||||
struct LOCO
|
||||
{
|
||||
int loco;
|
||||
byte speedCode;
|
||||
byte groupFlags;
|
||||
unsigned long functions;
|
||||
unsigned int funccounter;
|
||||
unsigned int speedcounter;
|
||||
};
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
static int highestUsedReg;
|
||||
};
|
@@ -242,7 +242,7 @@ void MotorDriver::setPower(POWERMODE mode) {
|
||||
// when switching a track On, we need to check the crrentOffset with the pin OFF
|
||||
if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) {
|
||||
senseOffset = ADCee::read(currentPin);
|
||||
DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
|
||||
if (Diag::ACK) DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
|
||||
}
|
||||
|
||||
IODevice::write(powerPin,invertPower ? LOW : HIGH);
|
||||
|
58
Release_Notes/IO_Bitmap.md
Normal file
58
Release_Notes/IO_Bitmap.md
Normal file
@@ -0,0 +1,58 @@
|
||||
Virtual Bitmap device pins.
|
||||
|
||||
a Bitmap device pin is a software representation of a virtual hardware device that has the ability to store a 16bit value.
|
||||
|
||||
This this is easier to manage than LATCH in EXRAIL as they can be explicitely set and tested without interfering with underlying hardware or breaching the 255 limit.
|
||||
|
||||
Virtual pins may be set, reset and tested in the same way as any other pin. Unlike sensors and leds, these device pins are both INPUT and OUTPUT These can be used in many ways:
|
||||
|
||||
As a simple digital flag to assist in inter-thread communication.
|
||||
A flag or value that can be set from commands and tested in EXRAIL.(e.g. to stop a sequence)
|
||||
As a counter for looping or occupancy counts such as trains passing over a multi track road crossing.
|
||||
As a collection of 16 digital bits that can be set, reset, toggled, masked and tested.
|
||||
|
||||
Existing <> and exrail commands for vpins work on these pins.
|
||||
|
||||
Virtual pin creation:
|
||||
HAL(Bitmap,firstpin,npins)
|
||||
creates 1 or more virtual pins in software. (RAM requirement approximately 2 bytes per pin)
|
||||
e.g. HAL(Bitmap,1000,20) creates pins 1000..1019
|
||||
|
||||
Simple use as flags:
|
||||
This uses the traditional digital pin commands
|
||||
SET(1013) RESET(1013) sets value 1 or 0
|
||||
SET(1000,20) RESET(1000,20) sets/resets a range of pins
|
||||
IF(1000) tests if pin value!=0
|
||||
|
||||
Commands can set 1/0 values using <z 1010> <z -1010> as for any digital output.
|
||||
BLINK can be used to set them on/off on a time pattern.
|
||||
|
||||
In addition, Exrail sensor comands work as if these pins were sensors
|
||||
ONBUTTON(1013) triggers when value changes from 0 to something.
|
||||
ONSENSOR(1013) triggers when value changes to or from 0.
|
||||
<S 1013 1013 1> and JMRI_SENSOR(1013) report <Q/q responses when changing to or from 0.
|
||||
|
||||
Use as analog values:
|
||||
Analog values may be set into the virtual pins and tested using the existing analog value commands and exrail macros.
|
||||
<z vpin value> <D ANIN vpin> etc.
|
||||
|
||||
Use as counters:
|
||||
For loop counting, counters can be incremented by BITMAP_INC(1013) and decremented by BITMAP_DEC(1013) and tested with IF/IFNOT/IFGTE etc.
|
||||
Counters be used to automate a multi track crossing where each train entering increments the counter and decrements it on clearing the crossing. Crossing gate automation can be started when the value changes from 0, and be stopped when the counter returns to 0. Detecting the first increment from 0 to 1 can be done with ONBUTTON(1013) and the automation can use IF(1013) or IFNOT(1013) to detect when it needs to reopen the road gates.
|
||||
|
||||
Use as binary flag groups:
|
||||
Virtual pins (and others that respond to an analog read in order to provide bitmapped digital data, such as SensorCam) can be set and tested with new special EXRAIL commands
|
||||
|
||||
IFBITMAP_ALL(vpin,mask) Bitwise ANDs the the vpin value with the mask value and is true if ALL the 1 bits in the mask are also 1 bits in the value.
|
||||
e.g. IFBITMAP_ALL(1013,0x0f) would be true if ALL the last 4 bits of the value are 1s.
|
||||
|
||||
IFBITMAP_ANY(1013,0x0f) would be true if ANY of the last 4 bits are 1s.
|
||||
|
||||
|
||||
Modifying bitmap values:
|
||||
BITMAP_AND(vpin,mask) performs a bitwise AND operation.
|
||||
BITMAP_OR(vpin,mask) performa a bitwise OR operation
|
||||
BITMAP_XOR(vpin,mask) performs a bitwise EXCLUSIVE OR (which is basically a toggle)
|
||||
|
||||
|
||||
|
67
SensorGroup.cpp
Normal file
67
SensorGroup.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "SensorGroup.h"
|
||||
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
|
||||
// called in loop to check sensors
|
||||
void SensorGroup::checkAll() {
|
||||
doExrailSensorGroup(GroupProcess::check, & USB_SERIAL);
|
||||
}
|
||||
|
||||
// called by command to get sensor list
|
||||
void SensorGroup::printAll(Print * serial) {
|
||||
(void)serial; // suppress unused warning
|
||||
doExrailSensorGroup(GroupProcess::print,serial);
|
||||
}
|
||||
|
||||
void SensorGroup::prepareAll() {
|
||||
doExrailSensorGroup(GroupProcess::prepare, & USB_SERIAL);
|
||||
}
|
||||
|
||||
void SensorGroup::dumpAll(Print * stream) {
|
||||
doExrailSensorGroup(GroupProcess::dump, stream);
|
||||
}
|
||||
|
||||
#else
|
||||
// if EXRAIL is not active, these functions are empty
|
||||
void SensorGroup::checkAll() {}
|
||||
void SensorGroup::printAll(Print * serial) {(void)serial;}
|
||||
void SensorGroup::prepareAll() {}
|
||||
#endif
|
||||
|
||||
// called by EXRAIL constructed doExrailSensorGroup for each group
|
||||
void SensorGroup::doSensorGroup(VPIN firstVpin, int nPins, byte* statebits,
|
||||
GroupProcess action, Print * serial, bool pullup) {
|
||||
|
||||
// Loop through the pins in the group
|
||||
for (auto i=0;i<nPins;i++) {
|
||||
// locate position of state bit
|
||||
byte stateByte=i/8;
|
||||
byte stateMask=1<<(i%8);
|
||||
VPIN vpin= firstVpin+i;
|
||||
switch(action) {
|
||||
case GroupProcess::prepare:
|
||||
IODevice::configureInput(vpin, pullup);
|
||||
__attribute__ ((fallthrough)); // to check the current state
|
||||
|
||||
case GroupProcess::check:
|
||||
// check for state change
|
||||
if ((bool)(statebits[stateByte]&stateMask) ==IODevice::read(vpin)) break; // no change
|
||||
// flip state bit
|
||||
statebits[stateByte]^=stateMask;
|
||||
if (action==GroupProcess::prepare) break;
|
||||
// fall through to print the changed value
|
||||
__attribute__ ((fallthrough));
|
||||
|
||||
case GroupProcess::print:
|
||||
StringFormatter::send(serial, F("<%c %d>\n"),
|
||||
(statebits[stateByte]&stateMask)?'Q':'q', vpin);
|
||||
break;
|
||||
|
||||
case GroupProcess::dump:
|
||||
StringFormatter::send(serial, F("<Q %d %d %c>\n"),
|
||||
vpin, vpin, pullup?'1':'0');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
SensorGroup.h
Normal file
28
SensorGroup.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef SensorGroup_h
|
||||
#define SensorGroup_h
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "IODevice.h"
|
||||
#include "StringFormatter.h"
|
||||
|
||||
|
||||
// reference to the optional exrail built function which contains the
|
||||
// calls to SensorGroup::doSensorGroup
|
||||
|
||||
enum GroupProcess:byte {prepare,print,check,dump};
|
||||
|
||||
class SensorGroup {
|
||||
public:
|
||||
static void checkAll();
|
||||
static void printAll(Print * serial);
|
||||
static void prepareAll();
|
||||
static void dumpAll(Print* serial);
|
||||
|
||||
// doSensorGroup is called from the automatically
|
||||
// built doExrailSensorGroup, once for each user defined group.
|
||||
static void doSensorGroup(VPIN vpin, int nPins, byte* statebits,
|
||||
GroupProcess action, Print * serial, bool pullup=false);
|
||||
private:
|
||||
static void doExrailSensorGroup(GroupProcess action, Print * stream);
|
||||
};
|
||||
#endif // SensorGroup_h
|
27
Sensors.cpp
27
Sensors.cpp
@@ -91,6 +91,9 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Sensor::checkAll(){
|
||||
|
||||
SensorGroup::checkAll();
|
||||
|
||||
uint16_t sensorCount = 0;
|
||||
|
||||
#ifdef USE_NOTIFY
|
||||
@@ -181,13 +184,25 @@ void Sensor::inputChangeCallback(VPIN vpin, int state) {
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Sensor::printAll(Print *stream){
|
||||
|
||||
if (stream == NULL) return; // Nothing to do
|
||||
|
||||
SensorGroup::printAll(stream);
|
||||
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
|
||||
StringFormatter::send(stream, F("<%c %d>\n"), tt->active ? 'Q' : 'q', tt->data.snum);
|
||||
}
|
||||
}
|
||||
|
||||
if (stream != NULL) {
|
||||
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
|
||||
StringFormatter::send(stream, F("<%c %d>\n"), tt->active ? 'Q' : 'q', tt->data.snum);
|
||||
}
|
||||
} // loop over all sensors
|
||||
} // Sensor::printAll
|
||||
void Sensor::dumpAll(Print *stream){
|
||||
|
||||
if (stream == NULL) return; // Nothing to do
|
||||
|
||||
SensorGroup::dumpAll(stream);
|
||||
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
|
||||
StringFormatter::send(stream, F("<Q %d %d %d>\n"),
|
||||
tt->data.snum, tt->data.pin,tt->data.pullUp);
|
||||
}
|
||||
} // Sensor::dumpAll
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static Function to create/find Sensor object.
|
||||
|
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "IODevice.h"
|
||||
#include "SensorGroup.h"
|
||||
|
||||
// Uncomment the following #define statement to use callback notification
|
||||
// where the driver supports it.
|
||||
@@ -81,6 +82,8 @@ public:
|
||||
static bool remove(int id);
|
||||
static void checkAll();
|
||||
static void printAll(Print *stream);
|
||||
static void dumpAll(Print* stream);
|
||||
|
||||
static unsigned long lastReadCycle; // value of micros at start of last read cycle
|
||||
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.
|
||||
|
241
Sniffer.cpp
Normal file
241
Sniffer.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include "Sniffer.h"
|
||||
#include "DIAG.h"
|
||||
//extern Sniffer *DCCSniffer;
|
||||
|
||||
static void packeterror() {
|
||||
#ifndef WIFI_LED
|
||||
#ifdef SNIFFER_LED
|
||||
digitalWrite(SNIFFER_LED,HIGH);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
static void clear_packeterror() {
|
||||
#ifndef WIFI_LED
|
||||
#ifdef SNIFFER_LED
|
||||
digitalWrite(SNIFFER_LED,LOW);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool halfbits2byte(uint16_t b, byte *dccbyte) {
|
||||
/*
|
||||
if (b!=0 && b!=0xFFFF) {
|
||||
Serial.print("[ ");
|
||||
for(int n=0; n<16; n++) {
|
||||
Serial.print(b&(1<<n)?"1":"0");
|
||||
}
|
||||
Serial.println(" ]");
|
||||
}
|
||||
*/
|
||||
for(byte n=0; n<8; n++) {
|
||||
switch (b & 0x03) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
// broken bits
|
||||
packeterror();
|
||||
return false;
|
||||
break;
|
||||
case 0x00:
|
||||
bitClear(*dccbyte, n);
|
||||
break;
|
||||
case 0x03:
|
||||
bitSet(*dccbyte, n);
|
||||
break;
|
||||
}
|
||||
b = b>>2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR blink_diag(int limit) {
|
||||
#ifndef WIFI_LED
|
||||
#ifdef SNIFFER_LED
|
||||
delay(500);
|
||||
for (int n=0 ; n<limit; n++) {
|
||||
digitalWrite(SNIFFER_LED,HIGH);
|
||||
delay(200);
|
||||
digitalWrite(SNIFFER_LED,LOW);
|
||||
delay(200);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool IRAM_ATTR cap_ISR_cb(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,void *user_data) {
|
||||
if (edata->cap_edge == MCPWM_BOTH_EDGE) {
|
||||
// should not happen at all
|
||||
// delays here might crash sketch
|
||||
blink_diag(2);
|
||||
return 0;
|
||||
}
|
||||
if (user_data) ((Sniffer *)user_data)->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
|
||||
//if (DCCSniffer) DCCSniffer->processInterrupt(edata->cap_value, edata->cap_edge == MCPWM_POS_EDGE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Sniffer::Sniffer(byte snifferpin) {
|
||||
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, snifferpin);
|
||||
// set capture edge, BIT(0) - negative edge, BIT(1) - positive edge
|
||||
// MCPWM_POS_EDGE|MCPWM_NEG_EDGE should be 3.
|
||||
//mcpwm_capture_enable(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, MCPWM_POS_EDGE|MCPWM_NEG_EDGE, 0);
|
||||
//mcpwm_isr_register(MCPWM_UNIT_0, sniffer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
|
||||
//MCPWM0.int_ena.cap0_int_ena = 1; // Enable interrupt on CAP0 signal
|
||||
|
||||
mcpwm_capture_config_t MCPWM_cap_config = { //Capture channel configuration
|
||||
.cap_edge = MCPWM_BOTH_EDGE, // according to mcpwm.h
|
||||
.cap_prescale = 1, // 1 to 256 (see .h file)
|
||||
.capture_cb = cap_ISR_cb, // user defined ISR/callback
|
||||
.user_data = (void *)this // user defined argument to callback
|
||||
};
|
||||
#ifndef WIFI_LED
|
||||
#ifdef SNIFFER_LED
|
||||
pinMode(SNIFFER_LED ,OUTPUT);
|
||||
#endif
|
||||
#endif
|
||||
blink_diag(3); // so that we know we have SNIFFER_LED
|
||||
DIAG(F("Init sniffer on pin %d"), snifferpin);
|
||||
ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &MCPWM_cap_config));
|
||||
}
|
||||
|
||||
#define SNIFFER_TIMEOUT 100L // 100 Milliseconds
|
||||
bool Sniffer::inputActive(){
|
||||
unsigned long now = millis();
|
||||
return ((now - lastendofpacket) < SNIFFER_TIMEOUT);
|
||||
}
|
||||
|
||||
#define DCC_TOO_SHORT 4000L // 4000 ticks are 50usec
|
||||
#define DCC_ONE_LIMIT 6400L // 6400 ticks are 80usec
|
||||
|
||||
void IRAM_ATTR Sniffer::processInterrupt(int32_t capticks, bool posedge) {
|
||||
byte bit = 0;
|
||||
diffticks = capticks - lastticks;
|
||||
if (lastedge != posedge) {
|
||||
if (diffticks < DCC_TOO_SHORT) {
|
||||
return;
|
||||
}
|
||||
if (diffticks < DCC_ONE_LIMIT) {
|
||||
bit = 1;
|
||||
} else {
|
||||
bit = 0;
|
||||
}
|
||||
// update state variables for next round
|
||||
lastticks = capticks;
|
||||
lastedge = posedge;
|
||||
bitfield = bitfield << (uint64_t)1;
|
||||
bitfield = bitfield + (uint64_t)bit;
|
||||
|
||||
// now the halfbit is in the bitfield. Analyze...
|
||||
|
||||
if ((bitfield & 0xFFFFFF) == 0xFFFFFC){
|
||||
// This looks at the 24 last halfbits
|
||||
// and detects a preamble if
|
||||
// 22 are ONE and 2 are ZERO which is a
|
||||
// preabmle of 11 ONES and one ZERO
|
||||
if (inpacket) {
|
||||
// if we are already inpacket here we
|
||||
// got a preamble in the middle of a
|
||||
// packet
|
||||
packeterror();
|
||||
} else {
|
||||
clear_packeterror(); // everything fine again at end of preable after good packet
|
||||
}
|
||||
currentbyte = 0;
|
||||
dcclen = 0;
|
||||
inpacket = true;
|
||||
halfbitcounter = 18; // count 18 steps from 17 to 0 and then look at the byte
|
||||
return;
|
||||
}
|
||||
if (inpacket) {
|
||||
halfbitcounter--;
|
||||
if (halfbitcounter) {
|
||||
return; // wait until we have full byte
|
||||
} else {
|
||||
// have reached end of byte
|
||||
//if (currentbyte == 2) debugfield = bitfield;
|
||||
byte twohalfbits = bitfield & 0x03;
|
||||
switch (twohalfbits) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
// broken bits
|
||||
inpacket = false;
|
||||
packeterror();
|
||||
return;
|
||||
break;
|
||||
case 0x00:
|
||||
case 0x03:
|
||||
// byte end
|
||||
uint16_t b = (bitfield & 0x3FFFF)>>2; // take 18 halfbits and use 16 of them
|
||||
if (!halfbits2byte(b, dccbytes + currentbyte)) {
|
||||
// broken halfbits
|
||||
inpacket = false;
|
||||
packeterror();
|
||||
return;
|
||||
}
|
||||
if (twohalfbits == 0x03) { // end of packet marker
|
||||
inpacket = false;
|
||||
dcclen = currentbyte+1;
|
||||
debugfield = bitfield;
|
||||
// We have something we want to give to the outpacket queue
|
||||
// Check length of outpacket queue
|
||||
if (outpacket.size() > 3) {
|
||||
// not good, these should have been fetched
|
||||
// the arbitraty number to check is THREE (see the holy grail)
|
||||
// blink_diag(1); DO NOT DO THIS HERE -> will crash
|
||||
packeterror(); // or what to do better?
|
||||
// take emergency action:
|
||||
while (!outpacket.empty()) {
|
||||
outpacket.pop_front();
|
||||
}
|
||||
}
|
||||
lastendofpacket = millis();
|
||||
DCCPacket temppacket(dccbytes, dcclen);
|
||||
if (!(temppacket == prevpacket)) {
|
||||
// we have something new to offer to the fetch routine
|
||||
// put it into the outpacket queue
|
||||
outpacket.push_back(temppacket);
|
||||
prevpacket = temppacket;
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
halfbitcounter = 18;
|
||||
currentbyte++; // everything done for this end of byte
|
||||
if (currentbyte >= MAXDCCPACKETLEN) {
|
||||
inpacket = false; // this is an error because we should have retured above
|
||||
packeterror(); // when endof packet marker was active
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // lastedge == posedge
|
||||
// this should not happen, check later
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
static void IRAM_ATTR sniffer_isr_handler(void *) {
|
||||
DCCSniffer.processInterrupt();
|
||||
}
|
||||
*/
|
||||
#endif // ESP32
|
76
Sniffer.h
Normal file
76
Sniffer.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* © 2025 Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <Arduino.h>
|
||||
#include <list>
|
||||
#include "driver/mcpwm.h"
|
||||
#include "soc/mcpwm_struct.h"
|
||||
#include "soc/mcpwm_reg.h"
|
||||
|
||||
#define MAXDCCPACKETLEN 8
|
||||
#include "DCCPacket.h"
|
||||
|
||||
class Sniffer {
|
||||
public:
|
||||
Sniffer(byte snifferpin);
|
||||
void IRAM_ATTR processInterrupt(int32_t capticks, bool posedge);
|
||||
inline int32_t getTicks() {
|
||||
noInterrupts();
|
||||
int32_t i = diffticks;
|
||||
interrupts();
|
||||
return i;
|
||||
};
|
||||
inline int64_t getDebug() {
|
||||
noInterrupts();
|
||||
int64_t i = debugfield;
|
||||
interrupts();
|
||||
return i;
|
||||
};
|
||||
inline DCCPacket fetchPacket() {
|
||||
// if there is no new data, this will create a
|
||||
// packet with length 0 (which is no packet)
|
||||
DCCPacket p;
|
||||
noInterrupts();
|
||||
if (!outpacket.empty()) {
|
||||
p = outpacket.front();
|
||||
outpacket.pop_front();
|
||||
}
|
||||
interrupts();
|
||||
return p;
|
||||
};
|
||||
bool inputActive();
|
||||
private:
|
||||
// keep these vars in processInterrupt only
|
||||
uint64_t bitfield = 0;
|
||||
uint64_t debugfield = 0;
|
||||
int32_t diffticks;
|
||||
int32_t lastticks;
|
||||
bool lastedge;
|
||||
byte currentbyte = 0;
|
||||
byte dccbytes[MAXDCCPACKETLEN];
|
||||
byte dcclen = 0;
|
||||
bool inpacket = false;
|
||||
// these vars are used as interface to other parts of sniffer
|
||||
byte halfbitcounter = 0;
|
||||
std::list<DCCPacket> outpacket;
|
||||
DCCPacket prevpacket;
|
||||
volatile unsigned long lastendofpacket = 0; // timestamp millis
|
||||
|
||||
};
|
||||
#endif
|
@@ -29,6 +29,7 @@ bool Diag::ETHERNET=false;
|
||||
bool Diag::LCN=false;
|
||||
bool Diag::RAILCOM=false;
|
||||
bool Diag::WEBSOCKET=false;
|
||||
bool Diag::SNIFFER=false;
|
||||
|
||||
|
||||
|
||||
|
@@ -32,7 +32,7 @@ class Diag {
|
||||
static bool LCN;
|
||||
static bool RAILCOM;
|
||||
static bool WEBSOCKET;
|
||||
|
||||
static bool SNIFFER;
|
||||
};
|
||||
|
||||
class StringFormatter
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* © 2022-2025 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023-2024 Paul M. Antoine
|
||||
* © 2024 Herb Morton
|
||||
* © 2024-2025 Herb Morton
|
||||
* © 2023 Colin Murdoch
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
MotorDriver * TrackManager::track[MAX_TRACKS] = { NULL };
|
||||
int16_t TrackManager::trackDCAddr[MAX_TRACKS] = { 0 };
|
||||
int16_t TrackManager::trackPwrMA[MAX_TRACKS] = { 0 };
|
||||
|
||||
int8_t TrackManager::lastTrack=-1;
|
||||
bool TrackManager::progTrackSyncMain=false;
|
||||
@@ -197,7 +198,7 @@ void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
|
||||
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr, bool offAtChange) {
|
||||
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
|
||||
|
||||
// Remember track mode we came from for later
|
||||
@@ -371,10 +372,9 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
|
||||
#endif
|
||||
#endif
|
||||
// Turn off power if we changed the mode of this track
|
||||
if (mode != oldmode) {
|
||||
if (mode != oldmode && offAtChange) {
|
||||
track[trackToSet]->setPower(POWERMODE::OFF);
|
||||
}
|
||||
|
||||
streamTrackState(NULL,trackToSet);
|
||||
//DIAG(F("TrackMode=%d"),mode);
|
||||
return true;
|
||||
@@ -451,7 +451,7 @@ const FSH* TrackManager::getModeName(TRACK_MODE tm) {
|
||||
if(tm & TRACK_MODIFIER_AUTO)
|
||||
modename=F("MAIN A");
|
||||
else if (tm & TRACK_MODIFIER_INV)
|
||||
modename=F("MAIN I>\n");
|
||||
modename=F("MAIN I");
|
||||
else
|
||||
modename=F("MAIN");
|
||||
}
|
||||
@@ -646,6 +646,37 @@ void TrackManager::reportCurrent(Print* stream) {
|
||||
StringFormatter::send(stream,F(">\n"));
|
||||
}
|
||||
|
||||
void TrackManager::reportCurrentLCD(uint8_t display, byte row) {
|
||||
int16_t trackPwrTotalMA = 0;
|
||||
FOR_EACH_TRACK(t) {
|
||||
bool pstate = TrackManager::isPowerOn(t); // checks if power is on or off
|
||||
TRACK_MODE tMode=(TrackManager::getMode(t)); // gets to current power mode
|
||||
int16_t DCAddr=(TrackManager::returnDCAddr(t));
|
||||
|
||||
if (pstate) { // if power is on do this section
|
||||
trackPwrMA[t]=(3*trackPwrMA[t]>>2) + ((track[t]->getPower()==POWERMODE::OVERLOAD) ? -1 :
|
||||
track[t]->raw2mA(track[t]->getCurrentRaw(false)));
|
||||
trackPwrTotalMA += trackPwrMA[t];
|
||||
if (tMode & TRACK_MODE_DC) { // Test if track is in DC or DCX mode
|
||||
SCREEN(display, row+t, F("%c: %S %d %dmA"), t+'A', (TrackManager::getModeName(tMode)),DCAddr, trackPwrMA[t]>>2);
|
||||
}
|
||||
else { // formats without DCAddress
|
||||
SCREEN(display, row+t, F("%c: %S %dmA"), t+'A', (TrackManager::getModeName(tMode)), trackPwrMA[t]>>2);
|
||||
}
|
||||
}
|
||||
else { // if power is off do this section
|
||||
trackPwrMA[t] = 0;
|
||||
if (tMode & TRACK_MODE_DC) { // DC / DCX
|
||||
SCREEN(display, row+t, F("%c: %S %d"), t+'A', (TrackManager::getModeName(tMode)),DCAddr);
|
||||
}
|
||||
else { // Not DC or DCX
|
||||
SCREEN(display, row+t, F("%c: %S"), t+'A', (TrackManager::getModeName(tMode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
SCREEN(display, row+lastTrack+1, F("%d Districts %dmA"), lastTrack+1, trackPwrTotalMA>>2);
|
||||
}
|
||||
|
||||
void TrackManager::reportGauges(Print* stream) {
|
||||
StringFormatter::send(stream,F("<jG"));
|
||||
FOR_EACH_TRACK(t) {
|
||||
@@ -669,9 +700,9 @@ void TrackManager::setJoin(bool joined) {
|
||||
FOR_EACH_TRACK(t) {
|
||||
if (track[t]->getMode() & TRACK_MODE_PROG) { // find PROG track
|
||||
tempProgTrack = t; // remember PROG track
|
||||
setTrackMode(t, TRACK_MODE_MAIN);
|
||||
// setPower() of the track called after
|
||||
// seperately after setJoin() instead
|
||||
setTrackMode(t, TRACK_MODE_MAIN, 0, false); // 0 = no DC loco; false = do not turn off pwr
|
||||
// then in some cases setPower() is called
|
||||
// seperately after the setJoin() as well
|
||||
break; // there is only one prog track, done
|
||||
}
|
||||
}
|
||||
@@ -679,9 +710,7 @@ void TrackManager::setJoin(bool joined) {
|
||||
if (tempProgTrack != MAX_TRACKS+1) {
|
||||
// setTrackMode defaults to power off, so we
|
||||
// need to preserve that state.
|
||||
POWERMODE tPTmode = track[tempProgTrack]->getPower(); // get current power status of this track
|
||||
setTrackMode(tempProgTrack, TRACK_MODE_PROG); // set track mode back to prog
|
||||
track[tempProgTrack]->setPower(tPTmode); // set power status as it was before
|
||||
setTrackMode(tempProgTrack, TRACK_MODE_PROG, 0, false); // 0 = no DC loco; false = do not turn off pwr
|
||||
tempProgTrack = MAX_TRACKS+1;
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
* © 2022 Chris Harlow
|
||||
* © 2022-2024 Harald Barth
|
||||
* © 2023 Colin Murdoch
|
||||
* © 2025 Herb Morton
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -71,7 +72,7 @@ class TrackManager {
|
||||
static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, mode);}
|
||||
|
||||
static const int16_t MAX_TRACKS=8;
|
||||
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
|
||||
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0, bool offAtChange=true);
|
||||
static bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
|
||||
static void loop();
|
||||
static POWERMODE getMainPower();
|
||||
@@ -87,6 +88,7 @@ class TrackManager {
|
||||
static void sampleCurrent();
|
||||
static void reportGauges(Print* stream);
|
||||
static void reportCurrent(Print* stream);
|
||||
static void reportCurrentLCD(uint8_t display, byte row);
|
||||
static void reportObsoleteCurrent(Print* stream);
|
||||
static void streamTrackState(Print* stream, byte t);
|
||||
static bool isPowerOn(byte t);
|
||||
@@ -113,6 +115,7 @@ class TrackManager {
|
||||
static void applyDCSpeed(byte t);
|
||||
|
||||
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC
|
||||
static int16_t trackPwrMA[MAX_TRACKS]; // for <JL ..> command
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
static byte tempProgTrack; // holds the prog track number during join
|
||||
#endif
|
||||
|
@@ -1,41 +0,0 @@
|
||||
// myCMRI.h to be included in myAutomation.h
|
||||
|
||||
// see myHal.cpp_example.txt for explanation of parameters
|
||||
|
||||
#include "IO_CMRI.h" // CMRI nodes
|
||||
// in config.h #define ARDUINOCMRI_COMPATIBLE
|
||||
|
||||
// define CMRI bus
|
||||
HAL(CMRIbus, 0, Serial5, 9600, 500, 51)
|
||||
|
||||
// define CMRI nodes
|
||||
HAL(CMRInode, 900, 72, 0, 4, 'M')
|
||||
|
||||
// define the pins - this is S-MINI emulation 24/48 I/O
|
||||
// 16 of 24 input pins
|
||||
//JMRI_SENSOR(900,16) // broadcast <Q/q sensor for state changes
|
||||
// ... these pins do not need to be defined unless needed for JMRI
|
||||
|
||||
// 16 turnouts require definition with vPin, id
|
||||
// define as PIN_TURNOUT in DCC-EX -- the servo parameters are done within the CMRI sketch
|
||||
PIN_TURNOUT(924, 924, "Pin 0 on PCA module")
|
||||
PIN_TURNOUT(925, 925, "Pin 1 on PCA module")
|
||||
PIN_TURNOUT(931, 931, "Pin 7 on PCA module")
|
||||
// ... thru 939
|
||||
|
||||
// define 16 or 32 pins for digital outputs
|
||||
//PARSE("<Z 940 940 0>")
|
||||
// ... these pins do not need to be defined unless needed for JMRI
|
||||
|
||||
// a testing sequence
|
||||
// pin 941 is wired to pin 907. LED on pin 943
|
||||
// pin 942 is wired to pin 911.
|
||||
// use commands <z 941> <z -941> etc.
|
||||
AUTOSTART
|
||||
SEQUENCE(99)
|
||||
AT(907) SET(943)
|
||||
AT(911) RESET(943)
|
||||
FOLLOW(99)
|
||||
|
||||
// CMRI sketch used for testing available here
|
||||
// https://www.trainboard.com/highball/index.php?threads/24-in-48-out-card-for-jmri.116454/page-2#post-1141569
|
@@ -146,7 +146,14 @@ void halSetup() {
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, 40);
|
||||
|
||||
// Alternative form to initialize 8 pins as output
|
||||
// INT pin -1, when INT is not used
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, -1, 255); 8 pins High
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0); 8 pins Low
|
||||
//PCF8574::create(200, 8, 0x23, -1, 0b11000010);
|
||||
// pins listed sequentially from 7 to 0
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a PCF8575 16-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
|
37
myTrackStatus.example.h
Normal file
37
myTrackStatus.example.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// ************* OLED JL Display Track mA Amperage **************** //
|
||||
// by Herb Morton [Ash] March 23, 2025
|
||||
// Colin Murdoch [ColinM]
|
||||
|
||||
// Inside your config.h file First edit OLED max char rows set to 17
|
||||
// #define MAX_CHARACTER_ROWS 17
|
||||
|
||||
// myAutomation.h
|
||||
// Reporting power status and mA for each track on the LCD
|
||||
HAL(Bitmap,8236,1) // create flag 8236
|
||||
AUTOSTART DELAY(5000)
|
||||
ROUTE("TRACKSTATUS"_hk, "Resume/Pause JL Display")
|
||||
IF(8236)
|
||||
RESET(8236)
|
||||
ROUTE_CAPTION("TRACKSTATUS"_hk, "Paused") ROUTE_INACTIVE("TRACKSTATUS"_hk)
|
||||
PRINT("Pause JL Display")
|
||||
SCREEN(0, 8, "Track status paused")
|
||||
SCREEN(0, 9, "")
|
||||
SCREEN(0,10, "") // several blank lines as needed
|
||||
SCREEN(0,11, "")
|
||||
SCREEN(0,12, "")
|
||||
SCREEN(0,13, "")
|
||||
SCREEN(0,14, "")
|
||||
SCREEN(0,15, "")
|
||||
SCREEN(0,16, "")
|
||||
DONE ENDIF
|
||||
SET(8236)
|
||||
ROUTE_CAPTION("TRACKSTATUS"_hk, "Running") ROUTE_ACTIVE("TRACKSTATUS"_hk)
|
||||
PRINT("Resume JL Display")
|
||||
FOLLOW("PAUSETRACKSTATUS"_hk)
|
||||
SEQUENCE("PAUSETRACKSTATUS"_hk)
|
||||
PARSE("<JL 0 8>") // screen 0 start on line 8
|
||||
PRINT("\n")
|
||||
DELAY(3000)
|
||||
IF(8236) FOLLOW("PAUSETRACKSTATUS"_hk) ENDIF
|
||||
DONE
|
||||
// ************ End OLED JL Display Track mA Amperage ************** //
|
@@ -1,5 +1,5 @@
|
||||
ECHO ON
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\arduino\sketches\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||
FOR /F "delims=" %%i IN ('dir %TMP%\CommandStation-EX.ino.elf /s /b /o-D') DO SET ELF=%%i
|
||||
SET DUMP=%TEMP%\OBJDUMP.txt
|
||||
echo Most recent subfolder: %ELF% >%DUMP%
|
||||
|
||||
|
@@ -32,7 +32,7 @@ upload_protocol = sam-ba
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:samd21-zero-usb]
|
||||
platform = atmelsam
|
||||
@@ -42,7 +42,7 @@ upload_protocol = sam-ba
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:Arduino-M0]
|
||||
platform = atmelsam
|
||||
@@ -51,7 +51,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
|
||||
[env:mega2560-debug]
|
||||
platform = atmelavr
|
||||
@@ -63,7 +63,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES
|
||||
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES ${env.build_flags}
|
||||
|
||||
[env:mega2560-no-HAL]
|
||||
platform = atmelavr
|
||||
@@ -75,7 +75,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DIO_NO_HAL
|
||||
build_flags = -DIO_NO_HAL ${env.build_flags}
|
||||
|
||||
[env:mega2560-I2C-wire]
|
||||
platform = atmelavr
|
||||
@@ -87,7 +87,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -DI2C_USE_WIRE
|
||||
build_flags = -DI2C_USE_WIRE ${env.build_flags}
|
||||
|
||||
[env:mega2560]
|
||||
platform = atmelavr
|
||||
@@ -106,7 +106,7 @@ lib_ignore = WiFi101
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags =
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:mega2560-eth]
|
||||
platform = atmelavr
|
||||
@@ -123,6 +123,7 @@ lib_ignore = WiFi101
|
||||
WiFiNINA_Generic
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:mega328]
|
||||
platform = atmelavr
|
||||
@@ -134,6 +135,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags= ${env.build_flags}
|
||||
|
||||
[env:unowifiR2]
|
||||
platform = atmelmegaavr
|
||||
@@ -145,7 +147,7 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
|
||||
build_flags = ${env.build_flags} "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
|
||||
|
||||
[env:nanoevery]
|
||||
platform = atmelmegaavr
|
||||
@@ -158,7 +160,7 @@ lib_deps =
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_speed = 19200
|
||||
build_flags =
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:uno]
|
||||
platform = atmelavr
|
||||
@@ -167,7 +169,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -mcall-prologues
|
||||
build_flags = -mcall-prologues ${env.build_flags}
|
||||
|
||||
[env:nano]
|
||||
platform = atmelavr
|
||||
@@ -177,7 +179,7 @@ framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
build_flags = -mcall-prologues
|
||||
build_flags = -mcall-prologues ${env.build_flags}
|
||||
|
||||
[env:ESP32]
|
||||
; Lock version to 6.7.0 as that is
|
||||
@@ -188,7 +190,7 @@ platform = espressif32 @ 6.7.0
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17
|
||||
build_flags = -std=c++17 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -197,7 +199,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f411re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -206,7 +208,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -218,7 +220,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f401re
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -243,7 +245,7 @@ platform = ststm32 @ 19.0.0
|
||||
board = nucleo_f446ze
|
||||
framework = arduino
|
||||
lib_deps = ${env.lib_deps}
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
|
||||
@@ -274,7 +276,7 @@ lib_ignore = WiFi101
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
@@ -293,7 +295,7 @@ lib_ignore = WiFi101
|
||||
WiFiEspAT
|
||||
WiFiMulti_Generic
|
||||
WiFiNINA_Generic
|
||||
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DCUSTOM_PERIPHERAL_PINS
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags} -DCUSTOM_PERIPHERAL_PINS
|
||||
monitor_speed = 115200
|
||||
monitor_echo = yes
|
||||
upload_protocol = stlink
|
||||
@@ -302,7 +304,7 @@ upload_protocol = stlink
|
||||
platform = teensy
|
||||
board = teensy31
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -310,7 +312,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy35
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -318,7 +320,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy36
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -326,7 +328,7 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy40
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore = NativeEthernet
|
||||
|
||||
@@ -334,6 +336,6 @@ lib_ignore = NativeEthernet
|
||||
platform = teensy
|
||||
board = teensy41
|
||||
framework = arduino
|
||||
build_flags = -std=c++17 -Os -g2
|
||||
build_flags = -std=c++17 -Os -g2 ${env.build_flags}
|
||||
lib_deps = ${env.lib_deps}
|
||||
lib_ignore =
|
||||
|
22
version.h
22
version.h
@@ -3,8 +3,26 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
#define VERSION "5.5.22"
|
||||
// - CMRI files included
|
||||
#define VERSION "5.5.37"
|
||||
// 5.5.37 - Bugfix: Keep power status of track when doing join/unjoin, new keep power option for setTrackMode()
|
||||
// 5.5.36 - make more diag conditional
|
||||
// - Bugfix: SSD1309 OLED controllers
|
||||
// 5.5.35 - BUGFIX DCC Packet Queue timing leak with single loco
|
||||
// 5.5.34 - STM32: Remove I2C interrupt blocking waveform interrupt
|
||||
// - Bugfix: Negative route Ids
|
||||
// 5.5.33 - Fix CONFIG_SERVO when default PCA9685 definition used.
|
||||
// 5.5.32 - Feature: Enable sniffer on CSB-1
|
||||
// 5.5.31 - <JL screen startRow> track status command
|
||||
// - myTrackStatus.example.h added
|
||||
// - Provide for WiFi false on ESP32
|
||||
// 5.5.30 - EXRAIL </> shows why tasks are waiting
|
||||
// 5.5.29 - Resolved compiler warnings
|
||||
// 5.5.28 - DCC Queue memory leak fix
|
||||
// 5.5.27 - PCF8574 output pin initialization parameter
|
||||
// 5.5.26 - PCA9554 and TCA9554/9534 I2C 8-bit GPIO expander drivers
|
||||
// 5.2.25 - IO_Bitmap and assicated Exrail macros
|
||||
// 5.5.24 - SensorCAM in I2C scan and automatically setClock
|
||||
// 5.5.23 - Reminder loop Idle packet optimization
|
||||
// 5.5.22 - (5.4.9) Handle non-compliant decoders returning 255 for cv 20 and confusing <R> with bad consist addresses.
|
||||
// - DCC 5mS gap to same loco DCC packet restriction
|
||||
// - Catch up MASTER for ESP32 IDF version check
|
||||
|
Reference in New Issue
Block a user