1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-11-22 23:56:13 +01:00

First WiThrottle prototype

This commit is contained in:
Asbelos 2020-06-27 15:36:32 +01:00
parent 1a1429bf72
commit 669356df7d
5 changed files with 302 additions and 5 deletions

12
DCC.cpp
View File

@ -62,6 +62,18 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
DCCWaveform::mainTrack.schedulePacket(b, nB, 3); // send packet 3 times
}
uint8_t DCC::getThrottleSpeed(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0) return -1;
return speedTable[reg].speedCode & 0x7F;
}
bool DCC::getThrottleDirection(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0) return false ;
return (speedTable[reg].speedCode & 0x80) !=0;
}
static void DCC::setFn( int cab, byte functionNumber, bool on) {
if (cab<=0 || functionNumber<0 || functionNumber>28) return;
int reg = lookupSpeedTable(cab);

2
DCC.h
View File

@ -36,6 +36,8 @@ class DCC {
// Public DCC API functions
static void setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection);
static uint8_t getThrottleSpeed(int cab);
static bool getThrottleDirection(int cab);
static void writeCVByteMain(int cab, int cv, byte bValue);
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
static void setFunction( int cab, byte fByte, byte eByte);

238
WiThrottle.cpp Normal file
View File

@ -0,0 +1,238 @@
/*
* 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
* implemenatatin as it is designed to run on the Arduino and not the ESP and is
* also calling directly into the DCCEX Api rather than simulating JMRI text commands.
* Refer JMRI WiFi Throttle Communications Protocol http://jmri.sourceforge.net/help/en/package/jmri/jmrit/withrottle/Protocol.shtml
*
*
* 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>
#include "WiThrottle.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "StringFormatter.h"
#include "Turnouts.h"
#include "DIAG.h"
#define LOOPLOCOS(THROTTLECHAR, CAB) for (int loco=0;loco<MAX_MY_LOCO;loco++) \
if (myLocos[loco].throttle==THROTTLECHAR && (CAB<0 || myLocos[loco].cab==CAB))
WiThrottle * WiThrottle::firstThrottle=NULL;
WiThrottle* WiThrottle::getThrottle(Print & stream, int wifiClient) {
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
if (wt->clientid==wifiClient) return wt;
return new WiThrottle(stream, wifiClient);
}
// One instance of WiTHrottle per connected client, so we know what the locos are
WiThrottle::WiThrottle(Print & stream, int wificlientid) {
DIAG(F("\nCreating new WiThrottle for client %d\n"),wificlientid);
nextThrottle=firstThrottle;
firstThrottle= this;
clientid=wificlientid;
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
StringFormatter::send(stream,F("VN2.0\nRL0\nPPA%x\nPTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{4\PTL"), DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
for(Turnout *tt=Turnout::firstTurnout;tt!=NULL;tt=tt->nextTurnout){
StringFormatter::send(stream,F("]\\[LT&d}|{%d}|{%d"), tt->data.id, tt->data.id, (bool)(tt->data.tStatus & STATUS_ACTIVE));
}
StringFormatter::send(stream,F("\n*10"));
}
WiThrottle::~WiThrottle() {
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;
}
}
}
void WiThrottle::parse(Print & stream, char * cmd) {
heartBeat=millis();
DIAG(F("\nWiThrottle parse (%d) %s"),clientid, cmd);
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
DCCWaveform::mainTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
StringFormatter::send(stream, F("PPA%c"),cmd[3]);
}
else if (cmd[1]='T' && cmd[2]=='A') { // PTA accessory toggle
// TODO... if we are given an address that is not a known Turnout...
// should we create one or just send the DCC message.
Turnout::activate(getInt(cmd+4),cmd[3]=='T');
}
break;
case 'N': // Heartbeat (2)
StringFormatter::send(stream, F("*10")); // 10 second timeout
break;
case 'M': // multithrottle
multithrottle(stream, cmd);
break;
}
}
int WiThrottle::getInt(char * cmd) {
int i=0;
while (cmd[0]>='0' && cmd[0]<='9') {
i=i*10 + (cmd[0]-'0');
cmd++;
}
return i;
}
int WiThrottle::getLocoId(char * cmd) {
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);
}
void WiThrottle::multithrottle(Print & stream, char* cmd){
char throttleChar=cmd[1];
int locoid=getLocoId(cmd+3); // -1 for *
char * aval=cmd;
while(*aval !=';' && *aval !='\0') aval++;
if (*aval) aval++;
switch(cmd[2]) {
case '+': // add loco
for (int loco=0;loco<MAX_MY_LOCO;loco++) {
if (myLocos[loco].throttle=='\0') {
myLocos[loco].throttle=throttleChar;
myLocos[loco].cab=locoid;
StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid);
// TODO... get known Fn states from DCC (need memoryStream improvements to handle data length)
// for(fKey=0; fKey<29; fKey++)StringFormatter::send(stream,F("M%cA%c<;>F0&s\n"),throttleChar,cmd[3],fkey);
StringFormatter::send(stream, F("M%c+%c%d<;>V0\n"), throttleChar, cmd[3], locoid);
StringFormatter::send(stream, F("M%c+%c%d<;>R1\n"), throttleChar, cmd[3], locoid);
StringFormatter::send(stream, F("M%c+%c%d<;>s1\n"), throttleChar, cmd[3], locoid);
break;
}
}
break;
case '-': // remove loco
LOOPLOCOS(throttleChar, locoid) {
myLocos[loco].throttle='\0';
DCC::setThrottle(myLocos[loco].cab,0,0);
StringFormatter::send(stream, F("M%c-<;>\n"), throttleChar);
}
break;
case 'A':
locoAction(stream,aval, throttleChar, locoid);
}
}
/*** TODO provide add feedback ***
void locoAdd(String th, String actionKey, int i) {
LocoThrottle[Throttle] = actionKey;
client[i].println("M"+th+"+"+actionKey+"<;>");
for(fKey=0; fKey<29; fKey++){
LocoState[Throttle][fKey] =0;
client[i].println("M"+th+"A"+actionKey+"<;>F0"+String(fKey));
}
client[i].println("M"+th+"+"+actionKey+"<;>V0");
client[i].println("M"+th+"+"+actionKey+"<;>R1");
client[i].println("M"+th+"+"+actionKey+"<;>s1");
}
*********/
void WiThrottle::locoAction(Print & stream, char* aval, char throttleChar, int cab){
// Note cab=-1 for all cabs in the consist called throttleChar.
switch (aval[0]) {
case 'V': // Vspeed
{
byte locospeed=getInt(aval+1);
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab,locospeed, DCC::getThrottleDirection(myLocos[loco].cab));
}
}
break;
case 'F': //F onOff function
{
bool onOff=aval[1]=='1';
int fKey = getInt(aval+2);
LOOPLOCOS(throttleChar, cab) {
DCC::setFn(myLocos[loco].cab, fKey,onOff);
}
}
break;
case 'q':
if (aval[1]=='V') { //qV
LOOPLOCOS(throttleChar, cab) {
StringFormatter::send(stream,F("M%cAL%d<;>V%d"), throttleChar, myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab));
}
}
else if (aval[1]=='R') { // qR
LOOPLOCOS(throttleChar, cab) {
StringFormatter::send(stream,F("M%cAL%d<;>R%d"), throttleChar, myLocos[loco].cab, DCC::getThrottleDirection(myLocos[loco].cab));
}
}
break;
case 'R':
{
bool forward=aval[1]!='0';
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab), forward);
}
}
break;
case 'X':
//Emergency Stop (TODO check we have the correct speed code here)
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab,1, DCC::getThrottleDirection(myLocos[loco].cab));
}
break;
case 'I': // Idle
case 'Q': // Quit
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab,0, DCC::getThrottleDirection(myLocos[loco].cab));
}
break;
}
}
void WiThrottle::loop() {
// for each WiThrottle, check the heartbeat
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
wt->checkHeartbeat();
}
void WiThrottle::checkHeartbeat() {
if(millis()-heartBeat > HEARTBEAT_TIMEOUT*1000) {
for (int loco=0;loco<MAX_MY_LOCO;loco++) {
if (myLocos[loco].throttle!='\0') {
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));
// TODO Which stream??? Multiple clients ?? StringFormatter::send(stream,F("M%cAL%d<;>V0"),myLocos[loco].throttle,myLocos[loco].cab);
}
}
}
}

38
WiThrottle.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef WiThrottle_h
#define WiTHrottle_h
struct MYLOCO {
char throttle;
int cab;
};
class WiThrottle {
public:
static void loop();
void parse(Print & stream, char * cmd);
static WiThrottle* getThrottle(Print & stream, int wifiClient);
private:
WiThrottle(Print & stream, int wifiClientId);
~WiThrottle();
static const int MAX_MY_LOCO=10;
static const int HEARTBEAT_TIMEOUT=10;
static WiThrottle* firstThrottle;
static int getInt(char * cmd);
static int getLocoId(char * cmd);
WiThrottle* nextThrottle;
int clientid;
MYLOCO myLocos[MAX_MY_LOCO];
bool heartBeatEnable;
unsigned long heartBeat;
void multithrottle(Print & stream, char* cmd);
void locoAction(Print & stream, char* aval, char throttleChar, int cab);
void accessory(Print & stream, char* cmd);
void checkHeartbeat();
};
#endif

View File

@ -2,7 +2,7 @@
#include "Config.h"
#include "DIAG.h"
#include "StringFormatter.h"
#include "WiThrottle.h"
const char PROGMEM READY_SEARCH[] ="\r\nready\r\n";
const char PROGMEM OK_SEARCH[] ="\r\nOK\r\n";
@ -77,7 +77,9 @@ bool WifiInterface::checkForOK( const int timeout, const char * waitfor, bool ec
void WifiInterface::loop() {
if (!connected) return;
WiThrottle::loop(); // check heartbeats
// read anything into a buffer, collecting info on the way
while (loopstate!=99 && Serial1.available()) {
int ch=Serial1.read();
@ -116,7 +118,6 @@ void WifiInterface::loop() {
} // switch
} // while
if (loopstate!=99) return;
// TODO remove > in data
streamer.write('\0');
DIAG(F("\nWifiRead:%d:%s\n"),connectionId,buffer);
@ -125,9 +126,15 @@ void WifiInterface::loop() {
// We know that parser will read the entire buffer before starting to write to it.
// Otherwise we would have to copy the buffer elsewhere and RAM is in short supply.
// TODO ... tell parser that callbacks are diallowed because we dont want to handle the async
parser.parse(streamer,buffer);
// TODO ... tell JMRI parser that callbacks are diallowed because we dont want to handle the async
if (buffer[0]=='<') parser.parse(streamer,buffer);
else WiThrottle::getThrottle(streamer, connectionId)->parse(streamer, buffer);
if (streamer.available()) { // there is a reply to send
DIAG(F("WiFiInterface Responding (%d) %s\n"),connectionId,buffer);
StringFormatter::send(Serial1,F("AT+CIPSEND=%d,%d\r\n"),connectionId,streamer.available());
streamer.write('\0');
if (checkForOK(1000,PROMPT_SEARCH,true)) Serial1.print((char *) buffer);