Bits and Droids logo
Mail icon

Euro Truck Simulator 2 Arduino controller

3/30/2024 | by Bits and Droids

A friend of mine is a huge fan of Euro Truck Simulator 2. When his birthday came up, it was a no-brainer that I wanted to create his personal truck macro pad, integrated with his favorite mechanical key switches and a custom touch.

This was my first time incorporating a button matrix into my final design, and I want to focus on this aspect in this article.

The keypad

A keypad/button matrix works like an old pirate treasure map. If we plot 2 lines, we can locate where the treasure is buried.

Middle left button pressedUpper right button pressed

We will outsource most of the work to the Keypad library. This library makes creating a quick working keypad easy with minimal coding. The approach they use is that the buttons act as a floodgate. One side of the button (i.e., the rows) will be made HIGH by our Arduino. So, an electrical signal tries to run through the button but is physically blocked. The other side will be LOW by default. The moment we press the button, the HIGH side will be low, and the low side will be HIGH until we close it again. Our library scans which row is HIGH/LOW and cross-references this with the columns. It creates a visual map, as we did above, only based on LOW-HIGH states.

Limitations

We will use this controller for truck simulators, so there isn't a need to create a macro pad that can press multiple buttons simultaneously. However, if we press certain button combinations, we encounter a limitation of this basic keypad approach.

If we press all 3 buttons on the first row, our logic starts to break. Since all the lines are wide open, there is no clear path to the button we've pressed. This could be avoided with diodes, but I will look at the basic approach for now.

The code

The libraries used are:

Arduino Playground - Keypad Library

GitHub - NicoHood/HID: Bring enhanced HID functions to your Arduino!

Let's start by looking at the full code and then breaking it down. First, we look at the example sketch included with the library.

cpp
/* @file MultiKey.ino || @version 1.0 || @author Mark Stanley || @contact mstanley@technologist.com || || @description || | The latest version, 3.0, of the keypad library supports up to 10 || | active keys all being pressed at the same time. This sketch is an || | example of how you can get multiple key presses from a keypad or || | keyboard. || # */ #include <Keypad.h> const byte ROWS = 4; //four rows const byte COLS = 3; //three columns char keys[ROWS][COLS] = { {'1','2','3'}, {'4','5','6'}, {'7','8','9'}, {'*','0','#'} }; byte rowPins[ROWS] = {5, 4, 3, 2}; //connect to the row pinouts of the kpd byte colPins[COLS] = {8, 7, 6}; //connect to the column pinouts of the kpd Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); unsigned long loopCount; unsigned long startTime; String msg; void setup() { Serial.begin(9600); loopCount = 0; startTime = millis(); msg = ""; } void loop() { loopCount++; if ( (millis()-startTime)>5000 ) { Serial.print("Average loops per second = "); Serial.println(loopCount/5); startTime = millis(); loopCount = 0; } // Fills kpd.key[ ] array with up-to 10 active keys. // Returns true if there are ANY active keys. if (kpd.getKeys()) { for (int i=0; i<LIST_MAX; i++) // Scan the whole key list. { if ( kpd.key[i].stateChanged ) // Only find keys that have changed state. { switch (kpd.key[i].kstate) { // Report active key state : IDLE, PRESSED, HOLD, or RELEASED case PRESSED: msg = " PRESSED."; break; case HOLD: msg = " HOLD."; break; case RELEASED: msg = " RELEASED."; break; case IDLE: msg = " IDLE."; } Serial.print("Key "); Serial.print(kpd.key[i].kchar); Serial.println(msg); } } } } // End loop

The code I made looks like this. It copies almost all the logic of the example sketch and fits in the logic to register a gamepad press.

cpp
#include <HID-Project.h> #include <HID-Settings.h> #include <Keypad.h> #define ENABLE_PULLUPS const byte ROWS = 3; //four rows const byte COLS = 3; //three columns byte keys[ROWS][COLS] = { {1,2,3}, {4,5,6}, {7,8,9}, }; const byte toggle3A = 9; const byte toggle3B = 16; const byte toggle2A = 15; const byte toggle2B = 14; const byte looseButton = 8; byte rowPins[ROWS] = { 4, 3, 2}; //connect to the row pinouts of the kpd byte colPins[COLS] = {5,6,7}; //connect to the column pinouts of the kpd Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); void setup() { pinMode(toggle3A, INPUT_PULLUP); pinMode(toggle3B, INPUT_PULLUP); pinMode(toggle2A, INPUT_PULLUP); pinMode(toggle2B, INPUT_PULLUP); pinMode(looseButton, INPUT_PULLUP); Gamepad.begin(); } void loop() { byte toggle3AState = digitalRead(toggle3A); if(toggle3AState == LOW){ Gamepad.press(10); } else if(toggle3AState == HIGH){ Gamepad.release(10); } byte toggle3BState = digitalRead(toggle3B); if(toggle3BState == LOW){ Gamepad.press(11); } else if(toggle3BState == HIGH){ Gamepad.release(11); } if(digitalRead(toggle2A) == LOW){ Gamepad.press(12); } if(digitalRead(toggle2B) == LOW){ Gamepad.press(13); } if(digitalRead(looseButton) == LOW){ Gamepad.press(16); } else if(digitalRead(looseButton) == HIGH){ Gamepad.release(16); } if(digitalRead(toggle2A) == HIGH){ Gamepad.release(12); } if(digitalRead(toggle2B) == HIGH){ Gamepad.release(13); } Gamepad.write(); // Fills kpd.key[ ] array with up-to 10 active keys. // Returns true if there are ANY active keys. if (kpd.getKeys()) { for (int i=0; i<LIST_MAX; i++) // Scan the whole key list. { if ( kpd.key[i].stateChanged ) // Only find keys that have changed state. { switch (kpd.key[i].kstate) { // Report active key state : IDLE, PRESSED, HOLD, or RELEASED case PRESSED:{ Gamepad.press(kpd.key[i].kchar); Gamepad.write(); break; } case HOLD: break; case RELEASED:{ Gamepad.release(kpd.key[i].kchar); Gamepad.write(); break; } case IDLE: break; } } } } } // End loop

The parts where we need to make the first adjustments are the pins connecting the rows/columns and what each button needs to register. By default, the example links chars to the keypad while we want to send integers. Why integers? because the Gamepad library takes integers to determine which button is pressed.

These buttons represent the default buttons recognized by Windows.

Can be 32 buttons connected to a single device in the Windows device manager.

cpp
//------------EXAMPLE-CODE------------------- //THE CODE IN THE EXAMPLE USES 4 ROWS WhILE WE ONLy NEED 3 const byte ROWS = 4; //four rows const byte COLS = 3; //three columns //WE WANT INTEGERS INSTEAD OF THESE CHARS char keys[ROWS][COLS] = { {'1','2','3'}, {'4','5','6'}, {'7','8','9'}, {'*','0','#'} }; //OUR ROWS/COLUMNS ARE CONNECTED TO DIFFERENT PINS byte rowPins[ROWS] = {5, 4, 3, 2}; //connect to the row pinouts of the kpd byte colPins[COLS] = {8, 7, 6}; //connect to the column pinouts of the kpd //------------OUR-OWN-IMPLEMENTATION------------------- //WE CHANGE THE ROWS TO 3 const byte ROWS = 3; //four rows const byte COLS = 3; //three columns //INTEGERS SMALLER THAN 255 CAN BE STORED AS BYTES TO SAVE SPACE byte keys[ROWS][COLS] = { {1,2,3}, {4,5,6}, {7,8,9}, }; //CHANGE THESE PINS TO THE CORRECT ONES YOU'RE USING byte rowPins[ROWS] = { 4, 3, 2}; //connect to the row pinouts of the kpd byte colPins[COLS] = {5,6,7}; //connect to the column pinouts of the kpd