bt-sensors-plugin-sk
Version:
Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors
247 lines (206 loc) • 9.01 kB
JavaScript
const WT901Sensor = require("./WitMotion/WT901Sensor");
const assert = require('node:assert').strict;
const test = require('node:test');
//const assert = require('node:assert');
class WT901BLE extends WT901Sensor {
static getIDRegex(){
return /^WT901BLE[a-f,A-F,0-9]{2}/
}
set(key, value) {
this.data[key] = value;
}
static TX_RX_SERVICE = "0000ffe5-0000-1000-8000-00805f9a34fb";
static NOTIFY_CHAR_UUID = "0000ffe4-0000-1000-8000-00805f9a34fb";
static WRITE_CHAR_UUID = "0000ffe9-0000-1000-8000-00805f9a34fb";
pollFreq = 0;
// comands and data format
// from https://wit-motion.gitbook.io/witmotion-sdk/ble-5.0-protocol/bluetooth-5.0-communication-protocol
static readMagneticField = 0x3a
static readQuaturions = 0x51
static readTemperature = 0x40 // TODO: implement temp
static readPower = 0x64 // TODO: read internal battery level of the device
static shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
assert(false)
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
assert(false)
return false;
}
}
return true;
}
static test() {
const sensor = new WT901BLE()
sensor.getName=()=>{return "WT901BLE fake"}
sensor.initSchema()
sensor.on("pitch", (t)=>{this.debug(`pitch => ${t}`)})
sensor.on("roll", (t)=>{this.debug(`roll => ${t}`)})
sensor.on("yaw", (t)=>{this.debug(`yaw => ${t}`)})
this.debug("starting tests")
var match = {AccX: -0.015, AccY: -0.054, AccZ: 1.0, AsX: 0.0, AsY: 0.0, AsZ: 0.0, AngX: -2.939, AngY: 1.099, AngZ: 82.639 };
sensor.emitValuesFrom(Buffer.from([0x55, 0x61, 0xe1, 0xff, 0x91, 0xff, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0xfd, 0xc8, 0x00, 0xc4, 0x3a, ]))
this.debug(sensor.data)
this.shallowEqual(sensor.data, match)
match = {'AccX': -0.025, 'AccY': -0.054, 'AccZ': 1.0, 'AsX': 0.0, 'AsY': 0.0, 'AsZ': 0.0, 'AngX': -2.939, 'AngY': 1.099, 'AngZ': 82.639}
sensor.emitValuesFrom(Buffer.from([0x55, 0x71, 0x3a, 0x00, 0xab, 0x0b, 0x48, 0x02, 0x68, 0x0d, 0xe9, 0xfd, 0xc8, 0x00, 0xc4, 0x3a, 0x94, 0x07, 0x00, 0x00, ]))
sensor.emitValuesFrom(Buffer.from([0x55, 0x61, 0xe1, 0xff, 0x91, 0xff, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe7, 0xfd, 0xc6, 0x00, 0xc4, 0x3a, ]))
match = {'AccX': -0.015, 'AccY': -0.054, 'AccZ': 1.0, 'AsX': 0.0, 'AsY': 0.0, 'AsZ': 0.0, 'AngX': -2.95, 'AngY': 1.088, 'AngZ': 82.639, 'HX': 24.892, 'HY': 4.867, 'HZ': 28.6}
this.shallowEqual(sensor.data, match)
sensor.emitValuesFrom(Buffer.from([0x55, 0x71, 0x51, 0x00, 0xef, 0x9f, 0x47, 0x03, 0x46, 0x01, 0x7f, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]))
sensor.emitValuesFrom(Buffer.from([0x55, 0x61, 0xdf, 0xff, 0x91, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0xfd, 0xc4, 0x00, 0xc4, 0x3a, ]))
match = {'AccX': -0.016, 'AccY': -0.054, 'AccZ': 1.0, 'AsX': 0.0, 'AsY': 0.0, 'AsZ': 0.0, 'AngX': -2.955, 'AngY': 1.077, 'AngZ': 82.639, 'HX': 24.892, 'HY': 4.867, 'HZ': 28.6, 'Q0': -0.75052, 'Q1': 0.0256, 'Q2': 0.00995, 'Q3': -0.66019}
this.shallowEqual(sensor.data, match)
this.debug("Tests passed")
}
static ImageFile = "wt901ble.jpg"
static Description = "WT901BLE 9 axis accelerometer"
static Manufacturer = "WitMotion"
emitValuesFrom(buffer){
this.processData(buffer)
}
async initSchema(){
super.initSchema()
this.addDefaultParam("zone")
this.data = {};
this.addDefaultPath("pitch","navigation.attitude.pitch")
this.addDefaultPath("yaw","navigation.attitude.yaw")
this.addDefaultPath("roll","navigation.attitude.roll")
//await this.initGATTConnection(true);
//await this.initGATTNotifications()
}
// Used for getting other data
async getBuffer(command) {
let result = Buffer.alloc(256);
let offset = 0;
let lastPacketTime = Date.now();
// Set up listener first
const responsePromise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
clearTimeout(timer);
if (completionTimer) clearTimeout(completionTimer);
this.rxChar.removeAllListeners("valuechanged");
reject(
new Error(
`Response timed out (+10s) from HSC14F device ${this.getName()}.`
)
);
}, 10000);
let completionTimer = null;
const valChanged = (buffer) => {
// HSC14F responses start with 0xaa followed by command byte
if (offset === 0 && (buffer[0] !== 0xaa || buffer[1] !== command)) {
this.debug(
`Invalid buffer from ${this.getName()}, expected command 0x${command.toString(
16
)}, got 0x${buffer[0].toString(16)} 0x${buffer[1].toString(16)}`
);
return;
}
buffer.copy(result, offset);
offset += buffer.length;
lastPacketTime = Date.now();
// Clear any existing completion timer
if (completionTimer) clearTimeout(completionTimer);
// Wait 200ms after last packet to consider response complete
// This allows multi-packet responses to assemble properly
completionTimer = setTimeout(() => {
result = Uint8Array.prototype.slice.call(result, 0, offset);
this.rxChar.removeAllListeners("valuechanged");
clearTimeout(timer);
resolve(result);
}, 200);
};
// Set up listener BEFORE sending command
this.rxChar.on("valuechanged", valChanged);
});
// Small delay to ensure listener is attached
await new Promise(r => setTimeout(r, 100));
// Send the command
try {
await this.sendCommand(command);
} catch (err) {
this.rxChar.removeAllListeners("valuechanged");
throw err;
}
// Wait for response
return responsePromise;
}
/** GATT Model ---------------------------------------------------
1. initSchema() that connects to the device and sets up characteristics
2. initGATTConnection() override if needed
3. initGATTNotifications() or initGATTInterval() depending on polling vs notifications
4. emitGATT() method that requests data and parses responses
5. Helper methods like sendReadFunctionRequest() if needed
*/
hasGATT(){
return true
}
usingGATT(){
return true
}
// register device for notifications
async initGATTNotifications(){
this.debug('in initGATTNotifications')
// now process notifications from device
await this.rxChar.startNotifications();
this.rxChar.on("valuechanged", (buffer)=>{
//this.debug(`rx notify: ${buffer}`)
this.processData(buffer)
})
//this.intervalID = setInterval( async ()=>{
// await this.emitGATT()
//}, 1000*(this?.pollFreq??60) )
}
/**
* Setup connection
*/
async initGATTConnection(isReconnecting = false) {
this.debug('in initGATTConnection')
// base class connects to device
await super.initGATTConnection(isReconnecting);
// Set up GATT characteristics
const gattServer = await this.device.gatt();
const txRxService = await gattServer.getPrimaryService(
this.constructor.TX_RX_SERVICE
);
this.rxChar = await txRxService.getCharacteristic(
this.constructor.NOTIFY_CHAR_UUID
);
//TODO: setup write
//this.txChar = await txRxService.getCharacteristic(
// this.constructor.WRITE_CHAR_UUID
//);
return this;
}
async emitGATT(){
this.debug(`in emitGATT, pollfreq: ${pollfreq}`)
//await this.emitGATT2()
}
async emitGATT2(){
// Get quaturinous
try {
await this.getAndEmit(this.readQuaturions)
}
catch (e) {
this.debug(`Failed to emit battery info for ${this.getName()}: ${e}`)
}
// delay for a bit, then send the mag field
setTimeout(async ()=>{
try {await this.getAndEmit(this.readMagneticField)}
catch (e) {
this.debug(`Failed to emit Cell Voltages for ${this.getName()}: ${e}`)
}
}, 1000)
}
async getAndEmit(cmd){
return this.getBuffer(cmd).then((buffer)=>{
this.processData(buffer)
})
}
}
module.exports=WT901BLE