mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2025-02-19 23:46:02 +01:00
Merge pull request #203 from DCC-EX/Temp-Ash
Adds EXRAIL and other changes since 3.1.6 into ESP88-Ash
This commit is contained in:
commit
92e8376fc7
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,3 +9,10 @@ Release/*
|
||||
config.h
|
||||
.vscode/extensions.json
|
||||
mySetup.h
|
||||
mySetup.cpp
|
||||
myHal.cpp
|
||||
myAutomation.h
|
||||
myFilter.cpp
|
||||
myAutomation.h
|
||||
myFilter.cpp
|
||||
myLayout.h
|
||||
|
@ -1,6 +1,9 @@
|
||||
/*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@ -16,16 +19,119 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "CommandDistributor.h"
|
||||
#include "SerialManager.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DIAG.h"
|
||||
#include "defines.h"
|
||||
#include "DCCWaveform.h"
|
||||
#include "DCC.h"
|
||||
|
||||
DCCEXParser * CommandDistributor::parser=0;
|
||||
#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;
|
||||
|
||||
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * streamer) {
|
||||
if (buffer[0] == '<') {
|
||||
if (!parser) parser = new DCCEXParser();
|
||||
parser->parse(streamer, buffer, streamer);
|
||||
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);
|
||||
}
|
||||
else WiThrottle::getThrottle(clientId)->parse(streamer, buffer);
|
||||
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);
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
/*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
*
|
||||
* © 2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@ -24,9 +27,21 @@
|
||||
class CommandDistributor {
|
||||
|
||||
public :
|
||||
static void parse(byte clientId,byte* buffer, RingStream * streamer);
|
||||
static void parse(byte clientId,byte* buffer, RingStream * ring);
|
||||
static void broadcastLoco(byte slot);
|
||||
static void broadcastSensor(int16_t id, bool value);
|
||||
static void broadcastTurnout(int16_t id, bool isClosed);
|
||||
static void broadcastPower();
|
||||
static void forget(byte clientId);
|
||||
private:
|
||||
static DCCEXParser * parser;
|
||||
static void broadcast(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];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -1,33 +1,35 @@
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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
|
||||
|
||||
|
||||
/*
|
||||
* © 2020,2021 Chris Harlow, Harald Barth, David Cutting,
|
||||
* Fred Decker, Gregor Baues, Anthony W - Dayton All rights reserved.
|
||||
*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 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
|
||||
@ -44,13 +46,16 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#include "DCCEX.h"
|
||||
|
||||
// Create a serial command parser for the USB connection,
|
||||
// This supports JMRI or manual diagnostics and commands
|
||||
// to be issued from the USB serial console.
|
||||
DCCEXParser serialParser;
|
||||
#ifdef WIFI_WARNING
|
||||
#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED
|
||||
#endif
|
||||
#ifdef ETHERNET_WARNING
|
||||
#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED
|
||||
#endif
|
||||
#ifdef EXRAIL_WARNING
|
||||
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
|
||||
#endif
|
||||
|
||||
void setup()
|
||||
{
|
||||
@ -58,7 +63,9 @@ void setup()
|
||||
|
||||
// Responsibility 1: Start the usb connection for diagnostics
|
||||
// This is normally Serial but uses SerialUSB on a SAMD processor
|
||||
Serial.begin(115200);
|
||||
|
||||
SerialManager::init();
|
||||
// Serial.begin(115200); // check this -- removed when resolve conflicts
|
||||
#ifdef ESP_DEBUG
|
||||
Serial.setDebugOutput(true);
|
||||
#endif
|
||||
@ -66,10 +73,10 @@ void setup()
|
||||
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
|
||||
@ -92,22 +99,25 @@ void setup()
|
||||
// STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h
|
||||
DCC::begin(MOTOR_SHIELD_TYPE);
|
||||
|
||||
#if defined(RMFT_ACTIVE)
|
||||
RMFT::begin();
|
||||
#endif
|
||||
// Start RMFT aka EX-RAIL (ignored if no automnation)
|
||||
RMFT::begin();
|
||||
|
||||
|
||||
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
|
||||
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
|
||||
#if __has_include ( "mySetup.h")
|
||||
#define SETUP(cmd) serialParser.parse(F(cmd))
|
||||
#include "mySetup.h"
|
||||
#undef SETUP
|
||||
#endif
|
||||
#define SETUP(cmd) DCCEXParser::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(1,F("Ready"));
|
||||
LCD(3,F("Ready"));
|
||||
CommandDistributor::broadcastPower();
|
||||
}
|
||||
|
||||
void loop()
|
||||
@ -118,9 +128,9 @@ void loop()
|
||||
// (loco reminders and power checks)
|
||||
DCC::loop();
|
||||
// Responsibility 2: handle any incoming commands on USB connection
|
||||
serialParser.loop(Serial);
|
||||
SerialManager::loop();
|
||||
|
||||
// Responsibility 3: Optionally handle any incoming WiFi traffic
|
||||
// Responsibility 3: Optionally handle any incoming WiFi traffic
|
||||
#if WIFI_ON
|
||||
#ifndef ESP_FAMILY
|
||||
WifiInterface::loop();
|
||||
@ -132,25 +142,27 @@ void loop()
|
||||
EthernetInterface::loop();
|
||||
#endif
|
||||
|
||||
#if defined(RMFT_ACTIVE)
|
||||
RMFT::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
|
||||
#ifdef ESP_FAMILY
|
||||
updateMinimumFreeMemory(128);
|
||||
#endif
|
||||
int freeNow = minimumFreeMemory();
|
||||
if (freeNow < ramLowWatermark)
|
||||
{
|
||||
if (freeNow < ramLowWatermark) {
|
||||
ramLowWatermark = freeNow;
|
||||
LCD(2,F("Free RAM=%5db"), ramLowWatermark);
|
||||
LCD(3,F("Free RAM=%5db"), ramLowWatermark);
|
||||
}
|
||||
}
|
||||
|
408
DCC.cpp
408
DCC.cpp
@ -1,7 +1,13 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth
|
||||
*
|
||||
* © 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.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@ -20,10 +26,15 @@
|
||||
#include "DIAG.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "EEStore.h"
|
||||
#endif
|
||||
#include "GITHUB_SHA.h"
|
||||
#include "version.h"
|
||||
#include "FSH.h"
|
||||
#include "IODevice.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
// This module is responsible for converting API calls into
|
||||
// messages to be sent to the waveform generator.
|
||||
@ -38,11 +49,11 @@
|
||||
// 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;
|
||||
@ -52,11 +63,16 @@ void DCC::begin(const FSH * motorShieldName, MotorDriver * mainDriver, MotorDriv
|
||||
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));
|
||||
|
||||
// Initialise HAL layer before reading EEprom.
|
||||
IODevice::begin();
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Load stuff from EEprom
|
||||
(void)EEPROM; // tell compiler not to warn this is unused
|
||||
EEStore::init();
|
||||
#endif
|
||||
|
||||
DCCWaveform::begin(mainDriver,progDriver);
|
||||
DCCWaveform::begin(mainDriver,progDriver);
|
||||
}
|
||||
|
||||
void DCC::setJoinRelayPin(byte joinRelayPin) {
|
||||
@ -68,7 +84,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 );
|
||||
@ -79,8 +95,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 > 127)
|
||||
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
b[nB++] = lowByte(cab);
|
||||
|
||||
@ -120,7 +136,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
|
||||
byte b[4];
|
||||
byte nB = 0;
|
||||
|
||||
if (cab > 127)
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
b[nB++] = lowByte(cab);
|
||||
if (byte1!=0) b[nB++] = byte1;
|
||||
@ -137,93 +153,74 @@ uint8_t DCC::getThrottleSpeed(int cab) {
|
||||
|
||||
bool DCC::getThrottleDirection(int cab) {
|
||||
int reg=lookupSpeedTable(cab);
|
||||
if (reg<0) return false ;
|
||||
if (reg<0) return true;
|
||||
return (speedTable[reg].speedCode & 0x80) !=0;
|
||||
}
|
||||
|
||||
// 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 > 127)
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
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);
|
||||
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;
|
||||
}
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
return;
|
||||
if (speedTable[reg].functions != previous) {
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
|
||||
// Change function according to how button was pressed,
|
||||
// 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;
|
||||
// Flip function state
|
||||
void DCC::changeFn( int cab, int16_t functionNumber) {
|
||||
if (cab<=0 || functionNumber>28) return;
|
||||
int reg = lookupSpeedTable(cab);
|
||||
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
|
||||
if (reg<0) return;
|
||||
unsigned long funcmask = (1UL<<functionNumber);
|
||||
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;
|
||||
}
|
||||
speedTable[reg].functions ^= funcmask;
|
||||
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
|
||||
return funcstate;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -231,10 +228,19 @@ 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;
|
||||
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;
|
||||
}
|
||||
|
||||
void DCC::setAccessory(int address, byte number, bool activate) {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, number, activate);
|
||||
#endif
|
||||
// use masks to detect wrong values and do nothing
|
||||
if(address != (address & 511))
|
||||
return;
|
||||
@ -246,6 +252,9 @@ void DCC::setAccessory(int address, byte number, bool activate) {
|
||||
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
|
||||
}
|
||||
|
||||
//
|
||||
@ -255,7 +264,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 > 127)
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
|
||||
b[nB++] = lowByte(cab);
|
||||
@ -276,7 +285,7 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
|
||||
bValue = bValue % 2;
|
||||
bNum = bNum % 8;
|
||||
|
||||
if (cab > 127)
|
||||
if (cab > HIGHEST_SHORT_ADDR)
|
||||
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
|
||||
|
||||
b[nB++] = lowByte(cab);
|
||||
@ -298,25 +307,25 @@ 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)
|
||||
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)
|
||||
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,
|
||||
@ -324,22 +333,22 @@ const ackOp FLASH VERIFY_BIT0_PROG[] = {
|
||||
};
|
||||
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,
|
||||
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
|
||||
CALLFAIL // bit not readable
|
||||
};
|
||||
|
||||
|
||||
const ackOp FLASH WRITE_BYTE_PROG[] = {
|
||||
BASELINE,
|
||||
WB,WACK,ITC1, // Write and callback(1) if ACK
|
||||
@ -347,14 +356,15 @@ const ackOp FLASH WRITE_BYTE_PROG[] = {
|
||||
VB,WACK,ITC1, // validate byte and callback(1) if correct
|
||||
CALLFAIL // callback (-1)
|
||||
};
|
||||
|
||||
|
||||
const ackOp FLASH VERIFY_BYTE_PROG[] = {
|
||||
BASELINE,
|
||||
VB,WACK, // validate byte
|
||||
ITCB, // if ok callback value
|
||||
BIV, // ackManagerByte initial value
|
||||
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
|
||||
@ -372,12 +382,12 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = {
|
||||
VB, WACK, ITCB, // verify merged byte and return it if acked ok
|
||||
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
|
||||
@ -399,13 +409,13 @@ const ackOp FLASH READ_CV_PROG[] = {
|
||||
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,
|
||||
@ -413,13 +423,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
|
||||
@ -431,8 +441,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
|
||||
@ -445,8 +455,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,
|
||||
@ -472,8 +482,8 @@ 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,
|
||||
CALLFAIL
|
||||
@ -492,12 +502,12 @@ 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
|
||||
CALLFAIL
|
||||
@ -540,24 +550,24 @@ void DCC::setLocoId(int id,ACK_CALLBACK callback) {
|
||||
callback(-1);
|
||||
return;
|
||||
}
|
||||
if (id<=127)
|
||||
if (id<=HIGHEST_SHORT_ADDR)
|
||||
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
|
||||
@ -572,58 +582,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 /////////////////////
|
||||
@ -658,20 +668,28 @@ 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++) {
|
||||
speedTable[reg].speedCode = (speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].loco==0) continue;
|
||||
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
|
||||
if (speedTable[reg].speedCode != newspeed) {
|
||||
speedTable[reg].speedCode = newspeed;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// determine speed reg for this loco
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0) speedTable[reg].speedCode = speedCode;
|
||||
int reg=lookupSpeedTable(loco);
|
||||
if (reg>=0 && speedTable[reg].speedCode!=speedCode) {
|
||||
speedTable[reg].speedCode = speedCode;
|
||||
CommandDistributor::broadcastLoco(reg);
|
||||
}
|
||||
}
|
||||
|
||||
DCC::LOCO DCC::speedTable[MAX_LOCOS];
|
||||
@ -679,9 +697,15 @@ int DCC::nextLoco = 0;
|
||||
|
||||
//ACK MANAGER
|
||||
ackOp const * DCC::ackManagerProg;
|
||||
ackOp const * DCC::ackManagerProgStart;
|
||||
byte DCC::ackManagerByte;
|
||||
byte DCC::ackManagerByteVerify;
|
||||
byte DCC::ackManagerStash;
|
||||
int DCC::ackManagerWord;
|
||||
byte DCC::ackManagerRetry;
|
||||
byte DCC::ackRetry = 2;
|
||||
int16_t DCC::ackRetrySum;
|
||||
int16_t DCC::ackRetryPSum;
|
||||
int DCC::ackManagerCv;
|
||||
byte DCC::ackManagerBitNum;
|
||||
bool DCC::ackReceived;
|
||||
@ -697,24 +721,29 @@ 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);
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
if (MotorDriver::commonFaultPin)
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
||||
DCCWaveform::progTrack.sentResetsSincePacket = 0;
|
||||
}
|
||||
|
||||
ackManagerCv = cv;
|
||||
ackManagerProg = program;
|
||||
ackManagerProgStart = program;
|
||||
ackManagerRetry = ackRetry;
|
||||
ackManagerByte = byteValueOrBitnum;
|
||||
ackManagerByteVerify = byteValueOrBitnum;
|
||||
ackManagerBitNum=byteValueOrBitnum;
|
||||
ackManagerCallback = callback;
|
||||
}
|
||||
@ -734,66 +763,67 @@ 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.)
|
||||
switch (opcode) {
|
||||
case BASELINE:
|
||||
if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return;
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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;
|
||||
@ -806,21 +836,32 @@ 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) {
|
||||
ackRetrySum ++;
|
||||
LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum);
|
||||
}
|
||||
callback(ackManagerByte);
|
||||
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);
|
||||
@ -831,78 +872,91 @@ void DCC::ackManagerLoop() {
|
||||
case CALLFAIL: // callback(-1)
|
||||
callback(-1);
|
||||
return;
|
||||
|
||||
|
||||
case BIV: // ackManagerByte initial value
|
||||
ackManagerByte = ackManagerByteVerify;
|
||||
break;
|
||||
|
||||
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( ackManagerByte + ((ackManagerStash - 192) << 8));
|
||||
return;
|
||||
callback( LONG_ADDR_MARKER | ( 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++;
|
||||
}
|
||||
}
|
||||
|
||||
void DCC::callback(int value) {
|
||||
// check for automatic retry
|
||||
if (value == -1 && ackManagerRetry > 0) {
|
||||
ackRetrySum ++;
|
||||
LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum);
|
||||
ackManagerRetry --;
|
||||
ackManagerProg = ackManagerProgStart;
|
||||
return;
|
||||
}
|
||||
|
||||
static unsigned long callbackStart;
|
||||
// We are about to leave programming mode
|
||||
// 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;
|
||||
@ -912,7 +966,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
|
||||
@ -921,32 +975,34 @@ 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);
|
||||
@ -959,10 +1015,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);
|
||||
|
||||
|
||||
}
|
||||
|
43
DCC.h
43
DCC.h
@ -1,5 +1,10 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Herb Morton
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -23,6 +28,16 @@
|
||||
#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
|
||||
@ -38,9 +53,11 @@ enum ackOp : byte
|
||||
ITC1, // If True Callback(1) (if prevous WACK got an ACK)
|
||||
ITC0, // If True callback(0);
|
||||
ITCB, // If True callback(byte)
|
||||
ITCBV, // If True callback(byte) - end of Verify Byte
|
||||
ITCB7, // If True callback(byte &0x7F)
|
||||
NAKFAIL, // if false 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)
|
||||
SETBIT, // sets bit number to next prog byte
|
||||
@ -64,8 +81,10 @@ enum CALLBACK_STATE : byte {
|
||||
|
||||
// Allocations with memory implications..!
|
||||
// Base system takes approx 900 bytes + 8 per loco. Turnouts, Sensors etc are dynamically created
|
||||
#ifdef ARDUINO_AVR_UNO
|
||||
#if defined(ARDUINO_AVR_UNO)
|
||||
const byte MAX_LOCOS = 20;
|
||||
#elif defined(ARDUINO_AVR_NANO)
|
||||
const byte MAX_LOCOS = 30;
|
||||
#else
|
||||
const byte MAX_LOCOS = 50;
|
||||
#endif
|
||||
@ -85,8 +104,9 @@ 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 int changeFn(int cab, int16_t functionNumber, bool pressed);
|
||||
static void changeFn(int cab, int16_t functionNumber);
|
||||
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);
|
||||
@ -113,8 +133,13 @@ public:
|
||||
static inline void setGlobalSpeedsteps(byte s) {
|
||||
globalSpeedsteps = s;
|
||||
};
|
||||
static inline int16_t setAckRetry(byte retry) {
|
||||
ackRetry = retry;
|
||||
ackRetryPSum = ackRetrySum;
|
||||
ackRetrySum = 0; // reset running total
|
||||
return ackRetryPSum;
|
||||
};
|
||||
|
||||
private:
|
||||
struct LOCO
|
||||
{
|
||||
int loco;
|
||||
@ -122,6 +147,9 @@ private:
|
||||
byte groupFlags;
|
||||
unsigned long functions;
|
||||
};
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
|
||||
private:
|
||||
static byte joinRelay;
|
||||
static byte loopStatus;
|
||||
static void setThrottle2(uint16_t cab, uint8_t speedCode);
|
||||
@ -132,7 +160,6 @@ private:
|
||||
static FSH *shieldName;
|
||||
static byte globalSpeedsteps;
|
||||
|
||||
static LOCO speedTable[MAX_LOCOS];
|
||||
static byte cv1(byte opcode, int cv);
|
||||
static byte cv2(int cv);
|
||||
static int lookupSpeedTable(int locoId);
|
||||
@ -141,9 +168,15 @@ private:
|
||||
|
||||
// ACK MANAGER
|
||||
static ackOp const *ackManagerProg;
|
||||
static ackOp const *ackManagerProgStart;
|
||||
static byte ackManagerByte;
|
||||
static byte ackManagerByteVerify;
|
||||
static byte ackManagerBitNum;
|
||||
static int ackManagerCv;
|
||||
static byte ackManagerRetry;
|
||||
static byte ackRetry;
|
||||
static int16_t ackRetrySum;
|
||||
static int16_t ackRetryPSum;
|
||||
static int ackManagerWord;
|
||||
static byte ackManagerStash;
|
||||
static bool ackReceived;
|
||||
|
19
DCCEX.h
19
DCCEX.h
@ -1,7 +1,8 @@
|
||||
/*
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020 Harald Barth. All rights reserved.
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -29,6 +30,7 @@
|
||||
#include "DCC.h"
|
||||
#include "DIAG.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include "SerialManager.h"
|
||||
#include "version.h"
|
||||
#include "WifiInterface.h"
|
||||
#ifdef ESP_FAMILY
|
||||
@ -40,10 +42,11 @@
|
||||
#include "LCD_Implementation.h"
|
||||
#include "LCN.h"
|
||||
#include "freeMemory.h"
|
||||
|
||||
#if __has_include ( "myAutomation.h")
|
||||
#include "RMFT.h"
|
||||
#define RMFT_ACTIVE
|
||||
#endif
|
||||
#include "IODevice.h"
|
||||
#include "Turnouts.h"
|
||||
#include "Sensors.h"
|
||||
#include "Outputs.h"
|
||||
#include "EXRAIL.h"
|
||||
#include "CommandDistributor.h"
|
||||
|
||||
#endif
|
||||
|
434
DCCEXParser.cpp
434
DCCEXParser.cpp
@ -1,6 +1,12 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* © 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.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -28,13 +34,24 @@
|
||||
#include "freeMemory.h"
|
||||
#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
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Figure out if we have enough memory for advanced features
|
||||
//
|
||||
#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO)
|
||||
// nope
|
||||
#else
|
||||
#define HAS_ENOUGH_MEMORY
|
||||
#endif
|
||||
|
||||
// These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter.
|
||||
// To discover new keyword numbers , use the <$ YOURKEYWORD> command
|
||||
const int16_t HASH_KEYWORD_PROG = -29718;
|
||||
@ -43,26 +60,36 @@ const int16_t HASH_KEYWORD_JOIN = -30750;
|
||||
const int16_t HASH_KEYWORD_CABS = -11981;
|
||||
const int16_t HASH_KEYWORD_RAM = 25982;
|
||||
const int16_t HASH_KEYWORD_CMD = 9962;
|
||||
const int16_t HASH_KEYWORD_WIT = 31594;
|
||||
const int16_t HASH_KEYWORD_WIFI = -5583;
|
||||
const int16_t HASH_KEYWORD_ACK = 3113;
|
||||
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_ETHERNET = -30767;
|
||||
const int16_t HASH_KEYWORD_MAX = 16244;
|
||||
const int16_t HASH_KEYWORD_MIN = 15978;
|
||||
const int16_t HASH_KEYWORD_LCN = 15137;
|
||||
const int16_t HASH_KEYWORD_RESET = 26133;
|
||||
const int16_t HASH_KEYWORD_RETRY = 25704;
|
||||
const int16_t HASH_KEYWORD_SPEED28 = -17064;
|
||||
const int16_t HASH_KEYWORD_SPEED128 = 25816;
|
||||
const int16_t HASH_KEYWORD_SERVO=27709;
|
||||
const int16_t HASH_KEYWORD_VPIN=-415;
|
||||
const int16_t HASH_KEYWORD_C=67;
|
||||
const int16_t HASH_KEYWORD_T=84;
|
||||
const int16_t HASH_KEYWORD_LCN = 15137;
|
||||
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;
|
||||
const int16_t HASH_KEYWORD_WIFI = -5583;
|
||||
const int16_t HASH_KEYWORD_ETHERNET = -30767;
|
||||
const int16_t HASH_KEYWORD_WIT = 31594;
|
||||
|
||||
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
|
||||
bool DCCEXParser::stashBusy;
|
||||
|
||||
Print *DCCEXParser::stashStream = NULL;
|
||||
RingStream *DCCEXParser::stashRingStream = NULL;
|
||||
byte DCCEXParser::stashTarget=0;
|
||||
@ -73,46 +100,8 @@ byte DCCEXParser::stashTarget=0;
|
||||
// calls the corresponding DCC api.
|
||||
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.
|
||||
|
||||
DCCEXParser::DCCEXParser() {}
|
||||
void DCCEXParser::flush()
|
||||
{
|
||||
if (Diag::CMD)
|
||||
DIAG(F("Buffer flush"));
|
||||
bufferLength = 0;
|
||||
inCommandPayload = false;
|
||||
}
|
||||
|
||||
void DCCEXParser::loop(Stream &stream)
|
||||
{
|
||||
while (stream.available())
|
||||
{
|
||||
if (bufferLength == MAX_BUFFER)
|
||||
{
|
||||
flush();
|
||||
}
|
||||
char ch = stream.read();
|
||||
if (ch == '<')
|
||||
{
|
||||
inCommandPayload = true;
|
||||
bufferLength = 0;
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
else if (ch == '>')
|
||||
{
|
||||
buffer[bufferLength] = '\0';
|
||||
parse(&stream, buffer, NULL); // Parse this (No ringStream for serial)
|
||||
inCommandPayload = false;
|
||||
break;
|
||||
}
|
||||
else if (inCommandPayload)
|
||||
{
|
||||
buffer[bufferLength++] = ch;
|
||||
}
|
||||
}
|
||||
Sensor::checkAll(&stream); // Update and print changes
|
||||
}
|
||||
|
||||
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
|
||||
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd, bool usehex)
|
||||
{
|
||||
byte state = 1;
|
||||
byte parameterCount = 0;
|
||||
@ -150,10 +139,15 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
|
||||
case 3: // building a parameter
|
||||
if (hot >= '0' && hot <= '9')
|
||||
{
|
||||
runningValue = 10 * runningValue + (hot - '0');
|
||||
runningValue = (usehex?16: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
|
||||
@ -171,66 +165,6 @@ 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;
|
||||
@ -249,6 +183,7 @@ void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
|
||||
|
||||
// Parse an F() string
|
||||
void DCCEXParser::parse(const FSH * cmd) {
|
||||
DIAG(F("SETUP(\"%S\")"),cmd);
|
||||
int size=strlen_P((char *)cmd)+1;
|
||||
char buffer[size];
|
||||
strcpy_P(buffer,(char *)cmd);
|
||||
@ -256,17 +191,20 @@ void DCCEXParser::parse(const FSH * cmd) {
|
||||
}
|
||||
|
||||
// See documentation on DCC class for info on this section
|
||||
|
||||
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')
|
||||
@ -314,10 +252,9 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
break; // invalid direction code
|
||||
|
||||
DCC::setThrottle(cab, tspeed, direction);
|
||||
if (params == 4)
|
||||
if (params == 4) // send obsolete format T response
|
||||
StringFormatter::send(stream, F("<T %d %d %d>\n"), p[0], p[2], p[3]);
|
||||
else
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
// speed change will be broadcast anyway in new <l > format
|
||||
return;
|
||||
}
|
||||
case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>
|
||||
@ -347,8 +284,12 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
|| ((subaddress & 0x03) != subaddress) // invalid subaddress (limit 2 bits )
|
||||
|| ((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
|
||||
DCC::setAccessory(address, subaddress,p[activep]==0);
|
||||
#else
|
||||
DCC::setAccessory(address, subaddress,p[activep]==1);
|
||||
#endif
|
||||
}
|
||||
return;
|
||||
|
||||
@ -377,8 +318,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>
|
||||
// Re-parse the command using a hex-only splitter
|
||||
params=splitHexValues(p,com)-1; // drop REG
|
||||
// NOTE: this command was parsed in HEX instead of decimal
|
||||
params--; // drop REG
|
||||
if (params<1) break;
|
||||
{
|
||||
byte packet[params];
|
||||
@ -439,54 +380,70 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|
||||
}
|
||||
break;
|
||||
|
||||
case '1': // POWERON <1 [MAIN|PROG]>
|
||||
case '0': // POWEROFF <0 [MAIN | PROG] >
|
||||
if (params > 1)
|
||||
break;
|
||||
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
switch (p[0])
|
||||
{
|
||||
case HASH_KEYWORD_MAIN:
|
||||
DCCWaveform::mainTrack.setPowerMode(mode);
|
||||
StringFormatter::send(stream, F("<p%c MAIN>\n"), opcode);
|
||||
return;
|
||||
|
||||
case HASH_KEYWORD_PROG:
|
||||
DCCWaveform::progTrack.setPowerMode(mode);
|
||||
if (mode == POWERMODE::OFF)
|
||||
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
|
||||
StringFormatter::send(stream, F("<p%c PROG>\n"), opcode);
|
||||
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);
|
||||
}
|
||||
else
|
||||
StringFormatter::send(stream, F("<p0>\n"));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
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 '0': // POWEROFF <0 [MAIN | PROG] >
|
||||
{
|
||||
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>
|
||||
}
|
||||
|
||||
if (main) DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
if (prog) {
|
||||
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
}
|
||||
DCC::setProgTrackSyncMain(false);
|
||||
|
||||
CommandDistributor::broadcastPower();
|
||||
return;
|
||||
}
|
||||
|
||||
case '!': // ESTOP ALL <!>
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
|
||||
return;
|
||||
|
||||
case 'c': // SEND METER RESPONSES <c>
|
||||
@ -509,6 +466,7 @@ 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);
|
||||
@ -518,7 +476,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;
|
||||
@ -539,16 +497,17 @@ 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) {
|
||||
if (atCommandCallback && !ringStream) {
|
||||
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
||||
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
|
||||
atCommandCallback(com);
|
||||
atCommandCallback((HardwareSerial *)stream,com);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@ -582,10 +541,8 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
|
||||
return true;
|
||||
|
||||
case 3: // <Z ID PIN IFLAG>
|
||||
if (p[0] < 0 ||
|
||||
p[1] > 255 || p[1] <= 1 || // Pins 0 and 1 are Serial to USB
|
||||
p[2] < 0 || p[2] > 7 )
|
||||
return false;
|
||||
if (p[0] < 0 || p[2] < 0 || p[2] > 7 )
|
||||
return false;
|
||||
if (!Output::create(p[0], p[1], p[2], 1))
|
||||
return false;
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
@ -603,7 +560,7 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
|
||||
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
|
||||
{
|
||||
gotone = true;
|
||||
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.iFlag, tt->data.oStatus);
|
||||
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.flags, tt->data.active);
|
||||
}
|
||||
return gotone;
|
||||
}
|
||||
@ -662,11 +619,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
|
||||
case 0: // <T> list turnout definitions
|
||||
{
|
||||
bool gotOne = false;
|
||||
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
|
||||
for (Turnout *tt = Turnout::first(); tt != NULL; tt = tt->next())
|
||||
{
|
||||
gotOne = true;
|
||||
StringFormatter::send(stream, F("<H %d %d %d %d>\n"), tt->data.id, tt->data.address,
|
||||
tt->data.subAddress, (tt->data.tStatus & STATUS_ACTIVE)!=0);
|
||||
tt->print(stream);
|
||||
}
|
||||
return gotOne; // will <X> if none found
|
||||
}
|
||||
@ -677,24 +633,60 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
return true;
|
||||
|
||||
case 2: // <T id 0|1> activate turnout
|
||||
{
|
||||
Turnout *tt = Turnout::get(p[0]);
|
||||
if (!tt)
|
||||
return false;
|
||||
tt->activate(p[1]);
|
||||
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
|
||||
}
|
||||
return true;
|
||||
case 2: // <T id 0|1|T|C>
|
||||
{
|
||||
bool state = false;
|
||||
switch (p[1]) {
|
||||
// Turnout messages use 1=throw, 0=close.
|
||||
case 0:
|
||||
case HASH_KEYWORD_C:
|
||||
state = true;
|
||||
break;
|
||||
case 1:
|
||||
case HASH_KEYWORD_T:
|
||||
state= false;
|
||||
break;
|
||||
default:
|
||||
return false; // Invalid parameter
|
||||
}
|
||||
if (!Turnout::setClosed(p[0], state)) return false;
|
||||
|
||||
case 3: // <T id addr subaddr> define turnout
|
||||
if (!Turnout::create(p[0], p[1], p[2]))
|
||||
return false;
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false; // will <x>
|
||||
return true;
|
||||
}
|
||||
|
||||
default: // Anything else is some kind of turnout create function.
|
||||
if (params == 6 && p[1] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n>
|
||||
if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
|
||||
return false;
|
||||
} else
|
||||
if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
|
||||
if (!VpinTurnout::create(p[0], p[2])) return false;
|
||||
} else
|
||||
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) {
|
||||
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
|
||||
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
|
||||
if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
|
||||
} else if (params==3 && p[2]>0 && p[2]<=512*4) { // <T id DCC nn>, 1<=nn<=2048
|
||||
// Linearaddress 1 maps onto decoder address 1/0 (not 0/0!).
|
||||
if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false;
|
||||
} else
|
||||
return false;
|
||||
} else
|
||||
if (params==3) { // legacy <T id addr subadd> for DCC accessory
|
||||
if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) {
|
||||
if (!DCCTurnout::create(p[0], p[1], p[2])) return false;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
if (params==4) { // legacy <T id n n n> for Servo
|
||||
if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false;
|
||||
} else
|
||||
return false;
|
||||
|
||||
StringFormatter::send(stream, F("<O>\n"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,13 +708,13 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
|
||||
return true;
|
||||
|
||||
case 0: // <S> list sensor definitions
|
||||
if (Sensor::firstSensor == NULL)
|
||||
return false;
|
||||
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
|
||||
{
|
||||
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
|
||||
}
|
||||
return true;
|
||||
if (Sensor::firstSensor == NULL)
|
||||
return false;
|
||||
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
|
||||
{
|
||||
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
|
||||
}
|
||||
return true;
|
||||
|
||||
default: // invalid number of arguments
|
||||
break;
|
||||
@ -745,17 +737,20 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
StringFormatter::send(stream, F("Free memory=%d\n"), minimumFreeMemory());
|
||||
break;
|
||||
|
||||
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX] Value>
|
||||
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
|
||||
if (params >= 3) {
|
||||
if (p[1] == HASH_KEYWORD_LIMIT) {
|
||||
DCCWaveform::progTrack.setAckLimit(p[2]);
|
||||
StringFormatter::send(stream, F("Ack limit=%dmA\n"), p[2]);
|
||||
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
|
||||
} else if (p[1] == HASH_KEYWORD_MIN) {
|
||||
DCCWaveform::progTrack.setMinAckPulseDuration(p[2]);
|
||||
StringFormatter::send(stream, F("Ack min=%dus\n"), p[2]);
|
||||
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
|
||||
} else if (p[1] == HASH_KEYWORD_MAX) {
|
||||
DCCWaveform::progTrack.setMaxAckPulseDuration(p[2]);
|
||||
StringFormatter::send(stream, F("Ack max=%dus\n"), p[2]);
|
||||
LCD(0, F("Ack Max=%uus"), 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>
|
||||
}
|
||||
} else {
|
||||
StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off"));
|
||||
@ -767,21 +762,23 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
Diag::CMD = onOff;
|
||||
return true;
|
||||
|
||||
#ifdef HAS_ENOUGH_MEMORY
|
||||
case HASH_KEYWORD_WIFI: // <D WIFI ON/OFF>
|
||||
Diag::WIFI = onOff;
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
|
||||
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
|
||||
Diag::ETHERNET = onOff;
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_WIT: // <D WIT ON/OFF>
|
||||
Diag::WITHROTTLE = onOff;
|
||||
return true;
|
||||
|
||||
|
||||
case HASH_KEYWORD_LCN: // <D LCN ON/OFF>
|
||||
Diag::LCN = onOff;
|
||||
return true;
|
||||
#endif
|
||||
|
||||
case HASH_KEYWORD_PROGBOOST:
|
||||
DCC::setProgTrackBoost(true);
|
||||
@ -793,11 +790,13 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
delay(50); // wait for the prescaller time to expire
|
||||
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);
|
||||
@ -809,6 +808,22 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
|
||||
StringFormatter::send(stream, F("128 Speedsteps"));
|
||||
return true;
|
||||
|
||||
case HASH_KEYWORD_SERVO: // <D SERVO vpin position [profile]>
|
||||
case HASH_KEYWORD_ANOUT: // <D ANOUT vpin position [profile]>
|
||||
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
|
||||
break;
|
||||
|
||||
case HASH_KEYWORD_ANIN: // <D ANIN vpin> Display analogue input value
|
||||
DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1]));
|
||||
break;
|
||||
|
||||
#if !defined(IO_MINIMAL_HAL)
|
||||
case HASH_KEYWORD_HAL:
|
||||
if (p[1] == HASH_KEYWORD_SHOW)
|
||||
IODevice::DumpAll();
|
||||
break;
|
||||
#endif
|
||||
|
||||
default: // invalid/unknown
|
||||
break;
|
||||
}
|
||||
@ -871,10 +886,21 @@ void DCCEXParser::callback_R(int16_t result)
|
||||
commitAsyncReplyStream();
|
||||
}
|
||||
|
||||
void DCCEXParser::callback_Rloco(int16_t result)
|
||||
{
|
||||
StringFormatter::send(getAsyncReplyStream(), F("<r %d>\n"), 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_Wloco(int16_t result)
|
||||
|
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -23,15 +26,13 @@
|
||||
#include "RingStream.h"
|
||||
|
||||
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
|
||||
typedef void (*AT_COMMAND_CALLBACK)(const byte * command);
|
||||
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
|
||||
|
||||
struct DCCEXParser
|
||||
{
|
||||
DCCEXParser();
|
||||
void loop(Stream & stream);
|
||||
void parse(Print * stream, byte * command, RingStream * ringStream);
|
||||
void parse(const FSH * cmd);
|
||||
void flush();
|
||||
|
||||
static void parse(Print * stream, byte * command, RingStream * ringStream);
|
||||
static void parse(const FSH * cmd);
|
||||
static void setFilter(FILTER_CALLBACK filter);
|
||||
static void setRMFTFilter(FILTER_CALLBACK filter);
|
||||
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
|
||||
@ -40,17 +41,13 @@ struct DCCEXParser
|
||||
private:
|
||||
|
||||
static const int16_t MAX_BUFFER=50; // longest command sent in
|
||||
byte bufferLength=0;
|
||||
bool inCommandPayload=false;
|
||||
byte buffer[MAX_BUFFER+2];
|
||||
int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
|
||||
int16_t splitHexValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
|
||||
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
|
||||
|
||||
bool parseT(Print * stream, int16_t params, int16_t p[]);
|
||||
bool parseZ(Print * stream, int16_t params, int16_t p[]);
|
||||
bool parseS(Print * stream, int16_t params, int16_t p[]);
|
||||
bool parsef(Print * stream, int16_t params, int16_t p[]);
|
||||
bool parseD(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseT(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseS(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parsef(Print * stream, int16_t params, int16_t p[]);
|
||||
static bool parseD(Print * stream, int16_t params, int16_t p[]);
|
||||
|
||||
static Print * getAsyncReplyStream();
|
||||
static void commitAsyncReplyStream();
|
||||
@ -61,7 +58,7 @@ struct DCCEXParser
|
||||
static RingStream * stashRingStream;
|
||||
|
||||
static int16_t stashP[MAX_COMMAND_PARAMS];
|
||||
bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
|
||||
static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
|
||||
static void callback_W(int16_t result);
|
||||
static void callback_B(int16_t result);
|
||||
static void callback_R(int16_t result);
|
||||
|
@ -1,5 +1,10 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow & David Cutting. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Chris Harlow
|
||||
* © 2021 David Cutting
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
|
@ -1,6 +1,8 @@
|
||||
/*
|
||||
* (c) 2021 Mike S. All rights reserved.
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
|
@ -1,8 +1,12 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
@ -17,7 +21,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma GCC optimize ("-O3")
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "defines.h"
|
||||
@ -47,10 +51,8 @@ void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) {
|
||||
&& (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 (MotorDriver::usePWM)
|
||||
DIAG(F("Signal pin config: high accuracy waveform"));
|
||||
else
|
||||
DIAG(F("Signal pin config: normal accuracy waveform"));
|
||||
DIAG(F("Signal pin config: %S accuracy waveform"),
|
||||
MotorDriver::usePWM ? F("high") : F("normal") );
|
||||
DCCTimer::begin(DCCWaveform::interruptHandler);
|
||||
}
|
||||
|
||||
@ -71,6 +73,8 @@ void IRAM_ATTR DCCWaveform::loop(bool ackManagerActive) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void IRAM_ATTR DCCWaveform::interruptHandler() {
|
||||
// call the timer edge sensitive actions for progtrack and maintrack
|
||||
// member functions would be cleaner but have more overhead
|
||||
@ -95,7 +99,7 @@ void IRAM_ATTR DCCWaveform::interruptHandler() {
|
||||
progTrack.checkAck();
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma GCC push_options
|
||||
|
||||
// An instance of this class handles the DCC transmissions for one track. (main or prog)
|
||||
// Interrupts are marshalled via the statics.
|
||||
@ -130,6 +134,7 @@ void DCCWaveform::setPowerMode(POWERMODE mode) {
|
||||
powerMode = mode;
|
||||
bool ison = (mode == POWERMODE::ON);
|
||||
motorDriver->setPower( ison);
|
||||
sentResetsSincePacket=0;
|
||||
}
|
||||
|
||||
|
||||
@ -140,6 +145,8 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
|
||||
if (!isMainTrack && !ackManagerActive && !progTrackSyncMain && !progTrackBoosted)
|
||||
tripValue=progTripValue;
|
||||
|
||||
// Trackname for diag messages later
|
||||
const FSH*trackname = isMainTrack ? F("MAIN") : F("PROG");
|
||||
switch (powerMode) {
|
||||
case POWERMODE::OFF:
|
||||
sampleDelay = POWER_SAMPLE_OFF_WAIT;
|
||||
@ -157,9 +164,9 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
|
||||
}
|
||||
// Write this after the fact as we want to turn on as fast as possible
|
||||
// because we don't know which output actually triggered the fault pin
|
||||
DIAG(F("*** COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S ***"), isMainTrack ? F("MAIN") : F("PROG"));
|
||||
DIAG(F("COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S"), trackname);
|
||||
} else {
|
||||
DIAG(F("*** %S FAULT PIN ACTIVE - OVERLOAD ***"), isMainTrack ? F("MAIN") : F("PROG"));
|
||||
DIAG(F("%S FAULT PIN ACTIVE - OVERLOAD"), trackname);
|
||||
if (lastCurrent < tripValue) {
|
||||
lastCurrent = tripValue; // exaggerate
|
||||
}
|
||||
@ -177,7 +184,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
|
||||
unsigned int maxmA=motorDriver->raw2mA(tripValue);
|
||||
power_good_counter=0;
|
||||
sampleDelay = power_sample_overload_wait;
|
||||
DIAG(F("*** %S TRACK POWER OVERLOAD current=%d max=%d offtime=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), mA, maxmA, sampleDelay);
|
||||
DIAG(F("%S TRACK POWER OVERLOAD current=%d max=%d offtime=%d"), trackname, mA, maxmA, sampleDelay);
|
||||
if (power_sample_overload_wait >= 10000)
|
||||
power_sample_overload_wait = 10000;
|
||||
else
|
||||
@ -189,7 +196,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
|
||||
setPowerMode(POWERMODE::ON);
|
||||
sampleDelay = POWER_SAMPLE_ON_WAIT;
|
||||
// Debug code....
|
||||
DIAG(F("*** %S TRACK POWER RESET delay=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), sampleDelay);
|
||||
DIAG(F("%S TRACK POWER RESET delay=%d"), trackname, sampleDelay);
|
||||
break;
|
||||
default:
|
||||
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
|
||||
@ -213,6 +220,8 @@ const bool DCCWaveform::signalTransform[]={
|
||||
/* WAVE_LOW_0 -> */ LOW,
|
||||
/* WAVE_PENDING (should not happen) -> */ LOW};
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void IRAM_ATTR DCCWaveform::interrupt2() {
|
||||
// calculate the next bit to be sent:
|
||||
// set state WAVE_MID_1 for a 1=bit
|
||||
@ -270,7 +279,7 @@ void IRAM_ATTR DCCWaveform::interrupt2() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma GCC pop_options
|
||||
|
||||
|
||||
// Wait until there is no packet pending, then make this pending
|
||||
@ -298,7 +307,7 @@ void DCCWaveform::setAckBaseline() {
|
||||
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 %dus and %dus"),
|
||||
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
|
||||
baseline,motorDriver->raw2mA(baseline),
|
||||
ackThreshold,motorDriver->raw2mA(ackThreshold),
|
||||
minAckPulseDuration, maxAckPulseDuration);
|
||||
@ -318,12 +327,14 @@ void DCCWaveform::setAckPending() {
|
||||
|
||||
byte DCCWaveform::getAck() {
|
||||
if (ackPending) return (2); // still waiting
|
||||
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%duS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
|
||||
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS 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.
|
||||
}
|
||||
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize ("-O3")
|
||||
void IRAM_ATTR DCCWaveform::checkAck() {
|
||||
// This function operates in interrupt() time so must be fast and can't DIAG
|
||||
if (sentResetsSincePacket > 6) { //ACK timeout
|
||||
@ -373,3 +384,4 @@ void IRAM_ATTR DCCWaveform::checkAck() {
|
||||
}
|
||||
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
|
||||
}
|
||||
#pragma GCC pop_options
|
||||
|
@ -1,8 +1,12 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
|
6
DIAG.h
6
DIAG.h
@ -1,7 +1,9 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
|
@ -1,5 +1,7 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -19,4 +21,4 @@
|
||||
|
||||
#include "DisplayInterface.h"
|
||||
|
||||
DisplayInterface *DisplayInterface::lcdDisplay = 0;
|
||||
DisplayInterface *DisplayInterface::lcdDisplay = 0;
|
||||
|
@ -1,5 +1,7 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -32,4 +34,4 @@ public:
|
||||
static DisplayInterface *lcdDisplay;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
112
EEStore.cpp
112
EEStore.cpp
@ -1,9 +1,12 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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,89 +21,92 @@
|
||||
* 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 "Turnouts.h"
|
||||
#include "Sensors.h"
|
||||
#include "Outputs.h"
|
||||
|
||||
#include "DIAG.h"
|
||||
#include "Outputs.h"
|
||||
#include "Sensors.h"
|
||||
#include "Turnouts.h"
|
||||
|
||||
#if defined(ARDUINO_ARCH_SAMD)
|
||||
ExternalEEPROM EEPROM;
|
||||
#endif
|
||||
|
||||
void EEStore::init(){
|
||||
void EEStore::init() {
|
||||
#if defined(ARDUINO_ARCH_SAMD)
|
||||
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three A pins grounded (0b1010000 = 0x50)
|
||||
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three
|
||||
// A pins grounded (0b1010000 = 0x50)
|
||||
#endif
|
||||
|
||||
eeStore=(EEStore *)calloc(1,sizeof(EEStore));
|
||||
|
||||
EEPROM.get(0,eeStore->data); // get eeStore data
|
||||
eeStore = (EEStore *)calloc(1, sizeof(EEStore));
|
||||
|
||||
if(strncmp(eeStore->data.id,EESTORE_ID,sizeof(EESTORE_ID))!=0){ // check to see that eeStore contains valid DCC++ ID
|
||||
sprintf(eeStore->data.id,EESTORE_ID); // if not, create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
|
||||
eeStore->data.nTurnouts=0;
|
||||
eeStore->data.nSensors=0;
|
||||
eeStore->data.nOutputs=0;
|
||||
EEPROM.put(0,eeStore->data);
|
||||
}
|
||||
EEPROM.get(0, eeStore->data); // get eeStore data
|
||||
|
||||
reset(); // set memory pointer to first free EEPROM space
|
||||
Turnout::load(); // load turnout definitions
|
||||
Sensor::load(); // load sensor definitions
|
||||
Output::load(); // load output definitions
|
||||
// check to see that eeStore contains valid DCC++ ID
|
||||
if (strncmp(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)) != 0) {
|
||||
// if not, create blank eeStore structure (no
|
||||
// turnouts, no sensors) and save it back to EEPROM
|
||||
strncpy(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID));
|
||||
eeStore->data.nTurnouts = 0;
|
||||
eeStore->data.nSensors = 0;
|
||||
eeStore->data.nOutputs = 0;
|
||||
EEPROM.put(0, eeStore->data);
|
||||
}
|
||||
|
||||
reset(); // set memory pointer to first free EEPROM space
|
||||
Turnout::load(); // load turnout definitions
|
||||
Sensor::load(); // load sensor definitions
|
||||
Output::load(); // load output definitions
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void EEStore::clear(){
|
||||
|
||||
sprintf(eeStore->data.id,EESTORE_ID); // create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
|
||||
eeStore->data.nTurnouts=0;
|
||||
eeStore->data.nSensors=0;
|
||||
eeStore->data.nOutputs=0;
|
||||
EEPROM.put(0,eeStore->data);
|
||||
|
||||
void EEStore::clear() {
|
||||
sprintf(eeStore->data.id,
|
||||
EESTORE_ID); // create blank eeStore structure (no turnouts, no
|
||||
// sensors) and save it back to EEPROM
|
||||
eeStore->data.nTurnouts = 0;
|
||||
eeStore->data.nSensors = 0;
|
||||
eeStore->data.nOutputs = 0;
|
||||
EEPROM.put(0, eeStore->data);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void EEStore::store(){
|
||||
reset();
|
||||
Turnout::store();
|
||||
Sensor::store();
|
||||
Output::store();
|
||||
EEPROM.put(0,eeStore->data);
|
||||
void EEStore::store() {
|
||||
reset();
|
||||
Turnout::store();
|
||||
Sensor::store();
|
||||
Output::store();
|
||||
EEPROM.put(0, eeStore->data);
|
||||
DIAG(F("EEPROM used: %d/%d bytes"), EEStore::pointer(), EEPROM.length());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void EEStore::advance(int n){
|
||||
eeAddress+=n;
|
||||
}
|
||||
void EEStore::advance(int n) { eeAddress += n; }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void EEStore::reset(){
|
||||
eeAddress=sizeof(EEStore);
|
||||
}
|
||||
void EEStore::reset() { eeAddress = sizeof(EEStore); }
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
int EEStore::pointer(){
|
||||
return(eeAddress);
|
||||
}
|
||||
int EEStore::pointer() { return (eeAddress); }
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void EEStore::dump(int num) {
|
||||
byte b;
|
||||
DIAG(F("Addr 0x char"));
|
||||
for (int n=0 ; n<num; n++) {
|
||||
EEPROM.get(n, b);
|
||||
DIAG(F("%d %x %c"),n,b,isprint(b) ? b : ' ');
|
||||
}
|
||||
byte b;
|
||||
DIAG(F("Addr 0x char"));
|
||||
for (int n = 0; n < num; n++) {
|
||||
EEPROM.get(n, b);
|
||||
DIAG(F("%d %x %c"), n, b, isprint(b) ? b : ' ');
|
||||
}
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
EEStore *EEStore::eeStore=NULL;
|
||||
int EEStore::eeAddress=0;
|
||||
EEStore *EEStore::eeStore = NULL;
|
||||
int EEStore::eeAddress = 0;
|
||||
#endif
|
||||
|
11
EEStore.h
11
EEStore.h
@ -1,6 +1,9 @@
|
||||
/*
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
* (c) 2020 Harald Barth. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -17,6 +20,7 @@
|
||||
* 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
|
||||
|
||||
@ -29,7 +33,7 @@ extern ExternalEEPROM EEPROM;
|
||||
#include <EEPROM.h>
|
||||
#endif
|
||||
|
||||
#define EESTORE_ID "DCC++"
|
||||
#define EESTORE_ID "DCC++1"
|
||||
|
||||
struct EEStoreData{
|
||||
char id[sizeof(EESTORE_ID)];
|
||||
@ -52,3 +56,4 @@ struct EEStore{
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DISABLE_EEPROM
|
||||
|
23
EXRAIL.h
Normal file
23
EXRAIL.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef EXRAIL_H
|
||||
#define EXRAIL_H
|
||||
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
#include "EXRAIL2.h"
|
||||
|
||||
class RMFT {
|
||||
public:
|
||||
static void inline begin() {RMFT2::begin();}
|
||||
static void inline loop() {RMFT2::loop();}
|
||||
};
|
||||
|
||||
#include "EXRAILMacros.h"
|
||||
|
||||
#else
|
||||
// Dummy RMFT
|
||||
class RMFT {
|
||||
public:
|
||||
static void inline begin() {}
|
||||
static void inline loop() {}
|
||||
};
|
||||
#endif
|
||||
#endif
|
984
EXRAIL2.cpp
Normal file
984
EXRAIL2.cpp
Normal file
@ -0,0 +1,984 @@
|
||||
/*
|
||||
* © 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 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];
|
||||
|
||||
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_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 operad 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 catcers ignored here
|
||||
case OPCODE_ONTHROW:
|
||||
case OPCODE_ONACTIVATE: // Activate event catcers 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');
|
||||
}
|
152
EXRAIL2.h
Normal file
152
EXRAIL2.h
Normal file
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 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/>.
|
||||
*/
|
||||
#ifndef EXRAIL2_H
|
||||
#define EXRAIL2_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.
|
||||
// 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.
|
||||
//
|
||||
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_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_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
|
||||
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
|
||||
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_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
|
||||
OPCODE_PRINT,OPCODE_DCCACTIVATE,
|
||||
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,OPCODE_IFGTE,OPCODE_IFLT,
|
||||
OPCODE_ROSTER,
|
||||
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 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();
|
||||
static void loop();
|
||||
RMFT2(int progCounter);
|
||||
RMFT2(int route, uint16_t cab);
|
||||
~RMFT2();
|
||||
static void readLocoCallback(int16_t cv);
|
||||
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 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);
|
||||
static void emitWithrottleDescriptions(Print * stream);
|
||||
|
||||
static RMFT2 * loopTask;
|
||||
static RMFT2 * pausingTask;
|
||||
void delayMe(long millisecs);
|
||||
void driveLoco(byte speedo);
|
||||
bool readSensor(uint16_t sensorId);
|
||||
bool skipIfBlock();
|
||||
bool readLoco();
|
||||
void loop2();
|
||||
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 delayTime;
|
||||
union {
|
||||
unsigned long waitAfter; // Used by OPCODE_AFTER
|
||||
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
|
||||
};
|
||||
bool timeoutFlag;
|
||||
byte taskId;
|
||||
|
||||
uint16_t loco;
|
||||
bool forward;
|
||||
bool invert;
|
||||
byte speedo;
|
||||
int16_t onTurnoutId;
|
||||
int16_t onActivateAddr;
|
||||
byte stackDepth;
|
||||
int callStack[MAX_STACK_DEPTH];
|
||||
};
|
||||
#endif
|
202
EXRAIL2MacroReset.h
Normal file
202
EXRAIL2MacroReset.h
Normal file
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* © 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 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 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 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 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
|
263
EXRAILMacros.h
Normal file
263
EXRAILMacros.h
Normal file
@ -0,0 +1,263 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 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/>.
|
||||
*/
|
||||
|
||||
#ifndef EXRAILMacros_H
|
||||
#define EXRAILMacros_H
|
||||
|
||||
// remove normal code LCD & SERIAL macros (will be restored later)
|
||||
#undef LCD
|
||||
#undef SERIAL
|
||||
|
||||
|
||||
// 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
|
||||
// extract the relevant parts.
|
||||
|
||||
// The entire automation script is contained within a byte array RMFT2::RouteCode[]
|
||||
// made up of opcode and parameter pairs.
|
||||
// ech opcode is a 1 byte operation plus 2 byte operand.
|
||||
// The array is normally built using the macros below as this makes it easier
|
||||
// to manage the cases where:
|
||||
// - padding must be applied to ensure the correct alignment of the next instruction
|
||||
// - large parameters must be split up
|
||||
// - multiple parameters aligned correctly
|
||||
// - a single macro requires multiple operations
|
||||
|
||||
// Descriptive texts for routes and animations are created in a sepaerate function which
|
||||
// can be called to emit a list of routes/automatuions in a form suitable for Withrottle.
|
||||
|
||||
// PRINT(msg) and LCD(row,msg) is implemented in a separate pass to create
|
||||
// a getMessageText(id) function.
|
||||
|
||||
// CAUTION: The macros below are multiple passed over myAutomation.h
|
||||
|
||||
// Pass 1 Implements aliases
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#undef ALIAS
|
||||
#define ALIAS(name,value) const int name=value;
|
||||
#include "myAutomation.h"
|
||||
|
||||
// Pass 2 convert descriptions to withrottle format emitter function
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
#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"
|
||||
}
|
||||
|
||||
// Pass 3... Create Text sending functions
|
||||
#include "EXRAIL2MacroReset.h"
|
||||
const int StringMacroTracker1=__COUNTER__;
|
||||
#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 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),
|
||||
#define ALIAS(name,value)
|
||||
#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 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 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 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),
|
||||
#define FON(func) OPCODE_FON,V(func),
|
||||
#define FREE(blockid) OPCODE_FREE,V(blockid),
|
||||
#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 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 POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
|
||||
#define POWEROFF OPCODE_POWEROFF,0,0,
|
||||
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
|
||||
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
|
||||
#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 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 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 SPEED(speed) OPCODE_SPEED,V(speed),
|
||||
#define START(route) OPCODE_START,V(route),
|
||||
#define STOP OPCODE_SPEED,V(0),
|
||||
#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 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
|
||||
const int StringMacroTracker2=__COUNTER__;
|
||||
const FLASH byte RMFT2::RouteCode[] = {
|
||||
#include "myAutomation.h"
|
||||
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
|
||||
|
||||
// Restore normal code LCD & SERIAL macro
|
||||
#undef LCD
|
||||
#define LCD StringFormatter::lcd
|
||||
#undef SERIAL
|
||||
#define SERIAL 0x0
|
||||
#endif
|
@ -1,5 +1,9 @@
|
||||
/*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX/CommandStation-EX
|
||||
*
|
||||
@ -17,12 +21,6 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
#if __has_include ( "config.h")
|
||||
#include "config.h"
|
||||
#else
|
||||
#warning config.h not found. Using defaults from config.example.h
|
||||
#include "config.example.h"
|
||||
#endif
|
||||
#include "defines.h"
|
||||
#if ETHERNET_ON == true
|
||||
#include "EthernetInterface.h"
|
||||
@ -176,6 +174,7 @@ void EthernetInterface::loop()
|
||||
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
|
||||
if (clients[socket] && !clients[socket].connected()) {
|
||||
clients[socket].stop();
|
||||
CommandDistributor::forget(socket);
|
||||
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
/*
|
||||
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020 Gregor Baues
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX/CommandStation-EX
|
||||
*
|
||||
@ -22,12 +27,8 @@
|
||||
|
||||
#ifndef EthernetInterface_h
|
||||
#define EthernetInterface_h
|
||||
#if __has_include ( "config.h")
|
||||
#include "config.h"
|
||||
#else
|
||||
#warning config.h not found. Using defaults from config.example.h
|
||||
#include "config.example.h"
|
||||
#endif
|
||||
|
||||
#include "defines.h"
|
||||
#include "DCCEXParser.h"
|
||||
#include <Arduino.h>
|
||||
#include <avr/pgmspace.h>
|
||||
|
5
FSH.h
5
FSH.h
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
|
@ -1 +1 @@
|
||||
#define GITHUB_SHA "50fcbc0"
|
||||
#define GITHUB_SHA "58ef7d2"
|
||||
|
222
I2CManager.cpp
222
I2CManager.cpp
@ -18,14 +18,40 @@
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <Wire.h>
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
// If not already initialised, initialise I2C (wire).
|
||||
// Include target-specific portions of I2CManager class
|
||||
#if defined(I2C_USE_WIRE)
|
||||
#include "I2CManager_Wire.h"
|
||||
#elif defined(ARDUINO_ARCH_AVR)
|
||||
#include "I2CManager_NonBlocking.h"
|
||||
#include "I2CManager_AVR.h" // Uno/Nano/Mega2560
|
||||
#elif defined(ARDUINO_ARCH_MEGAAVR)
|
||||
#include "I2CManager_NonBlocking.h"
|
||||
#include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi
|
||||
#else
|
||||
#define I2C_USE_WIRE
|
||||
#include "I2CManager_Wire.h" // Other platforms
|
||||
#endif
|
||||
|
||||
|
||||
// If not already initialised, initialise I2C
|
||||
void I2CManagerClass::begin(void) {
|
||||
//setTimeout(25000); // 25 millisecond timeout
|
||||
if (!_beginCompleted) {
|
||||
Wire.begin();
|
||||
_beginCompleted = true;
|
||||
_initialise();
|
||||
|
||||
// Probe and list devices.
|
||||
bool found = false;
|
||||
for (byte addr=1; addr<127; addr++) {
|
||||
if (exists(addr)) {
|
||||
found = true;
|
||||
DIAG(F("I2C Device found at x%x"), addr);
|
||||
}
|
||||
}
|
||||
if (!found) DIAG(F("No I2C Devices found"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,8 +60,8 @@ void I2CManagerClass::begin(void) {
|
||||
void I2CManagerClass::setClock(uint32_t speed) {
|
||||
if (speed < _clockSpeed && !_clockSpeedFixed) {
|
||||
_clockSpeed = speed;
|
||||
Wire.setClock(_clockSpeed);
|
||||
}
|
||||
_setClock(_clockSpeed);
|
||||
}
|
||||
|
||||
// Force clock speed to that specified. It can then only
|
||||
@ -44,39 +70,21 @@ void I2CManagerClass::forceClock(uint32_t speed) {
|
||||
if (!_clockSpeedFixed) {
|
||||
_clockSpeed = speed;
|
||||
_clockSpeedFixed = true;
|
||||
Wire.setClock(_clockSpeed);
|
||||
_setClock(_clockSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if specified I2C address is responding.
|
||||
// Returns 0 if OK, or error code.
|
||||
// Check if specified I2C address is responding (blocking operation)
|
||||
// Returns I2C_STATUS_OK (0) if OK, or error code.
|
||||
uint8_t I2CManagerClass::checkAddress(uint8_t address) {
|
||||
begin();
|
||||
Wire.beginTransmission(address);
|
||||
return Wire.endTransmission();
|
||||
return write(address, NULL, 0);
|
||||
}
|
||||
|
||||
bool I2CManagerClass::exists(uint8_t address) {
|
||||
return checkAddress(address)==0;
|
||||
}
|
||||
|
||||
// Write a complete transmission to I2C using a supplied buffer of data
|
||||
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size) {
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(buffer, size);
|
||||
return Wire.endTransmission();
|
||||
}
|
||||
|
||||
// Write a complete transmission to I2C using a supplied buffer of data in Flash
|
||||
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size) {
|
||||
uint8_t ramBuffer[size];
|
||||
memcpy_P(ramBuffer, buffer, size);
|
||||
return write(address, ramBuffer, size);
|
||||
}
|
||||
|
||||
|
||||
// Write a complete transmission to I2C using a list of data
|
||||
uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
|
||||
/***************************************************************************
|
||||
* Write a transmission to I2C using a list of data (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write(uint8_t address, uint8_t nBytes, ...) {
|
||||
uint8_t buffer[nBytes];
|
||||
va_list args;
|
||||
va_start(args, nBytes);
|
||||
@ -86,30 +94,38 @@ uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
|
||||
return write(address, buffer, nBytes);
|
||||
}
|
||||
|
||||
// Write a command and read response, returns number of bytes received.
|
||||
// Different modules use different ways of accessing registers:
|
||||
// PCF8574 I/O expander justs needs the address (no data);
|
||||
// PCA9685 needs a two byte command to select the register(s) to be read;
|
||||
// MCP23016 needs a one-byte command to select the register.
|
||||
// Some devices use 8-bit registers exclusively and some have 16-bit registers.
|
||||
// Therefore the following function is general purpose, to apply to any
|
||||
// type of I2C device.
|
||||
//
|
||||
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
uint8_t writeBuffer[], uint8_t writeSize) {
|
||||
if (writeSize > 0) {
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(writeBuffer, writeSize);
|
||||
Wire.endTransmission(false); // Don't free bus yet
|
||||
}
|
||||
Wire.requestFrom(address, readSize);
|
||||
uint8_t nBytes = 0;
|
||||
while (Wire.available() && nBytes < readSize)
|
||||
readBuffer[nBytes++] = Wire.read();
|
||||
return nBytes;
|
||||
/***************************************************************************
|
||||
* Initiate a write to an I2C device (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
|
||||
I2CRB req;
|
||||
uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);
|
||||
return finishRB(&req, status);
|
||||
}
|
||||
|
||||
// Overload of read() to allow command to be specified as a series of bytes.
|
||||
/***************************************************************************
|
||||
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * data, uint8_t dataLen) {
|
||||
I2CRB req;
|
||||
uint8_t status = write_P(i2cAddress, data, dataLen, &req);
|
||||
return finishRB(&req, status);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
|
||||
const uint8_t *writeBuffer, uint8_t writeLen)
|
||||
{
|
||||
I2CRB req;
|
||||
uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);
|
||||
return finishRB(&req, status);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
uint8_t writeSize, ...) {
|
||||
va_list args;
|
||||
@ -122,8 +138,104 @@ uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t rea
|
||||
return read(address, readBuffer, readSize, writeBuffer, writeSize);
|
||||
}
|
||||
|
||||
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize) {
|
||||
return read(address, readBuffer, readSize, NULL, 0);
|
||||
/***************************************************************************
|
||||
* Finish off request block by posting status, etc. (blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {
|
||||
if ((status == I2C_STATUS_OK) && rb)
|
||||
status = rb->wait();
|
||||
return status;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Get a message corresponding to the error status
|
||||
***************************************************************************/
|
||||
const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
|
||||
switch (status) {
|
||||
case I2C_STATUS_OK: return F("OK");
|
||||
case I2C_STATUS_TRUNCATED: return F("Transmission truncated");
|
||||
case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F("No response from device (address NAK)");
|
||||
case I2C_STATUS_TRANSMIT_ERROR: return F("Transmit error (data NAK)");
|
||||
case I2C_STATUS_OTHER_TWI_ERROR: return F("Other Wire/TWI error");
|
||||
case I2C_STATUS_TIMEOUT: return F("Timeout");
|
||||
case I2C_STATUS_ARBITRATION_LOST: return F("Arbitration lost");
|
||||
case I2C_STATUS_BUS_ERROR: return F("I2C bus error");
|
||||
case I2C_STATUS_UNEXPECTED_ERROR: return F("Unexpected error");
|
||||
case I2C_STATUS_PENDING: return F("Request pending");
|
||||
default: return F("Error code not recognised");
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Declare singleton class instance.
|
||||
***************************************************************************/
|
||||
I2CManagerClass I2CManager = I2CManagerClass();
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Helper functions associated with I2C Request Block
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/***************************************************************************
|
||||
* Block waiting for request block to complete, and return completion status.
|
||||
* Since such a loop could potentially last for ever if the RB status doesn't
|
||||
* change, we set a high limit (1sec, 1000ms) on the wait time and, if it
|
||||
* hasn't changed by that time we assume it's not going to, and just return
|
||||
* a timeout status. This means that CS will not lock up.
|
||||
***************************************************************************/
|
||||
uint8_t I2CRB::wait() {
|
||||
unsigned long waitStart = millis();
|
||||
do {
|
||||
I2CManager.loop();
|
||||
// Rather than looping indefinitely, let's set a very high timeout (1s).
|
||||
if ((millis() - waitStart) > 1000UL) {
|
||||
DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this);
|
||||
status = I2C_STATUS_TIMEOUT;
|
||||
// Note that, although the timeout is posted, the request may yet complete.
|
||||
// TODO: Ideally we would like to cancel the request.
|
||||
return status;
|
||||
}
|
||||
} while (status==I2C_STATUS_PENDING);
|
||||
return status;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Check whether request is still in progress.
|
||||
***************************************************************************/
|
||||
bool I2CRB::isBusy() {
|
||||
I2CManager.loop();
|
||||
return (status==I2C_STATUS_PENDING);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Helper functions to fill the I2CRequest structure with parameters.
|
||||
***************************************************************************/
|
||||
void I2CRB::setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
|
||||
this->i2cAddress = i2cAddress;
|
||||
this->writeLen = 0;
|
||||
this->readBuffer = readBuffer;
|
||||
this->readLen = readLen;
|
||||
this->operation = OPERATION_READ;
|
||||
this->status = I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
void I2CRB::setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
|
||||
const uint8_t *writeBuffer, uint8_t writeLen) {
|
||||
this->i2cAddress = i2cAddress;
|
||||
this->writeBuffer = writeBuffer;
|
||||
this->writeLen = writeLen;
|
||||
this->readBuffer = readBuffer;
|
||||
this->readLen = readLen;
|
||||
this->operation = OPERATION_REQUEST;
|
||||
this->status = I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
void I2CRB::setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
|
||||
this->i2cAddress = i2cAddress;
|
||||
this->writeBuffer = writeBuffer;
|
||||
this->writeLen = writeLen;
|
||||
this->readLen = 0;
|
||||
this->operation = OPERATION_SEND;
|
||||
this->status = I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
I2CManagerClass I2CManager = I2CManagerClass();
|
236
I2CManager.h
236
I2CManager.h
@ -17,13 +17,16 @@
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef I2CManager_h
|
||||
#define I2CManager_h
|
||||
#ifndef I2CMANAGER_H
|
||||
#define I2CMANAGER_H
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "FSH.h"
|
||||
|
||||
/*
|
||||
* Helper class to manage access to the I2C 'Wire' subsystem.
|
||||
* Manager for I2C communications. For portability, it allows use
|
||||
* of the Wire class, but also has a native implementation for AVR
|
||||
* which supports non-blocking queued I/O requests.
|
||||
*
|
||||
* Helps to avoid calling Wire.begin() multiple times (which is not)
|
||||
* entirely benign as it reinitialises).
|
||||
@ -33,13 +36,157 @@
|
||||
*
|
||||
* Thirdly, it provides a convenient way to check whether there is a
|
||||
* device on a particular I2C address.
|
||||
*
|
||||
* Non-blocking requests are issued by creating an I2C Request Block
|
||||
* (I2CRB) which is then added to the I2C manager's queue. The
|
||||
* application refers to this block to check for completion of the
|
||||
* operation, and for reading completion status.
|
||||
*
|
||||
* Examples:
|
||||
* I2CRB rb;
|
||||
* uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb);
|
||||
* ...
|
||||
* if (!rb.isBusy()) {
|
||||
* status = rb.status;
|
||||
* // Repeat write
|
||||
* I2CManager.queueRequest(&rb);
|
||||
* ...
|
||||
* status = rb.wait(); // Wait for completion and read status
|
||||
* }
|
||||
* ...
|
||||
* I2CRB rb2;
|
||||
* outbuffer[0] = 12; // Register number in I2C device to be read
|
||||
* rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1);
|
||||
* status = I2CManager.queueRequest(&rb2);
|
||||
* if (status == I2C_STATUS_OK) {
|
||||
* status = rb2.wait();
|
||||
* if (status == I2C_STATUS_OK) {
|
||||
* registerValue = inBuffer[0];
|
||||
* }
|
||||
* }
|
||||
* ...
|
||||
*
|
||||
* Synchronous (blocking) calls are also possible, e.g.
|
||||
* status = I2CManager.write(address, buffer, sizeof(buffer));
|
||||
*
|
||||
* When using non-blocking requests, neither the I2CRB nor the input or output
|
||||
* buffers should be modified until the I2CRB is complete (not busy).
|
||||
*
|
||||
* Timeout monitoring is possible, but requires that the following call is made
|
||||
* reasonably frequently in the program's loop() function:
|
||||
* I2CManager.loop();
|
||||
*
|
||||
*/
|
||||
|
||||
class I2CManagerClass {
|
||||
/*
|
||||
* Future enhancement possibility:
|
||||
*
|
||||
* I2C Multiplexer (e.g. TCA9547, TCA9548)
|
||||
*
|
||||
* A multiplexer offers a way of extending the address range of I2C devices. For example, GPIO extenders use address range 0x20-0x27
|
||||
* to are limited to 8 on a bus. By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64.
|
||||
* And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's
|
||||
* as many as 8192 input/output pins!
|
||||
* Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices.
|
||||
* The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus. For example, with one
|
||||
* multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders).
|
||||
* Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be
|
||||
* put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions.
|
||||
*
|
||||
* Changes required: Increase the size of the I2CAddress field in the IODevice class from uint8_t to uint16_t.
|
||||
* The most significant byte would contain a '1' bit flag, the multiplexer number (0-7) and bus number (0-7). Then, when performing
|
||||
* an I2C operation, the I2CManager would check this byte and, if zero, do what it currently does. If the byte is non-zero, then
|
||||
* that means the device is connected via a multiplexer so the I2C transaction should be preceded by a select command issued to the
|
||||
* relevant multiplexer.
|
||||
*
|
||||
* Non-interrupting I2C:
|
||||
*
|
||||
* I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state
|
||||
* machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function.
|
||||
* The speed at which I2C operations can be performed then becomes highly dependent on the frequency that
|
||||
* the loop() function is called, and may be adequate under some circumstances.
|
||||
* The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use)
|
||||
* becomes almost zero.
|
||||
*
|
||||
*/
|
||||
|
||||
// Uncomment following line to enable Wire library instead of native I2C drivers
|
||||
//#define I2C_USE_WIRE
|
||||
|
||||
// Uncomment following line to disable the use of interrupts by the native I2C drivers.
|
||||
//#define I2C_NO_INTERRUPTS
|
||||
|
||||
// Default to use interrupts within the native I2C drivers.
|
||||
#ifndef I2C_NO_INTERRUPTS
|
||||
#define I2C_USE_INTERRUPTS
|
||||
#endif
|
||||
|
||||
// Status codes for I2CRB structures.
|
||||
enum : uint8_t {
|
||||
// Codes used by Wire and by native drivers
|
||||
I2C_STATUS_OK=0,
|
||||
I2C_STATUS_TRUNCATED=1,
|
||||
I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2,
|
||||
I2C_STATUS_TRANSMIT_ERROR=3,
|
||||
I2C_STATUS_TIMEOUT=5,
|
||||
// Code used by Wire only
|
||||
I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error
|
||||
// Codes used by native drivers only
|
||||
I2C_STATUS_ARBITRATION_LOST=6,
|
||||
I2C_STATUS_BUS_ERROR=7,
|
||||
I2C_STATUS_UNEXPECTED_ERROR=8,
|
||||
I2C_STATUS_PENDING=253,
|
||||
};
|
||||
|
||||
// Status codes for the state machine (not returned to caller).
|
||||
enum : uint8_t {
|
||||
I2C_STATE_ACTIVE=253,
|
||||
I2C_STATE_FREE=254,
|
||||
I2C_STATE_CLOSING=255,
|
||||
};
|
||||
|
||||
typedef enum : uint8_t
|
||||
{
|
||||
OPERATION_READ = 1,
|
||||
OPERATION_REQUEST = 2,
|
||||
OPERATION_SEND = 3,
|
||||
OPERATION_SEND_P = 4,
|
||||
} OperationEnum;
|
||||
|
||||
|
||||
// Default I2C frequency
|
||||
#ifndef I2C_FREQ
|
||||
#define I2C_FREQ 400000L
|
||||
#endif
|
||||
|
||||
// Class defining a request context for an I2C operation.
|
||||
class I2CRB {
|
||||
public:
|
||||
volatile uint8_t status; // Completion status, or pending flag (updated from IRC)
|
||||
volatile uint8_t nBytes; // Number of bytes read (updated from IRC)
|
||||
|
||||
I2CManagerClass() {}
|
||||
inline I2CRB() { status = I2C_STATUS_OK; };
|
||||
uint8_t wait();
|
||||
bool isBusy();
|
||||
|
||||
void setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen);
|
||||
void setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);
|
||||
void setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);
|
||||
|
||||
uint8_t writeLen;
|
||||
uint8_t readLen;
|
||||
uint8_t operation;
|
||||
uint8_t i2cAddress;
|
||||
uint8_t *readBuffer;
|
||||
const uint8_t *writeBuffer;
|
||||
#if !defined(I2C_USE_WIRE)
|
||||
I2CRB *nextRequest;
|
||||
#endif
|
||||
};
|
||||
|
||||
// I2C Manager
|
||||
class I2CManagerClass {
|
||||
public:
|
||||
|
||||
// If not already initialised, initialise I2C (wire).
|
||||
void begin(void);
|
||||
@ -49,28 +196,93 @@ public:
|
||||
void forceClock(uint32_t speed);
|
||||
// Check if specified I2C address is responding.
|
||||
uint8_t checkAddress(uint8_t address);
|
||||
bool exists(uint8_t address);
|
||||
inline bool exists(uint8_t address) {
|
||||
return checkAddress(address)==I2C_STATUS_OK;
|
||||
}
|
||||
// Write a complete transmission to I2C from an array in RAM
|
||||
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size);
|
||||
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
|
||||
// Write a complete transmission to I2C from an array in Flash
|
||||
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size);
|
||||
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
|
||||
// Write a transmission to I2C from a list of bytes.
|
||||
uint8_t write(uint8_t address, int nBytes, ...);
|
||||
uint8_t write(uint8_t address, uint8_t nBytes, ...);
|
||||
// Write a command from an array in RAM and read response
|
||||
uint8_t read(uint8_t address, uint8_t writeBuffer[], uint8_t writeSize,
|
||||
uint8_t readBuffer[], uint8_t readSize);
|
||||
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);
|
||||
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);
|
||||
// Write a command from an arbitrary list of bytes and read response
|
||||
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
uint8_t writeSize, ...);
|
||||
// Write a null command and read the response.
|
||||
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize);
|
||||
void queueRequest(I2CRB *req);
|
||||
|
||||
// Function to abort long-running operations.
|
||||
void checkForTimeout();
|
||||
|
||||
// Loop method
|
||||
void loop();
|
||||
|
||||
// Expand error codes into text. Note that they are in flash so
|
||||
// need to be printed using FSH.
|
||||
static const FSH *getErrorMessage(uint8_t status);
|
||||
|
||||
private:
|
||||
bool _beginCompleted = false;
|
||||
bool _clockSpeedFixed = false;
|
||||
uint32_t _clockSpeed = 400000L; // 400kHz max on Arduino.
|
||||
|
||||
// Finish off request block by waiting for completion and posting status.
|
||||
uint8_t finishRB(I2CRB *rb, uint8_t status);
|
||||
|
||||
void _initialise();
|
||||
void _setClock(unsigned long);
|
||||
|
||||
#if !defined(I2C_USE_WIRE)
|
||||
// I2CRB structs are queued on the following two links.
|
||||
// If there are no requests, both are NULL.
|
||||
// If there is only one request, then queueHead and queueTail both point to it.
|
||||
// Otherwise, queueHead is the pointer to the first request in the queue and
|
||||
// queueTail is the pointer to the last request in the queue.
|
||||
// Within the queue, each request's nextRequest field points to the
|
||||
// next request, or NULL.
|
||||
// Mark volatile as they are updated by IRC and read/written elsewhere.
|
||||
static I2CRB * volatile queueHead;
|
||||
static I2CRB * volatile queueTail;
|
||||
static volatile uint8_t state;
|
||||
|
||||
static I2CRB * volatile currentRequest;
|
||||
static volatile uint8_t txCount;
|
||||
static volatile uint8_t rxCount;
|
||||
static volatile uint8_t bytesToSend;
|
||||
static volatile uint8_t bytesToReceive;
|
||||
static volatile uint8_t operation;
|
||||
static volatile unsigned long startTime;
|
||||
|
||||
static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled.
|
||||
|
||||
void startTransaction();
|
||||
|
||||
// Low-level hardware manipulation functions.
|
||||
static void I2C_init();
|
||||
static void I2C_setClock(unsigned long i2cClockSpeed);
|
||||
static void I2C_handleInterrupt();
|
||||
static void I2C_sendStart();
|
||||
static void I2C_sendStop();
|
||||
static void I2C_close();
|
||||
|
||||
public:
|
||||
// setTimeout sets the timout value for I2C transactions.
|
||||
// TODO: Get I2C timeout working before uncommenting the code below.
|
||||
void setTimeout(unsigned long value) { (void)value; /* timeout = value; */ };
|
||||
|
||||
// handleInterrupt needs to be public to be called from the ISR function!
|
||||
static void handleInterrupt();
|
||||
#endif
|
||||
|
||||
|
||||
};
|
||||
|
||||
extern I2CManagerClass I2CManager;
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
209
I2CManager_AVR.h
Normal file
209
I2CManager_AVR.h
Normal file
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. 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 I2CMANAGER_AVR_H
|
||||
#define I2CMANAGER_AVR_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "I2CManager.h"
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
|
||||
/****************************************************************************
|
||||
TWI State codes
|
||||
****************************************************************************/
|
||||
// General TWI Master staus codes
|
||||
#define TWI_START 0x08 // START has been transmitted
|
||||
#define TWI_REP_START 0x10 // Repeated START has been transmitted
|
||||
#define TWI_ARB_LOST 0x38 // Arbitration lost
|
||||
|
||||
// TWI Master Transmitter staus codes
|
||||
#define TWI_MTX_ADR_ACK 0x18 // SLA+W has been tramsmitted and ACK received
|
||||
#define TWI_MTX_ADR_NACK 0x20 // SLA+W has been tramsmitted and NACK received
|
||||
#define TWI_MTX_DATA_ACK 0x28 // Data byte has been tramsmitted and ACK received
|
||||
#define TWI_MTX_DATA_NACK 0x30 // Data byte has been tramsmitted and NACK received
|
||||
|
||||
// TWI Master Receiver staus codes
|
||||
#define TWI_MRX_ADR_ACK 0x40 // SLA+R has been tramsmitted and ACK received
|
||||
#define TWI_MRX_ADR_NACK 0x48 // SLA+R has been tramsmitted and NACK received
|
||||
#define TWI_MRX_DATA_ACK 0x50 // Data byte has been received and ACK tramsmitted
|
||||
#define TWI_MRX_DATA_NACK 0x58 // Data byte has been received and NACK tramsmitted
|
||||
|
||||
// TWI Miscellaneous status codes
|
||||
#define TWI_NO_STATE 0xF8 // No relevant state information available
|
||||
#define TWI_BUS_ERROR 0x00 // Bus error due to an illegal START or STOP condition
|
||||
|
||||
#define TWI_TWBR ((F_CPU / I2C_FREQ) - 16) / 2 // TWI Bit rate Register setting.
|
||||
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
#define ENABLE_TWI_INTERRUPT (1<<TWIE)
|
||||
#else
|
||||
#define ENABLE_TWI_INTERRUPT 0
|
||||
#endif
|
||||
|
||||
/***************************************************************************
|
||||
* Set I2C clock speed register.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
|
||||
unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2;
|
||||
for (uint8_t preScaler = 0; preScaler<=3; preScaler++) {
|
||||
if (temp <= 255) {
|
||||
TWBR = temp;
|
||||
TWSR = (TWSR & 0xfc) | preScaler;
|
||||
return;
|
||||
} else
|
||||
temp /= 4;
|
||||
}
|
||||
// Set slowest speed ~= 500 bits/sec
|
||||
TWBR = 255;
|
||||
TWSR |= 0x03;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initialise I2C registers.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_init()
|
||||
{
|
||||
TWSR = 0;
|
||||
TWBR = TWI_TWBR; // Set bit rate register (Baudrate). Defined in header file.
|
||||
TWDR = 0xFF; // Default content = SDA released.
|
||||
TWCR = (1<<TWINT); // Clear interrupt flag
|
||||
|
||||
pinMode(SDA, INPUT_PULLUP);
|
||||
pinMode(SCL, INPUT_PULLUP);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a start bit for transmission.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_sendStart() {
|
||||
bytesToSend = currentRequest->writeLen;
|
||||
bytesToReceive = currentRequest->readLen;
|
||||
// We may have initiated a stop bit before this without waiting for it.
|
||||
// Wait for stop bit to be sent before sending start.
|
||||
while (TWCR & (1<<TWSTO)) {}
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a stop bit for transmission (does not interrupt)
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_sendStop() {
|
||||
TWDR = 0xff; // Default condition = SDA released
|
||||
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Close I2C down
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_close() {
|
||||
// disable TWI
|
||||
I2C_sendStop();
|
||||
while (TWCR & (1<<TWSTO)) {}
|
||||
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Main state machine for I2C, called from interrupt handler or,
|
||||
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
|
||||
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_handleInterrupt() {
|
||||
if (!(TWCR & (1<<TWINT))) return; // Nothing to do.
|
||||
|
||||
uint8_t twsr = TWSR & 0xF8;
|
||||
|
||||
// Cases are ordered so that the most frequently used ones are tested first.
|
||||
switch (twsr) {
|
||||
case TWI_MTX_DATA_ACK: // Data byte has been transmitted and ACK received
|
||||
case TWI_MTX_ADR_ACK: // SLA+W has been transmitted and ACK received
|
||||
if (bytesToSend) { // Send first.
|
||||
if (operation == OPERATION_SEND_P)
|
||||
TWDR = GETFLASH(currentRequest->writeBuffer + (txCount++));
|
||||
else
|
||||
TWDR = currentRequest->writeBuffer[txCount++];
|
||||
bytesToSend--;
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
|
||||
} else if (bytesToReceive) { // All sent, anything to receive?
|
||||
while (TWCR & (1<<TWSTO)) {} // Wait for stop to be sent
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
|
||||
} else { // Nothing left to send or receive
|
||||
TWDR = 0xff; // Default condition = SDA released
|
||||
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
|
||||
state = I2C_STATUS_OK;
|
||||
}
|
||||
break;
|
||||
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
|
||||
if (bytesToReceive > 0) {
|
||||
currentRequest->readBuffer[rxCount++] = TWDR;
|
||||
bytesToReceive--;
|
||||
}
|
||||
/* fallthrough */
|
||||
case TWI_MRX_ADR_ACK: // SLA+R has been sent and ACK received
|
||||
if (bytesToReceive <= 1) {
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception
|
||||
} else {
|
||||
// send ack
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
|
||||
}
|
||||
break;
|
||||
case TWI_MRX_DATA_NACK: // Data byte has been received and NACK transmitted
|
||||
if (bytesToReceive > 0) {
|
||||
currentRequest->readBuffer[rxCount++] = TWDR;
|
||||
bytesToReceive--;
|
||||
}
|
||||
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
|
||||
state = I2C_STATUS_OK;
|
||||
break;
|
||||
case TWI_START: // START has been transmitted
|
||||
case TWI_REP_START: // Repeated START has been transmitted
|
||||
// Set up address and R/W
|
||||
if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))
|
||||
TWDR = (currentRequest->i2cAddress << 1) | 1; // SLA+R
|
||||
else
|
||||
TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W
|
||||
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
|
||||
break;
|
||||
case TWI_MTX_ADR_NACK: // SLA+W has been transmitted and NACK received
|
||||
case TWI_MRX_ADR_NACK: // SLA+R has been transmitted and NACK received
|
||||
case TWI_MTX_DATA_NACK: // Data byte has been transmitted and NACK received
|
||||
TWDR = 0xff; // Default condition = SDA released
|
||||
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
|
||||
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
|
||||
break;
|
||||
case TWI_ARB_LOST: // Arbitration lost
|
||||
// Restart transaction from start.
|
||||
I2C_sendStart();
|
||||
break;
|
||||
case TWI_BUS_ERROR: // Bus error due to an illegal START or STOP condition
|
||||
default:
|
||||
TWDR = 0xff; // Default condition = SDA released
|
||||
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
|
||||
state = I2C_STATUS_TRANSMIT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
ISR(TWI_vect) {
|
||||
I2CManagerClass::handleInterrupt();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* I2CMANAGER_AVR_H */
|
160
I2CManager_Mega4809.h
Normal file
160
I2CManager_Mega4809.h
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. 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 I2CMANAGER_MEGA4809_H
|
||||
#define I2CMANAGER_MEGA4809_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "I2CManager.h"
|
||||
|
||||
/***************************************************************************
|
||||
* Set I2C clock speed register.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
|
||||
uint16_t t_rise;
|
||||
if (i2cClockSpeed < 200000) {
|
||||
i2cClockSpeed = 100000;
|
||||
t_rise = 1000;
|
||||
} else if (i2cClockSpeed < 800000) {
|
||||
i2cClockSpeed = 400000;
|
||||
t_rise = 300;
|
||||
} else if (i2cClockSpeed < 1200000) {
|
||||
i2cClockSpeed = 1000000;
|
||||
t_rise = 120;
|
||||
} else {
|
||||
i2cClockSpeed = 100000;
|
||||
t_rise = 1000;
|
||||
}
|
||||
uint32_t baud = (F_CPU_CORRECTED / i2cClockSpeed - F_CPU_CORRECTED / 1000 / 1000
|
||||
* t_rise / 1000 - 10) / 2;
|
||||
TWI0.MBAUD = (uint8_t)baud;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initialise I2C registers.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_init()
|
||||
{
|
||||
pinMode(PIN_WIRE_SDA, INPUT_PULLUP);
|
||||
pinMode(PIN_WIRE_SCL, INPUT_PULLUP);
|
||||
PORTMUX.TWISPIROUTEA |= TWI_MUX;
|
||||
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
TWI0.MCTRLA = TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm;
|
||||
#else
|
||||
TWI0.MCTRLA = TWI_ENABLE_bm;
|
||||
#endif
|
||||
I2C_setClock(I2C_FREQ);
|
||||
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a start bit for transmission, followed by address and R/W
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_sendStart() {
|
||||
bytesToSend = currentRequest->writeLen;
|
||||
bytesToReceive = currentRequest->readLen;
|
||||
|
||||
// If anything to send, initiate write. Otherwise initiate read.
|
||||
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) & !bytesToSend))
|
||||
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
|
||||
else
|
||||
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a stop bit for transmission.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_sendStop() {
|
||||
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Close I2C down
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_close() {
|
||||
I2C_sendStop();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Main state machine for I2C, called from interrupt handler.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::I2C_handleInterrupt() {
|
||||
|
||||
uint8_t currentStatus = TWI0.MSTATUS;
|
||||
|
||||
if (currentStatus & TWI_ARBLOST_bm) {
|
||||
// Arbitration lost, restart
|
||||
TWI0.MSTATUS = currentStatus; // clear all flags
|
||||
I2C_sendStart(); // Reinitiate request
|
||||
} else if (currentStatus & TWI_BUSERR_bm) {
|
||||
// Bus error
|
||||
state = I2C_STATUS_BUS_ERROR;
|
||||
TWI0.MSTATUS = currentStatus; // clear all flags
|
||||
} else if (currentStatus & TWI_WIF_bm) {
|
||||
// Master write completed
|
||||
if (currentStatus & TWI_RXACK_bm) {
|
||||
// Nacked, send stop.
|
||||
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
|
||||
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
|
||||
} else if (bytesToSend) {
|
||||
// Acked, so send next byte
|
||||
if (currentRequest->operation == OPERATION_SEND_P)
|
||||
TWI0.MDATA = GETFLASH(currentRequest->writeBuffer + (txCount++));
|
||||
else
|
||||
TWI0.MDATA = currentRequest->writeBuffer[txCount++];
|
||||
bytesToSend--;
|
||||
} else if (bytesToReceive) {
|
||||
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
|
||||
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
|
||||
} else {
|
||||
// No more data to send/receive. Initiate a STOP condition.
|
||||
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
|
||||
state = I2C_STATUS_OK; // Done
|
||||
}
|
||||
} else if (currentStatus & TWI_RIF_bm) {
|
||||
// Master read completed without errors
|
||||
if (bytesToReceive) {
|
||||
currentRequest->readBuffer[rxCount++] = TWI0.MDATA; // Store received byte
|
||||
bytesToReceive--;
|
||||
} else {
|
||||
// Buffer full, issue nack/stop
|
||||
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
|
||||
state = I2C_STATUS_OK;
|
||||
}
|
||||
if (bytesToReceive) {
|
||||
// More bytes to receive, issue ack and start another read
|
||||
TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc;
|
||||
} else {
|
||||
// Transaction finished, issue NACK and STOP.
|
||||
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
|
||||
state = I2C_STATUS_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Interrupt handler.
|
||||
***************************************************************************/
|
||||
ISR(TWI0_TWIM_vect) {
|
||||
I2CManagerClass::handleInterrupt();
|
||||
}
|
||||
|
||||
#endif
|
224
I2CManager_NonBlocking.h
Normal file
224
I2CManager_NonBlocking.h
Normal file
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. 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 I2CMANAGER_NONBLOCKING_H
|
||||
#define I2CMANAGER_NONBLOCKING_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "I2CManager.h"
|
||||
#if defined(I2C_USE_INTERRUPTS)
|
||||
#include <util/atomic.h>
|
||||
#else
|
||||
#define ATOMIC_BLOCK(x)
|
||||
#define ATOMIC_RESTORESTATE
|
||||
#endif
|
||||
|
||||
// This module is only compiled if I2C_USE_WIRE is not defined, so undefine it here
|
||||
// to get intellisense to work correctly.
|
||||
#if defined(I2C_USE_WIRE)
|
||||
#undef I2C_USE_WIRE
|
||||
#endif
|
||||
|
||||
/***************************************************************************
|
||||
* Initialise the I2CManagerAsync class.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::_initialise()
|
||||
{
|
||||
queueHead = queueTail = NULL;
|
||||
state = I2C_STATE_FREE;
|
||||
I2C_init();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
|
||||
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
|
||||
I2C_setClock(i2cClockSpeed);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Helper function to start operations, if the I2C interface is free and
|
||||
* there is a queued request to be processed.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::startTransaction() {
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
if ((state == I2C_STATE_FREE) && (queueHead != NULL)) {
|
||||
state = I2C_STATE_ACTIVE;
|
||||
currentRequest = queueHead;
|
||||
rxCount = txCount = 0;
|
||||
// Copy key fields to static data for speed.
|
||||
operation = currentRequest->operation;
|
||||
// Start the I2C process going.
|
||||
I2C_sendStart();
|
||||
startTime = micros();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Function to queue a request block and initiate operations.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||
req->status = I2C_STATUS_PENDING;
|
||||
req->nextRequest = NULL;
|
||||
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
if (!queueTail)
|
||||
queueHead = queueTail = req; // Only item on queue
|
||||
else
|
||||
queueTail = queueTail->nextRequest = req; // Add to end
|
||||
startTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write to an I2C device (non-blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {
|
||||
// Make sure previous request has completed.
|
||||
req->wait();
|
||||
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
|
||||
queueRequest(req);
|
||||
return I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {
|
||||
// Make sure previous request has completed.
|
||||
req->wait();
|
||||
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
|
||||
req->operation = OPERATION_SEND_P;
|
||||
queueRequest(req);
|
||||
return I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a read from the I2C device, optionally preceded by a write
|
||||
* (non-blocking operation)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
|
||||
const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req)
|
||||
{
|
||||
// Make sure previous request has completed.
|
||||
req->wait();
|
||||
req->setRequestParams(i2cAddress, readBuffer, readLen, writeBuffer, writeLen);
|
||||
queueRequest(req);
|
||||
return I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* checkForTimeout() function, called from isBusy() and wait() to cancel
|
||||
* requests that are taking too long to complete.
|
||||
* This function doesn't fully work as intended so is not currently called.
|
||||
* Instead we check for an I2C hang-up and report an error from
|
||||
* I2CRB::wait(), but we aren't able to recover from the hang-up. Such faults
|
||||
* may be caused by an I2C wire short for example.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::checkForTimeout() {
|
||||
unsigned long currentMicros = micros();
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
I2CRB *t = queueHead;
|
||||
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && timeout > 0) {
|
||||
// Check for timeout
|
||||
if (currentMicros - startTime > timeout) {
|
||||
// Excessive time. Dequeue request
|
||||
queueHead = t->nextRequest;
|
||||
if (!queueHead) queueTail = NULL;
|
||||
currentRequest = NULL;
|
||||
// Post request as timed out.
|
||||
t->status = I2C_STATUS_TIMEOUT;
|
||||
// Reset TWI interface so it is able to continue
|
||||
// Try close and init, not entirely satisfactory but sort of works...
|
||||
I2C_close(); // Shutdown and restart twi interface
|
||||
I2C_init();
|
||||
state = I2C_STATE_FREE;
|
||||
|
||||
// Initiate next queued request if any.
|
||||
startTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Loop function, for general background work
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::loop() {
|
||||
#if !defined(I2C_USE_INTERRUPTS)
|
||||
handleInterrupt();
|
||||
#endif
|
||||
// Timeout is now reported in I2CRB::wait(), not here.
|
||||
// I've left the code, commented out, as a reminder to look at this again
|
||||
// in the future.
|
||||
//checkForTimeout();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Interupt handler. Call I2C state machine, and dequeue request
|
||||
* if completed.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::handleInterrupt() {
|
||||
|
||||
// Update hardware state machine
|
||||
I2C_handleInterrupt();
|
||||
|
||||
// Enable interrupts to minimise effect on other interrupt code
|
||||
interrupts();
|
||||
|
||||
// Check if current request has completed. If there's a current request
|
||||
// and state isn't active then state contains the completion status of the request.
|
||||
if (state != I2C_STATE_ACTIVE && currentRequest != NULL) {
|
||||
// Remove completed request from head of queue
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||
I2CRB * t = queueHead;
|
||||
if (t == queueHead) {
|
||||
queueHead = t->nextRequest;
|
||||
if (!queueHead) queueTail = queueHead;
|
||||
t->nBytes = rxCount;
|
||||
t->status = state;
|
||||
|
||||
// I2C state machine is now free for next request
|
||||
currentRequest = NULL;
|
||||
state = I2C_STATE_FREE;
|
||||
|
||||
// Start next request (if any)
|
||||
I2CManager.startTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fields in I2CManager class specific to Non-blocking implementation.
|
||||
I2CRB * volatile I2CManagerClass::queueHead = NULL;
|
||||
I2CRB * volatile I2CManagerClass::queueTail = NULL;
|
||||
I2CRB * volatile I2CManagerClass::currentRequest = NULL;
|
||||
volatile uint8_t I2CManagerClass::state = I2C_STATE_FREE;
|
||||
volatile uint8_t I2CManagerClass::txCount;
|
||||
volatile uint8_t I2CManagerClass::rxCount;
|
||||
volatile uint8_t I2CManagerClass::operation;
|
||||
volatile uint8_t I2CManagerClass::bytesToSend;
|
||||
volatile uint8_t I2CManagerClass::bytesToReceive;
|
||||
volatile unsigned long I2CManagerClass::startTime;
|
||||
unsigned long I2CManagerClass::timeout = 0;
|
||||
|
||||
#endif
|
126
I2CManager_Wire.h
Normal file
126
I2CManager_Wire.h
Normal file
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. 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 I2CMANAGER_WIRE_H
|
||||
#define I2CMANAGER_WIRE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include "I2CManager.h"
|
||||
|
||||
// This module is only compiled if I2C_USE_WIRE is defined, so define it here
|
||||
// to get intellisense to work correctly.
|
||||
#if !defined(I2C_USE_WIRE)
|
||||
#define I2C_USE_WIRE
|
||||
#endif
|
||||
|
||||
/***************************************************************************
|
||||
* Initialise I2C interface software
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::_initialise() {
|
||||
Wire.begin();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
|
||||
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
|
||||
Wire.setClock(i2cClockSpeed);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write to an I2C device (blocking operation on Wire)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
|
||||
Wire.beginTransmission(address);
|
||||
if (size > 0) Wire.write(buffer, size);
|
||||
rb->status = Wire.endTransmission();
|
||||
return I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation on Wire)
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
|
||||
uint8_t ramBuffer[size];
|
||||
const uint8_t *p1 = buffer;
|
||||
for (uint8_t i=0; i<size; i++)
|
||||
ramBuffer[i] = GETFLASH(p1++);
|
||||
return write(address, ramBuffer, size, rb);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Initiate a write (optional) followed by a read from the I2C device (blocking operation on Wire)
|
||||
* If fewer than the number of requested bytes are received, status is I2C_STATUS_TRUNCATED.
|
||||
***************************************************************************/
|
||||
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
|
||||
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)
|
||||
{
|
||||
uint8_t status = I2C_STATUS_OK;
|
||||
uint8_t nBytes = 0;
|
||||
if (writeSize > 0) {
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(writeBuffer, writeSize);
|
||||
status = Wire.endTransmission(false); // Don't free bus yet
|
||||
}
|
||||
if (status == I2C_STATUS_OK) {
|
||||
Wire.requestFrom(address, (size_t)readSize);
|
||||
while (Wire.available() && nBytes < readSize)
|
||||
readBuffer[nBytes++] = Wire.read();
|
||||
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
|
||||
}
|
||||
rb->nBytes = nBytes;
|
||||
rb->status = status;
|
||||
return I2C_STATUS_OK;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Function to queue a request block and initiate operations.
|
||||
*
|
||||
* For the Wire version, this executes synchronously, but the status is
|
||||
* returned in the I2CRB as for the asynchronous version.
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::queueRequest(I2CRB *req) {
|
||||
switch (req->operation) {
|
||||
case OPERATION_READ:
|
||||
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);
|
||||
break;
|
||||
case OPERATION_SEND_P:
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Loop function, for general background work
|
||||
***************************************************************************/
|
||||
void I2CManagerClass::loop() {}
|
||||
|
||||
// Loop function
|
||||
void I2CManagerClass::checkForTimeout() {}
|
||||
|
||||
|
||||
#endif
|
506
IODevice.cpp
Normal file
506
IODevice.cpp
Normal file
@ -0,0 +1,506 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "IODevice.h"
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
#include "IO_MCP23017.h"
|
||||
|
||||
#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR)
|
||||
#define USE_FAST_IO
|
||||
#endif
|
||||
|
||||
// Link to halSetup function. If not defined, the function reference will be NULL.
|
||||
extern __attribute__((weak)) void halSetup();
|
||||
extern __attribute__((weak)) void mySetup(); // Deprecated function name, output warning if it's declared
|
||||
|
||||
//==================================================================================================================
|
||||
// Static methods
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Static functions
|
||||
|
||||
// Static method to initialise the IODevice subsystem.
|
||||
|
||||
#if !defined(IO_NO_HAL)
|
||||
|
||||
// Create any standard device instances that may be required, such as the Arduino pins
|
||||
// and PCA9685.
|
||||
void IODevice::begin() {
|
||||
// Initialise the IO subsystem
|
||||
ArduinoPins::create(2, NUM_DIGITAL_PINS-2); // Reserve pins for direct access
|
||||
// Predefine two PCA9685 modules 0x40-0x41
|
||||
// Allocates 32 pins 100-131
|
||||
PCA9685::create(100, 16, 0x40);
|
||||
PCA9685::create(116, 16, 0x41);
|
||||
// Predefine two MCP23017 module 0x20/0x21
|
||||
// Allocates 32 pins 164-195
|
||||
MCP23017::create(164, 16, 0x20);
|
||||
MCP23017::create(180, 16, 0x21);
|
||||
|
||||
// Call the begin() methods of each configured device in turn
|
||||
for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) {
|
||||
dev->_begin();
|
||||
}
|
||||
_initPhase = false;
|
||||
|
||||
// Check for presence of deprecated mySetup() function, and output warning.
|
||||
if (mySetup)
|
||||
DIAG(F("WARNING: mySetup() function should be renamed to halSetup()"));
|
||||
|
||||
// Call user's halSetup() function (if defined in the build in myHal.cpp).
|
||||
// The contents will depend on the user's system hardware configuration.
|
||||
// The myHal.cpp file is a standard C++ module so has access to all of the DCC++EX APIs.
|
||||
if (halSetup)
|
||||
halSetup();
|
||||
}
|
||||
|
||||
// Overarching static loop() method for the IODevice subsystem. Works through the
|
||||
// list of installed devices and calls their individual _loop() method.
|
||||
// Devices may or may not implement this, but if they do it is useful for things like animations
|
||||
// or flashing LEDs.
|
||||
// The current value of micros() is passed as a parameter, so the called loop function
|
||||
// doesn't need to invoke it.
|
||||
void IODevice::loop() {
|
||||
unsigned long currentMicros = micros();
|
||||
|
||||
IODevice *lastLoopDevice = _nextLoopDevice; // So we know when to stop...
|
||||
// Loop through devices until we find one ready to be serviced.
|
||||
do {
|
||||
if (!_nextLoopDevice) _nextLoopDevice = _firstDevice;
|
||||
if (_nextLoopDevice) {
|
||||
if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED
|
||||
&& ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) {
|
||||
// Found one ready to run, so invoke its _loop method.
|
||||
_nextLoopDevice->_nextEntryTime = currentMicros;
|
||||
_nextLoopDevice->_loop(currentMicros);
|
||||
_nextLoopDevice = _nextLoopDevice->_nextDevice;
|
||||
break;
|
||||
}
|
||||
// Not this one, move to next one
|
||||
_nextLoopDevice = _nextLoopDevice->_nextDevice;
|
||||
}
|
||||
} while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all.
|
||||
|
||||
// Report loop time if diags enabled
|
||||
#if defined(DIAG_LOOPTIMES)
|
||||
static unsigned long lastMicros = 0;
|
||||
// Measure time since loop() method started.
|
||||
unsigned long halElapsed = micros() - currentMicros;
|
||||
// Measure time between loop() method entries.
|
||||
unsigned long elapsed = currentMicros - lastMicros;
|
||||
static unsigned long maxElapsed = 0, maxHalElapsed = 0;
|
||||
static unsigned long lastOutputTime = 0;
|
||||
static unsigned long halTotal = 0, total = 0;
|
||||
static unsigned long count = 0;
|
||||
const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec
|
||||
|
||||
// Ignore long loop counts while message is still outputting
|
||||
if (currentMicros - lastOutputTime > 3000UL) {
|
||||
if (elapsed > maxElapsed) maxElapsed = elapsed;
|
||||
if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed;
|
||||
halTotal += halElapsed;
|
||||
total += elapsed;
|
||||
count++;
|
||||
}
|
||||
if (currentMicros - lastOutputTime > interval) {
|
||||
if (lastOutputTime > 0)
|
||||
DIAG(F("Loop Total:%lus (%lus max) HAL:%lus (%lus max)"),
|
||||
total/count, maxElapsed, halTotal/count, maxHalElapsed);
|
||||
maxElapsed = maxHalElapsed = total = halTotal = count = 0;
|
||||
lastOutputTime = currentMicros;
|
||||
}
|
||||
lastMicros = currentMicros;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Display a list of all the devices on the diagnostic stream.
|
||||
void IODevice::DumpAll() {
|
||||
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
|
||||
dev->_display();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if the specified vpin is allocated to a device.
|
||||
bool IODevice::exists(VPIN vpin) {
|
||||
return findDevice(vpin) != NULL;
|
||||
}
|
||||
|
||||
// check whether the pin supports notification. If so, then regular _read calls are not required.
|
||||
bool IODevice::hasCallback(VPIN vpin) {
|
||||
IODevice *dev = findDevice(vpin);
|
||||
if (!dev) return false;
|
||||
return dev->_hasCallback;
|
||||
}
|
||||
|
||||
// Display (to diagnostics) details of the device.
|
||||
void IODevice::_display() {
|
||||
DIAG(F("Unknown device Vpins:%d-%d %S"),
|
||||
(int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
// Find device associated with nominated Vpin and pass configuration values on to it.
|
||||
// Return false if not found.
|
||||
bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
|
||||
IODevice *dev = findDevice(vpin);
|
||||
if (dev) return dev->_configure(vpin, configType, paramCount, params);
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IODevice::configure(): Vpin ID %d not found!"), (int)vpin);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read value from virtual pin.
|
||||
int IODevice::read(VPIN vpin) {
|
||||
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
|
||||
if (dev->owns(vpin))
|
||||
return dev->_read(vpin);
|
||||
}
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read analogue value from virtual pin.
|
||||
int IODevice::readAnalogue(VPIN vpin) {
|
||||
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
|
||||
if (dev->owns(vpin))
|
||||
return dev->_readAnalogue(vpin);
|
||||
}
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write value to virtual pin(s). If multiple devices are allocated the same pin
|
||||
// then only the first one found will be used.
|
||||
void IODevice::write(VPIN vpin, int value) {
|
||||
IODevice *dev = findDevice(vpin);
|
||||
if (dev) {
|
||||
dev->_write(vpin, value);
|
||||
return;
|
||||
}
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Write analogue value to virtual pin(s). If multiple devices are allocated
|
||||
// the same pin then only the first one found will be used.
|
||||
//
|
||||
// The significance of param1 and param2 may vary from device to device.
|
||||
// For servo controllers, param1 is the profile of the transition and param2
|
||||
// the duration, i.e. the time that the operation is to be animated over
|
||||
// in deciseconds (0-3276 sec)
|
||||
//
|
||||
void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
|
||||
IODevice *dev = findDevice(vpin);
|
||||
if (dev) {
|
||||
dev->_writeAnalogue(vpin, value, param1, param2);
|
||||
return;
|
||||
}
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin);
|
||||
#endif
|
||||
}
|
||||
|
||||
// isBusy, when called for a device pin is always a digital output or analogue output,
|
||||
// returns input feedback state of the pin, i.e. whether the pin is busy performing
|
||||
// an animation or fade over a period of time.
|
||||
bool IODevice::isBusy(VPIN vpin) {
|
||||
IODevice *dev = findDevice(vpin);
|
||||
if (dev)
|
||||
return dev->_read(vpin);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
|
||||
if (pinNumber >= 0)
|
||||
pinMode(pinNumber, INPUT_PULLUP);
|
||||
_gpioInterruptPin = pinNumber;
|
||||
}
|
||||
|
||||
// Private helper function to add a device to the chain of devices.
|
||||
void IODevice::addDevice(IODevice *newDevice) {
|
||||
// Link new object to the end of the chain. Thereby, the first devices to be declared/created
|
||||
// will be located faster by findDevice than those which are created later.
|
||||
// Ideally declare/create the digital IO pins first, then servos, then more esoteric devices.
|
||||
IODevice *lastDevice;
|
||||
if (_firstDevice == 0)
|
||||
_firstDevice = newDevice;
|
||||
else {
|
||||
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice)
|
||||
lastDevice = dev;
|
||||
lastDevice->_nextDevice = newDevice;
|
||||
}
|
||||
newDevice->_nextDevice = 0;
|
||||
|
||||
// If the IODevice::begin() method has already been called, initialise device here. If not,
|
||||
// the device's _begin() method will be called by IODevice::begin().
|
||||
if (!_initPhase)
|
||||
newDevice->_begin();
|
||||
}
|
||||
|
||||
// Private helper function to locate a device by VPIN. Returns NULL if not found.
|
||||
// This is performance-critical, so minimises the calculation and function calls necessary.
|
||||
IODevice *IODevice::findDevice(VPIN vpin) {
|
||||
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
|
||||
VPIN firstVpin = dev->_firstVpin;
|
||||
if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)
|
||||
return dev;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//==================================================================================================================
|
||||
// Static data
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Chain of callback blocks (identifying registered callback functions for state changes)
|
||||
IONotifyCallback *IONotifyCallback::first = 0;
|
||||
|
||||
// Start of chain of devices.
|
||||
IODevice *IODevice::_firstDevice = 0;
|
||||
|
||||
// Reference to next device to be called on _loop() method.
|
||||
IODevice *IODevice::_nextLoopDevice = 0;
|
||||
|
||||
// Flag which is reset when IODevice::begin has been called.
|
||||
bool IODevice::_initPhase = true;
|
||||
|
||||
|
||||
//==================================================================================================================
|
||||
// Instance members
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Method to check whether the id corresponds to this device
|
||||
bool IODevice::owns(VPIN id) {
|
||||
return (id >= _firstVpin && id < _firstVpin + _nPins);
|
||||
}
|
||||
|
||||
|
||||
#else // !defined(IO_NO_HAL)
|
||||
|
||||
// Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more.
|
||||
|
||||
void IODevice::begin() { DIAG(F("NO HAL CONFIGURED!")); }
|
||||
bool IODevice::configure(VPIN pin, ConfigTypeEnum configType, int nParams, int p[]) {
|
||||
if (configType!=CONFIGURE_INPUT || nParams!=1 || pin >= NUM_DIGITAL_PINS) return false;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, p[0]);
|
||||
#endif
|
||||
pinMode(pin, p[0] ? INPUT_PULLUP : INPUT);
|
||||
return true;
|
||||
}
|
||||
void IODevice::write(VPIN vpin, int value) {
|
||||
if (vpin >= NUM_DIGITAL_PINS) return;
|
||||
digitalWrite(vpin, value);
|
||||
pinMode(vpin, OUTPUT);
|
||||
}
|
||||
void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {}
|
||||
bool IODevice::isBusy(VPIN) { return false; }
|
||||
bool IODevice::hasCallback(VPIN) { return false; }
|
||||
int IODevice::read(VPIN vpin) {
|
||||
if (vpin >= NUM_DIGITAL_PINS) return 0;
|
||||
return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1)
|
||||
}
|
||||
int IODevice::readAnalogue(VPIN vpin) {
|
||||
pinMode(vpin, INPUT);
|
||||
noInterrupts();
|
||||
int value = analogRead(vpin);
|
||||
interrupts();
|
||||
return value;
|
||||
}
|
||||
void IODevice::loop() {}
|
||||
void IODevice::DumpAll() {
|
||||
DIAG(F("NO HAL CONFIGURED!"));
|
||||
}
|
||||
bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < NUM_DIGITAL_PINS); }
|
||||
void IODevice::setGPIOInterruptPin(int16_t) {}
|
||||
|
||||
// Chain of callback blocks (identifying registered callback functions for state changes)
|
||||
// Not used in IO_NO_HAL but must be declared.
|
||||
IONotifyCallback *IONotifyCallback::first = 0;
|
||||
|
||||
#endif // IO_NO_HAL
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Constructor
|
||||
ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
int arrayLen = (_nPins+7)/8;
|
||||
_pinPullups = (uint8_t *)calloc(3, arrayLen);
|
||||
_pinModes = (&_pinPullups[0]) + arrayLen;
|
||||
_pinInUse = (&_pinPullups[0]) + 2*arrayLen;
|
||||
for (int i=0; i<arrayLen; i++) {
|
||||
_pinPullups[i] = 0xff; // default to pullup on, for inputs
|
||||
_pinModes[i] = 0;
|
||||
_pinInUse[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Device-specific pin configuration. Configure should be called infrequently so simplify
|
||||
// code by using the standard pinMode function.
|
||||
bool ArduinoPins::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
|
||||
if (configType != CONFIGURE_INPUT) return false;
|
||||
if (paramCount != 1) return false;
|
||||
bool pullup = params[0];
|
||||
|
||||
int pin = vpin;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, pullup);
|
||||
#endif
|
||||
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
|
||||
uint8_t index = (pin-_firstVpin) / 8;
|
||||
_pinModes[index] &= ~mask; // set to input mode
|
||||
if (pullup) {
|
||||
_pinPullups[index] |= mask;
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
} else {
|
||||
_pinPullups[index] &= ~mask;
|
||||
pinMode(pin, INPUT);
|
||||
}
|
||||
_pinInUse[index] |= mask;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Device-specific write function.
|
||||
void ArduinoPins::_write(VPIN vpin, int value) {
|
||||
int pin = vpin;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Arduino Write Pin:%d Val:%d"), pin, value);
|
||||
#endif
|
||||
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
|
||||
uint8_t index = (pin-_firstVpin) / 8;
|
||||
// First update the output state, then set into write mode if not already.
|
||||
fastWriteDigital(pin, value);
|
||||
if (!(_pinModes[index] & mask)) {
|
||||
// Currently in read mode, change to write mode
|
||||
_pinModes[index] |= mask;
|
||||
// Since mode changes should be infrequent, use standard pinMode function
|
||||
pinMode(pin, OUTPUT);
|
||||
_pinInUse[index] |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
// Device-specific read function (digital input).
|
||||
int ArduinoPins::_read(VPIN vpin) {
|
||||
int pin = vpin;
|
||||
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
|
||||
uint8_t index = (pin-_firstVpin) / 8;
|
||||
if ((_pinModes[index] | ~_pinInUse[index]) & mask) {
|
||||
// Currently in write mode or not initialised, change to read mode
|
||||
_pinModes[index] &= ~mask;
|
||||
// Since mode changes should be infrequent, use standard pinMode function
|
||||
if (_pinPullups[index] & mask)
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
else
|
||||
pinMode(pin, INPUT);
|
||||
_pinInUse[index] |= mask;
|
||||
}
|
||||
int value = !fastReadDigital(pin); // Invert (5v=0, 0v=1)
|
||||
|
||||
#ifdef DIAG_IO
|
||||
//DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
// Device-specific readAnalogue function (analogue input)
|
||||
int ArduinoPins::_readAnalogue(VPIN vpin) {
|
||||
int pin = vpin;
|
||||
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
|
||||
uint8_t index = (pin-_firstVpin) / 8;
|
||||
if (_pinModes[index] & mask) {
|
||||
// Currently in write mode, change to read mode
|
||||
_pinModes[index] &= ~mask;
|
||||
// Since mode changes should be infrequent, use standard pinMode function
|
||||
if (_pinPullups[index] & mask)
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
else
|
||||
pinMode(pin, INPUT);
|
||||
}
|
||||
|
||||
// Since AnalogRead is also called from interrupt code, disable interrupts
|
||||
// while we're using it. There's only one ADC shared by all analogue inputs
|
||||
// on the Arduino, so we don't want interruptions.
|
||||
//******************************************************************************
|
||||
// NOTE: If the HAL is running on a computer without the DCC signal generator,
|
||||
// then interrupts needn't be disabled. Also, the DCC signal generator puts
|
||||
// the ADC into fast mode, so if it isn't present, analogueRead calls will be much
|
||||
// slower!!
|
||||
//******************************************************************************
|
||||
noInterrupts();
|
||||
int value = analogRead(pin);
|
||||
interrupts();
|
||||
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
void ArduinoPins::_display() {
|
||||
DIAG(F("Arduino Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) {
|
||||
#if defined(USE_FAST_IO)
|
||||
if (pin >= NUM_DIGITAL_PINS) return;
|
||||
uint8_t mask = digitalPinToBitMask(pin);
|
||||
uint8_t port = digitalPinToPort(pin);
|
||||
volatile uint8_t *outPortAdr = portOutputRegister(port);
|
||||
noInterrupts();
|
||||
if (value)
|
||||
*outPortAdr |= mask;
|
||||
else
|
||||
*outPortAdr &= ~mask;
|
||||
interrupts();
|
||||
#else
|
||||
digitalWrite(pin, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ArduinoPins::fastReadDigital(uint8_t pin) {
|
||||
#if defined(USE_FAST_IO)
|
||||
if (pin >= NUM_DIGITAL_PINS) return false;
|
||||
uint8_t mask = digitalPinToBitMask(pin);
|
||||
uint8_t port = digitalPinToPort(pin);
|
||||
volatile uint8_t *inPortAdr = portInputRegister(port);
|
||||
// read input
|
||||
bool result = (*inPortAdr & mask) != 0;
|
||||
#else
|
||||
bool result = digitalRead(pin);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
371
IODevice.h
Normal file
371
IODevice.h
Normal file
@ -0,0 +1,371 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef iodevice_h
|
||||
#define iodevice_h
|
||||
|
||||
// Define symbol DIAG_IO to enable diagnostic output
|
||||
//#define DIAG_IO Y
|
||||
|
||||
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
|
||||
//#define DIAG_LOOPTIMES
|
||||
|
||||
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
|
||||
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
|
||||
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
|
||||
#define IO_NO_HAL
|
||||
#endif
|
||||
|
||||
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
|
||||
// animation has completed. This switches off the servo motor, preventing
|
||||
// the continuous buzz sometimes found on servos, and reducing the
|
||||
// power consumption of the servo when inactive.
|
||||
// It is recommended to enable this, unless it causes you problems.
|
||||
#define IO_SWITCH_OFF_SERVO
|
||||
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
#include "I2CManager.h"
|
||||
#include "inttypes.h"
|
||||
|
||||
typedef uint16_t VPIN;
|
||||
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
|
||||
// This should be enough for 99% of users.
|
||||
#define VPIN_MAX 32767
|
||||
#define VPIN_NONE 65535
|
||||
|
||||
/*
|
||||
* Callback support for state change notification from an IODevice subclass to a
|
||||
* handler, e.g. Sensor object handling.
|
||||
*/
|
||||
|
||||
class IONotifyCallback {
|
||||
public:
|
||||
typedef void IONotifyCallbackFunction(VPIN vpin, int value);
|
||||
static void add(IONotifyCallbackFunction *function) {
|
||||
IONotifyCallback *blk = new IONotifyCallback(function);
|
||||
if (first) blk->next = first;
|
||||
first = blk;
|
||||
}
|
||||
static void invokeAll(VPIN vpin, int value) {
|
||||
for (IONotifyCallback *blk = first; blk != NULL; blk = blk->next)
|
||||
blk->invoke(vpin, value);
|
||||
}
|
||||
static bool hasCallback() {
|
||||
return first != NULL;
|
||||
}
|
||||
private:
|
||||
IONotifyCallback(IONotifyCallbackFunction *function) { invoke = function; };
|
||||
IONotifyCallback *next = 0;
|
||||
IONotifyCallbackFunction *invoke = 0;
|
||||
static IONotifyCallback *first;
|
||||
};
|
||||
|
||||
/*
|
||||
* IODevice class
|
||||
*
|
||||
* This class is the basis of the Hardware Abstraction Layer (HAL) for
|
||||
* the DCC++EX Command Station. All device classes derive from this.
|
||||
*
|
||||
*/
|
||||
|
||||
class IODevice {
|
||||
public:
|
||||
|
||||
// Parameter values to identify type of call to IODevice::configure.
|
||||
typedef enum : uint8_t {
|
||||
CONFIGURE_INPUT = 1,
|
||||
CONFIGURE_SERVO = 2,
|
||||
CONFIGURE_OUTPUT = 3,
|
||||
} ConfigTypeEnum;
|
||||
|
||||
typedef enum : uint8_t {
|
||||
DEVSTATE_DORMANT = 0,
|
||||
DEVSTATE_PROBING = 1,
|
||||
DEVSTATE_INITIALISING = 2,
|
||||
DEVSTATE_NORMAL = 3,
|
||||
DEVSTATE_SCANNING = 4,
|
||||
DEVSTATE_FAILED = 5,
|
||||
} DeviceStateEnum;
|
||||
|
||||
// Static functions to find the device and invoke its member functions
|
||||
|
||||
// begin is invoked to create any standard IODevice subclass instances.
|
||||
// Also, the _begin method of any existing instances is called from here.
|
||||
static void begin();
|
||||
|
||||
// configure is used invoke an IODevice instance's _configure method
|
||||
static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]);
|
||||
|
||||
// User-friendly function for configuring an input pin.
|
||||
inline static bool configureInput(VPIN vpin, bool pullupEnable) {
|
||||
int params[] = {pullupEnable};
|
||||
return IODevice::configure(vpin, CONFIGURE_INPUT, 1, params);
|
||||
}
|
||||
|
||||
// User-friendly function for configuring a servo pin.
|
||||
inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile=0, uint16_t duration=0, uint8_t initialState=0) {
|
||||
int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState};
|
||||
return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params);
|
||||
}
|
||||
|
||||
// write invokes the IODevice instance's _write method.
|
||||
static void write(VPIN vpin, int value);
|
||||
|
||||
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
|
||||
static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);
|
||||
|
||||
// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing
|
||||
// the output over a period of time.
|
||||
static bool isBusy(VPIN vpin);
|
||||
|
||||
// check whether the pin supports notification. If so, then regular _read calls are not required.
|
||||
static bool hasCallback(VPIN vpin);
|
||||
|
||||
// read invokes the IODevice instance's _read method.
|
||||
static int read(VPIN vpin);
|
||||
|
||||
// read invokes the IODevice instance's _readAnalogue method.
|
||||
static int readAnalogue(VPIN vpin);
|
||||
|
||||
// loop invokes the IODevice instance's _loop method.
|
||||
static void loop();
|
||||
|
||||
static void DumpAll();
|
||||
|
||||
// exists checks whether there is a device owning the specified vpin
|
||||
static bool exists(VPIN vpin);
|
||||
|
||||
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
|
||||
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
|
||||
// Without the shared interrupt, input states are scanned periodically to detect changes on
|
||||
// GPIO extender pins. If a shared interrupt pin is configured, then input states are scanned
|
||||
// only when the shared interrupt pin is pulled low. The external GPIO module releases the pin
|
||||
// once the GPIO port concerned has been read.
|
||||
void setGPIOInterruptPin(int16_t pinNumber);
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
// Constructor
|
||||
IODevice(VPIN firstVpin=0, int nPins=0) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
_nextEntryTime = 0;
|
||||
}
|
||||
|
||||
// Method to perform initialisation of the device (optionally implemented within device class)
|
||||
virtual void _begin() {}
|
||||
|
||||
// Method to configure device (optionally implemented within device class)
|
||||
virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
|
||||
(void)vpin; (void)configType; (void)paramCount; (void)params; // Suppress compiler warning.
|
||||
return false;
|
||||
};
|
||||
|
||||
// Method to write new state (optionally implemented within device class)
|
||||
virtual void _write(VPIN vpin, int value) {
|
||||
(void)vpin; (void)value;
|
||||
};
|
||||
|
||||
// Method to write an 'analogue' value (optionally implemented within device class)
|
||||
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
|
||||
(void)vpin; (void)value; (void) param1; (void)param2;
|
||||
};
|
||||
|
||||
// Method to read digital pin state (optionally implemented within device class)
|
||||
virtual int _read(VPIN vpin) {
|
||||
(void)vpin;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Method to read analogue pin state (optionally implemented within device class)
|
||||
virtual int _readAnalogue(VPIN vpin) {
|
||||
(void)vpin;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Method to perform updates on an ongoing basis (optionally implemented within device class)
|
||||
virtual void _loop(unsigned long currentMicros) {
|
||||
delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls.
|
||||
};
|
||||
|
||||
// Method for displaying info on DIAG output (optionally implemented within device class)
|
||||
virtual void _display();
|
||||
|
||||
// Destructor
|
||||
virtual ~IODevice() {};
|
||||
|
||||
// Non-virtual function
|
||||
void delayUntil(unsigned long futureMicrosCount) {
|
||||
_nextEntryTime = futureMicrosCount;
|
||||
}
|
||||
|
||||
// Common object fields.
|
||||
VPIN _firstVpin;
|
||||
int _nPins;
|
||||
|
||||
// Flag whether the device supports callbacks.
|
||||
bool _hasCallback = false;
|
||||
|
||||
// Pin number of interrupt pin for GPIO extender devices. The extender module will pull this
|
||||
// pin low if an input changes state.
|
||||
int16_t _gpioInterruptPin = -1;
|
||||
|
||||
// Static support function for subclass creation
|
||||
static void addDevice(IODevice *newDevice);
|
||||
|
||||
// Current state of device
|
||||
DeviceStateEnum _deviceState = DEVSTATE_DORMANT;
|
||||
|
||||
private:
|
||||
// Method to check whether the vpin corresponds to this device
|
||||
bool owns(VPIN vpin);
|
||||
// Method to find device handling Vpin
|
||||
static IODevice *findDevice(VPIN vpin);
|
||||
|
||||
IODevice *_nextDevice = 0;
|
||||
unsigned long _nextEntryTime;
|
||||
static IODevice *_firstDevice;
|
||||
|
||||
static IODevice *_nextLoopDevice;
|
||||
static bool _initPhase;
|
||||
};
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for PCA9685 16-channel PWM module.
|
||||
*/
|
||||
|
||||
class PCA9685 : public IODevice {
|
||||
public:
|
||||
static void create(VPIN vpin, int nPins, uint8_t I2CAddress);
|
||||
// Constructor
|
||||
PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress);
|
||||
enum ProfileType : uint8_t {
|
||||
Instant = 0, // Moves immediately between positions (if duration not specified)
|
||||
UseDuration = 0, // Use specified duration
|
||||
Fast = 1, // Takes around 500ms end-to-end
|
||||
Medium = 2, // 1 second end-to-end
|
||||
Slow = 3, // 2 seconds end-to-end
|
||||
Bounce = 4, // For semaphores/turnouts with a bit of bounce!!
|
||||
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
|
||||
};
|
||||
|
||||
private:
|
||||
// Device-specific initialisation
|
||||
void _begin() override;
|
||||
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
|
||||
// Device-specific write functions.
|
||||
void _write(VPIN vpin, int value) override;
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override;
|
||||
int _read(VPIN vpin) override; // returns the digital state or busy status of the device
|
||||
void _loop(unsigned long currentMicros) override;
|
||||
void updatePosition(uint8_t pin);
|
||||
void writeDevice(uint8_t pin, int value);
|
||||
void _display() override;
|
||||
|
||||
uint8_t _I2CAddress; // 0x40-0x43 possible
|
||||
|
||||
struct ServoData {
|
||||
uint16_t activePosition : 12; // Config parameter
|
||||
uint16_t inactivePosition : 12; // Config parameter
|
||||
uint16_t currentPosition : 12;
|
||||
uint16_t fromPosition : 12;
|
||||
uint16_t toPosition : 12;
|
||||
uint8_t profile; // Config parameter
|
||||
uint16_t stepNumber; // Index of current step (starting from 0)
|
||||
uint16_t numSteps; // Number of steps in animation, or 0 if none in progress.
|
||||
uint8_t currentProfile; // profile being used for current animation.
|
||||
uint16_t duration; // time (tenths of a second) for animation to complete.
|
||||
}; // 14 bytes per element, i.e. per pin in use
|
||||
|
||||
struct ServoData *_servoData [16];
|
||||
|
||||
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
|
||||
static const byte FLASH _bounceProfile[30];
|
||||
|
||||
const unsigned int refreshInterval = 50; // refresh every 50ms
|
||||
|
||||
// structures for setting up non-blocking writes to servo controller
|
||||
I2CRB requestBlock;
|
||||
uint8_t outputBuffer[5];
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for DCC accessory decoder.
|
||||
*/
|
||||
|
||||
class DCCAccessoryDecoder: public IODevice {
|
||||
public:
|
||||
static void create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
|
||||
// Constructor
|
||||
DCCAccessoryDecoder(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
|
||||
|
||||
private:
|
||||
// Device-specific write function.
|
||||
void _begin() override;
|
||||
void _write(VPIN vpin, int value) override;
|
||||
void _display() override;
|
||||
int _packedAddress;
|
||||
};
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for arduino input/output pins.
|
||||
*/
|
||||
|
||||
class ArduinoPins: public IODevice {
|
||||
public:
|
||||
static void create(VPIN firstVpin, int nPins) {
|
||||
addDevice(new ArduinoPins(firstVpin, nPins));
|
||||
}
|
||||
|
||||
// Constructor
|
||||
ArduinoPins(VPIN firstVpin, int nPins);
|
||||
|
||||
static void fastWriteDigital(uint8_t pin, uint8_t value);
|
||||
static bool fastReadDigital(uint8_t pin);
|
||||
|
||||
private:
|
||||
// Device-specific pin configuration
|
||||
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
|
||||
// Device-specific write function.
|
||||
void _write(VPIN vpin, int value) override;
|
||||
// Device-specific read functions.
|
||||
int _read(VPIN vpin) override;
|
||||
int _readAnalogue(VPIN vpin) override;
|
||||
void _display() override;
|
||||
|
||||
|
||||
uint8_t *_pinPullups;
|
||||
uint8_t *_pinModes; // each bit is 1 for output, 0 for input
|
||||
uint8_t *_pinInUse;
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "IO_MCP23008.h"
|
||||
#include "IO_MCP23017.h"
|
||||
#include "IO_PCF8574.h"
|
||||
|
||||
#endif // iodevice_h
|
165
IO_AnalogueInputs.h
Normal file
165
IO_AnalogueInputs.h
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef io_analogueinputs_h
|
||||
#define io_analogueinputs_h
|
||||
|
||||
// Uncomment following line to slow the scan cycle down to 1second ADC samples, with
|
||||
// diagnostic output of scanned values.
|
||||
//#define IO_ANALOGUE_SLOW
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/**********************************************************************************************
|
||||
* ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115.
|
||||
*
|
||||
* ADS1113 and ADS1114 are restricted to 1 input. ADS1115 has a multiplexer which allows
|
||||
* any of four input pins to be read by its ADC.
|
||||
*
|
||||
* The driver polls the device in accordance with the constant 'scanInterval' below. On first loop
|
||||
* entry, the multiplexer is set to pin A0 and the ADC is triggered. On second and subsequent
|
||||
* entries, the analogue value is read from the conversion register and then the multiplexer and
|
||||
* ADC are set up to read the next pin.
|
||||
*
|
||||
* The ADS111x is set up as follows:
|
||||
* Single-shot scan
|
||||
* Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms)
|
||||
* Comparator off
|
||||
* Gain FSR=6.144V
|
||||
* The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading
|
||||
* of 32767*(5.0/6.144) = 26666.
|
||||
*
|
||||
* A device is configured by the following:
|
||||
* ADS111x::create(firstVpin, nPins, i2cAddress);
|
||||
* for example
|
||||
* ADS111x::create(300, 1, 0x48); // single-input ADS1113
|
||||
* ADS111x::create(300, 4, 0x48); // four-input ADS1115
|
||||
*
|
||||
* Note: The device is simple and does not need initial configuration, so it should recover from
|
||||
* temporary loss of communications or power.
|
||||
**********************************************************************************************/
|
||||
class ADS111x: public IODevice {
|
||||
public:
|
||||
ADS111x(VPIN firstVpin, int nPins, uint8_t i2cAddress) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = min(nPins,4);
|
||||
_i2cAddress = i2cAddress;
|
||||
_currentPin = 0;
|
||||
for (int8_t i=0; i<_nPins; i++)
|
||||
_value[i] = -1;
|
||||
addDevice(this);
|
||||
}
|
||||
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) {
|
||||
new ADS111x(firstVpin, nPins, i2cAddress);
|
||||
}
|
||||
private:
|
||||
void _begin() {
|
||||
// Initialise ADS device
|
||||
if (I2CManager.exists(_i2cAddress)) {
|
||||
_nextState = STATE_STARTSCAN;
|
||||
#ifdef DIAG_IO
|
||||
_display();
|
||||
#endif
|
||||
} else {
|
||||
DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress);
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
}
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
|
||||
// Check that previous non-blocking write has completed, if not then wait
|
||||
uint8_t status = _i2crb.status;
|
||||
if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything.
|
||||
if (status == I2C_STATUS_OK) {
|
||||
switch (_nextState) {
|
||||
case STATE_STARTSCAN:
|
||||
// Configure ADC and multiplexer for next scan. See ADS111x datasheet for details
|
||||
// of configuration register settings.
|
||||
_outBuffer[0] = 0x01; // Config register address
|
||||
_outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n
|
||||
_outBuffer[2] = 0xA3; // 250 samples/sec, comparator off
|
||||
// Write command, without waiting for completion.
|
||||
I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb);
|
||||
|
||||
delayUntil(currentMicros + scanInterval);
|
||||
_nextState = STATE_STARTREAD;
|
||||
break;
|
||||
|
||||
case STATE_STARTREAD:
|
||||
// Reading the pin value
|
||||
_outBuffer[0] = 0x00; // Conversion register address
|
||||
I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register
|
||||
_nextState = STATE_GETVALUE;
|
||||
break;
|
||||
|
||||
case STATE_GETVALUE:
|
||||
_value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1];
|
||||
#ifdef IO_ANALOGUE_SLOW
|
||||
DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]);
|
||||
#endif
|
||||
|
||||
// Move to next pin
|
||||
if (++_currentPin >= _nPins) _currentPin = 0;
|
||||
_nextState = STATE_STARTSCAN;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else { // error status
|
||||
DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
int pin = vpin - _firstVpin;
|
||||
return _value[pin];
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d %S"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1,
|
||||
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
// ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms.
|
||||
// This is enough to allow the conversion to reliably complete in time.
|
||||
#ifndef IO_ANALOGUE_SLOW
|
||||
const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds.
|
||||
#else
|
||||
const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds.
|
||||
#endif
|
||||
enum : uint8_t {
|
||||
STATE_STARTSCAN,
|
||||
STATE_STARTREAD,
|
||||
STATE_GETVALUE,
|
||||
};
|
||||
uint16_t _value[4];
|
||||
uint8_t _i2cAddress;
|
||||
uint8_t _outBuffer[3];
|
||||
uint8_t _inBuffer[2];
|
||||
uint8_t _currentPin; // ADC pin currently being scanned
|
||||
I2CRB _i2crb;
|
||||
uint8_t _nextState;
|
||||
};
|
||||
|
||||
#endif // io_analogueinputs_h
|
68
IO_DCCAccessory.cpp
Normal file
68
IO_DCCAccessory.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "DCC.h"
|
||||
#include "IODevice.h"
|
||||
#include "DIAG.h"
|
||||
#include "defines.h"
|
||||
|
||||
#define PACKEDADDRESS(addr, subaddr) (((addr) << 2) + (subaddr))
|
||||
#define ADDRESS(packedaddr) ((packedaddr) >> 2)
|
||||
#define SUBADDRESS(packedaddr) ((packedaddr) % 4)
|
||||
|
||||
void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
|
||||
new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress);
|
||||
}
|
||||
|
||||
// Constructors
|
||||
DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
|
||||
_firstVpin = vpin;
|
||||
_nPins = nPins;
|
||||
_packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress);
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
void DCCAccessoryDecoder::_begin() {
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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
|
||||
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
|
||||
#endif
|
||||
#endif
|
||||
DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state);
|
||||
}
|
||||
|
||||
void DCCAccessoryDecoder::_display() {
|
||||
int endAddress = _packedAddress + _nPins - 1;
|
||||
DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
|
||||
ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));
|
||||
}
|
||||
|
255
IO_DFPlayer.h
Normal file
255
IO_DFPlayer.h
Normal file
@ -0,0 +1,255 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* DFPlayer is an MP3 player module with an SD card holder. It also has an integrated
|
||||
* amplifier, so it only needs a power supply and a speaker.
|
||||
*
|
||||
* This driver allows the device to be controlled through IODevice::write() and
|
||||
* IODevice::writeAnalogue() calls.
|
||||
*
|
||||
* The driver is configured as follows:
|
||||
*
|
||||
* DFPlayer::create(firstVpin, nPins, Serialn);
|
||||
*
|
||||
* Where firstVpin is the first vpin reserved for reading the device,
|
||||
* nPins is the number of pins to be allocated (max 5)
|
||||
* and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1).
|
||||
*
|
||||
* Example:
|
||||
* In mySetup function within mySetup.cpp:
|
||||
* DFPlayer::create(3500, 5, Serial1);
|
||||
*
|
||||
* Writing an analogue value 0-2999 to the first pin will select a numbered file from the SD card;
|
||||
* Writing an analogue value 0-30 to the second pin will set the volume of the output;
|
||||
* Writing a digital value to the first pin will play or stop the file;
|
||||
* Reading a digital value from any pin will return true(1) if the player is playing, false(0) otherwise.
|
||||
*
|
||||
* From EX-RAIL, the following commands may be used:
|
||||
* SET(3500) -- starts playing the first file on the SD card
|
||||
* SET(3501) -- starts playing the second file on the SD card
|
||||
* etc.
|
||||
* RESET(3500) -- stops all playing on the player
|
||||
* WAITFOR(3500) -- wait for the file currently being played by the player to complete
|
||||
* SERVO(3500,23,0) -- plays file 23 at current volume
|
||||
* SERVO(3500,23,30) -- plays file 23 at volume 30 (maximum)
|
||||
* SERVO(3501,20,0) -- Sets the volume to 20
|
||||
*
|
||||
* NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly
|
||||
* to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse.
|
||||
* A 1k resistor in series with the module's RX terminal will alleviate this.
|
||||
*/
|
||||
|
||||
#ifndef IO_DFPlayer_h
|
||||
#define IO_DFPlayer_h
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
class DFPlayer : public IODevice {
|
||||
private:
|
||||
HardwareSerial *_serial;
|
||||
bool _playing = false;
|
||||
uint8_t _inputIndex = 0;
|
||||
unsigned long _commandSendTime; // Allows timeout processing
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) :
|
||||
IODevice(firstVpin, nPins),
|
||||
_serial(&serial)
|
||||
{
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) {
|
||||
new DFPlayer(firstVpin, nPins, serial);
|
||||
}
|
||||
|
||||
protected:
|
||||
void _begin() override {
|
||||
_serial->begin(9600);
|
||||
_deviceState = DEVSTATE_INITIALISING;
|
||||
|
||||
// Send a query to the device to see if it responds
|
||||
sendPacket(0x42);
|
||||
_commandSendTime = micros();
|
||||
}
|
||||
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
// Check for incoming data on _serial, and update busy flag accordingly.
|
||||
// Expected message is in the form "7F FF 06 3D xx xx xx xx xx EF"
|
||||
while (_serial->available()) {
|
||||
int c = _serial->read();
|
||||
if (c == 0x7E)
|
||||
_inputIndex = 1;
|
||||
else if ((c==0xFF && _inputIndex==1)
|
||||
|| (c==0x3D && _inputIndex==3)
|
||||
|| (_inputIndex >=4 && _inputIndex <= 8))
|
||||
_inputIndex++;
|
||||
else if (c==0x06 && _inputIndex==2) {
|
||||
// Valid message prefix, so consider the device online
|
||||
if (_deviceState==DEVSTATE_INITIALISING) {
|
||||
_deviceState = DEVSTATE_NORMAL;
|
||||
#ifdef DIAG_IO
|
||||
_display();
|
||||
#endif
|
||||
}
|
||||
_inputIndex++;
|
||||
} else if (c==0xEF && _inputIndex==9) {
|
||||
// End of play
|
||||
if (_playing) {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Finished"));
|
||||
#endif
|
||||
_playing = false;
|
||||
}
|
||||
_inputIndex = 0;
|
||||
} else
|
||||
_inputIndex = 0; // Unrecognised character sequence, start again!
|
||||
}
|
||||
// Check if the initial prompt to device has timed out. Allow 1 second
|
||||
if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 1000000UL) {
|
||||
DIAG(F("DFPlayer device not responding on serial port"));
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
delayUntil(currentMicros + 10000); // Only enter every 10ms
|
||||
}
|
||||
|
||||
// Write with value 1 starts playing a song. The relative pin number is the file number.
|
||||
// Write with value 0 stops playing.
|
||||
void _write(VPIN vpin, int value) override {
|
||||
int pin = vpin - _firstVpin;
|
||||
if (value) {
|
||||
// Value 1, start playing
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Play %d"), pin+1);
|
||||
#endif
|
||||
sendPacket(0x03, pin+1);
|
||||
_playing = true;
|
||||
} else {
|
||||
// Value 0, stop playing
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Stop"));
|
||||
#endif
|
||||
sendPacket(0x16);
|
||||
_playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0.
|
||||
// Volume may be specified as second parameter to writeAnalogue.
|
||||
// If value is zero, the player stops playing.
|
||||
// WriteAnalogue on second pin sets the output volume.
|
||||
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
|
||||
uint8_t pin = vpin - _firstVpin;
|
||||
|
||||
// Validate parameter.
|
||||
volume = min(30,volume);
|
||||
|
||||
if (pin == 0) {
|
||||
// Play track
|
||||
if (value > 0) {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Play %d"), value);
|
||||
#endif
|
||||
sendPacket(0x03, value); // Play track
|
||||
_playing = true;
|
||||
if (volume > 0) {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Volume %d"), volume);
|
||||
#endif
|
||||
sendPacket(0x06, volume); // Set volume
|
||||
}
|
||||
} else {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Stop"));
|
||||
#endif
|
||||
sendPacket(0x16); // Stop play
|
||||
_playing = false;
|
||||
}
|
||||
} else if (pin == 1) {
|
||||
// Set volume (0-30)
|
||||
if (value > 30) value = 30;
|
||||
else if (value < 0) value = 0;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("DFPlayer: Volume %d"), value);
|
||||
#endif
|
||||
sendPacket(0x06, value);
|
||||
}
|
||||
}
|
||||
|
||||
// A read on any pin indicates whether the player is still playing.
|
||||
int _read(VPIN) override {
|
||||
return _playing;
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("DFPlayer Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1,
|
||||
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
private:
|
||||
// 7E FF 06 0F 00 01 01 xx xx EF
|
||||
// 0 -> 7E is start code
|
||||
// 1 -> FF is version
|
||||
// 2 -> 06 is length
|
||||
// 3 -> 0F is command
|
||||
// 4 -> 00 is no receive
|
||||
// 5~6 -> 01 01 is argument
|
||||
// 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 )
|
||||
// 9 -> EF is end code
|
||||
|
||||
void sendPacket(uint8_t command, uint16_t arg = 0)
|
||||
{
|
||||
uint8_t out[] = { 0x7E,
|
||||
0xFF,
|
||||
06,
|
||||
command,
|
||||
00,
|
||||
static_cast<uint8_t>(arg >> 8),
|
||||
static_cast<uint8_t>(arg & 0x00ff),
|
||||
00,
|
||||
00,
|
||||
0xEF };
|
||||
|
||||
setChecksum(out);
|
||||
|
||||
_serial->write(out, sizeof(out));
|
||||
}
|
||||
|
||||
uint16_t calcChecksum(uint8_t* packet)
|
||||
{
|
||||
uint16_t sum = 0;
|
||||
for (int i = 1; i < 7; i++)
|
||||
{
|
||||
sum += packet[i];
|
||||
}
|
||||
return -sum;
|
||||
}
|
||||
|
||||
void setChecksum(uint8_t* out)
|
||||
{
|
||||
uint16_t sum = calcChecksum(out);
|
||||
|
||||
out[7] = (sum >> 8);
|
||||
out[8] = (sum & 0xff);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // IO_DFPlayer_h
|
129
IO_ExampleSerial.cpp
Normal file
129
IO_ExampleSerial.cpp
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "IO_ExampleSerial.h"
|
||||
#include "FSH.h"
|
||||
|
||||
// Constructor
|
||||
IO_ExampleSerial::IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = nPins;
|
||||
_pinValues = (uint16_t *)calloc(_nPins, sizeof(uint16_t));
|
||||
_baud = baud;
|
||||
|
||||
// Save reference to serial port driver
|
||||
_serial = serial;
|
||||
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
// Static create method for one module.
|
||||
void IO_ExampleSerial::create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
|
||||
new IO_ExampleSerial(firstVpin, nPins, serial, baud);
|
||||
}
|
||||
|
||||
// Device-specific initialisation
|
||||
void IO_ExampleSerial::_begin() {
|
||||
_serial->begin(_baud);
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
|
||||
// Send a few # characters to the output
|
||||
for (uint8_t i=0; i<3; i++)
|
||||
_serial->write('#');
|
||||
}
|
||||
|
||||
// Device-specific write function. Write a string in the form "#Wm,n#"
|
||||
// where m is the vpin number, and n is the value.
|
||||
void IO_ExampleSerial::_write(VPIN vpin, int value) {
|
||||
int pin = vpin -_firstVpin;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("IO_ExampleSerial::_write Pin:%d Value:%d"), (int)vpin, value);
|
||||
#endif
|
||||
// Send a command string over the serial line
|
||||
_serial->print('#');
|
||||
_serial->print('W');
|
||||
_serial->print(pin);
|
||||
_serial->print(',');
|
||||
_serial->print(value);
|
||||
_serial->println('#');
|
||||
DIAG(F("ExampleSerial Sent command, p1=%d, p2=%d"), vpin, value);
|
||||
}
|
||||
|
||||
// Device-specific read function.
|
||||
int IO_ExampleSerial::_read(VPIN vpin) {
|
||||
|
||||
// Return a value for the specified vpin.
|
||||
int result = _pinValues[vpin-_firstVpin];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Loop function to do background scanning of the input port. State
|
||||
// machine parses the incoming command as it is received. Command
|
||||
// is in the form "#Nm,n#" where m is the index and n is the value.
|
||||
void IO_ExampleSerial::_loop(unsigned long currentMicros) {
|
||||
(void)currentMicros; // Suppress compiler warnings
|
||||
if (_serial->available()) {
|
||||
// Input data available to read. Read a character.
|
||||
char c = _serial->read();
|
||||
switch (_inputState) {
|
||||
case 0: // Waiting for start of command
|
||||
if (c == '#') // Start of command received.
|
||||
_inputState = 1;
|
||||
break;
|
||||
case 1: // Expecting command character
|
||||
if (c == 'N') { // 'Notify' character received
|
||||
_inputState = 2;
|
||||
_inputValue = _inputIndex = 0;
|
||||
} else
|
||||
_inputState = 0; // Unexpected char, reset
|
||||
break;
|
||||
case 2: // reading first parameter (index)
|
||||
if (isdigit(c))
|
||||
_inputIndex = _inputIndex * 10 + (c-'0');
|
||||
else if (c==',')
|
||||
_inputState = 3;
|
||||
else
|
||||
_inputState = 0; // Unexpected char, reset
|
||||
break;
|
||||
case 3: // reading reading second parameter (value)
|
||||
if (isdigit(c))
|
||||
_inputValue = _inputValue * 10 - (c-'0');
|
||||
else if (c=='#') { // End of command
|
||||
// Complete command received, do something with it.
|
||||
DIAG(F("ExampleSerial Received command, p1=%d, p2=%d"), _inputIndex, _inputValue);
|
||||
if (_inputIndex < _nPins) { // Store value
|
||||
_pinValues[_inputIndex] = _inputValue;
|
||||
}
|
||||
_inputState = 0; // Done, start again.
|
||||
} else
|
||||
_inputState = 0; // Unexpected char, reset
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IO_ExampleSerial::_display() {
|
||||
DIAG(F("IO_ExampleSerial Configured on VPins:%d-%d"), (int)_firstVpin,
|
||||
(int)_firstVpin+_nPins-1);
|
||||
}
|
||||
|
58
IO_ExampleSerial.h
Normal file
58
IO_ExampleSerial.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* To declare a device instance,
|
||||
* IO_ExampleSerial myDevice(1000, 10, Serial3, 9600);
|
||||
* or to create programmatically,
|
||||
* IO_ExampleSerial::create(1000, 10, Serial3, 9600);
|
||||
*
|
||||
* (uses VPINs 1000-1009, talke on Serial 3 at 9600 baud.)
|
||||
*
|
||||
* See IO_ExampleSerial.cpp for the protocol used over the serial line.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef IO_EXAMPLESERIAL_H
|
||||
#define IO_EXAMPLESERIAL_H
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
class IO_ExampleSerial : public IODevice {
|
||||
public:
|
||||
IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
|
||||
static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
|
||||
|
||||
protected:
|
||||
void _begin() override;
|
||||
void _loop(unsigned long currentMicros) override;
|
||||
void _write(VPIN vpin, int value) override;
|
||||
int _read(VPIN vpin) override;
|
||||
void _display() override;
|
||||
|
||||
private:
|
||||
HardwareSerial *_serial;
|
||||
uint8_t _inputState = 0;
|
||||
int _inputIndex = 0;
|
||||
int _inputValue = 0;
|
||||
uint16_t *_pinValues; // Pointer to block of memory containing pin values
|
||||
unsigned long _baud;
|
||||
};
|
||||
|
||||
#endif // IO_EXAMPLESERIAL_H
|
249
IO_GPIOBase.h
Normal file
249
IO_GPIOBase.h
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef IO_GPIOBASE_H
|
||||
#define IO_GPIOBASE_H
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
// GPIOBase is defined as a class template. This allows it to be instantiated by
|
||||
// subclasses with different types, according to the number of pins on the GPIO module.
|
||||
// For example, GPIOBase<uint8_t> for 8 pins, GPIOBase<uint16_t> for 16 pins etc.
|
||||
// A module with up to 64 pins can be handled in this way (uint64_t).
|
||||
|
||||
template <class T>
|
||||
class GPIOBase : public IODevice {
|
||||
|
||||
protected:
|
||||
// Constructor
|
||||
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin);
|
||||
// Device-specific initialisation
|
||||
void _begin() override;
|
||||
// Device-specific pin configuration function.
|
||||
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
|
||||
// Pin write function.
|
||||
void _write(VPIN vpin, int value) override;
|
||||
// Pin read function.
|
||||
int _read(VPIN vpin) override;
|
||||
void _display() override;
|
||||
void _loop(unsigned long currentMicros) override;
|
||||
|
||||
// Data fields
|
||||
uint8_t _I2CAddress;
|
||||
// Allocate enough space for all input pins
|
||||
T _portInputState;
|
||||
T _portOutputState;
|
||||
T _portMode;
|
||||
T _portPullup;
|
||||
T _portInUse;
|
||||
// Interval between refreshes of each input port
|
||||
static const int _portTickTime = 4000;
|
||||
|
||||
// Virtual functions for interfacing with I2C GPIO Device
|
||||
virtual void _writeGpioPort() = 0;
|
||||
virtual void _readGpioPort(bool immediate=true) = 0;
|
||||
virtual void _writePullups() {};
|
||||
virtual void _writePortModes() {};
|
||||
virtual void _setupDevice() {};
|
||||
virtual void _processCompletion(uint8_t status) {
|
||||
(void)status; // Suppress compiler warning
|
||||
};
|
||||
|
||||
I2CRB requestBlock;
|
||||
FSH *_deviceName;
|
||||
};
|
||||
|
||||
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
|
||||
// file as the class declaration (above). Otherwise it won't compile!
|
||||
|
||||
// Constructor
|
||||
template <class T>
|
||||
GPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) :
|
||||
IODevice(firstVpin, nPins)
|
||||
{
|
||||
_deviceName = deviceName;
|
||||
_I2CAddress = I2CAddress;
|
||||
_gpioInterruptPin = interruptPin;
|
||||
_hasCallback = true;
|
||||
// Add device to list of devices.
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void GPIOBase<T>::_begin() {
|
||||
// Configure pin used for GPIO extender notification of change (if allocated)
|
||||
if (_gpioInterruptPin >= 0)
|
||||
pinMode(_gpioInterruptPin, INPUT_PULLUP);
|
||||
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(400000);
|
||||
if (I2CManager.exists(_I2CAddress)) {
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
_portMode = 0; // default to input mode
|
||||
_portPullup = -1; // default to pullup enabled
|
||||
_portInputState = -1;
|
||||
_portInUse = 0;
|
||||
_setupDevice();
|
||||
_deviceState = DEVSTATE_NORMAL;
|
||||
} else {
|
||||
DIAG(F("%S I2C:x%x Device not detected"), _deviceName, _I2CAddress);
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration parameters for inputs:
|
||||
// params[0]: enable pullup
|
||||
// params[1]: invert input (optional)
|
||||
template <class T>
|
||||
bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
|
||||
if (configType != CONFIGURE_INPUT) return false;
|
||||
if (paramCount == 0 || paramCount > 1) return false;
|
||||
bool pullup = params[0];
|
||||
int pin = vpin - _firstVpin;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("%S I2C:x%x Config Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, pullup);
|
||||
#endif
|
||||
uint16_t mask = 1 << pin;
|
||||
if (pullup)
|
||||
_portPullup |= mask;
|
||||
else
|
||||
_portPullup &= ~mask;
|
||||
// Mark that port has been accessed
|
||||
_portInUse |= mask;
|
||||
// Set input mode
|
||||
_portMode &= ~mask;
|
||||
|
||||
// Call subclass's virtual function to write to device
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
// Port change will be notified on next loop entry.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Periodically read the input port
|
||||
template <class T>
|
||||
void GPIOBase<T>::_loop(unsigned long currentMicros) {
|
||||
T lastPortStates = _portInputState;
|
||||
if (_deviceState == DEVSTATE_SCANNING && !requestBlock.isBusy()) {
|
||||
uint8_t status = requestBlock.status;
|
||||
if (status == I2C_STATUS_OK) {
|
||||
_deviceState = DEVSTATE_NORMAL;
|
||||
} else {
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
DIAG(F("%S I2C:x%x Error:%d %S"), _deviceName, _I2CAddress, status,
|
||||
I2CManager.getErrorMessage(status));
|
||||
}
|
||||
_processCompletion(status);
|
||||
// Set unused pin and write mode pin value to 1
|
||||
_portInputState |= ~_portInUse | _portMode;
|
||||
|
||||
// Scan for changes in input states and invoke callback (if present)
|
||||
T differences = lastPortStates ^ _portInputState;
|
||||
if (differences && IONotifyCallback::hasCallback()) {
|
||||
// Scan for differences bit by bit
|
||||
T mask = 1;
|
||||
for (int pin=0; pin<_nPins; pin++) {
|
||||
if (differences & mask) {
|
||||
// Change detected.
|
||||
IONotifyCallback::invokeAll(_firstVpin+pin, (_portInputState & mask) == 0);
|
||||
}
|
||||
mask <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DIAG_IO
|
||||
if (differences)
|
||||
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if interrupt configured. If not, or if it is active (pulled down), then
|
||||
// initiate a scan.
|
||||
if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) {
|
||||
// TODO: Could suppress reads if there are no pins configured as inputs!
|
||||
|
||||
// Read input
|
||||
if (_deviceState == DEVSTATE_NORMAL) {
|
||||
_readGpioPort(false); // Initiate non-blocking read
|
||||
_deviceState= DEVSTATE_SCANNING;
|
||||
}
|
||||
}
|
||||
// Delay next entry until tick elapsed.
|
||||
delayUntil(currentMicros + _portTickTime);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void GPIOBase<T>::_display() {
|
||||
DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress,
|
||||
_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void GPIOBase<T>::_write(VPIN vpin, int value) {
|
||||
int pin = vpin - _firstVpin;
|
||||
T mask = 1 << pin;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value);
|
||||
#endif
|
||||
|
||||
// Set port mode output if currently not output mode
|
||||
if (!(_portMode & mask)) {
|
||||
_portInUse |= mask;
|
||||
_portMode |= mask;
|
||||
_writePortModes();
|
||||
}
|
||||
|
||||
// Update port output state
|
||||
if (value)
|
||||
_portOutputState |= mask;
|
||||
else
|
||||
_portOutputState &= ~mask;
|
||||
|
||||
// Call subclass's virtual function to write to device.
|
||||
return _writeGpioPort();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
int GPIOBase<T>::_read(VPIN vpin) {
|
||||
int pin = vpin - _firstVpin;
|
||||
T mask = 1 << pin;
|
||||
|
||||
// Set port mode to input if currently output or first use
|
||||
if ((_portMode | ~_portInUse) & mask) {
|
||||
_portMode &= ~mask;
|
||||
_portInUse |= mask;
|
||||
_writePullups();
|
||||
_writePortModes();
|
||||
// Port won't have been read yet, so read it now.
|
||||
_readGpioPort();
|
||||
// Set unused pin and write mode pin value to 1
|
||||
_portInputState |= ~_portInUse | _portMode;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
|
||||
#endif
|
||||
}
|
||||
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)
|
||||
}
|
||||
|
||||
#endif
|
188
IO_HCSR04.h
Normal file
188
IO_HCSR04.h
Normal file
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver.
|
||||
* It is operated through two signal pins. When the transmit pin is set to 1
|
||||
* for 10us, on the falling edge the transmitter sends a short transmission of
|
||||
* 8 pulses (like a sonar 'ping'). This is reflected off objects and received
|
||||
* by the receiver. A pulse is sent on the receive pin whose length is equal
|
||||
* to the delay between the transmission of the pulse and the detection of
|
||||
* its echo. The distance of the reflecting object is calculated by halving
|
||||
* the time (to allow for the out and back distance), then multiplying by the
|
||||
* speed of sound (assumed to be constant).
|
||||
*
|
||||
* This driver polls the HC-SR04 by sending the trigger pulse and then measuring
|
||||
* the length of the received pulse. If the calculated distance is less than
|
||||
* the threshold, the output state returned by a read() call changes to 1. If
|
||||
* the distance is greater than the threshold plus a hysteresis margin, the
|
||||
* output changes to 0. The device also supports readAnalogue(), which returns
|
||||
* the measured distance in cm, or 32767 if the distance exceeds the
|
||||
* offThreshold.
|
||||
*
|
||||
* It might be thought that the measurement would be more reliable if interrupts
|
||||
* were disabled while the pulse is being timed. However, this would affect
|
||||
* other functions in the CS so the measurement is being performed with
|
||||
* interrupts enabled. Also, we could use an interrupt pin in the Arduino for
|
||||
* the timing, but the same consideration applies. In any case, the DCC
|
||||
* interrupt occurs once every 58us, so any IRC code is much faster than that.
|
||||
* And 58us corresponds to 1cm in the calculation, so the effect of
|
||||
* interrupts is negligible.
|
||||
*
|
||||
* Note: The timing accuracy required for measuring the pulse length means that
|
||||
* the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot
|
||||
* provide the required accuracy.
|
||||
*/
|
||||
|
||||
#ifndef IO_HCSR04_H
|
||||
#define IO_HCSR04_H
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
class HCSR04 : public IODevice {
|
||||
|
||||
private:
|
||||
// pins must be arduino GPIO pins, not extender pins or HAL pins.
|
||||
int _trigPin = -1;
|
||||
int _echoPin = -1;
|
||||
// Thresholds for setting active state in cm.
|
||||
uint8_t _onThreshold; // cm
|
||||
uint8_t _offThreshold; // cm
|
||||
// Last measured distance in cm.
|
||||
uint16_t _distance;
|
||||
// Active=1/inactive=0 state
|
||||
uint8_t _value = 0;
|
||||
// Factor for calculating the distance (cm) from echo time (ms).
|
||||
// Based on a speed of sound of 345 metres/second.
|
||||
const uint16_t factor = 58; // ms/cm
|
||||
|
||||
public:
|
||||
// Constructor perfroms static initialisation of the device object
|
||||
HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) {
|
||||
_firstVpin = vpin;
|
||||
_nPins = 1;
|
||||
_trigPin = trigPin;
|
||||
_echoPin = echoPin;
|
||||
_onThreshold = onThreshold;
|
||||
_offThreshold = offThreshold;
|
||||
addDevice(this);
|
||||
}
|
||||
|
||||
// Static create function provides alternative way to create object
|
||||
static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) {
|
||||
new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold);
|
||||
}
|
||||
|
||||
protected:
|
||||
// _begin function called to perform dynamic initialisation of the device
|
||||
void _begin() override {
|
||||
pinMode(_trigPin, OUTPUT);
|
||||
pinMode(_echoPin, INPUT);
|
||||
ArduinoPins::fastWriteDigital(_trigPin, 0);
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
}
|
||||
|
||||
// _read function - just return _value (calculated in _loop).
|
||||
int _read(VPIN vpin) override {
|
||||
(void)vpin; // avoid compiler warning
|
||||
return _value;
|
||||
}
|
||||
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
(void)vpin; // avoid compiler warning
|
||||
return _distance;
|
||||
}
|
||||
|
||||
// _loop function - read HC-SR04 once every 50 milliseconds.
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
read_HCSR04device();
|
||||
// Delay next loop entry until 50ms have elapsed.
|
||||
delayUntil(currentMicros + 50000UL);
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"),
|
||||
_firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold);
|
||||
}
|
||||
|
||||
private:
|
||||
// This polls the HC-SR04 device by sending a pulse and measuring the duration of
|
||||
// the pulse observed on the receive pin. In order to be kind to the rest of the CS
|
||||
// software, no interrupts are used and interrupts are not disabled. The pulse duration
|
||||
// is measured in a loop, using the micros() function. Therefore, interrupts from other
|
||||
// sources may affect the result. However, interrupts response code in CS typically takes
|
||||
// much less than the 58us frequency for the DCC interrupt, and 58us corresponds to only 1cm
|
||||
// in the HC-SR04.
|
||||
// To reduce chatter on the output, hysteresis is applied on reset: the output is set to 1 when the
|
||||
// measured distance is less than the onThreshold, and is set to 0 if the measured distance is
|
||||
// greater than the offThreshold.
|
||||
//
|
||||
void read_HCSR04device() {
|
||||
// uint16 enough to time up to 65ms
|
||||
uint16_t startTime, waitTime, currentTime, maxTime;
|
||||
|
||||
// If receive pin is still set on from previous call, abort the read.
|
||||
if (ArduinoPins::fastReadDigital(_echoPin))
|
||||
return;
|
||||
|
||||
// Send 10us pulse to trigger transmitter
|
||||
ArduinoPins::fastWriteDigital(_trigPin, 1);
|
||||
delayMicroseconds(10);
|
||||
ArduinoPins::fastWriteDigital(_trigPin, 0);
|
||||
|
||||
// Wait for receive pin to be set
|
||||
startTime = currentTime = micros();
|
||||
maxTime = factor * _offThreshold * 2;
|
||||
while (!ArduinoPins::fastReadDigital(_echoPin)) {
|
||||
// lastTime = currentTime;
|
||||
currentTime = micros();
|
||||
waitTime = currentTime - startTime;
|
||||
if (waitTime > maxTime) {
|
||||
// Timeout waiting for pulse start, abort the read
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for receive pin to reset, and measure length of pulse
|
||||
startTime = currentTime = micros();
|
||||
maxTime = factor * _offThreshold;
|
||||
while (ArduinoPins::fastReadDigital(_echoPin)) {
|
||||
currentTime = micros();
|
||||
waitTime = currentTime - startTime;
|
||||
// If pulse is too long then set return value to zero,
|
||||
// and finish without waiting for end of pulse.
|
||||
if (waitTime > maxTime) {
|
||||
// Pulse length longer than maxTime, reset value.
|
||||
_value = 0;
|
||||
_distance = 32767;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check if pulse length is below threshold, if so set value.
|
||||
//DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance);
|
||||
_distance = waitTime / factor; // in centimetres
|
||||
if (_distance < _onThreshold)
|
||||
_value = 1;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif //IO_HCSR04_H
|
100
IO_MCP23008.h
Normal file
100
IO_MCP23008.h
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef IO_MCP23008_H
|
||||
#define IO_MCP23008_H
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
|
||||
class MCP23008 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
|
||||
new MCP23008(firstVpin, nPins, I2CAddress, interruptPin);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
MCP23008(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("MCP23008"), firstVpin, min(nPins, 8), I2CAddress, interruptPin) {
|
||||
|
||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||
outputBuffer, sizeof(outputBuffer));
|
||||
outputBuffer[0] = REG_GPIO;
|
||||
}
|
||||
|
||||
private:
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState);
|
||||
}
|
||||
void _writePullups() override {
|
||||
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
|
||||
// is intended for use as an output but hasn't been written to yet.
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse);
|
||||
}
|
||||
void _writePortModes() override {
|
||||
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
|
||||
uint8_t temp = ~(_portMode & _portInUse);
|
||||
I2CManager.write(_I2CAddress, 2, REG_IODIR, temp);
|
||||
// Enable interrupt-on-change for in-use pins that are inputs (_portMode=0)
|
||||
temp = ~_portMode & _portInUse;
|
||||
I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00);
|
||||
I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp);
|
||||
}
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer;
|
||||
I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);
|
||||
_portInputState = buffer;
|
||||
} else {
|
||||
// Queue new request
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xff;
|
||||
}
|
||||
void _setupDevice() override {
|
||||
// IOCON is set ODR=1 (open drain shared interrupt pin), INTPOL=0 (active-Low)
|
||||
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x04);
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
_writeGpioPort();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[1];
|
||||
uint8_t outputBuffer[1];
|
||||
|
||||
enum {
|
||||
// Register definitions for MCP23008
|
||||
REG_IODIR=0x00,
|
||||
REG_GPINTEN=0x02,
|
||||
REG_INTCON=0x04,
|
||||
REG_IOCON=0x05,
|
||||
REG_GPPU=0x06,
|
||||
REG_GPIO=0x09,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif
|
112
IO_MCP23017.h
Normal file
112
IO_MCP23017.h
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef io_mcp23017_h
|
||||
#define io_mcp23017_h
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
#include "FSH.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
* IODevice subclass for MCP23017 16-bit I/O expander.
|
||||
*/
|
||||
|
||||
class MCP23017 : public GPIOBase<uint16_t> {
|
||||
public:
|
||||
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
|
||||
new MCP23017(vpin, min(nPins,16), I2CAddress, interruptPin);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
MCP23017(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint16_t>((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
|
||||
outputBuffer, sizeof(outputBuffer));
|
||||
outputBuffer[0] = REG_GPIOA;
|
||||
}
|
||||
|
||||
private:
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);
|
||||
}
|
||||
void _writePullups() override {
|
||||
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
|
||||
// is intended for use as an output but hasn't been written to yet.
|
||||
uint16_t temp = _portPullup & _portInUse;
|
||||
I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8);
|
||||
}
|
||||
void _writePortModes() override {
|
||||
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
|
||||
uint16_t temp = ~(_portMode & _portInUse);
|
||||
I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8);
|
||||
// Enable interrupt for in-use pins which are inputs (_portMode=0)
|
||||
temp = ~_portMode & _portInUse;
|
||||
I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);
|
||||
I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8);
|
||||
}
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer[2];
|
||||
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
|
||||
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];
|
||||
} else {
|
||||
// Queue new request
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = ((uint16_t)inputBuffer[1]<<8) | inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xffff;
|
||||
}
|
||||
|
||||
void _setupDevice() override {
|
||||
// IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)
|
||||
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);
|
||||
_writePortModes();
|
||||
_writePullups();
|
||||
_writeGpioPort();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[2];
|
||||
uint8_t outputBuffer[1];
|
||||
|
||||
enum {
|
||||
REG_IODIRA = 0x00,
|
||||
REG_IODIRB = 0x01,
|
||||
REG_GPINTENA = 0x04,
|
||||
REG_GPINTENB = 0x05,
|
||||
REG_INTCONA = 0x08,
|
||||
REG_INTCONB = 0x09,
|
||||
REG_IOCON = 0x0A,
|
||||
REG_GPPUA = 0x0C,
|
||||
REG_GPPUB = 0x0D,
|
||||
REG_GPIOA = 0x12,
|
||||
REG_GPIOB = 0x13,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif
|
274
IO_PCA9685.cpp
Normal file
274
IO_PCA9685.cpp
Normal file
@ -0,0 +1,274 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "IODevice.h"
|
||||
#include "I2CManager.h"
|
||||
#include "DIAG.h"
|
||||
|
||||
// REGISTER ADDRESSES
|
||||
static const byte PCA9685_MODE1=0x00; // Mode Register
|
||||
static const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
|
||||
static const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
|
||||
// MODE1 bits
|
||||
static const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
|
||||
static const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
|
||||
static const byte MODE1_RESTART=0x80; /**< Restart enabled */
|
||||
|
||||
static const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
|
||||
static const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
|
||||
static const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
|
||||
|
||||
// Predeclare helper function
|
||||
static void writeRegister(byte address, byte reg, byte value);
|
||||
|
||||
// Create device driver instance.
|
||||
void PCA9685::create(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
|
||||
new PCA9685(firstVpin, nPins, I2CAddress);
|
||||
}
|
||||
|
||||
// Configure a port on the PCA9685.
|
||||
bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
|
||||
if (configType != CONFIGURE_SERVO) return false;
|
||||
if (paramCount != 5) return false;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"),
|
||||
vpin, params[0], params[1], params[2], params[3], params[4]);
|
||||
#endif
|
||||
|
||||
int8_t pin = vpin - _firstVpin;
|
||||
struct ServoData *s = _servoData[pin];
|
||||
if (s == NULL) {
|
||||
_servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));
|
||||
s = _servoData[pin];
|
||||
if (!s) return false; // Check for failed memory allocation
|
||||
}
|
||||
|
||||
s->activePosition = params[0];
|
||||
s->inactivePosition = params[1];
|
||||
s->profile = params[2];
|
||||
s->duration = params[3];
|
||||
int state = params[4];
|
||||
|
||||
if (state != -1) {
|
||||
// Position servo to initial state
|
||||
_writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = min(nPins, 16);
|
||||
_I2CAddress = I2CAddress;
|
||||
// To save RAM, space for servo configuration is not allocated unless a pin is used.
|
||||
// Initialise the pointers to NULL.
|
||||
for (int i=0; i<_nPins; i++)
|
||||
_servoData[i] = NULL;
|
||||
|
||||
addDevice(this);
|
||||
|
||||
// Initialise structure used for setting pulse rate
|
||||
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
|
||||
}
|
||||
|
||||
// Device-specific initialisation
|
||||
void PCA9685::_begin() {
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
|
||||
// In reality, other devices including the Arduino will limit
|
||||
// the clock speed to a lower rate.
|
||||
|
||||
// Initialise I/O module here.
|
||||
if (I2CManager.exists(_I2CAddress)) {
|
||||
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
|
||||
writeRegister(_I2CAddress, PCA9685_PRESCALE, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period.
|
||||
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
|
||||
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
|
||||
// In theory, we should wait 500us before sending any other commands to each device, to allow
|
||||
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
|
||||
// plenty of other stuff to do before we will send a command.
|
||||
#if defined(DIAG_IO)
|
||||
_display();
|
||||
#endif
|
||||
} else
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
|
||||
// Device-specific write function, invoked from IODevice::write().
|
||||
// For this function, the configured profile is used.
|
||||
void PCA9685::_write(VPIN vpin, int value) {
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value);
|
||||
#endif
|
||||
int pin = vpin - _firstVpin;
|
||||
if (value) value = 1;
|
||||
|
||||
struct ServoData *s = _servoData[pin];
|
||||
if (s != NULL) {
|
||||
// Use configured parameters
|
||||
_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);
|
||||
} // else { /* ignorethe request */ }
|
||||
}
|
||||
|
||||
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
|
||||
// Profile is as follows:
|
||||
// Bit 7: 0=Set PWM to 0% to power off servo motor when finished
|
||||
// 1=Keep PWM pulses on (better when using PWM to drive an LED)
|
||||
// Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds)
|
||||
// 1 (Fast) Move servo in 0.5 seconds
|
||||
// 2 (Medium) Move servo in 1.0 seconds
|
||||
// 3 (Slow) Move servo in 2.0 seconds
|
||||
// 4 (Bounce) Servo 'bounces' at extremes.
|
||||
//
|
||||
void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) {
|
||||
if (_deviceState == DEVSTATE_FAILED) return;
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d"),
|
||||
vpin, value, profile, duration);
|
||||
#endif
|
||||
int pin = vpin - _firstVpin;
|
||||
if (value > 4095) value = 4095;
|
||||
else if (value < 0) value = 0;
|
||||
|
||||
struct ServoData *s = _servoData[pin];
|
||||
if (s == NULL) {
|
||||
// Servo pin not configured, so configure now using defaults
|
||||
s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);
|
||||
if (s == NULL) return; // Check for memory allocation failure
|
||||
s->activePosition = 0;
|
||||
s->inactivePosition = 0;
|
||||
s->currentPosition = value;
|
||||
s->profile = Instant; // Use instant profile (but not this time)
|
||||
}
|
||||
|
||||
// Animated profile. Initiate the appropriate action.
|
||||
s->currentProfile = profile;
|
||||
uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit.
|
||||
s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds
|
||||
profileValue==Medium ? 20 : // 1.0 seconds
|
||||
profileValue==Slow ? 40 : // 2.0 seconds
|
||||
profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds
|
||||
duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)
|
||||
s->stepNumber = 0;
|
||||
s->toPosition = value;
|
||||
s->fromPosition = s->currentPosition;
|
||||
}
|
||||
|
||||
// _read returns true if the device is currently in executing an animation,
|
||||
// changing the output over a period of time.
|
||||
int PCA9685::_read(VPIN vpin) {
|
||||
if (_deviceState == DEVSTATE_FAILED) return 0;
|
||||
int pin = vpin - _firstVpin;
|
||||
struct ServoData *s = _servoData[pin];
|
||||
if (s == NULL)
|
||||
return false; // No structure means no animation!
|
||||
else
|
||||
return (s->stepNumber < s->numSteps);
|
||||
}
|
||||
|
||||
void PCA9685::_loop(unsigned long currentMicros) {
|
||||
for (int pin=0; pin<_nPins; pin++) {
|
||||
updatePosition(pin);
|
||||
}
|
||||
delayUntil(currentMicros + refreshInterval * 1000UL);
|
||||
}
|
||||
|
||||
// Private function to reposition servo
|
||||
// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.
|
||||
void PCA9685::updatePosition(uint8_t pin) {
|
||||
struct ServoData *s = _servoData[pin];
|
||||
|
||||
if (s == NULL) return; // No pin configuration/state data
|
||||
|
||||
if (s->numSteps == 0) return; // No animation in progress
|
||||
|
||||
if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {
|
||||
// Go straight to end of sequence, output final position.
|
||||
s->stepNumber = s->numSteps-1;
|
||||
}
|
||||
|
||||
if (s->stepNumber < s->numSteps) {
|
||||
// Animation in progress, reposition servo
|
||||
s->stepNumber++;
|
||||
if ((s->currentProfile & ~NoPowerOff) == Bounce) {
|
||||
// Retrieve step positions from array in flash
|
||||
byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);
|
||||
s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);
|
||||
} else {
|
||||
// All other profiles - calculate step by linear interpolation between from and to positions.
|
||||
s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);
|
||||
}
|
||||
// Send servo command
|
||||
writeDevice(pin, s->currentPosition);
|
||||
} else if (s->stepNumber < s->numSteps + _catchupSteps) {
|
||||
// We've finished animation, wait a little to allow servo to catch up
|
||||
s->stepNumber++;
|
||||
} else if (s->stepNumber == s->numSteps + _catchupSteps
|
||||
&& s->currentPosition != 0) {
|
||||
#ifdef IO_SWITCH_OFF_SERVO
|
||||
if ((s->currentProfile & NoPowerOff) == 0) {
|
||||
// Wait has finished, so switch off PWM to prevent annoying servo buzz
|
||||
writeDevice(pin, 0);
|
||||
}
|
||||
#endif
|
||||
s->numSteps = 0; // Done now.
|
||||
}
|
||||
}
|
||||
|
||||
// writeDevice takes a pin in range 0 to _nPins-1 within the device, and a value
|
||||
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
|
||||
void PCA9685::writeDevice(uint8_t pin, int value) {
|
||||
#ifdef DIAG_IO
|
||||
DIAG(F("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value);
|
||||
#endif
|
||||
// Wait for previous request to complete
|
||||
uint8_t status = requestBlock.wait();
|
||||
if (status != I2C_STATUS_OK) {
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, I2CManager.getErrorMessage(status));
|
||||
} else {
|
||||
// Set up new request.
|
||||
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
|
||||
outputBuffer[1] = 0;
|
||||
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
|
||||
outputBuffer[3] = value & 0xff;
|
||||
outputBuffer[4] = value >> 8;
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Display details of this device.
|
||||
void PCA9685::_display() {
|
||||
DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (int)_firstVpin,
|
||||
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
// Internal helper function for this device
|
||||
static void writeRegister(byte address, byte reg, byte value) {
|
||||
I2CManager.write(address, 2, reg, value);
|
||||
}
|
||||
|
||||
// Profile for a bouncing signal or turnout
|
||||
// The profile below is in the range 0-100% and should be combined with the desired limits
|
||||
// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here,
|
||||
// i.e. the bounce is the same on the down action as on the up action. First entry isn't used.
|
||||
const byte FLASH PCA9685::_bounceProfile[30] =
|
||||
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};
|
102
IO_PCF8574.h
Normal file
102
IO_PCF8574.h
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The PCF8574 is a simple device; it only has one register. The device
|
||||
* input/output mode and pullup are configured through this, and the
|
||||
* output state is written and the input state read through it too.
|
||||
*
|
||||
* This is accomplished by having a weak resistor in series with the output,
|
||||
* and a read-back of the other end of the resistor. As an output, the
|
||||
* pin state is set to 1 or 0, and the output voltage goes to +5V or 0V
|
||||
* (through the weak resistor).
|
||||
*
|
||||
* In order to use the pin as an input, the output is written as
|
||||
* a '1' in order to pull up the resistor. Therefore the input will be
|
||||
* 1 unless the pin is pulled down externally, in which case it will be 0.
|
||||
*
|
||||
* As a consequence of this approach, it is not possible to use the device for
|
||||
* inputs without pullups.
|
||||
*/
|
||||
|
||||
#ifndef IO_PCF8574_H
|
||||
#define IO_PCF8574_H
|
||||
|
||||
#include "IO_GPIOBase.h"
|
||||
|
||||
class PCF8574 : public GPIOBase<uint8_t> {
|
||||
public:
|
||||
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
|
||||
new PCF8574(firstVpin, nPins, I2CAddress, interruptPin);
|
||||
}
|
||||
|
||||
PCF8574(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
|
||||
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, min(nPins, 8), I2CAddress, interruptPin)
|
||||
{
|
||||
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
|
||||
}
|
||||
|
||||
private:
|
||||
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
|
||||
void _writeGpioPort() override {
|
||||
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
|
||||
}
|
||||
|
||||
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
|
||||
// Therefore, writing '1' in _writePortModes is enough to set the module to input mode
|
||||
// and enable pull-up.
|
||||
void _writePullups() override { }
|
||||
|
||||
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
|
||||
void _writePortModes() override {
|
||||
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
|
||||
}
|
||||
|
||||
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
|
||||
// When not in immediate mode, it initiates a request using the request block and returns.
|
||||
// When the request completes, _processCompletion finishes the operation.
|
||||
void _readGpioPort(bool immediate) override {
|
||||
if (immediate) {
|
||||
uint8_t buffer[1];
|
||||
I2CManager.read(_I2CAddress, buffer, 1);
|
||||
_portInputState = buffer[0];
|
||||
} else {
|
||||
requestBlock.wait(); // Wait for preceding operation to complete
|
||||
// Issue new request to read GPIO register
|
||||
I2CManager.queueRequest(&requestBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// This function is invoked when an I/O operation on the requestBlock completes.
|
||||
void _processCompletion(uint8_t status) override {
|
||||
if (status == I2C_STATUS_OK)
|
||||
_portInputState = inputBuffer[0];
|
||||
else
|
||||
_portInputState = 0xff;
|
||||
}
|
||||
|
||||
// Set up device ports
|
||||
void _setupDevice() override {
|
||||
_writePortModes();
|
||||
}
|
||||
|
||||
uint8_t inputBuffer[1];
|
||||
};
|
||||
|
||||
#endif
|
299
IO_VL53L0X.h
Normal file
299
IO_VL53L0X.h
Normal file
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* © 2021, Neil McKechnie. All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX API
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* It is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting
|
||||
* the reflection of the pulse. The time between the pulse and the receipt of reflections
|
||||
* is measured and used to determine the distance to the reflecting object.
|
||||
*
|
||||
* For economy of memory and processing time, this driver includes only part of the code
|
||||
* that ST provide in their API. Also, the API code isn't very clear and it is not easy
|
||||
* to identify what operations are useful and what are not.
|
||||
* The operation shown here doesn't include any calibration, so is probably not as accurate
|
||||
* as using the full driver, but it's probably accurate enough for the purpose.
|
||||
*
|
||||
* The device driver allocates up to 3 vpins to the device. A digital read on the first pin
|
||||
* will return a value that indicates whether the object is within the threshold range (1)
|
||||
* or not (0). An analogue read on the first pin returns the last measured distance (in mm),
|
||||
* the second pin returns the signal strength, and the third pin returns detected
|
||||
* ambient light level. By default the device takes around 60ms to complete a ranging
|
||||
* operation, so we do a 100ms cycle (10 samples per second).
|
||||
*
|
||||
* The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module,
|
||||
* you can use this address. However, the address can be modified by software. If
|
||||
* you select another address, that address will be written to the device and used until the device is reset.
|
||||
*
|
||||
* If you have more than one module, then you will need to specify a digital VPIN (Arduino
|
||||
* digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now,
|
||||
* when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once
|
||||
* all VL53L0X modules are turned off, the driver works through each module in turn by
|
||||
* setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address.
|
||||
* In this way, many VL53L0X modules can be connected to the one I2C bus, each one
|
||||
* using a distinct I2C address.
|
||||
*
|
||||
* WARNING: If the device's XSHUT pin is not connected, then it is very prone to noise,
|
||||
* and the device may even reset when handled. If you're not using XSHUT, then it's
|
||||
* best to tie it to +5V.
|
||||
*
|
||||
* The driver is configured as follows:
|
||||
*
|
||||
* Single VL53L0X module:
|
||||
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold);
|
||||
* Where firstVpin is the first vpin reserved for reading the device,
|
||||
* nPins is 1, 2 or 3,
|
||||
* i2cAddress is the address of the device (normally 0x29),
|
||||
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
|
||||
* and highThreshold is the distance at which the digital vpin state is set to 0 (in mm).
|
||||
*
|
||||
* Multiple VL53L0X modules:
|
||||
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin);
|
||||
* ...
|
||||
* Where firstVpin is the first vpin reserved for reading the device,
|
||||
* nPins is 1, 2 or 3,
|
||||
* i2cAddress is the address of the device (any valid address except 0x29),
|
||||
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
|
||||
* highThreshold is the distance at which the digital vpin state is set to 0 (in mm),
|
||||
* and xshutPin is the VPIN number corresponding to a digital output that is connected to the
|
||||
* XSHUT terminal on the module.
|
||||
*
|
||||
* Example:
|
||||
* In mySetup function within mySetup.cpp:
|
||||
* VL53L0X::create(4000, 3, 0x29, 200, 250);
|
||||
* Sensor::create(4000, 4000, 0); // Create a sensor
|
||||
*
|
||||
* When an object comes within 200mm of the sensor, a message
|
||||
* <Q 4000>
|
||||
* will be sent over the serial USB, and when the object moves more than 250mm from the sensor,
|
||||
* a message
|
||||
* <q 4000>
|
||||
* will be sent.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef IO_VL53L0X_h
|
||||
#define IO_VL53L0X_h
|
||||
|
||||
#include "IODevice.h"
|
||||
|
||||
class VL53L0X : public IODevice {
|
||||
private:
|
||||
uint8_t _i2cAddress;
|
||||
uint16_t _ambient;
|
||||
uint16_t _distance;
|
||||
uint16_t _signal;
|
||||
uint16_t _onThreshold;
|
||||
uint16_t _offThreshold;
|
||||
VPIN _xshutPin;
|
||||
bool _value;
|
||||
uint8_t _nextState = 0;
|
||||
I2CRB _rb;
|
||||
uint8_t _inBuffer[12];
|
||||
uint8_t _outBuffer[2];
|
||||
// State machine states.
|
||||
enum : uint8_t {
|
||||
STATE_INIT = 0,
|
||||
STATE_CONFIGUREADDRESS = 1,
|
||||
STATE_SKIP = 2,
|
||||
STATE_CONFIGUREDEVICE = 3,
|
||||
STATE_INITIATESCAN = 4,
|
||||
STATE_CHECKSTATUS = 5,
|
||||
STATE_GETRESULTS = 6,
|
||||
STATE_DECODERESULTS = 7,
|
||||
};
|
||||
|
||||
// Register addresses
|
||||
enum : uint8_t {
|
||||
VL53L0X_REG_SYSRANGE_START=0x00,
|
||||
VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13,
|
||||
VL53L0X_REG_RESULT_RANGE_STATUS=0x14,
|
||||
VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89,
|
||||
VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A,
|
||||
};
|
||||
const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29;
|
||||
|
||||
public:
|
||||
VL53L0X(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
|
||||
_firstVpin = firstVpin;
|
||||
_nPins = min(nPins, 3);
|
||||
_i2cAddress = i2cAddress;
|
||||
_onThreshold = onThreshold;
|
||||
_offThreshold = offThreshold;
|
||||
_xshutPin = xshutPin;
|
||||
_value = 0;
|
||||
addDevice(this);
|
||||
}
|
||||
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
|
||||
new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin);
|
||||
}
|
||||
|
||||
protected:
|
||||
void _begin() override {
|
||||
if (_xshutPin == VPIN_NONE) {
|
||||
// Check if device is already responding on the nominated address.
|
||||
if (I2CManager.exists(_i2cAddress)) {
|
||||
// Yes, it's already on this address, so skip the address initialisation.
|
||||
_nextState = STATE_CONFIGUREDEVICE;
|
||||
} else {
|
||||
_nextState = STATE_INIT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _loop(unsigned long currentMicros) override {
|
||||
uint8_t status;
|
||||
switch (_nextState) {
|
||||
case STATE_INIT:
|
||||
// On first entry to loop, reset this module by pulling XSHUT low. All modules
|
||||
// will be reset in turn.
|
||||
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);
|
||||
_nextState = STATE_CONFIGUREADDRESS;
|
||||
break;
|
||||
case STATE_CONFIGUREADDRESS:
|
||||
// On second entry, set XSHUT pin high to allow the module to restart.
|
||||
// On the module, there is a diode in series with the XSHUT pin to
|
||||
// protect the low-voltage pin against +5V.
|
||||
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1);
|
||||
// Allow the module time to restart
|
||||
delay(10);
|
||||
// Then write the desired I2C address to the device, while this is the only
|
||||
// module responding to the default address.
|
||||
I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress);
|
||||
_nextState = STATE_SKIP;
|
||||
break;
|
||||
case STATE_SKIP:
|
||||
// Do nothing on the third entry.
|
||||
_nextState = STATE_CONFIGUREDEVICE;
|
||||
break;
|
||||
case STATE_CONFIGUREDEVICE:
|
||||
// On next entry, check if device address has been set.
|
||||
if (I2CManager.exists(_i2cAddress)) {
|
||||
#ifdef DIAG_IO
|
||||
_display();
|
||||
#endif
|
||||
// Set 2.8V mode
|
||||
write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV,
|
||||
read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01);
|
||||
} else {
|
||||
DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress);
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
}
|
||||
_nextState = STATE_INITIATESCAN;
|
||||
break;
|
||||
case STATE_INITIATESCAN:
|
||||
// Not scanning, so initiate a scan
|
||||
_outBuffer[0] = VL53L0X_REG_SYSRANGE_START;
|
||||
_outBuffer[1] = 0x01;
|
||||
I2CManager.write(_i2cAddress, _outBuffer, 2, &_rb);
|
||||
_nextState = STATE_CHECKSTATUS;
|
||||
break;
|
||||
case STATE_CHECKSTATUS:
|
||||
status = _rb.status;
|
||||
if (status == I2C_STATUS_PENDING) return; // try next time
|
||||
if (status != I2C_STATUS_OK) {
|
||||
DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
|
||||
_deviceState = DEVSTATE_FAILED;
|
||||
_value = false;
|
||||
} else
|
||||
_nextState = 2;
|
||||
delayUntil(currentMicros + 95000); // wait for 95 ms before checking.
|
||||
_nextState = STATE_GETRESULTS;
|
||||
break;
|
||||
case STATE_GETRESULTS:
|
||||
// Ranging completed. Request results
|
||||
_outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;
|
||||
I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
|
||||
_nextState = 3;
|
||||
delayUntil(currentMicros + 5000); // Allow 5ms to get data
|
||||
_nextState = STATE_DECODERESULTS;
|
||||
break;
|
||||
case STATE_DECODERESULTS:
|
||||
// If I2C write still busy, return.
|
||||
status = _rb.status;
|
||||
if (status == I2C_STATUS_PENDING) return; // try again next time
|
||||
if (status == I2C_STATUS_OK) {
|
||||
if (!(_inBuffer[0] & 1)) return; // device still busy
|
||||
uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3);
|
||||
if (deviceRangeStatus == 0x0b) {
|
||||
// Range status OK, so use data
|
||||
_ambient = makeuint16(_inBuffer[7], _inBuffer[6]);
|
||||
_signal = makeuint16(_inBuffer[9], _inBuffer[8]);
|
||||
_distance = makeuint16(_inBuffer[11], _inBuffer[10]);
|
||||
if (_distance <= _onThreshold)
|
||||
_value = true;
|
||||
else if (_distance > _offThreshold)
|
||||
_value = false;
|
||||
}
|
||||
}
|
||||
// Completed. Restart scan on next loop entry.
|
||||
_nextState = STATE_INITIATESCAN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level.
|
||||
int _readAnalogue(VPIN vpin) override {
|
||||
int pin = vpin - _firstVpin;
|
||||
switch (pin) {
|
||||
case 0:
|
||||
return _distance;
|
||||
case 1:
|
||||
return _signal;
|
||||
case 2:
|
||||
return _ambient;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// For digital read, return zero for all but first pin.
|
||||
int _read(VPIN vpin) override {
|
||||
if (vpin == _firstVpin)
|
||||
return _value;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _display() override {
|
||||
DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"),
|
||||
_i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
|
||||
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
inline uint16_t makeuint16(byte lsb, byte msb) {
|
||||
return (((uint16_t)msb) << 8) | lsb;
|
||||
}
|
||||
uint8_t write_reg(uint8_t reg, uint8_t data) {
|
||||
// write byte to register
|
||||
uint8_t outBuffer[2];
|
||||
outBuffer[0] = reg;
|
||||
outBuffer[1] = data;
|
||||
return I2CManager.write(_i2cAddress, outBuffer, 2);
|
||||
}
|
||||
uint8_t read_reg(uint8_t reg) {
|
||||
// read byte from register and return value
|
||||
I2CManager.read(_i2cAddress, _inBuffer, 1, ®, 1);
|
||||
return _inBuffer[0];
|
||||
}
|
||||
};
|
||||
|
||||
#endif // IO_VL53L0X_h
|
@ -74,6 +74,10 @@ void LCDDisplay::loop() {
|
||||
LCDDisplay *LCDDisplay::loop2(bool force) {
|
||||
if (!lcdDisplay) return NULL;
|
||||
|
||||
// If output device is busy, don't do anything on this loop
|
||||
// This avoids blocking while waiting for the device to complete.
|
||||
if (isBusy()) return NULL;
|
||||
|
||||
unsigned long currentMillis = millis();
|
||||
|
||||
if (!force) {
|
||||
|
17
LCDDisplay.h
17
LCDDisplay.h
@ -19,15 +19,12 @@
|
||||
#ifndef LCDDisplay_h
|
||||
#define LCDDisplay_h
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "DisplayInterface.h"
|
||||
|
||||
#if __has_include ( "config.h")
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
// Allow maximum message length to be overridden from config.h
|
||||
#if !defined(MAX_MSG_SIZE)
|
||||
#define MAX_MSG_SIZE 16
|
||||
#define MAX_MSG_SIZE 20
|
||||
#endif
|
||||
|
||||
// Set default scroll mode (overridable in config.h)
|
||||
@ -39,17 +36,18 @@
|
||||
|
||||
class LCDDisplay : public DisplayInterface {
|
||||
public:
|
||||
LCDDisplay() {};
|
||||
static const int MAX_LCD_ROWS = 8;
|
||||
static const int MAX_LCD_COLS = MAX_MSG_SIZE;
|
||||
static const long LCD_SCROLL_TIME = 3000; // 3 seconds
|
||||
|
||||
// Internally handled functions
|
||||
static void loop();
|
||||
LCDDisplay* loop2(bool force);
|
||||
void setRow(byte line);
|
||||
void clear();
|
||||
LCDDisplay* loop2(bool force) override;
|
||||
void setRow(byte line) override;
|
||||
void clear() override;
|
||||
|
||||
size_t write(uint8_t b);
|
||||
size_t write(uint8_t b) override;
|
||||
|
||||
protected:
|
||||
uint8_t lcdRows;
|
||||
@ -63,6 +61,7 @@ protected:
|
||||
virtual void clearNative() = 0;
|
||||
virtual void setRowNative(byte line) = 0;
|
||||
virtual size_t writeNative(uint8_t b) = 0;
|
||||
virtual bool isBusy() = 0;
|
||||
|
||||
unsigned long lastScrollTime = 0;
|
||||
int8_t hotRow = 0;
|
||||
|
33
LCD_LCD.h
33
LCD_LCD.h
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow, Neil McKechnie. 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 "LiquidCrystal_I2C.h"
|
||||
LiquidCrystal_I2C LCDDriver(LCD_DRIVER); // set the LCD address, cols, rows
|
||||
// DEVICE SPECIFIC LCDDisplay Implementation for LCD_DRIVER
|
||||
LCDDisplay::LCDDisplay() {
|
||||
lcdDisplay=this;
|
||||
LCDDriver.init();
|
||||
LCDDriver.backlight();
|
||||
interfake(LCD_DRIVER);
|
||||
clear();
|
||||
}
|
||||
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; lcdRows=p3; }
|
||||
void LCDDisplay::clearNative() {LCDDriver.clear();}
|
||||
void LCDDisplay::setRowNative(byte row) { LCDDriver.setCursor(0, row); }
|
||||
void LCDDisplay::writeNative(char b){ LCDDriver.write(b); }
|
||||
void LCDDisplay::displayNative() { LCDDriver.display(); }
|
27
LCD_NONE.h
27
LCD_NONE.h
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow, Neil McKechnie. 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/>.
|
||||
*/
|
||||
|
||||
// dummy LCD shim to keep linker happy
|
||||
LCDDisplay::LCDDisplay() {}
|
||||
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; (void)p3;}
|
||||
void LCDDisplay::setRowNative(byte row) { (void)row;}
|
||||
void LCDDisplay::clearNative() {}
|
||||
void LCDDisplay::writeNative(char b){ (void)b;} //
|
||||
void LCDDisplay::displayNative(){}
|
||||
|
73
LCD_OLED.h
73
LCD_OLED.h
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* © 2021, Chris Harlow, Neil McKechnie. 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/>.
|
||||
*/
|
||||
|
||||
// OLED Implementation of LCDDisplay class
|
||||
// Note: this file is optionally included by LCD_Implementation.h
|
||||
// It is NOT a .cpp file to prevent it being compiled and demanding libraries
|
||||
// even when not needed.
|
||||
|
||||
#include "I2CManager.h"
|
||||
#include "SSD1306Ascii.h"
|
||||
SSD1306AsciiWire LCDDriver;
|
||||
|
||||
// DEVICE SPECIFIC LCDDisplay Implementation for OLED
|
||||
|
||||
LCDDisplay::LCDDisplay() {
|
||||
// Scan for device on 0x3c and 0x3d.
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(400000L); // Set max supported I2C speed
|
||||
for (byte address = 0x3c; address <= 0x3d; address++) {
|
||||
if (I2CManager.exists(address)) {
|
||||
// Device found
|
||||
DIAG(F("OLED display found at 0x%x"), address);
|
||||
interfake(OLED_DRIVER, 0);
|
||||
const DevType *devType;
|
||||
if (lcdCols == 132)
|
||||
devType = &SH1106_128x64; // Actually 132x64 but treated as 128x64
|
||||
else if (lcdCols == 128 && lcdRows == 4)
|
||||
devType = &Adafruit128x32;
|
||||
else
|
||||
devType = &Adafruit128x64;
|
||||
LCDDriver.begin(devType, address);
|
||||
lcdDisplay = this;
|
||||
LCDDriver.setFont(System5x7); // Normal 1:1 pixel scale, 8 bits high
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("OLED display not found"));
|
||||
}
|
||||
|
||||
void LCDDisplay::interfake(int p1, int p2, int p3) {
|
||||
lcdCols = p1;
|
||||
lcdRows = p2 / 8;
|
||||
(void)p3;
|
||||
}
|
||||
|
||||
void LCDDisplay::clearNative() { LCDDriver.clear(); }
|
||||
|
||||
void LCDDisplay::setRowNative(byte row) {
|
||||
// Positions text write to start of row 1..n
|
||||
int y = row;
|
||||
LCDDriver.setCursor(0, y);
|
||||
}
|
||||
|
||||
void LCDDisplay::writeNative(char b) { LCDDriver.write(b); }
|
||||
|
||||
void LCDDisplay::displayNative() {}
|
10
LCN.cpp
10
LCN.cpp
@ -48,18 +48,16 @@ void LCN::loop() {
|
||||
}
|
||||
else if (ch == 't' || ch == 'T') { // Turnout opcodes
|
||||
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
|
||||
Turnout * tt = Turnout::get(id);
|
||||
if (!tt) Turnout::create(id, LCN_TURNOUT_ADDRESS, 0);
|
||||
if (ch == 't') tt->data.tStatus |= STATUS_ACTIVE;
|
||||
else tt->data.tStatus &= ~STATUS_ACTIVE;
|
||||
if (!Turnout::exists(id)) LCNTurnout::create(id);
|
||||
Turnout::setClosedStateOnly(id,ch=='t');
|
||||
Turnout::turnoutlistHash++; // signals ED update of turnout data
|
||||
id = 0;
|
||||
}
|
||||
else if (ch == 'S' || ch == 's') {
|
||||
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
|
||||
Sensor * ss = Sensor::get(id);
|
||||
if (!ss) ss = Sensor::create(id, 255,0); // impossible pin
|
||||
ss->active = ch == 'S';
|
||||
if (!ss) ss = Sensor::create(id, VPIN_NONE, 0); // impossible pin
|
||||
ss->setState(ch == 'S');
|
||||
id = 0;
|
||||
}
|
||||
else id = 0; // ignore any other garbage from LCN
|
||||
|
4
LCN.h
4
LCN.h
@ -1,5 +1,7 @@
|
||||
/*
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
|
@ -119,7 +119,7 @@ void LiquidCrystal_I2C::clearNative() {
|
||||
|
||||
void LiquidCrystal_I2C::setRowNative(byte row) {
|
||||
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
|
||||
if (row > lcdRows) {
|
||||
if (row >= lcdRows) {
|
||||
row = lcdRows - 1; // we count rows starting w/0
|
||||
}
|
||||
command(LCD_SETDDRAMADDR | (row_offsets[row]));
|
||||
@ -196,7 +196,7 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
|
||||
outputBuffer[len++] = highnib;
|
||||
outputBuffer[len++] = lownib|En;
|
||||
outputBuffer[len++] = lownib;
|
||||
I2CManager.write(_Addr, outputBuffer, len);
|
||||
I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously
|
||||
}
|
||||
|
||||
// write 4 data bits to the HD44780 LCD controller.
|
||||
@ -208,12 +208,12 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) {
|
||||
uint8_t len = 0;
|
||||
outputBuffer[len++] = _data|En;
|
||||
outputBuffer[len++] = _data;
|
||||
I2CManager.write(_Addr, outputBuffer, len);
|
||||
I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously
|
||||
}
|
||||
|
||||
// write a byte to the PCF8574 I2C interface. We don't need to set
|
||||
// the enable pin for this.
|
||||
void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
|
||||
outputBuffer[0] = value | _backlightval;
|
||||
I2CManager.write(_Addr, outputBuffer, 1);
|
||||
I2CManager.write(_Addr, outputBuffer, 1); // Write command synchronously
|
||||
}
|
@ -66,19 +66,17 @@ class LiquidCrystal_I2C : public LCDDisplay {
|
||||
public:
|
||||
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
|
||||
void begin();
|
||||
void clearNative();
|
||||
void setRowNative(byte line);
|
||||
size_t writeNative(uint8_t c);
|
||||
void clearNative() override;
|
||||
void setRowNative(byte line) override;
|
||||
size_t writeNative(uint8_t c) override;
|
||||
|
||||
void display();
|
||||
void noBacklight();
|
||||
void backlight();
|
||||
|
||||
void command(uint8_t);
|
||||
void init();
|
||||
|
||||
private:
|
||||
void init_priv();
|
||||
void send(uint8_t, uint8_t);
|
||||
void write4bits(uint8_t);
|
||||
void expanderWrite(uint8_t);
|
||||
@ -89,6 +87,8 @@ private:
|
||||
uint8_t _backlightval;
|
||||
|
||||
uint8_t outputBuffer[4];
|
||||
// I/O is synchronous, so if this is called we're not busy!
|
||||
bool isBusy() override { return false; }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -1,7 +1,11 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
@ -71,7 +75,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, rawCurentTripValue(relative to offset)=%d"),
|
||||
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurrentTripValue(relative to offset)=%d"),
|
||||
currentPin-A0, senseOffset,rawCurrentTripValue);
|
||||
}
|
||||
|
||||
@ -143,16 +147,16 @@ int MotorDriver::getCurrentRaw() {
|
||||
bool irq = disableInterrupts();
|
||||
current = analogRead(currentPin)-senseOffset;
|
||||
enableInterrupts(irq);
|
||||
#elif defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
|
||||
#else // Uno, Mega and all the TEENSY3* but not TEENSY4*
|
||||
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 */
|
||||
#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);
|
||||
|
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
|
@ -1,4 +1,6 @@
|
||||
/*
|
||||
* © 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.
|
||||
@ -82,5 +84,27 @@
|
||||
#define IBT_2_WITH_ARDUINO F("IBT_2_WITH_ARDUINO_SHIELD"), \
|
||||
new MotorDriver(4, 5, 6, UNUSED_PIN, A5, 41.54, 5000, UNUSED_PIN), \
|
||||
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
|
||||
// YFROBOT Motor Shield (V3.1)
|
||||
#define YFROBOT_MOTOR_SHIELD F("YFROBOT_MOTOR_SHIELD"), \
|
||||
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
|
||||
|
114
Outputs.cpp
114
Outputs.cpp
@ -1,5 +1,9 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Harald Barth
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -82,30 +86,48 @@ 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"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to print all output states to stream in the form "<Y id state>"
|
||||
|
||||
// print all output states to stream
|
||||
void Output::printAll(Print *stream){
|
||||
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
|
||||
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.oStatus);
|
||||
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.active);
|
||||
} // Output::printAll
|
||||
|
||||
void Output::activate(int s){
|
||||
data.oStatus=(s>0); // if s>0, set status to active, else inactive
|
||||
digitalWrite(data.pin,data.oStatus ^ bitRead(data.iFlag,0)); // 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)
|
||||
if(num>0)
|
||||
EEPROM.put(num,data.oStatus);
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Object method to activate / deactivate the Output state.
|
||||
|
||||
void Output::activate(uint16_t s){
|
||||
s = (s>0); // Make 0 or 1
|
||||
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
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to locate Output object specified by ID 'n'.
|
||||
// Return NULL if not found.
|
||||
|
||||
Output* Output::get(uint16_t n){
|
||||
Output *tt;
|
||||
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);
|
||||
return(tt);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to delete Output object specified by ID 'n'.
|
||||
// Return false if not found.
|
||||
|
||||
bool Output::remove(uint16_t n){
|
||||
Output *tt,*pp=NULL;
|
||||
@ -125,52 +147,26 @@ 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 BrokenOutputData bdata;
|
||||
struct OutputData data;
|
||||
Output *tt;
|
||||
bool isBroken=1;
|
||||
|
||||
// This is a scary kluge. As we have two formats in EEPROM due to an
|
||||
// earlier bug, we don't know which we encounter now. So we guess
|
||||
// that if in all entries this byte has value of 7 or lower this is
|
||||
// an iFlag and thus the broken format. Otherwise it would be a pin
|
||||
// id. If someone uses only pins 0 to 7 of their arduino, they
|
||||
// loose. This is (if you look at an arduino) however unlikely.
|
||||
|
||||
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
|
||||
EEPROM.get(EEStore::pointer()+ i*sizeof(struct BrokenOutputData),bdata);
|
||||
if (bdata.iFlag > 7) { // it's a pin and not an iFlag!
|
||||
isBroken=0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( isBroken ) {
|
||||
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
|
||||
EEPROM.get(EEStore::pointer(),bdata);
|
||||
tt=create(bdata.id,bdata.pin,bdata.iFlag);
|
||||
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):bdata.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
|
||||
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
|
||||
pinMode(tt->data.pin,OUTPUT);
|
||||
tt->num=EEStore::pointer();
|
||||
EEStore::advance(sizeof(struct BrokenOutputData));
|
||||
}
|
||||
} else {
|
||||
struct OutputData data;
|
||||
EEPROM.get(EEStore::pointer(),data);
|
||||
// Create new object, set current state to default or to saved state from eeprom.
|
||||
tt=create(data.id, data.pin, data.flags);
|
||||
uint8_t state = data.setDefault ? data.defaultValue : data.active;
|
||||
tt->activate(state);
|
||||
|
||||
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
|
||||
EEPROM.get(EEStore::pointer(),data);
|
||||
tt=create(data.id,data.pin,data.iFlag);
|
||||
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):data.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
|
||||
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
|
||||
pinMode(tt->data.pin,OUTPUT);
|
||||
tt->num=EEStore::pointer();
|
||||
EEStore::advance(sizeof(struct OutputData));
|
||||
}
|
||||
if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
|
||||
EEStore::advance(sizeof(tt->data));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to store configuration and state of all Outputs to EEPROM
|
||||
|
||||
void Output::store(){
|
||||
Output *tt;
|
||||
@ -179,19 +175,26 @@ void Output::store(){
|
||||
EEStore::eeStore->data.nOutputs=0;
|
||||
|
||||
while(tt!=NULL){
|
||||
tt->num=EEStore::pointer();
|
||||
EEPROM.put(EEStore::pointer(),tt->data);
|
||||
tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
|
||||
EEStore::advance(sizeof(tt->data));
|
||||
tt=tt->nextOutput;
|
||||
EEStore::eeStore->data.nOutputs++;
|
||||
}
|
||||
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
#endif
|
||||
|
||||
Output *Output::create(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t v){
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static function to create an Output object
|
||||
// The obscurely named parameter 'v' is 0 if called from the load() function
|
||||
// and 1 if called from the <Z> command processing.
|
||||
|
||||
Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){
|
||||
Output *tt;
|
||||
|
||||
if (pin > VPIN_MAX) return NULL;
|
||||
|
||||
if(firstOutput==NULL){
|
||||
firstOutput=(Output *)calloc(1,sizeof(Output));
|
||||
tt=firstOutput;
|
||||
@ -204,20 +207,21 @@ Output *Output::create(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t v){
|
||||
}
|
||||
|
||||
if(tt==NULL) return tt;
|
||||
|
||||
tt->num = 0; // make sure new object doesn't get written to EEPROM until store() command
|
||||
tt->data.id=id;
|
||||
tt->data.pin=pin;
|
||||
tt->data.iFlag=iFlag;
|
||||
tt->data.oStatus=0;
|
||||
tt->data.flags=iFlag;
|
||||
|
||||
if(v==1){
|
||||
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):0; // sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
|
||||
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
|
||||
pinMode(tt->data.pin,OUTPUT);
|
||||
// sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
|
||||
if (tt->data.setDefault)
|
||||
tt->data.active = tt->data.defaultValue;
|
||||
else
|
||||
tt->data.active = 0;
|
||||
}
|
||||
IODevice::write(tt->data.pin, tt->data.active ^ tt->data.invert);
|
||||
|
||||
return(tt);
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
41
Outputs.h
41
Outputs.h
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* © 2020 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -20,37 +23,45 @@
|
||||
#define Outputs_h
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "IODevice.h"
|
||||
|
||||
struct OutputData {
|
||||
uint8_t oStatus;
|
||||
union {
|
||||
uint8_t oStatus; // (Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state, Bit 7=active)
|
||||
struct {
|
||||
unsigned int flags : 7; // Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state
|
||||
unsigned int : 1;
|
||||
};
|
||||
struct {
|
||||
unsigned int invert : 1;
|
||||
unsigned int setDefault : 1;
|
||||
unsigned int defaultValue : 1;
|
||||
unsigned int: 4;
|
||||
unsigned int active : 1;
|
||||
};
|
||||
};
|
||||
uint16_t id;
|
||||
uint8_t pin;
|
||||
uint8_t iFlag;
|
||||
VPIN pin;
|
||||
};
|
||||
|
||||
struct BrokenOutputData {
|
||||
uint8_t oStatus;
|
||||
uint8_t id;
|
||||
uint8_t pin;
|
||||
uint8_t iFlag;
|
||||
};
|
||||
|
||||
class Output{
|
||||
|
||||
public:
|
||||
void activate(int s);
|
||||
void activate(uint16_t s);
|
||||
bool isActive();
|
||||
static Output* get(uint16_t);
|
||||
static bool remove(uint16_t);
|
||||
#ifndef DISABLE_EEPROM
|
||||
static void load();
|
||||
static void store();
|
||||
static Output *create(uint16_t, uint8_t, uint8_t, uint8_t=0);
|
||||
#endif
|
||||
static Output *create(uint16_t, VPIN, int, int=0);
|
||||
static Output *firstOutput;
|
||||
struct OutputData data;
|
||||
Output *nextOutput;
|
||||
static void printAll(Print *);
|
||||
|
||||
private:
|
||||
int num; // EEPROM pointer (Chris has no idea what this is all about!)
|
||||
uint16_t num; // EEPROM address of oStatus in OutputData struct, or zero if not stored.
|
||||
|
||||
}; // Output
|
||||
|
||||
|
@ -1,109 +0,0 @@
|
||||
/*
|
||||
* (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/>.
|
||||
*/
|
||||
/*!
|
||||
* @file PWMServoDriver.cpp
|
||||
*
|
||||
* @mainpage Adafruit 16-channel PWM & Servo driver, based on Adafruit_PWMServoDriver
|
||||
*
|
||||
* @section intro_sec Introduction
|
||||
*
|
||||
* This is a library for the 16-channel PWM & Servo driver.
|
||||
*
|
||||
* Designed specifically to work with the Adafruit PWM & Servo driver.
|
||||
* This class contains a very small subset of the Adafruit version which
|
||||
* is relevant to driving simple servos at 50Hz through a number of chained
|
||||
* servo driver boards (ie servos 0-15 on board 0x40, 16-31 on board 0x41 etc.)
|
||||
*
|
||||
* @section author Author
|
||||
* Chris Harlow (TPL)
|
||||
*
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include "PWMServoDriver.h"
|
||||
#include "DIAG.h"
|
||||
#include "I2CManager.h"
|
||||
|
||||
|
||||
// REGISTER ADDRESSES
|
||||
const byte PCA9685_MODE1=0x00; // Mode Register
|
||||
const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
|
||||
const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
|
||||
// MODE1 bits
|
||||
const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
|
||||
const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
|
||||
const byte MODE1_RESTART=0x80; /**< Restart enabled */
|
||||
|
||||
const byte PCA9685_I2C_ADDRESS=0x40; /** First PCA9685 I2C Slave Address */
|
||||
const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
|
||||
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
|
||||
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
|
||||
|
||||
/*!
|
||||
* @brief Sets the PWM frequency for a chip to 50Hz for servos
|
||||
*/
|
||||
|
||||
byte PWMServoDriver::setupFlags=0; // boards that have been initialised
|
||||
byte PWMServoDriver::failFlags=0; // boards that have faild initialisation
|
||||
|
||||
bool PWMServoDriver::setup(int board) {
|
||||
if (board>3 || (failFlags & (1<<board))) return false;
|
||||
if (setupFlags & (1<<board)) return true;
|
||||
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(MAX_I2C_SPEED);
|
||||
|
||||
uint8_t i2caddr=PCA9685_I2C_ADDRESS + board;
|
||||
|
||||
// Test if device is available
|
||||
byte error = I2CManager.checkAddress(i2caddr);
|
||||
if (error) {
|
||||
DIAG(F("I2C Servo device 0x%x Not Found %d"),i2caddr, error);
|
||||
failFlags|=1<<board;
|
||||
return false;
|
||||
}
|
||||
|
||||
//DIAG(F("PWMServoDriver::setup %x prescale=%d"),i2caddr,PRESCALE_50HZ);
|
||||
writeRegister(i2caddr,PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
|
||||
writeRegister(i2caddr,PCA9685_PRESCALE, PRESCALE_50HZ);
|
||||
writeRegister(i2caddr,PCA9685_MODE1,MODE1_AI);
|
||||
writeRegister(i2caddr,PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
|
||||
setupFlags|=1<<board;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Sets the PWM output to a servo
|
||||
*/
|
||||
void PWMServoDriver::setServo(byte servoNum, uint16_t value) {
|
||||
int board=servoNum/16;
|
||||
int pin=servoNum%16;
|
||||
|
||||
if (setup(board)) {
|
||||
DIAG(F("SetServo %d %d"),servoNum,value);
|
||||
uint8_t buffer[] = {(uint8_t)(PCA9685_FIRST_SERVO + 4 * pin), // 4 registers per pin
|
||||
0, 0, (uint8_t)(value & 0xff), (uint8_t)(value >> 8)};
|
||||
if (value == 4095) buffer[2] = 0x10; // Full on
|
||||
byte error=I2CManager.write(PCA9685_I2C_ADDRESS + board, buffer, sizeof(buffer));
|
||||
if (error!=0) DIAG(F("SetServo error %d"),error);
|
||||
}
|
||||
}
|
||||
|
||||
void PWMServoDriver::writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d) {
|
||||
I2CManager.write(i2caddr, 2, hardwareRegister, d);
|
||||
}
|
@ -17,7 +17,7 @@ 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 manu more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
||||
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
|
||||
|
||||
|
||||
# What’s in this Repository?
|
||||
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX CommandStation-EX
|
||||
*
|
||||
@ -112,3 +113,12 @@ bool RingStream::commit() {
|
||||
//DIAG(F("Commit2 len=%d count=%d pr=%d pw=%d m=%d"),_len, _count,_pos_read,_pos_write,_mark);
|
||||
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);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
#ifndef RingStream_h
|
||||
#define RingStream_h
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC-EX CommandStation-EX
|
||||
*
|
||||
@ -35,6 +36,8 @@ class RingStream : public Print {
|
||||
void mark(uint8_t b);
|
||||
bool commit();
|
||||
uint8_t peekTargetMark();
|
||||
void printBuffer(Print * streamer);
|
||||
void flush();
|
||||
void info();
|
||||
|
||||
private:
|
||||
|
258
SSD1306Ascii.cpp
258
SSD1306Ascii.cpp
@ -89,121 +89,6 @@ 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};
|
||||
|
||||
//==============================================================================
|
||||
// SSD1306AsciiWire Method Definitions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Constructor
|
||||
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
|
||||
// Set size in characters in base class
|
||||
lcdRows = height / 8;
|
||||
lcdCols = width / 6;
|
||||
|
||||
I2CManager.begin();
|
||||
I2CManager.setClock(400000L); // Set max supported I2C speed
|
||||
for (byte address = 0x3c; address <= 0x3d; address++) {
|
||||
if (I2CManager.exists(address)) {
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("OLED display not found"));
|
||||
}
|
||||
|
||||
/* Clear screen by writing blank pixels. */
|
||||
void SSD1306AsciiWire::clearNative() {
|
||||
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
|
||||
for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) {
|
||||
setRowNative(r); // Position at start of row to be erased
|
||||
for (uint8_t c = 0; c <= m_displayWidth - 1; c += maxBytes-1) {
|
||||
uint8_t len = min(m_displayWidth-c, maxBytes-1) + 1;
|
||||
I2CManager.write_P(m_i2cAddr, blankPixels, len); // Write a number of blank columns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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*)GETFLASHW(&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)
|
||||
void SSD1306AsciiWire::setRowNative(uint8_t line) {
|
||||
// Calculate pixel position from line number
|
||||
uint8_t row = line*8;
|
||||
if (row < m_displayHeight) {
|
||||
m_row = row;
|
||||
m_col = m_colOffset;
|
||||
// Build output buffer for I2C
|
||||
uint8_t len = 0;
|
||||
outputBuffer[len++] = 0x00; // Set to command mode
|
||||
outputBuffer[len++] = SSD1306_SETLOWCOLUMN | (m_col & 0XF);
|
||||
outputBuffer[len++] = SSD1306_SETHIGHCOLUMN | (m_col >> 4);
|
||||
outputBuffer[len++] = SSD1306_SETSTARTPAGE | (m_row/8);
|
||||
I2CManager.write(m_i2cAddr, outputBuffer, len);
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Write a character to the OLED
|
||||
size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
|
||||
const uint8_t* base = m_font;
|
||||
|
||||
if (ch < m_fontFirstChar || ch >= (m_fontFirstChar + m_fontCharCount))
|
||||
return 0;
|
||||
// Check if character would be partly or wholly off the display
|
||||
if (m_col + fontWidth > m_displayWidth)
|
||||
return 0;
|
||||
#if defined(NOLOWERCASE)
|
||||
// Adjust if lowercase is missing
|
||||
if (ch >= 'a') {
|
||||
if (ch <= 'z')
|
||||
ch = ch - 'a' + 'A'; // Capitalise
|
||||
else
|
||||
ch -= 26; // Allow for missing lowercase letters
|
||||
}
|
||||
#endif
|
||||
ch -= m_fontFirstChar;
|
||||
base += fontWidth * ch;
|
||||
// Build output buffer for I2C
|
||||
outputBuffer[0] = 0x40; // set SSD1306 controller to data mode
|
||||
uint8_t bufferPos = 1;
|
||||
// Copy character pixel columns
|
||||
for (uint8_t i = 0; i < fontWidth; i++)
|
||||
outputBuffer[bufferPos++] = GETFLASH(base++);
|
||||
// Add blank pixels between letters
|
||||
for (uint8_t i = 0; i < letterSpacing; i++)
|
||||
outputBuffer[bufferPos++] = 0;
|
||||
|
||||
// Write the data to I2C display
|
||||
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos);
|
||||
m_col += fontWidth + letterSpacing;
|
||||
return 1;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
|
||||
@ -230,23 +115,6 @@ const uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {
|
||||
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
|
||||
|
||||
@ -271,15 +139,123 @@ const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
|
||||
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.
|
||||
};
|
||||
//==============================================================================
|
||||
// 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);
|
||||
// Set singleton address
|
||||
lcdDisplay = this;
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
DIAG(F("OLED display not found"));
|
||||
}
|
||||
|
||||
/* Clear screen by writing blank pixels. */
|
||||
void SSD1306AsciiWire::clearNative() {
|
||||
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
|
||||
for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) {
|
||||
setRowNative(r); // Position at start of row to be erased
|
||||
for (uint8_t c = 0; c <= m_displayWidth - 1; c += maxBytes-1) {
|
||||
uint8_t len = min(m_displayWidth-c, maxBytes-1) + 1;
|
||||
I2CManager.write_P(m_i2cAddr, blankPixels, len); // Write a number of blank columns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Set cursor position (by text line)
|
||||
void SSD1306AsciiWire::setRowNative(uint8_t line) {
|
||||
// Calculate pixel position from line number
|
||||
uint8_t row = line*8;
|
||||
if (row < m_displayHeight) {
|
||||
m_row = row;
|
||||
m_col = m_colOffset;
|
||||
// Before using buffer, wait for last request to complete
|
||||
requestBlock.wait();
|
||||
// Build output buffer for I2C
|
||||
uint8_t len = 0;
|
||||
outputBuffer[len++] = 0x00; // Set to command mode
|
||||
outputBuffer[len++] = SSD1306_SETLOWCOLUMN | (m_col & 0XF);
|
||||
outputBuffer[len++] = SSD1306_SETHIGHCOLUMN | (m_col >> 4);
|
||||
outputBuffer[len++] = SSD1306_SETSTARTPAGE | (m_row/8);
|
||||
I2CManager.write(m_i2cAddr, outputBuffer, len, &requestBlock);
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Write a character to the OLED
|
||||
size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
|
||||
const uint8_t* base = m_font;
|
||||
|
||||
if (ch < m_fontFirstChar || ch >= (m_fontFirstChar + m_fontCharCount))
|
||||
return 0;
|
||||
// Check if character would be partly or wholly off the display
|
||||
if (m_col + fontWidth > m_displayWidth)
|
||||
return 0;
|
||||
#if defined(NOLOWERCASE)
|
||||
// Adjust if lowercase is missing
|
||||
if (ch >= 'a') {
|
||||
if (ch <= 'z')
|
||||
ch = ch - 'a' + 'A'; // Capitalise
|
||||
else
|
||||
ch -= 26; // Allow for missing lowercase letters
|
||||
}
|
||||
#endif
|
||||
ch -= m_fontFirstChar;
|
||||
base += fontWidth * ch;
|
||||
// Before using buffer, wait for last request to complete
|
||||
requestBlock.wait();
|
||||
// Build output buffer for I2C
|
||||
outputBuffer[0] = 0x40; // set SSD1306 controller to data mode
|
||||
uint8_t bufferPos = 1;
|
||||
// Copy character pixel columns
|
||||
for (uint8_t i = 0; i < fontWidth; i++)
|
||||
outputBuffer[bufferPos++] = GETFLASH(base++);
|
||||
// Add blank pixels between letters
|
||||
for (uint8_t i = 0; i < letterSpacing; i++)
|
||||
outputBuffer[bufferPos++] = 0;
|
||||
|
||||
// Write the data to I2C display
|
||||
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);
|
||||
m_col += fontWidth + letterSpacing;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
@ -32,21 +32,6 @@
|
||||
//#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:
|
||||
@ -55,24 +40,18 @@ class SSD1306AsciiWire : public LCDDisplay {
|
||||
SSD1306AsciiWire(int width, int height);
|
||||
|
||||
// Initialize the display controller.
|
||||
void begin(const DevType* dev, uint8_t i2cAddr);
|
||||
void begin(uint8_t i2cAddr);
|
||||
|
||||
// Clear the display and set the cursor to (0, 0).
|
||||
void clearNative();
|
||||
void clearNative() override;
|
||||
|
||||
// Set cursor to start of specified text line
|
||||
void setRowNative(byte line);
|
||||
|
||||
// Initialize the display controller.
|
||||
void init(const DevType* dev);
|
||||
void setRowNative(byte line) override;
|
||||
|
||||
// Write one character to OLED
|
||||
size_t writeNative(uint8_t c);
|
||||
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:
|
||||
// Cursor column.
|
||||
@ -97,6 +76,7 @@ class SSD1306AsciiWire : public LCDDisplay {
|
||||
|
||||
uint8_t m_i2cAddr;
|
||||
|
||||
I2CRB requestBlock;
|
||||
uint8_t outputBuffer[fontWidth+letterSpacing+1];
|
||||
|
||||
static const uint8_t blankPixels[];
|
||||
|
223
Sensors.cpp
223
Sensors.cpp
@ -1,7 +1,10 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
@ -27,9 +30,9 @@ or be allowed to float HIGH if use of the Arduino Pin's internal pull-up resisto
|
||||
To ensure proper voltage levels, some part of the Sensor circuitry
|
||||
MUST be tied back to the same ground as used by the Arduino.
|
||||
|
||||
The Sensor code below utilizes exponential smoothing to "de-bounce" spikes generated by
|
||||
The Sensor code below utilises "de-bounce" logic to remove spikes generated by
|
||||
mechanical switches and transistors. This avoids the need to create smoothing circuitry
|
||||
for each sensor. You may need to change these parameters through trial and error for your specific sensors.
|
||||
for each sensor. You may need to change the parameters through trial and error for your specific sensors.
|
||||
|
||||
To have this sketch monitor one or more Arduino pins for sensor triggers, first define/edit/delete
|
||||
sensor definitions using the following variation of the "S" command:
|
||||
@ -66,47 +69,111 @@ 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"
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// checks one defined sensors and prints _changed_ sensor state
|
||||
// checks a number of defined sensors per entry and prints _changed_ sensor state
|
||||
// to stream unless stream is NULL in which case only internal
|
||||
// state is updated. Then advances to next sensor which will
|
||||
// be checked att next invocation.
|
||||
// be checked at next invocation. Each cycle of reading all sensors will
|
||||
// be initiated no more frequently than the time set by 'cycleInterval' microseconds.
|
||||
//
|
||||
// The list of sensors is divided such that the first part of the list
|
||||
// contains sensors that support change notification via callback, and the second
|
||||
// part of the list contains sensors that require cyclic polling. The start of the
|
||||
// second part of the list is determined from by the 'firstPollSensor' pointer.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Sensor::checkAll(Print *stream){
|
||||
void Sensor::checkAll(){
|
||||
uint16_t sensorCount = 0;
|
||||
|
||||
if (firstSensor == NULL) return;
|
||||
if (readingSensor == NULL) readingSensor=firstSensor;
|
||||
#ifdef USE_NOTIFY
|
||||
// Register the event handler ONCE!
|
||||
if (!inputChangeCallbackRegistered)
|
||||
IONotifyCallback::add(inputChangeCallback);
|
||||
inputChangeCallbackRegistered = true;
|
||||
#endif
|
||||
|
||||
bool sensorstate = digitalRead(readingSensor->data.pin);
|
||||
|
||||
if (!sensorstate == readingSensor->active) { // active==true means sensorstate=0/false so sensor unchanged
|
||||
// no change
|
||||
if (readingSensor->latchdelay != 0) {
|
||||
// enable if you want to debug contact jitter
|
||||
//if (stream != NULL) StringFormatter::send(stream, F("JITTER %d %d\n"),
|
||||
// readingSensor->latchdelay, readingSensor->data.snum);
|
||||
readingSensor->latchdelay=0; // reset
|
||||
if (firstSensor == NULL) return; // No sensors to be scanned
|
||||
if (readingSensor == NULL) {
|
||||
// Not currently scanning sensor list
|
||||
unsigned long thisTime = micros();
|
||||
if (thisTime - lastReadCycle >= cycleInterval) {
|
||||
// Required time elapsed since last read cycle started,
|
||||
// so initiate new scan through the sensor list
|
||||
readingSensor = firstSensor;
|
||||
lastReadCycle = thisTime;
|
||||
}
|
||||
} else if (readingSensor->latchdelay < 127) { // byte, max 255, good value unknown yet
|
||||
// change but first increase anti-jitter counter
|
||||
readingSensor->latchdelay++;
|
||||
} else {
|
||||
// make the change
|
||||
readingSensor->active = !sensorstate;
|
||||
readingSensor->latchdelay=0; // reset
|
||||
if (stream != NULL) StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
|
||||
}
|
||||
|
||||
readingSensor=readingSensor->nextSensor;
|
||||
// Loop until either end of list is encountered or we pause for some reason
|
||||
bool pause = false;
|
||||
while (readingSensor != NULL && !pause) {
|
||||
|
||||
// Where the sensor is attached to a pin, read pin status. For sources such as LCN,
|
||||
// which don't have an input pin to read, the LCN class calls setState() to update inputState when
|
||||
// a message is received. The IODevice::read() call returns 1 for active pins (0v) and 0 for inactive (5v).
|
||||
// Also, on HAL drivers that support change notifications, the driver calls the notification callback
|
||||
// routine when an input signal change is detected, and this updates the inputState directly,
|
||||
// so these inputs don't need to be polled here.
|
||||
VPIN pin = readingSensor->data.pin;
|
||||
if (readingSensor->pollingRequired && pin != VPIN_NONE)
|
||||
readingSensor->inputState = IODevice::read(pin);
|
||||
|
||||
// Check if changed since last time, and process changes.
|
||||
if (readingSensor->inputState == readingSensor->active) {
|
||||
// no change
|
||||
readingSensor->latchDelay = minReadCount; // Reset counter
|
||||
} else if (readingSensor->latchDelay > 0) {
|
||||
// change detected, but first decrement delay
|
||||
readingSensor->latchDelay--;
|
||||
} else {
|
||||
// change validated, act on it.
|
||||
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
|
||||
}
|
||||
|
||||
// Move to next sensor in list.
|
||||
readingSensor = readingSensor->nextSensor;
|
||||
|
||||
// Currently process max of 16 sensors per entry.
|
||||
// Performance measurements taken during development indicate that, with 128 sensors configured
|
||||
// on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices
|
||||
// within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.
|
||||
sensorCount++;
|
||||
if (sensorCount >= 16) pause = true;
|
||||
}
|
||||
|
||||
} // Sensor::checkAll
|
||||
|
||||
|
||||
#ifdef USE_NOTIFY
|
||||
// Callback from HAL (IODevice class) when a digital input change is recognised.
|
||||
// Updates the inputState field, which is subsequently scanned for changes in the checkAll
|
||||
// method. Ideally the <Q>/<q> message should be sent from here, instead of waiting for
|
||||
// the checkAll method, but the output stream is not available at this point.
|
||||
void Sensor::inputChangeCallback(VPIN vpin, int state) {
|
||||
Sensor *tt;
|
||||
// This bit is not ideal since it has, potentially, to look through the entire list of
|
||||
// sensors to find the one that has changed. Ideally this should be improved somehow.
|
||||
for (tt=firstSensor; tt!=NULL ; tt=tt->nextSensor) {
|
||||
if (tt->data.pin == vpin) break;
|
||||
}
|
||||
if (tt != NULL) { // Sensor found
|
||||
tt->inputState = (state != 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// prints all sensor states to stream
|
||||
@ -115,40 +182,62 @@ void Sensor::checkAll(Print *stream){
|
||||
|
||||
void Sensor::printAll(Print *stream){
|
||||
|
||||
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
|
||||
if (stream != NULL)
|
||||
if (stream != NULL) {
|
||||
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
|
||||
StringFormatter::send(stream, F("<%c %d>\n"), tt->active ? 'Q' : 'q', tt->data.snum);
|
||||
}
|
||||
} // loop over all sensors
|
||||
} // Sensor::printAll
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Static Function to create/find Sensor object.
|
||||
|
||||
Sensor *Sensor::create(int snum, int pin, int pullUp){
|
||||
Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
|
||||
Sensor *tt;
|
||||
|
||||
if(firstSensor==NULL){
|
||||
firstSensor=(Sensor *)calloc(1,sizeof(Sensor));
|
||||
tt=firstSensor;
|
||||
} else if((tt=get(snum))==NULL){
|
||||
tt=firstSensor;
|
||||
while(tt->nextSensor!=NULL)
|
||||
tt=tt->nextSensor;
|
||||
tt->nextSensor=(Sensor *)calloc(1,sizeof(Sensor));
|
||||
tt=tt->nextSensor;
|
||||
}
|
||||
if (pin > VPIN_MAX && pin != VPIN_NONE) return NULL;
|
||||
|
||||
if(tt==NULL) return tt; // problem allocating memory
|
||||
remove(snum); // Unlink and free any existing sensor with the same id, before creating the new one.
|
||||
|
||||
tt->data.snum=snum;
|
||||
tt->data.pin=pin;
|
||||
tt->data.pullUp=(pullUp==0?LOW:HIGH);
|
||||
tt->active=false;
|
||||
tt->latchdelay=0;
|
||||
pinMode(pin,INPUT); // set mode to input
|
||||
digitalWrite(pin,pullUp); // don't use Arduino's internal pull-up resistors for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
|
||||
tt = (Sensor *)calloc(1,sizeof(Sensor));
|
||||
if (!tt) return tt; // memory allocation failure
|
||||
|
||||
if (pin == VPIN_NONE)
|
||||
tt->pollingRequired = false;
|
||||
#ifdef USE_NOTIFY
|
||||
else if (IODevice::hasCallback(pin))
|
||||
tt->pollingRequired = false;
|
||||
#endif
|
||||
else
|
||||
tt->pollingRequired = true;
|
||||
|
||||
// Add to the start of the list
|
||||
tt->nextSensor = firstSensor;
|
||||
firstSensor = tt;
|
||||
|
||||
tt->data.snum = snum;
|
||||
tt->data.pin = pin;
|
||||
tt->data.pullUp = pullUp;
|
||||
tt->active = 0;
|
||||
tt->inputState = 0;
|
||||
tt->latchDelay = minReadCount;
|
||||
|
||||
if (pin != VPIN_NONE)
|
||||
IODevice::configureInput(pin, pullUp);
|
||||
// Generally, internal pull-up resistors are not, on their own, sufficient
|
||||
// for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
|
||||
|
||||
return tt;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Object method to directly change the input state, for sensors such as LCN which are updated
|
||||
// by means other than by polling an input.
|
||||
|
||||
void Sensor::setState(int value) {
|
||||
// Trigger sensor change to be reported on next checkAll loop.
|
||||
inputState = (value != 0);
|
||||
latchDelay = 0; // Don't wait for anti-jitter logic
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@ -166,27 +255,38 @@ bool Sensor::remove(int n){
|
||||
for(tt=firstSensor;tt!=NULL && tt->data.snum!=n;pp=tt,tt=tt->nextSensor);
|
||||
|
||||
if (tt==NULL) return false;
|
||||
|
||||
if(tt==firstSensor)
|
||||
firstSensor=tt->nextSensor;
|
||||
else
|
||||
pp->nextSensor=tt->nextSensor;
|
||||
|
||||
// Unlink the sensor from the list
|
||||
if(tt==firstSensor)
|
||||
firstSensor=tt->nextSensor;
|
||||
else
|
||||
pp->nextSensor=tt->nextSensor;
|
||||
#ifdef USE_NOTIFY
|
||||
if (tt==lastSensor)
|
||||
lastSensor = pp;
|
||||
if (tt==firstPollSensor)
|
||||
firstPollSensor = tt->nextSensor;
|
||||
#endif
|
||||
|
||||
// Check if the sensor being deleted is the next one to be read. If so,
|
||||
// make the following one the next one to be read.
|
||||
if (readingSensor==tt) readingSensor=tt->nextSensor;
|
||||
|
||||
free(tt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
void Sensor::load(){
|
||||
struct SensorData data;
|
||||
Sensor *tt;
|
||||
|
||||
for(uint16_t i=0;i<EEStore::eeStore->data.nSensors;i++){
|
||||
uint16_t i=EEStore::eeStore->data.nSensors;
|
||||
while(i--){
|
||||
EEPROM.get(EEStore::pointer(),data);
|
||||
tt=create(data.snum,data.pin,data.pullUp);
|
||||
tt=create(data.snum, data.pin, data.pullUp);
|
||||
EEStore::advance(sizeof(tt->data));
|
||||
}
|
||||
}
|
||||
@ -206,8 +306,15 @@ void Sensor::store(){
|
||||
EEStore::eeStore->data.nSensors++;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Sensor *Sensor::firstSensor=NULL;
|
||||
Sensor *Sensor::readingSensor=NULL;
|
||||
unsigned long Sensor::lastReadCycle=0;
|
||||
|
||||
#ifdef USE_NOTIFY
|
||||
Sensor *Sensor::firstPollSensor = NULL;
|
||||
Sensor *Sensor::lastSensor = NULL;
|
||||
bool Sensor::inputChangeCallbackRegistered = false;
|
||||
#endif
|
||||
|
76
Sensors.h
76
Sensors.h
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -20,29 +23,76 @@
|
||||
#define Sensor_h
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "IODevice.h"
|
||||
|
||||
#define SENSOR_DECAY 0.03
|
||||
// Uncomment the following #define statement to use callback notification
|
||||
// where the driver supports it.
|
||||
// The principle of callback notification is to avoid the Sensor class
|
||||
// having to poll the device driver cyclically for input values, and then scan
|
||||
// for changes. Instead, when the driver scans the inputs, if it detects
|
||||
// a change it invokes a callback function in the Sensor class. In the current
|
||||
// implementation, the advantages are limited because (a) the Sensor class
|
||||
// performs debounce checks, and (b) the Sensor class does not have a
|
||||
// static reference to the output stream for sending <Q>/<q> messages
|
||||
// when a change is detected. These restrictions mean that the checkAll()
|
||||
// method still has to iterate through all of the Sensor objects looking
|
||||
// for changes.
|
||||
#define USE_NOTIFY
|
||||
|
||||
struct SensorData {
|
||||
int snum;
|
||||
uint8_t pin;
|
||||
VPIN pin;
|
||||
uint8_t pullUp;
|
||||
};
|
||||
|
||||
struct Sensor{
|
||||
static Sensor *firstSensor;
|
||||
static Sensor *readingSensor;
|
||||
class Sensor{
|
||||
// The sensor list is a linked list where each sensor's 'nextSensor' field points to the next.
|
||||
// The pointer is null in the last on the list.
|
||||
|
||||
public:
|
||||
SensorData data;
|
||||
boolean active;
|
||||
byte latchdelay;
|
||||
struct {
|
||||
uint8_t active:1;
|
||||
uint8_t inputState:1;
|
||||
uint8_t latchDelay:6;
|
||||
}; // bit 7=active; bit 6=input state; bits 5-0=latchDelay
|
||||
|
||||
static Sensor *firstSensor;
|
||||
#ifdef USE_NOTIFY
|
||||
static Sensor *firstPollSensor;
|
||||
static Sensor *lastSensor;
|
||||
#endif
|
||||
// readingSensor points to the next sensor to be polled, or null if the poll cycle is completed for
|
||||
// the period.
|
||||
static Sensor *readingSensor;
|
||||
|
||||
// Constructor
|
||||
Sensor();
|
||||
Sensor *nextSensor;
|
||||
|
||||
void setState(int state);
|
||||
#ifndef DISABLE_EEPROM
|
||||
static void load();
|
||||
static void store();
|
||||
static Sensor *create(int, int, int);
|
||||
static Sensor* get(int);
|
||||
static bool remove(int);
|
||||
static void checkAll(Print *);
|
||||
static void printAll(Print *);
|
||||
#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 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.
|
||||
// should not be less than device scan cycle time.
|
||||
static const unsigned int minReadCount = 1; // number of additional scans before acting on change
|
||||
// E.g. 1 means that a change is ignored for one scan and actioned on the next.
|
||||
// Max value is 63
|
||||
bool pollingRequired = true;
|
||||
|
||||
#ifdef USE_NOTIFY
|
||||
static void inputChangeCallback(VPIN vpin, int state);
|
||||
static bool inputChangeCallbackRegistered;
|
||||
#endif
|
||||
|
||||
}; // Sensor
|
||||
|
||||
#endif
|
||||
|
82
SerialManager.cpp
Normal file
82
SerialManager.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* © 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
/*
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
/*
|
||||
* © 2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of DCC++EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -16,24 +17,33 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*!
|
||||
* @file PWMServoDriver.h
|
||||
*
|
||||
* Used to set servo positions on an I2C bus with 1 or more PCA96685 boards.
|
||||
*/
|
||||
#ifndef PWMServoDriver_H
|
||||
#define PWMServoDriver_H
|
||||
|
||||
#ifndef SerialManager_h
|
||||
#define SerialManager_h
|
||||
|
||||
class PWMServoDriver {
|
||||
public:
|
||||
static void setServo(byte servoNum, uint16_t pos);
|
||||
|
||||
private:
|
||||
static byte setupFlags;
|
||||
static byte failFlags;
|
||||
static bool setup(int board);
|
||||
static void writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d);
|
||||
};
|
||||
#include "Arduino.h"
|
||||
#include "defines.h"
|
||||
#include "RingStream.h"
|
||||
|
||||
#ifndef COMMAND_BUFFER_SIZE
|
||||
#define COMMAND_BUFFER_SIZE 100
|
||||
#endif
|
||||
|
||||
class SerialManager {
|
||||
public:
|
||||
static void init();
|
||||
static void loop();
|
||||
static void broadcast(RingStream * ring);
|
||||
|
||||
private:
|
||||
static SerialManager * first;
|
||||
SerialManager(Stream * myserial);
|
||||
void loop2();
|
||||
void broadcast2(RingStream * ring);
|
||||
Stream * serial;
|
||||
SerialManager * next;
|
||||
byte bufferLength;
|
||||
byte buffer[COMMAND_BUFFER_SIZE];
|
||||
bool inCommandPayload;
|
||||
};
|
||||
#endif
|
@ -99,6 +99,7 @@ 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;
|
||||
|
654
Turnouts.cpp
654
Turnouts.cpp
@ -1,9 +1,13 @@
|
||||
/*
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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,166 +22,518 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "Turnouts.h"
|
||||
|
||||
|
||||
#include "defines.h" // includes config.h
|
||||
#ifndef DISABLE_EEPROM
|
||||
#include "EEStore.h"
|
||||
#include "PWMServoDriver.h"
|
||||
#endif
|
||||
#include "StringFormatter.h"
|
||||
#include "CommandDistributor.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "Turnouts.h"
|
||||
#include "DCC.h"
|
||||
#include "LCN.h"
|
||||
#ifdef EESTOREDEBUG
|
||||
#include "DIAG.h"
|
||||
#endif
|
||||
|
||||
// print all turnout states to stream
|
||||
void Turnout::printAll(Print *stream){
|
||||
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
|
||||
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
|
||||
} // Turnout::printAll
|
||||
/*
|
||||
* Protected static data
|
||||
*/
|
||||
|
||||
bool Turnout::activate(int n,bool state){
|
||||
#ifdef EESTOREDEBUG
|
||||
DIAG(F("Turnout::activate(%d,%d)"),n,state);
|
||||
#endif
|
||||
Turnout * tt=get(n);
|
||||
if (tt==NULL) return false;
|
||||
tt->activate(state);
|
||||
turnoutlistHash++;
|
||||
return true;
|
||||
}
|
||||
/* static */ Turnout *Turnout::_firstTurnout = 0;
|
||||
|
||||
bool Turnout::isActive(int n){
|
||||
Turnout * tt=get(n);
|
||||
if (tt==NULL) return false;
|
||||
return tt->data.tStatus & STATUS_ACTIVE;
|
||||
}
|
||||
/*
|
||||
* Public static data
|
||||
*/
|
||||
/* static */ int Turnout::turnoutlistHash = 0;
|
||||
|
||||
/*
|
||||
* Protected static functions
|
||||
*/
|
||||
|
||||
// activate is virtual here so that it can be overridden by a non-DCC turnout mechanism
|
||||
void Turnout::activate(bool state) {
|
||||
#ifdef EESTOREDEBUG
|
||||
DIAG(F("Turnout::activate(%d)"),state);
|
||||
#endif
|
||||
if (data.address==LCN_TURNOUT_ADDRESS) {
|
||||
// A LCN turnout is transmitted to the LCN master.
|
||||
LCN::send('T',data.id,state);
|
||||
return; // The tStatus will be updated by a message from the LCN master, later.
|
||||
}
|
||||
if (state)
|
||||
data.tStatus|=STATUS_ACTIVE;
|
||||
else
|
||||
data.tStatus &= ~STATUS_ACTIVE;
|
||||
if (data.tStatus & STATUS_PWM)
|
||||
PWMServoDriver::setServo(data.tStatus & STATUS_PWMPIN, (data.inactiveAngle+(state?data.moveAngle:0)));
|
||||
else
|
||||
DCC::setAccessory(data.address,data.subAddress, state);
|
||||
// Save state if stored in EEPROM
|
||||
if (EEStore::eeStore->data.nTurnouts > 0 && num > 0)
|
||||
EEPROM.put(num, data.tStatus);
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Turnout* Turnout::get(int n){
|
||||
Turnout *tt;
|
||||
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;tt=tt->nextTurnout);
|
||||
return(tt);
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool Turnout::remove(int n){
|
||||
Turnout *tt,*pp=NULL;
|
||||
|
||||
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextTurnout);
|
||||
|
||||
if(tt==NULL) return false;
|
||||
|
||||
if(tt==firstTurnout)
|
||||
firstTurnout=tt->nextTurnout;
|
||||
else
|
||||
pp->nextTurnout=tt->nextTurnout;
|
||||
|
||||
free(tt);
|
||||
turnoutlistHash++;
|
||||
return true;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Turnout::load(){
|
||||
struct TurnoutData data;
|
||||
Turnout *tt;
|
||||
|
||||
for(uint16_t i=0;i<EEStore::eeStore->data.nTurnouts;i++){
|
||||
EEPROM.get(EEStore::pointer(),data);
|
||||
if (data.tStatus & STATUS_PWM) tt=create(data.id,data.tStatus & STATUS_PWMPIN, data.inactiveAngle,data.moveAngle);
|
||||
else tt=create(data.id,data.address,data.subAddress);
|
||||
tt->data.tStatus=data.tStatus;
|
||||
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to status byte within EEPROM
|
||||
EEStore::advance(sizeof(tt->data));
|
||||
#ifdef EESTOREDEBUG
|
||||
tt->print(tt);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Turnout::store(){
|
||||
Turnout *tt;
|
||||
|
||||
tt=firstTurnout;
|
||||
EEStore::eeStore->data.nTurnouts=0;
|
||||
|
||||
while(tt!=NULL){
|
||||
#ifdef EESTOREDEBUG
|
||||
tt->print(tt);
|
||||
#endif
|
||||
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to tstatus byte within EEPROM
|
||||
EEPROM.put(EEStore::pointer(),tt->data);
|
||||
EEStore::advance(sizeof(tt->data));
|
||||
tt=tt->nextTurnout;
|
||||
EEStore::eeStore->data.nTurnouts++;
|
||||
/* static */ Turnout *Turnout::get(uint16_t id) {
|
||||
// Find turnout object from list.
|
||||
for (Turnout *tt = _firstTurnout; tt != NULL; tt = tt->_nextTurnout)
|
||||
if (tt->_turnoutData.id == id) return tt;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Turnout *Turnout::create(int id, int add, int subAdd){
|
||||
Turnout *tt=create(id);
|
||||
tt->data.address=add;
|
||||
tt->data.subAddress=subAdd;
|
||||
tt->data.tStatus=0;
|
||||
return(tt);
|
||||
}
|
||||
|
||||
Turnout *Turnout::create(int id, byte pin, int activeAngle, int inactiveAngle){
|
||||
Turnout *tt=create(id);
|
||||
tt->data.tStatus= STATUS_PWM | (pin & STATUS_PWMPIN);
|
||||
tt->data.inactiveAngle=inactiveAngle;
|
||||
tt->data.moveAngle=activeAngle-inactiveAngle;
|
||||
return(tt);
|
||||
}
|
||||
|
||||
Turnout *Turnout::create(int id){
|
||||
Turnout *tt=get(id);
|
||||
if (tt==NULL) {
|
||||
tt=(Turnout *)calloc(1,sizeof(Turnout));
|
||||
tt->nextTurnout=firstTurnout;
|
||||
firstTurnout=tt;
|
||||
tt->data.id=id;
|
||||
// Add new turnout to end of chain
|
||||
/* static */ void Turnout::add(Turnout *tt) {
|
||||
if (!_firstTurnout)
|
||||
_firstTurnout = tt;
|
||||
else {
|
||||
// Find last object on chain
|
||||
Turnout *ptr = _firstTurnout;
|
||||
for ( ; ptr->_nextTurnout!=0; ptr=ptr->_nextTurnout) {}
|
||||
// Line new object to last object.
|
||||
ptr->_nextTurnout = tt;
|
||||
}
|
||||
turnoutlistHash++;
|
||||
return tt;
|
||||
turnoutlistHash++;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Remove nominated turnout from turnout linked list and delete the object.
|
||||
/* static */ bool Turnout::remove(uint16_t id) {
|
||||
Turnout *tt,*pp=NULL;
|
||||
|
||||
for(tt=_firstTurnout; tt!=NULL && tt->_turnoutData.id!=id; pp=tt, tt=tt->_nextTurnout) {}
|
||||
if (tt == NULL) return false;
|
||||
|
||||
if (tt == _firstTurnout)
|
||||
_firstTurnout = tt->_nextTurnout;
|
||||
else
|
||||
pp->_nextTurnout = tt->_nextTurnout;
|
||||
|
||||
delete (ServoTurnout *)tt;
|
||||
|
||||
turnoutlistHash++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Public static functions
|
||||
*/
|
||||
|
||||
/* static */ bool Turnout::isClosed(uint16_t id) {
|
||||
Turnout *tt = get(id);
|
||||
if (tt)
|
||||
return tt->isClosed();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// print debug info about the state of a turnout
|
||||
//
|
||||
#ifdef EESTOREDEBUG
|
||||
void Turnout::print(Turnout *tt) {
|
||||
if (tt->data.tStatus & STATUS_PWM )
|
||||
DIAG(F("Turnout %d ZeroAngle %d MoveAngle %d Status %d"),tt->data.id, tt->data.inactiveAngle, tt->data.moveAngle,tt->data.tStatus & STATUS_ACTIVE);
|
||||
/* static */ bool Turnout::setClosedStateOnly(uint16_t id, bool closeFlag) {
|
||||
Turnout *tt = get(id);
|
||||
if (!tt) return false;
|
||||
tt->_turnoutData.closed = closeFlag;
|
||||
|
||||
// I know it says setClosedStateOnly, but we need to tell others
|
||||
// that the state has changed too.
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
RMFT2::turnoutEvent(id, closeFlag);
|
||||
#endif
|
||||
|
||||
CommandDistributor::broadcastTurnout(id, closeFlag);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Static setClosed function is invoked from close(), throw() etc. to perform the
|
||||
// common parts of the turnout operation. Code which is specific to a turnout
|
||||
// type should be placed in the virtual function setClosedInternal(bool) which is
|
||||
// called from here.
|
||||
/* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) {
|
||||
#if defined(DIAG_IO)
|
||||
if (closeFlag)
|
||||
DIAG(F("Turnout::close(%d)"), id);
|
||||
else
|
||||
DIAG(F("Turnout %d Addr %d Subaddr %d Status %d"),tt->data.id, tt->data.address, tt->data.subAddress,tt->data.tStatus & STATUS_ACTIVE);
|
||||
}
|
||||
DIAG(F("Turnout::throw(%d)"), id);
|
||||
#endif
|
||||
Turnout *tt = Turnout::get(id);
|
||||
if (!tt) return false;
|
||||
bool ok = tt->setClosedInternal(closeFlag);
|
||||
|
||||
if (ok) {
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
// 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
|
||||
|
||||
Turnout *Turnout::firstTurnout=NULL;
|
||||
int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists
|
||||
#if defined(EXRAIL_ACTIVE)
|
||||
RMFT2::turnoutEvent(id, closeFlag);
|
||||
#endif
|
||||
|
||||
// Send message to JMRI etc.
|
||||
CommandDistributor::broadcastTurnout(id, closeFlag);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef DISABLE_EEPROM
|
||||
// Load all turnout objects
|
||||
/* static */ void Turnout::load() {
|
||||
for (uint16_t i=0; i<EEStore::eeStore->data.nTurnouts; i++) {
|
||||
Turnout::loadTurnout();
|
||||
}
|
||||
}
|
||||
|
||||
// Save all turnout objects
|
||||
/* static */ void Turnout::store() {
|
||||
EEStore::eeStore->data.nTurnouts=0;
|
||||
for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) {
|
||||
tt->save();
|
||||
EEStore::eeStore->data.nTurnouts++;
|
||||
}
|
||||
}
|
||||
|
||||
// Load one turnout from EEPROM
|
||||
/* static */ Turnout *Turnout::loadTurnout () {
|
||||
Turnout *tt = 0;
|
||||
// Read turnout type from EEPROM
|
||||
struct TurnoutData turnoutData;
|
||||
int eepromAddress = EEStore::pointer() + offsetof(struct TurnoutData, flags); // Address of byte containing the closed flag.
|
||||
EEPROM.get(EEStore::pointer(), turnoutData);
|
||||
EEStore::advance(sizeof(turnoutData));
|
||||
|
||||
switch (turnoutData.turnoutType) {
|
||||
case TURNOUT_SERVO:
|
||||
// Servo turnout
|
||||
tt = ServoTurnout::load(&turnoutData);
|
||||
break;
|
||||
case TURNOUT_DCC:
|
||||
// DCC Accessory turnout
|
||||
tt = DCCTurnout::load(&turnoutData);
|
||||
break;
|
||||
case TURNOUT_VPIN:
|
||||
// VPIN turnout
|
||||
tt = VpinTurnout::load(&turnoutData);
|
||||
break;
|
||||
default:
|
||||
// If we find anything else, then we don't know what it is or how long it is,
|
||||
// so we can't go any further through the EEPROM!
|
||||
return NULL;
|
||||
}
|
||||
if (tt) {
|
||||
// Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero.
|
||||
tt->_eepromAddress = eepromAddress + offsetof(struct TurnoutData, flags);
|
||||
}
|
||||
|
||||
#ifdef EESTOREDEBUG
|
||||
printAll(&Serial);
|
||||
#endif
|
||||
return tt;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*************************************************************************************
|
||||
* ServoTurnout - Turnout controlled by servo device.
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
// Private Constructor
|
||||
ServoTurnout::ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) :
|
||||
Turnout(id, TURNOUT_SERVO, closed)
|
||||
{
|
||||
_servoTurnoutData.vpin = vpin;
|
||||
_servoTurnoutData.thrownPosition = thrownPosition;
|
||||
_servoTurnoutData.closedPosition = closedPosition;
|
||||
_servoTurnoutData.profile = profile;
|
||||
}
|
||||
|
||||
// Create function
|
||||
/* static */ Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) {
|
||||
#ifndef IO_NO_HAL
|
||||
Turnout *tt = get(id);
|
||||
if (tt) {
|
||||
// Object already exists, check if it is usable
|
||||
if (tt->isType(TURNOUT_SERVO)) {
|
||||
// Yes, so set parameters
|
||||
ServoTurnout *st = (ServoTurnout *)tt;
|
||||
st->_servoTurnoutData.vpin = vpin;
|
||||
st->_servoTurnoutData.thrownPosition = thrownPosition;
|
||||
st->_servoTurnoutData.closedPosition = closedPosition;
|
||||
st->_servoTurnoutData.profile = profile;
|
||||
// Don't touch the _closed parameter, retain the original value.
|
||||
|
||||
// We don't really need to do the following, since a call to IODevice::_writeAnalogue
|
||||
// will provide all the data that is required! However, if someone has configured
|
||||
// a Turnout, we should ensure that the SET() RESET() and other commands that use write()
|
||||
// behave consistently with the turnout commands.
|
||||
IODevice::configureServo(vpin, thrownPosition, closedPosition, profile, 0, closed);
|
||||
|
||||
// Set position directly to specified position - we don't know where it is moving from.
|
||||
IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);
|
||||
|
||||
return tt;
|
||||
} else {
|
||||
// Incompatible object, delete and recreate
|
||||
remove(id);
|
||||
}
|
||||
}
|
||||
tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed);
|
||||
IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);
|
||||
return tt;
|
||||
#else
|
||||
(void)id; (void)vpin; (void)thrownPosition; (void)closedPosition;
|
||||
(void)profile; (void)closed; // avoid compiler warnings.
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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);
|
||||
EEStore::advance(sizeof(servoTurnoutData));
|
||||
|
||||
// Create new object
|
||||
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
|
||||
void ServoTurnout::print(Print *stream) {
|
||||
StringFormatter::send(stream, F("<H %d SERVO %d %d %d %d %d>\n"), _turnoutData.id, _servoTurnoutData.vpin,
|
||||
_servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile,
|
||||
!_turnoutData.closed);
|
||||
}
|
||||
|
||||
// ServoTurnout-specific code for throwing or closing a servo turnout.
|
||||
bool ServoTurnout::setClosedInternal(bool close) {
|
||||
#ifndef IO_NO_HAL
|
||||
IODevice::writeAnalogue(_servoTurnoutData.vpin,
|
||||
close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile);
|
||||
_turnoutData.closed = close;
|
||||
#else
|
||||
(void)close; // avoid compiler warnings
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
EEPROM.put(EEStore::pointer(), _turnoutData);
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _servoTurnoutData);
|
||||
EEStore::advance(sizeof(_servoTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*************************************************************************************
|
||||
* DCCTurnout - Turnout controlled by DCC Accessory Controller.
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
#if defined(DCC_TURNOUTS_RCN_213)
|
||||
const bool DCCTurnout::rcn213Compliant = true;
|
||||
#else
|
||||
const bool DCCTurnout::rcn213Compliant = false;
|
||||
#endif
|
||||
|
||||
// DCCTurnoutData contains data specific to this subclass that is
|
||||
// written to EEPROM when the turnout is saved.
|
||||
struct DCCTurnoutData {
|
||||
// DCC address (Address in bits 15-2, subaddress in bits 1-0
|
||||
uint16_t address; // CS currently supports linear address 1-2048
|
||||
// That's DCC accessory address 1-512 and subaddress 0-3.
|
||||
} _dccTurnoutData; // 2 bytes
|
||||
|
||||
// Constructor
|
||||
DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) :
|
||||
Turnout(id, TURNOUT_DCC, false)
|
||||
{
|
||||
_dccTurnoutData.address = address;
|
||||
_dccTurnoutData.subAddress = subAdd;
|
||||
}
|
||||
|
||||
// Create function
|
||||
/* static */ Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) {
|
||||
Turnout *tt = get(id);
|
||||
if (tt) {
|
||||
// Object already exists, check if it is usable
|
||||
if (tt->isType(TURNOUT_DCC)) {
|
||||
// Yes, so set parameters<T>
|
||||
DCCTurnout *dt = (DCCTurnout *)tt;
|
||||
dt->_dccTurnoutData.address = add;
|
||||
dt->_dccTurnoutData.subAddress = subAdd;
|
||||
// Don't touch the _closed parameter, retain the original value.
|
||||
return tt;
|
||||
} else {
|
||||
// Incompatible object, delete and recreate
|
||||
remove(id);
|
||||
}
|
||||
}
|
||||
tt = (Turnout *)new DCCTurnout(id, add, subAdd);
|
||||
return tt;
|
||||
}
|
||||
|
||||
// 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);
|
||||
EEStore::advance(sizeof(dccTurnoutData));
|
||||
|
||||
// Create new object
|
||||
DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress);
|
||||
|
||||
return tt;
|
||||
#else
|
||||
(void)turnoutData;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
void DCCTurnout::print(Print *stream) {
|
||||
StringFormatter::send(stream, F("<H %d DCC %d %d %d>\n"), _turnoutData.id,
|
||||
_dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed);
|
||||
// Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this.
|
||||
StringFormatter::send(stream, F("<H %d %d %d %d>\n"), _turnoutData.id,
|
||||
_dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed);
|
||||
}
|
||||
|
||||
bool DCCTurnout::setClosedInternal(bool close) {
|
||||
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
|
||||
// and Close writes a 0.
|
||||
// RCN-213 specifies that Throw is 0 and Close is 1.
|
||||
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant);
|
||||
_turnoutData.closed = close;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
EEPROM.put(EEStore::pointer(), _turnoutData);
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _dccTurnoutData);
|
||||
EEStore::advance(sizeof(_dccTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* VpinTurnout - Turnout controlled through a HAL vpin.
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
// Constructor
|
||||
VpinTurnout::VpinTurnout(uint16_t id, VPIN vpin, bool closed) :
|
||||
Turnout(id, TURNOUT_VPIN, closed)
|
||||
{
|
||||
_vpinTurnoutData.vpin = vpin;
|
||||
}
|
||||
|
||||
// Create function
|
||||
/* static */ Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) {
|
||||
Turnout *tt = get(id);
|
||||
if (tt) {
|
||||
// Object already exists, check if it is usable
|
||||
if (tt->isType(TURNOUT_VPIN)) {
|
||||
// Yes, so set parameters
|
||||
VpinTurnout *vt = (VpinTurnout *)tt;
|
||||
vt->_vpinTurnoutData.vpin = vpin;
|
||||
// Don't touch the _closed parameter, retain the original value.
|
||||
return tt;
|
||||
} else {
|
||||
// Incompatible object, delete and recreate
|
||||
remove(id);
|
||||
}
|
||||
}
|
||||
tt = (Turnout *)new VpinTurnout(id, vpin, closed);
|
||||
return tt;
|
||||
}
|
||||
|
||||
// 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);
|
||||
EEStore::advance(sizeof(vpinTurnoutData));
|
||||
|
||||
// Create new object
|
||||
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.
|
||||
void VpinTurnout::print(Print *stream) {
|
||||
StringFormatter::send(stream, F("<H %d VPIN %d %d>\n"), _turnoutData.id, _vpinTurnoutData.vpin,
|
||||
!_turnoutData.closed);
|
||||
}
|
||||
|
||||
bool VpinTurnout::setClosedInternal(bool close) {
|
||||
IODevice::write(_vpinTurnoutData.vpin, close);
|
||||
_turnoutData.closed = close;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
EEPROM.put(EEStore::pointer(), _turnoutData);
|
||||
EEStore::advance(sizeof(_turnoutData));
|
||||
EEPROM.put(EEStore::pointer(), _vpinTurnoutData);
|
||||
EEStore::advance(sizeof(_vpinTurnoutData));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* LCNTurnout - Turnout controlled by Loconet
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
// LCNTurnout has no specific data, and in any case is not written to EEPROM!
|
||||
// struct LCNTurnoutData {
|
||||
// } _lcnTurnoutData; // 0 bytes
|
||||
|
||||
// Constructor
|
||||
LCNTurnout::LCNTurnout(uint16_t id, bool closed) :
|
||||
Turnout(id, TURNOUT_LCN, closed)
|
||||
{ }
|
||||
|
||||
// Create function
|
||||
/* static */ Turnout *LCNTurnout::create(uint16_t id, bool closed) {
|
||||
Turnout *tt = get(id);
|
||||
if (tt) {
|
||||
// Object already exists, check if it is usable
|
||||
if (tt->isType(TURNOUT_LCN)) {
|
||||
// Yes, so return this object
|
||||
return tt;
|
||||
} else {
|
||||
// Incompatible object, delete and recreate
|
||||
remove(id);
|
||||
}
|
||||
}
|
||||
tt = (Turnout *)new LCNTurnout(id, closed);
|
||||
return tt;
|
||||
}
|
||||
|
||||
bool LCNTurnout::setClosedInternal(bool close) {
|
||||
// Assume that the LCN command still uses 1 for throw and 0 for close...
|
||||
LCN::send('T', _turnoutData.id, !close);
|
||||
// The _turnoutData.closed flag should be updated by a message from the LCN master, later.
|
||||
return true;
|
||||
}
|
||||
|
||||
// LCN turnouts not saved to EEPROM.
|
||||
//void save() override { }
|
||||
//static Turnout *load(struct TurnoutData *turnoutData) {
|
||||
|
||||
// Report 1 for thrown, 0 for closed.
|
||||
void LCNTurnout::print(Print *stream) {
|
||||
StringFormatter::send(stream, F("<H %d LCN %d>\n"), _turnoutData.id,
|
||||
!_turnoutData.closed);
|
||||
}
|
||||
|
||||
|
320
Turnouts.h
320
Turnouts.h
@ -1,7 +1,13 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 M Steve Todd
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2013-2016 Gregg E. Berman
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
@ -16,46 +22,284 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef Turnouts_h
|
||||
#define Turnouts_h
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "DCC.h"
|
||||
#include "LCN.h"
|
||||
#ifndef TURNOUTS_H
|
||||
#define TURNOUTS_H
|
||||
|
||||
const byte STATUS_ACTIVE=0x80; // Flag as activated
|
||||
const byte STATUS_PWM=0x40; // Flag as a PWM turnout
|
||||
const byte STATUS_PWMPIN=0x3F; // PWM pin 0-63
|
||||
const int LCN_TURNOUT_ADDRESS=-1; // spoof dcc address -1 indicates a LCN turnout
|
||||
struct TurnoutData {
|
||||
int id;
|
||||
uint8_t tStatus; // has STATUS_ACTIVE, STATUS_PWM, STATUS_PWMPIN
|
||||
union {uint8_t subAddress; char moveAngle;}; //DCC sub addrerss or PWM difference from inactiveAngle
|
||||
union {int address; int inactiveAngle;}; // DCC address or PWM servo angle
|
||||
//#define EESTOREDEBUG
|
||||
#include "Arduino.h"
|
||||
#include "IODevice.h"
|
||||
#include "StringFormatter.h"
|
||||
|
||||
// Turnout type definitions
|
||||
enum {
|
||||
TURNOUT_DCC = 1,
|
||||
TURNOUT_SERVO = 2,
|
||||
TURNOUT_VPIN = 3,
|
||||
TURNOUT_LCN = 4,
|
||||
};
|
||||
|
||||
/*************************************************************************************
|
||||
* Turnout - Base class for turnouts.
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
class Turnout {
|
||||
public:
|
||||
static Turnout *firstTurnout;
|
||||
static int turnoutlistHash;
|
||||
TurnoutData data;
|
||||
Turnout *nextTurnout;
|
||||
static bool activate(int n, bool state);
|
||||
static Turnout* get(int);
|
||||
static bool remove(int);
|
||||
static bool isActive(int);
|
||||
static void load();
|
||||
static void store();
|
||||
static Turnout *create(int id , int address , int subAddress);
|
||||
static Turnout *create(int id , byte pin , int activeAngle, int inactiveAngle);
|
||||
static Turnout *create(int id);
|
||||
void activate(bool state);
|
||||
static void printAll(Print *);
|
||||
#ifdef EESTOREDEBUG
|
||||
void print(Turnout *tt);
|
||||
#endif
|
||||
private:
|
||||
int num; // EEPROM address of tStatus in TurnoutData struct, or zero if not stored.
|
||||
}; // Turnout
|
||||
protected:
|
||||
/*
|
||||
* Object data
|
||||
*/
|
||||
|
||||
// The TurnoutData struct contains data common to all turnout types, that
|
||||
// is written to EEPROM when the turnout is saved.
|
||||
// The first byte of this struct contains the 'closed' flag which is
|
||||
// updated whenever the turnout changes from thrown to closed and
|
||||
// vice versa. If the turnout has been saved, then this byte is rewritten
|
||||
// when changed in RAM. The 'closed' flag must be located in the first byte.
|
||||
struct TurnoutData {
|
||||
union {
|
||||
struct {
|
||||
bool closed : 1;
|
||||
bool _rfu: 2;
|
||||
uint8_t turnoutType : 5;
|
||||
};
|
||||
uint8_t flags;
|
||||
};
|
||||
uint16_t id;
|
||||
} _turnoutData; // 3 bytes
|
||||
|
||||
// Address in eeprom of first byte of the _turnoutData struct (containing the closed flag).
|
||||
// Set to zero if the object has not been saved in EEPROM, e.g. for newly created Turnouts, and
|
||||
// for all LCN turnouts.
|
||||
uint16_t _eepromAddress = 0;
|
||||
|
||||
// Pointer to next turnout on linked list.
|
||||
Turnout *_nextTurnout = 0;
|
||||
|
||||
/*
|
||||
* Constructor
|
||||
*/
|
||||
Turnout(uint16_t id, uint8_t turnoutType, bool closed) {
|
||||
_turnoutData.id = id;
|
||||
_turnoutData.turnoutType = turnoutType;
|
||||
_turnoutData.closed = closed;
|
||||
add(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Static data
|
||||
*/
|
||||
|
||||
static Turnout *_firstTurnout;
|
||||
static int _turnoutlistHash;
|
||||
|
||||
/*
|
||||
* Virtual functions
|
||||
*/
|
||||
|
||||
virtual bool setClosedInternal(bool close) = 0; // Mandatory in subclass
|
||||
virtual void save() {}
|
||||
|
||||
/*
|
||||
* Static functions
|
||||
*/
|
||||
|
||||
static Turnout *get(uint16_t id);
|
||||
|
||||
static void add(Turnout *tt);
|
||||
|
||||
public:
|
||||
/*
|
||||
* Static data
|
||||
*/
|
||||
static int turnoutlistHash;
|
||||
static const bool useClassicTurnoutCommands;
|
||||
|
||||
/*
|
||||
* Public base class functions
|
||||
*/
|
||||
inline bool isClosed() { return _turnoutData.closed; };
|
||||
inline bool isThrown() { return !_turnoutData.closed; }
|
||||
inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; }
|
||||
inline uint16_t getId() { return _turnoutData.id; }
|
||||
inline Turnout *next() { return _nextTurnout; }
|
||||
void printState(Print *stream);
|
||||
/*
|
||||
* Virtual functions
|
||||
*/
|
||||
virtual void print(Print *stream) {
|
||||
(void)stream; // avoid compiler warnings.
|
||||
}
|
||||
virtual ~Turnout() {} // Destructor
|
||||
|
||||
/*
|
||||
* Public static functions
|
||||
*/
|
||||
inline static bool exists(uint16_t id) { return get(id) != 0; }
|
||||
|
||||
static bool remove(uint16_t id);
|
||||
|
||||
static bool isClosed(uint16_t id);
|
||||
|
||||
inline static bool isThrown(uint16_t id) {
|
||||
return !isClosed(id);
|
||||
}
|
||||
|
||||
static bool setClosed(uint16_t id, bool closeFlag);
|
||||
|
||||
inline static bool setClosed(uint16_t id) {
|
||||
return setClosed(id, true);
|
||||
}
|
||||
|
||||
inline static bool setThrown(uint16_t id) {
|
||||
return setClosed(id, false);
|
||||
}
|
||||
|
||||
static bool setClosedStateOnly(uint16_t id, bool close);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* ServoTurnout - Turnout controlled by servo device.
|
||||
*
|
||||
*************************************************************************************/
|
||||
class ServoTurnout : public Turnout {
|
||||
private:
|
||||
// ServoTurnoutData contains data specific to this subclass that is
|
||||
// written to EEPROM when the turnout is saved.
|
||||
struct ServoTurnoutData {
|
||||
VPIN vpin;
|
||||
uint16_t closedPosition : 12;
|
||||
uint16_t thrownPosition : 12;
|
||||
uint8_t profile;
|
||||
} _servoTurnoutData; // 6 bytes
|
||||
|
||||
// Constructor
|
||||
ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed);
|
||||
|
||||
public:
|
||||
// Create function
|
||||
static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed=true);
|
||||
|
||||
// Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
static Turnout *load(struct TurnoutData *turnoutData);
|
||||
void print(Print *stream) override;
|
||||
|
||||
protected:
|
||||
// ServoTurnout-specific code for throwing or closing a servo turnout.
|
||||
bool setClosedInternal(bool close) override;
|
||||
void save() override;
|
||||
|
||||
};
|
||||
|
||||
/*************************************************************************************
|
||||
* DCCTurnout - Turnout controlled by DCC Accessory Controller.
|
||||
*
|
||||
*************************************************************************************/
|
||||
class DCCTurnout : public Turnout {
|
||||
private:
|
||||
// DCCTurnoutData contains data specific to this subclass that is
|
||||
// written to EEPROM when the turnout is saved.
|
||||
struct DCCTurnoutData {
|
||||
// DCC address (Address in bits 15-2, subaddress in bits 1-0)
|
||||
struct {
|
||||
uint16_t address : 14;
|
||||
uint8_t subAddress : 2;
|
||||
};
|
||||
} _dccTurnoutData; // 2 bytes
|
||||
|
||||
// Constructor
|
||||
DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd);
|
||||
|
||||
public:
|
||||
// Create function
|
||||
static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd);
|
||||
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
static Turnout *load(struct TurnoutData *turnoutData);
|
||||
void print(Print *stream) override;
|
||||
// Flag whether DCC Accessory packets are to contain 1=close/0=throw(RCN-213) or 1=throw/0-close (DCC++ Classic)
|
||||
static const bool rcn213Compliant;
|
||||
|
||||
protected:
|
||||
bool setClosedInternal(bool close) override;
|
||||
void save() override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* VpinTurnout - Turnout controlled through a HAL vpin.
|
||||
*
|
||||
*************************************************************************************/
|
||||
class VpinTurnout : public Turnout {
|
||||
private:
|
||||
// VpinTurnoutData contains data specific to this subclass that is
|
||||
// written to EEPROM when the turnout is saved.
|
||||
struct VpinTurnoutData {
|
||||
VPIN vpin;
|
||||
} _vpinTurnoutData; // 2 bytes
|
||||
|
||||
// Constructor
|
||||
VpinTurnout(uint16_t id, VPIN vpin, bool closed);
|
||||
|
||||
public:
|
||||
// Create function
|
||||
static Turnout *create(uint16_t id, VPIN vpin, bool closed=true);
|
||||
|
||||
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
|
||||
static Turnout *load(struct TurnoutData *turnoutData);
|
||||
void print(Print *stream) override;
|
||||
|
||||
protected:
|
||||
bool setClosedInternal(bool close) override;
|
||||
void save() override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* LCNTurnout - Turnout controlled by Loconet
|
||||
*
|
||||
*************************************************************************************/
|
||||
class LCNTurnout : public Turnout {
|
||||
private:
|
||||
// LCNTurnout has no specific data, and in any case is not written to EEPROM!
|
||||
// struct LCNTurnoutData {
|
||||
// } _lcnTurnoutData; // 0 bytes
|
||||
|
||||
// Constructor
|
||||
LCNTurnout(uint16_t id, bool closed);
|
||||
|
||||
public:
|
||||
// Create function
|
||||
static Turnout *create(uint16_t id, bool closed=true);
|
||||
|
||||
|
||||
bool setClosedInternal(bool close) override;
|
||||
|
||||
// LCN turnouts not saved to EEPROM.
|
||||
//void save() override { }
|
||||
//static Turnout *load(struct TurnoutData *turnoutData) {
|
||||
|
||||
void print(Print *stream) override;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
596
WiThrottle.cpp
596
WiThrottle.cpp
@ -1,8 +1,12 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2020-2022 Harald Barth
|
||||
* © 2020-2021 M Steve Todd
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
* 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
|
||||
@ -40,6 +44,7 @@
|
||||
* WiThrottle.h sets the max locos per client at 10, this is ok to increase but requires just an extra 3 bytes per loco per client.
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include "defines.h"
|
||||
#include "WiThrottle.h"
|
||||
#include "DCC.h"
|
||||
#include "DCCWaveform.h"
|
||||
@ -48,12 +53,13 @@
|
||||
#include "DIAG.h"
|
||||
#include "GITHUB_SHA.h"
|
||||
#include "version.h"
|
||||
#include "EXRAIL2.h"
|
||||
#include "CommandDistributor.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))
|
||||
|
||||
WiThrottle * WiThrottle::firstThrottle=NULL;
|
||||
bool WiThrottle::annotateLeftRight=false;
|
||||
|
||||
WiThrottle* WiThrottle::getThrottle( int wifiClient) {
|
||||
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
||||
@ -83,6 +89,8 @@ WiThrottle::WiThrottle( int wificlientid) {
|
||||
initSent=false; // prevent sending heartbeats before connection completed
|
||||
heartBeatEnable=false; // until client turns it on
|
||||
turnoutListHash = -1; // make sure turnout list is sent once
|
||||
exRailSent=false;
|
||||
mostRecentCab=0;
|
||||
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
|
||||
}
|
||||
|
||||
@ -107,99 +115,127 @@ 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::firstTurnout;tt!=NULL;tt=tt->nextTurnout){
|
||||
StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), tt->data.id, tt->data.id, Turnout::isActive(tt->data.id)?'4':'2');
|
||||
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("\n"));
|
||||
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle
|
||||
int id=getInt(cmd+4);
|
||||
bool newstate=false;
|
||||
Turnout * tt=Turnout::get(id);
|
||||
if (!tt) {
|
||||
// If turnout does not exist, create it
|
||||
int addr = ((id - 1) / 4) + 1;
|
||||
int subaddr = (id - 1) % 4;
|
||||
Turnout::create(id,addr,subaddr);
|
||||
StringFormatter::send(stream, F("HmTurnout %d created\n"),id);
|
||||
}
|
||||
switch (cmd[3]) {
|
||||
case 'T': newstate=true; break;
|
||||
case 'C': newstate=false; break;
|
||||
case '2': newstate=!Turnout::isActive(id);
|
||||
}
|
||||
Turnout::activate(id,newstate);
|
||||
StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',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));
|
||||
if (annotateLeftRight) StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Left}|{2]\\[Right}|{4\n"));
|
||||
else StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{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++;
|
||||
else if (!exRailSent) {
|
||||
// Send EX-RAIL routes list if not already sent (but not at same time as turnouts above)
|
||||
exRailSent=true;
|
||||
#ifdef EXRAIL_ACTIVE
|
||||
RMFT2::emitWithrottleRouteList(stream);
|
||||
#endif
|
||||
// allow heartbeat to slow down once all metadata sent
|
||||
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
int WiThrottle::getLocoId(byte * cmd) {
|
||||
@ -209,139 +245,183 @@ 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;
|
||||
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++) {
|
||||
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++) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
switch (aval[0]) {
|
||||
case 'V': // Vspeed
|
||||
{
|
||||
int witSpeed=getInt(aval+1);
|
||||
LOOPLOCOS(throttleChar, 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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -349,24 +429,17 @@ int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
|
||||
}
|
||||
|
||||
void WiThrottle::loop(RingStream * stream) {
|
||||
// for each WiThrottle, check the heartbeat
|
||||
// for each WiThrottle, check the heartbeat and broadcast needed
|
||||
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
|
||||
wt->checkHeartbeat();
|
||||
|
||||
// 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()
|
||||
*/
|
||||
wt->checkHeartbeat(stream);
|
||||
|
||||
|
||||
}
|
||||
|
||||
void WiThrottle::checkHeartbeat() {
|
||||
void WiThrottle::checkHeartbeat(RingStream * stream) {
|
||||
// if eStop time passed... eStop any locos still assigned to this client and then drop the connection
|
||||
if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {
|
||||
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
|
||||
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);
|
||||
@ -374,15 +447,65 @@ void WiThrottle::checkHeartbeat() {
|
||||
}
|
||||
}
|
||||
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<127)?'S':'L';
|
||||
return (cab<=HIGHEST_SHORT_ADDR)?'S':'L';
|
||||
}
|
||||
|
||||
// Drive Away feature. Callback handling
|
||||
|
||||
|
||||
RingStream * WiThrottle::stashStream;
|
||||
WiThrottle * WiThrottle::stashInstance;
|
||||
byte WiThrottle::stashClient;
|
||||
@ -390,13 +513,26 @@ 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"));
|
||||
|
||||
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) { // long addr
|
||||
locoid = locoid ^ LONG_ADDR_MARKER;
|
||||
addrchar = 'L';
|
||||
} else
|
||||
addrchar = 'S';
|
||||
if (addrchar == 'L' && locoid <= HIGHEST_SHORT_ADDR )
|
||||
StringFormatter::send(stashStream,F("HMLong addr %d <= %d not supported\n"), locoid,HIGHEST_SHORT_ADDR);
|
||||
else {
|
||||
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();
|
||||
}
|
||||
|
20
WiThrottle.h
20
WiThrottle.h
@ -1,5 +1,7 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2021 Mike S
|
||||
* © 2020-2021 Chris Harlow
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of Asbelos DCC API
|
||||
*
|
||||
@ -24,6 +26,9 @@
|
||||
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 {
|
||||
@ -31,14 +36,15 @@ class WiThrottle {
|
||||
static void loop(RingStream * stream);
|
||||
void parse(RingStream * stream, byte * cmd);
|
||||
static WiThrottle* getThrottle( int wifiClient);
|
||||
static bool annotateLeftRight;
|
||||
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=4; // heartbeat at 4secs to provide messaging transport
|
||||
static const int ESTOP_SECONDS=8; // eStop if no incoming messages for more than 8secs
|
||||
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 WiThrottle* firstThrottle;
|
||||
static int getInt(byte * cmd);
|
||||
static int getLocoId(byte * cmd);
|
||||
@ -53,6 +59,8 @@ class WiThrottle {
|
||||
bool heartBeatEnable;
|
||||
unsigned long heartBeat;
|
||||
bool initSent; // valid connection established
|
||||
bool exRailSent; // valid connection established
|
||||
uint16_t mostRecentCab;
|
||||
int turnoutListHash; // used to check for changes to turnout list
|
||||
bool lastPowerState; // last power state sent to this client
|
||||
int DCCToWiTSpeed(int DCCSpeed);
|
||||
@ -60,8 +68,8 @@ class WiThrottle {
|
||||
void multithrottle(RingStream * stream, byte * cmd);
|
||||
void locoAction(RingStream * stream, byte* aval, char throttleChar, int cab);
|
||||
void accessory(RingStream *, byte* cmd);
|
||||
void checkHeartbeat();
|
||||
|
||||
void checkHeartbeat(RingStream * stream);
|
||||
void markForBroadcast2(int cab);
|
||||
// callback stuff to support prog track acquire
|
||||
static RingStream * stashStream;
|
||||
static WiThrottle * stashInstance;
|
||||
|
@ -1,4 +1,7 @@
|
||||
/*
|
||||
* © 2021 Fred Decker
|
||||
* © 2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
@ -85,7 +88,9 @@ void WifiInboundHandler::loop1() {
|
||||
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
|
||||
outboundRing->commit();
|
||||
if (!outboundRing->commit()) {
|
||||
DIAG(F("OUTBOUND FULL processing cmd:%s"),cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -150,7 +155,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
if (ch=='E' || ch=='l') { // ERROR or "link is not valid"
|
||||
if (clientPendingCIPSEND>=0) {
|
||||
// A CIPSEND was errored... just toss it away
|
||||
purgeCurrentCIPSEND();
|
||||
purgeCurrentCIPSEND();
|
||||
}
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
@ -229,6 +234,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
if (ch=='C') {
|
||||
// got "x C" before CLOSE or CONNECTED, or CONNECT FAILED
|
||||
if (runningClientId==clientPendingCIPSEND) purgeCurrentCIPSEND();
|
||||
else CommandDistributor::forget(runningClientId);
|
||||
}
|
||||
loopState=SKIPTOEND;
|
||||
break;
|
||||
@ -243,8 +249,9 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
|
||||
|
||||
void WifiInboundHandler::purgeCurrentCIPSEND() {
|
||||
// A CIPSEND was sent but errored... or the client closed just toss it away
|
||||
if (Diag::WIFI) DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
|
||||
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
|
||||
CommandDistributor::forget(clientPendingCIPSEND);
|
||||
DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
|
||||
for (int i=0;i<currentReplySize;i++) outboundRing->read();
|
||||
pendingCipsend=false;
|
||||
clientPendingCIPSEND=-1;
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
/*
|
||||
* © 2021 Harald Barth
|
||||
* © 2021 Fred Decker
|
||||
* (c) 2021 Fred Decker. All rights reserved.
|
||||
* (c) 2020 Chris Harlow. All rights reserved.
|
||||
*
|
||||
|
@ -1,22 +1,24 @@
|
||||
/*
|
||||
© 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/>.
|
||||
*/
|
||||
* © 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/>.
|
||||
*/
|
||||
#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 */
|
||||
@ -75,13 +77,13 @@ bool WifiInterface::setup(long serial_link_speed,
|
||||
(void) channel;
|
||||
#endif
|
||||
|
||||
#if NUM_SERIAL > 0
|
||||
#if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS)
|
||||
Serial1.begin(serial_link_speed);
|
||||
wifiUp = setup(Serial1, wifiESSID, wifiPassword, hostname, port, channel);
|
||||
#endif
|
||||
|
||||
// Other serials are tried, depending on hardware.
|
||||
#if NUM_SERIAL > 1
|
||||
#if NUM_SERIAL > 1 && !defined(SERIAL2_COMMANDS)
|
||||
if (wifiUp == WIFI_NOAT)
|
||||
{
|
||||
Serial2.begin(serial_link_speed);
|
||||
@ -89,7 +91,7 @@ bool WifiInterface::setup(long serial_link_speed,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if NUM_SERIAL > 2
|
||||
#if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS)
|
||||
if (wifiUp == WIFI_NOAT)
|
||||
{
|
||||
Serial3.begin(serial_link_speed);
|
||||
@ -277,10 +279,16 @@ 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
|
||||
@ -316,19 +324,45 @@ 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(const byte * command) {
|
||||
void WifiInterface::ATCommand(HardwareSerial * stream,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
/*
|
||||
* © 2020, Chris Harlow. All rights reserved.
|
||||
* © 2020-2021 Chris Harlow
|
||||
* © 2020, Harald Barth.
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify
|
||||
@ -37,7 +38,7 @@ public:
|
||||
const int port,
|
||||
const byte channel);
|
||||
static void loop();
|
||||
static void ATCommand(const byte *command);
|
||||
static void ATCommand(HardwareSerial * stream,const byte *command);
|
||||
|
||||
private:
|
||||
static wifiSerialState setup(Stream &setupStream, const FSH *SSSid, const FSH *password,
|
||||
|
@ -1,5 +1,8 @@
|
||||
/*
|
||||
* COPYRIGHT (c) 2020 Fred Decker
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020-2021 Harald Barth
|
||||
* © 2020-2021 Fred Decker
|
||||
* © 2020-2021 Chris Harlow
|
||||
*
|
||||
* This file is part of CommandStation-EX
|
||||
*
|
||||
@ -120,18 +123,81 @@ The configuration file for DCC-EX Command Station
|
||||
//
|
||||
// DEFINE LCD SCREEN USAGE BY THE BASE STATION
|
||||
//
|
||||
// Note: This feature requires an I2C enabled LCD screen using a PCF8574 based chipset.
|
||||
// or one using a Hitachi HD44780.
|
||||
// OR an I2C Oled screen.
|
||||
// To enable, uncomment one of the lines below
|
||||
// Note: This feature requires an I2C enabled LCD screen using a Hitachi HD44780
|
||||
// controller and a commonly available PCF8574 based I2C 'backpack'.
|
||||
// To enable, uncomment one of the #define lines below
|
||||
|
||||
// define LCD_DRIVER for I2C LCD address 0x3f,16 cols, 2 rows
|
||||
// #define LCD_DRIVER 0x3F,16,2
|
||||
// define LCD_DRIVER for I2C address 0x27, 16 cols, 2 rows
|
||||
// #define LCD_DRIVER 0x27,16,2
|
||||
|
||||
//OR define OLED_DRIVER width,height in pixels (address auto detected)
|
||||
// 128x32 or 128x64 I2C SSD1306-based devices are supported.
|
||||
// Also 132x64 I2C SH1106 devices.
|
||||
// Also 132x64 I2C SH1106 devices
|
||||
// #define OLED_DRIVER 128,32
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// Define scroll mode as 0, 1 or 2
|
||||
#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 works.
|
||||
//
|
||||
// #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
|
||||
//
|
||||
// According to norm RCN-213 a DCC packet with a 1 is closed/straight
|
||||
// and one with a 0 is thrown/diverging. In DCC++ Classic, and in previous
|
||||
// versions of DCC++EX, a turnout throw command was implemented in the packet as
|
||||
// '1' and a close command as '0'. The #define below makes the states
|
||||
// match with the norm. But we don't want to cause havoc on existent layouts,
|
||||
// so we define this only for new installations. If you don't want this,
|
||||
// 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 revese 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
|
||||
// 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
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
90
defines.h
90
defines.h
@ -1,22 +1,39 @@
|
||||
/*
|
||||
© 2020,2021 Harald Barth.
|
||||
* © 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
This file is part of CommandStation-EX
|
||||
#ifndef DEFINES_H
|
||||
#define DEFINES_H
|
||||
|
||||
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/>.
|
||||
|
||||
*/
|
||||
// defines.h relies on macros defined in config.h
|
||||
// but it may have already been included (for cosmetic convenence) by the .ino
|
||||
#ifndef MOTOR_SHIELD_TYPE
|
||||
#if __has_include ( "config.h")
|
||||
#include "config.h"
|
||||
#else
|
||||
#include "config.example.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@ -31,19 +48,30 @@
|
||||
// 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 ENABLE_WIFI && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO) || defined (ESP_FAMILY))
|
||||
#define WIFI_ON true
|
||||
#ifndef WIFI_CHANNEL
|
||||
#define WIFI_CHANNEL 1
|
||||
#endif
|
||||
#else
|
||||
#define WIFI_ON false
|
||||
#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO)) || defined(ARDUINO_AVR_NANO_EVERY) || 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
|
||||
#endif
|
||||
|
||||
#if ENABLE_ETHERNET && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
|
||||
#define ETHERNET_ON true
|
||||
#if ENABLE_ETHERNET
|
||||
#if defined(BIG_RAM)
|
||||
#define ETHERNET_ON true
|
||||
#else
|
||||
#define ETHERNET_WARNING
|
||||
#define ETHERNET_ON false
|
||||
#endif
|
||||
#else
|
||||
#define ETHERNET_ON false
|
||||
#define ETHERNET_ON false
|
||||
#endif
|
||||
|
||||
#if WIFI_ON && ETHERNET_ON
|
||||
@ -56,3 +84,13 @@
|
||||
// Currently only devices which can communicate at 115200 are supported.
|
||||
//
|
||||
#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
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
* © 2020, Harald Barth
|
||||
* © 2021, Neil McKechnie
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2021 Mike S
|
||||
* © 2020 Harald Barth
|
||||
*
|
||||
* This file is part of Asbelos DCC-EX
|
||||
*
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* © 2020, Harald Barth
|
||||
* © 2021, Neil McKechnie
|
||||
* © 2021 Neil McKechnie
|
||||
* © 2020 Harald Barth
|
||||
*
|
||||
* This file is part of DCC-EX
|
||||
*
|
||||
|
84
myAutomation.example.h
Normal file
84
myAutomation.example.h
Normal file
@ -0,0 +1,84 @@
|
||||
/* This is an automation example file.
|
||||
* The presence of a file called "myAutomation.h" brings EX-RAIL code into
|
||||
* the command station.
|
||||
* 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 first
|
||||
* instruction after E$XRAIL.
|
||||
* This task may simply follow a route, or may START
|
||||
* further tasks (thats is.. send a loco out along a route).
|
||||
*
|
||||
* Where the loco id is not known at compile time, a new task
|
||||
* can be creatd with the command:
|
||||
* </ START [cab] route>
|
||||
*
|
||||
* A ROUTE, AUTOMATION or SEQUENCE are internally identical in ExRail terms
|
||||
* but are just represented differently to a Withrottle user:
|
||||
* ROUTE(n,"name") - as Route_n .. to setup a route through a layout
|
||||
* AUTOMATION(n,"name") as Auto_n .. to send the current loco off along an automated journey
|
||||
* SEQUENCE(n) is not visible to Withrottle.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
* S20 S21
|
||||
* === START->================
|
||||
*/
|
||||
SEQUENCE(1)
|
||||
DELAY(10000) // wait 10 seconds
|
||||
FON(3) // Set Loco Function 3, Horn on
|
||||
DELAY(1000) // wait 1 second
|
||||
FOFF(3) // Horn off
|
||||
FWD(80) // Move forward at speed 80
|
||||
AT(21) // until we hit sensor id 21
|
||||
STOP // then stop
|
||||
DELAY(5000) // Wait 5 seconds
|
||||
FON(2) // ring bell
|
||||
REV(60) // reverse at speed 60
|
||||
AT(20) // until we get to S20
|
||||
STOP // then stop
|
||||
FOFF(2) // Bell off
|
||||
FOLLOW(1) // and follow sequence 1 again
|
||||
|
||||
/* SEQUENCE(2) is an automation example for a single loco Y shaped journey
|
||||
* S31,S32,S33 are sensors, T4 is a turnout
|
||||
*
|
||||
* S33 T4 S31
|
||||
* ===-START->=============================================
|
||||
* //
|
||||
* S32 //
|
||||
* ======================//
|
||||
*
|
||||
* Train runs from START to S31, back to S32, again to S31, Back to start.
|
||||
*/
|
||||
SEQUENCE(2)
|
||||
FWD(60) // go forward at DCC speed 60
|
||||
AT(31) STOP // when we get to sensor 31
|
||||
DELAY(10000) // wait 10 seconds
|
||||
THROW(4) // throw turnout for route to S32
|
||||
REV(45) // go backwards at speed 45
|
||||
AT(32) STOP // until we arrive at sensor 32
|
||||
DELAY(5000) // wait 5 seconds
|
||||
FWD(50) // go forwards at speed 50
|
||||
AT(31) STOP // and stop at sensor 31
|
||||
DELAY(5000) // wait 5 seconds
|
||||
CLOSE(4) // set turnout closed
|
||||
REV(50) // reverse back to S3
|
||||
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.
|
||||
|
||||
|
165
myHal.cpp_example.txt
Normal file
165
myHal.cpp_example.txt
Normal file
@ -0,0 +1,165 @@
|
||||
// Sample myHal.cpp file.
|
||||
//
|
||||
// To use this file, copy it to myHal.cpp and uncomment the directives and/or
|
||||
// edit them to satisfy your requirements. If you only want to use up to
|
||||
// two MCP23017 GPIO Expander modules and/or up to two PCA9685 Servo modules,
|
||||
// then you don't need this file as DCC++EX configures these for free!
|
||||
|
||||
// Note that if the file has a .cpp extension it WILL be compiled into the build
|
||||
// and the halSetup() function WILL be invoked.
|
||||
//
|
||||
// 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
|
||||
#include "IO_VL53L0X.h" // Laser time-of-flight sensor
|
||||
#include "IO_DFPlayer.h" // MP3 sound player
|
||||
|
||||
|
||||
//==========================================================================
|
||||
// The function halSetup() is invoked from CS if it exists within the build.
|
||||
// The setup calls are included between the open and close braces "{ ... }".
|
||||
// Comments (lines preceded by "//") are optional.
|
||||
//==========================================================================
|
||||
|
||||
void halSetup() {
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a PCA9685 PWM Servo driver module.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// First Vpin=100
|
||||
// Number of VPINs=16 (numbered 100-115)
|
||||
// I2C address of module=0x40
|
||||
|
||||
//PCA9685::create(100, 16, 0x40);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines an MCP23017 16-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// First Vpin=196
|
||||
// Number of VPINs=16 (numbered 196-211)
|
||||
// I2C address of module=0x22
|
||||
|
||||
//MCP23017::create(196, 16, 0x22);
|
||||
|
||||
|
||||
// Alternative form, which allows the INT pin of the module to request a scan
|
||||
// by pulling Arduino pin 40 to ground. Means that the I2C isn't being polled
|
||||
// all the time, only when a change takes place. Multiple modules' INT pins
|
||||
// may be connected to the same Arduino pin.
|
||||
|
||||
//MCP23017::create(196, 16, 0x22, 40);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines an MCP23008 8-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// First Vpin=300
|
||||
// Number of VPINs=8 (numbered 300-307)
|
||||
// I2C address of module=0x22
|
||||
|
||||
//MCP23008::create(300, 8, 0x22);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a PCF8574 8-port I2C GPIO Extender module.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// First Vpin=200
|
||||
// Number of VPINs=8 (numbered 200-207)
|
||||
// I2C address of module=0x23
|
||||
|
||||
//PCF8574::create(200, 8, 0x23);
|
||||
|
||||
|
||||
// Alternative form using INT pin (see above)
|
||||
|
||||
//PCF8574::create(200, 8, 0x23, 40);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines an HCSR04 ultrasonic ranging module.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// Vpin=2000 (only one VPIN per directive)
|
||||
// Number of VPINs=1
|
||||
// Arduino pin connected to TRIG=30
|
||||
// Arduino pin connected to ECHO=31
|
||||
// Minimum trigger range=20cm (VPIN goes to 1 when <20cm)
|
||||
// Maximum trigger range=25cm (VPIN goes to 0 when >25cm)
|
||||
// Note: Multiple devices can be configured by using a different ECHO pin
|
||||
// for each one. The TRIG pin can be shared between multiple devices.
|
||||
// Be aware that the 'ping' of one device may be received by another
|
||||
// device and position them accordingly!
|
||||
|
||||
//HCSR04::create(2000, 30, 31, 20, 25);
|
||||
//HCSR04::create(2001, 30, 32, 20, 25);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// The following directive defines a single VL53L0X Time-of-Flight range sensor.
|
||||
//=======================================================================
|
||||
// The parameters are:
|
||||
// VPIN=5000
|
||||
// Number of VPINs=1
|
||||
// I2C address=0x29 (default for this chip)
|
||||
// Minimum trigger range=200mm (VPIN goes to 1 when <20cm)
|
||||
// Maximum trigger range=250mm (VPIN goes to 0 when >25cm)
|
||||
|
||||
//VL53L0X::create(5000, 1, 0x29, 200, 250);
|
||||
|
||||
// For multiple VL53L0X modules, add another parameter which is a VPIN connected to the
|
||||
// module's XSHUT pin. This allows the modules to be configured, at start,
|
||||
// with distinct I2C addresses. In this case, the address 0x29 is only used during
|
||||
// initialisation to configure each device in turn with the desired unique I2C address.
|
||||
// The examples below have the modules' XSHUT pins connected to the first two pins of
|
||||
// the first MCP23017 module (164 and 165), but Arduino pins may be used instead.
|
||||
// The first module here is given I2C address 0x30 and the second is 0x31.
|
||||
|
||||
//VL53L0X::create(5000, 1, 0x30, 200, 250, 164);
|
||||
//VL53L0X::create(5001, 1, 0x31, 200, 250, 165);
|
||||
|
||||
|
||||
//=======================================================================
|
||||
// Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module.
|
||||
//=======================================================================
|
||||
// Parameters:
|
||||
// 10000 = first VPIN allocated.
|
||||
// 10 = number of VPINs allocated.
|
||||
// Serial1 = name of serial port (usually Serial1 or Serial2).
|
||||
// With these parameters, up to 10 files may be played on pins 10000-10009.
|
||||
// Play is started from EX-RAIL with SET(10000) for first mp3 file, SET(10001)
|
||||
// for second file, etc. Play may also be initiated by writing an analogue
|
||||
// value to the first pin, e.g. SERVO(10000,23,0) will play the 23rd mp3 file.
|
||||
// SERVO(10000,23,30) will do the same thing, as well as setting the volume to
|
||||
// 30 (maximum value).
|
||||
// Play is stopped by RESET(10000) (or any other allocated VPIN).
|
||||
// Volume may also be set by writing an analogue value to the second pin for the player,
|
||||
// e.g. SERVO(10001,30,0) sets volume to maximum (30).
|
||||
// The EX-RAIL script may check for completion of play by calling WAITFOR(pin), which will only proceed to the
|
||||
// following line when the player is no longer busy.
|
||||
// E.g.
|
||||
// SEQUENCE(1)
|
||||
// AT(164) // Wait for sensor attached to pin 164 to activate
|
||||
// SET(10003) // Play fourth MP3 file
|
||||
// LCD(4, "Playing") // Display message on LCD/OLED
|
||||
// WAITFOR(10003) // Wait for playing to finish
|
||||
// LCD(4, " ") // Clear LCD/OLED line
|
||||
// FOLLOW(1) // Go back to start
|
||||
|
||||
// DFPlayer::create(10000, 10, Serial1);
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -12,9 +12,14 @@
|
||||
default_envs =
|
||||
mega2560
|
||||
uno
|
||||
mega328
|
||||
unowifiR2
|
||||
nano
|
||||
src_dir = .
|
||||
include_dir = .
|
||||
|
||||
[env]
|
||||
build_flags = -Wall -Wextra
|
||||
|
||||
[env:samd21]
|
||||
platform = atmelsam
|
||||
@ -27,6 +32,42 @@ lib_deps =
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
|
||||
[env:mega2560-debug]
|
||||
platform = atmelavr
|
||||
board = megaatmega2560
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
build_flags = -DDIAG_IO -DDIAG_LOOPTIMES
|
||||
|
||||
[env:mega2560-no-HAL]
|
||||
platform = atmelavr
|
||||
board = megaatmega2560
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
build_flags = -DIO_NO_HAL
|
||||
|
||||
[env:mega2560-I2C-wire]
|
||||
platform = atmelavr
|
||||
board = megaatmega2560
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
build_flags = -DI2C_USE_WIRE
|
||||
|
||||
[env:mega2560]
|
||||
platform = atmelavr
|
||||
board = megaatmega2560
|
||||
@ -59,7 +100,20 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"g
|
||||
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
|
||||
|
||||
[env:nanoevery]
|
||||
platform = atmelmegaavr
|
||||
board = nano_every
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
arduino-libraries/Ethernet
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
upload_speed = 19200
|
||||
build_flags = -DDIAG_IO
|
||||
|
||||
[env:uno]
|
||||
platform = atmelavr
|
||||
@ -71,3 +125,13 @@ lib_deps =
|
||||
SPI
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
|
||||
[env:nano]
|
||||
platform = atmelavr
|
||||
board = nanoatmega328new
|
||||
board_upload.maximum_size = 32256
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
monitor_speed = 115200
|
||||
monitor_flags = --echo
|
||||
|
29
version.h
29
version.h
@ -3,8 +3,33 @@
|
||||
|
||||
#include "StringFormatter.h"
|
||||
|
||||
|
||||
#define VERSION "3.1.6"
|
||||
#define VERSION "3.2.0 rc13"
|
||||
// 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).
|
||||
// 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.
|
||||
// Increased use of display for showing loco decoder programming information.
|
||||
// Can disable EEPROM code
|
||||
// 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
|
||||
// Compiles on Nano Every
|
||||
// Bugfix: disgnostic display of ack pulses >32kus
|
||||
// Bugfix: Current read from wrong ADC during interrupt
|
||||
// ...
|
||||
// 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
|
||||
// 3.1.4 Refactor OLED and LCD drivers and remove unused code
|
||||
|
Loading…
Reference in New Issue
Block a user