Skip to content

Instantly share code, notes, and snippets.

@Venryx
Created December 10, 2020 11:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Venryx/a3fd1e2ca2fe7a4c87c9e4e630c48ec5 to your computer and use it in GitHub Desktop.
Save Venryx/a3fd1e2ca2fe7a4c87c9e4e630c48ec5 to your computer and use it in GitHub Desktop.
GearVR controller connection code, using NodeJS's "noble" bluetooth-stack
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