Jump to content

Building and developing a Bodnar DCC++ Handheld Throttle


Recommended Posts

In case you don't know what a Bodnar DCC++ throttle is, you can visit here:

 

http://www.trainelectronics.com/DCC_Arduino/DCC++/Throttle/index.htm

 

I chose to try to build a slightly modified Rotary Encoder & wireless version of his throttle.  This is also my first time coding Arduinos (although not working with C++, although I'm a tad rusty.)

 

Here's my progress so far (and please don't expect daily updates on this!)

Throttle.jpg.bc388b9c470c8be7bc6e9e385d1890a8.jpg

 

I had a look through the code and thought that it would be straightforward to modify the throttle to activate accessories if desired.  With this in mind, I used a 4x4 matrix keypad.  I also saw no reason why the rotary encoder couldn't be digitally debounced (even though I bought some capacitors in case I had to bail on that idea.)  The display is a 20x4

 

Dave's throttle uses an Arduino Mini, I've chosen to do the development on a Mega 2560 clone, although I have indeed purchased a Mini and the required USB adapter to flash it.

 

To get me started, I just flashed the code as was. Predictably, the rotary encoder did not work, and the implementation of the LCD display was confusing at best - so I went through the LCD commands, correcting them one by one to use the LiquidCrystal_I2C.h library and deleting the LCD.h inclusion.  I spent bit of time trying to figure out both how to digitally debounce the encoder, and how to make the interrupt coding work.  I found that I could achieve one of these, or the other, but not both.  Eventually, I gave up on the interrupt code and just called the encoder routine every time in the loop.  It works fine.

 

However, Dave's code (and if you're here Dave, I thank you very much for the start but...) is a tad untidy.  And also, some of it plain doesn't work, or at least doesn't work as it seems to have been expected to.  I've spent a bit of time tidying the code up and eliminating unused variables and code.  It seems to work OK.

 

Fix list so far:

  • The code uses keys 1 to 5 to flip bits 0 to 4 of the first DCC function set.  I think the fifth bit is actually the combination bit for directional headlamps (i.e. 'FL', but this needs a bit of investigation).  Pushing the 1 through 4 keys toggles the associated F1 through F4 according the NMRA spec, but 5 flips this 'FL' bit - referred to in the DCC++ coding as F0.  The code definitely isn't quite right.  Another corollary of this is that the 6 key changes what I believe is more commonly known as 'F5' (and 7=F6, 8=F7, 9=F8.)  The code uses the 0 key to turn off all the functions (he sets all the bits for the first two function banks to zero.)  I think this needs a close look at with a well set up directional locomotive (something I don't have) in order to make the unit function like a commercial device and/or match the DCC++ intent.
  • Dave suggests the * key will 'zero the speed of the locomotives'.  Well, it sorta does.  I can see how he meant to write the code, but it doesn't actually execute the way he might have expected.  First of all, at no point would his code reset the speeds of the locomotives to zero as the global variable he uses is never set to 1, which his subroutine uses as a flag to set all the speeds to zero.  However, what does happen in the function is that the track power is turned off - effectively 'setting all the loco speeds to zero'.  He also says that cancelling the * key action (the other function of which is to set the DCC ID of the current cab) with * or # will reset the power back on.  It does not, you need to twiddle the pot to turn the power back on.  But, what happens is that all the locos throttles stored in the hardware are whatever they were before.  At the very least, if you cycled through the loco IDs, all of them would throttle up.  I can imagine that if you really wanted to stop everything, this might be 'painful'.  I will fix this to make it a bit safer!  (This routine also ran one more loop than the number of locos available, with possibly unpredictable results - fixed.)

 

Things to do:

  • Figure out how to employ the ABCD keys.
  • Code functions 9 through 12 (could be assigned to A, B, C as F10, F11, F12...)
  • Change the display to use the new real estate more effectively.
  • Code a method of sending accessory messages...
  • ... and perhaps figure a way of making them practical.
  • Maybe: Change the hotkey method for quick locomotive selection so that more than four locos can be added and remembered, and those locos selected from a menu.
  • Eliminate unnecessary code as it is discovered (some more done tonight... why light the onboard pin13 LED at all?)

 

I did buy a 3.5" coloour TFT touch screen, which worked except for the SD card reader, so its going back.  Shame...  a small layout display should be relatively easy to code for a small layout that is already set up.  Flicking to turnout mode and tapping the right turnout with the probe would have worked OK.

 

 

Edited by FoxUnpopuli
FL/F0 editing.
  • Like 1
Link to post
Share on other sites

On 03/08/2020 at 21:07, FoxUnpopuli said:

Fix list so far:

  • The code uses keys 1 to 5 to flip bits 0 to 4 of the first DCC function set.
  • Dave suggests the * key will 'zero the speed of the locomotives'.  I will fix this to make it a bit safer!

Things to do:

  • Figure out how to employ the ABCD keys.
  • Code functions 9 through 12 (could be assigned to A, B, C as F10, F11, F12...)
  • Change the display to use the new real estate more effectively.
  • Code a method of sending accessory messages...
  • ... and perhaps figure a way of making them practical.
  • Maybe: Change the hotkey method for quick locomotive selection so that more than four locos can be added and remembered, and those locos selected from a menu.
  • Eliminate unnecessary code as it is discovered (some more done tonight... why light the onboard pin13 LED at all?)

 

 

Ooookay.  :)

I fixed the quick keys.  Now 0 actually flips FL/F0, which is the fifth bit, and 1 through 4 actually set functions 1 through 4.

I used the A, B and C keys to set F10, F11 and F12.  

Then, I thought I'd use the D key to set the extended functions, 13 through 28.

In order to do this, I rearranged the display thus:

 

throttleimage2.jpeg.9a4afda5cf43d06fefcc2d06b1eaf3ff.jpeg

So here we have four locos on the left.  Using the hash key swaps between them, what you can't see is that the cursor is blinking over the direction arrow to show which loco you're controlling.  Of course, rotating the encoder will adjust the speed, loco 7897 is set to 24 here, travelling in reverse.  

 

On the right is the function deck for the loco.  You can see dots for off and blobs for on.  Functions 0 through 12 and function 20 are all on for loco 7897.  When you swap locos, it updates the display to show you what's on and off for that loco.

 

To set functions 13 through 20, you simply hit 'D', and it puts the cursor on the 'F' to show it's waiting for further input - in this case, two digits.  If you don't hit 0,1 or 2 for the first key, it aborts.  When the throttle has a number, it does one of two things... if it's less than 13 or if it's 29, it simply aborts for now.  If it's 13 through 28, then it flips the bit for the function, just like the 0-9 and A-C keys for F0 to 13.

 

Here's some debug enabled code.  It's saying it saw the D key, then 28 was received, this is then adjusted down to 7 - as F28 is the most significant bit in the byte you have to send to set it.  The bitpattern is shown, followed by the decimal, and then the DCC++ serial message for setting F21 to F28 is set.  And then...  27... etc.

20:10:23.163 -> D
20:10:26.043 -> 2815
20:10:26.043 -> 7
20:10:26.043 -> 10000000 21to28 d 128<f 7897 223 128 > 
20:10:31.403 -> D
20:10:32.963 -> 2714
20:10:32.963 -> 6
20:10:32.963 -> 11000000 21to28 d 192<f 7897 223 192 > 

I've refined the code a little more, but it's still messy and I can see many ways to improve it.

 

Edit: when I've done that and it's 'tidy enough' I'll post it.  :)

 

 

 

Edited by FoxUnpopuli
Link to post
Share on other sites

The function code was substantially rebuilt this time.  It bugged me.

 

Typing 'D29' now sets all the function bits to zero, turning off all functions.

 

If not mentioned already, I now set the speed to zero when pressing the rotary encoder to change direction.  A quick way of just zeroing speed if you 'double click'.

 

As promised, some code to ridicule attached below.  :)

 

Keys 0-9 and A-C are still hotkeys for functions F0 to F12, but now any Dxx command will set function xx.

 

* now turns the power back on to the track after you've set the new loco ID. I'm not sure about this one.  I also see that the 'power on' command gets sent every time you twiddle the thottle.  This is a waste of bandwidth so I'll address that at some point.

 

I think I'm going to reassign A-C to do something else.  Any suggestions?

 

edit: found a bug because I didn't check the F29 addition...  ddf should go round the loop one more time, i.e.  ddf < 5, not 4.

 

/*
  Mark Fox's DCC++ 'Bodnar Throttle'.  I think I might brand it a 'Bodnar'.  :)
  Rewritten and customised from Dave Bodnar's code from June 16th, 2016, his version 2.6a
  Version 1.00 uses an Arduino MEGA2560 (devboard) with a 4x4 keypad, 20x4 I2C LCD Display, and uses digital debouncing on the KY-040 rotary encoder without an interrupt.
  This version is 1.03, a heavy edit streamlining all the function comms.
  Date 6th August 2020.
*/
#include "Arduino.h"
#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

// Pin definitions
#define RE_CLK 2 // Rotary encoder on 2,3,4
#define RE_DATA 3
#define RE_Button 4

#define I2C_ADDR    0x27

int debug = 0; // set to 1 to show debug info on serial port - assume that it will cause issues with DCC++ depending on what is sent

uint8_t zerodot[8] = {0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0};
uint8_t onedot[8] = {0x0, 0xe, 0x1f, 0x1b, 0x1f, 0xe, 0x0};

// Setup Keypad variables
const byte ROWS = 4; //four rows
const byte COLS = 4; //four columns
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
// #=35; *=42; 0-9=48to57; A-D=65to68

//Check all the following are right for the 4x4 keypad onto a Nano - Foxy
byte rowPins[ROWS] = {5, 6, 7, 8 }; //{8,7,6,5 }; //connect to the row pinouts of the keypad
byte colPins[COLS] = {9, 10, 11, 12}; // {12,11,10,9}; //connect to the column pinouts of the keypad

// Now setup the hardware
LiquidCrystal_I2C lcd(I2C_ADDR, 20, 4); // Setup LCD
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

static uint8_t prevNextCode = 0; // statics for rotary encoder
static uint16_t store = 0;

int buttonState = 0;
int re_absolute = 0;
char key ;

// Array set for 4 Locos maximum.  Needs code tidyup to make truly flexible.
int maxLocos = 4;// number of loco addresses
int LocoAddress[4] = {1111, 2222, 3333, 4444};
int LocoDirection[4] = {1, 1, 1, 1};
int LocoSpeed[4] = {0, 0, 0, 0};
// Neater way of storing Loco Function Bytes.
// Rows are Locos, Columns are the five sets of function groups
// 128 = prefix for functions 0 to 4 (last five bits are functions 04321.)
// 176 = prefix for functions 5 to 8
// 160 = prefix for functions 9 to 12
// 0 & 0 are full bitpatterns for functions 13 to 20 and 21 to 28.
byte LocoFNs[4][5] = {
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
  {128, 176, 160, 0, 0},
};

int ActiveAddress = 0; // make address1 active

int i = 0;
char VersionNum[] = "1.03"; ///////////////////////// //////////////////////VERSION HERE///////

void setup() {

  //Setup Encoder Here
  pinMode(RE_CLK, INPUT);
  pinMode(RE_CLK, INPUT_PULLUP);
  pinMode(RE_DATA, INPUT);
  pinMode(RE_DATA, INPUT_PULLUP);
  lcd.backlight();
  lcd.init();
  lcd.createChar(0, zerodot);
  lcd.createChar(1, onedot);
  lcd.home (); // go home
  Serial.begin (115200);
  getAddresses();  // read loco IDs from eeprom
  lcd.print("Fox DCC++ Throttle");
  lcd.setCursor(0, 1);
  lcd.print("20200803 v");
  for (int i = 0; i < 4; i++) {
    lcd.print(VersionNum[i]);
  }

  Serial.print("20200806 Version ");
  for (int i = 0; i < 4; i++) {
    Serial.print(VersionNum[i]);
  }
  if (debug == 1) Serial.println("");
  Serial.print("<0>");// power off to DCC++ unit
  delay(1500);
  // digitalWrite(ledPin, HIGH);           // Turn the LED on.
  // ledPin_state = digitalRead(ledPin);   // Store initial LED state. HIGH when LED is on.
  //  keypad.addEventListener(keypadEvent); // Add an event listener for this keypad
  lcd.clear();
  InitialiseSpeedsLCD();
  InitialiseFunctionLCD();
  lcd.setCursor(4, ActiveAddress);
  lcd.blink();

}  // END SETUP

void loop() {
  static int8_t re_val;

  // First check the keypad
  key = keypad.getKey();

  if (key) {
    if (debug == 1) {
      Serial.println(" ");
      Serial.println(key);
    }
    switch (key) {
      case '*':
        all2ZeroSpeed();
        InitialiseSpeedsLCD();
        getLocoAddress();
        updateSpeedsLCD();
        key = 0;
        break;
      case '#':
        ActiveAddress++;
        if (ActiveAddress >= maxLocos) ActiveAddress = 0;
        updateSpeedsLCD();
        InitialiseFunctionLCD();
        delay(200);
        key = 0;
        re_absolute = LocoSpeed[ActiveAddress];
        doDCCspeed();
        break;
      case 'D':
        //Do nowt for now - menu goes in here?
        doExtendedFunction();
        updateSpeedsLCD();
        break;
      default:
        // It's 0 - 9 or A - C so perform a loco function
        key = key - 48;
        if (key > 10) key = key - 7;
        doFunction(key);
        break;
    }
  }

  // Read encoder
  if ( re_val = read_rotary() ) {
    re_absolute += re_val;
    re_absolute = constrain(re_absolute, 0, 126);
    LocoSpeed[ActiveAddress] = re_absolute;
    doDCCspeed();
    updateSpeedsLCD();
  }

  buttonState = digitalRead(RE_Button);
  // Serial.println(buttonState);
  if (buttonState == LOW) {
    delay(50);
    buttonState = digitalRead(RE_Button); // check a 2nd time to be sure
    if (buttonState == LOW) {// check a 2nd time to be sure
      // Reverse direction...
      LocoDirection[ActiveAddress] = !LocoDirection[ActiveAddress];
      // ... and set speed to zero (saves loco running away on slow decel/accel set in decoder.)
      LocoSpeed[ActiveAddress] = 0;
      re_absolute = 0;
      doDCCspeed();
      updateSpeedsLCD();
      do {  // routine to stay here till button released & not toggle direction
        buttonState = digitalRead(RE_Button);
      }      while (buttonState == LOW);
    }
  }
}  //END LOOP



//START DO FUNCTION BUTTONS


void doExtendedFunction() {
  lcd.setCursor(9 , 0);
  int counter = 0;
  int total = 0;
  do {
    key = keypad.getKey();
    if (key) {
      counter++;
      // Abort if # or *
      if (key < 48) return;
      // otherwise...
      int number =  key - 48;
      // if it 3-9 or A-D, and this is the first key...
      if (number > 2 && counter == 1) {
        if (debug == 1) Serial.print("First Time, 3 to D");
        return;
      }
      // else we can assume it's 0,1,2...
      else if ( counter == 1 ) {
        lcd.setCursor(9 , number + 1);
        total = number * 10;
      }
      else if (counter == 2 && number < 10) {
        // Second time around... and 0-9
        lcd.setCursor(number + 10 , total / 10 + 1);
        total = total + number;
      }
      else if (counter == 2 && number > 9) {
        if (debug == 1) Serial.print("Second Time, A-D");
        return;
      }
    }
  } while (counter <= 1); //  collect exactly 2 digits
  if (debug == 1) Serial.print(total);

  doFunction(total);


}

void doFunction(int FunctoFlip) {
  // Will be passed a number from 0 to 28.
  int FuncSet;
  int FuncLoc;
  if (debug == 1) {
    Serial.print("doFunction - passed:");
    Serial.println(FunctoFlip);
  }
  switch (FunctoFlip)
  {
    case (0) ... (4):
      if (debug == 1) Serial.print("0-4");
      FuncSet = 0;
      FuncLoc = FunctoFlip - 1;
      if (FuncLoc == -1) FuncLoc = 4;
      break;
    case (5) ... (8):
      if (debug == 1) Serial.print("5-8");
      FuncSet = 1;
      FuncLoc = FunctoFlip - 5;
      break;
    case (9) ... (12):
      if (debug == 1) Serial.print("9-12");
      FuncSet = 2;
      FuncLoc = FunctoFlip - 9;
      break;
    case (13) ... (20):
      if (debug == 1) Serial.print("13-20");
      FuncSet = 3;
      FuncLoc = FunctoFlip - 13;
      break;
    case (21) ... (28):
      if (debug == 1) Serial.print("21-28");
      FuncSet = 4;
      FuncLoc = FunctoFlip - 21;
      break;
    case (29):
      if (debug == 1) Serial.print("29");
      // Maybe set all the LocoFNs to zero?
      // Like this!  :)
      LocoFNs[ActiveAddress][0] = 128;
      LocoFNs[ActiveAddress][1] = 176;
      LocoFNs[ActiveAddress][2] = 160;
      LocoFNs[ActiveAddress][3] = 0;
      LocoFNs[ActiveAddress][4] = 0;
      for (int ddf = 0; ddf < 4; ddf++) {
        doDCCfunction(ddf);
      }
      InitialiseFunctionLCD();

      return;
      break;
  }
  // What we now have is the number of which bit we'd like to change in that specific function's bitpattern
  // The next command is effectively 2^number, thus giving us a bitpattern of the number...
  FuncLoc = 1 << FuncLoc ;
  // ... which we can then simply XOR onto the existing bitpattern to flip the bit.
  LocoFNs[ActiveAddress][FuncSet] ^= FuncLoc;
  doDCCfunction(FuncSet);
  InitialiseFunctionLCD();
  if (debug == 1) {
    Serial.println("**");
    Serial.print(LocoFNs[ActiveAddress][FuncSet], BIN);
    Serial.print(" - ");
    Serial.println(LocoFNs[ActiveAddress][FuncSet], DEC);
    Serial.println(ActiveAddress);
    Serial.println(FuncSet);
    Serial.println("**");
  }
}

void getLocoAddress() {
  int saveAddress = LocoAddress[ActiveAddress];
  Serial.print("<0>");// power off to tracks
  int total = 0;
  int counter = 0;
  do {
    lcd.setCursor( counter , ActiveAddress);
    key = keypad.getKey();
    if (key == '#' || key == '*' || key == 'A' || key == 'B' || key == 'C' || key == 'D' ) { //abort when either is hit
      //LocoAddress[ActiveAddress] = saveAddress;
      total = saveAddress;
      break;// exits the do...while loop if above buttons pressed - ABORT new address
    }
    if (key) {
      counter++;
      int number =  key - 48;
      total = total * 10 + number;
      if (key == 48 && total == 0) {
        lcd.print(" ");
      } else {
        lcd.print(key);
      }
      if (debug == 1) Serial.print("Counter = ");
      if (debug == 1) Serial.print(counter);
      if (debug == 1) Serial.print("  key = ");
      if (debug == 1) Serial.print(key);
      if (debug == 1) Serial.print("   val = ");
      if (debug == 1) Serial.println(number);
    }
  } while (counter <= 3); //  collect exactly 4 digits
  //  lcd.noBlink();
  // If all zeroes entered, return to original address (DCC++ doesn't handle 0.)
  if (total == 0) total = saveAddress;
  LocoAddress[ActiveAddress] = total;
  if (debug == 1) Serial.print("Actually saving: ");
  if (debug == 1) Serial.println(total);
  saveAddresses();
  Serial.println("<1>");// power back on to tracks
  updateSpeedsLCD();
}


void doDCCspeed() {
  if (debug == 1) Serial.println(LocoDirection[ActiveAddress] );
  Serial.print("<1>");
  Serial.print("<t1 ");
  Serial.print(LocoAddress[ActiveAddress] );//locoID);
  Serial.print(" ");
  Serial.print(LocoSpeed[ActiveAddress] );
  Serial.print(" ");
  Serial.print(LocoDirection[ActiveAddress] );
  Serial.println(">");
}

void doDCCfunction(int FuncSetX) {
  Serial.write("<f ");
  Serial.print(LocoAddress[ActiveAddress] );
  Serial.print(" ");
  switch (FuncSetX) {
    case (0) ... (2):
      // First three function sets are plain single byte commands...
      break;
    case (3):
      // Last two 8-bit sets are prefixed with 222/223.
      Serial.print("222 ");
      break;
    case (4):
      Serial.print("223 ");
      break;
  }
  int fx = LocoFNs[ActiveAddress][FuncSetX];
  Serial.print(fx);
  Serial.print(" >");
}

void all2ZeroSpeed() {
  /* Loads of bugs here.
      A) tempx <= maxLocos meant five commands were sent, the fifth to a random(?) loco.
      B) LocoSpeed and Direction were set to those of loco 1.  Not good practice, although not required to be correct as <0> is sent after.
      As of 4thAugust2020, modified to do what it says it does.
  */
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Set the recorded speeds to zero...
    LocoSpeed[tempx] = 0;
    // ... then transmit the commands too.
    Serial.print("<t1 ");
    Serial.print(LocoAddress[tempx] );//locoID);
    Serial.print(" 0 ");
    Serial.print(LocoDirection[tempx] );
    Serial.write(">");
  }
}

void getAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    LocoAddress[xyz] = EEPROM.read(xyz * 2) * 256;
    LocoAddress[xyz] = LocoAddress[xyz] + EEPROM.read(xyz * 2 + 1);
    if (LocoAddress[xyz] >= 10000) LocoAddress[xyz] = 3;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz + 1);
    }
  }
  if (debug == 1) Serial.println(" ");
  maxLocos = EEPROM.read(20);
  if (debug == 1) Serial.print("EEPROM maxLocos = ");
  if (debug == 1) Serial.println(maxLocos);
  if (maxLocos >= 4) maxLocos = 4;
}

void saveAddresses() {
  int xxx = 0;
  for (int xyz = 0; xyz <= maxLocos - 1; xyz++) {
    xxx = LocoAddress[xyz] / 256;
    if (debug == 1) {
      Serial.println(" ");
      Serial.print("loco = ");
      Serial.print(LocoAddress[xyz]);
      Serial.print("  address# = ");
      Serial.print(xyz);
      Serial.print(" msb ");
      Serial.print(xxx);
      Serial.print(" writing to ");
      Serial.print(xyz * 2);
      Serial.print(" and ");
      Serial.print(xyz * 2 + 1);
    } // Endif
    EEPROM.write(xyz * 2, xxx);
    xxx = LocoAddress[xyz] - (xxx * 256);
    if (debug == 1) {
      Serial.print(" lsb ");
      Serial.print(xxx);
    }
    EEPROM.write(xyz * 2 + 1, xxx);
  }
  EEPROM.write(20, maxLocos);
}

void InitialiseSpeedsLCD() {
  for (int tempx = 0; tempx < maxLocos; tempx++) {
    // Prints LocoID(right justified), direction arrow, and speed(right justified)
    lcd.setCursor(0, tempx);
    String temp = "   " + String(LocoAddress[tempx] , DEC);
    int tlen = temp.length() - 4;
    lcd.print(temp.substring(tlen));
    // ... direction...
    if (LocoDirection[tempx] == 1 ) {
      lcd.print(">");
    }
    else {
      lcd.print("<");
    }
    // ... speed ...
    temp = "  " + String(LocoSpeed[tempx] , DEC);
    tlen = temp.length() - 3;
    lcd.print(temp.substring(tlen));
  }
  // Return cursor to direction arrow for loco under control
  lcd.setCursor(4, ActiveAddress);
}

void InitialiseFunctionLCD() {
  int funcount = 0;
  lcd.setCursor(9, 0);
  lcd.print("F0123456789");
  for (int tempy = 0; tempy < 3; tempy++) {
    lcd.setCursor(9, tempy + 1);
    lcd.print(tempy);
    for (int tempx = 0; tempx < 10; tempx++) {
      lcd.setCursor(tempx + 10, tempy + 1);
      funcount = tempy * 10 + tempx;
      // Funkiness to put function 0 on the fourth bit...
      if (funcount == 0) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], 4)));
      }
      // ...  and F1 through F4 on the zeroth to third bits.
      else if (funcount >= 1 && funcount <= 4) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][0], funcount - 1)));
      }
      else if (funcount >= 5 && funcount <= 8) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][1], funcount - 5)));
      }
      else if (funcount >= 9 && funcount <= 12) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][2], funcount - 9)));
      }
      else if (funcount >= 13 && funcount <= 20) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][3], funcount - 13)));
      }
      else if (funcount >= 21 && funcount <= 28) {
        lcd.write(byte(bitRead(LocoFNs[ActiveAddress][4], funcount - 21)));
      }
      else {
        // 29th location, hint that 29 will turn off all loco functions.
        lcd.print("X");
      }

    }
    lcd.setCursor(4, ActiveAddress);
  }
}

void updateSpeedsLCD() {
  int tempx = ActiveAddress;
  lcd.setCursor(0, tempx);
  String temp = "   " + String(LocoAddress[tempx] , DEC);
  int tlen = temp.length() - 4;
  lcd.print(temp.substring(tlen));
  if (LocoDirection[tempx] == 1 ) {
    lcd.print(">");
  }
  else {
    lcd.print("<");
  }
  temp = "  " + String(LocoSpeed[tempx] , DEC);
  tlen = temp.length() - 3;
  lcd.print(temp.substring(tlen));
  lcd.setCursor(4, ActiveAddress);
}

// Robust Rotary encoder reading
// Copyright John Main - best-microcontroller-projects.com
// A vald CW or  CCW move returns 1, invalid returns 0.
int8_t read_rotary() {
  static int8_t rot_enc_table[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0};

  prevNextCode <<= 2;
  if (digitalRead(RE_DATA)) prevNextCode |= 0x02;
  if (digitalRead(RE_CLK)) prevNextCode |= 0x01;
  prevNextCode &= 0x0f;

  // If valid then store as 16 bit data.
  if  (rot_enc_table[prevNextCode] ) {
    store <<= 4;
    store |= prevNextCode;
    if ((store & 0xff) == 0x2b) return -1;
    if ((store & 0xff) == 0x17) return 1;
  }
  return 0;
}

 

Edited by FoxUnpopuli
Mentioned the button zeroing speed.
Link to post
Share on other sites

  • 4 months later...

Interesting, I had a go at building the pot version of this just yesterday. I had to fiddle with the code to get it to compile and as you say it needs cleaning up but it appears to be basically there though. I haven't had it connected to the base station yet have you? I will have a better look at your code when I get access to a bigger screen.

  • Friendly/supportive 1
Link to post
Share on other sites

I have made the test setup in the picture. Layout under construction so using the time to work out what works for me (DCC++, JMRI, DCC++EX, wireless handheld throttle .....) Code is running on an Adafruit Trinket. Pins 2 and 7 are not available so had to move the keyboard and rotary encoder pins. Loco is a Hornby Morse Collieries (it was free!, but runs really nicely on DC and now DCC). Lais decoder, default address of 3.

 

Initial problem - the direction kept changing randomly. Added a pullup to the pin. The keyboard is about 30 years old but never used. Some keys work nicely (* and #, plus the rotary and button) but others generate multiple characters. Took a while not to have locos 1111, 2222, 3333 and 4444. Maybe some capacitors will help.

 

If I use a handheld throttle, I will probably get a 3.3V LCD module - they are a bit smaller so less chunky case needed.

 

Must add a big thanks to Mark for tidying up Dave Bodnar's code.

 

Cheers

Dave

20210105_082047_resized.jpg

20210105_082117.jpg

  • Thanks 1
Link to post
Share on other sites

Hello David, I have also added a pullup to the direction pin. Have you seen any functions working? I have hooked the throttle up to a DCC++ base with a small motor through a Lais decoder and this appears to work fine but when I connected the throttle to another arduino to monitor the serial output couldn't see any function commands being sent. I'm just off to solder some leds to my decoder test board to be sure.

Edited by Chuntybunt
  • Like 1
Link to post
Share on other sites

So I added a couple of LEDs to my dummy loco attached to decoder function outputs 1 and 2 and it all appears to work fine. Not sure about the higher functions yet as I haven't had a chance to test them and I don't have any bells and whistles locos anyway at the moment. I'm not sure I like the setup of the keypad but that's fairly straight forward to change and I will probably knock up a custom one anyway. But here you have a basic throttle (excluding a case which would probably be the most expensive single part) for under £15.00. there is no accessory control at present, I may look at that in the future but I don't intend having turnout control on DCC.

IMG_20210106_180950484_HDR.jpg

  • Friendly/supportive 1
Link to post
Share on other sites

  • 2 weeks later...

I built a DCC++ throttle, and expanded it awkwardly into a decoder programmer, after losing all patience with JMRI DecoderPro for this. The Bodnar code was the inspiration but I wrote mine from scratch, sending commands to the DCC++ base station over its serial interface.IMG_20210115_005924645.jpg.608561028b9ae175a7e6bac13a093446.jpg

IMG_20210115_010000338.jpg.2b226e3f12c905e7186b0a4dc3f27644.jpg

 

The control panel was an early experiment in CNC engraving, with one of those cheap desktop mills apparently intended for engraved circuit boards. Key lesson was not to expect good results from plywood offcuts… Electronically it's an Arduino Nano with an encoder, some keys, and that 7-segment display all wired directly to it with precarious ribbon cable.

 

I built it to act as a basic controller - turn right to make loco move right, turn left and it goes left. Then I added other modes, experimtally. In one, the encoder became the reverser, and the two function buttons below became "Accelerate" and "Brake". This made shunting an Inglenook layout more fun.

 

I've stripped everything off my layout and will be rewiring it from scratch, with a sensible power supply arrangement that's not the bodge you see in the photo. I plan to build a new controller to be fixed to the edge of the layout, along with separate or swappable throttle units which more accurately reflect a real train's controls e.g. reverser, power lever, brake lever, physical head- and tail-light switches, speedo showing MPH… but that's some way away at the moment!

 

I'd like the layout controller to be able to act like a TOPS terminal. Rather than plainly entering a decoder ID and sending speed instructions to it, you'd define a train (possibly including several locos/motors/decoders) and assign a specific cab to a throttle unit. Config info stored on an SD card then will map the throttle inputs to speed commands to specific decoders. (I'm dimly aware of stock DCC techniques for consisting, involving writing CV values, but I strongly believe the only job a decoder ought to be doing is making a motor turn at a specific speed, and all the fancy stuff should happen in the controller system.)

 

I'll award myself bonus points if I build a throttle unit that's a facsimile of a 1st-gen DMU control desk complete with gear-shift control, that can also be plugged into my computer and used with Diesel Railcar Simulator ;)

Edited by BusDriverMan
  • Like 1
Link to post
Share on other sites

Impressive BDM! Please keep us updated.

I don't seem to have the time or skill to tinker around with the code - my brownie points get used in the shed building the trackbed

  • Thanks 1
Link to post
Share on other sites

  • 4 months later...
Posted (edited)

Sorry I've been off radar for so long.

 

It's great you've tried (and modded) the new code!  Yes, I have had my code up and running connected to a base station using radio.  I used a Mega for my DCC++ station, it will let me do some extra things with the outputs.

 

I do agree the keymapping could do with a bit of work, it's not the most intuitive, but it's still early doors.

 

The intent was to put in a top menu with turnout/signal control in, but it's tricky to get much info on all the points of the layout on such a small screen!  I was thinking 'route selection'...  but then setting it up using the keypad would be even more of a nightmare.  We're getting towards full keyboard use...

 

Well done all, great stuff.  :)

 

Edited by FoxUnpopuli
Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...