Skip to content

Trackball Pheripheral

There are multiple interesting parts in this project.

  • It features a combination of swipe and press on the touchscreen (I wasn't able to find any examples that featured this in a single loop)
  • It uses the CH9329, which at the time of writing is a relatively new keyboard emulator with a new module.
  • It uses a 320x240 capacitive screen (ER-TFT032A3-3-4334) from buydisplay.com with the bodmer library (this is a BGR not an RGB screen, this needs to be set properly in usersetup.h).
  • Further it used an ESP32 and SPIFFS was used to store BMP images that were

The project was written in Visual Studio Code with the PlatfromIO plugin and the Arduino framework.

By swiping up/down/left/right, a different set of shortcuts can be selected. The below examples only include left/right/down, but up can easily be added with examples given.

The BMP files are 320x240 24 bit BMP files that were created in MS Paint.

Even though the LCD does not mention it, the backlight is dimmed using PWM (so the below code also shows how to PWM with an ESP32, which is different from PWM on an Arduino)

You can find the CH9329 library here.

trackballpheripheral

trackballpheripheral inside

Final Trackball pheripheral

user_setup.h:

#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   15  // Chip select control pin
#define TFT_DC    2  // Data Command control pin
#define TFT_RST   4  // Reset pin (could connect to RST pin)

Full code:

#include <Arduino.h>
//uses the bodmer TFT_eSPI library to control a Arduino 3.2"TFT LCD Touch Shield w/ILI9341 from buydisplay.com
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Adafruit_FT6206.h>
#include <CH9329_Keyboard.h>
#include <SPIFFS.h>
// =======================================================================
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
The bodmer library requires configuration of pins in User_Setup.h

The bodmer library wasn't able to drive the touchscreen driver, so for that I relied on Adafruit_FT6206

Full code:

/*  
 Make sure all the display driver and pin connections are correct by
 editing the User_Setup.h file in the TFT_eSPI library folder.

 #########################################################################
 ###### DON'T FORGET TO UPDATE THE User_Setup.h FILE IN THE LIBRARY ######
 #########################################################################
 */
// From User_Setup.h:
//#define TFT_MISO 19
//#define TFT_MOSI 23
//#define TFT_SCLK 18
//#define TFT_CS   15  // Chip select control pin
//#define TFT_DC    2  // Data Command control pin
//#define TFT_RST   4  // Reset pin (could connect to RST pin)

// =======================================================================
#include <Arduino.h>
//uses the bodmer TFT_eSPI library to control a Arduino 3.2"TFT LCD Touch Shield w/ILI9341 from buydisplay.com
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Adafruit_FT6206.h>
#include <CH9329_Keyboard.h>
#include <SPIFFS.h>
// =======================================================================
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library

// =======================================================================
// touch screen
// The FT6206 uses hardware I2C pins (SCL/SDA)
Adafruit_FT6206 ts = Adafruit_FT6206();

// This for the debounce of the touch function in the loop.
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 1000;    // the debounce time for the touch functiono

// below are the values to make swiping work
// in the end copilot wrote the code, because I couldn't get the example to work.
#define TOUCH_INTERVAL 100
#define TOUCH_THRESHOLD_X 60
#define TOUCH_THRESHOLD_Y 60
// for handling touched/release events
bool _touchedOld = false;
bool _touched = _touchedOld;
uint32_t _touchTS = 0;
// first and last points touched
TS_Point _touchFirst;
TS_Point _touchLast;
// =======================================================================
// Backlight control input
// Low: OFF High: ON
const int ledChannel = 0; //0 to 15
const int backlightPin = 32;
const int frequency = 5000; //common values are: 1000, 2000, 5000, 8000, 10000 says copilot
const int resolution = 8; //this is the bit depth of the PWM signal says copilot
const int pwmValue = 64; // 0-255, 64 seems to be a good value visually. I tried 50, worked at first, then stopped working after a while.
// =======================================================================
// variables for the menus
// 1 is fusion360, 2 is windows
int menu = 2;
// =======================================================================
uint16_t read16(fs::File &f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(fs::File &f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}

void drawBmp(const char *filename, int16_t x, int16_t y) {

  if ((x >= tft.width()) || (y >= tft.height())) return;

  fs::File bmpFS;

  // Open requested file on SD card
  bmpFS = SPIFFS.open(filename, "r");

  if (!bmpFS)
  {
    Serial.print("File not found");
    return;
  }

  uint32_t seekOffset;
  uint16_t w, h, row, col;
  uint8_t  r, g, b;

  uint32_t startTime = millis();

  if (read16(bmpFS) == 0x4D42)
  {
    read32(bmpFS);
    read32(bmpFS);
    seekOffset = read32(bmpFS);
    read32(bmpFS);
    w = read32(bmpFS);
    h = read32(bmpFS);

    if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0))
    {
      y += h - 1;

      bool oldSwapBytes = tft.getSwapBytes();
      tft.setSwapBytes(true);
      bmpFS.seek(seekOffset);

      uint16_t padding = (4 - ((w * 3) & 3)) & 3;
      uint8_t lineBuffer[w * 3 + padding];

      for (row = 0; row < h; row++) {

        bmpFS.read(lineBuffer, sizeof(lineBuffer));
        uint8_t*  bptr = lineBuffer;
        uint16_t* tptr = (uint16_t*)lineBuffer;
        // Convert 24 to 16 bit colours
        for (uint16_t col = 0; col < w; col++)
        {
          b = *bptr++;
          g = *bptr++;
          r = *bptr++;
          *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
        }

        // Push the pixel row to screen, pushImage will crop the line if needed
        // y is decremented as the BMP image is drawn bottom up
        tft.pushImage(x, y--, w, 1, (uint16_t*)lineBuffer);
      }
      tft.setSwapBytes(oldSwapBytes);
      Serial.print("Loaded in "); Serial.print(millis() - startTime);
      Serial.println(" ms");
    }
    else Serial.println("BMP format not recognized.");
  }
  bmpFS.close();
}
// =======================================================================
// displaying images:
//https://randomnerdtutorials.com/esp32-vs-code-platformio-spiffs/
using namespace fs;
// this works by placing files in a folder called "data" in the root of the project
// then selecting "Upload File System Image" from the PlatformIO menu

// =======================================================================
// function that takes in a digit which corresponds to a button,
// that will then execute a keyboard command with CH9329_Keyboard
void executeKeyCombo(int button, int menu){
  if(menu == 1){
    if(button == 1){
      CH9329_Keyboard.print("a");
    }else if(button == 2){
      CH9329_Keyboard.print("b");
    }else if(button == 3){
      CH9329_Keyboard.print("c");
    }else if(button == 4){
      CH9329_Keyboard.print("d");
    }else if(button == 5){
      CH9329_Keyboard.print("e");
    }else if(button == 6){
      CH9329_Keyboard.print("f");
    }
  }else if(menu == 2){
    if(button == 1){
      Serial.println("executeKeyCombo: windows button 1 keycombo executed");
      CH9329_Keyboard.press(KEY_LEFT_GUI);
      delay(50);
      CH9329_Keyboard.print("8");
      delay(50);
      CH9329_Keyboard.release(KEY_LEFT_GUI);
    }else if(button == 2){
      Serial.println("executeKeyCombo: windows button 2 keycombo executed");
      CH9329_Keyboard.press(KEY_LEFT_GUI);
      delay(50);
      CH9329_Keyboard.print("1");
      delay(50);
      CH9329_Keyboard.release(KEY_LEFT_GUI);
      delay(100);
      //hold down ctrl:
      CH9329_Keyboard.press(KEY_LEFT_CTRL);
      delay(50);
      CH9329_Keyboard.print("t");
      delay(50);
      CH9329_Keyboard.release(KEY_LEFT_CTRL);
      delay(50);
      CH9329_Keyboard.print("reddit.com");
      delay(50);
      CH9329_Keyboard.press(KEY_RETURN);
      delay(50);
      CH9329_Keyboard.release(KEY_RETURN);
    }else if(button == 3){
      Serial.println("executeKeyCombo: windows button 3 keycombo executed");
      CH9329_Keyboard.press(KEY_LEFT_GUI);
      delay(50);
      CH9329_Keyboard.print("9");
      delay(50);
      CH9329_Keyboard.release(KEY_LEFT_GUI);
    }else if(button == 4){
      CH9329_Keyboard.print("n");
    }else if(button == 5){
      CH9329_Keyboard.print("o");
    }else if(button == 6){
      CH9329_Keyboard.print("p");
    }
  }else if(menu == 3){
    if(button == 4){
      Serial.println("executeKeyCombo: civilization button 4 keycombo executed");
      CH9329_Keyboard.press(KEY_RETURN);
      delay(50);
      CH9329_Keyboard.release(KEY_RETURN);
    }else if(button == 5){
      Serial.println("executeKeyCombo: civilization button 5 keycombo executed");
      CH9329_Keyboard.print("h");
    }else if(button == 6){
      Serial.println("executeKeyCombo: windows button 6 keycombo executed");
      CH9329_Keyboard.print("z");
    }
  }
}

// =======================================================================
// function that returns an integer betwee 1 and 6 based on the touch location
// the grid is 3x2

// x 0 to x 100 first row
// x 170 to x 240 second row
// 
// y 320 to 230 column 1
// y 205 to 115 column 2
// y 90 to 0 column 3

int getButton(TS_Point pt){
  int button = 0;
  //if(pt.x < 100){
  //  if(pt.y > 230){
  //    button = 1;
  //  }else if(pt.y > 115 && pt.y < 205){
  //    button = 2;
  //  }else if(pt.y > 0 && pt.y < 90){
  //    button = 3;
  //  }
  //}else if(pt.x > 170){
  //  if(pt.y > 230){
  //    button = 4;
  //  }else if(pt.y > 115 && pt.y < 205){
  //    button = 5;
  //  }else if(pt.y > 0 && pt.y < 90){
  //    button = 6;
  //  }
  //}
  // I'm going to try a closer set of values, to see if that reduces the "misclicks"
  // like this there are no dead zones.
  if(pt.x < 120){
    if(pt.y > 210){
      button = 1;
    }else if(pt.y > 105 && pt.y < 210){
      button = 2;
    }else if(pt.y > 0 && pt.y < 105){
      button = 3;
    }
  }else{
    if(pt.y > 210){
      button = 4;
    }else if(pt.y > 105 && pt.y < 210){
      button = 5;
    }else if(pt.y > 0 && pt.y < 105){
      button = 6;
    }
  }
  Serial.print("getButton: ");
  Serial.print(button);
  Serial.println(" was pressed");
  return button;
}

// =======================================================================
void setup(void) {
// =======================================================================
  // this works
  Serial2.begin(CH9329_DEFAULT_BAUDRATE);
  CH9329_Keyboard.begin(Serial2);
  // CH9329_Keyboard.print("h");

  Serial.begin(9600);
  tft.init();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);

  // =======================================================================
  if (!ts.begin(40)) { 
    Serial.println("Unable to start touchscreen.");
  } 
  else { 
    Serial.println("Touchscreen started."); 
  }
  // =======================================================================
  //delay(5000);
  //delay(5000);
  if(!SPIFFS.begin(true)){
    delay(5000);
    Serial.println("An Error has occurred while mounting SPIFFS");
  }

  // =======================================================================
  // backlight dimming is working:
  ledcSetup(ledChannel, frequency, resolution);
  ledcAttachPin(backlightPin, ledChannel);
  ledcWrite(ledChannel, pwmValue); // 0-255, 64 seems to be a good value visually.

  // =======================================================================
  drawBmp("/windows.bmp", 0, 0);
}

// =======================================================================
void loop() {

  // The trick here is that the TS can be rotated, but the screen is not.
  if (millis() - _touchTS > TOUCH_INTERVAL) {
    _touchTS = millis();
    // process our touch events
    TS_Point pt = ts.getPoint();
    _touched = ts.touched();
    if(_touched){
      _touchLast = pt;
    }
    if (_touched != _touchedOld) {
      if (_touched) {
        _touchFirst = pt;
      } else {
        pt = _touchLast;
        // compute differences
        int32_t dx = pt.x-_touchFirst.x;
        int32_t dy = pt.y-_touchFirst.y;

        if (abs(dx) > TOUCH_THRESHOLD_X) {
          if (dx > 0) {
            Serial.println("DOWN");
            ledcWrite(ledChannel, 0);
            Serial.println("lcd backlight turned off");
          } else {
            Serial.println("UP");
            drawBmp("/civilization.bmp", 0, 0);
            ledcWrite(ledChannel, pwmValue);
            menu = 3;
            Serial.println("menu changed to civilization");
          }
        } else if (abs(dy) > TOUCH_THRESHOLD_Y) {
          if (dy > 0) {
            Serial.println("LEFT");
            drawBmp("/windows.bmp", 0, 0);
            ledcWrite(ledChannel, pwmValue);
            menu = 2;
            Serial.println("menu changed to windows");
          } else {
            Serial.println("RIGHT");
            drawBmp("/fusion360.bmp", 0, 0);
            ledcWrite(ledChannel, pwmValue);
            menu = 1;
            Serial.println("menu changed to fusion360");
          }
        }else{
          if ((millis() - lastDebounceTime) > debounceDelay) {
            lastDebounceTime = millis();
            // working single point touch example
            Serial.println("Touched!");
            // we have some minimum pressure we consider 'valid' (assume this is about "z" value)
            Serial.println(pt.x);
            Serial.println(pt.y);
            int buttonNumber = getButton(pt);
            Serial.println(buttonNumber);
            executeKeyCombo(buttonNumber, menu);
          }
        }
      }
    }
    _touchedOld = _touched;
  }
}