mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-07-30 19:03:44 +02:00
Compare commits
62 Commits
amuzzing1-
...
ESP32-moto
Author | SHA1 | Date | |
---|---|---|---|
|
164e1a52f8 | ||
|
0fbfb5945f | ||
|
0e5c89df23 | ||
|
6e05de275d | ||
|
c711be7980 | ||
|
ed2aa4c1d8 | ||
|
82df3a21dc | ||
|
c00d3a825d | ||
|
35ee03537d | ||
|
f7e90e7b73 | ||
|
2632d44ec9 | ||
|
c8e5123c0a | ||
|
e7e26551ce | ||
|
50b854c526 | ||
|
55a789d65a | ||
|
a69b7ee113 | ||
|
114686d124 | ||
|
005ddef665 | ||
|
10209ed6f3 | ||
|
71117bc7a1 | ||
|
97065e892d | ||
|
4668e116f4 | ||
|
5cbf0c2cad | ||
|
c02e976c9f | ||
|
55c7a0a1e8 | ||
|
d7e46ac625 | ||
|
877db433a4 | ||
|
4901f12fcd | ||
|
836ccc143e | ||
|
77ee57eb83 | ||
|
837b0a9fb6 | ||
|
a109ba4e01 | ||
|
c87a80928b | ||
|
c5b283bd8c | ||
|
500fe2f717 | ||
|
278f7618f4 | ||
|
9d74b0f6a5 | ||
|
31059a615c | ||
|
7d7b337f82 | ||
|
05eb0d763a | ||
|
b6cfc39d23 | ||
|
8a0ddb0d74 | ||
|
faeb3194db | ||
|
26bd3ac342 | ||
|
d174c05127 | ||
|
75dffd9dfa | ||
|
0a10dbea0b | ||
|
43191e225e | ||
|
50bb1c950b | ||
|
0bb6b577fa | ||
|
cf0c818138 | ||
|
426b27f0dd | ||
|
19b4893b5f | ||
|
1c7a5320d8 | ||
|
afd4626988 | ||
|
a194b8965c | ||
|
696d12fc5e | ||
|
35cba02ee7 | ||
|
fa1d1619b6 | ||
|
b048879eaa | ||
|
34474cbf5c | ||
|
7397a4089b |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
github: DCC-EX
|
||||
patreon: dccex
|
14
.github/workflows/label-sponsors.yml
vendored
14
.github/workflows/label-sponsors.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: Label sponsors
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
build:
|
||||
name: is-sponsor-label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: JasonEtco/is-sponsor-label-action@v1.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
3
.github/workflows/sha.yml
vendored
3
.github/workflows/sha.yml
vendored
@@ -24,11 +24,10 @@ jobs:
|
||||
sha=$(git rev-parse --short "$GITHUB_SHA")
|
||||
echo "#define GITHUB_SHA \"$sha\"" > GITHUB_SHA.h
|
||||
|
||||
- uses: EndBug/add-and-commit@v8 # You can change this to use a specific version
|
||||
- uses: EndBug/add-and-commit@v4 # You can change this to use a specific version
|
||||
with:
|
||||
add: 'GITHUB_SHA.h'
|
||||
message: 'Committing a SHA'
|
||||
commit: --amend
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,7 +7,7 @@ Release/*
|
||||
.pio/
|
||||
.vscode/
|
||||
config.h
|
||||
.vscode/*
|
||||
.vscode/extensions.json
|
||||
mySetup.h
|
||||
mySetup.cpp
|
||||
myHal.cpp
|
||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -3,8 +3,5 @@
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@@ -19,124 +16,16 @@
|
||||
* 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"
|
||||
|
||||
#if defined(BIG_MEMORY) | defined(WIFI_ON) | defined(ETHERNET_ON)
|
||||
// This section of CommandDistributor is simply not relevant on a uno or similar
|
||||
const byte NO_CLIENT=255;
|
||||
DCCEXParser * CommandDistributor::parser=0;
|
||||
|
||||
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);
|
||||
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * streamer) {
|
||||
if (buffer[0] == '<') {
|
||||
if (!parser) parser = new DCCEXParser();
|
||||
parser->parse(streamer, buffer, streamer);
|
||||
}
|
||||
ringClient=NO_CLIENT;
|
||||
}
|
||||
|
||||
void CommandDistributor::forget(byte clientId) {
|
||||
clients[clientId]=NONE_TYPE;
|
||||
}
|
||||
|
||||
|
||||
void CommandDistributor::broadcast(bool includeWithrottleClients) {
|
||||
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;
|
||||
if ( clients[clientId]==WITHROTTLE_TYPE && !includeWithrottleClients) continue;
|
||||
ring->mark(clientId);
|
||||
broadcastBufferWriter->printBuffer(ring);
|
||||
ring->commit();
|
||||
}
|
||||
if (ringClient!=NO_CLIENT) ring->mark(ringClient);
|
||||
|
||||
#endif
|
||||
broadcastBufferWriter->flush();
|
||||
}
|
||||
#else
|
||||
// For a UNO/NANO we can broadcast direct to just one Serial instead of the ring
|
||||
// Redirect ring output ditrect to Serial
|
||||
#define broadcastBufferWriter &Serial
|
||||
// and ignore the internal broadcast call.
|
||||
void CommandDistributor::broadcast(bool includeWithrottleClients) {
|
||||
(void)includeWithrottleClients;
|
||||
}
|
||||
#endif
|
||||
|
||||
void CommandDistributor::broadcastSensor(int16_t id, bool on ) {
|
||||
StringFormatter::send(broadcastBufferWriter,F("<%c %d>\n"), on?'Q':'q', id);
|
||||
broadcast(false);
|
||||
}
|
||||
|
||||
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(true);
|
||||
}
|
||||
|
||||
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(false);
|
||||
#if defined(WIFI_ON) | defined(ETHERNET_ON)
|
||||
WiThrottle::markForBroadcast(sp->loco);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastPower() {
|
||||
bool main=DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON;
|
||||
bool prog=DCCWaveform::progTrack.getPowerMode()==POWERMODE::ON;
|
||||
bool join=DCCWaveform::progTrackSyncMain;
|
||||
const FSH * reason=F("");
|
||||
char state='1';
|
||||
if (main && prog && join) reason=F(" JOIN");
|
||||
else if (main && prog);
|
||||
else if (main) reason=F(" MAIN");
|
||||
else if (prog) reason=F(" PROG");
|
||||
else state='0';
|
||||
|
||||
StringFormatter::send(broadcastBufferWriter,
|
||||
F("<p%c%S>\nPPA%c\n"),state,reason, main?'1':'0');
|
||||
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
|
||||
broadcast(true);
|
||||
}
|
||||
|
||||
void CommandDistributor::broadcastText(const FSH * msg) {
|
||||
StringFormatter::send(broadcastBufferWriter,F("%S"),msg);
|
||||
broadcast(false);
|
||||
else WiThrottle::getThrottle(clientId)->parse(streamer, buffer);
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@@ -27,22 +24,9 @@
|
||||
class CommandDistributor {
|
||||
|
||||
public :
|
||||
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 broadcastText(const FSH * msg);
|
||||
static void forget(byte clientId);
|
||||
static void parse(byte clientId,byte* buffer, RingStream * streamer);
|
||||
private:
|
||||
static void broadcast(bool includeWithrottleClients);
|
||||
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];
|
||||
static DCCEXParser * parser;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@@ -1,35 +1,33 @@
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// DCC-EX CommandStation-EX Please see https://DCC-EX.com
|
||||
// DCC-EX CommandStation-EX Please see https://DCC-EX.com
|
||||
//
|
||||
// This file is the main sketch for the Command Station.
|
||||
//
|
||||
// CONFIGURATION:
|
||||
//
|
||||
// CONFIGURATION:
|
||||
// Configuration is normally performed by editing a file called config.h.
|
||||
// This file is NOT shipped with the code so that if you pull a later version
|
||||
// of the code, your configuration will not be overwritten.
|
||||
//
|
||||
// If you used the automatic installer program, config.h will have been created automatically.
|
||||
//
|
||||
// To obtain a starting copy of config.h please copy the file config.example.h which is
|
||||
// shipped with the code and may be updated as new features are added.
|
||||
//
|
||||
//
|
||||
// To obtain a starting copy of config.h please copy the file config.example.h which is
|
||||
// shipped with the code and may be updated as new features are added.
|
||||
//
|
||||
// If config.h is not found, config.example.h will be used with all defaults.
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if __has_include ( "config.h")
|
||||
#include "config.h"
|
||||
#else
|
||||
#warning config.h not found. Using defaults from config.example.h
|
||||
#warning config.h not found. Using defaults from config.example.h
|
||||
#include "config.example.h"
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Chris Harlow, Harald Barth, David Cutting,
|
||||
* Fred Decker, Gregor Baues, Anthony W - Dayton
|
||||
* All rights reserved.
|
||||
*
|
||||
* © 2020,2021 Chris Harlow, Harald Barth, David Cutting,
|
||||
* Fred Decker, Gregor Baues, Anthony W - Dayton All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@@ -47,15 +45,11 @@
|
||||
*/
|
||||
|
||||
#include "DCCEX.h"
|
||||
#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
|
||||
#ifdef EXRAIL_WARNING
|
||||
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
|
||||
#endif
|
||||
|
||||
// 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;
|
||||
|
||||
void setup()
|
||||
{
|
||||
@@ -63,53 +57,54 @@ void setup()
|
||||
|
||||
// Responsibility 1: Start the usb connection for diagnostics
|
||||
// This is normally Serial but uses SerialUSB on a SAMD processor
|
||||
SerialManager::init();
|
||||
Serial.begin(115200);
|
||||
#ifdef ESP_DEBUG
|
||||
Serial.setDebugOutput(true);
|
||||
#endif
|
||||
|
||||
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
|
||||
|
||||
CONDITIONAL_LCD_START {
|
||||
// This block is still executed for DIAGS if LCD not in use
|
||||
// This block is still executed for DIAGS if LCD not in use
|
||||
LCD(0,F("DCC++ EX v%S"),F(VERSION));
|
||||
LCD(1,F("Lic GPLv3"));
|
||||
}
|
||||
LCD(1,F("Lic GPLv3"));
|
||||
}
|
||||
|
||||
// Responsibility 2: Start all the communications before the DCC engine
|
||||
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
|
||||
// Start Ethernet if it exists
|
||||
#if WIFI_ON
|
||||
#ifndef ESP_FAMILY
|
||||
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL);
|
||||
#else
|
||||
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL);
|
||||
#endif
|
||||
#endif // WIFI_ON
|
||||
|
||||
#if ETHERNET_ON
|
||||
EthernetInterface::setup();
|
||||
#endif // ETHERNET_ON
|
||||
|
||||
// Responsibility 3: Start the DCC engine.
|
||||
// Note: this provides DCC with two motor drivers, main and prog, which handle the motor shield(s)
|
||||
// Standard supported devices have pre-configured macros but custome hardware installations require
|
||||
// detailed pin mappings and may also require modified subclasses of the MotorDriver to implement specialist logic.
|
||||
// STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h
|
||||
DCC::begin(MOTOR_SHIELD_TYPE);
|
||||
|
||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||
DCC::begin();
|
||||
|
||||
// Start RMFT (ignored if no automnation)
|
||||
RMFT::begin();
|
||||
|
||||
|
||||
|
||||
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
|
||||
// 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) DCCEXParser::parse(F(cmd))
|
||||
#include "mySetup.h"
|
||||
#undef SETUP
|
||||
#endif
|
||||
#define SETUP(cmd) serialParser.parse(F(cmd))
|
||||
#include "mySetup.h"
|
||||
#undef SETUP
|
||||
#endif
|
||||
|
||||
#if defined(LCN_SERIAL)
|
||||
LCN_SERIAL.begin(115200);
|
||||
LCN::init(LCN_SERIAL);
|
||||
LCN_SERIAL.begin(115200);
|
||||
LCN::init(LCN_SERIAL);
|
||||
#endif
|
||||
|
||||
LCD(3,F("Ready"));
|
||||
CommandDistributor::broadcastPower();
|
||||
LCD(3,F("Ready"));
|
||||
}
|
||||
|
||||
void loop()
|
||||
@@ -119,36 +114,41 @@ void loop()
|
||||
// Responsibility 1: Handle DCC background processes
|
||||
// (loco reminders and power checks)
|
||||
DCC::loop();
|
||||
|
||||
// Responsibility 2: handle any incoming commands on USB connection
|
||||
SerialManager::loop();
|
||||
serialParser.loop(Serial);
|
||||
|
||||
// Responsibility 3: Optionally handle any incoming WiFi traffic
|
||||
// Responsibility 3: Optionally handle any incoming WiFi traffic
|
||||
#if WIFI_ON
|
||||
#ifndef ESP_FAMILY
|
||||
WifiInterface::loop();
|
||||
#endif
|
||||
#if defined(ARDUINO_ARCH_ESP8266) // on ESP32 own task
|
||||
WifiESP::loop();
|
||||
#endif
|
||||
#endif //WIFI_ON
|
||||
#if ETHERNET_ON
|
||||
EthernetInterface::loop();
|
||||
#endif
|
||||
|
||||
RMFT::loop(); // ignored if no automation
|
||||
|
||||
#if defined(LCN_SERIAL)
|
||||
LCN::loop();
|
||||
#if defined(LCN_SERIAL)
|
||||
LCN::loop();
|
||||
#endif
|
||||
|
||||
LCDDisplay::loop(); // ignored if LCD not in use
|
||||
LCDDisplay::loop(); // ignored if LCD not in use
|
||||
|
||||
// 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
|
||||
|
||||
static int ramLowWatermark = __INT_MAX__; // replaced on first loop
|
||||
#ifdef ESP_FAMILY
|
||||
updateMinimumFreeMemory(128);
|
||||
#endif
|
||||
int freeNow = minimumFreeMemory();
|
||||
if (freeNow < ramLowWatermark) {
|
||||
if (freeNow < ramLowWatermark)
|
||||
{
|
||||
ramLowWatermark = freeNow;
|
||||
LCD(3,F("Free RAM=%5db"), ramLowWatermark);
|
||||
}
|
||||
|
447
DCC.cpp
447
DCC.cpp
@@ -1,13 +1,7 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@@ -26,15 +20,14 @@
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "DCCTrack.h"
|
||||
#include "EEStore.h"
|
||||
#endif
|
||||
#include "GITHUB_SHA.h"
|
||||
#include "version.h"
|
||||
#include "FSH.h"
|
||||
#include "IODevice.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
#include "MotorDriver.h"
|
||||
|
||||
// This module is responsible for converting API calls into
|
||||
// messages to be sent to the waveform generator.
|
||||
@@ -49,30 +42,39 @@
|
||||
// Obtaining ACKs from the prog track using a function
|
||||
// There are no volatiles here.
|
||||
|
||||
const byte FN_GROUP_1=0x01;
|
||||
const byte FN_GROUP_2=0x02;
|
||||
const byte FN_GROUP_3=0x04;
|
||||
const byte FN_GROUP_4=0x08;
|
||||
const byte FN_GROUP_5=0x10;
|
||||
const byte FN_GROUP_1=0x01;
|
||||
const byte FN_GROUP_2=0x02;
|
||||
const byte FN_GROUP_3=0x04;
|
||||
const byte FN_GROUP_4=0x08;
|
||||
const byte FN_GROUP_5=0x10;
|
||||
|
||||
FSH* DCC::shieldName=NULL;
|
||||
byte DCC::joinRelay=UNUSED_PIN;
|
||||
byte DCC::globalSpeedsteps=128;
|
||||
|
||||
void DCC::begin(const FSH * motorShieldName, MotorDriver * mainDriver, MotorDriver* progDriver) {
|
||||
shieldName=(FSH *)motorShieldName;
|
||||
StringFormatter::send(Serial,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
|
||||
void DCC::begin() {
|
||||
StringFormatter::send(Serial,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE),
|
||||
MotorDriverContainer::mDC.getMotorShieldName(), F(GITHUB_SHA));
|
||||
|
||||
// BE AWARE, USES I2C PINS, MAY LEAD TO PIN CONFLICTS
|
||||
// Initialise HAL layer before reading EEprom.
|
||||
IODevice::begin();
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
//example how to use add:
|
||||
//MotorDriverContainer::mDC.add(new MotorDriver(16, 21, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 2.00, 2000, UNUSED_PIN, RMT_MAIN));
|
||||
// Load stuff from EEprom
|
||||
(void)EEPROM; // tell compiler not to warn this is unused
|
||||
EEStore::init();
|
||||
#endif
|
||||
|
||||
DCCWaveform::begin(mainDriver,progDriver);
|
||||
MotorDriverContainer::mDC.diag();
|
||||
DCCWaveform::begin(MotorDriverContainer::mDC.mainTrack(),MotorDriverContainer::mDC.progTrack());
|
||||
|
||||
// Add main and prog drivers to the main and prog packet sources (dcc-tracks).
|
||||
std::vector<MotorDriver*> v;
|
||||
v = MotorDriverContainer::mDC.getDriverType(RMT_MAIN|TIMER_MAIN);
|
||||
for (const auto& d: v) DCCTrack::mainTrack.addDriver(d);
|
||||
v = MotorDriverContainer::mDC.getDriverType(RMT_PROG|TIMER_PROG);
|
||||
for (const auto& d: v) DCCTrack::progTrack.addDriver(d);
|
||||
}
|
||||
|
||||
void DCC::setJoinRelayPin(byte joinRelayPin) {
|
||||
@@ -84,7 +86,7 @@ void DCC::setJoinRelayPin(byte joinRelayPin) {
|
||||
}
|
||||
|
||||
void DCC::setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection) {
|
||||
byte speedCode = (tSpeed & 0x7F) + tDirection * 128;
|
||||
byte speedCode = (tSpeed & 0x7F) + tDirection * 128;
|
||||
setThrottle2(cab, speedCode);
|
||||
// retain speed for loco reminders
|
||||
updateLocoReminder(cab, speedCode );
|
||||
@@ -95,8 +97,8 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
uint8_t b[4];
|
||||
uint8_t nB = 0;
|
||||
// DIAG(F("setSpeedInternal %d %x"),cab,speedCode);
|
||||
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
|
||||
if (cab > 127)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
b[nB++] = lowByte(cab);
|
||||
|
||||
@@ -128,7 +130,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
|
||||
|
||||
}
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
||||
DCCTrack::mainTrack.schedulePacket(b, nB, 0);
|
||||
}
|
||||
|
||||
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
@@ -136,13 +138,13 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
byte b[4];
|
||||
byte nB = 0;
|
||||
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
if (cab > 127)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
b[nB++] = lowByte(cab);
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
b[nB++] = byte2;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
|
||||
DCCTrack::mainTrack.schedulePacket(b, nB, 0);
|
||||
}
|
||||
|
||||
uint8_t DCC::getThrottleSpeed(int cab) {
|
||||
@@ -160,67 +162,86 @@ bool DCC::getThrottleDirection(int cab) {
|
||||
// Set function to value on or off
|
||||
void DCC::setFn( int cab, int16_t functionNumber, bool on) {
|
||||
if (cab<=0 ) return;
|
||||
|
||||
if (functionNumber>28) {
|
||||
//non reminding advanced binary bit set
|
||||
|
||||
if (functionNumber>28) {
|
||||
//non reminding advanced binary bit set
|
||||
byte b[5];
|
||||
byte nB = 0;
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
if (cab > 127)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
b[nB++] = lowByte(cab);
|
||||
if (functionNumber <= 127) {
|
||||
b[nB++] = 0b11011101; // Binary State Control Instruction short form
|
||||
b[nB++] = 0b11011101; // Binary State Control Instruction short form
|
||||
b[nB++] = functionNumber | (on ? 0x80 : 0);
|
||||
}
|
||||
else {
|
||||
b[nB++] = 0b11000000; // Binary State Control Instruction long form
|
||||
b[nB++] = 0b11000000; // Binary State Control Instruction long form
|
||||
b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0); // low order bits and state flag
|
||||
b[nB++] = functionNumber >>7 ; // high order bits
|
||||
}
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCTrack::mainTrack.schedulePacket(b, nB, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return;
|
||||
if (reg<0) return;
|
||||
|
||||
// Take care of functions:
|
||||
// Set state of function
|
||||
unsigned long previous=speedTable[reg].functions;
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
if (on) {
|
||||
speedTable[reg].functions |= funcmask;
|
||||
} else {
|
||||
speedTable[reg].functions &= ~funcmask;
|
||||
}
|
||||
if (speedTable[reg].functions != previous) {
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Flip function state
|
||||
void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||
if (cab<=0 || functionNumber>28) return;
|
||||
// Change function according to how button was pressed,
|
||||
// typically in WiThrottle.
|
||||
// Returns new state or -1 if nothing was changed.
|
||||
int DCC::changeFn( int cab, int16_t functionNumber, bool pressed) {
|
||||
int funcstate = -1;
|
||||
if (cab<=0 || functionNumber>28) return funcstate;
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return;
|
||||
if (reg<0) return funcstate;
|
||||
|
||||
// Take care of functions:
|
||||
// Imitate how many command stations do it: Button press is
|
||||
// toggle but for F2 where it is momentary
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
speedTable[reg].functions ^= funcmask;
|
||||
if (functionNumber == 2) {
|
||||
// turn on F2 on press and off again at release of button
|
||||
if (pressed) {
|
||||
speedTable[reg].functions |= funcmask;
|
||||
funcstate = 1;
|
||||
} else {
|
||||
speedTable[reg].functions &= ~funcmask;
|
||||
funcstate = 0;
|
||||
}
|
||||
} else {
|
||||
// toggle function on press, ignore release
|
||||
if (pressed) {
|
||||
speedTable[reg].functions ^= funcmask;
|
||||
}
|
||||
funcstate = (speedTable[reg].functions & funcmask)? 1 : 0;
|
||||
}
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
return funcstate;
|
||||
}
|
||||
|
||||
int DCC::getFn( int cab, int16_t functionNumber) {
|
||||
if (cab<=0 || functionNumber>28) return -1; // unknown
|
||||
int reg = lookupSpeedTable(cab);
|
||||
if (reg<0) return -1;
|
||||
if (reg<0) return -1;
|
||||
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
return (speedTable[reg].functions & funcmask)? 1 : 0;
|
||||
}
|
||||
|
||||
// Set the group flag to say we have touched the particular group.
|
||||
// A group will be reminded only if it has been touched.
|
||||
// A group will be reminded only if it has been touched.
|
||||
void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
|
||||
byte groupMask;
|
||||
if (functionNumber<=4) groupMask=FN_GROUP_1;
|
||||
@@ -228,13 +249,7 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
|
||||
else if (functionNumber<=12) groupMask=FN_GROUP_3;
|
||||
else if (functionNumber<=20) groupMask=FN_GROUP_4;
|
||||
else groupMask=FN_GROUP_5;
|
||||
flags |= groupMask;
|
||||
}
|
||||
|
||||
uint32_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;
|
||||
flags |= groupMask;
|
||||
}
|
||||
|
||||
void DCC::setAccessory(int address, byte number, bool activate) {
|
||||
@@ -251,10 +266,7 @@ void DCC::setAccessory(int address, byte number, bool activate) {
|
||||
b[0] = address % 64 + 128; // first byte is of the form 10AAAAAA, where AAAAAA represent 6 least signifcant bits of accessory address
|
||||
b[1] = ((((address / 64) % 8) << 4) + (number % 4 << 1) + activate % 2) ^ 0xF8; // second byte is of the form 1AAACDDD, where C should be 1, and the least significant D represent activate/deactivate
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, 2, 4); // Repeat the packet four times
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
RMFT2::activateEvent(address<<2|number,activate);
|
||||
#endif
|
||||
DCCTrack::mainTrack.schedulePacket(b, 2, 3); // Repeat the packet four times (3 2 1 0)
|
||||
}
|
||||
|
||||
//
|
||||
@@ -264,7 +276,7 @@ void DCC::setAccessory(int address, byte number, bool activate) {
|
||||
void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
|
||||
byte b[5];
|
||||
byte nB = 0;
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
if (cab > 127)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
|
||||
b[nB++] = lowByte(cab);
|
||||
@@ -272,7 +284,7 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = bValue;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCTrack::mainTrack.schedulePacket(b, nB, 3);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -285,7 +297,7 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
|
||||
bValue = bValue % 2;
|
||||
bNum = bNum % 8;
|
||||
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
if (cab > 127)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
|
||||
b[nB++] = lowByte(cab);
|
||||
@@ -293,7 +305,7 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
|
||||
b[nB++] = cv2(cv);
|
||||
b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;
|
||||
|
||||
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
|
||||
DCCTrack::mainTrack.schedulePacket(b, nB, 3);
|
||||
}
|
||||
|
||||
void DCC::setProgTrackSyncMain(bool on) {
|
||||
@@ -307,64 +319,64 @@ void DCC::setProgTrackBoost(bool on) {
|
||||
FSH* DCC::getMotorShieldName() {
|
||||
return shieldName;
|
||||
}
|
||||
|
||||
|
||||
const ackOp FLASH WRITE_BIT0_PROG[] = {
|
||||
BASELINE,
|
||||
W0,WACK,
|
||||
V0, WACK, // validate bit is 0
|
||||
V0, WACK, // validate bit is 0
|
||||
ITC1, // if acked, callback(1)
|
||||
FAIL // callback (-1)
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
const ackOp FLASH WRITE_BIT1_PROG[] = {
|
||||
BASELINE,
|
||||
W1,WACK,
|
||||
V1, WACK, // validate bit is 1
|
||||
V1, WACK, // validate bit is 1
|
||||
ITC1, // if acked, callback(1)
|
||||
FAIL // callback (-1)
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
|
||||
const ackOp FLASH VERIFY_BIT0_PROG[] = {
|
||||
BASELINE,
|
||||
V0, WACK, // validate bit is 0
|
||||
V0, WACK, // validate bit is 0
|
||||
ITC0, // if acked, callback(0)
|
||||
V1, WACK, // validate bit is 1
|
||||
ITC1,
|
||||
FAIL // callback (-1)
|
||||
ITC1,
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
const ackOp FLASH VERIFY_BIT1_PROG[] = {
|
||||
BASELINE,
|
||||
V1, WACK, // validate bit is 1
|
||||
V1, WACK, // validate bit is 1
|
||||
ITC1, // if acked, callback(1)
|
||||
V0, WACK,
|
||||
V0, WACK,
|
||||
ITC0,
|
||||
FAIL // callback (-1)
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
|
||||
const ackOp FLASH READ_BIT_PROG[] = {
|
||||
BASELINE,
|
||||
V1, WACK, // validate bit is 1
|
||||
V1, WACK, // validate bit is 1
|
||||
ITC1, // if acked, callback(1)
|
||||
V0, WACK, // validate bit is zero
|
||||
ITC0, // if acked callback 0
|
||||
FAIL // bit not readable
|
||||
CALLFAIL // bit not readable
|
||||
};
|
||||
|
||||
|
||||
const ackOp FLASH WRITE_BYTE_PROG[] = {
|
||||
BASELINE,
|
||||
WB,WACK,ITC1, // Write and callback(1) if ACK
|
||||
// handle decoders that dont ack a write
|
||||
VB,WACK,ITC1, // validate byte and callback(1) if correct
|
||||
FAIL // callback (-1)
|
||||
WB,WACK,ITC1, // Write and callback(1) if ACK
|
||||
// handle decoders that dont ack a write
|
||||
VB,WACK,ITC1, // validate byte and callback(1) if correct
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
|
||||
|
||||
const ackOp FLASH VERIFY_BYTE_PROG[] = {
|
||||
BASELINE,
|
||||
BIV, // ackManagerByte initial value
|
||||
VB,WACK, // validate byte
|
||||
VB,WACK, // validate byte
|
||||
ITCB, // if ok callback value
|
||||
STARTMERGE, //clear bit and byte values ready for merge pass
|
||||
// each bit is validated against 0 and the result inverted in MERGE
|
||||
// this is because there tend to be more zeros in cv values than ones.
|
||||
// this is because there tend to be more zeros in cv values than ones.
|
||||
// There is no need for one validation as entire byte is validated at the end
|
||||
V0, WACK, MERGE, // read and merge first tested bit (7)
|
||||
ITSKIP, // do small excursion if there was no ack
|
||||
@@ -380,14 +392,14 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, ITCBV, // verify merged byte and return it if acked ok - with retry report
|
||||
FAIL };
|
||||
|
||||
|
||||
CALLFAIL };
|
||||
|
||||
|
||||
const ackOp FLASH READ_CV_PROG[] = {
|
||||
BASELINE,
|
||||
STARTMERGE, //clear bit and byte values ready for merge pass
|
||||
// each bit is validated against 0 and the result inverted in MERGE
|
||||
// this is because there tend to be more zeros in cv values than ones.
|
||||
// this is because there tend to be more zeros in cv values than ones.
|
||||
// There is no need for one validation as entire byte is validated at the end
|
||||
V0, WACK, MERGE, // read and merge first tested bit (7)
|
||||
ITSKIP, // do small excursion if there was no ack
|
||||
@@ -402,20 +414,20 @@ const ackOp FLASH READ_CV_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, ITCB, // verify merged byte and return it if acked ok
|
||||
FAIL }; // verification failed
|
||||
VB, WACK, ITCB, // verify merged byte and return it if acked ok
|
||||
CALLFAIL }; // verification failed
|
||||
|
||||
|
||||
const ackOp FLASH LOCO_ID_PROG[] = {
|
||||
BASELINE,
|
||||
SETCV, (ackOp)19, // CV 19 is consist setting
|
||||
SETBYTE, (ackOp)0,
|
||||
SETBYTE, (ackOp)0,
|
||||
VB, WACK, ITSKIP, // ignore consist if cv19 is zero (no consist)
|
||||
SETBYTE, (ackOp)128,
|
||||
VB, WACK, ITSKIP, // ignore consist if cv19 is 128 (no consist, direction bit set)
|
||||
STARTMERGE, // Setup to read cv 19
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
@@ -423,13 +435,13 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, ITCB7, // return 7 bits only, No_ACK means CV19 not supported so ignore it
|
||||
|
||||
SKIPTARGET, // continue here if CV 19 is zero or fails all validation
|
||||
|
||||
SKIPTARGET, // continue here if CV 19 is zero or fails all validation
|
||||
SETCV,(ackOp)29,
|
||||
SETBIT,(ackOp)5,
|
||||
V0, WACK, ITSKIP, // Skip to SKIPTARGET if bit 5 of CV29 is zero
|
||||
|
||||
// Long locoid
|
||||
|
||||
// Long locoid
|
||||
SETCV, (ackOp)17, // CV 17 is part of locoid
|
||||
STARTMERGE,
|
||||
V0, WACK, MERGE, // read and merge bit 1 etc
|
||||
@@ -441,8 +453,8 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, NAKFAIL, // verify merged byte and return -1 it if not acked ok
|
||||
STASHLOCOID, // keep stashed cv 17 for later
|
||||
// Read 2nd part from CV 18
|
||||
STASHLOCOID, // keep stashed cv 17 for later
|
||||
// Read 2nd part from CV 18
|
||||
SETCV, (ackOp)18,
|
||||
STARTMERGE,
|
||||
V0, WACK, MERGE, // read and merge bit 1 etc
|
||||
@@ -455,8 +467,8 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, NAKFAIL, // verify merged byte and return -1 it if not acked ok
|
||||
COMBINELOCOID, // Combile byte with stash to make long locoid and callback
|
||||
|
||||
// ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that
|
||||
|
||||
// ITSKIP Skips to here if CV 29 bit 5 was zero. so read CV 1 and return that
|
||||
SKIPTARGET,
|
||||
SETCV, (ackOp)1,
|
||||
STARTMERGE,
|
||||
@@ -469,8 +481,8 @@ const ackOp FLASH LOCO_ID_PROG[] = {
|
||||
V0, WACK, MERGE,
|
||||
V0, WACK, MERGE,
|
||||
VB, WACK, ITCB, // verify merged byte and callback
|
||||
FAIL
|
||||
};
|
||||
CALLFAIL
|
||||
};
|
||||
|
||||
const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
|
||||
BASELINE,
|
||||
@@ -482,12 +494,12 @@ const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
|
||||
SETBIT,(ackOp)5,
|
||||
W0,WACK,
|
||||
V0,WACK,NAKFAIL,
|
||||
SETCV, (ackOp)1,
|
||||
SETBYTEL, // low byte of word
|
||||
SETCV, (ackOp)1,
|
||||
SETBYTEL, // low byte of word
|
||||
WB,WACK, // some decoders don't ACK writes
|
||||
VB,WACK,ITCB,
|
||||
FAIL
|
||||
};
|
||||
CALLFAIL
|
||||
};
|
||||
|
||||
const ackOp FLASH LONG_LOCO_ID_PROG[] = {
|
||||
BASELINE,
|
||||
@@ -502,16 +514,16 @@ const ackOp FLASH LONG_LOCO_ID_PROG[] = {
|
||||
V1,WACK,NAKFAIL,
|
||||
// Store high byte of address in cv 17
|
||||
SETCV, (ackOp)17,
|
||||
SETBYTEH, // high byte of word
|
||||
SETBYTEH, // high byte of word
|
||||
WB,WACK,
|
||||
VB,WACK,NAKFAIL,
|
||||
// store
|
||||
// store
|
||||
SETCV, (ackOp)18,
|
||||
SETBYTEL, // low byte of word
|
||||
SETBYTEL, // low byte of word
|
||||
WB,WACK,
|
||||
VB,WACK,ITC1, // callback(1) means Ok
|
||||
FAIL
|
||||
};
|
||||
CALLFAIL
|
||||
};
|
||||
|
||||
void DCC::writeCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback) {
|
||||
ackManagerSetup(cv, byteValue, WRITE_BYTE_PROG, callback);
|
||||
@@ -550,27 +562,28 @@ void DCC::setLocoId(int id,ACK_CALLBACK callback) {
|
||||
callback(-1);
|
||||
return;
|
||||
}
|
||||
if (id<=HIGHEST_SHORT_ADDR)
|
||||
if (id<=127)
|
||||
ackManagerSetup(id, SHORT_LOCO_ID_PROG, callback);
|
||||
else
|
||||
ackManagerSetup(id | 0xc000,LONG_LOCO_ID_PROG, callback);
|
||||
}
|
||||
|
||||
void DCC::forgetLoco(int cab) { // removes any speed reminders for this loco
|
||||
setThrottle2(cab,1); // ESTOP this loco if still on track
|
||||
setThrottle2(cab,1); // ESTOP this loco if still on track
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg>=0) speedTable[reg].loco=0;
|
||||
setThrottle2(cab,1); // ESTOP if this loco still on track
|
||||
}
|
||||
void DCC::forgetAllLocos() { // removes all speed reminders
|
||||
setThrottle2(0,1); // ESTOP all locos still on track
|
||||
setThrottle2(0,1); // ESTOP all locos still on track
|
||||
for (int i=0;i<MAX_LOCOS;i++) speedTable[i].loco=0;
|
||||
}
|
||||
|
||||
byte DCC::loopStatus=0;
|
||||
byte DCC::loopStatus=0;
|
||||
|
||||
void DCC::loop() {
|
||||
DCCWaveform::loop(ackManagerProg!=NULL); // power overload checks
|
||||
MotorDriverContainer::mDC.loop();
|
||||
ackManagerLoop(); // maintain prog track ack manager
|
||||
issueReminders();
|
||||
}
|
||||
@@ -582,58 +595,58 @@ void DCC::issueReminders() {
|
||||
// This loop searches for a loco in the speed table starting at nextLoco and cycling back around
|
||||
for (int reg=0;reg<MAX_LOCOS;reg++) {
|
||||
int slot=reg+nextLoco;
|
||||
if (slot>=MAX_LOCOS) slot-=MAX_LOCOS;
|
||||
if (slot>=MAX_LOCOS) slot-=MAX_LOCOS;
|
||||
if (speedTable[slot].loco > 0) {
|
||||
// have found the next loco to remind
|
||||
// have found the next loco to remind
|
||||
// issueReminder will return true if this loco is completed (ie speed and functions)
|
||||
if (issueReminder(slot)) nextLoco=slot+1;
|
||||
if (issueReminder(slot)) nextLoco=slot+1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool DCC::issueReminder(int reg) {
|
||||
unsigned long functions=speedTable[reg].functions;
|
||||
int loco=speedTable[reg].loco;
|
||||
byte flags=speedTable[reg].groupFlags;
|
||||
|
||||
|
||||
switch (loopStatus) {
|
||||
case 0:
|
||||
// DIAG(F("Reminder %d speed %d"),loco,speedTable[reg].speedCode);
|
||||
setThrottle2(loco, speedTable[reg].speedCode);
|
||||
break;
|
||||
case 1: // remind function group 1 (F0-F4)
|
||||
if (flags & FN_GROUP_1)
|
||||
if (flags & FN_GROUP_1)
|
||||
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
|
||||
break;
|
||||
break;
|
||||
case 2: // remind function group 2 F5-F8
|
||||
if (flags & FN_GROUP_2)
|
||||
if (flags & FN_GROUP_2)
|
||||
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
|
||||
break;
|
||||
break;
|
||||
case 3: // remind function group 3 F9-F12
|
||||
if (flags & FN_GROUP_3)
|
||||
if (flags & FN_GROUP_3)
|
||||
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
|
||||
break;
|
||||
break;
|
||||
case 4: // remind function group 4 F13-F20
|
||||
if (flags & FN_GROUP_4)
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
||||
if (flags & FN_GROUP_4)
|
||||
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
|
||||
flags&= ~FN_GROUP_4; // dont send them again
|
||||
break;
|
||||
break;
|
||||
case 5: // remind function group 5 F21-F28
|
||||
if (flags & FN_GROUP_5)
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
||||
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
|
||||
flags&= ~FN_GROUP_5; // dont send them again
|
||||
break;
|
||||
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.
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///// Private helper functions below here /////////////////////
|
||||
@@ -668,28 +681,20 @@ int DCC::lookupSpeedTable(int locoId) {
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
|
||||
void DCC::updateLocoReminder(int loco, byte speedCode) {
|
||||
|
||||
|
||||
if (loco==0) {
|
||||
// broadcast stop/estop but dont change direction
|
||||
for (int reg = 0; reg < MAX_LOCOS; 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);
|
||||
}
|
||||
speedTable[reg].speedCode = (speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
}
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0 && speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0) speedTable[reg].speedCode = speedCode;
|
||||
}
|
||||
|
||||
DCC::LOCO DCC::speedTable[MAX_LOCOS];
|
||||
@@ -721,21 +726,19 @@ void DCC::ackManagerSetup(int cv, byte byteValueOrBitnum, ackOp const program[]
|
||||
return;
|
||||
}
|
||||
|
||||
ackManagerRejoin=DCCWaveform::progTrackSyncMain;
|
||||
ackManagerRejoin=DCCWaveform::progTrackSyncMain;
|
||||
if (ackManagerRejoin ) {
|
||||
// Change from JOIN must zero resets packet.
|
||||
setProgTrackSyncMain(false);
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
}
|
||||
|
||||
DCCWaveform::progTrack.autoPowerOff=false;
|
||||
|
||||
DCCWaveform::progTrack.autoPowerOff=false;
|
||||
if (DCCWaveform::progTrack.getPowerMode() == POWERMODE::OFF) {
|
||||
DCCWaveform::progTrack.autoPowerOff=true; // power off afterwards
|
||||
DCCWaveform::progTrack.autoPowerOff=true; // power off afterwards
|
||||
if (Diag::ACK) DIAG(F("Auto Prog power on"));
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
if (MotorDriver::commonFaultPin)
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
}
|
||||
|
||||
ackManagerCv = cv;
|
||||
@@ -763,7 +766,7 @@ bool DCC::checkResets(uint8_t numResets) {
|
||||
void DCC::ackManagerLoop() {
|
||||
while (ackManagerProg) {
|
||||
byte opcode=GETFLASH(ackManagerProg);
|
||||
|
||||
|
||||
// breaks from this switch will step to next prog entry
|
||||
// returns from this switch will stay on same entry
|
||||
// (typically waiting for a reset counter or ACK waiting, or when all finished.)
|
||||
@@ -773,57 +776,57 @@ void DCC::ackManagerLoop() {
|
||||
if (checkResets(DCCWaveform::progTrack.autoPowerOff || ackManagerRejoin ? 20 : 3)) return;
|
||||
DCCWaveform::progTrack.setAckBaseline();
|
||||
callbackState=READY;
|
||||
break;
|
||||
case W0: // write 0 bit
|
||||
case W1: // write 1 bit
|
||||
break;
|
||||
case W0: // write 0 bit
|
||||
case W1: // write 1 bit
|
||||
{
|
||||
if (checkResets(RESET_MIN)) return;
|
||||
if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
|
||||
if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
|
||||
byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;
|
||||
byte message[] = {cv1(BIT_MANIPULATE, ackManagerCv), cv2(ackManagerCv), instruction };
|
||||
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
DCCTrack::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
callbackState=AFTER_WRITE;
|
||||
}
|
||||
break;
|
||||
|
||||
case WB: // write byte
|
||||
break;
|
||||
|
||||
case WB: // write byte
|
||||
{
|
||||
if (checkResets( RESET_MIN)) return;
|
||||
if (Diag::ACK) DIAG(F("WB cv=%d value=%d"),ackManagerCv,ackManagerByte);
|
||||
byte message[] = {cv1(WRITE_BYTE, ackManagerCv), cv2(ackManagerCv), ackManagerByte};
|
||||
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
DCCTrack::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
callbackState=AFTER_WRITE;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case VB: // Issue validate Byte packet
|
||||
{
|
||||
if (checkResets( RESET_MIN)) return;
|
||||
if (checkResets( RESET_MIN)) return;
|
||||
if (Diag::ACK) DIAG(F("VB cv=%d value=%d"),ackManagerCv,ackManagerByte);
|
||||
byte message[] = { cv1(VERIFY_BYTE, ackManagerCv), cv2(ackManagerCv), ackManagerByte};
|
||||
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
DCCTrack::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case V0:
|
||||
case V1: // Issue validate bit=0 or bit=1 packet
|
||||
{
|
||||
if (checkResets(RESET_MIN)) return;
|
||||
if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
|
||||
if (checkResets(RESET_MIN)) return;
|
||||
if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
|
||||
byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;
|
||||
byte message[] = {cv1(BIT_MANIPULATE, ackManagerCv), cv2(ackManagerCv), instruction };
|
||||
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
DCCTrack::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
||||
DCCWaveform::progTrack.setAckPending();
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case WACK: // wait for ack (or absence of ack)
|
||||
{
|
||||
byte ackState=2; // keep polling
|
||||
|
||||
|
||||
ackState=DCCWaveform::progTrack.getAck();
|
||||
if (ackState==2) return; // keep polling
|
||||
ackReceived=ackState==1;
|
||||
@@ -836,14 +839,14 @@ void DCC::ackManagerLoop() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case ITCB: // If True callback(byte)
|
||||
if (ackReceived) {
|
||||
callback(ackManagerByte);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case ITCBV: // If True callback(byte) - Verify
|
||||
if (ackReceived) {
|
||||
if (ackManagerByte == ackManagerByteVerify) {
|
||||
@@ -854,22 +857,22 @@ void DCC::ackManagerLoop() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case ITCB7: // If True callback(byte & 0x7F)
|
||||
if (ackReceived) {
|
||||
callback(ackManagerByte & 0x7F);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case NAKFAIL: // If nack callback(-1)
|
||||
if (!ackReceived) {
|
||||
callback(-1);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case FAIL: // callback(-1)
|
||||
|
||||
case CALLFAIL: // callback(-1)
|
||||
callback(-1);
|
||||
return;
|
||||
|
||||
@@ -879,63 +882,63 @@ void DCC::ackManagerLoop() {
|
||||
|
||||
case STARTMERGE:
|
||||
ackManagerBitNum=7;
|
||||
ackManagerByte=0;
|
||||
ackManagerByte=0;
|
||||
break;
|
||||
|
||||
|
||||
case MERGE: // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)
|
||||
ackManagerByte <<= 1;
|
||||
// ackReceived means bit is zero.
|
||||
// ackReceived means bit is zero.
|
||||
if (!ackReceived) ackManagerByte |= 1;
|
||||
ackManagerBitNum--;
|
||||
break;
|
||||
|
||||
case SETBIT:
|
||||
ackManagerProg++;
|
||||
ackManagerProg++;
|
||||
ackManagerBitNum=GETFLASH(ackManagerProg);
|
||||
break;
|
||||
|
||||
case SETCV:
|
||||
ackManagerProg++;
|
||||
ackManagerProg++;
|
||||
ackManagerCv=GETFLASH(ackManagerProg);
|
||||
break;
|
||||
|
||||
case SETBYTE:
|
||||
ackManagerProg++;
|
||||
ackManagerProg++;
|
||||
ackManagerByte=GETFLASH(ackManagerProg);
|
||||
break;
|
||||
|
||||
case SETBYTEH:
|
||||
ackManagerByte=highByte(ackManagerWord);
|
||||
break;
|
||||
|
||||
|
||||
case SETBYTEL:
|
||||
ackManagerByte=lowByte(ackManagerWord);
|
||||
break;
|
||||
|
||||
case STASHLOCOID:
|
||||
ackManagerStash=ackManagerByte; // stash value from CV17
|
||||
ackManagerStash=ackManagerByte; // stash value from CV17
|
||||
break;
|
||||
|
||||
case COMBINELOCOID:
|
||||
|
||||
case COMBINELOCOID:
|
||||
// ackManagerStash is cv17, ackManagerByte is CV 18
|
||||
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
|
||||
return;
|
||||
callback( ackManagerByte + ((ackManagerStash - 192) << 8));
|
||||
return;
|
||||
|
||||
case ITSKIP:
|
||||
if (!ackReceived) break;
|
||||
if (!ackReceived) break;
|
||||
// SKIP opcodes until SKIPTARGET found
|
||||
while (opcode!=SKIPTARGET) {
|
||||
ackManagerProg++;
|
||||
ackManagerProg++;
|
||||
opcode=GETFLASH(ackManagerProg);
|
||||
}
|
||||
break;
|
||||
case SKIPTARGET:
|
||||
break;
|
||||
default:
|
||||
case SKIPTARGET:
|
||||
break;
|
||||
default:
|
||||
DIAG(F("!! ackOp %d FAULT!!"),opcode);
|
||||
callback( -1);
|
||||
return;
|
||||
|
||||
return;
|
||||
|
||||
} // end of switch
|
||||
ackManagerProg++;
|
||||
}
|
||||
@@ -956,7 +959,7 @@ void DCC::callback(int value) {
|
||||
// Rule 1: If we have written to a decoder we must maintain power for 100mS
|
||||
// Rule 2: If we are re-joining the main track we must power off for 30mS
|
||||
|
||||
switch (callbackState) {
|
||||
switch (callbackState) {
|
||||
case AFTER_WRITE: // first attempt to callback after a write operation
|
||||
if (!ackManagerRejoin && !DCCWaveform::progTrack.autoPowerOff) {
|
||||
callbackState=READY;
|
||||
@@ -966,7 +969,7 @@ void DCC::callback(int value) {
|
||||
callbackState=WAITING_100;
|
||||
if (Diag::ACK) DIAG(F("Stable 100mS"));
|
||||
break;
|
||||
|
||||
|
||||
case WAITING_100: // waiting for 100mS
|
||||
if (millis()-callbackStart < 100) break;
|
||||
// stable after power maintained for 100mS
|
||||
@@ -975,34 +978,32 @@ void DCC::callback(int value) {
|
||||
// but if we will keep the power on, we must off it for 30mS
|
||||
if (DCCWaveform::progTrack.autoPowerOff) callbackState=READY;
|
||||
else { // Need to cycle power off and on
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
callbackStart=millis();
|
||||
callbackState=WAITING_30;
|
||||
if (Diag::ACK) DIAG(F("OFF 30mS"));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case WAITING_30: // waiting for 30mS with power off
|
||||
if (millis()-callbackStart < 30) break;
|
||||
//power has been off for 30mS
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
callbackState=READY;
|
||||
break;
|
||||
|
||||
|
||||
case READY: // ready after read, or write after power delay and off period.
|
||||
// power off if we powered it on
|
||||
if (DCCWaveform::progTrack.autoPowerOff) {
|
||||
if (Diag::ACK) DIAG(F("Auto Prog power off"));
|
||||
DCCWaveform::progTrack.doAutoPowerOff();
|
||||
if (MotorDriver::commonFaultPin)
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
}
|
||||
// Restore <1 JOIN> to state before BASELINE
|
||||
if (ackManagerRejoin) {
|
||||
setProgTrackSyncMain(true);
|
||||
if (Diag::ACK) DIAG(F("Auto JOIN"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ackManagerProg=NULL; // no more steps to execute
|
||||
if (Diag::ACK) DIAG(F("Callback(%d)"),value);
|
||||
(ackManagerCallback)( value);
|
||||
@@ -1015,10 +1016,10 @@ void DCC::displayCabList(Print * stream) {
|
||||
for (int reg = 0; reg < MAX_LOCOS; reg++) {
|
||||
if (speedTable[reg].loco>0) {
|
||||
used ++;
|
||||
StringFormatter::send(stream,F("cab=%d, speed=%d, dir=%c \n"),
|
||||
StringFormatter::send(stream,F("cab=%d, speed=%d, dir=%c \n"),
|
||||
speedTable[reg].loco, speedTable[reg].speedCode & 0x7f,(speedTable[reg].speedCode & 0x80) ? 'F':'R');
|
||||
}
|
||||
}
|
||||
StringFormatter::send(stream,F("Used=%d, max=%d\n"),used,MAX_LOCOS);
|
||||
|
||||
|
||||
}
|
||||
|
33
DCC.h
33
DCC.h
@@ -1,10 +1,5 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -28,16 +23,6 @@
|
||||
#include "MotorDrivers.h"
|
||||
#include "FSH.h"
|
||||
|
||||
#include "defines.h"
|
||||
#ifndef HIGHEST_SHORT_ADDR
|
||||
#define HIGHEST_SHORT_ADDR 127
|
||||
#else
|
||||
#if HIGHEST_SHORT_ADDR > 127
|
||||
#error short addr greater than 127 does not make sense
|
||||
#endif
|
||||
#endif
|
||||
const uint16_t LONG_ADDR_MARKER = 0x4000;
|
||||
|
||||
typedef void (*ACK_CALLBACK)(int16_t result);
|
||||
|
||||
enum ackOp : byte
|
||||
@@ -56,7 +41,7 @@ enum ackOp : byte
|
||||
ITCBV, // If True callback(byte) - end of Verify Byte
|
||||
ITCB7, // If True callback(byte &0x7F)
|
||||
NAKFAIL, // if false callback(-1)
|
||||
FAIL, // callback(-1)
|
||||
CALLFAIL, // callback(-1)
|
||||
BIV, // Set ackManagerByte to initial value for Verify retry
|
||||
STARTMERGE, // Clear bit and byte settings ready for merge pass
|
||||
MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)
|
||||
@@ -92,7 +77,7 @@ const byte MAX_LOCOS = 50;
|
||||
class DCC
|
||||
{
|
||||
public:
|
||||
static void begin(const FSH * motorShieldName, MotorDriver *mainDriver, MotorDriver *progDriver);
|
||||
static void begin();
|
||||
static void setJoinRelayPin(byte joinRelayPin);
|
||||
static void loop();
|
||||
|
||||
@@ -104,9 +89,8 @@ public:
|
||||
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
|
||||
static void setFunction(int cab, byte fByte, byte eByte);
|
||||
static void setFn(int cab, int16_t functionNumber, bool on);
|
||||
static void changeFn(int cab, int16_t functionNumber);
|
||||
static int changeFn(int cab, int16_t functionNumber, bool pressed);
|
||||
static int getFn(int cab, int16_t functionNumber);
|
||||
static uint32_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);
|
||||
@@ -140,6 +124,7 @@ public:
|
||||
return ackRetryPSum;
|
||||
};
|
||||
|
||||
private:
|
||||
struct LOCO
|
||||
{
|
||||
int loco;
|
||||
@@ -147,9 +132,6 @@ public:
|
||||
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);
|
||||
@@ -160,6 +142,7 @@ 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);
|
||||
@@ -224,6 +207,10 @@ private:
|
||||
#define ARDUINO_TYPE "TEENSY40"
|
||||
#elif defined(ARDUINO_TEENSY41)
|
||||
#define ARDUINO_TYPE "TEENSY41"
|
||||
#elif defined(ARDUINO_ARCH_ESP8266)
|
||||
#define ARDUINO_TYPE "ESP8266"
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
#define ARDUINO_TYPE "ESP32"
|
||||
#else
|
||||
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH AN ARDUINO UNO, NANO 328, OR ARDUINO MEGA 1280/2560
|
||||
#endif
|
||||
|
23
DCCEX.h
23
DCCEX.h
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020 Harald Barth. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -30,9 +29,14 @@
|
||||
#include "DCC.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "SerialManager.h"
|
||||
#include "version.h"
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#include "WifiESP8266.h"
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
#include "WifiESP32.h"
|
||||
#else
|
||||
#include "WifiInterface.h"
|
||||
#endif
|
||||
#if ETHERNET_ON == true
|
||||
#include "EthernetInterface.h"
|
||||
#endif
|
||||
@@ -43,7 +47,12 @@
|
||||
#include "Turnouts.h"
|
||||
#include "Sensors.h"
|
||||
#include "Outputs.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "EXRAIL.h"
|
||||
#include "RMFT.h"
|
||||
|
||||
// not yet in this branch
|
||||
//#if __has_include ( "myAutomation.h")
|
||||
// #include "RMFT.h"
|
||||
// #define RMFT_ACTIVE
|
||||
//#endif
|
||||
|
||||
#endif
|
||||
|
302
DCCEXParser.cpp
302
DCCEXParser.cpp
@@ -1,12 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -23,10 +17,12 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "DCC.h" // includes "Motordriver.h" and <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "StringFormatter.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DCCTrack.h"
|
||||
#include "Turnouts.h"
|
||||
#include "Outputs.h"
|
||||
#include "Sensors.h"
|
||||
@@ -34,10 +30,12 @@
|
||||
#include "GITHUB_SHA.h"
|
||||
#include "version.h"
|
||||
#include "defines.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
#include "EEStore.h"
|
||||
#include "DIAG.h"
|
||||
#ifndef ESP_FAMILY
|
||||
#include <avr/wdt.h>
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@@ -62,9 +60,7 @@ const int16_t HASH_KEYWORD_ON = 2657;
|
||||
const int16_t HASH_KEYWORD_DCC = 6436;
|
||||
const int16_t HASH_KEYWORD_SLOW = -17209;
|
||||
const int16_t HASH_KEYWORD_PROGBOOST = -6353;
|
||||
#ifndef DISABLE_EEPROM
|
||||
const int16_t HASH_KEYWORD_EEPROM = -7168;
|
||||
#endif
|
||||
const int16_t HASH_KEYWORD_LIMIT = 27413;
|
||||
const int16_t HASH_KEYWORD_MAX = 16244;
|
||||
const int16_t HASH_KEYWORD_MIN = 15978;
|
||||
@@ -81,12 +77,15 @@ 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;
|
||||
@@ -97,8 +96,46 @@ 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;
|
||||
}
|
||||
|
||||
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd, bool usehex)
|
||||
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)
|
||||
{
|
||||
byte state = 1;
|
||||
byte parameterCount = 0;
|
||||
@@ -136,15 +173,10 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
case 3: // building a parameter
|
||||
if (hot >= '0' && hot <= '9')
|
||||
{
|
||||
runningValue = (usehex?16:10) * runningValue + (hot - '0');
|
||||
runningValue = 10 * runningValue + (hot - '0');
|
||||
break;
|
||||
}
|
||||
if (hot >= 'a' && hot <= 'z') hot=hot-'a'+'A'; // uppercase a..z
|
||||
if (usehex && hot>='A' && hot<='F') {
|
||||
// treat A..F as hex not keyword
|
||||
runningValue = 16 * runningValue + (hot - 'A' + 10);
|
||||
break;
|
||||
}
|
||||
if (hot >= 'A' && hot <= 'Z')
|
||||
{
|
||||
// Since JMRI got modified to send keywords in some rare cases, we need this
|
||||
@@ -162,6 +194,66 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
return parameterCount;
|
||||
}
|
||||
|
||||
int16_t DCCEXParser::splitHexValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
|
||||
{
|
||||
byte state = 1;
|
||||
byte parameterCount = 0;
|
||||
int16_t runningValue = 0;
|
||||
const byte *remainingCmd = cmd + 1; // skips the opcode
|
||||
|
||||
// clear all parameters in case not enough found
|
||||
for (int16_t i = 0; i < MAX_COMMAND_PARAMS; i++)
|
||||
result[i] = 0;
|
||||
|
||||
while (parameterCount < MAX_COMMAND_PARAMS)
|
||||
{
|
||||
byte hot = *remainingCmd;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
|
||||
case 1: // skipping spaces before a param
|
||||
if (hot == ' ')
|
||||
break;
|
||||
if (hot == '\0' || hot == '>')
|
||||
return parameterCount;
|
||||
state = 2;
|
||||
continue;
|
||||
|
||||
case 2: // checking first hex digit
|
||||
runningValue = 0;
|
||||
state = 3;
|
||||
continue;
|
||||
|
||||
case 3: // building a parameter
|
||||
if (hot >= '0' && hot <= '9')
|
||||
{
|
||||
runningValue = 16 * runningValue + (hot - '0');
|
||||
break;
|
||||
}
|
||||
if (hot >= 'A' && hot <= 'F')
|
||||
{
|
||||
runningValue = 16 * runningValue + 10 + (hot - 'A');
|
||||
break;
|
||||
}
|
||||
if (hot >= 'a' && hot <= 'f')
|
||||
{
|
||||
runningValue = 16 * runningValue + 10 + (hot - 'a');
|
||||
break;
|
||||
}
|
||||
if (hot==' ' || hot=='>' || hot=='\0') {
|
||||
result[parameterCount] = runningValue;
|
||||
parameterCount++;
|
||||
state = 1;
|
||||
continue;
|
||||
}
|
||||
return -1; // invalid hex digit
|
||||
}
|
||||
remainingCmd++;
|
||||
}
|
||||
return parameterCount;
|
||||
}
|
||||
|
||||
FILTER_CALLBACK DCCEXParser::filterCallback = 0;
|
||||
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
|
||||
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
|
||||
@@ -180,7 +272,6 @@ 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);
|
||||
@@ -191,17 +282,15 @@ void DCCEXParser::parse(const FSH * cmd) {
|
||||
|
||||
void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
{
|
||||
#ifndef DISABLE_EEPROM
|
||||
(void)EEPROM; // tell compiler not to warn this is unused
|
||||
#endif
|
||||
if (Diag::CMD)
|
||||
DIAG(F("PARSING:%s"), com);
|
||||
int16_t p[MAX_COMMAND_PARAMS];
|
||||
while (com[0] == '<' || com[0] == ' ')
|
||||
com++; // strip off any number of < or spaces
|
||||
byte params = splitValues(p, com);
|
||||
byte opcode = com[0];
|
||||
byte params = splitValues(p, com, opcode=='M' || opcode=='P');
|
||||
|
||||
|
||||
if (filterCallback)
|
||||
filterCallback(stream, opcode, params, p);
|
||||
if (filterRMFTCallback && opcode!='\0')
|
||||
@@ -249,9 +338,10 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
break; // invalid direction code
|
||||
|
||||
DCC::setThrottle(cab, tspeed, direction);
|
||||
if (params == 4) // send obsolete format T response
|
||||
if (params == 4)
|
||||
StringFormatter::send(stream, F("<T %d %d %d>\n"), p[0], p[2], p[3]);
|
||||
// speed change will be broadcast anyway in new <l > format
|
||||
else
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
return;
|
||||
}
|
||||
case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>
|
||||
@@ -282,7 +372,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
|| ((p[activep] & 0x01) != p[activep]) // invalid activate 0|1
|
||||
) break;
|
||||
// Honour the configuration option (config.h) which allows the <a> command to be reversed
|
||||
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
|
||||
#ifdef DCC_ACCESSORY_RCN_213
|
||||
DCC::setAccessory(address, subaddress,p[activep]==0);
|
||||
#else
|
||||
DCC::setAccessory(address, subaddress,p[activep]==1);
|
||||
@@ -315,8 +405,8 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
|
||||
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
|
||||
case 'P': // WRITE TRANSPARENT DCC PACKET PROG <P REG X1 ... X9>
|
||||
// NOTE: this command was parsed in HEX instead of decimal
|
||||
params--; // drop REG
|
||||
// Re-parse the command using a hex-only splitter
|
||||
params=splitHexValues(p,com)-1; // drop REG
|
||||
if (params<1) break;
|
||||
{
|
||||
byte packet[params];
|
||||
@@ -324,7 +414,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
packet[i]=(byte)p[i+1];
|
||||
if (Diag::CMD) DIAG(F("packet[%d]=%d (0x%x)"), i, packet[i], packet[i]);
|
||||
}
|
||||
(opcode=='M'?DCCWaveform::mainTrack:DCCWaveform::progTrack).schedulePacket(packet,params,3);
|
||||
(opcode=='M'?DCCTrack::mainTrack:DCCTrack::progTrack).schedulePacket(packet,params,3);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -377,70 +467,61 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
}
|
||||
break;
|
||||
|
||||
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
|
||||
{
|
||||
bool main=false;
|
||||
bool prog=false;
|
||||
bool join=false;
|
||||
if (params > 1) break;
|
||||
if (params==0 || MotorDriver::commonFaultPin) { // <1> or tracks can not be handled individually
|
||||
main=true;
|
||||
prog=true;
|
||||
}
|
||||
if (params==1) {
|
||||
if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN>
|
||||
main=true;
|
||||
prog=true;
|
||||
join=true;
|
||||
}
|
||||
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 '1': // POWERON <1 [MAIN|PROG]>
|
||||
case '0': // POWEROFF <0 [MAIN | PROG] >
|
||||
if (params > 1)
|
||||
break;
|
||||
{
|
||||
bool main=false;
|
||||
bool prog=false;
|
||||
if (params > 1) break;
|
||||
if (params==0 || MotorDriver::commonFaultPin) { // <0> or tracks can not be handled individually
|
||||
main=true;
|
||||
prog=true;
|
||||
}
|
||||
if (params==1) {
|
||||
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>
|
||||
}
|
||||
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;
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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.
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
return;
|
||||
|
||||
case 'c': // SEND METER RESPONSES <c>
|
||||
@@ -463,7 +544,6 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
// TODO Send stats of speed reminders table
|
||||
return;
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
case 'E': // STORE EPROM <E>
|
||||
EEStore::store();
|
||||
StringFormatter::send(stream, F("<e %d %d %d>\n"), EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs);
|
||||
@@ -473,7 +553,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
EEStore::clear();
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
return;
|
||||
#endif
|
||||
|
||||
case ' ': // < >
|
||||
StringFormatter::send(stream, F("\n"));
|
||||
return;
|
||||
@@ -494,17 +574,16 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
return;
|
||||
|
||||
case 'F': // New command to call the new Loco Function API <F cab func 1|0>
|
||||
if(params!=3) break;
|
||||
if (Diag::CMD)
|
||||
DIAG(F("Setting loco %d F%d %S"), p[0], p[1], p[2] ? F("ON") : F("OFF"));
|
||||
DCC::setFn(p[0], p[1], p[2] == 1);
|
||||
return;
|
||||
|
||||
case '+': // Complex Wifi interface command (not usual parse)
|
||||
if (atCommandCallback && !ringStream) {
|
||||
if (atCommandCallback) {
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
atCommandCallback((HardwareSerial *)stream,com);
|
||||
atCommandCallback(com);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -648,7 +727,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -741,10 +822,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
|
||||
} else if (p[1] == HASH_KEYWORD_MIN) {
|
||||
DCCWaveform::progTrack.setMinAckPulseDuration(p[2]);
|
||||
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
|
||||
LCD(0, F("Ack Min=%dus"), p[2]); // <D ACK MIN 1500>
|
||||
} else if (p[1] == HASH_KEYWORD_MAX) {
|
||||
DCCWaveform::progTrack.setMaxAckPulseDuration(p[2]);
|
||||
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
|
||||
LCD(0, F("Ack Max=%dus"), p[2]); // <D ACK MAX 9000>
|
||||
} else if (p[1] == HASH_KEYWORD_RETRY) {
|
||||
if (p[2] >255) p[2]=3;
|
||||
LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCC::setAckRetry(p[2])); // <D ACK RETRY 2>
|
||||
@@ -783,17 +864,19 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
|
||||
case HASH_KEYWORD_RESET:
|
||||
{
|
||||
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
|
||||
delay(50); // wait for the prescaller time to expire
|
||||
#ifndef ESP_FAMILY
|
||||
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
|
||||
delay(50); // wait for the prescaler time to expire
|
||||
#else
|
||||
/* XXX do right thing to reboot */
|
||||
#endif
|
||||
break; // and <X> if we didnt restart
|
||||
}
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
|
||||
case HASH_KEYWORD_EEPROM: // <D EEPROM NumEntries>
|
||||
if (params >= 2)
|
||||
EEStore::dump(p[1]);
|
||||
return true;
|
||||
#endif
|
||||
|
||||
case HASH_KEYWORD_SPEED28:
|
||||
DCC::setGlobalSpeedsteps(28);
|
||||
@@ -883,21 +966,10 @@ void DCCEXParser::callback_R(int16_t result)
|
||||
commitAsyncReplyStream();
|
||||
}
|
||||
|
||||
void DCCEXParser::callback_Rloco(int16_t result) {
|
||||
const FSH * detail;
|
||||
if (result<=0) {
|
||||
detail=F("<r %d>\n");
|
||||
} else {
|
||||
bool longAddr=result & LONG_ADDR_MARKER; //long addr
|
||||
if (longAddr)
|
||||
result = result^LONG_ADDR_MARKER;
|
||||
if (longAddr && result <= HIGHEST_SHORT_ADDR)
|
||||
detail=F("<r LONG %d UNSUPPORTED>\n");
|
||||
else
|
||||
detail=F("<r %d>\n");
|
||||
}
|
||||
StringFormatter::send(getAsyncReplyStream(), detail, result);
|
||||
commitAsyncReplyStream();
|
||||
void DCCEXParser::callback_Rloco(int16_t result)
|
||||
{
|
||||
StringFormatter::send(getAsyncReplyStream(), F("<r %d>\n"), result);
|
||||
commitAsyncReplyStream();
|
||||
}
|
||||
|
||||
void DCCEXParser::callback_Wloco(int16_t result)
|
||||
|
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -26,13 +23,15 @@
|
||||
#include "RingStream.h"
|
||||
|
||||
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
|
||||
typedef void (*AT_COMMAND_CALLBACK)(const byte * command);
|
||||
|
||||
struct DCCEXParser
|
||||
{
|
||||
|
||||
static void parse(Print * stream, byte * command, RingStream * ringStream);
|
||||
static void parse(const FSH * cmd);
|
||||
DCCEXParser();
|
||||
void loop(Stream & stream);
|
||||
void parse(Print * stream, byte * command, RingStream * ringStream);
|
||||
void parse(const FSH * cmd);
|
||||
void flush();
|
||||
static void setFilter(FILTER_CALLBACK filter);
|
||||
static void setRMFTFilter(FILTER_CALLBACK filter);
|
||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||
@@ -41,13 +40,17 @@ struct DCCEXParser
|
||||
private:
|
||||
|
||||
static const int16_t MAX_BUFFER=50; // longest command sent in
|
||||
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
|
||||
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 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[]);
|
||||
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 Print * getAsyncReplyStream();
|
||||
static void commitAsyncReplyStream();
|
||||
@@ -58,7 +61,7 @@ struct DCCEXParser
|
||||
static RingStream * stashRingStream;
|
||||
|
||||
static int16_t stashP[MAX_COMMAND_PARAMS];
|
||||
static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
|
||||
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);
|
||||
|
12
DCCPacket.h
Normal file
12
DCCPacket.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
|
||||
|
||||
class dccPacket {
|
||||
public:
|
||||
byte data[MAX_PACKET_SIZE+1]; // space for checksum if needed
|
||||
byte length : 4; // future proof up to 15
|
||||
byte repeat : 4; // hopefully 15 enough for ever
|
||||
//byte priority : 2; // 0 repeats; 1 mobile function ; 2 accessory ; 3 mobile speed
|
||||
};
|
||||
|
189
DCCRMT.cpp
Normal file
189
DCCRMT.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* © 2021, Harald Barth.
|
||||
*
|
||||
* 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 "config.h"
|
||||
#include "defines.h"
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include "DIAG.h"
|
||||
#include "DCCRMT.h"
|
||||
#include "DCCWaveform.h" // for MAX_PACKET_SIZE
|
||||
#include "soc/gpio_sig_map.h"
|
||||
|
||||
#define DATA_LEN(X) ((X)*9+1) // Each byte has one bit extra and we have one EOF marker
|
||||
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,2,0)
|
||||
#error wrong IDF version
|
||||
#endif
|
||||
|
||||
void setDCCBit1(rmt_item32_t* item) {
|
||||
item->level0 = 1;
|
||||
item->duration0 = DCC_1_HALFPERIOD;
|
||||
item->level1 = 0;
|
||||
item->duration1 = DCC_1_HALFPERIOD;
|
||||
}
|
||||
|
||||
void setDCCBit0(rmt_item32_t* item) {
|
||||
item->level0 = 1;
|
||||
item->duration0 = DCC_0_HALFPERIOD;
|
||||
item->level1 = 0;
|
||||
item->duration1 = DCC_0_HALFPERIOD;
|
||||
}
|
||||
|
||||
// special long zero to trigger scope
|
||||
void setDCCBit0Long(rmt_item32_t* item) {
|
||||
item->level0 = 1;
|
||||
item->duration0 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;
|
||||
item->level1 = 0;
|
||||
item->duration1 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;
|
||||
}
|
||||
|
||||
void setEOT(rmt_item32_t* item) {
|
||||
item->val = 0;
|
||||
}
|
||||
|
||||
void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {
|
||||
RMTChannel *tt = (RMTChannel *)t;
|
||||
tt->RMTinterrupt();
|
||||
}
|
||||
|
||||
RMTChannel::RMTChannel(byte pin, byte ch, byte plen) {
|
||||
|
||||
// preamble
|
||||
preambleLen = plen+2; // plen 1 bits, one 0 bit and one EOF marker
|
||||
preamble = (rmt_item32_t*)malloc(preambleLen*sizeof(rmt_item32_t));
|
||||
for (byte n=0; n<plen; n++)
|
||||
setDCCBit1(preamble + n); // preamble bits
|
||||
#ifdef SCOPE
|
||||
setDCCBit0Long(preamble + plen); // start of packet 0 bit long version
|
||||
#else
|
||||
setDCCBit0(preamble + plen); // start of packet 0 bit normal version
|
||||
#endif
|
||||
setEOT(preamble + plen + 1); // EOT marker
|
||||
|
||||
// idle
|
||||
idleLen = 28;
|
||||
idle = (rmt_item32_t*)malloc(idleLen*sizeof(rmt_item32_t));
|
||||
for (byte n=0; n<8; n++) // 0 to 7
|
||||
setDCCBit1(idle + n);
|
||||
for (byte n=8; n<18; n++) // 8, 9 to 16, 17
|
||||
setDCCBit0(idle + n);
|
||||
for (byte n=18; n<26; n++) // 18 to 25
|
||||
setDCCBit1(idle + n);
|
||||
setDCCBit1(idle + 26); // end bit
|
||||
setEOT(idle + 27); // EOT marker
|
||||
|
||||
// data: max packet size today is 5 + checksum
|
||||
maxDataLen = DATA_LEN(MAX_PACKET_SIZE);
|
||||
data = (rmt_item32_t*)malloc(maxDataLen*sizeof(rmt_item32_t));
|
||||
|
||||
rmt_config_t config;
|
||||
// Configure the RMT channel for TX
|
||||
bzero(&config, sizeof(rmt_config_t));
|
||||
config.rmt_mode = RMT_MODE_TX;
|
||||
config.channel = channel = (rmt_channel_t)ch;
|
||||
config.clk_div = RMT_CLOCK_DIVIDER;
|
||||
config.gpio_num = (gpio_num_t)pin;
|
||||
config.mem_block_num = 2; // With longest DCC packet 11 inc checksum (future expansion)
|
||||
// number of bits needed is 22preamble + start +
|
||||
// 11*9 + extrazero + EOT = 124
|
||||
// 2 mem block of 64 RMT items should be enough
|
||||
|
||||
ESP_ERROR_CHECK(rmt_config(&config));
|
||||
/*
|
||||
// test: config another gpio pin
|
||||
gpio_num_t gpioNum = (gpio_num_t)(pin-1);
|
||||
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
|
||||
gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
|
||||
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX, 0, 0);
|
||||
*/
|
||||
|
||||
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
|
||||
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));
|
||||
|
||||
DIAG(F("Register interrupt on core %d"), xPortGetCoreID());
|
||||
|
||||
ESP_ERROR_CHECK(rmt_set_tx_loop_mode(channel, true));
|
||||
rmt_register_tx_end_callback(interrupt, this);
|
||||
rmt_set_tx_intr_en(channel, true);
|
||||
|
||||
DIAG(F("Starting channel %d signal generator"), config.channel);
|
||||
|
||||
// send one bit to kickstart the signal, remaining data will come from the
|
||||
// packet queue. We intentionally do not wait for the RMT TX complete here.
|
||||
//rmt_write_items(channel, preamble, preambleLen, false);
|
||||
RMTprefill();
|
||||
preambleNext = true;
|
||||
dataReady = false;
|
||||
RMTinterrupt();
|
||||
}
|
||||
|
||||
void RMTChannel::RMTprefill() {
|
||||
rmt_fill_tx_items(channel, preamble, preambleLen, 0);
|
||||
rmt_fill_tx_items(channel, idle, idleLen, preambleLen-1);
|
||||
}
|
||||
|
||||
const byte transmitMask[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
//bool RMTChannel::RMTfillData(const byte buffer[], byte byteCount, byte repeatCount=0) {
|
||||
bool RMTChannel::RMTfillData(dccPacket packet) {
|
||||
// dataReady: Signals to then interrupt routine. It is set when
|
||||
// we have data in the channel buffer which can be copied out
|
||||
// to the HW. dataRepeat on the other hand signals back to
|
||||
// the caller of this function if the data has been sent enough
|
||||
// times (0 to 3 means 1 to 4 times in total).
|
||||
if (dataReady == true || dataRepeat > 0) // we have still old work to do
|
||||
return false;
|
||||
if (DATA_LEN(packet.length) > maxDataLen) { // this would overun our allocated memory for data
|
||||
DIAG(F("Can not convert DCC bytes # %d to DCC bits %d, buffer too small"), packet.length, maxDataLen);
|
||||
return false; // something very broken, can not convert packet
|
||||
}
|
||||
|
||||
byte *buffer = packet.data;
|
||||
|
||||
// convert bytes to RMT stream of "bits"
|
||||
byte bitcounter = 0;
|
||||
for(byte n=0; n<packet.length; n++) {
|
||||
for(byte bit=0; bit<8; bit++) {
|
||||
if (buffer[n] & transmitMask[bit])
|
||||
setDCCBit1(data + bitcounter++);
|
||||
else
|
||||
setDCCBit0(data + bitcounter++);
|
||||
}
|
||||
setDCCBit0(data + bitcounter++); // zero at end of each byte
|
||||
}
|
||||
setDCCBit1(data + bitcounter-1); // overwrite previous zero bit with one bit
|
||||
setEOT(data + bitcounter++); // EOT marker
|
||||
dataLen = bitcounter;
|
||||
dataReady = true;
|
||||
dataRepeat = packet.repeat+1; // repeatCount of 0 means send once
|
||||
return true;
|
||||
}
|
||||
|
||||
void IRAM_ATTR RMTChannel::RMTinterrupt() {
|
||||
//no rmt_tx_start(channel,true) as we run in loop mode
|
||||
//preamble is always loaded at beginning of buffer
|
||||
if (dataReady) { // if we have new data, fill while preamble is running
|
||||
rmt_fill_tx_items(channel, data, dataLen, preambleLen-1);
|
||||
dataReady = false;
|
||||
}
|
||||
if (dataRepeat > 0) // if a repeat count was specified, work on that
|
||||
dataRepeat--;
|
||||
return;
|
||||
}
|
||||
#endif //ESP32
|
61
DCCRMT.h
Normal file
61
DCCRMT.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* © 2021, Harald Barth.
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include "DCCPacket.h"
|
||||
#include "driver/rmt.h"
|
||||
#include "soc/rmt_reg.h"
|
||||
#include "soc/rmt_struct.h"
|
||||
|
||||
// make calculations easy and set up for microseconds
|
||||
#define RMT_CLOCK_DIVIDER 80
|
||||
#define DCC_1_HALFPERIOD 58 //4640 // 1 / 80000000 * 4640 = 58us
|
||||
#define DCC_0_HALFPERIOD 100 //8000
|
||||
|
||||
class RMTChannel {
|
||||
public:
|
||||
RMTChannel(byte pin, byte ch, byte plen);
|
||||
void IRAM_ATTR RMTinterrupt();
|
||||
void RMTprefill();
|
||||
bool RMTfillData(dccPacket packet);
|
||||
//bool RMTfillData(const byte buffer[], byte byteCount, byte repeatCount);
|
||||
|
||||
static RMTChannel mainRMTChannel;
|
||||
static RMTChannel progRMTChannel;
|
||||
|
||||
private:
|
||||
|
||||
rmt_channel_t channel;
|
||||
// 3 types of data to send, preamble and then idle or data
|
||||
// if this is prog track, idle will contain reset instead
|
||||
rmt_item32_t *idle;
|
||||
byte idleLen;
|
||||
rmt_item32_t *preamble;
|
||||
byte preambleLen;
|
||||
rmt_item32_t *data;
|
||||
byte dataLen;
|
||||
byte maxDataLen;
|
||||
// flags
|
||||
volatile bool preambleNext = true; // alternate between preamble and content
|
||||
volatile bool dataReady = false; // do we have real data available or send idle
|
||||
volatile byte dataRepeat = 0;
|
||||
};
|
||||
#endif //ESP32
|
61
DCCTimer.cpp
61
DCCTimer.cpp
@@ -1,10 +1,5 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Chris Harlow
|
||||
* © 2021 David Cutting
|
||||
* All rights reserved.
|
||||
* © 2021, Chris Harlow & David Cutting. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -49,7 +44,7 @@
|
||||
|
||||
#include "DCCTimer.h"
|
||||
const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
|
||||
const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;
|
||||
const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME);
|
||||
|
||||
INTERRUPT_CALLBACK interruptHandler=0;
|
||||
|
||||
@@ -58,11 +53,11 @@ INTERRUPT_CALLBACK interruptHandler=0;
|
||||
|
||||
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
interruptHandler=callback;
|
||||
noInterrupts();
|
||||
noInterrupts();
|
||||
ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011; // speed up analogRead sample time
|
||||
TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
|
||||
TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; // 8 MHz ~ 0.125 us
|
||||
TCB0.CCMP = CLOCK_CYCLES -1; // 1 tick less for timer reset
|
||||
TCB0.CCMP = (CLOCK_CYCLES>>1) -1; // 1 tick less for timer reset
|
||||
TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
|
||||
TCB0.INTCTRL = TCB_CAPT_bm; // Enable the interrupt
|
||||
TCB0.CNT = 0;
|
||||
@@ -151,6 +146,50 @@ void DCCTimer::read(uint8_t word, uint8_t *mac, uint8_t offset) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#elif defined(ARDUINO_ARCH_ESP8266)
|
||||
|
||||
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
interruptHandler=callback;
|
||||
timer1_disable();
|
||||
|
||||
// There seem to be differnt ways to attach interrupt handler
|
||||
// ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);
|
||||
// ETS_FRC_TIMER1_NMI_INTR_ATTACH(interruptHandler);
|
||||
// Let us choose the one from the API
|
||||
timer1_attachInterrupt(interruptHandler);
|
||||
|
||||
// not exactly sure of order:
|
||||
timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);
|
||||
timer1_write(CLOCK_CYCLES);
|
||||
}
|
||||
// We do not support to use PWM to make the Waveform on ESP
|
||||
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
|
||||
return false;
|
||||
}
|
||||
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
|
||||
}
|
||||
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
// https://www.visualmicro.com/page/Timer-Interrupts-Explained.aspx
|
||||
|
||||
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
interruptHandler = callback;
|
||||
hw_timer_t *timer = NULL;
|
||||
timer = timerBegin(0, 2, true); // prescaler can be 2 to 65536 so choose 2
|
||||
timerAttachInterrupt(timer, interruptHandler, true);
|
||||
timerAlarmWrite(timer, CLOCK_CYCLES / 6, true); // divide by prescaler*3 (Clockbase is 80Mhz and not F_CPU 240Mhz)
|
||||
timerAlarmEnable(timer);
|
||||
}
|
||||
|
||||
// We do not support to use PWM to make the Waveform on ESP
|
||||
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
|
||||
return false;
|
||||
}
|
||||
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
|
||||
}
|
||||
|
||||
#else
|
||||
// Arduino nano, uno, mega etc
|
||||
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
|
||||
@@ -164,10 +203,10 @@ void DCCTimer::read(uint8_t word, uint8_t *mac, uint8_t offset) {
|
||||
|
||||
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
|
||||
interruptHandler=callback;
|
||||
noInterrupts();
|
||||
noInterrupts();
|
||||
ADCSRA = (ADCSRA & 0b11111000) | 0b00000100; // speed up analogRead sample time
|
||||
TCCR1A = 0;
|
||||
ICR1 = CLOCK_CYCLES;
|
||||
ICR1 = CLOCK_CYCLES>>1;
|
||||
TCNT1 = 0;
|
||||
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
|
||||
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
|
||||
|
16
DCCTimer.h
16
DCCTimer.h
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
* (c) 2021 Mike S. All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -39,4 +37,14 @@ class DCCTimer {
|
||||
private:
|
||||
};
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
extern portMUX_TYPE timerMux;
|
||||
#endif
|
||||
|
||||
#if !(defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266))
|
||||
#ifndef IRAM_ATTR
|
||||
#define IRAM_ATTR
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif //DCCTimer.h
|
||||
|
38
DCCTrack.cpp
Normal file
38
DCCTrack.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
#include "defines.h"
|
||||
#include "DCCTrack.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
DCCTrack::DCCTrack(DCCWaveform *w) {
|
||||
waveform = w;
|
||||
}
|
||||
|
||||
void DCCTrack::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
||||
dccPacket packet;
|
||||
|
||||
// add checksum now, makes stuff easier later
|
||||
byte checksum = 0;
|
||||
for (byte b = 0; b < byteCount; b++) {
|
||||
checksum ^= buffer[b];
|
||||
packet.data[b] = buffer[b];
|
||||
}
|
||||
packet.data[byteCount] = checksum;
|
||||
packet.length = byteCount + 1;
|
||||
packet.repeat = repeats;
|
||||
schedulePacket(packet);
|
||||
};
|
||||
|
||||
void DCCTrack::schedulePacket(dccPacket packet) {
|
||||
bool once=true;
|
||||
for (const auto& driver: mD) {
|
||||
if (driver->type() == RMT_MAIN || driver->type() == RMT_PROG) {
|
||||
//DIAG(F("DCCTrack::schedulePacket RMT l=%d d=%x"),packet.length, packet.data[0]);
|
||||
driver->schedulePacket(packet);
|
||||
}
|
||||
if (driver->type() & (TIMER_MAIN | TIMER_PROG) && waveform && once) {
|
||||
//DIAG(F("DCCTrack::schedulePacket WAVE l=%d d=%x"),packet.length, packet.data[0]);
|
||||
waveform->schedulePacket(packet);
|
||||
once=false;
|
||||
}
|
||||
}
|
||||
}
|
23
DCCTrack.h
Normal file
23
DCCTrack.h
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include "DCCPacket.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
class DCCTrack {
|
||||
public:
|
||||
DCCTrack(DCCWaveform *w);
|
||||
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
|
||||
void schedulePacket(dccPacket packet);
|
||||
inline void addDriver(MotorDriver *m) {
|
||||
mD.push_back(m);
|
||||
};
|
||||
static DCCTrack mainTrack;
|
||||
static DCCTrack progTrack;
|
||||
private:
|
||||
DCCWaveform *waveform;
|
||||
std::vector<MotorDriver *>mD;
|
||||
};
|
||||
|
||||
|
153
DCCWaveform.cpp
153
DCCWaveform.cpp
@@ -1,12 +1,8 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -24,14 +20,26 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "defines.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DCCTrack.h"
|
||||
#include "DCCTimer.h"
|
||||
#include "DIAG.h"
|
||||
#include "freeMemory.h"
|
||||
|
||||
// The two Waveforms which defines what happens when the
|
||||
// interrupt driven DCC signal is generated. This is tied
|
||||
// to the timer interrupts of the hardware.
|
||||
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
||||
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
||||
|
||||
|
||||
// The two different DCC _kinds_ of signals we want to be able
|
||||
// to genrate at the same time. When timer interupts are used,
|
||||
// these need the respective waveform
|
||||
DCCTrack DCCTrack::mainTrack(&DCCWaveform::mainTrack);
|
||||
DCCTrack DCCTrack::progTrack(&DCCWaveform::progTrack);
|
||||
|
||||
bool DCCWaveform::progTrackSyncMain=false;
|
||||
bool DCCWaveform::progTrackBoosted=false;
|
||||
int DCCWaveform::progTripValue=0;
|
||||
@@ -40,48 +48,89 @@ volatile uint8_t DCCWaveform::numAckSamples=0;
|
||||
uint8_t DCCWaveform::trailingEdgeCounter=0;
|
||||
|
||||
void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) {
|
||||
mainTrack.motorDriver=mainDriver;
|
||||
progTrack.motorDriver=progDriver;
|
||||
progTripValue = progDriver->mA2raw(TRIP_CURRENT_PROG); // need only calculate once hence static
|
||||
mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
progTrack.setPowerMode(POWERMODE::OFF);
|
||||
// Fault pin config for odd motor boards (example pololu)
|
||||
MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin())
|
||||
&& (mainDriver->getFaultPin() != UNUSED_PIN));
|
||||
// Only use PWM if both pins are PWM capable. Otherwise JOIN does not work
|
||||
MotorDriver::usePWM= mainDriver->isPWMCapable() && progDriver->isPWMCapable();
|
||||
DIAG(F("Signal pin config: %S accuracy waveform"),
|
||||
|
||||
if(mainDriver) {
|
||||
mainTrack.motorDriver=mainDriver;
|
||||
mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
}
|
||||
if(progDriver) {
|
||||
progTrack.motorDriver=progDriver;
|
||||
progTripValue = progDriver->mA2raw(TRIP_CURRENT_PROG); // need only calculate once hence static
|
||||
progTrack.setPowerMode(POWERMODE::OFF);
|
||||
}
|
||||
if(mainDriver && progDriver) {
|
||||
// Fault pin config for odd motor boards (example pololu)
|
||||
MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin())
|
||||
&& (mainDriver->getFaultPin() != UNUSED_PIN));
|
||||
// Only use PWM if both pins are PWM capable. Otherwise JOIN does not work
|
||||
MotorDriver::usePWM= mainDriver->isPWMCapable() && progDriver->isPWMCapable();
|
||||
}
|
||||
if(mainDriver || progDriver) {
|
||||
DIAG(F("Signal pin config: %S accuracy waveform"),
|
||||
MotorDriver::usePWM ? F("high") : F("normal") );
|
||||
DCCTimer::begin(DCCWaveform::interruptHandler);
|
||||
}
|
||||
DCCTimer::begin(DCCWaveform::interruptHandler);
|
||||
}
|
||||
|
||||
void DCCWaveform::loop(bool ackManagerActive) {
|
||||
mainTrack.checkPowerOverload(false);
|
||||
#ifdef SLOW_ANALOG_READ
|
||||
// Flag to hold if we need to run ack checking in loop
|
||||
volatile bool ackflag = 0;
|
||||
#endif
|
||||
|
||||
void IRAM_ATTR DCCWaveform::loop(bool ackManagerActive) {
|
||||
|
||||
//if (mainTrack.packetPendingRMT) {
|
||||
// mainTrack.rmtPin->RMTfillData(mainTrack.pendingPacket, mainTrack.pendingLength, mainTrack.pendingRepeats);
|
||||
// mainTrack.packetPendingRMT=false;
|
||||
// sentResetsSincePacket = 0 // later when progtrack
|
||||
//}
|
||||
|
||||
#ifdef SLOW_ANALOG_READ
|
||||
if (ackflag) {
|
||||
progTrack.checkAck();
|
||||
// reset flag AFTER check is done
|
||||
portENTER_CRITICAL(&timerMux);
|
||||
ackflag = 0;
|
||||
portEXIT_CRITICAL(&timerMux);
|
||||
} else {
|
||||
progTrack.checkPowerOverload(ackManagerActive);
|
||||
}
|
||||
#else
|
||||
progTrack.checkPowerOverload(ackManagerActive);
|
||||
#endif
|
||||
mainTrack.checkPowerOverload(false);
|
||||
}
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void DCCWaveform::interruptHandler() {
|
||||
void IRAM_ATTR DCCWaveform::interruptHandler() {
|
||||
// call the timer edge sensitive actions for progtrack and maintrack
|
||||
// member functions would be cleaner but have more overhead
|
||||
byte sigMain=signalTransform[mainTrack.state];
|
||||
byte sigProg=progTrackSyncMain? sigMain : signalTransform[progTrack.state];
|
||||
|
||||
// Set the signal state for both tracks
|
||||
mainTrack.motorDriver->setSignal(sigMain);
|
||||
progTrack.motorDriver->setSignal(sigProg);
|
||||
|
||||
if (mainTrack.motorDriver)
|
||||
mainTrack.motorDriver->setSignal(sigMain);
|
||||
if (progTrack.motorDriver)
|
||||
progTrack.motorDriver->setSignal(sigProg);
|
||||
// Move on in the state engine
|
||||
mainTrack.state=stateTransform[mainTrack.state];
|
||||
progTrack.state=stateTransform[progTrack.state];
|
||||
|
||||
|
||||
// WAVE_PENDING means we dont yet know what the next bit is
|
||||
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
|
||||
if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();
|
||||
else if (progTrack.ackPending) progTrack.checkAck();
|
||||
|
||||
if (mainTrack.state==WAVE_PENDING)
|
||||
mainTrack.interrupt2();
|
||||
if (progTrack.state==WAVE_PENDING)
|
||||
progTrack.interrupt2();
|
||||
#ifdef SLOW_ANALOG_READ
|
||||
else if (progTrack.ackPending && ackflag == 0) { // We need AND we are not already checking
|
||||
portENTER_CRITICAL(&timerMux);
|
||||
ackflag = 1;
|
||||
portEXIT_CRITICAL(&timerMux);
|
||||
}
|
||||
#else
|
||||
else if (progTrack.ackPending)
|
||||
progTrack.checkAck();
|
||||
#endif
|
||||
}
|
||||
#pragma GCC push_options
|
||||
|
||||
@@ -98,6 +147,7 @@ const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
|
||||
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
||||
isMainTrack = isMain;
|
||||
packetPending = false;
|
||||
packetPendingRMT = false;
|
||||
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
|
||||
state = WAVE_START;
|
||||
// The +1 below is to allow the preamble generator to create the stop bit
|
||||
@@ -117,12 +167,14 @@ POWERMODE DCCWaveform::getPowerMode() {
|
||||
void DCCWaveform::setPowerMode(POWERMODE mode) {
|
||||
powerMode = mode;
|
||||
bool ison = (mode == POWERMODE::ON);
|
||||
motorDriver->setPower( ison);
|
||||
if (motorDriver)
|
||||
motorDriver->setPower( ison);
|
||||
sentResetsSincePacket=0;
|
||||
}
|
||||
|
||||
|
||||
void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
|
||||
if (!motorDriver) return;
|
||||
if (millis() - lastSampleTaken < sampleDelay) return;
|
||||
lastSampleTaken = millis();
|
||||
int tripValue= motorDriver->getRawCurrentTripValue();
|
||||
@@ -206,7 +258,7 @@ const bool DCCWaveform::signalTransform[]={
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void DCCWaveform::interrupt2() {
|
||||
void IRAM_ATTR DCCWaveform::interrupt2() {
|
||||
// calculate the next bit to be sent:
|
||||
// set state WAVE_MID_1 for a 1=bit
|
||||
// or WAVE_HIGH_0 for a 0 bit.
|
||||
@@ -216,7 +268,9 @@ void DCCWaveform::interrupt2() {
|
||||
remainingPreambles--;
|
||||
// Update free memory diagnostic as we don't have anything else to do this time.
|
||||
// Allow for checkAck and its called functions using 22 bytes more.
|
||||
updateMinimumFreeMemory(22);
|
||||
#ifndef ESP_FAMILY
|
||||
updateMinimumFreeMemory(22);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,7 +295,8 @@ void DCCWaveform::interrupt2() {
|
||||
transmitRepeats--;
|
||||
}
|
||||
else if (packetPending) {
|
||||
// Copy pending packet to transmit packet
|
||||
portENTER_CRITICAL(&timerMux);
|
||||
// Copy pending packet to transmit packet
|
||||
// a fixed length memcpy is faster than a variable length loop for these small lengths
|
||||
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
|
||||
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
|
||||
@@ -250,6 +305,7 @@ void DCCWaveform::interrupt2() {
|
||||
transmitRepeats = pendingRepeats;
|
||||
packetPending = false;
|
||||
sentResetsSincePacket=0;
|
||||
portEXIT_CRITICAL(&timerMux);
|
||||
}
|
||||
else {
|
||||
// Fortunately reset and idle packets are the same length
|
||||
@@ -266,30 +322,33 @@ void DCCWaveform::interrupt2() {
|
||||
|
||||
// Wait until there is no packet pending, then make this pending
|
||||
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
||||
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
||||
if (byteCount > MAX_PACKET_SIZE+1) return; // has chksum
|
||||
while (packetPending);
|
||||
|
||||
byte checksum = 0;
|
||||
portENTER_CRITICAL(&timerMux);
|
||||
//byte checksum = 0;
|
||||
for (byte b = 0; b < byteCount; b++) {
|
||||
checksum ^= buffer[b];
|
||||
//checksum ^= buffer[b];
|
||||
pendingPacket[b] = buffer[b];
|
||||
}
|
||||
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
|
||||
pendingPacket[byteCount] = checksum;
|
||||
pendingLength = byteCount + 1;
|
||||
//pendingPacket[byteCount] = checksum;
|
||||
pendingLength = byteCount /*+ 1*/;
|
||||
pendingRepeats = repeats;
|
||||
packetPending = true;
|
||||
packetPendingRMT = true;
|
||||
sentResetsSincePacket=0;
|
||||
portEXIT_CRITICAL(&timerMux);
|
||||
}
|
||||
|
||||
// Operations applicable to PROG track ONLY.
|
||||
// (yes I know I could have subclassed the main track but...)
|
||||
|
||||
void DCCWaveform::setAckBaseline() {
|
||||
if (!motorDriver) return;
|
||||
if (isMainTrack) return;
|
||||
int baseline=motorDriver->getCurrentRaw();
|
||||
ackThreshold= baseline + motorDriver->mA2raw(ackLimitmA);
|
||||
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
|
||||
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %dus and %dus"),
|
||||
baseline,motorDriver->raw2mA(baseline),
|
||||
ackThreshold,motorDriver->raw2mA(ackThreshold),
|
||||
minAckPulseDuration, maxAckPulseDuration);
|
||||
@@ -308,8 +367,9 @@ void DCCWaveform::setAckPending() {
|
||||
}
|
||||
|
||||
byte DCCWaveform::getAck() {
|
||||
if (!motorDriver) return 0;
|
||||
if (ackPending) return (2); // still waiting
|
||||
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
|
||||
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%duS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
|
||||
ackMaxCurrent,motorDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
|
||||
if (ackDetected) return (1); // Yes we had an ack
|
||||
return(0); // pending set off but not detected means no ACK.
|
||||
@@ -317,14 +377,15 @@ byte DCCWaveform::getAck() {
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void DCCWaveform::checkAck() {
|
||||
void IRAM_ATTR DCCWaveform::checkAck() {
|
||||
if (!motorDriver) return;
|
||||
// This function operates in interrupt() time so must be fast and can't DIAG
|
||||
if (sentResetsSincePacket > 6) { //ACK timeout
|
||||
ackCheckDuration=millis()-ackCheckStart;
|
||||
ackPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int current=motorDriver->getCurrentRaw();
|
||||
numAckSamples++;
|
||||
if (current > ackMaxCurrent) ackMaxCurrent=current;
|
||||
|
@@ -1,12 +1,8 @@
|
||||
/*
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -24,6 +20,7 @@
|
||||
#ifndef DCCWaveform_h
|
||||
#define DCCWaveform_h
|
||||
|
||||
#include "DCCRMT.h"
|
||||
#include "MotorDriver.h"
|
||||
|
||||
// Wait times for power management. Unit: milliseconds
|
||||
@@ -31,10 +28,8 @@ const int POWER_SAMPLE_ON_WAIT = 100;
|
||||
const int POWER_SAMPLE_OFF_WAIT = 1000;
|
||||
const int POWER_SAMPLE_OVERLOAD_WAIT = 20;
|
||||
|
||||
// Number of preamble bits.
|
||||
const int PREAMBLE_BITS_MAIN = 16;
|
||||
const int PREAMBLE_BITS_PROG = 22;
|
||||
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
|
||||
//const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
|
||||
#include "DCCPacket.h"
|
||||
|
||||
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
|
||||
// to the transform array.
|
||||
@@ -84,8 +79,12 @@ class DCCWaveform {
|
||||
}
|
||||
return tripmA;
|
||||
}
|
||||
inline void schedulePacket(dccPacket packet) {
|
||||
schedulePacket(packet.data, packet.length, packet.repeat);
|
||||
};
|
||||
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
|
||||
volatile bool packetPending;
|
||||
volatile bool packetPendingRMT;
|
||||
volatile byte sentResetsSincePacket;
|
||||
volatile bool autoPowerOff=false;
|
||||
void setAckBaseline(); //prog track only
|
||||
|
6
DIAG.h
6
DIAG.h
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -21,4 +19,4 @@
|
||||
|
||||
#include "DisplayInterface.h"
|
||||
|
||||
DisplayInterface *DisplayInterface::lcdDisplay = 0;
|
||||
DisplayInterface *DisplayInterface::lcdDisplay = 0;
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -34,4 +32,4 @@ public:
|
||||
static DisplayInterface *lcdDisplay;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
13
EEStore.cpp
13
EEStore.cpp
@@ -1,12 +1,9 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -21,9 +18,6 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "defines.h"
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "EEStore.h"
|
||||
|
||||
#include "DIAG.h"
|
||||
@@ -109,4 +103,3 @@ void EEStore::dump(int num) {
|
||||
|
||||
EEStore *EEStore::eeStore = NULL;
|
||||
int EEStore::eeAddress = 0;
|
||||
#endif
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
* (c) 2020 Harald Barth. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -20,7 +17,6 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DISABLE_EEPROM
|
||||
#ifndef EEStore_h
|
||||
#define EEStore_h
|
||||
|
||||
@@ -56,4 +52,3 @@ struct EEStore{
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DISABLE_EEPROM
|
||||
|
990
EXRAIL2.cpp
990
EXRAIL2.cpp
@@ -1,990 +0,0 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021-2022 Harald Barth
|
||||
* © 2020-2022 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* EXRAILPlus planned FEATURE additions
|
||||
F1. [DONE] DCC accessory packet opcodes (short and long form)
|
||||
F2. [DONE] ONAccessory catchers
|
||||
F3. [DONE] Turnout descriptions for Withrottle
|
||||
F4. Oled announcements (depends on HAL)
|
||||
F5. Withrottle roster info
|
||||
F6. Multi-occupancy semaphore
|
||||
F7. [DONE see AUTOSTART] Self starting sequences
|
||||
F8. Park/unpark
|
||||
F9. [DONE] Analog drive
|
||||
F10. [DONE] Alias anywhere
|
||||
F11. [DONE]EXRAIL/ENDEXRAIL unnecessary
|
||||
F12. [DONE] Allow guarded code (as effect of ALIAS anywhere)
|
||||
F13. [DONE] IFGTE/IFLT function
|
||||
*/
|
||||
/* EXRAILPlus planned TRANSPARENT additions
|
||||
T1. [DONE] RAM based fast lookup for sequences ON* event catchers and signals.
|
||||
T2. Extend to >64k
|
||||
*/
|
||||
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "EXRAIL2.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DIAG.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "Turnouts.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
|
||||
// Command parsing keywords
|
||||
const int16_t HASH_KEYWORD_EXRAIL=15435;
|
||||
const int16_t HASH_KEYWORD_ON = 2657;
|
||||
const int16_t HASH_KEYWORD_START=23232;
|
||||
const int16_t HASH_KEYWORD_RESERVE=11392;
|
||||
const int16_t HASH_KEYWORD_FREE=-23052;
|
||||
const int16_t HASH_KEYWORD_LATCH=1618;
|
||||
const int16_t HASH_KEYWORD_UNLATCH=1353;
|
||||
const int16_t HASH_KEYWORD_PAUSE=-4142;
|
||||
const int16_t HASH_KEYWORD_RESUME=27609;
|
||||
const int16_t HASH_KEYWORD_KILL=5218;
|
||||
const int16_t HASH_KEYWORD_ROUTES=-3702;
|
||||
|
||||
// One instance of RMFT clas is used for each "thread" in the automation.
|
||||
// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.
|
||||
// The threads exist in a ring, each time through loop() the next thread in the ring is serviced.
|
||||
|
||||
// Statics
|
||||
const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track
|
||||
int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prog track
|
||||
bool RMFT2::diag=false; // <D EXRAIL ON>
|
||||
RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring.
|
||||
RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
|
||||
// when pausingTask is set, that is the ONLY task that gets any service,
|
||||
// and all others will have their locos stopped, then resumed after the pausing task resumes.
|
||||
byte RMFT2::flags[MAX_FLAGS];
|
||||
|
||||
LookList * RMFT2::sequenceLookup=NULL;
|
||||
LookList * RMFT2::onThrowLookup=NULL;
|
||||
LookList * RMFT2::onCloseLookup=NULL;
|
||||
LookList * RMFT2::onActivateLookup=NULL;
|
||||
LookList * RMFT2::onDeactivateLookup=NULL;
|
||||
|
||||
#define GET_OPCODE GETFLASH(RMFT2::RouteCode+progCounter)
|
||||
#define GET_OPERAND(n) GETFLASHW(RMFT2::RouteCode+progCounter+1+(n*3))
|
||||
#define SKIPOP progCounter+=3
|
||||
|
||||
|
||||
LookList::LookList(int16_t size) {
|
||||
m_size=size;
|
||||
m_loaded=0;
|
||||
if (size) {
|
||||
m_lookupArray=new int16_t[size];
|
||||
m_resultArray=new int16_t[size];
|
||||
}
|
||||
}
|
||||
|
||||
void LookList::add(int16_t lookup, int16_t result) {
|
||||
if (m_loaded==m_size) return; // and forget
|
||||
m_lookupArray[m_loaded]=lookup;
|
||||
m_resultArray[m_loaded]=result;
|
||||
m_loaded++;
|
||||
}
|
||||
|
||||
int16_t LookList::find(int16_t value) {
|
||||
for (int16_t i=0;i<m_size;i++) {
|
||||
if (m_lookupArray[i]==value) return m_resultArray[i];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* static */ void RMFT2::begin() {
|
||||
DCCEXParser::setRMFTFilter(RMFT2::ComandFilter);
|
||||
for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;
|
||||
int progCounter;
|
||||
|
||||
// counters to create lookup arrays
|
||||
int sequenceCount=0; // to allow for seq 0 at start
|
||||
int onThrowCount=0;
|
||||
int onCloseCount=0;
|
||||
int onActivateCount=0;
|
||||
int onDeactivateCount=0;
|
||||
|
||||
// first pass count sizes for fast lookup arrays
|
||||
for (progCounter=0;; SKIPOP) {
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) break;
|
||||
switch (opcode) {
|
||||
case OPCODE_ROUTE:
|
||||
case OPCODE_AUTOMATION:
|
||||
case OPCODE_SEQUENCE:
|
||||
sequenceCount++;
|
||||
break;
|
||||
|
||||
case OPCODE_ONTHROW:
|
||||
onThrowCount++;
|
||||
break;
|
||||
|
||||
case OPCODE_ONCLOSE:
|
||||
onCloseCount++;
|
||||
break;
|
||||
|
||||
case OPCODE_ONACTIVATE:
|
||||
onActivateCount++;
|
||||
break;
|
||||
|
||||
case OPCODE_ONDEACTIVATE:
|
||||
onDeactivateCount++;
|
||||
break;
|
||||
|
||||
default: // Ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create lookups
|
||||
sequenceLookup=new LookList(sequenceCount);
|
||||
onThrowLookup=new LookList(onThrowCount);
|
||||
onCloseLookup=new LookList(onCloseCount);
|
||||
onActivateLookup=new LookList(onActivateCount);
|
||||
onDeactivateLookup=new LookList(onDeactivateCount);
|
||||
|
||||
// Second pass startup, define any turnouts or servos, set signals red
|
||||
// add sequences onRoutines to the lookups
|
||||
for (int sigpos=0;;sigpos+=3) {
|
||||
VPIN redpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos);
|
||||
if (redpin==0) break; // end of signal list
|
||||
VPIN amberpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos+1);
|
||||
VPIN greenpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos+2);
|
||||
IODevice::write(redpin,true);
|
||||
if (amberpin) IODevice::write(amberpin,false);
|
||||
IODevice::write(greenpin,false);
|
||||
}
|
||||
|
||||
for (progCounter=0;; SKIPOP){
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) break;
|
||||
VPIN operand=GET_OPERAND(0);
|
||||
|
||||
switch (opcode) {
|
||||
case OPCODE_AT:
|
||||
case OPCODE_AFTER:
|
||||
case OPCODE_IF:
|
||||
case OPCODE_IFNOT: {
|
||||
int16_t pin = (int16_t)operand;
|
||||
if (pin<0) pin = -pin;
|
||||
IODevice::configureInput((VPIN)pin,true);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_TURNOUT: {
|
||||
VPIN id=operand;
|
||||
int addr=GET_OPERAND(1);
|
||||
byte subAddr=GET_OPERAND(2);
|
||||
DCCTurnout::create(id,addr,subAddr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_SERVOTURNOUT: {
|
||||
VPIN id=operand;
|
||||
VPIN pin=GET_OPERAND(1);
|
||||
int activeAngle=GET_OPERAND(2);
|
||||
int inactiveAngle=GET_OPERAND(3);
|
||||
int profile=GET_OPERAND(4);
|
||||
ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_PINTURNOUT: {
|
||||
VPIN id=operand;
|
||||
VPIN pin=GET_OPERAND(1);
|
||||
VpinTurnout::create(id,pin);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_ROUTE:
|
||||
case OPCODE_AUTOMATION:
|
||||
case OPCODE_SEQUENCE:
|
||||
sequenceLookup->add(operand,progCounter);
|
||||
break;
|
||||
|
||||
case OPCODE_ONTHROW:
|
||||
onThrowLookup->add(operand,progCounter);
|
||||
break;
|
||||
|
||||
case OPCODE_ONCLOSE:
|
||||
onCloseLookup->add(operand,progCounter);
|
||||
break;
|
||||
|
||||
case OPCODE_ONACTIVATE:
|
||||
onActivateLookup->add(operand,progCounter);
|
||||
break;
|
||||
|
||||
case OPCODE_ONDEACTIVATE:
|
||||
onDeactivateLookup->add(operand,progCounter);
|
||||
break;
|
||||
|
||||
case OPCODE_AUTOSTART:
|
||||
// automatically create a task from here at startup.
|
||||
new RMFT2(progCounter);
|
||||
break;
|
||||
|
||||
default: // Ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
SKIPOP; // include ENDROUTES opcode
|
||||
|
||||
DIAG(F("EXRAIL %db, fl=%d seq=%d, onT=%d, onC=%d"),
|
||||
progCounter,MAX_FLAGS,
|
||||
sequenceCount, onThrowCount, onCloseCount);
|
||||
|
||||
new RMFT2(0); // add the startup route
|
||||
}
|
||||
|
||||
// This filter intercepts <> commands to do the following:
|
||||
// - Implement RMFT specific commands/diagnostics
|
||||
// - Reject/modify JMRI commands that would interfere with RMFT processing
|
||||
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
|
||||
(void)stream; // avoid compiler warning if we don't access this parameter
|
||||
bool reject=false;
|
||||
switch(opcode) {
|
||||
|
||||
case 'D':
|
||||
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF>
|
||||
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1);
|
||||
opcode=0;
|
||||
}
|
||||
break;
|
||||
|
||||
case '/': // New EXRAIL command
|
||||
reject=!parseSlash(stream,paramCount,p);
|
||||
opcode=0;
|
||||
break;
|
||||
|
||||
default: // other commands pass through
|
||||
break;
|
||||
}
|
||||
if (reject) {
|
||||
opcode=0;
|
||||
StringFormatter::send(stream,F("<X>"));
|
||||
}
|
||||
}
|
||||
|
||||
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
|
||||
if (paramCount==0) { // STATUS
|
||||
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' ',
|
||||
task->speedo,
|
||||
task->forward?'F':'R'
|
||||
);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
// Now stream the flags
|
||||
for (int id=0;id<MAX_FLAGS; id++) {
|
||||
byte flag=flags[id];
|
||||
if (flag & ~TASK_FLAG) { // not interested in TASK_FLAG only. Already shown above
|
||||
StringFormatter::send(stream,F("\nflags[%d} "),id);
|
||||
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
|
||||
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
|
||||
}
|
||||
}
|
||||
StringFormatter::send(stream,F(" *>\n"));
|
||||
return true;
|
||||
}
|
||||
switch (p[0]) {
|
||||
case HASH_KEYWORD_PAUSE: // </ PAUSE>
|
||||
if (paramCount!=1) return false;
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
pausingTask=(RMFT2 *)1; // Impossible task address
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_RESUME: // </ RESUME>
|
||||
if (paramCount!=1) return false;
|
||||
pausingTask=NULL;
|
||||
{
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->loco) task->driveLoco(task->speedo);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
|
||||
case HASH_KEYWORD_START: // </ START [cab] route >
|
||||
if (paramCount<2 || paramCount>3) return false;
|
||||
{
|
||||
int route=(paramCount==2) ? p[1] : p[2];
|
||||
uint16_t cab=(paramCount==2)? 0 : p[1];
|
||||
int pc=sequenceLookup->find(route);
|
||||
if (pc<0) return false;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_ROUTES: // </ ROUTES > JMRI withrottle support
|
||||
if (paramCount>1) return false;
|
||||
StringFormatter::send(stream,F("</ROUTES "));
|
||||
emitWithrottleRouteList(stream);
|
||||
StringFormatter::send(stream,F(">"));
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// all other / commands take 1 parameter 0 to MAX_FLAGS-1
|
||||
|
||||
if (paramCount!=2 || p[1]<0 || p[1]>=MAX_FLAGS) return false;
|
||||
|
||||
switch (p[0]) {
|
||||
case HASH_KEYWORD_KILL: // Kill taskid
|
||||
{
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->taskId==p[1]) {
|
||||
delete task;
|
||||
return true;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case HASH_KEYWORD_RESERVE: // force reserve a section
|
||||
setFlag(p[1],SECTION_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_FREE: // force free a section
|
||||
setFlag(p[1],0,SECTION_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_LATCH:
|
||||
setFlag(p[1], LATCH_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_UNLATCH:
|
||||
setFlag(p[1], 0, LATCH_FLAG);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This emits Routes and Automations to Withrottle
|
||||
// Automations are given a state to set the button to "handoff" which implies
|
||||
// handing over the loco to the automation.
|
||||
// Routes are given "Set" buttons and do not cause the loco to be handed over.
|
||||
void RMFT2::emitWithrottleRouteList(Print* stream) {
|
||||
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL"));
|
||||
emitWithrottleDescriptions(stream);
|
||||
StringFormatter::send(stream,F("\n"));
|
||||
}
|
||||
|
||||
|
||||
RMFT2::RMFT2(int progCtr) {
|
||||
progCounter=progCtr;
|
||||
|
||||
// get an unused task id from the flags table
|
||||
taskId=255; // in case of overflow
|
||||
for (int f=0;f<MAX_FLAGS;f++) {
|
||||
if (!getFlag(f,TASK_FLAG)) {
|
||||
taskId=f;
|
||||
setFlag(f, TASK_FLAG);
|
||||
break;
|
||||
}
|
||||
}
|
||||
delayTime=0;
|
||||
loco=0;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
timeoutFlag=false;
|
||||
stackDepth=0;
|
||||
onTurnoutId=0; // Not handling an ONTHROW/ONCLOSE
|
||||
|
||||
// chain into ring of RMFTs
|
||||
if (loopTask==NULL) {
|
||||
loopTask=this;
|
||||
next=this;
|
||||
} else {
|
||||
next=loopTask->next;
|
||||
loopTask->next=this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RMFT2::~RMFT2() {
|
||||
driveLoco(1); // ESTOP my loco if any
|
||||
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
|
||||
if (next==this)
|
||||
loopTask=NULL;
|
||||
else
|
||||
for (RMFT2* ring=next;;ring=ring->next)
|
||||
if (ring->next == this) {
|
||||
ring->next=next;
|
||||
loopTask=next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::createNewTask(int route, uint16_t cab) {
|
||||
int pc=sequenceLookup->find(route);
|
||||
if (pc<0) return;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
|
||||
void RMFT2::driveLoco(byte speed) {
|
||||
if (loco<=0) return; // Prevent broadcast!
|
||||
if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
|
||||
if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) {
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
DCC::setThrottle(loco,speed, forward^invert);
|
||||
speedo=speed;
|
||||
}
|
||||
|
||||
bool RMFT2::readSensor(uint16_t sensorId) {
|
||||
// Exrail operands are unsigned but we need the signed version as inserted by the macros.
|
||||
int16_t sId=(int16_t) sensorId;
|
||||
|
||||
VPIN vpin=abs(sId);
|
||||
if (getFlag(vpin,LATCH_FLAG)) return true; // latched on
|
||||
|
||||
// negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting)
|
||||
bool s= IODevice::read(vpin) ^ (sId<0);
|
||||
if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sId);
|
||||
return s;
|
||||
}
|
||||
|
||||
// This skips to the end of an if block, or to the ELSE within it.
|
||||
bool RMFT2::skipIfBlock() {
|
||||
// returns false if killed
|
||||
short nest = 1;
|
||||
while (nest > 0) {
|
||||
SKIPOP;
|
||||
byte opcode = GET_OPCODE;
|
||||
switch(opcode) {
|
||||
case OPCODE_ENDEXRAIL:
|
||||
kill(F("missing ENDIF"), nest);
|
||||
return false;
|
||||
case OPCODE_IF:
|
||||
case OPCODE_IFCLOSED:
|
||||
case OPCODE_IFGTE:
|
||||
case OPCODE_IFLT:
|
||||
case OPCODE_IFNOT:
|
||||
case OPCODE_IFRANDOM:
|
||||
case OPCODE_IFRESERVE:
|
||||
case OPCODE_IFTHROWN:
|
||||
case OPCODE_IFTIMEOUT:
|
||||
nest++;
|
||||
break;
|
||||
case OPCODE_ENDIF:
|
||||
nest--;
|
||||
break;
|
||||
case OPCODE_ELSE:
|
||||
// if nest==1 then this is the ELSE for the IF we are skipping
|
||||
if (nest==1) nest=0; // cause loop exit and return after ELSE
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* static */ void RMFT2::readLocoCallback(int16_t cv) {
|
||||
progtrackLocoId=cv;
|
||||
}
|
||||
|
||||
void RMFT2::loop() {
|
||||
|
||||
// Round Robin call to a RMFT task each time
|
||||
if (loopTask==NULL) return;
|
||||
loopTask=loopTask->next;
|
||||
if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2();
|
||||
}
|
||||
|
||||
|
||||
void RMFT2::loop2() {
|
||||
if (delayTime!=0 && millis()-delayStart < delayTime) return;
|
||||
|
||||
byte opcode = GET_OPCODE;
|
||||
int16_t operand = GET_OPERAND(0);
|
||||
// if (diag) DIAG(F("RMFT2 %d %d"),opcode,operand);
|
||||
// Attention: Returning from this switch leaves the program counter unchanged.
|
||||
// This is used for unfinished waits for timers or sensors.
|
||||
// Breaking from this switch will step to the next step in the route.
|
||||
switch ((OPCODE)opcode) {
|
||||
|
||||
case OPCODE_THROW:
|
||||
Turnout::setClosed(operand, false);
|
||||
break;
|
||||
|
||||
case OPCODE_CLOSE:
|
||||
Turnout::setClosed(operand, true);
|
||||
break;
|
||||
|
||||
case OPCODE_REV:
|
||||
forward = false;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_FWD:
|
||||
forward = true;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_SPEED:
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_INVERT_DIRECTION:
|
||||
invert= !invert;
|
||||
driveLoco(speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_RESERVE:
|
||||
if (getFlag(operand,SECTION_FLAG)) {
|
||||
driveLoco(0);
|
||||
delayMe(500);
|
||||
return;
|
||||
}
|
||||
setFlag(operand,SECTION_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_FREE:
|
||||
setFlag(operand,0,SECTION_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_AT:
|
||||
timeoutFlag=false;
|
||||
if (readSensor(operand)) break;
|
||||
delayMe(50);
|
||||
return;
|
||||
|
||||
case OPCODE_ATTIMEOUT1: // ATTIMEOUT(vpin,timeout) part 1
|
||||
timeoutStart=millis();
|
||||
timeoutFlag=false;
|
||||
break;
|
||||
|
||||
case OPCODE_ATTIMEOUT2:
|
||||
if (readSensor(operand)) break; // success without timeout
|
||||
if (millis()-timeoutStart > 100*GET_OPERAND(1)) {
|
||||
timeoutFlag=true;
|
||||
break; // and drop through
|
||||
}
|
||||
delayMe(50);
|
||||
return;
|
||||
|
||||
case OPCODE_IFTIMEOUT: // do next operand if timeout flag set
|
||||
if (!timeoutFlag) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_AFTER: // waits for sensor to hit and then remain off for 0.5 seconds. (must come after an AT operation)
|
||||
if (readSensor(operand)) {
|
||||
// reset timer to half a second and keep waiting
|
||||
waitAfter=millis();
|
||||
delayMe(50);
|
||||
return;
|
||||
}
|
||||
if (millis()-waitAfter < 500 ) return;
|
||||
break;
|
||||
|
||||
case OPCODE_LATCH:
|
||||
setFlag(operand,LATCH_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_UNLATCH:
|
||||
setFlag(operand,0,LATCH_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_SET:
|
||||
IODevice::write(operand,true);
|
||||
break;
|
||||
|
||||
case OPCODE_RESET:
|
||||
IODevice::write(operand,false);
|
||||
break;
|
||||
|
||||
case OPCODE_PAUSE:
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
pausingTask=this;
|
||||
break;
|
||||
|
||||
case OPCODE_POM:
|
||||
if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1));
|
||||
break;
|
||||
|
||||
case OPCODE_POWEROFF:
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
CommandDistributor::broadcastPower();
|
||||
break;
|
||||
|
||||
case OPCODE_RESUME:
|
||||
pausingTask=NULL;
|
||||
driveLoco(speedo);
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_IF: // do next operand if sensor set
|
||||
if (!readSensor(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_ELSE: // skip to matching ENDIF
|
||||
if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFGTE: // do next operand if sensor>= value
|
||||
if (IODevice::readAnalogue(operand)<(int)(GET_OPERAND(1))) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFLT: // do next operand if sensor< value
|
||||
if (IODevice::readAnalogue(operand)>=(int)(GET_OPERAND(1))) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFNOT: // do next operand if sensor not set
|
||||
if (readSensor(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFRANDOM: // do block on random percentage
|
||||
if ((int16_t)random(100)>=operand) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFRESERVE: // do block if we successfully RERSERVE
|
||||
if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG);
|
||||
else if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFTHROWN:
|
||||
if (Turnout::isClosed(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFCLOSED:
|
||||
if (!Turnout::isClosed(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_ENDIF:
|
||||
break;
|
||||
|
||||
case OPCODE_DELAYMS:
|
||||
delayMe(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_DELAY:
|
||||
delayMe(operand*100L);
|
||||
break;
|
||||
|
||||
case OPCODE_DELAYMINS:
|
||||
delayMe(operand*60L*1000L);
|
||||
break;
|
||||
|
||||
case OPCODE_RANDWAIT:
|
||||
delayMe(random(operand)*100L);
|
||||
break;
|
||||
|
||||
case OPCODE_RED:
|
||||
doSignal(operand,true,false,false);
|
||||
break;
|
||||
|
||||
case OPCODE_AMBER:
|
||||
doSignal(operand,false,true,false);
|
||||
break;
|
||||
|
||||
case OPCODE_GREEN:
|
||||
doSignal(operand,false,false,true);
|
||||
break;
|
||||
|
||||
case OPCODE_FON:
|
||||
if (loco) DCC::setFn(loco,operand,true);
|
||||
break;
|
||||
|
||||
case OPCODE_FOFF:
|
||||
if (loco) DCC::setFn(loco,operand,false);
|
||||
break;
|
||||
|
||||
case OPCODE_DRIVE:
|
||||
{
|
||||
byte analogSpeed=IODevice::readAnalogue(operand) *127 / 1024;
|
||||
if (speedo!=analogSpeed) driveLoco(analogSpeed);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_XFON:
|
||||
DCC::setFn(operand,GET_OPERAND(1),true);
|
||||
break;
|
||||
|
||||
case OPCODE_XFOFF:
|
||||
DCC::setFn(operand,GET_OPERAND(1),false);
|
||||
break;
|
||||
|
||||
case OPCODE_DCCACTIVATE: {
|
||||
// operand is address<<3 | subaddr<<1 | active
|
||||
int16_t addr=operand>>3;
|
||||
int16_t subaddr=(operand>>1) & 0x03;
|
||||
bool active=operand & 0x01;
|
||||
DCC::setAccessory(addr,subaddr,active);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPCODE_FOLLOW:
|
||||
progCounter=sequenceLookup->find(operand);
|
||||
if (progCounter<0) kill(F("FOLLOW unknown"), operand);
|
||||
return;
|
||||
|
||||
case OPCODE_CALL:
|
||||
if (stackDepth==MAX_STACK_DEPTH) {
|
||||
kill(F("CALL stack"), stackDepth);
|
||||
return;
|
||||
}
|
||||
callStack[stackDepth++]=progCounter+3;
|
||||
progCounter=sequenceLookup->find(operand);
|
||||
if (progCounter<0) kill(F("CALL unknown"),operand);
|
||||
return;
|
||||
|
||||
case OPCODE_RETURN:
|
||||
if (stackDepth==0) {
|
||||
kill(F("RETURN stack"));
|
||||
return;
|
||||
}
|
||||
progCounter=callStack[--stackDepth];
|
||||
return;
|
||||
|
||||
case OPCODE_ENDTASK:
|
||||
case OPCODE_ENDEXRAIL:
|
||||
kill();
|
||||
return;
|
||||
|
||||
case OPCODE_JOIN:
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
DCC::setProgTrackSyncMain(true);
|
||||
CommandDistributor::broadcastPower();
|
||||
break;
|
||||
|
||||
case OPCODE_POWERON:
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
CommandDistributor::broadcastPower();
|
||||
break;
|
||||
|
||||
case OPCODE_UNJOIN:
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
CommandDistributor::broadcastPower();
|
||||
break;
|
||||
|
||||
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
|
||||
progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet
|
||||
DCC::getLocoId(readLocoCallback);
|
||||
break;
|
||||
|
||||
case OPCODE_READ_LOCO2:
|
||||
if (progtrackLocoId==LOCO_ID_WAITING) {
|
||||
delayMe(100);
|
||||
return; // still waiting for callback
|
||||
}
|
||||
if (progtrackLocoId<0) {
|
||||
kill(F("No Loco Found"),progtrackLocoId);
|
||||
return; // still waiting for callback
|
||||
}
|
||||
|
||||
loco=progtrackLocoId;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
break;
|
||||
|
||||
case OPCODE_START:
|
||||
{
|
||||
int newPc=sequenceLookup->find(operand);
|
||||
if (newPc<0) break;
|
||||
new RMFT2(newPc);
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SENDLOCO: // cab, route
|
||||
{
|
||||
int newPc=sequenceLookup->find(GET_OPERAND(1));
|
||||
if (newPc<0) break;
|
||||
RMFT2* newtask=new RMFT2(newPc); // create new task
|
||||
newtask->loco=operand;
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SETLOCO:
|
||||
{
|
||||
loco=operand;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration)
|
||||
IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2),GET_OPERAND(3));
|
||||
break;
|
||||
|
||||
case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin)
|
||||
if (IODevice::isBusy(operand)) {
|
||||
delayMe(100);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_PRINT:
|
||||
printMessage(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_ROUTE:
|
||||
case OPCODE_AUTOMATION:
|
||||
case OPCODE_SEQUENCE:
|
||||
if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
|
||||
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
|
||||
case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime
|
||||
case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime
|
||||
case OPCODE_ONCLOSE: // Turnout event catchers ignored here
|
||||
case OPCODE_ONTHROW:
|
||||
case OPCODE_ONACTIVATE: // Activate event catchers ignored here
|
||||
case OPCODE_ONDEACTIVATE:
|
||||
break;
|
||||
|
||||
default:
|
||||
kill(F("INVOP"),operand);
|
||||
}
|
||||
// Falling out of the switch means move on to the next opcode
|
||||
SKIPOP;
|
||||
}
|
||||
|
||||
void RMFT2::delayMe(long delay) {
|
||||
delayTime=delay;
|
||||
delayStart=millis();
|
||||
}
|
||||
|
||||
void RMFT2::setFlag(VPIN id,byte onMask, byte offMask) {
|
||||
if (FLAGOVERFLOW(id)) return; // Outside range limit
|
||||
byte f=flags[id];
|
||||
f &= ~offMask;
|
||||
f |= onMask;
|
||||
flags[id]=f;
|
||||
}
|
||||
|
||||
bool RMFT2::getFlag(VPIN id,byte mask) {
|
||||
if (FLAGOVERFLOW(id)) return 0; // Outside range limit
|
||||
return flags[id]&mask;
|
||||
}
|
||||
|
||||
void RMFT2::kill(const FSH * reason, int operand) {
|
||||
if (reason) DIAG(F("EXRAIL ERROR pc=%d, cab=%d, %S %d"), progCounter,loco, reason, operand);
|
||||
else if (diag) DIAG(F("ENDTASK at pc=%d"), progCounter);
|
||||
delete this;
|
||||
}
|
||||
|
||||
/* static */ void RMFT2::doSignal(VPIN id,bool red, bool amber, bool green) {
|
||||
//if (diag) DIAG(F(" dosignal %d"),id);
|
||||
for (int sigpos=0;;sigpos+=3) {
|
||||
VPIN redpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos);
|
||||
//if (diag) DIAG(F("red=%d"),redpin);
|
||||
if (redpin==0) {
|
||||
DIAG(F("EXRAIL Signal %d not defined"), id);
|
||||
return; // signal not found
|
||||
}
|
||||
if (redpin==id) {
|
||||
VPIN amberpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos+1);
|
||||
VPIN greenpin=GETFLASHW(RMFT2::SignalDefinitions+sigpos+2);
|
||||
//if (diag) DIAG(F("signal %d %d %d"),redpin,amberpin,greenpin);
|
||||
// If amberpin is zero, synthesise amber from red+green
|
||||
IODevice::write(redpin,red || (amber && (amberpin==0)));
|
||||
if (amberpin) IODevice::write(amberpin,amber);
|
||||
if (greenpin) IODevice::write(greenpin,green || (amber && (amberpin==0)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) {
|
||||
// Hunt for an ONTHROW/ONCLOSE for this turnout
|
||||
int pc= (closed?onCloseLookup:onThrowLookup)->find(turnoutId);
|
||||
if (pc<0) return;
|
||||
|
||||
// Check we dont already have a task running this turnout
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->onTurnoutId==turnoutId) {
|
||||
DIAG(F("Recursive ONTHROW/ONCLOSE for Turnout %d"),turnoutId);
|
||||
return;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
|
||||
task=new RMFT2(pc); // new task starts at this instruction
|
||||
task->onTurnoutId=turnoutId; // flag for recursion detector
|
||||
}
|
||||
|
||||
void RMFT2::activateEvent(int16_t addr, bool activate) {
|
||||
// Hunt for an ONACTIVATE/ONDEACTIVATE for this accessory
|
||||
int pc= (activate?onActivateLookup:onDeactivateLookup)->find(addr);
|
||||
if (pc<0) return;
|
||||
|
||||
// Check we dont already have a task running this address
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->onActivateAddr==addr) {
|
||||
DIAG(F("Recursive ON(DE)ACTIVATE for %d"),addr);
|
||||
return;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
|
||||
task->onActivateAddr=addr; // flag for recursion detector
|
||||
task=new RMFT2(pc); // new task starts at this instruction
|
||||
}
|
||||
|
||||
void RMFT2::printMessage2(const FSH * msg) {
|
||||
DIAG(F("EXRAIL(%d) %S"),loco,msg);
|
||||
}
|
||||
|
||||
// This is called by emitRouteDescriptions to emit a withrottle description for a route or autoomation.
|
||||
void RMFT2::emitRouteDescription(Print * stream, char type, int id, const FSH * description) {
|
||||
StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"),
|
||||
type,id,description, type=='R'?'2':'4');
|
||||
}
|
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* © 2021-2022 Chris Harlow
|
||||
* © 2020,2021 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// This file cleans and resets the RMFT2 Macros.
|
||||
// It is used between passes to reduce complexity in RMFT2Macros.h
|
||||
// DO NOT add an include guard to this file.
|
||||
|
||||
// Undefine all RMFT macros
|
||||
#undef ACTIVATE
|
||||
#undef ACTIVATEL
|
||||
#undef AFTER
|
||||
#undef ALIAS
|
||||
#undef AMBER
|
||||
#undef AT
|
||||
#undef ATTIMEOUT
|
||||
#undef AUTOMATION
|
||||
#undef AUTOSTART
|
||||
#undef BROADCAST
|
||||
#undef CALL
|
||||
#undef CLOSE
|
||||
#undef DEACTIVATE
|
||||
#undef DEACTIVATEL
|
||||
#undef DELAY
|
||||
#undef DELAYMINS
|
||||
#undef DELAYRANDOM
|
||||
#undef DONE
|
||||
#undef DRIVE
|
||||
#undef ELSE
|
||||
#undef ENDEXRAIL
|
||||
#undef ENDIF
|
||||
#undef ENDTASK
|
||||
#undef ESTOP
|
||||
#undef EXRAIL
|
||||
#undef FADE
|
||||
#undef FOFF
|
||||
#undef FOLLOW
|
||||
#undef FON
|
||||
#undef FREE
|
||||
#undef FWD
|
||||
#undef GREEN
|
||||
#undef IF
|
||||
#undef IFCLOSED
|
||||
#undef IFGTE
|
||||
#undef IFLT
|
||||
#undef IFNOT
|
||||
#undef IFRANDOM
|
||||
#undef IFRESERVE
|
||||
#undef IFTHROWN
|
||||
#undef IFTIMEOUT
|
||||
#undef INVERT_DIRECTION
|
||||
#undef JOIN
|
||||
#undef LATCH
|
||||
#undef LCD
|
||||
#undef LCN
|
||||
#undef ONACTIVATE
|
||||
#undef ONACTIVATEL
|
||||
#undef ONDEACTIVATE
|
||||
#undef ONDEACTIVATEL
|
||||
#undef ONCLOSE
|
||||
#undef ONTHROW
|
||||
#undef PAUSE
|
||||
#undef PIN_TURNOUT
|
||||
#undef PRINT
|
||||
#undef POM
|
||||
#undef POWEROFF
|
||||
#undef POWERON
|
||||
#undef READ_LOCO
|
||||
#undef RED
|
||||
#undef RESERVE
|
||||
#undef RESET
|
||||
#undef RESUME
|
||||
#undef RETURN
|
||||
#undef REV
|
||||
#undef ROSTER
|
||||
#undef ROUTE
|
||||
#undef SENDLOCO
|
||||
#undef SEQUENCE
|
||||
#undef SERIAL
|
||||
#undef SERIAL1
|
||||
#undef SERIAL2
|
||||
#undef SERIAL3
|
||||
#undef SERVO
|
||||
#undef SERVO2
|
||||
#undef SERVO_TURNOUT
|
||||
#undef SET
|
||||
#undef SETLOCO
|
||||
#undef SIGNAL
|
||||
#undef SPEED
|
||||
#undef START
|
||||
#undef STOP
|
||||
#undef THROW
|
||||
#undef TURNOUT
|
||||
#undef UNJOIN
|
||||
#undef UNLATCH
|
||||
#undef WAITFOR
|
||||
#undef XFOFF
|
||||
#undef XFON
|
||||
|
||||
#ifndef RMFT2_UNDEF_ONLY
|
||||
#define ACTIVATE(addr,subaddr)
|
||||
#define ACTIVATEL(addr)
|
||||
#define AFTER(sensor_id)
|
||||
#define ALIAS(name,value)
|
||||
#define AMBER(signal_id)
|
||||
#define AT(sensor_id)
|
||||
#define ATTIMEOUT(sensor_id,timeout_ms)
|
||||
#define AUTOMATION(id, description)
|
||||
#define AUTOSTART
|
||||
#define BROADCAST(msg)
|
||||
#define CALL(route)
|
||||
#define CLOSE(id)
|
||||
#define DEACTIVATE(addr,subaddr)
|
||||
#define DEACTIVATEL(addr)
|
||||
#define DELAY(mindelay)
|
||||
#define DELAYMINS(mindelay)
|
||||
#define DELAYRANDOM(mindelay,maxdelay)
|
||||
#define DONE
|
||||
#define DRIVE(analogpin)
|
||||
#define ELSE
|
||||
#define ENDEXRAIL
|
||||
#define ENDIF
|
||||
#define ENDTASK
|
||||
#define ESTOP
|
||||
#define EXRAIL
|
||||
#define FADE(pin,value,ms)
|
||||
#define FOFF(func)
|
||||
#define FOLLOW(route)
|
||||
#define FON(func)
|
||||
#define FREE(blockid)
|
||||
#define FWD(speed)
|
||||
#define GREEN(signal_id)
|
||||
#define IF(sensor_id)
|
||||
#define IFCLOSED(turnout_id)
|
||||
#define IFGTE(sensor_id,value)
|
||||
#define IFLT(sensor_id,value)
|
||||
#define IFNOT(sensor_id)
|
||||
#define IFRANDOM(percent)
|
||||
#define IFTHROWN(turnout_id)
|
||||
#define IFRESERVE(block)
|
||||
#define IFTIMEOUT
|
||||
#define INVERT_DIRECTION
|
||||
#define JOIN
|
||||
#define LATCH(sensor_id)
|
||||
#define LCD(row,msg)
|
||||
#define LCN(msg)
|
||||
#define ONACTIVATE(addr,subaddr)
|
||||
#define ONACTIVATEL(linear)
|
||||
#define ONDEACTIVATE(addr,subaddr)
|
||||
#define ONDEACTIVATEL(linear)
|
||||
#define ONCLOSE(turnout_id)
|
||||
#define ONTHROW(turnout_id)
|
||||
#define PAUSE
|
||||
#define PIN_TURNOUT(id,pin,description...)
|
||||
#define PRINT(msg)
|
||||
#define POM(cv,value)
|
||||
#define POWEROFF
|
||||
#define POWERON
|
||||
#define READ_LOCO
|
||||
#define RED(signal_id)
|
||||
#define RESERVE(blockid)
|
||||
#define RESET(pin)
|
||||
#define RESUME
|
||||
#define RETURN
|
||||
#define REV(speed)
|
||||
#define ROUTE(id, description)
|
||||
#define ROSTER(cab,name,funcmap...)
|
||||
#define SENDLOCO(cab,route)
|
||||
#define SEQUENCE(id)
|
||||
#define SERIAL(msg)
|
||||
#define SERIAL1(msg)
|
||||
#define SERIAL2(msg)
|
||||
#define SERIAL3(msg)
|
||||
#define SERVO(id,position,profile)
|
||||
#define SERVO2(id,position,duration)
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
|
||||
#define SET(pin)
|
||||
#define SETLOCO(loco)
|
||||
#define SIGNAL(redpin,amberpin,greenpin)
|
||||
#define SPEED(speed)
|
||||
#define START(route)
|
||||
#define STOP
|
||||
#define THROW(id)
|
||||
#define TURNOUT(id,addr,subaddr,description...)
|
||||
#define UNJOIN
|
||||
#define UNLATCH(sensor_id)
|
||||
#define WAITFOR(pin)
|
||||
#define XFOFF(cab,func)
|
||||
#define XFON(cab,func)
|
||||
#endif
|
@@ -1,9 +1,5 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX/CommandStation-EX
|
||||
*
|
||||
@@ -174,7 +170,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX/CommandStation-EX
|
||||
*
|
||||
|
7
FSH.h
7
FSH.h
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -44,6 +41,7 @@
|
||||
typedef char FSH;
|
||||
#define GETFLASH(addr) (*(const unsigned char *)(addr))
|
||||
#define GETFLASHW(addr) (*(const unsigned short *)(addr))
|
||||
#define GETFLASHP(addr) (*(void * const *)(addr))
|
||||
#define FLASH
|
||||
#define strlen_P strlen
|
||||
#define strcpy_P strcpy
|
||||
@@ -51,6 +49,7 @@ typedef char FSH;
|
||||
typedef __FlashStringHelper FSH;
|
||||
#define GETFLASH(addr) pgm_read_byte_near(addr)
|
||||
#define GETFLASHW(addr) pgm_read_word_near(addr)
|
||||
#define GETFLASHP(addr) pgm_read_ptr_near(addr)
|
||||
#define FLASH PROGMEM
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -1 +1 @@
|
||||
#define GITHUB_SHA "a26d988"
|
||||
#define GITHUB_SHA "ESP32-motordriver-2021129-00:12"
|
||||
|
@@ -72,7 +72,7 @@ void I2CManagerClass::I2C_sendStart() {
|
||||
bytesToReceive = currentRequest->readLen;
|
||||
|
||||
// If anything to send, initiate write. Otherwise initiate read.
|
||||
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) & !bytesToSend))
|
||||
if (operation == OPERATION_READ || (operation == OPERATION_REQUEST & !bytesToSend))
|
||||
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
|
||||
else
|
||||
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;
|
||||
@@ -157,4 +157,4 @@ ISR(TWI0_TWIM_vect) {
|
||||
I2CManagerClass::handleInterrupt();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
@@ -98,20 +98,22 @@ uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t rea
|
||||
* returned in the I2CRB as for the asynchronous version.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||
uint8_t status;
|
||||
switch (req->operation) {
|
||||
case OPERATION_READ:
|
||||
req->status = read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
|
||||
status = read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
|
||||
break;
|
||||
case OPERATION_SEND:
|
||||
req->status = write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
|
||||
status = write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
|
||||
break;
|
||||
case OPERATION_SEND_P:
|
||||
req->status = write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
|
||||
status = write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
|
||||
break;
|
||||
case OPERATION_REQUEST:
|
||||
req->status = read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
|
||||
status = read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
|
||||
break;
|
||||
}
|
||||
req->status = status;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* All rights reserved.
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
|
@@ -47,15 +47,11 @@ void DCCAccessoryDecoder::_begin() {
|
||||
// Device-specific write function. State 1=closed, 0=thrown. Adjust for RCN-213 compliance
|
||||
void DCCAccessoryDecoder::_write(VPIN id, int state) {
|
||||
int packedAddress = _packedAddress + id - _firstVpin;
|
||||
#if defined(HAL_ACCESSORY_COMMAND_REVERSE)
|
||||
state = !state;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC Write Linear Address:%d State:%d (inverted)"), packedAddress, state);
|
||||
#endif
|
||||
#else
|
||||
#ifdef DIAG_IO
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
|
||||
#endif
|
||||
#endif
|
||||
#if !defined(DCC_ACCESSORY_RCN_213)
|
||||
state = !state;
|
||||
#endif
|
||||
DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state);
|
||||
}
|
||||
|
@@ -69,6 +69,10 @@ protected:
|
||||
|
||||
I2CRB requestBlock;
|
||||
FSH *_deviceName;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
// workaround: Has somehow no min function for all types
|
||||
static inline T min(T a, int b) { return a < b ? a : b; };
|
||||
#endif
|
||||
};
|
||||
|
||||
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
|
||||
@@ -246,4 +250,4 @@ int GPIOBase<T>::_read(VPIN vpin) {
|
||||
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -97,4 +97,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -99,4 +99,4 @@ private:
|
||||
uint8_t inputBuffer[1];
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
4
LCN.h
4
LCN.h
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
|
145
MotorDriver.cpp
145
MotorDriver.cpp
@@ -1,11 +1,7 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -21,35 +17,47 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "defines.h"
|
||||
#include "MotorDriver.h"
|
||||
#include "DCCTimer.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
|
||||
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
|
||||
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
|
||||
#define isLOW(fastpin) (!isHIGH(fastpin))
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <driver/adc.h>
|
||||
#define pinToADC1Channel(X) (adc1_channel_t)(((X) > 35) ? (X)-36 : (X)-28)
|
||||
#endif
|
||||
|
||||
bool MotorDriver::usePWM=false;
|
||||
bool MotorDriver::commonFaultPin=false;
|
||||
|
||||
MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
|
||||
byte current_pin, float sense_factor, unsigned int trip_milliamps, byte fault_pin) {
|
||||
byte current_pin, float sense_factor, unsigned int trip_milliamps, byte fault_pin,
|
||||
driverType dt) {
|
||||
dtype = dt;
|
||||
powerPin=power_pin;
|
||||
getFastPin(F("POWER"),powerPin,fastPowerPin);
|
||||
pinMode(powerPin, OUTPUT);
|
||||
|
||||
signalPin=signal_pin;
|
||||
getFastPin(F("SIG"),signalPin,fastSignalPin);
|
||||
pinMode(signalPin, OUTPUT);
|
||||
|
||||
signalPin2=signal_pin2;
|
||||
if (signalPin2!=UNUSED_PIN) {
|
||||
dualSignal=true;
|
||||
getFastPin(F("SIG2"),signalPin2,fastSignalPin2);
|
||||
pinMode(signalPin2, OUTPUT);
|
||||
|
||||
if (dtype == RMT_MAIN) {
|
||||
signalPin=signal_pin;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
rmtChannel = new RMTChannel(signalPin, 0, PREAMBLE_BITS_MAIN);
|
||||
#endif
|
||||
dualSignal=false;
|
||||
} else if (dtype & (TIMER_MAIN | TIMER_PROG)) {
|
||||
signalPin=signal_pin;
|
||||
getFastPin(F("SIG"),signalPin,fastSignalPin);
|
||||
pinMode(signalPin, OUTPUT);
|
||||
|
||||
signalPin2=signal_pin2;
|
||||
if (signalPin2!=UNUSED_PIN) {
|
||||
dualSignal=true;
|
||||
getFastPin(F("SIG2"),signalPin2,fastSignalPin2);
|
||||
pinMode(signalPin2, OUTPUT);
|
||||
} else {
|
||||
dualSignal=false;
|
||||
}
|
||||
}
|
||||
else dualSignal=false;
|
||||
|
||||
brakePin=brake_pin;
|
||||
if (brake_pin!=UNUSED_PIN){
|
||||
@@ -63,8 +71,15 @@ MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8
|
||||
|
||||
currentPin=current_pin;
|
||||
if (currentPin!=UNUSED_PIN) {
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
pinMode(currentPin, ANALOG);
|
||||
adc1_config_width(ADC_WIDTH_BIT_12);
|
||||
adc1_config_channel_atten(pinToADC1Channel(currentPin),ADC_ATTEN_DB_11);
|
||||
senseOffset = adc1_get_raw(pinToADC1Channel(currentPin));
|
||||
#else
|
||||
pinMode(currentPin, INPUT);
|
||||
senseOffset=analogRead(currentPin); // value of sensor at zero current
|
||||
#endif
|
||||
}
|
||||
|
||||
faultPin=fault_pin;
|
||||
@@ -80,7 +95,7 @@ MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8
|
||||
if (currentPin==UNUSED_PIN)
|
||||
DIAG(F("MotorDriver ** WARNING ** No current or short detection"));
|
||||
else
|
||||
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurrentTripValue(relative to offset)=%d"),
|
||||
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurentTripValue(relative to offset)=%d"),
|
||||
currentPin-A0, senseOffset,rawCurrentTripValue);
|
||||
}
|
||||
|
||||
@@ -114,7 +129,7 @@ void MotorDriver::setBrake(bool on) {
|
||||
else setLOW(fastBrakePin);
|
||||
}
|
||||
|
||||
void MotorDriver::setSignal( bool high) {
|
||||
void IRAM_ATTR MotorDriver::setSignal( bool high) {
|
||||
if (usePWM) {
|
||||
DCCTimer::setPWM(signalPin,high);
|
||||
}
|
||||
@@ -152,16 +167,18 @@ int MotorDriver::getCurrentRaw() {
|
||||
bool irq = disableInterrupts();
|
||||
current = analogRead(currentPin)-senseOffset;
|
||||
enableInterrupts(irq);
|
||||
#else // Uno, Mega and all the TEENSY3* but not TEENSY4*
|
||||
#elif defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
|
||||
unsigned char sreg_backup;
|
||||
sreg_backup = SREG; /* save interrupt enable/disable state */
|
||||
cli();
|
||||
current = analogRead(currentPin)-senseOffset;
|
||||
#if defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
|
||||
overflow_count = 0;
|
||||
SREG = sreg_backup; /* restore interrupt state */
|
||||
#elif defined(ARDUINO_ARCH_ESP32)
|
||||
current = adc1_get_raw(pinToADC1Channel(currentPin))-senseOffset;
|
||||
#else
|
||||
current = analogRead(currentPin)-senseOffset;
|
||||
#endif
|
||||
if (sreg_backup & 128) sei(); /* restore interrupt state */
|
||||
#endif // outer #
|
||||
if (current<0) current=0-current;
|
||||
if ((faultPin != UNUSED_PIN) && isLOW(fastFaultPin) && isHIGH(fastPowerPin))
|
||||
return (current == 0 ? -1 : -current);
|
||||
@@ -180,8 +197,8 @@ int MotorDriver::mA2raw( unsigned int mA) {
|
||||
|
||||
void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & result) {
|
||||
// DIAG(F("MotorDriver %S Pin=%d,"),type,pin);
|
||||
(void) type; // avoid compiler warning if diag not used above.
|
||||
uint8_t port = digitalPinToPort(pin);
|
||||
(void) type; // avoid compiler warning if diag not used above.
|
||||
PORTTYPE port = digitalPinToPort(pin);
|
||||
if (input)
|
||||
result.inout = portInputRegister(port);
|
||||
else
|
||||
@@ -190,3 +207,69 @@ void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & res
|
||||
result.maskLOW = ~result.maskHIGH;
|
||||
// DIAG(F(" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH);
|
||||
}
|
||||
|
||||
bool MotorDriver::schedulePacket(dccPacket packet) {
|
||||
if(!rmtChannel) return true; // fake success if functionality is not there
|
||||
|
||||
outQueue.push(packet);
|
||||
uint16_t size = outQueue.size();
|
||||
if (size > 10) {
|
||||
DIAG(F("Warning: outQueue %d > 10"),size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MotorDriver::loop() {
|
||||
if (rmtChannel && !outQueue.empty() && rmtChannel->RMTfillData(outQueue.front()))
|
||||
outQueue.pop();
|
||||
}
|
||||
|
||||
MotorDriverContainer::MotorDriverContainer(const FSH * motorShieldName,
|
||||
MotorDriver *m0,
|
||||
MotorDriver *m1,
|
||||
MotorDriver *m2,
|
||||
MotorDriver *m3,
|
||||
MotorDriver *m4,
|
||||
MotorDriver *m5,
|
||||
MotorDriver *m6,
|
||||
MotorDriver *m7) {
|
||||
// THIS AUTOMATIC DOES NOT WORK YET. TIMER_MAIN AND TIMER_PROG required in CONSTRUCTOR
|
||||
// AND CAN NOT BE ADDED LATER
|
||||
if (m0) {
|
||||
if (m0->type() == TYPE_UNKNOWN)
|
||||
m0->setType(TIMER_MAIN);
|
||||
mD.push_back(m0);
|
||||
}
|
||||
if (m1) {
|
||||
if (m1->type() == TYPE_UNKNOWN)
|
||||
m1->setType(TIMER_PROG);
|
||||
mD.push_back(m1);
|
||||
}
|
||||
if (m2) mD.push_back(m2);
|
||||
if (m3) mD.push_back(m3);
|
||||
if (m4) mD.push_back(m4);
|
||||
if (m5) mD.push_back(m5);
|
||||
if (m6) mD.push_back(m6);
|
||||
if (m7) mD.push_back(m7);
|
||||
shieldName = (FSH *)motorShieldName;
|
||||
}
|
||||
|
||||
void MotorDriverContainer::loop() {
|
||||
// loops over MotorDrivers which have loop tasks
|
||||
if (mD.empty())
|
||||
return;
|
||||
for(const auto& d: mD)
|
||||
if (d->type() & (RMT_MAIN | RMT_PROG))
|
||||
d->loop();
|
||||
}
|
||||
|
||||
std::vector<MotorDriver*> MotorDriverContainer::getDriverType(driverType t) {
|
||||
std::vector<MotorDriver*> v;
|
||||
for(const auto& d: mD){
|
||||
if (d->type() & t)
|
||||
v.push_back(d);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
MotorDriverContainer MotorDriverContainer::mDC(MOTOR_SHIELD_TYPE);
|
||||
|
106
MotorDriver.h
106
MotorDriver.h
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -19,23 +16,36 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MotorDriver_h
|
||||
#define MotorDriver_h
|
||||
#include <vector>
|
||||
#include "defines.h"
|
||||
#include "FSH.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
// Virtualised Motor shield 1-track hardware Interface
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <queue>
|
||||
#include "DCCRMT.h"
|
||||
#endif
|
||||
|
||||
// Number of preamble bits (moved here so MotorDriver and Waveform know)
|
||||
const int PREAMBLE_BITS_MAIN = 16;
|
||||
const int PREAMBLE_BITS_PROG = 22;
|
||||
|
||||
#ifndef UNUSED_PIN // sync define with the one in MotorDrivers.h
|
||||
#define UNUSED_PIN 127 // inside int8_t
|
||||
#endif
|
||||
|
||||
#if defined(__IMXRT1062__)
|
||||
#if defined(__IMXRT1062__) || defined(ESP_FAMILY)
|
||||
typedef uint32_t PORTTYPE;
|
||||
struct FASTPIN {
|
||||
volatile uint32_t *inout;
|
||||
uint32_t maskHIGH;
|
||||
uint32_t maskLOW;
|
||||
};
|
||||
#else
|
||||
typedef uint8_t PORTTYPE;
|
||||
struct FASTPIN {
|
||||
volatile uint8_t *inout;
|
||||
uint8_t maskHIGH;
|
||||
@@ -43,16 +53,31 @@ struct FASTPIN {
|
||||
};
|
||||
#endif
|
||||
|
||||
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
|
||||
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
|
||||
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
|
||||
#define isLOW(fastpin) (!isHIGH(fastpin))
|
||||
|
||||
typedef byte driverType;
|
||||
const driverType TYPE_UNKNOWN=0;
|
||||
const driverType TIMER_MAIN=1;
|
||||
const driverType TIMER_PROG=2;
|
||||
const driverType RMT_MAIN=4;
|
||||
const driverType RMT_PROG=16;
|
||||
const driverType DC_ENA=32;
|
||||
const driverType DC_BRAKE=64;
|
||||
|
||||
class MotorDriver {
|
||||
public:
|
||||
MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
|
||||
byte current_pin, float senseFactor, unsigned int tripMilliamps, byte faultPin);
|
||||
virtual void setPower( bool on);
|
||||
virtual void setSignal( bool high);
|
||||
virtual void setBrake( bool on);
|
||||
virtual int getCurrentRaw();
|
||||
virtual unsigned int raw2mA( int raw);
|
||||
virtual int mA2raw( unsigned int mA);
|
||||
byte current_pin, float senseFactor, unsigned int tripMilliamps, byte faultPin,
|
||||
driverType t=TYPE_UNKNOWN);
|
||||
void setPower( bool on);
|
||||
void setSignal( bool high);
|
||||
void setBrake( bool on);
|
||||
int getCurrentRaw();
|
||||
unsigned int raw2mA( int raw);
|
||||
int mA2raw( unsigned int mA);
|
||||
inline int getRawCurrentTripValue() {
|
||||
return rawCurrentTripValue;
|
||||
}
|
||||
@@ -63,6 +88,13 @@ class MotorDriver {
|
||||
inline byte getFaultPin() {
|
||||
return faultPin;
|
||||
}
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
void loop();
|
||||
inline driverType type() { return dtype; };
|
||||
inline void setType(driverType t) { dtype = t; };
|
||||
bool schedulePacket(dccPacket packet);
|
||||
#endif
|
||||
|
||||
private:
|
||||
void getFastPin(const FSH* type,int pin, bool input, FASTPIN & result);
|
||||
void getFastPin(const FSH* type,int pin, FASTPIN & result) {
|
||||
@@ -87,5 +119,53 @@ class MotorDriver {
|
||||
if (doit) __enable_irq();
|
||||
}
|
||||
#endif
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
RMTChannel* rmtChannel;
|
||||
std::queue<dccPacket> outQueue;
|
||||
driverType dtype;
|
||||
#endif
|
||||
};
|
||||
|
||||
class MotorDriverContainer {
|
||||
public:
|
||||
MotorDriverContainer(const FSH * motorShieldName,
|
||||
MotorDriver *m0=NULL,
|
||||
MotorDriver *m1=NULL,
|
||||
MotorDriver *m2=NULL,
|
||||
MotorDriver *m3=NULL,
|
||||
MotorDriver *m4=NULL,
|
||||
MotorDriver *m5=NULL,
|
||||
MotorDriver *m6=NULL,
|
||||
MotorDriver *m7=NULL);
|
||||
static MotorDriverContainer mDC;
|
||||
inline void add(MotorDriver *m) {
|
||||
mD.push_back(m);
|
||||
};
|
||||
// void SetCapability(byte n, byte cap, char [] name);
|
||||
inline FSH *getMotorShieldName() { return shieldName; };
|
||||
inline void diag() {
|
||||
if (mD.empty()) {
|
||||
DIAG(F("Container empty"));
|
||||
return;
|
||||
}
|
||||
for(const auto& d: mD)
|
||||
DIAG(F("Container: mDType=%d"),d->type());
|
||||
};
|
||||
inline MotorDriver *mainTrack() {
|
||||
std::vector<MotorDriver *> v = getDriverType(TIMER_MAIN);
|
||||
if(v.empty()) return NULL;
|
||||
return v.front();
|
||||
};
|
||||
inline MotorDriver *progTrack() {
|
||||
std::vector<MotorDriver *> v = getDriverType(TIMER_PROG);
|
||||
if(v.empty()) return NULL;
|
||||
return v.front();
|
||||
};
|
||||
void loop();
|
||||
std::vector<MotorDriver*> getDriverType(driverType t);
|
||||
|
||||
private:
|
||||
std::vector<MotorDriver *>mD;
|
||||
FSH *shieldName;
|
||||
};
|
||||
#endif
|
||||
|
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020 Harald Barth. All rights reserved.
|
||||
@@ -89,22 +87,4 @@
|
||||
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN), \
|
||||
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
|
||||
|
||||
// Makeblock ORION UNO like sized board with integrated motor driver
|
||||
// This is like an Uno with H-bridge and RJ12 contacts instead of pin rows.
|
||||
// No current sense. Barrel connector max 12V, Vmotor max 15V. 1.1A polyfuse as output protection.
|
||||
// Main is marked M1 and near RJ12 #5
|
||||
// Prog is marked M2 and near RJ12 #4
|
||||
// For details see
|
||||
// http://docs.makeblock.com/diy-platform/en/electronic-modules/main-control-boards/makeblock-orion.html
|
||||
#define ORION_UNO_INTEGRATED_SHIELD F("ORION_UNO_INTEGRATED_SHIELD"), \
|
||||
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN), \
|
||||
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN)
|
||||
|
||||
// This is an example how to setup a motor shield definition for a motor shield connected
|
||||
// to an NANO EVERY board. You have to make the connectons from the shield to the board
|
||||
// as in this example or adjust the values yourself.
|
||||
#define NANOEVERY_EXAMPLE F("NANOEVERY_EXAMPLE"), \
|
||||
new MotorDriver(5, 6, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN),\
|
||||
new MotorDriver(9, 10, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
|
||||
|
||||
#endif
|
||||
|
14
Outputs.cpp
14
Outputs.cpp
@@ -1,9 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -86,9 +82,7 @@ the state of any outputs being monitored or controlled by a separate interface o
|
||||
**********************************************************************/
|
||||
|
||||
#include "Outputs.h"
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "EEStore.h"
|
||||
#endif
|
||||
#include "StringFormatter.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
@@ -108,11 +102,10 @@ void Output::activate(uint16_t s){
|
||||
data.active = s; // if s>0, set status to active, else inactive
|
||||
// set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
|
||||
IODevice::write(data.pin, s ^ data.invert);
|
||||
#ifndef DISABLE_EEPROM
|
||||
|
||||
// Update EEPROM if output has been stored.
|
||||
if(EEStore::eeStore->data.nOutputs > 0 && num > 0)
|
||||
EEPROM.put(num, data.oStatus);
|
||||
#endif
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@@ -148,7 +141,7 @@ bool Output::remove(uint16_t n){
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to load configuration and state of all Outputs from EEPROM
|
||||
#ifndef DISABLE_EEPROM
|
||||
|
||||
void Output::load(){
|
||||
struct OutputData data;
|
||||
Output *tt;
|
||||
@@ -183,7 +176,6 @@ void Output::store(){
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to create an Output object
|
||||
|
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -51,10 +48,8 @@ public:
|
||||
bool isActive();
|
||||
static Output* get(uint16_t);
|
||||
static bool remove(uint16_t);
|
||||
#ifndef DISABLE_EEPROM
|
||||
static void load();
|
||||
static void store();
|
||||
#endif
|
||||
static Output *create(uint16_t, VPIN, int, int=0);
|
||||
static Output *firstOutput;
|
||||
struct OutputData data;
|
||||
|
@@ -17,12 +17,12 @@ Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital
|
||||
* Control of all cab functions F0-F28 and F29-F68
|
||||
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
|
||||
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
|
||||
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
||||
* And manu more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
||||
|
||||
|
||||
# What’s in this Repository?
|
||||
|
||||
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
|
||||
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano. All sketch files are in the folder named CommandStation-EX and its subforlders.
|
||||
|
||||
To utilize this sketch, you can use the following:
|
||||
|
||||
@@ -69,7 +69,7 @@ in config.h.
|
||||
* Automatic slot (register) management
|
||||
* Automation (coming soon)
|
||||
|
||||
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
|
||||
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means to you, click [HERE](notes/rewrite.md).
|
||||
|
||||
# More information
|
||||
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)
|
||||
|
@@ -1,8 +1,8 @@
|
||||
#ifndef EXRAIL_H
|
||||
#define EXRAIL_H
|
||||
#ifndef RMFT_H
|
||||
#define RMFT_H
|
||||
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
#include "EXRAIL2.h"
|
||||
#if defined(RMFT_ACTIVE)
|
||||
#include "RMFT2.h"
|
||||
|
||||
class RMFT {
|
||||
public:
|
||||
@@ -10,7 +10,7 @@
|
||||
static void inline loop() {RMFT2::loop();}
|
||||
};
|
||||
|
||||
#include "EXRAILMacros.h"
|
||||
#include "RMFTMacros.h"
|
||||
|
||||
#else
|
||||
// Dummy RMFT
|
769
RMFT2.cpp
Normal file
769
RMFT2.cpp
Normal file
@@ -0,0 +1,769 @@
|
||||
/*
|
||||
* © 2020,2021 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include "RMFT2.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DIAG.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "Turnouts.h"
|
||||
|
||||
|
||||
// Command parsing keywords
|
||||
const int16_t HASH_KEYWORD_EXRAIL=15435;
|
||||
const int16_t HASH_KEYWORD_ON = 2657;
|
||||
const int16_t HASH_KEYWORD_START=23232;
|
||||
const int16_t HASH_KEYWORD_RESERVE=11392;
|
||||
const int16_t HASH_KEYWORD_FREE=-23052;
|
||||
const int16_t HASH_KEYWORD_LATCH=1618;
|
||||
const int16_t HASH_KEYWORD_UNLATCH=1353;
|
||||
const int16_t HASH_KEYWORD_PAUSE=-4142;
|
||||
const int16_t HASH_KEYWORD_RESUME=27609;
|
||||
const int16_t HASH_KEYWORD_KILL=5218;
|
||||
const int16_t HASH_KEYWORD_ROUTES=-3702;
|
||||
|
||||
// One instance of RMFT clas is used for each "thread" in the automation.
|
||||
// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.
|
||||
// The thrrads exist in a ring, each time through loop() the next thread in the ring is serviced.
|
||||
|
||||
// Statics
|
||||
const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track
|
||||
int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck
|
||||
bool RMFT2::diag=false; // <D EXRAIL ON>
|
||||
RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring.
|
||||
RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
|
||||
// when pausingTask is set, that is the ONLY task that gets any service,
|
||||
// and all others will have their locos stopped, then resumed after the pausing task resumes.
|
||||
byte RMFT2::flags[MAX_FLAGS];
|
||||
|
||||
#define GET_OPCODE GETFLASH(RMFT2::RouteCode+progCounter)
|
||||
#define GET_OPERAND(n) GETFLASHW(RMFT2::RouteCode+progCounter+1+(n*3))
|
||||
#define SKIPOP progCounter+=3
|
||||
|
||||
/* static */ void RMFT2::begin() {
|
||||
DCCEXParser::setRMFTFilter(RMFT2::ComandFilter);
|
||||
for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;
|
||||
int progCounter;
|
||||
// first pass startup, define any turnouts or servos, set signals red and count size.
|
||||
for (progCounter=0;; SKIPOP){
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) break;
|
||||
|
||||
switch (opcode) {
|
||||
case OPCODE_AT:
|
||||
case OPCODE_AFTER:
|
||||
case OPCODE_IF:
|
||||
case OPCODE_IFNOT:
|
||||
int16_t pin = (int16_t)GET_OPERAND(0);
|
||||
if (pin<0) pin = -pin;
|
||||
IODevice::configureInput((VPIN)pin,true);
|
||||
}
|
||||
|
||||
if (opcode==OPCODE_SIGNAL) {
|
||||
VPIN red=GET_OPERAND(0);
|
||||
VPIN amber=GET_OPERAND(1);
|
||||
VPIN green=GET_OPERAND(2);
|
||||
IODevice::write(red,true);
|
||||
if (amber) IODevice::write(amber,false);
|
||||
IODevice::write(green,false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opcode==OPCODE_TURNOUT) {
|
||||
VPIN id=GET_OPERAND(0);
|
||||
int addr=GET_OPERAND(1);
|
||||
byte subAddr=GET_OPERAND(2);
|
||||
DCCTurnout::create(id,addr,subAddr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opcode==OPCODE_SERVOTURNOUT) {
|
||||
int16_t id=GET_OPERAND(0);
|
||||
VPIN pin=GET_OPERAND(1);
|
||||
int activeAngle=GET_OPERAND(2);
|
||||
int inactiveAngle=GET_OPERAND(3);
|
||||
int profile=GET_OPERAND(4);
|
||||
ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opcode==OPCODE_PINTURNOUT) {
|
||||
int16_t id=GET_OPERAND(0);
|
||||
VPIN pin=GET_OPERAND(1);
|
||||
VpinTurnout::create(id,pin);
|
||||
continue;
|
||||
}
|
||||
// other opcodes are not needed on this pass
|
||||
}
|
||||
SKIPOP; // include ENDROUTES opcode
|
||||
DIAG(F("EXRAIL %db, MAX_FLAGS=%d"), progCounter,MAX_FLAGS);
|
||||
new RMFT2(0); // add the startup route
|
||||
}
|
||||
|
||||
// This filter intercepts <> commands to do the following:
|
||||
// - Implement RMFT specific commands/diagnostics
|
||||
// - Reject/modify JMRI commands that would interfere with RMFT processing
|
||||
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
|
||||
(void)stream; // avoid compiler warning if we don't access this parameter
|
||||
bool reject=false;
|
||||
switch(opcode) {
|
||||
|
||||
case 'D':
|
||||
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF>
|
||||
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1);
|
||||
opcode=0;
|
||||
}
|
||||
break;
|
||||
|
||||
case '/': // New EXRAIL command
|
||||
reject=!parseSlash(stream,paramCount,p);
|
||||
opcode=0;
|
||||
break;
|
||||
|
||||
default: // other commands pass through
|
||||
break;
|
||||
}
|
||||
if (reject) {
|
||||
opcode=0;
|
||||
StringFormatter::send(stream,F("<X>"));
|
||||
}
|
||||
}
|
||||
|
||||
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
|
||||
|
||||
if (paramCount==0) { // STATUS
|
||||
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
|
||||
(int)(task->taskId),task->progCounter,task->loco,
|
||||
task->invert?'I':' ',
|
||||
task->speedo,
|
||||
task->forward?'F':'R'
|
||||
);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
// Now stream the flags
|
||||
for (int id=0;id<MAX_FLAGS; id++) {
|
||||
byte flag=flags[id];
|
||||
if (flag & ~TASK_FLAG) { // not interested in TASK_FLAG only. Already shown above
|
||||
StringFormatter::send(stream,F("\nflags[%d} "),id);
|
||||
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
|
||||
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
|
||||
}
|
||||
}
|
||||
StringFormatter::send(stream,F(" *>\n"));
|
||||
return true;
|
||||
}
|
||||
switch (p[0]) {
|
||||
case HASH_KEYWORD_PAUSE: // </ PAUSE>
|
||||
if (paramCount!=1) return false;
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
pausingTask=(RMFT2 *)1; // Impossible task address
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_RESUME: // </ RESUME>
|
||||
if (paramCount!=1) return false;
|
||||
pausingTask=NULL;
|
||||
{
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->loco) task->driveLoco(task->speedo);
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
|
||||
case HASH_KEYWORD_START: // </ START [cab] route >
|
||||
if (paramCount<2 || paramCount>3) return false;
|
||||
{
|
||||
int route=(paramCount==2) ? p[1] : p[2];
|
||||
uint16_t cab=(paramCount==2)? 0 : p[1];
|
||||
int pc=locateRouteStart(route);
|
||||
if (pc<0) return false;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_ROUTES: // </ ROUTES > JMRI withrottle support
|
||||
if (paramCount>1) return false;
|
||||
StringFormatter::send(stream,F("</ROUTES "));
|
||||
emitWithrottleRouteList(stream);
|
||||
StringFormatter::send(stream,F(">"));
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// all other / commands take 1 parameter 0 to MAX_FLAGS-1
|
||||
|
||||
if (paramCount!=2 || p[1]<0 || p[1]>=MAX_FLAGS) return false;
|
||||
|
||||
switch (p[0]) {
|
||||
case HASH_KEYWORD_KILL: // Kill taskid
|
||||
{
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->taskId==p[1]) {
|
||||
delete task;
|
||||
return true;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case HASH_KEYWORD_RESERVE: // force reserve a section
|
||||
setFlag(p[1],SECTION_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_FREE: // force free a section
|
||||
setFlag(p[1],0,SECTION_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_LATCH:
|
||||
setFlag(p[1], LATCH_FLAG);
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_UNLATCH:
|
||||
setFlag(p[1], 0, LATCH_FLAG);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This emits Routes and Automations to Withrottle
|
||||
// Automations are given a state to set the button to "handoff" which implies
|
||||
// handing over the loco to the automation.
|
||||
// Routes are given "Set" buttons and do not cause the loco to be handed over.
|
||||
void RMFT2::emitWithrottleRouteList(Print* stream) {
|
||||
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL"));
|
||||
emitWithrottleDescriptions(stream);
|
||||
StringFormatter::send(stream,F("\n"));
|
||||
}
|
||||
|
||||
|
||||
RMFT2::RMFT2(int progCtr) {
|
||||
progCounter=progCtr;
|
||||
|
||||
// get an unused task id from the flags table
|
||||
taskId=255; // in case of overflow
|
||||
for (int f=0;f<MAX_FLAGS;f++) {
|
||||
if (!getFlag(f,TASK_FLAG)) {
|
||||
taskId=f;
|
||||
setFlag(f, TASK_FLAG);
|
||||
break;
|
||||
}
|
||||
}
|
||||
delayTime=0;
|
||||
loco=0;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
stackDepth=0;
|
||||
onTurnoutId=0; // Not handling an ONTHROW/ONCLOSE
|
||||
|
||||
// chain into ring of RMFTs
|
||||
if (loopTask==NULL) {
|
||||
loopTask=this;
|
||||
next=this;
|
||||
}
|
||||
else {
|
||||
next=loopTask->next;
|
||||
loopTask->next=this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RMFT2::~RMFT2() {
|
||||
driveLoco(1); // ESTOP my loco if any
|
||||
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
|
||||
if (next==this) loopTask=NULL;
|
||||
else for (RMFT2* ring=next;;ring=ring->next) if (ring->next == this) {
|
||||
ring->next=next;
|
||||
loopTask=next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::createNewTask(int route, uint16_t cab) {
|
||||
int pc=locateRouteStart(route);
|
||||
if (pc<0) return;
|
||||
RMFT2* task=new RMFT2(pc);
|
||||
task->loco=cab;
|
||||
}
|
||||
|
||||
|
||||
int RMFT2::locateRouteStart(int16_t _route) {
|
||||
if (_route==0) return 0; // Route 0 is always start of ROUTES for default startup
|
||||
for (int progCounter=0;;SKIPOP) {
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) {
|
||||
DIAG(F("RMFT2 sequence %d not found"), _route);
|
||||
return -1;
|
||||
}
|
||||
if ((opcode==OPCODE_ROUTE || opcode==OPCODE_AUTOMATION || opcode==OPCODE_SEQUENCE)
|
||||
&& _route==(int)GET_OPERAND(0)) return progCounter;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
void RMFT2::driveLoco(byte speed) {
|
||||
if (loco<=0) return; // Prevent broadcast!
|
||||
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
|
||||
}
|
||||
DCC::setThrottle(loco,speed, forward^invert);
|
||||
speedo=speed;
|
||||
}
|
||||
|
||||
bool RMFT2::readSensor(uint16_t sensorId) {
|
||||
// Exrail operands are unsigned but we need the signed version as inserted by the macros.
|
||||
int16_t sId=(int16_t) sensorId;
|
||||
|
||||
VPIN vpin=abs(sId);
|
||||
if (getFlag(vpin,LATCH_FLAG)) return true; // latched on
|
||||
|
||||
// negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting)
|
||||
bool s= IODevice::read(vpin) ^ (sId<0);
|
||||
if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sId);
|
||||
return s;
|
||||
}
|
||||
|
||||
bool RMFT2::skipIfBlock() {
|
||||
// returns false if killed
|
||||
short nest = 1;
|
||||
while (nest > 0) {
|
||||
SKIPOP;
|
||||
byte opcode = GET_OPCODE;
|
||||
switch(opcode) {
|
||||
case OPCODE_ENDEXRAIL:
|
||||
kill(F("missing ENDIF"), nest);
|
||||
return false;
|
||||
case OPCODE_IF:
|
||||
case OPCODE_IFNOT:
|
||||
case OPCODE_IFRANDOM:
|
||||
case OPCODE_IFRESERVE:
|
||||
nest++;
|
||||
break;
|
||||
case OPCODE_ENDIF:
|
||||
nest--;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* static */ void RMFT2::readLocoCallback(int16_t cv) {
|
||||
progtrackLocoId=cv;
|
||||
}
|
||||
|
||||
void RMFT2::loop() {
|
||||
|
||||
// Round Robin call to a RMFT task each time
|
||||
if (loopTask==NULL) return;
|
||||
|
||||
loopTask=loopTask->next;
|
||||
|
||||
if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2();
|
||||
}
|
||||
|
||||
|
||||
void RMFT2::loop2() {
|
||||
if (delayTime!=0 && millis()-delayStart < delayTime) return;
|
||||
|
||||
byte opcode = GET_OPCODE;
|
||||
int16_t operand = GET_OPERAND(0);
|
||||
// if (diag) DIAG(F("RMFT2 %d %d"),opcode,operand);
|
||||
// Attention: Returning from this switch leaves the program counter unchanged.
|
||||
// This is used for unfinished waits for timers or sensors.
|
||||
// Breaking from this switch will step to the next step in the route.
|
||||
switch ((OPCODE)opcode) {
|
||||
|
||||
case OPCODE_THROW:
|
||||
Turnout::setClosed(operand, false);
|
||||
break;
|
||||
|
||||
case OPCODE_CLOSE:
|
||||
Turnout::setClosed(operand, true);
|
||||
break;
|
||||
|
||||
case OPCODE_REV:
|
||||
forward = false;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_FWD:
|
||||
forward = true;
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_SPEED:
|
||||
driveLoco(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_INVERT_DIRECTION:
|
||||
invert= !invert;
|
||||
driveLoco(speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_RESERVE:
|
||||
if (getFlag(operand,SECTION_FLAG)) {
|
||||
driveLoco(0);
|
||||
delayMe(500);
|
||||
return;
|
||||
}
|
||||
setFlag(operand,SECTION_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_FREE:
|
||||
setFlag(operand,0,SECTION_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_AT:
|
||||
if (readSensor(operand)) break;
|
||||
delayMe(50);
|
||||
return;
|
||||
|
||||
case OPCODE_AFTER: // waits for sensor to hit and then remain off for 0.5 seconds. (must come after an AT operation)
|
||||
if (readSensor(operand)) {
|
||||
// reset timer to half a second and keep waiting
|
||||
waitAfter=millis();
|
||||
delayMe(50);
|
||||
return;
|
||||
}
|
||||
if (millis()-waitAfter < 500 ) return;
|
||||
break;
|
||||
|
||||
case OPCODE_LATCH:
|
||||
setFlag(operand,LATCH_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_UNLATCH:
|
||||
setFlag(operand,0,LATCH_FLAG);
|
||||
break;
|
||||
|
||||
case OPCODE_SET:
|
||||
IODevice::write(operand,true);
|
||||
break;
|
||||
|
||||
case OPCODE_RESET:
|
||||
IODevice::write(operand,false);
|
||||
break;
|
||||
|
||||
case OPCODE_PAUSE:
|
||||
DCC::setThrottle(0,1,true); // pause all locos on the track
|
||||
pausingTask=this;
|
||||
break;
|
||||
|
||||
case OPCODE_POM:
|
||||
if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1));
|
||||
break;
|
||||
|
||||
case OPCODE_POWEROFF:
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
Serial.println(F("<p0>")); // Tell JMRI
|
||||
break;
|
||||
|
||||
case OPCODE_RESUME:
|
||||
pausingTask=NULL;
|
||||
driveLoco(speedo);
|
||||
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
|
||||
break;
|
||||
|
||||
case OPCODE_IF: // do next operand if sensor set
|
||||
if (!readSensor(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFNOT: // do next operand if sensor not set
|
||||
if (readSensor(operand)) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFRANDOM: // do block on random percentage
|
||||
if (random(100)>=operand) if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_IFRESERVE: // do block if we successfully RERSERVE
|
||||
if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG);
|
||||
else if (!skipIfBlock()) return;
|
||||
break;
|
||||
|
||||
case OPCODE_ENDIF:
|
||||
break;
|
||||
|
||||
case OPCODE_DELAY:
|
||||
delayMe(operand*100L);
|
||||
break;
|
||||
|
||||
case OPCODE_DELAYMINS:
|
||||
delayMe(operand*60L*1000L);
|
||||
break;
|
||||
|
||||
case OPCODE_RANDWAIT:
|
||||
delayMe(random(operand)*100L);
|
||||
break;
|
||||
|
||||
case OPCODE_RED:
|
||||
doSignal(operand,true,false,false);
|
||||
break;
|
||||
|
||||
case OPCODE_AMBER:
|
||||
doSignal(operand,false,true,false);
|
||||
break;
|
||||
|
||||
case OPCODE_GREEN:
|
||||
doSignal(operand,false,false,true);
|
||||
break;
|
||||
|
||||
case OPCODE_FON:
|
||||
if (loco) DCC::setFn(loco,operand,true);
|
||||
break;
|
||||
|
||||
case OPCODE_FOFF:
|
||||
if (loco) DCC::setFn(loco,operand,false);
|
||||
break;
|
||||
|
||||
case OPCODE_XFON:
|
||||
DCC::setFn(operand,GET_OPERAND(1),true);
|
||||
break;
|
||||
|
||||
case OPCODE_XFOFF:
|
||||
DCC::setFn(operand,GET_OPERAND(1),false);
|
||||
break;
|
||||
|
||||
case OPCODE_FOLLOW:
|
||||
progCounter=locateRouteStart(operand);
|
||||
if (progCounter<0) kill(F("FOLLOW unknown"), operand);
|
||||
return;
|
||||
|
||||
case OPCODE_CALL:
|
||||
if (stackDepth==MAX_STACK_DEPTH) {
|
||||
kill(F("CALL stack"), stackDepth);
|
||||
return;
|
||||
}
|
||||
callStack[stackDepth++]=progCounter+3;
|
||||
progCounter=locateRouteStart(operand);
|
||||
if (progCounter<0) kill(F("CALL unknown"),operand);
|
||||
return;
|
||||
|
||||
case OPCODE_RETURN:
|
||||
if (stackDepth==0) {
|
||||
kill(F("RETURN stack"));
|
||||
return;
|
||||
}
|
||||
progCounter=callStack[--stackDepth];
|
||||
return;
|
||||
|
||||
case OPCODE_ENDTASK:
|
||||
case OPCODE_ENDEXRAIL:
|
||||
kill();
|
||||
return;
|
||||
|
||||
case OPCODE_JOIN:
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
DCC::setProgTrackSyncMain(true);
|
||||
Serial.println(F("<p1 JOIN>")); // Tell JMRI
|
||||
break;
|
||||
|
||||
case OPCODE_UNJOIN:
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
break;
|
||||
|
||||
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
|
||||
progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet
|
||||
DCC::getLocoId(readLocoCallback);
|
||||
break;
|
||||
|
||||
case OPCODE_READ_LOCO2:
|
||||
if (progtrackLocoId==LOCO_ID_WAITING) {
|
||||
delayMe(100);
|
||||
return; // still waiting for callback
|
||||
}
|
||||
if (progtrackLocoId<0) {
|
||||
kill(F("No Loco Found"),progtrackLocoId);
|
||||
return; // still waiting for callback
|
||||
}
|
||||
|
||||
loco=progtrackLocoId;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
break;
|
||||
|
||||
case OPCODE_START:
|
||||
{
|
||||
int newPc=locateRouteStart(operand);
|
||||
if (newPc<0) break;
|
||||
new RMFT2(newPc);
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SENDLOCO: // cab, route
|
||||
{
|
||||
int newPc=locateRouteStart(GET_OPERAND(1));
|
||||
if (newPc<0) break;
|
||||
RMFT2* newtask=new RMFT2(newPc); // create new task
|
||||
newtask->loco=operand;
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_SETLOCO:
|
||||
{
|
||||
loco=operand;
|
||||
speedo=0;
|
||||
forward=true;
|
||||
invert=false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration)
|
||||
IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2),GET_OPERAND(3));
|
||||
break;
|
||||
|
||||
case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin)
|
||||
if (IODevice::isBusy(operand)) {
|
||||
delayMe(100);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case OPCODE_PRINT:
|
||||
printMessage(operand);
|
||||
break;
|
||||
|
||||
case OPCODE_ROUTE:
|
||||
case OPCODE_AUTOMATION:
|
||||
case OPCODE_SEQUENCE:
|
||||
if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
|
||||
break;
|
||||
|
||||
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operad byte.
|
||||
case OPCODE_SIGNAL: // Signal definition ignore at run time
|
||||
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
|
||||
case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime
|
||||
case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime
|
||||
case OPCODE_ONCLOSE: // Turnout event catcers ignored here
|
||||
case OPCODE_ONTHROW: // Turnout definition ignored at runtime
|
||||
break;
|
||||
|
||||
default:
|
||||
kill(F("INVOP"),operand);
|
||||
}
|
||||
// Falling out of the switch means move on to the next opcode
|
||||
SKIPOP;
|
||||
}
|
||||
|
||||
void RMFT2::delayMe(long delay) {
|
||||
delayTime=delay;
|
||||
delayStart=millis();
|
||||
}
|
||||
|
||||
void RMFT2::setFlag(VPIN id,byte onMask, byte offMask) {
|
||||
if (FLAGOVERFLOW(id)) return; // Outside range limit
|
||||
byte f=flags[id];
|
||||
f &= ~offMask;
|
||||
f |= onMask;
|
||||
flags[id]=f;
|
||||
}
|
||||
|
||||
bool RMFT2::getFlag(VPIN id,byte mask) {
|
||||
if (FLAGOVERFLOW(id)) return 0; // Outside range limit
|
||||
return flags[id]&mask;
|
||||
}
|
||||
|
||||
void RMFT2::kill(const FSH * reason, int operand) {
|
||||
if (reason) DIAG(F("EXRAIL ERROR pc=%d, cab=%d, %S %d"), progCounter,loco, reason, operand);
|
||||
else if (diag) DIAG(F("ENDTASK at pc=%d"), progCounter);
|
||||
delete this;
|
||||
}
|
||||
|
||||
/* static */ void RMFT2::doSignal(VPIN id,bool red, bool amber, bool green) {
|
||||
// CAUTION: hides class member progCounter
|
||||
for (int progCounter=0;; SKIPOP){
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) return;
|
||||
if (opcode!=OPCODE_SIGNAL) continue;
|
||||
byte redpin=GET_OPERAND(0);
|
||||
if (redpin!=id)continue;
|
||||
byte amberpin=GET_OPERAND(1);
|
||||
byte greenpin=GET_OPERAND(2);
|
||||
// If amberpin is zero, synthesise amber from red+green
|
||||
IODevice::write(redpin,red || (amber && (amberpin==0)));
|
||||
if (amberpin) IODevice::write(amberpin,amber);
|
||||
if (greenpin) IODevice::write(greenpin,green || (amber && (amberpin==0)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) {
|
||||
|
||||
// Check we dont already have a task running this turnout
|
||||
RMFT2 * task=loopTask;
|
||||
while(task) {
|
||||
if (task->onTurnoutId==turnoutId) {
|
||||
DIAG(F("Recursive ONTHROW/ONCLOSE for Turnout %d"),turnoutId);
|
||||
return;
|
||||
}
|
||||
task=task->next;
|
||||
if (task==loopTask) break;
|
||||
}
|
||||
// Hunt for an ONTHROW/ONCLOSE for this turnout
|
||||
byte huntFor=closed ? OPCODE_ONCLOSE : OPCODE_ONTHROW ;
|
||||
// caution hides class progCounter;
|
||||
for (int progCounter=0;; SKIPOP){
|
||||
byte opcode=GET_OPCODE;
|
||||
if (opcode==OPCODE_ENDEXRAIL) return;
|
||||
if (opcode!=huntFor) continue;
|
||||
if (turnoutId!=(int16_t)GET_OPERAND(0)) continue;
|
||||
task=new RMFT2(progCounter); // new task starts at this instruction
|
||||
task->onTurnoutId=turnoutId; // flag for recursion detector
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void RMFT2::printMessage2(const FSH * msg) {
|
||||
DIAG(F("EXRAIL(%d) %S"),loco,msg);
|
||||
}
|
||||
|
||||
// This is called by emitRouteDescriptions to emit a withrottle description for a route or autoomation.
|
||||
void RMFT2::emitRouteDescription(Print * stream, char type, int id, const FSH * description) {
|
||||
StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"),
|
||||
type,id,description, type=='R'?'2':'4');
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2022 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -18,13 +16,13 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef EXRAIL2_H
|
||||
#define EXRAIL2_H
|
||||
#ifndef RMFT2_H
|
||||
#define RMFT2_H
|
||||
#include "FSH.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
// The following are the operation codes (or instructions) for a kind of virtual machine.
|
||||
// Each instruction is normally 3 bytes long with an operation code followed by a parameter.
|
||||
// Each instruction is normally 2 bytes long with an operation code followed by a parameter.
|
||||
// In cases where more than one parameter is required, the first parameter is followed by one
|
||||
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
|
||||
// searching easier as a parameter can never be confused with an opcode.
|
||||
@@ -32,52 +30,34 @@
|
||||
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
|
||||
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
|
||||
OPCODE_RESERVE,OPCODE_FREE,
|
||||
OPCODE_AT,OPCODE_AFTER,OPCODE_AUTOSTART,
|
||||
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,OPCODE_IFTIMEOUT,
|
||||
OPCODE_AT,OPCODE_AFTER,
|
||||
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
|
||||
OPCODE_IF,OPCODE_IFNOT,OPCODE_ENDIF,OPCODE_IFRANDOM,OPCODE_IFRESERVE,
|
||||
OPCODE_IFCLOSED, OPCODE_IFTHROWN,OPCODE_ELSE,
|
||||
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
|
||||
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_RANDWAIT,
|
||||
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
|
||||
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
|
||||
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,
|
||||
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
|
||||
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
|
||||
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,OPCODE_POM,
|
||||
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,
|
||||
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
|
||||
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,
|
||||
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
|
||||
OPCODE_PRINT,OPCODE_DCCACTIVATE,
|
||||
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,OPCODE_IFGTE,OPCODE_IFLT,
|
||||
OPCODE_ROSTER,
|
||||
OPCODE_PRINT,
|
||||
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Flag bits for status of hardware and TPL
|
||||
static const byte SECTION_FLAG = 0x80;
|
||||
static const byte LATCH_FLAG = 0x40;
|
||||
static const byte TASK_FLAG = 0x20;
|
||||
static const byte SPARE_FLAG = 0x10;
|
||||
static const byte COUNTER_MASK= 0x0F;
|
||||
static const byte SECTION_FLAG = 0x01;
|
||||
static const byte LATCH_FLAG = 0x02;
|
||||
static const byte TASK_FLAG = 0x04;
|
||||
|
||||
static const byte MAX_STACK_DEPTH=4;
|
||||
|
||||
static const short MAX_FLAGS=256;
|
||||
#define FLAGOVERFLOW(x) x>=MAX_FLAGS
|
||||
|
||||
class LookList {
|
||||
public:
|
||||
LookList(int16_t size);
|
||||
void add(int16_t lookup, int16_t result);
|
||||
int16_t find(int16_t value);
|
||||
private:
|
||||
int16_t m_size;
|
||||
int16_t m_loaded;
|
||||
int16_t * m_lookupArray;
|
||||
int16_t * m_resultArray;
|
||||
};
|
||||
|
||||
class RMFT2 {
|
||||
public:
|
||||
static void begin();
|
||||
@@ -89,17 +69,13 @@ class LookList {
|
||||
static void emitWithrottleRouteList(Print* stream);
|
||||
static void createNewTask(int route, uint16_t cab);
|
||||
static void turnoutEvent(int16_t id, bool closed);
|
||||
static void activateEvent(int16_t addr, bool active);
|
||||
static void emitTurnoutDescription(Print* stream,int16_t id);
|
||||
static const byte rosterNameCount;
|
||||
static void emitWithrottleRoster(Print * stream);
|
||||
static const FSH * getRosterFunctions(int16_t cabid);
|
||||
private:
|
||||
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
|
||||
static void streamFlags(Print* stream);
|
||||
static void setFlag(VPIN id,byte onMask, byte OffMask=0);
|
||||
static bool getFlag(VPIN id,byte mask);
|
||||
static bool getFlag(VPIN id,byte mask);
|
||||
static int locateRouteStart(int16_t _route);
|
||||
static int16_t progtrackLocoId;
|
||||
static void doSignal(VPIN id,bool red, bool amber, bool green);
|
||||
static void emitRouteDescription(Print * stream, char type, int id, const FSH * description);
|
||||
@@ -116,28 +92,18 @@ private:
|
||||
void kill(const FSH * reason=NULL,int operand=0);
|
||||
void printMessage(uint16_t id); // Built by RMFTMacros.h
|
||||
void printMessage2(const FSH * msg);
|
||||
|
||||
|
||||
|
||||
static bool diag;
|
||||
static const FLASH byte RouteCode[];
|
||||
static const FLASH int16_t SignalDefinitions[];
|
||||
static byte flags[MAX_FLAGS];
|
||||
static LookList * sequenceLookup;
|
||||
static LookList * onThrowLookup;
|
||||
static LookList * onCloseLookup;
|
||||
static LookList * onActivateLookup;
|
||||
static LookList * onDeactivateLookup;
|
||||
|
||||
|
||||
// Local variables - exist for each instance/task
|
||||
RMFT2 *next; // loop chain
|
||||
int progCounter; // Byte offset of next route opcode in ROUTES table
|
||||
unsigned long delayStart; // Used by opcodes that must be recalled before completing
|
||||
unsigned long waitAfter; // Used by OPCODE_AFTER
|
||||
unsigned long delayTime;
|
||||
union {
|
||||
unsigned long waitAfter; // Used by OPCODE_AFTER
|
||||
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
|
||||
};
|
||||
bool timeoutFlag;
|
||||
byte taskId;
|
||||
|
||||
uint16_t loco;
|
||||
@@ -145,7 +111,6 @@ private:
|
||||
bool invert;
|
||||
byte speedo;
|
||||
int16_t onTurnoutId;
|
||||
int16_t onActivateAddr;
|
||||
byte stackDepth;
|
||||
int callStack[MAX_STACK_DEPTH];
|
||||
};
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2022 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020,2021 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -18,9 +16,8 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EXRAILMacros_H
|
||||
#define EXRAILMacros_H
|
||||
#ifndef RMFTMacros_H
|
||||
#define RMFTMacros_H
|
||||
|
||||
// remove normal code LCD & SERIAL macros (will be restored later)
|
||||
#undef LCD
|
||||
@@ -28,7 +25,7 @@
|
||||
|
||||
|
||||
// This file will include and build the EXRAIL script and associated helper tricks.
|
||||
// It does this by including myAutomation.h several times, each with a set of macros to
|
||||
// It does this by incliding myAutomation.h several times, each with a set of macros to
|
||||
// extract the relevant parts.
|
||||
|
||||
// The entire automation script is contained within a byte array RMFT2::RouteCode[]
|
||||
@@ -49,145 +46,207 @@
|
||||
|
||||
// CAUTION: The macros below are multiple passed over myAutomation.h
|
||||
|
||||
// Pass 1 Implements aliases
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef ALIAS
|
||||
// Pass 1 Implements aliases and
|
||||
// converts descriptions to withrottle format emitter function
|
||||
// Most macros are simply ignored in this pass.
|
||||
|
||||
|
||||
#define ALIAS(name,value) const int name=value;
|
||||
#define EXRAIL void RMFT2::emitWithrottleDescriptions(Print * stream) {(void)stream;
|
||||
#define ROUTE(id, description) emitRouteDescription(stream,'R',id,F(description));
|
||||
#define AUTOMATION(id, description) emitRouteDescription(stream,'A',id,F(description));
|
||||
#define ENDEXRAIL }
|
||||
|
||||
#define AFTER(sensor_id)
|
||||
#define AMBER(signal_id)
|
||||
#define AT(sensor_id)
|
||||
#define CALL(route)
|
||||
#define CLOSE(id)
|
||||
#define DELAY(mindelay)
|
||||
#define DELAYMINS(mindelay)
|
||||
#define DELAYRANDOM(mindelay,maxdelay)
|
||||
#define DONE
|
||||
#define ENDIF
|
||||
#define ENDTASK
|
||||
#define ESTOP
|
||||
#define FADE(pin,value,ms)
|
||||
#define FOFF(func)
|
||||
#define FOLLOW(route)
|
||||
#define FON(func)
|
||||
#define FREE(blockid)
|
||||
#define FWD(speed)
|
||||
#define GREEN(signal_id)
|
||||
#define IF(sensor_id)
|
||||
#define IFNOT(sensor_id)
|
||||
#define IFRANDOM(percent)
|
||||
#define IFRESERVE(block)
|
||||
#define INVERT_DIRECTION
|
||||
#define JOIN
|
||||
#define LATCH(sensor_id)
|
||||
#define LCD(row,msg)
|
||||
#define LCN(msg)
|
||||
#define ONCLOSE(turnout_id)
|
||||
#define ONTHROW(turnout_id)
|
||||
#define PAUSE
|
||||
#define PRINT(msg)
|
||||
#define POM(cv,value)
|
||||
#define POWEROFF
|
||||
#define READ_LOCO
|
||||
#define RED(signal_id)
|
||||
#define RESERVE(blockid)
|
||||
#define RESET(pin)
|
||||
#define RESUME
|
||||
#define RETURN
|
||||
#define REV(speed)
|
||||
#define START(route)
|
||||
#define SENDLOCO(cab,route)
|
||||
#define SERIAL(msg)
|
||||
#define SERIAL1(msg)
|
||||
#define SERIAL2(msg)
|
||||
#define SERIAL3(msg)
|
||||
#define SERVO(id,position,profile)
|
||||
#define SERVO2(id,position,duration)
|
||||
#define SETLOCO(loco)
|
||||
#define SET(pin)
|
||||
#define SEQUENCE(id)
|
||||
#define SPEED(speed)
|
||||
#define STOP
|
||||
#undef SIGNAL
|
||||
#define SIGNAL(redpin,amberpin,greenpin)
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile)
|
||||
#define PIN_TURNOUT(id,pin)
|
||||
#define THROW(id)
|
||||
#define TURNOUT(id,addr,subaddr)
|
||||
#define UNJOIN
|
||||
#define UNLATCH(sensor_id)
|
||||
#define WAITFOR(pin)
|
||||
#define XFOFF(cab,func)
|
||||
#define XFON(cab,func)
|
||||
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Pass 2 convert descriptions to withrottle format emitter function
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
// setup for pass 2... Create getMessageText function
|
||||
#undef ALIAS
|
||||
#undef ROUTE
|
||||
#define ROUTE(id, description) emitRouteDescription(stream,'R',id,F(description));
|
||||
#undef AUTOMATION
|
||||
#define AUTOMATION(id, description) emitRouteDescription(stream,'A',id,F(description));
|
||||
void RMFT2::emitWithrottleDescriptions(Print * stream) {
|
||||
(void)stream;
|
||||
#include "myAutomation.h"
|
||||
}
|
||||
#undef AUTOMATION
|
||||
#define ROUTE(id, description)
|
||||
#define AUTOMATION(id, description)
|
||||
|
||||
// Pass 3... Create Text sending functions
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
const int StringMacroTracker1=__COUNTER__;
|
||||
#undef BROADCAST
|
||||
#define BROADCAST(msg) case (__COUNTER__ - StringMacroTracker1) : CommandDistributor::broadcastText(F(msg));break;
|
||||
#undef EXRAIL
|
||||
#undef PRINT
|
||||
#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break;
|
||||
#undef LCN
|
||||
#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break;
|
||||
#undef SERIAL
|
||||
#define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break;
|
||||
#undef SERIAL1
|
||||
#define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break;
|
||||
#undef SERIAL2
|
||||
#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break;
|
||||
#undef SERIAL3
|
||||
#define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break;
|
||||
#undef ENDEXRAIL
|
||||
#undef LCD
|
||||
#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break;
|
||||
|
||||
void RMFT2::printMessage(uint16_t id) {
|
||||
switch(id) {
|
||||
#include "myAutomation.h"
|
||||
default: break ;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pass 4: Turnout descriptions (optional)
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef TURNOUT
|
||||
#define TURNOUT(id,addr,subaddr,description...) case id: desc=F("" description); break;
|
||||
#undef PIN_TURNOUT
|
||||
#define PIN_TURNOUT(id,pin,description...) case id: desc=F("" description); break;
|
||||
#undef SERVO_TURNOUT
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) case id: desc=F("" description); break;
|
||||
|
||||
void RMFT2::emitTurnoutDescription(Print* stream,int16_t turnoutid) {
|
||||
const FSH * desc=F("");
|
||||
switch (turnoutid) {
|
||||
#include "myAutomation.h"
|
||||
default: break;
|
||||
}
|
||||
if (GETFLASH(desc)=='\0') desc=F("%d");
|
||||
StringFormatter::send(stream,desc,turnoutid);
|
||||
}
|
||||
|
||||
// Pass 5: Roster names (count)
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef ROSTER
|
||||
#define ROSTER(cabid,name,funcmap...) +1
|
||||
|
||||
const byte RMFT2::rosterNameCount=0
|
||||
#include "myAutomation.h"
|
||||
;
|
||||
|
||||
// Pass 6: Roster names emitter
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef ROSTER
|
||||
#define ROSTER(cabid,name,funcmap...) StringFormatter::send(stream,(FSH *)format,F(name),cabid,cabid<128?'S':'L');
|
||||
void RMFT2::emitWithrottleRoster(Print * stream) {
|
||||
static const char format[] FLASH ="]\\[%S}|{%d}|{%c";
|
||||
(void)format;
|
||||
StringFormatter::send(stream,F("RL%d"), rosterNameCount);
|
||||
#include "myAutomation.h"
|
||||
stream->write('\n');
|
||||
}
|
||||
|
||||
// Pass 7: functions getter
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef ROSTER
|
||||
#define ROSTER(cabid,name,funcmap...) case cabid: return F("" funcmap);
|
||||
const FSH * RMFT2::getRosterFunctions(int16_t cabid) {
|
||||
switch(cabid) {
|
||||
#include "myAutomation.h"
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 8 Signal definitions
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef SIGNAL
|
||||
#define SIGNAL(redpin,amberpin,greenpin) redpin,amberpin,greenpin,
|
||||
const FLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||
#include "myAutomation.h"
|
||||
0,0,0 };
|
||||
|
||||
// Last Pass : create main routes table
|
||||
// Only undef the macros, not dummy them.
|
||||
#define RMFT2_UNDEF_ONLY
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
// Define internal helper macros.
|
||||
// Everything we generate here has to be compile-time evaluated to
|
||||
// a constant.
|
||||
#define V(val) (byte)(((int16_t)(val))&0x00FF),(byte)(((int16_t)(val)>>8)&0x00FF)
|
||||
// Define macros for route code creation
|
||||
|
||||
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
|
||||
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
|
||||
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
|
||||
const int StringMacroTracker1=__COUNTER__;
|
||||
#define ALIAS(name,value)
|
||||
#define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) {
|
||||
#define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }}
|
||||
#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break;
|
||||
#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break;
|
||||
#define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break;
|
||||
#define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break;
|
||||
#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break;
|
||||
#define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break;
|
||||
#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break;
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Setup for Pass 3: create main routes table
|
||||
#undef AFTER
|
||||
#undef AMBER
|
||||
#undef AT
|
||||
#undef AUTOMATION
|
||||
#undef CALL
|
||||
#undef CLOSE
|
||||
#undef DELAY
|
||||
#undef DELAYMINS
|
||||
#undef DELAYRANDOM
|
||||
#undef DONE
|
||||
#undef ENDIF
|
||||
#undef ENDEXRAIL
|
||||
#undef ENDTASK
|
||||
#undef ESTOP
|
||||
#undef EXRAIL
|
||||
#undef FOFF
|
||||
#undef FOLLOW
|
||||
#undef FON
|
||||
#undef FREE
|
||||
#undef FWD
|
||||
#undef GREEN
|
||||
#undef IF
|
||||
#undef IFNOT
|
||||
#undef IFRANDOM
|
||||
#undef IFRESERVE
|
||||
#undef INVERT_DIRECTION
|
||||
#undef JOIN
|
||||
#undef LATCH
|
||||
#undef LCD
|
||||
#undef LCN
|
||||
#undef ONCLOSE
|
||||
#undef ONTHROW
|
||||
#undef PAUSE
|
||||
#undef POM
|
||||
#undef POWEROFF
|
||||
#undef PRINT
|
||||
#undef READ_LOCO
|
||||
#undef RED
|
||||
#undef RESERVE
|
||||
#undef RESET
|
||||
#undef RESUME
|
||||
#undef RETURN
|
||||
#undef REV
|
||||
#undef ROUTE
|
||||
#undef START
|
||||
#undef SEQUENCE
|
||||
#undef SERVO
|
||||
#undef SERVO2
|
||||
#undef FADE
|
||||
#undef SENDLOCO
|
||||
#undef SERIAL
|
||||
#undef SERIAL1
|
||||
#undef SERIAL2
|
||||
#undef SERIAL3
|
||||
#undef SETLOCO
|
||||
#undef SET
|
||||
#undef SPEED
|
||||
#undef STOP
|
||||
#undef SIGNAL
|
||||
#undef SERVO_TURNOUT
|
||||
#undef PIN_TURNOUT
|
||||
#undef THROW
|
||||
#undef TURNOUT
|
||||
#undef UNJOIN
|
||||
#undef UNLATCH
|
||||
#undef WAITFOR
|
||||
#undef XFOFF
|
||||
#undef XFON
|
||||
|
||||
// Define macros for route code creation
|
||||
#define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF
|
||||
#define NOOPERAND 0,0
|
||||
|
||||
#define ALIAS(name,value)
|
||||
#define EXRAIL const FLASH byte RMFT2::RouteCode[] = {
|
||||
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
|
||||
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
|
||||
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
|
||||
#define ENDTASK OPCODE_ENDTASK,NOOPERAND,
|
||||
#define DONE OPCODE_ENDTASK,NOOPERAND,
|
||||
#define ENDEXRAIL OPCODE_ENDTASK,NOOPERAND,OPCODE_ENDEXRAIL,NOOPERAND };
|
||||
|
||||
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
|
||||
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
|
||||
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
|
||||
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
|
||||
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
|
||||
#define AUTOSTART OPCODE_AUTOSTART,0,0,
|
||||
#define BROADCAST(msg) PRINT(msg)
|
||||
#define CALL(route) OPCODE_CALL,V(route),
|
||||
#define CLOSE(id) OPCODE_CLOSE,V(id),
|
||||
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
|
||||
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
|
||||
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
|
||||
#define DELAY(ms) OPCODE_DELAY,V(ms/100L),
|
||||
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
|
||||
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
|
||||
#define DONE OPCODE_ENDTASK,0,0,
|
||||
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
|
||||
#define ELSE OPCODE_ELSE,0,0,
|
||||
#define ENDEXRAIL
|
||||
#define ENDIF OPCODE_ENDIF,0,0,
|
||||
#define ENDTASK OPCODE_ENDTASK,0,0,
|
||||
#define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100L),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
|
||||
#define ENDIF OPCODE_ENDIF,NOOPERAND,
|
||||
#define ESTOP OPCODE_SPEED,V(1),
|
||||
#define EXRAIL
|
||||
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
|
||||
#define FOFF(func) OPCODE_FOFF,V(func),
|
||||
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
|
||||
@@ -196,68 +255,53 @@ const FLASH int16_t RMFT2::SignalDefinitions[] = {
|
||||
#define FWD(speed) OPCODE_FWD,V(speed),
|
||||
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
|
||||
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
|
||||
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
|
||||
#define IFGTE(sensor_id,value) OPCODE_IFGTE,V(sensor_id),OPCODE_PAD,V(value),
|
||||
#define IFLT(sensor_id,value) OPCODE_IFLT,V(sensor_id),OPCODE_PAD,V(value),
|
||||
#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),
|
||||
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
|
||||
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
|
||||
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
|
||||
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
|
||||
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
|
||||
#define JOIN OPCODE_JOIN,0,0,
|
||||
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,NOOPERAND,
|
||||
#define JOIN OPCODE_JOIN,NOOPERAND,
|
||||
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
|
||||
#define LCD(id,msg) PRINT(msg)
|
||||
#define LCN(msg) PRINT(msg)
|
||||
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
|
||||
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
|
||||
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
|
||||
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
|
||||
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
|
||||
#define PAUSE OPCODE_PAUSE,0,0,
|
||||
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
|
||||
#define PAUSE OPCODE_PAUSE,NOOPERAND,
|
||||
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
|
||||
#define POWEROFF OPCODE_POWEROFF,0,0,
|
||||
#define POWERON OPCODE_POWERON,0,0,
|
||||
#define POWEROFF OPCODE_POWEROFF,NOOPERAND,
|
||||
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
|
||||
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
|
||||
#define READ_LOCO OPCODE_READ_LOCO1,NOOPERAND,OPCODE_READ_LOCO2,NOOPERAND,
|
||||
#define RED(signal_id) OPCODE_RED,V(signal_id),
|
||||
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
|
||||
#define RESET(pin) OPCODE_RESET,V(pin),
|
||||
#define RESUME OPCODE_RESUME,0,0,
|
||||
#define RETURN OPCODE_RETURN,0,0,
|
||||
#define RESUME OPCODE_RESUME,NOOPERAND,
|
||||
#define RETURN OPCODE_RETURN,NOOPERAND,
|
||||
#define REV(speed) OPCODE_REV,V(speed),
|
||||
#define ROSTER(cabid,name,funcmap...)
|
||||
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
|
||||
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
|
||||
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
|
||||
#define SERIAL(msg) PRINT(msg)
|
||||
#define SERIAL1(msg) PRINT(msg)
|
||||
#define SERIAL2(msg) PRINT(msg)
|
||||
#define SERIAL3(msg) PRINT(msg)
|
||||
#define START(route) OPCODE_START,V(route),
|
||||
#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0),
|
||||
#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
|
||||
#define SET(pin) OPCODE_SET,V(pin),
|
||||
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
|
||||
#define SIGNAL(redpin,amberpin,greenpin)
|
||||
#define SET(pin) OPCODE_SET,V(pin),
|
||||
#define SPEED(speed) OPCODE_SPEED,V(speed),
|
||||
#define START(route) OPCODE_START,V(route),
|
||||
#define STOP OPCODE_SPEED,V(0),
|
||||
#define SIGNAL(redpin,amberpin,greenpin) OPCODE_SIGNAL,V(redpin),OPCODE_PAD,V(amberpin),OPCODE_PAD,V(greenpin),
|
||||
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
|
||||
#define PIN_TURNOUT(id,pin) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
|
||||
#define THROW(id) OPCODE_THROW,V(id),
|
||||
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
|
||||
#define UNJOIN OPCODE_UNJOIN,0,0,
|
||||
#define TURNOUT(id,addr,subaddr) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
|
||||
#define UNJOIN OPCODE_UNJOIN,NOOPERAND,
|
||||
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
|
||||
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
|
||||
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
|
||||
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
|
||||
|
||||
// Build RouteCode
|
||||
// PASS2 Build RouteCode
|
||||
const int StringMacroTracker2=__COUNTER__;
|
||||
const FLASH byte RMFT2::RouteCode[] = {
|
||||
#include "myAutomation.h"
|
||||
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Restore normal code LCD & SERIAL macro
|
||||
#undef LCD
|
@@ -1,284 +0,0 @@
|
||||
Version 4.0 Release Notes
|
||||
*************************
|
||||
|
||||
The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production Release. Release v4.0.0 is a Major release that adds significant new product design, plus Automation features and bug fixes. The team continues improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors. This release includes all of the Point Releases from v3.2.0 to v3.2.0 rc13.
|
||||
|
||||
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
|
||||
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)
|
||||
|
||||
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v0.0.0-Prod/CommandStation-EX.tar.gz)
|
||||
|
||||
**Known Issues**
|
||||
|
||||
- **Wi-Fi** - Requires sending `<AT>` commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup
|
||||
- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitations in its current sensing circuitry
|
||||
|
||||
**All New Major DCC++EX 4.0.0 features**
|
||||
|
||||
- **New HAL Hardware Abstraction Layer API** that automatically detects and greatly simplifies interfacing to many predefined accessory boards for servos, signals & sensors and added I/O (digital and analog inputs and outputs, servos etc).
|
||||
- HAL Support for;
|
||||
- MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.
|
||||
- PCA9685 PWM (servo & signal) control modules.
|
||||
- Analogue inputs on Arduino pins and on ADS111x I2C modules.
|
||||
- MP3 sound playback via DFPlayer module.
|
||||
- HC-SR04 Ultrasonic range sensor module.
|
||||
- VL53L0X Laser range sensor module (Time-Of-Flight).
|
||||
- A new `<D HAL SHOW>` command to list the HAL devices attached to the command station
|
||||
|
||||
**New Command Station Broadcast throttle logic**
|
||||
|
||||
- Synchronizes multiple WiThrottles and PC based JMRI Throttles for direction, speed and F-key updates
|
||||
|
||||
**New ‘Discovered Servers’ on WiFi Throttles**
|
||||
|
||||
- Our New multicast Dynamic Network Server (mDNS) enhancement allows us to display the available WiFi server connections to a DCC++EX Command Station. Selecting it allows your WiThrottle App to connect to and load Server Rosters and function keys to your throttle from the new DCC++EX Command Station Server Roster.
|
||||
|
||||
**New DCC++EX 4.0.0 with EX-RAIL Extended Railroad Automation Instruction Language**
|
||||
|
||||
- Use to control your entire layout or as a separate accessory/animation controller
|
||||
- Awesome, cleverly powerful yet simple user friendly scripting language for user built Automation & Routing scripts.
|
||||
- You can control Engines, Sensors, Turnouts, Signals, Outputs and Accessories that are entered into your new myAutomation.h file, then uploaded into the DCC++EX Command Station.
|
||||
- EX-RAIL scripts are automatically displayed as Automation {Handoff} and Route {Set} buttons on supported WiFi Throttle Apps.
|
||||
|
||||
**New EX-RAIL ‘Roster’ Feature**
|
||||
|
||||
- List and store user defined engine roster & function keys inside the command station, and automatically load them in WiFi Throttle Apps.
|
||||
- When choosing “DCC++EX” from discovered servers an Engine Driver or WiThrottle is directly connected to the Command Station.
|
||||
- The EX-RAIL ’ROSTER’ command allows all the engine numbers, names and function keys you’ve listed in your myAutomation.h file to automatically upload the Command Station's ‘Server Roster’ into your Engine Driver and WiThrottle Apps.
|
||||
|
||||
**New JMRI 4.99.2 and above specific DCC++EX 4.0 features**
|
||||
|
||||
- Enhanced JMRI DCC++ Configure Base Station pane for building and maintaining Sensor, Turnout and Output devices, or these can automatically be populated from the DCC++EX Command Station's mySetup.h file into JMRI.
|
||||
|
||||
- JMRI now supports multiple serial connected DCC++EX Command Stations, to display and track separate "Send DCC++ Command" and "DCC++ Traffic" Monitors for each Command Station at the same time.
|
||||
For example: Use an Uno DCC++EX DecoderPro Programming Station {DCC++Prg} on a desktop programming track and a second Mega DCC++EX EX-RAIL Command Station for Operations {DCC++Ops} on the layout with an additional `<JOINED>` programming spur or siding track for acquiring an engine and ‘Drive Away’ onto the mainline (see the DriveAway feature for more information).
|
||||
|
||||
**DCC++EX 4.0.0 additional product enhancements**
|
||||
|
||||
- Additional Motor Shields and Motor Board {boosters) supported
|
||||
- Additional Accessory boards supported for GPIO expansion, Sensors, Servos & Signals
|
||||
- Additional diagnostic commands like ‘D ACK RETRY’ and ‘D EXRAIL ON’ events, ‘D HAL SHOW’ devices and ‘D SERVO’ positions, and ‘D RESET’ the command station while maintaining the serial connection with JMRI
|
||||
- Automatic retry on failed ACK detection to give decoders another chance
|
||||
- New EX-RAIL ’/’ slash command allows JMRI to directly communicate with many EX-RAIL scripts
|
||||
- Turnout class revised to expand turnout capabilities and allow turnout names/descriptors to display in WiThrottle Apps.
|
||||
- Build turnouts through either or both mySetup.h and myAutomation.h files, and have them automatically passed to, and populate, JMRI Turnout Tables
|
||||
- Turnout user names display in Engine Driver & WiThrottles
|
||||
- Output class now allows ID > 255.
|
||||
- Configuration options to globally flip polarity of DCC Accessory states when driven from `<a>` command and `<T>` command.
|
||||
- Increased use of display for showing loco decoder programming information.
|
||||
- Can disable EEPROM memory code to allow room for DCC++EX 4.0 to fit on a Uno Command Station
|
||||
- Can define border between long and short addresses
|
||||
- Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms).
|
||||
- EEPROM layout change - deletes EEPROM contents on first start following upgrade.
|
||||
|
||||
**4.0.0 Bug Fixes**
|
||||
|
||||
- Compiles on Nano Every
|
||||
- Diagnostic display of ack pulses >32ms
|
||||
- Current read from wrong ADC during interrupt
|
||||
- AT(+) Command Pass Through
|
||||
- CiDAP WiFi Drop out and the WiThrottle F-key looping error corrected
|
||||
- One-off error in CIPSEND drop
|
||||
- Common Fault Pin Error
|
||||
- Uno Memory Utilization optimized
|
||||
|
||||
#### Summary of Release 3.1.0 key features and/or bug fixes by Point Release
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.16**
|
||||
|
||||
- Ignore CV1 bit 7 read if rejected by a non NMRA compliant decoder when identifying loco id
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.15**
|
||||
|
||||
- Send function commands just once instead of repeating them 4 times
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.14**
|
||||
|
||||
- Add feature to tolerate decoders that incorrectly have gaps in their ACK pulse
|
||||
- Provide proper track power management when joining and unjoining tracks with <1 JOIN>
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.13**
|
||||
|
||||
- Fix for CAB Functions greater than 127
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.12**
|
||||
|
||||
- Fixed clear screen issue for nanoEvery and nanoWifi
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.11**
|
||||
|
||||
- Reorganized files for support of 128 speed steps
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.10**
|
||||
|
||||
- Added Support for the Teensy 3.2, 3.5, 3.6, 4.0 and 4.1 MCUs
|
||||
- No functional change just changes to avoid complier warnings for Teensy/nanoEvery
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.9**
|
||||
|
||||
- Rearranges serial newlines for the benefit of JMRI
|
||||
- Major update for efficiencies in displays (LCD, OLED)
|
||||
- Add I2C Support functions
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.8**
|
||||
|
||||
- Wraps <* *> around DIAGS for the benefit of JMRI
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.7**
|
||||
|
||||
- Implemented support for older 28 apeed step decoders - Option to turn on 28 step speed decoders in addition to 128. If set, all locos will use 28 steps.
|
||||
- Improved overload messages with raw values (relative to offset)
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.6**
|
||||
|
||||
- Prevent compiler warning about deprecated B constants
|
||||
- Fix Bug that did not let us transmit 5 byte sized packets - 5 Byte commands like PoM (programming on main) were not being sent correctly
|
||||
- Support for Huge function numbers (DCC BinaryStateControl) - Support Functions beyond F28
|
||||
- <!> ESTOP all - New command to emergency stop all locos on the main track
|
||||
- <- [cab]> estop and forget cab/all cabs - Stop and remove loco from the CS. Stops the repeating throttle messages
|
||||
- `<D RESET>` command to reboot Arduino
|
||||
- Automatic sensor offset detect
|
||||
- Improved startup msgs from Motor Drivers (accuracy and auto sense factors)
|
||||
- Drop post-write verify - No need to double check CV writes. Writes are now even faster.
|
||||
- Allow current sense pin set to UNUSED_PIN - No need to ground an unused analog current pin. Produce startup warning and callback -2 for prog track cmds.
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.5**
|
||||
|
||||
- Fix Fn Key startup with loco ID and fix state change for F16-28
|
||||
- Removed ethernet mac config and made it automatic
|
||||
- Show wifi ip and port on lcd
|
||||
- Auto load config.example.h with warning
|
||||
- Dropped example .ino files
|
||||
- Corrected .ino comments
|
||||
- Add Pololu fault pin handling
|
||||
- Waveform speed/simplicity improvements
|
||||
- Improved pin speed in waveform
|
||||
- Portability to nanoEvery and UnoWifiRev2 CPUs
|
||||
- Analog read speed improvements
|
||||
- Drop need for DIO2 library
|
||||
- Improved current check code
|
||||
- Linear command
|
||||
- Removed need for ArduinoTimers files
|
||||
- Removed option to choose different timer
|
||||
- Added EX-RAIL hooks for automation in future version
|
||||
- Fixed Turnout list
|
||||
- Allow command keywords in mixed case
|
||||
- Dropped unused memstream
|
||||
- PWM pin accuracy if requirements met
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.4**
|
||||
|
||||
- "Drive-Away" Feature - added so that throttles like Engine Driver can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
|
||||
- WiFi Startup Fixes
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.3**
|
||||
|
||||
- Command to write loco address and clear consist
|
||||
- Command will allow for consist address
|
||||
- Startup commands implemented
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.2:**
|
||||
|
||||
- Create new output for current in mA for `<c>` command - New current response outputs current in mA, overlimit current, and maximum board capable current
|
||||
- Simultaneously update JMRI to handle new current meter
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.1:**
|
||||
|
||||
- Add back fix for jitter
|
||||
- Add Turnouts, Outputs and Sensors to `<s>` command output
|
||||
|
||||
**CommandStation-EX V3.0.0:**
|
||||
|
||||
**Release v3.0.0 was a major rewrite if earlier versions of DCC++. The code base was re-architeced and core changes were made to the Waveform generator to reduce overhead and make better use of Arduino.** **Summary of the key new features added in Release v3.0.0 include:**
|
||||
|
||||
- **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.
|
||||
- **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.
|
||||
- **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently.
|
||||
- **Add LCD/OLED support** - OLED supported on Mega only
|
||||
- **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.
|
||||
- **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers
|
||||
- **Individual track power control** - Ability to toggle power on either or both tracks, and to "JOIN" the tracks and make them output the same waveform for multiple power districts.
|
||||
- **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs
|
||||
- **New, simpler function command** - `<F>` command allows setting functions based on their number, not based on a code as in `<f>`
|
||||
- **Function reminders** - Function reminders are sent in addition to speed reminders
|
||||
- **Functions to F28** - All NMRA functions are now supported
|
||||
- **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)
|
||||
- **Diagnostic `<D>` commands** - See documentation for a full list of new diagnostic commands
|
||||
- **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM
|
||||
- **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers
|
||||
- **Rewritten packet generator** - Simplify and make smaller, remove idea of "registers" from original code
|
||||
- **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM
|
||||
- **Fix EEPROM bugs**
|
||||
- **Number of locos discovery command** - `<#>` command
|
||||
- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.
|
||||
- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.
|
||||
|
||||
**Key Contributors**
|
||||
|
||||
**Project Lead**
|
||||
|
||||
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
|
||||
|
||||
**CommandStation-EX Developers**
|
||||
|
||||
- Chris Harlow - Bournemouth, UK (UKBloke)
|
||||
- Harald Barth - Stockholm, Sweden (Haba)
|
||||
- Neil McKechnie - Worcestershire, UK (NeilMck)
|
||||
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
|
||||
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
|
||||
- M Steve Todd - Oregon, USA (MSteveTodd)
|
||||
- Scott Catalano - Pennsylvania
|
||||
- Gregor Baues - Île-de-France, France (grbba)
|
||||
|
||||
**Engine Driver and JMRI Interface**
|
||||
|
||||
- M Steve Todd
|
||||
|
||||
**exInstaller Software**
|
||||
|
||||
- Anthony W - Dayton, Ohio, USA (Dex, Dex++)
|
||||
|
||||
**Website and Documentation**
|
||||
|
||||
- Mani Kumar - Bangalor, India (Mani / Mani Kumar)
|
||||
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
|
||||
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
|
||||
- Roger Beschizza - Dorset, UK (Roger Beschizza)
|
||||
- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)
|
||||
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
|
||||
- Colin Grabham - Central NSW, Australia (Kebbin)
|
||||
|
||||
**WebThrotle-EX**
|
||||
|
||||
- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)
|
||||
- Mani Kumar - Bangalor, India (Mani /Mani Kumar)
|
||||
- Matt H - Somewhere in Europe
|
||||
|
||||
**Beta Testing / Release Management / Support**
|
||||
|
||||
- Larry Dribin - Release Management
|
||||
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
|
||||
- Herb Morton - Kingwood Texas, USA (Ash++)
|
||||
- Keith Ledbetter
|
||||
- Brad Van der Elst
|
||||
- Andrew Pye
|
||||
- Mike Bowers
|
||||
- Randy McKenzie
|
||||
- Roberto Bravin
|
||||
- Sam Brigden
|
||||
- Alan Lautenslager
|
||||
- Martin Bafver
|
||||
- Mário André Silva
|
||||
- Anthony Kochevar
|
||||
- Gajanatha Kobbekaduwe
|
||||
- Sumner Patterson
|
||||
- Paul - Virginia, USA
|
||||
|
||||
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
|
||||
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)
|
||||
|
||||
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.tar.gz)
|
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX CommandStation-EX
|
||||
*
|
||||
@@ -19,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "RingStream.h"
|
||||
#include "defines.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
RingStream::RingStream( const uint16_t len)
|
||||
@@ -31,27 +31,36 @@ RingStream::RingStream( const uint16_t len)
|
||||
_overflow=false;
|
||||
_mark=0;
|
||||
_count=0;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
_bufMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t RingStream::write(uint8_t b) {
|
||||
if (_overflow) return 0;
|
||||
portENTER_CRITICAL(&_bufMux);
|
||||
_buffer[_pos_write] = b;
|
||||
++_pos_write;
|
||||
if (_pos_write==_len) _pos_write=0;
|
||||
if (_pos_write==_pos_read) {
|
||||
_overflow=true;
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
return 0;
|
||||
}
|
||||
_count++;
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int RingStream::read() {
|
||||
if ((_pos_read==_pos_write) && !_overflow) return -1; // empty
|
||||
int RingStream::read(byte advance) {
|
||||
if ((_pos_read==_pos_write) && !_overflow) return -1; // empty
|
||||
if (_pos_read == _mark) return -1;
|
||||
portENTER_CRITICAL(&_bufMux);
|
||||
byte b=_buffer[_pos_read];
|
||||
_pos_read++;
|
||||
_pos_read += advance;
|
||||
if (_pos_read==_len) _pos_read=0;
|
||||
_overflow=false;
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
return b;
|
||||
}
|
||||
|
||||
@@ -69,11 +78,14 @@ int RingStream::freeSpace() {
|
||||
|
||||
// mark start of message with client id (0...9)
|
||||
void RingStream::mark(uint8_t b) {
|
||||
//DIAG(F("Mark1 len=%d count=%d pr=%d pw=%d m=%d"),_len, _count,_pos_read,_pos_write,_mark);
|
||||
portENTER_CRITICAL(&_bufMux);
|
||||
_mark=_pos_write;
|
||||
write(b); // client id
|
||||
write((uint8_t)0); // count MSB placemarker
|
||||
write((uint8_t)0); // count LSB placemarker
|
||||
_count=0;
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
}
|
||||
|
||||
// peekTargetMark is used by the parser stash routines to know which client
|
||||
@@ -82,17 +94,25 @@ uint8_t RingStream::peekTargetMark() {
|
||||
return _buffer[_mark];
|
||||
}
|
||||
|
||||
void RingStream::info() {
|
||||
DIAG(F("Info len=%d count=%d pr=%d pw=%d m=%d"),_len, _count,_pos_read,_pos_write,_mark);
|
||||
}
|
||||
|
||||
bool RingStream::commit() {
|
||||
//DIAG(F("Commit1 len=%d count=%d pr=%d pw=%d m=%d"),_len, _count,_pos_read,_pos_write,_mark);
|
||||
portENTER_CRITICAL(&_bufMux);
|
||||
if (_overflow) {
|
||||
DIAG(F("RingStream(%d) commit(%d) OVERFLOW"),_len, _count);
|
||||
// just throw it away
|
||||
_pos_write=_mark;
|
||||
_overflow=false;
|
||||
return false; // commit failed
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
return false; // commit failed
|
||||
}
|
||||
if (_count==0) {
|
||||
// ignore empty response
|
||||
_pos_write=_mark;
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
return true; // true=commit ok
|
||||
}
|
||||
// Go back to the _mark and inject the count 1 byte later
|
||||
@@ -102,14 +122,8 @@ bool RingStream::commit() {
|
||||
_mark++;
|
||||
if (_mark==_len) _mark=0;
|
||||
_buffer[_mark]=lowByte(_count);
|
||||
_mark=_len+1;
|
||||
//DIAG(F("Commit2 len=%d count=%d pr=%d pw=%d m=%d"),_len, _count,_pos_read,_pos_write,_mark);
|
||||
portEXIT_CRITICAL(&_bufMux);
|
||||
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);
|
||||
}
|
||||
|
14
RingStream.h
14
RingStream.h
@@ -1,8 +1,7 @@
|
||||
#ifndef RingStream_h
|
||||
#define RingStream_h
|
||||
/*
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX CommandStation-EX
|
||||
*
|
||||
@@ -29,15 +28,17 @@ class RingStream : public Print {
|
||||
|
||||
virtual size_t write(uint8_t b);
|
||||
using Print::write;
|
||||
int read();
|
||||
inline int read() { return read(1); };
|
||||
inline int peek() { return read(0); };
|
||||
int count();
|
||||
int freeSpace();
|
||||
void mark(uint8_t b);
|
||||
bool commit();
|
||||
uint8_t peekTargetMark();
|
||||
void printBuffer(Print * streamer);
|
||||
void flush();
|
||||
void info();
|
||||
|
||||
private:
|
||||
int read(byte advance);
|
||||
int _len;
|
||||
int _pos_write;
|
||||
int _pos_read;
|
||||
@@ -45,6 +46,9 @@ class RingStream : public Print {
|
||||
int _mark;
|
||||
int _count;
|
||||
byte * _buffer;
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
portMUX_TYPE _bufMux;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
||||
|
170
SSD1306Ascii.cpp
170
SSD1306Ascii.cpp
@@ -89,93 +89,28 @@ const uint8_t FLASH SSD1306AsciiWire::blankPixels[30] =
|
||||
{0x40, // First byte specifies data mode
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
|
||||
|
||||
/** Initialization commands for a 128x32 or 128x64 SSD1306 oled display. */
|
||||
const uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {
|
||||
// Init sequence for Adafruit 128x32/64 OLED module
|
||||
0x00, // Set to command mode
|
||||
SSD1306_DISPLAYOFF,
|
||||
SSD1306_SETDISPLAYCLOCKDIV, 0x80, // the suggested ratio 0x80
|
||||
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64 (initially)
|
||||
SSD1306_SETDISPLAYOFFSET, 0x0, // no offset
|
||||
SSD1306_SETSTARTLINE | 0x0, // line #0
|
||||
SSD1306_CHARGEPUMP, 0x14, // internal vcc
|
||||
SSD1306_MEMORYMODE, 0x02, // page mode
|
||||
SSD1306_SEGREMAP | 0x1, // column 127 mapped to SEG0
|
||||
SSD1306_COMSCANDEC, // column scan direction reversed
|
||||
SSD1306_SETCOMPINS, 0X12, // set COM pins
|
||||
SSD1306_SETCONTRAST, 0x7F, // contrast level 127
|
||||
SSD1306_SETPRECHARGE, 0xF1, // pre-charge period (1, 15)
|
||||
SSD1306_SETVCOMDETECT, 0x40, // vcomh regulator level
|
||||
SSD1306_DISPLAYALLON_RESUME,
|
||||
SSD1306_NORMALDISPLAY,
|
||||
SSD1306_DISPLAYON
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// This section is based on https://github.com/stanleyhuangyc/MultiLCD
|
||||
|
||||
/** Initialization commands for a 128x64 SH1106 oled display. */
|
||||
const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
|
||||
0x00, // Set to command mode
|
||||
SSD1306_DISPLAYOFF,
|
||||
SSD1306_SETDISPLAYCLOCKDIV, 0X80, // set osc division
|
||||
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64
|
||||
SSD1306_SETDISPLAYOFFSET, 0X00, // set display offset
|
||||
SSD1306_SETSTARTPAGE | 0X0, // set page address
|
||||
SSD1306_SETSTARTLINE | 0x0, // set start line
|
||||
SH1106_SET_PUMP_MODE, SH1106_PUMP_ON, // set charge pump enable
|
||||
SSD1306_SEGREMAP | 0X1, // set segment remap
|
||||
SSD1306_COMSCANDEC, // Com scan direction
|
||||
SSD1306_SETCOMPINS, 0X12, // set COM pins
|
||||
SSD1306_SETCONTRAST, 0x80, // 128
|
||||
SSD1306_SETPRECHARGE, 0X1F, // set pre-charge period
|
||||
SSD1306_SETVCOMDETECT, 0x40, // set vcomh
|
||||
SH1106_SET_PUMP_VOLTAGE | 0X2, // 8.0 volts
|
||||
SSD1306_NORMALDISPLAY, // normal / reverse
|
||||
SSD1306_DISPLAYON
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SSD1306AsciiWire Method Definitions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Constructor
|
||||
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
|
||||
m_displayWidth = width;
|
||||
m_displayHeight = height;
|
||||
// Set size in characters in base class
|
||||
lcdRows = height / 8;
|
||||
lcdCols = width / 6;
|
||||
m_col = 0;
|
||||
m_row = 0;
|
||||
m_colOffset = 0;
|
||||
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(400000L); // Set max supported I2C speed
|
||||
for (byte address = 0x3c; address <= 0x3d; address++) {
|
||||
if (I2CManager.exists(address)) {
|
||||
m_i2cAddr = address;
|
||||
if (m_displayWidth==132 && m_displayHeight==64) {
|
||||
// SH1106 display. This uses 128x64 centered within a 132x64 OLED.
|
||||
m_colOffset = 2;
|
||||
I2CManager.write_P(address, SH1106_132x64init, sizeof(SH1106_132x64init));
|
||||
} else if (m_displayWidth==128 && (m_displayHeight==64 || m_displayHeight==32)) {
|
||||
// SSD1306 128x64 or 128x32
|
||||
I2CManager.write_P(address, Adafruit128xXXinit, sizeof(Adafruit128xXXinit));
|
||||
if (m_displayHeight == 32)
|
||||
I2CManager.write(address, 5, 0, // Set command mode
|
||||
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
|
||||
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
|
||||
} else {
|
||||
DIAG(F("OLED configuration option not recognised"));
|
||||
return;
|
||||
}
|
||||
// Device found
|
||||
DIAG(F("%dx%d OLED display configured on I2C:x%x"), width, height, address);
|
||||
if (width == 132)
|
||||
begin(&SH1106_132x64, address);
|
||||
else if (height == 32)
|
||||
begin(&Adafruit128x32, address);
|
||||
else
|
||||
begin(&Adafruit128x64, address);
|
||||
// Set singleton address
|
||||
lcdDisplay = this;
|
||||
clear();
|
||||
@@ -197,6 +132,23 @@ void SSD1306AsciiWire::clearNative() {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise device
|
||||
void SSD1306AsciiWire::begin(const DevType* dev, uint8_t i2cAddr) {
|
||||
m_i2cAddr = i2cAddr;
|
||||
m_col = 0;
|
||||
m_row = 0;
|
||||
const uint8_t* table = (const uint8_t*)GETFLASHP(&dev->initcmds);
|
||||
uint8_t size = GETFLASH(&dev->initSize);
|
||||
m_displayWidth = GETFLASH(&dev->lcdWidth);
|
||||
m_displayHeight = GETFLASH(&dev->lcdHeight);
|
||||
m_colOffset = GETFLASH(&dev->colOffset);
|
||||
I2CManager.write_P(m_i2cAddr, table, size);
|
||||
if (m_displayHeight == 32)
|
||||
I2CManager.write(m_i2cAddr, 5, 0, // Set command mode
|
||||
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
|
||||
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Set cursor position (by text line)
|
||||
@@ -257,6 +209,82 @@ size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
|
||||
|
||||
/** Initialization commands for a 128x32 or 128x64 SSD1306 oled display. */
|
||||
const uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {
|
||||
// Init sequence for Adafruit 128x32/64 OLED module
|
||||
0x00, // Set to command mode
|
||||
SSD1306_DISPLAYOFF,
|
||||
SSD1306_SETDISPLAYCLOCKDIV, 0x80, // the suggested ratio 0x80
|
||||
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64 (initially)
|
||||
SSD1306_SETDISPLAYOFFSET, 0x0, // no offset
|
||||
SSD1306_SETSTARTLINE | 0x0, // line #0
|
||||
SSD1306_CHARGEPUMP, 0x14, // internal vcc
|
||||
SSD1306_MEMORYMODE, 0x02, // page mode
|
||||
SSD1306_SEGREMAP | 0x1, // column 127 mapped to SEG0
|
||||
SSD1306_COMSCANDEC, // column scan direction reversed
|
||||
SSD1306_SETCOMPINS, 0X12, // set COM pins
|
||||
SSD1306_SETCONTRAST, 0x7F, // contrast level 127
|
||||
SSD1306_SETPRECHARGE, 0xF1, // pre-charge period (1, 15)
|
||||
SSD1306_SETVCOMDETECT, 0x40, // vcomh regulator level
|
||||
SSD1306_DISPLAYALLON_RESUME,
|
||||
SSD1306_NORMALDISPLAY,
|
||||
SSD1306_DISPLAYON
|
||||
};
|
||||
|
||||
/** Initialize a 128x32 SSD1306 oled display. */
|
||||
const DevType FLASH SSD1306AsciiWire::Adafruit128x32 = {
|
||||
Adafruit128xXXinit,
|
||||
sizeof(Adafruit128xXXinit),
|
||||
128,
|
||||
32,
|
||||
0
|
||||
};
|
||||
|
||||
/** Initialize a 128x64 oled display. */
|
||||
const DevType FLASH SSD1306AsciiWire::Adafruit128x64 = {
|
||||
Adafruit128xXXinit,
|
||||
sizeof(Adafruit128xXXinit),
|
||||
128,
|
||||
64,
|
||||
0
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
// This section is based on https://github.com/stanleyhuangyc/MultiLCD
|
||||
|
||||
/** Initialization commands for a 128x64 SH1106 oled display. */
|
||||
const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
|
||||
0x00, // Set to command mode
|
||||
SSD1306_DISPLAYOFF,
|
||||
SSD1306_SETDISPLAYCLOCKDIV, 0X80, // set osc division
|
||||
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64
|
||||
SSD1306_SETDISPLAYOFFSET, 0X00, // set display offset
|
||||
SSD1306_SETSTARTPAGE | 0X0, // set page address
|
||||
SSD1306_SETSTARTLINE | 0x0, // set start line
|
||||
SH1106_SET_PUMP_MODE, SH1106_PUMP_ON, // set charge pump enable
|
||||
SSD1306_SEGREMAP | 0X1, // set segment remap
|
||||
SSD1306_COMSCANDEC, // Com scan direction
|
||||
SSD1306_SETCOMPINS, 0X12, // set COM pins
|
||||
SSD1306_SETCONTRAST, 0x80, // 128
|
||||
SSD1306_SETPRECHARGE, 0X1F, // set pre-charge period
|
||||
SSD1306_SETVCOMDETECT, 0x40, // set vcomh
|
||||
SH1106_SET_PUMP_VOLTAGE | 0X2, // 8.0 volts
|
||||
SSD1306_NORMALDISPLAY, // normal / reverse
|
||||
SSD1306_DISPLAYON
|
||||
};
|
||||
|
||||
/** Initialize a 132x64 oled SH1106 display. */
|
||||
const DevType FLASH SSD1306AsciiWire::SH1106_132x64 = {
|
||||
SH1106_132x64init,
|
||||
sizeof(SH1106_132x64init),
|
||||
128,
|
||||
64,
|
||||
2 // SH1106 is a 132x64 controller but most OLEDs are only attached
|
||||
// to columns 2-129.
|
||||
};
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
@@ -32,6 +32,21 @@
|
||||
//#define NOLOWERCASE
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Device initialization structure.
|
||||
|
||||
struct DevType {
|
||||
/* Pointer to initialization command bytes. */
|
||||
const uint8_t* initcmds;
|
||||
/* Number of initialization bytes */
|
||||
const uint8_t initSize;
|
||||
/* Width of the display in pixels */
|
||||
const uint8_t lcdWidth;
|
||||
/** Height of the display in pixels. */
|
||||
const uint8_t lcdHeight;
|
||||
/* Column offset RAM to display. Used to pick start column of SH1106. */
|
||||
const uint8_t colOffset;
|
||||
};
|
||||
|
||||
// Constructor
|
||||
class SSD1306AsciiWire : public LCDDisplay {
|
||||
public:
|
||||
@@ -40,17 +55,25 @@ class SSD1306AsciiWire : public LCDDisplay {
|
||||
SSD1306AsciiWire(int width, int height);
|
||||
|
||||
// Initialize the display controller.
|
||||
void begin(uint8_t i2cAddr);
|
||||
void begin(const DevType* dev, uint8_t i2cAddr);
|
||||
|
||||
// Clear the display and set the cursor to (0, 0).
|
||||
void clearNative() override;
|
||||
|
||||
// Set cursor to start of specified text line
|
||||
void setRowNative(byte line) override;
|
||||
|
||||
// Initialize the display controller.
|
||||
void init(const DevType* dev);
|
||||
|
||||
// Write one character to OLED
|
||||
size_t writeNative(uint8_t c) override;
|
||||
|
||||
// Display characteristics / initialisation
|
||||
static const DevType FLASH Adafruit128x32;
|
||||
static const DevType FLASH Adafruit128x64;
|
||||
static const DevType FLASH SH1106_132x64;
|
||||
|
||||
bool isBusy() override { return requestBlock.isBusy(); }
|
||||
|
||||
private:
|
||||
|
25
Sensors.cpp
25
Sensors.cpp
@@ -1,10 +1,8 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021, modified by Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -69,11 +67,8 @@ 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"
|
||||
#endif
|
||||
#include "IODevice.h"
|
||||
|
||||
|
||||
@@ -90,7 +85,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(){
|
||||
void Sensor::checkAll(Print *stream){
|
||||
uint16_t sensorCount = 0;
|
||||
|
||||
#ifdef USE_NOTIFY
|
||||
@@ -138,8 +133,10 @@ void Sensor::checkAll(){
|
||||
readingSensor->active = readingSensor->inputState;
|
||||
readingSensor->latchDelay = minReadCount; // Reset counter
|
||||
|
||||
CommandDistributor::broadcastSensor(readingSensor->data.snum,readingSensor->active);
|
||||
pause = true; // Don't check any more sensors on this entry
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next sensor in list.
|
||||
@@ -278,7 +275,7 @@ bool Sensor::remove(int n){
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef DISABLE_EEPROM
|
||||
|
||||
void Sensor::load(){
|
||||
struct SensorData data;
|
||||
Sensor *tt;
|
||||
@@ -306,7 +303,7 @@ void Sensor::store(){
|
||||
EEStore::eeStore->data.nSensors++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Sensor *Sensor::firstSensor=NULL;
|
||||
@@ -317,4 +314,4 @@ unsigned long Sensor::lastReadCycle=0;
|
||||
Sensor *Sensor::firstPollSensor = NULL;
|
||||
Sensor *Sensor::lastSensor = NULL;
|
||||
bool Sensor::inputChangeCallbackRegistered = false;
|
||||
#endif
|
||||
#endif
|
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -71,14 +68,12 @@ public:
|
||||
Sensor *nextSensor;
|
||||
|
||||
void setState(int state);
|
||||
#ifndef DISABLE_EEPROM
|
||||
static void load();
|
||||
static void store();
|
||||
#endif
|
||||
static Sensor *create(int id, VPIN vpin, int pullUp);
|
||||
static Sensor* get(int id);
|
||||
static bool remove(int id);
|
||||
static void checkAll();
|
||||
static void checkAll(Print *stream);
|
||||
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.
|
||||
|
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* © 2021 Chris Harlow
|
||||
* © 2022 Harald Barth
|
||||
* 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(Stream * myserial) {
|
||||
serial=myserial;
|
||||
next=first;
|
||||
first=this;
|
||||
bufferLength=0;
|
||||
inCommandPayload=false;
|
||||
}
|
||||
|
||||
void SerialManager::init() {
|
||||
while (!Serial && millis() < 5000); // wait max 5s for Serial to start
|
||||
Serial.begin(115200);
|
||||
new SerialManager(&Serial);
|
||||
#ifdef SERIAL3_COMMANDS
|
||||
Serial3.begin(115200);
|
||||
new SerialManager(&Serial3);
|
||||
#endif
|
||||
#ifdef SERIAL2_COMMANDS
|
||||
Serial2.begin(115200);
|
||||
new SerialManager(&Serial2);
|
||||
#endif
|
||||
#ifdef SERIAL1_COMMANDS
|
||||
Serial1.begin(115200);
|
||||
new SerialManager(&Serial1);
|
||||
#endif
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -99,7 +99,6 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
|
||||
case 'E': printEscapes(stream,(const FSH*)va_arg(args, char*)); break;
|
||||
case 'S': stream->print((const FSH*)va_arg(args, char*)); break;
|
||||
case 'd': printPadded(stream,va_arg(args, int), formatWidth, formatLeft); break;
|
||||
case 'u': printPadded(stream,va_arg(args, unsigned int), formatWidth, formatLeft); break;
|
||||
case 'l': printPadded(stream,va_arg(args, long), formatWidth, formatLeft); break;
|
||||
case 'b': stream->print(va_arg(args, int), BIN); break;
|
||||
case 'o': stream->print(va_arg(args, int), OCT); break;
|
||||
|
74
Turnouts.cpp
74
Turnouts.cpp
@@ -1,13 +1,10 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2021 Restructured Neil McKechnie
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -25,12 +22,9 @@
|
||||
|
||||
|
||||
#include "defines.h" // includes config.h
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "EEStore.h"
|
||||
#endif
|
||||
#include "StringFormatter.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "RMFT2.h"
|
||||
#include "Turnouts.h"
|
||||
#include "DCC.h"
|
||||
#include "LCN.h"
|
||||
@@ -74,7 +68,11 @@
|
||||
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) {
|
||||
@@ -114,11 +112,14 @@
|
||||
|
||||
// I know it says setClosedStateOnly, but we need to tell others
|
||||
// that the state has changed too.
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
#if defined(RMFT_ACTIVE)
|
||||
RMFT2::turnoutEvent(id, closeFlag);
|
||||
#endif
|
||||
|
||||
CommandDistributor::broadcastTurnout(id, closeFlag);
|
||||
// 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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,25 +140,25 @@
|
||||
bool ok = tt->setClosedInternal(closeFlag);
|
||||
|
||||
if (ok) {
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
turnoutlistHash++; // let withrottle know something changed
|
||||
|
||||
// Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress
|
||||
// is always zero for LCN turnouts.
|
||||
if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0)
|
||||
EEPROM.put(tt->_eepromAddress, tt->_turnoutData.flags);
|
||||
#endif
|
||||
EEPROM.put(tt->_eepromAddress, tt->_turnoutData.flags);
|
||||
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
#if defined(RMFT_ACTIVE)
|
||||
RMFT2::turnoutEvent(id, closeFlag);
|
||||
#endif
|
||||
|
||||
// Send message to JMRI etc.
|
||||
CommandDistributor::broadcastTurnout(id, closeFlag);
|
||||
// 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);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Load all turnout objects
|
||||
/* static */ void Turnout::load() {
|
||||
for (uint16_t i=0; i<EEStore::eeStore->data.nTurnouts; i++) {
|
||||
@@ -211,7 +212,13 @@
|
||||
#endif
|
||||
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.
|
||||
@@ -270,7 +277,6 @@
|
||||
|
||||
// Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
Turnout *ServoTurnout::load(struct TurnoutData *turnoutData) {
|
||||
#ifndef DISABLE_EEPROM
|
||||
ServoTurnoutData servoTurnoutData;
|
||||
// Read class-specific data from EEPROM
|
||||
EEPROM.get(EEStore::pointer(), servoTurnoutData);
|
||||
@@ -280,10 +286,6 @@
|
||||
Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition,
|
||||
servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed);
|
||||
return tt;
|
||||
#else
|
||||
(void)turnoutData;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed
|
||||
@@ -306,7 +308,6 @@
|
||||
}
|
||||
|
||||
void ServoTurnout::save() {
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Write turnout definition and current position to EEPROM
|
||||
// First write common servo data, then
|
||||
// write the servo-specific data
|
||||
@@ -314,7 +315,6 @@
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _servoTurnoutData);
|
||||
EEStore::advance(sizeof(_servoTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*************************************************************************************
|
||||
@@ -367,7 +367,6 @@
|
||||
|
||||
// Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
/* static */ Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) {
|
||||
#ifndef DISABLE_EEPROM
|
||||
DCCTurnoutData dccTurnoutData;
|
||||
// Read class-specific data from EEPROM
|
||||
EEPROM.get(EEStore::pointer(), dccTurnoutData);
|
||||
@@ -377,10 +376,6 @@
|
||||
DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress);
|
||||
|
||||
return tt;
|
||||
#else
|
||||
(void)turnoutData;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DCCTurnout::print(Print *stream) {
|
||||
@@ -401,7 +396,6 @@
|
||||
}
|
||||
|
||||
void DCCTurnout::save() {
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Write turnout definition and current position to EEPROM
|
||||
// First write common servo data, then
|
||||
// write the servo-specific data
|
||||
@@ -409,7 +403,6 @@
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _dccTurnoutData);
|
||||
EEStore::advance(sizeof(_dccTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -448,7 +441,6 @@
|
||||
|
||||
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
/* static */ Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) {
|
||||
#ifndef DISABLE_EEPROM
|
||||
VpinTurnoutData vpinTurnoutData;
|
||||
// Read class-specific data from EEPROM
|
||||
EEPROM.get(EEStore::pointer(), vpinTurnoutData);
|
||||
@@ -458,10 +450,6 @@
|
||||
VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed);
|
||||
|
||||
return tt;
|
||||
#else
|
||||
(void)turnoutData;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Report 1 for thrown, 0 for closed.
|
||||
@@ -477,7 +465,6 @@
|
||||
}
|
||||
|
||||
void VpinTurnout::save() {
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Write turnout definition and current position to EEPROM
|
||||
// First write common servo data, then
|
||||
// write the servo-specific data
|
||||
@@ -485,7 +472,6 @@
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _vpinTurnoutData);
|
||||
EEStore::advance(sizeof(_vpinTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
19
Turnouts.h
19
Turnouts.h
@@ -1,13 +1,9 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2021 Restructured Neil McKechnie
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -29,7 +25,7 @@
|
||||
//#define EESTOREDEBUG
|
||||
#include "Arduino.h"
|
||||
#include "IODevice.h"
|
||||
#include "StringFormatter.h"
|
||||
|
||||
|
||||
// Turnout type definitions
|
||||
enum {
|
||||
@@ -159,20 +155,19 @@ public:
|
||||
|
||||
inline static Turnout *first() { return _firstTurnout; }
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Load all turnout definitions.
|
||||
static void load();
|
||||
// Load one turnout definition
|
||||
static Turnout *loadTurnout();
|
||||
// Save all turnout definitions
|
||||
static void store();
|
||||
#endif
|
||||
|
||||
static void printAll(Print *stream) {
|
||||
for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout)
|
||||
StringFormatter::send(stream, F("<H %d %d>\n"),tt->getId(), tt->isThrown());
|
||||
tt->printState(stream);
|
||||
}
|
||||
|
||||
|
||||
static void printState(uint16_t id, Print *stream);
|
||||
};
|
||||
|
||||
|
||||
|
616
WiThrottle.cpp
616
WiThrottle.cpp
@@ -1,12 +1,8 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
* This file is part of Asbelos DCC 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
|
||||
@@ -53,8 +49,8 @@
|
||||
#include "DIAG.h"
|
||||
#include "GITHUB_SHA.h"
|
||||
#include "version.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "RMFT2.h"
|
||||
|
||||
|
||||
#define LOOPLOCOS(THROTTLECHAR, CAB) for (int loco=0;loco<MAX_MY_LOCO;loco++) \
|
||||
if ((myLocos[loco].throttle==THROTTLECHAR || '*'==THROTTLECHAR) && (CAB<0 || myLocos[loco].cab==CAB))
|
||||
@@ -115,127 +111,122 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
|
||||
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d)<-[%e]"),millis(),clientid,cmd);
|
||||
|
||||
if (initSent) {
|
||||
// Send power state if different than last sent
|
||||
bool currentPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
|
||||
if (lastPowerState != currentPowerState) {
|
||||
StringFormatter::send(stream,F("PPA%x\n"),currentPowerState);
|
||||
lastPowerState = currentPowerState;
|
||||
}
|
||||
// Send turnout list if changed since last sent (will replace list on client)
|
||||
if (turnoutListHash != Turnout::turnoutlistHash) {
|
||||
StringFormatter::send(stream,F("PTL"));
|
||||
for(Turnout *tt=Turnout::first();tt!=NULL;tt=tt->next()){
|
||||
int id=tt->getId();
|
||||
StringFormatter::send(stream,F("]\\[%d}|{"), id);
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
RMFT2::emitTurnoutDescription(stream,id);
|
||||
#else
|
||||
StringFormatter::send(stream,F("%d"), id);
|
||||
#endif
|
||||
StringFormatter::send(stream,F("}|{%c"), Turnout::isClosed(id)?'2':'4');
|
||||
StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), id, id, Turnout::isClosed(id)?'2':'4');
|
||||
}
|
||||
StringFormatter::send(stream,F("\n"));
|
||||
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
|
||||
}
|
||||
|
||||
else if (!exRailSent) {
|
||||
// Send EX-RAIL routes list if not already sent (but not at same time as turnouts above)
|
||||
// Send ExRail routes list if not already sent (but not at same time as turnouts above)
|
||||
exRailSent=true;
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
#ifdef RMFT_ACTIVE
|
||||
RMFT2::emitWithrottleRouteList(stream);
|
||||
#endif
|
||||
// allow heartbeat to slow down once all metadata sent
|
||||
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
DCCWaveform::mainTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
||||
if (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling
|
||||
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
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
|
||||
if (initSent) {
|
||||
StringFormatter::send(stream, F("*%d\n"),HEARTBEAT_SECONDS); // return timeout value
|
||||
}
|
||||
break;
|
||||
case 'M': // multithrottle
|
||||
multithrottle(stream, cmd);
|
||||
break;
|
||||
case 'H': // send initial connection info after receiving "HU" message
|
||||
if (cmd[1] == 'U') {
|
||||
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"));
|
||||
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
RMFT2::emitWithrottleRoster(stream);
|
||||
#endif
|
||||
// set heartbeat to 1 second because we need to sync the metadata
|
||||
StringFormatter::send(stream,F("*1\n"));
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
int WiThrottle::getInt(byte * cmd) {
|
||||
int i=0;
|
||||
while (cmd[0]>='0' && cmd[0]<='9') {
|
||||
i=i*10 + (cmd[0]-'0');
|
||||
cmd++;
|
||||
}
|
||||
return i;
|
||||
|
||||
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
|
||||
DCCWaveform::mainTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
||||
if (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling
|
||||
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
|
||||
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
|
||||
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
|
||||
}
|
||||
#if defined(RMFT_ACTIVE)
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
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
|
||||
if (initSent) {
|
||||
StringFormatter::send(stream, F("*%d\n"),HEARTBEAT_SECONDS); // return timeout value
|
||||
}
|
||||
break;
|
||||
case 'M': // multithrottle
|
||||
multithrottle(stream, cmd);
|
||||
break;
|
||||
case 'H': // send initial connection info after receiving "HU" message
|
||||
if (cmd[1] == 'U') {
|
||||
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"));
|
||||
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
|
||||
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
|
||||
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
|
||||
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
|
||||
}
|
||||
}
|
||||
int WiThrottle::getInt(byte * cmd) {
|
||||
int i=0;
|
||||
while (cmd[0]>='0' && cmd[0]<='9') {
|
||||
i=i*10 + (cmd[0]-'0');
|
||||
cmd++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
int WiThrottle::getLocoId(byte * cmd) {
|
||||
@@ -245,183 +236,143 @@ int WiThrottle::getLocoId(byte * cmd) {
|
||||
}
|
||||
|
||||
void WiThrottle::multithrottle(RingStream * stream, byte * cmd){
|
||||
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
|
||||
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
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"));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
for(int fKey=0; fKey<fkeys; fKey++) {
|
||||
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;
|
||||
mostRecentCab=locoid;
|
||||
StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid); //tell client to add loco
|
||||
//Get known Fn states from DCC
|
||||
for(int fKey=0; fKey<=28; fKey++) {
|
||||
int fstate=DCC::getFn(locoid,fKey);
|
||||
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),throttleChar,cmd[3],locoid,fstate,fKey);
|
||||
}
|
||||
//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);
|
||||
}
|
||||
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),throttleChar,cmd[3],locoid,fstate,fKey);
|
||||
}
|
||||
StringFormatter::send(stream, F("M%cA%c%d<;>V%d\n"), throttleChar, cmd[3], locoid, DCCToWiTSpeed(DCC::getThrottleSpeed(locoid)));
|
||||
StringFormatter::send(stream, F("M%cA%c%d<;>R%d\n"), throttleChar, cmd[3], locoid, DCC::getThrottleDirection(locoid));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar, int cab){
|
||||
// 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.
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
myLocos[loco].broadcastPending=true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'R':
|
||||
{
|
||||
bool forward=aval[1]!='0';
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
mostRecentCab=myLocos[loco].cab;
|
||||
DCC::setThrottle(myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab), forward);
|
||||
// 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;
|
||||
}
|
||||
// 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);
|
||||
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));
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, witSpeed);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'F': //F onOff function
|
||||
{
|
||||
bool funcstate;
|
||||
bool pressed=aval[1]=='1';
|
||||
int fKey = getInt(aval+2);
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
funcstate = DCC::changeFn(myLocos[loco].cab, fKey, pressed);
|
||||
if(funcstate==0 || funcstate==1)
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"), throttleChar, LorS(myLocos[loco].cab),
|
||||
myLocos[loco].cab, funcstate, fKey);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'q':
|
||||
if (aval[1]=='V') { //qV
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, DCCToWiTSpeed(DCC::getThrottleSpeed(myLocos[loco].cab)));
|
||||
}
|
||||
}
|
||||
else if (aval[1]=='R') { // qR
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, DCC::getThrottleDirection(myLocos[loco].cab));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'R':
|
||||
{
|
||||
bool forward=aval[1]!='0';
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
mostRecentCab=myLocos[loco].cab;
|
||||
DCC::setThrottle(myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab), forward);
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, forward);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'X':
|
||||
//Emergency Stop (speed code 1)
|
||||
LOOPLOCOS(throttleChar, cab) {
|
||||
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, -1);
|
||||
}
|
||||
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));
|
||||
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// convert between DCC++ speed values and WiThrottle speed values
|
||||
// convert between DCC++ speed values and WiThrottle speed values
|
||||
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
|
||||
}
|
||||
|
||||
// convert between WiThrottle speed values and DCC++ speed values
|
||||
// convert between WiThrottle speed values and DCC++ speed values
|
||||
int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
|
||||
if (WiTSpeed == 0) return 0; //stop is stop
|
||||
if (WiTSpeed == -1) return 1; //eStop value
|
||||
@@ -429,17 +380,24 @@ int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
|
||||
}
|
||||
|
||||
void WiThrottle::loop(RingStream * stream) {
|
||||
// for each WiThrottle, check the heartbeat and broadcast needed
|
||||
// for each WiThrottle, check the heartbeat
|
||||
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
||||
wt->checkHeartbeat(stream);
|
||||
|
||||
wt->checkHeartbeat();
|
||||
|
||||
// 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(RingStream * stream) {
|
||||
void WiThrottle::checkHeartbeat() {
|
||||
// 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);
|
||||
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
|
||||
LOOPLOCOS('*', -1) {
|
||||
if (myLocos[loco].throttle!='\0') {
|
||||
if (Diag::WITHROTTLE) DIAG(F("%l eStopping cab %d"),millis(),myLocos[loco].cab);
|
||||
@@ -447,65 +405,15 @@ void WiThrottle::checkHeartbeat(RingStream * stream) {
|
||||
}
|
||||
}
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
bool streamHasBeenMarked=false;
|
||||
LOOPLOCOS('*', -1) {
|
||||
if (myLocos[loco].throttle!='\0' && myLocos[loco].broadcastPending) {
|
||||
if (!streamHasBeenMarked) {
|
||||
stream->mark(clientid);
|
||||
streamHasBeenMarked=true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (streamHasBeenMarked) 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';
|
||||
return (cab<127)?'S':'L';
|
||||
}
|
||||
|
||||
// Drive Away feature. Callback handling
|
||||
|
||||
|
||||
RingStream * WiThrottle::stashStream;
|
||||
WiThrottle * WiThrottle::stashInstance;
|
||||
byte WiThrottle::stashClient;
|
||||
@@ -513,33 +421,13 @@ char WiThrottle::stashThrottleChar;
|
||||
|
||||
void WiThrottle::getLocoCallback(int16_t locoid) {
|
||||
stashStream->mark(stashClient);
|
||||
|
||||
if (locoid<=0) {
|
||||
StringFormatter::send(stashStream,F("HMNo loco found on prog track\n"));
|
||||
stashStream->commit(); // done here, commit and return
|
||||
return;
|
||||
if (locoid<0) StringFormatter::send(stashStream,F("HMNo loco found on prog track\n"));
|
||||
else {
|
||||
char addcmd[20]={'M',stashThrottleChar,'+',LorS(locoid) };
|
||||
itoa(locoid,addcmd+4,10);
|
||||
stashInstance->multithrottle(stashStream, (byte *)addcmd);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
DCC::setProgTrackSyncMain(true); // <1 JOIN> so we can drive loco away
|
||||
}
|
||||
|
||||
// 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);
|
||||
stashStream->commit(); // done here, commit and return
|
||||
return;
|
||||
}
|
||||
addrchar = 'L';
|
||||
} else {
|
||||
addrchar = 'S';
|
||||
}
|
||||
|
||||
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
|
||||
itoa(locoid,addcmd+4,10);
|
||||
stashInstance->multithrottle(stashStream, (byte *)addcmd);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
|
||||
DCC::setProgTrackSyncMain(true); // <1 JOIN> so we can drive loco away
|
||||
stashStream->commit();
|
||||
CommandDistributor::broadcastPower();
|
||||
|
||||
}
|
||||
|
18
WiThrottle.h
18
WiThrottle.h
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* © 2021 Mike S
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@@ -26,9 +24,6 @@
|
||||
struct MYLOCO {
|
||||
char throttle; //indicates which throttle letter on client, often '0','1' or '2'
|
||||
int cab; //address of this loco
|
||||
bool broadcastPending;
|
||||
uint32_t functionMap;
|
||||
uint32_t functionToggles;
|
||||
};
|
||||
|
||||
class WiThrottle {
|
||||
@@ -36,15 +31,14 @@ 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();
|
||||
|
||||
static const int MAX_MY_LOCO=10; // maximum number of locos assigned to a single client
|
||||
static const int HEARTBEAT_SECONDS=10; // heartbeat at 4secs to provide messaging transport
|
||||
static const int ESTOP_SECONDS=20; // eStop if no incoming messages for more than 8secs
|
||||
static const int HEARTBEAT_SECONDS=4; // heartbeat at 4secs to provide messaging transport
|
||||
static const int ESTOP_SECONDS=8; // eStop if no incoming messages for more than 8secs
|
||||
static WiThrottle* firstThrottle;
|
||||
static int getInt(byte * cmd);
|
||||
static int getLocoId(byte * cmd);
|
||||
@@ -68,8 +62,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(RingStream * stream);
|
||||
void markForBroadcast2(int cab);
|
||||
void checkHeartbeat();
|
||||
|
||||
// callback stuff to support prog track acquire
|
||||
static RingStream * stashStream;
|
||||
static WiThrottle * stashInstance;
|
||||
|
246
WifiESP32.cpp
Normal file
246
WifiESP32.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
© 2021, 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 <vector>
|
||||
#include "defines.h"
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#include <WiFi.h>
|
||||
#include "WifiESP32.h"
|
||||
#include "DIAG.h"
|
||||
#include "RingStream.h"
|
||||
#include "CommandDistributor.h"
|
||||
/*
|
||||
#include "soc/rtc_wdt.h"
|
||||
#include "esp_task_wdt.h"
|
||||
*/
|
||||
|
||||
#include "soc/timer_group_struct.h"
|
||||
#include "soc/timer_group_reg.h"
|
||||
void feedTheDog0(){
|
||||
// feed dog 0
|
||||
TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable
|
||||
TIMERG0.wdt_feed=1; // feed dog
|
||||
TIMERG0.wdt_wprotect=0; // write protect
|
||||
// feed dog 1
|
||||
//TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable
|
||||
//TIMERG1.wdt_feed=1; // feed dog
|
||||
//TIMERG1.wdt_wprotect=0; // write protect
|
||||
}
|
||||
|
||||
/*
|
||||
void enableCoreWDT(byte core){
|
||||
TaskHandle_t idle = xTaskGetIdleTaskHandleForCPU(core);
|
||||
if(idle == NULL){
|
||||
DIAG(F("Get idle rask on core %d failed"),core);
|
||||
} else {
|
||||
if(esp_task_wdt_add(idle) != ESP_OK){
|
||||
DIAG(F("Failed to add Core %d IDLE task to WDT"),core);
|
||||
} else {
|
||||
DIAG(F("Added Core %d IDLE task to WDT"),core);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void disableCoreWDT(byte core){
|
||||
TaskHandle_t idle = xTaskGetIdleTaskHandleForCPU(core);
|
||||
if(idle == NULL || esp_task_wdt_delete(idle) != ESP_OK){
|
||||
DIAG(F("Failed to remove Core %d IDLE task from WDT"),core);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
static std::vector<WiFiClient> clients; // a list to hold all clients
|
||||
static WiFiServer *server = NULL;
|
||||
static RingStream *outboundRing = new RingStream(2048);
|
||||
static bool APmode = false;
|
||||
|
||||
void wifiLoop(void *){
|
||||
for(;;){
|
||||
WifiESP::loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool WifiESP::setup(const char *SSid,
|
||||
const char *password,
|
||||
const char *hostname,
|
||||
int port,
|
||||
const byte channel) {
|
||||
bool havePassword = true;
|
||||
bool haveSSID = true;
|
||||
bool wifiUp = false;
|
||||
uint8_t tries = 40;
|
||||
|
||||
// tests
|
||||
// enableCoreWDT(1);
|
||||
// disableCoreWDT(0);
|
||||
|
||||
const char *yourNetwork = "Your network ";
|
||||
if (strncmp(yourNetwork, SSid, 13) == 0 || strncmp("", SSid, 13) == 0)
|
||||
haveSSID = false;
|
||||
if (strncmp(yourNetwork, password, 13) == 0 || strncmp("", password, 13) == 0)
|
||||
havePassword = false;
|
||||
|
||||
if (haveSSID && havePassword) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.begin(SSid, password);
|
||||
while (WiFi.status() != WL_CONNECTED && tries) {
|
||||
Serial.print('.');
|
||||
tries--;
|
||||
delay(500);
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
|
||||
wifiUp = true;
|
||||
} else {
|
||||
DIAG(F("Could not connect to Wifi SSID %s"),SSid);
|
||||
}
|
||||
}
|
||||
if (!haveSSID) {
|
||||
// prepare all strings
|
||||
String strSSID("DCC_");
|
||||
String strPass("PASS_");
|
||||
String strMac = WiFi.macAddress();
|
||||
strMac.remove(0,9);
|
||||
strMac.replace(":","");
|
||||
strMac.replace(":","");
|
||||
strSSID.concat(strMac);
|
||||
strPass.concat(strMac);
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
if (WiFi.softAP(strSSID.c_str(),
|
||||
havePassword ? password : strPass.c_str(),
|
||||
channel, false, 8)) {
|
||||
DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
|
||||
DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
|
||||
wifiUp = true;
|
||||
APmode = true;
|
||||
} else {
|
||||
DIAG(F("Could not set up AP with Wifi SSID %s"),strSSID.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!wifiUp) {
|
||||
DIAG(F("Wifi setup all fail (STA and AP mode)"));
|
||||
// no idea to go on
|
||||
return false;
|
||||
}
|
||||
server = new WiFiServer(port); // start listening on tcp port
|
||||
server->begin();
|
||||
// server started here
|
||||
|
||||
//start loop task
|
||||
if (pdPASS != xTaskCreatePinnedToCore(
|
||||
wifiLoop, /* Task function. */
|
||||
"wifiLoop",/* name of task. */
|
||||
10000, /* Stack size of task */
|
||||
NULL, /* parameter of the task */
|
||||
1, /* priority of the task */
|
||||
NULL, /* Task handle to keep track of created task */
|
||||
0)) { /* pin task to core 0 */
|
||||
DIAG(F("Could not create wifiLoop task"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// report server started after wifiLoop creation
|
||||
// when everything looks good
|
||||
DIAG(F("Server up port %d"),port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WifiESP::loop() {
|
||||
int clientId; //tmp loop var
|
||||
|
||||
// really no good way to check for LISTEN especially in AP mode?
|
||||
if (APmode || WiFi.status() == WL_CONNECTED) {
|
||||
if (server->hasClient()) {
|
||||
// loop over all clients and remove inactive
|
||||
for (clientId=0; clientId<clients.size(); clientId++){
|
||||
// check if client is there and alive
|
||||
if(!clients[clientId].connected()) {
|
||||
clients[clientId].stop();
|
||||
clients.erase(clients.begin()+clientId);
|
||||
}
|
||||
}
|
||||
WiFiClient client;
|
||||
while (client = server->available()) {
|
||||
clients.push_back(client);
|
||||
DIAG(F("New client %s"), client.remoteIP().toString().c_str());
|
||||
}
|
||||
}
|
||||
// loop over all connected clients
|
||||
for (clientId=0; clientId<clients.size(); clientId++){
|
||||
if(clients[clientId].connected()) {
|
||||
int len;
|
||||
if ((len = clients[clientId].available()) > 0) {
|
||||
// read data from client
|
||||
byte cmd[len+1];
|
||||
for(int i=0; i<len; i++) {
|
||||
cmd[i]=clients[clientId].read();
|
||||
}
|
||||
cmd[len]=0;
|
||||
outboundRing->mark(clientId);
|
||||
CommandDistributor::parse(clientId,cmd,outboundRing);
|
||||
outboundRing->commit();
|
||||
}
|
||||
}
|
||||
} // all clients
|
||||
|
||||
// something to write out?
|
||||
clientId=outboundRing->peek();
|
||||
if (clientId >= 0) {
|
||||
if ((unsigned int)clientId > clients.size()) {
|
||||
// something is wrong with the ringbuffer position
|
||||
outboundRing->info();
|
||||
} else {
|
||||
// we have data to send in outboundRing
|
||||
if(clients[clientId].connected()) {
|
||||
outboundRing->read(); // read over peek()
|
||||
int count=outboundRing->count();
|
||||
{
|
||||
char buffer[count+1];
|
||||
for(int i=0;i<count;i++) {
|
||||
int c = outboundRing->read();
|
||||
if (c >= 0)
|
||||
buffer[i] = (char)c;
|
||||
else {
|
||||
DIAG(F("Ringread fail at %d"),i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
buffer[count]=0;
|
||||
clients[clientId].write(buffer,count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} //connected
|
||||
|
||||
// when loop() is running on core0 we must
|
||||
// feed the core0 wdt ourselves as yield()
|
||||
// is not necessarily yielding to a low
|
||||
// prio task. On core1 this is not a problem
|
||||
// as there the wdt is disabled by the
|
||||
// arduio IDE startup routines.
|
||||
if (xPortGetCoreID() == 0)
|
||||
feedTheDog0();
|
||||
yield();
|
||||
}
|
||||
#endif //ESP32
|
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX
|
||||
/*
|
||||
* © 2021, 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
|
||||
@@ -18,32 +17,23 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SerialManager_h
|
||||
#define SerialManager_h
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#ifndef WifiESP32_h
|
||||
#define WifiESP32_h
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "defines.h"
|
||||
#include "RingStream.h"
|
||||
#include "FSH.h"
|
||||
|
||||
#ifndef COMMAND_BUFFER_SIZE
|
||||
#define COMMAND_BUFFER_SIZE 100
|
||||
#endif
|
||||
class WifiESP
|
||||
{
|
||||
|
||||
class SerialManager {
|
||||
public:
|
||||
static void init();
|
||||
static bool setup(const char *wifiESSID,
|
||||
const char *wifiPassword,
|
||||
const char *hostname,
|
||||
const int port,
|
||||
const byte channel);
|
||||
static void loop();
|
||||
static void broadcast(RingStream * ring);
|
||||
|
||||
private:
|
||||
static SerialManager * first;
|
||||
SerialManager(Stream * myserial);
|
||||
void loop2();
|
||||
void broadcast2(RingStream * ring);
|
||||
Stream * serial;
|
||||
SerialManager * next;
|
||||
byte bufferLength;
|
||||
byte buffer[COMMAND_BUFFER_SIZE];
|
||||
bool inCommandPayload;
|
||||
private:
|
||||
};
|
||||
#endif
|
||||
#endif //WifiESP8266_h
|
||||
#endif //ESP8266
|
270
WifiESP8266.cpp
Normal file
270
WifiESP8266.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
© 2021, 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 "defines.h"
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "WifiESP8266.h"
|
||||
#include "DIAG.h"
|
||||
#include "RingStream.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include <string.h>
|
||||
|
||||
static std::vector<AsyncClient*> clients; // a list to hold all clients
|
||||
static AsyncServer *server;
|
||||
|
||||
static RingStream *outboundRing = new RingStream(2048);
|
||||
|
||||
static void handleError(void* arg, AsyncClient* client, int8_t error) {
|
||||
(void)arg;
|
||||
DIAG(F("connection error %s from client %s"), client->errorToString(error), client->remoteIP().toString().c_str());
|
||||
}
|
||||
|
||||
static void handleData(void* arg, AsyncClient* client, void *data, size_t len) {
|
||||
(void)arg;
|
||||
//DIAG(F("data received from client %s"), client->remoteIP().toString().c_str());
|
||||
uint8_t clientId;
|
||||
for (clientId=0; clientId<clients.size(); clientId++){
|
||||
if (clients[clientId] == client) break;
|
||||
}
|
||||
if (clientId < clients.size()) {
|
||||
byte cmd[len+1];
|
||||
memcpy(cmd,data,len);
|
||||
cmd[len]=0;
|
||||
outboundRing->mark(clientId);
|
||||
CommandDistributor::parse(clientId,cmd,outboundRing);
|
||||
outboundRing->commit();
|
||||
}
|
||||
}
|
||||
|
||||
//static AsyncClient *debugclient = NULL;
|
||||
|
||||
bool sendData(AsyncClient *client, char* data, size_t count) {
|
||||
size_t willsend = 0;
|
||||
|
||||
// reply to client
|
||||
if (client->canSend()) {
|
||||
while (count > 0) {
|
||||
if (client->connected())
|
||||
willsend = client->add(data, count); // add checks for space()
|
||||
else
|
||||
willsend = 0;
|
||||
if (willsend < count) {
|
||||
DIAG(F("Willsend %d of count %d"), willsend, count);
|
||||
}
|
||||
if (client->connected() && client->send()) {
|
||||
count = count - willsend;
|
||||
data = data + willsend;
|
||||
} else {
|
||||
DIAG(F("Could not send promised %d"), count);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Did send all bytes we wanted
|
||||
return true;
|
||||
}
|
||||
DIAG(F("Aborting: Busy or space=0"));
|
||||
return false;
|
||||
}
|
||||
|
||||
static void deleteClient(AsyncClient* client) {
|
||||
uint8_t clientId;
|
||||
for (clientId=0; clientId<clients.size(); clientId++){
|
||||
if (clients[clientId] == client) break;
|
||||
}
|
||||
if (clientId < clients.size()) {
|
||||
clients[clientId] = NULL;
|
||||
}
|
||||
}
|
||||
static void handleDisconnect(void* arg, AsyncClient* client) {
|
||||
(void)arg;
|
||||
DIAG(F("Client disconnected"));
|
||||
deleteClient(client);
|
||||
}
|
||||
|
||||
static void handleTimeOut(void* arg, AsyncClient* client, uint32_t time) {
|
||||
(void)arg;
|
||||
(void)time;
|
||||
DIAG(F("client ACK timeout ip: %s"), client->remoteIP().toString().c_str());
|
||||
deleteClient(client);
|
||||
}
|
||||
|
||||
|
||||
static void handleNewClient(void* arg, AsyncClient* client) {
|
||||
(void)arg;
|
||||
DIAG(F("New client %s"), client->remoteIP().toString().c_str());
|
||||
|
||||
// add to list
|
||||
clients.push_back(client);
|
||||
|
||||
// register events
|
||||
client->onData(&handleData, NULL);
|
||||
client->onError(&handleError, NULL);
|
||||
client->onDisconnect(&handleDisconnect, NULL);
|
||||
client->onTimeout(&handleTimeOut, NULL);
|
||||
|
||||
}
|
||||
|
||||
/* Things one _might_ want to do:
|
||||
Disable soft watchdog: ESP.wdtDisable()
|
||||
Enable soft watchdog: ESP.wdtEnable(X) ignores the value of X and enables it for fixed
|
||||
time at least in version 3.0.2 of the esp8266 package.
|
||||
|
||||
Internet says:
|
||||
|
||||
I manage to complety disable the hardware watchdog on ESP8266 in order to run the benchmark CoreMark.
|
||||
|
||||
void hw_wdt_disable(){
|
||||
*((volatile uint32_t*) 0x60000900) &= ~(1); // Hardware WDT OFF
|
||||
}
|
||||
|
||||
void hw_wdt_enable(){
|
||||
*((volatile uint32_t*) 0x60000900) |= 1; // Hardware WDT ON
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
bool WifiESP::setup(const char *SSid,
|
||||
const char *password,
|
||||
const char *hostname,
|
||||
int port,
|
||||
const byte channel) {
|
||||
bool havePassword = true;
|
||||
bool haveSSID = true;
|
||||
bool wifiUp = false;
|
||||
|
||||
// We are server and should not sleep
|
||||
wifi_set_sleep_type(NONE_SLEEP_T);
|
||||
// connects to access point
|
||||
|
||||
const char *yourNetwork = "Your network ";
|
||||
if (strncmp(yourNetwork, SSid, 13) == 0 || strncmp("", SSid, 13) == 0)
|
||||
haveSSID = false;
|
||||
if (strncmp(yourNetwork, password, 13) == 0 || strncmp("", password, 13) == 0)
|
||||
havePassword = false;
|
||||
|
||||
if (haveSSID && havePassword) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.begin(SSid, password);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print('.');
|
||||
delay(500);
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
|
||||
wifiUp = true;
|
||||
}
|
||||
}
|
||||
if (!haveSSID) {
|
||||
// prepare all strings
|
||||
String strSSID("DCC_");
|
||||
String strPass("PASS_");
|
||||
String strMac = WiFi.macAddress();
|
||||
strMac.remove(0,9);
|
||||
strMac.replace(":","");
|
||||
strMac.replace(":","");
|
||||
strSSID.concat(strMac);
|
||||
strPass.concat(strMac);
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
if (WiFi.softAP(strSSID.c_str(),
|
||||
havePassword ? password : strPass.c_str(),
|
||||
channel, false, 8)) {
|
||||
DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
|
||||
DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
|
||||
wifiUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!wifiUp) {
|
||||
DIAG(F("Wifi all fail"));
|
||||
// no idea to go on
|
||||
return false;
|
||||
}
|
||||
|
||||
server = new AsyncServer(port); // start listening on tcp port
|
||||
|
||||
server->onClient(&handleNewClient, server);
|
||||
server->begin();
|
||||
DIAG(F("Server up port %d"),port);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WifiESP::loop() {
|
||||
AsyncClient *client = NULL;
|
||||
// Do something with outboundRing
|
||||
// call sendData
|
||||
int clientId=outboundRing->peek();
|
||||
if (clientId >= 0) {
|
||||
if ((unsigned int)clientId > clients.size()) {
|
||||
// something is wrong with the ringbuffer position
|
||||
outboundRing->info();
|
||||
client = NULL;
|
||||
} else {
|
||||
client = clients[clientId];
|
||||
}
|
||||
// if (client != debugclient) {
|
||||
// DIAG(F("new client pointer = %x from id %d"), client, clientId);
|
||||
// debugclient = client;
|
||||
// }
|
||||
} else {
|
||||
client = NULL;
|
||||
}
|
||||
if (clientId>=0 && client && client->connected() && client->canSend()) {
|
||||
outboundRing->read();
|
||||
int count=outboundRing->count();
|
||||
//DIAG(F("Wifi reply client=%d, count=%d"), clientId,count);
|
||||
{
|
||||
char buffer[count+1];
|
||||
for(int i=0;i<count;i++) {
|
||||
int c = outboundRing->read();
|
||||
if (c >= 0)
|
||||
buffer[i] = (char)c;
|
||||
else {
|
||||
DIAG(F("Ringread fail at %d"),i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
buffer[count]=0;
|
||||
//DIAG(F("SEND:%s COUNT:%d"),buffer,count);
|
||||
uint8_t tries = 3;
|
||||
while (! sendData(client, buffer, count)) {
|
||||
DIAG(F("senData fail"));
|
||||
yield();
|
||||
if (tries == 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef ESP_DEBUG
|
||||
static unsigned long last = 0;
|
||||
if (millis() - last > 60000) {
|
||||
last = millis();
|
||||
DIAG(F("+"));
|
||||
}
|
||||
#endif
|
||||
ESP.wdtFeed();
|
||||
}
|
||||
#endif //ESP_FAMILY
|
39
WifiESP8266.h
Normal file
39
WifiESP8266.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* © 2021, 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/>.
|
||||
*/
|
||||
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#ifndef WifiESP8266_h
|
||||
#define WifiESP8266_h
|
||||
|
||||
#include "FSH.h"
|
||||
|
||||
class WifiESP
|
||||
{
|
||||
|
||||
public:
|
||||
static bool setup(const char *wifiESSID,
|
||||
const char *wifiPassword,
|
||||
const char *hostname,
|
||||
const int port,
|
||||
const byte channel);
|
||||
static void loop();
|
||||
private:
|
||||
};
|
||||
#endif //WifiESP8266_h
|
||||
#endif //ESP8266
|
@@ -1,27 +1,22 @@
|
||||
/*
|
||||
© 2022 Mark Muzzin
|
||||
© 2021 Fred Decker
|
||||
© 2021 Fred Decker
|
||||
© 2020-2021 Chris Harlow
|
||||
© 2020, Chris Harlow. All rights reserved.
|
||||
© 2020, Harald Barth.
|
||||
|
||||
This file is part of Asbelos DCC 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/>.
|
||||
*/
|
||||
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* This file is part of Asbelos DCC 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 ARDUINO_AVR_UNO_WIFI_REV2
|
||||
#include <Arduino.h>
|
||||
#include "WifiInboundHandler.h"
|
||||
@@ -32,7 +27,7 @@
|
||||
WifiInboundHandler * WifiInboundHandler::singleton;
|
||||
|
||||
void WifiInboundHandler::setup(Stream * ESStream) {
|
||||
singleton = new WifiInboundHandler(ESStream);
|
||||
singleton=new WifiInboundHandler(ESStream);
|
||||
}
|
||||
|
||||
void WifiInboundHandler::loop() {
|
||||
@@ -41,328 +36,219 @@ void WifiInboundHandler::loop() {
|
||||
|
||||
|
||||
WifiInboundHandler::WifiInboundHandler(Stream * ESStream) {
|
||||
wifiStream = ESStream;
|
||||
clientPendingCIPSEND = -1;
|
||||
inboundRing = new RingStream(INBOUND_RING);
|
||||
outboundRing = new RingStream(OUTBOUND_RING);
|
||||
cipSendStatus = CIP_SEND_NONE;
|
||||
}
|
||||
wifiStream=ESStream;
|
||||
clientPendingCIPSEND=-1;
|
||||
inboundRing=new RingStream(INBOUND_RING);
|
||||
outboundRing=new RingStream(OUTBOUND_RING);
|
||||
pendingCipsend=false;
|
||||
}
|
||||
|
||||
|
||||
// Handle any inbound transmission
|
||||
// +IPD,x,lll:data is stored in streamer[x]
|
||||
// Other input returns
|
||||
void WifiInboundHandler::loop1()
|
||||
{
|
||||
// First handle all inbound traffic events because they will block the sending
|
||||
if (loop2() != INBOUND_IDLE) return;
|
||||
// Other input returns
|
||||
void WifiInboundHandler::loop1() {
|
||||
// First handle all inbound traffic events because they will block the sending
|
||||
if (loop2()!=INBOUND_IDLE) return;
|
||||
|
||||
WiThrottle::loop(outboundRing);
|
||||
WiThrottle::loop(outboundRing);
|
||||
|
||||
// if nothing is already CIPSEND pending, we can CIPSEND one reply
|
||||
if (clientPendingCIPSEND<0) {
|
||||
clientPendingCIPSEND=outboundRing->read();
|
||||
if (clientPendingCIPSEND>=0) {
|
||||
currentReplySize=outboundRing->count();
|
||||
pendingCipsend=true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no sends are pending we can check if there is any more data to send
|
||||
if (cipSendStatus == CIP_SEND_NONE) {
|
||||
|
||||
// If nothing is already CIPSEND pending, we can CIPSEND one reply
|
||||
if (clientPendingCIPSEND < 0) {
|
||||
|
||||
//Read the next character in the outbound ring. If its -1, no data in the ring
|
||||
clientPendingCIPSEND = outboundRing->read();
|
||||
|
||||
//If there is data in the ring
|
||||
if (clientPendingCIPSEND >= 0 ) {
|
||||
if (Diag::WIFI) { ts.cipSendStart = millis(); DIAG( F("cipSendStatus = CIP_SEND_PENDING")); }
|
||||
//Get the length of the reply to send
|
||||
currentReplySize = outboundRing->count();
|
||||
//Set the pending CIPSEND flag
|
||||
cipSendStatus = CIP_SEND_PENDING;
|
||||
if (pendingCipsend) {
|
||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), clientPendingCIPSEND, currentReplySize);
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), clientPendingCIPSEND, currentReplySize);
|
||||
pendingCipsend=false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cipSendStatus == CIP_SEND_PENDING) {
|
||||
if (Diag::WIFI) DIAG( F("WiFi: [[CIPSEND=%d,%d]]"), clientPendingCIPSEND, currentReplySize);
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSEND=%d,%d\r\n"), clientPendingCIPSEND, currentReplySize);
|
||||
if (Diag::WIFI) DIAG( F("cipSendStatus = CIP_SEND_SENT [%lms]"),millis()-ts.cipSendStart);
|
||||
cipSendStatus = CIP_SEND_SENT;
|
||||
return;
|
||||
}
|
||||
|
||||
// if something waiting to execute, we can call it
|
||||
int clientId = inboundRing->read();
|
||||
if (clientId >= 0) {
|
||||
int count = inboundRing->count();
|
||||
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"), clientId, count);
|
||||
byte cmd[count + 1];
|
||||
for (int i = 0; i < count; i++) cmd[i] = inboundRing->read();
|
||||
cmd[count] = 0;
|
||||
if (Diag::WIFI) DIAG(F("%e"), cmd);
|
||||
|
||||
outboundRing->mark(clientId); // remember start of outbound data
|
||||
CommandDistributor::parse(clientId, cmd, outboundRing);
|
||||
|
||||
// The commit call will either write the length bytes
|
||||
// OR rollback to the mark because the reply is empty or commend generated more than fits the buffer
|
||||
if (outboundRing->commit() == false) {
|
||||
DIAG(F("OUTBOUND FULL processing cmd:%s"), cmd);
|
||||
outboundRing->flush();
|
||||
inboundRing->flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed length atoi
|
||||
int WifiInboundHandler::antoi(char* str, int len) {
|
||||
int res = 0;
|
||||
|
||||
for (int i = 0; i != len; ++i)
|
||||
res = res * 10 + str[i] - '0';
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// if something waiting to execute, we can call it
|
||||
int clientId=inboundRing->read();
|
||||
if (clientId>=0) {
|
||||
int count=inboundRing->count();
|
||||
if (Diag::WIFI) DIAG(F("Wifi EXEC: %d %d:"),clientId,count);
|
||||
byte cmd[count+1];
|
||||
for (int i=0;i<count;i++) cmd[i]=inboundRing->read();
|
||||
cmd[count]=0;
|
||||
if (Diag::WIFI) DIAG(F("%e"),cmd);
|
||||
|
||||
outboundRing->mark(clientId); // remember start of outbound data
|
||||
CommandDistributor::parse(clientId,cmd,outboundRing);
|
||||
// The commit call will either write the lenbgth bytes
|
||||
// OR rollback to the mark because the reply is empty or commend generated more than fits the buffer
|
||||
if (!outboundRing->commit()) {
|
||||
DIAG(F("OUTBOUND FULL processing cmd:%s"),cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This loop processes the inbound bytes from an ES AT command processor
|
||||
|
||||
// This is a Finite State Automation (FSA) handling the inbound bytes from an ES AT command processor
|
||||
|
||||
WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
while (wifiStream->available()) {
|
||||
int ch = wifiStream->read();
|
||||
|
||||
INBOUND_STATE retVal = INBOUND_IDLE;
|
||||
char *stringStartPos = NULL;
|
||||
char *stringEndPos = NULL;
|
||||
char ch = 0;
|
||||
int i;
|
||||
bool exitLoop = false;
|
||||
|
||||
//Main message input loop
|
||||
//This loop fills the small receive buffer one message at a time
|
||||
//The execption is the +IPD message. Only the data up to the ':' character (end of length)
|
||||
//is put in this buffer. The actual +IPD data is written directly into the inbound buffer
|
||||
while (!exitLoop)
|
||||
{
|
||||
//We exit this loop either if there is no data available, or we have a complete message to process
|
||||
while (wifiStream->available() && messageToProcess == RECV_MSG_NONE)
|
||||
{
|
||||
//Read next character
|
||||
ch = wifiStream->read();
|
||||
|
||||
//Add to receive buffer if there is space
|
||||
if (recBufferPos < INBOUND_CMD_BUFFER) {
|
||||
recBuffer[recBufferPos++] = ch;
|
||||
}
|
||||
else {
|
||||
if (Diag::WIFI) DIAG(F("WARN: Message too long for INBOUND BUFFER, purging"));
|
||||
memset(recBuffer, 0, INBOUND_CMD_BUFFER);
|
||||
recBufferPos = 0;
|
||||
}
|
||||
|
||||
if (Diag::WIFI) StringFormatter::printEscape(ch);
|
||||
|
||||
//Process data ending in \r\n
|
||||
if ((recBuffer[recBufferPos - 2] == '\r') && (recBuffer[recBufferPos - 1] == '\n')) {
|
||||
messageToProcess = RECV_MSG_CRLF;
|
||||
}
|
||||
|
||||
//Ready to send data, will always be the first char in the message
|
||||
else if (recBuffer[0] == '>') {
|
||||
messageToProcess = RECV_MSG_SND_DATA;
|
||||
}
|
||||
|
||||
//IPD message
|
||||
else if ((stringStartPos = strstr(recBuffer, "+IPD,")) != NULL && (stringEndPos = strstr(recBuffer, ":")) != NULL) {
|
||||
|
||||
if (Diag::WIFI) { ts.ipdStart = millis(); DIAG( F("+IPD Message START")); }
|
||||
|
||||
//Advance the start string to the beginning of the length
|
||||
stringStartPos += strlen("+IPD,");
|
||||
|
||||
//Get the end of the client ID
|
||||
stringEndPos = strstr(stringStartPos, ",");
|
||||
|
||||
//Get the client Id
|
||||
runningClientId = antoi(stringStartPos, stringEndPos - stringStartPos);
|
||||
|
||||
//Get the data length
|
||||
stringStartPos = stringEndPos + 1;
|
||||
stringEndPos = strstr(stringStartPos, ":");
|
||||
dataLength = antoi(stringStartPos, stringEndPos - stringStartPos);
|
||||
|
||||
if(dataLength > 0) {
|
||||
//Set the remaining data length
|
||||
dataRemaining = dataLength;
|
||||
messageToProcess = RECV_MSG_IPD_MSG;
|
||||
}
|
||||
}
|
||||
// echo the char to the diagnostic stream in escaped format
|
||||
if (Diag::WIFI) {
|
||||
// DIAG(F(" %d/"), loopState);
|
||||
StringFormatter::printEscape(ch); // DIAG in disguise
|
||||
}
|
||||
|
||||
//Process messages
|
||||
switch (messageToProcess) {
|
||||
case RECV_MSG_NONE:
|
||||
//Check if the buffer position is not zero
|
||||
if (recBufferPos != 0) {
|
||||
//No message to process, but buffer has some data already
|
||||
retVal = INBOUND_BUSY;
|
||||
switch (loopState) {
|
||||
case ANYTHING: // looking for +IPD, > , busy , n,CONNECTED, n,CLOSED, ERROR, SEND OK
|
||||
|
||||
if (ch == '+') {
|
||||
loopState = IPD;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
//No message to process, no data in buffer
|
||||
retVal = INBOUND_IDLE;
|
||||
|
||||
if (ch=='>') {
|
||||
if (Diag::WIFI) DIAG(F("[XMIT %d]"),currentReplySize);
|
||||
for (int i=0;i<currentReplySize;i++) {
|
||||
int cout=outboundRing->read();
|
||||
wifiStream->write(cout);
|
||||
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
||||
}
|
||||
clientPendingCIPSEND=-1;
|
||||
pendingCipsend=false;
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
}
|
||||
exitLoop = true;
|
||||
break;
|
||||
|
||||
case RECV_MSG_CRLF:
|
||||
|
||||
//Check for a SEND FAIL message
|
||||
if (strstr(recBuffer, "SEND FAIL") != NULL) {
|
||||
if (Diag::WIFI) DIAG(F("[SEND FAIL - detected"));
|
||||
if (cipSendStatus == CIP_SEND_DATA_SENT) {
|
||||
if (Diag::WIFI) DIAG( F("cipSendStatus = CIP_SEND_NONE [%lms]"),millis()-ts.cipSendStart);
|
||||
cipSendStatus = CIP_SEND_NONE;
|
||||
}
|
||||
|
||||
if (ch=='R') { // Received ... bytes
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch=='S') { // SEND OK probably
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch=='b') { // This is a busy indicator... probabaly must restart a CIPSEND
|
||||
pendingCipsend=(clientPendingCIPSEND>=0);
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch>='0' && ch<='9') {
|
||||
runningClientId=ch-'0';
|
||||
loopState=GOT_CLIENT_ID;
|
||||
break;
|
||||
}
|
||||
|
||||
//Check for a SEND OK message
|
||||
else if (strstr(recBuffer, "SEND OK") != NULL) {
|
||||
if (Diag::WIFI) DIAG(F("[SEND OK - detected [%lms]"),millis()-ts.cipSendStart);
|
||||
if (cipSendStatus == CIP_SEND_DATA_SENT) {
|
||||
if (Diag::WIFI) DIAG( F("cipSendStatus = CIP_SEND_NONE [%lms]"),millis()-ts.cipSendStart);
|
||||
cipSendStatus = CIP_SEND_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
//Check for an error
|
||||
else if (strstr(recBuffer, "ERROR") != NULL) {
|
||||
if (clientPendingCIPSEND >= 0) {
|
||||
if (Diag::WIFI) DIAG(F("[ERROR detected - purging CIPSEND"));
|
||||
if (ch=='E' || ch=='l') { // ERROR or "link is not valid"
|
||||
if (clientPendingCIPSEND>=0) {
|
||||
// A CIPSEND was errored... just toss it away
|
||||
purgeCurrentCIPSEND();
|
||||
}
|
||||
}
|
||||
|
||||
//Check for an tcp connection closed
|
||||
else if (strstr(recBuffer, "link is not valid") != NULL) {
|
||||
if (clientPendingCIPSEND >= 0) {
|
||||
if (Diag::WIFI) DIAG(F("['link is not valid' detected - purging CIPSEND]"));
|
||||
purgeCurrentCIPSEND();
|
||||
//Clear out all data from esp8266 receive buffer
|
||||
while (wifiStream->available()) {
|
||||
wifiStream->read();
|
||||
}
|
||||
purgeCurrentCIPSEND();
|
||||
}
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case RECV_MSG_SND_DATA:
|
||||
if (Diag::WIFI) DIAG( F("cipSendStatus = CIP_SEND_DATA_SENT [XMIT START %d] [%lms]"),currentReplySize, millis()-ts.cipSendStart);
|
||||
for (i = 0; i < currentReplySize; i++)
|
||||
{
|
||||
//Read data from the outboundRing and write to to Wifi
|
||||
int cout = outboundRing->read();
|
||||
wifiStream->write(cout);
|
||||
if (Diag::WIFI) StringFormatter::printEscape(cout); // DIAG in disguise
|
||||
}
|
||||
|
||||
//Set CIPSEND flag to -1
|
||||
clientPendingCIPSEND = -1;
|
||||
if (Diag::WIFI) DIAG( F("cipSendStatus = CIP_SEND_DATA_SENT [XMIT END] [%lms]"),millis()-ts.cipSendStart);
|
||||
cipSendStatus = CIP_SEND_DATA_SENT;
|
||||
|
||||
case IPD: // Looking for I in +IPD
|
||||
loopState = (ch == 'I') ? IPD1 : SKIPTOEND;
|
||||
break;
|
||||
|
||||
case RECV_MSG_IPD_MSG:
|
||||
|
||||
//Mark the location with the client ID
|
||||
if (dataRemaining == dataLength) {
|
||||
if (Diag::WIFI) DIAG(F("Wifi inbound data(%d:%d) [%lms]:"), runningClientId, dataLength, millis()-ts.ipdStart);
|
||||
inboundRing->mark(runningClientId);
|
||||
|
||||
case IPD1: // Looking for P in +IPD
|
||||
loopState = (ch == 'P') ? IPD2 : SKIPTOEND;
|
||||
break;
|
||||
|
||||
case IPD2: // Looking for D in +IPD
|
||||
loopState = (ch == 'D') ? IPD3 : SKIPTOEND;
|
||||
break;
|
||||
|
||||
case IPD3: // Looking for , After +IPD
|
||||
loopState = (ch == ',') ? IPD4_CLIENT : SKIPTOEND;
|
||||
break;
|
||||
|
||||
case IPD4_CLIENT: // reading connection id
|
||||
if (ch >= '0' || ch <='9'){
|
||||
runningClientId=ch-'0';
|
||||
loopState=IPD5;
|
||||
}
|
||||
|
||||
while (wifiStream->available()) {
|
||||
//Read next character (data byte)
|
||||
ch = wifiStream->read();
|
||||
if (Diag::WIFI) StringFormatter::printEscape(ch);
|
||||
if (Diag::WIFI) DIAG(F("[+IPD %d]"), dataRemaining);
|
||||
dataRemaining--;
|
||||
|
||||
//Check if we would overflow the inbound ring
|
||||
if (inboundRing->freeSpace() <= (dataLength + 1))
|
||||
{
|
||||
DIAG(F("Wifi OVERFLOW IGNORING:"));
|
||||
|
||||
//If we have an overflow, flush the remaining bytes
|
||||
do{
|
||||
ch = wifiStream->read();
|
||||
}while(dataRemaining--);
|
||||
|
||||
outboundRing->flush();
|
||||
inboundRing->flush();
|
||||
|
||||
messageToProcess = RECV_MSG_IPD_DONE;
|
||||
else loopState=SKIPTOEND;
|
||||
break;
|
||||
|
||||
case IPD5: // Looking for , After +IPD,client
|
||||
loopState = (ch == ',') ? IPD6_LENGTH : SKIPTOEND;
|
||||
dataLength=0; // ready to start collecting the length
|
||||
break;
|
||||
|
||||
case IPD6_LENGTH: // reading for length
|
||||
if (ch == ':') {
|
||||
if (dataLength==0) {
|
||||
loopState=ANYTHING;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write data to inbound ring
|
||||
inboundRing->write(ch);
|
||||
|
||||
// Commit inbound ring data
|
||||
if (dataRemaining == 0) {
|
||||
if (Diag::WIFI) DIAG(F("COMMIT [%lms]:"),millis()-ts.ipdStart);
|
||||
if(inboundRing->commit() == false) {
|
||||
outboundRing->flush();
|
||||
inboundRing->flush();
|
||||
}
|
||||
messageToProcess = RECV_MSG_IPD_DONE;
|
||||
break;
|
||||
}
|
||||
if (Diag::WIFI) DIAG(F("Wifi inbound data(%d:%d):"),runningClientId,dataLength);
|
||||
if (inboundRing->freeSpace()<=(dataLength+1)) {
|
||||
// This input would overflow the inbound ring, ignore it
|
||||
loopState=IPD_IGNORE_DATA;
|
||||
if (Diag::WIFI) DIAG(F("Wifi OVERFLOW IGNORING:"));
|
||||
break;
|
||||
}
|
||||
inboundRing->mark(runningClientId);
|
||||
loopState=IPD_DATA;
|
||||
break;
|
||||
}
|
||||
dataLength = dataLength * 10 + (ch - '0');
|
||||
break;
|
||||
|
||||
case IPD_DATA: // reading data
|
||||
inboundRing->write(ch);
|
||||
dataLength--;
|
||||
if (dataLength == 0) {
|
||||
inboundRing->commit();
|
||||
loopState = ANYTHING;
|
||||
}
|
||||
break;
|
||||
|
||||
//Catch-all for messages we don't care about
|
||||
default:
|
||||
//Check if the buffer position is not zero
|
||||
if (recBufferPos != 0)
|
||||
{
|
||||
//In process of receiving, return with busy so we can wait for the remaining part of the message
|
||||
retVal = INBOUND_BUSY;
|
||||
}
|
||||
else {
|
||||
// Not receiving a message so this message was not important
|
||||
messageToProcess = RECV_MSG_GENERIC;
|
||||
}
|
||||
case IPD_IGNORE_DATA: // ignoring data that would not fit in inbound ring
|
||||
dataLength--;
|
||||
if (dataLength == 0) loopState = ANYTHING;
|
||||
break;
|
||||
}
|
||||
|
||||
//If we processed a message, clear and get ready for the next message
|
||||
if (messageToProcess != RECV_MSG_NONE && messageToProcess != RECV_MSG_IPD_MSG) {
|
||||
//Reset buffer to start processing next message
|
||||
memset(recBuffer, 0, INBOUND_CMD_BUFFER);
|
||||
//Reset receive buffer position
|
||||
recBufferPos = 0;
|
||||
//Reset message to process
|
||||
messageToProcess = RECV_MSG_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
case GOT_CLIENT_ID: // got x before CLOSE or CONNECTED
|
||||
loopState=(ch==',') ? GOT_CLIENT_ID2: SKIPTOEND;
|
||||
break;
|
||||
|
||||
case GOT_CLIENT_ID2: // got "x,"
|
||||
if (ch=='C') {
|
||||
// got "x C" before CLOSE or CONNECTED, or CONNECT FAILED
|
||||
if (runningClientId==clientPendingCIPSEND) purgeCurrentCIPSEND();
|
||||
}
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
|
||||
case SKIPTOEND: // skipping for /n
|
||||
if (ch=='\n') loopState=ANYTHING;
|
||||
break;
|
||||
} // switch
|
||||
} // available
|
||||
return (loopState==ANYTHING) ? INBOUND_IDLE: INBOUND_BUSY;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
//If CIP SEND was sent, flush out by writing 0s */
|
||||
if (cipSendStatus == CIP_SEND_SENT) {
|
||||
for (int i = 0; i < currentReplySize; i++) {
|
||||
int c = 0;
|
||||
wifiStream->write(c);
|
||||
}
|
||||
}
|
||||
// Clear CIP send status
|
||||
cipSendStatus = CIP_SEND_NONE;
|
||||
clientPendingCIPSEND = -1;
|
||||
// A CIPSEND was sent but errored... or the client closed just toss it away
|
||||
DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
|
||||
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
|
||||
pendingCipsend=false;
|
||||
clientPendingCIPSEND=-1;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@@ -1,25 +1,22 @@
|
||||
/*
|
||||
© 2022 Mark Muzzin
|
||||
© 2021 Harald Barth
|
||||
© 2021 Fred Decker
|
||||
(c) 2021 Fred Decker. All rights reserved.
|
||||
(c) 2020 Chris Harlow. All rights reserved.
|
||||
|
||||
This file is part of CommandStation-EX
|
||||
|
||||
This is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef WifiInboundHandler_h
|
||||
#define WifiInboundHandler_h
|
||||
|
||||
@@ -28,65 +25,57 @@
|
||||
#include "DIAG.h"
|
||||
|
||||
class WifiInboundHandler {
|
||||
public:
|
||||
static void setup(Stream * ESStream);
|
||||
static void loop();
|
||||
public:
|
||||
static void setup(Stream * ESStream);
|
||||
static void loop();
|
||||
|
||||
private:
|
||||
|
||||
private:
|
||||
static WifiInboundHandler * singleton;
|
||||
|
||||
|
||||
enum INBOUND_STATE : byte {
|
||||
INBOUND_BUSY, // keep calling in loop()
|
||||
INBOUND_IDLE // Nothing happening, outbound may xcall CIPSEND
|
||||
};
|
||||
|
||||
static WifiInboundHandler * singleton;
|
||||
enum LOOP_STATE : byte {
|
||||
ANYTHING, // ready for +IPD, n CLOSED, n CONNECTED, busy etc...
|
||||
SKIPTOEND, // skip to newline
|
||||
|
||||
// +IPD,client,length:data
|
||||
IPD, // got +
|
||||
IPD1, // got +I
|
||||
IPD2, // got +IP
|
||||
IPD3, // got +IPD
|
||||
IPD4_CLIENT, // got +IPD, reading cient id
|
||||
IPD5, // got +IPD,c
|
||||
IPD6_LENGTH, // got +IPD,c, reading length
|
||||
IPD_DATA, // got +IPD,c,ll,: collecting data
|
||||
IPD_IGNORE_DATA, // got +IPD,c,ll,: ignoring the data that won't fit inblound Ring
|
||||
|
||||
enum INBOUND_STATE : byte {
|
||||
INBOUND_BUSY, // Keep calling in loop()
|
||||
INBOUND_IDLE // Nothing happening, outbound may xcall CIPSEND
|
||||
};
|
||||
GOT_CLIENT_ID, // clientid prefix to CONNECTED / CLOSED
|
||||
GOT_CLIENT_ID2 // clientid prefix to CONNECTED / CLOSED
|
||||
};
|
||||
|
||||
enum RECV_MSG_STATE : byte {
|
||||
RECV_MSG_NONE, // No message to process
|
||||
RECV_MSG_CRLF, // Message ending in CRLF
|
||||
RECV_MSG_SND_DATA, // Send data message
|
||||
RECV_MSG_IPD_MSG, // +IPD Message in progress
|
||||
RECV_MSG_IPD_DONE, // +IPD Message done
|
||||
RECV_MSG_GENERIC // Catchall for incoming messages not used
|
||||
};
|
||||
|
||||
enum CIP_SEND_STATUS : byte {
|
||||
CIP_SEND_NONE,
|
||||
CIP_SEND_PENDING,
|
||||
CIP_SEND_SENT,
|
||||
CIP_SEND_DATA_SENT
|
||||
};
|
||||
|
||||
typedef struct _timeStamps {
|
||||
long cipSendStart;
|
||||
long ipdStart;
|
||||
} timeStamps_t;
|
||||
|
||||
WifiInboundHandler(Stream * ESStream);
|
||||
void loop1();
|
||||
INBOUND_STATE loop2();
|
||||
void purgeCurrentCIPSEND();
|
||||
int antoi(char* str, int len);
|
||||
Stream * wifiStream;
|
||||
|
||||
static const int INBOUND_CMD_BUFFER = 32;
|
||||
static const int INBOUND_RING = 512;
|
||||
static const int OUTBOUND_RING = 2048;
|
||||
|
||||
RingStream * inboundRing;
|
||||
RingStream * outboundRing;
|
||||
|
||||
RECV_MSG_STATE messageToProcess = RECV_MSG_NONE; // Track the current message to process
|
||||
|
||||
int runningClientId; // latest client inbound processing data or CLOSE
|
||||
int dataLength; // dataLength of +IPD
|
||||
int dataRemaining = 0; // remaining +IPD data to receive
|
||||
int clientPendingCIPSEND = -1;
|
||||
int currentReplySize;
|
||||
CIP_SEND_STATUS cipSendStatus;
|
||||
char recBuffer[INBOUND_CMD_BUFFER] = {'\0'};
|
||||
int recBufferPos = 0;
|
||||
int recBufferWatermark = 0;
|
||||
timeStamps_t ts = {0};
|
||||
|
||||
WifiInboundHandler(Stream * ESStream);
|
||||
void loop1();
|
||||
INBOUND_STATE loop2();
|
||||
void purgeCurrentCIPSEND();
|
||||
Stream * wifiStream;
|
||||
|
||||
static const int INBOUND_RING = 512;
|
||||
static const int OUTBOUND_RING = 2048;
|
||||
|
||||
RingStream * inboundRing;
|
||||
RingStream * outboundRing;
|
||||
|
||||
LOOP_STATE loopState=ANYTHING;
|
||||
int runningClientId; // latest client inbound processing data or CLOSE
|
||||
int dataLength; // dataLength of +IPD
|
||||
int clientPendingCIPSEND=-1;
|
||||
int currentReplySize;
|
||||
bool pendingCipsend;
|
||||
};
|
||||
#endif
|
||||
|
@@ -1,27 +1,26 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2022 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
© 2020, Chris Harlow. All rights reserved.
|
||||
© 2020, 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 "WifiInterface.h" /* config.h included there */
|
||||
#ifndef ESP_FAMILY
|
||||
#ifndef ARDUINO_AVR_UNO_WIFI_REV2
|
||||
// This code is NOT compiled on a unoWifiRev2 processor which uses a different architecture
|
||||
#include "WifiInterface.h" /* config.h included there */
|
||||
#include <avr/pgmspace.h>
|
||||
#include "DIAG.h"
|
||||
#include "StringFormatter.h"
|
||||
@@ -77,13 +76,13 @@ bool WifiInterface::setup(long serial_link_speed,
|
||||
(void) channel;
|
||||
#endif
|
||||
|
||||
#if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS)
|
||||
#if NUM_SERIAL > 0
|
||||
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 && !defined(SERIAL2_COMMANDS)
|
||||
#if NUM_SERIAL > 1
|
||||
if (wifiUp == WIFI_NOAT)
|
||||
{
|
||||
Serial2.begin(serial_link_speed);
|
||||
@@ -91,7 +90,7 @@ bool WifiInterface::setup(long serial_link_speed,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS)
|
||||
#if NUM_SERIAL > 2
|
||||
if (wifiUp == WIFI_NOAT)
|
||||
{
|
||||
Serial3.begin(serial_link_speed);
|
||||
@@ -279,16 +278,10 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
|
||||
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSERVER=0\r\n")); // turn off tcp server (to clean connections before CIPMUX=1)
|
||||
checkForOK(1000, true); // ignore result in case it already was off
|
||||
|
||||
|
||||
StringFormatter::send(wifiStream, F("AT+CIPMUX=1\r\n")); // configure for multiple connections
|
||||
if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;
|
||||
|
||||
if(!oldCmd) { // no idea to test this on old firmware
|
||||
StringFormatter::send(wifiStream, F("AT+MDNS=1,\"%S\",\"withrottle\",%d\r\n"),
|
||||
hostname, port); // mDNS responder
|
||||
checkForOK(1000, true); // dont care if not supported
|
||||
}
|
||||
|
||||
StringFormatter::send(wifiStream, F("AT+CIPSERVER=1,%d\r\n"), port); // turn on server on port
|
||||
if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;
|
||||
#endif //DONT_TOUCH_WIFI_CONF
|
||||
@@ -324,45 +317,19 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
|
||||
|
||||
|
||||
// This function is used to allow users to enter <+ commands> through the DCCEXParser
|
||||
// <+command> sends AT+command to the ES and returns to the caller.
|
||||
// Once the user has made whatever changes to the AT commands, a <+X> command can be used
|
||||
// to force on the connectd flag so that the loop will start picking up wifi traffic.
|
||||
// If the settings are corrupted <+RST> will clear this and then you must restart the arduino.
|
||||
|
||||
// Using the <+> command with no command string causes the code to enter an echo loop so that all
|
||||
// input is directed to the ES and all ES output written to the USB Serial.
|
||||
// The sequence "!!!" returns the Arduino to the normal loop mode
|
||||
|
||||
|
||||
void WifiInterface::ATCommand(HardwareSerial * stream,const byte * command) {
|
||||
void WifiInterface::ATCommand(const byte * command) {
|
||||
command++;
|
||||
if (*command=='\0') { // User gave <+> command
|
||||
stream->print(F("\nES AT command passthrough mode, use ! to exit\n"));
|
||||
while(stream->available()) stream->read(); // Drain serial input first
|
||||
bool startOfLine=true;
|
||||
while(true) {
|
||||
while (wifiStream->available()) stream->write(wifiStream->read());
|
||||
if (stream->available()) {
|
||||
int cx=stream->read();
|
||||
// A newline followed by !!! is an exit
|
||||
if (cx=='\n' || cx=='\r') startOfLine=true;
|
||||
else if (startOfLine && cx=='!') break;
|
||||
else startOfLine=false;
|
||||
stream->write(cx);
|
||||
wifiStream->write(cx);
|
||||
}
|
||||
}
|
||||
stream->print(F("Passthrough Ended"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (*command=='X') {
|
||||
connected = true;
|
||||
DIAG(F("++++++ Wifi Connction forced on ++++++++"));
|
||||
connected = true;
|
||||
DIAG(F("++++++ Wifi Connction forced on ++++++++"));
|
||||
}
|
||||
else {
|
||||
StringFormatter:: send(wifiStream, F("AT+%s\r\n"), command);
|
||||
checkForOK(10000, true);
|
||||
StringFormatter:: send(wifiStream, F("AT+%s\r\n"), command);
|
||||
checkForOK(10000, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,4 +371,5 @@ void WifiInterface::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif //ARDUINO_AVR_UNO_WIFI_REV2
|
||||
#endif //ESP_FAMILY
|
||||
|
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* All rights reserved.
|
||||
*
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@@ -20,6 +19,8 @@
|
||||
*/
|
||||
#ifndef WifiInterface_h
|
||||
#define WifiInterface_h
|
||||
#include "defines.h"
|
||||
#ifndef ESP_FAMILY
|
||||
#include "FSH.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include <Arduino.h>
|
||||
@@ -38,7 +39,7 @@ public:
|
||||
const int port,
|
||||
const byte channel);
|
||||
static void loop();
|
||||
static void ATCommand(HardwareSerial * stream,const byte *command);
|
||||
static void ATCommand(const byte *command);
|
||||
|
||||
private:
|
||||
static wifiSerialState setup(Stream &setupStream, const FSH *SSSid, const FSH *password,
|
||||
@@ -51,4 +52,5 @@ private:
|
||||
static bool checkForOK(const unsigned int timeout, const FSH *waitfor, bool echo, bool escapeEcho = true);
|
||||
static bool connected;
|
||||
};
|
||||
#endif
|
||||
#endif //ESP_FAMILY
|
||||
#endif
|
||||
|
@@ -1,8 +1,5 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* COPYRIGHT (c) 2020 Fred Decker
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@@ -29,7 +26,7 @@ The configuration file for DCC-EX Command Station
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// NOTE: Before connecting these boards and selecting one in this software
|
||||
// check the quick install guides!!! Some of these boards require a voltage
|
||||
// generating resistor on the current sense pin of the device. Failure to select
|
||||
// generating resitor on the current sense pin of the device. Failure to select
|
||||
// the correct resistor could damage the sense pin on your Arduino or destroy
|
||||
// the device.
|
||||
//
|
||||
@@ -44,7 +41,32 @@ The configuration file for DCC-EX Command Station
|
||||
// |
|
||||
// +-----------------------v
|
||||
//
|
||||
#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD
|
||||
//#define MOTOR_SHIELD_TYPE STANDARD_MOTOR_SHIELD
|
||||
|
||||
// https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/
|
||||
// 4 high at boot
|
||||
// BUG: WE STILL NEED AT LEAST ONE TIMER_* motor shield defined, otherwise the packet scheduling does not work
|
||||
// BUG: WE DO NOT HAVE ANY RMT_PROG CAPABILITY yet.
|
||||
#define ESP32_MOTOR_SHIELD F("ESP32"), \
|
||||
NULL /*new MotorDriver(16, 17, UNUSED_PIN, UNUSED_PIN, 36, 2.00, 2000, UNUSED_PIN, TIMER_MAIN)*/, \
|
||||
new MotorDriver(18, 19, UNUSED_PIN, UNUSED_PIN, 39, 2.00, 2000, UNUSED_PIN, TIMER_PROG), \
|
||||
new MotorDriver(16, 23, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 2.00, 2000, UNUSED_PIN, RMT_MAIN)
|
||||
|
||||
// ESP32 ADC1 only supported GPIO pins 32 to 39, for example
|
||||
// ADC1 CH4 = GPIO32, ADC1 CH5 = GPIO33, ADC1 CH0 = GPIO36
|
||||
//
|
||||
// Adjust pin usage according to info in
|
||||
// https://randomnerdtutorials.com/esp32-adc-analog-read-arduino-ide/
|
||||
// https://randomnerdtutorials.com/esp32-pinout-reference-gpios/
|
||||
//
|
||||
// Adjust conversion factor according to your voltage divider.
|
||||
//
|
||||
#define ESP32_MOTOR_SHIELD F("ESP32"), \
|
||||
new MotorDriver(16, 17, UNUSED_PIN, UNUSED_PIN, 32, 2.00, 2000, UNUSED_PIN),\
|
||||
new MotorDriver(18, 19, UNUSED_PIN, UNUSED_PIN, 33, 2.00, 2000, UNUSED_PIN)
|
||||
|
||||
#define MOTOR_SHIELD_TYPE ESP32_MOTOR_SHIELD
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The IP port to talk to a WIFI or Ethernet shield.
|
||||
@@ -56,6 +78,8 @@ The configuration file for DCC-EX Command Station
|
||||
// NOTE: Only supported on Arduino Mega
|
||||
// Set to false if you not even want it on the Arduino Mega
|
||||
//
|
||||
// Currently ESP32 single core only works with WIFI ON because of Watchdog code
|
||||
// and if you have an ESP32 you probably want WIFI anyway.
|
||||
#define ENABLE_WIFI true
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -85,7 +109,7 @@ The configuration file for DCC-EX Command Station
|
||||
// WIFI_PASSWORD is the network password for your home network or if
|
||||
// you want to change the password from default AP mode password
|
||||
// to the AP password you want.
|
||||
// Your password may not contain ``"'' (double quote, ASCII 0x22).
|
||||
// Your password may not conain ``"'' (double quote, ASCII 0x22).
|
||||
#define WIFI_PASSWORD "Your network passwd"
|
||||
//
|
||||
// WIFI_HOSTNAME: You probably don't need to change this
|
||||
@@ -125,36 +149,12 @@ The configuration file for DCC-EX Command Station
|
||||
|
||||
//OR define OLED_DRIVER width,height in pixels (address auto detected)
|
||||
// 128x32 or 128x64 I2C SSD1306-based devices are supported.
|
||||
// Use 132,64 for a SH1106-based I2C device with a 128x64 display.
|
||||
// Also 132x64 I2C SH1106 devices
|
||||
// #define OLED_DRIVER 128,32
|
||||
|
||||
// Define scroll mode as 0, 1 or 2
|
||||
// * #define SCROLLMODE 0 is scroll continuous (fill screen if poss),
|
||||
// * #define SCROLLMODE 1 is by page (alternate between pages),
|
||||
// * #define SCROLLMODE 2 is by row (move up 1 row at a time).
|
||||
#define SCROLLMODE 1
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// DISABLE EEPROM
|
||||
//
|
||||
// If you do not need the EEPROM at all, you can disable all the code that saves
|
||||
// data in the EEPROM. You might want to do that if you are in a Arduino UNO
|
||||
// and want to use the EX-RAIL automation. Otherwise you do not have enough RAM
|
||||
// to do that. Of course, then none of the EEPROM related commands work.
|
||||
//
|
||||
// #define DISABLE_EEPROM
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address
|
||||
// is 127 and the first long address is 128. There are manufacturers which have
|
||||
// another view. Lenz CS for example have considered addresses long from 100. If
|
||||
// you want to change to that mode, do
|
||||
//#define HIGHEST_SHORT_ADDR 99
|
||||
// If you want to run all your locos addressed long format, you could even do a
|
||||
//#define HIGHEST_SHORT_ADDR 0
|
||||
// We do not support to use the same address, for example 100(long) and 100(short)
|
||||
// at the same time, there must be a border.
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213
|
||||
@@ -168,32 +168,10 @@ The configuration file for DCC-EX Command Station
|
||||
// don't add it to your config.h.
|
||||
//#define DCC_TURNOUTS_RCN_213
|
||||
|
||||
// By default, the driver which defines a DCC accessory decoder
|
||||
// does send out the same state change on the DCC packet as it
|
||||
// receives. This means a VPIN state=1 sends D=1 (close turnout
|
||||
// or signal green) in the DCC packet. This can be reversed if
|
||||
// necessary.
|
||||
//#define HAL_ACCESSORY_COMMAND_REVERSE
|
||||
|
||||
// If you have issues with that the direction of the accessory commands is
|
||||
// reversed (for example when converting from another CS to DCC-EX) then
|
||||
// you can use this to reverse the sense of all accessory commmands sent
|
||||
// over DCC++. This #define likewise inverts the behaviour of the <a> command
|
||||
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
|
||||
// The following #define likewise inverts the behaviour of the <a> command
|
||||
// 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_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_COMMANDS
|
||||
//#define SERIAL2_COMMANDS
|
||||
//#define SERIAL3_COMMANDS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
98
defines.h
98
defines.h
@@ -1,26 +1,22 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
© 2020,2021 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/>.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef DEFINES_H
|
||||
#define DEFINES_H
|
||||
@@ -35,37 +31,47 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
#define ESP_FAMILY
|
||||
//#define ESP_DEBUG
|
||||
#define SLOW_ANALOG_READ
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
#if defined(ARDUINO_ARCH_ESP32)
|
||||
#define ESP_FAMILY
|
||||
#define SLOW_ANALOG_READ
|
||||
#else
|
||||
#define portENTER_CRITICAL(A) do {} while (0)
|
||||
#define portEXIT_CRITICAL(A) do {} while (0)
|
||||
#endif
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// WIFI_ON: All prereqs for running with WIFI are met
|
||||
// Note: WIFI_CHANNEL may not exist in early config.h files so is added here if needed.
|
||||
|
||||
#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO)) || defined(ARDUINO_AVR_NANO_EVERY)
|
||||
#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO) || defined(ESP_FAMILY))
|
||||
#define BIG_RAM
|
||||
#endif
|
||||
#if ENABLE_WIFI
|
||||
#if defined(BIG_RAM)
|
||||
#define WIFI_ON true
|
||||
#ifndef WIFI_CHANNEL
|
||||
#define WIFI_CHANNEL 1
|
||||
#endif
|
||||
#else
|
||||
#define WIFI_WARNING
|
||||
#define WIFI_ON false
|
||||
#endif
|
||||
|
||||
#if ENABLE_WIFI && defined(BIG_RAM)
|
||||
#define WIFI_ON true
|
||||
#ifndef WIFI_CHANNEL
|
||||
#define WIFI_CHANNEL 1
|
||||
#endif
|
||||
#else
|
||||
#define WIFI_ON false
|
||||
#define WIFI_ON false
|
||||
#endif
|
||||
|
||||
#if ENABLE_ETHERNET
|
||||
#if defined(BIG_RAM)
|
||||
#define ETHERNET_ON true
|
||||
#else
|
||||
#define ETHERNET_WARNING
|
||||
#define ETHERNET_ON false
|
||||
#endif
|
||||
#if ENABLE_ETHERNET && defined(BIG_RAM)
|
||||
#define ETHERNET_ON true
|
||||
#else
|
||||
#define ETHERNET_ON false
|
||||
#define ETHERNET_ON false
|
||||
#endif
|
||||
|
||||
#if WIFI_ON && ETHERNET_ON
|
||||
@@ -79,12 +85,8 @@
|
||||
//
|
||||
#define WIFI_SERIAL_LINK_SPEED 115200
|
||||
|
||||
#if __has_include ( "myAutomation.h")
|
||||
#if defined(BIG_RAM) || defined(DISABLE_EEPROM)
|
||||
#define EXRAIL_ACTIVE
|
||||
#else
|
||||
#define EXRAIL_WARNING
|
||||
#endif
|
||||
#if __has_include ( "myAutomation.h") && defined(BIG_RAM)
|
||||
#define RMFT_ACTIVE
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
67
esp32wdt.txt
Normal file
67
esp32wdt.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
The ESP-IDF has interrupt watchdogs and task watchdogs. Normally on
|
||||
each core there is a very low prio idle task (IDLE0, ILDE1) that feeds
|
||||
the watchdog (an internal timer) and if that timer expires a
|
||||
reboot/reset happens. This thought to enure that even the lowest prio
|
||||
tasks get run ever.
|
||||
|
||||
Now enter the Arduino IDE generated task loop(). When there are two cores,
|
||||
loop() is run on core1. If loop runs continiously, IDLE1 is never run and
|
||||
triggers the watchdog. It is said that this can be previented by one
|
||||
of the following:
|
||||
|
||||
1. Call delay(X) with big enough X
|
||||
2. Call yield()
|
||||
|
||||
While the delay() method works, big enough X to run idle seem to be in
|
||||
the ms range and there are definitely applications that can not accept
|
||||
a several ms long pause in loop().
|
||||
|
||||
The yield() method does not work because it only seems not to yield to
|
||||
a low prio task like IDLE1 in all circumstances.
|
||||
|
||||
Then the makers of the Arduino IDE did get the brilliant idea to
|
||||
disable that IDLE1 calls the watchdog. Then loop() can spin on core1
|
||||
and other tasks (like wifi or interrupts or whatever) can run on core0
|
||||
and are watched by the IDLE0 watchdog. All swell and well. Almost.
|
||||
|
||||
Enter: SINGLE CORE ESP32
|
||||
|
||||
As the IDLE0 watchdog is not disabled it will fire when loop() runs on
|
||||
core0. The next idea is to feed the watchdog from loop() just
|
||||
alongside the yield. There is a function called esp_task_wdt_feed(),
|
||||
so can that be used to feed the watchdog? Yes and no. While it
|
||||
will feed the watchdog, there is as well as check in the ESP-IDF
|
||||
that the watchdog is fed from ALL tasks that should feed it. So
|
||||
if the setup is that IDLE0 should feed the watchdog, we can not
|
||||
get away by calling esp_task_wdt_feed() from loop(). BUMMER!
|
||||
|
||||
But there seems to be a way around this. The watchdog is implemented
|
||||
by low level timers/counters and these are accessible. So we can feed
|
||||
the dog behind the back of the ESP-IDF:
|
||||
|
||||
#include "soc/timer_group_struct.h"
|
||||
#include "soc/timer_group_reg.h"
|
||||
void feedTheDog(){
|
||||
// feed dog 0
|
||||
TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable
|
||||
TIMERG0.wdt_feed=1; // feed dog
|
||||
TIMERG0.wdt_wprotect=0; // write protect
|
||||
// feed dog 1
|
||||
TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable
|
||||
TIMERG1.wdt_feed=1; // feed dog
|
||||
TIMERG1.wdt_wprotect=0; // write protect
|
||||
}
|
||||
|
||||
As I do not have a single core ESP32 I tested this by enabling the
|
||||
IDLE1 watchdog (which normally is disabled) and checking that I
|
||||
do get watchdog resets. Then I call feedTheDog() from loop()
|
||||
and the resets disappear. So I guess the feeding operation is
|
||||
successful. For a single core ESP32 of course only dog0 has
|
||||
to be fed.
|
||||
|
||||
Feed dog directly behind back of the ESP-IDF routines:
|
||||
https://forum.arduino.cc/t/esp32-a-better-way-than-vtaskdelay-to-get-around-watchdog-crash/596889/13
|
||||
Disable/Endable WDT code:
|
||||
https://github.com/espressif/arduino-esp32/commit/b8f8502f
|
||||
Get/set taskid on cores:
|
||||
https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/
|
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2020 Harald Barth
|
||||
* © 2020,2021 Harald Barth
|
||||
* © 2021, Neil McKechnie
|
||||
*
|
||||
* This file is part of Asbelos DCC-EX
|
||||
*
|
||||
@@ -28,6 +27,8 @@ extern "C" char* sbrk(int);
|
||||
#elif defined(__AVR__)
|
||||
extern char *__brkval;
|
||||
extern char *__malloc_heap_start;
|
||||
#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
|
||||
// supported but nothing needed here
|
||||
#else
|
||||
#error Unsupported board type
|
||||
#endif
|
||||
@@ -35,7 +36,7 @@ extern char *__malloc_heap_start;
|
||||
|
||||
static volatile int minimum_free_memory = __INT_MAX__;
|
||||
|
||||
#if !defined(__IMXRT1062__)
|
||||
#if !defined(__IMXRT1062__) && !defined(ARDUINO_ARCH_ESP8266) && !defined(ARDUINO_ARCH_ESP32)
|
||||
static inline int freeMemory() {
|
||||
char top;
|
||||
#if defined(__arm__)
|
||||
@@ -56,7 +57,18 @@ int minimumFreeMemory() {
|
||||
return retval;
|
||||
}
|
||||
|
||||
#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
|
||||
// ESP8266 and ESP32
|
||||
static inline int freeMemory() {
|
||||
return ESP.getFreeHeap();
|
||||
}
|
||||
// Return low memory value.
|
||||
int minimumFreeMemory() {
|
||||
int retval = minimum_free_memory;
|
||||
return retval;
|
||||
}
|
||||
#else
|
||||
// All types of TEENSYs
|
||||
#if defined(ARDUINO_TEENSY40)
|
||||
static const unsigned DTCM_START = 0x20000000UL;
|
||||
static const unsigned OCRAM_START = 0x20200000UL;
|
||||
@@ -109,4 +121,3 @@ void updateMinimumFreeMemory(unsigned char extraBytes) {
|
||||
if (spare < 0) spare = 0;
|
||||
if (spare < minimum_free_memory) minimum_free_memory = spare;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020 Harald Barth
|
||||
* © 2020, Harald Barth
|
||||
* © 2021, Neil McKechnie
|
||||
*
|
||||
* This file is part of DCC-EX
|
||||
*
|
||||
|
270
installer.json
270
installer.json
@@ -1,270 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Name": "BaseStationClassic",
|
||||
"Git": "DCC-EX/BaseStation-Classic",
|
||||
"Libraries": [
|
||||
{
|
||||
"Name": "Ethernet",
|
||||
"Repo": "arduino-libraries/Ethernet",
|
||||
"Location": "libraries/Ethernet",
|
||||
"LibraryDownloadAvailable": true
|
||||
}
|
||||
],
|
||||
"SupportedBoards": [
|
||||
{
|
||||
"Name": "Uno",
|
||||
"FQBN": "arduino:avr:uno",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Mega",
|
||||
"FQBN": "arduino:avr:mega",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
}
|
||||
],
|
||||
"DisplayName": "Base Station Classic",
|
||||
"InputFileLocation": "DCCpp",
|
||||
"AllowAdvanced": false,
|
||||
"ConfigFile": "DCCpp/Config.h"
|
||||
},
|
||||
{
|
||||
"Name": "CommandStation-EX",
|
||||
"Git": "DCC-EX/CommandStation-EX",
|
||||
"Libraries": [
|
||||
{
|
||||
"Name": "Ethernet",
|
||||
"Repo": "arduino-libraries/Ethernet",
|
||||
"Location": "libraries/Ethernet",
|
||||
"LibraryDownloadAvailable": true
|
||||
},
|
||||
{
|
||||
"Name": "DIO2",
|
||||
"Repo": "",
|
||||
"Location": "libraries/DIO2",
|
||||
"LibraryDownloadAvailable": true
|
||||
}
|
||||
],
|
||||
"SupportedBoards": [
|
||||
{
|
||||
"Name": "Uno",
|
||||
"FQBN": "arduino:avr:uno",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Mega",
|
||||
"FQBN": "arduino:avr:mega",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Mega Wifi",
|
||||
"FQBN": "arduino:avr:mega",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Nano",
|
||||
"FQBN": "arduino:avr:nano",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Nano Every",
|
||||
"FQBN": "arduino:megaavr:nanoevery",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "megaavr",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Teensy 3.x",
|
||||
"FQBN": "teensy:avr:teensy31",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "teensy",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "Teensy 4.x",
|
||||
"FQBN": "teensy:avr:teensy41",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "teensy",
|
||||
"Package": "arduino"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
},
|
||||
{
|
||||
"Name": "SAMD21",
|
||||
"FQBN": "SparkFun:samd",
|
||||
"Platforms": [
|
||||
{
|
||||
"Architecture": "avr",
|
||||
"Package": "arduino"
|
||||
},
|
||||
{
|
||||
"Architecture": "samd",
|
||||
"Package": "arduino"
|
||||
},
|
||||
{
|
||||
"Architecture": "samd",
|
||||
"Package": "SparkFun"
|
||||
}
|
||||
],
|
||||
"SupportedMotoShields": [
|
||||
{
|
||||
"Name": "Arduino Motor Shield",
|
||||
"Declaration": "STANDARD_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "Pololu MC33926 Motor Shield",
|
||||
"Declaration": "POLOLU_MOTOR_SHIELD"
|
||||
},
|
||||
{
|
||||
"Name": "FireBox MK1",
|
||||
"Declaration": "FIREBOX_MK1"
|
||||
},
|
||||
{
|
||||
"Name": "FireBox MK1S",
|
||||
"Declaration": "FIREBOX_MK1S"
|
||||
}
|
||||
],
|
||||
"ExtraLibraries": []
|
||||
}
|
||||
],
|
||||
"DisplayName": "CommandStation EX",
|
||||
"InputFileLocation": "",
|
||||
"AllowAdvanced": true,
|
||||
"ConfigFile": "config.h"
|
||||
}
|
||||
]
|
@@ -1,18 +1,19 @@
|
||||
/* This is an automation example file.
|
||||
* The presence of a file called "myAutomation.h" brings EX-RAIL code into
|
||||
* The presence of a file calle "myAutomation.h" brings EX-RAIL code into
|
||||
* the command station.
|
||||
* The automation may have multiple concurrent tasks.
|
||||
* The auotomation may have multiple concurrent tasks.
|
||||
* A task may
|
||||
* - Act as a ROUTE setup macro for a user to drive over
|
||||
* - drive a loco through an AUTOMATION
|
||||
* - automate some cosmetic part of the layout without any loco.
|
||||
*
|
||||
* At startup, a single task is created to execute the startup sequence.
|
||||
* At startup, a single task is created to execute the first
|
||||
* instruction after E$XRAIL.
|
||||
* This task may simply follow a route, or may START
|
||||
* further tasks (that is.. send a loco out along a route).
|
||||
* 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 created with the command:
|
||||
* can be creatd with the command:
|
||||
* </ START [cab] route>
|
||||
*
|
||||
* A ROUTE, AUTOMATION or SEQUENCE are internally identical in ExRail terms
|
||||
@@ -23,10 +24,11 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// This is the startup sequence, AKA SEQUENCE(0)
|
||||
SENDLOCO(3,1) // send loco 3 off along route 1
|
||||
SENDLOCO(10,2) // send loco 10 off along route 2
|
||||
DONE // This just ends the startup thread, leaving 2 others running.
|
||||
EXRAIL // myAutomation must start with the EXRAIL instruction
|
||||
// This is the default starting route, AKA SEQUENCE(0)
|
||||
SENDLOCO(3,1) // send loco 3 off along route 1
|
||||
SENDLOCO(10,2) // send loco 10 off along route 2
|
||||
DONE // This just ends the startup thread, leaving 2 others running.
|
||||
|
||||
/* SEQUENCE(1) is a simple shuttle between 2 sensors
|
||||
* S20 and S21 are sensors on arduino pins 20 and 21
|
||||
@@ -76,3 +78,7 @@ DONE // This just ends the startup thread, leaving 2 others running.
|
||||
AT(33) STOP
|
||||
DELAY(20000) // wait 20 seconds
|
||||
FOLLOW(2) // follow sequence 2... ie repeat the process
|
||||
|
||||
ENDEXRAIL // marks the end of the EXRAIL program.
|
||||
|
||||
|
||||
|
@@ -11,10 +11,6 @@
|
||||
// To prevent this, temporarily rename the file to myHal.txt or similar.
|
||||
//
|
||||
|
||||
// The #if directive prevent compile errors for Uno and Nano by excluding the
|
||||
// HAL directives from the build.
|
||||
#if !defined(IO_NO_HAL)
|
||||
|
||||
// Include devices you need.
|
||||
#include "IODevice.h"
|
||||
#include "IO_HCSR04.h" // Ultrasonic range sensor
|
||||
|
@@ -1,88 +1,16 @@
|
||||
The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production Release. Release v4.0.0 is a Major release that adds significant new product design, plus Automation features and bug fixes. The team continues improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors. This release includes all of the Point Releases from v3.2.0 to v3.2.0 rc13.
|
||||
The DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release. Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors. This release includes all of the Point Releases from v3.0.1 to v3.0.16.
|
||||
|
||||
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
|
||||
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)
|
||||
|
||||
|
||||
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v0.0.0-Prod/CommandStation-EX.tar.gz)
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)
|
||||
|
||||
**Known Issues**
|
||||
|
||||
- **Wi-Fi** - Requires sending `<AT>` commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup
|
||||
- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitations in its current sensing circuitry
|
||||
- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup
|
||||
- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitation in its current sensing circuitry
|
||||
|
||||
**All New Major DCC++EX 4.0.0 features**
|
||||
|
||||
- **New HAL Hardware Abstraction Layer API** that automatically detects and greatly simplifies interfacing to many predefined accessory boards for servos, signals & sensors and added I/O (digital and analog inputs and outputs, servos etc).
|
||||
- HAL Support for;
|
||||
- MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.
|
||||
- PCA9685 PWM (servo & signal) control modules.
|
||||
- Analogue inputs on Arduino pins and on ADS111x I2C modules.
|
||||
- MP3 sound playback via DFPlayer module.
|
||||
- HC-SR04 Ultrasonic range sensor module.
|
||||
- VL53L0X Laser range sensor module (Time-Of-Flight).
|
||||
- A new `<D HAL SHOW>` command to list the HAL devices attached to the command station
|
||||
|
||||
**New Command Station Broadcast throttle logic**
|
||||
|
||||
- Synchronizes multiple WiThrottles and PC based JMRI Throttles for direction, speed and F-key updates
|
||||
|
||||
**New ‘Discovered Servers’ on WiFi Throttles**
|
||||
|
||||
- Our New multicast Dynamic Network Server (mDNS) enhancement allows us to display the available WiFi server connections to a DCC++EX Command Station. Selecting it allows your WiThrottle App to connect to and load Server Rosters and function keys to your throttle from the new DCC++EX Command Station Server Roster.
|
||||
|
||||
**New DCC++EX 4.0.0 with EX-RAIL Extended Railroad Automation Instruction Language**
|
||||
|
||||
- Use to control your entire layout or as a separate accessory/animation controller
|
||||
- Awesome, cleverly powerful yet simple user friendly scripting language for user built Automation & Routing scripts.
|
||||
- You can control Engines, Sensors, Turnouts, Signals, Outputs and Accessories that are entered into your new myAutomation.h file, then uploaded into the DCC++EX Command Station.
|
||||
- EX-RAIL scripts are automatically displayed as Automation {Handoff} and Route {Set} buttons on supported WiFi Throttle Apps.
|
||||
|
||||
**New EX-RAIL ‘Roster’ Feature**
|
||||
|
||||
- List and store user defined engine roster & function keys inside the command station, and automatically load them in WiFi Throttle Apps.
|
||||
- When choosing “DCC++EX” from discovered servers an Engine Driver or WiThrottle is directly connected to the Command Station.
|
||||
- The EX-RAIL ’ROSTER’ command allows all the engine numbers, names and function keys you’ve listed in your myAutomation.h file to automatically upload the Command Station's ‘Server Roster’ into your Engine Driver and WiThrottle Apps.
|
||||
|
||||
**New JMRI 4.99.2 and above specific DCC++EX 4.0 features**
|
||||
|
||||
- Enhanced JMRI DCC++ Configure Base Station pane for building and maintaining Sensor, Turnout and Output devices, or these can automatically be populated from the DCC++EX Command Station's mySetup.h file into JMRI.
|
||||
|
||||
- JMRI now supports multiple serial connected DCC++EX Command Stations, to display and track separate "Send DCC++ Command" and "DCC++ Traffic" Monitors for each Command Station at the same time.
|
||||
For example: Use an Uno DCC++EX DecoderPro Programming Station {DCC++Prg} on a desktop programming track and a second Mega DCC++EX EX-RAIL Command Station for Operations {DCC++Ops} on the layout with an additional `<JOINED>` programming spur or siding track for acquiring an engine and ‘Drive Away’ onto the mainline (see the DriveAway feature for more information).
|
||||
|
||||
**DCC++EX 4.0.0 additional product enhancements**
|
||||
|
||||
- Additional Motor Shields and Motor Board {boosters) supported
|
||||
- Additional Accessory boards supported for GPIO expansion, Sensors, Servos & Signals
|
||||
- Additional diagnostic commands like ‘D ACK RETRY’ and ‘D EXRAIL ON’ events, ‘D HAL SHOW’ devices and ‘D SERVO’ positions, and ‘D RESET’ the command station while maintaining the serial connection with JMRI
|
||||
- Automatic retry on failed ACK detection to give decoders another chance
|
||||
- New EX-RAIL ’/’ slash command allows JMRI to directly communicate with many EX-RAIL scripts
|
||||
- Turnout class revised to expand turnout capabilities and allow turnout names/descriptors to display in WiThrottle Apps.
|
||||
- Build turnouts through either or both mySetup.h and myAutomation.h files, and have them automatically passed to, and populate, JMRI Turnout Tables
|
||||
- Turnout user names display in Engine Driver & WiThrottles
|
||||
- Output class now allows ID > 255.
|
||||
- Configuration options to globally flip polarity of DCC Accessory states when driven from `<a>` command and `<T>` command.
|
||||
- Increased use of display for showing loco decoder programming information.
|
||||
- Can disable EEPROM memory code to allow room for DCC++EX 4.0 to fit on a Uno Command Station
|
||||
- Can define border between long and short addresses
|
||||
- Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms).
|
||||
- EEPROM layout change - deletes EEPROM contents on first start following upgrade.
|
||||
|
||||
**4.0.0 Bug Fixes**
|
||||
|
||||
- Compiles on Nano Every
|
||||
- Diagnostic display of ack pulses >32ms
|
||||
- Current read from wrong ADC during interrupt
|
||||
- AT(+) Command Pass Through
|
||||
- CiDAP WiFi Drop out and the WiThrottle F-key looping error corrected
|
||||
- One-off error in CIPSEND drop
|
||||
- Common Fault Pin Error
|
||||
- Uno Memory Utilization optimized
|
||||
|
||||
#### Summary of Release 3.1.0 key features and/or bug fixes by Point Release
|
||||
#### Summary of key features and/or bug fixes by Point Release
|
||||
|
||||
**Summary of the key new features added to CommandStation-EX V3.0.16**
|
||||
|
||||
@@ -226,7 +154,7 @@ The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production R
|
||||
- Neil McKechnie - Worcestershire, UK (NeilMck)
|
||||
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
|
||||
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
|
||||
- M Steve Todd - Oregon, USA (MSteveTodd)
|
||||
- M Steve Todd -
|
||||
- Scott Catalano - Pennsylvania
|
||||
- Gregor Baues - Île-de-France, France (grbba)
|
||||
|
||||
@@ -246,7 +174,6 @@ The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production R
|
||||
- Roger Beschizza - Dorset, UK (Roger Beschizza)
|
||||
- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)
|
||||
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
|
||||
- Colin Grabham - Central NSW, Australia (Kebbin)
|
||||
|
||||
**WebThrotle-EX**
|
||||
|
||||
@@ -258,14 +185,13 @@ The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production R
|
||||
|
||||
- Larry Dribin - Release Management
|
||||
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
|
||||
- Herb Morton - Kingwood Texas, USA (Ash++)
|
||||
- Keith Ledbetter
|
||||
- Brad Van der Elst
|
||||
- BradVan der Elst
|
||||
- Andrew Pye
|
||||
- Mike Bowers
|
||||
- Randy McKenzie
|
||||
- Roberto Bravin
|
||||
- Sam Brigden
|
||||
- Sim Brigden
|
||||
- Alan Lautenslager
|
||||
- Martin Bafver
|
||||
- Mário André Silva
|
||||
@@ -276,7 +202,5 @@ The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production R
|
||||
|
||||
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
|
||||
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.zip)
|
||||
|
||||
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.tar.gz)
|
||||
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
|
||||
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)
|
||||
|
39
version.h
39
version.h
@@ -3,44 +3,25 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
#define VERSION "4.0.1"
|
||||
// 4.0.1 EXRAIL BROADCAST("msg")
|
||||
// EXRAIL POWERON
|
||||
// 4.0.0 Major functional and non-functional changes.
|
||||
// Engine Driver "DriveAway" feature enhancement
|
||||
// 'Discovered Server' multicast Dynamic Network Server (mDNS) displays available WiFi connections to a DCC++EX Command Station
|
||||
// New EX-RAIL "Extended Railroad Automation Instruction Language" automation capability.
|
||||
// EX-Rail Function commands for creating Automation, Route & Sequence Scripts
|
||||
// EX-RAIL “ROSTER” Engines Id & Function key layout on Engine Driver or WiThrottle
|
||||
// EX-RAIL DCC++EX Commands to Control EX-RAIL via JMRI Send pane and IDE Serial monitors
|
||||
// New JMRI feature enhancements;
|
||||
// Reads DCC++EX EEPROM & automatically uploades any Signals, DCC Turnouts, Servo Turnouts, Vpin Turnouts , & Output pane
|
||||
// Turnout class revised to expand turnout capabilities, new commands added.
|
||||
// Provides for multiple additional DCC++EX WiFi connections as accessory controllers or CS for a programming track when Motor Shields are added
|
||||
// Supports Multiple Command Station connections and individual tracking of Send DCC++ Command panes and DCC++ Traffic Monitor panes
|
||||
// New HAL added for I/O (digital and analogue inputs and outputs, servos etc)
|
||||
// Automatically detects & connects to supported devices included in your config.h file
|
||||
#define VERSION "3.2.0 ESP32"
|
||||
// 3.2.0 Major functional and non-functional changes.
|
||||
// New HAL added for I/O (digital and analogue inputs and outputs, servos etc).
|
||||
// Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.
|
||||
// Support for PCA9685 PWM (servo) control modules.
|
||||
// Support for analogue inputs on Arduino pins and on ADS111x I2C modules.
|
||||
// Support for MP3 sound playback via DFPlayer module.
|
||||
// Support for HC-SR04 Ultrasonic range sensor module.
|
||||
// Support for VL53L0X Laser range sensor module (Time-Of-Flight).
|
||||
// Added <D HAL SHOW> diagnostic command to show configured devices
|
||||
// New Processor Support added
|
||||
// Compiles on Nano Every and Teensy
|
||||
// Native non-blocking I2C drivers for AVR and Nano architectures (fallback to blocking Wire library for other platforms).
|
||||
// Can disable EEPROM code
|
||||
// Native non-blocking I2C drivers for AVR and Nano architectures (fallback
|
||||
// to blocking Wire library for other platforms).
|
||||
// EEPROM layout change - deletes EEPROM contents on first start following upgrade.
|
||||
// New EX-RAIL automation capability.
|
||||
// Turnout class revised to expand turnout capabilities, new commands added.
|
||||
// Output class now allows ID > 255.
|
||||
// Configuration options to globally flip polarity of DCC Accessory states when driven from <a> command and <T> command.
|
||||
// Configuration options to globally flip polarity of DCC Accessory states when driven
|
||||
// from <a> command and <T> command.
|
||||
// Increased use of display for showing loco decoder programming information.
|
||||
// Can define border between long and short addresses
|
||||
// Turnout and accessory states (thrown/closed = 0/1 or 1/0) can be set to match RCN-213
|
||||
// Bugfix: one-off error in CIPSEND drop
|
||||
// Bugfix: disgnostic display of ack pulses >32kus
|
||||
// Bugfix: Current read from wrong ADC during interrupt
|
||||
// 3.2.0 Development Release Includes all of 3.1.1 thru 3.1.7 enhancements
|
||||
// ...
|
||||
// 3.1.7 Bugfix: Unknown locos should have speed forward
|
||||
// 3.1.6 Make output ID two bytes and guess format/size of registered outputs found in EEPROM
|
||||
// 3.1.5 Fix LCD corruption on power-up
|
||||
|
Reference in New Issue
Block a user