2020-07-03 18:35:32 +02:00
|
|
|
/*
|
2022-01-07 02:28:35 +01:00
|
|
|
* © 2021 Neil McKechnie
|
|
|
|
* © 2021 Mike S
|
|
|
|
* © 2020-2022 Harald Barth
|
|
|
|
* © 2020-2021 M Steve Todd
|
|
|
|
* © 2020-2021 Chris Harlow
|
|
|
|
* All rights reserved.
|
2020-07-03 18:35:32 +02:00
|
|
|
*
|
2022-01-07 02:28:35 +01:00
|
|
|
* This file is part of CommandStation-EX
|
2020-07-03 18:35:32 +02:00
|
|
|
*
|
|
|
|
* 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/>.
|
|
|
|
*/
|
2020-06-27 16:36:32 +02:00
|
|
|
/*
|
|
|
|
* Truncated JMRI WiThrottle server implementation for DCC-EX command station
|
|
|
|
* Credit is due to Valerie Valley RR https://sites.google.com/site/valerievalleyrr/
|
|
|
|
* for showing how it could be done, but this code is very different to the original
|
2020-08-14 16:43:35 +02:00
|
|
|
* implementation as it is designed to run on the Arduino and not the ESP and is
|
2020-06-27 16:36:32 +02:00
|
|
|
* also calling directly into the DCCEX Api rather than simulating JMRI text commands.
|
2020-06-29 14:49:46 +02:00
|
|
|
* Refer JMRI WiFi Throttle Communications Protocol https://www.jmri.org/help/en/package/jmri/jmrit/withrottle/Protocol.shtml
|
2020-06-27 16:36:32 +02:00
|
|
|
*
|
|
|
|
*
|
|
|
|
* PROTOTYPE NOTES:
|
|
|
|
* There will be one WiThrottle instance created for each WiThrottle client detected by the WifiInterface.
|
|
|
|
* Some shortcuts have been taken and there are some things that are yet to be included:
|
|
|
|
* e.g. Full response to adding a loco.
|
|
|
|
* What to do about unknown turnouts.
|
|
|
|
* Broadcasting to other WiThrottles when things change.
|
|
|
|
* - Bear in mind that changes may have taken place due to
|
|
|
|
* other WiThrottles, OR JMRI commands received OR TPL automation.
|
|
|
|
* - I suggest that at the end of parse(), then anything that has changed and is of interest could
|
|
|
|
* be notified then. (e.g loco speeds, directions or functions, turnout states.
|
|
|
|
*
|
|
|
|
* WiThrottle.h sets the max locos per client at 10, this is ok to increase but requires just an extra 3 bytes per loco per client.
|
|
|
|
*/
|
|
|
|
#include <Arduino.h>
|
2021-08-03 23:12:25 +02:00
|
|
|
#include "defines.h"
|
2020-06-27 16:36:32 +02:00
|
|
|
#include "WiThrottle.h"
|
|
|
|
#include "DCC.h"
|
|
|
|
#include "DCCWaveform.h"
|
|
|
|
#include "StringFormatter.h"
|
|
|
|
#include "Turnouts.h"
|
|
|
|
#include "DIAG.h"
|
2020-09-29 17:51:01 +02:00
|
|
|
#include "GITHUB_SHA.h"
|
|
|
|
#include "version.h"
|
2022-01-30 18:31:26 +01:00
|
|
|
#include "EXRAIL2.h"
|
2021-12-16 13:32:14 +01:00
|
|
|
#include "CommandDistributor.h"
|
2022-02-23 16:44:34 +01:00
|
|
|
#include "TrackManager.h"
|
2022-03-02 15:24:49 +01:00
|
|
|
#include "DCCTimer.h"
|
2021-08-03 23:12:25 +02:00
|
|
|
|
2020-06-27 16:36:32 +02:00
|
|
|
#define LOOPLOCOS(THROTTLECHAR, CAB) for (int loco=0;loco<MAX_MY_LOCO;loco++) \
|
2020-08-13 22:38:22 +02:00
|
|
|
if ((myLocos[loco].throttle==THROTTLECHAR || '*'==THROTTLECHAR) && (CAB<0 || myLocos[loco].cab==CAB))
|
2020-06-27 16:36:32 +02:00
|
|
|
|
|
|
|
WiThrottle * WiThrottle::firstThrottle=NULL;
|
|
|
|
|
2022-10-19 11:10:37 +02:00
|
|
|
static uint8_t xstrncmp(const char *s1, const char *s2, uint8_t n) {
|
|
|
|
if (n == 0)
|
2022-08-07 20:12:42 +02:00
|
|
|
return 0;
|
2022-10-19 11:10:37 +02:00
|
|
|
do {
|
|
|
|
if (*s1 != *s2++)
|
|
|
|
return 1;
|
|
|
|
if (*s1++ == 0)
|
|
|
|
break;
|
|
|
|
} while (--n != 0);
|
|
|
|
return 0;
|
2022-08-07 20:12:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void WiThrottle::findUniqThrottle(int id, char *u) {
|
|
|
|
WiThrottle *wtmyid = NULL;
|
|
|
|
WiThrottle *wtmyuniq = NULL;
|
2022-10-18 22:20:38 +02:00
|
|
|
|
|
|
|
// search 1, look for clientid match
|
2022-08-07 20:12:42 +02:00
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle){
|
2022-10-18 22:20:38 +02:00
|
|
|
if (wt->clientid == id) {
|
2022-10-19 11:10:37 +02:00
|
|
|
if (xstrncmp(u, wt->uniq, 16) == 0) // should be most common case
|
2022-10-18 22:20:38 +02:00
|
|
|
return;
|
2022-10-19 11:10:37 +02:00
|
|
|
wtmyid = wt;
|
2022-10-18 22:20:38 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// search 2, look for string match
|
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle){
|
2022-10-19 11:10:37 +02:00
|
|
|
if (xstrncmp(u, wt->uniq, 16) == 0) {
|
2022-08-07 20:12:42 +02:00
|
|
|
wtmyuniq = wt;
|
2022-10-18 22:20:38 +02:00
|
|
|
break;
|
|
|
|
}
|
2022-08-07 20:12:42 +02:00
|
|
|
}
|
2022-10-18 22:20:38 +02:00
|
|
|
|
|
|
|
// analyse result of the two for loops:
|
2022-08-07 20:12:42 +02:00
|
|
|
if (wtmyid == NULL) { // should not happen
|
|
|
|
DIAG(F("Did not find my own wiThrottle handle"));
|
|
|
|
return;
|
|
|
|
}
|
2022-10-18 22:20:38 +02:00
|
|
|
// wtmyuniq == wtmyid has already returned in for loop 1
|
2022-08-07 20:12:42 +02:00
|
|
|
if (wtmyuniq == NULL) { // register uniq in the found id
|
|
|
|
strncpy(wtmyid->uniq, u, 16);
|
|
|
|
wtmyid->uniq[16] = '\0';
|
|
|
|
if (Diag::WITHROTTLE) DIAG(F("Client %d registered as %s"),wtmyid->clientid, wtmyid->uniq);
|
|
|
|
return;
|
|
|
|
}
|
2022-10-18 22:20:38 +02:00
|
|
|
// if we get here wtmyid and wtmyuniq point on objects but differnet ones
|
|
|
|
// so we need to do the copy (all other options covered above)
|
2022-08-07 20:12:42 +02:00
|
|
|
for(int n=0; n < MAX_MY_LOCO; n++)
|
|
|
|
wtmyid->myLocos[n] = wtmyuniq->myLocos[n];
|
|
|
|
wtmyid->heartBeatEnable = wtmyuniq->heartBeatEnable;
|
|
|
|
wtmyid->heartBeat = wtmyuniq->heartBeat;
|
|
|
|
wtmyid->initSent = wtmyuniq->initSent;
|
|
|
|
wtmyid->exRailSent = wtmyuniq->exRailSent;
|
|
|
|
wtmyid->mostRecentCab = wtmyuniq->mostRecentCab;
|
|
|
|
wtmyid->turnoutListHash = wtmyuniq->turnoutListHash;
|
|
|
|
wtmyid->lastPowerState = wtmyuniq->lastPowerState;
|
|
|
|
strncpy(wtmyid->uniq, u, 16);
|
|
|
|
wtmyid->uniq[16] = '\0';
|
|
|
|
if (Diag::WITHROTTLE)
|
|
|
|
DIAG(F("New client %d replaces old client %d as %s"), wtmyid->clientid, wtmyuniq->clientid, wtmyid->uniq);
|
|
|
|
forget(wtmyuniq->clientid); // do not use wtmyid after this
|
|
|
|
}
|
|
|
|
|
2020-07-05 22:00:27 +02:00
|
|
|
WiThrottle* WiThrottle::getThrottle( int wifiClient) {
|
2020-06-27 16:36:32 +02:00
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
2020-08-14 13:26:14 +02:00
|
|
|
if (wt->clientid==wifiClient) return wt;
|
2020-07-05 22:00:27 +02:00
|
|
|
return new WiThrottle( wifiClient);
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
2022-08-05 19:14:22 +02:00
|
|
|
void WiThrottle::forget( byte clientId) {
|
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
|
|
|
if (wt->clientid==clientId) {
|
|
|
|
delete wt;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-14 13:26:14 +02:00
|
|
|
bool WiThrottle::isThrottleInUse(int cab) {
|
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
|
|
|
if (wt->areYouUsingThrottle(cab)) return true;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool WiThrottle::areYouUsingThrottle(int cab) {
|
|
|
|
LOOPLOCOS('*', cab) { // see if I have this cab in use
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2020-08-13 22:38:22 +02:00
|
|
|
// One instance of WiThrottle per connected client, so we know what the locos are
|
2020-06-27 16:36:32 +02:00
|
|
|
|
2020-07-05 22:00:27 +02:00
|
|
|
WiThrottle::WiThrottle( int wificlientid) {
|
2021-03-25 15:23:38 +01:00
|
|
|
if (Diag::WITHROTTLE) DIAG(F("%l Creating new WiThrottle for client %d"),millis(),wificlientid);
|
2020-06-27 16:36:32 +02:00
|
|
|
nextThrottle=firstThrottle;
|
|
|
|
firstThrottle= this;
|
|
|
|
clientid=wificlientid;
|
2020-08-20 18:16:47 +02:00
|
|
|
initSent=false; // prevent sending heartbeats before connection completed
|
2020-08-12 17:17:05 +02:00
|
|
|
heartBeatEnable=false; // until client turns it on
|
2020-08-20 18:16:47 +02:00
|
|
|
turnoutListHash = -1; // make sure turnout list is sent once
|
2021-08-03 23:12:25 +02:00
|
|
|
exRailSent=false;
|
|
|
|
mostRecentCab=0;
|
2020-08-12 17:17:05 +02:00
|
|
|
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
WiThrottle::~WiThrottle() {
|
2022-08-07 20:12:42 +02:00
|
|
|
if (Diag::WITHROTTLE) DIAG(F("Deleting WiThrottle client %d"),this->clientid);
|
2020-06-27 16:36:32 +02:00
|
|
|
if (firstThrottle== this) {
|
|
|
|
firstThrottle=this->nextThrottle;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle) {
|
|
|
|
if (wt->nextThrottle==this) {
|
|
|
|
wt->nextThrottle=this->nextThrottle;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-26 14:58:25 +01:00
|
|
|
void WiThrottle::parse(RingStream * stream, byte * cmdx) {
|
2020-07-05 22:00:27 +02:00
|
|
|
|
2020-10-13 18:37:40 +02:00
|
|
|
byte * cmd=cmdx;
|
2020-07-05 22:00:27 +02:00
|
|
|
|
2020-06-27 16:36:32 +02:00
|
|
|
heartBeat=millis();
|
2021-03-25 15:23:38 +01:00
|
|
|
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d)<-[%e]"),millis(),clientid,cmd);
|
2020-08-12 17:17:05 +02:00
|
|
|
|
2020-08-20 18:16:47 +02:00
|
|
|
if (initSent) {
|
|
|
|
// Send turnout list if changed since last sent (will replace list on client)
|
|
|
|
if (turnoutListHash != Turnout::turnoutlistHash) {
|
|
|
|
StringFormatter::send(stream,F("PTL"));
|
2021-08-19 22:22:59 +02:00
|
|
|
for(Turnout *tt=Turnout::first();tt!=NULL;tt=tt->next()){
|
2022-04-13 00:10:29 +02:00
|
|
|
if (tt->isHidden()) continue;
|
2021-08-18 19:55:22 +02:00
|
|
|
int id=tt->getId();
|
2022-04-03 12:19:04 +02:00
|
|
|
const FSH * tdesc=NULL;
|
|
|
|
#ifdef EXRAIL_ACTIVE
|
|
|
|
tdesc=RMFT2::getTurnoutDescription(id);
|
|
|
|
#endif
|
|
|
|
char tchar=Turnout::isClosed(id)?'2':'4';
|
|
|
|
if (tdesc==NULL) // turnout with no description
|
|
|
|
StringFormatter::send(stream,F("]\\[%d}|{T%d}|{T%c"), id,id,tchar);
|
2022-04-13 00:10:29 +02:00
|
|
|
else
|
2022-04-03 12:19:04 +02:00
|
|
|
StringFormatter::send(stream,F("]\\[%d}|{%S}|{%c"), id,tdesc,tchar);
|
2020-08-20 18:16:47 +02:00
|
|
|
}
|
|
|
|
StringFormatter::send(stream,F("\n"));
|
|
|
|
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
|
2020-08-12 17:17:05 +02:00
|
|
|
}
|
2021-08-03 23:12:25 +02:00
|
|
|
|
|
|
|
else if (!exRailSent) {
|
2022-01-30 18:31:26 +01:00
|
|
|
// Send EX-RAIL routes list if not already sent (but not at same time as turnouts above)
|
2021-08-03 23:12:25 +02:00
|
|
|
exRailSent=true;
|
2022-01-31 03:00:29 +01:00
|
|
|
#ifdef EXRAIL_ACTIVE
|
2022-04-03 12:19:04 +02:00
|
|
|
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL"));
|
2022-04-08 17:13:15 +02:00
|
|
|
for (byte pass=0;pass<2;pass++) {
|
2022-04-13 00:10:29 +02:00
|
|
|
// first pass automations, second pass routes.
|
2022-10-26 19:28:22 +02:00
|
|
|
for (int ix=0;;ix+=2) {
|
|
|
|
int16_t id=GETHIGHFLASHW((pass?RMFT2::automationIdList:RMFT2::routeIdList),ix);
|
2022-04-08 17:13:15 +02:00
|
|
|
if (id==0) break;
|
|
|
|
const FSH * desc=RMFT2::getRouteDescription(id);
|
|
|
|
StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"),
|
|
|
|
pass?'A':'R',id,desc, pass?'4':'2');
|
|
|
|
}
|
2022-04-03 12:19:04 +02:00
|
|
|
}
|
|
|
|
StringFormatter::send(stream,F("\n"));
|
2021-12-29 16:13:37 +01:00
|
|
|
#endif
|
2022-01-06 23:03:57 +01:00
|
|
|
// allow heartbeat to slow down once all metadata sent
|
|
|
|
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
|
2021-08-03 23:12:25 +02:00
|
|
|
}
|
2020-08-12 17:17:05 +02:00
|
|
|
}
|
2022-01-06 23:03:57 +01:00
|
|
|
|
|
|
|
while (cmd[0]) {
|
|
|
|
switch (cmd[0]) {
|
|
|
|
case '*': // heartbeat control
|
|
|
|
if (cmd[1]=='+') heartBeatEnable=true;
|
|
|
|
else if (cmd[1]=='-') heartBeatEnable=false;
|
|
|
|
break;
|
|
|
|
case 'P':
|
|
|
|
if (cmd[1]=='P' && cmd[2]=='A' ) { //PPA power mode
|
2022-02-23 16:44:34 +01:00
|
|
|
TrackManager::setMainPower(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
|
|
|
/* TODO
|
2022-01-06 23:03:57 +01:00
|
|
|
if (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling
|
|
|
|
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
2022-02-23 16:44:34 +01:00
|
|
|
*/
|
|
|
|
|
2022-01-06 23:03:57 +01:00
|
|
|
CommandDistributor::broadcastPower();
|
|
|
|
}
|
2022-01-30 18:31:26 +01:00
|
|
|
#if defined(EXRAIL_ACTIVE)
|
2022-01-06 23:03:57 +01:00
|
|
|
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
|
|
|
|
// exrail routes are RA2Rn , Animations are RA2An
|
|
|
|
int route=getInt(cmd+5);
|
|
|
|
uint16_t cab=cmd[4]=='A' ? mostRecentCab : 0;
|
|
|
|
RMFT2::createNewTask(route, cab);
|
|
|
|
}
|
2021-08-03 23:12:25 +02:00
|
|
|
#endif
|
2022-01-06 23:03:57 +01:00
|
|
|
else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle
|
|
|
|
int id=getInt(cmd+4);
|
|
|
|
if (!Turnout::exists(id)) {
|
|
|
|
// If turnout does not exist, create it
|
|
|
|
int addr = ((id - 1) / 4) + 1;
|
|
|
|
int subaddr = (id - 1) % 4;
|
|
|
|
DCCTurnout::create(id,addr,subaddr);
|
|
|
|
StringFormatter::send(stream, F("HmTurnout %d created\n"),id);
|
|
|
|
}
|
|
|
|
switch (cmd[3]) {
|
|
|
|
// T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging.
|
|
|
|
case 'T':
|
|
|
|
Turnout::setClosed(id,false);
|
|
|
|
break;
|
|
|
|
case 'C':
|
|
|
|
Turnout::setClosed(id,true);
|
|
|
|
break;
|
|
|
|
case '2':
|
|
|
|
Turnout::setClosed(id,!Turnout::isClosed(id));
|
|
|
|
break;
|
|
|
|
default :
|
|
|
|
Turnout::setClosed(id,true);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
StringFormatter::send(stream, F("PTA%c%d\n"),Turnout::isClosed(id)?'2':'4',id );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
|
2022-08-06 19:23:52 +02:00
|
|
|
StringFormatter::send(stream, F("*%d\n"), initSent ? HEARTBEAT_SECONDS : HEARTBEAT_SECONDS/2); // return timeout value
|
2022-01-06 23:03:57 +01:00
|
|
|
break;
|
|
|
|
case 'M': // multithrottle
|
|
|
|
multithrottle(stream, cmd);
|
|
|
|
break;
|
|
|
|
case 'H': // send initial connection info after receiving "HU" message
|
|
|
|
if (cmd[1] == 'U') {
|
2022-08-07 20:12:42 +02:00
|
|
|
WiThrottle::findUniqThrottle(clientid, (char *)cmd+2);
|
2022-01-06 23:03:57 +01:00
|
|
|
StringFormatter::send(stream,F("VN2.0\nHTDCC-EX\nRL0\n"));
|
|
|
|
StringFormatter::send(stream,F("HtDCC-EX v%S, %S, %S, %S\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
|
|
|
|
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{4\n"));
|
2022-02-23 16:44:34 +01:00
|
|
|
StringFormatter::send(stream,F("PPA%x\n"),TrackManager::getMainPower()==POWERMODE::ON);
|
2022-01-31 03:00:29 +01:00
|
|
|
#ifdef EXRAIL_ACTIVE
|
2022-04-03 12:19:04 +02:00
|
|
|
StringFormatter::send(stream,F("RL%d"), RMFT2::rosterNameCount);
|
2022-10-26 19:28:22 +02:00
|
|
|
for (int16_t r=0;r<RMFT2::rosterNameCount;r+=2) {
|
|
|
|
int16_t cabid=GETHIGHFLASHW(RMFT2::rosterIdList,r);
|
2022-04-03 12:19:04 +02:00
|
|
|
StringFormatter::send(stream,F("]\\[%S}|{%d}|{%c"),
|
|
|
|
RMFT2::getRosterName(cabid),cabid,cabid<128?'S':'L');
|
|
|
|
}
|
|
|
|
stream->write('\n'); // end roster
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
2022-08-06 19:08:26 +02:00
|
|
|
// set heartbeat to 5 seconds because we need to sync the metadata (1 second is too short!)
|
2022-08-06 19:23:52 +02:00
|
|
|
StringFormatter::send(stream,F("*%d\n"), HEARTBEAT_SECONDS/2);
|
2022-01-06 23:03:57 +01:00
|
|
|
initSent = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'Q': //
|
|
|
|
LOOPLOCOS('*', -1) { // tell client to drop any locos still assigned to this WiThrottle
|
|
|
|
if (myLocos[loco].throttle!='\0') {
|
|
|
|
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), myLocos[loco].throttle, LorS(myLocos[loco].cab), myLocos[loco].cab);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) Quit"),millis(),clientid);
|
|
|
|
delete this;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// skip over cmd until 0 or past \r or \n
|
|
|
|
while(*cmd !='\0' && *cmd != '\r' && *cmd !='\n') cmd++;
|
|
|
|
if (*cmd!='\0') cmd++; // skip \r or \n
|
|
|
|
}
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
2022-01-06 23:03:57 +01:00
|
|
|
|
2020-06-29 12:37:05 +02:00
|
|
|
int WiThrottle::getInt(byte * cmd) {
|
2022-01-06 23:03:57 +01:00
|
|
|
int i=0;
|
2022-04-18 17:46:13 +02:00
|
|
|
bool negate=cmd[0]=='-';
|
|
|
|
if (negate) cmd++;
|
2022-01-06 23:03:57 +01:00
|
|
|
while (cmd[0]>='0' && cmd[0]<='9') {
|
|
|
|
i=i*10 + (cmd[0]-'0');
|
|
|
|
cmd++;
|
|
|
|
}
|
2022-04-18 17:46:13 +02:00
|
|
|
if (negate) i=0-i;
|
|
|
|
return i ;
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
2020-06-29 12:37:05 +02:00
|
|
|
int WiThrottle::getLocoId(byte * cmd) {
|
2020-06-27 16:36:32 +02:00
|
|
|
if (cmd[0]=='*') return -1; // match all locos
|
|
|
|
if (cmd[0]!='L' && cmd[0]!='S') return 0; // should not match any locos
|
|
|
|
return getInt(cmd+1);
|
|
|
|
}
|
2021-03-09 21:44:44 +01:00
|
|
|
|
2020-10-26 14:58:25 +01:00
|
|
|
void WiThrottle::multithrottle(RingStream * stream, byte * cmd){
|
2022-01-06 23:03:57 +01:00
|
|
|
char throttleChar=cmd[1];
|
|
|
|
int locoid=getLocoId(cmd+3); // -1 for *
|
|
|
|
byte * aval=cmd;
|
|
|
|
while(*aval !=';' && *aval !='\0') aval++;
|
|
|
|
if (*aval) aval+=2; // skip ;>
|
|
|
|
|
|
|
|
// DIAG(F("Multithrottle aval=%c cab=%d"), aval[0],locoid);
|
|
|
|
switch(cmd[2]) {
|
|
|
|
case '+': // add loco request
|
|
|
|
if (cmd[3]=='*') {
|
|
|
|
// M+* means get loco from prog track, then join tracks ready to drive away
|
|
|
|
// Stash the things the callback will need later
|
|
|
|
stashStream= stream;
|
|
|
|
stashClient=stream->peekTargetMark();
|
|
|
|
stashThrottleChar=throttleChar;
|
|
|
|
stashInstance=this;
|
|
|
|
// ask DCC to call us back when the loco id has been read
|
|
|
|
DCC::getLocoId(getLocoCallback); // will remove any previous join
|
|
|
|
return; // return nothing in stream as response is sent later in the callback
|
|
|
|
}
|
|
|
|
//return error if address zero requested
|
|
|
|
if (locoid==0) {
|
|
|
|
StringFormatter::send(stream, F("HMAddress '0' not supported!\n"), cmd[3] ,locoid);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
//return error if L or S from request doesn't match DCC++ assumptions
|
|
|
|
if (cmd[3] != LorS(locoid)) {
|
|
|
|
StringFormatter::send(stream, F("HMLength '%c' not valid for %d!\n"), cmd[3] ,locoid);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
//use first empty "slot" on this client's list, will be added to DCC registration list
|
|
|
|
for (int loco=0;loco<MAX_MY_LOCO;loco++) {
|
|
|
|
if (myLocos[loco].throttle=='\0') {
|
|
|
|
myLocos[loco].throttle=throttleChar;
|
|
|
|
myLocos[loco].cab=locoid;
|
|
|
|
myLocos[loco].functionMap=DCC::getFunctionMap(locoid);
|
|
|
|
myLocos[loco].broadcastPending=true; // means speed/dir will be sent later
|
|
|
|
mostRecentCab=locoid;
|
|
|
|
StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid); //tell client to add loco
|
|
|
|
int fkeys=29;
|
|
|
|
myLocos[loco].functionToggles=1<<2; // F2 (HORN) is a non-toggle
|
|
|
|
|
2022-01-30 18:31:26 +01:00
|
|
|
#ifdef EXRAIL_ACTIVE
|
2022-01-06 23:03:57 +01:00
|
|
|
const char * functionNames=(char *) RMFT2::getRosterFunctions(locoid);
|
|
|
|
if (!functionNames) {
|
|
|
|
// no roster, use presets as above
|
|
|
|
}
|
|
|
|
else if (GETFLASH(functionNames)=='\0') {
|
|
|
|
// "" = Roster but no functions given
|
|
|
|
fkeys=0;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// we have function names...
|
|
|
|
// scan names list emitting names, counting functions and
|
|
|
|
// flagging non-toggling things like horn.
|
|
|
|
myLocos[loco].functionToggles =0;
|
|
|
|
StringFormatter::send(stream, F("M%cL%c%d<;>]\\["), throttleChar,cmd[3],locoid);
|
|
|
|
fkeys=0;
|
|
|
|
bool firstchar=true;
|
|
|
|
for (int fx=0;;fx++) {
|
|
|
|
char c=GETFLASH(functionNames+fx);
|
|
|
|
if (c=='\0') {
|
|
|
|
fkeys++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (c=='/') {
|
|
|
|
fkeys++;
|
|
|
|
StringFormatter::send(stream,F("]\\["));
|
|
|
|
firstchar=true;
|
|
|
|
}
|
|
|
|
else if (firstchar && c=='*') {
|
|
|
|
myLocos[loco].functionToggles |= 1UL<<fkeys;
|
|
|
|
firstchar=false;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
firstchar=false;
|
|
|
|
stream->write(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
StringFormatter::send(stream,F("\n"));
|
|
|
|
}
|
|
|
|
|
2021-12-28 14:47:40 +01:00
|
|
|
#endif
|
2022-01-06 23:03:57 +01:00
|
|
|
|
|
|
|
for(int fKey=0; fKey<fkeys; fKey++) {
|
2020-10-12 23:24:37 +02:00
|
|
|
int fstate=DCC::getFn(locoid,fKey);
|
2021-12-28 14:47:40 +01:00
|
|
|
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),throttleChar,cmd[3],locoid,fstate,fKey);
|
2022-01-06 23:03:57 +01:00
|
|
|
}
|
|
|
|
//speed and direction will be published at next broadcast cycle
|
|
|
|
StringFormatter::send(stream, F("M%cA%c%d<;>s1\n"), throttleChar, cmd[3], locoid); //default speed step 128
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
StringFormatter::send(stream, F("HMMax locos (%d) exceeded, %d not added!\n"), MAX_MY_LOCO ,locoid);
|
|
|
|
break;
|
|
|
|
case '-': // remove loco(s) from this client (leave in DCC registration)
|
|
|
|
LOOPLOCOS(throttleChar, locoid) {
|
|
|
|
myLocos[loco].throttle='\0';
|
|
|
|
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'A':
|
|
|
|
locoAction(stream,aval, throttleChar, locoid);
|
|
|
|
}
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
2020-10-26 14:58:25 +01:00
|
|
|
void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar, int cab){
|
2022-01-06 23:03:57 +01:00
|
|
|
// Note cab=-1 for all cabs in the consist called throttleChar.
|
|
|
|
// DIAG(F("Loco Action aval=%c%c throttleChar=%c, cab=%d"), aval[0],aval[1],throttleChar, cab);
|
|
|
|
(void) stream;
|
|
|
|
switch (aval[0]) {
|
|
|
|
case 'V': // Vspeed
|
|
|
|
{
|
|
|
|
int witSpeed=getInt(aval+1);
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
mostRecentCab=myLocos[loco].cab;
|
|
|
|
DCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));
|
|
|
|
// SetThrottle will cause speed change broadcast
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'F': // Function key pressed/released
|
|
|
|
{
|
|
|
|
bool pressed=aval[1]=='1';
|
|
|
|
int fKey = getInt(aval+2);
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
bool unsetOnRelease = myLocos[loco].functionToggles & (1L<<fKey);
|
|
|
|
if (unsetOnRelease) DCC::setFn(myLocos[loco].cab,fKey, pressed);
|
|
|
|
else if (pressed) DCC::changeFn(myLocos[loco].cab, fKey);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'q':
|
|
|
|
if (aval[1]=='V' || aval[1]=='R' ) { //qV or qR
|
|
|
|
// just flag the loco for broadcast and it will happen.
|
2022-08-06 19:23:52 +02:00
|
|
|
bool foundone = false;
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
foundone = true;
|
2022-01-06 23:03:57 +01:00
|
|
|
myLocos[loco].broadcastPending=true;
|
2022-08-06 19:23:52 +02:00
|
|
|
}
|
|
|
|
if (!foundone)
|
|
|
|
StringFormatter::send(stream,F("HMCS loco list empty\n"));
|
2022-01-06 23:03:57 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'R':
|
|
|
|
{
|
|
|
|
bool forward=aval[1]!='0';
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
mostRecentCab=myLocos[loco].cab;
|
2022-06-05 23:07:03 +02:00
|
|
|
int8_t speed = DCC::getThrottleSpeed(myLocos[loco].cab);
|
|
|
|
if (speed < 0) //can not find any speed for this cab
|
|
|
|
speed = 0;
|
|
|
|
DCC::setThrottle(myLocos[loco].cab, speed, forward);
|
2022-01-06 23:03:57 +01:00
|
|
|
// setThrottle will cause a broadcast so notification will be sent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'X':
|
|
|
|
//Emergency Stop (speed code 1)
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));
|
|
|
|
// setThrottle will cause a broadcast so notification will be sent
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'I': // Idle, set speed to 0
|
|
|
|
case 'Q': // Quit, set speed to 0
|
|
|
|
LOOPLOCOS(throttleChar, cab) {
|
|
|
|
mostRecentCab=myLocos[loco].cab;
|
|
|
|
DCC::setThrottle(myLocos[loco].cab, 0, DCC::getThrottleDirection(myLocos[loco].cab));
|
|
|
|
// setThrottle will cause a broadcast so notification will be sent
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
2022-01-06 23:03:57 +01:00
|
|
|
// convert between DCC++ speed values and WiThrottle speed values
|
2020-08-20 18:16:47 +02:00
|
|
|
int WiThrottle::DCCToWiTSpeed(int DCCSpeed) {
|
|
|
|
if (DCCSpeed == 0) return 0; //stop is stop
|
|
|
|
if (DCCSpeed == 1) return -1; //eStop value
|
|
|
|
return DCCSpeed - 1; //offset others by 1
|
|
|
|
}
|
|
|
|
|
2022-01-06 23:03:57 +01:00
|
|
|
// convert between WiThrottle speed values and DCC++ speed values
|
2020-08-20 18:16:47 +02:00
|
|
|
int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
|
|
|
|
if (WiTSpeed == 0) return 0; //stop is stop
|
|
|
|
if (WiTSpeed == -1) return 1; //eStop value
|
|
|
|
return WiTSpeed + 1; //offset others by 1
|
|
|
|
}
|
|
|
|
|
2020-10-26 14:58:25 +01:00
|
|
|
void WiThrottle::loop(RingStream * stream) {
|
2021-12-15 20:51:01 +01:00
|
|
|
// for each WiThrottle, check the heartbeat and broadcast needed
|
2020-06-27 16:36:32 +02:00
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
2022-01-06 23:03:57 +01:00
|
|
|
wt->checkHeartbeat(stream);
|
|
|
|
|
2020-10-26 14:31:51 +01:00
|
|
|
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
|
|
|
|
2021-12-15 20:51:01 +01:00
|
|
|
void WiThrottle::checkHeartbeat(RingStream * stream) {
|
2020-09-29 17:51:01 +02:00
|
|
|
// if eStop time passed... eStop any locos still assigned to this client and then drop the connection
|
|
|
|
if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {
|
2022-01-06 23:03:57 +01:00
|
|
|
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
|
2020-08-20 18:16:47 +02:00
|
|
|
LOOPLOCOS('*', -1) {
|
2020-08-13 22:38:22 +02:00
|
|
|
if (myLocos[loco].throttle!='\0') {
|
2021-03-25 15:23:38 +01:00
|
|
|
if (Diag::WITHROTTLE) DIAG(F("%l eStopping cab %d"),millis(),myLocos[loco].cab);
|
2020-08-20 18:16:47 +02:00
|
|
|
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab)); // speed 1 is eStop
|
2022-08-15 10:50:55 +02:00
|
|
|
heartBeat=millis(); // We have just stopped everyting, we don't need to do that again at next loop.
|
2020-08-13 22:38:22 +02:00
|
|
|
}
|
2020-06-29 14:03:08 +02:00
|
|
|
}
|
2022-08-07 20:12:42 +02:00
|
|
|
//haba no, not necessary the only throttle and it may come back
|
|
|
|
//delete this;
|
2021-12-15 20:51:01 +01:00
|
|
|
return;
|
2022-01-06 23:03:57 +01:00
|
|
|
}
|
2021-12-15 20:51:01 +01:00
|
|
|
|
2021-12-15 21:53:55 +01:00
|
|
|
// send any outstanding speed/direction/function changes for this clients locos
|
|
|
|
// Changes may have been caused by this client, or another non-Withrottle or Exrail
|
2022-01-06 23:03:57 +01:00
|
|
|
bool streamHasBeenMarked=false;
|
|
|
|
LOOPLOCOS('*', -1) {
|
|
|
|
if (myLocos[loco].throttle!='\0' && myLocos[loco].broadcastPending) {
|
|
|
|
if (!streamHasBeenMarked) {
|
|
|
|
stream->mark(clientid);
|
|
|
|
streamHasBeenMarked=true;
|
2021-12-15 20:51:01 +01:00
|
|
|
}
|
2022-01-06 23:03:57 +01:00
|
|
|
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
|
|
|
|
uint32_t dccFunctionMap=DCC::getFunctionMap(cab);
|
|
|
|
uint32_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;
|
|
|
|
}
|
2021-12-15 20:51:01 +01:00
|
|
|
}
|
2022-01-06 23:03:57 +01:00
|
|
|
}
|
|
|
|
if (streamHasBeenMarked) stream->commit();
|
2020-06-27 16:36:32 +02:00
|
|
|
}
|
2020-07-22 16:14:07 +02:00
|
|
|
|
2021-12-15 20:51:01 +01:00
|
|
|
void WiThrottle::markForBroadcast(int cab) {
|
|
|
|
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
|
|
|
wt->markForBroadcast2(cab);
|
|
|
|
}
|
|
|
|
void WiThrottle::markForBroadcast2(int cab) {
|
|
|
|
LOOPLOCOS('*', cab) {
|
2022-01-06 23:03:57 +01:00
|
|
|
myLocos[loco].broadcastPending=true;
|
2021-12-15 20:51:01 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-07-22 16:14:07 +02:00
|
|
|
char WiThrottle::LorS(int cab) {
|
2022-01-06 23:03:57 +01:00
|
|
|
return (cab<=HIGHEST_SHORT_ADDR)?'S':'L';
|
2021-03-11 14:35:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Drive Away feature. Callback handling
|
2022-01-06 23:03:57 +01:00
|
|
|
|
2021-03-11 14:35:47 +01:00
|
|
|
RingStream * WiThrottle::stashStream;
|
|
|
|
WiThrottle * WiThrottle::stashInstance;
|
|
|
|
byte WiThrottle::stashClient;
|
|
|
|
char WiThrottle::stashThrottleChar;
|
|
|
|
|
2021-03-27 12:17:47 +01:00
|
|
|
void WiThrottle::getLocoCallback(int16_t locoid) {
|
2022-08-16 12:25:49 +02:00
|
|
|
//DIAG(F("LocoCallback mark client %d"), stashClient);
|
2021-03-11 14:35:47 +01:00
|
|
|
stashStream->mark(stashClient);
|
2022-01-06 23:03:57 +01:00
|
|
|
|
2022-03-07 17:30:47 +01:00
|
|
|
if (locoid<=0) {
|
2021-11-25 00:10:11 +01:00
|
|
|
StringFormatter::send(stashStream,F("HMNo loco found on prog track\n"));
|
2022-08-16 12:25:49 +02:00
|
|
|
//DIAG(F("LocoCallback commit (noloco)"));
|
2022-03-07 17:30:47 +01:00
|
|
|
stashStream->commit(); // done here, commit and return
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// short or long
|
|
|
|
char addrchar;
|
|
|
|
if (locoid & LONG_ADDR_MARKER) { // maker bit indicates long addr
|
|
|
|
locoid = locoid ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr
|
|
|
|
if (locoid <= HIGHEST_SHORT_ADDR ) { // out of range for long addr
|
|
|
|
StringFormatter::send(stashStream,F("HMLong addr %d <= %d unsupported\n"), locoid, HIGHEST_SHORT_ADDR);
|
2022-08-16 12:25:49 +02:00
|
|
|
//DIAG(F("LocoCallback commit (error)"));
|
2022-03-07 17:30:47 +01:00
|
|
|
stashStream->commit(); // done here, commit and return
|
|
|
|
return;
|
2021-11-26 19:32:45 +01:00
|
|
|
}
|
2022-03-07 17:30:47 +01:00
|
|
|
addrchar = 'L';
|
|
|
|
} else {
|
|
|
|
addrchar = 'S';
|
2021-03-11 14:35:47 +01:00
|
|
|
}
|
2022-03-07 17:30:47 +01:00
|
|
|
|
|
|
|
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
|
|
|
|
itoa(locoid,addcmd+4,10);
|
|
|
|
stashInstance->multithrottle(stashStream, (byte *)addcmd);
|
2022-03-07 19:46:29 +01:00
|
|
|
TrackManager::setMainPower(POWERMODE::ON);
|
2022-03-19 12:22:31 +01:00
|
|
|
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
|
2022-08-16 12:25:49 +02:00
|
|
|
//DIAG(F("LocoCallback commit success"));
|
2021-03-11 14:35:47 +01:00
|
|
|
stashStream->commit();
|
2022-03-07 17:30:47 +01:00
|
|
|
CommandDistributor::broadcastPower();
|
|
|
|
|
2021-03-11 14:35:47 +01:00
|
|
|
}
|