1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-12-23 21:01:25 +01:00

Merge branch 'Broadcast' into EXRAILPlus

This commit is contained in:
Asbelos 2021-12-15 19:59:59 +00:00
commit 2ddf583fbc
25 changed files with 643 additions and 192 deletions

View File

@ -16,16 +16,108 @@
* 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>
#include "CommandDistributor.h"
#include "SerialManager.h"
#include "WiThrottle.h"
#include "DIAG.h"
#include "defines.h"
#include "DCCWaveform.h"
#include "DCC.h"
DCCEXParser * CommandDistributor::parser=0;
const byte NO_CLIENT=255;
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * streamer) {
if (buffer[0] == '<') {
if (!parser) parser = new DCCEXParser();
parser->parse(streamer, buffer, streamer);
}
else WiThrottle::getThrottle(clientId)->parse(streamer, buffer);
RingStream * CommandDistributor::ring=0;
byte CommandDistributor::ringClient=NO_CLIENT;
CommandDistributor::clientType CommandDistributor::clients[8]={
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
RingStream * CommandDistributor::broadcastBufferWriter=new RingStream(100);
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
ring=stream;
ringClient=stream->peekTargetMark();
if (buffer[0] == '<') {
clients[clientId]=COMMAND_TYPE;
DCCEXParser::parse(stream, buffer, ring);
}
else {
clients[clientId]=WITHROTTLE_TYPE;
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
}
ringClient=NO_CLIENT;
}
void CommandDistributor::forget(byte clientId) {
clients[clientId]=NONE_TYPE;
}
void CommandDistributor::broadcast() {
broadcastBufferWriter->write((byte)'\0');
/* Boadcast to Serials */
SerialManager::broadcast(broadcastBufferWriter);
#if defined(WIFI_ON) | defined(ETHERNET_ON)
// If we are broadcasting from a wifi/eth process we need to complete its output
// before merging broadcasts in the ring, then reinstate it in case
// the process continues to output to its client.
if (ringClient!=NO_CLIENT) ring->commit();
/* loop through ring clients */
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
if (clients[clientId]==NONE_TYPE) continue;
ring->mark(clientId);
broadcastBufferWriter->printBuffer(ring);
ring->commit();
}
if (ringClient!=NO_CLIENT) ring->mark(ringClient);
#endif
broadcastBufferWriter->flush();
}
void CommandDistributor::broadcastSensor(int16_t id, bool on ) {
StringFormatter::send(broadcastBufferWriter,F("<%c %d>\n"), on?'Q':'q', id);
broadcast();
}
void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
// The string below contains serial and Withrottle protocols which should
// be safe for both types.
StringFormatter::send(broadcastBufferWriter,F("<H %d %d>\n"),id, !isClosed);
#if defined(WIFI_ON) | defined(ETHERNET_ON)
StringFormatter::send(broadcastBufferWriter,F("PTA%c%d\n"), isClosed?'2':'4', id);
#endif
broadcast();
}
void CommandDistributor::broadcastLoco(byte slot) {
DCC::LOCO * sp=&DCC::speedTable[slot];
StringFormatter::send(broadcastBufferWriter,F("<l %d %d %d %l>\n"),
sp->loco,slot,sp->speedCode,sp->functions);
broadcast();
#if defined(WIFI_ON) | defined(ETHERNET_ON)
WiThrottle::markForBroadcast(sp->loco);
#endif
}
void CommandDistributor::broadcastPower() {
const FSH * reason;
bool main=DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON;
bool prog=DCCWaveform::progTrack.getPowerMode()==POWERMODE::ON;
bool join=DCCWaveform::progTrackSyncMain;
if (main && prog && join) reason=F("1 JOIN");
else if (main && prog) reason=F("1");
else if (main) reason=F("1 MAIN");
else if (prog) reason=F("1 PROG");
else reason=F("0");
StringFormatter::send(broadcastBufferWriter,
F("<p%S>\nPPA%c\n"),reason, main?'1':'0');
LCD(2,F("Power %S"),reason);
broadcast();
}

View File

@ -24,9 +24,21 @@
class CommandDistributor {
public :
static void parse(byte clientId,byte* buffer, RingStream * streamer);
static void parse(byte clientId,byte* buffer, RingStream * ring);
static void broadcastLoco(byte slot);
static void broadcastSensor(int16_t id, bool value);
static void broadcastTurnout(int16_t id, bool isClosed);
static void broadcastPower();
static void forget(byte clientId);
private:
static DCCEXParser * parser;
static void broadcast();
static RingStream * ring;
static RingStream * broadcastBufferWriter;
static byte ringClient;
// each bit in broadcastlist = 1<<clientid
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE};
static clientType clients[8];
};
#endif

View File

@ -45,11 +45,12 @@
*/
#include "DCCEX.h"
// Create a serial command parser for the USB connection,
// This supports JMRI or manual diagnostics and commands
// to be issued from the USB serial console.
DCCEXParser serialParser;
#ifdef WIFI_WARNING
#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED
#endif
#ifdef ETHERNET_WARNING
#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED
#endif
void setup()
{
@ -57,7 +58,7 @@ void setup()
// Responsibility 1: Start the usb connection for diagnostics
// This is normally Serial but uses SerialUSB on a SAMD processor
Serial.begin(115200);
SerialManager::init();
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
@ -92,7 +93,7 @@ void setup()
// 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.
#if __has_include ( "mySetup.h")
#define SETUP(cmd) serialParser.parse(F(cmd))
#define SETUP(cmd) DCCEXParser::parse(F(cmd))
#include "mySetup.h"
#undef SETUP
#endif
@ -114,7 +115,7 @@ void loop()
DCC::loop();
// Responsibility 2: handle any incoming commands on USB connection
serialParser.loop(Serial);
SerialManager::loop();
// Responsibility 3: Optionally handle any incoming WiFi traffic
#if WIFI_ON
@ -135,6 +136,8 @@ void loop()
// Handle/update IO devices.
IODevice::loop();
Sensor::checkAll(); // Update and print changes
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop

24
DCC.cpp
View File

@ -28,6 +28,7 @@
#include "FSH.h"
#include "IODevice.h"
#include "RMFT2.h"
#include "CommandDistributor.h"
// This module is responsible for converting API calls into
// messages to be sent to the waveform generator.
@ -179,14 +180,17 @@ void DCC::setFn( int cab, int16_t functionNumber, bool on) {
// Take care of functions:
// Set state of function
unsigned long previous=speedTable[reg].functions;
unsigned long funcmask = (1UL<<functionNumber);
if (on) {
if (on) {
speedTable[reg].functions |= funcmask;
} else {
speedTable[reg].functions &= ~funcmask;
}
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
return;
if (speedTable[reg].functions != previous) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
}
}
// Change function according to how button was pressed,
@ -219,6 +223,7 @@ int DCC::changeFn( int cab, int16_t functionNumber, bool pressed) {
funcstate = (speedTable[reg].functions & funcmask)? 1 : 0;
}
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
return funcstate;
}
@ -243,6 +248,12 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
flags |= groupMask;
}
uint16_t DCC::getFunctionMap(int cab) {
if (cab<=0) return 0; // unknown pretend all functions off
int reg = lookupSpeedTable(cab);
return (reg<0)?0:speedTable[reg].functions;
}
void DCC::setAccessory(int address, byte number, bool activate) {
#ifdef DIAG_IO
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, number, activate);
@ -671,6 +682,7 @@ int DCC::lookupSpeedTable(int locoId) {
speedTable[reg].speedCode=128; // default direction forward
speedTable[reg].groupFlags=0;
speedTable[reg].functions=0;
CommandDistributor::broadcastLoco(reg);
}
return reg;
}
@ -681,13 +693,17 @@ void DCC::updateLocoReminder(int loco, byte speedCode) {
// broadcast stop/estop but dont change direction
for (int reg = 0; reg < MAX_LOCOS; reg++) {
speedTable[reg].speedCode = (speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
CommandDistributor::broadcastLoco(reg);
}
return;
}
// determine speed reg for this loco
int reg=lookupSpeedTable(loco);
if (reg>=0) speedTable[reg].speedCode = speedCode;
if (reg>=0) {
speedTable[reg].speedCode = speedCode;
CommandDistributor::broadcastLoco(reg);
}
}
DCC::LOCO DCC::speedTable[MAX_LOCOS];

6
DCC.h
View File

@ -101,6 +101,7 @@ public:
static void setFn(int cab, int16_t functionNumber, bool on);
static int changeFn(int cab, int16_t functionNumber, bool pressed);
static int getFn(int cab, int16_t functionNumber);
static uint16_t getFunctionMap(int cab);
static void updateGroupflags(byte &flags, int16_t functionNumber);
static void setAccessory(int aAdd, byte aNum, bool activate);
static bool writeTextPacket(byte *b, int nBytes);
@ -134,7 +135,6 @@ public:
return ackRetryPSum;
};
private:
struct LOCO
{
int loco;
@ -142,6 +142,9 @@ private:
byte groupFlags;
unsigned long functions;
};
static LOCO speedTable[MAX_LOCOS];
private:
static byte joinRelay;
static byte loopStatus;
static void setThrottle2(uint16_t cab, uint8_t speedCode);
@ -152,7 +155,6 @@ private:
static FSH *shieldName;
static byte globalSpeedsteps;
static LOCO speedTable[MAX_LOCOS];
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
static int lookupSpeedTable(int locoId);

View File

@ -29,6 +29,7 @@
#include "DCC.h"
#include "DIAG.h"
#include "DCCEXParser.h"
#include "SerialManager.h"
#include "version.h"
#include "WifiInterface.h"
#if ETHERNET_ON == true

View File

@ -28,7 +28,7 @@
#include "GITHUB_SHA.h"
#include "version.h"
#include "defines.h"
#include "CommandDistributor.h"
#include "EEStore.h"
#include "DIAG.h"
#include <avr/wdt.h>
@ -75,15 +75,12 @@ const int16_t HASH_KEYWORD_HAL = 10853;
const int16_t HASH_KEYWORD_SHOW = -21309;
const int16_t HASH_KEYWORD_ANIN = -10424;
const int16_t HASH_KEYWORD_ANOUT = -26399;
#ifdef HAS_ENOUGH_MEMORY
const int16_t HASH_KEYWORD_WIFI = -5583;
const int16_t HASH_KEYWORD_ETHERNET = -30767;
const int16_t HASH_KEYWORD_WIT = 31594;
#endif
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy;
Print *DCCEXParser::stashStream = NULL;
RingStream *DCCEXParser::stashRingStream = NULL;
byte DCCEXParser::stashTarget=0;
@ -94,44 +91,6 @@ byte DCCEXParser::stashTarget=0;
// calls the corresponding DCC api.
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.
DCCEXParser::DCCEXParser() {}
void DCCEXParser::flush()
{
if (Diag::CMD)
DIAG(F("Buffer flush"));
bufferLength = 0;
inCommandPayload = false;
}
void DCCEXParser::loop(Stream &stream)
{
while (stream.available())
{
if (bufferLength == MAX_BUFFER)
{
flush();
}
char ch = stream.read();
if (ch == '<')
{
inCommandPayload = true;
bufferLength = 0;
buffer[0] = '\0';
}
else if (ch == '>')
{
buffer[bufferLength] = '\0';
parse(&stream, buffer, NULL); // Parse this (No ringStream for serial)
inCommandPayload = false;
break;
}
else if (inCommandPayload)
{
buffer[bufferLength++] = ch;
}
}
Sensor::checkAll(&stream); // Update and print changes
}
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
{
@ -270,6 +229,7 @@ void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
// Parse an F() string
void DCCEXParser::parse(const FSH * cmd) {
DIAG(F("SETUP(\"%S\")"),cmd);
int size=strlen_P((char *)cmd)+1;
char buffer[size];
strcpy_P(buffer,(char *)cmd);
@ -338,10 +298,9 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
break; // invalid direction code
DCC::setThrottle(cab, tspeed, direction);
if (params == 4)
if (params == 4) // send obsolete format T response
StringFormatter::send(stream, F("<T %d %d %d>\n"), p[0], p[2], p[3]);
else
StringFormatter::send(stream, F("<O>\n"));
// speed change will be broadcast anyway in new <l > format
return;
}
case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>
@ -467,59 +426,65 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
}
break;
case '1': // POWERON <1 [MAIN|PROG]>
case '0': // POWEROFF <0 [MAIN | PROG] >
if (params > 1)
break;
{
POWERMODE mode = opcode == '1' ? POWERMODE::ON : POWERMODE::OFF;
DCC::setProgTrackSyncMain(false); // Only <1 JOIN> will set this on, all others set it off
if (params == 0 ||
(MotorDriver::commonFaultPin && p[0] != HASH_KEYWORD_JOIN)) // commonFaultPin prevents individual track handling
{
DCCWaveform::mainTrack.setPowerMode(mode);
DCCWaveform::progTrack.setPowerMode(mode);
if (mode == POWERMODE::OFF)
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
StringFormatter::send(stream, F("<p%c>\n"), opcode);
LCD(2, F("p%c"), opcode);
return;
}
switch (p[0])
{
case HASH_KEYWORD_MAIN:
DCCWaveform::mainTrack.setPowerMode(mode);
StringFormatter::send(stream, F("<p%c MAIN>\n"), opcode);
LCD(2, F("p%c MAIN"), opcode);
return;
case HASH_KEYWORD_PROG:
DCCWaveform::progTrack.setPowerMode(mode);
if (mode == POWERMODE::OFF)
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
StringFormatter::send(stream, F("<p%c PROG>\n"), opcode);
LCD(2, F("p%c PROG"), opcode);
return;
case HASH_KEYWORD_JOIN:
DCCWaveform::mainTrack.setPowerMode(mode);
DCCWaveform::progTrack.setPowerMode(mode);
if (mode == POWERMODE::ON)
{
DCC::setProgTrackSyncMain(true);
StringFormatter::send(stream, F("<p1 JOIN>\n"), opcode);
LCD(2, F("p1 JOIN"));
}
else
{
StringFormatter::send(stream, F("<p0>\n"));
LCD(2, F("p0"));
}
return;
}
break;
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
{
bool main=false;
bool prog=false;
bool join=false;
if (params > 1) break;
if (params==0) { // <1>
main=true;
prog=true;
}
return;
else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN>
main=true;
prog=true;
join=!MotorDriver::commonFaultPin;
}
else if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN>
main=true;
}
else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG>
prog=true;
}
else break; // will reply <X>
if (main) DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
if (prog) DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
DCC::setProgTrackSyncMain(join);
CommandDistributor::broadcastPower();
return;
}
case '0': // POWEROFF <0 [MAIN | PROG] >
{
bool main=false;
bool prog=false;
if (params > 1) break;
if (params==0) { // <0>
main=true;
prog=true;
}
else if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN>
main=true;
}
else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG>
prog=true;
}
else break; // will reply <X>
if (main) DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
if (prog) {
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
}
DCC::setProgTrackSyncMain(false);
CommandDistributor::broadcastPower();
return;
}
case '!': // ESTOP ALL <!>
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
return;
@ -728,9 +693,7 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
}
if (!Turnout::setClosed(p[0], state)) return false;
// Send acknowledgement to caller if the command was not received over Serial
// (acknowledgement messages on Serial are sent by the Turnout class).
if (stream != &Serial) Turnout::printState(p[0], stream);
return true;
}

View File

@ -27,11 +27,9 @@ typedef void (*AT_COMMAND_CALLBACK)(const byte * command);
struct DCCEXParser
{
DCCEXParser();
void loop(Stream & stream);
void parse(Print * stream, byte * command, RingStream * ringStream);
void parse(const FSH * cmd);
void flush();
static void parse(Print * stream, byte * command, RingStream * ringStream);
static void parse(const FSH * cmd);
static void setFilter(FILTER_CALLBACK filter);
static void setRMFTFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
@ -40,17 +38,14 @@ struct DCCEXParser
private:
static const int16_t MAX_BUFFER=50; // longest command sent in
byte bufferLength=0;
bool inCommandPayload=false;
byte buffer[MAX_BUFFER+2];
int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
int16_t splitHexValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
static int16_t splitHexValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
bool parseT(Print * stream, int16_t params, int16_t p[]);
bool parseZ(Print * stream, int16_t params, int16_t p[]);
bool parseS(Print * stream, int16_t params, int16_t p[]);
bool parsef(Print * stream, int16_t params, int16_t p[]);
bool parseD(Print * stream, int16_t params, int16_t p[]);
static bool parseT(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
@ -61,7 +56,7 @@ struct DCCEXParser
static RingStream * stashRingStream;
static int16_t stashP[MAX_COMMAND_PARAMS];
bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
static void callback_W(int16_t result);
static void callback_B(int16_t result);
static void callback_R(int16_t result);

View File

@ -170,6 +170,7 @@ void EthernetInterface::loop()
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
if (clients[socket] && !clients[socket].connected()) {
clients[socket].stop();
CommandDistributor::forget(socket);
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
}
}

View File

@ -46,6 +46,7 @@
#include "WiThrottle.h"
#include "DCCEXParser.h"
#include "Turnouts.h"
#include "CommandDistributor.h"
// Command parsing keywords
@ -469,7 +470,7 @@ void RMFT2::driveLoco(byte speed) {
if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) {
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
Serial.println(F("<p1>")); // tell JMRI
CommandDistributor::broadcastPower();
}
DCC::setThrottle(loco,speed, forward^invert);
speedo=speed;
@ -626,7 +627,7 @@ void RMFT2::loop2() {
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
DCC::setProgTrackSyncMain(false);
Serial.println(F("<p0>")); // Tell JMRI
CommandDistributor::broadcastPower();
break;
case OPCODE_RESUME:
@ -751,7 +752,7 @@ void RMFT2::loop2() {
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
DCC::setProgTrackSyncMain(true);
Serial.println(F("<p1 JOIN>")); // Tell JMRI
CommandDistributor::broadcastPower();
break;
case OPCODE_UNJOIN:

View File

@ -103,3 +103,12 @@ bool RingStream::commit() {
_buffer[_mark]=lowByte(_count);
return true; // commit worked
}
void RingStream::flush() {
_pos_write=0;
_pos_read=0;
_buffer[0]=0;
}
void RingStream::printBuffer(Print * stream) {
_buffer[_pos_write]='\0';
stream->print((char *)_buffer);
}

View File

@ -34,7 +34,8 @@ class RingStream : public Print {
void mark(uint8_t b);
bool commit();
uint8_t peekTargetMark();
void printBuffer(Print * streamer);
void flush();
private:
int _len;
int _pos_write;

View File

@ -67,6 +67,7 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
**********************************************************************/
#include "StringFormatter.h"
#include "CommandDistributor.h"
#include "Sensors.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
@ -87,7 +88,7 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
// second part of the list is determined from by the 'firstPollSensor' pointer.
///////////////////////////////////////////////////////////////////////////////
void Sensor::checkAll(Print *stream){
void Sensor::checkAll(){
uint16_t sensorCount = 0;
#ifdef USE_NOTIFY
@ -135,10 +136,8 @@ void Sensor::checkAll(Print *stream){
readingSensor->active = readingSensor->inputState;
readingSensor->latchDelay = minReadCount; // Reset counter
if (stream != NULL) {
StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
pause = true; // Don't check any more sensors on this entry
}
CommandDistributor::broadcastSensor(readingSensor->data.snum,readingSensor->active);
pause = true; // Don't check any more sensors on this entry
}
// Move to next sensor in list.

View File

@ -75,7 +75,7 @@ public:
static Sensor *create(int id, VPIN vpin, int pullUp);
static Sensor* get(int id);
static bool remove(int id);
static void checkAll(Print *stream);
static void checkAll();
static void printAll(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.

76
SerialManager.cpp Normal file
View File

@ -0,0 +1,76 @@
/*
* © 2021, Chris Harlow. All rights reserved.
*
* This file is part of DCC++EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SerialManager.h"
#include "DCCEXParser.h"
SerialManager * SerialManager::first=NULL;
SerialManager::SerialManager(HardwareSerial * myserial) {
serial=myserial;
next=first;
first=this;
bufferLength=0;
myserial->begin(115200);
inCommandPayload=false;
}
void SerialManager::init() {
#ifdef SERIAL3_COMMANDS
new SerialManager(&Serial3);
#endif
#ifdef SERIAL2_COMMANDS
new SerialManager(&Serial2);
#endif
#ifdef SERIAL1_COMMANDS
new SerialManager(&Serial1);
#endif
new SerialManager(&Serial);
}
void SerialManager::broadcast(RingStream * ring) {
for (SerialManager * s=first;s;s=s->next) s->broadcast2(ring);
}
void SerialManager::broadcast2(RingStream * ring) {
ring->printBuffer(serial);
}
void SerialManager::loop() {
for (SerialManager * s=first;s;s=s->next) s->loop2();
}
void SerialManager::loop2() {
while (serial->available()) {
char ch = serial->read();
if (ch == '<') {
inCommandPayload = true;
bufferLength = 0;
buffer[0] = '\0';
}
else if (ch == '>') {
buffer[bufferLength] = '\0';
DCCEXParser::parse(serial, buffer, NULL);
inCommandPayload = false;
break;
}
else if (inCommandPayload) {
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) buffer[bufferLength++] = ch;
}
}
}

48
SerialManager.h Normal file
View File

@ -0,0 +1,48 @@
/*
* © 2021, Chris Harlow. All rights reserved.
*
* This file is part of DCC++EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef SerialManager_h
#define SerialManager_h
#include "Arduino.h"
#include "defines.h"
#include "RingStream.h"
#ifndef COMMAND_BUFFER_SIZE
#define COMMAND_BUFFER_SIZE 100
#endif
class SerialManager {
public:
static void init();
static void loop();
static void broadcast(RingStream * ring);
private:
static SerialManager * first;
SerialManager(HardwareSerial * myserial);
void loop2();
void broadcast2(RingStream * ring);
HardwareSerial * serial;
SerialManager * next;
byte bufferLength;
byte buffer[COMMAND_BUFFER_SIZE];
bool inCommandPayload;
};
#endif

View File

@ -26,6 +26,7 @@
#include "EEStore.h"
#endif
#include "StringFormatter.h"
#include "CommandDistributor.h"
#include "RMFT2.h"
#include "Turnouts.h"
#include "DCC.h"
@ -70,11 +71,7 @@
turnoutlistHash++;
}
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
void Turnout::printState(Print *stream) {
StringFormatter::send(stream, F("<H %d %d>\n"),
_turnoutData.id, !_turnoutData.closed);
}
// Remove nominated turnout from turnout linked list and delete the object.
/* static */ bool Turnout::remove(uint16_t id) {
@ -118,10 +115,7 @@
RMFT2::turnoutEvent(id, closeFlag);
#endif
// Send message to JMRI etc. over Serial USB. This is done here
// to ensure that the message is sent when the turnout operation
// is not initiated by a Serial command.
tt->printState(&Serial);
CommandDistributor::broadcastTurnout(id, closeFlag);
return true;
}
@ -142,8 +136,7 @@
bool ok = tt->setClosedInternal(closeFlag);
if (ok) {
turnoutlistHash++; // let withrottle know something changed
#ifndef DISABLE_EEPROM
// Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress
// is always zero for LCN turnouts.
@ -155,10 +148,8 @@
RMFT2::turnoutEvent(id, closeFlag);
#endif
// Send message to JMRI etc. over Serial USB. This is done here
// to ensure that the message is sent when the turnout operation
// is not initiated by a Serial command.
tt->printState(&Serial);
// Send message to JMRI etc.
CommandDistributor::broadcastTurnout(id, closeFlag);
}
return ok;
}
@ -218,12 +209,6 @@
return tt;
}
#endif
// Display, on the specified stream, the current state of the turnout (1=thrown or 0=closed).
/* static */ void Turnout::printState(uint16_t id, Print *stream) {
Turnout *tt = get(id);
if (tt) tt->printState(stream);
}
/*************************************************************************************
* ServoTurnout - Turnout controlled by servo device.

View File

@ -25,7 +25,7 @@
//#define EESTOREDEBUG
#include "Arduino.h"
#include "IODevice.h"
#include "StringFormatter.h"
// Turnout type definitions
enum {
@ -165,10 +165,10 @@ public:
#endif
static void printAll(Print *stream) {
for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout)
tt->printState(stream);
StringFormatter::send(stream, F("<H %c %c>\n"),tt->getId(), tt->isThrown());
}
static void printState(uint16_t id, Print *stream);
};

View File

@ -388,21 +388,14 @@ int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
}
void WiThrottle::loop(RingStream * stream) {
// for each WiThrottle, check the heartbeat
// for each WiThrottle, check the heartbeat and broadcast needed
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
wt->checkHeartbeat();
wt->checkHeartbeat(stream);
// TODO... any broadcasts to be done
(void)stream;
/* MUST follow this model in this loop.
* stream->mark();
* send 1 digit client id, and any data
* stream->commit()
*/
}
void WiThrottle::checkHeartbeat() {
void WiThrottle::checkHeartbeat(RingStream * stream) {
// if eStop time passed... eStop any locos still assigned to this client and then drop the connection
if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
@ -413,9 +406,54 @@ void WiThrottle::checkHeartbeat() {
}
}
delete this;
return;
}
// send any speed/direction/function changes for this loco
LOOPLOCOS('*', -1) {
if (myLocos[loco].throttle!='\0' && myLocos[loco].broadcastPending) {
stream->mark(clientid);
myLocos[loco].broadcastPending=false;
int cab=myLocos[loco].cab;
char lors=LorS(cab);
char throttle=myLocos[loco].throttle;
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"),
throttle, lors , cab, DCCToWiTSpeed(DCC::getThrottleSpeed(cab)));
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"),
throttle, lors , cab, DCC::getThrottleDirection(cab));
// compare the DCC functionmap with the local copy and send changes
uint16_t dccFunctionMap=DCC::getFunctionMap(cab);
uint16_t myFunctionMap=myLocos[loco].functionMap;
myLocos[loco].functionMap=dccFunctionMap;
// loop the maps sending any bit changed
// Loop is terminated as soon as no changes are left
for (byte fn=0;dccFunctionMap!=myFunctionMap;fn++) {
if ((dccFunctionMap&1) != (myFunctionMap&1)) {
StringFormatter::send(stream,F("M%cA%c%d<;>F%c%d\n"),
throttle, lors , cab, (dccFunctionMap&1)?'1':'0',fn);
}
// shift just checked bit off end of both maps
dccFunctionMap>>=1;
myFunctionMap>>=1;
}
stream->commit();
}
}
}
void WiThrottle::markForBroadcast(int cab) {
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
wt->markForBroadcast2(cab);
}
void WiThrottle::markForBroadcast2(int cab) {
LOOPLOCOS('*', cab) {
myLocos[loco].broadcastPending=true;
}
}
char WiThrottle::LorS(int cab) {
return (cab<=HIGHEST_SHORT_ADDR)?'S':'L';
}

View File

@ -24,6 +24,8 @@
struct MYLOCO {
char throttle; //indicates which throttle letter on client, often '0','1' or '2'
int cab; //address of this loco
bool broadcastPending;
uint16_t functionMap;
};
class WiThrottle {
@ -31,7 +33,8 @@ class WiThrottle {
static void loop(RingStream * stream);
void parse(RingStream * stream, byte * cmd);
static WiThrottle* getThrottle( int wifiClient);
static void markForBroadcast(int cab);
private:
WiThrottle( int wifiClientId);
~WiThrottle();
@ -62,8 +65,8 @@ class WiThrottle {
void multithrottle(RingStream * stream, byte * cmd);
void locoAction(RingStream * stream, byte* aval, char throttleChar, int cab);
void accessory(RingStream *, byte* cmd);
void checkHeartbeat();
void checkHeartbeat(RingStream * stream);
void markForBroadcast2(int cab);
// callback stuff to support prog track acquire
static RingStream * stashStream;
static WiThrottle * stashInstance;

View File

@ -152,7 +152,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
if (ch=='E' || ch=='l') { // ERROR or "link is not valid"
if (clientPendingCIPSEND>=0) {
// A CIPSEND was errored... just toss it away
purgeCurrentCIPSEND();
purgeCurrentCIPSEND();
}
loopState=SKIPTOEND;
break;
@ -231,6 +231,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
if (ch=='C') {
// got "x C" before CLOSE or CONNECTED, or CONNECT FAILED
if (runningClientId==clientPendingCIPSEND) purgeCurrentCIPSEND();
else CommandDistributor::forget(runningClientId);
}
loopState=SKIPTOEND;
break;
@ -245,6 +246,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
void WifiInboundHandler::purgeCurrentCIPSEND() {
// A CIPSEND was sent but errored... or the client closed just toss it away
CommandDistributor::forget(clientPendingCIPSEND);
DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
pendingCipsend=false;

View File

@ -75,13 +75,13 @@ bool WifiInterface::setup(long serial_link_speed,
(void) channel;
#endif
#if NUM_SERIAL > 0
#if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS)
Serial1.begin(serial_link_speed);
wifiUp = setup(Serial1, wifiESSID, wifiPassword, hostname, port, channel);
#endif
// Other serials are tried, depending on hardware.
#if NUM_SERIAL > 1
#if NUM_SERIAL > 1 && !defined(SERIAL2_COMMANDS)
if (wifiUp == WIFI_NOAT)
{
Serial2.begin(serial_link_speed);
@ -89,7 +89,7 @@ bool WifiInterface::setup(long serial_link_speed,
}
#endif
#if NUM_SERIAL > 2
#if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS)
if (wifiUp == WIFI_NOAT)
{
Serial3.begin(serial_link_speed);

View File

@ -176,6 +176,18 @@ The configuration file for DCC-EX Command Station
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0
// (throw turnout).
//#define DCC_ACCESSORY_COMMAND_REVERSE
//#define DCC_ACCESSORY_RCN_213
//
// HANDLING MULTIPLE SERIAL THROTTLES
// The command station always operates with the default Serial port.
// Diagnostics are only emitted on the default serial port and not broadcast.
// Other serial throttles may be added to the Serial1, Serial2, Serial3 ports
// which may or may not exist on your CPU. (Mega has all 3)
// To monitor a throttle on one or more serial ports, uncomment the defines below.
// NOTE: do not define here the WiFi shield serial port or your wifi will not work.
//
//#define SERIAL1_COMMAND
//#define SERIAL2_COMMAND
//#define SERIAL3_COMMAND
/////////////////////////////////////////////////////////////////////////////////////

View File

@ -46,7 +46,7 @@
#define WIFI_CHANNEL 1
#endif
#else
#warning You have defined that you want WIFI but your hardware has not enough memory to do that, so WIFI DISABLED
#define WIFI_WARNING
#define WIFI_ON false
#endif
#else
@ -57,7 +57,7 @@
#if defined(BIG_RAM)
#define ETHERNET_ON true
#else
#warning You have defined that you want ETHERNET but your hardware has not enough memory to do that, so ETHERNET DISABLED
#define ETHERNET_WARNING
#define ETHERNET_ON false
#endif
#else

192
myAutomation2.h Normal file
View File

@ -0,0 +1,192 @@
/* This is an automation example file.
* The presence of a file calle "myAutomation.h" brings EX-RAIL code into
* the command station.
* The auotomation may have multiple concurrent tasks.
* A task may drive one loco through a ROUTE or may simply
* automate some other part of the layout without any loco.
*
* At startup, a single task is created to execute the first
* instruction after ROUTES.
* This task may simply follow a route, or may SCHEDULE
* further tasks (thats is.. send a loco out along a route).
*
* Where the loco id is not known at compile time, a new task
* can be creatd with the command:
* </ SCHEDULE [cab] route>
*
*/
// Include the name to pin mappings for my layout
#include "myLayout.h"
ALIAS(ROUTE_1,1)
ALIAS(UP_MOUNTAIN,8)
ALIAS(UP_MOUNTAIN_FROM_PROG,88)
ALIAS(INNER_LOOP,7)
ALIAS(INNER_FROM_PROG,77)
//EXRAIL // myAutomation must start with the EXRAIL instruction
// This is the default starting route, AKA ROUTE(0)
// START(999) // this is just a diagnostic test cycle
PRINT("started")
LCD(0,"EXRAIL RULES")
SERIAL("I had one of them but the leg fell off!")
DONE // This just ends the startup thread
/*AUTOSTART*/ ROUTE(ROUTE_1,"Close All")
LCD(1,"Bingo")
CLOSE(TOP_TURNOUT) DELAY(10)
CLOSE(Y_TURNOUT) DELAY(10)
CLOSE(MIDDLE_TURNOUT) DELAY(10)
CLOSE(JOIN_TURNOUT) DELAY(10)
CLOSE(LOWER_TURNOUT) DELAY(10)
CLOSE(CROSSOVER_TURNOUT) DELAY(10)
CLOSE(PROG_TURNOUT) DELAY(10)
PRINT("Close All completed")
ENDTASK
SEQUENCE(UP_MOUNTAIN) // starting at the lower closed turnout siding and going up the mountain
PRINT("Up Mountain started")
DELAY(10000) // wait 10 seconds
RESERVE(BLOCK_LOWER_MOUNTAIN)
CLOSE(LOWER_TURNOUT) CLOSE(JOIN_TURNOUT)
FWD(60) AT(Y_LOWER)
RESERVE(BLOCK_X_MOUNTAIN)
CLOSE(Y_TURNOUT) CLOSE(MIDDLE_TURNOUT)
FWD(40) AT(MIDDLE_C_BUFFER) STOP
FREE(BLOCK_X_MOUNTAIN) FREE(BLOCK_LOWER_MOUNTAIN)
DELAY(10000)
RESERVE(BLOCK_UPPER_MOUNTAIN) RESERVE(BLOCK_X_MOUNTAIN)
CLOSE(MIDDLE_TURNOUT) THROW(Y_TURNOUT) THROW(TOP_TURNOUT)
REV(55)
AFTER(Y_UPPER) FREE(BLOCK_X_MOUNTAIN)
REV(55) AT(TOP_T_BUFFER) STOP // At top of mountain
FREE(BLOCK_UPPER_MOUNTAIN)
DELAY(5000)
RESERVE(BLOCK_UPPER_MOUNTAIN)
THROW(TOP_TURNOUT)
FWD(60) AT(Y_UPPER)
RESERVE(BLOCK_X_MOUNTAIN)
THROW(Y_TURNOUT) CLOSE(MIDDLE_TURNOUT)
FWD(40) AT(MIDDLE_C_BUFFER) STOP
FREE(BLOCK_UPPER_MOUNTAIN) FREE(BLOCK_X_MOUNTAIN)
DELAY(6000)
RESERVE(BLOCK_LOWER_MOUNTAIN) RESERVE(BLOCK_X_MOUNTAIN)
CLOSE(MIDDLE_TURNOUT) CLOSE(Y_TURNOUT) CLOSE(JOIN_TURNOUT) CLOSE(LOWER_TURNOUT)
REV(60)
AFTER(Y_LOWER) FREE(BLOCK_X_MOUNTAIN)
AT(LOWER_C_BUFFER) STOP
FREE(BLOCK_LOWER_MOUNTAIN)
FOLLOW(UP_MOUNTAIN)
AUTOMATION(UP_MOUNTAIN_FROM_PROG,"Send up mountain from prog")
JOIN
RESERVE(BLOCK_LOWER_MOUNTAIN)
RESERVE(BLOCK_X_INNER)
RESERVE(BLOCK_X_OUTER)
// safe to cross
THROW(PROG_TURNOUT) THROW(CROSSOVER_TURNOUT) THROW(JOIN_TURNOUT)
FWD(45)
AFTER(JOIN_AFTER) STOP
CLOSE(PROG_TURNOUT) CLOSE(CROSSOVER_TURNOUT) CLOSE(JOIN_TURNOUT)
FREE(BLOCK_X_OUTER) FREE(BLOCK_X_INNER)
CLOSE(LOWER_TURNOUT)
REV(40) AT(LOWER_C_BUFFER) STOP
FREE(BLOCK_LOWER_MOUNTAIN)
FOLLOW(UP_MOUNTAIN)
SEQUENCE(INNER_LOOP)
FWD(50)
AT(CROSSOVER_INNER_BEFORE)
RESERVE(BLOCK_X_INNER)
CLOSE(CROSSOVER_TURNOUT)
FWD(50)
AFTER(CROSSOVER_INNER_AFTER)
FREE(BLOCK_X_INNER)
FOLLOW(INNER_LOOP)
// Turnout definitions
TURNOUT(TOP_TURNOUT, TOP_TURNOUT,0,"Top Station")
TURNOUT(Y_TURNOUT, Y_TURNOUT,0,"Mountain join")
TURNOUT(MIDDLE_TURNOUT, MIDDLE_TURNOUT,0,"Middle Station")
TURNOUT(JOIN_TURNOUT,JOIN_TURNOUT,0)
TURNOUT(LOWER_TURNOUT,LOWER_TURNOUT,0)
TURNOUT(CROSSOVER_TURNOUT,CROSSOVER_TURNOUT,0)
TURNOUT(PROG_TURNOUT,PROG_TURNOUT,0)
// Single slip protection
ONTHROW(2)
THROW(1)
DONE
ONCLOSE(1)
CLOSE(2)
DONE
ROUTE(61,"Call return test")
PRINT("In 61 test 1")
CALL(62)
PRINT("In 61 test 2")
CALL(62)
PRINT("In 61 test 3")
ACTIVATE(100,2)
DEACTIVATE(100,2)
DONE
SEQUENCE(62)
PRINT("In seq 62")
RETURN
ROUTE(63,"Signal test 40,41,42")
SIGNAL(40,41,42)
DELAY(2000)
RED(40)
DELAY(2000)
AMBER(40)
DELAY(2000)
GREEN(40)
FOLLOW(63)
ROUTE(64,"Func test 6772")
XFON(6772,1)
DELAY(5000)
XFOFF(6772,1)
DELAY(5000)
FOLLOW(64)
ROUTE(65,"Negative sensor test")
PRINT(" WAIT for -176")
AT(-176)
PRINT(" WAIT for 176")
AT(176)
PRINT("done")
DONE
ROUTE(123,"Activate stuff")
ACTIVATEL(5)
ACTIVATE(7,2)
DEACTIVATE(3,2)
DEACTIVATEL(6)
DONE
ONACTIVATEL(5)
PRINT("ACT 5")
DONE
ONACTIVATE(7,2)
PRINT("ACT 7,2")
DONE
ONDEACTIVATE(7,2)
PRINT("DEACT 7,2")
DONE
ONDEACTIVATEL(5)
PRINT("DEACT 5")
DONE