diff --git a/DCCpp_Uno/Config.h b/DCCpp_Uno/Config.h index 126f909..e75875e 100644 --- a/DCCpp_Uno/Config.h +++ b/DCCpp_Uno/Config.h @@ -14,7 +14,11 @@ Part of DCC++ BASE STATION for the Arduino // 0 = ARDUINO MOTOR SHIELD (MAX 18V/2A PER CHANNEL) // 1 = POLOLU MC33926 MOTOR SHIELD (MAX 28V/3A PER CHANNEL) -#define MOTOR_SHIELD_TYPE 0 +#define MOTOR_SHIELD_TYPE 1 + +// SET THIS TO 1 IF THE MOTOR SHIELD HAS CURRENT FEEDBACK +// SET THIS TO 0 IF THE MOTOR SHIELD DOES NOT HAVE CURRENT FEEDBACK +#define MOTOR_SHIELD_SUPPORTS_FEEDBACK 0 ///////////////////////////////////////////////////////////////////////////////////// // @@ -39,6 +43,7 @@ Part of DCC++ BASE STATION for the Arduino // //#define IP_ADDRESS { 192, 168, 1, 200 } +//#define IP_ADDRESS { 192, 168, 0, 42 } ///////////////////////////////////////////////////////////////////////////////////// // @@ -55,4 +60,25 @@ Part of DCC++ BASE STATION for the Arduino #define MAC_ADDRESS { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEF } ///////////////////////////////////////////////////////////////////////////////////// +// +// ENABLE THE WITHROTTLE INTERFACE +// + +#define WITHROTTLE_SUPPORT 1 + +///////////////////////////////////////////////////////////////////////////////////// +// +// ENABLE BONJOUR/ZEROCONF SUPPORT +// + +#define BONJOUR 0 + +///////////////////////////////////////////////////////////////////////////////////// +// +// ENABLE THE LCD THROTTLE. REQUIRES AN LCD WITH 5 BUTTONS. +// + +#define LCD_THROTTLE 0 + +///////////////////////////////////////////////////////////////////////////////////// diff --git a/DCCpp_Uno/DCCpp_Uno.ino b/DCCpp_Uno/DCCpp_Uno.ino index 9378374..c4321de 100644 --- a/DCCpp_Uno/DCCpp_Uno.ino +++ b/DCCpp_Uno/DCCpp_Uno.ino @@ -177,6 +177,12 @@ DCC++ BASE STATION is configured through the Config.h file that contains all use #include "EEStore.h" #include "Config.h" #include "Comm.h" +#if (LCD_THROTTLE == 1) +#include "LCDThrottle.h" +#endif +#if ((COMM_TYPE == 1) && (BONJOUR == 1)) +#include +#endif void showConfiguration(); @@ -185,6 +191,9 @@ void showConfiguration(); #if COMM_TYPE == 1 byte mac[] = MAC_ADDRESS; // Create MAC address (to be used for DHCP when initializing server) EthernetServer INTERFACE(ETHERNET_PORT); // Create and instance of an EnternetServer +#if (BONJOUR == 1) + const char bonjourname = "DCCpp._withrottle._tcp"; +#endif #endif // NEXT DECLARE GLOBAL OBJECTS TO PROCESS AND STORE DCC PACKETS AND MONITOR TRACK CURRENTS. @@ -196,11 +205,23 @@ volatile RegisterList progRegs(2); // create a shorter list CurrentMonitor mainMonitor(CURRENT_MONITOR_PIN_MAIN,""); // create monitor for current on Main Track CurrentMonitor progMonitor(CURRENT_MONITOR_PIN_PROG,""); // create monitor for current on Program Track +#if (LCD_THROTTLE == 1) +LCDThrottle *lcdThrottle; +#endif + /////////////////////////////////////////////////////////////////////////////// // MAIN ARDUINO LOOP /////////////////////////////////////////////////////////////////////////////// void loop(){ + +#if (LCD_THROTTLE == 1) + lcdThrottle->run(); +#endif + +#if ((COMM_TYPE == 1 && BONJOUR == 1)) + EthernetBonjour.run(); +#endif SerialCommand::process(); // check for, and process, and new serial commands @@ -210,7 +231,7 @@ void loop(){ } Sensor::check(); // check sensors for activate/de-activate - + } // loop /////////////////////////////////////////////////////////////////////////////// @@ -222,6 +243,11 @@ void setup(){ Serial.begin(115200); // configure serial interface Serial.flush(); + #if (LCD_THROTTLE == 1) + lcdThrottle = new LCDThrottle(); + lcdThrottle->begin(1); + #endif + #ifdef SDCARD_CS pinMode(SDCARD_CS,OUTPUT); digitalWrite(SDCARD_CS,HIGH); // Deselect the SD card @@ -253,6 +279,11 @@ void setup(){ Ethernet.begin(mac); // Start networking using DHCP to get an IP Address #endif INTERFACE.begin(); + + #if (BONJOUR == 1) + EthernetBonjour.begin("arduino"); + EthernetBonjour.addServiceRecord("DCCpp._withrottle", ETHERNET_PORT, MDNSServiceTCP, "jmri=4.5.7"); + #endif #endif SerialCommand::init(&mainRegs, &progRegs, &mainMonitor); // create structure to read and parse commands from serial line diff --git a/DCCpp_Uno/Inglenook.cpp b/DCCpp_Uno/Inglenook.cpp new file mode 100644 index 0000000..9429ff3 --- /dev/null +++ b/DCCpp_Uno/Inglenook.cpp @@ -0,0 +1,109 @@ +#include +#include "LCD.h" +//#include + +#include "Inglenook.h" + +// Game State Machine States +#define STATE_IDLE 0 +#define STATE_MENUS 1 +#define STATE_ACTION 2 +#define STATE_BUILD 3 +int game_state = STATE_MENUS; + +// Array holding the current sorting of the train to be built. +int train[TRAIN_LENGTH]; + +InglenookGame::InglenookGame() { + ; // Do nothing +} + +static void InglenookGame::begin() { + randomSeed(analogRead(44)); + car_index = -1; +} + +/* +void InglenookGame::printWelcome(LCD *lcd, char *row1, char *row2) { + lcd->clear(); + sprintf(row1, "INGLENOOK GAME"); + sprintf(row2, "Select to start"); + lcd->updateDisplay(row1, row2); // TODO: Fix this. +} +*/ + +int InglenookGame::carIndex() { + return(car_index); +} + +void InglenookGame::setCarIndex(int c) { + car_index = c; +} + +void InglenookGame::buildTrain() { + bool cars_used[NUM_CARS] = { false, false, false, false, false, false, false, false }; + int car = 0; + bool found_one = false; + game_state = STATE_BUILD; + for (int i = 0; i < TRAIN_LENGTH; i++) { + found_one = false; + do { + car = random(0, NUM_CARS) & 0xFFFF; + //Serial.println("found " + String(car)); + if (cars_used[car] == false) { + // Car is not used. Use it. + cars_used[car] = true; + train[i] = car; + found_one = true; + //Serial.println("using " + String(car)); + } // if + } while (!found_one); + //Serial.println("i = " + String(i) + " car = " + String(car)); + } // for +} + +void InglenookGame::doDisplayTrain(LCD *lcd, char *row1, char *row2) { + // DEBUG: + //Serial.println("BUILD THIS TRAIN"); + //for (int i = 0; i < TRAIN_LENGTH; i++) { + //Serial.print(String(i+1) + ": " + carnames[train[i]] + "; "); + //} + //Serial.println(""); + // END DEBUG + lcd->clear(); + sprintf(row1, "BUILD THIS TRAIN"); + sprintf(row2, ""); + for (int i = 0; i < TRAIN_LENGTH; i++) { + sprintf(row2, "%s%d ", row2, train[i]+1); + } + lcd->updateDisplay(row1, row2); + lcd->noBlink(); +} + +void InglenookGame::doListTrain(LCD *lcd, char *row1, char *row2, int car) { + String s1, s2; + switch(car) { + case 1: + s1 = "2:" + carnames[train[1]]; + s2 = "3:" + carnames[train[2]]; + break; + case 2: + s1 = "3:" + carnames[train[2]]; + s2 = "4:" + carnames[train[3]]; + break; + case 3: + s1 = "4:" + carnames[train[3]]; + s2 = "5:" + carnames[train[4]]; + break; + case 0: + default: + s1 = "1:" + carnames[train[0]]; + s2 = "2:" + carnames[train[1]]; + break; + } // switch(car) + lcd->clear(); + sprintf(row1, s1.c_str()); + sprintf(row2, s2.c_str()); + lcd->updateDisplay(row1, row2); +} + diff --git a/DCCpp_Uno/Inglenook.h b/DCCpp_Uno/Inglenook.h new file mode 100644 index 0000000..04f2c7d --- /dev/null +++ b/DCCpp_Uno/Inglenook.h @@ -0,0 +1,40 @@ +#ifndef INGLENOOK_H +#define INGLENOOK_H + +// NOTE: These are actually set by the rules of the game... +// WARNING: There are several places (initialization, etc.) where +// the value of these is assumed fixed at 5 and 8. +#define TRAIN_LENGTH 5 +#define NUM_CARS 8 + +const String carnames[NUM_CARS] = { // String names of cars on the layout (for display) + "Blue Boxcar", + "Red Boxcar", + "Green Boxcar", + "Black Tank Car", + "Black Gondola", + "Grey Hopper", + "Brown Flatcar", + "Brown Boxcar" +}; + +class InglenookGame { + private: + int car_index; + + public: + InglenookGame(); + void begin(); + void buildTrain(void); + void doDisplayTrain(LCD *lcd, char *row1, char *row2); + int carIndex(); + void setCarIndex(int c); + void doMenuDisplay(LCD *lcd, char *row1, char *row2); + void doListTrain(LCD *lcd, char *row1, char *row2, int car); + + protected: + //void printWelcome(LCD *lcd, char *row1, char *row2); +}; + + +#endif diff --git a/DCCpp_Uno/LCD.cpp b/DCCpp_Uno/LCD.cpp new file mode 100644 index 0000000..12ab9de --- /dev/null +++ b/DCCpp_Uno/LCD.cpp @@ -0,0 +1,195 @@ +#include +#include + +// Include our own header with definitions and such. +#include "LCD.h" + +// Include the necessary libraries for the different display shields. +#if (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_OSEPP) +#include +#include +#elif (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_ADAFRUIT) +#include +#include // is this necessary? +#else +#error CANNOT COMPILE -- INVALID LCD LIBRARY SELECTED +#endif + +// Create the local object of the display we are +// controlling. +#if (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_OSEPP) +static LCDKeypad lcd = LCDKeypad(); +#elif (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_ADAFRUIT) +static Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield(); +#endif + +// Define the size of the display. Pretty much assumes +// 16x2 but could be modded to change that. +#define LCD_NUM_ROWS 2 +#define LCD_NUM_COLS 16 + +// State machine for handling processing of the buttons. +#define DISPLAY_STATE_RUN 0 +#define DISPLAY_STATE_DEBOUNCE 1 +#define DISPLAY_STATE_DEBOUNCE_COMPLETE 2 +#define DISPLAY_STATE_LONG_DEBOUNCE 3 +#define DISPLAY_STATE_LONG_DEBOUNCE_COMPLETE 4 +#define DISPLAY_STATE_LONG_DEBOUNCE_WAIT 5 + +// ctor +LCD::LCD() { + // Nothing to do ... yet ... +} + +// Setup function. Call from setup() +void LCD::begin() { + buttonVal = KEYS_NONE; + displayState = DISPLAY_STATE_RUN; + lcd.begin(LCD_NUM_COLS, LCD_NUM_ROWS); +} + +// Run loop for processing the buttons. Call from loop() +void LCD::run() { + debounceButtons(); +} + +//------------------------------------------------ +// Pass-through methods for handling the display +void LCD::clear() { + lcd.clear(); +} + +void LCD::setCursor(int c, int r) { + lcd.setCursor(c, r); +} + +void LCD::blink() { + lcd.blink(); +} + +void LCD::noBlink() { + lcd.noBlink(); +} + +void LCD::cursor() { + lcd.cursor(); +} + +void LCD::noCursor() { + lcd.noCursor(); +} + +//------------------------------------------------ +// Custom methods + +// Update the two lines of the display with these two strings. +void LCD::updateDisplay(char *row1, char *row2) { + lcd.clear(); + lcd.setCursor(0,0); lcd.print(row1); + lcd.setCursor(0,1); lcd.print(row2); +} + + +// Get the current button value. +int LCD::getButtons() { + int bv = buttonVal; + buttonVal = KEYS_NONE; + return(bv); +} + +//------------------------------------------------ +// Private / Protected internal methods + +// Retrieve the hardware button value and translate +// it into one of our "standard" button values. +int LCD::getButton() { +#if (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_OSEPP) + // OSEPP LCDKeypad + int b = lcd.button(); + //Serial.print(String(b) + " "); + return(b); +#elif (LCD_DISPLAY_TYPE == LCD_DISPLAY_TYPE_ADAFRUIT) + // ADAFRUIT RGBLCD + uint8_t buttons = lcd.readButtons(); + if (buttons & BUTTON_UP) { + return(KEYS_UP); + } + if (buttons & BUTTON_DOWN) { + return(KEYS_DOWN); + } + if (buttons & BUTTON_LEFT) { + return(KEYS_LEFT); + } + if (buttons & BUTTON_RIGHT) { + return(KEYS_RIGHT); + } + if (buttons & BUTTON_SELECT) { + return(KEYS_SELECT); + } + return(KEYS_NONE); +#endif +} + +// Debounce the button press and figure out if it's a long press. +void LCD::debounceButtons() { + int button = getButton(); + static long startDebounce; + static int keyval; + int retv; + switch(displayState) { + case DISPLAY_STATE_RUN: + if (button != KEYS_NONE) { + Serial.println("Raw Key: " + String(button)); + startDebounce = millis(); + keyval = button; + displayState = DISPLAY_STATE_DEBOUNCE; + } + // Always return KEYS_NONE from this state + break; + + case DISPLAY_STATE_DEBOUNCE: + // actively debouncing... + if ((button != KEYS_NONE) && ((millis() - startDebounce) > LONG_PRESS_MS)) { + // Longer than 2 second hold + displayState = DISPLAY_STATE_DEBOUNCE_COMPLETE; + switch(keyval) { + case KEYS_RIGHT: + keyval = KEYS_LONG_RIGHT; break; + case KEYS_UP: + keyval = KEYS_LONG_UP; break; + case KEYS_DOWN: + keyval = KEYS_LONG_DOWN; break; + case KEYS_LEFT: + keyval = KEYS_LONG_LEFT; break; + case KEYS_SELECT: + keyval = KEYS_LONG_SELECT; break; + default: + break; + } + buttonVal = keyval; + keyval = KEYS_NONE; + Serial.println("2 second button press! Val = " + String(buttonVal)); + + } else if (button == KEYS_NONE) { + // Short press. + Serial.println("Short Press. Debounced Key: " + String(keyval)); + // Debounce complete. Decide if it's long or not. + displayState = DISPLAY_STATE_DEBOUNCE_COMPLETE; + buttonVal = keyval; + } else { + // Not finished debouncing yet. Do nothing. + } // KEYS_NONE + break; + + case DISPLAY_STATE_LONG_DEBOUNCE_WAIT: + case DISPLAY_STATE_DEBOUNCE_COMPLETE: + // Long press detected, waiting for release before handling more button presses + if (button == KEYS_NONE) { + // User has released button. + displayState = DISPLAY_STATE_RUN; + buttonVal = KEYS_NONE; + } + break; + + } // switch(displayState) +} diff --git a/DCCpp_Uno/LCD.h b/DCCpp_Uno/LCD.h new file mode 100644 index 0000000..edd6668 --- /dev/null +++ b/DCCpp_Uno/LCD.h @@ -0,0 +1,61 @@ +#ifndef LCD_H +#define LCD_H + +//------------------------------------------------ +// Generic wrapper class for various different +// 16x2 LCD shields with buttons.. Namely: +// * the 4/8-pin OSEPP model +// * the Adafruit I2C version +// +// Allows for long vs. short presses of the buttons +// (but not momentary action -- yet) + +// LCD Display Types: +// +// 0 = OSEPP LCDKeypad +// 1 = Adafruit RGB LCD +#define LCD_DISPLAY_TYPE_OSEPP 0 +#define LCD_DISPLAY_TYPE_ADAFRUIT 1 +#define LCD_DISPLAY_TYPE LCD_DISPLAY_TYPE_ADAFRUIT + +// Defines how long a "long press" is in milliseconds. +#define LONG_PRESS_MS 2000 + +// Define generic button names for multi-library compatibility +// Return values from getButtons(); +#define KEYS_NONE -1 +#define KEYS_RIGHT 0 +#define KEYS_UP 1 +#define KEYS_DOWN 2 +#define KEYS_LEFT 3 +#define KEYS_SELECT 4 +#define KEYS_LONG_RIGHT 128 +#define KEYS_LONG_UP 129 +#define KEYS_LONG_DOWN 130 +#define KEYS_LONG_LEFT 131 +#define KEYS_LONG_SELECT 132 + + +class LCD { + private: + byte displayState; + int buttonVal; + public: + LCD(); + void begin(); // Call from setup() + void run(); // Call from loop() + int getButtons(); + void updateDisplay(char *row1, char *row2); + void clear(); + void setCursor(int c, int r); + void blink(); + void noBlink(); + void cursor(); + void noCursor(); + protected: + int getButton(); + void debounceButtons(); + +}; + +#endif // LCD_H diff --git a/DCCpp_Uno/LCDThrottle.cpp b/DCCpp_Uno/LCDThrottle.cpp new file mode 100644 index 0000000..7bd98f0 --- /dev/null +++ b/DCCpp_Uno/LCDThrottle.cpp @@ -0,0 +1,815 @@ +#include +#include +#include +#include "SerialCommand.h" +#include "EEStore.h" +#include "LCDThrottle.h" +#include "Inglenook.h" + +//-------------------------------------------------------------------- +/* LCD Throttle + * + * Uses an LCD with buttons to provide a directly-connected throttle + * interface for DCC++. Includes an "Inglenook Sidings" game that will + * randomize a set of cars and tell you a 5-car train to build. + * + * Boots up in the Menu state. Menu options include: + * -- Use Throttle + * -- Set Track Power On/Off + * -- Set Address + * -- Set Display Mode + * -- Set maximum speed (in 128 speed steps) + * -- Play Inglenook Sidings + * + * A long (2-sec+) press on SELECT will toggle between Menu and Throttle view. + * + * Throttle views (Display Modes): + * -- Standard: Separate Direction and Speed indicators + * -- Switcher: Combined bidirectional Direction+Speed indicator + * + * Each click of LEFT/RIGHT will increase/decrease the speed by a fraction + * of the maximum speed set in the menus. The amount depends on the view mode. + * Standard view has 15 steps, so (e.g.) with max speed = 60, each click is + * 4 speed steps. + * Switcher view has +/- 7 steps, so (e.g.) with max speed = 63, each click is + * 9 speed steps. + * + * KNOWN BUGS: + * -- The "spinners" for the highest digit of the Set Address and Set Max Speed + * menu items don't work right. + * -- Some of the menu responses to navigation buttons aren't consistent + */ +//-------------------------------------------------------------------- + +#define MAX_SPEED 126 /*126 */ +#define MAX_NOTCH_NORMAL 15 +#define MAX_NOTCH_SWITCHER 7 +#define DEFAULT_CAB 3 + +#define THROTTLE_STATE_RUN 0 +#define THROTTLE_STATE_DEBOUNCE 1 +#define THROTTLE_STATE_MENUS 2 +#define THROTTLE_STATE_MENU_ACTION 3 +#define THROTTLE_STATE_GAME_MENUS 4 +#define THROTTLE_STATE_GAME_BUILD 5 + +struct LCDThrottleData LCDT_EEPROM_Store; + +// MAX_COMMAND_LENGTH is defined in DCC++ SerialCommand.h +//#define MAX_COMMAND_LENGTH 30 +// Buffer for writing DCC++ commands to the core base station code. +char command[MAX_COMMAND_LENGTH]; + +// Holder for display string construction +static char display[2][17]; + +// Menu subsystem "stuff" +static void menuUseEvent(MenuUseEvent e); +static void menuChangeEvent(MenuChangeEvent e); +static MenuBackend *menu; + +/** Constructor + * + */ +LCDThrottle::LCDThrottle() { + ; // do nothing +} + + +void LCDThrottle::begin(int reg) { + lcd = new LCD(); + lcd->begin(); + //EEPROM_GetAll(); + load(); + if (maxSpeed == 0) { + // EEPROM not yet initialized -> set default. + maxSpeed = MAX_SPEED; + } + if (cab == 0) { + // EEPROM not yet initialized -> set default. + cab = DEFAULT_CAB; + } + throttleState = THROTTLE_STATE_MENUS; // Always start in the menus. + jumpbackState = THROTTLE_STATE_MENUS; + this->reg = reg; // Store the register we should use. + notch = 0; // Idle the loco + speed = 0; + dir = FORWARD; + // TODO: Get the actual track power state from the base station. + power_state = false; // Assume track power is off. + sendPowerCommand(power_state); // Don't assume. Force it off. + // Fire up the Inglenook game code. + game = InglenookGame(); + game.begin(); + // Construct the menus. + menu = new MenuBackend(menuUseEvent, menuChangeEvent); + menuSetup(); + doMenuDisplay(); +} + +/** run() + * + * Main Run loop + */ +void LCDThrottle::run() { + // Run the underlying LCD stuff + lcd->run(); + + // Grab any button presses and process them + int button = lcd->getButtons(); + + switch(throttleState) { + + case THROTTLE_STATE_RUN: + switch(button) { + case KEYS_RIGHT: + // Speed up + increaseSpeed(); + sendThrottleCommand(); + updateDisplay(); + break; + + case KEYS_LEFT: + // Slow down + decreaseSpeed(); + sendThrottleCommand(); + updateDisplay(); + break; + + case KEYS_UP: + case KEYS_DOWN: + // For now, dumbly toggle direction with either up or down key. + // Maybe make this smarter or repurpose later. + // Possibly use up/down keys for Functions. + dir = (dir == FORWARD ? REVERSE : FORWARD); + sendThrottleCommand(); + updateDisplay(); + break; + + case KEYS_SELECT: + // For now, this (Short tap) is emergency stop. + speed = -1; + notch = 0; + sendThrottleCommand(); + // reset speed to Zero after sending "Emergency Stop" special value of -1 + // This won't hurt b/c the Base Station will set the loco's speed to zero as well. + speed = 0; + updateDisplay(); + break; + + case KEYS_LONG_SELECT: + // Idle the Loco and switch to Menus mode. + speed = 0; + notch = 0; + sendThrottleCommand(); + //Serial.println("JumpbackState == " + String(jumpbackState)); + // Jump back to the last non-throttle state we were in. + // This is so when playing the game you can hop back and forth + // directly between the throttle and the built train view. + throttleState = jumpbackState; + // What to display depends on which state we're returning to. + // TODO: Update the updateDisplay() method to handle this instead, if possible. + if (throttleState == THROTTLE_STATE_GAME_BUILD) { + game.doDisplayTrain(lcd, display[0], display[1]); + } else if (throttleState == THROTTLE_STATE_GAME_MENUS) { + doGameMenuDisplay(); + } else { + doMenuDisplay(); + } + break; + + default: + break; + // Do nothing. + } // switch(button) + break; + + case THROTTLE_STATE_DEBOUNCE: + // ??? + break; + + case THROTTLE_STATE_MENUS: + // Present the top-level menu. + switch(button) { + case KEYS_UP: + Serial.println("Up/Left"); + // Don't let the menu system move up to Root. + if (menu->getCurrent() == "Set Track Power") { + // do nothing. + } else { + menu->moveUp(); + } + doMenuDisplay(); + break; + + case KEYS_DOWN: + Serial.println("Down/Right"); + menu->moveDown(); + doMenuDisplay(); + break; + + case KEYS_RIGHT: + //menu->moveRight(); + doMenuDisplay(); + break; + + case KEYS_LEFT: + menu->moveLeft(); + doMenuDisplay(); + break; + + case KEYS_SELECT: + Serial.println("Select"); + throttleState = THROTTLE_STATE_MENU_ACTION; + doMenuAction(button); + break; + + case KEYS_LONG_SELECT: + // Jump to throttle mode. Save Menus as jumpback state. + Serial.println("Long Select"); + throttleState = THROTTLE_STATE_RUN; + jumpbackState = THROTTLE_STATE_MENUS; + updateDisplay(); + break; + } + break; + + case THROTTLE_STATE_MENU_ACTION: + // Handle top-level menu actions. + switch(button) { + case KEYS_SELECT: + // Do the action and return. + //EEPROM_StoreAll(); + store(); + throttleState = THROTTLE_STATE_MENUS; + doMenuDisplay(); + break; + + case KEYS_UP: + case KEYS_DOWN: + case KEYS_LEFT: + case KEYS_RIGHT: + doMenuAction(button); + break; + + case KEYS_LONG_SELECT: + // Jump back to throttle state. Don't retun here + // Return to the menu itself instead. + //EEPROM_StoreAll(); + store(); + throttleState = THROTTLE_STATE_RUN; + jumpbackState = THROTTLE_STATE_MENUS; + button = KEYS_NONE; + updateDisplay(); + break; + } + break; + + case THROTTLE_STATE_GAME_MENUS: + case THROTTLE_STATE_GAME_BUILD: + // Handle the game action. + doGameMenus(button); + break; + + default: + break; + + } // switch(throttleState) +} + +/** menuSetup() + * + * Construct the menu tree + */ +void LCDThrottle::menuSetup() { + MenuItem *miPower = new MenuItem("Set Track Power"); + MenuItem *miRun = new MenuItem("Use Throttle"); + MenuItem *miAddr = new MenuItem("Set Address"); + MenuItem *miDisp = new MenuItem("Select Display"); + MenuItem *miMax = new MenuItem("Max Speed"); + MenuItem *miGame = new MenuItem("Inglenook"); + menu->getRoot().add(*miPower); + miPower->add(*miRun); + miRun->add(*miAddr); + miAddr->add(*miDisp); + miDisp->add(*miMax); + miMax->add(*miGame); + MenuItem *miBuild = new MenuItem("Build Train"); + MenuItem *miList = new MenuItem("List Cars"); + MenuItem *miCarList[7] = { + new MenuItem("Car 0"), + new MenuItem("Car 1"), + new MenuItem("Car 2"), + new MenuItem("Car 3"), + new MenuItem("Car 4"), + new MenuItem("Car 5"), + new MenuItem("Car 6") + }; + miGame->addRight(*miBuild); + miBuild->add(*miList); + miList->addRight(*miCarList[0]); + for (int i = 0; i < NUM_CARS-2; i++) { + miCarList[i]->add(*miCarList[i+1]); + } + // Get off of the root node. + menu->moveDown(); +} + +/** doMenuDisplay() + * + * Change the display in response to the current top-level menu selection + */ +void LCDThrottle::doMenuDisplay() { + lcd->noCursor(); + + if (menu->getCurrent() == "Set Track Power") { + lcd->updateDisplay("THROTTLE MENU:", (power_state == true ? "Track Power ON" : "Track Power OFF")); + } + if (menu->getCurrent() == "Use Throttle") { + lcd->updateDisplay("THROTTLE MENU:", "Use Throttle"); + } + if (menu->getCurrent() == "Set Address") { + lcd->updateDisplay("THROTTLE MENU:", "Set Address"); + } + if (menu->getCurrent() == "Select Display") { + lcd->updateDisplay("THROTTLE MENU:", "Select Display"); + } + if (menu->getCurrent() == "Max Speed") { + lcd->updateDisplay("THROTTLE MENU:", "Max Speed Step"); + } + if (menu->getCurrent() == "Inglenook") { + lcd->updateDisplay("THROTTLE MENU:", "Inglenook Game"); + } +} + +/** doMenuAction() + * + * Take action in response to the user selecting a menu item + */ +void LCDThrottle::doMenuAction(int button) { + Serial.println("doMenuAction(" + String(button) + ")"); + + // Set Track Power: + // Selecting this menu item toggles the track power on or off. + if (menu->getCurrent() == "Set Track Power") { + power_state = !power_state; + sendPowerCommand(power_state); + doMenuDisplay(); + } + + // Use Throttle + // Selecting this jumps you to the throttle mode. + if(menu->getCurrent() == "Use Throttle") { + throttleState = THROTTLE_STATE_RUN; + jumpbackState = THROTTLE_STATE_MENUS; + updateDisplay(); + } + + // Inglenook + // Selecting this moves you into the game menus. + if (menu->getCurrent() == "Inglenook") { + throttleState = THROTTLE_STATE_GAME_MENUS; + menu->moveRight(); + doGameMenuDisplay(); + } + + // Select Display + // Selecting this allows you to change the throttle display mode + // Up/Down keys toggle the value. + // TODO: Left/Right probably should too + if (menu->getCurrent() == "Select Display") { + if (button == KEYS_UP || button == KEYS_DOWN) { + if (displayMode == DISPLAY_MODE_NORMAL) { + displayMode = DISPLAY_MODE_SWITCHER; + } else { + displayMode = DISPLAY_MODE_NORMAL; + } + } + lcd->updateDisplay("Select Display:", + displayMode == DISPLAY_MODE_NORMAL ? + "Standard" : "Switcher"); + } // Select Display + + // Set Address + // Selecting this allows you to "dial in" the loco address. + if (menu->getCurrent() == "Set Address") { + cab = calcIncValue(button, 3, cab, 9999, "%04d", "Set Address:"); + } // Set Address + + // Max Speed + // Selecting this allows you to change the maximum speed setting of the + // loco (effectively setting the speed range of the throttle "knob") + if (menu->getCurrent() == "Max Speed") { + maxSpeed = calcIncValue(button, 2, maxSpeed, 126, "%03d", "Max Speed:"); + } // Set Address + +} + +/** calcIncValue() + * + * Calculate the incremented value to show when "dialing" a settings number. + * TODO: Handling of the most significant digit is broken. + */ +int LCDThrottle::calcIncValue(int button, int maxpos, int val, int maxval, const char *fmt, const char *label) { + static int incval, incpos; + if (button == KEYS_SELECT) { + incval = 1; + incpos = maxpos; + + } else if (button == KEYS_UP) { + if (val + incval > 9999) { + // This will roll over the value. + // For now, don't do anything. + // TODO: Figure out how to roll ONLY the correct digit to zero. + } + else if (val == 9999) { val = 0; } + else { val += incval; } + + } else if (button == KEYS_DOWN) { + if (val - incval < 0) { + // This will roll under the value. + // For now, don't do anything. + // TODO: Figure out how to roll ONLY the correct digit to 9. + } + else if (val == 0) { val = maxval; } + else { val -= incval; } + } else if (button == KEYS_LEFT) { + if (incval < pow(10, maxpos)) { incval *= 10; } + if (incpos > 0) { incpos -= 1; } + } else if (button == KEYS_RIGHT) { + if (incval > 1) { incval /= 10; } + if (incpos < maxpos) { incpos += 1; } + } + Serial.println("val: " + String(val) + " incval: " + String(incval) + " incpos: " + String(incpos)); + sprintf(display[1], fmt, val); + lcd->updateDisplay(label, display[1]); + lcd->setCursor(incpos, 1); + lcd->cursor(); + return(val); +} + +/** sendPowerCommand() + * + * Send a Power on/off command to the DCC++ Base Station + */ +void LCDThrottle::sendPowerCommand(bool on) { + sprintf(command, ""); + sprintf(command, on == true ? "1" : "0"); + Serial.println("LCD Command: " + String(command)); + SerialCommand::parse(command); +} + +/** sendThrottleCommand() + * + * Send a Throttle command to the DCC++ Base Station + */ +void LCDThrottle::sendThrottleCommand() { + sprintf(command, ""); + sprintf(command, "t%d %d %d %d", reg, cab, speed, dir); + Serial.println("LCD Command: " + String(command)); + SerialCommand::parse(command); +} + +/** increaseSpeed() + * + * Increment the current speed by one "notch" + */ +void LCDThrottle::increaseSpeed() { + int tmp_notch; + if (displayMode == DISPLAY_MODE_NORMAL) { + // in DISPLAY_MODE_NORMAL, notch is 0->maxnotch. + // since this is the "increase" function we never have to worry about + // flipping the direction bit. + notch = (notch == MAX_NOTCH_NORMAL ? MAX_NOTCH_NORMAL : notch + 1); + speed = notch * (maxSpeed / MAX_NOTCH_NORMAL); + } else { + // in DISPLAY_MODE_SWITCHER the direction can change when "increasing" + // the throttle, it depends. Have to deal with absolute value. + // First, get the signed version of "notch" and increment it. + tmp_notch = (dir == REVERSE ? -notch : notch); + tmp_notch = (tmp_notch == MAX_NOTCH_SWITCHER ? MAX_NOTCH_SWITCHER : tmp_notch + 1); + // Now handle the possible sign change by setting the direction and + // storing notch = abs(tmp_notch) + if (tmp_notch >= 0) { + dir = FORWARD; + notch = tmp_notch; + } else { + dir = REVERSE; + notch = -tmp_notch; + } + speed = notch * (maxSpeed / MAX_NOTCH_SWITCHER); + } // if(displayMode) + + Serial.println("inc: N= " + String(notch) + " S=" + String(speed)); +} + +/** decreaseSpeed() + * + * Decrement the current speed by one "notch" + */ +void LCDThrottle::decreaseSpeed() { + int tmp_notch; + if (displayMode == DISPLAY_MODE_NORMAL) { + notch = (notch == 0 ? 0 : notch - 1); + speed = notch * (maxSpeed / MAX_NOTCH_NORMAL); + } else { + tmp_notch = (dir == REVERSE ? -notch : notch); + tmp_notch = (tmp_notch == -MAX_NOTCH_SWITCHER ? -MAX_NOTCH_SWITCHER : tmp_notch - 1); + if (tmp_notch < 0) { + dir = REVERSE; + notch = -tmp_notch; + } else { + dir = FORWARD; + notch = tmp_notch; + } + speed = notch * (maxSpeed / MAX_NOTCH_SWITCHER); + } + Serial.println("dec: N= " + String(notch) + " S=" + String(speed)); +} + +/** updateDisplay() + * + * Update the display when in Throttle mode + */ +void LCDThrottle::updateDisplay() { + switch(displayMode) { + case DISPLAY_MODE_SWITCHER: + // SWITCHER: Speed/Direction together + // Loco: + // <------0------> + // The blinking cursor shows the current value + lcd->clear(); + // Draw the line. + sprintf(display[0], "Loco: %04d", cab); + if (power_state == true) { + sprintf(display[1], "<------0------>"); + } else { + sprintf(display[1], "Track Power Off"); + } + lcd->updateDisplay(display[0], display[1]); + Serial.println("D0:" + String(display[0])); + Serial.println("D1:" + String(display[1])); + // Figure out where to put the cursor + if (notch == 0) { + lcd->setCursor(7,1); + } else { + int tmp_notch = notch; + if (tmp_notch == 0) { + tmp_notch += 7; + } else { + tmp_notch = (dir == FORWARD ? tmp_notch + 7 : 7 - tmp_notch); + } + Serial.println("S=" + String(speed) + " N=" + String(notch) + " T=" + String(tmp_notch)); + lcd->setCursor(tmp_notch, 1); + } + if (power_state == true) { + lcd->blink(); + } else { + lcd->noBlink(); + } + break; + + case DISPLAY_MODE_NORMAL: + default: + // NORMAL: Speed + Direction + // <-- --> + // 0--------------+ + // The length of the bar shows the speed + // The arrow shows the direction + lcd->clear(); + if (dir == REVERSE) { + sprintf(display[0], "<--- Loco: %04d", cab); + } else { + sprintf(display[0], "Loco: %04d --->", cab); + } + if (power_state == true) { + sprintf(display[1], "0 "); + if (speed > 0) { + for (int i = 0; i < notch-1; i++) { + display[1][i+1] = '-'; + } + display[1][notch] = '|'; + display[1][notch+1] = 0; + } + } else { + sprintf(display[1], "Track Power Off"); + } + lcd->updateDisplay(display[0], display[1]); + Serial.println("D0:" + String(display[0])); + Serial.println("D1:" + String(display[1])); + break; + } +} + +//--------------------------------------------------------------- +// MenuBackend support functions + +// TODO: I could probably use these effectively to clean up a bunch +// of those switch() statements above. + +/** menuChangeEvent() + * + * Callback for change events. + */ +static void menuChangeEvent(MenuChangeEvent changed) { + // Update the display to reflect the current menu state + // For now we'll use serial output. + Serial.print("Menu change "); + Serial.print(changed.from.getName()); + Serial.print(" -> "); + Serial.println(changed.to.getName()); +} + +/** menuUseEvent() + * + * Callback for "use" events + */ +static void menuUseEvent(MenuUseEvent used) { + //Serial.print("Menu use "); + //Serial.println(used.item.getName()); +} + +//--------------------------------------------------------------- +// EEPROM Interface Functions + +// Memory Locations (byte address) +// NOTE: Deprecated in favor of struct LCDThrottleData and +// DCC++ EEStore interface + +// DCC++ - compliant EEPROM access. +/** load() + * + * load sticky data from EEStore interface + */ +void LCDThrottle::load() { + EEStore::reset(); + EEPROM.get(EEStore::pointer(), displayMode); + EEStore::advance(sizeof(displayMode)); + EEPROM.get(EEStore::pointer(), cab); + EEStore::advance(sizeof(cab)); + EEPROM.get(EEStore::pointer(), maxSpeed); + EEStore::advance(sizeof(maxSpeed)); +} + +/** load() + * + * store sticky data from EEStore interface + */ +void LCDThrottle::store() { + EEStore::reset(); + EEPROM.put(EEStore::pointer(), displayMode); + EEStore::advance(sizeof(displayMode)); + EEPROM.put(EEStore::pointer(), cab); + EEStore::advance(sizeof(cab)); + EEPROM.put(EEStore::pointer(), maxSpeed); + EEStore::advance(sizeof(maxSpeed)); +} + +//--------------------------------------------------------------- +// Inglenook Game Functions + +/** doGameMenus(int button) + * + * Handle the Inglenook Game sub-menu + * + * param button: int -> current button value + */ +void LCDThrottle::doGameMenus(int button) { + switch(throttleState) { + case THROTTLE_STATE_GAME_MENUS: + switch(button) { + case KEYS_UP: + menu->moveUp(); + doGameMenuDisplay(); + break; + + case KEYS_DOWN: + menu->moveDown(); + doGameMenuDisplay(); + break; + + case KEYS_LEFT: + menu->moveLeft(); + if (menu->getCurrent() == "Inglenook") { + throttleState = THROTTLE_STATE_MENUS; + doMenuDisplay(); + } else { + doGameMenuDisplay(); + } + break; + + case KEYS_RIGHT: + menu->moveRight(); + doGameMenuDisplay(); + break; + + case KEYS_SELECT: + if (menu->getCurrent() == "List Cars") { + menu->moveRight(); // for this one "use" == "move right" + doGameMenuDisplay(); + } else if (menu->getCurrent() == "Build Train") { + throttleState = THROTTLE_STATE_GAME_BUILD; + game.buildTrain(); + game.doDisplayTrain(lcd, display[0], display[1]); + } + break; + + case KEYS_LONG_SELECT: + throttleState = THROTTLE_STATE_RUN; + jumpbackState = THROTTLE_STATE_GAME_MENUS; + updateDisplay(); + break; + } + break; + + case THROTTLE_STATE_GAME_BUILD: + switch(button) { + case KEYS_NONE: + break; + + case KEYS_LEFT: + case KEYS_SELECT: + throttleState = THROTTLE_STATE_GAME_MENUS; + doGameMenuDisplay(); + break; + + case KEYS_DOWN: + case KEYS_RIGHT: + if (game.carIndex() >= 3) { + game.setCarIndex(3); + } else { + game.setCarIndex(game.carIndex()+1); + } + Serial.println("List Train: Car index " + String(game.carIndex())); + game.doListTrain(lcd, display[0], display[1], game.carIndex()); + break; + + case KEYS_UP: + if (game.carIndex() > -1) { + game.setCarIndex(game.carIndex()-1); + } + if (game.carIndex() == -1) { + game.doDisplayTrain(lcd, display[0], display[1]); + } else { + Serial.println("List Train: Car index " + String(game.carIndex())); + game.doListTrain(lcd, display[0], display[1], game.carIndex()); + } + break; + + case KEYS_LONG_SELECT: + throttleState = THROTTLE_STATE_RUN; + jumpbackState = THROTTLE_STATE_GAME_BUILD; + updateDisplay(); + break; + + } // switch(buttons) + } // switch(state) +} + +/** doGameMenuDisplay() + * + * Handle display output for the Inglenook Game sub-menu + */ +void LCDThrottle::doGameMenuDisplay() { + lcd->clear(); + + if (menu->getCurrent() == "Build Train") { + sprintf(display[0], "Build Train"); + sprintf(display[1], "List Cars"); + } + if (menu->getCurrent() == "List Cars") { + sprintf(display[0], "List Cars"); + sprintf(display[1], ""); + } + if (menu->getCurrent() == "Car 0") { + sprintf(display[0],"%s%s", "1:",carnames[0].c_str()); + sprintf(display[1],"%s%s", "2:",carnames[1].c_str()); + } + if (menu->getCurrent() == "Car 1") { + sprintf(display[0],"%s%s", "2:",carnames[1].c_str()); + sprintf(display[1],"%s%s", "3:",carnames[2].c_str()); + } + if (menu->getCurrent() == "Car 2") { + sprintf(display[0],"%s%s", "3:",carnames[2].c_str()); + sprintf(display[1],"%s%s", "4:",carnames[3].c_str()); + } + if (menu->getCurrent() == "Car 3") { + sprintf(display[0],"%s%s", "4:",carnames[3].c_str()); + sprintf(display[1],"%s%s", "5:",carnames[4].c_str()); + } + if (menu->getCurrent() == "Car 4") { + sprintf(display[0],"%s%s", "5:",carnames[4].c_str()); + sprintf(display[1],"%s%s", "6:",carnames[5].c_str()); + } + if (menu->getCurrent() == "Car 5") { + sprintf(display[0],"%s%s", "6:",carnames[5].c_str()); + sprintf(display[1],"%s%s", "7:",carnames[6].c_str()); + } + if ((menu->getCurrent() == "Car 6") || (menu->getCurrent() == "Car 7")) { + sprintf(display[0],"%s%s", "7:",carnames[6].c_str()); + sprintf(display[1],"%s%s", "8:",carnames[7].c_str()); + } + lcd->updateDisplay(display[0], display[1]); + lcd->setCursor(0,0); + lcd->blink(); +} diff --git a/DCCpp_Uno/LCDThrottle.h b/DCCpp_Uno/LCDThrottle.h new file mode 100644 index 0000000..ebdd5d5 --- /dev/null +++ b/DCCpp_Uno/LCDThrottle.h @@ -0,0 +1,69 @@ +#ifndef LCD_THROTTLE_H +#define LCD_THROTTLE_H + +#include "LCD.h" +#include "Inglenook.h" + +// DCC++ Throttle using the buttons on a 16x2 + 5 button Display Shield + +#define DISPLAY_MODE_NORMAL 1 +#define DISPLAY_MODE_SWITCHER 2 +#define DISPLAY_MODE DISPLAY_MODE_NORMAL + +// Throttle Directions +#define FORWARD 1 +#define REVERSE 0 + +class MenuItem; + +class LCDThrottle { +private: + int throttleState; +int jumpbackState; + int reg; + int cab; + int speed; + int dir; + byte displayMode; + int notch; + LCD *lcd; + int maxSpeed; + bool power_state; + InglenookGame game; + + public: + LCDThrottle(); + void begin(int reg); + void run(); + + protected: + void sendThrottleCommand(); + void sendPowerCommand(bool on); + int debounceButtons(); + int getButton(); + void increaseSpeed(); + void decreaseSpeed(); + void updateDisplay(); + void menuSetup(); + void doMenuDisplay(); + void doMenuAction(int button); + void load(); + void store(); + void doGameMenus(int b); + void setupInglenookMenu(MenuItem *m); + void inglenookBegin(); + void doGameMenuDisplay(); + int calcIncValue(int button, int maxpos, int val, int maxval, const char *fmt, const char *label); + +}; + +struct LCDThrottleData { +byte dislay; +byte reserved1; +int address; +int maxspeed; +int reserved[5]; +}; + + +#endif // LCD_THROTTLE_H diff --git a/DCCpp_Uno/PacketRegister.cpp b/DCCpp_Uno/PacketRegister.cpp index 2d69d60..b47d2c0 100644 --- a/DCCpp_Uno/PacketRegister.cpp +++ b/DCCpp_Uno/PacketRegister.cpp @@ -322,10 +322,14 @@ void RegisterList::writeCVByte(char *s) volatile{ if(c>ACK_SAMPLE_THRESHOLD) d=1; } - - if(d==0) // verify unsuccessful +#if (MOTOR_SHIELD_SUPPORTS_FEEDBACK > 0) + if(d==0) { // verify unsuccessful bValue=-1; - + } +#else + bValue=-2; +#endif + INTERFACE.print(" 0) + if(d==0) { // verify unsuccessful bValue=-1; + } +#else + bValue=-2; +#endif INTERFACE.print(" return and only react to triggers. void Sensor::check(){ Sensor *tt; - +/* for(tt=firstSensor;tt!=NULL;tt=tt->nextSensor){ tt->signal=tt->signal*(1.0-SENSOR_DECAY)+digitalRead(tt->data.pin)*SENSOR_DECAY; @@ -81,7 +81,7 @@ void Sensor::check(){ INTERFACE.print(">"); } } // loop over all sensors - + */ } // Sensor::check /////////////////////////////////////////////////////////////////////////////// diff --git a/DCCpp_Uno/SerialCommand.cpp b/DCCpp_Uno/SerialCommand.cpp index 4d3f9b2..613b970 100644 --- a/DCCpp_Uno/SerialCommand.cpp +++ b/DCCpp_Uno/SerialCommand.cpp @@ -14,6 +14,7 @@ Part of DCC++ BASE STATION for the Arduino // See SerialCommand::parse() below for defined text commands. + #include "SerialCommand.h" #include "DCCpp_Uno.h" #include "Accessories.h" @@ -21,6 +22,9 @@ Part of DCC++ BASE STATION for the Arduino #include "Outputs.h" #include "EEStore.h" #include "Comm.h" +#ifdef WITHROTTLE_SUPPORT +#include "WiThrottle.hpp" +#endif extern int __heap_start, *__brkval; @@ -30,6 +34,7 @@ char SerialCommand::commandString[MAX_COMMAND_LENGTH+1]; volatile RegisterList *SerialCommand::mRegs; volatile RegisterList *SerialCommand::pRegs; CurrentMonitor *SerialCommand::mMonitor; +bool newConnect; /////////////////////////////////////////////////////////////////////////////// @@ -38,6 +43,7 @@ void SerialCommand::init(volatile RegisterList *_mRegs, volatile RegisterList *_ pRegs=_pRegs; mMonitor=_mMonitor; sprintf(commandString,""); + newConnect = true; } // SerialCommand:SerialCommand /////////////////////////////////////////////////////////////////////////////// @@ -48,13 +54,21 @@ void SerialCommand::process(){ #if COMM_TYPE == 0 while(INTERFACE.available()>0){ // while there is data on the serial line + if (newConnect) { + WiThrottle::sendIntroMessage(); + newConnect = false; + } c=INTERFACE.read(); - if(c=='<') // start of new command - sprintf(commandString,""); - else if(c=='>') // end of new command - parse(commandString); - else if(strlen(commandString)') + if (WiThrottle::isWTCommand(c)) { + WiThrottle::readCommand(c); + } else { + if(c=='<') // start of new command + sprintf(commandString,""); + else if(c=='>') // end of new command + parse(commandString); + else if(strlen(commandString)') + } } // while #elif COMM_TYPE == 1 @@ -62,16 +76,28 @@ void SerialCommand::process(){ EthernetClient client=INTERFACE.available(); if(client){ - while(client.connected() && client.available()){ // while there is data on the network - c=client.read(); - if(c=='<') // start of new command - sprintf(commandString,""); - else if(c=='>') // end of new command - parse(commandString); - else if(strlen(commandString)') - } // while - } +#ifdef WITHROTTLE_SUPPORT + WiThrottle::sendIntroMessage(); +#endif + while(client.connected()) { + while (client.available()) { // while there is data on the network + c=client.read(); +#ifdef WITHROTTLE_SUPPORT + if (WiThrottle::isWTCommand(c)) { + WiThrottle::readCommand(c); + } else { +#endif + // Process as a DCC++ command + if(c=='<') // start of new command + sprintf(commandString,""); + else if(c=='>') // end of new command + parse(commandString); + else if(strlen(commandString)') + } // else withrottle + } // while available + }// while connected + } // if client #endif @@ -80,6 +106,8 @@ void SerialCommand::process(){ /////////////////////////////////////////////////////////////////////////////// void SerialCommand::parse(char *com){ + + Serial.println("WCMD: " + String(com)); switch(com[0]){ diff --git a/DCCpp_Uno/WiThrottle.cpp b/DCCpp_Uno/WiThrottle.cpp new file mode 100644 index 0000000..654670a --- /dev/null +++ b/DCCpp_Uno/WiThrottle.cpp @@ -0,0 +1,538 @@ +/********************************************************************** + +WiThrottle.cpp +COPYRIGHT (c) 2017 Mark S. Underwood + +Part of DCC++ BASE STATION for the Arduino + +**********************************************************************/ + +#include +#include +#include "DCCpp_Uno.h" +#include "Comm.h" +#include "SerialCommand.h" +#include "PacketRegister.h" +#include "Outputs.h" +#include "Accessories.h" +#include "WiThrottle.hpp" + +#define FORCED_REGISTER_NUMBER 1 + +// Store decoder addresses for DCC turnouts here. These take +// priority over actual built-in outputs. One should take +// care not to have the name spaces overlap... + +#define MAX_DCC_TURNOUTS 5 +struct TurnoutData dccTurnouts[MAX_DCC_TURNOUTS] = { + { 1, 0, 1, 1 }, + { 1, 0, 2, 2 }, + { 1, 0, 3, 3 }, + { 1, 0, 4, 4 }, + { 1, 0, 5, 5 }, +}; + +extern RegisterList mainRegs; + +static char message[MAX_COMMAND_LENGTH+1]; +static char *command = SerialCommand::commandString; +static int address = 3; +static int speed = 0; +static int dir = 1; + +bool fstate[29] = { + false, false, false, false, false, + false, false, false, false, false, + false, false, false, false, false, + false, false, false, false, false, + false, false, false, false, false, + false, false, false, false +}; + + +static const byte byte1FuncOnVals[29] = { 144, 129, 130, 132, 136, + 177, 178, 180, 184, + 161, 162, 164, 168, + 222, 222, 222, 222, + 222, 222, 222, 222, + 223, 223, 223, 223, + 223, 223, 223, 223 }; + +static const byte byte1FuncOffVals[29] = { 128, 128, 128, 128, 128, + 176, 176, 176, 176, + 160, 160, 160, 160, + 222, 222, 222, 222, + 222, 222, 222, 222, + 223, 223, 223, 223, + 223, 223, 223, 223 }; + +static const byte byte2FuncOnVals[29] = { 1, 2, 4, 8, + 16, 32, 64, 128, + 1, 2, 4, 8, + 16, 32, 64, 128 }; + + +static void WiThrottle::readCommand(char c) { + char x; + sprintf(command, "%c", c); + +#if COMM_TYPE == 0 + + // Read all the bytes until we encounter a newline, end-of-string, or + // run out of bytes. + while(INTERFACE.available() > 0) { + x = INTERFACE.read(); + Serial.print(x); + if (x == '\n' || x == '\0') { + sprintf(command,"%s%c",command, x); + Serial.println("Received"); + parseToDCCpp(command); + } else if (strlen(command) < MAX_COMMAND_LENGTH) { + sprintf(command, "%s%c", command, x); + } + } + +#elif COMM_TYPE == 1 + + // Connect to the source + EthernetClient client = INTERFACE.available(); + + // Read all the bytes until we encounter a newline, end-of-string, or + // run out of bytes. + if (client) { + while (client.connected() && client.available()) { + x = client.read(); + if (x == '\n' || x == '\0') { + sprintf(command,"%s%c",command, x); + parseToDCCpp(command); + } else if (strlen(command) < MAX_COMMAND_LENGTH) { + sprintf(command, "%s%c", command, x); + } + } + } +#endif +} + +static void WiThrottle::parseToDCCpp(char *s) { + // Do something :) + Serial.print("RX: "); + Serial.println(s); + switch (s[0]) { + case 'T': // Throttle + case 'S': // Second Throttle + doThrottleCommand(NULL, s+1); + break; + case 'M': // Multi-Throttle + parseMCommand(s); + break; + case 'C': // Old "T" command - not used anymore. Kept for backward compatibility + if (s[1] == 'T') { + doThrottleCommand(NULL, s+2); + } + break; + case 'N': // Name of throttle + parseNCommand(s); + break; + case 'H': // Hardware (get device UUID) + parseHCommand(s); + break; + case 'P': // Panel stuff + parsePCommand(s); + break; + case '*': // Heartbeat + case 'D': // Direct hex packet + case 'R': // Roster stuff + case 'Q': // Quit + break; + default: + return(s); + } +} + +static bool WiThrottle::isWTCommand(char c) { + switch (c) { + case 'T': // Throttle + case 'S': // Second Throttle + case 'M': // Multi-Throttle + case 'D': // Direct hex packet + case '*': // Heartbeat + case 'C': // Old 'T' command + case 'N': // Name + case 'H': // Hardware + case 'P': // Panel + case 'R': // Roster + case 'Q': // Quit + //Serial.print(c); + //Serial.println(" is a WiThrottle cmd"); + return(true); + default: + return(false); + } +} + +static void WiThrottle::parsePCommand(char *p) { + // Commands: + // PPAx : Track power on/off + // PTAx : Turnout throw/close + // PRAx : Route set/unset + switch(p[1]) { + case 'P': + // Track power on or off + switch(p[2]) { + case 'A': + if (p[3] == '1') { + // Track power ON + Serial.println("Power ON"); + sprintf(command, "1"); + SerialCommand::parse(command); + INTERFACE.println("PPA1"); + } else if (p[3] == '0') { + // Track power OFF + Serial.println("Power OFF"); + sprintf(command, "0"); + SerialCommand::parse(command); + INTERFACE.println("PPA0"); + } // p[3] + // Else ignore. + break; + } // p[2] + break; + + case 'T': + // Turnout command + // C = close T = throw 2 = toggle + if (p[2] == 'A') { + handleTurnout(p+3); + } + break; + } // switch(p[1]) +} + +static void WiThrottle::parseHCommand(char *s) { + switch(s[1]) { + case 'U': + // get device UDID and do something with it. + Serial.println("RX Device ID: " + String(s+2)); + return; + default: + return; + } +} + +static void WiThrottle::parseNCommand(char *s) { + // Get the Name and store it somewhere... if needed. + Serial.print("Name = "); + Serial.println(String(s)); + // Reply * e.g. *10 for 10 seconds or *0 for no heartbeat expected + doPrintln("*0"); + return; +} + +static void WiThrottle::parseMCommand(char *s) { + char *key, *action; + switch(s[2]) { + case 'A': + case '+': + key = strtok(s, "<;>"); + action = strtok(NULL, "<;>"); + Serial.println("Key = " + String(key)); + Serial.println("Action = " + String(action)); + doThrottleCommand(key, action); + if (s[2] == '+') { + doPrint(key); + doPrint("<;>"); + doPrintln(action); + } + break; + case '-': + default: + return; + } +} + +static void WiThrottle::doThrottleCommand(char *key, char *action) { + int reg, spd, f; + byte byte1, byte2; + address = strtol(key+4, NULL, 10); + // TODO: When supporting multiple throttles, KEY will tell us which + // throttle to do the action on. + switch(action[0]) { + case 'V': // Velocity + // DCC++ Format: + // DCC++ Returns: + if (address < 0) { return; } + reg = getRegisterForCab(address); + //dir = getDirForCab(address); + sprintf(command, "t %d %d %s %d", reg, address, (action+1), dir ); + sscanf(action+1, "%d", &spd); + //speed = strtol((action+1), NULL, 10); + Serial.print("new speed = "); + Serial.print(action+1); + Serial.print(" dir = "); + Serial.println(dir); + SerialCommand::parse(command); + break; + + case 'X': // E-Stop + // DCC++ Format: with SPEED = -1 + // DCC++ Returns: + if (address < 0) { return; } + reg = getRegisterForCab(address); + dir = getDirForCab(address); + sprintf(command, "t %d %d -1 %d", reg, address, (action+1), dir); + SerialCommand::parse(command); + break; + + case 'F': // Function + case 'f': // force function (v>=2.0) + // WiThrottle Format: FxVV + // x = 1 (on) or 0 (off) + // VV is the function number + // DCC++ Format: + // DCC++ Returns: (none) + if (key[0] == 'M') { + address = strtol(key+4, NULL, 10); + } else { + ; // previous versions of the interface (current EngineDriver) + // send the older TF command, not the M command. In this case + // there is no address to be parsed in the command. + // Keep whatever address is stored from previous commands + } + f = strtol((action+2), NULL,10); + Serial.println("addr = " + String(address) + " F = " + String(f) + " is " + String(action[1] == '1' ? "ON" : "OFF")); + if (f < 0 || f > 28) { + // Invalid conversion + break; + } + // NOTE: strtol() returns zero on an invalid conversion + // That is harmless here. F0 is the headlight, so the worst + // thing that will happen on an invalid conversion is we tooggle + // the headlight. Oh well. + if ((action[1] == '1') || (f == 2)) { + // Button Pressed.. Take action + // Horn (F2) is momentary. Take action even if action[1] == 0 + // Toggle the state. + fstate[f] = !fstate[f]; + // Get the bytes to send. + byte1 = getFuncByte1(fstate[f], f); + byte2 = getFuncByte2(fstate[f], f); + // Build the DCC++ message and send it + if (byte2 == 255) { + sprintf(message, "f %d %d", address, byte1); + } else { + sprintf(message, "f %d %d %d", address, byte1, byte2); + } + SerialCommand::parse(message); + // Send the response to the WiThrottle + // Needs to be tested for EngineDriver + if (key[0] == 'M') { + doPrint(key); + doPrint("<;>"); + } else { + // Old "T" support + doPrint("T"); + } + action[1] = (fstate[f] == true ? '1' : '0'); + doPrintln(action); + } + break; + + case 'R': // Direction + // DCC++ Format: + // DCC++ Returns: + if (address < 0) { return; } + reg = getRegisterForCab(address); + spd = getSpeedForCab(address); + if (action[1] == '0') { + dir = 0; + } else { + dir = 1; + } + sprintf(command, "t %d %d %d %d", reg, address, spd, dir); + Serial.println("cmd = " + String(command)); + Serial.println("dir = " + String(dir)); + SerialCommand::parse(command); + break; + + case 'I': // Idle + // DCC++ Format: + // DCC++ Returns: + if (address < 0) { return; } + reg = getRegisterForCab(address); + dir = getDirForCab(address); + sprintf(command, "t %d %d 0 %d", reg, address, dir); + SerialCommand::parse(command); + break; + + case 'r': // Release + case 'd': // Dispatch + address = 0; + break; + + case 'L': // set long address + case 'S': // set short address + address = strtol((action+1), NULL, 10); + doPrintln("MT+L" + String(address) + "<;>"); + // Formulate a response + if (key[0] == 'M') { + doPrint(key); + doPrintln("<;>"); + } else { + // Old "T" or "S" support + // Needs to be tested + doPrintln(key); + } + break; + + case 'q': // request (v>=2.0) + handleRequest(action); + break; + + case 'Q': // quit + case 'E': // set address from roster (v>=1.7) + case 'C': // consist + case 'c': // consist lead from roster (v>=1.7) + case 's': // speed step mode (v>= 2.0) + case 'm': // momentary (v>=2.0) + default: + return; + } +} + +byte WiThrottle::getFuncByte1(bool t, int f) { + if (t) { + return(byte1FuncOnVals[f]); + } else { + return(byte1FuncOffVals[f]); + } +} + +byte WiThrottle::getFuncByte2(bool t, int f) { + if (f < 13) { + return(255); + } else if (t) { + return(byte2FuncOnVals[f-13]); + } else { + return(0); + } +} + +void WiThrottle::handleRequest(char *s) { + // TODO: Handle Requests + return; +} + +int WiThrottle::getRegisterForCab(int c) { + return(FORCED_REGISTER_NUMBER); +} + +int WiThrottle::getSpeedForCab(int c) { + //int spd = speed; + int spd = mainRegs.speedTable[FORCED_REGISTER_NUMBER]; + if (spd < 0) { spd = -spd; } + return(spd); +} + +int WiThrottle::getDirForCab(int c) { + // In the speedTable, reverse speeds are negative. + // See PacketRegister::setThrottle() + //int spd = 0; + // Have to special-handle the speed = 0 case + // since there is no implied direction. + + int spd = mainRegs.speedTable[FORCED_REGISTER_NUMBER]; + if (spd == 0) { + return(dir); + } else { + dir = (spd > 0 ? 1 : 0); + return(dir); + } +} + +void WiThrottle::sendIntroMessage(void) { + // Send version number of protocol supported + doPrintln("VN2.0"); + // Send the roster (no roster entries, so 0) + doPrintln("RL0"); + // Send Power Status + // check pin SIGNAL_ENABLE_PIN_MAIN + // PPA0=off PPA1=on PPA2=unknown + if (digitalRead(SIGNAL_ENABLE_PIN_MAIN) == HIGH) { + doPrintln("PPA1"); + } else { + doPrintln("PPA0"); + } + // Send any defined consists. + doPrintln("RCC0"); // 0 Consists defined. + // Send turnout info ... + doPrintln("PTT]\[Turnouts}|{Turnout]\[Closed}|{2]\[Thrown}|{4"); + // TODO: Send turnouts ("PTL") and Routes ("PRT") here. + // Send the port number + listTurnouts(); +#if COMM_TYPE == 1 + doPrint("PW"); + doPrintln(ETHERNET_PORT); +#endif +} + +#define TURNOUT_UNKNOWN 1 +#define TURNOUT_CLOSED 2 +#define TURNOUT_THROWN 4 + +void WiThrottle::listTurnouts(void) { + if (Output::firstOutput != NULL) { + doPrint("PTL"); + for (int i = 0; i < MAX_DCC_TURNOUTS; i++) { + if (dccTurnouts[i].id > 0) { + sprintf(command,"]\\[%d}|{%d}|{%d]", dccTurnouts[i].address, dccTurnouts[i].id, TURNOUT_UNKNOWN); + doPrint(command); + } + } + /* + Output *pt = Output::firstOutput; + while (pt != NULL) { + sprintf(command, "]\\[%d|%d|%d]", pt->data.id, pt->data.id, + (pt->data.oStatus == 0 ? TURNOUT_CLOSED : TURNOUT_THROWN)); + INTERFACE.print(command); + pt = pt->nextOutput; + } // while + */ + doPrintln("]"); // Send close bracket and EOL + } // if +} + +void WiThrottle::handleTurnout(char *s) { + // Substring of PTAxxx command, starting with byte 3 + int addr = strtol(s+1, NULL, 10); + for (int i = 0; i < MAX_DCC_TURNOUTS; i++) { + if (dccTurnouts[i].address == addr) { + sprintf(command, "a %d 0 %d", addr, (s[0] == 'C' ? 0 : 1)); + SerialCommand::parse(command); + dccTurnouts[i].tStatus = (s[0] == 'C'? 0 : 1); + return; + } + } // end for loop. + // If we got here, then it's not a defined DCC turnout. Might be + // an Output. Handle that differently. + // err... for now don't handle it at all. +} + +void WiThrottle::doPrint(char *x) { + Serial.print(x); + INTERFACE.print(x); +} + +void WiThrottle::doPrintln(char *x) { + Serial.println(x); + INTERFACE.println(x); +} + +void WiThrottle::doPrint(StringSumHelper& x) { + Serial.print(x); + INTERFACE.print(x); +} + +void WiThrottle::doPrintln(StringSumHelper& x) { + Serial.println(x); + INTERFACE.println(x); +} diff --git a/DCCpp_Uno/WiThrottle.hpp b/DCCpp_Uno/WiThrottle.hpp new file mode 100644 index 0000000..5a94dfe --- /dev/null +++ b/DCCpp_Uno/WiThrottle.hpp @@ -0,0 +1,44 @@ +/********************************************************************** + +WiThrottle.h +COPYRIGHT (c) 2017 Mark S. Underwood + +Part of DCC++ BASE STATION for the Arduino + +**********************************************************************/ + +#ifndef WITHROTTLE_H +#define WITHROTTLE_H + + +class WiThrottle { +private: + //static char command[MAX_COMMAND_LENGTH+1]; + //static int address; // someday will be an array? + +protected: + static void parseHCommand(char *s); + static void parseNCommand(char *s); + static void parseMCommand(char *s); + static void parsePCommand(char *s); + static void doThrottleCommand(char *key, char *action); + static byte getFuncByte1(bool t, int f); + static byte getFuncByte2(bool t, int f); + static int getRegisterForCab(int c); + static int getSpeedForCab(int c); + static int getDirForCab(int c); + static void handleRequest(char *s); + static void listTurnouts(void); + static void handleTurnout(char *s); + static void doPrint(char *x); + static void doPrintln(char *x); + static void doPrint(StringSumHelper& x); + static void doPrintln(StringSumHelper& x); +public: + static void parseToDCCpp(char *s); + static bool isWTCommand(char c); + static void readCommand(char c); + static void sendIntroMessage(void); +}; + +#endif // WITHROTTLE_H