Created
December 10, 2020 11:30
-
-
Save Venryx/a3fd1e2ca2fe7a4c87c9e4e630c48ec5 to your computer and use it in GitHub Desktop.
GearVR controller connection code, using NodeJS's "noble" bluetooth-stack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Characteristic, Peripheral, Service} from "@abandonware/noble"; | |
export class ControllerData { | |
accel: number[]; | |
gyro: number[]; | |
magX: number; | |
magY: number; | |
magZ: number; | |
timestamp: number; | |
temperature: number; | |
axisX: number; | |
axisY: number; | |
triggerButton: boolean; | |
homeButton: boolean; | |
backButton: boolean; | |
touchpadButton: boolean; | |
volumeUpButton: boolean; | |
volumeDownButton: boolean; | |
} | |
export class GearVRInput { | |
onDeviceDisconnected?: (ev: Event)=>any; | |
onControllerDataReceived?: (data: ControllerData)=>any; | |
device: Peripheral; | |
service: Service; | |
serviceWrite: Characteristic; | |
serviceNotify: Characteristic; | |
async Connect(device: Peripheral) { | |
console.log("Connecting to GearVR device..."); | |
this.device = device; | |
await device.connectAsync(); | |
//this.service = (await device.discoverServicesAsync([CBIUtils.UUID_CUSTOM_SERVICE]))[0]; | |
//const services = device.services[0]; | |
const services = await device.discoverServicesAsync(); | |
this.service = services.find(a=>a.uuid == CBIUtils.UUID_CUSTOM_SERVICE); | |
const characteristics = await this.service.discoverCharacteristicsAsync(); | |
this.serviceWrite = characteristics.find(a=>a.uuid == CBIUtils.UUID_CUSTOM_SERVICE_WRITE); | |
this.serviceNotify = characteristics.find(a=>a.uuid == CBIUtils.UUID_CUSTOM_SERVICE_NOTIFY); | |
//await this.serviceNotify.notifyAsync(false); | |
//await this.serviceNotify.notifyAsync(true); | |
await this.serviceNotify.subscribeAsync(); | |
//this.serviceNotify.on("notify", state=>this.OnNotificationReceived(state)); | |
//this.serviceNotify.on("read", data=>this.OnNotificationReceived(data)); | |
this.serviceNotify.on("data", data=>this.OnNotificationReceived(data)); | |
console.log("Connected to GearVR device."); | |
} | |
async StartReadingData() { | |
// have to do the SENSOR -> VR -> SENSOR cycle a few times to ensure it runs | |
for (let i = 0; i < 3; i++) { | |
if (i != 0) await new Promise(resolve=>setTimeout(resolve, 500)); | |
await this.RunCommand(CBIUtils.CMD_VR_MODE); | |
await this.RunCommand(CBIUtils.CMD_SENSOR); | |
} | |
console.log("Reading started..."); | |
} | |
async Disconnect() { | |
//await this.serviceNotify.unsubscribeAsync(); | |
await this.device.disconnect(); | |
} | |
OnNotificationReceived(containerBuffer: Buffer) { | |
// the NodeJS Buffer class apparently adds interpretation of raw-data; access (its slice of) the shared array-buffer backing, thus matching web-bluetooth ArrayBuffer | |
//const buffer = containerBuffer.buffer; | |
// Slice (copy) its segment of the underlying ArrayBuffer | |
const buffer = containerBuffer.buffer.slice(containerBuffer.byteOffset, containerBuffer.byteOffset + containerBuffer.byteLength); | |
//console.log("Got notification:", buffer); | |
//console.log("Got notification:", buffer.length, buffer.byteLength); | |
//console.log("Got notification:", buffer.byteLength); | |
//const {buffer} = e.target.value as {buffer: ArrayBuffer}; | |
const eventData = new Uint8Array(buffer); | |
// first data-packet can have byteLength of 2; not sure what this represents, so ignoring | |
if (eventData.byteLength < 4) return; | |
// Max observed value = 315 | |
// (corresponds to touchpad sensitive dimension in mm) | |
const axisX = ( | |
((eventData[54] & 0xF) << 6) + | |
((eventData[55] & 0xFC) >> 2) | |
) & 0x3FF; | |
// Max observed value = 315 | |
const axisY = ( | |
((eventData[55] & 0x3) << 8) + | |
((eventData[56] & 0xFF) >> 0) | |
) & 0x3FF; | |
// com.samsung.android.app.vr.input.service/ui/c.class:L222 | |
//const firstInt32 = new Int32Array(buffer.slice(0, 3))[0]; | |
const firstInt32 = new Int32Array(buffer.slice(0, 4))[0]; | |
//const firstInt32 = new Int32Array([...buffer.slice(0, 3), 0])[0]; | |
//const firstInt32 = new Int32Array(eventData.slice(0, 4))[0]; | |
//const firstInt32 = new Int32Array([...eventData.slice(0, 3), 0])[0]; | |
//const firstInt32 = new Int32Array([0, ...eventData.slice(0, 3)])[0]; | |
//const firstInt32 = new Uint32Array(eventData.slice(0, 4))[0]; | |
const timestamp = ((firstInt32 & 0xFFFFFFFF) / 1000) * CBIUtils.TIMESTAMP_FACTOR; | |
//const timestamp = (firstInt32 / 1000) * CBIUtils.TIMESTAMP_FACTOR; | |
//const timestamp = Date.now() * CBIUtils.TIMESTAMP_FACTOR; // workaround for now; obviously not ideal | |
// com.samsung.android.app.vr.input.service/ui/c.class:L222 | |
const temperature = eventData[57]; | |
const { | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex, | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex, | |
getMagnetometerFloatWithOffsetFromArrayBufferAtIndex, | |
} = CBIUtils; | |
// 3 x accelerometer and gyroscope x,y,z values per data event | |
const accel = [ | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 4, 0), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 6, 0), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 8, 0), | |
/*getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 4, 1), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 6, 1), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 8, 1),*/ | |
/*getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 4, 2), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 6, 2), | |
getAccelerometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 8, 2)*/ | |
].map(v=>v * CBIUtils.ACCEL_FACTOR); | |
const gyro = [ | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 10, 0), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 12, 0), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 14, 0), | |
/*getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 10, 1), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 12, 1), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 14, 1),*/ | |
/*getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 10, 2), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 12, 2), | |
getGyroscopeFloatWithOffsetFromArrayBufferAtIndex(buffer, 14, 2)*/ | |
].map(v=>v * CBIUtils.GYRO_FACTOR); | |
const magX = getMagnetometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 0); | |
const magY = getMagnetometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 2); | |
const magZ = getMagnetometerFloatWithOffsetFromArrayBufferAtIndex(buffer, 4); | |
const triggerButton = Boolean(eventData[58] & (1 << 0)); | |
const homeButton = Boolean(eventData[58] & (1 << 1)); | |
const backButton = Boolean(eventData[58] & (1 << 2)); | |
const touchpadButton = Boolean(eventData[58] & (1 << 3)); | |
const volumeUpButton = Boolean(eventData[58] & (1 << 4)); | |
const volumeDownButton = Boolean(eventData[58] & (1 << 5)); | |
this.onControllerDataReceived?.({ | |
accel, | |
gyro, | |
magX, magY, magZ, | |
timestamp, | |
temperature, | |
axisX, axisY, | |
triggerButton, | |
homeButton, | |
backButton, | |
touchpadButton, | |
volumeUpButton, | |
volumeDownButton, | |
}); | |
} | |
RunCommand(commandValue) { | |
const {getLittleEndianUint8Array, onBluetoothError} = CBIUtils; | |
return this.serviceWrite.writeAsync(Buffer.from(getLittleEndianUint8Array(commandValue)), false).catch(onBluetoothError); | |
} | |
} | |
export class CBIUtils { | |
static onBluetoothError = e=>{ | |
console.warn(`Error: ${e}`); | |
}; | |
static UUID_CUSTOM_SERVICE = "4f63756c-7573-2054-6872-65656d6f7465".replace(/-/g, ""); | |
static UUID_CUSTOM_SERVICE_WRITE = "c8c51726-81bc-483b-a052-f7a14ea3d282".replace(/-/g, ""); | |
static UUID_CUSTOM_SERVICE_NOTIFY = "c8c51726-81bc-483b-a052-f7a14ea3d281".replace(/-/g, ""); | |
static CMD_OFF = "0000"; | |
static CMD_SENSOR = "0100"; | |
static CMD_UNKNOWN_FIRMWARE_UPDATE_FUNC = "0200"; | |
static CMD_CALIBRATE = "0300"; | |
static CMD_KEEP_ALIVE = "0400"; | |
static CMD_UNKNOWN_SETTING = "0500"; | |
static CMD_LPM_ENABLE = "0600"; | |
static CMD_LPM_DISABLE = "0700"; | |
static CMD_VR_MODE = "0800"; | |
static GYRO_FACTOR = 0.0001; // to radians / s | |
static ACCEL_FACTOR = 0.00001; // to g (9.81 m/s**2) | |
static TIMESTAMP_FACTOR = 0.001; // to seconds | |
//static TIMESTAMP_FACTOR = 1; | |
static getAccelerometerFloatWithOffsetFromArrayBufferAtIndex = (arrayBuffer, offset, index)=>{ | |
const arrayOfShort = new Int16Array(arrayBuffer.slice(16 * index + offset, 16 * index + offset + 2)); | |
return (new Float32Array([arrayOfShort[0] * 10000.0 * 9.80665 / 2048.0]))[0]; | |
}; | |
static getGyroscopeFloatWithOffsetFromArrayBufferAtIndex = (arrayBuffer, offset, index)=>{ | |
const arrayOfShort = new Int16Array(arrayBuffer.slice(16 * index + offset, 16 * index + offset + 2)); | |
return (new Float32Array([arrayOfShort[0] * 10000.0 * 0.017453292 / 14.285]))[0]; | |
}; | |
static getMagnetometerFloatWithOffsetFromArrayBufferAtIndex = (arrayBuffer, offset)=>{ | |
const arrayOfShort = new Int16Array(arrayBuffer.slice(32 + offset, 32 + offset + 2)); | |
return (new Float32Array([arrayOfShort[0] * 0.06]))[0]; | |
}; | |
static getLength = (f1, f2, f3)=>Math.sqrt(f1 ** 2 + f2 ** 2 + f3 ** 2); | |
static getLittleEndianUint8Array = hexString=>{ | |
const leAB = new Uint8Array(hexString.length >> 1); | |
for (let i = 0, j = 0; i + 2 <= hexString.length; i += 2, j++) { | |
leAB[j] = parseInt(hexString.substr(i, 2), 16); | |
} | |
return leAB; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment