Skip to content

Instantly share code, notes, and snippets.

@vprusa
Last active November 3, 2023 18:16
Show Gist options
  • Save vprusa/d956927704f0bda9de0d4b196341589e to your computer and use it in GitHub Desktop.
Save vprusa/d956927704f0bda9de0d4b196341589e to your computer and use it in GitHub Desktop.
UART terminal (with performance test) sketch for google chrome's Web Bluetooth GATT communication with ESP32 BLE
//https://googlechrome.github.io/samples/web-bluetooth/get-characteristics.html?service=6e400001-b5a3-f393-e0a9-e50e24dcca9e&characteristic_read=6e400002-b5a3-f393-e0a9-e50e24dcca9e
var bluetoothDevice;
var bluetoothDeviceWriteChar;
var bluetoothDeviceNotifyChar;
var timeNow, timeWriteLast, timeNotifyLast;
function str2ab(str) {
var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
function sendValue() {
if(bluetoothDeviceWriteChar) {
var toSend = $("#toSend").val();
//console.log(toSend);
//console.log(bluetoothDeviceWriteChar);
var toSendAB = str2ab(toSend);
bluetoothDeviceWriteChar.writeValue(toSendAB);
}
}
function startPerfTest() {
console.log("startPerfTest");
// TODO send chars/strings and calculate avg wait time
}
function showReceivedValue(value, timeNow, timeDiff) {
//log(value);
$('#content table tr:last').after(
"<tr><td>" + timeNow + "</td><td>" + timeDiff + "</td><td>" + value + "</td></tr>"
);
}
function connect() {
exponentialBackoff(5 /* max retries */, 2 /* seconds delay */,
function toTry() {
time('Connecting to Bluetooth Device... ');
return bluetoothDevice.gatt.connect();
},
function success() {
log('> Bluetooth Device connected.');
},
function fail() {
time('Failed to reconnect.');
});
}
function onDisconnected() {
log('> Bluetooth Device disconnected');
connect();
}
function onReconnectButtonrClick(){
connect();
}
function onButtonClick() {
let serviceUuid = document.querySelector('#service').value;
if (serviceUuid.startsWith('0x')) {
serviceUuid = parseInt(serviceUuid);
}
let name = document.querySelector('#name').value;
let characteristicWriteUuid = document.querySelector('#characteristic-write').value;
if (characteristicWriteUuid.startsWith('0x')) {
characteristicWriteUuid = parseInt(characteristicWriteUuid);
}
let characteristicNotifyUuid = document.querySelector('#characteristic-notify').value;
if (characteristicNotifyUuid.startsWith('0x')) {
characteristicNotifyUuid = parseInt(characteristicNotifyUuid);
}
log('Requesting Bluetooth Device...');
function resolve(){
if (bluetoothDevice) {
return bluetoothDevice;
}
}
let promise = new Promise(
function(resolve, reject) {
if(bluetoothDevice) {
connect();
// TODO fix return values and reconnect ...
console.log(bluetoothDevice);
resolve(bluetoothDevice.gatt);
} else {
resolve(
navigator.bluetooth.requestDevice({ filters: [{services: [serviceUuid], name: [name]}]}).then(device => {
log('Connecting to GATT Server...');
bluetoothDevice = device;
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
return device.gatt.connect();
})
);
}
}
)
promise.then(server => {
log('Getting Service...');
return server.getPrimaryService(serviceUuid);
})
.then(service => {
log('Getting Characteristics...');
if (characteristicWriteUuid) {
// Get all characteristics that match this UUID.
console.log(service);
// this requires both characteristics...
return Promise.all([service.getCharacteristics(characteristicWriteUuid), service.getCharacteristics(characteristicNotifyUuid)]);
}
// Get all characteristics.
return service.getCharacteristics();
})
.then(characteristics => {
log('> Characteristics: ' +
characteristics.map(c => c[0].uuid + " ("+(c[0].properties.write === true ? "WRITE" : (c[0].properties.notify === true ? "NOTIFY":"?"))+")").join('\n' + ' '.repeat(19)));
console.log(characteristics);
bluetoothDeviceWriteChar = characteristics[0][0];
bluetoothDeviceNotifyChar = characteristics[1][0];
//timeNow = window.performance.now();
timeNow = $.now();
console.log("Time timeNow: " + timeNow);
bluetoothDeviceNotifyChar.addEventListener("characteristicvaluechanged",async function(ev){
console.log(ev);
var received = ab2str(ev.currentTarget.value.buffer);
if(received == -1 ) {return;}
console.log(received);
//timeNow = window.performance.now();
timeNow = $.now();
console.log(timeNow);
var timeDiff = timeNow - timeNotifyLast;
console.log("Time diff: " + timeDiff);
console.log(timeDiff);
if(lastTS){
var difference = ev.timeStamp - lastTS;
console.log("Time difference: " + difference);
}
showReceivedValue(received, timeNow, timeDiff);
lastTS = ev.timeStamp;
timeNotifyLast = timeNow;
});
bluetoothDeviceNotifyChar.startNotifications();
})
.catch(error => {
log('Argh! ' + error);
});
}
var lastTS;
/* Utils */
// This function keeps calling "toTry" until promise resolves or has
// retried "max" number of times. First retry has a delay of "delay" seconds.
// "success" is called upon success.
function exponentialBackoff(max, delay, toTry, success, fail) {
toTry().then(result => success(result))
.catch(_ => {
if (max === 0) {
return fail();
}
time('Retrying in ' + delay + 's... (' + max + ' tries left)');
setTimeout(function() {
exponentialBackoff(--max, delay * 2, toTry, success, fail);
}, delay * 1000);
});
}
function time(text) {
log('[' + new Date().toJSON().substr(11, 8) + '] ' + text);
}
/*
Video: https://www.youtube.com/watch?v=oCMOYS71NIU
Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleNotify.cpp
Ported to Arduino ESP32 by Evandro Copercini
6e400003-b5a3-f393-e0a9-e50e24dcca9e
Create a BLE server that, once we receive a connection, will send periodic notifications.
The service advertises itself as: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
Has a characteristic of: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E - used for receiving data with "WRITE"
Has a characteristic of: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E - used to send data with "NOTIFY"
6e400001-b5a3-f393-e0a9-e50e24dcca9e
6e400002-b5a3-f393-e0a9-e50e24dcca9e
6e400003-b5a3-f393-e0a9-e50e24dcca9e
The design of creating the BLE server is:
1. Create a BLE Server
2. Create a BLE Service
3. Create a BLE Characteristic on the Service
4. Create a BLE Descriptor on the characteristic
5. Start the service.
6. Start advertising.
In this example rxValue is the data received (only accessible inside that function).
And txValue is the data to be sent, in this example just a byte incremented every second.
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
unsigned long timeNow, timeLastRead, timeLastWrite;
BLEServer *pServer = NULL;
BLECharacteristic * pTxCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;
uint8_t txValue = 0;
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" // UART service UUID
#define CHARACTERISTIC_UUID_RX "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
#define CHARACTERISTIC_UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
#defiine TEST_PERF
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string rxValue = pCharacteristic->getValue();
timeNow = micros();
if (rxValue.length() > 0) {
Serial.print(timeNow);
Serial.print(" ");
unsigned long timeDiff = timeNow-timeLastRead;
Serial.print(timeDiff);
//Serial.println("*********");
Serial.print(" Received: ");
for (int i = 0; i < rxValue.length(); i++)
Serial.print(rxValue[i]);
Serial.println();
//Serial.println("*********");
timeLastRead=timeNow;
}
}
};
void setup() {
Serial.begin(115200);
// Create the BLE Device
BLEDevice::init("UART Service");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
pTxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX,
BLECharacteristic::PROPERTY_NOTIFY
);
pTxCharacteristic->addDescriptor(new BLE2902());
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX,
BLECharacteristic::PROPERTY_WRITE
);
pRxCharacteristic->setCallbacks(new MyCallbacks());
// Start the service
pService->start();
// Start advertising
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");
timeNow = micros();
Serial.print("Current time is: ");
Serial.println(timeNow);
}
uint8_t* string2char(String str){
return reinterpret_cast<uint8_t*>(&str[0]);
}
// counter is here as a prevention from waiting first measurement and when something unexpected happens once per time...
int congestionWaitCounter = -1;
void loop() {
if (deviceConnected) {
if (Serial.available()){
timeNow = micros();
String str = Serial.readString();
#ifdef TEST_PERF
for(int i =0; i<10; i++){
#endif
timeNow = micros();
Serial.print(timeNow);
Serial.print(" ");
unsigned long timeDiff = timeNow-timeLastWrite;
Serial.print(timeDiff);
Serial.print(" wrote: ");
Serial.println(str);
pTxCharacteristic->setValue(string2char(str), str.length());
pTxCharacteristic->notify();
timeLastWrite=timeNow;
#ifdef TEST_PERF
}
#endif
}
}
// disconnecting
if (!deviceConnected && oldDeviceConnected) {
delay(500); // give the bluetooth stack the chance to get things ready
pServer->startAdvertising(); // restart advertising
Serial.println("start advertising");
oldDeviceConnected = deviceConnected;
}
// connecting
if (deviceConnected && !oldDeviceConnected) {
// do stuff here on connecting
Serial.println("connected");
oldDeviceConnected = deviceConnected;
}
}
<!DOCTYPE html>
<html>
<head>
<META charset="UTF-8" />
<title>
BLE GATT UART test
</title>
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<script src="characteristics.js"></script>
<style>
#content table th {
padding-left: 10px ;
}
</style>
</head>
<body>
<form id="connectForm" method="GET">
<input id="name" type="text" autofocus="" placeholder="Bluetooth Name" value="UART Service">
<input id="service" type="text" placeholder="Bluetooth Service" value="6e400001-b5a3-f393-e0a9-e50e24dcca9e">
<input id="characteristic-write" type="text" placeholder="Bluetooth Characteristic" value="6e400002-b5a3-f393-e0a9-e50e24dcca9e">
<input id="characteristic-notify" type="text" placeholder="Bluetooth Characteristic" value="6e400003-b5a3-f393-e0a9-e50e24dcca9e">
<button onclick="onButtonClick()">Get characteristics</button>
<button onclick="onReconnectButtonrClick()">Reconnect</button>
</form>
<form id="terminalInput" method="GET">
<input id="toSend" type="text" placeholder="" value="test">
<button onclick="sendValue()">Send</button>
<button onclick="startPerfTest()">Perf</button>
</form>
<p>This page contains testing for GATT...</p>
<div id="output" class="output">
<div id="content">
<table><tr><th>Time</th><th>Diff</th><th>Msg</th></tr></table>
</div>
<div id="status"></div>
<pre id="log"></pre>
</div>
<script>
[].forEach.call(
document.querySelectorAll('form'),
function (el) {
console.log(el);
el.addEventListener('submit', function(event) {
event.stopPropagation();
event.preventDefault();
});
}
);
</script>
<script>
//log = ChromeSamples.log;
function elemLog(msg){
$("#log").append(msg);
$("#log").append("<br>");
}
log = elemLog
function isWebBluetoothEnabled() {
if (navigator.bluetooth) {
return true;
} else {
//ChromeSamples.setStatus('Web Bluetooth API is not available.\n' +
// 'Please make sure the "Experimental Web Platform features" flag is enabled.');
return false;
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment