Commit 7001bdda authored by Will Langford's avatar Will Langford
Browse files

init

parent aa2522dd
# bluetoothExperiments
## Experimenting with the RN4871
I made Neil's [simple FTDI board](http://academy.cba.mit.edu/classes/networking_communications/RN4871/hello.RN4871.ftdi.png) described on [this page](http://academy.cba.mit.edu/classes/networking_communications/index.html).
After reflowing the solder a few times, I got it to communicate with the Arduino serial monitor.
The command set is described in detail in [Microchip's User Guide](https://www.mouser.com/pdfdocs/RN4870-1UsersGuide.pdf).
By default the serial baudrate should be set to 115200 and a carriage return is used to delimit commands.
> EXCEPT! The initial `$$$` (which is used to put the module into command mode) must be sent with no carriage return.
So in the serial monitor send the following:
```
$$$
+ // turn echo on
SS,C0 // default service configuration (device info + UART transport...)
SN,myRN4871 // set name
R,1 // reboot
```
Sending the first `$$$` should be followed with a `CMD>` response if all went well. After each command we should receive a `AOK` response if it was successful or an `Err` response if it failed.
With those commands (and the module still plugged into the computer), I could now use the microchip smart sensor BLE app on my iPhone and connect with the module.
This is enough functionality to establish a UART bridge over bluetooth such that commands sent from the serial terminal program can be seen on the phone.
This module also lets you act as other standard bluetooth devices. For example, I got it to show up as a heart-rate monitor service. I found [this page](http://microchipdeveloper.com/ble:rn4870-app-example-updating-server-characteristics) very helpful for getting the bluetooth service functionality working. The following commands enable the heart rate monitor service and turn on three of the service "characteristics": heart-rate measurement, body sensor location, and heart rate control point.
```
CMD> PS,180D
AOK
CMD> PC,2A37,10,05
AOK
CMD> PC,2A38,02,05
AOK
CMD> PC,2A39,08,05
AOK
CMD> R,1
```
The details of these services and their corresponding characteristics and handles are described in the [official bluetooth service documentation](https://www.bluetooth.com/specifications/gatt/services).
I wanted to see if I could read the heart-rate measurements in my browser with Javascript. I first tried [this example](https://github.com/WebBluetoothCG/demos/tree/gh-pages/heart-rate-sensor) but found that my device wasn't showing up for some reason. I think I'm still not configuring something correctly because I also cannot see the device from the system preferences on my Mac or my iPhone. However, with a small modification I was able to get this to work. I simply replaced the `filters:[{services:[ 'heart_rate' ]` parameter of the `requestDevice()` function call with `acceptAllDevices':true,'optionalServices': [0x180D]`. Note that the optionalServices field appears to be required when accepting all devices and that the 0x180D is that of the "heart-rate service" and is what I entered above with `PS,180D`.
Now, I was able to connect to the sensor but I wasn't sending or receiving any data. To do this, I wrote a quick python script to feed random heart-rate values over the serialport:
```python
import serial
import time
import random
alpha = 0.1
value = 120;
# open serial port
ser = serial.Serial('/dev/cu.usbserial-FTF4ZHMI', 115200);
# enter command mode
ser.write('$$$'.encode('ascii'));
while(1):
# exponentially weighted moving average random heart rate
value = round(alpha*(random.random()*100+100) + (1-alpha)*value);
hexstring = hex(value)[2:]; # strip \x
string = "SHW,0072,00"+hexstring+"\r"; # combine into string
# convert to ascii bytes and send over serial
print(string.encode('ascii'));
ser.write(string.encode('ascii'));
# delay
time.sleep(0.25);
```
And the result is this:
![demo.mov](video/demo.mov)
The module also has an onboard [scripting language](http://ww1.microchip.com/downloads/en/DeviceDoc/50002466B.pdf) that looks just powerful enough to be useful for very minimal applications.
experiments with the RN4871
\ No newline at end of file
var canvas = document.querySelector('canvas');
var statusText = document.querySelector('#statusText');
statusText.addEventListener('click', function() {
statusText.textContent = 'Breathe...';
heartRates = [];
heartRateSensor.connect()
.then(() => heartRateSensor.startNotificationsHeartRateMeasurement().then(handleHeartRateMeasurement))
// .catch(error => {
// statusText.textContent = error;
// });
});
function handleHeartRateMeasurement(heartRateMeasurement) {
heartRateMeasurement.addEventListener('characteristicvaluechanged', event => {
var heartRateMeasurement = heartRateSensor.parseHeartRate(event.target.value);
statusText.innerHTML = heartRateMeasurement.heartRate + ' ❤';
heartRates.push(heartRateMeasurement.heartRate);
drawWaves();
});
}
var heartRates = [];
var mode = 'bar';
canvas.addEventListener('click', event => {
mode = mode === 'bar' ? 'line' : 'bar';
drawWaves();
});
function drawWaves() {
requestAnimationFrame(() => {
canvas.width = parseInt(getComputedStyle(canvas).width.slice(0, -2)) * devicePixelRatio;
canvas.height = parseInt(getComputedStyle(canvas).height.slice(0, -2)) * devicePixelRatio;
var context = canvas.getContext('2d');
var margin = 2;
var max = Math.max(0, Math.round(canvas.width / 11));
var offset = Math.max(0, heartRates.length - max);
context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = '#00796B';
if (mode === 'bar') {
for (var i = 0; i < Math.max(heartRates.length, max); i++) {
var barHeight = Math.round(heartRates[i + offset ] * canvas.height / 200);
context.rect(11 * i + margin, canvas.height - barHeight, margin, Math.max(0, barHeight - margin));
context.stroke();
}
} else if (mode === 'line') {
context.beginPath();
context.lineWidth = 6;
context.lineJoin = 'round';
context.shadowBlur = '1';
context.shadowColor = '#333';
context.shadowOffsetY = '1';
for (var i = 0; i < Math.max(heartRates.length, max); i++) {
var lineHeight = Math.round(heartRates[i + offset ] * canvas.height / 200);
if (i === 0) {
context.moveTo(11 * i, canvas.height - lineHeight);
} else {
context.lineTo(11 * i, canvas.height - lineHeight);
}
context.stroke();
}
}
});
}
window.onresize = drawWaves;
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
drawWaves();
}
});
(function() {
'use strict';
class HeartRateSensor {
constructor() {
this.device = null;
this.server = null;
this._characteristics = new Map();
}
connect() {
return navigator.bluetooth.requestDevice({'acceptAllDevices':true,'optionalServices': [0x180D]})
// return navigator.bluetooth.requestDevice({filters:[{services:[ 'heart_rate' ]}]})
.then(device => {
this.device = device;
return device.gatt.connect();
})
.then(server => {
this.server = server;
return Promise.all([
server.getPrimaryService('heart_rate').then(service => {
return Promise.all([
this._cacheCharacteristic(service, 'body_sensor_location'),
this._cacheCharacteristic(service, 'heart_rate_measurement'),
])
})
]);
})
}
/* Heart Rate Service */
getBodySensorLocation() {
return this._readCharacteristicValue('body_sensor_location')
.then(data => {
let sensorLocation = data.getUint8(0);
switch (sensorLocation) {
case 0: return 'Other';
case 1: return 'Chest';
case 2: return 'Wrist';
case 3: return 'Finger';
case 4: return 'Hand';
case 5: return 'Ear Lobe';
case 6: return 'Foot';
default: return 'Unknown';
}
});
}
startNotificationsHeartRateMeasurement() {
return this._startNotifications('heart_rate_measurement');
}
stopNotificationsHeartRateMeasurement() {
return this._stopNotifications('heart_rate_measurement');
}
parseHeartRate(value) {
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
value = value.buffer ? value : new DataView(value);
let flags = value.getUint8(0);
let rate16Bits = flags & 0x1;
let result = {};
let index = 1;
if (rate16Bits) {
result.heartRate = value.getUint16(index, /*littleEndian=*/true);
index += 2;
} else {
result.heartRate = value.getUint8(index);
index += 1;
}
let contactDetected = flags & 0x2;
let contactSensorPresent = flags & 0x4;
if (contactSensorPresent) {
result.contactDetected = !!contactDetected;
}
let energyPresent = flags & 0x8;
if (energyPresent) {
result.energyExpended = value.getUint16(index, /*littleEndian=*/true);
index += 2;
}
let rrIntervalPresent = flags & 0x10;
if (rrIntervalPresent) {
let rrIntervals = [];
for (; index + 1 < value.byteLength; index += 2) {
rrIntervals.push(value.getUint16(index, /*littleEndian=*/true));
}
result.rrIntervals = rrIntervals;
}
return result;
}
/* Utils */
_cacheCharacteristic(service, characteristicUuid) {
return service.getCharacteristic(characteristicUuid)
.then(characteristic => {
this._characteristics.set(characteristicUuid, characteristic);
});
}
_readCharacteristicValue(characteristicUuid) {
let characteristic = this._characteristics.get(characteristicUuid);
return characteristic.readValue()
.then(value => {
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
value = value.buffer ? value : new DataView(value);
return value;
});
}
_writeCharacteristicValue(characteristicUuid, value) {
let characteristic = this._characteristics.get(characteristicUuid);
return characteristic.writeValue(value);
}
_startNotifications(characteristicUuid) {
let characteristic = this._characteristics.get(characteristicUuid);
// Returns characteristic to set up characteristicvaluechanged event
// handlers in the resolved promise.
return characteristic.startNotifications()
.then(() => characteristic);
}
_stopNotifications(characteristicUuid) {
let characteristic = this._characteristics.get(characteristicUuid);
// Returns characteristic to remove characteristicvaluechanged event
// handlers in the resolved promise.
return characteristic.stopNotifications()
.then(() => characteristic);
}
}
window.heartRateSensor = new HeartRateSensor();
})();
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heart Rate Sensor Demo</title>
<meta name="description" content="Monitor a heart rate sensor with a Web Bluetooth app.">
<link rel="icon" sizes="192x192" href="../favicon.png">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="container">
<div id="statusText">GET &#x2764;</div>
<canvas id="waves"></canvas>
</div>
<script src="heartRateSensor.js"></script>
<script src="app.js"></script>
</body>
</html>
#container {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #333333;
font-family: Roboto;
}
#statusText {
position: absolute;
bottom: 50%;
left: 0;
right: 0;
text-align: center;
margin: 0 auto;
color: #fff;
font-size: 38px;
width: 128px;
overflow-x: visible;
cursor: default;
padding: 12px 0;
-webkit-user-select: none;
}
#waves {
width: 100%;
height: 100%;
display: block;
}
import serial
import time
import random
ser = serial.Serial('/dev/cu.usbserial-FTF4ZHMI', 115200);
ser.write('$$$'.encode('ascii'));
alpha = 0.1
value = 120;
while(1):
value = round(alpha*(random.random()*100+100) + (1-alpha)*value);
# print('SHW,0072,00a1\r'.encode('ascii'))
# ser.write('SHW,0072,00a1\r'.encode('ascii'));
hexstring = hex(value)[2:]
string = "SHW,0072,00"+hexstring+"\r"
print(string.encode('ascii'));
ser.write(string.encode('ascii'));
time.sleep(0.25)
\ No newline at end of file
File added
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment