BLE Treadmill Controller

All,
I'll give a brief overview of my project first, I'm needing a nudge in the right direction.

Project: Assault Air Runner (Manual treadmill) has bluetooth ability and works with Zwift with no issues. I want to connect to bluetooth and run in my own game. I've already created a prototype game, endless runner style. To start out I just need the speed output; but will include distance, calories and maybe pace.

Experience: Strong background in mechanical and 3d software. Complete beginner in programming.

Hardware: ESP32, Arduino and HC05, Leonardo on the way.

Project so far: I'm pretty proud to have gotten the BLE Client example to work and talk with the treadmill. I've gotten the characteristics and made sense of what I wanted, I think. Image is just me trying to make sense of numbers.

Question: I got the BLE client to work but when it reads back it lists 4 characters always with a square in the second position. I've tried to read the GATT paperwork but feel it is over my head. Could you guys point me in the right direction to include the BLE client and to indicate/notify the speed numbers? I want to build on the project as I go. If I can get the correct output, then I can work on pushing that to a keystroke pressed.

Treadmill output:
serviceUUID: a026ee07-0a7d-4ab3-97fa-f1500f9feb8b
CharacteristicUUID: a026e01d-0a7d-4ab3-97fa-f1500f9feb8b
Client Characteristic Configuration (0x2902)

Been doing some more research and found that the value is stored in little endian format by packet. Does anyone know a sketch example were you can grab only a certain packet?

To grab the characters in excel I know I'd use the MID formula like the following example:
A1: 73-58-12-EB-05-F4-01-16-03-DB-07-00-6C-00-31-00
Formula =MID(A1,43,2)
Result Speed = 31

I'm pretty proud to have gotten the BLE Client example to work and talk with the treadmill.

I think you've gotten farther along than most :wink:

Can you please post the code you are using, and indicate the place where it reads and displays the message from the treadmill.

A1: 73-58-12-EB-05-F4-01-16-03-DB-07-00-6C-00-31-00

I'm using the basic ESP32 BLE_Client and just changing the service and characteristic UUID that I got from the NRF Connect app. I see where it calls for the characteristic and writes a string. It's just way over my head to figure out why it only writes 4 characters and where it's pulling it from. Code snippet I think controls the output.

// Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");

    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
    }

Full code below:

/**
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("a026ee07-0a7d-4ab3-97fa-f1500f9feb8b");
// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("a026e01d-0a7d-4ab3-97fa-f1500f9feb8b");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEAdvertisedDevice* myDevice;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");


    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");

    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
    }

    if(pRemoteCharacteristic->canNotify())
      pRemoteCharacteristic->registerForNotify(notifyCallback);

    connected = true;
    return true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    String newValue = "Time since boot: " + String(millis()/1000);
    Serial.println("Setting new characteristic value to \"" + newValue + "\"");
    
    // Set the characteristic's value to be the array of bytes that is actually a string.
    pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }
  
  delay(1000); // Delay a second between loops.
} // End of loop

This should be called every second in loop(). Do you see this print out

String newValue = "Time since boot: " + String(millis()/1000);
Serial.println("Setting new characteristic value to \"" + newValue + "\"");

why it only writes 4 characters

Are you saying that you do not see the complete message from the teadmill( server) which you are expecting printed anywhere in the library example you are running?

A1: 73-58-12-EB-05-F4-01-16-03-DB-07-00-6C-00-31-00

I'm currently away from treadmill but will run another test tonight. The NRF bluetooth app shows the correct string of numbers, but the Arduino serial monitor outputs code below. Code below is an older test.

19:14:14.301 -> Setting new characteristic value to "Time since boot: 48"
19:14:14.345 -> Notify callback for characteristic a026e024-0a7d-4ab3-97fa-f1500f9feb8b of data length 3
19:14:14.391 -> data: ⸮T
19:14:15.366 -> Setting new characteristic value to "Time since boot: 49"
19:14:15.460 -> Notify callback for characteristic a026e024-0a7d-4ab3-97fa-f1500f9feb8b of data length 3
19:14:15.460 -> data: ⸮T

While writing this I got the idea to paste the data in Notepad++, data shown is (If image doesn't show, I attached also) To me this looks like an encoding problem?

I'll have to do a test and see if I can connect NRF Bluetooth log and Arduino serial monitor at same time. I've also got an log with more characteristics that displayed before getting treadmill values. Might try to input the gatt.writeDescriptor

V	10:53:02.469	Enabling notifications for a026e01d-0a7d-4ab3-97fa-f1500f9feb8b
D	10:53:02.469	gatt.setCharacteristicNotification(a026e01d-0a7d-4ab3-97fa-f1500f9feb8b, true)
D	10:53:02.470	gatt.writeDescriptor(00002902-0000-1000-8000-00805f9b34fb, value=0x0100)
I	10:53:02.521	Data written to descr. 00002902-0000-1000-8000-00805f9b34fb, value: (0x) 01-00
A	10:53:02.521	"Notifications enabled" sent
V	10:53:02.525	Notifications enabled for a026e01d-0a7d-4ab3-97fa-f1500f9feb8b
I	10:53:02.791	Notification received from a026e01d-0a7d-4ab3-97fa-f1500f9feb8b, value: (0x) 73-58-12-37-0D-83-02-8A-07-54-19-00-19-02-3F-00
A	10:53:02.791	"(0x) 73-58-12-37-0D-83-02-8A-07-54-19-00-19-02-3F-00" received

code.JPG

code.JPG

I've discovered the correct Characteristic UUID and changed my baud rate in the code and monitor to 9600. The serial monitor states data length of 16 characters but data output is still nonsense.

I think the issue is in the below line of code. I've read up on using the HEX but can't get it to work without errors. In the example it shows serial.print (x, HEX). So I assumed you could take the below code and with another set of brackets add the comma Hex.

Serial.println((char*)pData);
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
}

Good progress.

Serial.println((char*)pData);

In order to print out all the hex values of pData, replace that line with

for (byte i = 0 ; i < length; i++)
  {
    Serial.print(pData[i], HEX);
    if(i < length-1)Serial.print('-');
  }

(deleted)

...or you could simply test for less than sixteen, and print the leading zero.

(deleted)

sparky961:
Sure. What's the advantage?

A hundred bytes or so of flash memory on a 168.

I was thinking about this all night, woke up and saw you genius's had posted. Replaced the pData with Cattledog's code and IT WORKED!!!!!

Notify callback for characteristic a026e01d-0a7d-4ab3-97fa-f1500f9feb8b of data length 16
data: 73-58-12-14-0-C2-1-46-5-10-0-0-1-0-1C-0Setting new characteristic value to "Time since boot: 19"
Notify callback for characteristic a026e01d-0a7d-4ab3-97fa-f1500f9feb8b of data length 16
data: 73-58-12-15-0-C2-1-50-5-10-0-0-1-0-1D-0Setting new characteristic value to "Time since boot: 20"
Notify callback for characteristic a026e01d-0a7d-4ab3-97fa-f1500f9feb8b of data length 16
data: 73-58-12-16-0-C2-1-46-5-10-0-0-1-0-1C-0Setting new characteristic value to "Time since boot: 21"
Notify callback for characteristic a026e01d-0a7d-4ab3-97fa-f1500f9feb8b of data length 16

Now I can build on the project. I'm going to dig deeper and with Sparky's comment, make me think I can grab individual values. Which would be the second to last couplet of hex numbers. Once I get that, hopefully I'll be able to use the Leonardo to output to "W" key. Again Thanks to you guys this project could see an end.

Good result.

think I can grab individual values. Which would be the second to last couplet of hex numbers.

I'm not certain what values you want, but each value has an index. These index numbers for a 16 byte array run from 0 to 15. That is, the indexes are zero based.

The data at these indexes in memory is binary, and the hex is only there for display.

Referring to your original post, if you want distance which is in the two values at index 9 and index 10, and is presented little ended then

distance = pData[10]*256 + pData[9]

or equivalently

distance = pData[10] << 8 + pData[9];

Cattledog - I did not think it would be that easy. I literally only placed[14] as that was the speed identifier I needed. It output with 16 characters of the speed data so I'll limit that to just 2 characters. I can now take that and use it as my character input. 0-31 is walking speed, 31-50 is a jog, and anything greater than 50 is a sprint. My plan now is to connect the esp32 and a Leonardo to pass the keystroke. I hope to get a video of it when it works.

for (byte i = 0 ; i < length; i++)
  {
    Serial.print(pData[14]);
    if(i < length-1)Serial.print('-');
  }

Sparky and TheMemberFormallyKnownAsAwol, you two are way to smart for me! I'm not sure if the code you are posting would help my project or just proving a point. I don't even know where to start inputting it. I appreciate the input though. The more I learn and understand the better I can optimize the code.

pic2230:
Sparky and TheMemberFormallyKnownAsAwol, you two are way to smart for me! I'm not sure if the code you are posting would help my project or just proving a point. I don't even know where to start inputting it. I appreciate the input though. The more I learn and understand the better I can optimize the code.

sp. "too smart"

@sparky961 almost got their example for printing hex values with leading zeroes correct (just a couple of noob boo-boos), but didn't have the experience to test and evaluate it correctly
The code is compiled for a Diecimilia with a ATMEGA168 processor (all I had available to test it on) , but will compile just as well for a 328, as on a Uno.

To test the different codes, simply delete the comments at the start of the lines that begin "#define METHOD" one by one (and reinstate the comment on the previous line!) and compile the code to see the amount of object code created.

You'll see, as commented, that METHOD1 makes a noticeable (waay more than "12 bytes") saving in flash memory

// Baseline (no defines) = 1442 + 185 bytes (~10%)

//#define METHOD1 /* 1568 + 185 bytes (~10%) 
//#define METHOD2 /* 1700 + 185 bytes (~11%) 
//#define METHOD3 /* 1708 + 185 bytes (~11%) 

volatile uint8_t my_byte;

void printLeadingZeroHex (uint8_t myByte)  //the function name is overly-long, to make a point
{
#if defined(METHOD1)
  if(myByte < 16) { // this is different to @sparky961's incorrect attempt
    Serial.print('0'); // this too
  }
  Serial.print(myByte, HEX);
#endif

#if defined(METHOD2)
  Serial.print( (myByte >> 4) & 0x0F, HEX );
  Serial.print( (myByte >> 0) & 0x0F, HEX );
#endif

#if defined(METHOD3)
  Serial.print(myByte >> 4, HEX);
  Serial.print(myByte, HEX);
#endif
}

void setup() 
{
  Serial.begin(9600);
  printLeadingZeroHex (my_byte); 
}

void loop() 
{
}

It output with 16 characters of the speed data

If all you want is the data in pData[14] then there is not need to iterate through the entire array with the for() loop. Showing all the values was really only need to confirm that you had the right characteristic and the data returned to the Arduino was the same as you saw on the nrf tool.

You can extract what you need with the appropriate index.

You can replace this complete print out

for (byte i = 0 ; i < length; i++)
  {
    Serial.print(pData[14]);
    if(i < length-1)Serial.print('-');
  }

With this

Serial.print(" Speed = ");
Serial.println(pData[14]);

The entire discussion about the leading 0 in the HEX printouts of your data is irrelevant to your purposes. You were smart enough to recognize that 02 and 2 were the same value. Once you confirmed that you had the correct returned values, I don't believe that you have any need for hex value displays anywhere in your project.

I'd bet that you think of distances, speeds, calories in base 10 and time in hours/minutes/seconds.

Project Update. I attached an potentiometer to my Leonardo board to mimic the treadmill and used the base potentiometer sketch to add the key pressed commands. Opened my base Unity prototype game and tested. It worked and the character would run forward and backward. Next step is to connect the ESP32 and Leonardo.

Does anyone know a diagram for this. I've searched and searched and can't find one. I know I need to connect the RX/TX, and my understanding is I've got to step the power down to 3.3v for the ESP32. Seems to be no documents to connect ESP and Leonardo.

Code below for future reference.

 * Tutorial page: https://arduinogetstarted.com/tutorials/arduino-potentiometer
 */

float floatMap(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
int buttonState = 0;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  Keyboard.begin();
  
}

// the loop routine runs over and over again forever:
void loop() {
  // read the input on analog pin A0:
  int analogValue = analogRead(A0);
  // Rescale to potentiometer's voltage (from 0V to 5V):
  float voltage = floatMap(analogValue, 0, 1023, 0, 5);

  // print out the value you read:
  Serial.print("Analog: ");
  Serial.print(analogValue);
  delay(1000);
  if (analogValue == 0)
  {Keyboard.releaseAll();
  }
  if (analogValue > 0 && analogValue < 260)
  {Keyboard.press('w');
  }
  else
  Keyboard.release('w');
  if (analogValue > 260)
  {Keyboard.press('s');
  }
  else
  {
  Keyboard.release('s');
  }
}

Next step is to connect the ESP32 and Leonardo.

Do you only need the ESP32 to send data to the Leonardo?

Do you have GPIO17 Tx2 broken out on your ESP32 module?

If so, I believe that you can directly connect the ESP32 Tx2 to the hardware Serial1 Rx on the Leonardo (pin0). The ESP should send 3.3v TTL, and it should be able to be received by the Leonardo. A level shifter on this line might help if the voltage levels are not quite right for the Leonardo.

If you want bidirectional communication you will need the level shifter or voltage divider between the Leonardo Serial1 Tx and the ESP 32 Rx2.

I'm not sure where you are going with all this, and how much time you have for your development. I can certainly understand you wanting to leverage what you currently have working, but there may be better solutions.

The ESP32 S2 can have USB HID output, and there is a library for it.

Feather ESP32 S2

There is also an integrated BLE device with a 32U4 (used by Leonardo) processor in this product by Adafruit in the feather series. This should look like a Leonardo with BLE and you should be able to get it set up with the characteristics to receive from the treadmill.

At some point you may want to start a new thread (or at least retitle this one).

Thread renamed for better clarity. I've got a level converter and wanted to make sure I'm hooking everything up correctly so I don't fry anything. I did a quick sketch of what I think would work, I'd appreciate if someone could look it over. Goal is to only get power from USB to PC connection.