Skip to content

Instantly share code, notes, and snippets.

@standarddeviant
Created December 27, 2021 03:50
Show Gist options
  • Save standarddeviant/f0879b15a7f5ff8bd3795dba802d4ab3 to your computer and use it in GitHub Desktop.
Save standarddeviant/f0879b15a7f5ff8bd3795dba802d4ab3 to your computer and use it in GitHub Desktop.
BLE IMU Tx/Rx
<html>
<head>
<title>BLE IMU Receiver</title>
</head>
<body style="font-family:'Inconsolata', sans-serif;">
<div style="text-align: center;">
<h1>BLE IMU Receiver</h1>
Web-Bluetooth requires user interaction to scan + connect. Click the button below.<br>
<button id="scan-button" style="width:200; height:50;">
<h3>Scan + Connect</h3>
</button>
</div>
<div id="chart_time" style="min-height: 500px; width:100%;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.2.2/echarts.min.js" integrity="sha512-ivdGNkeO+FTZH5ZoVC4gS4ovGSiWc+6v60/hvHkccaMN2BXchfKdvEZtviy5L4xSpF8NPsfS0EVNSGf+EsUdxA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/msgpack5/5.3.2/msgpack5.min.js"></script>
<script>
var data_vecs_time = [ [], [], [], [], [], [] ];
let tmpnow = new Date();
var data_vecs_time_last_update = tmpnow.getTime()/1000.0;
const DATA_TIME_MAX_SECONDS = 30;
const CHART_UPDATE_EVERY_SECONDS = 0.5;
const xyz_colors = ["#1b9e77", "#d95f02", "#7570b3", "#1b9e77", "#d95f02", "#7570b3"]
sensor_chart_time = echarts.init(document.getElementById('chart_time'))
// const tixrange = [0, 1, 2, 3, 4, 5];
// const tylabels = ['X', 'Y', 'Z', 'X', 'Y', 'Z'];
const tixrange = [0, 1, 2];
const tylabels = ['X', 'Y', 'Z'];
let echarts_time_opts = {
grid: [
{left: '15%', top: '7%', width: '70%', height: '22%'},
{left: '15%', top: '40%', width: '70%', height: '22%'},
{left: '15%', bottom: '7%', width: '70%', height: '22%'},
// {left: '7%', top: '7%', width: '38%', height: '22%'},
// {left: '7%', top: '40%', width: '38%', height: '22%'},
// {left: '7%', bottom: '7%', width: '38%', height: '22%'},
// {right: '7%', top: '7%', width: '38%', height: '22%'},
// {right: '7%', top: '40%', width: '38%', height: '22%'},
// {right: '7%', bottom: '7%', width: '38%', height: '22%'},
],
title: [
{left: '20%', top: '0%', text: 'Accel [m/s^2]'},
// {right: '20%', top: '0%', text: 'Gyro [deg/s]' },
],
xAxis: tixrange.map((ix) => {
return {
gridIndex: ix,
animation: false,
boundaryGap: false,
type: 'value',
min: 'dataMin',
max: 'dataMax',
scale: true,
axisLabel: {
show: true,
minInterval: 5,
maxInterval: 10,
rotate: 45,
formatter: ((value) => {
dt = new Date(1000 * value);
var minutes = dt.getMinutes();
var seconds = dt.getSeconds();
var label = String(minutes) + ':';
// console.log(dt);
if(seconds < 10) {
label += '0';
}
label += String(seconds);
// console.log(label);
return label;
})
}
}
}),
yAxis: tixrange.map((ix) => {
return {
gridIndex: ix,
type: 'value',
scale: true,
minInterval: 0.01,
name: tylabels[ix],
nameLocation: 'middle',
nameGap: 30,
}
}),
series: tixrange.map((ix) => {
return {
data: data_vecs_time[ix],
xAxisIndex: ix,
yAxisIndex: ix,
type: 'line',
lineStyle: {
color: xyz_colors[ix]
},
symbol: 'none',
animation: false,
// animationDuration: 100,
hoverAnimation: false
}
})
};
console.log(echarts_time_opts);
sensor_chart_time.setOption(echarts_time_opts)
window.addEventListener('resize', function(){
if(sensor_chart_time != null && sensor_chart_time != undefined){
sensor_chart_time.resize();
}
});
let SVC_UUID = '05C53569-5E1A-4624-9623-2457010A2413'.toLowerCase();
let RX_UUID = '05C53569-5E1B-4624-9623-2457010A2413'.toLowerCase();
let TX_UUID = '05C53569-5E1C-4624-9623-2457010A2413'.toLowerCase();
var msgpack = msgpack5();
var bluetoothDevice;
var notifyCharac;
async function onButtonClick() {
console.log(SVC_UUID)
console.log(RX_UUID)
console.log(TX_UUID)
try {
if (!bluetoothDevice) {
await requestDevice();
}
await connectDeviceAndCacheCharacteristics();
console.log('Waiting for notifications...');
//await batteryLevelCharacteristic.readValue();
} catch(error) {
console.log('Argh! ' + error);
}
}
document.querySelector('#scan-button').addEventListener('click', function() {
//if (isWebBluetoothEnabled()) {
onButtonClick();
//}
});
async function requestDevice() {
console.log('Requesting any Bluetooth Device...');
bluetoothDevice = await navigator.bluetooth.requestDevice({
// filters: [...] <- Prefer filters to save energy & show relevant devices.
acceptAllDevices: true,
optionalServices: [SVC_UUID]
//filters: [ { services: [SVC_UUID] } ]
});
bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
}
async function connectDeviceAndCacheCharacteristics() {
if (bluetoothDevice.gatt.connected && notifyCharac) {
return;
}
console.log('Connecting to GATT Server...');
const server = await bluetoothDevice.gatt.connect();
console.log('Getting Custom Service...');
const service = await server.getPrimaryService(SVC_UUID);
console.log('Getting Battery Level Characteristic...');
notifyCharac = await service.getCharacteristic(TX_UUID);
console.log('Starting Custom Notifications...');
notifyCharac.addEventListener('characteristicvaluechanged', handleNotification);
await notifyCharac.startNotifications();
//document.querySelector('#startNotifications').disabled = false;
//document.querySelector('#stopNotifications').disabled = true;
}
/* This function will be called when `readValue` resolves and
* characteristic value changes since `characteristicvaluechanged` event
* listener has been added. */
function handleNotification(event) {
let now = new Date();
let data = event.target.value;
// console.log(data)
let acc_xyz = msgpack.decode(data);
// let dvecs = data_vecs_time;
// update echarts chart, we manually remove points from view,
// but we have total control
for(ix=0; ix<3; ix++) {
let dvec = data_vecs_time[ix];
dvec.push([now.getTime()/1000.0, acc_xyz[ix]]);
// TODO - find a more efficient way to trim old values from data
while(dvec.length >= 2) {
let time_span = Math.abs(dvec[dvec.length - 1][0] - dvec[0][0])
// console.log(time_span);
if(time_span <= DATA_TIME_MAX_SECONDS) {
break;
}
dvec.shift();
}
//console.log(dvec);
//console.log({time: Math.abs(now - last_update), thresh: ECHARTS_UPDATE_EVERY});
}
now = data_vecs_time[0][data_vecs_time[0].length - 1][0];
// console.log(`now is ${now}`);
// console.log(`data_vecs_time_last_update is ${data_vecs_time_last_update}`);
// console.log(`tdiff = ${Math.abs(now - data_vecs_time_last_update)}`)
if(Math.abs(now - data_vecs_time_last_update) >= CHART_UPDATE_EVERY_SECONDS) {
// console.log('ya?')
let fake_tixrange = [0, 1, 2]
data_vecs_time_last_update = now;
sensor_chart_time.setOption(
{
series : fake_tixrange.map((ix) => {
return {
data: data_vecs_time[ix]
}
})
}
);
}
}
async function onStartNotificationsButtonClick() {
try {
log('Starting Custom Notifications...');
await notifyCharac.startNotifications();
console.log('> Notifications started');
//document.querySelector('#startNotifications').disabled = true;
//document.querySelector('#stopNotifications').disabled = false;
} catch(error) {
console.log('Argh! ' + error);
}
}
async function onStopNotificationsButtonClick() {
try {
log('Stopping Battery Level Notifications...');
await batteryLevelCharacteristic.stopNotifications();
log('> Notifications stopped');
document.querySelector('#startNotifications').disabled = false;
document.querySelector('#stopNotifications').disabled = true;
} catch(error) {
log('Argh! ' + error);
}
}
function onResetButtonClick() {
if (batteryLevelCharacteristic) {
batteryLevelCharacteristic.removeEventListener('characteristicvaluechanged',
handleBatteryLevelChanged);
batteryLevelCharacteristic = null;
}
// Note that it doesn't disconnect device.
bluetoothDevice = null;
log('> Bluetooth Device reset');
}
async function onDisconnected() {
log('> Bluetooth Device disconnected');
try {
await connectDeviceAndCacheCharacteristics()
} catch(error) {
log('Argh! ' + error);
}
}
</script>
</body>
</html>
# NOTE: in main.py , put the following
# from ble_imu_transmitter import BLE_IMU_Transmitter
# ble_imu = BLE_IMU_Transmitter('m5atm')
from machine import Pin, Timer, SoftI2C
from mpu6886 import MPU6886
from neopixel import NeoPixel
import ubluetooth
import umsgpack
def f2u8(x, shift=9.8, scale=9.8*2):
_scale = 255 / scale
o = int( (x+shift) * _scale )
if o > 30:
return 30
elif o < 0:
return 0
return o
def a2u8(xyz):
return f2u8(xyz, shift=9.8, scale=9.8*64)
def a2rgb(a):
x,y,z = a
r,g,b = a2u8(x), a2u8(y), a2u8(z)
return r,g,b
def update_leds_a(a):
x,y,z = a
r,g,b = a2rgb(a)
for ix in range(NUM_LEDS):
np[ix] = (r, g, b)
np.write()
NUM_LEDS = 25
class BLE_IMU_Transmitter():
def __init__(self, name):
self.i2c = SoftI2C(scl=Pin(21), sda=Pin(25))
print(self.i2c)
self.sensor = MPU6886(self.i2c)
print("MPU6886 id: " + hex(self.sensor.whoami))
self.init_neopixel()
self.led_on = False
self.ui_timer = Timer(3)
self.sensor_timer = Timer(2)
self.name = name
self.ble = ubluetooth.BLE()
self.ble.active(True)
self.disconnected()
self.ble.irq(self.ble_irq)
self.register()
self.advertiser()
def init_neopixel(self):
self.np_pin = Pin(27, Pin.OUT)
self.np = NeoPixel(self.np_pin, NUM_LEDS)
for ix in range(NUM_LEDS):
self.np[ix] = (0,0,0)
self.np.write()
def connected(self):
# TODO - dry out connected + disconnected code
self.ui_timer.deinit()
def cn_cb(t):
bval = 10 * int(self.led_on)
for ix in range(NUM_LEDS):
self.np[ix] = (0, 0, bval)
self.np.write()
self.led_on = not self.led_on
self.ui_timer.init(period=1000, mode=Timer.PERIODIC, callback=cn_cb)
self.sensor_timer.deinit()
def sensor_cb(t):
acc_xyz = self.sensor.acceleration
mp_buf_acc_xyz = umsgpack.dumps(acc_xyz)
self.send(mp_buf_acc_xyz)
self.sensor_timer.init(period=200, mode=Timer.PERIODIC, callback=sensor_cb)
def disconnected(self):
# TODO - dry out connected + disconnected code
self.sensor_timer.deinit()
self.ui_timer.deinit()
def dc_cb(t):
rval = 10 * int(self.led_on)
for ix in range(NUM_LEDS):
self.np[ix] = (rval, 0, 0)
self.np.write()
self.led_on = not self.led_on
self.ui_timer.init(period=1000, mode=Timer.PERIODIC, callback=dc_cb)
def ble_irq(self, event, data):
global message
if event == 1: #_IRQ_CENTRAL_CONNECT:
# A central has connected to this peripheral
self.connected()
elif event == 2: #_IRQ_CENTRAL_DISCONNECT:
# A central has disconnected from this peripheral.
self.advertiser()
self.disconnected()
elif event == 3: #_IRQ_GATTS_WRITE:
# A client has written to this characteristic or descriptor.
buffer = self.ble.gatts_read(self.rx)
message = buffer.decode('UTF-8').strip()
print(message)
def register(self):
# Custom Service
SVC_UUID = '05C53569-5E1A-4624-9623-2457010A2413'
RX_UUID = '05C53569-5E1B-4624-9623-2457010A2413'
TX_UUID = '05C53569-5E1C-4624-9623-2457010A2413'
BLE_SVC = ubluetooth.UUID(SVC_UUID)
BLE_RX = (ubluetooth.UUID(RX_UUID), ubluetooth.FLAG_WRITE)
BLE_TX = (ubluetooth.UUID(TX_UUID), ubluetooth.FLAG_NOTIFY)
BLE_PRO = (BLE_SVC, (BLE_TX, BLE_RX,))
SERVICES = (BLE_PRO, )
((self.tx, self.rx,), ) = self.ble.gatts_register_services(SERVICES)
def send(self, data):
self.ble.gatts_notify(0, self.tx, data + '\n')
def advertiser(self):
name = bytes(self.name, 'UTF-8')
adv_data = bytearray('\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name
adv_data += bytearray('\x11\x07')
adv_data += bytearray('\x13\x24\x0A\x01\x57\x24\x23\x96\x24\x46\x1A\x5E\x69\x35\xC5\x05')
self.ble.gap_advertise(100, adv_data)
print(adv_data)
print("\r\n")
# adv_data
# raw: 0x02010209094553503332424C45
# b'\x02\x01\x02\t\tESP32BLE'
#
# 0x02 - General discoverable mode
# 0x01 - AD Type = 0x01
# 0x02 - value = 0x02
# https://jimmywongiot.com/2019/08/13/advertising-payload-format-on-ble/
# https://docs.silabs.com/bluetooth/latest/general/adv-and-scanning/bluetooth-adv-data-basics
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment